9.1 引言
本章将更详尽地说明进程组以及POSIX.1引入的会话的概念。还将介绍登陆shell(登录时所调用的)和所有从登陆shell启动的进程之间的关系。
9.1 终端登陆
系统管理员创建通常名为/etc/ttys的文件,其中每个终端设备都有一行,每一行说明设备名传递给getty程序的参数。当系统自举时,内核创建进程ID为1的进程,依旧是init进程。init进程使系统进入多用户状态。init进程读文件/etc/ttys,对每一个允许登陆的终端设备,init调用一次fork,所生成的子进程则执行(exec)getty程序。(init以空环境执行getty程序)
getty为终端设备调用open函数,以读、写方式将终端打开。如果设备是调制解调器,则open可能会在设备驱动程序中滞留,知道用户拨号调制解调器,并且呼叫被应答。一旦设备被打开,则文件描述符0、1、2就被设置到该设备。然后getty输出“login:”之类的信息,并等待用户键入用户名。如果终端支持多种速度,则getty可以测试特殊字符以便适当地更改终端速度(波特率)
当用户键入了用户名后,getty的工作就完成了。然后调用login程序。init以一个空环境调用getty。getty以终端名和在gettytab中说明的环境字符串为login创建一个环境。
login能执行多项工作。因为它得到了用户名,所以能调用getpwnam取得相应用户的口令文件登陆项。然后调用getpass以显示提示“password:”,接着读用户键入的口令。它调用crypt将用户键入的口令加密,并与该用户在阴影口令文件中登陆想的pw_passwd字段相比较。如果用户几次键入的口令都无效,则login以参数1调用exit表示登陆过程失败。父进程了解到子进程终止情况后,将在此调用fork,其后接着执行getty,对此终端重复上述过程。
如果用户正确登陆,login就将执行如下工作:
-将当前工作目录更改为该用户的起始目录
-调用chown改变该终端的所有权,是登陆用户成为它的所有者
-将对该终端设备的访问权限改变成用户读和写
-调用setgid和initgroups设置进程的组ID
-用login所得到的所有信息初始化环境:起始目录、shell、用户名,以及一个系统默认路径
-login进程改变为登陆用户的用户ID并调用该用户的登陆shell
到此为止,登录用户的登陆shell开始运行。
9.3 网络登陆
通过网络登陆时,终端和计算机之间的连接不是点对点链接。在这种情况下,login只是一种可用的服务,这与其他网络服务的性质相同。
在网络登陆情况下,所有登陆都经由内核的网络接口驱动程序,实现并不知道将会有多少这样的登陆。我们不是使进程等待每个可能的登陆,而是必须等待一个网络连接请求的到达。
为使同一个软件既能处理终端login,又能处理网络login,系统使用了一种称为伪终端的软件驱动程序,它仿真串行终端的运行行为,并将终端操作映射为网络操作
作为系统启动的一部分,init调用一个shell,使其执行shell脚本/etc/rc.由此shell脚本启动一个守护进程inetd。一旦此shell脚本终止,inetd的父进程就变成init。inetd等待TCP/IP连接请求到达主机,而当一个连接请求到达时,它执行一次fork,然后生成的子进程执行适当的程序。
我们假定到达了一个针对TELNET服务的TCP连接请求。TELNET是使用TCP协议的远程登陆应用程序。在另一台主机(它通过某种形式的网络与服务进程的主机相连接)上的用户,或者同一台主机上的用户启动TELNET客户进程,由此启动登陆过程:
telnet hostname
该客户进程打开一个到hostname主机的TCP连接,在hostname主机上启动的程序被称为TELNET服务进程。然后,客户进程和服务进程之间使用TELNET应用协议通过TCP连接交换数据。所发生的是启动客户进程的用户现在登录到了服务进程所在的主机。
然后,telnetd进程打开一个伪终端设备,并用fork分成两个进程。父进程处理通过网络连接的通信,子进程则执行login程序。父子进程通过伪终端相连接。在调用exec之前,子进程使其文件描述符0,1,2与伪终端相连。如果登陆正确,login就执行:更改当前工作目录为起始目录,设置登陆的组ID和用户ID,以及登陆用户的初始环境。然后login调用exec将自身替换为登陆用户的登陆shell
(当通过终端或网络登陆时,我们得到一个登陆shell,其标准输入、输出和标准出错连接到一个终端设备或者伪终端设备上。
9.4 进程组
函数getpgrp返回调用进程的进程组ID
#include<unistd.h> pid_t getpgrp(void); //返回值:调用进程的进程组ID
进程可以通过调用setpgid来加入一个现有的组或者创建一个新进程组
#include<unistd.h> int setpgid(pid_t pid,pid_t pgid); //返回值:若成功则返回0,若出错则返回-1
9.5 会话
会话是一个或多个进程组的集合。
通常是shell的管道线将几个进程编程一组的。进程调用setsid函数建立一个新会话
#include<unistd.h> pid_t setsid(void); //返回值:若成功则返回进程组ID,若出错则返回-1
如果调用此函数的进程不是一个进程组的组长,则此函数就会创建一个新会话,结果将发生下面3件事:
(1)该进程编程新会话首进程(会话首进程是创建该会话的进程),此时该进程是新会话中的惟一进程
(2)该进程称为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID
(3)该进程没有控制终端。如果在调用setsid之前该进程有一个控制终端,那么这种联系也会被中断
如果该调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不发生这种情况,通常先调用fork,然后使其父进程终止,而子进程继续。因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,所以这就保证子进程不会是一个进程组的组长。
9.6 控制终端
会话和进程组有一些其他特性:
-一个会话可以有一个控制终端。这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(在网络登陆情况下)
-建立与控制终端链接的会话首进程被称为控制进程
-一个会话中的几个进程组可被分成一个前台进程组以及一个或几个后台进程组
-如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组
-无论何时键入终端的中断键,就会将中断信号发送给前台进程组的所有进程
-无论何时键入终端的退出键,就会将退出信号发送给前台进程组的所有进程
-如果终端接口检测到调制解调器已经断开连接,则将挂断信号发送给控制进程
9.7 tcgetpgrp、tcsetpgrp和tcgetsid函数
需要有一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能了解将终端输入和终端产生的信号送到何处
#include<unistd.h> pid_t tcgetpgrp(int filedes); //返回值:若成功则返回前台进程组的进程ID,若出错则返回-1 int tcsetpgrp(int filedes,pid_t pgrpid); //返回值:若成功则返回0,若出错则返回-1
9.8 作业控制
作业控制要求下面三种形式的支持:
(1)支持作业控制的shell
(2)内核中的终端驱动程序必须支持作业控制
(3)内核必须提供对某些作业控制信号的支持
9.9 shell执行程序
管道线中最后一个进程是shell的子进程,该管道线中的第一个进程则是最后一个进程的子进程。当最后一个进程终止时,shell得到通知
9.10 孤儿进程组
我们曾提及,一个其父进程已经终止的进程称为孤儿进程,这种进程由init进程“收养”。现在我们要说明整个进程组也可称为“孤儿”
实例: 9_1 创建一个孤儿进程组
1 #include"apue.h" 2 #include<errno.h> 3 4 static void sig_hup(int signo) 5 { 6 printf("SIGHUP received,pid=%d\n",getpid()); 7 } 8 static void pr_ids(char *name) 9 { 10 printf("%s:pid=%d,ppid=%d,pgrp=%d,tpgrp=%d\n",name,getpid(),getppid(), 11 getpgrp(),tcgetpgrp(STDIN_FILENO)); 12 fflush(stdout); 13 } 14 int main() 15 { 16 char c; 17 pid_t pid; 18 pr_ids("parent"); 19 if((pid=fork())<0){ 20 err_sys("fork error"); 21 }else if(pid>0){ 22 sleep(5); 23 exit(0); 24 } 25 else{ 26 pr_ids("child"); 27 signal(SIGHUP,sig_hup); 28 kill(getpid(),SIGTSTP); 29 pr_ids("child"); 30 if(read(STDIN_FILENO,&c,1)!=1) 31 printf("read error from controlling TTY,errno=%d\n",errno); 32 exit(0); 33 }}