chromium-base库源码解读之引用计数拆解篇
chromium-base库作为业界标杆实现,值得每一位开发者细细品读。 本系列将围绕“基本概念、设计哲学、代码技巧”三个关键点来对chromium-base库进行深度剖析,本篇拆解“引用计数”。
chromium-base库源码解读之引用计数拆解篇
引用计数,应是开发者耳熟能详的概念,不做赘述。
封装一个引用计数类,无非就是内部维护一个计数器,然后提供一对Add/Release接口,在计数器归零时,将对象释放。引用计数类一般由开发者定义的子类继承,然后配合智能指针类一起使用,达到开发者无需关心对象的生命周期、通过智能指针将对象传来传去的目的(妈妈再也不用担心内存泄漏了(然而并不一定))。
chromium-base的引用计数类封装在base/memory/ref_counted.h
,分别命名为RefCounted
和RefCountedThreadSafe
,顾名思义,后者是线程安全的。
读本文前,最好先阅读《chromium-base库源码解读之智能指针拆解篇》。
RefCounted
这是个类模板,形如:
1 |
|
其他类如果想要使用RefCounted
类,一般会通过公有继承RefCounted<T>
来实现,T
就是类本身的名字。
1 |
|
另外它公有继承了subtle::RefCountedBase
这么个基类,subtle是base内部的命名空间,对外曝光的类去公有继承一个XXXBase
的基类,这是老套路了。
subtle::RefCountedBase
我们先看看基类(移除了调试用的各种脚手架):
1 |
|
知识点:C++11之后,禁用默认生成的拷贝构造器和赋值操作符有了更表意的写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ALL DISALLOW_xxx MACROS ARE DEPRECATED; DO NOT USE IN NEW CODE.
// Use explicit deletions instead. See the section on copyability/movability in
// //styleguide/c++/c++-dos-and-donts.md for more information.
// Put this in the declarations for a class to be uncopyable.
#define DISALLOW_COPY(TypeName) \
TypeName(const TypeName&) = delete
// Put this in the declarations for a class to be unassignable.
#define DISALLOW_ASSIGN(TypeName) TypeName& operator=(const TypeName&) = delete
// Put this in the declarations for a class to be uncopyable and unassignable.
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
DISALLOW_COPY(TypeName); \
DISALLOW_ASSIGN(TypeName)利用
=delete
语法糖,相比C++11之前的仅声明不定义的迂回写法,更加易懂。
64位机器需要对上溢和下溢做检查:
1 |
|
对于上溢来说,32位无须检查的理由是:32位架构中这种通过疯狂创建引用的攻击手法会因为没有足够的地址空间而不奏效;但对下溢来说,实际上32位架构也可以奏效,目前chromium-base库并没对32位的ReleaseImpl
做检查,这就为漏洞利用留了操作空间。
知识点:CHECK宏实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Discard log strings to reduce code bloat.
//
// This is not calling BreakDebugger since this is called frequently, and
// calling an out-of-line function instead of a noreturn inline macro prevents
// compiler optimizations.
#define CHECK(condition) \
UNLIKELY(!(condition)) ? IMMEDIATE_CRASH() : EAT_CHECK_STREAM_PARAMS()
// Macro for hinting that an expression is likely to be false.
#if !defined(UNLIKELY)
#if defined(COMPILER_GCC) || defined(__clang__)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#else
#define UNLIKELY(x) (x)
#endif // defined(COMPILER_GCC)
#endif // !defined(UNLIKELY)UNLIKELY宏的包装是为了提升gcc和clang对分支处理的效率,可以参考:
https://www.jianshu.com/p/2684613a300f
后两个宏不展开了,要根据编译器和指令集来细分,前者会导致程序崩溃,后者返回0。
回头来看RefCounted
类模板:
1 |
|
这里的kRefCountPreference
静态成员变量值得一提,内置默认的是subtle::kStartRefCountFromZeroTag
,象征着RefCountedBase
的ref_count_
成员的0值,因此构造器的初始化列表中subtle::RefCountedBase(T::kRefCountPreference)
默认会匹配到RefCountedBase::RefCountedBase(StartRefCountFromZeroTag)
,从而最终构造出来的对象一开始引用计数是0;当外部需要从1值初始化时,就需要在类T
内自己覆盖定义kRefCountPreference
。
显然,从使用者视角来看,定义一个又臭又长且意义不明的kRefCountPreference
非常奇怪,而针对这种情况最简单的处理就是提供一个封装好的宏定义,宏定义除了语句简洁以外,还有名称表意的效果:
1 |
|
实际上
kRefCountPreference
还会在智能指针scoped_refptr
的相关操作中引用到,毕竟引用计数对象要搭配智能指针类来使用,引用计数负责对象的生命周期,智能指针负责把对象传来传去。比如我们展开看上面的友元函数实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace subtle {
enum AdoptRefTag { kAdoptRefTag };
}
template <typename T>
scoped_refptr<T> AdoptRef(T* obj) {
using Tag = std::decay_t<decltype(T::kRefCountPreference)>;
static_assert(std::is_same<subtle::StartRefCountFromOneTag, Tag>::value,
"Use AdoptRef only if the reference count starts from one.");
// subtle::kAdoptRefTag用于占位,去匹配其private构造器(所以它也是scoped_refptr<T>的友元)
// scoped_refptr<T>::scoped_refptr(T* p, base::subtle::AdoptRefTag);
// 如此就区分开了public构造器scoped_refptr<T>::scoped_refptr(T* p);
// 后者会调用p->AddRef()来为T对象增加引用计数,而前者不会
return scoped_refptr<T>(obj, subtle::kAdoptRefTag);
}静态断言确保
T::kRefCountPreference
是subtle::StartRefCountFromOneTag
,即引用从1值起始。然后直接用obj
对象和subtle::kAdoptRefTag
构造一个scoped_refptr<T>
智能指针对象并返回,该指针也就指向了对象obj
。
RefCountedThreadSafe
这个是RefCounted
的线程安全版本。
1 |
|
老套路,先看基类。
subtle::RefCountedThreadSafeBase
1 |
|
知识点:我们知道对现代编译器来说,inline并不一定真的能inline,
ALWAYS_INLINE
宏用于确保inline生效。
1
2
3
4
5
6
7
#if defined(COMPILER_GCC) && defined(NDEBUG)
#define ALWAYS_INLINE inline __attribute__((__always_inline__))
#elif defined(COMPILER_MSVC) && defined(NDEBUG)
#define ALWAYS_INLINE __forceinline
#else
#define ALWAYS_INLINE inline
#endif
可以看到与非线程安全的版本大致相同,只是在几个关键点上有所区别:
引用计数器不再是个
uint32_t
,换成了使用原子操作的AtomicRefCount
。线程安全嘛,理所应当。
AtomicRefCount
是chromium-base对标准库std::atomic_int
的封装,后面展开。多了一个
AddRefWithCheck
接口。这个接口替代了
AddRef
,除了首次增加引用计数的场景,我们可以在RefCountThreadSafe
类中看到。该接口增强了保护,对previous value做了CHECK
。非X86架构因为"size regression"的成本而放弃了inline。
1
2
3
4
5
6
7
8
9
10
11
12
13// 对于X86架构的,由于Release、AddRef和AddRefWithCheck都在类内定义,所以都是inline
// 对非X86的就迂回一下,把这几个定义放到了类外
#if !defined(ARCH_CPU_X86_FAMILY)
bool RefCountedThreadSafeBase::Release() const {
return ReleaseImpl();
}
void RefCountedThreadSafeBase::AddRef() const {
AddRefImpl();
}
void RefCountedThreadSafeBase::AddRefWithCheck() const {
AddRefWithCheckImpl();
}
#endif疑点:这里的知识点我并不清楚,以后懂了再做补充。
回头看RefCountThreadSafe
:
1 |
|
与非线程安全版本基本一致,只是对AddRef
加强了保护。
AtomicRefCount
这个类非常简单,包装了std::atomic_int
。
这里用
int
应该是基于以下两种考虑的:
- 引用计数一般不会太离谱,不会超过int的正数范围
- 刚好配合
subtle::RefCountedThreadSafeBase::AddRefWithCheck
,用于在add之后检查是否溢出(CHECK(ref_count_.Increment() > 0);
)。
1 |
|
这里考虑CPU架构通用性以及性能,没有使用C++默认的memory_order_seq_cst
,而是精心设计了恰当的Acquire/Release语义。这些东西是为了防止多线程环境因指令重排序、CPU缓存而导致的依赖关系错乱,详细可以参考该文章:https://zhuanlan.zhihu.com/p/31386431,强烈推荐!