进程调用syscall wait()后,谁来唤醒它?

After a process calls syscall wait(), who will wake it up?

我有一个大致的想法,即流程可以在 ready_queue 中,其中 CPU 选择下一个 运行 的候选人。还有这些其他队列,进程在这些队列上等待(广义上的)事件。很久以前听OS的课程就知道有IO和中断的wait queues。我的问题是:

  1. 进程可以等待的事件有很多。每个这样的事件是否有对应的等待队列?

  2. 这些等待队列 created/destroyed 是动态的吗?如果是这样,哪个内核模块负责管理这些队列?调度员?是否有任何将永远存在的预定义队列?

  3. 为了最终让等待进程离开等待队列,内核是否有办法将每个实际事件(硬件或软件)映射到等待队列,然后移除ALL 该队列上的进程?如果是这样,内核采用什么机制?

举个例子:

....
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) 将父进程放入此队列,在某些映射中创建一个条目,上面写着 "If I (the kernel) ever receives an software interrupt, signal, or whatever that indicates that the child process is finished, scheduler should come look at this wait queue".

  4. 子进程结束,内核以某种方式注意到了这一事实。内核上下文切换到调度器,它在映射中查找父进程所在的等待队列。

  5. 调度器将父进程移动到就绪队列,发挥它的魔力,稍后父进程最终被选择到 运行。

  6. 父进程仍处于内核模式,在 wait(NULL) 系统调用中。现在系统调用其余部分的主要工作是退出内核模式,并最终 return 父进程进入用户空间。

  7. 进程在下一条指令上继续其旅程,稍后可能会在其他等待队列中等待直到完成。

PS:我希望知道OS内核的内部工作原理,一个进程在内核中经历了哪些阶段以及如何内核交互并操纵这些进程。我确实知道 wait() 系统调用 API 的语义和契约,这不是我想从这个问题中知道的。

为了等待 child 进程终止,parent 进程将只执行 wait() 系统调用。此调用将挂起 parent 进程,直到其任何 child 进程终止,此时 wait() 调用 returns 和 parent 进程可以继续。

wait( 调用的原型是:

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

pid_t wait(int *status);

wait 的 return 值是终止的 child 进程的 PID。 wait() 的参数是指向某个位置的指针,该位置将在 child 终止时接收其退出状态值。

当进程终止时,它会直接在自己的代码中或通过库代码间接执行 exit() 系统调用。 exit() 调用的原型是:

#include <std1ib.h>

void exit(int status);

exit() 调用没有 return 值,因为调用它的进程终止,因此无论如何都无法接收值。但是请注意,exit() 确实有一个参数值——status。除了使等待的 parent 进程恢复执行外,exit() 还通过 wait() 参数指向的位置 return 将状态参数值发送给 parent 进程。

事实上,wait() 可以通过状态参数指向的值 return 几条不同的信息。因此,提供了一个名为 WEXITSTATUS() 的宏(通过 访问),它可以提取 return child 的退出状态。以下代码片段展示了它的用法:

#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 指定要等待的内容,status 与简单的 wait() 参数相同,选项允许您指定对 waitpid() 的调用不应挂起 parent 进程,如果没有 child 进程已准备好报告其退出状态。

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);

一个只执行很短时间的 child 进程有可能在它的 parent 进程有机会为它等待()之前终止。在这些情况下,child 进程将进入一种状态,称为僵尸状态,在该状态下,它的所有资源都已释放回系统,但它的进程数据结构除外,该数据结构保持其退出状态。当 parent 最终等待 child 时,立即传递退出状态,然后进程数据结构也可以释放回系统。

让我们探索内核源代码。首先,似乎所有的 各种等待例程(wait、waitid、waitpid、wait3、wait4)在 相同的系统调用,wait4。这些天你可以在 内核通过查找宏 SYSCALL_DEFINE1 等,其中数字 是参数的数量,对于 wait4 恰好是 4。使用 google-based Free Electrons 中的自由文本搜索 Linux Cross 参考我们最终找到了definition:

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)

(我漏掉了这些摘录中的几行,你可以通过 non-consecutive 行号)。 do_wait() 将结构的另一个字段设置为一个函数的名称, child_wait_callback() 这是同一文件中的几行。其他 字段设置为 current。这是一个主要的 "global" 指向 有关当前任务的信息:

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->wait_chldexit 中找到了 current->signal, 并且结构 signal_struct 在同一个文件中:

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

所以我们上面的add_wait_queue()调用指的是这个 wait_chldexit wait_queue_head_t.

类型的结构

等待队列只是一个最初为空的 doubly-linked 结构列表,其中包含 struct list_headtypes.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() 系统调用设置了一个 将在下一个 运行nable 队列中删除进程的标志 重新安排并致电 do_wait_thread():

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

此例程为进程的每个 child 调用 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);

这很深入,但实际上只是想看看是否已经有 child 满足系统调用,我们可以 return 立即使用数据。这 对你来说有趣的情况是什么都找不到,但仍然有 运行ning child仁。我们最终调用 schedule(),这是进程给出的时间 为将来的事件设置 cpu 和我们的系统调用 "hangs"。

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

当进程被唤醒后,会继续执行后面的代码 schedule() 再次遍历所有 children 看是否等待 条件满足,可能 return 给调用者。

是什么唤醒了执行该操作的进程? child 死亡并产生一个 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 Device Drivers" 和 "Linux Kernel Development",或查看 可以在网上找到这些的早期版本。

对于从用户space到内核的"Anatomy Of A System Call" 你可能会阅读这些 lwn articles.


每当有任务时,整个内核都会大量使用等待队列, 需要等待一些条件。通过内核源代码进行 grep 查找 超过 1200 次调用 init_waitqueue_head() 这就是你如何初始化一个 您通过 kmalloc()-ing space 动态创建的等待队列 来保持结构。

DECLARE_WAIT_QUEUE_HEAD() 宏的 grep 发现超过 150 次使用 这个静态等待队列结构的声明。没有内在的 这些之间的区别。例如Adriver,两种方法任选其一 创建等待队列,通常取决于它是否可以管理 许多类似的设备,每个都有自己的队列,或者只需要一个设备。

没有中央代码负责这些队列,尽管有共同点 代码来简化它们的使用。例如,driver 可能会创建一个空的 安装和初始化时等待队列。当你用它从某些地方读取数据时 硬件,它可能会通过直接写入启动读操作 硬件寄存器,然后在其等待队列中排队一个条目(对于 "this" 任务,即 current)以放弃 cpu 直到硬件准备好数据。

然后硬件会中断 cpu,内核会调用 driver 的中断处理程序(在初始化时注册)。处理程序代码 将简单地在等待队列上调用 wake_up(),以便内核 将等待队列中的所有任务放回 运行 队列中。

当任务再次获得 cpu 时,它会从中断处继续(在 schedule()) 并检查硬件是否已完成操作,以及 然后可以return将数据提供给用户。

所以内核不负责 driver 的等待队列,因为它只 当 driver 调用它时查看它。没有来自的映射 等待队列的硬件中断,例如。

如果同一个等待队列中有多个任务,则有以下变体 wake_up() 调用仅可用于唤醒 1 个任务或所有任务 他们,或者只有那些处于可中断等待状态的人(即被设计为 能够取消操作并 return 给用户 信号)等。