Process Control
1. Process Indetifiers
每个进程都有一个唯一, 非负的PID(process ID, 进程ID). PID支持重用, 进程一旦被停用, 其PID会被其他新建进程重用. 但大多数UNIX系统会推迟该操作, 以保证新进程不会被误认为上一个进程. 以下是一些特殊进程, 具体细节因系统实现不同而不同:
- PID 0: scheduler process(调度进程), 用于进程调度, system process之一.
- PID 1: init process, bootstrap结束时由kernel调用, 程序位于
/etc/init
或/sbin/init
:- 负责初始化UNIX系统
- 该进程不会退出
- 该进程不是system process, 而是普通进程
- 该进程拥有superuser权限
/** |
2. fork Function
/** |
Child process的fork()
返回值为0, 而不是PPID(parent process's PID), 因为child process可通过getppid()
获得PPID. 一个进程可拥有多个child process, 因此parent process的fork()
返回值为child process的PID.
有些UNIX系统会提供fork()
的变种, 如Linux的clone()
.
2.1 Memory Sharing
Child process会复制parent process的大部分内存数据, 包括data segment, heap, stack, 因此两者不共享数据, 除了text segment(read-only).
由于复制数据会降低运行速度, 且fork-exec model(fork()
生成的child process执行exec()
以运行其他程序)被广泛使用, 因此child process并没有必要复制parent process的数据, copy-on-write(COW)应运而生: child process一开始不会复制parent process的数据, 而是共享同一区域内存. 只有当其中一方(child process或parent process)试图写入数据时才会复制被修改的内存区域.
2.2 File Sharing
fork()
会对parent process的每个file descriptor调用dup()
, 因此child process与parent process共享file descriptor. 由于parent process与child process指向相同的table entry, 因此child process可以在parent process所在文件的相同位置继续读写.
若parent process和child process在没有同步机制的情况下对同一file descriptor进行操作, 很可能导致数据丢失或逻辑错误. 有以下两种方法保证同步:
- parent process等待child process的操作完毕后再执行操作.
- parent process和child process各自关闭不需要的file descriptor, 避免两个进程操作同一file descriptor
2.3 Other Sharing
以下是parent process和child process的相同之处:
- real user ID, real group ID, effective user ID, 和effective group ID
- supplementary group IDs
- process group ID
- session ID
- controlling terminal
- set-user-ID和set-group-ID bits
- current working directory
- root directory
- file mode creation mask
- signal mask和dispositions
- 所有打开的file descriptor的close-on-exec flag
- environment variables
- attached shared memory segments
- memory mappings
- resource limits
以下是parent process与child process的不同之处:
- child process拥有自己的PID
- child process的PPID与parent process的PID相同, 因此, child process的PPID一定与parent process的PPID不同(init的PPID为0)
- child process的进程资源利用率(
getrusage()
)和CPU使用时长(times()
)重置为0 - child process的等待信号队列重置为空
- child process不会继承parent process的memory lock(
mlock()
) - child process不会继承parent process对semaphore的修改(
semop()
) - child process不会继承parent process的record lock(
fcntl()
) - child process不会继承parent process的timer(
setitimer()
,alarm()
,timer_create()
) - child process不会继承parent process未完成的异步I/O操作(
aio_read()
,aio_write(3)
), 也不会继承异步I/O上下文(io_steup()
)
以下是fork()
执行失败的主要原因:
- 系统内的进程数量达到上限: 进程数达到
RLIMIT_NPROC
资源上限, 或达到PID数量上限 - 该real user ID的child process数量达到上限
3. vfork Function
/** |
vfork()
与fork()
存在两点不同:
vfork()
生成的child process不复制parent process的地址空间, 因此共享parent process的所有数据. 只有调用exec()
或exit()
才会退出parent process的地址空间.fork()
不保证child process和parent process的执行顺序, 而vfork()
要求child process必须在parent process之前运行, 需要阻塞parent process, 直到child process调用exec()
或exit()
退出进程.
4. exit Functions
以下是五种正常中止进程的方式:
main()
中调用return()
, 相当于调用exit()
- 调用
exit()
. 会调用所有atexit()
注册的exit handler, 并关闭所有I/O streams. 由于exit()
由ISO C定义, 所以不处理file descritptor, 多进程, 和任务控制. - 调用
_exit()
或_Exit()
. ISO C定义_Exit()
中止进程且不调用exit handler._exit()
由POSIX.1定义, 因此会关闭file descriptor, 并将所有child process的parent process改为init. - 进程的最后一个线程的start routine调用
return()
, 该线程的返回值不会作为进程的返回值, 而是以0提出. - 进程的最后一个线程调用
pthread_exit()
.
以下是三种异常中止进程的方式:
- 调用
abort()
, 不会删除临时文件, 不关闭stream buffer, 也不会调用atexit()
注册的exit handler - 进程收到特定signal
- 进程调用
abort()
- 其他进程向当前进程发送signal
- kernel发送signal, 如发生错误
- 进程调用
- 进程的最后一个线程回应cancellation request
无论进程如何中止, kernel都会关闭所有open file descriptor, 并释放内存. 进程被中止时, 其parent process会收到exit status. 若进程调用exit()
, _exit()
, 或_Exit()
中止进程, 则exit status为其参数; 若进程异常中止, kernel会生成一个termination status, 其parent process可通过wait()
或waitpid()
获取termination status.
若进程中止时, 其child process仍未退出, 则child process的PPID改为1(init), 这样可保证任何进程在任何时刻都有一个PPID.
若child process中止时, 其parent process仍未退出, kernel会将中止的进程信息保留在内存中, 以便后续parent process调用wait()
或waitpid()
获取termination status. 保存的进程信息包括: 被中止进程的PID, CPU运行时间, termination status. 这些已被中止但未被parent process知晓的进程称为zombie process.
若进程的parent process为init, 则该进程永远不会成为zombie process, 因为无论进程何时中止, init都会调用wait()
获取其termination status. init的child process有两种生成方式:
- init直接生成的进程(
getty
) - 进程的parent process中止, 其parent process转为init
5. wait and waitpid Functions
无论进程如何中止, kernel都会向其parent process发送SIGCHLD
信号. 该信号为异步提醒, parent process可选择忽略该信号, 或提供一个signal handler处理该信号, 且信号默认处理会被忽略. wait()
和``waitpid()`可提供以下功能:
- 若所有child process都在运行, 则阻塞当前进程
- 若某个child process被中止, 则立刻返回该child process的termination status
- 若没有任何child process, 则立即返回错误
/** |
waitpid()
与wait()
的区别:
waitpid()
可等待指定PID的child process,wait()
只能等待当前进程的child processwaitpid()
提供nonblocking模式, 无需阻塞当前进程waitpid()
提供WUNTRACED和WCONTINUED来支持job control
6. waitid Function
Single UNIX Specification提供了额外的函数来获取exit status.
/** |
7. wait3 and wait4 Function
/** |
wait3(status, options, rusage);
等同于waitpid(-1, status, options)
, 但会返回额外的资源使用信息, 其中包括user CPU运行时间, system CPU运行时间, page fault的数量, 接收到的signal数量等.
8. exec Functions
当进程调用exec
时, 进程开始执行新程序, 其PID不会改变, 但text segment, data segment, heap, stack都会被替换.
/** |
除了PID, exec后的进程还继承了原本的一些属性:
- PID和PPID
- real user ID和real group ID
- supplementary group IDs
- process group ID
- session ID
- controlling terminal
- alarm时钟剩余的时间
- current working directory
- root directory
- file mode creation mask
- file lock
- process signal mask
- pending signal
- resource limits
- nice value
- tms_utime, tms_stime, tms_cutime, 和tms_cstime
若exec()
执行的程序设置了set-user-ID bit, 则EUID(effective user ID)会改为程序文件的owner ID; 若没有设置set-user-ID bit, 则EUID不变.
UNIX系统的实现中, 只有execve()
在kernel中调用system call, 其他的exec函数为库函数, 最后通过调用execve()
来调用system call
还有三种情况需要考虑:
- 若file descriptor设置了close-on-exec flag, 则exec会关闭该file descriptor
- exec会关闭所有directory streams, 因为
opendir()
会调用fcntl()
, 为每个directory stream设置close-on-exec flag - exec会保留进程原本的real user ID和real group ID, 但若程序文件设置了set-user-ID bit或set-group-ID bit, 则effective user ID会改变; 否则effective user ID保持不变, effective group ID同理.
9. Change User IDs and Group IDs
UNIX系统中的权限和访问控制基于user ID和group ID. 当进程需要额外权限时, 需将自身的user ID或group ID改为其他合适的ID. 设计应用时应遵循least-privilege model(最少权限模型), 保证非必要用户无权限访问某些文件, 以降低安全风险.
/** |
setuid()
遵循以下规则:
- 若进程为privileged process, 则将real user ID, effective user ID, 和saved set-user-ID设置为
uid
- 若进程不为privileged process, 但
uid
与进程的real user ID, effective user ID, 或saved set-user-ID相同, 则将effective user ID设置为uid
- 若不满足上述两条规则, 则返回-1, 并设置errno
setgid()
同理
以下是user ID被改变的情况:
ID | exec | setuid(uid) | ||
set-user-ID bit off | set-user-ID bit on | superuser | unprivileged user | |
real user ID | unchanged | unchanged | set to uid | unchanged |
effective user ID | unchanged | set from user ID of program file | set to uid | set to uid |
saved set-user-ID | copied from effective user ID | copied from effective user ID | set to uid | unchanged |
9.1 setreuid, setregid Functions
/** |
setresuid()
遵循以下规则:
- 若
ruid
,euid
, 或sgid
为-1, 则不修改real user ID, effective user ID, 或saved set-user-ID. - 若进程为privileged process, 则可将real user ID, effective user ID, 和saved set-user-ID设置为任意值
- 若进程为unprivileged process, 则只能设置effective user ID, 且effective user ID必须为当前进程的real user ID, effective user ID, 或saved set-user-ID
setresgid()
与上述同理.
9.2 seteuid and setegid Functions
/** |
seteuid()
遵循以下规则:
- 若进程为privileged process, 则可将effective user ID设置为任意值
- 若进程不为privileged process, 则只能将effective user ID设置为real user ID or saved set-user-ID
setegid()
与上述同理. 相比于setuid()
, 该函数不会修改saved set-user-ID, 因此当privileged process使用setuid()
降级自身权限以运行程序, 运行后无法恢复到superuser, 这时可使用seteuid()
.
以下是所有可以更改user ID的函数:
9.3 Example
Linux 3.2.0添加了at
程序, 可在指定时间点执行一些命令. 该程序的set-user-ID为daemon
. 为防止权限泄露, 执行命令时必须在user和daemon切换, 以下是执行步骤:
- 假设
at
程序的owner为root, 且设置了set-user-ID bit:
- real user ID = our user ID (不变)
- effective user ID = root (由于开启set-user-ID bit, 因此修改EUID)
- saved set-user-ID = root
at
程序调用seteuid()
, 将EUID改为进程的real user ID:
- real user ID = our user ID (不变)
- effective user ID = our user ID
- saved set-user-ID = root (不变)
- 当
at
访问daemon的配置文件(包含用户输入的命令和执行时间)时,at
会调用seteuid
将EUID设置为root:
- real user ID = our user ID (不变)
- effective user ID = root
- saved set-user-ID = root (不变)
- 访问root的文件后,
at
会调用seteuid()
降低权限, 将EUID改回为user ID:
- real user ID = our user ID (不变)
- effective user ID = our user ID
- saved set-user-ID = root (不变)
- daemon以root权限运行, 为运行用户输入的命令, daemon调用
fork()
生成子进程, 并让子进程调用setuid()
, 由于child process也是以root权限执行, 因此会修改所有ID:
- real user ID = our user ID
- effective user ID = our user ID
- saved set-user-ID = our user ID
10. Interpreter Files
现代UNIX系统中, interpreter file(解释器文件)作为文本文件, 会以#!pathname [optional-argument]
作为第一行, 感叹号和路径名之间的空格使可选的, 最常见的开头如下:
#!/bin/sh |
- pathname通常为absolute pathname(绝对路径), 因为该路径不会执行特殊操作(不会使用
PATH
) - interpreter file的处理在kernel进行, 会作为
exec
系统调用的一部分 - kernel执行的文件不是interpreter file, 而是第一行指定的文件
相比于使用interpreter, interpreter file有以下优点:
- 隐藏pathname: 用户不必知道使用哪个pathname, 直接执行文件即可:
/* Without using interpreter file */
awk -f awkexample optional-arguments
/* Using interpreter file */
awkexample optional-arguments - interpreter file提供了一种高效获取多个options的方式:
/* Without using interpreter file */
awk ’BEGIN {
for (i = 0; i < ARGC; i++)
printf "ARGV[%d] = %s\n", i, ARGV[i]
exit
}’ $*/* Using interpreter file */
#!/usr/bin/awk -f
BEGIN {
for (i = 0; i < ARGC; i++)
printf "ARGV[%d] = %s\n", i, ARGV[i]
exit
}
- UNIX默认使用
/bin/sh
作为shell, interpreter file可指定其他shell#!/bin/csh
以下是UNIX执行interpreter file的流程:
- shell读取命令并对文件名调用
execlp
, 由于interpreter file为可执行文件, 而不是机器可执行码, 因此会返回错误. - 以interpreter file内的pathname作为shell
- shell执行文件, 但运行
awk
, shell会调用fork()
,exec()
, 和wait()
11. system Function
ISO C定义了system()
, 该函数的实现依赖于系统调用.
|
在设置了set-user-ID bit的执行程序内调用system()
可能造成安全漏洞, 因此永远不要这么做. 若以root权限执行程序, system()
执行fork()
和exec()
后, child process的EUID会变为superuser, 而不是parent process的EUID, set-group-ID同理.
12. Process Accounting
UNIX系统中可开启process accounting(进程会计), 每当进程结束时, kernel都会写入一条accounting record(会计记录), 该记录会包含一些二进制数据, 如command名字, CPU占用时间, user ID, group ID, 和开始时间. acct()
函数可用于开启或关闭process accounting:
/** |
13. Process Scheduling
UNIX只有粗粒度的进程调度, 每个进程的调度策略和优先级由kernel决定. 通过修改nice值, 可让低优先级的进程优先被执行. 越小的nice值, 进程的优先级越高, 因此, nice值也可以理解为: nice的进程不会占用太多CPU的时间, 会为其他进程腾出更多运行时间. 只有privileged process可以降低nice值, 非privileged process只能增加nice值来降低自身优先级.
/** |
The Single UNIX Specification规定nice值的范围为$[0, 2*\text{NZERO}-1]$, NZERO
由系统决定. 但没有规定fork()创造的child process如何继承nice value.
14. Process Times
任何进程都可通过调用times()
获取自身进程和其已中止子进程的以下三种时间:
- User CPU time: 进程在user space中执行指令所使用的CPU时间
- System CPU time: 进程在kernel space中执行指令所使用的CPU时间
- Wall clock: User CPU time和System CPU time之和
struct tms { |