上一节我们讲了进程终止和进程等待等一系列问题,并做了相应的验证,本章将继续对进程控制进行学习,我们将学习进程程序替换,进行相关验证,运用系统进程程序替换接口,自己模拟写一个shell,该shell能够实现执行指令,等一系列命令行操作……
概念引入:
将可执行程序加载到内存,并且重新调整子进程的页表映射,使之指向新的进程的代码和数据段,这种过程就叫做程序替换。
子进程执行的是父进程的代码片段,那么如果我们想让创建出来的子进程,执行全新的程序呢?
此时就要用到:进程的程序替换。
原因:
我们一般在服务器设计(Linux编程)的时候,往往需要子进程干两件种类事情:
程序替换的原理:
效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!
父进程的映射关系:
调整子进程的页表,让其不再与父进程代码和数据有任何关系,而是指向自己的代码和自己的数据区。
注意:
小结:
fork
创建子进程,不想让子进程执行父进程代码片段。PID
都不变,压根就没有创建新的进程,只不过让新的进程执行了不同的程序罢了。每一个进程都有自己的CPU上下文,进程切换时会保存CPU数据。
上述我们讲了什么是程序替换,下面就要来见见猪跑了。
程序替换是由操作系统完成的,调用系统调用接口来完成操作。
明确告诉OS,我想怎么执行这个程序是什么,要不要带选项。
我们平时在命令行中敲的指令都是一个一个可执行程序。
六个exec替换函数:
int execl(const char *path, const char *arg, ...);
path:
arg:
… :
第一个参数是解决了,程序在哪里的问题,第二个参数往后所有的参数,解决的都是程序如何执行的问题。
代码演示:
#include
#include
int main()
{
//让我的程序执行系统上的: ls -a -i这样的一个命令
printf("我是一个进程,我的pid是 : %d\n", getpid());
//int ret = execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //带选项
//execl("/usr/bin/top", "top", NULL); //不带选项
//execl("/usr/bin/which", "which", "pwd", NULL); //不带选项
//下面这行代码没有打印出来
//一旦代码执行到这里,必然是进程替换失败了
//替换失败的情况
int ret = execl("/usr/bin/lsssss", "ls", "-l", "-a", NULL); //带选项
printf("我执行完毕了,我的pid : %d, ret = %d\n", getpid(), ret);
return 0;
}
一旦进程替换成功了,就不会再执行程序替换函数以后的代码了,因为直接去是该进程被替换掉了。
显而易见,代码中程序替换以后的打印内容并没有显示出来,说明进程替换以后的代码压根就没执行,而是去执行ls进程了。
总结:
所以程序替换不用判断返回值:
不需要返回值,一旦有值返回那么必然是返回失败了!!!因为只要成功了,就不会有返回值,而失败的时候,必然会继续向后执行!!最多通过返回值得到什么原因导致的替换失败!
引入进程创建:
int execv(const char *path, char *const argv[]);
实现的功能和execl
一模一样。
path:
argv[]:
#include
#include
#include
#include
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;
}
程序替换不仅可以替换成指令,还可以替换成我们自己写的可执行程序。
int execlp(const char *file, const char *arg, ...);
file:
arg:
代码演示:
#include
#include
#include
#include
//有时候不想让父进程做一件事,只想让子进程做一件事
//将进程创建引入进来
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);
if(ret == id)
{
sleep(2);
printf("wait success, ret : %d, 我所等待子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0x7F);
}
return 0;
}
作用和execI和execv是一样的,也是执行一个新的程序。
int execvp(const char *file, char *const argv[]);
file:
argv[]:
int execle(const char *path, const char *arg, ..., char * const envp[]);
envp[]:
execle:test.c程序替换代码演示:
#include
#include
#include
#include
int main()
{
//环境变量的指针声明
extern char** environ;
printf("我是父进程,我的pid是:%d\n", getpid());
pid_t id = fork();
if(id == 0)
{
//子进程
//我们要子进程执行全新的程序,以前我们是子进程执行父进程的代码片段
printf("我是子进程,我的pid是:%d\n", getpid());
//绝对路径
//execl("/home/Zh_Ser/linux/lesson16/mycmd", "mycmd", NULL);
//相对路径
//execl("./mycmd", "mycmd", NULL);
//我们来手动导入一个环境变量
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;
}
上述代码代码在程序替换的时候,执行了./mycmd,目的是手动导入环境变量的时候,执行./mycmd获取导入的环境变量。
mycmd.cpp代码演示:
#include
#include
#include
#include
using namespace std;
int main()
{
extern char** environ;
cout << "打印环境变量" << endl;
for (int i = 0; environ[i]; i++)
{
printf("%d: %s\n", i, environ[i]);
}
cout << "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" << endl;
//根据环境变量名,获取环境变量的内容
cout << "PATH:" << getenv("PATH") << endl;
cout << "----------------------------------------------" << endl;
cout << "MYPATH:" << getenv("MYPATH") << endl;
cout << "----------------------------------------------" << endl;
//程序崩溃了 -- 因为环境变量里根本就没有MYPATH
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
cout << "Hello World!" << endl;
return 0;
}
mycmd是为了获取环境变量。
如果我们用这种方式导入环境变量:
将mycmd.cpp中的getenv(“PATH”)给屏蔽掉,再执行test结果就可以将MYPATH打印出来:
e: 添加环境变量给目标进程,是覆盖式的!所以环境变量只剩下MYPATHT。
子进程会继承父进程的环境变量的!!(重点)
- 子进程会继承父进程的环境变量,当父进程调用fork()创建子进程时,子进程会继承父进程的所有环境变量。
- 当子进程调用execlp()等函数执行其他程序时,子进程也会继承父进程的环境变量。
- 如果需要在子进程中更改环境变量,可以使用setenv()或putenv()等函数进行更改。
- 但是,更改的环境变量只会影响当前进程和它的子进程,并不会影响父进程或其他进程的环境变量。
如何理解覆盖?(重点)
- 当子进程调用execle()函数替换自己的程序时,可以传递一个新的环境变量数组,以覆盖子进程继承的父进程的环境变量。如果不传递新的环境变量数组,子进程会继承父进程的环境变量。因此,如果在调用execle()函数时没有传递新的环境变量数组,子进程的环境变量不会被覆盖。
- 如果传递了新的环境变量数组,则子进程的环境变量将被替换为新的环境变量数组中的值。这可能会导致子进程无法访问父进程中的一些环境变量,除非在新的环境变量数组中显式地包含它们。
验证execle覆盖了子进程会继承父进程的环境变量:
我们在mycmd程序开始的地方,加了查看全部环境变量的代码:
extern char** environ;
for (int i = 0; environ[i]; i++)
{
printf("%d: %s\n", i, environ[i]);
}
目的是通过该代码查看子进程(mycmd)的环境变量,被execle传的env_覆盖之后的样子:
显而易见,子进程的环境变量只有env_[]的内容了!!!所以getenv("PATH")
才获取不到!!!
正确做法:
我们将全部环境变量传过去,将environ传过去。
补充:(重点)
详细说明:
- 如果PATH环境变量为空,execlp()函数会无法在环境变量中查找可执行文件的路径。但是,execlp()函数会检查一些默认路径,例如/bin、/usr/bin等,来查找可执行文件。因此,即使PATH为空execlp()函数也可能会在这些默认路径中找到可执行文件并执行它。
- 但是,如果在默认路径中也找不到可执行文件,则execlp()函数会执行失败,并将errno设置为ENOENT,表示无法找到可执行文件。因此,如果需要执行特定路径下的可执行文件,最好使用execv()或execve()等函数,并指定可执行文件的完整路径。这样可以避免依赖PATH环境变量来查找可执行文件的路径。
验证:
即使我们将父进程中的PATH给改了,命令行中都用不了ls,execlp照样可以找到ls并执行它。
int execvpe(const char *file, char *const argv[], char *const envp[]);
有了上面的基础这个想必就不用再啰嗦了,只是第二个参数传的不同,这里传的是一个指针数组。
为什么有那么多的接口?
严格意义来说不是系统接口,是基于系统接口之上的封装。
真正意义上的系统接口:
int execve(const char *filename, char *const argv[], char *const envp[]);
上述6个函数在执行时都会调用execve()函数,将参数列表和环境变量数组转换为execve()函数所需的格式,并调用execve()函数来执行可执行文件。因此,execve()函数是这些函数的底层实现。
只要我们懂得了程序替换的原理,会用程序替换的接口,就很好理解:
myshell代码实现:
#include
#include
#include
#include
#include
#include
#define SEP " "
#define NUM 1024
#define SIZE 128
char command_line[NUM];
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.创建进程,执行
//如果自己直接程序替换的话,就把自己写的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;
}
在命令行中操作cd时,会跳转路径,但是用绝对命令时,就不行了,还是原来的路径:
一般一个进程的路径是会被于进程继承的,路径的变化我们希望的是父进程路径的变化。
就不能用程序替换的方式来执行一些特殊的命令了:
重点:
- 程序替换影响的是子进程和父进程没关系,子进程一 跑就完了,曾经所有的操作就没有意义,路径切换就没意义了,所以我们要让父进程的路径发生变化。
- 如果有些行为,是必须让父进程shell执行的,不想让子进程执行,绝对不能创建子进程!只能是父进程自己实现对应的代码!
内建命令:
- 我们把由父进程自己提供的代码或者提供的逻辑(在命令行上体现的也是一个命令),但是这部分命令不是子进程执行的,而是父进程自己执行的,我们叫做内建命令。
- 由shell自己执行的命令,我们称之为内建(内置bind- in)命令。
更改工作目录的函数:
导入环境变量:
export不是一个可执行程序和cd,ls,cat等指令不同:
export是一个shell内置命令,用于设置环境变量。它并不是一个可执行程序,而是由shell解释器直接执行的命令。当我们在shell中使用export命令时,它会将指定的环境变量设置为当前shell进程的环境变量,以便后续的命令或程序可以使用该环境变量。
所以用execvp进行程序替换的时候,是不能替换成功的!
环境变量是数据,进程替换不是替换进程的代码和数据吗?
环境变量的数据,在进程的上下文中: