彻底搞懂文件描述符/文件句柄/文件指针的区别与联系

Prologue

处理了一起too many open files的报错,中途忽然感觉这三个概念很容易混淆,网上其他博客也是众说纷纭。于是做了一点考证,专门写一篇来尽量准确地记录下。

本文的内容有不少来自Linux领域的权威书籍,Michael Kerrisk所著《The Linux Programming Interface:A Linux and UNIX System Programming Handbook》的第4、5两章。

文件描述符 & 文件描述符表

文件描述符(file descriptor, fd)是Linux系统中对已打开文件的一个抽象标记,所有I/O系统调用对已打开文件的操作都要用到它。这里的“文件”仍然是广义的,即除了普通文件和目录外,还包括管道、FIFO(命名管道)、Socket、终端、设备等。

文件描述符是一个较小的非负整数,并且0、1、2三个描述符总是默认分配给标准输入、标准输出和标准错误。这就是常用的nohup ./my_script > my_script.log 2>&1 &命令里2和1的由来。

195230-ef8a47e51dd64da0.png

Linux系统中的每个进程会在其进程控制块(PCB)内维护属于自己的文件描述符表(file descriptor table)。表中每个条目包含两个域:一是控制该描述符的标记域(flags),二是指向系统级别的打开文件表中对应条目的指针。那么打开文件表又是什么呢?

打开文件表 & 文件句柄

内核会维护系统内所有打开的文件及其相关的元信息,该结构称为打开文件表(open file table)。表中每个条目包含以下域:

  • 文件的偏移量。POSIX API中的read()/write()/lseek()函数都会修改该值;
  • 打开文件时的状态和权限标记。通过open()函数的参数传入;
  • 文件的访问模式(只读、只写、读+写等)。通过open()函数的参数传入;
  • 指向其对应的inode对象的指针。内核也会维护系统级别的inode表,关于inode的细节请参考这篇文章

文件描述符表、打开文件表、inode表之间的关系可以用书中的下图来表示。注意图中的fd 0、1、2...只是示意下标,不代表三个标准描述符。

195230-9419f6373f41a8db.png

可见,一个打开的文件可以对应多个文件描述符(不管是同进程还是不同进程),一个inode也可以对应多个打开的文件。打开文件表中的一行称为一条文件描述(file description),也经常称为文件句柄(file handle)。

多嘴一句,“句柄”这个词在UNIX世界中并不很正式,但在Windows里遍地都是。Windows NT内核会将内存中的所有对象(文件、窗口、菜单、图标等一切东西)的地址列表维护成整数索引,这个整数就叫做句柄,逻辑上讲类似于“指针的指针”,感觉上还是有一些相通的地方的。

文件I/O API & 文件指针

说了这么多,用最基础的POSIX库函数写个示例程序吧。它将一个文件中的内容读出来,并原封不动地写入另外一个文件。

#include <fcntl.h>
#include <sys/stat.h>
#define BUF_SIZE 1024

int main(int argc,char *argv[]) {
  int inputFd, outputFd;
  char buf[BUF_SIZE];
  ssize_t numRead;

  inputFd = open("data.txt", O_RDONLY);
  if (inputFd == -1) {
    exit(EXIT_FAILURE);
  }
  outputFd = open(
    "data_copy.txt", 
    O_CREAT | O_WRONLY | O_TRUNC,
    S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
  );
  if (outputFd == -1) {
    exit(EXIT_FAILURE);
  }

  while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0) {
    if (write(outputFd, buf, numRead) != numRead) {
      exit(EXIT_FAILURE);
    }
  }
  
  close(inputFd);
  close(outputFd);
  exit(EXIT_SUCCESS);
}

严格来讲,POSIX提供的这些函数只是用户与内核之前的桥梁,实际仍位于系统调用层之上。但是现实应用中,我们一般也把它们叫做系统调用了(尽管不太正确)。

要使用open()/read()/write()/close()这些系统调用,必须引入fcntl.h头文件。open()返回的是文件描述符,其参数中传入的flags和mode值也会保存在打开文件表中。在整个读、写并最终关闭文件的过程中,操作的也都是文件描述符。

那么我们在大学C语言课程上学习的“文件指针”(file pointer)又是什么呢?这个就比较简单,继续看下面的栗子。

#include <stdio.h>
#include <stdlib.h>
#define BUF_SIZE 1024

int main(int argc,char *argv[]) {
  char buf[BUF_SIZE];
  FILE *inputFp;
  size_t numRead;

  inputFp = fopen("data.txt", "r");
  if (inputFp == NULL) {
    exit(EXIT_FAILURE);
  }

  while (!feof(inputFp)) {
    numRead = fread(buf, sizeof(char), sizeof(buf), inputFp);
    printf("%ld\t%s", numRead, buf);
  }

  fclose(inputFp);
  exit(EXIT_SUCCESS);
}

可见,文件指针就是FILE结构体的指针,与前两个概念不属于同一层。当通过文件指针操作文件时,需要调用C语言stdio.h中提供的文件API(fopen()、fread()等),而C标准库最终调用了POSIX的库函数。并且“file pointer”这个词里的“file”指的是狭义的文件,不包括管道、设备等其他东西,所以单纯用C API只能操作普通文件。

FILE结构体中是包含了文件描述符的,所以C语言也提供了互相转换的方法:

int inputFd;
FILE *inputFp;

inputFd = fileno(inputFp);
inputFp = fdopen(inputFd, "r");

文件描述符和文件句柄的限制

文章开头提到了"too many open files"这条报错信息,它的实际含义是文件描述符数量超限。用ulimit -a命令打印出各限制值:

~ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 127961
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65535
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 127961
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

其中open files一行就表示当前用户、当前终端、单个进程能拥有的文件描述符的数量阈值(很多文章都描述错了这一点),可以用ulimit -n [阈值]命令来临时修改,退出登录即失效。如果想要永久修改,可以将ulimit -n [阈值]写入用户的.bash_profile文件或/etc/profile中,也可以修改/etc/security/limits.conf:

~ vim /etc/security/limits.conf
# 用户名 软/硬限制 限制项 阈值
root soft nofile 65535
root hard nofile 65535

那么如何列出各个进程的文件描述符呢?可以利用lsof(list open files)命令。这个命令的用法很丰富,本文暂时不表。

既然有了进程级别的描述符数量限制,也就有系统级别的文件句柄数量限制。可以这样查看其阈值,以及当前已分配的句柄数:

~ cat /proc/sys/fs/file-max
3247469        # 阈值
~ cat /proc/sys/fs/file-nr
# 已分配且使用中 / 已分配但未使用 / 阈值
2976    0   3247469

如果需要临时修改,可以直接向file-max写入新值。永久生效的方法是修改/etc/sysctl.conf:

~ vim /etc/sysctl.conf
fs.file-max = 5242880
# 立即生效
~ sysctl -p

The End

最后总结一下吧。

  • 文件描述符是进程级别的,文件句柄是系统级别的,不能混用。它们在不同级别表示已打开的文件。
  • 文件描述符与文件句柄直接关联,文件句柄与inode直接关联。
  • 文件描述符在POSIX系统调用中直接可见,文件指针是C语言在其基础上的包装。
  • 文件句柄在UNIX里不是个正式概念,所以无论在系统还是C语言API中都不显式存在。

明天公司年会,民那晚安晚安。

  • 18
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值