Персональный
сайт
Игоря
Сысоева


 
english
обо мне
 
sysoev.ru
 
nginx
 
mod_accel
mod_realip
mod_deflate
программирование
всякая всячина
windows
freebsd
apache
pppd
unix
web
 
 

FreeBSD, KSE и потоки

 

15.12.2002

На данный момент во FreeBSD существует две реализации потоков (threads). Одна из них полностью сделана на пользовательском уровне и идёт в дистрибутиве в библиотеке libc_r. Эта библиотека использует только один процессор и любой блокирующийся системный вызов блокирует исполнение всего процесса. Кроме того, есть системный вызов rfork(2), клонирующий процесс и являющийся близким аналогом вызова clone(2) в Linux'е. В дистрибутиве нет библиотеки на основе rfork(), однако есть порт библиотеки LinuxThreads, в которой вместо clone() используется rfork(). Как и в Linux'е, потоки, созданные с помощью rfork(), по сути являются процессами, и, как следствие, у них есть проблемы с совместимостью со стандартом POSIX pthreads.

Так как оба варианта оставляли и оставляют желать лучшего, в 2000 году появилось предложение сделать потоки с использованием scheduler activations (активации планировщика), однако сама реализация началась в конце 2001 года. Осенью 2002 года появилась первая версия описания API KSE, отличающаяся от раннего предложения. На данный момент KSE находится всё ещё в стадии разработки во FreeBSD 5.0-CURRENT.

Немного о терминологии.
Различают пользовательские потоки (user threads) и ядерные потоки (kernel threads). В этой статье речь идёт только о первых. User threads могут поддерживаться на уровне ядра или нет. Поддержка на уровне ядра может быть в виде N:N (1:1), когда каждому пользовательскому потоку соответствует ядерный поток (как в Windows NT) или подобный ему объект (как LWP в Solaris), или в виде N:M, когда группе пользовательских потоков соответствует несколько ядерных объектов (LWP-unbound потоки в Solaris и реализации с использованием scheduler activations). Кроме того, есть ещё один экзотический вариант, когда поток с точки зрения реализации является процессом, как, например, потоки, созданные вызовами rfork() во FreeBSD, clone() в Linux и sproc() в IRIX. Необходимо заметить, что существование в системе kernel threads отнюдь не означает, что user threads могут ими поддерживаться, например, во FreeBSD 4.x kernel threads есть, но они никак не пересекаются с user threads.

Scheduler activations позволяет избежать ситуации, когда блокирующийся системный вызов блокирует исполнение всего процесса. Если поток делает подобный вызов, например, чтение из файла, то ядро инициирует операцию, сохраняет контекст этого потока и переходит обратно в пользовательский режим, вызывая специальный обработчик (upcall) планировщика пользовательских потоков. Планировщик может передать управление следующему готовому для исполнения потоку. Когда же операция заблокированного потока выполнится, его контекст будет передан планировщику в следующий upcall, и планировщик поставит его в очередь готовых потоков.

Текущее состояние KSE (Kernel Scheduling Entity, корявый перевод — единица планирования ядра) следующее. Как следует из названия, KSE - это тот объект, для которого ядро выделяет процессорное время. Процесс имеет одну и более групп KSE. В группе может быть несколько KSE, но их число ограничено количеством процессоров. Например, на однопроцессорной машине в каждой группе есть только одна KSE. Все KSE в одной группе имеют одинаковый приоритет, однако в пределах одного процесса группы могут иметь разные приоритеты. Кроме того, в одной группе KSE может быть несколько контекстов исполнения или thread'ов.

KSE позволяют реализовать пользовательские потоки как с полной поддержкой на уровне ядра (N:N), так и вида N:M с использованием upcall'ов ядра. В первом случае на каждый поток приходится своя группа, в каждой из которых одна KSE, исполняющая только один контекст (thread). Планировщика пользовательских потоков и upcall'ов нет. Во втором случае пользовательские потоки с одинаковым приоритетом работают в одной группе с одной, двумя или несколькими (если позволяет число процессоров) KSE, исполняющими несколько контекстов. Переключения между контекстами делаются планировщиком пользовательских потоков.

Надо заметить, что в процессе разработки KSE был изменён способ upcall'а. Первоначальный вариант был a la fork(), то есть, функция вызывалась один раз, а возвращалась неоднократно. Однако этот способ очень неэффективен на процессорах с регистровым окном, например, на SPARC'ах и Itanium'е, поэтому upcall стал вызываться как обработчик сигнала. На мой взгляд, этот способ нагляднее.

Насколько я знаю, существует несколько реализаций потоков с использованием scheduler activatons - в Sun Solaris, DEC (теперь уже Compaq) Tru64 UNIX и SGI IRIX. Насчёт двух последних ничего не скажу, а с Solaris'ом приключилась следующая история. В Solaris 2.2 появилась поддержка потоков на уровне ядра. Потоки могли быть одноуровневыми, когда каждому потоку соответствует один LWP (Light-Weight Proccess) и каждый поток всегда выполняется в контексте этого LWP (такие потоки называются LWP-bound), и двухуровневые, когда нескольким потокам соответствует несколько LWP, причём число LWP, как правило, меньше числа потоков (LWP-unbound потоки). В Solaris 2.6 стал использоваться аналог scheduler activations. Однако было замечено, что ряд программ работает лучше, если используются LWP-bound потоки и поэтому в Solaris 8 появилась возможность статически и динамически линковать программы таким образом, что все LWP-unbound потоки становились LWP-bound. А в Solaris 9 все потоки стали LWP-bound.

Интересно, что ещё весной 2002 года разработчики Linux планировали использовать scheduler activations, но осенью планы изменились и потоки решили оставить не только полностью на уровне ядра [PDF], но и, по-видимому, даже не меняя их реализацию в виде процессов. Повышению производительности должны способствовать новый O(1) планировщик процессов и futex'ы. Кроме того, были существенно ускорены процедуры создания и завершения потоков и преодолён предел в 8192 потока на x86 платформе.

Возможно, отказ Sun'а от двухуровневых потоков как-то повлиял на решение разработчиков Linux'а. Может показаться, что опыт Sun'а развенчал идею scheduler activations. Однако LWP-unbound потоки существенно отличаются от того, что предлагает KSE. Дело в том, что если unbound поток блокируется в ядре, то вместе с ним блокируется и LWP, в котором он работает. Процесс берёт следующий LWP из пула свободных LWP процесса и в контексте этого LWP делает upcall. Если же свободных LWP нет, то процессу посылается сигнал SIGWAITING и обработчик этого сигнала создаёт дополнительный LWP. Когда LWP освобождается, он помещается в пул свободных LWP, из которого удаляется по истечении некоторого времени. Таким образом, если характер работы процесса такой, что unbound потоки часто блокируются в ядре, то процессу будет выделено приблизительно столько же LWP, сколько потоков используется в процессе. В результате количество LWP будет практически таким же, как и в случае с bound потоками, и кроме того, ещё добавятся накладные расходы на взаимодействие с планировщиком пользовательского уровня и обслуживанием пула LWP.

Как и KSE, LWP Solaris'а - это единицы планирования процессорного времени, но в случае KSE их число в одной группе практически всегда постоянно и не может быть больше числа процессоров. Если процесс использует только одну группу, то KSE могут обеспечить загрузку всех имеющихся процессоров, не конкурируя друг с другом за процессоры. Таким образом, KSE позволяет достичь тех же целей, что и механизм Windows NT I/O completion ports, но без изменения исходного текста.

Ссылки:

(C) Игорь Сысоев
http://sysoev.ru