[深入 Linux 内核架构][笔记] 一、基本概述

深入 Linux 内核架构

作者:解琛
时间:2020 年 8 月 27 日

一、基本概述

我选择的是 Linux 2.6.24 版本的内核进行研究。

1.1 内核的任务

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

  • 从应用程序的视角看,内核是一台增强的计算机 。应用程序和硬件本身没有联系,只与内核有联系,内核是应用程序所知道的层次结构中的最底层。
  • 若干程序在系统中并发运行时,内核是一个资源管理程序 。负责将可用共享资源分配到各个系统进程中,并保证系统的完整性。
  • 内核是一个库 ,提供一组面向系统的命令。

1.2 实现策略

在操作系统实现方面,有以下两种范性。

1.2.1 微内核

基本功能直接由中央内核 实现,其他功能交给独立进程,进程通过明确定义的通信接口和中心内核通信。

1.2.2 宏内核

内核的全部代码,包括所有子系统都打包到一个文件中,内核中的每个函数都可以访问内核中的其他部分。

1.3 内核的组成部分

Linux 使用的整体式的宏内核架构。

内核的组成部分

1.3.1 进程、进程切换、调度

UNIX 操作系统下运行的应用程序、服务器以及其他程序都成为进程 。系统中真正运行的进程数目不超过 CPU 数目。

  • 内核需要借助 CPU 的帮助,负责进程切换。
  • 内核需要确定现存进程之间共享 CPU 的时间。

进程之间的切换称为进程切换 ,确定哪个进程运行多长时间的过程称为调度

1.3.2 UNIX 进程

Linux 对进程采用了一种层次结构,每一个进程都依赖于一个父进程。

内核启动 init 程序作为第一个进程。该进程负责进一步的系统初始化操作,并显示登录提示符或图形登录界面,init 是进程树的根。

我当前使用的是 Ubuntu18.04,使用 pstree 指令可以查看系统的进程树如下。

xiechen@xiechen-Ubuntu:/$ pstree
systemd─┬─ModemManager───2*[{ModemManager}]
        ├─NetworkManager─┬─dhclient
        │                └─2*[{NetworkManager}]
        ├─2*[VBoxClient───VBoxClient]
        ├─2*[VBoxClient───VBoxClient───2*[{VBoxClient}]]
        ├─VBoxClient───VBoxClient───3*[{VBoxClient}]
        ├─VBoxService───8*[{VBoxService}]
        ├─accounts-daemon───2*[{accounts-daemon}]
        ├─acpid
        ├─apache2───5*[apache2]
        ├─avahi-daemon───avahi-daemon
        ├─boltd───2*[{boltd}]
        ├─colord───2*[{colord}]
        ├─containerd───12*[{containerd}]
        ├─cron
        ├─cups-browsed───2*[{cups-browsed}]
        ├─cupsd───2*[dbus]
        ├─2*[dbus-daemon]
        ├─dockerd───11*[{dockerd}]
        ├─fcitx───{fcitx}
        ├─fcitx-dbus-watc
        ├─fwupd───4*[{fwupd}]
        ├─gdm3─┬─gdm-session-wor─┬─gdm-wayland-ses─┬─gnome-session-b─┬─gnome-shell─┬─Xwayland───6*[{Xwayland}]
        │      │                 │                 │                 │             └─17*[{gnome-shell}]
        │      │                 │                 │                 ├─gsd-a11y-settin───3*[{gsd-a11y-settin}]
        │      │                 │                 │                 ├─gsd-clipboard───8*[{gsd-clipboard}]
        │      │                 │                 │                 ├─gsd-color───9*[{gsd-color}]
        │      │                 │                 │                 ├─gsd-datetime───2*[{gsd-datetime}]
        │      │                 │                 │                 ├─gsd-housekeepin───2*[{gsd-housekeepin}]
        │      │                 │                 │                 ├─gsd-keyboard───9*[{gsd-keyboard}]
        │      │                 │                 │                 ├─gsd-media-keys───9*[{gsd-media-keys}]
        │      │                 │                 │                 ├─gsd-mouse───2*[{gsd-mouse}]
        │      │                 │                 │                 ├─gsd-power───3*[{gsd-power}]
    ......

树结构的拓展方式和新进程的创建方式密切相关。UNIX 中有 fork 和 exec 两种创建新进程的机制。

1.3.2.1 fork

fork 可以创建当前进程的一个副本,父进程和子进程只有PID(进程 ID)不同。

在该系统调用执行后,系统中有两个进程,都执行相同的操作,父进程的内容会被复制。

Linux 使用写时复制(copy on write的方式让 fork 操作更加高效。其原理是:将内存复制操作延迟到父进程或子进程向某内存页面写入数据之前,在只读访问的情况下,父进程和子进程都可以共用同一个内存页。

1.3.2.2 exec

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

1.3.2.3 线程

有无线程的进程对比

内核支持的程序执行形式除了 重量级进程 (UNIX 进程),还有一种程序执行形式是 线程 (轻量级进程)。

本质上一个进程可能由若干个线程组成,这些线程共享同样的数据和资源,但可能执行程序中不同的代码路径。

简言之,进程可以看作是一个正在执行的程序,而线程则是与主程序并行运行的程序函数或例程。

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

1.3.2.4 命名空间

传统的 Linux 中有许多全局变量,启用命名空间后,以前的全局资源现在具有不同的分组。每个命名空间可以包含一个特定的 PID 集合,可以提供文件系统的不同视图,在某个命名空间挂载的卷不会传播到其他的命名空间中。

1.3.3 地址空间与特权级别

约定:

容量单位字节数
KiB 2 10 2^{10} 210
MiB 2 20 2^{20} 220
GiB 2 30 2^{30} 230

内存区域是通过指针进行寻址,CPU 的字长决定了所能管理的地址空间的最大长度。

32 位系统为 2 32 = 4 2^{32}=4 232=4 GiB,64 位最多管理 2 64 = 8 2^{64}=8 264=8 GiB。

地址空间的最大长度与实际可用的物理内存数量无关,被称为 虚拟地址空间

Linux 讲虚拟地址空间划分为两个部分,内核空间和用户空间。

虚拟地址空间的划分

系统中每个用户进程都有自身的虚拟地址范围,从 0 到 TASK_SIZE,用户空间之上的区域保留给内核专用。

TASK_SIZE 是一个特定于计算机结构体系结构的常数,把地址空间按给定比例划分为两部分。

这种划分与可用的内存数量无关,各个系统进程的用户空间是完全彼此分离的,而虚拟地址空间顶部的内核空间总是同样的。

64 位的计算机实际使用的位数一般小于 64 位,如 42 位或 47 位,地址空间中可寻址的部分小于理论长度,该值大于计算机上实际可能的内存数量,因此是完全够用的。

1.3.3.1 特权级别

所有现代的 CPU 都提供了几种特权级别,进程可以驻留在某一特权级别,每个特权级别都有各种限制。

特权级别的环状系统

英特尔处理器区分 4 种特权级别,Linux 只使用两种不同的状态,核心态和用户态

种状态的关键区别在于对高于 TASK_SIZE 的内存区域的访问。用户状态禁止访问内核空间。用户进程不能操作或读取内核空间中的数据,无法执行内核空间中的代码。

内核还可以由异步硬件中断激活,然后在中断上下文中运行。与进程上下文中运行的区别是,在中断上下文中运行时,不能访问虚拟地址空间中的用户空间部分。因为中断可能随时发生,中断发生时可能是任一用户进程处于活动状态,由于该进程基本上与中断的原因无关,因此内核无权访问当前用户空间的内容。内核不能进入睡眠状态。

内核中还有 内核线程 在运行,内核线程不与任何特定的用户空间进程相关联,无权处理用户空间。内核可以进入睡眠状态,可以像普通进程一样被调度器跟踪。其可用于各种用途:从内存和块设备之间的数据同步、帮助调度器在 CPU 分配进程。

在内核态和用户状态执行,CPU 大部分时间都在执行用户空间中的代码,当应用程序执行系统调度的时候,切换到核心态,内核将完成其请求,在此期间,内核可以访问虚拟地址空间的用户部分。在系统调用完成之后,CPU 切换回用户状态。硬件中断也会使 CPU 切换到核心态,该情况下内核不能访问用户空间。

系统调用关系

通过指令 ps fax 可以快速查看内核线程,其名称都置于方括号内。

xiechen@xiechen-Ubuntu:~/workLog/githubPhotoes/0.课外学习的书籍/0.深入Linux内核架构$ ps fax
  PID TTY      STAT   TIME COMMAND
    2 ?        S      0:00 [kthreadd]
    3 ?        I<     0:00  \_ [rcu_gp]
    4 ?        I<     0:00  \_ [rcu_par_gp]
    6 ?        I<     0:00  \_ [kworker/0:0H-kb]
    9 ?        I<     0:00  \_ [mm_percpu_wq]
   10 ?        S      0:00  \_ [ksoftirqd/0]
   11 ?        I      0:04  \_ [rcu_sched]
   12 ?        S      0:00  \_ [migration/0]
   13 ?        S      0:00  \_ [idle_inject/0]
   14 ?        S      0:00  \_ [cpuhp/0]
   15 ?        S      0:00  \_ [cpuhp/1]
   16 ?        S      0:00  \_ [idle_inject/1]
   17 ?        S      0:00  \_ [migration/1]
   18 ?        S      0:00  \_ [ksoftirqd/1]
   20 ?        I<     0:00  \_ [kworker/1:0H-kb]
   21 ?        S      0:00  \_ [cpuhp/2]
   22 ?        S      0:00  \_ [idle_inject/2]
   23 ?        S      0:00  \_ [migration/2]
   24 ?        S      0:00  \_ [ksoftirqd/2]
   26 ?        I<     0:00  \_ [kworker/2:0H-kb]

在多处理器系统上,很多线程启动时都指定了 CPU,并限制只能在某个特定的 CPU 上运行,从内核线程名称后面的斜线和 CPU 编号都可以看到这一点。

1.3.3.2 虚拟和物理地址空间

在大多数情况下,单个虚拟地址空间要比系统的物理空间要大,因此内核和 CPU 需要考虑如何将实际可用的物理内存映射到虚拟地址空间的区域。

使用 页表 来为物理地址分配虚拟地址。虚拟地址关系到进程的用户空间和内核空间,物理地址用来寻址实际可用的内存。

虚拟地址空间被内核划分为许多等长的部分,称为 ,物理内存也被划分为同样大小的页。不同进程的同一虚拟地址实际上具有不同的意义。

物理内存页通常称为 页帧,相比而言,页专指虚拟地址空间中的页。

内核负责将虚拟地址空间映射到物理地址空间,因此可以决定哪些内存区域在进程之间共享,哪些不共享。

并非虚拟地址空间的所有页都映射到某个页帧。有以下几种情况。

  • 页没有使用
  • 数据尚不需要使用而没有载入内存中
  • 页已经换出硬盘,在需要时换回内存

称呼用户程序时有两个等价的名词:

  1. 用户层(userland)
  2. 某个程序在用户空间运行

1.3.4 页表

页表 是指用来将虚拟地址空间映射到物理地址空间的数据结构。

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

这里将虚拟地址划分为 4 个部分,需要一个 3 级页表。Linux 采用的是四级页表。

分配虚拟地址

第一部分称为 全局页目录(Page Global Directory, PGD) ,用于索引进程中有且仅有一个的数组,该数组是所谓的全局页目录 PGD。

第二个部分为 中间页目录(Page Middle Directory, PMD) ,其也是一个数组,PGD 指向 PMD。PGD 通过数组项找到对应的 PMD 之后,则开始使用 PMD 来索引,PMD 的数组项也是指针,指向下一级数组,称为页表或者页目录。

第三个部分为 页表数组(Page Table Entry),用于页表的索引。虚拟内存页和页帧之间的映射就此完成,页表数组项指向页帧。

第四个部分为 偏移量 ,其指定了页内部的一个字节位置。

总之,每个地址都指向地址空间中唯一定义的某个字节。

多级页表的优点为:对虚拟地址空间不需要的区域,不必创建中间页目录或页表,节省了大量空间。

多级页表的缺点为:每次访问内存时,必须主机访问多个数组才能将虚拟地址转化为物理地址。

针对多级页表的缺点,CPU 通过以下两种方式进行加速。

  1. 通过 内存管理单元(Memory Mannagement Unit, MMU) 优化内存访问操作。
  2. 将地址转换出现的最频繁的地址,保存到 地址转换后备缓冲器(Translation Lookaside Buffer, TLB) 的 CPU 高速缓存中。无需访问内存中的页表即可从高速缓存直接获得地址数据,因而大大加速了地址的转换。
1.3.4.1 与 CPU 的交互

IA-32 体系结构使用了两级页表,64 位体系地址空间比较大,需要三级或者四级的页表来实现。内核与体系结构无关的部分,总是假定使用四级页表。内存管理代码剩余部分的实现与 CPU 无关。

1.3.4.2 内存映射

内存映射可以将任意来源的数据传输到进程的虚拟地址空间中。作为映射目标的地址空间区域,可以像普通内存一样访问,但修改会自动传输到原始数据源。

内存映射可以实现用相同的函数处理不同的目标对象。内核在实现设备驱动程序时直接使用了内存映射,外设的输入输出可以直接映射到虚拟内存地址空间的区域中。

对相关的内存区域的读写会由系统重定向到设备,大大简化驱动程序的实现。

1.3.5 物理内存的分配

在内核分配内存时,必须记录页帧的已分配或空闲状态,以免两个进程使用同样的内存区域。

内存的分配和释放频繁,内核必须保证操作的实时性。

内核可以只分配完整的页帧,通过用户空间中的标准库来将内存划分为更小的区域。

标准库将来源于内核的页帧拆分成小的区域,并为进程分配内存。

1.3.5.1 伙伴系统

内核多数时候要求分配连续页,内核使用 伙伴系统 来快速检测内存中的连续区域。

在这里插入图片描述

系统中的空闲内存块总是两两分组,每组中的两个内存块称为 伙伴

伙伴的分配可以是彼此独立的。

如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存块,作为下一层次上某个内存块的伙伴。

内核对所有大小相同的伙伴都放置到同一个列表中进行管理。

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

如果下一个请求只需要 2 个连续页帧,则由 8 页组成的块会分裂成 2 个伙伴,每个伙伴包含 4 个页帧。其中一块放回伙伴列表中,而另一个再次分裂成 2 个伙伴,每个包含 2 页。其中 1 个回到伙伴系统,另一个传递给应用程序。

在系统运行时,服务器运行好几个星期乃至几个月是很正常的,许多桌面系统也趋于长期开机运行,那么会发生称为 碎片 的内存管理问题。

1.3.5.2 slab 缓存

内核本身需要使用比完整页帧小得多的内存块。

slab与伙伴系统的关系

由于内核无法使用标准库的函数,slab 缓存 在伙伴系统的基础上自行定义额外的内存管理层,将伙伴系统提供的页划分为更小的部分。

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

slab 缓存有两种方法分配内存:

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

  2. 对通常情况的小内存块的分配,内核针对不同大小的对象定义了一组 slab 缓存,可以用相同的函数访问这些缓存。(kmalloc、kfree)

对于超级计算机,slab 存在伸缩性的问题,对嵌入式系统来说,slab 分配器开销过大。

1.3.5.3 页面交换和页面回收

页面交换通过利用磁盘空间作为拓展内存,增大了可用的内存。

在内核需要更多内存时,不经常使用的页可写入硬盘,再需要访问相关数据时,内核会将相应的页切换回内存。

通过 缺页异常 机制,这种切换操作对应的应用程序是透明的。

进程无法感知缺页异常,所以页的换入和换出对进程是不可见的。

页面回收 用于将内存映射被修改的内容和底层的块设备同步,也称 数据会写

1.3.6 计时

内核必须能够测量时间及其不同时间点的时差。

jiffies 是一个合适的时间坐标。jiffies_64 和 jiffies(64 位和 32 位)的全局变量,会按恒定的时间间隔递增。

每种计算机底层体系结构都提供了一些执行周期性操作的手段,通常的形式是 定时器中断

jiffies 的递增频率同体系结构有关,取决于内核一个重要的常数 Hz [100 - 1000]。

基于 jiffies 的计时相对粒度较粗,内核可以使用高分辨率的定时器提供额外的计时手段,能够以纳秒级别的精确度和分辨率来计量时间。

内核计时的周期是可以动态改变的。

1.3.7 系统调用

系统调用 是用户进程和内核交互的经典方法。

POSIX 标准定义了许多系统调用,以及这些系统调用在所有遵从 POSIX 的系统,包括 Linux 上的语义。

传统的系统调用按不同的类别进行分组:

  1. 进程管理。创建新进程、查询信息、调试;

  2. 信号。发送信号,定时器以及相关的处理机制;

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

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

  5. 保护机制。读取和变更 UID / GID,命名空间的处理;

  6. 定时器函数和统计信息。

所有的这些函数都对内核提出了要求。

这些函数需要特别的保护机制来保证系统稳定性或安全不受危及。

许多调用依赖内核内部的结构或函数来得到所需的数据或结果,无法在用户空间来实现。

在发出系统调用时,处理器必须改变特权级别,从用户态切换到核心态,并将系统关键任务委派给内核执行,系统调用是必由之路。

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

设备驱动程序用于与系统连接的输入 / 输出装置通信。

对外设的访问利用 /dev 目录下的设备文件来完成。程序对设备的处理,完全类似于常规的文件。

外设可分为以下两类:

  1. 字符设备。提供连续的数据流;

  2. 块设备。应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。块设备不支持基于字符的寻址。

编写块设备驱动程序比字符设备复杂得多,因为内核为提高系统性能广泛地使用了缓存机制。

1.3.9 网络

网卡可以通过设备驱动程序控制,但不能利用设备文件访问,在内核中属于特殊情况。

网络通信期间,数据打包到了各种协议层中,在接收到数据时,内核必须针对各协议层的处理,对数据进行拆包和分析,才能将有效数据传递给应用程序。

在发送数据时,内核必须首先根据各个协议层的要求对数据进行打包,然后才能发送。

为支持通过文件接口处理网络链接(应用程序的观点),Linux 使用了源于 BSP 的套接字 抽象。

套接字可以理解为应用程序、文件接口、内核的网络实现之间的代理。

1.3.10 文件系统

Linux 的存储使用了层次式文件系统。

Linux 支持许多不同的文件系统:Ext2、Ext3、ReiserFS、XFS、VFAT(兼容DOS)等。

文件系统使用目录结构组织存储的数据,将其他元信息与实际数据关联起来。

文件系统

内核提供一个额外的软件层 VFS(Virtual Filesystem 或 Virtual Filesystem Switch),将各种底层文件系统的具体特性与应用层、内核隔离开来,即虚拟文件系统或虚拟文件系统交换器。

1.3.11 模块和热插拔

模块用于在运行时动态地向内核添加功能,如设备驱动程序、文件系统、网络协议等。

内核的任何子系统都可以模块化,这消除了宏内核和微内核相比的一个重要不利之处。

模块可以在运行时从内核卸载。

模块的本质是一个在内核空间运行的普通程序,可以访问内核中所有的函数和数据。

对支持热插拔而言,模块在本质上是必需的。某些总线(USB 和 FireWire)允许在系统运行时连接设备,而无需系统重启。

在系统检测到新的设备时,通过加载对应的模块,可以将必要的驱动程序自动添加到内核中。

加载只提供二进制代码的模块会污染内核,很多内核开发者认为它们是邪恶的化生。

1.3.12 缓存

内核使用缓存来改善系统性能。

从低速的块设备中读取的数据会暂时保存在内存中,下一次应用程序访问时,可以直接绕过低速的块设备,直接从内存中进行读取。

内核是通过基于页的内存映射来实现访问块设备的,因此缓存也按页组织,即页缓存(page cache)。

块缓存用于缓存没有组织成员的数据,已经被页缓存取代。

1.3.13 链表处理

内核提供的标准链表可用于将任何类型的数据结构彼此链接起来。加入链表的数据结构必须包含一个类型为 list_head 的成员,其中包含了正向和反向指针。

在这里插入图片描述

这种链表的第一个和最后一个元素都能达到 O ( 1 ) \mathbb O(1) O(1) 的访问时间,不管链表的大小如何,访问这两个元素花费的时间是一个常数。

struct list_head 被称为 链表元素,用作链表起点的元素被称为 表头

链表的标准处理函数如下。

  1. list add(new , head) 用于现存的 head 元素之后紧接着插入 new 元素;

  2. list_add_tail(new, head) 用于在 head 元素之前,紧接着插入 new 元素;

  3. list_del(entry) 从链表中删除一项;

  4. list_empty(head) 检测链表是否为空;

  5. list_splice(list, head) 负责合并两个链表,把 list 插入到另一个现存链表的 head 元素之后;

  6. list_entry(prt, type, member) 查找链表元素;

  7. list_for_each(pos, head) 用于遍历链表所在的元素。

1.3.14 对象管理和引用计数

内核中需要跟踪记录 C 语言中的结构的实例。这会导致代码复制。

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

  1. 引用计数;

  2. 管理对象链表(集合);

  3. 集合加锁;

  4. 将对象属性导出到用户空间(通过 sysfs 文件系统)。

1.3.14.1 一般性的内核对象

kobject 将嵌入其他数据结构,用作内核对象的基础。

/* kobject.h */

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 不是通过指针与其他数据结构连接起来的,必须直接嵌入。通过管理 kobject 达到了对包含 kobject 对象的管理。

由于 kobject 结构会嵌入到内核的许多数据结构中,需要保持该结构较小。

  • k_name 是对象的文本名称;
  • kref 用于简化引用计数的管理;
  • entry 是一个标准的链表元素,用于将若干 kobject 放置到一个链表中(集合);
  • kset 用于将对象和其他对象放置到同一个集合;
  • parent 是一个指向父对象的指针,用于在 kobject 之间建立层次关系;
  • ktype 提供了包含 kobject 的数据结构的更多详细信息,最重要的是用于释放该数据结构资源的析构器函数;
  • sd 用于支持内核对象与 sysfs 之间的关联;

kobject 抽象实际上提供了在内核中使用面向对象计数的可能性,而无需 C++ 的所有额外机制(二进制代码大小的膨胀和额外的开销)。

函数说明
kobject_get对 kobject 的引用计数器加 1;
kobject_put对 kobject 的引用计数器减 1;
kobject_register从层次结构中注册对象,同时在 sysfs 文件系统中创建一个对应项;
kobject_unregister从层次结构中删除对象,同时在 sysfs 文件系统中删除一个对应项;
kobject_init初始化一个 kobject,即将引用计数器设为初始值,初始化对象的链表元素;
kobject_add初始化一个内核对象,并使之显示在 sysfs 中;
kobject_cleanup在不需要 kobject 对象时释放分配的资源。
1.3.14.2 对象集合

多数情况下,需要将不同的内核对象归类到集合中。

kset 用于提供集合功能。

/* <kobject.h> */

struct kset
{
    struct kobj_type        * ktype;
    struct list head          list;
    ...
    struct kobject            kobj;
    struct kset_uevent_ops  * uevent_ops;
};

由于管理结合的结构只能是内核对象,kobj 和集合中其他的 kobject 无关,只是用来管理 kset 自身。

  • ktype 指向 kset 中各个内核对象公用的 kobj_type 结构;
  • list 是所有属于当前集合的内核对象的链表;
  • uevent_ops 提供了若干函数指针,用于将集合的状态信息提供给用户层;

kobj_type 用于描述内核对象的共同特性。

/* <kobject.h> */

struct kobj_type()
{
    ...
    struct sysfs_ops    * sysfs_ops;
    struct attribute    **default_attrs;
};

该结构提供了与 sysfs 文件系统的接口。

1.3.14.3 引用计数

引用计数用于检测内核中有多少地方使用了某个对象。

当内核的一个部分需要某个对象所包含的信息时,引用计数加 1,不需要时减 1,在计数下降到 0 后,从内存中释放该对象。

内核提供了下列数据结构处理引用计数。

/* <kref.h> */

struct kref
{
    atomic_t refcount;
};

提供了一个一般性的原子引用计数,原子 指的是,在对该变量的加 1 减 1 操作在多处理器系统上是安全的。

kref_initkref_getkref_put 用于对应用计数器进行初始化、加 1 和减 1 的操作。

这些函数有利于避免过度的代码复制。

1.3.15 数据类型

1.3.15.1 类型定义

内核通过 typedef 来定义各种数据类型,避免依赖于体系结构相关的特性。

1.3.15.2 字节序

为表示数字,现代计算机采用大端序(big endian)或小端序(little endian)格式。

字节序

在大端序格式中,最高有效字节存储在最低地址,而随着地址升高,字节的权重降低。

在小端序格式中,最低有效字节存储在最低地址,而随着地址升高,字节的权重升高。

一些体系结构(MIPS)支持两种字节序。

内核提供了各种函数和宏,可以在 CPU 使用的格式与特定的表示法之间转换。

cpu_to_le64 将 64 位数据类型转换为小端序格式,而 le64_to_cpu 则相反。

对 64 位、32 位和 16 位的数据类型,所有的小端序、大端序之间的转换例程都是可用的。

1.3.15.3 pre-cpu 变量

通过 DEFINE_PRE_CPU(name, type) 进行声明,其中 name 是变量名,type 为数据类型。

在有若干 CPU 的 SMP 系统上,会为每个 CPU 分别创建变量的一个实例,用于某个特定 CPU 的实例可以通过 get_cpu(name, cpu) 获得,其中 smp_processor_id() 可以返回当前活动处理器的 ID,用作 cpu 的参数。

采用 pre-cpu 变量,所需数据很可能存在与处理器的缓存中,因此可以更快速地访问。若在多处理器系统中使用,可能会被所有的 CPU 同时访问的变量,可能引发一些通信方面的问题,采用 per-cpu 的概念可以绕过这些问题。

1.3.15.4 访问用户空间

源码中多处指针都标记为 __user,该标识符对用户空间程序设计是未知的。

内核使用该记号来标识指向用户地址空间中区域的指针,在没有进一步预防措施的情况下,不能轻易访问这些指针指向的区域。

这是因为内存是通过页表映射到虚拟地址空间的用户控件部分的,而不是物理内存直接映射。因此内核必须确保指针所指向的页帧确实存在在物理内存中。

通过显式标记,可以支持自动检查工具(sparse)来确认实际上遵守了必要的条件。

1.3.16 内核代码增长情况

内核代码增长情况

内核开发是一个高度动态的过程,内核获得新特性和持续改进的速度是惊人的。

Linux 基金会的研究结果(KHCM)显示,每次内核发布,大约会加入 10000 个补丁,每次发布所添加的大量代码是由接近 1000 名开发者完成的。

平均下来,1 天 24 小时,1 周 7 天,每小时集成到内核的修改有 2.83 处之多。

这通过成熟的源代码管理和开发者之间的沟通来达成。

1.3.17 内核的特别之处

Linux 内核结构良好,细节一丝不苟,巧妙的解决方案在代码中处处可见,内核应该是什么样子,它现在就是什么样子。

在必要的情况下,内核会以上下文相关的方式重用比特位置,多次重载结构成员,从指针已经对齐的部分压榨出存储位,自由地使用 goto 语句等。

内核的编写不同于用户程序的地方有:

  1. 调试内核要比调试用户层程序困难。对用户层程序来说,有大量的调试器可用,但对于内核调试来说,调试器的实现难度要高很多;

  2. 内核提供了许多辅助函数;

  3. 用户层应用程序的错误会导致段错误(segmentation fault)或内存转储(core dump),而内核错误会导致整个系统的故障;

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

  5. 所有的内核代码必须是并发安全的,Linux 内核代码必须是可重入和线程安全的;

  6. 内核代码必须在小端序和大端序的计算机上都能够工作;

  7. 大多数的体系结构根本不允许在内核中进行浮点计算,需要想办法用整形来代替。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

解琛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值