在看Linux内核代码时,经常会遇到各种锁(lock)的使用。对于像spin_lock_irq
/spin_lock_irqsave
的区别感到困惑,每次都要重新查一下资料。遂决定写一篇文章记录下内核中使用到的锁,以及使用的场景。
与应用中的锁类似,内核中的锁也只是为了保护某个内核数据结构或者内存区域在多个执行路径时不被破坏,确保数据的一致性。Linux内核作为应用层服务的提供者,一方面要为应用提供系统调用接口(system call
),代表用户进程执行任务,即process context;同时与硬件直接交互,要响应硬件中断的请求,处理诸如网卡数据/串口数据等请求,即Interrupt Context. 内核就是在进程上下文/中断上下文直接来回切换,执行相应的任务请求。这就自然产生了数据的并发访问,产生了竞争条件(race condition
)。另一方面,目前大多数的系统都是多核CPU的,多个CPU同时访问内核数据也同样会产生竞争条件。
简单来说,内核中锁要做的事情就是确保临界区<critical section
>始终只有一个执行路径,就是说在有锁保护的情况下,临界区的执行不会被其他执行路径中断; 接下来,就分别看一看内核中常用的几个锁保护机制<对应的代码实现在/kernel/locking/
>:
- semaphore(信号量)
- Spin Lock(自旋锁)
- Mutex(互斥锁)
- Atomic(原子操作)
Semaphore(信号量)
semaphore
)是很常见的同步资源访问的方法,可以用于多个资源的访问控制;一般用于多个内核路径试图控制某个数据的并发访问,内核中对应的头文件在linux/semaphore.h
:
1 |
|
上述中count
就是需要同步访问的资源个数,一般在内核中都设置为1
,即等同于互斥锁。semaphore
有两个常用的操作方法:
down
: 获取锁,读应可用的资源减少,通常获取锁会将阻塞所执行的路径,让任务进入等待状态; 内核实现了好几种方法供调用:down
: 如果锁已被持有,则执行任务会被阻塞,在内核中不推荐使用该方法down_interruptible
: 允许获取锁时被中断,在接收到中断信号后,返回中断错误-EINTR
down_killable
: 执行任务阻塞时如果发生错误,则被中断返回-EINTR
down_trylock
: 尝试获取锁,如果已被占用,则直接返回,该方法支持在中断上下文中使用down_timeout
: 设置一个超时时间,超过该等待时间未获取到锁则返回-ETIME
up
: 释放锁,可以在任何执行路径执行该方法,即使未执行过down
也可以进行锁的释放
互斥锁的使用与semaphore
比较类似,具体的使用可以参考源码kernel/mutex.c
.
spin lock(自旋锁)
spin lock
是内核中最常用的同步方法,通常用于多个CPU执行路径尝试访问同一个内存数据时的同步并且执行任务不能休眠的场景。与其他如semaphore
不同的是,spin lock
不会让锁等待者进入休眠状态,而是执行一个简单的循环等待,如果此时锁被释放,则会尝试获取锁,这样就避免了上下文切换,从而提升效率。一般如果锁等待的时间如果超过系统上下文切换的时间,使用spin lock
则会较少任务的等待时间,改善系统性能。
除此之外,在某些特殊的场景比如在中断上下文与内核执行路径上共享数据时,就不能使用如semaphore
这类会时执行任务休眠的同步锁,因为内核一旦在处理中断时,发生进程调度,则可能发生中断无法被处理的情况。同样地,在处理中断时,也不能使用spin lock
以防止类似的情况;因此,在中断处理上下文中,使用spin lock
时要将本地中断禁止。另外,在可能发生内核抢占(kernel preemption
)的时候,如果被抢占任务执有spin lock
,就可能导致该锁一直未被释放。总结来说,使用spin lock
要注意如下几个原则:
- 内核抢占应该被禁止,以防出现竞争条件
- 本地中断需要被禁止,防止中断无法处理的情况
- 持有锁的时间越短越好,避免引起性能问题
跟上述几个场景对应,Linux中的spin lock
提供了好几个函数来实现不同场景下的同步<linux/spinlock.h
>:
spin_lock
: 获取锁,如果锁被持有了,则等待spin_lock_bh
: 获取锁,禁止了软中断/本地中断,但可以响应物理中断spin_lock_irq
: 获取锁时打开中断spin_lock_irqsave
: 获取锁时禁止本地中断
上述几个函数的实现都在kernel/locking/spinlock.c
中,如果不希望获取锁失败时等待,则可以通过spin_trylock*
来实现。