-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfd_close_bench.cpp
More file actions
325 lines (288 loc) · 14.6 KB
/
Copy pathfd_close_bench.cpp
File metadata and controls
325 lines (288 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// fd_close_bench.cpp
//
// 测试目标:对比子进程 fork 后清理继承 fd 的几种实现方式在不同
// RLIMIT_NOFILE(rlim_cur)下的性能差异。
//
// 对应场景:my_shell::exec() 里这段代码
// for (int f = 3; f < (int)limits.rlim_cur; f++)
// close(f);
// 这段循环的耗时和 rlim_cur 成正比 —— 无论实际打开了多少个 fd,
// 都要把 [3, rlim_cur) 整个区间遍历一遍逐个调用 close()。
// 如果系统 ulimit -n 设置得很大(比如 1048576),
// 即使当前只打开了 5 个 fd,也要执行 100 多万次系统调用。
//
// 本测试对比四种实现:
// 1. naive_loop : 原始逐个 close() 的写法(当前代码的做法)
// 2. close_range_full: 用 close_range(3, ~0U, 0) 一次系统调用关闭整个区间
// (Linux 5.9+ 内核支持,无需知道上限,内核自己截断)
// 3. close_range_real: 只关闭真实打开过的 fd 范围(模拟"提前记录最大 fd"的优化思路)
// 4. proc_fd_iterate : 读取 /proc/self/fd 目录拿到真实打开的 fd 列表,
// 只对这些 fd 逐个 close(),不依赖 close_range 系统调用,
// 适合内核 < 5.9、又不想盲目遍历整个 rlim_cur 区间的场景
//
// 编译: g++ -O2 -std=c++17 fd_close_bench.cpp -o fd_close_bench
// 运行: ./fd_close_bench
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <vector>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/resource.h>
#include <sys/syscall.h>
#include <errno.h>
#ifndef SYS_close_range
#define SYS_close_range 436 // x86_64 上 close_range 的 syscall 号(内核 5.9 引入)
#endif
// glibc 较老版本可能没有 close_range() 的封装,这里直接用 syscall 兜底调用
static int my_close_range(unsigned int first, unsigned int last, unsigned int flags)
{
return syscall(SYS_close_range, first, last, flags);
}
// 运行时探测当前内核是否真的支持 close_range(Linux 5.9+)。
// 不能只看编译期是否定义了 SYS_close_range —— 编译环境的内核头文件
// 可能支持,但实际运行的内核版本更低,syscall 会返回 ENOSYS。
// 用一个无副作用的调用(关闭一个空区间 [1,0))来探测,避免误关到真实 fd。
static bool close_range_supported()
{
errno = 0;
int ret = syscall(SYS_close_range, 1u, 0u, 0u); // first > last,内核应直接返回 0,不做任何实际关闭
if (ret == -1 && errno == ENOSYS) {
return false;
}
return true;
}
using Clock = std::chrono::high_resolution_clock;
// 打开 real_fd_count 个 /dev/null,模拟父进程实际持有的文件描述符数量
// (比如日志文件、socket、管道等),返回实际打开到的最大 fd 号 + 1。
// actual_opened 用于告诉调用方实际成功打开了多少个(可能小于 real_fd_count,
// 比如撞到了系统 ulimit -n 上限),调用方应据此判断测试数据是否可信。
static int open_dummy_fds(int real_fd_count, int* actual_opened = nullptr)
{
int max_fd = 3;
int opened = 0;
for (int i = 0; i < real_fd_count; i++) {
int fd = open("/dev/null", O_RDONLY);
if (fd < 0) {
fprintf(stderr, "open /dev/null failed(已打开 %d/%d 个,撞到系统限制): %s\n",
opened, real_fd_count, strerror(errno));
break;
}
opened++;
if (fd + 1 > max_fd) max_fd = fd + 1;
}
if (actual_opened) *actual_opened = opened;
return max_fd;
}
// 方式1:原始朴素循环,逐个调用 close(),遍历 [3, rlim_cur)
static double bench_naive_loop(unsigned int rlim_cur)
{
auto t0 = Clock::now();
for (unsigned int f = 3; f < rlim_cur; f++)
close(f);
auto t1 = Clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count();
}
// 方式2:用 close_range 一次性关闭整个区间,不需要知道精确上限
// flags=0 表示同步关闭;CLOSE_RANGE_UNSHARE 等 flag 这里不需要
static double bench_close_range_full(unsigned int rlim_cur)
{
(void)rlim_cur;
auto t0 = Clock::now();
// last 传 ~0U(即最大值),内核会自动截断到进程实际可能的 fd 上限,
// 不需要我们手动传 rlim_cur,效果等价但更简洁
if (my_close_range(3, ~0U, 0) == -1) {
fprintf(stderr, "close_range 调用失败: %s\n", strerror(errno));
}
auto t1 = Clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count();
}
// 方式3:只关闭真实打开过的 fd 区间(模拟提前得知"最大已用 fd"的场景)
static double bench_close_range_real(unsigned int real_max_fd)
{
auto t0 = Clock::now();
if (my_close_range(3, real_max_fd, 0) == -1) {
fprintf(stderr, "close_range 调用失败: %s\n", strerror(errno));
}
auto t1 = Clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count();
}
// 方式4:读取 /proc/self/fd 目录,拿到当前进程真实打开的 fd 列表,
// 只对这些 fd 逐个调用 close(),不需要知道 rlim_cur,也不依赖 close_range。
// 这是内核 < 5.9 时的常见替代方案(很多开源项目,比如早期 Python subprocess
// 的实现思路就是这样)。
//
// 注意几个实现细节:
// 1. opendir("/proc/self/fd") 本身会占用一个新的 fd(目录句柄),
// 这个 fd 不能在遍历过程中被自己关掉,要跳过。
// 2. readdir 拿到的目录项里,除了数字命名的 fd(如 "0","1","2",...),
// 还会有 "." 和 ".." 这两个非数字项,需要过滤掉。
// 3. 先把要关闭的 fd 收集到一个 vector 里,再统一关闭,
// 避免一边遍历目录一边关闭 fd 导致目录内容被修改、遍历状态错乱
// (readdir 对"遍历过程中删除当前项之外的项"的行为在 POSIX 里未定义)。
static double bench_proc_fd_iterate()
{
auto t0 = Clock::now();
DIR* dir = opendir("/proc/self/fd");
if (!dir) {
fprintf(stderr, "opendir /proc/self/fd 失败: %s\n", strerror(errno));
auto t1 = Clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count();
}
int dir_fd = dirfd(dir); // 目录本身占用的 fd,遍历时要排除,不能关闭它
std::vector<int> fds_to_close;
fds_to_close.reserve(64);
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
// 过滤掉 "." ".." 以及非纯数字的异常项
if (entry->d_name[0] == '.') continue;
char* endptr = nullptr;
long fd = strtol(entry->d_name, &endptr, 10);
if (endptr == entry->d_name || *endptr != '\0') continue; // 不是合法数字,跳过
if (fd < 3) continue; // 跳过 stdin/stdout/stderr
if ((int)fd == dir_fd) continue; // 跳过 opendir 自己占用的 fd
fds_to_close.push_back((int)fd);
}
// 遍历完成、拿到完整列表之后,再统一关闭,避免遍历中途改变目录内容
for (int fd : fds_to_close) {
close(fd);
}
closedir(dir); // 最后关闭目录句柄本身
auto t1 = Clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count();
}
// 方式5:三层 fallback 的生产级实现——这是之前建议放进 my_shell::exec() 里
// 实际使用的版本,之前一直没有被放进 benchmark 里跑过、只停留在建议层面。
// 优先级:close_range(最快) > /proc/self/fd 遍历(次优,任何内核都能跑)
// > 原始 naive 循环(最后兜底,比如容器没挂载 /proc 的极端情况)
static void close_inherited_fds(unsigned int rlim_cur)
{
// 第一层:优先尝试 close_range,一次系统调用搞定,几乎零开销
if (my_close_range(3, ~0U, 0) == 0) {
return;
}
// errno == ENOSYS 说明内核太老不支持;其他错误也直接降级到下一层,
// 不在这里纠结具体错误码,保证兜底逻辑始终能跑通
// 第二层:读 /proc/self/fd,只关闭真实打开过的 fd,不盲目遍历整个 rlim_cur 区间
DIR* dir = opendir("/proc/self/fd");
if (dir) {
int dir_fd = dirfd(dir);
std::vector<int> fds;
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (entry->d_name[0] == '.') continue;
char* end = nullptr;
long fd = strtol(entry->d_name, &end, 10);
if (end == entry->d_name || *end != '\0') continue;
if (fd < 3 || (int)fd == dir_fd) continue;
fds.push_back((int)fd);
}
for (int fd : fds) close(fd);
closedir(dir);
return;
}
// 第三层:/proc 都读不了(极少见,比如某些受限容器没挂载 /proc),
// 才退回最原始的暴力遍历,也就是现有代码里的写法
for (unsigned int f = 3; f < rlim_cur; f++)
close(f);
}
static double bench_close_inherited_fds(unsigned int rlim_cur)
{
auto t0 = Clock::now();
close_inherited_fds(rlim_cur);
auto t1 = Clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count();
}
int main()
{
// 先尝试把软限制提升到硬限制,尽量拿到更大的可用 fd 空间来做测试。
// 注意:这只是"软限制提到硬限制",不是无中生有把硬限制调大——
// 硬限制通常需要 root 权限或系统配置(/etc/security/limits.conf)才能调整,
// 普通进程内是改不了硬限制本身的。
struct rlimit cur_limit;
if (getrlimit(RLIMIT_NOFILE, &cur_limit) == 0) {
if (cur_limit.rlim_cur < cur_limit.rlim_max) {
struct rlimit new_limit = cur_limit;
new_limit.rlim_cur = cur_limit.rlim_max;
if (setrlimit(RLIMIT_NOFILE, &new_limit) == 0) {
cur_limit = new_limit;
}
}
} else {
fprintf(stderr, "getrlimit 失败: %s\n", strerror(errno));
cur_limit.rlim_cur = 1024; // 兜底值,避免后面用到未初始化的值
}
fprintf(stderr, "当前实际 RLIMIT_NOFILE: soft=%lu, hard=%lu\n\n",
(unsigned long)cur_limit.rlim_cur, (unsigned long)cur_limit.rlim_max);
// 运行前先探测当前内核是否真的支持 close_range,
// 而不是假设"编译器认识 SYS_close_range 就等于内核支持"。
// 这两者是两回事:编译期只看头文件/自定义宏,运行期才看实际内核版本。
bool has_close_range = close_range_supported();
if (!has_close_range) {
fprintf(stderr,
"警告:当前内核不支持 close_range()(需要 Linux >= 5.9),\n"
"close_range 相关的两组测试会被跳过,只运行 naive_loop 对照组。\n"
"这种情况下也印证了原代码里 fallback 到逐个 close() 循环是必要的。\n\n");
}
// 模拟场景:父进程实际只打开了少量真实 fd(比如 5 个),
// 但系统 ulimit -n 配置得很大,对比不同 rlim_cur 下三种方式的耗时
const int real_fd_count = 5;
// 覆盖常见的 ulimit -n 配置区间:默认值、常见调大值、极端调大值
std::vector<unsigned int> rlim_values = {
204800, 409600, 1048576, 1073741816
};
printf("%-12s %-14s %-10s %-18s %-18s %-16s %-18s %-14s\n",
"rlim_cur", "real_fds", "naive(ms)",
has_close_range ? "close_range_full(ms)" : "close_range_full(N/A)",
has_close_range ? "close_range_real(ms)" : "close_range_real(N/A)",
"proc_fd_iter(ms)",
"close_inherited(ms)",
"naive/range_full");
printf("--------------------------------------------------------------------------------------------------------------------------\n");
for (unsigned int rlim : rlim_values) {
// --- 测 naive_loop ---
open_dummy_fds(real_fd_count);
double t_naive = bench_naive_loop(rlim);
// naive 循环用 [3, rlim) 整个区间逐个 close,已确保把 dummy fd 清理干净
// --- 测 proc_fd_iterate(不依赖 close_range,任何内核都能跑)---
open_dummy_fds(real_fd_count);
double t_proc_fd = bench_proc_fd_iterate();
// --- 测 close_inherited_fds(三层 fallback 的生产级实现,实际会走 close_range 那一层)---
open_dummy_fds(real_fd_count);
double t_inherited = bench_close_inherited_fds(rlim);
if (!has_close_range) {
// 内核不支持 close_range,明确跳过这两组,而不是让 syscall
// 静默失败、fd 未被真正关闭、还打印一个看似正常的假数字
printf("%-12u %-14d %-10.4f %-18s %-18s %-16.4f %-18.4f %-14s\n",
rlim, real_fd_count, t_naive, "N/A", "N/A", t_proc_fd, t_inherited, "N/A");
continue;
}
// --- 测 close_range_full ---
int max_fd = open_dummy_fds(real_fd_count);
double t_range_full = bench_close_range_full(rlim);
(void)max_fd;
// close_range 调用成功,已经关闭,不需要额外清理
// --- 测 close_range_real(只关闭真实用到的区间)---
max_fd = open_dummy_fds(real_fd_count);
double t_range_real = bench_close_range_real((unsigned int)max_fd);
printf("%-12u %-14d %-10.4f %-18.4f %-18.6f %-16.4f %-18.4f %-14.1f\n",
rlim, real_fd_count, t_naive, t_range_full, t_range_real, t_proc_fd, t_inherited,
t_range_full > 0 ? t_naive / t_range_full : 0.0);
}
if (!has_close_range) {
printf("\n(本机内核不支持 close_range,以上 naive_loop 数据仍然真实有效,\n"
" 只是无法给出 close_range 的对比数据。建议换一台 Linux >= 5.9 的机器重跑本测试。)\n");
}
printf("\n说明:\n");
printf("- naive: 对应现有代码 for(f=3;f<rlim_cur;f++) close(f) 的写法,耗时随 rlim_cur 线性增长\n");
printf("- close_range_full: 单次系统调用关闭整个区间,内核内部优化,不随 rlim_cur 线性增长\n");
printf("- close_range_real: 仅关闭真实打开过的 fd 范围,作为理论最优对照组\n");
printf("- proc_fd_iter: 读取 /proc/self/fd 拿到真实打开的 fd 列表再逐个 close,\n");
printf(" 不依赖 close_range 系统调用,任何内核版本都能用,是老内核下比 naive 更优的折中方案\n");
printf("- close_inherited: 三层 fallback 的生产级实现(close_range -> /proc 遍历 -> naive),\n");
printf(" 本机内核支持 close_range,所以实际会走第一层,耗时应接近 close_range_full\n");
printf("- 若当前系统内核 < 5.9,close_range 不可用,syscall 会返回 ENOSYS\n");
return 0;
}