跳转至

进程相关的shell命令

在这一章之前,我们用了两章,介绍了和计算机网络有关的shell命令和相关的概念。现在我们将要进入下一个操作系统提供给我们的重要抽象——进程。我将简单介绍一下和进程相关的重要概念,然后介绍一些和进程相关的命令。

什么是进程

实际上,在前面两章,我们已经简单提及了进程这个概念。当时我们说,进程就是在运行中的程序。这是一个十分简化的理解。

在计算机发展的初期,计算机相比于现在价格高昂,计算能力比较弱,功能也很简单。那时还很难说有操作系统的概念,人们将写好的程序和相应的输入数据(放在某种存储设备中,比如打孔纸带或者磁带)依次放入计算机。这里我说的依次是指,前一个程序运行完成之后,再放入下一个程序和相应数据。因为计算能力比较弱,计算机需要话很多时间(几个小时或者更对)来完成任务。也就是说,计算机一次只能运行一个程序。

当任务比较少的的时候,这种方式看上去没什么太多问题。但是当任务比较多开始排队的时候,问题就出现了,由于不知道下一个任务何时结束,所以程序员或者管理员有时提交下一个任务会不及时,这就会导致这中间的计算资源被浪费。

因此人们为了解决这个问题提供了一种方式,大家将一批程序一次性提交给计算机,计算机的输出端有某种装置,它可以在前一个任务完成之后自动取出下一个程序(和它的数据)并读入。这样程序员只需要定期去检查是否输出了结果就行了。也就是说,这时计算机可以按顺序一次性执行一批程序。这样的系统也就被称为批处理系统。

后来,随着计算机内存大小的增加,人们终于有可能将多个程序同时加载到内存里面了。也就是任务不必完成一项再开始下一项了。于是人们发现,还可以利用这一点解决另外两个问题。第一,程序运行过程中不总是在使用全部计算资源,有的时候程序会将时间花在与存储设备或者其他外部设备交互上,这些时间里,cpu资源实际上是被浪费的。第二,一些程序可能因为错误,或者有人写出恶意程序,导致这些程序不能停止。如果发生了这种情况,后面的任务将永远无法开始运行。

解决这种问题的方式就是让每个程序共享cpu。每个程序可以在cpu上运行一段时间,然后它就必须主动或被动地让出cpu给其他程序运行。在一个程序和外部设备通信的时候,这个程序就可以把cpu让给其他程序使用,这样就解决了第一个问题;每个程序占用cpu足够长的时间之后就必须把cpu让给其他程序,这样就解决了第二个问题。但是从每个程序的视角来看,它们看上去像是独占了cpu,因为在放弃cpu给其他程序的前后,这个程序的运行是连贯的。这样,我们就有了进程的概念,每个正在运行的程序就是一个进程,在这些进程看来,它们似乎独占地使用cpu和内存,但是实际上进程之间是共享这些资源的。另外,进程还有一个重要特点,为了安全考虑,虽然进程共享物理内存,但是每个进程的内存是隔离的,也就是进程只能使用自己的内存数据。

总结一下这一节:进程是计算机上正在运行(准确来说是活跃,因为进程可能会放弃cpu而处于暂停状态)的程序,它们共享cpu和内存,但是操作系统让它们认为自己在独占这些资源。

shell对于进程的抽象

shell当中,并没有直接的进程的概念。在shell中进程被包装成了任务(job)。shell当中任务的概念和进程不完全一致,主要的区别就是,当我们使用管道连接多个命令的时候,每个子命令(这是我为了方便说明自己创造的词)实际上是一个单独的进程,但是shell会认为这个整个命令是一个任务。任务是shell中最重要的概念之一,因此,shell也提供了很多帮助我们管理和控制任务的命令。这些命令将会在下一章中介绍。

linux操作系统中,进程可以分成组,并且可以对同一个组的进程做相同的操作。因此,任务本质上就是将在同一个命令中的启动的进程分成了进程组。

任务的运行状态

任务总共有五种运行状态(进程也有类似的概念):正在运行(Running)的任务,停止(stopped)的任务,终止(terminated)的任务,结束(Done)的任务和被杀死(Killed)的任务。其中停止的任务和终止的任务的主要区别是,停止的任务实际上是暂停的,也就是之后还可以继续运行,终止的任务则是进程被终止或者执行完成不可能再继续运行的任务。

前台任务和后台任务

和任务相关的另一个重要的概念是前台(foreground)任务和后台(background)任务。这两个概念是对于正在运行的任务的,其中前台任务是指在启动之后会让shell暂停,等它运行结束之后shell才会继续运行的任务;而后台任务就是在启动之后shell不会暂停,可以继续使用shell启动其他任务。

进程间通信

从上文进程的特点中我们能发现,操作系统中提出进程这个概念的时候,最主要的目的就是在程序之间共享计算机资源的情况下实现隔离,比如每个进程只能访问到自己的内存数据。但是,有的时候,我们希望进程之间能协同完成一些任务,这就需要数据能够在进程之间传递。也就是进程之间的一个经典的问题,进程间通信,简称IPC(Inter Process Communication)。进程间通信的方式有很多,实际上大家可能会想起来,第九章中我们介绍过的tcp协议就可以实现进程间通信。tcp协议可以用来实现同一台计算机上两个进程之间的通信。不过对于一些应用场景来说,tcp协议并不好用,因为它的传输开销比较大(比较慢而且需要更多的内存),但是有的时候我们只希望在进程之间传递最简单的消息。因此,进程间通信还有其他的方式,接下来我们要介绍其中比较常用的一种——信号。

信号(软件中断)

什么是信号

信号是操作系统为我们提供的一种软件方式实现的中断。简单来说就是可以在一个程序的正常运行过程中,打断这个这个程序的运行并给它传递一个信息(也就是一个定义好的信号)。被打断正常执行之后,程序会立即进入一个特殊的中断处理程序,完成这个中断处理程序的执行之后会回到之前被打断的地方继续执行。Linux系统定义了30种标准信号,每种信号都可以表示一种信息。每一种信号都有一个默认的处理程序,大部分信号的默认处理程序都是什么也不做或者终止程序。如果我们想要让我们的程序在收到信号的时候做不同的行为,也可以在程序中自己定义某个或者某些信号的处理程序。下面这个表中是一些常用的信号的默认行为和含义(来自signal的manpage),其中默认行为这一栏中Term表示终止进程(Terminate),Core表示终止程序并转储内核(就当做是终止程序就行),Ign表示忽略信号(Ignore),Stop表示暂停进程(之后还可以恢复运行), Cont表示从暂停的状态中恢复(如果处于运行状态就忽略)。后面的Comment是对这个信号的简单解释。这个表格中的内容并不很重要,仅供参考。

Signal Standard Action Comment
SIGABRT P1990 Core Abort signal from abort(3)
SIGALRM P1990 Term Timer signal from alarm(2)
SIGBUS P2001 Core Bus error (bad memory access)
SIGCHLD P1990 Ign Child stopped or terminated
SIGCLD - Ign A synonym for SIGCHLD
SIGCONT P1990 Cont Continue if stopped
SIGFPE P1990 Core Floating-point exception
SIGHUP P1990 Term Hangup detected on controlling terminal or death of controlling process
SIGILL P1990 Core Illegal Instruction
SIGINT P1990 Term Interrupt from keyboard
SIGKILL P1990 Term Kill signal
SIGPIPE P1990 Term Broken pipe: write to pipe with no readers;
SIGQUIT P1990 Core Quit from keyboard
SIGSEGV P1990 Core Invalid memory reference
SIGSTOP P1990 Stop Stop process
SIGTSTP P1990 Stop Stop typed at terminal
SIGSYS P2001 Core Bad system call (SVr4);
SIGTERM P1990 Term Termination signal
SIGUSR1 P1990 Term User-defined signal 1
SIGUSR2 P1990 Term User-defined signal 2

从键盘发送信号

前面我们介绍了信号的概念和信号的作用。自然,我们会想问,如何向一个进程发送信号。经过前面的学习,大家想必已经能猜到,既然信号是操作系统提供的,那么操作系统肯定提供了发送信号的系统调用吧?没错,这回大家猜对了,操作系统提供了一系列和信号有关的系统调用,其中当然包括发送信号。当然,因为本文档的重点不是操作系统也不是系统编程,所以我在这里不会详细介绍这些系统调用的用法。

更进一步,因为操作系统有发送信号的系统调用,我们会想问,有没有能让我们发送信号的shell命令呢?当然有,不过先别急,这个会在下一节介绍。

在这之前,我们要先介绍一种更简便更常用的发送信号的方式。因为SIGINT和SIGSTOP这两个信号实在是太常用了,所以shell为我们提供了直接用键盘向进程发送这两个信号的能力。

发送SIGINT

当shell中正在运行一个前台任务的时候,按下Ctrl-C就可以向这个前台任务发送一个SIGINT(中断信号)。从上面的表中我们可以查到SIGINT的默认处理方式是终止(Terminate),所以对于大多数程序,在程序正在终端的前台运行的时候,我们可以通过在终端按下键盘的Ctrl-C来终止这个程序,这是十分有用的操作。我们可以尝试一下,现在shell中运行下面这个命令

sleep 10
这命令会让运行一个前台进程,但是这个进程什么也不做,休眠10秒钟之后结束。如果只运行这个命令,你会发现shell像是卡住了,10秒之后输出下一个prompt。但是如果你在这不到10秒钟的时候在终端按下Ctrl-C,你会发现shell直接输出了一个新的提示符,这说明我们使用Ctrl-C终止了sleep

发送SIGTSTP

当shell中正在运行一个前台任务的时候,按下Ctrl-Z就可以向这个前台任务发送一个SIGTSTP(终端暂停信号)。这个信号和SIGSTOP类似,不过是从终端发出的,所以叫终端暂停信号。从上面的表中我们可以查到SIGTSTP的默认处理方式是暂停(STOP)。这是一种特殊的状态,进程被永远地暂停执行,直到它收到另一个其他信号。并且shell也会记录这个任务的暂停状态。后面我们可以看到,除了发送其他信号,还可以使用另外的命令让这个任务继续执行。这里,shell处理在输出一个新的提示符之前,应该还会打印一个额外的提示。

查看进程信息的命令——ps

在介绍使用命令来发送信号之前,我们首先还需要介绍一个重要的概念,就是pid(Process Idification)。它是一个数字,标识了一个进程,在每个时刻,同一个系统中,每个进程的pid是唯一的。也就是,在系统中,我们使用一个进程的pid来表示这进程。那么我们当然要问,我们能获取一个进程的pid吗?当然可以,并且也是通过一个命令,也就是ps

这个命令的功能是将系统某一时刻,全部进程的状态打印在屏幕上。如果你只是使用ps你会发现,打印出来的内容很少,这是因为ps默认只打印在当前终端前台运行的进程。

ps命令的选项格式有两套系统,比较复杂,感兴趣的同学可以自己去阅读ps的手册,这里我们只介绍最常用的一个命令

ps aux
其中选项a标识打印所有进程的信息,u表示结果中显示对于用户来说重要的信息,包括进程所属的用户名,cpu和内存占用率,x表示显示所有进程(而不是只有前台进程组的进程)。

我们会看到,ps打印出来的信息当中,包含很多信息。其中对于我们来说重要的信息包括,在第二列的pid和最后一列的command。最后一列的command就表示当初启动这个进程的时候使用的命令,因为这里包括软件名称和命令行参数,所以我们实际上是使用这一列的信息来查找我们想找的进程的。如果利用我们后面会讲到的grep命令,就可以组合出查找特定进程的命令,就像下面这样

ps aux | grep bash
这个命令的含义就是,使用ps输出全部进程信息,然后使用grep筛选带有bash这个字符串的行。实际上也就是将bash运行的进程找出来。

向进程发送信号的命令——kill

虽然这个命令名字看上去很奇怪,但是kill不止可以用来发送信号让一个进程终止(虽然但是,的确有很多信号的默认行为是终止进程),它可以向任意进程(或进程组)发送任意信号。

列举信号和它们的信息

首先,kill的一个用法是可以列举它能发送的全部信号的名称和编号以及其他信息。你可以使用-l选项列举所有信号的简称。你还可以使用-t选项列举更详细的信息,第一列是编号,第二列是简称,第三列是全称。这个用法很简单,没什么可说的。

发送信号

使用kill来发送信号的方式也比较简单。首先正如他名字说的那样,你可以在后面直接接一个位置参数表示你要杀死的进程号。它会给这个进程发送一个SIGTERM信号,让这个进程直接终止(不过需要注意,虽然收到SIGTERM的默认行为是终止进程,但是程序可以更改这个行为,所以进程不一定会终止)。

而且明明有SIGKILL信号,这个信号的默认行为是终止进程,并且这个默认行为不能更改,为什么kill命令不会发送SIGKILL呢?这很奇怪。当然如果你想要强制终止一个进程,可以使用下面介绍的方式

另外,你可以在进程号前面加一个-号,这表示向这个进程所在的进程组的所有进程发送同一个信号。

你也可以指定发送的信号,并且有三种选项类型和两种表示信号的方式,总共可以组合出6种方式。当然,你只需要选择一种你最习惯的方式就行了。

三种选项类型,分别是(下面的x处可以使用后面的那两种信号的表示方式) 1. -x 2. -s x/--signal x(很明显,/前的是缩写形式) 3. -s=x/--signal=x

两种信号表示类型分别是使用编号,和使用简称。信号的编号和简称你都可以使用-t选项查到。

需要注意,实际上表示信号和表示进程虽然都有使用-,但是他们更接近位置参数的形式,所以它们的顺序很重要。同时指定的时候,一定是信号在前面,进程号在后面。

发送信号更方便的命令——pkill

这个命令的作用和表示信号的方式与kill几乎没有区别,但是这个命令不需要使用进程号,而是使用字符串检索匹配合适的进程名称。在一些时候这个命令使用起来更加方便,但是因为是基于字符串匹配的,所以有误伤的可能。如果你希望更精准地找到你想要的进程,可以使用-f/--full选项,这后面必须使用完整的命令(包括命令行选项)来匹配进程。

类似的命令还有pgrep,可以根据进程名称返回匹配的进程号,以及pwait。它们的具体用法可以查阅相关资料。

在后台运行任务

上面我们提到过,shell为我们维护了前台任务和后台任务的概念。前面我们使用的命令都是在shell中开启了一个新的前台进程,也就是说在这个命令的执行完成之前,我们不能执行其他的命令。但是很明显,有的时候我们需要在后台运行任务。下面我将介绍三种方式。

warning

注意,因为前台任务和后台任务是由shell为我们提供的概念,所以不同的shell的具体实现会有很多细节不同。下面的内容只在bash中有效。

直接启动后台任务——&

第一种方式很简单,我们可以直接启动后台任务。只需要在正常的命令的最后加上一个&即可,&和命令中间是否有空格都没关系。大家可以尝试下面这例子。

sleep 10
直接运行这个命令的效果就是什么也不发生,但是10s之后bash才会打印下一个命令行提示符。sleep就是只是启动了一个进程然后这个进程休眠10s之后结束。当然,我们也可以使用Ctrl-C或者Ctrl-Z来打断它。但是如果我们使用下面这个命令
sleep 10&
你会发现,bash首先打印出了一个类似这样的信息,然后才打印出命令行提示符。
[1] 3021
这里前面的[1]是bash任务的任务号,我们可以使用这个号码来表示一个任务(请注意,任务和进程的含义不完全相同),后面的3021则是在操作系统中这个任务所在的进程组号

这里我们也就能发现,实际上bash中的任务基本就是操作系统的一个进程组。因此,即使我们不能通过键盘操作向后台任务发送信号,也可以通过这个进程组号给后台任务发送信号。(还记得吗,我们上面在kill命令那里提到过,在号码前面加上-就可以表示进程组号)。

查看shell中的任务——jobs

在介绍下一种方法之前,我们首先需要知道如何查看shell中的任务,包括正在运行的任务和暂停的任务。这个命令就是jobs。这个命令的用法很简单,直接使用命令

jobs
就可以看到,类似这样的输出
[1]+  Stopped                 sleep 100
[2]-  Running                 sleep 100 &
最前面[]中的是shell中的任务编号,然后的Stopped/Running/Done/Killed表示这个任务的状态是暂停/正在运行/已经结束/被SIGKILL终止,最后是执行这个任务的时候使用的命令。和一些其他命令一样,我们也可以使用-l选项来打印更详细的信息。打印出来的内容会在状态信息前面多一个这个任务的进程组的编号。

在后台继续一个暂停的任务——bg

有了jobs命令的铺垫,现在我们可以介绍bg(background的缩写)命令了。这个命令接受一个暂停状态的任务的编号,可以将这个暂停状态的任务在后台继续执行。因此很明显,在使用这个命令之前我们首先要使用jobs来获取这个任务的编号。

我们可以这样来尝试这个命令,首先使用

sleep 100
来创建一个持续时间足够长的任务。这个任务运行在前台,所以我们可以使用键盘输入Ctrl-Z来给这个进程发送一个SIGTSTP信号。当然我们也可以使用kill命令给这个任务的进程组发送一个SIGSTOP信号。这时这个任务就暂停了。我们使用
jobs
来查看这个任务的信息。如果你使用的是kill命令来发送信号,我们还会发现,任务的状态后面除了Stopped以外还多了一个(signal),不过这是无关紧要的细节。从打印出来的信息我们就可以找到任务的编号。比如这里我们的任务编号是1,那么我们就可以使用
bg 1
来在后台继续执行这个任务。我们会得到这样的输出
[1]+ sleep 100 &
这表明我们的这个任务已经在后台继续运行起来了。我们还可以使用jobs命令来检查一下这个任务的状态。

使用信号在后台继续一个暂停的任务

最后我们还可以使用信号继续一个暂停的任务,这个继续的任务会自动在后台运行。前面我们在介绍SIGTSTP那里介绍任务的暂停状态的时候提到,如果我们向一个暂停的任务发送一个信号,它就会停止自己的状态,但是大部分信号都会有它原本的作用,会终止一个进程等等。不过如果仔细看我列出的那几个信号,你就会从中发现一个SIGCONT信号。这个信号就是专门用来给我们继续一个暂停的任务的。现在你可以重复一下上一节中前面的步骤,然后使用

jobs -l
来获得休眠的任务的进程组号,假设这个进程组号是4280,那么就可以使用
kill -CONT -4280
来向这个任务(注意要加-因为任务是进程组)发送一个SIGCONT信号。这个命令也有其他形式,例如使用SIGCONT的编号而不是-CONT,这里你可以根据你的习惯。然后再使用jobs命令检查一下任务的状态,你会发现这个任务在后台继续执行了。

将后台任务放到前台——fg

前面介绍了几种如何将一个任务在后台执行的方式,这一节的最后,我们再来介绍一下如何将一个后台任务放到前台执行。这个命令也很简单,就是fg(foreground),它和bg类似,但是它不止可以用于暂停的进程,也可以用于正在运行的进程。大家可以自己仿照上面的例子来尝试一下这个命令。

持续运行的后台任务

很多时候,我们需要后台任务的一个重要原因是有些程序我们需要持续运行,但是我们不希望它们占用终端资源影响我们运行其他任务。比如,如果你在你的机器搭建一个网站,那么你需要运行一个http server。我们不需要交互地使用者server,只需要它一直在后台自己运行就可以了。这种时候,我们就需要后台任务。但是,或许你们已经发现了,当我们关闭了终端的时候,比如你关闭了图形化界面的终端窗口,或者断开了和远程服务器的ssh连接。这时,前台任务当然不用说,但是即使是后台任务也会终止。

这是因为当我们关闭终端的时候,终端上运行的所有任务都会收到一个SIGHUP信号,这信号的默认行为就是终止。当然,如果是我们自己写的一个程序,我们可以通过重新注册一个信号处理程序来修改这种行为,但是如果是使用别人的程序,这么做就不可行。不过,shell也给我们提供了一种方式,就是nohup命令。在这个命令后面接我们要执行的命令,这任务就会在收到SIGHUP时什么也不做,继续执行。再配合上面提到的将任务在后台运行的方式,我们就可以实现,让一个命令在后台运行并且即使终端关闭了也不终止。当然,因为终端被关闭的时候shell会终止,所以你之后就没法在jobs的任务列表中找到它了,但是你仍然可以通过ps来找到它。

最后,bash中(其他的shell行为可能不同),当你使用nohup执行一个命令的时候,这命令的输出信息会被重定向到当前文件夹下的nohup.out文件中。当然,如果你不想让输出信息被放到这文件中的话,你可以在命令中自己使用重定向。

小结

这一章从操作系统中进程和信号的概念开始,介绍了shell中对进程的抽象,也就是任务。然后介绍了一些操作信号,进程和任务的命令。当然,对于操作系统角度的进程和信号等的内容,这一章虽然有讲解,但是十分简略。如果你希望对相关的话题了解更多,可以自己去查找相关资料或者去学习计算机系统导论和操作系统课程。在计算机系统导论中甚至有关于shell任务管理的实现的详细讨论。

这一章结束之后,初级篇最核心,最复杂的内容都基本完成了,恭喜你。虽然初级篇后面还有几章内容,但是都比较简单。