【Linux kernel】Linux 下的文件描述符(fd)

备注:这篇文章主要内容来自参考连接中的文章,少部分自己加的内容。由于转载只能填写一个链接,因此发的原创。

Linux 中一切皆文件,比如 C++ 源文件、视频文件、Shell脚本、可执行文件等,就连键盘、显示器、鼠标等硬件设备也都是文件。
一个 Linux 进程可以打开成百上千个文件,为了表示和区分已经打开的文件,Linux 会给每个文件分配一个编号(一个 ID),这个编号就是一个整数,被称为文件描述符(File Descriptor)。

从一个最常见的例子说起

在使用Linux的过程中, 我们平时经常看到下面这样的用法

echo log > /dev/null 2>&1
  • “>” :表示将输出结果重定向到哪里,例如:echo “123” > /home/123.txt
  • /dev/null :表示空设备文件

所以 echo log > /dev/null 表示把日志输出到空文件设备,也就是将打印信息丢弃掉,屏幕上什么也不显示。

  • 0 :表示stdout标准输入
  • 1 :表示stdout标准输出
  • 2 :表示stderr标准错误
  • & :表示等同于的意思

习惯上,标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1,标准错误(standard error)是 2。POSIX 定义了STDIN_FILENO、STDOUT_FILENO 和STDERR_FILENO 来代替 0、1、2。这三个符号常量的定义位于头文件 unistd.h 。
所以 2>&1 表示2的输出重定向等同于1,也就是标准错误输出重定向到标准输出。因为前面标准输出已经重定向到了空设备文件,所以标准错误输出也重定向到空设备文件。
这个用法平时很常见,重点是为什么这里是用 2 和 1 ,不是3456什么的呢?这要从 Linux 中的文件描述符说起。

Linux中的文件描述符(file descriptor)

我们知道在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候,我们每操作一次就找一次名字,这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。
文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作(在 Linux 的世界里,一切设备皆文件。我们可以系统调用中 I/O 的函数(I:input,输入;O:output,输出),对文件进行相应的操作( open()、close()、write() 、read() 等)。传统的 Linux 操作系统的标准 I/O 接口是基于数据拷贝操作的,即 I/O 操作会导致数据在操作系统内核地址空间的缓冲区和用户进程地址空间定义的缓冲区之间进行传输。)的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4…
Linux内核对所有打开的文件有一个文件描述符表格,里面存储了每个文件描述符作为索引与一个打开文件相对应的关系,简单理解就是下图这样一个数组,文件描述符(索引)就是文件描述符表这个数组的下标,数组的内容就是指向一个个打开的文件的指针。
在这里插入图片描述
上面只是简单理解,实际上关于文件描述符,Linux内核维护了3个数据结构:

  • 进程级的文件描述符表
  • 系统级的打开文件描述符表
  • 文件系统的i-node表

一个 Linux 进程启动后,会在内核空间中创建一个 PCB(进程控制块) 控制块,PCB(进程控制块) 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。进程级的描述符表的每一条记录了单个进程所使用的文件描述符的相关信息,进程之间相互独立,一个进程使用了文件描述符3,另一个进程也可以用3。除了进程级的文件描述符表,系统还需要维护另外两张表:打开文件表、i-node 表。这两张表存储了每个打开文件的打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息。

系统级的打开文件描述符表(Open file table):

  • 当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
  • 打开文件时的标识(open()的flags参数)
  • 文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)
  • 与信号驱动相关的设置
  • 对该文件i-node对象的引用,即i-node 表指针

文件系统的i-node表(i-node table):

  • 文件类型(例如:常规文件、套接字或FIFO)和访问权限
  • 一个指针,指向该文件所持有的锁列表
  • 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳

进程级的文件描述符表、系统级的打开文件描述符表以及i-node之间的关系如下图:

在这里插入图片描述
从本质上讲,这三种表都是结构体数组,0、1、2、73、1976 等都是数组下标。表头只是我自己添加的注释,数组本身是没有的。实线箭头表示指针的指向,虚线箭头是我自己添加的注释。
你看,文件描述符只不过是一个数组下标吗!
通过文件描述符,可以找到文件指针,从而进入打开文件表。该表存储了以下信息:

  • 文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。
  • 状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。
  • i-node 表指针。

然而,要想真正读写文件,还得通过打开文件表的 i-node 指针进入 i-node 表,该表包含了诸如以下的信息:

  • 文件类型,例如常规文件、套接字或 FIFO。
  • 文件大小。
  • 时间戳,比如创建时间、更新时间。
  • 文件锁。

对上图的进一步说明:

  • 在进程 A 中,文件描述符 1 和 20 都指向了同一个打开文件表项,标号为 23(指向了打开文件表中下标为 23 的数组元素),这可能是通过调用 dup()、dup2()、fcntl() 或者对同一个文件多次调用了 open() 函数形成的。
  • 进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向了同一个文件,这可能是在调用 fork() 后出现的(即进程 A、B 是父子进程关系),或者是不同的进程独自去调用 open() 函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
  • 进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件表项,但这些表项均指向 i-node 表的同一个条目(标号为 1976);换言之,它们指向了同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了 open() 调用。同一个进程两次打开同一个文件,也会发生类似情况。

有了以上对文件描述符的认知,我们很容易理解以下情形:

  • 同一个进程的不同文件描述符可以指向同一个文件。
  • 不同进程可以拥有相同的文件描述符;
  • 不同进程的同一个文件描述符可以指向不同的文件(一般也是这样,除了 0、1、2 这三个特殊的文件);
  • 不同进程的不同文件描述符也可以指向同一个文件。

一个获取文件描述符的实例

我们可以通过 open 系统调用得到一个指定文件的文件描述符。
open 函数需要传入一个文件路径和操作模式, 调用会返回一个整型的文件描述符, 具体方法签名如下

/**
* path 代表文件路径
* oflag 代表文件的打开模式,比如读,写等
*/
int open(char *path,  int oflag, ...)

我们写一段简单的代码来验证一下

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
	// 以只读模式打开 demo.txt 文件
	int fd = open("test.txt", O_RDONLY);
	if (fd == -1) {
		perror("open test.txt error\n");
		return EXIT_FAILURE;
	}
	// 打印获取到的文件描述符
	printf("test.txt fd = %d \n", fd);
	return EXIT_SUCCESS;
}
$ gcc test.c -o test
$ ./test
test.txt fd = 3

系统创建的每个进程默认会打开3个文件:

  • 标准输入(0)
  • 标准输出(1)
  • 标准错误(2)

为什么是 3 ? 因为 0、1、2 被占用了啊…

fork 函数可以创建多个进程, 该函数返回一个 int 值, 当返回值为 0 时代表当前是子进程正在执行,非 0 就为父进程在执行。
PS: 下面的代码并不规范,可能会产生僵尸进程和孤儿进程,但这并不是本文的重点…

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {

	int npid = fork();
	
	if (npid == 0 ){
	// 子进程
		int child_fd = open("demo.txt", O_RDONLY);
		pid_t child_pid = getpid();
		printf("child_pid = %d, child_fd = %d \n", child_pid, child_fd);
	} else {
	// 父进程
		int parent_fd = open("demo.txt", O_RDONLY);
		pid_t parent_pid = getpid();
		printf("parent_pid = %d, parent_fd = %d \n", parent_pid, parent_fd);
	}
	return EXIT_SUCCESS;
}
$ gcc test_process.c -o test_process
$ ./test_process
$ child_pid = 28212, child_fd = 3
  parent_pid = 28210, child_fd = 3

每个进程打开的都是同一个文件,而且返回的文件描述符也是一样的。文件描述符其实就是file结构体数组的索引。

Python中通过 sys 模块封装了标准输入、标准输出和错误输出。通过我们平时常用的内建函数 open 可以获取一个文件的文件描述符,首先创建一个 test.py 文件用于打开,然后创建一个 test2.py 文件,输入下面代码保存。 执行,发现新打开文件的文件描述符是3。

import sys
 
print('stdin fd = ', sys.stdin.fileno())
print('stdout fd = ', sys.stdout.fileno())
print('stderr fd = ', sys.stderr.fileno())
 
with open("test.py", "w") as f:
    print('test.py fd = ', f.fileno())
$ vim test.py
$ python test.py
('stdin fd = ', 0)
('stdout fd = ', 1)
('stderr fd = ', 2)
('test.py fd = ', 3)

最大打开文件描述符个数

最大文件描述符数的限制可以分为2种:

  • 用户级别的限制
  • 系统级别的限制

用户级别的限制

ulimit -n 命令可以看到本次登录的session其文件描述符的限制。

$ ulimit -n
32768

系统级别的限制

这一级别的限制是对整个系统的所有用户的限制,可以执行cat /proc/sys/fs/file-max来查看。

$ cat /proc/sys/fs/file-max
114615

最大文件描述符数的限制可以通过命令进行修改,这里就不详细介绍。

Linux文件描述符到底是什么?
带你破案:文件描述符到底是什么?
彻底弄懂 Linux 下的文件描述符(fd)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值