linux内核设计与实现摘录

来源:互联网 发布:python list变元组 编辑:程序博客网 时间:2024/04/28 20:48

【进程管理】
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,也可以达到数据共享的作用

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