前言
postgresql 的架构是基于多进程的,进程之间往往存在着数据共享,比如缓存读写。这时 postgresql 需要提高一种锁机制来支持并发访问,但是 linux 只实现了线程间的读写锁,所以 postgresql 自己实现进程间的读写锁,它主要利用了信号量和自旋锁来实现。
进程结构
每个进程都对应一个独立 PGPROC 实例,它包含了多个成员,下面只列出与锁相关的
struct PGPROC
{
BackendId backendId; /* This backend's backend ID (if assigned) */
PGSemaphore sem; // 信号量
// 正在等待的lwLock
bool lwWaiting; // 是否在等待lwlock
uint8 lwWaitMode; // lwlock加锁类型,有读锁和写锁
proclist_node lwWaitLink; // 位于等待列表中的位置
}
PGPROC 实例存储在进程共享内存里,在创建进程时会被初始化。这里需要提醒下,信号量 sem 的初始值为0,属于无名信号量。
锁结构
LWLock结构体表示读写锁,结构如下:
typedef struct LWLock
{
uint16 tranche; // tranche id,表示用于哪个方面
pg_atomic_uint32 state; // 状态值,包含多个标记位
proclist_head waiters; /* 等待进程链表 */
} LWLock;
需要介绍下 state 字段的标记位
#define LW_VAL_SHARED 1 // 1~23位表示读锁的数量
#define LW_VAL_EXCLUSIVE ((uint32) 1 << 24) // 是否写锁已经被占用
#define LW_FLAG_LOCKED ((uint32) 1 << 28) // 锁标记位,用来保护进程列表的并发操作
#define LW_FLAG_HAS_WAITERS ((uint32) 1 << 30) // 是否有进程在等待,用于快速判断等待列表是否为空
#define LW_FLAG_RELEASE_OK ((uint32) 1 << 29) // 是否可以执行唤醒操作(目前我还没弄懂为什么需要这个标记位)
加锁原理
LWLockAcquire函数负责整个加过程,这里将过程分为下面四步:
- 尝试获取锁,如果获取成功,会直接返回,否则执行第二步。
- 将自身进程添加到等待队列后,还会进行一次尝试获取锁,因为有可能在刚加入队列之前,锁恰好被释放。如果获取锁成功,则将自身从队列删除并且直接返回,否则执行第三步。
- 通过信号量阻塞,等待其他进程唤醒。
- 当因为锁释放被唤醒之后(该进程已经被唤醒进程从等待队列里删除了),会回到第一步
从上面可以看出来,这里采用的是非公平锁机制,也就是谁的速度快,谁就能获取到锁,没有先来先获取的顺序。
尝试获取锁
static bool LWLockAttemptLock(LWLock *lock, LWLockMode mode);
尝试通过 CAS 操作,设置LW_VAL_EXCLUSIVE或LW_VAL_SHAR

最低0.47元/天 解锁文章
1151

被折叠的 条评论
为什么被折叠?



