[APUE]进程关系(上)
一、终端登录
1. 4.3+BSD终端登录
系统管理员创建一个通常名为/etc/ttys的文件,其中,每个终端设备有一行,每一行说明设备名和传到getty程序的参数,这些参数说明了终端的波特率。当系统bootstrap时内核创建进程ID 1,也就是init进程。init进程使系统进入多用户状态。init读文件/etc/ttys,对每一个允许登录的终端设备,init调用一次fork,它所生成的子进程则执行程序getty。这种情况见下图:
图中每个进程的实际用户ID和有效用户ID都是0(即都有root权限)。init以空环境执行getty程序。
getty对终端设备调用open函数,以读、写方式将终端打开。如果设备是调制解调器,则open可能会在设备驱动程序中滞留,直到用户拨号调制解调器,并且线路被接通。一旦设备被打开,则文件描述符0、1、2就被设置到该设备。然后getty输出"login:"之类的信息,并等待用户键入用户名。如果终端支持多种速度,则getty可以测试特殊字符以便适当地更改终端速度(波特率)。
当用户键入用户名后,getty就完成了,然后它以类似于下列的方式调用login程序:
execle("/usr/bin/login", "login", "-p" username, (char *) 0, envp);
(在gettytab文件中可能会有一些选择使其调用其他程序,但系统默认是login程序)。init以一个空环境调用getty。getty以终端名(例如TERM=foo,其中终端foo的类型取自gettytab文件)和在gettytab中的环境字符串为login创建一个环境(envp参数)。-p标志通知login保留传给它的环境,也可以将其他环境字符串加到该环境中,但是不要替换它。下图显示了login刚被调用后这些进程的状态。
因为最初的init进程具有root权限,所以图中所有进程都有root权限。图中底部三个进程的进程ID相同,因为进程ID不会因执行exec而改变。并且除了最初的init进程,所有的进程均有一个父进程ID。
login能处理多项工作。因为它得到了用户名,所以能调用getpwnam取得相应用户的口令文件登陆项。然后调用getpass以显示提示"Password:"接着读用户键入的口令。它调用crypt将用户键入的口令加密,并与该用户口令文件中登陆项的pw_passwd字段相比较。如果用户几次键入的口令都无效,则login以参数1调用exit表示登录过程失败。父进程(init)了解到子进程的终止情况后,将再次调用fork,其后又跟着执行getty,对此终端重复上述过程。
如果用户正确登录,login就将当前工作目录更改为用户的home目录。它也调用chown改变该终端的所有权,使该用户成为所有者和组所有者。将对该终端设备的存取许可权改变成:用户读、写和组写。调用setgid及initgroup设置进程的组ID。然后调用login所得到的所有信息初始化环境:起始目录(HOME)、shell(SHELL)、用户名(USER和LOGNAME),以及一个系统默认路径(PATH)。最后login进程改变为登录用户的用户ID(setuid)并调用该用户的登陆shell,其方式类似于:
execl("/bin/sh", "-sh", (char *) 0);
argv[0]的第一个字符是一个标志,表示该shell被调用为登录shell。shell可以查看此字符,并相应地修改其启动过程
login所做的比上面说的要多。
到此为止,登录用户的登录shell开始运行。其父进程ID是init进程ID(进程ID 1),所以当此进程终止时,init进程会收到通知(接收到SIGGHLD信号),它会对该终端重复全部上述过程。登陆shell的文件描述符0,1和2设置为终端设备。下图显示了这种安排。
现在登录shell读其启动文件。这些启动文件通常改变某些环境变量,加上一些环境变量。当执行完启动文件后,用户最后得到shell的提示符,并能键入命令。
2. SVR4终端登录
SVR4支持两种方式的登录:(a)getty方式,这与上面的一样。(b)ttymon登录,这是SVR4的一种新功能。通常getty用于控制台,ttymon则用于其他终端的登录。
二、网络登录
1. 4.3+BSD网络登录
终端登录时,init知道哪些终端设备可用来登录,并为每一个设备生成一个getty进程。但是网络登录都经过内核的网络界面驱动程序,事先并不知道有多少个这样的登录。不是使一个进程等待每一个可能的登录,而是必须等待一个网络连接请求的到达。在4.3+BSD中,有一个称为inetd的进程,它等待大多数网络连接。
作为系统启动的一部分,init调用一个shell,使其执行shell脚本stc/rc。由此shell脚本启动一个精灵进程inetd。一旦此shell脚本终止,inetd的父进程就变成init。inetd等待TCP/IP连接请求到达主机,而当一个连接请求到达时,它执行一次fork,然后该子进程执行适当的程序。
我们假定到达了一个对于TELNET服务器的TCP连接请求。TELNET是使用TCP协议的远程登录应用程序。在另一个主机上的用户,或在同一个主机上的用户启动TELNET客户端进程启动登录过程:
telnet hostname
该客户端进程打开一个到名为hostname的主机的TCP连接,在hostname主机上启动的程序被称为TELNET服务器。然后客户端进程和服务器进程之间使用TELNET应用协议通过TCP连接交换数据。所发生的是启动客户端进程的用户现在登录到了服务器进程所在的主机。下图显示了在执行TELNET服务器进程(称为telnetd)中所涉及的进程序列。
然后telnetd进程打开一个伪终端设备,并用fork生成一个子进程。父进程处理通过网络连接的通信,子进程则执行login程序。父、子进程通过伪终端相连接。在调用exec之前,子进程使其文件描述符0,1,2与伪终端相连。如果登录正确,login就执行:更改当前工作目录为起始目录,设置登录用户的组ID和用户ID,以及登录用户的起始环境。然后login用exec将其自身替换为登录用户的登录shell。下图显示了到达这一点时的进程安排
当通过终端或网络登录时,我们得到一个登录shell,其标准输入、输出和标准出错连接到一个终端或者伪终端设备上。
2. SVR4网络登录
SVR4中网络登录的情况与4.3+BSD中的几乎一样。同样使用了inetd服务器进程,但是在SVR4中inetd是作为一种服务存取控制器sac调用的,其父进程不是init。最后得到的结果与上图一样。
三、进程组
每个进程除了有一进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合,每个进程组有一个唯一的进程组ID。进程组ID类似进程ID,它是一个正整数,并可存放在pid_t数据类型中。函数getpgrp返回调用进程的进程组ID
#include <sys/types.h>
#include <unistd.h>
pid_t getpgrp(void);
返回值: 调用进程的进程组ID
每个进程组都有一个组长进程。组长进程的标识是:其进程组ID等于其进程ID。
进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,该进程组就存在,与进程组长是否终止无关。从进程组创建到其中最后一个进程终离开(该进程可以终止也可以加入另一个进程组)的时间区间称为进程组的生命期。
进程调用setpgid可以参加一个现存的组或者创建一个新进程组
#include <sys/types.h>
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
返回值: 若成功则为0,出错为-1
这将pid进程的进程组ID设置为pgid。如果这两个参数相等,则由pid指定的进程变成进程组组长。
一个进程只能为它自己或它的子进程设置进程组ID。在它的子进程调用了exec后,它就不再改变该子进程的进程组ID
如果pid是0,则使用调用者的进程ID。如果pgid是0,则由pid指定的进程ID被用作为进程组ID。
如果系统不支持作业控制,那么就不定义_POSIX_JOB_CONTROL,在这种情况下,该函数返回错误,errno设置为ENOSYS。
在大多数作业控制shell中,在fork之后调用此函数,使父进程设置其子进程的进程组ID,然后使子进程设置其自己的进程组ID。这些调用中有一个是冗余的,但这样做可以保证父、子进程在进一步操作之前,子进程都进入了该进程组。如果不这样做的话,那么就产生一个竟态条件,因为它依赖于哪一个进程先执行。
四、对话期
对话期(session)是一个或多个进程组的集合。例如,可以有下图中所示的安排。在一个对话期中有三个进程组。通常由shell的管道线将几个进程编成一组的。例如下图中的安排可能是由下列形式的shell命令形成的:
proc1 | proc2 &
proc3 | proc4 | proc5
进程调用setsid函数就可以建立一个新对话期。
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
返回值:若成功则为进程组ID,若出错则为-1
如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新对话期,结果为:
- 此进程变成该新对话期的对话期的首进程(session leader, 对话期首进程是创建该对话期的进程)。此进程是该新对话期中的唯一进程。
- 此进程成为一个新进程组的组长进程。新进程组ID是此调用进程的进程ID。
- 此进程没有控制终端。如果在调用setsid之前此进程有一个控制终端,那么这种联系也被解除。
如果此调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不处于这种情况,通常先调用fork,然后使父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,所以其不可能是进程组组长。