一个进程调用syscall wait()后,谁会将其唤醒?

Ric*_*ich 5 scheduling process system-calls

我有一个大概的想法,一个进程可以在ready_queueCPU选择候选者下一个运行的地方.还有一些进程等待(广泛地说)事件的其他队列.我很久以前从OS课程中知道IO和中断有等待队列.我的问题是:

  1. 进程可以等待很多事件.是否有与每个此类事件对应的等待队列?

  2. 这些等待队列是动态创建/销毁的吗?如果是这样,哪个内核模块负责管理这些队列?调度程序?是否有任何预定义的队列始终存在?

  3. 为了最终从等待队列中获得等待进程,内核是否有从每个实际事件(硬件或软件)映射到等待队列的方法,然后删除该队列上的所有进程?如果是这样,内核采用了什么机制?

举个例子:

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

wait(NULL)是一个阻塞系统调用.我想知道父进程经历的剩余旅程.我对故事情节的看法如下:如果我错过关键步骤或者我完全错了,请纠正我:

  1. 通过libc运行时设置正常的系统.现在父进程处于内核模式,准备执行wait()syscall中的任何内容.

  2. wait(NULL) 创建一个等待队列,内核稍后可以在其中找到此队列.

  3. wait(NULL) 将父进程放到此队列上,在某个映射中创建一个条目,说明"如果我(内核)收到软件中断,信号或任何表明子进程已完成的内容,则调度程序应该查看此等待队列".

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

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

  6. 父进程仍处于内核模式,在wait(NULL)syscall中.现在,其余系统调用的主要工作是退出内核模式并最终将父进程返回到用户区.

  7. 该过程继续其下一条指令的行程,并可能稍后等待其他等待队列,直到它完成.

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

meu*_*euh 9

让我们来探索内核源代码.首先,似乎所有各种等待例程(wait,waitid,waitpid,wait3,wait4)最终都在同一个系统调用中wait4.这些天你可以通过查找宏找到内核中的系统调用,SYSCALL_DEFINE1因此,数字是参数的数量,wait4巧合的是4.在Free Electrons Linux Cross Reference中使用基于google的自由文本搜索我们最终找到的定义:

1674 SYSCALL_DEFINE4(wait4, pid_t, upid, int __user *, stat_addr,
1675                 int, options, struct rusage __user *, ru)
Run Code Online (Sandbox Code Playgroud)

宏似乎将每个参数拆分为其类型和名称.这个 wait4例程进行一些参数检查,将它们复制到一个wait_opts 结构中,然后调用do_wait(),这是同一个文件中的几行:

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

1551 static long do_wait(struct wait_opts *wo)
Run Code Online (Sandbox Code Playgroud)

(我错过了这些摘录中的行,你可以通过非连续的行号来判断). do_wait()将结构的另一个字段设置为函数名称,该函数 child_wait_callback()在同一文件中排成几行.另一个字段设置为current.这是一个主要的"全球",指向有关当前任务的信息:

1558         init_waitqueue_func_entry(&wo->child_wait, child_wait_callback);
1559         wo->child_wait.private = current;
Run Code Online (Sandbox Code Playgroud)

然后将该结构添加到专门为等待SIGCHLD信号的进程设计的队列中current->signal->wait_chldexit:

1560         add_wait_queue(&current->signal->wait_chldexit, &wo->child_wait);
Run Code Online (Sandbox Code Playgroud)

我们来看看吧current.很难找到它的定义,因为它根据架构而变化,并且在它之后找到最终的结构是一个兔子沃伦.例如current.h

  6 #define get_current() (current_thread_info()->task)
  7 #define current get_current()
Run Code Online (Sandbox Code Playgroud)

然后是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 */
Run Code Online (Sandbox Code Playgroud)

所以current指向a 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;
Run Code Online (Sandbox Code Playgroud)

所以我们发现current->signalcurrent->signal->wait_chldexit,结构signal_struct在同一个文件中:

670 struct signal_struct {
677         wait_queue_head_t       wait_chldexit;  /* for wait4() */
Run Code Online (Sandbox Code Playgroud)

所以add_wait_queue()我们上面的调用是指这种 wait_chldexit类型的结构wait_queue_head_t.

等待队列只是一个最初为空的,双向链接的包含types.h的结构列表 struct list_head

184 struct list_head {
185         struct list_head *next, *prev;
186 };
Run Code Online (Sandbox Code Playgroud)

调用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);
Run Code Online (Sandbox Code Playgroud)

此例程调用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);
Run Code Online (Sandbox Code Playgroud)

这非常深,但事实上只是试图看看是否有任何孩子已经满足了系统调用,我们可以立即返回数据.对你来说有趣的案例是什么时候找不到任何东西,但仍然有奔跑的孩子.我们最终调用schedule(),这是当进程放弃cpu并且我们的系统调用"挂起"以用于将来的事件时.

1594                 if (!signal_pending(current)) {
1595                         schedule();
1596                         goto repeat;
1597                 }
Run Code Online (Sandbox Code Playgroud)

当进程被唤醒时,它将继续使用代码,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);
Run Code Online (Sandbox Code Playgroud)

__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);
Run Code Online (Sandbox Code Playgroud)

我认为我们应该停在那里,这wait()显然是系统调用和等待队列使用的一个更复杂的例子.您可以在2005 年的这篇3页Linux Journal文章中找到更简单的机制演示.许多内容已经改变,但原理已经解释.您也可以购买"Linux设备驱动程序"和"Linux内核开发"这些书籍,或查看可在线找到的早期版本.

有关方式"解剖系统调用"从用户空间到内核你可能会阅读这些LWN 文章.


无论何时任务,等待队列都在整个内核中大量使用,需要等待某些条件.通过内核源代码的grep可以找到1200多个调用,init_waitqueue_head()这些调用是通过简单地kmalloc()用空间来保存结构来初始化动态创建的等待数.

DECLARE_WAIT_QUEUE_HEAD()宏的grep 发现超过150次使用静态waitqueue结构的声明.这些之间没有内在差异.例如,驱动程序可以选择任一种方法来创建等待队列,通常取决于它是否可以管理许多类似的设备,每个设备都有自己的队列,或者只是期望一个设备.

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

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

当任务再次获得cpu时,它将继续从中断(in schedule())并检查硬件是否已完成操作,然后可以将数据返回给用户.

因此内核不对驱动程序的等待队列负责,因为它只在驱动程序调用它时才查看它.例如,没有从硬件中断到等待队列的映射.

如果同一等待队列上有多个任务wake_up(),则可以使用该调用的变体来唤醒仅1个任务或所有任务,或仅唤醒那些处于可中断等待中的任务(即设计为能够取消操作并在信号的情况下返回给用户,等等.