对话期和进程组有一些其他特性:
这些特性见下图
登录时会自动建立控制终端
以下两个函数用来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能了解将终端输入和终端产生的信号送到何处。
#include <sys/types.h>
#include <unistd.h>
pid_t tcgetpgrp(int filedes);
返回值:成功返回前台进程组ID,出错-1
int tcsetpgrp(int filedes, pid_t pgrpid);
返回值:成功为0,出错为-1
函数tcgetpgrp返回前台进程组ID,它与在filedes上打开的终端相关。
如果进程有一个控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设置为pgrpid。pgrpid值应当是在同一会话期中的一个进程组的ID。filedes必须引用该对话期的控制终端。
大多数程序并不直接调用这两个函数。它们通常由作业控制shell调用。只有定义了—_POSIX_JOB_CONTROL,这两个函数才被定义了。否则它们返回出错。
作业控制允许在一个控制终端上启动多个作业(进程组),控制哪一个作业可以存取该终端,以及哪些作业在后台运行。作业控制要求三种形式的支持:
一个作业只是几个进程的集合,通常是一个进程管道。例如:
vim abc.c
在前台启动了只有一个进程的一个作业。下面的命令:
pr *.c | lpr &
make all &
在后台启动了两个作业。这两个作业所调用的进程都在后台运行。 当启动一个后台作业时,shell赋予它一个作业标识。并打印一个或几个进程ID。下面的操作过程显示了KornShell是如何处理这一点的。
$ make all > Make.out &
[1] 1475
$ pr *.c | lpr &
[2] 1490
$ 键入回车
[2] + Done pr *.c | lpr &
[1] + Done make all > Make.out &
make是作业号1,所启动的进程ID是1475.下一个管道线是作业号2,其中第一个进程的进程ID是1490。当作业已完成并且键入回车时,shell通知我们作业已完成。键入回车是为了让shell打印其提示符。shell并不在任意时间打印后台作业的状态改变,它只在打印其提示符之前这样做。
有三个特殊字符可使终端驱动程序产生信号,并将他们送至前台进程组,后台进程组作业不受影响。它们是:
如果后台作业试图读终端,终端驱动程序会检测这种情况,并且发送一个特定信号SIGTTIN给后台作业。这通常会停止此后台作业,而有关用户则会得到这种情况的通知,然后就可以将此作业转为前台作业运行,于是它就可以读终端。下面操作过程展示了这种情况:
$ cat > temp.foo & 在后台启动,但将从标准输入读
[1] 1681
$ 键入回车
[1] + Stopped (tty input) cat > temp.out &
$ fg &1 使1号作业成为前台作业
cat > temp.foo shell告诉我们现在哪一个作业在前台
hello, world 输入1行
^D 键入文件描述符
$ cat temp.foo 检查该行已送入文件
hello, world
shell在后台启动cat进程,但是当cat试图读其标准输入(控制终端)时,终端驱动程序知道它是后台作业,于是将SIGTTIN信号送至该后台作业。shell检测到其子进程的状态变化,并通知我们该作业已被停止。然后,用shell的fg命令将次停止的作业送入前台运行。这样做使shell将此作业转为前台进程组(tcsetpgrp),并将继续信号(SIGCONT)送给该进程组。因为该作业现在在前台进程组中,所以它可以读控制终端。
如果后台进程输出到控制终端会发生什么呢?这是一个可以允许或禁止的选择项。通常,可以用stty命令来改变这一选项
$ cat temp.foo & 在后台运行
[1] 1719
$ hello, world 在提示符出现后台作业的输出
键入回车
[1] + Done cat temp.foo &
$ stty tostop 禁止后台作业向控制终端输出
$ cat temp.foo & 在后台再次运行
[1] 1721
$ 键入回车,发现作业已停止
[1] + Stopped(tty output) cat temp.foo &
$ fg %1 将停止的作业恢复为前台作业
cat temp.foo shell告诉我们现在哪一个作业在前台
hello, world 该作业的输出
下图摘录了我们已说明的作业控制的某些功能。穿过终端驱动程序的实线表示:终端I/O和终端产生的信号总是从前台进程组连接到实际终端。对应于SIGTTOU信号的虚线表示后台进程组进程的输出是否出现在终端是可选择的。
首先使用不支持作业控制的经典的Bourne shell。如果执行:
ps -xj
则其输出为: PPID PID PGID SID TPGID COMMAND 1 163 163 163 163 -sh 163 163 163 163 163 ps 结果略去了现在无关的列。shell和ps命令两者位于同一对话期和前台进程组(163)中。因为163是在TGPID列中显示的进程组,所以称其为前台进程组。
说进程与终端进程组ID(TPGID列)相关联并不当。进程并没有终端进程控制组。进程属于一个进程组,而进程组属于一个对话期。对话期可能有,也可能没有控制终端。如果它确有一个控制终端,则此终端设备知道其前台进程的进程组ID。这一值可以用tcsetpgrp函数在终端驱动程序中设置。前台进程组ID是终端的一个属性,而不是进程的属性。取自终端设备驱动程序的该值是ps在TPGID列中打印的值。如果ps发现此对话期没有控制终端,则它在该列打印1。
如果在后台执行命令:
ps -xj &
则唯一改变的值是命令的进程ID。
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 163 163 163 163 ps
因为这种shell不知道作业控制,所以后台作业没有构成另一个进程组,也没有从后台作业处取走控制终端。
看一下Bourne shell如何处理管道线。执行下列命令:
ps -xj | cat1
其输出是:
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 200 163 163 163 cat1
200 201 163 163 163 ps
(程序cat1只是标准cat程序的一个副本,但名字不同)管道中最后一个进程是shell的子进程,该管道中的第一个进程则是最后一个进程的子进程。从中可以看出,shell fork一个它的副本,然后此副本再为管道线中的每条命令各fork一个进程。
&nsbp;如果在后台执行此管道线:
ps -xj | cat1 &
则只有进程ID改变了。因为shell并不处理作业控制,后台进程的进程组ID仍是163,如果终端进程组ID一样。
在没有作业控制时如果后台作业试图读控制终端,其处理方法是:如果该进程自己不重新定向标准输入,则shell自动将后台进程的标准输入重新定向到/dev/null。读/dev/null则产生一个文件结束。这就意味着后台cat进程立即读到文件尾,并正常结束。
在一条管道中执行三个进程:
ps -xj | cat1 | cat2
该管道中的最后一个进程是shell的子进程,而执行管道中其他命令的进程则是该最后进程的子进程。下图展示了所发生的情况:
一个父进程已终止的子进程称为孤儿进程(orphan process),这种进程由init进程收养。整个进程组也可以成为孤儿。 考虑一个进程,它fork了一个子进程然后终止。这在系统中是进场发生的,但是在父进程终止时,如果该子进程停止(用作业控制)该如何?下面的程序就是这种情况的一个例子。下图显示了程序已经启动,父进程已经fork了子进程之后的情况。
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include "ourhdr.h"
static void sig_hup(int);
static void pr_ids(char *);
int main(void)
{
char c;
pid_t pid;
pr_ids("parent");
if ( (pid = fork()) < 0) {
fprint(stderr, "fork error\n");
exit(1);
} else if (pid > 0) {
sleep(5); // sleep 5等待子进程退出
exit(0); // 父进程退出
} else { // 子进程
pr_ids("child");
signal(SIGHUP, sig_hup); //
kill(getpid(), SIGTSTP);
pr_ids("child");
if (read(0, &c, 1) != 1) {
printf("read error from control terminal,errno = %d\n", errno);
}
exit(0);
}
}
static void sig_hup(int signo) {
printf("SIGHUP received, pid = %d\n, getpid()");
return ;
}
static void pr_ids(char *name) {
printf("%s: pid = %d, ppid = %d, pgrp = %d\n", name, getpid(), getppid(), getpgrp());
fflush(stdout);
}
这里假定使用了一个作业控制shell。shell将前台进程放在一个进程组中(本例是512),shell则留在自己的组内(442)。子进程继承其父进程512进程组。在fork后:
下面是程序的输出:
因为两个进程,登录shell和子进程都写向终端,所以shell提示符和子进程的输出一起出现。
在子进程中调用pr_ids后程序企图读标准输入。正如前述,当后台进程组试图读控制终端时,则对该后台进程组产生SIGTTIN。但在这里这是一个孤儿进程组,如果内核用此信号终止它,则此进程组中的进程就再也不会继续。POSIX.1规定,read返回出错,其errno设置为EIO。
在父进程终止时,子进程变成后台进程组,因为父进程是由shell作为前台作业执行的。
原文:http://www.cnblogs.com/orlion/p/6445000.html