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

时间:2016-10-14 04:02:22

标签: process scheduling system-calls

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

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

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

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

  4. 举个例子:

    ....
    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. 该过程在下一条指令上继续行进,稍后可能会等待其他等待队列,直至完成。

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

2 个答案:

答案 0 :(得分:8)

让我们探索内核源代码。首先,它似乎全部 各种等待例程(等待,waitid,waitpid,wait3,wait4)最终进入 同一系统调用wait4。这些天你可以找到系统调用 内核通过查找宏SYSCALL_DEFINE1等等,其中的数字 是wait4巧合的参数数量。使用 基于谷歌的自由电子搜索Linux Cross Reference我们最终找到了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)

(我在这些摘录中错过了一行,你可以告诉他们 非连续的行号)。 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的{​​{1}}结构。

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

struct list_head

致电184 struct list_head { 185 struct list_head *next, *prev; 186 }; wait.c 暂时锁定结构并通过内联函数 wait.h 你可以找到的add_wait_queue()来电 list.h。 这会适当地设置next和prev指针以添加新项目 列表。 空列表有两个指针指向list_head结构。

将新条目添加到列表后,list_add()系统调用会设置a 标志,将从下一个可运行队列中删除进程 重新安排并致电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); ,这是该过程发出的时间 up cpu和我们的系统调用"挂起"为了未来的活动。

schedule()

当进程被唤醒时,它将继续执行代码 1594 if (!signal_pending(current)) { 1595 schedule(); 1596 goto repeat; 1597 } 再次通过所有的孩子,看看是否等待 条件得到满足,并可能返回给来电者。

唤醒这个过程的是什么?一个孩子死亡并产生一个SIGCHLD 信号。 在signal.c 流程在流程中调用schedule()

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()等待队列。 exit.c

wait_chldexit

我认为我们应该止步于此,因为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); 显然是其中之一 系统调用和等待队列使用的复杂示例。你可以找到 从2005年开始在这3页Linux Journal article中简单介绍一下这个机制。很多事情 已经改变了,但原则得到了解释。你也可以买书 " Linux设备驱动程序"和" Linux内核开发",或查看 这些可以在网上找到的早期版本。

对于"系统调用的解剖"从用户空间到内核的路上 您可以阅读这些lwn articles

每当执行任务时,等待队列在整个内核中都会被大量使用 需要等待一些条件。通过内核源代码找到一个grep 超过1200次wait()来电,这就是你初始化的方式 等待你通过简单地init_waitqueue_head()动态创建空间 保持结构。

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

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

然后硬件会中断cpu,内核会调用 驱动程序的中断处理程序(在初始化时注册)。处理程序代码 只需在等待队列上调用current,内核就可以 将等待队列中的所有任务放回运行队列中。

当任务再次获得cpu时,它会从它停止的地方继续(在 wake_up())并检查硬件是否已完成操作,并且 然后可以将数据返回给用户。

因此内核不对驱动程序的等待队列负责 当驱动程序调用它时会查看它。没有来自的映射 例如,硬件中断到等待队列。

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

答案 1 :(得分:0)

为了等待子进程终止,父进程将只执行wait()系统调用。此调用将暂停父进程,直到其任何子进程终止,此时wait()调用将返回,父进程可以继续。

等待的原型(电话是:

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

pid_t wait(int *status);

wait的返回值是终止的子进程的PID。 wait()的参数是指向一个位置的指针,该位置将在终止时接收子的退出状态值。

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

#include <std1ib.h>

void exit(int status);

exit()调用没有返回值,因为调用它的进程终止,因此无法接收值。但请注意,exit()确实采用参数值 - 状态。除了使等待的父进程恢复执行之外,exit()还通过wait()参数指向的位置将状态参数值返回给父进程。

实际上,wait()可以通过status参数指向的值返回几条不同的信息。因此,提供了一个名为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指定要等待的内容,status与简单的wait()参数相同,并且options允许您指定如果没有子进程准备好报告,则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);

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