并发和竞态

由于现在的内核已经发展到同时处理更多事情的时代,支持对称多处理(symmetric multiprocessing, SMP),这极大的提高了内核编程的复杂性,因此设备驱动程序在开始设计时就要考虑并发因素,并对内核提供的并发管理设施要有坚实的理解。

scull的缺陷

if(!dptr->data[s_pos])
{
    dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
    if(!dptr->data[s_pos])
        goto out;
}


假定有两个进程(“A”和“B”)正在独立地尝试向一个scull设备的相同偏移量写入数据,而且这两个进程同一时刻到达上述代码段的第一个if判断语句,如果指针是NULL,两个进程都会分配内存,每个进程都会将分配的结果指针赋值给dptr->data[s_pos],显然第二个完成赋值的进程(假定是B)会胜出,这样,由A进程分配的内存将丢失,从而永远不会返回到系统中。

上述过程就是一种竞态(race condition),竞态会导致对共享数据的非控制访问,这里导致的结果就是内存泄漏。但某些竞态经常会导致系统崩溃、数据被破坏或者产生安全问题。其结果是灾难性的。

并发及其管理

在现在的linux中存在大量的并发来源,因此会导致可能的竞态。

  • 正在运行的多个用户空间进程可能以一种令人惊讶的组合方式访问我们的代码
  • SMP系统甚至可能在不同的处理器上同时执行我们的代码
  • 内核代码是可抢占的,因此我们的驱动程序代码可能在任何时候丢失对处理器的独占,而拥有处理器的进程可能正在调用我们的驱动程序代码
  • 设备中断是异步事件,也会导致代码的并发执行
  • 内核还提供了许多可延迟代码执行的机制,如workqueue(工作队列),tasklet(小任务)以及timer(定时器)等,这些机制使得代码可在任何时刻执行,而不管当前进程在做什么
  • 在现代的热插拔世界中,设备可能会在我们正使用时消失

在任何事情可以任何时间发生的世界中,大部分竞态可通过使用内核的并发控制原语,并应用几个基本的原理来避免,首先介绍这些原理,然后再讲述如何应用这些原理的细节。

竞态通常是对资源的共享访问结果而产生。当两个执行线程需要访问相同的数据结构或硬件资源时,混合的可能性就永远存在。因此在设计驱动程序时,第一个要记住的规则是,只要可能,就应该避免资源的共享。如果没有并发的访问,也就不会产生竞态。因此编写的内核代码应该具有最少的共享。这种思想的最明显应用就是避免使用全局变量。

但事情的本质是共享是必须的,下面是资源共享的硬规则:

  1. 在单个执行线程之外共享硬件或软件资源的任何时候,必须显示地管理对该资源的访问,因为另处一个线程可能产生对该资源的不一致的观察。访问管理的常见技术称为“锁定”或者“互斥”——确保一次只有一个执行线程可操作该共享资源。
  2. 当内核代码创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在并正确工作。也是就说在对象尚不能正确工作时,不能将其对内核可用,在还有其它组件引用自己时要确保自己还能正确工作。

遵守上述规则需要仔细关注和规划对细节的处理。如果我们自己还没有认识到对被共享资源的并发访问,则其结果很容易让人迷惑不解。但是通过一些手段,大部分竞态可在其对我们或者我们的客户造成伤害前被处理掉。

信号量和互斥体

 我们需要建立一个临界区,在任意时刻,代码只能被一个线程执行。但并不是所有的临界区都是一样的,因此内核为不同的需求提供了不同的原语。

在我们的例子中,每个发生在进程上下文的对scull数据结构的访问都被认为是一个直接的用户请求,来自中断处理例程或者其它异步上下文的访问都不能发生。

  • 这里没有时间响应需求,应用程序开发都应该能理解I/O请求通常不会立刻得到满足
  • 在scull访问自己的数据结构时,scull产不拥有任何其它关键的系统资源

这一切意味着当scull驱动程序在等待访问数据结构而进入休眠时,不需要考虑其他内核组件

在这个上下文中,“进入休眠”是一个具有明确定义的术语。当一个linux进程到达某个时间点,此时它不能进行任何处理时,它将进入休眠(或“阻塞”)状态,这将把处理器让给其它执行线程直到将来它能够继续完成自己的处理为止。

在等待I/O完成时(另一进程在等待拥有资源的进程完成),进程经常会进入休眠状态,随着我们对内核理解的深入,将遇到大量不能休眠的情况,但scull中的write方法是可以休眠的,因此我们可以使用一种锁定机制,当进程在等待临界区的访问时,此机制可以让进程进入休眠状态。

但是在write方法中我们将使用kmalloc分配内存,该操作也可能会休眠,因此休眠可能在任何时刻发生。为了让我们临界区正常工作,我们选择使用的锁定原语必须在其他拥有这个锁并休眠的情况下工作

在可能出现休眠的情况下并不是所有的锁定机制都可用(稍后将看到一些不能休眠的锁定机制),对scull来说最适合的机制就是信号量(semaphore)。

一个信号量有时也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。

Linux信号量的实现

要使用信号量,内核代码必须包括<linux/semaphore.h>。相关类型是struct semaphore;

实际的信号量可通过几种途径来声明和初始化:

  1. 直接创建信号量,这通过sema_init完成:

    void sema_init(struct semaphore *sem, int val); 其中val是赋予一个信号量的初始值。

  2. 信号量通常用于互斥模式,为了让种情况更简单,可以用下面宏来声明和初始化一个互斥体:

    DECLARE_MUTEX(name); name被初始化为1

    DECLARE_MUTEX_LOCKED(name);name被初始化为0,这种情况下互斥体是锁定的。

  3. 如果互斥体必须在运行时被初始化,例如在动态分配互斥体的情况下,应使用下面的函数之一:

    void init_MUTEX(struct semaphore *sem);

    void init_MUTEX_LOCKED(struct semaphore *sem);

在Linux世界中,P函数被称为down——或者这个名字的其它变种,这里down指的是该函数减小了信号量的值,它也许会将调用都置于休眠状态,然后等待信号量变得可用,之后授予调用者对被保护资源的访问。下面是三个down版本:

  1. void down(struct semaphore *sem);减小信号量的值,并在必要时一直等待,没有必要不要使用它,它建立的进程不能被杀死(ps输出的D state)
  2. int down_interruptible(struct semaphore *sem);完成上面相同的工作,但操作是可中断的。最常用。它允许在等待在某个信号量上的用户空间进程可被用户中断。如果它被中断,该函数会返回非零值,而调用都不会拥有该信号量,对它的使用要始终检查返回值,并作出相应的响应
  3. int down_trylock(struct semaphore *sem); 永远不会休眠,如果信号量在调用时不可获得,立即返回一个非零值。

当一个线程成功调用到上述down的某个版本之后,就称该线程“拥有”(或“拿到”)了该信号量。这样,该线程就被赋予访问由该信号量保护的临界区的权利。当互斥操作完成后,必须返回该信号量。Linux等价于V的函数是up:

void up(struct semaphore *sem);

调用up之后,调用者不再拥有该信号量。任何拿到信号量的线程都必须通过一次且只有一次对up的调用而释放该信号量。如果在拥有一个信号量时发生错误,必须在将错误状态返回给调用者之前释放该信号量。我们很容易犯忘记释放信号量的错误,而其结果(进程在某些无关位置处被挂起)很难复现和跟踪。

读取者/写入者信号量

信号量对所有的调用者执行互斥,而不管每个线程到底想做什么。但是许多任务可以划分为两种不同的类型:一些任务只需要读取受保护的数据结构,而其他的则必须做修改。允许多个并发的读取者是可能的,只要他们当中没有哪个要做修改,这样可以大大提高性能,因为只读任务可并行完成它们的工作,而不需要等待其它读取者退出临界区。

Linux内核为这种情形提供了一种特殊的信号量类型,称为rwsem,(或者“reader/writer semaphore,读取者/写入者信号量”)。在驱动程序中使用这种信号量比较少,但偶尔也比较有用。

使用rwsem的代码必须包含<linux/rwsem.h>。相关数据类型是struct rw_semaphore;一个rwsem对象必须在运行时通过下的函数显示地初始化:

void init_rwsem(struct rw_semaphore *sem);

新初始化的rwsem可用于其后出现的任务(读取者或写入者)。对只读访问可用的接口如下:

void down_read(struct rw_semaphore *sem);提供对资源的只读访问,可和其它读取者并发访问,但它可能将进程置于不可中断的休眠

int down_read_trylock(struct rw_semaphore *sem);不会在读取访问不可获得时等待,注意它在授予访问时返回非零,其他情况下返回0。

void up_read(struct rw_semaphore *sem);down_read获得的rwsem对象最终必须通过up_read被释放

对写入者的接口类似于读取者接口:

void down_write(struct rw_semaphore *sem);

int down_write_trylock(struct rw_semaphore *sem);

void up_write(struct rw_semaphore *sem);

前三个与读取者对对应函数行为相同,当然它们是提供写入访问。

void downgrade_write(struct rw_semaphore *sem);

当某个快速改变获得了写入者锁,而其后是更长时间的只读访问的话,我们可以在结束修改之后调用downgrade_write,来允许其他读取者的访问。

一个rwsem可允许一个写入者或无限多个读取者拥有该信号量。写入者具有更高的优先级;如果写入者进入临界区,在其完成工作前,不会允许读取者获得访问。如果有大量的写入者竞争该信号量,则这种实现会导致读取者“饿死”,即长时间拒绝读取者访问。为此,最好在很少需要写入且写入者只会短期拥有信号量的时候使用rwsem

completion(完成)

内核编程中常见的一种模式是:在当前线程之外初始化某个活动,然后等待该活动的结束。这个活动可能是,创建一个新的内核线程或者新的用户空间进程、对一个已有进程的某个请求、或者某种类型的硬件动作等等。

这种情况下如果使用信号量来同步这两个任务,编码如下:

struct semaphore sem;

init_MUTEX_LOCKED(&sem);
start_external_task(&sem);//当外部任务完成工作时将调用up(&sem);
down(&sem);

然而信号量并不是适用这种情况的最好工具。原因如下:

  1. 如果存在针对该信号量的严重竞争,性能将受到影响
  2. 使用信号量在任务完成时进行通信,则调用down的线程几乎总是要等待,性能同样会受到影响
  3. 某些情况下,信号量可能在调用up的进程完成相关任务前消失

上述考虑导致2.4.7版内核中出现了"completion(完成)"接口,completion是一种轻量级的机制,它允许一个线程告诉另一线程某个工作已经完成。使用completion,必须包含<linux/completion.h>。

可利用下面接口创建completion:

DECLARE_COMPLETION(my_completion);

如果必须动态地创建和初始化completion,则使用下面的方法:

struct completion my_completion;
/*......*/
init_completion(&my_completion);

要等待completion,可用如下调用:

void wait_for_completion(struct completion *c);

注意,该函数执行一个不可中断的等待。如果代码调用了wait_for_completion且没有人会完成该任务,则将产生一个不可杀死的进程,目前已有可中断版本的补丁。

另一方面,实际的completion事件可通过调用下面函数之一来触发:

void complete(struct completion *c);    //只会唤醒一个等待线程
void complete_all(struct completion *c);//允许唤醒所有等待线程

这两个函数在是否有多个线程在等待相同的completion事件上有所不同。大多数情况下只会有一个等待者,因此这两个函数产生相同的结果。

一个completion通常是一单次设备(one-shot),它只会被使用一次然后被丢弃。但是仔细处理可以重复使用,如果没有使用complete_all,则我们可以重复使用一个completion结构,只要那个将要触发的事件是明确而不含糊的,就不会带来任何问题。但是如果使用了complete_all,则必须重新初始化它,下面宏可快速重新初始化:INIT_COMPLETION(struct completion c)。

completion示例:

DECLARE_COMPLETION(comp);

ssize_t complete_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) going to sleep\n",
        current->pid, current->comm);
    wait_for_completion(&comp);
    printk(KERN_DEBUG "awoken %i (%s) \n", current->pid, current->comm);
    return 0;
}

ssize_t complete_write(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
        current->pid, current->comm);
    complete(&comp);
    return count;  /*成功,以免重复*/
}

任何试图从该设备读取的进程都将等待(使用wait_for_completion),直到其他进程写入该设备为止。同一时刻有多个进程从该设备“读取”是可能的,每次向该设备写入将导致一个读取操作结束,但是没有办法知道是哪个进程。

completion机制的典型使用是模块退出时的内核线程终止。在这种原型中,某些驱动程序的内部工作由一个内核线程在while(1)循环中完成。当内核准备清除该模块时,exit函数会告诉线程退出并等等completion。为此,内核包含了可用于这种线程的一个特殊函数:

void complete_and_exit(struct completion *c, long retval);

自旋锁

信号量对互斥来讲是非常有用的工具,但它并不是唯一的这类工具。相反,大多数锁定通过一种称为“自旋锁(spinlock)”机制来实现。在正确使用的情况下,自旋锁通常可以提供比信号量更高的性能。但是自旋锁也带来了其他一组不同的使用限制。

自旋锁不能在可能休眠的代码中使用,因此,适用于自旋锁的核心规则是:任何拥有自旋锁的代码都必须是原子的,并且拥有锁的时间越短越好。自旋锁不能休眠,事实上,它不能因为任何原因放弃处理器,除了服务中断以外(某些情况下此时也不能放弃处理器)。内核抢占的情况由自旋锁本身处理,任何时候,只要内核代码拥有自旋锁,相关处理器上的抢占就会被禁止。自旋锁能在不能休眠的代码中使用,如中断处理例程。

休眠可能发生在许多无法预期的地方,如kmalloc、用户空间内核空间复制数据等,当我们编写要在自旋锁下执行的代码时,必须注意每一个所调用的函数。

自旋锁只有两个状态,要么“锁定”,要么“解锁”,如果线程检测到锁可用则锁定该锁并进入临界区,相反如果锁被其它线程获得则代码进入忙循环并重复检查这把锁,直到锁可能为止。这个循环就是自旋锁的自旋部分。“测试并设置”的操作必须以原子的方式完成,这样才能保证只有一个线程获得该锁。当存在自旋锁时,等待执行忙循环的处理器做不了任何工作。

自旋锁API介绍

自旋锁原语所需要包含的文件是<linux/spinlock.h>。相关数据类型是spinlock_t类型。和其它任何数据结构类似,一个自旋锁必须被初始化。

可在编译时通过下面代码完成:

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;

或者在运行时,调用下面函数:

void spin_lock(spinlock_t *lock);

释放已经获得的锁,调用下面函数:

void spin_unlock(spinlock_t *lock);

注意,所在的自旋锁等待在本质上都是不可中断的。一旦调用了spin_lock,在获得锁之前将一直处于自旋状态。

自旋锁函数

锁定一个自旋锁的函数实际有四个:

void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);保存先前的中断状态到flags中
void spin_lock_irq(spinlock_t *lock);软件和硬件中断例程中都可能访问锁
void spin_lock_bh(spinlock_t *lock);可能在软件中断例程中访问锁

释放自旋锁也有四种,严格对应于获取锁的函数:

void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);保存先前的中断状态到flags中
void spin_unlock_irq(spinlock_t *lock);软件和硬件中断例程中都可能访问锁
void spin_unlock_bh(spinlock_t *lock);可能在软件中断例程中访问锁

必须在同一个函数中调用spin_lock_irqsave和spin_unlock_irqrestore,否则代码可能在某些架构上出现问题。

还有如下非阻塞的自旋锁操作:

int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);

这两个函数在成功时返回非零值,否则返回零。对禁止中断的情况,没有对应的try版本。

读取者/写入者自旋锁

内核提供自旋锁的读取者/写入者形式,这种自旋锁和之前介绍的读取者/写入者信号量非常相似。

这种锁允许任意数量的读取者同时进入临界区,但写入者必须互斥访问。相关数据类型为rwlock_t类型,在<linux/spinlock.h>中定义,我们可以用下面两种形式声明和初始化它们:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */

rwlock_t my_rwlock;
rwlock_t_init(&my_rwlock); /* Dynamic way */

对读取者来讲,可使用的函数如下:

void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
void read_unlock_irqsave(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

这里并没有read_trylock函数可用。

用于写入者的函数类似于读取者,如下所示:

void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);

如果对锁的竞争导致饥饿,性能会变得很低。



锁陷阱

多年使用锁的经验说明我们很难轻松熟练地使用锁,许多使用方法都可能导致错误,下面是可能导致错误的东西。

不明确的规则

在编写代码时肯定会遇到几个函数,它们需要访问某个受特定锁保护的结构。这时我们要特别小心:如果某个获得锁的函数要调用其他同样试图获取这个锁的函数,我们的代码就会死锁。不论是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁;如果试图这么做,系统将挂起。

为了让锁正确工作,则不得不编写一些函数,这些函数假定调用者已经获取了相关的锁。通常,内部静态函数可通过这种方式编写,而提供给外部调用的函数则必须显示地处理锁定。在编写那些假定调用者已处理了锁定的内部函数时,我们自己应该显式地说明这种假定,否则几个月后,我们很难记清某函数是否需要拥有锁。

锁的顺序规则

在使用大量锁的系统中,有时需要获取多个锁,则:在必须获取多个锁时,应该始终以相同的顺序获得。但说起来容易做起来难,最好的办法是了解其它代码的做法。

有帮助的规则有两个:

  1. 如果我们必须获得一个局部锁,如设备锁,以及一个属于内核更中心位置的锁,则应该首先获取自己的局部锁。
  2. 如果我们拥有信号量和自旋锁的组合,则必须首先获得信号量;在拥有自旋锁时调用down(可能导致休眠)是个严重的错误。当然最好的办法是避免出现多个锁的情况。

除了锁之外的办法

 某些情况下,原子的访问可以不需要完整的锁,本节将讨论不使用锁的方法

免锁得法

大量的读取量/写入者情况,如果只有一个写入者,就可以使用一种循环缓冲区的数据结构来避免使用锁,内核有一个通用的循环缓冲区实现 ,有关其使用可参阅<linux/kfifo.h>。

原子变量

有时,共享资源可能恰好是一个简单的整数值。

假定我们的驱动程序维护着一个共享变量n_op,该变量表明有多少个设备操作正在并发的执行。通常即使是这个的操作n_op++也需要锁定。完整的锁机制对一个简单的整数来讲却显得有些浪费。针对这种情况,内核提供了一种原子的整数类型,相关数据类型为atomic_t,定义在<asm/atomic.h>中。

atomic_t v = ATOMIC_INIT(value);
void atomic_set(atomic_t *v, int i);  //设置v的值为整数i
int atomic_read(atomic_t *v);         //返回v的当前值
void atomic_add(int i, atomic_t *v);  //将i累加到v
void atomic_sub(int i, atomic_t *v);  //从v中减去i
void atomic_inc(atomic_t *v);         //增加一个原子变量
void atomic_dec(atomic_t *v);         //缩减一个原子变量
int atomic_inc_and_test(atomic_t *v); //操作后测试结果,原子值为0返回true,否则返回false
int atomic_dec_and_test(atomic_t *v); //同上
int atomic_sub_and_test(int i, atomic_t *v); //同上
int atomic_add_negative(int i, atomic_t *v); //将i累加到v,结果为负时返回true,否则为false
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);          //同上

对atomic_t变量的访问必须仅通过上述函数

位操作

atomic_t类型执行整数算术来讲比较有用,但是当需要以原子形式来操作单个位时,这种类型就无法派上用场了。这了实现这种操作,内核提供了一组可原子地修改和测试单个位的函数。函数在<asm/bitops.h>中声明

void set_bit(nr, void *addr);   //设置addr指向的数据项的第nr位
void clear_bit(nr, void *addr); //清除addr指向的数据项的第nr位
void change_bit(nr, void *addr);//切换指定的位
test_bit(nr, void *addr);//仅仅返回指定位的当前值
int test_and_set_bit(nr, void *addr);   //处理后返回这个位先前的值
int test_and_clear_bit(nr, void *addr); //同上
int test_and_change_bit(nr, void *addr);//同上

seqlock

2.6的内核包含两个新的机制,可提供对共享资源的快速、免锁访问。

当要保护的资源很小、很简单、会频繁被访问且写入很少发生且必须快速时,就可以使用seqlock。

seqlock允许读取者对资源的自由访问,但需要读取者检查是否和写入者发生冲突,如果有就需要重试对资源的访问。seqlock通常不能用于保护包含有指针的结构。

seqlock在<linux/seqlock.h>中定义。相关数据类型为seqlock_t类型,初始化方法有两种:

seqlock_t lock1 = SEQLOCK_UNLOCKED;

seqlock_t lock2;
seqlock_init(&lock2);

读取者访问通过一个获得一个符号的整数值进入临界区。在退出时,该顺序值会和当前值进行比较,如果不相等,则必须重试读取访问。其结果是读取者会如下编码:

unsigned int seq;

do {
    seq = read_seqbegin(&the_lock);
    /* 完成需要做的工作 */
} while(read_seqretry(&the_lock, seq);

如果在中断处理例程中使用seqlock,则应该使用IRQ安全版本:

unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);

写入者必须在进入由seqlock保护的临界区获得一个互斥锁。调用如下函数:

void write_seqlock(seqlock_t *lock);
void write_sequnlock(seqlock_t *lock);

写入锁使用自旋锁实现,因此自旋锁的常见限制也适用于写入锁。自旋锁的常见变种都可以使用:

void write_seqlock(rwlock_t *lock);
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);
int write_tryseqlock(seqlock_t *lock);//如果成功返回非零值
void write_sequnlock(seqlock_t *lock);
void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);


读取-复制-更新(read-copy-update, RCU)

RCU也是一种高级互斥机制,在正确条件下也可获得高的性能。它很少在驱动程序中使用,但很知名,为此我们必须做一些了解。

RCU对它可以保护的数据结构做了一些限定。它针对经常发读取而很少写入的情形做了优化。被保护的资源应该通过指针访问,而对这些资源的引用必须仅由原子代码拥有。在需要要修改该数据时,写入线程首先复制,然后修改副本,之后用新的版本替代相关指针,这也是该名称的由来。当内核确信老的版本上没有其他引用时,就可释放老的版本。

作为RCU的实际使用示例,可考虑网络路由表。第个外出的数据包都需要检查路由表以便确定应该使用哪个接口。这种检查很快,并且一旦内核找到了目标接口就不再需要那个路由表入口了。RCU可让路由表无需锁定地实现,从而获得较大的性能提高。内核中的Starmode射频IP驱动程序也是使用RCU来跟踪它自己的设备清单。

使用RCU的代码应包含<linux/rcupdate.h>。

在读取端,必须将引用数据结构的代码包括在rcu_read_lock和rcu_read_unlock调用之间,在这之间禁止内核抢占,如下所示:

struct my_stuff *stuff;

rcu_read_lock();
stuff = find_the_stuff(args...);
do_smomething_with(stuff);
rcu_read_unlock();

写入端,第一步只需分配一个新的结构,如果必要从老的结构的复制数据,然后将读取代码能看到的指针换掉。这时,读取端会假定修改已经完成,任何进入临界区的代码都将看到数据的新版本。第二步就是释放老的数据结构。当然其他处理器上可以还运行着引用老的数据的代码,因此不能立即释放。相反,写入代码必须等到能够确信不存在这样的引用。因为所有引用都是原子的,所以一旦系统中的每个处理器都至少调度了一次之后,所有的引用都会消失。因此RCU所做的就是设置一个回调函数并等待所有的处理器被调度,之后由回调函数执行清除工作。

修改受RCU保护的数据结构的代码必须通过分配一个struct rcu_head数据结构来获得清除用的回调函数,不需要初始化这个结构,它通常被内嵌在RCU保护的大资源中。

在修改完成后应该做如下调用:

void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);

在可安全释放资源时,给定的func会被调用,传递到call_rcu的相同参数也会传递到func这个函数。通常func要做的唯一工作就是调用kfree。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值