全部博文(43)
分类: LINUX
2010-12-28 17:51:53
1.1 内核的任务
1、从应用程序的视角看,你可以被认为是一台增强的计算机,将计算机抽象到一个高层次上。
2、当若干程序在同一系统中并发运行时,也可以将内核视为资源管理程序。
3、令一种研究内核的视角是将内核视为库,其提供了一组面向系统的命令。
1.2 内核的实现策略
当前,在操作系统实现方面,有以下两种主要范型:
1、微内核:这种范型中只有最基本的功能直接由中央内核(微内核)实现。所有其他的功能都委托给一些独立进程,这些进程通过明确定义的通信接口与中心内核通信。
2、宏内核:与微内核相反,宏内核是构建系统内核的传统方法。这种方法中,内核的全部代码,包括所有子系统(如内存管理,文件系统,设备驱动程序)都打包到一个文件中。
1.3 内核的组成部分
图为 linux内核的高层次概述以及完整的linux系统中的各个层次
1.3.1 进程、进程切换、调度
1、传统上,UNIX操作系统下进行的应用程序,服务器及其他程序都称为进程
2、内核借助于CPU的帮助,负责进程切换的结束细节。必须给各个进程造成一种错觉,即CPU总是可用的。通过在撤销进程的CPU资源之前保存进程所有与状态相关的要素,并将进程置于空闲状态,即可达到这一目的。在重新激活进程时,则将保存 的状态原样恢复。进程之间的切换称为进程切换。
3、内核还必须确定如何在现存进程之间共享CPU时间。重要进程得到的CPU时间多一点,次要进程得到的少一点。确定哪个进程运行时间的过程称为调度。
1.3.2 UNIX操作系统中有两种创建新进程的机制,分别是fork和exec。
(1)、fork可以创建当前进程的一个副本。父进程和子进程只有PID(进程ID)不同。在该系统调用执行之后,系统中有两个进程,都执行同样的操作。父进程内存的内容将被复制,至少从程序的角度看是这样。Linux使用了一种众所周知的技术来使fork操作更高效,该技术称为写时复制(copy on write),主要的原理是将内存复制操作延迟到父进程或者子进程向某内存页面写入数据之前,在只读访问的情况下父进程和子进程可以共用同一内存页。
(2)、exec将一个新程序加载到当前进程的内存中并执行。旧程序的内存页将刷出,其内容将替换为新的数据。然后开始执行新程序。
1、线程
进程并不是内核支持的唯一一种程序执行形式。除了重量级进程(有时也成为UNIX进程)之外,还有一种形式是线程(有时也成为轻量级进程)。
2、命名空间
1.3.3 地址空间与特权级别
1.3.4 页表
用来将虚拟地址空间映射到物理地址空间的数据结构成为页表。Linux采用四级分页。以三级分页为例:如图:
分配虚拟地址
虚拟地址的第一部分成为全局页目录(page Glogal Directory,PGD),PGD用于索引进程中的一个数组(每个进程有且仅有一个),该数组是所谓的全局页目录或PGD。PGD的数据项指向另一些数组的起始地址,这些数组称为中间页目录(Page Middle Directory,PMD)。
虚拟地址中的第二个部分称为PMD,在通过PGD中的数组项找到对应的PMD滞后,则使用PMD来索引PMD。PMD的数组项也是指针,指向下一级数组,称为页表或页目录。
虚拟地址的第三个部分称为PTE(Page Table Entry,页表数组),用作页表的索引。虚拟内存页和页帧之间的映射就此完成,因为页表的数组项是指向页帧的。
虚拟地址最后的一个部分称为偏移量。它制定了业内部分的一个字节位置。归根结底,每个地址都指向地址空间中唯一定义的某个字节。
当然,该方法也有一个缺点。每次访问内存时,必须逐级访问多个数组才能将虚拟地址转换为物理地址。CPU试图用一下两种方法加速该工程:
(1)、CPU中有一个专门的部分称为MMU(Memory management Unit,内存管理单元),该单元优化了内存访问操作。
(2)、地址转换中出现最频繁的那些地址,保存到称为地址转换后备缓冲器(translation Lookaside Buffer,TLB)的CPU高速缓存中。
在许多体系结构中高速缓存的运转时透明的,但某些体系结构则需要内核专门处理。这更意味着每当页表的内容变化时必须使TLB高速缓存无效。内核中凡涉及操作页表之处都必须调用相应的指令。如果针对不需要此类操作的体系结构编译内核,则相应调用自动变为空操作。
1.3.5 物理内存的分配
1、 伙伴系统
内核中很多时候要求分配连续页。为快速检测内存中的连续区域,内核采用了一种古老而历经检验的技术:伙伴系统。
系统中的空闲内存块总是两两分组,每组中的两个内存块称为伙伴。伙伴的分配可以是彼此独立的。但如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存块,作为下一层次上某个内存块的伙伴。如图示范了该系统,图中给出了一对伙伴,初始大小均为8页:
伙伴系统
内核对所有大小相同的伙伴(1、2、4、8、16或其他数目的页),都放置到同一个列表中管理。各有8页的一对伙伴也在相应的列表中。
如果系统现在需要8个页帧,则将16个页帧组成的块拆分为两个伙伴。其中一块用于满足应用程序请求,而剩余的8个页帧则放置到对应8页大小内存块的列表中。
如果下一个请求只需要2个连续页帧,则由8页组成的块会分裂成为两个伙伴,每个包含4个页帧。其中一块放回伙伴列表中,另一块再分裂成两个伙伴,每个包含2页。其中一个回到伙伴系统,另一个则传递给应用程序。
2、 Slab缓存
内核本身经常需要比完整页帧小得多的内存块。由于内核无法使用标准库的函数,因此必须在伙伴系统基础上自行定义额外的内存管理层,将伙伴系统提供的页划分为更小的部分。该方法不仅可以分配内存,还为频繁使用的小对象实现了一个一般性的缓存——slab缓存。
Slab可以用两种方法分配内存:
(1)、对频繁使用的对象,内核定义了只包含了所需类型对象实例的缓存。每次需要某对象时,可以从对应的缓存快速分配(使用后分配到缓存)。Slab缓存自动维护与伙伴系统的交互,在缓存用尽时会请求新的页帧。
(2)、对通常情况下消内存块的分配,内核针对不用大小的对象定义了一组slab缓存,可以像用户空间编程一样,用相同的函数访问这些缓存。不同之处是这些函数都增加了前缀k,表明是与内核相关的:kmalloc和kfree。
3、 页面交换和页面回收
页面交换通过利用磁盘空间作为扩展内存,从而增大了可用的内存。在内核需要更多内存时,不经常使用页可以写入硬盘。如果再需要访问相关数据,内核会将相应的页切换回内存。通过缺页异常机制,这种切换操作对应用程序是透明的。换出的页可以通过特别的页表项标识。在进程试图访问此类页帧时,CPU则启动一个可以被内核截取的缺页异常。此时内核可以将硬盘上的数据切换到内存中。接下来用户程序可以恢复运行。由于进程无法感知缺页异常,所以也的换入和换出对进程是完全不可见的。
页面回收用于将内存映射被修改 的内容与底层的快设备同步,为此有时也简称为数据回写。数据刷出后,内核即可以将页帧用于其它用途(类似于页面交换)。内核的数据结构包含了于此相关的所有信息,当再次需要改数据时,可根据相关信息用硬盘找到对应的数据并加载。
1.3.6 计时
内核能够测量时间以及不同时间点的时差,进程调度就会用到该功能。Jiffies是一个适合的时间坐标。名为jiffies_64和jiffies_32(分别是64位和32位)的全局变量,会按照恒定的时间间隔递增。每种计算机底层体系结构都提供了一些执行周期性操作的手段,通常的形式是定时器中断。对于上述两个各全局变量的更新可使用底层体系提供的各种定时器机制执行。
1.3.7 系统调用
系统调用时用户进程与内核交互的经典方法。POSIX标准定义了许多系统调用,以及这些系统调用在所有遵从POSIX的系统包括linux上的语义。传统的系统调用按不同的类别分组,如下所示:
【1】、进程管理:创建新进程,查询信息,调试。
【2】、信号:发送信号,定时器以及相关处理机制。
【3】、文件:创建,打开和关闭文件,从文件读取和向文件写入,查询信息和状态。
【4】、目录和文件系统:创建、删除和重命名目录,查询信息,连接,变更目录。
【5】、保护机制:读取和变更UID/GID,命名空间处理。
【6】、定时器函数:定时器函数和统计信息。
对所有的处理器来说,一个共同点就是:用户进程从用户状态切换到核心太,并将系统关键任务委派给内核执行,系统调用时必由之路。
1.3.8 设备驱动程序、块设备和字符设备
(1)、字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。
(2)、块设备:应用程序可以随机访问设备数据,程序可以自行确定读取数据的位置。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。
1.3.9 网络
网卡也可以通过设备驱动程序控制,但在内核中属于特殊状况,以为网卡不能利用设备文件访问。原因在于在网络通信期间,数据打包到了各种协议层中。在接收到数据时,内核必须针对各协议层的处理,对数据进行拆包与分析,然后喜爱能将有效数据传递给应用程序。在发送数据时,内核必须首先根据各个协议层的要求打包数据,然后才能发送。
为支持通过 文件接口处理网络连接(按照应用程序的观点),linux使用了源于BSD的套接字抽象。套接字可以看做应用程序、文件接口、内核的网络实现直接之间的代理。
1.3.10 文件系统
Linux支持许多不同的文件系统:标准的Ext2和Ext3文件系统、ReiserFS、XFS、VFAT(为兼容DOS),还有很多其他文件系统。
内核鼻息提供一个额外的软件层,将各种底层文件系统的具体特性与应用层(和内核自身)隔离开来。该软件层称为VFS(Virtual Filesystem或Virtual Filesystem Switch,虚拟文件系统或虚拟文件系统交换器)。VFS既是向下的接口(文件系统都必须实现该接口),同时也是向上的接口(用户进程通过系统调用最终能够访问文件系统功能)。如图:
虚拟文件系统层、文件系统实现和块设备层之间的互操作
1.3.11 模块和热插拔
模块用于在运行时动态地向内核添加功能,如设备驱动程序、文件系统、网络协议等,实际上内核的任何子系统几乎都可以模块化。这消除了宏内核与微内核相比一个重要的不利之处。
对于支持热插拔而言,模块在本质上是必须的某些总线(例如:USB和FireWire)允许在系统运行时连接设备,而无需系统重启。在系统检测到新设备时,通过加载对应的模块,可以将必要的驱动程序自动加到内核中。
1.3.12 缓存
内核使用缓存来改进系统性能。从低速的块设备读取的数据会暂时保持在内存中,即使数据在当时已经不再需要了。在应用程序下一次访问该数据时,它可以从访问速度较快的内存中读取,因而绕过了低速的块设备。由于内核是通过基于页的内存映射来实现访问的,因此缓存也是按照页组织。也就是说整页都缓存起来,故称为页缓存(page cache)。
块缓存用于缓存没有组织成页的数据,其重要性差得多。在传统的UNIX系统上,块缓存用作系统的主缓存,而linux很久以前也是这样。到如今,块缓存已经被页缓存取代了。
1.3.13 链表处理
C程序中重复出现的一项任务是对双链表的处理。内核页需要处理这样的链表。
标准双链表
双链表简表
有若干处理链表的标准函数,如下(其中参数的数据类型是struct list_head):
【1】、list_add(new, head)用于现存的head元素之后,紧接着插入new元素。
【2】、list_add_tail(new, head)用于在head元素之前,紧接着插入new元素。如果指定head为表头,由于链表是循环的,那么new元素就插入到链表的末尾(该函数因此而得名)。
【3】、list_del(entry)从链表中删除一项。
【4】、list_empty(head) 检测链表是否为空,也就是链表是否没有包含元素。
【5】、list_splice( list, head) 负责合并两个链表,把list插入到另一个现存链表的head元素之后。
【6】、查找链表元素必须使用list_entry。初看起来,其调用语法相当复杂:list_entry(ptr, type, member)。Ptr是指向数据结构中list_head成员实例的一个指针,type是该数据结构的类型,而member则是数据结构中表示链表元素的成员名。如果再链表中查找task_struct的实例,则需要下列示例调用struct task_struct = list_entry(ptr, struct task_struct, run_list)。
因为链表的实现不是类型安全的,所以需要显示指定类型。如果数据结构包含在多个链表中,则必须指定所要查找的链表元素,才能找到正确的链表元素。
【7】、list_for_each(pos, head)用于便利链表的说有元素。Pos表示链表中的当前位置,而head指定了表头。
1.3.14 对象管理和引用计数
1、一般性的内核对象
下列数据结构将嵌入其他数据结构中,用作内核对象的基础。
Struct kobject{
const char * k_name;
struct kref kref;
struct list_head entry;
struct kobject * parent;
struct kset * kset;
struct kobj_type * ktype;
struct sysfs_dirent * sd;
};
Kobject结构各个成员的语义如下表示:
【1】、k_name是对象的文本名称,可利用sysfs到处到用户空间。
【2】、kref类型为struct kref,用于简化引用计数的管理。
【3】、entry是一个标准的链表元素,用于将若干kobject放置到一个链表中(在这种情况下成为集合)。
【4】、将对象与其他对象放置到一个集合时,则需要kset。
【5】、parent是一个指向父对象的指针,可以用于在kobject之间建立层次结构。
【6】、ktype提供了包含kobject的数据结构的更多详细信息。其中,最重要的是用于释放该数据结构资源的析构器函数。
2、对象集合
在很多星狂下,必须将不同的内核对象归类到集合里,用到的数据结构定义如下:
Struct kset{
struct kobj_type * ktype;
struct list_head list;
……
struct kobject kobj;
struct kset_uevent_ops * uevent_ops;
};
有趣的是,kset是内核对象应用的第一个例子。由于管理集合的结构只能是内核对象,因此它可以通过先前讨论过的struct kobject管理。实际上kset中潜入了一个kobject的实例kobj.它与集合中包含的各个kobject无关,只是用来管理kset对象本身。
其他成员的含义如下:
【1】、ktype指向kset中各个内核对象共用的kobj_type结构;
【2】、list是所有属于当前集合的内核对象的链表;
【3】、uevent_ops提供了若干函数指针,用于将集合的状态信息传递给用户层。该机制有驱动程序模型的核心使用
另一个结构用于描述内核对象的共同特性。其定义如下:
Struct kobj_type{
……
struct sysfs_ops * sysfs_ops;
struct attribute ** default_attrs;
};
注意,kobj_type与内核对象的集合没有关系,kset已经提供了集合功能。该结构提供了与sysfs文件系统的接口。如果多个对象通过该文件西贡到处类似的信息,则可以简化,是多个对象共享一个ktype来提供所需的方法。
3、引用计数
引用计数用于检测内核中有多少地方使用了某个对象。内核提供了下列数据结构处理引用计数:
Struct kref{
aomic_t refcount;
};
该数据结构很简单,它只提供了一个一般性的原子引用计数。“原子”在这里以为着,对该变量的加1和减1操作在多处理器系统上是安全的,多处理器系统中可能会有多个代码路径同时访问一个对象。
1.3.15 数据类型
1、 类型定义
内核使用typedef来定义各种数据类型,以避免依赖于体系结构相关的特性。
2、 字节序
大段序格式,小端序格式。
3、 Per-cpu变量
普通的用户空间程序设计不会涉及的一个特殊事项就是所谓的per-cpu变量。他们是通过DEFINE_PER_CPU(name, type)声明,其中name 是变量名,而type是其数据类型(例如int[3]、struct hash等)。在单处理器系统上,这与常规的变量声明没有不同。在有若干CPU的SMP系统上,会为每个CPU分别创建变量的一个实例。用于某个特定CPU的实例可以通过get_cpu(name, cpu)获得,其中smp_processor_id()可以返回当前活动处理器的ID,用作前述的CPU参数。
4、 访问用户空间
源代码中多处指针都标记为__user,该标识符对用户空间程序设计是未知的。内核使用该标记来标识指向用户地址空间中区域的指针,在没有进一步预防措施的情况下,不能轻易访问这些指针指向的区域。
1.4 为什么内核是特别的
内核很神奇,但归根结底它只是一个大的C程序,带有一些会变代码。
颂扬了内核代码之后,还有许多不同于用户程序的严肃问题需要说明:
【1】、调试内核需要比调试用户层程序困难。
【2】、内核提供了许多辅助函数。
【3】、用户层应用程序的错误可能导致段错误(segmentation fault)或者内存转储(core dump),但内核错误会导致整个系统故障。甚至更糟的是:内核会继续运行,在错误发生若干小时以后系统离奇地崩溃。
【4】、必须考虑到内核运行的许多体系结构上根本不支持非对称的内存访问。
【5】、所有的内核代码都必须并发安全的。
【6】、内核代码必须在小端序和大端序计算机上都能工作。
【7】、大多数的体系结构根本不允许在内核中执行浮点计算。