linux设备驱动的并发控制学习笔记(代码片段)

Wu_Being Wu_Being     2022-12-04     749

关键词:

文章目录

并发和竞态

并发:多个执行单元同时、并行被执行。

共享资源:硬件资源、软件的全局变量和静态变量。

竞态:多个执行单元存在对共享资源的访问。

竞态发生情况:

  1. 对称多处理器(SMP)的多个CPU;
    a. CPU0 的进程和CPU1 的进程;
    b. CPU0 的进程和CPU1 的中断;
    c. CPU0 的中断和CPU1 的中断;

  2. 单CPU 内进程与抢占它的进程;
    Linux2.6 支持内核抢占调度

  3. 中断(硬中断、软中断、Tasklet、底半部)与进程之间;
    Linux2.6.35 取消嵌套中断

编译乱序和执行乱序

编译乱序:编译器行为,用barrier() 编译屏障处理;

执行乱序:处理器运行时的行为;ARM 处理器屏障指令:DMB/DS/ISB,Linux 定义读写屏障mb()、读屏障rmb()、写屏障wmb(),寄存器的读写__iormb()、__iowmb(),API readl()/readl_relaxed(), writel()/writel_relaxed()。

并发控制机制

  1. 中断屏蔽:针对单核处理器
  2. 原子操作:针对整型数据
  3. 自旋锁:多核或可抢占的单核
  4. 信号量:PV操作,不会忙等
  5. 互斥体:同信号量,进程级

中断屏蔽

中断屏蔽是对当前CPU屏蔽中断,适合单CPU,但长时间屏蔽中断比较危险。

  1. 中断和进程不再并发;
  2. 异步IO、进程调度依赖中断实现,抢占也能避免;
local_irq_disable();    // 屏蔽中断
...
critical section        // 临界区
...
local_irq_ensable();    // 开中断

local_irq_save(flag)local_irq_restore(flag) 处理屏蔽恢复中断,还可以保存恢复中断信息;

local_bh_disable()local_bh_enable() 只禁止和恢复中断的底半部;

原子操作

可以保证对一个整型数据的修改是排他性,包括位和整型变量的原子操作。

整型原子操作

  1. 设置原子变量的值
void atomic_set(atomic_t *v, int);
atomic_t v = ATOMIC_INIT(0);
  1. 获取原子变量的值
atomic_read(atomic_t *v);
  1. 原子变量的加减
void atomic_add(atomic_t *v, int);
void atomic_sub(atomic_t *v, int);
  1. 原子变量自增自减
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
  1. 操作并测试
int atomic_inc_and_test(atomic_t *v);   //test 为0 返回 ture
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
  1. 操作并返回
int atomic_add_return(int i, atomic_t *v);   //reture new data
int atomic_sub_return(int i, atomic_t *v); 
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);

linux kernel 休眠的wakeup sources 也是用到原子变量的机制。

位原子操作

  1. 设置位
void set_bit(nr, void *addr);       // 地址addr 的nr 位写位1
  1. 清除位
void clear_bit(nr, void *addr);
  1. 改变位
void change_bit(nr, void *addr);    // 地址addr 的nr 位反置
  1. 测试位
test_bit(nr, void *addr);           // 返回地址addr 的nr 位
  1. 测试并操作位
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_avaiable = ATOMIC_INIT(1); 

static ssize_t xxx_open(struct inode *inode, struct file *flip)

	...
    if(!atomic_dec_and_test(&xxx_available)) 
        atomic_inc(&xxx_available);
        return -EBUSY;               // 已经打开过了
    
	...
    return 0;                        // 成功打开


static int xxx_release(struct inode *inode, struct file *filp)

    atomic_inc(&xxx_avaiable);      // 释放设备
    return 0;

自旋锁

自旋锁的使用

spin_lock自旋锁主要针对SMP 或单CPU可抢占的情况,忙等待,但会受中断和底半部影响,所以:

  1. 进程上下文使用:spin_lock_irqsave()和spin_unlock_irqrestore();
  2. 中断上下文使用:spin_lock()和spin_unlock();
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()

自旋锁使用注意

  1. 会忙等待,临界区或共享资源太多会影响系统性能;
  2. cpu 同一个进程递归拿同一个锁,会死锁;(互斥锁也一样)
  3. 拿锁期间,不能调用会让系统调度的函数,如copy_from_user, copy_to_user, kmalloc, msleep 等;
  4. spin_lock_irqsave不能屏蔽其他cpu中断,如果其他cpu 中断没有加spin_lock,也会并发访问统一共享资源;

自旋锁使用例子

使用模板:

spinlock_t lock;
spin_lock_init(&lock);

spin_lock(&lock);
//ret = spin_trylock(&lock);  //如果获取不到,不自旋,返回false
...//临界区
spin_unlcok(&lock);

如下自旋锁使用的设备只能被一个进程打开:

int xxx_count = 0;      // 定义文件打开次数

static ssize_t xxx_open(struct inode *inode, struct file *flip)

	...
	spin_lock(&xxx_count);
    if(xxx_count)      // 已经打开过了
        spin_unlock(&xxx_count);
        return -EBUSY;              
    
    xxx_count++;         // 增加引出次数
    spin_unlock(&xxx_count);
	...
    return 0;                        // 成功打开


static int xxx_release(struct inode *inode, struct file *filp)

    spin_lock(&xxx_count);
    xxx_count--;         //减少加引出次数
    spin_unlock(&xxx_count);
    return 0;

读写自旋锁

rwlock 可以多个进程读,只能一个进程写,读和写不能同时进行。

使用模板:

rwlock_t lock;
rwlock_init(&lock);

// 读时候
read_lock(&lock);
...//临界区
read_unlcok(&lock);

// 写时候
write_lock_irqsave(&lock, flag);
//ret = write_trylock(&lock);  //如果获取不到,不自旋,返回false
...//临界区
write_unlcok_restore(&lock, flag);

顺序锁

seqlock 运行读写同时进行,但写和写不能同时,如果读过程有写需要重读。

写执行单元,还有write_seqlock_irqsavewrite_seqlock_irqwrite_seqlock_bh

write_seqlock(&seqlock);
//ret = write_tryseqlock(&seqlock);  //如果获取不到,不自旋,返回false
...//临界区
write_sequnlcok(&seqlock);

读执行单元,还有read_seqbegin_irqstoreread_seqretry_irqstore

do 
    seqnum = read_seqbegin(&seqlock);
    ...//临界区
 while (read_seqretry(&seqlock, seqnum));

读-复制-更新

RCU (Read-Copy-Update) 读端没有锁、内存屏障、原子指令等开销,只是标记读开始和结束;
写端再访问共享资源先复制一个副本,对副本修改,回调时机把指针指向副本。

允许多个读,允许多个写。逻辑如下:

struct foo 
    struct list_head list;
    int a, b, c;
;
LIST_HEAD(head);

p = search(head, key);
if(p == NULL) 
    // Take appropriate action, unlock and return


q = kmalloc(sizeof(*p), GFP_KERNEL);
*q = *p;
q->a = 1;
q->b = 2;
list_replace_rcu(&p->list, &q->list);
synchronize_rcu(); 宽限期(Grace Period):等待所有读完成。
kfree(p);

Linux RCU 操作:

1. 读锁定和解锁
rcu_read_lock(); // or rcu_read_lock_bh()
...// 临界区
rcu_read_unlock; // or rcu_read_unlock_bh()

2. 同步RCU
synchronize_rcu() 不会等待后续(Subsequent)的读临界区的完成.

3. 挂接回调
...

信号量

Semaphore 处理系统的同步和互斥问题,即操作系统的PV 操作,信号量值可以是0、1…N。

P(s):1. 如果信号量s大于0,进程继续运行;2. s为0,进程等待,直到V操作加信号量唤醒之。

// 定义信号量
struct semaphore sem;
// 初始化信号量为val
void sema_init(struct semaphore *sem, int val);

//获取信号量
void down(struct semaphore *sem);      // 会导致进程睡眠,不能再中断上下文使用
int down_interruptible(struct semaphore *sem); // 睡眠状态的进程可以被信号打断,返回非0,上面函数不能被信号打断
int down_trylock(struct semaphore *sem);  // 如果获取不到,返回非0,不会让调用者睡眠,可以中断上下文使用

if(down_interruptible(&sem))
    return -ERESTARTSYS;

//释放信号量
void up(struct semaphore *sem);

互斥问题

与自旋锁不同的时,等不到信号时,进程不会原地打转而是进入休眠等待状态。Linux 一般用mutex 代替来操作此类问题。

同步问题

进程A 执行down()等待信号量,进程B执行up()释放信号量,这样A 同步的等待B。即生产者和消费者问题。

互斥体

进程级,获取不到锁也会让当前进程休眠,调度上下文切换给其他进程运行。

用法和信号量完全一样,也有mutex_lock_interruptiblemutex_trylock

struct mutex my_mutex;
mutex_init(&my_mutex);

mutex_lock(&my_mutex);
... // 临界区
mutex_unlock(&my_mutex);

自旋锁和互斥锁

  1. 互斥锁的开销是上下文切换时间,自旋锁开销是临界区忙等时间,如果临界区小用自旋锁,如果大就用互斥锁;
  2. 互斥锁临界区可以保护会阻塞的代码(copy_to_user), 自旋锁不行,因为阻塞要调度上下文切换,如果进程切换出去,另外一个进程来获取这个自旋锁,也会死锁;
  3. 互斥锁用与进程上下文,如果有中断或软中断,则用自旋锁,或mutex_trylock;

完成量

Completion 用于执行单元等待另一个执行单元执行完成某事。

// 1. 定义完成量
struct completion my_completion;

// 2. 初始化完成量为0
init_completion(&my_completion);
reinit_completion(&my_completion);

// 3. 等待一个完成量被唤醒
void wait_for_completion(struct completion *c);

// 4. 完成量唤醒
void complete(struct completion *c);    // 只唤醒一个
void complete_all(struct completion *c);// 唤醒所有

相关code : https://github.com/1040003585/linux_driver_study/tree/main/song07–Concurrency-control

linux进程——学习笔记(代码片段)

...作系统进程的概念task_struct的内容进程抢占:并行和并发查看进程创建进程理解fork:进程的状态孤儿进程进程的优先级认识操作系统冯诺依曼体系结构是计算机的基本结构该结构规定了cpu(中央处理器)只和内存... 查看详情

linux驱动开发-字符设备控制技术笔记3(代码片段)

字符设备控制技术  笔记要做的自己看起来舒服和有头绪,这不又折腾切换编辑器来从新排版,有强迫症啊!对于字符控制,很多时候编写上层应用程序时,使用ioctl系统调用来控制设备,原型如下:/*fd:... 查看详情

设备树学习笔记(代码片段)

学习正点原子《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.2.pdf》个人笔记设备树编译  设备树源文件扩展名为.dts,DTS是设备树源码文件,DTB是将DTS编译以后得到的二进制文件。将.dts编译为.dtb需要用到DTC工具,... 查看详情

linux驱动程序中的并发控制-7(互斥体(mutex))-49(代码片段)

互斥体(mutex)互斥体(mutex)使用定义互斥体(#include<linux/mutex.h>)结构体structmutex /*1:unlocked,0:locked,negative:locked,possiblewaiters*/ atomic_t count; spinlock_t wait_lock; st 查看详情

linux驱动程序中的并发控制-7(互斥体(mutex))-49(代码片段)

互斥体(mutex)互斥体(mutex)使用定义互斥体(#include<linux/mutex.h>)结构体structmutex /*1:unlocked,0:locked,negative:locked,possiblewaiters*/ atomic_t count; spinlock_t wait_lock; str 查看详情

linux驱动程序中的并发控制(原子操作)-43(代码片段)

原子操作整型的原子操作使对整型的int的操作变成原子操作,要依靠一个数据类型:atomic_t。此结构体定义在include/linux/types.h文件中,定义如下:typedefstruct intcounter;atomic_t;相关的api#include<asm/atomic.h>函数描述ATO... 查看详情

linux驱动程序中的并发控制-1(原子操作)-43(代码片段)

原子操作整型的原子操作使对整型的int的操作变成原子操作,要依靠一个数据类型:atomic_t。此结构体定义在include/linux/types.h文件中,定义如下:typedefstruct intcounter;atomic_t;相关的api#include<asm/atomic.h>函数描述ATO... 查看详情

linux驱动程序中的并发控制-8(完成量(completion))-50(代码片段)

完成量(completion)完成量用于一个执行单元等待另一个执行单元执行完成某项工作。也就是说,如果在执行某段代码之前必须要执行另一段代码,就要使用完成量。完成量(completion)使用定义完成量结构... 查看详情

linux驱动程序中的并发控制-8(完成量(completion))-50(代码片段)

完成量(completion)完成量用于一个执行单元等待另一个执行单元执行完成某项工作。也就是说,如果在执行某段代码之前必须要执行另一段代码,就要使用完成量。完成量(completion)使用定义完成量结构... 查看详情

linux驱动程序中的并发控制(自旋锁)-44(代码片段)

自旋锁(spinlock)简介原子锁和自旋锁的使用范围原子操作是一种很好的避免竞态的方式,使用非常简单。但在某些方面却显得过于简单。例如,有很多数据需要被格式化,被添加到某些数据结构中,然后... 查看详情

springsecurity-学习笔记-会话管理之并发控制:同一账号只允许在一个设备登录

SpringSecurity-学习笔记-会话管理之并发控制:同一账号只允许在一个设备登录场景需求分析配置`web.xml``application-security.xml`参考消息场景接手一个老项目:先上pom.xml ...略 <dependency> <groupId>org.springfram... 查看详情

linux驱动程序中的并发控制-2(自旋锁)-44(代码片段)

自旋锁(spinlock)简介原子锁和自旋锁的使用范围原子操作是一种很好的避免竞态的方式,使用非常简单。但在某些方面却显得过于简单。例如,有很多数据需要被格式化,被添加到某些数据结构中,然后... 查看详情

学习笔记——《linux设备驱动程序(第三版)》linux设备模型:内核添加删除设备驱动程序(代码片段)

文章目录1.前言2.准备工作2.1.概念2.2.具体总线、设备、驱动结构体说明2.3.注册总线3.添加设备3.1.STEP1——发现设备并创建设备结构structXXX_dev3.2.STEP2——初始化设备结构3.3.STEP3——注册设备4.删除设备5.添加驱动程序6.删除驱动程... 查看详情

[linux高并发服务器]进程控制(代码片段)

[Linux高并发服务器]进程控制此博客为牛客项目教程:Linux高并发服务器的笔记,很多内容摘自其中进程退出C库和Linux系统都提供了进程退出函数exit()和_exit()exit()在_exit()的基础上还进行了刷新缓冲区,关闭文件描述符... 查看详情

linux驱动开发-字符设备控制技术笔记3(代码片段)

字符设备控制技术  笔记要做的自己看起来舒服和有头绪,这不又折腾切换编辑器来从新排版,有强迫症啊!对于字符控制,很多时候编写上层应用程序时,使用ioctl系统调用来控制设备,原型如下:/*fd:... 查看详情

redis学习笔记29——无锁的原子操作:redis如何应对并发访问(代码片段)

为了保证并发访问的正确性,Redis提供了两种方法,分别是加锁和原子操作。但是加锁会遇到两个问题:首先是加锁操作过多会降低系统的并发访问性能其次,Redis客户端要加锁时,需要使用分布式锁,而... 查看详情

redis学习笔记29——无锁的原子操作:redis如何应对并发访问(代码片段)

为了保证并发访问的正确性,Redis提供了两种方法,分别是加锁和原子操作。但是加锁会遇到两个问题:首先是加锁操作过多会降低系统的并发访问性能其次,Redis客户端要加锁时,需要使用分布式锁,而... 查看详情

sx1281驱动学习笔记二:驱动学习(代码片段)

...设置频点、接收发送地址、发送功率六、DIO配置七、收发控制逻辑八、SPI时序和BUSY通过上一篇博客:SX1281驱动学习笔记一:Lora驱动移植_无痕幽雨的博客-CSDN博客,能够实现简单的收和发了,下面继续学习。一、... 查看详情