nginx 基于原子操作、信号量以及文件锁实现了一个简单高效的互斥锁,当多个 worker 进程之间需要互斥操作时都会用到。下面来看下 nginx 是如何实现它的。
原子操作
在实现互斥锁时用到了原子操作,先来了解一下 nginx 下提供的两个原子操作相关的函数:
|
|
第一个函数是一个 CAS
操作,首先它比较 lock
地址处的变量是否等于 old
, 如果相等,就把 lock
地址处的变量设为 set
变返回成功,否则返回失败。注意上述过程是作为一个原子一起进行的,不会被打断。 用代码可以描述如下:
|
|
第二个函数是读取 value
地址处的变量,并将其与 add
相加的结果再写入 *lock
,然后返回原来 *lock
的值,这些操作也是作为一个整体完成的,不会被打断。用代码可描述如下:
|
|
nginx 在实现这两个函数时会首先判断有没有支持原子操作的库,如果有,则直接使用库提供的原子操作实现,如果没有,则会使用汇编语言自己实现。下面以 x86
平台下实现 ngx_atomic_cmp_set 的汇编实现方式,实现主要使用了 cmpxchgq
指令,代码如下:
|
|
上面的代码采用 gcc 嵌入汇编方式来进行编写,了解了 cmpxchgq
指令后还是比较容易理解的。
锁结构体
首先 nginx 使用 ngx_shmtx_lock
结构体表示锁,它的各个成员变量如下:
|
|
上面的结构体定义使用了两个宏:NGX_HAVE_ATOMIC_OPS
与 NGX_HAVE_POSIX_SEM
,分别用来代表操作系统是否支持原子变量操作与信号量。根据这两个宏的取值,可以有3种不同的互斥锁实现:
- 不支持原子操作。
- 支持原子操作,但不支持信号量
- 支持原子操作,也支持信号量
第1种情况最简单,会直接使用文件锁来实现互斥锁,这时该结构体只有 fd
、 name
和 spin
三个字段,但 spin
字段是不起作用的。对于2和3两种情况 nginx 均会使用原子变量操作来实现一个自旋锁,其中 spin
表示自旋次数。它们两个的区别是:在支持信号量的情况下,如果自旋次数达到了上限而进程还未获取到锁,则进程会在信号量上阻塞等待,进入睡眠状态。不支持信号量的情况,则不会有这样的操作,而是通过调度器直接 「让出」cpu。 下面对这三种情况下锁的实现分别进行介绍。
基于文件锁实现的锁
锁的创建
首先通过下面的函数创建一个锁:
|
|
阻塞锁的获取
当进程需要进行阻塞加锁时,通过下面的函数进行:
|
|
上面函数主要的操作就是通过 ngx_lock_fd
来获取锁,它的实现如下:
|
|
它主要是通过 fcntl
函数来获取文件锁。
非阻塞锁的获取
上面获取锁的方式是阻塞式的,在获取不到锁时进程会阻塞,但有时候我们并不希望这样,而是不能获取锁时直接返回,nginx 通过这么函数来非阻塞的获取锁:
|
|
可以看到与阻塞版本相比,非阻塞版本最主要的变化 ngx_lock_fd
换成了 ngx_trylock_fd
, 它的实现如下:
|
|
锁的释放
上面说了如何加锁,接下来看一下如何释放锁,逻辑比较简单,直接放代码:
|
|
|
|
基于原子操作实现锁
上面谈到了在不支持原子操作时,nginx 如何使用文件锁来实现互斥锁。现在操作系统一般都支持原子操作,用它实现互斥锁效率会较文件锁的方式更高,这也是 nginx 默认选用该种方式实现锁的原因,下面看一下它是如何实现的。
锁的创建
与上面一样,我们还是先看是如何创建锁的:
|
|
该函数的 addr
指针变量指向进行原子操作用到的原子变量,它的类型如下:
|
|
由于锁是多个进程之间共享的, 所以 addr
指向的内存都是在共享内存进行分配的。
阻塞锁的获取
与文件锁实现的互斥锁一样,依然有阻塞和非阻塞类型,下面首先来看下阻塞锁的实现,相比于文件锁实现的方式要复杂很多:
|
|
上面代码的实现流程通过注释已经描述的很清楚了,再强调一点就是,使用信号量与否的区别就在于获取不到锁时进行的操作不同,如果使用信号量,则会在信号量上阻塞,进程进入睡眠状态。而不使用信号量,则是暂时「让出」cpu,进程并不会进入睡眠状态,这会减少内核态与用户态度切换带来的开销,所以往往性能更好,因此在 nginx 中使用锁时一般不使用信号量,比如负载均衡均衡锁的初始化方式如下:
|
|
将spin值设为-1,表示不使用信号量。
非阻塞锁的获取
非阻塞锁的代码就比较简单了,因为是非阻塞的,所以在获取不到锁时不需要考虑进程是否需要睡眠,也就不需要使用信号量,实现如下:
|
|
锁的释放
释放锁时,主要操作是将原子变量设为0,如果使用信号量,则可能还需要唤醒在信号量上等候的进程:
|
|
其中 ngx_shmtx_wakeup
的实现如下:
|
|
锁的销毁
因为锁的销毁代码比较简单,就不分开进行说明了。对于基于文件锁实现的互斥锁在销毁时需要关闭打开的文件。对于基于原子变量实现的锁,如果支持信号量,则需要销毁创建的信号量,代码分别入下:
基于文件锁实现的锁的销毁:
|
|
基于原子操作实现的锁的销毁:
|
|