Linux 驱动框架---驱动中的并发

      并发指多个执行单元可以被同时、并行的执行,而并发执行的单元对共享资源的访问就容易导致竟态。并发产生的情况分为单核(抢占)和多核(并行)和中断(打断)。Linux为解决这一问题增加了一系列的接口来解决并发导致的竟态问题。其中原子操作是最基本的机制。

原子操作

  通常一句C代码在被翻译成汇编时可能不止一句,如常见的使用一个全局变量作为标志位来标志共享资源的使用情况这种机制的细节如下:

if(flags!= BUSY){
  flasg = BUSY;
  ops,,
  。。。

这种方式会有如下的风险,如果在线程级和中断都使用一段共享资源,当线程执行了判断之后因为执行或编译乱序对共享资源的操作早于置起标志位,然后中断来了此时中断也开始使用共享资源,执行完后线程继续,但是对于线程操作而言是被中断了的在有些情况下是不允许的所以需要一种单步(无法在分割为更小的执行步骤)就可以完成的原子操作来保证这种互斥所以有了原子操作,这是一种有硬件支持的操作所以肯定是汇编实现分整形和位原子操作。

整形

1、创建和设置原子变量
void atomic_set(atomic_t *v,int i);设置原子变量
atomic_t v = ATOMIC_INIT(x);定义并初始化为x
2、获取
atomic_read(atomic_t* v);
3、增加和减少
void atomic_add(in i ,atomic_t* v);v+=i
void atomic_sub(in i ,atomic_t* v);v-=i
4、自增和自减
void atomic_inc(atomic_t* v);//v++
void atomic_dec(atomic_t* v);//v--
5、操作并测试
int atomic_inc_and_test(atomic_t* v);//++v==0?
int atomic_add_and_test(in i ,atomic_t* v);v+=i;v==0?
int atomic_sub_and_test(in i ,atomic_t* v);v-=i;v==0?
6、操作并返回
int atomic_inc_and_return(atomic_t* v);//return ++v;
int atomic_dec_and_return(atomic_t* v);//return --v;
int atomic_add_and_return(in i ,atomic_t* v);return(v+=i);
int atomic_sub_and_return(in i ,atomic_t* v);return(v-=i);

如果是64位的平台还支持64位的整形操作atomic64_xxx()。原子操作是后续其他有些互斥实现操作的基础。

位原子

1、设置bit
void set_bit()
2、清除bit
void clear_bit()
3、取反对应bit
void change_bit()
4、测试bit
int test_bit()
5、测试和操作bit
int test_and_set()
int test_and_clear()
int test_and_change()

自旋锁

  自旋锁最初就是为了SMP系统设计的,实现在多处理器情况下保护临界区。所以在SMP系统中,自旋锁的实现是完整的本来面目。但是对于UP系统自旋锁可以说是SMP版本的阉割版。因为只有在SMP系统中的自旋锁才需要真正“自旋”。因为竟态产生有三种情况:

  • 单核支持抢占的内核中的进程间抢占
  • 单核不支持抢*断和进程间的抢占
  • 多核系统的真正并发执行

所以自旋锁的目的就是在不同的场景下分别阻止其中一种或多种竟态产生,从而保证这个临界区的内容不会同时被两个执行单元访问而造成数据额不同步或混乱。在内核中常常用来保护对内核数据结构的操作。执行的过程就是执行单元到达临界区判断临界区是否可用如果可以获取临界资源则获取资源继续执行,此时若另一个执行单元执行到达这里会发现资源被占用则会原地执行检查直到资源可用才可以继续往下执行。说道这里应该就会发现一个问题如果一个低优先级的的执行单元先获取了临界资源还未使用完此时高优先级任务来到了那么高优先级任务就会一直自旋并且,低优先级无法获取CPU时间无法继续执行临界资源无法释放就会造成死锁,同样中断也会出现类似情况所以,但单核系统中如果不支持抢占则自旋锁获取锁只需要关闭中断就可以,如果还支持强占则还需要关闭抢占;最后再来看多核系统环境下上面的两种情况的肯定都辉有除此之外还有就是多个核心代码的执行是并行的,要想A核心拿到共享资源后B核心不会拿到关闭中断和抢占是不够的,多核系统之间内存访问是相互可见的,所以在SMP平台下的自旋锁是需要操作内存从而做到和其他核心互斥访问共享资源的,这就需要有一块内存来标志共享资源的状态,综上这就是自旋锁的工作原理全部内容,其中抢占和单核还是多核是在编译构建内核时可以配置的所以配置好后自旋锁对应的API接口的实现就已经支持的当前系统配置的自旋操作,而是否需要屏蔽中断则是只有程序开发者知道,因为共享资源的访问会不会发生在中断中这是应用逻辑的内容。接下来记录一下自旋锁的常用API接口:

不会在任何中断例程中操作临界区:
static inline void spin_lock(spinlock_t*lock)

static inline void spin_unlock(spinlock_t*lock)
如果在软件中断中操作临界区:
static inline void spin_lock_bh(spinlock_t*lock)

static inline void spin_unlock_bh(spinlock_t*lock)
bh代表bottom half,也就是中断中的底半部,因内核中断的底半部一般通过软件中断(tasklet等)来处理而得名。
如果在硬件中断中操作临界区:
static inline void spin_lock_irq(spinlock_t*lock)

static inline void spin_unlock_irq(spinlock_t*lock)
如果在控制硬件中断的时候需要同时保存中断状态:
spin_lock_irqsave(lock, flags)

static inline void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags)

读写锁

     读写锁的出现是为了优化自旋锁的不足,因为常常存在这样一种情况,有一个线程只会读取共享资源而另一个进程则只会进行写入,这种情况很常见但是如果使用自旋锁系统的效率就很低,因为读接口等写接口使用完,所以为了解决这样缺点Linux内核定义了读写锁,即多个执行单元写操作相互互斥而写和读单元也互斥,但是读和读之间就不需要互斥这样的好处是明显的读取锁可以多次获取。API如下:

//定义
rwlock_t xxx_rwlock;
//初始化
rwlock_init(rwlock_t* lock);
//读锁定
read_lock(rwlock_t* lock);
read_lock_irqsave(rwlock_t* lock,unsigned long flags);//关中断并记录中断之前的值后续恢复
read_lock_irq(rwlock_t* lock);//关中断
read_lock_bh(rwlock_t* lock);//关底半部
//读解锁
read_unlock(rwlock_t* lock);
read_unlock_irqsave(rwlock_t* lock,unsigned long flags);//开中断并恢复中断之前的值后续
read_unlock_irq(rwlock_t* lock);//开中断
read_unlock_bh(rwlock_t* lock);//开中断
//写锁定
write_lock(rwlock_t* lock);
write_lock_irqsave(rwlock_t* lock,unsigned long flags);
write_lock_irq(rwlock_t* lock);
write_lock_bh(rwlock_t* lock);
//写解锁
write_unlock(rwlock_t* lock);
write_unlock_irqsave(rwlock_t* lock,unsigned long flags);
write_unlock_irq(rwlock_t* lock);
write_unlock_bh(rwlock_t* lock);

顺序锁

    顺序锁又是对读写锁性能的优化,增加了对读和写的并发支持,如果读取过程有过写的操作就需要重读,这个机制是要有编程人员主动调用接口查询的,读取结束后通过接口查询是否发生过写如果发生过就需要重新读取。API:

seqlock_init(x);       //动态初始化
DEFINE_SEQLOCK(x);     //静态初始化
//获取顺序锁
void write_seqlock(seqlock_t* sl);        //写加锁
int write_tryseqlock(seqlock_t* sl);      //尝试写加锁
write_seqlock_irqsave(lock, flags);       //local_irq_save() + write_seqlock()
write_seqlock_irq(lock);                  //local_irq_disable() + write_seqlock()
write_seqlock_bh(lock);                  //local_bh_disable() + write_seqlock()
//释放顺序锁
void write_sequnlock(seqlock_t* sl);         //写解锁
write_sequnlock_irqrestore(lock, flags);     //write_sequnlock() + local_irq_restore()
write_sequnlock_irq(lock);                   //write_sequnlock() + local_irq_enable()
write_sequnlock_bh(lock);                    //write_sequnlock() + local_bh_enable()

读操作

//读操作
unsigned int read_seqbegin(const seqlock_t* sl);
read_seqbegin_irqsave(lock, flags);          //local_irq_save() + read_seqbegin()

读执行单元在访问共享资源时要调用顺序锁的读函数,返回顺序锁s1的顺序号;该函数没有任何获得锁和释放锁的开销,只是简单地返回顺序锁当前的序号;

重读

int read_seqretry(const seqlock_t* sl, unsigned start);
read_seqretry_irqrestore(lock, iv, flags);

在顺序锁的一次读操作结束之后,调用顺序锁的重读函数,用于检查是否有写执行单元对共享资源进行过写操作;如果有就会重新读取共享资源;iv为顺序锁的id号;

信号量

  信号量是所有系统软件中最典型的用于同步和互斥的软件手段,进程执行前先获取信号量如果获取成功则继续执行如果获取不到则会阻塞。并且会将当前进程挂接到该对象的等待队列上,如果由进程释放这个信号量时就会唤醒这个队列上的线程,顺序是谁先阻塞先唤醒谁还是谁优先级高先唤醒谁??。内容比较简直接看API:

//定义
struct semaphore sem;
//初始化
void sema_init();
//获取信号量
void down();
//进程阻塞后可被信号唤醒
void down_interruptible();
//获取信号量非阻塞版
void down_trylock();
//释放信号量
void up();

互斥体

  互斥体应该是比信号量更加适合资源互斥的机制了,他和信号量的最大区别就是信号量的值可以大过1,而互斥信号量只能是0和1。他的常用使用API:

//定义
struct mutex my_mutex;
//初始化
void mutex_init();
//获取互斥
void mutex_lock();
//进程阻塞后可被信号唤醒
void mutex_lock_interruptible();
//获取互斥量非阻塞版
void mutex_lock_trylock();
//释放
void mutex_unlock();
//实例
struct mutex my_mutex;

mutex_init(&my_mutex);
mutex_lock(&my_mutex);
.
.
.
void mutex_unlock(&my_mutex);

完成量

比较冷门暂时不记录了,不用容易忘记。

 

Linux 驱动框架---驱动中的并发

上一篇:Linux 上编译 redis-6.0.6


下一篇:KAL1 LINUX 官方文档之安装 ---BTRFS安装