Unix之进程关系(九)
本文主体内容来自《UNIX环境高级编程第三版》。
本章将更详细地说明进程组以及POSIX.1引入的会话的概念。还将介绍登录shell(登录时所调用的)和所有从登录shell启动的进程之间的关系。
终端登录
我们现在描述的过程用于经由终端登录至UNIX系统。该过程几乎与所使用的终端类型无关,所使用的终端可以是基于字符的终端、仿真基于字符终端的图形终端,或者运行窗口系统的图形终端。
BSD终端登录
系统管理者创建通常名为/etc/ttys
的文件,其中,每个终端设备都有一行,每一行说明设备名和传到
getty
程序的参数。当系统自举时,内核创建进程ID为1的进程,也就是init进程。init进程使系统进入多用户模式。init读取文件/etc/ttys
,对每一个允许登录的终端设备,init调用一次fork,它所生成的子进程则exec
getty程序。这种情况示于图9-1中。
图9-1中所有进程的实际用户ID和有效用户ID都是0(也就是说,它们都具有超级用户特权)。init以空环境exec getty程序。
getty 对终端设备调用 open 函数,以读、写方式将终端打开。如果设备是调制解调器,则open可能会在设备驱动程序中滞留,直到用户拨号调制解调器,并且线路被接通。一旦设备被打开,则文件描述符0、1、2就被设置到该设备。然后getty输出“login: ”之类的信息,并等待用户键入用户名。
当用户键入了用户名后,getty的工作就完成了。然后它以类似于下列的方式调用login程序:
execle("/bin/login", "login", "-p", username, (char *)0, envp);
(在gettytab文件中可能会有一些选项使其调用其他程序,但系统默认是login程序)。init以一个空环境调用getty。getty以终端名(如TERM=foo,其中终端foo的类型取自gettytab文件)和在gettytab中说明的环境字符串为login创建一个环境(envp参数)。-p标志通知login
保留传递给它的环境,也可将其他环境字符串加到该环境中,但是不要替换它。图9-2显示了login刚被调用后这些进程的状态。
因为最初的init进程具有超级用户特权,所以图9-2中的所有进程都有超级用户特权。图9-2中底部3个进程的进程ID相同,因为进程ID不会因执行exec而改变。并且,除了最初的init进程,所有进程的父进程ID均为1。
login
能处理多项工作。因为它得到了用户名,所以能调用
getpwnam
取得相应用户的口令文件登录项。然后调用getpass
以显示提示“Password:
”,接着读用户键入的口令(自然,禁止回显用户键入的口令)。它调用crypt
将用户键入的口令加密,并与该用户在阴影口令文件中登录项的pw_passwd字段相比较。如果用户几次键入的口令都无效,则login以参数1调用exit表示登录过程失败。父进程(init)了解到子进程的终止情况后,将再次调用fork,其后又执行了getty,对此终端重复上述过程。
这是UNIX系统传统的用户身份验证过程。现代UNIX系统已发展到支持多个身份验证过程。例如,FreeBSD、Linux、Mac OS X 以及 Solaris 都支持被称为 PAM(Pluggable Authentication Modules,可插入的身份验证模块)的更加灵活的方案。PAM 允许管理人员配置使用何种身份验证方法来访问那些使用PAM库编写的服务。
如果应用程序需要验证用户是否具有适当的权限去执行某个服务,那么我们要么将身份验证机制编写到应用中,要么使用PAM库得到同样的功能。使用PAM的优点是,管理员可以基于本地策略、针对不同任务配置不同的验证用户身份的方法。
如果用户正确登录,login就将完成如下工作。
将当前工作目录更改为该用户的起始目录(chdir)。
调用chown更改该终端的所有权,使登录用户成为它的所有者。
将对该终端设备的访问权限改变成“用户读和写”。
调用setgid及initgroups设置进程的组ID。
用login得到的所有信息初始化环境:起始目录(HOME)、shell(SHELL)、用户 名(USER和LOGNAME)以及一个系统默认路径(PATH)。
login进程更改为登录用户的用户ID(setuid)并调用该用户的登录shell,其方式类似于:
execl("/bin/sh", "-sh", (char *)0)
;
argv[0]的第一个字符负号“−”是一个标志,表示该shell被作为登录shell调用。shell可以查看此字符,并相应地修改其启动过程。
login程序实际所做的比上面说的要多。它可选择地打印日期消息(message-of-the-day)文件、检查新邮件以及执行其他一些任务。
至此,登录用户的登录shell开始运行。其父进程ID是init进程(进程ID
1),所以当此登录shell终止时,init会得到通知(接到SIGCHLD
信号),它会对该终端重复全部上述过程。登录shell的文件描述符0、1和2设置为终端设备。图9-3显示了这种安排。
现在,登录 shell 读取其启动文件(Bourne shell和Korn shell是.profile,GNU Bourne-again shell是.bash_profile、.bash_login或.profile, C shell是.cshrc和.login)。这些启动文件通常更改某些环境变量并增加很多环境变量。例如,大多数用户设置他们自己的 PATH 并常常提示实际终端类型(TERM)。当执行完启动文件后,用户最后得到 shell提示符,并能键入命令。
Mac OS X终端登录
Mac OS X部分地基于FreeBSD,所以其终端登录进程与BSD终端登录进程的工作步骤基本相同。但是,Mac OS X有些不同之处。
init的工作是由
launchd
完成的。一开始提供的就是图形终端。
Linux终端登录
Linux的终端登录过程非常类似于BSD。确实,Linux login命令是从4.3BSD login命令派生出来的。BSD登录过程与Linux登录过程的主要区别在于说明终端配置的方式。
在System
V的init文件格式之后,有些Linux发行版的init程序使用了管理文件方式。在这些系统中,/etc/inittab
包含配置信息,指定了init应当为之启动getty进程的各终端设备。
其他Linux发行版本,如最近的Ubuntu发行版,配有称为“Upstart”的init程序。使用存放在/etc/init目录的*.conf命名的配置文件。
Solaris终端登录
Solaris支持两种形式的终端登录:(a)getty方式,这与前面对BSD终端登录的说明一样;(b)ttymon登录,这是SVR4引入的一种新特性。通常,getty用于控制台,ttymon则用于其他终端的登录。
网络登录
通过串行终端登录至系统和经由网络登录至系统两者之间的主要(物理上的)区别是:网络登录时,在终端和计算机之间的连接不再是点到点的。在网络登录情况下,login仅仅是一种可用的服务,这与其他网络服务(如FTP或SMTP)的性质相同。
在上节所述的终端登录中,init知道哪些终端设备可用来进行登录,并为每个设备生成一个getty进程。但是,对网络登录情况则有所不同,所有登录都经由内核的网络接口驱动程序(如以太网驱动程序),而且事先并不知道将会有多少这样的登录。因此必须等待一个网络连接请求的到达,而不是使一个进程等待每一个可能的登录。
为使同一个软件既能处理终端登录,又能处理网络登录,系统使用了一种称为伪终端(pseudo terminal)的软件驱动程序,它仿真串行终端的运行行为,并将终端操作映射为网络操作,反之亦然。
BSD网络登录
在BSD中,有一个inetd进程(有时称为因特网超级服务器),它等待大多数网络连接。本节将说明 BSD 网络登录中所涉及的进程序列。
作为系统启动的一部分,init调用一个shell,使其执行shell脚本/etc/rc。由此shell脚本启动一个守护进程inetd。一旦此shell脚本终止,inetd的父进程就变成init。inetd等待TCP/IP连接请求到达主机,而当一个连接请求到达时,它执行一次fork,然后生成的子进程exec适当的程序。
假定一个对于TELNET服务进程的TCP连接请求到达。TELNET是使用TCP协议的远程登录应用程序。在另一台主机(它通过某种形式的网络与服务进程主机相连接)上的用户,或在同一个主机上的一个用户启动TELNET客户进程,由此启动登录过程:
telnet hostname
该客户进程打开一个到hostname主机的TCP连接,在hostname主机上启动的程序被称为TELNET服务进程。然后,客户进程和服务进程之间使用TELNET应用协议通过TCP连接交换数据。启动客户进程的用户现在登录到了服务进程所在的主机(当然,假定用户在服务进程主机上有一个有效的账号)。图9-4显示了在执行TELNET服务进程(称为telnetd)中所涉及的进程序列。
然后,telnetd进程打开一个伪终端设备,并用fork分成两个进程。父进程处理通过网络连接的通信,子进程则执行login程序。父进程和子进程通过伪终端相连接。在调用exec之前,子进程使其文件描述符0、1、2与伪终端相连。如果登录正确,login就执行上述的同样步骤—更改当前工作目录为起始目录、设置登录用户的组ID、用户ID以及初始环境。然后login调用exec将其自身替换为登录用户的登录shell。图9-5显示了到达这一点时的进程安排。
Mac OS X网络登录
Mac OS X是部分地基于FreeBSD的,所以其网络登录与BSD网络登录基本相同。但Mac OS X上telnet守护进程是从launchd运行的。
telnet守护进程在Mac OS X中默认是禁用的(虽然可以通过launchctl(1)命令启用)。Mac OS X上执行网络登录的更好办法是用使ssh(安全shell命令)。
Linux网络登录
除了有些版本使用扩展的因特网服务守护进程xinetd代替inetd进程外,Linux网络登录的其他方面与BSD网络登录相同。xinetd进程对它所启动的各种服务的控制比inetd提供的控制更加精细。
Solaris网络登录
Solaris中网络登录的工作过程与BSD和Linux中的步骤几乎一样。同样使用了类似于BSD版的inetd服务进程,但是在Solaris中,inetd服务进程在服务管理设施(Service Management Facility,SMF)下作为restarter运行。这个restarter是守护进程,它负责启动和监视其他守护进程,如果其他守护进程失败的话,restarter重启这些失效进程。虽然inetd 服务程序由SMF中的主restarter启动,但实际上主restarter是由init程序启动的,最后得到的结果与图9-5中一样。
进程组
每个进程除了有一进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合。通常,它们是在同一作业中结合起来的,同一进程组中的各进程接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID。
函数 getpgrp
返回调用进程的进程组ID。
1 |
|
Single UNIX Specification定义了 getpgid
函数,参数是
pid
,返回该进程的进程组 ID。
1 |
|
若 pid
是0,返回调用进程的进程组ID,于是,getpgid(0);
等价于
getpgrp()
;
每个进程组有一个组长进程。组长进程的进程组ID等于其进程ID。
进程组组长可以创建一个进程组、创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命期。某个进程组中的最后一个进程可以终止,也可以转移到另一个进程组。
进程调用 setpgid 可以加入一个现有的进程组或者创建一个新进程组。
1 |
|
setpgid
函数将 pid
进程的进程组ID设置为
pgid
。如果这两个参数相等,则由 pid
指定的进程变成进程组组长。如果 pid
是0,则使用调用者的进程ID。另外,如果 pgid
是0,则由pid指定的进程ID用作进程组ID。
一个进程只能为它自己或它的子进程设置进程组ID。在它的子进程调用了exec后,它就不再更改该子进程的进程组ID。
会话
会话(session)是一个或多个进程组的集合。例如,可以具有下图所示的安排。其中,在一个会话中有3个进程组。
通常是由shell的管道将几个进程编成一组的。例如,图中的安排可能是由下列形式的shell命令形成的:
1 | procl | proc2 & |
进程调用 setsid
函数建立一个新会话。
1 |
|
如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。具体会发生以下3件事。
(1)该进程变成新会话的会话首进程(session leader,会话首进程是创建该会话的进程)。此时,该进程是新会话中的唯一进程。
(2)该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID。
(3)该进程没有控制终端(下一节讨论控制终端)。如果在调用
setsid
之前该进程有一个控制终端,那么这种联系也被切断。
如果该调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,这就保证了子进程不是一个进程组的组长。
Single UNIX
Specification只说明了会话首进程,而没有类似于进程ID和进程组ID的会话ID。显然,会话首进程是具有唯一进程ID的单个进程,所以可以将会话首进程的进程ID视为会话ID。会话ID这一概念是由SVR4引入的。历史上,基于BSD的系统并不支持这个概念,但后来改弦易辙也支持了会话ID。getsid
函数返回会话首进程的进程组ID。
1 |
|
如若 pid
是0,getsid
返回调用进程的会话首进程的进程组ID。出于安全方面的考虑,一些实现有如下限制:如若pid并不属于调用者所在的会话,那么调用进程就不能得到该会话首进程的进程组ID。
控制终端
会话和进程组还有一些其他特性。
一个会话可以有一个控制终端(controlling terminal)。这通常是终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。
建立与控制终端连接的会话首进程被称为控制进程(controlling process)。
一个会话中的几个进程组可被分成一个前台进程组(foreground process group)以及一个或多个后台进程组(background process group)。
如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。
无论何时键入终端的中断键(常常是Delete或Ctrl+C),都会将中断信号发送至前台进程组的所有进程。
无论何时键入终端的退出键(常常是Ctrl+),都会将退出信号发送至前台进程组的所有进程。
如果终端接口检测到调制解调器(或网络)已经断开连接,则将挂断信号发送至控制进程(会话首进程)。
这些特性示于下图中。
函数tcgetpgrp、tcsetpgrp和tcgetsid
需要有一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能知道将终端输入和终端产生的信号发送到何处。
1 |
|
函数 tcgetpgrp
返回前台进程组ID,它与在fd上打开的终端相关联。
如果进程有一个控制终端,则该进程可以调用 tcsetpgrp
将前台进程组ID设置为 pgrpid
。pgrpid
值应当是在同一会话中的一个进程组的ID。fd
必须引用该会话的控制终端。
大多数应用程序并不直接调用这两个函数。它们通常由作业控制shell调用。
给出控制TTY的文件描述符,通过 tcgetsid
函数,应用程序就能获得会话首进程的进程组ID。
1 |
|
需要管理控制终端的应用程序可以调用 tcgetsid
函数识别出控制终端的会话首进程的会话ID(它等价于会话首进程的进程组ID)。
作业控制
作业控制允许在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业在后台运行。
从shell使用作业控制功能的角度观察,用户可以在前台或后台启动一个作业。一个作业只是几个进程的集合,通常是一个进程管道。例如:
vi main.c
在前台启动了只有一个进程组成的作业。
下面的命令:
pr *.c | lpr &
make all &
在后台启动了两个作业。这两个后台作业调用的所有进程都在后台运行。
可以键入一个影响前台作业的特殊字符——挂起键(通常采用
Ctrl+Z),与终端驱动程序进行交互作用。键入此字符使终端驱动程序将信号SIGTSTP
发送至前台进程组中的所有进程,后台进程组作业则不受影响。实际上有3个特殊字符可使终端驱动程序产生信号,并将它们发送至前台进程组,它们是:
中断字符(一般采用Delete或Ctrl+C)产生SIGINT;
退出字符(一般采用Ctrl+)产生SIGQUIT;
挂起字符(一般采用Ctrl+Z)产生SIGTSTP。
下图总结了前面已说明的作业控制的某些功能。穿过终端驱动程序框的实线表明终端
I/O 和终端产生的信号总是从前台进程组连接到实际终端。对应于
SIGTTOU
信号的虚线表明后台进程组进程的输出是否出现在终端是可选择的。
FreeBSD实现
下面从session结构开始说明图中标出的各个字段。每个会话都分配一个session结构(例如,每次调用setsid时)。
s_count 是该会话中的进程组数。当此计数器减至0时,则可释放此结构。
s_leader 是指向会话首进程proc结构的指针。
s_ttyvp 是指向控制终端vnode结构的指针。
s_ttyp 是指向控制终端tty结构的指针。
s_sid 是会话ID。会话ID这一概念并非Single UNIX Specification的组成部分。
在调用setsid时,在内核中分配一个新的session结构。s_count设置为1,s_leader设置为调用进程 proc 结构的指针,s_sid 设置为进程 ID,因为新会话没有控制终端,所以s_ttyvp和s_ttyp设置为空指针。
接着说明 tty 结构。每个终端设备和每个伪终端设备均在内核中分配这样一种结构。
t_session 指向将此终端作为控制终端的session结构(注意,tty结构指向session结构,session结构也指向tty结构)。终端在失去载波信号时使用此指针将挂起信号发送给会话首进程。
t_pgrp 指向前台进程组的pgrp结构。终端驱动程序用此字段将信号发送给前台进程组。由输入特殊字符(中断、退出和挂起)而产生的3个信号被发送至前台进程组。
t_termios 是包含所有这些特殊字符和与该终端有关信息(如波特率、回显打开或关闭等)的结构。
t_winsize 是包含终端窗口当前大小的winsize型结构。当终端窗口大小改变时,信号SIGWINCH被发送至前台进程组。
为了找到特定会话的前台进程组,内核从session结构开始,然后用s_ttyp得到控制终端的tty结构,再用t_pgrp得到前台进程组的pgrp结构。
pgrp结构包含一个特定进程组的信息。其中各相关字段具体如下。
pg_id 是进程组ID。
pg_session 指向此进程组所属会话的session结构。
pg_members 是指向此进程组proc结构表的指针,该 proc 结构代表进程组的成员。proc结构中p_pglist结构是双向链表,指向该组中的下一个进程和上一个进程。直到遇到进程组中的最后一个进程,它的proc结构中p_pglist结构为空指针。
proc结构包含一个进程的所有信息。
p_pid 包含进程ID。
p_pptr 是指向父进程proc结构的指针。
p_pgrp 指向本进程所属的进程组的pgrp结构的指针。
p_pglist 是一个结构,其中包含两个指针,分别指向进程组中上一个和下一个进程。
最后还有一个vnode结构。如前所述,在打开控制终端设备时分配此结构。进程对/dev/tty
的所有访问都通过vnode结构。