当前位置: 首页 > postgresql, 死锁 > 正文

这是一次极度纯粹的 C 语言内核级内存推演。

屏蔽掉操作系统调度、屏蔽掉业务 SQL,直接进入 PostgreSQL 的共享内存(Shared Memory)深处。假设此时 PGPROC_1(业务进程 1)因为某种原因(例如申请 8 级强锁,或 Fast Path 已满),必须去全局哈希表中申请 LOCK_A(某张表的物理锁),并且最终申请成功(无冲突)

将时间的流速放慢到 CPU 指令周期级别,分 5 个微观物理帧(Frame),把这三大结构体(PGPROCLOCKPROCLOCK)的内存指针是如何完成完美咬合并改变状态的过程,一行代码一行代码地推演出来。


第 1 帧:寻址与靶标锁定(LOCK 结构体的确认)

【物理起点】PGPROC_1 带着一个目标(比如表 OID = 10000 的 8 级排他锁请求)冲进锁管理器。

  1. 哈希计算:CPU 提取表 OID 10000,经过哈希函数计算出一个 Hash Key。
  2. 第一张哈希表寻址:CPU 拿着 Hash Key,在 LockMethodLockHash(全局资源哈希表)中进行 $O(1)$ 查找。
  3. 靶标确立
    • 假设表不存在:如果这是系统第一次访问该表,内核会调用 ShmemAlloc() 在共享内存中动态分配一个 LOCK_A 结构体,并将其初始化(核心字段 grantMask = 0,代表没有任何人持有锁;procLocks 链表头为空)。
    • 假设表已存在:直接命中内存地址,获取到 LOCK_A 结构体的指针。

【当前内存图景】PGPROC_1 的指针和 LOCK_A 的指针在内存中隔空相望,尚未发生任何物理联系。


第 2 帧:契约的物理铸造(PROCLOCK 结构体的诞生)

既然 PGPROC_1 想要 LOCK_A,必须在它们之间建立合法的“租赁契约”。

  1. 组合哈希键:CPU 将 PGPROC_1 的内存地址指针和 LOCK_A 的内存地址指针,拼接在一起,作为联合 Hash Key。
  2. 第二张哈希表寻址:CPU 拿着这个联合 Key,冲向 LockMethodProcLockHash(契约哈希表)
  3. 分配内存:由于这是 PGPROC_1 第一次申请 LOCK_A,哈希表中必定找不到记录。内核立刻在共享内存中 malloc 出一个全新的结构体:PROCLOCK_1A

【底层指针的硬性绑定】

PROCLOCK_1A 诞生的第一微秒,内核立刻对其内部的两个常量指针进行不可逆的赋值:

  • PROCLOCK_1A -> tag.myLock = LOCK_A(指向上游的资源靶标)
  • PROCLOCK_1A -> tag.myProc = PGPROC_1(指向下游的所属进程)

此时,桥梁的物理墩柱已经打好,但桥面尚未通车(锁还没批下来)。


第 3 帧:纳秒级生死判决(状态掩码的位运算)

这是决定“申请成功”的最核心 CPU 指令。

  1. 提取请求掩码PGPROC_1 申请的是 8 级锁,CPU 提取 8 级锁对应的内部二进制位(假设为 10000000)。
  2. 读取资源状态:CPU 顺着指针看向 LOCK_A -> grantMask。因为此时没有别人持有冲突锁,假设 grantMask00000000
  3. 按位与(Bitwise AND)判定10000000 (请求位) & 00000000 (当前状态位) = 0
  4. 判决结果:结果等于 0!内核判定:绝对兼容,无冲突,予以放行!

第 4 帧:状态的物理突变(掩码改写)

既然判决成功,内存中的结构体必须立刻留下物理痕迹,以防接下来的其他并发进程“硬闯”。

  1. 改写契约状态(局部宣誓):CPU 找到刚刚建好的 PROCLOCK_1A 结构体,将其内部的 holdMask(当前契约持有的锁级别)的第 8 个比特位强行拨为 1
    • 物理变化:PROCLOCK_1A -> holdMask |= 10000000
  2. 改写资源状态(全局宣誓):CPU 顺着指针回到 LOCK_A 结构体,将其内部的 grantMask(全局授权状态)的第 8 个比特位强行拨为 1
    • 物理变化:LOCK_A -> grantMask |= 10000000

【推演意义】:从这一微秒开始,如果全库有任何其他进程再来查询 LOCK_A,它们在进行位运算时,就会因为这个 grantMask 中的 1 而发生冲突并被物理拦截。


第 5 帧:双向十字链表的编织(拓扑图闭合)

结构体建好了,状态也改了,最后一步是把它们“串”起来,保证系统能在 $O(1)$ 时间内进行双向追溯。PG 在这里使用了极其经典的 SHM_QUEUE(共享内存双向环形链表)。

  1. 串入资源的链表(物找人):CPU 将 PROCLOCK_1A 内部的 lockLink 节点,通过前后指针的修改,强行插入到 LOCK_A -> procLocks 这条双向链表的头部。
    • 效果:以后系统只要找到 LOCK_A,顺着链表一撸,就能精准拔出 PROCLOCK_1A,进而找到 PGPROC_1
  2. 串入进程的链表(人找物):CPU 将 PROCLOCK_1A 内部的 procLink 节点,通过前后指针的修改,强行插入到 PGPROC_1 -> myProcLocks 这条双向链表的头部。
    • 效果:等到事务 COMMIT 时,PGPROC_1 根本不需要去全局哈希表盲找,直接顺着自己身上的这条链表,就能瞬间找到 PROCLOCK_1A 并将其销毁。

物理推演终点(Success)

至此,PGPROC_1 申请 LOCK_A 成功的全部底层物理动作彻底结束。

终极物理图景:

在内存中,PGPROC_1LOCK_A 通过中间的 PROCLOCK_1A,被两根双向链表极其牢固地死死锁在一起。同时,LOCK_A 的二进制掩码灯(grantMask)已被点亮,向整个数据库实例宣告了它的排他性主权。执行引擎拿到绿灯,长驱直入,开始向底层物理磁盘下发真实的 I/O 修改指令。


了解 www.876873.xyz 的更多信息

订阅后即可通过电子邮件收到最新文章。

PGPROC_1 申请 LOCK_A 成功。内存中生成契约 PROCLOCK_1A(状态为 granted = true)。底层c语言结构体配合工作的流程::等您坐沙发呢!

发表评论