Персональный | |
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. Немного о терминологии. 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) Игорь Сысоев |