程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

linux同步机制-自旋锁

一、自旋锁(spinlock)

1.1  什么是自旋锁

自旋锁(spinlock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。自旋锁和信号量的主要区别在于,如果进程没有获取到自旋锁,就一直循环在那里看是否该自旋锁的保持者已经释放了锁。

自旋锁的实现有多种:比如CAS和ticket spinlock;目前linux自选锁的实现采用了ticket spinlock,我们会在后面源码实现时介绍。

  • CAS:spinlock用一个整形变量表示,其初始值为1,表示available的状态。当一个CPU获得spinlock后,会将该变量的值设为0,之后其他CPU试图获取这个spinlock时,会一直等待,直到CPU A释放spinlock,并将该变量的值设为1。其比较和交换值是通过cas汇编指令实现;
  • ticket spinlock:这类似于你去银行柜台办理业务,假设当前银行只有一个柜台,你需要在自助机上获得一个排队号码(相当于一个ticket),然后当柜台叫到的号码与你手中的号码一致时,你将坐上柜台前面的椅子,此时柜台为你服务,这也是这种实现方式被称为"ticket spinlock"的原因;

1.2 自旋锁具有的特点

自旋锁可以用于多核系统(SMP),也可以用于单核系统(UP),自旋锁在实现的时候调用preempt_disable关闭了内核抢占。也就是说运行在一个CPU的代码使用spin_lock加锁之后,基于该CPU的内核抢占就被禁止了。因此会产生以下影响:

  • 在单核系统:只需要禁止内核抢占,等同于关闭了进程切换,从而就不存在进程同步的问题。由于禁止了内核抢占,如果进程获取自旋锁之后,在临界区中睡眠,将会导致其他进程都无法获取CPU而运行,从而不能唤醒睡眠的自旋锁,因此禁止在自旋锁中使用睡眠等函数(除了中断,但是中断通常不会唤醒睡眠的自旋锁);
  • 在多核系统:虽然禁止了当前CPU内核抢占,但是如果存在多个CPU,仍然存在多个CPU对自旋锁共享变量同时访问的问题,因此在多核系统除了关闭CPU内核抢占、还需要通过独占指令ldrex、strex实现共享变量的互斥访问;

自旋锁的特点有:

  • spinlock是一种死等的锁机制;
  • semaphore可以允许多个执行单元进入,spinlock不行,一次只能有一个执行单元获取锁并进入临界区,其他的执行单元都是在门口不停的死等;
  • 执行时间短,由于spinlock死等这种特性,如果临界区执行时间太长,那么不断的在临界区门口“死等”的那些执行单元会浪费CPU;
  • 由于在中断上下文中是不允许睡眠的,因此spinlock可以在中断上下文中适用;而信号量和互斥锁都会导致睡眠,无法在中断上下文中使用;

1.3 自旋锁禁止内核抢占

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

在一个打开CONFIG_PREEMT特性的Linux系统中,一个在内核态执行的进程也有可能被切换出处理器。

典型的比如当前进程Task A正在内核态执行某一系统调用时,发生了一个外部中断,当中断处理函数返回时,由于内核的可抢占性,此时将会出现一个调度点,如果CPU的就绪队列中出现一个比当前被中断进程优先级更高的进程Task B,那么被中断的进程Task A将会被换出处理器,即便此时他正运行在内核态,单核处理器上的这种因为内核可抢占性所导致的两个不同进程并发执行的情形,非常类似SMP系统上运行在不同处理器上的进程之间的并发,因此为了保护共享的资源不会受到破坏,必须在进入临界区之前关闭内核的可抢占性。

 

上图中展示了Linux三种内核抢占模型:

  • PREEMPT_NONE::不支持抢占,中断退出后,需要等到低优先级任务主动让出CPU才发生抢占切换;
  • PREEMPT_VOLUNTARY自愿抢占,代码中增加抢占点,在中断退出后遇到抢占点时进行抢占切换;
  • PREEMPT抢占,当中断退出后,如果遇到了更高优先级的任务,立即进行任务抢占;

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内的内核抢占打扰,但是得到锁的代码路径在执行临界区的时候, 还可能受到中断和底半部的影响。

这里以硬件中断为例,试想一下,假设一个CPU上的进程A持有了一个spinlock,发生中断后,该CPU转而执行对应的hardirq。如果该hardirq也试图去持有这个spinlock,那么将无法获取成功,由于中断上下文中禁止调度(只可以被其中断嵌套),导致hardirq无法退出。在hardirq主动退出之前,进程A是无法继续执行以释放spinlock的,最终将导致该CPU上的代码不能继续向前运行,形成死锁(dead lock)

为了防止这种影响,所以与中断屏蔽联系使用。spin_lock /spin_unlock是自旋锁机制的基础,它们和:

  • 关中断local_irq_disable/开中断local_irq_enable
  • 关底半部local_bh_disable/开底半部local_bh_enable
  • 关中断并保存状态字local_irq_save/开中断并恢复状态字local_irq_restore

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

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,所以该核上的中断是不可能进入的,这避免了核内并发的可能性。

二、自旋锁的源码实现

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   单核CPU;

  • 位于kernel/locking/spinlock.c  多核CPU;

2.4 _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,这个函数是会禁止内核抢占。如果不禁止内核抢占,那么实际上_raw_spin_lock就是什么也没做,就会出现在UP架构下因内核抢占导致两个不同的进程并发执行的问题。

2.5 _raw_spin_lock多核实现

然后再来看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值的修改,大致介绍一下:

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

日期姓名金额
2023-09-06*源19
2023-09-11*朝科88
2023-09-21*号5
2023-09-16*真60
2023-10-26*通9.9
2023-11-04*慎0.66
2023-11-24*恩0.01
2023-12-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
2024-09-06*强8.8
2024-09-09*波1
2024-09-10*口1
2024-09-10*波1
2024-09-12*波10
2024-09-18*明1.68
2024-09-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(917)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示