Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Пробуждение после ожидания или активация
Вспомните, что, когда процесс вызывает fork (), создается новый процесс. Как говорилось ранее, процесс, вызывающий fork (), называется родительским, а новый процесс -дочерним. Новый процесс нужно передать планировщику для того, чтобы он получил доступ к процессору. Это происходит в функции do_f ork (). В функции do_f ork () операции, связанные с пробуждением процесса, выполняют две строчки; copy__process (), вызываемая в строке 1184 linux/kernel/fork, с, вызывает функцию sched_f ork (), которая инициализирует процесс для следующей его вставки в очередь выполнения планировщика; wake_up_f orked_process (), вызываемая в строке 1222 linux/kernel/f ork. с, получает инициализированный процесс 3.6 Слежение за процессом: базовые конструкции планировщика struct runqueue prio_arrayJ *active spinlockj lock taskj *curr, *idle
prlo_arrayJ *expired unsigned long nr_running init best_expired_prio P- prio_array_t *arrays[0] int nr_active unsigned long bitmap [BITMAP_SIZE]
г- prio_array_tarrays[1] int nr active unsigned long bitmap [BITMAP_SIZE]
Рис. 3.12. Массив приоритетов в очереди выполнения и устанавливает его в очередь выполнения. Инициализация и вставка разделяются для того, чтобы новый процесс можно было убить или завершить до того, как он попадет в планировщик. Процесс будет передан планировщику только в том случае, если он будет создан, инициализирован и не будет получать сигналов ожидания. Глава 3 • Процессы: принципиальная модель выполнения 3.6.2.1 sched_fork(): инициализация планировщика для нового ответвленного процесса Функция sched_fork() выполняет настройку инфраструктуры, требуемой планировщику для нового ответвленного процесса: kernel/sched.с 719 void sched_fork(task_t *р) 720 {
721 /* 722 * Здесь мы помечаем процесс выполняемым, но еще не помещаем 723 * в очередь выполнения. Это дает гарантию, что никто не запустит 724 * его на выполнение и что сигнал или другое внешнее 72 5 * событие не сможет его разбудить и вставить в очередь выполнения. 726 */ 727 p-> state = TASKJRUNNING; 728 INIT_LIST_HEAD(& p-> run_list); 729 p-> array = NULL; 730 spin_lock_init(& p-> switch_lock); Строка 727 Процесс обозначается выполняемым с помощью установки атрибута state в структуре задачи в TASK_RUNNING для того, чтобы удостовериться, что в очередь выполнения не добавлены события, а запуск процесса до do__f or k () и copy_process () проверяет, что процесс был создан правильно. Когда проверка завершена, do__f ork () добавляет его в очередь ожидания с помощью wake_up_f orked__process (). Строки 728-730 Инициализация поля run_list-npouecca. Когда процесс активируется, поле run_list связывает вместе очередь структуры массива приоритетов и очередь выполнения. Поле процесса array устанавливается в NULL для того, чтобы обозначить, что он не является частью ни одного из массивов приоритетов очереди выполнения. Следующий блок - sched_fork() со строки 731 до 739 работает с приоритетом обслуживания ядра. (Более подробную информацию об этом можно увидеть в гл. 7.) kernel/sched.с 740 /* 741 * Разделение временного среза между родителем и детьми, при котором 742 * общее количество временного среза, выделенного системой, 743 * не изменяется, обеспечивая более честное планирование. 744 */ 3.6 Слежение за процессом: базовые конструкции планировщика 745 local_irq_disable(); 746 p-> time_slice = (current-> time_slice +1) » 1;
747 /* 748 * Остатки первого временного среза возвращаются 749 * родителю, если дочерний процесс слишком рано закончит работу. 750 */
751 p-> first_time_slice = 1; 752 current-> time_slice »= 1; 753 p-> timestamp = sched_clock(); 754 if (! current-> time_slice) { 755 /* 756 * Это редкий случай, возникающий, когда у родителя остается только 757 * один миг от его временного среза. В этом случае блокировка 758 * очереди выполнения не представляет никаких сложностей. 759 */
760 current-> time_slice = 1; 761 preempt_disable(); 762 scheduler_tick(0, 0); 763 local_irq_enable(); 764 preempt_enable(); 765 } else 766 local__irq_enable (); 767 } Строки 740-753 После отключения локальных прерываний мы делим родительский временной срез между родителем и его детьми, используя оператор сдвига. Первый временной срез нового процесса устанавливается в 1, потому что он еще не выполнялся, а его время создания инициализируется в текущее время в наносекундах. Строки 754-767 Если временной срез родителя равен 1, в результате деления родителю остается время 0 на выполнение. Так как родитель является текущим процессом планировщика, нам понадобится планировщик для выбора нового процесса. Это делается с помощью вызова scheduler_tick() (в строке 762). Приоритет отключается для того, чтобы удостовериться, что планировщик выбирает новый процесс без прерывания. Как только это сделано, мы включаем приоритет и восстанавливаем локальные прерывания. В этой точке новый созданный процесс имеет специфическую для планировщика инициализированную переменную и имеет начальный временной срез, равный половине оставшегося временного среза родительского процесса. Заставляя процесс пожертвовать частью своего времени и передать его дочернему процессу, ядро позволяет процессам не Глава 3 • Процессы: принципиальная модель выполнения отнимать слишком большую часть процессорного времени. Если процессу выделить слишком много времени, нехороший процесс может наплодить множество дочерних и быстро съесть все процессорное время. После успешной инициализации процесса и проверки инициализации do_f ork () вызывает wake_up_forked_process (): kernel/sched.с 922 /* 923 * wake_up_forked__process - будит свежеответвленный процесс. 924 * Эта функция производит некоторые статистические приготовления 92 5 * планировщика, которые необходимо выполнить для каждого нового 92 6 * созданного процесса. 927 */ 928 void fastcall wake_up_forked_process(task_t * p) 929 { 93 0 unsigned long flags; 931 runqueue_t *rq = task_rq_lock(current, & flags); 933 BUG_ON(p-> state! = TASK_RUNNING); 935 /* 93 6 * Мы уменьшаем среднее время сна родительского процесса 937 * и дочернего процесса для предотвращения порождения 938 * сверхинтерактивных задач от сверхинтерактивных задач. 939 */ 940 current-> sleep_avg = JIFFIES_TO_NS(CURRENT_BONUS( current) * 941 PARENT_PENALTY / 100 * MAX_SLEEP_AVG / MAX_BONUS); 943 p-> sleep_avg = JIFFIES_TO_NS (CURRENT_BONUS (p) * 944 CHILD_PENALTY / 100 * MAX_SLEEP_AVG / MAX_BONUS); 946 p-> interactive_credit = 0; 947 948 p-> prio = effective_prio(p); 949 set_task_cpu(p, smp_processor_id()); 950 951 if (unlikely(! current-> array)) 952 __ activate_task(p, rq); 953 else { 954 p-> prio = current-> prio; 955 list_add_tail(& p-> run_list, & current-> run_list); 956 p-> array = current-> array; 957 p-> array-> nr_active++; 958 rq-> nr_running++; 3.6 Слежение за процессом: базовые конструкции планировщика 959 } 960 task__rq__unlock (rq, & flags); 961 } Строки 930-934 Первое, что делает планировщик, - это блокирование структуры очереди ожидания. Любое изменение очереди ожидания производится с заблокированной структурой. Также выдается сообщение об ошибке, если процесс не помечен как TASK_RUNNING. Процесс должен устанавливаться в это состояние в функции sched__f ork () (см. строку 727 в ранее приведенном kernel/sched. с). Строки 940-947 Планировщик вычисляет среднее время сна родительского и дочернего процессов. Значение среднего времени сна показывает, сколько времени процесс проводит в состоянии сна в сравнении с тем, сколько времени он выполняется. Оно увеличивается во время сна процесса и уменьшается каждый тик времени во время выполнения. Интерактивный, или ограниченный вводом-выводом, процесс затрачивает большую часть времени на ожидание ввода и обычно имеют высокое среднее время сна. Неинтерактивные, или ограниченные процессором, процессы тратят большую часть отводимого им времени на выполнение на процессоре вместо ожидания ввода-вывода и имеют низкое время сна. Так как пользователи желают видеть результат из ввода в виде клавиатурных нажатий или перемещений мыши, интерактивные процессы получают от планировщика больше преимуществ, чем неинтерактивные процессы. При этом планировщик принудительно вставляет интерактивный процесс в массив активных приоритетов, после того как их временной срез завершен. Для предотвращения порождения неинтерактивных дочерних процессов от интерактивных и, таким образом, избегания диспропорционального разделения процессора эти формулы используются для понижения среднего времени сна родителя и его детей. Если новый ответвленный процесс является интерактивным, после продолжительного сна он получит достаточный приоритет чтобы наверстать упущенные преимущества. Строка 948 Функция ef f ective_prio () изменяет статический приоритет процесса. Она возвращает приоритет в пределах от 100 до 139 (от MAX_RT_PRIO до MAX_PRI0-1). Статический приоритет процесса может быть изменен до 5 в каждом направлении на основе использования процессора в прошлом и времени, потраченного на сон, но он всегда остается в этих пределах. Из командной строки мы сообщаем о значении nice-процесса, который варьируется и от +19 до -20 (от наименьшего до максимального приоритета). Приоритет nice, равный 0, соответствует статическому приоритету 120. Глава 3 • Процессы: принципиальная модель выполнения Строка 949 Процесс устанавливает свой атрибут процессора в значение текущего процессора. Строки 951-960 В этом блоке кода новый процесс или дочерний процесс копирует информацию для планировщика из родителя, который является текущим, и затем вставляет себя в соответствующее место очереди выполнения. Мы закончили наше изменение очереди сообщении и теперь должны ее разблокировать. Разд. 3.7 и рис. 3.13 освещает этот процесс более подробно.
Рис. 3.13. Вставка в очередь выполнения Указатель array указывает на массив приоритетов в очереди выполнения. Если текущий процесс не указывает на массив приоритетов, это означает, что текущий процесс завершен или спит. В этом случае поле текущего процесса runlist не находится в массиве приоритетов очереди выполнения, что значит, что операция list__add_tail () (в строке 955) провалится. Вместо этого мы вставляем новый созданный процесс, используя __ activeate_task (), которая добавляет новый процесс в очередь без обращения к его родителю. В нормальной ситуации, когда текущий процесс ожидает процессорного времени в очереди выполнения, процесс добавляется в очередь в слот p__prior массива приоритета. Массив, в который добавляется процесс, имеет собственный счетчик процесса 3.6 Слежение за процессом: базовые конструкции планировщика nr_ac tive, который мы увеличиваем, как увеличиваем и собственный счетчик процесса nr_running. Наконец, мы снимаем блокировку с очереди выполнения. В случае, когда текущий процесс не указывает на массив приоритетов и очереди выполнения, полезно видеть, как планировщик управляет очередью выполнения и атрибутами массива приоритетов. kernel/sched.с 3 66 static inline void activate_task(task_t *p, runqueue_t *rq) 367 { 3 68 enqueue_task(p, rq-> active); 3 69 rq-> nr_running++; 370 } __ activate_task () помещает данный процесс р в активный массив приоритетов в очереди выполнения rq и увеличивает поле nr_running, которое является счетчиком общего количества процессов, находящихся в очереди выполнения. kernel/sched.с 311 static void enqueue_task (struct task_struct *p, prio_array_t *array) 312 { 313 list_add_tail(& p-> run_list, array-> queue + p-> prio); 314 ___ set_bit(p-> prio, array-> bitmap); 315 array-> nr_active++; 316 p-> array = array; 317 } Строки 311-312 enqueue_task() берет процесс р и помещает его в массив приоритетов array при инициализации различных аспектов массива приоритетов. Строка 313 run_list процесса добавляется в хвост очереди, находящейся в p-> prio в массиве приоритетов. Строка 314 Битовая карта массива приоритетов p-> prio устанавливается при запуске планировщика, когда он видит, что существует процесс, выполняемый с приоритетом p-> prio. Строка 315 Счетчик массива приоритетов процесса увеличивается, отражая добавление нового процесса. Глава 3 • Процессы: принципиальная модель выполнения Строка 316 Массив указателей процесса устанавливается в массив приоритетов, в который он только что был добавлен. Итак, акт добавления нового, напрямую ответвленного процесса даже при подробном рассмотрении кода может сбивать с толку из-за похожести используемых в планировщике имен. Процесс помещается в конец списка массива приоритетов очереди выполнения, в слот, определяемый приоритетом процесса. Затем процесс записывает в свою структуру местоположение в массиве приоритетов и список, в котором он находится. Очередь ожидания Мы обсудили процесс перехода между состояниями TASK_INTERRUPTIBLE и TASK_RUNNING или TASK_UNINTERRUPTIBLE. Теперь мы рассмотрим другую структуру, вовлеченную в этот переход. Когда процесс ожидает наступления внешнего события, он удаляется из очереди выполнения и помещается в очередь ожидания. Очередь ожидания - это дву связный список структур wait_queue_t. Структура wait_queue_t получает всю необходимую для слежения за ожидающим процессом информацию. Все задачи, ожидающие определенного события, помещаются в очередь ожидания. Задачи данной очереди ожидания пробуждаются, как только наступает ожидаемое ими событие, убирают себя из очереди ожидания и переходят обратно в состояние TASK_RUNNING. Вы можете вспомнить, что системный вызов sys_wait4 () использует очередь ожидания, когда родитель посылает требование на получение статуса ответвленного дочернего процесса. Обратите внимание, что задача, ожидающая внешнего события (и поэтому больше не находящаяся в очереди выполнения1), может находиться либо в состоянии TASK_UNINTERRUPTIBLE, либо в состоянии TASK_INTERRUPTIBLE. Очередь ожидания представляет собой двусвязный список структур wait_queue_t, хранящих указатели на структуры процесса для заблокированных процессов. Каждый список имеет в заголовке структуру wait_queue_head_t, которая обозначает голову списка и служит разделителем начала и конца списка, позволяющим избежать лишних пробегов по всему списку wait_queue_t. Рис. 3.14 иллюстрирует реализацию очереди ожидания. Теперь мы рассмотрим структуры wait_queue_t и wait_queue_head_t. include/linux/wait.h 19 typedef struct _ wait_queue wait_queue_t; 23 struct _ wait_queue { 1 Задача убирается из очереди выполнения, как только она засыпает, и, следовательно, передает управление другому процессу. Очередь ожидания
Рис. 3.14. Структура очереди ожидания 24 unsigned int flags; 25 ttdefine WQ_FLAG_EXCLUSIVE 0x01 2 6 struct task_struct * task; 27 wait_queue_func_t func; 28 struct list_head task_list; 29 }; 31 struct _ wait_queue_head { 32 spinlock_t lock; 33 struct list_head task_list; 34 }; 3 5________________ typedef struct wait_queue_head wait_queue_head_t; Структура wait_queue_t состоит из следующих полей: • flags. Может хранить значение WO FLAG EXCLUSIVE, соответствующее 1, и ~WO FLAG EXCLUSIVE, соответствующее 0. Флагом помечаются эксклюзивные процессы. Эксклюзивные и неэксклюзивные процессы описываются в подразд. 3.7.1. • task. Указатель на описатель процесса, помещаемого в очередь ожидания. • func. Структура, хранящая функцию, используемую для пробуждения задачи в очереди ожидания. Поле по умолчанию использует def ault_wake_f unction (), которая описывается в подразд. 3.7.2. Глава 3 • Процессы: принципиальная модель выполнения • wait_gueue_t определена следующим образом: include/1inux/wai t.h typedef int (*wait_queue_func__t) (wait_queue__t *wait, unsigned mode, int sync); где wait - указатель на очередь ожидания, mode имеет значение TASK_INTERRUPTIBLE или TASK_UNINTERRUPTIBLE, a sync определяет, нужно ли синхронизировать пробуждение. • task__list. Структура, хранящая указатели на предыдущий и следующий Структура__ wait__queue__head__t - является головой списка очереди ожидания и состоит из следующих полей: • lock. Единственная блокировка очереди позволяет синхронизировать добавление и удаление элементов из очереди ожидания. • task_list. Структура, указывающая на первый и последний элементы очереди ожидания. Раздел «Очередь ожидания» в гл. 10 («Добавление вашего кода в ядро») описывает пример реализации очереди ожидания. В общих чертах способ, которым процесс погружает себя в сон, требует вызова одного из макросов wait_event* (кратко описывающихся там же) или выполнения такой последовательности шагов, показанных в примере в гл. 10: 1. Описывая очередь ожидания, процесс засыпает с помощью DECLARE_WAITQUEUE. 2. Добавляет себя в очередь ожидания с помощью add_wait_queue() или add_wait_queue_exclusive (). 3. Состояние процесса изменяется на TASK_INTERRUPTIBLE или на TASK_UNINTERRUPTIBLE. 4. Проверяется наступление внешнего события или вызывается schedule (), если оно еще не наступило. 5. После наступления внешнего события устанавливается в состояние TASK_RUNNING. 6. Удаляет себя из очереди ожидания с помощью вызова remove_wait_queue (). Процесс пробуждения процесса обрабатывается с помощью вызова одного из макросов wake_up. Эти макросы пробуждают все процессы, которые принадлежат к ука- 3.7 Очередь ожидания занной очереди ожидания. Это устанавливает задачу в состояние TASK_RUNNING и помещает ее обратно в очередь выполнения. Давайте теперь рассмотрим, что происходит, когда мы вызываем функцию add_wait_queue (). Популярное:
|
Последнее изменение этой страницы: 2016-03-25; Просмотров: 762; Нарушение авторского права страницы