Chromium 中的指针

2024-10-15 pv

Chromium 是一个久经考验的开源浏览器引擎,是 Chrome 和 Edge 的共同基石。

其主要开发语言是 C++。

众所周知,C++ 是一门语法特性丰富,甚至可以说是略显复杂的编程语言。尽管多数情况下,我们使用的只是其中一个很小子集。

最直观的例子,《C++ Primer》这本经典的入门书,非常厚,第五版将近有 800 页。

絮絮叨叨说这么多,其实就是想表达一个观点:

看书,背诵标准库,是学不好编程的。要多动手,尤其是观摩、揣摩并动手模仿工程上的“最佳实践”和其解题思路。

这个观点,在去年的 文章🔗 文章提到过,是孟岩讲的:

我主张,在具备基础之后,学习任何新东西,都要抓住主线,突出重点。对于关键理论的学习,要集中精力,速战速决。而旁枝末节和非本质性的知识内容,完全可以留给实践去零敲碎打。

Chromium 对于 C++ 做了很多包装和裁剪,并形成一套独有的风格(Style),包括指针的使用,多线程,任务调度等。

后续会出一系列文章,也欢迎大家讨论。

第一篇文章,打算谈一谈 Chromium 中指针(Pointer)的使用。

1. 概述

指针这个概念,目前应该只在 C 和 C++ 语言中被广泛使用。

这是一个功能强大,使用灵活,但也经常导致程序异常退出和隐藏 bug。

指针指向的是一段内存地址(Memory address),这是 C/C++ 语言赋予程序员的一种几乎是操纵一切的能力:借助指针可以精准管理内存

无论是申请和释放的时机,还是内存共享,小小一个指针,蕴藏极大能量。

但是,任何强大的工具,如果没有保护措施或使用指南,便很容易犯错

关于指针,常见的错误大致有两种:

  • 内存忘记回收导致内存泄漏(Memory leak)
  • 野指针(Dangling reference)引用导致未定义行为(Undefined behavior)

关于第一个问题,STL 已经实现了一套机制:智能指针(Smart pointer)。针对不同场景,还贴心地提供了三个工具类,unique_ptrshared_ptrweak_ptr

除此,Chromium 还额外提供了一些辅助类,简化线程管理。

第二个问题,STL 目前没有涉及,暂时还需要依赖程序员的经验。

Chromium 则是开发出 raw_ptr,直接让程序在野指针调用处 Crash,而不是“稀里糊涂”地工作,将不确定的漏洞,转换为确定性崩溃

2. 智能指针

智能指针的核心是 引用计数(Reference counting)🔗

这是一个广为使用的对象生命期管理技巧。不仅可以用于管理内存

原理很简单。什么样的对象需要释放?不再被使用的。

因此用一个额外的控制块(Control block)记录对象被引用的次数,当引用计数为 0 的时候,对象被立即回收

STL 用这种方式,实现了 shared_ptr

不过引用计数存在循环引用(Circular reference)的问题。

举个最简单的例子,如果两个 shared_ptr 内部各自持有对方的原始指针,那么双方的引用计数都是 1,这两个对象将一直活着。

于是 STL 提出“弱引用”的概念,用 weak_ptr 表示。只有使用权,没有所有权,shared_ptr 指向的对象一定是有效的,但 weak_ptr 不是,具体可以通过 lock() 方法判断。

概念很直白,STL 的实现也很简洁。

shared_ptr 内部有两块内容,一部分是原始指针,另一部分是引用计数。

引用计数分两种,强引用计数绑定 shared_ptr 的数量,弱引用计数绑定 weak_ptr 的数量。区别在于,当强引用计数为 0 的时候,*ptr 释放,但控制块的部分还在,否则 weak_ptr 将无法获取关于对象是否还活着的信息。

控制块会在弱引用计数为 0 的时候销毁

引用计数固然强大,但额外增加的状态管理,会导致复杂性上升,性能下降。

于是对于对于排他性内存这样的使用场景,STL 提供了 unique_ptr,基本可视为原始指针的轻度封装。好处是所有权清晰,只有一个对象持有,对象析构时自动释放。当然,所有权可以通过 std::move() 转移。

通俗一点,unique_ptr 就是只实现了移动语义,且不支持拷贝操作的裸指针。在使用方式和性能上,与原始指针几乎一致。

STL 的这三个指针,适用于各类场景,包括多线程,因为引用计数是原子(Atomic)操作。

对于单线程环境,Chromium 额外提供了 WeakPtr

不要被表象迷惑,它跟 weak_ptr 很不一样。

weak_ptr 一定要结合 shared_ptr 使用,而且支持多线程。

WeakPtr 是单独使用,只可用于被创建线程。它并不是用于管理内存的工具,而是用来跟踪指向对象是否还活着,如果对象销毁,WeakPtr 将会自动置空。

在这一点上,倒是与 weak_ptr 类似:不参与控制持有对象的生命周期

WeakPtr 的实现也值得拿来讲讲,简单而实用。

WeakPtr 是由工厂类 WeakPtrFactory 创建,该类利用一个 标记(Flag)追踪对象的有效性。当对象销毁,该标记会被重置。

因为所有 WeakPtr 都持有该标记的引用,因此在调用时,首先判断标记的有效性,如果无效,解引用(Dereference)时返回 nullptr

3. raw_ptr

raw_ptr 是 Chromium 中用来替代原始裸指针 T* 的一种智能指针类型。

其目的,是在保持指针操作高效的前提下,增强内存安全,尤其是避免释放后引用问题(Use after free, UAF)

前面几个智能指针,通常用于简化内存管理,raw_ptr 更聚焦在野指针。

野指针的调用,会带来未定义行为,增加问题排查难度。因此,当指针指向的内存空间失效时,raw_ptr 会触发保护机制,程序直接崩溃。颇符合 Erlang 的思考哲学:

Let it crash.

内部使用到两套机制:特殊的内存分配器(PartitionAlloc)和标记位。

PartitionAlloc 的实现相当复杂。简单来说,这是一个为了空间效率、分配延迟和安全而特殊优化的内存分配器。对于无效内存的访问会触发错误。

标记位用于记录指针是否有效,伴随指针的操作而同步更新。

4. 总结

C++11 出来后,智能指针的使用变得广泛,极大降低了开发者内存管理的心智负担。

同时可用于管理生命周期并不明朗的对象。《Linux 多线程服务端编程》这本书有单独的一章谈及此事。

由此可见,封装(Encapsulation),是控制复杂度的绝佳方式。

当然,了解背后的基本原理和思想,无疑有助于更好地使用它们。

无论是 shared_ptr 还是 raw_ptr,说到底都是状态的管理。

两点技巧:尽可能少;穷举状态改变的所有时机。

之前有位朋友跟我讲,编程的本质是控制复杂度

延申出去,复杂度的核心就是态管理。

编程也好,做事也罢,我们不断学习的,是从混沌中摸索,甚至控制出一丝秩序的能力。

(完)

参考

  1. 引用计数 - 维基百科,自由的百科全书🔗
  2. PartitionAlloc Design🔗
  3. raw_ptr.md - Chromium Code Search🔗
  4. BackupRefPtr [PUBLIC] - Google 文档🔗
在 GitHub 上编辑本页面

最后更新于: 2024-10-16T02:53:58+08:00