PostgreSQL中的锁

  • 主要讲解PostgreSQL中所使用的锁及其原理

  PostgreSQL中关于spinLock的描述被放在spin.h和spin.c中。spin.h 中为硬件(平台)无关spinlock的实现的描述.

在实现时尤其需要注意是volatile 类型的使用;在使用该类型时候,在变量在内存中发生变化时候,不能够在使用该限定符来修饰变量,对于其中原因见:Lock-free Queue Impl中对于cmpxchg函数的描述。 其会产生ABA问题。

在使用spinlock的时候,为了防止编译器在优化过程中对相关代码优化导致的re-order问题[因为编译器在进行代码优化的过程中会将某些代码视作无用代码进行优化] ,因此我们在使用spinlock或者是其保护的共享内存对象时候,都应该使用volatile限定符对变量进行修饰。

但对于TAS指令的使用时候,对于某些系统结构上(weak memory ordering 弱内存序化)必须使用硬件层的memory fence指令来保证,指令的正确的顺序,因为在某些系统架构上(例如:alpha平台上)会导致代码的乱序执行。

wikipedia 上给出了对于memory ordering 的解释:

“Memory ordering is a group of properties of the modern microprocessors, characterising their possibilities in memory operations reordering. It is a type of out-of-order execution. Memory reordering can be used to fully utilize different cache and memory banks.

On most modern uniprocessors memory operations are not executed in the order specified by the program code. But in singlethreaded programs from the programmer’s point of view, all operations appear to have been executed in the order specified, with all inconsistencies hidden by hardware.”

该段话阐述了:对于多处理器环境下为了更多的使用多处理器的并行能力,一般会将程序分派到不同的处理器上执行,这便会导致程序的代码并不是我们所看到的顺序执行,而是某些独立的代码先于其它代码执行,当这些代码不存在着数据关联或者逻辑上的关联,此时便可以将这些代码分派到其它的处理器上执行;但是,当我们在单处理器上时候,编译器的优化则会少的很多,从程序猿的角度来看就像其代码是按照顺序执行的,但是所有的不一致性均被硬件层所规避。

更多参考:

1: Handling Memory Ordering in Multithreaded Applications with Oracle Solaris Studio 12

2:Weak vs. Strong Memory Models.

3: Memory ordering.

4:Memory ordering in Modern MicroProccessor.

在s_lock.h 中定义了硬件相关的spinlock的实现,在该文件中便使用了如下的代码 :

#if defined(__GNUC__) || defined(__INTEL_COMPILER)

static __inline__ int
tas(volatile slock_t *lock)
{
register slock_t _res = 1;
/*
* Use a non-locking test before asserting the bus lock. Note that the
* extra test appears to be a small loss on some x86 platforms and a small
* win on others; it’s by no means clear that we should keep it.
*/
__asm__ __volatile__(
” cmpb $0,%1 \n”
” jne 1f \n”
” lock \n”
” xchgb %0,%1 \n”
“1: \n”
: “+q”(_res), “+m”(*lock)
:
: “memory”, “cc”);
return (int) _res;
}

#else

static __inline__ int
tas(volatile slock_t *lock)
{
int ret;
ret = _InterlockedExchange(lock,1); /* this is a xchg asm macro */
return ret;
}

#endif

static __inline__ void
spin_delay(void)
{
__asm__ __volatile__(
” rep; nop \n”);
}

或者spin_delay被定义为: SPIN_DELAY() ((void) 0), 其仅仅是一个0,插入一条空指令,其作用类似于硬件指令中的 nop 指令。 同时,由于tas被大量的重复使用因此这些函数被定义为inline。

在s_lock.c 中我们看到s_lock函数 其原型定义如下:
int
s_lock(volatile slock_t *lock, const char *file, int line)
{

}

其中,slock_t 被定义为 typedef LONG slock_t;
其具体是实现为:
while (TAS_SPIN(lock))
{
SPIN_DELAY();
//进行尝试, 每次尝试后,增加尝试计数,并增大延迟时间。
if (++spins >= spins_per_delay) {
….
pg_usleep(cur_delay * 1000L);
….
}
}

至此,我们已经在硬件层上封装了一个锁的原始函数[硬件依赖],但是为了方便用户使用我们在其上面进行在封装,以便用户可以方便使用,其便是 spin.h 和spin.c 存在的原因。 在spin.h中我们定义了如下的四类宏:

#define SpinLockInit(lock) S_INIT_LOCK(lock)
#define SpinLockAcquire(lock) S_LOCK(lock)
#define SpinLockRelease(lock) S_UNLOCK(lock)
#define SpinLockFree(lock) S_LOCK_FREE(lock)

我们很容易从字面上可以看出, SpinLockInit对一个锁对象进行初始化操作, SpinLockAcquire获取一个锁对象,SpinLockRelease释放一个已经获得的锁对象, SpinLockFree释放相应的锁对象。 而这些宏所对于的实际的操作均定义在s_lock.h中。
但是在spin.c中我们也看到了当系统中已经定义了spinlock时候,即我们已经在s_lock.h/s_lock.c中完成了对锁底层的实现时,系统便使用该种方式,否则pg则使用semaphore来模拟锁的实现。 我们在spin.c中可以看到:

#ifdef HAVE_SPINLOCKS
xxx
#else
/*
* No TAS, so spinlocks are implemented as PGSemaphores.
*/
#endif

至于什么是信号量(Semaphore)请参考相关文档,这里不再详述。

下面我们讨论对于spinlock的使用场景,spinlock属于轻量级的锁,适用于对某个share object操作较多,并且占用时间不长的情景下使用,即我们在较小的代码中使用,对于复杂场景下的使用我们一般不采用spinlock,而采取其他方式。实例代码如下:

static void
BeginStrongLockAcquire(LOCALLOCK *locallock, uint32 fasthashcode)
{
SpinLockAcquire(&FastPathStrongRelationLocks->mutex);
FastPathStrongRelationLocks->count[fasthashcode]++;
locallock->holdsStrongLockCount = TRUE;
StrongLockInProgress = locallock;
SpinLockRelease(&FastPathStrongRelationLocks->mutex);
}

该段代码为PostgreSQL中的关于 PostGRS的primary lock的具体实现。其所采用的底层为我们上面所讨论的spinlock,我们可以说PG中的锁均以spinlock为基础,进而在此基础上所完成。

LigthWeight lock我们称为轻量锁,其主要是用来保护share-memory的数据结构,下面是PG对于Lwlock的说明,从中我们可以清楚的看到该种类型的锁的目的和作用,而这lwlock也是PostgreSQL所提供的锁的一种类型。其相应的代码分别在:lwlock.h/lwlock.c文件中。

“Lightweight locks are intended primarily to provide mutual exclusion of access to shared-memory data structures. Therefore, they offer both exclusive and shared lock modes (to support read/write and read-only access to a shared object). There are few other frammishes. User-level locking should be done with the full lock manager — which depends on LWLocks to protect its shared state. ”

Lwlock在实现的过程中使用了spinlock 作为其底层的实现机制。 在PostgreSQL中我们使用LWLock来描述轻量锁。 其相应的数据结构定义如下:

typedef struct LWLock
{
slock_t mutex; /* Protects LWLock and queue of PGPROCs */
bool releaseOK; /* T if ok to release waiters */
char exclusive; /* # of exclusive holders (0 or 1) */
int shared; /* # of shared holders (0..MaxBackends) */
PGPROC *head; /* head of list of waiting PGPROCs */
PGPROC *tail; /* tail of list of waiting PGPROCs, tail is undefined when head is NULL */

} LWLock;

从其数据结构的定义可以看出,其使用一个链表( PGPROC *head ,PGPROC *tail )来保存那些正在争取锁,而暂时无法得到满足的进程,我们称之为 Waiting List, 同时使用shared来描述先在共同持有该锁的进程数。Lwlock在所有已知的平台下其大小在16-32个字节。

为了系统在访问lwlock时的速度,我们将lwlock的大小限制为2的幂,这样在内存中可以进行对其从而加快其访问速度;同时,保证每个lwlock对象不会跨越cache边界,因而可以降低了cache竞争;故此,我们将lwlock进行包装,使用一些pad来进行填充,使其满足大小为2的幂。 其方式如下:

#define LWLOCK_PADDED_SIZE (sizeof(LWLock) <= 16 ? 16 : 32)

typedef union LWLockPadded
{
LWLock lock;
char pad[LWLOCK_PADDED_SIZE];
} LWLockPadded;

为了方便的跟踪我们所持有锁的情况,我们使用一个数组来保存我们说持有的锁的ID,其中 num_held_lwlocks 描述了我们所持有lwlock的数量,而数组held_lwlocks中描述了我们所持有的所的情况[我们根据其锁的ID来获取相应的锁对象]。我们可以使用print_lwlock_stats 函数来打印出每个所的具体状态信息,这里我们就不在详述该函数的具体实现,有兴趣可以阅读相应的实现[lwlock.c]。

#define MAX_SIMUL_LWLOCKS 100
static int num_held_lwlocks = 0;
static LWLockId held_lwlocks[MAX_SIMUL_LWLOCKS];

我们用 NON_EXEC_STATIC LWLockPadded *LWLockArray 来描述系统所分配的lwlock对象在系统内存中的地址。

在函数 void CreateLWLocks(void)中,系统将对lwlock对象进行分配相应的内存空间并对所分配的lwlock对象进行初始化。 具体操作如下:

void CreateLWLocks (void)
{
ptr = (char *) ShmemAlloc(spaceLocks);
ptr += 2 * sizeof(int);
/* Ensure desired alignment of LWLock array */
ptr += LWLOCK_PADDED_SIZE – ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE;
LWLockArray = (LWLockPadded *) ptr;
for (id = 0, lock = LWLockArray; id < numLocks; id++, lock++) //分配,初始化
{
SpinLockInit(&lock->lock.mutex);
lock->lock.releaseOK = true;
lock->lock.exclusive = 0;
lock->lock.shared = 0;
lock->lock.head = NULL;
lock->lock.tail = NULL;
}
LWLockCounter = (int *) ((char *) LWLockArray – 2 * sizeof(int));
LWLockCounter[0] = (int) NumFixedLWLocks;
LWLockCounter[1] = numLocks;
}

其中,有一点是我们需要特别注意的是下面三行代码,其具有非常重要的作用,我们将所分配的lwlock对象的前两个int地址中分别保存所期望的下一个ID和现有已分配的数量。

LWLockCounter = (int *) ((char *) LWLockArray – 2 * sizeof(int));
LWLockCounter[0] = (int) NumFixedLWLocks;
LWLockCounter[1] = numLocks;

其内存形式如下:
——————————————————————————————————————
NumFixedLWLocks | numLocks | lockID_1 | lockID_2 | …. | lockID_N | …
——————————————————————————————————————

当我们需要一个锁时候,我们首先会从LWLockArray 中获取一个可用的LockID然后将其分配,该操作由函数LWLockId LWLockAssign(void)来完成。其实现如下:

LWLockId LWLockAssign(void)
{
LWLockId result;
/* use volatile pointer to prevent code rearrangement */
volatile int *LWLockCounter;
LWLockCounter = (int *) ((char *) LWLockArray – 2 * sizeof(int));
//获取相应锁ID的首地址
SpinLockAcquire(ShmemLock);
if (LWLockCounter[0] >= LWLockCounter[1])
//判定是否有可用的ID
{
SpinLockRelease(ShmemLock);
elog(ERROR, “no more LWLockIds available”);
}
result = (LWLockId) (LWLockCounter[0]++);
//获取一个有效的ID
SpinLockRelease(ShmemLock);
return result;
}

在讨论完基本的操作后,我们将讨论lwlock的获取问题。 其由函数 void LWLockAcquire(LWLockId lockid, LWLockMode mode)来完成根据相应的条件获取一个lwlock,如果没有可用的lwlock则会导致系统sleep直到有可用的lwlock存在,而这也会导致cancel和die中断被屏蔽直到锁被释放。

对于LWLock的使用环境PostgreSQL 有如下的描述:

Since LWLocks are normally used to protect not-very-long sections of computation, a process needs to be able to acquire and release the same lock many times during a single CPU time slice, even in the presence of contention.

在函数执行时候,首先根据所请求锁的ID,从LWLockArray中获得所对于的锁对象, 由语句volatile LWLock *lock = &(LWLockArray[lockid].lock); 完成根据锁ID获取相应对象,然后将cancel 和die的的中断进行屏蔽;在完成上述操作后进入轮询部分,轮询是否具有可用的锁。 其具体的逻辑如下:

1) 首先,执行 SpinLockAcquire(&lock->mutex);来获得该琐上的mutex。

2)根据锁的类型做进一步的处理, 对于LW_EXCLUSIVE模式的锁,意味着该锁只能被持有一次,如果某个对象A持有该锁后,则另外一个对象B试图在申请获取该锁时候,则应该令对象B进行等待,直到其持有者-对象A使用完该锁并释放该锁,对象B方可获得对该锁的访问。

if (mode == LW_EXCLUSIVE) //如果锁的类型为LW_EXCLUSIVE模式则
{
if (lock->exclusive == 0 && lock->shared == 0)
{
lock->exclusive++;
mustwait = false;
}
else
mustwait = true;
}

当没有任何对象获得过该锁时候,则将 lock->exclusive设置为1, 表明该锁已经被使用,其它进程不能够在获得对该锁的访问, mustwait 用来描述是否需要进行轮询操作,当mustwait 为false时,表明已经获得想要的锁,可以退出轮询,否则继续进行轮询。当mode != LW_EXCLUSIVE 时,在lock->exclusive为非exclusive模式时,只需将该锁上的持有者的数量加 1并设置mustwait 以表明以成功获得对该锁的访问。
{
if (lock->exclusive == 0)
{
lock->shared++;
mustwait = false;
}
else
mustwait = true;
}

3)执行 SpinLockRelease(&lock->mutex); 释放mutext,同时将所获得的所锁的ID放入已获得链表中,以便后续进行查找。
SpinLockRelease(&lock->mutex);
held_lwlocks[num_held_lwlocks++] = lockid;

至此,我们分析完LWLock的获得过程。下面我们分别对函数 LWLockConditionalAcquire, LWLockAcquireOrWait 进行剖析。 从PostgreSQL中对于这两个函数描述分别为:

“LWLockConditionalAcquire – acquire a lightweight lock in the specified mode. If the lock is not available, return FALSE with no side-effects. If successful, cancel/die interrupts are held off until lock release.”

也就是说当我们使用LWLockConditionalAcquire 来申请一个锁时候,如果获得成功,则返回true并且将cancel和die中断进行屏蔽;否则,返回false。LWLockConditionalAcquire 实现上与 LWLockAcquire非常相似,只是没有了 LWLockAcquire函数中的轮询机制,只是进行一次的尝试,如果成功则获得所请求的锁并返回成功标志,否则返回失败标志。

对于LWLockAcquireOrWait函数PostgreSQL对其的描述如下:

“LWLockAcquireOrWait – Acquire lock, or wait until it’s free; The semantics of this function are a bit funky. If the lock is currently free, it is acquired in the given mode, and the function returns true. If the lock isn’t immediately free, the function waits until it is released nd returns false, but does not acquire the lock.”

从上文中的描述我们可以看出如果当前所请求锁是空闲的状态,则以所申请的模式将该锁分配并且函数返回true;否则,进行等待直到该锁被其持有者释放并且该函数返回false。该函数与上述函数有不同之处便是,即使当所申请的锁的持有者释放后,该该函数并不会获该锁,而是返回一个false。

下面我们就详细的分析一下该函数:该函数的原型为:bool LWLockAcquireOrWait(LWLockId lockid, LWLockMode mode),其中 lockid为所申请锁的ID,参数mode则表明申请所的模式:exclusive或者not exclusive.

1) 首先根据ID从LWLockArray中获得相应的LWLock对象, volatile LWLock *lock = &(LWLockArray[lockid].lock);

2)对cancel和die中断进行屏蔽,该项操作由宏 HOLD_INTERRUPTS(); 来完成对上述两个中断的屏蔽,直到该函数退出时候恢复这两个中断。

3)获得lock对象的mutex. SpinLockAcquire(&lock->mutex);

4)根据申请锁的类型更新 lock对象的状态。

if (mode == LW_EXCLUSIVE) // exclusive模式
{
//exclusive ==0表明该锁并没有被分配出去,shared ==0 其上所有共享该锁的对象为0
if (lock->exclusive == 0 && lock->shared == 0)
{
lock->exclusive++; //更新其状态为以分配
mustwait = false;
}
else
mustwait = true;
}
else //非exclusive模式
{
//当为非exclusive模式下,如果该锁没有被以exlusive模式分配,我们只需要对其 上所共享的对象数量进行加一,用以表明共享该锁的对象数量
if (lock->exclusive == 0)
{
lock->shared++; //增加共享该锁的对象数量
mustwait = false;
}
else
mustwait = true;
}

5) 判定是否需要执行等待操作。若mustwait 为false则表明用户已经获得其所申请的锁,此时系统可以返回用户,当mustwait 为true的情况下,表明该锁被其它对象所占用,此时需要进行等待直到所占用该锁的对象释放对该锁的占用,当系统在执行进行等待时候,将其等待的进程加入到锁的等待进程队列中。

if (mustwait)
{
proc->lwWaiting = true;
//设置进行的相关状态,进行等待。
proc->lwWaitMode = LW_WAIT_UNTIL_FREE;
proc->lwWaitLink = NULL;

if (lock->head == NULL) //将该进程加入到锁的等待队列中,链接到等待队列的末尾
lock->head = proc;
else
lock->tail->lwWaitLink = proc;
lock->tail = proc;
SpinLockRelease(&lock->mutex);
for (;;) //进行轮询其持有者已经释放对该锁的持有,若其持有者释放对该锁持有,则函数执行返回。
{
/* “false” means cannot accept cancel/die interrupt here. */
PGSemaphoreLock(&proc->sem, false);
//如果锁的持有者释放该锁后,proc->lwWaiting则该值为false,此时该函数等到该锁,此时函数应该退出并返回false。而 proc->lwWaiting则会在LWLockRelease函数中进行设置为false.
if (!proc->lwWaiting)
break;
extraWaits++;
} // End for
} else //mustwait 为false则获得该锁而返回true. 此处仅释放lock对象的mutex.
{
/* We are done updating shared state of the lock itself. */
SpinLockRelease(&lock->mutex);
}

6) 执行返回操作,若正确的获得所申请的锁对象,则将其加入其持有锁队列中并返回,否则返回false.

if (mustwait) //mustwait 为true
{
/* Failed to get lock, so release interrupt holdoff */
RESUME_INTERRUPTS();
}
else //为false情况,表明已经顺利的获得锁,此时应该将所获得的锁加入其持有锁的队列中。
{
/* Add lock to list of locks held by this backend */
held_lwlocks[num_held_lwlocks++] = lockid;
}
return !mustwait;

至此,我们对函数LWLockAcquireOrWait的分析完成。对于该函数的使用场景PG给出了如下的描述,供大家参考,在此不在多说。

“This is currently used for WALWriteLock: when a backend flushes the WAL, holding WALWriteLock, it can flush the commit records of many other backends as a side-effect. Those other backends need to wait until the flush finishes, but don’t need to acquire the lock anymore. They can just wake up, observe that their records have already been flushed, and return.”

在完成了LWlock的申请所有情况后,我们最后一个工作便是分析下锁的释放。在锁的持有者执行 LWLockRelease函数的调用后,将释放对该锁的持有。LWLockRelease函数的原型为: void LWLockRelease(LWLockId lockid) 其中lockid为所持有锁的ID。根据我们上述的加锁的分析我们可以大致得出:(1)对lock对象的exclusive,shared等状态信息更新;(2)对其等待队列上的进行进行处理 等大致的结论。下面我们就来详细的分析下LWLockRelease.
1) 根据所述锁的ID,由LWLockArray数组中获得 lock对象。

volatile LWLock *lock = &(LWLockArray[lockid].lock);

2)将其从锁持有队列中删除该该锁。

for (i = num_held_lwlocks; –i >= 0;) //由于该锁可能是刚刚加入的锁,因此这里使用了一个小技巧,从后往前查找。
{
if (lockid == held_lwlocks[i]) //查找出其所在的位置。
break;
}
num_held_lwlocks–; //更新持有锁的数量。
for (; i < num_held_lwlocks; i++) //执行删除操作。将该锁位置之后的锁,往前移动一位。
held_lwlocks[i] = held_lwlocks[i + 1];

3) 更新lock的状态,若该锁为exclusive模式,则将其复位,否则将共享对象的数量减一。

SpinLockAcquire(&lock->mutex);
/* Release my hold on lock */
if (lock->exclusive > 0) //表明其实exclusive模式
lock->exclusive–; //设置为0,若exclusive为1
else
{
Assert(lock->shared > 0);
lock->shared–; //将其共享该锁的对象数量减一。
}

4) 检查该锁上的等待队列中的进程,如果有需要唤醒的进程,则执行唤醒操作。

head = lock->head; //获得等待队列的首指针
if (head != NULL)
{
//判定是否可以执行释放操作,如果lock对象exclusive,shared 不为零则表明该锁仍然在使用。
if (lock->exclusive == 0 && lock->shared == 0 && lock->releaseOK)
{
bool releaseOK = true;
proc = head; //等待队列的首指针
//移动到等待队列到第一个lwWaitMode不为LW_WAIT_UNTIL_FREE的进程
while (proc->lwWaitMode == LW_WAIT_UNTIL_FREE && proc->lwWaitLink)
proc = proc->lwWaitLink;
//若等待队列想申请一个exclusive模式的锁,则将该进程单独唤醒;否则申请的共享模式的锁,此时应该将所有等待该锁的进程进行唤醒。
if (proc->lwWaitMode != LW_EXCLUSIVE) //为shared模式的锁,则唤醒队列上所有的
{
while (proc->lwWaitLink != NULL && proc->lwWaitLink->lwWaitMode != LW_EXCLUSIVE)
{
if (proc->lwWaitMode != LW_WAIT_UNTIL_FREE)
releaseOK = false;
proc = proc->lwWaitLink;
}
}
lock->head = proc->lwWaitLink; //将其从等待队列中移除
proc->lwWaitLink = NULL;
if (proc->lwWaitMode != LW_WAIT_UNTIL_FREE) //更新锁的标志
releaseOK = false;
lock->releaseOK = releaseOK;
}
else
{
/* lock is still held, can’t awaken anything */
head = NULL;
}
}

5) 通知等待队列上的所有相关的等待进程,其已经完成该锁的释放。

while (head != NULL)
{
LOG_LWDEBUG(“LWLockRelease”, lockid, “release waiter”);
proc = head;
head = proc->lwWaitLink;
proc->lwWaitLink = NULL;

proc->lwWaiting = false; //修改为false,表明该锁已经被释放,其它等待进程可以获得对该锁的申请,该参数在LWLockAcquireOrWait函数中被使用
PGSemaphoreUnlock(&proc->sem);
}

6) 恢复相关的中断并返回用户。

RESUME_INTERRUPTS();

至此,我们已经完成了对LWLock的分析。对于系统中的latch使用,将在另外的文档中进行描述。

在PostgreSQL中其中的全局变量MainLWLockArray中包含了系统中所使用到的各种类型的LWLock共42种类型。例如:有共享内存索引使用的 ShmemIndexLock,有Oid产生时候所使用的OidGenLock等等。 具体详细如下所示:

#define ShmemIndexLock (&MainLWLockArray[1].lock)
#define OidGenLock (&MainLWLockArray[2].lock)
#define XidGenLock (&MainLWLockArray[3].lock)
#define ProcArrayLock (&MainLWLockArray[4].lock)
#define SInvalReadLock (&MainLWLockArray[5].lock)
#define SInvalWriteLock (&MainLWLockArray[6].lock)
#define WALBufMappingLock (&MainLWLockArray[7].lock)
#define WALWriteLock (&MainLWLockArray[8].lock)
#define ControlFileLock (&MainLWLockArray[9].lock)
#define CheckpointLock (&MainLWLockArray[10].lock)
#define CLogControlLock (&MainLWLockArray[11].lock)
#define SubtransControlLock (&MainLWLockArray[12].lock)
#define MultiXactGenLock (&MainLWLockArray[13].lock)
#define MultiXactOffsetControlLock (&MainLWLockArray[14].lock)
#define MultiXactMemberControlLock (&MainLWLockArray[15].lock)
#define RelCacheInitLock (&MainLWLockArray[16].lock)
#define CheckpointerCommLock (&MainLWLockArray[17].lock)
#define TwoPhaseStateLock (&MainLWLockArray[18].lock)
#define TablespaceCreateLock (&MainLWLockArray[19].lock)
#define BtreeVacuumLock (&MainLWLockArray[20].lock)
#define AddinShmemInitLock (&MainLWLockArray[21].lock)
#define AutovacuumLock (&MainLWLockArray[22].lock)
#define AutovacuumScheduleLock (&MainLWLockArray[23].lock)
#define SyncScanLock (&MainLWLockArray[24].lock)
#define RelationMappingLock (&MainLWLockArray[25].lock)
#define AsyncCtlLock (&MainLWLockArray[26].lock)
#define AsyncQueueLock (&MainLWLockArray[27].lock)
#define SerializableXactHashLock (&MainLWLockArray[28].lock)
#define SerializableFinishedListLock (&MainLWLockArray[29].lock)
#define SerializablePredicateLockListLock (&MainLWLockArray[30].lock)
#define OldSerXidLock (&MainLWLockArray[31].lock)
#define SyncRepLock (&MainLWLockArray[32].lock)
#define BackgroundWorkerLock (&MainLWLockArray[33].lock)
#define DynamicSharedMemoryControlLock (&MainLWLockArray[34].lock)
#define AutoFileLock (&MainLWLockArray[35].lock)
#define ReplicationSlotAllocationLock (&MainLWLockArray[36].lock)
#define ReplicationSlotControlLock (&MainLWLockArray[37].lock)
#define CommitTsControlLock (&MainLWLockArray[38].lock)
#define CommitTsLock (&MainLWLockArray[39].lock)
#define ReplicationOriginLock (&MainLWLockArray[40].lock)
#define MultiXactTruncationLock (&MainLWLockArray[41].lock)
#define OldSnapshotTimeMapLock (&MainLWLockArray[42].lock)

Deduce A New EC Object.

诸位需要对于PostgreSQL中的EC(EquivalenceClass)类型以及 EM类型有一个初步的了解。了解查询引擎优化时候的相关数据结构的含义。虽然这些术语不是必须知道,但是对于这些术语的了解有助于对应PostgreSQL对于EC对象的推导有所帮助。

在函数 distribute_qual_to_rels 中的,1862 行中,当clause为subplan时候,我们创建了一个 restrictinfo: make_restrictinfo ,但是在接下来的语句中,我们判断该 restrictinfo 的mergeopfamilies时候,我们并为将该subplan中的 约束条件考虑进去(此处存在在优化可能,将subplan中的约束也考虑到,这样与fromlist中的进行同样考虑。)如本例中的:fromlist中的class.gno=”grade one” 与 subplan中的约束条件: student.classno= sub.classno。??? 是否是由于subplan为一个独立的部分????

if (restrictinfo->mergeopfamilies)
{
if (maybe_equivalence)
{

在process_equivalence函数中,当遍历完root->eq_classes后,我们会得到ec1和ec2两个变量的值。并根据ec1和ec2的值进行处理。

(1)当 ec1和ec2都不为null时候, 当 ec1 == ec2时候,表明这个两个EC对象是同一个等式。 例如: Col1= var1; Col1=Var;此时,将这个两个EC对 象合并; A= B和A=B的情况。 当ec1 != ec2时候,表明是两个不同的等式。此时将这个两个EC对象进行合并;Col1= var2; Col3 = var4;此时合并后的(合并到ec1中) Co11->em = col1->em —-> col3->em ; A= B和 C=D 合并后, (A,C) = (B,D)

(2)当ec1不为空时候,此时表明ec2为空,说明我们没有找到, 将em2添加到ec1中。例如: A= B 和 A=C 则合并后为A=(B=C).

( add item2 to ec1) em2 = add_eq_member(ec1, item2, item2_relids, item2_nullable_relids, false, item2_type);

(3) 当ec2不为空时候, (add item1 to ec2 ), 例如: A= B 和 D = B 则, (A=D)=B em1 = add_eq_member(ec2, item1, item1_relids, item1_nullable_relids, false, item1_type);

(4) 创建新的ec对象。

在sweep through过程中,通过两个循环遍历链表上的数据。eq_classes

ec
ec1 = ec2 = NULL;
em1 = em2 = NULL;
foreach(lc1, root->eq_classes)
{
EquivalenceClass *cur_ec = (EquivalenceClass *) lfirst(lc1);
ListCell *lc2;/* Never match to a volatile EC */
if (cur_ec->ec_has_volatile)
continue;
/*
* The collation has to match; check this first since it’s cheaper
* than the opfamily comparison.
*/
if (collation != cur_ec->ec_collation)
continue;
/*
* A “match” requires matching sets of btree opfamilies. Use of
* equal() for this test has implications discussed in the comments
* for get_mergejoin_opfamilies().
*/
if (!equal(opfamilies, cur_ec->ec_opfamilies))
continue;
foreach(lc2, cur_ec->ec_members)
{
EquivalenceMember *cur_em = (EquivalenceMember *) lfirst(lc2);
Assert(!cur_em->em_is_child); /* no children yet */
/*
* If below an outer join, don’t match constants: they’re not as
* constant as they look.
*/
if ((below_outer_join || cur_ec->ec_below_outer_join) && cur_em->em_is_const)
continue;
if (!ec1 && item1_type == cur_em->em_datatype && equal(item1, cur_em->em_expr))
{
ec1 = cur_ec;
em1 = cur_em;
if (ec2)
break;
}
if (!ec2 && item2_type == cur_em->em_datatype && equal(item2, cur_em->em_expr))
{
ec2 = cur_ec;
em2 = cur_em;
if (ec1)
break;
}
}
if (ec1 && ec2)
break;
}

因为已经将重复的em对象处理,因此在上述中找到一个相等的跳出循环进行进行处理即可,不用在进行后续的查找了。

在 generate_base_implied_equalities 函数中,我们将根据 EC中是否含有const变量来进行分类处理。当ec中含有const值时候,使用generate_base_implied_equalities_const来处理该ec,尝试产生推导后的等式。 当该ec中无相应的const值后,我们使用generate_base_implied_equalities_no_const来完成等式产生。

sc.cno = course.cno的EM对象的处理过程:

在 generate_base_implied_equalities_no_const 函数中,

ec2

例如本例中的:sc.cno 由于 sc 的rtindex为1, 故而会放在 prev_ems[1] 所指向的数组中。 而course.cno中的course的rtindex为5故而会放到 prev_ems[5]中。

在变量EC中的EM对象时候,当遇见prev_ems[x]中存在着EM对象时候,则我们可推导出现新的等式。 例如:当上图中的prev_em[5]中存在着 em1,em2,em3的等式后,则我们可以推导出: em1 = em2 , em1= em3, em2= em3这样的等式存在。 上述由 process_implied_equality 函数来完成。 而这也是数据库理论中的条件传递的功能。

prev_ems = (EquivalenceMember **)
palloc0(root->simple_rel_array_size * sizeof(EquivalenceMember *));

foreach(lc, ec->ec_members)
{
EquivalenceMember *cur_em = (EquivalenceMember *) lfirst(lc);
int relid;
Assert(!cur_em->em_is_child); /* no children yet */
if (!bms_get_singleton_member(cur_em->em_relids, &relid))
continue;
Assert(relid < root->simple_rel_array_size);
if (prev_ems[relid] != NULL)
{
EquivalenceMember *prev_em = prev_ems[relid];
Oid eq_op;
eq_op = select_equality_operator(ec,
prev_em->em_datatype,
cur_em->em_datatype);
if (!OidIsValid(eq_op))
{
/* failed… */
ec->ec_broken = true;
break;
}
process_implied_equality(root, eq_op, ec->ec_collation,
prev_em->em_expr, cur_em->em_expr,
bms_copy(ec->ec_relids),
bms_union(prev_em->em_nullable_relids,
cur_em->em_nullable_relids),
ec->ec_below_outer_join,
false);
}
prev_ems[relid] = cur_em;
}
pfree(prev_ems);
对于语法树中的每个quals对象,都会使用如下的语句来,创建一个 restrictinfo 对象。

restrictinfo = make_restrictinfo((Expr *) clause,
is_pushed_down,
outerjoin_delayed,
pseudoconstant,
relids,
outerjoin_nonnullable,
nullable_relids
);

PostgreSQL Bootstrap Explanation. -Bootstrap & BKI 详解

通常,多数的文章对于PostgreSQL系统在初始化过程中的了解多为:其使用bki类型的文件作为模版并将pg_xxx.h的头文件作为相应的catalog。但是文章中并未详细阐述系统在初始化过程中是如和完成bki文件到具体catalog的创建,初始数据的插入以及当我们在系统初始化时候,此时对于bki脚本文件这种类似于sql的脚本文件是对其进行解析和执行的。因为在执行initdb时候,存储引擎和查询引擎并未准备就绪,即便是存储引擎和查询引擎准备就绪,存储引擎和查询引擎也属于postgres进程,我们亦无法在initdb进程中直接使用postgres进程中的相关功能,那么我们又是以什么样的方式来使得initdb与postgres两进程之间的通信呢?

从PostgreSQL初始化的过程中,我们可以知道,此时的我们的存储引擎和查询引擎并未完成初始化和启动,那么在这bootstrap过程中我们又是完成对于bki脚本文件的解析和执行的呢?下面我们就来详细分析一下PostgreSQL的bootstrap机制。

我们知道,系统表是各个数据库中的最重要的元数据信息,系统表数据的丢失或者损害将可能导致整个系统不能正常启动或者正常的工作。通常,我们在使用initdb程序来创建一个初始的数据库库模版。在initdb的执行过程中将会创建PostgreSQL在执行过程中所需要的元数据表:pg_class, pg_am等元数据表。 那么很少人会思考,在initdb的过程中,我们需要做些什么样的工作?需要初始化并启动存储引擎和查询引擎吗?是否需要完成对于WAL以及buffer的模块的初始化和启动工作呢?

我们知道在src/include/catalog文件中存在大量的pg_xxxx.h形式的头文件,同时我们由这些头文件我们可以看出,这些.h的头文件正是我们在PostgreSQL中的catalog。例如:对于PostgreSQL中的保存系统中所有对象的系统表pg_class来说,我们从pg_class.h文件中可以看出, 其不但给出类该系统表的oid值,且给出了pg_class该系统表的结构信息,例如:系统表中每个属性的名称及类型,以及相应的初始数据信息。 下面我们就来详细的看看pg_class.h中的具体代码。

由pg_class.h中,我们可以看出,首先系统给出了pg_class系统表所具有的oid值,1259. 此时,可能读者就会想,我是否可以随便设置一个数字来表述pg_class的oid值,答案是,可以的。理论上,我们可以无原则的随便设置一个数字作为pg_class的oid值,但是理论比较是理论上的,在实际的操作过程并非无原则的。因为对于oid值,理论上也是有限的。我们无原则和无规律的使用最终会导致oid的枯竭,为了减少这种情况的发生。假使我们需要创建一个新的系统元数据表(在下面我们将给出详细介绍),我们可以使用 src/include/catalog目录下 unused_oids 来获取下一个未使用的oid或者使用 duplicate_oids 来测试是否存在着重复的oid值。

#define RelationRelationId 1259
#define RelationRelation_Rowtype_Id 83

在获得一有效的oid值后, 下面由CATALOG宏定义来给出关于该系统表的结构描述。 例如对于本例pg_class来说:

CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
{
NameData relname; /* class name */
Oid relnamespace; /* OID of namespace containing this class */
Oid reltype; /* OID of entry in pg_type for table’s * implicit row type */
Oid reloftype; /* OID of entry in pg_type for underlying * composite type */
Oid relowner; /* class owner */
Oid relam; /* index access method; 0 if not an index */
Oid relfilenode; /* identifier of physical storage file */
/* relfilenode == 0 means it is a “mapped” relation, see relmapper.c */
Oid reltablespace; /* identifier of table space for relation */
int32 relpages; /* # of blocks (not always up-to-date) */
float4 reltuples; /* # of tuples (not always up-to-date) */
int32 relallvisible; /* # of all-visible blocks (not always * up-to-date) */
Oid reltoastrelid; /* OID of toast table; 0 if none */
bool relhasindex; /* T if has (or has had) any indexes */
bool relisshared; /* T if shared across databases */
char relpersistence; /* see RELPERSISTENCE_xxx constants below */
char relkind; /* see RELKIND_xxx constants below */
int16 relnatts; /* number of user attributes *//*
* Class pg_attribute must contain exactly “relnatts” user attributes
* (with attnums ranging from 1 to relnatts) for this class. It may also
* contain entries with negative attnums for system attributes.
*/
int16 relchecks; /* # of CHECK constraints for class */
bool relhasoids; /* T if we generate OIDs for rows of rel */
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
TransactionId relminmxid; /* all multixacts in this rel are >= this.
* this is really a MultiXactId */

#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* NOTE: These fields are not present in a relcache entry’s rd_rel field. */
aclitem relacl[1]; /* access permissions */
text reloptions[1]; /* access-method-specific options */
#endif
} FormData_pg_class;

在给出相应的系统表的定义后,接下来就是所给出的用于bki脚本中,该元数据表中的初始数据。 而这些在pg_xxx.h中给出的系统表的原始数据将会被一系统工具正PostgrSQL编译和可执行程序的构建过程中被复制到bki文件中。

从上一段介绍中我们知道,bki文件将在PostgreSQL编译和可执行程序构建过程中由相应的系统工具构建。而这一工具就是:genbki.pl。其中, genbki.pl位于src/backend/catalog目标中,其为一个perl脚本文件。同样我们可以由该目录下的Makefile中所给出的描述来看出。

genbki
genbkicode
从genbki.pl中可以看出,其又会引用Catalog.pem文件中所给出的相应具体操作函数。 而Catalog.pem大致的功能为公共搜索指定目录下的所有pg_xxx.h类型的头文件并处理这些头文件:以这些头文件中所描述的元数据表中的信息来构建相应的bki脚本文件。 在正确的执行genbki.pl文件后,将会在src/backend/catalog中产生如下三个文件:(1)postgres.bki;(2)postgres.description;(3)postgres.shdescription。下面我们就来看一看postgres.bki中又包含了哪些东西。
bkidesc
在文章的开始部分,我们曾说在我们执行initdb进行初始化数据库时,将通过解析该bki文件来创建相应的初始数据库。当我们正确的完成Postgres的编译并在构建可执行程序时候,会将src/backend/catalog中的postgres.bki,postgres.descritpion以及postgres.shdescritpion复制到 share目录下。该目标下保存了我们在bootstrap时候所使用的相关文件。
bkishare
bkisharefolder

例如:除上述的三个文件外,还有相应的sql文件。 bki文件的相应形式如下,我们以pg_class为例,其经过genbki.pl处理后,在postgres.bki文件中所存在的形式如下:

create pg_class 1259 bootstrap rowtype_oid 83
(
relname = name ,
relnamespace = oid ,
reltype = oid ,
reloftype = oid ,
relowner = oid ,
relam = oid ,
relfilenode = oid ,
reltablespace = oid ,
relpages = int4 ,
reltuples = float4 ,
relallvisible = int4 ,
reltoastrelid = oid ,
relhasindex = bool ,
relisshared = bool ,
relpersistence = char ,
relkind = char ,
relnatts = int2 ,
relchecks = int2 ,
relhasoids = bool ,
relhaspkey = bool ,
relhasrules = bool ,
relhastriggers = bool ,
relhassubclass = bool ,
relrowsecurity = bool ,
relforcerowsecurity = bool ,
relispopulated = bool ,
relreplident = char ,
relfrozenxid = xid ,
relminmxid = xid ,
relacl = aclitem[] ,
reloptions = text[]
)
insert OID = 1247 ( pg_type 11 71 0 10 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1249 ( pg_attribute 11 75 0 10 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1255 ( pg_proc 11 81 0 10 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1259 ( pg_class 11 83 0 10 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ )
close pg_class

从上述的代码片段中我们可以,其非常类似于我们在创建关系表和进行数据插入时的sql脚本。这里需要请诸位读者特别注意 bootstrap 关键字,该关键用来描述该系统表在创建过程中所表现出行为。 一个系统表的定义带有该选项与不带有该选项表现出不同的行为特征,对于这点我们将在后续的文章中给出讨论。

上述文中,我们曾提及PostgreSQL是通过解析该bki并执行相应的操作来完成对于bootstrap过程中完成对于初始数据库的的构建。 在进一步的介绍PostgreSQL对该bki文件是如何解析和执行之前。 我们首先许多读者所关心的一个问题:如何创建一个定制化的系统表。下面,我们就来展示一下如果构建一个自定义的系统表。

首先,由上述的讨论可知,在src/include/backend/catalog目标中保存了相应的以pg_xxx.h为形式的头文件,而这些头文件正是我们说需要创建的系统表的描述。并通过src/backend/catalog目录中的genbki.pl来创新相应的bki文件,而该bki文件则用来在boostrap时候供initdb使用,用来构建相应的初始数据库。 假如我们需要创建一个名为pg_ext的系统表(为了方便起见,pg_ext具有与pg_class相同的表结构)。在完成在src/include/backend/catalog目录下添加一个名为pg_ext.h的头文件并使用unused_oid获的一个有效未使用的OID数值后,我们按照pg_class的形式,给出pg_ext系统表的定义如下:

CATALOG(pg_ext,3259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO {    NameData relname; /* class name */
Oid relnamespace; /* OID of namespace containing this class */
Oid reltype; /* OID of entry in pg_type for table’s * implicit row type */
Oid reloftype; /* OID of entry in pg_type for underlying * composite type */
Oid relowner; /* class owner */
Oid relam; /* index access method; 0 if not an index */
Oid relfilenode; /* identifier of physical storage file */
/* relfilenode == 0 means it is a “mapped” relation, see relmapper.c */
Oid reltablespace; /* identifier of table space for relation */
int32 relpages; /* # of blocks (not always up-to-date) */
float4 reltuples; /* # of tuples (not always up-to-date) */
int32 relallvisible; /* # of all-visible blocks (not always * up-to-date) */
Oid reltoastrelid; /* OID of toast table; 0 if none */
bool relhasindex; /* T if has (or has had) any indexes */
bool relisshared; /* T if shared across databases */
char relpersistence; /* see RELPERSISTENCE_xxx constants below */
char relkind; /* see RELKIND_xxx constants below */
int16 relnatts; /* number of user attributes */

/*
* Class pg_attribute must contain exactly “relnatts” user attributes
* (with attnums ranging from 1 to relnatts) for this class. It may also
* contain entries with negative attnums for system attributes.
*/
int16 relchecks; /* # of CHECK constraints for class */
bool relhasoids; /* T if we generate OIDs for rows of rel */
bool relhaspkey; /* has (or has had) PRIMARY KEY index */
bool relhasrules; /* has (or has had) any rules */
bool relhastriggers; /* has (or has had) any TRIGGERs */
bool relhassubclass; /* has (or has had) derived classes */
bool relispopulated; /* matview currently holds query results */
char relreplident; /* see REPLICA_IDENTITY_xxx constants */
TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
TransactionId relminmxid; /* all multixacts in this rel are >= this.
* this is really a MultiXactId */

#ifdef CATALOG_VARLEN /* variable-length fields start here */
/* NOTE: These fields are not present in a relcache entry’s rd_rel field. */
aclitem relacl[1]; /* access permissions */
text reloptions[1]; /* access-method-specific options */
#endif
} FormData_pg_ext;

这里BKI_BOOTSTRAP 参数说了,该系统表是否为bootstrap类型,当使用该参数后,我们需要明确的告知系统,需要在pg_class中插入一条关于该系统表的说明数据。当该系统表的类型为非bootstrap时,则无需告知系统而是由postgres自动的在创建该系统表之后,在pg_class中插入一条描述该系统表的数据。 同时,如果需要在创建该系统表后,指定该系统表中所含有的初始数据,则我们可以使用由DATA宏来进行描述。例如在我们创建pg_ext系统表之后,我们想使得该系统表具有初始数据,pg_type, pg_attribute 等,则我们可以按照pg_ext的表结构使用DATA宏定义来描述该数据。genbki.pl程序在生成postgres.

bki文件时候,将DATA宏所描述的信息转为为相应的语句操作,而DESCR则是相应的描述信息。

DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR(“”);
DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ ));
DESCR(“”);
DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR(“”);
DATA(insert OID = 1259 ( pg_ext PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ ));
DESCR(“”);

在pg_ext.h 文件中完成对于该系统表的定义后,我们使用genbki.pl来重新生成新的postgre.bki文件。 该命令的格式为:

genbki.pl Catalog.pm pg_xxx1.h pg_xxx2.h, …, pg_xxxN.h, pg_ext.h -I $(postgres_base_dir)/src/include/backend/catalog –set-version=9.6
其中,pg_xxxx.h 为 src/include/backend/catalog目录下所需要创建的系统表的头文件, 参数I用来表明路径信息,
而set-version则是用来指定版本信息,而该版本信息则会在initdb执行时候进行验证。 当该命令正确执行后,会在src/backend/catalog目录下创建postgres.bki等文件。

genbkimakefile

下面就是由genbki.pl处理过后postgres.bki文件中关于pg_ext的相关说明

create pg_ext 3259 bootstrap rowtype_oid 83
(
relname = name ,
relnamespace = oid ,
reltype = oid ,
reloftype = oid ,
relowner = oid ,
relam = oid ,
relfilenode = oid ,
reltablespace = oid ,
relpages = int4 ,
reltuples = float4 ,
relallvisible = int4 ,
reltoastrelid = oid ,
relhasindex = bool ,
relisshared = bool ,
relpersistence = char ,
relkind = char ,
relnatts = int2 ,
relchecks = int2 ,
relhasoids = bool ,
relhaspkey = bool ,
relhasrules = bool ,
relhastriggers = bool ,
relhassubclass = bool ,
relrowsecurity = bool ,
relforcerowsecurity = bool ,
relispopulated = bool ,
relreplident = char ,
relfrozenxid = xid ,
relminmxid = xid ,
relacl = aclitem[] ,
reloptions = text[]
)
insert OID = 1247 ( pg_type 11 71 0 10 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1249 ( pg_attribute 11 75 0 10 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1255 ( pg_proc 11 81 0 10 0 0 0 0 0 0 0 f f p r 29 0 t f f f f f f t n 3 1 _null_ _null_ )
insert OID = 1259 ( pg_ext 11 83 0 10 0 0 0 0 0 0 0 f f p r 31 0 t f f f f f f t n 3 1 _null_ _null_ )
close pg_ext

当我们所构建的系统表为非BKI_BOOTSTRAP , 则无需在pg_class中指定该关于该系统表的详细信息。

在我们正确的创建完bki文件后,将share目录中的bki文件替换为该文件,则我们在使用initdb进行数据库初始化时,我们会创建一个新的系统表pg_ext。

selectext

在介绍完如果创建自定义的系统表后,下面我们来分析一下postgres是如何依据该bki文件来构建初始数据库及其相应的元数据。

由initdb工程中的initdb.c中由main函数我们可以看出,在完成相关参数的设置后,将会执行initialize_data_directory函数,而在该函数中,又会进一步的执行函数bootstrap_template1来创建相应的初始数据库。那么bootstrap_template1函数中又完成那些工作呢? 由该函数中的bki_lines = readfile(bki_file); 代码可以看出,其首先会将bki文件中的内容进行读取,并将bki文件中的某些变量进行替换。此时,在完成对于这些变量的替换后,在bootstrap_template1函数中,我们将执行 PG_CMD_OPEN 操作,而由该宏定义可以知,其会进入到函数 popen_check中执行相应的操作。

我们由popen_check函数的定义可知,其第一个参数为一const char* 型命令行,系统将通过_popen函数来创建一个postgres进程。由popen函数的如下说明我们可以知道,正是通过调用该函数来完成启动一个postgres进程的操作。

“popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来 开启一个进程。这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose() 返回的终止状态与 shell 已执行 exit 一样。”

该函数的原型如下:
FILE * popen ( const char * command , const char * type ); int pclose ( FILE * stream );

而该函数的返回值为FILE* 类型,表明我们可以向使用文件系统一样,通过向该句柄中写入相应的数据来执行相应的操作。 bootstrap_template1函数中的如下代码就说明了一切:

PG_CMD_OPEN;
for (line = bki_lines; *line != NULL; line++)
{
PG_CMD_PUTS(*line);
free(*line);
}

PG_CMD_CLOSE;

而此时的变量cmd值为:”E:/postgres/bin/postgres.exe” –boot -x1 -F

bkidebug

经过该函数执行后,我们说创建的postgres进程将像一个文件系统一样接收我们通过FILE*所传递过来的命令来执行。

那么此时, 相应的问题又回到了postgres进程中:postgres以 –boot -x1 -F模式所启动的进程将被用来执行bki文件中所描述的bootstrap脚本。

我们在src/backend/main/main.c文件中的main函数中可以看出,当postgres启动的参数为boot时候,postgres进程将创建一个所谓的辅助进程来接手相应的处理工作。

if (argc > 1 && strcmp(argv[1], “–boot”) == 0)
AuxiliaryProcessMain(argc, argv); /* does not return */

在函数AuxiliaryProcessMain中,将依据上述相应的postgres进程创建时候所指定的参数来进行相应的处理。 在进行参数分解和辨识后,AuxiliaryProcessMain函数中将使用BaseInit 来进行基表参数的初始化工作。因为在某些参数需要在InitPostgres操作之前进行初始化工作。 在完成BaseInit操作后,AuxiliaryProcessMain函数继续依据-X所指定的类型来进行分类处理.
-X参数指明了该辅助进程所处于何种状态下,本例中 -X1表明了,当前的postgres进程是处于bootstrap状态下,因此AuxiliaryProcessMain 函数将进入到 bootstrap分支中。

bkisubmain

然后将具体的处理过程交由 BootstrapModeMain 函数来完成。 那么此时的问题又归结为BootstrapModeMain函数。下面我们就来看一看BootstrapModeMain函数,看看它又是如何完成执行bki文件呢?(由上述PG_CMD_PUTS(*line);代码可以,其是以一条一条的方式进行执行), 在完成 InitProcess(); 及InitPostgres(NULL, InvalidOid, NULL, InvalidOid, NULL); 操作后,函数来到 boot_yyparse, 这里对于lex/yacc比较熟悉的读者一眼就可以看出yyparse的作用。 对其就是对bki文件中的命令进行解析的工具,其实质上也是一个yacc。 到这里可能读者又会想到有一个这个boot_yyparse,那么肯定会有个套.l和.y文件与其相对应,用来描述相应的词法和语法规则。没错,在src\backend\bootstrap目录的bootscanner.l和bootparse.y文件正是描述bki文件中所出现的词法和语法规则。

下面,我们就以bki文件中create命令来分析一下,bootparse.y中描述的语法规则。由bootparse.y文件中我们找到 create语句的语法规则描述:

Boot_CreateStmt:
XCREATE boot_ident oidspec optbootstrap optsharedrelation optwithoutoids optrowtypeoid LPAREN
{
do_start();
numattr = 0;
elog(DEBUG4, “creating%s%s relation %s %u”,
$4 ? ” bootstrap” : “”,
$5 ? ” shared” : “”,
$2,
$3);
}

boot_column_list
{
do_end();
}

RPAREN
{
TupleDesc tupdesc;
bool shared_relation;
bool mapped_relation;
do_start();
tupdesc = CreateTupleDesc(numattr, !($6), attrtypes);
shared_relation = $5;
mapped_relation = ($4 || shared_relation);
if ($4)
{
if (boot_reldesc)
{
elog(DEBUG4, “create bootstrap: warning, open relation exists,
closing first”);
closerel(NULL);
}
boot_reldesc = heap_create($2,
PG_CATALOG_NAMESPACE,
shared_relation ? GLOBALTABLESPACE_OID : 0,
$3,
InvalidOid,
tupdesc,
RELKIND_RELATION,
RELPERSISTENCE_PERMANENT,
shared_relation,
mapped_relation,
true);
elog(DEBUG4, “bootstrap relation created”);
}
else
{
Oid id;
id = heap_create_with_catalog($2,
PG_CATALOG_NAMESPACE,
shared_relation ? GLOBALTABLESPACE_OID : 0,
$3,
$7,
InvalidOid,
BOOTSTRAP_SUPERUSERID,
tupdesc,
NIL,
RELKIND_RELATION,
RELPERSISTENCE_PERMANENT,
shared_relation,
mapped_relation,
true,
0,
ONCOMMIT_NOOP,
(Datum) 0,
false,
true,
false,
NULL);
elog(DEBUG4, “relation created with OID %u”, id);
}
do_end();
}
;

上述的XCREATE为对应的create关键字,而boot_ident则为我们所需要创建的系统表的名称,oidspec 为相应所指定系统表的oid值, optbootstrap则是我们boostrap类型,即在上述文中我们所提及的 BKI_BOOTSTRAP 参数。 从上述规则所对于的action中可以看出当 if ($4) 条件为true时,执行 heap_create 操作; 而当条件为false时,执行heap_create_with_catalog。 而$4正式我们在pg_ext.h和pg_class.h中所给出系统表的定义的相关参数,BKI_BOOTSTRAP。 当未给出BKI_BOOTSTRAP参数是,由函数heap_create_with_catalog创建相应的表,然后并将该基表信息插入到pg_class中;当给出BKI_BOOTSTRAP参数时,需要我们明确的在bki文件中给出相应的将基表信息插入到pg_class的语句。

到这里,我们相信诸位读者已经对于postgres的initdb执行由一个清晰而明确的认识了,上文我们详细的剖析了postgres在初始化过程中所发生的一切一切。