目录
前言
哈喽,小伙伴们大家好,今天我来带大家了解一下进程控制的相关知识。我将主要从四个方面进行讲解,分别是进程创建、进程终止、进程等待和进程替换。希望小伙伴们看完本文后能对进程有更加深刻的认识。
一、进程创建
1、fork函数
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
- fork函数有两个返回值:子进程中返回0,父进程返回子进程pid,出错返回-1。
- fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
fork的基本使用在上一篇文章中介绍过,这里不做赘述,下面我们重点思考两个问题。
(1)为何要给子进程返回0,给父进程返回子进程的pid?
计算机中的父子关系和现实中的父子关系一样,一个父亲可以有多个孩子,但一个孩子只能有一个父亲。假设老王有三个儿子,三个儿子统一管老王叫爸爸即可。但当三个儿子同时在场时,老王想要找他们中的一个时不能简单的称呼为儿子,因为这样会造成混淆,必须叫他们对应的姓名才能加以区分。
在操作系统中也是如此,父进程不需要标识,子进程需要标识,子进程是要执行任务的,父进程需要对它们进行区分。
(2)如何理解fork有两个返回值的问题?
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
在返回之前,这一系列操作已经完成,子进程已经创建成功,返回这条代码由父子进程一起执行,所以有两个返回值。
fork调用失败的原因:
- 系统中有太多的进程。
- 实际用户的进程数超过了限制。
2、写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
为何需要写时拷贝?
因为进程具有独立性,要保证两个进程的数据不互相干扰。
那么为何不在一开始就把数据分开呢?
因为子进程不一定会用到或写入父进程的所有数据,如果把所有数据都拷贝一份的话会造成内存浪费。写时拷贝可以保存按需分配,同时也满足了延时分配,就好比银行一样,有人取钱时它才会给,其余时间就可以自由支配这些资源,达到资源利用的最大化。
二、进程终止
1、退出码
进程运行的目的是为了完成某种工作,那么这种工作有没有成功完成呢?进程需要给出一个反馈,来供用户查看工作是否返程,这种反馈就叫做退出码。
指令: echo $? 打印最近一次的退出码
进程退出场景:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
前两种退出场景:当代码运行结果正确时,退出码为0,当代码运行结果不正确时,退出码为非0。因为成功了就是成功了,不需要加以解释,而失败了一般都是有原因的,需要将失败的原因反馈出来。不同的非0值代表着不同的失败原因。由于计算机是很擅长处理数字的,所以退出码在计算机中是以数字的形式传递。但人是非常不擅长处理数字的,需要将数字进行翻译才能够识别它代表的含义。
sterror()函数:把错误码转化为对应含义的字符串
我们可以通过sterror翻译错误码来确认任务失败的原因。
注意:如果是第三种退出场景代码异常终止(比如指针越界等情况),则退出码毫无意义。就好比如果一个人考试作弊被逮到,大家只会取关心他作弊这件事本身,没有人会去关心他考了多少分。
2、进程常见退出方法
2.1 正常退出
- return退出
- 调用exit
- _exit
注意: 只有main函数中的return代表进程退出,而在任何地方调用exit或_exit都代表进程退出。
exit和_exit的区别:exit最后也会调用_exit,只不过在调用之前还做了其它工作。
- 执行用户定义的清理函数
- 关闭所有打开的流,写入缓冲区中的数据
- 调用_exit
2.2 异常退出
ctrl+c,信号终止。进程异常退出会反馈信号,信号的详细内容之后再聊。我们只要先知道kill -l 可以打印信号列表即可。
进程终止后,操作系统会释放曾经申请的内存,释放曾经申请的数据结构,从队列等数据结构中移除。
三、进程等待
1、进程等待的必要性
子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程的问题,进而造成内存泄漏。进程等待一般由父进程来完成。
进程等待的作用:回收子进程资源,获取子进程的退出信息。
2、进程等待的方法
2.1 wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:成功返回被等待进程的pid,失败返回-1。
参数status:输出型参数 ,获取子进程退出状态,不关心则可以设置成NULL。
status不能简单的当成整形来看待,应该当成位图来看待。我们只关心后十六位。后十六位中的低七位代表进程退出时的退出信号,高八位代表进程退出时的退出码。
但在实际写代码中,我们一般不会用位操作来控制status,因为这样很复杂。库里给我提供了对应的宏。
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
wait的时候,父进程在干什么?
父进程什么也不干,只是单纯的在“等”。父子谁先运行不知道,但是wait之后,一般都是子进程先退出,父进程拿到子进程退出消息后才会退出。
2.2 waitpid方法
pid_ t waitpid(pid_t pid, int* status, int options);
参数:
- pid: 如果pid=-1,则等待任意一个子进程,与wait等价。若pid>0,等待进程ID与pid相等的进程。
- status:与wait相同
- options:暂时不关心 ,传0即可。
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int count=10;
while(count--)
{
printf("I am child\n");
sleep(1);
}
}
else
{
int status=0;
//father
pid_t ret=waitpid(id,&status,0);
if(ret>=0)
{
printf("wait success\n");
if(WIFEXITED(status))
{
printf("%d\n",WEXITSTATUS(status));
}
else
{
printf("child not exit normal\n");
}
}
}
return 0;
}
3、阻塞等待和非阻塞等待
阻塞等待:父进程什么也不干,一直卡在wait函数专门等子进程结束。
非阻塞等待:父进程调用wait函数时如果子进程还未结束,父进程先往下走,每隔一段时间调用一次wait函数查看子进程是否完成,在时间间隔时可以干其它事情。
在看一下waitpid:
pid_ t waitpid(pid_t pid, int* status, int options);
第三个参数options如果传0的话默认是阻塞等待,如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0,然后执行下面的代码。若等待成功,则和之前一样,返回该子进程的ID。
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int count=5;
while(count--)
{
printf("I am child\n");
sleep(3);
}
}
else
{
//father
while(1)
{
int status=0;
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret>0)
{
printf("wait success\n");
break;
}
else if(ret==0)
{
//子进程未完成
printf("father do other things\n");
sleep(1);
}
else
{
printf("waitpid error\n");
break;
}
}
}
return 0;
}
四、进程程序替换
1、替换原理
用fork创建子进程后,子进程和父进程共用的是一个代码(虽然可能是不同的代码路径)。但实际情况下,子进程一般不会和父进程用一套代码,而是会使用exec函数,使子进程的代码和数据被新程序替换,执行另一套程序。
注意:
- exec函数替换的仅仅是程序,不会创建新进程,所以子进程的pid不会改变。
- 进程程序替换,替换成功后将不会返回,exec后面的代码不会被执行。
- 如果exec调用失败,依旧执行原程序,后续代码不会受到影响。
2、替换函数
2.1 函数解释
其实有六种以exec开头的函数,统称exec函数:
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
系统实际调用的是最后一个函数,前五个函数是对最后一个函数进行的封装 。
注意:
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1,exec函数只有出错的返回值而没有成功的返回值。我们一般情况下不会关注exec函数的返回值,因为只要返回了就说明调用失败了。
- exec函数可以用系统的程序进行替换,可以用我们自己写的程序进行替换,甚至可以在c语言中调用phython,java等其它语言的程序进行替换。
2.2 命名理解
- l(list):表示参数采用列表传,一个一个传
- v(vector):表示参数用数组传,一起传过去
- p(path):有p自动搜索环境变量PATH下的路径
- e(env):表示自己维护环境变量,不要系统默认的
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};//自己定义的环境变量
//带l的,传参一个一个传
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,传入自己定义的环境变量
execle("ps", "ps", "-ef", NULL, envp);
//带v的,直接传一个数组
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,传入自己定义的环境变量
execve("/bin/ps", argv, envp);
以上就是今天要将的内容。本文主要从四个方面来讲解了进程控制的相关知识,不知道小伙伴们看完后有没有收获呢。感谢大家的阅读,来日方长,我们下次见~