1、shell 的初步实现
在学习了进程创建、进程终止、进程等待以及进程程序替换系列进程控制相关知识后,我们就可以自己实现一个简易的 shell 命令行解释器了;实现一个简易的 shell 大概可以分为如下几步:
输出提示符;
从终端获取命令行输入;
解析命令行输入信息;
创建子进程;
进程程序替换;
具体初步代码实现如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 1024 //一个命令的最大长度
#define OPT_NUM 64 //一个命令的最多选项
char lineCommand[NUM];
char* argv[OPT_NUM];
int main() {
while(1) {
//输出提示符
printf("[用户名@主机名 当前路径]$ ");
fflush(stdout);
//获取输入
char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin); //最后留一个位置来存放极端情况下的\0
if( ret == NULL ) {
perror("fgets");
exit(1);
}
lineCommand[strlen(lineCommand) - 1] = '\0'; //消除命令行中最后的换行符
//将输入解析为多个字符串存放到argv中,即字符串切割
argv[0] = strtok(lineCommand, " ");
int i = 1;
while(argv[i++] = strtok(NULL, " "));
//创建子进程
pid_t id = fork();
if(id == -1) {
perror("fork");
exit(1);
} else if (id == 0) { //子进程
//进程程序替换
int ret = execvp(argv[0], argv);
if(ret == -1) {
printf("No such file or directory\n");
exit(1);
}
} else { //父进程
//进程等待
pid_t ret = waitpid(id, NULL, 0);
if(ret == -1){
perror("wait");
exit(1);
}
}
}
return 1; //while循环正常情况下不会结束
}
2.功能完善:
2.1.ls颜色问题
ls 没有颜色功能,我们可以在 shell 对 ls 指令进行判断,然后手动为其加上 “–color=auto” 选项:
if(argv[0] != NULL && strcmp(argv[0], "ls") == 0) //ls颜色显示
{
argv[i++] = (char*)"--color=auto";
}
2.2.当前路径
当前路径我们运行上面的 myshell 可以发现,当我们 cd 更换路径后,pwd 命令还是显示原来的路径:
要理解并解决这个问题,我们首先要理解什么是当前路径:
可以看到,当 test 程序运行起来后,其在系统中一共有两个路径,其中 exe 路径是指 test 可执行程序在磁盘中的路径,而 cwd (current working directory) 则是指 当前进程的工作目录,它就是我们平时所说的 当前路径。
在 Linux 中,我们可以使用 chdir 系统调用来改变进程的工作目录:
问题:为什么我们的 shell 执行 cd 命令后目录不改变了?
原因:
myshell 是通过创建子进程的方式去执行命令行中的各种指令的,也就是说,cd 命令是由子进程去执行的,那么自然被改变也是子进程的工作目录,父进程的工作目录不受影响;
而当我们使用 PWD 指令来查看当前路径时,cd 指令对应的子进程已经执行完毕退出了,此时 myshell 又会给 PWD 创建一个新的子进程,且这个子进程的工作目录和父进程 myshell 相同,所以 PWD 打印出来的路径不变。
解决:
我们只需要对命令行传入的指令进行判断,如果是 cd 指令,就使用 chdir 将父进程的工作目录修改为指定的目录即可:
if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0) //cd改变父进程工作路径
{
if(myargv[1] != NULL)
chdir(myargv[1]); //myargv[1]中保存着指定路径
continue; //下面的语句不需要再执行,因为cd的目的已经达到了,直接读取下一条指令
}
2.3.内建命令
Linux 中的命令一共分为两种 – 内建命令和外部命令:
内建命令是 shell 程序的一部分,其功能实现在 bash 源代码中,不需要派生子进程来执行,也不需要借助外部程序文件来运行,而是由 shell 进程本身内部的逻辑来完成;
外部命令则是通过创建子进程,然后进行进程程序替换,运行外部程序文件等方式来完成。
tip:我们上面对 cd 指令就是以内置命令的逻辑来处理的 – myshell 遇到 cd 命令时,由自己直接来改变进程工作目录,处理完毕直接 continue,而不会创建子进程;
2.4.echo命令
同时,我们发现 echo 命令也是一个内置命令,这其实也很好的解释了 为什么 “echo $变量” 可以查看本地变量以及为什么 “echo $?” 可以获取最近一个进程的退出码 了:
虽然本地变量只在当前进程有效,但是使用 echo 查看本地变量时,shell 并不会创建子进程,而是直接在当前进程中查找,自然可以找到本地变量;
shell 可以通过进程等待的方式获取上一个子进程的退出状态,然后将其保存在 ? 变量中,当命令行输入 “echo $?” 时,直接输出 ? 变量中的内容,然后将 ? 置为0 (echo 正常退出的退出码),也不需要创建子进程。
int EXIT_CODE; //退出码 -- 全局变量
if(argv[0] != NULL && strcmp(argv[0], "echo") == 0) //处理echo内建命令
{
if(strcmp(argv[1], "$?") == 0){ //echo $?
printf("%d\n", EXIT_CODE);
EXIT_CODE = 0;
} else { //echo $变量
printf("%s\n", argv[1]+1);
}
continue;
}
//fork后面的内容
} else { //父进程
int status = 0;
pid_t ret = waitpid(id, &status, 0); //进程等待
EXIT_CODE = (status >> 8) & 0xFF; //获取退出码
if(ret == -1){
perror("wait");
exit(1);
}
}
3.终极具体代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 1024 //一个命令的最大长度
#define OPT_NUM 64 //一个命令的做多选项
char lineCommand[NUM];
char* argv[OPT_NUM];
int EXIT_CODE; //保存进程退出码
int main() {
while(1) {
//输出提示符
printf("[用户名@主机名 当前路径]$ ");
fflush(stdout);
//获取输入
char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin); //最后留一个位置来存放极端情况下的\0
if( ret == NULL ) {
perror("fgets");
exit(1);
}
lineCommand[strlen(lineCommand) - 1] = '\0'; //消除命令行中最后的换行符
//将输入解析为多个字符串存放到argv中,即字符串切割
argv[0] = strtok(lineCommand, " ");
int i = 1;
if(argv[0] != NULL && strcmp(argv[0], "ls") == 0) //ls颜色显示
{
argv[i++] = (char*)"--color=auto";
}
while(argv[i++] = strtok(NULL, " "));
if(argv[0] != NULL && strcmp(argv[0], "cd") == 0) //cd改变父进程工作路径
{
if(argv[1] != NULL)
chdir(argv[1]); //myargv[1]中保存着指定路径
continue;
}
if(argv[0] != NULL && strcmp(argv[0], "echo") == 0) //处理echo内建命令
{
if(strcmp(argv[1], "$?") == 0){ //echo $?
printf("%d\n", EXIT_CODE);
EXIT_CODE = 0;
} else { //echo $变量
printf("%s\n", argv[1]+1);
}
continue;
}
//创建子进程
pid_t id = fork();
if(id == -1) {
perror("fork");
exit(1);
} else if (id == 0) { //子进程
int ret = execvp(argv[0], argv); //进程程序替换
if(ret == -1) {
printf("No such file or directory\n");
exit(1);
}
} else { //父进程
int status = 0;
pid_t ret = waitpid(id, &status, 0); //进程等待
EXIT_CODE = (status >> 8) & 0xFF; //获取退出状态
if(ret == -1){
perror("wait");
exit(1);
}
}
}
return 1; //while循环正常情况下不会结束
}