第10章内核同步方法

Linux内核提供了一组相当完备的同步方法,这些方法使得内核开发者能编写出高效而又*竞争的代码。

10.1 原子操作

原子操作可以保证指令以原子的方式执行——执行过程不被打断。原子原本指的是不可分割的微粒,所以原子操作也就是不能被分割的指令。

两个原子操作绝对不可能并发地访问同一个变量,这样加操作就绝不会引起竞争。

内核提供两组原子操作接口——一组针对整数进行操作,另一组针对单独的位进行操作。在Linux支持的所有体系结构上都实现了这两组接口。大多数体系结构会提供支持原子操作的简单算术指令。而有些体系结构确实缺少简单的原子操作指令,但是也为单步执行提供了锁内存总线的指令,这就确保了其他改变内存的操作不能同时发生。

1、原子整数操作

针对整数的原子操作只能对atomic_t类型的数据进行处理。引入一个特殊数据类型,没有直接使用C中的int 类型,出于两个原因:首先,让原子函数只接收atomic_t类型的操作数,确保原子操作只与这种特殊类型数据一起使用。同时,也保证该类型的数据不会被传递给任何非原子函数。其次,使用atomic_t类型确保编译器不对相应的值进行访问优化——使得原子操作接收到正确的内存地址。最后,在不同体系结构上实现原子操作时,使用atomic_t可以屏蔽其间的差异。atomic_t类型定义在<include/linux/types.h>中:

typedef struct {
        volatile int counter;
} atomic_t;

使用原子整型操作需要的声明在<asm/atomic.h>文件中。有些体系结构会提供一些只能在该体系结构上使用的额外原子操作方法,但所有的体系结构都能保证内核使用到的所有操作的最小集。在写内核代码时,可以肯定,这个最小操作集在所有体系结构上都已实现了。

定义一个atomic_t类型的数据方法很平常,还可以在定义时给它设定初值:

atomic_t v;

atomic_t u = ATOMIC_INIT(0);

操作非常简单:

atomic_set(&v, 4);

atomic_add(2, &v);

atomic_inc(&v);

如果需要将atomic_t转换成int类型,使用atomic_read()完成:

printk("%d\n",atomic_read(&v));

原子整数操作最常见的用途是实现计数器。还可以用原子整数操作原子地执行一个操作并检查结果。常见的例子就是原子地减操作和检查。

/**
 * atomic_dec_and_test - decrement and test
 * @v: pointer of type atomic_t
 *
 * Atomically decrements @v by 1 and
 * returns true if the result is 0, or false for all other
 * cases.
 */
static inline int atomic_dec_and_test(atomic_t *v)
{
        unsigned char c;

        asm volatile(LOCK_PREFIX "decl %0; sete %1"
                     : "+m" (v->counter), "=qm" (c)
                     : : "memory");
        return c != 0;
}

表10-1列出了所有的标准原子整数操作(所有体系结构都包含这些操作)。某种特定的体系结构上实现的所有操作可以在文件<asm/atomic.h>中找到。

第10章内核同步方法

原子操作通常是inline函数,通过内嵌汇编指令来实现的。如果某个函数本来就是原子的,那么它往往会被定义成一个宏。

/**
 * atomic_read - read atomic variable(读原子变量)
 * @v: pointer of type atomic_t
 *
 * Atomically reads the value of @v.
 */
static inline int atomic_read(const atomic_t *v)
{
        return v->counter;
}

原子性与顺序性的比较

关于原子读取的讨论引发原子性与顺序性之间差异的讨论。一个字长的读取总是原子地发生,绝不可能对同一个字交错地进行写;读总是返回一个完整的字,这或者发生在写操作之前,或者之后,绝不可能发生在写的过程中。例如,如果一个整数初始化为42,然后又置为365,那么读取这个整数肯定会返回42或者365,而绝不会是二者的混合。这就是所谓的原子性。

也许代码比这有更多的要求。或许要求读必须在待定的写之前发生——这种需求其实不属于原子性要求,而是顺序要求。原子性确保指令执行期间不被打断,要么全部执行完毕,要么根本不执行。另一方面,顺序性确保即使两条或多条指令出现在独立的执行线程中,甚至独立的处理器上,依然要保持本该的执行顺序。

原子操作只保证原子性。顺序性通过屏障指令来实施。

编写代码,能使用原子操作时,不要使用复杂的加锁机制。对多数体系结构来讲,原子操作与更复杂的同步方法相比较,给系统带来的开销小,对高速缓存行的影响也小。但对于有高性能要求的代码,对多种同步方法进行测试比较,不失为一种明智的做法。

2、64位原子操作

随着64位体系结构普及,内核开发者确实在考虑原子变量除32位atomic_t类型外,也应引入64位的atomic64_t。因为移植性原因,atomic_t变量大小无法在体系结构之间改变。所以,atomic_t类型即便在64位体系结构下也是32位的,若要使用64位的原子变量,则要使用atomic64_t类型——其功能和其32位无差异,使用方法完全相同,不同的只有整型变量大小从32位变成64位。表10-2,是所有标准原子操作列表;有些体系结构实现的方法更多,但是没有移植性。atomic64_t类型是对长整型的一个简单封装。

include/linux/types.h

#ifdef CONFIG_64BIT
typedef struct {
        volatile long counter;
} atomic64_t;
#endif

第10章内核同步方法

 

所有64位体系结构都提供atomic64_t类型,以及一组对应的算术操作方法。为了便于在Linux支持的各种体系结构之间移植代码,开发者应该使用32位的atomic_t类型。把64位的atomic64_t类型留给特殊体系结构和需要64位的代码。

3、原子位操作

除原子整数操作外,内核提供一组针对位这一级数据进行操作的函数。与体系结构相关,定义在文件<asm/bitops.h>中。

位操作函数是对普通的内存地址进行操作的。其参数是一个指针和一个位号,第0位是给定地址的最低有效位。在32位机上,第31位是给定地址的最高有效位而第32位是下一个字的最低有效位。虽然使用原子位操作在多数情况下是对一个字长的内存进行访问,因而位号应该位于0~31,但是,对位号的范围并没有限制。

由于原子位操作是对普通的指针进行的操作。

第10章内核同步方法

内核还提供一组与上述操作对应的非原子位函数。非原子位函数与原子位函数的操作完全相同,但是,非原子位函数不保证原子性,且名字前多两个下划线。如果不需要原子性操作,那么这些非原子的位函数相比原子的位函数可能会执行得更快。

内核还提供两个例程用来从指定的地址开始搜素第一个被设置的位。

int find_first_bit(unsigned long *addr,unsigned int size);

int find_first_zero_bit(unsigned long *addr,unsigned int size);

这两个函数中第一个参数是一个指针,第二个参数是要搜索的总位数,返回值分别是第一个被设置的位的位号。如果搜索范围仅限于一个字,使用_ffs()和ffz()这两个函数更好,只需要给定一个要搜索的地址做参数。

与原子整数操作不同,代码一般无法选择是否使用位操作,它们是唯一的、具有可移植性的设置特定位方法,需要选择的是使用原子位操作还是非原子位操作。如果代码本身已经避免了竞争条件,可以使用非原子位操作,通常这样执行得更快,还要取决于具体的体系结构。