Linux 进程程序替换
本章我们将从三方面讲解进程程序替换
1.快速见识进程替换的效果
2.进程替换的原理
3.认识进程程序替换的全部接口
一.进程程序替换
1.进程程序替换导入
首先我们来认识一个函数:execl。它的作用是执行一个可执行命令,例如:
1 #include<stdio.h>2 #include<errno.h>3 #include<string.h>4 #include<stdlib.h>5 #include<unistd.h>6 #include<sys/types.h>7 #include<sys/wait.h>8 9 int main()10 {11 printf("我的程序要运行了!\n");12 execl("/usr/bin/ls","ls","-l","-a",NULL);13 printf("我的程序运行完毕了\n");14 return 0; 15 }
编译成可执行程序,我们运行看看效果
wujiahao@VM-12-14-ubuntu:~/process_test$ ./proc
我的程序要运行了!
total 36
drwxrwxr-x 2 wujiahao wujiahao 4096 Sep 26 15:58 .
drwxr-x--- 11 wujiahao wujiahao 4096 Sep 26 15:58 ..
-rw-rw-r-- 1 wujiahao wujiahao 64 Sep 26 15:55 Makefile
-rwxrwxr-x 1 wujiahao wujiahao 17208 Sep 26 15:58 proc
-rw-rw-r-- 1 wujiahao wujiahao 302 Sep 26 15:58 proc.c
我们发现,在execl上文的printf正常执行,然后执行了命令行程序,execl下文的printf函数却没有执行。我们可以把这个现象叫做进程程序替换。
2.进程程序替换的原理
exec系列接口的原理是什么?
当子进程执行到execl,会重新将函数内指定路径的可执行程序覆盖式的调换到当前物理内存中的数据段,也就是说execl使得当前子进程放下自己原来的执行流,转而去执行指定的可执行文件。
由之前学习的知识我们知道:
进程=数据结构+代码和数据
在这个过程中,映射关系可能有所调整,但是只对代码和数据进行替换,没有创建新的进程。
问题:为什么execl后半句的printf没执行?因为一旦程序替换成功,就去执行新代码了。后续的源代码被覆盖,不存在了。
接着我们来看看exec系列的函数返回值:
man exec
RETURN VALUEThe exec() functions return only if an error has occurred. The return value is -1, anderrno is set to indicate the error.
只有失败时有返回值,成功不会有返回值。也就是说exec函数不需要判断返回值,只要返回就代表出错了。举例:我们故意将可执行文件的路径写错
1 #include<stdio.h>2 #include<errno.h>3 #include<string.h>4 #include<stdlib.h>5 #include<unistd.h>6 #include<sys/types.h>7 #include<sys/wait.h>8 9 int main()10 {11 printf("我的程序要运行了!\n");12 13 int x= execl("/usr/bn/ls","ls","-l","-a",NULL);14 printf("我的程序运行完毕了:%d \n",x); 15 return 0;16 }
运行结果如下
wujiahao@VM-12-14-ubuntu:~/process_test$ ./proc
我的程序要运行了!
我的程序运行完毕了:-1
3.进程程序替换的全部接口
用man查看手册,exec系列的函数如下:我们一一进行介绍
EXEC(3) Linux Programmer's Manual EXEC(3)NAMEexecl, execlp, execle, execv, execvp, execvpe - execute a fileSYNOPSIS#include <unistd.h>extern char **environ;int execl(const char *pathname, const char *arg, .../* (char *) NULL */);int execlp(const char *file, const char *arg, .../* (char *) NULL */);int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);int execv(const char *pathname, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[],char *const envp[]);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):execvpe(): _GNU_SOURCE
1.execl:路径+程序名
int execl(const char *pathname, const char *arg, .../* (char *) NULL */);
可变参数列表:可以简单理解成命令的选项,命令行怎么写,这里怎么传就可以。但必须以NULL结尾,表示参数传递完成。
有没有一种方法,不要让替换影响我们当前的进程,让子进程去执行这个替换呢?
创建子进程,让子进程替换程序,父进程继续做自己的事。
为什么不会影响父进程?从宏观来说,进程具有独立性;从细节来说,数据和代码发生了写时拷贝。
1 #include<stdio.h>2 #include<errno.h>3 #include<string.h>4 #include<stdlib.h>5 #include<unistd.h>6 #include<sys/types.h>7 #include<sys/wait.h>8 9 int main()10 {11 printf("我的程序要运行了!\n");12 if(fork()==0){13 //child14 sleep(1);15 execl("/usr/bin/ls","/usr/bin/ls","-ln","-a",NULL);16 }17 //father18 waitpid(-1,NULL,0); 19 printf("我的程序运行完毕了:%d \n",x);20 return 0;21 }
运行结果如下:由子进程执行这个新任务,发生进程替换,而父进程不受影响。
wujiahao@VM-12-14-ubuntu:~/process_test$ ./proc
我的程序要运行了!
total 36
drwxrwxr-x 2 1002 1003 4096 Sep 26 16:27 .
drwxr-x--- 11 1002 1003 4096 Sep 26 16:27 ..
-rw-rw-r-- 1 1002 1003 64 Sep 26 15:55 Makefile
-rwxrwxr-x 1 1002 1003 17704 Sep 26 16:27 proc
-rw-rw-r-- 1 1002 1003 410 Sep 26 16:27 proc.c
我的程序运行完毕了
程序的加载,本质上都是进程创建的过程;bash是一个进程,他首先fork创建出子进程,然后它开始wait子进程。子进程转而进行程序替换,加载新的程序,就可以让子进程跑命令了。
exec系列的接口,其实就是加载器的范畴。exec允许我们调用自己的程序。比如这里我们要用一个源代码为c的proc调用一个原代码为C++的other:
1 #include<iostream>2 #include<unistd.h>3 4 int main(){5 std::cout<<"hello C++,My Pid is:"<<getpid()<<std::endl;6 return 0; 7 }
那我们的proc.c就可以这样写:
#include<stdio.h>2 #include<errno.h>3 #include<string.h>4 #include<stdlib.h>5 #include<unistd.h>6 #include<sys/types.h>7 #include<sys/wait.h>8 9 int main()10 {11 printf("我的程序要运行了!\n");12 if(fork()==0){13 //child14 printf("I am Child,My Pid is:%d\n",getpid());15 sleep(1);16 // execl("/usr/bin/ls","/usr/bin/ls","-ln","-a",NULL);17 execl("./other","other",NULL);18 //只有execl调用错误,才会执行这里的exit 19 exit(1);20 }21 //father22 waitpid(-1,NULL,0);23 printf("我的程序运行完毕了");24 return 0;25 }
可以看到程序可以正常执行,并且可执行程序的pid和子进程pid相同,说明发生了进程替换。
wujiahao@VM-12-14-ubuntu:~/process_test$ make
gcc -o proc proc.c -g
wujiahao@VM-12-14-ubuntu:~/process_test$ ls
Makefile other other.cc proc proc.c
wujiahao@VM-12-14-ubuntu:~/process_test$ ./proc
我的程序要运行了!
I am Child,My Pid is:3274033
hello C++,My Pid is:3274033
同样的,我们也可以认为我们能够通过exec接口调用py脚本程序,shell脚本程序甚至java程序。这里不再演示。
因为不管是什么类型的程序,只要它执行在操作系统中都会变成进程,就会进行程序替换(除了由浏览器渲染的前端程序)。
2.execlp:不需要携带路径,只需要告诉文件名
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
execlp会默认在PATH环境变量中查找指定命令。后面的参数同上。(带路径也能跑)
3.execv:路径+argv[]
第二个参数以指针数组形式呈现,也就是说未来使用我们需要提供一个命令行参数表。
int main()10 {11 printf("我的程序要运行了!\n");12 if(fork()==0){13 //child14 printf("I am Child,My Pid is:%d\n",getpid());15 sleep(1);16 char *const argv[]={(char* const)"ls",(char* const)"-l",(char* const)"-a",NULL};17 execv("/usr/bin/ls",argv); 18 // execl("/usr/bin/ls","/usr/bin/ls","-ln","-a",NULL);19 // execl("./other","other",NULL);20 //只有execl调用错误,才会执行这里的exit21 exit(1);22 }23 //father24 waitpid(-1,NULL,0);25 printf("我的程序运行完毕了");26 return 0;27 }
我们要考虑清楚:是谁在执行execv?是子进程。ls是c语言写的命令,也有main函数,所以我们可以经过execv,把argv[]传递给ls的main函数。如此我们就能更进一步理解main函数的参数argv[]是由上层程序传参了。
4.execvpe
int execvpe(const char *file, char *const argv[],char *const envp[]);
envp:环境变量表
我们此时需要一个能把这些参数打出来的程序,这里用other.cc为例
1 #include<iostream>2 #include<unistd.h>3 4 int main(int argc,char *argv[],char *env[]){5 std::cout<<"hello C++,My Pid is:"<<getpid()<<std::endl;6 7 //打印命令行参数表8 for(int i=0;i<argc;i++){9 printf("argv[%d]:%s\n",i,argv[i]);10 }11 12 printf("\n");13 //打印环境变量表 14 for(int i=0;env[i];i++){15 printf("env[%d]:%s\n",i,env[i]);16 }17 18 return 0;19 }20
在proc.c中我们也进行修改
printf("我的程序要运行了!\n");12 if(fork()==0){13 //child14 printf("I am Child,My Pid is:%d\n",getpid());15 sleep(1);16 char *const argv[]={(char* const)"ls",(char* const)"-l",(char* const)"-a",NULL};17 char *const env[]={(char* const)"MYVAL:123456",NULL};18 execvpe("./other",argv,env);
执行结果如下
wujiahao@VM-12-14-ubuntu:~/process_test$ ./proc
我的程序要运行了!
I am Child,My Pid is:3280615
hello C++,My Pid is:3280615
argv[0]:ls
argv[1]:-l
argv[2]:-aenv[0]:MYVAL:123456
通过这个例子我们更能理解命令行参数怎么来的:由上层exec系列函数传入
现在的问题是:我们现在打印出来的环境变量,只剩我们自己传入的环境变量了,历史的环境变量就不再使用了。那如果我们想以新增的方式交给子进程,该怎么做?
putenv
谁调用它,就在谁的进程地址空间里新增这个环境变量。
#include<sys/wait.h>8 char * const addenv[]={(char* const)"MYVAL=223344",NULL};9 int main()10 {11 printf("我的程序要运行了!\n");12 if(fork()==0){13 //child14 printf("I am Child,My Pid is:%d\n",getpid());15 sleep(1);16 char *const argv[]={(char* const)"other",(char* const)"-a",(char* const)"-b",NULL};17 // char *const env[]={(char* const)"MYVAL:123456",NULL};18 for(int i=0;addenv[i];i++){19 putenv(addenv[i]);20 }21 extern char ** environ;22 execvpe("./other",argv,environ);
这样我们就完成了新增而不是覆盖的操作
实际上即便不去传环境变量和命令行参数,子进程也会自动获得,这是因为有所谓的继承关系:命令行参数和环境变量在进程地址空间中是独立的一块,子进程会自动拷贝environ的全局指针,指向环境变量表。
execve:系统调用
以上的接口都会最终调用这个系统调用,这样我们也就能理解为什么上面地传递环境变量表会发生覆盖:如果我们自己手动传入环境变量表,就会默认使用我们的,如果不传,系统调用会自动返回
程序替换的内容基本结束,下章我们将前几章的内容串联,手动实现一个简单的shell程序。