38.操作系统启动完毕

38.操作系统启动完毕

书接上回,上回书咱们说到一个 shell 程序的执行原理,至此我们的操作系统终于将控制权转交给了 shell,由 shell 程序和我们人类进行友好的交互。

其实到这里,操作系统的使命就基本结束了。

此时我想到了之前有人问过我的一个问题,他说为什么现在的电脑开机后和操作系统启动前,还隔着好长一段时间,这段时间运行的代码是什么?

在我的继续追问下才知道,他说的操作系统的开始部分,是我们看到了诸如 Windows 登陆画面的时候。

图片

这个登陆画面就和我们 Linux 0.11 里讲的这个 shell 程序一样,已经可以说标志着操作系统启动完毕了,通过 shell 不断接受用户命令并执行命令的死循环过程中。

甚至在 Linux 0.11 里根本都找不到 shell 的源代码,说明 Linux 0.11 并没有认为 shell 是操作系统的一部分,它只是个普通的用户程序,和你在操作系统里自己写个 hello world 编译成 a.out 执行一样。在执行这个 shell 程序前已经可以认为操作系统启动完毕了。

操作系统就是初始化了一堆数据结构进行管理,并且提供了一揽子系统调用接口供上层的应用程序调用,仅此而已。再多做点事就是提供一些常用的用户程序,但这不是必须的。

OK,上一回我留了一个问题,shell 程序执行了,操作系统就结束了么?

此时我们不妨从宏观视角来看一下当前的进度。

图片

看最右边的蓝色部分的流程即可。

我们先是建立了操作系统的一些最基本的环境与管理结构,然后由进程 0 fork 出处于用户态执行的进程 1,进程 1 加载了文件系统并打开终端文件,紧接着就 fork 出了进程 2,进程 2 通过我们刚刚讲述的 execve 函数将自己替换成了 shell 程序。

如果看代码的话,其实我们此时处于一个以 rc 为标准输入的 shell 程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// main.c
void main(void) {    
    ...    
    if (!fork()) {        
        init();    
    }    
    for(;;) 
        pause();
}
void init(void) {    
    ...    
    // 一个以 rc 为标准输入的 shell    
    if (!(pid=fork())) {        
        ...        
        open("/etc/rc",O_RDONLY,0);        
        execve("/bin/sh",argv_rc,envp_rc);    
    }    
    // 等待这个 shell 结束    
    if (pid>0)        
        while (pid != wait(&i))    
            ...    
    // 大的死循环,不再退出了    
    while (1) {        
        // 一个以 tty0 终端为标准输入的 shell        
        if (!(pid=fork())) {            
            ...            
            (void) open("/dev/tty0",O_RDWR,0);            
            execve("/bin/sh",argv,envp);        
        }        
        // 这个 shell 退出了继续进大的死循环        
        while (1)            
            if (pid == wait(&i))                
                break;        
        ...    
    }
}

就是 open 了 /etc/rc, 然后 execve 了 /bin/sh 的这个程序,代码中标记为蓝色的部分。

shell 程序有个特点,就是如果标准输入为一个普通文件,比如 /etc/rc,那么文件读取后就会使得 shell 进程退出,

如果是字符设备文件,比如由我们键盘输入的 /dev/tty0,则不会使 shell 进程退出。

这就使得标准输入为 /etc/rc 文件的 shell 进程在读取完 /etc/rc 这个文件并执行这个文件里的命令后,就退出了。

所以,这个 /etc/rc 文件可以写一些你觉得在正式启动大死循环的 shell 程序之前,要做的一些事,比如启动一个登陆程序,让用户输入用户名和密码。

好了,那作为这个 shell 程序的父进程,也就是进程 0,在检测到 shell 进程退出后,就会继续往下走。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// main.c
void init(void) {    
    ...    
    // 一个以 rc 为标准输入的 shell    
    ...    
    // 等待这个 shell 结束    
    if (pid>0)        
        while (pid != wait(&i))    
    ...    
    // 大的死循环,不再退出了    
    while (1) {        
        ...    
    }
}

下面的 while(1) 死循环里,是和创建第一个 shell 进程的代码几乎一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// main.c
void init(void) {    
    ...    
    // 大的死循环,不再退出了    
    while (1) {        
        // 一个以 tty0 终端为标准输入的 shell        
        if (!(pid=fork())) {            
            ...            
            (void) open("/dev/tty0",O_RDWR,0);            
            execve("/bin/sh",argv,envp);        
        }        
        // 这个 shell 退出了继续进大的死循环        
        while (1)            
            if (pid == wait(&i))                
                break;        
        ...    
    }
}

只不过它的标准输入被替换成了 tty0,也就是接受我们键盘的输入。

这个 shell 程序不会退出,它会不断接受我们键盘输入的命令,然后通过 fork+execve 函数执行我们的命令,这在上一回讲过了。

当然,如果这个 shell 进程也退出了,那么操作系统也不会跳出这个大循环,而是继续重试。

整个操作系统到此为止,看起来就是这个样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// main.c
void main() {    
    // 初始化环境    
    ...    
    // 外层操作系统大循环    
    while(1) {        
        // 内层 shell 程序小循环        
        while(1) {            
            // 读取命令 read            
            ...            
            // 创建进程 fork            
            ...            
            // 执行命令 execve            
            ...        
        }    
    }
}

当然,这只是表层的。

除此之外,这里所有的键盘输入、系统调用、进程调度,统统都需要中断来驱动,所以很久之前我说过,操作系统就是个中断驱动的死循环,就是这个道理。

OK!到此为止,操作系统终于启动完毕,达到了怠速的状态,它本身设置好了一堆中断处理程序,随时等待着中断的到来进行处理,同时它运行了一个 shell 程序用来接受我们普通用户的命令,以同人类友好的方式进行交互。

完美!

updatedupdated2024-05-152024-05-15