进程关系
进程关系:
终端登录:
当我们登录到UNIX系统时所执行的各个程序。在早期的UNIX系统中,用户用哑终端(用硬连接到主机)进行登录。终端或者
是本地的(直接连接)或者是远程的(通过调制解调器连接)。在这两种情况下,登录都经由内核中的终端设备驱动程序。例如,
在PDP-11上常用的设备是DH-11和DZ-11。因为连接到主机上的终端设备数是固定的,所以同时的登录数也就有了已知的上限。
随着位映射图形终端的出现,开发了窗口系统,它向用户提供了与主机系统进行交互的新方式。创建终端窗口的应用也被开发出来,
它仿真了基于字符的终端,使得用户可以用熟悉的方式(即通过shell命令行)与主机进行交互。
1、BSD终端登录
系统管理者创建通常为/etc/ttys的文件,其中,每个终端设备都有一行说明设备名和传到getty程序的参数。例如,其中一个参数
说明了终端的波特率等。当系统自举时,内核创建进程ID为1的进程,也就是init进程。init进程使系统进入多用户模式。init读取
文件/etc/ttys,对每一个允许登录的终端设备,init调用一次fork,它所生成的子进程则exec getty程序。
getty程序的作用:
getty对终端设备调用open函数,以读、写方式将终端打开。如果设备是调制解调器,则open可能会在设备驱动程序中滞留,直到用户
拨号调制解调器,并且线路被连通。一旦设备被打开,则文件描述符0、1、2就被设备到该设备。然后getty输出"login:"之类的信息,
并等待用户键入用户名。如果终端支持多种速度,则getty可以测试特殊字符以便适当地更改终端速度。
当用户键入用户名后,getty的工作就完成了。然后它就类似于下列的方式调用login程序:
execle("/bin/login","login", "-p",username,(char *)0,envp);//系统默认的是login程序
init以一个空环境调用getty。getty以终端名(如TERM=foo,其中终端foo的类型取自gettyab文件)和在gettyab中说明的环境变量
为login创建一个环境(envp参数)。-p标志通知login保留传递给它的环境,也可将其他环境字符串加到该环境中,但是不要替换它。
login能处理多项工作。因为它得到了用户名,所以调用getpwnam取得相应用户的口令文件登录项。然后调用getpass(3)显示提示
“Password:”,用户输入口令后,它调用crypt(3)//rypt(const char *key, const char *salt) 函数为C语言函数之一,返回使用 DES、Blowfish 或 MD5 加密的字符串
将用户键入的口令加密,并与该用户在阴影口令文件中登录项的pw_passwd字段相比较。如果用户几次键入的口令都无效,则login以参数1
调用exit表示登录过程失败。父进程(init)了解到子进程的终止情况后,将再次调用fork,其后又执行了getty,重复。
2、Linux终端登录
类似与BSD
网络登录:
通过串行终端登录至系统和经由网络登录至系统两者之间的主要(物理上的)区别是:网络登录时,在终端和计算机之间的连接不再时点到点
的。在网络登录下,login仅仅是一种可用的服务,这与其他网络服务的性质相同。
在终端登录中,init知道那些终端设备可用来进行登录,并为每个设备生成一个getty进程。但是,对网络登录情况则不同,所有登录都经
由内核的网络接口驱动程序(如以太网驱动程序),而且事先并不知道将会由多少这样的登录。因此必须等待一个网络连接请求的到达,而不
是使一个进程等待每一个可能的登录。
为使同一个软件既能处理终端登录,又能处理网络登录,系统使用了一种称为伪终端的软件驱动程序,它仿真串行终端的运行行为,并将终端
操作映射为网络操作,反之亦然。
1、BSD网络登录
在BSD中,由一个inetd进程(有时称为因特网超级服务器),它等待大多数网络连接。
作为系统启动的一部分,init调用一个shell,使其执行shell脚本/etc/rc。由此shell脚本启动一个守护进程inetd。一旦此shell脚本
终止,inetd的父进程就变成了init。inetd等待TCP/IP连接请求到达主机,而当一个连接请求到达时,它执行一次fork,然后生成的子进程
exec适当的程序。
2、Linux网络登录:
除了有些版本把inetd替换为了xinetd。
进程组:
每个进程除了有一进程ID之外,还属于一个进程组。
进程组时一个或者多个进程的集合。通常,它们是在同一作业中结合起来的。同一进程组的各进程接收来自同一终端的各种信号。每个进程组
有一个唯一的进程组ID。进程组ID类似于进程ID---它是一个正整数,并可存放在pid_t数据类型中。函数getpgrp返回调用进程的进程组
ID。
#include <unistd.h>
pid_t getpgrp(void);
返回值:调用进程的进程组ID
Single UNIX Specification定义了getpgid函数:
#include <unistd.h>
pid_t getpgid(pid_t pid);
若pid是0,返回调用进程的进程组ID,于是,
getpgid(0);
等价于
getpgrp();
每个进程组有一个组长进程。组长进程的进程组ID等于其进程ID。
进程组组长可以创建一个进程组、创建该组中的进程,然后终止。只要在某个进程组有一个进程存在,则该进程组就存在,
这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命期。某个进程
组中的最后一个进程可以终止,也可以转移到另一个进程组。
进程调用setpgid可以加入一个现有的进程组或者创建一个新进程组。
#include <unistd.h>
int setpgid(pid_t pid,pid_t pgid);
返回值:成功,返回0,出错,返回-1
函数将pid进程的进程组ID设置为pgid。
会话:
进程调用setsid函数建立一个新会话:
#include <unistd.h>
pid_t setsid(void);
返回值:成功,返回进程组ID,失败,返回-1
如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。
1、该进程标程新会话的会话首进程。此时,该进程是新会话中的唯一进程。
2、该进程称为一个新进程组的组长进程,新进程组ID是该调用进程的进程ID
3、该进程没有控制终端。如果在调用setsid之前该进程有一个控制终端
getsid函数返回会话首进程的进程组ID:
#include <unistd.h>
pid_t getsid(pid_t pid);
返回值:若成功,返回会话首进程的进程组ID,出错,返回-1.
控制终端:
会话和进程组还有一些其他特性:
1、一个会话可以有一个控制终端。这通常是终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)
2、建立与控制终端连接的会话首进程被称为控制进程
3、一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组
4、如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组
5、无论和是键入终端的中断键,都会将中断信号发送至前台进程的所有进程
6、如果检测到调制器已经断开连接,则将挂断信号发送至控制进程。
函数tcgetpgrp、tcsetpgrp、tcgetsid:
需要有一种方法通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能直到将终端输入和终端产生的信号
发送到何处。
#include <unistd.h>
pid_t tcgetpgrp(int fd);
返回值:成功,返回前台进程组ID,出错,返回-1
int tcsetpgrp(int fd,pid_t pgrpid);
返回值:成功,返回0,出错,返回-1
函数tcgetpgrp返回前台进程组ID,它与在fd上打开的终端相关联。
如果进程有一个控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设置为pgrpid。
pgrpid值应当是在同一会话中的一个进程组的ID,fd必须引用该会话的控制终端。
大多数应用程序并不直接调用这两个函数。它们通常由作业控制shell调用。
给出控制TTY的文件描述符,通过tcgetsid函数,应用程序就能获得会话首进程的进程组ID。
#include <termios.h>
pid_t tcgetsid(int fd);
返回值:成功,返回会话首进程的进程组ID;出错,返回-1
需要管理控制终端的应用程序可以调用tcgetsid函数识别出控制终端的会话首进程的会话ID
(它等价于会话首进程的进程组ID)。
作业控制:
作业控制它允许在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业在后台运行。
作业控制要求以下3种形式的支持:
1、支持作业控制的shell
2、内核种的终端驱动程序必须支持作业控制
3、内核必须提供对某些作业控制信号的支持
从shell使用作业控制功能的角度观察,用户可以在前台或者后台启动一个作业。一个作业只是几个进程的集合,通常是一个
进程管道。例如:
vi main.c
在前台启动了一个只有一个进程组成的作业。下面命令:
pc *.c | lpr &
make all &
在后台启动了两个作业。这两个后台作业调用的所有进程都在后台运行。
我们需要一个支持作业控制的shell以使用由作业控制提供的功能。对于早期的系统,shell是否支持作业控制比较容易说明。
C shell支持作业控制,Bourne shell不支持,而Korn shell能否支持作业控制取决于主机是否支持作业控制。
当启动一个后台作业时,shell赋予它一个作业标识符,并打印一个或者多个进程ID。下面的脚本显示了Korn shell是如何
处理这一点的:
$ make all >Make.out &
[1] 1475
$ pr *.c | lpr &
[2] 1490
$ 键入回车
[2] + Done pr *.c | lpr &
[1] + Done make all > Make.out &
make是作业编号1,所启动的进程ID是1475。下一个管道是作业编号2,其第一个进程的进程ID是1490。当作业完成而且键入回车时,
shell通知作业已经完成。键入回车是为了让shell打印其提示符。shell并不在任意时刻打印后台作业的状态改变----它只在打印
其提示符让用户输入新的命令行之前才这样做。如果不这样处理,则当我们正输入一行时,它也可能输出,于时,就会引起混乱。
挂起键(通常时Crtl+Z),键入此字符使终端驱动程序将信号SIGTSTP发送至前台进程组中的所有进程,后台进程组作业不受影响。
实际上有3个特殊字符可使终端驱动程序产生信号,并将它们发送至前台进程组,它们是:
1、中断字符(一般采用Delete或者Ctrl+C)产生SIGINT
2、退出字符(Ctrl+\)产生SIGQUIT
3、挂起字符(Ctrl+Z)产生SIGTSTP
终端驱动程序必须处理与作业控制有关的另一种情况。我们可以有一个前台作业,若干个后台作业,这些作业中哪一个接受我们在终端
键入的字符?只有前台作业接受终端输入。如果后台作业试图读终端,这并不是一个错误,但是终端驱动程序将检测这种情况,并且向
后台发送一个特定信号SIGTTIN。该信号通常会停止此后台作业,而shell则向有关用户发出这种情况的通知,然后用户就可用shell命令
将此作业转为前台作业运行,它就可读终端了。下了操作显示了这一点:
$ cat temp.foo & 在后台执行
[1] 1719
$ hello,world 提示符后出现后台作业的输出
输入回车
[1] + Done cat temp.foo &
$ stty tostop 禁止后台作业输出至控制终端
[1] 1721
$ 键入回车,发现作业已终止
[1] + Stopped(SIGTTOU) cat temp.foo &
$ fg %1 在前台恢复停止的作业
cat temp.foo shell告诉我们现在哪一个作业在前台
hello,world 这是作业的输出
在用户禁止后台作业向控制终端写时,该作业的cat命令试图写其标准输出,此时,终端驱动程序识别出该写操作来自于后台进程,于是
向该作业发送SIGTTOU信号,cat进程阻塞。与上面例子一样,当用户使用shell的fd命令将该作业转为前台时,该作业继续执行直到完成。
前面以说明的作业控制的某些功能。穿过终端驱动程序的实现表明终端I/O和终端产生的信号总是从前台进程组连接到实际终端。对应于
SIGTTOU信号的虚线表明后台进程组进程的输出是否出现在终端是可选择的。
shell执行程序:
让我们检验一下shell是如何执行程序的,以及这与进程组、控制终端和会话等概念的关系。为次,再次使用ps命令。
首先使用不支持作业控制的、在Solaris上运行的经典Bourne shell。如果执行:
ps -o pid,ppid,pgid,sid,comm
则其输出可能是:
PID PPID PGID SID COMMAND
3749 3742 3749 3749 bash
26162 3749 26162 3749 ps
ps的父进程是shel,这正是我们所期望的。shell和ps命令两者位于同一会话和前台进程组(949)中。因为我们是一个用一个
支持作业控制的shell执行命令时得到该值的,所以称其为前台进程组。
如果在后台执行命令:
ps -o pid,ppid,pgid,sid,comm &
则唯一改变的值时命令的进程ID:
PID PPID PGID SID COMMAND
3749 3742 3749 3749 bash
26162 3749 26162 3749 ps
孤儿进程组:
一个其父进程已终止的进程称为孤儿进程,这种进程有init进程收养。
技术不分国界