📄 核心源码的物理布局.htm
字号:
<TD width="8%" height=4>
<DIV align=center><A href="mailto:joyfire@sina.com"><FONT
color=#ffffff>联系</FONT></A></DIV></TD></TR></TBODY></TABLE></TD></TR></TBODY></TABLE>
<TABLE borderColor=#666666 cellPadding=2 width="90%" align=center border=2>
<TBODY>
<TR>
<TD bgColor=#000000>
<P align=center><A href="http://joyfire.net/lsdp/index.htm"><FONT
color=#ffffff size=2>目录页</FONT></A> | <A
href="http://joyfire.net/lsdp/19.htm"><FONT color=#ffffff
size=2>上一页</FONT></A> | <A href="http://joyfire.net/lsdp/21.htm"><FONT
color=#ffffff size=2>下一页</FONT></A></P>
<P align=center><FONT face=黑体 color=#ffffff size=6>(LDD) Ch09-中断处理(下)(转载)
</FONT></P><SPAN style="LINE-HEIGHT: 1; LETTER-SPACING: 0pt"><FONT
color=#ffffff size=3>
<P>发信人: Altmayer (alt), 信区: GNULinux<BR>标 题: (LDD) Ch16-核心源码的物理布局(转载)<BR>发信站: 饮水思源 (2001年12月13日08:58:03 星期四), 站内信件<BR> <BR>【 以下文字转载自 <FONT
color=#00ff00>UNIXpost </FONT>讨论区 】<BR>【 原文由<FONT
color=#00ff00> altmayer.bbs@bbs.nju.edu.cn,</FONT> 所发表 】<BR> <BR>【 以下文字转载自 <FONT
color=#00ff00>altmayer </FONT>的信箱 】<BR> <BR> <BR>第十六章 核心源码的物理布局<BR> <BR> <BR> <BR>到目前为止,我们从写设备驱动程序的角度讨论了Linux核心。但一旦你开始研究核心,<BR>你会发现你想“全面理解它”。事实上,你可能整天在浏览源码,搜索源码树,目的只<BR>是想搞清楚核心不同部分之间的关系。<BR> <BR>这种“沉重的搜索”是我在家里专门设一台计算机的任务之一,并且这是从源码中获取<BR>信息的一个有效的办法。然而,在坐在你喜欢的shell提示符之前若能得到一些知识基础<BR>将会很有帮助。本章基于版本2.0.x,对Linux核心源文件提供一个快速的概览。文件布<BR>局在版本之间的改变并不大,尽管我不能保证将来会不会变。因此下面的信息对浏览核<BR></P></FONT><FONT
color=#ffffff size=3>
<P>心的其它版本应该也很有用,即使它不是权威。<BR> <BR>在本章中,每个给出的路径名都是相对于源码的根(通常是/usr/src/linux),而没有<BR>目录部分的文件名一般假设它居于“当前”目录----即正在讨论的哪个。头文件(当以<BR>角括弧的形式命名时----<和>)是相对于源码树的include目录给出的。我不想介绍Docu<BR>mantation目录,因为它的作用应该很清楚。<BR> <BR> <BR> <BR>引导核心<BR> <BR>看一个程序的一般方法是从执行开始的地方着手。至于Linux,很难说执行是从那里开始<BR>的----它取决于你是如何定义“开始”的。<BR> <BR>体系结构相关的开始点是start_kernel,在init/main.c中。这个函数是从体系结构特定<BR>的代码中被调用的,但它并不返回到那里。它掌管着转动轮子,因此可以被认为是“所<BR>有函数的母亲”,计算机生命的第一次呼吸。在start_kernel之前是一片混沌。<BR> <BR>在start_kernel被调用时,处理器已经被初始化了,保护模式(如果有)也被激活了,<BR>处理器在最高的优先级执行(通常被称为“管理员模式”),中断被关闭了。start_ker<BR>nel函数负责初始化所有的核心数据结构。这个通过调用外部函数来执行子任务,因为每<BR>个设置函数都在合适的核心子系统定义。start_kernel也调用parse_options(也在init/<BR></P></FONT><FONT
color=#ffffff size=3>
<P>main.c文件中)来对从用户或引导系统的程序处传来的命令行进行解码。<BR> <BR>命令行(与memory_start,memory_end一道)用setup_arch从计算机内存中获取。setup<BR>_arch,如它的名字提示,是体系结构特定的代码。<BR> <BR>init/main.c中的代码主要由#ifdefs组成。这是因为初始化是按步发生的,很多步可能<BR>被运行或跳过,这依赖于核心的编译时配置。命令行的解释也严重地依赖于条件,因为<BR>很多参数只有在被编译的核心含有特定的驱动程序时才有意义。<BR> <BR>由start_kernel调用的初始化函数有两种风格。一些函数没有参数,返回void;而另一<BR>些需要两个unsigned long参数,并返回另一个unsigned long值。其参数是memory_star<BR>t和 memory_end的当前值,即未分配的物理内存的边界。返回值是新的memory_start值<BR>(如你所已知的,核心用unsigned long表达内存地址)。这个技术允许子系统在物理内<BR>存的开始处分配一个持续的(和连续的)内存区域,如在第七章“把握内存”中“playi<BR>ng dirty”一节中提到的。这种技术的最大的缺点是它只能在引导时使用,对那些需要<BR>用于DMA的巨大内存区段的模块并不可用。<BR> <BR>初始化完成后,start_kernel打印出旗帜字符串,包括Linux版本号和编译时间,接着通<BR>过调用kernel_thread派生出(fork)一个init进程。<BR> <BR>start_kernel函数接着以任务0(所谓的“空闲”任务)的形式继续,并调用cpu_idle,<BR>它是一个调用idle的无限循环。在这一点,SMP(对称多处理器)的工作方式略有不同,但<BR></P></FONT><FONT
color=#ffffff size=3>
<P>我不打算讲述这个不同。idle函数的真正行为是体系结构相关的,对源码的简单搜索可<BR>以把你带到可以研究其功能的位置。<BR> <BR> <BR> <BR>引导之前<BR> <BR>在前一节,我把start_kernel看作第一个核心函数。不过,你可能对这点之前发生的事<BR>情感兴趣。<BR> <BR>在start_kernel之前运行的代码是低级的,包含汇编码,因此你可能对其细节不感兴趣<BR>。不过,我将介绍一下固件(在PC世界称为BIOS)将控制交给Linux后计算机中发生了什<BR>么。<BR> <BR>如果你对钻研低级代码没有兴趣,你可以直接跳到“Init进程”。下面提供了关于Intel<BR>,Alpha,Sparc引导代码的一些提示,因为这是我能访问的仅有的系统。(如果有人肯<BR>捐一些硬件,我将在下一版覆盖更多的平台)。<BR> <BR> <BR> <BR>设置X86处理器<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P>个人计算机是基于一个老的设计,后向兼容性一直有很高的优先级。因此,PC固件还是<BR>以一种老方式引导操作系统。一旦引导设备被选择,它的第一个扇区被加载到内存的0x7<BR>C00处,然后让出控制。<BR> <BR>刚加电的处理器处于实模式(也就是说,它象8086)并只能寻址物理内存的前640KB。其<BR>中一部分已经被固件管理的数据表格占用了。由于核心要比这个大,Linux的开发者必须<BR>找到一个不一般的方法将核心影响加载到内存。结果就是zImage,即核心的压缩映象,<BR>它可以被装入底端内存(但愿如此),并在进入保护模式后自解压缩到高端内存。<BR> <BR>这样引导扇区发现它面对着五百字节的代码,和半兆字节的空闲内存。引导代码真正做<BR>的依赖于系统是如何引导的。引导扇区可以是第一个核心扇区(如果你直接从软盘上引<BR>导zImage)或者lilo。如果Linux由loadlin引导,则没有引导扇区什么事,因为在loadl<BR>in运行时,系统已经被引导了。<BR> <BR> <BR> <BR>引导一个bare-bones zImage核心<BR> <BR>如果被引导的系统是软盘上的核心映象,在引导扇区执行的代码是arch/i386/boot/boot<BR>..S(一个实模式的汇编文件)。它将自己移到地址0x90000,从可引导设备上加载另外几<BR>个扇区,把它们放在紧挨自己的后面(也就是0x90200)。接着核心映象的其余部分被加<BR>载到地址0x10000(64KB:固件数据空间之后)。<BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>位于0x90200的代码是所谓的“设置”代码(arch/i386/boot/setup.S和arch/i386/boot<BR>/video.S),它负责各种硬件的初始化,以及对视频板子的初步检测,以便可以切换到<BR>不同的文本模式分辨率。这些任务在实模式中进行(使用loadlin时则是在VM86模式),<BR>因此可以使用BIOS调用,避免处理硬件特定的细节。<BR> <BR>setup.S接着把整个核心从0x10000(64KB)移到0x1000(4KB);这样在核心代码之前只有一<BR>页被浪费了----这页其实并没有真的浪费;它在系统中自有它的用处。代码的这种来回<BR>复制是为了摆脱被BIOS强加的内存布局,还能不至于覆盖重要的数据。最后setup.S进入<BR>保护模式,跳转到0x1000。<BR> <BR>arch/i386/boot/compressed/head.S(用gas写成,因为我们已经在保护模式了)设置栈。<BR>接着调用decompress_kernel,它把已解开的代码放在地址0x100000(1M)并跳转到那里。<BR> <BR>arch/i386/kernel/head.S是被解压缩核心的头;它建立最后的处理器设置(与硬件换页<BR>有关的寄存器处理)并调用start_kernel。这就是所有需要的----已经完成了。<BR> <BR> <BR> <BR>引导一个bare-bones bzImage核心<BR> <BR>随着越来越多的驱动程序为Linux核心开发出来,一个全特征的压缩核心不再能放入低端<BR></P></FONT><FONT
color=#ffffff size=3>
<P>内存。例如,这种情况对安装核心就可能发生,因为它为了能适应各种配置,塞满了不<BR>同的驱动程序。因此,必须设计另一种加载方法。bzImage就是大的zImage,它在不能放<BR>入低端内存时也可以被加载。<BR> <BR>有几种加载bzImage的办法,这取决于使用的引导加载程序(boot loader)。核心负责<BR>每种情况,现在我打算从原始软盘上是如何引导的。<BR> <BR>一个bzImage核心的引导扇区不能简单地将所有的压缩数据加载到低端内存,所以它必须<BR>欺骗(如多数实模式x86程序所做的那样)。如果被加载的映象是大的,引导扇区象往常<BR>一样加载“设置”扇区,但在主引导循环的每次叠代都有一个“助手”例程被调用。助<BR>手例程在setup.S中定义,因为引导扇区太小无法放下它。这个例程用一个BIOS调用将数<BR>据从低端移到高端内存,一次移动64KB,它还要重置目的地址,引导扇区用来从盘上传<BR>送下一次的数据。这样,在bootsect.S中的一般加载例程就不会用尽低端内存。<BR> <BR>在核心被加载后,setup.S象往常一样被调用。它除了改变上一个跳转指令的目的地址外<BR>,并不做任何特殊的工作。由于我们加载了一个大映象,处理器通过使用一台特殊的机<BR>器指令(它允许386在实模式段使用32位偏移)跳到0x100000而不是0x1000。<BR> <BR>解压缩和往常一样工作,但输出不能放在0x100000(1M),因为压缩的映象已经在那儿<BR>了。解压的数据被写到低端的内存直到用尽;接着被写到越过压缩映象的地方。这两个<BR>解压的片段通过执行另外的内存移动在0x100000处装配起来。但复制例程也居于高端内<BR>存,它必须首先将自己复制到低端内存已防止被覆盖;然后它把整个映象移到0x100000<BR></P></FONT><FONT
color=#ffffff size=3>
<P>。<BR> <BR>到这儿,游戏就结束了。但kernel/head.S并没有注意到发生的额外工作,所有事情照常<BR>进行。<BR> <BR> <BR> <BR>使用lilo<BR> <BR>lilo,Linux加载程序,居于引导扇区----或者是主引导扇区,或者是磁盘分区的第一个<BR>扇区。它使用BIOS调用从一个文件系统中加载核心。<BR> <BR>这个程序与核心映象面对同样的问题:在机器引导时,仅有半KB的代码被装入内存,而<BR>且只用几打的指令解码一个文件系统结构也是不可能的。lilo通过在安装时构造一个磁<BR>盘映射来解决这个问题。它用这个映射告诉BIOS从正确的地方获取每个核心块。这个技<BR>术很有效,但你在替换或者重写一个核心映象后必须重新安装lilo----你必须调用lilo<BR>命令,用一个新的核心块表来重新安装引导加载程序。<BR> <BR>实际上,lilo扩展了加载机制,它允许用户在引导时选择加载哪个映象。这个选择是通<BR>过一个映象的安装定义表来做到的。它用从不同的分区中取出的引导扇区代替它自己的<BR>引导扇区来实现。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P>lilo比一个barebone引导的最大好处(除了能从硬驱直接引导外)是它允许用户象核心<BR>传递一个命令行。这个命令行可以在lilo配置文件中指定,也可以在引导时交互给出。l<BR>ilo把命令行放在零页(我们将其在boot/head.S之前保持空闲)的后一半。这一页以后<BR>由setup_arch(在arch/i386/kernel/setup.c中定义)取得。<BR> <BR>lilo的最近版本(18版本甚至更新)可以加载bzImage,而老的发布是不能的。较新的版<BR>本可以用BIOS调用将数据加载到高端内存,象bootsect.S做的那样。<BR> <BR>当lilo完成加载,它跳到setup.S,事情就象我们以前看到的那样继续进行。<BR> <BR> <BR> <BR>使用loadlin<BR> <BR>loadlin用来将Linux从一个实模式操作系统中引导起来。与lilo类似,都是加载数据,<BR>传递命令行,跳至setup.S。但它有一个优点就是它可以在FAT分区中从一个指定的文件<BR>名加载核心,而不需要一个块的映射。这使得它比较稳定。如果你想加载bzImage,你需<BR>要loadlin的版本1.6或更新*。有趣的是注意到loadlin可能需要玩一些脏活才能加载整<BR>个核心,同时又不至于搞乱宿主操作系统。只有在核心的所有部分都被加载了,loadlin<BR>才能在合适的地址重新装配它,并调用它的入口点。<BR> <BR>其它引导方式<BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>还有一些程序可以引导Linux核心。其中的两个是Etherboot和syslinux,当然还有很多<BR>。不过我不打算在这里讲述它们,因为它们与我已经讲过的类似,至少与核心相关的部<BR>分如此。<BR> <BR>但要注意,引导一个Linux核心并不是象我说的这么简单。要进行大量的检测,版本号经<BR>常出现在特别的地方,以抓住用户的错误,并友好的回复。意思是如果发生了什么问题<BR>,系统可以在挂起前打印一条信息。局限在x86实模式的执行环境下很难完全避免发生错<BR>误时挂起,打印一条消息总比什么都没有强。<BR> <BR> <BR> <BR>设置Alpha处理器<BR> <BR>让一个Alpha到达能运行start_kernel这一点要比Intel处理器容易的多,因为在Alpha上<BR>不需要和实模式或内存限制做斗争。而且,Alpha工作站通常配有比PC好的固件,可以从<BR>文件系统装载一个完整的文件。我不想讨论装载一个文件时的实际步骤,因为这个代码<BR>没有随Linux发布,这样你无法检查它----我也不能,因此就无法谈论它。<BR> <BR>milo(迷你加载程序)程序是引导的一般选择。milo比固件要聪明,因为它理解Linux和<BR>它的文件系统,但又比核心笨,因为它不能运行进程。milo由固件从FAT分区执行,可以<BR>从ext2或ISO9660块设备上加载核心。象lilo和loadlin,milo也向核心传递一个命令行<BR></P></FONT><FONT
color=#ffffff size=3>
<P>。在Linux被加载到内存中正确的虚地址后,milo转向核心,自己消失。<BR> <BR>milo的有些特征依赖于核心源码,因为它需要访问设备,理解文件系统布局。配有驱动<BR>程序和文件系统类型,它可以根据文件名从硬盘或CD-ROM上取得核心映象。这个设计后<BR>面的想法与loadlin类似,只是milo使用Linux核心的代码,而不是取自别的操作系统环<BR>境。<BR> <BR>在Alpha上引导Linux并不总是可用milo。如果你的系统有SRM固件,就不能安装milo。相<BR>反,你可以使用arch/alpha/boot中的原始加载程序。这个加载程序很简单,能从硬盘或<BR>软驱中读取一个顺序区域,这与PC上zImage前面的引导扇区所做的工作一样。使用原始<BR>加载程序要求核心映象必须(在任何文件系统之外)被复制到磁盘上的连续区域。<BR> <BR>如果不考虑系统是如何引导的,控制被传递给arch/alpha/kernel/head.S,但Linus说:<BR>“没什么需要我们做的了”。源码只是设置几个指针,然后就跳到start_kernel。<BR> <BR> <BR> <BR>设置Sparc处理器<BR> <BR>Sparc计算机用一个称为silo的程序引导Linux。与lilo,milo命名方法类似,只是用“s<BR>”表示Sparc。引导Sparc比Alpha要简单一些;它的固件可以访问设备,silo只需要访问<BR>Linux文件系统,并与用户交互。出于这个目的,silo被链接到libext2,这是支持对未<BR></P></FONT><FONT
color=#ffffff size=3>
<P>安装分区上的文件进行处理的一个库。<BR> <BR>若不使用silo,也可以从软盘或网络上引导计算机。固件可以用RARP(反向ARP)和tftp<BR>协议从以太网上装载一个核心。事实上,我从未用软盘引导过我自己的工作站,因为Lin<BR>ux的Sparc发布允许通过网络引导来完成系统安装。<BR> <BR>对Sparc来说,的确没有什么特别的要求。没有实模式,也没有需要复制的内存。一旦核<BR>心被加载到RAM,它便开始执行。<BR> <BR> <BR> <BR>Init进程<BR> <BR>由start_kernel生成的线程派生出bdflush(源码见fs/buffer.c)和kswapd(在mm/vmsc<BR>an.c中定义),它们因此被赋予进程号2和3。init进程(pid 1)接着进行进一步的初始<BR>化,这在之前不可能完成;也就是,它运行与SMP相关的函数,如果需要,还有initrd引<BR>导技巧,以另一个核心线程的形式。在initrd结束后,init线程激活UMSDOS文件系统的<BR>“伪根(pseudo-root)”。<BR> <BR>在完成初始化后,init的实际作用是进入用户空间并执行一个程序(因此变成一个进程<BR>)。这样三个stdio通道被连到第一个虚拟控制台,核心试图从/etc执行init。如果失败<BR>,它将查看/bin和/sbin(在所有最近的发布中,init一定居于此)。如果init从这三个<BR></P></FONT><FONT
color=#ffffff size=3>
<P>目录的执行都失败了,进程将会执行/etc/rc,如果这个也失败了,它就循环,执行/bin<BR>/sh。在大多数情况下,函数能运行init成功;其它选项的目的是为了在init不能执行时<BR>允许系统恢复。<BR> <BR>如果核心命令行指定了一条要执行的命令,使用init=some_program导语,进程1就执行<BR>指定的命令,而不是调用init。<BR> <BR>不管系统是怎么设置的,init最终在用户空间执行,以后的核心操作都是对来自用户程<BR>序的系统调用的响应。<BR> <BR> <BR> <BR>kernel目录<BR> <BR>大多数关键的核心功能都是在这个目录实现的。这里最重要的源文件是sched.c,它值得<BR>特别对待。<BR> <BR> <BR> <BR>sched.c<BR> <BR>正如源文件自己表明的,这是“主核心文件”。它由调度程序和相关操作组成,例如让<BR></P></FONT><FONT
color=#ffffff size=3>
<P>进程睡眠和唤醒它们,以及核心计时器的管理(见第六章“时间流”中“核心计时器”<BR>一节),间隔计时器(它与计帐和性能刻划有关),以及预定义任务队列(见第六章“<BR>预定义任务队列”)。<BR> <BR>如果你对Linux调度程序的实时策略感兴趣,你可以在schedule函数及其相关者中找到低<BR>级的信息。其中一个相关者是goodness,它给进程赋优先值,并帮助调度程序选择下一<BR>个要运行的进程。<BR> <BR>与调度程序控制相关的函数(及系统调用)也在这个文件中定义。这包括设置和取得调<BR>度策略及优先级。在除Alpha外的其它体系结构上,系统调用nice也在这个源文件中。<BR> <BR>另外,取得和设置用户及组id也在sched.c中定义(除了Alpha),同时还有alarm调用*<BR>。<BR> <BR>在sched.c中还能找到的其它好东西包括show_tasks和show_state函数,它们实现了在第<BR>四章“调试技巧”中“系统挂起”一节所描述的“魔幻”键中的两个。<BR> <BR> <BR> <BR>进程控制<BR> <BR>目录的其它主要部分都是管理进程的。fork和exit系统调用在两个同名源文件中实现,<BR></P></FONT><FONT
color=#ffffff size=3>
<P>信号控制在signal.c中实现。大多数信号处理的调用在Alpha中实现的方法是不同的,以<BR>保证Alpha的移植与Digital Unix二进制兼容。<BR> <BR>fork的实现包括clone系统调用的代码,fork.c显示了clone的标志是如何使用的。应该<BR>注意sys_fork并不在fork.c中定义,因为Sparc的实现与其它的版本稍有不同;不过,多<BR>数sys_fork的实现只是调用do_fork,它在fork.c中定义。提供一个缺省实现(通常叫做<BR>do_fnct),而真正的系统调用(sys_fnct)则在各个移植中声明,这是Linux常用的一<BR>个技巧,随着新的移植的出现,这个技巧很可能扩展到其它的系统调用。<BR> <BR>exit.c实现sys_exit和不同的wait函数,以及信号的实际发送。(signal.c专用于信号<BR>处理,而不是发送。)<BR> <BR> <BR> <BR>模块化<BR> <BR>文件module.c和ksyms.c包含了在第二章“构造和运行模块”中描述的机制。module.c含<BR>有被insmod及相关程序使用的系统调用,ksyms.c声明不属于特定子系统的核心中的公共<BR>符号。其它的公共符号由特定核心子系统的初始化函数使用register_symtab声明。例如<BR>,fs/proc/procfs_syms.c为注册新文件声明/proc接口。<BR> <BR> <BR></P></FONT><FONT
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -