linux内核学习(转2)

·进程调度 

进程的调度是内核里面非常重要的一个部分,该模块完成了进程的切换功能,既它要选择一个最合适的进程去执行。我们知道进程的切换是要花费一定的时间的,如果调度器一个劲的调度进程,那么系统的利用率可想而知了——那会非常低。但是这样做有一个好处,那就是进程的响应时间变快了。相反,如果我们选择尽量让进程多运行一会时间,尽量少发生进程的调度,不错,如你所想,提高了系统的吞吐量。可是另一方面就带来一个缺点,那就是进程的响应时间变慢了。

所以在这块,内核开发者需要动一些脑筋了,他们需要用到大量的算法。以此来解决这样一对矛盾:如何平衡进程响应速度(响应时间短)和最大系统利用率(高吞吐量)。从2.4版本到现在最新的 2.6版本已经变动了两次调度器,现在最新的调度器叫完全公平调度器(CFS)。更新调度器是为了更加找到上面所说的那种平衡。

由于 linux的被使用在了不同的环境,从桌面环境、嵌入式系统一直到服务器。不同的环境,那么他们肯定会选择一个更加适合这个环境的调度器的。嵌入式系统需要的是响应实时,既响应时间短。而服务器一般则是需要的是一种公平的对待每个任务的调度器。Linux在实时方面并不是做的很好,起码内核源代码中的是这样。不过有很多人针对实时性,专门对内核源码做出了相应的修改,提高内核的实时性,这样的代码可以用到嵌入式系统上。

Linux的调度器像一般现代操作系统一样,提供了基于时间片的可抢占式的进程调度方式。基于时间片可以照顾那些低优先级的进程,而可抢占式有在一定程度上满足了实时性(响应时间短)的要求。

进程调度发生的时机是我们要注意的,既什么时候发现进程的切换。注意:schedule()函数为发生进程切换的函数。

主动式:进程自己调用schedule()函数。——我担心很多爱提问的人对这句话不懂,就解释这样一句话还要花很长时间。第一点,进程自己调用 schedule(),分为两种形式。一是进程在应用程序中调用了进程调度的系统调用,所以“进程自己调用schedule()函数”。二是进程为了得到某个硬件资源,所以进程发生了阻塞式的系统调用,并进入了内核空间。此时内核代进程去访问那个硬件资源,可是结果是硬件已经被占用,你现在得不到硬件资源。于是内核先把进程挂到这个资源的等待队列上面,并设置进程的状态为挂起状态(不可再被调度),然后内核代进程主动调用schedule()函数,我们也叫“进程自己调用schedule()函数”。你懂了吗?

这边要解释在进程管理中留下的一个问题:为什么要有两种任务挂起的状态呢?这边很多书籍都会这样简单的写:一个可以接受信号,一个不会接受信号。但是这样讲大多部分读者并不能弄明白的。其实是这样的:TASK_INTERRUPTIBLE这个挂起状态可以认为是可中断的挂起,TASK_UNINTERRUPTIBLE则是不可以中断的挂起。挂起是因为要等待某种资源,这点你要明白,否则我再怎么讲解,你都不会明白的。那等待某个资源为什么还要两种挂起方式呢?先说第一种,可中断的挂起,在等待某种资源的时候,当他接受到信号的时候,会被提前的唤醒,去执行信号处理函数,然后回到进程执行过程。什么是提前的唤醒?你可以认为是伪唤醒,既不是真正的硬件条件达到被调度执行,而是因为到来了一个信号,提前唤醒了。那么聪明的读者看到这边一定会问:那么被唤醒之后怎么办呢?岂不是还没有等到资源就被提前唤醒了吗?所以在这边,一般是用一个while(1)循环操作,当每次被唤醒的时候都会去循环的检查一次资源是否真的到来了。如果是被假唤醒了:那么内核就会再次执行循环体:内核代表应用程序本身,把进程自己设置为挂起状态,然后执行schedule()。如果是真的条件到来了,那么就把进程移出等待队列。(所以大家要注意了,不是把进程放到等待队列之上就发生了进程的切换,进程的切换还是需要内核代进程完成的。)那么大家就会发现一个问题:进程由可能会被经常的伪唤醒,去检查资源,再去发生进程切换,这样不是明显的浪费了处理器时间了吗?实时就是这样的,不过因为信号处理也是很重要的一个部分,所以内核中一般还是用这个挂起的状态。那么TASK_UNINTERRUPTIBLE就是不可中断的,当他收到信号之后,不会发生提前唤醒去处理信号。只有当资源达到之后,提供资源的模块发出唤醒操作,这才真正的唤醒了此进程。(由于篇幅有限,具体信号处理方面不再过多的解释,关于信号处理大家要去找书籍看。)

刚才讲到了发生进程切换的时机,一个是主动发生,那还有一个就是被动发生了,既进程被其他进程抢占了。被动式发生进程调度是这样的:当一个进程的时间片被用完了,那么它就会被其他进程抢占。还有一种情况是有优先级更加高的进程进入了可执行状态,当前进程也会被抢占。那么抢占发生在什么时机呢?一种是用户抢占,另一种是内核抢占。

用户抢占:你可以这样理解,发生在用户空间的抢占。我们知道用户空间和内核空间只有两种途径:一个是系统调用,还有一个就是中断。所以可想而知,进入或退出内核空间都会有量段特定的代码的:一段代码完成了用户态到核心态的切换,还有一段代码则是完成了从核心态到用户态的切换。那么我想告诉大家的是,用户抢占就发生在从核心态到用户态的切换代码中。既当系统调用完成之后从内核态到用户态的时候和中断处理完成之后从内核态到用户态的时候发生用户抢占。那么内核怎么知道要发生进程的抢占呢?若need_resched标志被设置之后,则会发生用户抢占,否则不会发生。那么谁来设置need_resched这个标志呢?一个是在定时器中断处理程序中(稍后会介绍),scheduler_tic()函数会减少当前进程的时间片。当时间片减为零的时候,设置那个标志,告诉内核:嗨,内核你注意了,这边有个进程需要被调度啦。稍后,内核就会检查这个标志,并发生进程切换。还有一个就是当一个优先级高的进程进入了可执行状态的时候,try_to_wake_up()(这个函数是唤醒等待队列上的进程的操作,这个是真的唤醒操作:唤醒等待该资源的一个进程,设置进程的优先级,并把它设计为可执行状态。)也会设置这个标志。

内核抢占:内核抢占是2.6内核新加入的一个内容。在没有内核抢占之前,抢占只能发生在了用户空间,然后在实时性方面会有很差的表现。引入了内核抢占,进程调度可以发生在内核空间了。这个时候引入了 preempt_count,叫内核抢占计数器。为什么要这个计数器?因为内核中不像应用程序中那么简单,内核路径非常复杂,有非常多的情况是不能发生抢占的(具体的不一一列出),所以设置这个计算器完成是出于安全考虑。只有当内核抢占计数器和need_resched同时满足条件的时候,才可以发生进程内核抢占。那么内核抢占发生在什么时机呢?一个是中断回到内核空间,二是解锁和使能软中断,具体这两个我们后面有机会介绍。

关于进程调度还有一个要注意的,那就是读者你要去理解进程切换发生了什么事情。第一点非常重要,我在这边做简单的介绍。我们知道每个处理器都有一个叫程序计数器(PC)的寄存器,(如果你不知道PC的作用,也不用看关于内核的东西了,先把处理器最基本的知识搞定。)PC指向了下一条指令的地址,所以我们大概可以这样理解:发生进程切换时,PC指针被保存到了一个安全的地方,然后从某个地方把要切换的进程的保存的PC指针放到这边,然后改变堆栈指针,就这样。具体细节请读者一定看明白。

那么进程调度也讲的够多了,到这边大家应该对进程调度有了一定的了解了吧。这个模块还是比较重要的和有意思的,读者可以话一定时间去了解。如果你还有疑问,就多看几遍,或者找本书看。

 

·内存管理 

在大家学习内存管理之前,一定要先把MMU这个东西弄明白,要不就会很难往深处理解,或者压根看不懂关于内存的管理。现在的出来器都含有了内存管理单元(MMU),MMU管理内存并把虚拟地址转换为物理地址。通常是以页为单位进行处理的,所以你会发现你所有得到的内存大小一般都是4KB的倍数(我们只讨论32位机器),很少有例外。

内存管理这节要介绍的是内核如何管理和分配内存,下一小节进程地址空间,介绍内核如何管理用户进程的地址空间。首先我们要知道只有内核才可以管理内存,内核需要用内存,那么它就自己给自己分配一块内存。如果是应用程序要用内存,那么应用程序会发出一个请求分配内存的系统调用,然后内核就给它分配一块内存空间。

我们需要弄明白什么是虚拟地址,什么是物理地址,它们是如何转化的。这个就是MMU要做的事情了。两个地址之间相互的转化还需要页表来完成操作,页表也是同样重要的哦。这部分内核需要理解的东西也就在这边了,把虚拟地址,页表,物理地址弄明白。理解这些对下部分内容也是非常重要的。如果你想要研究,是很容易理解的,出于篇幅限制,不讲解那部分内容。其他的就是只要掌握内存分配的接口,在你的驱动程序中要用到这样的接口。

当然如果你觉得MMU真的麻烦,那就不去看它了。MMU对应用程序来说完全是屏蔽的。如果你觉得没必要了解,那就不了解吧。

内核采用伙伴系统管理内存,至于伙伴系统,大家可以自己了解。现在就称它为分配器吧,它对外提供一个接口函数:要用内存就调用我吧,不要管我是这么分给你的了。当然这一切都必须由内核开发人员提前做好。所以我们这边只需要了linux操作系统是如何组织和安排内存的,然后就是要会用它们提供的请求内存分配接口。

这边还有一个重要的东西,那就是slab分配器。它建立在伙伴系统之上,在这缓冲者内核常用数据结构。这就大大提高了某些大型的数据结构的分配速度。比如在进程管理我们提过的task_struct,就在这边分配得到。同样slab分配器模块也给内核提供了相应的接口,如果内核要用,就请调用相应的接口吧。

还有一个地方需要注意了,那就是内核栈非常小,一般陪分配为8KB。不像用户空间的栈,可以动态的增长。如果你在编写你的内核程序中(大多部分是驱动程序),请尽量不要静态分配。也就是局部变量,当使用大型的数据结构的时候,为了防止可怜的内核态栈溢出,请尽量选择动态分配内存。

如果你理解了MMU,消化了虚拟地址和物理地址的关系,知道了页表的作用和位置,那么这章是不是还是蛮简单的?所以这部分的重点就是请了解内存分配的接口,以后你总会用的到的。

这边为大家展示一幅关于内存分配的结构图,对照这个图,有助于理解这两部分的内容。

 

 

·进程地址空间

这一部分内容全部是围绕着虚拟地址来讲诉的。由于有了MMU之后,带来了虚拟地址的这样一个概念。应用程序使用的地址,变为了虚拟的地址。我们知道虚拟地址有3GB空间的大小,应用程序在于不用担心内存太小的问题了。如果你还问:要是我的物理内存只有2GB怎么办呢?那显然就是你对虚拟地址和进程地址空间的知识了解不多了,那么本部分的内容就是为了解决这个问题。

由于有了虚拟地址的出现,程序只有在链接加载的时候才给分配虚拟地址,并建立到物理内存的映射。系统中每个进程都有3GB的虚拟地址空间可以使用,但是只有当进程确确实实的需要内存的时候,内核才会给进程分配内存。

系统中每个进程都会有一个叫做mm_struct的内存描述符,它表示进程的地址空间,存放了与进程的地址空间有关的全部信息。其中有一个很重要的成员就是虚拟内存区域vm_area_struct(VMA)。那么这个VMA代表什么呢?它代表着进程的一块虚拟内存空间,在这个结构体里面会有具体的虚拟地址空间的起始地址。VMA就是代表进程不同的虚拟地址空间区域。在这边好多书上都会例举一个程序,然后把程序所有的地址空间例举出来,在那你会对我所讲的东西弄的清清楚楚,虽然你现在可能一头雾水。你会发现,不同的区域都有它存在的理由,存在的价值,所有的VMA合到了一起组成了一个进程的所有地址空间。

这边的确是有点难懂,不过一旦你弄明白就会发现,哇,原来是这样的,真的很神奇。要慢慢理清思路,首先你要知道进程中用到的都是虚拟的地址。然后你会想要一个进程需要一个代码段,一个数据段,一个bss段,一个用户栈空间,一个堆空间,还有呢?还有库函数和动态链接程序(可能这边让你费解,现在不用管它,以后你会明白的)所需要的代码段,数据段,bss段。那么我告诉你,如果进程需要上面讲诉的那样的段或者堆栈空间,那么都会有一个VMA与之对应。然后如果进程要增加内存,或者真正需要内存的操作了,而且这时候进程还没有物理内存可以操作。那么它就会产生一个缺页异常,内核收到这个消息之后,会给进程分配一块真正的物理空间,并使这块物理内存映射到VMA中,那么进程你就可以使用了。内核对于内存的分配,永远是需要时才分配。看到这边你再对照上面的那图图,是不是明白了什么呢。

那么进程是怎么把虚拟地址转化到对应的物理地址的呢?那就要用到页表啦。所以你可以想象,每个进程一定都会有与之对于的页表项。有这样的页表项,使得MMU会正确的安装进程的虚拟地址找到它相应的物理地址。

没有头晕吧?书读百遍,其义自见。

 

·系统调用 

相比之前所讲诉的内容,系统调用就相对简单些了。前面介绍了,系统调用是进程进入内核空间的一种方法,稍后我们会接触到第二种进入内核的方式:中断。前面的内容已经介绍过为什么要进行系统调用了:出于安全的考虑,进程不能直接调用内核函数或者访问硬件。那么对于系统调用,我感觉大家掌握一点就可以了,那就是进出内核态的过程和路径。至于说会编写自己的系统调用就没有太大的必要了。

在我们研究进出内核态的过程和路径的问题,先介绍一个重要的东西,那就是C函数库。应用程序员编程是大多不必要了解内核的细节,他们只需要了解C库给他们提供的函数就可以了。这就是Unix编程的一大特色风格,“提供机制而不是策略”。就是说只需要提供给上层什么样的功能(机制),而上层完成不需要关心这是如何实现这些功能的(策略)。还是那个比方,你要在屏幕输出一串字符串,那么你在你的程序中会调用printf()函数输出你要输出的内容。那么你可想过C库帮你做了什么了吗?当我们调用C库来帮我们完成某些功能,C库帮我们做了封装,然后C库里面相应的例程帮我们调用了相应的系统调用完成了某些功能。还是这个比方,在C库调用了相应的封装例程之后调用的是 write()系统调用,进入内核。

那么我们现在就来看进程进出内核态的过程和路径的问题。首先我们知道:进程不可以直接调用内核函数,但是最终处理器还是从用户空间进入了内核空间,那这是怎么实现的呢?C库帮我做了一些事情之后,还是调用了系统调用。在编译汇编程序的时候,编译器会帮我们在这边放置一条软中断指令。然后引发软中断异常实现的:通过引发一起异常来促使系统切换到内核态去执行异常处理程序。这里就是关键:从用户空间进入了内核空间。如果你还不明白,那么我就来稍微解释下吧:

当产生一个异常的时候,程序计数器PC就会跳转到一个固定的地址。然后内核会在这个地方放置相应系统调用服务程序。然后在这个系统调用服务程序中,内核通过设置一些寄存器,改变处理器模式,使系统进入了内核态。现在我们已经在内核空间啦,你反应过来了吧。

那么还有一个问题:那就是内核怎么知道她要做什么呢?所以这边引出一个非常重要的东西,叫系统调用号。这个号是由内核开发官方人员放出的,如果你要用,请申请。这个系统调用号和系统调用一一对应,编译器和内核都知道这个对应关系。在对程序进行编译的时候,编译器会把这个号码放到一个寄存器当中。接下来当发生系统调用的时候,在内核的系统调用服务程序中,检查这个号码。这个号码就告诉了内核,进程想要请求的是哪种服务。然后内核查看系统调用表 sys_call_table,找到所调用的内核函数的入口地址。接着,就调用该函数,完成相应的系统调用服务。这时我们称:内核代表应用程序来执行该系统调用。等返回后,做一些系统检查最后返回到用户空间(这边的检查就是进程调度部分所说的用户抢占发生的时机)。我们又回到了用户空间啦。

那么到现在你明白系统调用的过程了吧。

 

·中断异常

中断异常也是一种进入内核态的方法之一,不过这个方法是不受程序所控制的,既我们无法预知和控制中断的到来。所以在中断的到来之前,我们必须做好准备。中断和系统调用一样,都会进入核心态,而且他们使用了一个同样的办法。那就是PC会跳到一个固定的地址,那么内核就会在那个地址事先安放一个跳转地址,这个地址跳转到中断服务程序的入口处。那么当中断发生以后,一切不可知的内容变为可知:内核永远知道代码的执行流程了。

那么中断处理程序来自什么地方呢?那我们就要看中断来自什么地方了。基本上所有的硬件只要与处理器通信,都会产生中断。这边拿键盘做比方,如果你按下一个按键,那么键盘硬件会产生一个中断。这个中断信号是一个电信号,处理器会检查是否有中断的到来了。如果它发现中断来了,那么它会自动的跳转到一个固定的地址,而那个地址早已经被内核安排好了相应的内容了。进入了中断服务程序的入口之后,入口程序会检查中断号(原理与系统调用号差不多),然后选择相应的中断服务程序来处理这个中断。

所以这样看来,固定的硬件都会有自己的中断服务程序。实时也是这样,当一个硬件需要中断的时候,那么在硬件相应的驱动程序中就会含有该硬件的中断服务程序,只需要在驱动程序中注册好硬件的中断服务程序就可以了。在2.6以后的内核中,中断有了自己的中断栈,以前都是用当前进程的内核栈。这样做可以使中断服务程序可以安心的使用自己的栈了。在执行中断服务的时候,是不允许发生阻塞,中断上下文中的代码应当迅速简洁,因为它打算了其他代码的执行。

在中断部分还有一个相当重要的内容,那就是所谓的中断下半部分。什么是中断下半部分?为什么要有中断下半部分?因为我们知道中断处理程序打断了其他代码的执行,所以它的执行应该短、平、快。但是很多时候我们会遇到这样的情况,那就是在中断服务程序中我们需要处理很多的东西,那怎么办呢?这个时候就引入了中断下半部分机制。在中断服务程序中完成一些与硬件有关的必要的操作,然后进入中断下半部分。在中断下半部分中,完成不是太急的工作。而且在中断下半部分的时候,代码是可以被中断的。所以这就保证了系统处于中断屏蔽的状态时间尽可能的短,以此来提高系统的响应能力。中断下半部分在驱动程序中用的非常多。如果你尝试写自己的驱动程序的时候,可以读一下别人的中断处理程序和相应的下半部分会有助于你的理解和编程。

 

·定时器中断

讲解完中断处理之后,介绍一下操作系统的心脏——定时器中断。它给系统提供了固定时间的中断间隔,也就是每过一个固定的时间,定时器都会发生一次中断。通过上面的学习,我们知道中断的发生,可以检查是否要发生进程的切换。所以在一定程度上,定时器中断可以调高系统的相应时间,尽量减少了进程长时间得到不处理器运行的饥饿情况。但是如果定时器中断频率太高,那么大部分处理器时间都花在了定时器中断处理程序上了,这显然是可以让人接受的。所以这里也存在一个平衡。那么这个时间定为多少呢?在X86个人计算机上,这个数字HZ被设置为1000,也就是说每秒钟,定时器会产生1000次中断。ARM处理器目前被设置为每秒100此,这个HZ的数值是可以自己修改的。

那么定时器中断具体来完成什么样的操作呢?在每秒HZ次的定时器中断服务程序中,内核需要完成这样几个比较重要的工作:更新系统时钟,执行到期的动态定时器,更新进程的时间片。

更新系统时钟并使用墙上时间更新实时时钟(RTC),来保证系统的时间准确。

执行到期的动态定时器,动态定时器是非常有用的一个东西,这个东西我们会经常接触到。最常见的就是我们用所有的电子产品的时候,为了省电,如果不去操作它,那么过一段时间之后屏幕就会熄灭。好比这个熄灭屏幕的时间为30秒,那么30秒的计时开始是从你最后一次操作器件开始的。所以这就需要一个动态的定时器,一旦操作一次之后就会注册一个动态定时器。在定时器到期(到期后屏幕会熄灭)之前,假如又有一次操作,那么就销毁前一个动态定时器,并创建一个新的动态定时器。定时器中断处理程序,会去处理那些到期的动态定时器。所以动态定时器就是完成这样的一个工作:帮助内核完成推后执行某些代码。在看内核书籍的时候,大部分书籍在这部分内容会教你如何去动态注册你的动态定时器:对于相应的接口,给定一个要延迟的时间,给定一个到期后执行的函数指针,你就完成了动态定时器的动态注册。

更新进程的时间片,我们在进程调度那边讨论过了,在定时器中断处理程序中会调用scheduler_tick(),来跟新进程的时间片。也就是减小进程的时间片,如果该函数发现进程的时间片给零,那么它就会设置need_resched这个标志。然后在定时器中断处理程序返回的时候,会有一个固定的例程来查看这个标志,如果它发现了这个标志被设置了,就会去调用schedule()来选择一个合适的进程运行。

定时器中断就介绍到这边,那么你现在应该知道定时器中断的重要性了吧,它比一般中断出现的更加普遍。理解定时器中断做了哪些事情非常有利于我们理解整个计算机系统的工作流程,不是吗?

 

·内核同步

内核同步是保证整个软件系统安全运行的一个非常重要的手段。我们一直说:引入了操作系统,使得访问硬件变得安全可靠,软件的执行也更加的安全。那么这是一种什么样的安全呢?内核又是通过提供什么样的机制来保证了这些安全性的呢?在这部分,我们还会接触到一个新的东西,那就是进程间通信,内核提供了一些机制,保证了进程间安全的通信。

先讨论第一部分:为什么要有内核同步?内核同步是用来保护哪些内容的?在我们学习操作系统原理的时候,会接触到这样的一个内容,那就是临界区:访问和操作共享数据的代码段。那我们要知道什么是同步,同步就是避免并发和防止竞争条件,也就是要防止多个进程同时访问同一个数据或者临界区。这里有一个非常经典的比方,那就是你的银行卡里面有100元钱。然后你去自动取款机取钱的动作和你爱人用存折小本在银行里取钱这个动作同时发生了,你们都想取80元钱,那么在这个时候会发生什么情况呢?你会都会取到80元吗?那银行绝对会保证不会让这样的事情发生的。在这个例子里面,存款就是数据,是取钱这个动作的代码要操作的数据。所以这段代码就是临界区,我们保证同步,就是为了防止这样的事情发生:多个进程同时进入了临界区。内核中有非常多的地方是需要保护的,防止多个进程同时访问,你现在明白为什么会需要内核同步了吧。

让我们在仔细理解下上面的例子,我们为了达到同步,保护的是什么东西?是代码吗?在linux内核中,提供了锁的机制,是给数据上锁,而不是代码上锁。一旦上锁就可以保证同一时间只可以有一个进程访问加锁的内容。那我们现在理解下,为什么是给数据加锁而不是代码?还有可能有很多读者在这边并不明白数据指的是什么,我认为那是因为你对软件方面的知识还不是太了解。我当初在接触到这边的时候,也对数据产生了一些迷惑。不过我相信通过一段时间的学习,你最终会明白这些数据到底可以是哪些了。其实这些数据就是内核的一些全局变量,或者一些重要的数据结构。如果当一段代码要对一个非常重要而且不能被同时访问的数据结构的某个数据操作的时候,内核就会首先对这段数据进行加锁,保证了现在只有我这段代码可以访问你这堆数据。其他人要访问,必须等待。我们在强调一下:保护是保护的数据,而不是代码。

现在看第二个问题:内核提供什么样的机制来保证同步。这部分内容在每本内核书籍中都会讲解的很详细,这些机制是非常有效和有用的。当你在编写驱动程序的时候应该会用到。其实我们已经接触到了一个,那就是信号量与等待队列。它们可以保证了对一个资源的安全访问。

然后就是关于进程间通信(IPC)的相关知识。前面介绍过,内核对进程提供了虚拟的进程地址空间,所以进程们看到的都只有自己的地址空间。进程会认为整个系统中只有自己一个进程在运行,那么他们就是看不到其他的进程的。所以当我们设计应用程或者设备驱动的时候,可能会遇到这样的问题:那就是进程需要和另一个进程通信。这个时候我们就要用到内核所给我们提供的IPC机制。IPC机制会提供给我们相应的接口,你只需要通过系统调用,然后就可以使用这些机制了。同样,你并不需要知道这些机制的内部实现原理(对于一般应用程序开发来说),但是你却达到了和进程通信的目的。我们要知道内核给我们提供了哪些进程间通信的机制,并掌握其中的一两种。

那么你现在应该清楚为什么要有内核同步机制了吧。你可以试着去了解他们的实现,不过这相当费力,不过蛮有意思的。如果你能做到了解那些接口,并知道什么时候该用这些接口了,那也可以了。

 

·虚拟文件系统

下面介绍内核中相当重要的一个部分,那就是虚拟文件系统(VFS)。如果你没有把内核的结构图记住,那么久请再回过去看一下吧,请看一下内核中虚拟文件系统 VFS所处的位置。它的位置是在系统调用的接口之下,因为很大一部分的系统调用需要用到文件系统的内容,至于为什么下面讲解。然后它又被放置在驱动程序之上,因为你也许听说过:linux中所有的硬件都被当成了文件来看待、来处理。所以当系统调用要用到硬件的时候,大部分都会进入VFS,由VFS来帮你完成那些硬件的操作。如果你曾经做过linux程序设计,那你现在一定会联想到什么了。既然我们说过了linux把所以的硬件都看出了文件,那么对硬件的操作就变成了对文件的操作(永远记得那句话:提供机制而不是策略。当你全心全意的做应用程序开发的时候,请直接去用接口,而不要去关心下面是如何实现的)。内核已经给我们屏蔽了所有的底层的操作,在我们只在意怎么去用的时候,不必过多的关心内核是如何实现这样的屏蔽和封装的。

既然内核对所以的硬件都使用了相同的屏蔽封装操作,所以我们就可以访问不同的介质的文件系统。因为内核(更确切的说是虚拟文件系统)对他们做了封装,然后VFS对内核提供相应的接口,然后在内核中我们就可以使用VFS提供的文件接口。如果应用程序也要使用,那么内核还要给他分配一个系统调用,然后应用程序通过系统调用,再去调用相应的接口,完成对不同硬件的操作。

当我们对对硬件操作的时候,其实已经是转化到了对文件的操作。这个时候你只是需要对文件进行操作就可以了。所以当你要操作硬件的时候就要像操作文件那样先打开文件,你会用到open()这个系统调用,这个系统调用会按照我上面所说的那个路径,一步一步,最后访问到了相应的硬件。

虚拟文件系统的实现用了面向对象的编程思想,不过为了效率起见,还是用了过程式的C语言来实现了面向对象的程序结构。也就是把一个对象的各种特性和状态,以及对这个对象的操作都封装到了一起。

这个部分还是需要读者去看相关的书籍的。这边会涉及到4个非常重要的结构。你可能要花点心思去了解其中相当重要的几个结构和部分。这对驱动程序设计是有非常大的帮助的。驱动程序,我们下面就会去接触到它。

 

·驱动程序

我们现在来看一个非常重要的内容,那就是设备驱动。在内核源码中,设备驱动占去了相当大的一部分。其实驱动程序已经不属于内核的内容了,为什么这样说呢?因为设备驱动程序都是基于内核提供的各种不同的机制实现的,驱动程序本身只调用内核提供的函数。所以这也是驱动程序也被称之为内核中的“应用程序”的原因。那为什么我们还要把驱动程序放到内核中讲解呢?原因是因为设备驱动运行在内核空间(如你所猜的那样,为了出于安全的考虑)。我们在看下关于内核模块结构图的图,你会发现,驱动之上是VFS。所以当我们访问到硬件的时候,我们是通过虚拟文件系统访问的。当然内核已经做了相应的屏蔽和封装,你可以不用关心实现原理了。

因为内核的开发并不是很容易的,当然也不需要我们去开发。那我们唯一可以做的事情就是编写设备驱动程序了。我是学习嵌入式系统开发的。这也是我当初为什么要下决心研究内核的原因,我想了解计算机系统工作原理,然后能编写设备驱动。学到现在我感觉我对已经有了内核的功底,那我就可以试着找一些驱动程序的资料来看,然后开发自己的设备驱动。这个能力在嵌入式系统设计的底层开发中是相当重要的。

编写自己的设备驱动程序,我们要了解设备驱动到底用了哪些内核所提供的机制,利用了这些机制,来实现了一个什么样的功能。当然在内核往设备程序转的时候,有个非常需要注意的那就是这个时候需要接触到大量关于硬件的知识了。需要能看芯片的芯片手册,看懂时序图,然后找到一种最合适的办法把芯片的功能展示出来,提供给应用程序。

这边有两本不错的书可以推荐下,一本是《Linux Device Drivers》(中文名:《linux设备驱动程序》,目前有第三版了),还要一本书叫《Linux设备驱动开发详解》(第二版)。如果你想做嵌入式系统的底层驱动程序开发,可以看看这两本书。

此部分大部分数据都会讲解如何编写并注册一个自己的设备驱动程序,一次来加深对设备驱动的理解。如果你理解了设备驱动的一个程序流程,或者说是应用程序访问硬件的一个流程你能明白,那就可以了。硬件的中断处理程序也是在设备驱动程序中的,需要被注册到内核中,然后当发生中断之后,内核找到相应的中断服务程序。

 

·网络

现在介绍简单我所讲述的内核中的最后一个模块,那就是关于网络。网络设备是唯一一个没有用到虚拟文件系统的硬件。这是由于网络的层次性格结构造成的。不过内核还是对网络做了相应的处理,使用了socket套接字。如果你想要网络编程,那么就使用内核提供的socket接口吧。并按照步骤建立连接之后,你就可以用 read()和write(),接收和发送数据。

 

 

 

以上就是全部的内容了,花了我整整3天的时间才完全写好。因为内容太长,并没有时间检查修改了。内容全都是对内核的理解和看法,也是一个学习过程的总结。如果你想学习内核,希望对你有帮助。

 

 

注:由于这是本人第一次写这种半学术性的文章,而且本人并没有花时间去检查和修改。所以我想文章中应该会有一些不正确或者表达不明确的内容。如果你看到了,请您谅解。当然如果你对这篇文章或者对内核的学习有一些想法,我们可以交流交流。我在人人社区建立了一个小组叫:linux kernel。可惜那个社区好像不方便搜索小组,所以欢迎大家加我校内,进入我的内核学习社区,我在那边分享了很多东西。你这样这样找到我:南京邮电大学 王伟。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章