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

```bash
echo log > /dev/null 2>&1
```

`>` ：表示将输出结果重定向到哪里，例如：echo“123” > /home/123.txt
`/dev/null` ：表示空设备文件

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

`1` ：表示 stdout 标准输出
`2` ：表示 stderr 标准错误
`&` ：表示等同于的意思

所以 `2>&1` 表示 2 的输出重定向等同于 1，也就是标准错误输出重定向到标准输出。因为前面标准输出已经重定向到了空设备文件，所以标准错误输出也重定向到空设备文件。

这个用法平时很常见，重点是为什么这里是用 `2` 和 `1` ，不是 3456 什么的呢？这要从 Linux 中的文件描述符说起。

## 文件描述符（file descriptor

我们知道在 Linux 系统中一切皆可以看成是文件，文件又可分为：`普通文件`、`目录文件`、`链接文件`和`设备文件`。在操作这些所谓的文件的时候，我们每操作一次就找一次名字，这会耗费大量的时间和效率。所以 Linux 中规定每一个文件对应一个索引，这样要操作文件的时候，我们直接找到索引就可以对其进行操作了。

文件描述符（file descriptor）就是内核为了高效管理这些`已经被打开的文件所创建的索引`，其是一个非负整数（通常是小整数），用于指代被打开的文件，所有执行 I/O 操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候，0 是标准输入，1 是标准输出，2 是标准错误。这意味着如果此时去打开一个新的文件，它的文件描述符会是 3，再打开一个文件文件描述符就是 4……

Linux 内核对所有打开的文件有一个文件描述符表格，里面存储了每个文件描述符作为索引与一个打开文件相对应的关系，简单理解就是下图这样一个数组，文件描述符（索引）就是文件描述符表这个数组的下标，数组的内容就是指向一个个打开的文件的指针。

![1](./files/图1.png)

**上面只是简单理解，实际上关于文件描述符，Linux 内核维护了 3 个数据结构**：

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

一个 Linux 进程启动后，会在内核空间中创建一个 PCB 控制块，PCB 内部有一个文件描述符表（File descriptor table），记录着当前进程所有可用的文件描述符，也即当前进程所有打开的文件。进程级的描述符表的每一条记录了单个进程所使用的文件描述符的相关信息，进程之间相互独立，一个进程使用了文件描述符`3`，另一个进程也可以用`3`。

除了进程级的文件描述符表，系统还需要维护另外两张表：打开文件表、i-node 表。这两张表存储了每个打开文件的打开文件句柄（open file handle）。一个打开文件句柄存储了与一个打开文件相关的全部信息。

**系统级的打开文件描述符表：**

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

**文件系统的 i-node 表：**

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

**文件描述符、打开的文件句柄以及 i-node 之间的关系如下图：**
![1](./files/图2.png)

- 在进程 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() 调用。同一个进程两次打开同一个文件，也会发生类似情况。

**这就说明：**

1. **同一个进程的不同文件描述符可以指向同一个文件；**
2. **不同进程可以拥有相同的文件描述符；不同进程的同一个文件描述符可以指向不同的文件（一般也是这样，除了 0、1、2 这三个特殊的文件）；**
3. **不同进程的不同文件描述符也可以指向同一个文件。**

## Linux 上打开文件举例

在 Linux 上用 `tail -f test.py` 打开一个文件，保持打开状态，再新打开一个新的 shell，输入命令`pidof tail` 获取 tail 进程的 pid 号，然后 `ll /proc/$pid/fd` 查看 tail 进程所使用的文件描述符列表，可以看到文件描述符确实是从**3**开始使用的。tail 不是编辑器不存在修改文件的情况，所以直接文件描述符直接打开的源文件。实际上可以使用 `ll /proc/$pid/fd`命令获取当前运行的任意进程的文件描述符使用情况。

![1](./files/图3.png)

## Linux 配置系统最大打开文件描述符个数

### 系统级限制

理论上系统内存有多少就可以打开多少的文件描述符，但是在实际中内核是会做相应的处理，一般最大打开文件数会是系统内存的 10%（以 KB 来计算），称之为系统级限制。这个数字可以通过 `cat /proc/sys/fs/file-max`或者 `sysctl -a | grep fs.file-max` 命令查看。

```bash
➜  ~ sudo sysctl -a | grep fs.file-max
fs.file-max = 9223372036854775807
➜  ~ cat /proc/sys/fs/file-max
9223372036854775807
```

更改系统级限制有临时更改和永久更改两种方式：

- 临时更改：session 断开或者系统重启后会恢复原来的设置值。使用命令 **sysctl -w fs.file-max=xxxx**，其中 xxxx 就是要设置的数字。
- 永久更改：vim 编辑 **/etc/sysctl.conf** 文件，在后面添加 **fs.file-max=xxxx**，其中 xxxx 就是要设置的数字。保存退出后还要使用**sysctl -p** 命令使其生效。

### 用户级限制

同时为了控制每个进程消耗的文件资源，内核也会对单个进程最大打开文件数做默认限制，即用户级限制。32 位系统默认值一般是 1024，64 位系统默认值一般是 65535，可以使用 ulimit -n 命令查看。

