Skip to content

LV030-自旋锁

一、自旋锁简介

1. 什么是自旋锁

1.1 自旋锁的基本概念

原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区呢?

举个例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任,这个时候怎么办?这就到了这一部分要学习的自旋锁了。

自旋锁是为了保护共享资源提出的一种锁机制。 自旋锁(spin lock) 是一种非阻塞锁, 也就是说, 如果某线程需要获取锁, 但该锁已经被其他线程占用时, 该线程不会被挂起, 而是在不断的消耗 CPU 资源, 不停的试图获取锁。

也就是,当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙 循环-旋转-等待 状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。

比如现在有个公用电话亭,一次肯定只能进去一个人打电话,现在电话亭里面有人正在打电话,相当于获得了自旋锁。此时我们到了电话亭门口,因为里面有人,所以我们不能进去打电话,相当于没有获取自旋锁,这个时候我们肯定是站在原地等待,我们可能因为无聊的等待而转圈圈消遣时光,反正就是哪里也不能去,要一直等到里面的人打完电话出来。终于,里面的人打完电话出来了,相当于释放了自旋锁,这个时候我们就可以使用电话亭打电话了,相当于获取到了自旋锁。

1.2 自旋锁的实现

linux 上的自旋锁有三种实现:

  • (1)在单 cpu,不可抢占内核中,自旋锁为空操作。
  • (2)在单 cpu,可抢占内核中,自旋锁实现为“禁止内核抢占”,并不实现“自旋”。
  • (3)在多 cpu,可抢占内核中,自旋锁实现为“禁止内核抢占” + “自旋”。

2. 短时间加锁场景?

在有些场景中, 同步资源(用来保持一致性的两个或多个资源)的锁定时间很短, 为了这一小段时间去切换线程, 线程挂起和恢复现场的花费可能会让系统得不偿失。 如果计算机有多个 CPU 核心, 能够让两个或以上的线程同时并行执行, 这样我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间, 直到持有锁的线程释放锁, 后面请求锁的线程才可以获取锁。

为了让后面那个请求锁的线程“稍等一下”, 我们需让它进行自旋, 如果在自旋完成后前面锁定同步资源的线程已经释放了锁, 那么该线程便不必阻塞, 并且直接获取同步资源, 从而避免切换线程的开销。 这就是自旋锁。

从这里我们可以看到自旋锁的一个缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就需要换其他的方法了,我们后面再说。

二、相关数据结构与 API

1. struct spinlock

Linux 内核使用结构体 struct spinlock 表示自旋锁:

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;

在使用自旋锁之前,肯定要先定义一个自旋锁变量,定义方法如下所示:

c
spinlock_t lock; // 定义自旋锁

2. 自旋锁相关 API

自旋锁相关函数定义在 spinlock.h - include/linux/spinlock.h

函数描述
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 函数适用于 SMP 或支持抢占的单 CPU 下线程之间的并发访问,也就是用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的 API 函数,否则的话会可能会导致 死锁 现象的发生。

三、衍生的其他类型锁

在自旋锁的基础上还衍生出了其他特定场合使用的锁,这些锁在驱动中其实用的不多,更多的是在 Linux 内核中使用。

1. 读写自旋锁

1.1 基本概念

现在有个学生信息表,此表存放着学生的年龄、家庭住址、班级等信息,此表可以随时被修改和读取。此表肯定是数据,那么必须要对其进行保护,如果我们现在使用自旋锁对其进行保护。每次只能一个读操作或者写操作,但是,实际上此表是可以并发读取的。只需要保证在修改此表的时候没人读取,或者在其他人读取此表的时候没有人修改此表就行了。也就是此表的读和写不能同时进行,但是可以多人并发的读取此表。像这样,当某个数据结构符合读/写或生产者/消费者模型的时候就可以使用读写自旋锁。

读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作。

1.2 数据结构

Linux 内核使用 rwlock_t 结构体表示读写锁,结构体定义如下:

c
typedef struct {
	arch_rwlock_t raw_lock;
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned int magic, owner_cpu;
	void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map dep_map;
#endif
} rwlock_t;

1.3 相关 API

读写锁操作 API 函数分为两部分,一个是给读使用的,一个是给写使用的,这些 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)打开下半部,并释放读锁。

2. 顺序锁

2.1 基本概念

顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。

4.2.2 数据结构

Linux 内核使用 seqlock_t 结构体表示顺序锁 :

c
typedef struct {
	struct seqcount seqcount;
	spinlock_t lock;
} seqlock_t;

2.3 相关 API

  • 初始化
函数描述
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)读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读

四、死锁问题?

1. 什么是死锁

死锁是指两个或多个事物在同一资源上相互占用, 并请求锁定对方的资源, 从而导致恶性循环的现象。 当多个进程因竞争资源而造成的一种僵局(互相等待) , 若无外力作用, 这些进程都将无法向前推进, 这种情况就是死锁。

2. 进程切换

  • 在非抢占式内核中,如果一个进程在内核态运行,其只有在以下两种情况会被切换:

(1)其运行完成(返回用户空间)

(2)主动让出 cpu(即主动调用 schedule 或内核中的任务阻塞——这同样也会导致调用 schedule)

  • 在抢占式内核中,如果一个进程在内核态运行,其只有在以下四种情况会被切换:

(1)其运行完成(返回用户空间)

(2)主动让出 cpu(即主动调用 schedule 或内核中的任务阻塞——这同样也会导致调用 schedule)

(3)当从中断处理程序正在执行,且返回内核空间之前(此时可抢占标志 premptcount 须为 0) 。

(4)当内核代码再一次具有可抢占性的时候,如解锁及使能软中断等。

禁止内核抢占只是关闭“可抢占标志”,而不是禁止进程切换。显式使用 schedule 或进程阻塞(此也会导致调用 schedule)时,还是会发生进程调度的。

3. 自旋锁的死锁

3.1 情况一

对于多核抢占与多核非抢占的情况,在使用自旋锁时,其情况基本是一致的。因为在多核抢占的情况下,使用自旋锁会禁止内核抢占,这样多核抢占就相当于多核非抢占的情况。

自旋锁会自动禁止抢占,也就说当线程 A 得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么进程 A 会自动放弃 CPU 使用权。进程 B 开始运行,进程 B 也想要获取锁,但是此时锁被 A 进程持有,而且内核抢占还被禁止了!进程 B 无法被调度出去,那么进程 A 就无法运行,锁也就无法释放,好了,死锁发生了! 如下图:

image-20250122103229395

在单 cpu 内核上不会出现上述情况,因为单 cpu 上的自旋锁实际没有“自旋功能”。

相应的解决办法是, 在自旋锁的使用过程中要尽可能短的时间内拥有自旋锁, 而且不能在临界区中调用导致线程休眠的函数。

3.2 情况二

第二种情况是进程 A 拥有自旋锁, 中断到来, CPU 执行中断函数, 进程 A 切换到中断处理函数, 在中断处理函数中又需要获得自旋锁, 访问共享资源, 此时中断处理函数无法获得锁, 只能自旋, 如下图所示:

image-20250122103247395

对于中断引发的死锁, 最好的解决方法就是在获取锁之前关闭本地中断, Linux 内核在 spinlock.h - include/linux/spinlock.h 文件中提供了相应的 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)将中断状态恢复到以前的状态, 打开中断并释放自旋锁
void spin_lock_bh(spinlock_t *lock)关闭下半部, 获取自旋锁
void spin_unlock_bh(spinlock_t *lock)打开下半部, 获取自旋锁

由于 Linux 内核运行是非常复杂的, 很难确定某个时刻的中断状态, 因此建议使用 spin_lock_irqsave 、spin_unlock_irqrestore, 因为这一组函数会保存中断状态, 在释放锁的时候会恢复中断状态。

五、注意事项

①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要学习的信号量和互斥体。

②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。

③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个我们正在持有的锁,那么我们就必须“自旋”,等待锁被释放,然而正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!

④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。

六、自旋锁 demo

加了锁之后,内部休眠太久的话,会卡死并产生崩溃,崩溃应该是死锁导致的,但是要是把锁加在内部循环的地方,会有不一样的效果,大概可以演示自旋锁的效果。这里有两个正常 demo,两个异常的 demo,异常的两个可以当做死锁的实例。

1. 正常使用的 demo1

1.1 demo 源码

1.1.1 sdriver_demo.c
c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#include <linux/delay.h> /* msleep 等延时函数 */

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

#define CHRDEV_NAME "sdev"    /* 设备名, cat /proc/devices 查看与设备号的对应关系 */
#define CLASS_NAME  "sclass"  /* 类名,在 /sys/class 中显示的名称 */
#define DEVICE_NAME "sdevchr" /* 设备节点名,在 /sys/class/class_name/ 中显示的名称以及 /dev/ 下显示的节点名 */
#define BUFSIZE     32        /* 设置最大偏移量为 32 */

#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)
#define CMD_TEST3 _IOW('S', 3, int)

struct __CMD_TEST{
	int a;
	int b;
	int c;
};

typedef struct __CHAR_DEVICE
{
    dev_t dev_num;         // 定义 dev_t 类型(32 位大小)的变量 dev_num, 用来存放设备号
    struct cdev s_cdev;    // 定义 cdev 结构体类型的变量 scdev
    struct class *class;   // 定于 struct class *类型结构体变量 class,表示要创建的类
    struct device *device; // 设备
    char buf[BUFSIZE];     // 设置数据存储数组 mem
    ulong run_cnt;         // 竞争测试用的一个共享数据
    spinlock_t s_lock;     // 自旋锁
} _CHAR_DEVICE;

_CHAR_DEVICE g_chrdev = {0};  //定义一个 device_test 结构体变量

static int scdev_ops_open(struct inode *pInode, struct file *pFile)
{
    PRT("This is scdev_ops_open!\n");
    pFile->private_data = &g_chrdev;  //设置私有数据
    return 0;
}

static ssize_t scdev_ops_read(struct file *pFile, char __user *buf, size_t size, loff_t *off)
{
    
    loff_t offset = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;
    _CHAR_DEVICE *p_chrdev=(_CHAR_DEVICE *)pFile->private_data; //在 write 函数中读取 private_data

    if (offset > BUFSIZE)
    {
        return 0;
    }

    if (count > BUFSIZE - offset)
    {
        count = BUFSIZE - offset;
    }

    if (copy_to_user(buf, p_chrdev->buf + offset, count))
    { 
        // 将 mem 中的值写入 buf,并传递到用户空间
        PRT("copy_to_user error!\n");
        return -1;
    }
    #if 0
    int i = 0;
    for (i = 0; i < BUFSIZE; i++)
    {
        PRT("buf[%d] %c\n", i, p_chrdev->buf[i]); // 将 mem 中的值打印出来
    }
    
    PRT("read offset is %llu, count is %d\n", offset, count);
    #endif
    *off = *off + count; // 更新偏移值

    return count;
}

static ssize_t scdev_ops_write(struct file *pFile, const char __user *buf, size_t size, loff_t *off)
{
    loff_t offset = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;
    int i = 0;

    _CHAR_DEVICE *p_chrdev=(_CHAR_DEVICE *)pFile->private_data; //在 write 函数中读取 private_data

    if (offset > BUFSIZE)
    {
        return 0;
    }

    if (count > BUFSIZE - offset)
    {
        count = BUFSIZE - offset;
    }

    if (copy_from_user(p_chrdev->buf + offset, buf, count))
    { 
        // 将 buf 中的值,从用户空间传递到内核空间
        PRT("copy_to_user error \n");
        return -1;
    }
    // 将拷贝的数据转化为 10 进制数
    kstrtoul_from_user(buf, count, 10, &p_chrdev->run_cnt);
    spin_lock(&p_chrdev->s_lock);
    for (i = p_chrdev->run_cnt; i > 0; i--)
    {
        PRT("addr=0x%px run_cnt=%ld current is %d\n", &p_chrdev->run_cnt, p_chrdev->run_cnt, i); // 循环打印数据
        mdelay(200);
    }
    spin_unlock(&p_chrdev->s_lock);
#if 0
    int i = 0;
    for (i = 0; i < BUFSIZE; i++)
    {
        PRT("buf[%d] %c\n", i, p_chrdev->buf[i]); // 将 mem 中的值打印出来
    }
    PRT("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
    #endif
    *off = *off + count;                         // 更新偏移值

    return count;
}

static int scdev_ops_release(struct inode *pInode, struct file *pFile)
{
    PRT("This is scdev_ops_release!\n");
    return 0;
}

static loff_t scdev_ops_llseek(struct file *pFile, loff_t offset, int whence)
{
    loff_t new_offset = 0; // 定义 loff_t 类型的新的偏移值
    switch (whence)    // 对 lseek 函数传递的 whence 参数进行判断
    {
        case SEEK_SET:
            if (offset < 0 || offset > BUFSIZE)
            {
                return -EINVAL; // EINVAL = 22 表示无效参数
            }
            new_offset = offset; // 如果 whence 参数为 SEEK_SET,则新偏移值为 offset
            break;
        case SEEK_CUR:
            if ((pFile->f_pos + offset < 0) || (pFile->f_pos + offset > BUFSIZE))
            {
                return -EINVAL;
            }
            new_offset = pFile->f_pos + offset; // 如果 whence 参数为 SEEK_CUR,则新偏移值为 pFile-> f_pos + offset,pFile-> f_pos 为当前的偏移值
            break;
        case SEEK_END:
            if (pFile->f_pos + offset < 0)
            {
                return -EINVAL;
            }
            new_offset = BUFSIZE + offset; // 如果 whence 参数为 SEEK_END,则新偏移值为 BUFSIZE + offset,BUFSIZE 为最大偏移量
            break;
        default:
            break;
    }
    pFile->f_pos = new_offset; // 更新 pFile-> f_pos 偏移值

    return new_offset;
}

static long scdev_ops_ioctl(struct file *pFile, unsigned int cmd, unsigned long arg)
{
    int val = 0;//定义 int 类型向应用空间传递的变量 val
    switch(cmd)
    {
        case CMD_TEST0:
            PRT("this is CMD_TEST0\n");
            break;
        case CMD_TEST1:
            PRT("this is CMD_TEST1\n");
            PRT("arg is %ld\n",arg);//打印应用空间传递来的 arg 参数
            break;
        case CMD_TEST2:
            val = 1;//将要传递的变量 val 赋值为 1
            PRT("this is CMD_TEST2\n");
            if(copy_to_user((int *)arg, &val, sizeof(val)) != 0)
            {
                //通过 copy_to_user 向用户空间传递数据
                PRT("copy_to_user error \n");
            }
            break;
        case CMD_TEST3:
        {
            struct __CMD_TEST cmd_test3 = {0};
            if (copy_from_user(&cmd_test3, (int *)arg, sizeof(cmd_test3)) != 0)
            {
                PRT("copy_from_user error\n");
            }
            PRT("cmd_test3.a = %d\n", cmd_test3.a);
            PRT("cmd_test3.b = %d\n", cmd_test3.b);
            PRT("cmd_test3.c = %d\n", cmd_test3.c);
            break;
        }
        default:
            break;
    }

    return 0;
}

static struct file_operations g_scdev_ops = {
    .owner = THIS_MODULE,//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = scdev_ops_open,
	.read = scdev_ops_read,
	.write = scdev_ops_write,
	.release = scdev_ops_release,
    .llseek = scdev_ops_llseek,
    .unlocked_ioctl = scdev_ops_ioctl,
}; // 定义 file_operations 结构体类型的变量 g_cdev_dev_ops

static int scdev_create(_CHAR_DEVICE *p_chrdev)
{
    int ret;          // 定义 int 类型的变量 ret,用来判断函数返回值
    int major, minor; // 定义 int 类型的主设备号 major 和次设备号 minor

    /* 初始化自旋锁 */
	spin_lock_init(&p_chrdev->s_lock);

    ret = alloc_chrdev_region(&p_chrdev->dev_num, 0, 1, CHRDEV_NAME); // 自动获取设备号,设备名为 chrdev_name
    if (ret < 0)
    {
        PRTE("alloc_chrdev_region is error!ret=%d\n", ret);
        goto err_alloc_devno;
    }
    major = MAJOR(p_chrdev->dev_num); // 使用 MAJOR()函数获取主设备号
    minor = MINOR(p_chrdev->dev_num); // 使用 MINOR()函数获取次设备号
    PRT("major is %d, minor is %d !\n", major, minor);

    cdev_init(&p_chrdev->s_cdev, &g_scdev_ops); // 使用 cdev_init()函数初始化 p_chrdev-> s_cdev 结构体,并链接到 cdev_ops 结构体
    p_chrdev->s_cdev.owner = THIS_MODULE;          // 将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    ret = cdev_add(&p_chrdev->s_cdev, p_chrdev->dev_num, 1); // 使用 cdev_add()函数进行字符设备的添加
    if (ret < 0)
    {
        PRTE("cdev_add is error !ret=%d\n", ret);
        goto err_cdev_add;
    }

    p_chrdev->class = class_create(THIS_MODULE, CLASS_NAME); // 使用 class_create 进行类的创建,类名称为 class_dev
    if(IS_ERR(p_chrdev->class))
    {
        ret = PTR_ERR(p_chrdev->class);
        goto err_class_create;
    }
    p_chrdev->device = device_create(p_chrdev->class, NULL, p_chrdev->dev_num, NULL, "%s", DEVICE_NAME); // 使用 device_create 进行设备的创建,设备名称为 device_dev
    if(IS_ERR(p_chrdev->device))
    {
        ret = PTR_ERR(p_chrdev->class);
        goto err_device_create;
    }
    PRT("scdev_create /dev/%s success!\n", DEVICE_NAME);
    return 0;
    // 一些列的错误处理
err_device_create:
    class_destroy(p_chrdev->class);   // 删除创建的类
err_class_create:
    cdev_del(&p_chrdev->s_cdev);      // 使用 cdev_del()函数进行字符设备的删除
err_cdev_add:
    unregister_chrdev_region(p_chrdev->dev_num, 1); //注销设备号
err_alloc_devno:
    return ret;
}

static void scdev_destroy(_CHAR_DEVICE *p_chrdev)
{
    // 需要注意的是, 字符设备的注册要放在申请字符设备号之后,
    // 字符设备的删除要放在释放字符驱动设备号之前。
    cdev_del(&p_chrdev->s_cdev);                    // 使用 cdev_del()函数进行字符设备的删除
    unregister_chrdev_region(p_chrdev->dev_num, 1); // 释放字符驱动设备号
    device_destroy(p_chrdev->class, p_chrdev->dev_num); // 删除创建的设备
    class_destroy(p_chrdev->class);                 // 删除创建的类
    PRT("scdev_destroy success!\n");
}

/**
 * @brief  sdrv_demo_init
 * @note   注册驱动
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdrv_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);

	// 注册字符设备
    ret = scdev_create(&g_chrdev);
    if (ret < 0) 
    {
        PRT("Failed to scdev_create!ret=%d\n", ret);
        goto err_scdev_create;
    }
    PRT("sdrv_demo module init success!\n");
	return 0;

err_scdev_create:
    return ret;
}

/**
 * @brief  sdrv_demo_exit
 * @note   注销驱动
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdrv_demo_exit(void)
{
    // 注销字符设备
    scdev_destroy(&g_chrdev);
    PRT("sdrv_demo module exit!\n");
}

module_init(sdrv_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdrv_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo xxx.ko 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */
1.1.2 app_demo.c
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <sys/ioctl.h>

#define BUFSIZE   32 /* 设置最大偏移量为 64, 方便打印完整的内存空间数据*/

void usage_info(void)
{
	printf("\n");
	printf("+++++++++++++++++++++++++++++++++++++++++\n");
	printf("+       help information  @sumu          +\n");
	printf("+++++++++++++++++++++++++++++++++++++++++\n");

	printf("help:\n");
	printf("use format: ./app_name /dev/device_name arg1 ... \n");
	printf("            ./app_demo.out /dev/sdevice run_cnt # run_cnt为运行次数 \n");
	printf("\n");

	printf("command info:\n");
	printf("  (1)load module  : insmod module_name.ko\n");
	printf("  (2)unload module: rmmod module_name.ko\n");
	printf("  (3)show module  : lsmod\n");
	printf("  (4)view device  : cat /proc/devices\n");
	printf("  (5)create device node: mknod /dev/device_name c major_num secondary_num \n");
	printf("  (6)show device node  : ls /dev/device_name \n");
	printf("  (7)show device vlass  : ls /sys/class \n");
	printf("+++++++++++++++++++++++++++++++++++++++++\n");
}

int main(int argc, char *argv[])
{
    int ret = 0;
    int fd = -1;
    pid_t pid = 0; //用于保存 fork 函数返回的父、子线程的 PID
    char *filename = NULL;
    char writebuf[BUFSIZE] = {0};

    printf("*** Build Time: %s %s,Git Version: %s Git Remote: %s***\n", 
            __DATE__, __TIME__, GIT_VERSION, GIT_PATH);
    // ./xxx.out /dev/sdevice x x x
    if (argc <= 2) 
    {
        usage_info();
        return -1;
    }
    // 解析参数
    filename = argv[1];
    snprintf (writebuf, sizeof(writebuf), "%s", argv[2]);
    printf("%s %s %s\n", argv[0], filename, argv[2]);
    // 创建子进程
    pid = fork();
    if (pid < 0)
    {
        printf("fork error!!\n"); // fork 函数执行错误
        return -1;
    }
    if (0 == pid) // 子进程
    {
        printf("This is child process!\n");
        /* 打开驱动文件 */
        fd = open(filename, O_RDWR);
        if (fd < 0)
        {
            printf("can't open file %s !\n", filename);
            return -1;
        }
        /* 向设备驱动写数据 */
        ret = write(fd, writebuf, strlen(writebuf));
        if (ret < 0)
        {
            printf("write file %s failed!\n", filename);
        }

        /* 关闭设备 */
        ret = close(fd);
        if (ret < 0)
        {
            printf("can't close file %s !\n", filename);
            return -1;
        }
    }
    #if 1
    else // 父进程
    {
        printf("This is parent process!\n");
        /* 打开驱动文件 */
        fd = open(filename, O_RDWR);
        if (fd < 0)
        {
            printf("can't open file %s !\n", filename);
            return -1;
        }
        /* 向设备驱动写数据 */
        ret = write(fd, writebuf, strlen(writebuf));
        if (ret < 0)
        {
            printf("write file %s failed!\n", filename);
        }

        /* 关闭设备 */
        ret = close(fd);
        if (ret < 0)
        {
            printf("can't close file %s !\n", filename);
            return -1;
        }
    }
    #endif
    return 0;
}

1.2 开发板验证

我们将编译得到的 sdriver_demo.ko、app_demo.out 拷贝到开发板。

  • (1)加载驱动
shell
insmod sdriver_demo.ko
image-20250121165825048
  • (2)这里换了种写法,app_demo.out 会创建子进程运行
shell
./app_demo.out /dev/sdevchr 10
image-20250121165930968

可以看到不再是像之前竞争演示那样交替打印了,是等一个打完再打另一个。

2. 正常使用的 demo2

这个 demo 是定义了一个变量,操作它的时候,用自旋锁锁定,保证操作的时候不会被打断而出现问题,相当于前面的原子操作,但是比原子操作能保护的范围更大些。

2.1 demo 源码

07_concurrency/04_spin_lock_3

2.2 开发板验证

我们将编译得到的 sdriver_demo.ko、app_demo.out 拷贝到开发板。

  • (1)加载驱动
shell
insmod sdriver_demo.ko
  • (2)这里换了种写法,app_demo.out 会创建子进程运行
shell
./app_demo.out /dev/sdevchr 2 # 这个参数没啥意义,我在内部写死了

usleep(1000*200*6)时,父子进程依次运行:

image-20250122111048057

usleep(1000*200*2)时,子进程运行失败:

image-20250122111333006

修改 app_demo.c 中下图位置的休眠时间可以控制子进程的运行时间,当父进程运行完再运行子进程时,子进程运行正常,当父进程未运行完毕就开始运行子进程,子进程就会打开节点失败,达到共享资源保护的目的。

image-20250122110709727

3. 死锁的 demo

3.1 demo 源码

这个 demo 是第一种情况, 即拥有自旋锁的进程 A 在内核态休眠了, 内核调度 B 进程, 碰巧 B 进程也要获得自旋锁, 依次产生死锁。

3.2 开发板验证

这个 demo 是在内核进行长时间休眠,会导致崩溃,先不管了,知道死锁的原理就可以了。崩溃的信息可以看这里:07_concurrency/04_spin_lock_1/debug_log.md

参考资料:

自旋锁在抢占(或非抢占)单核和多核中的作用_多核系统中, 任务可以通过自旋锁或者中断的方式独占 cpu-CSDN 博客