Linux进程的程序替换
Posted 风起、风落
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程的程序替换相关的知识,希望对你有一定的参考价值。
文章目录
1. 程序替换
1.创建子进程的目的是什么?
目标:为了让子进程帮父进程执行特定的任务
- 具体做法:1. 让子进程执行父进程的一部分代码
红框中的代码实际上是父进程的代码,在没有执行fork之前代码就有了,在没有创建子进程之前,父进程的代码加载到内存了,子进程被创建出来是没有独立的代码,这个代码是父进程的代码,父进程通过if判断分流让子进程去跑了
- 2.创建一个子进程不执行父进程的代码,而是让子进程在磁盘当中执行全新的程序,这种操作称之为进程的程序替换
2.了解程序是如何进行替换的
程序替换函数 execl
输入 man execl
查看程序替换接口
int execl(const char *path, const char *arg, …);
括号内部的 . . . 称为 可变参数列表,可以给c函数传递任意个数的参数
第一个参数为 要执行什么命令
第二个参数 为 要怎样执行程序
最后以NULL结尾表示参数传完了
创建test.c文件并输入以下内容
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
printf("begin......\\n");
printf("begin......\\n");
printf("begin......\\n");
printf("begin......\\n");
execl("/bin/ls", "ls", "-a", "-l", NULL);
printf("end........\\n");
printf("end........\\n");
printf("end........\\n");
printf("end........\\n");
运行可执行程序发现,只有begin 以及执行 ls -l -a显示的指令
再次修改test.c文件内容如下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
printf("begin......\\n");
printf("begin......\\n");
printf("begin......\\n");
printf("begin......\\n");
printf("我已经是一个进程啦,我的PID:%d\\n",getpid());
execl("/bin/ls", "ls", "-a", "-l", NULL);
printf("end........\\n");
printf("end........\\n");
printf("end........\\n");
printf("end........\\n");
test.c 经过编译形成mytest可执行程序,./可执行程序就变成进程了,CPU调度进程 ,打印出代码中的打印语句,同时调用程序替换execl,将ls程序执行起来了
[yzq@VM-8-8-centos nn]$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=c8ada1f7095f6b2bb7ddc848e088c2d615c3743e, stripped
使用的 /bin/ls 命令 实际上是一个可执行程序,所以ls程序是在磁盘上的
前面执行的是自己代码的一部分,当调用execl时,将磁盘中可执行程序替换当前进程的代码和数据
后半部分就不执行自己的代码了,执行ls所对应的代码 ,这个现象就叫做程序替换
程序替换就是让一个进程去执行另一个在磁盘中的程序,让一个进程把一个新的程序运行起来
3. 程序替换的基本原理
当前的进程执行当前代码时,如果执行了函数execl等接口,就会根据你所传入的程序的路径以及你要执行的名称及选项,把磁盘当中的一个其他的程序加载到对应的内存,
用新程序的代码替换当前进程的代码段,用当前进程的数据替换老进程的数据段
站在进程的角度
进程的程序替换有没有创建新的进程呢?
没有,只是将新的程序加载到当前进程的代码段和数据段,用CPU去调度当前进程就可以跑起来了
站在程序的角度
程序被加载了内存中,就可以称程序替换的接口(execl) 为加载器
当创建进程的时候,先有进程数据结构,还是先加载代码和数据?
修改test.c文件为以下内容
#include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 int main()
5
6 execl("/bin/ls", "ls", "-a", "-l", NULL); 7
此时运行可执行程序,自己就写了一个ls命令
创建子进程,让子进程调用execl,在调用execl把代码和数据加载到内存
所以当创建进程的时候,先有进程数据结构,再加载代码和数据
程序替换是整体替换,不是局部替换
修改test.c文件内容如下
#include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/wait.h>
5 int main()
6
7 pid_t id=fork();
8 if(id==0)
9
10 //child
11 printf("我是子进程:%d\\n",getpid());
12 execl("/bin/ls", "ls", "-a", "-l", NULL);
13
sleep(5);
14 printf("我是父进程:%d\\n",getpid());
15 waitpid(id,NULL,0);
16
查看子进程完成替换后会不会影响父进程,如果影响父进程,就不应该打印父进程的这句话了
过了5秒钟,父进程结果打印出来,说明父进程不受子进程影响
程序替换只会影响调用进程,进程具有独立性
父子进程都有自己独立的PCB 地址空间 页表 也会自己的映射关系
虽然代码有可能是跟父进程共享,当子进程进行程序替换的时候,子进程会加载新进程的代码和数据
操作系统会发生写时拷贝,将代码和数据进行区分 ,使子进程形成新的映射关系,从而使子进程不会影响到父进程
execl 返回值
如果出错了,execl返回值为-1
修改test.c文件内容如下
#include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/wait.h>
5 int main()
6
7 pid_t id=fork();
8 if(id==0)
9
10 //child
11 printf("我是子进程:%d\\n",getpid());
12 int n=execl("/bin/lsss", "lsss", "-a", "-l", NULL);//lsss命令不存在
13 printf("you can see me:%d\\n",n);
14 exit(0);
15
16 sleep(5);
17 printf("我是父进程:%d\\n",getpid());
18 waitpid(id,NULL,0);
19
20
输入的lsss命令不存在,查询报错后的execl的返回值
程序替换只要成功,就会跑去执行新程序,失败了就会继续向后运行
所以execl程序替换成功不会有返回值——>如果替换失败,一定有返回值——>如果失败了,必定返回——>只要有返回值就失败了
说明不用对execl函数的返回值进行判断,只要继续向后运行一定失败
4. 替换函数
1. execl
int execl(const char *path, const char *arg, …);
l 代表 list 链表
path:代表你想执行谁 (需要带路径)
执行一个程序最基本的原则为:找到它,加载执行它
arg:你想怎么执行它(若想执行ls指令,是只执行ls,还是执行ls- l 、ls -l -a指令
在命令行怎么执行这个命令,就把参数一个一个的传递给execl就可以了
最终以NULL结尾
具体的实现以及返回值问题上面在演示程序替换时已经使用过啦
2. execv
int execv(const char *path, char *const argv[]);
v代表vector 容器
path:代表你想执行谁 (需要带路径)
把原来需要一个一个传的参数放在argv[]数组中
修改test.c文件内容
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/wait.h>
5 int main()
6
7 pid_t id=fork();
8 if(id==0)
9
10 //child
11 printf("我是子进程:%d\\n",getpid());
12 char *const myargv[]="ls", "-l", "-a",NULL;
13 execv("/bin/ls",myargv);
14 exit(0);
15
16 sleep(5);
17 printf("我是父进程:%d\\n",getpid());
18 waitpid(id,NULL,0);
19
20
执行可执行程序,依旧可以执行ls指令
3. execlp
int execlp(const char *file, const char *arg, …);
带p:代表当执行程序的时候,只需要指定程序名即可,系统会自动在PATH环境变量中查找
file: 不需要传路径,只需要把在PATH环境变量的指令传过来
最后以NULL结尾
#include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/wait.h>
5 int main()
6
7 pid_t id=fork();
8 if(id==0)
9
10 //child
11 printf("我是子进程:%d\\n",getpid());
12 execlp("ls", "ls", "-l", "-a",NULL);
13 exit(0);
14
15 sleep(5);
16 printf("我是父进程:%d\\n",getpid());
17 waitpid(id,NULL,0);
18
执行可执行程序,依旧可以执行ls指令
4. execvp
int execvp(const char *file, char *const argv[]);
带p:代表当执行程序的时候,只需要指定程序名即可,系统会自动在PATH环境变量中查找
v代表vector 容器
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
pid_t id=fork();
if(id==0)
//child
printf("我是子进程:%d\\n",getpid());
char* const myargv[]="ls", "-l", "-a",NULL;
execvp("ls", myargv);
exit(0);
sleep(5);
printf("我是父进程:%d\\n",getpid());
waitpid(id,NULL,0);
5. execle
int execle(const char *path, const char *arg,
…, char * const envp[]);
path:代表你想执行谁 (需要带路径)
envp[]:代表自定义环境变量
如果调用程序替换时,若不想让子进程使用父进程的环境列表,想自定义环境变量,就可以自己传一个环境变量
在另一个目录中创建other.cc (以cc为结尾说明是一个c++程序),并输入以下内容
#include <iostream>
#include <unistd.h>
#include<stdlib.h>
using namespace std;
int main()
for(int i = 0; i < 5; i++)
cout<< "我是另一个程序,我的pid是:"<< getpid()<<endl;
cout << " MYENV: " << (getenv("MYENV")==NULL?"NULL":getenv("MYENV")) << endl;
sleep(1);
return 0;
修改test.c文件为以下内容
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
pid_t id=fork();
if(id==0)
//child
printf("我是子进程:%d\\n",getpid());
W> char*const myenv[]="MYENV=YouCanSeeMe", NULL;
execle("/home/mydir/my/mm/myother", "myother", NULL,myenv);
exit(0);
sleep(1);
printf("我是父进程:%d\\n",getpid());
int status=0;
waitpid(id,&status,0);
return 0;
第一个路径为other.cc生成的可执行程序 myother 所在的 绝对路径
2. 自定义shell
编写极简版本的shell(bash)
目标:为了深刻的理解shell的运行原理
输入 ps ajx |grep bash
,发现bash就是一个进程
由于shell是一个进程,所以用while死循环
缓冲区问题
正常来说,运行可执行程序会显示命令行,但是由于没有\\n刷新缓冲区,也没有使用相关的刷新库函数,所以命令行会一直在缓冲区中 直到 程序结束才显示,但是这是个死循环,所以什么都不会显示
执行可执行程序后即可显示命令行
fgets 使用出现空格问题
fgets 标准输入 按行获取
char *fgets(char *s, int size, FILE *stream);
从特定的标准输入当中获取对应的命令行输入,把对应的数据放在缓冲区中
执行可执行程序后,发现在命令行中输入 ls -a ,就会打印出 ls -a,但是输入时会多出一个空行
正常来说,都会使用回车来到下一行,而这个回车被fgets读取到了
将最后的回车符替换成’\\0’
此时就没有空格出现了
完整代码
: mybash.c ? ? ?? buffers
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#define MAX 1024
#define ARGC 64
#define SEP " "
int split (char*commandstr,char*argv[])
assert(commandstr);
assert(argv);
argv[0]=strtok(commandstr,SEP);//在commandstr以空格作为分割符
if(argv[0]==NULL)//切割失败
return -1;
int i=1;
while(1)
argv[i]=strtok(NULL,SEP);//继续切割空格
if(argv[i]==NULL)
break;
i++;
W>
void print(char*argv[])
int i=0;
for(i=0;argv[i];i++)
printf("%d:%s\\n",i,argv[i]);
int main()
while(1)
char commandstr[MAX]=
当我们fork()生成子进程后,子进程的代码与数据可以来自其他可执行程序。把磁盘上其他程序的数据以覆盖的形式给子进程。这样子进程就可以执行全新的程序了,这种现象称为程序替换。
1.进程替换注意事项
1.进程替换不会创建新进程,因为进程替换只是将该进程的数据替换为指定的可执行程序。而进程PCB没有改变,所以不是新的进程,进程替换后不会发生进程pid改变。
2.进程替换后,如果替换成功后则替换函数下的代码不会执行,因为进程替换是覆盖式的替换,替换成功后进程原来的代码就消失了。同理在进程替换失败后会执行替换函数后的代码
3.进程替换函数在进程替换成功后不返回,函数的返回值表示替换失败
4.进程替换成功后,退出码为替换后的进程的退出码
2.替换函数(unistd.h)
execl
函数原型
int execl(const char *path,const char *arg,…)
参数解释
path为可执行程序的路径,arg为如何执行这个可执行程序
…为可变参数,指的是给这执行程序携带的参数,在参数末尾加NULL表示参数结束
返回值:替换失败返回-1,替换成功不返回
eg:
观察上图发现进程替换成功后,替换函数下的打印没有执行,原因与注意事项的第二条相同
如果替换失败:
execlp
函数原型:
int execlp(const char *file, const char *arg,…)
参数解释:
file:要替换的目标程序,arg:如何执行这个程序,…为给这个程序传的参数。最后以NULL结束表示传参结束
返回值:进程替换失败后返回-1。
相比于execl :execp默认在Linux环境变量PATH中查找可执行程序
eg:
execv
函数原型:
int execv(const char* path,char* const argv[ ]);
参数解释:
argv数组:保存的是参数列表,将如何执行 可执行程序 和可执行程序需要的参数保存到字符串数组中,最后以NULL结尾表示参数结束,给execv
path:替换目标程序路径,
返回值:进程替换失败返回-1
eg:
可以发现argv数组与main函数的命令行参数相同
Linux_main函数命令行参数
execle
函数原型:
int execle(const char* path, const char* arg, …,char* const envp[ ])
参数解释:
path为替换的目标程序路径,arg与…表示如何执行替换后的程序以NULL结尾。
envp数组为要导入的环境变量
返回值:
替换失败返回-1
eg:
因为此时我们没有环境变量MYSTR所以第一行打印为空
这里在myProc子进程中用execle函数来导入环境变量MYSTR
注意:
1.导环境变量的数组最后以NULL结尾
2.导入环境变量后原系统环境变量的值被清空,这种导入环境变量的方式为覆盖式导入
替换函数的命名理解(execvp,execve)
替换函数前面的exec不变
l:参数采用列表
v:参数采用数组
p:不需要输入路径,在环境变量自动搜索
e:要导入自己的环境变量
所以execvp表示不需要输入路径,参数用数组传
execve表示需要输入路径,参数用数组传,自己维护环境变量
execvp拓展_简易shell
我们在命令行输入的命令可以用函数fgets来获取
定义一个字符数组
char Shell[100]
fgets(Shell,100,stdin);这样就可以将我们的命令存起来。
因为不同参数之间用空格相隔,所以我们可以用字符串分割函数strtok来分割处理成字符串存到数组中。
strtok函数在没有找到或者最后一次找到时返回NULL,这个函数可以将字符串分割为多个字符串。
execvp:不需要路径,参数用数组传,而strtok分割的正好为命令的参数,我们输入的命令又在环境变量中
所以我们利用进程替换可以实现简易的shell
代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define MAXLEN 1024
#define LEN 32
int main()
{
char shell[MAXLEN]={0};
char* DOS[LEN]={0};//保存处理后的参数
while(1)
{
printf("[dodamce@My_centos dir] ");
fgets(shell,MAXLEN,stdin);
shell[strlen(shell)-1]='\\0';
DOS[0]=strtok(shell," ");
int i=1;
while(DOS[i]=strtok(NULL," "))//strtok按照空格拆分字符串,并且将其存到DOS字符串数组中
{
i++;
}
pid_t id=fork();//创建子进程
if(id==0)
{
//child
execvp(DOS[0],DOS);//不需要路径,DOS第一个元素就是我们要替换的可执行程序
exit(1);
}
int status=0;//父进程阻塞等待,接受并打印子进程的退出码,防止僵尸进程
pid_t wait=waitpid(id,&status,0);
if(wait>0)
{
printf("Exit Code=%d\\n",WEXITSTATUS(status));
}
}
return 0;
}
3.替换函数总结
函数名 参数传递形式 路径 是否导入环境变量 execl 列表 需要可执行程序路径 不导入 使用当前环境变量 execlp 列表 默认在环境变量中找 不导入 使用当前环境变量 execle 列表 需要可执行程序路径 导入 使用导入的环境变量 execv 数组 需要可执行程序路径 不导入 使用当前环境变量 execvp 数组 默认在环境变量中找 不导入 使用当前环境变量 execve 数组 需要可执行程序路径 导入 使用导入的环境变量