`
网络接口
  • 浏览: 45076 次
文章分类
社区版块
存档分类
最新评论

linux驱动开发之信号量与自旋锁

阅读更多

在驱动程序中,当多个线程同时访问相同的资源时(驱动程序中的全局变量是一种典型的共享资源),可能会引发"竞态",因此我们必须对共享资源进行并发控制。Linux内核中解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。

自旋锁与信号量"类似而不类",类似说的是它们功能上的相似性,"不类"指代它们在本质和实现机理上完全不一样,不属于一类。

 

自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环查看是否该自旋锁的保持者已经释放了锁,"自旋"就是"在原地打转"。而信号量则引起调用者睡眠,它把进程从运行队列上拖出去,除非获得锁。这就是它们的"不类"。

 

但是,无论是信号量,还是自旋锁,在任何时刻,最多只能有一个保持者,即在任何时刻最多只能有一个执行单元获得锁。这就是它们的"类似"。

 

鉴于自旋锁与信号量的上述特点,一般而言,自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用;信号量适合于保持时间较长的情况,会只能在进程上下文使用。如果被保护的共享资源只在进程上下文访问,则可以以信号量来保护该共享资源,如果对共享资源的访问时间非常短,自旋锁也是好的选择。但是,如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。

 

与信号量相关的API主要有:

 

定义信号量

 

struct semaphore sem;

 

初始化信号量

 

void sema_init (struct semaphore *sem, int val);

 

该函数初始化信号量,并设置信号量sem的值为val

 

void init_MUTEX (struct semaphore *sem);

 

该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1,等同于sema_init (struct semaphore *sem, 1);

 

void init_MUTEX_LOCKED (struct semaphore *sem);

 

该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,等同于sema_init (struct semaphore *sem, 0);

 

获得信号量

 

void down(struct semaphore * sem);

 

该函数用于获得信号量sem,它会导致睡眠(这个睡眠和下面所说的不知道有什么不同,既然不能被其它地方唤醒,那么这个down有什么用呢?),因此不能在中断上下文使用;

 

int down_interruptible(struct semaphore * sem);

 

该函数功能与down类似,不同之处为,down不能被信号打断,但down_interruptible能被信号打断;(这个能被信号打断,有点疑惑,我现在做的项目是使用的是被中断打断,不知道它这个地方所说的是什么意思)

 

int down_trylock(struct semaphore * sem);

 

该函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0值。它不会导致调用者睡眠,可以在中断上下文使用。

 

释放信号量

 

void up(struct semaphore * sem);

 

该函数释放信号量sem,唤醒等待者。

 

与自旋锁相关的API主要有:

 

定义自旋锁

 

spinlock_t spin;

 

初始化自旋锁

 

spin_lock_init(lock)

 

该宏用于动态初始化自旋锁lock

 

获得自旋锁

 

spin_lock(lock)

 

该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放;

 

spin_trylock(lock)

 

该宏尝试获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假,实际上不再"在原地打转";

 

释放自旋锁

 

spin_unlock(lock)

 

该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用;

 

除此之外,还有一组自旋锁使用于中断情况下的API。

 

2.5.1 自旋锁和互斥体

访问共享资源的代码区域称作临界区。自旋锁(spinlock)和互斥体(mutex,mutual exclusion的缩写)是保护内核临界区的两种基本机制。我们逐个分析。

自旋锁可以确保在同时只有一个线程进入临界区。其他想进入临界区的线程必须不停地原地打转,直到第1个线程释放自旋锁。注意:这里所说的线程不是内核线程,而是执行的线程。

下面的例子演示了自旋锁的基本用法:

#include <linux/spinlock.h>

spinlock_t mylock = SPIN_LOCK_UNLOCKED; /* Initialize */

 

/* Acquire the spinlock. This is inexpensive if there

* is no one inside the critical section. In the face of

* contention, spinlock() has to busy-wait.

*/

spin_lock(&mylock);

 

/* ... Critical Section code ... */

 

spin_unlock(&mylock); /* Release the lock */

与自旋锁不同的是,互斥体在进入一个被占用的临界区之前不会原地打转,而是使当前线程进入睡眠状态。如果要等待的时间较长,互斥体比自旋锁更合适,因为自旋锁会消耗CPU资源。在使用互斥体的场合,多于2次进程切换时间都可被认为是长时间,因此一个互斥体会引起本线程睡眠,而当其被唤醒时,它需要被切换回来。

因此,在很多情况下,决定使用自旋锁还是互斥体相对来说很容易:

(1) 如果临界区需要睡眠,只能使用互斥体,因为在获得自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的;

(2) 由于互斥体会在面临竞争的情况下将当前线程置于睡眠状态,因此,在中断处理函数中,只能使用自旋锁。(第4章将介绍更多的关于中断上下文的限制。)

下面的例子演示了互斥体使用的基本方法:

#include <linux/mutex.h>

 

/* Statically declare a mutex. To dynamically

create a mutex, use mutex_init() */

static DEFINE_MUTEX(mymutex);

 

/* Acquire the mutex. This is inexpensive if there

* is no one inside the critical section. In the face of

* contention, mutex_lock() puts the calling thread to sleep.

*/

mutex_lock(&mymutex);

 

/* ... Critical Section code ... */

 

mutex_unlock(&mymutex);      /* Release the mutex */

为了论证并发保护的用法,我们首先从一个仅存在于进程上下文的临界区开始,并以下面的顺序逐步增加复杂性:

(1) 非抢占内核,单CPU情况下存在于进程上下文的临界区;

(2) 非抢占内核,单CPU情况下存在于进程和中断上下文的临界区;

(3) 可抢占内核,单CPU情况下存在于进程和中断上下文的临界区;

(4) 可抢占内核,SMP情况下存在于进程和中断上下文的临界区。

旧的信号量接口

互斥体接口代替了旧的信号量接口(semaphore)。互斥体接口是从-rt树演化而来的,在2.6.16内核中被融入主线内核。

尽管如此,但是旧的信号量仍然在内核和驱动程序中广泛使用。信号量接口的基本用法如下:

#include <asm/semaphore.h>  /* Architecture dependent header */

 

/* Statically declare a semaphore. To dynamically

create a semaphore, use init_MUTEX() */

static DECLARE_MUTEX(mysem);

 

down(&mysem);    /* Acquire the semaphore */

 

/* ... Critical Section code ... */

 

up(&mysem);      /* Release the semaphore */

1. 案例1:进程上下文,单CPU,非抢占内核

这种情况最为简单,不需要加锁,因此不再赘述。

2. 案例2:进程和中断上下文,单CPU,非抢占内核

在这种情况下,为了保护临界区,仅仅需要禁止中断。如图2-4所示,假定进程上下文的执行单元A、B以及中断上下文的执行单元C都企图进入相同的临界区。

 

图2-4 进程和中断上下文进入临界区

由于执行单元C总是在中断上下文执行,它会优先于执行单元A和B,因此,它不用担心保护的问题。执行单元A和B也不必关心彼此会被互相打断,因为内核是非抢占的。因此,执行单元A和B仅仅需要担心C会在它们进入临界区的时候强行进入。为了实现此目的,它们会在进入临界区之前禁止中断:

Point A:

local_irq_disable();  /* Disable Interrupts in local CPU */

/* ... Critical Section ...  */

local_irq_enable();   /* Enable Interrupts in local CPU */

但是,如果当执行到Point A的时候已经被禁止,local_irq_enable()将产生副作用,它会重新使能中断,而不是恢复之前的中断状态。可以这样修复它:

unsigned long flags;

 

Point A:

local_irq_save(flags);     /* Disable Interrupts */

/* ... Critical Section ... */

local_irq_restore(flags);  /* Restore state to what it was at Point A */

不论Point A的中断处于什么状态,上述代码都将正确执行。

3. 案例3:进程和中断上下文,单CPU,抢占内核

如果内核使能了抢占,仅仅禁止中断将无法确保对临界区的保护,因为另一个处于进程上下文的执行单元可能会进入临界区。重新回到图2-4,现在,除了C以外,执行单元A和B必须提防彼此。显而易见,解决该问题的方法是在进入临界区之前禁止内核抢占、中断,并在退出临界区的时候恢复内核抢占和中断。因此,执行单元A和B使用了自旋锁API的irq变体:

unsigned long flags;

 

Point A:

/* Save interrupt state.

* Disable interrupts - this implicitly disables preemption */

spin_lock_irqsave(&mylock, flags);

 

/* ... Critical Section ... */

 

/* Restore interrupt state to what it was at Point A */

spin_unlock_irqrestore(&mylock, flags);

我们不需要在最后显示地恢复Point A的抢占状态,因为内核自身会通过一个名叫抢占计数器的变量维护它。在抢占被禁止时(通过调用preempt_disable()),计数器值会增加;在抢占被使能时(通过调用preempt_enable()),计数器值会减少。只有在计数器值为0的时候,抢占才发挥作用。

4. 案例4:进程和中断上下文,SMP机器,抢占内核

现在假设临界区执行于SMP机器上,而且你的内核配置了CONFIG_SMP和CONFIG_PREEMPT。

到目前为止讨论的场景中,自旋锁原语发挥的作用仅限于使能和禁止抢占和中断,时间的锁功能并未被完全编译进来。在SMP机器内,锁逻辑被编译进来,而且自旋锁原语确保了SMP安全性。SMP使能的含义如下:

unsigned long flags;

 

Point A:

/*

- Save interrupt state on the local CPU

- Disable interrupts on the local CPU. This implicitly disables preemption.

- Lock the section to regulate access by other CPUs

*/

spin_lock_irqsave(&mylock, flags);

 

/* ... Critical Section ... */

 

/*

- Restore interrupt state and preemption to what it

was at Point A for the local CPU

- Release the lock

*/

spin_unlock_irqrestore(&mylock, flags);

在SMP系统上,获取自旋锁时,仅仅本CPU上的中断被禁止。因此,一个进程上下文的执行单元(图2-4中的执行单元A)在一个CPU上运行的同时,一个中断处理函数(图2-4中的执行单元C)可能运行在另一个CPU上。非本CPU上的中断处理函数必须自旋等待本CPU上的进程上下文代码退出临界区。中断上下文需要调用spin_lock()/spin_unlock():

spin_lock(&mylock);

 

/* ... Critical Section ... */

 

spin_unlock(&mylock);

除了有irq变体以外,自旋锁也有底半部(BH)变体。在锁被获取的时候,spin_lock_bh()会禁止底半部,而spin_unlock_bh()则会在锁被释放时重新使能底半部。我们将在第4章讨论底半部。

-rt树

实时(-rt)树,也被称作CONFIG_PREEMPT_RT补丁集,实现了内核中一些针对低延时的修改。该补丁集可以从www.kernel.org/pub/linux/kernel/projects/rt下载,它允许内核的大部分位置可被抢占,但是用自旋锁代替了一些互斥体。它也合并了一些高精度的定时器。数个-rt功能已经被融入了主线内核。详细的文档见http://rt.wiki.kernel.org/。

为了提高性能,内核也定义了一些针对特定环境的特定的锁原语。使能适用于代码执行场景的互斥机制将使代码更高效。下面来看一下这些特定的互斥机制。

2.5.2 原子操作

原子操作用于执行轻量级的、仅执行一次的操作,例如修改计数器、有条件的增加值、设置位等。原子操作可以确保操作的串行化,不再需要锁进行并发访问保护。原子操作的具体实现取决于体系架构。

分享到:
评论

相关推荐

    linux设备驱动详解(宋宝华 高清 非影印版)

    《Linux设备驱动开发详解》是一本介绍Linux设备驱动开发理论、框架与实例的书,《Linux设备驱动开发详解》以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射...

    Linux驱动程序开发第三版

    10. **内核同步和并发控制**:讲解信号量、互斥锁、自旋锁、RCU(读取复制更新)等内核同步机制,确保驱动程序在多线程环境中的正确性。 通过学习本书,读者不仅可以掌握Linux驱动程序开发的基本技能,还能对Linux...

    并发控制之自旋锁.pdf

    4. **自旋锁的定义与操作**:在Linux中,自旋锁通过`spinlock_t`类型表示。定义一个自旋锁使用`spinlock_t lock`,获取锁使用`spin_lock(&lock)`,尝试获取锁使用`spin_trylock(&lock)`,释放锁则用`spin_unlock(&...

    Linux设备驱动程序开发详解

    《Linux设备驱动开发详解》是一本介绍Linux设备驱动开发理论、框架与实例的书,《Linux设备驱动开发详解(第2版)》基于LDD6410开发板,以Linux2.6 版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、...

    linux驱动开发详解

    本书是一本介绍Linux设备驱动开发理论、框架与实例的书,本书以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射以及异步通知、阻塞I/O、非阻塞I/O等Linux 设备驱动...

    《Linux 设备驱动开发详解》第一版第一次印刷勘误

    本书是一本介绍Linux设备驱动开发理论、框架与实例的书,以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射以及异步通知、阻塞I/O、非阻塞I/O等Linux设备驱动...

    Linux设备驱动开发

    这是一本介绍Linux设备驱动开发理论、框架与实例的书,《Linux设备驱动开发详解(第2版)》基于LDD6410开发板,以Linux2.6 版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射...

    Linux驱动知识点总结.doc

    9. 自旋锁与信号量的使用: - 自旋锁适用于短暂的互斥,进程不会睡眠,适合中断服务程序。 - 信号量适用于可能需要长时间等待的互斥,因为中断服务程序中不能睡眠,所以使用自旋锁。 10. 原子操作: - 原子操作...

    linux驱动开发详解4

    本书是一本介绍Linux设备驱动开发理论、框架与实例的书,本书以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射以及异步通知、阻塞I/O、非阻塞I/O等Linux 设备驱动...

    linux驱动开发详解3

    本书是一本介绍Linux设备驱动开发理论、框架与实例的书,本书以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射以及异步通知、阻塞I/O、非阻塞I/O等Linux 设备驱动...

    linux驱动开发详解2

    本书是一本介绍Linux设备驱动开发理论、框架与实例的书,本书以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射以及异步通知、阻塞I/O、非阻塞I/O等Linux 设备驱动...

    linux驱动开发详解5

    本书是一本介绍Linux设备驱动开发理论、框架与实例的书,本书以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射以及异步通知、阻塞I/O、非阻塞I/O等Linux 设备驱动...

    i.MX6ULL实现自选锁驱动【Linux驱动】.zip

    本文将深入探讨基于NXP的i.MX6ULL处理器的Linux驱动程序开发,特别是自选锁(自旋锁)的实现。 i.MX6ULL是一款低功耗、高性能的应用处理器,广泛应用于嵌入式系统和物联网设备中。它基于ARM Cortex-A7架构,支持...

    Linux设备驱动中的互斥机制.pdf

    Linux 提供了多种互斥机制,包括中断屏蔽、原子操作、信号量和自旋锁等。本文将详细介绍这些互斥机制的优缺点和使用方法。 1. 中断屏蔽 中断屏蔽是避免竞态的简单方法,即在进入临界区之前屏蔽系统的中断。这可以...

    嵌入式Linux设备驱动开发

    本书是一本介绍Linux设备驱动开发理论、框架与实例的书,本书以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射以及异步通知、阻塞I/O、非阻塞I/O等Linux设备驱动...

    正点原子STM32P1开发板Linux驱动教程

    1. **信号量**:使用信号量来同步访问共享资源。 2. **互斥锁**:使用互斥锁来避免数据竞争。 3. **自旋锁**:在短时间内保护临界区。 #### 九、其他高级驱动开发实验 除了上述内容外,还包括一些更高级的驱动开发...

    Linux设备驱动开发详解 CD

    本书是一本介绍Linux设备驱动开发理论、框架与实例的书,本书以Linux 2.6版本内核为蓝本,详细介绍自旋锁、信号量、完成量、中断顶/底半部、定时器、内存和I/O映射以及异步通知、阻塞I/O、非阻塞I/O等Linux 设备驱动...

    RV1126实现信号量测试【Linux驱动】.zip

    在RV1126的Linux驱动程序开发中,信号量可能用于保护设备的注册表、内存缓冲区或其他关键资源。由于RV1126可能需要与硬件进行频繁交互,因此有效的同步机制对于避免数据不一致和系统崩溃至关重要。 在项目的代码中...

    嵌入式Linux的同步机制在设备驱动开发中的应用.pdf

    为了解决这个问题,嵌入式Linux提供了多种同步机制,如自旋锁(spinlock)、信号量(semaphore)等。 自旋锁是一种简单的互斥访问机制,适合于多处理器环境。当一个线程试图获取已被其他线程持有的自旋锁时,它会...

Global site tag (gtag.js) - Google Analytics