Linux设备驱动程序学习(四)——并发和竟态

   并发:是指多个单元同时、并行被执行,并发执行单元对共享资源的访问就很容易导致竞态。
   竞态: 假设有一个设备,执行单元A对其写入3000个字符’a’而另一个执行单元B对其写入4000个’b’,第三个执行单元C读取globalmem的所有字符,如果执行单元A、B对于设备的写入操作同时发生,此时就会造成竞态。

并发及管理

  竞态通常是作为对资源的共享访问结果而产生的。当两个执行线程需要访问相同的数据结构(或硬件资源)时,混合的可能性就永远存在。

  在设计自己的驱动程序时,第一个要记住的规则是:只要可能,就应该避免资源的共享。若没有并发访问,就不会有竞态。这种思想的最明显的应用是避免使用全局变量。但是,资源的共享是不可避免的,如硬件资源本质上就是共享、指针传递等等。
  资源共享的硬性规则

  • 在单个执行线程之外共享硬件或软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致状态判断,因此必须显示地管理对该资源的访问。–访问管理的常见技术成为“加锁”或者“互斥”:确保一次只有一个执行线程可操作共享资源。
  • 当内核代码创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。对象尚不能正确工作时,不能将其对内核可用。

信号量和互斥体

  一个信号量(semaphore)本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临界区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待知道其他人释放该信号。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。

  当信号量用于互斥时(即避免多个进程同在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量就称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称,Linux内核中几乎所有的信号量均用于互斥。
  要使用信号量,内核代码必须包含<asm/semaphore.h> ,相关的类型是struct semaphore。

信号量的创建和初始化

  直接创建信号量,通过sema_init()函数实现:
void sema_init(struct semaphore *sem,int val);
  其中val是赋予一个信号量的初始值。
  不过信号量一般用于互斥模式,因此内核提供了一组辅助函数和宏来申明和初始化一个互斥体宏申明初始化互斥体:

DECLARE_MUTEX(name);          //一个称为name的信号量变量被初始化为1(使用DECLARE_MUTEX)或者0  
DECLARE_MUTEX_LOCKED(name);    //互斥体的初始状态是锁定的,要让别的线程访问,那么必须先解锁。

  辅助函数初始化互斥体,如果互斥体要在运行时初始化(动态分配)的话,那么就必须使用方法来申明:

  void init_MUTEX(struct semaphore *sem);
  void init_MUTEX_LOCKED(struct semaphore *sem);

  既然互斥体已经申明了,那么P函数和V函数的使用也需要介绍,在Linux中P函数被称为down,下面是down的三个版本:

void down(struct semaphore*sem);     /*不推荐使用,会建立不可杀进程*/
int down_interruptible(struct semaphore*sem);      /*推荐使用,使用down_interruptible需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并做出相应的响应。*/
int down_trylock(struct semaphore*sem);       /*带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。*/

  当一个线程成功调用了以上的down方法时,就说明该线程拿到了信号量,该线程就拥有了访问由该信号量保护的临界区的权利,但当互斥操作完成后,该线程必须释放信号量,调用V函数(Linux中为up):

void up(struct semaphore*sem);/*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。*/

读取者/写入者信号量

  信号量对所有的调用者互斥,不每个线程的具体工作,但是许多线程分为两种类型:

  • 部分只需要读取受保护的数据结构,而不做修改。
  • 另一部分则是不仅读取,还做修改。

  而对于第一种情况来说,允许多个并发的读取者是允许的,因为它们只读就能完成任务,不会改变具体的数据,这样能大大提高性能。
  针对这种情形,Linux提供了一种特殊的信号量类型:“rwsem”。
  使用rwsem的代码必须包含头文件<linux/rwsem.h>,相关数据类型:

Struct rw_semphore;
一个rwsem对象的函数初始化:
void init_rwsem(struct rw_semphore *sem);
而其对应的P、V函数为:
void down_read(struct rw_semaphore *sem); 
int down_read_trylock(struct rw_semaphore *sem); 
void up_read(struct rw_semaphore *sem);    //通过这个函数释放

Completion(完成)接口

  completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。代码必须包含<linux/completion.h>。使用的代码如下:

DECLARE_COMPLETION(my_completion);  /* 创建completion(声明+初始化) */

struct completion my_completion;   /* 动态声明completion 结构体*/
static inline void init_completion(&my_completion);  /*动态初始化completion*/

void wait_for_completion(struct completion*c);    /* 等待completion */
void complete(struct completion*c);    /*唤醒一个等待completion的线程*/
void complete_all(struct completion*c);    /*唤醒所有等待completion的线程*/

/*如果未使用completion_all,completion可重复使用;否则必须使用以下函数重新初始化completion*/
INIT_COMPLETION(struct completion c);   /*快速重新初始化completion*/

  completion的典型应用是模块退出时的内核线程终止。在这种运行中,某些驱动程序的内部工作有一个内核线程在while(1)循环中完成。当内核准备清除该模块时,exit函数会告诉该线程退出并等待completion。为此内核包含了用于这种线程的一个特殊函数:
  void complete_and_exit(struct completion*c,long retval);

自旋锁

  信号量对于互斥来说是非常有用的工具,但是它不是内核提供的唯一一种方式,相反,大多数锁定都是通过“自旋锁”的机制来实现的,和信号量不同,自旋锁可在不休眠的代码中使用,比如中断处理例程。
  自旋锁是一个互斥设备,它只能有两个值:“锁定”和“解锁”。它通常实现为某个整数值之中的单个位。

  “测试并设置”的操作必须以原子方式完成。我们在进入临界区之前首先要进行一个原子操作(测试原子变量),如果测试结果表明资源已经没有被占用(即锁锁空闲),则程序获取这个自旋锁(相当于该程序在进入临界区后将进入该临界区的大门关闭),否则程序将不停的在原地重复测试,这也就是为什么叫它“自旋锁”。
  任何时候,只要内核代码拥有自旋锁,在相关CPU上的抢占就会被禁止。

使用自旋锁

  适用于自旋锁的核心规则:

  • 任何拥有自旋锁的代码都必须是原子的,除服务中断外(某些情况下也不能放弃CPU,如中断服务也要获得自旋锁。为了避免这种锁陷阱,需要在拥有自旋锁时禁止中断),不能放弃CPU(如休眠,休眠可发生在许多无法预期的地方)。否则CPU将有可能永远自旋下去(死机)。
  • 拥有自旋锁的时间越短越好。

  使用自旋锁要包含<linux/spinlock.h>头文件,具体操作:

spinlock_t my_lock= SPIN_LOCK_UNLOCKED;     /* 编译时初始化spinlock*/
void spin_lock_init(spinlock_t*lock);      /* 运行时初始化spinlock*/

/* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/
void spin_lock(spinlock_t*lock);    /* 获得spinlock*/
void spin_lock_irqsave(spinlock_t*lock,unsigned long flags);  /* 获得spinlock,禁止本地cpu中断,保存中断标志于flags*/
void spin_lock_irq(spinlock_t*lock);   /* 获得spinlock,禁止本地cpu中断*/
void spin_lock_bh(spinlock_t*lock)      /* 获得spinlock,禁止软件中断,保持硬件中断打开*/

/* 以下是对应的锁释放函数*/
void spin_unlock(spinlock_t*lock);
void spin_unlock_irqrestore(spinlock_t*lock,unsigned long flags);
void spin_unlock_irq(spinlock_t*lock);
void spin_unlock_bh(spinlock_t*lock);

/* 以下非阻塞自旋锁函数,成功获得,返回非零值;否则返回零*/
int spin_trylock(spinlock_t*lock);
int spin_trylock_bh(spinlock_t*lock);


/*新内核的<linux/spinlock.h>包含了更多函数*/

锁陷阱

  锁定模式必须在一开始就安排好,否则其后的改进将会非常困难

  不明确规则:如果某个获得锁的函数要调用其他同样试图获取这个锁的函数,代码就会锁死。(不允许锁的拥有者第二次获得同个锁。)为了锁的正确工作,不得不编写一些函数,这些函数假定调用这已经获得了相关的锁。
  锁的顺序规则

  • 在必须获取多个锁时,应始终以相同顺序获取。
  • 若必须获得一个局部锁和一个属于内核更中心位置的锁,应先获得局部锁。
  • 若我们拥有信号量和自旋锁的组合,必须先获得信号量。
  • 不得再拥有自旋锁时调用down。(可导致休眠)
  • 尽量避免需要多个锁的情况。
    细颗粒度和粗颗粒度的对比:应该在最初使用粗颗粒度的锁,除非有真正的原因相信竞争会导致问题。

除了锁之外的方法

  Linux内核提供了大量的锁原语,但是内核照样有些力不从心,所以除了信号量和自旋锁外,在某些特定情况下,原子的访问可以不需要完整的锁,以下就是不使用锁的办法:

免锁算法

  经常用于免锁的生产者/消费者任务的数据结构之一是循环缓冲区。它在设备驱动程序中相当普遍,如以前移植的网卡驱动程序。内核里有一个通用的循环缓冲区的实现在<linux/kfifo.h>。

原子变量

  完整的锁机制对一个简单的整数来讲显得浪费。内核提供了一种原子的整数类型,称为atomic_t,定义在<asm/atomic.h>。原子变量操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令。

原子操作

   性质:原子操作是指在执行过程中不会被代码路径所中断的操作。
   作用:从它的性质上我们就可以推算出它的功能:主要用作内核计数
   分类:位变量原子操作和整型变量原子操作

   整型原子操作:

  • 设置原子变量的值
    void atomic_set(atomic_t *v,int i);/*设置原子变量v的值为i*/
    atomic_t_v = ATOMIC_INIT(0);/*定义原子变量v并初始化为0*/
  • 获取原子变量的值
    int atomic_read(atomic_t *v);/*返回源自变量的值*/

  • 原子变量的加减

    void atomic_add(int i,atomic_t *v);/*将i累加到v所指向的原子变量*/
    void atomic_sub(int i,atomic_t *v);/*将i减少到v所指向的原子变量i*/
  • 原子变量自增/自减
  void atomic_inc(atomic_t*v);/*原子变量增加一*/
  void atomic_dec(atomic_t*v);/*原子变量减少一*/
  • 操作并测试
/*进行一个特定的操作并且测试结果; 如果, 在操作后, 原子值是 0, 那么返回值是
真; 否则, 它是假. 注意没有 atomic_add_and_test. */
 int atomic_inc_and_test(atomic_t *v); 
 int atomic_dec_and_test(atomic_t*v);
 int atomic_sub_and_test(atomic_t*v);

Int atomic_add_negative(int i, atomic *v);  //将证书变量i累加到v,返回值在结果为负数时返回ture,否则为false
  • 操作并返回
//就像 atomic_add 函数, 不过它们返回原子变量的新值给调用者. 
  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);
位原子操作
//设置位
 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);

  下面就演示一个实例来利用原子变量实现设备在同一时刻只能由一个进程的打开

static atomic_t xxx_available = ATOMIC_INIT(1);/*定义并初始化原子变量为1*/
static int xxx_open(struct inode*inode,struct file *filp)
{
 ...
 if(!atomic_dec_and_test(&xxx_available)){    //如果该原子减1后不为0,说明该设备已经被以进程打开
  atomic_inc(&xxx_available);      //将刚才减去的一加上
  return -EBUSY;      /*已经打开*/
  }
  ...
 return 0;   /*成功*/
}
static int xxx_release(struct inode*inode,struct file *filp)
{
 atomic_inc(&xxx_available);       /*释放设备*/
 return 0;
}

seqlock

  2.6内核包含了一对新机制打算来提供快速地, 无锁地存取一个共享资源。seqlock要求保护的资源小,简单,并且常常被存取, 并且很少写存取但是必须要快。seqlock 通常不能用在保护包含指针的数据结构
  seqlock 定义在<linux/seqlock.h> ,初始化和定义seqlock:

/*两种初始化方法*/
seqlock_t lock1 = SEQLOCK_UNLOCKED;    //宏定义

seqlock_t lock2;
seqlock_init(&lock2);    //调用函数

  读取访问通过获得一个无符号的整数顺序值而进入临界区。在退出时, 那个顺序值与当前值比较; 如果不相等, 读存取必须重试.读者代码形式:

unsignedint seq;
do {
    seq = read_seqbegin(&the_lock);
   } while read_seqretry(&the_lock, seq);
   这个类型的锁常常用在保护某种类型的简单计算,这个计算需要多个一致的值。
获得一个 seqlock-保护 的资源的读权限的函数:
unsigned int read_seqbegin(seqlock_t *lock); 
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags); 
int read_seqretry(seqlock_t *lock, unsigned int seq); 
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long 
flags); 
   获取一个 seqlock-保护 的资源的写权限的函数
void write_seqlock(seqlock_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); 

   释放一个 seqlock-保护的资源的写权限的函数. 
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); 

(4)、RCU机制
  需要使用读取-拷贝-更新(RCU)机制的包含文件:

void rcu_read_lock; 
void rcu_read_unlock; 

  获取对由 RCU 保护的资源的原子读权限的宏定义:
void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);
  安排一个回调在所有处理器已经被调度以及一个 RCU-保护的资源可用被安全的释放之后运行。

总结

  这一部分主要是了解Linux设备驱动程序的锁机制,学会怎么处理并发和竟态,相对重要的是信号量、自旋锁和原子操作,在大部分的设备驱动程序对于竟态的处理,大多使用上面的处理方式,其实在上一部分的源码分析中,scull字符驱动程序的实现就使用了信号量来给公共资源加锁,避免竟态的产生。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值