购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.3 内核组件

现在你已经了解了CPU架构的基础知识,是时候深入研究内核了。虽然Linux内核是一个整体(也就是说,讨论的所有组件都是单个二进制文件的一部分),但在代码库中有一些功能区域,我们可以识别并赋予它们专门的职责。

正如我们在2.1节中所讨论的,内核位于硬件和你想要运行的应用程序之间。你在内核代码库中找到的主要功能块如下:

·进程管理,例如根据可执行文件启动进程。

·内存管理,例如为进程分配内存或将文件映射到内存。

·网络,例如管理网络接口或提供网络栈。

·提供文件管理和支持文件创建和删除的文件系统。

·字符设备和设备驱动程序的管理。

这些功能组件通常具有相互依赖关系,确保内核开发人员的座右铭( https://oreil. ly/6YDeF )“内核永远不会破坏用户空间”是一项真正具有挑战性的任务。

在此基础上,让我们进一步了解内核组件。

2.3.1 进程管理

内核中有许多与进程管理相关的部分。其中一些处理特定于CPU架构的事情(比如中断),而另一些则专注于程序的启动和调度。

在我们了解Linux细节之前,我们要注意,进程通常是基于可执行程序(或二进制文件)的面向用户的单元。另外,线程是进程上下文中的执行单元。你可能遇到过术语“多线程”,这意味着一个进程有许多并行执行,可能运行在不同的CPU上。

有了这个一般的观点,让我们看看Linux是如何做到的。从最细粒度到最小单元,Linux有以下内容:

会话

包含一个或多个进程组,并表示带有可选tty附加的面向用户的高级单元。内核通过一个称为会话ID(SID)的数字来标识会话。

进程组

包含一个或多个进程,一个会话中最多有一个进程组作为前台进程组。内核通过一个称为进程组ID(PGID)的数字来标识进程组。

进程

对多个资源(地址空间、一个或多个线程、套接字等)进行分组的抽象,内核通过/ proc/self 为当前进程向你公开这些资源。内核通过一个叫作进程ID(PID)的数字来标识一个进程。

线程

由内核以进程的形式实现。也就是说,没有专门的数据结构来表示线程。相反,线程是与其他进程共享某些资源(如内存或信号处理程序)的进程。内核通过线程ID(TID)和线程组ID(TGID)来标识一个线程,共享的TGID值意味着一个多线程进程(在用户空间,还有内核线程,但这超出了本书讨论的范围)。

任务

在内核中,有一个名为task_struct的数据结构——在 sched.h https://oreil.ly/nIgz8 )中定义——它构成了实现进程和线程的基础。该数据结构捕获与调度相关的信息、标识符(如PID和TGID)、信号处理程序以及其他信息(例如与性能和安全性相关的信息)。简而言之,前面提到的所有单元都派生或锚定在任务中,但是,任务不会在内核之外公开。

我们将在第6章看到会话、进程组和进程的作用,并学习如何管理它们,它们将在第9章再次出现在容器的上下文中。

让我们来看看这些概念的实际应用:

❶bash shell进程的PID、PGID和SID为6756。从ls -al /proc/6756/task/6756/中,我们可以收集任务级别的信息。

❷ps进程的PID/PGID 6790和shell的SID相同。

我们在前面提到过,在Linux中,任务数据结构有一些与调度相关的信息。这意味着在任何给定的时间,进程都处于某种状态,如图2-2所示。

图2-2:Linux进程状态

严格地说,进程状态稍微复杂一些。例如,Linux区分了可中断睡眠和不可中断睡眠,还有僵尸态(在这种状态下,它失去了父进程)。如果你对细节感兴趣,请查看文章“Process States in Linux”( https://oreil.ly/XBXbU )。

不同的事件会导致状态转换。例如,一个正在运行的进程在执行一些I/O操作(比如从文件中读取)时可能会转换到等待态,并且无法继续执行(离开C P U)。

在快速了解了进程管理之后,让我们研究一个相关的主题:内存。

2.3.2 内存管理

虚拟内存使你的系统看起来好像它有比物理内存更多的内存。事实上,每个进程都有很多(虚拟)内存。它是这样工作的:物理内存和虚拟内存都被划分为固定长度的块,我们称为页面。

图2-3显示了两个进程的虚拟地址空间,每个进程都有自己的页表。这些页表将进程的虚拟页映射到主内存(又名R A M)中的物理页面。

图2-3:虚拟内存管理概览

多个虚拟页可以通过各自的进程级页表指向同一个物理页。在某种意义上,这就是内存管理的核心:有效地为每个进程提供其页面实际存在于RAM中的假象,同时优化地使用现有空间。

每次CPU访问进程的虚拟页面时,原则上CPU必须将进程使用的虚拟地址转换为相应的物理地址。为了加快这一过程(可能是多层的,因此很慢),现代CPU架构支持一种称为转换后援缓冲器(TLB)的片上查找( https://oreil.ly/y3xy0 )。TLB实际上是一个小缓存,在发生错误时,它会导致CPU通过进程页表计算页面的物理地址并更新TLB。

传统上,Linux的默认页面大小为4KB,但从内核v2.6.3开始,它支持大页面( https://oreil.ly/7rqLO ),以更好地支持现代架构和工作负载。例如,64位Linux允许每个进程使用最多128TB的虚拟地址空间(虚拟是理论上可寻址的内存地址数量),总共大约64TB的物理内存(物理是你机器中的RAM数量)。

好的,这是很多理论信息。让我们从更实际的角度来看一下。一个非常有用的工具是 /proc/meminfo 接口,它可以计算出内存相关的信息,比如有多少内存可用:

❶列出物理内存(RAM)的详细信息,这里是4GB。

❷列出虚拟内存的详细信息,这比34TB多一点。

❸列出大页面信息,显然这里的页面大小是2MB。

接下来,我们继续讨论下一个内核功能:网络。

2.3.3 网络

内核的一个重要功能是提供网络功能。无论你是想浏览网页还是将数据复制到远程系统,都依赖网络。

Linux网络栈遵循分层架构:

套接字

用于抽象通信。

传输控制协议(Transmission Control Protocol,TCP 和用户数据报协议(User Datagram Protocol,UDP)

分别用于面向连接的通信和无连接通信。

互联网协议(IP)

用于机器寻址。

这三个操作全部是由内核负责处理的。应用层协议(如HTTP或SSH),通常是在用户空间实现的。

你可以使用以下命令获得网络接口的概述(输出已编辑过):

此外,ip route可以为你提供路由信息。由于我们有一个专门的网络章节(第7章),我们将深入研究网络栈、支持的协议和典型操作,因此对于网络我们先介绍到这,然后转向下一个内核组件,块设备和文件系统。

2.3.4 文件系统

Linux使用文件系统来组织存储设备上的文件和目录,例如硬盘驱动器(HDD)和固态硬盘(SSD)或闪存。有许多类型的文件系统,例如ext4和btrfs或NTFS,并且你可以使用同一文件系统的多个实例。

最初引入虚拟文件系统(VFS)是为了支持多种文件系统类型和实例。VFS中的最高层提供了开放、关闭、读取和写入等函数的通用API抽象。在VFS的底部是文件系统抽象,称为给定文件系统的插件。

我们将在第5章更详细地讨论文件系统和文件操作。

2.3.5 设备驱动程序

驱动程序是在内核中运行的一段代码。它的任务是管理设备,设备可以是实际的硬件(比如键盘、鼠标或硬盘驱动器),也可以是伪设备[比如 /dev/pts/ 下的伪终端(它不是物理设备,但可以像物理设备一样被对待)]。

另一类有趣的硬件是图形处理单元(GPU, https://oreil.ly/os7pu ),传统上用于加速图形输出和减轻CPU的负载。近年来,GPU在机器学习环境中发现了一个新的用例( https://oreil.ly/qrVcY ),因此它们并不只与桌面环境相关。

驱动程序可以静态地构建到内核中,也可以构建为内核模块(请参见2.4.1节),以便在需要时动态加载。

如果你对以交互式方式探索设备驱动程序以及内核组件如何交互感兴趣,请查看Linux内核映射( https://oreil.ly/voBtR )。

内核驱动程序模型( https://oreil.ly/Cb6mw )很复杂,超出了本书的范围。但是,下面是一些与它交互的提示,足以让你知道在哪里可以找到什么。

要获得Linux系统上设备的概述,你可以使用以下命令:

此外,你可以使用以下命令列出已挂载的设备:

至此,我们已经介绍了Linux内核组件,接下来让我们转向内核和用户之间的接口。

2.3.6 系统调用

无论你坐在终端前,输入touch test.txt,还是你的某个应用程序想要从远程系统下载文件的内容,最终你都需要Linux将高级指令(如“创建文件”或“从某地址读取所有字节”)转换为一组具体的、依赖于架构的步骤。换句话说,内核公开的服务接口和用户空间实体调用是系统调用的集合,或者简称为syscall( https://oreil.ly/UF09U )。

Linux有数百个系统调用:大约300个或更多,这取决于CPU系列。然而,你和你的程序通常不会直接调用这些系统调用,而是通过我们所说的C标准库。标准库提供了包装器函数,并可在各种实现中使用,例如glibc( https://oreil.ly/mZPRy )或musl( https://oreil.ly/jnTCA )。

这些包装器库执行一项重要任务。它们负责系统调用执行过程中重复的低级处理。系统调用被实现为软件中断,导致异常,将控制转移到异常处理程序。每次调用系统调用时,都有一些步骤需要注意,如图2-4所示。

图2-4:Linux系统调用的执行步骤

1. 在 syscall.h 和依赖于架构的文件中定义的,内核使用的所谓的系统调用表,它实际上是内存中的函数指针数组(存储在名为sys_call_table的变量中),用于跟踪系统调用及其相应的处理程序。

2. 使用system_call()函数充当系统调用多路复用器,它首先将硬件上下文保存在栈上,然后执行检查(例如执行跟踪),然后跳转到sys_call_table中相应的系统调用编号索引所指向的函数。

3. 在使用sysexit完成系统调用之后,包装器库恢复硬件上下文,并在用户空间恢复程序执行。

在前面的步骤中,值得注意的是在内核模式和用户空间模式之间切换,这是一个需要花费时间的操作。

好吧,这有点枯燥和理论性,所以为了更好地理解系统调用在实践中的外观和感觉,让我们看一个具体的例子。例如,如果你没有应用程序的源代码,但想了解它的功能,那么我们将使用strace( https://oreil.ly/ksV9B )来查看幕后,strace是一个用于故障排除的工具。

让我们假设你想知道在执行看起来很无辜的ls命令时涉及哪些系统调用。下面是如何使用strace找到它:

❶对于strace ls,我们要求strace捕获ls使用的系统调用。注意,我编辑了输出,因为strace在我的系统上生成了大约162行(这个数字因发行版、架构和其他因素而异)。此外,你在这里看到的输出来自stderr,因此如果你想重定向它,你必须在这里使用2>。你将在第3章学到更多。

❷系统调用execve( https://oreil.ly/iasHW )执行 /usr/bin/ls ,导致shell进程被替换。

❸brk( https://oreil.ly/HRuNj )系统调用是一种过时的分配内存的方法,使用malloc更安全、更方便。请注意,malloc不是一个系统调用,而是一个函数,它反过来使用mallocopt来决定它需要使用brk系统调用还是mmap系统调用,这基于访问的内存量。

❹access系统调用检查进程是否被允许访问某个文件。

❺syscall openat打开/ etc/ld.so.cache 相对于目录文件描述符(这里是第一个参数AT_FDCWD,代表当前目录),并使用标记O_RDONLY|O_CLOEXEC(最后一个参数)。

❻ read系统调用从文件描述符(第一个参数,3)832字节(最后一个参数)读取到缓冲区(第二个参数)。

Strace非常有用,可以准确地查看调用了哪些系统调用(以何种顺序和使用哪些参数),从而有效地连接到用户空间和内核之间的实时事件流。它还适用于性能诊断。让我们看看curl命令花费最多时间的地方(输出被缩短):

❶使用-c选项生成所使用系统调用的概览统计信息。

❷放弃curl的所有输出。

有趣的是,curl命令花了几乎一半的时间在mmap和read系统调用上,而connect系统调用只花了0.3毫秒——还不错。

为了帮助你了解覆盖范围,表2-1列出了在内核组件和系统范围内广泛使用的系统调用示例。你可以通过手册页( https://oreil.ly/qLOA3 )的第2部分查看系统调用的详细信息,包括它们的参数和返回值。

表2-1:示例系统调用

网上有一个很好的交互式系统调用表( https://oreil.ly/HKu6Y ),其中有源代码引用。

现在你已经对Linux内核、它的主要组件和接口有了基本的了解,让我们继续讨论如何扩展它的问题。 zI7+ld1H8oFRTleqXa1huhLNhGp2y61q0u4YhGS0YW+VAPm6RyrlqXOSXr+gcfQb

点击中间区域
呼出菜单
上一章
目录
下一章
×