Давайте изучим исходники ядра. Во-первых, кажется, что все различные подпрограммы ожидания (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(¤t->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