一、实验题目
·设计一个 C 程序作为一个 shell 接口
它接受用户命令,然后在单独的进程中执行每个命令。这个项目可 以在任何 Linux、UNIX 或 Mac OS X 系统上完成。
shell 界面给用户一个提示,然后输入下一个命令。
示例:
osh> cat prog.c
提示符 osh> 和用户的下一个命令:
cat prog.c
实现 shell 接口的一种技术是让父进程首先读取用户在命令行中输入 的内容(在本例中为 cat prog.c),然后创建一个单独的子进程来执行该 命令。
UNIX shell 通常还允许子进程在后台或同时运行,我们在命令末尾 添加一个与号 (&)命令重写为
osh> cat prog.c &
父进程和子进程将同时运行。
用户在 osh> 提示符下输入命令 ps -ael,存储在 args 数组中的值是:
arg[0]=“ps”
arg[1]=“-ael”
arg[2]=“NULL”
这个 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()函数
它执行一次却返回两个值。一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的多进程方法。对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。
对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。
·如何启动另一程序的执行
在Linux中,使用exec类的函数。
exec类的函数不止一个,分别是:execl,execlp,execle,execv,execve和execvp
如果当前程序想启动另一程序的执行,但自己仍想继续运行的话,结合fork与exec的使用。
三、实验过程
·设计一个 C 程序作为一个 shell 接口
定义模块:
由要求里提到的“用户最多可以访问 10 个命令”,因此定义最大历史值为10。
定义history_count 来记录到目前为止用户所输入的所有命令个数。
定义数组history来记录具体的命令值。
另外。为了防止用户恶意以及检验命令的有效性,加入了命令最大 长度为100。
添加历史记录函数:
对主函数来说,循环一次执行一次,改变history_count 的值,使得 记录数增加。
对函数内部,分情况讨论:
- 当记录数小于最大记录数时,直接在当前index出存入 history 数组当中。
- 当记录数大于最大记录数时,先将所有记录前移,(将第10位 记录移动到第9位,以此类推),再将当前的命令存入第10位,实现总 记录数是10,当超过10会继续编号,如果用户输入了35条命令,则最 近的10条命令将编号为26到35。
主函数模块
·给出提示符号 osh>
·接受输入后,将换行符替换成结束符号\0后,传到add_to_history中,进行记录。
·定义信号变量,int run_in_background = 0;判断命令结尾是不是“&”, 如果有“&”将值改为1。再后续的程序中以此实现是否等待子进程结束, 实现父进程和子进程同时运行。
·接下来将输入,以“ ”为分割,存入arg数组当中,判断是否是”!!” 或者”!N”。
- 如果是”!!”,由history_count 的值判断是否有记录:
(1)如果有记录,找出上一记录输出,并且不改变arg的值,进入下一阶段直接用上一
循环的arg值再次执行fork()函数。
(2)如果没有记录,输出‘No commands in history’。
2. 如果是“!N”,判断对应的N是否存在,当N小于10的时候 直接在history中进行查找,取出命令。如果大于10,先对index进 行处理,使其对应到history的1-10的记录值上,取出命令。
(1) 如果有记录,取出命令后,更新arg的值,进入下一阶段, 再次执行fork()函
数。
(2) 如果没有记录,输出‘No such command in history’。
· 下一步进入创建进程模块,使用fork()函数创建进程,在子进程中执行execvp(),实现用户在命令行输入的命令。此时判断run_in_background 的值,决定是否要等待子进程结束。
四、实验结果与分析
·shell 界面给用户一个提示,然后输入下一个命令
1.输入cat 1.txt,实验现象如图所示
2.输入ls,实验现象如图
·父进程和子进程同时运行
输入cat 1.txt &,实验现象如图所示:
·创建历史记录
1、当用户输入 !! 执行历史记录中的最新命令,实验现象如图所示。
2、当用户输入单 ! 后跟整数 N,执行历史记录中的第 N 个命令,实验现象如图所示。(该例子中的输入是!11,读取第11条记录)
·基本的错误处理
1、如果历史中没有命令,输入 “!!”应该会产生一条消息“No commands in history”,实验现象如图所示。
2、如果没有与用单个 ! 输入的数字对应的命令,程序应该输出“No such command in history”。
(1)当输入值大于最大记录数,实验现象如图所示:
(2)当记录数很多,此时输入值不在最新的10条记录内,实验现象如图所示:
五、问题总结
1、挑战:如何实现history中只保存最新的10条数据。
方案:在小于10条记录时直接存入,当大于10条记录时,采用队列的 数据结构,将1位出列,所有位数前移,将最新的命令存在最大 位。
2、问题:在实现‘!!’和‘!N’时,发现没有输出,继续进行下一指令可 执行。
方案:分析代码后,发现是因为在每次循环,都直接将history_count加 1,将‘!!’和‘!N’也当做命令记录了,导致后面在history数 组 中寻找命令时,取出的命令时‘!!’和‘!N’,导致无法执 行,在函数内添加语句‘history_count--’解决。
3、问题:在实现‘!N’时,发现当N超过10,判断就会报错。
方案:分析代码后,发现是在记录历史记录时,将计数的history_count 只放在了当命令数小于记录数的条件下,造成只有在小于最大记 录数时,history_count才会计数。将history_count++移动到函数 初始端解决
4、问题:在实现‘!N’时,发现重复执行上一次的代码片段,虽然判断了 第N条命令是否存在,但是,执行的却是上一条指令。
方案:分析代码后,原因是没有更新arg数组,导致每次执行的语句中 arg的值是上一循环中的值。在‘!N’条件模块内,添加更新arg 数组的语句解决。
六、源代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX_COMMAND_LENGTH 100
#define MAX_HISTORY_SIZE 10
char history[MAX_HISTORY_SIZE][MAX_COMMAND_LENGTH];
int history_count = 0;
void add_to_history(char* command) {
history_count++;
if (history_count < MAX_HISTORY_SIZE) {
strcpy(history[history_count], command);
} else {
for (int i = 0; i < MAX_HISTORY_SIZE - 1; i++) {
strcpy(history[i], history[i + 1]);
}
strcpy(history[MAX_HISTORY_SIZE - 1], command);
}
}
void display_history() {
for (int i = 0; i < history_count; i++) {
printf("%d. %s\n", i + 1, history[i]);
}
}
int main() {
char input[MAX_COMMAND_LENGTH];
char* args[MAX_COMMAND_LENGTH];
char* token;
while (1) {
printf("osh> ");
fgets(input, MAX_COMMAND_LENGTH, stdin);
input[strcspn(input, "\n")] = 0; // Remove newline character
add_to_history(input);
// Check if command should run in the background
int run_in_background = 0;
if (input[strlen(input) - 1] == '&') {
run_in_background = 1;
input[strlen(input) - 1] = '\0'; // Remove the '&'
}
// Parse the input command
int i = 0;
token = strtok(input, " ");
while (token != NULL) {
args[i] = token;
token = strtok(NULL, " ");
i++;
}
args[i] = NULL;
f(strcmp(args[0], "#") == 0) {
break;
}
if (strcmp(args[0], "!!") == 0) {
history_count--;
if (history_count == 0) {
printf("No commands in history\n");
continue;
}
strcpy(input, history[history_count - 1]);
printf("Executing: %s\n", input);
} else if (args[0][0] == '!') {
history_count--;
int index = atoi(args[0] + 1);
if (index > 0 && index <= history_count && index > history_count - 10) {
if (history_count > 10)index = index - history_count + 10;
strcpy(input, history[index - 1]);
printf("Executing: %s\n", input);
token = strtok(input, " ");
while (token != NULL) {
args[i] = token;
token = strtok(NULL, " ");
i++;
}
args[i] = NULL;
} else {
printf("No such command in history\n");
continue;
}
}
pid_t pid = fork();
if (pid == 0) {
execvp(args[0], args);
exit(0);
} else {
if (!run_in_background) wait(NULL);
}
}
return 0;
}