进程和进程的基本操作
配套视频已上线:55 节视频,80+ 份实验素材,独立代码仓库
进程和进程的基本操作
前言
进程作为程序真正发挥作用时的“形态”,我们有必要对它的一些相关操作非常熟悉,这一节主要描述进程相关的概念和操作,将介绍包括程序、进程、作业等基本概念以及进程状态查询、进程通信等相关的操作。
什么是程序,什么又是进程
程序是指令的集合,而进程则是程序执行的基本单元。为了让程序完成它的工作,必须让程序运行起来成为进程,进而利用处理器资源、内存资源,进行各种 I/O
操作,从而完成某项特定工作。
从这个意思上说,程序是静态的,而进程则是动态的。
进程有区别于程序的地方还有:进程除了包含程序文件中的指令数据以外,还需要在内核中有一个数据结构用以存放特定进程的相关属性,以便内核更好地管理和调度进程,从而完成多进程协作的任务。因此,从这个意义上可以说“高于”程序,超出了程序指令本身。
如果进行过多进程程序的开发,又会发现,一个程序可能创建多个进程,通过多个进程的交互完成任务。在 Linux 下,多进程的创建通常是通过 fork
系统调用来实现。从这个意义上来说程序则”包含”了进程。
另外一个需要明确的是,程序可以由多种不同程序语言描述,包括 C 语言程序、汇编语言程序和最后编译产生的机器指令等。
下面简单讨论 Linux 下面如何通过 Shell 进行进程的相关操作。
进程的创建
通常在命令行键入某个程序文件名以后,一个进程就被创建了。例如,
让程序在后台运行
查看进程 ID
用pidof
可以查看指定程序名的进程ID:
查看进程的内存映像
程序被执行后,就被加载到内存中,成为了一个进程。上面显示了该进程的内存映像(虚拟内存),包括程序指令、数据,以及一些用于存放程序命令行参数、环境变量的栈空间,用于动态内存申请的堆空间都被分配好。
关于程序在命令行执行过程的细节,请参考《Linux 命令行下程序执行的一刹那》。
实际上,创建一个进程,也就是说让程序运行,还有其他的办法,比如,通过一些配置让系统启动时自动启动程序(具体参考 man init
),或者是通过配置 crond
(或者 at
)让它定时启动程序。除此之外,还有一个方式,那就是编写 Shell 脚本,把程序写入一个脚本文件,当执行脚本文件时,文件中的程序将被执行而成为进程。这些方式的细节就不介绍,下面了解如何查看进程的属性。
需要补充一点的是:在命令行下执行程序,可以通过 ulimit
内置命令来设置进程可以利用的资源,比如进程可以打开的最大文件描述符个数,最大的栈空间,虚拟内存空间等。具体用法见 help ulimit
。
查看进程的属性和状态
可以通过 ps
命令查看进程相关属性和状态,这些信息包括进程所属用户,进程对应的程序,进程对 cpu
和内存的使用情况等信息。熟悉如何查看它们有助于进行相关的统计分析等操作。
通过 ps 命令查看进程属性
查看系统当前所有进程的属性:
查看命令中包含某字符的程序对应的进程,进程 ID
是 1 。 TTY
为?表示和终端没有关联:
选择某个特定用户启动的进程:
按照指定格式输出指定内容,下面输出命令名和 cpu
使用率:
打印 cpu
使用率最高的前 4 个程序:
获取使用虚拟内存最大的 5 个进程:
通过 pstree 查看进程亲缘关系
系统所有进程之间都有“亲缘”关系,可以通过 pstree
查看这种关系:
上面会打印系统进程调用树,可以非常清楚地看到当前系统中所有活动进程之间的调用关系。
用top动态查看进程信息
该命令最大特点是可以动态地查看进程信息,当然,它还提供了一些其他的参数,比如 -S
可以按照累计执行时间的大小排序查看,也可以通过 -u
查看指定用户启动的进程等。
补充: top
命令支持交互式,比如它支持 u
命令显示用户的所有进程,支持通过 k
命令杀掉某个进程;如果使用 -n 1
选项可以启用批处理模式,具体用法为:
确保特定程序只有一个副本在运行
下面来讨论一个有趣的问题:如何让一个程序在同一时间只有一个在运行。
这意味着当一个程序正在被执行时,它将不能再被启动。那该怎么做呢?
假如一份相同的程序被复制成了很多份,并且具有不同的文件名被放在不同的位置,这个将比较糟糕,所以考虑最简单的情况,那就是这份程序在整个系统上是唯一的,而且名字也是唯一的。这样的话,有哪些办法来回答上面的问题呢?
总的机理是:在程序开头检查自己有没有执行,如果执行了则停止否则继续执行后续代码。
策略则是多样的,由于前面的假设已经保证程序文件名和代码的唯一性,所以通过 ps
命令找出当前所有进程对应的程序名,逐个与自己的程序名比较,如果已经有,那么说明自己已经运行了。
每次运行时先在指定位置检查是否存在一个保存自己进程 ID
的文件,如果不存在,那么继续执行,如果存在,那么查看该进程 ID
是否正在运行,如果在,那么退出,否则往该文件重新写入新的进程 ID
,并继续。
更多实现策略自己尽情发挥吧!
调整进程的优先级
在保证每个进程都能够顺利执行外,为了让某些任务优先完成,那么系统在进行进程调度时就会采用一定的调度办法,比如常见的有按照优先级的时间片轮转的调度算法。这种情况下,可以通过 renice
调整正在运行的程序的优先级,例如:`
获取进程优先级
调整进程的优先级
结束进程
既然可以通过命令行执行程序,创建进程,那么也有办法结束它。可以通过 kill
命令给用户自己启动的进程发送某个信号让进程终止,当然“万能”的 root
几乎可以 kill
所有进程(除了 init
之外)。例如,
结束进程
kill
命令默认会发送终止信号( SIGTERM
)给程序,让程序退出,但是 kill
还可以发送其他信号,这些信号的定义可以通过 man 7 signal
查看到,也可以通过 kill -l
列出来。
暂停某个进程
例如,用 kill
命令发送 SIGSTOP
信号给某个程序,让它暂停,然后发送 SIGCONT
信号让它继续运行。
可见 kill
命令提供了非常好的功能,不过它只能根据进程的 ID
或者作业来控制进程,而 pkill
和 killall
提供了更多选择,它们扩展了通过程序名甚至是进程的用户名来控制进程的方法。更多用法请参考它们的手册。
查看进程退出状态
当程序退出后,如何判断这个程序是正常退出还是异常退出呢?还记得 Linux 下,那个经典 hello world
程序吗?在代码的最后总是有条 return 0
语句。这个 return 0
实际上是让程序员来检查进程是否正常退出的。如果进程返回了一个其他的数值,那么可以肯定地说这个进程异常退出了,因为它都没有执行到 return 0
这条语句就退出了。
那怎么检查进程退出的状态,即那个返回的数值呢?
在 Shell
中,可以检查这个特殊的变量 $?
,它存放了上一条命令执行后的退出状态。
貌似返回 0 成为了一个潜规则,虽然没有标准明确规定,不过当程序正常返回时,总是可以从 $?
中检测到 0,但是异常时,总是检测到一个非 0 值。这就告诉我们在程序的最后最好是跟上一个 exit 0
以便任何人都可以通过检测 $?
确定程序是否正常结束。如果有一天,有人偶尔用到你的程序,试图检查它的退出状态,而你却在程序的末尾莫名地返回了一个 -1
或者 1,那么他将会很苦恼,会怀疑他自己编写的程序到底哪个地方出了问题,检查半天却不知所措,因为他太信任你了,竟然从头至尾都没有怀疑你的编程习惯可能会与众不同!
进程通信
为便于设计和实现,通常一个大型的任务都被划分成较小的模块。不同模块之间启动后成为进程,它们之间如何通信以便交互数据,协同工作呢?在《UNIX 环境高级编程》一书中提到很多方法,诸如管道(无名管道和有名管道)、信号(signal
)、报文(Message
)队列(消息队列)、共享内存(mmap/munmap
)、信号量(semaphore
,主要是同步用,进程之间,进程的不同线程之间)、套接口(Socket
,支持不同机器之间的进程通信)等,而在 Shell 中,通常直接用到的就有管道和信号等。下面主要介绍管道和信号机制在 Shell 编程时的一些用法。
无名管道(pipe)
在 Linux 下,可以通过 |
连接两个程序,这样就可以用它来连接后一个程序的输入和前一个程序的输出,因此被形象地叫做个管道。在 C 语言中,创建无名管道非常简单方便,用 pipe
函数,传入一个具有两个元素的 int
型的数组就可以。这个数组实际上保存的是两个文件描述符,父进程往第一个文件描述符里头写入东西后,子进程可以从第一个文件描述符中读出来。
如果用多了命令行,这个管子 |
应该会经常用。比如上面有个演示把 ps
命令的输出作为 grep
命令的输入:
也许会觉得这个“管子”好有魔法,竟然真地能够链接两个程序的输入和输出,它们到底是怎么实现的呢?实际上当输入这样一组命令时,当前 Shell 会进行适当的解析,把前面一个进程的输出关联到管道的输出文件描述符,把后面一个进程的输入关联到管道的输入文件描述符,这个关联过程通过输入输出重定向函数 dup
(或者 fcntl
)来实现。
有名管道(named pipe)
有名管道实际上是一个文件(无名管道也像一个文件,虽然关系到两个文件描述符,不过只能一边读另外一边写),不过这个文件比较特别,操作时要满足先进先出,而且,如果试图读一个没有内容的有名管道,那么就会被阻塞,同样地,如果试图往一个有名管道里写东西,而当前没有程序试图读它,也会被阻塞。下面看看效果。
这里的 echo
和 cat
是两个不同的程序,在这种情况下,通过 echo
和 cat
启动的两个进程之间并没有父子关系。不过它们依然可以通过有名管道通信。
这样一种通信方式非常适合某些特定情况:例如有这样一个架构,这个架构由两个应用程序构成,其中一个通过循环不断读取 fifo_test
中的内容,以便判断,它下一步要做什么。如果这个管道没有内容,那么它就会被阻塞在那里,而不会因死循环而耗费资源,另外一个则作为一个控制程序不断地往 fifo_test
中写入一些控制信息,以便告诉之前的那个程序该做什么。下面写一个非常简单的例子。可以设计一些控制码,然后控制程序不断地往 fifo_test
里头写入,然后应用程序根据这些控制码完成不同的动作。当然,也可以往 fifo_test
传入除控制码外的其他数据。
应用程序的代码
控制程序的代码
一个程序通过管道控制另外一个程序的工作
这样一种应用架构非常适合本地的多程序任务设计,如果结合 web cgi
,那么也将适合远程控制的要求。引入 web cgi
的唯一改变是,要把控制程序 ./control.sh
放到 web
的 cgi
目录下,并对它作一些修改,以使它符合 CGI
的规范,这些规范包括文档输出格式的表示(在文件开头需要输出 content-tpye: text/html
以及一个空白行)和输入参数的获取 (web
输入参数都存放在 QUERY_STRING
环境变量里头)。因此一个非常简单的 CGI
控制程序可以写成这样:
在实际使用时,请确保 control.sh
能够访问到 fifo_test
管道,并且有写权限,以便通过浏览器控制 app.sh
:
问号 ?
后面的内容即 QUERY_STRING
,类似之前的 $1
。
这样一种应用对于远程控制,特别是嵌入式系统的远程控制很有实际意义。在去年的暑期课程上,我们就通过这样一种方式来实现马达的远程控制。首先,实现了一个简单的应用程序以便控制马达的转动,包括转速,方向等的控制。为了实现远程控制,我们设计了一些控制码,以便控制马达转动相关的不同属性。
在 C 语言中,如果要使用有名管道,和 Shell 类似,只不过在读写数据时用 read
,write
调用,在创建 fifo
时用 mkfifo
函数调用。
信号(Signal)
信号是软件中断,Linux 用户可以通过 kill
命令给某个进程发送一个特定的信号,也可以通过键盘发送一些信号,比如 CTRL+C
可能触发 SGIINT
信号,而 CTRL+\
可能触发 SGIQUIT
信号等,除此之外,内核在某些情况下也会给进程发送信号,比如在访问内存越界时产生 SGISEGV
信号,当然,进程本身也可以通过 kill
,raise
等函数给自己发送信号。对于 Linux 下支持的信号类型,大家可以通过 man 7 signal
或者 kill -l
查看到相关列表和说明。
对于有些信号,进程会有默认的响应动作,而有些信号,进程可能直接会忽略,当然,用户还可以对某些信号设定专门的处理函数。在 Shell 中,可以通过 trap
命令(Shell 内置命令)来设定响应某个信号的动作(某个命令或者定义的某个函数),而在 C 语言中可以通过 signal
调用注册某个信号的处理函数。这里仅仅演示 trap
命令的用法。
类似地,如果设定信号 0 的响应动作,那么就可以用 trap
来模拟 C 语言程序中的 atexit
程序终止函数的登记,即通过 trap signal_handler SIGQUIT
设定的 signal_handler
函数将在程序退出时执行。信号 0 是一个特别的信号,在 POSIX.1
中把信号编号 0 定义为空信号,这常被用来确定一个特定进程是否仍旧存在。当一个程序退出时会触发该信号。
作业和作业控制
当我们为完成一些复杂的任务而将多个命令通过 |,\>,<, ;, (,)
等组合在一起时,通常这个命令序列会启动多个进程,它们间通过管道等进行通信。而有时在执行一个任务的同时,还有其他的任务需要处理,那么就经常会在命令序列的最后加上一个&,或者在执行命令后,按下 CTRL+Z
让前一个命令暂停。以便做其他的任务。等做完其他一些任务以后,再通过 fg
命令把后台任务切换到前台。这样一种控制过程通常被成为作业控制,而那些命令序列则被成为作业,这个作业可能涉及一个或者多个程序,一个或者多个进程。下面演示一下几个常用的作业控制操作。
创建后台进程,获取进程的作业号和进程号
把作业调到前台并暂停
使用 Shell 内置命令 fg
把作业 1 调到前台运行,然后按下 CTRL+Z
让该进程暂停
查看当前作业情况
启动停止的进程并运行在后台
不过,要在命令行下使用作业控制,需要当前 Shell,内核终端驱动等对作业控制支持才行。
参考资料
Last updated