Linux驱动开发笔记(七)——并发与竞争(下)——自旋锁信号量互斥体
视频:第10.2讲 Linux并发与竞争试验-自旋锁、信号量与互斥体_哔哩哔哩_bilibili
1. 自旋锁
文档:《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf》47.3节
函数
// include/linux/spinlock_types.h
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函数的定义在include/linux/spinlock.h
线程与线程之间:
表中的自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间。被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的 API 函数,否则可能导致死锁:
自旋锁会自动禁止抢占,也就是当线程A得到锁以后会暂时禁止内核抢占。如果线程A在持有锁期间进入休眠,那么线程A会自动放弃CPU,此时线程B开始运行,线程B要获取锁,但是此时锁被A线程持有,而且内核还禁止抢占,此时便死锁了。
线程与中断之间:
如上图,线程A先运行并获得lock。当线程A运行functionA时候,中断发生并抢走了CPU使用权。右边的中断服务函数也要获取lock,但lock被A占有,中断一直等待。但是在中断服务函数执行完之前,线程A无法执行,此时便死锁了。
最好的解决方法就是获取锁之前关闭本地中断(本CPU中断),Linux内核提供了相应的API函数:
使用spin_lock_irq/spin_unlock_irq时用户需要能够确定加锁之前的中断状态,但实际上很难做到,因此不推荐使用这两个函数。
建议使用spin_lock_irqsave/ spin_unlock_irqrestore,这组函数会保存中断状态 / 恢复中断状态。一般在线程中使用spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用spin_lock/spin_unlock,示例代码如下所示:
DEFINE_SPINLOCK(lock); // 定义并初始化一个自旋锁/* 线程A */
void functionA() {unsigned long flags; // 用于保存中断状态// 获取锁并关闭本地中断,保存中断状态到flagsspin_lock_irqsave(&lock, flags);/* 临界区 */// 在此执行共享资源操作// 释放锁并恢复中断状态spin_unlock_irqrestore(&lock, flags);
}/* 中断服务函数 */
void irq() {// 获取锁spin_lock(&lock);/* 临界区 */// 在此执行共享资源操作// 释放锁spin_unlock(&lock);
}
自旋锁注意事项
《开发指南》47.3.4节
①、等待自旋锁时处于“自旋”,因此锁的持有时间不能太长,否则会降低系统性能。如果临界区比较大、运行时间比较长,要选择其他的并发处理方式,如信号量和互斥体。
②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的API函数,否则可能导致死锁。
③、不能递归申请自旋锁,否则死锁。
④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管用的是单核还是多核SOC,都将其当做多核SOC来编写驱动程序。
2. 信号量
详见《开发指南》47.4节
相较于自旋锁,信号量的特点:
①、信号量会以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
②、信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,不适合使用信号量,因为频繁休眠、切换线程的开销要远大于信号量带来的优势。
函数
struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list;
};
DEFINE_SEAMPHORE(name) | 定义一个二值信号量,信号量的值为1 |
void sema_init(struct semaphore *sem, int val) | 设置信号量sem的值为val |
void down(struct semaphore *sem) | 获取信号量,信号量-1,导致休眠,不可被信号打断 |
void up(struct semaphore *sem) | 释放信号量,信号量+1,导致休眠,不可被信号打断 |
int down_trylock(struct semaphore *sem); | 获取信号量,信号量-1,导致休眠,可被信号打断 |
int down_interruptible(struct semaphore *sem) | 释放信号量,信号量+1,导致休眠,可被信号打断 |
3. 互斥体
详见《开发指南》47.5节
互斥访问:一次只能有一个线程可以访问共享资源。
虽然用二值信号量就可以进行互斥访问,但是Linux提供了更专业的机制——互斥体mutex,其特点如下:
①、mutex可以导致休眠,因此不能在中断中使用mutex。中断中只能使用自旋锁。
②、和信号量一样,mutex保护的临界区可以调用引起阻塞的API函数。
③、因为一次只有一个线程可以持有mutex,因此,必须由mutex的持有者释放mutex。并且mutex不能递归上锁和解锁。
mutex互斥体定义如下:
struct mutex { /* 1: unlocked, 0: locked, negative: locked, possible waiters */ atomic_t count; spinlock_t wait_lock;
};
函数
4. 自旋锁实验
第10.4讲 Linux并发与竞争试验-自旋锁、信号量以及互斥体操作实验_哔哩哔哩_bilibili
4.1 文件结构
9_SPINLOCK (工作区)
├── .vscode
│ ├── c_cpp_properties.json
│ └── settings.json
├── 9_spinlock.code-workspace
├── Makefile
├── spinlockAPP.c
└── spinlock.c
4.2 Makefile
CFLAGS_MODULE += -wKERNELDIR := /home/for/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek # 内核路径
# KERNELDIR改成自己的 linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路径(这个文件从正点原子“01、例程源码”中直接搜,cp到虚拟机里面)CURRENT_PATH := $(shell pwd) # 当前路径obj-m := spinlock.o # 编译文件build: kernel_modules # 编译模块kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
4.3 spinlock.c
主要有以下更新:
增加了 头文件引用 #include <linux/spinlock_types.h>
增加了 设备结构体中 自旋锁和设备状态 两个变量
增加了 驱动入口函数中 对自旋锁的初始化
增加了 led_open中 加锁解锁和临界区操作
增加了 led_release中 加锁解锁和临界区操作
备注:
实际使用过程中可能会有中断的问题,因此使用spin_lock_irqsave和spin_unlock_irqrestore更好。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/spinlock_types.h>#define GPIOLED_NAME "gpioled"
#define GPIOLED_CNT 1#define LEDON 1
#define LEDOFF 0/* 设备结构体 */
struct gpioled_dev{dev_t devid;int major;int minor;struct cdev cdev;struct device *device;struct class *class;struct device_node *nd;int led_gpio;int dev_status; /* 0表示设备可以使用,1不可使用*/spinlock_t lock;
};struct gpioled_dev gpioled;/* 操作集 */
static int led_release(struct inode *inode, struct file *filp){unsigned long irqflag;struct gpioled_dev *dev = filp->private_data;spin_lock_irqsave(&dev->lock, irqflag); // 加锁=================if(gpioled.dev_status){gpioled.dev_status = 0; // 设备标记为被空闲}spin_unlock_irqrestore(&dev->lock, irqflag); // 解锁===============return 0;
}
static int led_open(struct inode *inode, struct file *filp){unsigned long irqflag;filp->private_data = &gpioled;spin_lock_irqsave(&gpioled.lock, irqflag); // 加锁=================if(gpioled.dev_status){ /* 设备不可使用 */spin_unlock(&gpioled.lock); //解锁printk("BUSY!!!\r\n");return -EBUSY;}gpioled.dev_status = 1; // 设备标记为被占用spin_unlock_irqrestore(&gpioled.lock, irqflag); //解锁====================return 0;
}static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){int ret;unsigned char databuf[1];struct gpioled_dev *dev = filp->private_data;ret = copy_from_user(databuf, buf, count);if(ret < 0){return -EINVAL;}if(databuf[0] == LEDON){ //开灯gpio_set_value(dev->led_gpio, 0); // 低电平开灯} else if(databuf[0] == LEDOFF){gpio_set_value(dev->led_gpio, 1); // 高电平关灯}return 0;
}
static const struct file_operations led_fops = {.owner = THIS_MODULE,.write = led_write,.open = led_open,.release = led_release,
};/* 驱动入口 */
static int __init led_init(void){int ret = 0;/* 初始化自旋锁 */spin_lock_init(&gpioled.lock);gpioled.dev_status = 0;/* 1.注册字符设备驱动 */gpioled.devid = 0;if(gpioled.devid){gpioled.devid = MKDEV(gpioled.devid, 0);register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);} else {alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);gpioled.major = MAJOR(gpioled.devid);gpioled.minor = MINOR(gpioled.devid);}/* 2.初始化cdev */gpioled.cdev.owner = THIS_MODULE; cdev_init(&gpioled.cdev, &led_fops);/* 3.添加cdev */cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT); // 错误处理先略过了/* 4.创建类 */gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);if(IS_ERR(gpioled.class)){return PTR_ERR(gpioled.class);}/* 5.创建设备 */gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);if(IS_ERR(gpioled.device)){return PTR_ERR(gpioled.device);}/* 1.获取设备节点 */gpioled.nd = of_find_node_by_path("/gpioled"); // 找到刚才在imx6ull-alientek-emmc.dts根节点下加入的gpioled节点if(gpioled.nd == NULL){ret = -EINVAL;goto fail_findnode;}/* 2.获取LED对应的GPIO */ // 也就是节点中led-gpios那一行内容gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpios", 0);if(gpioled.led_gpio < 0){printk("can't find led_gpio\r\n");ret = -EINVAL;goto fail_findnode;}printk("led_gpio num = %d\r\n",gpioled.led_gpio);/* 3.申请IO */ret = gpio_request(gpioled.led_gpio, "led-gpios");if(ret){printk("Failed to request the led gpio\r\n");ret = -EINVAL;goto fail_findnode; }/* 4.使用IO,设置为输出 */ret = gpio_direction_output(gpioled.led_gpio, 1);if(ret){goto fail_setoutput; // 如果代码走到这一步,一定已经成功进行了IO申请,因此这里错误处理时需要释放IO}/* 5.输出高电平,关灯 */gpio_set_value(gpioled.led_gpio, 1);return 0;fail_setoutput:gpio_free(gpioled.led_gpio);
fail_findnode:return ret;
}/* 驱动出口 */
static void __exit led_exit(void){gpio_set_value(gpioled.led_gpio, 1); // 高电平 关灯/* 注销字符设备驱动 */cdev_del(&gpioled.cdev);unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);device_destroy(gpioled.class, gpioled.devid);class_destroy(gpioled.class);gpio_free(gpioled.led_gpio);}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
4.4 spinlockAPP.c
主要有以下更新:
完全没有更新:( 直接贴过来
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>/* * @description : main主程序 * @param - argc : argv数组元素个数 * @param - argv : 具体参数 * @return : 0 成功; else失败* 调用 ./atomicAPP <filename> <0:1> 0关灯,1开灯* ./atomicAPP /dev/gpioled 0 关灯* ./atomicAPP /dev/gpioled 1 开灯*/ #define LEDOFF 0
#define LEDON 1int main(int argc, char *argv[]){if(argc != 3){ // 判断用法是否错误printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0;unsigned char databuf[1];int retvalue = 0;int cnt = 0;filename = argv[1];fd = open(filename, O_RDWR); // 读写模式打开驱动文件filenameif(fd <0){printf("file %s open failed!\r\n");return -1;}databuf[0] = atoi(argv[2]); // char 2 intretvalue = write(fd, databuf, sizeof(databuf)); // 根据驱动里的操作集.write = led_write,执行led_write()函数if(retvalue <0){printf("LED Control Failed!\r\n");close(fd);return -1;}/* 模拟应用占用 */while(1){sleep(3);cnt++;printf("APP Runing times: %d\r\n",cnt);if(cnt >= 5)break;}printf("App finished!\r\n");close(fd);return 0;
}
4.5 测试
# VSCODE终端
make
arm-linux-gnueabihf-gcc spinlockAPP.c -o spinlockAPP
sudo cp spinlock.ko spinlockAPP /..../nfs/rootfs/lib/modules/4.1.15/# 串口
和原子操作实验8一样,如果一个应用程序没有执行完,再执行下一个应用程序会报错。
5. 信号量实验
懒得重新创新的文件夹,直接在上面的代码改了。
spinlock.c主要有以下更新:
增加了 头文件引用 #include <linux/semaphore.h>
增加了 设备结构体中 semaphore类型信号量
增加了 驱动入口函数中 对信号量的初始化
增加了 led_open中 信号量加1
增加了 led_release中 信号量减1。信号量的特点是能让应用休眠,因此led_release中不需要其他操作,信号量不为0时会自动唤醒休眠的应用。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>#define GPIOLED_NAME "gpioled"
#define GPIOLED_CNT 1#define LEDON 1
#define LEDOFF 0/* 设备结构体 */
struct gpioled_dev{dev_t devid;int major;int minor;struct cdev cdev;struct device *device;struct class *class;struct device_node *nd;int led_gpio;struct semaphore sem;
};struct gpioled_dev gpioled;/* 操作集 */
static int led_release(struct inode *inode, struct file *filp){struct gpioled_dev *dev = filp->private_data;up(&gpioled.sem); //信号量加1return 0;
}
static int led_open(struct inode *inode, struct file *filp){filp->private_data = &gpioled;down(&gpioled.sem); // 信号量减1。信号量的特点是能让应用休眠,因此此处不需要其他操作,信号量不为0时会自动唤醒休眠的应用。return 0;
}static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){int ret;unsigned char databuf[1];struct gpioled_dev *dev = filp->private_data;ret = copy_from_user(databuf, buf, count);if(ret < 0){return -EINVAL;}if(databuf[0] == LEDON){ //开灯gpio_set_value(dev->led_gpio, 0); // 低电平开灯} else if(databuf[0] == LEDOFF){gpio_set_value(dev->led_gpio, 1); // 高电平关灯}return 0;
}
static const struct file_operations led_fops = {.owner = THIS_MODULE,.write = led_write,.open = led_open,.release = led_release,
};/* 驱动入口 */
static int __init led_init(void){int ret = 0;/* 初始化信号量 */sema_init(&gpioled.sem, 1); // 二值信号量/* 1.注册字符设备驱动 */gpioled.devid = 0;if(gpioled.devid){gpioled.devid = MKDEV(gpioled.devid, 0);register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);} else {alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);gpioled.major = MAJOR(gpioled.devid);gpioled.minor = MINOR(gpioled.devid);}/* 2.初始化cdev */gpioled.cdev.owner = THIS_MODULE; cdev_init(&gpioled.cdev, &led_fops);/* 3.添加cdev */cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT); // 错误处理先略过了/* 4.创建类 */gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);if(IS_ERR(gpioled.class)){return PTR_ERR(gpioled.class);}/* 5.创建设备 */gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);if(IS_ERR(gpioled.device)){return PTR_ERR(gpioled.device);}/* 1.获取设备节点 */gpioled.nd = of_find_node_by_path("/gpioled"); // 找到刚才在imx6ull-alientek-emmc.dts根节点下加入的gpioled节点if(gpioled.nd == NULL){ret = -EINVAL;goto fail_findnode;}/* 2.获取LED对应的GPIO */ // 也就是节点中led-gpios那一行内容gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpios", 0);if(gpioled.led_gpio < 0){printk("can't find led_gpio\r\n");ret = -EINVAL;goto fail_findnode;}printk("led_gpio num = %d\r\n",gpioled.led_gpio);/* 3.申请IO */ret = gpio_request(gpioled.led_gpio, "led-gpios");if(ret){printk("Failed to request the led gpio\r\n");ret = -EINVAL;goto fail_findnode; }/* 4.使用IO,设置为输出 */ret = gpio_direction_output(gpioled.led_gpio, 1);if(ret){goto fail_setoutput; // 如果代码走到这一步,一定已经成功进行了IO申请,因此这里错误处理时需要释放IO}/* 5.输出高电平,关灯 */gpio_set_value(gpioled.led_gpio, 1);return 0;fail_setoutput:gpio_free(gpioled.led_gpio);
fail_findnode:return ret;
}/* 驱动出口 */
static void __exit led_exit(void){gpio_set_value(gpioled.led_gpio, 1); // 高电平 关灯/* 注销字符设备驱动 */cdev_del(&gpioled.cdev);unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);device_destroy(gpioled.class, gpioled.devid);class_destroy(gpioled.class);gpio_free(gpioled.led_gpio);}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
spinlockAPP.c不需要改,和自旋锁的一致。不同的是,一个程序在执行时再开始一个应用,并不会像前两次一样出现报错,而是自动休眠,待第一个应用执行结束后自动被唤醒并执行:
6. 互斥体实验
懒得重新创新的文件夹,直接在上面的代码改了。
spinlock.c主要有以下更新:
增加了 头文件引用 #include <linux/mutex.h>
增加了 设备结构体中 互斥体
增加了 驱动入口函数中 对互斥体的初始化
增加了 led_open中 互斥体上锁
增加了 led_release中 互斥体解锁。互斥体同样可以使自动休眠。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/mutex.h>#define GPIOLED_NAME "gpioled"
#define GPIOLED_CNT 1#define LEDON 1
#define LEDOFF 0/* 设备结构体 */
struct gpioled_dev{dev_t devid;int major;int minor;struct cdev cdev;struct device *device;struct class *class;struct device_node *nd;int led_gpio;struct mutex lock;
};struct gpioled_dev gpioled;/* 操作集 */
static int led_release(struct inode *inode, struct file *filp){struct gpioled_dev *dev = filp->private_data;mutex_unlock(&dev->lock); // 互斥体 解锁return 0;
}
static int led_open(struct inode *inode, struct file *filp){filp->private_data = &gpioled;mutex_lock(&gpioled.lock); // 互斥体 上锁return 0;
}static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){int ret;unsigned char databuf[1];struct gpioled_dev *dev = filp->private_data;ret = copy_from_user(databuf, buf, count);if(ret < 0){return -EINVAL;}if(databuf[0] == LEDON){ //开灯gpio_set_value(dev->led_gpio, 0); // 低电平开灯} else if(databuf[0] == LEDOFF){gpio_set_value(dev->led_gpio, 1); // 高电平关灯}return 0;
}
static const struct file_operations led_fops = {.owner = THIS_MODULE,.write = led_write,.open = led_open,.release = led_release,
};/* 驱动入口 */
static int __init led_init(void){int ret = 0;/* 初始化互斥体 */mutex_init(&gpioled.lock);/* 1.注册字符设备驱动 */gpioled.devid = 0;if(gpioled.devid){gpioled.devid = MKDEV(gpioled.devid, 0);register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);} else {alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);gpioled.major = MAJOR(gpioled.devid);gpioled.minor = MINOR(gpioled.devid);}/* 2.初始化cdev */gpioled.cdev.owner = THIS_MODULE; cdev_init(&gpioled.cdev, &led_fops);/* 3.添加cdev */cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT); // 错误处理先略过了/* 4.创建类 */gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);if(IS_ERR(gpioled.class)){return PTR_ERR(gpioled.class);}/* 5.创建设备 */gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);if(IS_ERR(gpioled.device)){return PTR_ERR(gpioled.device);}/* 1.获取设备节点 */gpioled.nd = of_find_node_by_path("/gpioled"); // 找到刚才在imx6ull-alientek-emmc.dts根节点下加入的gpioled节点if(gpioled.nd == NULL){ret = -EINVAL;goto fail_findnode;}/* 2.获取LED对应的GPIO */ // 也就是节点中led-gpios那一行内容gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpios", 0);if(gpioled.led_gpio < 0){printk("can't find led_gpio\r\n");ret = -EINVAL;goto fail_findnode;}printk("led_gpio num = %d\r\n",gpioled.led_gpio);/* 3.申请IO */ret = gpio_request(gpioled.led_gpio, "led-gpios");if(ret){printk("Failed to request the led gpio\r\n");ret = -EINVAL;goto fail_findnode; }/* 4.使用IO,设置为输出 */ret = gpio_direction_output(gpioled.led_gpio, 1);if(ret){goto fail_setoutput; // 如果代码走到这一步,一定已经成功进行了IO申请,因此这里错误处理时需要释放IO}/* 5.输出高电平,关灯 */gpio_set_value(gpioled.led_gpio, 1);return 0;fail_setoutput:gpio_free(gpioled.led_gpio);
fail_findnode:return ret;
}/* 驱动出口 */
static void __exit led_exit(void){gpio_set_value(gpioled.led_gpio, 1); // 高电平 关灯/* 注销字符设备驱动 */cdev_del(&gpioled.cdev);unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);device_destroy(gpioled.class, gpioled.devid);class_destroy(gpioled.class);gpio_free(gpioled.led_gpio);}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
实验同5信号量实验一致。