如何用atomic inc挽救神秘消失的 1s

Linux2.2.26源码分析与心得
刚注册了CDSN账号,想在这个平台上分享自己读Linux源代码的心得和体会。一方面是我希望把平常自己读代码的感悟写下来,回头来看不会忘记,另一方面则是鼓励自己多去写一点东西,把它培养成一种习惯。当然,我的理解并不一定100%正确,难免会有一些错误或是没有根据的猜想,如果读者们觉得有问题,我很欢迎大家指正我。
目前我仍然在边读书边理解,还未完全吃透代码,甚至连1%的代码可能都没见过,所以源码分析的干货不会太多,一般当我觉得我理解得差不多了我也许就会把它贴到博客上吧!我分析的Linux源代码的版本是2.2.26,之前大致把0.11的源码看完了。我感觉这个版本源码既没有2.6跨度那么大,也不像0.11、0.95那么原始,很适合我这种初学者研究阅读。
至于如何分析呢?因为内核极其庞大和复杂,而且子系统之间的互相的函数调用和变量引用也是错综复杂,所以不太可能按照函数的调用流程分析。另外如果先分析或是介绍一些基础知识,如X86汇编 ,计算机体系结构,虚拟内存blabla又太冗杂,这些内容网上都有,所以先不一一介绍了。所以我只想简单拿源文件一个个来分析吧!虽然这样不太合适er!另外这些文件都是i386架构下的........
另外关于研究Linux代码,看书我觉得《Linux完全注释》和《Understanding The Linux Kernel》这两本书很不错的一个,读源代码的话用Source Insight或是用这个网站很方便的。嗯,我感觉还是比较喜欢读代码,平时很少写代码呃........
代码索引:///
大家上过操作系统课,应该都听过互斥、竞争、信号量这些概念,相比大家都想知道这个东东在内核中是怎么实现的吧!
其实这个说简单也简单那,说复杂也很复杂。简单是因为原理很简单,就是一组数据结构,比如atomic(叫原子变量?),semaphore(信号量),spinlock(自旋锁)、rwlock(读写锁)之类的,以及对应的方法如atomic_add()、up()、down().....等方法,就这么多类型和方法,记住就好啦。原理上,一般来说如果有多个进程(姑且这么说吧~)想要修改一个变量,那么这个变量应该有一个对应的锁结构来保护它,这样一来任意一个进程在修改这个变量之前必须获得这个锁,获得完方可对变量进行操作,操作完再释放锁。举个例子,这就像很多人在食堂一楼充饭卡,尽管有很多想充,但只有一个人能在圈存机前操作,其他人只好在旁边等着玩手机一样。进程同样如此,如果锁被别人获取了,那么进程也只好“干等”,要么着急地原地打转(这就是所谓的自旋),直到锁被释放,要么睡一下(信号量),等别人把锁释放的同时让他叫醒自己。不管怎样,锁的目的就是让并发的进程访问变得序列化,好让相应的数据结构处于
consistent state(一致性状态),姑且现在就这么理解吧。当然锁的结构和实现还牵系到SMP架构,这个以后再说。
那么锁难在哪里呢?要知道我们我们分析的是内核中的锁,不是用户态的锁,它难在两点:
如何使用锁?何时使用锁?如何防止出现Dead Lock(死锁)发生?&
这个要考虑到所有kernel control path 的各种 interleave 的可能,就不介绍了,感觉一时说不完,关于锁的难点可以参考《ULK》这本书的Kernel synchronization这一章,讲得很通彻。
那今天我们分析那个锁呢?嗯,This is a question!好吧,就从最简单的atomic来说吧,这个算是个“锁”!但分析之前我想先简单分析下一些基本的常识:现在如果有一个变量var在内存地址addr处,我们想写程序把它 +1,怎么吧?很简单,用汇编写就是:&
addl $1, addr;
这条add指令可以把addr处的变量 +1s,另外还可用这个指令: 。如果这个变量之前的值是59,那么现在就是60啦,-1s也是如此。这条指令背后涉及三个操作,就像把大象装冰箱里。首先把原来的值59加载到某一寄存器,再对寄存器的值 +1,最后在把新值写回到addr处。Easy!
但如果在内核中有两个函数fun1和fun2想同时对addr进行+1,会发生什么事情呢?假设fun1先执行,那么在一个内存时钟周期内fun1把addr处的值59加载到寄存器中,这时突然fun2在下一个内存周期也读取addr的值59(此时addr还没变为60,因为fun1还没把值写入addr,一个内存周期内只能进行一个读或写操作),这样一来如果fun1先把+1后值60写到addr,那么之后fun2也会把同样的60写到addr,反之fun2先写也是同样的结果,最后两次+1操作原本应使addr处的变量+2,现在只能+1了,聪明的读者是不是能猜出来神秘消失的1s去哪了呢?
所以为了防止这个现象,我们必须对这个addr变量“加锁”,而且这个锁应该是很轻量的。因为对addr加1能以极快的速度就能完成,fun2只需稍稍等fun1把新值写入到addr之后再读取addr就没问题了,否则如果用信号量,那么总不能因为获取不到锁而让fun2函数sleep吧,这个开销是无比巨大的,而且这个操作太普通不过了,只要有并发就可以出现这种情况,每个都加锁是不现实的,自旋锁的话也不适用于这个场景。
没错,解决方法就是把内存总线锁住,比如刚才的指令,这样写就ok啦:LOCK addl $1,&
LOCK:本质上当汇编器看到这个助记符时,会在这条add指令翻译为机器代码时在代码opcode前面加一个1字节的prefix(指令前缀),当处理器执行这个指令时会首先看到这个锁存前缀,进而在整个add执行的几个处理器周期内,内存总线都是不能被其他在另外一个处理器上运行的函数使用的,当add执行完,那么每个CPU就可以肆无忌惮的读写内存了,具体某一时刻是那个CPU在读写内存取决于哪个CPU获得了内存周期。
这样一来,在fun1在执行整个add操作期间,fun2都是不能碰内存的,当fun2可以读addr的时候addr已经是60 了。当然LOCK锁存的范围是一条指令,而不是一个代码段,而且LOCK只对诸如add,sub,inc,dec之类的指令有效(这些指令都是一个性质:read-&modify-&write),另外像xchg指令(exchange)它是自带锁存buff的哦!
不过还有一些情况需要考虑:刚才假设的是fun1和fun2同时操作在同一变量,但这种情况只有在多处理器架构才可能出现,也就是SMP(Symmetrical Multi-Processing)对称多处理下,此时系统有多个平等的处理器,并且它们共享系统的内存。这样运行在CPU1上的func1就有可能和运行在CPU2上的func2产生竞争关系。反之,如果是在单处理上,那么当fun1正在执行操作的时候,fun2要么处于就绪(可以运行但内核未给它分配time slice),要么处于睡眠状态。总是只有当fun1被内核切换到fun2后fun2才会执行,那么显然在这种情况下不会产生竞争关系,所以在单处理器上用LOCK是没啥用的,没人跟你抢内存总线。
考虑到Linux的可扩展性,它既要在普通的单处理器上跑(当然现在都是多核甚至超线程的了),还要兼顾SMP多处理。那如何统一代码呢?其实很简单:
#ifdef __SMP__
#define LOCK & &
#define LOCK &&
使用如上C语言的条件编译就可以了,#开头的是预处理器指令(不是真的处理器指令呀!)。预处理器(preprocessor)在看到#开头的指令就是执行相应的动作,比如看到:#include &asm/atomic.h&就会自动把用头文件的内容添加进来。同样,在以上这个条件编译中,只要在编译的时候定义了 &__SMP__的话,LOCK就会被替换为”“,否则是”“,而在些代码中不管编译时有没有定义__SMP__,都可以直接使用。
1 #ifndef __ARCH_I386_ATOMIC__
///防止头文件重复被包含
2 #define __ARCH_I386_ATOMIC__
/* C语言不能给我们提供对应的原子操作
* Atomic operations that C can't guarantee us.
Useful for
* 这种操作适用于资源计数等..
* resource counting etc..
9 #ifdef __SMP__
//如果定义了“__SMP__”,LOCK为“”,否则为空
10 #define LOCK & &
12 #define LOCK &&
/*确保gcc不会自作聪明打乱我们的代码
* Make sure gcc doesn't try to be clever and move things around
*我们需要用户给我们的地址,而不是包含了
* on us. We need to use _exactly_ the address the user gave us,
*同样信息的某个别名(什么鬼?)
* not some alias that contains the same information.
20 #define __atomic_fool_gcc(x) (*(volatile struct { int a[100]; } *)x) //确保x是正确的地址,我也不知道为什么要定义成这样??!
22 #ifdef __SMP__
23 typedef struct {
} atomic_t;
25 typedef struct { } atomic_t;
从这里可以看出atomic_t本质上就是一个32位的int,但为啥在SMP下有一个volatile关键字呢? 在C语言中,如果volatile出现在代码前面,表示告诉编译器你不要优化我的代码;而出现变量前,表示编译器你不要为了省事而优化变量读取。什么意思呢?就是如果一个在进行宏替换的时候,&一个atomic变量已经出现寄存器了,那么编译器可能直接对寄存器操作,而不去改变对应的内存处的变量值,这在单处理器中没问题而且效率反而会高一点,但在多处理器下是个严重的问题,但你把寄存器的值改完后,别的CPU却可能读原来内存里的值,这样就不一致啦!!!
那为啥要atomic定义成结构体呢,这样不好嘛?&typedef &volatile int atomic_t;首先这样定义代码的执行效率是一样的,但前者可以享受到C编译器的类型检查“服务”。
显然聪明的编译器发现我们的语句有问题,类型不一致,这就是C的类型检查。其次最主要的还是对于这种原子变量,除了在初始化的时候需要直接赋值外,其他任何时候一次只能对它的counter进行加或减操作,不能直接对counter赋值的,比如说a.counter = 233; ,这样是没意义的。
28 #define ATOMIC_INIT(i)
30 #define atomic_read(v)
((v)-&counter)
31 #define atomic_set(v,i)
(((v)-&counter) = (i))
这里定义ATOMIC_INIT宏也是同样的道理,在初始化的时候虽然可以这样写:
a.counter = 1;
但这样写不更好吗?既让你时刻知道你是在初始化一个atomic_t类型的数据,又隐藏了atomic_t内部具体是如何实现的。
atomic a = ATOMIC_INIT(1);
然后是两个原子操作,一个读地址v处的值,一个将v处的值赋值为i。这里为啥没有LOCK前缀呢?__SMP__跑那去了呢?还记得我们刚才说要LOCK的指令把,比如add,它是read-&modify-&write类型的指令,所以需要在这三个阶段用LOCK锁存,而read指令就是读取v-&counter值,换个说法,就是把counter值加载到寄存器,所以不过是单处理还是多处理,你读取都没问题。同样,在atomic_set中你把v-&counter 设为i也不需要先read进寄存器,再把寄存器中的值赋为i,再写回去呀。换个说法,这个atomic_set可能被编译器直接编译:
movl $i, &(%reg);其中reg保存了v的地址,即v.counter的地址,这里i相当于汇编里的立即数,CPU直接从这条指令中获得立即数i的值,再写到reg寄存器保存的地址的对应内存中去就可以了,不用锁总线,也不用考虑SMP,fun1和fun2谁先赋值,谁就被后者写入的值覆盖了。
33 static __inline__ void atomic_add(int i, volatile atomic_t *v)
__asm__ __volatile__(
LOCK &addl %1,%0&
:&=m& (__atomic_fool_gcc(v))
:&ir& (i), &m& (__atomic_fool_gcc(v)));
这个是GCC的一个扩展,内联汇编,用于在C代码中内嵌汇编代码,标准C里面可没这玩意儿。
像LOCK前缀一般被放在汇编语句前面使用,可不能直接放在C的statement前面使用,毕竟一条C语句会产生很多条汇编语句。而如果我又想调用C语言的函数怎么办?毕竟我刚才的atomic a;是定义在C代码里的,我想使用atomic_add(1,&a);C函数调用释放这个“锁”么办?这里内联汇编就派上用场了。除此之外内联汇编还可以提高C语言的效率,并且可以表达一些C语句表达不了的意思,比如修改扩展标志寄存器EFlags ,只能用汇编,用C没有对应的语句!没有办法啊,我也很绝望。
关于内联汇编我就说到这里了,一时半会也介绍不完哈。值得注意的这个函数被定义为内联函数( &_inline_),首先这样可以减少函数调用,将函数体内联到被调用的地方;其次这个函数被内联之后其实就会多几条指令而已,不会增大代码的体积的,而且GCC会有相应的变量读取优化;最后,那个在头文件中最好别定义C函数,除非是inline函数。
总之废话了那么多,关键的一行语句就是: LOCK “addl %1,%0”;这里%1和%0分别对应i和 *v,这就等价于 LOCK “addl &i, &v ”;这也就是我们前面分析的那样。
41 static __inline__ void atomic_sub(int i, volatile atomic_t *v)
__asm__ __volatile__(
LOCK &subl %1,%0&
:&=m& (__atomic_fool_gcc(v))
:&ir& (i), &m& (__atomic_fool_gcc(v)));
49 static __inline__ void atomic_inc(volatile atomic_t *v)
__asm__ __volatile__(
LOCK &incl %0&
:&=m& (__atomic_fool_gcc(v))
:&m& (__atomic_fool_gcc(v)));
57 static __inline__ void atomic_dec(volatile atomic_t *v)
__asm__ __volatile__(
LOCK &decl %0&
:&=m& (__atomic_fool_gcc(v))
:&m& (__atomic_fool_gcc(v)));
上面的atomic_sub和atomic_int还有atomic_dec也是一样的道理。大家是不是也能看懂呢?
65 static __inline__ int atomic_dec_and_test(volatile atomic_t *v)
__asm__ __volatile__(
LOCK &decl %0; sete %1&
:&=m& (__atomic_fool_gcc(v)), &=qm& (c)
:&m& (__atomic_fool_gcc(v)));
return c != 0;
76 extern __inline__ int atomic_inc_and_test_greater_zero(volatile atomic_t *v)
__asm__ __volatile__(
LOCK &incl %0; setg %1&
:&=m& (__atomic_fool_gcc(v)), &=qm& (c)
:&m& (__atomic_fool_gcc(v)));
84 /* can be only 0 or 1 */
这里稍微说一下,这两个函数名字这末长,意思大家应该能猜出来。没错,第一个就是把v指针指向的atomic_t减一,如果v变成了0,那么char c保存的值就为1,即return c != 0;这个表达试的值为1,否则为0。这里sete指令是 SETifEqual的指令,如果前面那个decl指令减到了0,后面那个%1,即c,会被置为1。第二个略。
87 /* These are x86-specific, used by some header files */
// 这些函数是X86特定的,在一些头文件中被使用。
88 #define atomic_clear_mask(mask, addr) \
89 __asm__ __volatile__(LOCK &andl %0,%1& \
90 : : &r& (~(mask)),&m& (__atomic_fool_gcc(addr)) : &memory&)
92 #define atomic_set_mask(mask, addr) \
93 __asm__ __volatile__(LOCK &orl %0,%1& \
94 : : &r& (mask),&m& (__atomic_fool_gcc(addr)) : &memory&)
97 以上两个用C语言写差不多类似于:
#define atomic_clear_mask(mask, addr) & & mask & addr
#define atomic_set_mask(mask, addr) & & & & & &maks | &addr&
只是没有LOCK前缀而已。这两个位操作宏还是用得比较多的。
我觉得要想理解内核,汇编还是得学好的,其实汇编也不难,而且会X86汇编就行了。其他的比如GTD,LDT,TSS,LTB,IDT,PIC,IO-APIC,FDC不知比汇编高到哪里去了,我也和他们谈笑风生。基本功还是得夯实的。
之后我应该会介绍介绍内核中其他的锁结构,它们的底层实现或多或少都出现了atomic_t的身影,具体来说就是用了LOCK 锁存前缀,而且还要开始考虑中断问题了。本文没有考虑中断是因为中断是以指令为边界的,也就是说不管有没有LOCK前缀,中断都只能发生在addl指令之前或之后,绝不能发生在addl操作中间的。后面讲的锁都比较大, 不仅仅像LOCK锁住一条指令,它们锁住的是一个代码段,这个代码段就叫说 &Critical Section,是不是见过这个词呢?嗯嗯,我们下回再分解吧!
如果有什么问题或是疑问,欢迎指正哈!错别字也行,蛤蛤!
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:112次
排名:千里之外经验3579 米
在线时间52 小时
版本7.6.22
机型红米Note4X
签到次数99
MIUI版本7.6.22
通过手机发布
miui6内测版的一些bug,比如收不到短信,跳电,可以进入Recovery 清除全部数据,重启后就好了。反正我是这样好的。
还有手机U盘不能用的问题,其实是能用,需要你进入文件管理器的主界面去看,那里把U盘单独放置了。
分享到微信朋友圈
打开微信,点击底部的“发现”,使用 “扫一扫” 即可将网页分享到我的朋友圈。
经验2285 米
在线时间101 小时
版本V7.2.4.0.MAACNDB
机型小米手机5
签到次数45
MIUI版本V7.2.4.0.MAACNDB
MIUI因你更精彩!
经验1978 米
在线时间48 小时
版本V7.3.2.0.MXDCNDD
积分 2254, 距离下一级还需 2746 积分
积分 2254, 距离下一级还需 2746 积分
机型小米手机3/4 WCDMA版
签到次数28
MIUI版本V7.3.2.0.MXDCNDD
我卡刷线刷全跳,,
经验1692 米
在线时间103 小时
版本V8.5.2.0.NAACNED
积分 1971, 距离下一级还需 29 积分
积分 1971, 距离下一级还需 29 积分
机型小米手机5
签到次数71
MIUI版本V8.5.2.0.NAACNED
通过手机发布
经验1429 米
在线时间25 小时
版本5.8.19
积分 1555, 距离下一级还需 445 积分
积分 1555, 距离下一级还需 445 积分
机型红米手机1S WCDMA 3G版
签到次数98
MIUI版本5.8.19
我也是这样干的 貌似2天没有跳电 电池是德赛的 用座充充满的
经验3074 米
在线时间119 小时
版本7.7.18
机型小米Note 顶配版
签到次数66
MIUI版本7.7.18
通过手机发布
只能说解决部分问题,而有些问题一直都存在
经验4824 米
在线时间97 小时
版本7.6.25
机型红米Note4X
签到次数142
MIUI版本7.6.25
通过手机发布
谢谢分享,
经验4873 米
在线时间65 小时
机型小米Note
签到次数73
MIUI版本7.6.9
可以啊…………
经验1144 米
在线时间49 小时
版本5.9.24
积分 1584, 距离下一级还需 416 积分
积分 1584, 距离下一级还需 416 积分
机型红米手机1S-WCDMA/CDMA
MIUI版本5.9.24
通过手机发布
红米电信直接下载内测包卡刷包官方rec升级?
经验972 米
在线时间20 小时
积分 1101, 距离下一级还需 899 积分
积分 1101, 距离下一级还需 899 积分
机型小米Note 移动4G/联通4G
签到次数32
MIUI版本7.2.9
miui因你更精彩
“澎湃S1 ”芯片纪念勋章
参与活动回帖可得
参与红米Note 4X活动
APP 1000万
MIUI论坛APP注册用户突破1000万纪念勋章
已关注极客秀微信
已关注微信
关注腾讯微博
已关注腾讯微博
关注新浪微博
已关注新浪微博
MIUI五周年
MIUI五周年纪念勋章
Copyright (C) 2017 MIUI
京ICP备号 | 京公网安备34号 | 京ICP证110507号

我要回帖

更多关于 atomic blonde 的文章

 

随机推荐