转载地址:http://blog.csdn.net/dasgk/article/details/41542589
nginx中使用的锁是自己来实现的,这里锁的实现分为两种情况,一种是支持原子操作的情况,也就是由NGX_HAVE_ATOMIC_OPS这个宏来进行控制的,一种是不支持原子操作,这是是使用文件锁来实现。
首先我们要知道在用户空间进程间锁实现的原理,起始原理很简单,就是能弄一个让所有进程共享的东西,比如mmap的内存,比如文件,然后通过这个东西来控制进程的互斥。
说起来锁很简单,就是共享一个变量,然后通过设置这个变量来控制进程的行为。
我们先来看核心的数据结构,也就是说用来控制进程的互斥的东西。
这个数据结构可以看到和我上面讲得一样,那就是通过宏来分成两种。
1 如果支持原子操作,则我们可以直接使用mmap,然后lock就保存mmap的内存区域的地址
2 如果不支持原子操作,则我们使用文件锁来实现,这里fd表示进程间共享的文件句柄,name表示文件名。
接着来看代码,先来看支持原子操作的情况下的实现方式。这里要注意下,下面的函数基本都会有两个实现,一个是支持原子操作,一个是不支持的,我这里全部都是分开来分析的。
先来看初始化,初始化代码在ngx_event_module_init中。
下面这段代码是设置将要设置的共享区域的大小,这里cl的大小最好是要大于或者等于cache line。
通过代码可以看到这里将会有3个区域被所有进程共享,其中我们的锁将会用到的是第一个。
下面这段代码是初始化对应的共享内存区域。然后保存对应的互斥体指针。
下面我们来看ngx_shmtx_create的实现。
可以看到如果支持原子操作的话,非常简单,就是将共享内存的地址付给loc这个域。
然后来看nginx中如何来获得锁,以及释放锁。
我们先来看获得锁。
这里nginx分为两个函数,一个是trylock,它是非阻塞的,也就是说它会尝试的获得锁,如果没有获得的话,它会直接返回错误。
而第二个是lock,它也会尝试获得锁,而当没有获得他不会立即返回,而是开始进入循环然后不停的去获得锁,知道获得。不过nginx这里还有用到一个技巧,就是每次都会让当前的进程放到cpu的运行队列的最后一位,也就是自动放弃cpu。
先来看trylock
这个很简单,首先判断lock是否为0,为0的话表示可以获得锁,因此我们就调用ngx_atomic_cmp_set去获得锁,如果获得成功就会返回1,负责为0.
接下来详细描述下ngx_atomic_cmp_set,这里这个操作是一个原子操作,这是因为由于我们要进行比较+赋值两个操作,如果不是原子操作的话,有可能在比较之后被其他进程所抢占,此时再赋值的话就会有问题了,因此这里就必须是一个原子操作。
我们来看这个函数的实现,如果系统库不支持这个指令的话,nginx自己还用汇编实现了一个,其实实现也很简单,比如x86的话有一个cmpxchgl的指令,就是做这个的。
先来看如果系统库支持的情况,此时直接调用OSAtomicCompareAndSwap32Barrier。
来看函数的原型:
然后这个函数翻译成伪码的话就是这个:
这个代码就不解释了,很浅显易懂。
因此上面的trylock的代码: 的意思就是如果lock的值是0的话,就把lock的值修改为当前的进程id,否则返回失败。
然后来看这个的汇编实现,这里nginx实现了多个平台的比如x86,sparc,ppc.
我们来看x86的:
具体的这些指令和锁可以去看intel的相关手册。
接下来来看lock的实现,lock最终会调用ngx_spinlock,因此下面我要主要来分析这个函数。
我们来看spinklock,必须支持原子指令,才会有这个函数,这里nginx采用宏来控制的.
这里和trylock的处理差不多,都是利用原子指令来实现的,只不过这里如果无法获得锁,则会继续等待。
我们来看代码的实现:
通过上面的代码可以看到spin lock实现的很简单,就是一个如果无法获得锁,就进入忙等的过程,不过这里nginx还多加了一个处理,就是如果忙等太长,就放弃cpu,直到下次任务再次占有cpu。
接下来来看下PAUSE指令,这条指令主要的功能就是告诉cpu,我现在是一个spin-wait loop,然后cpu就不会因为害怕循环退出时,内存的乱序而需要处理,所引起的效率损失问题。
下面就是intel手册的解释:
引用
Improves the performance of spin-wait loops. When executing a “spin-wait loop,” a
Pentium 4 or Intel Xeon processor suffers a severe performance penalty when exiting
the loop because it detects a possible memory order violation. The PAUSE instruction
provides a hint to the processor that the code sequence is a spin-wait loop. The
processor uses this hint to avoid the memory order violation in most situations,
which greatly improves processor performance. For this reason, it is recommended
that a PAUSE instruction be placed in all spin-wait loops.
内核的spin lock也有用到这条指令的。
接下来就是unlokck。unlock比较简单,就是和当前进程id比较,如果相等,就把lock改为0,说明放弃这个锁。
然后就是不支持原子操作的情况,此时使用文件锁来实现的,这里就不介绍这种实现了,基本原来和上面的差不多,想要了解的,可以去看nginx的相关代码。
接下来我们来看nginx如何利用lock来控制子进程的负载均衡以及惊群。
先来大概解释下这两个概念。
负载均衡是为了解决有可能一个进程处理了多个连接,因此就需要让多个进程更平均的处理连接。
惊群也就是当我们多个进程阻塞在epoll这类调用的时候,当有数据可读的时候,多个进程会被同时唤醒,此时如果去accept的话,只能有一个进程accept到句柄。
在看代码之前,我们先来看ngx_use_accept_mutex这个变量,如果有这个变量,说明nginx有必要使用accept互斥体,这个变量的初始化在ngx_event_process_init中。
这里还有两个变量,一个是ngx_accept_mutex_held,一个是ngx_accept_mutex_delay,其中前一个表示当前是否已经持有锁,后一个表示,当获得锁失败后,再次去请求锁的间隔时间,这个时间可以看到可以在配置文件中设置的。
这里还有一个变量是ngx_accept_disabled,这个变量是一个阈值,如果大于0,说明当前的进程处理的连接过多。
下面就是这个值的初始化,可以看到初始值是全部连接的7/8(注意是负值0.
然后来看ngx_process_events_and_timers中的处理。
然后先来看NGX_POST_EVENTS标记,设置了这个标记就说明当socket有数据被唤醒时,我们并不会马上accept或者说读取,而是将这个事件保存起来,然后当我们释放锁之后,才会进行accept或者读取这个句柄。
而如果没有设置NGX_POST_EVENTS标记的话,nginx会立即accept或者读取句柄。
然后是定时器,这里如果nginx没有获得锁,并不会马上再去获得锁,而是设置定时器,然后在epoll休眠(如果没有其他的东西唤醒).此时如果有连接到达,当前休眠进程会被提前唤醒,然后立即accept。否则,休眠 ngx_accept_mutex_delay时间,然后继续try lock.
最后是核心的一个函数,那就是ngx_trylock_accept_mutex。这个函数用来尝试获得accept mutex.
这里可以看到大部分情况下,每次只会有一个进程在监听listen句柄,而只有当ngx_accept_disabled大于0的情况下,才会出现一定程度的惊群。
而nginx中,由于锁的控制(以及获得锁的定时器),每个进程都能相对公平的accept句柄,也就是比较好的解决了子进程负载均衡。