二十三:《atomic"/>
Linux 驱动开发 二十三:《atomic
文档位置:Linux4.1.15/Documentation/atomic_t.txt
。
本文档旨在作为 Linux
端口维护者的指南,指导他们如何正确实现原子计数器、位操作和自旋锁接口。
atomic_t
类型应该定义为有符号整数,而 atomic_long_t
类型应该定义为有符号长整数。此外,它们应该是不透明的,这样任何类型的转换为普通C整数类型都将失败。下面的内容应该足够了:
typedef struct { int counter; } atomic_t;
typedef struct { long counter; } atomic_long_t;
历史上,计数器曾被宣布不稳定。这是不鼓励的。请参阅 documentationvolatile -consider -harmful.txt
了解完整的原理。
local_t
类型和 atomic_t
非常相似。如果计数器是每个 CPU
并且仅由一个 CPU
更新,则 local_t
可能更合适。local_t
的语义请参见 Documentationlocal ops.txt
。
为 atomic_t
实现的第一个操作是初始化和普通读取。
#define ATOMIC_INIT(i) { (i) }
#define atomic_set(v, i) ((v)->counter = (i))
第一个宏用于定义,如:
static atomic_t my_counter = ATOMIC_INIT(1);
初始化器是原子的,因为如果在运行前使用初始化器,则保证原子操作的返回值正确地反映初始化值。如果在运行时使用初始化式,那么在从另一个线程原子读取值之前,需要一个适当的隐式或显式读内存屏障。
与所有 atomic_
接口一样,将前导"atomic_"
替换为 "atomic_long_"
以对 atomic_long_t
进行操作。
第二个接口可以在运行时使用,如:
struct foo { atomic_t counter; };
...struct foo *k;k = kmalloc(sizeof(*k), GFP_KERNEL);
if (!k)return -ENOMEM;
atomic_set(&k->counter, 0);
该设置是原子的,因为所有线程的原子操作的返回值都保证正确,反映已使用此操作设置的值或使用其他操作设置的值。
在通过从另一个线程进行原子读取保证操作设置的值是可读的之前,需要一个适当的隐式或显式内存屏障。
接下来,我们有:
#define atomic_read(v) ((v)->counter)
它只读取当前对调用线程可见的计数器值。
读取是原子的,因为如果任何其他线程在可能的运行时初始化后使用了适当的隐式或显式内存屏障,并且仅使用接口操作修改该值,则返回值保证是使用接口操作初始化或修改的值之一。tomic_read
并不能保证任何其他线程的运行时初始化是可见的,因此接口的用户必须使用适当的隐式或显式内存屏障来处理这个问题。
警告:atomic_read( )
和atomic_set( )
并不意味着障碍!警告:atomic_read( )
和 atomic_set( )
并不意味着障碍!
某些体系结构可能会选择使用 volatile
关键字、barrier
或 inline
程序集来保证 atomic_read( )
和 atomic_set( )
的某种程度的即时性。
这不能得到统一的保证,并且将来可能会更改,因此 atomic_t
的所有用户都应将 atomic_read( )
和 atomic_set( )
视为简单的 C
语句,这些语句可以由编译器或处理器完全重新排序或优化,并为每个用例显式调用适当的编译器和/或内存屏障。如果不这样做,将导致代码在与不同的体系结构或编译器优化一起使用时突然中断,甚至不相关的代码中的更改会改变编译器优化访问 atomic_t
变量的部分的方式。
正确对齐的指针、长整型、整数和字符(以及无符号的等效项)可以原子方式从存储到 atomic_read( )
和 atomic_set( )
所描述的相同意义上存储。
ACCESS_ONCE( )
宏应该用于防止编译器使用优化,否则这些优化可能会优化不存在的访问,或者另一方面可能会创建未经请求的访问。
例如,请考虑以下代码:
while (a > 0)do_something();
如果编译器可以证明 do_something( )
不存储到变量 a
,则编译器有权将其转换为以下内容:
tmp = a;
if (a > 0)for (;;)do_something();
如果您不希望编译器这样做(您可能也不希望这样做),那么您应该使用如下代码:
while (ACCESS_ONCE(a) < 0)do_something();
或者,您可以在循环中放置一个barrier()调用。
对于另一个示例,请考虑以下代码:
tmp_a = a;
do_something_with(tmp_a);
do_something_else_with(tmp_a);
如果编译器可以证明 do_something_with( )
不存储到变量 a
,则编译器有权按如下方式制造额外的负载:
tmp_a = a;
do_something_with(tmp_a);
tmp_a = a;
do_something_else_with(tmp_a);
如果代码期望将相同的值传递给 do_something_with( )
和 do_something_else_with( )
,这可能会致命地混淆您的代码。
如果 do_something_with( )
是一个非常频繁地使用寄存器的内联函数,编译器很可能会制造这种额外的加载:从变量 a
重新加载可以节省对堆栈的刷新,然后重新加载。若要防止编译器以这种方式攻击您的代码,请编写以下内容:
tmp_a = ACCESS_ONCE(a);
do_something_with(tmp_a);
do_something_else_with(tmp_a);
最后一个例子,考虑下面的代码,假设变量a是在启动时设置的,在第二个 CPU
上线之前,以后不会更改,所以不需要内存屏障。
if (a)b = 9;
elseb = 42;
编译器有权通过将上面的代码转换为下面的代码来生成一个额外的存储区:
b = 42;
if (a)b = 9;
这可能会给其他并发运行的代码带来致命的意外,因为如果 a
为 0
,那么 b
永远不会有值 42
。为了防止编译器这样做,可以编写如下代码:
if (a)ACCESS_ONCE(b) = 9;
elseACCESS_ONCE(b) = 42;
如果变量 a
可以在运行时更改,则在没有正确使用内存屏障、锁或原子操作的情况下,甚至不要考虑这样做!
警告:ACCESS_ONCE( )
并不意味着障碍!
现在,我们转到通常在汇编代码的帮助下实现的原子操作接口。
void atomic_add(int i, atomic_t *v);
void atomic_sub(int i, atomic_t *v);
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
这四个例程在给定的 atomic_t
值中添加和减去整数值。前两个例程传递用于进行调整的显式整数,而后两个例程使用隐式调整值 1
。
这两个例程的一个非常重要的方面是,它们不需要任何显式的记忆屏障。它们只需以 SMP
安全的方式执行 atomic_t
计数器更新。
接下来,我们有:
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
这些例程分别从给定的 atomic_t
中添加 1
和减去 1
,并在执行操作后返回新的计数器值。
与上述例程不同,这些基元要求在操作之前和之后包含显式内存屏障。必须这样做,以便原子操作调用之前和之后的所有内存操作都相对于原子操作本身进行强排序。
例如,它的行为应该像 smp_mb( )
调用在原子操作之前和之后都存在一样。
如果在实现中使用的原子指令提供了满足上述要求的显式内存 barrier
语义,那也是可以的。
让我们继续前进:
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
它们的行为类似于 atomic_{inc,dec}_return( )
,只是给出了显式计数器调整而不是隐式 "1"
。
其次:
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
这两个例程分别对给定的原子计数器递加 1
和递减 1
。它们返回一个布尔值,指示生成的计数器值是否为零。
同样,这些原语围绕原子操作提供了显式的内存 barrier
语义。
int atomic_sub_and_test(int i, atomic_t *v);
这与 atomic_dec_and_test( )
相同,只是给出了显式递减而不是隐式 "1"
。这个原语必须在操作周围提供显式的内存 barrier
语义。
int atomic_add_negative(int i, atomic_t *v);
给定的增量将添加到给定的原子计数器值中。返回一个布尔值,指示生成的计数器值是否为负数。这个原语必须在操作周围提供显式的内存 barrier
语义。
然后:
int atomic_xchg(atomic_t *v, int new);
这将对原子变量 v
执行原子交换操作,并设置给定的新值。它返回原子变量 v
在操作之前具有的旧值。
atomic_xchg
必须在操作周围提供显式内存屏障。
int atomic_cmpxchg(atomic_t *v, int old, int new);
这将对原子值 v
执行原子比较交换操作,并具有给定的新旧值。与所有 atomic_xxx
操作一样,只要通过 atomic_xxx
操作执行 *v
的所有其他访问,atomic_cmpxchg
将仅满足其原子性语义。
tomic_cmpxchg
必须在操作周围提供显式内存屏障。
atomic_cmpxchg
的语义与下面为 "cas"
定义的语义相同。
最后:
int atomic_add_unless(atomic_t *v, int a, int u);
如果原子值 v
不等于 u
,则此函数将 a
添加到 v
中,并返回非零。如果 v
等于 u
,则返回零。这是一个原子操作。
atomic_add_unless
必须在操作周围提供显式内存屏障,除非操作失败(返回 0
)。
atomic_inc_not_zero
,相当于 atomic_add_unless(v, 1, 0)
。
如果调用方需要围绕不返回值的 atomic_t 操作的内存屏障语义,则定义一组实现此目的的接口:
void smp_mb__before_atomic(void);
void smp_mb__after_atomic(void);
例如,smp_mb__before_atomic( )
可以这样使用:
obj->dead = 1;
smp_mb__before_atomic();
atomic_dec(&obj->ref_count);
它确保 atomic_dec( )
调用之前的所有内存操作都相对于原子计数器操作进行强排序。在上面的示例中,它保证在原子计数器递减之前,将 "1"
分配给 obj->dead
将全局对其他 CPU
可见。
如果没有显式 smp_mb__before_atomic( )
调用,该实现可以合法地允许原子计数器更新在 "obj->dead = 1;"
赋值之前对其他 CPU
可见。
在上述 atomic_t
实现需要内存屏障的情况下,缺少内存屏障可能会产生灾难性的后果。下面是一个示例,它遵循 Linux
内核中经常出现的模式。它是使用原子计数器来实现引用计数,并且它的工作原理是,一旦计数器降至零,就可以保证没有其他实体可以访问该对象:
static void obj_list_add(struct obj *obj, struct list_head *head)
{obj->active = 1;list_add(&obj->list, head);
}static void obj_list_del(struct obj *obj)
{list_del(&obj->list);obj->active = 0;
}static void obj_destroy(struct obj *obj)
{BUG_ON(obj->active);kfree(obj);
}struct obj *obj_list_peek(struct list_head *head)
{if (!list_empty(head)) {struct obj *obj;obj = list_entry(head->next, struct obj, list);atomic_inc(&obj->refcnt);return obj;}return NULL;
}void obj_poke(void)
{struct obj *obj;spin_lock(&global_list_lock);obj = obj_list_peek(&global_list);spin_unlock(&global_list_lock);if (obj) {obj->ops->poke(obj);if (atomic_dec_and_test(&obj->refcnt))obj_destroy(obj);}
}void obj_timeout(struct obj *obj)
{spin_lock(&global_list_lock);obj_list_del(obj);spin_unlock(&global_list_lock);if (atomic_dec_and_test(&obj->refcnt))obj_destroy(obj);
}
这是对网络的通用邻居发现代码中 ARP
队列管理的简化。Olaf Kirch
发现了一个错误。kfree_skb( )
中的记忆屏障非常清楚地暴露了 atomic_t
内存屏障要求。
鉴于上述方案,在执行原子计数器递减之前,必须由 obj
列表删除完成的 obj->active
更新对其他处理器可见。
否则,计数器可能会降至零,但仍会设置 obj->active
,从而触发 obj_destroy( )
中的断言。错误序列如下所示:
cpu 0 cpu 1obj_poke() obj_timeout()obj = obj_list_peek();... gains ref to obj, refcnt=2obj_list_del(obj);obj->active = 0 ...... visibility delayed ...atomic_dec_and_test()... refcnt drops to 1 ...atomic_dec_and_test()... refcount drops to 0 ...obj_destroy()BUG() triggers since obj->activestill seen as oneobj->active update visibility occurs
由于返回值的 atomic_t
操作所需的内存屏障语义,因此上述内存可见性序列永远不会发生。具体而言,在上述情况下,atomic_dec_and_test( )
计数器递减在 obj->active
更新之前不会全局可见。
作为历史说明,32
位 Sparc
过去只允许使用其 atomic_t
类型的 24
位。这是因为它使用 8
位作为 SMP
安全性的自旋锁。Sparc32
缺少"比较和交换"类型的指令。但是,32
位 Sparc
已被转移到"自旋锁哈希表"方案中,该方案允许实现完整的 32
位计数器。从本质上讲,一个自旋锁数组是根据正在操作的 atomic_t
的地址索引到的,并且该锁保护原子操作。Parisc
使用相同的方案。
另一个注意事项是,在旧的 386
上,返回值的 atomic_t
操作速度非常慢。
现在,我们将介绍原子位掩码操作。您会发现它们的 SMP
和内存屏障语义在形状和范围上与上面的 atomic_t
操作相似。
本机原子位操作被定义为对与 "unsigned long" C
数据类型的大小对齐的对象进行操作,并且该大小的最小值。每个 "unsigned long"
中位的字节序是 cpu
的本机字节序。
void set_bit(unsigned long nr, volatile unsigned long *addr);
void clear_bit(unsigned long nr, volatile unsigned long *addr);
void change_bit(unsigned long nr, volatile unsigned long *addr);
这些例程分别设置、清除和更改 "ADDR"
所指向的位掩码上由 "nr"
指示的位号。
它们必须以原子方式执行,但这些接口不需要隐式内存屏障语义。
int test_and_set_bit(unsigned long nr, volatile unsigned long *addr);
int test_and_clear_bit(unsigned long nr, volatile unsigned long *addr);
int test_and_change_bit(unsigned long nr, volatile unsigned long *addr);
与上面一样,除了这些例程返回一个布尔值,该布尔值指示更改的位是否设置为 _BEFORE_
原子位操作。
警告!该值必须是布尔值非常重要,即。"0"
或 "1"
。不要试图通过声明上述返回 "long"
并仅返回 "old_val&mask"
之类的内容来保存一些说明,因为这将不起作用。
首先,在使用这些接口的许多代码路径中,此返回值被截断为 int
,因此在 64
位上,如果将位设置为较高的 32
位,则测试人员将永远不会看到这一点。
出现此问题的一个很好的例子是 thread_info
标志操作。诸如 test_and_set_ti_thread_flag( )
之类的例程将返回值切碎到 int
中。在其他地方也发生了这样的事情。
这些例程(如返回值的 atomic_t
计数器操作)必须围绕其执行提供显式内存屏障语义。在原子位操作调用可见之前,必须使原子位操作之前的所有内存操作全局可见。同样,在任何后续内存操作可见之前,原子位操作必须全局可见。例如:
obj->dead = 1;
if (test_and_set_bit(0, &obj->flags))/* ... */;
obj->killed = 1;
test_and_set_bit( )
的实现必须保证 "obj->dead = 1;"
在 test_and_set_bit( )
完成的原子内存操作变得可见之前对 cpu
可见。同样,test_and_set_bit( )
完成的原子内存操作必须在 "obj->killed = 1;"
可见之前变得可见。
最后是基本操作:
int test_bit(unsigned long nr, __const__ volatile unsigned long *addr);
它返回一个布尔值,指示位 "nr"
是否在 "addr"
所指向的位掩码中设置。
如果在 {set,clear}_bit( )
周围需要显式内存屏障(不返回值,因此不需要提供内存屏障语义),则提供两个接口:
void smp_mb__before_atomic(void);
void smp_mb__after_atomic(void);
它们的用法如下,类似于其 atomic_t
操作兄弟:
/* All memory operations before this call will* be globally visible before the clear_bit().*/
smp_mb__before_atomic();
clear_bit( ... );/* The clear_bit() will be visible before all* subsequent memory operations.*/smp_mb__after_atomic();
有两个特殊的位操作具有锁 barrier
语义(acquirrelease
,与 spinlocks
相同)。它们的操作方式与其 non-_lock/unlock
后缀变体的方式相同,只是它们分别提供获取/释放语义。这意味着它们可以用于 bit_spin_trylock
和 bit_spin_unlock
类型操作,而无需指定任何更多的 barriers
。
int test_and_set_bit_lock(unsigned long nr, unsigned long *addr);
void clear_bit_unlock(unsigned long nr, unsigned long *addr);
void __clear_bit_unlock(unsigned long nr, unsigned long *addr);
__clear_bit_unlock
版本是非原子的,但它仍然实现了解锁屏障语义。如果锁本身在保护字中的其他位,这可能很有用。
最后,提供了位掩码操作的非原子版本。它们用于使用其他一些更高级别的 SMP
锁定方案来保护位掩码的上下文中,因此可以在实现中使用成本较低的非原子操作。它们的名称类似于上面的位掩码操作接口,只是接口名称前面有两个下划线作为前缀。
void __set_bit(unsigned long nr, volatile unsigned long *addr);
void __clear_bit(unsigned long nr, volatile unsigned long *addr);
void __change_bit(unsigned long nr, volatile unsigned long *addr);
int __test_and_set_bit(unsigned long nr, volatile unsigned long *addr);
int __test_and_clear_bit(unsigned long nr, volatile unsigned long *addr);
int __test_and_change_bit(unsigned long nr, volatile unsigned long *addr);
这些非原子变体也不需要任何特殊的内存屏障语义。
例程 xchg( )
和 cmpxchg( )
必须提供与返回值的原子和位操作相同的确切内存屏障语义。
自旋锁和 **rwlock
**也有记忆屏障期望。要遵循的规则很简单:
1、获取锁时,实现必须在任何后续内存操作之前使其全局可见。
2、释放锁时,实现必须使所有以前的内存操作在锁释放之前全局可见。
这最终将我们带到了 _atomic_dec_and_lock( )
。在 lib/dec_and_lock.c
中实现了一个架构中立的版本,但大多数平台都希望在汇编器中对此进行优化。
int _atomic_dec_and_lock(atomic_t *atomic, spinlock_t *lock);
原子递减给定计数器,如果将下降到零原子获取给定的自旋锁并执行计数器递减到零。如果它没有降到零,则不对自旋锁执行任何操作。
实际上,获得正确的内存屏障非常简单。只需满足自旋锁获取需求,即确保自旋锁操作在任何后续内存操作之前是全局可见的。
如果我们定义一个抽象的原子操作,我们可以更清楚地演示这个操作:
long cas(long *mem, long old, long new);
"cas"
代表"比较和交换"。它原子:
1、将 "old"
与当前位于 "mem"
处的值进行比较。
2、如果它们相等,则将 "new"
写入 "mem"
。
3、无论如何,将返回 "mem"
处的当前值。
作为示例用法,原子计数器更新可能如下所示:
void example_atomic_inc(long *counter)
{long old, new, ret;while (1) {old = *counter;new = old + 1;ret = cas(counter, old, new);if (ret == old)break;}
}
让我们使用 cas( )
来构建一个伪 C
atomic_dec_and_lock( )
:
int _atomic_dec_and_lock(atomic_t *atomic, spinlock_t *lock)
{long old, new, ret;int went_to_zero;went_to_zero = 0;while (1) {old = atomic_read(atomic);new = old - 1;if (new == 0) {went_to_zero = 1;spin_lock(lock);}ret = cas(atomic, old, new);if (ret == old)break;if (went_to_zero) {spin_unlock(lock);went_to_zero = 0;}}return went_to_zero;
}
现在,就内存障碍而言,只要 spin_lock( )
严格地命令所有后续内存操作(包括 cas( )
)相对于自身,事情就会好起来的。
换句话说,_atomic_dec_and_lock( )
必须保证在获取自旋锁之前,降到零的计数器永远不会可见。
请注意,这也意味着,对于计数器未降至零的情况,没有内存排序要求。
更多推荐
Linux 驱动开发 二十三:《atomic
发布评论