操作系统综合实验(二)——多进程编程
实验题目
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。
- 如果历史中没有命令,输入 ! !应该会产生一条消息“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的使用。实验过程
- 编写一个C程序作为shell接口
- 引入相关库和初始化变量
定义常量MAX_HISTORY来表示存储命令的最大个数为10个。
定义常量MAX_COMMAND_LENGTH表示输入命令长度最长为256个字符。
定义指针数组*history[MAX_HISTORY]来存储历史命令且最多存储10个命令。
定义变量history_count来记录历史命令的个数。
定义变量command_number来记录命令的编号。
- 引入相关库和初始化变量
1 |
|
- 添加命令历史记录函数
函数首先进行条件判断,如果数组存储的命令数量已经达到了10个,则释放掉最先存储的命令并利用for循环将其余存储的命令在数组中左移。
函数利用strup函数复制用户的命令并储存,strup函数返回值是一个指向新分配内存的指针,该内存中存储了用户输入命令的副本。
每次调用该函数都将记录命令编号的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”) 输出错误信息。
- Pid = 0
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 | // 处理历史命令(!! 和 !N) |
- 主函数
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 | // 主函数 |
实验结果与分析
Shell界面给出提示,用户输入命令

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

调用历史命令

- 基本的错误处理
- 无历史命令可调用

- 调用的命令不是最近10条

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





