1 进程基础
1.1 概念
- 定义
程序在计算机上的一次执行过程,执行中的程序。 - 本质
1、程序在地址空间中按照代码逻辑控制流执行
2、资源分配最小单位
进程是一个抽象概念
1.2 进程与程序
- 区别
进程 | 程序 |
---|---|
动态 | 静态 |
有生命周期 | 指令集合 |
只能对应一个程序 | 可以对应多个进程 |
- 从代码到程序
- 从程序到进程
1、内核将程序读入内存,为程序镜像分配内存空间。
2、内核为该进程分配进程标志符PID。
3、内核为该进程保存PID及相应的进程状态信息。
进程控制块(PCB):保存进程控制信息
1.3 进程状态
- 三态
1、运行(running)态:程序正在CPU上执行
2、就绪(ready)态:进程已获得到除CPU以外的所有必要的资源,获得CPU立即执行
3、等待(wait)态:又称为阻塞(blocked)态或睡眠(sleep)态,指等待某个事件发生而无法执行时,放弃CPU - 五态
1、创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
2、就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行
3、执行状态:进程处于就绪状态被调度后,进程进入执行状态
4、阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用
5、终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
1.3.1 查看进程状态
查看进程的命令
OS | 命令 | e.g. |
---|---|---|
Windows | tasklist | tasklist /FI "PID eq 进程PID" |
Linux | ps / pstree / top | - |
1、ps
命令
- 查看某进程
(1)通过进程PID查看:ps -p 进程PID
[root@localhost 1]# ps -p 3179
PID TTY TIME CMD
3179 pts/0 00:04:08 a.out
(2)通过命令行查看:ps -C 命令行
[root@localhost 1]# ps -C a.out
PID TTY TIME CMD
3179 pts/0 00:02:19 a.out
(3)查看程序的PID: pidof 程序名
[root@localhost 1]# pidof a.out
3179
- 查看进程
风格 | 命令 | 属性说明 |
---|---|---|
BSD风格 | ps aux | a : 终端上所有用户的进程; u :以用户为中心显示详细信息, x :无终端进程 |
System V风格 | ps -ef | e :所有进程; f :树状显示 |
- 列表示说明
ps aux
ps -ef
标识 | 含义 |
---|---|
USER | 用户 |
PID | 进程ID |
%CPU | 进程占用的CPU百分比 |
%MEM | 占用内存的百分比 |
VSZ | 进程虚拟大小 |
RSS | 常驻内存(内存中页的数量) |
TTY | 终端ID |
STAT | 进程状态 |
START | 启动进程的时间 |
TIME | 进程消耗CPU的时间 |
COMMAND | 命令的名称和参数 |
- 进程状态标识
标识 | 含义 |
---|---|
D | 不可中断Uninterruptible(usually IO) |
R | 正在运行,或在队列中的进程 |
S | 处于休眠状态 |
T | 停止或被追踪 |
Z | 僵尸进程 |
W | 进入内存交换(从内核2.6开始无效) |
X | 死掉的进程 |
< | 高优先级 |
n | 低优先级 |
s | 包含子进程 |
+ | 位于后台的进程组 |
2、pstree
命令
以树状图的方式展现进程之间的派生关系
需要安装
yum install psmisc
3、 top
命令
实时显示系统中各个进程的资源占用,类似Windows任务管理器。
1.3.2 获取PID
进程标识pid
:进程身份证号
函数 | 接口 |
---|---|
pid_t getpid() | 获取当前进程ID |
pid_t getppid() | 获取当前进程父进程ID |
实例:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
cout << getpid() << " " << getppid() << endl;//父进程创建的子进程
}
[root@localhost 1]# ps
PID TTY TIME CMD
2837 pts/0 00:00:00 bash
3758 pts/0 00:00:00 ps
[root@localhost 1]# ./a.out
3766 2837
可以看到bash
的pid
为2837
,而进程a.out
的ppid
也是2837
,是因为bash
创建的a.out
1.3.3 如何杀死进程
OS | 命令 |
---|---|
Windows | taskkill /F /PID 进程标识/taskkill /F /IM 程序名 |
Linux | kill 进程标识PID |
1、执行死循环
#include <iostream>
using namespace std;
int main(){
for(;;){}
}
2、Crtl+z可以停止进程,bg
可以打入后台执行,fg
可以把执行程序放入前端
[root@localhost 1]# ./a.out
^Z
[1]+ Stopped ./a.out
[root@localhost 1]# bg
[1]+ ./a.out &
[root@localhost 1]# fg
./a.out
3、放入后端之后执行ps
或者top
可以看到后台执行的a.out
也可以用jobs
可以查看所有后台执行的程序
[root@localhost 1]# bg
[1]+ ./a.out &
[root@localhost 1]# ps
PID TTY TIME CMD
2837 pts/0 00:00:00 bash
3461 pts/0 00:00:26 a.out
3494 pts/0 00:00:00 ps
4、kill
杀死进程
[root@localhost 1]# kill 3461
[1]+ Terminated ./a.out
[root@localhost 1]# ps
PID TTY TIME CMD
2837 pts/0 00:00:00 bash
3508 pts/0 00:00:00 ps
1.4 如何创建进程
1.4.1 分叉函数fork()
- 返回值
返回值 | 含义 |
---|---|
-1 | 失败 |
0 | 子进程逻辑控制流 |
其他(子进程PID) | 父进程逻辑控制流 |
- 特点
(1)调用一次,返回两次
(2)相同但是独立的地址空间
(3)并发(交叉)执行
(4)共享文件
实例1:调用一次,返回两次
#include <stdio.h>
#include <unistd.h>
int main(){
printf("PID:%d,PPID:%d\n",getpid(),getppid());
pid_t pid = fork();
if(pid == 0){// child
printf("this is child\n");
printf("res:%d,PID:%d,PPID:%d\n",pid,getpid(),getppid());
}else{
printf("this is father\n");
printf("res:%d,PID:%d,PPID:%d\n",pid,getpid(),getppid());
}
}
执行结果:
PID:4183,PPID:2837
this is father
res:4184,PID:4183,PPID:2837
this is child
res:0,PID:4184,PPID:2294
分析:父进程pid
为4183
,返回值为4184
,创建了pid
为4184
的子进程,子进程的返回值为0
.
实例2:相同但是独立的地址空间&并发执行
#include <stdio.h>
#include <unistd.h>
int main(){
int j=0;
pid_t pid = fork();
if(pid == 0){// child
int k;
for(k=0;k<150;k++)
printf("this is child: %p\t j%d\n",&j,++j);
}else{
int k;
for(k=0;k<150;k++)
printf("this is father: %p\t j%d\n",&j,--j);
}
}
this is father: 0x7fff15c2a240 j-147
this is father: 0x7fff15c2a240 j-148
this is father: 0x7fff15c2a240 j-149
this is father: 0x7fff15c2a240 j-150
this is child: 0x7fff15c2a240 j1
this is child: 0x7fff15c2a240 j2
this is child: 0x7fff15c2a240 j3
this is child: 0x7fff15c2a240 j4
this is child: 0x7fff15c2a240 j5
...
...
this is child: 0x7fff15c2a240 j147
this is child: 0x7fff15c2a240 j148
this is child: 0x7fff15c2a240 j149
this is child: 0x7fff15c2a240 j150
说明:
1、父子进程访问共同的地址,但是有各自的执行结果,是因为父类为子类创造了一块与父类完全相同的虚拟内存;
2、因执行过程快,看不到并发执行的过程,实际为并发执行。
实例3:共享文件
#include <stdio.h>
#include <unistd.h>
int i = 100;
int main(){
int j=100;
FILE* fd = fopen("./test","w+");
pid_t pid = fork();
if(pid == 0){// child
int k;
for(k=0;k<10;k++)
fprintf(fd,"this is child: i%d\t j%d\n",++i,++j);
}else{
int k;
for(k=0;k<10;k++)
fprintf(fd,"this is father: i%d\t j%d\n",--i,--j);
}
fprintf(fd,"%d\t%d\n",i,j);
}
[root@localhost internet]# ./a.out
[root@localhost internet]# cat test
this is father: i99 j99
this is father: i98 j98
this is father: i97 j97
this is father: i96 j96
this is father: i95 j95
this is father: i94 j94
this is father: i93 j93
this is father: i92 j92
this is father: i91 j91
this is father: i90 j90
90 90
this is child: i101 j101
this is child: i102 j102
this is child: i103 j103
this is child: i104 j104
this is child: i105 j105
this is child: i106 j106
this is child: i107 j107
this is child: i108 j108
this is child: i109 j109
this is child: i110 j110
110 110
- 本质
复制+分叉
概念 | 状态 | 硬件 | 特点 |
---|---|---|---|
并发(concurrency) | 两个或者多个进程在同时存在 | 单核 | 进程指令同时或者交错执行。 |
并行(parallellism) | 两个或者多个进程在同时执行 | 多核 | 一种特殊的并发 |
1.4.2 执行函数exec()
exec()
特点:
- 一次调用,失败返回
- 改朝换代,取而代之
PID不变
地址空间内容变化
实例:
#include <iostream>
#include <unistd.h>
using namespace std;
extern char** environ;
int main(){
//g++ exe.cpp -o exec
//execl("/bin/g++","g++","exec.cpp","-o","exec",0);
//execle("/bin/g++","g++","exec.cpp","-o","exec",0,environ);
//execlp("/g++","g++","exec.cpp","-o","exec",0);
char* cmd[] = {"g++","exec.cpp","-o","exec2",NULL};
//execv("/bin/g++",cmd);
//execve("/bin/g++",cmd,environ);
//execvp("g++",cmd);
execl("/bin/ps","ps",NULL);
cout << getpid() << endl;
return 0;
}
[root@localhost 1]# ./a.out
PID TTY TIME CMD
2837 pts/0 00:00:00 bash
5094 pts/0 00:00:00 ps
which g++
可以查看g++命令所在地址
1.4.3 系统函数system()
- 特点
一次调用,一次返回 - 本质
shell执行命令/程序
实例:
#include <iostream>
#include <unistd.h>
#include <cstdlib>
using namespace std;
int main(){
cout << getpid() << endl;
system("ps -o pid,ppid,cmd,s");
cout << getpid() << endl;
return 3;
}
[root@localhost 1]# g++ system.cpp
[root@localhost 1]# ./a.out
5275
PID PPID CMD S
2837 2828 bash S
5275 2837 ./a.out S
5276 5275 ps -o pid,ppid,cmd,s R
5275
./a.out
的父进程是bash
, ps -o pid,ppid,cmd,s
的父进程是./a.out
1.5 如何结束进程
方式 | 说明 |
---|---|
main 函数退出 | 只能用在main 函数内 |
调用exit() 函数 | 一般用在main 函数以外的函数 |
调用_exit() 函数 | 一般用来结束子进程 |
调用abort() 函数 | 一般用来异常退出 |
信号终止 | 终止其他进程 |
1.6 如何停止进程
1.6.1 休眠
int sleep(unsigned int secs)
- 参数
secs
指定休眠的秒数,-1
表示永久休眠 - 返回值
未休眠的秒数 - 特性
如果没有信号中断,休眠指定秒数返回0
,否则马上返回未休眠的秒数。
实例:计数器
#include <iostream>
#include <unistd.h>
#include <cstdio>
using namespace std;
int main(){
for(int i = 0;i < 10;i++){
//cout << i << endl;
printf("\r%d",i);
fflush(stdout);
sleep(1);
}
}
1.6.2 等待
pid_t wait(int* status):等价pid_t waitpid(-1,stauts,0)
pid_t waitpid(pid_t pid,int * status,int options)
- 参数
参数 | 含义 | 说明 |
---|---|---|
pid | 等待的进程 | 1、<-1 :等待进程组为pid 的所有进程;2、-1 : 等待任何子进程;3、0 :等待同组的进程;4、>0 :进程为pid 的子进程 |
status | 子进程结束状态 | 1、判断正常结束:使用WIFEXITED(status) ;2、判断异常结束使用WIFSIGNALED(status) ;3、判断暂停使用WIFSTOPPED(status) |
options | 选项 | WNOHANG 若子进程没有结束,返回0 ,不予以等待;若子进程结束,返回该子进程的ID。WUNTRACED 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。 |
- 返回值
返回值 | 含义 |
---|---|
-1 | 失败 |
其他 | 等待的PID |
1.7 特殊进程
概念 | 出现条件 | 导致结果 | 是否有害 |
---|---|---|---|
孤儿进程 | 父进程先于子进程退出 | init 进程作为新的父进程 | 无害 |
僵尸进程 | 子进程退出,父进程没有获取子进程的状态信息 | 调用wait 或waitpid | 有害,避免出现僵尸进程 |
1.7.1 僵尸进程
实例:
1、子进程执行完先退出,父进程一直执行
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
pid_t pid = fork();
if(pid == 0){
cout << "Child:" << getpid() << endl;
}else{
for(;;){}
}
}
2、执行,Crtl+z停止,执行bg
打入后台,查看进程状态
[root@localhost fork]# ./a.out
Child:5714
^Z
[1]+ Stopped ./a.out
[root@localhost fork]# bg
[1]+ ./a.out &
[root@localhost fork]# ps
PID TTY TIME CMD
2837 pts/0 00:00:00 bash
5713 pts/0 00:00:09 a.out
5714 pts/0 00:00:00 a.out <defunct>
5728 pts/0 00:00:00 ps
[root@localhost fork]# ps -o pid,ppid,cmd,s
PID PPID CMD S
2837 2828 bash S
5713 2837 ./a.out R
5714 5713 [a.out] <defunct> Z
5750 2837 ps -o pid,ppid,cmd,s R
3、此时子进程为僵尸进程,无法通过kill pid
杀死
[root@localhost fork]# kill 5714
[root@localhost fork]# ps -o pid,ppid,cmd,s
PID PPID CMD S
2837 2828 bash S
5713 2837 ./a.out R
5714 5713 [a.out] <defunct> Z
5827 2837 ps -o pid,ppid,cmd,s R
4、只能通过杀死父进程来杀掉僵尸进程
[root@localhost fork]# kill 5713
[1]+ Terminated ./a.out
[root@localhost fork]# ps -o pid,ppid,cmd,s
PID PPID CMD S
2837 2828 bash S
5848 2837 ps -o pid,ppid,cmd,s R
如何避免僵尸进程?
1、父进程等待子进程退出
实例:父进程加入wait()
,帮助回收子进程
#include <iostream>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main(){
pid_t pid = fork();
if(pid == 0){
cout << "Child:" << getpid() << endl;
}else{
for(;;){
sleep(1);
wait(NULL);
}
}
}
[root@localhost fork]# ./a.out
Child:6121
^Z
[1]+ Stopped ./a.out
[root@localhost fork]# bg
[1]+ ./a.out &
[root@localhost fork]# ps -o pid,ppid,cmd,s
PID PPID CMD S
2837 2828 bash S
6120 2837 ./a.out S
6135 2837 ps -o pid,ppid,cmd,s R
实例2:
#include <iostream>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
//子进程退回后父进程还在跑,占用资源,会出现僵尸进程,无法用KILL杀掉,只能通过杀掉父进程。
//如何避免僵尸进程?使用wait(NULL);
int main(){
cout << getpid() << endl;
pid_t pid = fork();
cout << pid << endl;
if(pid == 0){
sleep(5);
cout << "child:"<< getpid() << endl;
}else{
cout << "before wait" << endl;
wait(NULL);
cout << "end wait" << endl;
}
}
[root@localhost fork]# ./a.out
6261
6262
before wait
0
child:6262
end wait
执行过程:
1、打印父进程pid
,子进程和父进程同时执行,执行fork()
后,打印父进程的返回值(即子进程的pid
),父进程打印before wait
,子进程打印返回值(即0
)。
2、子进程等待5s,子进程打印child:pid
后结束。父进程执行wait(NULL)
杀掉僵尸进程,打印end wait
,父进程结束。
注:一般调用fork(),都要用wait();
2、更加安全的方式
实例:
#include <iostream>
#include <list>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
int main(){
cout << getpid() << endl;
list<pid_t> pids;
pid_t pid = -1;
for(int i = 0;i < 10;i++){
if(pid == 0){
sleep(5);
cout << "pid:"<< getpid() << endl;
}else{
pid = fork();
pids.push_front(pid);
}
}
if(pid != 0){
for(auto pid:pids){
cout << "before wait:"<< pid << endl;
waitpid(pid,NULL,0);
cout << "end wait:" << pid << endl;
}
pause();
}
}
自动调用handler
杀掉僵尸进程
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;
void handler(int){
pid_t pid = wait(NULL);
printf("%d exit\n",pid);
}
int main(){
signal(SIGCHLD,handler); //当一个子进程结束后,系统自动调用handler杀掉僵尸进程
pid_t pid = -1;
for(int i = 0;i < 10;i++){
if(pid == 0){
sleep(5);
printf("pid:%d\n",getpid());
}else{
pid = fork();
}
}
}
运行过程:第一编循环pid!=0生成一个子进程和一个父进程,第二次循环,父进程pid=0继续生成子进程,子进程等待5s输出,然后继续下一次循环。最后一次循环父进程一共生成9个子进程,每一个子进程结束都会调用handler函数收尸。
注:子进程退出时候,会给到SIGCHLD
一个信号,父进程执行handler
自动收尸
1.7.2 孤儿进程
实例1:孤儿进程
#include <iostream>
#include <unistd.h>
using namespace std;
//子进程在跑,而父进程提前退出,会出现孤儿进程
int main(){
if(fork()){
cout << "parent:" << getpid() << endl;
sleep(10);
cout << "parent:" << getpid() << "exit" << endl;
}else{
cout << "child:" << getpid() << endl;
sleep(50);
cout << "child:" << getpid() << "exit" << endl;
}
}
[root@localhost fork]# ./a.out
parent:8433
child:8434
parent:8433exit
[root@localhost fork]# ps
PID TTY TIME CMD
2837 pts/0 00:00:00 bash
8434 pts/0 00:00:00 a.out
8458 pts/0 00:00:00 ps
父进程退出,子进程还在跑,出现孤儿进程,孤儿进程无害,最终会被回收。
实例2:孤儿进程
#include <iostream>
#include <unistd.h>
using namespace std;
//孤儿进程无害,僵尸进程有害
int main(){
pid_t pid = -1;
for(int i = 0;i < 10;i++){
if(pid == 0){
sleep(5);
cout << i << " child pid: " << getpid() << endl;
}else{
pid = fork();
}
}
}
7 child pid: 8554
8 child pid: 8555
9 child pid: 8556
6 child pid: 8553
8 child pid: 8554
9 child pid: 8555
7 child pid: 8553
9 child pid: 8554
8 child pid: 8553
9 child pid: 8553
1.8 进程间的通信(Inter-Process Communication,IPC
)方式
- (1)管道:
它是半双工的,具有固定的读端和写端;它只能用于父子进程或者兄弟进程之间的进程的通信;
它可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。 - (2)FIFO:
可以在无关的进程之间交换数据,与无名管道不同; - (3)消息队列
基本原理:
A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。
特点:
消息队列是保存在内核中的消息链表,每个消息体都是固定⼤⼩的存储块。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
如果没有释放消息队列或者没有关闭操作系统,消息队列会⼀直存在。 - (4)信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点:
1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
4)支持信号量组。 - (4)共享内存
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区;
共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。