Linux 简介

引言

本文整理了 Linux 内核的相关知识,其他 Linux 相关文章均收录于 <Linux系列文章>

简介

内核的任务

内核是硬件与软件之间的一个中间层,它将计算机的硬件资源抽象到一个更高的层次上,帮助上层应用高效地使用资源,当若干个程序并发地运行时,内核扮演着一个资源管理器的角色,在这种情况下,内核将共享资源(CPU 时间,磁盘,网络等)分配到各个进程。

实现策略

操作系统的实现一般分为两个流派:

  • 微内核:最基本的功能由中央内核实现,其他功能将有一些独立的模块实现,这些模块可能包括(文件系统,内存管理等),理论上这种微内核的实现方案应该是最优的,它将系统的功能划分为不同的模块,各个模块独立的演化发展,再有中央内核控制各个模块的分工合作,但是由于各个模块之间的通讯需要太多的花销,导致微内核的方案一直发展缓慢。
  • 宏内核:和微内核相反,宏内核将所有操作系统的功能都打包在同一个文件中,不同模块的函数之间可以互相调用,目前来说,宏内核的性能是强于微内核的,Linux 也是依据宏内核的方案实现的。不过,在 Linux 中也有模块的概念,可以在系统运行时热插拔一些功能特性。

组成部分

尽管 Linux 是基于宏内核实现的,但是它在内部也对系统的不同功能模块进行了良好的划分,而不同功能模块之间难免有一些联系,下图概述了组成 Linux 系统的各个层面,以及内核中包含的一些重要子系统。
level-subsystem

进程

一般称在操作系统下运行的程序为进程,每个进程都有自己的虚拟地址空间,各个进程的地址空间是独立的,每个进程都会觉得自己是系统中唯一的一个进程。由于 Linux 是多任务系统,它支持并发执行若干进程,至少看上去是这样。我们知道,系统中同时真正运行的进程数目最多不超过 CPU 的数目,因此,内核会以一个较短的时间间隔在不同进程间切换执行,这就造成了所有进程都并行运行的假象。

内核会在进行进程切换时,将当前进程 A 的状态保存起来,然后再将 CPU 资源交给下一个进程 B 使用,一段时间后,当进程 A 需要重新执行时,就将之前保存的状态恢复原状,从而继续执行进程 A,这个过程就成为进程切换。

内核必须确定如何在现存进程之间共享 CPU 的运行时间,重要的程序多分配一些运行时间,次要的程序少分配一些,这个过程称之为进程调度。

Unix 进程

Linux 对进程采用了一种层级管理方式,每一个进程都依赖于一个父进程,除了内核启动的 Init 进程,该进程负责一部分系统初始化工作,并负责启动其他进程,所以其他进程都直接或间接地来源于它。
process-tree
Unix 操作系统中通过两个系统调用的配合,创建出一个新的进程:

  • fork: 创建一个当前进程的副本,父子进程只有进程 ID 不同,子进程会复制父进程的内存,不过为了效率更高,Linux 使用了众所周知的写时复制技术(将复制操作延迟到父进程或子进程向内存中写入数据之前)。
  • exec: 将一个新的程序加载到当前进程的内存中执行,旧程序的内存页会被刷出,其内容将替换为新的数据,然后开始执行新的程序。

进程并不是内核支持的唯一程序执行形式,除了重量级进程外,还有一种叫做线程的形式。一个进程可以由多个线程组成,这些线程共享了该进程的数据和资源,但是这些线程可能会执行在不同的代码路径上。之所以引入线程,是因为有些程序可能会需要多个代码执行流,而如果用多个进程来处理这个情况时又需要一个通信机制来交换数据。当使用线程来解决这个问题时,就不需要这部分通讯工作,因为所有的线程都共享该进程的数据和资源,它们只需要某种同步机制对资源进行互斥的访问即可。

Linux 通过 clone 方法创建线程,它的工作方式类似于 fork,但是它会进行精确的检查,以确认哪些资源与父进程共享,哪些资源为线程独立创建。
process-with-thread
在 Linux 中还有一个命名空间的概念,它使得不同的进程可以看到不同的系统视图,传统的 Linux 中使用了很多全局变量,例如进程 ID(PID)。系统中的每个进程都有一个唯一的 ID,我们可以通过该 ID 来访问进程,例如发送一个信号。启动命名空间后,以前的全局资源现在具有了不同的分组,每个命名空间内只能看到同组内的 PID 集合,除了 PID 之外,命名空间还可以对其他资源进行分组,比如文件系统,某个命名空间中挂载的卷不会在其他命名空间中被访问。

看过之前 Kubernetes 文章的同学可能很清楚命名空间的作用,因为容器技术就是利用了命名空间来建立系统的多个视图,每个容器内看起来都像是一个完整的计算机,但实际上这些虚拟的计算机视图可能从属于同一台物理机。通过这种方式,可以更加高效地使用系统资源,因为相较于完全的虚拟化技术(KVM),这种方案只需要运行一个内核就能管理所有的容器。不过,值得一提的是,并不是所有子系统都支持命名空间。

地址空间和特权空间

由于内存区域是通过指针寻址的,因此 CPU 的字长决定了所能管理的内存地址空间最大尺寸,对于 32 位的系统,是 4GB,对于 64 位的系统是 2^64 B。地址空间的最大长度和实际可用的物理内存大小是无关的,因此我们也经常称之为虚拟地址空间。从进程的角度来看,地址空间中只有自身这一个进程,无法感知到其他进程的存在。

Linux 将虚拟地址空间划分为两个部分,分别是内核空间和用户空间。系统中每个用户进程都有自己的虚拟地址范围,从 0 到 TASK_SIZE。用户之上的区域保留给系统内核使用,用户级进程无法访问。TASK_SIZE 是根据计算机体系结构确定的一个常数。在比较常见的 Intel 32 位体系(IA-32)中 TASK_SIZE 为 3GB,因此每个进程的虚拟地址空间为 3GB,内核空间为 1GB。

需要注意的是,这种划分是和可用内存的数量无关的,由于地址空间虚拟化的结果,每个用户进程都认为自身有 3GB 内存。各个进程的用户空间是完全彼此分离的。而虚拟地址空间顶部的内核空间总是同样的,无论当前执行的是哪个进程。

64 位计算机的情况可能更加复杂,因为如果使用完整的 64 位字长来描述地址空间,它所能描述的虚拟地址空间是巨大的,我们根本用不了这么大的空间,所以一般倾向于使用较短的位数来描述地址空间,比如 42 或者 47 位。这样做的优点是可以减少 CPU 的一些工作量。

内核把虚拟地址空间划分为两个部分,因此能够保护各个系统进程,使之彼此隔离。所有的现代 CPU 都提供了几种特权级别,进程可以驻留在某一特权级别。每个特权级别都有各种限制,例如对执行某些汇编语言指令或访问虚拟地址空间某一特定部分的限制。IA-32 体系结构使用 4 种特权级别构成的系统,各级别可以看作是环。内环能够访问更多的功能,外环则较少。
cpu-permission
尽管英特尔处理器区分 4 种特权级别,但 Linux 只使用两种不同的状态: 核心态和用户状态。两种状态的关键差别在于对高于 TASK_SIZE 的内存区域的访问,简而言之,在用户状态禁止访问内核空间。用户进程不能操作或读取内核空间中的数据,也无法执行内核空间中的代码。这是内核的专用领域。这种机制可防止进程无意间修改彼此的数据而造成相互干扰。

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

除了代表用户程序执行代码之外,内核还可以由异步硬件中断激活,然后在中断上下文中运行。与在进程上下文中运行的主要区别是,在中断上下文中运行不能访问虚拟地址空间中的用户空间部分。因为中断可能随机发生,中断发生时可能是任一用户进程处于活动状态,由于该进程基本上与中断的原因无关,因此内核无权访问当前用户空间的内容。在中断上下文中运行时,内核不能进入睡眠状态,因为中断上下文没有相应的进程描述符,所以无法被再次调度。

除了普通进程,系统中还有内核线程在运行。内核线程也不与任何特定的用户空间进程相关联,因此也无权处理用户空间。不过在其他许多方面,内核线程更像是普通的用户层应用程序,比如可以进入睡眠,也可以像普通进程那样被调度。内核线程可用于各种用途: 从内存和块设备之间的数据同步,到帮助调度器在 CPU 上分配进程。
cpu-invoke-switch
在核心态和用户状态执行。CPU 大多数时间都在执行用户空间中的代码。当应用程序执行系统调用时,则切换到核心态,内核将完成其请求。在此期间,内核可以访问虚拟地址空间的用户部分。在系统调用完成之后,CPU切换回用户状态。硬件中断也会使 CPU 切换到核心态,这种情况下内核不能访问用户空间。

大多数情况下,单个虚拟地址空间就比系统中可用的物理内存要大。更何况每个进程都有自身的虚拟地址空间。因此内核和 CPU 必须考虑如何将实际可用的物理内存映射到虚拟地址空间的区域。一般是用页表来为物理地址分配虚拟地址。虚拟地址关系到进程的用户空间和内核空间,而物理地址则用来寻址实际可用的内存。如下图所示两个进程的虚拟地址空间,都被内核划分为很多等长的部分。这些部分称之为页。物理内存也划分为同样大小的页。
address-page
图中的箭头表示了虚拟地址页被分配到的物理内存页,如进程 A 的虚拟内存页 1 映射到物理内存页 4,而进程 B 的虚拟内存页 1 映射到物理内存页 5,不同进程就是通过这种方式进行了内存的隔离,它们的地址空间被映射到不同的物理内存。从图中,我们还会发现可能不同进程的虚拟地址映射到的相同的物理内存页,这是通过内存共享的机制来进行的。此外,图中还可以看出并不是所有的虚拟地址空间都有对应的物理内存页,这可能是该地址尚未分配物理页,或者该物理页被换出到磁盘中了。

物理内存页经常被称为页帧,而页一般用来指虚拟地址空间的页。

页表

用来将虚拟地址空间映射到物理地址空间的数据结构称为页表。实现两个地址空间的关联最容易的方法是使用数组,对虚拟地址空间中的每一页,都分配一个数组项。该数组项指向与之关联的页帧,但这有个问题。例如,IA-32 体系结构中一个页有 4KB 大,在虚拟地址空间为 4GB 时,则需要包含 100 万项的数组,才能完成这个映射关系。在64位体系结构上时,情况会更糟糕,每个进程都需要自身的页表,因此系统的所有内存都要用来保存页表,也就是说这个方法是不切实际的。

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

为减少页表的大小并容许忽略不需要的区域,计算机体系结构的设计会将虚拟地址划分为多个部分。(具体在地址字的哪些位区域进行划分,可能依不同的体系结构而异)在下面的例子中,我将虚拟地址划分为 4 部分,这样就需要一个三级的页表。大多数体系结构都是这样的做法。但有一些采用了四级的页表,而 Linux 也采用了四级页表。为简化场景,我在这里会一直用三级页表阐述。
3-level-page-table
虚拟地址空间的第一部分是全局页目录项(PGD)的索引,它用来索引全局页目录,一个进程有且只有一项,该项中存储了中间页目录(PMD)的头指针,然后通过虚拟地址空间中的第二项作为偏移就可以得出该虚拟地址对应的 PMD 项,该项中存储了对应页表(PTE,Page Table Entry)的头指针,然后我们把虚拟地址中的第三项作为页表偏移,就可以得到对应页帧的物理地址,最后我们根据虚拟地址的最后一块 Offset 就能在页帧中进行寻址,指向页帧中的任意一个字节。

通过这种多级分页的方式,我们就可以对那些不需要的虚拟地址区域,不创建中间页目录和页表,从而省下了大量的内存。不过这种方式也有一些缺点,就是每次访存都需要逐级访问多个数组才能将虚拟地址转换为物理地址。对于这个问题,CPU 也有一些措施来进行加速:

  • CPU 中有一个专门的内存管理单元 MMU,该单元优化了内存的访问操作。
  • 对于地址转换中出现最频繁的那些地址,会被保存到地址转换后备缓冲器 TLB(页表缓存) 的 CPU 高速缓存中。这样无需访问页表就能从高速缓存直接获取地址数据,从而大大加速地址转换速度。

在许多体系结构中高速缓存的运转是透明的,但某些体系结构则需要内核专门处理。这更意味着每当页表的内容变化时必须使 TLB(页表缓存) 高速缓存无效。内核中凡涉及操作页表之处都必须调用相应的指令。如果针对不需要此类操作的体系结构编译内核,则相应调用自动变为空操作。

除了页和页帧的映射外,Linux 中还有一个内存映射的系统调用,看过我之前 RocketMQ 文章的同学可能对内存映射有些了解,它可以将任意来源的数据传输到进程的虚拟地址空间中。作为映射目标的地址空间区域,可以像普通内存那样用通常的方法访问。但,任何修改都会自动地传输到原始数据源。这样就可以用完全相同的函数来处理完全不同的目标对象,这一点在内核中被大量使用。例如文件的内容可以被映射到内存中,处理只需要读取相应的内存就能访问文件内容,而向内存中写入数据就会自动修改文件内容。内核将保证任何修改都会自动同步到文件。

物理内存的分配

在内核分配内存时,必须记录页帧的已分配或空闲状态,以免两个进程使用同样的内存区域。由于内存分配和释放非常频繁,内核还必须保证相关操作尽快完成。内核可以只分配完整的页帧。将内存划分为更小的部分的工作,则委托给用户空间中的标准库。标准库将来源于内核的页帧拆分为小的区域,并为进程分配内存。

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

系统中的空闲内存块总是两两分组,每组中的两个内存块称作伙伴。伙伴的分配可以是彼此独立的。但如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存块,作为下一层次上某个内存块的伙伴。下图示范了该系统,图中给出了一对伙伴,初始大小均为 8 页。
partner-system
内核对所有大小相同的伙伴(1、2、4、8、16或其他数目的页),都放置到同一个列表中管理。各有 8 页的一对伙伴也在相应的列表中。如果系统现在需要 8 个页帧,则将 16 个页帧组成的块拆分为两个伙伴。其中一块用于满足应用程序的请求,而剩余的 8 个页帧则放置到对应 8 页大小内存块的列表中。

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

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

在系统长期运行时,那么会发生称为碎片的内存管理问题。频繁的分配和释放页帧可能导致一种情况: 系统中有若干页帧是空闲的,但却散布在物理地址空间的各处。换句话说,系统中缺乏连续页帧组成的较大的内存块,而从性能上考虑,却又很需要使用较大的连续内存块。通过伙伴系统可以在某种程度上减少这种效应,但无法完全消除。如果在大块的连续内存中间刚好有一个页帧分配出去,很显然这两块空闲的内存是无法合并的。

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

  1. 对频繁使用的对象,内核定义了只包含了所需类型对象实例的缓存。每次需要某种对象时,可以从对应的缓存快速分配(使用后释放到缓存)。slab 缓存自动维护与伙伴系统的交互,在缓存用尽时会请求新的页帧。
  2. 对通常情况下小内存块的分配,内核针对不同大小的对象定义了一组 slab 缓存,可以像用户空间编程一样,用相同的函数访问这些缓存。不同之处是这些函数都增加了前缀k,表明是与内核相关联的: kmalloc 和 kfree。

下图综述了伙伴系统、slab 分配器以及内核其他方面之间的关联。
partner-slab
当物理内存不足时,内核会通过页面交换机制来扩展内存。在内核需要更多内存时,不经常使用的页可以写入硬盘。如果再次需要访问相关数据,内核会将相应的页切换回内存。这是通过缺页异常机制实现的,这种切换操作对应用程序来说是透明的。换出的页可以通过特别的页表项标识。在进程试图访问此类页帧时,CPU 则启动一个可以被内核截取的缺页异常。此时内核可以将硬盘上的数据切换到内存中。接下来用户进程可以恢复运行。由于进程无法感知到缺页异常,所以页的换入和换出对进程是完全不可见的。

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

计时

内核必须能够测量时间以及不同时间点的时差,进程调度就会用到该功能。jiffies 就是内核维护的时间坐标。名为 jiffies_64 和 jiffies (分别是64位和32位)的全局变量,会按恒定的时间间隔递增。每种计算机底层体系结构都提供了一些执行周期性操作的手段,通常的形式是定时器中断。对这两个全局变量的更新可使用底层体系结构提供的各种定时器机制执行。

jiffies 递增的频率同体系结构有关,取决于内核中一个主要的常数 HZ。该常数的值通常介于 100 和 1000 中间。换言之,jiffies 的值每秒递增的次数在 100 至 1000 次之间。

基于 jiffies 的计时相对粒度较粗,因为目前 1000HZ 已经算不上很高的频率了。在底层硬件能力允许的前提下,内核可使用高分辨率的定时器提供额外的计时手段,能够以纳秒级的精确度和分辨率来计量时间。计时的周期是可以动态改变的。在不需要的时候,可以关闭该周期性中断,这样就能让处理器降低耗电进入睡眠模式,所以动态改变计时周期对于笔记本电脑和嵌入式系统是很有用的。

系统调用

系统调用是用户进程与内核交互的经典方法。传统的系统调用按不同类别划分的话可分为如下几类。

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

所有这些函数都需要内核来完成,它们之所以不能以普通的用户库形式实现,因为计算机需要特别的保护机制来保证系统稳定性和安全。此外许多调用依赖内核内部的结构或函数来得到所需的数据或结果,这也导致了无法在用户空间实现。在发出系统调用时,处理器必须改变特权级别(还记得前面提到的 CPU 特权级别吗),从用户状态切换到核心态。Linux 对此没有标准化的做法,因为每个硬件平台都提供了特定的机制。有时候,在同样的体系结构上也会根据处理器类型使用不同的方法实现。Linux 使用了一个专用软件中断在 IA-32 处理器上执行系统调用。IA-32架构的现代处理器也提供了专用的汇编语句来执行系统调用。虽然各个架构的 CPU 可能切换到内核态的方式各不相同,但是有一点是不变的,那就是用户进程要从用户态切换到内核态,并将系统关键任务委派给内核执行,这部分工作就是系统调用的范畴。

设备驱动、块设备和字符设备

设备驱动程序用于与系统连接的输入输出装置通信,如硬盘、软驱、各种接口、声卡等。按照经典的 UNIX 箴言“万物皆文件”(everything is a file),对外设的访问可利用 /dev 目录下的设备文件来完成,程序对设备的处理完全类似于操作常规的文件。设备驱动程序的任务在于支持应用程序经由设备文件与设备通信。换言之,使得能够按适当的方式在设备上读取/写入数据。

外设可分为以下两类:

  1. 字符设备: 提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节字符来读写数据。举例来说,调制解调器是典型的字符设备。
  2. 块设备: 应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。

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

网络

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

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

文件系统

Linux 系统由数以千计乃至百万计的文件组成,其数据存储在硬盘或其他块设备(例如ZIP驱动、软驱、光盘等)。存储使用了层次式文件系统。文件系统使用目录结构组织存储的数据,并将其他元信息(例如所有者、访问权限等)与实际数据关联起来。Linux 支持许多不同的文件系统: 标准的 Ext2 和 Ext3 文件系统等。不同文件系统所基于的概念抽象,在某种程度上可以说是南辕北辙。Ext2 基于inode,即它对每个文件都构造了一个单独的管理结构,称为inode,并存储到磁盘上。inode包含了文件所有的元信息,以及指向相关数据块的指针。目录可以表示为普通文件,其数据包括了指向目录下所有文件的 inode 的指针,因而层次结构得以建立。

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

模块和热插拔

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

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

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

对支持热插拔而言,模块在本质上是必须的。某些总线(例如,USB和FireWire)允许在系统运行时连接设备,而无需系统重启。在系统检测到新设备时,通过加载对应的模块,可以将必要的驱动程序自动添加到内核中。

模块特性使得内核可以支持种类繁多的设备,而内核自身的大小却不会发生膨胀。在检测到连接的硬件后,只需要加载必要的模块,多余的驱动程序无需加入到内核。

内核社区中一个长期存在的争论则是围绕只提供二进制代码的模块展开的,即不提供源代码的模块。在大多数私有的操作系统上只提供二进制代码的模块是普遍存在的,但许多内核开发者认为它们是邪恶的化身。内核是开源软件,因此他们认为,出于各种法律和技术原因,模块也应该是开源的。但一些商业公司不这样看,他们认为开放驱动程序的源代码会削弱其商业地位。

目前可以将只提供二进制代码的模块加载到内核,但有很多限制。最重要的一点是,对任何明确规定调用者也必须使用 GPL 许可的函数,此类模块均不能访问。加载只提供二进制代码的模块会污染内核,每当发生点坏事,过错自然会归咎于相应的模块。如果内核被污染,则故障转储文件中会标记出来,而内核开发者一般不愿意解决此类导致崩溃的问题。因为二进制模块可能使内核的每个部分都发生了充分的震荡,不能假定内核仍然可以按预定的设计工作,所以这种情况下的支持工作最好留给相关模块的厂商处理。

缓存

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

块缓存用于缓存没有组织成页的数据,其重要性差得多。在传统的 UNIX 系统上,块缓存用作系统的主缓存,而 Linux 很久以前也是这样。到如今,块缓存已经被页缓存取代了。

参考内容

[1]《Linux内核设计与实现》
[2]《Linux系统编程》
[3]《深入理解Linux内核》
[4]《深入Linux内核架构》
[5] Linux 内核进程管理之进程ID
[6] 服务器三大体系SMP、NUMA、MPP介绍
[7] Linux中的物理内存管理 [一]
[8] Linux内核中的page migration和compaction机制简介
[9] 物理地址、虚拟地址(线性地址)、逻辑地址以及MMU的知识
[10] 逻辑地址
[11] linux内核学习笔记-struct vm_area_struct
[12] Linux中匿名页的反向映射
[13] 系统调用过程详解
[14] 再谈Linux内核中的RCU机制
[15] Unix domain socket 和 TCP/IP socket 的区别
[16] Linux通用块设备层
[17] ext2文件系统结构分析
[18] linux ACL权限规划:getfacl,setfacl使用
[18] 查找——图文翔解RadixTree(基数树)
[19] 页缓存page cache和地址空间address_space
[20] rocketmq使用的系统参数(dirty_background_ration dirty_ratio)
[21] Linux内存调节之zone watermark
[22] Linux的内存回收和交换
[23] Linux中的内存回收[一]
[24] linux内存源码分析 - 内存回收(整体流程)
[25] Linux 软中断机制分析
[26] 对 jiffies 溢出、回绕及 time_after 宏的理解
[27] learn-linux-network-namespace
[28] 显式拥塞通知
[29] 聊聊 TCP 长连接和心跳那些事
[30] 关于 TCP/IP,必知必会的十个问题
[31] TCP协议三次握手连接四次握手断开和DOS攻击
[32] TCP 的那些事儿(上)
[33] TCP 的那些事儿(下)

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/linux/introduction/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。