创建:父子进程的代码共享,数据写时拷贝各自私有。
退出:退出码概念;进程退出三种情况(代码跑完结果对;代码跑完结果不对,由退出码决定;代码出现异常,由退出信号决定)。
等待:为了解决僵尸问题(必要理由),获取子进程的退出信息(可选)(通过 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 去遍历它自己维护的本地变量表。