postgresql源码学习(60)—— VFD的作用及机制

news2025/4/3 1:25:39

 首先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 - 博客园

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2309150.html

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

    相关文章

    【CSS—前端快速入门】CSS 选择器

    CSS 1. CSS介绍 1.1 什么是CSS? CSS(Cascading Style Sheet)&#xff0c;层叠样式表&#xff0c;用于控制页面的样式&#xff1b; CSS 能够对网页中元素位置的排版进行像素级精确控制&#xff0c;实现美化页面的效果&#xff1b;能够做到页面的样式和 结构分离&#xff1b; 1…

    Linux安装jdk,node,mysql,redis

    准备工作&#xff1a; 1.安装VMware软件&#xff0c;下载CentOs7镜像文件&#xff0c;在VMware安装CentOs7 2.宿主机安装Xshell用来操作linux 3. .宿主机安装Xftp用来在宿主机和虚拟机的linux传输文件 案例1&#xff1a;在 /home/soft文件夹解压缩jdk17&#xff0c;并配置环…

    深度求索(DeepSeek)的AI革命:NLP、CV与智能应用的技术跃迁

    Deepseek官网&#xff1a;DeepSeek 引言&#xff1a;AI技术浪潮中的深度求索 近年来&#xff0c;人工智能技术以指数级速度重塑全球产业格局。在这场技术革命中&#xff0c;深度求索&#xff08;DeepSeek&#xff09;凭借其前沿的算法研究、高效的工程化能力以及对垂直场景的…

    Minio搭建并在SpringBoot中使用完成用户头像的上传

    Minio使用搭建并上传用户头像到服务器操作,学习笔记 Minio介绍 minio官网 MinIO是一个开源的分布式对象存储服务器&#xff0c;支持S3协议并且可以在多节点上实现数据的高可用和容错。它采用Go语言开发&#xff0c;拥有轻量级、高性能、易部署等特点&#xff0c;并且可以自由…

    阿里云 | 快速在网站上增加一个AI助手

    创建智能体应用 如上所示&#xff0c;登录阿里云百炼人工智能业务控制台&#xff0c;创建智能体应用&#xff0c;智能体应用是一个agent&#xff0c;即提供个人或者企业的代理或中间件组件应用&#xff0c;对接阿里云大模型公共平台&#xff0c;为个人或者企业用户提供大模型应…

    原型链与继承

    #搞懂还是得自己动手# 原型链 function Person(name) { this.name name; } Person.prototype.sayName function() { console.log(this.name); };const p new Person("Alice"); 原型链关系图&#xff1a; 原型链&#xff1a;person->Person.prototype->O…

    动态规划 ─── 算法5

    动态规划&#xff08;Dynamic Programming&#xff0c;简称 DP&#xff09;是一种用于解决复杂问题的算法设计技术&#xff0c;特别适用于具有重叠子问题和最优子结构性质的问题。动态规划通过将问题分解为更小的子问题&#xff0c;并存储子问题的解来避免重复计算&#xff0c;…

    博客系统--测试报告

    博客系统--测试报告 项目背景项目功能功能测试①登录功能测试②发布博客功能测试③删除文章功能测试④功能测试总结&#xff1a; 自动化测试自动化脚本执行界面&#xff1a; 性能测试 本博文主要针对个人实现的项目《博客系统》去进行功能测试、自动化测试、性能测试&#xff0…

    【博资考4】网安学院-硕转博考试内容

    【博资考4】硕转博考试内容 - 网络安全与基础理论 写在最前面一. **21年硕转博面试内容回顾**网络、逆向、操作系统、攻防、漏洞1. **网络安全常见攻击方式及其防范措施**1.1 **DDoS攻击&#xff08;分布式拒绝服务&#xff09;**1.2 **SQL注入攻击**1.3 **XSS攻击&#xff08;…

    GPT-4.5 怎么样?如何升级使用ChatGPTPlus/Pro? GPT-4.5设计目标是成为一款非推理型模型的巅峰之作

    GPT-4.5 怎么样&#xff1f;如何升级使用ChatGPTPlus/Pro? GPT-4.5设计目标是成为一款非推理型模型的巅峰之作 今天我们来说说上午发布的GPT-4.5&#xff0c;接下来我们说说GPT4.5到底如何&#xff0c;有哪些功能&#xff1f;有哪些性能提升&#xff1f;怎么快速使用到GPT-4.…

    基于专利合作地址匹配的数据构建区域协同矩阵

    文章目录 地区地址提取完成的处理代码 在专利合作申请表中&#xff0c;有多家公司合作申请。在专利权人地址中&#xff0c; 有多个公司的地址信息。故想利用这里多个地址。想用这里的地址来代表区域之间的专利合作情况代表区域之间的协同、协作情况。 下图是专利合作表的一部分…

    0x02 js、Vue、Ajax

    文章目录 js核心概念js脚本引入html的方式基础语法事件监听 Vuevue简介v-forv-bindv-if&v-showv-model&v-on Ajax js 核心概念 JavaScript&#xff1a;是一门跨平台、面向对象的脚本语言&#xff0c;用来控制网页行为实现交互效果&#xff0c;由ECMAScript、BOM、DOM…

    IDEA 使用codeGPT+deepseek

    一、环境准备 1、IDEA 版本要求 安装之前确保 IDEA 处于 2023.x 及以上的较新版本。 2、Python 环境 安装 Python 3.8 或更高版本 为了确保 DeepSeek 助手能够顺利运行&#xff0c;您需要在操作系统中预先配置 Python 环境。具体来说&#xff0c;您需要安装 Python 3.8 或更高…

    Linux笔记---一切皆文件

    1. 含义 “一切皆文件”是 Linux 对系统资源的高度抽象&#xff0c;通过文件接口屏蔽底层差异&#xff0c;提供了简洁、一致的操作方式。这种设计降低了系统复杂性&#xff0c;使得工具、脚本和应用程序能够以统一模式处理多样化资源&#xff0c;是 Linux 强大灵活性的重要基石…

    DeepSeek开源周,第五弹再次来袭,3FS

    Fire-Flyer 文件系统&#xff08;3FS&#xff09;总结&#xff1a; 一、核心特点 3FS 是一个专为 AI 训练和推理工作负载设计的高性能分布式文件系统&#xff0c;利用现代 SSD 和 RDMA 网络&#xff0c;提供共享存储层&#xff0c;简化分布式应用开发。其主要特点包括&#xf…

    【音视频】VLC播放器

    提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 一、vlc是什么&#xff1f; VLC Media Player&#xff08;简称VLC&#xff09;是一款免费、开源、跨平台的多媒体播放器&#xff0c;由非营利组织VideoLAN开发&#xff0c;最…

    【软件测试】_使用selenium进行自动化测试示例

    目录 1. 导入依赖 2. 使用selenium编写测试代码 3. 运行结果 4. 关于浏览器驱动管理及浏览器驱动配置 创建一个空项目用于进行selenium的自动化测试。 1. 导入依赖 <dependencies><!-- https://mvnrepository.com/artifact/io.github.bonigarcia/webdrivermanager…

    【清华大学】DeepSeek从入门到精通完整版pdf下载

    DeepSeek从入门到精通.pdf 一共104页完整版 下载链接: https://pan.baidu.com/s/1-gnkTTD7EF2i_EKS5sx4vg?pwd1234 提取码: 1234 或 链接&#xff1a;https://pan.quark.cn/s/79118f5ab0fd 一、DeepSeek 概述 背景与定位 DeepSeek 的研发背景 核心功能与技术特点&#xff08…

    题解 | 牛客周赛83 Java ABCDEF

    目录 题目地址 做题情况 A 题 B 题 C 题 D 题 E 题 F 题 牛客竞赛主页 题目地址 牛客竞赛_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ 做题情况 A 题 输出两个不是同一方位的字符中的任意一个就行 import java.io.*; import java.math.*; import java…

    C语言(16)---------->二维数组

    在学习二维数组之前&#xff0c;掌握一维数组是非常重要的。 对于一维数组的学习&#xff0c;读者可以参考我写过的博客&#xff1a; C语言&#xff08;15&#xff09;--------------&#xff1e;一维数组-CSDN博客 这里面由浅入深地介绍了C语言中一维数组的使用。 一、二维…