Linux驱动 之自旋锁、死锁学习记录:
内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:
一种是原地等待。
一种是挂起当前进程,调度其他进程执行(睡眠)。
linux 内核中最常见的锁就是Spinlock自旋锁,自旋锁是“原地等待”的方式解决资源冲突的,
即一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”。
(忙等待特性)。
自旋锁优点:
自旋锁不会使线程状态发生切换
一直处于用户态,即线程—直都是运行的;
不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。
(线程被阻塞后便进入内核(Linux )调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)。
自旋锁的使用
在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?
如果只有进程上下文的访问,那么可以考虑使用 semaphore或者mutex的锁机制,
但是现在中断上下文也掺和进来,那些可以导致睡眠的 lock就不能使用了,这时候,可以考虑使用spinlock。
在中断上下文,是不允许睡眠的,所以,这里需要的是一个不会导致睡眠的锁——spinlock。
换言之,中断上下文要用锁,首选 spinlock。
使用步骤:
我们要访问临界资源需要首先申请自旋锁
获取不到锁就自旋,如果能获得锁就进入临界区
当自旋锁释放后,自旋在这个锁的任务即可获得锁并进入临界区,退出临界区的任务必须释放自旋锁
内核中的spinlock_t的数据类型定义如下
typedef struct spinlock {
struct raw_spinlock rlock;
} spinlock_t;
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
} raw_spinlock_t;
自旋锁定义、初始化
spinlock_t lock;
spin_lock_init (&lock);
DEFINE_SPINLOCK(lock);
自旋锁的申请
spin_lock(lock); //成功获得自旋锁立即返回,否则自旋在那里直到该自旋锁的保持者释放
spin_trylock(lock); //成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转"
、、、、、 //临界区操作
自旋锁的释放
spin_unlock(lock);//释放自旋锁
自旋锁死锁的2种情况
1)拥有自旋锁的进程A在内核态阻塞了,内核调度B进程,碰巧B进程也要获得自旋锁,此时B只能自旋转。而此时抢占已经关闭,不会调度A讲程了,B永远自旋产生死锁。
2)进程A拥有自旋锁,中断到来,CPU执行中断函数,中断处理函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,也只能自旋,产生死锁。
B站:一口Linux:
针对单CPU,拥有自旋锁的任务不应该调度会引起休眠的函数,否则会导致死锁。
查看CPU核数:ps -et | grep softirq
虚拟机重新设置一下处理器数量,就好:
cxx@ubuntu16:~$ ps -ef | grep softirq
root 7 2 0 10:32 ? 00:00:00 [ksoftirqd/0]
cxx 2450 2404 0 10:34 pts/18 00:00:00 grep --color=auto softirq
cxx@ubuntu16:~$
cdev_hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/spinlock.h>
#define NEWCHRDEV_CNT 1 /* 设备号个数 */
#define NEWCHRDEV_NAME "hello" /* 名字 */
static spinlock_t lock; //定义
static int flage = 1;
/* newchrdev设备结构体 */
struct newchr_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchr_dev chr_hello; /* 设备hello */
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
#define DEAD 1
static int hello_open (struct inode *inode, struct file *filep)
{
printk("hello_open()\n");
spin_lock(&lock); //锁
printk("hello_open() spin_lock \n");
if(flage != 1){
spin_unlock(&lock);
return -EBUSY;
}
flage =0;
#if DEAD
#elif
spin_unlock(&lock);
printk("hello_open() spin_unlock \n");
#endif
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int hello_release (struct inode *inode, struct file *filep)
{
printk("hello_release()\n");
flage = 1;
#if DEAD
spin_unlock(&lock);
printk("hello_release() spin_unlock \n");
#endif
return 0;
}
/* 设备操作函数 */
static struct file_operations chr_hello_ops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int hello_init(void)
{
int result = 0;
printk("chrdev_hello init!\r \n");
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (chr_hello.major) { /* 定义了设备号 */
chr_hello.devid = MKDEV(chr_hello.major, 0);
/* 据定义设备号申请注册 */
result = register_chrdev_region(chr_hello.devid, NEWCHRDEV_CNT, NEWCHRDEV_NAME);
if(result < 0){
printk("register_chrdev fail \n");
goto out_err_1;
}
} else { /* 没有定义设备号,自动分配*/
result = alloc_chrdev_region(&chr_hello.devid, 0, NEWCHRDEV_CNT, NEWCHRDEV_NAME); /* 申请设备号 */
if(result < 0){
printk("alloc_chrdev_region fail \n"); //自动分配设备号错误
goto out_err_1;
}
chr_hello.major = MAJOR(chr_hello.devid); /* MAJOR宏获取分配号的主设备号 */
chr_hello.minor = MINOR(chr_hello.devid); /* MINOR宏获取分配号的次设备号 */
}
printk("chr_hello major=%d,minor=%d\r\n",chr_hello.major, chr_hello.minor);
/* 2、初始化cdev */
chr_hello.cdev.owner = THIS_MODULE;
cdev_init(&chr_hello.cdev, &chr_hello_ops);
/* 3、添加一个cdev */
cdev_add(&chr_hello.cdev, chr_hello.devid, NEWCHRDEV_CNT);
/* 4、创建类 */
chr_hello.class = class_create(THIS_MODULE, NEWCHRDEV_NAME);
if (IS_ERR(chr_hello.class)) {
printk(KERN_ERR "class_create() failed\n");
result = PTR_ERR(chr_hello.class);
goto out_err_2;
}
/* 5、创建设备 */
chr_hello.device = device_create(chr_hello.class, NULL, chr_hello.devid, NULL, NEWCHRDEV_NAME);
if (IS_ERR(chr_hello.device)) {
printk(KERN_ERR "device_create() failed\n");
result = PTR_ERR(chr_hello.device);
goto out_err_3;
}
spin_lock_init(&lock); //自旋锁初始化
return result;
//释放已申请的资源返回
out_err_3:
device_destroy(chr_hello.class, chr_hello.devid); /* 删除device */
out_err_2:
class_destroy(chr_hello.class); /* 删除class */
unregister_chrdev_region(chr_hello.devid, NEWCHRDEV_CNT); /* 注销设备号 */
cdev_del(&chr_hello.cdev);/* 删除cdev */
out_err_1:
return result;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void hello_exit(void)
{
printk("chrdev_hello exit!\r \n");
/* 注销字符设备驱动 */
device_destroy(chr_hello.class, chr_hello.devid); /* 删除device */
class_destroy(chr_hello.class); /* 删除class */
unregister_chrdev_region(chr_hello.devid, NEWCHRDEV_CNT); /* 注销设备号 */
cdev_del(&chr_hello.cdev);/* 删除cdev */
return;
}
//modinfo name.ko
MODULE_LICENSE("GPL"); //遵循GPL协议
MODULE_AUTHOR("CJX");
MODULE_DESCRIPTION("Just for Demon");
module_init(hello_init);
module_exit(hello_exit);
//cat proc/devices
app.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
char *filename = "/dev/hello";
int fd = open(filename,O_RDWR);
if(fd<0){
perror("open fail \n");
return -1;
}
printf("open file %s success!\r\n", filename);
sleep(20);
close(fd);
return 0;
}
直接宕机:尝试把spin_lock(lock);换成 spin_trylock(lock); 即可
如何避免死锁
1.如果中断处理函数中也要获得自旋锁,那么驱动程序需要在拥有自旋锁时禁止中断
2.自旋锁必须在可能的最短时间内拥有
3.避免某个获得锁的函数调用其他同样试图获取这个锁的函数,否则代码就会死锁;
不论是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁,如果试图这么做,系统将挂起
4.锁的顺序规则:
a)按同样的顺序获得锁;
b)如果必须获得一个局部锁和一个属于内核更中心位置的锁,则应该首先获取自己的局部锁;
c)如果我们拥有信号量和自旋锁的组合,则必须首先获得信号量;在拥有自旋锁时调用down(可导致休眠)是个严重的错误的。
自旋锁与信号量应用场合:
信号量是进程级的,用于多个进程之间对资源的互斥。如果竞争失败,会发生进程上下文切换,因为进程上下文切换的开销比较大,因此,只有当进程占用资源时间较长时,选用信号量才是较好的选择。
所要保护的临界资源访问时间比较短时,用自旋锁是非常方便的,它不会引起进程睡眠而导致上下文切换。
如果访问临界资源的时间较长,则选用信号量,否则选用自旋锁。
信号量所保护的临界资源区可包含可能引起阻塞的代码,而自旋锁则绝对要避免这样的代码,阻塞意味着需要进程上下文切换,如果进程被切换出去,这个时候如果另外一个进程想获得自旋锁的话,会引起死锁。
信号量存在于进程上下文,因此,如果被保护的资源需要在中断或者软终端情况下使用,则只能选择自旋锁。
参考学习视频:B站 一口Linux: https://space.bilibili.com/661326452/