- Linux 基础入门(新版)
- C 语言实现 Linux Shell 命令解释器
- C 语言实现 Linux touch 命令
- C 语言实现多线程排序
- 多线程生产者消费者模型仿真停车场
- 在 Github Pages 上部署自己的简历
- Linux 系统安装及配置邮件服务器
- Shell 脚本实现 Linux 系统监控
- C 语言实现 Linux 网络嗅探器
- Vim 编辑器
- Ansible 基础教程
- C 语言实现聊天室软件
- Linux 防火墙技术
- Linux 系统搭建及配置 DNS 服务器
- Linux 系统监控实战
- 操作系统原理与实践
- 实验环境的工作模式
- 使用方法
- Linux 系统安装配置版本控制服务器
- LAMP 部署及配置
- Linux 内核分析
- 正则表达式基础
- TCP/IP 网络协议基础
- Linux Web 运维(Nginx)实战
- Linux 命令实例练习
- 高级 Bash 脚本编程指南
- Memcache 基础教程
- 操作系统实验-基于 uCore OS
- Linux 系统编程
第 1 节 C 语言实现 Linux Shell 命令解释器
一、实验简介
《C 语言实现 Linux Shell 命令解释器》项目可以培养 Linux 系统编程能力,尤其是在多进程方面。可以了解 fork
、 execvp
等重要的系统调用。另外还能深入到底层理解 Linux Shell 的功能的实现手段。
1.1 知识点
- Shell 的基本概念和生命周期
- 进程控制相关的系统调用的使用(如
fork
,exev
)。 - 信号的概念及系统调用
signal
的使用。
1.2 效果截图
1.3 设计流程
二、实验主要步骤一:main 函数的设计
首先,让我们以自顶向下的方式探讨一下在 Linux Shell 的周期内主要做了哪些事。这里总结如下:
- 初始化:在这一步,一个典型的 Shell 应该读取配置文件并执行配置功能。这样可以改变 Shell 的行为。
- 解释:接下来,Shell 将从标准输入(可以是交互的方式或者一个脚本文件)中读入命令,然后执行它。
- 终止:在命令被执行之后,Shell 执行关闭命令,释放内存,最后终止。
- 这部分的代码如下:
/* 主函数入口 */
int main()
{
char *command;
int iter = 0;
command = (char *)malloc(MAX+1); //用于存储命令语句
chdir("/home/shiyanlou"); /* 通常在登陆 shell 的时候通过查找配置文件(/etc/passwd 文件)
中的起始目录,初始化工作目录,这里默认为家目录 */
while(1)
{
iter = 0;
/* 捕捉信号,当信号发生时调用信号处理函数 sig_handle */
signal(SIGINT, sig_handle); // SIGINT = 2, 用户键入 Ctrl-C
signal(SIGQUIT, sig_handle); /* SIGQUIT = 3 用户键入 Ctr+\ */
signal(SIGCHLD, sig_handle); //SIGCHLD = 17,一般在子进程中止时产生
signal(SIGTSTP, SIG_IGN); // SIGTSTP =20,一般有 Ctrl-D 产生
//用于输出提示符
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 函数
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;
}
3.1 处理标准输出重定向问题的函数: file_out
本函数的定义为: 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);
}
}
3.2 处理标准输入重定向问题的函数: file_in
本函数的定义原型为 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;
}
3.3 实现命令执行的最后一步: bf_exec
本函数定义的原型为: 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_DFL); /* 接受 SIGTSTP 信号(由 Ctrl-Z 的时候产生 ),
采取系统默认动作,暂停进程 */
execvp(arg[0], arg); // execvp 用于在子进程中替换为另一段程序
}
else //父进程
{
pid_t c;
signal(SIGTSTP, SIG_DFL); /* 接受 SIGTSTP 信号(由 Ctrl-Z 的时候产生 ),
采取系统默认动作,启动进程 */
c = wait(&pid); //等待子进程结束
dup2(current_out, 1); //还原默认的标准输出
dup2(current_in, 0); //还原默认的标准输入
return;
}
}
else // 后台执行
{
signal(SIGCHLD, bg_signal_handle);
if((pid = fork()) < 0)
{
printf("*** ERROR: forking child process failed\n");
return ;
}
else if(pid == 0) // 子进程
{
int f;
execvp(arg[0], arg);
}
else // 父进程
{
bg_struct_handle(pid, arg, 0);
dup2(current_out, 1);
dup2(current_in, 0);
return ;
}
}
}
四、实验总结
通过本实验,我们完成了一个支持输入输出重定向,支持后台运行的 Linux Shell 命令解释器。通过该项目的学习,可以更进一步的了解 Linux Shell 的工作机制。
- 该项目的完整代码如下:
#include<stdio.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/utsname.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<sys/ptrace.h>
#include<sys/types.h>
#define MAX 1024
#define MAX_COMM 100
// 全局声明
void print_prompt();
void bg_struct_handle(pid_t pid, char *arg[], int type);
char cwd[MAX]; //保存当前路径
char *all[MAX]; //all 用来保存命令行字符串
int current_out = 4;
int current_in = 5;
int fd[4];
typedef struct proc{ //针对后台进程
pid_t pid;
int status;
char *arg[MAX_COMM];
struct proc *next;
}proc;
proc *start;
/* 信号处理函数 */
void sig_handle(int sig)
{
if(sig == 2) // SIGINT = 2
{
printf("\nInstead of Ctrl-C type quit\n");
print_prompt();
}
else if(sig == 3) //SIGQUIT = 3
{
printf("\nType quit to exit\n");
print_prompt();
}
signal(sig, sig_handle); //SIGCHLD = 17
}
/* 用于输出提示符 */
void print_prompt()
{
// 调用 uname 获取系统信息
struct utsname uname_ptr;
uname(&uname_ptr);
//调用 getcwd 获取当前路径名,并存储在 cwd 指向的字符串
getcwd(cwd, sizeof(cwd));
setbuf(stdout, NULL); //禁用 buffer, 直接将 printf 要输出的内容输出
printf("<%s@%s:%s> ",uname_ptr.nodename, uname_ptr.sysname, cwd);
}
/* 扫描用户输入的命令行 */
void scan_command(char *command)
{
int bytes_read;
size_t nbytes = MAX;
bytes_read = getline(&command, &nbytes, stdin); /*从标准输入中读入包含
多条命令行语句的字符串,并保存在 command 变量中 */
bytes_read -= 1;
command[bytes_read] = '\0';
}
/* 以空格为分解命令及其参数 */
void *parse(char *command, int time)
{
char *comm; // 用于储存命令或命令的参数
if(time ==0)
comm = strtok(command, " ");
else
comm = strtok(NULL, " ");
return comm;
}
/* 分割用户以分号分隔的多命令,如“ls;cd”*/
void parse_semicolon(char *command)
{
int i ;
for (i=0; i < MAX; i++)
all[i] = (char *) malloc(MAX_COMM * sizeof(char));
i = 0;
all[i] = strtok(command, ";"); //注意 strtok 的用法,用来分割命令行
while(1)
{
i += 1;
all[i] = strtok(NULL, ";");
if(all[i] == NULL)
break;
}
}
/* 内建命令 cd 的实现 */
void cd(char *arg)
{
if(arg == NULL)
{
printf("insufficient arguments\n");
}
else
{
int cond;
cond = chdir(arg);
if(cond == -1)
{
printf("wrong path\n");
}
}
}
/* 调用 bg_struct_handle 对后台进程 proc 信息的处理 */
void bg_signal_handle()
{
int status;
pid_t pid;
pid = waitpid(-1, &status, WNOHANG);
proc *iterate;
iterate = start;
while(iterate != NULL)
{
if(iterate -> pid == getpid())
{
bg_struct_handle(pid, NULL, 1); // 将结束的子进程从 proc 上删除相关信息(第三个参数为 1)
}
}
}
/* 处理 proc 链表(增、删、打印) */
void bg_struct_handle(pid_t pid, char *arg[], int type)
{
proc *iterate, *new;
if(type == 0) // proc 链表上增加一个 proc 结构
{
if(start == NULL)
{
start = (proc *)malloc(sizeof(proc));
start -> pid = pid;
start -> status = 1;
start -> next = NULL;
int i = 0;
while(arg[i] != NULL)
{
start -> arg[i] = malloc(MAX_COMM * sizeof(char));
strcpy(start -> arg[i], arg[i]);
i += 1;
}
start -> arg[i] = NULL;
}
else
{
new = (proc *)malloc(sizeof(proc));
new -> pid = pid;
new -> status = 1;
new -> next = NULL;
int i = 0;
while(arg[i] != NULL)
{
new -> arg[i] = malloc(MAX_COMM * sizeof(char));
strcpy(new -> arg[i], arg[i]);
i += 1;
}
new -> arg[i] = NULL;
iterate = start ;
while(iterate -> next != NULL)
iterate = iterate -> next;
iterate -> next = new;
}
}
else if(type == 1) // proc 链表上删除一个 proc 结构
{
proc *preiter = NULL;
iterate = start;
while(iterate != NULL && iterate -> pid != pid )
{
preiter = iterate;
iterate = iterate -> next;
}
if(iterate == NULL)
{
printf("No Such Pid !\n");
return ;
}
else if(iterate -> pid == pid)
{
if(preiter == NULL)
{
start = iterate -> next;
free(iterate);
}
else
{
preiter -> next = iterate -> next;
free(iterate);
}
}
}
else if(type == 2) //迭代地打印 proc 链表的 proc 结构信息
{
int i = 1, a = 0;
iterate = start;
if (iterate == NULL)
{
printf("No Background jobs\n");
return ;
}
while(iterate != NULL)
{
a = 0;
setbuf(stdout, NULL);
printf("[%d] ",i);
while(iterate -> arg[a] != NULL)
{
printf("%s ", iterate -> arg[a]);
a += 1;
}
printf("[%d]\n", iterate -> pid);
i += 1;
iterate = iterate -> next;
}
}
return ;
}
/* 创建子进程,调用 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_DFL); /* 接受 SIGTSTP 信号(由 Ctrl-Z 的时候产生 ),
采取系统默认动作,暂停进程 */
execvp(arg[0], arg); // execvp 用于在子进程中替换为另一段程序
}
else //父进程
{
pid_t c;
signal(SIGTSTP, SIG_DFL); /* 接受 SIGTSTP 信号(由 Ctrl-Z 的时候产生 ),
采取系统默认动作,启动进程 */
c = wait(&pid); //等待子进程结束
dup2(current_out, 1); //还原默认的标准输出
dup2(current_in, 0); //还原默认的标准输入
return;
}
}
else // 后台执行
{
signal(SIGCHLD, bg_signal_handle);
if((pid = fork()) < 0)
{
printf("*** ERROR: forking child process failed\n");
return ;
}
else if(pid == 0) // 子进程
{
int f;
execvp(arg[0], arg);
}
else // 父进程
{
bg_struct_handle(pid, arg, 0);
dup2(current_out, 1);
dup2(current_in, 0);
return ;
}
}
}
/* 处理输出重定向文件的问题 */
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)
{
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 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;
}
/* 主函数入口 */
int main()
{
char *command;
int iter = 0;
command = (char *)malloc(MAX+1); //用于存储命令语句
chdir("/home/shiyanlou"); /* 通常在登陆 shell 的时候通过查找配置文件(/etc/passwd 文件)
中的起始目录,初始化工作目录,这里默认为家目录 */
while(1)
{
iter = 0;
/* 捕捉信号,当信号发生时调用信号处理函数 sig_handle */
signal(SIGINT, sig_handle); // SIGINT = 2, 用户键入 Ctrl-C
signal(SIGQUIT, sig_handle); /* SIGQUIT = 3 用户键入 Ctr+\ */
signal(SIGCHLD, sig_handle); //SIGCHLD = 17,一般在子进程中止时产生
signal(SIGTSTP, SIG_IGN); // SIGTSTP =20,一般有 Ctrl-D 产生
//用于输出提示符
print_prompt();
//扫描多条命令语句
scan_command(command);
// 基于分号解析出单条命令语句,并以字符串的形式存进全局变量 all 中
parse_semicolon(command);
// 迭代执行单条命令语句
while(all[iter] != NULL)
{
execute(all[iter]); //这是 shell 解释器的核心函数
iter += 1;
}
}
}
参考资料
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论