进程替换
一、概念
当我们fork()生成子进程后,子进程的代码与数据可以来自其他可执行程序。把磁盘上其他程序的数据以覆盖的形式给子进程。这样子进程就可以执行全新的程序了,这种现象称为程序替换。
二、原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
三、替换函数
1. execl
①函数原型:
c
int execl(const char*path,const char*arg,...)
②参数解析:
- path:为可执行程序的路径;
- arg:如何执行这个可执行程序(执行该程序的指令);
- ... :指的是给这执行程序携带的参数,在参数末尾加NULL表示参数结束;
- 返回值:函数调用失败返回-1,成功不返回
③示例:
替换成功:
c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("before\n");
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("after\n");
return 0;
}
替换失败:
c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("before\n");
execl("/usr/bin/la","ls","-a","-l",NULL);
printf("after\n");
return 0;
}
通过对比可以发现,替换成功以后,原程序后面的代码就不执行了,执行另一个程序 的代码,失败就接着执行原程序的代码。
2. execlp
①函数原型:
c
int execlp(const char *file, const char *arg, ...);
②参数解析:
- file:要替换的目标程序;
- arg:如何执行这个程序,...为给这个程序传的参数;
- 返回值:函数调用失败返回-1;
③示例:
比较于execl,execp默认在Linux环境变量PATH中查找可执行程序,所以传参可直接传可执行程序的名字,不用传绝对路径。
c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("before\n");
execlp("ls","ls","-a","-l",NULL);
printf("after\n");
return 0;
}
3.execle
①函数原型:
c
int execle(const char *path, const char *arg, ..., char * const envp[]);
②参数解析:
- path:替换目标程序路径;
- arg:如何执行这个程序,...为给这个程序传的参数;
- envp数组:要导入的环境变量;
- 返回值:失败返回-1;
③示例:
查看环境变量代码,用来验证后面的结果
c
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("path:%s\n",getenv("PATH"));
printf("aaa:%s\n",getenv("aaa"));
return 0;
}
c
#include<stdio.h>
#include<unistd.h>
int main()
{
char* envp[]={"aaa=hello",NULL};
printf("before\n");
execle("./a.out","./a.out",NULL,envp);
printf("after\n");
return 0;
}
注意:
- 导环境变量的数组最后以NULL结尾
- 导入环境变量后原系统环境变量的值被清空,这种导入环境变量的方式为覆盖式导入
4.execv
①函数原型:
c
int execv(const char *path, char *const argv[]);
②参数解析:
- path:替换目标程序路径;
- argv数组:保存的是参数列表,将如何执行 可执行程序 和可执行程序需要的参数保存到字符串数组中,最后以NULL结尾表示参数结束;
- 返回值:进程替换失败返回-1
③示例:
c
#include<stdio.h>
#include<unistd.h>
int main()
{
char* argv[]={"ls","-a","-l",NULL};
printf("before\n");
execv("/usr/bin/ls",argv);
printf("after\n");
return 0;
}
注:可以发现argv数组与main函数的命令行参数相同
5.execvp
①函数原型:
c
int execvp(const char *file, char *const argv[]);
②参数解析:
- file:要替换的目标程序;
- argv数组:保存的是参数列表,将如何执行 可执行程序 和可执行程序需要的参数保存到字符串数组中,最后以NULL结尾表示参数结束;
③示例:略
6.execvpe
①函数原型:
c
int execvpe(const char *file, char *const argv[], char *const envp[]);
②参数解析:
- file:要替换的目标程序;
- argv数组:保存的是参数列表,将如何执行 可执行程序 和可执行程序需要的参数保存到字符串数组中,最后以NULL结尾表示参数结束;
- envp数组:要导入的环境变量;
③示例:略
小结:
替换函数前面的exec不变
- l:参数采用列表
- v:参数采用数组
- p:不需要输入路径,在环境变量自动搜索
- e:要导入自己的环境变量
所以execvp表示不需要输入路径,参数用数组传
execve表示需要输入路径,参数用数组传,自己维护环境变量
四、实现一个简易的shell
c
#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<string.h>
#define MAX 1024
#define ARGC 64
#define SEP " "
//将输入的字符串切割并保存到argv中
int split(char*commandstr,char*argv[])
{
assert(commandstr);
assert(argv);
argv[0]=strtok(commandstr,SEP);
if(argv[0]==NULL)return -1; //若为NULL,则重新输入
int i=1;
while(argv[i++]=strtok(NULL,SEP));
return 0;
}
int main()
{ while(1)
{
char commandstr[MAX]={NULL}; //用于保存用户输入的指令
char*argv[ARGC]={NULL};
printf("[lx@hecs-%d myshell]$ ",getpid());
fflush(stdout);
char*s=fgets(commandstr,sizeof(commandstr),stdin); //获取指令
assert(s);
(void)s;
commandstr[strlen(commandstr)-1]='\0'; //去掉键盘输入的'\n'
int n=split(commandstr,argv); //切割输入的指令字符串
if(n!=0)continue;
pid_t id=fork();
if(id==0)
{
execvp(argv[0],argv); //程序替换
exit(0);
}
int status=0;
waitpid(id,&status,0); //等待子进程
}
return 0;
}
可以看到简易版的myshell就实现好了。
myshellplus
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <assert.h>
#define MAX 1024
#define ARGC 64
#define SEP " "
int split(char* commandstr,char* argv[])
{
assert(commandstr);
assert(argv);
argv[0] = strtok(commandstr,SEP);
if(argv[0] == NULL) return -1;
int i = 1;
while((argv[i++] = strtok(NULL,SEP)));
return 0;
}
void showEnv()
{
extern char** environ;
for(int i = 0; environ[i]; i++) printf("%d:%s\n",i,environ[i]);
}
int main()
{
extern int putenv(char* string);
char myenv[32][256];
int env_index = 0;
int exitCode = 0;
while(1)
{
char commandstr[MAX] = {0};
char* argv[ARGC] = {NULL};
printf("[hxy@mychaimachine]$ ");
fflush(stdout);
char* s = fgets(commandstr,sizeof(commandstr),stdin);
assert(s);
(void)s;
commandstr[strlen(commandstr) - 1] = '\0'; // 去掉键盘输入的\n
int n = split(commandstr,argv); // 切割字符串
if(n != 0) continue;
if(strcmp(argv[0],"cd") == 0)
{
if(argv[1] != NULL) chdir(argv[1]);
continue;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] != NULL)
{
strcpy(myenv[env_index],argv[1]); // 用户自己定义的环境变量,需要bash自己来维护
putenv(myenv[env_index++]);
}
continue;
}
else if(strcmp(argv[0],"env") == 0)
{
showEnv(); // env查看环境变量时,其实看的是父进程bash的变量
continue;
}
else if(strcmp(argv[0],"echo") == 0)
{
const char* target_env = NULL;
if(argv[1][0] == '$')
{
if(argv[1][1] == '?')
{
printf("%d\n",exitCode);
continue;
}
else target_env = getenv(argv[1] + 1);
if(target_env != NULL) printf("%s = %s\n",argv[1] + 1,target_env);
}
continue;
}
// ls设置颜色选项
if(strcmp(argv[0],"ls") == 0)
{
int pos = 0;
while(argv[pos] != NULL)
{
pos++;
}
argv[pos++] = (char*)"--color=auto";
argv[pos] = NULL;
}
pid_t id = fork();
if(id == 0)
{
// 子进程
execvp(argv[0],argv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
exitCode = WEXITSTATUS(status); // 获取最近一次进程的退出码
}
}
return 0;
}
进程替换的知识就讲到这了,如有错误还望指出,886!!!