Process Relationships
1. Terminal Logins
早期UNIX系统中, terminal要么是local(接线直连主机), 要么是remote(通过modem连接), 这些登录都要通过kernel内的terminal device driver.
随着bitmapped graphical terminal(位图图形终端)的出现, windowing system(窗口系统)提供了与主机交互的新方式. 应用程序可创建"terminal windows"(终端窗口)来模拟基于字符的terminal, 允许用户以熟悉的方式进行交互(如shell命令行).
有些系统允许用户登录后启动窗口系统, 其他系统会自动启动窗口系统. 我们现在讨论的是: 使用terminal登录UNIX系统, 无论使用何种terminal, 过程都是相似的:
- character-based terminal
- 模仿character-based terminal的graphical terminal
- 运行windowing system的graphical terminal
2. BSD Terminal Logins
/etc/ttys
文件中每一行表示一个terminal device(一种特殊device file), 每一行包含terminal device的名字, 执行的命令(通常为getty
), 和一些输入参数. 启动UNIX系统时, kernel会创建一个init
进程(PID 1), 该进程会读取/etc/ttys
并尝试启动每一个terminal device.
上述所有进程的real user ID和effective user ID均为0(拥有superuser权限). 除了init
进程, 其他进程的PPID均为1.
init
进程调用fork()
生成子进程, 子进程调用exec
执行getty
程序getty
调用open()
打开terminal device, 并将file descriptor 0, 1, 2设置为该terminal device.getty
的输出与login
类似: 等待用户输出用户名- 当用户输入用户名后,
getty
的任务结束, 并调用login
程序, 等同于:execle("/bin/login", "login", "-p", username, (char *)0, envp);
- 虽然
init
调用getty
时没有任何环境, 但getty
会为login
创造一个环境(envp
), 其中包括terminal的名字, 一些gettytab
中的环境字符串.-p
表示login
需保留传入的环境. login
执行以下操作:- 调用
getpwnam()
获取password file entry - 调用
getpass()
获取用户输入的password - 调用
crypt()
对password加密, 并与password file entry中的pw_passwd
对比 - 若多次登录失败(密码错误), 则调用
exit(1)
.init
会收到进程结束的通知, 并继续调用fork()
,exec()
和getty
程序.
- 调用
若成功登陆后, login
会执行以下操作:
- 调用
chdir()
切换到用户的home directory - 调用
chown()
将terminal device的所有权交给用户 - 将terminal device的读写权限交给login user
- 调用
setgid()
和initgroups()
设置group ID - 初始化environment, 如下:
- home directory(
HOME
) - shell(
SHELL
) - user name(
USER
和LOGNAME
) - 默认路径(
PATH
)
- home directory(
- 调用
setuid()
将进程的user ID改为当前用户, 并调用shellexecl("/bin/sh", "-sh", (char *)0);
由于login
所在的进程拥有superuser权限, 因此setuid()
可修改所有user ID(real user ID, effective user ID, saved set-user-ID). setgid()
也是相同的效果. 此时login
shell仍在运行, 其parent process为init
, 因此当login
结束时, init
会收到SIGCHLD
signal, 并重复执行上述流程.
最后login
shell会读取start-up file(Bourne shell中的.profile
; GNU Bourne-again shell中的.bash_profile
, .bash_login
, 或.profile
; C shell中的.cshrc
和.login
). 这些start-up file会修改或添加environment variables, 例如, 大部分用户会设置自己的PATH
.
3. Network Logins
serial terminal login与network login的主要区别: terminal与主机之间的connection是否为点对点.
对于terminal login, init
知道哪些terminal device可以登录, 并为每个terminal device生成各自getty
进程; 但对于network login, 所有登录都通过kernel的network interface driver, 且无法得知何时发生. 网络中任何主机都可能登录当前主机, 因此与其为网络上每个主机创建getty
进程, 不如等待网络请求.
为了给terminal login和network login一个统一入口, UNIX引入pseudo terminal(伪终端), 该driver用户模拟serial terminal的行为, 并将terminal操作映射为网络操作, 反之亦然.
3.1 BSD Network Logins
BSD中存在一个inetd
进程, 也称为internet superserver, 其会等待绝大多数网络连接. 作为系统启动的一部分, init
调用一个shell执行/etc/rc
shell script, 该script会启动inted
和其他daemon. 一旦该script中止运行, inetd
的parent process变为init
; inetd
等待TCP/IP连接请求, 当连接达到主机时, inetd
执行fork()
, 并对合适的程序执行exec
.
假设远程用户启动TELNET client开始登录, 向TELNET server发送TCP连接请求:
telnet hostname |
client向hostname开启一个TCP连接, 下图展示了TELNET server中的进程执行:
telnetd
开启一个pseudo terminal device, 并调用fork()
拆分为两个进程:
- parent process(
telnetd
): 处理网络连接中的通信请求 - child process: 调用
exec
执行login
程序. 调用exec
前会将stdin, stdout, stderr指向该pseudo terminal device.
无论通过terminal还是network登录, 都会创建一个login
shell, 其stdin, stdout, stderr指向terminal device或pseudo terminal device.
4. Process Groups
每个进程都有一个process ID, 且属于一个process group.
- 一个process group包含一个或多个进程, process group内的进程通常拥有相同任务.
- 当process group收到signal时, process group内的所有process都会收到signal.
- 每个process group拥有一个唯一process group ID. Process group ID与process ID类似: 都为正整数, 存放在
pid_t
数据类型中. - 每个process group可有零个或一个process group leader, 该进程的process ID与process group ID相同.
- 若process group中没有进程, 该group的生存周期结束.
/** |
setpgid()
遵循以下规则:
- 进程只能设置自身和其child process的process group ID
- 当child process调用
exec
后, 进程无法修改其process group ID
我们可向一个process, 或一个process group发送signal. 相同的, waitpid
函数允许我们等待某个process, 或某个process group中的一个process.
5. Sessions
Session包含一个或多个process group, session内的所有process group使用同一个terminal device. Session一般用于job control: session中某个process group作为foreground process group, 用于接收特殊字符产生的signal, 进程可设置当前session的foreground process group. 以下是terminal的访问控制:
- foreground process group中的进程允许读取terminal device; background process group中的进程尝试读取terminal device时会收到
SIGTTIN
signal. - foreground process group中的进程允许写入terminal device; background process group中的进程尝试写入terminal device时会收到
SIGTTOU
signal.
当session中没有与foreground process group ID匹配的process ID或process group ID, 则terminal没有foreground process group.
以下就是一个session和三个process group:
/** |
由于没有session ID的概念, 所以session由session leader决定. Session leader的PID也相当于session ID.
6. Controlling Terminal
Session和process group有以下几个特点:
- 一个session只有一个controlling terminal(通常为登录时的terminal device或pseudo terminal device)
- 与controlling terminal建立连接的session leader称为controlling process
- 一个session内的process group分为一个foreground process group和多个background process group
- 当用户按下terminal的interrupt key(通常为Control-C)或quit key(通常为Control-backslash)时, foreground process group中的所有进程都会收到interrupt signal
- 若controlling terminal检测到modem或network中断, controlling process会收到hang-up signal
7. tcgetpgrp, tcsetpgrp and tcgetsid Functions
进程可通过以下函数指定哪个process group为foreground process group, 这样terminal device driver就知道该向谁发送terminal input和terminal-generated signal.
/** |
若background process group内的进程调用tcsetpgrp()
, 且该进程没有阻塞或忽略SIGTTOU
signal, 该process group内的所有进程都会收到SIGTTOU
signal.
8. Job Control
Job control允许我们在一个terminal内启动多个job(多个process group), 其中一个job可以访问terminal, 其他job在后台运行. Job control的实现需要以下几点:
- shell支持job control
- kernel中的terminal driver支持job control
- kernel支持job-control signals
在shell中使用job control时, job要么是foreground job(等同于foreground process group), 要么是background job(等同于background process group). 一个job就是一组进程, 通常使用pipe(管道)串联进程. 我们可以在前台启动一个job, 其中包含一个进程:
vi main.c |
也可以在后台启动两个job:
pr *.c | lpr & |
当我们启动一个background job时, shell会为其分配一个job identifier, 并输出所有process ID:
$ make all > Make.out & |
- 第一行的
make
的job编号为1, PID为1475. 第三行的job编号为2, PID为1490 - 当job结束且我们按下
RETURN
时, shell会告诉我们job已完成. 由于shell无法告知background job的状态改变, 因此需要我们输入RETURN
. - terminal driver可接收三种特殊字符并将其转换为特定signal:
- interrupt character(Control-C):
SIGINT
- quit character(Control-Backslash):
SIGQUIT
- suspend character(Control-Z):
SIGTSTP
- interrupt character(Control-C):
可以存在一个或多个foreground job, 但只有foreground job可以接收terminal input(我们在terminal输入的字符). Background job可以去获取terminal input, 但terminal driver会向该background job发送SIGTTIN
signal, 该signal通常会停止background job.
cat > temp.foo & # background job, but will try to read from terminal |
- 当后台的
cat
尝试从terminal读取字符时, terminal driver会向其发送SIGTTIN
signal - shell检测到child process的状态变化(通过
wait()
和waitpid()
), 并通知用户该job已经停止 fg
命令将已停止的job改为foreground job(tcsetpgrp()
), 并向该job发送SIGCONT
signal(让job继续运行)- 由于job已成为foreground job, 因此可以从terminal读取用户输入
stty
可让background job无法向terminal输出数据:
cat temp.foo & # start background job |
禁止background job输出到terminal后, 当cat
尝试向terminal输出字符时, terminal driver会阻止background job的输出, 并向该job发送SIGTTOU
signal. 使用fg
命令后, job移至前台并顺利输出. 下图为job control的流程:
9. Shell Execution of Prgrams
本节将讨论shell如何执行程序, 以及和process group, terminal, session的关系.
9.1 The shell without job control: the Bourne shell on Solaris¶
$ ps -o pid,ppid,pgid,sid,comm |
ps
命令的parent process为shell- 由于shell不支持job control, 因此shell和
ps
程序处于同一session和同一foreground process group
$ ps -o pid,ppid,pgid,sid,comm & |
后台执行上述命令时, 唯一变动就是PID. 由于shell不支持job control, 因此不会为background job创建一个新的process group, background job也依然拥有terminal.
$ ps -o pid,ppid,pgid,sid,comm | cat1 |
上述为shell处理pipeline的情况: cat1
与cat
程序相同, 只是名字不同. cat1
为shell的child process, ps
为cat1
的child process. 看起来shell调用fork()
生成自身副本, 该副本又调用fork()
处理pipeline前面的部分.
$ ps -o pid,ppid,pgid,sid,comm | cat1 & |
后台执行上述命令时, 唯一变动就是PID.由于shell不支持job control, background process的process group ID仍为949, session的process group ID也不变.
若background process尝试从terminal读取输入, 如cat > temp.foo &
, 由于shell不支持job control, 若background process没有重定向自己的stdin, shell会自动将其stdin重定向到/dev/null
. 读取/dev/null
会返回一个end-of-file, 因此background process会读取到end-of-file并立即中止.
9.2 The shell with job control: Bourne-again shell on Linux
$ ps -o pid,ppid,pgid,sid,tpgid,comm |
ps
命令拥有自己的process group.ps
命令是所在process group的leader. 该process group为foreground process group, 因为其拥有controlling terminal.ps
命令执行期间, login shell为background process group.- 两个process group属于同一session.
$ ps -o pid,ppid,pgid,sid,tpgid,comm & |
后台执行上述命令时:
ps
命令依然拥有自己的process group- process group(5797)为background process group
- login shell为foreground process group
$ ps -o pid,ppid,pgid,sid,tpgid,comm | cat1 |
pipeline中执行两个进程:
ps
和cat1
属于一个新的process group, 作为foreground process group- login shell为其他进程的parent process. 但与Bourne shell不同的是, 其先创建
ps
, 后创建cat1
ps -o pid,ppid,pgid,sid,tpgid,comm | cat1 & |
后台执行上述命令: 结果相同, 但ps
和cat1
所在的process group为background process group.
10. Orphaned Process Groups
若child process仍在运行, 但其parent process中止, 则该child process称为orphaned, 其parent process改为init(PID 1).
|
以下是运行结果:
$ ./a.out |
- 当前程序成为foreground process group, parent process和child process处于同一process group(6099), shell的process group为2837
- 调用
fork()
后, parent process调用sleep()
, 保证child process先执行 - child process建立
SIGHUP
的signal handler - child process调用
kill()
, 向自身发送SIGSTP
. 该signal会暂停child process, 等同于用户在terminal输入suspend character(Control-Z) - 当parent process中止时, child process成为orphaned, 因此其paraent process ID为1, 也就是
init
进程 - 此时, child process成为orphaned process group的一员:
- 当controlling process中止时, 其他session就可以使用其terminal. 为防止之前session中其他进程使用terminal, 该session中的所有process group都被标记为orphaned process group
- 当process group成为orphan, 所有进程都会收到
SIGHUP
. 通常来说, 该signal会让进程终止, 但进程可选择忽略该signal, 或为该signal建立handler, 进程仍可在orphan process group中运行.
- shell收到两条输出, 来自login shell和child process. child process的PPID为1
- child process调用
read()
尝试从terminal读取输入.read()
返回error, 并将errno设置为EIO
.- 当background process group中的进程尝试读取terminal输入时, background process group会收到
SIGTTIN
; 但对于orphaned process group, 若kernel使用该signal停止进程, 则进程可能永远无法继续运行.
- 当background process group中的进程尝试读取terminal输入时, background process group会收到
- 由于parent process以foreground job执行, 因此parent process中止后, child process会被放入background process group中.
11. FreeBSD Implementation
session
: 每个session都有该一个structure
- s_count: session内的process group数量. 若该field为0, 则释放
session
- s_leader: 指向session leader的
proc
structure - s_ttyvp: 指向controlling terminal的
vnode
structure - s_ttyp: 指向controlling terminal的
tty
structure - s_sid: session ID(PID of session leader process)
调用setsid()
时, 会在kernel中分配一个session
. s_count
为1, s_leader
指向当前进程的proc
, s_sid
为当前进程的PID, 由于新建session没有controlling terminal, 因此s_ttyvp
和s_ttyp
为NULL.
tty
: 每个terminal device和pseudo terminal device都有该structure
- t_session: 指向controlling terminal的
session
structure - t_pgrp: 指向foreground process group的
pgrp
structure. Terminal driver使用该field向foreground process group发送signal(interrupt, quit, suspend). - t_termios: 包含special character和terminal相关信息的structure
- t_winsize: 包含当前terminal大小的
winsize
structure
pgrp
: 包含一个process group相关信息的structure
- pg_id: process group ID
- pg_session: 指向当前process group所属的session的
session
structure - pg_members: 指向一个
proc
structure列表, 包含当前process group的所有进程