WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 || 3 | 4 |   ...   | 10 |

«; ...»

-- [ Страница 2 ] --

Наиболее существенным отличием между потоками пространства ядра и обычными процессами является то, что потоки в пространстве ядра не имеют адресного про странства (значение указателя mm для них равно NULL). Эти потоки работают только в пространстве ядра, и их контекст не переключается в пространство пользователя.

Тем не менее потоки в пространстве ядра планируются и вытесняются так же, как и обычные процессы.

В ядре Linux потоки пространства ядра выполняют определенные задания, наиболее часто используемые, — это pdflush и ksoftirq. Эти потоки создаются при за грузке системы другими потоками пространства ядра. В действительности поток в пространстве ядра может быть создан только другим потоком, работающим в про странстве ядра. Интерфейс для запуска нового потока в пространстве ядра из уже существующего потока следующий:

int kernel_thread(int (*fn) (void * ), void * arg, unsigned long flags) Новая задача создается с помощью обычного системного вызова clone () с соот ветствующими значениями флагов, указанными в параметре flags. При возврате из системного вызова родительский поток режима ядра завершается и возвращает ука затель на структуру t ask_st ruct порожденного процесса. Порожденный процесс выполняет функцию, адрес которой указан в параметре fn, в качестве аргумента этой функции передается параметр arg. Для указания обычных флагов потоков про странства ядра существует флаг CLONE_KERNEL, который объединяет в себе флаги CLONE_FS, CLONE_FILES и CLONE_SIGHAND, так как большинство потоков простран ства ядра должны указывать эти флаги в параметре flags.

Чаще всего поток пространства ядра продолжает выполнять свою функцию вечно (или, по крайней мере, до перегрузки системы, но когда она произойдет в случае ОС Linux- неизвестно). Функция потока обычно содержит замкнутый цикл, в котором поток пространства ядра по необходимости возобновляет выполнение, исполняет свои обязанности и снова переходит в приостановленное состояние.

В следующих главах более детально будут рассмотрены конкретные примеры по токов пространства ядра.

Завершение процесса Как это ни грустно, но любой процесс в конечном итоге должен завершиться.

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

Обычно уничтожение процесса происходит тогда, когда процесс вызывает си стемный вызов exi t () явно или неявно при выходе из главной функции программы (компилятор языка С помещает вызов функции exi t () после возврата из функции main ()). Процесс также может быть завершен непроизвольно. Это происходит, ког да процесс получает сигнал или возникает исключительная ситуация, которую про Управление процессами цесс не может обработать или проигнорировать. Независимо от того, каким обра зом процесс завершается, основную массу работы выполняет функция doexec (), а именно указанные далее операции.

• Устанавливается флаг PF_EXITING в поле flags структуры task struct.

• Вызывается функция del_timer_sync (), чтобы удалить все таймеры ядра.

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

• Если включена возможность учета системных ресурсов, занятых процессами (BSD process accounting), то вызывается функция acct_process () для записи информации об учете ресурсов, которые использовались процессом.

• Вызывается функция exit_mm() для освобождения структуры mm_struct, занятой процессом. Если эта структура не используется больше ни одним про цессом (другими словами, не является разделяемой), то она освобождается со всем.

• Вызывается функция exit_sem (). Если процесс находится в очереди ожида ния на освобождение семафора подсистемы IPC, то в этой функции процесс удаляется из этой очереди.

• Вызываются функции exit_files (), exit_fs (), exit_namespace () и exit_signals () для уменьшения счетчика ссылок на объекты, которые от вечают файловым дескрипторам, данным по файловой системе, пространству имен и обработчикам сигналов соответственно. Если счетчик ссылок какого либо объекта достигает значения, равного нулю, то соответствующий объект больше не используется никаким процессом и удаляется.

• Устанавливается код завершения задания, который хранится в поле exitcode структуры task struct. Значение этого кода передается как аргумент функ ции exit () или задается тем механизмом ядра, из-за которого процесс завер шается.

• Вызывается функция exi t not i f у (), которая отправляет сигналы родитель скому процессу завершающегося задания и назначает новый родительский про цесс (reparent) для всех порожденных завершающимся заданием процессов, этим процессом становится или какой-либо один поток из группы потоков за вершающегося процесса, или процесс init. Состояние завершающегося про цесса устанавливается в значение TASK_ZOMBIE.

• Вызывается функция schedule () для переключения на новый процесс (см. гла ву 4, "Планирование выполнения процессов"). Поскольку процесс в состоянии TASK_ZOMBIE никогда не планируется на выполнение, этот код является по следним, который выполняется завершающимся процессом.

Исходный код функции do_exit () описан в файле kernel/exit.с.

К этому моменту освобождены все объекты, занятые задачей (если они использу ются только этой задачей). Задача больше не может выполняться (действительно, у нее больше нет адресного пространства, в котором она может выполняться), а кро ме того, состояние задачи — TASK_ZOMBIE. Единственные области памяти, которые теперь занимает процесс, — это стек режима ядра и слябовый объект, соответствен но содержащие структуры thread_inf о и task_struct.

Глава Задание завершено настолько, насколько остается возможность передать необхо димую информацию родительскому процессу.

Удаление дескриптора процесса После возврата из функции do_exit () дескриптор завершенного процесса все еще существует в системе, но процесс находится в состоянии TASK_ZOMBIE и не мо жет выполняться. Как уже рассказывалось выше, это позволяет системе получить информацию о порожденном процессе после его завершения. Следовательно, завер шение процесса и удаление его дескриптора происходят в разные моменты времени.

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

Семейство функций wait () реализовано через единственный (и достаточно сложный) системный вызов wait4 (). Стандартное поведение этой функции — при остановить выполнение вызывающей задачи до тех пор, пока один из ее порожден ных процессов не завершится. При этом возвращается идентификатор PID завершен ного порожденного процесса. В дополнение к этому, в данную функцию передается указатель на область памяти, которая после возврата из функции будет содержать код завершения завершившегося порожденного процесса.

Когда приходит время окончательно освободить дескриптор процесса, вызывает ся функция release_task (), которая выполняет указанные ниже операции.

• Вызывается функция free_uid () для декремента счетчика ссылок на инфор мацию о пользователе процесса. В системе Linux поддерживается кэш с инфор мацией о каждом пользователе, в частности сколько процессов и открытых файлов имеет пользователь. Если счетчик ссылок достигает значения нуль, то пользователь больше не имеет запущенных процессов и открытых файлов, в результате кэш уничтожается.

• Вызывается функция unhash_process () для удаления процесса из хеш-табли цы идентификаторов процессов pidhash и удаления задачи из списка задач.

• Если задача была в состоянии трассировки (ptrace), то родительским для нее снова назначается первоначальный родительский процесс и задача удаляется из списка задач, которые находятся в состоянии трассировки (pirate) данным процессом.

• В конце концов вызывается функция put_task_struct () для освобождения страниц памяти, содержащих стек ядра процесса и структуру thread_infо, a также освобождается слябовый кэш, содержащий структуру task_struct.

На данном этапе дескриптор процесса, а также все ресурсы, которые принадле жали только этому процессу, освобождены.

Дилемма "беспризорного" процесса Если родительский процесс завершается до того, как завершаются вес его потом ки, то должен существовать какой-нибудь механизм назначения нового родительского процесса для порожденных, иначе процессы, у которых нет родительского, навсегда останутся в состоянии зомби, что будет зря расходовать системную память. Решение этой проблемы было указано выше: новым родительским процессом становится или Управление процессами какой-либо один поток из группы потоков завершившегося родительского процес са, или процесс i ni t. При выполнении функции do_exit () вызывается функция notify_parent (), которая в свою очередь вызывает forget_original parent () для осуществления переназначения родительского процесса (reparent), как показано ниже.

struct task_struct *р, *reaper = father

;

struct list_head *list

;

if (father->exit_signal != -1) reaper = prev_thread(reaper)

;

else reaper = child_reaper

;

if (reaper == father) reaper = child_reaper

;

Этот программный код присваивает переменной reaper указатель на другое зада ние в группе потоков данного процесса. Если в этой группе потоков нет другого за дания, то переменной reaper присваивается значение переменной child_reaper, которая содержит указатель на процесс i ni t. Теперь, когда найден подходящий ро дительский процесс, нужно найти все порожденные процессы и установить для них полученное значение родительского процесса, как показано ниже.

list_for_each(list, &father->children) { р = list_entry(list, struct task_struct, sibling)

;

reparent_thread(p, reaper, child_reaper)

;

} list_for_each (list, sfather->ptracechildren) { p = list_entry(list, struct task:_struct, ptrace_list)

;

reparent_thread(p, reaper, child_reaper)

;

} В этом программном коде организован цикл по двум спискам: по списку порож денных процессов child list и по списку порожденных процессов, находящихся в со стоянии трассировки другими процессами ptraced child list. Основная причина, по которой используется именно два списка, достаточно интересна (эта новая особен ность появилась в ядрах серии 2.6). Когда задача находится в состоянии ptrace, для нее временно назначается родительским тот процесс, который осуществляет от ладку (debugging). Когда завершается истинный родительский процесс для такого задания, то для такой дочерней задачи также нужно осуществить переназначение родительского процесса. В ядрах более ранних версий это приводило к необходимо сти организации цикла по всем заданиям системы для поиска порожденных процессов.

Решение проблемы, как было указано выше, — это поддержка отдельного списка для порожденных процессов, которые находятся в состоянии трассировки, что уменыша ет число операций поиска: происходит переход от поиска порожденных процессов по всему списку задач к поиску только по двум спискам с достаточно малым числом элементов.

Когда для процессов переназначение родительского процесса прошло успешно, больше нет риска, что какой-либо процесс навсегда останется в состоянии зомби.

62 Глава Процесс Ini t периодически вызывает функцию wait () для всех своих порожден ных процессов и, соответственно, удаляет все зомби-процессы, назначенные ему.

Резюме В этой главе рассмотрена важная абстракция операционной системы — процесс.

Здесь описаны общие свойства процессов, их назначение, а также представлено сравнение процессов и потоков. Кроме того, описывается, как операционная си стема Linux хранит и представляет информацию, которая относится к процес сам (структуры task_struct и thread_infо), как создаются процессы (вызовы clone () и fork ()), каким образом новые исполняемые образы загружаются в адрес ное пространство (семейство вызовов exec ()), иерархия процессов, каким образом родительский процесс собирает информацию о своих потомках (семейство функций wait ()) и как в конце концов процесс завершается (непроизвольно или с помощью вызова exit ()).

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

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

Управление процессами Планирование выполнения процессов предыдущей главе были рассмотрены процессы— абстракция операционной системы, связанная с активным программным кодом. В этой главе представлен В планировщик процессов — код, который позволяет процессам выполняться.

Планировщик (scheduler) — это компонент ядра, который выбирает из всех про цессов системы тот, который должен выполняться следующим. Таким образом, пла нировщик (или, как еще его называют, планировщик выполнения процессов) можно рассматривать как программный код, распределяющий конечные ресурсы процес сорного времени между теми процессами операционной системы, которые могут выполняться. Планировщик является основой многозадачных (multitasking) операцион ных систем, таких как ОС Linux. Принимая решение о том, какой процесс должен выполняться следующим, планировщик несет ответственность за наилучшее исполь зование ресурсов системы и создает впечатление того, что несколько процессов вы полняются одновременно.

Идея, лежащая в основе планирования выполнения процессов, достаточно про ста. При наличии готовых к выполнению процессов, для того чтобы лучше исполь зовать процессорное время, необходимо, чтобы всегда выполнялся какой-нибудь процесс. Если в системе процессов больше, чем процессоров, то некоторые процес сы будут выполняться не во все моменты времени. Эти процессы готовы к выполне нию (runnable). Исходя из информации о наборе готовых к выполнению процессов, выбор того процесса, который должен выполняться в следующий момент времени, и есть то фундаментальное решение, которое принимает планировщик.

Многозадачные операционные системы— это те, которые могут выполнять по переменно или одновременно несколько процессов. На однопроцессорной машине такие системы создает иллюзию того, что несколько процессов выполняются одно временно. На многопроцессорной машине они позволяют процессам действительно выполняться параллельно на нескольких процессорах. На машинах любого типа эти системы позволяют процессам выполняться в фоновом режиме и не занимать про цессорное время, если нет соответствующей работы. Такие задания, хотя и находят ся в памяти, но не готовы к выполнению. Вместо этого данные процессы используют ядро, чтобы блокироваться до тех пор, пока не произойдет некоторое событие (ввод с клавиатуры, приход данных по сети, наступление некоторого момента времени в будущем и т.д.). Следовательно, ОС Linux может содержать 100 процессов в памяти, но только один из них будет в исполняемом состоянии.

Многозадачные (multitasking) операционные системы бывают двух видов: систе мы с кооперативной (cooperative) многозадачностью и системы с вытесняющей (preemptive, преемптивной) многозадачностью. Операционная система Linux, так же как и большин ство вариантов ОС Unix и других современных операционных систем, обеспечивает вытесняющую многозадачность. В системе с вытесняющей многозадачностью реше ние о том, когда один процесс должен прекратить выполнение, а другой возобно вить его, принимает планировщик. Событие, заключающееся в принудительном за мораживании выполняющегося процесса, называется вытеснением (preemption) этого процесса. Период времени, в течение которого процесс выполняется перед тем, как будет вытеснен, известен заранее. Этот период называется квантом времени (timesiice) процесса. В действительности квант времени соответствует той части процессорно го времени, которая выделяется процессу. С помощью управления величинами кван тов времени процессов планировщик принимает также и глобальное решение о пла нировании работы всей системы. При этом, кроме всего прочего, предотвращается возможность монопольного использования ресурсов всей системы одним процессом.

Как будет показано далее, величины квантов времени в операционной системе Linux рассчитываются динамически, что позволяет получить некоторые интересные пре имущества.

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

;

процесс может монополизировать процессор на большее время, чем это необходимо пользователю

;

"зависший" процесс, который никогда не передает управление системе, потенциально может привести к неработоспособности систе мы. К счастью, большинство операционных систем, разработанных за последнее де сятилетие, предоставляют режим вытесняющей моногозадачности. Наиболее извест ным исключением является операционная система Mac OS версии 9 и более ранних версий. Конечно, операционная система Unix имеет вытесняющую многозадачность с момента своего создания.

При разработке ядер ОС Linux серии 2.5, планировщик ядра был полностью реконструирован. Новый тип планировщика часто называется 0(1)-плаиировщиком (0(1) scheduler) в связи с соответствующим масштабированием времени выполнения алгоритма планирования1. Этот планировщик позволяет преодолеть недостатки предыдущих версий планировщика ядра Linux и обеспечить расширенную функцио нальность, а также более высокие характеристики производительности. В этой главе будут рассмотрены основы работы планировщиков, как эти основы использованы в О(1)-планировщике, а также цели создания 0(1)-планировшика, его устройство, прак тическая реализация, алгоритмы работы и соответствующие системные вызовы.

Обозначение О(1) — это пример обозначения "большого О". Практически, эта запись означает, что планировщик может выполнить все свои действии за постоянное время, независимо от объ ема входных данных. Полное объяснение того, что такое обозначение "большого О", приведено в приложении В, "Сложность алгоритмов".

66 Глава Стратегия планирования Стратегия (policy) планирования— это характеристики поведения планировщи ка, которые определяют, что и когда должно выполняться. Стратегия планирования определяет глобальный характер поведения системы и отвечает за оптимальное ис пользование процессорного времени. Таким образом, это понятие очень важное.

Процессы, ограниченные скоростью ввода-вывода и скоростью процессора Процессы можно классифицировать как те, которые ограничены скоростью ввода-вы вода (I/0-bound), и те, которые ограничены скоростью процессора (processor-bound). К пер вому типу относятся процессы, которые большую часть своего времени выполнения тратят на отправку запросов на ввод-вывод информации и на ожидание ответов на эти запросы. Следовательно, такие процессы часто готовы к выполнению, но могут выполняться только в течение короткого периода времени, так как в конце концов они блокируются в ожидании выполнения ввода-вывода (имеются в виду не только дисковые операции ввода-вывода, но и любой другой тип ввода-вывода информации, как, например, работа с клавиатурой).

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

Указанные классификации не являются взаимно исключающими. Процессы мо гут сочетать в себе оба типа поведения: сервер системы X Windows — это процесс, который одновременно интенсивно загружает процессор и интенсивно выполняет операции ввода-вывода. Некоторые процессы могут быть ограничены скоростью ввода-вывода, но время от времени начинают выполнять интенсивную процессор ную работу. Хороший пример — текстовый процессор, который обычно ожидает на жатия клавиш, но время от времени может сильно загружать процессор, выполняя проверку орфографии.

Стратегия планирования операционной системы должна стремиться к удо влетворению двух несовместных условий: обеспечение высокой скорости реакции процессов (малого времени задержки, low latency) и высокой производительности (throughput). Для удовлетворения этим требованиям часто в планировщиках при меняются сложные алгоритмы определения наиболее подходящего для выполнения процесса, которые дополнительно гарантируют, что все процессы, имеющие более низкий приоритет, также будут выполняться. В Unix-подобных операционных си стемах стратегия планирования направлена на то, чтобы процессы, ограниченные скоростью ввода-вывода, имели больший приоритет. Использование более высокого приоритета для процессов, ограниченных скоростью ввода-вывода, приводит к уве Ппэнировэние выполнения процессов личению скорости реакции процессов, так как интерактивные программы обычно ограничиваются скоростью ввода-вывода. В операционной системе Linux для обе спечения хорошей скорости реакции интерактивных программ применяется опти мизация по времени оклика (обеспечение малого времени задержки), т.е. процессы, ограниченные скоростью ввода-вывода, имеют более высокий приоритет. Как будет видно далее, это реализуется таким образом, чтобы не пренебрегать и процессами, ограниченными скоростью процессора.

Приоритет процесса Наиболее широко распространенным типом алгоритмов планирования является планирование с управлением по приоритетам (priority-based). Идея состоит в том, что бы расположить процессы по порядку в соответствии с их важностью и необходи мостью использования процессорного времени. Процессы с более высоким при оритетом будут выполняться раньше тех, которые имеют более низкий приоритет, в то время как процессы с одинаковым приоритетом планируются на выполнение циклически (по кругу, round-robin), т.е. периодически один за другим. В некоторых операционных системах, включая Linux, процессы с более высоким приоритетом получают также и более длительный квант времени. Процесс, который готов к вы полнению, у которого еще не закончился квант времени и который имеет наиболь ший приоритет, будет выполняться всегда. Как операционная система, так и пользо ватель могут устанавливать значение приоритета процесса и таким образом влиять на работу планировщика системы.

В операционной системе Linux используется планировщик с динамическим управ лением по приоритетам (dynamic priority-based), который основан на такой же идее.

Основной принцип состоит в том, что вначале устанавливается некоторое началь ное значение приоритета, а затем планировщик может динамически уменьшать или увеличивать это значение приоритета для выполнения своих задач. Например, ясно, что процесс, который тратит много времени на выполнение операций ввода-вывода, ограничен скоростью ввода-вывода. В операционной системе Linux такие процессы получают более высокое значение динамического приоритета. С другой стороны, процесс, который постоянно полностью использует свое значение кванта време ни, — это процесс, ограниченный скоростью процессора. Такие процессы получают меньшее значение динамического приоритета.

В ядре Linux используется два различных диапазона приоритетов. Первый — это параметр nice, который может принимать значения в диапазоне от -20 до 19, по умолчанию значение этого параметра равно 0. Большее значение параметра nice со ответствует меньшему значению приоритета — необходимо быть более тактичным к другим процессам системы (пicе— англ. тактичный, хороший). Процессы с меньшим значением параметра nice (большим значением приоритета) выполняются раньше процессов с большим значением niie (меньшим приоритетом). Значение параметра nice позволяет также определить, насколько продолжительный квант времени полу чит процесс. Процесс со значением параметра nice равным -20 получит квант вре мени самой большой длительности, в то время как процесс со значением параметра nice равным 19 получит наименьшее значение кванта времени. Использование пара метра nice — это стандартный способ указания приоритетов процессов для всех Unix подобных операционных систем.

68 Глава Второй диапазон значений приоритетов— это приоритеты реального времени (real-time priority), которые будут рассмотрены ниже. По умолчанию диапазон зна чений этого параметра лежит от 0 до 99. Все процессы реального времени имеют более высокий приоритет по сравнению с обычными процессами. В операционной системе Linux приоритеты реального времени реализованы в соответствии со стан дартом POSIX. В большинстве современных Unix-систем они реализованы по анало гичной схеме.

Квант времени Квант времени (timeslice2) — это численное значение, которое характеризует, как долго может выполняться задание до того момента, пока оно не будет вытес нено. Стратегия планирования должна устанавливать значение кванта времени, ис пользуемое по умолчанию, что является непростой задачей. Слишком большое зна чение кванта времени приведет к ухудшению интерактивной производительности системы— больше не будет впечатления, что процессы выполняются параллельно.

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

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

Планировщик ядра Linux поднимает значение приоритета для интерактивных задач, что позволяет им выполняться более часто. Поэтому в ОС Linux планировщик ис пользует достаточно большое значение кванта времени (рис 4.1). Более того, пла нировщик ядра Linux динамически определяет значение кванта времени процессов в зависимости от их приоритетов. Это позволяет процессам с более высоким при оритетом, которые считаются более важными, выполняться более часто и в течение большего периода времени. Использование динамического определения величины кванта времени и приоритетов позволяет обеспечить большую устойчивость и про изводительность планировщика.

Следует заметить, что процесс не обязательно должен использовать весь свой квант времени за один раз. Например, процесс, у которого квант времени равен 100 мс, не обязательно должен беспрерывно выполняться в течение 100 мс, рискуя потерять всю оставшуюся неистраченную часть кианта времени. Процесс может вы полняться в течение пяти периодов длительностью по 20 мс каждый.

Вместо термина timeslice (квант времени) иногда также используется quantum (квант) или proces sor slice. В ОС Linux применяется термин timeslice.

Планирование выполнения процессов Меньший приоритет или Больший приоритет или Минимум По умолчанию Максимум 5мс 100 мс 800 мс Рис. 4.1. Вычисление кванта времени процесса Таким образом, интерактивные задачи также получают преимущество от исполь зования продолжительного кванта времени, если даже вся продолжительность кван та времени не будет использована сразу, гарантируется, что такие процессы будут готовы к выполнению по возможности долго.

Когда истекает квант времени процесса, считается, что процесс потерял право выполняться. Процесс, у которого нет кванта времени, не имеет права выполнять ся до того момента, пока все другие процессы не используют свой квант времени.

Когда это случится, то у всех процессов будет значение оставшегося кванта времени, равное нулю. В этот момент значения квантов времени для всех процессов пересчи тываются. В планировщике ОС Linux используется интересный алгоритм для обра ботки ситуации, когда все процессы использовали свой квант времени. Этот алго ритм будет рассмотрен далее.

Вытеснение процесса Как уже упоминалось, операционная система Linux использует вытесняющую мно гозадачность. Когда процесс переходит в состояние TASK_RUNNING, ядро проверя ет значение приоритета этого процесса. Если это значение больше, чем приоритет процесса, который выполняется в данный момент, то активизируется планировщик, чтобы запустить новый процесс на выполнение (имеется в виду тот процесс, кото рый только что стал готовым к выполнению). Дополнительно, когда квант времени процесса становится равным нулю, он вытесняется и планировщик готов к выбору нового процесса.

Стратегия планирования в действии Рассмотрим систему с двумя готовыми к выполнению заданиями: программой для редактирования текстов и видеокодером. Программа для редактирования текстов ограничена скоростью ввода-вывода, потому что она тратит почти все свое время на ожидание ввода символов с клавиатуры пользователем (не имеет значение, с какой скоростью пользователь печатает, это не те скорости). Несмотря ни на что, при на жатии клавиши пользователь хочет, чтобы текстовый редактор отреагировал сразу же. В противоположность этому видеокодер ограничен скоростью процессора. Если не считать, что он время от времени считывает необработанные данные с диска и записывает результирующий видеоформат на диск, то кодер большую часть времени выполняет программу видеокодека для обработки данных, что легко загружает про цессор на все 100%. Для этой программы нет строгих ограничений на время выпол 70 Глава нения: пользователю не важно, запустится она на полсекунды раньше или на полсе кунды позже. Конечно, чем раньше она завершит работу, тем лучше.

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

Алгоритм планирования В предыдущих разделах была рассмотрена в самых общих чертах теория работы планировщика процессов в операционной системе Linux. Теперь, когда мы разобра лись с основами, можно более глубоко погрузиться в то, как именно работает плани ровщик ОС Linux.

Программный код планировщика операционной системы Linux содержится в файле kernel/sched.c. Алгоритм планирования и соответствующий программный код были существенно переработаны в те времена, когда началась разработка ядер серии 2.5. Следовательно, программный код планировщика является полностью но вым и отличается от планировщиков предыдущих версий. Новый планировщик раз рабатывался для того, чтобы удовлетворять указанным ниже требованиям.

• Должен быть реализован полноценный О(1) -планировщик. Любой алгоритм, использованный в новом планировщике, должен завершать свою работу за по стоянный период времени, независимо от числа выполняющихся процессов и других обстоятельств.

• Должна обеспечиваться хорошая масштабируемость для SMP-систем. Каждый процессор должен иметь свои индивидуальные элементы блокировок и свою индивидуальную очередь выполнения.

• Должна быть реализована улучшенная SMP-привязка (SMP affinity). Задания, для выполнения на каждом процессоре многопроцессорной системы, должны быть распределены правильным образом, и, по возможности, выполнение этих задач должно продолжаться на одном и том же процессоре. Осуществлять ми грацию заданий с одного процессора на другой необходимо только для умень шения дисбаланса между размерами очередей выполнения всех процессоров.

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

• Должна быть обеспечена равнодоступность ресурсов (fairness). Ни один про цесс не должен ощущать нехватку квантов времени за допустимый период.

Кроме того, ни один процесс не должен получить недопустимо большое значе ние кванта времени.

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

Новый планировщик позволяет удовлетворить всем этим требованиям.

Очереди выполнения Основная структура данных планировщика — это очередь выполнения (runqueue).

Очередь выполнения определена в файле kernel/sched.c3 в виде структуры st ruct runqueue. Она представляет собой список готовых к выполнению процес сов для данного процессора.

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

Комментарии объясняют назначения каждого поля.

struct runqueue { spinlock_t lock

;

/* спин-блокировка для защиты этой очереди выполнения*/ unsigned long nr_rinning

;

/* количество задач, готовых к выполнению */ unsigned long nr_switches

;

/* количество переключений контекста */ unsigned long expired timestamp

;

/* время последнего обмена массивами*/ unsigned long nr_uninterruptible

;

/* количество заданий в состоянии непрерываемого ожидания */ unsigned long long timestamp last tick

;

/* последняя метка времени планировщика */ struct task_struct *curr

;

/* текущее задание, выполняемое на данном процессоре */ struct task_struct *idle

;

/* холостая задача данного процессора */ struct mm_struct *prev_mm

;

/* поле mm_struct последнего выполняемого задания */ struct prio_array "active

;

/* указатель на активный массив приоритетов*/ struct prio_array 'expired

;

/* указатель на истекший массив приоритетов*/ struct prio_array arrays[2]

;

/* массивы приоритетов */ struct task_3truct *migration_thread

;

/* миграционный поток для данного процессора */ struct list_head migration_queue

;

/* миграционная очередь для данного процессора */ atomic_t nr_iowait

;

/* количество заданий, ожидающих на ввод-вывод */ }

;

Может возникнуть вопрос: почему используется файл kernel/sched.с, а не заголовочный файл include/linux/sched.h? Потому что желательно абстрагироваться от реализации кода планиров щика и обеспечил, доступность для остального кода ядра только лишь некоторых интерфейсов.

72 Глава Поскольку очередь выполнения — это основная структура данных планировщи ка, существует группа макросов, которые используются для доступа к определенным очередям выполнения. Макрос cpu_rq (processor) возвращает указатель на оче редь выполнения, связанную с процессором, имеющим заданный номер. Аналогично макрос this_rq () возвращает указатель на очередь, связанную с текущим процессо ром. И наконец, макрос task_rq(task) возвращает указатель на очередь, в которой находится соответствующее задание.

Перед тем как производить манипуляции с очередью выполнения, ее необходимо заблокировать (блокировки подробно рассматриваются в главе 8, "Введение в синхро низацию выполнения кода ядра"). Так как очередь выполнения уникальна для каждо го процессора, процессору редко необходимо блокировать очередь выполнения дру гого процессора (однако, как будет показано далее, такое все же иногда случается).

Блокировка очереди выполнения позволяет запретить внесение любых изменений в структуру данных очереди, пока процессор, удерживающий блокировку, выполня ет операции чтения или записи полей этой структуры. Наиболее часто встречается ситуация, когда необходимо заблокировать очередь выполнения, в которой выпол няется текущее задание. В этом случае используются функции tapk_rq_lock () и task_rq_unlock(), как показано ниже.

struct runqueue *rq

;

unsigned long flags

;

rq = task_rq_lock(task, &flags)

;

/* здесь можно производить манипуляции с очередью выполнения */ task_rq_unlock (rq, &flags)

;

Альтернативными функциями выступают функция this_rq_lock (), которая по зволяет заблокировать текущую очередь выполнения, и функция rqunlock (struct runqueue *rq), позволяющая разблокировать указанную в аргументе очередь.

Для предотвращения взаимных блокировок код, который захватывает блокиров ки нескольких очередей, всегда должен захватывать блокировки в одном и том же порядке, а именно в порядке увеличения значения адреса памяти очереди (в главе 8, "Введение в синхронизацию выполнения кода ядра", приведено полное описание).

Пример, как это осуществить, показан ниже.

/* для того, чтобы заблокировать... */ if (rql < rq2) ( spin_lock (s,rql->lock]

;

spin_lock(Srq2->lock)

;

} else ( spin_lock(Srq2->lock)

;

spin_lock(&rql->lock) } /* здесь можно манипулировать обеими очередями... */ /• для того, чтобы разблокировать... */ spin_unlock(brql->lock)

;

spin_unlock(&rq2->lock)

;

Планирование выполнения процессов С помощью функций double_rq_lock () и double_rq_unlock () указанные шаги можно выполнить автоматически. При этом получаем следующее.

double_rq_lock(rql, rq2)

;

/* здесь можно манипулировать обеими очередями...*/ double_rq_unlock(rql, rq2)

;

Рассмотрим небольшой пример, который показывает, почему важен порядок за хвата блокировок. Вопрос взаимных блокировок обсуждается в главах 8, "Введение в синхронизацию выполнения кода ядра" и 9, "Средства синхронизации в ядре". Эта проблема касается не только очередей выполнения: вложенные блокировки должны всегда захватываться в одном и том же порядке. Спин-блокировки используются для предотвращения манипуляций с очередями выполнения несколькими задачами одно временно. Принцип работы этих блокировок аналогичен ключу, с помощью которо го открывают дверь. Первое задание, которое подошло к двери, захватывает ключ, входит в дверь и закрывает ее с другой стороны. Если другое задание подходит к двери и определяет, что дверь закрыта (потому что за дверью находится первое за дание), то оно должно остановиться и подождать, пока первое задание на выйдет и не возвратит ключ. Ожидание называется спиннингом (вращением, spinning), так как на самом деле задание постоянно выполняет цикл, периодически проверяя, не возвра щен ли ключ. Теперь рассмотрим, что будет, если одно задание пытается сначала за блокировать первую очередь выполнения, а затем вторую, в то время как другое зада ние пытается сначала заблокировать вторую очередь, а затем — первую. Допустим, что первое задание успешно захватило блокировку первой очереди, в то время как второе задание захватило блокировку второй очереди. После этого первое задание пытается заблокировать вторую очередь, а второе задание — первую. Ни одно из заданий никогда не добьется успеха, так как другое задание уже захватило эту бло кировку. Оба задания будут ожидать друг друга вечно. Так же как в тупике дороги создается блокировка движения, так и неправильный порядок захвата блокировок приводит к тому, что задания начинают ожидать друг друга вечно, и тоже возникает тупиковая ситуация, которая еще называется взаимоблокировкой. Если оба задания захватывают блокировки в одном и том же порядке, то рассмотренной ситуации произойти не может. В главах 8 и 9 представлено полное описание блокировок.

Массивы приоритетов Каждая очередь выполнения содержит два массива приоритетов (priority arrays): ак тинный и истекший. Массивы приоритетов определены в файле kernel/sched.c в виде описания struct prio_array. Массивы приоритетов — это структуры дан ных, которые обеспечивают 0(1)-планирование. Каждый массив приоритетов содер жит для каждого значения приоритета одну очередь процессов, готовых к выполне нию. Массив приоритетов также содержит битовую маску приоритетов (priority bitmap), используемую для эффективного поиска готового к выполнению задания, у которого значение приоритета является наибольшим в системе.

struct prio_array ( int nr_active

;

/* количество заданий */ unsigned long bitmap[BITMAP_SIZE]

;

/* битовая маска приоритетов */ struct list head queue[MAX_PRIO]

;

/* очереди приоритетов */ }

;

74 Глава Константа MAX_PRIO — это количество уровней приоритета в системе. По умолча нию значение этой константы равно 140. Таким образом, для каждого значения при оритета выделена одна структура st ruct list_head. Константа BITMAP_SIZE — это размер массива переменных, каждый элемент которого имеет тип unsigned long.

Каждый бит этого массива соответствует одному действительному значению приори тета. В случае 140 уровней приоритетов и при использовании 32-разрядных машин ных слов, значение константы BITMAP_SIZE равно 5. Таким образом, поле bitmap — это массив из пяти элементов, который имеет длину 160 бит.

Все массивы приоритетов содержат поле bitmap, каждый бит этого поля соот ветствует одному значению приоритета в системе. В самом начале значения всех битов равны 0. Когда задача с определенным приоритетом становится готовой к вы полнению (то есть значение статуса этой задачи становится равным TASK_RUNNING), соответствующий этому приоритету бит поля bitmap устанавливается в значение 1.

Например, если задача с приоритетом, равным 7, готова к выполнению, то устанав ливается бит номер 7. Нахождение задания с самым высоким приоритетом в системе сводится только лишь к нахождению самого первого установленного бита в битовой маске. Так как количество приоритетов неизменно, то время, которое необходимо затратить на эту операцию поиска, постоянно и не зависит от количества процес сов, выполняющихся в системе. Более того, для каждой поддерживаемой аппарат ной платформы в ОС Linux должен быть реализован быстрый алгоритм поиска перво го установленного бита (find first set) для проведения быстрого поиска в битовой маске.

Эта функция называется sched_fi nd_fi rst_bi t (). Для многих аппаратных плат форм существует машинная инструкция нахождения первого установленного бита в заданном машинном слове4. Для таких систем нахождение первого установленного бита является тривиальной операций и сводится к выполнению этой инструкции не сколько раз.

Каждый массив приоритетов также содержит массив очередей, представленных структурами struct list_head. Этот массив называется queue. Каждому значению приоритета соответствует своя очередь. Очереди реализованы в виде связанных списков, и каждому значению приоритета соответствует список всех процессов си стемы, готовых к выполнению, имеющих это значение приоритета и находящихся в очереди выполнения данного процессора. Нахождение задания, которое будет вы полняться следующим, является простой задачей и сводится к выбору следующего элемента из списка. Все задания с одинаковым приоритетом планируются на выпол нение циклически.

Массив приоритетов также содержит счетчик nr_active, значение которого со ответствует количеству готовых к выполнению заданий в данном массиве приорите тов.

Пересчет квантов времени Во многих операционных системах (включая и более старые версии ОС Linux) используется прямой метод для пересчета значения кванта времени каждого зада ния, когда все эти значения достигают нуля.

Для аппаратной платформы х86 используется инструкция bsfl, а для платформы РРС — инструк ция cntlzw.

Планирование выполнения процессов Обычно это реализуется с помощью цикла по всем задачам в системе, например, следующим образом.

for (каждого задания в системе) ( пересчитать значение приоритета пересчитать значение кванта времени } Значение приоритета и другие атрибуты задачи используются для определения нового значения кванта времени. Такой подход имеет некоторые проблемы.

• Пересчет потенциально может занять много времени. Хуже того, время такого расчета масштабируется как О (n), где n — количество задач в системе.

• Во время пересчета должен быть использован какой-нибудь тип блокировки для защиты списка задач и отдельных дескрипторов процессов. В результате получается высокий уровень конфликтов при захвате блокировок.

• Отсутствие определенности в случайно возникающих пересчетах значений квантов времени является проблемой для программ реального времени.

• Откровенно говоря, это просто нехорошо (что является вполне оправданной причиной для каких-либо усовершенствований ядра Linux).

Новый планировщик ОС Linux позволяет избежать использования цикла пере счета приоритетов. Вместо этого в нем применяется два массива приоритетов для каждого процессора: активный (active) и истекший (expired). Активный массив при оритетов содержит очередь, в которую включены все задания соответствующей очереди выполнения, для которых еще не иссяк квант времени. Истекший массив приоритетов содержит все задания соответствующей очереди, которые израсходо вали свой квант времени. Когда значение кванта времени для какого-либо задания становится равным нулю, то перед тем, как поместить это задание в истекший мас сив приоритетов, для него вычисляется новое значение кванта времени. Пересчет значений кванта времени для всех процессов проводится с помощью перестановки активного и истекшего массивов местами. Так как на массивы ссылаются с помощью указателей, то переключение между ними будет выполняться так же быстро, как и перестановка двух указателей местами. Показанный ниже код выполняется в функ ции schedule ().

struct prio_array array = rq->active

;

if (!array->nr_active) { rq->active = rq->expired

;

rq->expired = array

;

} Упомянутая перестановка и есть ключевым, моментом O(1)-планировщика. Вместо того чтобы все время пересчитывать значение приоритета и кванта времени для каждого процесса, О(1)-планировщик выполняет простую двухшаговую перестанов ку массивов. Такая реализация позволяет решить указанные выше проблемы.

76 Глава Функция schedule () Все действия по выбору следующего задания на исполнение и переключение на выполнение этого задания реализованы в виде функции schedule (). Эта функция вызывается явно кодом ядра при переходе в приостановленное состояние (sleep), a также в случае когда какое-либо задание вытесняется. Функция schedule () выпол няется независимо каждым процессором. Следовательно, каждый процессор само стоятельно принимает решение о том, какой процесс выполнять следующим.

Функция schedule () достаточно проста, учитывая характер тех действий, кото рые она выполняет. Следующий код позволяет определить задачу с наивысшим при оритетом.

struct task_struct *prev, *next

;

struct list_head *queue

;

struct prio_array *array

;

int idx

;

prev = current

;

array = rq->active

;

idx = sched_find_first_bit(array->bitmap)

;

queue = array->queue + idx

;

next = listentry(queue->next, struct task struct, run_ist)

;

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

schedule() sched_find_first_set() Бит 0 приоритет Бит 7 приоритет Список всех готовых к выполнению задач а соответствии с их приоритетами Массив приоритетов Бит 139 приоритет длиной 140 бит Список готовых Запуск первого процесса в списке к выполнению заданий, имеющих приоритет Рис. 4.2. Алгоритм работы О(1)-планировщка операционной системы Linux Планирование выполнения процессов Если полученные значения переменных prev и next не равны друг другу, то для выполнения выбирается новое задание (next). При этом для переключения с за дания, на которое указывает переменная prev, на задание, соответствующее пере менной next, вызывается функция context_switch (), зависящая от аппаратной платформы. Переключение контекста будет рассмотрено в одном из следующих раз делов.

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

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

Кроме того, были рассмотрены процессы, ограниченные скоростью ввода-вывода и скоростью процессора, а также было описано, почему желательно поднимать при оритет интерактивных задач. Теперь давайте рассмотрим код, который реализует эти соображения.

Процессы имеют начальное значение приоритета, которое называется nice. Это значение может лежать в диапазоне от -20 до 19, по умолчанию используется зна чение 0. Значение 19 соответствует наиболее низкому приоритету, а значение -20 — наиболее высокому. Значение параметра nice хранится в поле static_prio струк туры task_struct процесса. Это значение называется статическим приоритетом, потому что оно не изменяется планировщиком и остается таким, каким его указал пользователь. Планировщик свои решения основывает на динамическом приорите те, которое хранится в поле prio. Динамический приоритет вычисляется как функ ция статического приоритета и интерактивности задания.

Функция effective_prio () возвращает значение динамического приоритета за дачи. Эта функция исходит из значения параметра пicе для данной задачи и вычисля ет для этого значения надбавку или штраф в диапазоне от -5 до 5, в зависимости от интерактивности задачи. Например, задание с высокой интерактивностью, которое имеет значение параметра nice, равное 10, может иметь динамический приоритет, равный 5. И наоборот, программа со значением параметра nice, равным 10, которая достаточно активно использует процессор, может иметь динамический приоритет, равный 12. Задачи, которые обладают умеренной интерактивностью, не получают ни надбавки, ни штрафа, и их динамический приоритет совпадает со значением па раметра nice.

Конечно, планировщик по волшебству не может определить, какой процесс яв ляется интерактивным. Для определения необходима некоторая эвристика, которая отражает, является ли процесс ограниченным скоростью ввода-вывода или скорос тью процессора. Наиболее выразительный показатель — сколько времени задача на ходится в приостановленном состоянии (sleep). Если задача проводит большую часть времени в приостановленном состоянии, то она ограничена вводом-выводом. Если задача больше времени находится в состоянии готовности к выполнению, чем в при 78 Глава остановленном состоянии, то эта задача не интерактивна. В экстремальных случаях, если задача большую часть времени находится в приостановленном состоянии, то она полностью ограничена скоростью ввода-вывода

;

если задача все время готова к выполнению, то она ограничена скоростью процессора.

Для реализации такой эвристики в ядре Linux предусмотрен изменяемый пока затель того, как соотносится время, которое процесс проводит в приостановлен ном состоянии, со временем, которое процесс проводит в состоянии готовности к выполнению. Значение этого показателя хранится в поле sleep_avg структуры task_struct. Диапазон значений этого показателя лежит от нуля до значения MAX_SLEEP_AVG, которое по умолчанию равно 10 мс. Когда задача становится гото вой к выполнению после приостановленного состояния, значение поля sleep_avg увеличивается на значение времени, которое процесс провел в приостановленном состоянии, пока значение sleep_avg не достигнет MAX_SLEEP_AVG. Когда задача выполняется, то в течение каждого импульса таймера (timer tick) значение этой пе ременной уменьшается, пока оно не достигнет значения 0.

Такой показатель является, на удивление, надежным. Он рассчитывается не толь ко на основании того, как долго задача находится в приостановленном состоянии, но и на основании того, насколько мало задача выполняется. Таким образом, задача, которая проводит много времени в приостановленном состоянии и в то же время постоянно использует свой квант времени, не получит большой прибавки к прио ритету: показатель работает не только для поощрения интерактивных задач, но и для наказания задач, ограниченных скоростью процессора. Этот показатель также устойчив по отношению к злоупотреблениям. Задача, которая получает повышенное Значение приоритета и большое значение кванта времени, быстро утратит свою над бавку к приоритету, если она постоянно выполняется и сильно загружает процес сор. В конце концов, такой показатель обеспечивает малое время реакции. Только что созданный интерактивный процесс быстро достигнет высокого значения поля sleep_avg. Несмотря на все сказанное, надбавка и штраф применяются к значению параметра nice, так что пользователь также может влиять на работу системного пла нировщика путем изменения значения параметра nice процесса.

Расчет значения кванта времени, наоборот, более прост, так как значение ди намического приоритета уже базируется на значении параметра nice и на интерак тивности (эти показатели планировщик учитывает как наиболее важные). Поэтому продолжительность кванта времени может быть просто выражена через значение динамического приоритета. Когда создается новый процесс, порожденный и роди тельский процессы делят пополам оставшуюся часть кванта времени родительского процесса. Такой подход обеспечивает равнодоступность ресурсов и предотвращает возможность получения бесконечного значения кванта времени путем постоянного создания порожденных процессов. Однако после того, как квант времени задачи ис сякает, это значение пересчитывается на основании динамического приоритета за дачи. Функция task_timeslice () возвращает новое значение кванта времени для данного задания. Расчет просто сводится к масштабированию значения приоритета в диапазон значений квантов времени. Чем больше значение приоритета задачи, тем большей продолжительности квант времени получит задание в текущем цикле выполнения. Максимальное значение кванта времени равно MAX_TIMESLICE, кото рое по умолчанию равно 200 мс. Даже задания с самым низким приоритетом полу чают квант времени продолжительностью MIN_TIMESLICE, что соответствует 10 мс.

Планирование выполнения процессов Задачи с приоритетом, используемым по умолчанию (значение параметра nice, равно О и отсутствует надбавка и штраф за интерактивность), получают квант времени про должительностью 100 мс, как показано в табл. 4.1.

Таблица 4.1. Продолжительности квантов времени планировщика Продолжительность Тип задания Значение параметра nice кванта времени Вновь созданное То же, что и у родительского Половина от родительского процесса процесса Минимальный приоритет +19 5 мс (MIN_TIMESUCE) Приоритет по умолчанию 0 100 мс (DEF_TIMESLICE) Максимальный приоритет -20 800 мс (MAX_TIMESLICE) Для интерактивных задач планировщик оказывает дополнительную услугу: если задание достаточно интерактивно, то при исчерпании своего кванта времени оно будет помещено не в истекший массив приоритетов, а обратно в активный массив приоритетов. Следует вспомнить, что пересчет значений квантон времени произво дится путем перестановки активного и истекшего массивов приоритетов: активный массив становится истекшим, а истекший— активным. Такая процедура обеспечива ет пересчет значений квантов времени, который масштабируется по времени как O(1). С другой стороны, это может привести к тому, что интерактивное задание станет готовым к выполнению, но не получит возможности выполняться, так как оно "застряло" в истекшем массиве. Помещение интерактивных заданий снова в ак тивный массив позволяет избежать такой проблемы. Следует заметить, что это зада ние не будет выполняться сразу же, а будет запланировано на выполнение по кругу вместе с другими заданиями, которые имеют такой же приоритет. Данную логику реализует функция scheduler_tick (), которая вызывается обработчиком преры ваний таймера (обсуждается в главе 10, "Таймеры и управление временем"), как по казано ниже.

struct task_struct *task = current

;

struct runqueue *rq = this_rq()

;

if (!--task->time_slice) { if (!TASK_INTERACTIVE(task) || EXPIRED_STARVING(rq)) enqueue_task(task, rq->expired)

;

else enqueue_task(task, rq->active)

;

} Показанный код уменьшает значение кванта времени процесса и проверяет, не стало ли это значение равным нулю. Если стало, то задание является истекшим и его необходимо поместить в один из массивов. Для этого код вначале проверяет ин терактивность задания с помощью макроса ТАSK_INTERACTIVE (). Этот макрос на основании значения параметра nice рассчитывает, является ли задание "достаточно интерактивным". Чем меньше значение nice (чем выше приоритет), тем менее инте рактивным должно быть задание. Задание со значением параметра nice, равным 19, никогда не может быть достаточно интерактивным для помещения обратно в актив 80 Глава ный массив. Наоборот, задание со значением nice, равным -20, должно очень сильно использовать процессор, чтобы его не поместили в активный массив. Задача со зна чением nice, используемым по умолчанию, т.е. равным нулю, должна быть достаточно интерактивной, чтобы быть помещенной обратно в активный массив, но это также отрабатывается достаточно четко. Следующий макрос, EXPIRED_STARVING (), про веряет, нет ли в истекшем массиве процессов, особенно нуждающихся в выполнении (startving), когда массивы не переключались в течение достаточно долгого времени.

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

Переход в приостановленное состояние и возврат к выполнению Приостановленное состояние задачи (состояние ожидания, заблокированное состояние, sleeping, blocked) представляет собой специальное состояние задачи, в ко тором задание не выполняется. Это является очень важным, так как в противном случае планировщик выбирал бы на выполнение задания, которые не "хотят" вы полняться, или, хуже того, состояние ожидания должно было бы быть реализовано в виде цикла, занимающего время процессора. Задачи могут переходить в приоста новленное состояние по нескольким причинам, но в любом случае— в ожидании наступления некоторого события. Событием может быть ожидание наступления некоторого момента времени, ожидание следующей порции данных при файловом вводе-ныводе или другое событие в аппаратном обеспечении. Задача также может переходить в приостановленное состояние непроизвольным образом, когда она пытается захватить семафор в режиме ядра (эта ситуация рассмотрена в главе 9, "Средства синхронизации в ядре"). Обычная причина перехода в приостановленное состояние — это выполнение операций файлового ввода-вывода, например задание вызывает функцию read () для файла, который необходимо считать с диска. Еще один пример— задача может ожидать на ввод данных с клавиатуры. В любом случае ядро ведет себя одинаково: задача помечает себя как находящуюся в приостанов ленном состоянии, помещает себя в очередь ожидания (wail queue), удаляет себя из очереди выполнения и вызывает функцию schedul ed для выбора нового процесса на выполнение. Возврат к выполнению (wake up) происходит в обратном порядке:

задача помечает себя как готовую к выполнению, удаляет себя из очереди ожидания и помепгает себя в очередь выполнения.

Как указывалось в предыдущей главе, с приостановленным состоянием связаны два значения поля состояния процесса: TASK_INTERRUPTIBLE и TASK_UNINTERRUPTIBLE.

Они отличаются только тем, что в состоянии TASK_UNINTERRUPTIBLE задача игно рирует сигналы, в то время как задачи в состоянии TASK_INTERRUPTIBLE возвраща ются к выполнению преждевременно и обрабатывают пришедший сигнал. Оба типа задач, находящихся в приостановленном состоянии, помещаются в очередь ожида ния, ожидают наступления некоторого события и не готовы к выполнению.

Приостановленное состояние обрабатывается с помощью очередей ожидания (wait queue). Очередь ожидания— это просто список процессов, которые ожидают Планирование выполнении процессов наступления некоторого события. Очереди ожидания в ядре представляются с по мощью типа данных wait_queue_head_t. Они могут быть созданы статически с по мощью макроса DECLARE_WAIT_QUEUE_HEAD () или выделены динамически с после дующей инициализацией с помощью функции init_waitqueue_head (). Процессы помещают себя в очередь ожидания и устанавливают себя в приостановленное со стояние. Когда происходит событие, связанное с очередью ожидания, процессы, на ходящиеся в этой очереди, возвращаются к выполнению. Важно реализовать пере ход в приостановленное состояние и возврат к выполнению правильно, так чтобы избежать конкуренции за ресурсы (race).

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

/* пусть q — это очередь ожидания (созданная в другом месте), где мы хотим находиться в приостановленном состоянии */ DECLARE_WAITQUEUE(wait, current)

;

add_wait_queue(q, &wait)

;

set_current_State(TASK_INTERRUPTIBLE)

;

/* или TASK_UNINTERRUPTIBLE */ /* переменная condition характеризует наступление события, которого мы ожидаем */ while (!condition) schedule()

;

set_current_state(TASK_RUNNING)

;

remove_wait queue(q, &wait)

;

Опишем шаги, которые должна проделать задача для того, чтобы поместить себя в очередь ожидания.

• Создать элемент очереди ожидания с помощью макроса DECLARE_WAITQUEUE ().

• Добавить себя в очередь ожидания с помощью функции add wait_queue ().

С помощью этой очереди ожидания процесс будет возвращен в состояние го товности к выполнению, когда условие, на выполнение которого ожидает про цесс, будет выполнено. Конечно, для этого где-то в другом месте должен быть код, который вызывает функцию wake_up () для данной очереди, когда про изойдет соответствующее событие.

• Изменить состояние процесса в значение TASK_INTERRUPTIBLE или TASK_ UNINTERRUPTIBLE.

• Проверить, не выполнилось ли ожидаемое условие. Если выполнилось, то больше нет необходимости переходить в приостановленное состояние. Если нет, то вызвать функцию schedule ().

• Когда задача становится готовой к выполнению, она снова проверяет выпол нение ожидаемого условия. Если условие выполнено, то производится выход 82 Глава из цикла. Если нет, то снова вызывается функция schedule () и повторяется проверка условия.

• Когда условие выполнено, задача может установить свое состояние в значе ние TASK_RUNNING и удалить себя из очереди ожидания с помощью функции remove_wait_queue().

Если условие выполнится перед тем, как задача переходит в приостановленное состояние, то цикл прервется и задача не перейдет в приостановленное состояние по ошибке. Следует заметить, что во время выполнения тела цикла код ядра часто может выполнять и другие задачи. Например, перед выполнением функции sched ule () может возникнуть необходимость освободить некоторые блокировки и захва тить их снова после возврата из этой функции

;

если процессу был доставлен сигнал, то необходимо возвратить значение -ERESTARTSYS

;

может возникнуть необходи мость отреагировать на некоторые другие события.

Возврат к выполнению (wake up) производится с помощью функции wake_up (), которая возвращает все задачи, ожидающие в данной очереди, в состояние готов ности к выполнению. Вначале вызывается функция try_to_wake_up (), которая устанавливает поле состояния задачи в значение TASK_RUNNING, далее вызывается функция activate_task () для добавления задачи в очередь выполнения и устанав ливается флаг need_resched в ненулевое значение, если приоритет задачи, которая возвращается к выполнению, больше приоритета текущей задачи. Код, который от вечает за наступление некоторого события, обычно вызывает функцию wakeup () после того, как это событие произошло. Например, после того как данные прочи таны с жесткого диска, подсистема VFS вызывает функцию wake_up () для очереди ожидания, которая содержит все процессы, ожидающие поступления данных.

Важным может быть замечание о том, что переход в приостановленное состоя ние часто сопровождается ложными переходами к выполнению. Это возникает по тому, что переход задачи в состояние выполнения не означает, что событие, которо го ожидала задача, уже наступило: поэтому переход в приостановленное состояние должен всегда выполняться в цикле, который гарантирует, что условие, на которое ожидает задача, действительно выполнилось (рис. 4.3).

Балансировка нагрузки Как уже рассказывалось ранее, планировщик операционной системы Linux ре ализует отдельные очереди выполнения и блокировки для каждого процессора в симметричной многопроцессорной системе. Это означает, что каждый процессор поддерживает свой список процессов и выполняет алгоритм планирования только для заданий из этого списка. Система планирования, таким образом, является уни кальной для каждого процессора. Тогда каким же образом планировщик обеспечива ет какую-либо глобальную стратегию планирования для многопроцессорных систем?

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

Планирование выполнения процессов Функция add_wait_que-je() помещает задачу в очередь ожидания, устанавливает состояние задачи TASK_INTERRUPTIBLE и вызывает функцию schedule(). Функция scheduled вызывает функцию deactivate_task(), которая удаляет задачу из очереди выполнения.

Задача готова к выполнению Задача не готова к выполнению Получен сигнал, задача устанавливается в состояние TASK_RUNNING и выполняет обработчик сигнала Событие, на которое ожидала задача, произошло, и функция try_to_wake_up() устанавливает задачу в состояние TASK_RUNNING, вызывает функцию activate_task() и функцию schedule().

Функция remove_wait_quaue () удаляет задачу из очереди ожидания.

Рис. 4,3. Переход в приостановленное состояние (sleeping) и возврат к выполнению (wake up) Если обнаруживается дисбаланс, то процессы из самой загруженной очереди вы полнения выталкиваются в текущую очередь, В идеальном случае каждая очередь вы полнения будет иметь одинаковое количество процессов. Такая ситуация, конечно, является высоким идеалом, к которому система балансировки может только прибли зиться.

Система балансировки нагрузки реализована в файле kernel /sched. с в виде функции load_balance (). Эта функция вызывается в двух случаях. Она вызывается функцией schedule (), когда текущая очередь выполнения пуста. Она также вызы вается по таймеру с периодом в 1 мс, когда система не загружена, и каждые 200 мс в другом случае. В однопроцессорной системе функция load_balance () не вызыва ется никогда, в действительности она даже не компилируется в исполняемый образ ядра, питому что в системе только одна очередь выполнения и никакой балансиров ки не нужно.

Функция балансировки нагрузки вызывается при заблокированной очереди вы полнения текущего процессора, прерывания при этом также запрещены, чтобы за щитить очередь выполнения от конкурирующего доступа. В том случае, когда функ ция load balance() вызывается из функции schedule (), цель ее вызова вполне ясна, потому что текущая очередь выполнения пуста и нахождение процессов в других очередях с последующим их проталкиванием в текущую очередь позволяет получить преимущества. Когда система балансировки нагрузки активизируется по средством таймера, то ее задача может быть не так очевидна. В данном случае это необходимо для устранения любого дисбаланса между очередями выполнения, что бы поддерживать их в почти одинаковом состоянии, как показано на рис. 4.4.

84 Глава Процесс 1 Процесс Процесс 2 Процесс Процесс З Процесс Процесс 4 Процесс load_balancer() Процесс 5 Процесс Проталкивание процесса Процесс 6 Процесс из одной очереди в другую для уменьшения дисбаланса Процесс 20 Процесс Очередь выполнения Очередь выполнения процессора 1,всего процессора 2, всего 15процессов 20 процессов Рис. 4.4. Система балансировки нагрузки Функция load_balance () и связанные с ней функции сравнительно большие и сложные, хотя шаги, которые они предпринимают, достаточно ясны.

• Функция load_balance () вызывает функцию find_busiest_queue () для определения наиболее загруженной очереди выполнения. Другими словами — очередь с наибольшим количеством процессов в ней. Если нет очереди выпол нения, количество процессов в которой на 25% больше, чем в дайной очереди, то функция f ind_busiest_queue () возвращает значение NULL и происходит возврат из функции load_balance (). В другом случае возвращается указатель на самую загруженную очередь.

• Функция load_balance () принимает решение о том, из какого массива прио ритетов самой загруженной очереди будут проталкиваться процессы. Истекший массив является более предпочтительным, так как содержащиеся в нем задачи не выполнялись достаточно долгое время и, скорее всего, не находятся в кэше процессора (т.е. не активны в кэше, not "cache hot"). Если истекший массив приоритетов пуст, то ничего не остается, как использовать активный массив.

• Функция load_balance () находит непустой список заданий, соответствующий самому высокому приоритету (с самым маленьким номером), так как важно бо лее равномерно распределять задания с высоким приоритетом, чем с низким.

• Каждое задание с данным приоритетом анализируется для определения зада ния, которое не выполняется, не запрещено для миграции из-за процессорной привязки и не активно в кэше.Если найдена задача, которая удовлетворяет этому критерию, то вызывается функция pull_task () для проталкивания этой задачи из наиболее загруженной очереди в данную очередь.

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

Планирование выполнения процессов Далее показана функция load_balance (), немного упрощенная, но содержащая все цажные детали.

static int load_balance(int this_cpu, runqueue_t *this_rq, struct sched_doraain *sd, enum idle_type idle) { struct sched_group *group

;

runqueue_t *busiest

;

unsigned long imbalance

;

int nr_moved

;

spin_lock(&this_rq->lock)

;

group = find_busiest_group(sd, this_cpu, &imbalance, idle)

;

if (!group) goto out_balanced

;

busiest = find_busiest_queue(group)

;

if (!busiest) goto out_balanced

;

nr_moved = 0

;

if (busiest->nr_running > 1) { double_lock_balance(this_rq, busiest)

;

nr_moved = move_tasks(this_rq, this_cpu, busiest, imbalance, sd, idle)

;

spin_unlock(&busiest->lock)

;

} spin_unlock(&this rq->lock)

;

if (!nr_moved) { sd->nr_balance_failed++

;

if (unlikely(sd->nr_balance_failed > sd->cache_nice_tries+2)) { int wake = 0

;

spin_lock(abusiest->lock)

;

if (!busiest->active_balance) { busiest->active_balance = 1

;

busiest->push_cpu = this_cpu

;

wake = 1

;

} spin_unlock(&busiest->lock)

;

if (wake) wake_up_process(busiest->migration_thread)

;

sd->nr_balance_failed = sd->cache_nice_tries

;

) } else sd->nr_balance_failed = 0

;

sd->balance_interval = sd->min_interval

;

return nr_moved

;

86. Глава out_balanced:

spin_unlock (&this_rq->lock)

;

if (sd->balance_interval < sd->max_interval) sd->balance_interval *= 2

;

return 0

;

} Вытеснение и переключение контекста Переключение контекста — это переключение от одной, готовой к выпол нению задачи к другой. Это переключение производится с помощью функции context_switch(), определенной в файле kernel/sched.с. Данная функция вы зывается функцией schedule (), когда новый процесс выбирается для выполнения.

При этом выполняются следующие шаги.

• Вызывается функция switch_mm (), которая определена в файле include/asm/ mmu_context.h и предназначена для переключения от виртуальной памяти старого процесса к виртуальной памяти нового процесса.

• Вызывается функция switch_to (), определенная в файле include /asm/ system.h, для переключения от состояния процессора предыдущего процесса к состоянию процессора нового процесса. Эта процедура включает восстанов ление информации стека ядра и регистров процессора.

Ядро должно иметь информацию о том, когда вызывать функцию schedule ().

Если эта функция будет вызываться только тогда, когда программный код вызывает ее явно, то пользовательские программы могут выполняться неопределенное время.

Поэтому ядро поддерживает флаг need_resched для того, чтобы сигнализировать, необходимо ли вызывать функцию schedule () (табл. 4.2). Этот флаг устанавлива ется функцией schediiler_tick (), когда процесс истрачивает свой квант времени, и функцией try_to_wake_up (), когда процесс с приоритетом более высоким, чем у текущего процесса, возвращается к выполнению. Ядро проверяет значение этого флага, и если он установлен, то вызывается функция schedule () для переключения на новый процесс. Этот флаг является сообщением ядру о том, что планировщик должен быть активизирован по возможности раньше, потому что другой процесс должен начать выполнение.

Таблица 4.2. Функции для управления флагом need_resched Функция Назначение set_tsk_need_resched (task) Установить флаг need_resched для данного процесса clear_tsk_need_resched (task) Очистить флаг need_resched для данного процесса need_resched() Проверить значение флага need_resched для данного процесса. Возвращается значение true, если этот флаг установлен, и false, если не установлен Планирование выполнения процессов Во время переключения в пространство пользователи или при возврате из пре рывания, значение флага need_resched проверяется. Если он установлен, то ядро активизирует планировщик перед тем, как продолжить работу.

Этот флаг не является глобальной переменной, так как обращение к дескрипто ру процесса получается более быстрым, чем обращение к глобальным данным (из-за скорости обращения к переменной current и потому, что соответствующие данные могут находиться в кэше). Исторически, этот флаг был глобальным в ядрах до серии 2.2. В ядрах серий 2.2 и 2.4 этот флаг принадлежал структуре task_struct и имел тип int. В серии ядер 2.6 этот флаг перемещен в один определенный бит специаль ной переменной флагов структуры thread info. Легко видеть, что разработчики ядра никогда не могут быть всем довольны.

Вытеснение пространства пользователя Вытеснение пространства пользователя (user preemption) происходит в тот мо мент, когда ядро собирается возвратить управление режиму пользователя, при этом устанавливается флаг need_resched и, соответственно, активизируется планиров щик. Когда ядро возвращает управление в пространство пользователя, то оно нахо дится в безопасном и "спокойном" состоянии. Другими словами, если продолжение выполнения текущего задания является безопасным, то безопасным будет также и выбор нового задания для выполнения. Поэтому когда ядро готовится возвратить управление в режим пользователя или при возврате из прерывания или после си стемного вызова, происходит проверка флага need_resched. Если этот флаг уста новлен, то активизируется планировщик И выбирает новый, более подходящий процесс для исполнения. Как процедура возврата из прерывания, так и процедура возврата из системного вызова являются зависимыми от аппаратной платформы и обычно реализуются на языке ассемблера в файле entry.S (этот файл, кроме кода входа в режим ядра, также содержит и код выхода из режима ядра). Если коротко, то вытеснение пространства пользователя может произойти в следующих случаях.

• При возврате в пространство пользователя из системного вызова.

• При возврате в пространство пользователя из обработчика прерывания.

Вытеснение пространства ядра Ядро операционной системы Linux, в отличие от ядер большинства вариантов ОС Unix, является полностью преемптивным (вытесняемым, preemptible). В непре емптивных ядрах код ядра выполняется до завершения. Иными словами, планиров щик не может осуществить планирование для выполнения другого задания, пока какое-либо задание выполняется в пространстве ядра — код ядра планируется на вы полнение кооперативно, а не посредством вытеснения. Код ядра выполняется до тех пор, пока он не завершится (возвратит управление в пространство пользователя) или пока явно не заблокируется. С появлением серии ядер 2.6, ядро Linux стало пре емптивным: теперь есть возможность вытеснить задание в любой момент, конечно, пока ядро находится в состоянии, когда безопасно производить перепланирование выполнения.

В таком случае когда же безопасно производить перепланирование? Ядро способ но вытеснить задание, работающее в пространстве ядра, когда это задание не удер 88 Глава живает блокировку. Иными словами, блокировки используются в качестве маркеров тех областей, в которые задание не может быть вытеснено. Ядро рассчитано на мно гопроцессорность (SMP-safe), поэтому если блокировка не удерживается, то код ядра является реентерабельным и его вытеснять безопасно.

Первое изменение, внесенное для поддержки вытеснения пространства ядра, — это введение счетчика преемптивности preempt_count в структуру thread_infо каждого процесса. Значение этого счетчика вначале равно нулю и увеличивается на единицу при каждом захвате блокировки, а также уменьшается на единицу при каж дом освобождении блокировки. Когда значение счетчика равно нулю— ядро явля ется вытесняемым. При возврате из обработчика прерывания, если возврат выпол няется в пространство ядра, ядро проверяет значения переменных need_resched и preempt_count. Если флаг need_resched установлен и значение счетчика preemptcount равно нулю, значит, более важное задание готово к выполнению и выполнять вытеснение безопасно. Далее активизируется планировщик. Если зна чение счетчика preempt_count не равно нулю, значит, удерживается захваченная блокировка и выполнять вытеснение не безопасно. В таком случае возврат из об работчика прерывания происходит в текущее выполняющееся задание. Когда осво бождаются все блокировки, удерживаемые текущим заданием, значение счетчика preempt_count становится равным нулю. При этом код, осуществляющий освобож дение блокировки, проверяет, не установлен ли флаг need_resched. Если установ лен, то активизируется планировщик. Иногда коду ядра необходимо иметь возмож ность запрещать или разрешать вытеснение в режиме ядра, что будет рассмотрено в главе 9.

Вытеснение пространства ядра также может произойти явно, когда задача бло кируется в режиме ядра или явно вызывается функция schedul e (). Такая форма преемптивности ядра всегда поддерживалась, так как в этом случае нет необходимо сти в дополнительной логике, которая бы давала возможность убедиться, что вытес нение проводить безопасно. Предполагается, что если код явно вызывает функцию schedule (), то точно известно, что перепланирование производить безопасно.

Вытеснение пространства ядра может произойти в следующих случаях.

• При возврате из обработчика прерывания в пространство ядра.

• Когда код ядра снова становится преемптивным.

• Если задача, работающая в режиме ядра, явно вызывает функцию schedule ().

• Если задача, работающая в режиме ядра, переходит в приостановленное состо яние, т.е. блокируется (что приводит к вызову функции schedule ()).

Режим реального времени Операционная система Linux обеспечивает две стратегии планирования в режи ме реального времени (real-lime): SCHED_FIFO и SCHED_RR. Стратегия планирования SCHED_OTHER является обычной стратегией планирования, т.е. стратегий планирова ния не в режиме реального времени. Стратегия SCHED_FIFO обеспечивает простой алгоритм планирования по идеологии "первым вошел — первым обслужен" (first-in first-out, FIFO) без квантов времени. Готовое к выполнению задание со стратегией планирования SCHED_FIFO всегда будет планироваться на выполнение перед всеми заданиями со стратегией планирования SCHED_OTHER. Когда задание со стратегией Планирование выполнения процессов SCHED_FIFO становится готовым к выполнению, то оно будет продолжать выпол няться до тех пор, пока не заблокируется или пока явно не отдаст управление. Две или более задач с одинаковым приоритетом, имеющие стратегию планирования SCHED_FIFO, будут планироваться на выполнение по круговому алгоритму (round robin). Если задание, имеющее стратегию планирования SCHED_FIFO, является гото вым к выполнению, то все задачи с более низким приоритетом не могут выполнять ся до тех пор, пока это задание не завершится.

Стратегия SCHED_RR аналогична стратегии SCHED_FIFO, за исключением того, что процесс может выполняться только до тех пор, пока не израсходует предопре деленный ему квант времени. Таким образом, стратегия SCHED_RR — это стратегия SCHED_FIFO с квантами времени, т.е. круговой алгоритм планирования (round-robin) реального времени. Когда истекает квант времени процесса со стратегией планиро вания SCHED_RR, то другие процессы с таким же приоритетом планируются по кру говому алгоритму. Квант времени используется только для того, чтобы переплани ровать выполнение заданий с таким же приоритетом. Так же как в случае стратегии SCHED_FIFO, процесс с более высоким приоритетом сразу же вытесняет процессы с более низким приоритетом, а процесс с более низким приоритетом никогда не смо жет вытеснить процесс со стратегией планирования SCHED_RR, даже если у послед него истек квант времени.

Обе стратегии планирования реального времени используют статические при оритеты. Ядро не занимается расчетом значений динамических приоритетов для за дач реального времени. Это означает, что процесс, работающий в режиме реального времени, всегда сможет вытеснить процесс с более низким значением приоритета.

Стратегии планирования реального времени в операционной системе Linux обе спечивают так называемый мягкий режим реального времени (soft real-time). Мягкий режим реального времени обозначает, что ядро пытается планировать выполнение пользовательских программ в границах допустимых временных сроков, но не всегда гарантирует выполнение этой задачи. В противоположность этому операционные системы с жестким режимом реального времени (hard real-time) всегда гарантируют выполнение всех требований по планированию выполнения процессов в заданных пределах. Операционная система Linux не может гарантировать возможности плани рования задач реального времени. Тем не менее стратегия планирования ОС Linux гарантирует, что задачи реального времени будут выполняться всякий раз, когда они готовы к выполнению. Хотя в ОС Linux и отсутствуют средства, гарантирующие ра боту в жестком режиме реального времени, тем не менее производительность пла нировщика ОС Linux в режиме реального времени достаточно хорошая. Ядро серии 2.6 в состоянии удовлетворить очень жестким временным требованиям.

Приоритеты реального времени лежат в диапазоне от 1 до MAX_RT_PRIO минус 1, По умолчанию значение константы MAX_RT_PRIO равно 100, поэтому диапазон зна чений приоритетов реального времени по умолчанию составляет от 1 до 99. Это пространство приоритетов объединяется с пространством значений параметра nice для стратегии планирования SCHED_OTHER, которое соответствует диапазону прио ритетов от значения MAX_RT_PRIO до значения (MAX_RT_PRIO+40). По умолчанию это означает, что диапазон значений параметра nice от -20 до +19 взаимно однознач но отображается в диапазон значений приоритетов от 100 до 139.

90 Глава Системные вызовы для управления планировщиком Операционная система Linux предоставляет семейство системных вызовов для управления параметрами планировщика. Эти системные вызовы позволяют мани пулировать приоритетом процесса, стратегией планирования и процессорной при вязкой, а также предоставляют механизм, с помощью которого можно явно передать процессор (yield) в использование другим заданиям.

Существуют различные книги, а также дружественные страницы системного руко водства (man pages), которые предоставляют информацию об этих системных вызо вах (реализованных в библиотеке С без особых интерфейсных оболочек, а прямым вызовом системной функции). В табл. 4.3 приведен список этих функций с кратким описанием. О том, как системные вызовы реализованы в ядре, рассказывается в гла ве 5, "Системные вызовы".

Таблица 4.3. Системные вызовы для управления планировщиком Системный вызов Описание nice () Установить значение параметра nice schedsetscheduler () Установить стратегию планирования sched_getscheduler () Получить стратегию планирования sched_setparam () Установить значение приоритета реального времени sched_getparam () Получить значение приоритета реального времени sched_get_priority_max () Получить максимальное значение приоритета реального времени Eched_get_priority_min () Получить минимальное значение приоритета реального времени sched_rr_get_interval () Получить продолжительность кванта времени sched_setaffinity() Установить процессорную привязку sched_getaffinity () Получить процессорную привязку sched_yield () Временно передать процессор другим заданиям Системные вызовы, связанные с управлением стратегией и приоритетом Системные вызовы sched_setscheduler () и sched_getcheduler () позволяют соответственно установить и получить значение стратегии планирования и приори тета реального времени для указанного процесса. Реализация этих функций, так же как и для большинства остальных системных вызовов, включает большое количество разнообразных проверок, инициализаций и очистку значений аргументов. Полезная работа включает в себя только чтение или запись полей policy и rt _pri ori t y структуры task_struct указанного процесса.

Системные вызовы sched_setparam () и sched_getparam () позволяют устано вить и получить значение приоритета реального времени для указанного процесса.

Последняя функция просто возвращает значение поля rt_priority, инкапсулиро ванное в специальную структуру sched_param. Вызовы sched_get_priority_max () Планирование выполнения процессов и sched_get_prioritymin () возвращают соответственно максимальное и мини мальное значение приоритета реального времени для указанной стратегии планиро вания. Максимальное значение приоритета для стратегий планирования реального времени равно (MAX_USER_RT_PRIO-1), а минимальное значение - 1.

Для обычных задач функция nice () увеличивает значение статического приори тета вызывающего процесса на указанную в аргументе величину. Только пользователь root может указывать отрицательные значения, т.е. уменьшать значение параметра nice и соответственно увеличивать приоритет. Функция nice () вызывает функцию ядра set_user_nice (), которая устанавливает значение полей stati c_pri a и prio структуры task_struct.

Системные вызовы управления процессорной привязкой Планировщик ОС Linux может обеспечивать жесткую процессорную привязку (processor affinity). Хотя планировщик пытается обеспечивать мягкую или естествен ную привязку путем удержания процессов на одном и том же процессоре, он также позволяет пользователям сказать: "Эти задания должны выполняться только на ука занных процессорах независимо ни от чего". Значение жесткой привязки хранится в виде битовой маски в поле cpus_allowed структуры task_struct. Эта битовая маска содержит один бит для каждого возможного процессора в системе. По умол чанию все биты установлены в значение 1, и поэтому процесс потенциально может выполняться на всех процессорах в системе. Пользователь с помощью функции sched_setaffinity () может указать другую битовую маску с любой комбинацией установленных битов. Аналогично функция sched_getaffinity () возвращает тку щее значение битовой маски cpus_allowed.

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

Поскольку родительский процесс выполняется на дозволенном процессоре, то и по рожденный процесс также будет выполняться на дозволенном процессоре. Во-вто рых, когда привязка процесса изменяется, ядро использует миграционные потоки (mig ration threads) для проталкивания задания на дозволенный пессор. Следовательно, процесс всегда выполняется только на том процессоре, которому сроцоответствует у новленный бит в поле cpus_allowed дескриптора процесса.

Передача процессорного времени Операционная система Linux предоставляет системный вызов sched_yield () как механизм, благодаря которому процесс может явно передать процессор под управление другим ожидающим процессам. Этот вызов работает путем удаления процесса из активного массива приоритетов (где он в данный момент находится, потому что процесс выполняется) с последующим помещением этого процесса в ис текший массив. Получаемый аффект состоит не только в том, что процесс вытес няется и становшея последним в списке заданий с соответствующим приоритетом, а также в том, что помещение процесса в истекший массив гарантирует, что этот процесс не будет выполняться некоторое время. Так как задачи реального времени никогда не могут быть помещены в истекший массив, они составляют специальный случай. Поэтому они только перемещаются в конец списка заданий с таким же зна чением приоритета (и не помещаются в истекший массив). В более ранних версиях Глава ОС. Linux семантика вызова sched_yield () была несколько иной. В лучшем случае задание только лишь перемещалось в конец списка заданий с данным приоритетом.

Сегодня для пользовательских программ и даже для потоков пространства ядра должна быть полная уверенность в том, что действительно необходимо отказаться от использования процессора, перед тем как ввязывать функцию sched_yield ().

В коде ядра, для удобства, можно вызывать функцию yi el d (), которая прове ряет, что состояние задачи равно TASK_RUNNING, а после этого вызывает функцию sched_yield (). Пользовательские программы должны использовать системный вы зов sched_yield ().

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

Проблемы, которые остались, включают возможность точной настройки (или даже полную замену) алгоритма оценки степени интерактивности задания, который приносит много пользы, когда работает правильно, и приносит много неудобств, когда выполняет предсказания неверно. Работа над альтернативными реализациями продолжается. Когда-нибудь мы увидим новую реализацию в основном ядре.

Улучшение поведения планировщика для NUMA систем (систем с неоднородным доступом к памяти) становится все более актуальной задачей, так как количество ма шин на основе NUMA-платформ возрастает. Поддержка доменов планирования (schedu ler domain) — абстракция, которая позволяет описать топологию процессов

;

она была включена в ядро 2.6 в одной из первых версий.

Эта глава посвящена теории планирования процессов, а также алгоритмам и специфической реализации планировщика ядра Linux. В следующей главе будет рас смотрен основной интерфейс, который предоставляется ядром для выполняющихся процессов, — системные вызовы.

Планирование выполнения процессов Системные вызовы дро операционной системы предоставляет набор интерфейсов, благодаря ко торым процессы, работающие в пространстве пользователя, могут взаимодей Я ствовать с системой. Эти интерфейсы предоставляют пользовательским програм мам доступ к аппаратному обеспечению и другим ресурсам операционной системы.

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

Системные вызовы являются прослойкой между аппаратурой и процессами, ра ботающими в пространстве пользователя. Эта прослойка служит для трех главных целей. Во-первых, она обеспечивает абстрактный интерфейс между аппаратурой и пространством пользователя. Например, при записи или чтении данных из файла прикладным программам нет дела до типа жесткого диска, до среды, носителя ин формации, и даже до типа файловой системы, на которой находится файл. Во-вто рых, системные вызовы гарантируют безопасность и стабильность системы. Так как ядро работает посредником между ресурсами системы и пространством пользовате ля, оно может принимать решение о предоставлении доступа в соответствии с пра вами пользователей и другими критериями. Например, это позволяет предотвратить возможность неправильного использования аппаратных ресурсов программами, во ровство каких-либо ресурсов у других программ, а также возможность нанесения вреда системе. И наконец, один общий слой между пространством пользователя и остальной системой позволяет осуществить виртуальное представление процессов, как обсуждается в главе 3, "Управление процессами".

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

;

они являются единственной законной точкой входа в ядро. Другие интер фейсы ядра, такие как файлы устройств или файлы на файловой системе /ргос, в конечном счете сводятся к обращению через системные вызовы.

Интересно, что в ОС Linux реализовано значительно меньше системных вызо вов, чем во многих других операционных системах1.

В этой главе рассказывается о роли и реализации системных вызовов в операци онной системе Linux.

API, POSIX и библиотека С Обычно прикладные программы не разрабатываются с непосредственным ис пользованием системных вызовов, при этом используются программные интерфей сы приложений (Application Programing Interface, API). Это является важным, так как в таком случае нет необходимости в корреляции между интерфейсами, которые используют приложения, и интерфейсами, которые предоставляет ядро. Различные API определяют набор программных интерфейсов, которые используются приложе ниями. Эти интерфейсы могут быть реализованы с помощью одного системного вы зова, нескольких системных вызовов, а также вообще без использования системных вызовов. В действительности, может существовать один и тот же программный ин терфейс приложений для различных операционных систем, в то время как реализа ция этих API может для разных ОС существенно отличаться.

Один из наиболее популярных программных интерфейсов приложений в мире Unix-подобных систем базируется на стандарте POSIX. Технически стандарт POSIX включает в себя набор стандартов IЕЕЕ2, целью которого является обеспечение пе реносимого стандарта операционной системы, приблизительно базирующегося на ОС Unix. ОС Linux соответствует стандарту POSIX.

Стандарт POSIX является хорошим примером соотношения между интерфейсом API и системными вызовами. Для большинства Unix-подобных операционных си стем вызовы интерфейса API, определенные в стандарте POSIX, сильно коррелиру ют с системными вызовами. Конечно, стандарт POSIX создавался для того, чтобы сделать те интерфейсы, которые предоставляли ранние версии ОС Unix, похожими между собой. С другой стороны, некоторые операционные системы, далекие от OS Unix, такие как Windows NT, предоставляют библиотеки, совместимые со стандар том POSIX.

Частично интерфейс к системным вызовам в операционной системе Linux, так же как и в большинстве Unix-систем, обеспечивается библиотекой функций на язы ке С. Библиотека С реализует главный программный интерфейс приложений для Unix-систем, что включает стандартную библиотеку языка программирования С и интерфейс системных вызовов. Библиотека С используется всеми программами, на писанными на языке программирования С, а также, в связи со свойствами языка С, может быть легко использована для программ, написанных на других языках про граммирования.

Дли аппаратной платформы x86 существует около 250 системных вызовов (для каждой аппаратной платформы разрешается определять свои уникальные системные вызовы). Хотя не для всех опера ционных систем опубликованы действительные системные вызовы, но по оценкам для некоторых операционных систем таких вызовов более тысячи.

IEEE, eye-trple-E (Институт инженеров по электротехнике и радиоэлектронике, Institute of Electrical and Electronics Engineers) является бесприбыльной профессиональной ассоциацией, дей ствующей во многих технических областях и отвечающей за многие важные стандарты, такие как стандарт POSIX. Больше информации доступно по адресу: ht t p: //wwn. i eee. org.

96 Глава -> вызов pri ntf () pri rt f () в библиотеке С -> write() в библиотеке С Системный вызов write() Приложение > Библиотека С > Ядро Рис. 5.1. Взаимоотношения между приложением, библиотекой С и ядром на примере вызова функции p r i nt f ( ) Дополнительно библиотека функций С также представляет большую часть API стандарта POSIX.

С точки зрения прикладного программиста, системные вызовы не существенны:

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

Общий девиз для интерфейсов ОС Unix — это "предоставлять механизм, а не стратегию". Другими словами, системные вызовы существуют для того, чтобы обе спечить определенную функцию в наиболее абстрактном смысле. А то, каким обра зом используется эта функция, ядра не касается.

Вызовы syscall Системные вызовы (часто называемые syscall в ОС Linux) обычно реализуются в виде вызова функции. Для них могут быть определены один или более аргументов (inputs), которые могут приводить к тем или иным побочным эффектам3, например к записи данных в файл или к копированию некоторых данных в область памяти, на которую указывает переданный указатель. Системные вызовы также имеют возвра щаемое значение типа long4, которое указывает на успешность выполнения опера ции или на возникшие ошибки. Обычно, но не всегда, возвращение отрицательного значения указывает на то, что произошла ошибка. Возвращение нулевого значения обычно (но не всегда) указывает на успешность выполнения операции. Системные вызовы ОС Unix в случае ошибки записывают специальный код ошибки в глобаль ную переменную errno. Значение этой переменной может быть переведено в удобо читаемую формy с помощью библиотечной функции perror ().

Системные вызовы, конечно, имеют определенное поведение. Например, систем ный вызов getpi d () определен для того, чтобы возвращать целочисленное значе ние, равное значению идентификатора PID текущего процесса. Реализация этой функции в ядре очень проста.

Следует обратить внимание на слово "могут". Хотя почти вге вызовы создают различные побоч ные эффекты (т.е. приводят к каким-либо иэменениям в состоянии системы), тем не менее неболь шое количество вызовов, как, например, вызов getpid (), просто возвращают некоторые данные ядра.

Тип long используется для совместимости с 64-разрядными платформами.

Системные вызовы asmlinkage long sys_getpid(void) { return current->tgid

;

) Следует заметить, что в определении ничего не говорится о способе реализации.

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

Даже из такого примера можно сделать пару наблюдений, которые касают ся системных вызовов. Во-первых, следует обратить внимание на модификатор asmlinkage в объявлении функции. Это волшебное слово дает компилятору инфор мацию о том, что обращение к аргументам этой функции должно производиться только через стек. Для всех системных вызовов использование этого модификатора является обязательным. Во-вторых, следует обратить внимание, что системный вы зов getpid () объявлен в ядре, как sys_getpid (). Это соглашение о присваивании имен используется для всех системных вызовов операционной системы Linux: си стемный вызов bar () должен быть реализован с помощью функции sys_bar ().

Номера системных вызовов Каждому системному вызову операционной системы Linux присваивается номер системного вызова (syscall number). Этот уникальный номер используется для обращения к определенному системному вызову. Когда процесс выполняет системный вызов из пространства пользователя, процесс не обращается к системному вызову по имени.

Номер системного вызова является важным атрибутом. Однажды назначенный номер не должен меняться никогда, иначе это нарушит работу уже скомпилирован ных прикладных программ. Если системный вызов удаляется, то соответствующий номер не может использоваться повторно. В операционной системе Linux предусмо трен так называемый "не реализованный" ("not implemented") системный вызов — функция sys_ni_syscall (), которая не делает ничего, кроме того, что возвращает значение, равное -ENOSYS, — код ошибки, соответствующий неправильному систем ному вызову. Эта функция служит для "затыкания дыр" в случае такого редкого со бытии, как удаление системного вызова.

Ядро поддерживает список зарегистрированных системных вызовов в таблице системных вызовов. Эта таблица хранится в памяти, на которую указывает перемен ная sys_call_table. Данная таблица зависит от аппаратной платформы и обычно определяется в файле entry.S. В таблице системных вызовов каждому уникальному номеру системного вызова назначается существующая функция syscall.

Может быть, интересно, почему вызов get pi d () возвращает поле tgid, которое является иден тификатором группы потоков (thread group ID)? Это делается потому, что дли обычных процессов значение параметра TGID равно значению параметра PID. При наличии нескольких потоков зна чение параметра TGID одинаково дли всек потоков одной группы. Такая реализация дает возмож ность различным потокам вызывать функцию getpid () и получать одинаковое значение параме тра PID.

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

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

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

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

Обработчик исключительной ситуации в данном случае и является обработчиком си стемного вызова (system call handler). Для аппаратной платформы х8б это программ ное прерывание определено как машинная инструкция i nt $0x80. Она приводит в действие механизм переключения в режим ядра и выполнение вектора исключи тельной ситуации с номером 128, который является обработчиком системных вы зовов. Обработчик системных вызовов— это функция с очень подходящим именем system_call (). Данная функция зависима от аппаратной платформы и определена в файле entry.S. В новых процессорах появилась такая новая функция, как sysent er. Эта функция обеспечивает более быстрый и специализированный способ входа в ядро для выполнения системного вызова, чем использование инструкции программ ного прерывания — i nt. Поддержка такой функции была быстро добавлена в ядро.

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

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

Большая часть дальнейшего описания процесса обработки системных вызовов базируется на вер сии для аппаратной платформы x86. Но не стоит волноваться, для других аппаратных платформ это выполняется аналогичным образом.

Системные вызовы Для аппаратной платформы х86 номер системного вызова сохраняется в регистре процессора еах перед тем, как вызывается программное прерывание. Обработчик системных вызовов после этого считывает это значение из регистра еах. Для других аппаратных платформ выполняется нечто аналогичное.

Функция system_call() проверяет правильность переданного номера системно го вызова путем сравнения его со значением постоянной NR_syscalls. Если значе ние номера больше или равно значению NR_syscalls, то функция возвращает зна чение -ENOSYS. В противном случае вызывается соответствующий системный вызов следующим образом:

call *sys_call_table(,%eax,4) Так как каждый элемент таблицы системных вызовов имеет длину 32 бит (4 байт), то ядро умножает данный номер системного вызова на 4 для получения нужной по зиции в таблице системных вызовов (рис. 5.2).

Вызов функции Оболочка функции Вызов функции Вызов функции read() read() system_call() sys_read() Приложение Оболочка Обработчик Функция функции read () Системных sys_read() в библиотеке С вызовов Пространство пользователя Пространство ядра Рис. 5.2. Запуск обработчика системных вызовов и выполнение системного вызова Передача параметров В дополнение к номеру вызова, большинство системных вызовов требует пере дачи им одного или нескольких параметров. Во время перехвата исключительной ситуации пространство пользователя должно каким-либо образом передать ядру эти параметры. Самый простой способ осуществить такую передачу — это сделать по аналогии с передачей номера системной функции: параметры хранятся в регистрах процессора. Для аппаратной платформы х86 регистры ebx, ecx, edx, esi, edi со держат соответственно первые пять аргументов. В случае редких ситуаций с шестью или более аргументами, используется один регистр, который содержит указатель на память пространства пользователя, где хранятся все параметры.

Возвращаемое значение также передается в пространство пользователя через ре гистр. Для аппаратной платформа х86 оно хранится в регистре еах.

100 Глава Реализация системных вызовов Реализация системного вызова в ОС Linux не связана с поведением обработчика системных вызовов. Добавление нового системного вызова в операционной системе Linux является сравнительно простым делом. Тяжелая работа связана с разработкой и реализацией самого системного вызова. Регистрация его в ядре проста. Давайте рассмотрим шаги, которые необходимо предпринять, чтобы написать новый систем ный вызов в операционной системе Linux.

Первый шаг в реализации системного вызова — это определение его назначения, т.е. что он должен делать. Каждый системный вызов должен иметь только одно на значение. Мультиплексные системные вызовы (один системный вызов, который выполняет большой набор различных операций, в зависимости от значения флага, передаваемого в качестве аргумента) в операционной системе Linux использовать не рекомендуется. Для примера того, как не надо делать, можно обратиться к системной функции i oct l ().

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

Важным является разработка интерфейса с прицелом на будущее. Не ограниче ны ли возможности функции без необходимости? Разрабатываемый системный вы зов должен быть максимально общим. Не нужно полагать, что завтра он будет ис пользоваться так же, как сегодня. Назначение системного вызова должно оставаться постоянным, но его использование может меняться. Является ли системный вызов переносимым? Не нужно делать допущений о возможном размере машинного слова или порядка следования байтов. В главе 19, "Переносимость", рассматриваются со ответствующие вопросы. Нужно удостовериться, что никакие неверные допущения не будут мешать использованию системного вызова в будущем. Помните девиз Unix:

"Обеспечивать механизм, а не стратегию".

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

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

Например, системные вызовы для файлового ввода-вывода данных должны про верить, является ли значение файлового дескриптора допустимым. Функции, свя занные с управлением процессами, должны проверить, является ли значение пере данного идентификатора PID допустимым. Каждый параметр должен проверяться не только на предмет допустимости и законности, но и на предмет правильности значения.

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

• Указатель указывает на область памяти в пространстве пользователя. Нельзя, чтобы процесс заставил ядро обратиться к памяти ядра от имени процесса.

• Указатель указывает на область памяти в адресном пространстве текущего про цесса. Нельзя позволять, чтобы процесс заставил ядро читать данные других процессов.

• Для операций чтения есть права на чтение области памяти. Для операций за писи есть права на запись области памяти. Нельзя, чтобы процессы смогли обойти ограничения на чтение и запись.

Ядро предоставляет две функции для выполнения необходимых проверок при копировании данных в пространство пользователя и из него. Следует помнить, что ядро никогда не должно слепо следовать за указателем в пространстве пользователя!

Одна из этих двух функций должна использоваться всегда.

Для записи в пространство пользователя предоставляется функция copy_to_user ().

Она принимает три параметра: адрес памяти назначения в пространстве пользовате ля

;

адрес памяти источника в пространстве ядра

;

и размер данных, которые необхо димо скопировать, в байтах.

Для чтения из пространства пользователя используется функция copy_from_user (), которая аналогична функции copy_to_user (). Эта функция считывает данные, на которые указывает второй параметр, в область памяти, на которую указывает пер вый параметр, количество данных — третий параметр.

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

В случае такой ошибки стандартным является возвращение системным вызовом зна чения -EFAULT.

Давайте рассмотрим пример системного вызова, который использует функции copy_from_user () и copy_to_user (). Системный вызов silly_copy () является до крайности бесполезным. Он просто копирует данные из своего первого параме тра во второй. Это очень не эффективно, так как используется дополнительное про межуточное копирование в пространство ядра безо всякой причины. Но зато это позволяет проиллюстрировать суть дела.

/* * Системный вызов silly copy — крайне бесполезная функция, * которая копирует len байтов иэ области памяти, * на которую указывает параметр src, в область памяти, * на которую указывает параметр dst, с использованием ядра * безо всякой на то причины. Но это хороший пример!

*/ asmlinkage long sys_silly_copy(unsigned long *src, unsigned long *dst, unsigned long len) } 102 Глава unsigned long buf

;

/* возвращаем ошибку, если размер машинного слова в ядре не совпадает с размером данных, переданных пользователем */ if (len != sizeof(buf)) return -EINVAL

;

/* копируем из src, который является адресом в пространстве пользователя, в buf */ if (copy_from_user (&buf, src, len)) return -EFAULT

;

/* копируем из buf в dst, который гоже является адресом в пространстве пользователя */ if (copy_to_user (dst, &buf, len) ) return -EFAULT

;

/* возвращаем количество скопированных данных */ return len

;

} Следует заметить, что обе функции, copy_from_user () и copy_to_user (), мо гут блокироваться. Это возникает, например, если страница памяти, содержащая дан ные пользователя, не находится в физической памяти, а в данный момент вытеснена на диск. В таком случае процесс будет находиться в приостановленном состоянии до тек пор, пока обработчик прерываний из-за отсутствия страниц (page fault handler) не возвратит страницу памяти в оперативную память из файла подкачки на диске.

Последняя проверка — это проверка на соответствие правам доступа. В старых версиях ядра Linux стандартом было использование функции suser () для систем ных вызовов, которые требуют прав пользователя root. Эта функция просто про веряла, запущен ли процесс от пользователя root. Сейчас эту функцию убрали и заменили более мелко структурированным набором системных "возможностей ис пользования" (capabilities). В новых системах предоставляется возможность прове рять специфические права доступа к специфическим ресурсам. Функция capable () с допустимым значением флага, определяющего тип прав, возвращает ненулевое зна чение, если пользователь обладает указанным правом, и нуль— в противном случае.

Например, вызов capable (CAP_SYS_NICE) проверяет, имеет ли вызывающий про цесс возможность модифицировать значение параметра nice других процессов. По умолчанию суперпользователь владеет всеми правами, а пользователь, не являющий ся пользователем root, не имеет никаких дополнительных прав. Следующий пример системного вызова, который демонстрирует использование возможностей использо вания, тоже является практически бесполезным.

asmlinkage long sys_am_i_popular (void) { /* Проверить, имеет пи право процесс использовать возможность CAP_SYS_NICE */ if (!capable(CAP_SYS_NICE)) return -EPERM

;

/* Возвратить нуль, чтобы обозначить успешное завершение */ return 0

;

} Список всех "возможностей использования" и прав, которые за ними закрепле ны, содержится в файле .

Системные вызовы Контекст системного вызова Как уже обсуждалось в главе 3, "Управление процессами", при выполнении си стемного вызова ядро работает в контексте процесса. Указатель current указывает на текущее задание, которое и есть процессом, выполняющим системный вызов.

В контексте процесса ядро может переходит в приостановленное состояние (на пример, если системный вызов блокируется при вызове функции или явно вызывает функцию schedule ()), а также является полностью вытесняемым. Эти два момента важны. Возможность переходить в приостановленное состояние означает, что си стемный вызов может использовать большую часть функциональных возможностей ядра. Как будет видно из главы 6, "Прерывания и обработка прерываний", наличие возможности переходить в приостановленное состояние значительно упрощает про граммирование ядра. Тот факт, что контекст процесса является вытесняемым, под разумевает, что, как и в пространстве пользователя, текущее задание может быть вытеснено другим заданием. Так как новое задание может выполнить тот же систем ный вызов, необходимо убедиться, что системные вызовы являются реентерабель ными. Это очень похоже на требования, выдвигаемые для симметричной мультипро цессорной обработки. Способы защиты, которые обеспечивают реентерабельность, описаны в главе 8, "Введение в синхронизацию выполнения кода ядра", и в главе 9, "Средства синхронизации в ядре".

После завершение системного вызова управление передается обратно в функцию system_call (), которая в конце концов производит переключение в пространство пользователя, и далее выполнение пользовательского процесса продолжается.

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

• Добавляется запись в конец таблицы системных вызовов. Это необходимо сде лать для всех аппаратных платформ, которые поддерживают этот системный вызов (для большинства системных вызовов — это все возможные платформы).

Положение системного вызова в таблице — это номер системного вызова, на чиная с нуля. Например, десятая запись таблицы соответствует системному вы зову с номером девять.

• Для всех поддерживаемых аппаратных платформ номер системной функции должен быть определен в файле include/linux/unistd.h.

• Системный вызов должен быть вкомпилирован в образ ядра (в противополож ность компиляции в качестве загружаемого модуля ). Это просто соответствует размещению кода в каком-нибудь важном файле каталога kernel/.

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

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

104 Глава Давайте более детально рассмотрим эти шаги на примере функции системного вызова, fоо (). Вначале функция sys_fоо () должна быть добавлена в таблицу си стемных вызовов. Для большинства аппаратных платформ таблица системных вызо вов размещается в файле entry.S и выглядит примерно следующим образом.

ENTRY(sys_call_table).long sys_restart_syscall /* 0 */.long sys_exit.long sys_fork.long sys_read.long sys_write.long sys_open /* 5 */...

.long sys_timer_delete.long sys_clock_settime.long sys_clock_gettime /* 280 */.long sys_clock_getres.long sys_clock_nanosleep Необходимо добавить новый системный вызов в конец этого списка:

.long sys_foo Нашему системному вызову будет назначен следующий свободный номер, 283, котя мы этого явно и не указывали. Для каждой аппаратной платформы, которую мы будем поддерживать, системный вызов должен быть добавлен в таблицу систем ных вызовов соответствующей аппаратной платформы (нет необходимости получать номер системного вызова для каждой платформы). Обычно необходимо сделать си стемный вызов доступным для всех аппаратных платформ. Следует обратить внима ние на договоренность указывать комментарии с номером системного вызова через каждые пять записей, что позволяет быстро найти, какой номер какому системному вызову соответствует.

Далее необходимо добавить номер системного вызова в заголовочный файл include/asm/unistd.h, который сейчас выглядит примерно так.

/* * This file contains the system call numbers.

*/ #define NR_restart_syscall #define NR_exit #define NR_fork #define NR_read #define HR_write #define NR_open...

#define NR_mq_unlink #define NR_mq_timedsend #define NR_mq_timedreceive #define NR_mq_notify #define NR_mq_getsetattr Системные вызовы В конец файла добавляется следующая строка.

#define NR_foo В конце концов необходимо реализовать сам системный вызов fоо (). Так как си стемный вызов должен быть вкомпилорован в образ ядра во всех конфигурациях, мы его поместим в файл kernel/sys.с. Код необходимо размещать в наиболее под ходящем файле. Например, если функция относится к планированию выполнения процессов, то ее необходимо помещать в файл sched.с.

/* * sys_foo - всеми любимый системный вызов.

* * Возвращает размер стека ядра процесса */ asmlinkage long sys_foo(void) { return THREAD_SIZE

;

} Это все! Загрузите новое ядро. Теперь из пространства пользователя можно вы звать системную функцию foo ().

Pages:     | 1 || 3 | 4 |   ...   | 10 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.