文章目录
- 🦄 进程替换
- 🦩 execl()函数
- 🦩 execlp()函数
- 🦩 execle()函数
- 🦩 execv()函数
- 🦩 execvp()函数
- 🦩 execvpe()函数
- 🦩 execve()函数
- 🦄 简单Shell命令行解释器的实现
- 🦩 大致框架与命令行提示符
- 🦩 获取用户输入信息
- 🦩 将缓冲区内的字符串进行分块
- 🦩 分析并执行指令
- 🦩 对cd命令进行处理
- 🦩 简单Shell实现代码演示(供参考)
🦄 进程替换
在『 Linux 』Process Control进程控制(万字)-CSDN博客 中提到了些进程控制中的概念,但是在这篇文章当中对于进程替换的概念以及用法并没有完全;
在本篇文章中将对上篇文章中的进程替换的各个接口进行补充;
进程替换,按照字面意义上即为一个进程在运行过程当中替换为另一个进程;
在之前的博客当中可能提到过, 当一个程序被加载进内存当中时对应的内存会新生成一个对应的进程;
而在进程替换当中可以完美的对上面的理论进行一个反驳,即并不是每个程序加载到内存当中都会新生成一个对应的进程;
以该图为例,该图中一个正在执行的进程经过了进程替换,将磁盘中的程序的代码和数据加载到了被替换的进程对应的PCB
结构体当中;
当然在物理内存当中需要对应的为该新载入的进程的数据代码开辟一块新的内存空间;
但实际上在进程地址空间来看的话也仅仅只是将对应的映射关系进行修改;
当新的程序代码数据被加载进物理内存时,随着进程逐渐发生替换,对应的原有的代码和数据也将渐渐被释放;
因为只是仅仅的发生映射关系的转换,故对应的PID
等mm_struct
内的数据都不会作修改;
在上篇文章中简单的使用了execl()
进程替换函数进行了进程替换的演示;
#include <unistd.h>
#include <iostream>
using namespace std;
int main() {
cout << "hello world1" << endl;
cout << "hello world1" << endl;
cout << "hello world1" << endl;
printf("当前程序为myproc 且PID为:%d \n", getpid());
execl("./test_/mytest", "mytest", NULL);
cout << "hello world2" << endl;
cout << "hello world2" << endl;
cout << "hello world2" << endl;
return 0;
}
这段程序中替换的程序的代码如下:
#include <iostream>
#include<unistd.h>
using namespace std;
int main() {
printf("当前程序为mytest 且PID为:%d\n", getpid());
return 0;
}
且该程序运行后的最终结果为:
$ ./myproc
hello world1
hello world1
hello world1
当前程序为myproc 且PID为:14115
当前程序为mytest 且PID为:14115
该段程序可以清楚证明对应的发生进程替换时对应的PID
不会发生变化;
在上篇博客当中只介绍了一个exec
家族的函数;
但是这样的函数一共有7
个;
虽然7
个接口函数实际的功能结果相同,但是对应的在传参上中有所不同;
🦩 execl()函数
-
execl()
的函数原型:int execl(const char *path, const char *arg0, ... /* (char *) NULL */);
该函数的功能为执行指定的路径下的可执行文件,并用传递给它的参数替换原有的程序;
这意味着原始的程序将被新程序替换,原始程序的代码将不再执行,而被
path
参数所指定的可执行文件加载并开始执行; -
参数:
path
参数是可执行文件的路径;arg0
表示要传递给程序的第一个参数,通常是新程序的名称且他为一个字符串;...
可选的参数列表,这些参数将作为进程替换后新程序的命令行参数传递,且参数列表必须以空指针(char*)NULL
结尾; -
示例:
#include <unistd.h> #include <stdio.h> int main() { printf("This is the original program\n"); execl("/bin/ls", "ls", "-l", NULL); printf("This is the original program\n"); perror("execl"); exit(-1); }
在该段代码中的原始程序若是未被新进程所替换时将会打印出两次
This is the original program\n
;而运行该段代码的结果为:
$ ./mytest This is the original program total 84 -rw-rw-r-- 1 _USER _USER 84 Mar 14 13:49 makefile -rwxrwxr-x 1 _USER _USER 76536 Mar 14 13:51 mytest -rw-rw-r-- 1 _USER _USER 357 Mar 14 13:51 test.cpp
当进程发生替换了之后,原有进程的代码数据将被替换,故对应的代码不会执行;
在使用进程替换时需要使用
errno
指定出对应的问题;
🦩 execlp()函数
-
execlp()
函数原型:int execlp(const char *file, const char *arg0, ... /* (char *) NULL */);
execlp()
函数的工作方式与execl()
类似,但不同之处在于它不需要指定文件的完整路径;它会在系统的
PATH
环境变量中搜索file
参数所指定的可执行文件,找到后执行它; -
参数:
file
为要执行的可执行文件,可以是一个简单的文件名而不需要完整的路径名;arg0
为要传递给新程序的第一个参数,一般来说这个参数为需要执行新程序的名称;...
为可选参数,这些参数将作为新程序的命令行参数并进行传递; -
示例:
int main() { printf("This is the original program\n"); execlp("ls", "ls", "-l", NULL); printf("This is the original program\n"); perror("execlp"); exit(1); }
在该段程序当中原始程序中将输出一条消息后调用
execlp()
函数执行ls
命令;若是进程替换成功将会执行
ls -l
的命令,若是未替换成功将会退出并返回1
同时打印出第二句This is the original program
;该程序运行的结果如下:
$ ./mytest This is the original program total 84 -rw-rw-r-- 1 _USER _USER 84 Mar 14 13:49 makefile -rwxrwxr-x 1 _USER _USER 76528 Mar 14 14:07 mytest -rw-rw-r-- 1 _USER _USER 582 Mar 14 14:07 test.cpp
🦩 execle()函数
-
execle()
函数原型int execle(const char *path, const char *arg0, ..., char *const envp[]);
该函数与
execl()
函数和execlp()
函数类似,execle()
函数会将当前的进程替换为指定路径下的可执行文件;但是该函数与前两者不同的是,
execle()
函数允许你传递一个自定义的环境变量数组给新程序;一般这个环境变量数组通过
envp
参数进行传递; -
参数:
path
参数表示要执行的可执行文件路径的字符串;arg0
表示要传递给新程序的第一个参数,一般情况下该参数为新程序的名字;...
表示可选参数列表,这些参数将作为新程序的命令行参数并进行传递,且参数列表必须以空指针(char*)NULL
进行结尾;envp[]
指向一个以NULL
结束的环境变量数组,其中每个元素都是形如NAME=VALUE
的字符串; -
示例:
int main() { printf("This is the original program\n"); char *env[] = {(char *)"MYVAR=Hello", NULL}; execle("/usr/bin/env", "env", NULL, env); printf("This is the original program\n"); perror("execle"); exit(1); }
在该示例当中,原始程序将输出一条消息后调用
execle()
函数来执行/usr/bin/env
的命令;由于第一个参数指定了完整的路径,故
execle()
函数将会直接执行该命令;同时通过
env
参数传递了一个自定义的环境变量数组给新的程序;最终的执行结果为:
$ ./mytest This is the original program MYVAR=Hello
🦩 execv()函数
-
execv()
函数原型int execv(const char *path, char *const argv[]);
该函数的工作方式与
execl
和execlp
函数类似,与之不同的是该函数使用了不同的参数传递方式;execv()
函数将参数作为一个字符串数组传给新的程序而不是通过函数参数列表进行传递使得该函数在传参时能够更加灵活; -
参数:
path
参数表示要执行的可执行文件的路径的字符串;argv[]
指向一个以NULL
结尾的字符串数组,每个元素都表示新程序的命令行参数;argv[0]
通常是新程序的名称,后序的参数依次排列且最后一个元素必须是NULL
; -
示例:
int main() { printf("This is the original program\n"); char *args[] = {(char *)"ls", (char *)"-l", NULL}; execv("/bin/ls", args); printf("This is the original program\n"); perror("execv"); return 1; }
在该示例当中,原始程序将输出一条消息并用
execv()
函数执行/bin/ls
的命令且带-l
参数;args
数组包含了要传递给ls
命令的参数列表;与
execl()
和execlp()
函数不同,execv()
函数将参数作为一个字符串传递给新的程序;最终的执行结果为:
$ ./mytest This is the original program total 84 -rw-rw-r-- 1 _USER _USER 84 Mar 14 13:49 makefile -rwxrwxr-x 1 _USER _USER 76568 Mar 14 14:41 mytest -rw-rw-r-- 1 _USER _USER 1142 Mar 14 14:41 test.cpp
🦩 execvp()函数
-
execvp()
函数原型:int execvp(const char *file, char *const argv[]);
该函数的工作方式与
execv()
函数类似,但该函数不要求指定可执行文件的完整路径;该函数将在系统的
PATH
环境变量中搜索file
参数指定的可执行文件并执行; -
参数:
file
表示要执行的可执行文件的名称,该参数可以是一个简单的文件名而不需要包含完整的路径;argv[]
指向一个以NULL
结束的字符串数组,每个元素表示新程序的命令行参数,argv[0]
通常表示新程序的名字,后面的参数依次排列且最后一个元素必须是NULL
; -
示例:
int main() { printf("This is the original program\n"); char *args[] = {(char *)"ls", (char *)"-l", NULL}; execvp("ls", args); printf("This is the original program\n"); perror("execvp"); return 1; }
在该示例中原始程序将输出一条消息并调用
execvp()
函数执行ls
命令并带有-l
参数;由于
ls
并未指出完整的路径故execvp()
将在PATH
中搜索ls
可执行文件并执行找到的第一个匹配项;与
execv()
函数类似,若是execvp()
函数调用失败 (例如指定的可执行文件不存在) 将返回-1
并设置errno
指示错误类型;最终的执行结果为:
$ ./mytest This is the original program total 84 -rw-rw-r-- 1 _USER _USER 84 Mar 14 13:49 makefile -rwxrwxr-x 1 _USER _USER 76560 Mar 14 14:53 mytest -rw-rw-r-- 1 _USER _USER 1416 Mar 14 14:53 test.cpp
🦩 execvpe()函数
-
execvpe()
函数原型:int execvpe(const char *file, char *const argv[], char *const envp[]);
execvpe()
函数的工作方式与execvp()
函数类似,与之不同的是该函数额外提供了一个参数允许指定自定义的环境变量; -
参数:
file
表示要执行的可执行文件的名称,它可以是一个简单的文件名而不需要包含完整的路径;argv[]
指向一个以NULL
结束的字符串数组,每个元素表示新程序的命令行参数且argv[0]
通常表示新程序的名称,后序的参数依次排列;数组的最后一个元素必须是NULL
指针;envp[]
指向一个以NULL
结束的环境变量数组,其中每个元素都是形如NAME=VALUE
的字符串; -
示例:
int main() { printf("This is the original program\n"); char *args[] = {(char*)"ls", (char*)"-l", NULL}; char *env[] = {(char *)"MYVAR=Hello", NULL}; execvpe("ls", args, env); printf("This is the original program\n"); perror("execvpe"); return 1; }
在该示例中,原始程序将输出一条消息并调用
execvp()
函数执行ls
命令并带有-l
参数;同时传递了一个自定义的环境变量数组给新的程序;
最终的执行结果:
$ ./mytest This is the original program total 84 -rw-rw-r-- 1 _USER _USER 84 Mar 14 13:49 makefile -rwxrwxr-x 1 _USER _USER 76600 Mar 14 15:24 mytest -rw-rw-r-- 1 _USER _USER 1696 Mar 14 15:24 test.cpp
🦩 execve()函数
-
execve()
函数原型:int execve(const char *filename, char *const argv[], char *const envp[]);
execve()
函数的工作方式是将当前进程的映像(image)替换为指定路径下的可执行文件;与其他
exec
系列函数不同的是该函数为一个系统调用,将直接与操作系统内核交互并执行新的程序,而其他exec
函数通常是标准库提供的函数,最终将调用execve()
系统调用来执行新的程序;execve()
函数直接与操作系统内核进行通信,提供了更直接更底层的接口,可直接控制程序的执行;其他
exec
函数则是再标准库中实现的高层接口,或许会做出一些额外的处理(路径搜索,参数组织等)后再调用execve()
;由于
execve()
函数直接暴露了系统调用的细节故提供了更大的灵活性和控制性;用户程序可以直接操作参数和环境变量使得可以自行管理文件描述符等从而实现更复杂的执行需求;
-
参数:
filename
指向要执行的可执行文件路径的字符串;argv[]
参数指向一个以NULL
结束的字符串数组,每个元素表示新程序的命令行参数;argv[0]
通常为新程序的名称,后序的参数依次排列,数组的最后一个元素必须是NULL
指针;envp[]
指向一个以NULL
结束的环境变量数组,其中每个元素都是形如NAME=VALUE
的字符串; -
示例:
int main() { printf("This is the original program\n"); char *args[] = {(char *)"ls", (char *)"-l", NULL}; char *env[] = {(char*)"MYVAR=Hello", NULL}; execve("/bin/ls", args, env); printf("This is the original program\n"); perror("execve"); return 1; }
在这个示例中,原始程序将输出一条消息后调用
execve
函数来执行/bin/ls
命令,并带有-l
参数;并且传递了一个自定义的环境变量数组给新的程序;
最终运行结果:
$ ./mytest This is the original program total 84 -rw-rw-r-- 1 _USER _USER 84 Mar 14 13:49 makefile -rwxrwxr-x 1 _USER _USER 76600 Mar 14 16:05 mytest -rw-rw-r-- 1 _USER _USER 2073 Mar 14 16:05 test.cpp
🦄 简单Shell命令行解释器的实现
命令行解释器(Command Line Interpreter) 是一种与操作系统进行交互的软件程序;
其允许用户命令行界面(CLI)输入命令并根据命令控制OS与其对应的应用程序;
命令行解释器通常称为Shell
;
它充当了用户和操作系统之间的中间层并提供了一种文本方式来执行各项操作;
命令行解释器的主要功能包括:
- 解释和执行命令
- 管理文件系统
- 进程管理
- 环境配置
- 用户交互
- 脚本执行
🦩 大致框架与命令行提示符
在一般的情况下在Shell
当中将会显示对应的命令行提示符使用户方便进行输入;
一般的情况下命令行提示符只需要打印即可;
同时Shell
必然是一个常驻进程,即一般情况下进程不退出,需要使用循环进行控制;
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
using namespace std;
int main() {
// cout << "hello world" << endl;
/*
命令行解释器是一个常驻进程,一般情况常驻进程不退出
*/
while (true) {
// 1.打印出提示信息
printf("[SilverChariot@local MyShell]# ");
fflush(stdout);
}
}
使用fflush()
刷新输出缓冲区防止在循环当中打印换行;
🦩 获取用户输入信息
当打印完提示信息时需要获取用户的输入信息;
声名一个数组充当字符串缓冲区并使用fgets()
函数获取对应的用户输入信息(需要提前使用memset()
对空间进行初始化);
#define NUM 1034 // 保存完整的命令行字符串的大小
char cmd_line[NUM]; // 缓冲区 - 用于保存完整的命令行字符串
/*
......
*/
memset(cmd_line, '\0', sizeof cmd_line); // 将缓冲区进行初始化
// 2.获取用户输入信息(指令 及 选项 )
if (fgets(cmd_line, sizeof cmd_line, stdin) == nullptr)
continue; // 如果从输入流中获取数据失败则进行下一次循环 该次循环不算
cmd_line[strlen(cmd_line) - 1] = '\0';
// 由于输入换行后该缓冲区将会存储一个换行并且进行打印
// 故需要将改缓冲区的换行修正为'\0'
// cout << "echo :" << cmd_line << endl;// --debug 用于打印是否正确
当用户输入完输入信息时为了能够让计算机识别结束输入流一般会输入一个\n
;
为了防止\n
不被打印需要在对应的cmd_line[strlen(cmd_line) - 1]
处置为\0
;
由于是一个循环,若是从输入流中获取数据失败则进行下一次循环continue
;
在该处可以将用户的输入信息进行打印从而判断该处逻辑是否出现对应问题;
🦩 将缓冲区内的字符串进行分块
由于需要在后期对用户的输入信息进行分析故需要先将用户的输入信息进行分块;
声名一个字符串数组char* []
用户保存分块后的命令行字符串子串;
在C++
中可以使用substr()
对字符串进行分块;
在C语言
当中则可以使用strtok()
对字符串进行分块;
char *g_argv[SIZE]; // 用于保存打散后的命令行字符串子串
#define SIZE 32 // 保存打散后命令字符串子串的数组大小
#define SEP " " // 作为分隔符// 3.将缓冲区内的字符串进行分块 即命令行字符串解析工作
/*
......
*/
g_argv[0] =
strtok(cmd_line, SEP); // 第一次调用strtok函数的时候需要传入原始字符串
int index = 1;
while (g_argv[index++] = strtok(nullptr, SEP)) {
; // 第二次调用时若是需要分割的是原始字符串则传入空null
}
根据strtok()
函数对字符串进行分块;
strtok()
函数参考【std::string::substr】在此不作赘述;
🦩 分析并执行指令
当数据拆分完毕后需要对指令进行分析与执行;
一般情况下由子进程对指令进行执行,父进程则负责分析以及等待子进程退出;
使用fork()
创建子进程并使用对应的进程替换接口使子进程能够运行对应的命令;
此处使用的进程替换接口为execvp()
函数,具体参考上文的对于execvp()
函数的解释;
// 5.创建进程 子进程执行指令 父进程等待分析指令
pid_t id = fork();
if(id == 0){
//子进程
cout << "子进程进行执行" << endl;
execvp(g_argv[0], g_argv);
exit(1);
} else if (id > 0) {
// 父进程
int status = 0;
waitpid(-1, &status,0);
if(WIFEXITED(status)){
cout << "WEXITSTATUS:" << WEXITSTATUS(status) << endl;
}
} else {
exit(-1);
}
🦩 对cd命令进行处理
当到这一步时大部分的指令都能够执行;
但是对应的cd
命令并不能在该处编写的Shell
中起作用;
原因是需要发生目录变化时一般为父进程发生变化,子进程的目录变化并不影响父进程;
故需要在fork()
创建子进程前使用strcmp()
对cd
进行特殊处理;
若是遇到cd
命令时则可以使用chdir()
接口函数进行路径的变化;
// 4.用于cd命令 需要在父进程阶段进行
if(strcmp("cd",g_argv[0]) == 0){
// if (g_argv[1] != nullptr && chdir(g_argv[1]) != 0) {
// cerr << "chdir failed: " << strerror(errno) << endl;
// }
if (g_argv[1] != nullptr) chdir(g_argv[1]);
continue;
}
🦩 简单Shell实现代码演示(供参考)
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
using namespace std;
#define NUM 1034 // 保存完整的命令行字符串的大小
#define SIZE 32 // 保存打散后命令字符串子串的数组大小
#define SEP " " // 作为分隔符
char *g_argv[SIZE]; // 用于保存打散后的命令行字符串子串
char cmd_line[NUM]; // 缓冲区 - 用于保存完整的命令行字符串
// shell 运行原理 : 子进程执行命令,父进程等待以及解析命令
int main() {
// cout << "hello world" << endl;
/*
命令行解释器是一个常驻进程,一般情况常驻进程不退出
*/
while (true) {
// 1.打印出提示信息
printf("[SilverChariot@local MyShell]# ");
fflush(stdout);
memset(cmd_line, '\0', sizeof cmd_line); // 将缓冲区进行初始化
// 2.获取用户输入信息(指令 及 选项 )
if (fgets(cmd_line, sizeof cmd_line, stdin) == nullptr)
continue; // 如果从输入流中获取数据失败则进行下一次循环 该次循环不算
cmd_line[strlen(cmd_line) - 1] =
'\0'; // 由于输入换行后该缓冲区将会存储一个换行并且进行打印
// 故需要将改缓冲区的换行修正为'\0'
// cout << "echo :" << cmd_line << endl;// --debug 用于打印是否正确
// 3.将缓冲区内的字符串进行分块 即命令行字符串解析工作
g_argv[0] =
strtok(cmd_line, SEP); // 第一次调用strtok函数的时候需要传入原始字符串
int index = 1;
while (g_argv[index++] = strtok(nullptr, SEP)) {
; // 第二次调用时若是需要分割的是原始字符串则传入空null
}
/*
//用于debug
for (index = 0; g_argv[index]; ++index) {
printf("g_argv[%d] : %s\n", index, g_argv[index]);
}
*/
// 4.用于cd命令 需要在父进程阶段进行
if(strcmp("cd",g_argv[0]) == 0){
// if (g_argv[1] != nullptr && chdir(g_argv[1]) != 0) {
// cerr << "chdir failed: " << strerror(errno) << endl;
// }
if (g_argv[1] != nullptr) chdir(g_argv[1]);
continue;
}
// 5.创建进程 子进程执行指令 父进程等待分析指令
pid_t id = fork();
if(id == 0){
//子进程
cout << "子进程进行执行" << endl;
execvp(g_argv[0], g_argv);
exit(1);
} else if (id > 0) {
// 父进程
int status = 0;
waitpid(-1, &status,0);
if(WIFEXITED(status)){
cout << "WEXITSTATUS:" << WEXITSTATUS(status) << endl;
}
} else {
exit(-1);
}
}
return 0;
}