您的位置:首页 > 财经 > 产业 > 哈尔滨建设工程招投标信息网_北京装饰公司排行榜_广告营销推广方案_北京千锋教育培训机构怎么样

哈尔滨建设工程招投标信息网_北京装饰公司排行榜_广告营销推广方案_北京千锋教育培训机构怎么样

2025/3/4 21:02:47 来源:https://blog.csdn.net/Hehuyi_In/article/details/105600145  浏览:    关键词:哈尔滨建设工程招投标信息网_北京装饰公司排行榜_广告营销推广方案_北京千锋教育培训机构怎么样
哈尔滨建设工程招投标信息网_北京装饰公司排行榜_广告营销推广方案_北京千锋教育培训机构怎么样

 首先VFD是Virtual File Descriptor,即虚拟文件描述符,既然是虚拟的,一定先有物理的。

一、 物理文件描述符(File Descriptor, FD)

1.  什么是 FD

       它是操作系统提供给用户程序访问和操作文件或其他 I/O 资源的抽象接口,使用户无需关心底层的具体实现。

       在 Linux 中,文件描述符是一个非负整数(通常从 0 开始)。每个进程都有一个独立的文件描述符表,表中每一项指向一个打开的文件或其他 I/O 资源(如管道、套接字等)。

2. 为什么需要设置FD上限

① 防止无限制打开FD
  • 防止恶意程序:如果没有文件描述符的上限限制,恶意程序可能会通过打开大量文件描述符来耗尽系统资源,导致系统无法正常运行。

  • 防止程序错误:程序中的错误(如未关闭文件描述符)可能会导致文件描述符泄漏,最终耗尽系统资源。通过设置上限,可以尽早发现并处理这些问题。

② 资源限制
  • 内存资源:每个文件描述符都需要占用一定的内核内存资源(如文件描述符表、文件表、inode 表等)。如果不加限制,可能会导致系统内存耗尽,影响系统的稳定性。

  • 内核性能:操作系统内核需要为每个文件描述符维护一些数据结构。如果文件描述符数量过多,可能会导致内核数据结构过大,影响系统性能。

  • 进程性能:每个进程的文件描述符表需要在内核中维护,如果文件描述符数量过多,可能会导致进程切换时的性能下降。

  • 防止系统崩溃:如果文件描述符数量过多,可能会导致系统资源耗尽,进而导致系统崩溃或不可用。通过设置上限,可以确保系统的稳定性。

3. FD的上限设置

在 Linux 中,文件描述符设置分为三层:

  • 系统级限制:操作系统内核通常会设置一个全局的文件描述符上限(如 /proc/sys/fs/file-max),限制整个系统能够打开的文件描述符数量。

  • 用户级限制:每个用户可以打开的文件描述符数量也受到限制(如 ulimit -n)。

  • 进程级限制:每个进程能够打开的文件描述符数量也受到限制(通常为 1024 或更高,/proc/sys/fs/file-max)。

二、 为什么需要VFD

        在看到FD设置上限原因的时候,我觉得其实非常像数据库中的连接数。对于应用而言,业务连接通常远超DB上限,希望DB连接数可以近似无限;对DB而言,连接数过高又可能导致内存占用过多、连接管理消耗资源过多等问题。

        在DB中,解决这个矛盾的方法是引入连接池。而确实,VFD之于FD就是类似连接池的功能

        操作系统对单个进程能够打开的文件描述符数量有限制(通常为 1024),可是对PG进程而言,这个值远远不够,而频繁申请和关闭FD又过度消耗资源,影响性能。参考连接池的优势,可以很方便地理解VFD的作用:

  • 逻辑上突破FD上限:使得对PG而言,进程可以打开的文件数近乎无限
  • 高效资源管理:避免频繁打开和关闭文件,从而减少系统调用的开销
  • 缓存与资源回收:VFD 机制结合了 LRU策略,确保最不常用的文件描述符能够被及时释放,从而为新的文件操作腾出空间。同时可以缓存常用的文件描述符,再度提升性能。

三、核心数据结构

1. VFD的核心机制

  • PG中的通过全局的 VfdCache(类似连接池)来管理VFD(连接池中的每个连接)
  • 每个 VFD 对应一个实际的FD(真正的连接)
  • PG 通过 LRU 策略来管理这些 VFD,确保能够及时释放不常用的 VFD

这里我们先看看每部分的数据结构长什么样

2. 单个VFD

VFD 的数据结构定义如下(位于 src/include/storage/fd.c):

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;

主要字段含义

  • fd:当前 VFD 对应的真正FD。如果为-1,则表示该 VFD 当前未分配。
  • fdstate:VFD 的状态标志(如是否正在使用、是否可关闭等)。
  • owner:该 VFD 的资源所有者,用于资源管理。
  • nextFree:指向下一个空闲的 VFD,用于空闲 VFD 链表的管理。
  • lruMoreRecently和lruLessRecently:用于 LRU 链表的双向链接,分别指向最近更多和更少使用的 VFD。

 可以看出,整个 VFD 数组被组织为两部分:空闲VFD列表 和 LRU池。

typedef struct vfd
{
...File		nextFree;		/* link to next free VFD, if in freelist */File		lruMoreRecently;	/* doubly linked recency-of-use list */File		lruLessRecently;
...
} Vfd;
  • 空闲 VFD 列表在逻辑上是一个单向列表。所有未被使用的 VFD 都会被串联在这个单链表中。被使用完毕释放的 VFD 也会被串回这个链表中。

  • LRU 池在逻辑上是一个双向链表,链表的头部是最近使用的 VFD,尾部是最少使用的 VFD。每个 VFD 通过lruMoreRecently和lruLessRecently指针连接到链表中

    3. VfdCache

          如前所说,VfdCache类似连接池,而VFD类似连接池中的每个连接,因此VfdCache实际就是由VFD组成的一个数组。

           nfile变量记录了 VFD 数组中实际使用了多少个FD,这样 VFD 机制才能在打开的文件数量即将超出 OS 限制时,关闭最近最久未被使用的FD。

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

          ​​​​​ 

    四、 VFD中的LRU机制

           LRU 池是一个双向链表,链表的头部是最近使用的 VFD,尾部是最少使用的 VFD。每个 VFD 通过lruMoreRecently和lruLessRecently指针连接到链表中。

    1. 初始化

    ① Postmaster进程启动时

    计算LRU池最多可以打开的文件数 max_safe_fds,这个值的上限跟真正的进程FD上限一致。

    /** set_max_safe_fds*		Determine number of file descriptors that fd.c is allowed to use*/
    void
    set_max_safe_fds(void)
    {int			usable_fds;int			already_open;/*----------* We want to set max_safe_fds to*			MIN(usable_fds, max_files_per_process - already_open)* less the slop factor for files that are opened without consulting* fd.c.  This ensures that we won't exceed either max_files_per_process* or the experimentally-determined EMFILE limit.*----------*/count_usable_fds(max_files_per_process,&usable_fds, &already_open);max_safe_fds = Min(usable_fds, max_files_per_process - already_open);/** Take off the FDs reserved for system() etc.*/max_safe_fds -= NUM_RESERVED_FDS;/** Make sure we still have enough to get by.*/if (max_safe_fds < FD_MINFREE)ereport(FATAL,(errcode(ERRCODE_INSUFFICIENT_RESOURCES),errmsg("insufficient file descriptors available to start server process"),errdetail("System allows %d, we need at least %d.",max_safe_fds + NUM_RESERVED_FDS,FD_MINFREE + NUM_RESERVED_FDS)));elog(DEBUG2, "max_safe_fds = %d, usable_fds = %d, already_open = %d",max_safe_fds, usable_fds, already_open);
    }

    ② backend启动时

           VfdCache 的初始化函数会在每个backend启动时调用,为 VfdCache 数组分配内存,并将fd变量均设置为未使用。 SizeVfdCache=1 表示 VfdCache 数组中当前有一个 VFD,这个值用于跟踪缓存的大小,确保PG知道当前有多少个VFD被分配和管理。

           此时 VfdCache 并不包含任何有效的VFD,VfdCache 的第一个元素 VfdCache[0] 被用作双向链表的头节点,这个节点不会存储实际的 VFD。

    /** InitFileAccess --- initialize this module during backend startup** This is called during either normal or standalone backend start.* It is *not* called in the postmaster.*/
    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);
    }

           这里个人有个小疑问咨询了下AI,先记录下结果

    2. 进程打开/关闭文件过程概览

    ① 当进程打开文件时:分配Vfd,在VfdCache中通过nextFree指针查找空闲的VFD。

    • 如果能找到,将文件元信息记录至Vfd中,继续下一步打开文件
    • 如果没有空闲的 VFD,调用AllocateVfd函数,启动扩容机制。初始VfdCache size 是 32,每次扩容为原来的 2 倍。并将新增的VFD加入FreeList
    static File
    AllocateVfd(void)
    {Index		i;File		file;DO_DB(elog(LOG, "AllocateVfd. Size %zu", 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;
    }

    ② 进程获得VFD后,检查LRU池是否已满,即是否还有可用FD

    • 如果未满,使用该VFD打开物理文件,并调用LruInsert函数(再调用Insert函数)将VFD插入LRU池。这个函数本质只做一件事,将新的VFD插到最常使用的链头。

    static void
    Insert(File file)
    {Vfd		   *vfdP;Assert(file != 0);DO_DB(elog(LOG, "Insert %d (%s)",file, VfdCache[file].fileName));DO_DB(_dump_lru());vfdP = &VfdCache[file];vfdP->lruMoreRecently = 0;vfdP->lruLessRecently = VfdCache[0].lruLessRecently;VfdCache[0].lruLessRecently = file;VfdCache[vfdP->lruLessRecently].lruMoreRecently = file;DO_DB(_dump_lru());
    }
    • 如果已满,调用ReleaseLruFile函数,删除LRU池尾(最少使用)的VFD,再使用获得的VFD打开新的物理文件。这个函数本质还是调用的LruDelete函数,只是它固定删除池尾的VFD。
    /** Release one kernel FD by closing the least-recently-used VFD.*/
    static bool
    ReleaseLruFile(void)
    {DO_DB(elog(LOG, "ReleaseLruFile. Opened %d", nfile));if (nfile > 0){/** There are opened files and so there should be at least one used vfd* in the ring.*/Assert(VfdCache[0].lruMoreRecently != 0);LruDelete(VfdCache[0].lruMoreRecently);return true;			/* freed a file */}return false;				/* no files available to free */
    }

    ③ 用完文件关闭时,从LRU池删除VFD

    • 通过LruDelete函数(再调用delete函数)实现,将指定VFD从LRU池中删除,并关闭该VFD对应文件,同时将其fd置为VFD_CLOSED表示已空闲,并将其加回FreeList中

    static void
    LruDelete(File file)
    {Vfd		   *vfdP;Assert(file != 0);DO_DB(elog(LOG, "LruDelete %d (%s)",file, VfdCache[file].fileName));vfdP = &VfdCache[file];/** Close the file.  We aren't expecting this to fail; if it does, better* to leak the FD than to mess up our internal state.*/if (close(vfdP->fd) != 0)elog(vfdP->fdstate & FD_TEMP_FILE_LIMIT ? LOG : data_sync_elevel(LOG),"could not close file \"%s\": %m", vfdP->fileName);vfdP->fd = VFD_CLOSED;--nfile;/* delete the vfd record from the LRU ring */Delete(file);
    }
    static void
    Delete(File file)
    {Vfd		   *vfdP;Assert(file != 0);DO_DB(elog(LOG, "Delete %d (%s)",file, VfdCache[file].fileName));DO_DB(_dump_lru());vfdP = &VfdCache[file];VfdCache[vfdP->lruLessRecently].lruMoreRecently = vfdP->lruMoreRecently;VfdCache[vfdP->lruMoreRecently].lruLessRecently = vfdP->lruLessRecently;DO_DB(_dump_lru());
    }

    参考

    《PostgreSQL数据库内核分析》

    https://zhuanlan.zhihu.com/p/550996343

    PostgreSQL源码学习笔记(4)-存储管理_postgres 堆文件-CSDN博客

    Postgres 源码学习 2—Postgres 的 VFD 机制-腾讯云开发者社区-腾讯云

    PolarDB for PostgreSQL 内核解读系列第四讲:PostgreSQL 存储管理(二)_哔哩哔哩_bilibili

    海山数据库(He3DB)技术分享:He3DB Virtual File Descriptor实现原理 - dawn1221 - 博客园

    版权声明:

    本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

    我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com