系统相关
首页 > 系统相关> > linux驱动移植-进程同步之自旋锁

linux驱动移植-进程同步之自旋锁

作者:互联网

一、自旋锁(spinlock)

1.1  什么是自旋锁

自旋锁(spinlock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。

为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(Test-AndSet)某个内存变量。由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。

当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。

驱动程序在持有自旋锁时绝对不能进入睡眠,而在拥有信号量时就可以:自旋锁禁止内核抢占;而信号量不禁止内核抢占。基于这个原因:

当然,自旋锁的睡眠的情况包含考虑多核CPU和中断的因素。自旋锁睡眠时,只是当前CPU的睡眠以及当前CPU的禁止内核抢占,所以,如果存在多个CPU,那么其他活动的CPU可以继续运行使操作系统功能正常,并有可能完成相应工作而唤醒睡眠了的自旋锁,从而没有造成系统死机;自旋锁睡眠时,如果允许中断处理,那么中断的代码是可以正常运行的,但是中断通常不会唤醒睡眠的自旋锁,因此系统仍然运行不正常。

1.2 自旋锁具有的特点

1.3 自旋锁禁止内核抢占

自旋锁禁止内核抢占这是为什么呢?

为了防止系统进入死锁状态,需要在真正上锁前,调用preempt_disable来关闭内核抢占。

1.4 自旋锁的使用

定义自旋锁:

spinlock_t lock;

初始化自旋锁:

spin_lock_init(&lock);

获得自旋锁:

spin_lock(&lock);

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

spin_trylock(&lock)

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

释放自旋锁:

spin_unlock(&lock);

该函数释放自旋锁lock, 它与spin_trylock或spin_lock配对使用。

1.5 中断情况下自旋锁的使用

尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的内核抢占打扰,但是得到锁的代码路径在执行临界区的时候, 还可能受到中断和底半部的影响。为了防止这种影响,所以与中断屏蔽联系使用。

spin_lock /spin_unlock是自旋锁机制的基础,它们和:

结合就形成了整套自旋锁机制,关系如下:

spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()

在多核编程的时候, 如果进程和中断可能访问同一片临界资源,我们一般需要在进程上下文中调用spin_lock_irqsave /spin_unlock_irqrestore,在中断上下文中调用spin_lock/spin_unlock。

例如,在CPU0上,无论是进程上下文,还是中断上下文获得了自旋锁,此后,如果CPU1无论是进程上下文, 还是中断上下文, 想获得同一自旋锁,都必须忙等待,这避免一切核间并发的可能性。同时,由于每个核的进程上下文持有锁的时候用的是spin_lock_irqsave,所以该核上的中断是不可能进入的,这避免了核内并发的可能性。

1.6 

二、自旋锁的源码实现

2.1 spinlock_t结构头

spinlonk_t结构体定义位于include/linux/spinlock_types.h文件中:

typedef struct spinlock {
        union {
                struct raw_spinlock rlock;
        };
} spinlock_t;

在该文件,定位到struct raw_spinlock结构体:

typedef struct raw_spinlock {
        arch_spinlock_t raw_lock;
} raw_spinlock_t;

最后定位到arch_spinlock_t,该函数也是和硬件体系相关的函数,位于arch/arm/include/asm/spinlock_types.h:

typedef struct {
        union {
                u32 slock;
                struct __raw_tickets {
#ifdef __ARMEB__                        // 大端 高字节保存在低位
                        u16 next;
                        u16 owner;
#else
                        u16 owner;
                        u16 next;
#endif
                } tickets;
        };
} arch_spinlock_t;

owner表示持有这个数字的进程可以获取自旋锁;

next表示如果后续再有进程请求获取这个自旋锁,就给它分配这个数字;

2.2 spin_lock_init

宏spin_lock_init位于include/linux/spinlock.h文件中:

#define spin_lock_init(_lock)                           \
do {                                                    \
        spinlock_check(_lock);                          \
        raw_spin_lock_init(&(_lock)->rlock);            \
} while (0)

在当前文件定位到宏raw_spin_lock_init:

# define raw_spin_lock_init(lock)                               \
        do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)
#endif

再次定位到宏__RAW_SPIN_LOCK_UNLOCKED,该宏位于include/linux/spinlock_types.h:

#define __RAW_SPIN_LOCK_INITIALIZER(lockname)   \
        {                                       \
        .raw_lock = __ARCH_SPIN_LOCK_UNLOCKED,  \
        SPIN_DEBUG_INIT(lockname)               \
        SPIN_DEP_MAP_INIT(lockname) }

#define __RAW_SPIN_LOCK_UNLOCKED(lockname)      \
        (raw_spinlock_t) __RAW_SPIN_LOCK_INITIALIZER(lockname)

这里使用__ARCH_SPIN_LOCK_UNLOCKED初始化结构体成员raw_lock,该宏位于arch/arm/include/asm/spinlock_types.h:

#define __ARCH_SPIN_LOCK_UNLOCKED       { { 0 } }

这样owner、next都被初始化为0。

2.3  spin_lock

我们再来看一下获取自旋锁宏spin_lock,位于include/linux/spinlock.h:

static __always_inline void spin_lock(spinlock_t *lock)
{
        raw_spin_lock(&lock->rlock);
}

定位到当前文件宏raw_spin_lock:

#define raw_spin_lock(lock)     _raw_spin_lock(lock)

_raw_spin_lock有两个实现:

先介绍include/linux/spinlock_api_up.h中的实现:

#define _raw_spin_lock(lock)                    __LOCK(lock)
#define ___LOCK(lock) \
  do { __acquire(lock); (void)(lock); } while (0)
#define __LOCK(lock) \
  do { preempt_disable(); ___LOCK(lock); } while (0)

这里___LOCK函数啥也没做,所以我们重点关注preempt_disable,这个函数是会禁止内核抢占。

然后再来看kernel/locking/spinlock.c中的实现:

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
        __raw_spin_lock(lock);
}

__raw_spin_lock定义在include/linux/spinlock_api_smp.h中:

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
        preempt_disable();
        spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
        LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

首先禁止内核抢占,然后执行spin_acquire,该函数位于include/linux/lockdep.h:

#define lock_acquire_exclusive(l, s, t, n, i)           lock_acquire(l, s, t, 0, 1, n, i)
#define spin_acquire(l, s, t, i)                lock_acquire_exclusive(l, s, t, NULL, i)
# define lock_acquire(l, s, t, r, c, n, i) do { } while (0)

可以看到这个函数啥也没做,我们最来到LOCK_CONTENDED,也是位于include/linux/lockdep.h:

#define LOCK_CONTENDED(_lock, try, lock)                        \
do {                                                            \
        if (!try(_lock)) {                                      \
                lock_contended(&(_lock)->dep_map, _RET_IP_);    \
                lock(_lock);                                    \
        }                                                       \
        lock_acquired(&(_lock)->dep_map, _RET_IP_);                     \
} while (0)

第三个参数为do_raw_spin_lock,位于kernel/locking/spinlock_debug.c:

/*
 * We are now relying on the NMI watchdog to detect lockup instead of doing
 * the detection here with an unfair lock which can cause problem of its own.
 */
void do_raw_spin_lock(raw_spinlock_t *lock)
{
        debug_spin_lock_before(lock);
        arch_spin_lock(&lock->raw_lock);
        mmiowb_spin_lock();
        debug_spin_lock_after(lock);
}

定位到arm体系架构代码,arch/arm/include/asm/spinlock.h:

/*
 * ARMv6 ticket-based spin-locking.
 *
 * A memory barrier is required after we get a lock, and before we
 * release it, because V6 CPUs are assumed to have weakly ordered
 * memory.
 */

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
        unsigned long tmp;
        u32 newval;
        arch_spinlock_t lockval;

        prefetchw(&lock->slock);
        __asm__ __volatile__(
"1:     ldrex   %0, [%3]\n"
"       add     %1, %0, %4\n"
"       strex   %2, %1, [%3]\n"
"       teq     %2, #0\n"
"       bne     1b"
        : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
        : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
        : "cc");

        while (lockval.tickets.next != lockval.tickets.owner) {
                wfe();
                lockval.tickets.owner = READ_ONCE(lock->tickets.owner);
        }

        smp_mb();
}

这里我们就不具体分析这个汇编代码了,这里汇编代码本质上还是利用CPU的独占访问指令实现对slock值的修改,大致介绍一下:

执行成功之后等价于执行如下指令:

lockval=lock->slock;   // 保存旧值
lock->slock += 1<<16;  // 修改后的新值 slock是一个union,由next和owner组成,等价于next++;
newlock = lock->slock;

然后再来看一下C代码:

举个例子假设有三个进程执行这段代码,lock->slock初始值为0:

 指令 进程1 进程2 进程3 影响

 

 ldrex R0, [R3]

 

设置独占标记  R0=[R3 ]

 

 add R0, R0, R4

 

 R0=R0+(1<<16)

 ldrex R0, [R3]

 

 

设置独占标记  R0=[R3 ]

4

    ldrex R0, [R3]

设置独占标记  R0=[R3 ]

5     add R0, R0, R4 R0=R0+(1<<16)

6

 add R0, R0, R4

 

 

 R0=R0+(1<<16)

7

 strex R2, R1, [R3]

 

 

执行成功,R1写回[R3],清除独占标记  

8

 

 strex R1, R0, [R3]

 

没有独占标记,执行失败

9

 teq R2, #0

 

 

 相等 R2=0

10

    strex R2, R1, [R3]

没有独占标记,执行失败

11

 

 teq R2, #0

 

不相等 R2=1

12

 

 b 1b

   跳转

13

    teq R2, #0 不相等 R2=1
14     b 1b  跳转
15 汇编执行完毕    

lockval=R1=0                        当前保存进程值

lock->slock=R3=1<<16        lock->tickets.next=1

16 lockval.tickets.next != lockval.tickets.owner    

条件不满足,获取spinlock,进入临界区

17   再次经历上面步骤,不过此时lock->slock初始值为1<<16    
18   ....    
19     ...  
20   汇编执行完毕  

lockval=R1=1<<16             当前保存进程值

lock->slock=R3=2<<16      lock->tickets.next=2

21     汇编执行完毕

lockval=R1=2<<16             当前保存进程值

lock->slock=R3=3<<16      lock->tickets.next=3

22   lockval.tickets.next != lockval.tickets.owner  

lockval=1<<16 ,条件成立 开始死等

23   lockval.tickets.owner = READ_ONCE(lock->tickets.owner)   lockval=1<<16      
24     lockval.tickets.next != lockval.tickets.owner lockval=2<<16 ,条件成立  开始死等
25     lockval.tickets.owner = READ_ONCE(lock->tickets.owner) lockval不变
26 spin_unlock     lock->slock=3<<16+1     lock->tickets.owner=1
27   lockval.tickets.next != lockval.tickets.owner   成立 开始死等
28   lockval.tickets.owner = READ_ONCE(lock->tickets.owner)   lockval=1<<16 +1     
    lockval.tickets.next != lockval.tickets.owner   条件不满足,获取spinlock,进入临界区

代码大致流程如下,三个进程同时修改lock->slock(联合体,或者说lock->tickets),这个变量是三个进程共享的:

这样保证了spinlock的唤醒机制是先到先唤醒,后到后唤醒,保证了公平性;

看完这个我们再来通俗解释一下自旋锁的实现:

三、自旋锁示例程序

 

3.1 使用注意事项

 

3.2 示例程序

 

修改信号量示例里面的驱动程序:

#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>

#define OK   (0)
#define ERROR  (-1)

/* 自旋锁 */
static spinlock_t lock;
static int count = 0;

int hello_open(struct inode *p, struct file *f)
{
    /* 获取自旋锁 */
    spin_lock(&lock);
    if(count >= 1){
        spin_unlock(&lock);        
        printk("device busy,hello_open failed");
        return ERROR;
    }
    count++;
    spin_unlock(&lock);        
    printk("hello_open\n");
    return 0;
}

ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk("hello_write\n");
    return 0;
}

ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk("hello_read\n");
    return 0;
}

int hello_close(struct inode *inode, struct file *file)
{
     /* 获取自旋锁 */
    spin_lock(&lock);
    count--;
    spin_unlock(&lock);        
    return 0;
}

struct file_operations hello_fops = {
    .owner   =   THIS_MODULE,
    .open    =   hello_open,
    .read    =   hello_read,
    .write   =   hello_write,
    .release =   hello_close,
};

dev_t devid;                      // 起始设备编号
struct cdev hello_cdev;          // 保存操作结构体的字符设备 
struct class *hello_cls;

int hello_init(void)
{
    
    /* 动态分配字符设备: (major,0) */
    if(OK == alloc_chrdev_region(&devid, 0, 1,"hello")){   // ls /proc/devices看到的名字
        printk("register_chrdev_region ok\n");
    }else {
        printk("register_chrdev_region error\n");
        return ERROR;
    }
    
     cdev_init(&hello_cdev, &hello_fops);
     cdev_add(&hello_cdev, devid, 1);


    /* 创建类,它会在sys目录下创建/sys/class/hello这个类  */
     hello_cls = class_create(THIS_MODULE, "hello");
     if(IS_ERR(hello_cls)){
         printk("can't create class\n");
         return ERROR;
     }
    /* 在/sys/class/hello下创建hellos设备,然后mdev通过这个自动创建/dev/hello这个设备节点 */
     device_create(hello_cls, NULL, devid, NULL, "hello"); 

     /* 初始化自旋锁 */
     spin_lock_init(&lock);
     return 0;
}

void __exit hello_exit(void)
{
    printk("hello driver exit\n");
    /* 注销类、以及类设备 /sys/class/hello会被移除*/
    device_destroy(hello_cls, devid);
    class_destroy(hello_cls);

    cdev_del(&hello_cdev);
    unregister_chrdev_region(devid, 1);
    return;
}


module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

参考文章

[1]ARM平台下独占访问指令LDREX和STREX的原理与使用详解

[2]七、Linux驱动之并发控制

[3]10.按键之互斥、阻塞机制(详解)

[4]深入分析Linux自旋锁【转】

标签:tickets,进程同步,lock,linux,hello,自旋,spin,spinlock
来源: https://www.cnblogs.com/zyly/p/15928982.html