实验题目

Project 1 — UNIX Shell和历史记录

该项目包括设计一个 C 程序作为一个 shell 接口,它接受用户命令,然后在单独的进程中执行每个命令。这个项目可以在任何 Linux、UNIX 或 Mac OS X 系统上完成。
shell 界面给用户一个提示,然后输入下一个命令。下面的示例说明了提示符 osh> 和用户的下一个命令:cat prog.c 。 (此命令使用 UNIX cat 命令在终端上显示文件 prog.c。)

          `cosh> cat prog.c

`

实现 shell 接口的一种技术是让父进程首先读取用户在命令行中输入的内容(在本例中为 cat prog.c),然后创建一个单独的子进程来执行该命令。除非另有说明,否则父进程在继续之前等待子进程退出。这在功能上类似于图 3.10 中所示的新流程创建。但是,UNIX shell 通常还允许子进程在后台或同时运行。为此,我们在命令末尾添加一个与号 (&)。因此,如果我们将上面的命令重写为

    `osh> cat prog.c &   `       父进程和子进程将同时运行。

单独的子进程是使用 fork() 系统调用创建的,用户的命令是使用系统调用 exec() 系列之一执行的(如第 3.3.1 节所述)。

用户在 osh> 提示符下输入命令 ps -ael,存储在 args 数组中的值是:
这个 args 数组将被传递给 execvp() 函数,它具有以下原型:

      `execvp (char *command, char *params[])` 

这里,command 表示要执行的命令,params 存储此命令的参数。对于这个项目,execvp() 函数应该被调用为 execvp(args [0], args)。请务必检查用户是否包含 & 以确定父进程是否要等待子进程退出。

创建历史记录

下一个任务是修改 shell 接口程序,使其提供history功能,允许用户访问最近输入的命令。通过使用该功能,用户最多可以访问 10 个命令。命令会从1开始连续编号,超过10会继续编号。例如,如果用户输入了35条命令,则最近的10条命令将编号为26到35。

  1. 当用户输入 !! 执行历史记录中的最新命令。
  2. 当用户输入单 ! 后跟整数 N,执行历史记录中的第 N 个命令。

    错误处理

    该程序还应该管理基本的错误处理。
  • 如果历史中没有命令,输入 ! !应该会产生一条消息“No commands in history”。
  • 如果没有与用单个 ! 输入的数字对应的命令,程序应该输出“No such command in history”

相关原理与知识

  • 进程的概念
    进程是操作系统中最基本、重要的概念。
    多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律而引进的一个概念。所有的多道程序操作系统都建立在进程的基础上。
    操作系统引入进程概念的原因:
    • 从理论角度看,是对正在运行的程序过程的抽象;
    • 从实现角度看,是一种数据结构,目的在于清晰地刻划动态系统的内在规律,有效管理和调度进入计算机系统处理器运行的程序。
  • 进程的切换
    从正在运行的进程中收回处理器,再调度待运行进程(ready queue)来占用处理器
  • Linux下进程的结构
    在Linux操作系统中,进程在内存里有三部分的数据:“数据段”、”堆栈段”和“代码段”。

    • 代码段:存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。
    • 堆栈段:存放子程序的返回地址、子程序的参数以及程序的局部变量。
    • 数据段:存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。
  • fork()函数
    在Linux下产生子进程的系统调用。

  • 如何启动另一程序的执行
    在Linux中,使用exec类的函数。
    exec类的函数不止一个,分别是:execl,execlp,execle,execv,execve和execvp
    如果当前程序想启动另一程序的执行,但自己仍想继续运行的话,结合fork与exec的使用。

    实验过程

  1. 编写一个C程序作为shell接口
    • 引入相关库和初始化变量
      定义常量MAX_HISTORY来表示存储命令的最大个数为10个。
      定义常量MAX_COMMAND_LENGTH表示输入命令长度最长为256个字符。
      定义指针数组*history[MAX_HISTORY]来存储历史命令且最多存储10个命令。
      定义变量history_count来记录历史命令的个数。
      定义变量command_number来记录命令的编号。
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_HISTORY 10
#define MAX_COMMAND_LENGTH 256

char *history[MAX_HISTORY]; // 存储历史命令
int history_count = 0; // 历史命令数量
int command_number = 1; // 命令连续编号
  1. 添加命令历史记录函数
    函数首先进行条件判断,如果数组存储的命令数量已经达到了10个,则释放掉最先存储的命令并利用for循环将其余存储的命令在数组中左移。
    函数利用strup函数复制用户的命令并储存,strup函数返回值是一个指向新分配内存的指针,该内存中存储了用户输入命令的副本。
    每次调用该函数都将记录命令编号的command_number增加。
1
2
3
4
5
6
7
8
9
10
11
12
// 添加命令到历史记录 
void add_to_history(char *command) {
if (history_count >= MAX_HISTORY) {
free(history[0]); // 释放最早的命令
for (int i = 1; i < MAX_HISTORY; i++) {
history[i - 1] = history[i]; // 移动命令
}
history_count--;
}
history[history_count++] = strdup(command); // 复制命令并存储
command_number++; // 命令编号递增
}
  1. 执行用户命令函数
    函数首先首先判断用户命令是否以&符号结尾,并用变量background进行标记。
    strtok 函数会查找 command 中第一个分隔符的位置,然后将该位置之前的部分(即命令或参数)存储在字符指针数组args的第一位。在 while 循环中,strtok 的第一个参数被设置为 NULL,这意味着 strtok 将继续在上一次调用时处理的字符串中寻找下一个分隔符的位置,并将该位置之前的部分(即命令的各个参数)存储进数组中。参数列表(args数组)以 NULL 结尾。
    使用fork函数创建子进程,将函数返回值赋予变量pid:
    • Pid = 0
      表示当前代码运行在子进程中,通过函数execvp执行用户命令,如果执行成功则进程结束,否则通过perror函数输出错误信息并调用exit(1)结束子进程。
    • Pid > 0
      程序处在父进程中,对background值进行判断,如果用户命令以&符号结尾,则父进程不会等待子进程,反之则调用wait(NULL)等待子进程。
    • Pid < 0
      如果 fork() 返回 -1,表示创建子进程失败,父进程会通过 perror(“fork”) 输出错误信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 执行用户输入的命令 
void execute_command(char *command) {
int background = 0; // 后台执行标志

// 检查是否包含 &,并移除
if (command[strlen(command) - 1] == '&') {
command[strlen(command) - 1] = '\0'; // 移除 &
background = 1;
}

// 解析命令和参数
char *args[256];
int i = 0;
args[i++] = strtok(command, " ");
while ((args[i] = strtok(NULL, " ")) != NULL) {
i++;
}
args[i] = NULL; // 参数列表以 NULL 结尾

// 创建子进程执行命令
pid_t pid = fork();
if (pid == 0) {
// 子进程
execvp(args[0], args);
perror("execvp"); // 如果 execvp 失败
exit(1);
} else if (pid > 0) {
// 父进程
if (!background) {
wait(NULL); // 非后台执行,等待子进程
}
} else {
perror("fork"); // fork 失败
}
}
  1. 处理历史命令函数
    使用strcmp函数来比较用户输入的命令是否为“!!”。
    如果是“!!”,进一步检查history_count是否为0,即历史命令记录中是否有命令。
    如果没有历史命令(history_count == 0),则打印”No commands in history”并返回,不再执行后续操作。
    如果有历史命令,则调用execute_command函数来执行历史记录的最后一条命令,即history[history_count - 1]。
    如果用户输入的命令不是“!!”,但以“!”开头,这表明用户想要执行特定编号的历史命令。
    使用atoi函数将用户输入的命令编号部分转换为整数n。
    通过命令连续编号command_number减去历史命令计数history_count得到计历史记录中最早命令的编号start_number。
    检查编号n是否在历史命令编号范围内(即start_number到command_number-1之间)。如果不在范围内,则打印”No such command in history”并返回。
    如果编号有效,则调用execute_command函数来执行对应编号的历史命令。。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 处理历史命令(!! 和 !N)
void handle_history_command(char *input) {
if (strcmp(input, "!!") == 0) {
if (history_count == 0) {
printf("No commands in history\n");
return;
}
execute_command(history[history_count - 1]); // 执行最新命令
} else if (input[0] == '!') {
int n = atoi(input + 1); // 提取命令编号
int start_number = command_number - history_count; // 当前历史记录中最早的命令编号
if (n < start_number || n >= command_number) {
printf("No such command in history\n");
return;
}
execute_command(history[n - start_number]); // 执行第 N 条命令
}
}
  1. 主函数
    1) 读取用户命令
    使用while(1)创建一个无限循环,程序将在此循环中持续读取用户输入的命令。输出提示符osh>,表示程序正在等待用户输入命令。
    Fgets函数从标准输入(通常是键盘)读取一行字符,并将其存储在input数组中。sizeof(input)确保读取的字符数不超过数组的最大长度。
    input[strcspn(input, “\n”)] = 0;用于去除用户输入命令后可能存在的换行符\n。strcspn函数返回字符串input中第一个换行符的位置,将其替换为字符串结束符\0。
    2) 处理用户命令
    检查用户输入的第一个字符是否为!,如果是,则调用handle_history_command函数处理历史命令。
    如果输入的命令不是以!开头的普通命令,则首先调用add_to_history函数将命令添加到历史记录中,随后调用execute_command函数来执行该命令。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 主函数 
int main() {
char input[MAX_COMMAND_LENGTH];

while (1) {
printf("osh> "); // 提示符
fgets(input, sizeof(input), stdin);
input[strcspn(input, "\n")] = 0; // 移除换行符

if (input[0] == '!') {
handle_history_command(input); // 处理历史命令
} else {
add_to_history(input); // 添加到历史记录
execute_command(input); // 执行命令
}
}

// 释放历史记录内存
for (int i = 0; i < history_count; i++) {
free(history[i]);
}

return 0;
}

实验结果与分析

  • Shell界面给出提示,用户输入命令
    Shell界面给出提示,用户输入命令

  • 用户输入&符号,父进程和子进程同时运行
    用户输入&符号,父进程和子进程同时运行

  • 调用历史命令
    在这里插入图片描述

  • 基本的错误处理
    • 无历史命令可调用
      无历史命令可调用
    • 调用的命令不是最近10条
      调用的命令不是最近10条

问题总结

  1. 问题:用户输入的&符号被程序当做要执行命令的一部分。
    方案:在处理命令时先判断命令是否以&符号结尾,用标记变量标记后删除命令结尾的&符号。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_HISTORY 10
#define MAX_COMMAND_LENGTH 256

char *history[MAX_HISTORY]; // 存储历史命令
int history_count = 0; // 历史命令数量
int command_number = 1; // 命令连续编号

// 添加命令到历史记录
void add_to_history(char *command) {
if (history_count >= MAX_HISTORY) {
free(history[0]); // 释放最早的命令
for (int i = 1; i < MAX_HISTORY; i++) {
history[i - 1] = history[i]; // 移动命令
}
history_count--;
}
history[history_count++] = strdup(command); // 复制命令并存储
command_number++; // 命令编号递增
}

// 执行用户输入的命令
void execute_command(char *command) {
int background = 0; // 后台执行标志

// 检查是否包含 &,并移除
if (command[strlen(command) - 1] == '&') {
command[strlen(command) - 1] = '\0'; // 移除 &
background = 1;
}

// 解析命令和参数
char *args[256];
int i = 0;
args[i++] = strtok(command, " ");
while ((args[i] = strtok(NULL, " ")) != NULL) {
i++;
}
args[i] = NULL; // 参数列表以 NULL 结尾

// 创建子进程执行命令
pid_t pid = fork();
if (pid == 0) {
// 子进程
execvp(args[0], args);
perror("execvp"); // 如果 execvp 失败
exit(1);
} else if (pid > 0) {
// 父进程
if (!background) {
wait(NULL); // 非后台执行,等待子进程
}
} else {
perror("fork"); // fork 失败
}
}

// 处理历史命令(!! 和 !N)
void handle_history_command(char *input) {
if (strcmp(input, "!!") == 0) {
if (history_count == 0) {
printf("No commands in history\n");
return;
}
execute_command(history[history_count - 1]); // 执行最新命令
} else if (input[0] == '!') {
int n = atoi(input + 1); // 提取命令编号
int start_number = command_number - history_count; // 当前历史记录中最早的命令编号
if (n < start_number || n >= command_number) {
printf("No such command in history\n");
return;
}
execute_command(history[n - start_number]); // 执行第 N 条命令
}
}

// 主函数
int main() {
char input[MAX_COMMAND_LENGTH];

while (1) {
printf("osh> "); // 提示符
fgets(input, sizeof(input), stdin);
input[strcspn(input, "\n")] = 0; // 移除换行符

if (input[0] == '!') {
handle_history_command(input); // 处理历史命令
} else {
add_to_history(input); // 添加到历史记录
execute_command(input); // 执行命令
}
}

// 释放历史记录内存
for (int i = 0; i < history_count; i++) {
free(history[i]);
}

return 0;
}