netlib.narod.ru | < Назад | Оглавление | Далее > |
В предыдущем разделе кратко упоминалось о том, что процессы (т.е. выполняющиеся программы) могут быть переведены в состояние ожидания определенного события («спящее» состояние) и выведены из этого состояния после прихода события. Реализованная в ядре технология заключается в связывании с каждым событием очереди ожидания (wait queue). Процесс, который должен получить событие, переводится в режим ожидания и помещается в очередь. После прихода события ядро сканирует очередь и активизирует ожидающие задания. Проблема удаления из очереди находится полностью в компетенции заданий.
Очереди ожидания — на удивление мощный механизм, используемый повсеместно во всем ядре. К тому же реализация этого механизма требует не столь уж много кода.
18662: Эта простая структура данных хранит узел очереди ожидания. Она имеет всего лишь два члена:
Как правило, очередь ожидания представляется указателем на первый элемент (т.е. голову списка). Посмотрите на log_wait (строка 25647), где очередь ожидания используется в printk.
16840: При помощи этого макроса код ядра переводит текущий выполняемый процесс в режим ожидания в очереди wq до тех пор, пока не будет удовлетворено заданное условие condition (которое может быть произвольным выражением).
16842: Если условие истино, процесс не должен ожидать.
16844: В противном случае процесс будет находиться в состоянии ожидания до тех пор, пока условие не станет истинным. Это завершается обращением к __wait_event (строка 16824). Поскольку __wait_event отделено от wait_event, фрагменты кода ядра, которым известно, что условие ожидания ложно, могут вызывать __wait_event напрямую, а не через макрос. В данном случае макрос выполняет избыточную проверку. Если условие истино, wait_event пропускает код, помещающий процесс в очередь ожидания.
Код wait_event заключен в несколько необычную конструкцию:
do { /* . . . */ } while (0)
Этот небольшой трюк не настолько хорошо известен, как он того заслуживает. Идея состоит в том, чтобы заставить помещенный внутрь конструкции код действовать подобно одному оператору. Рассмотрим следующий макрос, который вызывает free, если p является ненулевым указателем:
#define FREE1(p) if (p) free(p)
Все хорошо до тех пор, пока FREE1 не будет задействован в ситуации наподобие:
if (expression) FREE1(p); else printf ("expression ложно.\n");
После разворачивания макроса FREE1 конструкция else ассоциируется не с тем if (т.е. с if относящимся к FREE1).
Я наблюдал, как некоторые программисты так решали подобную проблему:
#define FREE2(p) if (p) { free(p); } #define FREE3(p) { if (p) { free(p); } }
Ни одно из вышеприведенных решений нельзя считать удовлетворительным — точка с запятой, которую программист естественно ставит после вызова макроса, портит разворачиваемый текст. Возьмите, к примеру, FREE2. После разворачивания макроса и добавления отступов для лучшего чтения компилятор получит такой текст:
if (expression) if (p) { free(p); } ; else printf("expression ложно.\n");
Результат — синтаксическая ошибка, поскольку else не связан ни с одним if. Аналогичная проблема имеет место и с FREE3. После некоторых размышлений становится ясно, что абсолютно неважно, есть ли if внутри тела макроса. Ту же проблему можно получить и в результате заключения тела макроса в скобки, причем неважно, что находится внутри макроса.
Вот почему применяется трюк с do/while(0). Посмотрите на макрос FREE4, свободный от описанных выше проблем:
#define FREE4(p) \ do { \ if (p) \ free(p); \ } while (0)
После помещения макроса в тот же самый код и его разворачивания получается:
if (expression) do { if (p) free(p); } while (0); /* ";" после макроса. */ else printf("expression ложно.\n");
Разумеется, такой код работает корректно. Компилятор выполнит оптимизацию и минимизирует накладные расходы для этого поддельного цикла, поэтому никаких потерь в скорости не будет, а макрос будет работать именно так, как необходимо.
Перед завершением обсуждения данной проблемы нельзя не заметить, что даже несмотря на приемлимость последнего решения, написание функций гораздо лучше написания макросов. Если накладные расходы на вызов функции непозволительны (что имеет место для ядра, но в гораздо меньшей степени для других задач), воспользуйтесь inline-функциями. (Последнее доступно только в компиляторе С++, gcc или в компиляторах, поддерживающих последний стандарт ISO C, где добавлены inline-функции.)
16824: __wait_event переводит процесс в режим ожидания в очереди wq до тех пор, пока condition не станет истинным.
16829: Локальная переменная __wait связывается с очередью через вызов add_wait_queue (строка 16791). Следует отметить, что __wait хранится на стеке, а не в куче ядра — это один из типичных трюков, примененных в ядре. __wait удаляется из очереди ожидания перед завершением макроса, так что указатель на __wait в очереди оказывается всегда допустимым.
16830: Циклическая передача управления другим процессам до тех пор, пока не будет удовлетворено условие (см. ниже).
16831: Процесс переводится в состояние TASK_UNINTERRUPTIBLE (строка 16190), которое означает, что процесс находится в режиме ожидания и не должен из него выходить даже по сигналу. Сигналы описываются в главе 6, а состояния процессов — в главе 7.
16832: Если условие удовлетворяется, цикл может быть завершен.
Заметьте, что если условие истинно при первом проходе цикла, присваивание в предыдущей строке оказывается излишним, поскольку установка состояния процесса выполняется еще раз после завершения цикла. Однако, __wait_event исходит из предположения, что к моменту начала выполнения макроса условие еще не удовлетворено. Нет ничего вредного в некоторой задержке установки переменной состояния процесса. В очень редких случаях может оказаться так, что условие было ложным в начале выполнения __wait_event и истинным в момент достижения строки 16832. Такое изменение приведет к проблеме лишь в случае, если condition оценивается в фрагменте кода, где важную роль играет то, в какое состояние переходит процесс. Ничего подобного в коде ядра я не встречал.
16834: Вызов schedule (строка 26686, см. главу 7) для переключения ЦП на другой процесс. Возврат из вызова schedule не будет осуществлен до тех пор, пока процесс не получит ЦП вновь — это произойдет только после активизации процесса из очереди ожидания.
16836: Цикл завершен, так что условие удовлетворено. Процесс переводится в состояние TASK_RUNNING (строка 16188) и он готов продолжать свое выполнение.
16837: Процесс удаляется из очереди ожидания путем вызова remove_wait_queue (строка 16814).
wait_event_interruptible и __wait_event_interruptible (соответственно, строки 16868 и 16847) похожи на wait_event и __wait_event за тем лишь исключением, что они разрешают прерывание ожидающего процесса по сигналу. (Описание сигналов находится в главе 6.)
Кроме того, wait_event_interruptible заключено в скобки:
({ /* . . . */ })
Подобно трюку с do/while(0), показанное выше заставляет заключенный внутри код действовать как один модуль. Здесь заключенный внутри код — это одиночное выражение, а не оператор, т.е. он оценивается как одно значение, которое может участвовать в больших выражениях. Причина применения трюка связана со своеобразной магией непереносимости в gcc, когда последнее вычисленное в таком блоке выражение делается значением целого блока. В случае использования wait_event_interruptible в выражении, тело макроса выполняется и значением макроса будет значение __ret (см. строку 16873). С подобной концепцией хорошо знакомы программирующие на Lisp, однако она может показаться обескураживающей для тех, кто имел дело только с С и ему подобными процедурными языками.
26829: Это функция, активизирующая процессы, которые находятся в очереди ожидания. Она вызывается в wake_up и wake_up_interruptable (соответственно, строки 16612 и 16614). Эти макросы воспринимают параметр mode; процессы будут активизироваться только в случае, если они находятся в состояниях, определяемых mode.
26833: Как будет более подробно поясняться в главе 10, блокировки используются для защиты доступа к ресурсам; они особенно важны в версии SMP, когда один ЦП изменяет данные, которые в это же время читает другой ЦП, или, скажем, оба ЦП пытаются одновременно модифицировать одни и те же данные. В нашем случае совершенно очевидным ресурсом, который необходимо защитить, будет очередь ожидания. Что самое интересное, так это то, что все очереди ожидания защищаются одной и той же блокировкой. Поступать таким образом оказывается гораздо проще, нежели определять собственную блокировку для каждой очереди, однако в версии SMP может случиться так, что система будет ожидать блокировки в момент, когда это совершенно противопоказано.
26838: Проход по непустой очереди и вызов wake_up_process (строка 26356) для каждого ожидающего процесса с подходящим состоянием. Как утверждалось ранее, в этом месте процессы (узлы очереди) из очереди не удаляются, в основном, потому, что конкретный процесс может требовать оставаться в очереди, несмотря на активизацию процессов (это видно в __wait_event).
netlib.narod.ru | < Назад | Оглавление | Далее > |