感谢阅读East-sunrise学习分享——进程控制
博主水平有限,如有差错,欢迎斧正感谢有你 码字不易,若有收获,期待你的点赞关注我们一起进步
在一个进程的生命周期中,有4个周期
1.进程创建
2.进程终止
3.进程等待
4.进程替换
进程的这4个周期都有其作用和意义,我们也可以对其进行控制
今天我们就以这4个周期为切入点来学习进程控制
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程pid,出错返回-1
进程调用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调用失败的原因
我们在平时编程时,main函数写完都会加上return 0
;
int main()
{
//...
return 0;
}
return后面的值是代表进程退出的时候,对应的退出码。代码的执行结果正确与否我们可以通过标定各种退出码来检测。
而平时我们不关心代码的执行结果的错对,便是直接return 0
;同样,如果我们关心代码的执行结果,可以根据不同情况设置不同的return+退出码。
#include
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表示错误
但是计算机知道123对应的都是什么错误,我们人不知道,所以我们可以将错误码转化为错误信息打印出来
函数原型:
#include
char* strerror(int errnum);
遍历打印错误码:
#include
#include
#include
int main()
{
for(int i = 0; i < 150; i++)
{
printf("num[%d]:%s\n",i,strerror(i));
}
return 0;
}
由此我们可以了解到系统定义的有意义的退出码一共是133个,每一个都有其对应的原因
查看进程的退出码
echo $?
$?
会记录最近一个进程在命令行执行完毕时的退出码,供我们查看。
正常终止(可以通过$?查看进程退出码)
异常终止
库函数exit
函数原型:
#include
void exit(int status);
库函数exit在进程退出后,会主动刷新缓冲区
系统调用_exit
函数原型:
#include
void _exit(int status);
系统调用_exit在进程退出后,不会主动刷新缓冲区
#include
#include
int main()
{
printf("hello world!\n");
exit(20);
//退出码可以自定义
printf("hello world!!!!!\n");
return 0;
}
头文件:
#include
#include
函数原型:
pid_t wait(int* status);
返回值:等待成功返回被等待进程的pid,失败返回-1
参数:status是一个输出型参数,用于获取子进程的退出码和退出状态,不关心则可以设置为NULL
通过wait函数回收子进程资源:
#include
#include
#include
#include
#include
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;
}
函数原型:
pid_t waitpid(pid_t pid,int* status,int options);
返回值:
WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0参数:
获取子进程status
&status
,这样才能取到其值status是一个32位的数,都是我们只关心它低16个比特位。status的次低8位(8-15)保存的是进程的退出状态,即是退出码;低7位(0~7)保存的是进程的终止信号。
一个进程假如因为异常终止,操作系统会识别到这个进程有异常操作(比如除0、野指针、越界访问…)于是会给这个进程发信号,进程接收到这个终止信号后便会终止(代码没执行完,异常终止)
我们可以使用 kill -l 指令查看进程因为异常收到的各种信号原因
由此,我们根据status的值,便能将进程的所有退出情况反映出来
具体代码呈现
#include
#include
#include
#include
#include
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对应上图的异常信息码就是:段错误
阻塞式等待:当父进程调用wait/waitpid等待子进程时,第三个参数若为0,则是阻塞式等待所谓的阻塞式等待,如果子进程还未退出,父进程会暂停工作,一直等候着子进程
非阻塞式等待:当父进程调用waitpid等待子进程时,第三个参数若为WNOHANG,则是非阻塞式等待所谓的非阻塞式等待,如果父进程检测到子进程尚未退出,直接返回0,父进程不会原地不动地等他,而是继续执行自己的代码,进行自己的工作。如果使用while循环,便能实现非阻塞式等待轮询
非阻塞式等待不会占用父进程的时间,父进程可以在轮询的过程中做其他事情
#include
#include
#include
#include
#include
#include
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;
}
在开始介绍进程程序替换之前,我们再回顾进程创建的目的
- 父进程希望复制自己,创建子进程;使父子进程同时执行同一个程序中不同的代码段(共享一个程序)
- 创建一个子进程去执行不同的程序
通过上文的介绍中,我们想实现第一个目的其实很简单,因为用fork创建进程,本就是以父进程为模板复制创建的而我们若要实现第二个目的,则需要实现对进程中程序的替换
通过之前对进程的铺垫学习,我们知道一个进程创建之后,对应的会在物理地址空间中载入其对应的代码和数据
⭕程序本质上就是磁盘中的可执行程序,因此程序替换的本质:将磁盘指定位置上的程序的代码和数据加载到内存中,覆盖进程本身的代码和数据,达到进程程序替换,使进程执行指定的程序
值得注意的是
- 进程具有独立性,所以当对子进程进行程序替换时,会发生写实拷贝
- 由于进程程序替换的本质是对进程原有的代码、数据进行覆盖,所以该进程原有的执行程序替换的后续代码也不会再执行,因为同样也被一并覆盖了
实现如上的操作,有一系列的替换函数能够实现
替换函数:
#include
//系统调用函数
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
#include
#include
#include
#include
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一旦调用成功,后续代码将被覆盖,所以返回值也没什么意义了
exec函数在一个系统调用函数的基础上再封装出6个函数,为的就是适用于其他不同场景因此掌握exec其他函数命名的意义才能灵活调用
函数名带有 | 意义 |
---|---|
l(list) | 表示参数采用列表 |
v(vector) | 以数组的形式传参 |
p(path) | 操作系统会自动从环境变量PATH中搜索程序路径(我们可以直接传程序名) |
e(environ) | 可自定义环境变量 |
#include
#include
#include
#include
#include
#include
#include
#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;
}
写在最后 我们今天的学习分享之旅就到此结束了
感谢能耐心地阅读到此
码字不易,感谢三连
关注博主,我们一起学习、一起进步