作业控制允许在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问终端,以及哪些作业可以在后台运行。作业控制要求下面三种形式的支持:
(1) 支持作业控制的shell。
(2) 内核中的终端驱动程序必须支持作业控制。
(3) 内核必须提供对某些作业控制信号的支持。
从shell使用作业控制功能角度讲,用户可以在前台或后台启动一个作业。一个作业只是几个进程的集合。通常是一个进程的管道线。
当启动一个后台作业时,shell赋予它一个作业标识,并打印一个或几个进程ID。
shell并不在任意时刻打印后台作业的状态改变,它只在打印其提示符让用户输入新的命令行之前才这样做。
有三种特殊字符可使终端驱动程序产生信号,并将它们送至前台进程组:
(1) 中断字符(Ctrl+C)产生SIGINT。
(2) 退出字符(Ctrl+\)产生SIGQUIT。
(3) 挂起字符(Ctrl+Z)产生SIGTSTP。
只有前台作业接收终端输入。如果后台作业试图读终端,那么这并不是一个错误,但是终端驱动程序将检测这种情况,并且向后台作业发送一个特定信号SIGTTIN。该信号通常会暂时停止此后台作业,而shell则向有关用户发出这种情况的通知,然后用户就可用shell命令将此作业转为前台作业运行,于是它就可读终端。下列操作过程演示了这一点:
$ cat > temp.foo & 在后台启动,但将从标准输入读 [1] 3240 $ 键入回车 [1]+ 已停止 cat > temp.foo SIGTTIN信号 $ fg %1 使1号作业成为前台作业 cat > temp.foo shell告诉我们现在哪一个作业在前台 hello, world 键入一行 Ctrl+D键入结束符
shell在后台启动cat进程,但是cat试图读其标准输入(控制终端)时,终端驱动程序知道它是个后台作业,于是将SIGTTIN信号送至后台作业。shell检测到其子进程的状态改变,并通知我们作业已被停止。然后,我们用shell的fg命令将此停止的作业送入前台。这样做可以使shell将此作业置入前台进程组(tcsetpgrp),并将继续信号(SIGCONT)送给该进程组。因为该作业现在位于前台进程组中,所以它可以读终端。
在用户禁止后台作业写到控制终端时,如果后台作业试图写到其标准输出,终端驱动程序将向该作业发送SIGTTOUT信号。
Linux支持作业控制。
$ ps -o pid,ppid,pgrp,session,tpgid,comm PID PPID PGRP SESS TPGID COMMAND 3337 2880 3337 3337 3367 bash 3367 3337 3367 3337 3367 ps
shell将前台作业(ps)放入了它自己的进程组(3367)。ps命令是组长进程,并且是该进程组中的唯一进程。
进一步讲,此进程组有控制终端,所以它是前台进程组。我们的登陆shell在执行ps命令时是后台进程组。但需要注意的是,这两个进程组3337和3367都是同一会话的成员。
在后台执行此程序:
$ ps -o pid,ppid,pgrp,session,tpgid,comm & [1] 4357 $ PID PPID PGRP SESS TPGID COMMAND 3337 2880 3337 3337 3337 bash 4357 3337 4357 3337 3337 ps [1]+ 完成 ps -o pid,ppid,pgrp,session,tpgid,comm
再一次,ps命令被放入它自己的进程组中,但是此时进程组(4357)不再是前台进程组,而是一个后台进程组。TPGID 3337指示前台进程组时用户的登陆shell。
考虑一个进程,它fork了一个子进程然后终止。如果在父进程终止之时,子进程停止,则会发生什么情况呢?子进程如何继续,以及子进程是否知道它已经是孤儿进程?
《UNIX环境高级编程》P229:程序清单9-1 创建一个孤儿进程组
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <signal.h> static void sig_hup(int signo) { printf("SIGHUP received, pid = %d\n", getpid()); } static void pr_ids(char *name) { printf("%s: pid = %d, ppid = %d, pgrp = %d, tpgrp = %d\n", name, getpid(), getppid(), getpgrp(), tcgetpgrp(STDIN_FILENO)); fflush(stdout); } int main(void) { char c; pid_t pid; pr_ids("parent"); if ((pid = fork()) < 0) { fprintf(stderr, "fork error\n"); } else if (pid > 0) { // 父进程 sleep(5); // 父进程休眠5秒,然子进程在父进程终止前运行 exit(0); // 父进程终止 } else { // 子进程 pr_ids("child"); signal(SIGHUP, sig_hup); // 建立信号处理程序 kill(getpid(), SIGTSTP); // 向自身发送停止信号 pr_ids("child"); if (read(STDIN_FILENO, &c, 1) != 1) printf("read error from controlling TTY, errno = %d\n", errno); exit(0); } }
当父进程终止时,子进程成为孤儿进程,所以其父进程ID成为1,也就是init进程ID。子进程成为一个孤儿进程组成员。
POSIX.1将孤儿进程组定义为:该组中每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员。对孤儿进程组的另一种描述如下:一个进程组不是孤儿进程组的条件是,该组中有一个进程,其父进程在属于同一会话的另一个组中。如果进程组不是孤儿进程组,那么在属于同一会话的另一个组中的父进程就有机会重新启动该组中停止的进程。在这里,进程组中的没一个进程的父进程都属于另一个会话,所以该进程组是孤儿进程组。
因为父进程终止后,进程组成为孤儿进程组,POSIX.1要求向新的孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。
在处理了挂断信号后,子进程继续。对挂断信号的系统默认动作时终止该进程,为此必须提供一个信号处理程序以捕捉该信号。
下面是程序清单9-1的输出:
$ ./01 parent: pid = 4423, ppid = 3337, pgrp = 4423, tpgrp = 4423 child: pid = 4424, ppid = 4423, pgrp = 4423, tpgrp = 4423 SIGHUP received, pid = 4424 child: pid = 4424, ppid = 1698, pgrp = 4423, tpgrp = 3337 read error from controlling TTY, errno = 5 $ ps -axu | grep 1698 user 1698 0.0 0.1 41748 4092 ? Ss 14:23 0:00 init --user --state-fd 41 --restart
子进程的父进程ID变成init(1698,不是书上所说的1,Ubuntu 14.14上测试,Linux 3.13.0-32-generic)。
在进程调用pr_ids后,程序试图读标准输入。当后台进程试图读控制终端时,则对该后台进程组中产生SIGTTIN。但在这里,这是一个孤儿进程组,如果内核试图停止它,则此进程组中的进程就再也不会继续。POSIX.1规定,在这种情况下read返回出错,并将errno设置为EIO(5)。
最后,要注意的是父进程终止时,子进程被置入后台进程组中,因为父进程是有shell作为前台作业执行的。
略