1

系列目录

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 界面。本节代码在 src/task/scheduler.c 中。

首先启动 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 的方式也并非标准答案,只需要让系统各个重要的任务成功运行并调度起来就可以了。


navi
612 声望191 粉丝

naive