Пишу игрушечную ОС (о реализации sleep)

от автора


Очередной пост для блога, посвященного работе над игрушечной ОС. В прошлый раз я писал про необходимость в простеньком драйвере AHCI (SATA). Прежде чем начать двигаться в этом направлении, я решил набросать инфраструктуру драйверов: общий интерфейс драйвера + уточнённый интерфейс драйвера устройства хранения. Формулирование этих интерфейсов выявила проблему, на которую я ранее не обращал внимания — проблему портируемости.

Не зависящий от платформы код (например, большая часть планировщика, вспомогательный код типа kprintf, …) у меня перемешивается с кодом, заточенным только под x86_64 (системные таблицы дескрипторов, APIC, прерывания, …). Хотя ничего не мешало мне сформулировать интерфейс драйвера, жёстко привязанного к x86_64 (в частности, свободно оперировать PCI-адресами), мне стало ясно, что без чёткого отделения кода, специфичного для конкретной платформы, от общего портируемого кода я буду лишь усугублять ситуацию. Итак, я принял решение перебрать всё написанное, отделив общий код (в корне src/) от кода, специфичного для платформы (в src/x86_64/). Этим я и занимался последние две недели.

Опишу вкратце механизм разделения кода на примере планировщика. Интерфейс планировщика src/schedule.h подключает (#include) специальный файл src/x86_64/schedule.inc, содержащий зависящие от платформы static inline функции (как интерфейсные, так и внутренние). Все внутренние символы (не относящиеся к интерфейсным, но не могущие быть static) предваряются префиксом "__". Основная реализация планировщика находится в src/schedule.c, отдельные внутренние функции и код ассемблера в src/x86_64/schedule.c. Таким образом, кода планировщика «распыляется» на две директории. Разумеется, эта сложность лишь для общего случая, тогда как многие модули строятся по упрощённой схеме. Например, для cpu_info (информация о логических процессорах) заголовок раполагается в src/, а реализация — в src/x86_64/. Или полностью платформозависимый APIC целиком помещён в src/x86_64/.

Теперь об обещанной функции sleep. В отличие от мьютекса реализация sleep потребовала некоторой модификации планировщика (пусть и минимальной). В интерфейсную часть добавились функции:

typedef void (*timer_proc)(uint64_t ticks);  uint64_t get_ticks(void); timer_proc get_timer_proc(void); uint64_t get_timer_ticks(void); // is zeroed after triggering void set_timer_proc(timer_proc proc); // called within a timer interrupt void set_timer_ticks(uint64_t ticks); // not thread-safe 

Т.е. теперь планировщик выступает ещё и в роли таймера: хранит счётчик тиков (внутренних прерываний по таймеру), а также вызывает функцию-обработчик, как только число тиков достигнет заданного числа. Рассмотрим реализацию этого механизма.

static inline void handle_timer(int cpu) {   if (cpu == get_bsp_cpu())     ticks++;    if (timer_ticks[cpu] && timer_ticks[cpu] <= ticks) {     uint64_t prev_ticks = timer_ticks[cpu];     timer_ticks[cpu] = 0;     set_outer_spinlock(true);     timer_proc_(prev_ticks);     set_outer_spinlock(false);   } } 

Функция handle_timer вызывается в каждом прерывании по таймеру. Несмотря на то, что счётчик ticks инкрементируется только для bootstrap-процессора, таймер независимо программируется для каждого из логических процессоров. Оборачивание вызова обработчика в set_outer_spinlock нужно для того, чтобы вызов release_spinlock внутри обработчика случайно не выполнил инструкцию STI (не забываем, что мы находимся в контексте прерывания).

Вот теперь, пользуясь этим расширенным функционалом планировщика, мы можем реализовать sleep.

struct sleep_node {   struct sleep_node *next;   thread_id thread;   uint64_t ticks; };  static struct sleep_data {   struct sleep_node *tail;   struct mem_pool pool;   struct spinlock lock; } sleeping[CONFIG_CPUS_MAX];  static void sleep_timer_proc(UNUSED uint64_t ticks) {   struct sleep_data *slp = &sleeping[get_cpu()];   if (slp->tail) {     struct sleep_node *node = slp->tail;     slp->tail = slp->tail->next;     if (slp->tail)       set_timer_ticks(slp->tail->ticks);     resume_thread(node->thread);     free_block(&slp->pool, node);   } }  err_code sleep(uint64_t period) {   struct sleep_data *slp = &sleeping[get_cpu()];   err_code err = ERR_NONE;    acquire_spinlock(&slp->lock, 0);   struct sleep_node *node = alloc_block(&slp->pool);   if (node) {     node->thread = get_thread();     node->ticks = get_ticks() + period / CONFIG_SCHEDULER_TICK_INTERVAL;      if (!slp->tail || slp->tail->ticks > node->ticks) { // is first to wake up       node->next = slp->tail, slp->tail = node;       set_timer_ticks(node->ticks);     }     else {       struct sleep_node *prev = slp->tail;       while (prev->next && prev->next->ticks <= node->ticks)         prev = prev->next;       node->next = prev->next, prev->next = node;     }      pause_this_thread(&slp->lock);   }   else     err = ERR_OUT_OF_MEMORY;   if (err)     release_spinlock(&slp->lock);    return err; } 

Вышеприведённый код нуждается в некоторых пояснениях:

1. Экземпляр структуры sleep_data соответствует логическому процессору. Пул для sleep_node у каждого процессора независимый, поскольку mem_pool не потокобезопасен.

Именно поэтому в коде мьютекса у меня прячется серьёзная ошибка: для всех мьютексов предусмотрен единый пул для mutex_node, а нужно, чтобы каждый мьютекс имел свой собственный пул. Планирую исправить это в ближайшее время.

2. Как видно из кода, при добавлении в список потоки ранжируются по времени пробуждения (в тиках).

3. Функция sleep_timer_proc и есть тот обработчик, который вызывается планировщиком в контексте прерывания по таймеру. Её задача — разбудить нужный поток.

Остальное кажется достаточно прозрачным.

ссылка на оригинал статьи http://habrahabr.ru/post/181870/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *