【Linux】从 fork() 到 exec():理解 Linux 进程程序替换的魔法
1.前言
进程程序替换是指一个进程用另一个新的可执行程序来替换当前正在执行的程序,这个过程通过通过exec
系列函数完成。在Linux或UNIX系统中,进程程序替换通常发生在一个进程通过fork()
创建了子进程之后,子进程用exec()
函数加载和执行另一个程序。
也就是说,进程程序替换就是在不改变进程的PID(进程ID)的情况下,用一个全新的程序来替换当前的内存空间和执行内容。
当程序调用一种exec
函数时,该进程的用户空间代码和数据完全被新的程序替换,从新程序的启动例程开始执行。
2.替换函数
exec
函数是一个系列函数,负责替换当前进程的映像。常见的exec
函数包括:
#include <unistd.h>
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 execve(const char* path,char* const argv[],char* const envp[]);
2.1 函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不在返回。
- 如果调用出错则返回-1。
- 所以exec函数只有出错的返回值而没有成功的返回值。
使用execl测试
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork failed");
exit(1);
}
if(id == 0)
{
//child
printf("i am a child process,replacing myself with /bin/ls\n");
execl("/bin/ls","ls",NULL);//使用exec替换为ls程序
//如果execl执行成功,下行将不会被执行。
perror("execl failed");
exit(1);
}
else
{
//father
wait(NULL);//等待子进程结束
printf("child process finished\n");
}
return 0;
}
执行结果:
i am a child process,replacing myself with /bin/ls
a.out pReplaceTest.c
child process finished
在这个例子中:
- 父进程创建了一个子进程。
- 子进程使用
execl()
来替换自己,将当前的进程映像替换为/bin/ls
程序。 - 替换成功后,子进程开始执行
ls
程序的代码,将不在执行原理的代码。 - 父进程调用
wait()
等待子进程结束,执行ls
命令将结果输出到控制台。
在程序的替换函数,通常不会让父进程去执行,而是交给子进程去执行,因为父进程还需要进行完成它的时,同时也防止替换的程序造成程序崩溃。由于进程间的独立性,即使子进程去执行execl
函数时候,替换的也是子进程的代码和数据,而父进程的代码和数据是不会被影响的。
提问:发生了子进程的程序替换,此时:父进程的代码还是共享的吗?
回答:
不是,此时发生了写时拷贝,也就是会拷贝一份代码和数据出来,然后子进程再被它的execl还是进行程序替换。
2.2 命名理解
这些函数的名称还是太相似了,为了方便记忆,可以按一下规律记忆一下。
- l(list):表示参数采用列表。
- v(vector):参数用数组。
- p(path):有p自动搜索环境变量PATH。
- e(env):表示自己维护环境变量。
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,需要自己组装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 不是 | 不是,需要自己组装环境变量 |
2.3 exec系列函数介绍
execl
函数在上文已经介绍,这里就跳过了。
2.3.1 execlp函数
execlp
函数和execl
函数的区别在于,execlp在第一个参数时候,不需要全路径,只需要写上执行命令的文件名即可,表示你需要执行谁,往后也就是和execl的参数一样。
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
execlp("ls","ls","-a","-l",NULL);
exit(1);
}
else if(id>0)
{
int ret = wait(NULL);
if(ret<0)
{
perror("wait failed");
}
else
{
printf("wait success\n");
}
}
else
{
perror("fork failed");
exit(1);
}
return 0;
}
执行结果
drwxrwxr-x 2 ubuntu ubuntu 4096 Oct 20 14:18 .
drwxr-x--- 11 ubuntu ubuntu 4096 Oct 20 14:18 ..
-rwxrwxr-x 1 ubuntu ubuntu 16168 Oct 20 12:39 a.out
-rwxrwxr-x 1 ubuntu ubuntu 16168 Oct 20 14:18 execlp
-rw-rw-r-- 1 ubuntu ubuntu 519 Oct 20 14:18 execlpTest.c
-rw-rw-r-- 1 ubuntu ubuntu 552 Oct 20 12:39 pReplaceTest.c
-rw-rw-r-- 1 ubuntu ubuntu 1 Oct 20 14:11 test.c
wait success
2.3.2 execle函数
execle函数比execl函数多一个e,按照上的设定。需要在最后一个参数需要给execle传入自定义的环境变量数组。
它的使用情况:如果你需要给你执行的一个新的程序,加载一些自定义的环境变量给新的程序时候,以可以使用该函数。
exeTest.c文件:打印环境变量的值,这个文件假如自己执行自己的话那么会打印默认的环境变量。假如其他文件使用execle传参给exeTest.c的话,exeTest.c就会执行该execle传递过来的环境变量。
#include <stdio.h>
int main()
{
extern char** environ;
int i = 0;
for(;environ[i];i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
execle测试
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
char* const myEnv[] = {
"MYVAL1 = 123",
"MYVAL2 = 456",
"MYVAL3 = 789",
NULL
};
execle("./myexe","./myexe",NULL,myEnv);
exit(1);
}
else if(id>0)
{
int ret = wait(NULL);
if(ret>0)
{
printf("wait success\n");
}
else
{
perror("wait failed\n");
}
}
else
{
perror("fork failed");
exit(1);
}
return 0;
}
结果:
MYVAL1 = 123
MYVAL2 = 456
MYVAL3 = 789
wait success
2.3.3 execv函数
execv和execl函数没什么区别。execv函数把execl的以列形式的传参,变成了以数组的形式的传参。
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork failed");
exit(1);
}
if(id == 0)
{
//child
char* const argv[]={
"ls",
"-a",
"-l",
NULL
};
execv("/bin/ls",argv);
//如果execv执行成功,下行将不会被执行。
perror("execl failed");
exit(1);
}
else
{
//father
wait(NULL);//等待子进程结束
printf("child process finished\n");
}
return 0;
}
执行结果:
drwxrwxr-x 2 ubuntu ubuntu 4096 Oct 20 14:54 .
drwxr-x--- 11 ubuntu ubuntu 4096 Oct 20 14:54 ..
-rwxrwxr-x 1 ubuntu ubuntu 16216 Oct 20 14:54 a.out
-rwxrwxr-x 1 ubuntu ubuntu 16216 Oct 20 14:41 execle
-rw-rw-r-- 1 ubuntu ubuntu 672 Oct 20 14:38 execleTest.c
-rwxrwxr-x 1 ubuntu ubuntu 16168 Oct 20 14:18 execlp
-rw-rw-r-- 1 ubuntu ubuntu 519 Oct 20 14:18 execlpTest.c
-rw-rw-r-- 1 ubuntu ubuntu 575 Oct 20 14:54 execvTest.c
-rw-rw-r-- 1 ubuntu ubuntu 162 Oct 20 14:40 exeTest.c
-rwxrwxr-x 1 ubuntu ubuntu 16024 Oct 20 14:40 myexe
-rw-rw-r-- 1 ubuntu ubuntu 552 Oct 20 12:39 pReplaceTest.c
-rw-rw-r-- 1 ubuntu ubuntu 1 Oct 20 14:11 test.c
child process finished
如此一来,我们就讲完了:l v p e
的各个结果。其他函数只需要在此基础上进行配合就是了。
4. 总结
- 进程程序替换是指用一个新的可执行程序替换当前进程的内存空间和执行内容,但进程ID不变。
- 常用的替换函数是
exec
系列函数(如execl()
、execvp()
)。 - 它常用于父进程通过
fork()
创建子进程后,子进程用exec()
替换为新的程序来执行指定任务。 - 替换后的进程将完全抛弃原来的代码和数据,并开始执行新加载的程序。
如果我文章对你有所帮助的话,点个关注吧~
- 点赞
- 收藏
- 关注作者
评论(0)