C 语言实现简单 shell:Getting Deep
楔子
最近在读《操作系统导论》的进程部分。为了熟悉操作系统的进程 API(至少基础的那几个),我想到的最好练习方式是写个简单的 shell —— Charles's First Shell,cfsh。
我一直认为在计算机科学中,越底层越高级;我想起同济大学的同学们大一的计算机通识课是用conio.h来写命令行菜单,惊为天人。不过扯远了,既然是 OS 练习,那么语言肯定是 C。So, this is getting deep and dark.
测试
C
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
int main(void) {
char input[1024]; // 输入缓冲区
char *token, *ptr; // strtok()想要的
char *args[20]; // 允许20个参数
char err_msg[256] = ""; // 错误信息
int status, sig, ret; // 子进程的返回值
signal(SIGINT, SIG_IGN); // Shell不受Ctrl+C影响
while (printf("cfsh%s> ", err_msg), fgets(input, sizeof(input), stdin)) {
// C中的逗号表达式:顺序执行,取最后一项为值
err_msg[0] = '\0';
// 逻辑删除一个“字符串”:将其首位置为'\0'
char *newline = strrchr(input, '\n');
// fgets会读取换行符,需要strip;strrchr()逆向匹配,返回指向第一个匹配字符的指针
if (newline != NULL) {
*newline = '\0';
} else {
return -1;
}
int child = fork();
// 从这里开始分裂
if (child < 0) { // 父进程的fork()返回值小于零则表示失败
perror("Unable to fork()");
} else if (child == 0) { // 子进程的fork()返回值是0
signal(SIGINT, SIG_DFL); // 子进程接收到Ctrl+C应该停止
int i;
for (i = 0, ptr = input; ; i++, ptr = NULL) {
// 分词器,注意不能写int i = 0, ptr = input;这会让编译器认为ptr是int
token = strtok(ptr, " ");
// strtok()维持了一个全局指针,所以它不能嵌套使用,否则全局指针会被覆盖
if (token == NULL) {
args[i] = NULL;
break;
}
args[i] = token;
}
execvp(args[0], args);
// 执行,正常的exec()调用从不返回
perror("Unable to execvp()");
exit(127);
} else { // 父进程的fork()正常返回值是子进程的PID
waitpid(child, &status, 0);
// status用于从僵尸子进程的PCB获取信息;wait()之后,子进程彻底灰飞烟灭
if (WIFSIGNALED(status)) {
// 一点对现阶段学习意义不大的宏
sig = WTERMSIG(status);
snprintf(err_msg, sizeof(err_msg), " [%s]", strsignal(sig));
printf("cfsh: child received signal %d\n", sig);
} else if (WIFEXITED(status)) {
ret = WEXITSTATUS(status);
if (ret != 0) snprintf(err_msg, sizeof(err_msg), " [%d]", ret);
}
}
}
return 0;
}
这个版本定死了缓冲区大小,更重要的是不支持参数传入(main(void)),也没有任何内置函数和管道支持。不过对于熟悉几个基础 UNIX 进程 API 来说是足够了的:
Plaintext
charles@Workspace \~/W/ostep-exercise> ./cfsh
cfsh> ls -la
total 32
drwxrwxr-x 2 charles charles 4096 Sep 3 19:10 .
drwxrwxr-x 18 charles charles 4096 Sep 3 10:49 ..
-rwxrwxr-x 1 charles charles 16576 Sep 3 19:10 cfsh
-rw-rw-r-- 1 charles charles 1556 Sep 3 19:10 cfsh.c
cfsh> echo hello world
hello world
cfsh> hi
Unable to execvp(): No such file or directory
cfsh [127]> sleep 20
^C
cfsh [Interrupt]>也许日后会补一下管道实现……先立个 flag。但 408 对于实操这一块并不看重,所以我大概率不会搞——不如先把处理机调度和 P、V 操作的知识点看看。
从 Go 转向 C 确实会有各种不适应,尤其是在和底层数据结构打交道时;但我想总是可以 get through 的。
Forget about language fads now
Like templates and Haskell’s “do”,
Because C has been the future,
Since 1972!
—— John Wickerson, Write it in C!