文章目录
- 替换原理
- 站在进程的角度
- 站在程序的角度
- 初体验及理解原理
- 替换函数
- 函数解释
- 命名理解
- exec系列函数与main函数之间的关系
- 在一个程序中调用我们自己写的程序
替换原理
创建子进程的目的是什么?
->想让子进程执行父进程代码的一部分执行父进程对应的磁盘代码中的一部分
->想让子进程执行一个全新的程序
让子进程想办法加载磁盘上指定的程序,执行新程序的代码和数据
->
进程的程序替换
站在进程的角度
没有创建新的进程(PCB尤其是pid是没有变化的),只是程序的替换
站在程序的角度
- 这个程序被加载到内存中了->称
exec函数
为"加载器" - 既然我自己的代码能加载到新的程序,操作系统也是
- 先创造进程数据结构(PCB),在再需要的时候加载代码和数据
初体验及理解原理
int execl(const char *path, const char *arg, ...);
将指定的程序加载到内存中,让制定进程进行执行需要解决两个问题:1.找到程序 2.如何执行(cmd 选项一 选项二)
后面的三个点:可变参数列表:可以给函数不同数量的参数,例如:printf\scanf
示范程序:
#include<stdio.h>
#include<unistd.h>
int main()
{
//.c->exe->load->process->运行->执行我们现在所写的程序
printf("process is running...\n");
//load->exe
execl(
"/usr/bin/ls"/*要执行哪一个程序*/,
"ls",
"-a",
"-l",
NULL/*你想怎么执行*/
);//所有的exel程序都必须以NULL结尾,证明你把参数传完了
//但是此时就不能打印出下面的句子
//因为他也是代码,是在execl的,execl执行完毕的时候,代码已经被完全覆盖,开始执行新的程序的代码了,所以printf就无法执行了!
//但是如果调用失败了,就是没有替换成功,就会打印出来下面的句子
printf("process running done...\n");
}
由上述的例子我们可以看出这个函数的特点:
只有失败才会返回-1(只需要做错误处理,即:只要返回值,就一定是调用错误了)
perror("execl");
可以打印错误原因.
调用成功时则不返回,因为此时已经替换了,后续的代码已经没有意义了,没有必要返回值了.(成功了就和接下来的代码无关了)
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
由虚拟地址空间+页表来保证进程的独立性,一旦有执行流想替换代码或者数据,就发生写时拷贝.->说明写时拷贝在代码区也可以发生的!
但是我们一般不直接在父进程进行程序替换,而是创建新的子进程进行程序替换.
于是我们优化上面的代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
##include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
//这里的替换会影响父进程吗?
//不会。进程具有独立性,在执行exec时会发生写时拷贝
//类比:命令行怎么写,这里就怎么传参
sleep(1);
execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);
exit(-1);//must failed
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if (ret > 0)
{
printf("wait success: \nexit code: %d,sig: %d\n",(status>>8)&0xFF,status&0x7F);
}
}
替换函数
其实有六种以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[]);
其中,
execve
是系统调用,其余的都是封装,是为了让我们有更多的选择性
函数解释
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值
不用对该函数进行返回值判断,只要继续向后运行一定是失败的
命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
可以将所有的执行参数放入数组中,统一传递,而不用进行使用可变参数方案.
char* const argv_[] = {
"-ls",
"-a",
"-l",
"--color=auto",
NULL
};
execv("usr/bin/ls",argv_);
p(path) : 有p自动搜索环境变量PATH
带p字符的函数,不用告诉我程序的路径,你只要告诉我程序的名字,我会自动在环境变量PATH中进行可执行程序的查找.
execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);
这里前面的ls是告诉系统我要执行谁,第二个是告诉系统我想要怎么执行
e(env) : 表示自己维护环境变量
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
##include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
sleep(1);
char* const envp_[] = {
(char*)"MYENV=1122233334444",
NULL;
}
extern char** environ;//声明系统默认的环境变量列表
execle("./mybin","mybin",NULL,environ);//这里是只传了系统默认的环境变量
putenv((char*)"MYENV=444333222111");//将指定环境变量导入系统中environ指向对应的环境变量表中
execle("./mybin","mybin",NULL,envp_);//这里是只传了你自定义的环境变量
execle("./mybin","mybin",NULL,environ);//实际上,默认环境变量你不传,子进程也能获取. 这里是传了默认的环境变量加上你自己添加的环境变量
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if (ret > 0)
{
printf("wait success: \nexit code: %d,sig: %d\n",(status>>8)&0xFF,status&0x7F);
}
}
exec系列函数与main函数之间的关系
exec*系列函数的意义是:
将我们的程序加载到内存中
int main(int argc,char* argv[],char* env[])
所以应该是先加载到内存中,之后才是调用main函数,main函数中的argc
argc[]
分别是exec函数中的前面的两个参数,env[]
是从exec函数的环境变量参数中加载出来的
exec调用举例如下:
#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的,需要自己组装环境变量
//没有带e的话会默认传入系统默认的环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
在一个程序中调用我们自己写的程序
首先,我们需要在makefile文件中同时创建两个文件并且同时删除两个文件,那该怎么书写呢?
.PHONY:all
all: mybin myexec
mybin:mybin.c
gcc -o $@ $^ -std=c99
myexec:myexec.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f myexec mybin
myexec.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
##include<sys/types.h>
#include<sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
execl("./mybin", "mybin", NULL);
exit(-1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if (ret > 0)
{
printf("wait success: \nexit code: %d,sig: %d\n",(status>>8)&0xFF,status&0x7F);
}
}
可以使用程序替换调用任何后端语言所对应的可执行!!!
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
下图exec函数族 一个完整的例子: