Zach的博客

进程关系

终端登录

对于终端设备登陆,系统管理员首先创建一个配置文件,每一个终端设备在配置文件中占一行。当系统自举时,内核创建进程ID为1的进程,即init进程。init进程使系统进入多用户模式,它读取终端设备的配置文件,对灭一个允许登录的终端设备,它调用一次fork,然后用生成的子进程调用exec,执行getty程序。而getty则对终端设备调用open函数,以读写方式打开终端。如果设备时调制解调器,则open会在驱动中滞留,知道用户拨号调制解调器,并且线路被接通。一旦设备被打开,文件描述符0,1,2就被舍知道该设备,然后getty输入login:之类的信息,等到用户输入用户名。

一旦用户输入了用户名,那么getty的工作就完成了,然后它调用login程序,提示用户输入密码,如果用户输入正确,那么:

  • 将当前工作目录改为用户的起始目录。
  • 调用chown更改终端的所有权,是登录用户称为它的所有者
  • 更改终端设备的访问权限为当前用户可读写
  • 调用setgidinitgroups设置进程的组ID
  • login得到的所有信息初始化环境:HOME、shell、USERNAME、LOGNAME以及一个系统默认路径(PATH)。
  • login进程更改为登录用户的用户ID(setuid)并调用该用户的登录shell

执行流程示意如下:

login_shell.png

注意一点,init是以特权模式运行的,login因为也在特权模式下运行,setuid会改变进程的3个用户ID:实际用户ID、有效用户ID和保存的设置用户ID。

当用户登出时,shell进程被终止,于是init进程得到通知,它会对终端设备重复上述过程。

网络登录

网络登录和通过串行终端登录的不同之处在于init进程无法知道会有多少个用户通过网络登录,它也就无法通过预先配置终端文件生成进程的方式来等到用户登录。

当用户通过网络来登录时,守护进程inetd(linux下时xinetd)会收到请求,它fork一个进程来处理这个连接请求。一个telnet登录示意如下:

telnet_login.png

telnetd进程打开一个伪终端设备,并用fork分成两个进程,父进程负责处理网络请求,子进程则执行login。父进程和子进程通过伪终端相连接。

需要理解的重点是:我们得到一个登录shell时,其标准输入、标准输出和标准错误要么连接到一个终端设备,要么连接到一个伪终端设备。

进程组

进程组是一个或者多个进程的集合。通常,它们是在同一作业中结合起来的,同一进程组的各个进程收到来自统一终端的各种信号。每个进程组有一个唯一的进程组ID。每一个进程组有一个组长进程,组长进程的进程ID即进程组ID。需要注意的是,如果组长进程退出了,进程组仍然存在,而且进程组ID不变,只有当进程组内所有的进程都退出了,那么进程组的生命周期才算结束。

相关接口:

1
2
3
4
5
6
#include <unistd.h>
pid_t getpgrp(); /* 返回当前进程组ID*/

pid_t getpgid(pid_t pid); /* 返回进程ID为pid的进程组ID,pid = 0时和`getpgrp`等效 */

int setpgid(pid_t pid, pid_t pgid); /* 加入一个现有进程组或创建一个新进程组,进程职能为它自己和其子 进程设置进程组 */

会话

会话(session)是一个或者多个进程组的集合。通常是由shell的管道将几个进程变成一组。

1
2
3
#include <unistd.h>

pid_t setsid();

setsid函数建立一个新的会话。函数调用成功的前提是调用进程不是一个进程组的组长进程。如果调用成功:

  • 进程变成新会话进程的首进程(session leader
  • 该进程称为一个新的进程组的组长进程,进程组ID为当前进程ID,会话ID也为当前进程ID。
  • 该进程没有控制终端,如果在调用setsid之前有控制终端,这个联系也被切断。
1
2
3
#include <unistd.h>

pid_t getsid(pid_t pid);

getsid函数返回对应进程的会话首进程的进程组ID,如果pid == 0那么返回当前进程的对应值。出于安全考虑,如果pid不属于调用者所在的会话,那么调用失败。

控制终端

会话和进程组还有一些其他特性:

  • 一个会话可以有一个控制终端(controlling terminal),这通常是终端或者伪终端设备。
  • 与控制终端建立连接的会话首进程称为控制进程。
  • 一个会话的进程组可以被分割为一个前台进程组(foreground process group)和若干个后台进程组(background process group)。
  • 如果一个会话有一个控制终端,则它有一个前台进程组
  • 中断键(Ctrl-C)、退出键(Ctrl-\)都被发送至前台进程组的所有进程。
  • 如果终端断开连接,则挂断信息发送至控制进程。

特性示意:

session_property.png

有时候后台进程组也需要和终端交互,这时候后台进程组可以打开终端设备。当然用户也可以配置终端以防止后台进程和终端进行交互。

1
2
3
4
5
#include <unistd.h>

pid_t tcgetpgrp(int fd); /* 返回前台进程组ID,它与fd上打开的终端相关联 */
int tcsetpgrp(int fd, pid_t pgrpid); /* 将前台进程组设置为pgrpid,fd必须饮用该会话的控制终端 */
pid_t tcgetsid(int fd); /* 得到控制控端的会话首进程的会话ID */

孤儿进程组

POSIX.1将孤儿进程组定义为:该组的成员的父进程要么是该组的一个成员,要么不是改组所属会话的成员。POSIX.1要求向新的孤儿进程组中处于停滞状态的每一个成员发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。

一个孤儿进程的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.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(int argc, char *argv[])
{

char c;
pid_t pid;

pr_ids("parent");
if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
sleep(5);
} else {
pr_ids("child");
signal(SIGHUP, sig_hup);
kill(getpid(), SIGTSTP);
pr_ids("child");
if (read(STDIN_FILENO, &c, 1) != 1) {
printf("read error %d on controlling TTY\n", errno);
}
}
return 0;
}

子进程设置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

由于子进程是孤儿进程组,它如果尝试向终端读取数据,就会出错。