Внедрение пакета потоков пользовательского уровня
Мне поручили в классе создать библиотеку потоков пользовательского уровня на C. Мне было интересно, может ли кто-нибудь дать мне список вещей, которые нужно прочитать, чтобы это сделать. У меня есть хорошая идея, с чего начать, но любые ресурсы на потоках пользовательского уровня и некоторые применимые аспекты языка C, которые могли бы помочь, были бы чрезвычайно ценными.
Я не совсем понимаю, как реализовать планировщик для такого. Предположим, что я хорошо понимаю язык C и некоторые из его более полезных функций библиотеки.
Ответы
Ответ 1
Я сделал это для домашнего задания без написания ассемблера вообще. Механизм переключения нити setjmp
/longjmp
. В этом было выделение памяти для каждого стека потоков, а затем очень тщательное массирование значений в jmp_buff
, поэтому выполнение переходит к следующему стеку потоков.
См. также Russ Coxs довольно читаемый libtask.
Редактировать в ответ на комментарий OP: При принятии решения о переключении потоков есть два основных направления: упреждающее и совместное. В упреждающей модели у вас есть что-то вроде сигнала таймера, который заставляет поток выполнения переходить к центральному потоку диспетчера, который выбирает следующий поток для запуска. В совместной модели потоки "выводятся" друг к другу, либо явно (например, путем вызова функции yield()
, которую вы предоставляете), либо неявно (например, запрашивая блокировку, удерживаемую другим потоком).
Взгляните на API libtask на пример совместной модели, в частности на описание функции taskyield()
. Это явный доход, о котором я упоминал. Существуют также неблокирующие функции ввода-вывода, которые включают неявный доход - текущая "задача" переносится на удержание до тех пор, пока ввод-вывод не завершится, но другие задачи получат возможность запуска.
Ответ 2
Простой кооперативный планировщик может быть выполнен в C с использованием swapcontext, посмотрите на пример в man файле swapcontext здесь, это его вывод:
$ ./a.out
main: swapcontext(&uctx_main, &uctx_func2)
func2: started
func2: swapcontext(&uctx_func2, &uctx_func1)
func1: started
func1: swapcontext(&uctx_func1, &uctx_func2)
func2: returning
func1: returning
main: exiting
Итак, как вы видите, это вполне выполнимо.
Примечание. Если вы меняете контекст внутри обработчика сигнала таймера, у вас есть упреждающий планировщик, но я не уверен, что это безопасно или возможно сделать.
Изменить: я нашел это на странице руководства sigaction, которая предполагает, что можно переключить контекст внутри обработчика сигнала:
Если SA_SIGINFO указан в sa_flags, тогда sa_sigaction (вместо sa_handler) определяет функцию обработки сигналов для signum. Эта функция принимает номер сигнала в качестве первого аргумента, а указатель на siginfo_t в качестве второго аргумента и указатель на ucontext_t (cast to void *) в качестве третьего аргумента.
Ответ 3
Вы можете найти реализацию Apple с открытым исходным кодом. Обратите внимание, что большая часть кода на самом деле является кодом сборки, потому что для него требуются некоторые специализированные вещи, которые вы не можете сделать на C, например, получение обратного адреса кадра стека или переход на произвольный адрес.
Потоки пользователя (также обычно называемые "волокнами" ) обычно используют кооперативную модель; то есть потоки выполняются до тех пор, пока они не решат, что у них было достаточно времени, а затем перейти к другому потоку. Используя очередь приоритетов, вы можете реализовать планировщик, выполняющий задачу, которая выполнялась в течение кратчайшего промежутка времени. (Планировщик отслеживает выполняемые задачи, а работающая задача возвращается, когда она решает, что этого достаточно. Планировщик обновляет время выполнения задачи, а затем дает тот, у которого было наименьшее время выполнения.)