进程控制
fork
在Linux中,fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值:
在子进程中返回0,父进程中返回子进程的PID,子进程创建失败返回-1。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程。
- 将父进程部分数据结构内容拷贝至子进程。
- 添加子进程到系统进程列表当中。
- fork返回,开始调度器调度。
进程终止
进程退出场景
在我们写的程序中,代码运行会有三种情况:
- 代码运行完成,正确——代码结束
- 代码运行完毕,结果不正确
- 代码异常终止
main函数中的return的返回值为进程的退出码!
进过测试总结——return返回的值为main函数的退出码!
return的返回值含义(还有很多但想表达的是只有0为成功其他为错误原因):
代码异常终止
通俗的讲:代码跑了一半报错终止运行了——程崩溃!
崩溃后的程序它的返回值是没有意义的!例子:
进程退出的方式
- main函数return,代表进程退出
- 非main函数返回——函数返回
- exit在任地方调用都是代表终止进程,参数都是退出码!
-
exit与return是一样的可以退出进程
-
_exit终止进程,强制终止进程,不要进行进程的收尾工作,比如不是刷新缓冲区!exit()会刷新缓冲区(用户缓冲区)
进程退出OS层面做了什么?
- 系统层面上,少了一个进程:free PCB,free mm_struct,free页表和各种映射关系,代码+数据申请的空间也要给free了
进程异常退出
情况一:向进程发生信号导致进程异常退出。
例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。
情况二:代码错误导致进程运行时异常退出。
例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。
进程等待
进程wait是什么?
fork() ;
子进程:帮助父进程完成某些任务
父进程:而父进程要想知道知道子进程帮我完成了什么任务就要用wait/waitpid等待子进程退出为什么要父进程等待子进程?
1.通过获取子进程退出的性息,能够得知进程执行结果!
2.可以保证:时序问题,子进程先退出,父进程后退出。
3.进程退出的时候会先进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,来释放子进程占用的资源!注意:一但进程变成僵尸进程那他就流弊了无敌了kill-9也杀不死他,应为没有办法杀一个死去的进程!
如何解决僵尸进程?
使用wait来回收子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child[%d] is running: cnt- is:%d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(0);//子进程在这里结束
}
sleep(10);
printf("fahter wait begin\n");
pid_t ret = wait(NULL);
if(ret > 0)
{
printf("fahter wait :%d, success\n", ret);
}
else{
printf("father wait failed!\n");
}
sleep(10);
return 0;
}
效果如下:
总结:wait可以回收僵尸进程
status可以帮助我们收到子进程结束的三种反馈情况!
我们程序代码跑完结果对or结果不对是靠进程退出码来判定的,但是如何证明我们的代码是跑完了的呢,而不是“先帝创业未半而中道崩殂”的呢?——如果一个程序应为代码异常而终止的问题导致程序收到的其他信号。所以我们可以判断一个进程是否有收到信号来判断进程是否是异常终止的!下面的表应该怎么看呢?
我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
解释:次底8位为退出状态——退出码,而底7位为终止信号!所以我们以后只需要判断退出码它的底7位为0它就没有收到信号
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //退出信号
系统当中提供了两个宏来获取退出码和退出信号。
- WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
- WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //获取退出码
需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。
进程等待的方法
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child[%d] is running: cnt- is:%d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(0);
}
sleep(10);
printf("fahter wait begin\n");
// pid_t ret = wait(NULL);
// pid_t ret = waitpid(id,NULL,0);等待指定一个进程
// pid_t ret = waitpid(-1,NULL,0);//等待任意一个子进程
int status = 0;
pid_t ret = waitpid(-1,&status,0);
if(ret > 0)
{
printf("fahter wait :%d, success, status exit code:%d ,status exit signal%d\n", ret,
(status>>8)&0xFF, status & 0x7F);//看信号位与退出码
}
else{
printf("father wait failed!\n");
}
sleep(10);
return 0;
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("i am a child process!,pid :%d ,ppid: %d \n",getpid(),getppid());
exit(10);
}
结果:
i am a child process!,pid :20681 ,ppid: 19703
我们发现进程/a.out 的父进程居然是-bash——bash是所有启动进程的父进程!bash是如何得到进程的的退出结果的呢?一定是通过wait来获得进程的退出结果的! 所以我们可以使用echo可以查看子进程的退出码!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("child[%d] is running: cnt- is:%d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(0);
}
sleep(5);
printf("fahter wait begin\n");
// pid_t ret = wait(NULL);
// pid_t ret = waitpid(id,NULL,0);等待指定一个进程
// pid_t ret = waitpid(-1,NULL,0);//等待任意一个子进程
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret>0)
{
if(WIFEXITED(status))//没有收到任何的退出信号的
{
//正常结束的,获取对应的退出码!
printf("exit code: %d\n",WEXITSTATUS(status));
}
else{
printf("error, get s signal!\n");
}
}
}
waitpit
作用:waitpid会暂时停止进程的执行,直到有信号来到或子进程结束。
函数说明:
如果在调用 waitpid()时子进程已经结束,则 waitpid()会立即返回子进程结束状态值。
子进程的结束状态值会由参数 status 返回,而子进程的进程识别码也会一起返回。
如果不在意结束状态值,则参数 status 可以设成 NULL。
参数:
参数 pid 为欲等待的子进程识别码,其他数值意义如下:
- pid<-1 等待进程组识别码为 pid 绝对值的任何子进程。
- pid=-1 等待任何子进程,相当于 wait()。
- pid=0 等待进程组识别码与进程相同的任何子进程。
- pid>0 等待任何子进程识别码为 pid 的子进程。
参数 status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
参数 options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。
理解waitpit
阻塞
阻塞与非阻塞
*阻塞通俗的讲就是你和你女朋友出去玩但是她要化妆你要等她一直等
非阻塞为你和你女朋友出去玩但是她要化妆你要等她,但过五分钟打电话问一下直到她下来。——这种方式为基于非阻塞的轮询方案!*
注:阻塞与非阻塞都是等待的一种方法
阻塞的本质:其实是进程的PCB被放入等待队列,将进程的状态改为S状态
返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度。
非阻塞等待
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 3;
while(cnt)
{
printf("child[%d] is running: cnt- is:%d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(0);
}
int status = 0;
while(1)
{
pid_t ret = waitpid(id,&status,WNOHANG);
if(ret == 0)
{
// 子进程没有退出,但是waitpid等待是成功的,需要父进程重新进行等待
printf("父进程运行");
}
else if (ret > 0)
{
// 子进程退出,waitpid也成功l,获取到对应的结果了
printf("获得子进程的退出码%d,子进程的退出信号%d",(status>>8)&0xFF,status&0x7F);
break;
}
else
{
// 等待进程失败
perror("waitpid");
break;
}
}
sleep(1);
}
进程程序替换
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换(借尸还魂),并从新程序的启动例程开始执行。
看上图为子进程的他的地址空间(虚拟内存)和页表没有变化只有物理内存改变了通过修改物理内存的数据数据和代码从而改变子进程运行全新的内容(借尸还魂),但要注意的是原本子进程与父进程是公用一个空间(代码与数据)但子进程的数据被改变后会写实拷贝从此子进程与父进程的的数据不在有关联**(父亲是老师儿子开公司)**
有的人要问了如何替换呢??(小朋友你是否有很多问号?)
替换函数(exec系列函数)
替换函数有六种以exec开头的函数,它们统称为exec函数:
execl
int execl(const char *path, const char *arg, ...);
第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
例如,要执行的是ls程序。
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
也可以
execl("/usr/bin/ls", "ls", "-ali", NULL);
execlp
int execlp(const char *file, const char *arg, ...);
第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
例如,要执行的是ls程序。
execlp("ls", "ls", "-a", "-i", "-l", NULL);
execle
int execle(const char *path, const char *arg, ..., char *const envp[]);
第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量。
例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。
char* myenvp[] = { "MYVAL=qwe", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
execv
int execv(const char *path, char *const argv[]);
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
例如,要执行的是ls程序。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
12
execvp
int execvp(const char *file, char *const argv[]);
第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
例如,要执行的是ls程序。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execve
int execve(const char *path, char *const argv[], char *const envp[]);
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。
例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。
char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=qwe", NULL };
execve("./mycmd", myargv, myenvp);