《UNIX环境高级编程》第9章 进程关系

作者:神秘网友 发布时间:2020-09-09 10:33:43

《UNIX环境高级编程》第9章 进程关系

《UNIX环境高级编程》第9章 进程关系

9.1 前言

上一章我们了解到进程之间具有关系。首先每个进程都有一个父进程(初始的内核级进程通常是自己的父进程)。父进程能够得到通知并能取得子进程的退出状态。也提到了如何等待进程组中任意一个进程终止。
本章将详细说明进程组以及会话的概念。还将介绍登录shell(登录时所调用的shell)和所有从登录shell启动的进程之间的关系。

9.2 终端登录

先说明当我们登录到UNIX系统时所执行的各个程序。
在早期的UNIX系统,用户用哑终端(用硬连接到主机)进行登录,因为连接到主机上的终端设备数是固定的,所以同时登录数也就有了已知的上限。
随着位映射图像终端的出现,开发出了窗口系统,它向用户提供了与主机系统进行交互的新方式。创建终端窗口的应用也被开发出来,它仿真了基于字符的终端,使得用户可用用熟悉的方式(即shell命令)与主机进行交互。
我们现在描述的过程用于经由终端登录至UNIX系统。该过程几乎与所使用的终端类型无关,所使用的终端可以是基于字符的终端仿真基于字符的终端,或者运行窗口系统的图形终端


这里说明两种平台的终端登录:
1. BSD终端登录
BSD的终端登录过程比较经典,linux也是其后继者。
1.1 系统管理者创建通常名为/etc/ttys的文件,其中每个终端设备都有一行,每行说明设备名传到getty程序的参数。例如其中一个参数说明了波特率等等。
1.2 当系统自举时,内核创建进程ID为1的进程,也就是init进程。
1.3 init进程使系统进入多用户模式。
1.4 init读取/etc/ttys,对每一个允许登录的终端设备,init调用一次fork,它所生成的子进程则exec getty(get teletypewriter) 程序。init以空环境exec getty程序。
1.5 getty对终端设备调用open函数,以读、写方式打开终端,此时会得到该终端的文件描述符。一旦该设备被打开,文件描述符0、1、2就被通过dup2函数关联到一起,从而共享终端设备的文件表项。然后getty输出“login :”之类的信息,并等待用户键入用户名。
1.6 当用户键入了用户名后,getty的工作就完成了。然后它以类似于以下方式调用login程序:
c
execle("/bin/login","login","-p",username,(char *)0,envp);

其中envp环境变量是根据gettytab文件中的环境字符串生成的,“-p”参数通知login保留传递给它的环境,也可以将其它环境字符串添加到该环境中,但是不要替换它。
1.7 login能处理多项工作。因为它得到了用户名,所以能调用getpwnam取得相应用户的口令文件登陆项。然后login调用getpass以显示“Password:”,接着读入用户键入的口令,它调用crypt将口令加密,并与该用户在阴影口令文件中登录项的pw_passwd字段相比较。
1.8 如果用户几次键入的口令都无效,则login以参数1调用exit表示登陆失败。父进程(init)了解到子进程的终止情况后,将再次调用fork,然后又执行了getty,对此终端执行上述过程。
《UNIX环境高级编程》第9章 进程关系


如果用户正确登录,login就将完成如下工作:
- 将当前工作目录更改为该用户的起始目录(chdir)。
- 调用chown更改该终端的所有权,使登录用户成为它的所有者。
- 将对该终端设备的访问权限改变成“用户读和写”。
- 调用setgid及initgroups设置进程的组ID。
- 用login得到的所有信息初始化环境:起始目录、shell、用户名、以及一个系统默认路径(PATH).
- login进程更改为登录用户的ID(setuid)并调用登录用户的shell,类似于:execl("/bin/sh","-sh",(char *)0);
至此,用户登录的登录shell得以开始运行。其父进程是init进程,所以此shell终止时,init会得到通知(接收到SIGCHLD信号),它会对该终端重复全部上述过程。登录shell的文件描述符0、1和2设置为终端设备。
1.9 现在,登录shell读取其启动文件(.profile),这些启动文件通常更改某些环节变量设置他们自己的PATH,当执行完启动文件后,用户最后得到shell提示符,并能键入命令。


  1. Linux终端登录
    linux的终端登录过程非常类似于BSD,它们的主要区别在于说明终端配置的方式。
    Ubuntu使用的init程序叫作“Upstart”,并使用存放在/etc/init目录的*.conf命名的配置文件。例如:运行/dev/tty1上的getty需要的说明可能放在/etc/init/tty1.conf文件中。

9.3 网络登录

通过串行终端登录至系统和经由网络登录至系统两者之间的主要区别是:网络登录时,在终端和计算机之间的连接不再是点到点的。在网络登录情况下,login仅仅是一种可用的服务,这与其他网络服务(如FTP和SMTP)性质相同。
在上述的终端登录中,init知道哪些终端设备可用用来登录,并为每个设备生成一个getty进程。但是,对网络登录情况有所不同,因为事先并不知道有多少个这样的登录。因此必须等待一个网络连接请求的到达,而不是使一个进程等待每一个可能的登录。
为使同一个软件既能处理终端登录,又能处理网络登录,系统使用了一种称为伪终端的软件驱动程序,它仿真串行终端的运行行为,并将终端操作映射为网络操作,反之亦然。


1.BSD网络登录
在BSD中,有一个inetd进程(有时称为英特网超级服务器),它等待大多数网络连接。
作为系统启动的一部分,init调用一个shell,使其执行shell脚本/etc/rc。由此shell脚本启动一个守护进程inetd。一旦此shell脚本终止,inetd的父进程就变成init。
inetd等待TCP/IP连接请求到达主机,而当一个连接请求到达时,它执行一次fork,然后子进程exec适当的程序。


以telnet为例:
- 主机A启动telnet客户端进程,通过 telnet hostname登录远端名为hostname的主机B。
- 主机B的inetd进程收到来自主机A的请求。
- 主机B的inetd进程fork一个子进程并exec主机B上的TELNET进程。
- 然后Telnet进程打开一个伪终端,并用fork分成两个进程。父进程通过网络连接的通行,子进程执行login程序。
需要理解的是:当通过终端或网络登录时,我们得到一个登录shell,其标准输入、标准输出、标准错误连接到一个终端设备或一个伪终端上。后面将会了解到这一登录shell是一个会话的开始,而此终端或伪终端则是会话的控制终端。


2.linux网络登录
除了有些版本使用扩展的因特网服务进程xinetd代替inetd进程外,Linux网络登录的其他方面与BSD网络登录相同。xinetd进程对它所启动的各种服务的控制比inetd提供的控制更加精细。

9.4 进程组

每个进程除了有一个进程ID外,还属于一个进程组。
进程组是一个或多个进程的集合。通常,他们是在同一作业中结合起来的,同一进程组中的各进程接收来自同一终端的各种信号。每一个进程组有一个唯一的进程组ID。
进程组ID类似于进程ID,它是一个正整数,并可存放在pid_t数据类型中。

#include <unistd.h>
pid_t getpgrp(void);       //返回调用进程的进程组ID.

pid_t getpgid(pid_t pid);  //返回进程号为pid的进程组ID,若pid=0,则等价于getpgrp

每个进程组有一个组长进程。组长进程的进程ID等于该进程组的进程组ID。
只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到最后一个进程离开为止的时间区间称为进程组的生命周期。某个进程组中的最后一个进程可以终止,也可以转移到另一个新的进程组。
进程调用setpgid可以创建一个进程组也可以键入到一个现有的进程组。

#include <unistd.h>
int setpgid(pid_t pid,pid_t pgid);  
  • setpgid函数将pid进程的进程组ID设置为pgid,如果这两个参数相等,则由pid 指定的进程变成进程组组长;
  • 如果pdi=0,则使用调用者的进程ID;
  • 如果pgid=0,则由pid指定的进程ID用作进程组ID;
  • 一个进程只能为自己或它的子进程设置进程组ID。在它的子进程调用了exec后,他就不能更改该子进程的ID了。

在大多数作业控制shell中,在fork之后调用此函数,使父进程设置其子进程进程组ID,并且也使子进程设置其自己的进程组ID,这两个调用中有一个是冗余的,但让父子进程都这样做可以保证,在父进程和子进程认为子进程已经进入了该进程组之前,这确实已经发生了。如果不这样做,在fork之后,由于父进程和子进程运行的先后顺序不确定,会因为子进程的组员身份取决于哪个进程首先执行而产生竞争条件。

9.5 会话

会话(session)是一个或多个进程组的集合。其结构可以如下,在一个会话中有3个进程组:
《UNIX环境高级编程》第9章 进程关系
通常是由shell的管道将几个进程编成一组的。


进程调用setsid函数建立一个新会话

#include <unistd.h>
pid_t setsid(void);  

如果调用此函数的进程不是一个进程组组长,则此函数创建一个新会话。具体会发生以下3件事:
- 该进程会变成新会话的首进程(session leader,会话首进程是创建该会话的进程)。此时,该进程是新会话中的唯一进程。
- 该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID。
- 该进程没有控制终端,如果之前有一个控制终端,那么这种联系也被切断。
如果该调用进程已经是一个进程组的组长,则此函数返回出错。 为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,这就保证了子进程不是一个进程组的组长。


getsid函数返回会话首进程的进程组ID:

#include <unistd.h>
pid_t getsid(pid_t pid);  

如果pid是0,getsid返回调用进程的会话首进程的进程组ID。

9.6 控制终端

会话和进程组还有一些其他的特性:
- 一个会话可以有一个控制终端(controlling terminal)。这通常是终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。
- 建立与控制终端连接的会话首进程被称为控制进程(controlling process)
一个会话中的几个进程组可以被分成一个前台进程组(foreground process group)以及一个或多个后台进程组(background process group)
- 如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。
- 无论何时键入终端的退出键(常常是ctrl+\),都会将退出信号发送至前台进程组的所以进程。
- 如果终端接口检测到调制解调器或网络已经断开,则将挂断信号发送至控制进程(会话首进程)

《UNIX环境高级编程》第9章 进程关系

9.7 函数tcgetpgrp、tcsetpgrp和tcgetsid

http://www.wowotech.net/process_management/process-tty-basic.html

需要一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能知道将终端输入和终端产生的信号发送到何处

#include <unistd.h>
pid_t tcgetpgrp(int fd);    //返回终端为fd的前台进程组ID

int tcsetpgrp(int fd ,pid_t pgrpid);   //将前台进程组ID设置为pgrpid,终端为fd。

函数tcgetpgrp返回前台进程组ID,它与在fd上打开的终端相关联。
如果进程有一个控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设置为pgrpid。pgrpid值应当是在同一个会话中的一个进程组的ID。fd必须引用该会话的控制终端。


#include <termios.h>
pid_t tcgetsid(int fd);

给出控制tty的文件描述符,通过tcgetsid函数,可以获得会话首进程的进程组ID。

9.8 作业(进程组)控制

作业控制可以允许在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业在后台运行。
作业控制需要以下3中形式的支持:
- 支持作业控制的shell。
- 内核中的终端驱动程序必须支持作业控制。
- 内核必须提供对某些作业控制信号的支持。


我们可以键入一个影响前台作业的特殊字符-挂起键(通常是Ctrl+Z),与终端驱动程序进行交互。键入此字符是终端驱动程序将信号SIGTSTP发送至前台进程组中所有进程,后台进程组则不受影响。实际上有3个特殊字符可以使终端驱动程序产生信号,并将它们发送至前台进程组,它们是:
- 中断字符(一般是Delete或Ctrl+C)产生STGINT;
- 退出字符(一般是Ctrl+\)产生STGQUIT;
- 挂起字符(一般是Ctrl+Z)产生SIGTSTP;


终端驱动程序必须处理与作业控制有关的另一种情况。我们可以有一个前台作业,若干个后台作业,这些作业中哪一个接收我们在终端上键入的字符呢?只有前台作业接收终端输入如果后台作业试图读终端,这并不是一个错误,但终端驱动程序将检测这种情况,并向后台作业发送一个特定信号SIGTTIN。该信号通常会停止此后台作业,而shell则向有关用户发出这种情况通知,然后用户就可以使用shell命令将此作业转为前台作业运行,于是它就可以读终端。

这里有几个shell命令用于作业控制:
- jobs :返回当前shell运行的作业程序。
- fg %num:将编号为num的后台程序调入前台。
- bg :将程序调入后台运行。

9.9 shell执行程序

看的稀里糊涂,星期一就这么不在状态。。。
跳过。

9.10 孤儿进程组

前面看到父进程终止的子进程会成为孤儿进程(orphan process),这种进程由init进程收养。现在我们要说明整个进程组也可能变成“孤儿”,以及POSIX.1如何处理它。
孤儿进程组(orphaned process group)的定义为:该组中每个成员的父进程是该组的一个成员,或者其父进程不是该组所属会话的成员。 另一种对孤儿进程组的描述是:一个进程组不是孤儿进程组的条件是-该组中有一个进程,其父进程在属于同一个会话的另一个进程组中。


9.11 FreeBSD实现

前面说明了进程、进程组、会话和控制终端的各种属性,下面简要说明一下FreeBSD中的实现。
从session结构开始说明,每个会话都会分配一个session结构(例如每次调用setsid时)。
- s_count 是该会话中的进程组数。当此计数器减至0时,可释放次结构。
- s_leader 是指向会话首进程(创建该会话的进程)proc结构的指针
- s_ttyvp 是指向控制终端vnode结构的指针
- s_ttyp 是指向终端tty结构的指针。
- s_sid 时会话ID。
在调用setsid时,在内核中分配一个新的session结构。s_count 设置为1,s_leader设置为调用进程proc结构的指针,s_sid 设置为进程ID,因为新会话没有控制终端,所以 s_ttyp和s_sid 设置为空指针。


接着说明tty结构。每个终端设备和每个伪终端设备均在内核中分配这样一个结构:
- t_session 指向将此终端作为控制终端的session结构。(tty结构指向session结构,session结构也指向tty结构)
- 《UNIX环境高级编程》第9章 进程关系
- t_pgrp 指向前台进程组的pgrp结构。
- t_termios 包含所以这些特殊字符和该终端有关信息。
- t_winsize 包含终端窗口当前大小的winsize结构。当终端窗口改变时,信号SIGWINCH被发送至前台进程组。
- pg_id 时进程组ID。
- pg_session 时指向此进程组所属会话的session结构。
- pg_members 是指向此进程组proc结构表的指针,该proc结构代表进程组的成员。
- p_pid 包含进程ID。
- p_pptr 是指向父进程proc结构的指针。
- p_pgrp 指向本进程所属的进程组的pgrp结构的指针。
- p_pglist 是一个结构,其中包含两个指针,分别指向进程组中上一个和下一个进程。
最后还有一个vnode结构。如前所述,在打开控制终端设备时分配此结构。进程对/Dev/tty的所有访问都通过vnode结构。

9.12 小结

  • 本章说明了进程组之间的关系-会话,它由若干个进程组成。
  • 本章说明了作业控制是如何由支持作业控制的shell实现的。
  • 说明了进程的控制终端/dev/tty。

《UNIX环境高级编程》第9章 进程关系相关教程

  1. 进程之间的通信之AIDL
  2. [docker]将容器的进程映射到主机-nginx
  3. 进程间通信-管道通信
  4. Linux--僵尸进程与孤儿进程总结
  5. 进程与线程
  6. 【进程通信】之管道通信
  7. 信号同步
  8. 进程、线程、协程、异步、非堵塞IO,多路复用详解