Skip to content

Latest commit

 

History

History
232 lines (135 loc) · 13.1 KB

0X08_进程管理.md

File metadata and controls

232 lines (135 loc) · 13.1 KB

0X08 进程管理

进程

进程的准确定义其实现在还有争议.简单点说,一个程序执行一次就是一个进程(或者说电脑在做的一件事).

为了后续叙述方便我就说一下我自己对进程的理解(不一定对):

我们都知道计算机要执行一个任务,需要硬件与软件,算法和资源的配合.但是在硬件资源中,CPU和内存又占有特殊地位.一个程序运行的时候可以不需要打印机,不需要键盘输入,不需要显示器,但是不能没有CPU和内存.

为了合理规划任务执行,提高硬件利用率.就需要对CPU和内存的使用进行管理.这就引申出了操作系统的基本任务中的两个--内存管理和CPU管理.

进程就是在CPU管理和内存管理中提出的一个概念.一般来说,一个进程对应一个任务.把CPU工作时间,内存空间,其他硬件设备使用权等资源提供给它,它就能运作并完成我们的任务(如果进程中运行的程序代码没有问题的话).

但是,认为一个进程对应一个程序也是不妥的.因为我们所说的"任务"其实是一个模糊的概念.你可以把写完今天的英语作业看作一个任务.也可以把写完今天的数学作业看作一个任务.也可以把写完今天的所有作业看作一个任务.同样,一个进程中可以包含一个程序,也可以包含多个相关的程序.甚至可能是一个程序多次启动,产生多个进程--例如之前遇到过的在一个bash中启动另一个bash.

具体是那种情况,就看启动进程的代码怎么写了.实现具体程序的时候,选择单进程还是多进程,选择几个进程,这些问题一言难尽,不在这里做讨论.感兴趣可以去自行搜索一下相关内容.

进程树

就像我们做事一样,一个大任务看起来很困难.那就可以把大任务先分割成若干小任务去做.小任务如果还复杂,就分割成更小的任务.很多情况下为了及时性或者提高系统资源利用率等原因,把所有任务排个队,挨个完成的单进程模式并不是最好的选择.

当一个进程认为有必要把一部分任务分配给另一个进程的时候,就可以调用系统中特定的系统接口,会创建一个新的进程.被创建的那个新进程相对于原来的进程,叫做子进程.而创建新进程的进程叫做父进程.父子关系是相对的,很多进程一方面是一个进程的子进程,另一方面又是很多别的进程的父进程.

在进程管理的时候.如果一个进程没有子进程,那么它本身运行完就算完成了.但是如果一个进程有子进程没有运行完,我们就认为它本身没有运行完(因为子进程其实是分担了父进程的部分任务).因而,正常情况下,有子进程尚未终止的进程是不允许终止的.如果一定要终止,需要先对其子进程做处理(比如把要终止进程的子进程交给要终止的进程的父进程作为要终止的进程的父进程的子进程).

一般来说,能创建进程的只有另一个进程,除了一开机启动的那个初始进程.如果不考虑游离的进程的话,一个系统中所有的进程按照父子关系联结起来就构成一个树的形状.称为进程树.之后介绍的一些工具可以形象地展示出进程树.

顺便提一句,进程树的根叫做init进程,其进程号(Process ID)为1.

实验准备

如果没有多个同时存在的进程,讨论进程管理就是没有意义的.但是到现在为止我们所介绍的大部分命令只需要很短的时间就能运行完.因此我们需要自己用脚本写一个不会自动终止的程序用于演示进程.下边是我用Lua写的一个死循环的程序.文件名为loop.lua.

while true do
end

后台运行

很多从bash启动的程序都是默认前台运行的.所谓前台运行就是说会占用终端.如果我们直接执行loop.lua,那么终端就会被它阻塞,无法输入别的命令.所以需要让它后台运行(仍然执行脚本中的命令,但是不占用键盘和显示器).

在输入命令时如果后接&则会后台运行.例如:

lua loop.lua &

jobs和kill

使用bash内置命令jobs可以查看当前bash下运行的工作.后加参数-l显示详细信息

如果执行jobs前已经后台启动了loop.lua,那么就能看到这个进程.例如我这里显示:

[1]+ 1117 Running lua loop.lua &

其中方括号内的1是工作号(job number),1117是进程号(process ID).Running是执行状况.lua loop.lua是进程名.&符表示程序后台运行.(进程在操作系统中由PID(Process ID)标识.在使用bash的进程管理时,还会给每个可以管理的进程分配一个工作号(job number).)

想要终止这个后台进程,使用这个命令杀死进程:

kill -9 %1

在百分号后写的是工作序号,也就是用jobs查看工作的时候,loop.lua这一项的序号.

更多查看进程的方法

ps命令会给当前的进程状态做一个快照,然后输出.但是它不显示进程的工作号.仅仅显示进程的进程号.

ps -A

后加参数-A会显示所有进程.

pstree命令则会把进程按照进程树的树形结构排布输出.

ps, pstree, jobs所查看的都是一瞬间的静态信息.top命令显示的信息则是不断刷新的.要退出top程序,按键q.

关于kill

进程之间,为了协调合作或者相互控制需要进行通信.kill命令其实是bash用于向其他进程发送信号的命令.但是日常最常用的用途就是用它来发送强制终止进程的信号.

要想从命令行终止进程则使用kill命令加要终止的进程的PID。(前提是你拥有终止该进程的权限。)例如:

kill 1810

这时kill默认发送普通的结束请求(SIGTERM,对应数字为18),这个信号不会强制其他进程终止.但是有的程序有时会忽视这个请求。所以还需要加额外参数指定强制终止信号:

kill -s KILL 1810

其中参数-s表示要向进程发送信息,KILL是要发送的信息,意在让进程立即终止.

更简单的写法是:

kill -9 1810

指定进程也有多种方法.之前例子中,kill后的1810是程序PID.也可以用工作号指定.但是工作号之前需要加百分号%.

%+%-可以用于最近后台启动或暂停过的进程.具体指那个进程,可以通过jobs列表中工作号后的加减号辨别.带加号的被称为默认作业.带减号的是下一个默认作业.默认作业完成后下一个默认作业成为默认作业.%后接进程名的部分拼写也可以指进程.这其中还可以用一些正则表达式规则.

为了进一步说明kill命令先介绍一个suspend命令(这是bash的内置命令).如果执行这个命令的shell不是一开登录打开的shell,那么就会暂停当前shell.只有接收到SIGCONT信号时才会继续运行.

就如刚才所说,kill命令不仅仅用于终止进程.它的完整功能是给指定进程传送信号.kill所支持的信号可以通过kill -l查看.

可以做一个实验,打开两个虚拟终端,假设其中一个的bash的PID是2619(可以用ps、htop等工具查看).在2619上运行suspend命令暂停bash.然后在另一个终端执行kill -18 2619就可以继续被暂停的bash (SIGCONT对应数字18).

个人认为有一个更好用的命令killall.它是按名称搜索的.

进程状态

进程有三种基本状态:就绪,执行,阻塞。外加创造进程时的开始和完成进程后的结束,一共五种状态。而进程管理就是根据各个进程的执行状况和现在的资源情况让进程们在这几种状态之间来回切换。为了调度方便,一般还增加一种挂起状态(其实是两种状态——静止阻塞,静止就绪)。

在进行调度的时候,其实只用操心进程是不是在'跑'(run).如果进程正常推进,则一切ok.对于正在运行的进程,我们可以通过命令让它暂停推进.如果进程暂停.我们又可以用命令让进程继续运行.

此外我们还能用命令让进程停止运行(SIGTERM).如果进程无法停止,则可以强制停止(KILL).

其他进程调度指令

fg后接进程,将指定进程调至前台运行。

bg后接进程,将指定进程调指后台运行。

wait后接子进程。执行wait命令时bash将等待,当指定子进程结束后返回bash。

disown后接子进程。当bash退出时不会连带用disown指定的子进程一起退出。由于原有父进程终止,这个子进程将被挂到bash的父进程上。

以上4个进程调度指令都是bash的内置命令.

Coreutils还提供了一个作用有点类似于disown的指令nohup.这两个指令一般都用来使一个进程在bash退出之后仍然能运行(这在服务器上很常用.一般一个任务跑几个小时,几天甚至更长,并且不能中断.而你总不能一直保持ssh连接.)

要理解这两个指令的工作原理,需要先说一下如果父进程终止,子进程如何处理的问题.如果父进程接到终止命令,它一般会给自己的子进程发送终止命令.然后子进程结到命令后也就终止.我们在bash中启动的进程,默认都是bash的子进程,所以bash一退出我们的所有子进程都会退出.

disown的原理是指定一个子进程(用子进程工作号).当父进程退出的时候,这个子进程就不在属于原有父进程,而是属于父进程的父进程.例如(这里假设启动的lua进程的工作号是1):

lua loop.lua &
disown %1
exit

然后在另一个终端中用ps,pstree或者htop查看进程.

而nohup是一个'前缀'指令.在要执行的指令之间加nohup,那么这个指令启动的进程就不会收到父进程发来的终止命令的影响(手册上用的词是'immune').这里举个例子:

nohup lua loop.lua &
exit

然后在另一个终端中用ps,pstree或者htop查看进程.

注意disown和nohup对输入输出流的处理不一样.disown的进程默认情况下输出流仍然和Shell一致.而nohup一般会关闭进程输入流,并且把输出流定向到它指定的文件.(如果不知道什么是输入输出流,可以先放一放,后边会介绍输入输出流.)

演示中所有lua进程都是后台运行.后台运行不是必须的,但是如果不后台运行,终端被占用了我们一般没法输入命令.

内存查看

之前介绍的内容都主要是进程管理.这一小节说一说内存管理.

内存管理方面主要是系统和程序的事情,其实需要用户做的事情不多.这里介绍一种查看内存使用状态的方法:

cat /proc/meminfo

这里的cat命令表示把后边的文件的内容打印到终端(这个命令本身后边还会提到).而/proc之前已经说过是一个专门放特殊文件的文件夹.里边的文件的内容可能随着计算机的运行而经常改变.文件内容反映了计算机的运行状况.而这个meminfo文件就反映了计算机的内存信息.

另外,使用htop也能看到内存使用情况.

systemd

还记得之前说的PID为1的进程么?在现在的大多数Linux发行版中,这个进程都来自systemd.它提供了系统和服务管理器,并且负责启动系统的其他部分.这里我们不深入讨论Linux系统启动的机制,仅仅介绍几个systemd提供的命令.

shutdown命令用于关机.如果你的发行版直接执行shutdown之后会延时一会才关机.那么就试试加参数now,例如:

shutdown now

如果你要停止系统运行但是不关闭电源,那么就使用halt命令.这个命令类等价于:

shutdown -h

如果要停止系统运行并且关闭电源,就使用命令poweroff.这个命令等价于:

shutdown -P

另外注意,shutdown命令默认是会关闭电源的.

还有reboot命令用于重启计算机.

快捷键和其他

进程控制快捷键可以在无法向bash输入命令时控制进程(例如一个前台进程正在运行).但是这些快捷键只对前台进程有效.另外这组快捷键并非由bash定义,而是由终端驱动解析,可以通过Coreutils提供的stty命令查看或修改(stty -a查看所有快捷键)。

  • C-z 暂停当前进程,并把控制权交还bash。
  • C-y 延迟暂停进程,当进程试图读取输入流时暂停进程并把控制权交还bash。
  • C-s 暂停当前进程,但不交还控制权给bash。
  • C-q 使用C-s暂停进程后使用C-q继续进程。
  • C-c 终止当前进程,返回bash。

(我这里采用了Emacs手册的快捷键记法,'C-z'表示Ctrl加z键.在使用stty -a命令查看时,它会把Ctrl键打印为'^'.例如'eof=^D;',这条信息表示用C-d输入EOF(文件结尾符).)

这些快捷键的本质是向当前前台进程发送信号.例如C-c发送SIGINT信号,C-z发送SIGTSTP信号.

既然说到了终端驱动就再多说一句,不论我们用的是不是虚拟终端,对于系统来说我们都是通过/dev路径下的一个设备在与之交互.要查看当前我们的shell所占用的终端设备则使用tty命令.

sleep命令

Coreutils提供了一个叫做sleep的命令.当执行到它的时候shell就会暂停工作一会.例如:

sleep 5

就表示暂停5秒.可以用这个命令来实验进程相关的操作.

在脚本编写中,延时命令可以用来控制计时或者防止某些操作过于频繁地进行.