原子、信号量、互斥锁、自旋锁

来源:互联网 发布:水利bim软件 编辑:程序博客网 时间:2022/05/28 05:32

一、原子操作
所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。
原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。
原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。原子类型定义如下:
typedef struct { volatile int counter; } atomic_t;
volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。

    原子操作API包括:     atomic_read(atomic_t * v);     该函数对原子类型的变量进行原子读操作,它返回原子类型的变量v的值。     atomic_set(atomic_t * v, int i);     该函数设置原子类型的变量v的值为i。     void atomic_add(int i, atomic_t *v);     该函数给原子类型的变量v增加值i。     atomic_sub(int i, atomic_t *v);     该函数从原子类型的变量v中减去i。     int atomic_sub_and_test(int i, atomic_t *v);     该函数从原子类型的变量v中减去i,并判断结果是否为0,如果为0,返回真,否则返回假。     void atomic_inc(atomic_t *v);     该函数对原子类型变量v原子地增加1。     void atomic_dec(atomic_t *v);     该函数对原子类型的变量v原子地减1。     int atomic_dec_and_test(atomic_t *v);     该函数对原子类型的变量v原子地减1,并判断结果是否为0,如果为0,返回真,否则返回假。     int atomic_inc_and_test(atomic_t *v);     该函数对原子类型的变量v原子地增加1,并判断结果是否为0,如果为0,返回真,否则返回假。     int atomic_add_negative(int i, atomic_t *v);     该函数对原子类型的变量v原子地增加I,并判断结果是否为负数,如果是,返回真,否则返回假。     int atomic_add_return(int i, atomic_t *v);     该函数对原子类型的变量v原子地增加i,并且返回指向v的指针。     int atomic_sub_return(int i, atomic_t *v);     该函数从原子类型的变量v中减去i,并且返回指向v的指针。     int atomic_inc_return(atomic_t * v);     该函数对原子类型的变量v原子地增加1并且返回指向v的指针。     int atomic_dec_return(atomic_t * v);     该函数对原子类型的变量v原子地减1并且返回指向v的指针。     原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构struct ipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中),使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1。     当不需要引用该IP碎片时,就使用函数ipq_put来释放该IP碎片,ipq_put使用函数atomic_dec_and_test把引用计数减1并判断引用计数是否为0,如果是就释放IP碎片。函数ipq_kill把IP碎片从ipq队列中删除,并把该删除的IP碎片的引用计数减1(通过使用函数atomic_dec实现)。

二、信号量(semaphore)

    Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,因此它与System V的IPC机制信号量毫不相干。     信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。     一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。     当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

Semaphore 操作API:
⒈ 声明初始化信号量

     DECLARE_MUTEX(name)     该宏声明一个信号量name并初始化它的值为1,即声明一个互斥锁。     DECLARE_MUTEX_LOCKED(name)     该宏声明一个互斥锁name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。     void sema_init (struct semaphore *sem, int val);     该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。 void init_MUTEX (struct semaphore *sem); 
#define init_MUTEX(sem)sema_init(sem, 1)

该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。

    void init_MUTEX_LOCKED (struct semaphore *sem); #define init_MUTEX_LOCKED(sem)sema_init(sem, 0)函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态

⒉ 获取一个信号量
void down(struct semaphore * sem);
该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。

    int down_interruptible(struct semaphore * sem);        

该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。

    int down_trylock(struct semaphore * sem);     该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用。

⒊  释放一个信号量
void up(struct semaphore * sem);
该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

Basic usage of the semaphore interface is as follows:

#include <linux/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 */

三、互斥锁
互斥锁主要用于实现内核中的互斥访问功能。内核互斥锁是在原子 API 之上实现的,但这对于内核用户是不可见的。
对它的访问必须遵循一些规则:
同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行 解锁。互斥锁不能进行递归锁定或解锁。
一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。
使用中的互斥锁是不能被重新初始化的。
并且互斥锁不能用于中断上下文。但是互斥锁比当前的内核信号量选项更快,并且更加紧凑,因此如果它们满足您的需求,那么它们将是您明智的选择。

Mutex 操作API:
⒈ 定义并初始化:
struct mutex mutex;
mutex_init(&mutex);
⒉ 获取互斥锁:
void mutex_lock(struct mutex *lock);
int mutex_trylock(struct mutex *lock);
⒊ 释放互斥锁:
void mutex_unlock(struct mutex *lock);
⒋ 清除互斥锁
void mutex_destroy(struct mutex *lock) –清除互斥锁,使互斥锁不可用
用mutex_destroy()函数解除由lock指向的互斥锁的任何状态。在调用执行这个函数的时候,lock指向的互斥锁不能在被锁状态。储存互斥锁的内存不被释放。
返回值–mutex_destroy()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
⒌ 测试互斥锁的状态
static inline int mutex_is_locked(struct mutex *lock)–测试互斥锁的状态
这个调用实际上编译成一个内联函数。如果互斥锁被持有(锁定),那么就会返回 1;否则,返回 0。
Basic mutex usage is as follows:

#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 */

四、自旋锁
在概念上,自旋锁非常简单。一个自旋锁是一个互斥设备,它只能有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的单个位。期望获得某特定锁的代码测试相关的位。如果锁可用,则“锁定”位被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环并重复检查这个锁,直到该锁可用为止。这个循环就是自旋锁的“自旋”部分。
适用于自旋锁的核心规则是:任何拥有自旋锁的代码都必须是原子的。它不能休眠,事实上,它不能因为任何原因放弃处理器,除了服务中断外(某些情况下此时也不能放弃处理器)。(休眠可发生在许多无法预期的地方;当我们编写需要在自旋锁下执行代码时,必须注意每一个所调用的函数。); 自旋锁必须在可能的最短时间内拥有。
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。
由于自旋锁使用者一般保持锁时间非常短,自旋锁的效率远高于互斥锁。(不需要切换)
自旋锁可以在任何上下文使用,而信号量(和互斥锁)适合保持时间较长的情况,它们会导致调用者睡眠,因此信号量(和互斥锁)只能在进程上下文使用(_trylock的变种能够在中断上下文使用)。(中断代码中不允许睡眠)
自旋锁保持期间是抢占失效的,而信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
缺点:一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间)。所以,自旋锁不应该被长时间持有。当然,可以采用另外的方式处理对锁的争用:让请求线程睡眠,直到锁重新可用时在唤醒它。但是,这里有两次明显的上下文切换,被阻塞的线程要换入或换出。因此,持有自旋锁的时间最好小于完成两次上下文切换的耗时。
Spin lock 操作API:
⒈ 初始化
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
或者在运行时,调用下面的函数:
void spin_lock_init(spinlock_t *lock);
⒉ 获得自旋锁
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock)
spin_loc_irqsave 禁止中断(只在本地处理器)在获得自旋锁之前; 之前的中断状态保存在 flags 里. 如果你绝对确定在你的处理器上没有禁止中断的(或者, 换句话说, 你确信你应当在你释放你的自旋锁时打开中断), 你可以使用 spin_lock_irq 代替, 并且不必保持跟踪 flags. 最后, spin_lock_bh 在获取锁之前禁止软件中断, 但是硬件中断留作打开的.
如果你有一个可能被在(硬件或软件)中断上下文运行的代码获得的自旋锁, 你必须使用一种 spin_lock 形式来禁止中断. 其他做法可能死锁系统, 迟早. 如果你不在硬件中断处理里存取你的锁, 但是你通过软件中断(例如, 在一个 tasklet 运行的代码, 后面会讲到), 你可以使用 spin_lock_bh 来安全地避免死锁, 而仍然允许硬件中断被服务.
⒊ 释放自旋锁
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
每个 spin_unlock 变体恢复由对应的 spin_lock 函数锁做的工作. 传递给 spin_unlock_irqrestore 的 flags 参数必须是传递给 spin_lock_irqsave 的同一个变量. 你必须也调用 spin_lock_irqsave 和 spin_unlock_irqrestore 在同一个函数里. 否则, 你的代码可能破坏某些体系.
还有一套非自旋的自旋锁操作:
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
这些函数成功时返回非零( 获得了锁 ), 否则 0. 没有”try”版本来禁止中断.
Basic spinlock usage is as follows:

#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 */
0 0
原创粉丝点击