文章目录
一、进程终止
进程退出场景
1、代码运行完毕,结果正确
2、代码运行完毕,结果不正确
3、代码异常终止 (程序崩溃,返回退出码已经没有意义了,这会去处理信号,后面会讲解)
进程退出码
进程退出码为进程退出时的退出信息,这些信息能够给我们反映该进程的结果是否正确,错误的原因。
echo $? # 打印最近一次进程退出时的退出码
退出码为0:进程正常退出并且结构正确
退出为非0:进程结果不正确
每个退出码都会对应相应的信息。
C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
for(int i=0;i<150;i++)
{
printf("%d:%s\n",i,strerror(i));
}
return 0;
}
当
我们在Linux下输错指令时,会给我们提示。
例如:
当前bash为所有进程的父进程,bash拿取ls进程的退出码来给出相应的处理。
进程正常退出方法
exit函数退出
#include <unistd.h>
void exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值。
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
1.执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
int main()
{
printf("hello world");
exit(0);
}
_exit函数退出
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
int main()
{
printf("hello world");
_exit(0);
}
main函数return
非main函数返回为函数返回,main return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
int main()
{
return 123;
}
小结:
1.main函数return,代表进程退出!!
2.非main函数的return 代表函数返回!!
3.exit()在任意地方调用,都代表终止进程,参数是退出码!!
4. exit() 和 main函数return,都会刷新 输出缓存区(用户级缓存区)
_exit() , 能够强制终止进程,但是不会刷新缓存区
进程退出,os(kernel)层做了什么呢?
系统层面,少了一个进程:free PCB,free mm_struct,free 页表和各种映射关系, 代码和数据申请的空间也要给free掉,系统给进程开辟的所有资源都被回收。
进程异常退出
1、收到某种信号退出
例如:kill -9 pid
2、代码错误,例如除数为0,越界,指针问题被操作系统检查到,操作系统给进程发送信号。
例如:除数为0;
int main()
{
int a=10;
a/=0;
return 0;
}
ps:信号部分,在进程信号章节会讲解。
二、进程等待
进程等待是什么?
目前我们通过fork():创建子进程,创建后,子进程执行的时候还有一个父进程在执行,我们不能确定谁先退出。创建子进程通常是帮助父进程完成某种任务,所以父进程需要知道子进程完成的怎么样,让父进程fork之后,需要通过wait/waitpid等待子进程退出;
为什么要让父进程等待呢?
1.通过获取子进程退出的信息,能够得知子进程执行结果。
2、 可以保证:时序问题,子进程先退出,父进程后退出。
3、进程,退出的时候会先进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,释放该子进程占用的资源!!
wait()
功能:等待子进程退出。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
代码一、父进程没有调用wait()5秒过后 子进程进入僵尸状态,而父子进程还在运行,父子进程不会回收子进程;直到父子进程结束后,才会对子进程处理。
#include<string.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
int pid=fork();
if(pid==0)
{
int n=5;
while(n)
{
printf("ppid:%d,pid:%d,%d\n",getppid(),getpid(),n);
sleep(1);
n--;
}
}
else
{
int a=10;
while(a)
{
printf("father\n");
sleep(1);
}
}
return 0;
}
代码二、父进程在阻塞等待子进程结束,子进程5s后结束子进程进入僵尸状态,父进程10s后等待子进程成功并处理子进程。
#include<string.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
int pid=fork();
if(pid==0)
{
//child
int n=5;
while(n)
{
printf("ppid:%d,pid:%d,%d\n",
getppid(),getpid(),n);
sleep(1);
n--;
}
}
//father
sleep(10);
int pid=wait(NULL);
if(pid>0)printf("wait success\n");
else printf("wait failed\n");
}
return 0;
}
waitpid()
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
1、当正常返回的时候waitpid返回收集到的子进程的进程ID;
2、如果不加第三个参数,父进程默认阻塞等待(进程停滞)。如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0,有则返回收集的进程pid;
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
1、两个宏方式来获取退出码和终止信号
WIFEXITED(status): 若为正常终止子进程返回的状态则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
注意:
1、如果子进程已经退出,调用wait/waitpid时,waitwaitpid会立即返回,并且释放资源,获得子进程退出信息。
2、如果不存在该子进程,则立即出错返回-1。
int main()
{
int pid=fork();
if(pid==0)
{
int n=5;
while(n)
{
printf("ppid:%d,pid:%d,%d\n",getppid(),getpid(),n);
sleep(1);
n--;
}
}
else
{
int ret=waitpid(pid,NULL,0);// 等待指定的一个pid
//int ret=waitpid(-1,NULL,0);//等待任意一个进程
//int ret=waitpid(pid+1,NULL,0);//等待失败返回-1
if(ret>0)printf("wait success\n");
else printf("wait failed\n");
}
return 0;
}
获取子进程status
1、status为子进程退出时的退出信息。
2、wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
3、status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
status有两种情况:
a、进程正常退出时,158位存放退出码,而70不使用,一般存放0
b、进程异常退出,15~8位不使用,第7位存放core dump 标志,该标志主要意思是如果为1则生成了调试代码,如果为0没有生成调试代码;7~0位为存放信号信息。
获取status
方式一、
int main()
{
int pid=fork();
if(pid==0)
{
int n=5;
while(n)
{
printf("ppid:%d,pid:%d,%d\n",getppid(),getpid(),n);
sleep(1);
n--;
}
exit(1);
}
else
{
int status=0;
int ret=waitpid(pid,&status,0);// 等待指定的一个pid
if(ret>0)printf("wait success,pid:%d,
status exit code :%d,
status exit singnal%d\n" ,pid, (status>>8&0xff), status&0x7f);
else printf("wait failed\n");
}
return 0;
}
方式二、
想要获取子进程的结束状态,为了方便我们也可以通过系统里 写好的宏
WIFEXITED(status) :收到信号返回false,否则true
WEXITSTATUS(status): 获取退出码
进程就两种情况,如果异常则会收到信号,否则才需要获取退出码。
int main()
{
int pid=fork();
if(pid==0)
{
int n=5;
while(n)
{
printf("ppid:%d,pid:%d,%d\n",getppid(),getpid(),n);
sleep(1);
n--;
}
exit(1);
}
else
{
int status=0;
int ret=waitpid(pid,&status,0);// 等待指定的一个pid
if(WIFEXITED(status))// 没有收到任何退出信号的
// 正常结束的,获取对应的退出码
printf("exit code:%d\n",WEXITSTATUS(status));
else printf("error,get a signal!\n");
}
return 0;
}
理解waitpid()
waitpid是系统调用接口
父进程调用waitpid(输出型参数status_p),子进程有它自己的PCB,mm_stuct,页表,通过这些进行映射到实际的物理内存中,子进程退出时先进入到僵尸状态:PCB保存进程退出时的退出数据,退出数据包含,int exit_code,int signal ,然后父进程拿到两个退出数据,通过输出型参数status_p返回,status_p是一个指针,通过解引用,就是能找到用户层status实体,并且让父进程
*status_p |=(exit_code<<8);
*status_p |=(signal);
阻塞等待与非阻塞等待
阻塞等待:
张三与女朋友的场景1 :张三约了女朋友去逛街吃饭,张三来到女朋友家楼下,张三打电话给女朋友,张三:“你好了吗,我在楼下”,女:“没有,你电话别挂,我好了就叫你”,张三一直等待电话,女:“好了,我这就下来”,挂电话。
结论:
阻塞的本质:其实是进程的PCB被放入了等待队列,并将进程的状态改为S状态。
返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度,才能拿到子进程的返回结果。
非阻塞等待:
张三与女朋友的场景2: 张三约了女朋友去逛街吃饭,张三来到女朋友家楼下,张三打电话给女朋友,张三:“你好了吗,我在楼下”,女:“没有”,张三:“我等下在打给你”,张三去做别的事,张三做完以后,张三打电话给女朋友,张三:“你好了吗”,女朋友:“没有”,……张三一只重复这几件事,直到有一次打电话时女朋友做完事了。
结论:
非阻塞等待意味着,可能需要多次检测,不会被放到等待队列里。
基于非阻塞等待的轮询方案代码演示:
int main()
{
int pid=fork();
if(pid==0)
{
int n=5;
while(n)
{
printf("ppid:%d,pid:%d,%d\n",getppid(),getpid(),n);
sleep(1);
n--;
}
exit(1);
}
else
{
int status=0;
while(1)
{
pid_t ret=waitpid(pid,&status,WNOHANG);
if(ret==0){
//子进程还在运行,等待是成功的,需要父进程重复进行等待
printf("DO father things!\n");
}
else if(ret>0){
// 等待成功获取到了对应的结果,并且子进程退出了
printf("exit code:%d\n",WEXITSTATUS(status));
break;
}
else{ //ret<0
//等待失败,子进程收到了某种信号导致,进程终止
perror("waitpid");
break;
}
sleep(1);
}
}
return 0;
}
三、进程程序替换
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数
以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动
例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
程序的本质就是一个文件!! 文件=程序代码+程序数据。进程不变,仅仅替换当前进程的数据和代码的技术,叫做进程的程序替换。程序替换的本质就是把程序的进程代码+数据,加载进特定进程的上下文中!!
如何理解程序加载到内存?
C/C++程序需要先用加载器加载到内存中,创建进程。这里的加载器本质就是exec系列的函数调用。
我们创建子进程的目的:让子进程执行父进程代码的一部分!
现在我向让子进程执行一个“全新的程序”呢??
接下来我们就做个程序替换的实验:
int main()
{
int pid=fork();
if(pid==0){
// child
printf("i ma a child process\n");
sleep(5);
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("do thing\n");
printf("do thing\n");
printf("do thing\n");}
else
{
//father
while(1)
{
printf("i am a father\n");
sleep(1);
}
}
return 0;
}
子进程进行进程程序替换后,会影响父进程的代码和数据吗?
进程具有独立性,必须在不发生修改的情况父子代码数据是共享,子进程的代码数据被替换,替换会更改代码区的代码数据,所以会发生写时拷贝!
上述代码会 运行execl指令后不会在运行下面的代码,而父进程依然在运行 i am a father;
实验小结:
1、进程具有独立性
2、父子代码不发生修改的情况的共享的,如果发生修会发生写时拷贝,代码替换在原进程上替换代码区和数据区,既然在共享区修改,那么一定会发生写时拷贝。
3、只要进程的程序替换成功,就不会执行后续的代码,意味着exec*系列的函数,只要exec 返回 了,就一定是因为调用失败!失败返回就需要执行后续的代码!
4、进程程序替换目的就是为了在不创建新进程情况下,执行一个全新的程序。
替换函数的使用
#include <unistd.h>
extern char **environ;
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 execvpe(const char *file, char *const argv[],
char *const envp[]);
path: 你要执行的目标程序的全路径(所在路径/文件名)
arg:可变参数列表 c语言的知识点,我们只要知道怎么使用即可
要指向的目标程序在命令行上怎么执行,例如:
execl("/usr/bin/ls","ls","-a","-l",NULL);
注意: 结尾必须是以NULL作为参数传递的结束!!
上面以及介绍了execl的使用,下面来看看其它6种相似的函数,
注意:6个函数都是第三方库函数,把系统函数execve()进行封装;
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
命名理解
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
使用理解
万变不离其一,执行谁,怎么执行?
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
事实上,只有execve是真正的系统调用,其它五个函数最终都调execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示 :
四、实现一个捡漏版的shell解释器
其实shell需要执行的逻辑非常简单,其只需循环执行以下步骤:
- 获取命令行。
- 解析命令行。
- 创建子进程。
- 替换子进程。
- 等待子进程退出。
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<unistd.h>
#include<string.h>
#define NUM 128
int main()
{
char *argv[NUM]={NULL};
char command[NUM];
// 让程序一直跑
for(; ;){
//1、提示框
command[0]=0;
printf("[who@hostname mydir]#");
//2 获取指令
fgets(command,NUM,stdin);
command[strlen(command)-1]='\0';
fflush(stdout);
//3、解析指令
const char* sep=" ";
argv[0]=strtok(command,sep);
int i=1;
while(argv[i]=strtok(NULL,sep))i++;
for(i=0;argv[i];i++)
{
printf("%s ",argv[i]);
}
puts("");
//4、检查命令是否是需要shell本身执行的,内建命令
if(strcmp(argv[0],"cd")==0){
if(argv[1]!=NULL)chdir(argv[1]);
continue;
}
// 执行第三方命令
if(fork()==0)
{
execvp(argv[0],argv);
}
waitpid(-1,NULL,0);
}
return 0;
}
内建命令指的是:该命令由自己执行,不需要子进程执行。
调用内建命令的函数即可,例如:cd的 内建函数为cddir();
chdir() 是系统接口,改变工作路径。
五、函数和进程之间的相似性
exec/exit就像call/return
一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。
这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下 :
一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回