前言
因为是一年前的实验,很多细节还有知识点我都已经遗忘了,但我还是尽可能地把各个细节讲清楚,请见谅。
1.实验目的
综合利用进程控制的相关知识,结合对shell功能的和进程间通信手段的认知,编写简易shell程序,加深操作系统的进程控制和shell接口的认识。
2.实验内容
可以使用Linux或其它Unix类操作系统,全面实践进程控制、进程间通信的手段,编写简易shell程序要求如下:
1. 学习Shell,系统编程,实现一个基本的Shell。
2. shell是Linux等系统中的一个命令解释器, 它接受输入的命令, 解释之后与操作系统进行交互. 在Linux终端Terminal输入的指令就是被shell接收的。在shell中实现输入输出。
3. 在自己编写的Shell中 实现bash的基本指令包括 cd ,ls 管道等指令
3.实验的内容与过程
实验前需要掌握的知识点:
在实验前,我们应该先明白shell有以下几个功能:
实现一个命令解析的程序
命令包含内部命令、外部命令和非法命令
内部命令包含:使用帮助的help命令,打印内容echo,目录切换cd,退出程序exit或q
外部命令包含:系统命令,在$PATH下的命令
非法命令:找不到的命令
Shell的工作流程主要如下:
①打印提示符:可参照bash提示符,如用户名@主机名,或者自定义提示符,如myshell >
②接收用户输入的命令:按行读取内容
③解析用户输入的命令:解析行内容,按照空格来分隔成字符串数组
④执行命令:执行命令并打印命令结果到终端
⑤循环第一步
初步框架:
根据shell的工作流程,我们不难得出代码的初步框架:
根据上述代码框架,我们先编写出自己的框架。
分析:
getcwd函数用于获取当前工作目录。因为shell的命令输入是一直运行的,所以我们用while(1)来确保其不断运行。接着在while中有三个自定义函数,分别为:print_promt(用来显示命令行提示符:打印用户名和当前工作目录)、read_line(用来读取用户输入的命令)、parse_cmd(用来处理用户输入的命令)。接下来我就解释一下这三个自定义函数的功能。
print_promt函数:
print_promt函数:(打印的用户名一定要记得改)
分析:
这个函数的主要功能为打印命令行提示符,打印的内容主要为用户名和当前的工作目录,分别用蓝色字体和绿色字体输出。
打印结果:
read_line函数:
分析:
这个函数的主要功能为读取用户输入的命令,通过fgets函数将命令写入全局变量command中,如果读入失败的话,则退出程序。
Parse_command函数:
分析:
这个函数主要用来判断用户输入的命令是什么命令。当有输入命令时,用自定义函数check来判断,如果返回值为1则说明当前命令为内部命令,调用handleInternalCommand这个自定义函数来处理内部命令。如果在command里面查找到有字符‘|’则说明是一个带有管道的命令,那么就调用executeCommandWithPipes这个自定义函数来处理命令。如果上述两种情况都不满足,则说明是外部命令,便调用自定义函数executeExternalCommand来处理外部命令。接下来就对这几个自定义函数进行解释。
Check函数:
分析:
这个函数主要用来判断命令是否为内部命令,如果是则返回1,否则返回0。(命令可以自行添加)
HandleInternalCommand函数:
分析:
这个函数主要用来处理内部命令。因为我实现了三种内部命令(help、exit和cd)。如果判断出输入的命令为help,则打印出我们的提示信息。如果判断出输入的命令为exit,则退出程序。如果判断输入的命令为cd,则将路径存放起来,改变当前工作目录并保存。如果这三个命令都不是,则输出无效命令的提示消息。
Executeexternalcommand函数:
分析:
这个函数主要用来执行外部命令。如果我们判断当前输入命令为外部命令时调用该函数。在这个函数中,我们先创建一个子进程,在子进程中,我们分割出命令,然后调用execvp函数去执行外部命令。如果执行时出错,则说明是无效命令,并输出相应的提示,一直等到子进程结束。
executeCommandWithPipes函数:
分析:
这个函数主要是执行带有管道的命令。首先将命令按照管道符号“|”分割成多个子命令(由于分割的技巧,我的管道可以识别字符‘|’有空格和没有空格的情况,下面会展示),接着根据子命令个数创建相应个数的管道。接着在管道数组中创建子进程(跟执行外部命令时的道理类似)。在执行子进程时,我们要判断该子进程是第几个子进程:如果不是第一个子命令,则将管道的输入重定向到上一个子命令的输出;如果不是最后一个子命令,则将管道的输出重定向到下一个子命令的输入。(这两个过程可以通过调用dup2函数实现),接着关闭所有管道文件描述符并且执行子命令,一直等到所有子进程都结束了才算真正的执行完成了。注意,一定要关闭所有的管道文件描述符。
将以上的代码结合在一起就能实现一个简单的shell。至此,本次实验已经成功完成了。
4.完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_COMMAND_LENGTH 100
#define MAX_ARGUMENTS 10
#define MAX_PIPES 10
#define RED "\e[0;31m"
#define L_RED "\e[1;31m"
#define GREEN "\e[0;32m"
#define L_GREEN "\e[1;32m"
#define BLUE "\e[0;34m" //正常设置
#define L_BLUE "\e[1;34m" //蓝色线条粗一些(0为正常,1为粗体)
#define END "\033[0m"
void handleInternalCommand(char *command);
void executeExternalCommand(char *command);
void executeCommandWithPipes(char *command);
char *inner_commmand[] = {"help", "exit", "cd"}; // 内部命令
char current_directory[MAX_COMMAND_LENGTH]; // 当前工作目录
char command[MAX_COMMAND_LENGTH]; //存放输入命令
//判断是否为内部命令
int check(char *command)
{
for (int i = 0; i < 3; i++)
{
if (i == 2)
{
if (strncmp(command, "cd ", 3) == 0)
return 1;
}
if (strcmp(command, inner_commmand[i]) == 0)
return 1;
}
return 0;
}
// 输出命令行提示符
void print_prompt()
{
printf("\e[1;34msztu202100202016\033[0m:\e[1;32m%s\033[0m$ ", current_directory);
}
void read_line()
{
// 读取用户输入的命令
if (fgets(command, sizeof(command), stdin) == NULL)
{
// 读取失败,退出程序
exit(0);
}
// 删除末尾的换行符
command[strcspn(command, "\n")] = '\0';
}
void parse_cmd()
{
if (strlen(command) > 0)
{
if (check(command) == 1)
{
// 内部命令
handleInternalCommand(command);
}
else if (strstr(command, "|") != NULL)
{
// 带有管道的命令
executeCommandWithPipes(command);
}
else
{
// 外部命令
executeExternalCommand(command);
}
}
}
// 内部命令的处理函数
void handleInternalCommand(char *command)
{
if (strcmp(command, "help") == 0)
{
printf("这是一个简单的shell程序。\n");
printf("支持的命令:\n");
printf(" help - 显示帮助信息\n");
printf(" exit - 退出myshell\n");
printf(" cd [目录] - 改变当前工作目录\n");
printf(" author:Horizon\n");
}
else if (strcmp(command, "exit") == 0)
{
exit(0);
}
else if (strncmp(command, "cd ", 3) == 0)
{
// 获取目标目录
char *targetDir = &command[3];
// 改变当前工作目录
if (chdir(targetDir) != 0)
{
printf("无法改变目录:%s\n", targetDir);
}
else
{
getcwd(current_directory, sizeof(current_directory)); // 更新当前工作目录
}
}
else
{
printf("无效命令:%s\n", command);
}
}
// 执行外部命令
void executeExternalCommand(char *command)
{
pid_t pid = fork();
if (pid < 0)
{
// fork() 出错
printf("无法创建子进程。\n");
return;
}
else if (pid == 0)
{
// 子进程
char *args[MAX_ARGUMENTS];
// 将命令分割成参数
int argIndex = 0;
char *token = strtok(command, " ");
while (token != NULL && argIndex < MAX_ARGUMENTS - 1)
{
args[argIndex] = token;
token = strtok(NULL, " ");
argIndex++;
}
args[argIndex] = NULL;
// 执行外部命令
execvp(args[0], args);
// execvp() 只在出错时返回
printf("无效命令:%s\n", command);
exit(0);
}
else
{
// 等待子进程结束
int status;
waitpid(pid, &status, 0);
}
}
// 执行带有管道的命令
void executeCommandWithPipes(char *command)
{
char *pipes[MAX_PIPES];
int pipeCount = 0;
// 将命令按照管道符号 "|" 分割成多个子命令
char *token = strtok(command, "|");
while (token != NULL && pipeCount < MAX_PIPES)
{
pipes[pipeCount] = token;
token = strtok(NULL, "|");
pipeCount++;
}
// 创建管道
int pipefds[2 * pipeCount];
for (int i = 0; i < pipeCount; i++)
{
if (pipe(pipefds + 2 * i) < 0)
{
perror("无法创建管道");
exit(1);
}
}
// 执行子命令
for (int i = 0; i < pipeCount; i++)
{
pid_t pid = fork();
if (pid < 0)
{
// fork() 出错
perror("无法创建子进程");
exit(1);
}
else if (pid == 0)
{
// 子进程
// 如果不是第一个子命令,则将管道的输入重定向到上一个子命令的输出
if (i > 0)
{
if (dup2(pipefds[2 * (i - 1)], STDIN_FILENO) < 0)
{
perror("无法重定向输入");
exit(1);
}
}
// 如果不是最后一个子命令,则将管道的输出重定向到下一个子命令的输入
if (i < pipeCount - 1)
{
if (dup2(pipefds[2 * i + 1], STDOUT_FILENO) < 0)
{
perror("无法重定向输出");
exit(1);
}
}
// 关闭所有管道文件描述符
for (int j = 0; j < 2 * pipeCount; j++)
{
close(pipefds[j]);
}
// 执行子命令
executeExternalCommand(pipes[i]);
exit(0);
}
}
// 关闭所有管道文件描述符
for (int i = 0; i < 2 * pipeCount; i++)
{
close(pipefds[i]);
}
// 等待所有子进程结束
for (int i = 0; i < pipeCount; i++)
{
int status;
wait(&status);
}
}
int main()
{
getcwd(current_directory, sizeof(current_directory)); // 获取当前工作目录
while (1)
{
// 显示命令行提示符
print_prompt();
//读取命令
read_line();
// 处理命令
parse_cmd();
}
return 0;
}
运行的结果大家自行试验就行了,我就不展示了。
至此,我们的实验大功告成!如果大家有什么想法,可以在评论区提出,一起交流。