Linux进程管理:创建,终止,等待
目录
进程创建
fork函数初识
写时拷贝
fork常规用法
fork调用失败的原因
进程终止
进程退出场景
exit
_exit
我们所谈的缓冲区在哪里?
进程等待
为什么要等待
是什么?
编辑
怎么做到的
进程创建
fork函数初识
在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
程调用fork,当控制转移到内核中的fork代码后,内核做
-
分配新的内存块和内核数据结构给子进程
-
将父进程部分数据结构内容拷贝至子进程
-
添加子进程到系统进程列表当中
-
fork返回,开始调度器调度
当一个进程调用 fork 之后,就有两个二进制代码相同的进程,而且它们都运行到相同的地方,但每个进程都将可以开始它们自己的旅程。
这里看到了三行输出,一行before,两行after。进程1233707先打印before消息,然后它又打印after。另一个after消息由1233708印的。注意到进程1233708没有打印before,为什么呢?如下图所示
所以,fork 之前父进程独立执行,fork 之后,父子两个执行流分别执行。
注意,fork 之后,谁先执行完全由调度器决定。
fork函数返回值
子进程返回 0,父进程返回的是子进程的 pid。
写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图
因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证! 写时拷贝,是一种延时申请技术,可以提高整机内存的使用率。
fork常规用法
通常我们创建子进程,是为了让子进程为我们干事的,不然我们创建子进程干嘛,所以一般我们创建子进程有两种,第一种我们创建子进程,想让子进程帮我执行我自己写的代码的一部分,通过if else分流,父子进程各自执行代码的一部分
第二种,这种场景是我们创建一个子进程,但我期望我的子进程帮我执行全新的程序,比如说我们在命令行中执行我们对应的命令,我们已经验证过了,我们所输入的命令本质上就是一个进程,而一旦它变成一个进程后,它的父进程就是bash,相当于我们输入命令时,是bash创建了子进程,但是这个子进程不是为了执行bash的,而是我们新启动的命令或者程序的。
fork调用失败的原因
系统中有太多的进程 ,内存不足
实际用户的进程数超过了限制
进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
进程退出场景
• 代码运行完毕,结果正确
• 代码运行完毕,结果不正确
• 代码异常终止
子进程也是进程,子进程是由父进程创建的,子进程执行后退出,退出的情况应该要返回给父进程
main函数的返回值,一般都是返回给父进程的,如何去查看程序的退出数字?
用 echo $? 命令
打印最近一个程序(进程)的退出码,进程退出码
为什么我刚刚echo $? 打印的还是1,再次执行该命令,打印的却是0呢?
原因很简单,echo $?查的是最近一次的程序(进程)的退出码,第二次查的是echo命令的退出码
我们得到了退出码,应该怎样获取其退出码的含义呢,系统已经给我们了一份退出码和退出码的描述,就叫做strerror
可是我们又不知道错误码有多少,那么我们就用代码来看一下
我们看到系统一共提供了133个错误码和错误码信息的描述
而在C语言中,系统提供了errno对象,它可以直接根据错误设置错误码,当函数运行出错,会将错误码赋值给errno变量
所以我们以后写代码,如果想把错误信息反馈出来,我们就可以直接在返回值return errno。
进程执行结果对还是不对,由进程的退出码决定
那么如果程序异常呢?
main函数结束,表示进程结束,其他函数,只表示自己的函数调用完成,返回
exit
表示引起进程终止
其参数status,表示状态,其实就是进程的退出码
可以看到,在代码中任何地方调用exit,都会导致进程结束,函数后续代码不执行,不返回,将exit的状态值返回给父进程bash,子进程的退出码!!
进程退出的具体做法
1.return
2.exit
_exit
除了exit,还有一个_exit,终止调用我的进程,即谁调用我谁就退出
我们做了解即可
我们下面来区分exit和_exit的区别
exit,是C语言提供的
_exit,是系统提供的
我们通过对缓冲区的测试可以看到二者的差异
进程如果exit退出的时候,exit(),会进行缓冲区刷新
进程如果_exit退出的时候,_exit(),不会进行缓冲区刷新
exit是库函数,_exit是系统调用,库函数和系统调用的关系是上下层关系,库函数会调用相关的系统调用来完成某些任务
终止进程,你用C语言上的exit就能把进程终止了吗?不能
这个世界上的能真正杀掉进程的,只有操作系统,因为操作系统是进程的管理者
换句话说,我们看起来语言上的调用的是exit,但实际上语言上的exit也会调用系统中的_exit,因为库函数没有终止进程的能力,它只有调用系统给它的接口,才能完成终止进程
我们所谈的缓冲区在哪里?
或者一定不在哪里?
一定不是操作系统内部的缓冲区,如果是内部的缓冲区,exit和_exit都应该刷新,但是_exit并没有刷新,因此缓冲区肯定不是操作系统内部的缓冲区
它是库缓冲区,C语言提供的
进程等待
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏,另外,进程一旦变成僵⼫状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
为什么要等待
为了解决内存泄漏问题
为了获取子进程的退出信息
是什么?
父进程调用接口wait或者waitpid等待子进程,就叫进程等待
我们先写一个僵尸进程的代码
然后我们用wait函数来解决僵尸进程的问题
waitpid
wait只剩waitpid的婴儿版,我们再来学习waitpid
pid_t waitpid(pid_t pid, int *wstatus, int options);
我们来看一下函数接口,waitpid和wait的返回值一样,都是返回子进程的pid,wstatus是输出型参数,option一会再来谈,是用来作阻塞控制的
pid参数,>0表示只等待pid对应的子进程,
=-1,表示等待任意子进程,就是wait的作用
wait和waitpid结果是一样的
那wait和waitpid什么时候会等待失败?
wait失败就是父进程没有子进程
而waitpid失败就是pid传错了
如图所示,就是失败的情况,我们把pid+1
statue
statue是输出型参数,当父进程交给子进程一个任务,子进程完成后,父进程怎么评估它的完成情况呢?
通过进程退出码,即main函数的返回值,来判定子进程的返回结果是否正确
那么我们用statue对其进行回收,我们用代码来看一下
我们发现,最后打印出来的statue是256,但是我们故意设的子进程exit的返回值是1,为什么不等于1呢?
所以statue一定不是只有退出码,它不只是我们所想的拿到退出码
因为除了代码正常跑完,返回对应值的情况,还有一种异常情况,也需要statue来考虑
当我们知道statue的组成后,就可以手动进行更改
在Linux系统中会存在许多信号,查看Linux所有的信号,命令叫做kill -l
这么多信号,我们发现左侧是信号的值,右侧是信号的名称,其实这些都是宏
可是我们发现这里并没有0号信号,一旦一个进程异常终止,那么这个父进程的statue,低七比特位会保存异常时对应的信号编号
如果没有异常
1.低七个比特位,是0
2.一旦低七比特网不为0,那它就是异常退出的,此时退出码无意义
也就是说,如果未来,低七比特位为0,我们才看次八位比特位
当我们在程序中添加野指针,即让程序异常,看一下运行结果
sigsegv表示程序段错误
在Linux利用statue的15个比特位就能够准确把一个子进程的相关的三种运行情况,都能确定清楚了。
怎么做到的
1.子进程退出信息存在哪里?
我们之前说过,当一个子进程退出,进入僵尸状态,会把代码数据,页表删掉,但是PCB不能释放,那么子进程的退出信息,只能放在僵尸进程的task_struct
我们来看一下task_struct中的子进程退出信息
虽然我们可以通过位运算来转换获取退出码信息,但是其实系统给我们直接定义了宏,供我们直接使用
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是
否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的
退出码)
option
pid返回值
>0:等待结束
=0:调用结束,但是子进程没有退出
<0:失败
我们主要了解一下WNOHANG,如果我们不带该选项,当子进程不退,我们会阻塞在这里,但是带上WNOHANG选项,它的作用是如果子进程没有退出,我们就立即返回,即非阻塞调用
举个例子,当我叫一个人出去玩,到他家楼下了,给它打电话,但是他才刚起床,还要洗漱穿衣服,于是我就把电话挂了,但是我是个急性子,就一分打一次电话,问他好了没,这个过程,我就是用户,这个人就是操作系统,打电话就是一次调用,这个过程叫做非阻塞轮询(NON BLOCK)
所有的轮询都是通过循环完成的
非阻塞调用,在询问的间歇,用户依旧可以干自己的事情,可以让等待方,和操作系统并发的做事情,效率更高,单位时间做更多的事情。
第二次我来找这个人,我打了电话,但是不挂断,让他一直接着电话准备,什么时候好了,直接给我说一声,我再挂电话,于是这个人不挂电话,直到把所有的准备工作都做完,这个过程,一直打着电话不挂断的过程,叫做阻塞调用
如上是我们非阻塞轮询的代码,我们可以看到运行结果中,父进程会不断的去询问子进程是否结束,直到子进程结束,成功等待到了子进程的信息。
如何让父进程做其他的事情?
我们写一份代码,重点让我们理解非阻塞调用,可以解放父进程,可以让父进程在轮询时,干自己的事情。
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<errno.h>
#include <stdio.h>
#include<string.h>
#include<stdlib.h>
//函数指针
typedef void (*func_t)();
#define NUM 5
func_t handlers[NUM];
void Download()
{printf("下载任务\n");
}void Fflush()
{printf("刷新任务\n");
}void Log()
{printf("日志任务\n");
}
void registerHandler(func_t h[],func_t f)
{int i = 0;for(;i<NUM;i++){if(h[i] == NULL) break;}if(i == NUM)return;h[i] = f;h[i+1] = NULL;
}
int main()
{registerHandler(handlers,Download);registerHandler(handlers,Fflush);registerHandler(handlers,Log);pid_t id = fork();if(id == 0){//子进程while(1){printf("我是一个子进程,pid :%d,ppid:%d\n",getpid(),getppid());sleep(1);}exit(10);}//父进程while(1){int statue = 0;pid_t rid = waitpid(id,&statue,WNOHANG);if(rid>0){printf("wait success,rid:%d,exit code:%d,exit signal:%d\n",rid,(statue>>8)&0xFF,statue&0x7F);break;}else if(rid == 0){//函数指针进行回调处理int i = 0;for(;handlers[i];i++){handlers[i]();}printf(" 本轮调用结束,子进程没有退出\n");sleep(1);}else{printf("等待失败\n");break;}}return 0;}
我们就可以让父进程在阻塞时,去干其他的事情