После того как процесс вызовет системный вызов wait(), кто его разбудит?

У меня есть общее представление о том, что процесс может находиться в ready_queue, где ЦП выбирает кандидата для запуска следующим. И есть эти другие очереди, в которых процесс ожидает (в широком смысле) событий. Я давно знаю из курсов ОС, что есть очереди ожидания для ввода-вывода и прерываний. Мои вопросы:

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

  2. Эти очереди ожидания создаются/уничтожаются динамически? Если да, то какой модуль ядра отвечает за управление этими очередями? Планировщик? Существуют ли какие-либо предопределенные очереди, которые всегда будут существовать?

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

Чтобы привести пример:

....
pid = fork();
if (pid == 0) { // child process
    // Do something for a second;
}
else { // parent process
    wait(NULL);
    printf("Child completed.");
}
....

wait(NULL) — блокирующий системный вызов. Я хочу знать остальную часть пути, который проходит родительский процесс. Мой взгляд на сюжетную линию выглядит следующим образом, ПОЖАЛУЙСТА, поправьте меня, если я пропущу важные шаги или если я полностью ошибаюсь:

  1. Обычная настройка системного вызова через среду выполнения libc. Теперь родительский процесс находится в режиме ядра и готов выполнить все, что находится в системном вызове wait().

  2. wait(NULL) создает очередь ожидания, в которой ядро ​​позже может найти эту очередь.

  3. wait(NULL) помещает родительский процесс в эту очередь, создает запись в некоторой карте, которая гласит: «Если я (ядро) когда-либо получит программное прерывание, сигнал или что-то еще, что указывает на завершение дочернего процесса, планировщик должен посмотреть на это ожидание очередь".

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

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

  6. Родительский процесс все еще находится в режиме ядра внутри системного вызова wait(NULL). Теперь основная задача остальной части системного вызова — выйти из режима ядра и в конечном итоге вернуть родительский процесс на пользовательскую землю.

  7. Процесс продолжает свое путешествие со следующей инструкцией и может позже ожидать завершения в других очередях ожидания.

PS: я надеюсь узнать внутреннюю работу ядра ОС, какие этапы процесса проходят в ядре и как ядро ​​взаимодействует с этими процессами и манипулирует ими. Я знаю семантику и контракт API системных вызовов wait(), и это не то, что я хочу знать из этого вопроса.


person Rich    schedule 14.10.2016    source источник


Ответы (2)


arrow_upward
10
arrow_downward

Давайте изучим исходники ядра. Во-первых, кажется, что все различные подпрограммы ожидания (wait, waitid, waitpid, wait3, wait4) заканчиваются одним и тем же системным вызовом wait4. В наши дни вы можете найти системные вызовы в ядре, ища макросы SYSCALL_DEFINE1 и так далее, где число — это количество параметров, что для wait4 совпадает с 4. Используя поиск свободного текста на основе Google в Free Electrons Перекрестный справочник Linux мы в конце концов находим определение:

1674 SYSCALL_DEFINE4(wait4, pid_t, upid, int __user *, stat_addr,
1675                 int, options, struct rusage __user *, ru)

Здесь макрос как бы разбивает каждый параметр на его тип и имя. Эта подпрограмма wait4 выполняет некоторую проверку параметров, копирует их в структуру wait_opts и вызывает do_wait(), что представляет собой несколько строк в том же файле:

1677         struct wait_opts wo;
1705         ret = do_wait(&wo);

1551 static long do_wait(struct wait_opts *wo)

(Я пропускаю строки в этих отрывках, как вы можете сказать по непоследовательным номерам строк). do_wait() задает другое поле структуры для имени функции, child_wait_callback() которое находится несколькими строками выше в том же файле. В другом поле установлено значение current. Это основной «глобальный», который указывает на информацию о текущей задаче:

1558         init_waitqueue_func_entry(&wo->child_wait, child_wait_callback);
1559         wo->child_wait.private = current;

Затем эта структура добавляется в очередь, специально созданную для процесса ожидания сигналов SIGCHLD, current->signal->wait_chldexit:

1560         add_wait_queue(&current->signal->wait_chldexit, &wo->child_wait);

Давайте посмотрим на current. Довольно сложно найти его определение, поскольку оно варьируется в зависимости от архитектуры, и следовать ему, чтобы найти окончательную структуру, — это что-то вроде кроличьего нора. Например, current.h

  6 #define get_current() (current_thread_info()->task)
  7 #define current get_current()

затем thread_info.h

163 static inline struct thread_info *current_thread_info(void)
165         return (struct thread_info *)(current_top_of_stack() - THREAD_SIZE);

 55 struct thread_info {
 56         struct task_struct      *task;          /* main task structure */

Итак, current указывает на task_struct, который мы находим в sched.h

1460 struct task_struct {
1461         volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
1659 /* signal handlers */
1660         struct signal_struct *signal;

Итак, мы нашли current->signal из current->signal->wait_chldexit, а структура signal_struct находится в том же файле:

670 struct signal_struct {
677         wait_queue_head_t       wait_chldexit;  /* for wait4() */

Таким образом, вызов add_wait_queue(), который мы получили выше, относится к этой структуре wait_chldexit типа wait_queue_head_t.

Очередь ожидания — это просто изначально пустой двусвязный список структур, содержащих struct list_head types.h

184 struct list_head {
185         struct list_head *next, *prev;
186 };

Вызов add_wait_queue() wait.c временно блокирует структуру и с помощью встроенной функции wait .h вызывает list_add(), который вы можете найти в list.h. Это устанавливает указатели next и prev соответствующим образом, чтобы добавить новый элемент в список. Пустой список имеет два указателя, указывающих на структуру list_head.

После добавления новой записи в список системный вызов wait4() устанавливает флаг, который удалит процесс из очереди выполнения при следующем перепланировании, и вызывает do_wait_thread():

1573         set_current_state(TASK_INTERRUPTIBLE);
1577                 retval = do_wait_thread(wo, tsk);

Эта процедура вызывает wait_consider_task() для каждого потомка процесса:

1501 static int do_wait_thread(struct wait_opts *wo, struct task_struct *tsk)
1505         list_for_each_entry(p, &tsk->children, sibling) {
1506                 int ret = wait_consider_task(wo, 0, p);

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

1594                 if (!signal_pending(current)) {
1595                         schedule();
1596                         goto repeat;
1597                 }

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

Что побуждает процесс сделать это? Ребенок умирает и генерирует сигнал SIGCHLD. В signal.c do_notify_parent() вызывается процесс, когда он умирает:

1566  * Let a parent know about the death of a child.
1572 bool do_notify_parent(struct task_struct *tsk, int sig)
1656         __wake_up_parent(tsk, tsk->parent);

__wake_up_parent() вызывает __wake_up_sync_key() и использует именно ту очередь ожидания wait_chldexit, которую мы установили ранее. exit.c

1545 void __wake_up_parent(struct task_struct *p, struct task_struct *parent)
1547         __wake_up_sync_key(&parent->signal->wait_chldexit,
1548                                 TASK_INTERRUPTIBLE, 1, p);

Я думаю, что мы должны остановиться на этом, так как wait() явно является одним из наиболее сложных примеров системного вызова и использования очередей ожидания. Вы можете найти более простое представление механизма в этой 3-страничной статье журнала Linux за 2005 год. все изменилось, но принцип объяснен. Вы также можете купить книги «Драйверы устройств Linux» и «Разработка ядра Linux» или ознакомиться с их более ранними изданиями, которые можно найти в Интернете.

Для «Анатомии системного вызова» на пути от пространства пользователя к ядру вы можете прочитать эти lwn статьи.


Очереди ожидания активно используются во всем ядре всякий раз, когда задача должна ожидать выполнения какого-либо условия. Grep через исходники ядра находит более 1200 вызовов init_waitqueue_head(), именно так вы инициализируете очередь ожидания, которую вы динамически создали, просто kmalloc() заполняя пространство для хранения структуры.

Команда grep для макроса DECLARE_WAIT_QUEUE_HEAD() находит более 150 применений этого объявления статической структуры очереди ожидания. Между ними нет принципиальной разницы. Драйвер, например, может выбрать любой метод для создания очереди ожидания, часто в зависимости от того, может ли он управлять многими похожими устройствами, каждое со своей собственной очередью, или ожидает только одно устройство.

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

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

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

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

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

person meuh    schedule 18.10.2016
comment
Большое спасибо за глубокое погружение в код! Можете ли вы также, на более высоком уровне, ответить на мои первые 3 вопроса? После прочтения вашего ответа создается впечатление, что планировщик заранее создал всевозможные очереди ожидания для всех возможных типов событий, которые может ожидать процесс. Это правильно? - person Rich; 20.10.2016
comment
Я добавил некоторые пояснения в конце после горизонтальной линии. - person meuh; 20.10.2016

arrow_upward
0
arrow_downward

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

Прототип вызова wait(:

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

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

Когда процесс завершается, он выполняет системный вызов exit() либо непосредственно в собственном коде, либо косвенно через библиотечный код. Прототип вызова exit():

#include <std1ib.h>

void exit(int status);

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

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

#include <sys/wait.h>

int statval, exstat;
pid_t pid;

pid = wait(&statval);
exstat = WEXITSTATUS(statval);

На самом деле версия wait(), которую мы только что видели, является лишь самой простой версией, доступной в Linux. Новая версия POSIX называется waitpid. Прототип для waitpid():

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

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

Различные возможности для параметра pid:

< -1 wait for a child whose PGID is -pid
-1   same behavior as standard wait()
0    wait for child whose PGID = PGID of calling process
> 0  wait for a child whose PID = pid

Стандартный вызов wait() теперь является избыточным, так как следующий вызов waitpid() точно эквивалентен:

#include <sys/wait.h>

int statval;
pid_t pid;

pid = waitpid(-1, &statval, 0);

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

person zombie    schedule 17.10.2016
comment
Я ценю ваше подробное объяснение семантики этих системных вызовов. Но это совсем не то, к чему был задан вопрос. Я хочу знать, как работает внутренняя часть ОС. Особенно, как работают очереди ожидания. Например, как родитель получает уведомление, когда его дочерний процесс завершается. - person Rich; 17.10.2016
comment
я не знаю ни о каких очередях ожидания своих указателей. родитель получает не тогда, когда выход вызывается в дочернем элементе, а если он не был перехвачен, то это будет нечто, называемое зомби (прямо как мой ник здесь) - person zombie; 17.10.2016
comment
Вам не кажется, что есть пробел, когда вы говорите, что родитель получил уведомление, когда выход вызывается в дочернем процессе? Это то, что я хочу знать, т. е. как ядро ​​находит родителя и пробуждает его из некоторых очередей ожидания, если таковые имеются. - person Rich; 18.10.2016