进程和作业控制
Unix中,每个对象或者由文件表示,或者由进程表示。简单来讲,文件就是一个输出源或者输出目标。进程是一个正在运行的程序。文件提供对数据的访问,而进程使事件发生。
进程来源何处?系统如何管理自己的进程?如何控制自己的进程?
内核管理进程的方式
精确讲,进程是一个加载到内存中准备运行的程序,再加上程序所需的数据,以及跟踪管理程序状态所需要的各种信息。
所有进程由内核管理。
当进程创建时,内核赋予其一个唯一标识号,叫做进程ID或 PID 。为了跟踪管理系统中的所有进程,内核维护一个进程表(process table)。按照 PID 索引,每个进程在进程表中有一个条目。除了 PID ,每个条目还包含有描述和管理进程所需的信息。
进程共享系统的资源:处理器、内存、I/O设备、网络连接等。为了管理这样一个复杂的工作负荷,内核提供了一个复杂的调度服务,即 调度器(scheduler)。
调度器一直维护这一个所有正在等待执行的进程的列表。通过复杂的算法,调度器每次选择一个进程,给予这个进程在一个短暂的事件间隔(称为时间片)中运行的机会(多处理器系统中,调度器每次可以选择多个进程)。
时间片又称 CPU时间。通常是10毫秒(千分之十秒)的 CPU 时间。一旦时间片用尽,该进程就返回调度列表,由内核启动另一个进程。
进程分叉到死亡
进程是如何创建的呢?
内核为进程提供基本服务:
- 内存管理(虚拟内存管理,包括分页)
- 进程管理(进程创建、终止、调度)
- 进程间通信(本地、网络)
- 输入、输出(通过设备驱动程序,即与物理设备实际通信的程序)
- 文件管理
- 安全和访问控制
- 网络访问(如 TCP、IP)
当进程需要内核执行服务时,它就使用系统调用发送请求。最重要的系统调用就是那些用户进程控制和 I/O 的系统调用。
系统调用 | 目的 |
---|---|
fork | 创建当前进程的一个副本(原始进程称为 父进程(parent process),新进程和父进程一模一样,称为 子进程(child process)) |
wait | 等待另一个进程结束执行 |
exec | 在当前进程中执行一个新的程序 |
exit | 终止当前进程 |
kill | 向另一个进程发送一个信号 |
open | 打开一个用户读取或写入的文件 |
read | 从文件中读取数据 |
write | 向文件中写入数据 |
close | 关闭文件 |
显示当前 shell 的 PID:echo $$
命令有两种类型:内部命令和外部命令。内部命令直接由shell解释不创建新进程。外部命令需要shell运行一个单独的程序。
shell 使用 fork 系统调用创建一个全新的进程。
- 子进程使用 exec 系统调用将它自身从运行 shell 的进程变成运行外部程序的进程;
- 父进程使用 wait 系统调用暂停,直到子进程执行结束。
外部程序结束,子进程使用 exit 系统调用停止自身。称为进程 死亡(die) 或 终止(terminate)。故意停止一个进程,称为 杀死(kill)。
进程死亡时,进程所使用的资源(内存、文件等)都被释放,从而可以被其他进程使用。
子进程已死但仍然没有被回收称为 僵进程(zombie),尽管僵进程已经死了,进程表里仍然保存这自己的条目,这是因为该条目包含着,最近死亡的子进程的数据,而父进程可能对这些数据感兴趣。
在类UNIX系统中,僵尸进程是指完成执行(通过
exit
系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块,处于"终止状态“的进程。这发生于子进程需要保留表项以允许其父进程读取子进程的退出状态:一旦退出态通过wait
系统调用读取,僵尸进程条目就从进程表中删除,称之为"回收”(reaped)。正常情况下,进程直接被其父进程wait
并由系统回收。进程长时间保持僵尸状态一般是错误的并导致资源泄漏。
父进程一直在等待子进程的死亡,当子进程成为僵进程之后,立即被内核唤醒。
- 现在父进程有机会查看进程表中的僵进程条目,看看发生了什么结果。
- 然后内核将进程表中的僵进程条目移除。
孤儿进程和废弃进程
- 当父进程分叉后,意外死亡 ,只剩下子进程,会发生什么?
子进程继续执行,称为 孤儿 进程。孤儿进程完成工作死亡时,没有父进程被唤醒,以僵进程的形式存在。
现代操作系统,孤儿进程将自动被 #1 进程 (init进程)收养,孤儿进程死亡时,init 进程充当父进程,快速清理僵进程。
父进程创建子进程,但是没有等待进程死亡?(仅当程序有 bug ,允许程序创建子进程而不等待子进程死亡)
情况1. 父进程死亡,形成孤儿进程如上。
情况2. 子进程死亡时,子进程就称为了僵进程(没有父进程读取子进程的退出状态,导致进程表的条目无法被回收)。
如果程序以偶然的方式创建了一个僵进程,那么将没有办法清除这个进程(kill 对僵进程无效),毕竟无法杀死已经死掉的东西。
为了清除成为僵进程的废弃子进程,可以使用 kill 程序终止父进程,父进程死亡,僵进程就成为孤儿进程,从而自动被init进程收养。适当的时候,init 进程将履行继父的职责,清除僵尸进程的参与信息。
区分父进程和子进程
fork 两个相同的进程,父进程和子进程。如果两个进程相同,那么父进程怎么直到它是父进程,子进程如何知道他是子进程呢?
当 fork 系统调用结束它的工作时,它向父进程和子进程各传递一个数值,这个数值成为返回值。子进程返回值是 0,父进程的返回值是新创建的进程的的进程 ID。
第一个进程:init
如果进程是使用 fork 创建,那么每个子进程必须有一个父进程。那么在某个地方必然存在第一个进程。
在引导过程的末尾,内核 “手动” 创建一个特殊的进程,不是通过fork。这个进程的 PID 是 0。称为 空闲进程(idle process)。
在执行了一些重要的功能(例如 初始化内核所需要的数据结构)之后,空闲进程进行分叉,创建#1号进程。然后空闲进程执行一个非常简单的程序,本质是一个无穷的循环,不做任何事情(因此这个进程被命名为空闲进程)。这里的思想是,每当没有进程等待执行时,调度器就运行空闲进程。
进程#1执行设置内核以及结束引导过程所需的剩余步骤。因此称它为 初始化进程(init process),具体而言,初始化进程,打开系统控制台,挂载根文件系统,运行包含在文件/etc/inittab 中的脚本。在这一过程中,init 多次fork,创建运行系统所需的基本进程(如,运行级别设置),并允许用户登录,在这一过程中,init 成为系统中所有其他进程的祖先。
与 空闲进程(#0) 进程不同,初始化进程 (#1) 进程永远不会停止。且是进程表中的第一个进程,一直存在进程表中,直到系统关闭。
Linux系统在引导时加载Linux内核后,便由Linux内核加载init程序,由init程序完成余下的引导过程,比如加载执行级别,加载服务,启动Shell/图形化界面等等。– 维基百科
前台进程与后台进程
在命令的末尾键入一个 &
字符,可以将前台进程转换成后台进程 (也叫 异步进程(asynchronous process))。
- 前台进程: shell 在提示用户输入新命令之前等待当前程序结束,这样的进程。
- 后台进程:shell 启动一个程序,但又让该程序自己运行,这样的进程。
大多数unix程序从标准输入(stdin)读取输入,将输出写到标准输出(stdout),错误消息则写入标准错误(stderr)。stdin 相连 键盘,stdout 和 stderr 相连显示器。我们可以重定向 stdin、 stdout 和 stderr。如果用户想要在进程自己结束之前终止进程,可以按 ^C 发送 intr 信号,或者 ^\ 发送 quit 信号,区别是 quit 信号会生成一个供调试使用的磁芯转存。
异步进程 默认情况下标准输入与空文件 dev/null
相连,也不响应 intr 和 quit 信号。
当后台程序试图从标准 I/O读取并写到标准 I/O时,会发生什么?
- 后台运行的程序试图从 stdin 读取数据,但是stdin 什么都没有,进程将无限期的暂停,等待输入。(可通过
fg
命令,将其移动至前台) - 后台运行的程序向 stdout 和 stderr 写入数据时,将显示在显示器上。
作业控制
作业控制使多个进程同时运行成为可能:一个进程在前台运行,其他进程在后台运行。
作业的本质是将每条输入的命令视为一个作业,该作业由一个唯一的作业号(job number)标识。
作业控制命令 | |
---|---|
jobs | 显示作业列表 |
ps | 显示进程列表 |
fg | 将作业移动至前台 |
bg | 将作业移动至后台 |
supend | 挂起当前 shell |
^Z | 挂起当前前台作业 |
kill | 向作业发送信号,默认情况下,终止作业 |
变量 | |
---|---|
echo $$ | 显示当前 shell 的 PID |
echo $! | 显示上一条移至后台的命令的 PID |
终端设置 | |
---|---|
stty tostop | 挂起试图向终端写数据的后台作业 |
stty -tostop | 关闭 tostop |
shell 选项:Bash 、Korn shell | |
---|---|
set -o monitor | 允许作业控制 |
set +o nomonitor | 关闭 monitor |
set -o notify | 当后台作业结束时,立即通报 |
set +o nonotify | 关闭 notify |
作业和进程的区别
- 进程是正在执行或者准备执行的程序,作业指解释整个命令行所需的全部进程。
- 进程内核控制的,作业 shell 控制的。
- 内核使用进程表记录进程,shell 使用 **作业表(job table)**记录作业。
# 1个进程;1个作业
date
# 4个进程;1个作业
who | cut -c 1-8 | sort | uniq -c
date; who; uptime; cal 12 2008
前后台运行示例
作业结束通知显示时机
# 后台运行作业
ls > temp &
# 此时会输出 [作业ID] 进程ID
[1] 4003
# 如果一个作业由多个程序构成的话 显示最后一个程序的进程ID
who | cut -c 1-8 | sort |uniq -c &
# 会输出 4356 是 uniq 的进程ID
[2] 4356
# 当后台作业结束时,shell 不会立即通知您,以防止干扰您正在做的事情。
# shell 会一直等待,直到要显示下一个 shell 提示时显示,如下
[1] Done ls > temp
# 强制作业结束时,立即通知您
set -o notify
# 恢复默认设置
set +o notify
# C-Shell 家族 分别对应
set notify
unset notify
挂起作业
任何时候作业有三种状态:前台运行;后台运行;暂停,等待信号恢复。
暂停前台作业,可以按 ^Z 键(Ctrl-Z),即发送 susp 信号。我们称将进程**挂起(suspend)**或进程 停止(stop)(实际是临时中止,可以重新启动)。永久停止必须 ^C 键或 kill
命令。
恢复挂起的程序,使用 fg
命令。
# eg: 挂起 vim 然后查看 cal 说明书,然后再返回 vim
vi a.txt
^Z # 挂起vim
man cal
fg # 返回vim
挂起作业时,进程会无限期停止。如果试图注销系统,shell 会得到一条警告,你可以 fg 将挂起的作业移动到前台,并正确的退出程序,或者第二次注销系统(可能会丢失数据)。
# eg: 挂起shell
# 切换 root 用户
su -
# 做一些工作 ...
# 挂起root用户的 shell,有密码需 -f 强制挂起
suspend -f
# 做一些工作...
# 回到root的shell
fg
显示作业列表
# -l 会显示进程号
jobs [-l]
# 将作业移动至前台
fg %[job]
%[job]
# 将作业移动至后台
bg [%job...]
作业号 | 含义 |
---|---|
%% | 当前作业 |
%+ | 当前作业 |
%- | 前一个作业 |
%n | 作业#n |
%name | 含有指定命令名的作业 |
%?name | 命令中任意位置含有name的作业 |
ps: 当准备后台运行程序,但是输入命令时忘记键入 & 字符,只需按下 ^Z 挂起作业,然后使用 bg 命令将作业移动至后台。
ps 程序的使用
ps 难用的原因是20世纪80年代,unix 分支:官方 Unix(AT&T公司)、非官方(加利福尼亚大学伯克利分校)。UNIX 选项 和 BSD 选项。
unix 选项以连字符 -
开头,BSD 选项没有连字符。
# unix 选项
ps [-aefFly] [-p pid] [-u userid]
# BSD
ps [ajluvx] [p pid] [U userid]
UNIX 选项
显示哪些进程? | |
---|---|
ps | 与您用户标识和终端相关的进程 |
ps -a | 与任何用户标识和终端相关的进程 |
ps -e | 所有进程(包含守护进程) |
ps -p pid | 与指定进程 ID pid 相关的进程 |
ps -u userid | 与指定用户标识 userid 相关的进程 |
显示哪些数据列? | |
---|---|
ps | PID TTY TIME CMD |
ps -f | UID PID PPID C TTY TIME CMD |
ps -F | UID PID PPID C SZ RSS STIME TTY TIME CMD |
ps -l | F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD |
ps -ly | S UID PID PPID C PRI NI RSS SZ WCHAN TTY TIME CMD |
有用的特殊组合 | |
---|---|
ps | 显示自己的进程 |
ps -ef | 显示所有用户进程,完整给输出 |
ps -a | 显示所有非守护进程的 进程 |
ps -t - | (仅显示所有守护进程) |
数据列说明:
UNIX 标题 | 含义 |
---|---|
ADDR | 进程表中的 虚拟地址 |
C | 处理器利用率(废弃率) |
CMD | 正在执行的命令名 |
F | 与进程相关的标识 |
NI | nice 值,用于设置优先级 |
PID | 进程 ID |
PPID | 父进程 ID |
PRI | 优先级(较大的数字:优先级低) |
RSS | 内存驻留空间 |
S | 状态代码(D、R、S、T、Z) |
STIME | 累计系统时间 |
SZ | 物理页的大小(内存管理) |
TIME | 累计 CPU 时间 |
TTY | 控制终端的完整名称 |
UID | 用户标识 |
WCHAN | 等待通道 |
BSD 选项
显示哪些进程? | |
---|---|
ps | 与你的用户标识和终端相关的进程 |
ps a | 与任何用户标识和终端相关的进程 |
ps e | 所有进程(包含守护进程) |
ps p pid | 与指定进程 ID pid 相关的进程 |
ps U userid | 与指定用户标识 userid 相关的进程 |
显示哪些数据列? | |
---|---|
ps | PID TT STAT TIME COMMAND |
ps j | USER PID PPID PGID SESS JOBC STAT TT TIME COMMAND |
ps l | UID PID PPID CPU PRI NI VSZ RSS WCHAN STAT TT TIME COMMAND |
ps u | USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND |
ps v | PID STAT TIME SL RE PAGEN VSZ RSS LIM TSIZ %CPU %MEM COMMAND |
有用的特殊组合 | |
---|---|
ps | 显示自己的进程 |
ps ax | 显示所有进程 |
ps aux | 显示所有进程,完整输出 |
数据列说明:
BSD 标题 | 含义 |
---|---|
%CPU | CPU 使用百分比 |
%MEM | 真实内存使用百分比 |
CMD | 正被执行的命令的名称 |
COMMAND | 正被执行的命令的完整名称 |
CPU | 短期 CPU 使用(调度) |
JOBC | 作业控制统计 |
LIM | 内存使用限额 |
NI | nice 值,用户设置优先级 |
PAGEIN | 总的缺页错误(内存错误 ) |
PGID | 进程组号 |
PID | 进程号ID |
PPID | 父进程的进程ID |
PRI | 调度有限级 |
RE | 内存驻留时间(单位秒) |
RSS | 内存驻留空间大小(内存管理) |
SESS | 会话指针 |
SL | 睡眠时间(单位秒) |
STARTED | 定时启动 |
STAT | 状态代码(O、R、S、T、Z) |
TIME | 积累的CPU时间 |
TSIZ | 文本大小(单位KB) |
TT | 控制终端的缩写名称 |
TTY | 控制终端的完整名称 |
UID | 用户标识 |
USER | 用户名 |
VSZ | 虚拟大小(单位KB) |
WCHAN | 等待通道 |
状态码说明:
Linux、FreeBSD | |
---|---|
D | 不可中断睡眠:等待时间结束(通常是I/O,D=“磁盘”) |
I | 空闲:超过20秒的睡眠(仅仅适用于 FreeBSD) |
R | 正在运行或可运行(可运行=正在运行队列中等待) |
S | 可中断睡眠:等待事件结束 |
T | 挂起:由作业控制信号挂起或者因为追踪而被挂起 |
Z | 僵进程:终止后,父进程没有等待 |