一,VFD概述

操作系统对于单个进程能打开的文件数有限制,而数据库系统的进程经常需要打开很多文件进行操作。为了避免这个限制,PostgreSQL实现了VFD(虚拟文件描述符)机制。其实质并没有改变操作系统的限制,只是每个进程维护了一个自己的LRU(Least Recently Used,近期最少使用算法)池来管理本进程打开的所有VFD。当进程需要打开文件时,就从该池中申请VFD,池中每个每个VFD都对应一个物理上已经打开的文件。

当LRU池未满时(即进程打开的文件数位超过系统限制),进程可以照常申请一个VFD用来打开一个物理文件;而当LRU池已满的时候,进程需要先从池中删除一个VFD并关闭其物理文件,这样打开新的文件时就不会因为超出操作系统的限制而打开文件失败。在LRU池中,使用替换最长时间未使用的VFD的策略。

二,VFD机制详述

1. VFD结构

PostgreSQL使用一个Vfd结构体来描述一个VFD虚拟文件描述符:

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        seekPos;        /* current logical file position */
    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 */
    int            fileMode;        /* mode to pass to open(2) */
} Vfd;
  • fd记录该VFD所对应的物理文件描述符。如果当前VFD没有打开文件描述符(即没有对应的物理文件描述符),则其值为VFD_CLOSED(VFD_CLOSED=-1)。
  • fdstate是VFD的标志位:①如果它的第0位置1,即为FD_DIRTY,表明该文件的内容已被修改过,但还没有写回磁盘,在关闭此文件是要将该文件同步到磁盘里。②如果它的第1位置1,即为FD_TEMPORARY,表明该文件是临时文件,需要在关闭时删除。
  • nextfree指向下一个空闲的VFD,其数据类型File其实是一个整数(不是<stdio.h>里面的FILE), 表示VFD在VFD数组中的下标。
  • lruMoreRecently指向比该VFD最近更常用的VFD。
  • lruLessRecently指向比该VFD最近更不常用的VFD。
  • seekPos记录该VFD的当前读写指针的位置。
  • fileName表示该VFD对应文件的文件名,如果是空闲的VFD,则fileName位空值。
  • fileFlags表示该文件打开时的标志,包括只读、只写、读写等。
  • fileMode表示文件创建时所指定的模式。

2. VfdCache链表

/*
 * 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;

VfdCache是由Vfd结构构成的一个数组,数组大小为SizeVfdCache。每个进程在VfdCache上面维护了两个链表:一个是LRU池——由Vfd结构中的lruMoreRecently和lruLessRecently链在一起的双向链表/环;一个FreeList链表——由Vfd结构中的nextfree链在一起的单链表。比较特殊的是VfdCache[0]这个元素,它实际并不给任何VFD使用,它只是LRU环的头部和尾部,以及FreeList的头部。两者的结构分别如下图:

lru_ring

free_list

比如上图中的a1、a2、a3、an代表VFD,不同的VFD使用lruLessRecently和lruMoreRecently链接在一起。而且VfdCache[0]是lruLessRecently的头部,是lruMoreRecently的尾部,也就是说a1是最近使用最多的,an是最近使用最少的。其实当我们从FreeList中获取一个VFD后,就是链在VfdCache[0]后面,认为它是最近刚使用过的。LRU池的大小与操作系统对于进程打开文件数的限制是一致的。在PostgreSQL的实现中使用全局变量max_safe_fds来记录该限制数(默认值是32),在Postmaster进程的启动过程中会调用set_max_safe_fds函数来检测操作系统限制,并设置max_safe_fds的值。

3. VFD的分配和回收流程

(1)当后台进程启动时(一个客户端对应一起后台进程),会调用InitFileAccess函数创建VfdCache头,即VfdCache[0].

void
InitFileAccess(void)
{
    Assert(SizeVfdCache == 0);  /* call me only once */

    /* initialize cache header entry */
    VfdCache = (Vfd *) malloc(sizeof(Vfd));
    if (VfdCache == NULL)
        ereport(FATAL,
                (errcode(ERRCODE_OUT_OF_MEMORY),
                 errmsg("out of memory")));

    MemSet((char *) &(VfdCache[0]), 0, sizeof(Vfd));
    VfdCache->fd = VFD_CLOSED;

    SizeVfdCache = 1;

    /* register proc-exit hook to ensure temp files are dropped at exit */
    on_proc_exit(AtProcExit_Files, 0);
}

(2)进程打开第一个文件时(调用AllocateVfd函数),将初始化VfdCache数组,置其大小为32,为其中每一个Vfd结构分配内存空间,将Vfd结构中的fd字段置为VFD_CLOSED,并将所有元素放在FreeList上。分配一个VFD,即从FreeList头取一个Vfd,并打开该文件,将该文件的相关信息(包括物理文件描述符、文件名、各种标志等)记录在分配的Vfd中。若FreeList上没有空闲的VFD,则将VfdCache数组扩大一倍,新增加的VFD放入FreeList链表中。

static File
AllocateVfd(void)
{
    Index       i;
    File        file;

    DO_DB(elog(LOG, "AllocateVfd. Size %lu", SizeVfdCache));

    Assert(SizeVfdCache > 0);    /* InitFileAccess not called? */

    if (VfdCache[0].nextFree == 0)
    {
        /*
         * The free list is empty so it is time to increase the size of the
         * array.  We choose to double it each time this happens. However,
         * there's not much point in starting *real* small.
         */
        Size        newCacheSize = SizeVfdCache * 2;
        Vfd        *newVfdCache;

        if (newCacheSize < 32)
            newCacheSize = 32;

        /*
         * Be careful not to clobber VfdCache ptr if realloc fails.
         */
        newVfdCache = (Vfd *) realloc(VfdCache, sizeof(Vfd) * newCacheSize);
        if (newVfdCache == NULL)
            ereport(ERROR,
                    (errcode(ERRCODE_OUT_OF_MEMORY),
                     errmsg("out of memory")));
        VfdCache = newVfdCache;

        /*
         * Initialize the new entries and link them into the free list.
         */
        for (i = SizeVfdCache; i < newCacheSize; i++)
        {
            MemSet((char *) &(VfdCache[i]), 0, sizeof(Vfd));
            VfdCache[i].nextFree = i + 1;
            VfdCache[i].fd = VFD_CLOSED;
        }
        VfdCache[newCacheSize - 1].nextFree = 0;
        VfdCache[0].nextFree = SizeVfdCache;

        /*
         * Record the new size
         */
        SizeVfdCache = newCacheSize;
    }

    file = VfdCache[0].nextFree;

    VfdCache[0].nextFree = VfdCache[file].nextFree;

    return file;
}

(3)关闭文件时,将该文件所对应的VFD插入到FreeList的头部。

static void
FreeVfd(File file)
{
    Vfd        *vfdP = &VfdCache[file];

    DO_DB(elog(LOG, "FreeVfd: %d (%s)",
               file, vfdP->fileName ? vfdP->fileName : ""));

    if (vfdP->fileName != NULL)
    {
        free(vfdP->fileName);
        vfdP->fileName = NULL;
    }
    vfdP->fdstate = 0x0;

    vfdP->nextFree = VfdCache[0].nextFree;
    VfdCache[0].nextFree = file;
}

(4)进程获取到VFD之后,需要检查LRU池是否已满,也就是说检查当前进程所打开的物理文件个数是否已经达到了操作系统的限制。如果没有超过限制,进程可以使用该VFD打开物理文件并将其插入到LRU池中;否则需要使用LRU池替换算法,先关闭一个VFD及其所对应的物理文件,然后再使用获得的VFD来打开物理文件。

File
PathNameOpenFile(FileName fileName, int fileFlags, int fileMode)
{
    char       *fnamecopy;
    File        file;
    Vfd        *vfdP;

    DO_DB(elog(LOG, "PathNameOpenFile: %s %x %o",
               fileName, fileFlags, fileMode));

    /*
     * We need a malloc'd copy of the file name; fail cleanly if no room.
     */
    fnamecopy = strdup(fileName);
    if (fnamecopy == NULL)
        ereport(ERROR,
                (errcode(ERRCODE_OUT_OF_MEMORY),
                 errmsg("out of memory")));

    file = AllocateVfd();
    vfdP = &VfdCache[file];

    while (nfile + numAllocatedDescs >= max_safe_fds)
    {
        if (!ReleaseLruFile())
            break;
    }

    vfdP->fd = BasicOpenFile(fileName, fileFlags, fileMode);

    if (vfdP->fd < 0)
    {
        FreeVfd(file);
        free(fnamecopy);
        return -1;
    }
    ++nfile;
    DO_DB(elog(LOG, "PathNameOpenFile: success %d",
               vfdP->fd));

    Insert(file);

    vfdP->fileName = fnamecopy;
    /* Saved flags are adjusted to be OK for re-opening file */
    vfdP->fileFlags = fileFlags & ~(O_CREAT | O_TRUNC | O_EXCL);
    vfdP->fileMode = fileMode;
    vfdP->seekPos = 0;
    vfdP->fileSize = 0;
    vfdP->fdstate = 0x0;
    vfdP->resowner = NULL;

    return file;
}

4. LRU池操作

对LRU池的操作其实就是动态的维护链表的一些插入,删除等操作:

  • Delete - delete a file from the Lru ring
  • LruDelete - remove a file from the Lru ring and close its FD
  • Insert - put a file at the front of the Lru ring
  • LruInsert - put a file at the front of the Lru ring and open it
  • ReleaseLruFile - Release an fd by closing the last entry in the Lru ring
  • AllocateVfd - grab a free (or new) file record (from VfdArray)
  • FreeVfd - free a file record
    因为函数都比较简单,这里就不再赘述了。

三,函数调用图

一方面,PostgreSQL为了打破OS对进程可打开的文件描述符的限制而实现了VFD机制,就是本篇博客主要讲的这个。另一方面,为了防止文件描述符(又称文件句柄)的泄露,它在标准C库和POSIX C库基础上封装了一些文件操作函数,这些函数很多都可以在事务结束时释放事务内打开的文件描述符。所以,如果我们做PostgreSQL的内核开发,在文件操作方面,应该尽可能使用这些封装的API,而不是原生的C API。最后来一张函数调用图:

pg_fd

这些函数都在PostgreSQL源码的fd.c中,有兴趣的可以看一下。

本文参考自《PostgreSQL数据库内核分析》及PostgreSQL源码。