基于Linux0.11源代码的操作系统内核典型处理过程分析1

基于Linux0.11源代码的操作系统内核典型处理过程分析1

---进程1执行setup得到硬盘分区表信息

一、背景

操作系统内核的实现复杂性毋庸置疑,其内部各个模块间,软件硬件间的相互协作处理十分复杂,再加上不同进程的切换调度,内核态和用户态之间的相互转换,使得理解其工作原理变得很困难,总有种不识庐山真面目,只缘身在此山中的感觉。

对此,我个人在学习和实践的过程中间走了很多弯路,不敢说现在已经清楚,但至少在如何更好地学习和掌握OS原理和重要的实现细节上有一些方法性的感悟,其中最有效的一点还是确定一条需要理清楚的主干,然后把这条主干上的重要逻辑部分的作用搞清楚,再进一步把每个逻辑部分中的重要实现原理和具体实现方式及关键细节搞清楚,按照这个顺序来一步一步积累,虽然OS的内涵博大精深,但当把想要理解的模块的关键主干抓住了,剩下的基于不同情况下的特殊处理方式和不同OS版本的具体实现上的差别都比较好入手理解和学习了。

因此这里我把自己学习开发OS的过程中发现的一些典型处理逻辑提取出来,整理成资料,实践和总结的过程也让自己受益不少,对很多东西也有种豁然开朗的感觉,希望对大家也能有用。

二、setup获取硬盘分区表信息过程分析

1. setup执行上下文:setup是由进程1执行的过程,实际上是进程10号进程创建出来后要做的第一件事情,虽然setup里包含了获取硬盘分区信息表信息,建立虚拟盘和安装根文件系统设备等很多工作,但这里只分析到获取硬盘分区表信息过程,个人认为这已经是一个可以独立分析和理解的典型过程之一。

进程0 --> 进程1 -->setup <--> sys_setup

( 用户态 )( 内核态 )

可以看出,setup的实际工作是由其对应的系统调用处理函数sys_setup完成的,也即setup入口时是工作在用户态,随后由系统调用进入内核态执行,待OS执行完sys_setup过程处理完相应的请求后返回到用户态,即我们看到的setup()函数返回。关于系统调用的处理流程又是一个典型且重要的过程,我会再另外总结自己的学习心得,这里暂时放一下,并不影响我们要理解的操作过程。

2. sys_setup 获取硬盘分区表信息过程概要分析

sys_setup源代码中关于获得硬盘分区表信息的处理细节很多,我个人觉得最重要的是理清其如何从一块硬盘里得到相应的硬盘分区表信息,至于对硬盘的类型判断和是否存在第二块硬盘涉及的相关处理在本文中不再涉及,这不是要关心的重点。

主要流程如下:

1) 获取硬盘自身参数信息

这一步的作用很明显,如果要读硬盘分区表信息,先要知道即将处理的硬盘自身的基本信息,这样才能对硬盘操作。而这些信息已经在OS最初启动初期setup.s调用BIOS中断得到并保存在物理内存0x90080开始的地方了。

2) 从缓冲区链表中获取一个缓冲区块

这一步的作用是先准备好一块内存,即将来得到的硬盘分区表信息存放的地方。获得这个缓冲区块后,系统会把它标识锁上,这样别的进程就不会有机会操作,相当于临界资源。

3) 准备一条要获取硬盘分区表信息的请求,将其放到硬盘请求等待队列中,进程1睡眠

Linux对设备的操作是封装成请求(request)放到请求队列中统一调度执行的,原因很简单,设备属于有限资源,要使用的人(进程)很多,要求也五花八门,因此统一请求的格式和统一调度管理是很必要的。这里应当注意,request的数目不是无限的,在Linux0.11request最多只有32个,如果请求数已经满了,则当前的进程就得睡眠等待了。

请求发出后,进程1会进入不可中断的睡眠模式,等待所请求的信息完成,除了显示的wake_up重置其状态外,进程调度不会有机会重新执行它。

4) 硬盘设备执行相应的请求获取所要求的信息

这个过程也很复杂,我们忽略硬盘复位和错误检查等细节,总的来说硬盘会读取所要求的数据,每读完一个扇区就发送一次中断,这时相应的中断处理函数就将本次读的数据复制到第(1)步获得的缓冲区中,然后检查是否把所要求的所有扇区都读完了,以调整缓冲区的相应指针准备下一次接收。对于Linux0.11获取硬盘分区表信息来说需要读的是一个块,也就是2个扇区。

如果全部数据接收完毕则置请求等待的进程的状态state为就绪态。

5) sys_setup执行完毕,从内核态返回进程1

由于数据已经获得完毕,进程的状态被置为就绪态(TASK_RUNNING),因此在下一次时钟中断到来时执行进程切换操作就会继续执行进程1,之所以不会切换到别的进程是因为此时系统除了特殊的进程0外就只有进程1。此时就会执行sys_setup从内核态返回的相关操作返回到进程1的用户态了,这样整个过程就执行完毕了。

这里需要指出的是,很多资料和帖子都把将进程的状态state置为0(TASK_RUNNING)称为唤醒该进程,我个人觉得这样很容易造成误解,因为“唤醒”给人的感觉是这个进程已经得到了运行,但实际上它只是被置为就绪态,表明了其可以被调度运行的资格,并没有实际运行,即使在进程调度时也许即将切换的进程也未必就是这个进程,因此个人感觉还是称呼其为置为就绪态更准确一些。

上面大致说明了进程1获取硬盘分区表信息的主要流程,相应关键的具体函数

调用序列如下:

setup(): 系统调用入口。

sys_setup(): 实际的系统调用处理函数,会从内存中取出硬盘参数信息。

bread(): 完成从设备(这里是硬盘)上读去相应信息到缓冲区的操作。

getblk(): 获取一块缓冲区,返回其首地址。

ll_rw_block(): 块设备的读写函数,实际上是逻辑层,由它组织封装设备请求操作,并不参与实际读写操作。

wait_on_buffer(): ll_rw_block最终的结果是发出请求,之后会返回到bread中,此时就需要等待缓冲区被填充,因此要将进程1通过sleep_on置为不可中断的睡眠模式

make_request(): 实际封装请求。

add_request(): 将请求加入设备的请求队列,对于本例它将立即得到执行。

do_hd_request(): 硬盘的实际请求处理函数。

hd_out(): 硬盘的实际操作命令封装函数,将触发真正的硬盘操作。

read_intr(): 硬盘读操作完毕后的中断处理函数,实现了将读取的信息复制到缓冲区

end_request(): 请求结束操作,如果请求处理完毕,则调用wake_up唤醒等待该请求的进程。

3. 一些重要细节

1) 本文讨论的实际上是一个块设备的典型操作流程,涉及到的主要是3方面:设备驱动操作、缓冲区处理、进程调度,虽然主要流程看起来比较简单,但实际的处理细节上主要涉及到的各种分支情况基本上就属于这3个方面,比如请求队列已满、所涉及的缓冲块已存在等等,个人感觉先抓主干,然后再参考这三个方面理细节就稍微容易一些。

2) 关于缓冲区的很多操作都是在关中断方式下执行的,这样做实际上是造成了临界区的效果,目的是防止在临界资源执行过程中被其他的进程或者中断处理误操作,典型的例子就是很多人讨论的wait_on_buffer操作。

3) 关于请求队列,事实上请求队列的使用是包括两部分的:

一是整个请求队列是有限的,申请时是从队列中获取空闲请求资源,且该队列中预留了一定数目的读请求,保证不会全是写请求;

二是封装好实际请求之后,该请求被挂在相应设备的请求链表上,这样每个设备能看见的只是自己要处理的请求链表,并不是整个请求队列,具体可以参考add_request实现,它的第一个参数就是块设备全局变量blk_dev的相应设备结构体地址,这个设备结构体里包含了请求指针,用来索引与该设备相关的请求链表。

你可能感兴趣的:(linux)