《Linux系统编程(第2版)》——2.11 内核内幕

简介: 这一节将介绍Linux内核如何实现I/O,重点说明内核的三个主要的子系统:虚拟文件系统(VFS)、页缓存(page cache)和页回写(page writeback)。通过这些子系统间的协作,Linux I/O看起来无缝运行且更加高效。

本节书摘来自异步社区《Linux系统编程(第2版)》一书中的第2章,第2.11节,作者:【美】Robert Love著,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.11 内核内幕

这一节将介绍Linux内核如何实现I/O,重点说明内核的三个主要的子系统:虚拟文件系统(VFS)、页缓存(page cache)和页回写(page writeback)。通过这些子系统间的协作,Linux I/O看起来无缝运行且更加高效。

2.11.1 虚拟文件系统
虚拟文件系统,有时也称虚拟文件交换(virtual file switch),是一种抽象机制,支持Linux内核在不了解(甚至不需要了解)文件系统类型的情况下,调用文件系统函数并操作文件系统的数据。

虚拟文件系统通过通用文件模型(common file model)实现这种抽象,它是Linux上所有文件系统的基础。通过函数指针以及各种面向对象方法,通用文件模型提供了一种Linux内核文件系统必须遵循的框架。它支持虚拟文件系统向文件系统发起请求。框架提供了钩子(hook),支持读、建立链接、同步等功能。然后,每个文件系统注册函数,处理相应的操作。

这种方式的前提是文件系统之间必须具有一定的共性。比如,虚拟文件系统是基于索引节点、superblock(超级块)和目录项,而非UNIX系的文件系统可能根本就没有索引节点的概念,需要特殊处理。而事实上,Linux确实做到了:它可以很好地支持如FAT和NTFS这样的文件系统。

虚拟文件系统的优点真是“不胜数”:系统调用可以在任意媒介的任意文件系统上读,工具可以从任何一个文件系统拷贝到另一个上。所有的文件系统都支持相同的概念、相同接口和相同调用。一切都可以正常工作——而且工作得很好。

当应用发起read()系统调用时,其执行过程是非常奇妙的:从C库获取系统调用的定义,在编译时转化为相应的trap语句[8]。一旦进程从用户空间进入内核,交给系统调用handler处理,然后交给read()系统调用。内核确定给定的文件描述符所对应的对象类型,并调用相应的read()函数。对于文件系统而言,read()函数是文件系统代码的一部分。然后,该函数继续后续操作——比如从文件系统中读取数据,并把数据返回给用户空间的read()调用,该调用返回系统调用handler,它把数据拷贝到用户空间,最后read()系统调用返回,程序继续执行。

对于系统程序员来说,虚拟文件系统带来的“变革”是很深刻的。程序员不需要担心文件所在的文件系统及其存储介质。通用的系统调用——比如read()、write()等,可以在任意支持的文件系统和存储介质上操作文件。

2.11.2 页缓存
页缓存是通过内存保存最近在磁盘文件系统上访问过的数据的一种方式。相对于当前的处理器速度而言,磁盘访问速度过慢。通过在内存中保存被请求数据,内核后续对相同数据的请求就可以直接从内存中读取,避免了重复访问磁盘。

页缓存利用了“时间局部性(temporal locality)”原理,它是一种“引用局部性(locality of reference)”,时间局部性的理念基础是认为刚被访问的资源在不久后再次被访问的概率很高。在第一次访问时对数据进行缓存,虽然消耗了内存,但避免了后续代价很高的磁盘访问。

内核查找文件系统数据时,会首先查找页缓存。只有在缓存中找不到数据时,内核才会调用存储子系统从磁盘读取数据。因此,第一次从磁盘中读取数据后,就会保存到页缓存中,应用在后续读取时都是直接从缓存中读取并返回。页缓存上的所有操作都是透明的,从而保证其数据总是有效的。

Linux页缓存大小是动态变化的。随着I/O操作把越来越多的数据存储到内存中,页缓存也变得越来越大,消耗掉空闲的内存。如果页缓存最终消耗掉了所有的空闲内存,而且有新的请求要求分配内存,页缓存就会被“裁剪”,释放它最少使用的页,让出空间给“真正的”内存使用。这种“裁剪”是无缝自动处理的。通过这种动态变化的缓存,Linux可以使用所有的系统内存,并缓存尽可能多的数据。

一般来说,把进程内存中很少使用的页缓存“交换(swap)”给磁盘要比清理经常使用的页缓存更有意义,因为如果清理掉经常使用的,下一次读请求时又得把它读到内存中。(交换支持内核在磁盘上存储数据,得到比机器RAM更大的内存空间。)Linux内核实现了一些启发式算法,来平衡交换数据和清理页缓存(以及其他驻存项)。这些启发式算法可能会决定通过交换数据到磁盘来替代清理页缓存,尤其当被交换的数据没有在使用时。

交换-缓存之间的平衡可以通过 /proc/sys/vm/swappiness 来调整。虚拟文件数可以是0到100,默认是60。值越大,表示优先保存页缓存,交换数据;值越小,表示优先清理页缓存,而不是交换数据。

另一种引用局部性是“空间局部性(sequential locality)”,其理论基础是认为数据往往是连续访问的[9]。基于这个原理,内核实现了页缓存预读技术。预读是指在每次读请求时,从磁盘数据中读取更多的数据到页缓存中——实际上,就是多读几个比特。当内核从磁盘中读取一块数据时,它还会读取接下来一两块数据。一次性读取较大的连续数据块会比较高效,因为通常不需要磁盘寻址。此外,由于在进程处理第一块读取到的数据时,内核可以完成预读请求。正如经常发生的那样,如果进程继续对接下来连续块提交新的读请求,内核就可以直接返回预读的数据,而不用再发起磁盘I/O请求。

和页缓存类似,内核对预读的管理也是动态变化的。如果内核发现一个进程一直使用预读的数据,它就会增加预读窗口,从而预读进更多的数据。预读窗口最小16KB,最大128KB。反之,如果内核发现预读没有带来任何有效命中——也就是说,应用随机读取文件,而不是连续读——它可以把预读完全关闭掉。

页缓存对程序员而言是透明的。一般来说,系统程序员无法优化代码以更好地利用页缓存机制——除非自己在用户空间实现这样一种缓存。通常情况下,要最大限度利用页缓存,唯一要做的就是代码实现高效。此外,如果代码高效,还可以利用预读机制。虽然并不总是如此,连续I/O访问通常还是远远多于随机访问。

2.11.3 页回写
正如2.3.6小节介绍的那样,内核通过缓冲区来延迟写操作。当进程发起写请求,数据被拷贝到缓冲区,并将该该缓冲区标记为“脏”缓冲区,这意味着内存中的拷贝要比磁盘上的新。此时,写请求就可以返回了。如果后续对同一份数据块有新的写请求,缓冲区就会更新成新的数据。对该文件中其他部分的写请求则会开辟新的缓冲区。

最后,那些“脏”缓冲区需要写到磁盘,将磁盘文件和内存数据同步。这个过程就是所谓的“回写(writeback)”。以下两个情况会触发回写:

当空闲内存小于设定的阈值时,“脏”缓冲区就会回写到磁盘上,这样被清理的缓冲区会被移除,释放内存空间。
当“脏”缓冲区的时长超过设定的阈值时,该缓冲区就会回写到磁盘。通过这种方式,可以避免数据一直是“脏”数据。
回写是由一组称为flusher的内核线程来执行的。当出现以上两种场景之一时,flusher线程被唤醒,并开始将“脏”缓冲区写到磁盘,直到不满足回写的触发条件。

可能存在多个flusher线程同时执行回写。多线程是为了更好地利用并行性,避免拥塞。拥塞避免(congestion avoidance)机制确保在等待向某个块设备进行写操作时,还能够支持其他的写操作。如果其他块设备存在脏缓冲区,不同flusher线程会充分利用每一块设备。这种方式解决了之前内核的一处不足: 先前版本的flusher线程(pdflush以及bdflush)会一直等待某个块设备,而其他块设备却处于空闲状态。在现代计算机上,Linux内核可以使多个磁盘处于饱和状态。

缓冲区在内核中是通过buffer_head数据结构来表示的。该数据结构跟踪和缓冲区相关的各种元数据,比如缓冲区是否是“脏”缓冲区。同时,它还维护了一个指向真实数据的指针。这部分数据保存在页缓存中。通过这种方式,实现了缓冲子系统(buffer subsystem)和页缓存之间的统一。

在早期的Linux内核版本中(2.4以前),缓冲子系统与页缓存是分离的,因此同时存在页缓存和缓冲缓存。这意味着数据可以同时在缓冲缓存(作为“脏”缓冲区)和页缓存(用来缓存数据)中存在。不可避免地,对这两个独立缓存进行同步需要很多工作。因此,在2.4 Linux内核中引入了统一的页缓存,这个改进很不错!

在Linux中,延迟写和缓冲子系统使得写操作性能很高,其代价是如果电源出现故障,可能会丢失数据。为了避免这种风险,关键应用可以使用同步I/O来保证(在本章先前讨论过)。

相关文章
|
2月前
|
监控 Linux 开发者
理解Linux操作系统内核中物理设备驱动(phy driver)的功能。
综合来看,物理设备驱动在Linux系统中的作用是至关重要的,它通过与硬件设备的紧密配合,为上层应用提供稳定可靠的通信基础设施。开发一款优秀的物理设备驱动需要开发者具备深厚的硬件知识、熟练的编程技能以及对Linux内核架构的深入理解,以确保驱动程序能在不同的硬件平台和网络条件下都能提供最优的性能。
120 0
|
5月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
236 67
|
3月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
101 0
|
5月前
|
存储 Linux
Linux内核中的current机制解析
总的来说,current机制是Linux内核中进程管理的基础,它通过获取当前进程的task_struct结构的地址,可以方便地获取和修改进程的信息。这个机制在内核中的使用非常广泛,对于理解Linux内核的工作原理有着重要的意义。
211 11
|
6月前
|
自然语言处理 监控 Linux
Linux 内核源码分析---proc 文件系统
`proc`文件系统是Linux内核中一个灵活而强大的工具,提供了一个与内核数据结构交互的接口。通过本文的分析,我们深入探讨了 `proc`文件系统的实现原理,包括其初始化、文件的创建与操作、动态内容生成等方面。通过对这些内容的理解,开发者可以更好地利用 `proc`文件系统来监控和调试内核,同时也为系统管理提供了便利的工具。
250 16
|
8月前
|
Ubuntu Linux 开发者
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
442 15
|
8月前
|
安全 Linux 测试技术
Intel Linux 内核测试套件-LKVS介绍 | 龙蜥大讲堂104期
《Intel Linux内核测试套件-LKVS介绍》(龙蜥大讲堂104期)主要介绍了LKVS的定义、使用方法、测试范围、典型案例及其优势。LKVS是轻量级、低耦合且高代码覆盖率的测试工具,涵盖20多个硬件和内核属性,已开源并集成到多个社区CICD系统中。课程详细讲解了如何使用LKVS进行CPU、电源管理和安全特性(如TDX、CET)的测试,并展示了其在实际应用中的价值。
195 4
|
9月前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
379 13
|
10月前
|
负载均衡 算法 Linux
深入探索Linux内核调度器:公平与效率的平衡####
本文通过剖析Linux内核调度器的工作机制,揭示了其在多任务处理环境中如何实现时间片轮转、优先级调整及完全公平调度算法(CFS),以达到既公平又高效地分配CPU资源的目标。通过对比FIFO和RR等传统调度策略,本文展示了Linux调度器如何在复杂的计算场景下优化性能,为系统设计师和开发者提供了宝贵的设计思路。 ####
195 26
|
10月前
|
缓存 并行计算 Linux
深入解析Linux操作系统的内核优化策略
本文旨在探讨Linux操作系统内核的优化策略,包括内核参数调整、内存管理、CPU调度以及文件系统性能提升等方面。通过对这些关键领域的分析,我们可以理解如何有效地提高Linux系统的性能和稳定性,从而为用户提供更加流畅和高效的计算体验。
388 24