由于每个操作系统限制了一个进程能打开的文件数(例如:ubuntu 为1024),因此进程能获得的文件描述符是有限的。对于经常需要打开许多文件的数据库进程来说,很容易会超过操作系统对于文件描述符数量的限制。
为解决这个问题,PostgreSQL 中使用了虚拟文件描述符 (VFD) 机制,通过 VFD 管理真实的文件描述符,帮助进程摆脱操作系统的限制。typedef struct vfd
typedef struct vfd
{
int fd;
unsigned short fdstate;
ResourceOwner resowner;
File nextFree;
File lruMoreRecently;
File lruLessRecently;
off_t fileSize;
char *fileName;
int fileFlags;
mode_t fileMode;
} Vfd;
VDF 中各字段含义:
-
fd 记录该 VFD 所对应的真实文件描述符,如果当前 VFD 没有打开文件,则其值为 VFD_CLOSED (-1)。
-
fdstate 标记位:如 FD_DELETE_AT_CLOSE,表明该文件在关闭时要被删除。
-
nextFree:指向下一个空闲的 VFD,其数据类型 File 表示其在 VFD 数组中的下标。
-
lruMoreRecently:指向比该 VFD 最近更常用的虚拟文件描述符。
-
lnuLessRecently:指向比该 VFD 最近更不常用的虚拟文件描述符。
-
fileSize:当前文件大小。
-
fleName:该 VFD 对应文件的文件名,如果是空闲的 VFD 则 fileName 为空。
-
fileFlags:该文件打开时的标志,包括只读、只写、读写等。
-
fileMode:文件创建时所指定的模式。
VFD 数组 VfdCache 作为 LRU 池管理文件描述符,并根据需要打开和关闭实际的 OS 文件描述符。LRU 池使用数组实现,数组元素是 VFD 结构体,数组大小会根据需要增长(最大1024)。
static Vfd *VfdCache;
static Size SizeVfdCache = 0;
当 LRU 池未满时,进程可以申请一个 VFD 用来打开物理文件;而当 LRU 池已满时,进程需要首先关闭一个 VFD。在 LRU 中,使用替换最长时间未使用 VFD 策略。
进程在 VfdCache 上保持了两个链表,一个是 LRU 池(双向链表),另一个是 FreeList (空闲链表,记录了所有可被分配的 VFD)。前者通过 lruMoreRecently 属性和 lruLessRecently 属性来链接,后者则通过 nextFree 属性来链接。
VfdCache [0] 不是可用的 VFD,它仅用来标识 FreeList 和 LRU 池的链表头部。对 LRU 池里的 VFD 的操作主要包括以下三种:
(1) 将 VFD 插入 LRU 池
打开一个新的 VFD 时,会将该 VFD 对应的物理文件打开,并将该 VFD 插入到 VfdCache[O] 之后的位置。例如,我们要插入的 VFD 为 a,首先要根据 a 中的 fd 字段判断对应的物理文件是否已经打开。如果没有打开则根据 a 中的 fileName 打开此文件,并插入到 LRU 池中。
插入时要将 a 的 lruMoreRecently 指向 VfdCache[0],VfdCache[0] 的 lruLessRecently 指向 a,然后 a 的 lruLessRecently 指向 V1,V1 的 lruMoreRecently 指向 a。
(2) 从 LRU 池删除 VFD
进程使用完一个文件并关闭它时,将该文件的 VFD 从 LRU 池中删除,并将该 VFD 对应的文件关闭掉。例如,我们要对 V2 操作,则首先将 V1 的 lrulessRecently 指向 V3,将 V3 的 lruMoreRecently 指向 V1。
这样就将 V2 从 LRU 池中删除了,如果V2 的fdstate 被置为 FD_DELETE_AT_CLOSE,则要先将V2对应的文件删除,再清掉FD_DELETE_AT_CLOSE 位,接着将 fileSize 置为 0,将 fd 置为 VFD_CLOSED。这样 V2 就变为空闲,最后将其加入到空闲链表中。
(3) 删除 LRU 池尾 VFD
当 LRU 池已满,而此时又要打开新的文件时,就需将池尾的 VFD 删掉,这样新打开的 VFD 就可以插入到 LRU 中。注意,这里被删除的 VFD 仅仅只是从 LRU 池中脱链并关闭其对应的物理文件,VFD 本身并不做其他修改和删除。