目录
一、引言
在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核需要一些同步机制来同步各执行单元对共享数据的访问。尤其是在多处理器系统上,更需要同步机制来协调不同处理器上的执行单元对共享数据的操作。Linux内核作为操作系统的核心,提供了多种同步机制,其中原子操作是一种基础且重要的同步手段。原子操作能够确保指令以原子的方式执行,执行过程不会被任何其他任务或事件打断,从而保证了数据操作的完整性和一致性。
二、原子操作的基本概念
(一)定义
原子操作,从物理学的角度类比,就如同物质的最小微粒,是不可再分的操作单位。在计算机领域,原子操作指的是该操作绝不会在执行完毕前被任何其他任务或事件打断,是最小的执行单位,不可能有比它更小的执行单位。例如,在多线程或多进程环境中,对一个共享变量的原子操作可以保证在操作期间不会被其他线程或进程干扰,从而避免数据竞争和不一致的问题。
(二)特点
- 不可分割性:原子操作一旦开始执行,就会一直运行到结束,中间不会有任何上下文切换。例如,在单处理器系统中,能够在单条指令中完成的操作通常可以认为是原子操作,因为中断只能发生于指令之间。但在对称多处理器(SMP)结构中,情况会更复杂,即使是单条指令也可能受到其他处理器的干扰。
- 硬件依赖性:原子操作需要硬件的支持,因此是架构相关的。不同的处理器架构可能采用不同的方式来实现原子操作。例如,x86架构通过使用LOCK指令前缀来确保某些指令在执行时的原子性;而ARM架构则提供了LDREX和STREX等指令来支持原子操作。
(三)作用
原子操作主要用于实现资源计数和同步。很多引用计数(refcnt)就是通过原子操作实现的。例如,在TCP/IP协议栈的IP碎片处理中,碎片队列结构中的引用计数器就是原子类型,通过原子操作来保证引用计数的正确性。当创建IP碎片时,使用原子操作将引用计数设置为1;当引用该IP碎片时,使用原子操作将引用计数加1;当不需要引用该IP碎片时,使用原子操作将引用计数减1并判断是否为0,如果为0则释放该IP碎片。
三、原子操作的实现原理
(一)x86架构下的原子操作
在x86架构下,处理器提供了在指令执行期间对总线加锁的手段。通过在需要原子操作的指令前附加LOCK指令前缀将总线锁定,保证了指令执行时不会受到其它处理器的影响。当某条指令被加上LOCK指令前缀时,该指令在执行前,会把处理器的#HLOCK引脚拉低,该引脚被拉低导致总线被锁,其他处理器不能访问总线,直到指令执行完毕,处理器的#HLOCK引脚恢复以后,总线的访问权才被释放。x86架构只有指定的几个指令才可以附加LOCK指令前缀,操作系统把这些附加了LOCK指令前缀的指令包装后,做成多种原子操作API供应用使用。
(二)ARM架构下的原子操作
ARM架构下的原子操作不使用总线锁定的方式,而是采用独占访问机制。ARM提供了一对独占内存加载和存储的指令,用于支持原子操作实现:
- LDREX指令:内存独占加载指令。从内存中以独占的方式加载内存地址的值到通用寄存器里,同时会通过独占监控器(exclusive monitor)来监控该内存的访问,将这个内存地址标记为独占访问模式,保证以独占的方式来访问这个内存地址,不受其他因素的影响。
- STREX指令:内存独占存储指令。以独占的方式把新的数据存储到内存中。它会检查内存是否有独占访问标记,如果有则更新内存值并清空标记,否则不更新内存。
对于ARM处理器而言,比如atomic_inc()底层的实现会调用到atomic_add(),其代码如下:
static inline void atomic_add(int i, atomic_t *v)
{
unsigned long tmp;
int result;
prefetchw(&v->counter);
__asm__ __volatile__("@ atomic_add\n"
"1: ldrex %0, [%3]\n"
" add %0, %0, %4\n"
" strex %1, %0, [%3]\n"
" teq %1, #0\n"
" bne 1b"
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
: "r" (&v->counter), "Ir" (i)
: "cc");
}
四、原子操作的API和使用示例
(一)原子类型定义
在Linux内核中,针对整数的原子操作采用了一个特殊的数据类型atomic_t
,其定义如下:
typedef struct { volatile int counter; } atomic_t;
volatile
修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。
(二)常用原子操作API
- 初始化原子变量:
ATOMIC_INIT(i)
用于在定义原子变量时,初始化为指定的值。例如:static atomic_t count = ATOMIC_INIT(1);
- 读取原子变量:
atomic_read(atomic_t *v)
返回原子变量的值。 - 设置原子变量:
atomic_set(atomic_t *v, int i)
将原子变量的值设置为i
。 - 原子变量加操作:
atomic_add(int i, atomic_t *v)
将原子变量的值加上i
;atomic_inc(atomic_t *v)
将原子变量的值自增1。 - 原子变量减操作:
atomic_sub(int i, atomic_t *v)
将原子变量的值减去i
;atomic_dec(atomic_t *v)
将原子变量的值自减1。 - 原子变量操作并测试:
atomic_sub_and_test(int i, atomic_t *v)
将原子变量的值减去i
,并判断结果是否为0,如果为0则返回真,否则返回假。
(三)使用示例
下面通过一个简单的例子来说明原子操作的使用。假设我们有一个内核模块,需要对一个计数器进行并发操作,为了避免数据竞争,我们可以使用原子操作来实现:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <asm/atomic.h>
static atomic_t counter = ATOMIC_INIT(0);
static int __init test_atomic_init(void)
{
atomic_inc(&counter);
printk(KERN_INFO "Counter value: %d\n", atomic_read(&counter));
return 0;
}
static void __exit test_atomic_exit(void)
{
atomic_dec(&counter);
printk(KERN_INFO "Counter value: %d\n", atomic_read(&counter));
}
module_init(test_atomic_init);
module_exit(test_atomic_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Atomic operation test module");
在这个例子中,我们定义了一个原子变量counter
并初始化为0。在模块初始化时,使用atomic_inc
将计数器加1,并打印计数器的值;在模块退出时,使用atomic_dec
将计数器减1,并打印计数器的值。通过原子操作,我们确保了计数器的并发操作是安全的。
五、原子操作的应用场景
(一)资源引用计数
在很多系统中,需要对资源的引用进行计数,以确保资源在不再被引用时能够被正确释放。原子操作可以很好地实现资源引用计数,避免了使用复杂的锁机制带来的开销。例如,在内存管理中,每个内存块可以有一个引用计数器,当有新的引用时,使用原子操作将引用计数加1;当引用释放时,使用原子操作将引用计数减1,并判断是否为0,如果为0则释放该内存块。
(二)并发控制
原子操作可以用于实现简单的并发控制,例如自旋锁的实现。自旋锁是一种基于忙等待的锁机制,当一个线程尝试获取锁时,如果锁已经被其他线程持有,它会一直自旋等待,直到锁可用。自旋锁的实现可以使用原子操作来保证锁的原子性。例如:
static atomic_t spinlock = ATOMIC_INIT(0);
void spin_lock(atomic_t *lock)
{
while (atomic_cmpxchg(lock, 0, 1) != 0) {
// 自旋等待
}
}
void spin_unlock(atomic_t *lock)
{
atomic_set(lock, 0);
}
在这个例子中,spin_lock
函数使用atomic_cmpxchg
原子操作来尝试获取锁,如果锁的值为0,则将其设置为1并返回;如果锁的值不为0,则继续自旋等待。spin_unlock
函数使用atomic_set
将锁的值设置为0,释放锁。
(三)状态标志
在多线程或多进程环境中,我们经常需要使用状态标志来表示某个条件是否满足。原子操作可以确保对状态标志的读写操作是原子的,避免了数据竞争。例如,一个线程可能需要等待另一个线程完成初始化操作,这时可以使用原子标志位来实现:
#include <linux/atomic.h>
static atomic_t initialized = ATOMIC_INIT(0);
void init_thread(void)
{
// 执行初始化操作
atomic_set(&initialized, 1);
}
void worker_thread(void)
{
while (atomic_read(&initialized) == 0) {
// 等待初始化完成
}
// 执行后续工作
}
在这个例子中,init_thread
函数在完成初始化后,使用atomic_set
将initialized
标志设置为1;worker_thread
函数使用atomic_read
来检查initialized
标志的值,如果为0则继续等待,直到标志值变为1。
六、总结
原子操作作为Linux内核同步机制的重要组成部分,具有不可分割性、硬件依赖性等特点,能够有效地实现资源计数和同步,避免数据竞争和不一致的问题。通过使用原子操作的API,我们可以在多线程或多进程环境中安全地对共享数据进行操作。同时,原子操作在资源引用计数、并发控制和状态标志等方面都有广泛的应用。然而,原子操作也有一定的局限性,例如只能处理简单的操作,对于复杂的同步需求,可能需要结合其他同步机制来实现。在实际开发中,我们应该根据具体的需求合理选择和使用原子操作,以提高系统的性能和稳定性。