欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 资讯 > 【PGCCC】Postgresql LWLock 原理

【PGCCC】Postgresql LWLock 原理

2024/11/8 12:20:46 来源:https://blog.csdn.net/PGCCC/article/details/143586405  浏览:    关键词:【PGCCC】Postgresql LWLock 原理

前言

postgresql 的架构是基于多进程的,进程之间往往存在着数据共享,比如缓存读写。这时 postgresql 需要提高一种锁机制来支持并发访问,但是 linux 只实现了线程间的读写锁,所以 postgresql 自己实现进程间的读写锁,它主要利用了信号量和自旋锁来实现。

进程结构

每个进程都对应一个独立 PGPROC 实例,它包含了多个成员,下面只列出与锁相关的

struct PGPROC 
{BackendId	backendId;		/* This backend's backend ID (if assigned) */PGSemaphore sem;			// 信号量// 正在等待的lwLockbool		lwWaiting;		// 是否在等待lwlockuint8		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函数负责整个加过程,这里将过程分为下面四步:

  1. 尝试获取锁,如果获取成功,会直接返回,否则执行第二步。
  2. 将自身进程添加到等待队列后,还会进行一次尝试获取锁,因为有可能在刚加入队列之前,锁恰好被释放。如果获取锁成功,则将自身从队列删除并且直接返回,否则执行第三步。
  3. 通过信号量阻塞,等待其他进程唤醒。
  4. 当因为锁释放被唤醒之后(该进程已经被唤醒进程从等待队列里删除了),会回到第一步

从上面可以看出来,这里采用的是非公平锁机制,也就是谁的速度快,谁就能获取到锁,没有先来先获取的顺序。

尝试获取锁

static bool LWLockAttemptLock(LWLock *lock, LWLockMode mode);

尝试通过 CAS 操作,设置LW_VAL_EXCLUSIVE或LW_VAL_SHARED标记位。来获取锁。使用到的 CAS 操作函数如下:

// 如果当前ptr指向的值,等于expected指向的值,那么就会更改ptr指向的值为newval。
// 无论是否成功修改,当前ptr的最新值会保存到 expected。
// 返回true表示修改成功,返回false表示修改失败
bool pg_atomic_compare_exchange_u32(volatile pg_atomic_uint32 *ptr, uint32 *expected, uint32 newval);

当尝试获取写锁时,需要检查EXCLUSIVE和SHARED标记位,只有两者为零,才能成功获取。

当尝试获取读锁时,只需要检查EXCLUSIVE标记位。如果为零,就认为成功获取。

添加到等待队列

对于队列的操作,都需要使用自旋锁来设置LW_FLAG_LOCKED 标记位,才有权利操作队列。当设置成功后,会将自身添加到等待列表,然后更新自身进程对应PGPROC实例的 lwWaiting 和 lwWaitMode 成员,最后释放锁,清除标记位。

static void LWLockQueueSelf(LWLock *lock, LWLockMode mode);

当加入到队列后,还需要更新LW_FLAG_HAS_WAITERS标记位,表示有进程在等待。

等待信号量

postgresql 是多进程架构的,进程之间使用信号量来同步。当进程获取锁失败时,会将自身添加到等待队列里,然后通过信号量进行阻塞,直到被别的进程唤醒。对于 postgresql 的每个进程都有一个信号量,由PGPROC实例的 sem 成员表示。sem 信号量的初始值为0,它的数值代表着满足的条件数。

因为这个 sem 信号量会有多个用途,所以每次唤醒操作,并不一定是期望的条件发生,所以进程在被唤醒之后,需要一次条件检查。如果是因为其他条件唤醒,还需要记录被唤醒次数,在事后需要将执行相应次数的唤醒操作。

// 获取锁之前
extraWaits = 0
for (;;)
{// 执行信号量阻塞PGSemaphoreLock(proc->sem);// 当被唤醒之后,需要检查唤醒条件是不是因为锁释放,// 如果是锁释放的原因,proc->lwWaiting 会为falseif (!proc->lwWaiting)break;// 如果不是期望条件,需要记录次数extraWaits++;
}// 获取到锁之后
// 因为刚刚占有了其余条件的唤醒,所以现在需要进行补偿
while (extraWaits-- > 0)PGSemaphoreUnlock(proc->sem);

假设下面有一个进程A对应信号量A,它会被用于两个方面,条件A和条件B。

----------                          ------------                          ------------进程A    |    阻塞条件A ---->      |  信号量A   |   <----- 因为条件A唤醒   |   进程B   ||    阻塞条件B ---->      |           |   <----- 因为条件B唤醒   |           |
----------                          ------------                          ------------

进程A正在阻塞条件A,此时进程B因为条件B唤醒了进程A。但进程A发现并不是期望的条件所唤醒的,所以它会继续阻塞,直到满足条件A被唤醒,最后进程A还会执行一次唤醒操作。

离开等待队列

当获取到锁之后,如果自身在等待队列中,需要将其删除掉。如果之后队列为空,需要清除LW_FLAG_HAS_WAITERS标记位,当然这些操作都需要获取LW_FLAG_LOCKED锁。

源码

下面展示了LWLockAcquire的源码,描述了整个加锁过程

bool LWLockAcquire(LWLock *lock, LWLockMode mode);
{bool result = true;int	extraWaits = 0;for (;;){// 尝试获取锁,如果成功则直接返回bool mustwait = LWLockAttemptLock(lock, mode);if (!mustwait)break;// 步骤A,获取锁失败,则将自身添加到等待队列里LWLockQueueSelf(lock, mode);// 步骤C// 在添加到队列时,有可能锁刚好释放。因为这时还未添加到队列里,所以不会通知它。// 所以这里需要再次尝试获取锁mustwait = LWLockAttemptLock(lock, mode);// 如果刚好成功获取了别人释放的锁,那么需要将自身从等待队列中删除if (!mustwait){LWLockDequeueSelf(lock);break;}// 等待信号量,因为这个信号量是共享的,所以此次唤醒有可能是别的通知,而不是此次锁释放的原因// 通过检查 proc->lwWaiting 就可以判断是否是因为锁释放的原因唤醒的for (;;){PGSemaphoreLock(proc->sem);if (!proc->lwWaiting)break;extraWaits++;}/* Retrying, allow LWLockRelease to release waiters again. */pg_atomic_fetch_or_u32(&lock->state, LW_FLAG_RELEASE_OK);// result为false表示需要将result = false;}// 将锁添加到队列里held_lwlocks[num_held_lwlocks].lock = lock;held_lwlocks[num_held_lwlocks++].mode = mode;// 因为刚刚有别的原因造成唤醒,所以现在需要唤醒其他等待进程相同次数while (extraWaits-- > 0)PGSemaphoreUnlock(proc->sem);return result;
}

释放锁

LWLockRelease函数负责释放锁,定义如下:

void LWLockRelease(LWLock *lock);

释放锁分为下列步骤:

  1. 清除锁标记位,如果之前占用的是写锁,那么清除LW_VAL_EXCLUSIVE标记位,如果是读锁,那么将读锁数量减1。
  2. 检查LW_FLAG_HAS_WAITERS和LW_FLAG_RELEASE_OK标记位,如果都设置了并且现在读写锁都没有被占用,那么需要执行唤醒操作。
  3. 从等待队列里按照 FIFO 的顺序,按照读锁优先,写锁互斥的原则,取出等待进程。然后将这些进程对应PGPROC的
    lwWaiting成员设置为 false,并且通过成员sem信号量来唤醒。

下面列举了两种情况来阐述锁释放的原理:

  1. 假设进程等待队列为readlock0, readlock1, writelock1, readlock2,这时释放了写锁。按照 FIFO
    顺序,首先会释放读锁readlock0,然后会将后面的所有读锁都唤醒,包括readlock1和readlock2。
  2. 假设进程等待队列为writelock0, readlock2, readlock3,
    readlock4,正在占用的读锁readlock0,
    readlock1。目前读锁readlock0释放,但由于readlock1还被占用,所以不会触发唤醒。当readlock1释放时,才会触发唤醒。按照写锁互斥的原则,只有writelock0会被唤醒,之后的读锁不会被唤醒。

在取出进程之后,会清除LW_FLAG_RELEASE_OK标记位。如果检查到队列为空,还会清除LW_FLAG_HAS_WAITERS标记位。

LWLock 分类

lwlock 在 postgresql 中使用的场景非常多,为了更好管理这些 lwlock,postgresql 按照用途将它们分类,大体分为三个部分:

  1. 系统场景,定义在src/backend/storage/lmgr/lwlocknames.txt文件里
  2. 内置场景,比如buffer,wal等,由BuiltinTrancheIds表示,定义在src/include/storage/lwlock.h文件
  3. 被用于插件场景,由RequestNamedLWLockTranche 表示

作者:zhmin
链接:https://zhmin.github.io/posts/postgresql-lwlock/

#PG证书#PG中级#postgresql培训#postgresql考试#postgresql认证

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com