🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
🌸专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。
🌸相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法。
本章我们将学习一个强大的功能——程序替换。之前我们创建的子进程只能完成简单的一些任务且部分代码继承自父进程。有了程序替换以后,我们可以让子进程轻松的做更多的事情。学会了程序替换,我们可以编写一个简易的shell玩玩了,由此也可以对前几章的内容作复习与巩固~
我们一直在提子进程,那么创建子进程的目的是什么呢?无非是想让它帮助我们做某件事情。
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支)。如果此时我们想要子进程执行一个全新的程序该怎么做呢?这就需要用到程序替换了~
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
接下来我们就认识几个程序替换相关的函数,并演示如何操作。
一共有六种以exec开头的函数,称exec函数:
#includeextern char **environ; 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 execvpe(const char *file, char *const argv[],char *const envp[]);
在这里,我们先以execl为例。
🔔示例1——execl
在代码中,我们尝试在子进程中进行程序替换,替换为ls指令。execl使用时:
#include#include #include int main() { pid_t id = fork(); if(id == 0) { // 子进程 execl("/bin/ls","ls","-a","-n","-l",NULL); printf("程序替换失败\n"); } // 父进程 printf("等待子进程成功,child_id:%d\n",wait(NULL)); return 0; }
如图所示,结果正是我们想要的。
根据对示例1的观察,我们发现:
由此我们可以得出结论:
其实四个函数的功能是类似的,都用于完成程序替换。只不过针对不同的场景,我们可以选择不同的函数。
这些函数根据函数名就大致可以判断如何使用:
🔔其余函数示例
#include#include #include int main() { extern char** environ; pid_t id = fork(); if(id == 0) { // 子进程 char* const myargv[] = {"ls","-a","-l",NULL}; // 带l的,需要跟上路径 execl("/bin/ls","ls","-a","-n","-l",NULL); // 带p的,可以使用环境变量PATH,无需写全路径 execlp("ls","ls","-a","-l",NULL); // 带V的,可以使用自己的参数列表数组 execvp("ls",myargv); // 带e的,需要自己组装环境变量 execvpe("ls",myargv,environ); printf("程序替换失败\n"); } // 父进程 printf("等待子进程成功,child_id:%d\n",wait(NULL)); return 0; }
有了前几章所讲知识以及在、本章程序替换部分的知识,我们可以试着自己实现一个简易的shell。
#include#include #include #include #include #include #include #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] = {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; pid_t id = fork(); if(id == 0) { // 子进程 execvp(argv[0],argv); // 程序替换 exit(1); } int status = 0; waitpid(id,&status,0); // 等待子进程 } return 0; }
如图所示,我们用简短的50行代码写了一个简易的shell(命令行解释器)。其实Linux源码中的内容可远不止这些,而且我们实现的shell所能实现的功能非常少,非常简陋。例如,ls 并没有对不同的文件“上色”。
接下来,我们可以完善上述的代码,继续添加一些小功能。
上文中的myshell是非常简陋的,有许多指令诸如:cd、export、env等指令并不能正确执行。
就用cd来举例,myshell执行指令其实是交给子进程去做的,子进程的执行结果并不会影响父进程。也就是说,cd指令需要mybash自己去执行。
于是,我们可以在创建子进程之前用if做判断,若用户输入的指令为内建命令,则让父进程执行该指令。
#include#include #include #include #include #include #include #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; }
本章的内容就到这里了,觉得对你有帮助的话就支持一下博主把~
点击下方个人名片,交流会更方便哦~
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓