第5章:并发与竞态条件-8:Introduction to the Spinlock API

In continuation of the previous text 第5章:并发与竞态条件-7:Spinlocks, let's GO ahead.

Introduction to the Spinlock API

The required include file for the spinlockprimitives is . An actual lockhas the type spinlock_t. Like any other data structure, a spinlock must be initialized. This initialization may be done at compile time as follows:

使用自旋锁原语需要包含头文件 <linux/spinlock.h> 头文件。自旋锁的实际类型是spinlock_t。与其他数据结构一样,自旋锁必须初始化,可通过以下两种方式:编译时初始化

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;

or at runtime with:运行时初始化

void spin_lock_init(spinlock_t *lock);

Before entering a critical section, your code must obtain the requisite lock with:

进入临界区前,代码必须通过以下函数获取所需的锁:

void spin_lock(spinlock_t *lock);

Note that all spinlockwaits are, by their nature, uninterruptible. Once you call spin_lock, you will spin until the lock becomes available. To release a lock that you have obtained, pass it to:

注意:所有自旋锁的等待本质上都是不可中断的。一旦调用spin_lock,代码会一直自旋,直到锁可用。释放已获取的锁需调用:

void spin_unlock(spinlock_t *lock);

There are many other spinlockfunctions, and we will lookat them all shortly. But none of them depart from the core idea shown by the functions listed above. There is very little that one can do with a lock, other than lock and release it. However, there are a few rules about how you must work with spinlocks. We will take a moment to look at those before getting into the full spinlock interface.

自旋锁还有许多其他函数,我们稍后会逐一介绍,但它们都离不开上述核心思想。对锁的操作除了获取和释放外,几乎没有其他功能。不过,使用自旋锁有几条必须遵守的规则,在介绍完整接口前,我们先花点时间了解这些规则。

Spinlocks and Atomic Context

Imagine for a moment that your driver acquires a spinlockand goes about its busi ness within its critical section. Somewhere in the middle, your driver loses the pro cessor. Perhaps it has called a function (copy_from_user, say) that puts the process to sleep. Or, perhaps, kernel preemption kicks in, and a higher-priority process pushes your code aside. Your code is now holding a lockthat it will not release any time in the foreseeable future. If some other thread tries to obtain the same lock, it will, in the best case, wait (spinning in the processor) for a very long time. In the worst case, the system could deadlock entirely.

不妨设想这样一种场景:你的驱动获取了自旋锁,然后在临界区内执行操作。执行过程中,驱动突然失去了处理器使用权 —— 可能是调用了会让进程睡眠的函数(比如 copy_from_user),也可能是内核抢占触发,更高优先级的进程将你的代码挤掉。此时你的代码持有锁,却在可预见的时间内无法释放。如果其他线程尝试获取同一个锁,最好的情况是长时间自旋等待,最坏的情况则是系统完全死锁。

Most readers would agree that this scenario is best avoided. Therefore, the core rule that applies to spinlocks is that any code must, while holding a spinlock, be atomic. It cannot sleep; in fact, it cannot relinquish the processor for any reason except to service interrupts (and sometimes not even then).

大多数开发者都会认同,这种场景必须避免。因此,自旋锁的核心规则是:持有自旋锁时,代码必须处于原子上下文。它不能睡眠,实际上,除了响应中断(有时甚至中断也不允许),不能以任何理由放弃处理器使用权。

The kernel preemption case is handled by the spinlock code itself. Any time kernel code holds a spinlock, preemption is disabled on the relevant processor. Even uni processor systems must disable preemption in this way to avoid race conditions. That is why proper locking is required even if you never expect your code to run on a multiprocessor machine.

内核抢占的问题由自旋锁代码自身处理:只要内核代码持有自旋锁,当前处理器的抢占就会被禁用。即使是单处理器系统,也必须通过这种方式禁用抢占以避免竞争条件 —— 这就是为什么即使你的代码永远不会运行在多处理器机器上,也需要实现正确的锁机制。

Avoiding sleep while holding a lockcan be more difficult; many kernel functions can sleep, and this behavior is not always well documented. Copying data to or from user space is an obvious example: the required user-space page may need to be swapped in from the diskbefore the copy can proceed, and that operation clearly requires a sleep. Just about any operation that must allocate memory can sleep; kmalloc can decide to give up the processor, and wait for more memory to become available unless it is explicitly told not to. Sleeps can happen in surprising places; writing code that will execute under a spinlockrequires paying attention to every function that you call.

持有锁时避免睡眠难度更大:许多内核函数可能会睡眠,且这种行为并非总能被清晰文档化。最典型的例子是与用户空间的数据拷贝:所需的用户空间页面可能需要从磁盘换入后才能完成拷贝,而换页操作必然会导致睡眠。几乎所有需要分配内存的操作都可能睡眠 ——kmalloc 除非被明确指定不允许,否则可能会放弃处理器,等待更多内存可用。睡眠可能发生在意想不到的地方,因此编写自旋锁保护下的代码时,必须关注每一个调用的函数。

Here’s another scenario: your driver is executing and has just taken out a lock that controls access to its device. While the lockis held, the device issues an interrupt, which causes your interrupt handler to run. The interrupt handler, before accessing the device, must also obtain the lock. Taking out a spinlock in an interrupt handler is a legitimate thing to do; that is one of the reasons that spinlockoperations do not sleep. But what happens if the interrupt routine executes in the same processor as the code that tookout the lockoriginally? While the interrupt handler is spinning, the noninterrupt code will not be able to run to release the lock. That processor will spin forever.

还有一种场景:你的驱动正在执行,且刚刚获取了保护设备访问的锁。持有锁期间,设备触发了中断,导致中断处理程序运行。而中断处理程序在访问设备前,也必须获取同一个锁。在中断处理程序中使用自旋锁是合法的(这也是自旋锁操作不睡眠的原因之一),但如果中断处理程序与持有锁的代码运行在同一个 CPU上,问题就会出现:中断处理程序自旋等待锁释放,而持有锁的非中断代码被中断阻塞,无法运行释放锁 —— 这个 CPU 会陷入无限自旋。

Avoiding this trap requires disabling interrupts (on the local CPU only) while the spinlockis held. There are variants of the spinlockfunctions that will disable inter rupts for you (we’ll see them in the next section). However, a complete discussion of interrupts must wait until Chapter 10.

避免这种陷阱的方法是:持有自旋锁时,禁用本地 CPU 的中断。自旋锁有专门的函数变体可自动帮你禁用中断(下一节会介绍)。不过,关于中断的完整讨论需要等到第 10 章。

The last important rule for spinlockusage is that spinlocks must always be held for the minimum time possible. The longer you hold a lock, the longer another proces sor may have to spin waiting for you to release it, and the chance of it having to spin at all is greater. Long lockhold times also keep the current processor from schedul ing, meaning that a higher priority process—which really should be able to get the CPU—may have to wait. The kernel developers put a great deal of effort into reduc ing kernel latency (the time a process may have to wait to be scheduled) in the 2.5 development series. A poorly written driver can wipe out all that progress just by holding a lockfor too long. To avoid creating this sort of problem, make a point of keeping your lock-hold times short.

使用自旋锁的最后一条重要规则是:锁的持有时间必须尽可能短。持有锁的时间越长,其他处理器自旋等待的时间就越长,发生竞争的概率也越高。长时间持有锁还会阻止当前处理器进行调度,导致本应获得 CPU 的高优先级进程被迫等待。内核开发者在 2.5 开发系列中,投入了大量精力降低内核延迟(进程等待调度的时间)。而一个编写糟糕的驱动,只需长时间持有锁,就能让这些优化成果付诸东流。为避免此类问题,务必确保锁的持有时间极短。

补充说明:

  1. 原子上下文的核心定义 ​​​

    ​​​​​​​原子上下文是指代码执行不能被打断的环境,分为两类 —— 中断上下文(如中断处理程序)和进程上下文(持有自旋锁、禁用抢占的代码)。

  2. 禁止睡眠的具体场景

    持有自旋锁时,不能调用以下函数:
    1. 可能睡眠的内存分配函数(如 kmalloc(GFP_KERNEL)vmalloc);

    2. 与用户空间交互的函数(如 copy_from_usercopy_to_user);

    3. 等待队列相关函数(如 wait_eventschedule);

    4. 其他可能触发调度或睡眠的内核函数(如 msleepfile_operations 中的阻塞操作)。

  3. 中断禁用的粒度

    ​​​​​​​​​​​​​​仅需禁用本地 CPU的中断,无需全局禁用 —— 自旋锁的竞争仅发生在同一锁上,禁用本地中断可避免同一 CPU 上的中断处理程序自旋死锁,同时不影响其他 CPU 的中断响应。

  4. 锁持有时间的优化技巧

    • 临界区仅包含必要的原子操作(如修改共享变量、读取设备寄存器);

    • 复杂计算、I/O 操作等耗时任务,移到锁外部执行;

    • 避免在临界区内调用外部函数(尤其是未明确文档化是否睡眠的函数)。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeeplyMind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值