Linux内核源代码情景分析笔记
好吧,首先我承认我要是读者的话,这篇文章我看着也头疼,因为写的太长太泛(其主要部分集中在内存管理,进程管理,文件系统)!原本是想按自己理解的精简精简的,按照操作系统中两个核心的抽象概念“进程”和“文件”开始介绍的,可以说操作系统引入这两个概念是系统的核心,其中进程的管理可以说是对内存和cpu的抽象管理,当然基础内存管理是必须的。而文件可以说是对设备的一层抽象(不仅仅是存储文件的设备),而底层的实现则需要设备驱动程序。而且进程与文件之间可以说是一个相辅相成的作用,从进程控制块就能看出,所以没办法分开来介绍那个。所以这篇文章还是按照学习的过程一点一点来总结介绍的。再者,其原因之一是linux系统实在庞大,之二是我写不出很高度概括性的东西,而且很多地方我怕以后有误解,所以写的很严谨,不敢说太多的大话迷惑大家,迷惑自己。 最后希望咱们学计算机的都能做个程序员,工程师之类的,而不是码农!好了,下面就进入主题-Linux内核源代码情景分析。
1预备知识 1.1linux内核简介 linux发展史 linux源代码组成 linux内核版和发行版的命名方式 1.2 Intelx86的寻址方式 早期8086和8088处理器是16位,寻址方式为实模式,后来随着技术的发展,人们意识到实模式的不足(完全把计算机暴露在用户的眼皮下),就产生了保护模式,关于这两点的区别,主要在于对内存的访问是否有进行保护,主要是越界和越权。越界就是用户不能访问的地址范围不允许访问,用户没有权限的地址范围不能访问。所以早期计算机工作者们就在建立了段式内存管理,把内存地址进行分段(比如,16段,每段大小64k,刚好1M)然后cpu中设置了四个段寄存器:CS,DS,SS,ES,分别用于可执行代码的指令,数据,堆栈,其他。由于早期intel内存大小采用1M,而cpu确实16位,如果不做一定额处理,16位总线是不能访问超过64k的范围,所以每个段寄存器的内容作为内存地址的高16位,同时与段内偏移地址作为低16进行相加(重叠12位),这样就刚好形成了20位的内存地址,即1M。 由于这种管理机制无法提供对内存的保护,所以基于段式内存管理机制,到后来的386以后,人们采用保护模式,并且数据总线为32位了,人们在原来的四个段寄存器的基础上加上两个段寄存器FS,GS,为了实现保护模式的作用。所以数据总线的地址内容应该有如下设计思想 1根据指令的性质来确定应该使用哪个段寄存器 2根据段寄存器的内容,找到相应的地址段描述结构 3根据地址段描述结构得到基地址 4将指令发出的地址作为位移,与段描述结构中规定的段长度比较,是否越界 5根据地址段描述结构符中的访问权限来确定是否越权 6将指令发出的地址作为位移,与基地址相加得出实际的物理地址。 上面就是保护模式下段式内存管理的设计思想,下面就是实现过程。 1段寄存器,由于段地址描述结构栈8个字节,故地址为8的整数倍,所以段寄存器低三位可以用于其他作用。 2段地址描述结构(GDTR,LDTR),其数据的主要作用在于描述基地址,权限,范围。
通过上面的段寄存器映射到过渡的段地址描述结构,最后就是指令发出的地址与段地址描述结构提供的基地址来映射到最后的物理地址了(前提是没有采用分页内存管理)。 实模式与保护模式介绍的相关资料 http://wenku.baidu.com/link?url=hbpDCdrBxA_agf7u9pLZCdXIXvXwPXMvIbKsSgRZiPp9F8jcD6mRqsgDI9-dB9_8cuQS8Bz_IMxg85ZjytPOxiU_OBlk4_Gg7Nlo3mY8tg_ 1.3 i386页式管理机制 由于段式管理机制是实实在在的,没有抽象出来,不方便内存管理,人们就又在段式管理的基础上增加了分页式管理,从而由虚拟地址经过段式管理映射得到的地址不再是物理地址了,人们称为线性地址,在线性地址的基础上经过页面映射后得到的才是真正的物理地址。 在分页机制中,线性地址的结构被人为的划分为 而整个页面映射的过程为 这里解释两点 1 记得前面我的每个段地址描述结构都占8个字节,所以地址一定按8的整数倍对其,这里我们的页面大小人为的划分为4k,即按4k对其,所以我们的目录项和页表同样可以拿出低12位用作他用。至于怎样实现,那是硬件上的问题。 2 为什么采用多级分页机制,而不简单直接的使用页表和页内偏移地址来进行分页,目的是在于节省内存,至于是怎么节省的,我当初上操作系统课程始终不明白,现在搞明白了。因为操作系统在创建进程时会为进程分配一个进程控制块,里面其中有一部分就是对虚存管理的页面数据结构,如果不采用多级分页机制的话,那么每个进程关页面表就占 个字节,即1M,即是进程用不着那么页表项(因为32位地址默认个用户的虚存空间是4G,但一个进程很难想象会用完这4G),但还是会占内存,但是采用多级分页的话,那么不需要的页表项就不进行分配,从而也就节省了内存,但是相反一个进程真的用完了4G的虚存,那么他需要的目录项和页表项占的内存就多了目录项这一部分了。 1.4linux内核源代码中C语言代码 Linux内核的主体是由GNU的C语言编写,CNU为此提供了编译器gcc,而gcc从C++语言中吸收了inline和const,其实GNU的C和C++是合为一体的,gcc既是C的编译器也是C++的编译器,所以linux中很多C语言代码自然就有C++风格在里面,所以需要一点C++基础。但这不是问题关键,关键在学习linux中C语言设计思想。 1常见的宏定义对简单函数的实现
#define DUMP_WRITE(addr,nr) \ do{ memcpy(bufp,addr,nr);bufp+=nr} while(0)2使用结构体封装多个队列
Typedef struct page{ Struct list_head list; Struct page *next_hash; ………………… }mem_map_t;3使用宏定义与函数实现简单初始化
#define IRQ(x,y) \ IRQ##x##y##_interrupt #define IRQLIST_16(x) \ IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \ IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \ IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \ IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f) void (*interrupt[NR_IRQS])(void) = { IRQLIST_16(0x0), #ifdef CONFIG_X86_IO_APIC IRQLIST_16(0x1), IRQLIST_16(0x2), IRQLIST_16(0x3), IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7), IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb), IRQLIST_16(0xc), IRQLIST_16(0xd) #endif }; #undef IRQ #undef IRQLIST_161.5linux内核源代码中的汇编语言代码 汇编语言有两套格式,在dos/windows领域中采用的汇编语言都是由intel定义的指令格式,也就是我们教材里面的汇编语言格式。另外一种就是UNIX领域使用的AT&T定义的格式,所以在linux内核中所使用的汇编格式是AT&T。尽管两种汇编语言指令不一样,但是指令的风格很类似,所以提供了一种互相学习的条件。而且AT&T汇编同样适用于gcc编译器,所以在c语言中可以嵌入式汇编语句,所以看懂linux内核代码,需要AT&T汇编基础以及嵌入式汇编基础。这里不详细介绍,后面在进程切换一节中借助一段内核中用来实现内存空间切换的汇编代码来介绍一下嵌入式AT&T汇编。 详细参见:
#define switch_to(prev,next,last) do { \ asm volatile("pushl %%esi\n\t" \ "pushl %