V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
kuanat
V2EX  ›  Linux

Linux 漫谈(二)

  •  9
     
  •   kuanat · 9 小时 12 分钟前 · 1555 次点击

    文章第一部分在 https://0.51bbc.workers.dev/t/1180785

    0x20 性能是永恒的追求

    尽管所有人都知道“过早优化是万恶之源”,但是对于性能的追求、尽可能发挥硬件的潜能,到今天仍然是软件行业最核心的竞争指标。

    什么叫“过早”优化?从纯技术的观点出发,针对瓶颈点的改善措施才有意义,不是关键障碍的部分做改良就算是“过早”了。相对来说,技术上的误区还是比较容易规避的。我举个比较正面的例子,Donald Knuth 写的《计算机程序设计艺术》也就是常说的 TAOCP,书中涉及的算法都有数学上的复杂度上下限,这样的信息就非常适合指导优化方案。实践当中,用技术手段获得数据支撑,确认瓶颈点往往是比优化本身更重要的工作。

    对于开发者来说,更可怕的“过早”优化是思维逻辑上的。比如很多人会希望自己的程序代码能像云服务一样动态扩展,支撑得起 0~1/1~100 这样的业务增长,于是在开发初期就用上了复杂的细分技术。再比如出于“我可以不用,但你不能不给”的想法,为极少用到的功能提供支持。这样的想法本身没有错,只是多数情况下没有与之匹配的开发资源做支撑,设计层面的优化就变成工程层面的累赘。

    事实上没有人经得起性能优化的诱惑,毕竟在软件开发领域,性能往往就是商业的命门。

    0x21 Red Hat 转型

    要论谁最懂性能的价值,我觉得 Red Hat 说第一没什么悬念。

    2003 年之前的 Red Hat ,主营业务一方面是卖 Linux 发行版光盘和出版各种技术书籍,另一方面是靠 1999 年收购 Cygnus Solutions 的团队,为企业提供 gcc/gdb 开发维护服务。此时红帽的年营收还不到一亿美元,而且并不稳定。

    Cygnus 这个名字是不是有些眼熟?没错,它就是 Cygwin 的开发者。Cygnus 的主要业务是将 gcc/gdb 这样的工具移植到各种 CPU 架构上。在 2000 年前后随着 Windows 的普及,Cygnus 选择将 Unix 底层的 API 转换为 Windows API ,这样不需要修改 gcc 源码即可运行在 Windows 上,于是就有了 Cygwin 。

    这次收购对于 Red Hat 来说最大的价值是获得了一个核心开发团队,专门负责用户空间工具链的开发,加上 Red Hat 的原班人马组建的内核团队,将 2003 年的 Linux 2.6 内核打造成了可用于廉价 x86 硬件的服务器操作系统。

    这个 Linux 2.6 的影响力有多夸张呢?它使得云业务成为可能,Red Hat 的营收在五年内翻了五倍,同时因为蚕食了竞争对手的市场空间,直接导致了 Sun(Solaris) 被 Oracle 收购。

    我认为甚至不需要列数字,只需要看文字说明就能想象到 2.6 内核带来的性能提升:

    • 实现了 O(1) 的进程调度。这样 Linux 具备了运行大型应用(大量进程)的可能。

    • 实现了细粒度的内核锁。原本内核只有一个锁,现在细分为了上千个独立的功能锁,使得内核可以支持 SMP 多核心处理器。

    • 支持了抢占式( preemptive )。这使得系统响应时间可控,内核最大延迟降低到毫秒级。

    • 支持了 NPTL(Native POSIX Thread Library) 原生线程模型。原本 Linux 是没有线程概念的,NPTL 配合 futex 可以为 Java/MySQL 这种重度依赖线程的应用提供高性能的线程创建销毁支持。

    • 网络栈实现了 epoll 模型。这一改动使得 Linux 可以支持单机 C10K 连接,后续 Nginx 就是得益于此。

    内核调度器的负责人是 Ingo Molnar ,网络栈部分是 David Miller 。这些内核特性想要发挥出来,离不开 Cygnus 团队的贡献。关键人物 Ulrich Drepper 是 glibc 的维护者,基于新内核特性重写了 glibc 的线程库。基本上 Linux 2.6 之后,内核就重度依赖 gcc 编译器了。

    毫不夸张地说,Red Hat 就是赢在了性能上。它们对于性能的优化,使得 Linux 可以在廉价的 x86 设备上,以不到十分之一的成本实现了传统 Unix 服务器的效能,直接影响了整个云计算的时代。

    我不好评说到底是 Red Hat 的超前眼光,还是机缘巧合运气爆棚,从这段历史可以看出来两件事:一是开源世界的主要贡献都来自于商业公司,包括 Linus Torvalds 本人当时也是从众多企业公共资助的 OSDL 组织拿工资;二是技术想要落地、生态迁移是个漫长的过程,如果不是 Cygnus/glibc 团队也在红帽,谁也说不清用上新内核特性要花多久。

    0x22 疯狂的 NT

    如果看一下 2003 年以后服务器市场份额,就会发现一个很有意思的事情,Linux 抢夺的是 Unix 服务器的市场,而 Windows 服务器却没有受太大影响。除开商业推广的因素,很大的原因还是 Windows 服务器在当时是真的快。

    实际上在 2000 前后,IIS 5.0 甚至打不过 Linux+Apache 的组合,尽管当时 Linux 还没有 2.6 的内核优化,也没有 epoll 模型。所以在 Windows Server 2003 中,NT 内核加入了 HTTP 驱动模块,静态内容不经用户空间的 IIS 就在内核直接处理了。这直接使得 Windows 保住了服务器市场的份额。

    我之前将 NT 比作激进派,就是因为它会将商业需求作为首要目标,即使付出一定的代价。

    在 1993 年 NT 设计之初,3.1 版本基本上是纯粹的微内核设计,此时的图形子系统是运行在用户态的 CSRSS(Client/Server Runtime Subsystem),结果就是简单的绘图操作都很慢,当时的 CPU 难以支持。

    于是之后的 NT 4.0 版本直接将 GDI 和显卡驱动一起移动到了内核中,减少了上下文切换和内存拷贝之后,图形性能获得了质的飞跃。直到今天 Windows 11 中 WinForms(Win32 API) 依然是最快的 UI 框架。在很长一段时间里,Windows 的图形系统性能相对 Linux/macOS 一直有着碾压性的优势。

    但通过这样极端的方式获得性能优势的同时,也带来了严重的稳定性问题,因为显示驱动造成的蓝屏死机占了总数的 20% 以上。所以从 Vista 版本之后,NT 内核又尝试改变驱动模型( WDDM )从而将容易崩溃的代码移出内核。

    再回到设计理念的问题上,无论 Dave Cutler 再怎么推崇微内核,在性能这个第一优先指标面前,他还是要选择最务实甚至最激进的做法。所以我一直讲,评价设计的好坏要结合具体的需求背景以及设计诉求来看。既满足了性能指标,又获得了商业成功,尽管付出了未来十几年的兼容性包袱代价,我仍旧认为这都是好的设计。

    实际上前面只是为了性能这个话题特地找的两个例子,NT 内核虽然激进但它也有很多经得起时间考验的优秀设计。

    比如 NT 内核中有个名为 IOCP(Input/Output Completion Port) 的机制,在 NT 内核设计之初就存在了。不同于 epoll 模型的内核唤醒进程然后进程读取数据,它直接由内核将数据写入进程缓冲然后再唤醒。这个设计使得 SQL Server 一度是最快的数据库软件。

    0x23 关于 XNU 的强行找补

    因为这一章的主题是性能,而 XNU 是最不关注性能的那个,或者换个说法,由于 XNU 是 Mach+BSD 的设计,本身 Mach 部分涉及性能优化的也很有限。

    某种程度上说,XNU 的性能优化都转向了软硬件结合了,特别是到 x86 和 Apple Silicon 的两次转型。举个最直观的例子,异构(大小核)调度是个很困难的问题,苹果在芯片中就加入了一个硬件控制器,这样软件传递 QoS 优先级标签就可以了。

    这个做法就让人很难评价……因为它本质上是个手动指派优先级,和软件算法逻辑毫无关系。但它的效果确实不错,至于不错的原因更多是苹果基本不维护旧代码,把兼容任务扔给开发者,开发者要自己声明某个任务是前台还是后台。

    在 M1 的时候,普通版本是四个能效核,Pro 版本去掉了两个能效核换成了性能核。按照苹果的设计,所有后台任务都要运行在能效核上,这就导致 Pro 版本在商店应用安装或者后台索引任务上性能甚至远远落后于普通版本。你猜苹果是如何解决这个问题的?没错,过两年就不用解决了。

    记得我在前言中提到的“为什么开发者应该学习 Linux”吗?很重要的一点是 Linux 的代码库是开源的。从我上面对内核性能特性的简单举例就能看出,现在的 Linux 在大部分功能上都是 State of the Art (最优)版的实现,或者说最佳实践,覆盖了绝大多数开发需求的场景,本身就是极好的学习对象。

    更重要的一点是,LKML ( Linux 内核邮件列表)中的讨论也是非常有价值的,能够从中了解到代码是如何写的,想要解决什么问题这样更重要的信息。尽管 XNU 某种意义上也是源码可见,但学习的价值就非常有限。我选择将文章发在这里,其实也是看中了这里的讨论氛围,交流可以让文章产生更大的价值。

    0x24 内核的性能核心

    在上一章中提到,操作系统最重要的功能是通过分时方式切换不同的应用程序运行。由于物理上内存是共享的,那么就需要隔离机制,将内核独立出来一方面可以方便管理虚拟内存,另一方面也可以利用硬件机制获得更好的安全性。

    要理解内核对于性能的需求,就要理解 CPU 的工作原理。CPU 本身并没有任何多任务的概念,而且在某一个时刻,CPU 只有当前运行状态的少数信息,这其中就包括 PC 也就是之后要执行什么指令。如果我们能将 CPU 某个时刻的状态进行保存和还原,就能从逻辑上执行任意数量的应用程序。这个过程就叫做 Context Switch 上下文切换。

    这里所谓 CPU 的状态,在内核中就是简单的数据结构,其中包括了寄存器、栈指针等信息。之后在需要切换进程的时候,通过中断介入然后执行这个状态保存和恢复过程,就可以完成进程的切换。但此时还有个问题要解决,不同进程在内存中的位置是不同的,CPU 需要知道物理地址才能加载对应内存中的代码。

    现代内核的虚拟内存实现基本都是将物理内存按照页( Pages )划分,在应用程序自身看来,总是在一个私有的线性地址空间中,而在内核看来,只需要维护一个指向特定页的指针,就可以完成物理地址和虚拟地址之间的转换。在 CPU 中这个页面指针存储在 CR3 寄存器中。(这里描述的是一个极度简化的模型)

    现在还剩最后一个问题,由于应用程序本身是按照私有的虚拟地址空间加载的,此时所有的诸如跳转等指令,目标也是虚拟地址。当 CPU 执行这个指令的时候,就需要做虚拟地址到物理地址的转换。这是一个极高频率的操作,所以 CPU 设计了 TLB(Translation Lookaside Buffer) 的缓冲区用来存储地址转换的结果。

    到现在整个过程就比较清楚了,每次上下文切换都会伴随着 CPU 状态的重置,以及 TLB 的更新( flush )。理解了这一点,也就理解了内核性能的核心瓶颈之一:上下文切换。受到计算机硬件本身的限制,存储一定是个多级缓存的结构,上下文切换会使得 CPU 不得不等待数据和指令从更慢的缓存逐级加载,从而产生性能瓶颈。(为了改善上下文切换的效率,现代 CPU 在硬件层面有了很多对 TLB 的优化。但即便 TLB 本身性能有所改善,随之而来的缓存更新也不可避免。这里为了表述方便也对模型做了简化。)

    之前提到 Linux/NT 各种性能优化时,总是讲到某某功能的内核实现。因为本质上内核也是独立的进程,进程和内核之间也会发生上下文切换(准确地说是模式切换,取决于虚拟内存的实现),那么将特定功能移入内核,就可以减少上下文切换带来的性能开销。

    内核性能的另一个瓶颈来源是 IPC ,它不仅受制于上下文切换的效率,还常常伴随着大量数据交换。具体内容会在之后的章节中讨论。

    整篇文章的写作风格还是偏漫谈,所以部分技术表达可能不是很准确,行文也比较简略,如果有兴趣的话可以尝试让 AI 对细节进行解释。

    12 条回复    2025-12-25 11:37:08 +08:00
    dashupc
        1
    dashupc  
       8 小时 43 分钟前 via Android
    学习了,感谢分享
    kkchan1999
        2
    kkchan1999  
       8 小时 38 分钟前
    学习了
    kelvansun
        3
    kelvansun  
       8 小时 36 分钟前
    来了来了,又可以学习了。
    CosmoLau
        4
    CosmoLau  
       8 小时 28 分钟前
    已严肃学习
    moudy
        5
    moudy  
       8 小时 13 分钟前   ❤️ 1
    linux 具体的性能分析没有深入关注过,我日常要处理的是嵌入式操作系统的性能调优。这里系统调用的开销是极大的。嵌入式系统往往是重度 io 依赖,叠加现在日趋严格的功能安全和信息安全需求,导致大量的逻辑不能直接放权给应用层,必须经由系统内核处理,可以理解为用户态驱动为主。加上如今的应用对实时吞吐量要求高了一个台阶,怎么合理安排系统架构实在是一个现实的挑战。
    kuanat
        6
    kuanat  
    OP
       7 小时 42 分钟前
    @moudy #5

    如果只是说内核的性能,一般是关注上下文切换、IPC 、锁和 IO 。其中锁和 IO 多数与资源有关,上下文切换主要与硬件相关。所以内核能做的优化或者说实现方案主要是减小锁的粒度和范围,异步/缓冲/零拷贝 IO ,剩下的就是 IPC 设计相关了。嵌入式领域如果能用主线内核最好了,但多数时候可能绕不开上游厂家的 bsp 。我个人理解多数情况下不需要关心这些系统层面的安全和性能,毕竟关心了可能也没办法动。如果有极高的 io 类性能需求,多数可能要走类似 dpdk 这样的用户态方案。
    bcllemon
        7
    bcllemon  
       7 小时 16 分钟前
    学习了,感谢分享
    thezhxin
        8
    thezhxin  
       7 小时 9 分钟前
    学习了,感谢分享
    forsuperper
        9
    forsuperper  
       6 小时 56 分钟前
    学习学习~
    Maboroshii
        10
    Maboroshii  
       6 小时 18 分钟前
    这么快就更新了,支持支持!

    说到 windows 和 linux 比性能的历史故事还挺有趣的。 这么说来 Linus 最早写的 linux 好像很弱鸡呀,还得是有老板在后面砸钱才能优化出这么厉害的代码
    josephzeng
        11
    josephzeng  
       5 小时 14 分钟前
    学习了,谢谢分享。
    Danswerme
        12
    Danswerme  
       5 小时 8 分钟前
    感谢分享,期待后续!
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   5158 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 411ms · UTC 08:45 · PVG 16:45 · LAX 00:45 · JFK 03:45
    ♥ Do have faith in what you're doing.