目录
一、基础知识
1.1、进程的概念
1.2、多进程(任务)并行的实现
1.3、重要指令
1.4、父子进程和进程ID
二、多进程编程
2.1创建子进程 (fork/vfork 叉子)
2.1.1 fork
2.1.2 vfork
2.2进程结束
2.2.1正常退出
2.2.2异常退出
2.3等待进程结束并资源回收
2.3.1 wait函数
2.3.2 waitpid 函数
2.4替换进程内容(exec系列函数)
进程我们了解了,其实就可以说是一个后台运行的任务
而多进程(多任务)并行就好比如我们的电脑可以上谷歌浏览器、微信、网易云音乐
它们都是一个个任务,在执行各自的功能
看上去它们“同时”一起运行
对吧~
就绪状态: 未占到CPU, 进程准备好了,等待系统调度器调度。
运行状态: 占到CPU , 已经开始运行。
暂停状态: 没占,收到外部暂停信号,暂停运行 (不在参与任务调度)
挂起(睡眠)状态: IO资源不满足, 导致进程睡眠。 (不在参与任务调度)(例如键盘输入)
僵尸状态: 进程已经结束, 但是资源(内存、硬件接口)没有回收。
(1) ps ——— 查看进程信息
ps aux ——— 显示系统所有的进程
ps -elf ——— 显示系统所有的进程(通用)
(2)top ———— 动态查看进程信息
(3)pstree ———— 查看父子关系结构的进程
(4)kill -9 进程号 ——— 杀死进程。(通过信号)
Linux中的进程都是由其它进程启动。如果进程a启动了进程b, 所以称a是b的父进程, b是a的子进程
Linux启动时,0进程启动1号进程(init )和2号进程(内核线程), 0号进程退出, 其它进程是由1、2直接或间接产生
1号进程(init ) 是所有用户进程的祖先
2号进程(内核线程) 是内核进程的祖先
进程号 PID (process ID)(类型pid_t , 什么什么_t 都是正整数) :
每个任务拥有唯一ID, 由操作系统管理和分配
每个进程创建会分配一个ID , 结束会取消一个ID
取消的那个ID会延时重复使用 , 但不会同时出现同一个ID
相关函数:
函数getpid() 获取本进程的ID
函数getppid() 获取父进程的ID (get perent pid)
(小彩蛋) 想让程序在后台运行执行指令
./执行文件 &
相信大家有用过叉子,它有“一分为二”的形状
而我们今天要介绍的是fork函数 , 它也有这样的能力
调用fork函数时
复制父进程的进程空间来创建子进程
上图是父进程的进程空间,其中代码段是不会不复制到子进程的,而是共享
其它段需要复制,属于写拷贝 (即只有改的时候, 才需要拷贝)
这样提高效率, 节省资源
总而言之,相当于克隆了一个自己
现在我们要让它们分别干不同的事
在fork函数执行完毕后
则有两个进程,一个是子进程,一个是父进程
在子进程中,fork函数返回0,在父进程中,fork返回子进程的进程ID
因此, 我们可以通过fork返回的值来判断当前进程是子进程还是父进程
从而让它们同时干不同的事情
示例如下(fork):
#include
#include
#include
int main()
{
printf("+++process %d start running!ppid = %d\n",getpid(),getppid());
pid_t pid = fork();
if(pid)//父进程
{
printf("parent:process %d start running!ppid = %d\n",getpid(),getppid());
//do something
//...
}
else//子进程
{
printf("child:process %d start running!ppid = %d\n",getpid(),getppid());
//do something
//...
exit(0);
}
exit(0);
}
注意⚠️:如果父进程提前结束,那么子进程就会变成孤儿进程 (知识点在下一节2.3)
另外还有一个函数vfork
但是子进程和父进程是不能同时运行的
由于函数不复制父进程的进程空间, 而是抢占父进程的资源, 导致父进程堵塞, 无法继续运行
子进程完成后, 父进程才能继续运行
示例如下(vfork):
#include
#include
#include
#include
int main()
{
printf("+++process %d start running!ppid = %d\n",getpid(),getppid());
pid_t pid = vfork();
if(pid){//父进程
printf("parent:process %d start running!ppid = %d\n",getpid(),getppid());
printf("parent:process %d finish running!ppid = %d\n",getpid(),getppid());
}
else{//子进程
printf("child:process %d start running!ppid = %d\n",getpid(),getppid());
printf("child:process %d finsish running!ppid = %d\n",getpid(),getppid());
exit(0);
}
exit(0);
}
学会创建子进程后
我们来学习一下进程结束
不管是子进程还是父进程
进程的退出分为正常退出和异常退出
进程的正常退出有四种,如下:
1、return 只是代表函数的结束, 返回到函数调用的地方。
2、进程的所有线程都结束。
3、exit() 代表整个进程的结束,无论当前执行到哪一行代码, 只要遇到exit() , 这个进程就会马上结束。
4、 _exit() 或者 _Exit() 是系统调用函数。
_exit() / _Exit 和 exit 的区别:
_exit() / _Exit 是 系统调用函数, exit 是库函数
exit 它是通过调用_exit()来实现退出的
但exit() 多干了两件事情: 清空缓冲区、调用退出处理函数
退出处理函数:
进程正常退出,且调用exit()函数,会自动调用退出处理函数
退出处理函数可以做一些清理工作
需要先登记才生效,退出处理函数保存在退出处理函数栈中(先进后出的原则)
示例如下(退出处理函数):
#include
#include
#include
void func1(void)
{
printf("%s\n",__func__);
}
void func2(void)
{
printf("%s\n",__func__);
}
void func3(void)
{
printf("%s\n",__func__);
}
int main()
{
atexit(func1);//先登记
atexit(func2);
atexit(func3);
printf("hello!");
exit(0);
//_exit(0); //无法调用退出处理函数
//return 0; //无法调用退出处理函数
}
输出结果:
hello!func3
func2
func1
进程除了正常退出, 还有异常退出
1、被信号打断( ctrl + c ,段错误 , kill -9)
2、最后线程(主线程)被取消。
子进程退出时, 不管是正常还是异常, 父进程会收到信号
子进程退出后,内存上的资源必须是父进程负责回收
但是有时候会出现下面两种情况 :
1、子进程先结束, 会通知父进程(通过信号), 让父进程回收资源 , 如果父进程不处理信号, 子进程则变成僵尸进程
2、父进程先结束,子进程就会变成孤儿进程, 就会由1号进程(init )负责回收,但在实际编程中要避免这种情况, 因为1号进程很忙
wait函数等待子进程的结束信号
它是阻塞函数,只有任意一个子进程结束,它才能继续往下执行,否则卡住那里等
它获得结束子进程的PID以及 退出状态/退出码 , 并且回收子进程的内存资源
#include
#include
pid_t wait(int * status);
status是传出参数, 传出退出状态/ 退出码
演示代码(wait):
#include
#include
#include
int main()
{
pid_t pid = fork();
if(!pid){//子1
printf("child %d start running!\n",getpid());
sleep(10);
printf("child exit!\n");
exit(10);
}
pid = fork();
if(!pid){//子2
printf("child %d start running!\n",getpid());
sleep(15);
printf("child exit!\n");
exit(30);
}
//父进程
//等待子进程结束
int status = 0;
pid_t pid1 = 0;
pid1 = wait(&status);//等待任意一个子进程的结束
if(WIFEXITED(status)){
printf("%d正常结束!退出码 = %d\n",pid1,WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
printf("%d被信号打断!信号 = %d\n",pid1,WTERMSIG(status));
}
pid1 = wait(&status);//等待任意一个子进程的结束
if(WIFEXITED(status)){
printf("%d正常结束!退出码 = %d\n",pid1,WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
printf("%d被信号打断!信号 = %d\n",pid1,WTERMSIG(status));
}
return 0;
}
wait函数的加强版
可以选择等指定哪个子进程 , 还可以选择等待方式(可以选择堵塞、不堵塞)
代码示例(waitpid):
#include
#include
#include
int main()
{
pid_t pid1 = fork();
if(!pid1){//子1
printf("child1 %d start running!\n",getpid());
sleep(1);
printf("child1 exit!\n");
exit(10);
}
pid_t pid2 = fork();
if(!pid2){//子2
printf("child2 %d start running!\n",getpid());
while(1);
printf("child2 exit!\n");
exit(30);
}
//父进程
//等待子进程结束
int status = 0;
pid_t pid = 0;
printf("等待child2结束 \n");
pid = waitpid(pid2,&status,0);//指定子进程,堵塞
if(pid==0){
printf("没有等到子进程!\n");
return -1;
}
printf("child2结束 \n");
if(WIFEXITED(status)){
printf("%d正常结束!退出码 = %d\n",pid1,WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
printf("%d被信号打断!信号 = %d\n",pid1,WTERMSIG(status));
}
return 0;
}
编译
gcc 5waitpid.c -o 5waitpid
在后台运行
./5waitpid &
输出
等待child2结束
child1 3966 start running!
child2 3967 start running!
child1 exit!
回车几下
waitpid此时还在等child2退出
用信号杀死进程
kill -9 3967
输出
child2结束
3966被信号打断!信号 = 9
[1]+ Done ./5waitpid
可见,我们已经成功指定哪个子进程
(小彩蛋)
有时候忘记回收子进程资源
或者你的代码不是很完美
那么
该怎么一键收回父进程所有的僵尸子进程的资源呢?
while(waitpid(-1, NULL, WNOHANG)> 0);
fork/ vfork 产生的子进程内容和父进程完全一致, 但是在很多时候, 我们希望新的子进程去执行全新的程序
而exec系列函数就提供了这样的功能,使用一个程序去替换进程的内容 (不会产生新的进程,是替换 )
单独使用没有意义,一般 是和fork/ vfork 连用。 用fork/ vfork 产生子进程, 然后用exec替代
使用vfork堵塞父进程, 抢了资源, 但是使用exec后, 子进程替换了内容, 便不抢占资源了,父进程继续执行,不用等子进程
exec系列函数太多
这里讲常用的一个
代码如下(execl):
#include
#include
#include
int main(int argc,char *argv[],char *env[])
{
printf("pid = %d\n",getpid());
pid_t pid = vfork();
if(!pid){//子
printf("child pid = %d\n",getpid());
int res = execl("./tst","tst",NULL);//用这个程序替换掉子进程内容
if(res==-1){
perror("execl");
}
exit(0);
}
//父
printf("parent running!\n");
sleep(1);//....
wait(NULL);
printf("%d end!\n",getpid());
return 0;
}
整理不易
点赞收藏关注喔~❤️❤️❤️❤️❤️❤️