看了上一篇PG虚拟文件描述符(VFD)机制——FD LRU池一:postgresql-8.4.1/src/backend/storage/file/fd.c,了解了FD LRU池的原理和API。但是我们还没有了解清楚VFD是怎么复用的FD。在操作系统中,当一个进程创建或是打开一个文件时,操作系统会为该文件分配一个唯一文件描述符(或叫文件句柄),并通过该文件描述符来唯一标识和操作该文件。参考Linux/UNIX系统编程手册中关于文件I/O的描述:
所有执行I/O操作的系统调用都以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道、FIFO、socket、终端设备和普通文件。按照惯例,大多数程序都期望能够使用如下3种标准的文件描述符(程序继承了shell文件描述符的副本,在交互式shell中,这3个文件描述符通常指向shell运行所在的终端,如果命令行指定对输入/输出进行重定向操作,那么shell会对文件描述符做适当修改,然后再启动程序)。
文件描述符 | 用途 | POSIX名称 | stdio流 |
0 | 标准输入 | STDIN_FILENO | stdin |
1 | 标准输出 | STDOUT_FILENO | stdout |
2 | 标准错误 | STDERR_FILENO | stderr |
stdin、stdout和stderr变量永远对应文件描述符0、1、2嘛?
虽然stdin、stdout和stderr变量在程序初始化时用于指代进程的标准输入、标准输出和标准错误,但是调用freopen()库函数可以使这些变量指代其他任何文件对象。作为其操作的一部分,freopen可以将流(stream)重新打开之际一并更换隐匿其中的文件描述符。换而言之,针对stdout调用freopen()函数后,无法保证stdout变量仍然为1。C 库函数 FILE *freopen(const char *filename, const char *mode, FILE *stream) 把一个新的文件名 filename 与给定的打开的流 stream 关联,同时关闭流中的旧文件。
也就是说文件描述符和文件不是永远绑定(一一对应)的关系,那么文件描述符和打开文件之间的关系到底是怎样呢?
这里有三个概念:针对每个进程,文件描述符都自成一套。多个文件描述符可以指向同一打开文件。这些文件描述符可在相同或不同的进程中打开。如下图所示,共有3个内核维护的数据结构可以来理解这三个概念:进程级的文件描述符表、系统级的打开文件表、文件系统的i-node表。文件描述符表(open file descriptor)每个条目都记录了单个文件描述符的相关信息:控制文件描述符操作的一组标志和对打开文件句柄的引用。内核对所有打开的文件维护一个系统级的描述符表格(open file description table),也称为打开文件表(open file table),并将表中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息(当前文件偏移量,打开文件时所使用的状态标志,文件访问模式,与信号驱动I/O相关的设置,对该文件i-node对象的应用)。对于i-node表暂且不详细叙述,可以查找肥叔菌的其他博客。我们可以借用数据库里面的概念理解,虽然不是很恰当:这里的文件描述符表中的条目就像我们客户机上的JDBC连接,打开文件表就像我们数据库服务器中的表,而这里inode指针相当于保存有服务器文件路径的字段。
每个操作系统都对进程能打开的文件数加以限制,因此此进程获得的文件描述符是有限的。我们上一段在理论上分析了复用文件描述符的可行性,PG的VFD机制像数据库的连接池复用连接一样创建池来缓存数量有限的文件描述符。进程使用了VFD后,自身会觉得可以打开任意多的文件,不再受操作系统的限制。
从VFD LRU的函数中提取和复用文件描述符相关的代码进行分析。先看一下处理LRU池的代码,static void Delete(File file) 仅仅是将索引为File的VFD从池中剔除,也就是调整前后VFD的lruLessRecently和lruMoreRecently。不改变索引为File的VFD中的数据。static void LruDelete(File file) 使用Delete函数从LRU池中断开该VFD,向该VFD中保存打开文件的偏移量(vfdP->seekPos = lseek(vfdP->fd, (off_t) 0, SEEK_CUR);),调用close系统调用关闭vfdP->fd文件描述符,并将该VFD中的fd置为VFD_CLOSED。static void Insert(File file)仅仅将索引为File的VFD加入LRU头部,也就是调整前后VFD的lruLessRecently和lruMoreRecently。不改变索引为File的VFD中的数据。static int LruInsert(File file)函数有复用的逻辑。进入函数后,先判断索引为file的vfd的文件描述符是否为VFD_CLOSED。如果不是VFD_CLOSED,说明文件处于打开状态,使用Insert函数插入LRU池。如果是,并且打开文件超过警戒线,需要先从LRU池中释放(ReleaseLruFile)最久使用的VFD(逻辑是一直删除到小于警戒线或没有可释放的VFD了)。使用BasicOpenFile文件打开文件(该函数使用系统函数open打开文件,当无法申请到文件描述符时调用ReleaseLruFile释放文件描述符)。static bool ReleaseLruFile(void)在lru池中有VFD时使用LruDelete删除最久未使用的VFD。static File AllocateVFD(void)用于申请一个空闲的VFD,如果没有进行LRU池扩容。FreeVfd用于清空VFD中的fileName,设置fdstate为0x0,并将该VFD放入空闲链表。
和文件描述符有关的函数有LruDelete(使用close向系统归还了相关文件描述符)、LruInsert(有执行流程会使用系统函数open申请文件描述符)、ReleaseLruFile(调用了LruDelete)。从这些函数中可以看出整个流程还是使用到了系统函数open、close,也就是LRU池中的VFD和文件描述符还是一对一关系,只是做了警戒线,达到警戒线,就需要把最久没有使用的文件描述符归还给系统,以供其他VFD占用。
看两个和警戒线相关的函数:
count_usable_fds ---计算系统允许我们打开多少个FD,并估计已经打开了多少。
如果usable_fds达到max_to_probe,我们将停止计数。 注意:max_to_probe的一个较小的值可能会导致低估已经打开的文件描述符的数量; 我们必须在已使用FD的集合中填写任何“空白”,然后才能计算已打开的文件描述符,并给出正确的答案。 实际上,几十个max_to_probe应该足以确保获得良好的结果。我们假设stdin(FD 0)可用于duping
1 static void count_usable_fds(int max_to_probe, int *usable_fds, int *already_open) 2 { 3 int *fd; 4 int size; 5 int used = 0; 6 int highestfd = 0; 7 int j; 8 9 #ifdef HAVE_GETRLIMIT 10 struct rlimit rlim; 11 int getrlimit_status; 12 #endif 13 14 size = 1024; 15 fd = (int *) palloc(size * sizeof(int)); 16 17 #ifdef HAVE_GETRLIMIT 18 #ifdef RLIMIT_NOFILE /* most platforms use RLIMIT_NOFILE */ 19 getrlimit_status = getrlimit(RLIMIT_NOFILE, &rlim); 20 #else /* but BSD doesn't ... */ 21 getrlimit_status = getrlimit(RLIMIT_OFILE, &rlim); 22 #endif /* RLIMIT_NOFILE */ 23 if (getrlimit_status != 0) 24 ereport(WARNING, (errmsg("getrlimit failed: %m"))); 25 #endif /* HAVE_GETRLIMIT */ 26 27 /* dup until failure or probe limit reached */ 28 for (;;) 29 { 30 int thisfd; 31 32 #ifdef HAVE_GETRLIMIT 33 34 /* 35 * don't go beyond RLIMIT_NOFILE; causes irritating kernel logs on 36 * some platforms 37 */ 38 if (getrlimit_status == 0 && highestfd >= rlim.rlim_cur - 1) 39 break; 40 #endif 41 42 thisfd = dup(0); // dup调用复制一个打开的文件描述符oldfd,并返回新描述符,二者都指向同一打开的文件句柄。 43 if (thisfd < 0) 44 { 45 /* Expect EMFILE or ENFILE, else it's fishy */ 46 if (errno != EMFILE && errno != ENFILE) 47 elog(WARNING, "dup(0) failed after %d successes: %m", used); 48 break; 49 } 50 51 if (used >= size) 52 { 53 size *= 2; 54 fd = (int *) repalloc(fd, size * sizeof(int)); 55 } 56 fd[used++] = thisfd; 57 58 if (highestfd < thisfd) 59 highestfd = thisfd; 60 61 if (used >= max_to_probe) 62 break; 63 } 64 65 /* release the files we opened */ 66 for (j = 0; j < used; j++) 67 close(fd[j]); 68 69 pfree(fd); 70 71 /* 72 * Return results. usable_fds is just the number of successful dups. We 73 * assume that the system limit is highestfd+1 (remember 0 is a legal FD 74 * number) and so already_open is highestfd+1 - usable_fds. 75 */ 76 *usable_fds = used; 77 *already_open = highestfd + 1 - used; 78 }
set_max_safe_fds设置警戒值 MIN(usable_fds, max_files_per_process - already_open)
1 void set_max_safe_fds(void) 2 { 3 int usable_fds; 4 int already_open; 5 6 /*---------- 7 * We want to set max_safe_fds to 8 * MIN(usable_fds, max_files_per_process - already_open) 9 * less the slop factor for files that are opened without consulting 10 * fd.c. This ensures that we won't exceed either max_files_per_process 11 * or the experimentally-determined EMFILE limit. 12 *---------- 13 */ 14 count_usable_fds(max_files_per_process, 15 &usable_fds, &already_open); 16 17 max_safe_fds = Min(usable_fds, max_files_per_process - already_open); 18 19 /* 20 * Take off the FDs reserved for system() etc. 21 */ 22 max_safe_fds -= NUM_RESERVED_FDS; 23 24 /* 25 * Make sure we still have enough to get by. 26 */ 27 if (max_safe_fds < FD_MINFREE) 28 ereport(FATAL, 29 (errcode(ERRCODE_INSUFFICIENT_RESOURCES), 30 errmsg("insufficient file descriptors available to start server process"), 31 errdetail("System allows %d, we need at least %d.", 32 max_safe_fds + NUM_RESERVED_FDS, 33 FD_MINFREE + NUM_RESERVED_FDS))); 34 35 elog(DEBUG2, "max_safe_fds = %d, usable_fds = %d, already_open = %d", 36 max_safe_fds, usable_fds, already_open); 37 }