Linux内核架构概述

来源:互联网 发布:error 1130 mysql 编辑:程序博客网 时间:2024/06/02 04:25

1.1内核的任务

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

   从应用程序的角度来看,内核可以被认为是一台增强的计算机,将计算机抽象到一个高层次上。

   当若干程序在同一系统中并发运行时,也可以将内核视为自愿管理程序。

   另一种研究内核的视角是将内核视为库,其提供了一组面向系统的命令。

1.2 实现策略

   宏内核:宏内核是构建系统内核的传统方法。在这种方法中,内核的全部代码,包括所有子系统(如内存管理,文件系统,设备驱动程序)都打包到一个文件中。内核中的每个函数都可以访问内核中所有其他部分。

   在系统运行中,模块可以插入到内核代码中,也可以移除,这使得可以向内核动态添加功能,弥补了宏内核的缺陷。模块特性依赖于内核与用户层之间设计精巧的通信方法,这使得模块的热插拔和动态装载得以实现。

1.3内核的组成部分

    

1.3.1进程、进程切换、调度

     传统上,Unix操作系统下运行的应用程序、服务器及其他程序都称为进程。每个进程都在CPU的虚拟内存中分配地址空间。各个进程的地址空间是完全独立的,因此进程并不会意识到彼此的存在。从进程的角度来看,它会认为自己是系统中唯一的进程。如果进程想要彼此通信,例如交换数据,那么必须使用特定的内核机制。    

    由于Linux是多任务的系统,它支持并发执行的若干进程。系统中同时真正在运行的进程数目最多不超过CPU数目,因此内核会按照短的时间间隔在不同的进程之间切换。

   内核借助于CPU的帮助,负责进程切换的技术细节。必须给各个进程造成一种错觉,即CPU总是可用的。通过在撤销进程的CPU资源之前保存进程所有与状态相关的要素,并将进程置于空闲状态,即可达到这一目的。在重新激活进程时,则将保存的状态原样恢复。进程之间切换称之为进程切换

  内核还必须确定如何在现存进程之间共享CPU时间。重要进程得到的CPU时间多一点,次要进程得到的要少一点。确定哪个进程运行多长时间过长称之为调度


1.3.2 Unix进程

    Linux对进程采用了一种层次系统,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程,该进程负责进一步的系统初始化操作,并显示登录提示符或图形登录界面。因此init程序是进程树的根,所有进程都直接或者间接起源自该进程,使用pstree可以输出这个进程树。

   Unix操作系统有两种创建新进程的机制,分别是fork和exec。

    fork可以创建当前进程的一个副本,父进程和子进程只有pid不同,在该系统调用执行之后,系统有两个进程,都执行同样的操作。父进程内存的内容将被复制,至少从程序的角度来看是这样的。Linux使用了一种众所周知的技术来使用fork操作更高效,该技术称为写时复制,主要的原理是将内存复制操作延迟到父进程或子继承向某内存页面写入数据之前,在只读访问的情况下父进程和子进程可以共用同一内存页。

   exec将一个新程序加载到当前进程的内存中并执行。旧程序的内存页将刷出,将内容将替换为新的数据,然后开始执行新程序。

   1、线程

   由于线程和主程序共享同样的地址空间,主程序自动就可以访问接收到的数据,因此除了为防止线程访问同一内存区而采用互斥机制外,就不需要什么通信了。

  Linux用clone方法创建线程,其工作方式类似于fork,但启用了精确的检查,以确认哪些资源与父进程共享,哪些资源为线程独立创建。这种细粒度的资源分配拓展了一般的线程的概念,在一定程度上允许线程与进程之间的连续转换。

  2、命名空间

   在内核2.6的开发期间,对命名空间的支持被集成到了许多子系统中, 这使得不同的进程可以看到不同的系统视图。传统的Linux使用许多全局量,例如进程ID,系统中的每个进程都有一个唯一的标识符用户可使用ID来访问进程,例如向进程发个信号。启用命名空间之后,以前全局资源具有不同分组。每个命名空间可以包含一个特定PID集合,或可以提供文件系统的不同视图,在某个命名空间中挂载的卷不会传播到其他命名空间中。

   

1.3.3 地址空间与特权级别

    由于内存区域是通过指针寻址,因此CPU的字长决定了所能管理的地址空间的最大长度。对于32位系统,是2的32次方=4GB,对更现代的64位处理器可以管理2的64次方B。

   地址空间的最大长度与实际可用的物理内存数量无关,因此被称为虚拟地址空间,使用该术语的另一个理由是,从系统中每个进程的角度来看,地址空间只有自身一个进程,而无法感知到其他进程的存在。应用程序无需关注其他进程的存在,好像计算机只有一个进程一样。

   Linux将虚拟地址空间划分为两部分,分别称为内核空间和用户空间。

    

    系统中每个用户进程都有自身的虚拟地址范围,从0到TASK_SIZE,用户空间之上的区域保留给内核使用,用户进程不能访问。TASK_SIZE是一个特定于计算机体系结构的常数,把地址空间按给比例划分两部分,在IA-32系统中,地址空间在3GiB处划分,因此每个进程的虚拟地址空间为3GB。由于虚拟地址空间的总长度是4GiB,所以内核空间有1GiB可用。

    这种划分与可用的内存数量无关。一般64位实际使用的位数小于64位,如42位或者47位。

    1、特权级别。

    内核把虚拟地址空间划分成为两个部分,因此能够保护各个系统进程,使之彼此隔离。

    Linux值使用两种不同的状态:核心态和用户状态。这两种状态的关键差别在于对高于TASK_SIZE的内存区域的访问,简而言之,在用户状态禁止访问内核空间,用户进程不能操作或者读取内核空间中的数据,也无法执行内核空间中的代码,这是内核的专用领域,这种机制可防止进程无意间修噶彼此的数据而造成相互干扰。

    从用户状态到核心态的切换通过系统调用的特定转换手段完成,且系统调用的执行因具体系统而不同。如果普通进程想要执行任何影响整个系统的操作,则只能借助系统调用向内核发出请求。内核首先检查进程是否允许执行想要的操作,然后代表进程执行所需的操作,接下来返回到用户状态。

   除了代表用户程序执行代码之外,内核还可以由异步硬件中断激活,然后再中断上下文中运行,与在进程上下文运行的主要区别是,在中断上下文运行不能访问虚拟地址空间中的用户空间部分,因为中断可能随机访问。

    除了普通进程,系统还有内核线程在运行。内核线程也不与任何特定的用户空间进程相关联,因此也无权处理用户空间。不过在其他许多方面,内核线程更像普通的用户层应用程序。与在中断上下文运转的内核相比,内核线程可以进入睡眠状态,也可以像系统中的普通程序一样被调度器跟踪。内核线程可用于各种用途:从内存和块设备之间的数据同步,到帮助调度器在CPU上分配进程。

    通过ps fax可以查看内核线程。

    2、虚拟和物理地址空间

    大多数情况下,单个虚拟空间就比系统中可用的物理内存要大。不能进行一一映射。一个可取的方法是,用页表来为物理地址分配虚拟地址。虚拟地址关系到进程的用户空间和内核空间,而物理地址则用来寻址实际可用的内存。

    

    图示两个进程的虚拟地址空间,都被内核划分为很多等长的部分。这些部分称之为页。物理内存也划分为同样大小的页。

    物理内存页经常称作页帧。相比直线,页则专指虚拟地址空间中的页。

    虚拟地址空间和物理内存之间的映射也使得进程之间的隔离有点松动,也有可能两个进程显式共享的页帧,因为两个虚拟地址空间中的页可以映射到同一个物理内存页,由于内核负责将虚拟地址空间映射到物理地址空间,因此可以决定哪些内存区域在进程之间共享,哪些不共享。

   并非虚拟地址空间的所有页都映射到某个页帧。这可能是因为页没有使用,或者是数据尚不需要使用而没有载入内存中,还可能是页已经换出硬盘,将在需要时在换回内存。

 

1.3.4 页表

      用来将虚拟地址空间映射到物理地址空间的数据结构成为页表。实现两个地址空间的关联最容易的方法是使用数组,对虚拟地址空间中的每一页,都分配一个数组项。该数组项指向与之关联的页帧,但是有一个问题,IA-32体系结构使用4KiB页,在虚拟地址空间为4GiB前提下,则需要包含100万项的数组,在64为体系结构上,情况会更糟糕。

    因为虚拟地址空间的大部分区域都没有使用,因而也没有关联到页帧,那么就可以使用功能相同但内存用量少得多的模型:多级分页。

   为了减少页表的大小并容许忽略不需要的区域,计算机体系饥饿哦股的设计会将虚拟地址划分为多个部分,我将虚拟地址划分成4部分,这样就需要一个三级的页表,大多数体系结构都是这样的做法,Linux也是采用了四级页表。

     

     虚拟地址的第一部分称为全局页目录(Page Global Directory,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高速缓存中。无需访问内存中的页表即可从高速缓存直接获得地址数据,因为大大加速了地址转换。

   1、与CPU交互

    IA-32体系结构在将虚拟地址映射到物理地址时,只使用了两级页表。而64位体系结构,地址空间比较大,需要三级或者四级的页表,内核与体系结构无关的部分总是假定使用四级页表。

   2、内存映射

    内存映射是一种重要的抽象的手段,在内核中大量使用,也可以用于用户应用程序。映射方法可以将任意来源的数据传输到进程的虚拟地址空间中。作为映射目标的地址空间区域,可以像普通内存那样用通常的方法访问。但任何修改都会自动传输到原数据源。这样就可以使用相同的函数来处理完全不同的目标对象。例如,文件的内容可以映射到内存中。处理只需读取相应的你村即可访问文件内容,或向内存写入数据来修改文件的内容。内核将保证任何修改都会自动同步到文件中。

    内核在实现设备驱动程序时直接使用了内存映射。外设的输入、输出可以映射到虚拟地址空间的区域。对相关内存区域的读写会由系统重定向到设备,因而大大简化了驱动程序的实现。

 

1.3.5 物理内存的分配

    在内核分配内存时,必须记录页帧的已分配或者空闲状态,以免两个进程使用同样的内存区域。由于内存分配和释放非常频繁,内核必须保证相关操作即尽快完成。

   1、伙伴系统

    内核中很多时候要求分配连续页。为快速检测内存中的连续区域,内核采用了一种古老而历经检验的技术:伙伴系统。

   系统的空闲内存块总是两两分组,每组中的两个内存块称作伙伴。伙伴的分配可以是彼此独立的。但是如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存卡,作为下一层次上某个内存块的伙伴。

   内核对所有大小相同的伙伴,都放置到同一个列表中管理。各有8页的一对伙伴也在相应的列表中。

   如果系统需要8个页帧,则将16个页帧组成的块拆分成两个伙伴。其中一块用于满足应用程序的请求,而剩余的8帧则放置到对应8页大小内存块的列表中。

   在应用程序释放内存时,内核可以直接检查地址,来判断是否能够创建一组伙伴,并合并为一个更大的内存块放到伙伴列表中,这刚好是内存块分裂的逆过程。

   存在一个碎片的内存管理问题 ,频繁的分配和释放页帧也可能导致一种情况,系统中有若干页帧是空闲的,但却散步在物理地址空间的各处,换句话来说,系统中缺乏连续页帧组成的较大的内存块,而从性能考虑来看,却又很需要使用较大的连续内存块。通过伙伴系统可以在某种程序减少这种效应,但无法消除。

   2、slab缓存

   内核本身经常需要比完整页帧小得多的内存块,由于内核无法使用标准库的函数,因而必须在伙伴系统基础上自行定义额外的内存管理层,将伙伴系统提供的页划分为更小的部分。该方法不仅可以分配内存,还为频繁使用的小对象实现了一个一般性的缓存——slab缓存。它可以用两种方法分配内存。

   (1) 对频繁使用的对象,内核定义了只包含了所需类型对象实例的缓存。每次需要某种对象时,可以从对象的缓存快速分配,slab缓存自动维护与伙伴系统的交互,在缓存用尽时会请求新的页帧。

   (2)对通常的情况下内存块的分配,内核针对不同大小的对象定义了一组slab缓存,可以像用户空间编程一样,用相同的函数访问缓存。不同之处是这些函数都增加了前缀k,表明示内核相关的:kmalloc和kfree。

   (在嵌入式系统,slab分配器的开销可能太大)


   3、页面交换和页面回收

    页面交换通过利用磁盘空间称作扩展内存,从而增大了可用的内存。在内核需要更多内存时,不经常使用的页可以写入硬盘。如果需要访问相关数据,内核会将相应的页切换回内存。通过缺页异常机制,这种切换操作对应对应用程序是透明的。换出的也可以通过特别的页表项标识。在进程视图访问此类页帧时,CPU则启动一个可以被内核截取的缺页异常。此时内核可以将硬盘上的数据切换到内存中,接下来用户进程可以恢复运行,由于进程无法感知到缺页异常,所以页的换入和换出对进程是完全不可见的。

   页面回收用于内存映射被修改的内容与底层的块设备同步,为此有时也简称为数据回写,数据刷出后,内核即可将页帧用于其他用户,内核的数据结构包含了与此相关的所有信息,当再次需要改数据时,可根据相关信息从硬盘找到相应的数据并加载。

   

1.3.6计时

    内核必须能够测量时间以及不同时间点的时差,进程调度就会用到该功能。jiffies是一个合适的时间坐标。名为jiffies_64和jiffies的全局变量,会按恒定的时间间隔递增。在底层硬件允许的前提下,内核可使用高分辨率的定时器提供额外的计时手段,能够以纳秒级的精确度和分辨率来计量时间。

   

1.3.7 系统调用

    系统调用时用户进程与内核交互的经典方法。POSIX标准定义了许多系统调用,以及这些系统调用在所有遵从POSIX的系统包括Linux上的语义。传统系统调用按不同类别分组,如下所示:

   进程管理:创建新进程、查询信息,调试

   信号:发送信号,定时器以及相关处理机制。

   文件:创建、打开和关闭文件,从文件读取和向文件写入,查询信息和状态。

   目录和文件系统:创建、删除和重命名目录,查询信息,链接,变更目录。

   保护机制:读取和变更UID、GID,命名空间的处理。

    定时器函数:定时器函数和统计信息。

    对所有的处理器来说,一个共同点就是:用户进程从用户状态切换到核心态,并将系统关键任务委派给内核执行,系统调用时必由之路。

   

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

    按照经典的UNIX箴言:一切皆文件,对外设的访问可利用/dev目录下的设备文件来完成,程序对设备的处理完全类似于常规文件,设备驱动程序的任务在于支持应用程序经由设备文件与设备通信,换言之,使得能够按照适当的方式在设备商读取/写入数据:

   字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。

   块设备:与应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是个典型的例子。

  

1.3.9 网络

     网卡也可以通过设备字符驱动程序控制,但是内核中属于特殊状况,因为网卡不能利用设备文件访问。原因在于在网络通信期间,数据打包到了各种协议层中,在接收到数据时,内核必须针对各协议层的处理,对数据进行拆包与分析,然后才能将有效数据传递给应用程序,在发送数据时,内核必须首先根据各个协议层要求打包数据,然后才能发送。

   为支持通过文件接口处理网络连接,Linux使用了源于BSD的套接字抽象。套接字可以看做应用程序,文件接口,内核的网络实现之间代理。


1.3.10 文件系统

   Linux系统由数以千计乃至百万计的文件组成,其数据存储在硬盘或者其他块设备,存储使用了层次式,文件系统,文件系统使用目录结构组织存储的数据,并将其他元信息与实际数据关联起来。Linux支持许多不同的文件系统:标准的是EXT2和EXT3(目前新版的是采用EXT4),ReiserFS,XFS,VFAT,还有其他很多不同的文件系统。EXT2基于inode,即它对每个文件都构造了一个单独的管理结构,称为indoe,并存储到磁盘上,inode包含了文件所有元信息,以及指向相关数据块的指针。目录可以表示为普通文件,其数据包包括了指向目录下所有文件的inode的指针,因而层次结构得以建立。

   内核必须提供一个额外的软件层,将各种底层文件系统的具体特性与应用层(和内核自身)隔离开来。该软件层称为VFS(Virtual Filesystem Switch,虚拟文件系统)。VFS既是向下的接口,也是向上的接口,用户进程通过系统调用最终能访问文件系统功能。

  


1.3.11 模块和热插拔

    模块用于在运行时动态地向内核添加功能,如设备驱动程序、文件系统、网络协议等,实际上内核的任何子系统几乎都可以模块化,这消除了宏内核和微内核相比一个重要的不利之处。

   模块还可以在运行时从内核卸载,这在开发新的内核组件很有用。

   模块在本质上不过是普通程序,只是在内核空间而不是在用户空间执行而已。模块必须提供某些代码段在模块初始化时执行,以便向内核注册和注销模块。另外,模块代码与普通内核代码的权利都是相同的,可以像编译到内核中的代码一样,访问内核中所有的函数和数据。

 

1.3.12缓存

    内核使用缓存来改进系统性能,从低速的块设备读取的数据会暂时保持在内存中,即使数据在当时已经不再需要了,在应用程序下一次访问该数据时,它可以从访问速度较快的内存中读取,因而绕过了低速的块设备,由于内核是通过基于页的内存映射来实现访问设备的,因此缓存也按页组织,也就是说整页都缓存起来,故称之为页缓存。

   

1.3.13链表处理

    内核提供标准链表可用于将任何类型的数据结构彼此链接起来。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
struct list_head
{
    struct list_head *next, *prev;
};
//该成员如下放置在数据结构中
struct task_struct
{
    ...
    struct list_head run_list;
    ...
};

1.3.14 对象管理和引用计数

    内核采用了一般性的方法来管理内核对象。所引入的框架并不只是为了防止代码复制,同时也为内核不同部分管理的对象提供了一致的视图,在内核许多部分可以有效的使用相关信息,如电源管理。

    一般性的内核对象机制可用于执行下列对象操作:

    引用计数

   管理对象链表

   集合加锁

   将对象属性导出到用户空间

  1、一般性的内核对象

 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
struct kojbect
{
    const char *k_name;   //对象文本名称
    struct kref kref;     //引用计数的管理
    struct list_head entry;  //将若干kobject放置到一个链表中
    struct kojbect *parent;  //指向父对象的指针
    struct kset *kset;       //将对象与其他对象放置到一个集合
    struct kobj_type *ktype;  //用于释放该数据结构资源的析构器函数
    struct sysfs_dirent *sd;  //
};

1.3.15 数据类型

    内核使用typedef来定义各种数据类型,如__s8和__u8分别是有符号的。


1.4 内核是特别的

    内核很神奇,但归根到底它只是一个很大的C程序,带有一些汇编代码。在必要的情况下,内核会以上下文相关的方式重用比特位置,多次重载结构成员,从指针已经对齐的部分压榨又一存储位,自由的使用goto语句,还有其他东西。

   调试内核通常要比调试用户层程序困难。

   内核提供了许多辅助函数,类似于用户空间的C语言程序库,但内核领域中的东西总是朴素的多。

   用户层应用程序的错误可能导致段错误(segment fault)或者内存转存(core dump),但是内核错误会导致整个系统挂载。

   必须考虑到内核运行的许多体系结构根本不支持非对齐的内存访问。

  所有内核代码都必须是并发安全的。

  

0 0
原创粉丝点击