当系统自举时,内核创建进程ID为1的进程,也就是Init进程。init读取文件/etc/ttys,对每一个允许登陆的终端设备,init调用一次fork,它所生成的子进程则exec getty程序。
图9-1中所有进程的实际用户ID和有效用户ID都是0(也就是说,它们都具有超级用户特权)。init以空环境exec getty程序。
getty对终端设备调用open函数,以读、写方式将终端打开。如果设备是调制解调器,则open可能会在设备驱动程序中滞留,知道用户拨号调制解调器,并且线路被接通。一旦设备被打开,则文件描述符0,1,2就被设置到该设备。然后getty射出“login:”之类的信息,并等待用户键入用户名。
当用户键入了用户名后,getty的工作就完成了。然后它以类似于谢列的方式调用login程序:
execle(“/bin/login”,”login”,”-p”,username,(char *)0,envp);
init以一个空环境调用getty。getty以终端名和在文件gettytab中说明的环境字符串为login创建一个环境(envp参数)。
login能处理多项工作。因为它得到了用户名,所以能调用getpwnam取得相应用户的口令文件登陆项。然后调用getpass以显示“Password:”,接着读用户键入的口令。它调用crypt将用户键入的口令加密,并与该用户在阴影口令文件中登陆项的pw_passwd字段相比较。
如果用户正确登陆,login就将执行如下工作:
(1)将当前工作目录更改为该用户的起始目录(chdir)。
(2)调用chown改变该终端的所有权,使登陆用户成为它的所有者。
(3)将对该终端设备的访问权限改变成用户读和写。
(4)调用setgid及initgroups设置进程的组ID。
(5)用login所得到的所有信息初始化环境:起始目录(HOME),shell(SHELL)、用户名(USER和LOGNAME),以及一个系统默认路径(PATH).
(6)login进程改变为登陆用户的用户ID(setuid)并调用该用户的登陆shell。
至此,登录用户的登陆shell开始执行。登陆shell的文件描述符0、1和2设置为终端设备。
网络登陆时,在终端和计算机之间的连接不再是点到点的。在网络登陆情况下,login仅仅是一种可用的服务,这与其他网络服务(如FTP或SMTP)的性质相同。所有网络登陆都经由内核的网络接口驱动程序,因此必须等待一个网络连接请求的到达,而不是使一个进程等待每一个可能的登陆。
在BSD中,有一个inetd进程(有时称为因特网超级服务器),它等待大多数网络连接。作为系统启动的一部分,init调用一个shell,使其执行shell脚本/etc/rc。由此shell脚本启动一个守护进程inetd。inetd等待TCP/IP连接请求到达主机,而当一个连接请求到达时,它执行一次fork,然后生成的子进程exec适当的程序。
telnetd进程打开一个伪终端设备,并用fork分成两个进程。父进程处理通过网络连接的通信,子进程则执行login程序。
每个进程除了有一进程ID之外,还属于一个进程组。同一进程组中的各进程接受来自同一终端的各种信号。
函数getpgrp返回调用进程的进程组ID:
#include
pid_t getpgrp(void);
返回调用进程的进程组ID
pid_t getpgid(pid_t pid);//若pid是0,返回调用进程的进程组ID
若成功,返回进程组ID,若出错,然会-1
进程调用setpgid可以加入一个现有的进程组或者创建一个新进程组:
#include
int setpgid(pid_t pid,pid_t pgid);//将pid进程的进程组ID设置为pgid
若成功,返回0;若出错,返回-1
注:一个进程智能为它自己或它的子进程设置进程组ID。在它的子进程调用了exec后,它就不再更改该子进程的进程组ID。
会话(session)是一个或多个进程组的集合。
通常是由shell的管道将几个进程编称一组的。例如,图9-6中的安排可能是由下列形式的shell命令形成的:
proc1 | proc2 &
proc3 | proc4 | proc5
进程调用setsid函数建立一个新会话:
#include
pid_t setsid(void);
若成功,返回进程组ID;若出错,返回-1
如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话:
(1)该进程变成新会话的会话首进程(会话首进程是创建该会话的进程)。此时,该进程是新会话中的唯一进程。
(2)该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID。
(3)该进程没有控制终端。
如果该调用进程已经是一个进程组的组长,则此函数返回出错。
#include
pid_t getsid(pid_t pid);
若成功,返回会话首进程的进程组ID;若出错,返回-1
若pid是0,getsid返回调用进程的会话首进程的进程组ID。
需要由一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能知道将终端输入和终端产生的信号发送到何处。
#include
pid_t tcgetpgrp(int fd);
若成功,返回前台进程组ID;若出错,返回-1
int tcsetpgrp(int fd,pid_t pgrpid);//将前台进程组ID设置为pgrpid
若成功,返回0;若出错,返回-1
#include
pid_t tcgetsid(int fd);//获得会话首进程的进程组ID
若成功,返回会话首进程的进程组ID;若出错,返回-1
首先使用不支持作业控制的、在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都处于同一后台进程组。
一个其父进程已终止的进程称为孤儿进程,这种进程由init进程“收养”。
一个进程组不是孤儿进程组的条件是,该进程组中有一个进程,其父进程属于同一会话的另一个组中。如果进程组不是孤儿进程组,那么在属于同一会话的另一个组中的父进程就有机会重新启动该组中停止的进程。在这里,进程组中每一个进程的父进程都属于另一个会话。所以此进程组是孤儿进程组。