Nginx原子操作及自旋锁实现

855 阅读5分钟

Nginx原子操作

执行原子操作的变量只有整形

这两种整型都使用了volatile关键字告诉C编译器不要做优化

nginx原子操作提供的2个方法

  • ngx_atomic_cmp_set
入参:
ngx_auomic_t*lock,
ngx_atomic_unit_t old,
ngx_atomic_unit set

compare & set
  • ngx_atomic_fetch_add
入参:
ngx_atomic_t*value,
ngx_atomic_int_t add 

x86的SMP多核架构下的原子操作

当无法实现原子操作时
就只能用volatile 关键字在C语言级别上模拟原子操作
目前绝大多数体系架构都是支持原子操作的

Nginx在源代码中实现对整型的原子操作
需通过内联汇编语言直接操作硬件才能做到

使用GCC编译器在C语言中嵌入汇编语言的方式使用__asm__关键字

__asm__volatile(汇编语句部分)
:输出部分
:输入部分
:破坏描述部分

volatile关键字用于限制GCC编译器对这段代码做优化

GCC如何内联汇编语言

汇编语句

引号中所包含的汇编语句可以直接用占位符%来引用C语言中的变量 
(最多10个,%0~%9)

介绍个汇编语句
  • cmpxchgl r,[m]

首先会用m比较eax寄存器中的值
如果相等 则把m的值设为r,zf标志位设为1
否则将zf标志位设为0,寄存器中的值设为m

输出部分

将寄存器中的值设置为C语言变量中

输入部分

将C语言变量设置到寄存器中

破坏描述部分

通知编译器使用了哪些寄存器、内存

ngx_atomic_cmp_set

从内存中获取lock变量(不使用寄存器)
把old变量写入eax寄存器
把set变量写入通用寄存器

首先锁住总线防止多核的并发执行
接着判断原子变量oldold值是否相等
若相等 则把lock值设为set 同时res为1
若不相等 则设res为0 

自旋锁

每当内核调度到这个进程执行时
就持续检查是否可以获得到锁
在拿不到锁时
这个进程的代码将会一直在自旋锁代码处执行
直到其他进程释放了锁且当前进程获得到锁
代码才会继续向下执行

适用场景

自旋锁主要为多处理器操作系统而设置
解决的共享资源保护场景是进程使用锁的时间非常短
(如果太久 会占用大量的CPU资源)

如果使用锁的进程不希望自己进入睡眠状态
特别它处理的是非常核心的事件时
这时应该使用自旋锁
大部分情况下Nginx的worker进程最好都不要进入睡眠状态
因为它非常繁忙
这个进程的epoll上可能会有十万甚至百万的TCP连接等待着处理
进程一旦睡眠后必须等待其他时间的唤醒
这中间极其频繁的进程间切换带来的负载消耗可能无法让用户接受

注意

自选锁对于单处理器操作系统来说一样有效
不进入睡眠状态并不意味着其他可执行状态的进程得不到执行
Linux内核中对于每个处理器都有一个运行队列
自选锁可以仅仅调整当前进程在运行队列中的顺序
或者调整进程的时间片
这都会为当前处理器上的其他进程提供被调度的机会
以使得锁被其他进程释放

Nginx基于原子操作的自旋锁ngx_spinlock的实现

入参:

a、

lock是原子变量表达的锁
值为0表示锁是被释放的
值不为0表示锁已经被某个进程持有了

b、value参数表示希望当锁没有被任何进程持有时
(也就是lock值为0)把lock值设为value表示当前进程持有了

c、spin参数表示在多处理器系统内
当ngx_spinlock方法没有拿到锁时
当前进程在内核的一次调度中
该方法等待其他处理器释放锁的时间

ngx_spinlock实现逻辑

不要立刻让出CPU

在多处理器下
更好的做法是当前进程不要立刻让出正在使用的CPU处理器
而是等待一段时间
看看其他处理器上的进程是否会释放锁
这会减少进程间切换的次数

检查lock是否释放的频率越来越小

随着等待的次数越来越多
实际去检查lock是否释放的频率会越来越小
因为检查lock值会更消耗CPU
而执行ngx_cpu_pause对于CPU能耗很节省的

nginx_cpu_pause

nginx_cpu_pause是在许多架构体系中专门为了自选锁而提供的命令
它会告诉CPU现在处于自选锁等待状态
通常一些CPU会将自己置于节能状态 降低功耗
"注意"在执行ngx_cpu_pause后
当前进程没有让出正使用的处理器

ngx_sched_yield

当前进程仍然处于可执行状态
但暂时让出处理器
使得处理器优先调度其他可执行状态的进程
在进程被内核再次调度时 
在for循环代码中可以期望其他进程释放锁