在内核中会经常看到spin_lock(自旋锁),它到底是个神马东西?在驱动相关的书籍和论坛中查阅了不少资料,看的也是云里雾里,现在将知识罗列总结一下,便于日后回顾!
1.自旋锁定义
在Linux内核并发控制中最常见的锁就是自旋锁。自旋锁最多只能被一个可执行的线程持有。如果一个执行线程试图获得一个已经被持有的自旋锁,那么该线程就会一直进行“ 忙循环 — 旋转 — 等待锁重新可用 ”,这就是自旋的过程。要是锁未被持有(争用),请求锁的执行线程就可以立即得到它,继续执行,而代码继续进入临界区。
在任意时间内,自旋锁都可以防止多于一个执行线程进入临界区。即使多个线程在给定时间自旋,也只有一个线程可获得该锁。同一个锁可以用在多个位置,比如对于给定数据的所有访问都可以得到保护和同步。
2. 死锁的情况
自旋锁最初是为了多处理器系统(SMP,multiprocessing system)使用而设计的,但是只要考虑到并发问题,单处理器在运行可抢占内核时,其行为就类似于SMP。因此,自旋锁对于SMP和单处理器可抢占内核都适用。
可以想象,当一个处理器处于自旋状态时,它做不了任何的工作,因此自旋锁对于单处理器不可抢占内核是没有意义的,实际上非抢占式的单处理器系统上自旋锁常被实现为空操作,不做任何的事情。
自旋锁的重要特性如下:
1)被自旋锁保护的临界区代码在执行时,不能进入休眠。
2)被自旋锁保护的临界区代码在执行时,不能被其他中断打断。
3)被自旋锁保护的临界区代码在执行时,内核不能被抢占。
从以上几个特性可以归纳出共性:被自旋锁保护的临界区代码在执行时,它不能因为任何原因放弃处理器。
考虑上面第一种和第三种情况,若你的内核代码请求到一个自旋锁并且在它的临界区里做它的事情,在中间某处,你的代码失去了处理器。或许它已调用了一个函数(copy_from_user,假设)使进程进入睡眠。也或许,内核抢占发威,一个更高优先级的进程将你的代码推到了一边。此时,正好某个别的线程想获取同一个锁,如果这个线程运行在和你的内核代码不同的处理器上(幸运的情况),那么它可能要自旋等待一段时间(可能很长),当你的代码从休眠中唤醒或者重新得到处理器并释放锁,它就能得到锁。而最坏的情况是,那个想获取锁得线程刚好和你的代码运行在同一个处理器上,这时它将一直持有CPU进行自旋操作,而你的代码是永远不可能有任何机会来获得CPU释放这个锁了,这就是悲催的死锁。
考虑上面第二种情况,和上面第一种情况类似。假设我们的驱动程序正在运行,并且已经获取了一个自旋锁,这个锁控制着对设备的访问。在拥有这个锁得时候,设备产生了一个中断,它导致中断处理例程被调用,而中断处理例程在访问设备之前,也要获得这个锁。当中断处理例程和我们的驱动程序代码在同一个处理器上运行时,由于中断处理例程持有CPU不断自旋,我们的代码将得不到机会释放锁,这也将导致死锁。
因此,如果我们有一个自旋锁,它可以被运行在(硬件或软件)中断上下文中的代码获得,则必须使用某个禁用中断的 spin_lock 形式的锁来禁用本地中断(注意,只是禁用本地CPU的中断,不能禁用别的处理器的中断),使用其他的锁定函数迟早会导致系统死锁(导致死锁的时间可能不定,但是发生上述死锁情况的概率肯定是有的,看处理器怎么调度了)。如果我们不会在硬中断处理例程中访问自旋锁,但可能在软中断(例如,以tasklet的形式运行的代码)中访问,则应该使用 spin_lock_bh,以便在安全避免死锁的同时还能服务硬件中断。
3. 自旋锁函数
3.1 基本函数
void spin_lock(spinlock_t *lock);
最基本得自旋锁函数,它不失效本地中断。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
在获得自旋锁之前禁用硬中断(只在本地处理器上),而先前的中断状态保存在flags中。
void spin_lock_irq(spinlock_t *lock);
在获得自旋锁之前禁用硬中断(只在本地处理器上),不保存中断状态。
void spin_lock_bh(spinlock_t *lock);
在获得锁前禁用软中断,保持硬中断打开状态。
3.2 函数定义
自旋锁的实现接口定义在\include\linux\spinlock.h
自旋锁的结构体 spinlock_t
spinlock_t()
typedef struct {
/**
* 该字段表示自旋锁的状态,值为1表示未加锁,任何负数和0都表示加锁
*/
volatile unsigned int slock;
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned magic;
#endif
#ifdef CONFIG_PREEMPT
/**
* 表示进程正在忙等待自旋锁。
* 只有内核支持SMP和内核抢占时才使用本标志。
*/
unsigned int break_lock;
#endif
} spinlock_t;
spin_lock()
/**
* 当内核不可抢占时,spin_lock的实现过程。
*/
#define _spin_lock(lock) \
do { \
/**
* 调用preempt_disable禁用抢占。
*/
preempt_disable(); \
/**
* _raw_spin_lock对自旋锁的slock字段执行原子性的测试和设置操作。
*/
_raw_spin_lock(lock); \
__acquire(lock); \
} while(0)
函数_raw_spin_lock()对自旋锁的SLOCK字段执行原子性的测试和设置操作。
#define _raw_spin_lock(x) \
do { \
CHECK_LOCK(x); \
if ((x)->lock&&(x)->babble) { \
(x)->babble--; \
printk("%s:%d: spin_lock(%s:%p) already locked by %s/%d\n", \
__FILE__,__LINE__, (x)->module, \
(x), (x)->owner, (x)->oline); \
} \
(x)->lock = 1; \
(x)->owner = __FILE__; \
(x)->oline = __LINE__; \
} while (0)
spin_unlock()
#define _spin_unlock(lock) \
do { \
_raw_spin_unlock(lock); \
preempt_enable(); \
__release(lock); \
} while (0)
函数 _raw_spin_unlock()
static inline void _raw_spin_unlock(spinlock_t *lock)
{
#ifdef CONFIG_DEBUG_SPINLOCK
BUG_ON(lock->magic != SPINLOCK_MAGIC);
BUG_ON(!spin_is_locked(lock));
#endif
__asm__ __volatile__(
spin_unlock_string
);
}
宏函数spin_unlock_string
在spin_unlock_string中,%0即为锁 - > s 锁,movb指令将锁 - > s 锁定为1,movb指令本身就是原子操作,所以不需要锁总线。
#define spin_unlock_string \
"movb $1,%0" \
:"=m" (lock->slock) : : "memory"
自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只有一个线程位于临界区内,这就为多处理器机器提供了防止并发访问所需的保护机制。
在单处理机器上,编译的时候不会加入自旋锁,仅会被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁就会被剔除出内核。
内核提供的禁止中断同时请求锁的接口
spin_lock_irqsave()
保存中断的当前状态,并禁止本地中断,然后再去获取指定的锁。
unsigned long __lockfunc _spin_lock_irqsave(spinlock_t *lock)
{
unsigned long flags;
local_irq_save(flags);
preempt_disable();
_raw_spin_lock_flags(lock, flags);
return flags;
}
pin_unlock_irqrestore()
对指定的锁解锁,然后让中断恢复到加锁前的状态。
void __lockfunc _write_unlock_irqrestore(rwlock_t *lock, unsigned long flags)
{
_raw_write_unlock(lock);
local_irq_restore(flags);
preempt_enable();
}
如果能确定中断在加锁前是激活的,那就不需要在解锁后恢复中断以前的状态。也就可以无条件地在解锁时激活中断。这时可以使用spin_lock_irq()和spin_unlock_irq()。
spin_lock_irq()
禁止本地中断并获取指定的锁。
void __lockfunc _read_lock_irq(rwlock_t *lock)
{
local_irq_disable();
preempt_disable();
_raw_read_lock(lock);
}
spin_unlock_irq()
释放指定的锁,并激活本地中断。
void __lockfunc _spin_unlock_irq(spinlock_t *lock)
{
_raw_spin_unlock(lock);
local_irq_enable();
preempt_enable();
}
在使用spin_lock_irq()方法时,需要确定中断原来是否处于激活状态。一般不建议使用。
spin_lock_init()
动态初始化指定的spinlock_t,(此时只有一个指向spinlock_t类型地指针,没有它的实体)
#define spin_lock_init(lock) do { (void)(lock); } while(0)
spin_try_lock()
试图获的某个特定的自旋锁。如果该锁已经被争用,那么该函数立即返回一个非0值,而不会自旋等待锁被释放; 如果成功地获得了这个自旋锁,该函数返回0。
int __lockfunc _spin_trylock(spinlock_t *lock)
{
preempt_disable(); //使抢占计数加1
if (_raw_spin_trylock(lock))
return 1;
preempt_enable(); // 使抢占计数减1,并在thread_info描述符的TIF_NEED_RESCHED标志被置为1的情况下,调用preempt_schedule()
return 0;
}
spin_is_locked()
用于检查特定的锁当前是否已被占用,如果已被占用,返回非0值;否则返回0。
#define spin_is_locked(x) \
({ \
CHECK_LOCK(x); \
if ((x)->lock&&(x)->babble) { \
(x)->babble--; \
printk("%s:%d: spin_is_locked(%s:%p) already locked by %s/%d\n", \
__FILE__,__LINE__, (x)->module, \
(x), (x)->owner, (x)->oline); \
} \
0; \
})
4. 总结
自旋锁使用总结:
(1) 一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,会特别浪费处理器时间。所以自旋锁不应该被长时间持有。因此,自旋锁应该使用在:短时间内进行轻量级加锁。
(2)还可以采取另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它这样处理器不必循环等待,可以执行其他任务。
但是让请求线程睡眠的处理也会带来一定开销:会有两次上下文切换,被阻塞的线程要换出和换入。所以,自旋持有锁的时间最好小于完成两次上下文的耗时,也就是让持有自旋锁的时间尽可能短。(在抢占式内核中,锁持有等价于系统的调度等待时间),信号量可以在发生争用时,等待的线程能投入睡眠,而不是旋转。
(3)在单处理机器上,自旋锁是无意义的。因为在编译时不会加入自旋锁,仅仅被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除出内核。
(4)Linux内核中,自旋锁是不可递归的。如果试图得到一个你正在持有的锁,你必须去自旋,等待你自己释放这个锁。但这时你处于自旋忙等待中,所以永远不会释放锁,就会造成死锁现象。
(5)在中断处理程序中,获取锁之前一定要先禁止本地中断(当前处理器的中断),否则,中断程序就会打断正持有锁的内核代码,有可能试图去争用这个已经被持有的自旋锁。这样就会造成双重请求死锁(中断处理程序会自旋,等待该锁重新可用,但锁的持有者在这个处理程序执行完之前是不可能运行的)。
(6)锁真正保护的是数据(共享数据),而不是代码。
自旋锁一般这样被使用:
/* 定义一个自旋锁 */
spinlock_t lock;
/* 初始化自旋锁 */
spin_lock_init(&lock);
/* 获取自旋锁,保护临界区 */
spin_lock(&lock);
. . . //临界区
/* 解锁 */
spin_unlock(&lock);
5. 实例代码
驱动层:
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <mach/gpio.h>
#include <linux/delay.h>
#include <mach/regs-gpio.h> /*S5PV210_GPH3_BASE*/
#define EINT_DEVICE_ID 1
#define DRIVER_NAME "key_eint_race"
#define err(msg) printk(KERN_ERR "%s: " msg "\n", DRIVER_NAME)
#define __debug(fmt, arg...) printk(KERN_DEBUG fmt, ##arg)
#define GPH3CON (unsigned long)(S5PV210_GPH3_BASE+ 0x00)
#define GPH3DAT (unsigned long)(S5PV210_GPH3_BASE + 0x04)
#define GPH2UP (unsigned long)(S5PV210_GPH2_BASE + 0x08)
static int major = 0; /* Driver Major Number */
static int minor = 0; /* Driver Minor Number */
struct class *key_class;
static struct device *key_device;
static unsigned int key;
static spinlock_t my_spin_lock; //
static unsigned int deal_key_value(unsigned int data)
{
key = data;
udelay(1000);
return key;
}
static unsigned int deal_key_value_excl(unsigned int data)
{
unsigned int value;
unsigned long flag;
/*
* 自旋锁解决了SMP多处理的并发问题,对于内核的抢占也可以起到关闭的作用,
* 可仍没有解决中断产生的并发问题,但是不用担心,我们可以采用spin_lock
* 的变体spin_lock_irqsave函数完成同时关中断的功能
*
* 注意:此处用自旋锁并不合理,因为我们的临界区udelay了1ms(时间很长),
* 但我们本例子的目的是验证自旋锁的作用,在实际编程中要特别注意
*/
spin_lock_irqsave(&my_spin_lock,flag); //获取自旋锁
value =deal_key_value(data);
spin_unlock_irqrestore(&my_spin_lock,flag); //释放自旋锁
return value;
}
irqreturn_t buttons_interrupt(int irq, void *dev_id)
{
deal_key_value_excl((unsigned int)dev_id);
//__debug("in eint function...\n");
return IRQ_HANDLED;
}
static void key_io_port_init(void)
{
unsigned long reg_val;
reg_val = readl(GPH3CON);
reg_val &= ~((0x0f<<0) | (0x0f<<4));
reg_val |= ((0x01<<0) | (0x01<<4));
writel(reg_val, GPH3CON);
reg_val = readl(GPH3DAT);
reg_val &= ~((0x01<<0) | (0x01<<1));
writel(reg_val, GPH3DAT);
reg_val = readl(GPH2UP);
reg_val &= ~(0x03<<8);
reg_val |= 0x02<<8;
writel(reg_val, GPH2UP);
}
static ssize_t key_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
int key_num;
int cpy_len;
int retval;
key_num = deal_key_value_excl(current->pid);
cpy_len = min(sizeof(key_num), count);
retval = copy_to_user(buf, &key_num, cpy_len);
return (cpy_len - retval);
}
/* Driver Operation structure */
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.read = key_read,
};
static int __init key_eint_init(void)
{
int retval;
key_io_port_init();
spin_lock_init(&my_spin_lock);
//__debug("in key_eint_init\n");
retval = set_irq_type(IRQ_EINT(20),IRQ_TYPE_EDGE_FALLING);
if(retval){
err("IRQ_EINT20 set irq type failed");
goto error;
}
retval = request_irq(IRQ_EINT(20), buttons_interrupt, IRQF_DISABLED,
"KEY1", (void *)EINT_DEVICE_ID);
if(retval){
err("request eint20 failed");
goto error;
}
/* Driver register */
major = register_chrdev(major, DRIVER_NAME, &key_fops);
if(major < 0){
err("register char device fail");
retval = major;
goto error_register;
}
key_class=class_create(THIS_MODULE,DRIVER_NAME);
if(IS_ERR(key_class)){
err("class create failed!");
retval = PTR_ERR(key_class);
goto error_class;
}
key_device=device_create(key_class,NULL, MKDEV(major, minor), NULL,DRIVER_NAME);
if(IS_ERR(key_device)){
err("device create failed!");
retval = PTR_ERR(key_device);
goto error_device;
}
__debug("register myDriver OK! Major = %d\n", major);
return 0;
error_device:
class_destroy(key_class);
error_class:
unregister_chrdev(major, DRIVER_NAME);
error_register:
free_irq(IRQ_EINT(20), (void *)EINT_DEVICE_ID);
error:
return retval;
}
static void __exit key_eint_exit(void)
{
//__debug("in key_eint_exit\n");
free_irq(IRQ_EINT(20), (void *)EINT_DEVICE_ID);
unregister_chrdev(major, DRIVER_NAME);
device_destroy(key_class,MKDEV(major, minor));
class_destroy(key_class);
return;
}
module_init(key_eint_init);
module_exit(key_eint_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Eric");
应用层:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
/*Linux内核是抢占式内核,在一个系统调用未结束前,
另一个系统调用也可以进入进程上下文,访问同一缓冲区*/
int main(void)
{
int status;
pid_t pid;
//打开文件raceStation
int fd_driver;
if((fd_driver = open("/dev/key_eint_race", O_RDWR)) < 0){
printf("file open error\n");
exit(1);
}
//创建子进程
if((pid = fork()) < 0){
perror("fork:");
exit(1);
}
else if(pid == 0){ //判断如果是子进程
int num;
while(1){
read(fd_driver,&num,sizeof(num));
printf("the num value is <son-%d>: %d\n",getpid(), num);
// usleep(50*1000);
}
close(fd_driver);
}else{ //判断如果是父进程
int num;
while(1){
read(fd_driver,&num,sizeof(num));
printf("the num value is <father-%d>: %d\n",getpid(), num);
// usleep(50*1000);
}
pid = wait(&status);
close(fd_driver);
}
}