一、直接答:fd 是 Linux 内核给"打开的文件/网络连接"分配的整数 ID
"fd = File Descriptor(文件描述符) ------是 Linux 内核 给每一个打开的文件 / 网络连接 / 设备 分配的一个非负整数 ID 。一切 I/O(文件 / 网络 / 设备)都用 fd 操作。"
核心:
- fd 不是文件本身 ------是指向内核数据结构的句柄
- fd 0 = 标准输入(stdin)
- fd 1 = 标准输出(stdout)
- fd 2 = 标准错误(stderr)
- fd 3+ = 你打开的文件 / 网络连接
二、fd 在 Linux 内核的 4 大真相
2.1 fd 是非负整数
// C 语言(Linux 内核)系统调用
int fd = open("test.txt", O_RDONLY); // 返回 3(3 是 fd)
int socket_fd = socket(AF_INET, SOCK_STREAM, 0); // 返回 4(4 是 fd)
老哥注意:
- fd 是整数(0, 1, 2, 3, 4, ...)
- 不是对象,不是类
- 操作系统自动分配
2.2 fd 是进程级的
# Linux 进程 fd 列表
# /proc/<pid>/fd/ 目录里看到进程的所有 fd
ls -la /proc/1234/fd/
0 -> /dev/null # 标准输入
1 -> /dev/null # 标准输出
2 -> /dev/null # 标准错误
3 -> /home/test.txt # 用户打开的文件
4 -> socket:[12345] # 网络连接
注意:
- 每个进程都有自己的 fd 表
- fd 3 在进程 A 和进程 B 是不同的
2.3 fd 是一切 I/O 的入口
┌─────────────────────────────────────────────┐
│ Linux 内核视角:一切 I/O 都是 fd │
├─────────────────────────────────────────────┤
│ │
│ 文件 I/O → fd(open/read/write) │
│ 网络 I/O → fd(socket/accept/send) │
│ 设备 I/O → fd(open/read/write) │
│ 管道 I/O → fd(pipe) │
│ 事件通知 → fd(epoll) │
│ │
│ ⚠️ 一切 I/O 都是 fd │
│ │
└─────────────────────────────────────────────┘
2.4 fd 是有限的资源
# Linux 默认限制
ulimit -n
# 输出 1024(默认 1024 个 fd)
# 修改
ulimit -n 65535 # 改成 65535 个
注意:
- 单个进程最多打开 1024 个 fd(默认)
- 每个 TCP 连接占 1 个 fd
- 1w 并发连接 = 1w 个 fd (必须调大 ulimit -n)
三、fd 在 NIO 中的真实角色 (老哥最关心)
3.1 NIO Channel = fd 的 Java 包装
// Java NIO 底层
ServerSocketChannel channel = ServerSocketChannel.open();
// 1. 调用 OS 的 socket() 系统调用
// 2. 拿到一个 fd(比如 5)
// 3. JDK 把 fd 包装成 ServerSocketChannel 对象
// 真实代码(OpenJDK 源码)
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
// 内部调用:net.openServerSocketChannel() → SocketDispatcher.open()
// → 调 OS 的 socket() 系统调用 → 拿到 fd
}
关键:
- NIO Channel 不是"装多个 BIO" (老哥之前问的)
- NIO Channel 是 fd 的 Java 包装
- 每个 Channel 1 个 fd
3.2 NIO Selector 是 fd 集合的管理器
// NIO Selector 真实结构
public abstract class Selector {
// 1. 内部维护一个 fd 集合
// 2. 调用 epoll_wait() 系统调用
// 3. 当某个 fd 有事件时,回调通知
}
// 真实使用
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
// 1. channel 的 fd 被加到 selector 内部
// 2. selector 内部维护 epoll fd 集合
老哥注意:
- Selector 内部维护一个 fd 集合
- epoll_wait() 等待 fd 事件
- 不是遍历所有 fd,是事件驱动
四、fd 4 大经典场景
4.1 文件 I/O
// C 语言:打开文件
int fd = open("test.txt", O_RDONLY); // 拿到 fd
char buf[1024];
read(fd, buf, sizeof(buf)); // 用 fd 读
close(fd); // 关闭 fd
Java 对应:
FileInputStream fis = new FileInputStream("test.txt");
// 内部调 open() 拿 fd,读完调 close()
// 老哥用 Java 看不到 fd,但 fd 在底层
4.2 网络 I/O
// C 语言:TCP 服务端
int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 拿到 fd
bind(server_fd, ...);
listen(server_fd, 5);
int client_fd = accept(server_fd, ...); // 接受连接,拿到新的 fd
read(client_fd, buf, sizeof(buf));
close(client_fd);
Java NIO 对应:
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 内部调 socket() 拿 fd
// accept() 返回 SocketChannel,1 个新 fd
4.3 事件通知(epoll)
// C 语言:epoll 监听多个 fd
int epoll_fd = epoll_create(1); // 创建 epoll fd
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event); // 注册 fd
epoll_wait(epoll_fd, events, 100, -1); // 等待事件
// 当 server_fd 有事件时,epoll_wait() 立即返回
Java NIO 对应:
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
// 内部调 epoll_ctl(),把 fd 加到 epoll
selector.select(); // 内部调 epoll_wait()
4.4 标准 I/O
// C 语言:标准输入输出
int fd = 0; // 标准输入
int fd = 1; // 标准输出
int fd = 2; // 标准错误
老哥注意:
- Java 的
System.in= fd 0 - Java 的
System.out= fd 1 - Java 的
System.err= fd 2
五、fd 在 NIO 中完整流程
┌──────────────────────────────────────────────────┐
│ Java NIO 完整 I/O 流程(fd 全程跟踪) │
├──────────────────────────────────────────────────┤
│ │
│ 1. Channel 创建 │
│ ↓ Java: ServerSocketChannel.open() │
│ ↓ 调 OS: socket() 系统调用 │
│ ↓ 拿到 fd = 5 │
│ ↓ JDK 把 fd 包装成 ServerSocketChannel 对象 │
│ │
│ 2. Channel 注册到 Selector │
│ ↓ Java: channel.register(selector, OP_ACCEPT) │
│ ↓ 调 OS: epoll_ctl(ADD, fd=5, ...) │
│ ↓ 把 fd=5 加到 epoll 监听集合 │
│ │
│ 3. Selector 监听 │
│ ↓ Java: selector.select() │
│ ↓ 调 OS: epoll_wait() │
│ ↓ 阻塞等待 fd=5 有事件 │
│ │
│ 4. 新连接到达 │
│ ↓ OS 内核:fd=5 有 ACCEPT 事件 │
│ ↓ 唤醒 epoll_wait() │
│ ↓ Java: selectedKeys() 返回 SelectionKey │
│ │
│ 5. 接受新连接 │
│ ↓ Java: serverChannel.accept() │
│ ↓ 调 OS: accept() 系统调用 │
│ ↓ 拿到新 fd=6(客户端连接) │
│ ↓ JDK 把 fd=6 包装成 SocketChannel 对象 │
│ ↓ 把 fd=6 注册到 selector │
│ │
│ 6. 读数据 │
│ ↓ Java: channel.read(buffer) │
│ ↓ 调 OS: read(fd=6, buffer) │
│ ↓ 阻塞读数据 │
│ │
│ 7. 关闭连接 │
│ ↓ Java: channel.close() │
│ ↓ 调 OS: close(fd=6) │
│ │
└──────────────────────────────────────────────────┘
六、fd 在 NIO 中 4 大核心要点
6.1 每个连接 1 个 fd
1w 个 TCP 连接
↓
1w 个 fd(0-10000)
↓
BIO:1w 个 Socket 对象 = 1w 个线程
NIO:1w 个 Channel 对象 = 1w 个 fd = 1 个 Selector
6.2 fd 是有限资源
# 默认 1024 个 fd / 进程
# 1w 并发必须调大
ulimit -n 65535
老哥 Spring Cloud Gateway 实战:
- Linux 必须调大 ulimit -n
- 生产环境一般 65535 或 100 万
6.3 fd 是 OS 资源
// fd 数量 / 进程
// fd 数量 / 系统
// ulimit -n # 进程级
// cat /proc/sys/fs/file-max # 系统级
老哥注意:
- fd 数量受 3 层限制 :
- 硬件(内存 / CPU)
- OS(系统级 file-max)
- 进程(ulimit -n)
6.4 fd 是 I/O 的核心抽象
Linux 内核视角:
- 一切都是文件
- 一切 I/O 都是 fd
- 网络连接 = fd
- 文件 = fd
- 设备 = fd
- 管道 = fd