Linux进程的控制
一、进程创建
1.fork函数
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程的列表当中
- fork返回,开始由调度器调度
2.fork函数的返回值
//fork() --- 创建一个子进程
#include <unistd.h> //所需要的头文件
pid_t fork(void);
返回值:
- 给父进程返回子进程的PID;
- 给子进程返回0;
- 子进程创建失败会返回-1;
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:
#include<stdio.h>
#include<unistd.h>
int main(){
pid_t id=fork();
printf("mypid is %d, fork() return %d\n",getpid(),id);
return 0;
}
运行结果第一行是父进程的,它的pid是1746,fork()的返回值是子进程的pid;第二行是子进程的,它的pid是1747,fork()的返回值是0。
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
3.写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
在修改内容之前,父子进程的数据和代码都是共享的,当任意一方试图写入时,操作系统会识别到缺页中断,所谓的缺页中断:是指计算机在执行程序的过程中,当出现异常情况或特殊请求时,计算机停止现行程序的运行,转向对这些异常情况或特殊请求的处理,处理结束后再返回现行程序的间断处,继续执行原程序。那么,操作系统重新分配一块空间,将旧空间的数据拷贝下来,此时操作系统也会重新映射页表。
4.fork函数常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。一个进程要执行一个不同的程序。例如子进程fork返回之后,调用exec类函数。
5.fork函数调用失败的原因
系统中有太多的进程,导致内存严重不足,无法加载数据
实际用户的进程数超过了限制
二、进程终止
1.进程的退出场景以及退出码
进程一旦退出,就会存在以下三种情况:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
对于这三种情况,作为用户怎样才能知道某个进程是以什么样的形式退出的呢?那么就有了退出码的概念。Linux系统中,程序可以在执行终止后传递指给其父进程,这个值被称为退出码。用户就可以通过相应的退出码,对进程退出状态做以判断。
例如:我们的main函数,每次都会写上return 0
;其实他就是进程的退出码。我们可以通过 echo $?
来获取最近一次进程退出时的退出码。
#include<stdio.h>
int main(){
printf("hello world!\n");
return 0;
}
除了main函数以外,我们在命令行中输入的指令它也是进程,指令的正确与否也会有相应的退出码。例如:我们在命令中输入正确的指令和错误的指令,分别查看一下对应的退出码。
- 正确的指令,进程结束后,返回的退出码是0
- 错误的指令,进程结束后,返回的退出码是127
从上图的结果,验证了进程不同的退出状态,对应了不同的退出码。每个退出码都有对应的信息,我们用0表示success、!0 表示failed。正常退出就只有一种,异常退出会对应不同的值,匹配相应的错误信息。
2.如何查看退出码对应的错误信息
在C语言中有这样一个函数 — strerror,它是将对应的数字转换为对应的错误信息
#include<stdio.h>
#include<string.h>
int main(){
for(int i=0;i<140;i++){
printf("%d: %s\n",i,strerror(i));
}
return 0;
}
3.进程常见的退出方法
1.return退出
刚刚我们已经介绍过main函数是通过return退出进程,需要注意与其他函数(非main函数)的return进行区分,非main函数的return是函数返回,而main函数的return是进程退出。
2.exit()退出
相信大家对exit函数也并不陌生,它也是用来进程退出的,有所不同的是,exit函数可以在代码中的任何位置退出进程。
#include<unistd.h>
void exit(int status);
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
printf("hello linux!");
sleep(1);
exit(12);
}
对于上面的代码,我们想要打印的内容并没有立即打印出来,这是因为数据被暂时保存在了输出缓存区中,无论是exit还是return在进程退出前都会刷新缓存区
3._exit()退出
除了上面两种方法来退出进程,我们还可以使用_exit函数来使进程退出。也是可以在代码中的任何位置终止进程,但是 _exit函数终止进程时,是强制终止,不会进行进程的后续收尾工作,如:刷新缓冲区。
#include<unistd.h>
void _exit(int status);
//参数:status 定义了进程的终止状态,父进程通过wait来获取该值
exit最后也会调用_exit,但在调用 _exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓冲数据均被写入
- 调用_exit
atexit函数和on_exit函数:注册一个在正常进程终止时调用的函数
#include<unistd.h>
#include<stdlib.h>
int main(){
printf("hello linux!");
sleep(1);
_exit(12);
}
4.return、exit和_exit的区别
- _exit()执行后会立即返回给内核,而exit()要先执行一些清楚操作,然后将控制权交给内核。
- 调用_exit()函数时,其会关闭进程所有的文件描述符,清理内存,以及其他一些内核清理函数,但不会刷新流(stdin、stdout、stderr)。exit()函数是在 _exit函数上的一个封装,它会调用 _exit,并在调用之前先刷新流。
- return是一种更常见的退出进程的方法。执行return(num)等同于执行exit(num),因为调用main的运行时函数会将main的返回值 当作exit的参数。
以上是正常退出的情况,和进程的==退出码(1)==有关;
对于进程的异常退出,就是程序执行了一半后由于地址访问错误、主动终止进程(通过kill -9 或者 ctrl +c ==信号(2)直接在进程运行中,杀掉进程)或者代码错误等。(注:这里的(1)、(2)==是标记,和下文status有关)
三、进程等待
1.进程等待的必要性
- 子进程退出,父进程如果不获取到子进程的退出信息,就可能造成僵尸进程的问题,就而造成内存泄漏
- 子进程一旦变成僵尸状态,所谓的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果是对还是不对,或者是否正常退出
- 父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。
2.进程等待的方法
1.wait方法
函数原型以及所需要的头文件
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
返回值:等待成功则返回等待的进程的PID,等待失败,返回-1;
参数:输出形参数,获取子进程退出状态,不关心则可以设置为NULL
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
/*
代码含义:通过fork创建子进程,实现5次打印,然后终止掉子进程,子进程便处于僵尸状态。同时父进程是处于等待状态的,在10秒后开始对子进程进行处理(也就是获取子进程的pid),处理结束,子进程退出。父进程此时再次等待10秒后退出。
*/
int main() {
pid_t id = fork();
if(id == 0){
int ret = 5;
while(ret){
printf("child[%d] is running:ret is %d\n", getpid(), ret);
ret--;
sleep(1);
}
exit(0);
}
sleep(10);
printf("father wait begin..\n");
pid_t cur = wait(NULL);
if(cur > 0){
printf("father wait:%d success\n", cur);
}
else{
printf("father wait failed\n");
}
sleep(10);
}
对上面的代码进行编译之后,我们再编写一个shell脚本,进行进程的持续检测。
while :; do ps axj | head -1 && ps axj | grep a.out | grep -v grep; sleep 1; echo "**********************"; done
2.waitpid方法
函数原型以及所需的头文件
#include<sys/types.h>
#include<sys/wait.h>
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,不予以等待。若正常结束,则返回该子进程的PID。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
/* 代码含义:通过fork创建子进程,实现5次打印,然后终止掉子进程,子进程便处于僵尸状态。同时父进程是处于等待状态的,在10秒后开始对子进程进行处理(也就是获取子进程的pid),处理结束,子进程退出。父进程此时再次等待10秒后退出。
*/
int main() {
pid_t id = fork();
if(id == 0){
int ret = 5;
while(ret){
printf("child[%d] is running:ret is %d\n", getpid(), ret);
ret--;
sleep(1);
}
exit(0);
}
sleep(10);
printf("father wait begin..\n");
//pid_t cur = waitpid(id, NULL, 0);//等待指定一个子进程
pid_t cur = waitpid(-1, NULL, 0);//等待任意一个子进程
if(cur > 0){
printf("father wait:%d success\n", cur);
}
else{
printf("father wait failed\n");
}
sleep(10);
}
结果和wait一样
3.获取子进程status
1.什么是status
int* status:
它是一种输出型的参数
所谓获取子进程的status,就是获取子进程退出时的退出信息;
首先,在子进程中分别用exit(0)和exit(10)来中断子进程,父进程获取status值,判断进程的退出状态。
#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){
int ret = 3;
while(ret){
printf("child[%d] is running:ret is %d\n", getpid(), ret);
ret--;
sleep(1);
}
exit(0);//比较exit(10)或任意值
}
printf("father wait begin..\n");
int status = 0;
pid_t cur = waitpid(id, &status, 0);
if(cur > 0){
printf("father wait:%d success,status:%d\n", cur, status);
}
else
printf("father wait failed\n");
}
}
通过上面的运行结果,我们本来以为status应该是0和10,但和预期的结果却有所不同。这里我们仔细思考一下:父进程拿到什么样的status结果,一定是和子进程如何退出强相关的。子进程的退出不也是进程退出嘛。
进程退出是三种情况:
1.代码运行完毕,结果正确
2.代码运行完毕,结果不正确
3.代码异常终止
也就是父进程只要通过status反馈出这三种情况,做出相应的决策。
所以代码中的int status就不能简单的理解为单纯的整数了!!!
2.status的构成
在上文中,我们对status有了一定的了解后,接下来谈一谈status的构成。
status是由32个比特位构成的一个整数,目前阶段我们只使用低16位来表示进程退出的结果,如下图所示,就是status低16位的表示图;
进程正常退出有两种,与退出码有关,异常退出与信号有关;(结合上文中进程退出的概念),所以这里我们就需要获取到两组信息:退出码与信号;如果没有收到信号,就表明我们所执行的代码是正常跑完的,然后在判断进程的退出码,究竟是何原因使进程结束的;反之则是异常退出,也就不需要关心退出码了;
3.如何获取status
结合下图,我们用次低8位表示进程退出时的退出状态,也就是退出码;用低7位表示进程终止时所对应的信号;
此时,我们想要拿到这个退出码和信号的值,我们是不是只要拿到了这低16个比特位中的次低8位和低7位就可以了;具体操作如下图所示
status exit_code = (status >> 8) & 0xFF; //退出码 status exit_code = status7 & 0x7F; //退出信号
接下来将上面的代码重新整理如下:测试子进程的退出状态时的退出码及是否获取到了信号
#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){
int ret = 3;
while(ret){
printf("child[%d] is running:ret is %d\n", getpid(), ret);
ret--;
sleep(1);
}
exit(10);
}
printf("father wait begin..\n");
int status = 0;
pid_t cur = waitpid(id, &status, 0);
if(cur > 0){
printf("father wait:%d success, status exit_code:%d, status exit_signal:%d\n", cur, (status >> 8)& 0xFF, status & 0x7F);
}
else
printf("father wait failed\n");
}
}
通过运行结果可以看出,刚好与我们所给定的退出码对应,并且没有接收到信号,意味着正常退出,但是这里我们所给定的退出码是10,至于退出码的含义可以自由设定。
综上所述,我们理解一些进程退出的三种情况:
1.代码运行完毕,结果正确;对应如下:
2.代码运行完毕,结果不正确;对应如下:
3.代码异常终止;对应如下:
接下来我们再看一段代码以及运行结果:
int main(){
printf("I am a child process!: pid: %d,ppid: %d\n",getpid(),getppid());
exit(10);
return 0;
}
我们通过这三张图,可以发现,命令行解释器(bash)能够获取到退出码(echo $?
),并且bash是命令行启动的所有进程的父进程(不难看出,我们通过进程查看18785,相应的进程就是bash)所以,bash也一定是通过子进程去执行这段程序,也一定通过wait方式得到子进程的退出结果,刚好我们能看到echo $?
能够查到子进程的退出码!
其实这里就是想说,系统也是有自带的,能够获取退出码与退出信号的宏
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
4.阻塞等待与非阻塞等待
这里我们所讲的阻塞等待和非阻塞等待,其实就是waitpid函数的第三个参数,我们之前并未提及,直接给的是0,这种是默认行为,阻塞等待;如果设置为WNOHANG,表示的是非阻塞等待方式。
**阻塞等待:**父进程一直在等待子进程,什么事都不干,直到子进程正常退出。
**非阻塞等待:**父进程的PCB由运行队列转变为等待队列,直到子进程结束,操作系统获取到子进程退出的信号时,再将父进程从等待队列中调度到运行队列,由父进程去获取子进程的退出码以及退出信号。
//基于阻塞等待的轮询访问
#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){
int ret = 10;
while(ret){
printf("child[%d] is running:ret is %d\n", getpid(), ret);
ret--;
sleep(1);
}
exit(1);
}
int status = 0;
while(1){
pid_t cur = waitpid(id, &status, WNOHANG);
if(cur == 0){
//子进程没有退出,但是waitpid等待是成功的,需要继续重复进行等待
printf("Do father things!\n");
}
else if(cur > 0){
//子进程退出了,waipid也成功了,获取到了对应的结果
printf("father wait:%d success, status exit_code:%d, status exit_signal:%d\n", cur, (status >> 8)& 0xFF, status & 0x7F);
break;
}
else{
//等待失败了
perror("waitpid");
break;
}
sleep(1);
}
}
从运行结果可以看出,父进程在不断的查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。
四、进程程序替换
1.替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
从上图可以看出,进程程序替换前后,进程本身并没有发生任何变化,只是所执行的代码发生了改变。如果子进程进行程序替换,会影响父进程的代码和数据吗?
不会。首先进程是具有独立性的,虽然子进程共享父进程的代码和数据,但是由于进行了程序替换,发生了代码和数据的修改,此时就会进行写时拷贝。所有子进程进行程序替换时,并不会影响父进程的代码和数据。
2.替换函数
其实有六种以exec开头的函数,统称为exec函数:他们需要的头文件均在 #include<unistd.h>
中
1.execl函数
int execl(const char *path, const char *arg, ...);
// path --- 可执行程序的路径
// arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// 例如:
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
#include<stdio.h>
#include<unistd.h>
int main(){
printf("hello execl\n");
int ret = execl("/usr/bin/ls","ls","-a","-l",NULL);
// "/usr/bin/ls" 路径 "ls","-a","-l" 程序执行方式
printf("I have finished the work,replace result: %d\n",ret);
return 0;
}
上面的实验是没有子进程的,是一个纯单进程的实验,下面将演示一个多进程的例子执行ls指令,而且我们会发现一个现象,我们明明在源代码中写了两条printf函数语句,但是结果只有一条打印出来,原因是,当我们进行程序替换之后,子进程将不再执行原来的父进程的代码块,由进程程序替换的结果我们可以知道,子进程中的代码内容完全被替换成新程序的代码。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
int main(){
printf("I am a process,my pid:%d,ppid:%d\n",getpid(),getppid());
//creat process
pid_t id=fork();
if(id==0){ //child
printf("I am a child process,I am about to perform a program replacement,replacing ls -a -l in the system,my pid:%d,ppid:%d\n",getpid(),getppid());
execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(1);
}
else{ //parent
printf("I am parent process,I am waitting for the child process exit,my pid:%d,ppid:%d\n",getpid(),getppid());
sleep(2);
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret<0){
printf("waiting for child process failed\n");
}
else{
printf("the result of waitting for the child process to succeedis:%d,exit code:%d,abnormal signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);
}
}
return 0;
}
上面的实验思路就是父进程创建一个子进程,然后本来子进程是要执行父进程的代码块和父进程进行代码共享的,但是我们在子进程中调用execl函数接口,因此,在子进程中会进行程序替换
2.execlp函数
int execlp(const char *file, const char *arg, ...);
// file --- 可执行程序的名字
// arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// 例如:
execlp("ls", "ls", "-a", "-l", NULL);
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
printf("I am a process,my pid:%d\n",getpid());
pid_t id=fork();//creat child process
if(id==0){ //child
printf("I am child process,I am preparing to replace the process program,my pid:%d\n",getpid());
execlp("ls","ls","-a","-l",NULL);
exit(-1);
}
else{ //parent
printf("I am parent child,I am waitting child process exit,my pid:%d\n",getpid());
sleep(2); //wait
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret<0){
printf("waitting child process failed\n");
}
else{
printf("waiting child process succeed,wait result:%d,child process exitcode:%d,child process abnormal signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);
}
}
return 0;
}
execlp中的p指的是环境变量中的PATH,指的是系统直接到环境变量PATH中去寻找对应的程序,因此在传第一个参数的时候不需要带路径
注意:execlp("ls","ls","-a","-l",NULL)
这个参数列表中有两个“ls”,我们需要知道的是这两个ls的含义是不一样的,是不能省略的,第一个是告诉系统要执行哪个程序,好让系统知道去找谁,第二个是为了告诉系统怎么执行这个程序
3.execle函数
int execle(const char *path, const char *arg, ..., char * const envp[]);
// path --- 可执行程序的路径
// arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// envp --- 自己维护的环境变量
// 例如:
char* envp[] = { "Myval=12345", NULL };
execle("./myexe", "myexe", NULL, Myval);
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
printf("I am a process,my pid:%d\n",getpid());
pid_t id=fork();
if(id==0){
printf("I am child process,I am prepare the program replacement,my pid:%d\n",getpid());
extern char** environ;
execle("./test","test",NULL,environ);
}
else{
printf("I am parent process,I am waitting child process exit,my pid:%d\n",getpid());
sleep(2);
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret<0){
printf("wait failed\n");
}
else{
printf("wait succeed,wait result:%d,child process exitcode:%d,child process abnormal signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);
}
}
return 0;
}
导出自己的环境变量
注意:
extern char** environ
;
execle("./test","test",NULL,environ);
上面的实验中,我们使用函数execle传入第三方变量environ,environ的作用是获取系统中的环境变量,上面这个代码的意思就是将这个变量environ传给test程序,那么如果想要在该程序中打印自定义的环境变量,则需要将自定义环境变量加入到系统环境变量,test程序才能获取,如果没有加入系统环境变量,则getenv()函数获取不到对应的环境变量则会返回空指针,那么就会导致printf函数打印空指针从而造成程序崩溃
除了上面的情况外,我们也可以自己传入自定义的环境变量
4.execv函数
int execv(const char *path, char *const argv[]);
// path --- 你要执行程序的路径
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
// 例如:
char* argv[] = { "ls", "-a", "-l", NULL };
execv("/usr/bin/ls", argv);
这个函数和上面介绍的execl是类似的,都需要做两件事,第一是要知道新程序的路径,第二知道怎么执行新程序(是否携带选项进行执行),但是这个函数和上面介绍的函数的区别就是这个函数的第二个参数是一个字符指针数组,上面的execl函数是一个一级字符指针,其实区别就在于,execl函数传参传的是字符串列表,这个函数传参传的是字符指针数组
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(){
//creat child process
pid_t id = fork();
if(id==0){
//child
printf("I am child process,I need to perform program replacement and execute ls execution,my pid:%d\n",getpid());
char* const _arg[]={
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
execv("/usr/bin/ls",_arg);
exit(1);//if the replacement is successful,it will not be executed here
}
else{ //parent
printf("I am parent process,I am waitting the child process exit,my pid:%d\n",getpid());
sleep(2); //wait
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret<0){
printf("waitting the child process failed\n");
}
else{
printf("waitting the child process succeed,the result is:%d,child process exitcode:%d,child process abnormal signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);
}
}
return 0;
}
需要注意的是:
-
execv函数中使用的是字符指针数组,而不是传字符串列表
-
数组中的字符串是常量字符串,也就是const char类型,而这个字符指针数组中存放的又是char的,是支持修改的,因此这里需要将const char强转为char类型
5.execvp函数
int execvp(const char *file, char *const argv[]);
// file --- 你要执行程序的名字
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
// 例如:
char* argv[] = { "ls", "-a", "-l", NULL };
execvp("ls", argv);
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(){
printf("I am a process,my pid:%d\n",getpid());
pid_t id=fork(); //create child process
if(id==0){ //child
printf("I am child process,I am perparing to perform program replacement,my pid:%d\n",getpid());
char* const _arg[]={
(char*)"ls", (char*)"-a", (char*)"-l", NULL
};
execvp("ls",_arg);
}
else{ //parent
printf("I am parent process,I am waitting the child process exit,my pid:%d\n",getpid());
sleep(2);
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret<0){
printf("parent process wait failed\n");
}
else{
printf("parent process wait child process succeed,wait result:%d,child process exitcode:%d,child process abnormal signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);
}
}
return 0;
}
6.execve函数
int execvpe(const char *file, char *const argv[], char *const envp[]);
// file --- 你要执行程序的路径
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
// envp --- 自己维护的环境变量
//例如:
char* argv[] = { "mycmd", NULL };
char* envp[] = { "Myval=12345", NULL };
execve("./myexe", argv, envp);
3.函数解释
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。也就是说,exec系列函数只要返回了,就意味着调用失败。
4.命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记
- l(list):表示参数采用列表
- v(vector):参数用数组
- p(path):有p自动搜索环境变量PATH
- e(env):表示自己维护环境变量
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,须自己装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 不是 | 不是,须自己装环境变量 |
事实上,只有execve才是真正的系统调用,其他五个函数最终都是调用的execve,所以execve在man手册的第二节,而其他五个函数都是在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。
下图为exec系列函数族之间的关系: