终端登录
对于终端设备登陆,系统管理员首先创建一个配置文件,每一个终端设备在配置文件中占一行。当系统自举时,内核创建进程ID为1的进程,即init
进程。init
进程使系统进入多用户模式,它读取终端设备的配置文件,对灭一个允许登录的终端设备,它调用一次fork
,然后用生成的子进程调用exec
,执行getty
程序。而getty
则对终端设备调用open
函数,以读写方式打开终端。如果设备时调制解调器,则open
会在驱动中滞留,知道用户拨号调制解调器,并且线路被接通。一旦设备被打开,文件描述符0,1,2就被舍知道该设备,然后getty
输入login:
之类的信息,等到用户输入用户名。
一旦用户输入了用户名,那么getty
的工作就完成了,然后它调用login
程序,提示用户输入密码,如果用户输入正确,那么:
- 将当前工作目录改为用户的起始目录。
- 调用
chown
更改终端的所有权,是登录用户称为它的所有者 - 更改终端设备的访问权限为当前用户可读写
- 调用
setgid
和initgroups
设置进程的组ID - 用
login
得到的所有信息初始化环境:HOME、shell、USERNAME、LOGNAME以及一个系统默认路径(PATH)。 login
进程更改为登录用户的用户ID(setuid
)并调用该用户的登录shell
。
执行流程示意如下:
注意一点,init
是以特权模式运行的,login
因为也在特权模式下运行,setuid
会改变进程的3个用户ID:实际用户ID、有效用户ID和保存的设置用户ID。
当用户登出时,shell
进程被终止,于是init
进程得到通知,它会对终端设备重复上述过程。
网络登录
网络登录和通过串行终端登录的不同之处在于init
进程无法知道会有多少个用户通过网络登录,它也就无法通过预先配置终端文件生成进程的方式来等到用户登录。
当用户通过网络来登录时,守护进程inetd
(linux下时xinetd
)会收到请求,它fork
一个进程来处理这个连接请求。一个telnet
登录示意如下:
telnetd
进程打开一个伪终端设备,并用fork
分成两个进程,父进程负责处理网络请求,子进程则执行login
。父进程和子进程通过伪终端相连接。
需要理解的重点是:我们得到一个登录shell时,其标准输入、标准输出和标准错误要么连接到一个终端设备,要么连接到一个伪终端设备。
进程组
进程组是一个或者多个进程的集合。通常,它们是在同一作业中结合起来的,同一进程组的各个进程收到来自统一终端的各种信号。每个进程组有一个唯一的进程组ID。每一个进程组有一个组长进程,组长进程的进程ID即进程组ID。需要注意的是,如果组长进程退出了,进程组仍然存在,而且进程组ID不变,只有当进程组内所有的进程都退出了,那么进程组的生命周期才算结束。
相关接口:
1 | #include <unistd.h> |
会话
会话(session)是一个或者多个进程组的集合。通常是由shell的管道将几个进程变成一组。
1 | #include <unistd.h> |
setsid
函数建立一个新的会话。函数调用成功的前提是调用进程不是一个进程组的组长进程。如果调用成功:
- 进程变成新会话进程的首进程(
session leader
) - 该进程称为一个新的进程组的组长进程,进程组ID为当前进程ID,会话ID也为当前进程ID。
- 该进程没有控制终端,如果在调用
setsid
之前有控制终端,这个联系也被切断。
1 | #include <unistd.h> |
getsid
函数返回对应进程的会话首进程的进程组ID,如果pid == 0
那么返回当前进程的对应值。出于安全考虑,如果pid
不属于调用者所在的会话,那么调用失败。
控制终端
会话和进程组还有一些其他特性:
- 一个会话可以有一个控制终端(controlling terminal),这通常是终端或者伪终端设备。
- 与控制终端建立连接的会话首进程称为控制进程。
- 一个会话的进程组可以被分割为一个前台进程组(foreground process group)和若干个后台进程组(background process group)。
- 如果一个会话有一个控制终端,则它有一个前台进程组
- 中断键(Ctrl-C)、退出键(Ctrl-\)都被发送至前台进程组的所有进程。
- 如果终端断开连接,则挂断信息发送至控制进程。
特性示意:
有时候后台进程组也需要和终端交互,这时候后台进程组可以打开终端设备。当然用户也可以配置终端以防止后台进程和终端进行交互。
1 | #include <unistd.h> |
孤儿进程组
POSIX.1将孤儿进程组定义为:该组的成员的父进程要么是该组的一个成员,要么不是改组所属会话的成员。POSIX.1要求向新的孤儿进程组中处于停滞状态的每一个成员发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。
一个孤儿进程的例子如下:
1 | #include <stdio.h> |
子进程设置SIGHUP信号的处理函数后将自己停止,当父进程退出后,子进程变成一个新的孤儿进程组的成员,于是系统向这个进程组发送SIGHUP信号,子进程的处理函数被触发,之后子进程又收到SIGCONT信号,程序继续执行。
输出:
1 | ➜ ~ ./a.out parent: pid = 16564, ppid = 4501, pgrp = 16564, tpgrp = 16564 child: pid = 16565, ppid = 16564, pgrp = 16564, tpgrp = 16564 SIGHUP received, pid=16565 child: pid = 16565, ppid = 1, pgrp = 16564, tpgrp = 4501 read error 5 on controlling TTY |
由于子进程是孤儿进程组,它如果尝试向终端读取数据,就会出错。