[{"content":"心中的内核 —— 在阅读内核代码之前先理解内核 原文：The Kernel in the Mind\n[TOC]\n介绍 这并非一部教你编写内核代码的指南，而是一场探寻Linux内核设计思维的思想之旅。\n在系统编程的世界里，人们常常迷失于符号定义、头文件结构与实现细节的丛林中。然而剥离代码表象，内核实则是一个井然有序的响应式系统——它受上下文环境所规约，以分离原则为基石，从内存管理到任务调度的每一处设计，都浸透着精准的设计意图。\n本系列内容专为那些渴望在内核源码之外构建内核运行心智模型的探索者而作。无论你是初次叩响Linux内核内部机制的大门，还是带着全新思考重返这片领域，我们聚焦的核心始终是行为逻辑，而非语法细节。\n每一篇文章起初都是独立成章的思考札记，而当它们汇聚在一起，便勾勒出一幅概念图谱——这幅图谱不关乎函数调用的细枝末节，而是内核如何响应外部请求、执行机制约束、实现模块隔离与提供系统服务的底层逻辑。 内核掌管着系统运行的方方面面，让我们一同洞悉它的运行之道。\n01 内核不是进程，而是系统 Linux 内核既不是进程、守护进程（daemon），也不是应用程序。它是一个具有特权的、驻留在内存中的环境，构成了操作系统的基础。与用户程序不同，它不被调度，没有进程标识符（PID），也不像传统任务那样启动或停止。相反，它始终存在——在引导时加载到内存中——并管理硬件和软件之间的所有交互。\n一旦被引导加载程序加载，内核就会在 start_kernel() 中开始执行，在那里它初始化内存管理、设备接口和核心子系统。在这个一次性设置之后，内核不会作为独立任务继续运行。相反，它成为一个响应式执行层，仅在需要时被调用——由用户进程、硬件事件或其自己的内部线程调用。\n内核代码在三种主要上下文中执行：（1）通过用户进程发起的系统调用（system call），（2）通过硬件触发的中断处理程序（interrupt handlers），以及（3）在完全在内核空间中运行的长期存在的内核线程（kernel threads）中。这些由内核自身创建和管理的线程处理后台任务，例如内存回收、I/O 调度和同步。尽管它们出现在进程列表中（通常用方括号括起来），但它们不是用户空间的守护进程，也从不执行用户空间代码。\n第一个这样的线程是 kthreadd，分配的 PID 为 2。它在 rest_init() 函数的初始化最后阶段创建，负责生成所有其他内核线程。就像 PID 1（init 或 systemd）启动用户空间一样，PID 2 标志着内核线程运行时的开始。\n内核线程的数量不是固定的。在系统启动时，系统可能会创建 20-40 个基本线程——每个核心对应一个用于软中断（soft IRQs）、 watchdog、迁移助手和早期工作队列的线程。随着系统变得活跃，会根据需要为 I/O、内存管理、文件系统和设备驱动程序创建额外的线程。在典型的现代 Linux 系统上，可能会同时运行 100-150 个内核线程，并随着工作负载动态扩展。\n尽管它们可见，但内核线程不是独立程序。内核本身不是运行的任务——它是一个永远存在的执行环境。它是被进入的，而不是被调度的。它提供结构、控制和特权——使所有任务能够运行，同时作为任务本身保持不可见。\n简而言之，内核不是系统内的一个进程——它是系统的核心。始终驻留，始终具有特权，始终处于控制之中。\n02 为进程服务：内核的首要职责 在运行时，Linux 内核管理内存、调度任务、处理 I/O、响应中断并实施系统安全策略。这些职责至关重要，但它们本身并非最终目标。\n内核存在的意义是为用户进程服务。\n其工作是确保每个进程可靠、安全且高效地运行。如果内核未能响应系统调用、分配内存、访问存储或实施隔离机制，则意味着其核心目标的失败。\n重要的是，内核不会自主运行。它仅在三种情况下进入执行状态：来自用户空间的系统调用、硬件设备的中断，或计划执行系统任务的内部线程。这些情况中的每一种都是对外部需求的响应，而这些需求通常源自用户进程。内核仅在需要时才被激活。\n试想启动一个进程时会发生什么。用户调用 exec，内核必须通过虚拟文件系统解析二进制路径，使用底层文件系统驱动程序加载文件，分配并映射内存，通过安全模块验证访问权限，并将进程调度为可执行状态。这些步骤中的每一个都涉及不同的子系统，没有任何一个子系统能够独立完成任务。为了启动单个进程，所有步骤必须按顺序完成。\n即使是一个简单的读取调用也会跨越多个边界。系统调用处理程序会从进程的任务结构（task structure）中验证文件描述符。虚拟文件系统（Virtual File System，VFS）会定位关联的文件对象。根据文件类型的不同，读取请求可能会发往普通文件、管道或套接字。如果内存缓冲区位于未映射的页面上，内存管理器必须先解决缺页（page fault）问题，然后才能复制数据。只有当所有这些操作都成功完成后，内核才会返回用户空间。\n相同的模式适用于所有 I/O、网络和进程间通信。用户的每一个操作都会引发一系列内部协调工作。内核的任何一个部分都无法单独交付结果，始终需要整个系统协同工作。\n内核线程也不例外。当回收内存或刷新脏缓冲区时，它们并非为自身行动，而是为了保持系统健康，使用户进程能够持续运行。它们的工作直接支持用户空间中正在进行或未来的执行。\n这就是 Linux 内核的结构。每个子系统都围绕进程支持进行组织，每项内部服务的存在都是为了响应、支持或保护进程的执行。它不是一个闲置的核心，而是一个响应式、协作式的系统。 内核的重要性并非在于它执行了许多任务，而在于它为其他事物提供服务时执行这些任务。\n那个“其他事物”就是用户进程。\n03 代码之前的概念图 Linux 内核并非按功能组织，而是由必须在并发、硬件交互和故障条件下成立的规则构成。这些规则定义了执行流程、可安全调用的代码以及允许的操作。它们并非实现细节，而是其设计的基础。\n内核在所有处理器和任务上运行，但不会盲目共享执行状态。每次进入内核的调用都与当前线程的身份相关：内存空间、文件描述符、信号状态和特权级别。函数基于此上下文运行，而非全局变量。这种隔离可防止干扰，并支持代码在任务间的安全复用。\n进入内核的方式决定了其可以执行的操作。系统调用、陷阱、中断和内部线程以不同的约束条件进入。有些路径可以睡眠，有些则不能；有些会访问用户内存，有些则停留在内核空间。这些差异决定了哪些函数有效、哪些必须预先分配，以及控制如何返回。它们塑造了内核代码的编写方式和调用时机。\n内核还必须表现出可预测的行为。快速路径避免阻塞，对时间敏感的代码避免使用锁和分支，抢占和调度受到严格控制。这些需求催生了 per-CPU 变量、无锁数据结构和有界执行时间。确定性是响应能力和故障隔离的必要条件。\narch/ 下特定于体系结构的代码处理底层入口、上下文切换、陷阱和分页。它将硬件行为转换为核心内核的一致接口。这种抽象在不影响对 CPU 特定行为控制的前提下实现了可移植性。\n内存根据策略分配。请求在原子性、对齐方式、设备可见性和可回收性方面各不相同。子系统有不同的需求，内核通过分层分配器和受保护的 API 来响应。这些路径由正确性而非便利性决定。\n内核被设计为可恢复。如果驱动程序行为异常或用户输入无效，它不能崩溃或损坏状态。故障被隔离，缓冲区被验证，转换受到保护。鲁棒性是核心设计考量。\n用户空间通过定义的接口进入，但这些接口并不定义内核的结构。重要的是控制如何流动、触及哪些资源以及必须保留哪些保证。\n不能将内核视为实用程序的集合。其结构的存在是为了维护安全性、隔离性、确定性和可恢复性。这是它的编写方式，也必须是理解它的方式。\n04 作为分层系统的内核：虚拟、映射、隔离、控制 Linux 内核并未呈现单一、统一的系统视图，而是公开了许多受控视图——每个视图都与任务绑定，由上下文塑造，并受策略约束。这些视图并非动态组装而成，而是通过虚拟、映射、抽象、隔离和控制等层次构建而来。\n这种结构的存在是为了在并发、抢占和硬件故障情况下使行为可预测。每个层都有定义好的作用域，没有任何一层是单独运行的。内核避免使用全局状态，而是依赖映射、间接和抽象，从而使访问是有意为之的，执行是受限的。\n执行始于硬件边界。特定于体系结构的代码处理陷阱、故障和中断，定义了 CPU 如何响应系统调用或页面故障而进入内核。从一开始，内核就将执行与当前任务和调度上下文绑定。\n任务并非自主运行，它们被排队、分配给 CPU，并根据需要被抢占。调度器执行策略和公平性。定时器、RCU（读-复制-更新）和延迟工作限制了并发性和时间安排。\n抽象定义了内核公开功能的方式。系统调用作用于实现标准接口的内核对象。VFS（虚拟文件系统）抽象文件系统，块层抽象设备，网络栈抽象协议。像 file_operations 和 netdev_ops 这样的接口定义了行为，而不暴露实现。\n调度遵循接口表。文件、套接字和设备不暴露内部结构。read() 或 ioctl() 等操作通过函数指针路由。行为是动态选择的，支持替换和模块化重用。\n访问通过映射解析。文件描述符变为 file struct，虚拟地址变为物理页面，路径变为 dentry 和 inode。这些转换是任务范围的且经过验证，没有任何内容是直接访问的。\n间接性强制分离。内核通过引用——函数表、 per - 任务指针、页表——而非直接访问来路由行为和访问。即使用户空间内存也被视为请求，通过 copy_from_user() 等助手函数解析。间接性确保所有访问都经过中介且具有上下文意识。\n每个任务携带自己的上下文：内存映射、文件表、凭据、命名空间。这些结构定义了它能看到什么和做什么。Cgroups 限制使用，LSM（Linux 安全模块）执行策略。默认情况下不相信任何输入，每个转换都经过验证。\n05 单体形式，协同行为：真正的内核模型 Linux 内核在结构上是单体的。其核心子系统——调度、内存管理、文件系统、网络和驱动程序——被编译成单个二进制文件。它们共享一个地址空间，以特权模式运行，并直接相互调用。在结构上没有隔离来分隔组件。但在运行时，内核行为由所有子系统都必须遵循的系统级约束所塑造。\n执行上下文决定了内何在任何时刻可以执行的操作。代码在进程、内核线程、中断或软中断（softirq）上下文中运行。进程和内核线程上下文允许睡眠、阻塞、用户内存访问和页面故障（page faults）处理。中断和软中断上下文则不允许。这些路径对时间敏感，并且不能阻塞或进行调度，因为这样做会延迟其他任务。页面故障处理是不允许的，因为解决故障可能涉及 I/O、内存分配或回收，所有这些都需要睡眠。这些约束是全局适用的，并影响每个内核决策。\n子系统通过这些共享规则进行交互。调度器避免抢占原子路径。分配器在阻塞前检查上下文和标志。文件系统通过从非阻塞状态到阻塞状态的有序转换来执行 I/O。网络栈从中断上下文开始，经过软中断和工作队列。设备驱动程序延迟无法安全就地完成的工作。这不是惯例，而是设计。子系统将工作通过有效阶段进行处理，而不是一次性处理所有事情。\n同步机制也反映了相同的原则。自旋锁（spinlocks）用于原子路径。互斥锁（mutexes）仅在允许睡眠的地方使用。RCU（读-复制-更新）使读取者能够在不锁定的情况下继续操作。顺序锁（Seqlocks）允许对更新进行快速重试。这些原语是基于上下文和访问模式选择的，而不是开发人员的偏好。用法通过宏、断言和在内核中一致执行的规则来验证。\n内存访问遵循相同的模型。访问用户内存需要进程上下文。故障处理只能在允许睡眠的地方进行，因为解决故障可能涉及磁盘 I/O 或内存回收。分配行为取决于标志和上下文。同一个函数可能会阻塞、立即返回或失败，具体取决于它在哪里运行。内存管理要考虑可见性、局部性和上下文。\n延迟执行将这些层连接起来。从中断开始的工作被传递到软中断，然后到工作队列，最后到内核线程。每个步骤的设计都满足下一步的约束。这种分阶段模型支持 I/O、网络、定时器和驱动程序。\n内核被构建为一个二进制文件，但它作为一个协同系统运行。子系统不能独立运行。它们遵循由上下文、时间和并发性定义的共享模型。它在形式上是单体的，但在执行上是模块化和有原则的。\n06 内核对象揭示设计——函数仅执行设计 使用 Linux 内核的工作通常始于跟踪。调用一个系统调用，路径会在调用栈和分支中展开。数据流动，锁被获取，结构被更新。跟踪显示了内核在特定路径上的行为，但这只是表面现象。它揭示的是执行过程，而非结构。\n要将内核理解为一个系统，关注点必须从过程转移到对象——从函数的作用转移到其背后持续存在的事物。\n内核通过一组长期存在、相互关联的对象来运行，这些对象既代表状态，又代表控制。执行以 task_struct 为锚点，它将调度、内存映射、凭据、打开的文件和命名空间联系在一起。该结构链接到用于地址空间的 mm_struct、用于身份和特权的 cred，以及用于持久文件元数据的 inode。当进程通信时，msg_queue 对象管理流和阻塞。当数据包移动时，sk_buff 跟踪它们在栈层中的传输。每个对象都扮演着特定的角色，但没有一个是单独行动的。它们共同协调访问、并发和策略。它们的契约——所有权、可见性和生命周期——构成了内核执行背后的稳定基础。\n这些对象不会被直接访问。大多数路径从当前活动任务 current 开始。从那里，文件描述符、PID 或 IPC 密钥等句柄通过受引用计数、锁定或 RCU 保护的查找路径解析为内部结构。这种间接性并非优化——它强制实现有效性和隔离。\n以 do_msgsnd() 为例。跟踪显示了参数如何被处理、如何找到队列以及如何将消息入队。但队列限制、发送方阻塞和唤醒行为全部由 msg_queue 对象定义。函数执行一系列操作，而对象定义了该序列必须遵守的契约。\n上下文进一步影响访问。在进程上下文中，允许阻塞和分配；在中断上下文中，则不允许。在两种上下文中使用的对象必须反映这一点——某些字段必须是原子的，其他字段则是延迟的，还有一些字段是禁止访问的。这些不是最佳实践，而是结构性保证。\n作用域和生命周期是明确的。task_struct 在退出时释放；cred 若被共享则可能继续存在；inode 可以在缓存中持续存在，超出使用时间；net_device 可能在整个系统运行时间内都存在。每个对象都定义了谁可以访问它、它的有效时间以及如何安全地停用它。\n函数在这个模型内运行——它们不定义模型。内核不是在调用栈上运行，而是在超越调用栈持续存在的对象上运行，这些对象强制实现责任、约束和连续性。跟踪显示发生了什么，对象解释为什么它有效。对它们了解得越多——它们的角色、生命周期和访问模式——内核就越清晰易懂。\n07 无冲突的代码——内核如何在并发风暴中保持安全 Linux 内核为所有进程和线程所共享，每个进程、每个线程、每个 CPU 都运行相同的代码库。然而，系统并未在并发的压力下崩溃，线程之间没有数据冲突，没有泄漏的文件描述符，也没有损坏的状态。\n为什么？因为内核是围绕间接性、上下文感知，以及关键的无状态代码而设计的。\n大多数内核代码避免使用持久的全局状态，不在函数内部跟踪“谁在调用”。相反，它依赖外部上下文：一个通常通过 current 宏访问的 per - 线程指针，该指针告诉内核调用者的身份、可以访问的内存，以及拥有的文件或凭据。\n这使得内核代码在功能上是无状态的。每次调用不依赖全局变量，仅对从调用线程上下文解析的数据进行操作。这就是内核可重入的原因：同一个函数可以在多个 CPU 上为多个线程运行，而不会产生干扰。\n以 sys_read() 为例，该函数对每个调用者来说看起来都一样。但在内部，它访问 current-\u0026gt;files，使用线程自己的内核模式栈，并写入映射到该进程内存的缓冲区。代码路径是相同的，但每次运行看到的东西不同。\n什么发生了变化？\n输入、指针、引用。\n这就是关键。逻辑保持共享，但数据是私有的。内核不会为每个线程重写函数，它只是遵循作用于活动任务的正确指针。\n对于管道、缓存或套接字等共享数据，内核应用细粒度锁：自旋锁（spinlocks）、互斥锁（mutexes）和 per - CPU 结构，以最小化争用。在极高热度的路径中，它使用 RCU（读 - 复制 - 更新），这是一种无锁同步策略，允许读取者在更新并行发生时并发访问数据。RCU 是现代内核中可扩展读取性能的基石。\n这种设计很强大，但它依赖于正确性。\n如果内核跟随了错误的指针，一切都会崩溃。一个使用后释放（use - after - free）漏洞可能会留下对已重新用途内存的引用。缓冲区溢出可能会损坏相邻结构，改变线程所看到的内容，甚至劫持其身份。\n这些漏洞会颠覆模型，违反上下文是私有的、隔离的和可信赖的保证。\n但当设计得以维持时——当内存受到保护且指针有效时——内核会非常健壮。它能同时安全且高效地处理数千个线程。\n内核并不避免并发，而是为并发而构建。\n它的代码是通用的，但执行始终是特定的——由间接性驱动，由隔离保护，并且结构上避免假设。\n这就是一个内核能够为所有进程和线程服务的方式——永远不会忘记谁是谁。\n08 间接的力量——一个内核如何为所有进程服务 如果内核被映射到每个进程中，它如何避免混淆？为什么一个线程的系统调用不会干扰另一个线程的内存或状态？并且，单一的内核镜像如何在不复制自身的情况下为所有用户和CPU服务？\n答案是间接性。\n间接性意味着不直接访问数据，而是通过一个根据上下文不同而解析结果不同的引用进行访问。内核不指向固定的全局结构，而是使用一个通常称为current的 per - 线程引用，来定位与正在运行任务相关的数据。这就是共享的内核区分数千个隔离进程的方式。\n内核空间在代码上是共享的，而非在上下文上。\n每个进程都映射相同的高地址范围，其中包含内核代码、只读数据、全局符号、设备映射和动态加载的模块。这些区域由共享的物理页面支持，在所有进程中高效且一致。\n但当进程通过系统调用、页面故障或中断进入内核时，它会带来自己的执行上下文。这就是间接性变得至关重要的地方。\n内核不使用全局变量来保存每个进程的状态。相反，每个CPU或线程维护一个指向当前正在运行任务的指针，该指针通常从寄存器或内核栈派生，并通过current宏暴露。因此，当内核代码访问current-\u0026gt;files时，它跟随的是指向该进程文件描述符表的指针，该指针在运行时动态解析。\n这种重定向持续发生。每个系统调用、每个调度决策、每次对内存映射、凭据和信号处理程序的访问都使用间接性来确保正确性，即使相同的代码在所有线程和进程中运行。\n这同样适用于内核栈。每个线程都有自己的内核模式栈，在线程创建时分配。当CPU切换到内核模式时，它会切换到与活动线程关联的栈。没有两个线程共享此空间。局部变量、保存的寄存器和返回地址保持隔离，即使对于瞬态执行状态也能保持安全性。\n间接性是使内核具有可扩展性和安全性的原因。代码是统一的且始终被映射，但上下文（内核看到和修改的内容）严格绑定到当前正在运行的线程。没有这种模型，内核要么需要为每个进程复制自身，要么就得接受关键数据的不安全共享。\n09 内核的设备模型：硬件如何成为/dev 磁盘驱动器不知道/dev/sda是什么，网卡也不知道eth0是什么。\n而内核也不指望它们知道。\n相反，内核维护着一个结构化模型——一个抽象的层级体系——弥合了物理硬件和用户空间可见的逻辑接口之间的差距。从总线和中断到文件描述符和套接字API，这个模型定义了设备如何被发现、命名和使用。\n这一切都始于总线。PCIe、USB、I²C——这些是设备用来自我宣告的通道。内核的总线子系统（drivers/pci/、drivers/usb/等）扫描每条总线，探测连接的设备。如果设备用可识别的厂商和类别响应，内核就会创建一个相应的内部对象（struct pci_dev、usb_device或i2c_client）并注册它。\n但仅有设备没有驱动程序是无用的。一旦被发现，内核就会将设备与驱动程序（知道如何操作它的代码）匹配。块设备驱动程序可能会为PCIe NVMe SSD注册一个gendisk，USB驱动程序可能会为串行适配器暴露一个tty接口，网络驱动程序会注册一个net_device并准备好数据包交换队列。这些驱动程序不需要知道哪个应用程序会使用该设备，只需要知道如何使其正常工作。\n在驱动程序之上，内核将设备组织成类：块设备、字符设备、网络设备。这里的抽象更加明确。驱动程序绑定到这些类，内核则暴露统一的接口（/dev/sda、/dev/ttyUSB0、eth0），而不管底层总线或设备细节如何。对于块设备，内核管理请求队列；对于字符设备，它通过file_operations路由系统调用；对于网络接口，它与IP栈、套接字层和流量控制集成。\n这些接口就是用户空间所看到的。/dev中的文件、/sys/class/net/中的名称、open()返回的文件描述符。应用程序不在乎存储设备是通过SATA、NVMe还是USB大容量存储连接的，这正是关键所在。内核将物理层面的东西抽象为稳定、可导航和统一的东西。\n然而，在这种简单性之下是精心的编排。DMA映射、IOMMU转换、中断路由——这些确保数据高效且安全地移动。对/dev/sda的写入会变成一系列内存操作、排队的请求、DMA传输，最终是设备I/O。发送到eth0的数据包会变成一个sk_buff，传递给驱动程序，为DMA映射，并在网络上传输——所有这些转换都不会暴露给应用程序。\n内核的设备模型使这一切成为可能。它将设备的本质与工作方式分离，将连接方式与使用方式分离。设备被发现、命名、匹配、抽象——然后才作为可用的东西出现在用户空间。\n这就是/dev/sda存在的原因。\n不是因为硬件创建了它，而是因为内核创建了它。\n10 内核如何看待内存：不是映射，而是责任 我们学习内存时，通常从图表入手：虚拟与物理、用户空间与内核空间、低内存与高内存。这些图表很有帮助，它们为我们提供了一幅地图——内存的布局、地址空间中各部分的位置，以及系统的宏观架构。\n但这种视角仍然是静态的。\n它没有展示系统运行时内存的行为，没有展示页面如何分配、回收或移动，没有揭示内存如何在子系统之间共享或为硬件锁定，也没有解释为何某些内存永远不能交换，或为何一个分配器与另一个并存。这些视图描述了内存的形态，却未说明内存的意义，以及内核如何有目的地使用它。\n内核不将内存视为平坦空间来管理，而是将其视为责任。它根据每个子系统的工作方式，响应其需求。内存不是以通用块的形式分配的，而是根据手头任务赋予相应的形式、结构和规则。\n这就是内核称它们为子系统的原因。每个子系统本身就是一个系统。调度器移动线程并管理上下文，网络栈缓冲数据包并处理流控制，文件系统管理元数据、缓存和日志记录，驱动程序分配硬件可见的缓冲区，甚至内存管理器也会跟踪自身——区域、使用情况和回收策略。每个子系统请求内存时，不仅关注大小，还关注意图——如何使用、生命周期长短以及必须遵循的约束。\n内核会倾听，并通过专注、轻量级的接口回应。kmalloc返回快速、对齐的内存供内核内部使用，Slab缓存为可重用的结构化固定大小对象提供服务，vmalloc从分散的物理页面创建虚拟连续的缓冲区，DMA API确保硬件访问的物理安全性，mmap为用户进程提供灵活、受保护的内存视图，并通过陷阱延迟填充和保护。这些不仅仅是API，更是代码与系统行为之间的契约。\n每个请求都流经相同的核心分配器，但带有不同的标志、约束和假设。调用可以阻塞吗？内存需要固定吗？它是可移动的还是可回收的？是短期还是长期存在的？内核跟踪此上下文并相应地分配——无声、高效、持续地进行。\n从外部看，内存似乎很简单：一个指针、一个段、一个页面。但在内核内部，内存不是平坦的——它是分层的、有形状的，并由需求决定。每个子系统不仅仅需要内存，还需要一个适合其功能的工作空间。内核不仅仅是分配，更是理解。\n当我们理解每个意图时，就会明白这些接口为何存在。我们不再被多样性淹没，而是专注于需要完成的任务，并相信内核已经知道如何去做。\n这就是它维持系统运行的方式。\n11 内存不是一个地方，而是一个系统 从系统启动的那一刻起，内核就通过结构化方式管理内存。固件表定义可用区域，内核将其注册为物理段，分类到区域（zone）中，并为每个页面映射元数据。vmemmap区域为不连续的物理内存提供页面描述符的线性视图。分配器基础设施在用户空间启动前完成初始化。\n每个物理页面由一个struct page表示。这些描述符被所有内存子系统使用——包括匿名内存、文件映射内存、slab、vmalloc、页面缓存和回收系统。它们始终被引用，任何分配、映射或回收操作都离不开它们。\n每个进程被分配一个由mm_struct跟踪的虚拟地址空间，其中的区域由定义了边界和标志的vm_area_struct描述。这些区域在发生缺页（page fault）时延迟填充。内核遍历页表，安装中间层级，检查权限，并根据需要分配物理内存。匿名缺页分配新页面，文件映射缺页实例化folio并填充页面缓存。相同的匿名页面可通过KSM合并，大页面可通过THP提升。\n回收是异步且分代的。内核扫描LRU列表或评估MGLRU代际。在压力下触发直接回收，Cgroups隔离内存域，收缩器（shrinker）释放子系统特定的缓存。回收的页面被逐出、交换或回写，DAMON可观察访问模式以优化策略。\nZSWAP和ZRAM提供压缩交换功能，页面在到达磁盘前在RAM中压缩。通过延迟分配和回收减少内存压力，页面迁移和NUMA平衡根据访问局部性重新定位页面，迁移由缺页或后台扫描触发。内存热插拔（memory hotplug）在运行时更新区域边界，ZONE_DEVICE支持CPU无法直接寻址的内存。\n内核通过专用内部接口分配内存：页面分配器返回大块内存，slab分配器服务小对象，vmalloc提供虚拟连续区域，vmap将物理页面列表映射到连续虚拟范围，get_user_pages为内核或设备访问固定用户内存，ioremap创建内核可访问的设备内存映射。\n内核不被动观察内存使用，而是在分配时强制边界，通过元数据跟踪所有权，并通过定义的规则恢复可用性。缺页解决访问问题，保护机制引发陷阱，回收解决不平衡，交换、压缩和迁移响应系统状态。没有轮询，没有被动监控。\n内核将内存作为所有权和重用的分层系统进行管理——从启动到关闭，跨越架构、配置和工作负载。\n一切都经过它，没有它则无物运行。\n12 内核始终存在——你知道它在哪里吗？ 大多数开发者在用户空间中度过时间。内存被管理，崩溃被遏制，隔离得到保证。但在这一切之下是内核——已映射、存在且对软件与硬件之间的每一次交互都至关重要。\n内核不会出现在进程列表中，无法被终止，不能像用户进程一样向它发送信号或跟踪它。然而，每次从文件读取、发送数据包或分配内存时，都在进入内核。内核始终存在，但隐藏在特权边界和硬件保护之后。\n其内存空间是每个进程地址映射的一部分，但只有内核可以访问它。这种分离至关重要：如果用户空间可以自由写入内核空间，一个简单的错误就可能导致系统崩溃，或者更糟。用户空间和内核空间之间的界限不仅仅是技术上的区别，更是使安全、稳定计算成为可能的一道墙。\n但内核的内存空间不是单个块，而是有意构造的。有用于代码、静态数据、动态分配、设备映射、模块加载、每个CPU变量等的区域。每个区域都有自己的规则：有些是只读的，有些是无缓存的，有些直接映射到物理RAM，有些为了灵活性或安全性而虚拟构建。\n当一切正常时，不需要考虑这些。但当出现故障时——当在内核地址中遇到页面错误、驱动程序行为异常或系统无明显原因崩溃时——理解内存映射就变得至关重要。这能让你知道一个地址是在已加载的模块、slab缓存还是设备寄存器中。这是停止猜测并开始诊断的方法。\n内核内存不仅仅是内核所在的地方，更是控制所在的地方。它管理设备访问、为系统管理内存、跟踪任务和线程并处理中断。该空间的每个角落都有意义。即使轻微误用，也可能触发未定义行为、不稳定或需要数天才能显现的微妙数据损坏。\n不必每天记忆地址或深入研究页表转储，但需要在内心中建立内核如何布局内存、存在哪些区域以及如何使用这些区域的模型。这种理解会影响底层代码的编写方式、分析故障的方式，以及设计不仅功能正常而且可靠的系统的方式。\n是的，内核始终存在。但直到了解它在哪里以及如何存在，才只看到了系统的一半。\n13 不只是代码执行：内核实际执行的内容 Linux 内核不仅仅是执行代码，它还控制代码被允许做什么、何时做以及由谁来做。这种控制并非是建议性的或外部分层的，而是在权限与动作交汇的时刻直接执行。\n当发出系统调用时，内核会根据目标资源的权限检查进程的凭据（其 UID、GID、补充组和能力）。这些检查决定是否允许访问。没有它们，用户之间以及用户空间和系统之间就没有界限。\n但仅有权限是不够的。内核还定义了进程可以看到什么。通过使用命名空间（namespaces），它重新映射每个进程对 PID、挂载点和网络接口的视图。这种隔离使进程甚至无法知道其他资源的存在。没有它，权限检查就失去了意义。\n即使进程能够看到某个资源，也不意味着它能够控制该资源。\n特权执行确保影响系统状态的操作（如配置网络设备或加载模块）仅允许从正确的特权级别执行。用户模式和内核模式之间的转换在运行时会被严格验证。\n为了避免“全有或全无”的 root 访问，Linux 应用了能力（capabilities）机制。内核并非通过 UID 0 授予完全控制权，而是按进程强制实施范围狭窄的权利（例如用于网络的 CAP_NET_ADMIN 或用于调试的 CAP_SYS_PTRACE）。在敏感操作期间会直接检查这些能力。\n并非所有动作在所有上下文中都有效。系统调用内的代码可能会阻塞或分配内存，而中断内的代码则不能。内核持续跟踪此执行上下文，并执行安全操作。没有外部逻辑能够可靠地做出此决定。\n策略执行增加了另一个维度。seccomp 过滤系统调用，SELinux 和 AppArmor 等 LSM（Linux 安全模块）应用强制访问规则，Cgroups 控制 CPU、内存和设备使用。这些控制直接集成到系统调用路径、调度器和资源记账中。\n在虚拟化环境中，内核（或其支持的虚拟机管理程序）仲裁客户操作系统的访问。特权指令和硬件 I/O 会被捕获和模拟。没有这一点，客户机可能会危及主机。虚拟化执行保护的是整个系统，而不仅仅是进程。\n这些机制并非独立运作。能力与 LSM 协同工作，命名空间限制可见性，而 Cgroups 限制使用。即使权限允许，上下文也会阻止不安全的动作；即使合法，策略规则也会拒绝危险行为。\n它们共同阻止未授权访问、意外披露、特权升级、不稳定执行和跨虚拟机妥协。内核集成并执行这些机制，并非因为它优雅，而是因为它是唯一具有全面可见性和权威性的层。每个动作都流经它，控制必须存在于那里。\n14 boot结束之处：内核开始之处 在 Linux 中，系统从硬件级设置过渡到与架构无关的内核核心，存在一个精确的时刻，这个时刻由位于 /init/main.c 中的 start_kernel() 函数定义。\n执行甚至更早从引导加载程序（bootloader）开始。加电后，系统固件初始化处理器和内存控制器，然后加载 GRUB 或 U - Boot 等引导加载程序。引导加载程序将内核镜像和可选的 initrd 放入内存，准备引导参数，并跳转到内核特定于架构的入口点。\n这会触发特定于架构的设置阶段，该阶段由汇编语言和早期阶段的 C 代码编写。在 x86 上，这包括 head.S 和 head64.c 等文件。CPU 进入 64 位长模式执行，构建临时页表，清除 .bss 段，并设置初始栈。系统在单个核心上运行，中断禁用，没有调度器，也没有动态内存，其唯一目的是为 C 代码准备一个安全的环境。\n最终，控制权到达 C 级入口点（如 x86_64_start_kernel()），然后调用 start_kernel()。这标志着特定于平台的设置结束，与架构无关的内核接管。\nstart_kernel() 的早期部分在所谓的“早期 C”中运行：这是一个最小且受约束的上下文，内核后来依赖的许多核心子系统尚未可用。没有内存分配器，没有抢占，没有并发。代码不能让步或阻塞，完全依赖静态分配的内存。\n为解决这些循环依赖，内核使用分阶段初始化。轻量级的早期实现（如早期 RCU、早期 printk 和 memblock 分配器）替代完整系统，使相关功能在早期启动期间能安全运行。然后，随着 start_kernel() 的推进，完整的基础设施按严格顺序上线：分配器、调度器、定时器、中断、每个 CPU 区域、RCU 和工作队列。内核变得能够进行异步、并发执行。\n一旦稳定，它会调用 initcalls（初始化调用）：这是一系列结构化的函数，用于启动更高层的子系统，如块 I/O、文件系统、设备驱动程序和网络。\n最后，内核调用 rest_init()。在这里，它创建第一个内核线程：kthreadd，以及启动用户空间的线程（通常是 /sbin/init 或备用 shell）。\n此时，内核完全激活。它调度任务，处理 I/O 和中断，管理内存，并在 CPU 之间协调子系统。\nstart_kernel() 不仅仅是一个函数，它是 Linux 自举的方式。它通过分层、静态设计和精心排序解决依赖关系，在裸机和运行系统之间架起桥梁。在此之后，内核不再对机器做出反应，而是按自己的规则运行。\n15 从vmlinuz到eBPF：Linux内核内部实际运行的内容 Linux内核不仅仅是一个静态的二进制文件。它是一个动态的、可扩展的系统，不同类型的代码会在不同时间、出于不同目的进入并在内核空间中执行。\n它始于vmlinuz，即引导时加载的核心内核镜像。其中包含了 核心组件：调度器、内存管理器、中断处理程序和系统调用调度逻辑。它始终处于映射状态，始终存在，但并不像进程那样被调度。相反，它是响应式的——当陷阱、系统调用或中断发生时进入，并尽快退出。它不等待或循环，只是做出响应。\n编译到这个镜像中的内容（以及被排除在外或做成模块化的内容）由内核配置决定。通过.config，你可以决定哪些功能始终存在，哪些可以按需加载，哪些完全不包含在内。这从内核运行的第一刻起就塑造了它的占用空间、功能和行为。\n接下来是内核模块（.ko文件），它们在运行时扩展内核。这些模块包括设备驱动程序、文件系统、网络栈等。它们不是静态镜像的一部分，而是根据需要加载。一旦插入，它们的运行方式就像内置的内核代码一样，可以完全访问内部API和内存。它们可以在不重启的情况下卸载或更新，使系统具有灵活性和模块化特性。\n然后是eBPF，它采用了不同的方法。eBPF程序用受限的C或Rust编写，编译成安全的字节码，由内核验证，并在运行时注入。它们附着在特定的钩子上（系统调用、跟踪点、网络接口），仅在被触发时执行。它们不存在于内核镜像或模块中，而是在严格约束下在内核空间中运行。启用后，eBPF提供了一种安全高效的方式来观察和扩展内核行为，通过bpftrace等工具被广泛使用，无需修改代码或重启即可跟踪运行中的系统。\n热补丁是另一种运行时代码形式，用于应用于已在运行的内核以修复错误或漏洞。热补丁会在内存中替换特定函数，重定向执行而无需重启。只要系统运行，它就保持活动状态。不过，除非重新应用或包含在较新的内核镜像中，否则它不会在重启后保留。\n在这一切开始之前，还有initramfs。尽管它在用户空间中运行，但会在内核引导后立即执行，并在其控制下运行。它准备系统（加载模块、挂载文件系统），然后移交给真正的init。它不是内核代码，但定义了内核早期使用的内容。\n在内核中运行的不只是编译进去的内容。它还包括驻留的、可加载的、可注入的或可替换的代码，所有这些代码要么源自内核源代码，要么遵循其规则。内核不仅仅是一个二进制文件，它是一个活的系统，由配置塑造，并通过设计进行扩展。\n16 无状态CPU，有状态内核：执行如何被协调 在机器层面，CPU从根本上是无状态的。它一条接一条地执行指令，使用寄存器和内存的内容，而不感知任务、所有权或历史。在任何周期中，它只处理呈现给它的内容，不了解之前或之后的情况。CPU不跟踪正在执行哪个任务或它属于哪里；结构和连续性完全由内核维护。\n这种设计是有意为之的。保持CPU无状态可保留速度、简洁性和通用性。CPU通过其格式化的指令、指令指针（IP）、栈指针（SP）和通用寄存器来暴露其功能。IP决定下一条指令从哪里获取；SP控制临时数据的推入和弹出。它们共同定义了代码和数据的流动。CPU本身不管理上下文或保留连续性；它精确地遵循这些流动，没有记忆。\n在Linux中，内核是真正有状态的实体。它记录并管理每个执行路径的完整上下文，包括CPU状态、内存映射和调度信息。每个执行路径（无论是用户空间进程、进程内的线程还是内核线程）都由一个task_struct表示，其中包含精确暂停和稍后恢复执行所需的一切。\n每个任务都有一个私有的内核栈。对于用户程序，这个栈与用户空间栈是分开的。当任务进入内核模式（通过系统调用、页面错误或中断）时，CPU切换到私有内核栈，允许内核安全地保存寄存器并管理临时数据。没有它，特权操作将不可靠。\n上下文切换是内核用于在执行路径之间移动的机制。它将当前任务的完整CPU状态保存到其task_struct中，恢复另一个任务的状态，并让CPU从那里继续执行。CPU对此并不知晓，它只是从新状态继续执行。这使得许多独立任务可以共享单个核心，在同一时间只有一条指令运行的情况下创造出并行执行的假象。\n在人类时间尺度上，上下文切换发生得如此之快，以至于多个程序似乎在同时进行。但在CPU层面，每个周期都严格专用于一个任务。内核对任务状态和调度决策的精心管理为本身无状态的CPU带来了结构。\nCPU不记忆，它执行。内核记住一切。它推进每个执行路径，在无数次切换中保留身份和连续性，在幕后协调系统。\nCPU带来执行的精确性；内核带来时间上的连续性——它们共同将简单的执行转化为协调的系统行为。\n17 内核构建的内容——逐层构建 Linux内核是分层构建的，但并非作为随意的软件抽象。从上下文切换到内存隔离再到中断处理，每一层的存在都是为了直接解决CPU硬件的限制。内核的结构不是为了掩盖底层机器，而是为了完善仅靠硬件无法提供的功能。\nLinux内核在CPU设计的约束下运行。其核心机制（任务切换、抢占、中断处理和内存保护）不是可选功能，而是对CPU所暴露和省略内容的必要响应。\n现代CPU提供执行单元、寄存器、特权级别、指令指针以及用于中断和虚拟内存转换的硬件机制，但它们不跟踪任务、执行公平性、保留执行历史或管理并发。CPU只执行当前加载的内容，仅此而已。\n上下文切换的存在是因为CPU不会在任务之间保留状态。切换时，内核会将完整的寄存器集（包括栈指针、指令指针和标志）显式保存到即将离开的任务的task_struct中，然后恢复下一个任务的状态。CPU只执行指令，不知道切换已经发生。\n每个任务的内核栈是必需的，因为CPU在从用户模式切换到内核模式时不会分配或隔离栈内存。内核为每个任务分配一个私有的内核栈，并确保所有特权操作都在那里进行，这保证了一致性和内存安全性。\n抢占和调度完全在内核中实现。CPU不衡量时间片或对执行进行优先级排序。内核注入定时器中断，评估调度决策，并使用重新调度标志来控制任务何时让步或继续。公平性和策略是内核级别的构造。\n中断处理反映了更多的约束。CPU引发中断时不考虑当前任务状态。内核在硬中断（hardirq）上下文中处理这一点，在该上下文中不允许睡眠或阻塞。如果需要进一步的工作，它会将执行推迟到软中断（softirq）、任务小项（tasklet）或工作队列（workqueue）上下文，这些上下文在更宽松的环境中运行。\n内存保护使用CPU的MMU，但内核设置页表并在上下文更改时切换它们。CPU执行访问权限，但内核定义内存映射、分配空间并执行所有权边界。\n每个内核层都对应着CPU不做的事情：它不跟踪上下文、隔离栈、管理时间或安全处理嵌套执行。内核相应地构建每个机制。\nLinux内核的架构不是从CPU抽象出来的，而是由它定义的。内核一层一层地填补硬件留下的空白。\n18 内核执行路径：在哪里运行，以及为何重要 Linux内核代码在不同的上下文中运行，每个上下文都有其独特的规则和约束。这些路径定义了内核在给定时刻可以执行的操作（例如是否可以睡眠、阻塞、抢占或访问用户内存），并支配着系统对用户调用、硬件事件和内部活动的响应方式。\n当用户进程发出系统调用时，CPU切换到内核模式，但仍处于该进程的上下文中。内核使用该进程的task_struct执行，此时具有更高的特权。在这种模式下，进程可以睡眠、阻塞、分配内存并处理页面错误。大多数同步系统服务都在该上下文中运行。\n内核线程也在进程上下文中执行，但不与任何用户空间任务绑定。它们由内核创建，用于内存回收、I/O调度或线程创建等后台任务，生命周期较长，像普通任务一样被调度。它们可以完全访问内核服务，并可根据需要睡眠或阻塞。\n当硬件触发中断时，会进入中断上下文。相应的处理程序会立即异步执行，且不在任何进程上下文中。它不能睡眠或阻塞，只能执行有限栈空间下的原子操作。此路径针对低延迟进行了优化，设计为快速退出。任何大量工作必须被推迟。\n内核提供了几种延迟执行路径来处理此类工作。软中断（SoftIRQs）是由子系统或中断处理程序调用的静态注册处理程序。它们在原子上下文中运行，不能睡眠。如果未立即处理，它们会由每个CPU的ksoftirqd线程在进程上下文中执行，但仍受软中断约束。\n任务小项（Tasklets）构建在软中断之上，提供了更简单的序列化API。它们不可抢占，并绑定到特定CPU，确保不会在同一核心上并发运行。与软中断一样，它们不能阻塞或睡眠。\n当延迟工作需要睡眠能力时，内核使用工作队列（Workqueues）。这些工作由kworker线程在完整的进程上下文中执行，因此适用于起源于中断上下文但需要阻塞操作灵活性的任务。工作队列在驱动程序实现中被广泛使用。\n定时器（Timers）允许内核代码在延迟后调度执行。当定时器到期时，其回调函数在软中断上下文中调用，必须简短且非阻塞。\nRCU回调将内存回收推迟到所有读取者完成之后。宽限期过后，根据系统配置，回调函数要么在软中断上下文中运行，要么通过专用线程运行。\n每条路径的存在都有其原因。内核强制实施这些边界以维持正确性、隔离性和响应性。选择正确的上下文并非可有可无，这对编写安全且功能正常的内核代码至关重要。\n19 追踪执行的模板 理解内核并非始于分类，而是始于流程。每次进入内核都会启动一条既定的执行路径——由上下文塑造，通过接口路由，并受到底层分层职责的约束。\n执行始于一个触发条件：系统调用、硬件中断、处理器异常或已调度任务。每种触发条件都有其自身约束。在进程上下文中，允许睡眠和阻塞；在中断上下文中，严格禁止睡眠和阻塞；软中断（SoftIRQ）和任务小项（Tasklet）在原子上下文中运行，灵活性有限；工作队列（Workqueue）和内核线程在线程上下文中执行，但无法访问用户内存。\n一旦执行跨越入口边界，控制权便传递给子系统。这并非通过直接调用实现，而是通过已注册的接口：系统调用表、文件操作向量、协议钩子和驱动程序回调。间接性并非偶然——它定义了谁拥有控制权，以及当前适用的规则。\n每个子系统对其数据和逻辑拥有独占所有权。内存管理通过mm_struct、vm_area_struct和struct page跟踪映射和分配；VFS（虚拟文件系统）抽象文件操作，而具体文件系统管理inode、dentry和file；网络通过sk_buff、套接字状态和按命名空间的路由表处理流量。这些结构是特定领域的，对它们的控制意味着责任。\n执行可能跨层移动——文件系统可能分配内存，网络处理程序可能将工作入队，驱动程序可能启动DMA（直接内存访问）——但转换绝非临时决定。所有移动都经过中介：共享状态在锁保护下访问，通过引用跟踪，并由上下文决定条件。通过命名空间、控制组（cgroup）、能力（capability）和安全模块维持隔离。虚拟化和容器依赖这些相同机制，而非例外。\n每条路径都遵循其开始时的相同规则完成。系统调用返回用户空间，中断处理程序退出到被抢占的任务，内核线程让步，延迟工作要么重新调度要么结束。清理遵循所有权原则：释放内存、减少引用、释放锁。无物脱离结构。\n追踪任何执行时：识别入口、确定上下文、定位调度点、观察所属子系统、跟踪访问的数据，并理解路径如何结束。这同样适用于read系统调用、页面错误、数据包接收或定时器到期。执行不仅是运行的内容，还包括在哪里、何时以及在谁的控制下运行。\n这不是作为代码的内核，而是作为流程的内核——在每次转换时都是结构化、受规范且明确的。呈现的不是描述，而是一个模板：一种可复用的方式，用于在任何条件下追踪跨任何子系统的任何执行路径。即使组件不同，问题始终相同。这种一致性使内核可追踪，且此模型可靠。\n20 中断不是干扰，而是设计 “中断”一词意味着打断、干扰，是意外之事。但在内核以及其底层架构中，中断并非如此。它不是混乱，不是冲突，而是系统主张行动权的方式，无论当前运行的是什么。它是结构化的、可预期的，受设计约束。\n在内核处理中断之前，它早已做出决策：中断将去往何处、如何处理，以及谁不负责处理它。\n每条中断线都注册有处理程序。内核设置向量表、初始化本地和I/O APIC、分配优先级、屏蔽或取消屏蔽中断线，并将每个源路由到逻辑CPU。这些不是反应，而是声明。系统预先构建了时间、设备和其他CPU可能进行干预的确切路径。\n当中断发生时（无论是定时器滴答、网络数据包还是来自另一个核心的关闭请求），CPU切换到内核模式。它保存当前执行状态并开始执行处理程序。但此处理程序不属于它所中断的任务。该任务可能在用户空间，可能在系统调用中途，可能处于空闲状态，这都无关紧要。中断跨越该边界，却不成为其一部分。\n内核精确处理这种区别。处理程序在中断上下文中运行，使用当前任务的内核栈，但从不声明任务的身份。它不修改任务状态，不改变其调度状态，不留下任何痕迹。\n这就是中断不能睡眠的原因。不仅因为它必须快速，还因为它不能成为任务的一部分。睡眠意味着此执行可以被暂停并在调度规则下恢复，就好像它是一个线程。但它不是，它根本不是线程的一部分，而是系统从外部进行的干预。\n当需要更多工作时，内核会移交任务。它委托给软中断或将函数入队到工作队列。这些路径可以安全调度、阻塞和拥有线程。中断路径则不然，它的定义是无上下文、无所有权、无延续。\n当处理程序完成时，内核决定接下来发生什么。如果需要重新调度任务，它会切换；如果不需要，被中断的任务会恢复。无论哪种方式，栈都是完整的，边界得到尊重。被中断的内容和响应的内容之间没有泄漏。\n这就是内核获得控制权的方式。不仅是它可以被中断，还在于它可以响应而不被纠缠。中断不属于它所抢占的逻辑，它的存在是为了确保系统对不能等待用户代码注意到的事件保持响应。\n因此，“中断”这个名字可能听起来像是一种干扰，但设计讲述了不同的故事。\n它不是流程的干扰，\n而是流程之外的路径——结构化、精确且有边界。\n21 执行是逻辑的，位置是物理的 当进程在Linux中运行时，它似乎会精确地从上次停止的位置继续。寄存器被恢复，栈有效，内存布局符合预期。中断处理程序在已知的CPU上下文中执行，系统调用完成时不会中断。从运行代码的角度来看，环境是一致的。\n在这种稳定性之下，系统始终在变化。为了平衡负载、改善内存局部性或响应硬件事件，内核会在CPU和内存节点之间移动实体。任务、内存页面和中断处理程序的这种移动称为迁移。这是系统操作的一个持续部分，并且发生时不会中断执行逻辑。\n使其成为可能的是内核将执行与位置严格分离。执行状态通过上下文切换得以保留。任务可能在不同的CPU上运行，但在恢复之前，其程序计数器、栈和虚拟内存会被完全恢复。进程不会察觉到这种变化。\n内存迁移遵循相同的原则。页面可以在NUMA节点之间移动或在压缩期间重新排列。内核更新页表、使过时的TLB条目失效，并维持一致的虚拟地址视图。只要映射保持不变，底层页面可以自由移动。\n中断也会迁移。设备IRQ会在CPU之间重新路由以分配负载。然而，处理程序仍然在正确的上下文中运行，使用有效的每个CPU结构。传递路径发生变化，但处理程序的执行环境不变。\n关联性对执行可能发生的位置施加了约束，但不会将其固定在适当的位置。任务的CPU关联性定义了其符合条件的位置，调度器会尊重该边界。在这些限制内，可以自由进行迁移。这同样适用于IRQ关联性、内存策略和每个CPU的基础设施。\n这之所以有效，是因为每个子系统都是协调的且具有状态意识。迁移线程在CPU之间安全地执行任务移动。内存子系统跟踪反向映射并更新引用。调度器管理运行队列和抢占以保持执行的一致性。每个部分都确保自身的正确性，因此即使系统重新定位，逻辑模型也能保持稳定。\n这种分离使系统能够扩展和适应而不会失去完整性。代码继续运行，不知道其下方正在进行的物理重组。内核移动它必须移动的东西，并保留它不能保留的东西。\n执行是逻辑的，迁移是物理的。它们之间的边界不是概念上的，而是由允许进程、页面或处理程序移动而不破坏运行内容的机制来维持的。\n22 不仅仅是一段代码：每个内核路径内部的过程 内核代码中的函数不仅仅是返回结果，它必须在严格规则下运行——受系统上下文、安全策略、共享结构和并发控制的约束。它不仅要执行逻辑，还要安全、一致且与系统的其余部分协调地执行。这是程序性的，而不仅仅是功能性的。\n每条内核路径都涉及一系列维度：意图、上下文、执行、对象、状态和同步。这些不是孤立的阶段或层，而是系统行为的相互依存的方面，通常分布在多个函数中。\n意图是指函数预期要做的事情，通常反映在其签名中——名称、参数和返回类型。参数定义调用者提供什么以及数据如何进入系统。在内核代码中，它们必须高效且安全地传递。有些是复制的，有些是通过引用传递的。用户指针需要验证，大型结构作为指针传递以减少开销。每种形式都反映了意图、成本和正确性。\n上下文指的是函数执行的位置。进程上下文允许阻塞、页面错误和带可睡眠标志的分配，中断上下文则不允许。在中断上下文中运行的代码绝不能睡眠，并且必须快速完成。函数必须明确考虑这一点，违反上下文规则可能导致死锁、崩溃或未定义行为。\n执行验证操作是否被允许。权限、能力、命名空间和安全模块都参与其中。这些检查嵌入在执行路径中，而不是在其外部。执行是核心操作——读取、写入、映射、调度等，它仅在验证后开始。\n对象指的是涉及的共享结构：task_struct、file、inode、mm_struct、socket。这些结构可能被并发修改，安全访问需要引用计数、锁或RCU。状态包括持久更改：文件位置、计数器、时间戳、I/O统计信息，这些在并发情况下和中断后必须保持一致。\n同步确保正确性。锁、屏障和原子操作防止竞争条件，没有同步，对对象或状态的更新都不安全。\nvfs_write反映了这些维度。它的意图是用户写入请求，其上下文是具有出错权限的进程模式，执行检查模式和安全钩子，执行委托给文件系统，对象包括file和inode，状态在位置和记账中更新，同步确保正确的顺序和互斥。\n没有单个函数能完整表达所有维度，但每条内核路径都经过它们。一些函数验证，另一些更新，有些只起保护作用。了解这些维度至关重要，它们定义了内核如何在并发、共享的特权系统中保持控制。\n23 内核如何自我通信——内部通信工具 在 Linux 内核内部，代码在不同的上下文中运行：用户发起的系统调用、硬件中断、延迟处理程序和内部内核线程。每个上下文都在特定的约束下运行——有些可以阻塞或睡眠，另一些则必须快速执行且不被中断。尽管存在这些差异，内核仍通过一组专为安全高效通信设计的内部工具，实现了跨上下文的数据交换和协调。\n共享内存结构提供了基础。缓冲区、队列和状态字段等对象可在多个执行路径中被并发访问。根据上下文的阻塞能力，使用自旋锁、原子操作或 RCU 等无锁技术来维持同步。这些机制在不影响并发性或响应性的前提下，确保了一致性。\n为支持阻塞操作，内核使用等待队列（wait queues）。无法立即进行的系统调用可能会阻塞并在等待队列上睡眠。另一个上下文（通常是中断处理程序或内核线程）可在条件变化后唤醒该进程。这将请求与其解决解耦，而不会浪费 CPU 周期。\n中断处理程序面临最严格的限制：它们不能阻塞、分配内存或访问用户空间。其作用是确认硬件并标记延迟工作。如果需要额外处理，内核会调度软中断（softirq）或任务小项（tasklet），它们会在中断后不久运行，但仍处于非阻塞约束范围内。\n对于需要更多时间或灵活性的操作，内核使用工作队列（workqueues）。这些工具将任务委托给内核管理的线程，使其能够独立运行并完全访问内核服务。工作队列广泛用于延迟 I/O、内存回收和异步设备处理。\n为实现快速无锁信号传递，内核使用原子标志和计数器。更新单个位或计数器可向另一个上下文通知就绪、进展或完成状态。这些在网络和存储等性能关键路径中尤为常见。\n内核线程是由内核创建的持久性、调度器管理的任务。与中断或软中断不同，它们可以阻塞、睡眠并使用任何内核 API。它们处理后台工作、延迟清理和必须独立于用户活动运行的周期性任务。\n回调（Callbacks）完善了协调模型。子系统注册函数指针，以便在满足特定条件时调用。这些处理程序在遇到条件的上下文中执行，实现了子系统间响应迅速、解耦的行为。\n这些工具（共享内存、等待队列、工作队列、原子变量、内核线程和回调）共同构成了 Linux 内核的通信架构。每个工具都针对其执行上下文量身定制，实现了整个系统精确、可靠的协调。\n24 内核模块仅通过导出符号相互认知 内核模块是独立编译的内核功能单元，设计为可在运行时加载。它们提供了一种灵活的方式来扩展内核（通常用于设备驱动程序、文件系统、密码学例程或协议实现），而无需完全重建或重启。一旦插入，模块就成为运行内核的一部分，在特权空间中运行，可完全访问内核环境。\n尽管具有这种级别的访问权限，但内核模块在设计上是隔离的。除非内核函数或变量已被显式导出，否则模块不能引用或调用它们。内核不提供发现、延迟绑定或符号查找功能。所有交互必须通过通过EXPORT_SYMBOL或EXPORT_SYMBOL_GPL声明的预定义接口进行。\n这种边界是有意设定的。内核不保证稳定的内部ABI，并且除了导出的接口外不提供任何支持。任何未显式导出的符号都被视为内部符号，可能在版本之间更改或删除。导出接口是模块与内核其余部分之间唯一受支持的边界。\n加载模块时，其未定义的符号会与仅包含内核选择公开的标识符的全局符号表进行匹配。如果缺少必需的符号，模块将无法加载。解析仅在加载时进行一次，不能动态调整。\n模块不能访问彼此的内部符号，除非这些符号被显式导出。模块是一起开发、在相同配置中编译，还是放在相关的源目录中，都没有区别。没有EXPORT_SYMBOL，符号就是不可见的。从模块的角度来看，未导出的内容就不存在。\n模块分层和堆叠很常见，但必须严格显式。一个模块可能依赖另一个模块来注册回调、提供处理程序表或公开实用函数，但只能通过导出符号和已建立的注册点实现。运行时不会发现或连接任何内容。\n这不是技术限制，而是有意的架构决策。内核强制实施这种分离，以保持内部灵活性、版本独立性和系统完整性。像task_struct、cred和mm_struct等关键结构是内核行为的核心，除非通过安全接口有意公开，否则永远不能直接访问。\n每个模块都被视为外部代码，无论它与内核功能的集成多么紧密。它必须声明许可证，预先解决依赖关系，并将其访问限制为内核明确选择公开的接口。\n内核模块仅通过导出符号相互认知，没有例外。这不是惯例，而是强制的设计：精确、有意，且对Linux内核的长期稳定性至关重要。\n25 搭建组件之间的桥梁 现代系统由独立运行的部件构成。内存以页为单位组织，磁盘存储块，网卡传输数据包，CPU在寄存器中逐次执行指令。每个组件都按自身规则运行，对其他组件毫无所知。\n用户空间看不到这些细节。进程打开文件、发送数据、分配内存并执行逻辑，却无需了解这些请求是如何实现的。这种假象之所以成立，是因为内核能看到底层的每个组件，并懂得如何在它们之间进行转换。\n内核并非通过扁平化来统一系统，而是维持各组件的分离，通过精心控制的结构将一个域映射到另一个域。它跟踪边界并解决可能导致系统无法使用的不匹配问题。\n文件描述符不是文件，而是对struct file的引用，该结构指向inode，inode又映射到块范围，通过块层解析为磁盘I/O。当请求读取时，内核沿此路径确定数据的物理位置，将I/O操作入队，并将结果复制到用户内存（若该内存已映射、可写且符合策略允许）。\n用户空间中的指针实为虚拟地址。内核通过进程的页表将其映射，页表指向物理内存。这些页面可能是匿名的、基于文件的，或当前已换出。每个页面都在内核空间中被跟踪，其元数据描述了引用计数、状态、标志和访问约束。这不仅是地址转换，更是一组保证，可在负载下维持隔离性、安全性和公平性。\n网络数据包以DMA缓冲区的形式到达NIC，内核将其提取到sk_buff结构中，解析协议头，对流量分类，并将它们入队以传递给匹配的套接字。该套接字属于某个进程，进程看到的是流。但该流之所以存在，是因为内核强制实现了一致性，处理了重新排序、超时、确认和流量控制。每次发送或接收调用都依赖这种底层结构。\n每个子系统都不了解其他子系统。内存子系统知道页，却不知道文件；文件系统知道inode和块，却不知道进程；网络栈理解数据包和协议，却不知道程序或流。它们在架构上是独立的。只有内核能看到所有子系统，并维持映射，使它们以一致、可控和可组合的方式交互。\n内核不消除限制，而是理解限制。它尊重每个组件的自然边界，并在它们之间搭建桥梁——虚拟与物理、逻辑与程序、共享与隔离的桥梁。用户空间依赖的每一个抽象（每一个系统调用、每一个文件、每一个套接字、每一个映射页）之所以存在，只是因为内核在不兼容的域之间进行中介，并将系统维系在一起。\n26 libc之外：用户空间与内核的真实通信方式 当大多数人思考用户空间如何与Linux内核通信时，他们会想到libc。这是有道理的——libc提供了open、read、write和malloc等常见函数，它带来了便利性、可移植性和一致的API。但libc只是一个层，并非唯一的接口。\n在其之下是一个更广泛且经过精心设计的系统。libc封装了系统调用，但这些系统调用可以直接调用，无需借助库。更重要的是，并非所有的内核交互都完全依赖系统调用。\nLinux内核通过多种接口展示自身。像/proc和/sys这样的虚拟文件系统提供了对内部状态和配置的结构化访问；ioctl支持特定于设备的控制路径，这些路径不适合标准的读/写模型；mmap允许用户空间和内核空间之间直接内存映射，以实现高效I/O；ptrace等工具为调试提供了底层进程控制；Netlink套接字在用户空间和内核子系统之间实现了结构化的异步通信；而eBPF则引入了一个可编程运行时，可在预定义的钩子处安全地将逻辑注入内核。\n这些接口的存在是有原因的。内核并非强制使用单一路径，而是支持一系列交互方式，每种方式都适合不同的目的。脚本可能从/proc读取数据，对性能要求严格的服务可能依赖mmap或io_uring，跟踪工具可能附加eBPF程序来实时观察内核行为。\n在内部，这些路径汇聚到共享逻辑。无论是处理系统调用、文件读取还是Netlink消息，内核都使用通用的调度表、内部抽象和子系统来处理请求。表面可能不同，但基础始终是统一的。\n这种灵活性并非偶然，它反映了塑造内核开发的原则：不破坏用户空间、保持兼容性、为可扩展性而设计。即使内部发生演变，接口也保持稳定；机制优于固定策略；模块化使子系统能够独立发展，同时与整体保持一致。\n这正是Linux同时具备稳定性和适应性的原因。旧工具继续有效，新工具获得发展空间。多种接口并存并非碎片化，而是有意的设计。libc仍然是最常见的内核入口路径，但它只是众多路径之一，所有路径都被设计得安全、有目的性且精确。\n这不是偶然，而是设计。\n27 CPU不移动数据——但没有CPU，什么都无法移动 每次I/O操作都是CPU、内核和硬件之间的协同工作。CPU不直接传输数据，它发起数据交换、准备内存并等待完成。\n这始于内存映射I/O（MMIO），设备的控制寄存器会作为物理地址暴露出来。当CPU向这些地址写入时，就是在发出命令：配置、启动、停止或请求状态。这些命令会被设备的控制器接收，控制器负责将高层指令转换为底层硬件操作。无论是USB主机控制器、SATA主机总线适配器（HBA）还是NVMe引擎，控制器都会解释这些写入操作，并在设备端管理相关操作。\nMMIO事务通过系统总线（通常是PCI Express）传输。总线就像连接CPU、内存和设备的硬件通道，负责在端点之间路由命令和传输数据。\n发出这些命令、准备内存和设置传输的逻辑由设备驱动程序处理，驱动程序是内核的一个组件，充当操作系统和设备之间的软件接口。驱动程序会为控制器设置DMA地址，并在内核中注册中断处理程序。\n一旦命令发出，控制器就会接管。内核已经代表设备分配并映射了内存缓冲区。CPU告诉控制器这些缓冲区的位置，然后退到一旁。\n此时，直接内存访问（DMA）成为主要角色。控制器使用其DMA引擎在设备本地内存和系统内存之间直接传输数据，完全绕过CPU。这使得高吞吐量设备能够高效运行，而不会因每个字节的传输而占用CPU周期。\n传输完成后，控制器不会将数据推回CPU，而是引发中断——通过中断控制器路由的硬件信号，促使CPU暂停、切换上下文并调用内核级处理程序。处理程序检查状态，将缓冲区标记为已完成，并可能在中断上下文之外调度后续处理。然后，CPU恢复之前的任务。\n每个部分都扮演着不同的角色：CPU发起操作，控制器执行操作，总线负责连接，DMA移动数据，中断提供通知，驱动程序进行协调，而内核将所有部分结合在一起——确保安全性、管理内存并维持协调。\n从read()这样的系统调用到网络数据包的到达，这种流程始终在表面之下持续发生。\nCPU不搬运数据，但没有它系统就无法工作。\n28 时间与精度：内核眼中的CPU执行 运行在2.4GHz频率下的现代英特尔x86-64 CPU，每秒可完成24亿个时钟周期。在这样的速度下，单个操作发生得太快，难以直观理解其意义。为了更好地理解CPU的内部活动，我们可以对时间进行缩放：将一个CPU周期视为人类的一天。\n按此比例，1个CPU秒将代表24亿天，约合657.5万年。2.4GHz下的1个CPU秒 ≈ 657.5万年\n这种时间缩放有助于阐明每个CPU周期的独特性。即使是最小的操作（如一次简单的加法、寄存器移动或内存查找），都是纳秒级的独立且经过深思熟虑的步骤。尽管每秒会发生数十亿次操作，但每个周期在纳秒级别上都是有意为之、结构化且独立的。\n在此比例下，即使是单个系统调用或中断也会成为重大事件。系统调用涉及用户空间与内核之间的协同转换。保存上下文、切换页表、执行系统调用逻辑以及恢复执行状态，通常需要100到200个周期。中断也遵循严格的流程。当硬件中断发生时，CPU会保存关键寄存器，并通过快速硬中断处理程序将控制权转移给内核，通常会将进一步的工作推迟到软中断处理。即使在高负载下，这些转换也能可靠地发生。\n系统通过快速上下文切换处理数千个任务。通过仅用数百个周期保存和恢复执行上下文，CPU和内核维持了许多操作同时进行的假象。每个上下文切换、进程调度和中断处理操作，都在CPU内部时间尺度的精度内精心编排。\n内核管理CPU执行和系统活动，但仅通过CPU驱动的指令与设备通信。所有内核操作最终都是为用户空间服务，确保内核之外的进程能够可靠执行。\n随着操作从CPU寄存器延伸到缓存再到主内存，延迟会增加。然而，内核和CPU在所有层级上都保持精确同步。每条指令、内存访问、上下文切换和中断都符合严格的结构，确保即使在纳秒级分辨率下，执行也能保持可预测、连贯和可靠。\n内核设计不仅关乎功能，还关乎与处理器内部时间尺度的协调。在这个尺度下，每个周期都代表着有意义的动作，每个操作都为每秒数十亿次执行中的系统稳定性和意图做出贡献。\n29 内核在虚拟化中的角色：理解KVM 基于内核的虚拟机（KVM，Kernel-based Virtual Machine）使Linux内核能够向用户空间应用程序提供硬件辅助的虚拟化功能。它本身并不是一个完整的虚拟机管理程序，而是一个暴露现代CPU内置虚拟化功能的内核模块。当与QEMU等用户空间虚拟机监视器结合使用时，KVM便构成了一个完整、高效且模块化的虚拟化平台。\nQEMU在用户空间运行，负责定义虚拟硬件、分配内存和模拟设备。要启动虚拟机，QEMU会通过/dev/kvm接口与内核通信，请求创建虚拟机及其虚拟CPU（vCPU）。每个vCPU都由一个内核线程支持，并像任何其他任务一样由Linux内核调度。\n在支持如Intel VT-x或AMD-V等虚拟化扩展的处理器上，客户机代码在不同于主机的特殊CPU模式下执行。这种模式区别对于客户机而言称为非root模式，对于主机而言称为root模式。这些模式是处理器虚拟化功能集的一部分，与传统的特权级别（如0环或3环）完全独立。例如，客户机内核在0环中运行，但处于非root模式，而主机内核则在0环（ring 0）的root模式下运行。\n当虚拟CPU执行客户机代码时，它在非root模式下运行，允许大多数指令直接在硬件上执行。这实现了高性能且开销极小。然而，某些操作（如访问I/O端口、修改控制寄存器或执行特权系统指令）在此模式下是不允许的。当CPU遇到此类指令或预定义条件时，会执行VM退出（VMEXIT），将控制权从非root模式转换为root模式，把执行交还给内核。\nKVM会检查退出原因并相应地处理。如果退出涉及CPU内部状态或内存管理，可能在内核中解决；对于与I/O或设备相关的操作，该事件会转发给QEMU，由其执行所需的模拟。然后，QEMU使用KVM接口更新客户机的虚拟CPU状态，KVM从中断点恢复客户机的执行。\n为了隔离客户机内存，KVM使用如扩展页表（EPT，Extended Page Tables）等硬件辅助技术，将客户机物理地址转换为主机物理地址。这些映射由内核管理，并在发生更改时同步，确保安全且一致的内存访问。\n当虚拟机关闭时，KVM会释放所有相关资源并重置CPU的虚拟化状态。在整个生命周期中，KVM管理执行转换、隔离边界和底层CPU控制，而QEMU处理更高级别的编排。它们共同提供了一个高性能、安全且深度集成到Linux内核中的虚拟化解决方案。\n30 两个世界，一个CPU：虚拟化中的root操作和非root操作 现代英特尔处理器的虚拟化依赖于通过虚拟机扩展（VMX，Virtual Machine Extensions）建立的执行环境的严格划分。这种分离定义了两个操作世界：VMX root模式和VMX非root模式，从而在CPU的严格控制下实现安全、高效的虚拟化。\n当KVM模块设置了CR4.VMXE并执行VMXON时，VMX操作开始，将CPU转换为root模式并启用VMX指令。在客户机运行之前，KVM为每个虚拟CPU（vCPU）分配并配置一个虚拟机控制结构（VMCS，Virtual Machine Control Structure），其中包含客户机和主机处理器状态以及执行和控制字段。\n客户机执行以VMLAUNCH开始或以VMRESUME恢复。CPU从VMCS加载客户机状态并进入VMX非root模式。在这种模式下，客户机操作系统直接在硬件上运行，同时与主机隔离，即使在 0 环（ring 0）执行也是如此。\n大多数客户机指令在执行时无需虚拟机管理程序干预，除非被VMCS控制所禁止。特权指令、I/O端口访问、控制寄存器修改或外部中断会触发VM退出。在VM退出期间，CPU将客户机状态保存到 VMCS 中，恢复主机状态，记录退出原因，并将控制权交还给在root模式下运行的KVM。\nKVM读取退出原因并处理该事件。CPU状态更改、特权操作或中断处理直接在内核中处理，而设备访问或用户驱动的事件则转发给QEMU等用户空间监视器。如果vCPU线程被抢占，KVM会保存客户机上下文并让步给Linux调度器。\n在vCPU之间切换时，KVM使用VMPTRLD加载新vCPU的专用VMCS。每个vCPU维护自己的VMCS，切换涉及更新活动VMCS指针以确保客户机之间的隔离。Linux调度器将vCPU视为普通线程，允许在vCPU之间或客户机与主机进程之间进行公平调度。\n重新调度后，当再次选择vCPU时，KVM会根据需要更新VMCS，并使用VMRESUME恢复客户机执行。\nVMCS在转换过程中维护处理器状态，包括通用寄存器、控制寄存器、指令指针、标志和执行控制。对VMCS字段的精心管理可最大限度地减少开销、保持隔离并确保客户机行为正确。\n客户机和主机之间的内存一致性通过INVEPT和INVVPID等指令维护，从而能够选择性地使地址映射和TLB条目失效，而无需完全刷新处理器。\n当客户机终止时，KVM发出VMXOFF，结束VMX操作并将CPU恢复为正常的主机执行。\n通过root模式和非root模式之间的结构化划分、每个vCPU的专用VMCS结构以及KVM转换的协调，现代处理器提供了安全高效的硬件虚拟化。\n31 内核与VirtIO：无需模拟的网络驱动程序 当虚拟机发送或接收数据包时，Linux内核不会模拟物理网卡，而是直接使用虚拟机监视器（VMM）暴露的虚拟设备，并通过VirtIO驱动程序提供的半虚拟化接口工作。就网络而言，该驱动程序是virtio_net，它完全在内核空间中运行，无需设备模拟即可处理数据包流。\nVirtIO定义了客户机与主机之间的共享内存传输机制，它用基于内存映射环和事件信号的精简协议取代了模拟硬件的开销。客户机看到的是标准网络接口，主机则直接移动数据，双方通过各自的内核进行协调。\n在客户机内部，virtio_net注册一个虚拟以太网接口。应用程序进行诸如send()和recv()之类的套接字调用，而意识不到不涉及任何物理网卡。在底层，驱动程序分配数据包缓冲区并将它们组织成称为virtqueue的环形结构，每个结构包含一个描述符表、一个用于出站缓冲区的可用环和一个用于已完成缓冲区的已用环。\n这项工作完全由客户机内核管理，它用描述符填充可用环，处理后从已用环回收描述符，并处理信号（在数据包准备好时通知主机，在数据到达时响应中断）。\n主机内核也扮演着同样积极的角色。当虚拟机在QEMU等虚拟机监视器下启动时，VMM会配置VirtIO网络设备，并通过/dev/vhost-net向主机注册客户机的内存布局和virtqueue地址。从那里开始，vhost_net模块接管，它作为内核线程运行，完全绕过用户空间以实现高性能网络。\n对于出站流量，vhost_net直接从客户机的TX virtqueue读取数据，并将数据包转发到TAP设备（主机上的虚拟第2层接口）。对于入站流量，TAP接口接收以太网帧，vhost_net将它们写入客户机的RX缓冲区，更新环状态，并引发eventfd。KVM的irqfd机制将其转换为传递给客户机的虚拟中断。\nTAP设备连接到Linux网桥，该网桥充当虚拟交换机，在虚拟机和物理网卡之间路由流量。主机内核管理此流程，确保所有组件之间可靠高效地传递数据包。\n这一切都不涉及硬件模拟，数据包永远不会通过模拟设备。相反，两个内核都在共享协议中执行各自的角色。VirtIO不模仿硬件，而是实现直接协作。\n这不是模拟，而是真实的内核到内核的协调。\n32 一切仍由操作系统掌控 现代系统涵盖诸多层面：语言、运行时、解释器、容器、模型、协议。但每一层最终都会将控制权传递给同一个核心层。\n内核仍在管理着这条路径。\n执行始于结构化逻辑：创建进程、映射内存、调度指令、将输入输出作为流打开、连接套接字、驱动程序移动数据、处理中断、激活硬件。每次转换都由内核验证、隔离和调节。\n即便如今——当代码在托管运行时中运行、由事件触发、在容器内或跨虚拟机执行时——控制流依然似曾相识。每次内存访问、I/O操作和任务切换都通过内核暴露的接口进行。边界依然存在，契约依然有效。\n这背后的结构并非新事物，它由冯·诺依曼架构定义：共享内存中的代码和数据，指令按顺序提取和执行，状态一次更新一个操作。这仍是通用机器的模型，内核完全在其中运行，协调所有更高层。\n但并非所有变化都是渐进的，有些变化可能是结构性的：\n当系统不再基于冯·诺依曼模型运行时（即没有取指-解码-执行循环、没有共享指令/数据内存、没有程序计数器），结构就会改变。 当内存不再按字节寻址时（即访问变为基于图、内容寻址或与逻辑物理共存），接口就会中断。 当执行不再通过系统调用，且用户空间与内核空间的边界消失时，控制模型就不再适用。 当不再需要内核来抽象设备、隔离进程或调度计算时，它就不再是系统的核心。 当逻辑不再是编写、编译或符号解释的，而是通过学习、涌现或无需离散指令的模拟来实现时，软件本身就已发生转变。 在那之前，结构依然稳固。\n系统仍一次运行一条指令，仍将代码存储为数据，仍会陷入内核，仍在等待权限，仍只在被询问时回应。\n而当它必须真正运行时，内核仍会响应。\n33 对齐即理解 本节的标题的含义是说，Linux的文档、代码实现和运行时行为都是保持一致的，即三者“对齐”，对齐了就容易通过任意一部分去了解其他部分，不存在理解负担。这里的“对齐”不是指技术上的字节对齐。​\t—— 张小方注\n没有任何单一来源能完整描述内核是什么或其行为方式。\n文档概述预期，代码定义已实现的内容，运行时行为展示系统在真实条件下的实际表现。每一层都至关重要，孤立来看都不完整。\n文档反映设计意图——系统旨在做什么、受哪些约束、在何种上下文中运行。它可能描述接口、锁规则、内存语义或策略边界，但本质上是不完整的。文档分部分开发，清晰度和覆盖范围参差不齐，通常滞后于代码，且子系统之间存在差异。\n代码定义机制，实现控制流、数据结构、状态转换和执行边界。其行为精确，但目的未必总是明确。约束可能隐含，命名可能反映早期设计，决策背后的原因往往缺失。代码展示内核做了什么，但未必解释为何这样做。\n执行揭示实际行为，展示哪些路径处于活跃状态、哪些锁存在竞争，以及在真实时序和并发条件下哪些假设成立。行为由配置、硬件拓扑和工作负载塑造，反映实际发生的情况——而非预期或理论上允许的情况。\n当这些层达成对齐时，理解才会浮现。\n对齐并非指完全一致，而是指将文档、代码和行为结合审视——行为可追溯至其实现，实现在上下文中被理解。这使得清晰推理成为可能：结构如何填充、为何选择特定路径、观察到的行为是否有效。\n这并非调试练习，而是学习方法。文档、代码和运行时之间的每处差异都揭示了有意义的内容。缺口可能反映历史，不匹配可能反映演进，未明说的假设可能反映性能、可移植性或遗留约束。\n差异是可预期的。文档可能过时，代码可能执行不再被描述的约束，运行时可能倾向于曾被视为例外的路径。这些未必是错误，而是持续变化的系统的副作用。\n内核并非静态的。代码渐进演化，文档独立维护，行为通过集成和使用呈现。理解取决于比较这些视角并解析它们所揭示的内容。\n这个过程并非线性。它始于阅读，但通过跟踪、观察和关联才变得真实。一次提交解释变更，一次跟踪显示其影响，一个结构揭示其生命周期。这些共同形成可测试和推理的模型。\n文档提供意图，代码定义机制，执行揭示真相。\n只有通过它们的对齐，理解才会成形。\n这就是方法。\n34 如果内核不是由 Linus 创建和维护，会怎样？ 本节是一首歌颂Linux内核贡献者的打油诗，小方翻译时，雅致谈不上，只能尽量表达其原意。​\t——译者：张小方\n到 20 世纪 90 年代初，相关理念已初具雏形。\nGPL（通用公共许可证）为软件自由奠定了法律框架。\nUNIX 展现了可组合系统的强大力量。\n自由软件工具正广泛传播。\n但理念无法自行构成一个系统，\n更无法形成一个持久的系统。\n接下来发生的不只是一个项目，\n而是一系列清晰、务实且悄然激进的决策。\nLinus 将内核按 GPL 发布，\n此举并非为了发表声明，而是为了让所有人的贡献都能安全纳入。\n他选择 C 语言，\n并非出于怀旧，而是因为它能提供精确性、可预测性以及对每个字节的控制。\n他构建了具有模块化边界的单体内核，\n在性能与灵活性、简洁性与可扩展性之间取得平衡。\n当内核发展超出原有工具的支持范围时，\n他没有等待解决方案，\n而是自己编写了一个。\nGit 不仅仅是工具，更是大规模信任的基础设施。\n最重要的是，他创建了一种变革需肩负责任的流程：\n子系统有所有者，\n所有权意味着审查，\n审查意味着信任。\n代码并非通过共识在 kernel 中流动，\n而是通过信任链流转：\n从贡献者到维护者，从维护者到集成树，\n再通过拉取请求到达 Linus。\n每一步的把关依据并非头衔，而是赢得并维持的信任。\n这种信任成为了结构，\n而这种结构得以存续。\n这并非由某个基金会设计，\n也不是从委员会中产生，\n更不是为速度而优化。\n它由约束塑造，\n源于在不丧失可靠性的前提下管理复杂性的需求。\n这正是它得以扩展的原因，\n得以吸纳新架构、驱动程序、文件系统、调度器，\n得以引入成千上万的贡献者，\n同时不损害系统的完整性。\n不难想象另一种起源：\n由企业发起，\n由委员会驱动的标准，\n为某产品线构建的内核，仅维护至下一个产品发布。\n或许它会附带保密协议而非邮件列表，\n或许补丁需通过审批链而非公开审查，\n或许开发速度会更快——直到某一天无法继续。\n它可能更早被采用，\n可能发布时功能更多，\n但不会像现在这样持久。\n内核的持久力不仅仅源于清晰的抽象或巧妙的代码，\n更是关于如何接受变革、如何分担责任、如何赢得并维护信任等决策的结果。\n这些决策早早做出，\n始终如一地执行，\n历经时间考验。\n没有任何部分是必然的，\n它的存续并非与生俱来，\n而是通过纪律、结构和信任得以维护。\n它仍在运行，\n不是因为一成不变，\n而是因为它被设计为可谨慎变革，\n因为信任从未被视为事后考虑。\n而这，同样是一个决策。\n35 配置并非定制，而是内核的身份标识 Linux内核既非为嵌入式系统而打造，也不是专为服务器而设计。它的构建目标是成为目标所需的样子，而它了解自身角色的唯一途径便是通过配置。\n对于内核而言，.config并非一组偏好设置，而是一项结构性决策。它定义了内核被允许知晓什么、必须忽略什么，以及将包含自身的哪些部分。这并非定制层，而是身份的声明。\n这种区别之所以有效，是因为内核在保持稳固的部分和可变的部分之间划清了界限。\n保持稳固的是设计：系统调用接口、调度器、内存管理器、设备模型和内部进程架构。这些是不变的，无论运行在嵌入式环境还是云规模环境，无论是最小化还是功能丰富的场景，它们都定义了内核的结构。\n硬件随目标而变化。嵌入式系统可能使用SPI连接的传感器和最小化的MMU；服务器可能依赖PCIe设备、NUMA内存和虚拟化扩展。但内核并非通过为每个平台重写逻辑来适应，而是通过框架保持一致性。\n框架架起了逻辑与物理的桥梁，它们定义设备的功能，而非连接方式。网络栈不关心NIC是在PCIe上还是USB上，输入子系统不区分I2C上的触摸屏和PS/2上的键盘。重要的是每个驱动程序都符合共享框架。\n这些框架是保持稳固的一部分，它们为不同的物理配置和统一的内核行为提供了共同基础。由于接口不变，逻辑也不会改变，只有其背后的硬件会改变，甚至这也只是因为目标需要。\n可变的是内核周围系统的接口：ACPI或设备树、PCI或SPI、NUMA或flatmem、抢占式或实时、服务器级或精简版。内核不会在运行时扩展自身以匹配环境，而是在构建时就被塑造以履行分配的角色。\n该角色不是被发现的，而是被声明的。架构、可用驱动程序和启用的子系统在内核运行前就已选定。内核不是通过反应来适应，而是通过接受定义来适应。\n这就是为什么配置并非表面功夫，它不是对已存在的内核进行调优，而是告诉内核它被允许成为哪个版本的自身。一旦构建完成，该身份就固定了。\n无论最终是在小型ARM板还是虚拟机监控器主机中，内核都按配置的方式运行。这不是因为它的猜测，而是因为它被指示如此。\n配置不是定制，而是承诺。\n36 内存生命周期与塑造它的角色 在Linux内核中，内存通过一系列构成生命周期的职责进行管理。这些角色（请求者、分配器、访问者、所有者、释放器等）并非显式声明，而是通过函数、结构和约定来体现。\n这个过程始于内核子系统（如文件系统、网络栈或驱动程序）使用kmalloc、vmalloc或alloc_pages请求内存，这是请求者的角色。\n分配器（主要在mm/中实现，由slab、slub、buddy或vmalloc支持）选择一个内存区域，更新struct page或struct folio中的元数据，并返回一个指针。它授予访问权，但不跟踪内存的使用方式或时间。\n访问者使用该内存存储内核结构、缓冲区或状态，这需要精确操作。诸如越界访问或释放后使用等错误并非源于分配，而是源于误用。\n所有权通过引用计数或RCU跟踪。像sk_buff、inode和net_device这样的结构管理自身的生命周期，当引用数降至零时，该内存就有资格释放。\n释放器使用kfree、vfree或__free_pages释放内存，将其返回给分配器。分配器可能将其合并到空闲列表或应用中毒模式，但不验证正确性，这仍是所有者的责任。\n误用由KASAN、page_owner和kmemleak等跟踪器捕获，它们暴露释放后使用、跟踪分配位置并报告泄漏。这些工具支持开发，但不属于核心内存路径。\n诸如ftrace、perf和eBPF等观察者监控时间、频率和分配行为，它们提供洞察而不影响逻辑。\n访问控制由SELinux、AppArmor和cgroups执行，它们在分配前评估凭证并应用策略，其角色是执行约束，而非直接管理内存。\n在压力下，内核调用回收器（kswapd、收缩器和OOM逻辑）从缓存和匿名页面回收内存，这些操作独立于原始分配器运行。\n页面也可能被迁移器移动以进行压缩、大分配或NUMA平衡，这些操作在保留数据的同时更新映射。\n在硬件边界，内存管理单元（MMU）维护页表并执行权限，错误在硬件中隔离无效访问。\n尽管此逻辑的大部分位于mm/中，但没有单个模块管理整个生命周期，每项职责由内核的不同部分处理。内存安全不是通过中央监督来维护的，而是通过一致的边界和协调来维护的。\n内核中的内存流经函数和结构，而非声明。这些角色未被命名但真实存在，通过设计执行并通过纪律维护。\n37 中断如何在不变中演变 Linux 始终分两个阶段处理中断：立即运行的快速上半部分，以及完成剩余工作的延迟下半部分。尽管这一模型从最早的内核起就保持一致，但其背后的机制已发生显著演变。\n在 Linux 1.x 和 2.0 中，下半部分实现为全局静态处理程序列表。即使在多处理器系统上，一次也只能运行一个，这种序列化设计随着对称多处理的普及成为可扩展性瓶颈。\n为解决这一问题，Linux 2.3 引入软中断（softirqs），这是一种在中断上下文中处理延迟工作的 per-CPU 机制。软中断对网络、定时器和 RCU 仍至关重要，但其不能睡眠或被抢占，在负载下可能导致延迟。为简化使用，任务小项（tasklets）作为更高层接口被添加，管理序列化并抽象软中断细节，但保留了相同的原子约束。随着工作负载变得更复杂，这种灵活性的缺乏开始受限。\nLinux 2.5 引入工作队列（workqueues），允许延迟工作完全在线程上下文中运行。工作队列处理程序可以睡眠、阻塞，并像任何其他内核线程一样与调度器交互。如今，工作队列是通用延迟处理的标准机制，已取代任务小项和自定义线程逻辑的大部分用途。\n一个重大转变是线程化中断处理程序的引入，最初在 PREEMPT_RT 补丁集中开发，并在 Linux 2.6.30 中合并到主线。在这种模型中，上半部分仅确认中断，唤醒专用内核线程处理其余部分。这些处理程序可以睡眠、按优先级调度，并且完全可抢占，非常适合实时或对延迟敏感的工作负载。现在许多驱动程序默认使用线程化 IRQ。\n从 Linux 2.6 到 4.x，内核通过 blk-mq、RPS 和 RFS 等功能改进中断处理，同时合并关键的 PREEMPT_RT 增强功能。这些更改提高了可扩展性和响应性，而不改变快速上半部分执行和延迟下半部分工作的核心模型。\nLinux 6.x 继续这一进程。NAPI（网络轮询机制）现在可以在专用线程而非软中断上下文中运行，这改善了调度控制和 CPU 隔离，尤其在有严格性能或延迟约束的系统中。\n曾经是软中断便捷抽象的任务小项现在已弃用，其序列化、原子执行不再与内核向灵活的基于线程的基础设施的转变一致。仍依赖它们的子系统正在迁移到工作队列或线程化 IRQ。\n在此过程中，中断模型始终不变：处理紧急事务，延迟其余部分。改变的是延迟处理的实现方式——从序列化、不可抢占的路径转向并发、可调度的执行。接口保持不变，只是内核更擅长实现它。\n38 并发之外的同步机制 大多数关于同步的解释始于线程并终于锁，但仅线程安全并不能反映内核的设计承受能力。并发只是内核必须防范的多个维度之一。内核中同步的目的不是保护代码不被多个线程执行，而是维护不相互等待的执行上下文之间共享状态的完整性。\n在内核空间中，同一个函数可能同时在多个CPU上执行。重要的不是代码是否被重入，而是它触及的数据是否受到保护。临界区由数据而非代码定义。同步确保共享内存结构（任务列表、文件描述符、套接字缓冲区）即使在被可抢占线程、中断处理程序或延迟下半部分访问时也能保持一致。被序列化的不是调用路径，而是数据的边界。\n系统安全不仅依赖于线程间的原子性，还包括对抢占的控制、中断的排除、核心间的可见性、对象生命周期的执行以及访问的限制。竞争、崩溃或安全漏洞可能并非源于并行执行本身，而是源于不安全的访问——通过中断更新、过早释放或对共享状态的未检查修改。\n内核提供自旋锁、互斥锁和顺序锁用于互斥，使用RCU在延迟回收时实现无锁读取，在必须维护局部性的区域禁用抢占和CPU迁移，应用per-CPU变量完全消除竞争。为确保内存安全，它将引用计数与kref等生命周期感知对象结合，并使用RCU确保读取者从不在内存被释放后观察到它。在可见性必须跟随初始化的无锁路径中，它使用内存屏障强制执行顺序。这些机制不是避免竞争的通用解决方案，而是在特定上下文中消除特定形式不安全的目标工具。\n内核不将同步视为优化，而是视为基础。任何可从多个上下文访问的数据必须考虑每一种可能的交互——SMP并发、中断抢占、软中断干扰、NMI进入和异步拆卸。它必须在抢占下安全，在CPU迁移中正确，通过每个引用有效，在释放后不可用。它必须在使用前准备好，并在整个可见期受到保护。\n确保内核安全的不是它并行运行，而是它保持对共享内容的控制。内核中的同步是对该控制的执行，它保护数据而非函数，维护状态而非流程，确保共享结构即使通过多个执行路径到达也能保持一致。\n39 这从来不是关于炒作，而是关于硬件 Linux 的成功并非源于追逐潮流，而是源于对真实硬件变化的持续适应。在过去十年中，CPU 获得了更多核心和新的调度模型，内存变得分层且持久化，存储从旋转磁盘转向基于队列的快速闪存，网络速度已快到可与本地内存访问相媲美。内核在每个阶段都在演进——不是通过营销，而是通过人们在真实系统上运行它、遇到真实问题并修复它们。\n内核在两个方向上运作：朝向用户空间和朝向硬件。它避免破坏用户空间，因为这样做从根本上就是错误的，而不仅仅是冒险的。基于早期内核版本构建的程序应该继续工作，这种稳定性被描述为一种责任而非仅仅是一项政策。当向后兼容变得不切实际时——例如放弃 386 支持——它会以谨慎和明确的理由进行。\n内核在必要时也接受变革。它早期采用了 64 位支持，现在同时支持 GCC 和 Clang，并在内存安全重要的领域采用 Rust。这些变革是由解决问题驱动的，而非追求新奇。\n同样重要的是 Linux 刻意回避的领域。它并非为每种设备或具有极端约束的深度嵌入式系统而设计。这种局限性被描述为一个明确的决定，而非弱点。Linux 提供了一个完整的内核，包含调度、内存管理、驱动程序和用户空间接口。它不能也不应该适用于不需要这些功能的硬件——更小的内核在那里更有意义。\n作者强调了这种务实的范围：Linux 并非试图做所有事情，而是旨在在有意义的地方做正确的事情。它之所以持续成长，是因为它被积极使用，而使用会产生错误报告，这些报告导致修复，从而为每个人改进内核。从单板计算机到云基础设施，Linux 通过一次解决一个真实问题而取得成功。\n内存分层、安全虚拟机隔离、异步 I/O 和可编程网络等功能并非为了追随潮流而添加——它们是因为系统在真实工作负载下需要可靠性而添加的。内核之所以保持可信赖，是因为它专注于硬件，成长不是为了可见性而是为了实用性，改进是因为人们运行它、测试它并依赖它。\n这种方法并非过时——它是稳定的、有目的性的，而且仍然正是复杂系统所需要的。\n40 从意图到 I/O：内核如何看待文件、磁盘和设备 当一个进程调用 read() 时，它请求的是数据，而非磁盘。随之而来的是跨内核层的结构化交接，每一层都有明确的职责。内核对文件的感知方式与用户不同——通过结构、委派和执行。从意图到 I/O 的路径穿越三个核心组件：虚拟文件系统（VFS）、块 I/O 子系统和设备驱动程序。\nVFS 是解释用户空间请求的第一层。它将路径名还原为内部对象：dentry、inode 和文件描述符。它无需知道挂载了哪个文件系统；其角色是\u0026quot;提供统一的接口并将操作分派到正确的实现\u0026quot;。通过抽象和间接性，VFS 将逻辑访问与物理布局分离。\n文件系统驱动程序（ext4、xfs 等）将文件偏移量转换为设备上的逻辑块地址。它遍历元数据结构，解析映射，并确定请求的数据驻留在何处。它不与硬件交互。相反，它构造一个 bio——一个描述 I/O 操作的结构，包含内存页面、偏移量和方向。驱动程序通过 submit_bio() 提交它。\n至此，文件上下文已消失。bio 层不跟踪用户状态或系统调用来源。它专注于移动数据：\u0026ldquo;哪些页面、哪些扇区、哪个方向\u0026rdquo;。它可能通过多队列（blk-mq）框架合并、拆分或调度 bio，将它们转换为设备特定的请求。\n在栈的末端是设备驱动程序。 它不关心是什么触发了请求或数据代表什么。它的工作是将请求转换为硬件命令：设置 DMA、发出操作并处理完成。\u0026ldquo;它在不了解文件、路径或进程的情况下执行 I/O。\u0026rdquo;\nI/O 栈是可扩展的，因为每一层都恪守其角色。VFS 解释结构。Bio 描述 I/O。驱动程序执行。\u0026ldquo;没有一层承担另一层的职责。\u0026rdquo;\n设备映射器（Device Mapper） 正是因此设计而存在。它可以无缝地嵌入 bio 和驱动程序之间，提供用于加密、镜像或配置的虚拟块设备——而不触及 VFS 或文件系统逻辑。它处理 bio 并传递它们，符合块接口规范。文件系统在上方不变地运行。驱动程序在下方毫不知情。\u0026ldquo;系统保持连贯，因为边界得以维持。\u0026rdquo;\nLinux I/O 栈之所以持久，是因为它的层是受纪律约束的。读操作从 VFS 开始，变成 bio，并在驱动程序中完成。设备映射器或循环设备等可选层可以拦截或转换请求而不会破坏模型。eBPF 等工具可以在不干扰的情况下观察路径。\n\u0026ldquo;内核看到的不是文件。它看到的是一个任务：解析、传输、完成。\u0026ldquo;每一层都恰好做它应该做的事——不多不少。这就是它运作的原因。\n41 心中的内核——效率至上而非历史遗留原因：为什么内核仍用 C 语言开发 系统语言的目的不是服务于程序员。而是让内核能够独立运行——直接在硬件上。没有运行时。没有依赖。只有代码、机器以及它们之间的接口。\n内核使用 C 语言并非因为历史原因。而是因为没有其他语言能在软件与硬件交汇之处提供同等级别的效率、控制和结构清晰度。\nC 是一种组件语言。源文件定义函数、结构体和指针。头文件仅声明需要对外可见的内容。编译结果形成 ABI——一个稳定的二进制契约，允许每个模块在不同构建和架构之间可预测地运行。\n内核不是在运行时定义的；它是在构建时塑造的。通过 C 预处理器，.config 设置使用条件宏激活或排除功能。这些决策控制编译路径，将内核裁剪到特定硬件，并完全消除未使用的逻辑。看似通用的行为是通过基于模式的代码复用（宏和内联函数）实现的——而非通过模板或反射。\nC 在内核设计中扮演三个角色。首先，它直接表达与硬件数据手册对齐的控制逻辑——寄存器、内存映射 I/O 和控制流。其次，它实现紧凑、可预测的算法来管理 CPU 时间、内存和线程。第三，它通过引用和组合来构建系统。嵌套结构体、内嵌回调和函数指针实现了模块化框架，无需继承。\nC 中的结构体在内存中精确布局。每个字段根据 ABI 强制的对齐规则放置。填充仅在必要时引入，总大小不仅反映数据还反映布局。这使得每次访问都是可预测的，每个偏移量都是有意义的，结构体适合二进制级通信。\nC 中的指针不仅仅代表地址。函数指针绑定到代码。结构体指针指向状态。void * 引用原始内存——它不是用来转换任何东西的。它不转换；它解释。它允许对扁平内存进行有结构的访问，其中结构由程序员基于已知类型施加——而非通过改变类型本身。内存是扁平的，void * 是在该空间中表示数据的最自然方式。\nC 中的可见性是刻意的。静态符号保持内部。外部声明仅定义共享的内容。在内核中，符号导出通过 EXPORT_SYMBOL 显式控制，它定义了模块在运行时可以链接的内容。没有隐式可见性——每个边界都是有意为之的。\nC 提供对内存、执行和结构的直接访问，而不对机器之上的任何东西做假设。这就是它仍然是内核语言的原因——不是因为它古老，而是因为它仍然是描述系统如何独立运行的最高效和最诚实的方式。\n42 活着的内核：Linux 内核架构的实用概述 当 Linux 启动时，在固件和引导加载程序完成后，压缩的内核镜像被加载到内存中，同时加载的还有 initramfs——一个最小的临时根文件系统，用于在真正的根文件系统接管之前帮助完成早期设置，例如加载驱动程序。\nLinux 是一个具有模块化能力的单体内核。 核心组件被编译成单个二进制文件，但可选功能（如设备驱动程序）可以在运行时动态加载或卸载。\n用户空间与内核空间 内核在用户空间和内核空间之间实行严格分离。用户空间运行有限特权的应用程序，而内核空间以完全特权运行，允许它管理内存、调度进程、控制 I/O 设备。在 x86 上，Ring 3 用于用户应用程序（最低特权），Ring 0 用于内核。转换通过系统调用发生。\n七大核心子系统 1. 进程调度器 — 使用抢占式、基于优先级的调度管理多任务。默认的完全公平调度器（CFS）使用红黑树高效选择下一个要运行的任务。它还支持实时策略（SCHED_FIFO、SCHED_RR、SCHED_DEADLINE）、CPU 亲和性和负载均衡。\n2. 内存管理 — 每个进程获得自己的虚拟地址空间。MMU 和页表将虚拟地址转换为物理地址。该子系统处理页面分配、交换、共享内存、写时复制和内存映射文件。\n3. 虚拟文件系统（VFS） — 一个抽象层，为所有支持的文件系统提供统一接口，无论后端是 ext4、Btrfs、XFS、NFS 还是 tmpfs。它管理 inode、dentry、文件描述符和挂载点，将用户接口与存储后端解耦。\n4. 设备驱动程序 — 为存储、输入、网络、图形和声音设备抽象硬件细节。它们可以静态编译进内核，也可以作为可加载内核模块在运行时动态加载。\n5. 网络栈 — 实现完整的 TCP/IP 栈，支持路由、套接字通信、通过 iptables/nftables 实现防火墙、NAT，以及包括 IPv4、IPv6、ARP 和 ICMP 在内的协议。高级功能包括流量整形、桥接、隧道和虚拟网络接口。\n6. 系统调用接口 — 作为用户空间应用程序访问特权内核服务的受控网关。示例包括 read()、write()、fork()、execve()、mmap() 和 brk()。\n7. IPC 与命名空间 — 提供管道/FIFO、信号量、互斥锁、消息队列和共享内存。命名空间将全局资源分区为隔离视图，而 cgroups 限制资源使用。它们共同构成了进程隔离和容器化的基础。\n可加载内核模块 LKM 让单体内核获得灵活性——独立组件在运行时加载或卸载，用于驱动程序、文件系统和网络协议。modprobe、insmod 和 rmmod 等工具无需重启即可管理它们。\n系统调用机制（逐步解析） 应用程序通过 libc 调用库函数（例如 read()） 特殊 CPU 指令（syscall、int 0x80 或 svc）切换到内核模式 内核读取系统调用号（在 x86_64 上从 rax 获取）并通过系统调用表分派 结果放入返回寄存器，sysret 切换回用户模式 内存保护与进程隔离 MMU 通过带权限标志的每进程页表强制执行分离。未授权访问触发页面错误或段错误，内核通过终止违规进程来响应。内核内存对用户模式代码不可访问。这保证了安全性、稳定性和多任务安全。\n内核被描述为一个通用、模块化和安全的操作系统核心，协调调度、内存管理、I/O 和 IPC，同时在各种硬件平台上维护系统完整性和性能。\n43 Linux 内核漫步：理解其子系统 Linux 内核并非一个单体的庞然大物，而是一个结构良好、高度模块化的系统，设计用于跨设备、架构和用例工作。\narch/ — 包含特定于架构的代码，适用于 x86、ARM 和 RISC-V 等平台，包括引导逻辑和低层内存管理。\ninit/ — 在特定于架构的设置之后，真正的初始化在此发生，挂载根文件系统并启动第一个用户空间进程。\nkernel/ — 被描述为中枢神经系统，处理进程调度、信号、计时和系统调用。\nipc/ — 实现 System V IPC 机制：消息队列、信号量和共享内存，用于进程间通信。\nmm/ — 管理内存管理，包括虚拟/物理内存、分页、交换、NUMA 和透明大页。\nfs/ — 包含 VFS 层和文件系统实现，如 ext4、Btrfs、XFS 和 NFS。\nnet/ — 包含完整的网络协议栈，从以太网到 TCP/IP，再到 VPN 和隧道协议。\ndrivers/ — 被称为 Linux 传奇硬件支持的核心，涵盖显卡、USB、声卡、网络适配器等。\nlib/ — 共享工具，包括数据结构、字符串函数和压缩算法。\ninclude/ — 头文件，包含将内核连接在一起的声明、常量和宏。\nsecurity/ — 实现基于 LSM 的模块，如 SELinux 和 AppArmor。\nblock/ — 管理低层存储 I/O，用于硬盘、SSD 和闪存。\nio_uring/ — 现代异步 I/O 接口，使用共享环形缓冲区最小化上下文切换和系统调用。\nvirt/ — 虚拟化基础设施，支持 KVM、客户/主机通信和半虚拟化。\ncrypto/ — 模块化加密 API，包含 AES、SHA 和 RSA 等算法。\ncerts/ — X.509 证书，用于验证签名内核模块，支持安全启动。\nsound/ — ALSA 驱动的音频支持，包含驱动程序和数字混音逻辑。\ntools/ — 用户空间工具，用于调试和性能调优，包括 perf 和 bpf。\nscripts/ — 构建时工具和实用程序，用于配置、代码生成和编译。\nrust/ — 新兴的基于 Rust 的内核代码，在不牺牲性能的情况下提供内存安全。\n内核是一个活的、不断进化的有机体，具有清晰的边界、深思熟虑的抽象和专门构建的子系统——鼓励读者一次探索一个子系统。\n44 共享代码，分离状态：我在内核内存管理中的第一课 我最近开始探索 Linux 内核的工作方式——已经遇到了一些改变我对操作系统理解的观念。\n最令人惊讶的教训之一是关于内核本身如何被映射到内存中。内核不像用户程序那样作为单独的进程运行。然而，它存在于每个进程的地址空间中。它的代码和只读数据在所有进程中以固定的虚拟地址映射。这不是复制——这是一个共享的、只读的映射，节省内存并保持系统高效。\n但真正让我印象深刻的是内核如何将共享逻辑与可变状态分离。虽然内核代码在整个系统中是相同的，但每个进程都有自己的内核栈——一个仅在该进程进入内核模式时使用的私有内存区域。每任务结构如 task_struct、内存映射和调度元数据都随之而来。这些都是分开管理的，以维护进程隔离。\n这种设计允许内核保持集中和轻量级，同时仍然为每个进程提供安全和私有的上下文来运行。内存模型被描述为优雅的：一个共享的内核，但个性化的执行环境。\n学习这些引发了关于并发的问题。同一个内核函数可能同时在多个 CPU 上运行——处理不同的系统调用、服务中断或处理后台任务。虽然我才刚刚开始，但我已经能看到为什么线程安全和可重入性在内核开发中如此重要。共享逻辑和每进程状态意味着内核需要精确协调访问。\n我现在的重点是理解这些内存基础：代码如何映射，栈如何隔离，内核如何保持一切既高效又安全。即使在这个早期阶段，这种视角已经改变了我对系统设计的思考方式。\n这只是开始，但感觉已经用新的眼光看待 Linux 了。鼓励那些走类似道路进入系统或内核内部的人从内存开始。那是所有事情开始变得有意义的地方。\n45 进程如何工作——从内核的视角 虽然在探索内核开发和虚拟化时，作者退后一步来理解一些基本的东西：Linux 中的进程到底是什么？从外部看，我们将进程视为运行的程序，但从 Linux 内核的角度看，进程是一个丰富、复杂的数据结构——不仅仅是一个执行单元，而是内核在整个生命周期中管理的完整对象。\n核心：task_struct 核心是 task_struct——内核对进程或线程的内部表示，定义在 include/linux/sched.h 中。它包含内核管理执行所需的一切：\nstruct task_struct { pid_t pid; long state; struct mm_struct *mm; struct files_struct *files; struct thread_struct thread; ... }; 它很大（通常 9-12 KB），因为它存储了 CPU 状态和调度信息、内存布局（mm_struct）、打开的文件、信号、凭据、命名空间和 cgroup 数据，以及审计、跟踪和安全字段。\n创建：fork()、clone() 和 execve() fork() 创建调用进程的几乎相同的副本。 clone() 更灵活——它允许你共享内存、文件或信号处理程序（这就是线程的创建方式）。 execve() 用新的程序镜像替换当前进程内存空间。 总结：fork() 复制，execve() 替换。\n内存和执行上下文 每个 task_struct 指向一个 mm_struct，它定义了进程的内存布局——栈、堆、代码和映射区域。thread_struct 存储 CPU 寄存器，以便进程可以被暂停和恢复。调度器使用这个来高效地在进程之间切换。每个进程有一个内核栈和一个用户栈。\n系统调用：跨越边界 当用户进程进行系统调用（如 read() 或 open()）时，它从用户模式转换到内核模式。内核切换 CPU 以使用内核栈，查找系统调用处理程序，在内核上下文中运行，然后返回用户空间。这种交互是严格控制和安全的，这使得 Linux 具有鲁棒性。\n进程的生与死 当进程结束时，它调用 exit()，清理资源，变成僵尸进程直到其父进程调用 wait()。如果没有父进程存活，init（PID 1）将回收它。\n线程？它们也只是任务 在 Linux 中，线程也是 task_struct 条目。唯一的区别是它们共享什么。如果两个任务共享同一个 mm_struct，它们就在同一个内存空间中运行——这就是使它们成为线程的原因。\n理解 Linux 内核如何看待和管理进程是令人大开眼界的。它是模块化的、高效的，为大规模灵活性而构建。一切都从 task_struct 开始。\n46 如何进入内核：系统调用、陷阱和中断 Linux 内核不是进程、线程或守护进程，而是一个特权的、驻留在内存中的环境，始终存在但从未被调度。内核不像程序那样运行——它是被进入的。\n进入内核意味着 CPU 从用户模式（Ring 3）转换到内核模式（Ring 0）。这是一个硬件控制的转换，处理器使用专用的内核栈以提升的特权从内核的内存空间执行指令。没有启动新进程；在用户空间中运行的同一线程继续在内核内部执行。内核是被线程执行的，在需要时。\n三个明确定义的场景导致这种进入：系统调用、硬件中断和 CPU 异常/陷阱。\n1. 系统调用：来自用户空间的有意进入 当用户空间程序需要特权操作（如打开文件）时，它调用库函数如 read()，该函数发出特殊指令——在 x86_64 上是 syscall，在 ARM64 上是 svc #0。这些不是普通的跳转——它们是硬件管理的特权转换。\n此时，CPU 保存用户模式寄存器状态，切换到线程的内核模式栈，禁用某些用户可见的标志，并跳转到存储在型号特定寄存器（如 MSR_LSTAR）中的预定义内核入口点（如 entry_SYSCALL_64）。\n同一线程现在在自己的内核栈上执行内核代码。内核处理请求（如运行 sys_read()），然后执行受控返回到 Ring 3。线程本身没有改变。没有发生上下文切换。\n2. 硬件中断：来自设备的异步进入 硬件设备（网卡、磁盘控制器、定时器）可以随时使用中断请求（IRQ）向 CPU 发出信号。当 IRQ 被触发时，CPU 暂停当前运行的线程，使用中断描述符表切换到 Ring 0，并跳转到设备特定的内核处理程序。\n与系统调用不同，中断处理程序在中断上下文中执行。它们不关联任何用户进程，不能睡眠，并且必须快速返回。额外的处理被延迟到内核线程（如 kworker/*），通过软中断或工作队列。\n3. 异常和故障：非计划的转换 当进程访问无效内存、除以零或触发页面错误时，会发生故障或陷阱。这些不是故意的——它们是用户空间中不正确或不完整执行的结果。\nCPU 使用故障特定的入口陷入内核，进入 Ring 0，并跳转到相应的故障处理程序（如 page_fault()）。内核可能解决问题（加载内存页面）或终止进程（通过 SIGSEGV）。同一线程仍在运行——它只是穿越到了内核，以便系统可以响应故障。\n关于内核映射的说明 内核被永久映射到内存中——特别是每个用户进程的虚拟地址空间中。在 64 位系统上，低地址范围用于用户内存，而高地址范围（如 x86_64 上的 0xffff800000000000）保留给内核。\n虽然这种映射存在于每个进程中，但该区域被标记为特权。在 Ring 3 中运行时无法访问。用户模式访问触发页面错误。只有在 Ring 0 中——通过系统调用、中断或陷阱——才能使用内核内存。\n这就是为什么我们说内核始终存在：它存在于每个进程的内存空间中，但在被显式进入之前保持不可访问。\n没有被调度的执行 在所有三种进入场景中，内核都是作为事件的结果被进入的。CPU 使用与当前线程或 CPU 关联的内核栈，从已映射的内存执行内核代码。没有全局的\u0026quot;内核线程\u0026quot;接管。\n这解释了为什么内核没有 PID，不出现在 ps 中，在空闲时似乎不活动。然而它始终被映射，始终具有特权，始终准备好接管控制。\nLinux 内核不是进程表的参与者。通过系统调用、中断和陷阱，它被短暂而精确地进入，响应需求而执行。它不会自己运行——它是有目的地被进入的，然后退出。它不是进程——它是系统。\n47 现代 Intel 64 位 CPU 内部有什么？ 从系统层面看现代 x86_64 CPU，通过 Linux 内部、虚拟化和系统调用/硬件交互的学习，可以将 CPU 组织为八个主要组件区域。\n1. 执行单元 每个核心包含一个 ALU（整数算术和逻辑）和一个 FPU（浮点运算），处理从系统调用逻辑到实时数学的核心计算任务。\n2. SIMD 单元 SSE、AVX 和 AVX2 扩展在多个数据上同时执行单条指令，适用于图形、科学计算和机器学习加速。\n3. 寄存器 在 64 位模式下，寄存器集包括通用寄存器（RAX-R15）、RIP（指令指针）、RSP（栈指针）、控制寄存器（CR0、CR3、CR4）用于分页/保护，以及 MSR 用于配置 SYSENTER、SYSCALL 和 VMX 行为。这些携带系统调用参数、返回值、指针和系统状态。\n4. 内存管理 MMU 将虚拟内存转换为物理内存。通过 CR3 访问的页表定义映射，TLB 缓存转换以加速，支持大页（2MB/1GB），EPT 允许虚拟机中的嵌套分页。这是 fork、exec 和 VM 内存隔离的基础。\n5. 缓存层次结构 三级缓存最小化内存延迟：L1（约 32KB，最快，每核心）、L2（约 256KB-1MB，每核心）和 L3（8MB-30MB+，共享）。预取器预测访问模式以提高吞吐量。\n6. 中断控制 本地 APIC 处理每核心中断，而 IOAPIC 路由来自设备的硬件 IRQ，实现实时调度、核心间通信和设备事件处理。\n7. 定时器与计数器 TSC 提供高分辨率计时，HPET 实现精确调度，PMU 硬件事件计数器支持性能分析。内核使用这些进行任务调度和性能测量。\n8. 特权与模式控制 Ring 0（内核）与 Ring 3（用户）模式，通过 SYSCALL/SYSENTER 快速进入，以及 VMX root/non-root 用于主机与客户虚拟化。这些转换及其导致的陷阱（VMEXIT）使虚拟机监控程序成为可能。\n指令集扩展 SSE/AVX/AVX-512 用于并行化，AES-NI 用于加密加速，SGX 用于安全飞地，Intel VT-x/VMX 用于硬件虚拟化。\n48 select() 和 poll() 如何为 epoll() 铺平道路 早期 UNIX 网络 在早期 UNIX 网络中，处理多个客户端连接通常意味着为每个连接创建一个新线程或进程。它能工作但缺乏效率。一个更好的方法出现了：与其在每个连接上阻塞，不如等待其中任何一个就绪。这个概念催生了 I/O 多路复用和 select() 系统调用。\nselect() — 革命性的飞跃 select() 是开创性的。它使单线程程序能够同时监视多个套接字。你提供一组文件描述符，它报告哪些已准备好读取或写入。服务器现在可以处理并发客户端而无需生成多个线程——这是那个时代的重大进步。\nselect() 的局限性 随着 Web 的扩展，select() 暴露出显著的弱点。它带有硬编码的文件描述符限制，通常为 1024。每次调用都需要将整个描述符集复制到内核中并线性扫描它们。随着连接数增加，性能下降。\npoll() — 向前一步 poll() 消除了文件描述符上限并提供了更灵活的 API。然而，其核心机制仍然是线性扫描器。完整的描述符列表仍然需要在每次调用时重建。对于轻量级应用程序来说足够了；对于大规模系统来说证明是不够的。\nepoll() — 一切都改变了 Linux 引入了 epoll()，专为规模化而设计。对事件的兴趣只需注册一次，内核维护对哪些描述符就绪的感知。内核不再重复扫描列表，而是仅在发生变化时通知应用程序。这种内部状态管理使 epoll() 大幅提高效率。它还引入了边沿触发通知，使应用程序能够进一步减少开销。\n对现代系统的影响 这种模型解锁了现代事件驱动服务器如 NGINX。NGINX 不再为每个连接生成线程，而是采用单个 epoll 驱动的事件循环监视数千个套接字。这种架构转变是 NGINX 主导高性能 Web 托管的关键原因。\nNode.js 等更高层次的平台遵循相同的哲学。虽然开发者从不在 JavaScript 中直接编写 epoll()，但 Node.js 构建在 libuv 之上，这是一个跨平台事件库，在 Linux 上使用 epoll()，在 macOS 上使用 kqueue，在 Windows 上使用 IOCP。这让开发者能够以简洁的单线程风格编写异步、非阻塞的应用程序，而系统处理繁重的工作。\n从 select() 到 poll() 到 epoll() 的演进不仅仅是 API 的进化——它构成了互联网如何扩展的基础。API、消息系统、流媒体平台和实时应用程序都依赖于这一演进。关键洞察：不是更多的线程，而是更智能的事件——只在必要时响应。\n这种转变悄然改变了互联网的运作方式，一切都始于一个简单的问题：\u0026ldquo;我可以从这个套接字读取了吗？\u0026rdquo;\n49 epoll() 之后：io_uring 如何重新定义 Linux I/O 文章追溯了 Linux I/O 机制从 select()/poll() 经过 epoll() 到 io_uring 的演进。\nepoll 时代 当 epoll 出现时，它取代了需要在每次调用时重建文件描述符集并线性扫描的旧方法。NGINX 和 Node.js 在其事件驱动模型上蓬勃发展，使用单线程事件循环处理数千个连接。\nepoll 的局限性 随着系统扩展，epoll 显示出弱点。它仍然使用两步流程——等待就绪，然后执行 I/O——意味着每个操作至少需要两个系统调用，一个检查，一个行动。\nio_uring 登场 io_uring 于 2019 年在 Linux 5.1 中由 Jens Axboe 引入，采用了根本不同的方法。它不仅仅发出就绪信号，而是让你提前提交整个操作。请求进入共享内存环，内核负责执行。完成事件出现在单独的环中供应用程序消费。\n关键洞察： 当你的应用程序看到完成事件时，数据已经到位了。对于读取，内核已经填充了缓冲区——不需要后续系统调用。这消除了额外的往返，减少了上下文切换，并提供了对结果的即时访问。\n基于完成的 I/O 模型 这种方法避免了不必要的开销，消除了来回的系统调用，并利用用户空间和内核空间之间的共享内存在负载下实现重大性能提升。名字反映了这一点：通过用户空间环形缓冲区进行 I/O。\n思维转变 文章将其描述为从问\u0026quot;这个准备好了吗？\u0026ldquo;转变为说\u0026quot;在你可以的时候做这个，然后告诉我。\u0026rdquo;\n采用状态 io_uring 在性能关键系统中正在获得关注。NGINX 提供实验性支持，MySQL 正在探索其好处，更新的项目将其作为核心组件采用。\nepoll 仍然是经过实战检验和可靠的，但 io_uring 代表了从 select→poll→epoll→io_uring 演进的自然下一步。它不是替代品，而是向前的飞跃，减少摩擦，削减开销，释放新的性能潜力。\n50 多任务与虚拟化——真正的区别是什么？ 多任务：一个操作系统，多个进程 在典型的多任务系统中，主机操作系统拥有硬件并调度多个进程（应用程序、守护进程、服务）。每个进程获得自己的内存空间，但它们都信任同一个内核。操作系统使用上下文切换来暂停和恢复进程——保存 CPU 寄存器、程序计数器和内存映射。这被描述为分时共享，在共享单个监管者的同时提供并行的假象。\n虚拟化：一个主机，多个操作系统 在虚拟化中，主机操作系统（或虚拟机监控程序）运行多个客户操作系统。每个客户操作系统管理自己的进程、文件系统、驱动程序和内核，每个都相信它拥有整台机器。但虚拟机监控程序保持控制。\n现代 CPU（Intel VT-x、AMD-V）通过以下方式提供帮助：\n隔离特权 CPU 状态（控制寄存器、中断表等） 允许对 CPUID 或 HLT 等指令进行陷阱和退出机制 为每个客户维护虚拟 CPU 上下文（VMCS） 确保一个客户不能干扰另一个客户或主机 这不仅仅是多任务处理进程——而是多任务处理整个操作系统，每个操作系统都有自己拥有裸机硬件的假象。\n关键区别 多任务：\n多个进程 一个操作系统内核（监管者） 软件隔离 虚拟化：\n多个操作系统内核 一个监管者（虚拟机监控程序）监督所有客户操作系统，每个客户操作系统是自己进程的监管者 硬件和虚拟机监控程序隔离 虚拟机监控程序是将虚拟化与多任务区分开来的东西。 它不仅仅是共享时间——它创建安全的虚拟硬件环境。\n快速术语表 虚拟机监控程序 — 运行和管理虚拟机的软件，将每个客户操作系统与主机和其他客户隔离。 CPUID — 报告 CPU 功能的指令；虚拟机监控程序捕获它以控制客户看到的内容。 HLT（停机指令） — 停止 CPU 直到下一个中断；在 VM 中，它被捕获以便主机可以调度其他工作。 VT-x（Intel 虚拟化技术） — Intel 对虚拟化的硬件支持；实现高效的客户/主机切换和隔离。 VMCS（虚拟机控制结构） — CPU 在虚拟化转换期间用于存储客户和主机状态的内存结构。 CR3（控制寄存器 3） — 持有页表的基地址，用于虚拟到物理内存转换。切换 CR3 切换进程或操作系统内存空间。 GDT（全局描述符表） — 在 x86 保护模式下定义内存段和访问权限。每个操作系统通常设置自己的 GDT。 IDT（中断描述符表） — 将硬件和软件中断映射到其处理程序函数。客户操作系统设置自己的 IDT。 VMX（虚拟机扩展） — Intel 的硬件虚拟化功能，引入新的 CPU 模式（root 和 non-root）和指令（VMXON、VMLAUNCH）以安全运行客户操作系统。 VMEXIT — 从客户（non-root）模式返回到主机（root）模式的转换，由特定指令或事件（CPUID、HLT、I/O）触发。虚拟机监控程序处理事件并决定下一步。 51 内核始终存在——即使在它没有运行的时候 核心问题 Linux 内核不是进程、线程或调度实体。如果内核缺少这些属性，它如何能始终存在于系统中？\n答案涉及内存结构、硬件保护，以及 CPU 将内核视为每个进程下方的特权基底。\n内核始终被映射 内核在启动时被永久加载到物理内存中，并映射到每个用户进程的虚拟地址空间。在 x86_64 上，用户空间占据 0x0 到 0x00007fffffffffff，而内核空间从 0xffff800000000000 及以上开始——在 48 位寻址中每个区域跨越 128 TiB。\n每个进程共享相同的内核映射。这不是复制，而是由内核 MMU 插入的全局一致的虚拟区域。然而，当 CPU 在 Ring 3 中运行时，此范围不可访问。\n硬件保护：Ring 0 或故障 内核内存页面在页表中携带仅监管者标志。如果用户空间尝试读取或跳转到内核内存，CPU 通过用户/监管者位和 CR0 的写保护位触发页面错误。\n安全访问内核内存的唯一方法是进入 Ring 0——通过 syscall、int 或 SVC 等机制。这种方法产生快速转换（系统调用时不切换页表），一致的每线程内核栈访问，以及统一的执行基底。\n每线程内核栈 每个线程获得自己的内核模式栈，在创建时分配。当 CPU 进入内核时，它切换到这个栈，该栈不与用户空间共享，由溢出保护的保护页守护，用于控制流和局部变量。没有专门的\u0026quot;内核线程\u0026quot;等待系统调用——栈和代码只是准备好并被映射。\n不需要调度——然而内核调度 内核不被调度，但它编写调度。当 nanosleep()、poll() 或 read() 等系统调用导致阻塞时，内核将线程置于睡眠状态，决定唤醒时间，并选择下一个可运行实体。它的代码在导致转换的任何东西中执行（用户线程系统调用或中断处理程序），然后返回用户空间或让步给调度器。\n始终存在，从未运行 内核的性质是三重的：始终作为每个地址空间的一部分映射到内存中，始终通过 Ring 0 门控保护，始终准备好栈和处理程序。然而它从未\u0026quot;传统地运行\u0026rdquo;——它不会出现在 top 或 ps 中，不能接收信号，不会循环或调度自己。\n它不在你的程序旁边运行；它从下方使能它们。\n存在而非执行 内核没有自己的生命周期，而是定义了其他一切的生命周期。它仅在硬件控制的时刻激活：边界穿越、设备中断和故障。有时它处理系统调用，响应 IRQ，或让步给内核线程——安静地醒来，完成工作，然后再次让步。\n内核不是你看到的东西。它是使其他一切可见的东西。\n52 仍然流经它的一切 现代系统跨越许多层——语言、运行时、解释器、容器、模型、协议——但每一层最终都将控制权传递给同一个核心层：内核仍然管理着那条路径。\n执行开始时是结构化的逻辑：创建进程，映射内存，调度指令，将 I/O 打开为流，连接套接字，驱动程序移动数据，处理中断，硬件被激活。每次转换都由内核验证、隔离和调解。\n即使代码在托管运行时内运行，由事件触发，在容器中，跨虚拟机——控制流仍然熟悉。每个内存访问、I/O 操作和任务切换都通过内核接口进行。边界仍然存在。契约仍然有效。\n这根植于冯·诺依曼架构——代码和数据在共享内存中，指令被顺序获取和执行，状态一次更新一个操作。这仍然是通用机器的模型，内核完全在其中运作，协调所有更高的层。\n什么会构成结构性变化 当系统不再运行在冯·诺依曼模型上时——没有取指-译码-执行循环，没有共享的指令/数据内存，没有程序计数器——结构就改变了。\n当内存不再以字节为单位寻址时——当访问变成基于图的、内容寻址的或物理上与逻辑共存时——接口就打破了。\n当执行不再通过系统调用，用户/内核空间边界消解时——控制模型不再适用。\n当内核不再需要抽象设备、隔离进程或调度计算时——它不再是中心。\n当逻辑不再被编写、编译或符号解释，行为被学习、涌现或没有离散指令地模拟时——软件本身已经转变。\n在那之前，结构保持不变。系统仍然一次运行一条指令，将代码存储为数据，陷入内核，等待许可，只在被询问时回答。而当它必须真正运行时，内核仍然回答。\n53 我为什么持续书写内核 内核是一个无法用单一解释捕获的宇宙。\n每次你认为理解了它，就会发现另一层——另一个子系统，另一个约束，另一个设计决策回响穿越数十年的代码。这不是因为内核是混乱的。恰恰相反。它是因为内核是一致的，以至于它的简单性具有欺骗性。\n你学习调度器，然后发现调度器不能脱离内存管理来理解。你学习内存管理，然后发现它依赖于中断处理。你学习中断处理，然后发现它与同步原语交织在一起。每一个都会引向另一个，不是因为糟糕的设计，而是因为这就是系统的工作方式。\n内核不是一个你可以逐章阅读然后宣称完成的项目。它是一个活的文档，每次硬件变化、每次新工作负载出现、每次有人在代码中发现更好的方式时都在增长。\n我持续书写它，因为每次我写一个解释，我都会学到一些新东西。不是关于代码本身——而是关于代码背后的推理。为什么选择这个数据结构而不是另一个？为什么这个锁在这里而不是那里？为什么这个接口以这种方式暴露？\n这些问题没有在手册页中回答。它们没有在注释中回答。它们在代码的行为中回答——在它如何响应负载、如何处理故障、如何在变化中保持正确。\n我持续书写它，因为有人需要。系统编程的世界充满了假设你已经知道一切的文档。但你不是天生就知道的。你是通过阅读、犯错、再阅读来学习的。\n如果这些文章帮助哪怕一个人少走一条弯路，少犯一个错误，少感到一点迷失——那就值得了。\n内核是一个宇宙，但它是你可以进入的宇宙。一次一个概念。\n54 成为内核开发者需要什么——以及有多少人？ Linux 内核是世界上最大、最活跃的开源项目之一。它由来自全球各地的数千名贡献者维护，跨越数百个子系统。\n角色与技能层级 内核社区有明确的层级结构：\n首次贡献者 — 从修复文档错误、编码风格问题或小 bug 开始。内核有严格的代码审查流程，每个补丁都必须通过维护者的审查。\n定期贡献者 — 随着经验积累，贡献者开始处理更复杂的功能和修复。他们学会了内核的编码规范、提交消息格式和审查文化。\n子系统维护者 — 负责特定子系统（如网络、文件系统、驱动程序）的代码审查、合并和维护。他们拥有对特定代码区域的最终决定权。\n顶级维护者 — 如 Linus Torvalds，负责整个内核树的最终合并决策。\n贡献趋势 内核每年接收数万个补丁。顶级贡献组织包括 Intel、Red Hat、SUSE、Google、AMD 和 ARM。贡献不仅来自公司，也来自独立开发者和学术机构。\n源代码树的规模 Linux 内核源代码树现在超过 4000 万行代码，包括：\n数千个驱动程序 数十个文件系统 完整的网络协议栈 多架构支持（x86、ARM、RISC-V 等） 虚拟化、安全、实时等子系统 子系统补丁分布 驱动程序子系统接收最多的补丁，其次是架构特定代码、网络栈和文件系统。这反映了内核的主要工作——支持不断增长的硬件生态系统。\n内核开发的独特之处 内核开发与用户空间开发有显著不同：\n没有稳定的内部 API——任何内部函数都可能在版本之间改变 必须考虑并发、中断上下文和内存限制 代码必须在多种架构和配置上工作 提交过程严格，需要经过多轮审查 成为内核开发者不需要天才，但需要耐心、细致和对系统编程的热情。内核社区欢迎新贡献者，并提供了详细的文档和指南来帮助入门。\n","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/%E5%BF%83%E4%B8%AD%E7%9A%84%E5%86%85%E6%A0%B8/","summary":"\u003ch1 id=\"心中的内核--在阅读内核代码之前先理解内核\"\u003e心中的内核 —— 在阅读内核代码之前先理解内核\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e原文：\u003ca href=\"https://www.linkedin.com/in/moon-hee-lee/\"\u003eThe Kernel in the Mind\u003c/a\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/%E5%BF%83%E4%B8%AD%E7%9A%84%E5%86%85%E6%A0%B8/figure0-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e[TOC]\u003c/p\u003e\n\u003ch3 id=\"介绍\"\u003e介绍\u003c/h3\u003e\n\u003cp\u003e这并非一部教你编写内核代码的指南，而是一场探寻Linux内核设计思维的思想之旅。\u003c/p\u003e\n\u003cp\u003e在系统编程的世界里，人们常常迷失于符号定义、头文件结构与实现细节的丛林中。然而剥离代码表象，内核实则是一个井然有序的响应式系统——它受上下文环境所规约，以分离原则为基石，从内存管理到任务调度的每一处设计，都浸透着精准的设计意图。\u003c/p\u003e","title":"心中的内核"},{"content":"指令集(ISA)，微架构，操作系统 指令集简单来说就是一套语言规范，定义指令类型，寄存器，数据类型，还有cpu的管理模式等。而微架构是依据指令集设计出的处理器硬件结构与实现方案。相同的指令集设计出来的微架构可能完全不同，比如Intel和amd的芯片都是x86指令集。但是设计出来的cpu结构完全不同，性能和能耗也天差地别。影响cpu性能的大部分是微架构设计，工艺制程，系统级整合等，而指令集带来的差异往往只是小头。指令集的流行主要是商业和生态决定的。\n操作系统的内核及驱动源码，绝大部分都由高级语言（如 C 语言）编写，核心逻辑在不同 CPU 架构上是相同的。只有少量直接与硬件交互的底层代码（例如上下文切换、中断处理、MMU 配置，以及驱动的外设寄存器访问、DMA、中断注册等），需要依赖内核提供的架构抽象层。这部分抽象层会根据 x86、ARM 等指令集分别实现，驱动本身则通过统一接口调用，因此源码通常也跨架构通用。把这些代码按目标架构编译后，生成对应的内核镜像和驱动模块，就能在该 CPU 上使用了。\nriscv指令集 riscv只是64位宽的数据和地址寻址，为什么指令的编码宽度只有32位\n指令长度和 CPU 位数没有必然联系，它是由“编码效率”和“代码密度”的权衡决定的。 RISC-V 的基础整数指令集虽然需要操作 64 位数据，但指令本身只需要编码32 位.\n为什么指令格式中字段位置不统一，比如有些指令立即数被分散在不同位。\n固定字段（rs1, rs2, rd）：为了并行读寄存器，绝对位置恒定。 立即数碎片化：是被固定字段挤压后的必然形态，但通过巧妙的位映射，硬件拼接零开销。 共享位位置：不同格式的立即数符号位、高位等有意重叠，让加法器和扩展电路能复用 riscv的体系结构是基于加载和存储的体系结构设计的，所以所有的数据处理都要在通用寄存器中完成 函数调用栈 描述：分配一个全新的虚拟内存页作为栈，左上角为低地址右下角为高地址。\n1. 入口点与主函数\nmain 不是进程的起点。在 main 执行之前，操作系统和运行时库会完成一系列初始化工作（如设置栈指针、全局指针、调用全局构造函数等），然后才跳转到 main。 main 的返回地址指向其调用者（通常是 __libc_start_main 或启动代码中的下一条指令。该返回地址在调用 main 时已被保存在特定的位置（在 RISC‑V 中为 ra 寄存器；在 x86‑64 中被压入栈）。 2. 函数栈帧的创建与使用\n当一个函数（例如 main）被调用时，它会通过减小栈指针sp的值分配栈帧。\n栈帧用于：\n保存返回地址（若该函数还会调用其他函数，即非叶子函数，则必须将返回地址保存到栈上；叶子函数可以视调用约定省略此步）。 保存被调用者保存的寄存器（callee‑saved registers），以便函数返回时恢复调用者的状态。 存放局部变量和临时数据。 为传递超出寄存器数量的参数预留空间（通常放在调用者栈帧的顶部，或由调用者分配的“参数构造区”）。 3. 参数传递\n调用函数前，参数先被放入特定的寄存器（如 RISC‑V 的 a0‑a7，x86‑64 的 rdi、rsi 等）。 若参数数量超过寄存器容量，多余的参数被压入栈（通常放在当前调用者的栈帧中，由调用者负责分配和清理）。 4. 调用与返回的通用流程\ncall 指令：将程序计数器（PC）跳转到被调用函数的起始地址，并同时保存返回地址（保存方式因架构而异：RISC‑V 写入 ra，x86‑64 压入栈）。\n被调用函数入口：\n分配栈帧（addi sp, sp, -N 或等效操作）。 将返回地址和被调用者保存的寄存器（如 ra、fp、s0‑s11 等）存入栈帧。 执行函数体。 函数返回前：\n将返回值放入约定的寄存器（如 a0）或内存地址。 从栈帧中恢复之前保存的返回地址和寄存器。 释放栈帧（addi sp, sp, +N）。 ret 指令：将程序计数器恢复为返回地址，继续执行调用者的后续指令。 异常处理 异常类型 中断：异步 异常：同步 指令访问异常 数据访问异常 加载异常 存储异常 系统调用：同步 异常时cpu做的事 CPU 硬件动作 具体内容（以现代通用处理器为例） 1. 识别并仲裁 判断事件优先级（如不可屏蔽中断 \u0026gt; 错误异常 \u0026gt; 普通中断），选择一个最高优先级的事件处理 2. 保存关键上下文（部分） 将程序计数器（PC/EPC）、程序状态字（PSR/EFLAGS/SPSR） 自动保存到特定寄存器或栈上（不同架构不同：x86自动压入内核栈，ARM存入ELR/SPSR，RISC-V存入mepc/mcause） 3. 切换执行特权级 自动提升到内核模式/超级用户模式（如x86的Ring0，ARM的EL1，RISC-V的M-mode或S-mode），并切换到内核栈（通过TSS、SP_EL1或mscratch交换） 4. 查找处理程序入口 根据事件类型读取中断/异常向量表（x86的IDT，ARM的异常向量表，RISC-V的mtvec表），获得入口地址 5. 跳转执行 将PC设置为入口地址，开始执行异常入口代码（通常是操作系统的底层汇编） 6. （可选）屏蔽同类事件 很多CPU会自动清除中断使能位（如x86的IF位），防止处理过程中被同一类型的事件打断 总结：硬件完成的工作是“强拉”CPU进入一个已知的安全状态，并交给操作系统软件。这些操作只需要几个时钟周期，极其高效。\n异常时os做的事 此时CPU已经跳转到OS预置的异常处理入口地址（例如 vector_swi, irq_handler, exception_vectors 等）。这部分代码必须用汇编编写，因为它需要操作CPU特殊寄存器、切换栈指针、并协调与高层C代码的调用约定。\nOS 底层入口动作 详细说明 1. 保存所有通用寄存器 硬件只保存了PC和状态标志，OS必须将剩余的所有通用寄存器（如R0-R31，x0-x29等）压入内核栈，形成结构化的“pt_regs”或“trap frame”，以便后续恢复 2. 确定事件原因 读取硬件寄存器的值，跳转到合适的异常处理程序中 3. 恢复上下文 异常或者中断处理完成后，恢复保存在栈上的上下文 4. 返回异常现场 从C函数返回后，恢复所有保存的寄存器，执行一条特殊的“异常返回”指令，由硬件恢复PC和特权级，继续执行原程序 异常的处理策略 异常类型 OS 高级处理动作（C代码） 外部硬件中断 1. 关中断（通常已经在入口处由硬件或汇编做了） 2. 调用中断控制器接口读取中断号 3. 根据中断号查找注册的设备驱动处理函数（如 request_irq 注册的handler） 4. 执行驱动的中断服务例程（ISR） 5. 处理完后，判断是否需要进行软中断（下半部）或唤醒等待进程 6. 开中断（可能） 系统调用（陷阱） 1. 判断系统调用号是否合法 2. 从用户空间拷贝参数（用 copy_from_user） 3. 执行对应的内核服务函数（如文件系统、进程管理、网络） 4. 将结果返回给用户 5. 检查是否需要重新调度 缺页（故障） 1. 获取发生故障的虚拟地址（读取控制寄存器） 2. 检查地址是否合法（是否在进程地址空间内） 3. 如果合法，执行页面分配或磁盘读取（换页） 4. 修改页表，TLB失效 5. 返回后硬件会自动重试那条指令 非法指令/除法错误 1. 向当前进程发送信号（如 SIGILL, SIGFPE） 2. 如果用户自定义了信号处理函数，则调用它；否则终止进程（core dump） 调试异常/断点 1. 暂停当前进程，唤醒调试器（如gdb） 2. 允许调试器查看或修改内存/寄存器 关键点：在这一阶段，OS可能修改进程的内存空间、调度状态、甚至切换到另一个进程。这些操作都不会影响硬件对下一次异常的处理。\n","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/riscv/","summary":"\u003ch2 id=\"指令集isa微架构操作系统\"\u003e指令集(ISA)，微架构，操作系统\u003c/h2\u003e\n\u003cp\u003e指令集简单来说就是一套语言规范，定义指令类型，寄存器，数据类型，还有cpu的管理模式等。而微架构是依据指令集设计出的处理器硬件结构与实现方案。相同的指令集设计出来的微架构可能完全不同，比如Intel和amd的芯片都是x86指令集。但是设计出来的cpu结构完全不同，性能和能耗也天差地别。影响cpu性能的大部分是微架构设计，工艺制程，系统级整合等，而指令集带来的差异往往只是小头。指令集的流行主要是商业和生态决定的。\u003c/p\u003e","title":"riscv体系结构"},{"content":"系统概述与启动流程 (关注宏观状态流转：从 Machine Mode 到 Supervisor Mode，再到第一个用户进程的诞生)\n核心概念 启动流程的本质是特权级的逐级让渡：从硬件最高特权（Machine Mode）出发，经过层层初始化和降权，最终将控制权交给最不信任的用户程序。整个过程是一条从\u0026quot;全权控制\u0026quot;到\u0026quot;受限执行\u0026quot;的信任链——每一步都在缩小 CPU 的权限范围，同时建立更多的软件抽象层。\n通用执行流程 一次完整的启动遵循以下宏观阶段：\n裸机入口（汇编阶段）：\nCPU 加电后，硬件将每个核心（hart）跳转到固定地址（0x80000000）。 此时没有任何 C 运行时环境——无栈、无全局变量。汇编代码的唯一职责是为每个核心计算独立的栈地址，然后跳转到 C 函数。多核系统中，每个核心按自己的 ID 获得不重叠的栈空间，避免并发踩踏。 Machine Mode 初始化与降级：\nMachine Mode 是硬件最高特权级，负责配置只有它才能访问的寄存器： 设置返回特权级：将 mstatus.MPP 设为 Supervisor Mode，使得\u0026quot;异常返回\u0026quot;指令（mret）执行后降级而非保持最高权限。 设置返回地址：将异常程序计数器（RISC-V: mepc）设为内核 main() 函数地址。 委托异常：将所有中断/异常委托给 Supervisor Mode 处理，让内核能自己管理中断而不需要固件介入。 配置物理内存保护：允许 Supervisor 访问全部物理内存。 启动定时器：配置时钟中断，为后续的抢占式调度打下基础。 执行 mret：CPU 跳转到 main()，特权级降为 Supervisor Mode。 内核 C 环境建立：\nmain() 中的初始化顺序体现了依赖链——先有内存分配器（其他子系统都需要分配内存），再有页表（启用虚拟地址），然后是进程表、中断处理、设备驱动，最后是文件系统： 物理页分配器（kinit）：管理空闲物理内存 内核页表（kvminit）：建立内核的虚拟地址映射 启用分页（kvminithart）：让虚拟地址翻译生效 进程表（procinit）：初始化 PCB 数组 中断向量（trapinit/trapinithart）：设置异常处理入口 设备中断（plicinit/plicinithart）：配置中断控制器 块缓存/文件系统/文件表：持久化子系统 第一个用户进程（userinit） 多核同步：\n只有 CPU 0（BSP, Bootstrap Processor）执行全部初始化。其他核心（AP）通过内存屏障等待 BSP 完成后，只启用分页和中断，直接进入调度器。内存屏障确保 BSP 的写操作对其他核心可见，防止指令重排导致 AP 看到不一致的初始化状态。 第一个用户进程诞生：\nuserinit() 创建 PID=1 的进程，但此时它只有空的地址空间。真正的用户程序是通过 exec() 系统调用加载 /init 程序完成的——这发生在该进程首次被调度器选中时的 forkret() 中。选择在进程上下文（而非 main()）中加载文件系统和执行 exec，是因为文件系统操作需要 sleep() 等进程原语，而 main() 还没有进程上下文。 设计权衡与工业界对比 维度 xv6 的选择 工业界 (Linux) 的做法 CPU 拓扑 硬编码单核假设，BSP 串行初始化 解析 Device Tree 动态发现硬件，多核并行启动 (SMP) 硬件描述 地址硬编码在头文件中 Device Tree Blob (DTB) 运行时描述硬件拓扑 初始化策略 一次性全部初始化 按需初始化，部分模块延迟到首次使用 启动协议 QEMU 直接跳转到内核 UEFI/BIOS → bootloader → 内核的标准启动链 用户态启发 程序加载的通用逻辑：任何程序执行都遵循\u0026quot;建立运行环境 → 加载代码 → 设置参数 → 跳转入口\u0026quot;的模式。理解 exec 的流程有助于理解动态链接器（ld.so）的工作原理。 依赖初始化顺序：大型 C++ 服务的启动同样面临子系统依赖问题。理解\u0026quot;先内存管理，再虚拟内存，再进程，最后文件系统\u0026quot;的顺序，有助于设计自己的服务初始化链。 源码锚点 kernel/start.c（Machine Mode 配置与降级）、kernel/main.c（子系统初始化顺序）、kernel/proc.c:userinit()（第一个进程的创建）\n核心机制：中断/Trap 与 锁/并发 (关注特权级切换的通用抽象，以及并发控制的边界与规则)\n核心概念 Trap 的本质：特权级切换的基础设施。无论是系统调用（主动陷入）、缺页异常（被动触发）还是硬件中断（外部信号），都通过同一套机制进入内核。它是用户态与内核态之间唯一的\u0026quot;合法通道\u0026quot;。 锁的核心目的：在多核并发环境下，用串行化换取共享状态的一致性。 一、Trap 机制：特权级切换的完整路径 为什么需要这么复杂？ 特权级切换时，CPU 必须完成四件事：保存\u0026quot;从哪来\u0026quot;（用户态的全部寄存器状态）、确认\u0026quot;到哪去\u0026quot;（内核的处理入口和栈）、切换\u0026quot;信任域\u0026quot;（页表从用户空间切换到内核空间）、处理完毕后精确恢复——仿佛什么都没发生。任何一个环节出错，都会导致系统崩溃或安全漏洞。\n通用执行流程 无论 x86、ARM 还是 RISC-V，一次完整的 Trap 都遵循以下 5 个宏观阶段：\n触发与陷落 (Trap)：\n用户态执行特殊指令（如 RISC-V 的 ecall，x86 的 syscall/int 0x80），或遇到异常（如缺页），或硬件发出中断信号。 硬件自动接管：提升 CPU 特权级，自动保存最关键的返回地址（异常程序计数器，RISC-V: sepc）和状态寄存器，并跳转到预设的内核入口（RISC-V: stvec 寄存器指向的地址）。 上下文保存 (Save Context)：\n软件接管（通常是一段精心设计的汇编代码，如 xv6 的 trampoline）。 将所有通用寄存器保存到内存中的固定结构——陷入帧 (Trapframe)，防止后续内核代码覆盖用户态数据。 每个进程都有自己独立的 trapframe，它既保存用户寄存器快照，也存放内核元数据（内核页表地址、内核栈指针、处理函数入口）。 Trapframe 的设计体现了\u0026quot;一个结构，两个角色\u0026quot;的思想——前 5 个字段是内核元数据（告诉 trampoline 代码\u0026quot;回到内核需要什么\u0026quot;），后面全部是用户寄存器快照：\n// kernel/proc.h — 每个进程独享一份，映射到用户页表的固定虚拟地址 struct trapframe { /* 内核元数据：trampoline 代码切换环境时使用 */ uint64 kernel_satp; // 内核页表地址 uint64 kernel_sp; // 该进程的内核栈顶 uint64 kernel_trap; // usertrap() 函数地址 uint64 epc; // 用户态程序计数器（被中断的那条指令） uint64 kernel_hartid; // CPU 核心 ID /* 用户寄存器快照：完整保存用户态执行现场 */ uint64 ra, sp, gp, tp; uint64 t0-t6; // 临时寄存器 uint64 s0-s11; // 保存寄存器 uint64 a0-a7; // 参数/返回值寄存器 }; 为什么 trapframe 放在用户页表的固定虚拟地址？ 因为切换页表的那一刻，内核页表还不可用，必须有一个在用户页表中就能访问到的、存放内核信息的地方。这是一个\u0026quot;鸡生蛋\u0026quot;问题的精巧解法。\n环境切换 (Switch Environment)：\n切换栈：从用户栈切换到该进程专属的内核栈（防止用户态篡改内核栈）。 切换页表：从用户页表切换到内核页表。 关键难点：切换页表的瞬间，CPU 仍在执行指令。因此，这段切换代码所在的内存页（Trampoline）必须在用户页表和内核页表中等值映射（映射到相同的虚拟地址），否则切换瞬间会导致页错误崩溃。这是整个 Trap 机制中最精巧的设计之一。 分发与执行 (Dispatch \u0026amp; Execute)：\n进入内核 C 函数（如 usertrap）。 查表路由：根据\u0026quot;陷入原因\u0026quot;（RISC-V: scause 寄存器）判断是系统调用、设备中断还是异常，分别走不同处理路径。 系统调用的进一步分发：根据寄存器中的\u0026quot;系统调用号\u0026quot;（RISC-V: a7），在系统调用派发表中找到对应的内核函数（如 sys_write）： // kernel/syscall.c — 函数指针数组，系统调用号作为索引 static uint64 (*syscalls[])(void) = { [SYS_fork] sys_fork, [SYS_exit] sys_exit, [SYS_wait] sys_wait, [SYS_pipe] sys_pipe, [SYS_read] sys_read, [SYS_write] sys_write, [SYS_exec] sys_exec, [SYS_open] sys_open, // ... 共 21 个系统调用 }; void syscall(void) { int num = p-\u0026gt;trapframe-\u0026gt;a7; // 从 trapframe 读取系统调用号 if (num \u0026gt; 0 \u0026amp;\u0026amp; num \u0026lt; NELEM(syscalls) \u0026amp;\u0026amp; syscalls[num]) p-\u0026gt;trapframe-\u0026gt;a0 = syscalls[num](); // 调用并把返回值写入 a0 } 参数获取：从之前保存的 Trapframe 中提取用户传入的参数（RISC-V: a0-a5），并进行严格的合法性校验——所有用户指针必须通过 copyin()/copyout() 访问，不能直接解引用，防止恶意用户传入内核地址。 恢复与返回 (Restore \u0026amp; Return)：\n将系统调用的返回值写入用户态的返回值寄存器（RISC-V: a0）。 设置返回特权级（清除\u0026quot;先前特权级\u0026quot;标志位，使 sret 后回到 User Mode）。 逆向执行上述步骤：从 Trapframe 恢复所有通用寄存器 → 切换回用户页表和用户栈 → 执行特权返回指令（RISC-V: sret），CPU 回到用户态继续执行下一条指令。 内核态中断处理 当内核自身遇到中断（如定时器中断），不需要切换页表和栈——因为已经在内核中了。直接保存 caller-saved 寄存器到当前内核栈，调用内核的 trap 处理函数。关键约束：进入内核 trap 处理时中断必须关闭，防止嵌套中断导致栈溢出。\n信号 (Signal) — 用户态异步事件注入 xv6 没有实现信号机制，但信号是 Unix/Linux 中最重要的进程间通信方式之一。它的核心问题是：内核如何在不破坏用户态执行现场的前提下，强制用户程序执行一段特定代码？\n信号解决的矛盾 进程通常只能通过系统调用主动与内核交互。但有些事件需要内核异步通知进程：\n用户按下 Ctrl+C（SIGINT） 子进程退出（SIGCHLD） 非法内存访问（SIGSEGV） 定时器到期（SIGALRM） 信号的本质是内核向用户态注入的一次\u0026quot;软件中断\u0026quot;——它打断进程的正常执行流，强制跳转到用户注册的处理函数。\n信号投递的通用流程 1. 信号产生： 内核事件（如 Ctrl+C）或 kill() 系统调用 → 设置目标进程的 pending 信号位 2. 信号投递（在 trap 返回用户态之前检查）： usertrap() / prepare_return() 检查 pending \u0026amp; ~blocked → 如果有待处理信号： a. 在用户栈上构造一个\u0026#34;信号帧\u0026#34;（保存当前的 trapframe） b. 修改 trapframe 中的 PC → 指向用户注册的 handler c. 修改 trapframe 中的 SP → 指向信号帧 d. 将信号编号作为参数传给 handler 3. 信号处理函数执行： 用户态 handler(signum) 正常执行 4. 信号返回： handler 结尾调用 sigreturn() 系统调用 → 内核从信号帧恢复原始 trapframe → 进程从被中断的地方继续执行，仿佛什么都没发生 信号处理的三种模式 模式 含义 典型信号 默认行为 内核预设的处理（终止、忽略、core dump、停止） SIGINT → 终止 忽略 用户显式设置为 SIG_IGN SIGPIPE 自定义处理 用户注册 handler 函数 SIGINT → 自定义清理逻辑 关键设计难点 1. 重入问题：如果信号处理函数中又收到同一个信号怎么办？sigaction 的 SA_RESTART 标志控制被中断的系统调用是否自动重启。\n2. 信号帧的构造位置：信号帧必须放在用户栈上（而非内核栈），因为 sigreturn 需要从用户态调用。如果栈空间不足（如栈溢出场景），SIGSEGV 的处理本身也会失败——这就是为什么有些系统会保留一个备用信号栈 (sigaltstack)。\n3. 多信号的竞争：多个信号同时 pending 时的投递顺序未定义（POSIX 不保证）。内核通常按信号编号从小到大投递。\n4. 与 trap 的集成：信号检查必须在 trap 返回路径的最后一步——因为此时 trapframe 已经准备好返回用户态，注入信号帧不会破坏内核状态。\nxv6 的 killed 机制是信号的极简原型：xv6 的 p-\u0026gt;killed 标志就是一种\u0026quot;只有 SIGKILL\u0026quot;的信号系统——进程在 trap 返回时检查 killed 标志，如果为 1 则退出。信号机制将其泛化为：多种信号类型 + 用户可注册处理函数 + 可阻塞/忽略。\n设计权衡与工业界对比 维度 xv6 的选择 工业界 (Linux) 的做法 Trap 入口 全部走 trampoline（统一入口） syscall 有专用快速入口 (vDSO)，减少路径长度 上下文保存 全部寄存器保存到 trapframe 仅保存 caller-saved，使用 pt_regs 结构 系统调用表 静态数组，编译时固定 动态系统调用表，支持版本兼容和过滤 页表切换 每次 trap 都切换 内核页表全局共享，单次写入；PCID 减少 TLB 刷新 信号处理 不支持 在 trap 返回路径中注入信号处理帧 参数校验 copyin/copyout 拷贝数据 copy_from_user + 地址范围检查，更高效 用户态启发 系统调用开销：每次系统调用经历完整 trap 路径（数千周期）。这解释了为什么高性能服务器使用 epoll/io_uring 等批量 I/O 机制——减少 trap 次数就是减少开销。一次 writev 比多次 write 高效得多。 vDSO 的价值：gettimeofday() 等频繁调用通过映射内核代码到用户空间，避免 trap 开销。这是\u0026quot;用空间换时间\u0026quot;在特权级切换场景的应用。 源码锚点 kernel/trampoline.S（trampoline page，跨特权级切换的\u0026quot;两栖\u0026quot;代码）、kernel/trap.c:usertrap()（Trap 分发入口）、kernel/syscall.c（系统调用派发表与参数解析）\n二、锁与并发控制 为什么需要锁？ 多核系统中，多个 CPU 可能同时访问同一数据。如果没有同步机制，就会出现竞态条件——结果取决于指令执行的相对时序，不可复现、不可预测。锁的核心目的是保护共享状态的一致性，代价是牺牲并发度。\n两种锁的语义 自旋锁 (Spinlock) —— 短期、不可睡眠的锁：\n获取不到就忙等待（spin），不放弃 CPU。 持有期间必须关中断。原因：如果持有锁时被中断打断，而中断处理函数又尝试获取同一把锁 → 死锁。这是\u0026quot;自旋锁必须配关中断\u0026quot;的根本原因。 适用场景：临界区极短（几条指令），如修改一个计数器、操作链表指针。 获取锁的核心是一个原子交换操作——确保多核竞争时只有一个能成功：\n// kernel/spinlock.c void acquire(struct spinlock *lk) { push_off(); // 关中断（嵌套计数） if (holding(lk)) panic(\u0026#34;acquire\u0026#34;); // 死锁检测：自己不能重入 // 原子交换：将 lk-\u0026gt;locked 设为 1，返回旧值 // __ATOMIC_ACQUIRE 确保临界区的内存操作不会被重排到获取锁之前 while (__atomic_exchange_n(\u0026amp;lk-\u0026gt;locked, 1, __ATOMIC_ACQUIRE) != 0) ; // 忙等待直到旧值为 0 lk-\u0026gt;cpu = mycpu(); // 记录持有者（用于调试） } void release(struct spinlock *lk) { if (!holding(lk)) panic(\u0026#34;release\u0026#34;); lk-\u0026gt;cpu = 0; // __ATOMIC_RELEASE 确保临界区的修改在释放锁之前对其他 CPU 可见 __atomic_store_n(\u0026amp;lk-\u0026gt;locked, 0, __ATOMIC_RELEASE); pop_off(); // 恢复中断状态 } RISC-V 细节：__atomic_exchange_n 编译为 amoswap.w.aq（原子交换 + acquire 语义），__atomic_store_n 编译为 fence rw,w + sw（release 语义）。acquire/release 配对确保了临界区的内存可见性。\n睡眠锁 (Sleeplock) —— 长期、可睡眠的锁：\n获取不到就让出 CPU（sleep），等待被唤醒。 持有期间可以睡眠（如等待磁盘 I/O 完成）。 底层基于自旋锁 + sleep/wakeup 条件等待机制实现。 适用场景：临界区可能很长，如 inode 操作（涉及磁盘读写）。 sleep/wakeup — 条件等待机制 这是 xv6 中最精巧的并发原语之一。它基于**通道（channel）**的条件变量，用一个指针地址作为匹配标识。\n为什么\u0026quot;顺序\u0026quot;是生死攸关的？ sleep 的核心问题是防止丢失唤醒 (lost wakeup)。看以下两个版本：\nCPU 0 (sleep) CPU 1 (wakeup) ───────────── ────────────── 错误版本：先释放条件锁，再获取进程锁 release(lk) ← 释放条件锁 │ │ ← 时间窗口：wakeup 可能在此刻发生 │ acquire(\u0026amp;p-\u0026gt;lock) │ p-\u0026gt;state != SLEEPING ← 还没设！ │ release(\u0026amp;p-\u0026gt;lock) │ wakeup 白跑了 ▼ acquire(\u0026amp;p-\u0026gt;lock) p-\u0026gt;state = SLEEPING ← 太晚了，wakeup 已经过去 sched() ← 永远睡死 ───────────────────────────────────────────────────── 正确版本：先获取进程锁，再释放条件锁 acquire(\u0026amp;p-\u0026gt;lock) ← 锁住自己 │ release(lk) ← 释放条件锁 │ 此时 wakeup 尝试 acquire(\u0026amp;p-\u0026gt;lock) │ 但拿不到——必须等我们设好状态 ▼ p-\u0026gt;state = SLEEPING sched() ← 安全睡眠 │ ▼ wakeup 终于拿到 p-\u0026gt;lock p-\u0026gt;state == SLEEPING ✓ p-\u0026gt;state = RUNNABLE ← 唤醒成功 错误版本 — 先释放条件锁，再获取进程锁：\nsleep(chan, lk): release(lk) // ← 释放条件锁 acquire(\u0026amp;p-\u0026gt;lock) // ← 获取进程锁 p-\u0026gt;chan = chan p-\u0026gt;state = SLEEPING sched() 问题：在 release(lk) 和 acquire(\u0026amp;p-\u0026gt;lock) 之间，另一个 CPU 可能调用 wakeup(chan)——此时当前进程还没设为 SLEEPING，wakeup 找不到它。等当前进程继续执行设为 SLEEPING 后，wakeup 已经过去了，进程永远睡死。\nxv6 的正确版本 — 先获取进程锁，再释放条件锁：\n// kernel/proc.c void sleep(void *chan, struct spinlock *lk) { struct proc *p = myproc(); acquire(\u0026amp;p-\u0026gt;lock); // ① 先锁住自己——确保 wakeup 不会\u0026#34;错过\u0026#34;我们 release(lk); // ② 再释放条件锁——此时即使 wakeup 发生， // 它也拿不到 p-\u0026gt;lock，必须等我们设好状态 p-\u0026gt;chan = chan; // ③ 设置睡眠通道 p-\u0026gt;state = SLEEPING; // ④ 标记为睡眠 sched(); // ⑤ 让出 CPU // 被 wakeup 唤醒后继续执行： p-\u0026gt;chan = 0; // 清除通道 release(\u0026amp;p-\u0026gt;lock); // 释放进程锁 acquire(lk); // 重新获取条件锁 } // kernel/proc.c void wakeup(void *chan) { struct proc *p; for (p = proc; p \u0026lt; \u0026amp;proc[NPROC]; p++) { if (p != myproc()) { acquire(\u0026amp;p-\u0026gt;lock); // 必须获取 p-\u0026gt;lock if (p-\u0026gt;state == SLEEPING \u0026amp;\u0026amp; p-\u0026gt;chan == chan) p-\u0026gt;state = RUNNABLE; // 标记为可运行（唤醒） release(\u0026amp;p-\u0026gt;lock); } } } 关键洞察：p-\u0026gt;lock 在 sleep 和 wakeup 之间形成了一个临界区。sleep 在持有 p-\u0026gt;lock 的期间设置 chan 和 state，wakeup 在持有 p-\u0026gt;lock 的期间检查它们。这保证了\u0026quot;设置睡眠状态\u0026quot;和\u0026quot;检查睡眠状态\u0026quot;之间不会被插入任何东西——唤醒不会丢失。\n类比 POSIX 条件变量：这个模式等价于 pthread_cond_wait 中\u0026quot;持有 mutex → 检查条件 → 调用 wait（原子地释放 mutex 并睡眠）\u0026ldquo;的范式。如果在检查条件之前释放 mutex，就可能丢失信号——这是所有条件变量的通病。\n锁的使用规则 锁序规则：所有代码必须以相同的顺序获取多把锁，否则会死锁（如 A→B 和 B→A 的循环等待）。 持锁睡眠规则：持有自旋锁时不能睡眠（因为睡眠需要调用调度器，而调度器可能需要同一把锁）。 中断规则：持有自旋锁时中断必须关闭（防止中断处理函数重入同一把锁）。 从全局锁到细粒度锁：xv6 的并发瓶颈与优化思路 xv6 中大量使用全局锁（如 bcache.lock 保护整个块缓存、kmem.lock 保护全部物理页），这意味着同一时刻只有一个 CPU 能访问这些共享资源。在多核系统上，这会导致严重的锁竞争 (lock contention)——CPU 花在等锁上的时间比真正干活的时间还多。\n细粒度锁的核心思想 将一把大锁拆成多把小锁，每把小锁只保护数据的一个子集，让不访问同一子集的操作可以并行：\nxv6 的做法（粗粒度）： bcache.lock ──保护──→ 所有 NBUF 个缓存块 优化后（细粒度）： bcache.bucket[0].lock ──保护──→ hash 值为 0 的缓存块 bcache.bucket[1].lock ──保护──→ hash 值为 1 的缓存块 ... bcache.bucket[n].lock ──保护──→ hash 值为 n 的缓存块 典型优化案例：块缓存 (bcache) xv6 的 bget() 用一把全局锁保护整个 LRU 链表。优化方案：\n哈希分桶：将缓存块按 (dev, blockno) 哈希到 N 个桶中，每个桶一把锁。查找缓存时只需锁对应的桶，不同桶的操作完全并行。 per-CPU 缓存：每个 CPU 维护自己的 LRU 列表，只有自己的列表为空时才从其他 CPU \u0026ldquo;偷\u0026rdquo; 一个块。消除了单点竞争。 LRU 的近似替代：精确 LRU 需要全局排序，是锁竞争的根源。改用 CLOCK 算法（近似 LRU）或采样策略，用\u0026quot;足够好\u0026quot;换取\u0026quot;足够快\u0026rdquo;。 其他细粒度锁优化场景 场景 xv6 的锁 优化方向 物理页分配 全局 kmem.lock per-CPU free list + 批量借用 进程表遍历 遍历时逐个 acquire(\u0026amp;p-\u0026gt;lock) RCU 读端无锁 + 延迟回收 inode 缓存 itable.lock 全局锁 per-bucket 哈希表 文件描述符表 ftable.lock 全局锁 per-fd 或 per-CPU 细粒度锁的代价：更多锁 = 更多内存、更复杂的锁序管理、更高的死锁风险。Linux 用 lockdep 在运行时检测锁依赖图，自动报告潜在死锁。没有 lockdep 的情况下，必须手动维护锁序文档。\n无锁的终极方案：RCU (Read-Copy-Update) 让读操作完全无锁——读端只需禁止抢占（rcu_read_lock），写端先复制一份数据修改，再原子替换指针，最后等待所有读端离开临界区后释放旧数据。代价是写操作更复杂，且需要垃圾回收机制。\n设计权衡与工业界对比 维度 xv6 的选择 工业界 (Linux) 的做法 自旋锁实现 简单忙等待 ticket lock → qspinlock（减少缓存行 bouncing） 读写并发 不支持 rwlock：读并发、写独占 无锁机制 无 RCU (Read-Copy-Update)：读操作零开销 死锁检测 无 lockdep 运行时锁依赖图检测 锁粒度 全局锁（如 bcache.lock） per-CPU / per-namespace 细粒度锁 条件等待 channel 指针 + sleep/wakeup POSIX 条件变量 + futex 用户态启发 用户态锁的实现：pthread_mutex 最终通过 futex 实现——无竞争时在用户态自旋（零系统调用），有竞争时才陷入内核睡眠。这与 xv6 的 spinlock/sleeplock 思路一脉相承。 条件变量模式：sleep/wakeup 的 channel 模式是 POSIX 条件变量的底层原型。理解它有助于正确使用 pthread_cond_wait——必须在持有 mutex 的情况下检查条件，否则会丢失信号。 锁粒度的选择：xv6 的全局 bcache.lock 是性能瓶颈的典型例子。高并发服务中，应该尽量使用细粒度锁或无锁数据结构（如 concurrent hash map）。 源码锚点 kernel/spinlock.c:acquire()（自旋锁与中断控制）、kernel/sleeplock.c:acquiresleep()（睡眠锁实现）、kernel/proc.c:sleep()/wakeup()（条件等待机制）\n资源管理模块（进程与内存） (关注状态机、资源交接抽象，以及虚拟内存的映射机制)\n核心概念 进程控制块 (PCB)：进程在内核中的\u0026quot;身份证\u0026quot;。它封装了程序运行所需的全部资源：地址空间、打开的文件、执行上下文、父子关系。进程的状态机驱动着整个 OS 的运行。 虚拟内存的本质：为每个进程提供独立的、连续的地址空间假象，实际物理内存可能是碎片化的。它是隔离、抽象和按需分配三大价值的基石。 一、进程管理 进程状态机 进程的生命周期是一个状态机，每个状态对应一种资源占有方式：\nfork() scheduler() UNUSED ──→ USED ────→ RUNNABLE ⇄ RUNNING ↑ ↓ ↓ sleep() exit() ↓ ↓ SLEEPING ──→ ZOMBIE ──→ UNUSED (wait 回收) RUNNABLE：万事俱备，只等调度器选中。 SLEEPING：主动让出 CPU，等待某个事件（I/O 完成、锁释放、子进程退出）。 ZOMBIE：已死但未葬——进程已退出，但 PCB 还在等父进程 wait() 收集退出状态。 PCB 是进程在内核中的\u0026quot;身份证\u0026quot;，它封装了进程运行所需的全部资源：\n// kernel/proc.h — 进程控制块 struct proc { struct spinlock lock; /* 受 p-\u0026gt;lock 保护：进程核心状态 */ enum procstate state; // UNUSED / USED / SLEEPING / RUNNABLE / RUNNING / ZOMBIE void *chan; // sleep 通道（指针地址作为匹配标识） int killed; // 终止标记 int xstate; // 退出状态码（返回给父进程） int pid; /* 受 wait_lock 保护：进程关系 */ struct proc *parent; // 父进程指针 /* 私有数据：不需要锁保护 */ uint64 kstack; // 内核栈虚拟地址 uint64 sz; // 用户地址空间大小 pagetable_t pagetable; // 用户页表 struct trapframe *trapframe; // 陷入帧（保存用户态寄存器） struct context context; // 内核上下文（swtch 使用，仅保存 callee-saved 寄存器） struct file *ofile[NOFILE]; // 打开的文件表（最多 16 个） struct inode *cwd; // 当前工作目录 char name[16]; // 进程名（调试用） }; 注意锁的分层：p-\u0026gt;lock 保护进程状态，wait_lock 保护父子关系。这种分层避免了在频繁的状态变更中持有全局锁，是细粒度锁设计的体现。\nfork / exec / wait / exit — 资源的复制、重建、回收 fork 解决的矛盾：如何创建一个与当前执行环境几乎相同的独立实体？\nxv6 的做法是全量复制——复制页表、复制物理页、复制文件描述符引用。这保证了父子进程完全独立，但代价高昂（O(内存大小)）。子进程的返回值被设为 0（通过修改 trapframe 中的 a0），父进程返回子进程 PID，这是区分父子的唯一手段。\nexec 解决的矛盾：如何在不改变进程身份（PID）的前提下，彻底替换它正在执行的程序？\n它销毁旧的地址空间，按 ELF 格式构建新的——逐段加载代码和数据、分配用户栈（含 guard page 防止栈溢出越界）、将参数字符串和 argv 指针压入栈。最后设置新的程序计数器和栈指针。exec 是\u0026quot;换脑手术\u0026quot;——PID 不变，但地址空间、代码、数据全部换新。\nexit/wait 解决的矛盾：进程退出后，谁来回收它的资源？\nxv6 设计了 ZOMBIE 状态作为\u0026quot;缓冲\u0026quot;——exit 不直接释放 PCB，而是等父进程通过 wait() 收集退出状态后再释放。exit 同时将孤儿进程重新挂到 init 进程下，确保总有人负责回收。如果父进程先死，孤儿进程不会成为\u0026quot;幽灵\u0026quot;。\n上下文切换 上下文切换的本质是保存当前进程的内核寄存器，恢复下一个进程的寄存器，跳转执行。只需保存 callee-saved 寄存器（返回地址、栈指针、s0-s11），因为 caller-saved 寄存器已经在调用约定中由调用者保存。\n// kernel/proc.h — 上下文结构，仅 14 个寄存器（vs trapframe 的 31 个） struct context { uint64 ra; // 返回地址 uint64 sp; // 栈指针 uint64 s0-s11; // callee-saved 寄存器 }; 调度器选中进程后调用 swtch 从调度器上下文切换到进程上下文；进程需要让出 CPU 时调用 swtch 从进程上下文切回调度器上下文。注意：内核态的上下文切换不需要切换页表——所有进程共享内核页表。\n为什么只需保存 14 个寄存器？ RISC-V 调用约定中，ra, sp, s0-s11 是 callee-saved（被调用函数必须保存恢复），t0-t6, a0-a7 是 caller-saved（调用者自己负责）。swtch 是一个普通函数调用，所以编译器已经帮我们保存了 caller-saved 寄存器。这就是为什么理解调用约定对理解上下文切换至关重要。\n调度器 xv6 使用简单的 Round-Robin 调度：每个 CPU 独立运行一个无限循环，遍历进程表找到 RUNNABLE 进程切换执行。没有优先级，没有时间片量化（仅靠定时器中断强制抢占）。没有进程时执行 wfi（Wait For Interrupt）降低功耗。\n管道 — 进程间通信 管道是 xv6 中唯一的进程间通信机制，它的设计体现了\u0026quot;文件即接口\u0026quot;的思想——管道和普通文件共享同一套 read/write 系统调用：\n// kernel/pipe.c — 管道就是一个环形缓冲区 + 两个文件描述符 struct pipe { struct spinlock lock; char data[PIPESIZE]; // 512 字节的环形缓冲区 uint nread; // 已读字节数 uint nwrite; // 已写字节数 int readopen; // 读端是否打开 int writeopen; // 写端是否打开 }; 同步规则：写满时写端睡眠、唤醒读端；读空时读端睡眠、唤醒写端。关闭写端后读端返回 0（EOF），关闭读端后写端返回 -1。\n设计权衡与工业界对比 维度 xv6 的选择 工业界 (Linux) 的做法 fork 实现 全量复制物理页 COW (Copy-On-Write)：共享物理页，写入时才复制 线程模型 无（进程即执行单位） clone() 支持共享地址空间的线程 PID 管理 简单递增 PID namespace 隔离，PID 回收与复用 调度算法 简单 Round-Robin CFS (Completely Fair Scheduler) 基于虚拟运行时间 进程上限 编译时常量 (64) 动态分配，受 pid_max 和资源限制 COW 为何重要？ fork 后紧跟 exec 是 shell 执行命令的标准模式。如果 fork 全量复制，exec 又立即丢弃，复制就白做了。COW 让 fork 的代价从 O(内存大小) 降到 O(页表大小)。\nCOW fork 的核心原理 COW 的核心思想是：fork 时不复制物理页，而是让父子进程共享同一份物理内存，仅在某一方尝试写入时才真正复制。\n实现的关键是对页表权限位的利用：\nfork 时： 1. 创建子进程页表，但不分配新物理页 2. 将父子双方的 PTE 都标记为\u0026#34;只读\u0026#34;（清除 PTE_W） 3. 记录每个物理页的引用计数（共享计数） 写入时（触发 Store Page Fault）： 1. 陷入内核，检测到是 COW 页（PTE_V 有效，PTE_W 无效，但标记为 COW） 2. 如果该物理页引用计数 == 1：直接将 PTE 改为可写（无需复制，已经是独占） 3. 如果引用计数 \u0026gt; 1：分配新物理页 → 复制内容 → 更新 PTE 指向新页并设为可写 → 原物理页引用计数减 1 关键设计：COW 页在页表中是\u0026quot;只读\u0026quot;的，但并非真的只读——缺页处理函数会区分\u0026quot;真正的只读页越权写入\u0026quot;（应 SIGSEGV）和\u0026quot;COW 页的正常写入\u0026quot;（应复制）。这需要一个额外的位（如 RISC-V 的 RSW 位或软件维护的 bitmap）来标记哪些页是 COW 页。\nCOW 的连锁效应：当物理页被复制后，如果该页还被其他进程共享，需要通过 TLB shootdown 通知其他 CPU 刷新 TLB 中的旧翻译——否则其他 CPU 可能继续使用旧的只读映射。这是多核 COW 的隐藏复杂性。\n用户态启发 fork 的性能意识：fork 后不 exec 的场景（如网络服务 fork 子进程处理请求）要注意内存复制代价。现代做法是用 vfork 或 posix_spawn 替代。 僵尸进程：父进程不 wait，子进程变僵尸占用 PCB。这就是 daemon 要 double fork 的原因——让 init 成为祖先进程负责回收。 上下文切换的开销：每次上下文切换都有 TLB 刷新、缓存污染的隐性代价。高并发服务倾向于用协程/用户态线程减少内核态切换。 源码锚点 kernel/proc.c:kfork()（fork 的全量复制语义）、kernel/exec.c:kexec()（exec 的地址空间重建）、kernel/proc.c:scheduler()/sched()（调度器与上下文切换）\n二、内存管理 虚拟内存解决了什么矛盾？ 程序需要连续的地址空间来简化编程，但物理内存是碎片化的、多进程共享的。虚拟内存通过页表这个\u0026quot;翻译层\u0026quot;，为每个进程提供独立的、连续的地址空间假象。它同时带来三大价值：\n隔离：一个进程的野指针不会破坏另一个进程的数据。 抽象：程序不需要关心物理内存布局。 按需分配：可以分配比物理内存更大的地址空间（配合延迟分配和 swap）。 虚拟地址空间布局 代码段（.text，可读可执行）→ 数据段（.data，可读可写）→ 堆（向上增长，通过 sbrk 扩展）→ 栈（固定大小，向下增长）→ guard page（不可访问，检测栈溢出）→ trapframe（陷入帧，内核写入）→ trampoline（陷入入口代码，与内核共享）。\n内核地址空间使用直接映射（虚拟地址 = 物理地址 + 偏移），简化了内核的地址转换。\n页表遍历与映射 页表的本质是一个函数：虚拟地址 → 物理地址 + 权限。\nxv6 使用 RISC-V 的 Sv39 方案，39 位虚拟地址被分为三级索引，每级 9 位（512 个条目），最后一级索引到具体的页表项（PTE）。每个 PTE 包含物理页号和权限位（读/写/执行/用户可访问）。\n// 虚拟地址的位划分 (Sv39)： // [38..30] 9位 → L2 索引（页目录） // [29..21] 9位 → L1 索引（页目录） // [20..12] 9位 → L0 索引（页表） // [11..0] 12位 → 页内偏移（4KB 页） walk() 是页表遍历的核心——从根页表开始，逐级索引，找到最终的 PTE：\n// kernel/vm.c pte_t *walk(pagetable_t pagetable, uint64 va, int alloc) { if (va \u0026gt;= MAXVA) panic(\u0026#34;walk\u0026#34;); for (int level = 2; level \u0026gt; 0; level--) { // 从 L2 到 L1 逐级下降 pte_t *pte = \u0026amp;pagetable[PX(level, va)]; // 用虚拟地址的对应 9 位做索引 if (*pte \u0026amp; PTE_V) { // 有效 → 下一级页表已存在 pagetable = (pagetable_t)PTE2PA(*pte); // 提取物理地址，继续 } else { if (!alloc || (pagetable = kalloc()) == 0) return 0; // 不分配或内存不足 memset(pagetable, 0, PGSIZE); *pte = PA2PTE(pagetable) | PTE_V; // 建立映射，继续 } } return \u0026amp;pagetable[PX(0, va)]; // 返回 L0 的 PTE 指针 } 关键设计：walk 返回的是 PTE 的指针而非值，调用者可以直接修改它来建立或改变映射。alloc 参数控制是否按需创建中间级页表——这就是\u0026quot;按需构建页表\u0026quot;的机制，与延迟分配配合，避免为未使用的地址空间浪费页表内存。\n物理页分配器 物理页分配器管理空闲的物理内存页。xv6 使用最简单的数据结构——空闲链表：每个空闲页的起始位置存放指向下一个空闲页的指针。\n// kernel/kalloc.c — 空闲页复用自身的内存作为链表节点 struct run { struct run *next; // 指向下一个空闲页（就存在该页自己的内存中） }; struct { struct spinlock lock; // 保护 freelist 的全局锁 struct run *freelist; // 空闲页链表头 } kmem; kalloc() 从链表头取出一个页，kfree() 将页放回链表头。全局锁保护链表操作。\n精巧之处：空闲页没有被使用，所以可以借用它的内存存放链表指针——零额外空间开销。这是侵入式链表 (intrusive list) 的经典应用。\n调试技巧：kfree 用特定值填充（memset(pa, 1, PGSIZE)），kalloc 用另一个值填充，方便检测悬挂引用和未初始化读取——访问已释放的页面会看到 0x0101...，访问未初始化的页面会看到 0x0505...。\n延迟分配 (Lazy Allocation) sbrk() 可以只增加进程的虚拟地址空间大小，不立即分配物理页。当进程实际访问该地址时，触发缺页异常，内核才分配物理页并建立映射——如果分配了但从未使用，物理页永远不会被浪费。这是一种\u0026quot;按需付费\u0026quot;的策略。\nmmap — 内存映射文件与灵活的地址空间管理 sbrk() 只能管理堆区域，而 mmap 允许用户程序将文件或匿名内存映射到虚拟地址空间的任意位置，是现代 OS 中最重要的内存管理接口。\nmmap 解决的矛盾 用户程序需要三种能力，sbrk() 都无法提供：\n文件映射：将文件内容直接映射到内存，通过指针访问代替 read()/write()，减少数据拷贝。 匿名内存：分配不与任何文件关联的大块内存（如 malloc 对大块的分配）。 灵活布局：在地址空间的任意位置映射，不受堆增长方向的限制。 核心机制 // 用户态接口 void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); 内核需要维护一个 VMA (Virtual Memory Area) 链表，记录进程地址空间中每一段映射的属性：\n// Linux 中的 vm_area_struct（xv6 未实现，但原理一致） struct vm_area { uint64 start, end; // 映射的虚拟地址范围 uint64 flags; // 权限：PROT_READ / PROT_WRITE / PROT_EXEC uint64 file_offset; // 文件偏移（匿名映射时为 -1） struct file *file; // 关联的文件（匿名映射时为 NULL） struct vm_area *next; // 链表指针 }; mmap 的两种模式 文件映射 匿名映射 flags MAP_FILE MAP_ANONYMOUS 数据来源 磁盘文件 零页 典型用途 共享库加载、数据库 mmap malloc 大块分配、线程栈 共享 vs 私有 MAP_SHARED：写回磁盘；MAP_PRIVATE：COW 通常 MAP_PRIVATE 缺页处理的扩展 mmap 区域的缺页处理比 sbrk 更复杂——内核需要查找 VMA 链表确定该地址属于哪个映射，然后：\n文件映射：从磁盘读取对应偏移的数据到新分配的物理页 匿名映射：分配零填充的物理页 COW：复制并解除共享 mmap 的性能陷阱：MAP_SHARED 文件映射的写入会直接修改 page cache，由内核的 pdflush 线程异步刷盘。如果不调 msync()，崩溃后可能丢失数据——这与 write() + fsync() 的语义不同。\n与 sbrk 的关系：现代 malloc 实现（如 glibc）对小块用 sbrk 扩展堆，对大块（通常 \u0026gt; 128KB）用 mmap(MAP_ANONYMOUS) 分配独立区域。这样大块释放时可以直接 munmap 归还给内核，而 sbrk 只能收缩堆顶。\n设计权衡与工业界对比 维度 xv6 的选择 工业界 (Linux) 的做法 页表级数 3 级 (Sv39) 4~5 级 (Sv48/Sv57)，支持更大地址空间 物理分配器 简单空闲链表 + 全局锁 Buddy System（减少碎片）+ Slab（对象缓存）+ per-CPU 缓存 延迟分配 基础支持 (lazy sbrk) 完整支持 (mmap lazy, demand paging) COW 不支持 fork 时默认 COW 大页 不支持 2MB/1GB Huge Pages，减少 TLB miss 内存回收 无 swap kswapd + LRU 链表 + swap 分区 用户态启发 栈溢出检测：guard page 的设计启发——在栈底放一个不可访问的页，溢出时触发段错误而非静默破坏数据。调试 segfault 时，如果地址靠近栈区域，大概率是栈溢出。 malloc 的底层：用户态 malloc 最终通过 sbrk() 或 mmap() 向内核申请虚拟地址空间。理解虚拟地址布局有助于诊断内存问题——代码段地址说明函数指针错误，栈地址说明栈溢出，堆地址说明堆损坏。 虚拟内存与性能：页表遍历是开销（每级一次内存访问），这就是 TLB（地址翻译缓存）存在的原因。大页 (Huge Pages) 通过减少页表级数和 TLB miss 来提升性能，对数据库等大内存应用尤为重要。 源码锚点 kernel/vm.c:walk()（三级页表遍历）、kernel/kalloc.c:kalloc()/kfree()（物理页分配器）、kernel/proc.c:growproc()（延迟分配的入口）\n持久化模块（文件系统与日志） (关注 inode 抽象、路径解析，以及崩溃恢复的原子性保证)\n核心概念 VFS 抽象：将磁盘这种线性的、固定大小的块设备，组织成树形的、可按名存取的文件层次结构。文件、目录、设备都被统一为 inode，通过文件描述符表对外暴露统一的读写接口。 日志 (Logging) 的原子性保证：文件系统操作通常需要修改多个磁盘块。如果中途崩溃（断电），文件系统会处于不一致状态。Write-Ahead Logging 通过\u0026quot;先写日志，再写真实位置\u0026quot;确保操作的原子性——崩溃恢复时检查日志，有完整事务则重放，否则丢弃。 一、文件系统的五层架构 xv6 的文件系统分为五层，每层为上层提供抽象，隐藏下层的复杂性：\n路径层 namei() —— 将 \u0026#34;/a/b/c\u0026#34; 解析为 inode ↓ 目录层 dirlookup() / dirlink() —— 目录是包含 (name, inum) 对的特殊文件 ↓ 文件层 ialloc() / iget() / readi() / writei() —— inode 的分配、缓存与读写 ↓ 日志层 begin_op() / log_write() / end_op() —— 崩溃恢复的原子性保证 ↓ 块缓存层 bread() / bwrite() / brelse() —— 磁盘块的内存缓存与并发同步 ↓ 磁盘 virtio_disk_rw() 路径层不关心数据在磁盘的哪个扇区，块缓存层不关心它缓存的是文件数据还是 inode 元数据。这种分层是\u0026quot;关注点分离\u0026quot;原则的典型体现。\n文件系统对外暴露的统一接口是文件描述符，它通过引用计数实现共享：\n// kernel/file.h — 文件描述符的内核表示 struct file { enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type; // 三种文件类型 int ref; // 引用计数（dup 时增加，close 时减少，归零时释放） char readable; // 是否可读 char writable; // 是否可写 struct pipe *pipe; // FD_PIPE: 管道 struct inode *ip; // FD_INODE / FD_DEVICE: 指向 inode uint off; // FD_INODE: 当前读写偏移 short major; // FD_DEVICE: 设备号 }; 为什么 file 需要引用计数？ 因为 dup 系统调用可以让多个文件描述符指向同一个 file 结构。只有当所有引用都关闭后，才能释放底层资源。这是引用计数模式的典型应用——类似 C++ 的 shared_ptr。\n二、路径解析与 inode 路径解析 (namei) 路径解析是从根目录（或当前目录）开始，逐级查找路径组件的过程。每一步都是\u0026quot;锁定目录 inode → 在目录内容中查找名字 → 获取下一级 inode → 解锁\u0026quot;的循环。这是一个递归下降的过程，类似于编译器中语法分析器对 AST 的遍历。\ninode — 文件的内核表示 inode 是文件系统中最核心的抽象。每个文件有一个 inode，记录类型（普通文件/目录/设备）、大小、链接数、数据块位置。\ninode 有两层表示——磁盘上的持久化版本和内存中的缓存版本：\n// kernel/fs.h — 磁盘上的 inode（永久存储） struct dinode { short type; // 文件类型：0=空闲, 1=目录, 2=文件, 3=设备 short major; // 主设备号（仅设备类型） short minor; // 次设备号（仅设备类型） short nlink; // 硬链接计数（降为 0 时文件可被回收） uint size; // 文件大小（字节） uint addrs[NDIRECT + 1]; // 数据块地址：12 个直接 + 1 个间接 }; // kernel/file.h — 内存中的 inode（缓存版本，额外包含管理字段） struct inode { uint dev; // 设备号 uint inum; // inode 编号（磁盘上的位置） int ref; // 引用计数（有多少指针指向此 inode） struct sleeplock lock; // 保护以下字段（操作可能涉及磁盘 I/O，用睡眠锁） int valid; // 是否已从磁盘读取（0 = 需要 ilock 时重新读入） /* 以下是从磁盘 dinode 复制来的字段 */ short type, major, minor, nlink; uint size; uint addrs[NDIRECT + 1]; }; 两层分离的价值：iget() 通过引用计数管理内存 inode 的生命周期（ref=0 时可回收），ilock() 通过 valid 标志实现延迟加载（首次锁定时才从磁盘读入）。这类似于数据库的 buffer pool 管理——频繁访问的元数据放在内存中，减少磁盘 I/O。\n数据块映射 (bmap) inode 通过 addrs[] 数组指向数据块。前 12 个是直接块（小文件高效，无需间接寻址），第 13 个是一级间接块（指向一个包含 256 个块号的块，支持约 270KB 的文件）。\n这是\u0026quot;用少量元数据寻址大量数据\u0026quot;的基本思路。直接块覆盖小文件（大多数文件都很小），间接块覆盖大文件。工业级文件系统（如 ext4）用多级间接块和 Extent 树支持 TB 级文件。\n从一级间接到多级索引：大文件支持 xv6 的 addrs[12] 只有一级间接块，单个文件最大约 270KB（(12 + 256) × 1024）。要支持大文件，核心思路是增加间接层级：\nxv6 现状： addrs[0..11] → 12 个直接块（12KB） addrs[12] → 一级间接块（256 × 1KB = 256KB） 最大文件 ≈ 268KB 扩展方案（双间接 + 三间接）： addrs[12] → 一级间接块：256 个数据块指针 addrs[13] → 双间接块：256 × 256 = 65536 个数据块指针 addrs[14] → 三间接块：256 × 256 × 256 = 16M 个数据块指针 最大文件 ≈ 16GB（使用 4KB 块时可达 TB 级） bmap 的扩展是递归的：\n// 扩展后的 bmap 伪代码 static uint bmap(struct inode *ip, uint bn) { if (bn \u0026lt; NDIRECT) // 直接块 return ip-\u0026gt;addrs[bn]; bn -= NDIRECT; if (bn \u0026lt; NINDIRECT) // 一级间接 return indirect(ip-\u0026gt;addrs[NDIRECT], bn); bn -= NINDIRECT; if (bn \u0026lt; NINDIRECT * NINDIRECT) // 双间接 return double_indirect(ip-\u0026gt;addrs[NDIRECT+1], bn); bn -= NINDIRECT * NINDIRECT; return triple_indirect(ip-\u0026gt;addrs[NDIRECT+2], bn); // 三间接 } 多级索引的代价：每增加一级间接，就多一次磁盘 I/O 才能获取数据块地址。对于随机读写，三间接块可能需要 4 次 I/O（三间接块本身 → 双间接块 → 单间接块 → 数据块）。这就是为什么 ext4 引入了 Extent 树——用连续块范围（起始块号 + 长度）替代逐块索引，一个 extent 可以描述数 MB 的连续数据，大幅减少元数据 I/O。\nExtent 树的核心思想 传统索引（xv6 风格）： 每个数据块都需要一个指针 → 大文件需要大量间接块 Extent 树（ext4 风格）： 每个 extent 描述一段连续的块范围： struct extent { uint start_block; // 起始物理块号 uint length; // 连续块数 }; 一个 extent 可以覆盖 1MB+ 的数据，只需一个条目 为什么大多数文件很小？ 文件大小的分布是高度偏斜的——绝大多数文件只有几 KB，少数文件达到 GB 级。xv6 的 12 直接块设计正是针对这个分布的优化：小文件（\u0026lt; 12KB）零间接 I/O，只有大文件才需要间接块。这也是为什么 ext4 会将小 extent 直接存在 inode 中（inline extent），避免额外的元数据块。\n三、块缓存 块缓存是磁盘块在内存中的缓存副本，它同时承担两个角色：\n性能缓存：减少慢速磁盘 I/O（内存访问比磁盘快 10 万倍）。 同步点：确保对同一磁盘块的并发访问被序列化（通过 sleep lock）。 // kernel/buf.h — 每个缓存块的结构 struct buf { int valid; // 是否已从磁盘读取 int disk; // 磁盘是否正在使用此 buf（DMA 传输中） uint dev; // 设备号 uint blockno; // 磁盘块号 struct sleeplock lock;// 保护此块的并发访问（用睡眠锁，因为 I/O 可能耗时） uint refcnt; // 引用计数（\u0026gt;0 时不回收） struct buf *prev, *next; // LRU 双向链表指针 uchar data[BSIZE]; // 块数据（1024 字节） }; 缓存使用 LRU（最近最少使用）策略管理：最近使用的放在链表头部，需要回收时从尾部获取。bread() 查找缓存，未命中则从磁盘读取；brelse() 释放缓存并移到 LRU 头部。\n四、日志 — 崩溃恢复的原子性保证 核心矛盾 创建文件需要同时修改 inode 表、数据块位图、目录块。如果写完 inode 表后断电，位图和目录还没更新，文件系统就不一致了——可能有两个文件指向同一块磁盘空间，或者分配了一块\u0026quot;幽灵\u0026quot;块。\n解决方案：Write-Ahead Logging (WAL) 核心思想：先把所有修改写到磁盘的日志区，确认日志完整后再写到真实位置。\n事务流程：\nbegin_op()：标记事务开始，检查日志空间是否足够（不够则睡眠等待）。 文件系统操作中调用 log_write()：标记块为\u0026quot;脏\u0026quot;，暂不写磁盘。同一块多次写入只记录一次（日志吸收）。 end_op()：标记事务结束，如果是最后一个并发事务则触发提交。 提交过程（commit()）： // kernel/log.c static void commit() { if (log.lh.n \u0026gt; 0) { write_log(); // ① 将脏块从块缓存复制到磁盘的日志区 write_head(); // ② 写入日志头 —— 这是\u0026#34;提交点\u0026#34;，事务从此不可逆 install_trans(0); // ③ 将日志块复制到磁盘的真实位置 log.lh.n = 0; // ④ 清空日志 write_head(); // ⑤ 写入空日志头 —— 标记事务完全结束 } } 为什么 write_head() 是提交点？ 因为日志头记录了\u0026quot;本次事务修改了哪些块\u0026quot;。如果崩溃发生在 ② 之前，日志头为空，恢复时忽略——事务被丢弃。如果崩溃发生在 ② 之后 ③ 之前，日志头非空，恢复时重放日志——事务被完成。这把一个多块写入的复杂操作简化为一个\u0026quot;单块写入是否成功\u0026quot;的原子判断。\n崩溃恢复：启动时检查日志头——如果非空（说明上次提交后没来得及写到真实位置），则重放日志；如果为空（说明上次事务正常完成或未开始），则忽略。\n关键洞察：write_head() 是原子性保证的边界——只要日志头没写完整，事务就不算提交，崩溃后丢弃。这把一个多块写入的复杂操作简化为一个\u0026quot;单块写入是否成功\u0026quot;的判断。\n日志吸收：如果同一个块在一次事务中被多次写入，只记录最后一次。这减少了日志空间的使用。\n设计权衡与工业界对比 维度 xv6 的选择 工业界 (Linux/ext4) 的做法 Buffer Cache 固定大小 LRU 链表 + 全局锁 Page Cache + Radix Tree + per-CPU LRU 日志类型 物理 redo log 支持物理/逻辑日志，可配置 journal 模式 文件大小上限 ~270KB（12 直接 + 1 间接） TB 级（Extent 树 + 多级间接） 目录索引 线性扫描 HTree / B-tree 索引 并发 粗粒度锁 per-inode 锁 + RCU 无锁路径查找 文件系统类型 单一简单 FS VFS 抽象层支持 ext4/xfs/btrfs/\u0026hellip; 用户态启发 数据库为何需要自己的 WAL？ 文件系统的日志保证的是文件系统元数据的一致性，不保证用户数据的原子性。数据库需要更细粒度的事务控制（跨多行、跨多表的原子操作），所以必须自己实现 WAL。 随机 I/O 的代价：理解 bmap 有助于理解为什么顺序读写比随机读写快——顺序访问块号连续，可以预读；随机访问每次都要查间接块，且磁盘寻道时间差异巨大。 fsync 的意义：write() 只写到内核缓存，fsync() 才刷到磁盘。不调 fsync，崩溃后可能丢失最近写入。对数据库和日志系统来说，fsync 的时机就是持久性的边界。 为什么 copy-on-write 文件系统（如 btrfs）更安全？ 它们永不覆写数据块，而是写到新位置再原子更新指针。这与日志层的思路异曲同工，但粒度更细，甚至不需要日志区。 源码锚点 kernel/fs.c:namei()（路径解析）、kernel/log.c:commit()（日志提交的五步原子操作）、kernel/bio.c:bread()（块缓存读取）\n内核的编译与构建 从源码到可运行内核：构建流程概述 xv6 的构建过程分为四个阶段，每个阶段的产物是下一阶段的输入。理解这个流程有助于在修改内核代码时知道\u0026quot;改了什么，影响了什么\u0026quot;。\n阶段一：内核编译 kernel/*.S (汇编) + kernel/*.c (C源码) → 分别编译为 kernel/*.o (目标文件) → 由链接脚本 kernel.ld 链接为 kernel/kernel (ELF可执行文件) 汇编文件（entry.S, swtch.S, trampoline.S, kernelvec.S）：处理硬件级别的操作——启动入口、上下文切换、陷入处理、中断向量。 C 文件：内核的全部逻辑——进程管理、内存管理、文件系统、设备驱动、系统调用。 链接脚本 kernel.ld：定义内核在内存中的布局——代码段在前（0x80000000），数据段紧随其后，末尾的 end 符号标记可用物理内存的起点。 阶段二：用户库编译 user/ulib.c, user/printf.c, user/umalloc.c → 编译为 .o 文件 user/usys.pl (Perl脚本) → 自动生成 usys.S (系统调用桩代码) → 编译为 usys.o usys.pl 是一个关键的代码生成器——它为每个系统调用生成一小段汇编桩代码，负责将参数放入约定寄存器并执行 ecall 指令。这样用户程序调用 write() 时，最终会执行这段桩代码陷入内核。\n阶段三：用户程序编译与链接 user/*.c (如 cat.c, sh.c, ls.c) + 用户库 (ULIB) → 分别链接为 user/_cat, user/_sh, user/_ls 等可执行文件 每个用户程序独立链接，使用 user.ld 链接脚本定义用户地址空间布局。\n阶段四：磁盘镜像生成 mkfs/mkfs.c → 编译为 mkfs/mkfs (主机上运行的工具) mkfs/mkfs fs.img README $(UPROGS) → fs.img (文件系统镜像) mkfs 是一个在主机（开发机）上运行的程序，它将用户程序和 README 文件打包为一个初始的文件系统镜像。这个镜像包含完整的文件系统结构——超级块、inode 区域、数据块区域。\n最终产物与运行 # 一键构建并运行 make qemu # 等价于： # 1. 编译内核 → kernel/kernel # 2. 编译用户程序 → user/_* # 3. 生成文件系统镜像 → fs.img # 4. 启动 QEMU 模拟器加载内核和磁盘镜像 QEMU 启动时的配置：-machine virt（RISC-V 虚拟机）、-m 128M（128MB 内存）、-smp 3（3 个 CPU 核心）、-bios none（无 BIOS，直接加载内核）、-drive file=fs.img（磁盘镜像）。\n关键编译选项说明 选项 含义 为什么需要 -mcmodel=medany 代码可在任意地址运行 内核加载在 0x80000000 非零地址 -ffreestanding 不依赖主机运行时环境 内核没有标准库 -fno-builtin-* 禁用编译器内建函数替换 确保调用 xv6 自己的实现 -nostdlib 不链接标准库 内核是独立的运行环境 -fno-omit-frame-pointer 保留帧指针 便于栈回溯调试 -Wall -Werror 严格编译警告 代码质量保证 源码锚点 Makefile（完整构建规则与编译选项）、kernel/kernel.ld（内核链接脚本）、mkfs/mkfs.c（文件系统镜像生成工具）\n阅读建议：本笔记是全局复习的框架，重点在\u0026quot;为什么这样设计\u0026quot;和\u0026quot;提供了什么抽象\u0026quot;。建议结合源码阅读时，先理解每个模块的核心矛盾和设计选择，再深入具体实现。实现细节会随架构和需求变化，但设计原理是通用的。\n","permalink":"https://mem.leabol.top/posts/xv6/xv6%E5%88%86%E6%9E%90%E4%B8%8E%E6%80%BB%E7%BB%93/","summary":"\u003ch2 id=\"系统概述与启动流程\"\u003e系统概述与启动流程\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003e(关注宏观状态流转：从 Machine Mode 到 Supervisor Mode，再到第一个用户进程的诞生)\u003c/em\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch3 id=\"核心概念\"\u003e核心概念\u003c/h3\u003e\n\u003cp\u003e启动流程的本质是\u003cstrong\u003e特权级的逐级让渡\u003c/strong\u003e：从硬件最高特权（Machine Mode）出发，经过层层初始化和降权，最终将控制权交给最不信任的用户程序。整个过程是一条从\u0026quot;全权控制\u0026quot;到\u0026quot;受限执行\u0026quot;的信任链——每一步都在缩小 CPU 的权限范围，同时建立更多的软件抽象层。\u003c/p\u003e","title":"xv6 分析与总结"},{"content":"cpp这门语言与其他语言设计上的不同之处,在于cpp为了高效运行,没有采用在堆上分配对象通过GC管理对象。而是选择了一种更加高效但是更加困难的方式，通过尽量在栈上分配内存。因为栈上分配和释放内存只需要几条cpu指令就能完成，而在栈上则需要调用分配函数这需要更多的时间。而且栈的内存布局更加紧凑，对缓存更加的友好。但是也因为栈的先进先出的特性，这让对象的生命周期更加的难以管理，因为栈帧返回后直接就释放了。而且函数参数的传参也变得复杂，有很多的方式减少传参带来的开销，许多cpp特性就是为了解决这个问题产生的。\ncpp为了管理堆上的对象，使用了一个栈上的局部对象代表或者管理一个堆上的对象。这就是raii，当对象出了作用域，自动调用析构函数释放堆上的资源，这是编译器添加的。但是这样就多了一个问题，该如何跨函数作用域甚至是跨线程传递对象，于是又有了shareptr，move等方法。\n说到智能指针，他们可以指定deleter函数，来控制资源释放的方式，这里可以用很多黑魔法。但是因为deleter函数指指针对象的内部成员，并不是存的函数指针，也就是说不同的deleter函数会导致有不同的内存布局，造成不同的abi，这里一个很坑的地方。另外shareptr其实是cpp的一个有点妥协的设计，cpp的理念就是明确所有权，生命周期可预测，零成本抽象。但是shareptr使对象的生命周期变得不可预测，引入了隐藏成本，破坏了局部推理，必须全局理解才能判断对象行为。所以一定要谨慎使用shareptr。\n学习cpp的过程中要始终牢记：内存布局，生命周期，所有权。\n内存中的值语义 在语法层面上，cpp将内存中的值分为了左值，纯右值，将亡值。左值就是一个有稳定地址的量，将亡值是有稳定地址的值但是即将被移动的值，右值即使没有稳定地址的值，这里的稳定地址是语法层面的可取地址，而不是真的物理地址。\n函数操作的行为：函数的输入参数也就是形参有以下：\nT a ，值拷贝 T\u0026amp; a， 左值引用 const T\u0026amp; a，常量左值引用 T\u0026amp;\u0026amp; a， 右值引用 T a这种形式的形参行为复杂，他可以接受左值也可以接受右值，接受的值的类型如果不一致会尝试隐式转换。1. 当实参是一个左值的时候，他会调用对象的拷贝函数，成本高。2. 如果实参是一个临时对象类型的右值，在17之前通常会被优化成直接构造在形参中，在17之后这点被强制执行了，所以传入的右值几乎没有开销，只有一次构造的开销。如果是一个将亡值类型的右值，则会调用移动构造到形参处。总结：如果要拷贝副本，或者想要移动资源就使用值拷贝的形参\nT\u0026amp; a这种形式的形参，只能传入左值的实参，其他类型会变异报错。所以他的因为较为简单，直接在源数据处操作\nconst T\u0026amp; a， 可以接受左值和右值，是最安全高效的一种形参。当实参为左值时行为和T\u0026amp; a类似，但是不能修改源数据；当实参是右值的时候，他会延长这个右值的生命周期直到引用周期结束为止。如果是将亡值他的的行为和正常的左值引用一样，不会有什么拷贝和移动操作。ps：对于小的可复制平凡类型直接值拷贝比这种形式更高效\nT\u0026amp;\u0026amp; a这种形式的形参只能接受右值实参，他的行为会绑定传入的右值实参，这一点和const T\u0026amp;相同，但是他可以修改移动这个右值，而const T\u0026amp;不行。所以通常使用在移动函数上。如果是模板的话这个则代表完美转发。\n执行层和状态层 执行层： thread， stack， scheduler， coroutine，control flow 状态层：object，heapmemory，ownership，resource，cachestate 程序的执行就是执行层不断操作状态层\n对象和线程 为什么协程困难\n异步为什么破坏直觉\n","permalink":"https://mem.leabol.top/posts/cpp/cpp/","summary":"\u003cp\u003ecpp这门语言与其他语言设计上的不同之处,在于cpp为了高效运行,没有采用在堆上分配对象通过GC管理对象。而是选择了一种更加高效但是更加困难的方式，通过尽量在栈上分配内存。因为栈上分配和释放内存只需要几条cpu指令就能完成，而在栈上则需要调用分配函数这需要更多的时间。而且栈的内存布局更加紧凑，对缓存更加的友好。但是也因为栈的先进先出的特性，这让对象的生命周期更加的难以管理，因为栈帧返回后直接就释放了。而且函数参数的传参也变得复杂，有很多的方式减少传参带来的开销，许多cpp特性就是为了解决这个问题产生的。\u003c/p\u003e","title":"cpp的理解"},{"content":"线程 线程的创建 在cpp中线程的创建有两种形式\n默认构造 传入可调用对象如: 函数, 函数对象, 对象的成员函数, 对象的静态函数, lambda表达式 线程的构造函数会按值复制所有参数,这点很重要, 对于想要传递引用的变量时,要使用std::ref()\n创建的格式如下\nthread(_Callable\u0026amp;\u0026amp; __f, _Args\u0026amp;\u0026amp;... __args) f是可调用对象, args是f的参数, 注意,当传递的是对象的成员函数时, args的第一个参数是对象的指针,原因很简单,想要调用对象的方法,总得知道是哪个对象吧\n线程的返回值 线程的ff会被忽略,所以只能通过\n传入一个参数的引用来保存, 使用std::ref()) 讲返回值保存到对象的成员变量 使用promise对象 其他 线程对象只能移动不能复制\n线程结束需要主线程join()\n关于ref()/cref() 由于cpp中的引用有很多的限制\n不能有引用数组 容器内容不能是引用 不能有指针的引用 不能有引用的引用 但有很多函数需要传递引用, 比如bind, thread等,或者使用stl管理引用对象\n所以通过std::ref()来创建一个std::reference_wrapper\u0026lt;T\u0026gt;对象, 这个对象中封装了一个裸指针, 从而实现了传递引用变量了\n而cref就是const 引用了,\n","permalink":"https://mem.leabol.top/posts/cpp/cpp%E7%BA%BF%E7%A8%8B/","summary":"\u003ch1 id=\"线程\"\u003e线程\u003c/h1\u003e\n\u003ch2 id=\"线程的创建\"\u003e线程的创建\u003c/h2\u003e\n\u003cp\u003e在cpp中线程的创建有两种形式\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e默认构造\u003c/li\u003e\n\u003cli\u003e传入可调用对象如: 函数, 函数对象, 对象的成员函数, 对象的静态函数, lambda表达式\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e线程的构造函数\u003cstrong\u003e会按值复制所有参数\u003c/strong\u003e,这点很重要, 对于想要传递引用的变量时,要使用\u003ccode\u003estd::ref()\u003c/code\u003e\u003c/p\u003e","title":"cpp线程的创建"},{"content":"为ide配置代码转跳 所有的ide或者实现代码转跳,语法检查的功能都是依赖cmake,Makefile等构建脚本中的编译选项的, 因为这些功能的实现需要知道项目的完整构建流程, 包括文件之间的依赖关系, 已经特殊的编译选项如不同的平台.\n可以使用make -nB 来查看编译时都有的编译指令, 但是这些指令太多了, 可以使用bear工具的bear -- make \u0026lt;\u0026gt;帮助我们直接生成一个compile_command.josn的文件, 这里就是整个项目所有的文件的依赖关系, 通常有几千行.\nps: vscode中的c_cpp_properties.json文件是用来告知 VS Code 头文件路径（includePath）、预处理器宏定义（defines）以及编译器路径。其目的是为了让 IntelliSense（代码补全、跳转、波浪线纠错）正常工作。\nsetting.json文件则是用来配置编辑器,如字体,缩进, 格式化等, 分为工作区和全局两个文件\n调试器gdb 通常的linux系统自带的gdb是x86_64编译的, 不能调试riscv架构的程序,\n需要下载gdb-multiarch或者riscv64-unknown-elf-gdb和 riscv64-linux-gnu-gdb\n1.远程gdb调试 在服务端会运行gdbserver,他会监听指定的端口\n在客户端上运行(gdb) target remote localhost:26000就能远程调试gdb\n.gdbinit是一个gdb运行脚本, 不过这个脚本需要在gdb配置中添加到白名单路径\n调试 在 xv6 操作系统中，你观察到的 0x5050505050505050（即二进制的 01010101...）是一个经过刻意设计的魔数（Magic Number）。\n这种做法在系统编程和内核开发中非常普遍，其核心原因可以概括为以下几点：\n1. 显式初始化与确定性 在 C 语言中，局部变量或未显式初始化的内存区域（如堆栈）通常包含“垃圾数据”（上一次运行留下的残余值）。\n目的：xv6 在分配 trapframe 页面时，会使用 memset 或类似的循环将内存填充为特定的模式。 原因：如果寄存器中存在随机数据，调试时很难判断一个错误的值是由于代码逻辑产生的，还是由于读取了未初始化的内存。 2. 易于辨识的视觉特征 0x5050505050505050 在十六进制下的模式非常整齐。\n调试效率：当你在使用 gdb 调试或查看内核 dump 信息时，一排整齐的 0x5050... 能够让你瞬间意识到：该寄存器尚未被赋予有效值，或者 上下文切换（Context Switch）尚未正确加载用户态数据。 3. 陷阱值（Poisoning） 这种填充行为通常被称为 内存着色（Memory Coloring） 或 毒化（Poisoning）。\n地址非法性：在 RISC-V 架构中，0x5050505050505050 通常是一个非法的虚拟地址（不在内核空间也不在当前进程的用户空间范围内）。 触发异常：如果内核代码错误地将这个“默认值”当作指针解引用，程序会立即触发 load/store page fault。这比使用 0 作为默认值更好，因为有时 0 地址（NULL）在特定的逻辑中可能被掩盖，而这种奇特的模式能确保系统在错误发生时立即崩溃，便于定位。 4. 稳定性验证 对于 ecall 的处理流程，内核需要将用户态寄存器保存到 trapframe 中。如果你在 trap 处理程序中看到寄存器依然是 0x5050...，这说明：\ntrampoline.S 中的 save 逻辑可能没有被执行。 或者是 stvec 寄存器指向的地址不正确，导致 CPU 没有跳转到预期的保存逻辑。 git 从其他分支上获取一个文件或文件夹 若要将另一个分支（目标分支）的特定文件复制到当前分支，最直接且标准的方法是使用 git checkout 或 git restore 命令。\n1. 使用 git checkout (传统方法) 这是最常用的命令，它可以将指定分支的文件内容覆盖到当前工作区和暂存区。\ngit checkout \u0026lt;分支名\u0026gt; -- \u0026lt;文件路径\u0026gt; \u0026lt;分支名\u0026gt;: 目标文件所在的分支名称。 \u0026lt;文件路径\u0026gt;: 文件的完整路径。如果是多个文件，用空格隔开；如果是整个目录，指定目录名即可。 示例：\n若要将 develop 分支下的 src/main.py 复制到当前分支：\ngit checkout develop -- src/main.py 2. 使用 git restore (新版本推荐) Git 2.23 版本引入了 restore 命令，专门用于恢复或拉取文件内容，语义比 checkout 更明确。\ngit restore --source \u0026lt;分支名\u0026gt; \u0026lt;文件路径\u0026gt; --source: 指定源引用（分支、提交哈希或 Tag）。 注意: 默认情况下，该操作仅修改工作区。若要同时更新暂存区（index），可以添加 --staged 参数，或者在执行后手动 git add。 3. 操作流程建议 由于上述操作会直接修改当前工作区的文件内容，建议按以下步骤进行以确保安全：\n确认当前状态: 执行 git status 确保当前分支没有未提交的更改。\n执行复制: 使用上述任一命令拉取文件。\n查看差异: 执行 git diff --cached 查看拉取进来的文件内容是否正确。\n提交更改:\ngit add \u0026lt;文件路径\u0026gt; git commit -m \u0026#34;From \u0026lt;分支名\u0026gt;: sync \u0026lt;文件路径\u0026gt;\u0026#34; 4. 特殊情况：仅查看内容而不覆盖 如果你只想查看另一个分支的文件内容，而不希望直接覆盖当前文件，可以使用 show 命令：\ngit show \u0026lt;分支名\u0026gt;:\u0026lt;文件路径\u0026gt; 该命令会将文件内容输出到终端。如果需要另存为新文件，可以使用重定向：\ngit show \u0026lt;分支名\u0026gt;:\u0026lt;文件路径\u0026gt; \u0026gt; \u0026lt;新文件名\u0026gt; 您是否需要了解如何处理因文件路径在不同分支中发生变更而导致的定位问题？\n以下是 Git 中这几个核心指令的常用功能分类。为了保持逻辑清晰，我将它们按照本地分支管理、远程交互和撤销恢复三个维度进行解构：\n1. 本地分支管理 (Local Branching) git branch（分支管理） 主要用于分支的增、删、查、改，但不负责“进入”分支。\ngit branch：列出所有本地分支。 git branch -a：查看所有分支（包括本地和远程追踪分支）。 git branch ：在当前提交点创建一个新分支，但不切换。 git branch -d ：删除已合并的分支。 git branch -D ：强制删除未合并的分支。 git branch -m ：重命名当前分支。 git switch（分支切换 - 现代推荐） 专门用于切换分支，避免了 checkout 的歧义。\ngit switch ：切换到已有分支。 git switch -c ：创建并切换到新分支（相当于 branch + checkout）。 git switch -：快速回到上一个分支。 git checkout（检出 - 传统指令） 功能涵盖了切换分支、切换历史版本。\ngit checkout ：切换分支。 git checkout -b ：创建并切换分支。 git checkout ：将工作区恢复到某个特定的历史提交状态（处于 Detached HEAD 状态）。 2. 远程仓库交互 (Remote Interaction) git remote（远程源管理） 管理本地仓库与远程服务器（如 GitHub/GitLab）的连接。\ngit remote -v：查看当前配置的所有远程仓库地址。 git remote add ：添加一个新的远程仓库（通常命名为 origin）。 git remote set-url ：修改现有远程仓库的 URL。 git remote show ：查看远程仓库的详细分支信息及同步状态。 git push（推送） 将本地仓库的提交上传到远程仓库。\ngit push ：将本地分支推送到远程对应分支。 git push -u ：推送并建立上游追踪关系（Upstream）。 git push --delete ：删除远程服务器上的分支。 git push --force：强制推送（会覆盖远程历史，需谨慎使用）。 3. 撤销与恢复 (Undo \u0026amp; Restore) git restore（恢复 - 现代推荐） 专门用于撤销修改，替代了部分 checkout 和 reset 的功能。\ngit restore ：丢弃工作区的修改，恢复到最后一次提交的状态。 git restore --staged ：将文件从暂存区（Index）撤回工作区（即 unstage 操作）。 git restore -s ：将某个文件恢复到指定提交（source）时的状态。 核心功能对比表 指令 核心操作对象 典型场景 switch 分支指针 (HEAD) 我要在不同的开发任务（分支）间跳转。 branch 分支元数据 我要查看、创建或销毁分支。 checkout 分支/提交/文件 我要切换分支或查看某个旧版本代码。 remote 远程配置 我要修改 GitHub 仓库的关联地址。 restore 物理文件内容 我改乱了代码，想要撤销修改回到干净状态。 push 提交记录 (Commits) 我要把写好的代码同步给同事。 ","permalink":"https://mem.leabol.top/posts/%E5%B7%A5%E5%85%B7/gdb-git/","summary":"\u003ch2 id=\"为ide配置代码转跳\"\u003e为ide配置代码转跳\u003c/h2\u003e\n\u003cp\u003e所有的ide或者实现代码转跳,语法检查的功能都是依赖\u003cstrong\u003ecmake,Makefile\u003c/strong\u003e等构建脚本中的编译选项的, 因为这些功能的实现需要知道项目的完整构建流程, 包括文件之间的依赖关系, 已经特殊的编译选项如不同的平台.\u003c/p\u003e","title":"gdb/git的一些使用"},{"content":"Hash 【C++ 哈希表查找口诀】\n想知道有没有 key？ → 用 count: if (map.count(key))\n想查并用它的 value？ → 用 find: if (map.find(key) != map.end())\n口诀：\ncount 大于 0，就说明有 find 不等于 end，就说明找到了 两数之和 vector\u0026lt;int\u0026gt; twoSum(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { unordered_map\u0026lt;int, int\u0026gt; hashtable; for (int i = 0; i \u0026lt; nums.size(); i++){ auto it = hashtable.find(target - nums[i]);//判断是否存在 if (it != hashtable.end()){ return {i, it-\u0026gt;second}; } hashtable[nums[i]] = i;// 添加元素, 这里的添加顺序不可提前,因为我想做的是先查找,如果先添加会影响这一次查找 } return {}; } ","permalink":"https://mem.leabol.top/posts/dsa/hash/","summary":"\u003ch1 id=\"hash\"\u003eHash\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e【C++ 哈希表查找口诀】\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e想知道有没有 key？\n→ 用 count: \u003ccode\u003e if (map.count(key))\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e想查并用它的 value？\n→ 用 find:  \u003ccode\u003eif (map.find(key) != map.end())\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e口诀：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ecount 大于 0，就说明有\u003c/li\u003e\n\u003cli\u003efind 不等于 end，就说明找到了\u003c/li\u003e\n\u003c/ul\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"两数之和\"\u003e两数之和\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e vector\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e twoSum(vector\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026amp;\u003c/span\u003e nums, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e target) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        unordered_map\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e hashtable;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e nums.size(); i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eauto\u003c/span\u003e it  \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e hashtable.find(target \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e nums[i]);\u003cspan style=\"color:#75715e\"\u003e//判断是否存在\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (it \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e hashtable.end()){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e {i, it\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003esecond};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            hashtable[nums[i]] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e i;\u003cspan style=\"color:#75715e\"\u003e// 添加元素,  这里的添加顺序不可提前,因为我想做的是先查找,如果先添加会影响这一次查找\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e {};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Hash"},{"content":"计算机与物理世界交互的方式，通过各种io设备，能够使用计算机做任何事\n就像人操作各种机器一样，机器提供各种开关和按钮，仪表和指示灯，进料口和出料口。计算机中的io设备将这些物理操作抽象成了寄存器和地址，通过访问或设置不同的寄存器和地址实现和io设备的交互\n但是cpu的运行速度远快于io设备，直接让cpu操作这些设备，就像一个工人在一台反应极慢的设备上死磕，对于cpu利用率很低，所以为了匹配cpu的速度，io设备也需要升级，不能事无巨细的要cpu来做。\n所以很多的IO设备都有设备控制器，就是IO设备上的许多芯片，他们通电之后类似一个简易的cpu，可以自主的执行设备操作，不需要cpu亲自执行。cpu只需要和设备控制器交流，之后只用等待结果即可，cpu就可以做其他事情了。\n那么就有了下面两个问题：\n通信方式：cpu如何与设备进行通信\n内存映射IO：将IO设备上的寄存器或者DRAM的地址映射内存地址空间中，这样cpu可以想访问内存地址一样和设备进行交互 端口映射IO：cpu直接与IO设备通信，使用特殊的cpu指令，设备也可以使用自己的通信协议 数据传输技术：数据在何时，以何种方式传输\n轮询：设备使用一个寄存器表示状态，cpu轮询设备状态，如果是可执行，就处理数据 终端驱动：当数据准备好时，设备发起中断给cpu，cpu执行中断处理程序，处理数据 ","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/io%E8%AE%BE%E5%A4%87/","summary":"\u003cp\u003e计算机与物理世界交互的方式，通过各种io设备，能够使用计算机做任何事\u003c/p\u003e\n\u003cp\u003e就像人操作各种机器一样，机器提供各种开关和按钮，仪表和指示灯，进料口和出料口。计算机中的io设备将这些物理操作抽象成了寄存器和地址，通过访问或设置不同的寄存器和地址实现和io设备的交互\u003c/p\u003e","title":"IO设备"},{"content":"计算机中的硬件, 软件,操作系统都是可以看作一个状态机\n先讲os, os的模型简单说就是一个管理多个软件(状态机)的一个状态机.\nos的每个状态就是当前时间下的所有其他进程的状态, 在单核处理器下, 每次os的状态转移时会随机选择一个程序运行下一步,这就是调度. os的状态机中除了进程外,还有为了进程通信的缓冲区.\n计算机系统中不确定性是有os引入的, os中的进程调度****和系统外部的输入输出的不确定性, 导致了程序的不可预测性\n程序内部也是一个状态机, 状态包括变量,pc.当程序需要结束退出时, 需要借助外界的系统调用\nfork, execve, exit fork会将进程一个字节一个字节的复制一份,类似一个叉子的分叉,他们拥有相同的过去. 他们拥有相同的打开文件,资源. 父进程会返回子进程的id, 子进程返回0. 在os的管理下, 会形成一个树状的父子进程关系, 当父进程先于子进程结束时, 子进程将会交给os的systemd管理视为托孤. 每个进程结束退出时,会向他的父进程发送SIGCHLD信号通知.\nexecve将会一个进程的状态机重置, 然后开始执行execve指定的程序. 这里重置的只是进程内部的状态机, 不会重置进程的外部状态,包括打开的文件和资源等等. 当execve成功执行后, 将不会再执行execve后的代码了\nexit 进程终止时内核会发送SIGCHLD信号给父进程，父进程用wait/waitpid获取状态\n","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/os/","summary":"\u003cp\u003e计算机中的硬件, 软件,操作系统都是可以看作一个\u003cstrong\u003e状态机\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e先讲os, os的模型简单说就是一个管理多个软件(状态机)的一个状态机.\u003c/p\u003e\n\u003cp\u003eos的每个状态就是当前时间下的所有其他进程的状态, 在单核处理器下, 每次os的状态转移时会随机选择一个程序运行下一步,这就是\u003cstrong\u003e调度\u003c/strong\u003e. os的状态机中除了进程外,还有为了进程通信的缓冲区.\u003c/p\u003e","title":"OS"},{"content":" 这是一个关于linux的一种自我的想象, 是我自己为了更好的理解linux而写的一种笔记, 我通过不同的视角,来描述这个程序的世界, 主要的目的不是为了完全掌握或者完整的类比操作系统的所有结构. 我的目的只是在于对os的各个部分有一个更加直观的理解, 这种理解是一种直观的, 表面的理解, 因此他并不能提高自己的linux编程水平, 就像来到一个新的城市, 我可以直观的了解城市的大致样貌, 但是关于城市是如何事无巨细的运行的, 这必须要真实的去体验才能真正了解.\n写这些东西的目的:\n了解程序是如何运行, 程序能做什么, 如何做 了解os的整体结构, 以及他们如何相互工作 不会考虑的地方:\n不会考虑硬件的工作 ","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/%E7%AE%80%E4%BB%8B/","summary":"\u003cblockquote\u003e\n\u003cp\u003e这是一个关于linux的一种自我的想象, 是我自己为了更好的理解linux而写的一种笔记, 我通过不同的视角,来描述这个程序的世界, 主要的目的不是为了完全掌握或者完整的类比操作系统的所有结构. 我的目的只是在于对os的各个部分有一个更加直观的理解, 这种理解是一种直观的, 表面的理解, 因此他并不能提高自己的linux编程水平, 就像来到一个新的城市, 我可以直观的了解城市的大致样貌, 但是关于城市是如何事无巨细的运行的, 这必须要真实的去体验才能真正了解.\u003c/p\u003e","title":"OS简介"},{"content":"RISC-V Trap / Syscall / Trampoline 机制完整解析 一、整体流程概览 1.1 总览 用户态通过 ecall 触发异常 → 硬件最小保存 → 跳转到 stvec → trampoline 完成上下文保存 + 切页表 → 进入内核 → 执行 syscall → 再通过 trampoline 返回用户态。\n1.2 完整流程图（逻辑） 用户态执行 ecall 硬件： 保存 sepc 设置 scause 修改 sstatus 跳转 stvec trampoline (uservec): 使用 sscratch 保存寄存器到 trapframe 切换到内核页表 跳转 usertrap 内核处理 (usertrap): 判断 syscall 执行系统调用 返回 (usertrapret + userret): 恢复寄存器 切回用户页表 sret 返回 二、Trap 发生时的关键问题 2.1 Trap 刚发生时的状态 权限：S-mode 页表：仍是用户页表 寄存器：全部是用户态数据 栈：用户栈（不可用） 2.2 核心矛盾 系统已经进入内核态，但仍运行在用户地址空间中\n三、sscratch 的设计哲学 3.1 问题：如何获取 trapframe？ 要求：\n不能用寄存器（不可信） 不能访问内存（未准备好） 3.2 解决方案：sscratch 内核提前写入 trapframe 地址 用户态无法访问 3.3 原子交换 csrrw a0, sscratch, a0 效果：\na0 = trapframe sscratch = 原 a0 3.4 设计思想 最小信任原则 原子性 零依赖启动 3.5 结论 sscratch 是 trap 入口唯一可信锚点\n四、为什么必须有 trampoline 4.1 核心问题 trap 时：\n页表仍是用户页表 内核代码不可见 4.2 如果直接跳内核 结果：\npage fault 无法执行 4.3 trampoline 的本质 一段在用户页表和内核页表中都映射的代码\n4.4 它解决的三个问题 能执行第一条指令 能保存寄存器 切页表后还能继续执行 4.5 两阶段进入内核 阶段1：trampoline\n保存寄存器 切页表 阶段2：内核函数\n执行逻辑 4.6 类比 trampoline = 安全隔离舱（airlock）\n五、trapframe 的作用 5.1 内容 所有通用寄存器 kernel_sp kernel_satp kernel_trap 5.2 本质 用户上下文 + 内核入口信息\n5.3 作用 保存现场 提供返回依据 六、“一种硬件优化方案”的分析 6.1 方案描述 增加寄存器保存内核页表 trap 自动切页表 直接进入内核 6.2 优点 避免 trampoline 6.3 核心问题 1. 无法确定当前进程 仍需找到：\nkernel stack trapframe 2. 无法保存寄存器 问题仍然存在：\n寄存器污染 3. 硬件复杂度增加 违反 RISC-V 极简原则\n4. 不通用 限制 OS 设计\n6.4 结论 只是转移问题，而不是解决问题\n七、为什么不用 direct map 7.1 direct map 思路 在用户页表中映射内核\n7.2 问题：安全漏洞 Spectre / Meltdown 推测执行可读取内核数据 7.3 KPTI 用户页表不包含内核 内核页表独立 7.4 结果 必须使用 trampoline\n八、RISC-V vs x86 设计对比 特性 x86 RISC-V 自动切栈 ✔ ✘ 自动保存寄存器 ✔ ✘ trap复杂度 高 低 灵活性 低 高 8.1 本质差异 x86：硬件帮你做 RISC-V：软件自己做\n九、操作系统跨架构设计 9.1 是否需要适配？ 必须\n9.2 分层结构 通用层 调度 文件系统 内存管理 架构层 trap 上下文切换 页表 9.3 Linux 结构示例 arch/ x86/ riscv/ arm64/ 十、完整流程总结 10.1 进入内核 ecall 硬件保存最小状态 跳 trampoline 保存寄存器 切页表 进入内核 10.2 返回用户 准备返回 恢复寄存器 切页表 sret 10.3 核心思想 分阶段、安全、最小信任\n十一、核心设计哲学总结 11.1 RISC-V 极简硬件 软件控制 高灵活性 11.2 trampoline 必须存在的安全过渡层\n11.3 sscratch 唯一可信入口锚点\n11.4 OS设计原则 不信任用户态 分阶段执行 最小权限原则 十二、总结 trap = 受控异常 trampoline = 安全桥梁 sscratch = 启动锚点 ","permalink":"https://mem.leabol.top/posts/xv6/risc-v-trap-%E6%9C%BA%E5%88%B6%E5%AE%8C%E6%95%B4%E8%A7%A3%E6%9E%90/","summary":"\u003ch1 id=\"risc-v-trap--syscall--trampoline-机制完整解析\"\u003eRISC-V Trap / Syscall / Trampoline 机制完整解析\u003c/h1\u003e\n\u003chr\u003e\n\u003ch1 id=\"一整体流程概览\"\u003e一、整体流程概览\u003c/h1\u003e\n\u003ch2 id=\"11-总览\"\u003e1.1 总览\u003c/h2\u003e\n\u003cp\u003e用户态通过 \u003ccode\u003eecall\u003c/code\u003e 触发异常 → 硬件最小保存 → 跳转到 \u003ccode\u003estvec\u003c/code\u003e → trampoline 完成上下文保存 + 切页表 → 进入内核 → 执行 syscall → 再通过 trampoline 返回用户态。\u003c/p\u003e","title":"RISC-V trap / syscall / trampoline 机制完整解析"},{"content":"tmux的常用指令(自己配置) prefix Prefix = Ctrl+a（因为我们把默认 Ctrl+b 改了） “Prefix + x”意思是先按 Ctrl+a，松开后再按 x 常用操作 新建窗口 Prefix + c 作用：开一个新窗口，并且继承当前目录 水平/垂直分屏 Prefix + - 作用：上下分屏（vertical split） Prefix + | 作用：左右分屏（horizontal split） 另外你配置里还保留了： Prefix + \u0026quot; Prefix + % 这两个也会在当前目录分屏 面板间移动（Vim 风格） Prefix + h：切到左边 pane Prefix + j：切到下边 pane Prefix + k：切到上边 pane Prefix + l：切到右边 pane 切换窗口 Alt + Left：上一个窗口 Alt + Right：下一个窗口 重命名窗口 Prefix + , 然后输入新名字回车 进入复制模式（Vi） Prefix + Enter：进入 copy-mode 在 copy-mode 里： v：开始选择 y：复制并退出 copy-mode Y：复制到系统剪贴板并退出（前提装了 wl-copy 或 xclip） 重载配置 Prefix + r 作用：不重启 tmux，立即重新加载配置 发送前缀给程序（很少用） Prefix + Ctrl+a 作用：把 Ctrl+a 传给 pane 里的程序（比如你在嵌套 tmux 时会用到） 你这份配置里“自动生效”的项（不需要按键）\n鼠标可直接点选 pane、拖动边界、滚动 窗口编号自动重排（关掉某窗口后编号连续） 历史滚动更长（history-limit 200000） 剪贴板联动打开（set-clipboard on） 状态栏主题和边框颜色已设置 tmux外的常用指令 修改 session 名字 在 tmux 里执行：\ntmux rename-session -t 旧名字 新名字 如果你当前就在要改名的 session，也可以直接：\ntmux rename-session 新名字 快速查看当前有哪些 session：\ntmux ls 启动不同的 session 有三种典型方式：\n新建一个指定名字的 session tmux new -s dev tmux new -s test 连接到已有 session tmux attach -t dev 不确定是否存在时，用“有就连、没有就建”（最推荐） tmux new -As dev 在 tmux 内切换到另一个 session 如果你已经在 tmux 里，不想先退出，可以直接：\ntmux switch-client -t dev ","permalink":"https://mem.leabol.top/posts/%E5%B7%A5%E5%85%B7/tmux/","summary":"\u003ch2 id=\"tmux的常用指令自己配置\"\u003etmux的常用指令(自己配置)\u003c/h2\u003e\n\u003ch3 id=\"prefix\"\u003eprefix\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix = Ctrl+a（因为我们把默认 Ctrl+b 改了）\u003c/li\u003e\n\u003cli\u003e“Prefix + x”意思是先按 Ctrl+a，松开后再按 x\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"常用操作\"\u003e常用操作\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e新建窗口\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix + c\u003c/li\u003e\n\u003cli\u003e作用：开一个新窗口，并且继承当前目录\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003e\u003cstrong\u003e水平/垂直分屏\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix + -\u003c/li\u003e\n\u003cli\u003e作用：上下分屏（vertical split）\u003c/li\u003e\n\u003cli\u003ePrefix + |\u003c/li\u003e\n\u003cli\u003e作用：左右分屏（horizontal split）\u003c/li\u003e\n\u003cli\u003e另外你配置里还保留了：\n\u003cul\u003e\n\u003cli\u003ePrefix + \u0026quot;\u003c/li\u003e\n\u003cli\u003ePrefix + %\n这两个也会在当前目录分屏\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"3\"\u003e\n\u003cli\u003e\u003cstrong\u003e面板间移动（Vim 风格）\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix + h：切到左边 pane\u003c/li\u003e\n\u003cli\u003ePrefix + j：切到下边 pane\u003c/li\u003e\n\u003cli\u003ePrefix + k：切到上边 pane\u003c/li\u003e\n\u003cli\u003ePrefix + l：切到右边 pane\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"4\"\u003e\n\u003cli\u003e\u003cstrong\u003e切换窗口\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003eAlt + Left：上一个窗口\u003c/li\u003e\n\u003cli\u003eAlt + Right：下一个窗口\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"5\"\u003e\n\u003cli\u003e\u003cstrong\u003e重命名窗口\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix + ,\u003c/li\u003e\n\u003cli\u003e然后输入新名字回车\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"6\"\u003e\n\u003cli\u003e\u003cstrong\u003e进入复制模式（Vi）\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix + Enter：进入 copy-mode\u003c/li\u003e\n\u003cli\u003e在 copy-mode 里：\n\u003cul\u003e\n\u003cli\u003ev：开始选择\u003c/li\u003e\n\u003cli\u003ey：复制并退出 copy-mode\u003c/li\u003e\n\u003cli\u003eY：复制到系统剪贴板并退出（前提装了 wl-copy 或 xclip）\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"7\"\u003e\n\u003cli\u003e\u003cstrong\u003e重载配置\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix + r\u003c/li\u003e\n\u003cli\u003e作用：不重启 tmux，立即重新加载配置\u003c/li\u003e\n\u003c/ul\u003e\n\u003col start=\"8\"\u003e\n\u003cli\u003e\u003cstrong\u003e发送前缀给程序（很少用）\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003ePrefix + Ctrl+a\u003c/li\u003e\n\u003cli\u003e作用：把 Ctrl+a 传给 pane 里的程序（比如你在嵌套 tmux 时会用到）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e你这份配置里“自动生效”的项（不需要按键）\u003c/p\u003e","title":"tmux的常用指令"},{"content":"系统调用syscall(ecall) 1.用户态的准备工作 cpu核心在用户线程(进程)上工作, pc(程序计数器寄存器)指向下一个执行的用户态指令(.text), sp(栈指针寄存器)指向当前用户态线程的栈顶,用于分割栈帧和栈帧中变量的定位.\n当pc指向的下一条指令是ecall并开始执行时\n权限提升：由 User Mode (U-Mode) 进入 Supervisor Mode (S-Mode)。\n状态备份：\n将 ecall 指令本身的地址存入 sepc (Supervisor Exception Program Counter)。 将 sstatus 的 SPP (Supervisor Previous Privilege) 位设置为 0（表示来自 U-Mode）。 将 sstatus 的 SPIE 位存入当前中断使能位，并随后关闭中断 (SIE = 0)。 异常原因：将 scause 寄存器设置为 8 (Environment call from U-mode)。\n控制权转移:\n将pc修改成寄存器stvec(trap vec)指向的地址, 这个地址指向trampoline页的uservec入口\n​\ttrampoline是跨页执行的桥梁(用户页表和内核页表)\n2. 为进入内核做准备 cpu核心开始执行pc指向的指令(uservec)\n**原子交换: **将寄存器a0和寄存器sscratch交换值 sscratch预先存入了该进程的trapframe的虚拟地址, trapframe结构体是用来保存从用户态进入内核态前的寄存器状态和进入内核时所需要的页表,中断入口,内核入口等的地址,位于trampline的底下一页.所以 trapframe = 用户上下文 + 内核入口信息\n保存寄存器: 将以a0(已经指向了trapframe)为基址,将所有的通用寄存器(包括和sscratch交换的a0)保存进trapfram结构体中 当所有通用寄存器保存完毕后, 开始加载内核态状态信息\n加载内核信息：从 trapframe 中读取预存的 kernel_satp（内核页表基址）、kernel_sp（内核栈指针）和 kernel_trap（即 usertrap 函数的地址）到指定的寄存器上。\n切换页表：通过 csrw satp, t1 切换页表, t1中是刚才读入的kernel_satp。此时，MMU 开始使用内核页表，由于 trampoline 是等值映射，流水线可以继续执行。\n​\ttrampline和trapframe在用户态和内核态中的虚拟地址都是一样的,由于内核页表通常不包含用户态的数据映射，Trampoline 页必须在用户页表和内核页表中进行等值映射 (Identity Mapping)，以确保切换 satp 寄存器时指令流水线不会崩溃。\n刷新 TLB：执行 sfence.vma 确保后续地址转换使用新的页表。\n跳转：将 pc 改为 kernel_trap 的值，正式进入内核态的 C 函数usertrap。\n3.进入内核态 进入内核态的第一个入口函数usertrap, 会检查一些必要条件,然后决定执行是执行函数\n权限检查: 检查是否是从user mode进入\n中断类型检查：验证 scause 是系统调用, 异常中断,软件中断, 如果是系统调用则进入syscall\np-\u0026gt;trapframe-\u0026gt;epc += 4，使返回后的 PC 指向下一条用户指令。\n开启中断\nsyscall函数：\n从 trapframe-\u0026gt;a7 读取系统调用号。 检查索引是否合法，然后通过**系统调用派发表（syscalls array）**调用对应的实现函数。 参数读取：对应的内核处理函数（如 sys_write）会调用 argint、argaddr 等辅助函数。这些函数实质上是直接去读取保存在 trapframe 中的 a0 到 a5 寄存器的值。 返回值处理：系统调用函数的返回值被写入 p-\u0026gt;trapframe-\u0026gt;a0。 调度检查: 当处理函数返回后,会检查是否有定时器中断, 如果有就进入进程调度处理中 4. 离开内核态 (usertrapret \u0026amp; userret) 准备返回工作\n关闭中断\n存入内核信息: 将 kernel_satp（内核页表基址）、kernel_sp（内核栈指针）和 kernel_trap（即 usertrap 函数的地址), kernel_hartid, 全部存入 trapframe 中, 为了下次进入内核读取\n设置csr寄存器：\n将sstatus的SPP位清零并开启中断在用户态中 将sepc恢复成之前保持的用户态的pc 转跳到trampline.S的userret汇编函数\nuserret (汇编)：\n切换回用户页表。 恢复所有用户通用寄存器。 将 trapframe 地址再次存入 sscratch。 执行 sret：CPU 回到 User Mode，将 pc 设置为 sepc 的值，并打开中断（如果 sstatus 如此配置）。 thinking 我把用户态和内核态理解为两个不同的“虚拟世界”，页表就像一副 VR 眼镜，决定了 CPU 如何解释虚拟地址。\n在 trap 发生时，CPU 虽然切换到了更高权限，但仍然使用原来的用户页表，因此无法直接访问内核代码。\n为了解决这个问题，系统会构造一段特殊的内存区域（trampoline），它在用户页表和内核页表中具有相同的虚拟地址，并映射到同一物理地址。注意trapframe是不同的虚拟地址\n这样，在切换页表的过程中，当前正在执行的代码不会消失，以为pc映射的物理地址依旧能够找到, 从而保证指令流的连续性，使系统能够安全地从用户态过渡到内核态。\n因此，trampoline 的本质不是“入口”，而是一个保证页表切换过程中执行连续性的桥梁。\n|---------------|---|===================| |\tuser\t|\t|\t内核地址\t| |_______________|___|===================| 地址空间的转换 在计算机体系结构与操作系统设计中，**地址空间映射（Address Space Mapping）**是实现进程隔离、内存保护及硬件资源抽象的核心机制。\n1. 映射的本质：解构与重组（The Mechanism） 地址映射并非简单的“数字替换”，而是一套基于**分级索引（Multi-level Indexing）**的逻辑转换。\n空间分割： 虚拟地址（VA）被划分为 VPN（虚拟页号） 和 Offset（页内偏移）。在 RISC-V Sv39 架构中，VPN 进一步拆分为三级索引（9-9-9），这种设计确保了页表本身可以离散地存储在物理内存中，避免了为巨额虚拟空间预留连续物理内存的开销。 属性解耦： 每一个页表项（PTE）不仅记录了物理页号（PPN），还承载了元数据控制位（R/W/X/U/A/D）。这使得同一块物理内存在不同的映射关系下，可以展现出完全不同的访问权限。 2. 映射的策略：直接映射与离散映射（Mapping Strategies） 直接映射（Direct Mapping / Identity Mapping）： 定义： 虚拟地址 VA 线性对应物理地址 PA（通常满足 VA = PA 或 VA = PA + Const）。 专业价值： 主要用于内核启动初始化与高效率内存管理。它允许内核在不经过复杂计算的情况下直接操作物理设备和页表项。 离散映射（Fully Associative Mapping）： 定义： 虚拟地址与物理地址之间不存在代数关系，映射关系完全随机。 专业价值： 这是**用户空间（Userspace）**的基础。它消除了物理内存碎片的影响，通过“虚连实散”的方式，为每个进程提供了一个连续、私有且从零开始的地址空间幻觉。 3. 映射的关键同步：页表切换与原子性（The Context Switch） 地址空间的映射关系是动态切换的，这引入了计算机系统中最精密的同步问题：\n状态连续性（Continuity）： 在切换 satp（根页表寄存器）的瞬间，必须存在一个恒等映射区（Trampoline）。该区域在旧映射与新映射下保持 VA =\u0026gt; PA 的绝对一致，以确保指令流水线中的 PC 指针不会因地址翻译规则的突变而指向非法区域。 TLB 刷新（TLB Flushing）： 由于硬件会缓存映射结果（TLB），在更改映射规则或切换进程后，必须执行 sfence.vma 指令。这在专业上称为地址空间的屏障（Barrier），防止旧映射残留导致的安全漏洞或逻辑错误。 4. 映射的演进：延迟绑定与按需映射（Advanced Semantics） 现代操作系统利用映射机制实现了很多高性能特性：\n写时复制（Copy-on-Write, COW）： 通过将映射标记为只读，捕获硬件异常（Page Fault），实现物理内存延迟分配。 延迟分配（Lazy Allocation）： walk(..., 0) 与 walk(..., 1) 的选择体现了**按需分页（Demand Paging）**的思想。只有当程序真正触碰到地址时，映射关系才会从逻辑定义转变为物理存在。 地址空间布局随机化（ASLR）： 故意破坏映射的规律性，增加攻击者预测内存布局的难度，这是映射机制在安全领域的高级应用。 总结 地址空间映射不仅仅是 VA 到 PA 的转换，它是操作系统对硬件资源的数字化定义。\n专业定义： 地址映射是一种通过硬件（MMU）加速的、由软件（Kernel）定义的空间编址协议。它将碎片化的、有限的物理资源，转化为统一的、受保护的、近乎无限的逻辑资源。\n启动 加载内核代码进入内存 bootloader按照链接脚本把内核代码加载到物理内存的固定位置(0x800000000), 并将pc指针指向entry.S的_entry函数地址\n初始化硬件, 在进入内核前 为每个cpu核心初始化栈 在_entry中,每个核心都会执行一遍,通过每个核心的hartid,计算自己的栈的sp地址.然后跳转到c语言代码strat()中\n最初的c语言 进入C语言, 第一步就是设置mstatus寄存器,用来记录之前的特权等级,把他设置成s mode, 当然这里是骗人的, 这是为了在mret时能够返回到s mode故意设置的,有种编造自己的来历的感觉\n然后再设置寄存器mepc, 把他设置成mret后要跳到哪里去, 这里是跳到main.c的main()\n并且将satp寄存器设置为0,关闭页表的使用,直接使用物理地址,以防万一\n然后将异常和中断都代理给s mode, 并打开s mode下的中断源使能位, 因为中断异常默认走m mode, 这样设置,可以直接让内核处理大部分的中断异常\n在然后给s mode授权物理地址的访问权限,m mode下给了s mode几乎全部的物理内存和RWX权限\n初始化定时器中断 ??\n将每个cpu核心的tp寄存器中写入hartid\n然后mret\n从开机到目前为止,所有核心都是并行的执行代码,初始化自己的寄存器和权限设置的\nm mode也太权威了, 原来内核态的权利都是他给的, 就连访问内存的权限都是他给的\n进入内核代码 现在所有的cpu核心都是以s mode进入内核态,开始执行内核初始化函数,在执行main()函数时,只由一个核心执行,避免出现多核竞争导致内核初始化失败.\n","permalink":"https://mem.leabol.top/posts/xv6/travel_xv6/","summary":"\u003ch2 id=\"系统调用syscallecall\"\u003e系统调用syscall(ecall)\u003c/h2\u003e\n\u003ch3 id=\"1用户态的准备工作\"\u003e1.用户态的准备工作\u003c/h3\u003e\n\u003cp\u003ecpu核心在用户线程(进程)上工作, pc(程序计数器寄存器)指向下一个执行的用户态指令(.text), sp(栈指针寄存器)指向当前用户态线程的栈顶,用于分割栈帧和栈帧中变量的定位.\u003c/p\u003e","title":"travel_xv6"},{"content":"tty 所有终端设备的泛称 pty 伪终端 pts 伪终端从属 terminal 终端模拟器\nterminal,一个输入输出设备, 在现代os中已经抽象成了控制终端**(controlling terminal), 可以由伪终端,虚拟控制台,串口终端,usb串口担任控制终端**. 它可以\n传递信号给进程 默认为输入输出的通道 控制前后台应用的切换 标识身份,有些进程需要确认自己在终端运行 [ 系统中有多个 TTY 设备 ] │ ├── /dev/tty1 (虚拟控制台) ├── /dev/pts/0 (伪终端) ├── /dev/pts/1 (另一个伪终端) ├── /dev/ttyS0 (串口) └── /dev/ttyUSB0 (USB 串口) │ │ [ 进程创建会话 ] │ ▼ [ 会话关联一个控制终端 ] ← 从上面选一个\u0026#34;任命\u0026#34; │ │ 例如：任命 /dev/pts/0 为控制终端 ▼ [ 该会话的前台进程组 ] ← 能通过这个控制终端接收 Ctrl+C 等信号 控制终端 是一个会话（Session）所关联的终端设备，它是进程接收键盘输入、显示输出、以及接收控制信号（Ctrl+C/Ctrl+Z）的\u0026quot;通道\u0026quot;。\n/dev/tty 这个文件是一个\u0026quot;代理\u0026quot;，它本身不是控制终端，而是动态指向控制终端的快捷方式。写入 /dev/tty 的数据是直接写入控制终端设备，绕过 stdout 重定向。但如果控制终端是 PTY，数据会进入内核缓冲队列，由读取它的程序（如 bash）决定最终显示位置\n进程可以打开任意 TTY 设备读写，但这不改变它的控制终端身份。控制终端在会话生命周期内是固定的，不能像切换文件描述符那样随意更换。：唯一\u0026quot;更换\u0026quot;方式是创建新会话（setsid() + 在新终端启动. 没有控制终端的进程，打开 /dev/tty 会失败。\n伪终端是一个虚拟终端,他模仿真实终端与shell交互.为什么要模拟而不是直接交互, 是因为在现代电脑中是使用终端模拟软件交互的, 终端模拟器（xterm/SSH/ttyd）运行在用户空间，无法直接操作物理键盘/屏幕驱动, 但 bash/vim 又期望 isatty() 返回真(是一个真实的物理终端)、能接收 Ctrl+C 信号. 所以内核提供 PTY：Slave 端骗进程,模拟真实终端与进程交互, Master 端让模拟器控制数据流,来模拟用户输入\n当打开一个终端时, 终端模拟器会向系统的ptmx申请一对伪终端:master和slave, 他们是一对文件描述符. 然后终端模拟器与master相连接。 Master 端面向用户（终端模拟器），Slave 端面向程序（Shell）。 数据从 Master 写入后，进入内核的 TTY 缓冲队列，由 内核行规程（Line Discipline） 管理。 在规范模式下，行规程负责处理退格、回显，并拦截特殊字符（如 Ctrl+C 生成 SIGINT 信号）。 处理后的数据输入输入缓冲区交给 Slave 端，进程通过读取 Slave 端获取输入。但是一旦确认是 Ctrl+C，行规程不会把 0x03 放入 Slave 的输入缓冲区。 相反，它直接调用内核的信号发送函数（类似于 kill_pgrp()），向该 Slave 终端关联的前台进程组发送 SIGINT 信号。 进程的输出也会写入 Slave 端，经过行规程处理后，由 Master 端 读出并显示给用户\npts还有一种 原始模式（Raw Mode）,它们会修改行规程配置，关闭“特殊字符处理”和“回显”。，Ctrl+C 不会被行规程拦截，而是作为一个普通字节 0x03 传给进程\n[ 终端模拟器 ] \u0026lt;==读写== [ Master FD ] \u0026lt;==内核缓冲== [ 行规程 (Line Discipline) ] \u0026lt;==读写== [ Slave FD ] \u0026lt;==dup2== [ Bash/Shell ] ^ ^ ^ ^ ^ | | | | | (显示给用户) (伪终端主端) (核心逻辑处理) (伪终端从端) (实际运行程序) ttyd的调用过程\n1. ttyd 调用 openpty() → 获得 Master/Slave FD 2. fork() 创建子进程 3. 子进程调用 setsid() → 创建新会话 4. 子进程打开 Slave FD（/dev/pts/0） 5. 🔑 内核自动：因为这是会话领导进程首次打开终端 → 将该 Slave 设为控制终端 6. 子进程 dup2(slave_fd, STDIN/OUT) + exec(\u0026#34;/bin/bash\u0026#34;) 7. 结果：bash 的控制终端 = /dev/pts/0（伪终端 Slave） ps 可以将伪终端看成是一个中间加了特殊处理程序的管道,就可以想操作pipe那样使用\n另外许多程序会检测他的输入输出设备是终端还是管道,比如ls如果他的输出是终端就会正常显示彩色,如果是管道那么他会将颜色符号删去再输入, 比如vim打开时会检查他的输入是否是终端,只有终端才能打开\n","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/tty/","summary":"\u003cp\u003etty 所有终端设备的泛称  pty 伪终端  pts 伪终端从属  terminal 终端模拟器\u003c/p\u003e\n\u003cp\u003eterminal,一个输入输出设备, 在现代os中已经抽象成了控制终端**(controlling terminal)\u003cstrong\u003e, 可以由伪终端,虚拟控制台,串口终端,usb串口担任\u003c/strong\u003e控制终端**. 它可以\u003c/p\u003e","title":"tty"},{"content":"系统总览 进程管理 内存管理 文件系统 trap 系统调用 驱动 用户函数 ","permalink":"https://mem.leabol.top/posts/xv6/xv6%E6%80%BB%E8%A7%88%E5%9B%BE/","summary":"\u003ch2 id=\"系统总览\"\u003e系统总览\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"00-overview\" loading=\"lazy\" src=\"/images/xv6%E6%80%BB%E8%A7%88/00-overview.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"进程管理\"\u003e进程管理\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"02-process\" loading=\"lazy\" src=\"/images/xv6%E6%80%BB%E8%A7%88/02-process.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"内存管理\"\u003e内存管理\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"03-memory\" loading=\"lazy\" src=\"/images/xv6%E6%80%BB%E8%A7%88/03-memory.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"文件系统\"\u003e文件系统\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"04-filesystem\" loading=\"lazy\" src=\"/images/xv6%E6%80%BB%E8%A7%88/04-filesystem.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"trap\"\u003etrap\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"05-trap\" loading=\"lazy\" src=\"/images/xv6%E6%80%BB%E8%A7%88/05-trap.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"系统调用\"\u003e系统调用\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"06-syscall\" loading=\"lazy\" src=\"/images/xv6%E6%80%BB%E8%A7%88/06-syscall.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"驱动\"\u003e驱动\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"07-devices\" loading=\"lazy\" src=\"/images/xv6%E6%80%BB%E8%A7%88/07-devices.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"用户函数\"\u003e用户函数\u003c/h2\u003e\n\u003cp\u003e\u003cimg alt=\"08-userspace\" loading=\"lazy\" src=\"/images/xv6%E6%80%BB%E8%A7%88/08-userspace.png\"\u003e\u003c/p\u003e","title":"xv6总览图"},{"content":"选择排序 ​\t在i~n-1范围内,找到最小值并放在i的位置上, 然后在i+1 ~ n-1范围内继续\nfor(int i = 0; i \u0026lt; n - 1; i++){ int min = i; for (int j = i + 1; j \u0026lt; n; j++){ if (arr[j] \u0026lt; arr[min]){ min = j; } } swap(arr[i], arr[j]); } 冒泡排序 在0~i范围内, 相邻位置较大的数向下滚动, 最大值最终来到i的位置,然后0 ~ i-1范围继续\nfor(int end = n - 1; end \u0026gt; 0; end--){ for(int i = 0; i \u0026lt; end; i++){ if (arr[i] \u0026gt; arr[i+1]){ swap(arr[i], arr[i+1]); } } } 插入排序 0~i范围内已经有序, 新来的数从右往左滑倒不再小的位置插入,然后继续\nfor(int i = 1; i \u0026lt; n; i++){ for(int j = i; j \u0026gt; 0 \u0026amp;\u0026amp; arr[j] \u0026lt; arr [j-1]; j--){ swap(arr[j], arr[j-1]); } } ","permalink":"https://mem.leabol.top/posts/dsa/%E4%B8%89%E4%B8%AA%E5%9F%BA%E7%A1%80%E6%8E%92%E5%BA%8F/","summary":"\u003ch2 id=\"选择排序\"\u003e选择排序\u003c/h2\u003e\n\u003cp\u003e​\t\u003cstrong\u003e在i~n-1范围内,找到最小值并放在i的位置上, 然后在i+1 ~ n-1范围内继续\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e i \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e; i \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e n \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e; i\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e min \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e i;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e j \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e i \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e; j \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e n; j\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (arr[j] \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e arr[min]){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t\tmin \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e j;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t\t}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\tswap(arr[i], arr[j]);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"冒泡排序\"\u003e冒泡排序\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e在0~i范围内, 相邻位置较大的数向下滚动, 最大值最终来到i的位置,然后0 ~ i-1范围继续\u003c/strong\u003e\u003c/p\u003e","title":"三个基础排序"},{"content":"二分查找 在有序数组中确定num的存在 二分查找的两种写法 // 左闭右开 //更适合找边界、插入位置等复杂问题 int searchInsert(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int left = 0; int right = nums.size();// !! while (left \u0026lt; right) {// !! int mid = left + ((right - left) \u0026gt;\u0026gt; 1);// \u0026gt;\u0026gt;运算的优先级低于+ if (nums[mid] == target) return mid; else if (nums[mid] \u0026lt; target) left = mid + 1;// !! else right = mid;// !! } return left; } // 闭区间 //更适合标准查找、简单理解 int searchInsert(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int left = 0; int right = nums.size() - 1;// !! while (left \u0026lt;= right) {// !! int mid = left + ((right - left) \u0026gt;\u0026gt; 1); if (nums[mid] == target) return mid; else if (nums[mid] \u0026lt; target) left = mid + 1;// !! else right = mid - 1;// !! } return left; } 在有序数组中找到\u0026gt;=num的最左位置 int findLeft(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int left = 0; int right = nums.size() - 1;// !! int ans = -1; while (left \u0026lt;= right) {// !! int mid = left + ((right - left) \u0026gt;\u0026gt; 1); if (nums[mid] \u0026gt;= target) ans = mid; right = mid - 1; else left = mid + 1;// !! } return ans; } 在有序数组中找到\u0026lt;=num的最右位置 int findRight(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int left = 0; int right = nums.size() - 1;// !! int ans = -1; while (left \u0026lt;= right) {// !! int mid = left + ((right - left) \u0026gt;\u0026gt; 1); if (nums[mid] \u0026lt;= target) ans = mid; left = mid + 1; else right = mid - 1;// !! } return ans; } ","permalink":"https://mem.leabol.top/posts/dsa/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/","summary":"\u003ch2 id=\"二分查找\"\u003e二分查找\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e在有序数组中确定num的存在\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 id=\"二分查找的两种写法\"\u003e二分查找的两种写法\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 左闭右开\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e//更适合找边界、插入位置等复杂问题\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esearchInsert\u003c/span\u003e(vector\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026amp;\u003c/span\u003e nums, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e target) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e left \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e right \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e nums.size();\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (left \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e right) {\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e left \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e ((right \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e left) \u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e);\u003cspan style=\"color:#75715e\"\u003e// \u0026gt;\u0026gt;运算的优先级低于+\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nums[mid] \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e target)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e mid;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nums[mid] \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e target)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                left \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                right \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e left;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 闭区间\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e//更适合标准查找、简单理解\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esearchInsert\u003c/span\u003e(vector\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026amp;\u003c/span\u003e nums, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e target) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e left \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e right \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e nums.size() \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (left \u003cspan style=\"color:#f92672\"\u003e\u0026lt;=\u003c/span\u003e right) {\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e left \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e ((right \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e left) \u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nums[mid] \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e target)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e mid;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nums[mid] \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e target)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                left \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                right \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e left;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003col\u003e\n\u003cli\u003e在有序数组中找到\u0026gt;=num的最左位置\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efindLeft\u003c/span\u003e(vector\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026amp;\u003c/span\u003e nums, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e target) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e left \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e right \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e nums.size() \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e    \t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e ans \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (left \u003cspan style=\"color:#f92672\"\u003e\u0026lt;=\u003c/span\u003e right) {\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e left \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e ((right \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e left) \u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nums[mid] \u003cspan style=\"color:#f92672\"\u003e\u0026gt;=\u003c/span\u003e target)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                ans \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                right \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                left \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e ans;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003col\u003e\n\u003cli\u003e在有序数组中找到\u0026lt;=num的最右位置\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efindRight\u003c/span\u003e(vector\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026amp;\u003c/span\u003e nums, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e target) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e left \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e right \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e nums.size() \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e    \t\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e ans \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (left \u003cspan style=\"color:#f92672\"\u003e\u0026lt;=\u003c/span\u003e right) {\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e left \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e ((right \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e left) \u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nums[mid] \u003cspan style=\"color:#f92672\"\u003e\u0026lt;=\u003c/span\u003e target)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                ans \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                left \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                right \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e mid \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e;\u003cspan style=\"color:#75715e\"\u003e// !!\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e ans;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"二分查找"},{"content":"\n","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/%E5%86%85%E6%A0%B8-%E5%AD%98%E5%82%A8/","summary":"\u003cp\u003e\u003cimg alt=\"Linux-storage-stack-diagram_v6.18\" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8-%E5%AD%98%E5%82%A8/Linux-storage-stack-diagram_v6.18.png\"\u003e\u003c/p\u003e","title":"内核存储栈"},{"content":"\n","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/%E5%86%85%E6%A0%B8%E5%9B%BE/","summary":"\u003cp\u003e\u003cimg alt=\"bccBPF Tracing\" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/bccBPF-Tracing.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"benchmark \" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/benchmark.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"BPF \" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/BPF.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"bpftraceeBPF\" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/bpftraceeBPF.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"observability perf-tools\" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/observability-perf-tools.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"observability sar\" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/observability-sar.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"observability tools\" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/observability-tools.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"performance\" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/performance.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"tuning \" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/tuning.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"v2-1ae6a81556274bdf23396628a7784d31_r\" loading=\"lazy\" src=\"/images/%E5%86%85%E6%A0%B8%E5%9B%BE/v2-1ae6a81556274bdf23396628a7784d31_r.jpg\"\u003e\u003c/p\u003e","title":"内核结构图"},{"content":"重要的数据结构 struct sockaddr 通信用的地址表, 记录了使用什么来通信和目的地址\n​\t这是一个在套接字编程中用来表示\u0026quot;地址\u0026quot;的通用结构, 核心作用是为不同的通行提供一个统一的地址表示接口\nstruct sockaddr { sa_family_t sa_family; // 地址族（如 AF_INET, AF_UNIX） char sa_data[14]; // 地址数据（具体含义取决于 sa_family） }; struct sockaddr有很多变体, 适用于不同的通信类型, 但第一项必须是sa_family_t sa_family\n内核通过识别sa_family, 来决定使用何种协议栈处理信息\n常见的struct sockaddr有\n结构体 用途 sa_family struct sockaddr 通用地址结构（抽象基类） AF_UNSPEC,AF_INET, 等 struct sockaddr_in IPv4 地址 + 端口 AF_INET struct sockaddr_in6 IPv6 地址 + 端口 AF_INET6 struct sockaddr_un Unix 域套接字（本地通信） AF_UNIX/AF_LOCAL struct sockaddr_ll 链路层地址（如抓包用） AF_PACKET struct sockaddr_nl Netlink 套接字 AF_NETLINK struct sockaddr_storage 足够大的通用结构，用于存储任意地址 任意 在使用socket编程时, 调用的函数的参数中需要将所有的``socket_xx转换成struct sockaddr`\n","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/%E5%86%85%E6%A0%B8-%E7%BD%91%E7%BB%9C/","summary":"\u003ch2 id=\"重要的数据结构\"\u003e重要的数据结构\u003c/h2\u003e\n\u003ch3 id=\"struct-sockaddr\"\u003estruct sockaddr\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e通信用的地址表, 记录了使用什么来通信和目的地址\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e​\t这是一个在套接字编程中用来表示\u0026quot;地址\u0026quot;的通用结构, 核心作用是为不同的通行提供一个统一的地址表示接口\u003c/p\u003e","title":"内核网络"},{"content":"双指针 快慢指针 删除有序数组中的重复项 一个有序数组nums，原地删除重复出现的元素，使得出现次数超过两次的元素只出现两次 ，返回删除后数组的新长度\nint removeDuplicates(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int n = nums.size(); if (n \u0026lt;= 2) { return n; } int slow = 2; for (int fast = 2; fast \u0026lt; n; ++fast){ if (nums[fast] != nums[slow-2]){// fast 是读指针，不是写指针。我们无法知道前面已经写了多少个相同的值。只有 slow 才能记录我们已经保留了多少个相同的值。 nums[slow++] = nums[fast]; } } return slow; } 移除元素 在数组中,原地移除所有数值等于val的元素\nint removeElement(vector\u0026lt;int\u0026gt;\u0026amp; nums, int val) { int slow = 0;//可写入的下标 int n = nums.size(); for (int fast = 0; fast \u0026lt; n; fast++){ if (nums[fast] != val){ nums[slow++] = nums[fast]; } } return slow; } 相向指针 移除元素 在数组中,原地移除所有数值等于val的元素, 相较于使用快慢指针的方法, 这种方法减少了元素的写入\nint removeElement(vector\u0026lt;int\u0026gt;\u0026amp; nums, int val) { if (nums.size() == 0){ return 0; } int left = 0;//数组判断元素和可写入的下标 int right = nums.size() - 1;//数组末尾的下标 while (left \u0026lt;= right){ if (nums[left] == val){ nums[left] = nums[right--]; }else{ left++; } } return left; } ","permalink":"https://mem.leabol.top/posts/dsa/double_pointer/","summary":"\u003ch1 id=\"双指针\"\u003e双指针\u003c/h1\u003e\n\u003ch2 id=\"快慢指针\"\u003e快慢指针\u003c/h2\u003e\n\u003ch4 id=\"删除有序数组中的重复项\"\u003e删除\u003cstrong\u003e有序\u003c/strong\u003e数组中的重复项\u003c/h4\u003e\n\u003cblockquote\u003e\n\u003cp\u003e一个有序数组nums，原地删除重复出现的元素，使得出现次数超过两次的元素只出现两次 ，返回删除后数组的新长度\u003c/p\u003e\u003c/blockquote\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eremoveDuplicates\u003c/span\u003e(vector\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u0026amp;\u003c/span\u003e nums) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e n \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e nums.size();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (n \u003cspan style=\"color:#f92672\"\u003e\u0026lt;=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e n;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e slow \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e (\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e fast \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e; fast \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e n; \u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003efast){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (nums[fast] \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e nums[slow\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e2\u003c/span\u003e]){\u003cspan style=\"color:#75715e\"\u003e// fast 是读指针，不是写指针。我们无法知道前面已经写了多少个相同的值。只有 slow 才能记录我们已经保留了多少个相同的值。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e                nums[slow\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e nums[fast];\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e slow;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch4 id=\"移除元素\"\u003e移除元素\u003c/h4\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在数组中,原地移除所有数值等于val的元素\u003c/p\u003e","title":"双指针"},{"content":"基数与序数的规律 ​\t对于一个数据, 要先判断是基数还是序数, 如果要计数,还要考虑是否包括边界\n基数: 绝对的,有一个基底数, 日常生活中基底是1; 编程中一般是0; 常见的基数概念有 数量, 个数, 体积, 面积, 时间段, 年龄(周岁), 分数 序数: 相对的, 表示先后顺序, 常见的概念: 位次, 时间点 端点的开闭规律: 全开 ​\t基本的转换规律: 基数 = 序数 - based + 1\n常见场景 从日常的1-based转换成0-based: 访问“第n个元素”时，索引是 n-1 计算从第m到第n之间的数量为 n-m 端点的开闭规律: 若要截取从第 m 到第 n 个元素（1-based） 半开半闭区间, 实际索引范围是 [m-1, n), 个数是n-m 全开区间, 实际索引范围是 (m, n), 个数是n-m-1 全闭区间 , 实际索引范围是 [m-1, n-1], 个数是n-m+1 字符串长度为 len，最后一个字符的索引是 len - 1（因0-based） 一个序数可以加减一个基数, 表示顺序的移动 ","permalink":"https://mem.leabol.top/posts/dsa/%E5%9F%BA%E6%95%B0%E4%B8%8E%E5%BA%8F%E6%95%B0%E7%9A%84%E8%A7%84%E5%BE%8B/","summary":"\u003ch2 id=\"基数与序数的规律\"\u003e基数与序数的规律\u003c/h2\u003e\n\u003cp\u003e​\t\u003cstrong\u003e对于一个数据, 要先判断是基数还是序数, 如果要计数,还要考虑是否包括边界\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e基数: 绝对的,有一个基底数, 日常生活中基底是1; 编程中一般是0; 常见的基数概念有 数量, 个数, 体积, 面积, 时间段, 年龄(周岁), 分数\u003c/li\u003e\n\u003cli\u003e序数: 相对的, 表示先后顺序, 常见的概念: 位次, 时间点\n\u003cul\u003e\n\u003cli\u003e端点的开闭规律: 全开\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e​\t基本的转换规律: \u003ccode\u003e 基数 = 序数 - based + 1\u003c/code\u003e\u003c/p\u003e","title":"基数与序数的规律"},{"content":"堆的核心算法解析 核心要点 这个精简版堆只保留了最核心的两个算法：\n节点索引计算公式（堆的基础） 父节点索引 = (当前索引 - 1) / 2 左子节点索引 = 当前索引 * 2 + 1 右子节点索引 = 当前索引 * 2 + 2 核心算法1: 上浮调整 (heapifyUp) 用途: 插入新元素后维护堆性质 原理: 新元素从末尾开始，与父节点比较，如果优先级更高就向上交换\n插入流程: 1. 将新元素添加到数组末尾 2. 从末尾位置开始向上调整 3. 与父节点比较，满足条件就交换 4. 重复直到堆性质满足或到达根节点 核心算法2: 下沉调整 (heapifyDown) 用途: 删除堆顶后维护堆性质 原理: 从堆顶开始，与子节点比较，让优先级最高的节点上浮\n删除流程: 1. 保存堆顶元素(要返回的值) 2. 用最后一个元素替换堆顶 3. 删除最后一个元素 4. 从堆顶开始向下调整 5. 在父子三个节点中找优先级最高的 6. 如果子节点优先级更高就交换 7. 重复直到堆性质满足 算法可视化 插入过程示例 插入序列: 4, 10, 3, 5, 1 插入4: [4] 插入10: [4] -\u0026gt; [10,4] (10比4大,向上交换) 插入3: [10,4,3] (3比10小,不需要调整) 插入5: [10,4,3,5] -\u0026gt; [10,5,3,4] (5比4大,向上交换) 插入1: [10,5,3,4,1] (1比4小,不需要调整) 删除过程示例 从 [10,5,3,4,1] 开始删除 删除10: [10,5,3,4,1] -\u0026gt; [1,5,3,4] -\u0026gt; [5,4,3,1] (用1替换10,然后下沉调整) 删除5: [5,4,3,1] -\u0026gt; [1,4,3] -\u0026gt; [4,1,3] (用1替换5,然后下沉调整) 关键理解点 堆的本质: 完全二叉树，用数组存储，满足堆序性质 索引关系: 数组下标直接对应树节点位置 调整方向: 插入→上浮(与父节点比较) 删除→下沉(与子节点比较) 时间复杂度: 插入: O(log n) 删除: O(log n) 查看堆顶: O(1) Compare参数说明 greater\u0026lt;T\u0026gt;: 最大堆(大的元素优先级高) less\u0026lt;T\u0026gt;: 最小堆(小的元素优先级高) 这就是堆的全部核心！掌握这两个调整算法，就掌握了堆的精髓。\n","permalink":"https://mem.leabol.top/posts/dsa/heap_core_algorithm/","summary":"\u003ch1 id=\"堆的核心算法解析\"\u003e堆的核心算法解析\u003c/h1\u003e\n\u003ch2 id=\"核心要点\"\u003e核心要点\u003c/h2\u003e\n\u003cp\u003e这个精简版堆只保留了\u003cstrong\u003e最核心的两个算法\u003c/strong\u003e：\u003c/p\u003e\n\u003ch3 id=\"节点索引计算公式堆的基础\"\u003e节点索引计算公式（堆的基础）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-txt\" data-lang=\"txt\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e父节点索引 = (当前索引 - 1) / 2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e左子节点索引 = 当前索引 * 2 + 1  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e右子节点索引 = 当前索引 * 2 + 2\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"核心算法1-上浮调整-heapifyup\"\u003e核心算法1: 上浮调整 (heapifyUp)\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e用途\u003c/strong\u003e: 插入新元素后维护堆性质\n\u003cstrong\u003e原理\u003c/strong\u003e: 新元素从末尾开始，与父节点比较，如果优先级更高就向上交换\u003c/p\u003e","title":"堆的核心算法解析"},{"content":"每个线程会在进程的堆上创建的，栈之间有隔离区。\n栈是默认线程独占的，而堆是共享资源，很多函数会有的缓冲区就在堆中的他是共享的。\n每个线程栈和共享资源都可以看作是一个状态机。\n并发导致了每条指令的执行变得不确定，程序的状态机状态是指数级增加的。\n另外编译器会优化代码，导致理解并发更加困难，虽然可以通过使用汇编或者volatile控制编译器的行为，但是还是要慎重\nmutex解决资源访问的互斥性\n条件变量解决了资源访问的同步性\nvoid produce(){ mutex_lock(\u0026amp;lock); while(!(cond)){ cond_wait(\u0026amp;cv, \u0026amp;lock); } assert(cond); do_something(); cond_broadcast(\u0026amp;cv); mutex_unlock(\u0026amp;lock); } void consumer(){ mutex_lock(\u0026amp;lock); while(!(cond_consumer)){ cond_wait(\u0026amp;cv,\u0026amp;lock); } assert(cond_consumer); consume(); cond_broadcast(\u0026amp;cv); mutex_unlock(\u0026amp;lock); } 对于增加全局计数器的情况，可以通过添加一个局部锁，设定一个临界值，达到临界值后再添加到全局变量中。\n只有真正的临界区才需要加锁，这样既可以提高效率，也能减少出错的可能\n再锁的内部，可以优化返回路径减少检查函数返回的地方，从而减少出错的可能\n","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/%E5%B9%B6%E5%8F%91/","summary":"\u003cp\u003e每个线程会在进程的堆上创建的，栈之间有隔离区。\u003c/p\u003e\n\u003cp\u003e栈是默认线程独占的，而堆是共享资源，很多函数会有的缓冲区就在堆中的他是共享的。\u003c/p\u003e\n\u003cp\u003e每个线程栈和共享资源都可以看作是一个状态机。\u003c/p\u003e","title":"并发"},{"content":" 为了追求极限性能，cpu硬件搞出了独立缓存(L1/L2)和乱序执行，但也留下了“数据不同步”和“顺序错乱”的烂摊子。硬件用MESI协议和内存屏障指令来修补。编程语言为了抹平不同平台(xv6/arm)的差异，制定了内存模型契约，让程序员通过指定内存序(relaxed/acquire/release/sc)来指挥编译器和cpu自动插入屏障。而程序员利用这些契约和底层的硬件CAS指令，最终构建出了无锁算法以及互斥锁等同步原语，为多线程并发提供了正确同步的基础设施。\n摘要：\n因为有 L1/L2 独立缓存（导致多核数据不同步） ➜ 所以硬件设计了 MESI 协议（第 1 层）。\n即使数据同步了，因为 CPU 乱序和写缓冲（导致多变量间顺序错乱） ➜ 所以硬件提供了 内存屏障与原子汇编指令（第 2 层）。\n硬件指令太难用了且平台不统一 ➜ 所以高级语言制定了统一的 高级语言内存模型（第 3 层） 来对齐标准。\n程序员利用语言内存模型 ➜ 组合出了面向变量的 CAS 和无锁数据结构（第 4 层）。\n为了防止无锁自旋榨干 CPU，同时为了管理宏观复杂的业务 ➜ 最终联合操作系统内核开发出了 互斥锁 Mutex（第 5 层）。\n第1层：L1/L2 独立缓存 这一层对于程序透明，用户无法直接操控\n核心痛点：多级独立缓存导致的数据不同步 解决方法：缓存一致性协议，如MESI MESI保证缓存一致性，而非复合操作的原子性。单字读写原子性通常由CPU架构保证。\n第2层：内存屏障与原子汇编指令 核心痛点：即使单个数据同步了，因为 CPU 存在乱序执行和写缓冲区延迟，导致了多变量之间的执行顺序错乱。\n解决方法：硬件内存屏障（Memory Barrier / Fence）和原子指令。\n四种经典屏障：LoadLoad, LoadStore, StoreStore, StoreLoad。 硬件 CAS（Compare-And-Swap）（如 x86 的 LOCK CMPXCHG）。 锁定机制：缓存锁（Cache Locking） vs 总线锁（Bus Locking）。\n第3层：高级语言内存模型与编译器 核心痛点：不同 CPU 架构（x86 和 ARM）的指令完全不统一，而且编译器会代码优化重排。\n解决方法：高级语言联合制定了统一的 高级语言内存模型契约\n硬件内存模型：强内存模型(x86)和弱内存模型(arm/riscv)\n软件内存模型：数据竞争和happens-before\n宽松relaxed：最弱，只保证操作的原子性，不保证顺序 release：写入缓存后会强制刷新本地缓存，保证release前的操作一定发生，但是不会全局同步到其他线程 acquire：读取时会使本地缓存无效再读取，保证之后的操作不会在acquire操作之前执行 顺序一致性seq_cst：最强，每次写入缓存时会暂停并等待所有线程同步完成后再继续执行，每次读取会标记本地缓存无效后再读取，保证所有线程看到的操作的顺序一致 注意：这只是本人抽象出来的模型，不代表具体的硬件是这样执行的\n第4层：原子操作 4.1 语言级原子操作（Atomic Operations） 承上启下：高级语言（C++11/Rust/Java）将第 2 层的硬件原子指令（如 LOCK CMPXCHG）与第 3 层的内存顺序（Memory Order）绑定，封装为面向对象的原子类型（如 std::atomic）。 原子核心操作（软件 CAS）： compare_exchange_strong：绝对可靠的比较并交换，底层对应硬核锁机制。 compare_exchange_weak：允许伪失败（Spurious Failure）的 CAS。在 ARM 等弱内存模型（LL/SC 架构）上，它的循环效率比 strong 更高。 特性：所有原子操作均在用户态（User Space）完成，不发生上下文切换（Context Switch），没有内核开销。 第 5 层：基于原子操作构建同步机制 5.1 Mutex（互斥锁） 核心痛点：第 4 层的自旋锁和无锁结构在竞争极其激烈时，会导致大量线程在用户态疯狂重试、空转，造成严重的 CPU 资源浪费。 解决方法（混合锁机制 Fast-Path / Slow-Path）： Fast-Path（无竞争，用户态解决）：线程调用 mutex.lock() 时，内部先尝试一次轻量级的 CAS。如果此时没有竞争，直接抢锁成功，整个过程完全不惊动操作系统内核。 Slow-Path（有竞争，内核态挂起）：如果 CAS 失败，线程不再自旋。它会通过系统调用（System Call，如 Linux 的 futex）陷入操作系统内核。内核的调度器将该线程的状态改为“阻塞/休眠”，挂入该锁的等待队列，并触发上下文切换，把 CPU 让给其他有用的线程。 唤醒：当锁持有者调用 unlock() 时，再次陷入内核，由内核将等待队列里的休眠线程唤醒，重新放入 CPU 就绪队列。 5.2 RWLock（读写锁） 演进逻辑：在“读多写少”的业务场景下，Mutex 无论是读是写一律互斥，效率太低。 内部机制：内部通常维护两个控制变量：一个记录当前活跃的读者数量（Counter），一个记录是否有写者在等待（Flag）。 行为特征： 读锁（Shared Lock）：利用原子操作自增读者计数（Counter），允许多个线程同时获取读锁。 写锁（Exclusive Lock）：表现退化为传统 Mutex，当计数器不为 0 时必须陷入内核阻塞休眠。 策略抉择：需要处理“读者优先”还是“写者优先”的饥饿问题（防止写者被源源不断的读者无限期卡死）。 5.3 Spinlock（自旋锁） 演进逻辑：当我们需要保护一段极短的临界区，又不想让线程去睡觉时，最直接的方法就是让 CPU 用原子操作进行“死循环硬扛”。 构造原理：利用一个原子的布尔标记。 加锁（Lock）：使用 relaxed 或 acquire 原子操作，在一个死循环里不断执行 CAS，尝试将标记从 false 改为 true。如果失败，CPU 就原地空转。 解锁（Unlock）：使用 release 内存序，直接将标记写回 false。由于使用了 release，可以确保临界区内所有的修改在解锁这一刻全部刷新，对下一个抢到锁的线程可见。 优缺点：响应速度极快（纳秒级），但高并发竞争时会榨干 CPU 算力。 5.4 Lock-Free（无锁）数据结构 演进逻辑：为了彻底消除“锁”带来的线程挂起和死锁问题，程序员利用 CAS + 循环重试（Retry Loop） 来构建复杂的数据结构。 核心代表：无锁栈（Treiber Stack）、无锁队列（Michael-Scott Queue）、原子自增计数器。 运作机制（以无锁入队为例）： 读取当前队列的尾节点指针 tail。 在用户态准备好新节点。 使用 CAS(\u0026amp;tail-\u0026gt;next, nullptr, new_node) 尝试挂载新节点。 如果中途被别的线程捷足先登（CAS 失败），说明有竞争，放弃当前操作，重新读取最新的 tail 并循环重试，直到成功。 无锁陷阱：ABA 问题（一个值从 A 变成 B 又变回 A，CAS 无法察觉）。解决方法通常是引入版本号或使用内存管理黑科技（如 Epoch-based Reclamation, Hazard Pointers）。 5.5 Semaphore（信号量） 演进逻辑：Mutex 只能保护稀缺度为 1 的资源（非黑即白）。如果某种资源的数量是 $N$（如数据库连接池、限流器的并发上限），就需要用到信号量。 内部机制：内部维护一个原子的资源计数器（Permits）和一个内核线程等待队列。 行为特征： P 操作（Acquire/Wait）：尝试原子地将计数器减 1。如果计数器 $\\ge 0$，直接放行；如果计数器 $\u0026lt; 0$，说明资源枯竭，线程通过系统调用进入内核休眠排队。 V 操作（Release/Signal）：原子地将计数器加 1。如果加 1 后计数器仍 $\\le 0$，说明队列里有嗷嗷待哺的休眠线程，内核会负责唤醒队列头部的线程。 应用层：基于同步机制构建并发程序 当熟练掌握了 1 到 5 层的全部原理后，在实际编写上层并发程序（如高性能 Web 服务器、分布式存储引擎）时，思考逻辑将上升到宏观的设计模式：\n临界区最小化原则：无论使用有锁还是无锁，永远让锁/原子操作保护的代码块尽可能短，避免在临界区内做耗时的 I/O 或大内存分配。 避免死锁（Deadlock 防范）：在使用第 5 层的 Mutex/RWLock 时，若涉及多把锁，必须严格规定加锁顺序（锁分级制度），或者使用破坏死锁必要条件的尝试加锁机制（如 try_lock）。 线程间协同协作：结合第 5 层的条件变量（Condition Variable）与 Mutex 共同构建经典的“生产者-消费者”模型。利用条件变量让线程在“等待某个条件成立”时挂起，避免用 while 循环去轮询原子变量。 架构选择偏好： 对于高吞吐、极致响应、竞争不激烈的底层基础库（如内存分配器 jemalloc, 无锁网络框架）：优先采用 4层无锁算法与自旋锁。 对于业务复杂、临界区长、竞争激烈的上层应用（如数据库事务管理、文件读写流）：优先采用 5层互斥锁、读写锁与信号量，用内核调度换取系统的平稳与高资源利用率。 ","permalink":"https://mem.leabol.top/posts/cpp/%E5%B9%B6%E5%8F%91%E6%9E%B6%E6%9E%84%E4%B8%8E%E5%90%8C%E6%AD%A5%E5%8E%9F%E8%AF%AD/","summary":"\u003cblockquote\u003e\n\u003cp\u003e为了追求极限性能，cpu硬件搞出了独立缓存(L1/L2)和乱序执行，但也留下了“数据不同步”和“顺序错乱”的烂摊子。硬件用MESI协议和内存屏障指令来修补。编程语言为了抹平不同平台(xv6/arm)的差异，制定了内存模型契约，让程序员通过指定内存序(relaxed/acquire/release/sc)来指挥编译器和cpu自动插入屏障。而程序员利用这些契约和底层的硬件CAS指令，最终构建出了无锁算法以及互斥锁等同步原语，为多线程并发提供了正确同步的基础设施。\u003c/p\u003e","title":"并发架构与同步原语"},{"content":"循环数组 需要三个变量:头下标l,尾下标r,队列的size\n范围[l, r), size== r-l\n加入元素: 判断size\u0026lt;capacity, 将x放入尾部, 尾部++(如果超过数组边界,返回到数组开头) size++\n弹出元素: 判断size\u0026gt;0, 从头部取出x, 头部++(如果超过数组边界,返回到数组开头) size\u0026ndash;\n提示: 只要size符合判定的要求, 操作一定可以成立\n","permalink":"https://mem.leabol.top/posts/dsa/%E5%BE%AA%E7%8E%AF%E6%95%B0%E7%BB%84/","summary":"\u003ch2 id=\"循环数组\"\u003e循环数组\u003c/h2\u003e\n\u003cp\u003e需要三个变量:\u003cstrong\u003e头下标l,尾下标r,队列的size\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e范围[l, r)\u003c/strong\u003e,  \u003cstrong\u003esize== r-l\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e加入元素: 判断\u003cstrong\u003esize\u0026lt;capacity\u003c/strong\u003e, 将x放入尾部, 尾部++(如果超过数组边界,返回到数组开头) size++\u003c/p\u003e","title":"循环数组"},{"content":"🛠️ 调试记录：xv6 用户程序内存布局冲突异常 1. 问题描述 (Description) 在 xv6 实验环境中，向内核添加自定义系统调用（Syscall）并编写用户态测试程序（Test Case）时，程序在系统调用逻辑执行完毕后，无法正常返回用户态继续执行。表现为进程卡死或触发硬件级页错误（Page Fault），而代码逻辑本身（内核实现）经检查无误。\n2. 发生情况 (Occurrence) 该问题通常发生在以下场景：\n增量编译失效：频繁修改 user/ 下的测试源文件，但 Makefile 未能正确触发 fs.img 的完全重构。 符号偏移冗余：旧的测试程序二进制残留在文件系统镜像中，导致内核 exec 加载的物理地址与当前代码编译出的虚拟地址符号表（Symbols）不匹配。 环境不一致：在 GDB 调试期间，手动修改了用户头文件（如 user.h）却未进行 make clean。 3. 核心特征 (Characteristics) 当该问题发生时，通常伴随以下底层硬件反馈：\n异常代码 (scause)： 0x000000000000000d (Load Page Fault)：尝试读取未映射或权限错误的地址。 0x000000000000000f (Store Page Fault)：在 printf 等操作栈空间时触发写入异常。 异常地址 (sepc \u0026amp; stval)： sepc 指向用户态一个非常小的地址（如 0x154），这通常是用户程序刚从内核返回后的第一条指令位置。 stval 指向一个非法地址（如 0x3048），该地址超出了当前进程分配的 sz（进程大小）范围。 运行表现：内核态的 printf 能正常输出，但返回用户态后立即卡死，且必须通过外部输入（如回车触发中断）才能强制内核抛出 usertrap 报错并终止进程（PID 报错）。 4. 故障原理分析 (Root Cause Analysis) 归途迷失：系统调用通过 sret 返回用户态时，硬件会根据 trapframe-\u0026gt;epc 恢复程序计数器。 地址脱节：由于镜像中的二进制文件版本过旧，该地址对应的内存页可能并未被 exec 正确加载或映射。 陷阱循环：CPU 在用户态执行第一条指令即触发 Page Fault，再次跳回内核。由于此时处于非法状态，内核认为该进程发生了无法修复的错误，遂将其杀掉。 5. 排查与解决方法 (Troubleshooting \u0026amp; Resolution) 第一步：原子化重构（首选方案） 强制删除所有中间产物，确保文件系统镜像（fs.img）与最新的代码同步。\nBash\nr m m m a a k k f e e s . c q i l e m e m g a u n - g d b # # # 彻 清 重 底 理 新 删 对 构 除 象 建 旧 文 并 镜 件 启 像 动 第二步：地址空间校验 使用交叉编译工具链检查用户程序的入口地址：\n# 查看 sepc 指向的地址到底对应哪一行代码 riscv64-unknown-elf-addr2line -e user/_your_test_bin [sepc_address] # 检查 ELF 文件的段布局 riscv64-unknown-elf-readelf -S user/_your_test_bin 第三步：GDB 实时观测 在内核返回用户态的关键点设置断点，观察页表是否正常：\n在 usertrapret 函数末尾设断。 检查 p-\u0026gt;sz（进程大小）是否足以涵盖 stval 报错的地址。 确认 trapframe-\u0026gt;epc 是否指向了预期的用户态指令。 对象池数组与状态残留导致的问题排查 1.问题描述 在做sys_trace时, 出现了能够正常追踪第一个程序,但是后续的程序的trace_mark被污染, 即使没有trace指令也会trace之前的mark\n2.故障特点 下一条命令会继承上一个trace的mark值, 如果上一个执行了mark = 0, 那么下一条命令会正常工作\n3.问题原因 系统的进程是从在一个proc[]的数组上的,为了效率, 创建进程就往数组上添加,但是销毁进程时不会将数组的数据清理,导致如果在创建新进程的时候,如果没有显示的清理原来进程的状态,这个数据就会被下一个数据继承\n4.排查与解决 通过 GDB 检查 proc[] 数组发现，进程销毁后 trace_mark 残留在内存中\n初始化（防止污染）： 在 kernel/proc.c 的 allocproc() 中，找到空闲槽位后立即执行 p-\u0026gt;trace_mask = 0;。这确保了所有新进程（无论是 init 还是 fork 来的）起始状态都是干净的。\n继承（实现功能）： 在 kernel/proc.c 的 fork() 中，加入 np-\u0026gt;trace_mask = p-\u0026gt;trace_mask;。这确保了追踪状态能从父进程（如 trace 程序）传递给目标进程（如 ls）。\n笔记 在文件中,可以不用为每个.c文件配一个.h文件, 可以用一个defs.h,包含来定义其他函数\n文件的分类 核心判别标准不是“我想实现什么功能”，而是**“我正在操作哪种资源”。\n可以把 xv6 的内核源码按“资源所有权”进行如下归类：\n1. 进程管理：proc.c 与 sysproc.c 这是你最近接触最多的部分，它们管理的是执行流（Execution Flow）。\nproc.c (底层实现)： 负责进程的“生老病死”。它直接操作 struct proc 数组、管理调度器（Scheduler）、处理内核栈分配、以及 sleep/wakeup 等底层同步机制。 sysproc.c (系统调用接口)： 它是进程相关系统调用的“门面”。 分类原则： 只要这个系统调用的操作对象是进程本身（如 fork, exit, wait, getpid, sbrk, sleep），就写在这里。 特点： 它通常不直接操作硬件，而是调用 proc.c 里的函数。 2. 文件系统：fs.c 与 sysfile.c 这是管理持久化数据（Persistent Data）**和**设备抽象的地方。\nfs.c (底层实现)： 负责磁盘布局、Inode 的读写、日志（Logging）和目录检索。 sysfile.c (系统调用接口)： 它是文件相关系统调用的入口。 分类原则： 只要操作对象涉及 文件描述符（FD）、文件路径或管道（Pipe）（如 open, read, write, close, dup, link, mkdir），就写在这里。 为什么容易混淆？ 有些调用（如 fstat）看似在获取信息，但因为它通过 FD 索引，所以归类为 sysfile.c。 3. 内存管理：kalloc.c 与 vm.c 这是管理物理空间与虚拟地址转换的地方。\nkalloc.c： 极其单一，只管物理内存页的分配与回收（那个 kmem.freelist 链表）。 vm.c： 负责页表（Page Table）。它决定了虚拟地址如何映射到物理地址，包括内核页表的初始化和用户页表的拷贝（copyin/copyout）。 4. 陷阱与硬件：trap.c 与 trampoline.S 这是管理特权级切换的地方。\ntrap.c： 中转站。当 CPU 发生中断或系统调用从 trampoline 跳回来后，由它决定是去执行 syscall() 还是处理时钟中断。 5. 快速决策表：我该把代码写在哪？ 你可以通过这个思维导图式的规则来判断：\n操作目标 涉及文件 举例 进程属性 (PID, 优先级, 状态) sysproc.c getpid, trace, kill 文件/路径/管道/IO sysfile.c open, pipe, chdir 内存空间大小 sysproc.c sbrk (虽然涉及内存，但它是改变进程空间大小) 底层页表操作 vm.c mappages, uvmcopy 底层物理内存 kalloc.c kalloc, kfree 6. 一个典型的例子：sysinfo 实验 你在做 sysinfo 时可能发现了：\n你需要统计空闲内存 -\u0026gt; 去 kalloc.c 加个函数。 你需要统计进程数 -\u0026gt; 去 proc.c 加个函数。 系统调用入口写在哪？ * 由于 sysinfo 返回的是系统全局状态，并不局限于文件或特定进程。但由于它更偏向于系统资源（类似进程信息），通常放在 sysproc.c 中。 总结建议：\n如果你发现一个系统调用的参数里有 int fd，那它 100% 属于 sysfile.c；如果它的参数是时间、PID 或者只是单纯的数值，那它通常属于 sysproc.c。\n","permalink":"https://mem.leabol.top/posts/xv6/%E8%B0%83%E8%AF%95%E8%AE%B0%E5%BD%95xv6-%E7%94%A8%E6%88%B7%E7%A8%8B%E5%BA%8F%E5%86%85%E5%AD%98%E5%B8%83%E5%B1%80%E5%86%B2%E7%AA%81%E5%BC%82%E5%B8%B8/","summary":"\u003ch2 id=\"-调试记录xv6-用户程序内存布局冲突异常\"\u003e🛠️ 调试记录：xv6 用户程序内存布局冲突异常\u003c/h2\u003e\n\u003ch3 id=\"1-问题描述-description\"\u003e1. 问题描述 (Description)\u003c/h3\u003e\n\u003cp\u003e在 xv6 实验环境中，向内核添加自定义系统调用（Syscall）并编写用户态测试程序（Test Case）时，程序在系统调用逻辑执行完毕后，无法正常返回用户态继续执行。表现为进程卡死或触发硬件级页错误（Page Fault），而代码逻辑本身（内核实现）经检查无误。\u003c/p\u003e","title":"调试记录：xv6 用户程序内存布局冲突异常"},{"content":"不同的进程在各自的房间内运行, 每个房间有自己的编号pid, 进程之间互不干涉, 房间内有许多种与外界通信的方式, 他们有各自的用途和特性, 分布在墙壁上; 房间内还分成多个区域, 分别是: 代码区(.text), 已初始化的全局/静态数据区(.data), 未初始化的全局/静态数据区(.bss), 堆, 栈, 内存映射区, 环境变量与命令行参数区, 内核空间\n内存区域名词解释 代码区(.text) 这是整个房间的执行手册, 只可以执行不可以更改\n存放程序的机器指令的地方 只读 + 可执行 多个进程运行同一程序时，可以共享同一份 .text（节省内存） 由 exec() 加载，从可执行文件（如 ELF）中读取.text 已初始化的全局/静态数据区(.data) 房间的初始化时就存在的数据\n可读可写 程序启动时就分配好空间和初始值 未初始化的全局/静态数据区(.bss) 留给未初始化的全局/静态变量的空间, 初始化房间时一次性清零\n在程序加载时自动清零 不占用可执行文件空间（只记录大小），节省磁盘空间 堆 房间里的“可扩展货架”——按需搭建，用完可拆\n栈 房间里的“工作台”——临时操作，用完即清\n内存映射区 房间的“外接模块区”——插入外部功能模块\n堆和栈之间，或单独区域\n作用：通过 mmap()\n映射以下内容：\n动态链接库（如 libc.so） 文件映射（mmap 文件） 匿名映射（用于大块内存分配） 线程栈（pthread_create 使用） 可读、可写、可执行（取决于映射方式）\n是现代程序加载共享库的主要方式\n环境变量与命令行参数区 房间入口处的“任务说明书”和“环境配置单\n栈的最顶端（启动时） 内核空间（Kernel Space） 房间顶部的“特权控制区”——只有通过“请求孔”才能短暂进入\n交互 通信机制 基于内核的ipc\n管道 匿名管道\n通过系统调用孔，使用pipe()命令, 内核会返回两个文件描述一个用于读数据, 另一个用于写数据; 通常使用一个int fd[2]来存储这两个描述符, 1是写入端, 0是读入端,\n如果系统调用成功, 那么在墙上就出现了两个分别标记这1和0的两个通道口, 这两个通道直接通向内核, 所有的数据都将先到达内核缓冲区,再到达另一端. 除此之外, 这两个通道在同一时间只能操作一个, 只有所有的写端或者读端全部关闭, 才可以打开另一端, 这使用内核来控制的.\n单向通信（半双工） 基于字节流 只能在父子进程间使用 命令行中 |符号的实现基础 系统调用：pipe() 命名管道（FIFO）\n\u0026ldquo;命名管道（FIFO）就像是一个带名字的“公共信箱”，存在于文件系统中。任何知道其路径名的进程都可以通过名字打开它，进行通信。它的通信机制与匿名管道类似：基于字节流、半双工、数据在内核缓冲区中传递。\n与匿名管道不同的是，命名管道有名字、不需要进程间有亲缘关系，并且可以在多个无关进程间共享。\n虽然管道文件本身可以长期存在（直到被删除），但其中的数据仍是临时的，不会持久化存储。\n通过文件系统中的命名节点访问 无亲缘关系的进程也可使用 系统调用：mkfifo() 特点：简单易用，适合少量数据传输\n消息队列 通过系统调用, 在内核中申请一个“带分类邮箱”的邮局系统，每封信（消息）有编号（类型）、内容，收件人可以按类型取信。即使进程关闭, 邮箱也不会消失\n消息的链表结构，存储在内核中 通信过程： 进程A创建消息队列：msgget() 发送消息：msgsnd() 进程B接收消息：msgrcv() 特点： 支持不同类型消息（消息优先级） 允许非阻塞操作 信号 异步通知机制（如 SIGINT、SIGKILL） 发送信号：kill()系统调用 接收处理：注册信号处理函数 signal()或 sigaction() 特点：轻量级但只能传递信号值，不能携带数据 信号量 用于进程间同步（非数据交换） 控制对共享资源的访问（类似锁机制） 操作： P操作（等待/减操作）sem_wait() V操作（释放/加操作）sem_post() 特点：适用于复杂的同步场景 共享内存 最高效的IPC方式 工作机制： 创建共享内存区域：shmget() 映射到进程地址空间：shmat() 直接读写内存 分离映射：shmdt() 关键问题： 需要同步机制（如信号量）避免竞态条件 实现复杂，但有最佳性能 基于文件系统的ipc\n内存映射文件 文件映射到进程的虚拟地址空间 系统调用：mmap() 特点： 文件修改自动同步到内存 适合大文件操作 可在进程间共享 文件锁定 通过 fcntl()或 flock()实现 协作机制： 写时获取排他锁 读时获取共享锁 基于网络的ipc\n套接字 最通用的IPC机制 类型： UNIX域套接字（本地进程间） 网络套接字（跨机器通信） 过程： 服务器：socket()→ bind()→ listen()→ accept() 客户端：socket()→ connect() 特点： 支持TCP（可靠流）和UDP（不可靠数据报） 可用于分布式系统 高级抽象机制的ipc\nrpc 远程过程调用框架 工作流程： 客户端调用本地代理（stub） stub将调用信息序列化并发送 服务器stub反序列化并执行实际函数 结果返回客户端 代表实现： gRPC CORBA Java RMI Dbus 面向桌面环境的IPC系统 用于Linux桌面应用间通信 特点： 基于消息传递 支持对象模型 ","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/%E8%BF%9B%E7%A8%8B/","summary":"\u003cp\u003e不同的进程在各自的房间内运行, 每个房间有自己的编号\u003ccode\u003epid\u003c/code\u003e, 进程之间互不干涉, 房间内有许多种与外界通信的方式, 他们有各自的用途和特性, 分布在墙壁上; 房间内还分成多个区域, 分别是: \u003ccode\u003e代码区(.text)\u003c/code\u003e,  \u003ccode\u003e已初始化的全局/静态数据区(.data)\u003c/code\u003e, \u003ccode\u003e未初始化的全局/静态数据区(.bss)\u003c/code\u003e, \u003ccode\u003e堆\u003c/code\u003e, \u003ccode\u003e栈\u003c/code\u003e, \u003ccode\u003e内存映射区\u003c/code\u003e, \u003ccode\u003e环境变量与命令行参数区\u003c/code\u003e, \u003ccode\u003e 内核空间\u003c/code\u003e\u003c/p\u003e","title":"进程与通信"},{"content":"链表 删除特定节点 ListNode* removeElements(ListNode* head, int val) { ListNode dummy(0, head); ListNode* cur = \u0026amp;dummy; while (cur-\u0026gt;next) {// if (cur-\u0026gt;next-\u0026gt;val == val) { ListNode* tmp = cur-\u0026gt;next; cur-\u0026gt;next = cur-\u0026gt;next-\u0026gt;next;//比条件可以多一个next delete tmp; } else cur = cur-\u0026gt;next; } return dummy.next; } 两两交换相邻节点 ​\t在改变单链表时,优先改变上游的指针\nListNode* swapPairs(ListNode* head) { if (!head || !head-\u0026gt;next) return head; ListNode dummy(0, head); ListNode* cur = \u0026amp;dummy; while (cur-\u0026gt;next \u0026amp;\u0026amp; cur-\u0026gt;next-\u0026gt;next) { ListNode* p1 = cur-\u0026gt;next; ListNode* p2 = cur-\u0026gt;next-\u0026gt;next; cur-\u0026gt;next = p2;//先改头-\u0026gt;p2 cur--\u0026gt;p1--\u0026gt;p2--\u0026gt;other p1-\u0026gt;next = p2-\u0026gt;next;//在改p1-\u0026gt;other p2-\u0026gt;next = p1;//最后在p2-\u0026gt;p1; cur = p1; } return dummy.next; } 合并有序链表 struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode *next) : val(x), next(next) {} }; class Solution { public: ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { // 创建一个哑节点作为新链表的起点 ListNode dummy(0); ListNode* curr = \u0026amp;dummy; // 当前指针用于构建新链表 // 同时遍历两个链表 while (l1 \u0026amp;\u0026amp; l2) { if (l1-\u0026gt;val \u0026lt;= l2-\u0026gt;val) { curr-\u0026gt;next = l1; l1 = l1-\u0026gt;next; } else { curr-\u0026gt;next = l2; l2 = l2-\u0026gt;next; } curr = curr-\u0026gt;next; // 移动当前指针 } // 将剩余部分直接连接到新链表末尾 curr-\u0026gt;next = (l1 != nullptr) ? l1 : l2; // 返回合并后的头节点 return dummy.next; } }; 环形链表 struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; class Solution { public: bool hasCycle(ListNode *head) { // 边界情况：空链表或只有一个节点且无环 if (head == nullptr || head-\u0026gt;next == nullptr) { return false; } // 初始化快慢指针 ListNode *slow = head; ListNode *fast = head; // 快指针及其下一个节点都非空时继续循环 while (fast != nullptr \u0026amp;\u0026amp; fast-\u0026gt;next != nullptr) { slow = slow-\u0026gt;next; // 慢指针每次走一步 fast = fast-\u0026gt;next-\u0026gt;next; // 快指针每次走两步 if (slow == fast) { // 快慢指针相遇，说明存在环 return true; } } // 遍历结束未相遇，说明没有环 return false; } }; 在有序链表中删除相同元素 ​\t在链表的循环中, 判断条件一般是所有指针中最前面的指针\nListNode* deleteDuplicates(ListNode* head) { if (!head) { return head; } ListNode* cur = head; while (cur-\u0026gt;next){ if (cur-\u0026gt;val == cur-\u0026gt;next-\u0026gt;val){ ListNode *tmp = cur-\u0026gt;next; cur-\u0026gt;next = cur-\u0026gt;next-\u0026gt;next; delete tmp; }else{ cur = cur-\u0026gt;next; } } return head; } ","permalink":"https://mem.leabol.top/posts/dsa/link/","summary":"\u003ch1 id=\"链表\"\u003e链表\u003c/h1\u003e\n\u003ch2 id=\"删除特定节点\"\u003e删除特定节点\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eremoveElements\u003c/span\u003e(ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e head, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e val) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ListNode dummy(\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e, head);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e cur \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003edummy;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext) {\u003cspan style=\"color:#75715e\"\u003e//\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eval \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e val) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e tmp \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;\u003cspan style=\"color:#75715e\"\u003e//比条件可以多一个next\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e                \u003cspan style=\"color:#66d9ef\"\u003edelete\u003c/span\u003e tmp;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            } \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                cur \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e dummy.next;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"两两交换相邻节点\"\u003e两两交换相邻节点\u003c/h2\u003e\n\u003cp\u003e​\t在改变单链表时,\u003cstrong\u003e优先改变上游的指针\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eswapPairs\u003c/span\u003e(ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e head) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003ehead \u003cspan style=\"color:#f92672\"\u003e||\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003ehead\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e head;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ListNode dummy(\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e, head);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e cur \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003edummy;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e p1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e p2 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            cur\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e p2;\u003cspan style=\"color:#75715e\"\u003e//先改头-\u0026gt;p2  cur--\u0026gt;p1--\u0026gt;p2--\u0026gt;other\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            p1\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e p2\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;\u003cspan style=\"color:#75715e\"\u003e//在改p1-\u0026gt;other\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            p2\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e p1;\u003cspan style=\"color:#75715e\"\u003e//最后在p2-\u0026gt;p1;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            cur \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e p1;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e dummy.next;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"合并有序链表\"\u003e合并有序链表\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eListNode\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e val;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ListNode \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003enext;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ListNode() \u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e val(\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e), next(\u003cspan style=\"color:#66d9ef\"\u003enullptr\u003c/span\u003e) {}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ListNode(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e x) \u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e val(x), next(\u003cspan style=\"color:#66d9ef\"\u003enullptr\u003c/span\u003e) {}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ListNode(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e x, ListNode \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003enext) \u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e val(x), next(next) {}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eSolution\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e mergeTwoLists(ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e l1, ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e l2) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 创建一个哑节点作为新链表的起点\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        ListNode \u003cspan style=\"color:#a6e22e\"\u003edummy\u003c/span\u003e(\u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ListNode\u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e curr \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003edummy;  \u003cspan style=\"color:#75715e\"\u003e// 当前指针用于构建新链表\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 同时遍历两个链表\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (l1 \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e l2) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (l1\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eval \u003cspan style=\"color:#f92672\"\u003e\u0026lt;=\u003c/span\u003e l2\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003eval) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                curr\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e l1;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                l1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e l1\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            } \u003cspan style=\"color:#66d9ef\"\u003eelse\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                curr\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e l2;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                l2 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e l2\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            curr \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e curr\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;  \u003cspan style=\"color:#75715e\"\u003e// 移动当前指针\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 将剩余部分直接连接到新链表末尾\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        curr\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e (l1 \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enullptr\u003c/span\u003e) \u003cspan style=\"color:#f92672\"\u003e?\u003c/span\u003e l1 : l2;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 返回合并后的头节点\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e dummy.next;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"环形链表\"\u003e环形链表\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eListNode\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e val;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ListNode \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003enext;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ListNode(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e x) \u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e val(x), next(\u003cspan style=\"color:#66d9ef\"\u003enullptr\u003c/span\u003e) {}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eSolution\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ebool\u003c/span\u003e hasCycle(ListNode \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ehead) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 边界情况：空链表或只有一个节点且无环\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (head \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enullptr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e||\u003c/span\u003e head\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enullptr\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e false;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 初始化快慢指针\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        ListNode \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003eslow \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e head;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ListNode \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003efast \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e head;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 快指针及其下一个节点都非空时继续循环\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (fast \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enullptr\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e fast\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enullptr\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            slow \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e slow\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;                  \u003cspan style=\"color:#75715e\"\u003e// 慢指针每次走一步\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e            fast \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e fast\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext\u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003enext;            \u003cspan style=\"color:#75715e\"\u003e// 快指针每次走两步\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (slow \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e fast) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#75715e\"\u003e// 快慢指针相遇，说明存在环\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e                \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e true;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 遍历结束未相遇，说明没有环\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e false;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"在有序链表中删除相同元素\"\u003e在有序链表中删除相同元素\u003c/h2\u003e\n\u003cp\u003e​\t在链表的循环中, 判断条件一般是所有指针中最前面的指针\u003c/p\u003e","title":"链表"},{"content":"现代操作系统的内核（如 Linux 内核）中有一个专门的模块叫做：\n网络协议栈（Network Stack) 主要职责详解 职责 类比说明 技术术语 1. 接收和发送数据包 公司前台接收快递 \u0026amp; 发送包裹 数据链路层、IP 层、传输层 2. IP 地址管理 给每个员工分配邮箱地址 IPv4 / IPv6 地址配置 3. 路由选择 快递要走哪条路线最短最快 路由表（Routing Table） 4. 封装与解封装 包裹加标签 / 拆标签 数据封装（Encapsulation）与解封装（Decapsulation） 5. 传输控制（TCP） 控制文件是否完整送达 TCP 流量控制、拥塞控制 6. 端口号管理 不同部门接收不同类型的快递 端口绑定、监听、转发 7. 安全防护（防火墙） 保安检查包裹内容 Netfilter / iptables / nftables 8. NAT 转换 公司统一出口代理 Network Address Translation 9. 域名解析支持 内部电话簿查询联系方式 DNS 解析缓存、本地 hosts 10. 支持多种协议 公司支持各种沟通方式（电话、邮件、视频会议） 支持 TCP、UDP、ICMP、HTTP、HTTPS、FTP 等 类比图：操作系统网络部门的组织架构 小组 类比角色 职责 套接字接口组（Socket Layer） 客户接待员 接收进程请求（如浏览器访问网页） 传输组（TCP / UDP） 快递打包组 控制可靠传输或快速发送 网络组（IP 层） 邮政分拣中心 决定发往哪个城市（IP 地址） 链路组（MAC 层） 快递站 决定发给哪个局域网内的目标主机 路由组（Routing） 导航调度中心 选择最优路径（下一跳） 设备驱动组（NIC Driver） 快递员 实际把包裹送出去（通过网卡） 安全组（Netfilter / Firewall） 保安检查岗 检查是否允许通行 NAT 组 公司代理出口 统一管理内部员工对外通信 DNS 缓存组 内部电话簿管理员 记录域名与 IP 的对应关系 Linux 的网络部分是一个庞大的子系统，主要包括以下几个关键模块：\n+--------------------------------------+ | 用户空间 | | 应用程序：curl, telnet, 浏览器 | | 使用 socket API 发送和接收数据 | +-----------↓--------------------------+ ↓ +-----------↓--------------------------+ | 系统调用层（Socket Layer） | | 如 sys_socket(), sys_sendto() 等 | +-----------↓--------------------------+ ↓ +-----------↓--------------------------+ | 传输层（Transport Layer） | | TCP / UDP 协议处理 | | 分段、端口号、确认、重传等 | +-----------↓--------------------------+ ↓ +-----------↓--------------------------+ | 网络层（IP 层） | | IP 封装、路由决策、TTL、分片重组 | +-----------↓--------------------------+ ↓ +-----------↓--------------------------+ | 链路层（Data Link Layer） | | MAC 地址封装、ARP 查询、帧封装 | +-----------↓--------------------------+ ↓ +-----------↓--------------------------+ | 设备驱动 + 物理层（NIC Driver） | | 数据帧写入网卡缓冲区 | | 实际通过网线/WiFi 发出 | +-----------↓--------------------------+ ✅ 1. 套接字接口组（Socket Layer） 接收应用程序的请求（如 send(), recv()） 分配并管理端口号（如随机端口 54321） 支持多种协议（TCP、UDP、RAW socket） 将数据传递给传输层（TCP/UDP), 是用户空间与内核空间的桥梁 ✅ 2. 传输组（TCP / UDP） TCP：可靠传输、流量控制、拥塞控制 UDP：快速但不保证送达 ✅ 3. 网络组（IP 层） 处理 IP 地址、TTL（生存时间）\n分片重组（当包太大时）\n查找路由表\n使用 ARP 协议获取目标 MAC\n决定从哪个网卡发出去\n支持多路径负载均衡\n✅ 4. 链路组（MAC 层） 添加 MAC 地址头（源 MAC、目标 MAC） 将传输数据帧传给驱动 ✅ 6. 设备驱动组（NIC Driver） 控制网卡硬件（如 Intel E1000、Realtek RTL8111） DMA 机制：直接内存访问，提高效率 中断通知：数据已发完 / 已收到 ✅ 7. 安全组（Netfilter / Firewall） 匹配规则（iptables/nftables）\n修改、丢弃、转发数据包\n连接状态跟踪（conntrack）\n物理网络 ↓ 设备驱动（接收数据包） ↓ 链路层（验证 MAC 地址） ↓ 网络层（IP 层） ↓ Netfilter PREROUTING（DNAT） ↓ 路由决策（确定目标主机） ↓ Netfilter INPUT（防火墙） ↓ 传输层（TCP/UDP） ↓ 套接字接口（应用程序） ✅ 8. NAT 组（Network Address Translation） 修改源地址和端口 实现内网访问外网 支持端口复用（PAT） 相关工具的使用 socket api 1. lsof - 查看进程打开的网络连接 用途：列出进程打开的文件描述符（包括 Socket 连接）。\n常用命令：\n#查看网络连接 lsof -i\t# 查看所有网络连接 lsof -i -p \u0026lt;PID\u0026gt; # 按 PID 过滤 lsof -i -c nginx # 按进程名过滤（如 nginx） lsof -i :80 # 查看 80 端口的占用 lsof -i :8080-8090 # 查看 8080 到 8090 端口的连接 lsof -i TCP # 仅 TCP 连接 lsof -i UDP # 仅 UDP 连接 #查看打开文件 lsof -p \u0026lt;PID\u0026gt; # 查看进程打开的所有文件（含 Socket） lsof /var/log/nginx.log # 查看谁在访问某个文件 输出关键字段：\nCOMMAND：进程名称。 PID：进程 ID。 USER：运行进程的用户。 TYPE：类型（如 IPv4、IPv6）。 NODE：协议（如 TCP、UDP）。 NAME：连接地址（如 localhost:8080-\u0026gt;1.2.3.4:443）。 2. netstat 或 ss - 网络连接统计 用途：查看系统网络连接、路由表、接口统计等（ss 是更现代的替代工具）。\n常用命令：\n# 查看所有 TCP 连接及关联进程（需要 root 权限） netstat -tulnp # 查看某个进程的网络连接（按 PID） netstat -anp | grep \u0026lt;PID\u0026gt; ss -a # 显示所有连接（包括监听和非监听） ss -tunlp # 常用组合：显示所有 TCP/UDP 监听和连接 #按照状态过滤 ss -t state established # 查看所有已建立的 TCP 连接 ss -t state listening # 查看所有监听的 TCP 端口 ss -t state time-wait # 查看 TIME-WAIT 状态的连接 #按端口或ip过滤 ss -tunlp sport = :80 # 查看源端口为 80 的连接 !!! 等号左右空格不可少 ss -tunlp dport = :443 # 查看目标端口为 443 的连接 !!! 等号左右空格不可少 ss dst 192.168.1.100 # 目标 IP 为 192.168.1.100 的连接 ss src 10.0.0.1 # 源 IP 为 10.0.0.1 的连接 参数说明：\n-t：TCP 连接。 -u：UDP 连接。 -n：不解析域名（直接显示 IP）。 -l：仅监听中的连接。 -p：显示关联进程。 -4/-6：仅 IPv4 或 IPv6。 输出示例:\nN t u e c d t p p i d S E U t S N a T C t A O e B N N R 0 0 e c v - Q S 0 0 e n d - Q L 1 * o 9 : c 2 5 a . 3 l 1 5 6 3 A 8 d . d 1 r . e 1 s 0 s 0 : : P 5 o 4 r 3 t 2 1 P e e 3 r 9 * . : A 1 * d 5 d 6 r . e 6 s 6 s . : 1 P 8 o : r 8 t 0 👉 Rec-Q 和 Send-Q 表示当前队列中的数据大小 👉 Local Address:Port 是你的本地地址和端口 👉 Peer Address:Port 是目标地址和端口 👉 State 表示连接状态（ESTAB = 已连接）\n3. strace - 跟踪进程的 Socket 系统调用 用途：实时跟踪进程的 Socket 相关系统调用（如 socket, bind, connect, send, recv）。\n示例：\n# 跟踪进程的所有网络相关系统调用 strace -e trace=network -p \u0026lt;PID\u0026gt; # 跟踪特定系统调用（如 connect 和 send） strace -e trace=connect,sendto,recvfrom -p \u0026lt;PID\u0026gt; strace -f -e trace=network -p \u0026lt;PID\u0026gt; # 跟踪进程及其子进程的网络调用 strace -e trace=network -p \u0026lt;PID\u0026gt; -o network.log # 输出到文件 # 查看进程与某个 IP 的交互（结合 grep） strace -p \u0026lt;PID\u0026gt; -s 1024 -e trace=network 2\u0026gt;\u0026amp;1 | grep \u0026#34;1.2.3.4\u0026#34; 关键系统调用：\nsocket()：创建 Socket。\nbind()：绑定地址。\nconnect()：发起连接。\nsend()/recv()：发送/接收数据。\naccept()：接受连接。\n** 调试常见网络问题** 场景 1：连接被拒绝 若 connect 返回 ECONNREFUSED，表示目标端口未监听：\nconnect(3, {sa_family=AF_INET, ...}, 16) = -1 ECONNREFUSED (Connection refused) 场景 2：DNS 解析失败 若 getaddrinfo 失败，可能是域名解析问题：\nsocket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 3 sendto(3, \u0026#34;example.com\u0026#34;, ...) = 32 recvfrom(3, 0x7ffd..., 1024, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable) 场景 3：数据传输异常 通过 sendto 和 recvfrom 的返回值，判断是否发送/接收了预期大小的数据：\nsendto(3, \u0026#34;payload\u0026#34;, 1024, 0, NULL, 0) = 512 # 实际发送 512 字节（可能被截断） recvfrom(3, buffer, 4096, 0, NULL, NULL) = -1 EAGAIN # 非阻塞模式下无数据可读 使用场景总结 快速定位端口占用： lsof -i :80 或 ss -tulnp | grep 80。 查看进程的实时网络调用： strace -e trace=network -p \u0026lt;PID\u0026gt; 。 传输层 同上ss 网络层 1. ip addr（IP 地址管理） 作用: 查看、添加或删除网络接口的 IP 地址（替代传统的 ifconfig）。\n常用命令\n命令 作用 ip addr show 查看所有接口的 IP 地址 ip addr add 192.168.1.100/24 dev eth0 给 eth0 添加 IP ip addr del 192.168.1.100/24 dev eth0 删除 eth0 的 IP 输出解析 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether 00:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff inet 192.168.1.100/24 brd 192.168.1.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::211:22ff:fe33:4455/64 scope link valid_lft forever preferred_lft forever 1. 第一行：接口状态与基本属性 2: eth0：2 是内核分配的接口索引号。eth0 是接口名称（通常是以太网卡）。 \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt;： BROADCAST：支持广播通信。 MULTICAST：支持组播。 UP：接口已启用。 LOWER_UP：物理链路已连接（如网线插好）。 mtu 1500：最大传输单元（MTU）为 1500 字节（标准以太网值）。 qdisc fq_codel：使用的队列算法为 fq_codel（公平队列+流量控制）。 state UP：接口处于活动状态。 group default：接口属于默认组。 qlen 1000：传输队列长度为 1000 个数据包。 2. 第二行：MAC 地址 link/ether 00:11:22:33:44:55：接口的 MAC 地址（物理地址）。 brd ff:ff:ff:ff:ff:ff：广播 MAC 地址（所有位为 FF 表示广播帧）。 3. 第三行：IPv4 地址配置 inet 192.168.1.100/24： IPv4 地址为 192.168.1.100。 子网掩码为 /24（即 255.255.255.0）。 brd 192.168.1.255：广播地址 scope global：地址作用域为全局（可跨子网通信）。 eth0：地址绑定的接口名称。 附加行：IPv4 地址有效期 valid_lft forever：地址永久有效（无过期时间）。 preferred_lft forever：地址永久优先（无临时降级）。 2. ip route（路由管理） 作用: 管理 路由表（决定数据包如何转发）。\n常用命令\n命令 作用 ip route show 查看当前路由表 ip route add 10.0.0.0/24 via 192.168.1.1 dev eth0 添加静态路由 ip route add default via 192.168.1.1 设置默认网关 ip route del 10.0.0.0/24 删除路由 输出解析 default via 192.168.1.1 dev eth0 proto static metric 100 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 10.0.0.0/24 via 192.168.1.1 dev eth0 default via 192.168.1.1：默认网关（所有非本地流量走 192.168.1.1）。 192.168.1.0/24 dev eth0：本地子网，直接通过 eth0 通信。 10.0.0.0/24 via 192.168.1.1：静态路由，访问 10.0.0.0/24 的流量走 192.168.1.1。 dev eth0: 数据包出口的网络接口（网卡） proto static: 路由来源：static（手动配置）、kernel（内核自动生成）、dhcp 等 metric 100: 路由优先级（值越小优先级越高） scope link: 局域网通信 4. ip neigh（ARP 缓存管理） 作用: 查看和管理 ARP 缓存表（IP 和 MAC 地址的映射）。\n常用命令\nip neigh show\t#查看 ARP 表 输出解析\n192.168.1.1 dev eth0 lladdr 00:11:22:33:44:55 REACHABLE 192.168.1.100 dev eth0 lladdr aa:bb:cc:dd:ee:ff STALE lladdr 00:11:22:33:44:55**：对应的 MAC 地址。 REACHABLE：ARP 条目有效（STALE 表示可能过期）。 5. traceroute（路由追踪） 作用: 显示 数据包从本机到目标主机的路径（经过哪些路由器）。\n常用命令 : traceroute \u0026lt;ip\u0026gt;\n输出解析\ntraceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets 1 192.168.1.1 (192.168.1.1) 1.234 ms 1.123 ms 1.456 ms 2 10.0.0.1 (10.0.0.1) 5.678 ms 6.789 ms 7.123 ms 3 203.0.113.1 (203.0.113.1) 10.111 ms 11.222 ms 12.333 ms 4 8.8.8.8 (8.8.8.8) 15.444 ms 16.555 ms 17.666 ms 30 hops max：最多追踪 30 跳（防止无限循环）。 ","permalink":"https://mem.leabol.top/posts/%E5%86%85%E6%A0%B8/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E6%A0%88/","summary":"\u003cp\u003e现代操作系统的内核（如 Linux 内核）中有一个专门的模块叫做：\u003c/p\u003e\n\u003ch1 id=\"网络协议栈network-stack\"\u003e网络协议栈（Network Stack)\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E6%A0%88/image-20251012194550481.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"主要职责详解\"\u003e主要职责详解\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e职责\u003c/th\u003e\n          \u003cth style=\"text-align: left\"\u003e类比说明\u003c/th\u003e\n          \u003cth\u003e技术术语\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e1. 接收和发送数据包\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e公司前台接收快递 \u0026amp; 发送包裹\u003c/td\u003e\n          \u003ctd\u003e数据链路层、IP 层、传输层\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e2. IP 地址管理\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e给每个员工分配邮箱地址\u003c/td\u003e\n          \u003ctd\u003eIPv4 / IPv6 地址配置\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e3. 路由选择\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e快递要走哪条路线最短最快\u003c/td\u003e\n          \u003ctd\u003e路由表（Routing Table）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e4. 封装与解封装\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e包裹加标签 / 拆标签\u003c/td\u003e\n          \u003ctd\u003e数据封装（Encapsulation）与解封装（Decapsulation）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e5. 传输控制（TCP）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e控制文件是否完整送达\u003c/td\u003e\n          \u003ctd\u003eTCP 流量控制、拥塞控制\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e6. 端口号管理\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e不同部门接收不同类型的快递\u003c/td\u003e\n          \u003ctd\u003e端口绑定、监听、转发\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e7. 安全防护（防火墙）\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e保安检查包裹内容\u003c/td\u003e\n          \u003ctd\u003eNetfilter / iptables / nftables\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e8. NAT 转换\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e公司统一出口代理\u003c/td\u003e\n          \u003ctd\u003eNetwork Address Translation\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e9. 域名解析支持\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e内部电话簿查询联系方式\u003c/td\u003e\n          \u003ctd\u003eDNS 解析缓存、本地 hosts\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e10. 支持多种协议\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd style=\"text-align: left\"\u003e公司支持各种沟通方式（电话、邮件、视频会议）\u003c/td\u003e\n          \u003ctd\u003e支持 TCP、UDP、ICMP、HTTP、HTTPS、FTP 等\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"类比图操作系统网络部门的组织架构\"\u003e类比图：操作系统网络部门的组织架构\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e小组\u003c/th\u003e\n          \u003cth\u003e类比角色\u003c/th\u003e\n          \u003cth\u003e职责\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e套接字接口组（Socket Layer）\u003c/td\u003e\n          \u003ctd\u003e客户接待员\u003c/td\u003e\n          \u003ctd\u003e接收进程请求（如浏览器访问网页）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e传输组（TCP / UDP）\u003c/td\u003e\n          \u003ctd\u003e快递打包组\u003c/td\u003e\n          \u003ctd\u003e控制可靠传输或快速发送\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e网络组（IP 层）\u003c/td\u003e\n          \u003ctd\u003e邮政分拣中心\u003c/td\u003e\n          \u003ctd\u003e决定发往哪个城市（IP 地址）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e链路组（MAC 层）\u003c/td\u003e\n          \u003ctd\u003e快递站\u003c/td\u003e\n          \u003ctd\u003e决定发给哪个局域网内的目标主机\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e路由组（Routing）\u003c/td\u003e\n          \u003ctd\u003e导航调度中心\u003c/td\u003e\n          \u003ctd\u003e选择最优路径（下一跳）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e设备驱动组（NIC Driver）\u003c/td\u003e\n          \u003ctd\u003e快递员\u003c/td\u003e\n          \u003ctd\u003e实际把包裹送出去（通过网卡）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e安全组（Netfilter / Firewall）\u003c/td\u003e\n          \u003ctd\u003e保安检查岗\u003c/td\u003e\n          \u003ctd\u003e检查是否允许通行\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eNAT 组\u003c/td\u003e\n          \u003ctd\u003e公司代理出口\u003c/td\u003e\n          \u003ctd\u003e统一管理内部员工对外通信\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eDNS 缓存组\u003c/td\u003e\n          \u003ctd\u003e内部电话簿管理员\u003c/td\u003e\n          \u003ctd\u003e记录域名与 IP 的对应关系\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eLinux 的网络部分是一个庞大的子系统，主要包括以下几个关键模块：\u003c/p\u003e","title":"网络协议栈"},{"content":"为什么叫补码(Two\u0026rsquo;s complement) 计算机的加减法运算天生是一个模2^N的同余类上的运算，满2^N会抛弃进位，那表示有符号数时使用同余类代表很正常吧，比如-1 = 2^N -1 (mod 2^N)，-2 = 2^N-2 (mod 2^N)，这实际上就是补码了。\n","permalink":"https://mem.leabol.top/posts/%E7%AE%97%E6%B3%95/%E5%8E%9F%E7%A0%81-%E5%8F%8D%E7%A0%81%E5%92%8C%E8%A1%A5%E7%A0%81/","summary":"\u003ch3 id=\"为什么叫补码twos-complement\"\u003e为什么叫补码(Two\u0026rsquo;s complement)\u003c/h3\u003e\n\u003cp\u003e计算机的加减法运算天生是一个\u003ca href=\"https://zhida.zhihu.com/search?content_id=190505781\u0026amp;content_type=Answer\u0026amp;match_order=1\u0026amp;q=%E6%A8%A12%5EN\u0026amp;zhida_source=entity\"\u003e模2^N\u003c/a\u003e的\u003ca href=\"https://zhida.zhihu.com/search?content_id=190505781\u0026amp;content_type=Answer\u0026amp;match_order=1\u0026amp;q=%E5%90%8C%E4%BD%99%E7%B1%BB\u0026amp;zhida_source=entity\"\u003e同余类\u003c/a\u003e上的运算，满2^N会抛弃进位，那表示有符号数时使用同余类代表很正常吧，比如-1 = 2^N -1 (mod 2^N)，-2 = 2^N-2 (mod 2^N)，这实际上就是\u003ca href=\"https://zhida.zhihu.com/search?content_id=190505781\u0026amp;content_type=Answer\u0026amp;match_order=1\u0026amp;q=%E8%A1%A5%E7%A0%81\u0026amp;zhida_source=entity\"\u003e补码\u003c/a\u003e了。\u003c/p\u003e","title":"为什么叫补码"},{"content":"关于位图存储与数组下标存储的探讨 最近在看《编程珠玑》，书中的第一章提到了位图。位图存储使用二进制位的位数信息，来表示数字。例如，存储1、3、4、6、8，这五个数字， 可以使用\u0026lt;1, 0, 1, 1, 0, 1, 0, 1\u0026gt;这样的向量来表示，这样可以大大减少存储空间， 只用了一个字节（8bit），就可以存储原来需要的8个数字才能表示的信息。\n当我跟朋友谈到这个方法时，他却产生了质疑，他觉得这样不如直接按照数组下标存储来的快。他认为数组下标直接存储，直接一步完成，二位图则需要每次位运算寻址，会非常费时间。于是我们就决定写一个程序，实际跑一下看看。\n这是位图的代码\ntypedef struct { uint32_t *bits; size_t size; } Bitmap; Bitmap *create_bitmap(size_t max_num) { size_t num_words = (max_num + 31) / 32; // 计算需要多少个32位字 Bitmap *bm = malloc(sizeof(Bitmap)); if (!bm) { perror(\u0026#34;Bitmap allocation failed\u0026#34;); exit(EXIT_FAILURE); } bm-\u0026gt;bits = calloc(num_words, sizeof(uint32_t)); // 分配约 1亿位 ≈ 12.5MB bm-\u0026gt;size = max_num; if (!bm-\u0026gt;bits) { perror(\u0026#34;Bitmap bits allocation failed\u0026#34;); exit(EXIT_FAILURE); } return bm; } void bitmap_set(Bitmap *bm, int num) { size_t word_idx = num / 32; size_t bit_idx = num % 32; bm-\u0026gt;bits[word_idx] |= (1U \u0026lt;\u0026lt; bit_idx); } int bitmap_get(const Bitmap *bm, int num) { size_t word_idx = num / 32; size_t bit_idx = num % 32; return (bm-\u0026gt;bits[word_idx] \u0026gt;\u0026gt; bit_idx) \u0026amp; 1U; } 位运算把每个字节大小的内存当作为一组，使用除法判断组数， 再用对8取余，得出每组的位次\n下面是数组的代码\n#define N 10000000 // 处理 1000 万个整数 #define MAX_NUM 100000000 // 数字范围：0 ~ 1亿 void array_set(int *arr, int num) { arr[num] = 1; } int array_get(const int *arr, int num) { return arr[num]; } // 初始化数组（全部置0） int *create_array() { int *arr = calloc(MAX_NUM, sizeof(int)); // 分配 1亿个int ≈ 400MB if (!arr) { perror(\u0026#34;Array allocation failed\u0026#34;); exit(EXIT_FAILURE); } return arr; } 结果确实出乎了我们的意料，总结来说如下\n当数据导向在大小在10MB一下时， 数组比位图略有优势 当数据大于10MB时， 位图大约要快5-10倍 然而当数据非常大时，比如1一个数， 虽然位图还是要快于数组，但两者没那么明显了 这是因为\n在很小数据的时候，cpu缓存可以完全存下数据，而数组的指令开销更少，所以会略快 而当数据在大时，数组就无法在缓存中完全存储了，而位图还可以。于是cpu的缓存命中率就会下降，cpu需要从内存中加载数据，我们都知道cpu缓存的速度比内存快几十倍，所以数组方式的就慢了很多 但是当数据非常大时， 这时位图也无法完全缓存了，在这种情况下，影响程序运行速度的主要因素就是从内存加载的速度了，这时候两者时间接近。但是位图的存储更加紧凑，命中概率更高，所以还是会快一些。 何时选择哪种方案？ 用数组： 当数字范围小（例如 0~1000），或需要频繁读取且容忍内存浪费时。\n用位图： 当处理海量数据（如爬虫去重、数据库索引），或数字范围大但稀疏分布时。\n实际应用案例： Linux 内核页表 使用位图管理内存页 Redis 的 Bitmap 类型 用于高效存储布尔值 总结 实践是检验真理的唯一标准 我们对于计算机的了解不足，忘记了计算机是一个复杂的系统，程序运行的时间受到多种因素影响。而我们都以理想的角度去理解，就导致了形而上学的认识。\n收获 认识到了cpu缓存命中带来的影响 对不同数据结构在不同条件下的性能影响 ","permalink":"https://mem.leabol.top/posts/os/%E5%85%B3%E4%BA%8E%E4%BD%8D%E5%9B%BE%E5%AD%98%E5%82%A8%E4%B8%8E%E6%95%B0%E7%BB%84%E4%B8%8B%E6%A0%87%E5%AD%98%E5%82%A8%E7%9A%84%E6%8E%A2%E8%AE%A8/","summary":"\u003ch1 id=\"关于位图存储与数组下标存储的探讨\"\u003e关于位图存储与数组下标存储的探讨\u003c/h1\u003e\n\u003cp\u003e最近在看《编程珠玑》，书中的第一章提到了位图。位图存储使用二进制位的位数信息，来表示数字。例如，存储1、3、4、6、8，这五个数字， 可以使用\u0026lt;1, 0, 1, 1, 0, 1, 0, 1\u0026gt;这样的向量来表示，这样可以大大减少存储空间， 只用了一个字节（8bit），就可以存储原来需要的8个数字才能表示的信息。\u003c/p\u003e","title":"关于位图存储与数组下标存储的探讨"},{"content":"集合 集合一般被定义为：由一个或多个确定的元素所构成的整体。 集合有什么特性呢？\n首先，集合里的元素类型不一定相同。 你可以将商品看作一个集合，也可以将整个商店看作一个集合，这个商店中有人或者其他物品也没有关系。\n其次，集合里的元素没有顺序。 我们不会这样讲：我想要集合中的第三个元素，因为集合是没有顺序的。\n事实上，这样的集合并不直接存在于编程语言中。然而，实际编程语言中的很多数据结构，就是在集合的基础上添加了一些规则形成的。\n列表 列表（又称线性列表）的定义为：是一种数据项构成的有限序列，即按照一定的线性顺序，排列而成的数据项的集合。\n列表的概念是在集合的特征上形成的，它具有顺序，且长度是可变的。你可以把它看作一张购物清单：\n购物清单中的条目代表的类型可能不同，但是按照一定顺序进行了排列； 购物清单的长度是可变的，你可以向购物清单中增加、删除条目。 在编程语言中，列表最常见的表现形式有数组和链表，而我们熟悉的栈和队列则是两种特殊类型的列表。除此之外，向列表中添加、删除元素的具体实现方式会根据编程语言的不同而有所区分。\n数组 数组是列表的实现方式之一，也是面试中经常涉及到的数据结构。\n正如前面提到的，数组是列表的实现方式，它具有列表的特征，同时也具有自己的一些特征。然而，在具体的编程语言中，数组这个数据结构的实现方式具有一定差别。比如 C++ 和 Java 中，数组中的元素类型必须保持一致，而 Python 中则可以不同。Python 中的数组叫做 list，具有更多的高级功能。\n那么如何从宏观上区分列表和数组呢？这里有一个重要的概念：索引。\n首先，数组会用一些名为 索引 的数字来标识每项数据在数组中的位置，且在大多数编程语言中，索引是从 0 算起的。我们可以根据数组中的索引，快速访问数组中的元素。\n而列表中没有索引，这是数组与列表最大的不同点。\n其次，数组中的元素在内存中是连续存储的，且每个元素占用相同大小的内存。\n相反，列表中的元素在内存中可能彼此相邻，也可能不相邻。比如列表的另一种实现方式——链表，它的元素在内存中则不一定是连续的。\n","permalink":"https://mem.leabol.top/posts/%E7%AE%97%E6%B3%95/%E9%9B%86%E5%90%88%E5%88%97%E8%A1%A8%E5%92%8C%E6%95%B0%E7%BB%84/","summary":"\u003ch3 id=\"集合\"\u003e\u003cem\u003e集合\u003c/em\u003e\u003c/h3\u003e\n\u003chr\u003e\n\u003cp\u003e\u003ca href=\"https://baike.baidu.com/item/%E9%9B%86%E5%90%88/2908117?fr=aladdin\"\u003e集合\u003c/a\u003e一般被定义为：由一个或多个确定的元素所构成的整体。\n集合有什么特性呢？\u003c/p\u003e\n\u003cp\u003e首先，\u003cstrong\u003e集合里的元素类型不一定相同\u003c/strong\u003e。 你可以将商品看作一个集合，也可以将整个商店看作一个集合，这个商店中有人或者其他物品也没有关系。\u003c/p\u003e","title":"集合 列表 数组"},{"content":"理解Linux的file descriptor(文件描述符) ​\t我们知道在Linux系统中一切皆可以看成是文件，文件又可分为：普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候，我们每操作一次就找一次名字，这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引，这样要操作文件的时候，我们直接找到索引就可以对其进行操作了。\n​\t文件描述符（file descriptor）就是内核为了高效管理这些已经被打开的文件所创建的索引，其是一个非负整数（通常是小整数），用于指代被打开的文件，所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候，0是标准输入，1是标准输出，2是标准错误。这意味着如果此时去打开一个新的文件，它的文件描述符会是3，再打开一个文件文件描述符就是4\u0026hellip;\u0026hellip;\nLinux内核对所有打开的文件有一个文件描述符表格，里面存储了每个文件描述符作为索引与一个打开文件相对应的关系，简单理解就是下图这样一个数组，文件描述符（索引）就是文件描述符表这个数组的下标，数组的内容就是指向一个个打开的文件的指针。\n文件描述符指向了由系统内核维护的一个file table中的某个条目(entry)。这个解释可能过于抽象，不过在正式详细介绍fd之前，有必要先了解用户程序和系统内核之间的工作过程。\n注: 本文描述的所有场景仅限于类unix系统环境，在windows中这玩意叫file handle(臭名昭著的翻译: 句柄)。\nUser space \u0026amp; Kernel space 现代操作系统会把内存划分为2个区域，分别为Use space(用户空间) 和 Kernel space(内核空间)。用户的程序在User space执行，系统内核在Kernel space中执行。\n用户的程序没有权限直接访问硬件资源，但系统内核可以。比如读写本地文件需要访问磁盘，创建socket需要网卡等。因此用户程序想要读写文件，必须要向内核发起读写请求，这个过程叫system call。\n内核收到用户程序system call时，负责访问硬件，并把结果返回给程序。\nFileInputStream fis = new FileInputStream(\u0026#34;/tmp/test.txt\u0026#34;); byte[] buf = new byte[256]; fis.read(buf); 上面代码的流程如下图所示\nFile Descriptor 上面简单介绍了User space和Kernel space，这对于理解fd有很大的帮助。fd会存在，就是因为用户程序无法直接访问硬件，因此当程序向内核发起system call打开一个文件时，在用户进程中必须有一个东西标识着打开的文件，这个东西就是fd。\nfile tables ​\t一个 Linux 进程启动后，会在内核空间中创建一个 PCB 控制块，PCB 内部有一个文件描述符表（File descriptor table），记录着当前进程所有可用的文件描述符，也即当前进程所有打开的文件。进程级的描述符表的每一条记录了单个进程所使用的文件描述符的相关信息，进程之间相互独立，一个进程使用了文件描述符3，另一个进程也可以用3。除了进程级的文件描述符表，系统还需要维护另外两张表：打开文件表、i-node 表。这两张表存储了每个打开文件的打开文件句柄（open file handle）。一个打开文件句柄存储了与一个打开文件相关的全部信息。\n和fd相关的一共有3张表，分别是file descriptor、file table、inode table，如下图所示。\nfile descriptors\nfile descriptors table由用户进程所有，每个进程都有一个这样的表，这里记录了进程打开的文件所代表的fd，fd的值映射到file table中的条目(entry)。\n另外，每个进程都会预留3个默认的fd: stdin、stdout、stderr;它们的值分别是0、1，2。\nInteger value Name symbolic constant file stream 0 Standard input STDIN_FILENO stdin 1 Standard output STDOUT_FILENO stdout 2 Standard error STDERR_FILENO stderr file table\nfile table是全局唯一的表，由系统内核维护。这个表记录了所有进程打开的文件的状态(是否可读、可写等状态)，同时它也映射到inode table中的entry。如下:\n当前文件偏移量（调用read()和write()时更新，或使用lseek()直接修改） 打开文件时的标识（open()的flags参数） 文件访问模式（如调用open()时所设置的只读模式、只写模式或读写模式） 与信号驱动相关的设置 对该文件i-node对象的引用，即i-node 表指针 inode table\ninode table同样是全局唯一的，它指向了真正的文件地址(磁盘中的位置)，每个entry全局唯一。内容如下:\n文件类型（例如：常规文件、套接字或FIFO）和访问权限 一个指针，指向该文件所持有的锁列表 文件的各种属性，包括文件大小以及与不同类型操作相关的时间戳 ​\t文件描述符、打开的文件句柄以及i-node之间的关系如下图：\n在进程 A 中，文件描述符 1 和 20 都指向了同一个打开文件表项，标号为 23（指向了打开文件表中下标为 23 的数组元素），这可能是通过调用 dup()、dup2()、fcntl() 或者对同一个文件多次调用了 open() 函数形成的。\n进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向了同一个文件，这可能是在调用 fork() 后出现的（即进程 A、B 是父子进程关系），或者是不同的进程独自去调用 open() 函数打开了同一个文件，此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。\n进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件表项，但这些表项均指向 i-node 表的同一个条目（标号为 1976）；换言之，它们指向了同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了 open() 调用。同一个进程两次打开同一个文件，也会发生类似情况。\n这就说明：同一个进程的不同文件描述符可以指向同一个文件；不同进程可以拥有相同的文件描述符；不同进程的同一个文件描述符可以指向不同的文件（一般也是这样，除了 0、1、2 这三个特殊的文件）；不同进程的不同文件描述符也可以指向同一个文件。\n流程 当程序向内核发起system call open()，内核将会\n允许程序请求 创建一个entry插入到file table，并返回file descriptor 程序把fd插入到fds中。 当程序再次发起read()system call时，需要把相关的fd传给内核，内核定位到具体的文件(fd –\u0026gt; file table –\u0026gt; inode table)向磁盘发起读取请求，再把读取到的数据返回给程序处理。\n下面是read这个函数的定义，第一个参数fd即file descriptor。\nssize_t read(int fd, void *buf, size_t count); 同样的，writesystem call函数如下\nssize_t write(int fd, const void *buf, size_t nbytes); 从上面的结果来看，fd就是file table的一个索引，指向了file table中的entry。\n查看进程的file descriptors linux系统可以通过/proc/pid/fd文件夹查看进程的fd，比如我的redis进程id为96104，执行以下命令查看\nls -l /proc/96104/fd 参考 https://wiyi.org/linux-file-descriptor.html\nhttps://blog.csdn.net/yushuaigee/article/details/107883964\n","permalink":"https://mem.leabol.top/posts/os/%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6/","summary":"\u003ch1 id=\"理解linux的file-descriptor文件描述符\"\u003e理解Linux的file descriptor(文件描述符)\u003c/h1\u003e\n\u003cp\u003e​\t\t我们知道在Linux系统中一切皆可以看成是文件，文件又可分为：普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候，我们每操作一次就找一次名字，这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引，这样要操作文件的时候，我们直接找到索引就可以对其进行操作了。\u003c/p\u003e","title":"file descriptor"},{"content":"linux select函数 在Linux中，我们可以使用select函数实现I/O端口的复用，传递给 select函数的参数会告诉内核：\n我们所关心的文件描述符\n对每个描述符，我们所关心的状态。(我们是要想从一个文件描述符中读或者写，还是关注一个描述符中是否出现异常)\n我们要等待多长时间。(我们可以等待无限长的时间，等待固定的一段时间，或者根本就不等待)\n从 select函数返回后，内核告诉我们一下信息：\n对我们的要求已经做好准备的描述符的个数\n对于三种条件哪些描述符已经做好准备.(读，写，异常)\n有了这些返回信息，我们可以调用合适的I/O函数(通常是 read 或 write)，并且这些函数不会再阻塞.\n函数声明 #include \u0026lt;sys/select.h\u0026gt; int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout); 返回值：做好准备的文件描述符的个数，超时为0，错误为 -1.\n参数 struct timeval 首先我们先看一下最后一个参数。它指明我们要等待的时间：\nstruct timeval{ long tv_sec; /*秒 */ long tv_usec; /*微秒 */ } 有三种情况： timeout == NULL 等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或者是捕获到一个信号时函数会返回。如果捕获到一个信号， select函数将返回 -1,并将变量 erro设为 EINTR。\ntimeout-\u0026gt;tv_sec == 0 \u0026amp;\u0026amp;timeout-\u0026gt;tv_usec == 0不等待，直接返回。加入描述符集的描述符都会被测试，并且返回满足要求的描述符的个数。这种方法通过轮询，无阻塞地获得了多个文件描述符状态。\ntimeout-\u0026gt;tv_sec !=0 ||timeout-\u0026gt;tv_usec!= 0 等待指定的时间。当有描述符符合条件或者超过超时时间的话，函数返回。在超时时间即将用完但又没有描述符合条件的话，返回 0。对于第一种情况，等待也会被信号所中断。\nreadset, writset, exceptse 中间的三个参数 readset, writset, exceptset,指向描述符集。这些参数指明了我们关心哪些描述符，和需要满足什么条件(可写，可读，异常)。一个文件描述集保存在 fd_set 类型中。fd_set类型变量每一位代表了一个描述符。我们也可以认为它只是一个由很多二进制位构成的数组。如下图所示：\nfd_set类型 对于 fd_set类型的变量我们所能做的就是声明一个变量，为变量赋一个同种类型变量的值，或者使用以下几个宏来控制它：\n#include \u0026lt;sys/select.h\u0026gt; int FD_ZERO(int fd, fd_set *fdset); int FD_CLR(int fd, fd_set *fdset); int FD_SET(int fd, fd_set *fd_set); int FD_ISSET(int fd, fd_set *fdset); FD_ZERO宏将一个 fd_set类型变量的所有位都设为 0，使用FD_SET将变量的某个位置位。清除某个位时可以使用 FD_CLR，我们可以使用 FD_ISSET来测试某个位是否被置位。\n当声明了一个文件描述符集后，必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位，操作如下：\nfd_set rset; int fd; FD_ZERO(\u0026amp;rset); FD_SET(fd, \u0026amp;rset); FD_SET(stdin, \u0026amp;rset); select返回后，用FD_ISSET测试给定位是否置位：\nif(FD_ISSET(fd, \u0026amp;rset) { ... } 具体解释select的参数： （1）intmaxfdp是一个整数值，是指集合中所有文件描述符的范围，即所有文件描述符的最大值加1，不能错。\n说明：对于这个原理的解释可以看上边fd_set的详细解释，fd_set是以位图的形式来存储这些文件描述符。maxfdp也就是定义了位图中有效的位的个数。\n（2）fd_set*readfds是指向fd_set结构的指针，这个集合中应该包括文件描述符，我们是要监视这些文件描述符的读变化的，即我们关心是否可以从这些文件中读取数据了，如果这个集合中有一个文件可读，select就会返回一个大于0的值，表示有文件可读；如果没有可读的文件，则根据timeout参数再判断是否超时，若超出timeout的时间，select返回0，若发生错误返回负值。可以传入NULL值，表示不关心任何文件的读变化。\n（3）fd_set*writefds是指向fd_set结构的指针，这个集合中应该包括文件描述符，我们是要监视这些文件描述符的写变化的，即我们关心是否可以向这些文件中写入数据了，如果这个集合中有一个文件可写，select就会返回一个大于0的值，表示有文件可写，如果没有可写的文件，则根据timeout参数再判断是否超时，若超出timeout的时间，select返回0，若发生错误返回负值。可以传入NULL值，表示不关心任何文件的写变化。\n（4）fd_set*errorfds同上面两个参数的意图，用来监视文件错误异常文件。\n（5）structtimeval* timeout是select的超时时间，这个参数至关重要，它可以使select处于三种状态，第一，若将NULL以形参传入，即不传入时间结构，就是将select置于阻塞状态，一定等到监视文件描述符集合中某个文件描述符发生变化为止；第二，若将时间值设为0秒0毫秒，就变成一个纯粹的非阻塞函数，不管文件描述符是否有变化，都立刻返回继续执行，文件无变化返回0，有变化返回一个正值；第三，timeout的值大于0，这就是等待的超时时间，即 select在timeout时间内阻塞，超时时间之内有事件到来就返回了，否则在超时后不管怎样一定返回，返回值同上述。\n说明：\n函数返回：\n（1）当监视的相应的文件描述符集中满足条件时，比如说读文件描述符集中有数据到来时，内核(I/O)根据状态修改文件描述符集，并返回一个大于0的数。\n（2）当没有满足条件的文件描述符，且设置的timeval监控时间超时时，select函数会返回一个为0的值。\n（3）当select返回负值时，发生错误。\n理解select模型 理解select模型的关键在于理解fd_set,为说明方便，取fd_set长度为1字节，fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。\n（1）执行fd_set set;FD_ZERO(\u0026amp;set);则set用位表示是0000,0000。\n（2）若fd＝5,执行FD_SET(fd,\u0026amp;set);后set变为0001,0000(第5位置为1)\n（3）若再加入fd＝2，fd=1,则set变为0001,0011\n（4）执行select(6,\u0026amp;set,0,0,0)阻塞等待\n（5）若fd=1,fd=2上都发生可读事件，则select返回，此时set变为0000,0011。注意：没有事件发生的fd=5被清空。\n基于上面的讨论，可以轻松得出select模型的特点：\n（1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)＝512，每bit表示一个文件描述符，则我服务器上支持的最大文件描述符是512*8=4096。据说可调，另有说虽然可调，但调整上限受于编译内核时的变量值。\n（2）将fd加入select监控集的同时，还要再使用一个数据结构array保存放到select监控集中的fd，一是用于再select返回后，array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空，则每次开始 select前都要重新从array取得fd逐一加入（FD_ZERO最先），扫描array的同时取得fd最大值maxfd，用于select的第一个参数。\n（3）可见select模型必须在select前循环array（加fd，取maxfd），select返回后循环array（FD_ISSET判断是否有时间发生）。\n","permalink":"https://mem.leabol.top/posts/os/linux-select%E5%87%BD%E6%95%B0/","summary":"\u003ch1 id=\"linux-select函数\"\u003elinux select函数\u003c/h1\u003e\n\u003cp\u003e在Linux中，我们可以使用select函数实现I/O端口的复用，传递给 select函数的参数会告诉内核：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e我们所关心的文件描述符\u003c/p\u003e","title":"linux select函数"},{"content":"Makefile 是 make 工具的配置文件，主要用于定义如何构建和管理项目的编译过程。Makefile 是 C/C++ 编译项目中的重要工具，尤其在多文件、多模块的项目中，为了方便复杂项目的管理，可以通过自动化规则提高编译效率。\n这篇文章是 Makefile 的入门教程的介绍，包括其基本语法、常用功能、以及实际使用中的高级技巧。\nMakefile 的基本概念 安装 make：\nsudo apt-get update sudo apt-get install make 核心功能 自动化编译：根据文件的依赖关系自动决定哪些文件需要重新编译。 提高效率：只编译发生变化的部分。 多任务支持：可以定义清理、打包、测试等任务。 使用方法 运行make命令时，默认会读取当前目录下的Makefile 文件。\nmake # 执行默认的目标 make \u0026lt;target\u0026gt; # 执行指定的目标 例如：\nmake clean # 执行 \u0026#34;clean\u0026#34; 目标 Makefile 的基本语法 1. 目标语法 Makefile 的基本语法如下：\ntarget: prerequisites commands target：目标文件或命令名称。例如可执行文件、目标文件或任务名称。 prerequisites：依赖文件或其他目标。只有依赖文件发生变化时，target 才会被重新生成。 commands：生成目标的命令（必须以 Tab 开头）。 示例：\nhello: hello.o gcc -o hello hello.o hello.o: hello.c gcc -c hello.c 在示例代码中：\n如果 hello.c 被修改，则 hello.o 会重新生成。\n如果 hello.o 被修改，则 hello 会重新生成。\n2. 变量定义 Makefile 支持变量定义，用于简化配置和重复的命令。\n示例1：\nCC = gcc CFLAGS = -Wall -g # 简单赋值 CC :=$(XX) gcc 当前有效 // 立马获取当前XX变量的值 # 递归赋值 CC =$(XX) gcc\t// 去递归寻找XX变量的最后一个值 # 条件赋值 CC ?= gcc\t// 如果有CC变量，则该语句无效 # 追加赋值 CC +=$(XX) gcc\t// 在原来值的基础上，进行追加 # 使用：$(CC) 获取变量的值 示例2：\nx := jake y := $(x) and rose x := tonly all : echo \u0026#34;x=$(x),y=$(y)\u0026#34; 使用变量（示例1）\nhello: hello.o $(CC) $(CFLAGS) -o hello hello.o 输出：（示例2）\nx=windx,y=jake and rose 示例\nCC = gcc CFLAGS = -Wall -O2 hello: hello.o $(CC) $(CFLAGS) -o hello hello.o hello.o: hello.c $(CC) $(CFLAGS) -c hello.c 3. 内置变量 Makefile 提供了一些常用的内置变量，可以减少重复工作。\n变量 | 功能\n变量 功能 $@ 当前目标的名称（target）。 $\u0026lt; 第一个依赖文件的名称（prerequisite）。 $^ 所有依赖文件的名称。 代码片：\nCC = gcc CFLAGS = -Wall -g hello: hello.o $(CC) $(CFLAGS) -o $@ $^ 在这里：\n$@ 是 hello（目标）。 $^ 是 hello.o（所有依赖文件）。 4. 通配符和自动化规则 Makefile 支持通配符和模式匹配规则，用于简化多文件项目的编写。\n通配符\n*.c：匹配所有 .c 文件。 $(wildcard *.c)：列出当前目录下所有 .c 文件。 $(patsubst %.c, %.o, $(wildcard *.c))：将所有 .c 文件替换为对应的 .o 文件。 自动化规则\n使用模式规则可以自动生成目标文件：\n%.o: %.c $(CC) $(CFLAGS) -c $\u0026lt; -o $@ 表示所有 .o 文件都可以由对应的 .c 文件生成。 代码：\nCC = gcc CFLAGS = -Wall -O0 -g SRC = $(wildcard *.c) OBJ = $(patsubst %.c, %.o, $(SRC)) hello: $(OBJ) $(CC) $(CFLAGS) -o $@ $^ %.o: %.c $(CC) $(CFLAGS) -c $\u0026lt; -o $@ 5. 伪目标 伪目标（phony targets）是指不生成实际文件的目标，通常用于清理、测试等任务。\n伪目标也是一个目标，该目标不需要生成，每一次去执行该目标，都会执行。\n定义伪目标：\n.PHONY: clean # 将当前目标设定为伪目标，每次执行该目标都会执行命令 clean: rm -f *.o hello 使用 .PHONY 声明可以避免冲突（例如当前目录下存在名为 clean 的文件时）。 6. 自动推导规则 在使用make编译.c源文件时，可以省略编译一个.c文件所使用的命令。这是因为make存在一个默认的规则，能够自动完成对.c文件的编译并生成对应的.o文件。它执行命令“cc -c”来编译.c源文件。对于上边的例子，此默认规则就使用命令“cc -c main.c -o main.o”来创建文件“main.o”。因此对一个目标文件是“N.o”，倚赖文件是“N.c”的规则。可以省略其规则的命令行，使用 make 的默认命令。此默认规则称为make的隐含规则（关于隐含规则可查看我上传至资源中的 《GUN make中文手册》第九章 [ 使用隐含规则 ] 一章，有详细说明。）\n我们书写 Makefile 时，对于一个.c文件如果使用 make 的隐含规则，那么它会被自动作为对应.o文件的一个依赖文件（对应是指：文件名除后缀外，其余都相同的两个文件）。因此我们也可以在规则中省略目标的倚赖.c文件。\n自动推导规则_1\n自动推导规则_2\n我们可以看见，当app文件被创建之后，再次执行 make 命令时，提示：make: 'app' is up to date.，此处是 make 的时间戳管理方式，如果目标生成的时间 晚于 依赖，该目标是最新的，那么该目标就不需要再编译重新生成。（如果想详细了解makefile的时间戳管理机制，可以点击查看这篇文章，对 Makefile 的时间戳管理机制有更详细的说明。\u0026lt;\n笨猫猫的小茶馆：Makefile 时间戳管理2 赞同 · 0 评论文章\n）\n项目示例 这是一个多文件 C 项目的示例 Makefile：\n项目目录 project/ ├── main.c ├── utils.c ├── utils.h ├── Makefile ``` ##### Makefile ```bash # 编译器和编译参数 CC = gcc CFLAGS = -Wall -g # 源文件和目标文件 SRC = main.c utils.c OBJ = $(SRC:.c=.o) TARGET = my_program # 默认目标 all: $(TARGET) # 链接目标文件 $(TARGET): $(OBJ) $(CC) $(CFLAGS) -o $@ $^ # 编译规则（自动化规则） %.o: %.c $(CC) $(CFLAGS) -c $\u0026lt; -o $@ # 清理目标 .PHONY: clean clean: rm -f $(OBJ) $(TARGET) 运行说明 构建项目 make 清理项目： make clean 这么写缺失中间的流程，接下来我结合图片将 makefile 管理项目的实际流程走一遍：\n要求：（虽然是简易版用于教学的，但其中的过程和项目管理的思路，即使在大型项目中也是一致的，不同的只是划分的层级结构、架构更为复杂而已。）\n创建项目目录 include/project.h 集合的头文件 jack 文件夹下的文件 将 jack.c 通过编译生成 jack.o 文件 ——\u0026gt; 然后将 jack.o 放入 obj 文件夹下 此处的Makefile用于管理main.c文件，将main.c编译转化为main.o，将其保存在obj文件夹下统一管理 main.o文件生成成功并放在了统一文件夹obj下，接下来，将main.o和jack.o做链接操作生成app文件，因此，我们需要在obj文件夹下创建Makefile文件，用于将所有集合在此处的.0 文件链接生成可执行文件app 成功生成了可执行文件app 因为main :会自动寻址至依赖的第一个文件，结果将提示文件已存在。所以这里我们要将需要生成的文件 main 设为伪目标。 至此，前面的步骤我们共计完成了 4 步：\n第一步，我们将 jack 文件夹下的 jack.c 生成了 jack.o ，将其放在 obj 文件夹下；\n第二步，我们将 main 文件夹下的 main.c 生成了 main.o，将其放在 obj 文件夹下；\n第三步，在 obj 文件夹下，将这里所有集合着的 .o 文件 最终生成了 可执行文件 app。\n最后，我们在整个项目目录下，生成一个 Makefile 文件，用于管理整个项目，这样，最终我们只需要 make 一次，便能够得到整体项目的可执行文件，而不需要经过上述如此繁琐的步骤。\nmake -C 文件夹/[目标] // 去到该文件夹下 去执行该文件夹下的 Makefile 的 第一个目标[目标] 因为第一个目标已存在，所以此处我们需要提前设置mainclean为伪目标。\n最终生成可执行文件 app 优化 Makefile 代码。 添加共享变量，在最终的 Makefile 文件中添加共享变量，这样的话，可供同一级目录 和 该目录下的所有子目录中的 Makefile 文件皆可以共享到最上层的 Makefile文件 中的共享变量。 export 共享变量 // 当前同一级目录下的子目录都可以共享该变量 这时候进入了一个新人，分配任务给 rose，创建 rose 文件夹并加入这部分模块的代码， 至此，以上为 Makefile 项目管理在开发中的实际应用（简易版）。\n高级功能 条件判断 根据条件设置变量或执行不同的规则。\nDEBUG = 1 ifeq ($(DEBUG), 1) CFLAGS += -g else CFLAGS += -O2 endif 包含其他 Makefile 使用 include 将多个 Makefile 文件整合在一起。\ninclude common.mk 这种方式适合大型项目，将公共配置提取到单独的文件中。 一些常见问题与技巧 如何调试 Makefile？ 运行 make 时加上-n或--dry-run参数，可以查看执行的命令而不真正运行：\nmake -n 如何提高编译效率？ 使用 make 的并行编译选项-j，可以同时编译多个目标：\nmake -j4 其中 4 表示最多允许 4 个任务同时执行。 综上。这篇文章仅仅只介绍了 makefile 的简单用法，更多更详细的内容可以下载资源查看 makefile 手册（中文版），Makefile 是一个强大的工具，适合用于管理从简单到复杂的编译流程。\n本章主要介绍了：\n基础语法：理解目标、依赖和命令的结构。 变量和内置变量：使用变量提高可读性和灵活性。 自动化规则：使用通配符和模式规则减少重复代码。 伪目标：定义清理、测试等任务。 高级功能：条件判断、多文件组织、并行编译。 通过不断优化 Makefile，可以显著提高项目的管理效率和可维护性，适合运用和管理大型项目。\n以上。仅供学习与分享交流，请勿用于商业用途！转载需提前说明。\n","permalink":"https://mem.leabol.top/posts/%E5%B7%A5%E5%85%B7/makefile/","summary":"\u003cp\u003e\u003cstrong\u003eMakefile\u003c/strong\u003e 是 make 工具的配置文件，主要用于定义如何构建和管理项目的编译过程。Makefile 是 C/C++ 编译项目中的重要工具，尤其在多文件、多模块的项目中，为了方便复杂项目的管理，可以通过自动化规则提高编译效率。\u003c/p\u003e","title":"Makefile 的基本概念"},{"content":"为什么Lisp语言如此先进?\n一个前辈的官网\n","permalink":"https://mem.leabol.top/posts/%E4%B8%80%E4%BA%9B%E7%BD%91%E7%AB%99/","summary":"\u003cp\u003e\u003ca href=\"https://www.ruanyifeng.com/blog/2010/10/why_lisp_is_superior.html\"\u003e为什么Lisp语言如此先进?\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://norvig.com/\"\u003e一个前辈的官网\u003c/a\u003e\u003c/p\u003e","title":"一些网站"},{"content":"文件控制函数 file control 功能描述 fcntl函数可以用来对已打开的文件描述符进行各种控制操作, 以改变已打开文件的的各种属性\n头文件\n#include \u0026lt;unistd.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; 函数原型 int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); c int fcntl(int fd, int cmd, struct flock *lock); 描述 fcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符.\n针对cmd的值,fcntl能够接受第三个参数（arg）\nfcntl函数有5种功能c 复制一个现有的描述符（cmd=F_DUPFD）.\n获得／设置文件描述符标记(cmd=F_GETFD或F_SETFD).\n获得／设置文件状态标记(cmd=F_GETFL或F_SETFL).\n获得／设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).\n获得／设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).\ncmd 选项 F_DUPFD 返回一个如下描述的(文件)描述符:\n（1）最小的大于或等于arg的一个可用的描述符\n（2）与原始操作符一样的某对象的引用\n（3）如果对象是文件(file)的话,返回一个新的描述符,这个描述符与arg共享相同的偏移量(offset)\n（4）相同的访问模式(读,写或读/写)\n（5）相同的文件状态标志(如:两个文件描述符共享相同的状态标志)\n（6）与新的文件描述符结合在一起的close-on-exec标志被设置成交叉式访问execve(2)的系统调用\nF_GETFD 取得与文件描述符fd联合close-on-exec标志,类似FD_CLOEXEC.\n如果返回值和FD_CLOEXEC进行与运算结果是0的话,文件保持交叉式访问exec(),否则如果通过exec运行的话,文件将被关闭(arg被忽略)\nF_SETFD 设置close-on-exec旗标。该旗标以参数arg的FD_CLOEXEC位决定。\nF_GETFL 取得fd的文件状态标志,如同下面的描述一样(arg被忽略)\nF_SETFL 设置给arg描述符状态标志,可以更改的几个标志是：O_APPEND， O_NONBLOCK，O_SYNC和O_ASYNC。\nF_GETOWN 取得当前正在接收SIGIO或者SIGURG信号的进程id或进程组id,进程组id返回成负值(arg被忽略)\nF_SETOWN 设置将接收SIGIO和SIGURG信号的进程id或进程组id,进程组id通过提供负值的arg来说明,否则,arg将被认为是进程id\n命令字(cmd)F_GETFL和F_SETFL的标志如下面的描述:\nO_NONBLOCK 非阻塞I/O ; 如果read(2)调用没有可读取的数据,或者如果write(2)操作将阻塞,read或write调用返回-1和EAGAIN错误 O_APPEND 强制每次写(write)操作都添加在文件大的末尾,相当于open(2)的O_APPEND标志\nO_DIRECT 最小化或去掉reading和writing的缓存影响.系统将企图避免缓存你的读或写的数据.\n如果不能够避免缓存,那么它将最小化已经被缓存了的数 据造成的影响.如果这个标志用的不够好,将大大的降低性能\nO_ASYNC 当I/O可用的时候,允许SIGIO信号发送到进程组,例如:当有数据可以读的时候\n**注意：**在修改文件描述符标志或文件状态标志时必须谨慎，先要取得现在的标志值，然后按照希望修改它，最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令，这样会关闭以前设置的标志位。\n例子：\n//设置socket为非阻塞模式(套接字立即返回，不管I/O是否完成，该函数所在的线程会继续运行) fcntl(fd,F_SETFL,fcntl(fd,F_GETFD,0)|O_NONBLOCK); //F_SETFL 设置给arg描述符状态标志,可以更改的几个标志是：O_APPEND， O_NONBLOCK，O_SYNC和O_ASYNC。 //F_GETFL 取得fd的文件状态标志,如同下面的描述一样(arg被忽略) //O_NONBLOCK 非阻塞I/O; fcntl的返回值 与命令有关。如果出错，所有命令都返回－1，如果成功则返回某个其他值。\n下列三个命令有特定返回值：\nF_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。\n第一个返回新的文件描述符，第二个返回相应标志，最后一个返回一个正的进程ID或c负的进程组ID。\n一：第一种类似于dup操作，在这里不做举例。（fcnlt(oldfd, F_DUPFD, 0) \u0026lt;==\u0026gt;dup2(oldfd, newfd)） 二：设置close-on-exec旗标 在此函数中创建子进程，调用execl\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; int main() { pid_t pid; //以追加的形式打开文件 int fd = fd = open(\u0026#34;test.txt\u0026#34;, O_TRUNC | O_RDWR | O_APPEND | O_CREAT, 0777); if(fd \u0026lt; 0) { perror(\u0026#34;open\u0026#34;); return -1; } printf(\u0026#34;fd = %d\\n\u0026#34;, fd); fcntl(fd, F_SETFD, 0);//关闭fd的close-on-exec标志 write(fd, \u0026#34;hello c program\\n\u0026#34;, strlen(\u0026#34;hello c program!\\n\u0026#34;)); pid = fork(); if(pid \u0026lt; 0) { perror(\u0026#34;fork\u0026#34;); return -1; } if(pid == 0) { printf(\u0026#34;fd = %d\\n\u0026#34;, fd); int ret = execl(\u0026#34;./main\u0026#34;, \u0026#34;./main\u0026#34;, (char *)\u0026amp;fd, NULL); if(ret \u0026lt; 0) { perror(\u0026#34;execl\u0026#34;); exit(-1); } exit(0); } wait(NULL); write(fd, \u0026#34;hello c++ program!\\n\u0026#34;, strlen(\u0026#34;hello c++ program!\\n\u0026#34;)); close(fd); return 0; } main测试函数c\nint main(int argc, char *argv[]) { int fd = (int)(*argv[1]);//描述符 printf(\u0026#34;fd = %d\\n\u0026#34;, fd); int ret = write(fd, \u0026#34;hello linux\\n\u0026#34;, strlen(\u0026#34;hello linux\\n\u0026#34;)); if(ret \u0026lt; 0) { perror(\u0026#34;write\u0026#34;);c return -1; } close(fd); return 0; } 执行后文件结果：\n[root@centOS5 class_2]# cat test.txt hello c program hello linuxc hello c++ program! 三：用命令F_GETFL和F_SETFL设置文件标志，比如阻塞与非阻塞 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;string.h\u0026gt; /**********************使能非阻塞I/O******************** *int flags; *if(flags = fcntl(fd, F_GETFL, 0) \u0026lt; 0) *{ * perror(\u0026#34;fcntl\u0026#34;); * return -1; *} *flags |= O_NONBLOCK; *if(fcntl(fd, F_SETFL, flags) \u0026lt; 0) *{ * perror(\u0026#34;fcntl\u0026#34;); * return -1; *} *******************************************************/ /**********************关闭非阻塞I/O****************** flags \u0026amp;= ~O_NONBLOCK; if(fcntl(fd, F_SETFL, flags) \u0026lt; 0) { perror(\u0026#34;fcntl\u0026#34;); return -1; } *******************************************************/ int main() { char buf[10] = {0}; int ret; int flags; //使用非阻塞io if(flags = fcntl(STDIN_FILENO, F_GETFL, 0) \u0026lt; 0) { perror(\u0026#34;fcntl\u0026#34;); return -1; } flags |= O_NONBLOCK; if(fcntl(STDIN_FILENO, F_SETFL, flags) \u0026lt; 0) { perror(\u0026#34;fcntl\u0026#34;); return -1; } while(1) { sleep(2); ret = read(STDIN_FILENO, buf, 9); if(ret == 0) { perror(\u0026#34;read--no\u0026#34;); } else { printf(\u0026#34;read = %d\\n\u0026#34;, ret); } write(STDOUT_FILENO, buf, 10); memset(buf, 0, 10); } return 0;c } 四：设置异步IO 五：设置获取记录锁 结构体flock的指针：\nstruct flcok { short int l_type; /* 锁定的状态*/ //这三个参数用于分段对文件加锁，若对整个文件加锁，则：l_whence=SEEK_SET,l_start=0,l_len=0; short int l_whence;/*决定l_start位置*/ off_t l_start; /*锁定区域的开头位置*/ off_t l_len; /*锁定区域的大小*/ c pid_t l_pid; /*锁定动作的进程*/ }; _type 有三种状态:\nF_RDLCK 建立一个供读取用的锁定\nF_WRLCK 建立一个供写入用的锁定\n​ F_UNLCK 删除之前建立的锁定\nl_whence 也有三种方式:\nSEEK_SET 以文件开头为锁定的起始位置。\nSEEK_CUR 以目前文件读写位置为锁定的起始位置\nSEEK_END 以文件结尾为锁定的起始位置。\n#include \u0026#34;filelock.h\u0026#34; /* 设置一把读锁 */ int readLock(int fd, short start, short whence, short len) { struct flock lock; lock.l_type = F_RDLCK; lock.l_start = start; lock.l_whence = whence;//SEEK_CUR,SEEK_SET,SEEK_END lock.l_len = len; lock.l_pid = getpid(); // 阻塞方式加锁 if(fcntl(fd, F_SETLKW, \u0026amp;lock) == 0) return 1; return 0; } /* 设置一把读锁 , 不等待 */ int readLocknw(int fd, short start, short whence, short len) { struct flock lock; lock.l_type = F_RDLCK; lock.l_start = start; lock.l_whence = whence;//SEEK_CUR,SEEK_SET,SEEK_END lock.l_len = len; lock.l_pid = getpid(); // 非阻塞方式加锁 if(fcntl(fd, F_SETLK, \u0026amp;lock) == 0) return 1; return 0; } /* 设置一把写锁 */ int writeLock(int fd, short start, short whence, short len) { struct flock lock; lock.l_type = F_WRLCK; lock.l_start = start; lock.l_whence = whence; lock.l_len = len; lock.l_pid = getpid(); //阻塞方式加锁 if(fcntl(fd, F_SETLKW, \u0026amp;lock) == 0) return 1; return 0; } /* 设置一把写锁 */ int writeLocknw(int fd, short start, short whence, short len) { struct flock lock; lock.l_type = F_WRLCK; lock.l_start = start; lock.l_whence = whence; lock.l_len = len; lock.l_pid = getpid(); //非阻塞方式加锁 if(fcntl(fd, F_SETLK, \u0026amp;lock) == 0) return 1; return 0; } /* 解锁 */ int unlock(int fd, short start, short whence, short len) { struct flock lock; lock.l_type = F_UNLCK; lock.l_start = start; lock.l_whence = whence; lock.l_len = len; lock.l_pid = getpid(); if(fcntl(fd, F_SETLKW, \u0026amp;lock) == 0) return 1;c return 0; } ","permalink":"https://mem.leabol.top/posts/os/linux-fcntl%E5%87%BD%E6%95%B0/","summary":"\u003ch1 id=\"文件控制函数--file-control\"\u003e文件控制函数  file control\u003c/h1\u003e\n\u003ch2 id=\"功能描述\"\u003e功能描述\u003c/h2\u003e\n\u003cp\u003efcntl函数可以用来对已打开的文件描述符进行各种控制操作, 以改变已打开文件的的各种属性\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e头文件\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;unistd.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#include\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;fcntl.h\u0026gt;\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"函数原型\"\u003e\u003cstrong\u003e函数原型\u003c/strong\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efcntl\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e fd, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e cmd);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efcntl\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e fd, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e cmd, \u003cspan style=\"color:#66d9ef\"\u003elong\u003c/span\u003e arg);         \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ec\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efcntl\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e fd, \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e cmd, \u003cspan style=\"color:#66d9ef\"\u003estruct\u003c/span\u003e flock \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003elock);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"描述\"\u003e\u003cstrong\u003e描述\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003efcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符.\u003c/p\u003e","title":"文件控制函数"}]