《Unix环境高级编程》笔记 第九章-进程关系

本文详细介绍了Unix系统中终端登录、网络登录的过程,以及进程、进程组、会话、控制终端之间的关系。通过终端登录时,init进程启动getty程序,getty执行login验证用户,然后启动shell。网络登录涉及inetd进程处理网络请求,调用telnetd服务程序,通过伪终端实现。文章还讨论了会话、进程组的概念,以及如何为会话分配控制终端。同时解释了控制终端与进程组、会话之间的逻辑关系及其作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 终端登录

本节介绍经由终端登陆至UNIX系统所执行的各个程序。

1.1 终端是什么 Terminal

https://blog.youkuaiyun.com/yazhouren/article/details/78793367

终端就是处理计算机主机输入输出的一套设备,它用来显示主机运算的输出,并且接受主机要求的输入,典型的终端包括显示器键盘套件,打印机打字机套件等

1.2 终端类型

终端文件(/dev/…)都是字符设备文件 (char)

  • 本地终端
    用VGA连接主机和显示器,用PS/2或者USB连接主机和键盘,这样的一个显示器/键盘组合就是一个本地终端。
  • 用串口连接的远程终端
    通过串口线把主机接到另外一个有显示器和键盘的主机,通过运行一个终端模拟程序,比如***“Windows超级终端”***来将这台主机的显示器和键盘借给串口对端的主机。
  • 用TCP/IP承载的远程终端
    类似Telnet,SSH这般。

可见上述的三类中,前两类都是在本地就直接关联了物理设备的,比如VGA口啊,PS/2口啊,串口啊之类的,这种终端叫做物理终端而第三类在本地则没有关联任何物理设备,注意,不要把物理网卡当成终端关联的物理设备,它只是隧道关联的物理设备,这里的物理网卡完全可以换成卡车,它们与终端并不直接相关,所以这类不直接关联物理设备的终端叫做伪终端

  • 串行端口终端(Serial Port Terminal)

    在linux中的表现形式/dev/ttyS#

    是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。有段时间这些串行端口设备通常被称为终端设备,因为那时它的最大用途就是用来连接终端。这些串行端口所对应的设备名称是/dev/tts/0(或/dev/ttyS0), /dev/tts/1(或/dev/ttyS1)等,设备号分别是(4,0), (4,1)等,分别对应于DOS系统下的COM1、COM2等。若要向一个端口发送数据,可以在命令行上把标准输出重定向到这些特殊文件名上即可。例如,在命令行提示符下键入:echo test > /dev/ttyS1会把单词”test”发送到连接在ttyS1(COM2)端口的设备上。可接串口来实验

  • 伪终端

    https://www.cnblogs.com/zzdyyy/p/7538077.html

    在linux中的表现形式/dev/pts/#

    虚拟终端和串行终端的数目是有限的,然而,网络终端和图形终端窗口的数目确是不受限制的,这是通过伪终端实现的。即为远程连接的终端或图形界面下打开的终端接口

    伪终端是伪终端master和伪终端slave(终端设备文件)这一对字符设备/dev/ptmx是用于创建一对master、slave的文件。当一个进程打开它时,获得了一个master的文件描述符(file descriptor),同时在/dev/pts下创建了一个slave设备文件。

    master端是更接近用户显示器、键盘的一端,slave端是在虚拟终端上运行的CLI(Command Line Interface,命令行接口)程序。Linux的伪终端驱动程序,会把“master端(如键盘)写入的数据”转发给slave端供程序输入,把“程序写入slave端的数据”转发给master端供(显示器驱动等)读取。

在这里插入图片描述

我们打开的“终端”桌面程序,其实是一种终端模拟器(即为使用Ctrl+Alt+T打开的那个)。当终端模拟器运行时,它通过/dev/ptmx打开master端,创建了一个伪终端对,并让shell运行在slave端。当用户在终端模拟器中按下键盘按键时,它产生字节流并写入master中,shell便可从slave中读取输入;shell和它的子程序,将输出内容写入slave中,由终端模拟器负责将字符打印到窗口中。

利用伪终端完成远程登录:

每次用户通过客户端连接服务端的时候,服务端创建一个伪终端master、slave字符设备对,在slave端运行login程序,将master端的输入输出通过网络传送至客户端。至于客户端,则将从网络收到的信息直接关联到键盘/显示器上。我们将这个过程描述为下图:

在这里插入图片描述

  • 虚拟终端

    对应的文件是/dev/tty#

    虚拟终端是机器正常启动后自动启动的控制台。Linux除了有图形化界面外,还有纯命令行界面,并且默认情况下,你可以同时操作最多6个纯命令行界面,这些纯命令行界面被称作Virtual Terminal(虚拟终端)。

    当你登录Linux服务器的时候,默认只能使用虚拟终端。此外,即便是普通的桌面环境,当你需要重新配置图形界面,或者图形界面因为内部异常等原因崩溃了的时候,你还可以切换到虚拟终端继续执行操作。可以用Ctrl+Alt+F1到Ctrl+Alt+F6来切换(缺省只开6个虚拟终端)
    其设备控制文件分别为/dev/tty1、/dev/tty2、/dev/tty3、/dev/tty4、/dev/tty5和/dev/tty6
    还有/dev/tty0是对应当前的虚拟终端

  • 控制台:注意区分控制台和终端的区别

    https://blog.youkuaiyun.com/lwy19880425/article/details/52791247

    对应的文件是/dev/console

    **在主机的系统启动完成之前,终端是不能连接到主机上的。**为了能记录出主机开机过程的日志,也便于在主机出故障无法启动操作系统时进行检修维护,就多了一个叫做控制台的设备。**一台主机有且只能有一个控制台。**文件主机的重要日志,比如开机关机的日志和记录,重要应用程序的日志,都会输出到控制台来。

    console 相当于电视机机体上的控制面版,一般只有一个;terminal 相当于遥控器,可以有很多个

    计算机启动的时候,所有的信息都会显示到控制台上,而不会显示到终端上。也就是说,控制台是计算机的基本设备,而终端是附加设备。 当然,由于控制台也有终端一样的功能,控制台有时候也被模糊的统称为终端。 计算机操作系统中,与终端不相关的信息,比如内核消息,后台服务消息,都可以显示到控制台上,但不会显示到终端上。

    简单的说,能直接显示系统消息的那个终端称为控制台,其他的则称为终端。但是在linux系统中,这个概念也已经模糊化了

    比如下面这条命令:

    echo "hello,world" > /dev/console 
    

    这条命令的目的是将"hello,world"显示到控制台上。/dev/console是控制台设备的设备名。在linux中,在字符模式下,你无论在哪个虚拟终端下执行这条命令,字符hello,world都会显示在当前的虚拟终端下。也就是说,linux把当前的虚拟终端当作控制台来看待。可见,linux中已经完全淡化了控制台和终端的区别。但是在其他的UNIX类系统中,却很明显的有虚拟终端和控制台的区别。比如freeBSD系统。

    在freebsd中,只有第一个“终端”才是真正的控制台。(就是说按alt+f1得到的那个虚拟终端) ,你无论在哪个虚拟终端上执行上面的那条命令(哪怕是通过网络连接的伪终端上执行这条命令)。hello,world字符总会显示到第一个“终端”也就是真正的控制台上。另外,其他的一些系统内部信息,比如哪个用户在哪个终端登陆,系统有何严重错误警告等信息,全都显示在这个真正的控制台上。在这里,就明显的区分了终端和控制台的概念。

    再简单的说,控制台是直接和计算机相连接的原生设备,终端是通过电缆、网络等等和主机连接的设备

  • 控制终端:

    对应的文件是/dev/tty

    这是一个逻辑概念,即用户正在控制的终端,可以为串行终端,虚拟终端和伪终端。

  • 其它类型

    Linux系统中还针对很多不同的字符设备存在有很多其它种类的终端设备特殊文件。例如针对ISDN设备的/dev/ttyIn终端设备等

1.3 通过终端登录Unix系统的过程

在这里插入图片描述

  1. 当系统启动时,内核创建进程ID为1的进程也就是init进程,init进程使系统进入多用户状态。init进程根据配置文件/etc/inittab确定需要打开哪些终端(这个配置文件中的每一项记录了终端设备名和要传到getty程序的参数等,如该终端设备的波特率),对每一个允许登录的终端设备,init调用一次fork,它所生成的子进程则执行getty(exec)程序。(不同操作系统配置文件可能不同)

  2. getty通过open函数以读写方式打开该终端设备(把文件描述符0、1、2都指向该控制终端)。然后getty输出longin:之类的信息,并等待用户键入用户名。

  3. 当用户键入用户名后,getty工作完成。然后以类似以下形式调用login程序

    execle(/bin/login”,”login”,-p”,username,(char *)0,envp)
    

    init以一个空环境调用getty,getty则为login创建一个环境(envp参数,意为环境表)。-p参数意为通知login程序保留传递给它的环境(可以添加该环境表中的环境变量,但是不能替换它)。

  4. login处理多项工作,由于它的命令行参数有用户名,因此它能调用getpwnam取得相应用户的口令文件登录项,然后显式提示Password:,且在输入密码期间关闭终端的回显,因此我们在linux下输入密码时是看不到的,然后验证帐号密码的正确性。

    login程序读取用户键入的口令,将该口令加密并与阴影文件中对应项相比较。密码验证无误后,login程序做如下工作(但是如果用户几次键入口令无效,则login调用exit(1)。父进程了解到子进程的终止情况后,将再次调用fork,然后执行getty,然后重复此功能。):

    • login将切换目录到用户的home目录

    • 调用chown更改该终端的所有权,使登录用户成为它的所有者

    • 对该终端设备的访问权限改为“用户读和写”

    • 调用setgid和initgroups设置进程的组ID

    • 用login得到的所有信息初始化环境:起始目录HOME、SHELL、用户名USER和LOGNAME以及系统默认路径PATH

    • login更改进程权限(由于是init进程fork的子进程,因此之前一直是超级用户权限),通过setuid将进程的用户ID更改为登录用户的用户ID(由于setuid是由超级用户调用的,因此它更改所有3个用户ID:实际用户ID、有效用户ID、保存的用户ID)

    • 调用该用户的shell

      execl(/bin/sh”,-sh”,(char *)0)
      

      注意argv[0]参数的程序名前面加了⼀个**‘-’**,这样bash就知道自己是作为登录Shell启动的。然后执行登录Shell的启动脚本。

  5. 登录shell读取其启动文件如.bash_profile、.bash_login、.profile。这些启动文件通常改变一些环境变量并增加很多环境变量。当执行完启动文件后,用户得到shell提示符(如$),就可以键入命令了。

从getty开始exec到login,再exec到bash,其实都是同一个进程,因此打开的控制终端设备(字符设备文件)没变,文件描述符0、1、2也仍然指向控该制终端。由于fork会复制PCB信息,所以由Shell启动的其它进程也都是如此。

在这里插入图片描述

2. 通过网络登录Unix系统(以TELNET为例)

对于终端登录,init进程知道哪些终端设备可以登录,并为每个设备生成一个getty进程。但是网络登录不再是点对点登录,事先不知道有多少登录,必须等待网络请求到达,而不是使一个进程等待每一个可能的登录

inetd进程

因特网超级服务器,它等待大多数网络连接。

系统启动时,init进程调用一个shell,使其执行shell脚本/etc/rc。该shell脚本启动一个守护进程inetd。inetd进程等待TCP/IP连接请求到达主机,每当一个连接请求到达时,它执行一次fork使生成的子进程exec适当的程序(即让子进程处理该TCP/IP连接)

假定另一台主机通过telnet hostname的形式远程登录该主机,即该客户进程打开一个到hostname主机的TCP连接。在hostname主机上启动的程序称为TELNET服务进程(即inetd守护进程fork后exec的程序就是TELNET服务程序,称为telnetd),客户进程和服务进程使用TELNET应用协议通过TCP连接交换数据。

在这里插入图片描述

然后telnetd服务进程打开一个伪终端设备,并用fork分成两个进程。父进程处理通过网络连接的通信,子进程则执行login程序(子进程exec login之前将文件描述符0,1,2与伪终端相连)。如果登录正确,就执行终端登录的同样步骤。然后login调用exec执行登录用户的登录shell

在这里插入图片描述

2.5 补充:procfs文件系统

在许多类 Unix 计算机系统中, procfs 是进程文件系统 (file system) 的缩写,包含一个伪文件系统(启动时动态生成的文件系统)用于用户通过内核访问进程信息。事实上,相当多Linux命令都是调用此目录中的文件来显示系统相关信息。

这个文件系统通常被挂载到 /proc 目录。由于 /proc 不是一个真正的文件系统,它也就不占用存储空间,只是占用有限的内存

procfs 文件系统会在 /proc 下为每个进程创建一个目录,名字是该进程的 pid。目录下有很多文件, 用于记录进程的运行情况和统计信息等。因为进程有创建,也有终止,所以 /proc/ 下记录进程信息的目录(以及目录下的文件)也会发生变化。当然该目录下的这些文件都是只读的,我们并不能更改这些文件,只能用于获取进程的运行信息

proc目录下每个pid目录包含:

  • /proc/PID/cmdline, 启动该进程的命令行.
  • /proc/PID/cwd, 当前工作目录的符号链接.
  • /proc/PID/environ影响进程的环境变量的名字和值.
  • /proc/PID/exe, 最初的可执行文件的符号链接, 如果它还存在的话。
  • /proc/PID/fd, 一个目录,包含每个打开的文件描述符的符号链接.
  • /proc/PID/fdinfo, 一个目录,包含每个打开的文件描述符的位置和标记
  • /proc/PID/maps, 一个文本文件包含内存映射文件与块的信息。
  • /proc/PID/mem, 一个二进制图像(image)表示进程的虚拟内存, 只能通过ptrace化进程访问.
  • /proc/PID/root, 该进程所能看到的根路径的符号链接。如果没有chroot监狱,那么进程的根路径是/.
  • /proc/PID/status包含了进程的基本信息,包括运行状态、内存使用。
  • /proc/PID/task, 一个目录包含了硬链接到该进程启动的任何任务

比如有一个进程id是32332的进程,那么可在/proc目录下:

$ cd /proc/32332
$ ls
attr       clear_refs       cpuset   fd       limits     mem         net        oom_score      personality  schedstat  smaps_rollup  status   timerslack_ns
autogroup  cmdline          cwd      fdinfo   loginuid   mountinfo   ns         oom_score_adj  projid_map   sessionid  stack         syscall  uid_map
auxv       comm             environ  gid_map  map_files  mounts      numa_maps  pagemap        root         setgroups  stat          task     wchan
cgroup     coredump_filter  exe      io       maps       mountstats  oom_adj    patch_state    sched        smaps      statm         timers
$ cat cmdline # 打印出启动该进程的命令行为./a.out
./a.out
$ cat status # 打印出该进程的基本信息(这里只显示了部分)
Name:	a.out
Umask:	0022
State:	S (sleeping)
Tgid:	32381
Ngid:	0
Pid:	32381
PPid:	31825
TracerPid:	0
Uid:	0	0	0	0
Gid:	0	0	0	0

除此之外,/proc目录下还有一些其他文件,通过这些文件我们可以得到计算机系统的一些基本信息,如通过cpuinfo可以获取到CPU 的信息(型号, 家族, 缓存大小等),meminfo获取物理内存、交换空间等的信息,version获取到内核版本等等。

$ cat version # 获取内核版本
Linux version 4.15.0-136-generic (buildd@lcy01-amd64-029) (gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)) #140-Ubuntu SMP Thu Jan 28 05:20:47 UTC 2021

这里还有一个特殊目录sys,这个目录里的文件大部分都是可写的,可以通过改写这些文件达到修改内核参数的目的,系统命令sysctl就是通过这个目录实现所有功能的

3. 进程组

每个进程除了有一个进程ID即pid之外,还属于一个进程组。

进程组是一个或多个进程的集合。进程组中的进程接收来自同一终端的各种信号,通常它们与一组作业相关联

每个进程组有一个唯一的进程组ID,即一个正整数。函数getpgrp返回调用进程的进程组ID

pid_t getpgrp(void);

或者使用getpgid获取指定进程的进程组ID

pid_t getpgid(pid_t pid);

若pid为0,则返回调用进程的进程组ID,即getpgid(0)等价于getpgrp()

每个进程组都有一个组长进程,进程组ID等于组长进程PID

只要进程组中有一个进程存在,则该进程组就存在,这与组长进程是否终止无关。从进程组创建到最后一个进程离开(或终止)为止的时间区间称为进程组生命期。

setpgid函数

用于创建或加入一个进程组。setpgid将pid进程的进程组ID设为pgid。

int setpgid(pid_t pid, pid_t pgid);
  • 如果pid==pgid,则pid进程成为进程组长。

  • 如果pid==0,则设置的是调用者进程的进程组ID

  • 如果pgid==0,则由pid指定的进程ID用作进程组ID

一个进程只能为它自己或它的子进程设置进程组ID。而且子进程exec新的程序之后,父进程就不能再更改该子进程的进程组ID

4. 会话 session

会话是一个或多个进程组的集合

在这里插入图片描述

如上图中的会话有3个进程组。

通常由shell管道将几个进程编成一组。上面的安排可能由下列形式的shell命令形成:

proc1 | proc2 & # 这是后台进程组
proc3 | proc4 | proc5 # 这是前台进程组

通过setsid函数建立新会话

pid_t setsid(void); // 成功返回进程组ID

如果调用进程不是进程组长,则此函数创建一个新会话:

  • 该进程成为新会话的会话首进程(会话首进程是创建该会话的进程)。此时新会话中只有这一个进程
  • 该进程成为新的进程组的组长进程(即该进程组ID是调用进程的进程ID)
  • 该进程没有控制终端。即使在setsid之前该进程有一个控制终端,现在这种联系也被切断
  • 如果调用进程是一个进程组长,则函数出错。这个限制的合理在于如果允许进程组组长迁移到新的会话,而进程组的其他成员仍然在老的会话中,那么,就会出现同一个进程组的进程分属不同的会话之中的情况,这就破坏了进程组和会话的严格的层次关系。

注意,并没有会话ID这一概念:

通常将会话首进程pid视为会话ID(会话首进程总是一个进程组长)。函数getsid函数返回会话首进程的进程组ID

pid_t getsid(pid_t pid);

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

如果pid不属于调用者所在会话,那么函数出错。

5. 控制终端

对应的文件是/dev/tty

这是一个逻辑概念,即用户正在控制的终端,可以为串行终端,虚拟终端和伪终端。

  • 一个会话可以有一个控制终端,通常是终端设备(终端登录)或伪终端设备(网络登录)
  • 建立与控制终端连接的会话首进程称为控制进程
  • 一个会话中的几个进程组可以分为一个前台进程组以及一个或多个后台进程组
  • 如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组
  • 无论何时键入终端的中断键(Delete或Ctrl+C),会将中断信号发送至前台进程组的所有进程
  • 无论何时键入终端的退出键(Ctrl+\),会将退出信号发送至前台进程组的所有进程
  • 如果终端接口检测到网络已断开连接,则将挂断信号发送至控制进程(会话首进程)

用户登录系统为例,可能存在如下图所示的情况:

在这里插入图片描述

通常我们不必担心控制终端,登录时,将自动建立控制终端(如通过终端登录Unix时,getty通过open函数以读写方式打开该终端设备,把文件描述符0、1、2都指向该控制终端)。

如何为会话分配一个控制终端:
  • 当会话首进程打开第一个尚未与一个会话相关联的终端设备时,只要在调用open时没有O_NOCTTY,将会将此终端作为控制终端分配给该会话
  • 会话首进程以TIOCSCTTY作为request参数调用ioctl时,会为该会话分配控制终端。为了使此函数成功执行,此会话不能已经有一个控制终端(因此此操作通常跟在setsid调用之后,setsid保证此进程是一个没有控制终端的会话首进程)

程序能与控制终端对话的方法是open文件/dev/tty,如果程序没有控制终端,则打开此设备将失败。

6. tcgetpgrp、tcsetpgrp和tcgetsid函数

需要知道哪一个进程组是前台进程组,这样终端设备驱动程序就能知道将终端输入和终端产生的信号发往何处

pid_t tcgetpgrp(int fd); // 成功返回前台进程组ID
int tcsetpgrp(int fd, pid_t pgrp); 
  • tcgetpgrp函数返回前台进程组ID,fd引用该会话的控制终端

  • tcsetpgrp函数将前台进程组ID设为pgrp。pgrp应为同一会话中的一个进程组ID,fd引用该会话的控制终端

大多数应用程序不直接调用这两个函数,它们通常由作业控制shell调用

通过tcgetsid函数获取控制终端的会话首进程的进程组ID

pid_t tcgetsid(int fd);

7. 进程、进程组、会话之间的关系及作用

https://blog.youkuaiyun.com/21cnbao/article/details/104489577

http://www.ty2y.com/study/linuxsessionhh.html

进程组和会话在进程之间形成了两级的层次关系:

进程组是一组相关进程的集合,会话是一组相关进程组的集合

一个进程会有如下 ID:

  • PID:进程的唯一标识。如果一个进程含有多个线程,所有线程调用 getpid 函数会返回相同的值。

  • PGID:进程组 ID。每个进程都会有进程组 ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组 ID

  • SID:会话 ID。每个进程也都有会话 ID。默认情况下,新创建的进程会继承父进程的会话 ID

  • PPID:是程序的父进程号。

7.1 进程组

进程组和会话是为了支持 shell 作业控制而引入的概念

最常见的创建进程组的场景就是在 shell 中执行管道命令:如cmd1 | cmd2,其实现可能是shell复刻一个子进程,然后等待该子进程终止。这个子进程运行以下代码:

int pd[2];
pipe(pd);//创建管道
int pid = fork();//复刻一个子进程
if(pid){
    //父进程执行此部分
    close(pd[0]);//指定该进程为写入端
    close(1);//关闭fd1(stdout)
    dup(pd[1]);//I/O重定向,fd1与pd[1]代指同一个文件
    close(pd[1]);//关闭pd[1],此时只有fd1代指管道的写入端口
    exec(cmd1);//更改执行映像为cmd1
}
else{
    //子进程执行此部分
    close(pd[1]);//指定该进程为读取端
    close(0);//关闭fd0(stdin)
    dup(pd[0]);//I/O重定向,fd0与pd[0]代指同一个文件
    close(pd[0]);//关闭pd[0],此时只有fd0代指管道的读取端口
    exec(cmd2);//更改执行映像为cmd2
}

cmd1和cmd2进程通过管道协同完成一项工作,它们隶属于同一个进程组(子进程会继承父进程的进程组 ID),其中 cmd1进程是进程组的组长

引入了进程组的概念,可以更方便地管理这一组进程了。比如这项工作放弃了,不必向每个进程一一发送信号,可以直接将信号发送给进程组,进程组内的所有进程都会收到该信号。

7.2 会话

当有新的用户登录 Linux 时,登录进程会为这个用户创建一个会话

用户的登录 shell 就是会话的首进程(也是建立与控制终端连接的控制进程)。会话的首进程 ID 会作为整个会话的 ID。会话是一个或多个进程组的集合,包括了登录用户的所有活动

在终端中只能有一个会话。当我们打开一个新的终端时,总会创建一个新的会话。

就进程间的关系来说,session 由一个或多个进程组组成。一般情况下,来自单个登录的所有进程都属于同一个 session。

前台进程和后台进程

用户在 shell 中可以同时执行多个命令。对于耗时很久的命令(如编译大型工程),用户不必在傻傻的等待命令运行完毕才执行下一个命令。

用户在执行命令时,可以在命令的结尾添加“&”符号,表示将命令放入后台执行。这样该命令对应的进程组即为后台进程组

$ ./a.out &
[1] 32576 # 对于后台进程组,终端会显示 [作业id] 该后台进程组的第一个进程pid

在任意时刻,可能同时存在多个后台进程组,但是不管什么时候都只能有一个前台进程组。只有在前台进程组中进程才能在控制终端读取输入。当用户在终端输入信号生成终端字符(如 ctrl+c、ctrl+z、ctr+\等)时,对应的信号只会发送给前台进程组

Session 中的每个进程组被称为一个 job,有一个 job 会成为 session 的前台 job(foreground),其它的 job 则是后台 job(background)。每个 session 连接一个控制终端(control terminal),控制终端中的输入被发送给前台 job,从前台 job 产生的输出也被发送到控制终端上。同时由控制终端产生的信号,比如 ctrl + z 等都会传递给前台 job。

一般情况下 session 和终端是一对一的关系,当我们打开多个终端窗口时,实际上就创建了多个 session

Session 的意义在于多个工作(job)在一个终端中运行,其中的一个为前台 job,它直接接收该终端的输入并把结果输出到该终端。其它的 job 则在后台运行。

shell 中可以存在多个进程组,无论是前台进程组还是后台进程组,它们或多或少存在一定的联系,为了更好地控制这些进程组(或者称为作业),系统引入了会话的概念。

会话的意义在于将很多的工作集中在一个终端,选取其中一个作为前台来直接接收终端的输入及信号,其他的工作则放在后台执行

session 的诞生与消亡:

通常,新的 session 由系统登录程序创建,session 中的领头进程是运行用户登录 shell 的进程。新创建的每个进程都会属于一个进程组,当创建一个进程时,它和父进程在同一个进程组、session 中。

将进程放入不同 session 的惟一方法是使用 setsid 函数使其成为新 session 的领头进程。这还会将 session 领头进程放入一个新的进程组中。

当 session 中的所有进程都结束时 session 也就消亡了。实际使用中比如网络断开了,session 肯定是要消亡的。另外就是正常的消亡,比如让 session 的领头进程退出。一般情况下 session 的领头进程是 shell 进程,如果它处于前台,我们可以使用 exit 命令或者是 ctrl + d 让它退出。或者我们可以直接通过 kill 命令杀死 session 的领头进程。这里面的原理是:当系统检测到挂断(hangup)条件时,内核中的驱动会将 SIGHUP 信号发送到整个 session。通常情况下,这会杀死 session 中的所有进程。

session 与终端的关系
如果 session 关联的是伪终端,这个伪终端本身就是随着 session 的建立而创建的,session 结束,那么这个伪终端也会被销毁。
如果 session 关联的是 tty1-6,tty 则不会被销毁。因为该终端设备是在系统初始化的时候创建的,并不是依赖该会话建立的,所以当 session 退出,tty 仍然存在。只是 init 系统在 session 结束后,会重启 getty 来监听这个 tty。

7.3 控制终端

**控制终端是进程的一个属性。**通过 fork 系统调用创建的子进程会从父进程那里继承控制终端。这样,session 中的所有进程都从 session 领头进程那里继承控制终端。Session 的领头进程称为终端的控制进程(controlling process)。简单点说就是:**一个 session 只能与一个终端关联,这个终端被称为 session 的控制终端(controlling terminal)。**同时只能由 session 的领头进程来建立或者改变终端与 session 的联系。

支持 job control 的 shell 必须能够控制在某一时刻由哪个 job 使用终端。否则,可能会有多个 job 试图同时从终端读取数据,这会导致进程在接收用户输入时的混乱。

shell 一次只允许一个 job(进程组)访问控制终端。来自控制终端的某些输入会导致信号被发送到与控制终端关联的 job(进程组)中的所有进程。该 job 被称为控制终端上的前台 job。由 shell 管理的其他 job 在不访问终端的情况下,被称为后台 job

Shell 的职责是通知 job 何时停止何时启动,还要把 job 的信息通知给用户,并提供机制允许用户继续暂停的 job、在前台和后台之间切换 job。比如前台 job 可以无限制的自由使用控制终端,而后台 job 则不可以。当后台 job 中的进程试图从其控制终端读取数据时,通常会向进程组发送 SIGTTIN 信号。这通常会导致该组中的所有进程停止(变成 stopped 状态)。类似地,当后台 job 中的进程试图写入其控制终端时,默认行为是向进程组发送 SIGTTOU 信号,但是否允许写入的控制会更加的复杂。

8. 作业控制

它允许在一个终端上启动多个作业(进程组),控制哪一个作业可以访问该终端,以及哪些作业在后台运行
我们可以键入一个影响前台作业的特殊字符:挂起键(一般采用Ctrl+Z)与终端进行交互作用。键入此字符使终端驱动程序将信号 SIGTSTP 送至前台进程组中的所有进程,后台进程组作业则不受影响。实际上有三个特殊字符可使终端驱动程序产生信号,并将它们送至前台进程组,它们是:

  • 中断字符(一般采用DELETE或Ctrl+C)产生 SIGINT。
  • 退出字符(一般采用Ctrl+\)产生 SIGQUIT。
  • 挂起字符(一般采用Ctrl+Z)产生 SIGTSTP。

注意只有前台作业接受终端输入,因为每一个对话只有一个前台作业。当后台试图去读取时,终端驱动会使其STOPPED信号(SIGTTIN)停止这个后台作业,而shell则向用户提供相关信息。然后用户就可以用shell命令(fg)将此作业转为前台作业

在这里插入图片描述

shell 在后台起动 cat 进程,但是当 cat 试图读其标准输入(控制终端)时,终端驱动程序知道它是个后台作业,于是将 SIGTTIN 信号送至该后台作业。shell 检测到其子进程的状态改变(回忆对 wait 和 waitpid 的讨论),并通知我们该作业已被停止。然后,用 shell 的 fg 命令将此停止的作业送入前台运行。这样做使 shell 将此作业转为前台进程组( tcsetpgrp),并将继续信号( SIGCONT )送给该进程组。因为该作业现在前台进程组中,所以它可以读控制终端。
对于后台作业写入控制终端,我们可以允许或禁止后台作业输出到控制终端

在这里插入图片描述

在用户禁止后台作业向控制终端写时,该作业的cat命令试图写其标准输出,此时终端驱动程序识别出该写操作来源于后台进程,于是向该作业SIGTTOU信号,cat进程阻塞。与之前例子一样,用户使用shell的fg命令将该作业转为前台时,该作业继续完成

在这里插入图片描述

9. shell执行程序(以bash为例)

对于以下指令:

$ ps -o pid,ppid,pgid,sid,comm
  PID  PPID  PGID   SID COMMAND
  565   453   565   565 bash
  588   565   588   565 ps

可见ps的父进程是shell。ps和shell位于同一会话中(会话首进程即为shell进程),且ps位于前台进程组中

对于管道命令,执行以下命令:

$ ps -o pid,ppid,pgid,sid,comm | cat | cat
  PID  PPID  PGID   SID COMMAND
  565   453   565   565 bash
 1007   565  1007   565 ps
 1008   565  1007   565 cat
 1009   565  1007   565 cat

可见管道两边的进程都是shell的子进程,即shell对于管道中的每一条命令fork一个子进程。且上面的ps和两个cat都位于前台进程组

但如果是下面的情况:ps和两个cat都位于同一后台进程组。

$ ps -o pid,ppid,pgid,sid,comm | cat | cat &
[1] 1204
$ PID  PPID  PGID   SID COMMAND
  565   453   565   565 bash
 1202   565  1202   565 ps
 1203   565  1202   565 cat
 1204   565  1202   565 cat

如果一个后台进程试图读其控制终端:

$ cat > ttt &
[1] 1497
$
[1]+  Stopped                 cat > ttt
$ fg %1
cat > ttt
wudi     
123456
^D

如果后台作业试图读控制终端,则会产生信号SIGTTIN。导致后台进程停止。之后通过fg命令将指定1号作业设为前台作业,此时该作业可以从控制终端中获取输入

注意:不同shell对创建各个进程的方式不同。

本节中例子是以bash作为示例,而如果是经典的sh,那么对于管道命令则是管道的最后一个进程是shell的子进程,而管道的其他进程则是最后一条命令的子进程。除此之外还有其他区别,具体看书

在这里插入图片描述

10. 孤儿进程组

孤儿进程:

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:

如果子进程先退出了,父进程还未结束并且没有调用 wait或者 waitpid获取 子进程的状态信息,则子进程残留的状态信息会变成僵尸进程。

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

孤儿进程不会占用系统资源,最终是由init进程托管,由init进程来释放。

孤儿进程组: 整个进程组也可以成为孤儿

该组中每个成员的父进程要么是该组的一个成员,要么在其它会话中。

或者说,一个进程组不是孤儿进程组的条件是:该组中有一个进程,其父进程在属于同一个会话的另一个组中。

孤儿进程组提出原因(作用):

当一个终端控制进程(即会话首进程)终止后,那么这个终端可以用来建立一个新的会话。这可能会产生一个问题,当该终端控制进程终止后,原来旧的会话(一个或者多个进程组的集合)中的任一进程可再次访问这个的终端

为了防止这类问题的产生,在会话首进程(也是控制进程)终止后仍继续运行的进程组被标记为孤儿进程组。当一个进程组成为孤儿进程组时,posix.1要求向孤儿进程组中处于停止状态的进程发送SIGHUP(挂起)信号,系统对于这种信号的默认处理是终止进程,然而如果无视这个信号或者另行处理的话那么这个挂起进程仍可以继续执行。但它仍然无法再访问终端。

  • 父进程终止后,进程组成为了孤儿进程组。那么新的孤儿进程组中处于停止(stopped)状态的每一个进程都会收到挂断(SIGHUP)信号,接着又收到继续(SIGCONT)信号

    也就是说,进程组成为孤儿进程组后,孤儿进程组中的状态为stopped的进程会被激活。前提是需要对SIGHUP信号自处理,对挂断信号系统默认的动作是终止进程

  • 孤儿进程组成为后台进程组,且没有控制终端

  • 孤儿进程组去读控制终端时,read返回出错并将errno设置为EIO

11. FreeBSD中会话、进程组、进程、控制终端之间的关系实现

在这里插入图片描述

  • session结构体:每个会话都分配一个session结构(如调用setsid时)

    • s_count:进程组数
    • s_leader:会话首进程
    • s_ttyvp:指向控制终端设备文件的vnode结构(进程对/dev/tty的所有访问都通过vnode结构)
    • s_ttyp:指向控制终端tty结构
    • s_sid:会话ID(会话首进程ID)
  • tty结构:每个终端设备和每个伪终端设备均在内核分配这样一种结构

    • t_session:指向将此终端用作控制终端的会话
    • t_pgrp:前台进程组,由此可见前台进程组是终端的属性而不是进程的属性
    • t_termios:包含与该终端的有关信息(如波特率等)
    • t_winsize:包含终端窗口当前大小等信息
  • pgrp结构:包含进程组信息

    • pg_id:进程组ID
    • pg_session:此进程组所属会话
    • pg_members:此进程组的进程成员
  • proc结构:包含一个进程的所有信息

    • p_pid:进程pid
    • p_pptr:指向父进程
    • p_pgrp:指向所属进程组
    • p_pglist:指向所属进程组中上一个和下一个进程

为了找到特定会话的前台进程组,内核从session结构开始,然后用s_ttyp得到控制终端的tty结构体,然后从t_pgrp得到前台进程组的pgrp结构体

  • s_leader:会话首进程

  • s_ttyvp:指向控制终端设备文件的vnode结构(进程对/dev/tty的所有访问都通过vnode结构)

  • s_ttyp:指向控制终端tty结构

  • s_sid:会话ID(会话首进程ID)

  • tty结构:每个终端设备和每个伪终端设备均在内核分配这样一种结构

    • t_session:指向将此终端用作控制终端的会话
    • t_pgrp:前台进程组,由此可见前台进程组是终端的属性而不是进程的属性
    • t_termios:包含与该终端的有关信息(如波特率等)
    • t_winsize:包含终端窗口当前大小等信息
  • pgrp结构:包含进程组信息

    • pg_id:进程组ID
    • pg_session:此进程组所属会话
    • pg_members:此进程组的进程成员
  • proc结构:包含一个进程的所有信息

    • p_pid:进程pid
    • p_pptr:指向父进程
    • p_pgrp:指向所属进程组
    • p_pglist:指向所属进程组中上一个和下一个进程

为了找到特定会话的前台进程组,内核从session结构开始,然后用s_ttyp得到控制终端的tty结构体,然后从t_pgrp得到前台进程组的pgrp结构体

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值