在Linux系统中,每个进程都受一组资源限制(resource limits,简称rlimit)的约束,比如最大打开文件数(RLIMIT_NOFILE)、最大内存锁定(RLIMIT_MEMLOCK)、用户进程数上限(RLIMIT_NPROC)等。当线上服务出现“Too many open files”或者fork失败时,第一时间想到的就是查看进程的rlimit设置。传统做法是在用户态通过/proc/<pid>/limits读取,但如果需要在内核态实时观测——比如统计哪些进程频繁逼近文件句柄上限——就需要借助eBPF直接访问内核的task_struct结构。bpf_get_current_task辅助函数正是完成这件事的关键入口。

一、理解rlimit与task_struct中的存储结构
Linux内核使用struct rlimit来描述一项资源限制,它只有两个成员:rlim_cur表示软限制,rlim_max表示硬限制。每个进程的所有资源限制存放在task_struct->signal->rlim数组中,数组按资源编号索引,RLIMIT_NOFILE对应索引7,RLIMIT_NPROC对应6,RLIMIT_MEMLOCK对应8,以此类推。
内核提供了一个便捷的辅助函数task_rlimit_max(task, rlim)和task_rlimit(task, rlim)来读取硬限制和软限制,但eBPF程序无法直接调用这些内联函数,必须通过BPF CO-RE(Compile Once, Run Everywhere)机制手动追踪字段偏移。由于signal是指向signal_struct的指针,且受RCU保护,在BPF程序中访问时建议使用bpf_rcu_read_lock(较新内核支持)或确认处于可睡眠上下文之外的安全路径。
常见的资源编号定义如下,编写代码时需要对照内核头文件include/uapi/asm-generic/resource.h:
#define RLIMIT_CPU 0 /* CPU时间限制,秒 */ #define RLIMIT_FSIZE 1 /* 文件最大尺寸 */ #define RLIMIT_DATA 2 /* 数据段最大值 */ #define RLIMIT_STACK 3 /* 栈最大值 */ #define RLIMIT_CORE 4 /* core文件大小 */ #define RLIMIT_RSS 5 /* 常驻内存最大值 */ #define RLIMIT_NPROC 6 /* 用户进程数上限 */ #define RLIMIT_NOFILE 7 /* 打开文件数上限 */ #define RLIMIT_MEMLOCK 8 /* 锁定内存上限 */ #define RLIMIT_AS 9 /* 地址空间上限 */
二、编写读取rlimit的BPF程序
下面以libbpf为例,编写一个挂载在sys_enter_openat跟踪点上的BPF程序,每当进程打开文件时就记录它的RLIMIT_NOFILE值和当前已打开的文件数,从而发现即将触碰上限的进程。
程序的核心逻辑分三步:第一步通过bpf_get_current_task拿到当前进程的task_struct指针;第二步用BPF CO-RE读取signal->rlim[RLIMIT_NOFILE];第三步将结果写入ring buffer供用户态消费。完整代码如下:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct rlimit_event {
__u32 pid;
__u64 rlim_cur; /* 软限制 */
__u64 rlim_max; /* 硬限制 */
char comm[16];
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_open_rlimit(void *ctx)
{
struct rlimit_event *e;
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(e->comm, sizeof(e->comm));
/* 获取当前进程的task_struct */
struct task_struct *task = (void *)bpf_get_current_task();
/* 读取信号结构体指针 */
struct signal_struct *sig = BPF_CORE_READ(task, signal);
/* 读取rlim数组中的RLIMIT_NOFILE项(索引7) */
struct rlimit rl = BPF_CORE_READ(sig, rlim[7]);
e->rlim_cur = rl.rlim_cur;
e->rlim_max = rl.rlim_max;
bpf_ringbuf_submit(e, 0);
return 0;
}需要注意几点细节。第一,rlim是一个内嵌数组,BPF_CORE_READ(sig, rlim[7])这种写法在libbpf 0.6以上版本可用,它会通过BTF信息自动计算数组元素的偏移;如果使用较老版本的libbpf,需要拆成BPF_CORE_READ_ARRAY风格的显式访问。第二,RLIM_INFINITY的值是~0ULL,在用户态展示时要特殊处理成“无限制”,否则显示一个巨大的数字会让运维人员困惑。第三,验证器对指针解引用检查严格,signal指针必须通过BPF_CORE_READ读取而不是直接解引用,否则程序会加载失败。
三、用户态加载与数据处理
用户态程序的职责是加载BPF程序、消费ring buffer事件并格式化输出。以下是基于libbpf的完整骨架代码:
#include <stdio.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include <unistd.h>
#include "rlimit_trace.skel.h"
static volatile bool exiting = false;
static void handle_event(void *ctx, int cpu, void *data, __u32 size)
{
struct rlimit_event *e = data;
if (e->rlim_cur == ~0ULL)
printf("pid=%-7d comm=%-16s NOFILE soft=unlimited\n",
e->pid, e->comm);
else
printf("pid=%-7d comm=%-16s NOFILE soft=%llu hard=%llu\n",
e->pid, e->comm, e->rlim_cur, e->rlim_max);
}
int main(void)
{
struct rlimit_trace *skel;
struct ring_buffer *rb;
skel = rlimit_trace__open_and_load();
if (!skel) {
fprintf(stderr, "加载BPF程序失败\n");
return 1;
}
rb = ring_buffer__new(bpf_map__fd(skel->maps.events),
handle_event, NULL, NULL);
rlimit_trace__attach(skel);
while (!exiting) {
ring_buffer__poll(rb, 100);
sleep(1);
}
ring_buffer__free(rb);
rlimit_trace__destroy(skel);
return 0;
}运行该程序后,每当有进程调用open系统调用,终端就会打印出该进程的NOFILE软硬限制。将其与/proc/<pid>/fd目录下的文件描述符数量对比,即可计算句柄使用率。一个实用的改进方向是在BPF程序里维护一个按pid聚合的map,记录每个进程的open调用计数,当计数达到软限制的90%时输出告警事件,这样就能在资源耗尽前主动预警,而不是等错误发生后再排查。
四、内核版本兼容性与常见坑
rlim数组在signal_struct中的位置长期保持稳定,从2.6时代至今没有大的变动,这让BPF CO-RE的重定位几乎不会失败。但仍有几个兼容性问题值得关注。首先,如果目标内核没有开启CONFIG_DEBUG_INFO_BTF,vmlinux.h无法生成,此时可以退回到硬编码偏移的方式,通过bpf_probe_read_kernel配合手动计算的offset读取,但这种方式在内核升级后可能失效。其次,在容器环境中,bpf_get_current_task返回的是宿主机视角的task_struct,pid是内核态的tgid,与容器内namespace的pid不同,输出时若要显示容器内pid,需要额外读取nsproxy相关字段做转换。
另一个常见的坑是忽略RLIM_INFINITY的判断。有些工具直接把rlim_cur当普通数字累加统计,遇到无限制的进程会产生溢出或误报。此外,如果要在非跟踪点上下文(比如socket过滤器)中访问task_struct,要确认该上下文允许调用bpf_get_current_task,部分程序类型的验证器会拒绝此调用。建议先用bpftool prog load做干跑测试,提前发现验证器报错。
总结来看,借助bpf_get_current_task配合CO-RE读取signal->rlim数组,可以在内核态以极低开销获取任意进程的资源限制,结合ring buffer和用户态聚合逻辑,很容易扩展成一套完整的rlimit监控与预警工具,为排查文件句柄耗尽、进程数超限等经典线上问题提供第一手数据。
eBPFbpf_get_current_task_rlimit进程资源限制修改时间:2026-09-02 04:04:34