做一个简易的shell
shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可。
其实shell需要执行的逻辑非常简单,其只需循环执行以下步骤:
- 获取命令行。
- 解析命令行。
- 创建子进程。
- 替换子进程。
- 等待子进程退出。
其中,创建子进程使用fork函数,替换子进程使用exec系列函数,等待子进程使用wait或者waitpid函数。
于是我们可以很容易实现一个简易的shell,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
#define STREND '\0'
char* argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE];
int lastcode = 0;
//下面都和重定向有关
#define NoneRedir -1
#define StdinRedir 0
#define StdoutRedir 1
#define AppendRedir 2
#define IgnSpace(buf, pos) do{while(isspace(buf[pos])) pos++;}while(0)
int redir_type = NoneRedir;
char* filename = NULL;
const char* UserName()
{
char* username = getenv("USER");
if(username) return username;
else return "None";
}
const char* HostName()
{
char* hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "None";
}
const char* CurrentWorkDir()
{
char* currentworkdir = getenv("PWD");
if(currentworkdir) return currentworkdir;
else return "None";
}
char* Home()
{
return getenv("HOME");
}
int Interactive (char out[], int size)
{
//输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$", UserName(), HostName(), CurrentWorkDir());
fgets(out, size, stdin); //将键盘输入的,读取到commandline数组里面
out[strlen(out)-1] = '\0'; //最少输入一个回车吧,将"ls -a -l\n"将回车置为'\0'
return strlen(out); //返回字符串的个数,如果只有一个回车,即为空串,啥也不干
}
void CheckRedir(char in[])
{
//ls -a -l > log.txt
//ls -a -l >> log.txt
//cat < log.txt
redir_type = NoneRedir; //每次进来初始化,因为全局变量
filename = NULL;
int pos = strlen(in) -1; //倒着来
while(pos>=0)
{
if(in[pos] == '>')
{
if(in[pos-1] == '>')
{
redir_type = AppendRedir;
in[pos-1] = STREND;
pos++;
IgnSpace(in, pos);
filename = in+pos;
break;
}
else
{
redir_type = StdoutRedir;
in[pos++] = STREND;
IgnSpace(in, pos);
filename = in + pos;
break;
}
}
else if(in[pos] == '<')
{
redir_type = StdinRedir;
in[pos++] = STREND;
IgnSpace(in, pos);
filename = in + pos;
break;
}
else
{
pos--;
}
}
}
void Split(char in[])
{
CheckRedir(in); //检查是否需要重定向
int i = 0;
argv[i++] = strtok(in, SEP); //"ls -a -l" 以空格进行分隔切割
while(argv[i++] = strtok(NULL, SEP)); //最后一次切割失败返回NULL存到最后一个位置,表达式值为NULL 循环结束
if(strcmp(argv[0], "ls") == 0) //如果是ls命令带上颜色
{
argv[i-1] = (char*)"--color";
argv[i] = NULL;
}
}
int BuildCmd()
{
int ret = 0;
//1.检测是否是内建命令 ,是 1, 不是 0
if(strcmp("cd", argv[0]) == 0)
{
//2. 执行
ret = 1;
char* target = argv[1]; //cd xxx or cd cd 空 默认进入家目录
if(!target) target = Home(); //为空 就将target改成家目录
chdir(target); //改变当前工作目录
char temp[1024]; //由于cd xxx 可能是相对路径,所以PWD环境变量要更改成对应的绝对路径,所以调用getcwd函数,可以同步将PWD环境变量修改成绝对路径,这样pwd指令可以显示出绝对路径
getcwd(temp, 1024); //获取当前工作目录的绝对路径,并存到temp字符数组里面
snprintf(pwd, SIZE, "PWD=%s", temp); //将"pwd=%s"%s用temp替换后整体输出
//到pwd字符数组里面,再导出pwd为新的环境变量
putenv(pwd); //导出环境变量pwd修改
}
else if(strcmp("export", argv[0]) == 0)
{
ret = 1;
if(argv[1])
{
strcpy(env, argv[1]); //因为argv【1】指向的会变,所以新导入的环境变量需要保存起来
putenv(env);
}
}
else if(strcmp("echo", argv[0]) == 0)
{
ret = 1;
if(argv[1] == NULL)
{
printf("\n");
}
else
{
if(argv[1][0] == '$')
{
if(argv[1][1] == '?')
{
printf("%d\n", lastcode); //echo $? 输出最近进程退出码
lastcode = 0;
}
else
{
char* e = getenv(argv[1]+1); //echo $其它环境变量名
if(e) printf("%s\n", e);
}
}
else
{
printf("%s\n", argv[1]); //正常输出
}
}
}
return ret;
}
void Execute()
{
pid_t id = fork();
if(id==0)
{
umask(0);
int fd = -1;
if(redir_type == StdinRedir)
{
fd = open(filename, O_RDONLY, 0666);
dup2(fd, 0);
}
else if(redir_type == StdoutRedir)
{
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir_type == AppendRedir)
{
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
dup2(fd, 1);
}
else
{
//do nothing
}
//让子进程执行命名
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) lastcode = WEXITSTATUS(status);
}
int main()
{
while(1)
{
char commandline[SIZE];
//1.打印命令提示符,获取用户输入的命令字符串
int n = Interactive(commandline, SIZE);
if(n==0) continue; //返回字符串的个数,如果只有一个回车,即为空串,啥也不干
//2.对命令行字符串进行切割,放到自定义的argv表里面
Split(commandline);
//3.切割完,判断命令是否是内建命令,是内建命令myshell进程自己执行,否则子进程执行
//n 返回1是内建命令,后面不用执行了,返回0不是内建命令,创建子进程执行
n = BuildCmd();
if(n) continue;
// 4. 不是内建命令,创建子进程执行这个命令
Execute();
}
return 0;
}
说明:
当执行./myshell命令后,便是我们自己实现的shell在进行命令行解释,我们自己实现的shell在子进程退出后都打印了子进程的退出码,我们可以根据这一点来区分我们当前使用的是Linux操作系统的shell还是我们自己实现的shell。
函数和进程之间的相似性
如果你只学了C语言、C++或是JAVA等高级语言,你可能只知道函数间可以相互调用,但当你学习了进程的相关知识后,你的视野也就不止于此了,因为各个程序之间其实也是可以相互调用的。
如果你学过C语言,你应该有以下认识:
- 一个C程序由很多函数组成,一个函数可以调用另一个函数,同时传递给它一些参数。
- 被调用的函数执行一定的操作,然后返回一个值。
- 每个函数都有它自己的局部变量。
- 不同函数通过call/return系统进行通信。
这种通过参数和返回值,在拥有私有数据的函数间通信的模式是结构化程序设计的基础,Linux鼓励将这种应用于程序之内的模式扩展到程序之间。
一个进程可以使用fork创建一个子进程,然后使用exec系列函数将子进程的代码和数据替换为另一个程序的代码和数据,之后子进程就用该程序的数据执行该程序的代码,从而达到程序之间相互调用的效果。
pid_t id = fork();
if (id == 0){
execvp(myargv[0], myargv);
exit(1);
}
当这个被调用的程序执行完毕后,通过exit(n)来返回一个值。调用它的进程可以通过wait或waitpid来获取这个返回值。
wait(&status);
waitpid(id, &status, 0);
程序之间相互调用带来的好处之一
我们都知道各个语言有自己独特的优势,当我们做某一技术开发时,可能需要用到多种语言,而我们最终就是利用程序之间的相互调用使得各个语言之间可以进行衔接。
例如,一个C程序可以通过exec系列函数调用shell脚本、python以及C++等语言实现的程序。
shell脚本:
python:
C++:
我们使用以下C程序,便可以分别调用以上三个程序。
调用shell脚本运行结果: