$ make all > Make.out & [1] 1475 # 作业编号是 1,所启动的进程 ID 是 1475 $ pr *.c | lpr & [2] 1490 # 作业编号是 2,其第一个进程的进程 ID 是 1490
只有前台作业可以接收终端输入,若后台作业试图读终端,则终端驱动程序会向后台作业发送一个 SIGTTIN 信号,它通常会停止此后台作业,而 shell 则向有关用户发出通知,然后用户就可以用 shell 命令将此作业转为前台作业运行,于是它就可以读终端了(不过在没有作业控制时,其处理方法是:如果该进程自己没有重定向标准输入,则 shell 自动将后台进程的标准输入重定向到 /dev/null。读 /dev/null 将产生一个文件结束,这意味着该后台进程会立即终止)。如下过程所示。
$ cat > temp.foo & # 在后台启动,但将从标准输入读 [1] 1681 $ [1] + Stopped (SIGTTIN) # SIGTTIN 信号停止后台作业 $ fg %1 # 将该后台作业转变为前台作业,并发送继续信号(SIGCONT) cat > temp.foo # shell 告诉我们现在哪一个作业在前台 hello, world # 输入一行 ^D # 输入文件结束符 $ cat temp.foo # 检查文件 hello, world
而后台作业是否输出到控制终端是可以使用 stty 命令设置的(默认是允许的)。当禁止后台作业向控制终端写时,终端驱动程序就会向试图写其标准输出的后台作业发送 SIGTTOU 信号,它通常会阻塞该后台作业。如下过程所示。
$ cat temp.foo & # 在后台执行 [1] 1719 $ hello, world # 这是后台作业的输出 [1] + Done cat temp.foo & $ stty tostop # 禁止后台作业输出到控制终端 $ cat temp.foo & # 再执行一次 [1] 1721 $ # 键入回车,发现后台作业已停止 [1] + Stopped(SIGTTOU) cat temp.foo & $ fg %1 # 在前台恢复作业运行 cat temp.foo # shell 告诉我们现在哪一个作业在前台 hello.world
下图总结了作业控制的某些功能。图中穿过终端驱动程序框的实线表明终端 I/O 和终端产生的信号总是从前台进程组连接到实际终端,而对应于 SIGTTOU 信号的虚线表明后台进程组进程的输出是否出现在终端是可选择的。
一个父进程已终止的进程称为孤儿进程,它会由 init 进程“收养”,而整个进程组也可成为“孤儿”。POSIX.1 将孤儿进程组定义为:该组中每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员。即一个进程组不是孤儿进程组的条件是:该组中有一个进程,其父进程在属于同一会话的另一个组中。如果进程组不是孤儿进程组,那么在属于同一会话的另一个组中的父进程就有机会重新启动该组中停止的进程。
考虑一个进程,它 fork 了一个子进程后就终止,但在它终止时,如果该子进程停止(用作业控制)又将如何?子进程如何继续,以及是否知道它已经是孤儿进程?
下面这个程序演示了这种情形。该程序假定使用了一个作业控制 shell,shell 会将前台进程放在它们自己的进程组中,shell 则留在自己的进程组内。子进程继承其父进程的进程组。在 fork 后:
* 父进程睡眠 5 秒,这是让子进程在父进程终止前运行的一种权宜之计。
* 子进程为挂断信号建立信号处理程序,以观察 SIGHUP 信号是否已发送给子进程。
* 子进程用 kill 函数向其自身发送停止信号 SIGTSTP(类似于使用 Ctrl+Z)。
* 当父进程终止时,进程组包含一个停止的进程,该子进程成为孤儿进程,其父进程变成 init 进程,同时成为一个孤儿进程组的成员。POSIX.1 要求向新孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <errno.h> static void sig_hup(int signo){ printf("SIGHUP received, pid = %ld\n", (long)getpid()); } static void pr_ids(char *name){ printf("%s: pid = %ld, ppid = %ld, pgrp = %ld, tpgrp = %ld\n", name, (long)getpid(), (long)getppid(), (long)getpgrp(), (long)tcgetpgrp(STDIN_FILENO)); fflush(stdout); } int main(void){ char c; pid_t pid; pr_ids("parent"); if((pid=fork()) < 0){ printf("fork error\n"); }else if(pid > 0){ sleep(5); // sleep to let child stop itself }else{ pr_ids("child"); signal(SIGHUP, sig_hup); // establish signal handler kill(getpid(), SIGTSTP); // stop ourself pr_ids("child"); // prints only if we're continued if(read(STDIN_FILENO, &c, 1) != 1) printf("read error %d on controlling TTY\n", errno); // pr_ids("child"); // Linux 中 read 后才变为后台进程组 } exit(0); }
运行结果示例:
$ ./orphanPgrpDemo.out parent: pid = 7083, ppid = 82002, pgrp = 7083, tpgrp = 7083 child: pid = 7084, ppid = 7083, pgrp = 7083, tpgrp = 7083 SIGHUP received, pid = 7084 child: pid = 7084, ppid = 1, pgrp = 7083, tpgrp = 82002 # 前台进程组变为了 82002 read error 5 on controlling TTY
这里父进程终止后,子进程变成了后台进程组,此时子进程第二次调用 pr_ids 后,程序企图读标准输入,所以会对该后台进程组产生 SIGTTIN 信号。但因为这是一个孤儿进程组,如果内核用此信号停止它,则此进程组中的进程就再也不会继续。因此 POSIX.1 规定,这种情况下 read 返回出错,并将 errno 设置为 EIO(值一般为 5)。另外需要注意的是,在某些如 Linux 的实现中,子进程在父进程终止后依然是前台进程组,所以其后的 read 调用可以正常读取标准输入,不过它在 read 之后也会转变为后台进程组。