目录
[a. 正常终止(可以通过 echo $? 查看进程退出码):](#a. 正常终止(可以通过 echo $? 查看进程退出码):)
[b. 异常退出:](#b. 异常退出:)
[a. main函数返回值(return 退出)](#a. main函数返回值(return 退出))
[I. 退出码:](#I. 退出码:)
[II. 错误码:](#II. 错误码:)
[III. 异常信号:用 kill -l 查看](#III. 异常信号:用 kill -l 查看)
[b. _exit 函数&&exit 函数](#b. _exit 函数&&exit 函数)
[a. wait && waitpid](#a. wait && waitpid)
[b. 获取子进程status](#b. 获取子进程status)
[编辑 3、阻塞等待&&非阻塞等待](#编辑 3、阻塞等待&&非阻塞等待)
[a. 阻塞等待](#a. 阻塞等待)
[b. 非阻塞等待](#b. 非阻塞等待)
一、进程终止
1、进程退出场景
1、代码运行完毕,结果正确2、代码运行完毕,结果错误3、代码异常终止
2、进程常见退出方法
a. 正常终止(可以通过 echo $? 查看进程退出码):
I. 从main返回 II. 调用 exit III. _exit
b. 异常退出:
ctrl + c,信号终止
3、分类
a. main函数返回值(return 退出)
main函数返回值,叫做进程的退出码,一般0,表示进程执行成功,非0表示失败。
I. 退出码:
进程的退出码(exit_code),也称为退出状态或返回值,是一个整数,用于表示进程执行结束时的状态,通常由进程的主函数(C语言中是main函数)返回,或者是在进程遇到异常或者错误是由操作系统进行设置。可以使用****echo ?****命令查看最近一次进程退出的退出码信息。? 是一个特殊的shell变量,保存了上一个命令的退出码。
II. 错误码:
进程的错误码( errno )用于表示进程在执行系统调用或库函数时遇到的错误情况,每个错误码都对应一个特定的错误情况,是的程序能够识别并处理这些错误。而进程的退出码是一个整数,用于表示进程执行结束时的状态。
错误码errno通常是一组预定义的数值,每个值对应一个特定的错误情况。这些定义通常包含在系统的头文件<errno.h>中。当系统调用或库函数失败时,它们会设置全局变量errno为相应的错误码。
如果errno的值为0,则表示系统调用成功,如果errno的值不为0,则表示系统调用失败,并且其值对应着特定的错误代码。常见处理errno的方法,如使用perror函数或者strerror函数**,perror函数可以将errno的值映射为对应错误信息,并将其打印到标准错误流(stderr),而strerror函数则可以将errno的值转化为对应的错误字符串。**
III. 异常信号:用 kill -l 查看
所以,任何进程最终的执行情况,我们都可以使用两个数字表明具体的执行情况
b. _exit 函数&&exit 函数
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
while(1)
{
printf("I am a process: %d\n", getpid());
sleep(1);
exit(3); // exit 终止进程,status:进程退出时候,退出码
}
}
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
void Print()
{
printf("hello\n");
exit(5);
}
int main()
{
while(1)
{
printf("I am a process: %d\n", getpid());
sleep(1);
Print();
//exit(3); // exit 终止进程,status:进程退出时候,退出码
}
}
这就告诉我们,exit就是用来终止进程的,exit(退出码)。
那么这个_exit又是什么呢?把上面的代码中的 exit 改成 _exit 验证一下,结果和exit是一样。难道说exit 和 _exit 是一样的吗?它们到底有什么区别?
所以,可以得出,exit 会支持刷新缓冲区,而_exit不支持。
二、进程等待
1、为什么要进行进程等待?
2、进程等待的方法
a. wait && waitpid
对于waitpid中的参数:
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(用于查看进程是否退出)。
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(用于查看你进程的退出码)。
options:
WNOHANG: 若pid指定的子进程还没有结束,则waitpid()函数返回0,不予等待。若正常结束,则返回该子进程的ID。
对于waitpid中的返回值:
正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中的waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
cpp
#include <stdio.h>
#include <errno.h>
#include <string.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
int cnt = 5;
while(cnt)
{
printf("Child is running, pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
cnt--;
}
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);//阻塞等待
if(rid > 0)
{
printf("wait sucess, rid: %d, status: %d\n", rid, status);
}
return 0;
}
等待成功,但是我们发现这个status怎么是256呢?这256从哪来的?
b. 获取子进程status
**wait 和 waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,则表示不关心子进程的退出状态信息,否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。**status不能简单的当作整型来看待,可以当作位图来看待。
可以用下图表示(只研究status低16比特位):
那么回到刚刚的问题,上面那个代码的256是怎么算的呢?
**我们上面说过,**任何进程最终执行情况,我们都可以使用两个数字表明具体执行情况。
3、阻塞等待&&非阻塞等待
阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。阻塞时,在调用返回结果前,当前线程会被挂起,并在得到结果之后返回。非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前进程。
a. 阻塞等待
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//阻塞等待
pid_t id = fork();
if(id<0) return 1;
else if(id == 0)
{
printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
sleep(3);
exit(100);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
printf("wait sucess, rid: %d, status: %d, exitnum: %d, signo1: %d, signo2:: %d, isexit: %d\n",
rid, status, status&0x7f, (status>>8)&0xff, WEXITSTATUS(status), WIFEXITED(status));
}
}
return 0;
}
看这串代码中,status&0x7f,(status>>8)&0xff,这两个是什么啊?
b. 非阻塞等待
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id= fork();
if(id < 0) return 1;
else if( id == 0)
{
printf("child is running..., pid is: %d\n",getpid());
sleep(5);
exit(10);
}
else
{
int status = 0;
pid_t rid = 0;
do
{
rid = waitpid(-1, &status, WNOHANG);//非阻塞等待
if(rid == 0)
{
printf("child is running...\n");
}
sleep(1);
}while(rid == 0);
if(WIFEXITED(status) && rid == id)
{
printf("wait child 5s success, child return code is: %d\n",WEXITSTATUS(status));
}
else{
printf("wait child failed\n");
return 1;
}
}
三、进程程序替换
1、替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另外一个程序。当进程调用一种exec函数时,该进程的用户空间和数据被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
下面我们来用一个最简单的exec的接口来表示一下替换原理:
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n",getpid());
printf("exec begin...\n");
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);//注意这里是NULL,不是"NULL"
printf("exec end ...\n");
return 0;
}
这里代码最后的printf并没有被执行?为什么?
上面替换原理说了:当进程调用一种exec函数时,该进程的用户空间和数据被新程序替换,从新程序的启动例程开始执行。
2、替换函数
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1。
所以exec函数只有出错的返回值,而没有成功的返回值。
那这么多我们该如何记住它们呢?---看命名
带p:PATH,你不用告诉系统,程序在哪里,只要告诉我名字是什么,系统替换的时候,会自动去PATH环境变量中查找。
execlp:
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n",getpid());
pid_t id=fork();
if(id == 0)
{
sleep(3);
printf("exec begin...\n");
// execl("/usr/bin/ls", "ls", "-a", "-l", NULL);//注意这里是NULL,不是"NULL"
execlp("ls", "ls","-a","-l",NULL);
printf("exec end...\n");
exit(1);
}
pid_t rid= waitpid(id,NULL,0);
if(rid>0)
{
printf("wait sucess\n");
}
exit(1);
}
带v**:vector 数组** 带l**:list,列表,参数列表**
execv:
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n",getpid());
pid_t id=fork();
if(id == 0)
{
char *const argv[] = {
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
sleep(3);
printf("exec begin...\n");
execv("/usr/bin/ls", argv);
printf("exec end...\n");
exit(1);
}
pid_t rid= waitpid(id,NULL,0);
if(rid>0)
{
printf("wait sucess\n");
}
exit(1);
}
execvp:
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n",getpid());
pid_t id=fork();
if(id == 0)
{
char *const argv[] = {
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
sleep(3);
printf("exec begin...\n");
execvp("ls", argv);
printf("exec end...\n");
exit(1);
}
pid_t rid= waitpid(id,NULL,0);
if(rid>0)
{
printf("wait sucess\n");
}
exit(1);
}
当然对于所有的替换函数,我们都不仅可以和上面一样执行系统的指令,也可以执行自己的程序。
cpp
//test.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("I am a process, pid: %d\n",getpid());
pid_t id=fork();
if(id == 0)
{
char *const argv[] = {
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
sleep(3);
printf("exec begin...\n");
execl("./mytest", "mytest", "-a", "-b", NULL);//注意这里是NULL,不是"NULL"
printf("exec end...\n");
exit(1);
}
pid_t rid= waitpid(id,NULL,0);
if(rid>0)
{
printf("wait sucess\n");
}
exit(1);
}
//mytest.cc
#include <iotream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
cout<<"hello world"<<endl;
return 0;
}
execle,execve,execvpe:
带e:自己维护环境变量 execle,execve,execvpe
先观察下面代码:
cpp
//test.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main
{
printf("I am a process, pid: %d\n", getpid());
pid_t id=fork();
if(id == 0)
{
sleep(1);
execl("./mytest","mytest",NULL);
printf("exec end...\n");
exit(1);
}
pid_t rid = waitpid(id, NULL, 0);
if(rid > 0)
{
printf("wait sucess\n");
}
exit(1);
}
//mytest.cc
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
for(int i = 0; environ[i]; i++)
{
printf("env[%d]: %s\n", i, environ[i]);
}
cout<<"hello world"<<endl;
return 0;
}
这里我们传递环境变量表了吗?? ---没有,子进程默认就拿到了,它是怎么做到的?
默认可以通过地址空间继承的方式,让所有子进程拿到环境变量,进程替换不会替换环境变量数据
1、如果我们想让子进程继承全部的环境变量,直接能拿到
2、如果单纯的新增我们可以使用putenv
3、那么我想设置全新的环境变量给子进程呢?
cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
char* const env[]={
(char*)"one=1111111111111",
(char*)"two=2222222222222",
NULL
};
printf("I am a process, pid: %d\n",getpid());
pid_t id=fork();
if(id == 0)
{
sleep(1);
execle("./mytest","mytest",NULL,env);
printf("exec end...\n");
exit(1);
}
pid_t rid= waitpid(id,NULL,0);
if(rid>0)
{
printf("wait sucess\n");
}
exit(1);
}
这样我就设置了全新的环境变量给子进程!
总结:
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,都是系统调用 execve的封装,所以execve在man手册的第2节,其它函数在man手册的第3节。