深刻Linux内核架构——简介与概述

1、内核的任务

纯技术层面上,内核是硬件与软件的之间的一个中间层。做用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各类设备和组件进行寻址。linux

  • 从应用程序视角上看,内核能够被认为是一台加强的计算机,将计算机抽象到一个高层次上。应用程序与硬件本没有联系,只与内核有联系,内核是应用程序所知道的层次结构中的最底层。
  • 当若干程序在同一系统中并发运行时,也能够将内核视为资源管理程序。内核负责将可用共享资源分配到各个系统进程,同时保证系统的完整性。
  • 将内核视为库,其提供了一组面向系统的命令。一般系统调用用于向计算机发送请求。

2、实现策略

在操做系统的实现方面,有两种主要的泛型:微内核和宏内核。设计模式

  • 微内核:只有最基本的功能直接由内核实现。全部其余的功能(譬如文件系统、内存管理等)都委托给一些独立的进程,这些进程经过明肯定义的通讯接口与内核通讯。这种设计方式优势包括:动态可扩展性和在运行时切换重要组件。但因为在各个组件之间支持复杂通讯须要额外的CPU时间,因此尽管微内核在各研究领域早已成为活跃主题,但在使用性方面进展甚微。
  • 宏内核:宏内核是构建系统内核的传统方法。这种方法中,内核的所有代码包括全部子系统(如内存管理、文件系统、设备驱动程序)都打包到一个文件中。内核中每一个函数均可以访问内核全部其余部分,容易致使源代码中出现复杂的嵌套。

当前宏内核的性能仍强于微内核,Linux采起的是宏内核的设计模式。可是也进行了必定程度上的改进,系统运行中,模块能够插入到内核代码中,也能够移除。数组

3、内核的组成部分

图1概述了组成完整Linux系统的各个层次,以及内核所包含的一些重要子系统。缓存

图1 Linux内核的高层次概述以及完整的Linux系统中的各个层次安全

一、进程、进程切换、调度

进程:传统上UNIX操做系统下运行的应用程序、服务器及其余程序都称为进程。每一个进程都在CPU的虚拟内存中分配地址空间。各个进程的地址空间是彻底独立的。Linux是多任务系统,支持并发执行的若干进程。系统中同时真正在运行的进程数目最多不超过CPU的数目。服务器

进程切换:进程之间的切换。内核借助CPU的帮助,负责进程切换的技术细节。经过在撤销进程的CPU资源以前保存进程全部与状态相关的要素,并将进程置于空闲状态。从新激活进程时,将保存的状态原样恢复。网络

调度:内核必须肯定如何在现存进程之间共享CPU时间。重要进程获得的CPU时间多一点,次要进程少一点,肯定哪一个进程运行多长时间的过程称为调度。数据结构

二、UNIX进程

 Linux对进程采用了一种层次系统,每一个进程都依赖于一个父进程。内核启动init程序做为第一个进程,负责进一步系统初始化工做,init是进程树的根,全部进程都直接或间接起源于该进程(在linux系统终端输入pstree查看进程树)。并发

树型结构的扩展方式与新进程建立方式密切相关。UNIX操做系统中建立新进程的机制有两个,分别是fork和execfork能够建立当前进程的一个副本,除了PID,子进程彻底复制父进程的内存内容。在Linux中,采用写时复制(copy on write)技术,将内存复制操做延迟到父进程或子进程向某内存页面写入数据以前,在只读访问的状况下父进程和子进程共用一个内存页,提升了执行效率。exec将一个新程序加载到当前进程的内存中并执行,旧程序的内存页被刷出,其内容替换为新数据,开始执行新程序。ide

线程:有时也称为轻量级进程。本质上一个进程可能由若干线程组成,这些线程共享一样的数据和资源,但可能执行程序中不一样的代码路径。Linux用clone方法建立线程,工做方式相似于fork,可是会检查确认哪些资源与父进程共享,哪些资源为线程独立建立。细粒度的资源分配扩展了通常线程的概念,在必定程度上容许线程与进程之间的连续转换。

命名空间:传统的Linux使用了许多全局量,启用命名空间后,之前的全局资源具备了不一样的分组。每一个命名空间能够包含一个特定的PID集合,或能够提供文件系统的不一样视图,在某个命名空间中挂载的卷不会传播到其余命名空间中。(并不是内核的全部部分都彻底支持命名空间)命名空间的经典做用之一:能够经过称为容器的命名空间来创建系统的多个视图,一台物理机中能够运行多个虚拟机。与彻底的虚拟解决方案(如KVM)相比,计算机上多了一个内核来管理全部的容器。

三、地址空间与特权级别

因为内存区域是经过指针寻址,所以CPU的字长决定了所能管理的地址空间的最大长度。以32位系统为例,能够管理232B=4GB的内存。

地址空间的最大长度与实际可用的物理内存数量无关,所以被称为虚拟地址空间。从系统中每一个进程的角度看,地址空间中只有自身一个进程,没法感知到其余进程的存在。Linux将虚拟地址空间划分为两个部分,分别为内核空间用户空间,如图2所示。

              

图2 虚拟地址空间的划分

        

 

图3特权级别的环状系统

系统中每一个进程都有自身的虚拟地址范围,从0到TASK_SIZE,用户空间之上的区域保留给内核专用。以IA-32为例,地址空间在3GB处划分,每一个进程虚拟地址空间都是3GB,内核空间由1GB可用。此划分与内存数量无关。因为地址空间虚拟化的结果,每一个用户进程都认为自身有3GB内存。各个系统进程的用户空间彼此彻底分离。(64位计算机可管理巨大的理论虚拟地址空间,操做系统中倾向于使用小于64的位数,实际使用的如42为或47位等,这样作能够节省一些CPU的工做量)。

特权级别:内核把虚拟地址空间划分为两个部分,所以可以保护各个系统进程,使之彼此分离。全部现代的CPU都提供了几种特权级别,进程能够驻留在某一个特权级别。IA-32体系结构使用4种特权级别构成的系统,各级别能够看做是环,如图3所示。Intel处理器有4种特权级别,Linux只使用两种不一样的状态:核心态和用户状态。两种状态的关键差异在于用户状态禁止访问内存空间。从用户状态到核心态的切换经过系统调用的特定转换手段完成,且系统调用的执行因具体系统而不一样。图4概述了不一样的执行上下文(详细讨论见下一篇博客)。此外,在多处理器系统中,线程启动时能够指定CPU,并限制只能在特定CPU上运行。

 图4 在核心态和用户态执行(CPU大多时间在执行用户空间中代码,当应用程序执行系统调用时切换到核心态,此时,内核能够访问虚拟地址空间用户部分。系统调用结束后CPU回到用户状态。硬件中断也可使CPU切换到核心态,这种状况下内核不能访问用户空间)

大多状况下,单个虚拟地址空间就比系统中可用的物理内存大。此外,每一个进程也都有自身的虚拟地址空间,所以,内核和CPU必须考虑如何将实际可用的物理内存映射到虚拟地址空间的区域。Linux内核中用页表来为物理地址分配虚拟地址。虚拟地址关系到进程的用户空间和内核空间,而物理地址则用来寻址实际可用的内存。原理如图5所示。两个进程的虚拟地址空间都被划分为不少等长的部分(页),物理内存一样进行划分。

 图5 虚拟地址和物理地址

物理内存页常常称为页帧,页则指虚拟地址空间中的划分单位。虚拟地址空间和物理内存之间的映射也使得进程之间的隔离有一点点松动(内核负责将虚拟地址空间映射到物理地址空间,决定哪些内存区域在进程之间共享哪些不共享)。图5代表并不是虚拟地址空间的全部页都映射到某个页帧(多是由于页没有使用,或者数据尚不须要使用而没有载入)。

四、页表

用来将虚拟地址空间映射到物理地址空间的数据结构称为页表,对于页表的管理采用多级分页模型。如图6所示,将虚拟地址划分红4部分,这样须要一个三级页表。(当前Linux内核采用了四级页表)此处以三级页表为例,虚拟地址的第一部分称为全局页目录(Page Global Directory, PGD),用于索引进程中的一个数组(每一个进程有且仅有一个);虚拟地址的第二个部分称为PMD(Page Middle Directory),并经过PGD中的数组项找到对应的PMD以后,使用PMD来索引PMD;虚拟地址的第三部分称为PTE(Page Table Entry),用做页表的索引,页表的数组项指向页帧,虚拟内存页和页帧之间的映射由此完成;虚拟内存的最后一部分称为偏移量,它指定了页内部的一个字节的位置。每一个地址都指向地址空间中惟必定义的某个字节。

 图6 分配虚拟地址

页表对虚拟地址空间中不须要的区域,没必要建立中间页目录或页表,节省了大量的内存。可是每次访问内存时,必须逐级访问多个数组才能将虚拟地址转化为物理地址。

CPU试图使用MMU(Memory Management Unit)优化内存访问操做;同时对于地址转换中出现最高频的那些地址,保存到地址转换后备缓冲器(Translation Lookaside Buffer)的CPU高速缓存中。在许多体系结构中高速缓存的运转是透明的,但某些体系结构则须要内核专门处理。

与CPU交互:IA-32体系结构在将虚拟地址映射到物理地址时,只用了两级页表,64位体系结构中须要三级或四级页表,内核与体系结构无关的部分老是假定使用四级页表。对于只支持二级或三级页表的CPU来讲,内核中体系结构相关代码必须经过空页表对缺乏的页表进行仿真。

内存映射:内存映射是一种重要的抽象手段。映射方法能够将任意来源的数据传输到虚拟地址空间中,做为映射目标的地址空间区域,能够像普通内存那也用一般的方法访问。内核在时限设备驱动程序时,直接使用了内存映射,外设的输入/输出能够映射到虚拟地址空间的区域中,对相关内存区域的读写会由系统重定向到设备,简化了驱动程序的实现。

五、物理内存的分配

内核分配内存时,会记录页帧已分配或空闲状态,以避免两个进程使用一样的内存区域。内核只分配完整的页帧,将内存划分为更小的部分工做则委托给用户空间中的标准库。

内核采用伙伴系统进行快速检测内存中的连续区域。系统中的内存块老是两两分组,每组中两个内存块称为伙伴。若两个伙伴都空闲,则将其合并为一个更大内存块,做为下一层次上的某个内存块的伙伴。图7师范了伙伴系统,初始大小为8页。从上到下,若是系统须要8个页帧,则将16个页帧组成的块分为两个伙伴,往下相似...。

 图7 伙伴系统

内核自己常常须要比完整页帧小得多的内存块,因为内核没法使用标准库函数,所以在伙伴系统的基础上,设置了内存管理层,将伙伴系统提供的页划分为更小的部分,此外还为频繁使用的小对象设置了slab缓存。slab缓存自动维护与伙伴系统的交互,在缓存用尽时会请求信的页帧。图8综述了伙伴系统、slab分配器以及内核其余方面之间的关联。

 图8 页帧的分配由伙伴系统进行,而slab分配器则负责分配小内存以及提供通常性的内核缓存

页面交换经过利用磁盘空间做为扩展内存,增大了可用内存。内核须要更多内存时,不常用的页可用写入硬盘,再须要访问的时候经过缺页异常机制,将相应的页切换回内存。

页面回收用于将内存映射被修改的内容与底层的块设备同步。有时简称数据回写。

六、计时

全局变量jiffies_64和jiffies(分别是64位和32位)为内核的时间坐标,会按恒定的时间间隔递增。(对其的更新操做可以使用底层体系结构提供的各类定时器机制执行)

基于jiffies的计时相对粒度较粗,在底层硬件能力容许的前提下,内核可以使用高分辨率的定时器提供额外的计时手段,可以以纳秒级的精确度和分辨率计量时间。

计时的周期能够动态改变,动态改变计时周期对于供电受限的系统(好比笔记本电脑和嵌入式系统)是颇有用的。

七、系统调用

系统调用是用户进程与内核交互的经典方法。POSIX标准定义了许多系统调用,以及这些系统调用在全部听从POSIX的系统包括Linux上的语义。传统的系统调用按不一样类别分组,为:

  • 进程管理:建立新进程,查询信息,调试。
  • 信号:发送信号,定时器以及相关处理机制。
  • 文件:建立、打开和关闭文件,从文件读取和向文件写入,查询信息和状态。
  • 目录和文件系统:建立、删除和重命名目录,查询信息,连接,变动目录。
  • 保护机制:读取和变动UID/GID,命名空间的处理。
  • 定时器函数:定时器函数和统计信息。

用户进程要从用户状态切换到核心态,并将系统关键任务委派给内核执行,系统调用是必由之路。(不一样的是不一样的硬件平台提供的切换机制不尽相同)

八、设备驱动程序、块设备和字符设备

设备驱动程序用于与系统连接的输入/输出装置通讯,如硬盘、软驱、各类借口、声卡等。

外设能够分为块设备和字符设备。

块设备:应用程序能够随机访问设备数据,程序可自行肯定读取数据的位置。数据的读写只能以块的倍数(一般512B)进行,不支持基于字符的寻址。(应用:硬盘)

字符设备:提供连续的数据流,应用程序能够顺序读取,一般不支持随机存取。相反,此类设备支持按字节/字符来读写数据。(应用:调制解调器)

因为内核为提升系统性能,普遍使用了缓存机制,块设备驱动程序比字符设备复杂。

九、网络

 因为在网络通讯期间,数据打包到了各类协议层中。接收到数据时,内核必须针对各协议层的处理,对数据进行拆包与分析,而后将有效数据传递给应用程序;发送数据时,内核必须首先根据各协议层的要求打包数据,才能发送。Linux使用了源于BSD的套接字抽象,以支持经过文件接口处理网络链接。套接字能够看做为应用程序、文件接口、内核的网络实现之间的代理。

十、文件系统

 Linux系统由大量文件组成,其数据存储在硬盘或其余块设备。存储使用了层次式文件系统。Linux支持许多不一样的文件系统:标准的Ext二、Ext3和Ext4文件系统、ReiserFS、XFS、VFAT(为兼容DOS)等等。不一样的文件系统基于不一样的概念抽象。此外内核必须提供一个VFS(Virtual Filesystem或Virtual Filesystem Switch),将各类底层文件系统的具体特性与应用层隔离。如图9所示,VFS既是向下的接口(全部文件系统都必须实现该接口),同时也是向上的接口(用户进程经过系统调用最终可以访问文件系统功能)。

 图9 虚拟文件系统层、文件系统实现和块设备层之间的互操做

十一、模块和热插拔

 模块用于在运行时动态向内核添加功能(如设备驱动程序、文件系统、网络协议等),消除了宏内核与微内核相比一个重要的不利之处。模块也能够在运行时从内核写在,方便了开发新内核组件。

模块本质上也是普通程序,它必须提供某些代码段在模块初始化和终止时执行,以便向内核注册和注销模块。模块代码能够像编译到内核中的代码同样,访问内核全部函数和数据。

对支持热插拔而言,模块本质上是必须的。某些总线(好比USB)容许在系统运行时链接设备,无需重启,系统检测到设备时,经过加载对应的模块,将驱动添加到内核中。(某些模块开不开源有争论)

十二、缓存

内核使用缓存来改进性能。从低速的块设备读取的数据会暂时保持在内存中,应用程序下次访问数据时,能够绕太低速的块设备。因为内核经过基于页的内存映射来实现对块设备的访问,所以缓存也按页组织,称为页缓存。块缓存用于缓存没有组织成页的数据,现在已被页缓存取代。

1三、链表处理

C程序中重复出现的一项任务是对双向链表的处理,内核一样须要处理这样的链表。内核提供了一个标准链表,可用于将任何类型的数据结构彼此链接起来(非类型安全)。加入链表的数据结构必须包含一个类型为list_head的成员,其中包含了正向和反向指针。链表的起点是list_head的实例,一般用LIST_HEAD(list_name)宏来声明初始化。图10为内核创建的标准双链表示意图。对链表进行操做时,内核定义了一些API。

 图10 标准双链表

  • list_add(new,head):在紧接head以后插入new元素。
  • list_add_tail(new,head):在head以前(即链表末尾)插入new元素。
  • list_del(entry):从链表中删除一项。
  • list_empty(head):检测链表是否为空。
  • list_splice(list,head):在head后插入list链表,合并两个链表。
  • list_entry(ptr,type,member):查找链表元素。(ptr为指向数据结构list_head的指针,type是该数据结构的类型,member是数据结构中表示链表元素的成员名)
  • list_for_each(pos,head):遍历链表全部元素。

1四、对象管理和引用计数

内核中许多地方须要跟踪记录C语言中结构的实例,这会致使代码复制。所以,在内核2.5开发期间,采用通常性的方法来管理内核对象,它不止是为了防止代码复制,同时也为内核不一样部分管理的对象提供了一致的视图,在许多部分能够有效地使用相关信息。通常性的内核对象机制可用于执行:引用计数;管理对象链表;集合加锁;将对象属性导出到用户空间(经过sysfs文件系统)。

(1)通常性的内核对象

通常性的内核对象抽象成了一个结构体kobject,用做内核对象的基础。

1 struct kobject{
2         const char              * k_name; //对象的文本名称
3         struct kref             kref; //用于简化引用计数的管理
4         struct list_head        entry; //标准链表元素
5         struct kobject          * parent; //一个指向父对象的指针
6         struct kset             * kset; //将对象与其余对象放置到一个集合时须要
7         struct kobj_type        *ktype; //提供了包含kobject数据结构更多详细信息
8         struct sysfs_dirent     * sd;            
9 }

kobject与面向对象语言(C++/Java)中的对象概念的性质类似。kobject抽象提供了在内核使用面向对象技术的可能性。

(2)对象集合

不少状况下,须要将不一样的内核对象归类到集合中(好比全部字符设备集合,全部基于PCI的设备集合等)。

1 struct kset{
2     struct kobj_type      * ktype; //指向kset中各内核对象的公用kobj_type结构,提供了与sysfs文件系统的接口
3     struct list_head      list;//当前集合的内核对象链表
4 ...
5     struct kobject       kobj;
6     struct kset_uevent_ops   * uevent_ops; //提供了若干函数指针,将集合状态传递给用户层
7 }

 kset是内核对象应用的第一个例子,它对kobject的管理是在kset中嵌入了一个kobject的实例kobj,它与集合中包含的各个kobject无关,只是用来管理kset对象自己。

(3)引用计数

引用计数用于检测内核中有多少地方使用了某个对象。每当内核的一个部分须要某个对象所包含的信息时,该对象引用计数加1,若是再也不须要,则引用计数减1。当计数为0时,内核知道再也不须要该对象,便从内存中将其释放。(对引用计数的操做为原子操做)

1五、数据类型

(1)类型定义

内核使用typedef定义各类数据类型,避免依赖于体系结构相关的特性。(好比sector_t用于指定块设备扇区编号,pid_t表示进程ID,_s8(8位有符号数),_u8(8位无符号数)等)

(2)字节序

现代计算机采用大端序(big endian)或小端序(little endian)格式。大端序中,高位在低字节;小端序中低位在低字节。内核提供了各类函数和宏,能够在CPU使用的格式与特定表示法之间转换。

(3)per_cpu变量

per_cpu变量是经过DEFINE_PER_CPU(name,type)声明,在有若干CPU的SMP系统上,会为每一个CPU分别建立变量的一个实例。用于某个特定CPU的实例能够经过get_cpu(name,cpu)得到,其中smp_processor_id()能够返回当前活动处理器ID。采用per_cpu变量好处 :所需数据极可能存在于处理器缓存中,所以能够快速访问;绕过了多处理器系统中使用可能被全部CPU同时访问的变量的通讯问题。

(4)访问用户空间

源代码中多处指针标记为_user,表示对用户空间程序设计未知,在没有进一步预防措施时,不能轻易访问这些指针指向的区域。由于内存是经过页表映射到虚拟地址空间的用户空间部分的,不是由物理内存直接映射的,内核须要确保指针所指的页帧确实存在于物理内存中。

4、内核特别之处

  • 调试内核一般比调试用户层程序困难。
  • 内核提供了许多辅助函数,相似于用户空间的C语言库,但内核领域中的东西老是朴素得多。
  • 用户层程序错误可能会致使segmentation fault或core dump,但内核错误会致使整个系统故障。
  • 必须考虑到内核运行的许多体系结构上根本不支持非对齐的内存访问。
  • 全部内核代码都必须是并发安全的。对于多处理器计算机的支持,Linux内核代码必须是可冲入和线程安全的。
  • 内核代码必须在小端序和大端序计算机上都可以工做。
  • 大多数的体系结构根本不容许在内核中执行浮点计算,所以计算须要想办法用整型来替代。
相关文章
相关标签/搜索