前言
上一节我们介绍了 **进程终止**和 **进程等待**等一系列问题,并做了相应的验证,本章将继续对进程控制进行介绍,重点学习进程程序替换,并进行相应验证,在此基础上,自己模拟实现一个shell,该shell能够实现执行命令操作。。
目录
- 1.进程程序替换
- 1.1为什么要做进程程序替换?
- 1.2 进程程序替换的原理
- 1.3 六个exec替换函数
- 1.3.1 execl函数:
- 1.3.2 execv 函数:
- 1.3.3 execlp函数:
- 1.3.4 execvp函数:
- 1.3.5 execle函数:
- 1.3.6 execve函数:
- 1.4 实现简易版shell
- 1.4.1 Shell 内建命令等问题的解决
1.进程程序替换
概念引入:
将可执行程序加载到内存,并且重新调整 子进程的页表映射,使之指向新的进程的代码和数据段,这种过程就叫做程序替换
- 用fork创建子进程后执行的是和父进程相同的程序,因为代码共享
- 但,如果我们想让创建出来的子进程,执行全新的程序呢,此时就需要用到进程的程序替换。
- 进程程序替换就相当于一个加载器的角色。
1.1为什么要做进程程序替换?
原因:
- 原因是我们想让我们的子进程执行一个全新的程序。
- 不同语言写的功能(比如python shell) 互相调用,这就是为什么要有程序替换的原因。
我们一般在服务器设计(Linux编程)的时候,往往需要子进程干两件种类事情:
- 1.让子进程执行父进程的代码片段(代码共享)
- 2.让子进程执行磁盘中一个全新的程序(使用shell脚本, 想让客户端执行对应的程序,通过我们的进程,执行其他人写的进程代码等等)
1.2 进程程序替换的原理
程序替换的原理:
- 将磁盘中的待执行的程序,加载入内存结构
- 重新建立页表映射,谁执行程序替换,就重新建立子进程的映射关系
效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!
![
](https://i-blog.csdnimg.cn/direct/f4c725a0e19742b0831dec30e48d9525.png)
调整子进程的页表,让其不再与父进程代码和数据有任何关系,而是指向自己的代码和自己的数据区
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
总结:
- 说白了就是让fork创建子进程,不想让子进程执行父进程代码片段。
- 我们想让子进程执行磁盘当中全新的程序,而且我们没有创建新的进程。
- 因为子进程的内核数据结构基本没变,只是重新建立了虚拟到物理的映射关系罢了
1.3 六个exec替换函数
程序替换的是子进程:(重点)
进程替换永远影响的是进程的本身,子进程的替换永远不会影响父进程,因为进程具有独立性
重新建立的是页表映射但并不影响内核数据结构的具体情况.
其实有六种以exec开头的函数,统称exec函数:
#include <unistd.h> %需要包含头文件
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数解释
- 这些函数如果调用成功,则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值(注意:此函数调用失败,才有返回值)
函数命名理解
- (list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
1.3.1 execl函数:
int execl(const char *path, const char *arg, ...);
path:这个是路径,可执行程序的路径
arg: 可变参数,可以传多个参数,参数要以列表形式写,比如(1s -1 -a) ,就要写成"ls",“-l”,“-a”,NULL,
注意:最后必须是NULL结尾,表示参数传递完毕。
代码演示:
#include <stdio.h>
#include <unistd.h>
int main()
{
//让我的程序执行系统上的: ls -a -i这样的一个命令
printf("我是一个进程,我的pid是 : %d\n", getpid());
//int ret = execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //带选项
//替换失败的情况
int ret = execl("/usr/bin/lsssss", "ls", "-l", "-a", NULL); //带选项
printf("我执行完毕了,我的pid : %d, ret = %d\n", getpid(), ret);
return 0;
}
- 一旦替换成功,是将当前进程的代码和数据全部替换了!!
- 前一个printf被执行是因为程序替换并没有执行。
1.3.2 execv 函数:
int execv(const char *path, char *const argv[]); %%
实现的功能和execl一模一样。
path:这个是路径,可执行程序的路径
argv[]: 和execl的唯一区别就是传参方式的不一样,这个要传入数组har* const argv_[] = {"ls","-l","-a","-i", NULL};
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
printf("我是父进程,我的pid是:%d\n", getpid());
pid_t id = fork();
if(id == 0)
{
//子进程
//我们要子进程执行全新的程序,以前我们是子进程执行父进程的代码片段
printf("我是子进程,我的pid是:%d\n", getpid());
//char* const argv_[] = {
// (char*)"ls",
// (char*)"-l",
// (char*)"-a",
// (char*)"-i",
// NULL
//};
char* const argv_[] = {
(char*)"top",
NULL
};
//execv("/usr/bin/ls", argv_);
execv("/usr/bin/top", argv_);
}
//一定是父进程
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
sleep(2);
printf("进程等待成功!\n");
}
return 0;
}
1.3.3 execlp函数:
int execlp(const char *file, const char *arg, ...);
file: 你想执行程序的名字,注意:命名中带P的,可以不用带可执行程序的路径。
arg:以列表形式传参
代码演示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
//有时候不想让父进程做一件事,只想让子进程做一件事
//将进程创建引入进来
int main()
{
printf("我是父进程,我的pid是:%d\n", getpid());
pid_t id = fork();
if(id == 0)
{
printf("我是子进程,我的pid是:%d\n", getpid());
execlp("ls", "ls", "-a", "-l", "-i", NULL);//这里出现了两个ls,含义一样吗?-- 不一样!
//第一个参数是供系统去找要执行谁的指令,后面一坨是表示如何执行该指令
exit(100); //只要执行了exit,就意味着,execl系列的函数失败了 -- 进程替换失败了
}
//一定是父进程
int status = 0;
int ret = waitpid(id, &status, 0);%0代表阻塞等待
if(ret == id)
{
sleep(2);
printf("wait success, ret : %d, 我所等待子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0x7F);
}
return 0;
}
1.3.4 execvp函数:
作用和execIp是一样的,只不过传参形式不一样。。
int execvp(const char *file, char *const argv[]);
file: 你想执行程序的名字,注意:命名中带P的,可以不用带可执行程序的路径。
arg:以数组形式传参
代码演示:
char* const argv_[] = {
(char*)"top",
NULL
};
execvp("ls", argv_ );
1.3.5 execle函数:
多了一个参数,是环境变量。
int execle(const char *path, const char *arg, ..., char * const envp[]);
path:这个是路径,可执行程序的路径
arg: 可变参数,可以传多个参数,参数要以列表形式写,比如(1s -1 -a) ,就要写成"ls",“-l”,“-a”,NULL,
注意:最后必须是NULL结尾,表示参数传递完毕。
envp:环境变量
代码演示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
//环境变量的指针声明
extern char** environ;
printf("我是父进程,我的pid是:%d\n", getpid());
pid_t id = fork();
if(id == 0)
{
printf("我是子进程,我的pid是:%d\n", getpid());
//我们来手动导入一个环境变量
char* const env_[] = {
(char*)"MYPATH=You Can See Me!!",
NULL
};
//e: 添加环境变量给目标进程,是覆盖式的!
//execle("./mycmd", "mycmd", NULL, env_);
//execle("/usr/bin/ls", "ls", NULL, env_);
execle("./mycmd", "mycmd", NULL, environ);
exit(100); //只要执行了exit,就意味着,execl系列的函数失败了 -- 进程替换失败了
}
//一定是父进程
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
sleep(2);
printf("进程等待成功!\n");
}
return 0;
}
使用execle()添加环境变量给目标进程,是覆盖式的!会把原来的environ环境变量都改掉
所以环境变量只剩下MYPATHT。
正确做法是:将全部环境变量传过去,将environ传过去。
补充重点1:
- 子进程会继承父进程的环境变量的,当父进程调用fork()创建子进程时,子进程会继承父进程的所有环境变量。
- 当子进程调用execlp()等函数执行其他程序时,子进程也会继承父进程的环境变量.
- 如果需要在子进程中更改环境变量,可以使用setenv()或putenv()等函数进行更改.
- 更改的环境变量只会影响当前进程和它的子进程,并不会影响父进程或其他进程的环境变量
补充重点2:
ls 是一个常见的系统命令, 它通常位于系统的某个标准路径(如 /bin 或 /usr/bin),即使 PATH 为空,execlp() 会检查这些标准路径,找到 ls 的可执行文件并执行它。
如果PATH环境变量为空,execlp()函数会无法在环境变量中查找可执行文件的路径。但是,execle()函数会检查一些默认路径,例如/bin、/usr/bin等,来查找可执行文件。因此,即使PATH为空execle()函数也可能会在这些默认路径中找到可执行文件并执行它。
但是,如果在默认路径中也找不到可执行文件,则execle()函数会执行失败,并将errno设置为ENOENT,表示无法找到可执行文件。因此,如果需要执行特定路径下的可执行文件,最好使用execv()或execve()等函数,并指定可执行文件的完整路径。这样可以避免依赖PATH环境变量来查找可执行文件的路径。
1.3.6 execve函数:
int execve(const char *path, char *const argv[], char *const envp[]);
有了上面的execle()函数的讲解,理解就不复杂了,只是第二个参数传的不同,这里传的是一个指针数组。
为什么有那么多的接口?
- 目的是:适配应用场景。
- 其实上述函数都是对系统调用接口的封装。
1.4 实现简易版shell
只要我们懂得了程序替换的原理,会用程序替换的接口,就很好理解:
- shell本身执行起来就是个死循环
- shell创建子进程,将子进程给替换掉就ok了
要写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父等待子进程退出(wait)
#include <stdio.h>
#include <string.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define SEP " "
#define MAX_CMD 1024
#define SIZE 128
char command_line[MAX_CMD];
char* command_args[SIZE];
char env_buffer[NUM];
extern char** environ;
//对应上层的内建命令
int ChangeDir(const char* new_path)
{
chdir(new_path);
return 0;//调用成功
}
void PutEnvInMyShell(char* new_env)
{
putenv(new_env);
}
int main()
{
//shell本质就是一个死循环
while(1)
{
//不关心获取这些属性的接口,搜索一下都有
//1.显示提示符
printf("[用户名@我的主机名 当前目录]# ");
fflush(stdout);
//2.获取用户输入
memset(command_line, '\0', sizeof(command_line)); //初始化
//从键盘获取,标准输入,stdin,获取到的是C风格的字符串(stdio.h结尾的),'\0'结尾
fgets(command_line, NUM, stdin);
command_line[strlen(command_line) - 1] = '\0';//清空\n回车
//printf("%s\n", command_line);
//3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分 -- 因为这些参数一定得以列表或者数组方式传递给程序替换接口
//shell必须切分,因为必须调用execl函数
//将第一个字符串地址用0号下标指向,第二个字符串地址用1号下标指向
command_args[0] = strtok(command_line, SEP);
int index = 1;
//给ls命令添加颜色: 如果提取出来的程序名是ls -- 1下标设置成改颜色的
if(strcmp(command_args[0], "ls") == 0) command_args[index++] = (char*)"--color=auto";
//strtok截取成功返回字符串起始地址
//截取失败,返回NULL
while(command_args[index++] = strtok(NULL, SEP));
//for debug
//int i = 0;
//for(i = 0; i < index; i++)
//{
// printf("%d : %s\n", i, command_args[i]);
//}
//4.TODO -- 编写后面的逻辑,内建命令(由父Shell自己实现的自己调用的一个函数)
if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
{
//让调用方进行路径切换,父进程
ChangeDir(command_args[1]);
continue;
}
//走到这里一定是将命令行参数解析完了,包括命令 + 选项
//将环境变量的信息导入在了父进程的上下文当中
if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
{
//环境变量列表(是个指针数组,每个元素是个指针指向一个环境变量)
//我们传的是一个字符串首地址,但是环境变量的内容还是我们自己维护的
//目前,环境变量信息在comman_line,会被清空,那么环境变量当然就没有了
//所以此处我们需要自己保存一下环境变量的内容
strcpy(env_buffer, command_args[1]);
PutEnvInMyShell(env_buffer);
//PutEnvInMyShell(command_args[1]);//MYENV=112233
continue;
}
//5.创建进程Fork,执行
//如果自己直接程序替换的话,就把自己写的shell给替换了
pid_t id = fork();
if(id == 0)
{
//子进程
//6.程序替换
//execvpe(command_args[0], command_args, environ);
execvp(command_args[0], command_args);
exit(1);//执行到这里,子进程一定替换失败了
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("等待子进程成功: sig: %d, code: %d\n", status & 0x007F, (status & 0xFF00) >> 8);
}
}//end while
return 0;
}
1.4.1 Shell 内建命令等问题的解决
-
cd命令的处理
在命令行中使用cd指令,会跳转路径,如果使用绝对命令,就不行了 -
一个进程也存在对应路径,进程对应的路径可以理解成:当进程启动的时候,在那个路径启动,这个进程所在的路径就是当前进程所启动的路径。
-
如果我们不对cd进行特殊处理,则子进程路径切换后,并不影响父进程的路径,会发现命令路径还是没变化。
所以,对应这个命令,就不能用程序替换的方式,来执行一些特殊的命令了。
而是,在父进程中将一些命令,单独处理。
重点:
程序替换影响的是子进程和父进程没关系,子进程一 跑就完了,曾经所有的操作就没有意义,路径切换就没意义了,所以我们要让父进程的路径发生变化。
如果有些行为,是必须让父进程shell执行的,不想让子进程执行,绝对不能创建子进程!只能是父进程自己实现对应的代码!
正确做法:
使用系统中,更改工作目录的函数。
if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
{
//让调用方进行路径切换,父进程
ChangeDir(command_args[1]);
continue;
}
-
内建命令:
由shell自己执行的命令,我们称之为内建(内置bind- in)命令。 -
export的处理:
导入环境变量: -
export不是一个可执行程序和cd,ls,cat等指令不同。
-
export是一个shell内置命令,用于设置环境变量。它并不是一个可执行程序,而是由shell解释器直接执行的命令
所以,在使用execvp进行程序替换的时候,是不能替换成功的!
注意:
- 环境变量是属于系统的数据,子进程在执行程序替换时,当前进程的环境变量数据,不会被替换掉,而且是以父进程为模版继承下来的
- 所以才会让父进程以内建命令的方式putenv,子进程就能直接获取了.
- 环境变量会被子进程继承下去,所以他会有全局属性。
- 必须,Export放在父进程的内建命令中实现,因为放在子进程中,父进程内的环境变量,就没有改变,是有问题的。。
尾声
看到这里,相信大家对这个Linux有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦