﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>IT博客-【Z&amp;Y】幸福小筑-文章分类-Linux源码分析</title><link>http://www.cnitblog.com/drizztzou/category/164.html</link><description>                          天道酬勤
Now its the time for me to work hard for my girl!</description><language>zh-cn</language><lastBuildDate>Mon, 26 Sep 2011 05:08:19 GMT</lastBuildDate><pubDate>Mon, 26 Sep 2011 05:08:19 GMT</pubDate><ttl>60</ttl><item><title>Linux内核编程</title><link>http://www.cnitblog.com/drizztzou/articles/745.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Tue, 05 Jul 2005 01:36:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/745.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/745.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/745.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/745.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/745.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: 瀚海星云 -- Linux精华区文章阅读 发信人: kingpaul (劫机男孩), 信区: Linux标  题: Linux内核编程(转载)发信站: 云水逍遥站 (2002年05月02日21:18:20 星期四), 站内信件【 以下文字转载自 Kernel 讨论区 】【 原文由 kingpaul 所发表 】Linux...&nbsp;&nbsp;<a href='http://www.cnitblog.com/drizztzou/articles/745.html'>阅读全文</a><img src ="http://www.cnitblog.com/drizztzou/aggbug/745.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-05 09:36 <a href="http://www.cnitblog.com/drizztzou/articles/745.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>内核启动详解</title><link>http://www.cnitblog.com/drizztzou/articles/732.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:39:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/732.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/732.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/732.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/732.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/732.html</trackback:ping><description><![CDATA[第一部分 背景知识简介<BR><BR>　　几乎所有编写代码的人都有这种体会：如今在计算机这个行业中，许多技术不是你不懂，而是你不知道。所以，在分析之前有些背景知识是必须要知道的。<BR><BR>　　一. 硬盘结构简介<BR><BR>　　1. 硬盘参数释疑<BR><BR>　　到目前为止, 人们常说的硬盘参数还是古老的 CHS (Cylinder/Head/Sector)参数. 那么为什么要使用这些参数, 它们的意义是什么? 它们的取值范围是什么?<BR>　　很久以前, 硬盘的容量还非常小的时候, 人们采用与软盘类似的结构生产硬盘，也就是硬盘盘片的每一条磁道都具有相同的扇区数，由此产生了所谓的3D参数 (Disk Geometry)。既磁头数(Heads), 柱面数(Cylinders), 扇区数(Sectors)，以及相应的寻址方式。<BR><BR>　　其中:<BR>　　磁头数(Heads) 表示硬盘总共有几个磁头,也就是有几面盘片, 最大为 255 (用 8 个二进制位存储)；<BR>　　柱面数(Cylinders) 表示硬盘每一面盘片上有几条磁道, 最大为 1023(用 10 个二进制位存储)；<BR>　　扇区数(Sectors) 表示每一条磁道上有几个扇区, 最大为 63 (用 6个二进制位存储)；<BR>　　每个扇区一般是 512个字节(理论上讲这不是必须的， 但好象都取此值)。<BR><BR>　　据此，磁盘最大容量为:<BR>　　255 * 1023 * 63 * 512 / 1048576 = 8024 MB ( 1M = 1048576 Bytes )<BR>　　或硬盘厂商常用的单位:<BR>　　255 * 1023 * 63 * 512 / 1000000 = 8414 MB ( 1M = 1000000 Bytes )<BR><BR>　　在 CHS 寻址方式中, 磁头, 柱面, 扇区的取值范围分别为 0 到 Heads - 1,0 到 Cylinders - 1, 1 到 Sectors (注意是从 1 开始)。<BR><BR>　　2. 基本 Int 13H 调用简介<BR><BR>　　BIOS Int 13H调用是 BIOS 提供的磁盘基本输入输出中断调用, 它可以完成磁盘(包括硬盘和软盘)的复位, 读/写, 校验, 定位, 诊断, 格式化等功能。它使用的就是 CHS 寻址方式, 因此最大只能访问 8 GB 左右的硬盘 ( 本文中如不作特殊说明, 均以 1M = 1048576 字节为单位).<BR>　　而更不幸的是，标准的IDE接口容许256个扇区／磁道、65536个柱面及16个磁头。它自己本身可以存取 137438953472(128 GB)，但是加上BIOS方面63个扇区与1024个柱面的限制后，就只剩528482304(1024*16*63 = 504MB)可以定址得到，这就是所谓标准IDE硬盘只认前504MB问题。<BR><BR>　　3. 现代硬盘结构简介<BR><BR>　　在老式硬盘中, 由于每个磁道的扇区数相等 (与软盘一样), 所以外道的记录密度要远低于内道, 因此会浪费很多磁盘空间。为了解决这一问题, 进一步提高硬盘容量, 人们改用等密度结构生产硬盘， 也就是说, 外圈磁道的扇区比内圈磁道多。采用这种结构后, 硬盘不再具有实际的3D参数, 寻址方式也改为线性寻址, 即以扇区为单位进行寻址。<BR>　　为了与使用3D寻址的老软件兼容 (如使用BIOS Int13H接口的软件), 在硬盘控制器内部安装了一个地址翻译器, 由它负责将老式3D参数翻译成新的线性参数。这也是为什么现在硬盘的3D参数可以有多种选择的原因 (不同的工作模式对应不同的3D参数, 如 LBA, LARGE, NORMAL)。<BR><BR>　　4. 扩展 Int 13H 简介<BR><BR>　　虽然现代硬盘都已经采用了线性寻址, 但是由于基本 Int 13H 的制约, 使用 BIOS Int 13H 接口的程序, 如 DOS 等还是只能访问 8 G 以内的硬盘空间。为了打破这一限制, Microsoft 等几家公司制定了扩展 Int 13H 标准(Extended Int13H，详见附录A), 采用线性寻址方式存取硬盘，所以突破了 8 G 的限制,而且还加入了对可拆卸介质 (如活动硬盘) 的支持。<BR><BR>　　二. Boot Sector 结构简介<BR><BR>　　1. Boot Sector 的组成<BR><BR>　　Boot Sector 也就是硬盘的第一个扇区, 它由 MBR (Master Boot Record)，DPT (Disk Partition Table) 和 Boot Record ID(Magic Number) 三部分组成。<BR><BR>　　MBR 又称作主引导记录，占用 Boot Sector 的前 446 个字节 ( 0 to 0x1BD ),包含了硬盘的一系列参数和一段系统主引导程序。引导程序主要是用来在系统硬件自检完后负责从活动分区中装载并运行系统引导程序(引导操作系统)。它的最后一条执行语句是一条JMP指令,跳到操作系统的引导程序去。这里往往是引导型病毒的注入点，也是各种多系统引导程序的注入点。但是由于引导程序本身完成的功能比较简单，所以我们完全可以判断该引导程序的合法性（比如看JMP指令的合法性），因而也易于修复。象命令fdisk/mbr可以修复MBR和 KV300这类软件可以查杀任意类型的引导型病毒，就是这个道理。<BR>　　DPT 即主分区表，占用 64 个字节 (0x1BE to 0x1FD),记录了磁盘的基本分区信息。主分区表分为四个分区项, 每项 16 字节, 分别记录了每个主分区的信息(因此最多可以有四个主分区)。<BR>　　Boot Record ID 即引导区标记，占用两个字节 (0x1FE and 0x1FF), 对于合法引导区, 它等于 0xAA55, 这是判别引导区是否合法的标志.<BR>　　Boot Sector 的具体结构如下图所示: 见附件<BR><BR>　　<BR><BR>　　2. 主分区表的结构<BR><BR>　　主分区表由四个分区项构成, 每一项的结构如下:<BR><BR>　　BYTE State : 分区状态, 0 = 未激活, 0x80 = 激活 (注意此项)<BR>　　BYTE StartHead : 分区起始磁头号<BR>　　WORD StartSC : 分区起始扇区和柱面号, 低字节的低6位为扇区号,高2位为柱面号的第 9,10 位, 高字节 为柱面号的低 8 位<BR>　　BYTE Type : 分区类型, 如 0x0B = FAT32, 0x83 = Linux 等, 00 表示此项未用<BR>　　BYTE EndHead : 分区结束磁头号<BR>　　WORD EndSC : 分区结束扇区和柱面号, 定义同前<BR>　　DWORD Relative : 在线性寻址方式下的分区相对扇区地址 (对于基本分区即为绝对地址)<BR>　　DWORD Sectors : 分区大小 (总扇区数)<BR><BR>　　注意：在 DOS / Windows 系统下, 基本分区必须以柱面为单位划分( Sectors * Heads 个扇区), 如对于 CHS 为 764/255/63 的硬盘, 分区的最小尺寸为 255 * 63 * 512 / 1048576 = 7.844 MB。<BR><BR>　　3. 扩展分区简介<BR><BR>　　由于主分区表中只能分四个分区, 有时无法满足需求, 因此设计了一种扩展分区格式。 基本上说, 扩展分区的信息是以链表形式存放的, 但也有一些特别的地方。<BR>　　首先，主分区表中要有一个基本扩展分区项, 所有扩展分区都隶属于它,也就是说其他所有扩展分区的空间都必须包括在这个基本扩展分区中。 对于DOS / Windows 来说, 扩展分区的类型为 0x05。<BR>　　除基本扩展分区以外的其他所有扩展分区则以链表的形式级联存放, 后一个扩展分区的数据项记录在前一个扩展分区的分区表中, 但两个扩展分区的空间并不重叠。<BR>　　扩展分区类似于一个完整的硬盘, 必须进一步分区才能使用。但每个扩展分区中只能存在一个其他分区, 此分区在 DOS/Windows 环境中即为逻辑盘。因此每一个扩展分区的分区表 (同样存储在扩展分区的第一个扇区中)中最多只能有两个分区数据项(包括下一个扩展分区的数据项)。<BR>　　扩展分区和逻辑盘的示意图如下： 见附件<BR>　　<BR>　　<BR>　 三. 系统启动过程简介<BR><BR>　　系统启动过程主要由一下几步组成(以硬盘启动为例)：<BR><BR>　　1. 开机；<BR>　　2. BIOS 加电或按reset键后都要进行系统复位，复位后指令地址为 0ffff:fff0,这个地方只有一条JMP指令, 跳转到系统自检 ( Power On Self Test -- POST )程序处；<BR>　　3. 系统自检完成后,将硬盘的第一个扇区 (0头0道1扇区, 也就是Boot Sector)读入内存地址 0000:7c00 处；<BR>　　4. 检查 (WORD) 0000:7dfe 是否等于 0xaa55, 若不等于则转去尝试其他启动介质, 如果没有其他启动介质 则显示 "No ROM BASIC" 然后死机；<BR>　　5. 跳转到 0000:7c00 处执行 MBR 中的程序；<BR>　　6. MBR程序 首先将自己复制到 0000:0600 处, 然后继续执行；<BR>　　7. 在主分区表中搜索标志为活动的分区，如果没有发现活动分区或有不止一个活动分区, 则转停止；<BR>　　8. 将活动分区的第一个扇区读入内存地址 0000:7c00 处；<BR>　　9. 检查 (WORD) 0000:7dfe 是否等于 0xaa55, 若不等于则 显示 "Missing Operating System" 然后停止, 或尝 试软盘启动或；<BR>　　10. 跳转到 0000:7c00 处继续执行特定系统的启动程序；<BR>　　11. 启动系统...<BR><BR>　　以上步骤中 2,3,4,5 步是由 BIOS 的引导程序完成. 6,7,8,9,10步由MBR中的引导程序完成.<BR><BR>　　一般多系统引导程序 (如 SmartFDISK, BootStar, PQBoot 等)都是将标准主引导记录替换成自己的引导程序, 在运行系统启动程序之前让用户选择要启动的分区。<BR>　　而某些系统自带的多系统引导程序 (如 lilo, NT Loader 等)则可以将自己的引导程序放在系统所处分区的第一个扇区中, 在 Linux中即为 SuperBlock (其实 SuperBlock 是两个扇区)。<BR><BR>　　注：以上各步骤中使用的是标准 MBR, 其他多系统引导程序的引导过程可能与此不同。<BR><BR>　　下面简要说明一下系统复位后的指令地址0ffff:fff0(物理地址0x0fffffff0)：<BR>　　在实地址模式下，内存有两个保留区域：系统初始化区和中断向量表区。地址0x00000~0x003ff 是为中断向量保留的，256个可能的中断，每一个保留4字节的跳转向量；地址0xfffffff0~0xffffffff是为系统初始化保留的，此处一般只有一条JMP指令，跳到系统初始引导程序。<BR>　　系统复位后，cs = 0x0f000、eip = 0x0000fff0，而系统初始引导程序安排在 0x0ffff0000~0x0ffffffff, 一般为ROM固件，使初始引导程序工作于内存实际地址空间以外的另一存储段中。此区域的16~31位都应为1，cs及eip的初值已保证地址线的16~19位为1，而20~31位，即地址线的高12位，则须由硬件强制置 1，这由一个标志触发器在系统每次复位时置位实现触发。而由于段间转移指令要重新装入cs寄存器，因此，每当执行段间转移指令时，此标志触发器复位，以后，再次访问时，不再向高12位地址线提供“1”信号，程序从此正常地工作于前1MB的地址空间。<BR><BR><BR>　　第二部分 硬盘MBR主引导代码分析<BR><BR>　　一.程序流程<BR><BR>　　（引导扇区是指硬盘相应分区的第一个扇区，是和操作系统有关的，操作系统的引导是由它来完成的；而MBR主引导程序并不负责引导操作系统，MBR是和操作系统无关的，他的任务是把控制权转交给操作系统的引导程序.）<BR><BR>　　1 将程序代码由0:7C00H移动到0:0600H（注，BIOS把MBR放在0:7C00H处）<BR>　　2 搜索可引导分区，即80H标志<BR>　　成功：goto 3<BR>　　失败：跳入ROM BASIC<BR>　　无效分区表：goto 5<BR>　　3 读引导扇区<BR>　　失败：goto 5<BR>　　成功：goto 4<BR>　　4 验证引导扇区最后是否为55AAH<BR>　　失败：goto 5<BR>　　成功：goto 6<BR>　　5 打印错误进入无穷循环<BR>　　6 跳到0:7C00H进行下一步启动工作<BR><BR>　　二.代码注释<BR><BR>　　下面将用汇编语言写出这一段代码，并进行说明。<BR><BR>;MBR.ASM<BR>; MASM MBR<BR>; LINK MBR<BR>; EXE2BIN MBR<BR><BR>.MODEL tiny<BR>.CODE<BR><BR>　　;设置寄存器及堆栈值<BR><BR>org 0<BR>Head:<BR>Start:<BR>cli<BR>xor ax,ax<BR>mov ss,ax<BR>mov sp,7C00H ;ss:sp=0:7C00H<BR>mov si,sp<BR>push ax<BR>pop es<BR>push ax<BR>pop ds ;es=ds=0<BR>sti<BR><BR>　　;将程序代码由0:7C00H移动到0:0600H处<BR><BR>cld<BR>mov di,600H<BR>mov cx,100H ;100H Words=512 Bytes，即一个扇区大小<BR>repne movsw<BR>db 0EAH ;这个是FAR JUMP的机器码<BR>dw offset Continue+600H, 0000H ;这个是跳转目的地址，即0:061DH<BR><BR>　　;搜索可引导分区<BR><BR>Continue:<BR><BR>mov si,600H+1BEH ;si指向分区表<BR>mov bl,4 ;四个分区<BR><BR>FindBoot:<BR><BR>cmp byte ptr[si],80H<BR>je SaveRec ;读扇区位置<BR>cmp byte ptr[si],0<BR>jne Invaild ;无效分区<BR>add si,10H<BR>dec bl<BR>jnz FindBoot<BR>int 18H ;进入ROM BASIC<BR><BR>　　;读取引导分区的扇区，柱面号<BR><BR>SaveRec:<BR><BR>mov dx,[si]<BR>mov cx,[si+2]<BR>mov bp,si<BR><BR>　　;检查其余分区表<BR><BR>FindNext:<BR><BR>add si,10H<BR>dec bl<BR>jz SetRead<BR>cmp byte ptr[si],0 ;是否存在非法分区<BR>je FindNext<BR><BR>Invaild:<BR><BR>mov si,offset ErrMsg1+600H<BR><BR>　　;字符串输出子程序<BR><BR>PrintStr:<BR><BR>lodsb<BR>cmp al,0<BR>je DeadLock<BR>push si<BR>mov bx,7<BR>mov ah,0EH ;输出字符<BR>int 10H<BR>pop si<BR>jmp short PrintStr ;下一字符<BR><BR>DeadLock:<BR><BR>jmp short DeadLock ;无穷循环，也可以写成jmp $<BR><BR>　　;读引导扇区<BR><BR>SetRead:<BR><BR>mov di,5 ;读取次数<BR><BR>ReadBoot:<BR><BR>mov bx,7C00H<BR>mov ax,201H<BR>push di<BR>int 13H ;cx,dx已经在SaveRec处得到<BR>pop di<BR>jnc GoBoot ;成功则启动<BR>xor ax,ax<BR>int 13H ;reset驱动器，然后再读取<BR>dec di<BR>jnz ReadBoot<BR><BR>mov si,offset ErrMsg2+600H<BR>jmp short PrintStr 失败输出信息，并进入无穷循环<BR><BR>　　;检查读入的引导扇区<BR><BR>GoBoot:<BR><BR>mov si,offsetErrMsg3+600H<BR>mov di,7C00H+1FEH<BR>cmp word ptr[di],0AA55H<BR>jne PrintStr ;非AA55标志则输出错误信息<BR>mov si,bp ;si指向可启动分区<BR>db 0EAH,0,7CH,0,0 ;跳转至0:7C00H<BR><BR>ErrMsg1 db 'Invaild partition table',0<BR>ErrMsg2 db 'Error loading operating system',0<BR>ErrMsg3 db 'Missing operating system',0<BR><BR>Tail:<BR><BR>FillNum equ 1BEH-(Tail-Head) ;计算填0数目<BR>db FillNum dup(0)<BR><BR>　　;四个分区表项数据，跟分区情况有关，详细含义另解<BR><BR>PartTable db 80H,1,1,0,4,4,0D1H,2,11H,0,0,0,0FEH,0FFH,0,0<BR>db 0,0,0C1H,3,5,4,0D1H,0FEH,0FFH,0FFH,0,0,0ACH,53H,0,0<BR>db 20H dup(0)<BR><BR>ID dw 0AA55H<BR><BR>end start<BR><BR>;如果开始试用org 600H，那么访问数据时就不必加上600H，如mov si,offset<BR>ErrMsg2+600H<BR>;可写为mov si,offset ErrMsg2，这时就不能用exe2bin得到数据，必须试用debug<BR>;debug mbr.exe<BR>;-nmbr.bin<BR>;-rcx 200<BR>;-wcs:600<BR>;-q<BR><BR>　　在硬盘的第一个扇区上保存着分区信息,即主分区表,共有四项，读取分区表必须使用bios的int 13h，一般使用debug就可以了：<BR><BR>debug<BR>-a<BR>xxxx:0100 mov ax,201<BR>mov bx,200<BR>mov cx,1<BR>mov dx,80 ;如果是第二个硬盘则是81...<BR>int 13<BR>int 20<BR>xxxx:????<BR>-g=100<BR><BR>　　这时xxxx:0200开始的512字节就是分区表所在的扇区，前面一部分为MBR，在debug中用-d3be l40就可以看到64字节的分区表信息，16个字节为一项，用-e命令就可以修改，改完后可以重新写回去，只要把前面代码中的mov ax,201改为mov ax,301即可，或者直接把102处的2改成3，比如：<BR><BR>-e 102<BR>xxxx:0102 02.3<BR>-g=100<BR><BR>　　这样就写回去了，不过修改硬盘的第一个扇区须非常谨慎。<BR><BR>　　下面说一下分区表项的具体意义，取其中一项举个例子：<BR><BR>　　80 01 01 00 0B 3F FF 00 3F 00-00 00 81 4F 2F 00<BR><BR>　　1 (80)引导标志，80代表可引导，00代表不可引导，一般必须且只能有一个分区表项的引导标志为 80，除非你自己修改MBR<BR>　　2 (01)分区开始磁头<BR>　　3,4 (01 00)=(0,1)分区开始柱面和扇区（后面后详解）<BR>　　5 (0B)分区类型（后面有详解）<BR>　　6 (3F)=(63)分区结束磁头<BR>　　7,8 (FF 00)=(768,63)分区结束柱面和扇区（同上）<BR>　　9-12 (3F 00 00 00)=(63)此分区前扇区总数，即相对扇区数<BR>　　13-16 (81 4F 2F 00)=(002F4F81H=3100545)此分区扇区总数<BR><BR>　　柱面和扇区共用两个字节表示，而柱面号为10位，最大1023，扇区号为6位，最大63，具体各位分布如下图：<BR>　　　　　　　　　　 扇区号<BR>　　　　　　　　　_____|____<BR>　　　　　　　　　 |　　　|<BR>　　( 7 6 5 4 3 2 1 0 ) ( 7 6 5 4 3 2 1 0 )<BR>　　　|__|　　　　　　　　 |___________|<BR>　　　　|___________________|<BR>　　　　　　　　　|<BR>　　　　　　　　柱面号<BR><BR>　　关于分区类型，常见的有：<BR><BR>　　00 未用，Unused<BR>　　01 DOS-12(FAT 12)<BR>　　02 XENIX<BR>　　04 DOS-16(FAT 16)（分区&lt;32M的，应该已没有了）<BR>　　05 EXTEND(DOS扩展分区)<BR>　　06 BIGDOS(&gt;32M)（这个才是现在常说的FAT 16）<BR>　　07 HPFS(OS/2)（NTFS也是这个标记，好像是）<BR>　　0B FAT 32<BR>　　0F 这个一时不确定<BR>　　50 DM<BR>　　63 386/ix(unix)<BR>　　64 NET286(Novell)<BR>　　65 NET386(Novell)<BR>　　82 Linux swap<BR>　　83 Linux native<BR>　　FF BBT(UNIX Bad Block Table)<BR><BR>　　下面有几个算式用来计算分区参数：<BR><BR>　　1）第一分区参数<BR><BR>　　扇区总数=(结束柱面+1)*磁头数*每柱面扇区数-相对扇区数，例如：3100545=(768+1)*64*63-63<BR><BR>　　2）其它分区参数<BR><BR>　　扇区总数=(结束柱面-起始柱面+1)*磁头数*每柱面扇区数，如下例：<BR>　　00 00 C1 01 05 3F FF FD C0 4F-2F 00 C0 90 0F 00<BR>　　000F90C0H=1020096，(FF FD)=(1021,63),(C1 01)=(769,1)，1020096=(1021-769+1)*64*63<BR><BR>　　3）第一分区相对扇区=每柱面扇区数<BR>　　其它分区相对扇区=上一分区相对扇区+上一分区扇区总数<BR><BR>　　扩展分区信息是一个链状结构，在删除分区时,把前面的分区删掉会导致后面的分区也找不到，原因就在于此，我们从主分区表中取出扩展分区项进行一下分析，如下：<BR><BR>　　00 00 01 C0 05 FE BF 6E C0 10-2F 00 EF A6 69 00<BR><BR>　　由此我们可以得到数据：<BR>　　开始磁头：00　<BR>　　开始柱面扇区：01 C0=(192,1)<BR><BR>　　用debug<BR><BR>　　debug<BR>　　-a100<BR>　　xxxx:0100 mov ax,201<BR>　　mov bx,200<BR>　　mov cx,c001 ;开始柱面扇区号<BR>　　mov dx,80 ;dh中为开始磁头号，这里为0<BR>　　int 13<BR>　　int 20<BR>　　xxxx:????<BR>　　-g=100<BR>　　-d3be l10<BR><BR>　　读出的扇区中有两个16字节的分区表项和最后的一个55AA标志，这两个分区表项为：<BR><BR>　　00 01 01 C0 06 FE 7F 97 3F 00-00 00 99 F2 34 00<BR>　　00 00 41 98 05 FE BF 6E D8 F2-34 00 17 B4 34 00<BR><BR>　　第一个分区类型为6，其实这是第一个逻辑分区，含义和主分区表项相同<BR>　　第二个分区类型为5，这其实是指向下一个扩展分区表的<BR>　　从这里我们可以得到：<BR>　　开始磁头：0<BR>　　开始柱面扇区：41 98=(408,1)<BR><BR>　　继续用debug读出（mov cx,9841)得到<BR><BR>　　00 01 41 98 06 FE BF 6E 3F 00-00 00 D8 B3 34 00<BR><BR>　　只有一个表项，是第二个逻辑盘，而且是逻辑盘链的最后一个。<BR>　　可以看到，主分区表是非常重要的，所以除了小心操作外，还应当进行备份，最安全的备份方式就是用笔抄下来，当然，每次重新进行分区后还应当及时更新。从前面可以看出，分区表里最重要的还是柱面号，其它比如磁头号都是0或者1或者最大值，扇区号都是1或63（最大值），扇区总数什么的也都能算出来，所以分区时最好把各分区的柱面号记下来，这样一旦分区信息被破坏就可以进行恢复了。<BR>　　如果主分区表不幸丢失或者逻辑分区链被破坏，那么只要从硬盘上找出还存在的分区信息，就有可能部分恢复分区信息，甚至全部可以恢复，不过这样的麻烦大家还是应尽量避免。<BR>　　在Linux里有一种方法可以恢复主引导扇区（包括主分区表），用如下的命令：<BR>　　dd if=/boot/boot.NNNN of=/dev/hda bs=512 count=1<BR>　　其中：boot.NNNN 是我们在安装Linux之前整个主引导扇区的备份，NNNN是分区的主次设备号；bs（buffer size)是指重写的字节数。如只是为了修复主引导记录MBR(比如，想把LILO卸载掉)，而不是恢复整个主引导扇区，则用如下的命令：<BR>　　dd if=/boot/boot.NNNN of=/dev/hda bs=446 count=1<BR>　　只把主引导扇区的备份文件boot.NNNN的前446个字节重写入主引导扇区。<img src ="http://www.cnitblog.com/drizztzou/aggbug/732.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:39 <a href="http://www.cnitblog.com/drizztzou/articles/732.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux 运行时内核分析</title><link>http://www.cnitblog.com/drizztzou/articles/728.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:33:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/728.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/728.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/728.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/728.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/728.html</trackback:ping><description><![CDATA[我们基于RH9 内核从两部分来分析Linux系统动态运行过程<BR>一：系统初始化开始，Linux进入保护模式，初始内存系统、中断系统、文件系<BR>统等，直到创建第一个用户进程。<BR>二：用户进程通过系统调用主动进入内核，CPU 接受中断请求被动执行各种中<BR>断服务。<BR>第一部分 系统初始化<BR>进入保护模式 Arch/i386/boot/Setup.s<BR>gdt:<BR>.fill GDT_ENTRY_KERNEL_CS,8,0 #空12×(8个字节=2个双字)，用0 填充。<BR>.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)<BR>.word 0 # base address = 0<BR>.word 0x9A00 # code read/exec<BR>.word 0x00CF # granularity = 4096, 386<BR># (+5th nibble of limit)<BR>.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)<BR>.word 0 # base address = 0<BR>.word 0x9200 # data read/write<BR>.word 0x00CF # granularity = 4096, 386<BR>gdt_48:<BR>.word 0x8000 # gdt limit=2048,<BR># 256 GDT entries<BR>我们进入保护模式之后选择子首先应该是0x60 = 1100000b, 右移3 位为1100 =<BR>0xC = 12 ,这就是code在GDT里的位置。当然这只是第一次进入保护模式，以<BR>后还会调整。<BR>进入分页模式 arch/i386/kernel/head.S<BR>.org 0x1000<BR>ENTRY(swapper_pg_dir)<BR>.long 0x00102007 #基地址为0x00102000,就是下面pg0,0x00100000是内核<BR>加载的地方,1M的位置<BR>.long 0x00103007<BR>.fill BOOT_USER_PGD_PTRS-2,4,0 #768-2 (long) 0<BR>/* default: 766 entries */<BR>.long 0x00102007<BR>.long 0x00103007<BR>/* default: 254 entries */<BR>.fill BOOT_KERNEL_PGD_PTRS-2,4,0 #256-2(long) 0<BR># 这个说明了内核是1G，用户就3G。<BR>.org 0x2000<BR>ENTRY(pg0)<BR>.org 0x3000<BR>ENTRY(pg1)<BR>说明映射了两个页面,一个页面代表4M, 每个pg0 里的一项代表4K。具体pg0<BR>数据是用程序填充的 参考L82。L101 通过短跳转进入分页(但eip 还不是),L104<BR>通过绝对地址完全进入分页。比如L105的地址是0xC0100058 ＝1100 0000 0001<BR>0000 0000 0000 0101 1000B,高10 位为1100 0000 00 = 0x300 = 768。从<BR>swapper_pg_dir里找768项就是0x00102007,检查之后基地址为0x00102000;就是<BR>pg0,再看0xC0100058 中间10位01 0000 0000B = 0x100 = 256,再找pg0 的256项<BR>应该为00100***,那么最后的物理地址为0x00100058,所以内核里的虚拟地址换<BR>物理地址很简单，就是0xC0100058 - 0xC0000000 = 0x00100058 就是物理地址。<BR>现在系统只是映射了8M的内存空间，之后还会调整。<BR>内存管理<BR>物理内存管理<BR>第一次探测 Arch/i386/boot/setup.S L281<BR>在这里使用了三种方法，通过调用0x15 中断：<BR>E820H 得到memory map。<BR>E801H 得到大小。<BR>88H 0-64M.<BR>第二次探测<BR>start_kernel() -&gt; setup_arch()<BR>这里主要是对物理内存的初始化，建立zone区。并且初始化page。<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory_region()<BR>把bios 里的memory map 拷贝到 e820 这个全局变量里。<BR>会在屏幕上显示<BR>BIOS-provided physical RAM map:<BR>BIOS-e820: 0000000000000000 – 000000000009FC00 (usable)<BR>BIOS-e820: 0000000000100000－0000000002000000 (usable)<BR>BIOS-e820 说的是通过e820 来读取成功的数据,第一个代表起始地址,后面接<BR>着的是大小。usable 说明内存可用。还有 reserved, ACPI data, ACPI NVS 等。<BR>第二条说明起始地址为0x100000 = 1M,大小是0x2000000 = 32M 内存。<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory ()<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory ()-&gt;find_max_pfn()<BR>通过e820计算出最大可用页面。<BR>e820.map[1].addr = 0x100000 e820.map[1].size = 0x1f00000,那么start =<BR>PFN_UP(0x100000) = (0x100000+0x1000(4096 = 1&gt;&gt;12)-1)&gt;&gt;12 =<BR>0x100FFF&gt;&gt;12=0x100, end = PFN_DOWN(0x2000000) = 0x2000000 &gt;&gt; 12 =<BR>0x2000 = 8192,那么max_pfn = 0x2000 就是最大的页面<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory ()-&gt;find_max_low_pfn()<BR>主要是高端内存的限制。<BR>max_low_pfn = max_pfn 不能大于896M， PFN_DOWN((-0xC0000000 –<BR>0x8000000(128&lt;&lt;20)) = (0x38000000 = 896M限制)) = 0x38000<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory ()-&gt; init_bootmem()<BR>引导内存分配只使用在引导过程中，为内核提供保留分配页,并且建立内存的<BR>位图。<BR>比如_end 为0xc02e5f18,start_pfn = PFN_UP(__pa(&amp;end)) = 0x2e6，所以<BR>start_pfn 指的是内核结束的下一个页面((_pa(&amp;_end) &gt;&gt; 12 =0x2e5 ,那么<BR>init_bootmen(start_pfn,max_low_pfn) = init_bootmen(0x2e6,0x2000)<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory ()-&gt;<BR>init_bootmem()-&gt;init_bootmem_core()<BR>init_bootmen(0x2e6,0x2000) = init_bootmem_core(&amp;contig_page_data, 0x2e6, 0,<BR>0x2000),mapsize就是建立内存位图大小需要多少字节 1024＝(8192-0＋7)/8，<BR>1个位代表着4K的一个页面。<BR>bdata-&gt;node_bootmem_map = phys_to_virt(mapstart &lt;&lt; PAGE_SHIFT) =<BR>phys_to_virt(0x2e6&lt;&lt;12) = phys_to_virt(0x2e6000) = 0xc02e6000<BR>bdata-&gt;node_boot_start = (start &lt;&lt; PAGE_SHIFT) = 0<BR>bdata-&gt;node_low_pfn = end = 8192<BR>把0xc02e6000 开始0xff 填充1024 个字节 0xc02e6400,这样就代表着所有内<BR>存不可用。<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory ()-&gt; register_bootmem_low_pages<BR>e()<BR>根据e820和内存位图来标识位图那些内存可用。<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory ()-&gt; register_bootmem_low_pages<BR>e()-&gt;free_bootmem()-&gt;free_bootmem_core()<BR>第一次调用 free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size)) =<BR>free_bootmem(PFN_PHYS( 0), PFN_PHYS(159))= free_bootmem(0, 0x9F000))<BR>free_bootmem_core(contig_page_data.bdata, 0, 0x9f000)<BR>eidx代表这块内存共占用多少页面<BR>eidx = (addr + size - bdata-&gt;node_boot_start)/PAGE_SIZE = (0+0x9f000-0)/0x1000=0x9f<BR>end = (addr + size)/PAGE_SIZE＝（0+0x9f000）/0x1000=0x9f<BR>start = (addr + PAGE_SIZE-1) / PAGE_SIZE = (0+0x1000-1)/0x1000=0<BR>sidx = start - (bdata-&gt;node_boot_start/PAGE_SIZE)=0-(0/0x1000)=0<BR>每一次循环清1 位，第一次循环0x9f次清到0x9f /8= 0x13，所以第一次清到<BR>了0xc02e6013的第7位159%8 = 7，所以0xc02e6013的数据应该是0x10000000,<BR>第二次free_bootmem_core(contig_page_data.bdata, 0x100000, 0x 1F00000)<BR>eidx = (addr + size - bdata-&gt;node_boot_start)/PAGE_SIZE = 0x2000<BR>end = (addr + size)/PAGE_SIZE = 0x2000<BR>start = (addr + PAGE_SIZE-1) / PAGE_SIZE = 0x100<BR>sidx = start - (bdata-&gt;node_boot_start/PAGE_SIZE) = 0x100<BR>这次从0xc02e6020 （ 0x100/8 ） 开始清0x2000-0x100 次, 清到<BR>0xc02e6400(0xc02e6020+0x1f00/8)<BR>通过free_bootmem的操作，我们已经把内存的位图标识出来。<BR>start_kernel() -&gt; setup_arch() -&gt; setup_memory ()-&gt;reserve_bootmem()<BR>保留内存，说明这部分不能用于动态分配。<BR>reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +<BR>bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY)) ＝<BR>reserve_bootmem(0x100000, (PFN_PHYS(2e6) +1024 + 4096-1) - (0x100000))=<BR>reserve_bootmem(0x100000, 1E73FF)<BR>保留内核开始1M 到建立的内存位图的结束地方，因为位图是接着内核的下<BR>一个页面存放的，所以一起保留，对于+ PAGE_SIZE-1 操作就是位图的结束<BR>位置也与内存边界对齐。重新改写内存位图之后对于我们的情况是<BR>0xc02e6020(1M)到0xc02e605C 都改成了1（保留）0xc02e605D还是0<BR>reserve_bootmem(0,PAGE_SIZE)= reserve_bootmem(0,4096)<BR>保留物理内存的第0 页内存。0x0-0x4096<BR>start_kernel() -&gt; setup_arch() -&gt; paging_init()<BR>当我们上面的物理内存都完成时，我们就要对所有内存进行管理，需要重新<BR>建立页面映射。<BR>start_kernel() -&gt; setup_arch() -&gt; paging_init()-&gt;pagetable_init()<BR>我们根据已有的内存信息，重新修改页面表<BR>end = (unsigned long)__va(max_low_pfn*PAGE_SIZE) = 0xC2000000<BR>pgd_base = swapper_pg_dir = 0xC0101000<BR>i = __pgd_offset(PAGE_OFFSET) = (0xC0000000&gt;&gt;22)&amp;(1024-1)=768<BR>pgd = pgd_base + i= 0xC0101C00<BR>pgd就是内核模式的第一个页目录<BR>第一个for循环就是说从768开始到1024结束，也就是swapper_pg_dir结束，<BR>这部分都是内核的页目录，也就是我们要修改的页目录。<BR>第二个for循环用于中间页表，对我们2 层i386 体系无效。<BR>alloc_bootmem_low_pages(PAGE_SIZE) = __alloc_bootmem(0x1000,0x1000,0)<BR>-&gt;__alloc_bootmem_core(pgdat-&gt;bdata, 0x1000,0x1000,0)<BR>eidx = bdata-&gt;node_low_pfn - (bdata-&gt;node_boot_start &gt;&gt; PAGE_SHIFT) =<BR>8192-(0&gt;&gt;12) = 8192 代表着这个区域可用的页面，实际上就是整个内存。<BR>我们以前保留过第0 页面的内存，所以我们分配内存就是从第一页面开始，<BR>重新写内存位图将已分配的页面做保留标记。这次运行后我们分配了一个页<BR>面（0x4096），开始地址为0xc0001000，与此同时，0xc02e6000 = 0x3 = 0011。<BR>第三个for循环用于写页表，写满这个我们新分配的页表。<BR>第一次循环的时候pte = 0xc0001000;vaddr = 0xc0000000;<BR>*pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL) =<BR>mk_pte_phys(0, MAKE_GLOBAL(_PAGE_PRESENT | _PAGE_RW |<BR>_PAGE_DIRTY | _PAGE_ACCESSED) =<BR>mk_pte_phys(0, MAKE_GLOBAL(0x001 | 0x002 | 0x040 | 0x020) =<BR>mk_pte_phys(0, ( MAKE_GLOBAL(0x001 | 0x002 | 0x040 | 0x020) = 0x63)) =<BR>mk_pte_phys(0, pgprot_t __ret = __pgprot(0x63)) = mk_pte_phys(0, __ret) =<BR>__mk_pte((0) &gt;&gt; 12, __ret) = __pte(((0)&lt;&lt;12)| pgprot_val(__ret)) = __pte(0x63)<BR>= 0x00000063<BR>第二次运行时 vaddr = 0xc0001000;<BR>*pte = 0x00001063<BR>当pmd = 0xc0101c00 时<BR>set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base))) ＝<BR>(pmd,__pmd((_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED |<BR>_PAGE_DIRTY) + 0x00001000) = (pmd,__pmd(0x001 | 0x002 | 0x020 | 0x040<BR>+0x00001000) = set_pmd(pmd,__pmd( 0x00001063)<BR>这就是结果0xc0101c00=0x00001063下一次pmd是0xc0101c04＝0x00002063<BR>对于我们现在的状态，我们是32M 内存，end = 0xC2000000,所以只映射了8<BR>项,0xC0101C1C=0x00008063<BR>现在要建立专用区的页表了，专用区是位于虚拟地址为0xfffff000 往回的一<BR>个区域。Enum fixed_addresses 描述了专用区的功能<BR>vaddr=__fix_to_virt(__end_of_fixed_addresses-1)&amp;PMD_MASK= 0xFFC00000<BR>这就是专用区起始地址所在的中级页目录边界地址<BR>fixrange_init(vaddr, 0, pgd_base) = fixrange_init(0xffc00000, 0, 0xc0101000)<BR>我们要在里面建立专用区的页面。<BR>0xffc00000 处于页目录的1023 映射的位置（最后一项），所以又申请了一个<BR>页面0xc0009000，（没有作为页表使用）。因为这个页面是接着我们以前的目<BR>录页面申请的，所以这个页面与目录页面在物理上是连续的，但在虚拟地址<BR>却差很大。0xC0101FFC = 0x00009067,这就是目录映射了。<BR>当我们运行完时我们就把所有的页表及页目录都建立好，通过load_cr3 与<BR>__flush_tlb_all重新刷新分页机制。<BR>start_kernel() -&gt; setup_arch() -&gt; paging_init()-&gt;zone_sizes_init()<BR>根据内存位图开始建立分区。<BR>max_dma = virt_to_phys((char*) MAX_DMA_ADDRESS) &gt;&gt; PAGE_SHIFT =<BR>virt_to_phys(0xC1000000) &gt;&gt; 12 = 0x1000<BR>对于我们32M内存来说，zones_size = { 4096, 4096, 0 }<BR>start_kernel() -&gt; setup_arch() -&gt; paging_init()-&gt;zone_sizes_init()-&gt;free_area_init()<BR>建立分区数据结构：标记所有页面保留，标记所有内存队列空，清除内存位<BR>图。（需要详细）<BR>map_size = (totalpages + 1)*sizeof(struct page) ＝ 0xE000 根据page结构看<BR>8192 个页面需要多大空间。<BR>lmem_map =alloc_bootmem_node(pgdat, 0xE000) =<BR>__alloc_bootmem_node(pgdat, 0xE000, 0x10, 0x1000000) = 0xc1000000<BR>lmem_map = (struct page *)(PAGE_OFFSET +<BR>MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET))<BR>= 0xC0000000+MAP_ALIGN(0x1000000) = 0xC0000000+<BR>((((0x1000000) % sizeof(mem_map_t)) == 0) ? (0x1000000) : ((0x1000000) +<BR>sizeof(mem_map_t) - ((0x1000000) % sizeof(mem_map_t))))=<BR>0xC0000000+((8==0)?0x1000000: 0x1000038-8) = 0xC1000030<BR>因为cache需要对齐<BR>接下来循环初始化每个区<BR>zone-&gt;zone_mem_map = mem_map + offset; 这个区的mem_map 结构位置<BR>zone-&gt;zone_start_paddr = zone_start_paddr; 这个区的开始虚拟地址。<BR>…<BR>for (i = 0; i &lt; size; i++) {<BR>struct page *page = mem_map + offset + i;<BR>…<BR>}<BR>初始化这个区的所有page 结构。在page 里所有的页面都是保留的，不能投<BR>入使用。<BR>…<BR>for (i = 0; ; i++) {<BR>unsigned long bitmap_size;<BR>INIT_LIST_HEAD(&amp;zone-&gt;free_area[i].free_list);<BR>…<BR>}<BR>这是伙伴分配系统的初始化<BR>这就说明了每个区有每个区自己的管理系统。<BR><A href="http://www-900.ibm.com/developerWorks/cn/linux/l-numa/index.shtml" target=_blank><FONT color=#002c99>http://www-900.ibm.com/developerWor...uma/index.shtml</FONT></A><BR>start_kernel() -&gt; setup_arch() -&gt; paging_init()-&gt;zone_sizes_init()-&gt;free_area_init()<BR>-&gt;build_zonelists()<BR>start_kernel() -&gt; setup_arch() -&gt; dmi_scan_machine()<BR>dmi_iterate()<BR>isa_memcpy_fromio(buf, fp, 15)= memcpy_fromio(buf,__ISA_IO_base +<BR>0xF0000,15) = __memcpy(buf,__io_virt(0xC00F0000),(15))<BR>buf = 0xC00F0000 里的数据，循环一直到fp = 0xFFFFF为止，每次fp 加0x10<BR>当这些执行完的时候，内核的基础也就大部分已经建立 ，我们在继续看第<BR>三部分的初始化。<BR>第三次探测<BR>start_kernel() -&gt;mem_init()<BR>start_kernel() -&gt;mem_init()-&gt;free_pages_init()<BR>start_kernel() -&gt;mem_init()-&gt;free_pages_init()-&gt;free_all_bootmem_core()<BR>struct page *page = pgdat-&gt;node_mem_map; 这就是第一个page.<BR>idx = bdata-&gt;node_low_pfn - (bdata-&gt;node_boot_start &gt;&gt; PAGE_SHIFT) =<BR>8192-0&gt;&gt;12 = 8192<BR>第一个循环就是根据我们以前建立的内存位图来初始化Page。<BR>start_kernel() -&gt;mem_init()-&gt;free_pages_init()-&gt;free_all_bootmem_core()-&gt;<BR>__free_page()-&gt;__free_pages()-&gt;__free_pages_ok()<BR>释放内存页（需要详细）<BR>第二个循环就是释放内存位图本身的内存。<BR>执行完后以后的内存操作就可以使用Page 了，第三阶段的内存初始化完毕，还<BR>有一部分零散的内存操作会在别的部分讨论，至此，物理内存管理结束。<BR>虚拟内存管理<BR>中断系统<BR>描述<BR>X86 微机通过外接两片级连的可编程中断控制器8259A，接收更多的外部中<BR>断。每个8259A控制器可以管理8 条中断线，两个可以控制15 条中断。<BR>由于Intel 公司保留0-31 号中断向量用来处理异常事件，所以硬件中断必须<BR>设在31以后，Linux则在实模式下初始化时把硬件中断设在0x20-0x2F。还有一<BR>个是0x80是系统调用。<BR>中断门与陷阱门区别：通过中断门门进入中断服务程序时CPU 会自动将中断关<BR>闭，而通过陷阱门进入则不会。<BR>原理<BR>Linux的中断实现原理：考虑中断处理的效率，Linux的中断处理程序分为两<BR>个部分：上半部和下半部。上半部的功能是“登记中断”，具体的中断实现会在<BR>下半部。之所以上半部要快，因为上半部是完全屏蔽中断的。下半部有以下几种<BR>实现方式：bottom half, task queue（没看）, tasklet, softicq，在我们分析的这个版<BR>本中，我认为bottom half以及softicq 都是基于tasklet实现的，以后会具体介绍。<BR>第一次中断设置arch/i386/kernel/head.S<BR># L325<BR>ignore_int 这是默认的中断服务程序，实际上什么也不做。<BR>L359<BR>SYMBOL_NAME(idt_descr):<BR>.word IDT_ENTRIES*8-1 # idt contains 256 entries<BR>.long SYMBOL_NAME(idt_table) #idt_table定义在Arch/i386/Kernel/Traps.c<BR>L245<BR>lidt idt_descr<BR>这样如果有中断、异常发生就会去ignore_int空转一次。<BR>第二次中断设置<BR>start_kernel() -&gt; trap_init ()<BR>主要是对一些系统保留的中断向量初始化<BR>struct desc_struct idt_table[256] Arch/i386/kernel/Traps.c L67<BR>extern irq_desc_t irq_desc [NR_IRQS]; include/linux/irq.h L67<BR>idt_table = 0xC028B000<BR>陷阱门 只允许在系统级下调用<BR>set_trap_gate(0,&amp;divide_error)＝_set_gate(idt_table+0,15,0, 0xC0109060);<BR>0xC028B000 = 0xC0108F0000609060<BR>中断门 只允许在系统级下调用<BR>set_intr_gate(2,&amp;nmi)＝_set_gate(idt_table+2,14,0, 0xC0109108);<BR>0xC028B010 = 0xC0108E0000609108<BR>系统门 可以用户级调用，否则无法提供系统调用了。看第三个参数为3<BR>set_system_gate(3,&amp;int3) ＝_set_gate(idt_table+3,15,3, 0xC0109138);<BR>0xC028B018 = 0xC010EF0000609138<BR>start_kernel() -&gt; trap_init () -&gt; cpu_init()<BR>初始化CPU，并且重新装载GDT 与IDT。<BR>我们先说一下ccurrent 宏。通过这个宏可以得到当前运行进程的struct<BR>task_struct 。<BR>cpu_gdt_table Arch/i386/Kernel/head.S L423<BR>然后通过两行汇编重新装载GDT与IDT。<BR>start_kernel() -&gt; init_IRQ()<BR>主要是对外设的中断初始化<BR>init_ISA_irqs() 中断请求队列的初始化<BR>init_ISA_irqs()-&gt;init_8259A() 可编程中断控制器8259A的初始化<BR>继续初始化中断向量，从0x20（32）开始16个，因为之前是保留的。<BR>void (*interrupt[NR_IRQS])(void) = {<BR>IRQLIST_16(0x0),<BR>… }<BR>#define IRQLIST_16(x) \<BR>IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \<BR>IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \<BR>IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \<BR>IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)<BR>#define IRQ(x,y) \<BR>IRQ##x##y##_interrupt<BR>#<BR>define NR_IRQS 224<BR>255 -31 = 224 所以interrupt 定义的Intel保留之后的中断<BR>对于IRQ##x##y##_interrupt函数的定义在 include/asm-i386/hw_irq.h中<BR>#define BUILD_IRQ(nr) \<BR>asmlinkage void IRQ_NAME(nr); \<BR>__asm__( \<BR>"\n"__ALIGN_STR"\n" \<BR>SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \<BR>"pushl $"#nr"-256\n\t" \<BR>"jmp common_interrupt");<BR>经过处理之后得到：<BR>asmlinkage void IRQ0x01_interrupt(); \<BR>__asm__( \<BR>"\n"__ALIGN_STR"\n" \<BR>IRQ0x01_interrupt:\n\t" \<BR>"pushl $0x01-256\n\t" \ ＃这就是每个中断的最大区别<BR>"jmp common_interrupt");<BR>#define BUILD_COMMON_IRQ() \<BR>asmlinkage void call_do_IRQ(void); \<BR>__asm__( \<BR>"\n" __ALIGN_STR"\n" \<BR>"common_interrupt:\n\t" \<BR>SAVE_ALL \<BR>"pushl $ret_from_intr\n\t" \<BR>SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \<BR>"jmp "SYMBOL_NAME_STR(do_IRQ));<BR>*/<BR>中断发生时都会运行do_IRQ 这个公共的中断处理函数。返回时就会返回到<BR>ret_from_intr继续运行后再返回。<BR>idt_table里存放了具体的中断服务的内存地址，CPU产生中断会通过idt_table<BR>去运行具体的中断服务程序。除了系统保留的中断是运行各自不同的中断服<BR>务，其它都会运行到do_IRQ 这个公共中断服务程序里来。如果有两个设备<BR>使用了同一个中断的话，我们就需要有一种机制来识别具体是那个设备发生<BR>了中断。<BR>中断服务队列的初始化<BR>init_IRQ() 初始化完成后，并没有具体的中断服务程序，真正的中断服务程<BR>序要到具体设备的初始化程序将其中断服务程序通过request_irq()向系统“登<BR>记”，挂入某个中断请求队列以后才会发生。<BR>request_iqrt()-&gt;setup_irq()<BR>将具体的中断服务链入相应的中断请求队列。<BR>下面这个图就是有中断服务链入后的数据结构的简图。<BR>中断服务的响应（上半部）<BR>do_IRQ() Arch/i386/kernel/irq.c L563<BR>desc-&gt;handler-&gt;ack(irq);<BR>中断处理器（如i8259A）在将中断请求“上报”给CPU以后，希望CPU<BR>给一个确认（ACK），表示CPU在处理了。<BR>IRQ_INPROGRESS 主要是为多处理器设置的，IRQ_PENDING 说明再<BR>次有同样的中断发生，可能是SMP系统结构中，一个CPU正在中断服务，<BR>而另一个又进入do_IRQ()，这时IRQ_INPROGRESS标志为1，所以返回了，<BR>也可能是在单处理器中CPU 已经在中断服务程序中，但是因为某种原因又<BR>将中断开启了，而且在同一个中断通道中再次产生了中断，也会因为<BR>IRQ_INPROGRESS标志为1 返回<BR>handle_IRQ_event()是for 循环里的主题，进入for 循环时desc-&gt;status的<BR>IRQ_PENDING标志为0，如果执行完handle_IRQ_event()后，发现desc-&gt;status<BR>的IRQ_PENDING 标志为1 就说明发生了上述情况。那就在来循环，直到<BR>IRQ_PENDING标志为0为止。<BR>desc-&gt;handler-&gt;end(irq);<BR>if (softirq_pending(cpu))<BR>do_softirq();<BR>这就是下半部的了，我们在下半部里再分析。<BR>do_IRQ()-&gt;handle_IRQ_event()<BR>irq_enter(cpu, irq);说明那个CPU在那个中断服务程序里。<BR>irq_exit(cpu, irq);与上面相反。<BR>循环执行这个中断通道的所有中断程序，因为Linux也不知道是那个设备产<BR>生的中断。（如果中断是共用的话）到了自己的中断服务程序里自己检查。<BR>action是struct irqaction,看上面的图。所以struct irqaction里的handler一般是<BR>在关中断里执行的（可以自己开中断），所以要快速处理，否则中断太久会<BR>丢失别的中断。<BR>第三次中断设置（软中断）<BR>下半部<BR>原始bottom half<BR>原理：<BR>static void (*bh_base[32])(void); Arch/i386/Kernel/Softirq.c L274<BR>可以用来指向一个具体的bh 函数。同时又设置两个32 位bh_active(中断请<BR>求寄存器)和bh_mask（中断屏蔽寄存器）。<BR>使用方式<BR>void init_bh(int nr, void (*routine)(void)) L311<BR>void remove_bh(int nr) L317<BR>static inline void mark_bh(int nr) Include/linux/interrupt.h L228<BR>将bh_active 的nr 位置一，如果bh_mask 中的相应位也是1，那么系统在执<BR>行完do_IRQ()后，以及每次系统调用结束后都会在函数do_buttom_half()中<BR>执行相应的bh函数。<BR>原始bottom half机制有很大局限性，主要是个数限制在32 个以内，所以在<BR>2.4内核里，bottom half使用的是tasklet 机制，只是在接口上保持向下兼容。<BR>task queue<BR>include/linux/tqueue.h L38<BR>struct tq_struct {<BR>struct list_head list; /* linked list of active bh's */<BR>unsigned long sync; /* must be initialized to zero */<BR>void (*routine)(void *); /* function to call */<BR>void *data; /* argument to function */<BR>};<BR>typedef struct list_head task_queue; L64<BR>使用方式<BR>#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q) L66<BR>static inline int queue_task(struct tq_struct *bh_pointer, task_queue *bh_list)<BR>L100 – L119<BR>static inline void run_task_queue(task_queue *list)<BR>tasklet<BR>主要是考虑是为了更好的支持SMP，提高SMP 的利用率；不同的tasklet可<BR>以同时运行在不同的CPU上，同时也是Linux推荐使用的方式，我们将重点<BR>解释<BR>struct tasklet_struct Include/Linux/Interrupt.h L104<BR>{<BR>struct tasklet_struct *next;<BR>unsigned long state;<BR>atomic_t count;<BR>void (*func)(unsigned long);<BR>unsigned long data;<BR>};<BR>struct tasklet_struct bh_task_vec[32]; L275<BR>使用方式（这中方式是我们普通使用的方式）<BR>void my_tasklet_func(unsigned long);<BR>DECLARE_TASKLET(my_tasklet, my_tasklet_func, data) L113<BR>static inline void tasklet_schedule(&amp;my_tasklet) L158<BR>另外的一些调用接口<BR>DECLARE_TASKLET_DISABLED(name, func, data) L116<BR>static inline void tasklet_disable(struct tasklet_struct *t) L179<BR>static inline void tasklet_enable(struct tasklet_struct *t) L186<BR>extern void tasklet_kill(struct tasklet_struct *t); L198<BR>extern void tasklet_init(struct tasklet_struct *t, L199<BR>我们看一下原始bottom half就怎么用tasklet实现的以及于我们普通的tasklet<BR>的区别。<BR>init_bh(TIMER_BH, timer_bh); 初始化，<BR>mark_bh(TIMER_BH) = tasklet_hi_schedule(bh_task_vec+ TIMER_BH)<BR>提出对timer_bh的执行请求。<BR>可以看到，bh_task_vec就是实现bh的tasklet_struct，看下面的softirq_init()<BR>mark_bh()-&gt;tasklet_hi_schedule()<BR>将bh 的tasklet_struct 挂到当前CPU 的软中断执行队列，并通过<BR>__cpu_raise_softirq()正式发出软中断请求。这样子后bh_action就会在将来的<BR>某个适合的时间在do_softirq()得以执行。而bh_action 的主要功能就是实现<BR>了原始bottom half，既在任意时间最多只有一个CPU在执行bh函数。但是<BR>tasklet_struct本身是支持SMP 结构的。<BR>tasklet_hi_schedule 与tasklet_schedule都是将tasklet_struct链入到CPU队列，<BR>不同的是__cpu_raise_softirq() 正式发出软中断请求不一样，<BR>tasklet_hi_schedule 使用的是HI_SOFTIRQ，而tasklet_schedule 使用的是<BR>TASKLET_SOFTIRQ。而这两种都是在softirq_init里我们初始化的，这样我<BR>们就建立起来了softicq。<BR>softicq<BR>这是底层机制，很少直接使用，softicq 基于tasklet实现更复杂更庞大的软中<BR>断子系统。<BR>struct softirq_action Include/Linux/Interrupt.h L69<BR>{<BR>void (*action)(struct softirq_action *);<BR>void *data;<BR>};<BR>extern struct tasklet_head tasklet_vec[NR_CPUS]; L131<BR>start_kernel() -&gt; softirq_init ()<BR>对用于hb 的32 个用于bh的tasklet_struct结构调用tasklet_init()以后，它们<BR>的函数指针func全都指向bh_action()。bh_action就是tasklet实现bh的机制了<BR>open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);<BR>这是我们普通的tasklet_struct要用到的软中断。<BR>open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);<BR>这是tasklet_struct实现的bh调用。<BR>软中断的执行<BR>do_softirq()<BR>软中断服务不允许在一个硬中断服务程序内部执行，也不允许在一个软<BR>中断服务程序内部执行，所以通过in_interrupt()加以检查。<BR>h-&gt;action 就是串行化执行软中断，当bh 的tasklet_struct 链入的时候，就能<BR>在这里执行，在bh里重新锁了所有CPU，导致一个时间只有一个CPU可以<BR>执行bh 函数，但是do_softirq 是可以在多CPU 上同时执行的。而每个<BR>tasklet_struct在一个时间上是不会出现在两个CPU上的。<BR>只有当Linux初始化完成开启中断后，中断系统才可以开始工作。（开始心跳J）<BR>文件系统<BR>Linux使用了VFS（虚拟文件系统）对上提供统一的接口来提供服务。对下使用<BR>了“总线”来读取各种文件系统及设备。如图所示：<BR>一个块设备的文件及节点在引导之初是不可访问的，只有将它“安装”在文<BR>件系统的某个节点上才可访问。最初只有一个“根”节点“/”。<BR>VFS（虚拟文件系统）<BR>在讲解文件系统时，有很多重要的数据结构需要弄清楚，我们根据需要一个<BR>一个的介绍数据结构之间的关系。<BR>下图是VFS 体系结构,对于具体的文件系统都要融入到这个体系结构里来。<BR>从下图可以看到：每一个转载点都有一个struct vfsmount结构。而每一个节点都<BR>有struct dentry 和 struct inode。而这两个结构侧重面有些不同。 struct dentry主<BR>要是VFS使用，而struct inode主要是由各个具体的文件系统自己解释。通过struct<BR>inode，使各个具体的文件系统融入到VSF体系里来。<BR>在讲解根文件系统的安装之前要先说以下系统堆栈。<BR>我们回过头看<BR>lss stack_start,%esp esp = 0xC0272000 /Arch/i386/Kernel/Head.S<BR>L107<BR>这是在进入分页后执行的第一条语句，它的意义是建立内核堆栈。<BR>ENTRY(stack_start) L317<BR>.long SYMBOL_NAME(init_task_union)+8192<BR>.long __KERNEL_DS<BR>堆栈大小是8K（8192）。并且将init_task_union 写到堆里，这是为系统的进<BR>程准备环境。（可以把内核也看成是一个进程，以后可以看到，不管是内核本身<BR>还是内核创建的进程都是用的这一个堆栈）堆栈底部为0xC0270000,并且<BR>init_task_union就初始化在0xC0270000处。<BR>union task_union init_task_union Arch/i386/Kernel/Init_task.c L22<BR>__attribute__((__section__(".data.init_task"))) =<BR>{ INIT_TASK(init_task_union.task) };<BR>union task_union { include/linux/Sched.h L684<BR>task_t task;<BR>unsigned long stack[INIT_TASK_SIZE/sizeof(long)];<BR>};<BR>typedef struct task_struct task_t; L159<BR>说明task_union既可以是task的结构，也可以是stack数组。<BR>#define INIT_TASK(tsk) Include/linux/Sched.h L636<BR>这个宏是初始化一个task_struct结构。这个结构是一个进程的结构,也就是说，<BR>内核编译时，init_task_union 就已经写了数据，这些数据都是固定的。而我们关<BR>心的是task结构里的fs这一项。&amp;fs = 0xc0270640, fs=0xC0254FE0<BR>fs = static struct fs_struct init_fs = INIT_FS; Arch/i386/Kernel/Init_task.c L9<BR>我们看到，fs这项在编译是写的是&amp;init_fs的地址。而init_fs是通过INIT_FS宏<BR>填充好的。所以我们得到了一个如下的图，这个图是在内核编译是就确定了的。<BR>可以看到，task_struct代表的是一个进程，fs_struct代表着该进程的部分环境。<BR>根文件系统的安装过程<BR>第一次安装：<BR>register_filesystem () fs/Suer.c L99<BR>任何一种文件系统都需要通过调用该函数进行注册，系统才能识别。<BR>这个函数很简单，就是遍历一次注册的文件系统链表，如果没有注册的<BR>话就不将其加入到链表中。file_systems＝0xC02BA3E0 就是链表的头，也就<BR>是root_fs_type的位置。<BR>start_kernel () -&gt;vfs_caches_init ()-&gt;mnt_init()-&gt;init_rootfs()<BR>注册了rootfs_fs_type这种文件系统。<BR>static DECLARE_FSTYPE(rootfs_fs_type, "rootfs", ramfs_read_super,<BR>FS_NOMOUNT|FS_LITTER); fs/ramfs/Inode.c L321<BR>DECLARE_FSTYPE是个宏，定义一个file_system_type的结构，展开为<BR>struct file_system_type rootfs_fs_type = {<BR>name: "rootfs"，<BR>read_super: ramfs_read_super，<BR>fs_flags: FS_NOMOUNT|FS_LITTER，<BR>owner: NULL，<BR>}<BR>start_kernel () -&gt;vfs_caches_init ()-&gt;mnt_init()-&gt;init_mount_tree ()<BR>do_kern_mount()<BR>对文件系统的预安装。<BR>通过alloc_vfsmnt 初始化一个struct vfsmount, mnt = 0xC10EE104，通过flags<BR>来判断应该走那条线路，<BR>do_kern_mount()-&gt;get_fs_type(“rootfs”)<BR>可以看到首先通过文件系统的名字遍历文件系统的链表得到文件系统的<BR>file_system_type的结构，fs=0xC025AB0C，try_inc_mod_count是对文件系统<BR>的使用进行计数。request_module 这一行的意义是如果没有找到文件系统则<BR>通过加载的模块方式来看看有没有该文件系统。我们这里的情况是有该文件<BR>系统，所以就返回了该文件系统的指针。<BR>do_kern_mount()-&gt;get_sb_nodev()<BR>get_sb_nodev(0xC025AB0C,0,“rootfs”,0)<BR>do_kern_mount()-&gt;get_sb_nodev()-&gt;get_anon_super()<BR>get_anon_super（0xC025AB0C，0，0）分配一个到超级块，并让这个超<BR>级块与file_system_type建立关联。<BR>通过该文件系统所提供的read_super 函数指针读取该文件系统的超级块<BR>的具体数据（主要是生成struct inode与struct dentry让其与超级块建立关联，<BR>稍侯我们在介绍这两个结构）。实际上我们看到rootfs是一个ramfs系统。超<BR>级块的读取是ramfs_read_super，与ramfs文件系统是一样的。这是一个内存<BR>文件系统。<BR>当get_sb_nodev成功返回后，带回来了一个初始化好的struct super_block。<BR>在将struct vfsmount与struct super_block关联后返回，这样就又带回了一个<BR>初始化好的struct vfsmount。当do_kern_mount()返回时，具体的文件系统那<BR>边就已经准备就绪。<BR>再次初始化一个struct namespace，并且将vsfmount 挂在namespace 下，将<BR>namespace挂到当前进程的mnt_list 里，说明已经安装了，最后将namespace<BR>的根设成当前进程的根。<BR>首先通过do_kern_mount 预安装rootfs这个文件系统得到它得挂接点，初始化后，<BR>通过最后两个set_fs_…来把当前得fs 指向了该文件系统。实际上也就是说，系<BR>统第一个“安装”的文件系统是rootfs，这个时候又多了几个数据结构，它们之<BR>间的关系如图所示：<BR>struct task_struct<BR>init_task_union<BR>fs<BR>namespace<BR>struct fs_struct<BR>struct namespace<BR>namespace<BR>root<BR>struct vfsmount<BR>mnt(0xC10EE104)<BR>mnt_sb<BR>mnt_root<BR>mnt_devname<BR>="root_fs"<BR>mnt_mountpoint<BR>mnt_parent<BR>file_systems struct file_system_type<BR>rootfs<BR>name :"rootfs"<BR>super_block<BR>struct super_block<BR>rootfs_super_block<BR>s_root<BR>s_type<BR>struct dentry<BR>dentry(0xC10E9094)<BR>d_sb<BR>d_inode<BR>INIT_FS<BR>root<BR>pwd<BR>rootmnt<BR>pwdmnt<BR>0xC02BA3E0<BR>0xC025AB0C<BR>0xC10EC000<BR>super_blocks<BR>0xC02590A0<BR>struct inode<BR>inode(0xC10EA044)<BR>i_sb<BR>第一次初始化内存结构<BR>RAMFS 文件系统<BR><A href="http://www.linuxforum.net/forum/showflat.php?Cat=&amp;Board=linuxK&amp;Number=" target=_blank><FONT color=#002c99>http://www.linuxforum.net/forum/sho...=linuxK&amp;Number=</FONT></A><BR>99919&amp;page=164&amp;view=collapsed&amp;sb=5&amp;o=all<BR>ext2 文件系统<BR><A href="http://www-900.ibm.com/developerworks/cn/linux/filesystem/ext2/#1" target=_blank><FONT color=#002c99>http://www-900.ibm.com/developerwor...esystem/ext2/#1</FONT></A><BR>我们回顾一下ROOTFS 是怎么装载上去的。<BR>开始时init_task_union里是只指向INIT_FS的，可是INIT_FS 里没什么东西，<BR>注册完rootfs文件系统之后，试图装载rootfs文件系统，那么首先要生成一个装<BR>载点struct vfsmount，并且要读取文件系统生成超级块struct super_block，在<BR>ROOTFS 读取超级块的过程中会在生成struct dentry，这个是对于VFS文件系统<BR>用的(VFS 看到一个节点，不用管它是在什么文件系统中，对各种文件系统统一<BR>接口)。而struct inode则是真正的节点（每种文件系统实现自己对节点的具体操<BR>作）。因为这个节点是用于挂接用的，所以要通过struct vfsmount 来标识这个节<BR>点同时也是个挂接点。这样它们就建立了联系，也就是上图表示。<BR>为什么根系统的安装会在系统初始化的尽头呢？J<BR>通过上面的链接我们对Ext2 文件系统应该有了一些了解，我们想要知道的就是<BR>Ext2 的那种物理布局情况是如何融入到VFS 体系结构的。<BR>第二次安装：<BR>init () -&gt; prepare_namespace()<BR>这里就是真正的根文件系统的安装。首先要判断做为根的是什么设备，并且<BR>在root_fs文件系统里创建目录以及创建节点和设备。然后通过mount_root()以只<BR>读方式装载根文件系统。在mount_root()里可以看出，根文件系统是挂接到了<BR>root_fs文件系统的/ root下，最后在mount_block_root通过sys_chdir将当前目录<BR>设置到了/root。出来后卸载/dev 目录，最后在prepare_namespace 的结束处通过<BR>sys_mount重新完成了根目录的挂接（以移动方式）。<BR>在调用mount_root()之前如图所示：<BR>init () -&gt; prepare_namespace()-&gt;sys_mkdir()<BR>系统调用，创建目录。<BR>这个地方创建的是 root_fs的目录，实际上就是一个VFS的节点，因为root_fs<BR>是内存文件系统。首先通过path_lookup-&gt; path_init 得到目录，nd-&gt;dentry =<BR>0xC10E0904, nd-&gt;mnt=0xC10EE104,这两个数据看上面的图。path_lookup-&gt;<BR>path_walk 里跳转到last_component:然后再跳转到lookup_parent:设置值后跳到<BR>return_base:返回。lookup_create(),它的作用是查找缓冲，如果没有就创建一个新<BR>的。lookup_hash()-&gt;cached_lookup 在hash 里找，因为我们是新创建的，一定不<BR>存在，还有就是root_fs不提供d_hash的操作。所以dentry=0,这是我们就要自己<BR>创建一个新的。new=0xC10E9430,创建了struct dentry还不行，还要创建与之对<BR>应的struct inode，通过lookup 跳转到了ramfs_lookup 函数里，fs/ramfs/inode.c L55<BR>可以看到在这个函数里永远返回NULL，并且通过d_add 函数把dentry-&gt;inode<BR>也置成空。这样一来在lookup_hash的dentry=new了，但是现在并没有inode,回<BR>到lookup_create 里，就会跳转到enoent，返回失败。在回到sys_mkdir，发现失<BR>败后会调用vfs_mkdir 来创建。May_create 检查有没有权限，然后通过具体的文<BR>件系统的mkdir 来创建节点。这里的mkdir 是ramfs_mkdir.里面很简单，只是生<BR>成一个inode结构，并且与dentry挂钩。<BR>普通文件系统的装载<BR>假设我们已经启动Linux到了shell里。<BR>执行mount –t iso9600 /dev/cdrom /mnt<BR>sys_mount ()<BR>它也是mount 的系统调用<BR>sys_mount (dev_name=0x8104f78 "/dev/hda",<BR>dir_name=0x8106f98 "/mnt",<BR>type=0xbfffff88 "iso9660",<BR>flags=3236757504,<BR>data=0x8104f68)<BR>可以看到，这是从用户空间进来的。所以首先把数据拷贝到系统空间。<BR>sys_mount ()-&gt;do_mount ()<BR>这也是mount 的主体函数，<BR>do_mount (dev_name=0xc1be8000 "/dev/hda",<BR>dir_name=0xc10a9000 "/mnt",<BR>type_page=0xc1be9000 "iso9660",<BR>flags=3236757504,<BR>data_page=0xc1be7000)<BR>经过一些判断之后就要进入path_lookup<BR>sys_mount ()-&gt;do_mount ()-&gt;path_lookup()-&gt;path_init()<BR>可以看到，这个函数的主要目的就是根据name 来确定目录，如果是’/’，那<BR>初始化的目录就是根，否则就是当前目录，对于fs_struct 里rootmnt 就是根<BR>的挂接点，root就是挂接点的目录，而pwdmnt 与pwd 则是当前目录的挂接<BR>点以及当前目录。current 代表了当前进程的task_struct，每一个进程都有一<BR>个task_struct结构，在进程部分我们在继续讨论。通过path_init之后，struct<BR>nameidata *nd 则设置好了初始目录（这个结构只是暂时使用的）。<BR>sys_mount ()-&gt;do_mount ()-&gt;path_lookup()-&gt;path_walk()-&gt;link_path_walk()<BR>因为初始目录已经设置好，直接到‘/’子后的字符。这是如果*name为空说<BR>明我们完成任务，返回。link_count 用于防止函数嵌套。<BR>第一个for循环主要处理路径问题，在我们现在的环境里，*name=“mnt”<BR>所以this.name = “mnt”, this.len = 3,并且通过if (!c) 到了 last_component处。<BR>因为我们只是一次目录，如果有多层目录的话往下有几种情况，比如在界面上我<BR>们打的是 “/mnt/”或“/mnt/aaa/” 这样就走 last_with_slashes,说明走的是目录。<BR>另外就是要处理 “.”与“..”的情况了，我们都知道“.”与“..”代表的意义。<BR>所以“..”就是向上走了一层继续，“.”的情况就是不发生变化，继续，这就相<BR>当于在界面上打“mnt/../mnt”或“mnt/./../mnt”，如果不是“.”与“..”,那说明<BR>是正常的目录，就要继续往下走。比如我们在界面上键入的是/mnt/cdrom,那么就<BR>通过cached_lookup 在cache 里查mnt 这个节点有没有，如果没有就通过<BR>real_lookup 从文件系统里读出来并建立struct dentry。对读出来的struct dentry所<BR>指向的目录进行判断，是不是安装点，是不是符号链接，详细的操作过程在具体<BR>的文件系统里讨论，我们现在先看VFS层。<BR>如果是目录就会跳转到last_with_slashes，标记上是目录便继续到<BR>last_component，如果标记中有LOOKUP_PARENT，说明找的是根，便跳<BR>lookup_parent处理，紧接着还是判断“.”与“..”的情况，假如我们在界面上写<BR>的是“/mnt/..”或“/mnt/.”，这种情况就是在这里调整跳到return_reval处理。可<BR>以看出一个目录或文件都是找的struct dentry，那么这个结构也就是整个VFS的<BR>中心结构。我们通过path_init 与path_walk确定了我们要安装的目录在系统的所<BR>在。通过do_add_mount就把文件系统安装到了该目录。<BR>补充<BR>在文件系统里一个很重要的数据结构是file_oprations<BR>进程管理及调度<BR>说明：<BR>在Linux 的实现中，一个进程或一个线程都拥有一个task_struct。都拥有专<BR>用的系统堆栈空间，他们的区别只是在于线程没有用户空间。<BR>下面我们继续看在Linux初始化过程中创建的第一个内核线程<BR>rest_init-&gt; init_idle()<BR>首先初始化idle，也就是CPU无事做的时候运行的函数。<BR>init_idle(0xC0270000, 0);<BR>首先可以看到idle_rq= cpu_rq(0)=runqueues=0xC028BA00, runqueues 是<BR>一个以CPU 个数为下标的数组。代表是每个CPU 的运行队列。然后使用<BR>set_tsk_need_resched 设置current-&gt;need_resched = 1.<BR>rest_init-&gt; kernel_thread()<BR>kernel_thread( 0xC010502C &lt;init&gt;, arg=0x0,flags=3584)<BR>rest_init-&gt; kernel_thread()-&gt;arch_kernel_thread()<BR>自己做一个struct pt_regs，为什么要做一个？往下看<BR>rest_init-&gt; kernel_thread()-&gt;arch_kernel_thread()-&gt;do_fork()<BR>do_fork(clone_flags=8392448, stack_start=0, regs=0xc0271f58,<BR>stack_size=0,parent_tidptr=0x0, child_tidptr=0x0)<BR>返回0xC10D0000，也就是新的task_struct<BR>rest_init-&gt; kernel_thread()-&gt;arch_kernel_thread()-&gt;do_fork()-&gt;copy_process()<BR>dup_task_struct 拷贝一个task_struct,还根据参数决定是否拷贝深层的数据结<BR>构，比如：copy_files、copy_fs、等。最后设置需要调度。<BR>拷贝出来的线程开始并没有运行，要等到调度的时候才能运行。这个时候系<BR>统有了一个“进程”和一个线程了<BR>rest_init-&gt; cpu_idle()<BR>CPU 的“初始化”都结束了，CPU 进入idle,其实一进入idle 就是开始了进<BR>程调度。可以看到current-&gt;need_resched(init_idle())这个是1.所以不会在while里<BR>循环。<BR>rest_init-&gt; cpu_idle()-&gt;schedule()<BR>这是整个系统的进程调度算法（核心），不但这里有调用，很多地方都有调<BR>用。<BR>首先我们看一下runqueues 这个结构的说明，虽然这是2.4 的内核，但是调<BR>度算法却使用的是2.6 的。应该是RH自己修改的吧，我也不清楚，猜的。J<BR><A href="http://www-900.ibm.com/developerWorks/cn/linux/kernel/l-kn26sch/index.shtml" target=_blank><FONT color=#002c99>http://www-900.ibm.com/developerWor...sch/index.shtml</FONT></A><BR><A href="http://www.linuxfans.org/nuke/modules.php?name=News&amp;file=article&amp;op=view&amp;sid" target=_blank><FONT color=#002c99>http://www.linuxfans.org/nuke/modul...cle&amp;op=view&amp;sid</FONT></A><BR>=2368<BR>prev = current; 当前进程，我们这里现在是0xC0270000<BR>rq = this_rq(); 当前运行schedule 的CPU，我们这里现在只有一个CPU，<BR>所以是runqueues,0xC028BA00<BR>prev-&gt;last_run = jiffies; 刷新当前进程的运行时间<BR>spin_lock_irq(&amp;rq-&gt;lock); 对当前的CPU 的运行队列加锁。并禁用当前<BR>CPU中断。这里我们的进程的运行状态是TASK_RUNNING(init_idle()L1916)，<BR>判断nr_running,如果当前CPU上没有进程要运行的话，next=rq-&gt;idle.<BR>判断nr_active，本进程组中的进程数如果为0，就把active 与expired对掉。<BR>我们看一下我们这里的rn_running与nr_active是多少。回过头去看<BR>rest_init-&gt; init_idle()-&gt;deactivate_task()<BR>deactivate_task(0xC0270000, 0xC028BA00),看上面的数据<BR>那么我们这里nr_running = ?,rq-&gt;active-&gt;nr_active = ?，再回头看<BR>start_kernel()-&gt;sched_init()<BR>我们看到runqueues 是一个全局变量。开始的时候里面是空。这里就是对它<BR>的初始化<BR>首先通过循环按支持的CPU数来初始化runqueues，然将内核看做是一个进<BR>程idle来初始化。<BR>start_kernel()-&gt;sched_init()-&gt;wake_up_forked_process()<BR>首先调整sleep_avg 等，因为第 一次进来current-&gt;array 是空。所以要走<BR>__activate_task。<BR>start_kernel()-&gt;sched_init()-&gt;wake_up_forked_process()-&gt;activate_task()<BR>执行完这里current-&gt;array=runqueues-&gt;active(0xC028BA24);<BR>并且runqueues-&gt;active-&gt;nr_active = 1,runqueues-&gt;active-&gt;nr_running=1;<BR>回到deactivate_task<BR>现在又把nr_nr_running--，nr_active—都变成了0,。并且把array也清了，那<BR>么这个时候又恢复成原来的样子的。<BR>在继续往下看<BR>rest_init-&gt; kernel_thread()-&gt;arch_kernel_thread()-&gt;do_fork()<BR>再次调用wake_up_forked_process,这样相当于activate_task() 没有执行。J<BR>那么我们这里nr_running=1,rq-&gt;active-&gt;nr_active=1<BR>回到调度<BR>继续向下sched_find_first_bit,它用于快速定位就绪队列中高优先级任务<BR>next = 0xC10D0000，这就是我们新创建的线程的task_struct 的起始地址，接<BR>着使用clear_tsk_need_resched 清掉need_resched<BR>判断prev 与next 是不是一个进程，是的话可以看到，什么也没有做。不是<BR>的话我们就要开始进行进程切换了。<BR>context_switch 主要就处理进程的用户空间。每个进程都有一个struct<BR>mm_struct 代表着自己的用户空间，但是线程却没有用户空间，怎么办？只好暂<BR>时借用一下上一个进程的用户空间。（会有问题吗？没有，因为现在是在内核空<BR>间，内核访问用户空间都是一样的，就算是有问题，那也是返回到用户空间也会<BR>出现问题。再加上线程并不使用用户空间，所以没有关系）<BR>switch_to这个就是进程切换的关键所在。(主要是切换进程堆栈，已经说过，<BR>不管是进程或是线程，都有自己的堆栈)<BR>prev-&gt;thread 里的部分数据<BR>struct thread_struct {<BR>…<BR>unsigned long esp0 = 0<BR>unsigned long eip = 0<BR>unsigned long esp = 0<BR>unsigned long fs = 0<BR>unsigned long gs = 0<BR>…<BR>};<BR>next-&gt;thread<BR>struct thread_struct {<BR>…<BR>unsigned long esp0 = C10D2000<BR>unsigned long eip = C0108F60<BR>unsigned long esp = C10D1FC4<BR>unsigned long fs = 0<BR>unsigned long gs = 0<BR>…<BR>};<BR>我们回头看一下我们的线程是什么时候把thread 这个结构初始化的。<BR>rest_init-&gt; kernel_thread()-&gt;arch_kernel_thread()-&gt;)-&gt;do_fork()-&gt;copy_process()<BR>-&gt;copy_thread()<BR>childregs = ((struct pt_regs *)(THREAD_SIZE+(unsigned long) p))- 1<BR>= ((struct pt_regs *)(8192+0xC10D0000))- 1 = 0xC10D4000 – sizeof( pt_regs)<BR>= 0xC10D4000 – 60 = 0xC10D1FC4<BR>在这里实际上是打造一个堆栈的环境，这个堆栈就是我们生成的线程用的堆<BR>栈。那么这个线程是怎么使用这个堆栈的呢？那就要看调度了。<BR>那么我们运行完这个函数，里面初始化成什么样子呢？<BR>Childregs数据：<BR>ebx = 0xC010502C<BR>eip=0xc01071E4<BR>xes=xds=0x68<BR>xcs=0x60,<BR>orig_eax=0xFFFFFFFF,其它为0，其实childregs 大部分是我们开始在<BR>arch_kernel_thread自己做的那个regs。自己可以对照一下。<BR>(struct task_struct*)(0xC10D0000) -&gt;thread 的数据<BR>esp = 0xC10D1FC4<BR>esp0 = 0xC10D2000<BR>eip = 0xC0108F60 ret_form_fork 的地址(怎么会是它，为什么不是<BR>kernel_thread_helper，回头看copy_thread, p-&gt;thread.eip = (unsigned long)<BR>ret_from_fork)<BR>这就完成了next-&gt;thread的初始化，现在回过头来看调度<BR>switch_to 反汇编代码<BR>0xc01160c5 &lt;schedule+321&gt;: mov %esi,%eax<BR>0xc01160c7 &lt;schedule+323&gt;: mov %ecx,%edx<BR>0xc01160c9 &lt;schedule+325&gt;: push %esi<BR>0xc01160ca &lt;schedule+326&gt;: push %edi<BR>0xc01160cb &lt;schedule+327&gt;: push %ebp<BR>0xc01160cc &lt;schedule+328&gt;: mov %esp,0x350(%esi)<BR>0xc01160d2 &lt;schedule+334&gt;: mov 0x350(%ecx),%esp<BR>0xc01160d8 &lt;schedule+340&gt;: movl $0xc01160ed,0x34c(%esi)<BR>0xc01160e2 &lt;schedule+350&gt;: pushl 0x34c(%ecx)<BR>0xc01160e8 &lt;schedule+356&gt;: jmp 0xc0107654 &lt;__switch_to&gt;<BR>0xc01160ed &lt;schedule+361&gt;: pop %ebp<BR>0xc01160ee &lt;schedule+362&gt;: pop %edi<BR>0xc01160ef &lt;schedule+363&gt;: pop %esi<BR>执行switch_to 之前寄存器状态 当eip = 0xC01160CC<BR>eax 0xC028BA00 0xC0270000<BR>ecx 0xC10D0000<BR>edx 0x0 0xC010D000<BR>ebx 0xC0255700<BR>esp 0xC0271FB4 0xC0271FA8<BR>ebp 0xC0271FC4<BR>esi 0xC0270000<BR>edi 0xC028BA00<BR>eip 0xC01160C5 0xC01160CC<BR>eflags 0x46<BR>cs 0x60<BR>ss 0x68<BR>ds 0x68<BR>es 0x68<BR>fs 0x0<BR>gs 0x0<BR>当0xC01160CC执行完后，那么当前进程的ESP 就放到了prev-&gt;thread.esp 里,也<BR>就是说0xC0270000 -&gt;thread.esp = 0xC0271FA8<BR>当0xC01160D2执行完后ESP = 0xC10D1FC4，这样就切换了堆栈，也就是说，<BR>现在切换了进程。但是，仅仅切换了堆栈进程还不能向下运行。还要做一些工作。<BR>当0xC01160E2 执行完后prev-&gt;thread.eip = 0xC01160ED, next-&gt;thread.eip =<BR>0xC0108F60 压入了堆栈。这个地址就是返回地址了（如果碰到ret 的话）对于我<BR>们线程来说，通过0xC01160E8 这行执行__switch_to 后就返回到0xC0108F60 去<BR>了,因为我们这里是新创建的线程。<BR>如果不是线程是进程呢,那下面那几句pop 就很有用了，这部分到了第二部分再<BR>讲。<BR>rest_init-&gt; cpu_idle()-&gt;schedule()-&gt;context_switch()-&gt;__switch_to()<BR>arch/i386/kernel/process.c L710<BR>tss-&gt;esp0 = next-&gt;esp0 = 0xC10D2000<BR>主要是处理TSS，当这个函数返回时，返回到ret_form_fork = 0xC0108F60，最<BR>后要运行到kernel_thread_helper = 0xC01071E4这里。通过call 0xC010502C，就<BR>到init 里去了。<BR>kernel_thread_helper arch/i386/kernel/process.c L494<BR>这是一个宏。做的事情很简单，首先是压参数，接着调用init 函数。执行完<BR>返回来的时候，再调用了do_exit，注消了进程。可以看到在Linux 的线程实现<BR>实际上就是使用一个进程来执行函数，接着注销进程。这种进程的开销还是很小<BR>的。回头看copy_process<BR>网络系统<BR>Linux的网络系统如图所示<BR>用户模式<BR>内核模式<BR>套接字文件系统<BR>TCP UDP<BR>IP<BR>PPP 以太网... ...<BR>ARP<BR>init()-&gt;do_basic_setup()-&gt; sock_init()<BR>主要是注册sockfs这个文件系统，也就实现了sock的初始化。<BR>现在我们看PF_INET 这种地址家族的注册<BR>#define module_init(x) __initcall(x) include/linux/init.h L112<BR>init()-&gt;do_basic_setup()-&gt;do_initcalls()-&gt;inet_init() net/ipv4/af_inet.c L1102<BR>init()-&gt;do_basic_setup()-&gt;do_initcalls()-&gt;inet_init()-&gt;sock_register()<BR>注册地址家族，这里注册的是inet_family地址家族<BR>在RH9 这个版本中最多支持32 种地址家族<BR>注册完PF_INET 家族之后就要注册PF_INET家族的协议<BR>inet_protocol_base很有趣，使用了C语言技巧<BR>我们看inet_protocol_base的定义<BR>#define IPPROTO_PREVIOUS &amp;udp_protocol<BR>static struct inet_protocol icmp_protocol = {<BR>handler: icmp_rcv,<BR>next: IPPROTO_PREVIOUS,<BR>protocol: IPPROTO_ICMP,<BR>name: "ICMP"<BR>};<BR>#undef IPPROTO_PREVIOUS<BR>#define IPPROTO_PREVIOUS &amp;icmp_protocol<BR>struct inet_protocol *inet_protocol_base = IPPROTO_PREVIOUS; L100<BR>所以它指向的是 icmp_protocol，但是我们再在protocol.c 里向上看，在定义<BR>icmp_protocol 时， IPPROTO_PREVIOUS 指向的是udp_protocol ， 所以<BR>icmp_protocol里的next = udp_protocol，同理，向上推，实际上这是在编译时就<BR>完成了一个链表。最后IPPROTO_PREVIOU 指的就是一个头。现在明白之后就<BR>知道循环是怎么回事了。<BR>通过inet_add_protocol把协议添加到inet_protos表中<BR>接着各种注册信息。<BR>整体来说，网络的初始化比较简单。我们会在第二部分从用户角度上来分析网络<BR>具体的流程。<BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/728.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:33 <a href="http://www.cnitblog.com/drizztzou/articles/728.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux 内核配置系统浅析</title><link>http://www.cnitblog.com/drizztzou/articles/727.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:31:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/727.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/727.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/727.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/727.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/727.html</trackback:ping><description><![CDATA[随着 Linux 操作系统的广泛应用，特别是 Linux 在嵌入式领域的发展，越来越多的人开始投身到 Linux 内核级的开发中。面对日益庞大的 Linux 内核源代码，开发者在完成自己的内核代码后，都将面临着同样的问题，即如何将源代码融入到 Linux 内核中，增加相应的 Linux 配置选项，并最终被编译进 Linux 内核。这就需要了解 Linux 的内核配置系统。<BR><BR>众所周知，Linux 内核是由分布在全球的 Linux 爱好者共同开发的，Linux 内核每天都面临着许多新的变化。但是，Linux 内核的组织并没有出现混乱的现象，反而显得非常的简洁，而且具有很好的扩展性，开发人员可以很方便的向 Linux 内核中增加新的内容。原因之一就是 Linux 采用了模块化的内核配置系统，从而保证了内核的扩展性。<BR><BR>本文首先分析了 Linux 内核中的配置系统结构，然后，解释了 Makefile 和配置文件的格式以及配置语句的含义，最后，通过一个简单的例子--TEST Driver，具体说明如何将自行开发的代码加入到 Linux 内核中。在下面的文章中，不可能解释所有的功能和命令，只对那些常用的进行解释，至于那些没有讨论到的，请读者参考后面的参考文献。<BR><BR>1． 配置系统的基本结构<BR><BR>Linux内核的配置系统由三个部分组成，分别是：<BR><BR>1, Makefile：分布在 Linux 内核源代码中的 Makefile，定义 Linux 内核的编译规则； <BR>2, 配置文件（config.in）：给用户提供配置选择的功能； <BR>3, 配置工具：包括配置命令解释器（对配置脚本中使用的配置命令进行解释）和配置用户界面（提供基于字符界面、基于 Ncurses 图形界面以及基于 Xwindows 图形界面的用户配置界面，各自对应于 Make config、Make menuconfig 和 make xconfig）。<BR><BR>这些配置工具都是使用脚本语言，如 Tcl/TK、Perl 编写的（也包含一些用 C 编写的代码）。本文并不是对配置系统本身进行分析，而是介绍如何使用配置系统。所以，除非是配置系统的维护者，一般的内核开发者无须了解它们的原理，只需要知道如何编写 Makefile 和配置文件就可以。所以，在本文中，我们只对 Makefile 和配置文件进行讨论。另外，凡是涉及到与具体 CPU 体系结构相关的内容，我们都以 ARM 为例，这样不仅可以将讨论的问题明确化，而且对内容本身不产生影响。<BR><BR>2． Makefile<BR><BR>2.1 Makefile 概述<BR><BR>Makefile 的作用是根据配置的情况，构造出需要编译的源文件列表，然后分别编译，并把目标代码链接到一起，最终形成 Linux 内核二进制文件。<BR><BR>由于 Linux 内核源代码是按照树形结构组织的，所以 Makefile 也被分布在目录树中。Linux 内核中的 Makefile 以及与 Makefile 直接相关的文件有：<BR><BR>1, Makefile：顶层 Makefile，是整个内核配置、编译的总体控制文件。 <BR>2, .config：内核配置文件，包含由用户选择的配置选项，用来存放内核配置后的结果（如 make config）。 <BR>3, arch/*/Makefile：位于各种 CPU 体系目录下的 Makefile，如 arch/arm/Makefile，是针对特定平台的 Makefile。 <BR>4, 各个子目录下的 Makefile：比如 drivers/Makefile，负责所在子目录下源代码的管理。 <BR>5, Rules.make：规则文件，被所有的 Makefile 使用。 <BR><BR>用户通过 make config 配置后，产生了 .config。顶层 Makefile 读入 .config 中的配置选择。顶层 Makefile 有两个主要的任务：产生 vmlinux 文件和内核模块（module）。为了达到此目的，顶层 Makefile 递归的进入到内核的各个子目录中，分别调用位于这些子目录中的 Makefile。至于到底进入哪些子目录，取决于内核的配置。在顶层 Makefile 中，有一句：include arch/$(ARCH)/Makefile，包含了特定 CPU 体系结构下的 Makefile，这个 Makefile 中包含了平台相关的信息。<BR><BR>位于各个子目录下的 Makefile 同样也根据 .config 给出的配置信息，构造出当前配置下需要的源文件列表，并在文件的最后有 include $(TOPDIR)/Rules.make。<BR><BR>Rules.make 文件起着非常重要的作用，它定义了所有 Makefile 共用的编译规则。比如，如果需要将本目录下所有的 c 程序编译成汇编代码，需要在 Makefile 中有以下的编译规则：<BR><BR>%.s: %.c<BR>$(CC) $(CFLAGS) -S $&lt; -o $@<BR><BR>有很多子目录下都有同样的要求，就需要在各自的 Makefile 中包含此编译规则，这会比较麻烦。而 Linux 内核中则把此类的编译规则统一放置到 Rules.make 中，并在各自的 Makefile 中包含进了 Rules.make（include Rules.make），这样就避免了在多个 Makefile 中重复同样的规则。对于上面的例子，在 Rules.make 中对应的规则为：<BR><BR>%.s: %.c<BR>$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$(*F)) $(CFLAGS_$@) -S $&lt; -o $@<BR><BR>2.2 Makefile 中的变量<BR><BR>顶层 Makefile 定义并向环境中输出了许多变量，为各个子目录下的 Makefile 传递一些信息。有些变量，比如 SUBDIRS，不仅在顶层 Makefile 中定义并且赋初值，而且在 arch/*/Makefile 还作了扩充。<BR><BR>常用的变量有以下几类：<BR><BR>1） 版本信息<BR>版本信息有：VERSION，PATCHLEVEL, SUBLEVEL, EXTRAVERSION，KERNELRELEASE。版本信息定义了当前内核的版本，比如 VERSION=2，PATCHLEVEL=4，SUBLEVEL=18，EXATAVERSION=-rmk7，它们共同构成内核的发行版本KERNELRELEASE：2.4.18-rmk7<BR><BR>2） CPU 体系结构：ARCH<BR>在顶层 Makefile 的开头，用 ARCH 定义目标 CPU 的体系结构，比如 ARCH:=arm 等。许多子目录的 Makefile 中，要根据 ARCH 的定义选择编译源文件的列表。<BR><BR>3） 路径信息：TOPDIR, SUBDIRS<BR>TOPDIR 定义了 Linux 内核源代码所在的根目录。例如，各个子目录下的 Makefile 通过 $(TOPDIR)/Rules.make 就可以找到 Rules.make 的位置。<BR>SUBDIRS 定义了一个目录列表，在编译内核或模块时，顶层 Makefile 就是根据 SUBDIRS 来决定进入哪些子目录。SUBDIRS 的值取决于内核的配置，在顶层 Makefile 中 SUBDIRS 赋值为 kernel drivers mm fs net ipc lib；根据内核的配置情况，在 arch/*/Makefile 中扩充了 SUBDIRS 的值，参见4）中的例子。<BR><BR>4） 内核组成信息：HEAD, CORE_FILES, NETWORKS, DRIVERS, LIBS<BR>Linux 内核文件 vmlinux 是由以下规则产生的：<BR><BR><BR>vmlinux: $(CONFIGURATION) init/main.o init/version.o linuxsubdirs<BR>$(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o \<BR>--start-group \<BR>$(CORE_FILES) \<BR>$(DRIVERS) \<BR>$(NETWORKS) \<BR>$(LIBS) \<BR>--end-group \<BR>-o vmlinux<BR><BR>可以看出，vmlinux 是由 HEAD、main.o、version.o、CORE_FILES、DRIVERS、NETWORKS 和 LIBS 组成的。这些变量（如 HEAD）都是用来定义连接生成 vmlinux 的目标文件和库文件列表。其中，HEAD在arch/*/Makefile 中定义，用来确定被最先链接进 vmlinux 的文件列表。比如，对于 ARM 系列的 CPU，HEAD 定义为： <BR>HEAD := arch/arm/kernel/head-$(PROCESSOR).o \<BR>arch/arm/kernel/init_task.o<BR><BR>表明 head-$(PROCESSOR).o 和 init_task.o 需要最先被链接到 vmlinux 中。PROCESSOR 为 armv 或 armo，取决于目标 CPU。 CORE_FILES，NETWORK，DRIVERS 和 LIBS 在顶层 Makefile 中定义，并且由 arch/*/Makefile 根据需要进行扩充。 CORE_FILES 对应着内核的核心文件，有 kernel/kernel.o，mm/mm.o，fs/fs.o，ipc/ipc.o，可以看出，这些是组成内核最为重要的文件。同时，arch/arm/Makefile 对 CORE_FILES 进行了扩充：<BR><BR># arch/arm/Makefile<BR><BR># If we have a machine-specific directory, then include it in the build.<BR>MACHDIR := arch/arm/mach-$(MACHINE)<BR>ifeq ($(MACHDIR),$(wildcard $(MACHDIR)))<BR>SUBDIRS += $(MACHDIR)<BR>CORE_FILES := $(MACHDIR)/$(MACHINE).o $(CORE_FILES)<BR>endif<BR><BR>HEAD := arch/arm/kernel/head-$(PROCESSOR).o \<BR>arch/arm/kernel/init_task.o<BR>SUBDIRS += arch/arm/kernel arch/arm/mm arch/arm/lib arch/arm/nwfpe<BR>CORE_FILES := arch/arm/kernel/kernel.o arch/arm/mm/mm.o $(CORE_FILES)<BR>LIBS := arch/arm/lib/lib.a $(LIBS)<BR><BR><BR>5） 编译信息：CPP, CC, AS, LD, AR，CFLAGS，LINKFLAGS<BR>在 Rules.make 中定义的是编译的通用规则，具体到特定的场合，需要明确给出编译环境，编译环境就是在以上的变量中定义的。针对交叉编译的要求，定义了 CROSS_COMPILE。比如：<BR><BR><BR>CROSS_COMPILE = arm-linux-<BR>CC = $(CROSS_COMPILE)gcc<BR>LD = $(CROSS_COMPILE)ld<BR>......<BR><BR>CROSS_COMPILE 定义了交叉编译器前缀 arm-linux-，表明所有的交叉编译工具都是以 arm-linux- 开头的，所以在各个交叉编译器工具之前，都加入了 $(CROSS_COMPILE)，以组成一个完整的交叉编译工具文件名，比如 arm-linux-gcc。<BR>CFLAGS 定义了传递给 C 编译器的参数。<BR>LINKFLAGS 是链接生成 vmlinux 时，由链接器使用的参数。LINKFLAGS 在 arm/*/Makefile 中定义，比如：<BR><BR># arch/arm/Makefile<BR><BR>LINKFLAGS :=-p -X -T arch/arm/vmlinux.lds<BR><BR><BR>6） 配置变量CONFIG_*<BR>.config 文件中有许多的配置变量等式，用来说明用户配置的结果。例如 CONFIG_MODULES=y 表明用户选择了 Linux 内核的模块功能。<BR>.config 被顶层 Makefile 包含后，就形成许多的配置变量，每个配置变量具有确定的值：y 表示本编译选项对应的内核代码被静态编译进 Linux 内核；m 表示本编译选项对应的内核代码被编译成模块；n 表示不选择此编译选项；如果根本就没有选择，那么配置变量的值为空。<BR><BR>2.3 Rules.make 变量<BR><BR>前面讲过，Rules.make 是编译规则文件，所有的 Makefile 中都会包括 Rules.make。Rules.make 文件定义了许多变量，最为重要是那些编译、链接列表变量。<BR><BR>O_OBJS，L_OBJS，OX_OBJS，LX_OBJS：本目录下需要编译进 Linux 内核 vmlinux 的目标文件列表，其中 OX_OBJS 和 LX_OBJS 中的 "X" 表明目标文件使用了 EXPORT_SYMBOL 输出符号。<BR><BR>M_OBJS，MX_OBJS：本目录下需要被编译成可装载模块的目标文件列表。同样，MX_OBJS 中的 "X" 表明目标文件使用了 EXPORT_SYMBOL 输出符号。<BR><BR>O_TARGET，L_TARGET：每个子目录下都有一个 O_TARGET 或 L_TARGET，Rules.make 首先从源代码编译生成 O_OBJS 和 OX_OBJS 中所有的目标文件，然后使用 $(LD) -r 把它们链接成一个 O_TARGET 或 L_TARGET。O_TARGET 以 .o 结尾，而 L_TARGET 以 .a 结尾。<BR><BR>2.4 子目录 Makefile<BR><BR>子目录 Makefile 用来控制本级目录以下源代码的编译规则。我们通过一个例子来讲解子目录 Makefile 的组成：<BR><BR>#<BR># Makefile for the linux kernel.<BR>#<BR># All of the (potential) objects that export symbols.<BR># This list comes from 'grep -l EXPORT_SYMBOL *.[hc]'.<BR><BR>export-objs := tc.o<BR><BR># Object file lists.<BR><BR>obj-y :=<BR>obj-m :=<BR>obj-n :=<BR>obj- :=<BR><BR>obj-$(CONFIG_TC) += tc.o<BR>obj-$(CONFIG_ZS) += zs.o<BR>obj-$(CONFIG_VT) += lk201.o lk201-map.o lk201-remap.o<BR><BR># Files that are both resident and modular: remove from modular.<BR><BR>obj-m := $(filter-out $(obj-y), $(obj-m))<BR><BR># Translate to Rules.make lists.<BR><BR>L_TARGET := tc.a<BR><BR>L_OBJS := $(sort $(filter-out $(export-objs), $(obj-y)))<BR>LX_OBJS := $(sort $(filter $(export-objs), $(obj-y)))<BR>M_OBJS := $(sort $(filter-out $(export-objs), $(obj-m)))<BR>MX_OBJS := $(sort $(filter $(export-objs), $(obj-m)))<BR><BR>include $(TOPDIR)/Rules.make<BR><BR>a) 注释<BR>对 Makefile 的说明和解释，由#开始。<BR><BR>b) 编译目标定义<BR>类似于 obj-$(CONFIG_TC) += tc.o 的语句是用来定义编译的目标，是子目录 Makefile 中最重要的部分。编译目标定义那些在本子目录下，需要编译到 Linux 内核中的目标文件列表。为了只在用户选择了此功能后才编译，所有的目标定义都融合了对配置变量的判断。<BR>前面说过，每个配置变量取值范围是：y，n，m 和空，obj-$(CONFIG_TC) 分别对应着 obj-y，obj-n，obj-m，obj-。如果 CONFIG_TC 配置为 y，那么 tc.o 就进入了 obj-y 列表。obj-y 为包含到 Linux 内核 vmlinux 中的目标文件列表；obj-m 为编译成模块的目标文件列表；obj-n 和 obj- 中的文件列表被忽略。配置系统就根据这些列表的属性进行编译和链接。<BR>export-objs 中的目标文件都使用了 EXPORT_SYMBOL() 定义了公共的符号，以便可装载模块使用。在 tc.c 文件的最后部分，有 "EXPORT_SYMBOL(search_tc_card);"，表明 tc.o 有符号输出。<BR>这里需要指出的是，对于编译目标的定义，存在着两种格式，分别是老式定义和新式定义。老式定义就是前面 Rules.make 使用的那些变量，新式定义就是 obj-y，obj-m，obj-n 和 obj-。Linux 内核推荐使用新式定义，不过由于 Rules.make 不理解新式定义，需要在 Makefile 中的适配段将其转换成老式定义。<BR><BR>c) 适配段<BR>适配段的作用是将新式定义转换成老式定义。在上面的例子中，适配段就是将 obj-y 和 obj-m 转换成 Rules.make 能够理解的 L_TARGET，L_OBJS，LX_OBJS，M_OBJS，MX_OBJS。<BR>L_OBJS := $(sort $(filter-out $(export-objs), $(obj-y))) 定义了 L_OBJS 的生成方式：在 obj-y 的列表中过滤掉 export-objs（tc.o），然后排序并去除重复的文件名。这里使用到了 GNU Make 的一些特殊功能，具体的含义可参考 Make 的文档（info make）。<BR><BR>d) include $(TOPDIR)/Rules.make<BR><BR>3． 配置文件<BR><BR>3.1 配置功能概述<BR><BR>除了 Makefile 的编写，另外一个重要的工作就是把新功能加入到 Linux 的配置选项中，提供此项功能的说明，让用户有机会选择此项功能。所有的这些都需要在 config.in 文件中用配置语言来编写配置脚本， <BR>在 Linux 内核中，配置命令有多种方式：<BR><BR>配置命令 解释脚本 <BR>Make config, make oldconfig scripts/Configure <BR>Make menuconfig scripts/Menuconfig <BR>Make xconfig scripts/tkparse <BR><BR>以字符界面配置（make config）为例，顶层 Makefile 调用 scripts/Configure， 按照 arch/arm/config.in 来进行配置。命令执行完后产生文件 .config，其中保存着配置信息。下一次再做 make config 将产生新的 .config 文件，原 .config 被改名为 .config.old<BR><BR>3.2 配置语言<BR><BR>1） 顶层菜单<BR>mainmenu_name /prompt/ /prompt/ 是用'或"包围的字符串，'与"的区别是'…'中可使用$引用变量的值。mainmenu_name 设置最高层菜单的名字，它只在 make xconfig 时才会显示。<BR><BR>2） 询问语句 <BR><BR>bool /prompt/ /symbol/<BR>hex /prompt/ /symbol/ /word/<BR>int /prompt/ /symbol/ /word/<BR>string /prompt/ /symbol/ /word/<BR>tristate /prompt/ /symbol/<BR><BR>询问语句首先显示一串提示符 /prompt/，等待用户输入，并把输入的结果赋给 /symbol/ 所代表的配置变量。不同的询问语句的区别在于它们接受的输入数据类型不同，比如 bool 接受布尔类型（ y 或 n ），hex 接受 16 进制数据。有些询问语句还有第三个参数 /word/，用来给出缺省值。<BR><BR>3） 定义语句 <BR><BR>define_bool /symbol/ /word/<BR>define_hex /symbol/ /word/<BR>define_int /symbol/ /word/<BR>define_string /symbol/ /word/<BR>define_tristate /symbol/ /word/<BR><BR>不同于询问语句等待用户输入，定义语句显式的给配置变量 /symbol/ 赋值 /word/。<BR><BR>4） 依赖语句 <BR><BR>dep_bool /prompt/ /symbol/ /dep/ ...<BR>dep_mbool /prompt/ /symbol/ /dep/ ...<BR>dep_hex /prompt/ /symbol/ /word/ /dep/ ...<BR>dep_int /prompt/ /symbol/ /word/ /dep/ ...<BR>dep_string /prompt/ /symbol/ /word/ /dep/ ...<BR>dep_tristate /prompt/ /symbol/ /dep/ ...<BR><BR>与询问语句类似，依赖语句也是定义新的配置变量。不同的是，配置变量/symbol/的取值范围将依赖于配置变量列表/dep/ …。这就意味着：被定义的配置变量所对应功能的取舍取决于依赖列表所对应功能的选择。以dep_bool为例，如果/dep/ …列表的所有配置变量都取值y，则显示/prompt/，用户可输入任意的值给配置变量/symbol/，但是只要有一个配置变量的取值为n，则/symbol/被强制成n。<BR>不同依赖语句的区别在于它们由依赖条件所产生的取值范围不同。<BR><BR>5） 选择语句<BR><BR><BR>choice /prompt/ /word/ /word/<BR><BR>choice 语句首先给出一串选择列表，供用户选择其中一种。比如 Linux for ARM 支持多种基于 ARM core 的 CPU，Linux 使用 choice 语句提供一个 CPU 列表，供用户选择：<BR><BR>choice 'ARM system type' \<BR>"Anakin CONFIG_ARCH_ANAKIN \<BR>Archimedes/A5000 CONFIG_ARCH_ARCA5K \<BR>Cirrus-CL-PS7500FE CONFIG_ARCH_CLPS7500 \<BR>……<BR>SA1100-based CONFIG_ARCH_SA1100 \<BR>Shark CONFIG_ARCH_SHARK" RiscPC<BR><BR>Choice 首先显示 /prompt/，然后将 /word/ 分解成前后两个部分，前部分为对应选择的提示符，后部分是对应选择的配置变量。用户选择的配置变量为 y，其余的都为 n。<BR><BR>6） if语句<BR><BR><BR>if [ /expr/ ] ; then<BR>/statement/ <BR>...<BR>fi<BR><BR>if [ /expr/ ] ; then<BR>/statement/<BR>...<BR>else<BR>/statement/<BR>...<BR>fi<BR><BR>if 语句对配置变量（或配置变量的组合）进行判断，并作出不同的处理。判断条件 /expr/ 可以是单个配置变量或字符串，也可以是带操作符的表达式。操作符有：=，!=，-o，-a 等。<BR><BR>7） 菜单块（menu block）语句 <BR><BR>mainmenu_option next_comment<BR>comment '…..'<BR>…<BR>endmenu<BR><BR>引入新的菜单。在向内核增加新的功能后，需要相应的增加新的菜单，并在新菜单下给出此项功能的配置选项。Comment 后带的注释就是新菜单的名称。所有归属于此菜单的配置选项语句都写在 comment 和 endmenu 之间。<BR><BR>8） Source 语句<BR>source /word/<BR>/word/ 是文件名，source 的作用是调入新的文件。<BR><BR>3.3 缺省配置<BR><BR>Linux 内核支持非常多的硬件平台，对于具体的硬件平台而言，有些配置就是必需的，有些配置就不是必需的。另外，新增加功能的正常运行往往也需要一定的先决条件，针对新功能，必须作相应的配置。因此，特定硬件平台能够正常运行对应着一个最小的基本配置，这就是缺省配置。<BR><BR>Linux 内核中针对每个 ARCH 都会有一个缺省配置。在向内核代码增加了新的功能后，如果新功能对于这个 ARCH 是必需的，就要修改此 ARCH 的缺省配置。修改方法如下（在 Linux 内核根目录下）：<BR><BR>1, 备份 .config 文件 <BR>2, cp arch/arm/deconfig .config <BR>3, 修改 .config <BR>4, cp .config arch/arm/deconfig <BR>5, 恢复 .config <BR><BR>如果新增的功能适用于许多的 ARCH，只要针对具体的 ARCH，重复上面的步骤就可以了。<BR><BR>3.4 help file<BR><BR>大家都有这样的经验，在配置 Linux 内核时，遇到不懂含义的配置选项，可以查看它的帮助，从中可得到选择的建议。下面我们就看看如何给给一个配置选项增加帮助信息。<BR><BR>所有配置选项的帮助信息都在 Documentation/Configure.help 中，它的格式为：<BR><BR>&lt;description&gt;<BR>&lt;variable name&gt;<BR>&lt;help file&gt;<BR><BR>&lt;description&gt; 给出本配置选项的名称，&lt;variable name&gt; 对应配置变量，&lt;help file&gt; 对应配置帮助信息。在帮助信息中，首先简单描述此功能，其次说明选择了此功能后会有什么效果，不选择又有什么效果，最后，不要忘了写上"如果不清楚，选择 N（或者）Y"，给不知所措的用户以提示。<BR><BR>4． 实例<BR><BR>对于一个开发者来说，将自己开发的内核代码加入到 Linux 内核中，需要有三个步骤。首先确定把自己开发代码放入到内核的位置；其次，把自己开发的功能增加到 Linux 内核的配置选项中，使用户能够选择此功能；最后，构建子目录 Makefile，根据用户的选择，将相应的代码编译到最终生成的 Linux 内核中去。下面，我们就通过一个简单的例子--test driver，结合前面学到的知识，来说明如何向 Linux 内核中增加新的功能。<BR><BR>4.1 目录结构<BR><BR>test driver 放置在 drivers/test/ 目录下：<BR><BR>$cd drivers/test<BR>$tree<BR>.<BR>|-- Config.in<BR>|-- Makefile<BR>|-- cpu<BR>| |-- Makefile<BR>| `-- cpu.c<BR>|-- test.c<BR>|-- test_client.c<BR>|-- test_ioctl.c<BR>|-- test_proc.c<BR>|-- test_queue.c<BR>`-- test<BR>|-- Makefile<BR>`-- test.c<BR><BR>4.2 配置文件<BR><BR>1） drivers/test/Config.in <BR><BR>#<BR># TEST driver configuration<BR>#<BR>mainmenu_option next_comment<BR>comment 'TEST Driver'<BR><BR>bool 'TEST support' CONFIG_TEST<BR>if [ "$CONFIG_TEST" = "y" ]; then<BR>tristate 'TEST user-space interface' CONFIG_TEST_USER<BR>bool 'TEST CPU ' CONFIG_TEST_CPU<BR>fi<BR><BR>endmenu<BR><BR>由于 test driver 对于内核来说是新的功能，所以首先创建一个菜单 TEST Driver。然后，显示 "TEST support"，等待用户选择；接下来判断用户是否选择了 TEST Driver，如果是（CONFIG_TEST=y），则进一步显示子功能：用户接口与 CPU 功能支持；由于用户接口功能可以被编译成内核模块，所以这里的询问语句使用了 tristate（因为 tristate 的取值范围包括 y、n 和 m，m 就是对应着模块）。<BR><BR>2） arch/arm/config.in<BR>在文件的最后加入：source drivers/test/Config.in，将 TEST Driver 子功能的配置纳入到 Linux 内核的配置中。<BR><BR>4.3 Makefile<BR><BR>1）drivers/test/Makefile<BR><BR><BR># drivers/test/Makefile<BR>#<BR># Makefile for the TEST.<BR>#<BR><BR>SUB_DIRS :=<BR>MOD_SUB_DIRS := $(SUB_DIRS)<BR>ALL_SUB_DIRS := $(SUB_DIRS) cpu<BR><BR>L_TARGET := test.a<BR>export-objs := test.o test_client.o<BR><BR>obj-$(CONFIG_TEST) += test.o test_queue.o test_client.o<BR>obj-$(CONFIG_TEST_USER) += test_ioctl.o<BR>obj-$(CONFIG_PROC_FS) += test_proc.o<BR><BR>subdir-$(CONFIG_TEST_CPU) += cpu<BR><BR>include $(TOPDIR)/Rules.make<BR><BR>clean:<BR>for dir in $(ALL_SUB_DIRS); do make -C $$dir clean; done<BR>rm -f *.[oa] .*.flags<BR><BR>drivers/test 目录下最终生成的目标文件是 test.a。在 test.c 和 test-client.c 中使用了 EXPORT_SYMBOL 输出符号，所以 test.o 和 test-client.o 位于 export-objs 列表中。然后，根据用户的选择（具体来说，就是配置变量的取值），构建各自对应的 obj-* 列表。由于 TEST Driver 中包一个子目录 cpu，当 CONFIG_TEST_CPU=y（即用户选择了此功能）时，需要将 cpu 目录加入到 subdir-y 列表中。<BR><BR>2）drivers/test/cpu/Makefile <BR><BR># drivers/test/test/Makefile<BR>#<BR># Makefile for the TEST CPU <BR>#<BR><BR>SUB_DIRS :=<BR>MOD_SUB_DIRS := $(SUB_DIRS)<BR>ALL_SUB_DIRS := $(SUB_DIRS)<BR><BR>L_TARGET := test_cpu.a<BR><BR>obj-$(CONFIG_test_CPU) += cpu.o<BR><BR><BR>include $(TOPDIR)/Rules.make<BR><BR>clean:<BR>rm -f *.[oa] .*.flags<BR><BR><BR>3）drivers/Makefile <BR><BR>……<BR>subdir-$(CONFIG_TEST) += test<BR>……<BR>include $(TOPDIR)/Rules.make<BR><BR>在 drivers/Makefile 中加入 subdir-$(CONFIG_TEST)+= test，使得在用户选择 TEST Driver 功能后，内核编译时能够进入 test 目录。<BR><BR>4）Makefile <BR><BR>……<BR>DRIVERS-$(CONFIG_PLD) += drivers/pld/pld.o<BR>DRIVERS-$(CONFIG_TEST) += drivers/test/test.a<BR>DRIVERS-$(CONFIG_TEST_CPU) += drivers/test/cpu/test_cpu.a<BR><BR>DRIVERS := $(DRIVERS-y)<BR>……<BR><BR>在顶层 Makefile 中加入 DRIVERS-$(CONFIG_TEST) += drivers/test/test.a 和 DRIVERS-$(CONFIG_TEST_CPU) += drivers/test/cpu/test_cpu.a。如何用户选择了 TEST Driver，那么 CONFIG_TEST 和 CONFIG_TEST_CPU 都是 y，test.a 和 test_cpu.a 就都位于 DRIVERS-y 列表中，然后又被放置在 DRIVERS 列表中。在前面曾经提到过，Linux 内核文件 vmlinux 的组成中包括 DRIVERS，所以 test.a 和 test_cpu.a 最终可被链接到 vmlinux 中。<BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/727.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:31 <a href="http://www.cnitblog.com/drizztzou/articles/727.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>linux内核分析----初始化</title><link>http://www.cnitblog.com/drizztzou/articles/724.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:30:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/724.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/724.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/724.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/724.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/724.html</trackback:ping><description><![CDATA[linux内核分析----初始化<BR>2004-04-23 15:18 pm<BR>作者：e4gle <BR>来自：Linux知识宝库<BR>联系方式：无名<BR><BR>参考我提供的三个文件：bootsect.txt,head.txt,setup.txt<BR>至于x86的引导无非如下步骤：<BR>1,cpu初始化自身，在固定位置执行一条指令。<BR>2,这条指令条转到bios中。<BR>3,bios找到启动设备并获取mbr,该mbr指向我们的lilo<BR>4，bios装载并把控制权交给lilo<BR>5，压缩内核自解压，并把控制权转交给解压内核。<BR>简单点讲，就是cpu成为内核引导程序的引导程序的引导程序的引导程序，西西。<BR>这时内核将跳转到start_kernel是/init/main.c的重点函数，main.c函数很多定义都是为此函数服务的，这里我简要介绍一下这个函数的初始化流程。<BR><BR>初始化内核：<BR>从start_kernel函数（/init/main.c）开始系统初始化工作，好，我们首先分析这个函数：<BR>函数开始首先：<BR>#ifdef __SMP__<BR>static int boot_cpu = 1;<BR>/* "current" has been set up, we need to load it now *//*定义双处理器用*/<BR>if (!boot_cpu)<BR>initialize_secondary();<BR>boot_cpu = 0;<BR>#endif<BR><BR>定义双处理器。<BR><BR>printk(linux_banner); /*打印linux banner*/<BR>打印内核标题信息。<BR><BR>开始初始化自身的部分组件（包括内存，硬件终端，调度等），我来逐个分析其中的函数：<BR>setup_arch(&amp;command_line, &amp;memory_start, &amp;memory_end);/*初始化内存*/<BR>返回内核参数和内核可用的物理地址范围<BR>函数原型如下：<BR>setup_arch(char **, unsigned long *, unsigned long *);<BR>返回内存起始地址：<BR>memory_start = paging_init(memory_start,memory_end);<BR>看看paging_init的定义，是初始化请求页：<BR>paging_init(unsigned long start_mem, unsigned long end_mem)（会在以后的内存管理子系统分析时详细介绍）<BR>{<BR>int i;<BR>struct memclust_struct * cluster;<BR>struct memdesc_struct * memdesc;<BR><BR>/* initialize mem_map[] */<BR>start_mem = free_area_init(start_mem, end_mem);/*遍历查找内存的空闲页*/<BR><BR>/* find free clusters, update mem_map[] accordingly */<BR>memdesc = (struct memdesc_struct *)<BR>(hwrpb-&gt;mddt_offset + (unsigned long) hwrpb);<BR>cluster = memdesc-&gt;cluster;<BR>for (i = memdesc-&gt;numclusters ; i &gt; 0; i--, cluster++) {<BR>unsigned long pfn, nr;<BR><BR>/* Bit 0 is console/PALcode reserved. Bit 1 is<BR>non-volatile memory -- we might want to mark<BR>this for later */<BR>if (cluster-&gt;usage &amp; 3)<BR>continue;<BR>pfn = cluster-&gt;start_pfn;<BR>if (pfn &gt;= MAP_NR(end_mem)) /* if we overrode mem size */<BR>continue;<BR>nr = cluster-&gt;numpages;<BR>if ((pfn + nr) &gt; MAP_NR(end_mem)) /* if override in cluster */<BR>nr = MAP_NR(end_mem) - pfn;<BR><BR>while (nr--)<BR>clear_bit(PG_reserved, &amp;mem_map[pfn++].flags);<BR>}<BR><BR>memset((void *) ZERO_PAGE(0), 0, PAGE_SIZE);<BR><BR>return start_mem;<BR>}<BR><BR>trap_init(); 初始化硬件中断<BR>/arch/i386/kernel/traps.c文件里定义此函数<BR><BR>sched_init() 初始化调度<BR>/kernel/sched.c文件里有详细的调度算法（这些会在以后进程管理和调度的结构分析中详细介绍）<BR><BR>parse_options(command_line) 分析传给内核的各种选项（随后再详细介绍）<BR><BR>memory_start = console_init(memory_start,memory_end) 初始化控制台<BR><BR>memory_start = kmem_cache_init(memory_start, memory_end) 初始化内核内存cache（同样，在以后的内存管理分析中介绍此函数）<BR><BR>sti()；接受硬件中断<BR><BR>kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGHAND);<BR>current-&gt;need_resched = 1; need_resched标志增加，调用schedule（调度里面会详细说明）<BR>cpu_idle(NULL) 进入idle循环以消耗空闲的cpu时间片<BR><BR>已经基本完成内核初始化工作，已经把需要完成的少量责任传递给了init，所身于地工作不过是进入idle循环以消耗空闲的cpu时间片。所以在这里调用了cpu_idle(NULL)，它从不返回，所以当有实际工作好处理时，该函数就会被抢占。<BR><BR>parse_options函数：<BR>static void __init parse_options(char *line)/*参数收集在一条长命令行中，内核被赋给指向该命令行头部的指针*/<BR>{<BR>char *next;<BR>char *quote;<BR>int args, envs;<BR><BR>if (!*line)<BR>return;<BR>args = 0;<BR>envs = 1;/* TERM is set to 'linux' by default */<BR>next = line;<BR>while ((line = next) != NULL) {<BR><BR>quote = strchr(line,'"');<BR>next = strchr(line, ' ');<BR>while (next != NULL &amp;&amp; quote != NULL &amp;&amp; quote &lt; next) {<BR><BR>next = strchr(quote+1, '"');<BR>if (next != NULL) {<BR>quote = strchr(next+1, '"');<BR>next = strchr(next+1, ' ');<BR>}<BR>}<BR>if (next != NULL)<BR>*next++ = 0;<BR>/*<BR>* check for kernel options first..<BR>*/<BR>if (!strcmp(line,"ro")) {<BR>root_mountflags |= MS_RDONLY;<BR>continue;<BR>}<BR>if (!strcmp(line,"rw")) {<BR>root_mountflags &amp;= ~MS_RDONLY;<BR>continue;<BR>}<BR>if (!strcmp(line,"debug")) {<BR>console_loglevel = 10;<BR>continue;<BR>}<BR>if (!strcmp(line,"quiet")) {<BR>console_loglevel = 4;<BR>continue;<BR>}<BR>if (!strncmp(line,"init=",5)) {<BR>line += 5;<BR>execute_command = line;<BR>args = 0;<BR>continue;<BR>}<BR>if (checksetup(line))<BR>continue;<BR><BR>if (strchr(line,'=')) {<BR>if (envs &gt;= MAX_INIT_ENVS)<BR>break;<BR>envp_init[++envs] = line;<BR>} else {<BR>if (args &gt;= MAX_INIT_ARGS)<BR>break;<BR>argv_init[++args] = line;<BR>}<BR>}<BR>argv_init[args+1] = NULL;<BR>envp_init[envs+1] = NULL;<BR>}<BR><BR><BR><BR>[这个贴子最后由e4gle在 2002/08/25 06:30pm 编辑]<BR><BR>////////////////////////////////////////////////////////////////////<BR>// setup.txt<BR>// Copyright(C) 2001, Feiyun Wang<BR>////////////////////////////////////////////////////////////////////<BR>// analysis on linux/arch/i386/boot/setup.S (for linux 2.2.17)<BR>////////////////////////////////////////////////////////////////////<BR><BR>////////////////////////////////////////////////////////////////////<BR>// export the margin tags for .text, .data and .bss<BR>{<BR>.text<BR>begtext:<BR><BR>.data<BR>begdata:<BR><BR>.bss<BR>begbss:<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>.text<BR>start()<BR>SYSSEG = 0x1000<BR>SETUPSEG = 0x9020<BR>modelist = end of .text:<BR>{<BR>// if loaded by bootsect.S,<BR>// you can assume CS=SETUPSEG (=0x9020), otherwise...<BR>goto start_of_setup();<BR><BR>/*<BR><A href="http://lxr.linux.no/source/Documentation/i386/boot.txt" target=_blank><FONT color=#002c99>http://lxr.linux.no/source/Documentation/i386/boot.txt</FONT></A><BR>Offset/Size Proto Name Meaning<BR>01F1/1 ALL setup_sects The size of the setup in sectors<BR>01F2/2 ALL root_flags If set, the root is mounted readonly<BR>01F4/2 ALL syssize DO NOT USE - for bootsect.S use only<BR>01F6/2 ALL swap_dev DO NOT USE - obsolete<BR>01F8/2 ALL ram_size DO NOT USE - for bootsect.S use only<BR>01FA/2 ALL vid_mode Video mode control<BR>01FC/2 ALL root_dev Default root device number<BR>01FE/2 ALL boot_flag 0xAA55 magic number<BR>0200/2 2.00+ jump Jump instruction<BR>0202/4 2.00+ header Magic signature "HdrS"<BR>0206/2 2.00+ version Boot protocol version supported<BR>0208/4 2.00+ realmode_swtch Boot loader hook<BR>020C/4 2.00+ start_sys_seg Points to kernel version string<BR>0210/1 2.00+ type_of_loader Boot loader identifier<BR>0211/1 2.00+ loadflags Boot protocol option flags<BR>0212/2 2.00+ setup_move_size Move to high memory size (used with hooks)<BR>0214/4 2.00+ code32_start Boot loader hook<BR>0218/4 2.00+ ramdisk_image initrd load address (set by boot loader)<BR>021C/4 2.00+ ramdisk_size initrd size (set by boot loader)<BR>0220/4 2.00+ bootsect_kludge DO NOT USE - for bootsect.S use only<BR>0224/4 2.01+ heap_end_ptr Free memory after setup end<BR>0226/2 N/A pad1 Unused<BR>0228/4 2.02+ cmd_line_ptr 32-bit pointer to the kernel command line<BR>*/<BR>.ascii "HdrS"<BR>.word 0x0201<BR>realmode_swtch: // boot loader hook<BR>.word 0, 0<BR>start_sys_seg: // pointer to kernel version string<BR>.word SYSSEG<BR>.word kernel_version<BR>type_of_loader:<BR>.byte 0<BR>loadflags:<BR>#ifndef __BIG_KERNEL__<BR>.byte 0x00<BR>#else<BR>.byte LOADED_HIGH = 1<BR>#endif<BR>setup_move_size:<BR>.word 0x8000<BR>code32_start: // boot loader hook<BR>#ifndef __BIG_KERNEL__<BR>.long 0x1000<BR>#else<BR>.long 0x100000<BR>#endif<BR>ramdisk_image: // initrd load address (set by boot loader)<BR>.long 0<BR>ramdisk_size: // initrd size (set by boot loader)<BR>.long 0<BR>bootsect_kludge: // DO NOT USE - for bootsect.S use only<BR>.word bootsect_helper(), SETUPSEG<BR>heap_end_ptr: // free memory after setup end<BR>.word modelist+1024<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// check signature to see if all code loaded<BR>start_of_setup()<BR>{<BR>// get disk type, bootlin depends on this<BR>// <A href="http://www.ctyme.com/intr/rb-0639.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-0639.htm</FONT></A><BR>int13h/AH=15h(AL=0, DL=0x81);<BR><BR>#ifdef SAFE_RESET_DISK_CONTROLLER<BR>int13h/AH=0(AL=0, DL=0x80); // reset hd0<BR>#endif<BR><BR>// check signature at the end of setup code<BR>if (setup_sig1!=SIG1 || setup_sig2!=SIG2) {<BR>// since size of setup may &gt; 4 sectors,<BR>// the rest code may be loaded at SYSSEG<BR>goto bad_sig;<BR>}<BR>goto goodsig1;<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// some small functions<BR>prtstr() { /* ... print ASCIIz string at DS:SI */}<BR>prtsp2() { /* ... print double space */ }<BR>prtspc() { /* ... print single space */ }<BR>prtchr() { /* ... print ASCII AL */ }<BR>beep() { /* ... beep */ }<BR><BR>////////////////////////////////////////////////////////////////////<BR>goodsig1() { goto goodsig; } // making near jumps<BR><BR>////////////////////////////////////////////////////////////////////<BR>// move rest setup code from SYSSEG:0 to CS:0800<BR>// TODO: it won't work if image loaded at 0x100000?<BR>bad_sig()<BR>DELTA_INITSEG = 0x0020 (= SETUPSEG - INITSEG)<BR>SYSSEG = 0x1000<BR>{<BR>BX = (CS-DELTA_INITSEG):[497]; // i.e. setup_sects<BR>// first 4 sectors have been loaded<BR>CX = (BX - 4) &lt;&lt; 8; // rest code in words<BR>start_sys_seg = (CX &gt;&gt; 3) + SYSSEG; // real system code start<BR><BR>move SYSSEG:0 to CS:0800 (CX*2 bytes);<BR><BR>if (setup_sig1!=SIG1 || setup_sig2!=SIG2) {<BR>prtstr("No setup signature found ...");<BR>halt;<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// check if loader compatible with image<BR>good_sig()<BR>LOADHIGH = 1<BR>{<BR>if ((loadflags &amp; LOADHIGH) &amp;&amp; (!type_of_loader)) {<BR>// Nope, old loader want to load big kernel<BR>prtstr("Wrong loader: giving up.");<BR>halt;<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// get memory size<BR>// set the keyboard repeat rate to max<BR>// check video adapter<BR>// get hd0 &amp; hd1 data<BR>// check for Micro Channel (MCA) bus<BR>// check for PS/2 pointing device<BR>// check for APM BIOS<BR>// move code to INITSEG/SETUPSEG<BR>// load IDT and GDT<BR>// enable and test A20<BR>// reset coprocessor<BR>// reprogram the interrupts<BR>// switch to protected mode<BR>// goto KERNEL<BR>loader_ok()<BR>SETUPSET = 0x9020<BR>INITSEG = 0x9000<BR>DELTA_INITSEG = 0x20<BR>{<BR>// DS = CS - DELTA_INITSEG when entering this function<BR><BR>// get memory size<BR>#ifndef STANDARD_MEMORY_BIOS_CALL<BR>(double word)DS:[0x1E0] = 0;<BR>try {<BR>// get memory size for &gt;64M configurations<BR>// <A href="http://www.ctyme.com/intr/rb-1739.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-1739.htm</FONT></A><BR>int15h/AX=E801h;<BR>// AX = extended memory between 1M and 16M, in KB<BR>// BX = extended memory above 16M, in 64K blocks<BR>(double word)DS:[0x1E0] = ((EBX &amp; 0xFFFF) &lt;&lt; 6)<BR>+ (EAX &amp; 0xFFFF);<BR>}<BR>#else<BR>(double word)DS:[0x1E0] = 0;<BR>#endif<BR><BR>// get extended memory size<BR>// <A href="http://www.ctyme.com/intr/rb-1529.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-1529.htm</FONT></A><BR>int15h/AH=88h;<BR>DS:[2] = AX; // KB of contiguous memory from 100000h<BR><BR>// set the keyboard repeat rate to max<BR>// <A href="http://www.ctyme.com/intr/rb-1757.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-1757.htm</FONT></A><BR>int16h/AX=0305h(BX=0);<BR><BR>// check video adapter and its parameters, see video.S<BR>video();<BR><BR>// get hd0 &amp; hd1 data<BR>// <A href="http://www.ctyme.com/intr/rb-6135.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-6135.htm</FONT></A><BR>// <A href="http://www.ctyme.com/intr/rb-6184.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-6184.htm</FONT></A><BR>// pointers in 0:0104 &amp; 0:0118 respectively<BR>move hd0 data to CS-DELTA_INITSEG:0080 (16 bytes);<BR>move hd1 data to CS-DELTA_INITSEG:0090 (16 bytes);<BR><BR>// get disk type, check if hd1 exists<BR>// <A href="http://www.ctyme.com/intr/rb-0639.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-0639.htm</FONT></A><BR>int13h/AH=15h(AL=0, DL=0x81);<BR>if (failed || AH!=03h) { // AH=03h if is a hard disk<BR>clear CS-DELTA_INITSEG:0090 (16 bytes);<BR>}<BR><BR>// check for Micro Channel (MCA) bus<BR>DS:[0xA0] = 0; // set table length to 0<BR>try {<BR>// get system configuration<BR>// <A href="http://www.ctyme.com/intr/rb-1594.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-1594.htm</FONT></A><BR>int15h/AH=C0h; // ES:BX = ROM configuration table<BR>move ROM configuration table to CS-DELTA_INITSEG:00A0;<BR>// first 16 bytes only<BR>}<BR><BR>// check PS/2 pointing device<BR>DS:[0x1FF] = 0;<BR>// get equipment list<BR>// <A href="http://www.ctyme.com/intr/rb-0575.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-0575.htm</FONT></A><BR>int11h();<BR>if (has psmouse) {<BR>DS:[0x1FF] = 0xAA;<BR>}<BR><BR>#ifdef CONFIG_APM<BR>// check for APM BIOS<BR><BR>DS:[0x40] = 0;<BR>// Advanced Power Management v1.0+ - installation check<BR>// <A href="http://www.ctyme.com/intr/rb-1394.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-1394.htm</FONT></A><BR>int15h/AX=5300h(BX=0);<BR>// check both CF and BX for APM support<BR>if (APM &amp;&amp; 32-bit protected mode interface supported) {<BR>int15h/AX=5304h(BX=0); // disconnect interface<BR>try {<BR>// clear return values first<BR>clear EBX, CX, DX, ESI, DI;<BR>// connect 32bit protect mode APM interface<BR>// <A href="http://www.ctyme.com/intr/rb-1397.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-1397.htm</FONT></A><BR>int15h/AX=5303h(BX=0);<BR>if (supported) {<BR>DS:[0x42] = 32-bit code segment base address;<BR>DS:[0x44] = offset of entry point;<BR>DS:[0x48] = 16-bit code segment base address;<BR>DS:[0x4A] = 16-bit data segment base address;<BR>DS:[0x4E] = APM BIOS code segment length;<BR>DS:[0x52] = APM BIOS data segment length;<BR>int15h/AX=5300h(BX=0); // check again<BR>if (APM supported) {<BR>INITSET:[0x40] = APM version;<BR>INITSET:[0x4C] = APM flags;<BR>}<BR>else { // should not happen<BR>// disconnect interface<BR>int15h/AX=5304h(BX=0);<BR>}<BR>}<BR>else {<BR>// clear 32bit support<BR>INITSET:[0x4C] &amp;= ~0x0002;<BR>}<BR>}<BR>}<BR>#endif<BR><BR>// call mode switch<BR>if (realmode_swtch) {<BR>far realmode_swtch(); // mode switch hook<BR>}<BR>else {<BR>far default_switch(); // see below<BR>}<BR><BR>// set code32: 0x100000 for big kernel, otherwise 0x1000<BR>(double word) code32 = code32_start;<BR>if (!(loadflags &amp; LOADED_HIGH)) {<BR>// normal low loaded zImage<BR>move start_sys_seg:0 to (0100:0 ... CS-DELTA_INITSEG:0);<BR>// move 0x1000 bytes each time<BR>}<BR>if (CS!=SETUPSEG) {<BR>cli; // disable interrupts<BR><BR>// store new SS in DX<BR>DX = SS;<BR>AX = CS - DELTA_INITSEG;<BR>if (DX&gt;=AX) {<BR>DX = DX + INITSEG - AX; // i.e. SS-CS+SETUPSEG<BR>}<BR><BR>// move CS-DELTA_INITSEG:0 to INITSEG:0 (setup_move_size bytes)<BR>// TODO: why not do this in one step?<BR>ES = INITSEG;<BR>move _DOWNWARD_ from CS-DELTA_INITSEG:setup_move_size-1 to<BR>(INITSEG:setup_move_size-1<BR>... INITSEG:move_self_here+0x200);<BR>// setup_move_size-move_self_here-0x200 bytes<BR>// INITSEG:move_self_here+0x200 = SETUPSEG:move_self_here<BR>goto SETUPSEG:move_self_here;<BR>move_self_here:<BR>move the rest to INITSEG:move_self_here+0x200-1 ... INITSEG:0;<BR>// move_self_here+0x200 bytes<BR>DS = SETUPSEG;<BR>SS = DX;<BR>}<BR>// CS==SETUPSEG is true now<BR><BR>// Protected Mode Basics<BR>// <A href="http://x86.ddj.com/articles/pmbasics/tspec_a1_doc.htm" target=_blank><FONT color=#002c99>http://x86.ddj.com/articles/pmbasics/tspec_a1_doc.htm</FONT></A><BR><BR>lidt idt_48; // load idt with 0, 0;<BR>// new code added here in linux 2.4<BR>lgdt gdt_48; // load gdt with whatever appropriate;<BR><BR>// enable A20<BR>empty_8042();<BR>outportb(0x64, 0xD1); // command write<BR>empty_8042();<BR>outportb(0x60, 0xDF); // A20 on<BR>empty_8042();<BR><BR>#define TEST_ADDR 0x7C<BR><BR>// test A20<BR>GS = AX = 0xFFFF;<BR>BX = 0:[TEST_ADDR];<BR>do {<BR>0:[TEST_ADDR] = ++AX;<BR>} while (AX==GS:[TEST_ADDR+0x10]);<BR>0:[TEST_ADDR] = BX;<BR><BR>// reset coprocessor<BR>outportb(0xF0, 0);<BR>delay();<BR>outportb(0xF1, 0);<BR>delay();<BR><BR>// reprogram the interrupts<BR>outportb(0x20, 0x11); // initialize 8259A-1<BR>delay();<BR>outportb(0xA0, 0x11); // initialize 8259A-2<BR>delay();<BR>outportb(0x21, 0x20); // start of hardware int's (0x20)<BR>delay();<BR>outportb(0xA1, 0x28); // start of hardware int's 2 (0x28)<BR>delay();<BR>outportb(0x21, 0x04); // 8259-1 is master<BR>delay();<BR>outportb(0xA1, 0x02); // 8259-2 is slave<BR>delay();<BR>outportb(0x21, 0x01); // 8086 mode<BR>delay();<BR>outportb(0xA1, 0x01); // 8086 mode<BR>delay();<BR>outportb(0xA1, 0xFF); // mask off all interrupts for now<BR>delay();<BR>outportb(0x21, 0xFB); // mask all irq's but irq2, which is cascaded<BR><BR>// protected mode!<BR>mov ax, #1;<BR>lmsw ax;<BR>jmp flush_instr;<BR>flush_instr:<BR>xor bx, bx;<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>{<BR>db 0x66, 0xea<BR>code32:<BR>dd 0x1000<BR>dw __KERNEL_CS // 0x10, defined in asm-i386/segment.h<BR>// goto 10:1000 or 10:100000<BR><BR>kernel_version:<BR>.ascii UTS_RELEASE // defined in makefile<BR>.ascii "("<BR>.ascii LINUX_COMPILE_BY<BR>.ascii "@"<BR>.ascii LINUX_COMPILE_HOST<BR>.ascii ")"<BR>.ascii UTS_VERSION<BR>db 0<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// default real mode switch routine<BR>far default_switch()<BR>{<BR>cli;<BR>outportb(0x70, 0x80); // disable NMI<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// get called when using bootsect loader _AND_ have bzImage to load<BR>far bootsect_helper(ES)<BR>{<BR>if (bootsect_es==0) {<BR>type_of_loader = 0x20; // bootsect-loader, version 0<BR>AX = ES &gt;&gt; 4;<BR>CS:[bootsect_src_base+2] = AH;<BR>bootsect_es = ES;<BR>AX = ES - SYSSEG;<BR>}<BR>else {<BR>if (BX==0) { // 64K aligned<BR>// copy extended memory<BR>// <A href="http://www.ctyme.com/intr/rb-1527.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-1527.htm</FONT></A><BR>try {<BR>int15h/AX=87h(CX=0x8000,<BR>ES:SI=CS:bootsect_gdt);<BR>}<BR>catch {<BR>bootsect_panic:<BR>prtstr("INT15 refuses to access high memory."<BR>" Giving up.");<BR>halt;<BR>}<BR>ES = bootsect_es;<BR>CS:[bootsect_dst_base+2]++;<BR>}<BR>AH = CS:[bootsect_dst_base+2] &lt;&lt; 4;<BR>AL = 0;<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>{<BR>bootsect_gdt:<BR>.word 0, 0, 0, 0<BR>.word 0, 0, 0, 0<BR>bootsect_src:<BR>.word 0xFFFF<BR>bootsect_src_base:<BR>.byte 0, 0, 1 ! base = 0x010000<BR>.byte 0x93 ! typbyte<BR>.word 0 ! limit16, base24 =0<BR>bootsect_dst:<BR>.word 0xFFFF<BR>bootsect_dst_base:<BR>.byte 0, 0, 0x10 ! base = 0x100000<BR>.byte 0x93 ! typbyte<BR>.word 0 ! limit16, base24 =0<BR>.word 0, 0, 0, 0 ! used by BIOS<BR>.word 0, 0, 0, 0<BR>bootsect_es:<BR>.word 0<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// check that the keyboard command queue is empty<BR>empty_8042()<BR>{<BR>timeout = 0xFFFFFF; // local variable<BR>for (;<IMG class=inlineimg title=Wink alt="" src="http://www.gd-linux.org/bbs/images/smilies/wink.gif" border=0> {<BR>if (--timeout==0) return;<BR>delay();<BR>inportb(0x64, AL); // 8042 status port<BR>if (AL &amp; 1) { // output buffer<BR>delay();<BR>inportb(0x60, .);<BR>continue;<BR>}<BR>if (!(AL &amp; 2)) return; // input buffer<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// read the CMOS clock, return the seconds in AL<BR>gettime()<BR>{<BR>// get real-time clock time<BR>// <A href="http://www.ctyme.com/intr/rb-2273.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-2273.htm</FONT></A><BR>int1Ah/AH=02h(); // DH = seconds (in BCD)<BR>AL = DH &amp; 0x0F;<BR>AH = DH &gt;&gt; 4;<BR>AAD;<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>delay() { /* needed after doing I/O */ }<BR><BR>////////////////////////////////////////////////////////////////////<BR>{<BR>gdt:<BR>.word 0,0,0,0 ! dummy<BR>.word 0,0,0,0 ! unused<BR><BR>.word 0xFFFF ! 4Gb - (0x100000*0x1000 = 4Gb)<BR>.word 0x0000 ! base address=0<BR>.word 0x9A00 ! code read/exec<BR>.word 0x00CF ! granularity=4096, 386 (+5th nibble of limit)<BR><BR>.word 0xFFFF ! 4Gb - (0x100000*0x1000 = 4Gb)<BR>.word 0x0000 ! base address=0<BR>.word 0x9200 ! data read/write<BR>.word 0x00CF ! granularity=4096, 386 (+5th nibble of limit)<BR>idt_48:<BR>.word 0 ! idt limit=0<BR>.word 0, 0 ! idt base=0L<BR>gdt_48:<BR>.word 0x800 ! gdt limit=2048, 256 GDT entries<BR>.word 512+gdt, 0x9 ! gdt base = 0x9XXXX<BR>// +512 because of bootsect<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// Included video setup &amp; detection code "video.S"<BR>/*<BR>Positions of various video parameters passed to the kernel<BR>(see also include/linux/tty.h)<BR>CS-DELTA_INITSEG segment<BR>#define PARAM_CURSOR_POS 0x00<BR>#define PARAM_VIDEO_PAGE 0x04<BR>#define PARAM_VIDEO_MODE 0x06<BR>#define PARAM_VIDEO_COLS 0x07<BR>#define PARAM_VIDEO_EGA_BX 0x0a<BR>#define PARAM_VIDEO_LINES 0x0e<BR>#define PARAM_HAVE_VGA 0x0f<BR>#define PARAM_FONT_POINTS 0x10<BR>#define PARAM_LFB_WIDTH 0x12<BR>#define PARAM_LFB_HEIGHT 0x14<BR>#define PARAM_LFB_DEPTH 0x16<BR>#define PARAM_LFB_BASE 0x18<BR>#define PARAM_LFB_SIZE 0x1c<BR>#define PARAM_LFB_LINELENGTH 0x24<BR>#define PARAM_LFB_COLORS 0x26<BR>#define PARAM_VESAPM_SEG 0x2e<BR>#define PARAM_VESAPM_OFF 0x30<BR>#define PARAM_LFB_PAGES 0x32<BR>*/<BR>video()<BR>{<BR>FS = DS;<BR>DS = ES = CS;<BR>GS = 0;<BR>basic_detect();<BR><BR>#ifdef CONFIG_VIDEO_SELECT<BR>if (FS:[0x1FA]!=ASK_VGA) {<BR>// ASK_VGA=0xFFFD, defined in asm-i386/boot.h<BR>mode_set();<BR>if (failed) {<BR>prtstr("You passed an undefined mode number.");<BR>}<BR>}<BR>else {<BR>mode_menu();<BR>}<BR><BR>#ifdef CONFIG_VIDEO_RETAIN<BR>restore_screen();<BR>#endif<BR><BR>#endif<BR><BR>mode_params();<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>SIG1 = 0xAA55<BR>SIG2 = 0x5A5A<BR>{<BR>setup_sig1: .word SIG1<BR>setup_sig2: .word SIG2<BR>modelist:<BR><BR>.text<BR>endtext:<BR><BR>.data<BR>enddata:<BR><BR>.bss<BR>endbss:<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// end of file<BR><BR><BR><BR><BR><BR><BR><BR>WSS(<A href="http://www.whitecell.org/" target=_blank><FONT color=#002c99>http://www.whitecell.org</FONT></A>)<BR>一个非营利性民间技术组织<BR>致力于各种系统安全技术的研究<BR>坚持传统的hacker精神<BR>追求技术的精纯<BR><BR>编辑　　发贴时间2002/08/25 06:23pm　此 IP 为代理服务器IP: 已设置保密<BR>该用户目前不在线 e4gle <BR>　帅哥 此人为版主<BR>头衔: 论坛版主 午马 天秤座<BR><BR><BR><BR>级别: 精灵<BR>魅力: 2286<BR>经验: 1824<BR>金钱: 5165 幽妮石<BR>来自: WSS　<BR>总发贴数: 232 篇<BR>注册日期: 2002/08/14<BR>消息　查看　搜索　好友　邮件　主页　复制　引用　回复贴子回复　<BR><BR>[这个贴子最后由e4gle在 2002/08/25 06:30pm 编辑]<BR><BR>////////////////////////////////////////////////////////////////////<BR>// bootsect.txt<BR>////////////////////////////////////////////////////////////////////<BR>// Copyright(C) 2001, Feiyun Wang<BR>// analysis on linux/arch/i386/boot/bootsect.S (linux 2.2.17)<BR>////////////////////////////////////////////////////////////////////<BR><BR>////////////////////////////////////////////////////////////////////<BR>#if 0<BR>int 3;// for debugger<BR>#endif<BR><BR>_main()<BR>BOOTSEG = 0x07C0<BR>INITSEG = 0x9000<BR>{<BR>move BOOTSEG:0 to INITSEG:0 (512 bytes);<BR>goto INITSEG:go; // CS:IP = INITSEG:go<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// prepare disk parameter table<BR>go()<BR>INITSEG = 0x9000<BR>{<BR>set SS:SP to INITSEG:3FF4;// 0x4000-0x0C<BR>copy disk parameter table (pointer in 0:0078)<BR>to INITSEG:3FF4 (12 bytes);<BR>patch sector count to 36 (offset 4 in parameter table, 1 byte);<BR>set disk parameter table pointer (0:0078) to INITSEG:3FF4;<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// load the setup-sectors directly after the bootblock<BR>load_setup()<BR>setup_sects = SETUPSECS = 4<BR>INITSEG = 0x9000<BR>{<BR>for(;<IMG class=inlineimg title=Wink alt="" src="http://www.gd-linux.org/bbs/images/smilies/wink.gif" border=0> {<BR>int13h/AH=0(DL=0); // reset FDC<BR>try {<BR>// <A href="http://www.ctyme.com/intr/rb-0607.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-0607.htm</FONT></A><BR>int13h/AH=02h(AL=setup_sects,<BR>ES:BX=INITSEG:0200,<BR>CX=2, DX=0);<BR>break;<BR>}<BR>catch (disk error) {<BR>print_nl();<BR>print_hex(SP);<BR>}<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// get disk drive parameters, specifically sectors#/track<BR>ok_load_setup()<BR>global variables: disksizes, sectors<BR>{<BR>#if 0<BR>// get disk drive parameters<BR>// <A href="http://www.ctyme.com/intr/rb-0621.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-0621.htm</FONT></A><BR>int13h/AH=08h(DL=0);<BR>CH = 0;<BR>// seems not completed yet<BR>#else<BR>// probe sectors with disksize[] = {36, 18, 15, 9}<BR>SI = &amp;disksizes;<BR>for (;<IMG class=inlineimg title=Wink alt="" src="http://www.gd-linux.org/bbs/images/smilies/wink.gif" border=0> {<BR>sectors = DS:[SI++];<BR>if (SI&gt;=disksizes+4) break;<BR>try {<BR>int13h/AH=02h(AL=1,<BR>ES:BX=INITSEG<IMG class=inlineimg title=Frown alt="" src="http://www.gd-linux.org/bbs/images/smilies/frown.gif" border=0>(setup_sects+1)&lt;&lt;9),<BR>CX=sectors, DX=0);<BR>break;<BR>}<BR>catch {<BR>}<BR>}<BR>#endif<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// print out "Loading"<BR>// load the system image<BR>// set root_dev<BR>// jump to the setup-routine loaded directly after the bootblock<BR>got_sectors()<BR>INITSEG = 0x9000<BR>SYSSEG = 0x1000<BR>SETUPSEG = 0x9020<BR>global variable: root_dev<BR>{<BR>// int10h/AH=03h <A href="http://www.ctyme.com/intr/rb-0088.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-0088.htm</FONT></A><BR>// int10h/AH=13h <A href="http://www.ctyme.com/intr/rb-0210.htm" target=_blank><FONT color=#002c99>http://www.ctyme.com/intr/rb-0210.htm</FONT></A><BR>print out "Loading";<BR><BR>read_it(ES=SYSSEG);<BR>kill_motor();<BR>print_nl();<BR><BR>if (!root_dev) {<BR>switch (sectors) {<BR>case 15: root_dev = 0x0208;// /dev/ps0 - 1.2Mb<BR>break;<BR>case 18: root_dev = 0x021C;// /dev/PS0 - 1.44Mb<BR>break;<BR>case 36: root_dev = 0x0220;// /dev/fd0H2880 - 2.88Mb<BR>break;<BR>default: root_dev = 0x0200;// /dev/fd0 - autodetect<BR>break;<BR>}<BR>}<BR><BR>goto SETUPSEG:0;<BR>// see linux/arch/i386/boot/setup.S<BR>}<BR><BR>word sread = 0; // sectors read of current track<BR>word head = 0; // current head<BR>word track = 0; // current track<BR>////////////////////////////////////////////////////////////////////<BR>// load the system image<BR>read_it(ES)<BR>setup_sects = SETUPSECS = 4<BR>SYSSEG = 0x1000<BR>syssize = SYSSIZE = 0x7F00<BR>{<BR>sread = setup_sects + 1; // plus 1 bootsect<BR>if (ES &amp; 0x0fff) halt; // not 64KB aligned<BR>BX = 0;<BR><BR>for (;<IMG class=inlineimg title=Wink alt="" src="http://www.gd-linux.org/bbs/images/smilies/wink.gif" border=0> {<BR>rp_read:<BR>#ifdef __BIG_KERNEL__<BR>.word 0x1eff, 0x220;<BR>// call far * bootsect_kludge, see setup.S<BR>#else<BR>AX = ES - SYSSEG;<BR>#endif<BR>if (AX&gt;syssize) return;<BR><BR>ok1_read:<BR>// get proper AL (sectors to read),<BR>// not to across tracks or make BX overflow<BR>AX = sectors - sread;<BR>CX = BX + (AX &lt;&lt; 9);<BR>// TODO: I think CX!=0 can be omitted<BR>if (CX overflow &amp;&amp; CX!=0) {<BR>AX = (-BX) &gt;&gt; 9;<BR>}<BR>ok2_read:<BR>read_track(AL, ES:BX);<BR>CX = AX;<BR>AX += sread;<BR>if (AX==sectors) {<BR>if (head==1) track++;<BR>head = 1 - head;<BR>AX = 0;<BR>}<BR>ok3_read:<BR>sread = AX;<BR>BX += CX &lt;&lt; 9;<BR>if (BX overflow) {<BR>ES += 0x1000;<BR>BX = 0;<BR>}<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// read disk with (sread, track, head)<BR>read_track(AL, ES:BX)<BR>{<BR>for (;<IMG class=inlineimg title=Wink alt="" src="http://www.gd-linux.org/bbs/images/smilies/wink.gif" border=0> {<BR>printf(".");<BR><BR>// set CX, DX according to (sread, track, head)<BR>DX = track;<BR>CX = sread + 1;<BR>CH = DL;<BR><BR>DX = head;<BR>DH = DL;<BR>DX &amp;= 0x0100;<BR><BR>try {<BR>int13h/AH=02h(AL, ES:BX, CX, DX);<BR>return;<BR>}<BR>catch (disk error) {<BR>bad_rt:<BR>print_all();<BR>int13h/AH=0h(DL=0); // reset FDC<BR>}<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// some small functions<BR>print_all() { /* ... print out error, AX, BX, CX, DX */ }<BR>print_nl() { /* ... print CR LF */ }<BR>print_hex() { /* ... print the word pointed by SS:BP in hex */ }<BR>kill_motor() { outportb(0x3F2, 0); /* turn off floppy drive motor */}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// global variables for bootsect.S<BR>{<BR>sectors:<BR>.word 0<BR>disksizes:<BR>.byte 36, 18, 15, 9<BR>msg1:<BR>.byte 13, 10<BR>.ascii "Loading"<BR><BR>/*<BR><A href="http://lxr.linux.no/source/Documentation/i386/boot.txt" target=_blank><FONT color=#002c99>http://lxr.linux.no/source/Documentation/i386/boot.txt</FONT></A><BR>Offset/Size Proto Name Meaning<BR>01F1/1 ALL setup_sects The size of the setup in sectors<BR>01F2/2 ALL root_flags If set, the root is mounted readonly<BR>01F4/2 ALL syssize DO NOT USE - for bootsect.S use only<BR>01F6/2 ALL swap_dev DO NOT USE - obsolete<BR>01F8/2 ALL ram_size DO NOT USE - for bootsect.S use only<BR>01FA/2 ALL vid_mode Video mode control<BR>01FC/2 ALL root_dev Default root device number<BR>01FE/2 ALL boot_flag 0xAA55 magic number<BR>*/<BR>.org 497<BR>setup_sects:<BR>.byte SETUPSECS<BR>root_flags:<BR>.word CONFIG_ROOT_RDONLY<BR>syssize:<BR>.word SYSSIZE<BR>swap_dev:<BR>.word SWAP_DEV<BR>ram_size:<BR>.word RAMDISK<BR>vid_mode:<BR>.word SVGA_MODE<BR>root_dev:<BR>.word ROOT_DEV<BR>boot_flag:<BR>.word 0xAA55<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// end of file<BR><BR><BR><BR><BR>[这个贴子最后由e4gle在 2002/08/25 06:30pm 编辑]<BR><BR>////////////////////////////////////////////////////////////////////<BR>// head.txt<BR>// Copyright(C) 2001, Feiyun Wang<BR>////////////////////////////////////////////////////////////////////<BR>// analysis on linux/arch/i386/kernel/head.S (for linux 2.2.17)<BR>////////////////////////////////////////////////////////////////////<BR><BR>////////////////////////////////////////////////////////////////////<BR>startup_32()<BR>__KERNEL_DS = 0x18;<BR>CL_MAGIC = 0xA33F;<BR>CL_MAGIC_ADDR = 0x90020;<BR>CL_BASE_ADDR = 0x90000;<BR>CL_OFFSET = 0x90022;<BR>{<BR>cld;<BR>DS = ES = FS = GS = __KERNEL_DS (= 0x18);<BR><BR>#ifdef __SMP__<BR>if (BX) {<BR>CR4 |= mmu_cr4_features-__PAGE_OFFSET;<BR>// mmu_cr4_features defined in arch/i386/mm/init.c<BR>// __PAGE_OFFSET defined in include/asm-i386/page.h<BR>}<BR>#endif<BR><BR>CR3 = 0x101000;// page table pointer<BR>CR0 |= 0x80000000;// set PG bit<BR><BR>SS:ESP = stack_start;<BR><BR>#ifdef __SMP__<BR>if (BX) {<BR>EFLAG = 0;<BR>goto checkCPUtype;<BR>}<BR>#endif<BR><BR>clear BSS (__bss_start .. _end);<BR>setup_idt();<BR>EFLAGS = 0;<BR>move 0x90000 to empty_zero_page (i.e. 0x5000) (2 KByte);<BR>clear empty_zero_page+2K (2 KByte);<BR><BR>if (CL_MAGIC==*(CL_MAGIC_ADDR)) {<BR>move *(CL_OFFSET)+CL_BASE_ADDR to empty_zero_page+2K (2 KByte);<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>checkCPUtype()<BR>global variables: ready<BR>// see include/asm-i386/processor.h struct cpuinfo_x86<BR>// boot_cpu_data defined in arch/i386/kernel/setup.c<BR>struct cpuinfo_x86 boot_cpu_data;<BR>__KERNEL_DS = 0x18;<BR>{<BR>X86_CPUID = -1;<BR><BR>X86 = 3;<BR>save original EFLAGS;<BR>check AC bit in EFLAGS;<BR>if (AC bit not changed) goto is386;<BR><BR>X86 = 4;<BR>check ID bit in EFLAGS;<BR>restore original EFLAGS;<BR>if (ID bit not changed) goto is486;<BR><BR>// get CPU info,<BR>// &lt;&gt; Vol.2 P.3-110<BR>CPUID(EAX=0);<BR>X86_CPUID = EAX;<BR>X86_VENDOR_ID = EBX;<BR>*((&amp;X86_VENDOR_ID)+4) = ECX;<BR>*((&amp;X86_VENDOR_ID)+8) = EDX;<BR>if (!EAX) goto is486;<BR><BR>CPUID(EAX=1);<BR>CL = AL;<BR>X86 = AH &amp; 0x0f;// family<BR>X86_MODEL = (AL &amp; 0xf0) &gt;&gt; 4;// model<BR>X86_MASK = AL &amp; 0x0f;// stepping id<BR>X86_CAPABILITY = EDX;// feature<BR><BR>is486:<BR>// save PG, PE &amp; ET, set AM, WP, NE &amp; MP<BR>EAX = (CR0 &amp; 0x80000011) | 0x50022;<BR>goto 2f;<BR>is386:<BR>restore original EFLAGS;<BR>// save PG, PE &amp; ET, set MP<BR>EAX = (CR0 &amp; 0x80000011) | 0x02;<BR>2f:<BR>CR0 = EAX;<BR>check_x87();<BR><BR>#ifdef __SMP__<BR>if (ready) {<BR>CR4 |= 0x10;// set PSE, turn on 4 MByte pages<BR>CR3 = CR3;<BR>}<BR>ready++;<BR>#endif;<BR><BR>lgdt gdt_descr;<BR>lidt idt_descr;<BR>DS = ES = FS = GS = __KERNEL_DS (= 0x18);<BR><BR>#ifdef __SMP__<BR>SS = __KERNEL_DS (= 0x18);<BR>#else<BR>SS:ESP = stack_start;<BR>#endif<BR><BR>lldt 0;<BR>cld;<BR>start_kernel();<BR>halt;<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>{<BR>#ifdef __SMP__<BR>ready:.byte 0;<BR>#endif<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>check_x87()<BR>{<BR>X86_HARD_MATH = 0;<BR>clts;<BR>fninit;<BR>fstsw ax;<BR>if (al) {<BR>// no coprocessor, set EM;<BR>// TODO; why not use |=?<BR>cr0 ^= 0x04;<BR>}<BR>else {<BR>X86_HARD_MATH = 1;<BR>fsetpm;// 0xDB, 0xE4<BR>}<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>setup_idt()<BR>{<BR>edx = &amp;ignore_int;<BR>eax = __KERNEL_CS &lt;&lt; 16;<BR>ax = dx;<BR>dx = 0x8E00;// interrupt gate, dpl = 0, present<BR><BR>set all entries in idt_table to eax:edx;// 256*8 Bytes<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>{<BR>stack_start:<BR>.long init_task_union+8192;<BR>.long __KERNEL_DS;<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>ignore_init()<BR>{<BR>printk("Unknown interrupt ");<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>{<BR>NR_TASKS = 512;// defined in include/linux/tasks.h<BR>IDT_ENTRIES = 256;<BR>GDT_ENTRIES = 12+2*NR_TASKS;<BR><BR>.word 0;<BR>idt_descr:<BR>.word IDT_ENTRIES*8-1;<BR>idt:<BR>.long idt_table;<BR><BR>.word 0;<BR>gdt_descr:<BR>.word GDT_ENTRIES*8-1;<BR>gdt:<BR>.long gdt_table;<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>{<BR>.org 0x1000;<BR>swapper_pg_dir:<BR>.long 0x00102007;<BR>.fill __USER_PGD_PTRS-1, 4, 0;// 767 entries<BR>.long 0x00102007;<BR>.fill __KERNEL_PGD_PTRS-1, 4, 0;// 255 entries<BR><BR>.org 0x2000;<BR>pg0:<BR>// ...<BR><BR>.org 0x3000;<BR>empty_bad_page:<BR><BR>.org 0x4000;<BR>empty_bad_page_table:<BR><BR>.org 0x5000;<BR>empty_zero_page:<BR><BR>.org 0x6000;<BR>.data<BR>gdt_table:<BR>.quad 0x0000000000000000;// null<BR>.quad 0x0000000000000000;// not used<BR>.quad 0x00cf9a000000ffff;// 0x10 kernel 4GB code at 0x00000000<BR>.quad 0x00cf92000000ffff;// 0x18 kernel 4GB data at 0x00000000<BR>.quad 0x00cffa000000ffff;// 0x20 user 4GB code at 0x00000000<BR>.quad 0x00cff2000000ffff;// 0x28 user 4GB data at 0x00000000<BR>.quad 0x0000000000000000;// not used<BR>.quad 0x0000000000000000;// not used<BR><BR>.quad 0x0040920000000000;// 0x40 APM setup for bad BIOS<BR>.quad 0x00409a0000000000;// 0x48 APM CS code<BR>.quad 0x00009a0000000000;// 0x50 APM CS 16 code (16 bit)<BR>.quad 0x0040920000000000;// 0x58 APM DS data<BR>.fill 2*NR_TASKS, 8, 0;<BR><BR>.section .text.lock<BR>stext_lock:<BR>}<BR><BR>////////////////////////////////////////////////////////////////////<BR>// end of file<BR><BR><BR><BR><BR><BR>全文结束 <BR><BR><BR>所有分类 <BR>1: 非技术类 <BR><BR>2: 基础知识 <BR><BR>3: 指令大全 <BR><BR>4: shell <BR><BR>5: 安装启动 <BR><BR>6: xwindow <BR><BR>7: kde <BR><BR>8: gnome <BR><BR>9: 输入法类 <BR><BR>10: 美化汉化 <BR><BR>11: 网络配置 <BR><BR>12: 存储备份 <BR><BR>13: 杂项工具 <BR><BR>14: 编程技术 <BR><BR>15: 网络安全 <BR><BR>16: 内核技术 <BR><BR>17: 速度优化 <BR><BR>18: apache <BR><BR>19: email <BR><BR>20: ftp服务 <BR><BR>21: cvs服务 <BR><BR>22: 代理服务 <BR><BR>23: samba <BR><BR>24: 域名服务 <BR><BR>25: 网络过滤 <BR><BR>26: 其他服务 <BR><BR>27: nfs <BR><BR>28: oracle <BR><BR>29: dhcp <BR><BR>30: mysql <BR><BR>31: php <BR><BR>32: ldap <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/724.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:30 <a href="http://www.cnitblog.com/drizztzou/articles/724.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>linux内核分析---系统调用实现代码分析</title><link>http://www.cnitblog.com/drizztzou/articles/725.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:30:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/725.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/725.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/725.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/725.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/725.html</trackback:ping><description><![CDATA[2001年6月5日<BR>启动早就读完，现在为了写笔记再从启动之后粗略的大体读一遍，基本就是几个大模块：启动和初始化，<BR>中断信号，进程及调度，内存管理，文件系统，网络，驱动和模块等，我主要也从这几块入手。由于启动<BR>部分在start_kernel之前牵涉到大量的x86体系相关的汇编知识，需要大量的时间，于是我跳过，先把握<BR>整个系统的大体脉络，然后做二次，三次分析。网络部分的分析，我会从4.4BSD-Lite的代码中分析。<BR><BR>系统调用：<BR><BR>先说一下系统调用，奇怪的很，所有的读核资料都没有把系统调用单独提出来说，我觉得还是比较重要的<BR>。用户和系统内核通信的关键的枢纽，不过分吧，呵呵。仔细研究一下它的机制，准备花三天时间，手头<BR>有些书和资料，帮助我理解。<BR><BR>概念：（明晰一下基本概念）<BR>系统调用发生在用户进程，通过一些特殊的函数（如open）来请求内核提供服务，这时，用户进程挂起，<BR>内核验证用户请求，尝试之行，并把结果反馈给用户进程，接着用户进程重新启动。这些机制在一般的编<BR>程书里都有，我就是来通过源代码的实现来讨论这种机制。<BR><BR>具体实现代码：arch/i386/kernel/entry.S（内核版本2.2.14）<BR>从entry.S的第171行，就是system_call开始，171-248行代码贴出来，分析以注释形式：<BR><BR>ENTRY(system_call) \所有系统调用的入口点，参数system_call是所希望激活的系统调用的数<BR>pushl %eax# 保存orig_eax，这个值就是希望系统调用数<BR>SAVE_ALL <BR>/*SAVE_ALL宏定义如下：<BR>#define SAVE_ALL <BR>cld; <BR>pushl %es; <BR>pushl %ds; <BR>pushl %eax; <BR>pushl %ebp; <BR>pushl %edi; <BR>pushl %esi; <BR>pushl %edx; <BR>pushl %ecx; <BR>pushl %ebx; <BR>movl $(__KERNEL_DS),%edx; <BR>movl %dx,%ds; <BR>movl %dx,%es;<BR>他的作用是先把所有寄存器的值压栈，然后在system_call返回之前使用RESTORE_ALL把栈从栈中弹出<BR>，在这其中system_call可以根据需要子去使用寄存器的值。任何它调用的c函数都可以从栈中查找到所希<BR>望的参数，因为SAVE_ALL已经把所有寄存器的值都压入栈中了 */<BR>--------------------------------------------------------------------------------------------<BR>GET_CURRENT(%ebx) /*利用GET_CURRENT宏从ebx中取得当前任务指针，GET_CURRENT宏定义<BR>如下： <BR>#define GET_CURRENT(reg) <BR>movl %esp, reg; <BR>andl $-8192, reg;这段代码应该很好理解，把esp指移到reg变量，减去8129得到当前任务地址<BR>*/<BR>--------------------------------------------------------------------------------------------<BR>cmpl $(NR_syscalls),%eax /*察看保存在eax中的系统调用数是否超过最大数（常数NR_syscalls代表系统调用的最大数）如果确实超过了，请看下面一句:jae badsys，程序则跳转到badsys<BR>。*/<BR>jae badsys<BR>testb $0x20,flags(%ebx)# PF_TRACESYS/*检查系统调用是否正在被跟踪*/<BR>jne tracesys /*如果系统调用被跟踪，则程序跳转到tracesys*/<BR>call *SYMBOL_NAME(sys_call_table)(,%eax,4)/*调用系统函数*/<BR>/*SYMBOL_NAME宏不处理任何工作，只是简单的被文本参数（也就是系统调用名）所替换，所以可以忽略<BR>sys_call_table也定义在entry.S（373行）中，是一张由指向实现各种系统调用的内核函数的函数指针组<BR>成的表：<BR>ENTRY(sys_call_table)<BR>.long SYMBOL_NAME(sys_ni_syscall)/* 0 - old "setup()" system call*/<BR>.long SYMBOL_NAME(sys_exit)<BR>.long SYMBOL_NAME(sys_fork)<BR>.long SYMBOL_NAME(sys_read)<BR>.long SYMBOL_NAME(sys_write)<BR>.long SYMBOL_NAME(sys_open)/* 5 */<BR>.long SYMBOL_NAME(sys_close)<BR>.long SYMBOL_NAME(sys_waitpid)<BR>.long SYMBOL_NAME(sys_creat)<BR>.long SYMBOL_NAME(sys_link)<BR>.long SYMBOL_NAME(sys_unlink)/* 10 */<BR>.long SYMBOL_NAME(sys_execve)<BR>.long SYMBOL_NAME(sys_chdir)<BR>.long SYMBOL_NAME(sys_time)<BR>.long SYMBOL_NAME(sys_mknod)<BR>.long SYMBOL_NAME(sys_chmod)/* 15 */<BR>.long SYMBOL_NAME(sys_lchown)<BR>.long SYMBOL_NAME(sys_ni_syscall)/* old break syscall holder */<BR>.long SYMBOL_NAME(sys_stat)<BR>.long SYMBOL_NAME(sys_lseek)<BR>.long SYMBOL_NAME(sys_getpid)/* 20 */<BR>.long SYMBOL_NAME(sys_mount)<BR>.long SYMBOL_NAME(sys_oldumount)<BR>.long SYMBOL_NAME(sys_setuid)<BR>.long SYMBOL_NAME(sys_getuid)<BR>.long SYMBOL_NAME(sys_stime)/* 25 */<BR>.long SYMBOL_NAME(sys_ptrace)<BR>.long SYMBOL_NAME(sys_alarm)<BR>.long SYMBOL_NAME(sys_fstat)<BR>.long SYMBOL_NAME(sys_pause)<BR>.long SYMBOL_NAME(sys_utime)/* 30 */<BR>.long SYMBOL_NAME(sys_ni_syscall)/* old stty syscall holder */<BR>.long SYMBOL_NAME(sys_ni_syscall)/* old gtty syscall holder */<BR>.long SYMBOL_NAME(sys_access)<BR>.long SYMBOL_NAME(sys_nice)<BR>.long SYMBOL_NAME(sys_ni_syscall)/* 35 */ /* old ftime syscall holder */<BR>.long SYMBOL_NAME(sys_sync)<BR>.long SYMBOL_NAME(sys_kill)<BR>.long SYMBOL_NAME(sys_rename)<BR>.long SYMBOL_NAME(sys_mkdir)<BR>.long SYMBOL_NAME(sys_rmdir)/* 40 */<BR>.long SYMBOL_NAME(sys_dup)<BR>.long SYMBOL_NAME(sys_pipe)<BR>.long SYMBOL_NAME(sys_times)<BR>.long SYMBOL_NAME(sys_ni_syscall)/* old prof syscall holder */<BR>.long SYMBOL_NAME(sys_brk)/* 45 */<BR>.long SYMBOL_NAME(sys_setgid)<BR>.long SYMBOL_NAME(sys_getgid)<BR>.long SYMBOL_NAME(sys_signal)<BR>.long SYMBOL_NAME(sys_geteuid)<BR>.long SYMBOL_NAME(sys_getegid)/* 50 */<BR>.long SYMBOL_NAME(sys_acct)<BR>.long SYMBOL_NAME(sys_umount)/* recycled never used phys() */<BR>.long SYMBOL_NAME(sys_ni_syscall)/* old lock syscall holder */<BR>.long SYMBOL_NAME(sys_ioctl)<BR>.long SYMBOL_NAME(sys_fcntl)/* 55 */<BR>.long SYMBOL_NAME(sys_ni_syscall)/* old mpx syscall holder */<BR>.long SYMBOL_NAME(sys_setpgid)<BR>.long SYMBOL_NAME(sys_ni_syscall)/* old ulimit syscall holder */<BR>.long SYMBOL_NAME(sys_olduname)<BR>.long SYMBOL_NAME(sys_umask)/* 60 */<BR>.long SYMBOL_NAME(sys_chroot)<BR>.long SYMBOL_NAME(sys_ustat)<BR>.long SYMBOL_NAME(sys_dup2)<BR>.long SYMBOL_NAME(sys_getppid)<BR>.long SYMBOL_NAME(sys_getpgrp)/* 65 */<BR>.long SYMBOL_NAME(sys_setsid)<BR>.long SYMBOL_NAME(sys_sigaction)<BR>.long SYMBOL_NAME(sys_sgetmask)<BR>.long SYMBOL_NAME(sys_ssetmask)<BR>.long SYMBOL_NAME(sys_setreuid)/* 70 */<BR>.long SYMBOL_NAME(sys_setregid)<BR>.long SYMBOL_NAME(sys_sigsuspend)<BR>.long SYMBOL_NAME(sys_sigpending)<BR>.long SYMBOL_NAME(sys_sethostname)<BR>.long SYMBOL_NAME(sys_setrlimit)/* 75 */<BR>.long SYMBOL_NAME(sys_getrlimit)<BR>.long SYMBOL_NAME(sys_getrusage)<BR>.long SYMBOL_NAME(sys_gettimeofday)<BR>.long SYMBOL_NAME(sys_settimeofday)<BR>.long SYMBOL_NAME(sys_getgroups)/* 80 */<BR>.long SYMBOL_NAME(sys_setgroups)<BR>.long SYMBOL_NAME(old_select)<BR>.long SYMBOL_NAME(sys_symlink)<BR>.long SYMBOL_NAME(sys_lstat)<BR>.long SYMBOL_NAME(sys_readlink)/* 85 */<BR>.long SYMBOL_NAME(sys_uselib)<BR>.long SYMBOL_NAME(sys_swapon)<BR>.long SYMBOL_NAME(sys_reboot)<BR>.long SYMBOL_NAME(old_readdir)<BR>.long SYMBOL_NAME(old_mmap)/* 90 */<BR>.long SYMBOL_NAME(sys_munmap)<BR>.long SYMBOL_NAME(sys_truncate)<BR>.long SYMBOL_NAME(sys_ftruncate)<BR>.long SYMBOL_NAME(sys_fchmod)<BR>.long SYMBOL_NAME(sys_fchown)/* 95 */<BR>.long SYMBOL_NAME(sys_getpriority)<BR>.long SYMBOL_NAME(sys_setpriority)<BR>.long SYMBOL_NAME(sys_ni_syscall)/* old profil syscall holder */<BR>.long SYMBOL_NAME(sys_statfs)<BR>.long SYMBOL_NAME(sys_fstatfs)/* 100 */<BR>.long SYMBOL_NAME(sys_ioperm)<BR>.long SYMBOL_NAME(sys_socketcall)<BR>.long SYMBOL_NAME(sys_syslog)<BR>.long SYMBOL_NAME(sys_setitimer)<BR>.long SYMBOL_NAME(sys_getitimer)/* 105 */<BR>.long SYMBOL_NAME(sys_newstat)<BR>.long SYMBOL_NAME(sys_newlstat)<BR>.long SYMBOL_NAME(sys_newfstat)<BR>.long SYMBOL_NAME(sys_uname)<BR>.long SYMBOL_NAME(sys_iopl)/* 110 */<BR>.long SYMBOL_NAME(sys_vhangup)<BR>.long SYMBOL_NAME(sys_idle)<BR>.long SYMBOL_NAME(sys_vm86old)<BR>.long SYMBOL_NAME(sys_wait4)<BR>.long SYMBOL_NAME(sys_swapoff)/* 115 */<BR>.long SYMBOL_NAME(sys_sysinfo)<BR>.long SYMBOL_NAME(sys_ipc)<BR>.long SYMBOL_NAME(sys_fsync)<BR>.long SYMBOL_NAME(sys_sigreturn)<BR>.long SYMBOL_NAME(sys_clone)/* 120 */<BR>.long SYMBOL_NAME(sys_setdomainname)<BR>.long SYMBOL_NAME(sys_newuname)<BR>.long SYMBOL_NAME(sys_modify_ldt)<BR>.long SYMBOL_NAME(sys_adjtimex)<BR>.long SYMBOL_NAME(sys_mprotect)/* 125 */<BR>.long SYMBOL_NAME(sys_sigprocmask)<BR>.long SYMBOL_NAME(sys_create_module)<BR>.long SYMBOL_NAME(sys_init_module)<BR>.long SYMBOL_NAME(sys_delete_module)<BR>.long SYMBOL_NAME(sys_get_kernel_syms)/* 130 */<BR>.long SYMBOL_NAME(sys_quotactl)<BR>.long SYMBOL_NAME(sys_getpgid)<BR>.long SYMBOL_NAME(sys_fchdir)<BR>.long SYMBOL_NAME(sys_bdflush)<BR>.long SYMBOL_NAME(sys_sysfs)/* 135 */<BR>.long SYMBOL_NAME(sys_personality)<BR>.long SYMBOL_NAME(sys_ni_syscall)/* for afs_syscall */<BR>.long SYMBOL_NAME(sys_setfsuid)<BR>.long SYMBOL_NAME(sys_setfsgid)<BR>.long SYMBOL_NAME(sys_llseek)/* 140 */<BR>.long SYMBOL_NAME(sys_getdents)<BR>.long SYMBOL_NAME(sys_select)<BR>.long SYMBOL_NAME(sys_flock)<BR>.long SYMBOL_NAME(sys_msync)<BR>.long SYMBOL_NAME(sys_readv)/* 145 */<BR>.long SYMBOL_NAME(sys_writev)<BR>.long SYMBOL_NAME(sys_getsid)<BR>.long SYMBOL_NAME(sys_fdatasync)<BR>.long SYMBOL_NAME(sys_sysctl)<BR>.long SYMBOL_NAME(sys_mlock)/* 150 */<BR>.long SYMBOL_NAME(sys_munlock)<BR>.long SYMBOL_NAME(sys_mlockall)<BR>.long SYMBOL_NAME(sys_munlockall)<BR>.long SYMBOL_NAME(sys_sched_setparam)<BR>.long SYMBOL_NAME(sys_sched_getparam) /* 155 */<BR>.long SYMBOL_NAME(sys_sched_setscheduler)<BR>.long SYMBOL_NAME(sys_sched_getscheduler)<BR>.long SYMBOL_NAME(sys_sched_yield)<BR>.long SYMBOL_NAME(sys_sched_get_priority_max)<BR>.long SYMBOL_NAME(sys_sched_get_priority_min) /* 160 */<BR>.long SYMBOL_NAME(sys_sched_rr_get_interval)<BR>.long SYMBOL_NAME(sys_nanosleep)<BR>.long SYMBOL_NAME(sys_mremap)<BR>.long SYMBOL_NAME(sys_setresuid)<BR>.long SYMBOL_NAME(sys_getresuid)/* 165 */<BR>.long SYMBOL_NAME(sys_vm86)<BR>.long SYMBOL_NAME(sys_query_module)<BR>.long SYMBOL_NAME(sys_poll)<BR>.long SYMBOL_NAME(sys_nfsservctl)<BR>.long SYMBOL_NAME(sys_setresgid)/* 170 */<BR>.long SYMBOL_NAME(sys_getresgid)<BR>.long SYMBOL_NAME(sys_prctl)<BR>.long SYMBOL_NAME(sys_rt_sigreturn)<BR>.long SYMBOL_NAME(sys_rt_sigaction)<BR>.long SYMBOL_NAME(sys_rt_sigprocmask)/* 175 */<BR>.long SYMBOL_NAME(sys_rt_sigpending)<BR>.long SYMBOL_NAME(sys_rt_sigtimedwait)<BR>.long SYMBOL_NAME(sys_rt_sigqueueinfo)<BR>.long SYMBOL_NAME(sys_rt_sigsuspend)<BR>.long SYMBOL_NAME(sys_pread)/* 180 */<BR>.long SYMBOL_NAME(sys_pwrite)<BR>.long SYMBOL_NAME(sys_chown)<BR>.long SYMBOL_NAME(sys_getcwd)<BR>.long SYMBOL_NAME(sys_capget)<BR>.long SYMBOL_NAME(sys_capset) /* 185 */<BR>.long SYMBOL_NAME(sys_sigaltstack)<BR>.long SYMBOL_NAME(sys_sendfile)<BR>.long SYMBOL_NAME(sys_ni_syscall)/* streams1 */<BR>.long SYMBOL_NAME(sys_ni_syscall)/* streams2 */<BR>.long SYMBOL_NAME(sys_vfork) /* 190 */<BR>我们来继续看本行的三个参数<IMG class=inlineimg title=Frown alt="" src="http://www.gd-linux.org/bbs/images/smilies/frown.gif" border=0>,%eax,4),实现数组索引。当然，这个数组是以sys_call_table作为索<BR>引的，称为偏移。三个参数分别代表：数组的基地址，索引（eax,也就是系统调用数）和大小，或每个数<BR>组元素中的字节数-----这里是4。由于数组基地址为空，所以赋予0---但它需要和偏移地址sys_call_table相加，简单的说是sys_call_table被当作数组的基地址。我把本行用c重写如下：<BR>(sys_call_table)[EAX]();<BR>当然，c还要处理许多工作，如为你纪录数组元素的大小。不要忘记，系统调用的参数早已经存储在堆栈<BR>中了，以便于system_call使用SAVE_ALL把他们压栈。<BR>--------------------------------------------------------------------------------------------<BR>movl %eax,EAX(%esp)# 系统调用返回<BR>/*它在EAX寄存器中的返回值（这个值同时也是system_call的返回值）被存储了起来。返回值被存储在堆<BR>栈中的EAX内，以使得RESTORE_ALL可以迅速地恢复实际的EAX寄存器及其他寄存器的值。*/<BR><BR>6月6日：<BR>以下代码依然是system_call的一部分，是一个可以命名为ret_from_sys_call和ret_from_intr的独立<BR>入口点。它们偶尔会被c直接调用，也可以从system_call的其他部分跳转过来。<BR>ALIGN<BR>.globl ret_from_sys_call<BR>.globl ret_from_intr<BR>ret_from_sys_call:<BR>movl SYMBOL_NAME(bh_mask),%eax<BR>andl SYMBOL_NAME(bh_active),%eax<BR>jne handle_bottom_half<BR>/*检测bottom half是否激活，如果激活，程序就跳转到handle_bottom_half执行，bottom half是中断进<BR>程的一部分，以后再提及，中断进程我的概念也很模糊。*/<BR>ret_with_reschedule:<BR>cmpl $0,need_resched(%ebx)/*检查进程是否为再次调度做了标记*/<BR>jne reschedule/*如果是，就跳转到reschedule*/<BR>cmpl $0,sigpending(%ebx)/*检查是否还有挂起信号*/<BR>jne signal_return/*如果有，则程序跳转到signal_return*/<BR>restore_all:<BR>RESTORE_ALL/*system_call的退出点，参看前面SAVE_ALL的用法*/<BR><BR>ALIGN<BR>signal_return:/*当system_call从系统调用返回前，如果它检测到需要将信号传送给当前的进程时，才<BR>会执行到signal_return。它通过使中断再次可用开始执行。*/<BR>sti# we can get here from an interrupt handler<BR>testl $(VM_MASK),EFLAGS(%esp)/*检测是否返回虚拟8086模式*/<BR>movl %esp,%eax<BR>jne v86_signal_return/*如果是，就跳转到v86_signal_return（由于虚拟8086我也不太理解，<BR>所以就跳过了，：（*/<BR>xorl %edx,%edx /*system_call需要调用c函数do_signal来释放信号。do_signal需要两个参数<BR>，这两个参数都是通过寄存器来传递的；第一个是EAX寄存器，另一个是edx寄存器。system_call已经把<BR>第一个参数的值赋给了eax；现在，把edx寄存器和寄存器本身进行xor操作，从而将其清0，这样do_signal就认为这是一个空指针。*/<BR>call SYMBOL_NAME(do_signal) /*好，现在就可以调用do_signal来传递信号了*/<BR>jmp restore_all /*然后跳转到restore_all结束*/<BR><BR>ALIGN<BR>v86_signal_return:<BR>call SYMBOL_NAME(save_v86_state)<BR>movl %eax,%esp<BR>xorl %edx,%edx<BR>call SYMBOL_NAME(do_signal)<BR>jmp restore_all<BR><BR>ALIGN<BR>tracesys: /*前面说过，当有当前进程的系统调用被其祖先跟踪，如strace或truss程序，程序就跳转到<BR>此。*/<BR>movl $-ENOSYS,EAX(%esp) /*system_call把存储在堆栈中的EAX拷贝赋予-ENOSYS。*/<BR>call SYMBOL_NAME(syscall_trace) /*调用syscall_trace*/<BR>movl ORIG_EAX(%esp),%eax /*在172行再从所作的拷贝中恢复EAX的值*/<BR>call *SYMBOL_NAME(sys_call_table)(,%eax,4) /*调用实际的系统调用。*/<BR>movl %eax,EAX(%esp)/*把系统调用的返回值置入堆栈中EAX的位置。*/<BR>call SYMBOL_NAME(syscall_trace) /*再次调用syscall_trace*/<BR>jmp ret_from_sys_call /*被跟踪的系统调用已经返回，控制流程跳转到ret_from_sys_call*/<BR>badsys: /*前面说过，当系统调用数超过边界值时程序就跳转到这里。*/<BR>movl $-ENOSYS,EAX(%esp) /*这时system_call必须返回-ENOSYS，82行把ENOSYS赋值为38。调用<BR>者会识别这个错误*/<BR>jmp ret_from_sys_call /*跳转到ret_from_sys_call*/<BR><BR>ALIGN<BR>ret_from_exception:/*在诸如除0之类的cpu异常中断情况下将执行到这里；system_call内部代码不会执<BR>行到这个标号*/<BR>movl SYMBOL_NAME(bh_mask),%eax<BR>andl SYMBOL_NAME(bh_active),%eax<BR>jne handle_bottom_half<BR>ALIGN<BR>ret_from_intr:<BR>GET_CURRENT(%ebx)<BR>movl EFLAGS(%esp),%eax# mix EFLAGS and CS<BR>movb CS(%esp),%al<BR>testl $(VM_MASK | 3),%eax# return to VM86 mode or non-supervisor?<BR>jne ret_with_reschedule<BR>jmp restore_all<BR><BR>ALIGN<BR>handle_bottom_half:<BR>call SYMBOL_NAME(do_bottom_half)<BR>jmp ret_from_intr<BR><BR>ALIGN<BR>reschedule:<BR>call SYMBOL_NAME(schedule) # test<BR>jmp ret_from_sys_call<BR>这以上的代码，我都还不太怎么明白，等我弄明白了就补齐，但基本的system_call的内部核心代码都介<BR>绍完了。<BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/725.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:30 <a href="http://www.cnitblog.com/drizztzou/articles/725.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>linux内核分析</title><link>http://www.cnitblog.com/drizztzou/articles/722.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:29:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/722.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/722.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/722.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/722.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/722.html</trackback:ping><description><![CDATA[启动<BR><BR>　　当PC启动时，Intel系列的CPU首先进入的是实模式，并开始执行位于地址0xFFFF0处的代码，也就是ROM-BIOS起始位置的代码。BIOS先进行一系列的系统自检，然后初始化位于地址0的中断向量表。最后BIOS将启动盘的第一个扇区装入到0x7C00，并开始执行此处的代码。这就是对内核初始化过程的一个最简单的描述。<BR>　　最初，linux核心的最开始部分是用8086汇编语言编写的。当开始运行时，核心将自己装入到绝对地址0x90000，再将其后的2k字节装入到地址0x90200处，最后将核心的其余部分装入到0x10000。<BR><BR>　　当系统装入时，会显示Loading...信息。装入完成后，控制转向另一个实模式下的汇编语言代码boot/Setup.S。Setup部分首先设置一些系统的硬件设备，然后将核心从 0x10000处移至0x1000处。这时系统转入保护模式，开始执行位于0x1000处的代码。<BR><BR>　　接下来是内核的解压缩。0x1000处的代码来自于文件Boot/head.S，它用来初始化寄存器和调用decompress_kernel( )程序。decompress_kernel( )程序由Boot/inflate.c, Boot/unzip.c和Boot../misc.c组成。解压缩后的数据被装入到了0x100000处，这也是 linux不能在内存小于2M的环境下运行的主要原因。<BR><BR>　　解压后的代码在0x1010000处开始执行，紧接着所有的32位的设置都将完成： IDT、 GDT和LDT将被装入，处理器初始化完毕，设置好内存页面，最终调用start_kernel过程。这大概是整个内核中最为复杂的部分。<BR><BR>[系统开始运行]<BR>linux kernel 最早的C代码从汇编标记startup_32开始执行<BR><BR>|startup_32:<BR>|start_kernel<BR>|lock_kernel<BR>|trap_init<BR>|init_IRQ<BR>|sched_init<BR>|softirq_init<BR>|time_init<BR>|console_init<BR>|#ifdef CONFIG_MODULES<BR>|init_modules<BR>|#endif<BR>|kmem_cache_init<BR>|sti<BR>|calibrate_delay<BR>|mem_init<BR>|kmem_cache_sizes_init<BR>|pgtable_cache_init<BR>|fork_init<BR>|proc_caches_init<BR>|vfs_caches_init<BR>|buffer_init<BR>|page_cache_init<BR>|signals_init<BR>|#ifdef CONFIG_PROC_FS<BR>|proc_root_init<BR>|#endif<BR>|#if defined(CONFIG_SYSVIPC)<BR>|ipc_init<BR>|#endif<BR>|check_bugs<BR>|smp_init<BR>|rest_init<BR>|kernel_thread<BR>|unlock_kernel<BR>|cpu_idle<BR><BR><BR>·startup_32 [arch/i386/kernel/head.S]<BR>·start_kernel [init/main.c]<BR>·lock_kernel [include/asm/smplock.h]<BR>·trap_init [arch/i386/kernel/traps.c]<BR>·init_IRQ [arch/i386/kernel/i8259.c]<BR>·sched_init [kernel/sched.c]<BR>·softirq_init [kernel/softirq.c]<BR>·time_init [arch/i386/kernel/time.c]<BR>·console_init [drivers/char/tty_io.c]<BR>·init_modules [kernel/module.c]<BR>·kmem_cache_init [mm/slab.c]<BR>·sti [include/asm/system.h]<BR>·calibrate_delay [init/main.c]<BR>·mem_init [arch/i386/mm/init.c]<BR>·kmem_cache_sizes_init [mm/slab.c]<BR>·pgtable_cache_init [arch/i386/mm/init.c]<BR>·fork_init [kernel/fork.c]<BR>·proc_caches_init<BR>·vfs_caches_init [fs/dcache.c]<BR>·buffer_init [fs/buffer.c]<BR>·page_cache_init [mm/filemap.c]<BR>·signals_init [kernel/signal.c]<BR>·proc_root_init [fs/proc/root.c]<BR>·ipc_init [ipc/util.c]<BR>·check_bugs [include/asm/bugs.h]<BR>·smp_init [init/main.c]<BR>·rest_init<BR>·kernel_thread [arch/i386/kernel/process.c]<BR>·unlock_kernel [include/asm/smplock.h]<BR>·cpu_idle [arch/i386/kernel/process.c]<BR><BR>start_kernel( )程序用于初始化系统内核的各个部分，包括：<BR><BR>*设置内存边界，调用paging_init( )初始化内存页面。<BR>*初始化陷阱，中断通道和调度。<BR>*对命令行进行语法分析。<BR>*初始化设备驱动程序和磁盘缓冲区。<BR>*校对延迟循环。<BR><BR>最后的function'rest_init' 作了以下工作:<BR><BR>·开辟内核线程'init'<BR>·调用unlock_kernel<BR>·建立内核运行的cpu_idle环, 如果没有调度，就一直死循环<BR><BR>实际上start_kernel永远不能终止.它会无穷地循环执行cpu_idle.<BR><BR>最后，系统核心转向move_to_user_mode( )，以便创建初始化进程（init）。此后，进程0开始进入无限循环。<BR><BR>初始化进程开始执行/etc/init、/bin/init 或/sbin /init中的一个之后，系统内核就不再对程序进行直接控制了。之后系统内核的作用主要是给进程提供系统调用，以及提供异步中断事件的处理。多任务机制已经建立起来，并开始处理多个用户的登录和fork( )创建的进程。<BR><BR>[init]<BR>init是第一个进程，或者说内核线程<BR><BR>|init<BR>|lock_kernel<BR>|do_basic_setup<BR>|mtrr_init<BR>|sysctl_init<BR>|pci_init<BR>|sock_init<BR>|start_context_thread<BR>|do_init_calls<BR>|(*call())-&gt; kswapd_init<BR>|prepare_namespace<BR>|free_initmem<BR>|unlock_kernel<BR>|execve<BR><BR>[目录]<BR><BR>--------------------------------------------------------------------------------<BR><BR><BR>启动步骤<BR><BR>系统引导：<BR>涉及的文件<BR>./arch/$ARCH/boot/bootsect.s<BR>./arch/$ARCH/boot/setup.s<BR><BR>bootsect.S<BR>　这个程序是linux kernel的第一个程序，包括了linux自己的bootstrap程序，<BR>但是在说明这个程序前，必须先说明一般IBM PC开机时的动作(此处的开机是指<BR>"打开PC的电源"):<BR><BR>　　一般PC在电源一开时，是由内存中地址FFFF:0000开始执行(这个地址一定<BR>在ROM BIOS中，ROM BIOS一般是在FEOOOh到FFFFFh中)，而此处的内容则是一个<BR>jump指令，jump到另一个位於ROM BIOS中的位置，开始执行一系列的动作，包<BR>括了检查RAM，keyboard，显示器，软硬磁盘等等，这些动作是由系统测试代码<BR>(system test code)来执行的，随着制作BIOS厂商的不同而会有些许差异，但都<BR>是大同小异，读者可自行观察自家机器开机时，萤幕上所显示的检查讯息。<BR><BR>　　紧接着系统测试码之后，控制权会转移给ROM中的启动程序<BR>(ROM bootstrap routine)，这个程序会将磁盘上的第零轨第零扇区读入<BR>内存中(这就是一般所谓的boot sector，如果你曾接触过电脑病<BR>毒，就大概听过它的大名)，至於被读到内存的哪里呢? --绝对<BR>位置07C0:0000(即07C00h处)，这是IBM系列PC的特性。而位在linux开机<BR>磁盘的boot sector上的正是linux的bootsect程序，也就是说，bootsect是<BR>第一个被读入内存中并执行的程序。现在，我们可以开始来<BR>看看到底bootsect做了什么。<BR><BR>第一步<BR>　首先，bootsect将它"自己"从被ROM BIOS载入的绝对地址0x7C00处搬到<BR>0x90000处，然后利用一个jmpi(jump indirectly)的指令，跳到新位置的<BR>jmpi的下一行去执行，<BR><BR>第二步<BR>　接着，将其他segment registers包括DS，ES，SS都指向0x9000这个位置，<BR>与CS看齐。另外将SP及DX指向一任意位移地址( offset )，这个地址等一下<BR>会用来存放磁盘参数表(disk para- meter table )<BR><BR>第三步<BR>　接着利用BIOS中断服务int 13h的第0号功能，重置磁盘控制器，使得刚才<BR>的设定发挥功能。<BR><BR>第四步<BR>　完成重置磁盘控制器之后，bootsect就从磁盘上读入紧邻着bootsect的setup<BR>程序，也就是setup.S，此读入动作是利用BIOS中断服务int 13h的第2号功能。<BR>setup的image将会读入至程序所指定的内存绝对地址0x90200处，也就是在内存<BR>中紧邻着bootsect 所在的位置。待setup的image读入内存后，利用BIOS中断服<BR>务int 13h的第8号功能读取目前磁盘的参数。<BR><BR>第五步<BR>　再来，就要读入真正linux的kernel了，也就是你可以在linux的根目录下看<BR>到的"vmlinuz" 。在读入前，将会先呼叫BIOS中断服务int 10h 的第3号功能，<BR>读取游标位置，之后再呼叫BIOS 中断服务int 10h的第13h号功能，在萤幕上输<BR>出字串"Loading"，这个字串在boot linux时都会首先被看到，相信大家应该觉<BR>得很眼熟吧。<BR><BR>第六步<BR>　接下来做的事是检查root device，之后就仿照一开始的方法，利用indirect<BR>jump 跳至刚刚已读入的setup部份<BR><BR>第七步<BR>setup.S完成在实模式下版本检查，并将硬盘，鼠标，内存参数写入到 INITSEG<BR>中，并负责进入保护模式。<BR><BR>第八步<BR>操作系统的初始化。<BR><BR><BR><BR><BR><BR>[目录]<BR><BR>--------------------------------------------------------------------------------<BR><BR><BR>bootsect.S<BR><BR>1.将自己移动到0x9000:0x0000处，为内核调入留出地址空间；<BR>2.建立运行环境(ss=ds=es=cs=0x9000, sp=0x4000-12)，保证起动程序运行；<BR>3.BIOS初始化0x1E号中断为软盘参数表，将它取来保存备用；<BR>4.将setup读到0x9000:0x0200处；<BR>5.测试软盘参数一个磁道有多少个扇区（也没有什么好办法，只能试试36, 18, 15, 9对不对了）；<BR>6.打印“Loading”；<BR>7.读入内核到0x1000:0000（如果是bzImage， 则将每个64K移动到0x100000处，在实模式下，只能调用0x15号中断了，这段代码无法放在bootsect中所以只能放在setup中，幸好此时setup已经读入了）；<BR>8.到setup去吧<BR>发发信人: seis (矛), 信区: linux<BR>标 题: linux操作系统内核引导程序详细剖析<BR>发信站: BBS 水木清华站 (Fri Feb 2 14:12:43 2001)<BR><BR>! bootsect.s (c) 1991, 1992 Linus Torvalds 版权所有<BR>! Drew Eckhardt修改过<BR>! Bruce Evans (bde)修改过<BR>!<BR>! bootsect.s 被bios-启动子程序加载至0x7c00 (31k)处，并将自己<BR>! 移到了地址0x90000 (576k)处，并跳转至那里。<BR>!<BR>! bde - 不能盲目地跳转，有些系统可能只有512k的低<BR>! 内存。使用中断0x12来获得(系统的)最高内存、等。<BR>!<BR>! 它然后使用BIOS中断将setup直接加载到自己的后面(0x90200)(576.5k)，<BR>! 并将系统加载到地址0x10000处。<BR>!<BR>! 注意! 目前的内核系统最大长度限制为(8*65536-4096)(508k)字节长，即使是在<BR>! 将来这也是没有问题的。我想让它保持简单明了。这样508k的最大内核长度应该<BR>! 是足够了，尤其是这里没有象minix中一样包含缓冲区高速缓冲(而且尤其是现在<BR>! 内核是压缩的 icon_smile.gif<BR>!<BR>! 加载程序已经做的尽量地简单了，所以持续的读出错将导致死循环。只能手工重启。<BR>! 只要可能，通过一次取得整个磁道，加载过程可以做的很快的。<BR><BR>#include /* 为取得CONFIG_ROOT_RDONLY参数 */<BR>!! config.h中(即autoconf.h中)没有CONFIG_ROOT_RDONLY定义!!!?<BR><BR>#include<BR><BR>.text<BR><BR>SETUPSECS = 4 ! 默认的setup程序扇区数(setup-sectors)的默认值;<BR><BR>BOOTSEG = 0x7C0 ! bootsect的原始地址;<BR><BR>INITSEG = DEF_INITSEG ! 将bootsect程序移到这个段处(0x9000) - 避开;<BR>SETUPSEG = DEF_SETUPSEG ! 设置程序(setup)从这里开始(0x9020);<BR>SYSSEG = DEF_SYSSEG ! 系统加载至0x1000(65536)(64k)段处;<BR>SYSSIZE = DEF_SYSSIZE ! 系统的大小(0x7F00): 要加载的16字节为一节的数;<BR>!! 以上4个DEF_参数定义在boot.h中:<BR>!! DEF_INITSEG 0x9000<BR>!! DEF_SYSSEG 0x1000<BR>!! DEF_SETUPSEG 0x9020<BR>!! DEF_SYSSIZE 0x7F00 (=32512=31.75k)*16=508k<BR><BR>! ROOT_DEV &amp; SWAP_DEV 现在是由"build"中编制的;<BR>ROOT_DEV = 0<BR>SWAP_DEV = 0<BR>#ifndef SVGA_MODE<BR>#define SVGA_MODE ASK_VGA<BR>#endif<BR>#ifndef RAMDISK<BR>#define RAMDISK 0<BR>#endif<BR>#ifndef CONFIG_ROOT_RDONLY<BR>#define CONFIG_ROOT_RDONLY 1<BR>#endif<BR><BR>! ld86 需要一个入口标识符，这和通常的一样;<BR>.globl _main<BR>_main:<BR>#if 0 /* 调试程序的异常分支，除非BIOS古怪（比如老的HP机）否则是无害的 */<BR>int 3<BR>#endif<BR>mov ax,#BOOTSEG !! 将ds段寄存器置为0x7C0;<BR>mov ds,ax<BR>mov ax,#INITSEG !! 将es段寄存器置为0x9000;<BR>mov es,ax<BR>mov cx,#256 !! 将cx计数器置为256(要移动256个字, 512字节);<BR>sub si,si !! 源地址 ds:si=0x07C0:0x0000;<BR>sub di,di !! 目的地址es:di=0x9000:0x0000;<BR>cld !! 清方向标志;<BR>rep !! 将这段程序从0x7C0:0(31k)移至0x9000:0(576k)处;<BR>movsw !! 共256个字(512字节)(0x200长);<BR>jmpi go,INITSEG !! 间接跳转至移动后的本程序go处;<BR><BR>! ax和es现在已经含有INITSEG的值(0x9000);<BR><BR>go: mov di,#0x4000-12 ! 0x4000(16k)是&gt;=bootsect + setup 的长度 +<BR>! + 堆栈的长度 的任意的值;<BR>! 12 是磁盘参数块的大小 es:di=0x94000-12=592k-12;<BR><BR>! bde - 将0xff00改成了0x4000以从0x6400处使用调试程序(bde)。如果<BR>! 我们检测过最高内存的话就不用担心这事了，还有，我的BIOS可以被配置为将wini驱动<BR>表<BR>! 放在内存高端而不是放在向量表中。老式的堆栈区可能会搞乱驱动表;<BR><BR>mov ds,ax ! 置ds数据段为0x9000;<BR>mov ss,ax ! 置堆栈段为0x9000;<BR>mov sp,di ! 置堆栈指针INITSEG:0x4000-12处;<BR>/*<BR>* 许多BIOS的默认磁盘参数表将不能<BR>* 进行扇区数大于在表中指定<BR>* 的最大扇区数( - 在某些情况下<BR>* 这意味着是7个扇区)后面的多扇区的读操作。<BR>*<BR>* 由于单个扇区的读操作是很慢的而且当然是没问题的，<BR>* 我们必须在RAM中(为第一个磁盘)创建新的参数表。<BR>* 我们将把最大扇区数设置为36 - 我们在一个ED 2.88驱动器上所能<BR>* 遇到的最大值。<BR>*<BR>* 此值太高是没有任何害处的，但是低的话就会有问题了。<BR>*<BR>* 段寄存器是这样的: ds=es=ss=cs - INITSEG,(=0X9000)<BR>* fs = 0, gs没有用到。<BR>*/<BR><BR>! 上面执行重复操作(rep)以后，cx为0;<BR><BR>mov fs,cx !! 置fs段寄存器=0;<BR>mov bx,#0x78 ! fs:bx是磁盘参数表的地址;<BR>push ds<BR>seg fs<BR>lds si,(bx) ! ds:si是源地址;<BR>!! 将fs:bx地址所指的指针值放入ds:si中;<BR>mov cl,#6 ! 拷贝12个字节到0x9000:0x4000-12开始处;<BR>cld<BR>push di !! 指针0x9000:0x4000-12处;<BR><BR>rep<BR>movsw<BR><BR>pop di !! di仍指向0x9000:0x4000-12处(参数表开始处);<BR>pop si !! ds =&gt; si=INITSEG(=0X9000);<BR><BR>movb 4(di),*36 ! 修正扇区计数值;<BR><BR>seg fs<BR>mov (bx),di !! 修改fs:bx(0000:0x0078)处磁盘参数表的地址为0x9000:0x4000-12;<BR>seg fs<BR>mov 2(bx),es<BR><BR>! 将setup程序所在的扇区(setup-sectors)直接加载到boot块的后面。!! 0x90200开始处<BR>;<BR>! 注意，es已经设置好了。<BR>! 同样经过rep循环后cx为0<BR><BR>load_setup:<BR>xor ah,ah ! 复位软驱(FDC);<BR>xor dl,dl<BR>int 0x13<BR><BR>xor dx,dx ! 驱动器0, 磁头0;<BR>mov cl,#0x02 ! 从扇区2开始，磁道0;<BR>mov bx,#0x0200 ! 置数据缓冲区地址=es:bx=0x9000:0x200;<BR>! 在INITSEG段中,即0x90200处;<BR>mov ah,#0x02 ! 要调用功能号2(读操作);<BR>mov al,setup_sects ! 要读入的扇区数SETUPSECS=4;<BR>! (假释所有数据都在磁头0、磁道0);<BR>int 0x13 ! 读操作;<BR>jnc ok_load_setup ! ok则继续;<BR><BR>push ax ! 否则显示出错信息。保存ah的值(功能号2);<BR>call print_nl !! 打印换行;<BR>mov bp,sp !! bp将作为调用print_hex的参数;<BR>call print_hex !! 打印bp所指的数据;<BR>pop ax<BR><BR>jmp load_setup !! 重试!<BR><BR>!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!<BR>!!INT 13 - DISK - READ SECTOR(S) INTO MEMORY<BR>!! AH = 02h<BR>!! AL = number of sectors to read (must be nonzero)<BR>!! CH = low eight bits of cylinder number<BR>!! CL = sector number 1-63 (bits 0-5)<BR>!! high two bits of cylinder (bits 6-7, hard disk only)<BR>!! DH = head number<BR>!! DL = drive number (bit 7 set for hard disk)<BR>!! ES:BX -&gt; data buffer<BR>!! Return: CF set on error<BR>!! if AH = 11h (corrected ECC error), AL = burst length<BR>!! CF clear if successful<BR>!! AH = status (see #00234)<BR>!! AL = number of sectors transferred (only valid if CF set for some<BR>!! BIOSes)<BR>!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!<BR><BR><BR>ok_load_setup:<BR><BR>! 取得磁盘驱动器参数，特别是每磁道扇区数(nr of sectors/track);<BR><BR>#if 0<BR><BR>! bde - Phoenix BIOS手册中提到功能0x08只对硬盘起作用。<BR>! 但它对于我的一个BIOS(1987 Award)不起作用。<BR>! 不检查错误码是致命的错误。<BR><BR>xor dl,dl<BR>mov ah,#0x08 ! AH=8用于取得驱动器参数;<BR>int 0x13<BR>xor ch,ch<BR><BR>!!!!!!!!!!!!!!!!!!!!!!!!!!!<BR>!! INT 13 - DISK - GET DRIVE PARAMETERS (PC,XT286,CONV,PS,ESDI,SCSI)<BR>!! AH = 08h<BR>!! DL = drive (bit 7 set for hard disk)<BR>!!Return: CF set on error<BR>!! AH = status (07h) (see #00234)<BR>!! CF clear if successful<BR>!! AH = 00h<BR>!! AL = 00h on at least some BIOSes<BR>!! BL = drive type (AT/PS2 floppies only) (see #00242)<BR>!! CH = low eight bits of maximum cylinder number<BR>!! CL = maximum sector number (bits 5-0)<BR>!! high two bits of maximum cylinder number (bits 7-6)<BR>!! DH = maximum head number<BR>!! DL = number of drives<BR>!! ES<IMG class=inlineimg title="Big Grin" alt="" src="http://www.gd-linux.org/bbs/images/smilies/biggrin.gif" border=0>I -&gt; drive parameter table (floppies only)<BR>!!!!!!!!!!!!!!!!!!!!!!!!!!!!<BR><BR>#else<BR><BR>! 好象没有BIOS调用可取得扇区数。如果扇区36可以读就推测是36个扇区，<BR>! 如果扇区18可读就推测是18个扇区，如果扇区15可读就推测是15个扇区，<BR>! 否则推测是9. [36, 18, 15, 9]<BR><BR>mov si,#disksizes ! ds:si-&gt;要测试扇区数大小的表;<BR><BR>probe_loop:<BR>lodsb !! ds:si所指的字节 =&gt;al, si=si+1;<BR>cbw ! 扩展为字(word);<BR>mov sectors, ax ! 第一个值是36，最后一个是9;<BR>cmp si,#disksizes+4<BR>jae got_sectors ! 如果所有测试都失败了，就试9;<BR>xchg ax,cx ! cx = 磁道和扇区(第一次是36=0x0024);<BR>xor dx,dx ! 驱动器0，磁头0;<BR>xor bl,bl !! 设置缓冲区es:bx = 0x9000:0x0a00(578.5k);<BR>mov bh,setup_sects !! setup_sects = 4 (共2k);<BR>inc bh<BR>shl bh,#1 ! setup后面的地址(es=cs);<BR>mov ax,#0x0201 ! 功能2(读)，1个扇区;<BR>int 0x13<BR>jc probe_loop ! 如果不对，就试用下一个值;<BR><BR>#endif<BR><BR>got_sectors:<BR><BR>! 恢复es<BR><BR>mov ax,#INITSEG<BR>mov es,ax ! es = 0x9000;<BR><BR>! 打印一些无用的信息(换行后，显示Loading)<BR><BR>mov ah,#0x03 ! 读光标位置;<BR>xor bh,bh<BR>int 0x10<BR><BR>mov cx,#9<BR>mov bx,#0x0007 ! 页0，属性7 (normal);<BR>mov bp,#msg1<BR>mov ax,#0x1301 ! 写字符串，移动光标;<BR>int 0x10<BR><BR>! ok, 我们已经显示出了信息，现在<BR>! 我们要加载系统了(到0x10000处)(64k处)<BR><BR>mov ax,#SYSSEG<BR>mov es,ax ! es=0x01000的段;<BR>call read_it !! 读system，es为输入参数；<BR>call kill_motor !! 关闭驱动器马达；<BR>call print_nl !! 打印回车换行；<BR><BR>! 这以后，我们来检查要使用哪个根设备(root-device)。如果已指定了设备(!=0)<BR>! 则不做任何事而使用给定的设备。否则的话，使用/dev/fd0H2880 (2,32)或/dev/PS0<BR>(2,2icon_cool.gif<BR>! 或者是/dev/at0 (2,icon_cool.gif之一，这取决于我们假设我们知道的扇区数而定。<BR>!! |__ ps0?? (x,y)--表示主、次设备号？<BR><BR>seg cs<BR>mov ax,root_dev<BR>or ax,ax<BR>jne root_defined<BR>seg cs<BR>mov bx,sectors !! sectors = 每磁道扇区数；<BR>mov ax,#0x0208 ! /dev/ps0 - 1.2Mb;<BR>cmp bx,#15<BR>je root_defined<BR>mov al,#0x1c ! /dev/PS0 - 1.44Mb !! 0x1C = 28;<BR>cmp bx,#18<BR>je root_defined<BR>mov al,0x20 ! /dev/fd0H2880 - 2.88Mb;<BR>cmp bx,#36<BR>je root_defined<BR>mov al,#0 ! /dev/fd0 - autodetect;<BR>root_defined:<BR>seg cs<BR>mov root_dev,ax !! 其中保存由设备的主、次设备号；<BR><BR>! 这以后(所有程序都加载了)，我们就跳转至<BR>! 被直接加载到boot块后面的setup程序去：<BR><BR>jmpi 0,SETUPSEG !! 跳转到0x9020:0000(setup程序的开始位置);<BR><BR><BR>! 这段程序将系统(system)加载到0x10000(64k)处,<BR>! 注意不要跨越64kb边界。我们试图以最快的速度<BR>! 来加载，只要可能就整个磁道一起读入。<BR>!<BR>! 输入(in): es - 开始地址段(通常是0x1000)<BR>!<BR>sread: .word 0 ! 当前磁道已读的扇区数；<BR>head: .word 0 ! 当前磁头；<BR>track: .word 0 ! 当前磁道；<BR><BR>read_it:<BR>mov al,setup_sects<BR>inc al<BR>mov sread,al !! 当前sread=5；<BR>mov ax,es !! es=0x1000；<BR>test ax,#0x0fff !! (ax AND 0x0fff, if ax=0x1000 then zero-flag=1 )；<BR>die: jne die ! es 必须在64kB的边界；<BR>xor bx,bx ! bx 是段内的开始地址；<BR>rp_read:<BR>#ifdef __BIG_KERNEL__<BR>#define CALL_HIGHLOAD_KLUDGE .word 0x1eff, 0x220 ! 调用 far * bootsect_kludge<BR>! 注意: as86不能汇编这；<BR>CALL_HIGHLOAD_KLUDGE ! 这是在setup.S中的程序；<BR>#else<BR>mov ax,es<BR>sub ax,#SYSSEG ! 当前es段值减system加载时的启始段值(0x1000)；<BR>#endif<BR>cmp ax,syssize ! 我们是否已经都加载了？(ax=0x7f00 ?)；<BR>jbe ok1_read !! if ax &lt;= syssize then 继续读；<BR>ret !! 全都加载完了，返回！<BR>ok1_read:<BR>mov ax,sectors !! sectors=每磁道扇区数；<BR>sub ax,sread !! 减去当前磁道已读扇区数，al=当前磁道未读的扇区数(ah=0)；<BR>mov cx,ax<BR>shl cx,#9 !! 乘512，cx = 当前磁道未读的字节数；<BR>add cx,bx !! 加上段内偏移值，es:bx为当前读入的数据缓冲区地址；<BR>jnc ok2_read !! 如果没有超过64K则继续读；<BR>je ok2_read !! 如果正好64K也继续读；<BR>xor ax,ax<BR>sub ax,bx<BR>shr ax,#9<BR>ok2_read:<BR>call read_track !! es:bx -&gt;缓冲区，al=要读的扇区数，也即当前磁道未读的扇区数；<BR><BR>mov cx,ax !! ax仍为调用read_track之前的值，即为读入的扇区数；<BR>add ax,sread !! ax = 当前磁道已读的扇区数；<BR>cmp ax,sectors !! 已经读完当前磁道上的扇区了吗？<BR>jne ok3_read !! 没有，则跳转；<BR>mov ax,#1<BR>sub ax,head !! 当前是磁头1吗？<BR>jne ok4_read !! 不是(是磁头0)则跳转(此时ax=1)；<BR>inc track !! 当前是磁头1，则读下一磁道(当前磁道加1)；<BR>ok4_read:<BR>mov head,ax !! 保存当前磁头号；<BR>xor ax,ax !! 本磁道已读扇区数清零；<BR>ok3_read:<BR>mov sread,ax !! 存本磁道已读扇区数；<BR>shl cx,#9 !! 刚才一次读操作读入的扇区数 * 512；<BR>add bx,cx !! 调整数据缓冲区的起始指针；<BR>jnc rp_read !! 如果该指针没有超过64K的段内最大偏移量，则跳转继续读操作；<BR>mov ax,es !! 如果超过了，则将段地址加0x1000(下一个64K段);<BR>add ah,#0x10<BR>mov es,ax<BR>xor bx,bx !! 缓冲区地址段内偏移量置零；<BR>jmp rp_read !! 继续读操作；<BR><BR><BR>read_track:<BR>pusha !! 将寄存器ax,cx,dx,bx,sp,bp,si,di压入堆栈；<BR>pusha<BR>mov ax,#0xe2e ! loading... message 2e = . !! 显示一个.<BR>mov bx,#7<BR>int 0x10<BR>popa<BR><BR>mov dx,track !! track = 当前磁道；<BR>mov cx,sread<BR>inc cx !! cl = 扇区号，要读的起始扇区；<BR>mov ch,dl !! ch = 磁道号的低8位；<BR>mov dx,head !!<BR>mov dh,dl !! dh = 当前磁头号；<BR>and dx,#0x0100 !! dl = 驱动器号(0)；<BR>mov ah,#2 !! 功能2(读)，es:bx指向读数据缓冲区；<BR><BR>push dx ! 为出错转储保存寄存器的值到堆栈上；<BR>push cx<BR>push bx<BR>push ax<BR><BR>int 0x13<BR>jc bad_rt !! 如果出错，则跳转；<BR>add sp, #8 !! 清(放弃)堆栈上刚推入的4个寄存器值；<BR>popa<BR>ret<BR><BR>bad_rt: push ax ! 保存出错码；<BR>call print_all ! ah = error, al = read；<BR><BR><BR>xor ah,ah<BR>xor dl,dl<BR>int 0x13<BR><BR><BR>add sp,#10<BR>popa<BR>jmp read_track<BR><BR>/*<BR>* print_all是用于调试的。<BR>* 它将打印出所有寄存器的值。所作的假设是<BR>* 从一个子程序中调用的，并有如下所示的堆栈帧结构<BR>* dx<BR>* cx<BR>* bx<BR>* ax<BR>* error<BR>* ret &lt;- sp<BR>*<BR>*/<BR><BR>print_all:<BR>mov cx,#5 ! 出错码 + 4个寄存器<BR>mov bp,sp<BR><BR>print_loop:<BR>push cx ! 保存剩余的计数值<BR>call print_nl ! 为了增强阅读性，打印换行<BR><BR>cmp cl, #5<BR>jae no_reg ! 看看是否需要寄存器的名称<BR><BR>mov ax,#0xe05 + A - l<BR>sub al,cl<BR>int 0x10<BR><BR>mov al,#X<BR>int 0x10<BR><BR>mov al,#:<BR>int 0x10<BR><BR>no_reg:<BR>add bp,#2 ! 下一个寄存器<BR>call print_hex ! 打印值<BR>pop cx<BR>loop print_loop<BR>ret<BR><BR>print_nl: !! 打印回车换行。<BR>mov ax,#0xe0d ! CR<BR>int 0x10<BR>mov al,#0xa ! LF<BR>int 0x10<BR>ret<BR><BR>/*<BR>* print_hex是用于调试目的的，打印出<BR>* ss:bp所指向的十六进制数。<BR>* !! 例如，十六进制数是0x4321时，则al分别等于4,3,2,1调用中断打印出来 4321<BR>*/<BR><BR>print_hex:<BR>mov cx, #4 ! 4个十六进制数字<BR>mov dx, (bp) ! 将(bp)所指的值放入dx中<BR>print_digit:<BR>rol dx, #4 ! 循环以使低4比特用上 !! 取dx的高4比特移到低4比特处。<BR>mov ax, #0xe0f ! ah = 请求的功能值，al = 半字节(4个比特)掩码。<BR>and al, dl !! 取dl的低4比特值。<BR>add al, #0x90 ! 将al转换为ASCII十六进制码(4个指令)<BR>daa !! 十进制调整<BR>adc al, #0x40 !! (adc dest, src ==&gt; dest := dest + src + c )<BR>daa<BR>int 0x10<BR>loop print_digit<BR>ret<BR><BR><BR>/*<BR>* 这个过程(子程序)关闭软驱的马达，这样<BR>* 我们进入内核后它的状态就是已知的，以后也就<BR>* 不用担心它了。<BR>*/<BR>kill_motor:<BR>push dx<BR>mov dx,#0x3f2<BR>xor al,al<BR>outb<BR>pop dx<BR>ret<BR><BR>!! 数据区<BR>sectors:<BR>.word 0 !! 当前每磁道扇区数。(36||18||15||9)<BR><BR>disksizes: !! 每磁道扇区数表<BR>.byte 36, 18, 15, 9<BR><BR>msg1:<BR>.byte 13, 10<BR>.ascii "Loading"<BR><BR>.org 497 !! 从boot程序的二进制文件的497字节开始<BR>setup_sects:<BR>.byte SETUPSECS<BR>root_flags:<BR>.word CONFIG_ROOT_RDONLY<BR>syssize:<BR>.word SYSSIZE<BR>swap_dev:<BR>.word SWAP_DEV<BR>ram_size:<BR>.word RAMDISK<BR>vid_mode:<BR>.word SVGA_MODE<BR>root_dev:<BR>.word ROOT_DEV<BR>boot_flag: !! 分区启动标志<BR>.word 0xAA55<BR><BR><BR><BR><BR><BR>[目录]<BR><BR>--------------------------------------------------------------------------------<BR><BR><BR>setup.S<BR><BR>1、按规定得有个头，所以一开始是惯用的JMP；<BR>2、头里边内容很丰富，具体用法走着瞧；<BR>3、自我检测，不知道有什么用，防伪造？防篡改？<BR>4、如果装载程序不对，只好死掉！以下终于走入正题；<BR>5、获取内存容量（使用了三种办法，其中的E820和E801看不明白，int 15倒是老朋友了--应该是上个世纪80年代末认识的了，真佩服十年过去了，情意依旧，不过遇上一些不守规矩的BIOS，不知道还行不行）；<BR>6、将键盘重复键的重复率设为最大，灵敏一点？<BR>7、检测硬盘，不懂，放这里干什么？<BR>8、检测MCA总线（不要问我这是什么）；<BR>9、检测PS/2鼠标，用int 11，只是不知道为何放这里；<BR>10、检测电源管理BIOS；唉，书到用时方恨少，不懂的太多了，真不好意思；不过也没有关系， 不懂的就别去动它就行了；以下要进入内核了；<BR>11、 在进入保护模式之前，可以调用一个你提供的试模式下的过程，让你最后在看她一眼，当然你要是不提供，那就有个默认的，无非是塞住耳朵闭上眼睛禁止任何中断，包括著名的NMI ；<BR>12、设置保护模式起动后的例程地址， 你可以写自己的例程，但不是代替而是把它加在setup提供的例程的前面（显示一个小鸭子？）；<BR>13、如果内核是zImage， 将它移动到0x10000处；<BR>14、如果自己不在0x90000处，则移动到0x90000处；<BR>15、建立idt, gdt表；<BR>16、启动A20；<BR>17、屏住呼吸，屏闭所有中断；<BR>18、启动！movw $1, %ax ; lmsw %ax; 好已经进入保护模式下，马上进行局部调整；<BR>19、jmpi 0x100000, __KERNEL_CS，终于进入内核；<BR>setup.S<BR>A summary of the setup.S code 。The slight differences in the operation of setup.S due to a big kernel is documented here. When the switch to 32 bit protected mode begins the code32_start address is defined as 0x100000 (when loaded) here.<BR>code32_start:<BR><BR>#ifndef __BIG_KERNEL__<BR>.long 0x1000<BR>#else<BR>.long 0x100000<BR>#endif<BR><BR>After setting the keyboard repeat rate to a maximum, calling video.S, storing the video parameters, checking for the hard disks, PS/2 mouse, and APM BIOS the preparation for real mode switch begins.<BR><BR>The interrupts are disabled. Since the loader changed the code32_start address, the code32 varable is updated. This would be used for the jmpi instruction when the setup.S finally jumps to compressed/head.S. In case of a big kernel this is loacted at 0x100000.<BR><BR>seg cs<BR>mov eax, code32_start !modified above by the loader<BR>seg cs<BR>mov code32,eax<BR><BR>!code32 contains the correct address to branch to after setup.S finishes After the above code there is a slight difference in the ways the big and small kernels are dealt. In case of a small kernel the kernel is moved down to segment address 0x100, but a big kernel is not moved. Before decompression, the big kernel stays at 0x100000. The following is the code that does thischeck.test byte ptr loadflags,<BR><BR>#LOADED_HIGH<BR>jz do_move0 ! a normal low loaded zImage is moved<BR>jmp end_move ! skip move<BR><BR>The interrupt and global descriptors are initialized:<BR><BR>lidt idt_48 ! load idt wit 0,0<BR>lgdt gdt_48 ! load gdt with whatever appropriate<BR><BR>After enabling A20 and reprogramming the interrupts, it is ready to set the PE bit:<BR><BR>mov ax,#1<BR>lmsw ax<BR>jmp flush_instr<BR>flush_instr:<BR>xor bx.bx !flag to indicate a boot<BR>! Manual, mixing of 16-bit and 32 bit code<BR>db 0x166,0xea !prefix jmpi-opcode<BR>code32: dd ox1000 !this has been reset in caes of a big kernel, to 0x100000<BR>dw __KERNEL_CS<BR><BR>Finally it prepares the opcode for jumping to compressed/head.S which in the big kernel is at 0x100000. The compressed kernel would start at 0x1000 in case of a small kernel.<BR><BR>compressed/head.S<BR><BR>When setup.S relinquishes control to compressed/head.S at beginning of the compressed kernmel at 0x100000. It checks to see if A20 is really enabled otherwise it loops forever.<BR><BR>Itinitializes eflags, and clears BSS (Block Start by Symbol) creating reserved space for uninitialized static or global variables. Finally it reserves place for the moveparams structure (defined in misc.c) and pushes the current stack pointer on the stack and calls the C function decompress_kernel which takes a struct moveparams * as an argument<BR><BR>subl $16,%esp<BR>pushl %esp<BR>call SYMBOL_NAME(decompress_kernel)<BR>orl ??,??<BR>jnz 3f<BR><BR>Te C function decompress_kernel returns the variable high_loaded which is set to 1 in the function setup_output_buffer_if_we_run_high, which is called in decompressed_kernel if a big kernel was loaded.<BR>When decompressed_kernel returns, it jumps to 3f which moves the move routine.<BR><BR>movl $move_routine_start,%esi ! puts the offset of the start of the source in the source index register<BR>mov $0x1000,?? ! the destination index now contains 0x1000, thus after move, the move routine starts at 0x1000<BR>movl $move_routine_end,??<BR>sub %esi,?? ! ecx register now contains the number of bytes to be moved<BR>! (number of bytes between the labels move_routine_start and move_routine_end)<BR>cld<BR>rep<BR>movsb ! moves the bytes from ds:si to es:di, in each loop it increments si and di, and decrements cx<BR>! the movs instruction moves till ecx is zero<BR><BR>Thus the movsb instruction moves the bytes of the move routine between the labels move_routine_start and move_routine_end. At the end the entire move routine labeled move_routine_start is at 0x1000. The movsb instruction moves bytes from ds:si to es:si.<BR><BR>At the start of the head.S code es,ds,fs,gs were all intialized to __KERNEL_DS, which is defined in /usr/src/linux/include/asm/segment.h as 0x18. This is the offset from the goobal descriptor table gdtwhich was setup in setup.S. The 24th byte is the start of the data segment descriptor, which has the base address = 0. Thus the moe routine is moved and<BR>starts at offset 0x1000 from __KERNEL_DS, the kernel data segment base (which is 0).<BR>The salient features of what is done by the decompress_kernel is discussed in the next section but it is worth noting that the when the decompressed_kernel function is invoked, space was created at the top of the stack to contain the information about the decompressed kernel. The decompressed kernel if big may be in the high buffer and in the low buffer. After the decompressed_kernel function returns, the decompressed kernel has to be moved so that we<BR>have a contiguous decompressed kernel starting from address 0x100000. To move the decompressed kernel, the important parameters needed are the start addresses of the high buffer and low buffer, and the number of bytes in the high and low buffers. This is at the top of the stack when decompressed_kernel returns (the top of the stack was passed as an argument : struct moveparams*, and in the function the fileds of the moveparams struture was adjusted toreflect the state of the decompression.)<BR><BR>/* in compressed../misc.c */<BR>struct moveparams {<BR>uch *low_buffer_start; ! start address of the low buffer<BR>int count; ! number of bytes in the low buffer after decompression is doneuch *high_buffer_start; ! start address of the high buffer<BR>int hcount; ! number of bytes in the high buffer aftre decompression is done<BR>};<BR><BR>Thus when the decompressed_kernel returns, the relevant bytes are popped in the respective registers as shown below. After preparing these registers the decompressed kernel is ready to be moved and the control jumps to the moved move routine at __KERNEL_CS:0x1000. The code for setting the appropriate registers is given below:<BR><BR>popl %esi ! discard the address, has the return value (high_load) most probably<BR>popl %esi ! low_buffer_start<BR>popl ?? ! lcount<BR>popl ?? ! high_buffer_count<BR>popl ?? ! hcount<BR>movl %0x100000,??<BR>cli ! disable interrutps when the decompressed kernel is being moved<BR>ljmp $(__KERNEL_CS), $0x1000 ! jump to the move routine which was moved to low memory, 0x1000<BR><BR>The move_routine_start basically has two parts, first it moves the part of the decompressed kernel in the low buffer, then it moves (if required) the high buffer contents. It should be noted that the ecx has been intialized to the number of bytes in the low end buffer, and the destination index register di has been intialized to 0x100000.<BR>move_routine_start:<BR><BR>rep ! repeat, it stops repeating when ecx == 0<BR>movsb ! the movsb instruction repeats till ecx is 0. In each loop byte is transferred from ds:esi to es:edi! In each loop the edi and the esi are incremented and ecx is decremented<BR>! when the low end buffer has been moved the value of di is not changed and the next pasrt of the code! uses it to transfer the bytes from the high buffer<BR>movl ??,%esi ! esi now has the offset corresponding to the start of the high buffer<BR>movl ??,?? ! ecx is now intialized to the number of bytes in the high buffer<BR>rep<BR>movsb ! moves all the bytes in the high buffer, and doesn’t move at all if hcount was zero (if it was determined, in! close_output_buffer_if_we_run_high that the high buffer need not be moveddown )<BR>xorl ??,??<BR>mov $0x90000, %esp ! stack pointer is adjusted, most probably to be used by the kernel in the intialization<BR>ljmp $(__KERNEL_CS), $0x100000 ! jump to __KERNEL_CS:0X100000, where the kernel code starts<BR>move_routine_end:At the end of the this the control goes to the kernel code segment.<BR><BR><BR>linux Assembly code taken from head.S and setup.S<BR>Comment code added by us<BR><BR><BR><BR><BR>[目录]<BR><BR>--------------------------------------------------------------------------------<BR><BR><BR>head.S<BR><BR>因为setup.S最后的为一条转跳指令，跳到内核第一条指令并开始执行。指令中指向的是内存中的绝对地址，我们无法依此判断转跳到了head.S。但是我们可以通过Makefile简单的确定head.S位于内核的前端。<BR>在arch/i386 的 Makefile 中定义了<BR>HEAD := arch/i386/kernel/head.o<BR><BR>而在linux总的Makefile中由这样的语句<BR>include arch/$(ARCH)/Makefile<BR>说明HEAD定义在该文件中有效<BR><BR>然后由如下语句：<BR><BR>vmlinux: $(CONFIGURATION) init/main.o init/version.o linuxsubdirs<BR>$(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o <BR>$(ARCHIVES) <BR>$(FILESYSTEMS) <BR>$(DRIVERS) <BR>$(LIBS) -o vmlinux<BR>$(NM) vmlinux | grep -v '(compiled)|(.o$$)|( a )' | sort &gt; System.map<BR><BR>从这个依赖关系我们可以获得大量的信息<BR><BR>1&gt;$(HEAD)即head.o的确第一个被连接到核心中<BR><BR>2&gt;所有内核中支持的文件系统全部编译到$(FILESYSTEMS)即fs/filesystems.a中<BR>所有内核中支持的网络协议全部编译到net.a中<BR>所有内核中支持的SCSI驱动全部编译到scsi.a中<BR>...................<BR>原来内核也不过是一堆库文件和目标文件的集合罢了,有兴趣对内核减肥的同学,<BR>可以好好比较一下看究竟是那个部分占用了空间。<BR><BR>3&gt;System.map中包含了所有的内核输出的函数，我们在编写内核模块的时候<BR>可以调用的系统函数大概就这些了。<BR><BR><BR>好了，消除了心中的疑问，我们可以仔细分析head.s了。<BR><BR>Head.S分析<BR><BR>1 首先将ds,es,fs,gs指向系统数据段KERNEL_DS<BR>KERNEL_DS 在asm/segment.h中定义，表示全局描述符表中<BR>中的第三项。<BR>注意：该此时生效的全局描述符表并不是在head.s中定义的<BR>而仍然是在setup.S中定义的。<BR><BR>2 数据段全部清空。<BR><BR>3 setup_idt为一段子程序，将中断向量表全部指向ignore_int函数<BR>该函数打印出：unknown interrupt<BR>当然这样的中断处理函数什么也干不了。<BR><BR>4 察看数据线A20是否有效，否则循环等待。<BR>地址线A20是x86的历史遗留问题，决定是否能访问1M以上内存。<BR><BR>5 拷贝启动参数到0x5000页的前半页，而将setup.s取出的bios参数<BR>放到后半页。<BR><BR>6 检查CPU类型<BR>@#$#%$^*@^?(^%#$%!#!@?谁知道干了什么？<BR><BR>7 初始化页表，只初始化最初几页。<BR><BR>1&gt;将swapper_pg_dir（0x2000)和pg0(0x3000)清空<BR>swapper_pg_dir作为整个系统的页目录<BR><BR>2&gt;将pg0作为第一个页表，将其地址赋到swapper_pg_dir的第一个32<BR>位字中。<BR><BR>3&gt;同时将该页表项也赋给swapper_pg_dir的第3072个入口，表示虚拟地址<BR>0xc0000000也指向pg0。<BR><BR>4&gt;将pg0这个页表填满指向内存前4M<BR><BR>5&gt;进入分页方式<BR>注意：以前虽然在在保护模式但没有启用分页。<BR><BR>--------------------<BR>| swapper_pg_dir | -----------<BR>| |-------| pg0 |----------内存前4M<BR>| | -----------<BR>| |<BR>--------------------<BR>8 装入新的gdt和ldt表。<BR><BR>9 刷新段寄存器ds,es,fs,gs<BR><BR>10 使用系统堆栈，即预留的0x6000页面<BR><BR>11 执行start_kernel函数，这个函数是第一个C编制的<BR>函数，内核又有了一个新的开始。<BR><BR><BR><BR><BR><BR>[目录]<BR><BR>--------------------------------------------------------------------------------<BR><BR><BR>compressed../misc.c<BR><BR>compressed../misc.c<BR>The differences in decompressing big and small kernels.<BR><A href="http://www.vuse.vanderbilt.edu/~knopfdg/documentation/hw3_part3.htm" target=_blank><FONT color=#002c99>http://www.vuse.vanderbilt.edu/~kno...n/hw3_part3.htm</FONT></A><BR>The function decompressed_kernel is invoked from head.S and a parameter to the top of the stack is passed to store the results of the decompression namely, the start addresses of the high and the low buffers which contain the decompressed kernel and the numebr of bytes in each buffer (hcount and lcount).<BR>int decompress_kernel(struct moveparams *mv)<BR>{<BR>if (SCREEN_INFO.orig_video_mode == 7) {<BR>vidmem = (char *) 0xb0000;<BR>vidport = 0x3b4;<BR>} else {<BR>vidmem = (char *) 0xb8000;<BR>vidport = 0x3d4;<BR>}<BR>lines = SCREEN_INFO.orig_video_lines;<BR>cols = SCREEN_INFO.orig_video_cols;<BR>if (free_mem_ptr &lt; 0x100000) setup_normal_output_buffer(); // Call if smallkernel<BR>else setup_output_buffer_if_we_run_high(mv); // Call if big kernel<BR>makecrc();<BR>puts("Uncompressing linux... ");<BR>gunzip();<BR>puts("Ok, booting the kernel. ");<BR>if (high_loaded) close_output_buffer_if_we_run_high(mv);<BR>return high_loaded;<BR>}<BR><BR>The first place where a distinction is made is when the buffers are to be setup for the decmpression routine gunzip(). Free_mem_ptr, is loaded with the value of the address of the extern variabe end. The variable end marks the end of the compressed kernel. If the free_mem-ptr is less than the 0x100000,then a high buffer has to be setup. Thus the function setup_output_buffer_if_we_run_high is called and the pointer to the top of the moveparams structure is passed so that when the buffers are setup, the start addresses fields are updated in moveparams structure. It is also checked to see if the high buffer needs to be moved down after decompression and this is reflected by the hcount which is 0 if we need not move the high buffer down.<BR><BR>void setup_output_buffer_if_we_run_high(struct moveparams *mv)<BR>{<BR>high_buffer_start = (uch *)(((ulg)&amp;end) HEAP_SIZE);<BR>//the high buffer start address is at the end HEAP_SIZE<BR>#ifdef STANDARD_MEMORY_BIOS_CALL<BR>if (EXT_MEM_K &lt; (3*1024)) error("Less than 4MB of memory. ");<BR>#else<BR>if ((ALT_MEM_K &gt; EXT_MEM_K ? ALT_MEM_K : EXT_MEM_K) &lt; (3*1024)) error("Less<BR>than 4MB of memory. ");<BR>#endif<BR>mv-&gt;low_buffer_start = output_data = (char *)LOW_BUFFER_START;<BR>//the low buffer start address is at 0x2000 and it extends till 0x90000.<BR>high_loaded = 1; //high_loaded is set to 1, this is returned by decompressed_kernel<BR>free_mem_end_ptr = (long)high_buffer_start;<BR>// free_mem_end_ptr points to the same address as te high_buffer_start<BR>// the code below finds out if the high buffer needs to be moved after decompression<BR>// if the size if the low buffer is &gt; the size of the compressed kernel and the HEAP_SIZE<BR>// then the high_buffer_start has to be shifted up so that when the decompression starts it doesn’t<BR>// overwrite the compressed kernel data. Thus when the high_buffer_start islow then it is shifted<BR>// up to exactly match the end of the compressed kernel and the HEAP_SIZE. The hcount filed is<BR>// is set to 0 as the high buffer need not be moved down. Otherwise if the high_buffer_start is too<BR>// high then the hcount is non zero and while closing the buffers the appropriate number of bytes<BR>// in the high buffer is asigned to the filed hcount. Since the start address of the high buffer is<BR>// known the bytes could be moved down<BR>if ( (0x100000 LOW_BUFFER_SIZE) &gt; ((ulg)high_buffer_start)) {<BR>high_buffer_start = (uch *)(0x100000 LOW_BUFFER_SIZE);<BR>mv-&gt;hcount = 0; /* say: we need not to move high_buffer */<BR>}<BR>else mv-&gt;hcount = -1;<BR>mv-&gt;high_buffer_start = high_buffer_start;<BR>// finally the high_buffer_start field is set to the varaible high_buffer_start<BR>}<BR><BR>After the buffers are set gunzip() is invoked which decompresses the kernel Upon return, bytes_out has the number of bytes in the decompressed kernel.Finally close_output_buffer_if_we_run_high is invoked if high_loaded is non zero:<BR><BR>void close_output_buffer_if_we_run_high(struct moveparams *mv)<BR>{<BR>mv-&gt;lcount = bytes_out;<BR>// if the all of decompressed kernel is in low buffer, lcount = bytes_out<BR>if (bytes_out &gt; LOW_BUFFER_SIZE) {<BR>// if there is a part of the decompressed kernel in the high buffer, the lcount filed is set to<BR>// the size of the low buffer and the hcount field contains the rest of the bytes<BR>mv-&gt;lcount = LOW_BUFFER_SIZE;<BR>if (mv-&gt;hcount) mv-&gt;hcount = bytes_out - LOW_BUFFER_SIZE;<BR>// if the hcount field is non zero (made in setup_output_buffer_if_we_run_high)<BR>// then the high buffer has to be moved doen and the number of bytes in the high buffer is<BR>// in hcount<BR>}<BR>else mv-&gt;hcount = 0; // all the data is in the high buffer<BR>}<BR>Thus at the end of the the decompressed_kernel function the top of the stack has the addresses of the buffers and their sizes which is popped and the appropriate registers set for the move routine to move the entire kernel. After the move by the move_routine the kernel resides at 0x100000. If a small kernel is being decompressed then the setup_normal_output_buffer() is invoked from decompressed_kernel, which just initializes output_data to 0x100000 where the decompressed kernel would lie. The variable high_load is still 0 as setup_output_buffer_if_we_run_high() is not invoked. Decompression is done starting at address 0x100000. As high_load is 0, when decompressed_kernel returns in head.S, a zero is there in the eax. Thus the control jumps directly to 0x100000. Since the decompressed kernel lies there directly and the move routine need not be called.<BR><BR>linux code taken from misc.c<BR>Comment code added by us<BR><BR><BR><BR><BR><BR>[目录]<BR><BR>--------------------------------------------------------------------------------<BR><BR><BR>内核解压<BR><BR>概述<BR>----<BR>1) linux的初始内核映象以gzip压缩文件的格式存放在zImage或bzImage之中, 内核的自举代码将它解压到1M内存开始处. 在内核初始化时, 如果加载了压缩的initrd映象, 内核会将它解压到内存盘中, 这两处解压过程都使用了lib/inflate.c文件.<BR><BR>2) inflate.c是从gzip源程序中分离出来的, 包含了一些对全局数据的直接引用, 在使用时需要直接嵌入到代码中. gzip压缩文件时总是在前32K字节的范围内寻找重复的字符串进行编码, 在解压时需要一个至少为32K字节的解压缓冲区, 它定义为window[WSIZE].inflate.c使用get_byte()读取输入文件, 它被定义成宏来提高效率. 输入缓冲区指针必须定义为inptr, inflate.c中对之有减量操作. inflate.c调用flush_window()来输出window缓冲区中的解压出的字节串, 每次输出长度用outcnt变量表示. 在flush_window()中, 还必须对输出字节串计算CRC并且刷新crc变量. 在调用gunzip()开始解压之前, 调用makecrc()初始化CRC计算表. 最后gunzip()返回0表示解压成功.<BR><BR><BR>3) zImage或bzImage由16位引导代码和32位内核自解压映象两个部分组成. 对于zImage, 内核自解压映象被加载到物理地址0x1000, 内核被解压到1M的部位. 对于bzImage, 内核自解压映象被加载到1M开始的地方, 内核被解压为两个片段, 一个起始于物理地址0x2000-0x90000,另一个起始于高端解压映象之后, 离1M开始处不小于低端片段最大长度的区域. 解压完成后,这两个片段被合并到1M的起始位置.<BR><BR><BR>解压根内存盘映象文件的代码<BR>--------------------------<BR><BR>; drivers/block/rd.c<BR>#ifdef BUILD_CRAMDISK<BR><BR>/*<BR>* gzip declarations<BR>*/<BR><BR>#define OF(args) args ; 用于函数原型声明的宏<BR>#ifndef memzero<BR>#define memzero(s, n) memset ((s), 0, (n))<BR>#endif<BR>typedef unsigned char uch; 定义inflate.c所使用的3种数据类型<BR>typedef unsigned short ush;<BR>typedef unsigned long ulg;<BR>#define INBUFSIZ 4096 用户输入缓冲区尺寸<BR>#define WSIZE 0x8000 /* window size--must be a power of two, and */<BR>/* at least 32K for zip's deflate method */<BR><BR>static uch *inbuf; 用户输入缓冲区,与inflate.c无关<BR>static uch *window; 解压窗口<BR>static unsigned insize; /* valid bytes in inbuf */<BR>static unsigned inptr; /* index of next byte to be processed in inbuf */<BR>static unsigned outcnt; /* bytes in output buffer */<BR>static int exit_code;<BR>static long bytes_out; 总解压输出长度,与inflate.c无关<BR>static struct file *crd_infp, *crd_outfp;<BR><BR>#define get_byte() (inptr<BR>/* Diagnostic functions (stubbed out) */ 一些调试宏<BR>#define Assert(cond,msg)<BR>#define Trace(x)<BR>#define Tracev(x)<BR>#define Tracevv(x)<BR>#define Tracec(c,x)<BR>#define Tracecv(c,x)<BR><BR>#define STATIC static<BR><BR>static int fill_inbuf(void);<BR>static void flush_window(void);<BR>static void *malloc(int size);<BR>static void free(void *where);<BR>static void error(char *m);<BR>static void gzip_mark(void **);<BR>static void gzip_release(void **);<BR><BR>#include "../../lib/inflate.c"<BR><BR>static void __init *malloc(int size)<BR>{<BR>return kmalloc(size, GFP_KERNEL);<BR>}<BR><BR>static void __init free(void *where)<BR>{<BR>kfree(where);<BR>}<BR><BR>static void __init gzip_mark(void **ptr)<BR>{<BR>; 读取用户一个标记<BR>}<BR><BR>static void __init gzip_release(void **ptr)<BR>{<BR>; 归还用户标记<BR>}<BR><BR>/* ===========================================================================<BR>* Fill the input buffer. This is called only when the buffer is empty<BR>* and at least one byte is really needed.<BR>*/<BR><BR>static int __init fill_inbuf(void) 填充输入缓冲区<BR>{<BR>if (exit_code) return -1;<BR>insize = crd_infp-&gt;f_op-&gt;read(crd_infp, inbuf, INBUFSIZ,<BR>if (insize == 0) return -1;<BR>inptr = 1;<BR>return inbuf[0];<BR>}<BR><BR>/* ===========================================================================<BR>* Write the output window window[0..outcnt-1] and update crc and bytes_out.<BR>* (Used for the decompressed data only.)<BR>*/<BR><BR>static void __init flush_window(void) 输出window缓冲区中outcnt个字节串<BR>{<BR>ulg c = crc; /* temporary variable */<BR>unsigned n;<BR>uch *in, ch;<BR><BR>crd_outfp-&gt;f_op-&gt;write(crd_outfp, window, outcnt,<BR>in = window;<BR>for (n = 0; n ch = *in++;<BR>c = crc_32_tab[((int)c ^ ch) 0xff] ^ (c &gt;&gt; icon_cool.gif; 计算输出串的CRC<BR>}<BR>crc = c;<BR>bytes_out += (ulg)outcnt; 刷新总字节数<BR>outcnt = 0;<BR>}<BR><BR>static void __init error(char *x) 解压出错调用的函数<BR>{<BR>printk(KERN_ERR "%s", x);<BR>exit_code = 1;<BR>}<BR><BR><BR>static int __init<BR>crd_load(struct file * fp, struct file *outfp)<BR>{<BR>int result;<BR><BR>insize = 0; /* valid bytes in inbuf */<BR>inptr = 0; /* index of next byte to be processed in inbuf */<BR>outcnt = 0; /* bytes in output buffer */<BR>exit_code = 0;<BR>bytes_out = 0;<BR>crc = (ulg)0xffffffffL; /* shift register contents */<BR><BR>crd_infp = fp;<BR>crd_outfp = outfp;<BR>inbuf = kmalloc(INBUFSIZ, GFP_KERNEL);<BR>if (inbuf == 0) {<BR>printk(KERN_ERR "RAMDISK: Couldn't allocate gzip buffer ");<BR>return -1;<BR>}<BR>window = kmalloc(WSIZE, GFP_KERNEL);<BR>if (window == 0) {<BR>printk(KERN_ERR "RAMDISK: Couldn't allocate gzip window ");<BR>kfree(inbuf);<BR>return -1;<BR>}<BR>makecrc();<BR>result = gunzip();<BR>kfree(inbuf);<BR>kfree(window);<BR>return result;<BR>}<BR><BR>#endif /* BUILD_CRAMDISK */<BR><BR>32位内核自解压代码<BR>------------------<BR><BR>; arch/i386/boot/compressed/head.S<BR><BR>.text<BR>#include ·<BR>#include<BR>.globl startup_32 对于zImage该入口地址为0x1000; 对于bzImage为0x101000<BR>startup_32:<BR>cld<BR>cli<BR>movl $(__KERNEL_DS),%eax<BR>movl %eax,%ds<BR>movl %eax,%es<BR>movl %eax,%fs<BR>movl %eax,%gs<BR><BR>lss SYMBOL_NAME(stack_start),%esp # 自解压代码的堆栈为misc.c中定义的16K字节的数组<BR>xorl %eax,%eax<BR>1: incl %eax # check that A20 really IS enabled<BR>movl %eax,0x000000 # loop forever if it isn't<BR>cmpl %eax,0x100000<BR>je 1b<BR><BR>/*<BR>* Initialize eflags. Some BIOS's leave bits like NT set. This would<BR>* confuse the debugger if this code is traced.<BR>* XXX - best to initialize before switching to protected mode.<BR>*/<BR>pushl $0<BR>popfl<BR>/*<BR>* Clear BSS 清除解压程序的BSS段<BR>*/<BR>xorl %eax,%eax<BR>movl $ SYMBOL_NAME(_edata),%edi<BR>movl $ SYMBOL_NAME(_end),%ecx<BR>subl %edi,%ecx<BR>cld<BR>rep<BR>stosb<BR>/*<BR>* Do the decompression, and jump to the new kernel..<BR>*/<BR>subl $16,%esp # place for structure on the stack<BR>movl %esp,%eax<BR>pushl %esi # real mode pointer as second arg<BR>pushl %eax # address of structure as first arg<BR>call SYMBOL_NAME(decompress_kernel)<BR>orl %eax,%eax # 如果返回非零,则表示为内核解压为低端和高端的两个片断<BR>jnz 3f<BR>popl %esi # discard address<BR>popl %esi # real mode pointer<BR>xorl %ebx,%ebx<BR>ljmp $(__KERNEL_CS), $0x100000 # 运行start_kernel<BR><BR>/*<BR>* We come here, if we were loaded high.<BR>* We need to move the move-in-place routine down to 0x1000<BR>* and then start it with the buffer addresses in registers,<BR>* which we got from the stack.<BR>*/<BR>3:<BR>movl $move_routine_start,%esi<BR>movl $0x1000,%edi<BR>movl $move_routine_end,%ecx<BR>subl %esi,%ecx<BR>addl $3,%ecx<BR>shrl $2,%ecx # 按字取整<BR>cld<BR>rep<BR>movsl # 将内核片断合并代码复制到0x1000区域, 内核的片段起始为0x2000<BR><BR>popl %esi # discard the address<BR>popl %ebx # real mode pointer<BR>popl %esi # low_buffer_start 内核低端片段的起始地址<BR>popl %ecx # lcount 内核低端片段的字节数量<BR>popl %edx # high_buffer_start 内核高端片段的起始地址<BR>popl %eax # hcount 内核高端片段的字节数量<BR>movl $0x100000,%edi 内核合并的起始地址<BR>cli # make sure we don't get interrupted<BR>ljmp $(__KERNEL_CS), $0x1000 # and jump to the move routine<BR><BR>/*<BR>* Routine (template) for moving the decompressed kernel in place,<BR>* if we were high loaded. This _must_ PIC-code !<BR>*/<BR>move_routine_start:<BR>movl %ecx,%ebp<BR>shrl $2,%ecx<BR>rep<BR>movsl # 按字拷贝第1个片段<BR>movl %ebp,%ecx<BR>andl $3,%ecx<BR>rep<BR>movsb # 传送不完全字<BR>movl %edx,%esi<BR>movl %eax,%ecx # NOTE: rep movsb won't move if %ecx == 0<BR>addl $3,%ecx<BR>shrl $2,%ecx # 按字对齐<BR>rep<BR>movsl # 按字拷贝第2个片段<BR>movl %ebx,%esi # Restore setup pointer<BR>xorl %ebx,%ebx<BR>ljmp $(__KERNEL_CS), $0x100000 # 运行start_kernel<BR>move_routine_end:<BR><BR>; arch/i386/boot/compressed../misc.c<BR><BR>/*<BR>* gzip declarations<BR>*/<BR><BR>#define OF(args) args<BR>#define STATIC static<BR><BR>#undef memset<BR>#undef memcpy<BR>#define memzero(s, n) memset ((s), 0, (n))<BR><BR><BR>ypedef unsigned char uch;<BR>typedef unsigned short ush;<BR>typedef unsigned long ulg;<BR><BR>#define WSIZE 0x8000 /* Window size must be at least 32k, */<BR>/* and a power of two */<BR><BR>static uch *inbuf; /* input buffer */<BR>static uch window[WSIZE]; /* Sliding window buffer */<BR><BR>static unsigned insize = 0; /* valid bytes in inbuf */<BR>static unsigned inptr = 0; /* index of next byte to be processed in inbuf */<BR>static unsigned outcnt = 0; /* bytes in output buffer */<BR><BR>/* gzip flag byte */<BR>#define ASCII_FLAG 0x01 /* bit 0 set: file probably ASCII text */<BR>#define CONTINUATION 0x02 /* bit 1 set: continuation of multi-part gzip file */<BR>#define EXTRA_FIELD 0x04 /* bit 2 set: extra field present */<BR>#define ORIG_NAME 0x08 /* bit 3 set: original file name present */<BR>#define COMMENT 0x10 /* bit 4 set: file comment present */<BR>#define ENCRYPTED 0x20 /* bit 5 set: file is encrypted */<BR>#define RESERVED 0xC0 /* bit 6,7: reserved */<BR><BR>#define get_byte() (inptr<BR>/* Diagnostic functions */<BR>#ifdef DEBUG<BR># define Assert(cond,msg) {if(!(cond)) error(msg);}<BR># define Trace(x) fprintf x<BR># define Tracev(x) {if (verbose) fprintf x ;}<BR># define Tracevv(x) {if (verbose&gt;1) fprintf x ;}<BR># define Tracec(c,x) {if (verbose (c)) fprintf x ;}<BR># define Tracecv(c,x) {if (verbose&gt;1 (c)) fprintf x ;}<BR>#else<BR># define Assert(cond,msg)<BR># define Trace(x)<BR># define Tracev(x)<BR># define Tracevv(x)<BR># define Tracec(c,x)<BR># define Tracecv(c,x)<BR>#endif<BR><BR>static int fill_inbuf(void);<BR>static void flush_window(void);<BR>static void error(char *m);<BR>static void gzip_mark(void **);<BR>static void gzip_release(void **);<BR><BR>/*<BR>* This is set up by the setup-routine at boot-time<BR>*/<BR>static unsigned char *real_mode; /* Pointer to real-mode data */<BR><BR>#define EXT_MEM_K (*(unsigned short *)(real_mode + 0x2))<BR>#ifndef STANDARD_MEMORY_BIOS_CALL<BR>#define ALT_MEM_K (*(unsigned long *)(real_mode + 0x1e0))<BR>#endif<BR>#define SCREEN_INFO (*(struct screen_info *)(real_mode+0))<BR><BR>extern char input_data[];<BR>extern int input_len;<BR><BR>static long bytes_out = 0;<BR>static uch *output_data;<BR>static unsigned long output_ptr = 0;<BR><BR><BR>static void *malloc(int size);<BR>static void free(void *where);<BR>static void error(char *m);<BR>static void gzip_mark(void **);<BR>static void gzip_release(void **);<BR><BR>static void puts(const char *);<BR><BR>extern int end;<BR>static long free_mem_ptr = (long)<BR>static long free_mem_end_ptr;<BR><BR>#define INPLACE_MOVE_ROUTINE 0x1000 内核片段合并代码的运行地址<BR>#define LOW_BUFFER_START 0x2000 内核低端解压片段的起始地址<BR>#define LOW_BUFFER_MAX 0x90000 内核低端解压片段的终止地址<BR>#define HEAP_SIZE 0x3000 为解压低码保留的堆的尺寸,堆起始于BSS的结束<BR>static unsigned int low_buffer_end, low_buffer_size;<BR>static int high_loaded =0;<BR>static uch *high_buffer_start /* = (uch *)(((ulg) + HEAP_SIZE)*/;<BR><BR>static char *vidmem = (char *)0xb8000;<BR>static int vidport;<BR>static int lines, cols;<BR><BR>#include "../../../../lib/inflate.c"<BR><BR>static void *malloc(int size)<BR>{<BR>void *p;<BR><BR>if (size if (free_mem_ptr<BR>free_mem_ptr = (free_mem_ptr + 3) ~3; /* Align */<BR><BR>p = (void *)free_mem_ptr;<BR>free_mem_ptr += size;<BR><BR>if (free_mem_ptr &gt;= free_mem_end_ptr)<BR>error(" Out of memory ");<BR><BR>return p;<BR>}<BR><BR>static void free(void *where)<BR>{ /* Don't care */<BR>}<BR><BR>static void gzip_mark(void **ptr)<BR>{<BR>*ptr = (void *) free_mem_ptr;<BR>}<BR><BR>static void gzip_release(void **ptr)<BR>{<BR>free_mem_ptr = (long) *ptr;<BR>}<BR><BR>static void scroll(void)<BR>{<BR>int i;<BR><BR>memcpy ( vidmem, vidmem + cols * 2, ( lines - 1 ) * cols * 2 );<BR>for ( i = ( lines - 1 ) * cols * 2; i vidmem[ i ] = ' ';<BR>}<BR><BR>static void puts(const char *s)<BR>{<BR>int x,y,pos;<BR>char c;<BR><BR>x = SCREEN_INFO.orig_x;<BR>y = SCREEN_INFO.orig_y;<BR><BR>while ( ( c = *s++ ) != '<BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/722.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:29 <a href="http://www.cnitblog.com/drizztzou/articles/722.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>什么是module 以及如何写一个module </title><link>http://www.cnitblog.com/drizztzou/articles/721.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:28:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/721.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/721.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/721.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/721.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/721.html</trackback:ping><description><![CDATA[不知道在什幺时候，Linux 出现了 module 这种东西，的确，它是 Linux 的一大革新。有了 module 之后，写 device driver 不再是一项恶梦，修改 kernel 也不再是一件痛苦的事了。因为你不需要每次要测试 driver 就重新 compile kernel 一次。那简直是会累死人。Module 可以允许我们动态的改变 kernel，加载 device driver，而且它也能缩短我们 driver development 的时间。在这篇文章里，我将要跟各位介绍一下 module 的原理，以及如何写一个 module。 <BR><BR>module 翻译成中文就是模块，不过，事实上去翻译这个字一点都没意义。在讲模块之前，我先举一个例子。相信很多人都用过 RedHat。在 RedHat 里，我们可以执行 sndconfig，它可以帮我们 config 声卡。config 完之后如果捉得到你的声卡，那你的声卡马上就可以动了，而且还不用重新激活计算机。这是怎幺做的呢 ? 就是靠module。module 其实是一般的程序。但是它可以被动态载到 kernel 里成为 kernel的一部分。载到 kernel 里的 module 它具有跟 kernel 一样的权力。可以 access 任何 kernel 的 data structure。你听过 kdebug 吗 ? 它是用来 debug kernel 的。它就是先将它本身的一个 module 载到 kernel 里，而在 user space 的 gdb 就可以经由跟这个 module 沟通，得知 kernel 里的 data structure 的值，除此之外，还可以经由载到 kernel 的 module 去更改 kernel 里 data structure。 <BR><BR>我们知道，在写 C 程序的时候，一个程序只能有一个 main。Kernel 本身其实也是一个程序，它本身也有个 main，叫 start_kernel()。当我们把一个 module 载到 kernel 里的时候，它会跟 kernel 整合在一起，成为 kernel 的一部分。请各位想想，那 module 可以有 main 吗 ? 答案很明显的，是 No。理由很简单。一个程序只能有一个 main。在使用 module 时，有一点要记住的是 module 是处于被动的角色。它是提供某些功能让别人去使用的。 <BR><BR>Kernel 里有一个变量叫 module_list，每当 user 将一个 module 载到 kernel 里的时候，这个 module 就会被记录在 module_list 里面。当 kernel 要使用到这个 module 提供的 function 时，它就会去 search 这个 list，找到 module，然后再使用其提供的 function 或 variable。每一个 module 都可以 export 一些 function 或变量来让别人使用。除此之外，module 也可以使用已经载到 kernel 里的 module 提供的 function。这种情形叫做 module stack。比方说，module A 用到 module B 的东西，那在加载 module A 之前必须要先加载 module B。否则 module A 会无法加载。除了 module 会 export 东西之外，kernel 本身也会 export 一些 function 或 variable。同样的，module 也可以使用 kernel 所 export 出来的东西。由于大家平时都是撰写 user space 的程序，所以，当突然去写 module 的时候，会把平时写程序用的 function 拿到 module 里使用。像是 printf 之类的东西。我要告诉各位的是，module 所使用的 function 或 variable，要嘛就是自己写在 module 里，要嘛就是别的 module 提供的，再不就是 kernel 所提供的。你不能使用一般 libc 或 glibc所提供的 function。像 printf 之类的东西。这一点可能是各位要多小心的地方。(也许你可以先 link 好，再载到 kernel，我好象试过，但是忘了) <BR><BR>刚才我们说到 kernel 本身会 export 出一些 function 或 variable 来让 module 使用，但是，我们不是万能的，我们怎幺知道 kernel 有开放那里东西让我们使用呢 ? Linux 提供一个 command，叫 ksyms，你只要执行 ksyms -a 就可以知道 kernel 或目前载到 kernel 里的 module 提供了那些 function 或 variable。底下是我的系统的情形: <BR><BR>c0216ba0 drive_info_R744aa133<BR>c01e4a44 boot_cpu_data_R660bd466<BR>c01e4ac0 EISA_bus_R7413793a<BR>c01e4ac4 MCA_bus_Rf48a2c4c<BR>c010cc34 __verify_write_R203afbeb<BR>. . . . . <BR><BR>在 kernel 里，有一个 symbol table 是用来记录 export 出去的 function 或 variable。除此之外，也会记录着那个 module export 那些 function。上面几行中，表示 kernel 提供了 drive_info 这个 function/variable。所以，我们可以在 kernel 里直接使用它，等载到 kernel 里时，会自动做好 link 的动作。由此，我们可以知道，module 本身其实是还没做 link 的一些 object code。一切都要等到 module 被加载 kernel 之后，link 才会完成。各位应该可以看到 drive_info 后面还接着一些奇怪的字符串。_R744aa133，这个字符串是根据目前 kernel 的版本再做些 encode 得出来的结果。为什幺额外需要这一个字符串呢 ? <BR><BR>Linux 不知道从那个版本以来，就多了一个 config 的选项，叫做 Set version number in symbols of module。这是为了避免对系统造成不稳定。我们知道 Linux 的 kernel 更新的很快。在 kernel 更新的过程，有时为了效率起见，会对某些旧有的 data structure 或 function 做些改变，而且一变可能有的 variable 被拿掉，有的 function 的 prototype 跟原来的都不太一样。如果这种情形发生的时候，那可能以前 2.0.33 版本的 module 拿到 2.2.1 版本的 kernel 使用，假设原来 module 使用了 2.0.33 kernel 提供的变量叫 A，但是到了 2.2.1 由于某些原因必须把 A 都设成 NULL。那当此 module 用在 2.2.1 kernel 上时，如果它没去检查 A 的值就直接使用的话，就会造成系统的错误。也许不会整个系统都死掉，但是这个 module 肯定是很难发挥它的功能。为了这个原因，Linux 就在 compile module 时，把 kernel 版本的号码 encode 到各个 exported function 和 variable 里。 <BR><BR>所以，刚才也许我们不应该讲 kernel 提供了 drive_info，而应该说 kernel 提供了 driver_info_R744aa133 来让我们使用。这样也许各位会比较明白。也就是说，kernel 认为它提供的 driver_info_R744aa133 这个东西，而不是 driver_info。所以，我们可以发现有的人在加载 module 时，系统都一直告诉你某个 function 无法 resolved。这就是因为 kernel 里没有你要的 function，要不然就是你的 module 里使用的 function 跟 kernel encode 的结果不一样。所以无法 resolve。解决方式，要嘛就是将 kernel 里的 set version 选项关掉，要嘛就是将 module compile 成 kernel 有办法接受的型式。 <BR><BR>那有人就会想说，如果 kernel 认定它提供的 function 名字叫做 driver_info_R744aa133 的话，那我们写程序时，是不是用到这个 funnction 的地方都改成 driver_info_R744aa133 就可以了。答案是 Yes。但是，如果每个 function 都要你这样写，你不会觉得很烦吗 ? 比方说，我们在写 driver 时，很多人都会用到 printk 这个 function。这是 kernel 所提供的 function。它的功能跟 printf 很像。用法也几乎都一样。是 debug 时很好用的东西。如果我们 module 里用了一百次 printk，那是不是我们也要打一百次的 printk_Rdd132261 呢 ? 当然不是，聪明的人马上会想到用 #define printk printk_Rdd132261 就好了嘛。所以啰，Linux 很体贴的帮我们做了这件事。 <BR><BR>如果各位的系统有将 set version 的选项打开的话，那大家可以到 /usr/src/linux/include/linux/modules 这个目录底下。这个目录底下有所多的 ..ver档案。这些档案其实就是用来做 #define 用的。我们来看看 ksyms.ver 这个档案里，里面有一行是这样子的 : <BR><BR>#define printk _set_ver(printk) <BR><BR>set_ver 是一个 macro，就是用来在 printk 后面加上 version number 的。有兴趣的朋友可以自行去观看这个 macro 的写法。用了这些 ver 檔，我们就可以在 module 里直接使用 printk 这样的名字了。而这些 ver 档会自动帮我们做好 #define 的动作。可是，我们可以发现这个目录有很多很多的 ver 檔。有时候，我们怎幺知道我们要呼叫的 function 是在那个 ver 档里有定义呢 ? Linux 又帮我们做了一件事。/usr/src/linux/include/linux/modversions.h 这个档案已经将全部的 ver 档都加进来了。所以在我们的 module 里只要 include 这个档，那名字的问题都解决了。但是，在此，我们奉劝各位一件事，不要将 modversions.h 这个档在 module 里 include 进来，如果真的要，那也要加上以下数行: <BR><BR>#ifdef MODVERSIONS<BR>#include &lt;linux/modversions.h&gt;<BR>#endif <BR><BR>加入这三行的原因是，避免这个 module 在没有设定 kernel version 的系统上，将 modversions.h 这个档案 include 进来。各位可以去试试看，当你把 set version 的选项关掉时，modversions.h 和 modules 这个目录都会不见。如果没有上面三行，那 compile 就不会过关。所以一般来讲，modversions.h 我们会选择在 compile 时传给 gcc 使用。就像下面这个样子。 <BR><BR>gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS main.c \<BR>-include usr/src/linux/include/linux/modversions.h <BR><BR>在这个 command line 里，我们看到了 -D__KERNEL__，这是说要定义 __KERNEL__ 这个 constant。很多跟 kernel 有关的 header file，都必须要定义这个 constant 才能 include 的。所以建议你最好将它定义起来。另外还有一个 -DMODVERSIONS。这个 constant 我刚才忘了讲。刚才我们说要解决 fucntion 或 variable 名字 encode 的方式就是要 include modversions.h，其实除此之外，你还必须定义 MODVERSIONS 这个 constant。再来就是 MODULE 这个 constant。其实，只要是你要写 module 就一定要定义这个变量。而且你还要 include module.h 这个档案，因为 _set_ver 就是定义在这里的。 <BR><BR>讲到这里，相信各位应该对 module 有一些认识了，以后遇到 module unresolved 应该不会感到困惑了，应该也有办法解决了。 <BR><BR>刚才讲的都是使用别人的 function 上遇到的名字 encode 问题。但是，如果我们自己的 module 想要 export 一些东西让别的 module 使用呢。很简单。在 default 上，在你的 module 里所有的 global variable 和 function 都会被认定为你要 export 出去的。所以，如果你的 module 里有 10 个 global variable，经由 ksyms，你可以发现这十个 variable 都会被 export 出去。这当然是个很方便的事啦，但是，你知道，有时候我们根本不想把所有的 variable 都 export 出去，万一有个 module 没事乱改我们的 variable 怎幺办呢 ? 所以，在很多时候，我们都只会限定几个必要的东西 export 出去。在 2.2.1 之前的 kernel (不是很确定) 可以利用 register_symtab 来帮我们。但是，现在更新的版本早就出来了。所以，在此，我会介绍 kernel 2.2.1 里所提供的。kernel 2.2.1 里提供了一个 macro，叫做 EXPORT_SYMBOL，这是用来帮我们选择要 export 的 variable 或 function。比方说，我要 export 一个叫 full 的 variable，那我只要在 module 里写: <BR><BR>EXPORT_SYMBOL(full); <BR><BR>就会自动将 full export 出去，你马上就可以从 ksyms 里发现有 full 这个变量被 export 出去。在使用 EXPORT_SYMBOL 之前，要小心一件事，就是必须在 gcc 里定义 EXPORT_SYMTAB 这个 constant，否则在 compile 时会发生 parser error。所以，要使用 EXPORT_SYMBOL 的话，那 gcc 应该要下: <BR><BR>gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS -DEXPORT_SYMTAB \<BR>main.c -include /usr/src/linux/include/linux/modversions.h <BR><BR>如果我们不想 export 任何的东西，那我们只要在 module 里下 <BR><BR>EXPORT_NO_SYMBOLS; <BR><BR>就可以了。使用 EXPORT_NO_SYMBOLS 用不着定义任何的 constant。其实，如果各位使用过旧版的 register_symbol 的话，一定会觉得新版的方式比较好用。至少我是这样觉得啦。因为使用 register_symbol 还要先定义出自己的 symbol_table，感觉有点麻烦。 <BR><BR>当我们使用 EXPORT_SYMBOL 把一些 function 或 variable export 出来之后，我们使用 ksyma -a 去看一些结果。我们发现 EXPORT_SYMBOL(full) 的确是把 full export出来了 : <BR><BR>c8822200 full [my_module]<BR>c01b8e08 pci_find_slot_R454463b5<BR>. . . <BR><BR>但是，结果怎幺跟我们想象中的不太一样，照理说，应该是 full_Rxxxxxx 之类的东西才对啊，怎幺才出现 full 而已呢 ? 奇怪，问题在那里呢 ? <BR><BR>其实，问题就在于我们没有对本身的 module 所 export 出来的 function 或 variable 的名字做 encode。想想，如果在 module 的开头。我们加入一行 <BR><BR>#define full full_Rxxxxxx <BR><BR>之后，我们再重新 compile module 一次，载到 kernel 之后，就可以发现 ksyms -a 显示的是 <BR><BR>c8822200 full_Rxxxxxx [my_module]<BR>c01b8e08 pci_find_slot_R454463b5<BR>. . . . . <BR><BR>了。那是不是说，我们要去对每一个 export 出来的 variable 和 function 做 define 的动作呢 ? 当然不是啰。记得吗，前头我们讲去使用 kernel export 的 function 时，由于 include 了一些 .ver 的档案，以致于我们不用再做 define 的动作。现在，我们也要利用 .ver 的档案来帮我们，使我们 module export 出来的 function 也可以自动加入 kernel version 的 information。也就是变成 full_Rxxxxxx 之类的东西。 <BR><BR>Linux 里提供了一个 command，叫 genksyms，就是用来帮我们产生这种 .ver 的档案的。它会从 stdin 里读取 source code，然后检查 source code 里是否有 export 的 variable 或 function。如果有，它就会自动为每个 export 出来的东西产生一些 define。这些 define 就是我们之前说的。等我们有了这些 define 之后，只要在我们的 module 里加入这些 define，那 export 出来的 function 或 variable 就会变成上面那个样子。 <BR><BR>假设我们的程序都放在一个叫 main.c 的档案里，我们可以使用下列的方式产生这些 define。 <BR><BR>gcc -E -D__GENKSYMS__ main.c | genksyms -k 2.2.1 &gt; main.ver <BR><BR>gcc 的 -E 参数是指将 preprocessing 的结果 show 出来。也就是说将它 include 的档案，一些 define 的结果都展开。-D__GENKSYMS__ 是一定要的。如果没有定义这个 constant，你将不会看到任何的结果。用一个管线是因为 genksyms 是从 stdin 读资料的，所以，经由管线将 gcc 的结果传给 genksyms。-k 2.2.1 是指目前使用的 kernel 版本是 2.2.1，如果你的 kernel 版本不一样，必须指定你的 kernel 的版本。产生的 define 将会被放到 main.ver 里。产生完 main.ver 档之后，在 main.c 里将它 include 进来，那一切就 OK 了。有件事要告诉各位的是，使用这个方式产生的 module，其 export 出来的东西会经由 main.ver 的 define 改头换面。所以如果你要让别人使用，那你必须将 main.ver 公开，不然，别人就没办法使用你 export 出来的东西了。 <BR><BR>讲了这幺多，相信各位应该都已经比较清楚 module 在 kernel 中是怎幺样一回事，也应该知道为什幺有时候 module 会无法加载了。除此之外，各位应该还知道如何使自己 module export 出来的东西也具有 kernel version 的 information。 <BR><BR>接下来，要跟各位讲的就是，如何写一个 module 了。其实，写一个 module 很简单的。如果你了解我上面所说的东西。那我再讲一次，再用个例子，相信大家就都会了。要写一个 module，必须要提供两个 function。这两个 function 是给 insmod 和 rmmod 使用的。它们分别是 init_module()，以及 cleanup_module()。 <BR><BR>int init_module();<BR>void cleanup_module(); <BR><BR>相信大家都知道在 Linux 里可以使用 insmod 这个 command 来将某个 module 加载。比方说，我有一个 module 叫 hello.o，那使用 insmod hello.o 就可以将 hello 这个 module 载到 kernel 里。观察 /etc/modules 应该就可以看到 hello 这个 module 的名字。如果要将 hello 这个 module 移除，则只要使用 rmmod hello 就可以了。insmod 在加载 module 之后，就会去呼叫 module 所提供的 init_module()。如果传回 0 表示成功，那 module 就会被加载。如果失败，那加载的动作就会失败。一般来讲，我们在 init_module() 做的事都是一些初始化的工作。比方说，你的 module 需要一块内存，那你就可以在 init_module() 做 kmalloc 的动作。想当然尔。cleanup_module() 就是在 module 要移除的时候做的事。做的事一般来讲就是一些善后的工作，比方像把之前 kmalloc 的内存 free 掉。 <BR><BR>由于 module 是载到 kernel 使用的，所以，可能别的 module 会使用你的 module，甚至某些 process 也会使用到你的 module，为了避免 module 还有人使用时就被移除，每个 module 都有一个 use count。用来记录目前有多少个 process 或 module 正在使用这个 module。当 module 的 use count 不等于 0 时，module 是不会被移除掉的。也就是说，当 module 的 use count 不等于 0 时，cleanup_module() 是不会被呼叫的。 <BR><BR>在此，我要介绍三个 macro，是跟 module 的 use count 有关的。 <BR><BR>MOD_INC_USE_COUNT<BR>MOD_DEC_USE_COUNT<BR>MOD_IN_USE <BR><BR>MOD_INC_USE_COUNT 是用来增加 module 的 use count，而 MOD_DEC_USE_COUNT 是用来减少 module 的 use count。至于 MOD_IN_USE 则是用来检查目前这个 module 是不是被使用中。也就是检查 use count 是否为 0。module 的 use count 必须由写 module 的人自己来 maintain。系统并不会自动为你把 use count 加一或减一。一切都得由自己控制。下面有一个例子，但是，并不会介绍这三个 macro 的使用方法。将来如果有机会，我再来介绍这三个 macro 的用法。 <BR><BR>这个例子很简单。其实只是示范如何使用 init_module() 以及 cleanup_module() 来写一个 module。当然，这两个 function 只是构成 module 的基本条件罢了。至于 module 里要提供的功能则是看各人的需要。 <BR><BR>main.c<BR>#define MODULE<BR>#include &lt;linux/module.h&gt;<BR>#include &lt;asm/uaccess.h&gt;<BR>int full;<BR>EXPORT_SYMBOL(full); /* 将 full export 出去 */<BR>int init_module( void )<BR>{<BR>printk( "&lt;5&gt; Module is loaded\n" );<BR>return 0;<BR>}<BR>void cleanup_module( void )<BR>{<BR>printk( "&lt;5&gt; Module is unloaded\n" );<BR>} <BR><BR>关于 printk 是这样子的，它是 kernel 所提供的一个打印讯息的 function。kernel 有 export 这个 function。所以你可以自由的使用它。它的用法跟 printf 几乎一模一样。唯独讯息的开头是 &lt;5&gt;，其实，不见得这三个字符啦。也可以是 &lt;4&gt;，&lt;3&gt;，&lt;7&gt; 等等的东西。这是代表这个讯息的 prioirty 或 level。&lt;5&gt; 表示的是跟 KERNEL 有关的讯息。 <BR><BR>main.ver: <BR><BR>利用 genksyms 产生出来的。 <BR><BR>gcc -E -D__GENKSYMS__ main.c | genksyms -k 2.2.1 &gt; main.ver <BR><BR>接下来，就是要把 main.c compile 成 main.o <BR><BR>gcc -D__KERNEL__ -DMODVERSIONS -DEXPORT_SYMTAB -c \<BR>-I/usr/src/linux/include/linux -include \<BR>/usr/src/linux/include/linux/modversions.h \<BR>-include ./main.ver main.c <BR><BR>好了。main.o 已经成功的 compile 出来了，现在下一个 command， <BR><BR>insmod main.o <BR><BR>检查看 /proc/modules 里是否有 main 这个 module。如果有，表示 main 这个 module 已经载到 kernel 了。再下一个指令，看看 full export 出去的结果。 <BR><BR>ksyms <BR><BR>结果显示 <BR><BR>Address Symbol Defined by<BR>c40220e0 full_R355b84b2 [main]<BR>c401d04c ne_probe [ne]<BR>c401a04c ei_open [8390]<BR>c401a094 ei_close [8390]<BR>c401a504 ei_interrupt [8390]<BR>c401af1c ethdev_init [8390]<BR>c401af80 NS8390_init [8390] <BR><BR>可以看到 full_R355b84b2，表示，我们已经成功的将 full 的名字加上 kernel version 的 information 了。当我们不需要这个 module 时，我们就可以下一个 command， <BR><BR>rmmod main <BR><BR>这样 main 就会被移除掉了。再检查看看 /proc/modules 就可以发现 main 那一行不见了。各位现在可以看一下 /var/log/message 这个档案，应该可以发现以两行 <BR><BR>Apr 12 14:19:05 host kernel: Module is loaded<BR>Apr 12 14:39:29 host kernel: Module is unloaded <BR><BR>这两行就是 printk 印出来的。 <BR><BR>关于 module 的介绍已经到此告一段落了。其实，使用 module 实在是很简单的一件事。对于要发展 driver 或是增加 kernel 某些新功能的人来讲，用 module 不啻为一个方便的方式。希望这篇文章对各位能有所帮助。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/721.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:28 <a href="http://www.cnitblog.com/drizztzou/articles/721.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>内核等待队列机制介绍</title><link>http://www.cnitblog.com/drizztzou/articles/502.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 17:00:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/502.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/502.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/502.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/502.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/502.html</trackback:ping><description><![CDATA[相信很多写程序的人都写过 socket 的程序。当我们 open 一个 socket 之后，接着去读取这个 socket，如果此时没有任何资料可供读取，那 read 就会 block 住。(这是没有加上 O_NONBLOCK 的情形)，直到有资料可读取才会传回来。在 Linux kernel 里有一个数据结构可以帮助我们做到这样的功能。这个数据结构就是这里要跟各位介绍的 wait queue。在 kernel 里，wait_queue 的应用很广，举凡 device driver semaphore 等方面都会使用到 wait_queue 来 implement。所以，它算是 kernel 里蛮基本的一个数据结构。 <BR><BR>接下来，我要跟各位介绍一下 wait_queue 的用法，以及用一个例子来说明如何使用 wait_queue。最后，我会带各位去 trace 一下 wait_queue 的原始程序代码，看看 wait_queue 是如何做到的。 <BR><BR>我想有件事要先提及的是 Linux 在 user space 跟在 kernel space 上的差异。我们知道 Linux 是 multi-tasking 的环境，同时可以有很多人执行很多的程序。这是从 user 的观点来看的。如果就 kernel 的观点来看，是没有所谓的 multi-tasking 的。在 kernel 里，只有 single-thread。也就是说，如果你的 kernel code 正在执行，那系统里只有那部分在执行。不会有另一部分的 kernel code 也在运作。当然，这是指 single processor 的情况下，如果是 SMP 的话，那我就不清楚了。我想很多人都在 Windows 3.1 下写过程序，在那种环境下写程序，每一个程序都必须适当的将 CPU 让给别的程序使用。如果有个程序里面有一个 <BR><BR>while (1); <BR><BR>的话，那保证系统就停在那里了。这种的多任务叫做 non-preemptive。它多任务的特性是由各个程序相互合作而造成的。在 Linux 的 user space 下，则是所谓的 preemptive，各个 process 喜欢执行什么就执行什么，就算你在你的程序里加上 while(1); 这一行也不会影响系统的运作。反正时间到了，系统自动就会将你的程序停住，让别的程序去执行。这是在 user space 的情况下，在 kernel 这方面，就跟 Windows 3.1 程序是一样的。在 kernel 里，你必须适当的将 CPU 的执行权释放出来。如果你在 kernel里加入 while(1); 这一行。那系统就会跟 Windows 3.1 一样。卡在那里。当然啦，我是没试过这样去改 kernel，有兴趣的人可以去试试看，如果有不同的结果，请记得告诉我。 <BR><BR>假设我们在 kernel 里产生一个 buffer，user 可以经由 read，write 等 system call 来读取或写资料到这个 buffer 里。如果有一个 user 写资料到 buffer 时，此时 buffer 已经满了。那请问你要如何去处理这种情形呢 ? 第一种，传给 user 一个错误讯息，说 buffer 已经满了，不能再写入。第二种，将 user 的要求 block 住，等有人将 buffer 内容读走，留出空位时，再让 user 写入资料。但问题来了，你要怎么将 user 的要求 block 住。难道你要用 <BR><BR>while ( is_full ); <BR>write_to_buffer; <BR><BR>这样的程序代码吗? 想想看，如果你这样做会发生什么事? 第一，kernel会一直在这个 while 里执行。第二个，如果 kernel 一直在这个 while 里执行，表示它没有办法去 maintain系统的运作。那此时系统就相当于当掉了。在这里 is_full 是一个变量，当然，你可以让 is_full 是一个 function，在这个 function里会去做别的事让 kernel 可以运作，那系统就不会当。这是一个方式。但是，如果我们使用 wait_queue 的话，那程序看起来会比较漂亮，而且也比较让人了解，如下所示: <BR><BR><BR>struct wait_queue *wq = NULL; /* global variable */ <BR>while ( is_full ) { <BR>interruptible_sleep_on( &amp;wq ); <BR>} <BR>write_to_buffer(); <BR><BR>interruptible_sleep_on( &amp;wq ) 是用来将目前的 process，也就是要求写资料到 buffer 的 process放到 wq 这个 wait_queue 里。在 interruptible_sleep_on 里，则是最后会呼叫 schedule() 来做 schedule 的动作，也就是去找另一个 process 来执行以维持系统的运作。当执行完 interruptible_sleep_on 之后，要求 write 的 process 就会被 block 住。那什么时候会恢复执行呢 ? 这个 process 之所以会被 block 住是因为 buffer 的空间满了，无法写入。但是如果有人将 buffer 的资料读取掉，则 buffer 就有空间可以让人写入。所以，有关于叫醒 process 的动作应该是在 read buffer 这方面的程序代码做的。 <BR><BR>extern struct wait_queue *wq; <BR>if ( !is_empty ) { <BR>read_from_buffer(); <BR>wake_up_interruptible( &amp;wq ); <BR>} <BR>.... <BR><BR>以上的程序代码应该要放在 read buffer 这部分的程序代码里，当 buffer 有多余的空间时,我们就呼叫 wake_up_interruptible( &amp;wq ) 来将挂在 wq 上的所有 process 叫醒。请记得，我是说将 wq 上的所有 process 叫醒，所以，如果如果有10个 process 挂在 wq 上的话，那这 10 个都会被叫醒。之后，至于谁先执行。则是要看 schedule 是怎么做的。就是因为这 10 个都会被叫醒。如果 A 先执行，而且万一很不凑巧的，A 又把 buffer 写满了，那其它 9 个 process 要怎么办呢? 所以在 write buffer 的部分，需要用一个 while 来检查 buffer 目前是否满了.如果是的话，那就继续挂在 wq 上面. <BR><BR>上面所谈的就是 wait_queue 的用法。很简单不是吗? 接下来，我会再介绍一下 wait_queue 提供那些 function 让我们使用。让我再重申一次。wait_queue 应设为 global variable，比方叫 wq，只要任何的 process 想将自己挂在上面，就可以直接叫呼叫 sleep_on 等 function。要将 wq 上的 process 叫醒。只要呼叫 wake_up 等 function 就可以了. <BR><BR>就我所知，wait_queue 提供4个 function 可以使用，两个是用来将 process 加到 wait_queue 的: <BR><BR>sleep_on( struct wait_queue **wq ); <BR>interruptible_sleep_on( struct wait_queue **wq ); <BR><BR>另外两个则是将process从wait_queue上叫醒的。 <BR><BR>wake_up( struct wait_queue **wq ); <BR>wake_up_interruptible( struct wait_queue **wq ); <BR><BR>我现在来解释一下为什么会有两组。有 interruptible 的那一组是这样子的。当我们去 read 一个没有资料可供读取的 socket 时，process 会 block 在那里。如果我们此时按下 Ctrl+C，那 read() 就会传回 EINTR。像这种的 block IO 就是使用 interruptible_sleep_on() 做到的。也就是说，如果你是用 interruptible_sleep_on() 来将 process 放到 wait_queue 时，如果有人送一个 signal 给这个 process，那它就会自动从 wait_queue 中醒来。但是如果你是用 sleep_on() 把 process 放到 wq 中的话，那不管你送任何的 signal 给它，它还是不会理你的。除非你是使用 wake_up() 将它叫醒。sleep 有两组。wake_up 也有两组。wake_up_interruptible() 会将 wq 中使用 interruptible_sleep_on() 的 process 叫醒。至于 wake_up() 则是会将 wq 中所有的 process 叫醒。包括使用 interruptible_sleep_on() 的 process。 <BR><BR>在使用 wait_queue 之前有一点需要特别的小心，呼叫 interruptible_sleep_on() 以及 sleep_on() 的 function 必须要是 reentrant。简单的说，reentrant 的意思是说此 function不会改变任何的 global variable，或者是不会 depend on 任何的 global variable，或者是在呼叫 interruptible_sleep_on() 或 sleep_on() 之后不会 depend on 任何的 global variable。因为当此 function 呼叫 sleep_on() 时，目前的 process 会被暂停执行。可能另一个 process 又会呼叫此 function。若之前的 process 将某些 information 存在 global variable，等它恢复执行时要使用，结果第二行程进来了，又把这个 global variable 改掉了。等第一个 process 恢复执行时，放在 global variable 中的 information 都变了。产生的结果恐怕就不是我们所能想象了。其实，从 process 执行指令到此 function 中所呼叫的 function 都应该是要 reentrant 的。不然，很有可能还是会有上述的情形发生. <BR><BR>由于 wait_queue 是 kernel 所提供的，所以，这个例子必须要放到 kernel 里去执行。我使用的这个例子是一个简单的 driver。它会 maintain 一个 buffer，大小是 8192 bytes。提供 read跟 write 的功能。当 buffer 中没有资料时，read() 会马上传回，也就是不做 block IO。而当 write buffer 时，如果呼叫 write() 时，空间已满或写入的资料比 buffer 大时，就会被 block 住，直到有人将 buffer 里的资料读出来为止。在 write buffer 的程序代码中，我们使用 wait_queue 来做到 block IO 的功能。在这里，我会将此 driver 写成 module，方便加载 kernel。 <BR><BR>第一步，这个 driver 是一个简单的 character device driver。所以，我们先在 /dev 下产生一个 character device。major number 我们找一个比较没人使用的，像是 54，minor number 就用 0。接着下一个命令. <BR><BR>mknod /dev/buf c 54 0 <BR><BR>mknod 是用来产生 special file 的 command。/dev/buf 表示要产生叫 buf 的档案，位于 /dev 下。 c 表示它是一个 character device。54 为其 major number，0 则是它的 minor number。有关 character device driver 的写法。有机会我再跟各位介绍，由于这次是讲 wait_queue，所以，就不再多提 driver 方面的东西. <BR><BR>第二步，我们要写一个 module，底下是这个 module 的程序代码: <BR><BR>buf.c <BR>#define MODULE <BR>#include <BR>#include <BR>#include <BR>#include <BR>#include <BR>#define BUF_LEN 8192 <BR><BR>int flag; /* when rp = wp,flag = 0 for empty,flag = 1 for <BR>non-empty */ <BR>char *wp，*rp; <BR>char buffer[BUF_LEN]; <BR>EXPORT_NO_SYMBOLS; /* don\t export anything */ <BR><BR>static ssize_t buf_read( struct file *filp，char *buf，size_t count， <BR>loff_t *ppos ) <BR>{ <BR>return count; <BR>} <BR><BR>static ssize_t buf_write( struct file *filp，const char *buf，size_t count， <BR>loff_t *ppos ) <BR>{ <BR>return count; <BR>} <BR><BR>static int buf_open( struct inode *inode，struct file *filp ) <BR>{ <BR>MOD_INC_USE_COUNT; <BR>return 0; <BR>} <BR><BR>static int buf_release( struct inode *inode，struct file *filp ) <BR>{ <BR>MOD_DEC_USE_COUNT; <BR>return 0; <BR>} <BR><BR>static struct file_operations buf_fops = { <BR>NULL， /* lseek */ <BR>buf_read, <BR>buf_write, <BR>NULL， /* readdir */ <BR>NULL， /* poll */ <BR>NULL， /* ioctl */ <BR>NULL， /* mmap */ <BR>buf_open， /* open */ <BR>NULL， /* flush */ <BR>buf_release， /* release */ <BR>NULL， /* fsync */ <BR>NULL， /* fasync */ <BR>NULL， /* check_media_change */ <BR>NULL， /* revalidate */ <BR>NULL /* lock */ <BR>}; <BR><BR>static int buf_init() <BR>{ <BR>int result; <BR><BR>flag = 0; <BR>wp = rp = buf; <BR><BR>result = register_chrdev( 54，\"buf\"，&amp;buf_fops ); <BR>if ( result &lt; 0 ) { <BR>printk( \"&lt;5&gt;buf: cannot get major 54 \" ); <BR>return result; <BR>} <BR><BR>return 0; <BR>} <BR><BR>static void buf_clean() <BR>{ <BR>if ( unregister_chrdev( 54，\"buf\" ) ) { <BR>printk( \"&lt;5&gt;buf: unregister_chrdev error \" ); <BR>} <BR>} <BR><BR>int init_module( void ) <BR>{ <BR>return buf_init(); <BR>} <BR><BR>void cleanup_module( void ) <BR>{ <BR>buf_clean(); <BR>} <BR><BR>有关 module 的写法，请各位自行参考其它的文件，最重要的是要有 init_module()和 cleanup_module() 这两个 function。我在这两个 function 里分别做 initialize 和 finalize 的动作。现在分别解释一下。在 init_module() 里，只有呼叫 buf_init() 而己。其实，也可以将 buf_init() 的 code 写到 init_module() 里。只是我觉得这样比较好而已。 <BR><BR>flag = 0; <BR>wp = rp = buf; <BR>result = register_chrdev( 54，\"buf\"，&amp;buf_fops ); <BR>if ( result &lt; 0 ) { <BR>printk( \"&lt;5&gt;buf: cannot get major 54 \" ); <BR>return result; <BR>} <BR>return 0; <BR><BR>init_buf() 做的事就是去注册一个 character device driver。在注册一个 character device driver 之前，必须要先准备一个型别为 file_operations 结构的变量，file_operations 里包含了一些 function pointer。driver 的作者必须自己写这些 function。并将 function address 放到这个结构里。如此一来，当 user 去读取这个 device 时，kernel 才有办法去呼叫对应这个 driver 的 function。其实，简要来讲。character device driver 就是这么一个 file_operations 结构的变量。file_operations 定义在 这个档案里。它的 prototype 在 kernel 2.2.1 与以前的版本有些微的差异，这点是需要注意的地方。 <BR><BR>register_chrdev() 看名字就大概知道是要注册 character device driver。第一个参数是此 device 的 major number。第二个是它的名字。名字你可以随便取。第三个的参数就是一个 file_operations 变量的地址。init_module() 必须要传回 0，module 才会被加载。 <BR><BR>在 cleanup_module() 的部分，我们也是只呼叫 buf_clean() 而已。它做的事是 unregister 的动作。 <BR><BR>if ( unregister_chrdev( 54，\"buf\" ) ) { <BR>printk( \"&lt;5&gt;buf: unregister_chrdev error \" ); <BR>} <BR><BR>也就是将原本记录在 device driver table 上的资料洗掉。第一个参数是 major number。第二个则是此 driver 的名称，这个名字必须要跟 register_chrdev() 中所给的名字一样才行。 <BR><BR>现在我们来看看此 driver 所提供的 file_operations 是那些。 <BR><BR>static struct file_operations buf_fops = { <BR>NULL， /* lseek */ <BR>buf_read, <BR>buf_write, <BR>NULL， /* readdir */ <BR>NULL， /* poll */ <BR>NULL， /* ioctl */ <BR>NULL， /* mmap */ <BR>buf_open， /* open */ <BR>NULL， /* flush */ <BR>buf_release， /* release */ <BR>NULL， /* fsync */ <BR>NULL， /* fasync */ <BR>NULL， /* check_media_change */ <BR>NULL， /* revalidate */ <BR>NULL /* lock */ <BR>}; <BR><BR>在此，我们只打算 implement buf_read()，buf_write()，buf_open，和 buf_release()等 function 而已。当 user 对这个 device 呼叫 open() 的时候，buf_open() 会在最后被 kernel 呼叫。相同的，当呼叫 close()，read()，和 write() 时，buf_release()，buf_read()，和 buf_write() 也都会分别被呼叫。首先，我们先来看看 buf_open()。 <BR><BR>static int buf_open( struct inode *inode，struct file *filp ) <BR>MOD_INC_USE_COUNT; <BR>return 0; <BR>} <BR><BR>buf_open() 做的事很简单。就是将此 module 的 use count 加一。这是为了避免当此 module 正被使用时不会被从 kernel 移除掉。相对应的，在 buf_release() 中，我们应该要将 use count 减一。就像开启档案一样。有 open()，就应该要有对应的 close() 才行。如果 module 的 use count 在不为 0 的话，那此 module 就无法从 kernel 中移除了。 <BR><BR>static int buf_release( struct inode *inode，struct file *filp ) <BR>{ <BR>MOD_DEC_USE_COUNT; <BR>return 0; <BR>} <BR><BR>接下来，我们要看一下buf_read()和buf_write()。 <BR><BR>static ssize_t buf_read( struct file *filp，char *buf，size_t count, <BR>loff_t *ppos ) <BR>{ <BR>return count; <BR>} <BR><BR>static ssize_t buf_write( struct file *filp，const char *buf， <BR>size_t count，loff_t *ppos ) <BR>{ <BR>return count; <BR>} <BR><BR>在此，我们都只是回传 user 要求读取或写入的字符数目而已。在此，我要说明一下这些参数的意义。filp 是一个 file 结构的 pointer。也就是指我们在 /dev 下所产生的 buf 档案的 file 结构。当我们呼叫 read() 或 write() 时，必须要给一个 buffer 以及要读写的长度。Buf 指的就是这个 buffer，而 count 指的就是长度。至于 ppos 是表示目前这个档案的 offset 在那里。这个值对普通档案是有用的。也就是跟 lseek() 有关系。由于在这里是一个 drvice。所以 ppos 在此并不会用到。有一点要小心的是，上面参数 buf 是一个地址，而且还是一个 user space 的地址，当 kernel 呼叫 buf_read() 时，程序在位于 kernel space。所以你不能直接读写资料到 buf 里。必须先切换 FS 这个 register 才行。 <BR><BR>Makefile <BR>P = buf <BR>OBJ = buf.o <BR>INCLUDE = -I/usr/src/linux/include/linux <BR>CFLAGS = -D__KERNEL__ -DMODVERSIONS -DEXPORT_SYMTAB -O $(INCLUDE) <BR>-include /usr/src/linux/include/linux/modversions.h <BR>CC = gcc <BR><BR>$(P): $(OBJ) <BR>ld -r $(OBJ) -o $(P).o <BR><BR>.c.o: <BR>$(CC) -c $(CFLAGS) $&lt; <BR><BR>clean: <BR>rm -f *.o *~ $(P) <BR><BR>加入上面这个 Makefile，打入 make 之后，就会产生一个 buf.o 的档案。利用 insmod 将 buf.o 载到 kernel 里。相信大家应该都用过 /dev/zero 这个 device。去读取这个 device，只会得到空的内容。写资料到这个 device 里也只会石沈大海。现在你可以去比较 buf 和 zero 这两个 device。两者的行为应该很类似才是。 <BR><BR>第三步，我们在第二步中 implement 一个像 zero 的 device driver。我们现在要经由修改它来使用 wait_queue。首先，我们先加入一个 global variable，write_wq，并把它设为 NULL。 <BR><BR>struct wait_queue *write_wq = NULL; <BR><BR>然后，在 buf_read() 里，我们要改写成这个样子。 <BR><BR>static ssize_t buf_read( struct file *filp，char *buf，size_t count， <BR>loff_t *ppos ) <BR>{ <BR>int num，nRead; <BR>nRead = 0; <BR>while ( ( wp == rp ) &amp;&amp; !flag ) { /* buffer is empty */ <BR>return 0; <BR>} <BR><BR>repeate_reading: <BR>if ( rp &lt; wp ) { <BR>num = min( count，( int ) ( wp-rp ) ); <BR>} <BR>else { <BR>num = min( count，( int ) ( buffer+BUF_LEN-rp ) ); <BR>} <BR>copy_to_user( buf，rp，num ); <BR>rp += num; <BR>count -= num; <BR>nRead += num; <BR>if ( rp == ( buffer + BUF_LEN ) ) <BR>rp = buffer; <BR>if ( ( rp != wp ) &amp;&amp; ( count &gt; 0 ) ) <BR>goto repeate_reading; <BR>flag = 0; <BR>wake_up_interruptible( &amp;write_wq ); <BR>return nRead; <BR>} <BR><BR>在前头我有提到，buf 的地址是属于 user space 的。在 kernel space 中，你不能像普通写到 buffer 里一样直接将资料写到 buf 里，或直接从 buf 里读资料。Linux 里使用 FS 这个 register 来当作 kernel space 和 user space 的切换。所以，如果你想手动的话，可以这样做: <BR><BR>mm_segment_t fs; <BR>fs = get_fs(); <BR>set_fs( USER_DS ); <BR>write_data_to_buf( buf ); <BR>set_fs( fs ); <BR><BR>也就是先切换到 user space，再写资料到 buf 里。之后记得要切换回来 kernel space。这种自己动手的方法比较麻烦，所以 Linux 提供了几个 function，可以让我们直接在不同的 space 之间做资料的搬移。诚如各位所见，copy_to_user() 就是其中一个。 <BR><BR>copy_to_user( to，from，n ); <BR>copy_from_user( to，from，n ); <BR><BR>顾名思义，copy_to_user() 就是将资料 copy 到 user space 的 buffer 里，也就是从 to 写到 from，n 为要 copy 的 byte 数。相同的，copy_from_user() 就是将资料从 user space 的 from copy 到位于 kernel 的 to 里，长度是 n bytes。在以前的 kernel 里，这两个 function 的前身是 memcpy_tofs() 和 memcpy_fromfs()，不知道为什么到了 kernel 2.2.1之后，名字就被改掉了。至于它们的程序代码有没有更改就不太清楚了。至于到那一版才改的。我没有仔细去查，只知道在 2.0.36 时还没改，到了 2.2.1 就改了。这两个 function 是 macro，都定义在 里。要使用前记得先 include 进来。 <BR><BR>相信 buf_read() 的程序代码应当不难了解才对。不知道各位有没有看到，在buf_read() 的后面有一行的程序，就是 <BR><BR>wake_up_interruptible( &amp;write_wq ); <BR><BR>write_wq 是我们用来放那些想要写资料到 buffer，但 buffer 已满的 process。这一行的程序会将挂在此 queue 上的 process 叫醒。当 queue 是空的时，也就是当 write_wq 为 NULL 时，wake_up_interruptible() 并不会造成任何的错误。接下来，我们来看看更改后的 buf_write()。 <BR><BR>static ssize_t buf_write( struct file *filp，const char *buf，size_t count，loff_t *ppos ) <BR>{ <BR>int num，nWrite; <BR>nWrite = 0; <BR>while ( ( wp == rp ) &amp;&amp; flag ) { <BR>interruptible_sleep_on( &amp;write_wq ); <BR>} <BR><BR>repeate_writing: <BR>if ( rp &gt; wp ) { <BR>num = min( count，( int ) ( rp - wp ) ); <BR>} <BR>else { <BR>num = min( count，( int ) ( buffer + BUF_LEN - wp ) ); <BR>} <BR>copy_from_user( wp，buf，num ); <BR>wp += num; <BR>count -= num; <BR>nWrite += num; <BR>if ( wp == ( buffer + BUF_LEN ) ) { <BR>wp = buffer; <BR>} <BR>if ( ( wp != rp ) &amp;&amp; ( count &gt; 0 ) ) { <BR>goto repeate_writing; <BR>} <BR>flag = 1; <BR>return nWrite; <BR>} <BR><BR>我们把 process 丢到 write_wq 的动作放在 buf_write() 里。当 buffer 已满时，就直接将 process 丢到 write_wq 里. <BR><BR>while ( ( wp == rp ) &amp;&amp; flag ) { <BR>interruptible_sleep_on( &amp;write_wq ); <BR>} <BR><BR>好了。现在程序已经做了一些修改。再重新 make 一次，利用 insmod 将 buf.o 载到 kernel 里就行了。接着，我们就来试验一下是不是真正做到 block IO. <BR><BR># cd /dev <BR># ls -l ~/WWW-HOWTO <BR>-rw-r--r-- 1 root root 23910 Apr 14 16:50 /root/WWW-HOWTO <BR># cat ~/WWW-HOWTO &gt; buf <BR><BR>执行到这里，应该会被 block 住。现在，我们再开一个 shell 出来. <BR><BR># cd /dev <BR># cat buf <BR>..。( contents of WWW-HOWTO ) ..。skip ... <BR><BR>此时，WWW-HOWTO 的内容就会出现了。而且之前 block 住的 shell 也已经回来了。最后，试验结束，可以下 <BR><BR># rmmod buf <BR><BR>将 buf 这个 module 从 kernel 中移除。以上跟各位介绍的就是 wait_queue 的使用。希望能对各位有所助益。 <BR><BR>我想对某些人来讲，会使用一个东西就够了。然而对某些人来讲，可能也很希望知道这项东西是如何做出来的。至少我就是这种人。在下面，我将为各位介绍 wait_queue 的 implementation。如果对其 implementation 没兴趣，以下这一段就可以略过不用看了。 <BR><BR>wait_queue 是定义在 里，我们可以先看看它的数据结构是怎么样: <BR><BR>struct wait_queue { <BR>struct task_struct * task; <BR>struct wait_queue * next; <BR>}; <BR><BR>很简单是吧。这个结构里面只有二个字段，一个是 task_struct 的 pointer，另一个则是 wait_queue 的 pointer。很明显的，我们可以看出 wait_queue 其实就是一个 linked list,而且它还是一个 circular linked list。 其中 task_struct 就是用来指呼叫 sleep_on 等 function的 process。在 Linux 里，每一个 process 是由一个 task_struct 来描叙。task_struct 是一个很大的的结构，在此我们不会讨论。Linux 里有一个 global variable，叫 current，它会指到目前正在执行的 process 的 task_struct 结构。这也就是为什么当 process 呼叫 system call，切换到 kernel 时，kernel 会知道是那个 process 呼叫的。 <BR><BR>好，我们现在来看看 interruptible_sleep_on() 和 sleep_on() 是如何做的。这两个 function 都是位于 /usr/src/linux/kernel/sched.c 里。 <BR><BR>void interruptible_sleep_on(struct wait_queue **p) <BR>{ <BR>SLEEP_ON_VAR <BR>current-&gt;state = TASK_INTERRUPTIBLE; <BR>SLEEP_ON_HEAD <BR>schedule(); <BR>SLEEP_ON_TAIL <BR>} <BR><BR>void sleep_on(struct wait_queue **p) <BR>{ <BR>SLEEP_ON_VAR <BR>current-&gt;state = TASK_UNINTERRUPTIBLE; <BR>SLEEP_ON_HEAD <BR>schedule(); <BR>SLEEP_ON_TAIL <BR>} <BR><BR>各位有没有发现这两个 function 很类似。是的，它们唯一的差别就在于 <BR><BR>current-&gt;state = ... <BR><BR>这一行而已。之前，我们有说过，interruptible_sleep_on() 可以被 signal 中断，所以，其 current-&gt;state 被设为 TASK_INTERRUPTIBLE。而 sleep_on() 没办法被中断，所以 current-&gt;state 设为 TASK_UNINTERRUPTIBLE。接下来，我们只看 interruptible_sleep_on() 就好了。毕竟它们两的差异只在那一行而已。 <BR><BR>在 sched.c 里，SLEEP_ON_VAR 是一个 macro，其实它只是定义两个区域变量出来而已。 <BR><BR>#defineSLEEP_ON_VAR <BR>unsigned long flags; <BR>struct wait_queue wait; <BR><BR>刚才我也说过，current 这个变量是指到目前正在执行的 process 的 task_struct 结构。所以 current-&gt;state = TASK_INTERRUPTIBLE 会设定在呼叫 interruptible_sleep_on() 的 process 身上。至于 SLEEP_ON_HEAD 做的事，则是将 current 的值放到 SLEEP_ON_VAR 宣告的 wait 变量里，并把 wait 放到 interruptible_sleep_on() 的参数所属的 wait_queue list 中。 <BR><BR>#defineSLEEP_ON_HEAD <BR>wait.task = current; <BR>write_lock_irqsave(&amp;waitqueue_lock，flags); <BR>__add_wait_queue(p，&amp;wait); <BR>write_unlock(&amp;waitqueue_lock); <BR><BR>wait 是在 SLEEP_ON_VAR 中宣告的区域变量。其 task 字段被设成呼叫 interruptible_sleep_on() 的 process。至于 waitqueue_lock 这个变量是一个 spin lock。 waitqueue_lock 是用来确保同一时间只能有一个 writer。但同一时间则可以有好几个 reader。也就是说 waitqueue_lock 是用来保证 critical section 的 mutual exclusive access。 <BR><BR>unsigned long flags; <BR>write_lock_irqsave(&amp;waitqueue_lock，flags); <BR>...critical section ... <BR>write_unlock(&amp;waitqueue_lock) <BR><BR>学过 OS 的人应该知道 critical section 的作用是什么，如有需要，请自行参考 OS 参考书。在 critical section 里只做一件事，就是将 wait 这个区域变量放到 p 这个 wait_queue list 中。 p 是 user 在呼叫 interruptible_sleep_on() 时传进来的，它的型别是 struct wait_queue **。在此， critical section 只呼叫 __add_wait_queue()。 <BR><BR>extern inline void __add_wait_queue(struct wait_queue ** p， <BR>struct wait_queue * wait) <BR>{ <BR>wait-&gt;next = *p ? : WAIT_QUEUE_HEAD(p); <BR>*p = wait; <BR>} <BR><BR>__add_wait_queue() 是一个inline function，定义在 中。WAIT_QUEUE_HEAD()是个很有趣的 macro，待会我们再讨论。现在只要知道它会传回这个 wait_queue 的开头就可以了。所以，__add_wait_queue() 的意思就是要把 wait 放到 p 所属的 wait_queue list 的开头。但是，大家还记得吗? 在上面的例子里，一开始我们是把 write_wq 设为 NULL。也就是说 *p 是 NULL。所以，当 *p 是 NULL 时, <BR><BR><BR>wait-&gt;next = WAIT_QUEUE_HEAD(p) <BR><BR>是什么意思呢? <BR><BR>所以，现在，我们来看一下 WAIT_QUEUE_HEAD() 是怎么样的一个 macro，它是定义在 里。 <BR><BR>#define WAIT_QUEUE_HEAD(x) ((struct wait_queue *)((x)-1)) <BR><BR>x 型别是 struct wait_queue **，因为是一个 pointer，所以大小是 4 byte。因此，若 x 为 100 的话，那 ((x)-1) 就变成 96。如下图所示。 WAIT_QUEUE_HEAD(x) 其实会传回 96，而且将其转型为 struct wait_queue*，各位可以看看。原本的 wait_queue* 只配制在 100-104 之间。现在 WAIT_QUEUE_HEAD(x) 却直接传回96，但是 96-100 这块位置根本没有被我们配置起来。更妙的事。由于 x 是一个 wait_queue list 的开头，我们始终不会用到 96-100 这块，我们只会直接使用到 100-104 这块内存。这也算是 wait_queue 一项比较奇怪的 implementation 方式吧。下面有三张图，第一张表示我们宣告了一个 wait_queue* 的变量，地址在 100。另外还有一个 wait_queue 的变量，名叫 wait。第二张图是我们呼叫 interruptible_sleep_on() 之后得到的结果。第三张则是我们又宣告一个 wait_queue，名叫 ano_wait，将 ano_wait 放到 wait_queue list 后的结果就第三张图所显示的。<A style="COLOR: #003793" href="http://linuxfab.cx/Columns/10/wqq.GIF" target=_blank>http://linuxfab.cx/Columns/10/wqq.GIF</A> <BR><BR>在 interruptible_sleep_on() 中，当呼叫完 SLEEP_ON_HEAD 之后，目前的 process 就已经被放到 wait_queue 中了。接下来会直接呼叫 schedule()，这个 function 是用来做 scheduling 用的。current 所指到的 process 会被放到 scheduling queue 中等待被挑出来执行。执行完 schedule() 之后，current 就没办法继续执行了。而当 current 以后被 wake up 时，就会从 schedule() 之后，也就是从 SLEEP_ON_TAIL 开始执行。SLEEP_ON_TAIL 做的事刚好跟 SLEEP_ON_HEAD 相反，它会将此 process 从 wait_queue 中移除。 <BR><BR>#defineSLEEP_ON_TAIL <BR>write_lock_irq(&amp;waitqueue_lock); <BR>__remove_wait_queue(p，&amp;wait); <BR>write_unlock_irqrestore(&amp;waitqueue_lock，flags); <BR><BR>跟 SLEEP_ON_HEAD 一样。SLEEP_ON_TAIL 也是利用 spin lock 包住一个 critical section。 <BR><BR>extern inline void __remove_wait_queue(struct wait_queue ** p，struct <BR>wait_queue * wait) <BR>{ <BR>struct wait_queue * next = wait-&gt;next; <BR>struct wait_queue * head = next; <BR>struct wait_queue * tmp; <BR>while ((tmp = head-&gt;next) != wait) { <BR>head = tmp; <BR>} <BR>head-&gt;next = next; <BR>} <BR><BR>__remove_wait_queue() 是一个 inline function，也是同样定义在 里。是用来将 wait 从 p 这个 wait_queue list 中移除掉。 <BR><BR>现在，大家应该已经清楚了 interruptible_sleep_on() 和 sleep_on() 的做法，也应该比较清楚 wait_queue 是如何的做到 block IO。接下来，我们继续看 wake_up_interruptible() 和 wake_up() 是如何 implement 的。wake_up_interruptible() 和 wake_up() 其实是两个 macro，都定义在 里。 <BR><BR>#define wake_up(x) __wake_up((x),TASK_UNINTERRUPTIBLE | <BR>TASK_INTERRUPTIBLE) <BR>#define wake_up_interruptible(x) __wake_up((x),TASK_INTERRUPTIBLE) <BR><BR>从这里可以看出，两个 macro 几乎是一样的，差别只在于传给 __wake_up() 中的一个 flag 有所差异而已。其实，wake_up() 传给 __wake_up() 的是 TASK_UNINTERRUPTIBLE|TASK_INTERRUPTIBLE，意思是说它会将 wait_queue list 中 process-&gt;state 是 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的所有 process 叫醒。而 wake_up_interruptible() 则只将 state是 TASK_INTERRUPTIBLE 的叫醒. <BR><BR>void __wake_up(struct wait_queue **q，unsigned int mode) <BR>{ <BR>struct wait_queue *next; <BR>read_lock(&amp;waitqueue_lock); <BR>if (q &amp;&amp; (next = *q)) { <BR>struct wait_queue *head; <BR>head = WAIT_QUEUE_HEAD(q); <BR>while (next != head) { <BR>struct task_struct *p = next-&gt;task; <BR>next = next-&gt;next; <BR>if (p-&gt;state &amp; mode) <BR>wake_up_process(p); <BR>} <BR>} <BR>read_unlock(&amp;waitqueue_lock); <BR>} <BR><BR>在 wake up 的过程中，我们不需要设定 write lock，但是仍要设定 read lock，这是为了避免有人在我们读取 wait_queue 时去写 wait_queue list 的内容，造成 inconsistent。在这段程序代码中，是去 transverse 整个 list，如果 process 的 state 跟 mode 有吻合，则呼叫 wake_up_process() 将它叫醒。 <BR><BR>void wake_up_process(struct task_struct * p) <BR>{ <BR>unsigned long flags; <BR>spin_lock_irqsave(&amp;runqueue_lock，flags); <BR>p-&gt;state = TASK_RUNNING; <BR>if (!p-&gt;next_run) { <BR>add_to_runqueue(p); <BR>reschedule_idle(p); <BR>} <BR>spin_unlock_irqrestore(&amp;runqueue_lock，flags); <BR>} <BR><BR>在此，runqueue_lock 也是一个 spin lock，kernel 依然在此设一个 critical section 以方便更改 run queue。Run queue 是用来放可以执行的 process 用的。在放入 run queue 之前，会先将 process 的 state 设为 TASK_RUNNING。 <BR><BR>wait_queue 其实是一个蛮好用的东西。相信只要各位有机会去修改 kernel 的话，都应该有机会用到它才对。希望对大家有点帮助。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/502.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 01:00 <a href="http://www.cnitblog.com/drizztzou/articles/502.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux操作系统内核引导程序详细剖析</title><link>http://www.cnitblog.com/drizztzou/articles/501.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:59:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/501.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/501.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/501.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/501.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/501.html</trackback:ping><description><![CDATA[! bootsect.s (c) 1991, 1992 Linus Torvalds 版权所有 <BR>! Drew Eckhardt修改过 <BR>! Bruce Evans (bde)修改过 <BR>! <BR>! bootsect.s 被bios-启动子程序加载至0x7c00 (31k)处，并将自己 <BR>! 移到了地址0x90000 (576k)处，并跳转至那里。 <BR>! <BR>! bde - 不能盲目地跳转，有些系统可能只有512k的低 <BR>! 内存。使用中断0x12来获得(系统的)最高内存、等。 <BR>! <BR>! 它然后使用BIOS中断将\setup\直接加载到自己的后面(0x90200)(576.5k)， <BR>! 并将系统加载到地址0x10000处。 <BR>! <BR>! 注意! 目前的内核系统最大长度限制为(8*65536-4096)(508k)字节长，即使是在 <BR>! 将来这也是没有问题的。我想让它保持简单明了。这样508k的最大内核长度应该 <BR>! 是足够了，尤其是这里没有象minix中一样包含缓冲区高速缓冲(而且尤其是现在 <BR>! 内核是压缩的 :-) <BR>! <BR>! 加载程序已经做的尽量地简单了，所以持续的读出错将导致死循环。只能手工重启。 <BR>! 只要可能，通过一次取得整个磁道，加载过程可以做的很快的。 <BR><BR>#include /* 为取得CONFIG_ROOT_RDONLY参数 */ <BR>!! config.h中(即autoconf.h中)没有CONFIG_ROOT_RDONLY定义!!!? <BR><BR>#include <BR><BR>.text <BR><BR>SETUPSECS = 4 ! 默认的setup程序扇区数(setup-sectors)的默认值; <BR><BR>BOOTSEG = 0x7C0 ! bootsect的原始地址; <BR><BR>INITSEG = DEF_INITSEG ! 将bootsect程序移到这个段处(0x9000) - 避开; <BR>SETUPSEG = DEF_SETUPSEG ! 设置程序(setup)从这里开始(0x9020); <BR>SYSSEG = DEF_SYSSEG ! 系统加载至0x1000(65536)(64k)段处; <BR>SYSSIZE = DEF_SYSSIZE ! 系统的大小(0x7F00): 要加载的16字节为一节的数; <BR>!! 以上4个DEF_参数定义在boot.h中: <BR>!! DEF_INITSEG 0x9000 <BR>!! DEF_SYSSEG 0x1000 <BR>!! DEF_SETUPSEG 0x9020 <BR>!! DEF_SYSSIZE 0x7F00 (=32512=31.75k)*16=508k <BR><BR>! ROOT_DEV &amp; SWAP_DEV 现在是由\"build\"中编制的; <BR>ROOT_DEV = 0 <BR>SWAP_DEV = 0 <BR>#ifndef SVGA_MODE <BR>#define SVGA_MODE ASK_VGA <BR>#endif <BR>#ifndef RAMDISK <BR>#define RAMDISK 0 <BR>#endif <BR>#ifndef CONFIG_ROOT_RDONLY <BR>#define CONFIG_ROOT_RDONLY 1 <BR>#endif <BR><BR>! ld86 需要一个入口标识符，这和通常的一样; <BR>.globl _main <BR>_main: <BR>#if 0 /* 调试程序的异常分支，除非BIOS古怪（比如老的HP机）否则是无害的 */ <BR>int 3 <BR>#endif <BR>mov ax,#BOOTSEG !! 将ds段寄存器置为0x7C0; <BR>mov ds,ax <BR>mov ax,#INITSEG !! 将es段寄存器置为0x9000; <BR>mov es,ax <BR>mov cx,#256 !! 将cx计数器置为256(要移动256个字, 512字节); <BR>sub si,si !! 源地址 ds:si=0x07C0:0x0000; <BR>sub di,di !! 目的地址es:di=0x9000:0x0000; <BR>cld !! 清方向标志; <BR>rep !! 将这段程序从0x7C0:0(31k)移至0x9000:0(576k)处; <BR>movsw !! 共256个字(512字节)(0x200长); <BR>jmpi go,INITSEG !! 间接跳转至移动后的本程序go处; <BR><BR>! ax和es现在已经含有INITSEG的值(0x9000); <BR><BR>go: mov di,#0x4000-12 ! 0x4000(16k)是&gt;=bootsect + setup 的长度 + <BR>! + 堆栈的长度 的任意的值; <BR>! 12 是磁盘参数块的大小 es:di=0x94000-12=592k-12; <BR><BR>! bde - 将0xff00改成了0x4000以从0x6400处使用调试程序(bde)。如果 <BR>! 我们检测过最高内存的话就不用担心这事了，还有，我的BIOS可以被配置为将wini驱动表 <BR>! 放在内存高端而不是放在向量表中。老式的堆栈区可能会搞乱驱动表; <BR><BR>mov ds,ax ! 置ds数据段为0x9000; <BR>mov ss,ax ! 置堆栈段为0x9000; <BR>mov sp,di ! 置堆栈指针INITSEG:0x4000-12处; <BR>/* <BR>* 许多BIOS的默认磁盘参数表将不能 <BR>* 进行扇区数大于在表中指定 <BR>* 的最大扇区数( - 在某些情况下 <BR>* 这意味着是7个扇区)后面的多扇区的读操作。 <BR>* <BR>* 由于单个扇区的读操作是很慢的而且当然是没问题的， <BR>* 我们必须在RAM中(为第一个磁盘)创建新的参数表。 <BR>* 我们将把最大扇区数设置为36 - 我们在一个ED 2.88驱动器上所能 <BR>* 遇到的最大值。 <BR>* <BR>* 此值太高是没有任何害处的，但是低的话就会有问题了。 <BR>* <BR>* 段寄存器是这样的: ds=es=ss=cs - INITSEG,(=0X9000) <BR>* fs = 0, gs没有用到。 <BR>*/ <BR><BR>! 上面执行重复操作(rep)以后，cx为0; <BR><BR>mov fs,cx !! 置fs段寄存器=0; <BR>mov bx,#0x78 ! fs:bx是磁盘参数表的地址; <BR>push ds <BR>seg fs <BR>lds si,(bx) ! ds:si是源地址; <BR>!! 将fs:bx地址所指的指针值放入ds:si中; <BR>mov cl,#6 ! 拷贝12个字节到0x9000:0x4000-12开始处; <BR>cld <BR>push di !! 指针0x9000:0x4000-12处; <BR><BR>rep <BR>movsw <BR><BR>pop di !! di仍指向0x9000:0x4000-12处(参数表开始处); <BR>pop si !! ds =&gt; si=INITSEG(=0X9000); <BR><BR>movb 4(di),*36 ! 修正扇区计数值; <BR><BR>seg fs <BR>mov (bx),di !! 修改fs:bx(0000:0x0078)处磁盘参数表的地址为0x9000:0x4000-12; <BR>seg fs <BR>mov 2(bx),es <BR><BR>! 将setup程序所在的扇区(setup-sectors)直接加载到boot块的后面。!! 0x90200开始处; <BR>! 注意，\es\已经设置好了。 <BR>! 同样经过rep循环后cx为0 <BR><BR>load_setup: <BR>xor ah,ah ! 复位软驱(FDC); <BR>xor dl,dl <BR>int 0x13 <BR><BR>xor dx,dx ! 驱动器0, 磁头0; <BR>mov cl,#0x02 ! 从扇区2开始，磁道0; <BR>mov bx,#0x0200 ! 置数据缓冲区地址=es:bx=0x9000:0x200; <BR>! 在INITSEG段中,即0x90200处; <BR>mov ah,#0x02 ! 要调用功能号2(读操作); <BR>mov al,setup_sects ! 要读入的扇区数SETUPSECS=4; <BR>! (假释所有数据都在磁头0、磁道0); <BR>int 0x13 ! 读操作; <BR>jnc ok_load_setup ! ok则继续; <BR><BR>push ax ! 否则显示出错信息。保存ah的值(功能号2); <BR>call print_nl !! 打印换行; <BR>mov bp,sp !! bp将作为调用print_hex的参数; <BR>call print_hex !! 打印bp所指的数据; <BR>pop ax <BR><BR>jmp load_setup !! 重试! <BR><BR>!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! <BR>!!INT 13 - DISK - READ SECTOR(S) INTO MEMORY <BR>!! AH = 02h <BR>!! AL = number of sectors to read (must be nonzero) <BR>!! CH = low eight bits of cylinder number <BR>!! CL = sector number 1-63 (bits 0-5) <BR>!! high two bits of cylinder (bits 6-7, hard disk only) <BR>!! DH = head number <BR>!! DL = drive number (bit 7 set for hard disk) <BR>!! ES:BX -&gt; data buffer <BR>!! Return: CF set on error <BR>!! if AH = 11h (corrected ECC error), AL = burst length <BR>!! CF clear if successful <BR>!! AH = status (see #00234) <BR>!! AL = number of sectors transferred (only valid if CF set for some <BR>!! BIOSes) <BR>!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! <BR><BR><BR>ok_load_setup: <BR><BR>! 取得磁盘驱动器参数，特别是每磁道扇区数(nr of sectors/track); <BR><BR>#if 0 <BR><BR>! bde - Phoenix BIOS手册中提到功能0x08只对硬盘起作用。 <BR>! 但它对于我的一个BIOS(1987 Award)不起作用。 <BR>! 不检查错误码是致命的错误。 <BR><BR>xor dl,dl <BR>mov ah,#0x08 ! AH=8用于取得驱动器参数; <BR>int 0x13 <BR>xor ch,ch <BR><BR>!!!!!!!!!!!!!!!!!!!!!!!!!!! <BR>!! INT 13 - DISK - GET DRIVE PARAMETERS (PC,XT286,CONV,PS,ESDI,SCSI) <BR>!! AH = 08h <BR>!! DL = drive (bit 7 set for hard disk) <BR>!!Return: CF set on error <BR>!! AH = status (07h) (see #00234) <BR>!! CF clear if successful <BR>!! AH = 00h <BR>!! AL = 00h on at least some BIOSes <BR>!! BL = drive type (AT/PS2 floppies only) (see #00242) <BR>!! CH = low eight bits of maximum cylinder number <BR>!! CL = maximum sector number (bits 5-0) <BR>!! high two bits of maximum cylinder number (bits 7-6) <BR>!! DH = maximum head number <BR>!! DL = number of drives <BR>!! ES:DI -&gt; drive parameter table (floppies only) <BR>!!!!!!!!!!!!!!!!!!!!!!!!!!!! <BR><BR>#else <BR><BR>! 好象没有BIOS调用可取得扇区数。如果扇区36可以读就推测是36个扇区， <BR>! 如果扇区18可读就推测是18个扇区，如果扇区15可读就推测是15个扇区， <BR>! 否则推测是9. [36, 18, 15, 9] <BR><BR>mov si,#disksizes ! ds:si-&gt;要测试扇区数大小的表; <BR><BR>probe_loop: <BR>lodsb !! ds:si所指的字节 =&gt;al, si=si+1; <BR>cbw ! 扩展为字(word); <BR>mov sectors, ax ! 第一个值是36，最后一个是9; <BR>cmp si,#disksizes+4 <BR>jae got_sectors ! 如果所有测试都失败了，就试9; <BR>xchg ax,cx ! cx = 磁道和扇区(第一次是36=0x0024); <BR>xor dx,dx ! 驱动器0，磁头0; <BR>xor bl,bl !! 设置缓冲区es:bx = 0x9000:0x0a00(578.5k); <BR>mov bh,setup_sects !! setup_sects = 4 (共2k); <BR>inc bh <BR>shl bh,#1 ! setup后面的地址(es=cs); <BR>mov ax,#0x0201 ! 功能2(读)，1个扇区; <BR>int 0x13 <BR>jc probe_loop ! 如果不对，就试用下一个值; <BR><BR>#endif <BR><BR>got_sectors: <BR><BR>! 恢复es <BR><BR>mov ax,#INITSEG <BR>mov es,ax ! es = 0x9000; <BR><BR>! 打印一些无用的信息(换行后，显示\Loading\) <BR><BR>mov ah,#0x03 ! 读光标位置; <BR>xor bh,bh <BR>int 0x10 <BR><BR>mov cx,#9 <BR>mov bx,#0x0007 ! 页0，属性7 (normal); <BR>mov bp,#msg1 <BR>mov ax,#0x1301 ! 写字符串，移动光标; <BR>int 0x10 <BR><BR>! ok, 我们已经显示出了信息，现在 <BR>! 我们要加载系统了(到0x10000处)(64k处) <BR><BR>mov ax,#SYSSEG <BR>mov es,ax ! es=0x01000的段; <BR>call read_it !! 读system，es为输入参数； <BR>call kill_motor !! 关闭驱动器马达； <BR>call print_nl !! 打印回车换行； <BR><BR>! 这以后，我们来检查要使用哪个根设备(root-device)。如果已指定了设备(!=0) <BR>! 则不做任何事而使用给定的设备。否则的话，使用/dev/fd0H2880 (2,32)或/dev/PS0 (2,28) <BR>! 或者是/dev/at0 (2,8)之一，这取决于我们假设我们知道的扇区数而定。 <BR>!! |__ ps0?? (x,y)--表示主、次设备号？ <BR><BR>seg cs <BR>mov ax,root_dev <BR>or ax,ax <BR>jne root_defined <BR>seg cs <BR>mov bx,sectors !! sectors = 每磁道扇区数； <BR>mov ax,#0x0208 ! /dev/ps0 - 1.2Mb; <BR>cmp bx,#15 <BR>je root_defined <BR>mov al,#0x1c ! /dev/PS0 - 1.44Mb !! 0x1C = 28; <BR>cmp bx,#18 <BR>je root_defined <BR>mov al,0x20 ! /dev/fd0H2880 - 2.88Mb; <BR>cmp bx,#36 <BR>je root_defined <BR>mov al,#0 ! /dev/fd0 - autodetect; <BR>root_defined: <BR>seg cs <BR>mov root_dev,ax !! 其中保存由设备的主、次设备号； <BR><BR>! 这以后(所有程序都加载了)，我们就跳转至 <BR>! 被直接加载到boot块后面的setup程序去： <BR><BR>jmpi 0,SETUPSEG !! 跳转到0x9020:0000(setup程序的开始位置); <BR><BR><BR>! 这段程序将系统(system)加载到0x10000(64k)处, <BR>! 注意不要跨越64kb边界。我们试图以最快的速度 <BR>! 来加载，只要可能就整个磁道一起读入。 <BR>! <BR>! 输入(in): es - 开始地址段(通常是0x1000) <BR>! <BR>sread: .word 0 ! 当前磁道已读的扇区数； <BR>head: .word 0 ! 当前磁头； <BR>track: .word 0 ! 当前磁道； <BR><BR>read_it: <BR>mov al,setup_sects <BR>inc al <BR>mov sread,al !! 当前sread=5； <BR>mov ax,es !! es=0x1000； <BR>test ax,#0x0fff !! (ax AND 0x0fff, if ax=0x1000 then zero-flag=1 )； <BR>die: jne die ! es 必须在64kB的边界； <BR>xor bx,bx ! bx 是段内的开始地址； <BR>rp_read: <BR>#ifdef __BIG_KERNEL__ <BR>#define CALL_HIGHLOAD_KLUDGE .word 0x1eff, 0x220 ! 调用 far * bootsect_kludge <BR>! 注意: as86不能汇编这； <BR>CALL_HIGHLOAD_KLUDGE ! 这是在setup.S中的程序； <BR>#else <BR>mov ax,es <BR>sub ax,#SYSSEG ! 当前es段值减system加载时的启始段值(0x1000)； <BR>#endif <BR>cmp ax,syssize ! 我们是否已经都加载了？(ax=0x7f00 ?)； <BR>jbe ok1_read !! if ax &lt;= syssize then 继续读； <BR>ret !! 全都加载完了，返回！ <BR>ok1_read: <BR>mov ax,sectors !! sectors=每磁道扇区数； <BR>sub ax,sread !! 减去当前磁道已读扇区数，al=当前磁道未读的扇区数(ah=0)； <BR>mov cx,ax <BR>shl cx,#9 !! 乘512，cx = 当前磁道未读的字节数； <BR>add cx,bx !! 加上段内偏移值，es:bx为当前读入的数据缓冲区地址； <BR>jnc ok2_read !! 如果没有超过64K则继续读； <BR>je ok2_read !! 如果正好64K也继续读； <BR>xor ax,ax <BR>sub ax,bx <BR>shr ax,#9 <BR>ok2_read: <BR>call read_track !! es:bx -&gt;缓冲区，al=要读的扇区数，也即当前磁道未读的扇区数； <BR>mov cx,ax !! ax仍为调用read_track之前的值，即为读入的扇区数； <BR>add ax,sread !! ax = 当前磁道已读的扇区数； <BR>cmp ax,sectors !! 已经读完当前磁道上的扇区了吗？ <BR>jne ok3_read !! 没有，则跳转； <BR>mov ax,#1 <BR>sub ax,head !! 当前是磁头1吗？ <BR>jne ok4_read !! 不是(是磁头0)则跳转(此时ax=1)； <BR>inc track !! 当前是磁头1，则读下一磁道(当前磁道加1)； <BR>ok4_read: <BR>mov head,ax !! 保存当前磁头号； <BR>xor ax,ax !! 本磁道已读扇区数清零； <BR>ok3_read: <BR>mov sread,ax !! 存本磁道已读扇区数； <BR>shl cx,#9 !! 刚才一次读操作读入的扇区数 * 512； <BR>add bx,cx !! 调整数据缓冲区的起始指针； <BR>jnc rp_read !! 如果该指针没有超过64K的段内最大偏移量，则跳转继续读操作； <BR>mov ax,es !! 如果超过了，则将段地址加0x1000(下一个64K段); <BR>add ah,#0x10 <BR>mov es,ax <BR>xor bx,bx !! 缓冲区地址段内偏移量置零； <BR>jmp rp_read !! 继续读操作； <BR><BR><BR><BR>read_track: <BR>pusha !! 将寄存器ax,cx,dx,bx,sp,bp,si,di压入堆栈； <BR>pusha <BR>mov ax,#0xe2e ! loading... message 2e = . !! 显示一个\.\ <BR>mov bx,#7 <BR>int 0x10 <BR>popa <BR><BR>mov dx,track !! track = 当前磁道； <BR>mov cx,sread <BR>inc cx !! cl = 扇区号，要读的起始扇区； <BR>mov ch,dl !! ch = 磁道号的低8位； <BR>mov dx,head !! <BR>mov dh,dl !! dh = 当前磁头号； <BR>and dx,#0x0100 !! dl = 驱动器号(0)； <BR>mov ah,#2 !! 功能2(读)，es:bx指向读数据缓冲区； <BR><BR>push dx ! 为出错转储保存寄存器的值到堆栈上； <BR>push cx <BR>push bx <BR>push ax <BR><BR>int 0x13 <BR>jc bad_rt !! 如果出错，则跳转； <BR>add sp, #8 !! 清(放弃)堆栈上刚推入的4个寄存器值； <BR>popa <BR>ret <BR><BR>bad_rt: push ax ! 保存出错码； <BR>call print_all ! ah = error, al = read； <BR><BR><BR>xor ah,ah <BR>xor dl,dl <BR>int 0x13 <BR><BR><BR>add sp,#10 <BR>popa <BR>jmp read_track <BR><BR>/* <BR>* print_all是用于调试的。 <BR>* 它将打印出所有寄存器的值。所作的假设是 <BR>* 从一个子程序中调用的，并有如下所示的堆栈帧结构 <BR>* dx <BR>* cx <BR>* bx <BR>* ax <BR>* error <BR>* ret &lt;- sp <BR>* <BR>*/ <BR><BR>print_all: <BR>mov cx,#5 ! 出错码 + 4个寄存器 <BR>mov bp,sp <BR><BR>print_loop: <BR>push cx ! 保存剩余的计数值 <BR>call print_nl ! 为了增强阅读性，打印换行 <BR><BR>cmp cl, #5 <BR>jae no_reg ! 看看是否需要寄存器的名称 <BR><BR>mov ax,#0xe05 + \A - l <BR>sub al,cl <BR>int 0x10 <BR><BR>mov al,#\X <BR>int 0x10 <BR><BR>mov al,#\: <BR>int 0x10 <BR><BR>no_reg: <BR>add bp,#2 ! 下一个寄存器 <BR>call print_hex ! 打印值 <BR>pop cx <BR>loop print_loop <BR>ret <BR><BR>print_nl: !! 打印回车换行。 <BR>mov ax,#0xe0d ! CR <BR>int 0x10 <BR>mov al,#0xa ! LF <BR>int 0x10 <BR>ret <BR><BR>/* <BR>* print_hex是用于调试目的的，打印出 <BR>* ss:bp所指向的十六进制数。 <BR>* !! 例如，十六进制数是0x4321时，则al分别等于\4\,\3\,\2\,\1\调用中断打印出来 4321 <BR>*/ <BR><BR>print_hex: <BR>mov cx, #4 ! 4个十六进制数字 <BR>mov dx, (bp) ! 将(bp)所指的值放入dx中 <BR>print_digit: <BR>rol dx, #4 ! 循环以使低4比特用上 !! 取dx的高4比特移到低4比特处。 <BR>mov ax, #0xe0f ! ah = 请求的功能值，al = 半字节(4个比特)掩码。 <BR>and al, dl !! 取dl的低4比特值。 <BR>add al, #0x90 ! 将al转换为ASCII十六进制码(4个指令) <BR>daa !! 十进制调整 <BR>adc al, #0x40 !! (adc dest, src ==&gt; dest := dest + src + c ) <BR>daa <BR>int 0x10 <BR>loop print_digit <BR>ret <BR><BR><BR>/* <BR>* 这个过程(子程序)关闭软驱的马达，这样 <BR>* 我们进入内核后它的状态就是已知的，以后也就 <BR>* 不用担心它了。 <BR>*/ <BR>kill_motor: <BR>push dx <BR>mov dx,#0x3f2 <BR>xor al,al <BR>outb <BR>pop dx <BR>ret <BR><BR>!! 数据区 <BR>sectors: <BR>.word 0 !! 当前每磁道扇区数。(36||18||15||9) <BR><BR>disksizes: !! 每磁道扇区数表 <BR>.byte 36, 18, 15, 9 <BR><BR>msg1: <BR>.byte 13, 10 <BR>.ascii \"Loading\" <BR><BR>.org 497 !! 从boot程序的二进制文件的497字节开始 <BR>setup_sects: <BR>.byte SETUPSECS <BR>root_flags: <BR>.word CONFIG_ROOT_RDONLY <BR>syssize: <BR>.word SYSSIZE <BR>swap_dev: <BR>.word SWAP_DEV <BR>ram_size: <BR>.word RAMDISK <BR>vid_mode: <BR>.word SVGA_MODE <BR>root_dev: <BR>.word ROOT_DEV <BR>boot_flag: !! 分区启动标志 <BR>.word 0xAA55 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/501.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:59 <a href="http://www.cnitblog.com/drizztzou/articles/501.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux内核分析方法谈</title><link>http://www.cnitblog.com/drizztzou/articles/496.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:49:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/496.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/496.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/496.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/496.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/496.html</trackback:ping><description><![CDATA[&nbsp;&nbsp; Linux的最大的好处之一就是它的源码公开。同时，公开的核心源码也吸引着无数的电脑爱好者和程序员；他们把解读和分析Linux的核心源码作为自己的最大兴趣，把修改Linux源码和改造Linux系统作为自己对计算机技术追求的最大目标。 <BR>　　Linux内核源码是很具吸引力的，特别是当你弄懂了一个分析了好久都没搞懂的问题；或者是被你修改过了的内核，顺利通过编译，一切运行正常的时候。那种成就感真是油然而生！而且，对内核的分析，除了出自对技术的狂热追求之外，这种令人生畏的劳动所带来的回报也是非常令人着迷的，这也正是它拥有众多追随者的主要原因： <BR><BR>首先，你可以从中学到很多的计算机的底层知识，如后面将讲到的系统的引导和硬件提供的中断机制等；其它，象虚拟存储的实现机制，多任务机制，系统保护机制等等，这些都是非都源码不能体会的。 <BR><BR>同时，你还将从操作系统的整体结构中，体会整体设计在软件设计中的份量和作用，以及一些宏观设计的方法和技巧：Linux的内核为上层应用提供一个与具体硬件不相关的平台；同时在内核内部，它又把代码分为与体系结构和硬件相关的部分，和可移植的部分；再例如，Linux虽然不是微内核的，但他把大部分的设备驱动处理成相对独立的内核模块，这样减小了内核运行的开销，增强了内核代码的模块独立性。 <BR><BR>而且你还能从对内核源码的分析中，体会到它在解决某个具体细节问题时，方法的巧妙：如后面将分析到了的Linux通过Botoom_half机制来加快系统对中断的处理。 <BR><BR><BR>最重要的是：在源码的分析过程中，你将会被一点一点地、潜移默化地专业化。一个专业的程序员，总是把代码的清晰性，兼容性，可移植性放在很重要的位置。他们总是通过定义大量的宏，来增强代码的清晰度和可读性，而又不增加编译后的代码长度和代码的运行效率；他们总是在编码的同时，就考虑到了以后的代码维护和升级。 甚至，只要分析百分之一的代码后，你就会深刻地体会到，什么样的代码才是一个专业的程序员写的，什么样的代码是一个业余爱好者写的。而这一点是任何没有真正分析过标准代码的人都无法体会到的。 <BR>　　然而，由于内核代码的冗长，和内核体系结构的庞杂，所以分析内核也是一个很艰难，很需要毅力的事；在缺乏指导和交流的情况下，尤其如此。只有方法正确，才能事半功倍。正是基于这种考虑，作者希望通过此文能给大家一些借鉴和启迪。 <BR><BR>　　由于本人所进行的分析都是基于2.2.5版本的内核；所以，如果没有特别说明，以下分析都是基于i386单处理器的2.2.5版本的Linux内核。所有源文件均是相对于目录/usr/src/linux的。 <BR><BR>方法之一：从何入手 <BR><BR>　　要分析Linux内核源码，首先必须找到各个模块的位置，也即要弄懂源码的文件组织形式。虽然对于有经验的高手而言，这个不是很难；但对于很多初级的Linux爱好者，和那些对源码分析很有兴趣但接触不多的人来说，这还是很有必要的。 <BR><BR>　　1、Linux核心源程序通常都安装在/usr/src/linux下，而且它有一个非常简单的编号约定：任何偶数的核心（的二个数为偶数，例如2.0.30）都是一个稳定地发行的核心，而任何奇数的核心（例如2.1.42）都是一个开发中的核心。 <BR><BR>　　2、核心源程序的文件按树形结构进行组织，在源程序树的最上层，即目录/usr/src/linux下有这样一些目录和文件： <BR><BR>◆ COPYING: GPL版权申明。对具有GPL版权的源代码改动而形成的程序，或使用GPL工具产生的程序，具有使用GPL发表的义务，如公开源代码； <BR><BR>◆ CREDITS: 光荣榜。对Linux做出过很大贡献的一些人的信息； <BR><BR>◆ MAINTAINERS: 维护人员列表，对当前版本的内核各部分都有谁负责； <BR><BR>◆ Makefile: 第一个Makefile文件。用来组织内核的各模块，记录了个模块间的相互这间的联系和依托关系，编译时使用；仔细阅读各子目录下的Makefile文件对弄清各个文件这间的联系和依托关系很有帮助； <BR><BR>◆ ReadMe: 核心及其编译配置方法简单介绍； <BR><BR>◆ Rules.make: 各种Makefilemake所使用的一些共同规则； <BR><BR>◆ REPORTING-BUGS:有关报告Bug 的一些内容； <BR><BR>● Arch/ ：arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系结构，例如i386就是关于intel cpu及与之相兼容体系结构的子目录。PC机一般都基于此目录； <BR><BR>● Include/: include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在 include/linux子目录下，与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录； <BR><BR>● Init/： 这个目录包含核心的初始化代码(注：不是系统的引导代码)，包含两个文件main.c和Version.c，这是研究核心如何工作的好的起点之一。 <BR><BR>● Mm/：这个目录包括所有独立于 cpu 体系结构的内存管理代码，如页式存储管理内存的分配和释放等；而和体系结构相关的内存管理代码则位于arch/*/mm/，例如arch/i386/mm/Fault.c； <BR><BR>● Kernel/：主要的核心代码，此目录下的文件实现了大多数linux系统的内核函数，其中最重要的文件当属sched.c；同样，和体系结构相关的代码在arch/*/kernel中； <BR><BR>● Drivers/： 放置系统所有的设备驱动程序；每种驱动程序又各占用一个子目录：如，/block 下为块设备驱动程序，比如ide（ide.c）。如果你希望查看所有可能包含文件系统的设备是如何初始化的，你可以看drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘，也初始化网络，因为安装nfs文件系统的时候需要网络； <BR><BR>● Documentation/: 文档目录,没有内核代码，只是一套有用的文档，可惜都是English的，看看应该有用的哦； <BR><BR>● Fs/: 所有的文件系统代码和各种类型的文件操作代码，它的每一个子目录支持一个文件系统, 例如fat和ext2； <BR><BR>● Ipc/: 这个目录包含核心的进程间通讯的代码； <BR><BR>● Lib/: 放置核心的库代码； <BR><BR>● Net/: 核心与网络相关的代码； <BR><BR>● Modules/: 模块文件目录，是个空目录，用于存放编译时产生的模块目标文件； <BR><BR>● Scripts/: 描述文件，脚本，用于对核心的配置； <BR><BR>　　一般，在每个子目录下，都有一个 Makefile 和一个Readme 文件，仔细阅读这两个文件，对内核源码的理解很有用。 <BR><BR>　　对Linux内核源码的分析，有几个很好的入口点：一个就是系统的引导和初始化，即从机器加电到系统核心的运行；另外一个就是系统调用，系统调用是用户程序或操作调用核心所提供的功能的接口。对于那些对硬件比较熟悉的爱好者，从系统的引导入手进行分析，可能来的容易一些；而从系统调用下口，则可能更合适于那些在dos或Uinx、Linux下有过C编程经验的高手。这两点，在后面还将介绍到。 <BR>方法之二：以程序流程为线索，一线串珠 <BR><BR>　　从表面上看，Linux的源码就象一团扎乱无章的乱麻，其实它是一个组织得有条有理的蛛网。要把整个结构分析清楚，除了找出线头，还得理顺各个部分之间的关系，有条不紊的一点一点的分析。 <BR><BR>　　所谓以程序流程为线索、一线串珠，就是指根据程序的执行流程，把程序执行过程所涉及到的代码分析清楚。这种方法最典型的应用有两个：一是系统的初始化过程；二是应用程序的执行流程：从程序的装载，到运行，一直到程序的退出。 <BR><BR>　　为了简便起见，遵从循序渐进的原理，现就系统的初始化过程来具体的介绍这种方法。系统的初始化流程包括：系统引导，实模式下的初始化，保护模式下的初始化共三个部分。下面将一一介绍。 <BR><BR>　inux系统的常见引导方式有两种：Lilo引导和Loadin引导；同时linux内核也自带了一个bootsect-loader。由于它只能实现linux的引导，不像前两个那样具有很大的灵活性（lilo可实现多重引导、loadin可在dos下引导linux）,所以在普通应用场合实际上很少使用bootsect-loader。当然，bootsect-loader也具有它自己的优点：短小没有多余的代码、附带在内核源码中、是内核源码的有机组成部分，等等。 <BR><BR>　　bootsect-loader在内和源码中对应的程序是 /Arch/i386/boot/bootsect.S 。下面将主要是针对此文件进行的分析。 <BR>几个相关文件： <BR><BR>&lt;1&gt; /Arch/i386/boot/bootsect.S <BR><BR>&lt;2&gt; /include/linux/config.h <BR><BR>&lt;3&gt; /include/asm/boot.h <BR><BR>&lt;4&gt; /include/linux/autoconf.h <BR><BR>引导过程分析： <BR><BR>对于Intel x86 PC , 开启电源后, 机器就会开始执行ROM BIOS的一系列系统测试动作，包括检查RAM，keyboard，显示器，软硬磁盘等等。执行完bios的系统测试之后，紧接着控制权会转移给ROM中的启动程序(ROM bootstrap routine)；这个程序会将磁盘上的第0轨第0扇区（叫boot sector或MBR ，系统的引导程序就放在此处）读入内存中，并放到自0x07C0:0x0000开始的512个字节处；然后处理机将跳到此处开始执行这一引导程序；也即装入MBR中的引导程序后， CS:IP = 0x07C0:0x0000 。加电后处理机运行在与8086相兼容的实模式下。 <BR><BR>如果要用bootsect-loader进行系统引导，则必须把bootsect.S编译连接后对应的二进制代码置于MBR； 当ROM BIOS 把bootsect.S编译连接后对应的二进制代码装入内存后，机器的控制权就完全转交给bootsect； 也就是说，bootsect将是第一个被读入内存中并执行的程序。 <BR><BR>Bootsect接管机器控制权后，将依次进行以下一些动作： <BR><BR>1．首先，bootsect将它"自己"(自位置0x07C0:0x0000开始的512个字节)从被ROM BIOS载入的地址0x07C0:0x0000处搬到0x9000:0000处; 这一任务由bootsect.S的前十条指令完成；第十一条指令“jmpi go,INITSEG”则把机器跳转到“新”的bootsect的“jmpi go,INITSEG”后的那条指令“go: mov di,#0x4000-12”；之后，继续执行bootsect的剩下的代码；在bootsect.S中定义了几个常量： <BR><BR>BOOTSEG = 0x07C0 bios 载入 MBR的约定位置的段址； <BR><BR>INITSEG = 0x9000 bootsect.S的前十条指令将自己搬到此处(段址) <BR><BR>SETUPSEG =0x9020 装入Setup.S的段址 <BR><BR>SYSSEG =0x1000 系统区段址 <BR><BR>对于这些常量可参见/include/asm/boot.h中的定义；这些常量在下面的分析中将会经常用到； <BR><BR>2．以0x9000:0x4000-12为栈底，建立自己的栈区；其中0x9000:0x4000-12到0x9000:0x4000的一十二个字节预留作磁盘参数表区； <BR><BR>3．在0x9000:0x4000-12到0x9000:0x4000的一十二个预留字节中建立新的磁盘参数表，之所以叫“新”的磁盘参数表，是相对于bios建立的磁盘参数表而言的。由于设计者考虑到有些老的bios不能准确地识别磁盘“每个磁道的扇区数”，从而导致bios建立的磁盘参数表妨碍磁盘的最高性能发挥，所以，设计者就在bios建立的磁盘参数表的基础上通过枚举法测试，试图建立准确的“新”的磁盘参数表(这是在后继步骤中完成的)；并把参数表的位置由原来的0x0000:0x0078搬到0x9000:0x4000-12；且修改老的磁盘参数表区使之指向新的磁盘参数表； <BR><BR>4．接下来就到了load_setup子过程；它调用0x13中断的第2号服务；把第0道第2扇区开始的连续的setup_sects (为常量4)个扇区读到紧邻bootsect的内存区；，即0x9000:0x0200开始的2048个字节；而这四个扇区的内容即是/arch/i386/boot/setup.S编译连接后对应的二进制代码； 也就是说，如果要用bootsect-loader进行系统引导，不仅必须把bootsect.S编译连接后对应的二进制代码置于MBR，而且还得把setup.S编译连接后对应的二进制代码置于紧跟MBR后的连续的四个扇区中；当然，由于setup.S对应的可执行码是由bootsect装载的，所以，在我们的这个项目中可以通过修改bootsect来根据需要随意地放置setup.S对应的可执行码； <BR><BR>5．load_setup子过程的唯一出口是probe_loop子过程；该过程通过枚举法测试磁盘“每个磁道的扇区数”; <BR><BR>6.接下来几个子过程比较清晰易懂:打印我们熟悉的“Loading”；读入系统到0x1000:0x0000; 关掉软驱马达；根据的5步测出的“每个磁道的扇区数”确定磁盘类型；最后跳转到0x9000:0x0200,即setup.S对应的可执行码的入口，将机器控制权转交setup.S;整个bootsect代码运行完毕； <BR>　出于简便考虑，在此分析中，我忽略了对大内核的处理的分析，因为对大内核的处理，只是此引导过程中的一个很小的部分，并不影响对整体的把握。完成了系统的引导后，系统将进入到初始化处理阶段。系统的初始化分为实模式和保护模式两部分。 <BR>II、实模式下的初始化 <BR><BR>　　实模式下的初始化,主要是指从内核引导成功后，到进入保护模式之前系统所做的一些处理。在内核源码中对应的程序是 /Arch/i386/boot/setup.S；以下部分主要是针对此文件进行的分析。这部分的分析主要是要弄懂它的处理流程和INITSEG(9000:0000)段参数表的建立，此参数表包含了很多硬件参数，这些都是以后进行保护模式下初始化，以及核心建立的基础。 <BR><BR>1. 几个其它相关文件： <BR><BR>&lt;1&gt; /Arch/i386/boot/bootsect.S <BR><BR>&lt;2&gt; /include/linux/config.h <BR><BR>&lt;3&gt; /include/asm/boot.h <BR><BR>&lt;4&gt; /include/ asm/segment.h <BR><BR>&lt;5&gt; /include/linux/version.h <BR><BR>&lt;6&gt; /include/linux/compile.h <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/496.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:49 <a href="http://www.cnitblog.com/drizztzou/articles/496.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux 内存管理子系统导读</title><link>http://www.cnitblog.com/drizztzou/articles/486.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:28:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/486.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/486.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/486.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/486.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/486.html</trackback:ping><description><![CDATA[本文主要针对2.4的kernel。 <BR><BR>关于本文的组织： <BR><BR>我的目标是‘导读’，提供linux内存管理子系统的整体概念，同时给出进一步深入研究某个部分时的辅助信息(包括代码组织,文件和主要函数的意义和一些参考文档)。之所以采取这种方式，是因为我本人在阅读代码的过程中，深感“读懂一段代码容易，把握整体思想却极不容易”。而且，在我写一些内核代码时，也觉得很多情况下，不一定非得很具体地理解所有内核代码，往往了解它的接口和整体工作原理就够了。当然，我个人的能力有限，时间也很不够，很多东西也是近期迫于讲座压力临时学的：），内容难免偏颇甚至错误，欢迎大家指正。 <BR><BR><BR><BR>存储层次结构和x86存储管理硬件（MMU） <BR><BR>这里假定大家对虚拟存储，段页机制有一定的了解。主要强调一些很重要的或者容易误解的概念。 <BR><BR><BR>存储层次 <BR><BR>高速缓存(cache) --〉 主存(main memory) ---〉 磁盘(disk) <BR><BR>理解存储层次结构的根源：CPU速度和存储器速度的差距。 <BR><BR>层次结构可行的原因：局部性原理。 <BR><BR>LINUX的任务: <BR><BR><BR>减小footprint，提高cache命中率，充分利用局部性。 <BR><BR><BR>实现虚拟存储以满足进程的需求，有效地管理内存分配，力求最合理地利用有限的资源。 <BR><BR>参考文档： <BR><BR>《too little,too small》by Rik Van Riel, Nov. 27,2000. <BR><BR>以及所有的体系结构教材：） <BR><BR><BR>MMU的作用 <BR><BR>辅助操作系统进行内存管理，提供虚实地址转换等硬件支持。 <BR><BR><BR>x86的地址 <BR><BR>逻辑地址： 出现在机器指令中，用来制定操作数的地址。段：偏移 <BR><BR>线性地址：逻辑地址经过分段单元处理后得到线性地址，这是一个32位的无符号整数，可用于定位4G个存储单元。 <BR><BR>物理地址：线性地址经过页表查找后得出物理地址，这个地址将被送到地址总线上指示所要访问的物理内存单元。 <BR><BR><BR>LINUX: 尽量避免使用段功能以提高可移植性。如通过使用基址为0的段，使逻辑地址==线性地址。 <BR><BR><BR>x86的段 <BR><BR>保护模式下的段：选择子+描述符。不仅仅是一个基地址的原因是为了提供更多的信息：保护、长度限制、类型等。描述符存放在一张表中(GDT或LDT)，选择子可以认为是表的索引。段寄存器中存放的是选择子，在段寄存器装入的同时，描述符中的数据被装入一个不可见的寄存器以便cpu快速访问。（图）P40 <BR><BR>专用寄存器：GDTR(包含全局描述附表的首地址),LDTR（当前进程的段描述附表首地址）,TSR（指向当前进程的任务状态段） <BR><BR><BR>LINUX使用的段： <BR><BR>__KERNEL_CS： 内核代码段。范围 0-4G。可读、执行。DPL=0。 <BR><BR>__KERNEL_DS：内核代码段。范围 0-4G。可读、写。DPL=0。 <BR><BR>__USER_CS：内核代码段。范围 0-4G。可读、执行。DPL=3。 <BR><BR>__USER_DS：内核代码段。范围 0-4G。可读、写。DPL=3。 <BR><BR>TSS(任务状态段)：存储进程的硬件上下文，进程切换时使用。（因为x86硬件对TSS有一定支持，所有有这个特殊的段和相应的专用寄存器。） <BR><BR>default_ldt：理论上每个进程都可以同时使用很多段，这些段可以存储在自己的ldt段中，但实际linux极少利用x86的这些功能，多数情况下所有进程共享这个段，它只包含一个空描述符。 <BR><BR>还有一些特殊的段用在电源管理等代码中。 <BR><BR>（在2.2以前，每个进程的ldt和TSS段都存在GDT中，而GDT最多只能有8192项，因此整个系统的进程总数被限制在4090左右。2。4里不再把它们存在GDT中，从而取消了这个限制。） <BR><BR>__USER_CS和__USER_DS段都是被所有在用户态下的进程共享的。注意不要把这个共享和进程空间的共享混淆：虽然大家使用同一个段，但通过使用不同的页表由分页机制保证了进程空间仍然是独立的。 <BR><BR><BR><BR>x86的分页机制 <BR><BR>x86硬件支持两级页表，奔腾pro以上的型号还支持Physical address Extension Mode和三级页表。所谓的硬件支持包括一些特殊寄存器(cr0-cr4)、以及CPU能够识别页表项中的一些标志位并根据访问情况做出反应等等。如读写Present位为0的页或者写Read/Write位为0的页将引起CPU发出page fault异常，访问完页面后自动设置accessed位等。 <BR><BR><BR>linux采用的是一个体系结构无关的三级页表模型（如图），使用一系列的宏来掩盖各种平台的细节。例如，通过把PMD看作只有一项的表并存储在pgd表项中（通常pgd表项中存放的应该是pmd表的首地址），页表的中间目录(pmd)被巧妙地‘折叠’到页表的全局目录(pgd)，从而适应了二级页表硬件。 <BR><BR><BR>6. TLB <BR><BR>TLB全称是Translation Look-aside Buffer,用来加速页表查找。这里关键的一点是：如果操作系统更改了页表内容，它必须相应的刷新TLB以使CPU不误用过时的表项。 <BR><BR><BR>7. Cache <BR><BR>Cache 基本上是对程序员透明的，但是不同的使用方法可以导致大不相同的性能。linux有许多关键的地方对代码做了精心优化，其中很多就是为了减少对cache不必要的污染。如把只有出错情况下用到的代码放到.fixup section，把频繁同时使用的数据集中到一个cache行（如struct task_struct），减少一些函数的footprint，在slab分配器里头的slab coloring等。 <BR><BR>另外，我们也必须知道什么时候cache要无效：新map/remap一页到某个地址、页面换出、页保护改变、进程切换等，也即当cache对应的那个地址的内容或含义有所变化时。当然，很多情况下不需要无效整个cache，只需要无效某个地址或地址范围即可。实际上， <BR><BR>intel在这方面做得非常好用，cache的一致性完全由硬件维护。 <BR><BR><BR><BR>关于x86处理器更多信息，请参照其手册：Volume 3: Architecture and Programming Manual,可以从<A style="COLOR: #003793" href="ftp://download.intel.com/design/pentium/MANUALS/24143004.pdf" target=_blank>ftp://download.intel.com/design/pentium/MANUALS/24143004.pdf</A>获得 <BR><BR><BR>8． Linux 相关实现 <BR><BR>这一部分的代码和体系结构紧密相关，因此大多位于arch子目录下，而且大量以宏定义和inline函数形式存在于头文件中。以i386平台为例，主要的文件包括： <BR><BR><BR>page.h <BR><BR>页大小、页掩码定义。PAGE_SIZE,PAGE_SHIFT和PAGE_MASK。 <BR><BR>对页的操作，如清除页内容clear_page、拷贝页copy_page、页对齐page_align <BR><BR>还有内核虚地址的起始点：著名的PAGE_OFFSET:)和相关的内核中虚实地址转换的宏__pa和__va.。 <BR><BR>virt_to_page从一个内核虚地址得到该页的描述结构struct page *.我们知道，所有物理内存都由一个memmap数组来描述。这个宏就是计算给定地址的物理页在这个数组中的位置。另外这个文件也定义了一个简单的宏检查一个页是不是合法：VALID_PAGE(page)。如果page离memmap数组的开始太远以至于超过了最大物理页面应有的距离则是不合法的。 <BR><BR>比较奇怪的是页表项的定义也放在这里。pgd_t,pmd_t,pte_t和存取它们值的宏xxx_val <BR><BR><BR><BR>pgtable.h pgtable-2level.h pgtable-3level.h <BR><BR>顾名思义，这些文件就是处理页表的，它们提供了一系列的宏来操作页表。pgtable-2level.h和pgtable-2level.h则分别对应x86二级、三级页表的需求。首先当然是表示每级页表有多少项的定义不同了。而且在PAE模式下，地址超过32位，页表项pte_t用64位来表示(pmd_t,pgd_t不需要变)，一些对整个页表项的操作也就不同。共有如下几类： <BR><BR>[pte/pmd/pgd]_ERROR 出措时要打印项的取值，64位和32位当然不一样。 <BR><BR>set_[pte/pmd/pgd] 设置表项值 <BR><BR>pte_same 比较 pte_page 从pte得出所在的memmap位置 <BR><BR>pte_none 是否为空。 <BR><BR>__mk_pte 构造pte <BR><BR>pgtable.h的宏太多，不再一一解释。实际上也比较直观，通常从名字就可以看出宏的意义来了。pte_xxx宏的参数是pte_t,而ptep_xxx的参数是pte_t *。2.4 kernel在代码的clean up方面还是作了一些努力，不少地方含糊的名字变明确了，有些函数的可读性页变好了。 <BR><BR>pgtable.h里除了页表操作的宏外，还有cache和tlb刷新操作，这也比较合理，因为他们常常是在页表操作时使用。这里的tlb操作是以__开始的，也就是说，内部使用的，真正对外接口在pgalloc.h中（这样分开可能是因为在SMP版本中，tlb的刷新函数和单机版本区别较大，有些不再是内嵌函数和宏了）。 <BR><BR><BR>8.3 pgalloc.h <BR><BR>包括页表项的分配和释放宏/函数,值得注意的是表项高速缓存的使用： <BR><BR>pgd/pmd/pte_quicklist <BR><BR>内核中有许多地方使用类似的技巧来减少对内存分配函数的调用，加速频繁使用的分配。如buffer cache中buffer_head和buffer，vm区域中最近使用的区域。 <BR><BR>还有上面提到的tlb刷新的接口 <BR><BR>8.4 segment.h <BR><BR>定义 __KERNEL_CS[DS] __USER_CS[DS] <BR><BR><BR>参考： <BR><BR>《Understanding the Linux Kernel》的第二章给了一个对linux 的相关实现的简要描述， <BR><BR><BR><BR>物理内存的管理。 <BR><BR>2.4中内存管理有很大的变化。在物理页面管理上实现了基于区的伙伴系统（zone based buddy system）。区(zone)的是根据内存的不同使用类型划分的。对不同区的内存使用单独的伙伴系统(buddy system)管理,而且独立地监控空闲页等。 <BR><BR>(实际上更高一层还有numa支持。Numa(None Uniformed Memory Access)是一种体系结构，其中对系统里的每个处理器来说,不同的内存区域可能有不同的存取时间(一般是由内存和处理器的距离决定)。而一般的机器中内存叫做DRAM，即动态随机存取存储器，对每个单元，CPU用起来是一样快的。NUMA中访问速度相同的一个内存区域称为一个Node，支持这种结构的主要任务就是要尽量减少Node之间的通信，使得每个处理器要用到的数据尽可能放在对它来说最快的Node中。2.4内核中node&amp;#0;相应的数据结构是pg_data_t，每个node拥有自己的memmap数组，把自己的内存分成几个zone，每个zone再用独立的伙伴系统管理物理页面。Numa要对付的问题还有很多，也远没有完善，就不多说了) <BR><BR>一些重要的数据结构粗略地表示如下： <BR><BR><BR><BR><BR>基于区的伙伴系统的设计&amp;#0;物理页面的管理 <BR><BR>内存分配的两大问题是：分配效率、碎片问题。一个好的分配器应该能够快速的满足各种大小的分配要求，同时不能产生大量的碎片浪费空间。伙伴系统是一个常用的比较好的算法。(解释：TODO) <BR><BR>引入区的概念是为了区分内存的不同使用类型（方法？），以便更有效地利用它们。 <BR><BR>2.4有三个区：DMA, Normal, HighMem。前两个在2.2实际上也是由独立的buddy system管理的，但2.2中还没有明确的zone的概念。DMA区在x86体系结构中通常是小于16兆的物理内存区，因为DMA控制器只能使用这一段的内存。而HighMem是物理地址超过某个值(通常是约900M)的高端内存。其他的是Normal区内存。由于linux实现的原因，高地址的内存不能直接被内核使用，如果选择了CONFIG_HIGHMEM选项，内核会使用一种特殊的办法来使用它们。（解释：TODO）。HighMem只用于page cache和用户进程。这样分开之后，我们将可以更有针对性地使用内存，而不至于出现把DMA可用的内存大量给无关的用户进程使用导致驱动程序没法得到足够的DMA内存等情况。此外，每个区都独立地监控本区内存的使用情况，分配时系统会判断从哪个区分配比较合算，综合考虑用户的要求和系统现状。2.4里分配页面时可能会和高层的VM代码交互(分配时根据空闲页面的情况，内核可能从伙伴系统里分配页面，也可能直接把已经分配的页收回&amp;#0;reclaim等)，代码比2.2复杂了不少，要全面地理解它得熟悉整个VM工作的机理。 <BR><BR>整个分配器的主要接口是如下函数(mm.h page_alloc.c)： <BR><BR><BR>struct page * alloc_pages(int gfp_mask, unsigned long order) 根据gftp_mask的要求，从适当的区分配2^order个页面，返回第一个页的描述符。 <BR><BR><BR>#define alloc_page(gfp_mask) alloc_pages(gfp_mask,0) <BR><BR><BR>unsigned long __get_free_pages((int gfp_mask, unsigned long order) 工作同alloc_pages,但返回首地址。 <BR><BR><BR>#define __get_free_page(gfp_mask) __get_free_pages(gfp_mask,0) <BR><BR><BR>get_free_page 分配一个已清零的页面。 <BR><BR><BR>__free_page(s) 和free_page(s)释放页面（一个/多个）前者以页面描述符为参数，后者以页面地址为参数。 <BR><BR><BR>关于Buddy算法，许多教科书上有详细的描述，<UNDERSTANDING the Linux Kernel>第六章对linux的实现有一个很好的介绍。关于zone base buddy更多的信息，可以参见Rik Van Riel 写的" design for a zone based memory allocator"。这个人是目前linuxmm的维护者，权威啦。这篇文章有一点过时了，98年写的，当时还没有HighMem，但思想还是有效的。还有，下面这篇文章分析2.4的实现代码： <BR><BR><A style="COLOR: #003793" href="http://home.earthlink.net/~jknapka/linux-mm/zonealloc.html" target=_blank>http://home.earthlink.net/~jknapka/linux-mm/zonealloc.html</A>。 <BR><BR><BR>2. Slab--连续物理区域管理 <BR><BR>单单分配页面的分配器肯定是不能满足要求的。内核中大量使用各种数据结构，大小从几个字节到几十上百k不等，都取整到2的幂次个页面那是完全不现实的。2.0的内核的解决方法是提供大小为2,4,8,16,...,131056字节的内存区域。需要新的内存区域时，内核从伙伴系统申请页面，把它们划分成一个个区域，取一个来满足需求；如果某个页面中的内存区域都释放了，页面就交回到伙伴系统。这样做的效率不高。有许多地方可以改进： <BR><BR><BR>不同的数据类型用不同的方法分配内存可能提高效率。比如需要初始化的数据结构，释放后可以暂存着，再分配时就不必初始化了。 <BR><BR><BR>内核的函数常常重复地使用同一类型的内存区，缓存最近释放的对象可以加速分配和释放。 <BR><BR><BR>对内存的请求可以按照请求频率来分类，频繁使用的类型使用专门的缓存，很少使用的可以使用类似2.0中的取整到2的幂次的通用缓存。 <BR><BR><BR>使用2的幂次大小的内存区域时高速缓存冲突的概率较大，有可能通过仔细安排内存区域的起始地址来减少高速缓存冲突。 <BR><BR><BR>缓存一定数量的对象可以减少对buddy系统的调用，从而节省时间并减少由此引起的高速缓存污染。 <BR><BR>2.2实现的slab分配器体现了这些改进思想。 <BR><BR>主要数据结构 <BR><BR>接口： <BR><BR>kmem_cache_create/kmem_cache_destory <BR><BR>kmem_cache_grow/kmem_cache_reap 增长/缩减某类缓存的大小 <BR><BR>kmem_cache_alloc/kmem_cache_free 从某类缓存分配/释放一个对象 <BR><BR>kmalloc/kfree 通用缓存的分配、释放函数。 <BR><BR>相关代码(slab.c)。 <BR><BR>相关参考： <BR><BR><A style="COLOR: #003793" href="http://www.lisoleg.net/lisoleg/memory/slab.pdf" target=_blank>http://www.lisoleg.net/lisoleg/memory/slab.pdf</A> ：Slab发明者的论文，必读经典。 <BR><BR><UNDERSTANDING the Linux Kernel>第六章，具体实现的详细清晰的描述。 <BR><BR>AKA2000年的讲座也有一些大虾讲过这个主题，请访问aka主页：www.aka.org.cn <BR><BR><BR>3．vmalloc/vfree &amp;#0;物理地址不连续，虚地址连续的内存管理 <BR><BR>使用kernel页表。文件vmalloc.c，相对简单。 <BR><BR><BR>三、2.4内核的VM(完善中。。。) <BR><BR><BR>进程地址空间管理 <BR><BR>创建，销毁。 <BR><BR>mm_struct, vm_area_struct, mmap/mprotect/munmap <BR><BR>page fault处理，demand page, copy on write <BR><BR><BR>相关文件： <BR><BR>include/linux/mm.h：struct page结构的定义，page的标志位定义以及存取操作宏定义。struct vm_area_struct定义。mm子系统的函数原型说明。 <BR><BR>include/linux/mman.h:和vm_area_struct的操作mmap/mprotect/munmap相关的常量宏定义。 <BR><BR>memory.c：page fault处理，包括COW和demand page等。 <BR><BR>对一个区域的页表相关操作: <BR><BR>zeromap_page_range: 把一个范围内的页全部映射到zero_page <BR><BR>remap_page_range：给定范围的页重新映射到另一块地址空间。 <BR><BR>zap_page_range：把给定范围内的用户页释放掉，页表清零。 <BR><BR>mlock.c： mlock/munlock系统调用。mlock把页面锁定在物理内存中。 <BR><BR>mmap.c:：mmap/munmap/brk系统调用。 <BR><BR>mprotect.c： mprotect系统调用。 <BR><BR>前面三个文件都大量涉及vm_area_struct的操作，有很多相似的xxx_fixup的代码，它们的任务是修补受到影响的区域，保证vm_area_struct 链表正确。 <BR><BR><BR><BR>交换 <BR><BR>目的： <BR><BR><BR>使得进程可以使用更大的地址空间。 <BR><BR><BR>同时容纳更多的进程。 <BR><BR>任务： <BR><BR><BR>选择要换出的页 <BR><BR><BR>决定怎样在交换区中存储页面 <BR><BR><BR>决定什么时候换出 <BR><BR><BR>kswapd内核线程：每10秒激活一次 <BR><BR>任务：当空闲页面低于一定值时，从进程的地址空间、各类cache回收页面 <BR><BR>为什么不能等到内存分配失败再用try_to_free_pages回收页面？原因： <BR><BR><BR>有些内存分配时在中断或异常处理调用，他们不能阻塞 <BR><BR><BR>有时候分配发生在某个关键路径已经获得了一些关键资源的时候，因此它不能启动IO。如果不巧这时所有的路径上的内存分配都是这样，内存就无法释放。 <BR><BR>kreclaimd 从inactive_clean_list回收页面，由__alloc_pages唤醒。 <BR><BR>相关文件： <BR><BR>mm/swap.c kswapd使用的各种参数以及操作页面年龄的函数。 <BR><BR>mm/swap_file.c 交换分区/文件的操作。 <BR><BR>mm/page_io.c 读或写一个交换页。 <BR><BR>mm/swap_state.c swap cache相关操作,加入/删除/查找一个swap cache等。 <BR><BR>mm/vmscan.c 扫描进程的vm_area，试图换出一些页面（kswapd）。 <BR><BR>reclaim_page：从inactive_clean_list回收一个页面，放到free_list <BR><BR>kclaimd被唤醒后重复调用reclaim_page直到每个区的 <BR><BR>zone-&gt;free_pages&gt;= zone-&gt;pages_low <BR><BR>page_lauder：由__alloc_pages和try_to_free_pages等调用。通常是由于freepages + inactive_clean_list的页太少了。功能：把inactive_dirty_list的页面转移到inactive_clean_list，首先把已经被写回文件或者交换区的页面(by bdflush)放到inactive_clean_list，如果freepages确实短缺，唤醒bdflush，再循环一遍把一定数量的dirty页写回。 <BR><BR>关于这几个队列(active_list,inactive_dirty_list,inactive_clean_list)的逻辑，请参照：文档：RFC: design for new VM，可以从lisoleg的文档精华获得。 <BR><BR>, <BR><BR><BR>page cache、buffer cache和swap cache <BR><BR>page cache：读写文件时文件内容的cache，大小为一个页。不一定在磁盘上连续。 <BR><BR>buffer cache：读写磁盘块的时候磁盘块内容的cache，buffer cache的内容对应磁盘上一个连续的区域，一个buffer cache大小可能从512(扇区大小)到一个页。 <BR><BR>swap cache： 是page cache的子集。用于多个进程共享的页面被换出到交换区的情况。 <BR><BR><BR><BR>page cache 和 buffer cache的关系 <BR><BR>本质上是很不同的，buffer cache缓冲磁盘块内容，page cache缓冲文件的一页内容。page cache写回时会使用临时的buffer cache来写磁盘。 <BR><BR><BR>bdflush： 把dirty的buffer cache写回磁盘。通常只当dirty的buffer太多或者需要更多的buffer而内存开始不足时运行。page_lauder也可能唤醒它。 <BR><BR>kupdate： 定时运行，把写回期限已经到了的dirty buffer写回磁盘。 <BR><BR><BR>2.4的改进：page cache和buffer cache耦合得更好了。在2.2里，磁盘文件的读使用page cache，而写绕过page cache，直接使用buffer cache，因此带来了同步的问题：写完之后必须使用update_vm_cache()更新可能有的page cache。2.4中page cache做了比较大的改进，文件可以通过page cache直接写了，page cache优先使用high memory。而且，2.4引入了新的对象：file address space，它包含用来读写一整页数据的方法。这些方法考虑到了inode的更新、page cache处理和临时buffer的使用。page cache和buffer cache的同步问题就消除了。原来使用inode+offset查找page cache变成通过file address space+offset；原来struct page 中的inode成员被address_space类型的mapping成员取代。这个改进还使得匿名内存的共享成为可能（这个在2.2很难实现，许多讨论过）。 <BR><BR><BR>4. 虚存系统则从freeBSD借鉴了很多经验，针对2.2的问题作了巨大的调整。 <BR><BR>文档：RFC: design for new VM不可不读。 <BR><BR>由于时间仓促，新vm的很多细微之处我也还没来得及搞清楚。先大致罗列一下，以后我将进一步完善本文，争取把问题说清楚。另外，等这学期考试过后，我希望能为大家提供一些详细注释过的源代码。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/486.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:28 <a href="http://www.cnitblog.com/drizztzou/articles/486.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux最新稳定内核2.4.x的网络接口源码的结构</title><link>http://www.cnitblog.com/drizztzou/articles/485.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:21:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/485.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/485.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/485.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/485.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/485.html</trackback:ping><description><![CDATA[一.前言 <BR><BR>　　Linux的源码里，网络接口的实现部份是非常值得一读的，通过读源码，不仅对网络协议会有更深的了解，也有助于在网络编程的时候，对应用函数有更精确的了解和把握。 <BR><BR>　　本文把重点放在网络接口程序的总体结构上，希望能作为读源码时一些指导性的文字。 <BR><BR>　　本文以Linux2.4.16内核作为讲解的对象，内核源码可以在<A style="COLOR: #003793" href="http://www.kernel.org/" target=_blank>http://www.kernel.org</A>上下载。我读源码时参考的是<A style="COLOR: #003793" href="http://lxr.linux.no/" target=_blank>http://lxr.linux.no/</A>这个交差参考的网站，我个人认为是一个很好的工具，如果有条件最好上这个网站。 <BR><BR>　　二.网络接口程序的结构 <BR><BR>　　Linux的网络接口分为四部份：网络设备接口部份，网络接口核心部份，网络协议族部份，以及网络接口socket层。 <BR>　　网络设备接口部份主要负责从物理介质接收和发送数据。实现的文件在linu/driver/net目录下面。 <BR><BR>　　网络接口核心部份是整个网络接口的关键部位，它为网络协议提供统一的发送接口，屏蔽各种各样的物理介质，同时有负责把来自下层的包向合适的协议配送。它是网络接口的中枢部份。它的主要实现文件在linux/net/core目录下，其中linux/net/core/dev.c为主要管理文件。 <BR><BR>　　网络协议族部份是各种具体协议实现的部份。Linux支持TCP/IP，IPX，X.25，AppleTalk等的协议，各种具体协议实现的源码在linux/net/目录下相应的名称。在这里主要讨论TCP/IP(IPv4)协议，实现的源码在linux/net/ipv4,其中linux/net/ipv4/af_inet.c是主要的管理文件。 <BR><BR>　　网络接口Socket层为用户提供的网络服务的编程接口。主要的源码在linux/net/socket.c <BR><BR><BR>　　三.网络设备接口部份 <BR><BR>　　物理层上有许多不同类型的网络接口设备, 在文件include/linux/if_arp.h的28行里定义了ARP能处理的各种的物理设备的标志符。网络设备接口要负责具体物理介质的控制，从物理介质接收以及发送数据，并对物理介质进行诸如最大数据包之类的各种设置。这里我们以比较简单的3Com3c501 太网卡的驱动程序为例，大概讲一下这层的工作原理。源码在Linux/drivers/net/3c501.c。 <BR><BR>我们从直觉上来考虑，一个网卡当然最主要的是完成数据的接收和发送，在这里我们来看看接收和发送的过程是怎么样的。 <BR><BR>　　发送相对来说比较简单，在Linux/drivers/net/3c501.c的行475 开始的el_start_xmit()这个函数就是实际向3Com3c501以太网卡发送数据的函数，具体的发送工作不外乎是对一些寄存器的读写，源码的注释很清楚，大家可以看看。 <BR><BR>　　接收的工作相对来说比较复杂。通常来说，一个新的包到了，或者一个包发送完成了，都会产生一个中断。Linux/drivers/net/3c501.c的572开始el_interrupt()的函数里面，前半部份处理的是包发送完以后的汇报，后半部份处理的是一个新的包来的，就是说接收到了新的数据。el_interrupt()函数并没有对新的包进行太多的处理，就交给了接收处理函数el_receive()。el_receive()首先检查接收的包是否正确，如果是一个“好”包就会为包分配一个缓冲结构(dev_alloc_skb())，这样驱动程序对包的接收工作就完成了，通过调用上层的函数netif_rx()(net/core/dev.c1214行) ，把包交给上层。 <BR><BR>　　现在驱动程序有了发送和接收数据的功能了，驱动程序怎么样和上层建立联系呢？就是说接收到包以后怎么送给上层，以及上层怎么能调用驱动程序的发送函数呢？ <BR><BR>　　由下往上的关系，是通过驱动程序调用上层的netif_rx()(net/core/dev.c 1214行)函数实现的，驱动程序通过这个函数把接到的数据交给上层，请注意所有的网卡驱动程序都需要调用这个函数的，这是网络接口核心层和网络接口设备联系的桥梁。 <BR><BR>　　由上往下的关系就复杂点。网络接口核心层需要知道有多少网络设备可以用，每个设备的函数的入口地址等都要知道。网络接口核心层会大声喊，“嘿，有多少设备可以帮我发送数据包？能发送的请给我排成一队！”。这一队就由dev_base开始，指针structnet_device *dev_base (Linux/include/linux/netdevice.h 436行)就是保存了网络接口核心层所知道的所有设备。对于网络接口核心层来说，所有的设备都是一个net_device结构，它在include/linux/netdevice.h,line 233里被定义，这是从网络接口核心层的角度看到的一个抽象的设备，我们来看看网络接口核心层的角度看到的网络设备具有的功能： <BR><BR>　　struct net_device { <BR>　　……… <BR>　　open() <BR>　　stop() <BR>　　hard_start_xmit() <BR>　　hard_header() <BR>　　rebuild_header() <BR>　　set_mac_address() <BR>　　do_ioctl() <BR>　　set_config() <BR>　　hard_header_cache() <BR>　　header_cache_update() <BR>　　change_mtu() <BR>　　tx_timeout() <BR>　　hard_header_parse() <BR>　　neigh_setup() <BR>　　accept_fastpath() <BR>　　……… <BR>　　} <BR><BR>　　如果网络接口核心层需要由下层发送数据的时候，在dev_base找到设备以后，就直接调dev-&gt;hard_start_xmit()的这个函数来让下层发数据包。 <BR><BR><BR>　　驱动程序要让网络接口核心层知道自己的存在，当然要加入dev_base所指向的指针链，然后把自己的函数以及各种参数和net_device里的相应的域对应起来。加入dev_base所指向的指针链是通过函数register_netdev(&amp;dev_3c50)(linux/drivers/net/net_init.c, line 532) <BR><BR>　　建立的。而把自己的函数以和net_device里的相应的域及各种参数关系的建立是在el1_probe1()(Linux/drivers/net/3c501.c)里进行的： <BR><BR>　　el1_probe1(){ <BR>　　……… <BR>　　dev-&gt;open = &amp;el_open; <BR>　　dev-&gt;hard_start_xmit = &amp;el_start_xmit; <BR>　　dev-&gt;tx_timeout = &amp;el_timeout; <BR>　　dev-&gt;watchdog_timeo = HZ; <BR>　　dev-&gt;stop = &amp;el1_close; <BR>　　dev-&gt;get_stats = &amp;el1_get_stats; <BR>　　dev-&gt;set_multicast_list = &amp;set_multicast_list; <BR>　　……… <BR>　　ether_setup(dev); <BR>　　……… <BR><BR>　　} <BR><BR>　　进一步的对应工作在ether_setup(dev) (drivers/net/net_init.c, line 405 )里进行。我们注意到dev-&gt;hard_start_xmit =&amp;el_start_xmit，这样发送函数的关系就建立了，上层只知道调用dev-&gt;hard_start_xmit这个来发送数据，上面的语句就把驱动程序实际的发送函数告诉了上层。 <BR><BR>　　四.网络接口核心部分 <BR><BR>　　刚才谈论了驱动程序怎么和网络接口核心层衔接的。网络接口核心层知道驱动程序以及驱动程序的函数的入口是通过*dev_base指向的设备链的，而下层是通过调用这一层的函数netif_rx()(net/core/dev.c <BR>1214行) 把数据传递个这一层的。 <BR><BR>　　网络接口核心层的上层是具体的网络协议，下层是驱动程序，我们以及解决了下层的关系，但和上层的关系没有解决。先来讨论一下网络接口核心层和网络协议族部份的关系，这种关系不外乎也是接收和发送的关系。 <BR><BR>　　网络协议，例如IP，ARP等的协议要发送数据包的时候会把数据包传递给这层，那么这种传递是通过什么函数来发生的呢？网络接口核心层通过dev_queue_xmit()(net/core/dev.c,line975)这个函数向上层提供统一的发送接口，也就是说无论是IP，还是ARP协议，通过这个函数把要发送的数据传递给这一层，想发送数据的时候就调用这个函数就可以了。dev_queue_xmit()做的工作最后会落实到dev-&gt;hard_start_xmit()，而dev-&gt;hard_start_xmit()会调用实际的驱动程序来完成发送的任务。例如上面的例子中，调用dev-&gt;hard_start_xmit()实际就是调用了el_start_xmit()。 <BR><BR>　　现在讨论接收的情况。网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214行)接收了上层发送来的数据，这时候当然要把数据包往上层派送。所有的协议族的下层协议都需要接收数据，TCP/IP的IP协议和ARP协议，SPX/IPX的IPX协议，AppleTalk的DDP和AARP协议等都需要直接从网络接口核心层接收数据，网络接口核心层接收数据是如何把包发给这些协议的呢？这时的情形和于下层的关系很相似，网络接口核心层的下面可能有许多的网卡的驱动程序，为了知道怎么向这些驱动程序发数据，前面以及讲过时，是通过*dev_base这个指针指向的链解决的，现在解决和上层的关系是通过static struct packet_ptype_base[16]( net/core/dev.c line 164)这个数组解决的。这个数组包含了需要接收数据包的协议，以及它们的接收函数的入口。 <BR><BR>　　从上面可以看到，IP协议接收数据是通过ip_rcv()函数的，而ARP协议是通过arp_rcv()的，网络接口核心层只要通过这个数组就可以把数据交给上层函数了。 <BR><BR><BR>　　如果有协议想把自己添加到这个数组，是通过dev_add_pack()(net/core/dev.c, line233)函数，从数组删除是通过dev_remove_pack()函数的。Ip层的注册是在初始化函数进行的void __init ip_init(void) (net/ipv4/ip_output.c, line 1003) <BR><BR>　　{ <BR>　　……… <BR>　　dev_add_pack(&amp;ip_packet_type); <BR>　　……… <BR><BR>　　} <BR><BR>　　重新到回我们关于接收的讨论，网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214行)接收了上层发送来的数据，看看这个函数做了些什么。 <BR><BR>　　由于现在还是在中断的服务里面，所有并不能够处理太多的东西，剩下的东西就通过cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ) <BR><BR>　　交给软中断处理， 从open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL)可以知道NET_RX_SOFTIRQ软中断的处理函数是net_rx_action()(net/core/dev.c, line 1419)，net_rx_action()根据数据包的协议类型在数组ptype_base[16]里找到相应的协议，并从中知道了接收的处理函数，然后把数据包交给处理函数，这样就交给了上层处理，实际调用处理函数是通过net_rx_action()里的pt_prev-&gt;func()这一句。例如如果数据包是IP协议的话，ptype_base[ETH_P_IP]-&gt;func()(ip_rcv()),这样就把数据包交给了IP协议。 <BR><BR>　　五.网络协议部分 <BR><BR>　　协议层是真正实现是在这一层。在linux/include/linux/socket.h里面，Linux的BSD <BR>Socket定义了多至32支持的协议族，其中PF_INET就是我们最熟悉的TCP/IP协议族(IPv4, 以下没有特别声明都指IPv4)。以这个协议族为例，看看这层是怎么工作的。实现TCP/IP协议族的主要文件在inux/net/ipv4/目录下面，Linux/net/ipv4/af_inet.c为主要的管理文件。 <BR><BR>　　在Linux2.4.16里面，实现了TCP/IP协议族里面的的IGMP,TCP,UDP,ICMP,ARP,IP。我们先讨论一下这些协议之间的关系。IP和ARP协议是需要直接和网络设备接口打交道的协议，也就是需要从网络核心模块(core) <BR>接收数据和发送数据的。而其它协议TCP,UDP,IGMP,ICMP是需要直接利用IP协议的，需要从IP协议接收数据，以及利用IP协议发送数据，同时还要向上层Socket层提供直接的调用接口。可以看到IP层是一个核心的协议，向下需要和下层打交道，又要向上层提供所以的传输和接收的服务。 <BR><BR>　　先来看看IP协议层。网络核心模块(core) 如果接收到IP层的数据，通过ptype_base[ETH_P_IP] 数组的IP层的项指向的IP协议的ip_packet_type-&gt;ip_rcv()函数把数据包传递给IP层,也就是说IP层通过这个函数ip_rcv()(linux/net/ipv4/ip_input.c)接收数据的。ip_rcv()这个函数只对IP数据保做了一些checksum的检查工作，如果包是正确的就把包交给了下一个处理函数ip_rcv_finish()(注意调用是通过NF_HOOK这个宏实现的)。现在，ip_rcv_finish()这个函数真正要完成一些IP层的工作了。IP层要做的主要工作就是路由，要决定把数据包往那里送。路由的工作是通过函数ip_route_input()(/linux/net/ipv4/route.c,line 1622)实现的。对于进来的包可能的路由有这些： <BR><BR>　　属于本地的数据(即是需要传递给TCP，UDP，IGMP这些上层协议的) ； <BR>　　需要要转发的数据包(网关或者NAT服务器之类的)； <BR>　　不可能路由的数据包(地址信息有误)； <BR><BR><BR>　　我们现在关心的是如果数据是本地数据的时候怎么处理。ip_route_input()调用ip_route_input_slow()(net/ipv4/route.c, line 1312)，在ip_route_input_slow()里面的1559行rth-&gt;u.dst.input= <BR><BR>　　ip_local_deliver，这就是判断到IP包是本地的数据包，并把本地数据包处理函数的地址返回。好了，路由工作完成了，返回到ip_rcv_finish()。ip_rcv_finish()最后调用拉skb-&gt;dst-&gt;input(skb)，从上面可以看到，这其实就是调用了ip_local_deliver()函数，而ip_local_deliver(),接着就调用了ip_local_deliver_finish()。现在真正到了往上层传递数据包的时候了。 <BR><BR>　　现在的情形和网络核心模块层(core) 往上层传递数据包的情形非常相似,怎么从多个协议选择合适的协议，并且往这个协议传递数据呢？网络网络核心模块层(core) 通过一个数组ptype_base[16]保存了注册了的所有可以接收数据的协议，同样网络协议层也定义了这样一个数组struct net_protocol*inet_protos[MAX_INET_PROTOS](/linux/net/ipv4/protocol.c#L102),它保存了所有需要从IP协议层接收数据的上层协议(IGMP，TCP，UDP，ICMP)的接收处理函数的地址。我们来看看TCP协议的数据结构是怎么样的： <BR><BR>　　linux/net/ipv4/protocol.c line67 <BR>　　static struct inet_protocol tcp_protocol = { <BR>　　handler: tcp_v4_rcv,// 接收数据的函数 <BR>　　err_handler: tcp_v4_err,// 出错处理的函数 <BR>　　next: IPPROTO_PREVIOUS, <BR>　　protocol: IPPROTO_TCP, <BR>　　name: "TCP" <BR>　　}; <BR><BR>　　第一项就是我们最关心的了，IP层可以通过这个函数把数据包往TCP层传的。在linux/net/ipv4/protocol.c的上部，我们可以看到其它协议层的处理函数是igmp_rcv(), <BR>udp_rcv(), icmp_rcv()。同样在linux/net/ipv4/protocol.c，往数组inet_protos[MAX_INET_PROTOS] 里面添加协议是通过函数inet_add_protocol()实现的，删除协议是通过 inet_del_protocol()实现的。inet_protos[MAX_INET_PROTOS]初始化的过程在linux/net/ipv4/af_inet.c inet_init()初始化函数里面。 <BR><BR>　　inet_init(){ <BR>　　…… <BR>　　printk(KERN_INFO "IP Protocols: "); <BR>　　for (p = inet_protocol_base; p != NULL;) { <BR>　　struct inet_protocol *tmp = (struct inet_protocol *) p-&gt;next; <BR>　　inet_add_protocol(p);// 添加协议 <BR>　　printk("%s%s",p-&gt;name,tmp?", ":"\n"); <BR>　　p = tmp; <BR>　　……… <BR>　　} <BR>如果你在Linux启动的时候有留意启动的信息, 或者在linux下打命令dmesg就可以看到这一段程序输出的信息： <BR>　　IP Protocols： ICMP，UDP，TCP，IGMP也就是说现在数组inet_protos[]里面有了ICMP，UDP，TCP，IGMP四个协议的inet_protocol数据结构，数据结构包含了它们接收数据的处理函数。 <BR><BR>　　Linux 2.4.16在linux/include/linux/socket.h里定义了32种支持的BSDsocket协议，常见的有TCP/IP,IPX/SPX,X.25等，而每种协议还提供不同的服务，例如TCP/IP协议通过TCP协议支持连接服务，而通过UDP协议支持无连接服务，面对这么多的协议，向用户提供统一的接口是必要的，这种统一是通过socket来进行的。 <BR><BR>　　在BSD socket网络编程的模式下，利用一系列的统一的函数来利用通信的服务。例如一个典型的利用TCP协议通信程序是这样： <BR><BR>　　sock_descriptor = socket(AF_INET,SOCK_STREAM,0); <BR>　　connect(sock_descriptor, 地址，) ； <BR>　　send(sock_descriptor,”hello world”); <BR>　　recv(sock_descriptor,buffer,1024,0); <BR><BR>　　第一个函数指定了协议Inet协议，即TCP/IP协议，同时是利用面向连接的服务，这样就对应到TCP协议，以后的操作就是利用socket的标准函数进行的。 <BR><BR>　　从上面我们可以看到两个问题，首先socket层需要根据用户指定的协议族(上面是AF_INET) <BR>从下面32种协议中选择一种协议来完成用户的要求，当协议族确定以后，还要把特定的服务映射到协议族下的具体协议，例如当用户指定的是面向连接的服务时，Inet协议族会映射到TCP协议。 <BR><BR>　　从多个协议中选择用户指定的协议，并把具体的出理交给选中的协议，这和一起网络核心层向上和向下衔接的问题本质上是一样的，所以解决的方法也是一样的，同样还是通过数组。在Linux/net/socket.c定义了这个数组staticstruct net_proto_family *net_families[NPROTO] 。数组的元素已经确定了，net_families[2] 是TCP/IP协议，net_families[3] <BR><BR>　　是X.25协议，具体那一项对应什么协议，在include/linux/socket.h有定义。但是每一项的数据结构net_proto_family的ops是空的，也就是具体协议处理函数的地址是不知道的。协议的处理函数和ops建立联系是通过sock_register()(Linux/net/socket.c)这个函数建立的，例如TCP/IP协议的是这样建立关系的： <BR><BR>　　int __init inet_init(void) (net/ipv4/af_inet.c) <BR>　　{ <BR>　　(void) sock_register(&amp;inet_family_ops); <BR><BR>　　} <BR><BR>　　只要给出AF_INET(在宏里定义是2)，就可以找到net_failies[2] 里面的处理函数了。 <BR><BR>　　协议的映射完成了，现在要进行服务的映射了。上层当然不可能知道下层的什么协议能对应特定的服务，所以这种映射自然由协议族自己完成。在TCP/IP协议族里，这种映射是通过struct <BR>list_head inetsw[SOCK_MAX]( net/ipv4/af_inet.c) <BR><BR>　　这个数组进行映射的，在谈论这个数组之前我们来看另外一个数组inetsw_array[](net/ipv4/af_inet.c) <BR><BR>　　static struct inet_protosw inetsw_array[] = <BR>　　{ <BR>　　{ <BR>　　type: SOCK_STREAM, <BR>　　protocol: IPPROTO_TCP, <BR>　　prot: &amp;tcp_prot, <BR>　　ops: &amp;inet_stream_ops, <BR>　　capability: -1, <BR>　　no_check: 0, <BR>　　flags: INET_PROTOSW_PERMANENT, <BR>　　}, <BR><BR>　　{ <BR>　　type: SOCK_DGRAM, <BR>　　protocol: IPPROTO_UDP, <BR>　　prot: &amp;udp_prot, <BR>　　ops: &amp;inet_dgram_ops, <BR>　　capability: -1, <BR>　　no_check: UDP_CSUM_DEFAULT, <BR>　　flags: INET_PROTOSW_PERMANENT, <BR>　　}, <BR><BR>　　{ <BR>　　type: SOCK_RAW, <BR>　　protocol: IPPROTO_IP, /* wild card */ <BR>　　prot: &amp;raw_prot, <BR>　　ops: &amp;inet_dgram_ops, <BR>　　capability: CAP_NET_RAW, <BR>　　no_check: UDP_CSUM_DEFAULT, <BR>　　flags: INET_PROTOSW_REUSE, <BR>　　} <BR>　　}; <BR><BR>　　我们看到，SOCK_STREAM映射到了TCP协议，SOCK_DGRAM映射到了UDP协议，SOCK_RAW映射到了IP协议。现在只要把inetsw_array里的三项添加到数组inetsw[SOCK_MAX]就可以了，添加是通过函数inet_register_protosw()实现的。在inet_init() <BR><BR>　　(net/ipv4/af_inet.c) 里完成了这些工作。 <BR><BR>　　还有一个需要映射的就是socket其它诸如accept,send(), <BR><BR>　　connect(),release(),bind()等的操作函数是怎么映射的呢？我们来看一下上面的数组的TCP的项 <BR>　　{ <BR>　　type: SOCK_STREAM, <BR>　　protocol: IPPROTO_TCP, <BR>　　prot: &amp;tcp_prot, <BR>　　ops: &amp;inet_stream_ops, <BR>　　capability: -1, <BR>　　no_check: 0, <BR>　　flags: INET_PROTOSW_PERMANENT, <BR>　　}, <BR><BR>　　我们看到这种映射是通过ops，和prot来映射的，我们再来看看 tcp_prot这一项： <BR><BR>　　struct proto tcp_prot = { <BR>　　name: "TCP", <BR>　　close: tcp_close, <BR>　　connect: tcp_v4_connect, <BR>　　disconnect: tcp_disconnect, <BR>　　accept: tcp_accept, <BR>　　ioctl: tcp_ioctl, <BR>　　init: tcp_v4_init_sock, <BR>　　destroy: tcp_v4_destroy_sock, <BR>　　shutdown: tcp_shutdown, <BR>　　setsockopt: tcp_setsockopt, <BR>　　getsockopt: tcp_getsockopt, <BR>　　sendmsg: tcp_sendmsg, <BR>　　recvmsg: tcp_recvmsg, <BR>　　backlog_rcv: tcp_v4_do_rcv, <BR>　　hash: tcp_v4_hash, <BR>　　unhash: tcp_unhash, <BR>　　get_port: tcp_v4_get_port, <BR>　　}; <BR>　　所以的映射都已经完成了，用户调用connect()函数，其实就是调用了tcp_v4_connect()函数，按照这幅图，读起源码来就简单了很多了。 <BR><BR>　　六 Socket层 <BR><BR>　　上一节把socket层大多数要讨论的东西都谈论了，现在只讲讲socket 层和用户的衔接。 <BR><BR>　　系统调用socket(),bind(),connect(),accept,send(),release()等是在Linux/net/socket.c里面的实现的,系统调用实现的函数是相应的函数名加上sys_的前缀。 <BR><BR>　　现在看看当用户调用socket()这个函数，到底下面发生了什么。 <BR><BR>　　Socket(AF_INET,SOCK_STREAM,0)调用了sys_socket(),sys_socket()接着调用socket_creat(),socket_creat()就要根据用户提供的协议族参数在net_families[]里寻找合适的协议族，如果协议族没有被安装就要请求安装该协议族的模块，然后就调用该协议族的create()函数的处理句柄。根据参数AF_INET，inet_creat()就被调用了，在inet_creat()根据服务类型在inetsw[SOCK_MAX] <BR><BR>　　选择合适的协议，并把协议的操作集赋给socket就是了，根据SOCK_STREAM，TCP协议被选中， <BR>　　inet_creat(){ <BR>　　answer=inetsw [用户要求服务服务] ； <BR>　　sock-&gt;ops = answer-&gt;ops; <BR>　　sk-&gt;prot = answer-&gt;prot <BR>　　} <BR><BR>　　到此为止，上下都打通了，该是大家都源码的时候了。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/485.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:21 <a href="http://www.cnitblog.com/drizztzou/articles/485.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>