需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)
一个Shell是一直在运行着的,为了保证Shell本身的运行稳定,所以每次在接收到指令的时候,会派生子进程,把子进程替换成要执行的指令,执行完毕之后子进程被回收,然后再次回到等待指令的时候。
主要步骤:
- 输出命令提示符
- 从终端获取命令行输入
- 解析命令行输入信息;
- 创建子进程;
- 进程程序替换;
- 进程等待;
通过上述的分析,我们可以画出以下的流程图:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>
#define NUM 1024//输入缓冲区大小
#define OPT_NUM 64//命令参数最大个数
char lineCommand[NUM];//输入缓冲区
char* argv[OPT_NUM];
int main()
{
while(1)//死循环,因为Shell要一只运行着
{
//打印输出命令提示符
printf("[用户名@主机名 当前路径]$ ");
fflush(stdout);//由于打印命令提示符的时候没有换行,所以这里手动刷新缓冲区
//获取输入
char* str = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);//最后一个位置用于在极端情况下保证字符串内有'\0'
assert(str);//判断合法性
(void)str;
lineCommand[strlen(lineCommand) - 1] = '\0';//消除行命令中的换行
//命令解析(字符串切割)
argv[0] = strtok(lineCommand, " ");
int i = 1;
while(argv[i++] = strtok(NULL, " "));//使用字符串切割函数依次拿到每个参数
//创建子进程
pid_t id = fork();
if(id == -1)
{
perror("fork");
exit(errno);
}
else if(id == 0)
{
//child
//进程程序替换
execvp(argv[0], argv);
//执行到此处的时候,证明进程替换错误
perror("exec:");
exit(errno);
}
else
{
//parent
//进程等待
int status = 0;//退出状态
pid_t ret = waitpid(id, &status, 0);//阻塞等待
if(ret == -1)
{
perror("wait fail");
exit(errno);
}
}
}
return 0;
}
可以看到,这里我们自己的Shell已经能够执行Linux的基本指令啦,但是我们的ls好像是没有颜色的,那么这里我们在Shell里面加上特判
if(argv[0] != NULL && strcmp(argv[0], "ls") == 0) //ls颜色显示
{
argv[i++] = (char*)"--color=auto";
}
我们知道,在进程运行起来之后,在/proc
目录下会有一个内存级的目录存放进程的相关信息,我们可以在这里找到我们运行的进程。
可以看到,这里有两个路径。
- exe:代表可执行文件在磁盘中的路径
- cwd:current work directory,当前进程的工作路径,即我们常说的当前路径
当前路径可以更改吗?
可以,当前路径可以通过系统调用
chdir
更改
path
:想要更改的路径
return value
:调用成功返回0,失败返回-1
可以看到,cwd已经被更改。
在我们之前实现的Shell中,如果想要使用cd命令更改当前路径,似乎是做不到的
这是因为,按照我们之前的思路,每次都是派生子进程来执行命令,那么子进程的工作目录会切换到我们指定的路径,但是子进程执行完毕之后就被回收了,我们在父进程看不见路径的切换。
所以有了上述chdir
的前置知识,那么就很好解决这个问题了:在命令解析之后,如果发现遇到了cd
命令,就不要派生子进程,直接使用父进程执行系统调用chdir
。
Linux下的命令分为两种:内建命令和外部命令
- 内建命令是 shell 程序的一部分,其功能实现在 bash 源代码中,不需要派生子进程来执行,也不需要借助外部程序文件来运行,而是由 shell 进程本身内部的逻辑来完成;
- 外部命令则是通过创建子进程,然后进行进程程序替换,运行外部程序文件等方式来完成。
我们可以使用type命令来区分内建命令还是外部命令
注:博主这里使用的是汉化过的云服务器,所以结果是中文,翻译上有一点差别
我们上面对cd
命令的处理就是内建命令的一种,myshell 遇到 cd 命令时,由自己直接来改变进程工作目录,处理完毕直接 continue,而不会创建子进程。
同时,我们发现 echo 命令也是一个内置命令,这其实也很好的解释了 为什么 “echo $变量” 可以查看本地变量以及为什么 “echo $?” 可以获取最近一个进程的退出码 了:
虽然本地变量只在当前进程有效,但是使用 echo 查看本地变量时,shell 并不会创建子进程,而是直接在当前进程中查找,自然可以找到本地变量;
shell 可以通过进程等待的方式获取上一个子进程的退出状态,然后将其保存在 ? 变量中,当命令行输入 “echo $?” 时,直接输出 ? 变量中的内容,然后将 ? 置为0 (echo 正常退出的退出码),也不需要创建子进程。
那么,我们可以为我们的Shell添加echo
命令了。
最后,经过一系列的优化,我们最终得到了一个简易的Shell的demo,这里附上源码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>
#define NUM 1024//输入缓冲区大小
#define OPT_NUM 64//命令参数最大个数
char lineCommand[NUM];//输入缓冲区
char* argv[OPT_NUM];
int EXIT_CODE;
int main()
{
while(1)//死循环,因为Shell要一只运行着
{
//打印输出命令提示符
printf("[用户名@主机名 当前路径]$ ");
fflush(stdout);//由于打印命令提示符的时候没有换行,所以这里手动刷新缓冲区
//获取输入
char* str = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);//最后一个位置用于在极端情况下保证字符串内有'\0'
assert(str);//判断合法性
(void)str;
lineCommand[strlen(lineCommand) - 1] = '\0';//消除行命令中的换行
//命令解析(字符串切割)
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)
{
if(argv[1] != NULL)
{
chdir(argv[1]);
}
else
{
printf("no such file or directory\n");
}
continue;
}
if(argv[0] != NULL && strcmp(argv[0], "echo") == 0)
{
if(strcmp(argv[1], "$?") == 0)
{
printf("%d\n", EXIT_CODE);
EXIT_CODE = 0;
}
else
{
printf("%s\n", argv[1]);
}
continue;
}
//创建子进程
pid_t id = fork();
if(id == -1)
{
perror("fork");
exit(errno);
}
else if(id == 0)
{
//child
//进程程序替换
execvp(argv[0], argv);
//执行到此处的时候,证明进程替换错误
perror("exec:");
exit(errno);
}
else
{
//parent
//进程等待
int status = 0;//退出状态
pid_t ret = waitpid(id, &status, 0);//阻塞等待
EXIT_CODE = (status >> 8) & 0xFF;
if(ret == -1)
{
perror("wait fail");
exit(errno);
}
}
}
return 0;
}
本节完