系列目录
- 序篇
- 准备工作
- BIOS 启动到实模式
- GDT 与保护模式
- 虚拟内存初探
- 加载并进入 kernel
- 显示与打印
- 全局描述符表 GDT
- 中断处理
- 虚拟内存完善
- 实现堆和 malloc
- 第一个内核线程
- 多线程运行与切换
- 锁与多线程同步
- 进入用户态
- 进程的实现
- 系统调用
- 简单的文件系统
- 加载可执行程序
- 键盘驱动
- 运行 shell
shell 命令行
这是本系列最后一篇了,为这个 OS 加一个用户界面 shell,这算是 Linux 编程中最入门的经典教科书项目了,网上也可以找到很多小教程。这里也不多浪费时间,仅展示一下它的核心部分:
void print_shell() {
printf("bash> ");
}
while (1) {
print_shell();
while (1) {
int32 c = read_char();
if (c == '\n') {
run_program();
break;
} else if (c < 128) {
printf(c);
}
}
shell 本质上只是一个壳,正如它的名字,它提供一个和用户交互的命令行界面,不停地等待用户输入字符并反馈打印出来;一旦用户按下了回车键,那么表示需要运行之前输入的命令行,这在 run_program 函数里实现:
void run_program() {
// Parse cmd and get program and args.
// ..
// (fork + exec) new prgoram.
int32 pid = fork();
if (pid < 0) {
printf("fork failed");
} else if (pid > 0) {
// parent
int32 status;
wait(pid, &status);
} else {
// child
int32 ret = exec(program, args_index, (char**)args);
exit(ret);
}
}
这里首先 parse 用户刚才敲回车之前输入的命令行字符串,解析出可执行程序名,以及参数。然后就是经典的 fork + exec
组合,运行这个程序。程序名和参数都会被传递到 exec
系统调用的处理函数 process_exec,那里会从磁盘上读取该用户可执行文件并执行。这里命令行输入的程序名都很简单,也没有什么路径的概念,因为我们使用的 naive_fs 只有一层结构,所有文件全在顶层,所以直接用文件名就可以了。
fork 后的 parent 进程会调用 wait 系统调用阻塞等待 child 结束。关于 wait 和 exit 这组系统调用,我没有在这个系列里详细展开,读者可以自行阅读源码。
kernel 启动任务
我们来看一下这个 kernel 启动的过程,后台开启了哪些任务,以及如何最终进入 shell 界面。本节代码在 中。
首先启动 kernel main 进程/线程,它是最原始的祖先进程,会做这几件事情:
- 创建 kernel 资源清理线程 kernel_clean_thread,这是一个后台线程,我用它专门做 process/thread 的资源最终回收工作,平时它是睡眠的,只有当有 process/thread 消亡需要清理时会唤醒;
- 创建 init 进程以即线程 kernel_init_thread,它会成为第一个用户进程,运行用户程序 init;在
init
程序里,我创建了shell
进程,然后init
进程就进入阻塞;在实际的 Linux 系统中,真实的init
进程应该还需要作为一个后台任务,专门负责等待接管回收所有的孤儿进程(Orphan Process
),我这里就不实现了,感兴趣的同学可以查资料学习一下; - 上述两项工作完成后,这个原始线程就变成了 cpu_idle 线程,所谓 cpu idle 就是一条指令
hlt
,它是在系统真的没有任何任务需要运行的情况才会被运行,它会使 CPU 进入一个低功耗运转的状态;
当然以上只是我个人启动 kernel 任务的实现方式,和 Linux 有点像但并不完全一致;这其实很随意的,这毕竟只是我们自己写的一个玩具 OS 而已,Linux 的方式也并非标准答案,只需要让系统各个重要的任务成功运行并调度起来就可以了。