PostgreSQL缓冲区

举报
宁谷花雨 发表于 2021/11/10 10:40:35 2021/11/10
【摘要】 一、缓冲区类型数据访问层这一层主要是数据库引擎中对于数据的逻辑使用的层次,如用户查询数据,数据通过索引被访问到,索引不是直接从存储介质上请求 IO,而是从数据缓存层中读取,如果缓存中没有,则由缓存层负责向底层(数据存储层)获取相应的数据。 数据缓存区在数据存储层与数据库访问层之间,是一个重要的模块,起着关联内外存储介质的作用,在访问时,将表和索引中的页面从持久化存储加载到共享数据缓存中,然后...

一、缓冲区类型


数据访问层

这一层主要是数据库引擎中对于数据的逻辑使用的层次,如用户查询数据,数据通过索引被访问到,索引不是直接从存储介质上请求 IO,而是从数据缓存层中读取,如果缓存中没有,则由缓存层负责向底层(数据存储层)获取相应的数据。

 

数据缓存区

在数据存储层与数据库访问层之间,是一个重要的模块,起着关联内外存储介质的作用,在访问时,将表和索引中的页面从持久化存储加载到共享数据缓存中,然后直接对它们进行操作。写数据实际是改写缓存区,然后把缓存区标记为 “Dirty”,在必要的时候(替换:如缓存区满需要新的缓存区、CheckPoint:系统做检查点时刷出内存的脏数据等,bgwriter:周期性地少量页面刷写)被刷出缓存(写被修改的数据到外存文件)。

 

数据存储层

数据库的存储层直接和存储介质交互(实际是和 OS 的 IO 调度交互)。

数据缓存层发现缓存中没有对应的数据可以向数据访问层提供,则缓存管理器直接向数据存储层请求进行 IO,使得数据能够被读到缓存中。

 

1Local Memory Area:每个backend process都有自己私有的本地缓冲区,在具体使用时才会创建,内存大小是固定的或可变的。

1Temp_buffers:用于临时表,此参数设置每个session使用的临时缓冲区的最大数量。此参数可以在单个session中更改,但只能在session中首次使用临时表之前更改。PG使用此内存区域保存每个session的临时表,这些表将在连接关闭时清除。

2work_mem:用于排序(order bydistinctmerge-join)、hash-join,此参数指定内部排序操作和哈希表在写入临时磁盘文件之前要使用的内存量。

疑问:1、一个语句的并行查询时,是否使用多个work_mem?

3Maintenance_work_mem:用于限制vacuumcreate indexreindexalter table add foreign key所使用的最大内存量。

由于每个session只能执行这些操作中的一个,而且PG也会限制这些操作同时执行,所以Maintenance_work_mem可以比work_mem设置的更大一些。

注:当autovacuum运行时,可能会分配多达autovacuum_max_workers次的内存,因此请小心不要将默认值设置的太高。

 

2Shared Memory:所有进程共用的,系统启动时创建。

1Shared Buffer Pool:用于缓存表和索引等的数据page(内部使用NBuffers表示)。

2WAL Buffer:用于还未写入磁盘的WAL日志的共享内存大小。

3Commit Logclog保存了所有事务的状态信息,是并发控制机制的一部分(不可配置,通过NBuffers计算获得)。

 


 

 

共享缓冲区(Shared Buffer Pool)

本地缓冲区(Temp Buffers)

分配空间位置

共享内存

内存上下文

结构组成

缓冲区描述符

缓冲区

缓冲区策略(包括哈希表和共享策略控制块)

缓冲区描述符

缓冲区

哈希表

缓冲区空间

初始化之后全部分配

使用时分配

访问者

多个session间共享

session独享

锁保护

需要

不需要

Pin计数

PrivateRefCount(每个session私有)和refcount(多个session共同控制)

LocalRefCount

替换策略

Ring机制、空闲缓冲区链表和Clock Sweep算法

Clock Sweep算法

 

 

二、动态hashshmemIndex

 


 

 

三、Buffer Pool

1、hashtable与buffer映射


 

2、空闲列表


typedef struct

{

      pg_atomic_uint32  nextVictimBuffer;

      Int                              firstFreeBuffer;

      Int                              lastFreeBuffer;

} BufferStrategyControl;

 

当drop/truncate table、drop database时,把buffer加入到free-list;

如果free-list里没有buffer可用时,通过clock sweep页面置换算法获取buffer

 

3、buffer与文件映射


 

 

typedef struct RelFileNode

{

        Oid spcNode; /* tablespace */

        Oid dbNode; /* database */

        Oid relNode; /* relation */

} RelFileNode;

typedef struct buftag

{

        RelFileNode        rnode;

        ForkNumber       forkNum;

        BlockNumber     blockNum;

} BufferTag;

 

 

 

四、缓冲区管理区锁

1、BufMappingLock:

系统范围的 LWLock,从理论上保护从缓存区标记(页面标识符)到缓存区的映射。(物理上可以看成是保护 buf_table.c 维护的 hash 表)为了查找标签是否有 buffer,只需要在 BufMappingLock 上获取共享锁即可。

 

请注意,如果找到的缓存区,在释放 BufMappingLock 之前必须锁定。要改变任何缓存区的页分配,必须在 BufMappingLock 上持有排他锁。此锁必须在调整缓存区的标头字段和更改 buf_table 哈希表期间保持。唯一需要独占锁的操作是在已经不在共享缓存区中的页中读取,这至少需要一个内核调用,并且通常需要等待 I/O,所以无论如何它都会很慢。

 

BufMappingLock 被拆分为 NUM_BUFFER_PARTITIONS(128) 个独立的锁,每个锁保护一部分缓存区标记空间。这允许进一步减少正常代码路径中的竞争。特定缓存区标记所属的分区由标记的哈希值的低位决定。上述规则独立适用于每个分区。如果必须同时锁定多个分区,则必须按分区编号顺序锁定它们,以避免出现死锁的风险。

操作方式:通过LWLockAcquire加锁,通过LWLockRelease解锁。

 

2buffer header lockBufferDesc->state字段

自旋锁,在检查或更改该缓存头的字段时必须获取这个锁。这允许诸如 ReleaseBuffer 之类的操作进行本地状态更改,而无需获得任何系统范围的锁。这里使用了自旋锁,而不是 LWLock,因为在任何情况下都不需要持有多于几个指令的锁。

 

保护范围:BufferDesc结构体中的state字段

操作方式:通过 LockBufHdr 加锁,通过 UnlockBufHdr 解锁。

 

3pin

引用计数,必须「持有一个缓存的 pin」(增加它的引用计数)才能被允许做任何事情。未被 pin 的缓存区随时会被回收并重新用于不同的页面,对未 pin 的缓存访问是不安全的。通常 pin 通过 ReadBuffer 获取,通过 ReleaseBuffer 释放。单个后端同时多次锁定页面是正常的,而且确实很常见;缓存区管理器可以有效地处理这种情况。较长时间持有一个 pin 也是可以的 —— 例如,顺序扫描对当前页面持有 pin,直到处理完该页上的所有元组为止,如果是 JOIN 的外部扫描,这可能需要相当长的时间。类似地,btree 索引扫描也可以持有当前索引页上的 pin。这是可以的,因为正常操作从来不等待页面的 pin 计数降为零。(任何可能需要等待的事情都通过等待获取关系级别锁来处理,这就是为什么您最好先持有一个锁。)但是,pin不能跨事务边界持有。

      保护范围:保护缓冲区不被提前回收。

      操作方式:通过PinBuffer加锁,通过UnpinBuffer解锁。

 

4Content LockBufferDesc->content_lock字段

 

LWLock锁,有两种类型的内容锁,共享和独占,它们的作用:多个后端可以在同一缓存区上持有共享锁,但独占锁阻止其他人持有共享锁或独占锁。(这些锁也可以称为读锁和写锁。)这些锁是短期的,不应长期持有。

缓存内容锁的获取和释放由 LockBuffer() 完成,对于单个后端进程来说,无法在同一缓存上获取多个锁。

在尝试锁定缓存区之前,必须先持有对缓存的 pin。

操作方式:通过 LockBuffer 加锁解锁。

保护范围:

1、共享锁:当读取页面时,后端进程获取存储页面的缓冲区描述符的共享content_lock锁。

2、独占锁:

1)将行(即tuples)插入存储页面或更改存储页面中tuples的t_xmin/t_xmax字段(简单地说,当删除或更新行时,相关tuples的这些字段将被更改)。

2)物理地移除tuples或压缩存储页面上的free space(由vacuum和HOT触发)。

3)冻结存储页面中的tuples

 

5、buffer_strategy_lock:BufferStrategyControl->buffer_strategy_lock字段

自旋锁, buffer_strategy_lock 为访问缓存区空闲列表或选择缓存区进行替换的操作提供互斥。为了提高效率,这里使用自旋锁而不是轻量级锁;在保留 buffer_strategy_lock 时,不应获取其他任何类型的锁。这对于允许在多个后端以合理的并发度进行缓存区替换至关重要。

 

保护范围:

BufferDesc结构体中的freeNext字段;

BufferStrategyControl结构体中的nextVictimBuffer、firstFreeBuffer、lastFreeBuffer。

操作方式:参考StrategyGetBuffer函数。

 

6io_in_progress 锁:共享缓存中

一组LWLock,用于等待缓存区上的 I/O 完成。当后台进程将对应缓存页加载至缓存或者刷出缓存,都需要这个缓存描述符上的io_in_progress_lock 排它锁。

进行读写的进程在一段时间内会获得独占锁,需要等待完成的进程会尝试获得共享锁(它们在获得共享锁后会立即释放),在 LWLock 代表非平凡资源的系统中,需要这么多锁,真烦人。

也许我们可以使用每个后端 LWLock 来代替(缓存头将包含一个字段来显示哪个后端正在执行其 I/O )。

 

保护范围:当PostgreSQL进程(从/到)存储(loads/write)页面数据时,该进程在访问持久化存储时持有对应描述符的独占io_in_progress锁。

操作方式:BufferDescriptorGetIOLock获取锁,通过StartBufferIO加锁,通过TerminateBufferIO解锁。

 

7、缓存buffer访问规则:

1)要扫描一个页面的元组,必须持有一个 pin 和 content_lock 共享或独占锁。要检查共享缓存区中元组的提交状态(XID 和状态位),同样必须持有 pin 和共享锁或排他锁。

2)一旦你确定一个元组是有用的(对当前事务可见),你就可以放弃内容锁,但是只要你持有缓存 pin,就可以继续访问元组的数据。这是堆扫描通常完成的,因为 heap_fetch 返回的元组包含指向共享缓存区中的元组数据的指针。因此,在保持 pin 时,元组不能离开(参见规则#5)。其状态可以改变,但假定在初步确定可见性后,这无关紧要。

3)要增加元组或改变现有元组的 xmin/xmax 字段,必须在包含缓存区上持有 pin 和排他内容锁。这确保了其他人在执行可见性检查时不会看到元组的部分更新状态。

4)更新元组提交状态位(即,或 HEAP_XMIN_COMMITTED, HEAP_XMIN_INVALID, HEAP_XMAX_COMMITTED 或 HEAP_XMAX_INVALID 到 t_infomask)中,同时仅保留缓存区上的共享锁和 pin。这是正常的,因为另一个后端进程查看元组在同一时间会或相同的位进入字段,所以很少或没有冲突更新的风险;更重要的是,如果确实发生了冲突,那将仅仅意味着丢失一个位更新,并且需要稍后再次执行。这四个位只是提示(它们在 pg_xact 中缓存了事务状态查找的结果),因此如果它们被冲突的更新重置为零,不会造成很大的伤害。但是,请注意,通过设置 HEAP_XMIN_INVALID 和 HEAP_XMIN_COMMITTED 来冻结元组;这是一个关键更新,因此需要排他缓存区锁(并且必须记录 WAL 日志)。

5)要物理地删除页面上的元组或压缩空闲空间,必须持有一个 pin 和一个排他锁,在持有排他锁时必须检查缓存区的共享引用计数为 1(即,没有其他后端持有 pin)。如果满足这些条件,则其他后端无法执行页面扫描,直到删除排他锁,并且其他后端无法持有对现有元组的引用,而该元组的引用可能要再次检查。注意,另一个后端可能会在执行清理时锁定缓存区(增加 refcount),但是在获取共享或排他内容锁之前,它无法实际检查页面。

获取锁的规则#5 是由缓存管理器的 LockBufferForCleanup() 或 ConditionalLockBufferForCleanup() 完成的。这两个函数首先会获得排他锁,然后检查共享 pin 计数当前是否为 1。如果不为 1,ConditionalLockBufferForCleanup() 释放排他锁,然后返回 false,而 LockBufferForCleanup()释放排他锁(但不释放调用者的 pin),等待另一个后端发出信号。然后重试。当 UnpinBuffer 将共享 pin 计数减至 1 时,会发送该信号。如上所述,此操作可能需要等待很长时间才能获得锁,但对于并发 VACUUM 来说,这应该不重要。当前实现仅支持对任何特定共享缓存区上的 pin-count-1 的单个等待器。对于 VACUUM 来说,这已经足够了,因为我们不允许在一个关系上同时使用多个 VACUUM。任何希望在恢复或 VACUUM 之外获得清理锁的人都必须使用带条件函数(ConditionalLockBufferForCleanup)。

 

五、缓冲区管理

1、获取已经在Buffer Pool的页面

 


流程描述:

1、创建所需页面的buffer tag,计算hash值;

2、获取BufMappingLock分区锁,加shared锁;

3、从buffer hashtable中找到buffer tag,获取buffer id;

4、根据buffer id获取buffer descriptor

       pin buffer

     释放BufMappingLock分区锁;

5、Buffer Pool中获取buffer id对应的page

  • 在具体读取页面的行时,进程持有对应buffer descriptorshared content_lock,多个进程可以并发读取。
  • 当修改该页面的行时,进程获取exclusive content_lockdirty bit也要设为1
  • 在获取页面后,对应的refcount要加1

 

 

2、从存储加载到空的槽位

 


流程描述:

1、查找buffer hashtable,没有找到对应的buffer tag

2、freelist中获取第一个空的buffer descriptorpin

1)获取buffer_strategy_lock自旋锁

2)从空闲列表中弹出一个可用的buffer descriptor

3)释放buffer_strategy_lock自旋锁

4)pin buffer

3、获取BufMappingLock分区锁,加exclusive锁;

4、创建包含buffer tagbuffer idhash entry,插入buffer hashtable

5、从存储中加载指定页到buffer pool,使用buffer id为数组下标

1)获取exclusive io_in_progress_lock

2)io_in_progress bit置1,防止其他进程获取buffer descriptor

3)从存储中加载指定页到buffer pool

4)修改buffer descriptor的状态,io_in_progress bit置0,valid bit置1

5)释放io_in_progress_lock

6、释放BufMappingLock分区锁;

7、Buffer Pool中获取buffer id对应的page

 

 

3、从存储加载到Buffer Pool的牺牲槽位

 


流程描述:

1、查找buffer hashtable,没有找到对应的buffer tag

2、使用clock-sweep算法选中牺牲的buffer pool槽位,在buffer hashtable中获取包含牺牲槽位buffer id的旧表项,在buffer descriptorpin住牺牲槽位;

3、flushwritefsync)牺牲槽位的页表数据,否则直接到4

脏页必须先落盘,所占的slot才能被牺牲。刷脏包括以下步骤:

1)获取buffer descriptor的shared content_lock和exclusive io_in_progress_lock

2)修改buffer descriptor状态,io_in_progress bit置1,drity bit置0

3)根据具体情况,使用XLogFlush将WAL缓冲区的WAL写入WAL段文件

4)flush牺牲页

5)修改buffer descriptor状态,io_in_progress置0,valid bit置1

6)释放io_in_progress_lock和content_lock

4、获取旧页表的exclusive BufMappingLock分区锁;

5、获取新页表的exclusive BufMappingLock分区锁,添加到buffer table

1)创建包含新buffer tag和buffer id的hash entry

2)获取新的exclusive BufMappingLock分区锁

3)插入到buffer hashtable

6、删除buffer hashtable中旧的hash entry,释放旧的BufMappingLock分区锁;

7、从存储中加载到牺牲slot,更新buffer descriptorflagdirty位置0,初始化其他bit

8、释放新的BufMappingLock分区锁;

9、Buffer Pool中获取指定buffer id对应的page

 

 

4、clock sweep页面置换算法

 

置换算法:

1、获取nextVictimBuffer指向的候选buffer descriptor

2、如果候选buffer descriptor未pinned,进入3,否则进入4

3、如果候选buffer descriptor的usage_count为0,选择该buffer descriptor对应slot作为牺牲,进入5,否则usage_count-1,继续执行4

4、将nextVictimBuffer顺时针指向下一个buffer descriptor,并返回1,重复直至找出牺牲slot

5、返回牺牲slot的buffer id

 

 

采用clock sweep,而不用LRU。

buffer descriptor是循环链表。

nextVictimBuffer是32位unsigned int,总是指向某个buffer descriptor并按顺时针顺序旋转

BufferTag结构体state字段是一个无符号32位的变量

  • 18 bits refcount:当前共多少个session正在锁定访问该缓存页,如果没有session访问该页面,本文称为该缓存描述符unpinned,否则称为该缓存描述符pinned
  • 4 bits usage count:标记访问该缓存页的次数,值越大说明该Buffer经常被使用。这个属性用于缓存页淘汰算法。
  • 10 bits of flags,表示一些缓存页的其他状态

 

 

注意:

由于一个session可以多次访问同一个Buffer,因此session通过PrivateRefCount来记录自己的引用次数,只有当自己对一个Buffer的引用减少到0,才会真正去修改refcount。PrivateRefCount在session PinBuffer时将其值加1,UnpinBuffer时将其值减1。

 

 

5、ring buffer

 

Ring buffer的作用:

          当读写大表时,可能将缓存中数据都替换出去(缓存污染),buffer pool的命中率会很低。

          ring buffer使用后立刻释放,通过缓存环(buffer ring)特性来解决这个问题:

重复使用缓存的一个子集,一旦 ring 已满,循环使用一个前面使用过的页面

buffer ring 特性降低了顺序扫描、VACUUM 和批量写入的缓存消耗

 

Ring Buffer的使用场景

批量读取

当扫描一个对象的大小超过buffer pool的1/4时,ring buffer默认值为256KB(可配置:bulk_read_ring_size)。

批量读取的ring buffer大小是256KB,是因为其足够小可以放入L2缓冲区,在OS层面高效缓存。

 

批量写入

当执行下列SQL时,ring buffer默认值为16MB(可配置: bulk_write_ring_size )。

        1)copy from …

        2)create table as …

        3)create materialized view 或 refresh materialized view

        4)alter table …

 

auto vacuum

auto vacuum进程运行时,ring buffer值为NBuffers / 32 / autovacuum_max_workers 。

 

 

六、函数解析

 

 

1BufferDesc -- 共享缓冲区的共享描述符(状态)数据

typedef struct BufferDesc

{

    //buffer tag

    BufferTag   tag;            /* ID of page contained in buffer */

    //buffer索引编号(0开始)

    int         buf_id;         /* buffer's index number (from 0) */

    /* state of the tag, containing flags, refcount and usagecount */

    //tag状态,包括flags/refcount和usagecount

    pg_atomic_uint32 state;

    //pin-count等待进程ID

    int         wait_backend_pid;   /* backend PID of pin-count waiter */

    //空闲链表链中下一个空闲的buffer

    int         freeNext;       /* link in freelist chain */

    //缓冲区内容锁

    LWLock      content_lock;   /* to lock access to buffer contents */

} BufferDesc;

 

state 是一个无符号32位的变量,包含:

18 bits refcount,当前一共有多少个后台进程正在访问该缓存页,如果没有进程访问该页面,本文称为该缓存描述符unpinned,否则称为该缓存描述符pinned

4 bits usage count,最近一共有多少个后台进程访问过该缓存页,这个属性用于缓存页淘汰算法,下文将具体讲解。

10 bits of flags,表示一些缓存页的其他状态,buffer header锁定如下:

          #define BM_LOCKED               (1U << 22)  /* buffer header is locked */

//数据需要写入(标记为DIRTY)

#define BM_DIRTY                (1U << 23)  /* data needs writing */

//数据是有效的

#define BM_VALID                (1U << 24)  /* data is valid */

//已分配buffer tag

#define BM_TAG_VALID            (1U << 25)  /* tag is assigned */

//正在R/W

#define BM_IO_IN_PROGRESS       (1U << 26)  /* read or write in progress */

//上一个I/O出现错误

#define BM_IO_ERROR             (1U << 27)  /* previous I/O failed */

//开始写则变DIRTY

#define BM_JUST_DIRTIED         (1U << 28)  /* dirtied since write started */

//存在等待sole pin的其他进程

#define BM_PIN_COUNT_WAITER     (1U << 29)  /* have waiter for sole pin */

//checkpoint发生,必须刷到磁盘上

#define BM_CHECKPOINT_NEEDED    (1U << 30)  /* must write for checkpoint */

//持久化buffer(不是unlogged或者初始化fork)

#define BM_PERMANENT            (1U << 31)  /* permanent buffer (not unlogged, or init fork) */

 

 

必须持有Buffer header锁(BM_LOCKED标记)才能检查或修改tag/state/wait_backend_pid字段。

通常来说,buffer header lock是spinlock,它与标记位(flags)、参考计数(refcount)、使用计数(usagecount)组合到一个原子变量中(pg_atomic_uint32 state)。

这个布局设计允许我们执行原子操作,而不需要实际获得或者释放spinlock(比如:增加或者减少参考计数)。

buf_id字段在初始化后不会出现变化,因此不需要锁定。

freeNext通过buffer_strategy_lock锁而不是buffer header lock保护。

LWLock可以很好的处理自己的状态。

注意:buffer header lock不用于控制buffer中的数据访问。

 

当buffer header lock被锁定后,没有人能改变state字段的值。

buffer header lock的持有者可以在单次写入过程中对state字段进行复杂的更新,更新完成后释放锁(清除BM_LOCKED标志)。

另一方面,如果没有持有buffer header lock时进行state字段的更新,必须使用CAS,即使用CAS确保BM_LOCKED没有被设置。

比如原子的增加/减少、AND/OR等操作是不允许的。

 

一种例外情况是,如果我们已有buffer pinned了,该buffer的tag就不能改变(在本进程之下), 因此我们不需要锁定buffer header就可以检查tag了。

 同时,在执行一次性的flags读取时不需要锁定buffer header;这种情况通常用于我们不希望正在测试的flag bit将被改变。

 

 如果其他进程已经对该page进行了buffer pinned,那么本后台进程不能物理的从磁盘页面中删除pinned。因此,本后台进程需要等待直到其他pins清除。

这可以通过存储本后台进程自己的PID到wait_backend_pid中,并设置标记位BM_PIN_COUNT_WAITER。目前每个Page只能有一个等待进程。

 

 对于local buffer headers,我们使用相同的struct结构体,但并不需要使用locks,而且并不是所有的标记位(flag bits)都要使用。

 为了避免不必要的负载,state字段的维护不需要原子操作(CAS) (比如:pg_atomic_read_u32() and pg_atomic_unlocked_write_u32())。

 

 在增加或者记录struct结构体的成员变量时,需要小心避免增加结构体的大小。保持结构体大小在64字节内(通常的CPU缓存线大小),这对于性能是非常重要的。

 

 

2PinBuffer函数:

1、判断page是否已经被本backend process pin住了(判断PrivateRefCountArray和PrivateRefCountHash中有没有当前page),如果已经被pinned了,则转到9。

2、申请一个PrivateRefCountEntry。

3、原子读取buf->state值到本地临时变量old_buf_state。

4、判断当前page的buffer header是否被锁定(BM_LOCKED),如果被锁定时,需要等待锁释放。

5、把 old_buf_state 值赋值给本地临时变量 buf_state;

6、buf_state中的refcount加1

7、buf_state中的usagecount加1(如果超过usagecount的最大值时,就不需要加1了)

#define BM_MAX_USAGE_COUNT        5

8、设置buf->state值:原子操作,如果buf->state ==old_buf_state,则把buf_state赋值给buf->state,否则跳转到步骤4(一直重试直到成功)。

9、PrivateRefCountEntry.refcount++。

10、在ResourceOwner中记录已经被pin的page。

 

 

3UnpinBuffer函数:

1、根据buffer获取PrivateRefCountEntry

2、从ResourceOwner中删除该page。

3、PrivateRefCountEntry.refcount--。

4、如果PrivateRefCountEntry.refcount == 0时(不应该再持有content lock、IOLock),需要进一步处理。

4.1、修改buffer->state中的refcount值

4.1.1、原子读取buf->state值到本地临时变量old_buf_state。

4.1.2、判断当前page的buffer header是否被锁定(BM_LOCKED),如果被锁定时,需要等待锁释放。

4.1.3、把 old_buf_state 值赋值给本地临时变量 buf_state;

4.1.4、buf_state中的refcount减1

4.1.5、设置buf->state值:原子操作,如果buf->state ==old_buf_state,则把buf_state赋值给buf->state,否则跳转到步骤4.2(一直重试直到成功)。

4.2、如果存在其他backend process正在等待要pinpage,则唤醒等待的backend process

4.2.1、获取buf_state,同时给buffer加上锁标识BM_LOCKED。

4.2.2、如果存在其他backend process正在等待要pin该page,并且本backend process在此时对该page的refcount==1时,需要通知唤醒正在等待的backend process,具体如下:

4.2.2.1、在buffer->state字段上去掉BM_PIN_COUNT_WAITER、BM_LOCKED标识。

4.2.2.2、通知唤醒正在等待的backend process。

4.3、从PrivateRefCountEntry中删除buffer。

 

 

4BufferAlloc函数:

"strategy"可以是缓存替换策略对象,如为默认策略,则为NULL.

如使用默认读取策略,则选中的缓冲buffer的usage_count会加一,但也可能不会增加(详细参见PinBuffer).

 

返回的buffer已pinned并已标记为持有指定的页面.

如果确实已持有指定的页面,*foundPtr设置为T.

否则的话,*foundPtr设置为F,buffer标记为IO_IN_PROGRESS,ReadBuffer将会执行I/O操作.

 

*foundPtr跟buffer的BM_VALID标记是重复的,但为了ReadBuffer中的简化,仍然保持这个参数.

 

在进入或者退出的时候,不需要持有任何的Locks.

 

1、创建BufferTag,并根据BufferTag确定hash值和分区锁ID,获取分区锁newPartitionLock。

2、对分区锁newPartitionLock加LW_SHARED锁。

3、检查block是否已在buffer pool中。

      根据BufferTag,从SharedBuffer hashtable中查询获取到buff_id。

4、如果在缓冲区Buffer Pool中找到了该bufferbuff_id >= 0

4.1、获取buffer描述符,并pin buffer

4.2pagepinned后,立即释放分区锁newPartitionLock

4.3、设置BufferAlloc函数返回值:foundPtr = true。

4.4、如果pin page是无效的(PinBuffer返回F),则执行StartBufferIO函数,如果StartBufferIO函数失败了,则重新设置返回值:foundPtr = false。

         无效的原因是:(a)有其他进程仍然读入了该page,或者(b)上一次读取尝试失败。

    在这里必须等到其他活动的读取完成,然后在page状态仍然不是BM_VALID时设置读取尝试

    StartBufferIO过程执行这些工作。

4.5、BufferAlloc函数执行结束,返回 buf。

 

5、如果Buffer Pool中找不到该bufferbuf_id < 0),这时候需要初始化一个page buffer

5.1、释放分区锁newPartitionLock。

5.2、执行循环,寻找合适的buffer。

5.2.1、预留一个PrivateRefCountEntry,确保在自旋锁尚未持有时,有一个空闲的refcount入口(条目)。

5.2.2、从Shared Buffer Pool中选择一个待淘汰的buffer。

     选择出一个buffer,需要持有该Buffer Header的自旋锁(BM_LOCKED)。

StrategyGetBuffer函数

5.2.3、在仍持有自旋锁的情况下必须拷贝buffer flags到本地临时变量oldFlags

        oldFlags = buf_state & BUF_FLAG_MASK;

5.2.4、pin buffer,并释放buffer自旋锁(BM_LOCKED)。

PinBuffer_Locked函数。

5.2.5、如果选择的buffer是脏页(oldFlags & BM_DIRTY),尝试把buffer数据刷新到磁盘上。

5.2.5.1、检查脏页标识,oldFlags & BM_DIRTY。

5.2.5.2、对buffer加ContentLock的LW_SHARED锁,如果加锁失败,可能是其他进程已经锁定了该buffer,放弃(unpin buffer),获取另一个(跳转到5.2)。

需要持有buffer ContentLock共享锁来刷出该缓冲区(否则的话,我们可能会写入无效的数据,原因比如是其他进程在我们写入时压缩page)

在这里,必须使用条件锁来避免死锁

StrategyGetBuffer返回时虽然buffer尚未pinned, 其他进程可能已经pinned该buffer并且同时已持有独占锁.

如果我们尝试无条件的锁定,那么因为等待而阻塞.其他进程稍后又会等待本进程,那么死锁就会发生.

 (在实际中,两个后台进程在尝试分裂B树索引pages, 而第二个正好尝试分裂第一个进程通过StrategyGetBuffer获取的page时,会发生这种情况).

 

5.2.5.3、非默认的策略处理。

如使用非默认的策略,则写缓冲会请求WAL flush,让策略确定如何继续以及写入/重用缓冲或者选择另外一个待淘汰的buffer.

     我们需要锁定,检查page的LSN,因此不能在StrategyGetBuffer中完成.

5.2.5.3.1、在持有buffer header lock时读取LSN(读完后立即释放buffer header锁)

5.2.5.3.2、需要flush WAL并且StrategyRejectBuffer

5.2.5.3.3、清除lock/pin并循环到另外一个buffer。

5.2.5.4、现在可以自行I/O

5.2.5.4.1、执行FlushBuffer函数。

5.2.5.4.2、释放BufferDescriptor ContentLock。

5.2.5.4.3、将缓冲区添加到挂起的写回请求列表中。

ScheduleBufferTagForWriteback函数。

5.2.6、如buffer标记为BM_TAG_VALID,计算原tag的hashcode和partition lock ID,并锁定新旧分区锁

否则需要新的分区,锁定新分区锁,重置原分区锁和原hash值.

修改有效缓冲区的相关性,需要在原有和新的映射分区上持有独占锁

5.7.1、buffer标记为BM_TAG_VALIDoldFlags & BM_TAG_VALID)时,buffer是有效的。

需要计算原tag的hashcode(oldHash)和partition lock ID(oldPartitionLock).

     这里是否值得存储hashcode在BufferDesc中而无需再次计算?可能不值得.

          对新旧两个分区锁加LW_EXCLUSIVE锁,必须首先锁定更低一级编号的分区以避免死锁。

5.7.2、如果buffer是无效的,仅需要新的分区。对新分区上LW_EXCLUSIVE锁。

 

5.2.7、尝试使用buffer的新tag构造hash表入口。

这可能会失败,因为在我们写入时其他进程可能已为我们希望读入的同一个block分配了另外一个buffer.

     注意我们还没有删除原有tag的hash表入口.

5.2.7.1、调用BufTableInsert函数,向hash talbe中插入新buffer tag入口。

5.2.7.2、存在冲突处理(buf_id == -1 成功,buf_id >= 0 存在冲突)

存在冲突时,意味着某个进程已完成了我们准备做的事情.

     在这里只需要像一开始处理的那样,视为已在缓冲池发现该buffer.

     首先,放弃计划使用的buffer.

 

5.2.7.2.1Unpin buffer

5.2.7.2.2、放弃原有的partition lock,注意:仅释放oldPartitionLock ,不释放newPartitionLock,如果oldPartitionLock == newPartitionLock时,oldPartitionLock也不会释放。

根据buf_id获取buffer

Pin buffer

释放分区锁newPartitionLock

设置函数返回值:foundPtr = true

如果pin page是无效的,则执行StartBufferIO函数,如果StartBufferIO函数失败了,则重新设置返回值:foundPtr = false

BufferAlloc函数执行结束,返回 buf

 

5.2.8、不存在冲突(buf_id < 0),锁定buffer header,如缓冲区没有变脏或者被pinned,则已找到buf,跳出循环

否则,解锁buffer header,删除hash表入口,释放锁,重新寻找buffer

 

         需要锁定缓冲头部,目的是修改tag。

 

在我们执行I/O和标记新的hash表入口时,某些进程可能已经pinned或者重新弄脏了buffer.

如出现这样的情况,不能回收该缓冲区;必须回滚我们所做的所有事情,并重新寻找新的待淘汰的缓冲区.

 

5.2.8.1、获取buf->state字段,同时锁定buffer header(BM_LOCKED)

5.2.8.2、如果buf->state字段中refcount == 1 并且 无BM_DIRTY标识,则表示已经OK了。

5.2.8.3、否则需要重新寻找buffer,回滚前面做的事后跳转到5.2.

5.2.8.3.1、解锁buffer header。

5.2.8.3.2、删除hash表入口

5.2.8.3.3、释放分区锁:oldPartitionLock 、newPartitionLock 。

5.2.8.3.4、Unpin buffer。

 

6、可以重新设置buffer tag,完成后解锁buffer header,删除原有的hash表入口,释放分区锁.

 

         现在终于可以安全的给buffer重命名了

 

如需要,清除BM_VALID标记,清除脏标记位.

我们还需要重置usage_count,因为最近使用旧内容不再相关.

(usage_count从1开始,因此buffer可以在一个时钟周期经过后仍能存活)

 

 

确保标记为BM_PERMANENT的buffer必须在每次checkpoint时刷到磁盘上.

Unlogged buffers只需要在shutdown checkpoint时才需要写入,除非它们"init" forks,

这些操作需要类似持久化关系一样处理.

 

6.1、设置buf->tag、buf->state

6.2、释放buffer header锁(BM_LOCKED)。

6.3、从hashtable中删除旧分区oldPartition的hash表入口,并释放分区锁oldPartitionLock 。

6.4、释放分区锁newPartitionLock 。

 

 

7、执行StartBufferIO,设置*foundPtr标记.

         使用StartBufferIO函数从磁盘重新读取page内容。

缓冲区内存已无效.

尝试获取io_in_progress lock.如StartBufferIO返回F,意味着其他进程已在我们完成前读取该缓冲区,

因此对于BufferAlloc()来说,已无事可做.

 

8、返回buf

 

 

5ReadBuffer_common函数:

 

1、确保有空间存储buffer pin

    ResourceOwnerEnlargeBuffers(CurrentResourceOwner)函数

2、初始化相关变量和执行相关判断(是否扩展isExtend?是否临时表isLocalBuf?)

        如为P_NEW,则需扩展。如果调用方要求P_NEW,则替换适当的块号。

isExtend = (blockNum == P_NEW)

3、如果是本地缓存时(临时表),调用LocalBufferAlloc分配一个buf,返回获取buffer描述符,同时,设置是否在缓存命中的标记(变量found)

4、如果是共享缓存时(非临时表),调用BufferAlloc分配一个buf,返回获取buffer描述符,同时,设置是否在缓存命中的标记(变量found)。

5、如果在缓存中命中(buffer已在buffer pool)

5.1、如果是非扩展buffer,更新统计信息,如有需要,锁定buffer并返回

5.1.1、设置返回值:hit = true。

5.1.2、设置VacuumPageHintVacuumCostBalance

5.1.3、如果不是本地缓存时,RBM_ZERO_AND_LOCK模式,调用者期望page锁定后才返回

mode == RBM_ZERO_AND_LOCK时,加ContentLockLW_EXCLUSIVE锁;

mode == RBM_ZERO_AND_CLEANUP_LOCK时,。。。;

 

5.1.4、根据buffer描述符读取buffer并返回buffer

5.2、如果为扩展buffer,则获取block

    程序执行来到这里,进程尝试扩展relation但发现了先前已存在的标记为BM_VALID的buffer.

    这种情况之所以发生是因为mdread对于在EOF之后的读不会报错(zero_damaged_pages设置为ON), 并且先前尝试读取EOF的block遗留了"valid"的已初始化(填充0)的buffer.

    不幸的是,我们同样发现因为Linux内核的bug(有时候会返回lseek/SEEK_END结果)导致这种情况.

    在这种情况下,先前已存在的buffer会存储有效的数据,这些数据不希望被覆盖.

    由于合法的情况下应该总是留下一个零填充的缓冲区,如果不是PageIsNew,则报错。

 

5.2.1、获取block。

5.2.2、如果PageIsNew返回F,则报错。

 

         在成功执行前,必须执行smgrextend,否则的话page不能被内核保留, 同时下一个P_NEW调用会确定返回同样的page.

         清除BM_VALID位,执行BufferAlloc没有执行的StartBufferIO调用,然后继续。

 

5.2.3、如果是本地缓冲区(临时表),只需要调整标记,buf_state中去掉BM_VALID。

5.2.4、如果是共享缓冲区(非临时表),循环,清除BM_VALID标记,直至StartBufferIO返回T为止。

 

 

6、如果没有在缓存中命中(buffer不在Buffer Pool中),则获取block

 

如果到了这个份上,我们已经为page分配了buffer,但其中的内容还没有生效.

如果是共享内存,那么设置IO_IN_PROGRESS标记.

注意:如果smgrextend失败,我们将以一个已分配但设置为BM_VALID的buffer结束这次调用

 

6.1、如果是扩展buffer时,通过填充0初始化buffer,调用smgrextend扩展block

buffers使用0填充,对于使用全0填充的page,不要设置checksum

调用smgrextend函数。

 

     注意:这里我们不会执行ScheduleBufferTagForWriteback.虽然我们实质上正在执行写操作.

     起码,在Linux平台,执行这个操作会破坏“延迟分配”机制,导致文件碎片.

 

6.2、如果是普通block

 

读取page,除非调用者期望覆盖它并且希望我们分配buffer

 

6.2.1、如为RBM_ZERO_AND_LOCK或者RBM_ZERO_AND_CLEANUP_LOCK模式,初始化为0

6.2.2、其他模式时,通过smgr(存储管理器)读取block,如需要,则跟踪I/O时间,同时检查垃圾数据

6.2.2.1smgr(存储管理器)读取block

6.2.2.2、检查page header和checksum是否有效(PageIsVerified((Page) bufBlock, blockNum),

如果page有效,什么也不做;

如果page是无效的,且当 mode == RBM_ZERO_ON_ERROR || zero_damaged_pages时,初始化为0

                    mode 不是 RBM_ZERO_ON_ERROR时,报错。

 

7、已扩展了buffer或者已读取了block

 

RBM_ZERO_AND_LOCK模式下,在标记page为有效之前获取buffer content lock, 确保在调用者初始化之前没有其他进程看到已初始化为0的page

 

由于没有其他进程可以搜索page内容,因此获取独占锁和cleanup-strength锁没有区别.

(注意不能在这里使用LockBuffer()或者LockBufferForCleanup(),因为这些函数假定buffer有效)

 

7.1、如果是本地缓冲区(临时表),只需要调整标记,在buf_state中增加BM_VALID。

7.2、如果是共享缓冲区(非临时表)

 

7.2.1、如需要,锁定buffer。如果 mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK时,上ContentLock的LW_EXCLUSIVE锁。

7.2.2、调用TerminateBufferIO(bufHdr, false, BM_VALID),设置BM_VALID,中断IO,唤醒等待的进程。

7.3、更新统计信息。

7.4、返回buffer。

 

 

6FlushBuffer函数:物理地写出一个共享缓冲区。

 

注意:这实际上只是将缓冲区内容传递给内核;真正的磁盘写入只有在内核喜欢时才会发生。从我们的角度来看,这是可以的,因为我们可以从WAL重做更改。

但是,我们需要通过fsync强制更改磁盘,然后才能检查WAL。

调用者必须在缓冲区上保留pin,并且缓冲区的共享内容锁。(注意:共享锁不会阻止缓冲区中提示位的更新,因此页面可能会在写入过程中更改,但我们假设这不会使写入的数据无效。)

如果调用者有缓冲区关系的smgr引用,则将其作为第二个参数传递。如果不是,则传递NULL

 

1、执行StartBufferIO函数。

      需要在io_in_process锁的保护下。

     如果StartBufferIO返回false,那么其他人会在我们之前刷新缓冲区,所以我们不需要做任何事情

 

2、设置错误报告回调函数,创建一个SMgrRelation对象

3、得到缓冲区块的日志序列号LSN,buf_state中去除BM_JUST_DIRTIED标记。(需要buffer header lock保护)

4、如果buf_state带有BM_PERMANENT标识时,在导出数据之前通过函数XLogFlush(Xlog.c)先将日志写到磁盘上。

 

强制XLOG刷新到缓冲区的LSN。这实现了基本的WAL规则,即日志更新必须在它们描述的任何数据文件更改之前到达磁盘。

但是,此规则不适用于未标记的关系,这些关系在崩溃后将丢失。大多数未标记的关系页不带有LSN,因为我们从不为它们发出WAL记录,因此通过缓冲区LSN刷新将是无用的,但无害的。

然而,GiST索引在内部使用LSN跟踪页面拆分,因此未标记的GiST页面带有由GetFakeLSNForUnloggedRel生成的“伪”LSN。

伪LSN计数器不太可能但可能会超过WAL插入点;如果真的发生了这种情况,试图刷新WAL日志将失败,并造成灾难性的全系统后果。

为了确保不会发生这种情况,如果缓冲区不是永久性的,请跳过刷新。

 

5、最后调用函数smgrwrite(smgr.c)将缓冲区的数据导入到kernel,并调用函数TerminateBufferIO,见6.5.8,标记停止I/O。

 

5.1、获取block。

现在可以安全地将缓冲区写入磁盘了。请注意,当我们忙于日志刷新时,其他人不应该能够编写它,因为我们有io_in_progress锁

5.2、如果需要,更新页面校验和。由于我们在缓冲区上只有共享锁,其他进程可能正在更新其中的提示位,所以如果我们进行校验和,我们必须将页面复制到私有存储。

5.3、调用smgrwrite函数把buffer写入磁盘。bufToWrite是共享缓冲区或副本,视情况而定。

5.4、调用TerminateBufferIO函数,将缓冲区标记为干净(除非设置了BM_JUST_DIRTIED),并释放io_in_progress锁。

 

 

 

 

 

7LockBufferForCleanup-锁定缓冲区以准备删除项。

 

只有当调用者(a)在缓冲区上持有独占锁,并且(b)观察到没有其他后端在缓冲区上持有pin时,才能从磁盘页删除项。如果有一个pin,那么另一个后端可能会有一个指向缓冲区的指针(例如,对某个项的heapscan引用——有关更多详细信息,请参阅自述)。但是,如果在清理开始后添加了一个管脚,则可以;在我们释放独占锁之前,新到达的后端将无法查看页面。

 

要实现此协议,可能的删除程序必须锁定缓冲区,然后调用LockBufferForCleanup()。LockBufferForCleanup()与LockBuffer(buffer,buffer_LOCK_EXCLUSIVE)类似,只是它会循环,直到成功观察到pin count=1为止。

 

 

 

 

 

8StartBufferIO:标记缓冲区块,开始启动I/O操作。

*在某些场景中,存在多个后端可以同时尝试相同I/O操作的竞争条件。如果其他人已经在这个缓冲区上启动了I/O,那么我们将阻止io_in_progress lock锁,直到他完成为止。

*输入操作仅在非BM_VALID的缓冲区上尝试,输出操作仅在BM_VALID和BM_DIRTY的缓冲区上尝试,因此我们始终可以判断工作是否已经完成。

*如果我们成功地将缓冲区标记为I/O繁忙,则返回true;如果其他人已经完成了此工作,则返回false。

 

 

TerminateBufferIO:标记缓冲区块,结束I/O操作。

AbortBufferIO:标记缓冲区块,中断I/O操作出错)

 

 

9CheckPointBuffers函数:

在检查点时间将缓冲池中的所有脏块刷新到磁盘。注意:临时关系不参与检查点,因此它们不需要刷新。

 

1、调用BufferSync函数,写出池中的所有脏缓冲区。

 

 

10BufferSync函数:

在检查点时间调用该函数以写出所有脏共享缓冲区。

应传入检查点请求标志。如果设置了CHECKPOINT_IMMEDIATE,则禁用写入之间的延迟;

如果设置了CHECKPOINT_IS_SHUTDOWN、CHECKPOINT_END_OF_RECOVERY、CHECKPOINT_FLUSH_ALL标识时,我们甚至会写入unlogged buffers,否则将跳过这些缓冲区。

其余的标志目前在此处无效。

 

1、调用函数ResourceOwnerEnlargeBuffers(Resowner.c)来保证ResourceOwner可以获得缓冲区

2、除非这是一个shutdown checkpoint或者我们被明确告知,否则我们只写需要持久化的脏页缓冲区。但在关机或恢复结束时,我们会写入所有脏缓冲区。

3、遍历所有缓冲区块,添加自旋锁,找到所有被标记为BM_DIRTY的缓冲区,并标记它为BM_CHECKPOINT_NEEDED,并通过num_to_scan来记录数目并释放自旋锁。

 

循环所有缓冲区,并使用BM_CHECKPOINT_NEEDED标记需要的缓冲区。使用num_to_scan来计数,这样我们就可以估计需要做多少工作。

这允许我们只写checkpoint开始时变脏的页面,而不写在checkpoint进行过程中变脏的页面。

无论何时,要么通过本函数后面的操作,要么通过后台进程,要么通过bgwriter,把标记为BM_CHECKPOINT_NEEDED的page写入磁盘,并清除BM_CHECKPOINT_NEEDED标志。

在此点之后被弄脏的任何缓冲区都不会设置标志。

请注意,如果我们未能写入一些缓冲区,我们可能会保留仍设置了BM_CHECKPOINT_NEEDED标记的缓冲区。这是可以的,因为标记了BM_CHECKPOINT_NEEDED的缓冲区可以在下次checkpoint时继续写入磁盘。

4、对需要写入的缓冲区进行排序,以减少随机IO的可能性。排序对于实现表空间之间的写操作平衡也很重要。如果不平衡写操作,我们可能会一个接一个地写入表空间;可能导致底层系统过载。

5、在各个表空间中的写进度上构建一个最小堆,并计算单个已处理缓冲区的总进度的一部分有多大。

 

6、遍历要设置检查点的缓冲区,并写入(仍然)标记为BM_CHECKPOINT_所需的缓冲区。写操作在表空间之间是平衡的;否则,排序将导致一次只有一个表空间接收写操作,从而导致硬件使用效率低下。

调用函数CheckpointWriteDelay,由bgwrite来决定是否延迟同步到磁盘。

 

 

11、StrategyGetBuffer函数

StrategyGetBuffer在BufferAlloc()中,由bufmgr调用,用于获得下一个候选的buffer.

BufferAlloc()中唯一稍微困难的需求是选择的buffer不能被其他后台进程pinned.

strategy是BufferAccessStrategy对象,如为默认策略,则为NULL.

为了确保没有其他后台进程在我们完成之前pin buffer,必须返回仍持有buffer header自旋锁的buffer.

 

 

其主要的处理逻辑如下:

1、如策略对象不为空,则从环形缓冲区中获取buffer,如成功则返回buf。

如果给定了一个策略对象,看看是否可以选择一个buffer.

    我们假定策略对象不需要buffer_strategy_lock锁.

 

2、如需要,则唤醒后台进程bgwriter,从共享内存中读取一次,然后根据该值设置latch。

        我们不希望依赖自旋锁实现这一点,所以强制从共享内存中读取一次,然后根据该值设置latch.

    我们需要走完这一步,否则的话bgprocno在检查期间或之后被重置,因为编译器可能重新从内存中读取数据.

 

         如果bgwriter出现异常宕机,可能会出现latch被设置为错误的进程.

    但是由于PGPROC->procLatch从来没有被释放过,最坏的结果是我们设置了一些任意进程的latch。

 

在设置latch前,首先重置bgwprocno为-1

在这里不需要请求"令人生厌"的ProcArrayLock.

因为procLatch未曾释放过,因此实际上是没有问题的,所以我们可能会设置错误的进程(或没有进程)latch。

 

3、计算buffer分配请求,这样bgwriter可以估算buffer消耗的比例.

注意通过策略对象进行的buffer回收不会在这里计算.

 

4、检查freelist中是否存在buffer,如存在,则执行相关判断逻辑,如成功,则返回buf。

不需要请求锁,首次检查在freelist中是否存在buffer.

     因为我们不需要在每次StrategyGetBuffer()调用时都使用自旋锁,

     在这里请求自旋锁有点郁闷 -- 因为大多数情况下都没有用,即大多数情况下buffer都被使用了,freelist是空的.

     这显然存在一个竞争,其中缓冲区被放在空闲列表中,但进程却看不到存储

     -- 但这是无害的,在下次buffer申请期间使用. 

 

如果在空闲列表中有buffer存在,请求自旋锁,从空闲列表中弹出一个可用的buffer.

     然后检查该buffer是否可用,如不可用则继续处理.

 

注意freeNext字段通过buffer_strategy_lock锁来保护,而不是使用独立的缓冲区自旋锁保护, 因此不需要持有自旋锁就可以维护这些字段.

 

 

5、空闲链表中找不到或者满足不了条件,则执行"clock sweep"算法。

使用clock sweep算法,选择buffer,执行相关判断,如成功,则返回buf。如无法获取,在尝试过trycounter次后,报错。

 

5.1、使用clock sweep算法获取一个buffer描述符。

5.2、如果buffer已pinned,或者有一个非零值的usage_count,不能使用这个buffer. 减少usage_count(除非已pinned)继续扫描.

5.2.1、获取buf->state,并加buffer header锁(BM_LOCKED)。

5.2.2、如果buf_state->refcount == 0时

如果usagecount == 0,返回buffer

如果usagecount > 0usagecount - 1,解锁buffer header后,继续循环重新获取一个buffer(跳转到5.1)。

5.2.3、如果buf_state->refcount > 0时,trycounter - 1,如果 trycounter  == 0了,则报错。

在没有改变任何状态的情况,我们已经完成了所有buffers的遍历,

           因此所有的buffers已pinned(或者在搜索的时候pinned).

          我们希望某些进程会周期性的释放buffer,但如果实在拿不到,那报错总比傻傻的死循环要好.

 

 

 

 

12GetBufferFromRing函数:

1、我们直接获取缓冲区current下一个位置所指的缓冲区,如果current已经指到最后的话,我们就将其置零,重新开始。

2、如果当前环所指位置的缓冲区是无效的,就说明这个位置的缓冲区还不在环中,我们直接返回NULL,之后会有后续的操作也就是上面的AddBufferToRing将新的缓冲区添加进环里。

3、虽然我们成功地从环里取得了一个缓冲区,但是我们并不知道这个缓冲区是否可用,

      所以我们要根据缓冲区描述器来判断它是否被其它人引用了,如果这个缓冲区的引用计数为0且使用计数小于等于1,那么这个缓冲区对于我们来说就是可用的,我们可将其返回。

4、如果这个缓冲区被别人引用了的话,我们只好返回NULL,然后使用函数AddBufferToRing添加一个可用的缓冲区。

 

13StrategyRejectBuffer函数:

函数GetBufferFromRing从环中得到一个缓冲区的时候并没有注意这个缓冲区中的数据是不是脏的,也就是说我们在通过缓冲区策略获得的缓冲区可能是一个脏的缓冲区,而如果我们要使用它的话就得把这些脏数据写出去,那么在写这些脏数据之前我们又要写日志数据,所以这种情况的处理是比较麻烦的,既然这样我们何不放弃这个缓冲区,另外选择一个牺牲者呢?这个函数的设计就是基于这样的想法。如果这个函数最终返回true缓冲区管理器就选择一个新的牺牲者,如果是false缓冲区管理器就将这个缓冲区的脏数据写出去然后重新使用它。

 

1、我们只有在缓冲区策略模式为BAS_BULKREAD时才进行这种判断。

2、如果当前buffer不是ring buffer中的current buffer时,返回false。

3、将这个脏的缓冲区从环中删除掉,选择新的牺牲者。

 

 

14ClockSweepTick()函数:

StrategyGetBuffer()的辅助过程,移动时针(当前位置往前一格),返回时针指向的buffer.

nextVictimBuffer可视为时钟的时针,把缓冲区视为环形缓冲区,时针循环周而往复的循环,如缓冲区满足unpinned(ref_count == 0) && usage_count == 0的条件,则选中该缓冲,否则,usage_count减一,继续循环,直至找到满足条件的buffer为止.选中的buffer一定是buffers中较少使用的那个.

 

其主要的处理逻辑如下:

1、原子移动时针一格。

  如果有多个进程执行这个操作,这可能会导致缓冲返回的顺序稍微有些混乱.

 

15BufTableInsert函数:

给定tag和buffer ID,插入到哈希表中,如该tag相应的条目已存在,则不处理.

如成功插入,则返回-1.如冲突的条目已存在,则返回条目的buffer ID.

调用者必须持有tag分区BufMappingLock独占锁.

 

 

16、ResourceOwner

 

在PostgreSQL中,每个事务(包括其子事务)在执行时,需要跟踪其占用的内存资源。为简化查询相关资源的管理(如pin和表级锁等),PG中引入了ResourceOwner对象(例如,需要跟踪一个缓冲区的pin锁或一个表的锁资源,以保障在事务结束或失败的时候能够被及时释放)的概念。这些与查询相关的资源必须以某种可靠的方式被跟踪,以确保当查询结束后被释放,甚至是查询由于错误被中止时资源被释放。相对于期望整个执行过程拥有牢不可破的数据结构,PG更愿意采用单例模式去跟踪这些资源。ResourceOwner记录了大多数事务过程中使用到的资源,主要包括:

 

父节点、子节点的ResourceOwner指针,用于构成资源跟踪器之间的树状结构

占用的共享缓冲区数组和持有的缓冲区pin锁个数

Cache的引用次数以及占用的Cache中存储的数据链表,包括CatCache、RelCache以及缓存查询计划的PlanCache

TupleDesc引用次数以及占用的TupleDesc数组

Snapshot引用次数以及占用的Snapsot数组

 

 

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。