背景
PostgreSQL 后端进程经常需要打开大量的文件,包括表文件、索引文件、临时文件(用于排序或构造 hash 表)等。由于操作系统允许一个进程能够打开的文件数量是有上限的,为了防止后端进程打开文件时因超出 OS 的限制而失败,PostgreSQL 提供了 虚拟文件描述符 (VFD) 机制。VFD 抽象层能够对使用 VFD API 的代码屏蔽对 OS 文件描述符的管理细节,使更高层的代码能够打开比 OS 限制的数量更多的文件。
设计
VFD 机制最核心的设计,就是使用一个 LRU (Least-Recently-Used) 缓存池来管理当前进程所有的 操作系统文件描述符,同时对更上层的代码暴露出 虚拟文件描述符 (Virtual File Descriptor) 供使用。理想情况下,除了 VFD 的内部实现,其它部分内核代码不应该直接调用 C 库函数去操作文件。当 PostgreSQL 后端进程需要打开一个文件,且此时进程已经打开的文件数量将要超过 OS 允许的最大数量时,VFD 从其管理的 OS 文件描述符中选出最近最久未被使用的文件描述符并关闭它,此时打开这个文件就不会被 OS 拒绝。这个过程对使用 VFD API 的更高层代码来说是无感知的,仿佛可以不受数量限制地打开文件一样。
虚拟文件描述符
PostgreSQL 中,所有的 VFD 在物理上被组织成一个数组。VfdCache 是指向这个数组起始位置的指针,SizeVfdCache 保存这个数组的长度。这个数组的长度会随着对 VFD 需求量的增加而动态扩容。
nfile 变量记录了 VFD 数组中到底管理了多少个操作系统的文件描述符,这样 VFD 机制才能在打开的文件数量即将超出 OS 限制时,关闭最近最久未被使用的文件描述符。
/*
* Virtual File Descriptor array pointer and size. This grows as
* needed. 'File' values are indexes into this array.
* Note that VfdCache[0] is not a usable VFD, just a list header.
*/
static Vfd *VfdCache;
static Size SizeVfdCache = 0;
/*
* Number of file descriptors known to be in use by VFD entries.
*/
static int nfile = 0;虚拟文件描述符的结构定义如下:
typedef struct vfd
{
int fd; /* current FD, or VFD_CLOSED if none */
unsigned short fdstate; /* bitflags for VFD's state */
ResourceOwner resowner; /* owner, for automatic cleanup */
File nextFree; /* link to next free VFD, if in freelist */
File lruMoreRecently; /* doubly linked recency-of-use list */
File lruLessRecently;
off_t fileSize; /* current size of file (0 if not temporary) */
char *fileName; /* name of file, or NULL for unused VFD */
/* NB: fileName is malloc'd, and must be free'd when closing the VFD */
int fileFlags; /* open(2) flags for (re)opening the file */
mode_t fileMode; /* mode to pass to open(2) */
} Vfd;其中包括了 VFD 的状态信息:
fd用于保存真正的 OS 文件描述符,如果未使用,那么将会被设置为VFD_CLOSEDfdstate保存了 VFD 的状态标志位resowner表示这个 VFD 的持有者,方便后续的自动清理
此外,还包括 VFD 数组的管理信息。整个 VFD 数组被组织为两部分:LRU 池和空闲 VFD 列表。
LRU 池在逻辑上是一个双向链表,管理了所有正在持有 OS 文件描述符的 VFD。当一个 VFD 被使用后,它就会从 LRU 链表中移动到队头;此时 LRU 双向链表的尾部就是最近最久未被使用过的那个 VFD。
空闲 VFD 列表在逻辑上是一个单向链表。所有未被使用的 VFD 都会被串联在这个单链表中。被使用完毕释放的 VFD 也会被串回这个链表中。
上述两个部分虽然在逻辑上是双向或单向链表,但在形态上还是存放在 VFD 数组中,通过保存以下三个数组下标来代替链表本应该拥有的指针:
nextFree是下一个空闲 VFD 在数组中的下标,用于串联空闲 VFD 列表lruMoreRecently是后一个被使用过的 VFD 在数组中的下标lruLessRecently是前一个被使用过的 VFD 在数组中的下标
后两者相当于双向链表中的 prev 和 next 指针,用于串联 LRU 池。VFD 数组中的第一个元素 VfdCache[0] 永远不使用,该元素中的空间将会被分别作为两个链表的头指针。
最后剩下的几个信息,是真正向操作系统打开文件时传入的参数:
fileSize表示文件大小fileName表示文件名fileFlags表示打开文件时的标志位fileMode表示打开文件时的模式(权限)
低层函数
LruInsert / Insert
LruInsert() 函数会向 OS 申请真正打开 VFD 所对应的 OS 文件描述符,然后把这个 VFD 添加到 LRU 池的队头。LRU 池中的 VFD 是真正持有 OS 文件描述符的 VFD。把 VFD 移动到 LRU 池的队头是通过 Insert() 函数实现的,它会修改 VFD 的 LRU 双向链表指针。
LruDelete / Delete
LruDelete() 函数会真正释放 VFD 所持有的 OS 文件描述符,然后将 VFD 从 LRU 池中移除,因为这个 VFD 已经不再持有 OS 文件描述符了。移除动作是由 Delete() 函数实现的,它负责修改 VFD 的 LRU 指针,重新串联这个 VFD 之前和之后的 VFD。
ReleaseLruFile / ReleaseLruFiles
这两个函数负责不断关闭 LRU 池中最近最久未被使用的 VFD 所持有的 OS 文件描述符,直到将 LRU 池中的 VFD 数量控制到 OS 允许的安全范围以下。具体的实现方式就是调用上面的 LruDelete() 来关闭 OS 文件描述符并从 LRU 池中移除。
AllocateVfd / FreeVfd
这两个函数负责在 VFD 数组中占用一个空闲的 VFD(但并不打开底层的 OS 文件),以及归还 VFD。
在分配 VFD 时,如果 VFD 数组的空闲链表已经为空,那么就需要使用 realloc() 重新分配一个更大的 VFD 数组(通常是原 VFD 数组长度的两倍),并把新分配数组的后一半 VFD 初始化到空闲链表中以备未来使用。然后从空闲链表中摘下一个 VFD 并返回其下标。
归还 VFD 的过程很直接:将 VFD 恢复为初始状态,然后将其重新放回空闲链表中。
BasicOpenFile:Open 系统调用的替代者
这个函数封装了传统的 open() 系统调用。理论上,PostgreSQL 内核的其它部分不应再直接使用 open() 系统调用。这个函数将会返回一个裸的 OS 文件描述符,而不是 VFD——所以调用这个函数的代码需要保证这个文件描述符不会被泄露。
该函数内部真正调用了 open() 来获取一个 OS 文件描述符。如果失败,那么将会试图从 LRU 池中删除一个已有的文件描述符,然后再度重试,直到成功为止。
FileAccess:访问文件
每个高层文件访问接口都会调用 FileAccess()。调用这个函数意味着 VFD 持有的 OS 文件描述符将要被使用。那么:
如果这个文件在 OS 层面还没有被打开,那么调用
LruInsert()打开文件并将 VFD 插入 LRU 池如果这个文件已被打开,那么先将 VFD 从 LRU 池中移除,然后将 VFD 插入到 LRU 池的队头,表示这个 VFD 最近刚刚被访问
高层文件操作接口
打开文件
PathNameOpenFilePerm() 将会根据参数打开文件,并返回一个 VFD。经历了以下步骤:
调用
AllocateVfd()从 VFD 数组中拿到一个空闲 VFD 并初始化调用
ReleaseLruFiles()把 LRU 池中的 VFD 减少到操作系统允许的水平调用
BasicOpenFilePerm()打开一个 OS 文件描述符,并关联到新分配的 VFD 上调用
Insert()把新分配的 VFD 添加到 LRU 池中返回新分配的 VFD
关闭文件
FileClose() 将会关闭一个 VFD 所对应的一切:
如果 VFD 对应的 OS 文件描述符已被打开,那么调用
close()关掉它,然后调用Delete()从 LRU 池里移除这个 VFD如果 VFD 对应的文件被设置了 关闭时删除 的标志(临时文件),那么调用
unlink()删掉它!调用
FreeVfd()清空这个 VFD 并重新归还到 VFD 数组的空闲链表中
其它文件操作
除去文件的打开与关闭以外,其它文件操作需要基于 文件已被打开 的假设进行。这些文件操作都被 PostgreSQL 内核做了一层封装。在进行真正的文件操作之前,需要先使用打开文件后持有的 VFD 调用一次 FileAccess() 函数。这个函数能够保证:
VFD 内部持有的 OS 文件描述符已经打开(如果没打开,那就立刻打开,或许会导致其它 OS 文件描述符被关闭)
VFD 在 LRU 池中的位置移动到队头,因为这个 VFD 对应的 OS 文件描述符最近被使用了
以文件库函数 read() 的包装 FileRead() 为例:
int
FileRead(File file, char *buffer, int amount, off_t offset,
uint32 wait_event_info)
{
int returnCode;
Vfd *vfdP;
Assert(FileIsValid(file));
DO_DB(elog(LOG, "FileRead: %d (%s) " INT64_FORMAT " %d %p",
file, VfdCache[file].fileName,
(int64) offset,
amount, buffer));
returnCode = FileAccess(file);
if (returnCode < 0)
return returnCode;
vfdP = &VfdCache[file];
retry:
pgstat_report_wait_start(wait_event_info);
returnCode = pread(vfdP->fd, buffer, amount, offset);
pgstat_report_wait_end();
if (returnCode < 0)
{
/* OK to retry if interrupted */
if (errno == EINTR)
goto retry;
}
return returnCode;
}总结
PostgreSQL 内核中的 VFD 机制用于防止 PostgreSQL 后端进程受 OS 对进程打开文件数量的限制。VFD 内部维护了一个 LRU 池来管理所有被打开的 OS 文件描述符。使用 VFD 的高层接口来操作文件,就可以享受到 VFD 为我们屏蔽掉的文件描述符管理所带来的的便利。
VFD 的实现思想与操作系统的进程调度有些类似。OS 上的进程有成百上千个,而 CPU 只有一个(或几个)。从使用者的角度看,这些进程似乎都在同时执行,只有 OS 知道每一个时刻只有一个进程在一个 CPU 核心上运行;在 PostgreSQL 中,类似地,从 VFD 使用者的角度看,似乎能够同时持有远超操作系统数量限制的文件描述符,但只有 VFD 知道,每一个时刻打开的 OS 文件描述符数量必定小于操作系统对进程打开文件数量的限制。
一招瞒天过海。




