Chromium 中的指针
2024-10-15
示例代码:https://github.com/plantree/examples/blob/main/cpp/shared_ptr.h🔗
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_ptr
,shared_ptr
和 weak_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
,说到底都是状态的管理。
两点技巧:尽可能少;穷举状态改变的所有时机。
之前有位朋友跟我讲,编程的本质是控制复杂度。
延申出去,复杂度的核心就是态管理。
编程也好,做事也罢,我们不断学习的,是从混沌中摸索,甚至控制出一丝秩序的能力。
(完)
参考
- 本文作者:Plantree
- 本文链接:https://plantree.me/blog/2024/pointer-in-chromium/
- 版权声明:所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
最后更新于: 2024-11-20T09:44:17+08:00