所谓原子操作,就是“不可中断的一个或一系列操作”。

硬件级的原子操作:在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。在多处理器结构中(Symmetric Multi-Processor)就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在X86平台生,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。对于其他平台的CPU,实现各不相同,有的是通过关中断来实现原子操作(sparc),有的通过CMPXCHG系列的指令来实现原子操作(IA64)。本文主要探讨X86平台下原子操作的实现。

软件级别的原子操作:软件级别的原子操作实现依赖于硬件原子操作的支持。

Linux内核提供了两组原子操作接口:一组是针对整数进行操作;另一组是针对单独的位进行操作。

1、原子整数操作

原子操作通常针对int或bit类型的数据,但是Linux并不能直接对int进行原子操作,而只能通过atomic_t的数据结构来进行。目前了解到的原因有两个。

一是在老的Linux版本,atomic_t实际只有24位长,低8位用来做锁。这是由于Linux是一个跨平台的实现,可以运行在多种 CPU上,有些 类型的CPU比如SPARC并没有原生的atomic指令支持,所以只能在32位int使用8位来做同步锁,避免多个线程同时访问。(最新版SPARC实现已经突破此限制)。 原子整数操作最常见的用途就是实现计数器。常见的用法是:

atomic_t use_cnt;

atomic_set(&use_cnt, 2);

atomic_add(4, &use_cnt);

atomic_inc(use_cnt);

在X86平台上,atomic_t定义如下:

[cpp] view plain copy
  1. typedef struct {
  2. int counter;
  3. } atomic_t;

下面选取atomic_add来进行分析:

[cpp] view plain copy
  1. static inline void atomic_add(int i, atomic_t *v)
  2. {
  3. asm volatile(LOCK_PREFIX "addl %1,%0"
  4. : "+m" (v->counter)
  5. : "ir" (i));
  6. }

可以看到,atomic_add使用了gcc提供的内嵌汇编来实现,是用一个addl指令来实现增加操作。重点看一下LOCK_PREFIX宏,它就是上文提到的锁总线操作,也就是它保证了操作的原子性。LOCK_PREFIX定义如下:

[cpp] view plain copy
  1. #define LOCK_PREFIX \
  2. ".section .smp_locks,\"a\"\n"   \
  3. "  .align 4\n"                  \
  4. "  .long 661f\n" /* address */  \
  5. ".previous\n"                   \
  6. "661:\n\tlock; "

展开后变成:

[cpp] view plain copy
  1. .section .smp_locks,"a"
  2. .align 4
  3. .long 661f
  4. .previous
  5. 661:
  6. lock;

逐条解释如下:
.section .smp_locks,"a"
下面的代码生成到 .smp_locks 段里,属性为"a", allocatable
  .align 4
四字节对齐
  .long 661f
生成一个整数,值为下面的 661 标号的实际地址,f 表示向前引用,如果 661 标号出现
在前面,要写 661b。
.previous
代码生成恢复到原来的段,也就是 .text
661:
数字标号是局部标号,5.3 Symbol Names
        lock;
开始生成指令,lock 前缀
这段代码汇编后,在 .text 段生成一条 lock 指令前缀 0xf0,在 .smp_locks 段生成四个字节的 lock 前缀的地址,链接的时候,所有的.smp_locks 段合并起来,形成一个所有 lock 指令地址的数组,这样统计 .smp_locks 段就能知道代码里有多少个加锁的指令被生成,猜测是为了调试目的。

搜索了一下,找到了相关引用处,当一个内核模块被加载时,会调用module_finalize函数:

[cpp] view plain copy
  1. int module_finalize(const Elf_Ehdr *hdr,
  2. const Elf_Shdr *sechdrs,
  3. struct module *me)
  4. {
  5. const Elf_Shdr *s, *text = NULL, *alt = NULL, *locks = NULL,
  6. *para = NULL;
  7. char *secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset;
  8. for (s = sechdrs; s < sechdrs + hdr->e_shnum; s++) {
  9. if (!strcmp(".text", secstrings + s->sh_name))
  10. text = s;
  11. if (!strcmp(".altinstructions", secstrings + s->sh_name))
  12. alt = s;
  13. if (!strcmp(".smp_locks", secstrings + s->sh_name))
  14. locks= s;
  15. if (!strcmp(".parainstructions", secstrings + s->sh_name))
  16. para = s;
  17. }
  18. if (alt) {
  19. /* patch .altinstructions */
  20. void *aseg = (void *)alt->sh_addr;
  21. apply_alternatives(aseg, aseg + alt->sh_size);
  22. }
  23. if (locks && text) {
  24. void *lseg = (void *)locks->sh_addr;
  25. void *tseg = (void *)text->sh_addr;
  26. alternatives_smp_module_add(me, me->name,
  27. lseg, lseg + locks->sh_size,
  28. tseg, tseg + text->sh_size);
  29. }
  30. if (para) {
  31. void *pseg = (void *)para->sh_addr;
  32. apply_paravirt(pseg, pseg + para->sh_size);
  33. }
  34. return module_bug_finalize(hdr, sechdrs, me);
  35. }

上面的代码说,如果模块有 .text 和 .smp_locks 段,就调这个来处理,做什么呢?

[cpp] view plain copy
  1. void alternatives_smp_module_add(struct module *mod, char *name,
  2. void *locks, void *locks_end,
  3. void *text,  void *text_end)
  4. {
  5. struct smp_alt_module *smp;
  6. if (noreplace_smp)
  7. return;
  8. if (smp_alt_once) {
  9. if (boot_cpu_has(X86_FEATURE_UP))
  10. alternatives_smp_unlock(locks, locks_end,
  11. text, text_end);
  12. return;
  13. }
  14. ........//省略无关代码
  15. }

上面的代码说,如果是单处理器(UP),就调这个:

[cpp] view plain copy
  1. static void alternatives_smp_unlock(u8 **start, u8 **end, u8 *text, u8 *text_end)
  2. {
  3. u8 **ptr;
  4. char insn[1];
  5. if (noreplace_smp)
  6. return;
  7. add_nops(insn, 1);
  8. for (ptr = start; ptr < end; ptr++) {
  9. if (*ptr < text)
  10. continue;
  11. if (*ptr > text_end)
  12. continue;
  13. text_poke(*ptr, insn, 1);
  14. };
  15. }

看到这里就能明白,这是内核配置了 smp,但是实际运行到单处理器上时,通过运行期间打补丁,根据 .smp_locks 里的记录,把 lock 指令前缀替换成 nop 以消除指令加锁的开销,这个优化真是极致了……,可能考虑很多用户直接使用的是配置支持 SMP 编译好的内核而特地对 x86/x64 做的这个优化。

2、原子位操作的实现

编写代码时,以如下的方式进行操作

unsigned long word = 0;

set_bit(0, &word); /*第0位被设置*/

set_bit(1, &word); /*第1位被设置*/

clear_bit(1, &word); /*第1位被清空*/

change_bit(0, &word); /*翻转第0位*/

为什么关注原子操作?
1)在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能开销昂贵的锁。
2)借助于原子操作,我们可以实现互斥锁。
3)借助于互斥锁,我们可以把一些列操作变为原子操作。

我们重点关注一下以下两个函数的实现:

[cpp] view plain copy
  1. /**
  2. * clear_bit - Clears a bit in memory
  3. * @nr: Bit to clear
  4. * @addr: Address to start counting from
  5. *
  6. * clear_bit() is atomic and may not be reordered.  However, it does
  7. * not contain a memory barrier, so if it is used for locking purposes,
  8. * you should call smp_mb__before_clear_bit() and/or smp_mb__after_clear_bit()
  9. * in order to ensure changes are visible on other processors.
  10. */
  11. static inline void clear_bit(int nr, volatile void *addr)
  12. {
  13. asm volatile(LOCK_PREFIX "btr %1,%0" : ADDR : "Ir" (nr));
  14. }
[cpp] view plain copy
  1. /*
  2. * clear_bit_unlock - Clears a bit in memory
  3. * @nr: Bit to clear
  4. * @addr: Address to start counting from
  5. *
  6. * clear_bit() is atomic and implies release semantics before the memory
  7. * operation. It can be used for an unlock.
  8. */
  9. static inline void clear_bit_unlock(unsigned nr, volatile void *addr)
  10. {
  11. barrier();
  12. clear_bit(nr, addr);
  13. }

第一个clear_bit函数比较好理解,和上面atomic系列的函数实现类似。但是注意到clear_bit_unlock函数中多了一个barrier函数,这是什么操作呢?
这就是有名的“内存屏障“或”内存栅栏“操作,先来补充一下这方面的知识。

可以看一下barrier的定义:

[cpp] view plain copy
  1. #define barrier() __asm__ __volatile__("": : :"memory")

解释一下:__volatitle__是防止编译器移动该指令的位置或者把它优化掉。"memory",是提示编译器该指令对内存修改,防止使用某个寄存器中已经load 的内存的值。lock 前缀是让cpu 的执行下一行指令之前,保证以前的指令都被正确执行。

事实上,不止barrier,还有一个mb系列的函数也起着内存屏障的功能:

[cpp] view plain copy
  1. #include <asm/system.h>
  2. "void rmb(void);"
  3. "void wmb(void);"
  4. "void mb(void);"

这些函数在已编译的指令流中插入硬件内存屏障,具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier 函数的超集。解释一下:编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对I/O 操作进行类似优化很可能造成致命错误。所以要使用内存屏障,以强制该语句前后的指令以正确的次序完成。

其实在指令序列中放一个wmb 的效果是使得指令执行到该处时,把所有缓存的数据写到该写的地方,同时使得wmb 前面的写指令一定会在wmb后面 的写指令之前执行。

回到上面的函数,当clear_bit函数不用于实现锁的目的时,不用给它加上内存屏障(我的理解:不管是不是读到最新的数据,这一位就是要清零,不管加不加内存屏障,结果都是一样的);而当用于实现锁的目的时,必须使用clear_bit_unlock函数,其实现中使用了内存屏障,以此来确保此处的修改能在其他CPU上看到(我的理解:加锁操作就是为了在多个CPU间进行同步的目的,所以要避免寄存器优化,其他CPU每次都读内存这样才能看到最新的变化,这块不是太明白)。这种操作也叫做serialization,即在执行这条指令前,CPU必须要完成前面所有对memory的访问指令(read and write),这样是为了避免编译器进行某些优化。

同样使用serialization操作的还有test_and_set_bit函数:

[cpp] view plain copy
  1. /**
  2. * test_and_set_bit - Set a bit and return its old value
  3. * @nr: Bit to set
  4. * @addr: Address to count from
  5. *
  6. * This operation is atomic and cannot be reordered.
  7. * It also implies a memory barrier.
  8. */
  9. static inline int test_and_set_bit(int nr, volatile void *addr)
  10. {
  11. int oldbit;
  12. asm volatile(LOCK_PREFIX "bts %2,%1\n\t"
  13. "sbb %0,%0" : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");
  14. return oldbit;
  15. }

解释一下:
1)memory 强制gcc 编译器假设RAM 所有内存单元均被汇编指令修改,这样cpu 中的registers 和cache 中已缓存的内存单元中的数据将作废。cpu 将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu 又将registers,cache 中的数据用于去优化指令,而避免去访问内存。

2)sbb  $0,0(%%esp)表示将数值0 减到esp 寄存器中,而该寄存器指向栈顶的内存单元。减去一个0,esp 寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock 指令,在__asm__,__volatile__,memory 的作用下,用作cpu 的内存屏障。

这种写法和前面的clear_bit_unlock中先写一个barrier函数,再写一个正常内嵌汇编函数的功能是一样的。

【Linux】linux内核原子操作的实现相关推荐

  1. linux系统内核文百科,Linux之内核中的文件系统 -电脑资料

    文件描述符 一般说起文件和文件系统的时候,都会下意识的想到它们存在于磁盘上,管理各种文件呢?即运行时文件系统在内核中的表示. 我们知道,进程是操作系统分配资源的基本单位,文件也是在进程中被处理的.比如 ...

  2. linux c内核开发,嵌入式uClinux的内核结构和开发环境

    1 引言 嵌入式操作系统是嵌入式系统的灵魂,而且在同一个硬件平台上可以嵌入不同的嵌入式操作系统.比如ARM7TDMI内核,可以嵌入Nucleus.VxWorks.uClinux等操作系统.在此主要对u ...

  3. linux内核报告,Linux升级内核报告.docx

    Linux升级内核报告精要 我的Linux 内核升级记录 准备工作内核安装包的下载下载地址为: HYPERLINK "/pub/linux/kernel/" /pub/linux/ ...

  4. linux 内核 ntfs,Linux大脑 内核 内核编译(NTFS)

    Linux大脑 "内核" 关于它 什么是内核 kernel(内核)是操作系统的核心,相当于人的大脑,掌控所有的硬件设备的控制权,也就是希望计算机帮你完成各项工作,那都需要通过内核的 ...

  5. linux pti性能影响,Linux修正内核:Intel打补丁性能狂降、AMD不受影响

    Linux修正内核:Intel打补丁性能狂降.AMD不受影响 由于Meltdown和Spectre两个严重内核级漏洞造成的安全事件愈演愈烈,其中不可否认的是,搭载Intel处理器的Linux服务器.数 ...

  6. linux追加内核参数,Linux设置内核参数的方法

    1 内核参数的查看方法 使用"sysctl -a"命令可以查看所有正在使用的内核参数.内核参数比较多(一般多达500项),按照前缀主要分为以下几大类:net.ipv4.net.ip ...

  7. linux img 内核启动,linux的启动流程(initrd.img)

    http://www.ibm.com/developerworks/cn/linux/l-initrd.html 一.从哪里到哪里 本文旨在描述linux中内核如何调用启动,然后如何从img的文件系统 ...

  8. 配置树莓派linux的内核和编译并将镜像拷贝至树莓派

    驱动代码的编写需要一个提前编译好的内核,编译内核就必须配置,配置的最终目标会生成.config文件,该文件指导makefile去把有用的东西组织成内核. 如何生成.config文件: 第一种方式: 厂 ...

  9. Linux升级内核的正确姿势

    Linux升级内核的正确姿势 很多童鞋在玩耍linux发行版的时候,都会遇到各种各样的问题,比如:网卡不能使用,亮度不能调节,触摸板不能识别,蓝牙不能使用等等,这些关系都和linux的内核有关系. 什 ...

  10. linux 的内核参数优化,Linux服务器内核参数优化

    Linux服务器内核参数优化 cat >> /etc/sysctl.conf << EOF #kernel optimization net.ipv4.tcp_fin_time ...

最新文章

  1. sql序列(2) sql语句功能表
  2. Ubuntu返回到Gnome经典桌面!
  3. 【蓝桥杯-第五届】 啤酒和饮料
  4. 过拟合与模型调优(part2)--重抽样技术
  5. oracle+标记要,oracle ORA-00031:session marked for kill(标记要终止的会话)解决方法
  6. oracle分区存储过程示例,Oracle 存储过程示例
  7. 休息是为了更好的出发
  8. 深度学习(7) - 长短时记忆网络(LSTM)
  9. 对Python列表进行封装和二次开发实现自定义栈结构
  10. 使用 rtcwake 定时唤醒休眠的linux
  11. nginx limit_req限速设置
  12. 停机坪上的飞机有可能被偷走吗?
  13. Python日报0507 - PyQt5实现打卡登记系统
  14. php eval 引号,PHP手册-eval()(可以将单引号中的变量解析)
  15. python自动化:uiautomation、pyautogui操作会计记账系统(6):打印会计凭证
  16. java 确认邮箱地址的可达性
  17. 前序、中序、后序排列
  18. go调用python
  19. jupyter notebook中使用matplotlib的相关问题
  20. Java中的 if条件语句

热门文章

  1. apollo7.0------浅谈激光雷达运动补偿(二)--计算解析
  2. 运筹帷幄的“懒蚂蚁”
  3. 微信公众号——创建标签,给粉丝打标签。
  4. springMVC源码之组件介绍
  5. 华为鸿蒙系统问世微信红包,一个巨头的诞生 华为鸿蒙车机系统问世
  6. php 统计汉字,PHP 统计实时统计汉字个数和区别
  7. [Erlang危机](3.1)常见过载情景
  8. 分别编写两个类Point2D,Point3D来表示二维空间和三维空间的点,使之满足下列要求
  9. 用友NC65产品的对账节点联查业务帐
  10. 基于android的互动健身平台,基于Android和ARM的智能健身系统的设计与实现