如何在 Linux 上通过 C API 判断给定的 fd 的类型?

码中飞翔 2019-06-27

最近接到一个任务,需要判断传过来的 fd 是不是属于 eventfd/signalfd 这一类特定的 fd。因为这一类 fd 不支持某些操作,如果调用时不加判断,会报 Invalid Argument 错误。按理说,如果能把 fd 类型作为一个额外的参数传进来,就能轻松解决问题了。不过因为一些限制,拿到手时只有 fd 这一个整数。好在需要过滤的地方不在重要的路径上,所以 hack 一点的办法应该也能被接受。

既然 Linux 能够根据一个整数 fd 调用特定的操作,那么内核层面上一定会有一个 fd 到实际文件类型的映射。所以最好的打算,就是 Linux 把这个映射通过 syscall 的方式暴露出来了,然后我只要调用它,就能完成工作了。可惜找了一段时间,依然没有找到对应的 syscall。看来是不存在这样的好事了。

补充一个前提,我正在编写的是 C 代码。如果写的是脚本的话,至少有两种方法可以获取特定 fd 的类型。

一种是调用 lsof -p $pid -a -d $fd

¥ lsof -p 16166 -a -d 15
COMMAND   PID USER   FD      TYPE             DEVICE SIZE/OFF       NODE NAME
xxxxx   16166  lzx   15u  a_inode               0,13        0      10094 [eventfd]

我们可以看到 进程 16166 的第 15 号 fd 是一个 eventfd。

另一种是调用 cat /proc/$pid/fdinfo/$fd

¥ cat /proc/16166/fdinfo/16
pos:    0
flags:    02004002
mnt_id:    13
sigmask:    0000000000010000

该操作并不会直接显示 fd 的类型。但是因为不同类型的 fdinfo 格式不一样,你可以根据 man procfs 里面的说明,解析出具体 fd 的类型。比如 进程 16166 的第 16 号 fd 是一个 signalfd。

毫无疑问,对于 C 程序,上面两种方法都不太合适。虽说并不是无法实现,但是你让一个 C 程序每次判断 fd 类型时都要读完整个 fdinfo 文件,然后再通过一个 parser 解析出具体的 fd 类型,无论从性能上还是代码量上都不可接受。

也许存在某个 API,可以直接获取 fd 类型?为什么不看看神奇的 lsof 的源码呢?

抱着源码里可能隐藏着谜底的想法,我下载了 lsof 的代码。打开底下一个名为 linux 的文件夹,看了里面的文件几眼,我感觉被欺骗了。

没想到 lsof 你这个浓眉大眼的,居然也是用读 /proc 的方式来获取 fd 信息的!
看来这条路也断了。难道真的非得去解析 fdinfo 不可吗……

仔细看多几眼,lsof 貌似只是从 /proc/$pid/fd 里面读取了基本的信息。有些信息,比如我想要的 NAME,不是直接从那里面获取的。看来还是有希望的。我试着找找看,看看有什么关键的操作。

以下略去一些曲折的碰壁和反复的试错…… 反正我费了若干个小时,走了些弯路,终于明白汲汲以求的 NAME 是怎么得到的。其实就是 readlink /proc/$pid/fd/$fd!

对于 regular file,readlink /proc/$pid/fd/$fd 返回的是文件路径。而对于 eventfd/signalfd 这一类不可能有真正的 inode 的文件,返回的是 anon_inode:[eventfd]。于是我需要做的事情相对简单了:
先拼接 readlink 的文件名参数,然后调用 readlink,解析返回来的文件名。由于我只需要知道某个整数 fd 在本进程内的类型,可以用 /proc/self 代替 /proc/$pid。这么一来,我就只需要拼接后面的 $fd 部分。因为 fd 是一个整数,整个文件名 buffer 大小是有上限的,而且这个上限不大。所以可以把这个 buffer 弄成静态分配的,不用操心内存管理。接下来是 readlink 这个系统调用,原来最好的打算里面,也是逃不开一次系统调用的,所以这部分不算额外的开销。最后是对返回文件名的解析,凭借该文件名有着固定前缀的特性,我们可以先 strncmp 判断是否是 anon_inode:[,接着一路解析出方括号内具体的 fd 类型。也不是很难就是了。


后来看了相关的内核代码,估计应该不会有更好的方法了。

下面是 eventfd 创建代码:

/* fs/eventfd.c */
    struct file *file;
    ...
    file = anon_inode_getfile("[eventfd]", &eventfd_fops, ctx,
                  O_RDWR | (flags & EFD_SHARED_FCNTL_FLAGS));

你可以这么认为,anon_inode_get_file 返回了 file 的一个实例,而这个实例组合了 eventfd_fops 这个 eventfd 功能的具体实现。eventfd 之所以跟其他 fd 不同,全靠 eventfd_fops 里面定义的方法。如果内核要想提供一个 API,返回某个 fd 的类型,最好是每个 fd 都实现返回自己的类型方法。但是 eventfd_fops 这个结构体里面,没法返回 eventfd 标识的方法。有一个 show_fdinfo 的方法,不过这个方法是提供给 /proc/$pid/fdinfo 展示用的。另外 eventfd_fops 这个结构体是 static 的,所以也排除存在 if (file_operation == &eventfd_fops) return enum_eventfd; 这样的代码的可能性。

signalfd 的情况也差不多。


最后我没有采用 readlink 的方式,而是改由创建特殊 fd 的代码顺便把 fd 编号记录下来,接收到 fd 之后,我再去查找,看看是否是特殊的 fd。

相关推荐