免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 5147 | 回复: 9
打印 上一主题 下一主题

[内核同步] 原子操作如何保证原子性 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2013-09-07 16:24 |只看该作者 |倒序浏览
本帖最后由 ycnian 于 2013-09-07 16:29 编辑

大家好,我正在看内核同步部分的代码,但是没有弄清楚原子操作到底是怎么回事。举个例子来说,atomic_add()可以实现原子加操作,代码如下。
static inline void atomic_add(int i, atomic_t *v)
{
        asm volatile(LOCK_PREFIX "addl %1,%0"
                     : "+m" (v->counter)
                     : "ir" (i));
}

其中LOCK_PREFIX定义如下:
#ifdef CONFIG_SMP
#define LOCK_PREFIX_HERE \
                ".section .smp_locks,\"a\"\n"   \
                ".balign 4\n"                   \
                ".long 671f - .\n" /* offset */ \
                ".previous\n"                   \
                "671:"
#define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
#else /* ! CONFIG_SMP */
#define LOCK_PREFIX_HERE ""
#define LOCK_PREFIX ""
#endif

先不考虑SMP了,只看单处理器的情况,这时候可以直接忽略掉LOCK_PREFIX。这种情况下,我对atomic_add()进行了汇编,结果如下:
00000000 <atomic_add>:
(第1条指令)   0:        55                      push   %ebp
(第2条指令)   1:        89 e5                   mov    %esp,%ebp
(第3条指令)   3:        8b 45 0c                mov    0xc(%ebp),%eax   // 这是v->counter的地址.
(第4条指令)   6:        8b 55 08                mov    0x8(%ebp),%edx   // 这是变量i的值
(第5条指令)   9:        8b 4d 0c                mov    0xc(%ebp),%ecx   // 可能是编译器的问题,这条指令可以忽略
(第6条指令)   c:        01 10                   add    %edx,(%eax)      // 求和,并把结果写到v->counter中.
(第7条指令)   e:        5d                      pop    %ebp
(第8条指令)   f:        c3                      ret

也就是说atomic_add()汇编的结果是8条指令。假设v的初始值是3,现在两个进程分别执行原子加操作: 进程1 atomic_add(2, v)    进程2 atomic_add(5, v)。
进程1先执行,当执行完第5条指令后发生了进程切换,此时v中的值还是3。进程2接着执行,且中间没有发生进程切换,当进程2执行完毕后v的值变成了8。现在进程1接着执行第6条指令 “add    %edx,(%eax)”。寄存器eax中保存的是v的地址,CPU将执行三个操作:1.将v的值读取到寄存器中(现在v的值已经是8了),2.求和(结果是10),3.将v的值写到内存中。这样的话,对于进程1来说,执行atomic_add(2, v)前v的值是3,执行后的值是10了,这还能叫原子操作吗?

不知道是我的理解有误,还是怎么回事?大家怎么理解的?非常感谢。我看的内核版本是3.6.0。

论坛徽章:
0
2 [报告]
发表于 2013-09-07 16:53 |只看该作者
可不可以这么理解:这些原子操作不是针对进程的,而是针对整个系统的。虽然对于进程1来说atomic_add()的结果不对,但是对于整个系统来说,最后的结果是10,这是正确的。内核中的原子操作是为了避免SMP系统中的异常情况,因为多个处理器可以同时操作内存单元。比如进程1在CPU1上执行,进程2在CPU2上执行。这种情况下如果不加lock的话,add就不能认为是原子操作了。因为add包含了读、求和、写三个步骤。进程1执行读、求和操作(结果是5),进程2执行读、求和操作(结果是8),进程1执行写操作(结果是5),进程2执行写操作(结果是8)。这样的话对于整个系统来说,结果就错了。因此SMP系统中需要使用lock。

论坛徽章:
0
3 [报告]
发表于 2013-09-07 18:27 |只看该作者
接着说我现在的理解。进程1执行atomic_add(2, v)之前v的值是2,执行之后v的值是10。这个结果看似错了,其实没有错。我们看汇编后生成的8条指令,按照前面的例子,进程1执行完第5条指令后发生了进程切换,但是其实这时候atomic_add(2, v)还没有读取v的值,前几条指令只获取了v的地址,可以看作一些准备操作。当进程再次切换回来后进程1接着执行"add    %edx,(%eax)"。因为这时候v的实际值是8,进程1当然要在v当前值的基础上加2,只要保证“读数据->求和->写数据”这个步骤不被其他进程或者其他CPU打搅就行了,这就叫原子操作。不过程序中需要注意:

.......
v = 3;
atomic_add(2, v);
// 现在v的值不一定是5
.......

论坛徽章:
0
4 [报告]
发表于 2013-09-07 18:50 |只看该作者
不过我又有新问题了,看另外一个函数int atomic_dec_and_test(atomic_t *v),这个函数对v进行减1操作。如果v减到0了,函数返回true,否则返回false。当v减到0时表示系统中某个资源已经空闲不用了,可以删除这个资源了。这个函数汇编后的代码如下:
00000000 <atomic_dec_and_test>:
(第1条指令)   0:        55                      push   %ebp
(第2条指令)   1:        89 e5                   mov    %esp,%ebp        // 寄存器ebp中保存栈帧
(第3条指令)   3:        53                      push   %ebx
(第4条指令)   4:        83 ec 10                sub    $0x10,%esp       // 这里留出16字节保存局部变量
(第5条指令)   7:        8b 45 08                mov    0x8(%ebp),%eax   // v的地址
(第6条指令)   a:        8b 55 08                mov    0x8(%ebp),%edx   // 忽略
(第7条指令)   d:        f0 ff 08                decl   (%eax)           // v--
(第8条指令)  10:        0f 94 c3                sete   %bl              // 如果ZF的值为1就将bl设置为1 (如果v的计数减到0了就将bl设置为1).
(第9条指令)  13:        88 5d fb                mov    %bl,-0x5(%ebp)   // 将bl的值保存到堆栈中
(第10条指令)  16:       80 7d fb 00             cmpb   $0x0,-0x5(%ebp)  // 比较bl是否为0  (b的实际值是1)
(第11条指令)  1a:       0f 95 c0                setne  %al              // 如果bl!=0,就将al设置为1;否则将al设置为0. (显然,现在al=1)
(第12条指令)  1d:       0f b6 c0                movzbl %al,%eax         // 将寄存器eax高24位设置为0.
(第13条指令)  20:       83 c4 10                add    $0x10,%esp
(第14条指令)  23:       5b                      pop    %ebx
(第15条指令)  24:       5d                      pop    %ebp
(第16条指令)  25:       c3                      ret

其中第7条指令执行v--操作,并且把结果写到内存中了。假设执行atomic_dec_and_test()之前v的值是1,那么现在v的值就是0了。同样,进程可以在这个时候被切换(执行了第7条指令,没有执行第8条指令)。假设切换到了另一个进程,并且另一个进程执行了atomic_inc()(这个函数执行v++),这时v==1,表示这个资源仍然在使用,然后切换回开始的进程。由于从8条指令开始,后面的指令已经跟v没有关系了。根据第8-16条指令的执行过程可以看出,最后atomic_dec_and_test()的结果是true。true表示这个资源已经不被使用了,可以释放了(比如清除内存等)。但是实际上v的值是1,因为有另外一个进程在使用这个资源。如果把这个资源释放了,那么不就出错了吗??????

论坛徽章:
0
5 [报告]
发表于 2013-09-07 19:06 |只看该作者
本帖最后由 ycnian 于 2013-09-07 19:12 编辑

其实不用考虑4楼这么极端的情况。一般情况下,资源释放不会是原子操作

........
if (atomic_dec_and_test()) {
    // 开始释放资源
    // 仍然在释放资源
    .......
    // 资源释放完了
    return;
}
........

进程在释放资源的过程中同样可能被切换到另外一个进程,另外一个进程执行atomic_inc(),开始使用这个资源。切换回原来的进程后,继续释放资源,就出错了。内核不会这么弱智的,应该在其他层面进行了保护,只是我还没有看到而已(估计是自旋锁或者信号量),接着看代码。也欢迎大家帮我梳理梳理,谢谢。

论坛徽章:
0
6 [报告]
发表于 2013-09-08 08:13 |只看该作者
原子性都是cpu汇编指令保证的,和编程一样检查标志位,然后cpu内部多核保持一致

单核不用保证原子性

论坛徽章:
0
7 [报告]
发表于 2013-09-08 12:32 来自手机 |只看该作者
按照定义,atomic操作就应该是不被中断打断也不会有上下文的切换,否则就不叫原子操作可。会不会楼主的反汇编代码有误。理论上讲,这么多步的操作,要想达到atomic效果,至少应该有关中断的指令

论坛徽章:
17
水瓶座
日期:2013-08-29 12:09:27白羊座
日期:2014-08-07 12:36:42丑牛
日期:2014-07-24 12:44:41寅虎
日期:2014-04-16 16:15:33寅虎
日期:2014-03-12 09:28:43摩羯座
日期:2014-03-06 13:22:04技术图书徽章
日期:2014-03-06 11:34:50天蝎座
日期:2014-01-09 11:31:44寅虎
日期:2013-12-27 17:01:44双子座
日期:2013-12-27 12:32:29双子座
日期:2013-12-25 09:03:33丑牛
日期:2013-12-24 16:18:44
8 [报告]
发表于 2013-09-08 12:49 |只看该作者
static inline int atomic_add_return(int i, atomic_t *v)
{
        unsigned long flags;
        int val;

        raw_local_irq_save(flags);
        val = v->counter;
        v->counter = val += i;
        raw_local_irq_restore(flags);

        return val;
}
#define atomic_add(i, v)        (void) atomic_add_return(i, v)


摘抄于3.10.1,arch/arm/include/asm/atomic.h,不知道LZ是哪个内核版本的代码?

论坛徽章:
0
9 [报告]
发表于 2013-09-08 15:42 |只看该作者
回复 8# asuka2001
我看的X86架构的代码,内核版本是3.6.0,我发现自己以前对内核同步的理解有误。


   

论坛徽章:
0
10 [报告]
发表于 2013-09-10 10:02 |只看该作者
ycnian 发表于 2013-09-07 18:50
不过我又有新问题了,看另外一个函数int atomic_dec_and_test(atomic_t *v),这个函数对v进行减1操作。如果 ...


所以atomic_inc的返回值必须大于1才行,如果atomic_inc都是1,那么你引用的资源本来就是0,那个资源早就是无效的了。
常用做的get 和put 操作的指针的引用计数时,这个是可以保证的,因为get的时候肯定是从另外一个有效的指针来做atomic_inc的,
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP