一、 初识进程替换
1、为什么要学习进程替换
在前面我们讲过如何创建一个子进程,创建一个子进程能够帮我们父进程完成一些任务,但是前面我们创建的子进程都有一定的缺陷,那就是我们创建的子进程只能执行父进程的部分代码,而不能独立于父进程去执行一个父进程没有的代码,如果我们想要子进程去执行不同于父进程的代码,这时就需要学习进程程序替换了!
2、进程程序替换的原理
在学习进程程序替换之前我们先来感受一下进程替换,看下面一段代码:
#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() fail:"); exit(-1); } else if (id == 0) { printf("我是子进程,我的pid是: %d\n", getpid()); //进行程序替换 int n = execl("/bin/ls", "ls", "-a", "-l", NULL); printf("我是子进程,我的pid是: %d\n", getpid()); printf("我是子进程,我的pid是: %d\n", getpid()); printf("我是子进程,我的pid是: %d\n", getpid()); printf("我是子进程,我的pid是: %d\n", getpid()); if (n == -1) { printf("进程程序替换失败!\n"); exit(-1); } } int status = 0; pid_t Pid = wait( & status); printf("我是父进程,等待子进程成功!子进程的pid是: %d\n", Pid); if (WIFEXITED(status)) { printf("子进程正常退出,退出码为: %d\n", WEXITSTATUS(status)); } else { printf("子进程退出异常,退出信号为: %d\n", status & 0x7F); } return 0; }
执行结果:
我们发现,子进程在调用完execl
后下面的代码就全变了,变成了去执行ls
命令的代码了,也就是说子进程中的代码与数据被磁盘中的文件给替换了,从而让我们子进程执行一个不同于父进程的代码。
好了,看完了现象,我们来看一看进程替换的原理:
替换原理如图所示:
当我们子进程调用了execl
后便将磁盘中的另一个程序的代码与数据拷贝给子进程,此时子进程发生写时拷贝,代码与数据都被拷贝至一个新的位置,与父进程彻底分裂。
这里对于进程程序替换有几个要点要好好理解:
- 程序替换是整体替换不能局部替换,执行程序替换,新的代码和数据就被加载了,
execl
后续的代码属于老代码,直接被替换了。设机会执行了! - 进程的程序替换,并没有创建新的进程!进程程序替换只是将原来进程的代码与数据进行了替换。
- 进程具有独立性,子进程的程序替换,并不会影响父进程
二、进程程序替换的接口
明白了进程程序替换的原理后我们就要开始学习进程替换的使用了,在Linux中我们使用man execl
命令可以看到许多有关进程程序替换的接口。
这里我们可以看到C库中为我们提供了 6 个关于进程程序替换的接口,下面我们就来一 一学习一下!
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1。
- 所以
exec类
函数只有出错的返回值而没有成功的返回值。
命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p,程序就自动搜索环境变量PATH
- e(env) : 表示使用自己维护环境变量
1、execl函数
函数原型:
int execl(const char *path, const char *arg, ...); //此函数的参数属于可变参数
- 第一个参数表示要替换的程序的绝对路径
- 第二个参数表示要执行的程序是谁
- 第三个参数表示要怎样执行这个程序,(可以不写)
- 由于是可变参数,所以参数列表最后一个参数一定要写上
NULL
告诉函数,参数传递完毕
代码示例:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { printf("我是一个进程,我的pid是: %d\n", getpid()); int n = execl("/bin/ls", "ls", "-a", "-l", NULL); if(n == -1) { perror("execl() fail:"); exit(-1); } return 0; }
运行结果:
2、execv函数
函数原型:
int execv(const char *path, char *const argv[]);
- 第一个参数表示要替换的程序的绝对路径
- 第二个参数是一个指向不能改变的指针数组,包含了要执行的程序是谁以及怎么执行的。
- 这个指针数组最后一个元素必须指向NULL,方便告诉函数,参数传递完毕
代码演示:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { char* const argv[]={ "ls", "-a", "-l", NULL }; printf("我是一个进程,我的pid是: %d\n", getpid()); int n = execv("/bin/ls", argv); if(n == -1) { perror("execl() fail:"); exit(-1); } return 0; }
运行结果:
3、execlp函数
函数原型:
int execlp(const char *file, const char *arg, ...);
- 第一个参数表示要执行的程序是谁。
- 第二个参数及以后表示要怎样执行这个程序,(第三个参数可以不写)
- 由于是可变参数,所以参数列表最后一个参数一定要写上
NULL
告诉函数,参数传递完毕
代码示例:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { printf("我是一个进程,我的pid是: %d\n", getpid()); int n = execlp("ls", "ls", "-a", "-l", NULL); if(n == -1) { perror("execl() fail:"); exit(-1); } return 0; }
运行结果:
4、execvp函数
函数原型:
int execvp(const char *file, char *const argv[]);
- 第一个参数表示要执行的程序是谁。
- 第二个参数是一个指向不能改变的指针数组,包含了要执行的程序怎么执行。
- 这个指针数组最后一个元素必须指向NULL,方便告诉函数,参数传递完毕
代码示例:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { char* const argv[]={ "ls", "-a", "-l", NULL }; printf("我是一个进程,我的pid是: %d\n", getpid()); int n = execvp("ls", argv); if(n == -1) { perror("execl() fail:"); exit(-1); } return 0; }
5、execle函数
函数原型:
int execle(const char *path, const char *arg, ...,char *const envp[]);
- 第一个参数表示要替换的程序的绝对路径
- 第二个及以后参数表示要怎样执行这个程序
- 由于是可变参数,所以参数列表倒数第二个参数一定要写上
NULL
告诉函数,参数传递完毕 - 最后一个参数是一个指向不能改变的指针数组,里面记录了自定义的环境变量。
我们来看一看下面的代码来理解这个execle
函数:
#include<iostream> #include<stdlib.h> using namespace std; int main() { cout << "---------------------------" << endl; cout << "这时一个C++的进程,自定义的环境变量是MYNAME:" << endl; cout << (getenv("MYNAME") == NULL ? "NULL" : getenv("MYNAME")) << endl; cout << getenv("PATH") << endl; cout << "---------------------------" << endl; return 0; }
当我们单独运行此C++编写的程序时,由于没有传递"MYNAME"环境变量,所以,我们只能看到NULL,与PATH对应的环境变量。
当我们运行下面的代码时,为 myproc 程序传递了环境变量 argv,我们就能看到"MYNAME"对应的环境变量,但是环境变量表只能有一个,于是我们就看不到了默认的环境变量了。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { printf("我是一个C进程\n"); char* const argv[]={ "MYNAME=you can see me?", NULL }; int n = execle("./practice/myproc", "myproc", NULL, argv); if(n == -1) { perror("execl() fail:"); exit(-1); } return 0; }
运行结果:
如果我们即想要默认的环境变量,又想要自定义的环境变量怎么办呢?我们有两种方法,一种是在Linux命令行中使用export
命令添加想要添加的环境变量,另一种是调用putenv
。
- 利用命令行中的
export
命令
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { extern char** environ; printf("我是一个C进程\n"); int n = execle("./practice/myproc", "myproc", NULL, environ); if(n == -1) { perror("execl() fail:"); exit(-1); } return 0; }
- 调用
putenv
函数
那个进程调用了该函数就会在那个进程在环境变量表里面添加一个环境变量。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { extern char** environ; putenv("MYNAME=you can see me?"); printf("我是一个C进程\n"); int n = execle("./practice/myproc", "myproc", NULL, environ); if(n == -1) { perror("execl() fail:"); exit(-1); } return 0; }
6、总结
讲到这里,对于execvpe
函数相信不用我讲,你也能参照前面的函数给出答案了!
但是我们发现上面的exec
类中少了execve
函数这时怎么回事呢?我们使用man手册查看一下。
execve
是函数调用,在2号手册,刚才我们讲的函数是C库函数,3号手册是C语言的库函数,C库函数exec
类底层调用的都是exceve
系统调用。
三、进程程序替换的补充强调
进程程序替换,我们可以替换任何编程语言写的可执行程序,因为进程程序替换是操作系统提供的系统调用,是系统级别的操作。
下一章我们可以利用进程程序替换制作一个简单的shell程序,加深对于进程程序替换以及shell的运行的理解。