tag 标签: Linux内核锁

相关博文
  • 热度 6
    2023-6-6 22:55
    1184 次阅读|
    0 个评论
    1、原子操作思想 原子操作 (atomic operation) ,不可分割的操作。其通过原子变量来实现,以保证单个 CPU 周期内,读写该变量,不能被打断,进而判断该变量的值,来解决并发引起的互斥。 Atomic 类型的函数可以在执行期间禁止中断,并保证在访问变量时的原子性。 同时, Linux 内核提供了两类原子操作的接口,分别是针对 位 和 整型变量 的原子操作。 2、整型变量原子操作 2.1 API接口 对于整形变量的原子操作,内核提供了一系列的 API 接口 /*设置原子变量的值*/ atomic_t v = ATOMIC_INIT ( 0 ); /* 定义原子变量v并初始化为0 */ void atomic_set ( atomic_t * v , int i ); /* 设置原子变量的值为i */ ​ /*获取原子变量的值*/ atomic_read ( atomic_t * v ); /* 返回原子变量的值*/ ​ /*原子变量的加减*/ void atomic_add ( int i , atomic_t * v ); /* 原子变量增加i */ void atomic_sub ( int i , atomic_t * v ); /* 原子变量减少i */ ​ /*原子变量的自增,自减*/ void atomic_inc ( atomic_t * v ); /* 原子变量增加1 */ void atomic_dec ( atomic_t * v ); /* 原子变量减少1 */ ​ /*原子变量的操作并测试*/ int atomic_inc_and_test ( atomic_t * v ); /*进行对应操作后,测试原子变量值是否为0*/ int atomic_dec_and_test ( atomic_t * v ); int atomic_sub_and_test ( int i , atomic_t * v ); ​ /*原子变量的操作并返回*/ int atomic_add_return ( int i , atomic_t * v ); /*进行对应操作后,返回新的值*/ int atomic_sub_return ( int i , atomic_t * v ); int atomic_inc_return ( atomic_t * v ); int atomic_dec_return ( atomic_t * v ); 2.2 API实现 我们下面就介绍几个稍微有代表性的接口实现 以下基于 Linux 内核源码 4.19 ,刚看是看的时候,有点摸不着头脑,因为定义的地方和引用的地方较多,不太容易找到,后来才慢慢得窥门径。 2.2.1 原子变量结构体 typedef struct { int counter ; } atomic_t ; 结构体名称 : atomic_t 文件位置 : include/linux/types.h 主要作用 :原子变量结构体,该结构体只包含一个整型成员变量 counter ,用于存储原子变量的值。 2.2.2 设置原子变量操作 2.2.2.1 ATOMIC_INIT #define ATOMIC_INIT(i) { (i) } 函数介绍 :定义了一个ATOMIC类型的变量,并初始化为给定的值。 文件位置 : arch/arm/include/asm/atomic.h ,由 include/linux/atomic.h 引用 实现方法 :这个宏定义比较简单,通过大括号将值包裹起来作为一个结构体,结构体的第一个成员就用就是给定的该值。 2.2.2.2 atomic_set #define atomic_set(v,i) counter), (i)) ​ #define WRITE_ONCE(x, val) \ ({ \ union { typeof(x) __val; char __c ; } __u = \ { .__val = (__force typeof(x)) (val) }; \ __write_once_size(&(x), __u.__c, sizeof(x)); \ __u.__val; \ }) ​ static __always_inline void __write_once_size ( volatile void * p , void * res , int size ) { switch ( size ) { case 1 : * ( volatile __u8 * ) p = * ( __u8 * ) res ; break ; case 2 : * ( volatile __u16 * ) p = * ( __u16 * ) res ; break ; case 4 : * ( volatile __u32 * ) p = * ( __u32 * ) res ; break ; case 8 : * ( volatile __u64 * ) p = * ( __u64 * ) res ; break ; default : barrier (); __builtin_memcpy (( void * ) p , ( const void * ) res , size ); barrier (); } } 函数介绍 :该函数也用作初始化原子变量 文件位置 :由 include/linux/atomic.h 引用 arch/arm/include/asm/atomic.h ,再引用 include/linux/compiler.h 实现方式 :通过调用 WRITE_ONCE 来实现,其中 WRITE_ONCE 宏实现了一些屏蔽编译器优化的技巧,确保写入操作是原子的。 atomic_set 调用 WRITE_ONCE 将 i 的值写入原子变量 counter 中, WRITE_ONCE 以保证操作的原子性 WRITE_ONCE 用来保证操作的原子性 创建 union 联合体,包括 __val 和 __C 成员变量 定义一个 __U 变量,使用强制转换将参数 __val 转换为 typeof(x) 类型,传递给联合体变量 __u.__val 调用 __write_once_size 函数,将 __c 的值写入到 x 指向的内存地址中。 函数返回 __u.__val。 union 联合体 它的特点是存储多种数据类型的值,但是所有成员共享同一个内存空间,这样可以节省内存空间。 主要作用是将一个非字符类型的数据 x 强制转换为一个字符类型的数据,以字符类型数据来访问该区块的内存单元。 __write_once_size 函数实现了操作的原子性,核心有以下几点: 该函数在向内存写入数据时使用了 volatile 关键字,告诉编译器不要进行优化,每次操作都从内存中读取最新的值。 函数中的 switch 语句保证了对不同大小的数据类型使用不同的存储方式,可以保证内存访问的原子性。 对于默认情况,则使用了 __builtin_memcpy 函数进行复制,而这个函数具有原子性。 barrier() 函数指示 CPU 要完成所有之前的内存操作,以及确保执行顺序与其他指令不发生重排。 2.2.3 原子变量的加减 2.2.3.1 ATOMIC_OPS /* * ARMv6 UP and SMP safe atomic ops. We use load exclusive and * store exclusive to ensure that these are atomic. We may loop * to ensure that the update happens. */ ​ #define ATOMIC_OP(op, c_op, asm_op) \ static inline void atomic_##op(int i, atomic_t *v) \ { \ unsigned long tmp; \ int result; \ \ counter); \ __asm__ __volatile__("@ atomic_" #op "\n" \ "1: ldrex %0, \n" \ " " #asm_op " %0, %0, %4\n" \ " strex %1, %0, \n" \ " teq %1, #0\n" \ " bne 1b" \ counter) \ counter), "Ir" (i) \ : "cc"); \ } \ ​ #define ATOMIC_OP_RETURN(op, c_op, asm_op) \ static inline int atomic_##op##_return_relaxed(int i, atomic_t *v) \ { \ unsigned long tmp; \ int result; \ \ counter); \ \ __asm__ __volatile__("@ atomic_" #op "_return\n" \ "1: ldrex %0, \n" \ " " #asm_op " %0, %0, %4\n" \ " strex %1, %0, \n" \ " teq %1, #0\n" \ " bne 1b" \ counter) \ counter), "Ir" (i) \ : "cc"); \ \ return result; \ } ​ #define ATOMIC_FETCH_OP(op, c_op, asm_op) \ static inline int atomic_fetch_##op##_relaxed(int i, atomic_t *v) \ { \ unsigned long tmp; \ int result, val; \ \ counter); \ \ __asm__ __volatile__("@ atomic_fetch_" #op "\n" \ "1: ldrex %0, \n" \ " " #asm_op " %1, %0, %5\n" \ " strex %2, %1, \n" \ " teq %2, #0\n" \ " bne 1b" \ counter) \ counter), "Ir" (i) \ : "cc"); \ \ return result; \ } ​ #define ATOMIC_OPS(op, c_op, asm_op) \ ATOMIC_OP(op, c_op, asm_op) \ ATOMIC_OP_RETURN(op, c_op, asm_op) \ ATOMIC_FETCH_OP(op, c_op, asm_op) 找 atomic_add 找半天,还找到了不同的架构下面。:( 原来内核通过各种宏定义将其操作全部管理起来,宏定义在内核中的使用也是非常广泛了。 函数作用 :通过一些列宏定义,来实现原子变量的 add 、 sub 、 and 、 or 等原子变量操作 文件位置 : arch/arm/include/asm/atomic.h 实现方式 : 我们以 atomic_##op 为例来介绍,其他大同小异! #define ATOMIC_OP(op, c_op, asm_op) \ static inline void atomic_##op(int i, atomic_t *v) \ { \ unsigned long tmp; \ int result; \ \ counter); \ __asm__ __volatile__("@ atomic_" #op "\n" \ "1: ldrex %0, \n" \ " " #asm_op " %0, %0, %4\n" \ " strex %1, %0, \n" \ " teq %1, #0\n" \ " bne 1b" \ counter) \ counter), "Ir" (i) \ : "cc"); \ } 首先是函数名称 atomic_##op ,通过 ## 来实现字符串的拼接,使函数名称可变,如 atomic_add 、 atomic_sub 等 调用 prefetchw 函数,预取数据到 L1 缓存,方便操作,提高程序性能,但是不要滥用。 __asm__ __volatile__ :表示汇编指令 "@ atomic_" #op "\n" :为汇编注释 "1: ldrex %0, \n" :将 %3 存储地址的数据,读入到 %0 地址中, ldrex 为独占式的读取操作。 " " #asm_op " %0, %0, %4\n" : " #asm_op " 表示作为宏定义传进来的参数,表示不同的操作码 add 、 sub 等,操作 %0 和 %4 对应的地址的值,并将结果返回到 %0 地址处 " strex %1, %0, \n" :表示将 %0 地址处的值写入 %3 地址处, strex 为独占式的写操作,写入的结果会返回到 %1 地址中 " teq %1, #0\n" :测试 %1 寄存器的值是否为0,如果不等于0,则执行下面的 " bne 1b" 操作,跳转到 1 代码标签的位置,也就是 ldrex 前面的 1 的位置 counter) :根据汇编语法,前两个为输出操作数,第三个为输入输出操作数 counter), "Ir" (i) :根据汇编语法,这两个为输入操作数 : "cc" :表示可能会修改条件码寄存器,编译期间需要优化。 通过 ldrex 和 strex 两个独占式的操作,保证了读写的原子性。 2.2.3.2 atomic _ add和atomic _ sub定义 ATOMIC_OPS ( add , += , add ) ATOMIC_OPS ( sub , -= , sub ) 通过宏定义来实现 atomic_add 和 atomic_sub 的定义,下面我们就不一一分析了,原理都是通过 ARM 提供的 ldrex strex 也就是我们常说的 Load 和 Store 指令实现读取操作,确保操作的原子性。 3、位原子操作 3.1 API接口 void set_bit ( nr , void * addr ); // 设置位:设置addr地址的第nr位,所谓设置位即是将位写为1 void clear_bit ( nr , void * addr ); // 清除位:清除addr地址的第nr位,所谓清除位即是将位写为0 void change_bit ( nr , void * addr ); // 改变位:对addr地址的第nr位进行反置。 test_bit ( nr , void * addr ); // 测试位:返回addr地址的第nr位。 int test_and_set_bit ( nr , void * addr ); // 测试并设置位 int test_and_clear_bit ( nr , void * addr ); // 测试并清除位 int test_and_change_bit ( nr , void * addr ); // 测试并改变位 3.2 API实现 同样,我们还是简单介绍几个接口,其他核心实现原理相同 3.2.1 set_bit #define set_bit(nr,p) ATOMIC_BITOP(set_bit,nr,p) #define ATOMIC_BITOP(name,nr,p) \ (__builtin_constant_p(nr) ? ____atomic_##name(nr, p) : _##name(nr,p)) extern void _set_bit(int nr, volatile unsigned long * p); /* * These functions are the basis of our bit ops. * * First, the atomic bitops. These use native endian. */ static inline void ____atomic_set_bit(unsigned int bit, volatile unsigned long *p) { unsigned long flags; unsigned long mask = BIT_MASK(bit); p += BIT_WORD(bit); raw_local_irq_save(flags); *p |= mask; raw_local_irq_restore(flags); } #define BIT_MASK(nr) (1UL << ((nr) % BITS_PER_LONG)) #define BIT_WORD(nr) ((nr) / BITS_PER_LONG) #ifdef CONFIG_64BIT #define BITS_PER_LONG 64 #else #define BITS_PER_LONG 32 #endif /* CONFIG_64BIT */ 函数介绍 :该函数用于原子操作某个地址的某一位。 文件位置 : /arch/arm/include/asm/bitops.h 实现方式 : __builtin_constant_p : GCC 的一个内置函数,用来判断表达式是否为常量,如果为常量,则返回值为1 ____atomic_set_bit 函数中 BIT_MASK ,用于获取操作位的掩码,将要设置的位设置为1,其他为0 BIT_WORD :确定要操作位的偏移,要偏移多少个字 通过 raw_local_irq_save 和 raw_local_irq_restore 中断屏蔽来保证位操作 *p |= mask; 的原子性 4、总结 该文章主要详细了解了 Linux 内核锁的原子操作,原子操作分为两种:整型变量的原子操作和位原子操作。 整型变量的原子操作:通过 ldrex 和 strex 来实现 位原子操作:通过中断屏蔽来实现。
  • 热度 3
    2023-5-18 21:51
    1933 次阅读|
    0 个评论
    【Linux内核锁】一、内核锁的由来 在 Linux 设备驱动中,我们必须要解决的一个问题是: 多个进程对共享资源的并发访问,并发的访问会导致竞态。 1、并发和竞态 并发 (Concurrency) :指的是多个执行单元同时、并行的被执行。 竞态 (RaceConditions) :并发执行的单元对共享资源的访问,容易导致竞态。 共享资源:硬件资源和软件上的全局变量、静态变量等。 解决竞态的途径是:保证对共享资源的互斥访问。 互斥访问:一个执行单元在访问共享资源的时候,其他执行单元被禁止访问。 临界区 (Critical Sections) :访问共享资源的代码区域成为临界区。临界区需要以某种互斥机制加以保护。 常见的互斥机制包括 :中断屏蔽,原子操作,自旋锁,信号量,互斥体等。 2、竞态发生的场合 多对称处理器(SMP)的多个CPU之间 多个CPU使用共同的系统总线,可以访问共同的外设和存储器。在 SMP 的情况下,多核 (CPU0、CPU1) 的竞态可能发生于: CPU0 的进程和 CPU1 的进程之间 CPU0 的进程和 CPU1 的中断之间 CPU0 的中断和 CPU1 的中断之间 单CPU内,该进程与抢占它的进程之间 在单CPU内,多个进程并发执行,当一个进程执行的时间片耗尽,也有可能被另一个高优先级进程打断,会发生竞态。 中断(软中断、硬中断、Tasklet、底半部)与进程之间 当一个进程正在执行,一个外部/内部中断(软中断、硬中断、Tasklet等)将其打断,会导致竞态发生。 3、编译乱序和执行乱序 除了并发访问导致的竞态外,还需要了解编译器和处理器的一些特点所引发的一些问题。 3.1 编译乱序 现代的高性能编译器 在目标代码优化上都具有 乱序优化 的能力,编译器为了尽量 提高Cache命中率以及CPU的Load/Store单元的工作效率 ,可以对访存的指令进行乱序,减少逻辑上不必要的访存。 因此,在打开编译器优化后,生成的汇编码并没有严格按照代码的逻辑顺序执行,这是正常的。 为了解决编译乱序的问题,可以加入 barrier() 编译屏障 , 该屏障可以阻挡编译器的优化。设置屏障的前后,可以保证执行的语句不乱。 加入 barrier() 编译屏障,即可保证正确的执行顺序。 例子: #define barrier() __asm__ __volatile__("": : :"memory") ​ int main ( int argc , char * argv , e ; e = d ; barrier (); b = a ; c = a ; return 0 ; } 3.2 执行乱序 编译乱序是编译器的行为,而执行乱序就是处理器运行时的行为。 高级的 CPU 往往会根据自身的缓存特性,将访存指令重新排序执行! 这样就导致了多个顺序的指令,后发的指令仍有可能先执行完毕。 这种执行乱序,在多个 CPU 之间,以及单个 CPU 内部,都是非常常见的。 3.2.1 多CPU之间 处理器为了解决多核之间,一个 CPU 的行为对另一个 CPU 可见的情况, ARM 处理器引入了内存屏障指令: DMB(数据内存屏障),保证在该指令前的所有指令,内存访问完成,再去访问该指令之后的访存动作 DSB(数据同步屏障),保证在该指令前的所有访存指令执行完毕(访存,缓存,跳转预测,TLB维护等)完成 ISB(指令同步屏障), Flush 流水线,保证所有 在ISB之后执行 的指令都是从缓存或者内存中获得。 3.2.2 单CPU内部 在单 CPU 中,我们常遇到访问外设寄存器时,某些外设寄存器就对读写顺序有很高的要求,为了避免执行乱序的发生,这时候就需要 CPU 的一些内存屏障指令了。 CPU 内部,为了解决这种问题, CPU 提供了一些内存屏障指令: 可以参考 Documentation/memory-devices.txt 和 Documentation/io_ordering.txt 读写屏障: mb() 读屏障: rmb() 写屏障: wmb() 寄存器读屏障 __iormb()__ 寄存器写屏障 __iowmb()__ #define writeb_relaxed(v,c) __raw_writeb(v,c) #define writew_relaxed(v,c) __raw_writew((__force u16) cpu_to_le16(v),c) #define writel_relaxed(v,c) __raw_writel((__force u32) cpu_to_le32(v),c) ​ #define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; }) #define readw(c) ({ u16 __v = readw_relaxed(c); __iormb(); __v; }) #define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; }) ​ #define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); }) #define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); }) #define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); }) writel 与 writel_relaxed 的区别就在于有无屏障。 4、总结 由上文可知,为了解决 并发导致的竞态问题 高性能的编译器编译乱序问题 高性能的 CPU 带来的执行乱序问题 CPU 和 ARM 处理器提供的内存屏障指令等,这也是内核锁存在的意义。