1 自旋锁
1.1 自旋锁简介
原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任,需要本节要讲的锁机制,在 Linux
内核中就是自旋锁。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。比如现在有个公用电话亭,一次肯定只能进去一个人打电话,现在电话亭里面有人正在打电话,相当于获得了自旋锁。此时你到了电话亭门口,因为里面有人,所以你不能进去打电话,相当于没有获取自旋锁,这个时候你肯定是站在原地等待,你可能因为无聊的等待而转圈圈消遣时光,反正就是哪里也不能去,要一直等到里面的人打完电话出来。终于,里面的人打完电话出来了,相当于释放了自旋锁,这个时候你就可以使用电话亭打电话了,相当于获取到了自旋锁。
自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。把自旋锁比作一个变量 a,变量 a=1 的时候表示共享资源可用,当 a=0的时候表示共享资源不可用。现在线程 A 要访问共享资源,发现 a=0(自旋锁被其他线程持有),那么线程 A 就会不断的查询 a 的值,直到 a=1。从这里我们可以看到自旋锁的一个缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。所以自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法了,这个我们后面会讲解。
Linux 内核使用结构体 spinlock_t 表示自旋锁,结构体定义如下所示:
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;
在使用自旋锁之前,肯定要先定义一个自旋锁变量,定义方法如下所示:
spinlock_t lock; //定义自旋锁
定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁。
1.2 自旋锁API
自旋锁基本 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
线程与中断并发访问处理 API 函数
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,示例代码如下所示:
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) /* 释放锁 */
}
2 自旋锁实验
上一个实验我们使用原子变量实现了一次只能有一个应用程序访问 LED 灯,本节我们使用自旋锁来实现此功能。在使用自旋锁之前,先回顾一下自旋锁的使用注意事项:
①、自旋锁保护的临界区要尽可能的短,因此在 open 函数中申请自旋锁,然后在 release 函数中释放自旋锁的方法就不可取。我们可以使用一个变量来表示设备的使用情况,如果设备被使用了那么变量就加一,设备被释放以后变量就减 1,我们只需要使用自旋锁保护这个变量即可。
②、考虑驱动的兼容性,合理的选择 API 函数。
综上所述,在本节例程中,我们通过定义一个变量 dev_stats 表示设备的使用情况,dev_stats为 0 的时候表示设备没有被使用,dev_stats 大于 0 的时候表示设备被使用。驱动 open 函数中先判断 dev_stats 是否为 0,也就是判断设备是否可用,如果为 0 的话就使用设备,并且将 dev_stats加 1,表示设备被使用了。使用完以后在 release 函数中将 dev_stats 减 1,表示设备没有被使用了。因此真正实现设备互斥访问的是变量 dev_stats,但是我们要使用自旋锁对 dev_stats 来做保护。
2.1 实验程序编写
1、本次实验在原子操作实验基础上完成,新建工程,spinlock.c驱动文件内容如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/atomic.h>
#define GPIOLED_CNT 1
#define GPIOLED_NAME "gpioled"
#define LEDOFF 0
#define LEDON 1
/*gpioled 设备结构体*/
struct gpioled_dev
{
dev_t devid;
int major;
int minor;
struct cdev cdev;
struct class *class;
struct device *device;
struct device_node *nd;
int led_gpio;
int dev_status; /*0表示设备可以使用,1表示不可使用*/
spinlock_t lock; /* 自旋锁 */
};
struct gpioled_dev gpioled; /*LED*/
static int led_open(struct inode *inode, struct file *filp)
{
unsigned long irqflag;
filp->private_data = &gpioled;
//spin_lock(&gpioled.lock);
spin_lock_irqsave(&gpioled.lock, irqflag); /* 上锁 */
if (gpioled.dev_status)
{ /* 如果设备被使用了 */
//spin_unlock(&gpioled.lock);
spin_unlock_irqrestore(&gpioled.lock, irqflag); /* 解锁 */
return -EBUSY;
}
gpioled.dev_status++; /* 如果设备没有打开,标记被使用*/
//spin_unlock(&gpioled.lock);
spin_unlock_irqrestore(&gpioled.lock, irqflag); /* 解锁 */
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
unsigned long irqflag;
struct gpioled_dev *dev = filp->private_data;
/* 关闭驱动文件的时候将 dev_stats 减 1 */
//spin_lock(&dev->lock);
spin_lock_irqsave(&dev->lock, irqflag); /* 上锁 */
if (dev->dev_status)
{
dev->dev_status--; /*标记驱动可以使用*/
}
//spin_unlock(&dev->lock);
spin_unlock_irqrestore(&dev->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;
/*1, 初始化自旋锁*/
spin_lock_init(&gpioled.lock);
gpioled.dev_status = 0;
/*1,注册字符设备驱动*/
gpioled.major = 0;
if (gpioled.major)
{ /*给定主设备号*/
gpioled.devid = MKDEV(gpioled.major, 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);
}
printk("gpioled major =%d, minor =%d \r\n", gpioled.major, gpioled.minor);
/*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");
if (gpioled.nd == NULL)
{
ret = -EINVAL;
goto fail_findnode;
}
/*2,获取LED所对应的GPIO*/
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-gpio");
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;
}
/*5,输出低电平,点亮led灯*/
gpio_set_value(gpioled.led_gpio, 0);
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);
/*释放IO*/
gpio_free(gpioled.led_gpio);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("supersmart");
第一步:驱动入口函数 led_init 中调用 spin_lock_init 函数初始化自旋锁。
第二步:在open函数中使用自旋锁实现对设备的互斥访问。
第三步:在 release 函数中将 dev_stats 减 1,表示设备被释放了,可以被其他的应用程序使用。将 dev_stats 减 1 的时候需要自旋锁对其进行保护。
2、测试APP内容如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*
*argc:应用程序参数个数
* argv[]:具体的参数内容,字符串形式
* ./spinlockAPP <filename> <0:1> 0 关灯,1 开灯
* ./spinlockAPP /dev/gpioled 0 关灯
* ./spinlockAPP /dev/gpioled 1 开灯
*/
#define LEDOFF 0
#define LEDON 1
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
unsigned char cnt;
if (argc != 3)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("file %s open failed!\r\n", filename);
return -1;
}
databuf[0] = atoi(argv[2]); /*将字符转化为数字*/
retvalue = write(fd, databuf, sizeof(databuf));
if (retvalue < 0)
{
printf("LED Control Failed ! \r\n");
close(fd);
return -1;
}
/*模拟应用占用驱动25s*/
while(1)
{
sleep(5);
cnt++;
printf("App runing times:%d\r\n",cnt);
if(cnt>=5) break;
}
printf("App runing finish!\r\n");
close(fd);
return 0;
}
3 运行测试
1、编译驱动程序(略)
2、编译测试 APP (略)
3、运行测试
加载驱动
先执行./spinlockApp /dev/gpioled 1& 命令,让 spinlockAPP 软件模拟占用 25S 的 LED 灯。
再执行./spinlockApp /dev/gpioled 0 命令,驱动正常工作的话并不会马上关闭 LED 灯,会提示你“file /dev/gpioled open failed!”,必须等待第一个 spinlockApp 软件运行完成(25S 计时结束)才可以再次操作 LED 灯。
卸载驱动