您的位置:首页 > 运维架构 > Linux

linux内核设计与实现摘录及 Linux的内核源代码 的阅读

2011-09-21 11:37 561 查看
自:http://hi.baidu.com/donghaozheng/blog/item/67c79c45056b8831869473d2.html
Linux的内核源代码 的阅读介绍

Linux的内核源代码可以从很多途径得到。一般来讲,在安装的linux系统下,/usr/src/linux目录下的东西就是内核源代码。

对于源代码的阅读,要想比较顺利,事先最好对源代码的知识背景有一定的了解。对于linux内核源代码来讲,我认为,基本要求是:1、操作系统的基本知识;2、对C语言比较熟悉,最好要有汇编语言的知识和GNU C对标准C的扩展的知识的了解。另外在阅读之前,还应该知道Linux内核源代码的整体分布情况。我们知道现代的操作系统一般由进程管理、内存管理、文件系统、驱动程序、网络等组成。看一下Linux内核源代码就可看出,各个目录大致对应了这些方面。Linux内核源代码的组成如下(假设相对于linux目录):

arch 这个子目录包含了此核心源代码所支持的硬件体系结构相关的核心代码。如对于X86平台就是i386。

include 这个目录包括了核心的大多数include文件。另外对于每种支持的体系结构分别有一个子目录。

init 此目录包含核心启动代码。

mm 此目录包含了所有的内存管理代码。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下,如对应于X86的就是arch/i386/mm/fault.c 。

drivers 系统中所有的设备驱动都位于此目录中。它又进一步划分成几类设备驱动,每一种也有对应的子目录,如声卡的驱动对应于drivers/sound。

ipc 此目录包含了核心的进程间通讯代码。

modules 此目录包含已建好可动态加载的模块。

fs Linux支持的文件系统代码。不同的文件系统有不同的子目录对应,如ext2文件系统对应的就是ext2子目录。

kernel 主要核心代码。同时与处理器结构相关代码都放在arch/*/kernel目录下。

net 核心的网络部分代码。里面的每个子目录对应于网络的一个方面。

lib 此目录包含了核心的库代码。与处理器结构相关库代码被放在arch/*/lib/目录下。

scripts此目录包含用于配置核心的脚本文件。

Documentation 此目录是一些文档,起参考作用。

俗话说:“工欲善其事,必先利其器”。 阅读象Linux核心代码这样的复杂程序令人望而生畏。它象一个越滚越大的雪球,阅读核心某个部分经常要用到好几个其他的相关文件,不久你将会忘记你原来在干什么。所以没有一个好的工具是不行的。由于大部分爱好者对于Window平台比较熟悉,并且还是常用Window系列平台,所以在此我介绍一个Window下的一个工具软件:Source Insight。这是一个有30天免费期的软件,可以从www.sourcedyn.com下载。安装非常简单,和别的安装一样,双击安装文件名,然后按提示进行就可以了。安装完成后,就可启动该程序。这个软件使用起来非常简单,是一个阅读源代码的好工具。它的使用简单介绍如下:先选择Project菜单下的new,新建一个工程,输入工程名,接着要求你把欲读的源代码加入(可以整个目录加)后,该软件就分析你所加的源代码。分析完后,就可以进行阅读了。对于打开的阅读文件,如果想看某一变量的定义,先把光标定位于该变量,然后点击工具条上的相应选项,该变量的定义就显示出来。对于函数的定义与实现也可以同样操作。别的功能在这里就不说了,有兴趣的朋友可以装一个Source
Insight,那样你阅读源代码的效率会有很大提高的。怎么样,试试吧!

linux内核设计与实现摘录

【进程管理】

1.进程

在linux系统中,进程创建通常是调用fork()系统调用的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。

调用fork()的进程被称为父进程,新产生的进程被称为子进程。在该调用结束时,在返回点这个相同的位置上,父进程恢复执行,子进程开始执行。

通常创建新的进程都是为了执行新的、不同的程序,而接着调用exec()这族函数就可以创建新的地址空间,并把新的程序载入。

最终,程序通过exit()系统调用退出执行。这个函数会终结进程并将其占用的资源释放掉。

进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid()为止。
进程的另一个名字是任务(task)。Linux内核通常把进程也叫做任务。

内核把进程存放在叫做任务队列(task list)的双向循环链表中。链表中每一项都是类型为task_struct,称为进程描述符(process descrpotr)的结构,包含了一个具体进程的所有信息。
linux进程之间存在一个明显的继承关系。所有进程都是PID为1的init进程的后代。

系统中每个进程必有一个父进程。相应的,每个进程也可以拥有一个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。
2.进程上下文

当一个程序调用了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。

除非在此期间有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间继续执行。
系统调用和异常处理程序是内核明确定义的接口,进程只有通过这些接口才能陷入内核执行。--对内核的所有访问都必须通过这些接口。
3.线程

Linux实现线程的机制非常特别。从内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当作进程来实现。

内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个使用某些共享资源的进程。

每个线程都拥有唯一隶属自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是该进程和其他一些进程共享某些资源,如地址空间)
上述线程机制的实现与Microsoft Windows或是Sun Solaris等操作系统的实现差异非常大。这些系统都是在内核中提供了专门支持线程的机制。在这些系统中,进程是重量级的,而线程被抽象成一种耗费较少资源,运行迅速的执行单元。而对于Linux来说,线程只是一种进程间共享资源的手段(linux的进程本身足够轻量的了)。举例来说,假如我们有一个包含四个线程的进程,在windows中,通常会有一个包含指向四个不同线程的指针的进程描述符。该描述符负责描述像地址空间、打开的文件这样的共享资源。线程本身再去描述它独占的资源。相反,Linux仅仅创建四个进程并分配四个普通的task_struct结构,建立者四个进程时指定他们共享某些资源就行了。

4.内核线程(主要了解这个概念)

内核经常需要在后台执行一些操作,这种任务可以通过内核线程(kernel thread)完成--独立运行在内核空间的标准进程。

内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(实际上它的mm指针被设置为NULL)。

它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。

这些线程在系统启动时由另外一些内核线程启动。实际上,内核线程也只能由其他内核线程创建。

在现有内核线程中创建一个新的内核线程的方法如下:

int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)

一般情况下,内核线程会将它在创建时得到的函数永远执行下去,该函数通常由一个循环构成,在需要的时候,

这个内核线程就会被唤醒和执行,完成了当前任务,它会自行休眠。
----------------------------------------------------------------------------------------------------

【系统调用】
1.系统调用号

每一个系统调用被赋予一个独一无二的系统调用号,应用程序通过调用号指明想执行哪个系统调用,不会提及系统调用的名称。

一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用。

系统维护着一张系统调用表,记录了每一个注册的系统调用。
2.系统调用处理程序

用户空间的程序不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以在内核的地址空间读写的话,系统安全就会失去控制。

所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换至内核态,这样内核就可以代表应用程序来执行该系统调用了。
通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。

X86系统上的软中断由int$0x80指令产生。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用程序,叫system_call()。
因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的,需要把系统调用号一并传给内核,

这个传递动作是通过在触发中断前把调用号装入eax寄存器实现的。system_call通过给定的系统调用号,执行相应的系统调用。
3.参数传递

系统调用需要的参数也像传递系统调用号一样,通过寄存器传递。在X86系统上,ebx、ecx、edx、esi和edi按照顺序存放前五个参数。

给用户空间返回的值夜通过寄存器传递。在X86系统上,它存放在eax寄存器中。
系统调用要求功能明确、必要且要考虑长远,Linux尽量避免系统调用的添加。

跟内核的交互,可通过创建设备节点,用read、write访问,用ioctl()来进行特别的设置操作和获取特别信息。
----------------------------------------------------------------------------------------------------

【内存管理】
1.页

内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字,但是,内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。从虚拟内存的角度来看,页就是最小单位。
内核用struct page结构标识系统中的每个物理页。必须要理解的一点是page结构与物理页相关,而并非与虚拟页相关。

这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。系统中的每个物理页都要分配一个这样的结构体。
2.区

由于硬件的限制,内核并不能对所有的页一视同仁。有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。

由于存在这种限制,内核把页划分为不同的区(zones)。
Linux使用了三种区:

ZONE_DMA:这个区包含的页能用来执行DMA操作。

ZONE_NORMAL:这个区包含的都是能正常映射的页。

ZONE_HIGHEM:这个区包含“高端内存”,其中的页并不能永久地映射到内核地址空间。

3.slab层

分配和释放数据结构是所有内核最普遍的操作之一。为了便于数据的频繁分配和回收,编程者常常会用到一个空闲链表。该空闲链表包含有可供使用的、已经分配好的数据结构块。当代码需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,再把数据放进去。以后,当不再需要这个数据结构的实例时,就把它放回空闲链表,而不是释放掉它。Linux提供了slab层(也就是所谓的slab分配器),其扮演了“通用数据结构缓存层”的角色。
4.内存分配

void *kmalloc(size_t size, int flags)

这个函数返回一个指向内存块的指针,其内存块至少要有size的大小。所分配的内存区在物理上是连续的。

分配的内存可能比你请求的还多,但是你无法知道到底多了多少。因为内核分配器本质上是基于页的,因此在可用内存内,某些分配可能向上取整。但内存绝不会少于所需的内存,如果内核不能找到所需的最少量,那么,分配就会失败,函数返回NULL。
5.内核栈

内核的栈大小固定,大多数32位体系的结构上,栈为8KB。

每个进程都有自己的内核栈。进程在内核执行期间的整个调用链必须放在自己的内核栈上。深度嵌套会导致溢出。

中断处理程序也使用被它们打断的进程的堆栈。

在栈上进行大量静态分配,比如分配大型数组和大型结构体,是很危险的。

内核栈溢出时悄无声息,不会有任何错误提示,因为内核没有在管理内核上做足工作,因此,当栈溢出时,多出的数据就会直接溢出来,覆盖掉紧邻堆栈末端的东西。
内核是完全信赖自己的,这点与用户空间不同,如果你有非法操作,内核会开开心心地把自己挂起来,停止运行。

-----------------------------------------------------------------------------------------------------------

【块I/O层】

块设备

块设备中最小的可寻址单元是扇区。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元--块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次传输多个扇区。大多数块设备的扇区都是512字节。块是文件系统的一种抽象--只能基于块来访问文件系统。虽然物理磁盘寻址是按照扇区级进行的,但是内核执行的所有磁盘操作都是按照块进行的。块和扇区的大小都是2的整数倍,一般块是数个扇区大小,另外块大小不能超过一个页的长度。
-----------------------------------------------------------------------------------------------------------------

【进程地址空间】

1.内核除了管理本身的内存外,还必须管理进程的地址空间--也就是系统中每个用户空间进程所看到的内存。
进程只能访问有效范围内的内存地址,另外每个地址范围也具有特定的访问属性,如只读或不可执行等属性。

如果一个进程访问了不在有效范围中的地址,或以不正确的方式访问有效地址,那么内核就会终止该进程,并返回“段错误”信息。
内存区域可以包含各种内存对象,比如:代码段;数据段;BSS段(零页);堆;用户空间栈;C库、DLL等的代码段数据段bss段等;内存映射文件;共享内存等。
2. 内存描述符

内核使用内存描述符结构体(mm_struct)表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。

其中mm_users域记录正在使用该进程的进程数目。比如,如果两个进程共享该地址空间,那么mm_users的值便等于2;mm_count域是mm_struct结构体的主引用计数,只要mm_users不为0,那么mm_count值就等于1。当mm_users的值减为0(两个线程都退出)时,mm_count域的值才为0,此说明已经没有任何指向该mm_struct结构体的引用了,这时该结构体会被销毁。
所有的进程的mm_struct结构体都链接在双向链表中,该链表的首元素是init_mm内存描述符,代表init进程的地址空间。这个特性跟进程描述符很像,所有的进程描述符也是链接成了一个双向链表。
在进程的进程描述符中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。

fork()函数利用copy_mm()函数复制父进程的内存描述符,也就是current->mm域给其子进程。
3. 内存区域

内存区域由vm_area_struct结构体描述(又称VMA),该结构体描述了指定地址空间内连续区间上的一个独立内存范围。

内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性,比如访问权限等,另外,相应的操作也都一致。

采用面向对象的方法使VMA结构体可以代表多种类型的内存区域--比如内存映射文件或进程的用户空间栈等。

(个人理解:该结构体用于描述内存空间里某段内存的含义,比如从哪到哪为代码段,哪到哪为数据段,哪到哪位共享区,哪到哪为用户空间栈等,因此他们不能有内存重叠。且每一个进程地址空间都会有对应的内存区域来描述)
(mmap,内存映射,实际上就是一个内存区域vma的创建过程。通常用在文件映射,使对文件的访问变为内存操作,而不再是低效的文件I/O,也可以达到数据共享的作用)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: