9 进程关系
9.1 引言
9.2 终端登录
1 BSD终端登录
系统管理者创建通常名为/etc/ttys
的文件,其中每个终端设备都有一行,每一行说明设备名和传到getty程序的参数。当系统自举时,内核创建进程ID为1的init进程。init进程使系统进入多用户模式。init读取文件etc/ttys
,对每一个允许登录的终端设备,init调用一次fork,它所产生的子进程则exec getty程序。
getty对终端设备调用open函数,以读、写方式将终端打开。如果设备是调制解调器,则open可能会在设备驱动程序中滞留,直到用户拨号调制解调器,并且线路被接通。一旦设备被打开,则文件描述符0、1、2就被设置到该设备。然后getty输出login:
之类的信息,并等待用户键入用户名。此后getty的使命就完成了,后面交给login程序: execle("/bin/login", "login", "-p", username, (char *)0, envp);
![9-1](https://i-blog.csdnimg.cn/blog_migrate/508183f9ef6946ff6c7ab1fbb69dca89.png)
图9-1 为允许终端登录,init调用的进程
因为最初的init进程具有超级用户特权,所以图上fork出的进程都有超级用户特权。
![9-2](https://i-blog.csdnimg.cn/blog_migrate/301834d9c793032ea27e2a004e687dd6.png)
图9-2 login调用后进程的状态
上图下边3个进程的进程ID相同,因为进程ID不会因执行exec而改变。并且,除了最初的init进程,所有进程的父进程ID均为1。
login能处理多项工作。首先他得到用户名后可以调用getpwnam取得相应用户的口令文件登录项。然后调用getpass以显示提示Passwd:
,接着读用户键入的口令(会禁止回显)。他调用crypt将口令加密,并和shadow文件中的登录项pw_passwd
字段相比较。若比较错误,login以参数1调用exit表示登录失败。父进程(init)了解到子进程终止情况后,再次调用fork,执行getty,重复上述过程。
如果用户正确登录,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可以查看次字符,并相应的修改其启动过程。
![9-3](https://i-blog.csdnimg.cn/blog_migrate/f09465d9403be1250622e7dc61da3db6.png)
图9-3 终端登录完成各种设置后的进程安排
2 Mac OS X 终端登录
它部分基于FreeBSD,登录进程工作基本相同,但有下列不同之处:
- init的工作是由launchd完成的。
- 一开始提供的就是图形终端。
3 Linux 终端登录
它非常类似于BSD,因为Linux login命令是从4.3BSDlogin命令派生出来的。BSD和Linux登录过程主要区别在于说明终端配置的方式。
最近的Ubuntu版本配有称为“Upstart”的init程序。使用存放在/etc/init
目录的 *.conf
的配置文件。如,运行/dev/tty1
上的getty需要的说明可能放在/etc/init/tty1.conf
文件中。
4 Solaris 终端登录
支持两种形式的终端登录:
- getty方式,与前面BSD登录说明一样;
- ttymon登录,这是SVR4引入的一种新特性。通常,getty用于控制台,ttymon则用于其他终端的登录。
ttymon命令是服务访问设施(Service Access Facility,SAF)的一部分。SAF的目的是用一致的方式对提供系统访问的服务进行管理。
9.3 网络登录
通过串行终端登录至系统和由网路登录到系统两者之间主要(物理上的)区别:网路登录时,在终端和计算机之间的连接不再是点对点,login仅仅是一种可用的服务,这与其他网络服务的性质相同。
![9-4](https://i-blog.csdnimg.cn/blog_migrate/ad915d49aff50bc2b74ed18afe9e9bdf.png)
图9-4 执行TELNET服务进程时调用的进程序列
![9-5](https://i-blog.csdnimg.cn/blog_migrate/cd4cd9795edb8ecf3adf4f0d28d9c13d.png)
图9-4 网络登录完成各种设置后的进程安排
9.4 进程组
#include <unistd.h>
pid_t getpgrp(void);
return: 调用进程的进程组ID
早期BSD派生系统中参数是pid,后用下函数模仿此种行为。
#include <unistd.h>
pid_t getpgid(pid_t pid);
return: 进程组ID,error:-1
int setpgid(pid_t pid, pid_t pgid);
return: 0,error: -1
可加入一个现有的进程组或创建一个新进程组。
组长进程
1) 组长进程标识: 其进程组ID == 其进程ID
2) 组长进程可以创建一个进程组,创建该进程组中的进程,然后终止
3) 只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关
4) 进程组生存期: 进程组创建到最后一个进程离开(终止或转移到另一个进程组)进程组id == 父进程id,即父进程为组长进程
PID: process ID
PPID: process parent ID
PGID: process group ID
SID: session ID
TGID: thread group ID
9.5 会话
会话(session)是一个或多个进程组的集合。shell中的管道讲几个进程编成一组,如:
proc1 | proc2 &
proc3 | proc4 | proc5
![9-6](https://i-blog.csdnimg.cn/blog_migrate/fc094e469617a398f1c77fe7ea5ba78a.png)
图9-6 进程组和会话中进程安排
#include <unistd.h>
pid_t setsid(void);
return: 进程组ID;error:-1
建立一个会话.
#include <unistd.h>
pid_t getsid(pid_t pid);
return: 会话首进程的进程组ID,error: -1
9.6 控制终端
会话和进程组有一些其他特性:
- 一个会话可以有一个控制终端。
- 建立和控制终端连接的会话首进程被称为控制进程。
- 一个会话中的几个进程组可被分为一个前台进程组以及一个或者多个后台进程组
- 如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组
- 无论何时键入终端的中断键(常常是DELETE或Ctrl+C),就会将中断信号发送给前台进程组的所有进程
- 无论何时键入终端的退出键(常常是Ctrl+\),就会将退出信号发送给前台进程组中的所有进程
- 如果终端接口检测到调制解调器已经断开连接,则将挂断信号发送给控制进程(会话首进程)。
这些特性于下图:
![9-7](https://i-blog.csdnimg.cn/blog_migrate/749032342e753b91a50e868b3e2cfdc7.png)
图9-7 进程组、会话和控制终端
9.7 函数tcgetpgrp、tcsetpgrp和tcgetsid
需要一种方法来通知内核哪一个进程组是前台进程组:
#include <unistd.h>
pid_t tcgetpgrp(int fd);
return: 前台进程组ID,error: -1
int tcsetpgrp(int fd, pid_t pgrpid);
return: 0,error: -1
#include <termios.h>
pid_t tcgetsid(int fd);
return: 会话首进程的进程组ID,error: -1
9.8 作业控制
它是BSD在1980左右增加的一个特性。它允许一个终端上启动多个作业(进程组),由它控制哪一个作业可以访问该终端以及那些作业在后台运行。
作业控制要求下面3点形式支持:
1)支持作业的shell;
2)内核中的终端驱动程序必须支持作业控制;
3)内核必须提供对某些作业控制信号的支持。
9.9 shell执行程序
首先使用不支持作业控制的、在Solaris上运行的经典Bourne shell。如果执行:
ps -o pid,ppid,pgid,sid,comm
则其输出:
PID PPID PGID SID COMMAND
949 947 949 949 sh
1774 949 949 949 ps
//ps的父进程是shell。shell和ps命令两者位于同一会话和前台进程组。
如果在后台执行命令:
ps -o pid,ppid,pgid,sid,comm &
则唯一改变的值是命令的进程ID:
PID PPID PGID SID COMMAND
949 947 949 949 sh
1812 949 949 949 ps
//因为这种shell不知道作业控制,所以没有将后台作业放入自己的进程组,也没有从后台作业处取走控制终端。
ps -o pid,ppid,pgid,sid,comm | cat1
其输出是:
PID PPID PGID SID COMMAND
949 947 949 949 sh
1823 949 949 949 cat1
1824 1823 949 949 ps
注:管道中的最后一个进程是shell的子进程,而执行管道中其他命令的进程则是该最后进程的子进程.
如果在后台执行此管道:
ps -o pid,ppid,pgid,sid,comm | cat1 &
则只改变进程ID。
现在让我们用一个运行在Linux上的作业控制shell来检验同一个例子(使用Bourne-again shell):
ps -o pid,ppid,pgid,sid,tpgid,comm
其输出为:
PID PPID PGID SID TPGID COMMAND
2837 2818 2837 2837 5796 bash
5796 2837 5796 2837 5796 ps
可看到与Bourne shell例子的区别:Bourne-again shell将前台作业(ps)放入了它自己的进程组(5796)。ps命令是进程组组长进程,也是该进程组的唯一进程。
在后台执行此进程:
ps -o pid,ppid,pgid,sid,tpgid,comm &
其输出为:
PID PPID PGID SID TPGID COMMAND
2837 2818 2837 2837 2837 bash
5797 2837 5797 2837 2837 ps
再一次,ps命令被放入它自己的进程组,但是此时进程组(5797)不再是前台进程组,而是一个后台进程组。TPGID 2837指示前台进程组是登陆shell。
按谢列方式在一个管道中执行两个进程:
ps -o pid,ppid,pgid,sid,tpgid,comm | cat1
其输出为:
PID PPID PGID SID TPGID COMMAND
2837 2818 2837 2837 5799 bash
5799 2837 5799 2837 5799 ps
5800 2837 5799 2837 5799 cat1
两个进程ps和cat1都在一个新进程组(5799)中,这是一个前台进程组。Bourne-again shell是两个进程的父进程。
在后台执行此管道:
ps -o pid,ppid,pgid,sid,tpgid,comm | cat1 &
结果类似:
PID PPID PGID SID TPGID COMMAND
2837 2818 2837 2837 2837 bash
5801 2837 5801 2837 2837 ps
5802 2837 5801 2837 2837 cat1
//ps和cat1都处于同一后台进程组。
注意,使用的shell不同,创建各个进程的顺序也可能不同。
9.10 孤儿进程组
一个其父进程已终止的进程称为孤儿进程,这种进程由init进程“收养”。
一个进程组不是孤儿进程组的条件是,该进程组中有一个进程,其父进程属于同一会话的另一个组中。如果进程组不是孤儿进程组,那么在属于同一会话的另一个组中的父进程就有机会重新启动该组中停止的进程。在这里,进程组中每一个进程的父进程都属于另一个会话。所以此进程组是孤儿进程组。
9.11 FreeBSD实现
前面说明了进程、进程组、会话和控制终端的各种属性,值得观察一下所有这些是如何实现的。
从session结构(对话期结构)说明图中标出的各个字段。
- s_count 对话期中的进程组数,当为0时,则可释放此结构
- s_leader 对话期的首进程proc结构指针。
- s_ttyvp 指向控制终端vnode结构的指针
- s_ttyp 指向控制终端tty结构的指针
接着说明tty 结构(终端设备结构),每个终端和伪终端设备均在内核中分配这样一种结构。
- t_sesion 指向以此终端作为控制终端的session结构
- t_pgrp 前台进程组的pgrp结构
- t_termios 包含此终端的有关信息(波特率、回送打开或关闭等,见11章)
- t_winsize 此终端的窗口大小的winsize结构
再是pgrp结构(进程组结构),它包含一个特定进程组的信息。
- pg_id 进程组ID
- pg_seeeion 指向此进程组所属的session结构
- pg_mem 指向进程组的第一个进程proc结构的指针
再是proc结构,它包含一个进程的所有信息。
- p_pgrpnxt 指向下一个进程的指针
- p_pid 进程ID
- p_pptr 指向父进程的proc结构的指针
- p_pgrp 指向所属的进程组的pgrp结构的指针
最后还有一个vnode结构。在打开控制终端设备分配此结构。进程对/dev/tty
的所有访问都通过vnode结构。
内核从session结构开始,然后用s_ttyp
得到控制终端的tty结构。然后用 t_pgrp
得到前台进程组的pgrp结构