运行hellow world操作系统都做了哪些工作

透彻理解操作系统最好的方法之一就是查看当一个最简单的程序在计算机上运行时,操作系统都做了些什么。

我们以一个C语言版的“”“hello world”为例:

#include 
void main()
{
    printf("hello world\n");
}

这段程序被编译、链接之后会生成一个可执行文件。我们在Linux操作系统上运行这个程序,最终会在屏幕上输出“hello world”。表面上看,在屏幕上显示的“hello world”都是我们写的程序的功劳,其实我们写的程序只起到了很小的作用。很明显的是,这个程序里使用了C语言的库函数printf。
那么操作系统都做了哪些工作呢?
上面的代码我们编译生成一个名叫hello的可执行文件。

第一步:用户输入命令,shell进程被唤醒,对命令进行解析。

为实现这一步,系统将至少进行如下准备工作。

(1)用户敲击键盘后,键入的信息记录在终端设备文件(tty0)上。

如果系统要以文件的形式对终端设备进行操作,首先要构建一套文件系统,之后加载该文件系统,以便在此基础上对文件进行操作。这套文件系统包括“超级块”、“逻辑块位图”、“i节点位图”、“文件i节点”、“数据块”等。其次,还要根据文件的不同功能对数据进行分类,包括普通文件、设备文件、目录文件等。tty0就属于设备文件。有了这些准备才有可能对终端设备文件tty0进行操作。

(2)敲击键盘后,还要产生键盘中断信号,系统要能够对键盘中断信号进行处理。

首先,这个中断信号会通过可编程中断控制器8259A,所以要对8259A这个中断控制器进行设置,然后,信号会被传达给CPU,CPU要通过中断描述符表寄存器(IDTR)找到内存中的中断描述符表,再通过搜索中断描述符表找到键盘中断处理程序,并执行该程序。要实现这些操作,就要构建一整套中断服务体系,其中包括中断描述符表寄存器(IDTR)进行设置和建立一个中断描述符表,用以和中断服务程序相挂接,然后还要编写中断服务程序,以便能够为具体的中断动作服务。此外,还要将这些中断服务程序与中断描述符表相挂接。

(3)中断服务程序开始执行后,唤醒shell进程,之后通过进程调度机制,由进程0切换到shell进程去执行。

这需要系统建立一整套进程管理机制。就shell来说,要创建进程并加载shell程序,这样才能构建人机交互界面;同时,还要创建一个进程0,并在其他进程都不处于就绪态时切换到进程0去执行,而且一旦有进程被唤醒,就又立即切换到该进程执行。这个机制要适用于操作系统中的所有进程。既然要支持多进程执行,就还要设计一套进程轮询机制,即产生时钟中断,导致进程切换。这个机制里面又有很多问题需要考虑,比如时钟中断服务程序的设计和8253定时器的设置,等等。

(4)shell进程通过执行自己的程序从tty0这个终端设备文件上读取用户键入的指令信息,然后解析该指令,并准备进行相应的处理。当然,这条指令不是敲击一次键盘就能输入的。每次敲击键盘,都会重复上诉动作,然后shell进程再次睡眠,并等待下一次键盘中断的产生。

到这里为止,系统仅仅是对用户键入符命令进行响应,正式处理还没有开始。

第二步:shell程序解析出用户命令后,调用fork函数创建一个用户进程,以便对hello world文件的程序进程控制。

系统在这里至少要为用户进程创建一套进程管理结构。每个进程都要有一套这样的结构,以便控制讲台加载的程序。这套结构十分复杂,包括时间片、优先级、进程状态、进程对应的文件、进程的任务状态描述符表(TSS),以及进程的局部数据描述符表(LTD),等等。其中的每一项又与系统的运行有着千丝万缕的关系。比如说TSS,它里面存放着当前进程运行时所有寄存器中的数据,一旦发生进程切换,系统就将当前各个寄存器中的数据存储在TSS中,同时用即将切换到的进程中的TSS中的数据来设置各个寄存器中的值,最后再切换。可见,这个TSS中的数据是进程切换的根本保障。再比如LDT,它里面存放着当前进程的代码段描述符和数据段描述符,这两个描述符都直接控制着进程所控制的程序,而进程运行的根本目的就是执行用户的程序。
另外,每个进程都会有TSS和LDT,为了便于管理,还需要再设计一套数据结构,就是全局描述符表(GDT)。所有进程的TSS和LDT大索引都存放在这个GDT中。系统为了方便操作GDT,并进一步操作LDT和TSS,还要对CPU中关于这三个表的专用寄存器进行设置,他们就是全局描述符表寄存器、局部数据寄存器和任务状态寄存器。
但仅有这些,还是远远不够的。系统启动之初是实模式,各个段寄存器中都是实际的地址值,直到进入保护模式,段寄存器中的数值才变成了段选择符,这样GDT才能参与应用,所以系统还要为实模式到保护模式的转换做全方位的准备工作。
以上这些只是针对TSS和LDT进行的展开分析。进程管理结构中其他成员与系统之间同样有着紧密的关系。例如,进程调度的最基本方式就是通过时间片轮转,而时间片轮转的最重要的参数数据就是当前进程的时间片。再例如,只有进程才能够操作文件,所以进程就要与文件全面建立关系,包括文件的i节点、文件管理表中的表项。进程自身的文件管理指针表,等等。
创建进程,就必须创建进程管理结构,而进程管理结构中的成员一个都不能少,都要创建并设置。除了进程管理结果就,创建进程的时候,还要为新进程复制页表和创建页目录项。这些都与内存页面的应用有直接关系,而内存的应用策略又是整个操作系统中最复杂的应用策略之一。

第三步:新进程创建完毕后,加载hello world文件对应的程序。

要完成这一步,进程就要在两方面进行全方位的准备:一方面是文件,另一方面是内存。hello world 程序一定是以可执行文件的方式存储在硬盘上的,所以,在文件加载之前一定要检测文件是否可用,主要表现在对文件i节点的检测和对文件头的检测两方面。i节点是文件的管理信息。只要涉及i节点,就一定离不开对i节点的查找,于是就要解析文件路径、操作目录文件和目录项、操作i节点表等,一件事情都不能少做。文件头存储在数据块中,要操作数据块又离不开逻辑块位图的支持,这样一来,整个文件系统中涉及的全部内容都要用到了。
具备了载入文件的条件后,就要将hello world文件载入到内存中了。这样系统就要解决所有与内存相关的问题,包括要与原来进程共享的页面接触关系,这就涉及页面引用计数、页面三级管理机制(页目录表、页表、页面)、页面数据(只读/可读可写)等一系列问题,系统就要为此简历页写保护等机制来解决这些问题。
但仅仅解决这些问题还远远不够,程序的加载也是很讲究策略的,其中最重要的就是缺页中断机制,即必须根据需要来分析是不是需要申请新的页面来加载程序的内容。为此又要对许多数据进行判断,这样才能确定加载的必要性,比如线性地址所对应的物理地址是否被映射到了线性地址空间内等,这就需要一套物理地址到线性地址的映射方案。另外,缺页中断机制的设计也很有讲究,缺页并不等于一定要把外设的程序加载进来。比如,由压栈导致的缺页同样要申请新的页面来载入数据,但这与外设一点关系都没有。这些都是缺页中断机制设计时需要全面考虑的问题。
总之,hello world程序的加载,几乎涉及文件管理与内存管理的各个方面。而且,以上所诉还仅仅是针对hello world这一个进程加载所进行的最基本的介绍。Linux是支持多进程执行的,每个进程都有可能加载自己的程序,而文件和内存有事所有程序可以共用的资源,他们之间还存在着更为复杂的管理悬系。比如,两个进程加载同一个hello world文件时,设计要不要共享,如何共享,共享后页面的引用计数如何计算,读写属性如何确定,等等。

第四步:hello world程序开始执行,将“hello world”字符串显示在屏幕上。

hello world程序加载进内存后就要开始执行了。这个程序比较简单,就是讲hello world这个字符串显示到屏幕上。但是,即便如此,系统也要为此做很多的工作。其中最主要的就是关于显示方面的工作,比如,显卡属性如何确定,显卡是单色还是彩色;显存位置如何确定,显示在屏幕上的位置又如何确定;如果字符数量过多,要不要滚动显示,如何滚动显示,等等。这些问题都要操作系统来做,而且直接与显示器的底层交互。

通过以上四点,我们归纳出,操作系统的一部分任务是为应用程序的运行提供使用硬盘、显示器、键盘灯外设的基础程序,应用程序就必须写这些程序,而且所有应用程序都要写的这部分程序的内容也都差不多。所有,我们也可以把操作系统看成所有应用程序共有的部分。
像Linux这样的现代操作系统,不仅为应用程序提供了对外设的支持,还支持多个程序同时运行。这就要求操作系统不但要支持外设,还必须对运行的多个程序进行有效的组织、管理和协调,防止某个程序独占CPU、内存、外设等资源,使得其他程序无法正常运行。此外,还要防止正在运行的程序之间相互读写和相互覆盖,确保所有程序正确运行。最关键的是,操作系统不能被应用程序直接读写,更不能被应用程序覆盖

你可能感兴趣的:(运行hellow world操作系统都做了哪些工作)