# 并发与竞争 ## 原子操作 原子操作只能对整形变量或者位进行保护 ### 原子整形操作 ```c // include/linux/types.h // 如果使用 32位的 SOC typedef struct { int counter; } atomic_t; // 如果使用 64位的 SOC typedef struct { long long counter; } atomic64_t; // 下面所有介绍都基于32位系统说明 // 使用示例 atomic_t a; /*定义a */ atomic_t v = ATOMIC_INIT(0); /* 定义并初始化原子变零v=0 */ atomic_set(&v,10); /* 设置v=10 */ atomic_read(&v); /* 读取v的值,肯定是10 */ atomic_inc(&v); /* v的值加1,v=11 */ ``` 基本API | 函数 | 功能 | | ------------------------------------------- | ----------------------------------------- | | ATOMIC_INIT(int i) | 定义原子变量的时候对其初始化 | | int atomic_read(atomic_t *v) | 读取v的值,并且返回 | | void atomic_set(atomic_t *v, int i) | 向v写入 i值 | | void atomic_add(int i, atomic_t *v) | 给v加上 i值 | | void atomic_sub(int i, atomic_t *v) | 从v减去 i值 | | void atomic_inc(atomic_t *v) | 给v加 1,也就是自增 | | void atomic_dec(atomic_t *v) | 从v减 1,也就是自减 | | int atomic_dec_return(atomic_t *v) | 从v减 1,并且返回 v的值 | | int atomic_inc_return(atomic_t *v) | 给v加 1,并且返回 v的值 | | int atomic_sub_and_test(int i, atomic_t *v) | 从v减 i,如果结果为 0就返回真,否则返回假 | | int atomic_dec_and_test(atomic_t *v) | 从v减 1,如果结果为 0就返回真,否则返回假 | | int atomic_inc_and_test(atomic_t *v) | 给v加 1,如果结果为 0就返回真,否则返回假 | | int atomic_add_negative(int i, atomic_t *v) | 给v加 i,如果结果为负就返回真,否则返回假 | ### 原子位操作 基本API | 函数 | 功能 | | ---------------------------------------- | --------------------------------------------- | | void set_bit(int nr, void *p) | 将p地址的第 nr位置 1。 | | void clear_bit(int nr,void *p) | 将p地址的第 nr位清零。 | | void change_bit(int nr, void *p) | 将p地址的第 nr位进行翻转。 | | int test_bit(int nr, void *p) | 获取p地址的第 nr位的值。 | | int test_and_set_bit(int nr, void *p) | 将p地址的第 nr位置 1,并且返回 nr位原来的值。 | | int test_and_clear_bit(int nr, void *p) | 将p地址的第 nr位清零,并且返回 nr位原来的值。 | | int test_and_change_bit(int nr, void *p) | 将p地址的第 nr位翻转,并且返回 nr位原来的值。 | ### 原子变量实验 参见代码【07.atomic】工程,该工程在【05.led_pinctrl_gpio】工程上只修改入口参数和open函数 入口函数新增原子变量的初始化 ```c static int __init led_init(void) { /* * 初始化原子变量 初始值为1 */ atomic_set(&g_led_dev.lock, 1); /* * 3:注册自己的 platform_driver 到平台的总线驱动链表里面 * 注册的过程中会根据匹配规则 到【总线平台设备链表】里面找, * 如果匹配成功会调用驱动里面的platform_driver下的probe函数 */ printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__); return platform_driver_register(&led_driver); } ``` open函数新增原子变量来防止重复open的问题 ```c static int led_drv_open (struct inode *node, struct file *file) { /* * 如果dec后 通过判断原子变量的值来检查LED有没有被别的应用使用 * 1:lock初始化为1 * 2:第一次open后,lock=0 atomic_dec_and_test(&g_led_dev.lock) 为真,执行下面的hw_led_init * 3:如果第二次open的时候,atomic_dec_and_test(&g_led_dev.lock)为假(lock=-1), 故而执行atomic_inc(&g_led_dev.lock),把lock=0;且直接返回,提示用户Dev busy * 4:如果再有应用open的话,和3条一样 */ if (!atomic_dec_and_test(&g_led_dev.lock)) { atomic_inc(&g_led_dev.lock); /* 小于0的话就加1,使其原子变量等于0 */ printk("%s %s line %d:Dev busy locking\n", __FILE__, __FUNCTION__, __LINE__); return -EBUSY; /* LED被使用,返回忙 */ } hw_led_init(g_led_dev.nd); file->private_data = &g_led_dev; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } ``` ## 自旋锁 概念:对于自旋锁而言,如果自旋锁正在被线程 A持有,线程 B想要获取自旋锁,那么线程 B就会处于忙循环-旋转-等待状态,线程B不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待线程A释放锁。 缺点:等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁 的持有时间不能太长。自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法了。 注意☀️:自旋锁加锁后。不要调用引起系统睡眠和阻塞的API的函数,否则会可能导致锁死现象 Linux关于自旋锁的结构体定义如下 ```c typedef struct spinlock { union { struct raw_spinlock rlock; #ifdef CONFIG_DEBUG_LOCK_ALLOC # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) struct { u8 __padding[LOCK_PADSIZE]; struct lockdep_map dep_map; }; #endif }; } spinlock_t; ``` 自旋锁API 函数 | 函数 | 功能 | | ------------------------------------ | ------------------------------------------------------------ | | DEFINE_SPINLOCK(spinlock_t lock) | 定义并初始化一个自旋变量。 | | int spin_lock_init(spinlock_t *lock) | 初始化自旋锁。 | | void spin_lock(spinlock_t *lock) | 获取指定的自旋锁,也叫做加锁。 | | void spin_unlock(spinlock_t *lock) | 释放指定的自旋锁。 | | int spin_trylock(spinlock_t *lock) | 尝试获取指定的自旋锁,如果没有获取到就立即返回0,不在自旋等待 | | int spin_is_locked(spinlock_t *lock) | 检查指定的自旋锁是否被获取,如果没有被获取就返回非0,否则返回0 | 和中断有关的自旋锁 | 函数 | 功能 | | ------------------------------------------------------------ | -------------------------------------------------------- | | void spin_lock_irq(spinlock_t *lock) | 禁止本地中断并获取自旋锁 | | void spin_unlock_irq(spinlock_t *lock) | 激活本地中断,并释放自旋锁 | | void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) | 保存中断状态,禁止本地中断,并获取自旋锁。 | | void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁 | 使用场景: 一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock 应用举例 ```c DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */ /* 线程A */ void functionA () { unsigned long flags; /* 中断状态 */ spin_lock_irqsave(&lock, flags) /* 获取锁 */ /* 临界区 */ spin_unlock_irqrestore(&lock, flags) /* 释放锁 */ } /* 中断服务函数 */ void irq() { spin_lock(&lock) /* 获取锁 */ /* 临界区 */ spin_unlock(&lock) /* 释放锁 */ } ``` 下半部 (BH)也会竞争共享资源,有些资料也会将下半部叫做底半部。如果要在下半部里面使用自旋锁, API函数: | 函数 | 功能 | | ------------------------------------- | -------------------------- | | void spin_lock_bh(spinlock_t *lock) | 关闭下半部,并获取自旋锁。 | | void spin_unlock_bh(spinlock_t *lock) | 打开下半部,并释放自旋锁 | ### 自旋锁实验 参见代码【08.spinlock】工程,该工程在【07.atomic】工程上只修改入口函数、open和close函数 1. 入口函数 ```c static int __init led_init(void) { /* * 初始化自旋锁 */ spin_lock_init(&g_led_dev.lock); g_led_dev.dev_stats = 0; /* * 3:注册自己的 platform_driver 到平台的总线驱动链表里面 * 注册的过程中会根据匹配规则 到【总线平台设备链表】里面找, * 如果匹配成功会调用驱动里面的platform_driver下的probe函数 */ printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__); return platform_driver_register(&led_driver); } ``` 2. open函数 ```c static int led_drv_open (struct inode *node, struct file *file) { unsigned long flags; spin_lock_irqsave(&g_led_dev.lock, flags); /* 上锁 */ if (g_led_dev.dev_stats) { /* 如果设备被使用了 */ printk("%s %s line %d:dev is busy\n", __FILE__, __FUNCTION__, __LINE__); spin_unlock_irqrestore(&g_led_dev.lock, flags);/* 解锁 */ return -EBUSY; } hw_led_init(g_led_dev.nd); file->private_data = &g_led_dev; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } ``` 3. close函数 ```c static int led_drv_close (struct inode *node, struct file *file) { unsigned long flags; struct led_char_dev *dev = file->private_data; /* 关闭驱动文件的时候将dev_stats减1 */ spin_lock_irqsave(&dev->lock, flags); /* 上锁 */ if (dev->dev_stats) { dev->dev_stats--; } spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */ printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); hw_led_reinit(); return 0; } ``` ## 读写自旋锁 在自旋锁的基础上衍生而来 特点:允许读并发 ```c typedef struct { arch_rwlock_t raw_lock; } rwlock_t; ``` 读写锁API - 创建和初始化 | 函数 | 功能 | | -------------------------------- | ------------------ | | DEFINE_RWLOCK(rwlock_t lock) | 定义并初始化读写锁 | | void rwlock_init(rwlock_t *lock) | 初始化读写锁 | - 读锁 | 函数 | 功能 | | ------------------------------------------------------------ | ---------------------------------------------------- | | void read_lock(rwlock_t *lock) | 获取读锁 | | void read_unlock(rwlock_t *lock) | 释放读锁 | | void read_lock_irq(rwlock_t *lock) | 禁止本地中断,并且获取读锁 | | void read_unlock_irq(rwlock_t *lock) | 打开本地中断,并且释放读锁 | | void read_lock_irqsave(rwlock_t *lock,unsigned long flags) | 保存中断状态,禁止本地中断,并获取读锁 | | void read_unlock_irqrestore(rwlock_t *lock,unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放读 | | void read_lock_bh(rwlock_t *lock) | 关闭下半部,并获取读锁 | | void read_unlock_bh(rwlock_t *lock) | 打开下半部,并释放读锁 | - 写锁 | 函数 | 功能 | | ------------------------------------------------------------ | ------------------------------------------------------ | | void write_lock(rwlock_t *lock) | 获取写 锁 | | void write_unlock(rwlock_t *lock) | 释放写 锁 | | void write_lock_irq(rwlock_t *lock) | 禁止本地中断,并且获取写 锁 | | void write_unlock_irq(rwlock_t *lock) | 打开本地中断,并且释放写 锁 | | void write_lock_irqsave(rwlock_t *lock,unsigned long flags) | 保存中断状态,禁止本地中断,并获取写 锁 | | void write_unlock_irqrestore(rwlock_t *lock,unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁 | | void write_lock_bh(rwlock_t *lock) | 关闭下半部,并获取读锁 | | void write_unlock_bh(rwlock_t *lock) | 打开下半部,并释放读锁。 | ## 顺序锁 在读写锁的基础上衍生而来允许读写并发,但是在读操作的时候同时进行了写操作,最好重新读取,保证读取的数据正确 最好重新进行读取,保证数据完整性。顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。 Linux 关于顺序锁的定义 ```c typedef struct { struct seqcount seqcount; spinlock_t lock; } seqlock_t; ``` - 顺序锁定义并初始化 | 函数 | 功能 | | ------------------------------- | ------------------ | | DEFINE_SEQLOCK(seqlock_t sl) | 定义并初始化顺序锁 | | void seqlock_ini seqlock_t *sl) | 初始化顺序锁 | - 顺序锁写操作 | 函数 | 功能 | | ------------------------------------------------------------ | -------------------------------------------------- | | void write_seqlock(seqlock_t *sl) | 获取写顺序锁。 | | void write_sequnlock(seqlock_t *sl) | 释放写顺序锁。 | | void write_seqlock_irq(seqlock_t *sl) | 禁止本地中断,并且获取写顺序锁 | | void write_sequnlock_irq(seqlock_t *sl) | 打开本地中断,并且释放写顺序锁。 | | void write_seqlock_irqsave(seqlock_t *sl,unsigned long flags) | 保存中断状态,禁止本地中断,并获取写顺序锁。 | | void write_sequnlock_irqrestore(seqlock_t *sl,unsigned long flags) | 恢复以前的中断状态,并激活本地中断,释放写顺序锁。 | | void write_seqlock_bh(seqlock_t *sl) | 关闭下半部,并获取写读锁。 | | void write_sequnlock_bh(seqlock_t *sl) | 打开下半部,并释放写读锁。 | - 顺序锁读操作 | 函数 | 功能 | | ---------------------------------------------------------- | ------------------------------------------------------------ | | unsigned read_seqbegin(const seqlock_t *sl) | 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号。 | | unsigned read_seqretry(const seqlock_t *sl,unsigned start) | 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读 | 自旋锁使用注意事项 - 因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要 短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处 理方式,比如稍后要讲的信号量和互斥体。 - 自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API函数,否则的话可能 导致 死锁。 - 不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就 必须“自旋”, 等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己 把自己锁死了! - 在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还 是多核的 SOC,都将其当做多核 SOC来编写驱动程序。 ## 信号量 相比于自旋锁,信号量可以使线程进入休眠状态,但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态 以后会切换线程,切换线程就会有开销。总结一下信号量的特点: - 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合; - 因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠; - 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。 数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。线程申请一次信号量就会自减一次知道为0;如果要互斥的访问共享资源那么信号量的值就不能大于1,此时的信号量就是一个二值信号量。 Linux内核使用 semaphore结构体表示信号量,结构体内容如下所示: ```c struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; }; ``` 信号量的API函数 | 函数 | 功能 | | ---------------------------------------------- | ------------------------------------------------------------ | | DEFINE_SEAMPHORE(name) | 定义一个信号量,并且设置信号量的值为 | | void sema_init(struct semaphore *sem, int val) | 初始化信号量sem,设置信号量值为 val。 | | void down(struct semaphore *sem) | 获取信号量,因为会导致休眠,因此不能在中断中使用。 | | int down_trylock(struct semaphore *sem); | 尝试获取信号量,如果能获取到信号量就获取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。 | | int down_interruptible(struct semaphore *sem) | 获取信号量,和down类似,只是使用 down进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。 | | void up(struct semaphore *sem) | 释放信号量 | 应用举例 ```c struct semaphore sem; /* 定义信号量 */ sema_init(&sem, 1); /* 初始化信号量 */ down(&sem); /* 申请信号量 */ /* 临界区 */ up(&sem); /* 释放信号量 */ ``` ### 信号量实验 参见代码【09.semaphone】工程,入口函数、open和close函数 1. 入口函数 ```c static int __init led_init(void) { /* * 初始化信号量 */ sema_init(&g_led_dev.sem, 1); /* * 3:注册自己的 platform_driver 到平台的总线驱动链表里面 * 注册的过程中会根据匹配规则 到【总线平台设备链表】里面找, * 如果匹配成功会调用驱动里面的platform_driver下的probe函数 */ printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__); return platform_driver_register(&led_driver); } ``` 2. open函数 ``` static int led_drv_open (struct inode *node, struct file *file) { /* 获取信号量 */ if (down_interruptible(&g_led_dev.sem)) { /* 获取信号量,进入休眠状态的进程可以被信号打断 */ printk("%s %s line %d:sem get err ,dev busy\n", __FILE__, __FUNCTION__, __LINE__); return -ERESTARTSYS; } #if 0 down(&gpioled.sem); /* 不能被信号打断 */ #endif hw_led_init(g_led_dev.nd); file->private_data = &g_led_dev; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } ``` 3. close函数 ```c static int led_drv_close (struct inode *node, struct file *file) { struct led_char_dev *dev = file->private_data; up(&dev->sem); /* 释放信号量,信号量值加1 */ printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); hw_led_reinit(); return 0; } ``` ## 互斥体 将信号量的值设置为1就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux提供了一个比信号量更专业的机制来进行 互斥,它就是互斥体 mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申 请互斥体。 在我们编写 Linux驱动的时候遇到需要互斥访问的地方建议使用 mutex LINUX关于互斥体的定义如下 ```c struct mutex { atomic_t count; /* 1: unlocked, 0: locked, negative: locked, possible waiters */ spinlock_t wait_lock; }; ``` 在使用 mutex之前要先定义一个 mutex变量。在使用 mutex的时候要注 意如下几点: - mutex可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。 - 和信号量一样,mutex保护的临界区可以调用引起阻塞的 API函数。 - 因为一次只有一个线程可以持有mutex,因此,必须由 mutex的持有者释放 mutex。并且 mutex不能递归上锁和解锁。 关于互斥体的API函数 | 函数 | 功能 | | ------------------------------------------------ | ----------------------------------------------------- | | DEFINE_MUTEX(name) | 定义并初始化一个mutex变量。 | | void mutex_init(mutex *lock) | 初始化mutex。 | | void mutex_lock(struct mutex *lock) | 获取mutex,也就是给 mutex上锁。如果获取不到就进休眠。 | | void mutex_unlock(struct mutex *lock) | 释放mutex,也就给 mutex解锁。 | | int mutex_trylock(struct mutex *lock) | 尝试获取mutex,如果成功就返回 1,如果失败就返回 0。 | | int mutex_is_locked(struct mutex *lock) | 判断 mutex是否被获取,如果是的话就返回1,否则返回 0。 | | int mutex_lock_interruptible(struct mutex *lock) | 使用此函数获取信号量失败进入休眠以后可以被信号打断。 | 应用举例 ```c struct mutex lock; /* 定义一个互斥体 */ mutex_init(&lock); /* 初始化互斥体 */ mutex_lock(&lock); /* 上锁 */ /* 临界区 */ mutex_unlock(&lock); /* 解锁 */ ``` ### 互斥体实验 参见工程【10.mutex】 1. 入口函数 ```c static int __init led_init(void) { /* * 初始化互斥变量 */ mutex_init(&g_led_dev.lock); /* * 3:注册自己的 platform_driver 到平台的总线驱动链表里面 * 注册的过程中会根据匹配规则 到【总线平台设备链表】里面找, * 如果匹配成功会调用驱动里面的platform_driver下的probe函数 */ printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__); return platform_driver_register(&led_driver); } ``` 2. open函数 ```c static int led_drv_open (struct inode *node, struct file *file) { /* 获取互斥体,可以被信号打断 */ if (mutex_lock_interruptible(&g_led_dev.lock)) { printk("%s %s line %d:mutex get err dev busy\n", __FILE__, __FUNCTION__, __LINE__); return -ERESTARTSYS; } #if 0 mutex_lock(&g_led_dev.lock); /* 不能被信号打断 */ #endif hw_led_init(g_led_dev.nd); file->private_data = &g_led_dev; printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } ``` 3. close函数 ```c static int led_drv_close (struct inode *node, struct file *file) { struct led_char_dev *dev = file->private_data; /* 释放互斥锁 */ mutex_unlock(&dev->lock); printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); hw_led_reinit(); return 0; } ```