3、使用网络登陆终端
使用网络和串口终端登陆系统的主要区别是:电脑和终端之间连接的方式不是点到点的。在这种情况下,login只是一个可以使用的服务,就像类似ftp和smtp这样的网络服务一样。
有了我们之前叙述过的终端登陆,init知道哪些终端设备对于login来说是可以使用的,然后它会启动一个为每一个设备启动一个getty进程。然而,在网络登陆的情况下,所有的login来自内核的网络接口驱动(例如以太网卡驱动),我们无法提前知道这些发生的时间。我们需要使用等待网络连接请求的方式来替代用每一个进程等待每一个可能的请求login的方式。
为了使用同样的软件来处理网络连接和终端连接,使用一个被称作pseudo terminal(伪终端)来模拟串口终端并把终端操作映射成为网络操作,反之亦然。(关于pseudo terminal我们后面会详细介绍)
(1)BSD下面的网络登陆
在BSD下面,有一个单一的进程用来等待大多数的网络连接,这个进程是inetd进程,有时候也被称作"internet super server".这里我们将要看看在BSD系统中网络方式登陆时启动进程的顺序和过程。
在系统启动的时候,init会启动一个shell来运行/etc/rc脚本,通过这个脚本启动的守护进程中,其中有一个就是inetd进程。当shell脚本结束的时候,inetd的父进程变成了init; inetd会等待到达主机的TCP/IP连接请求。当一个连接请求到达的时候,inetd会fork并exec一个合适的程序。
假设tcp请求来自telnet的server(telnet是一个使用tcp协议的远程登陆程序),在其他主机(以某种网络连接的形式连接到本地服务主机)的用户,或者在本地服务主机发起一个telnet client:
#telnet hostname
client从"hostname"主机打开一个tcp连接,在hostname上面启动的程序叫做telnet server.client和server通过tcp连接,使用telnet协议互相传送数据。这样,启动client端的user就会登陆到server的host上面。参考资料中给出了启动telnet server(telnetd)的过程。简单叙述如下:
- 最初的init进程调用fork创建子进程,然后子进程exec执行一个sh脚本(/etc/rc)
- 通过前面的脚本,启动inetd守护进程,这个守护进程开始等待telnet客户端发起的TCP连接请求。
- 当收到一个telnet请求的时候,inetd调用fork创建子进程,子进程再调用exec执行telnetd
从这之后,telnetd进程会打开一个伪终端设备,然后调用fork得到两个进程。父进程处理来自网络连接的通信;子进程调用exec执行login程序;父进程和子进程通过伪终端联系起来。在执行exec之前,子进程设置文件描述符号0,1,2到伪终端.如果我们正确登陆了,login之后的过程就和我们之前叙述的一样了:它会切换到我们的home目录,设置gid,uid和初始环境变量,然后login调用exec把它自己替换成我们的login shell。
参考资料给出了login之后简略的过程,大致如下:
- init经过前面的过程,进入loginshell
- 这时候0,1,2文件描述符号和伪终端设备驱动联系上了。
- 设备驱动通过telnetd和telnet client之间的网络连接和用户终端交互。
这里,在设备驱动和用户终端之间有许多复杂的过程,后面会详细叙述。现在先总结一下这个整体的过程如下:
最初的init进程调用fork创建子进程,然后子进程exec执行一个sh脚本(/etc/rc)
通过前面的脚本,启动inetd守护进程,这个守护进程开始等待telnet客户端发起的TCP连接请求。
当收到一个telnet请求的时候,inetd调用fork创建子进程,子进程再调用exec执行telnetd
telnetd再fork,fork之后其中一个进程(telnetd)负责在网络上和伪终端之间收发数据;另外一个进程调用exec执行login,login将切换到我们的home目录,设置gid,uid和初始环境变量,然后login调用exec把它自己替换成我们的login shell,然后通过虚拟终端和用户交互。
-
telnetd fork的两个进程(这里姑且叫做telnetd,login shell)之间是通过如下方式进行交互:
...network messages....<->telnetd<->pseudo terminal device <->login shell<->......
这里比较重要的一点是,我们登陆的时候是通过terminal(终端),还是network;我们的login shell的标准输入标准输出和标准错误输出是连接到终端设备或者虚拟终端设备.后面我们将会知道,login shell是posix.1的部分,终端或者伪终端是session的control terminal.
(2)MacOs X 的网络登陆
由于是基于Free BSD的,所以过程一样。
(3)Linux上的网络登陆
Linux上面的网络登陆和BSD上面一样,不同的地方是使用了不同的inetd进程,叫做xinetd.xinetd进程比inetd的控制层次好。
(4)Solaris上的网络登陆
目前本人还没用到,不说了,具体参考参考资料。
译者注
原文参考
4、进程组ID
除了进程ID,进程还有组ID.进程组是一个或者多个进程的集合,通常和相同的job相互关联,job从同样的终端接收信号。每个进程有唯一的进程组id,进程组id类似进程id是一个正数可以存放在pid_t中。
函数getpgrp返回调用进程的进程组id。如下:
#include <unistd.h>
pid_t getpgrp(void);
原来BSD体系的系统getpgrd函数有一个pid参数,会返回对应那个pid进程id的进程所在的组。Single UNIX Specification定义了getpgid函数作为extension,用来模拟这个函数。
#include <unistd.h>
pid_t getpgid(pid_t pid);
如果传入的参数是0,那么返回调用进程的组id.(这和没有参数的getpgid调用一样)
进程组leader可以创建一个进程组,可以在组内创建一个进程,然后终止退出。只要进程组内有进程,无论进程组leader是否存在,进程组也都会存在。组内最后一个进程可以终止,也可以加入一个其它的进程组。
通过调用如下函数创建或者加入一个已经存在的进程组:
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
函数正确运行的时候,返回0;错误的时候返回1(其值一般为-1)。
函数会将进程id等于pid的进程的进程组id设置成pgid.如果两个参数相等,那么进程pid就变成了一个进程组leader.如果pid等于0,那么进程id假设就是调用该函数进程的进程id.当然,如果pgid为0,那么就使用pid进程的进程组id。
一个进程只能设置它自己或者它的子进程的进程组id。另外,如果它的子进程调用了exec,那么这个进程就不能够改变它这个子进程的进程组id了。
在大多数的作业控制shell中,这个函数在执行fork之后会被调用,一般都是在父进程中设置一下子进程的进程组id,然后再在子进程中设置一下它自己的进程组id;这里实际上有一个调用是冗余的,但是通过这样做(也就是在父子进程中都调用setpgid)我们可以保证子进程被放在了它自己的进程组中(在父子两进程做这样的假设之前),如果我们没有这样做,那么我们可能会碰到一些竞争条件,这个竞争条件会导致子进程的进程组成员关系取决于哪个进程被首先执行。
当我们后面说到信号的时候,我们将会看到我们是如何发送一个信号到某个单个的进程(通过进程pid进行标识),或者某个进程组(通过进程组id进行标识)的。类似地,在前面的waitpid函数中我们已经看到,我们可以等待某个特定的进程或者进程组。