文章目录
- 💐专栏导读
- 💐文章导读
- 🐧程序进程替换
- 🐦替换原理
- 🐦替换函数
- 🐔观察与结论
- 🐔函数命名理解
- 🐧myshell编写
- 🔔代码展示
- 🔔效果展示
- 🐧myshell_plus
- 🔔代码展示
- 🔔效果展示
💐专栏导读
🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
🌸专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。
🌸相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法。
💐文章导读
本章我们将学习一个强大的功能——程序替换
。之前我们创建的子进程只能完成简单的一些任务且部分代码继承自父进程。有了程序替换以后,我们可以让子进程轻松的做更多的事情。学会了程序替换,我们可以编写一个简易的shell
玩玩了,由此也可以对前几章的内容作复习与巩固~
🐧程序进程替换
我们一直在提子进程,那么创建子进程的目的是什么呢?无非是想让它帮助我们做某件事情。
用fork
创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支)。如果此时我们想要子进程执行一个全新的程序
该怎么做呢?这就需要用到程序替换了~
🐦替换原理
当进程调用一种exec
函数时,该进程的用户空间代码和数据完全被新程序替换
,从新程序的启动例程开始执行。调用exec
并不创建新进程,所以调用exec
前后该进程的id
并未改变。
接下来我们就认识几个程序替换相关的函数,并演示如何操作。
🐦替换函数
一共有六种以exec
开头的函数,称exec
函数:
#include <unistd.h>
extern 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[]);
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回
-1
。 - 所以
exec函数只有出错的返回值而没有成功的返回值
。
在这里,我们先以execl为例。
🔔示例1——execl
在代码中,我们尝试在子进程中进行程序替换,替换为ls
指令。execl
使用时:
- 第一个参数是程序所在路径;
- 剩下的参数为执行该程序时想要传递的命令行参数(简述:平时你在命令行中怎么用,就怎么传参);
- 当确定想要传递的参数都给出后,一定要以
NULL
结尾。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
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的观察,我们发现:
- 子进程中,
execl
替换后剩下的语句未执行(printf
); - 子进程发生替换并未影响父进程;
由此我们可以得出结论:
- 因为进程具有独立性,尽管父子进程刚开始用的是同一个代码和数据,但是当程序替换发生后,由于
写时拷贝
的存在,仅仅只是子进程的代码和数据被替换后的程序覆盖了,并不会影响父进程。 - 程序替换函数,一旦替换发生,原来的代码在替换的语句执行后,就已经被新程序的代码和数据覆盖了,所以
printf
并未执行;
🐔函数命名理解
其实四个函数的功能是类似的,都用于完成程序替换。只不过针对不同的场景,我们可以选择不同的函数。
这些函数根据函数名就大致可以判断如何使用:
l (list)
:表示参数采用列表 ;v (vector)
:参数用数组 ;p (path)
:有p自动搜索环境变量PATH
;e (env)
:表示自己维护环境变量;
🔔其余函数示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
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
。
🐧myshell编写
🔔代码展示
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <assert.h>
#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_plus
上文中的myshell
是非常简陋的,有许多指令诸如:cd
、export
、env
等指令并不能正确执行。
就用cd
来举例,myshell
执行指令其实是交给子进程去做的,子进程的执行结果并不会影响父进程。也就是说,cd
指令需要mybash
自己去执行。
- 我们把让
bash
自己执行的命令叫作内建命令
。我们之前学到过的几乎所有关于环境变量的命令都是内建命令
。
于是,我们可以在创建子进程之前用if
做判断,若用户输入的指令为内建命令
,则让父进程执行该指令。
🔔代码展示
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <assert.h>
#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;
}
🔔效果展示
本章的内容就到这里了,觉得对你有帮助的话就支持一下博主把~
点击下方个人名片,交流会更方便哦~
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓