自旋锁

自旋锁的核心思想是 “忙等待” (Busy-Waiting)。当一个执行单元(CPU 核心、进程上下文线程、中断上下文等)尝试获取一个已经被其他执行单元持有的自旋锁时,它不会进入睡眠状态(阻塞),而是会在一个紧凑的循环中不断地检查锁的状态(”旋转”),直到锁被释放。

  1. 忙等待:

    • 核心行为: 获取锁失败的执行单元在 CPU 上循环检查锁的状态 (while (lock_is_held);)。它持续占用着 CPU 核心,不做其他有用工作。
    • 目的: 避免了进程上下文切换的开销(保存/恢复寄存器、更新数据结构、调度等)。在锁被持有时间非常短的情况下,忙等待的总开销可能小于睡眠唤醒的开销。
    • 代价: 如果锁被持有时间较长,忙等待会浪费大量 CPU 周期,显著降低系统性能。因此,自旋锁只适用于临界区执行时间非常短的场景
  2. 互斥性:

    • 自旋锁保证在任意时刻,最多只有一个执行单元持有锁,从而确保对共享资源的互斥访问。
  3. 不可睡眠:

    • 最重要规则: 在持有自旋锁期间,执行单元绝对不能睡眠(阻塞)或主动放弃 CPU(如调用 schedule(), kmalloc(GFP_KERNEL), 等待信号量等)!
    • 原因:
      • 死锁风险: 如果持有锁的执行单元 A 睡眠了,另一个执行单元 B 尝试获取该锁时会在其 CPU 上忙等待。执行单元 A 需要被唤醒才能释放锁,但唤醒它的执行单元 C 可能也需要获取同一个锁,导致 C 也忙等待。最终,没有执行单元能释放锁,系统死锁。
      • 性能灾难: 忙等待的 CPU 无法做任何有用工作,而持有锁的进程在睡眠,导致锁长时间无法释放,其他所有等待该锁的 CPU 都在空转。
  4. SMP (Symmetric Multi-Processing) 优化:

    • 在单核 CPU 上,自旋锁的实现相对简单(通常只需要关闭抢占,防止当前任务被更高优先级任务抢占导致长时间持有锁)。
    • 多核 CPU 上,自旋锁的实现必须高效处理多个核心同时竞争锁的情况。现代内核的自旋锁通常基于底层硬件的原子操作内存屏障指令实现。常见的底层实现包括:
      • Test-And-Set (TAS): 原子地测试一个内存位置的值并设置新值。
      • Compare-And-Swap (CAS): 更强大的原子操作(前面原子操作部分讲过)。
      • Ticket Lock: 一种更公平的自旋锁,避免某些 CPU 核心一直抢不到锁(饥饿)。每个竞争者获取一个递增的“票号”,锁按票号顺序释放。spinlock_t 在 x86 上通常使用 ticket lock。
      • Queued Spinlock: 进一步优化,将等待者组织成队列,减少缓存行颠簸(多个 CPU 反复读写同一个表示锁状态的内存位置,导致缓存失效)。
  5. 内存屏障:

    • 自旋锁的获取和释放操作内部都隐含着内存屏障 (Memory Barrier)
    • 获取锁 (Acquire Barrier): 确保在进入临界区(获取锁之后)之前,所有在锁获取操作之前的读/写操作已经完成(不会被重排到临界区内),并且临界区内的读/写操作不会被重排到锁获取操作之前。
    • 释放锁 (Release Barrier): 确保在离开临界区(释放锁)之前,所有在临界区内的读/写操作已经完成(不会被重排到释放锁之后),并且释放锁操作之后的读/写操作不会被重排到临界区内。
    • 作用: 保证临界区内的内存访问操作对其他 CPU 核心是可见的,并且顺序符合程序逻辑,防止数据竞争和不一致。
  6. 关闭抢占 (仅限进程上下文):

    • 单核可抢占内核中,即使没有其他 CPU 核心,一个高优先级任务也可能抢占当前持有自旋锁的任务。
    • 为了防止这种情况(避免高优先级任务在同一个 CPU 上忙等待同一个锁导致死锁),在进程上下文中获取自旋锁时,内核会自动关闭当前 CPU 核心的抢占 (preempt_disable())
    • 释放锁时会重新开启抢占 (preempt_enable())。
    • 在中断上下文或 SMP 环境中,抢占关闭不是主要问题(中断上下文不可被抢占,SMP 有其他核心在忙等)。

自旋锁API函数

核心变量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

初始化

1
2
3
DEFINE_SPINLOCK(my_lock); // 静态初始化一个名为 my_lock 的自旋锁
spinlock_t my_lock;
int spin_lock_init(&my_lock); // 动态初始化(通常在模块初始化或数据结构初始化时调用)

获取锁:

1
void spin_lock(spinlock_t *lock);
  • 尝试获取锁 lock
  • 如果锁已被占用,则在当前 CPU 核心上忙等待,直到锁被释放。
  • 在单核可抢占内核中,获取锁时会关闭当前 CPU 的抢占
  • 包含必要的获取内存屏障

释放锁:

1
void spin_unlock(spinlock_t *lock);
  • 释放锁 lock
  • 在单核可抢占内核中,释放锁时会重新开启当前 CPU 的抢占
  • 包含必要的释放内存屏障

尝试获取锁 (非阻塞):

1
int spin_trylock(spinlock_t *lock);
  • 尝试获取锁 lock
  • 如果锁当前可用,则获取它并返回 非零值 (通常是 1)
  • 如果锁已被占用,则立即返回 0不会忙等待
  • 用途: 在不能等待或不确定锁状态时尝试获取锁,避免可能的忙等待。成功与否需要检查返回值。

中断安全的 API 函数

当中断处理程序(特别是软中断硬中断)和进程上下文可能访问同一个由自旋锁保护的共享资源时,必须使用中断安全的 API。否则,在进程上下文持有锁时发生中断,中断处理程序尝试获取同一个锁,会导致中断处理程序在同一个 CPU 上忙等待。由于持有锁的进程上下文被中断抢占,无法释放锁,导致死锁

image-20250728152522917

  1. 获取锁 + 保存本地中断状态并禁用中断:

    1
    spin_lock_irq(spinlock_t *lock);
    • 在获取锁 lock 之前保存当前 CPU 的中断使能状态到 EFLAGS 寄存器(隐含)并禁用本地 CPU 的中断(硬中断 + 软中断)
    • 然后获取锁(行为同 spin_lock)。
    • 为什么? 防止在获取锁之后、进入临界区之前发生中断。确保临界区执行期间,当前 CPU 不会被中断处理程序(可能访问同一共享资源)抢占。
    • 释放时: 需要配对使用 spin_unlock_irq
  2. 释放锁 + 恢复本地中断状态:

    1
    spin_unlock_irq(spinlock_t *lock);
    • 释放锁 lock
    • 然后恢复之前保存的本地 CPU 中断使能状态(可能重新开启中断)。
    • 必须与 spin_lock_irq 配对使用
  3. 获取锁 + 保存本地中断状态并禁用中断 (显式保存状态):

    1
    2
    unsigned long flags;
    spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
    • 在获取锁 lock 之前保存当前 CPU 的中断使能状态到变量 flags(由调用者提供)并禁用本地 CPU 的中断(硬中断 + 软中断)
    • 然后获取锁。
    • 优点:spin_lock_irq 更安全。适用于无法确定当前中断是否已经被禁用的情况(例如,在调用深层函数中加锁)。它总是保存当前状态并禁用中断,释放时再精确恢复。
    • 最佳实践: 在中断上下文不确定的情况下,优先使用 spin_lock_irqsave/spin_unlock_irqrestore
  4. 释放锁 + 恢复本地中断状态 (使用保存的状态):

    1
    spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
    • 释放锁 lock
    • 然后根据 flags 变量恢复之前保存的本地 CPU 中断使能状态
    • 必须与 spin_lock_irqsave 配对使用
  5. 仅禁用软中断 (Bottom-Half) 的锁:

    1
    2
    spin_lock_bh(spinlock_t *lock);
    spin_unlock_bh(spinlock_t *lock);
    • spin_lock_bh: 获取锁 lock 之前仅禁用本地 CPU 的软中断 (Bottom Halves, 如 Tasklet, Softirq),不关硬中断
    • spin_unlock_bh: 释放锁 lock 之后重新启用本地 CPU 的软中断
    • 用途: 当共享资源只会被进程上下文和软中断访问,而不会被硬中断访问时使用。性能略好于完全关中断的版本。常见于网络栈等场景。

实验:

我们实验对LED的设备文件进行管理,在有单元对其进行访问的时候,其它所有单元都不能对其进行访问,主要的原理如下:

  1. 定义自旋锁变量
    在设备结构体struct leddev_struct中定义了自旋锁变量:

    1
    spinlock_t lock;    /* 自旋锁 */
  2. 初始化自旋锁
    在驱动入口函数leddrv_probe中初始化自旋锁:

    1
    2
    /* 初始化自旋锁 */
    spin_lock_init(&leddev->lock);
  3. 使用自旋锁保护共享资源
    代码中使用int dev_status;变量作为设备使用状态的标志(0 表示未使用,>0表示已使用),这是一个共享资源,需要通过自旋锁进行保护。

    • 上锁操作spin_lock_irqsave(&leddev->lock,flags);
      这个函数会:
      • 保存当前中断状态到 flags
      • 禁用本地中断
      • 获取自旋锁
    • 解锁操作spin_unlock_irqrestore(&leddev.lock, flags)
      这个函数会:
      • 释放自旋锁
      • 恢复之前保存的中断状态

代码:

驱动代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>

#define LEDDRV_CNT 1
#define LEDDRV_NAME "gpioled"
#define LED_OPEN 1
#define LED_CLOSE 0

struct leddev_struct{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
struct gpio_desc *leddes;
int dev_status;
spinlock_t lock;
};
struct leddev_struct *leddev;

static int ledopen(struct inode *node, struct file *file)
{
unsigned long flags;
file->private_data = leddev;
spin_lock_irqsave(&leddev->lock,flags);
if(leddev->dev_status){
spin_unlock_irqrestore(&leddev->lock,flags);
return -EBUSY;
}
leddev->dev_status++;
spin_unlock_irqrestore(&leddev->lock,flags);
gpiod_direction_output(leddev->leddes,0);
//printk("led device open\n");
return 0;
}

static int ledrelease (struct inode *node, struct file *file)
{
//printk("led device closed\n");
unsigned long flags;
struct leddev_struct *leddev=(struct leddev_struct *)file->private_data;
spin_lock_irqsave(&leddev->lock,flags);
if(leddev->dev_status){
leddev->dev_status--;
}
spin_unlock_irqrestore(&leddev->lock,flags);
return 0;
}
static ssize_t ledwrite (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
struct leddev_struct *leddev=(struct leddev_struct *)file->private_data;
int ret=0;
char receive;
if(size!=1)
return -EINVAL;
ret=copy_from_user(&receive,buf,size);
if(ret<0){
return -EFAULT;
}

if(receive==LED_CLOSE){
//printk("led close\n");
gpiod_set_value(leddev->leddes,0);
}
else if(receive==LED_OPEN){
//printk("led open\n");
gpiod_set_value(leddev->leddes,1);
}
return size;
}

static struct file_operations led_fops={
.owner=THIS_MODULE,
.open=ledopen,
.write=ledwrite,
.release=ledrelease,
};

int leddrv_probe(struct platform_device *pdev){
int ret=0;
struct device *dev=&pdev->dev;
printk("matched success!\n");
leddev=devm_kzalloc(dev,sizeof(struct leddev_struct),GFP_KERNEL);
if(!leddev){
printk("kzalloc failed!\n");
return -ENOMEM;
}
if(leddev->major)
{
leddev->devid=MKDEV(leddev->major,0);
ret=register_chrdev_region(leddev->devid,LEDDRV_CNT,LEDDRV_NAME);
}
else{
ret=alloc_chrdev_region(&leddev->devid,0,LEDDRV_CNT,LEDDRV_NAME);
leddev->major=MAJOR(leddev->devid);
}
if(ret<0){
printk("devid register failed!\n");
goto chrdev_fail;
}
printk("major is %d\n",leddev->major);

cdev_init(&leddev->cdev,&led_fops);
ret=cdev_add(&leddev->cdev,leddev->devid,LEDDRV_CNT);
if(ret<0)
{
printk("cdev create error!\n");
goto cdev_fail;
}

leddev->class=class_create(THIS_MODULE,LEDDRV_NAME);
if(IS_ERR(leddev->class))
{
printk("class create error!\n");
ret=PTR_ERR(leddev->class);
goto class_fail;
}

leddev->device=device_create(leddev->class,NULL,leddev->devid,NULL,LEDDRV_NAME);
if(IS_ERR(leddev->device))
{
printk("device create error!\n");
ret=PTR_ERR(leddev->device);
goto device_fail;
}

leddev->leddes=devm_gpiod_get(dev,"led",GPIOD_OUT_LOW);
if(IS_ERR(leddev->leddes))
{
printk("leddes get error!\n");
ret=PTR_ERR(leddev->leddes);
goto leddes_fail;
}
platform_set_drvdata(pdev, leddev);

spin_lock_init(&leddev->lock);

return 0;
leddes_fail:
device_destroy(leddev->class,leddev->devid);
device_fail:
class_destroy(leddev->class);
class_fail:
cdev_del(&leddev->cdev);
cdev_fail:
unregister_chrdev_region(leddev->devid,LEDDRV_CNT);
chrdev_fail:

return ret;
}
int leddev_resume(struct platform_device *pdev){
struct leddev_struct *leddev = platform_get_drvdata(pdev);

device_destroy(leddev->class,leddev->devid);
class_destroy(leddev->class);
cdev_del(&leddev->cdev);
unregister_chrdev_region(leddev->devid,LEDDRV_CNT);

return 0;
}


const struct of_device_id led_device_id[]={
{.compatible="gpio-led"},
{},
};
struct platform_driver led_dev_driver={
.probe=leddrv_probe,
.driver={
.of_match_table=led_device_id,
.name="gpioled",
},
.remove=leddev_resume,
};

static int __init leddrv_init(void){
int ret=0;
platform_driver_register(&led_dev_driver);
return ret;
}

static void __exit leddrv_exit(void){
platform_driver_unregister(&led_dev_driver);
}


module_init(leddrv_init);
module_exit(leddrv_exit);

MODULE_AUTHOR("KEVIN");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("GPIO LED Platform Driver");

应用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char **argv) {
int fd;
char input_mode;

if (argc < 3) {
printf("Usage: ./%s /dev/<device> module(0/1)\n", argv[0]);
exit(1);
}

// 打开设备文件(确保有写权限)
fd = open(argv[1], O_WRONLY);
if (fd < 0) {
perror("open failed"); // 显示系统错误
exit(1);
}

// 解析并验证模块参数
input_mode = atoi(argv[2]);
if (input_mode != 0 && input_mode != 1) {
printf("Error: module must be 0 or 1\n");
exit(1);
}

// 写入数据并验证写入成功
if (write(fd, &input_mode, sizeof(input_mode)) != sizeof(input_mode)) {
perror("write failed"); // 显示系统错误
printf("Failed to write mode=%d\n", input_mode);
exit(1);
}
while(1);//让代码一直运行,来验证自旋锁的效果

close(fd); // 显式关闭文件描述符(虽然 exit 会自动关闭,但显式更好)
exit(0);
}

实验效果:

我们首先在开发板上启动驱动模块,随后使用命令打开led灯

image-20250728180023396

我们再通过mobaxterm使用命令关闭led灯:

image-20250728175724978

可以发现显示open failed: Device or resource busy,通过结果可以知道该设备已经被占用了。