环境
选项 | 参数 |
---|---|
系统 | Windows 10 下 VMware Workstation 虚拟机 ubuntu 14.04.6 desktop i386 |
GCC |
gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.4) |
功能
- 解析并执行用户提交的命令行
- 提供
ls
、mkdir
、rmdir
、pwd
、ps
等内部命令(多进程并使用 execvp 函数调用) - 最后一个参数为
&
时,命令后台执行,同时父进程应当等待子进程 - 提供历史查询功能:当用户按下
Ctrl+C
时输出最近的 10 个命令行 - 执行历史命令行:当用户输入
r x
时,执行首字母为x
的最近的命令行,如果没有参数则执行最近的命令 - 用户输入
Ctrl+D
时退出程序
步骤
Ctrl+C
实验要求在用户按下 Ctrl+C
时,不退出程序,而改为输出最近输入的 10
条命令,首先需要捕获,然后重新定义它的功能。
void my_handle(int sig);
int main()
{
signal(SIGINT, my_handle);
/* ... */
}
void my_handle(int sig)
{
int i, j;
printf("\nCaught Control C\n");
if (idx == -1)
printf("Never entered a command.\n");
else
for (i = 0; i < MAX_HISTORY; ++i)
if (!history[j = (idx - i) < 0 ? idx - i + MAX_HISTORY : idx - i][0])
break;
else
printf("%2d: %s\n", i + 1, history[j]);
}
发现 Ctrl+C
的确变成了输出最近输入的 10
条命令,但是在输出完之后依旧会退出,如下图
最后发现 Ctrl+C
每次发出 SIGINT
信号,但是在 SIGINT
信号处理完之后,其处理函数会重置,不再是 my_handle
,所以在每次调用 my_handle
时都需要重新设置,作如下更改:
void my_handle(int sig)
{
signal(SIGINT, my_handle);
/* ... */
}
更改完之后如下
发现缺少 COMMAND->
提示,重新把它加上。
Ctrl+D
实验要求在用户按下 Ctrl+D
时退出程序,由于 Ctrl+D
并非如 Ctrl+C
一般发送一个信号,而是作为一个特殊的字符。在键盘缓冲区内有数据时, Ctrl+D
会刷新缓冲区,使程序能读取到这些字符;在键盘缓冲区内没有字符时,Ctrl+D
会向程序发送 EOF
,使用 getc
等函数读取时会返回 -1
,所以需要自行实现 readline
类的输入函数并判断是否键入 Ctrl+D
。
输入并解析
由于存储的历史记录为完整的命令,除了在输入之后需要解析之外,在使用 r x
命令时也需要再次解析,所以选择将解析的功能剥离出 setup
。
首先使用 getc
函数逐个读取字符,直接使用 scanf
等函数不易判断 EOF
:
/**
* 读取命令并调用 parse_command 解析命令
*
* @param buffer 输入的命令
* @param args 存放解析完的参数
* @param background 为 1 时表示后台执行
**/
void setup(char buffer[], char* args[], int* background)
{
/**
* i 表示命令中当前字符的下标
* j 表示参数个数
* k 用于查找历史记录
**/
int i, j, k;
// 读取命令
i = 0;
do {
// 读取一字符
buffer[i] = getc(stdin);
// 捕获 Ctrl+D
if (buffer[i] == EOF) {
printf("\nCaught Control D\n");
exit(0);
}
} while (buffer[i++] != '\n');
// 将 '\n' 结尾的字符串替换为 '\0' 结尾的标准字符串
buffer[--i] = '\0';
// 解析命令
j = parse_command(buffer, args);
/* 后续处理 */
}
对命令行进行解析,以空白符为分隔:
/**
* 解析命令
*
* @param buffer 输入的命令
* @param args 存放解析完的参数
*
* @return 参数个数
**/
int parse_command(char buffer[], char* args[])
{
/**
* i 表示命令中当前字符的下标
* j 表示命令中当前参数的下标
* k 表示命令中当前参数的长度
* c 用于读取的字符并处理
**/
int i, j, k;
char c;
// 指针置 NULL 但不释放
i = 0;
while (args[i])
args[i++] = NULL;
// 分割参数
j = k = 0;
for (i = 0; i < MAX_SIZE; ++i)
if (isspace(buffer[i]) || !buffer[i]) {
if (i && !isspace(buffer[i - 1])) {
/**
* 分配空间并拷贝字符串
* k + 1 保留 '\0' 的空间
**/
args[j] = malloc(sizeof(char) * (k + 1));
// 标识字符串末尾
c = buffer[i];
buffer[i] = '\0';
// 拷贝参数
strcpy(args[j++], buffer + i - k);
buffer[i] = c;
k = 0;
}
if (!buffer[i]) break;
} else ++k;
// 返回参数个数
return j;
}
由于 r x
命令出现在历史记录中可能导致产生命令死循环,所以禁止其加入历史记录:
void setup(char buffer[], char* args[], int* background)
{
/* 输入命令 */
// 解析命令
j = parse_command(buffer, args);
// 如果输入了一条非空命令
if (j) {
if (strcmp("r", args[0]))
// 保存命令,FIFO
strcpy(history[idx = (idx + 1) % MAX_HISTORY], buffer);
else if (idx == -1)
// 没有历史记录
printf("Never entered a command.\n");
else {
k = idx;
// 执行历史中记录的命令
if (args[1] && args[1][0])
// 查找命令
for (i = 0; i < MAX_HISTORY; ++i)
if (args[1][0] == history[k = (idx - i) < 0 ? idx - i + MAX_HISTORY : idx - i][0])
break;
// 释放空间
i = 0;
while (args[i])
free(args[i++]);
//解析命令
j = 0;
if (k == MAX_HISTORY)
printf("Command not found.\n");
else
j = parse_command(history[k], args);
}
// 后台执行
if (j && !strcmp("&", args[j - 1])) {
// 设置 background 值
*background = 1;
--j;
}
}
// 以 NULL 标识参数的结束
args[j] = NULL;
}
main
函数,实现整体功能:
int main()
{
char buffer[MAX_SIZE];
/**
* 存储参数
*
* (MAX_SIZE + 1) / 2 保证能存储所有参数
* 1 保证有多余的空位存储 NULL 以标识参数的结尾
**/
char* args[(MAX_SIZE + 1) / 2 + 1];
int background, i;
pid_t pid;
// 捕获 Ctrl+C
signal(SIGINT, my_handle);
memset(history, 0, sizeof(history));
while (1) {
background = 0;
printf("COMMAND-> ");
// 读取并解析命令
setup(buffer, args, &background);
pid = fork();
if (pid == -1) {
fprintf(stderr, "fork error\n");
// 释放空间
i = 0;
while (args[i])
free(args[i++]);
} else if (pid == 0) {
execvp(args[0], args);
} else if (background) {
pid = wait(NULL);
printf("[parent] child process %d done\n", pid);
}
}
}
父进程等待
测试中发现功能基本已经完善,但是在输入后台命令时,只有执行第一条后台命令时父进程会等待子进程,之后的命令父进程都不会等待,如下图
分析 wait
函数发现只要有一个子进程完成,wait
函数就会返回,所以父进程不会等待之后的子进程。
将 wait
函数更改为 waitpid
,等待指定的子进程:
pid = waitpid(pid, NULL, 0);
最终结果如下
至此完成所有功能。
实验总结
本次实验主要学习了如何使用多进程来完成工作,因为 execvp
会替换当前进程,所以如果不使用多进程将会导致每次执行只能解析运行一条指令。同时了解了如何设置专门的信号处理函数,信号在编程中如何使用,也了解到了 Ctrl+C
与 Ctrl+D
的区别。
个人博客:https://wilfredshen.cn/