🌈感谢阅读East-sunrise学习分享——进程控制
博主水平有限,如有差错,欢迎斧正🙏感谢有你 码字不易,若有收获,期待你的点赞关注💙我们一起进步
🌈在一个进程的生命周期中,有4个周期
1.进程创建
2.进程终止
3.进程等待
4.进程替换
进程的这4个周期都有其作用和意义,我们也可以对其进行控制
今天我们就以这4个周期为切入点来学习进程控制🚀🚀
目录
一、进程创建
1.再识fork函数
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程pid,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
1.如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?
因为父进程:子进程 = 1:n (n>=1)
所以子进程不论如何都能找得到它的父进程,但是就需要给父进程返回各个子进程的id,便于父进程找到它的某个子进程
2.如何理解fork函数有两个返回值?
fork函数的核心操作内容便是创建进程,在fork函数之后,会有两个进程(两个执行流)彼此共同享用代码。而当一个函数准备return的时候,一般即是核心代码已经执行完毕。那么就意味着此时经过fork函数的调用后已经有了两个进程,才进行return。因此父进程和子进程各自执行return也就使得fork函数有两个返回值
3.如何理解同一个id值,会保存两个不同的值,让if else if同时执行?
这个问题经过我们之前对进程地址空间的学习后便十分容易了。调用fork函数的时候我们会用一个变量去接收其返回值pid_t id = fork( );而返回的本质就是写入,谁先返回,谁就先写入id。而因为进程具有独立性,因此在父子进程写入时是发生了写实拷贝。(具体情况和具体分析在之前博客已有涉及到)此时父子进程的id值不相同,但是打印出来的地址是相同的,因为打印出来的地址是虚拟地址。
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
二、进程终止
1.main函数返回值
我们在平时编程时,main函数写完都会加上return 0
;
int main()
{
//...
return 0;
}
return后面的值是代表进程退出的时候,对应的退出码。代码的执行结果正确与否我们可以通过标定各种退出码来检测。
而平时我们不关心代码的执行结果的错对,便是直接return 0
;同样,如果我们关心代码的执行结果,可以根据不同情况设置不同的return+退出码。
#include <stdio.h>
int addToTarget(int from,int to)
{
int sum = 0;
for(int i = from; i < to ; i++)
{
sum += i;
}
return sum;
}
int main()
{
int num = addToTarget(1,100);
if(num == 5050)
return 0;
else
return 1;
}
对于程序的退出码,系统是规定0表示成功,非0表示错误
2.查看程序退出码
但是计算机知道123对应的都是什么错误,我们人不知道,所以我们可以将错误码转化为错误信息打印出来
函数原型:
#include <string.h>
char* strerror(int errnum);
遍历打印错误码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
for(int i = 0; i < 150; i++)
{
printf("num[%d]:%s\n",i,strerror(i));
}
return 0;
}
由此我们可以了解到系统定义的有意义的退出码一共是133个,每一个都有其对应的原因
查看进程的退出码
echo $?
$?
会记录最近一个进程在命令行执行完毕时的退出码,供我们查看。
3.进程退出
3.1进程退出的情况
- 代码运行完毕,结果正确
- 代码运行完毕,结果错误
- 代码未运行完毕,异常终止
3.2进程常见退出方法
正常终止(可以通过$?查看进程退出码)
- 从main返回(return)
- 调用exit
- 调用_exit
异常终止
- ctrl+c或由于其他异常(除0错误、野指针…)导致程序异常终止
库函数exit
函数原型:
#include <stdlib.h>
void exit(int status);
库函数exit在进程退出后,会主动刷新缓冲区
系统调用_exit
函数原型:
#include <unistd.h>
void _exit(int status);
系统调用_exit在进程退出后,不会主动刷新缓冲区
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello world!\n");
exit(20);
//退出码可以自定义
printf("hello world!!!!!\n");
return 0;
}
三、进程等待
1.进程等待必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出
- 父进程通过进程等待的方式,1.回收子进程资源 2.获取子进程退出信息
2.进程等待的方法
头文件:
#include <sys/types.h>
#include <sys/wait.h>
2.1系统调用wait
函数原型:
pid_t wait(int* status);
返回值:等待成功返回被等待进程的pid,失败返回-1
参数:status是一个输出型参数,用于获取子进程的退出码和退出状态,不关心则可以设置为NULL
通过wait函数回收子进程资源:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork fail");
exit(-1);
}
else if(id == 0)
{
//子进程
int cnt = 5;
while(cnt--)
{
printf("子进程:%d,父进程:%d %d\n",getpid(),getppid(),cnt);
sleep(1);
}
exit(0);//子进程退出
}
sleep(8);//让父进程暂时休眠
pid_t ret = wait(NULL);//等待子进程,回收子进程资源
if(id > 0)
{
//父进程
printf("等待成功:%d\n",ret);
}
return 0;
}
2.2系统调用waitpid
函数原型:
pid_t waitpid(pid_t pid,int* status,int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程id
- 如果设置了选项
WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0 - 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
参数:
- pid:pid = -1,等待任意子进程,与wait等效;pid > 0,等待其进程id与pid相同的进程(指定具体进程)
- status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出);WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
获取子进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充;在进行函数调用时,我们需传入
&status
,这样才能取到其值 - 如果传递NULL,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
status是一个32位的数,都是我们只关心它低16个比特位。status的次低8位(8-15)保存的是进程的退出状态,即是退出码;低7位(0~7)保存的是进程的终止信号。
一个进程假如因为异常终止,操作系统会识别到这个进程有异常操作(比如除0、野指针、越界访问…)于是会给这个进程发信号,进程接收到这个终止信号后便会终止(代码没执行完,异常终止)
我们可以使用 kill -l 指令查看进程因为异常收到的各种信号原因
由此,我们根据status的值,便能将进程的所有退出情况反映出来
- 正常终止 – 代码运行完毕,结果正确 ----> 终止信号为0(没有异常)、退出状态为0(运行结果正确)
- 正常终止 – 代码运行完毕,结果错误 ----> 终止信号为0(没有异常)、退出状态为非0(运行结果错误)
- 异常终止 – 代码未执行完毕,程序异常终止 ----> 终止信号非0(出现异常)、退出状态为0(退出状态位没有使用)
具体代码呈现
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork fail");
exit(-1);
}
else if(id == 0)
{
//子进程
int cnt = 5;
while(cnt--)
{
printf("子进程:%d,父进程:%d %d\n",getpid(),getppid(),cnt);
sleep(1);
}
//exit(20);//子进程退出
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(id > 0)
{
//父进程
printf("等待成功:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8)&0xFF);
}
return 0;
}
进程的退出码我们也可以自定义设置
🎈我们可以故意制造异常看看结果
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork fail");
exit(-1);
}
else if(id == 0)
{
//子进程
int cnt = 5;
while(cnt--)
{
printf("子进程:%d,父进程:%d %d\n",getpid(),getppid(),cnt);
sleep(1);
//野指针
int* p = NULL;
*p = 100;
}
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(id > 0)
{
//父进程
printf("等待成功:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8)&0xFF);
}
return 0;
}
11对应上图的异常信息码就是:段错误
2.3进程等待具体过程
- 子进程退出后变成僵尸状态,task_struct中的代码和数据会被释放掉,但是这时PCB还没能完全释放,因为此时里面存放着自己的退出信号、退出码
- wait/waitpid是系统调用,操作系统有资格也有能力去读取子进程的PCB
- 父进程通过进程等待的方式,获取子进程的退出信息,然后回收子进程资源(释放PCB)让子进程结束僵尸状态
3.阻塞/非阻塞式等待
阻塞式等待:当父进程调用wait/waitpid等待子进程时,第三个参数若为0,则是阻塞式等待🚩所谓的阻塞式等待,如果子进程还未退出,父进程会暂停工作,一直等候着子进程
非阻塞式等待:当父进程调用waitpid等待子进程时,第三个参数若为WNOHANG,则是非阻塞式等待🚩所谓的非阻塞式等待,如果父进程检测到子进程尚未退出,直接返回0,父进程不会原地不动地等他,而是继续执行自己的代码,进行自己的工作。如果使用while循环,便能实现非阻塞式等待轮询
非阻塞式等待不会占用父进程的时间,父进程可以在轮询的过程中做其他事情🎯
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
void OtherTask()
{
printf("The child process is running , parent process is running other task\n");
}
int main()
{
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
//child
int cnt = 5;
while (cnt--)
{
printf("child process is running!\n");
sleep(1);
}
exit(10);//子进程退出
}
int status = 0;
//父进程对子进程进行轮询
while (1)
{
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret == 0)//子进程尚未退出
{
printf("wait done, but child is running...parent running other things\n");
OtherTask();
}
else if (ret > 0)//waitpid调用成功,子进程退出,返回值为被等待的子进程的id
{
printf("wait sucess,exit code:%d,sig:%d\n", (status >> 8) & 0xFF, status & 0x7F);
break;//结束轮询
}
else//等于-1表示调用失败 (waitpid的第一个参数传入的是不存在的子进程id,则会失败)
{
printf("waitpid call fail\n");
break;
}
sleep(1);
}
return 0;
}
四、进程程序替换
1.替换原理
💭在开始介绍进程程序替换之前,我们再回顾进程创建的目的
- 父进程希望复制自己,创建子进程;使父子进程同时执行同一个程序中不同的代码段(共享一个程序)
- 创建一个子进程去执行不同的程序
通过上文的介绍中,我们想实现第一个目的其实很简单,因为用fork创建进程,本就是以父进程为模板复制创建的🎯而我们若要实现第二个目的,则需要实现对进程中程序的替换
通过之前对进程的铺垫学习,我们知道一个进程创建之后,对应的会在物理地址空间中载入其对应的代码和数据
⭕程序本质上就是磁盘中的可执行程序,因此程序替换的本质:将磁盘指定位置上的程序的代码和数据加载到内存中,覆盖进程本身的代码和数据,达到进程程序替换,使进程执行指定的程序🎯
📌值得注意的是
- 进程具有独立性,所以当对子进程进行程序替换时,会发生写实拷贝
- 由于进程程序替换的本质是对进程原有的代码、数据进行覆盖,所以该进程原有的执行程序替换的后续代码也不会再执行,因为同样也被一并覆盖了
2.替换函数
1.认识exec函数
🌏实现如上的操作,有一系列的替换函数能够实现
替换函数:
#include <unistd.h>
//系统调用函数
int execve(const char *path, char *const argv[], char *const envp[]);
//由上面的系统调用再进行封装的函数
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 execl(const char *path, const char *arg, ...);
程序替换我们需要进行两步操作
- 找到程序(告诉系统要执行哪个程序)
- 介绍如何执行
如上的两步便从函数的两个参数得以实现
- 第一个参数需要传入程序的地址
- 第二个参数传入执行程序的方式(可理解为,你在命令行中怎么执行,就怎么传参)
- 我们注意到参数中还有 … 这叫做可变参数列表;因为平时我们执行的时候会带上不定的选项,如:ls -a -i … 由此可以实现我们根据具体需求传入具体个数的参数,但是最后要以NULL结尾,表示已经传入完毕
🌰举个栗子:父进程创建一个子进程去执行ls程序
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
execl("/user/bin/ls","ls","-l","-a","--color=auto",NULL);
exit(-1);
}
wait(NULL);//父进程回收子进程
return 0;
}
🚩值得注意的是,exec*函数仅在发生错误替换失败时才返回-1;调用成功后是没有返回值的,因为exec一旦调用成功,后续代码将被覆盖,所以返回值也没什么意义了
2.exec函数的命名理解
exec函数在一个系统调用函数的基础上再封装出6个函数,为的就是适用于其他不同场景🎯因此掌握exec其他函数命名的意义才能灵活调用
函数名带有 | 意义 |
---|---|
l(list) | 表示参数采用列表 |
v(vector) | 以数组的形式传参 |
p(path) | 操作系统会自动从环境变量PATH中搜索程序路径(我们可以直接传程序名) |
e(environ) | 可自定义环境变量 |
3.制作一个简易的shell
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#define NUM 1024
#define OPT_NUM 64
char* lineCommand[NUM]
int main()
{
while(1)
{
//输出提示符
printf("用户名@主机名 当前路径# ");
fflush(stdout);
//获取用户输入
char* s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);
assert(s != NULL);
//清除最后一个\n
lineCommand[strlen(lineCommand)-1] = 0;
//"ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1-n 字符串切割
char* myargv[OPT_NUM];
myargv[0] = strtok(lineCommand," ");
int i = 1;
if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
{
myargv[i++] = (char*)"--color=auto";
}
//如果没有子串了,strtok -> NULL ,myargv[end] = NULL
while(myargv[i++] = strtok(NULL," "));
//进程替换
pif_t id = fork();
if(id == 0)
{
//child
execvp(myargv[0],myargv);
exit(1);
}
wait(NULL);
}
return 0;
}
🌈🌈写在最后 我们今天的学习分享之旅就到此结束了
🎈感谢能耐心地阅读到此
🎈码字不易,感谢三连
🎈关注博主,我们一起学习、一起进步