Linux中的并发 竞态控制

 因为程序的并发执行而导致的竞态是 Linux内核中一个非常复杂的方面。对于设备的驱动程序开发者而言,熟悉Linux内核提供的并发互斥的处理机制相当重要。所谓竞态,简而言之,是多个内核线程有可能对同一资源进行操作时可能导致的内核数据紊乱的行为。共享数据是并发的根本原因。
并发的来源
我把并发来源分为两个大的方面来分别进行讨论:单处理器和多处理器。
1.单处理器
对于单处理器而言,并发主要来自于中断,可抢占的内核和各种延迟队列。
2.多处理器
多处理器的情况更加复杂,在同一时间,同一代码可同时在不同处理器上执行。对这种并发的处理要更加棘手。
内核的互斥设施
驱动程序开发者对内核提供的互斥机制的确切理解是写出高安全性代码的关键。在Linux内核中,这种设施主要是信号量和自旋锁。
1.信号量
信号量的原理主要是基于对一 内存 单元的原子性的测试和设置操作(atomic test and set),不同处理器有不同的汇编语言用于提供这种机制。因为内存单元对于多处理器系统中的每个处理器都是共享的,所以这种机制同样适用于多处理器。
相对于自选锁,信号量的最大特点是允许调用它的线程进入睡眠状态。这意味着试图获得某一信号量的线程会导致对处理器拥有权的丧失,也即出现进程的切换。

  操作:

  1. 定义 struct semaphore sem;

  2. 初始化 void sema_init(struct semaphore *sem, int val);

  void init_MUTEX(struct semaphore *sem); //信号量sem的值设置为1

  void init_MUTEX_LOCKED(struct semaphore *sem); //信号量sem的值设置为0

  DECLARE_MUTEX(name) //name的信号量初始化为1

  DECLARE_MUTEX_LOCKED(name) //name的信号量初始化为0

  3.获取 void down(struct semaphore *sem); //会导致休眠 ,不能用在中断上下文

  int down_interruptible(struct semaphore *sem); 能获得返回0,否则返回非0,不会导致休眠

  Int down_trylock(struct semaphore *sem); //不会休眠 可在中断上下文使用

  4. 释放 void up(struct semaphore *sem); 释放sem,唤醒等待者

  信号量用于同步

如果信号量被初始化为0,则它可以用于同步,同步意味着一个执行单元的继续执行需等待另一执行单元完成某事,保证执行的先后顺序。

  完成量用于同步

  Linux提供了一种比信号量更好的同步机制,即完成量(pletion)

  操作:1. 定义 struct pletion my_pletion;

  2. 初始化 init_pletion(&my_pletion);

  DECLARE_COMPLETION(my_pletion); 定义并初始化

  3. 等待 void wait_for_pletion(struct pletion *c);

  4. 唤醒void plete(struct pletion *c);

  void plete_all(struct pletion *c);

  自旋锁vs信号量

  严格意义上说,信号量和自旋锁属于不同层次的互斥手段前者的实现依赖于后者。

  信号量是进程级的,用于多个进程之间对资源的互斥,所以适用于进程占用资源时间比较长的时候,而自旋锁不能在临界区长时间停留。

  信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程间的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。

  如果被保护的共享资源需要在中断或软中断情况下使用,则只能选择自旋锁。如果一定要使用信号量,则只能通过down_trylock()进行,以避免阻塞

  读写信号量

  它可允许N个读执行单元同时访问共享资源,而最多只能有一个写执行单元。

  操作:1. 定义struct rw_semaphore my_rws;

  初始化void init_rwsem(struct rw_semaphore *sem);

  2. 读信号量获取 void down_read(struct rw_semaphore *sem);

  int down_read_trylock(struct rw_semaphore *sem);

  3. 读信号量释放void up_read(struct rw_semaphore *sem);

  4. 写信号量获取 void down_write(struct rw_semaphore *sem);

  int down_write_trylock(struct rw_semaphore *sem);

  3. 写信号量释放void up_write(struct rw_semaphore *sem);

  互斥体 (内核中真实存在的mutex

  操作:1. 定义初始化 struct mutex my_mutex;

  Mutex_init (&my_mutex);

  2. 获取 void fastcall mutex_lock(struct mutex *lock);

  int fastcall mutex_lock_interruptible(struct mutex *lock);

  int fastcall mutex_trylock (struct mutex *lock); 获取不到不睡眠

mutex_lockmutex_lock_interruptible区别在于前者睡眠不可被信号打断

  3.释放void fastcall mutex_unlock(struct mutex *lock);


2.
自旋锁

自旋锁的出现最初是为了解决多处理器上出现的互斥问题。其原理是,试图获得锁的线程去进行原子性的位测试和设置操作(atomic bit test and set),如果没有别的处理器进入临界区,那么当前线程将获得锁,锁定之后进入临界区。如果有别的处理器在临界区中,那么当前处理器将进入忙等待状态,直到其他处理器解开该锁。与信号量最大的区别是,试图进入临界区的线程如果得不到锁,那么就一直不停地执行atomic bit test操作而不会进入睡眠状态。这种情况下,试图获得锁的处理器上将啥工作也干不了(除了进行这种atomic bit test操作外),正因为如此,所以要求获得锁的线程要以最短的时间结束临界区中的操作。

在单处理器上,自旋锁更确切的意义是,一个试图获得锁的线程实际上是向系统通告:当我执行下面代码时,不要将我切换出处理器。因此,单处理器上的自旋锁代码实际是在完成对处理器的强制拥有的操作。而在单处理器上,中断和抢占是最大可能的并发源头,所以,实际上单处理器上的自旋锁的实现代码简单归结为:关中断和关闭抢占。理解spin lock的实现代码需要深刻的处理器相关知识和汇编语言。
基于以上的实现原理,使用自旋锁的一个核心规则是:拥有锁的线程绝对不可以放弃处理器。因为假设它放弃了处理器,那么另一个获得处理器线程的代码如果想获得该锁,将不得不等待很长的时间,更坏的情况下导致系统的死锁。更具体的描述:在单处理器上,一个获得锁的线程必然运行在中断和抢占都关闭的环境中,这种情况下被动的放弃处理器已经成为不可能,只有主动放弃处理器才会出现,比如在互斥代码中调用了copy_from_user,该函数的实现中使进程进入休眠成为可能,如果恰巧下一个被调度运行的线程试图获得该锁,那么系统将进入死锁状态,因为这种情况下后来被调度的线程已经没有主动放弃处理器的可能(因为它此时正运行试图获得锁的代码,不可能调用到那些有可能主动放弃处理器的函数)

自旋锁的使用

  自旋锁是一种对临界资源进行互斥访问的典型手段。

  操作:1. 定义 spinlock_t lock;

  2. 初始化 spin_lock_init(&lock);

  3 获得 spin_lock(&lock); 自旋等待 spin_trylock(&lock); 非阻塞,立即返回。

  4. 释放 spin_unlock(&lock);

  注意:自旋锁实际是忙等锁;自旋锁可能导致系统死锁。

具体的例子:

假设系统中有AB两个线程,当A线程执行到下面的代码时将可能会丢失处理器:
spinlock_t   g_lock = SPIN_LOCK_UNLOCKED;  //一个全局的spin_lock 变量
A代码:
spin_lock_init(&g_lock);
spin_lock (&g_lock);
copy_from_user(…);  //A线程在该函数中失去处理器的拥有权
被调度执行的B线程代码:

spin_lock (&g_lock); //因为已经休眠的A线程没有释放g_lock,所以B线程将永远在此处自旋下去。注意此时中断和抢占都是关闭的,即被动放弃处理器已经不可能。
互斥问题的复杂性恰恰就在这里:休眠可发生在许多无法预期的地方。因此当我们编写需要在自旋锁下执行的代码时,必须注意每一个所调用的函数

 
  读写自旋锁

  读写自旋锁可允许读的并发。

  操作:1. 定义初始化 rwlock_t my_rwlock = RW_LOCK_UNLOCKED;

  rwlock_t my_rwlock;

  rwlock_init(&my_rwlock);

  2. 读锁定 void read_lock(rw_lock_t *lock);

  3. 读解锁 void read_unlock(rw_lock_t *lock);

  4. 写锁定 void write_lock(rw_lock_t *lock);

       void write_trylock(rw_lock_t *lock);

  5. 写解锁 void write_unlock(rw_lock_t *lock);

  顺序锁

  顺序锁中读或写单元都不会被对方阻塞,但是写写仍然互斥。

  顺序锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写单元可能使得指针失效。

  操作:1. 获得锁void write_seqlock(seqlock_t *sl);

  void write_tryseqlock(seqlock_t *sl);

  2. 释放锁void write_sequnlock(seqlock_t *sl);

  读完后需要进行检查在读期间是否有写操作,如果有则需要重新进行读操作。

  -拷贝-更新

  对于被RCU保护的共享数据结构,读执行单元不需要获得任何锁就可以访问它,不需要锁也使得使用更容易,因为死锁问题就不需要考虑。

  使用RCU写执行单元在访问它前需首先复制一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的CPU都退出对共享数据的操作的时候。

  操作:1. 读锁定 rcu_read_lock() rcu_read_lock_bh()

  2. 读解锁 rcu_read_unlock() rcu_read_unlock_bh()

  3. 同步RCU synchronize_rcu()

  4 挂接回调 void fastcall call_rcu(struct rcu_head *head, void (*func)(struct_rcu_head *rcu) );

  RCU还增加了链表操作函数的RCU版本。

  2.6 RCU得到普遍使用。


3
中断屏蔽

  中断屏蔽将使得中断与进程之间的并发不再发生,而且,由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。但是,需要注意是的是长时间的中断是危险的,有可能导致数据丢失或着系统崩溃。

  local_irq_disable()local_irq_enable()都只能禁止和使能本CPU内的中断,不能解决SMPCPU引发的竞态。

  local_irq_save(flags)除了进行禁止中断操作以外,还保存目前CPU的中断位信息,local_irq_restore(flags)进行相反的操作。

  如果只想禁止中断的底半部,应使用local_bh_disable(),使能底半部使用local_bh_enable()


4
  原子操作

  原子操作指的是在执行过程中不会被别的代码路径所中断的操作。

  Linux内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类,分别针对位和整型变量进行原子操作。

  整型原子操作

  1. 设置值 void atomic_set(atomic_t *v,int i); Atomic_t v = ATOMIC_INT(0);

  2. 获取值 atomic_read(atomic_t *v);

  3. 加减 void atomic_add(int i,atomic_t *v); void atomic_sub(int i,atomic_t *v);

  4. 自增自减 void atomic_inc(atomic_t *v); void atomic_dec(atomic_t *v);

  5. 操作测试 int atomic_inc_and_test(atomic_t *v); int atomic_dec_and_test(atomic_t *v); int atomic_sub_and_test(int i, atomic_t *v);

  操作后测试其值是否为0,为0返回true,否则返回false

  6. 操作返回int atomic_inc_and_return(atomic_t *v); int atomic_dec_and_return(atomic_t *v); int atomic_sub_and_return(int i, atomic_t *v); int atomic_add_and_return(int i, atomic_t *v);

  操作后返回新值。

  位原子操作

  1. 设置位 void set_bit(nr,void *addr); 设置addr地址的第nr位,即将位写1

  2. 清除位 void clear_bit(nr,void *addr); 将位写为0

  3. 改变位 void change_bit(nr,void *addr); 将位进行反置。

  4. 测试位test_bit(nr,void *addr); 返回第nr位。

  5. 测试操作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 ***_available =ATOMIC_INIT(1);

  ***_open

  {

  if(!atomic_dec_and_test(&***_available))

  {

  atomic_inc(&***_available);

  return - EBUSY;

  }

  }

  ***_release

  {

  atomic_inc(&***_available);

  }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值