Linux之进程替换
进程替换
- 1.什么是进程替换
- 2.替换函数
- 2.1 execl函数
- 2.2 execv函数
- 2.3 execlp函数
- 2.4 execvp函数
- 2.5 在自己的C程序上如何运行其他语言的程序?
- 2.6 execle 函数
- 2.7 小结
- 3.一个简易的shell
1.什么是进程替换
fork()之后,父子各自执行父进程代码的一部分,父子代码共享,数据写时拷贝各自一份,如果子进程想就执行一个全新的进程呢?
进程的程序替换,来完成这个功能。
程序替换:是通过特定的接口,加载磁盘上的一个权限的程序(代码和数据),加载到进程的地址空间中。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exex函数以执行另外的一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新进程替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec函数前后进程的id并未改变。
所以,进程替换,并没有创建新的子进程,所谓的exec*函数,本质就是加载程序的函数。
2.替换函数
*exec 函数:**功能其实就是加载器的底层接口
2.1 execl函数
1 #include <stdio.h>
2 #include <unistd.h>
3
4
5 int main()
6 {
7 printf("当前进程的开始代码!\n");
8
9 // execl("/usr/bin/ls","ls",NULL);
10 // execl("/usr/bin/ls","ls","-l",NULL);
11 execl("/usr/bin/ls","ls","-l","--color=auto","-a",NULL);
12 printf("当前进程的结束代码!\n");
13
14 return 0;
15 }
运行结果:
[jyf@VM-12-14-centos 进程]$ ./mytest11
当前进程的开始代码!
. Makefile myproc.c mytest10 mytest2 mytest4
可见,并没有执行printf(“当前进程的结束代码!\n”);这条语句,进程发生了替换。
execl是程序替换,调用该函数成功之后,会将当前进程的所有代码和数据都进行替换!包括已经执行的和没有执行的!所以一旦调用成功,后续的所有代码都不会执行。
进程替换应用实例
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main()
7 {
8 //为什么我要创建子进程?
9 //如果不创建,我们替换的进程就是父进程,如果创建了,我们替换的进程就是子进程,而不影响父进程。
10 //因为我们想让父进程聚焦在读取数据,解析数据,指派进程执行代码的功能!
11
12 //1.显示一个提示符:root@localhost#
13 //2.获取用户输入的字符串,fgets,scanf,-> ls -a -l
14 //3.对字符串进行解析
15 while(1)
16 {
17 pid_t id = fork();
18 if(id == 0)
19 {
20 //子进程
21 printf("子进程开始运行,pid:%d\n",getpid());
22 sleep(3);
23 execl("/usr/bin/ls","ls","-a","-l",NULL);
24 exit(1);
25 }
26 else{
27 ;//父进程
28 printf("父进程开始运行:%d\n",getpid());
29 int status = 0;
30 pid_t id = waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出!
31 if(id>0)
32 {
33 printf("wait success,exit code:%d\n",WEXITSTATUS(status));
34 }
35 }
36 }
37 return 0;
38 }
加载新程序之前,父子进程的代码和数据的关系?代码共享,数据写时拷贝。
当子进程加载新程序的时候,不就是一种写入吗?代码要不要写时拷贝呢?将父子进程的代码分离?必须分离。
int execl(const char* path,const char* arg,…); 父子进程在代码和数据上就彻底分开了,虽然曾经并不冲突。
2.2 execv函数
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("子进程开始运行:%d\n",getpid());
char* const _argv[] = {(char*)"ls",(char*)"-a",(char*)"-l",NULL};
execv("/usr/bin/ls",_argv);
exit(1);
}
else
{
printf("父进程开始运行:%d\n",getpid());
int status =0;
pid_t id = waitpid(-1,&status,0);
if(id>0)
{
printf("wait success,exit code:%d\n",WEXITSTATUS(status));
}
}
return 0;
}
运行结果:
[jyf@VM-12-14-centos lesson3-28]$ ./mytest
父进程开始运行:26522
子进程开始运行:26523
total 36
drwxrwxr-x 2 jyf jyf 4096 Mar 28 22:45 .
drwx------ 19 jyf jyf 4096 Mar 28 17:26 ..
-rwxrwxr-x 1 jyf jyf 8616 Mar 28 22:45 mytest
-rwxrwxr-x 1 jyf jyf 8616 Mar 28 21:57 mytest1
-rw-rw-r-- 1 jyf jyf 532 Mar 28 22:45 test1.c
wait success,exit code:0
2.3 execlp函数
要执行程序,必须先找到程序!带路径,不带路径都能找到吗?只要在环境变量中存在即可。
我会自己在环境变量PATH中进行查找,你不用告诉我你要执行的程序在哪里!!
2.4 execvp函数
// int execvp(const char *file, char *const argv[]);
execvp("ls",_argv);
const char* file:具体要执行的程序,它会到环境变量里面去找
具体实施方法如execv函数。
2.5 在自己的C程序上如何运行其他语言的程序?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
const char* myfile = "/home/jyf/lesson3-28/mycmd";
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("子进程开始运行:%d\n",getpid());
W> char* const _argv[] = {(char*)"ls",(char*)"-a",(char*)"-l",NULL};
// execv("/usr/bin/ls",_argv);
//execlp("ls","ls","-a","-l",NULL);
//execvp("ls",_argv);
//execl(myfile,"mycmd","-a",NULL);
//execlp("python","python","test.py",NULL); //运行python程序
//execl("/usr/bin/python","python","test.py",NULL); //运行python程序
execlp("bash","bash","test.sh",NULL); //运行shell程序
exit(1);
}
else
{
printf("父进程开始运行:%d\n",getpid());
int status =0;
pid_t id = waitpid(-1,&status,0);
if(id>0)
{
printf("wait success,exit code:%d\n",WEXITSTATUS(status));
}
}
return 0;
}
2.6 execle 函数
int execle(const char *path, const char *arg, ..., char * const envp[]);
char* const envp[ ]:将环境变量传递给要替换的程序
具体做法请细看:
exec.c文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
# define NUM 16
extern char** environ;
const char* myfile = "/home/jyf/lesson3-28/mycmd";
//const char* myfile = "./mycmd";
int main()
{
char* const envp[NUM] = {
W> "MY_VAL=1234567", //中间不能加空格!!!
NULL
};
pid_t id = fork();
if(id == 0)
{
printf("子进程开始运行:%d\n",getpid());
W> char* const _argv[NUM] = {(char*)"ls",(char*)"-a",(char*)"-l",NULL};
// execv("/usr/bin/ls",_argv);
//execlp("ls","ls","-a","-l",NULL);
//execvp("ls",_argv);
//execl(myfile,"mycmd","-a",NULL);
//execlp("python","python","test.py",NULL);
//execl("/usr/bin/python","python","test.py",NULL);
//execlp("bash","bash","test.sh",NULL);
execle(myfile,"mycmd","-a",NULL,envp);
exit(1);
}
else
{
printf("父进程开始运行:%d\n",getpid());
int status =0;
pid_t id = waitpid(-1,&status,0);
if(id>0)
{
printf("wait success,exit code:%d\n",WEXITSTATUS(status));
}
}
return 0;
}
mycmd.c文件
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
if(argc != 2)
{
printf("can not execute!\n");
exit(1);
}
printf("获取环境变量:MY_VAL:%s\n",getenv("MY_VAL"));
if(strcmp(argv[1],"-a") == 0)
{
printf("hello a!\n");
}
else if(strcmp(argv[1],"-b") == 0)
{
printf("hello b!\n");
}
else
{
printf("default!\n");
}
return 0;
}
运行结果:
2.7 小结
命名理解:这些函数看起来很容易混,但只要掌握规律就很好记。
l(list):表示参数要自己给具体路径
v(vector):表示可变参数都放到数组中
p(path):有p自动在环境变量中搜索PATH
e(env):表示自己创建维护环境变量,将环境变量传递给要替换的进程
为什么要替换?
一定和应用场景有关,我们有时候必须让子进程执行新的程序
3.一个简易的shell
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char* g_argv[SIZE];
//写一个环境变量的buffer,用来测试
char g_myval[64];
//shell 运行原理:父进程读取命令,解析命令,派发给子进程命令,让子进程去执行命令
int main()
{
//0.命令行解释器,一定是一个常驻内存的进程,不退出
while(1)
{
//1.打印出提示信息,[root@localhost myshell]#
printf("[root@localhost myshell]# ");
fflush(stdout);
memset(cmd_line,'\0',sizeof(cmd_line));
//2.获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
if(fgets(cmd_line,sizeof(cmd_line),stdin) == NULL)
{
continue;
}
cmd_line[strlen(cmd_line)-1] = '\0';//将回车键'\n',替换成'\0'
//"ls -a -l -i\n\0"
// printf("echo %s\n",cmd_line);
g_argv[0] = strtok(cmd_line,SEP);//将输入的字符串按空格进行分割,第一次调用,要传入原始字符串
int index = 1;
if(strcmp(g_argv[0],"ls") == 0) //ls自带调色实现
{
W> g_argv[index++] = "--color=auto";
}
if(strcmp(g_argv[0],"ll") == 0) //ll命令的简写的实现
{
W> g_argv[0] = "ls";
W> g_argv[1] = "-l";
W> g_argv[2] = "--color=auto";
index+=2;
}
W> while(g_argv[index++] = strtok(NULL,SEP));//第二次调用,如果还要解析原始字符串,传入NULL
//for debug
//for(index =0;g_argv[index];index++)
//{
//procrintf("g_argv[%d]: %s\n",index,g_argv[index]);
//}
//4.TODO,内置命令,让父进程自己执行的命令,我们叫内置命令,内建命令
//内建命令本质其实就是shell中的一个函数调用
if(strcmp(g_argv[0],"cd") == 0) //not child execute ,father execute
{
if(g_argv[1]!=NULL)
{
chdir(g_argv[1]); //cd path
}
continue;
}
if(strcmp(g_argv[0],"export") == 0&&g_argv[1]!=NULL)
{
//int i=0;
strcpy(g_myval,g_argv[1]);
int ret = putenv(g_myval);
if(ret ==0) printf("%s,export success\n",g_myval);
//for(i=0;environ[i];i++)
// {
// printf("%d:%s\n",i,environ[i]);
// }
//continue;
}
//5.fork()
pid_t id = fork();
if(id ==0)
{
//printf("下面功能让子进程进行的\n");
//printf("child:%s\n",getenv("MY_VAL"));
//printf("child:%s\n",getenv("PATH"));
//不是说好程序替换会替换代码和数据吗?
//环境变量相关的数据,会被替换吗?不会!!!
//因为这些东西是被写入到配置文件的
execvp(g_argv[0],g_argv);
exit(1);
}
}
//father
int status = 0;
pid_t ret = waitpid(-1,&status,0);
if(ret>0)
{
//printf("exit code:%d\n",WEXITSTATUS(status));
}
}
return 0;
}
shell执行的命令 通常有两种
1.第三方提供的在磁盘上有具体二进制文件的可执行程序(由子进程执行)
2.shell内部,自己实现的方法,由自己(父进程)来执行
为什么不都让子进程来执行呢?
因为有些命令像cd,export,就是要影响shell本身的,shell代表的是用户。
重要的小知识
我们putenv(),即导入一个环境变量,并不是将此环境变量的值拷贝一份放到环境变量表的后面,而是在环境变量表的后面用一个指针指向你新加入的环境变量的地址;像上面的简易shell一样,我们之前明明已经将我们所需要的环境变量的值导入到了环境变量表中,但子进程为什么还是无法getenv()获取呢?因为上面的简易shell中,我们的环境变量的值存储在g_argv[1]中,但g_argv[1]每次输入后都会变的,它的存储值在改变,也就是说环境变量表后面的指针指向的地址的里面的值在变化,所以,后面子进程无法获取到。
解决方法:用一个buffer数组来存储我们新加的环境变量。
不是说好程序替换会替换代码和数据吗?
环境变量相关的数据,会被替换吗?不会!!!
因为这些东西是被写入到配置文件的
环境变量,是写在配置文件中的,shell启动的时候,通过读取配置文件获得起始环境变量。(进入到用户的主目录,cd ~,在vim .bash_profile可查看)
内容如下: