学习

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!