UNIX环境高级编程 学习笔记 第九章 进程关系

每个进程都有一个父进程,这个父进程通常是初始的内核级进程。

早期UNIX系统中,用户用哑终端(只有输入输出字符功能,没有处理器和硬盘,通过串行接口连接到主机,一切工作交给主机做)进行登录,这些哑终端或者是本地直接连接的,或者是通过调制解调器远程连接的,这两种情况下,登录都经过内核中的终端设备驱动程序。因为连到主机上的终端设备数是固定的,所以同时登录数也有已知的上限。

随着位映射图形终端的出现,开发出了窗口系统,创建终端窗口的应用也开始出现,它仿真了基于字符的终端。

现在,某些平台允许用户登录后启动一个窗口系统,另一些则为用户自动启动一个窗口系统(可能用户还没登录,仍需在窗口系统中登录),这取决于窗口系统是如何配置的。

BSD终端登录过程:系统管理员通常创建名为/etc/ttys的文件,文件中,每个终端设备都有一行内容,该行内容说明设备名和传到getty程序的参数(如终端波特率等)。系统自举时,内核创建进程ID为1的进程(init进程),该进程会使系统进入多用户模式。init进程会读取文件/etc/ttys,对每个允许登录的终端设备,init进程调用一次fork,该子进程exec getty程序:
在这里插入图片描述
上图中所有进程的实际用户ID和有效用户ID都是0,即它们都具有超级用户权限。调用exec时,是以空环境调用的。

getty程序对终端设备调用open函数,以读写方式打开终端,如果设备是调制解调器,则open函数可能会等待设备驱动程序,直到用户拨号调制解调器,且接通线路,一旦设备被打开,文件描述符0、1、2就被设置到该设备,然后getty程序输出提示符,并等待用户键入用户名。如果终端支持多种速度,getty程序可测试特殊字符以适当地更改终端速度(波特率)。当用户键入了用户名后,getty程序的工作就完成了,它用类似于以下的方式调用login程序:
在这里插入图片描述
在getty程序的数据文件gettytab中,可能有一些选项能使getty调用其他的程序,但系统默认是调用login程序。init进程以空环境调用getty,getty以终端类型(TERM环境变量,取自gettytab数据文件)和在gettytab文件中说明的环境字符串为login进程创建一个环境,上述代码中的-p标志表示login保留传给它的环境变量,同时也可将其他环境变量加到该环境中,但不要替换它。以上环境变量变化过程图示:
在这里插入图片描述
上图中所有进程都有超级用户特权,下面3个进程的进程ID相同且父进程ID都是1。

login进程用得到的用户名调用getpwnam取得相应用户的口令文件项;然后调用getpass显示密码输入提示符且禁止回显用户键入的口令;得到用户口令后,login进程调用crypt将用户键入的口令加密,并与用户在阴影口令文件中的加密口令相比较。如果用户几次键入的口令都无效,则login程序以1调用exit表示登录过程失败。父进程(init)知道子进程的终止状态后,会再次调用fork,之后再调用getty,即重复以上过程。

以上是UNIX传统用户身份验证过程,现代UNIX支持多个身份验证过程,如PAM(Pluggable Authentication Modules,可插入的身份验证模块)可提供更灵活的方案。如应用需要验证用户是否具有权限执行某个服务,我们可以将身份验证机制编写到应用中,使用PAM库也能达到同样目的,PAM库的优点是管理员可基于不同服务配置不同的验证用户身份的方法。

login程序在用户输入正确的口令后,执行以下操作:
1.将当前工作目录改为用户的起始目录(chdir函数)。

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

3.对该设备的访问权限改为用户读写。

4.调用setgid(setgid函数会更改3个组ID:实际组ID、有效组ID、保存的组ID)和initgroups设置进程的组ID。

5.用login得到的信息初始化环境:起始目录(HOME)、shell(SHELL)、用户名(USER和LOGNAME)、默认的系统路径(PATH)。

6.将login进程的用户ID改为登录用户(setuid函数会更改3个用户ID:实际用户ID、有效用户ID、保存的用户ID),并执行类似于以下的命令调用用户的登录shell:
在这里插入图片描述
上图中,argv[0]的第一个字符是一个负号,这表示该shell被作为登录shell调用,shell可查看该字符,以修改其启动过程。

除以上操作外,login进程可选择地打印每日消息文件(即显示一条每日消息或欢迎消息)、检查新邮件或执行其他任务。

至此,登录用户的登录shell开始运行,其父进程ID是1,当此登录shell终止时,init进程会收到SIGCHLD信号,它对该终端重复全部上述过程。登录shell的文件描述符0、1、2会设置为终端设备。
在这里插入图片描述
之后,登录shell执行其启动文件(Bourne shell和Korn shell是.profile;GNU Bourne-again shell是.bash_profile、.bash_login、.profile;C shell是.cshrc、.login),这些启动文件通常更改某些环境变量并增加很多环境变量(如增加环境变量PATH、提示终端类型TERM等)。执行完启动文件后,打印提示符,用户就可键入命令了。

Mac OS X终端登录过程:Mac OS X部分基于FreeBSD,因此其终端登录过程与BSD终端登录过程基本一致,不同点在于Mac OS X有以下变化:
1.init进程的工作由launched进程完成。

2.一开始提供的就是图形终端。

Linux终端登录过程:Linux终端登录过程类似BSD,Linux的login命令是从4.3 BSD的login命令派生的,BSD登录过程与Linux的登录过程主要区别在于说明终端配置的方式。

有些Linux发行版的init程序使用了/etc/inittab文件包含配置信息(指定了一些终端设备,init进程会为这些终端设备调用getty程序),而非使用/etc/ttys文件。

其他Linux发行版,如Ubuntu有称为Upstart的init程序,使用存放在/etc/init目录下的*.conf命名的配置文件,如运行/dev/tty1的getty程序需要的说明可能存放在/etc/init/tty1.conf文件中。

Solaris终端登录:支持两种方式:
1.getty方式,与BSD终端登录相同。通常用于控制台登录。

2.ttymon登录:SVR4引入的新特性。通常用于除控制台外的其他终端登录。

ttymon命令是服务访问设施(Service Access Facility,SAF)的一部分,SAF的目的是用一致的方式对提供系统访问的服务进行管理。init进程是sac(service access controller,服务访问控制器)的父进程,sac进程调用fork,当系统进入多用户状态时,sac进程的子进程执行ttymon程序。ttymon进程监控在配置文件中列出的所有终端端口,当用户键入登录名时,它调用一次fork,之后ttymon进程的子进程执行login命令,login进程向用户发出提示,要求输入口令,口令正确后,login进程exec登录用户的登录shell,此登录shell的父进程是ttymon进程,而getty登录过程中,登录shell的父进程是init。

通过串行终端登录到系统和经由网络登录到系统的区别是:网络登录时,在终端和计算机之间的连接不再是点到点的,网络登录时,login仅仅是一种可用的服务,与其他网络服务(如FTP、SMTP)性质相同。

非网络登录时,init进程知道哪些终端设备可用来进行登录,并为每个设备生成一个getty进程。但网络登录时,所有登录都经由内核的网络接口驱动程序(如以太网驱动),且事先不知道会有多少个这样的登录。

为同一个软件既能处理终端登录,又能处理网络登录,系统使用了称为伪终端的软件驱动程序,它仿真串行终端的运行行为,并将终端操作映射为网络操作,反之亦然。

BSD网络登录过程:作为系统的启动部分,init进程调用一个shell,使该shell执行shell脚本/etc/rc,此shell脚本启动一个守护进程inetd,一旦此shell脚本终止,inetd的父进程就变为init进程。inetd进程等待TCP/IP连接请求到达主机,每当一个连接请求到达,它调用一次fork,然后生成的子进程exec适当的程序。

假定一个对于telnet服务进程的TCP连接请求到达,以下是执行telnet服务进程(telnetd)中所涉及的进程序列:
在这里插入图片描述
之后telnetd进程打开一个伪终端设备,并调用fork,父进程处理通过网络连接的通信,子进程执行login程序,父进程和子进程通过伪终端相连接。子进程调用exec执行login之前,子进程使其文件描述符0、1、2与伪终端相连,如果login登录正确,login就执行将当前工作目录改为起始目录、设置登录用户的组ID和用户ID、初始环境等工作。之后login调用exec执行用户的登录shell。以上过程图示:
在这里插入图片描述
通过终端或网络登录时,我们得到一个登录shell,其标准输入、标准输出和标准错误要么连接到一个终端设备,要么连接到一个伪终端设备上。这一登录shell是一个POSIX.1会话的开始,而此终端或伪终端是会话的控制终端。

Mac OS X网络登录过程:Mac OS X部分基于FreeBSD,其网络登录与BSD网络登录基本相同,但Mac OS X上telnet守护进程是从launchd进程运行的。telnet守护进程在Mac OS X中默认是禁用的(虽然可以通过launchctl命令启动),Mac OS X上执行网络登录的更好方法是ssh。

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

Solaris网络登录过程:与BSD和Linux中步骤几乎一样,但Solaris中,inetd进程作为服务管理设施(Service Management Facility,SMF)的restarter运行,restarter是守护进程,负责启动和监视其他守护进程。inetd由SMF中的主restarter启动,主restarter由init进程启动。

进程属于一个进程组,进程组是一个或多个进程的集合,通常,它们是在同一作业中结合起来的,同一进程组中的进程接受来自同一终端的各种信号,每个进程组有一个唯一的进程组ID,进程组ID是一个正整数,可存放在pid_t数据类型中。获取调用进程的进程组ID:
在这里插入图片描述
早起BSD派生的系统中,该函数的参数是pid,返回该进程的进程组ID,SUS中定义了以下函数模仿此行为:
在这里插入图片描述
如果pid参数为0,返回调用进程的进程组ID。

每个进程组有一个组长进程,组长进程的进程组ID等于其进程ID。

进程组组长可创建一个进程组、创建该组中的进程。只要某个进程组中有一个进程存在,该进程组就存在,与其组长进程是否终止无关。某个进程组中的最后一个进程可以终止,也可以转移到另一个进程组,之后原进程组消失。

加入或创建一个新进程组:
在这里插入图片描述
setpgid函数将参数pid表示的进程的进程组ID设为参数pgid表示的进程组ID,如果这两个参数相等,则进程id为参数pid的进程变成进程组组长。pid参数为0时,使用调用者的进程ID;pgid参数为0时,使用调用者的进程ID作为进程组ID。

一个进程只能为它自己或它的子进程设置进程组ID,子进程调用exec后,父进程就不能再更改这个子进程的进程组ID。

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

信号可发送给一个进程或一个进程组中所有进程。waitpid函数可等待某个进程或指定进程组中的一个进程终止。

会话是一个或多个进程组的集合:
在这里插入图片描述
通常是由shell的管道将几个进程编成一组的,上图中的情形可能是由以下shell命令形成的:

proc1 | proc2 & proc3 | proc4 | proc5

创建一个会话:
在这里插入图片描述
如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。调用setsid的进程发生的事:
1.该进程变成新会话的会话首进程(session leader,即创建该会话的进程),此进程此时是新会话中的唯一进程。
2.该进程成为一个新进程组的组长进程。
3.该进程没有控制终端。如果调用setsid前该进程有一个控制终端,那么这种联系也会被切断。

如果调用setsid的进程已经是一个进程组的组长,则此函数返回出错。为了保证调用setsid的进程不是一个进程组的组长,通常先调用fork,然后使其父进程终止,因为子进程继承了父进程的进程组ID,而子进程的进程ID不同于父进程的进程ID,因此保证了子进程不是一个进程组的组长。

SUS只说明了会话首进程,但没有会话ID,可将会话首进程的进程ID视为会话ID。会话ID由SVR4引入,历史上基于BSD的系统不支持会话ID的概念,后来才支持了会话ID。一些实现(如Solaris)与SUS保持一致,不使用“会话ID”,而是称其为“会话首进程的进程组ID”,会话首进程总是一个进程组的组长进程,因此会话首进程的进程组ID与会话首进程的进程ID相等。

获取会话首进程的进程ID:
在这里插入图片描述
如果参数pid为0,则返回调用进程的会话首进程的进程组ID。出于安全考虑,一些实现有如下限制:如果参数pid不属于调用者所在的会话,那么就不能获得参数pid进程所在会话的会话首进程的进程组ID。

会话和进程组的一些特性:
1.一个会话可以有一个控制终端,终端登录情况下通常是终端设备,网络登录情况下通常是伪终端。

2.建立到控制终端的连接的会话首进程称为控制进程。

3.一个会话中的几个进程组可被分为一个前台进程组和一个或多个后台进程组。

4.如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。

5.键入终端的中断键(Delete或Ctrl+C)或退出键(Ctrl+\),会将中断信号发到前台进程组的所有进程。

6.如果终端接口检测到调制解调器(或网络)已经断开连接,则将挂断信号发送到控制进程。
在这里插入图片描述
登录时,会自动建立到控制终端的连接。

POSIX.1将如何分配一个控制终端的机制交给具体实现来选择。当会话首进程打开第一个尚未与一个会话相关联的终端设备时,只要在调用open时没有指定O_NOCTTY标志,System V派生的系统将此终端作为控制终端分配给会话;基于BSD的系统当会话首进程用TIOCSCTTY作为request参数调用ioctl时(同时将第三个参数设为空指针),为会话分配控制终端,但此时该会话不能已经有控制终端(通常ioctl调用紧跟在setsid调用后,setsid函数保证此进程是一个没有控制终端的会话首进程),BSD系统不使用open函数的O_NOCTTY标志,除了以兼容模式支持其他系统时。
在这里插入图片描述
有时程序想与控制终端通信,但标准输出、标准输入、标准错误被重定向了,保证程序是在跟控制终端交流的方法是通过open函数打开文件/dev/tty,这个特殊文件在内核中是控制终端的同义词,如果程序没有控制终端,对于此设备的open函数将失败。

典型例子是用来读口令的getpass函数(终端回显关闭),此函数被crypt程序调用,如以下命令:

crypt < salaries | lpr

该命令将文件salaries解密,并将输出送到打印缓冲服务程序。由于crypt命令从标准输入读输入文件salaries,所以标准输入不能用于输入口令。并且crypt命令经过设计,每次运行此程序时都要输入加密口令,这样防止了用户将口令存放在文件中(这样是一个安全漏洞)。

需要有一种方法来通知内核哪个进程组是前台进程组,这样终端设备驱动程序就能知道将终端输入和终端产生的信号发送到何处。获取和设置前台进程组的函数:
在这里插入图片描述
tcgetpgrp函数返回参数fd关联的终端的前台进程组ID。

如果进程有一个控制终端,则该进程可调用tcsetpgrp函数将前台进程组ID设为参数pgrpid,pgrpid参数值应当是同一会话中的一个进程组的ID,fd参数必须引用的是该会话的控制终端。

以上两个函数大多应用不直接调用,而是通常由作业控制shell调用。

通过与终端关联的文件描述符获取该终端的会话首进程的进程组ID:
在这里插入图片描述
作业控制是BSD在1980年左右增加的特性,它允许在一个终端上启动多个作业(进程组),它控制哪个作业可以访问该终端以及哪些作业在后台运行。作业控制需要以下三方面支持:
1.支持作业控制的shell。

2.内核中终端驱动程序必须支持作业控制。

3.内核提供对作业控制信号的支持。

SVR3提供了一种不同的作业控制,称为shell层,但POSIX.1选择了BSD形式的作业控制。POSIX.1的早期版本中,对作业控制的支持是可选的,现在是必需的。

用户可在shell的前台或后台启动一个作业,一个作业只是几个进程的集合,通常是一个进程管道,如:

vi main.c

以上命令在前台启动了一个只有一个进程组成的作业。以下命令:

pr *.c | lpr & 
make all &

在后台启动了两个作业。

启动一个后台作业时,shell赋予它一个作业标识符,并打印一个或多个进程ID,以下是Korn shell的处理:
在这里插入图片描述
make命令的作业编号是1,所启动的进程ID是1475。管道的作业编号为2,其第一个进程的进程ID为1490。当作业完成后键入回车时,shell通知作业已完成(shell只在打印shell提示符让用户输入新命令前才打印后台作业的状态改变,如果不是这样,在我们输入时也可能输出,就会引起混乱)。

以下特殊字符产生的信号只会发送到前台进程组:
1.中断字符(Delete或Ctrl+C),产生SIGINT。

2.退出字符(Ctrl+\),产生SIGQUIT。

3.挂起字符(Ctrl+Z),产生SIGTSTP。

只有前台作业才能接收终端上键入的字符,如果后台作业试图读终端,终端驱动程序会向后台作业发送一个特定的信号SIGTTIN,该信号通常挂起此作业,且shell向用户发出这种情况的通知,之后用户可用shell命令将此作业转为前台作业运行,于是它就可以读终端:
在这里插入图片描述
上例在Mac OS X 10.6.8中不起作用,当试图把cat命令放到前台时,read函数将返回失败,并将errno设为EINTR。Mac OS X基于BSD,在FreeBSD下本例正常运行,因此这应该是Mac OS X的一个bug。

以上过程中,shell先在后台启动cat进程,当cat试图读标准输入(控制终端)时,终端驱动程序知道它是后台作业,于是将SIGTTIN信号发送到后台作业从而挂起它,shell此时检测到其子进程状态改变(通过wait或waitpid函数),通知我们该作业已被停止,然后我们用fg命令将此挂起的作业送入前台运行,这样此作业就被shell转变为了前台进程组(通过tcsetpgrp函数),并将继续信号SIGCONT送给该进程组,之后此进程就可正常读控制终端了。

有选项可以控制是否允许后台作业读控制终端,通常,stty命令可改变这一选项:
在这里插入图片描述
上例中,当用户禁止后台作业向控制终端写后,该作业的cat命令试图向其写,此时终端驱动程序识别出该写操作来自后台进程,于是向该作业发送SIGTTOU信号,于是cat进程被阻塞。
在这里插入图片描述
在不支持作业控制的Solaris上运行的Bourne shell中执行以下命令:

ps -o pid, ppid, pgid, sid, comm

则输出可能是:
在这里插入图片描述
ps命令默认输出与调用者关联的终端中所有有效用户ID为调用者UID的进程;-o选项控制输出格式。可见ps进程的父进程是shell,shell和ps两者位于同一会话和前台进程组(949)。

如果以上命令在后台执行,输出中只有进程ID有改变:
在这里插入图片描述
因为这种shell不知道作业控制,所以没有将后台作业放入后台作业自己的后台进程组,也没有从后台作业处取走控制终端。

查看Bourne shell如何处理管道:

ps -o pid,ppid,pgid,sid,comm | cat1

输出为:
在这里插入图片描述
cat1命令是cat命令的一个副本,与cat完全相同。

可见管道中最后一个进程是shell的子进程,管道中的第一个进程是管道中最后一个进程的子进程,可见,shell fork一个它自身的副本,然后此副本再为管道中的每条命令fork一个子进程。

如果在后台执行以上管道命令,则输出中唯一的不同只是进程ID,因为此shell不处理作业控制。

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

cat > temp.foo &

以上命令在有作业控制时,后台作业被放在后台进程组,如果后台作业试图读控制终端,则会产生信号SIGTTIN;在没有作业控制时,如果该进程自己没有重定向标准输入,则shell自动将后台进程的标准输入重定向到/dev/null,而读/dev/null会产生一个文件结束符,cat命令会立即读到文件尾,并正常终止。

以上过程是后台进程通过标准输入正常读控制终端的处理方法,但如果后台进程通过打开/dev/tty读控制终端:

crypt < salaries | lpr &

crypt程序正常流程为打开/dev/tty,更改终端特性(禁止回显),然后从该设备读,最后重置该终端特性。如果没有作业控制,会有两个进程(shell和crypt进程)同时读一个设备,执行结果依赖于系统。执行以上命令时可能发生的一种情况为,crypt先在终端上打印提示符让用户输入密码,但读取我们口令的是shell,且shell会将加密口令当成命令执行,而我们的下一次终端输入才会被crypt读取作为口令,于是salaries就不能正确被译码,结果将一堆无用信息送到了打印机。

在Bourne shell中执行以下命令:

ps -o pid,ppid,pgid,sid,comm | cat1 | cat2

其输出为:
在这里插入图片描述
输出也有可能为:
在这里插入图片描述
造成输出不同的原因为ps进程与ps的父进程(cat2进程)产生竞争条件,当ps进程的父进程还未exec cat2进程时,ps进程获取了进程列表,就会产生以上第二张图的输出。
在这里插入图片描述
由于管道中的最后一个进程是shell的子进程,因此只有当管道中最后一个进程终止时,shell才会得到通知。

使用有作业控制的Bourne-again shell执行以下命令:

ps -o pid,ppid,pgid,sid,tpgid,comm

输出为:
在这里插入图片描述
粗体显示的是前台进程组ID。可见有作业控制时,shell将前台作业放入了该作业自己的进程组。ps进程是进程组组长进程,且此进程组具有控制终端,因此它是前台进程组。登录shell在执行ps命令时是后台进程组。

在后台重新执行以上命令:

ps -o pid,ppid,pgid,sid,tpgid,comm &

输出为:
在这里插入图片描述
ps进程再次被放入该进程自己的进程组,此时前台进程组是shell。

在管道中执行以下两个命令:

ps -o pid,ppid,pgid,sid,tpgid,comm | cat1

输出为:
在这里插入图片描述
管道中的这两个进程都在新的进程组中,且新进程组是前台进程组。管道中所有进程的父进程都是shell。如果在后台执行以上管道命令:

ps -o pid,ppid,pgid,sid,tpgid,comm | cat1 &

输出为:
在这里插入图片描述
唯一的改变就是ps和cat1进程都处于同一后台进程组。

以上过程中,先创建的ps进程再创建的cat1进程,使用的shell不同,创建的顺序也可能不同。

如下图,父进程fork子进程,然后父进程即将终止前:
在这里插入图片描述
上图描述了以下代码的过程:

#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

static void sig_hup(int signo) {
    printf("SIGHUP received, pid = %ld\n", (long)getpid());
}

static void pr_ids(char *name) {
    printf("%s: pid = %ld, ppid = %ld, pgrp = %ld, tpgrp = %ld\n",
    	name, (long)getpid(), (long)getppid(), (long)getpgrp(), (long)tcgetpgrp(STDIN_FILENO));

    fflush(stdout);
}

int main() {
    char c;
    pid_t pid;

    pr_ids("parent");
    if ((pid = fork()) < 0) {
        printf("fork error\n");
		exit(1);
    } else if (pid > 0) {
        sleep(5);    // sleep to let child stop itself
    } else {
        pr_ids("child");
		signal(SIGHUP, sig_hup);    // establish signal handler
		kill(getpid(), SIGTSTP);    // stop ourself
		pr_ids("child");    // pirnts only if we're continued
		if (read(STDIN_FILENO, &c, 1) != 1) {
		    printf("read error %d on controlling TTY\n", errno);
		}
	
		exit(0);
    }
}

关于以上代码的解释:
1.父进程睡眠5秒,是让子进程在父进程终止之前运行的权宜之计。

2.子进程为挂断信号SIGHUP建立信号处理程序。

3.子进程用kill函数向自身发送停止信号SIGTSTP,相当于用终端挂起字符Ctrl+Z停止(挂起)一个前台作业。

4.父进程终止时,子进程变为孤儿进程,子进程的父进程ID将成为1。

5.此时子进程成为一个孤儿进程组的成员,POSIX.1将孤儿进程组定义为:该组中每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员。一个进程组不是孤儿进程组的条件是:该组中有一个进程,其父进程在属于同一会话的另一个组中。如果进程组不是孤儿进程组,那么在属于同一会话中的另一进程组中的父进程就有机会重新启动该组中停止的进程。

6.POSIX.1要求向新孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),之后再向其发送继续信号(SIGCONT)。

7.子进程处理完挂断信号后,子进程继续。对挂断信号的默认系统动作是终止该进程。

以上程序的输出:
在这里插入图片描述
由于登录shell和子进程同时向终端写,因此shell提示符和子进程的输出一起出现。子进程第二次调用pr_ids后,程序企图读标准输入,此时子进程已经是后台作业,因此会对该后台进程组产生SIGTTIN,此信号会停止进程,但由于此进程所属进程组是孤儿进程组,停止后就不能再继续,因此POSIX.1规定,read函数返回出错,errno设为EIO(本系统中该值为5)。

父进程终止时,子进程会变为后台进程组,因为父进程是由shell作为前台作业执行的。
在这里插入图片描述
每个会话都有一个session结构,调用setsid创建会话时会分配一个该结构,该结构中字段含义:
1.s_count:该会话中进程组数,当此计数器减到0时,可释放此结构。

2.s_leader:指向会话首进程的proc结构的指针。

3.s_ttyvp:指向控制终端的vnode结构的指针。

4.s_ttyp:指向控制终端的tty结构的指针。

5.s_sid:会话ID(此概念非SUS的组成部分)。

调用setsid时,在内核中分配一个新的session结构,并将s_count字段设为1,s_leader字段设为指向调用进程的proc结构的指针,s_sid字段设为调用进程ID,由于新会话没有控制终端,因此s_ttyvp和s_ttyp结构设为空指针。

每个终端设备和伪终端设备在内核中都有tty结构,该结构中字段含义如下:
1.t_session:指向将此终端设为控制终端的session结构。终端在失去载波信号时使用此指针将挂起信号发送给会话首进程。

2.t_pgrp:指向前台进程组的pgrp结构。终端驱动程序使用此字段将信号(输入特殊字符时产生的中断、退出、挂起信号)发送给前台进程组。

3.t_termios:关于此终端的所有特殊字符和相关信息(波特率、是否开启回显等)。

4.t_winsize:终端窗口当前大小的winsize结构,终端窗口大小改变时,SIGWINCH信号发送到前台进程组。

为找到特定会话的前台进程组,内核通过session.s_ttyp得到终端的tty结构,然后通过tty.t_pgrp得到前台进程组的pgrp结构。

pgrp结构包含一个特定进程组的信息,其中字段含义如下:
1.pg_id:进程组ID。

2.pg_session:指向此进程组所属会话的session结构。

3.pg_members:指向此进程组中进程的proc结构表的指针。

进程的proc结构字段含义:
1.p_pglist:是一个双向指针,指向该进程组中的下一个和上一个进程。

2.p_pid:进程ID。

3.p_pptr:指向父进程proc结构的指针。

4.p_pgrp:指向本进程所属进程组的pgrp结构指针。

打开控制终端设备时分配vnode结构,进程对/dev/tty的所有访问都通过vnode结构。

utmp(包含当前登录的用户信息)和wtmp(所有用户的登录注销记录)文件由init进程写,原因是登录shell的父进程是init进程,登录shell终止时它收到SIGCHLD信号,所以init进程知道什么时候终端用户注销。而在网络登录过程中,没有包含init进程(init进程派生出子进程inetd,之后从telnet来的网络登录请求发给inetd,inetd收到网络登录请求后派生出子进程,子进程会exec telnetd),此时此文件中内容由一个处理登录并检测注销的进程写(即telnetd)。

验证一个进程建立新会话后会变成进程组组长,且不再有控制终端:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    pid_t pid;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
		exit(1);
    } else if (pid == 0) {
		if (open("/dev/tty", O_RDWR) < 0) {
		    printf("open /dev/tty fail, child progress don't have ct\n");
		    exit(1);
		} else {
		    printf("before setsid, child progress have ct\n");
		}

        pid_t childProgressGroupId = setsid();
		if (childProgressGroupId == -1) {
		    printf("setsid error\n");
		    exit(1);
		}
	
		printf("child pgid : %ld, child pid: %ld\n", childProgressGroupId, getpid());
	
	    // 虽然失去了控制终端,但其标准输出文件描述符还关联在父进程的终端,仍然可以输出到该终端
		if (open("/dev/tty", O_RDWR) < 0) {
		    printf("open /dev/tty fail, child progress don't have ct\n");
		}
    }

    exit(0);
}

运行以上程序:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值