《C 语言实现 Linux Shell 命令解释器》项目可以培养 Linux 系统编程能力,尤其是在多进程方面。可以了解fork、execvp 等重要的系统调用。另外还能深入到底层理解 Linux Shell 的功能的实现手段。为了测试方便,代码只放在一个文件中。
- Shell 的基本概念
- 进程控制相关的系统调用的使用(如 fork,exev)
- 信号的概念及系统调用 signal的使用
首先,以自顶向下的方式探讨一下在 Linux Shell 的周期内主要做了哪些事: 初始化:在这一步,一个典型的 Shell 应该读取配置文件并执行配置功能。这样可以改变 Shell 的行为。 解释:接下来,Shell 将从标准输入(可以是交互的方式或者一个脚本文件)中读入命令,然后执行它。 终止:在命令被执行之后,Shell 执行关闭命令,释放内存,最后终止。 详细见代码注释:
/* 主函数入口 */
/* 主函数入口 */
int main()
{
char *command;
int iter = 0;
command = (char *)malloc(MAX+1); //用于存储命令语句
chdir("/home/"); /* 通常在登陆 shell 的时候通过查找配置文件(/etc/passwd 文件)
中的起始目录,初始化工作目录,这里默认为家目录 */
while(1)
{
iter = 0;
//定义信号处理方式
signal_set();
//用于输出提示符
print_prompt();
//扫描多条命令语句
scan_command(command);
// 基于分号解析出单条命令语句,并以字符串的形式存进全局变量 all 中
parse_semicolon(command);
// 迭代执行单条命令语句
while(all[iter] != NULL)
{
execute(all[iter]); //这是 shell 解释器的核心函数
iter += 1;
}
}
}
- 注:print_prompt()、scan_command(command) 和 parse_semicolon(command) 三个函数是自定义函数,功能分别为:用于输出提示符,扫描多条命令语句,基于分号解析出单条命令语句,并以字符串的形式存进全局变量 all 中。概念简单,其详细代码在项目完整代码中。
execute函数完成的主要功能如下:
- 判断用户执行的是否是 Shell 的内建命令,如果是则执行(这里为了强调概念,没有过多的添加内建命令)
- 解析单条命令语句,即将命令名和命令参数解析并保存在字符串数组,以便调用 bf_exec命令。
- 识别输入输出重定向符号,利用底层系统调用函数 dup2完成上层输入输出重定向
- 识别后台运行符号,通过设置自定义函数 bf_exec的模式为1实现,这里要说明一点:所谓后台执行,无非就是让前台 Shell 程序以非阻塞的形式执行,并没有对后台程序做任何操作
- 这部分的完整代码如下:
/* 执行单条命令行语句 */
void execute(char *command)
{
char *arg[MAX_COMM];
char *try;
arg[0] = parse(command, 0); //获得命令名称的字符串指针,如“ls”
int t = 1;
arg[t] = NULL;
if (strcmp(arg[0], "cd") == 0) // 处理内嵌命令“cd”的情况
{
try = parse(command, 1);
cd(try);
return ;
}
if (strcmp(arg[0], "exit") == 0) // 为了方便用户推出shell
exit(0);
// 循环检测剩下的命令参数,即检测是否:重定向?管道?后台执行?普通命令参数?
while (1)
{
try = parse(command, 1);
if (try == NULL)
break;
else if (strcmp(try, ">") == 0) // 重定向到一个文件的情况
{
try = parse(command, 1); // 得到输出文件名
file_out(arg, try, 0); // 参数 0 代表覆盖的方式重定向
return;
}
else if (strcmp(try, ">>") == 0) // 追加重定向到一个文件
{
try = parse(command, 1);
file_out(arg, try, 1); // 参数 1 代表追加的方式重定向
return;
}
else if (strcmp(try, "<") == 0) // 标准输入重定向
{
try = parse(command, 1); // 输入重定向的输入文件
char *out_file = parse(command, 1); // 输出重定向的输出文件
if (out_file != NULL)
{
if (strcmp(out_file, ">") == 0) // 输入输出文件给定
{
out_file = parse(command, 1);
if (out_file == NULL)
{
printf("Syntax error !!\n");
return;
}
else
file_in(arg, try, out_file, 1); // 参数 1 针对双重定向 < >
}
else if (strcmp(out_file, ">>") == 0)
{
out_file = parse(command, 1);
if (out_file == NULL)
{
printf("Syntax error !!\n");
return;
}
else
file_in(arg, try, out_file, 2); // 参数 2 针对双重定向 < >>
}
}
else
{
file_in(arg, try, NULL, 0); // 模式 0 针对单一的输入重定向
}
}
//处理后台进程
else if (strcmp(try, "&") == 0) // 后台进程
{
bf_exec(arg, 1); // bf_exec 的第二个参数为 1 代表后台进程
return;
}
else //try是一个命令参数
{
arg[t] = try;
t += 1;
arg[t] = NULL;
}
}
bf_exec(arg, 0); // 参数 0 表示前台运行
return;
}
本函数的定义为: void file_out(char *arg[], char *out_file, int type)。 arg 代表命令行参数。 out_file 代表重定向的文件名。 通过** dup2(f, 1)** 语句使得标准输出文件(文件描述符为1)重定向到制定的文件out_file(文件描述符f)。至于是追加重定向还是覆盖重定向取决于函数 open 的打开模式(是否是 O_APPEND)。 这部分的完整代码如下:
/* 处理输出重定向文件的问题 */
void file_out(char *arg[], char *out_file, int type)
{
int f;
current_out = dup(1);
if(type == 0) //处理以覆盖的方式重定向“>”
{
f = open(out_file, O_WRONLY | O_CREAT, 0777);
dup2(f, 1); //复制文件描述符f,并指定为 1,也就是让标准输出重定向到指定的文件
close(f);
bf_exec(arg, 0);
}
else // 处理以追加的方式重定向“>>”
{
f = open(out_file, O_WRONLY | O_CREAT | O_APPEND , 0777); //以 O_APPEND 模式打开
dup2(f, 1);
close(f);
bf_exec(arg, 0);
}
}
本函数的定义原型为 void file_in(char *arg[], char *in_file, char *out_file, int type) 类似于 file_out函数,该本分仍然采用 dup2(in, 0)来完成标准输入文件(文件描述符0)重定向到执行的文件 in_file(文件描述符为 in)。
- 这部分的完整代码如下:
/* 处理输入重定向文件的问题 */
void file_in(char *arg[], char *in_file, char *out_file, int type)
{
int in;
in = open(in_file, O_RDONLY);
current_in = dup(0); //
dup2(in, 0);
close(in);
if(type == 0) // 单一的输入重定向
{
printf("Going to execute bf_exec\n"); //debug remove it
bf_exec(arg, 0);
}
else if(type == 1) // 双重重定向 `< ... >`
{
file_out(arg, out_file, 0);
}
else // 双重重定向 `< ... >>`
{
file_out(arg, out_file, 1);
}
return;
}
本函数定义的原型为:void bf_exec(char *arg[], int type), arg 指命令行,type标识前台还是后台运行。
- 实现命令的执行必须在 Shell 父进程下 fork 一个子进程,并在子进程中调用 execvp 函数装载子进程执行的代码(所要执行的命令)。
- 程序的前后台执行取决于父进程在子进程执行过程中是否是阻塞的。代码中,前台进程的父进程需要调用 wait(&pid) 阻塞,等待前台执行的命令执行完才能唤醒。
- 这部分的完整代码如下:
/* 创建子进程,调用 execvp 函数执行命令程序(前后台)*/
void bf_exec(char *arg[], int type)
{
pid_t pid;
if(type == 0) // 前台执行
{
if((pid = fork()) < 0)
{
printf("*** ERROR: forking child process failed\n");
return ;
}
// 父子进程执行代码的分离
else if(pid == 0) //子进程
{
signal(SIGTSTP, SIG_IGN);
signal(SIGINT,SIG_DFL);//用于杀死子进程
signal(SIGQUIT, SIG_IGN);
execvp(arg[0], arg); // execvp 用于在子进程中替换为另一段程序
}
else //父进程
{
signal(SIGCHLD, bg_signal_handle);
signal(SIGINT, sig_handle); // SIGINT = 2, 用户键入Ctrl-C
signal(SIGQUIT, sig_handle); /* SIGQUIT = 3 用户键入Ctr+\ */
signal(SIGTSTP, sig_handle); // SIGTSTP =20,一般有Ctrl-Z产生
pid_t c;
c = wait(&pid); //等待子进程结束
dup2(current_out, 1); //还原默认的标准输出
dup2(current_in, 0); //还原默认的标准输入
return;
}
}
else // 后台执行
{
if((pid = fork()) < 0)
{
printf("*** ERROR: forking child process failed\n");
return ;
}
else if(pid == 0) // 子进程
{
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGTSTP,SIG_IGN);
execvp(arg[0], arg);
}
else // 父进程
{
signal(SIGCHLD, bg_signal_handle);
signal(SIGINT, sig_handle); // SIGINT = 2, 用户键入Ctrl-C
signal(SIGQUIT, sig_handle); /* SIGQUIT = 3 用户键入Ctr+\ */
signal(SIGTSTP, sig_handle); // SIGTSTP =20,一般有Ctrl-Z产生
bg_struct_handle(pid, arg, 0);
dup2(current_out, 1);
dup2(current_in, 0);
return ;
}
}
}
通过本实验,我们完成了一个支持输入输出重定向,支持后台运行的 Linux Shell 命令解释器。通过该项目的学习,可以更进一步的了解 Linux Shell 的工作机制。在使用重定向 > 和>> 时 建议将目录切换到自己的家目录,否则没权限建立重定向文件。
但是在本次实验中出现了一点bug还没有解决,重定< file1
和< file1 >> file2
的功能在运行时没有效果。希望各位童鞋能够改进代码,代码中的不足也希望大家能够指出,谢谢。
- 该项目的完整代码见:github