锁从功能上划分主要有互斥锁和读写锁。
不主动释放CPU的锁是spinlock,可以用于竞争较少并且临界区较小的场景,也可以用于中断上下文中。
锁一般用信号量、原子操作(cas)或文件锁来实现。
nginx中实现了互斥锁和读写锁以及spinlock。
互斥锁
加锁:
lock是一个原子变量,0表示没有上锁,ngx_pid表示被对应的进程加锁,如果当前被其它进程加锁,说明其它进程正在执行临界区内的代码,不会马上释放锁,所以会根据spin一会儿再尝试加锁,如果不spin而一直不断尝试访问共享变量将会增加核间通信的负担,ngx_cpu_pause()防止循环代码被优化。
支持posix信号量的情况下,如果spin几次都没有加锁成功则使用信号量进行排队,wait表示排队的个数。
循环调用sem_wait(&mtx->sem) 将进程阻塞等待其它进程释放锁,其它进程释放锁时会检查是否有进程进行wait,如果有则通过信号量wakeup正在wait的一个进程,用信号量避免一直占用CPU。
解锁:
解锁就是将lock赋值为0。然后会调用ngx_shmtx_wakeup(mtx)函数。
wakeup函数检查当前有没有进程在排队,如果有排队的进程,则通过sem_post(&mtx->sem)激活一个排队的进程。
这个for循环是一个小技巧,先将wait共享变量保存在本地局部变量中,然后对wait进行检查,最后再通过cas修改wait共享变量,如果在这中间wait变量被其它进程修改,那么本次cas就是失败,这样就可以保证将wait检查和wait修改两个步骤原子执行,这也是乐观锁的实现。
读写锁
nginx实现的读写锁对写锁不太友好,可能会有写锁饥饿的问题。
#define NGX_RWLOCK_WLOCK ((ngx_atomic_uint_t) -1) 定义写锁宏
加写锁:
没有锁的时候加写锁。
加读锁:
加读锁的时候依然用到了上面的技巧,如果没有加写锁,则加读共享锁。
解锁:
先解写锁,再解读锁。
spinlock
加锁:
spinlock加锁居然也会调用yield,说明这个spinlock目前只能用于进程上下文。
解锁:
#define ngx_unlock(lock) *(lock) = 0
这个地方没有指明内存序会不会有问题呢?我理解其它进程会在本进程解锁的一段时间之后才会发现这次解锁,会不会造成饥饿?待研究。
读写锁(优先写锁)
加写锁:
如果当前无锁,则加写锁,如果当前被上锁,则通过信号量等待。
加读锁:
如果当前不是写锁并且队列中没有等待的写锁,则加读锁,否则自旋加读锁。
解锁:
解写锁,然后weakup等待的写锁。
解读锁,如果读锁全部完成,然后weakup等待的写锁。
weakup函数不变,如果wait不为0,则激活一个写锁。不知道是不是正确啊,哈哈
也看到用两个互斥锁实现读写锁的例子,仅使用互斥锁实现读写锁,有才啊
网友评论