linux线程私有数据的原理
线程私有数据的应用
errno
, pthread_self()
都是线程私有数据。比如”名字”, 每个线程都有, 但却各不相同。muduo库使用了gcc提供__thread
做到了这点(类似于c++11提供的thread_local
, 它们基本等价)。此外pthread也提供了线程私有数据的支持。
线程私有数据原理
windows
和linux
实现的方式各不相同, 其中前者依赖了相关api, 见维基百科此处。本文只讨论linux
下simple and stupid的做法。
linux
内核特别配置了fs
寄存器, 用来实现线程私有数据(Thread Specified Data即TSD)。线程初次访问fs:某个变量地址
可以触发缺页异常, 然后内核将会映射线程私有数据的地址到fs:某个地址的变量
。那到底是什么意思? 从下面的代码及其汇编可以找到答案:
1 | struct big { |
对应汇编:
1 | val1: |
这里的val1
相当于一个指针(说法不太恰当, 但只要对汇编有点理解就能明白这个比喻)。访问fs:val1
的时候, 由于内核之前没有对这里做任何映射, 触发缺页异常产生进入内核态。
触发缺页异常后, 内核将建立一块TSD内存, 它的初始值就是val1
的数据, TSD被存放在某个地址空间, 可以被用户态读写。接着, 内核将fs:val1
映射到建立的TSD内存, 从异常处返回。然后用户态程序重新访问fs:val1
这块数据就能正常拿到自己的私有数据了😃。
以后这个进程再次访问的时候, 由于fs:val1
的映射关系已经做好了。直接读出来的就是映射过的TSD内存了。
对于程序员, 我们当然不需要自己用fs
乱搞一通, 上面提到的__thread
就是实现线程私有数据的一个方式。此外glibc提供了pthread_key
相关的API也能实现线程私有数据(但是原理是一致的, 都是上面提到的访问fs
进入缺页异常, 然后内核建立映射)。然而muduo使用了__thread
而没有使用glibc提供的那些, 原因下面讨论。
pthread_key有何不妥?
glibc提供了pthread_key_create
, pthread_getspecific
, pthread_setspecific
等api用来设置访问线程私有数据。然而因为以下几点, 它们不常用甚至不应该被使用:
- 慢, glibc维护了数据结构来管理这些键, 要
create
和get
时可能需要查表(我认为甚至可能有锁)。且设计函数调用, 这点也有开销 - 数量受限, glibc限制了能创建的键数目(取决于glibc, 一般是128个), 然而天知道第三方库会创建多少个
pthread_key
__thread如何取地址/或者用地址进行读写?
上面的文本使得我们对线程私有数据有了最基本的认识。前面我们演示了读写, 看起来图景很清晰, 基本就是内核搞定了一切。然而却没有这么简单, 读写好说, 但取地址就略显麻烦了:
1 | __thread int val; |
对应的汇编:
1 | val: |
读值/写值的过程很简单, 读写fs:地址
即可, 这点我们前面以及提到过了。
但是读地址有点让人摸不着头脑, 它的逻辑是先从fs:0
读出一个值, 然后加上val
(注意我之前把val
比作地址), 这就取得了它的地址。我们能通过这个地址修改此线程的这个私有数据, 它就是普通的内存地址, 没有任何魔法, 即之前提到的TSD内存区域的一块内存。
这么做的原因是线程需要先从fs:0
读出它的TSD基地址(这也是线程的一个私有数据), 加上val
才是映射的实际地址, 这就是fs:val
映射的实际地址了。