воскресенье, 25 августа 2013 г.

PhantomEx: Корректный выход из потока. Функция завершения потока.

Эта заметка будет совсем короткой, и посвящена она ещё одному аспекту работы с потоками.

Представим себе ситуацию, когда нам нужен поток, выполняющий свои функции, и завершающий свою работу. Попробуем создать такой поток, и посмотрим что у нас получится. Для этого уберем бесконечный цикл из одного из наших потоков

Листинг 97. Поток с конечным временем выполнения (main.c)

/*-----------------------------------------------------------------
//        Поток #1
//---------------------------------------------------------------*/

void task01(void)
{
    char tmp_str[256];

    vs01 = (vscreen_t*) get_vscreen();

    vs01->cur_x = 0;
    vs01->cur_y = start_y;

    dec2dec_str(count01, tmp_str);

    vprint_text(vs01, "I'm kernel thread #1: ");

    vs01->cur_x = 22;

    vprint_text(vs01, tmp_str);
   
    count01++;

    destroy_vscreen(vs01);
}

Теперь пересоберем ядро и загрузим нашу ОС.




Такс, приплыли... Исключение #PF. Чем была вызвана эта ошибка? Отладка по шагам может дать ответ на причину такого поведения системы, но и без неё можно найти ответ на этот вопрос.

Дело в том, что переключившись на этот поток после его создания, процессор выполнил вход в него, вывел на экран значение счетчика, и даже удалил не нужный более виртуальный экран. А вот по завершении он взял из стека хранящееся там произвольное значение и использовал его в качестве адреса возврата. Естественно этот адрес привел процессор в недоступную область памяти. Хотя мог привести и на отображенную страницу, где, скажем, хранились бы некие статические данные, и приняв их за исполняемый код, процессор выплюнул бы нам исключение #UD.

Таким образом необходимо обеспечить корректную передачу управления после завершения функции потока. Куда мы можем передать управление? Например в планировщик. Напишем такую функцию

Листинг 98. Функция завершения потока (scheduler.c)

/*----------------------------------------------------------------
 *        Выход из потока
 *--------------------------------------------------------------*/

void thread_exit(thread_t* thread)
{
    /* Отключаем прерывания */
    asm volatile ("cli");

    /* Удаляем поток из очереди */
    list_remove(&thread->list_item);
    /* Уменьшаем число потоков в процессе, которому он принадлежит */
    thread->process->threads_count--;

    /* Освобождаем память, занятую потоком под стек и собственный описатель */
    kfree(thread->stack);
    kfree(thread);

    /* Грузим в ECX адрес планировщика */
    asm volatile ("mov %0, %%ecx"::"a"(&task_switch));

    /* Включаем прерывания */
    asm volatile ("sti");

    /* Вызываем планировщик */
    asm volatile ("call *%ecx");
}

В общем-то ничего сложного: мы убираем поток из очереди, изменяем число потоков в породившем данный поток процессе, освобождаем память занятую под стек и описатель потока, а затем передаем управление планировщику, который будет работать с очередь потоков дальше.

Теперь модифицируем код функции потока

Листинг 99. Поток с конечным временем выполнения (main.c)

thread_t* thread01;
thread_t* thread02;

/*-----------------------------------------------------------------
//        Поток #1
//---------------------------------------------------------------*/

void task01(void)
{
    char tmp_str[256];

    vs01 = (vscreen_t*) get_vscreen();

    vs01->cur_x = 0;
    vs01->cur_y = start_y;

    dec2dec_str(count01, tmp_str);

    vprint_text(vs01, "I'm kernel thread #1: ");

    vs01->cur_x = 22;

    vprint_text(vs01, tmp_str);
   
    count01++;

    destroy_vscreen(vs01);

    /* Завершаем данный поток */
    thread_exit(thread01);
}

Только надо поместить описание указателей потоков перед описанием их функций, чтобы не нарваться на синтаксическую ошибку. Что мы имеем теперь


Теперь всё в порядке! Поток, отработав однократно, завершается и система продолжает нормальную работу. В нашем распоряжении минимально необходимый набор функций для работы с потоками уровня ядра.