1.基本作用
在前面我们介绍过互斥锁用于共享资源的互斥访问,但是互斥锁有一个缺点就是资源开销比较大。因为获取互斥锁和释放互斥锁过程中都需要进出内核,如果共享资源自身不是很大的话,比如可能就是修改一个变量的值,代码里也就几行,这个时候就可以考虑使用自旋锁来实现互斥访问。
自旋锁顾名思义,当锁已经被别人获取的时候,再次去获取锁的人会一直“自旋”,也就是一直在检测锁的状态直到锁被释放。自旋锁一般用在多核的情况下,比如现在有两个线程分别跑在两个核上,然后都要去访问共享资源,如下图所示。
cpu核0上运行线程A: 获取锁保护资源 count++; 释放锁 cpu核1上运行线程B: 获取锁保护资源 count--; 释放锁
这里的共享资源就是count这个变量,对这个变量的访问需要被保护起来。如果使用互斥锁来实现,假设线程A首先获取到锁,那么当线程B去获取锁时会被阻塞然后调度其他线程运行,直到线程A执行完count++然后释放锁,线程B得到机会被再次调度运行,然后执行count--。我们发现这里其实count++或者count--代码执行的时间是很短的,用互斥锁的话线程B被阻塞调度其他线程运行然后又调度回来执行,这中间花去了不少时间,得不偿失。这里就可以用自旋锁来实现互斥,线程B发现锁已经被线程A获取了,然后一直检测锁的状态直到锁被线程A释放,因为线程A占用锁的时间很短,所以线程B很快就可以获取到锁,这样就避免了调度来调度去的时间开销,提高了程序的性能。
2.自旋锁相关接口
以下接口只能用在驱动和内核中,如果想在应用层使用的话,可以使用posix中规定的自旋锁接口。
2.1初始化自旋锁
自旋锁使用之前,需要初始化,这是通过API_SpinInit 接口来实现的,如下所示。
LW_API INT API_SpinInit(spinlock_t *psl);
- psl:自旋锁变量地址。在SylixOS下,自旋锁类型是spinlock_t,使用之前需要进行初始化。
成功返回0,失败返回错误码。
2.2 获取自旋锁
当需要保护共享资源时,需要先申请自旋锁,这是通过API_SpinLock 接口实现的,如下所示。
LW_API INT API_SpinLock(spinlock_t *psl);
- psl:自旋锁变量地址。
成功返回0,失败返回错误码。此接口调用完不会改变cpu原有的中断状态,也就是如果在调用接口之前cpu的中断状态是开状态,调用完之后中断状态还是开状态。并且调用成功之后当前线程会被锁定在当前cpu核上执行,不会被调度到其他核上运行。
2.3 释放自旋锁
访问完共享资源就可以释放自旋锁了,这是通过API_SpinUnlock 接口实现的,如下所示。
LW_API INT API_SpinUnlock(spinlock_t *psl);
- psl:自旋锁变量地址。
这个接口是和API_SpinLock 接口成对使用的,成功返回0,失败返回错误码。并且在释放的时候会尝试进行一次调度,让其他线程有机会运行。
2.4 获取自旋锁同时关闭中断
有时候可能会有这么一种需求,在获取到自旋锁的同时需要将cpu中断状态关闭,这是通过API_SpinLockIrq 接口来实现的,如下所示。
LW_API INT API_SpinLockIrq(spinlock_t *psl, INTREG *iregInterLevel);
- psl:自旋锁变量地址。
- iregInterLevel:用来保存调用之前中断状态值的。
这个接口和API_SpinLock 接口作用类似,不同的是调用完之后cpu的中断状态是关闭的,也就是在当前cpu核上不会处理任何中断,直到中断被再次打开。成功返回0,失败返回错误码,并且同样调用成功之后当前线程会被锁定在当前cpu核上执行,不会被调度到其他核上运行。
2.5 释放自旋锁同时打开中断
和API_SpinLockIrq 接口相对应的释放自旋锁并打开中断的接口是API_SpinUnlockIrq ,如下所示。
LW_API INT API_SpinUnlockIrq(spinlock_t *psl, INTREG iregInterLevel);
- psl:自旋锁变量地址。
- iregInterLevel:之前保存的中断状态值。
成功返回0,失败返回错误码。并且在释放的时候会尝试进行一次调度,让其他线程有机会运行。
2.6 销毁自旋锁
自旋锁使用完毕之后需要进行销毁以回收资源,这是通过API_SpinDestory 接口实现的,如下所示。
LW_API INT API_SpinDestory(spinlock_t *psl);
- psl:自旋锁变量地址。
成功返回0,失败返回错误码。在SylixOS的实现中,这个接口基本是个空函数,并没有做什么实质的资源回收,但是为了代码的健壮性,建议还是要调这个接口,因为保不准以后哪个版本之后这个函数就有真正的回收资源的不同实现了。
3.接口使用示例
spinlock_t spin_lock; 1. 初始化自旋锁 API_SpinInit(&spin_lock); 2. 获取自旋锁 API_SpinLock(&spin_lock); 或者 INTREG flag; API_SpinLockIrq(&spin_lock, &flag); 3. 访问共享资源 4. 释放自旋锁 API_SpinUnlock(&spin_lock); 或者 API_SpinUnlockIrq(&spin_lock, flag); 5. 使用完毕后,销毁自旋锁 API_SpinDestory(&spin_lock);
关于自旋锁还有其他一些接口,具体可以参见内核源码SylixOS/kernel/include/k_api.h。
4.注意事项
- 禁止在上述的lock和unlock接口之间调用内核系统接口或者能引起阻塞和调度的接口,如果一定要用这些系统接口的话,可以使用LW_SPIN_LOCK_TASK 和LW_SPIN_UNLOCK_TASK 这两个接口,这两个接口同时也是posix中spinlock相关实现中使用的。
- 禁止在中断上下文中使用LW_SPIN_LOCK_TASK 和LW_SPIN_UNLOCK_TASK 这两个接口。
2023年7月3日 14:24 1F
LW_SPIN_LOCK_TASK 和LW_SPIN_UNLOCK_TASK 这两个接口与上述的lock和unlock还有别的区别吗?这俩之间既然允许使用系统接口,为什么不之间无脑用这两个宏呢
2023年8月8日 10:42 B1
@ yuwan 带task的我没咋用过,不过看名字应该是只能在线程上下文环境下使用的,而不带task的可以在中断中使用。用哪个接口看实际需求,是需要在线程和线程之间保护资源还是线程和中断之间保护资源,以及要不要关中断之类的。