进程
进程就是“运行中的程序”。程序是存储于外存上的静态实体,如果从来没有运行,程序没有任何意义;程序(实际上是部分程序)被放到内存运行,这时它成为了一个活动实体,成为进程。操作系统会为每一个进程分配一个数据结构,称之为进程控制块(PCB),用于进程相关信息的存储,以及用于进程调度。在操作系统出现之前,程序直接运行于裸机之上。那时硬件资源的利用率不高,程序编写要事无巨细亲历亲为,更别提对硬件资源的保护和安全性。操作系统为程序的运行提供了大量便利,可以说,操作系统就是为了运行程序而存在,即为了进程而存在。因此,进程是操作系统中首屈一指的概念。
ps命令
ps(process status/snapshot)
命令用于显示系统中进程的当前状态的快照。
当然,此时只显示前台的进程。如果想查看全部进程,需要使用-e
选项。
此时显示的进程过多,可能需要用more分页。
-l
选项可以查看更多细节,也常常同-e
选项合用。
![image-20211204161441034](https://i-blog.csdnimg.cn/blog_migrate/14e5e4fa795a566055f40b753c86a15f.png)
分别介绍一下每一列的含义:
-
F
:不再使用 -
S
:进程状态。R
表示真在运行或是可运行;S
表示休眠;T代表停止;Z
代表僵尸进程 -
UID
:该进程的用户(即该进程的所有者——往往是该进程的)的ID。 -
PID
:(process ID)进程ID -
PPID
:(parent process ID) 父进程ID -
C
:CPU占用率 -
PRI
:优先级,数字越小优先级越高,内核动态修改,用户无权修改 -
NI
:Nice number,即niceness级别,含义是“对别人友善”——即降低自己的优先级。取值范围是 -20~19,默认为0。PRI (new)=PRI (old) + nice number
。可以用nice
命令和renice
命令对nice number进行调整,从而间接地调整进程优先级(PRI)。需要注意的是,只有超级用户才能降低nice number数(从而提高优先级),而其他用户只能提高nice number。 -
ADDR
:不再使用。 -
SZ
:进程占用内存大小 -
WCHAN
:(wait channel)进程等待的事件是什么 -
TTY
:进程用的终端的设备编号 -
CMD
:进程对应的程序名
以上的用法是System V的形式。另外,ps命令还支持BSD系统的命令形式,其中最重要的命令形式就是
ps aux
运行效果如下
![image-20211204161617182](https://i-blog.csdnimg.cn/blog_migrate/70b874cecc8b15236b6659167cf6b70b.png)
top命令
除了ps
,top
命令也是最常使用的监视进程的命令,它更像windows中的任务管理器。在终端输入
top
![image-20211204204853186](https://i-blog.csdnimg.cn/blog_migrate/0c68ddf10c1fdb67c66cf1ffd63a5251.png)
/proc文件系统
Linux和大部分的Unix系统都支持/proc文件系统。这是个伪文件系统,其中的文件实际上是内核内存及其数据结构的视图(view)。
ls /proc
![image-20211204205041656](https://i-blog.csdnimg.cn/blog_migrate/cc5e3222ec9e24022e27ca337d14bbef.png)
/proc
下的以数字编号的目录,都是以对应进程的PID命名。这样,每个进程的信息就通过目录分隔开来。
/proc
文件系统中包含的每个进程的信息可以使用ps
等命令进行查看。由于大部分文件都是文本文件,所以也可以直接用文本编辑器打开。
vi /proc/3798/status
可以查看进程3798的状态。
此外,也可以通过
vi /proc/cpuinfo
查看CPU的信息
![image-20211204205339778](https://i-blog.csdnimg.cn/blog_migrate/b3095cf7e59f6b34506e567715ea4f25.png)
strace命令
使用strace
命令可以实时显示一个进程所有的系统调用。这使得一个进程的行为一览无余。也可以通过-p
选项strace一个已经运行的进程。
![image-20211204205843086](https://i-blog.csdnimg.cn/blog_migrate/9ef172721b7d0ab32b34ab57007a9623.png)
strace命令的很多选项都非常有用。比如,-f
选项用于跟踪被fork出的进程,可用于监视守护进程(daemon)比如httpd
。-e file
选项只显示文件操作。
进程创建
进程从哪里来?
“进程从哪里来?”这一问题与以下问题相似:“小猫从哪里来?小狗从哪里来?”猫是猫妈妈生的;狗是狗妈妈生的;进程是进程它妈生的——它叫parent process
。如果存在进程间的这种创建与被创建的关系,分别称之为parent process(父进程)
和child process(子进程)
。
fork()
一个进程可以通过系统调用fork()创建子进程。fork的意思是分叉,一个变成两个。
例:使用fork()创建子进程
#include <stdio.h>
main(){
printf("I am %d\n", getpid());
fork();
printf("I am %d\n", getpid());
}
getpid()
可以得到该进程自己的pid。该程序运行结果为:
![image-20211204210218352](https://i-blog.csdnimg.cn/blog_migrate/5741387b9a32143c4cde241fd49bee40.png)
很显然,程序最开始执行时,只有一个进程3450。而在fork()
之后,除了最开始的3450之外,还出现了一个新的进程3451。
在传统的Unix模型中,当一个进程调用fork()
后,会创建一个新的进程,这个新的进程称为原来进程的子进程,此时原来的进程称为父进程。子进程会复制父进程的一切,包括代码段、变量、打开文件的文件描述符等等——当然,由于Linux采用写时拷贝(copy on write),所以子进程并不是真的把父进程完全拷贝了。但对于程序员来说,逻辑上可以认为是子进程是父进程的完全复制,甚至是IP寄存器,即无论父子进程,都会从fork()的下一条指令开始执行。父子进程唯一不同的地方,就是fork()的返回值——父进程的fork()返回的是子进程的pid;而子进程未曾调用过fork,形式上得到的返回值为0。因此,我们可以通过fork()的返回值来判断当前进程是父进程还是子进程,尽管它们拥有相同的代码。
例:通过fork()的返回值来区分父子进程。
#include <stdio.h>
#include <stdlib.h>
main(){
int rv;
printf("I am parent, I'm %d\n", getpid());
if((rv=fork())==-1){
perror("cannot fork");
exit(1);
}
if(0==rv){/*child*/
printf("I am child, I'm %d\n", getpid());
}else{/*parent*/
printf("I am parent, I'm %d, my child is %d\n", getpid(),rv);
}
}
运行结果:
![image-20211204210539105](https://i-blog.csdnimg.cn/blog_migrate/755be6b905d052176acebf8436bf3642.png)
多进程编程与之前的经验的两点显著不同:
其一,父进程和子进程的代码都写到了同一个程序里。但一般都是父进程运行父进程的代码,子进程运行子进程的代码,互不干扰。这有点像合同,甲乙双方签订同一份合同,合同里分别包含甲和乙的权责,甲和乙分别履行自己的权责,而不会履行对方的权责。
其二,当子进程被创建后,父子进程彼此独立,同时运行,共同竞争操作系统资源。此时,如没有特殊的编程限定,父子进程执行的先后顺序取决于操作系统的进程调度。因而,多进程并发执行时,其运行结果对于程序员来说,似乎是随机的(但事实上对于一个给定的系统,调度策略是固定的,运行结果往往是一定的)。这就导致了多进程编程的不确定性,诸如进程同步等问题都可能接踵而来。
进程如何运行一个程序?
exec
函数族是一系列以exec
开头的库函数,exec
代表execute a file
——执行一个可执行文件。比较常用的是execvp
和execlp
。
例:使用execlp运行ls /。
#include <stdio.h>
#include <unistd.h>
main(){
printf("about to ls\n");
execlp("ls","ls","/",NULL);
printf("OK\n");
}
运行结果:
![image-20211204211051703](https://i-blog.csdnimg.cn/blog_migrate/7c16e92a6ffb648d994367058f8a5993.png)
通过execlp
执行了ls /
这一程序,结果也无误。但问题是我们注意到程序中最后一行printf("OK\n");
并没有被执行。
原因很简单,当我们试图运行ls程序时,这时候我们需要生成一个进程包括进程所拥有的内存等资源去加载ls程序的指令,但很显然exec
函数并没有创建新进程的能力,而不得不将自己代码替换为ls的代码。因此,当ls /
开始执行时,最后的printf("OK\n");
和其他的代码都已不复存在了。
解决方案是,在exec
之前先fork
,让子进程去exec
。这样我们才能保留父进程的代码。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main(){
int rv;
printf("About to ls\n");
if((rv=fork())==-1){
perror("cannot fork");
exit(1);
}
if(0==rv){/*child*/
execlp("ls","ls","/",NULL);
}else{/*parent*/
printf("OK\n");
}
}
运行结果:
![image-20211204211404603](https://i-blog.csdnimg.cn/blog_migrate/afcef82968c28992e05bfaaa50e33daa.png)
我们注意到,尽管“OK”已经被打印出来了,但我的期望是先“ls”,再打印“OK”。这是我们需要父子进程间的协作。wait()系统调用可以让父进程一直等待,直到子进程运行结束。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main(){
int rv;
printf("About to ls\n");
if((rv=fork())==-1){
perror("cannot fork");
exit(1);
}
if(0==rv){/*child*/
execlp("ls","ls","/",NULL);
}else{/*parent*/
wait(NULL);
printf("OK\n");
}
}
运行结果:
![image-20211204211554021](https://i-blog.csdnimg.cn/blog_migrate/29a9d282cefec6c525ecae01588ed898.png)
wait()
wait()
系统调用除了等待子进程结束之外,它还能释放子进程资源——主要是指子进程的退出状态。C99规定主函数main
的返回值必须是int
类型,就是说main
函数一定要return一个整数。这个整数就是进程的退出状态,是一个8 bit的无符号整数,其取值范围是0~255。也可以通过exit()
函数将这个退出状态值返回。当一个进程运行结束后,其退出状态会一直被保留,此时的进程成为了一种称为僵尸进程(zombie)的状态,直到它的父进程用wait()
系统调用查看该进程或父进程运行结束。另外,子进程也可能被信号杀死,父进程也可以通过wait()
得知是哪个信号杀死子进程的。
当子进程退出时(或被信号杀死)父进程之所以能够立即知道,是因为在子进程退出时,父进程会接收到SIGCHLD信号。
先看一下man 2 wait
![image-20211204211958128](https://i-blog.csdnimg.cn/blog_migrate/07b83d119e5e38e3b793a0e1aadb3311.png)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main(){
int rv;
int status;
if((rv=fork())==-1){
perror("cannot fork");
exit(1);
}
if(0==rv){/*child*/
sleep(10);
exit(10);
}else{/*parent*/
wait(&status);
printf("%d\t%d\n", status>>8, status&0x7F);
}
}
运行结果:
第一次运行:我们用信号9杀死子进程,父进程能够打印出该信号数,由于子进程没有运行结束就被杀死,它的exit value是0。
![image-20211205084425179](https://i-blog.csdnimg.cn/blog_migrate/d4cdc49f4562c66dd283d1ab643cd9c2.png)
第二次运行:我们没有杀死子进程,等子进程通过exit(10)运行结束,父进程可以得到其exit value。
![image-20211205084513468](https://i-blog.csdnimg.cn/blog_migrate/fca52671eb1f9aebeeb581d0b1a74a09.png)
僵尸进程
首先编译运行以下代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main(){
int rv;
int status;
if((rv=fork())==-1){
perror("cannot fork");
exit(1);
}
if(0==rv){/*child*/
exit(10);
}else{/*parent*/
sleep(100);
}
}
运行结果:
![image-20211205084742600](https://i-blog.csdnimg.cn/blog_migrate/99de0454cd4aa6573cbde1c9a5be3387.png)
子进程运行结束后,大部分资源都会被释放,但exit value
会保存下来,此时子进程成为了一个僵尸进程(zombie)
,直到它的父进程用wait()系统调用查看该进程或父进程运行结束才能消失。在进行多进程编程时,类似于httpd(Apache HTTP Server)
这种程序,如果不对僵尸进程进行处理,很可能就会出现大量的僵尸进程。
由于当子进程退出时(或被信号杀死),父进程会接收到SIGCHLD
信号。当我们设置父进程忽略该信号时,则系统不会保留僵尸进程,则这时不再会出现僵尸进程了。
例:通过忽略SIGCHLD
消除僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
main(){
int rv;
int status;
if((rv=fork())==-1){
perror("cannot fork");
exit(1);
}
if(0==rv){/*child*/
exit(10);
}else{/*parent*/
signal(SIGCHLD, SIG_IGN);
sleep(100);
}
}
运行结果:
![image-20211205085129112](https://i-blog.csdnimg.cn/blog_migrate/9bf4ec91ac86efa364f563ba305feb03.png)
此时子进程直接退出,而不再作为僵尸进程存在了。
孤儿进程
编译运行下面的程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main(){
int rv;
int status;
if((rv=fork())==-1){
perror("cannot fork");
exit(1);
}
if(0==rv){/*child*/
sleep(100);
}else{/*parent*/
exit(0);
}
}
运行结果:
![image-20211205085640130](https://i-blog.csdnimg.cn/blog_migrate/1e952a56f4ec60cf934bb1b04033450a.png)
任何一个进程都必须有它的父进程。示例程序父进程先于子进程结束。此时进程12954成为了一个孤儿进程,此时1号进程(即init进程)会接收孤儿进程,并负责wait()这些孤儿进程。