2024-10-11 简易模拟 shell & 内建命令

创建:父子进程的代码共享,数据写时拷贝各自私有。

退出:退出码概念;进程退出三种情况(代码跑完结果对;代码跑完结果不对,由退出码决定;代码出现异常,由退出信号决定)。

等待:为了解决僵尸问题(必要理由),获取子进程的退出信息(可选)(通过 status 位图获取退出码和退出信号,根据这两个数据指标获得子进程的运行结果);阻塞和非阻塞的概念。

替换:原理:子进程所对应的PCB、地址空间这样的结果不变,把对应的可执行程序的代码和数据重新进行覆盖,如果进行程序替换的话,可以让该进程直接执行一个新的程序。


一、PrintCommandLine

用户名、主机名和当前工作路径都可以通过 getenv 获取。

string GetUserName(){
	return NULL == getenv("USER") ? "None" : getenv("USER");
}

string GetHostName(){
	return NULL == getenv("HOSTNAME") ? "None" : getenv("HOSTNAME");
}

string GetPWD(){
	return NULL == getenv("PWD") ? "None" : getenv("PWD");
}

string MakeCommandLine(){
	char command_line[basesize];
	snprintf(command_line, basesize, "[%s@%s %s]# ", GetUserName().c_str(), GetHostName().c_str(), GetPWD().c_str());
	return command_line;
}

void PrintCommandLine(){
	printf("%s", MakeCommandLine().c_str());
	fflush(stdout);
}

二、GetCommandLine

将用户在命令行输入的内容当做一个完整的字符串,包含空格,所以不建议使用 cin 和 scanf,getline 和 fgets 都可以。

提取命令行内容时最好是“纯净”的字符串,不要出现换行符等冗余内容,利用 strlen() - 1 将最后位置的元素置 0。

判断是否只输入了换行符,如是,则返回 false。

bool GetCommandLine(char commandline_buffer[], int size){
	char *ret = fgets(commandline_buffer, size, stdin);
	if(!ret){
		return false;
	}
	commandline_buffer[strlen(commandline_buffer) - 1] = 0; //如果只获取到换行符就是空字符串
	if(0 == strlen(commandline_buffer)) return false;
	return true;
}

三、ParseCommandLine

 将读取的命令行内容拆分为指针数组,结尾设置为NULL。

设置全局变量 gargv 和 gargc。

strtok 接口:

#include <string.h>

char *strtok(char *str, char *delim);

【注】1. 首次使用分割后想再次切割历史字符串需要传 nullptr;2. 如果是按照指定字符进行分割的话返回的是字符串地址,否则返回 nullptr(切到最后)。

利用 strtok 将字符串切分好传入 gargv 数组,同时 gargc 进行元素个数计数。

bool ParseCommandLine(char commandline_buffer[], int len){
	(void)len;
	memset(gargv, 0, sizeof(gargv));
	gargc = 0;
	const char *sep = " ";
	gargv[gargc++] = strtok(commandline_buffer, sep);	
	while( gargv[gargc++] = strtok(nullptr, sep) );
	gargc--;
}

测试

void debug(){
	printf("argc: %d\n", gargc);
	int i = 0;
	for(; gargv[i]; ++i){
		printf("argv[%d] = %s\n", i, gargv[i]);
	}
}

四、ExecuteCommandLine

最好不要让shell直接执行命令,如果是错误命令,shell会崩溃,所以使用子进程执行。

子进程执行命令(exec*),再退出。

bool ExecuteCommand(){
	pid_t id = fork();
	if(id < 0) return false;
	if(0 == id){
		execvp(gargv[0], gargv);	
		exit(1);
	}
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
	if(rid > 0){
		//等待成功
	}
}

五、内建命令

【问题】上述模拟实现的 shell 并不能解析 ..

每个进程都有一个当前路径的概念,当子进程执行 cd .. 时改的是子进程的路径而非 shell 的,所以 cd 这样的命令不能由子进程执行。

【结论】在命令行中,有些命令必须由子进程执行;有些命令(built command,内建命令)不能由子进程执行,必须由 shell 自己执行。

shell 自己执行命令,本质是 shell 调用自己的函数。

【问题】为何 cd 后命令行展示的当前路径没有改变呢?

可以通过系统调用 getcwd 获取路径,再维护更新环境变量。

【注】shell 的工作路径和对应的环境变量“缓冲区”最好设置成全局变量,避免设置在临时函数中。

环境变量是由 shell 自己维护的;自己模拟的 shell 是从系统继承的,当修改时有写时拷贝机制,子进程(模拟 shell)的全局环境变量表就与系统 shell 的分离了(模拟 shell 的环境变量表还是半成品)。

如果模拟 shell 实现并维护了自己完整的环境变量表后,export 和 echo 命令就不需要创建子进程来执行,而是内建命令(export 如果是子进程执行,就不能修改父进程 shell 的环境变量表)。

六、InitEnv

从父进程 shell 直接读取:

void InitEnv(){
	extern char **environ;
	int i = 0;
	while(environ[i]){
		genv[i] = (char*)malloc(strlen(environ[i] + 1);
	strncpy(genv[i], environ[i], strlen(environ[i]));
	i++;
	}
	genv[i] = nullptr;
}

模拟 shell 中使用自建命令如何继承模拟 shell 的环境变量表呢?在 ExecuteCommand() 中使用 execvpe 即可。

bool ExecuteCommand(){
	pid_t id = fork();
	if(id < 0) return false;
	if(0 == id){
		execvpe(gargv[0], gargv, genv);	
		exit(1);
	}
	int status = 0;
	pid_t rid = waitpid(id, &status, 0);
	if(rid > 0){
		//等待成功
	}
}

void addenv(const char *e){
	int i = 0;
	while(genv[i]){
		i++;
	}
	genv[i] = (char*)malloc(strlen(e) + 1);
	strncpy(genv[i], e, strlen(e) + 1);
	genv[++i] = nullptr;
}

bool CEBuiltCommand(){
	if(strcmp(gargv[0], "cd") == 0){
		if(gargc == 2){
			chdir(gargv[1]);
		}
		return true;
	}
	else if(strcmp(gargv[0], "export") == 0){
		if(gargc == 2){
			addenv(gargv[1]);
		}
		return true;
	}
	else if(strcmp(gargv[0], "env") == 0){
		for(int i = 0; genv[i]; i++){
			printf("%s\n", genv[i]);
		}
		return true;
	}
	return false;
}

【Q】echo 命令为何是内建命令呢?

【A】本地变量表无法通过 execv 这样的接口调取,所以子进程不能看到。

【Q】echo 是如何查看的呢?

【A】让 shell 去遍历它自己维护的本地变量表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值