Linux 0.11-操作系统启动完毕-39

本文详细解析了操作系统启动后shell程序的执行过程,包括登录画面的作用、shell作为用户程序的角色、以及/etc/rc文件的配置功能。深入探讨了shell如何通过fork和execve管理用户命令的执行。
摘要由CSDN通过智能技术生成

Linux 0.11-操作系统启动完毕-39


操作系统启动完毕

书接上回,上回书咱们说到一个 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 程序。

// 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 进程退出后,就会继续往下走。

// main.c
void init(void) {
    ...
    // 一个以 rc 为标准输入的 shell
    ...
    // 等待这个 shell 结束
    if (pid>0)
        while (pid != wait(&i))
    ...
    // 大的死循环,不再退出了
    while (1) {
        ...
    }
}

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

// 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 进程也退出了,那么操作系统也不会跳出这个大循环,而是继续重试。

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

// main.c
void main() {
    // 初始化环境
    ...
    // 外层操作系统大循环
    while(1) {
        // 内层 shell 程序小循环
        while(1) {
            // 读取命令 read
            ...
            // 创建进程 fork
            ...
            // 执行命令 execve
            ...
        }
    }
}

当然,这只是表层的。

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

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

完美!

欲知后事如何,且听下回分解。


转载

本文转载至闪客图解操作系统系列文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值