readdir_r弃用的性能影响及FUSE文件系统多线程同步疑问
我之前做叠加型FUSE文件系统的时候也踩过这个readdir的坑,刚好帮你理清楚这些问题:
首先明确readdir的线程安全边界
POSIX标准说得很清楚:readdir() 针对不同的目录流(也就是不同的DIR*指针)是线程安全的,但如果多个线程同时操作同一个DIR*,那不管是单个readdir()调用,还是整个读取目录的流程,都是完全不安全的。
为什么?因为DIR结构体内部藏着当前读取的偏移位置、临时缓冲区这些状态,readdir()每次调用都会修改这些状态。多线程并发操作同一个DIR*的话,轻则导致返回的目录条目重复、缺失,重则直接让程序崩溃——毕竟你没法预料两个线程同时修改同一个内存区域会搞出什么乱子。
同步范围该怎么选?
你的纠结很正常,但其实最优解根本不是在“单个调用加锁”和“整个流程加锁”里选:
- 如果你给单个
readdir()调用加锁,看起来粒度细,但实际没用——就算每个调用都加锁,多个线程共享同一个DIR*的话,读取的条目会被打乱(比如线程A读了2个条目,线程B插进来读3个,再换回A继续读),这完全不符合用户进程对“读取整个目录”的预期,而且锁竞争的开销也没少多少。 - 如果你把整个读取流程放进临界区,那所有读取该目录的请求都会被串行化,高并发场景下性能会崩得很惨。
真正的最优方案:不要共享DIR*
每个需要读取目录的线程/请求,都独立调用opendir()打开自己的DIR*,读完后立刻closedir()关闭。这样每个线程都有自己的目录流,完全不需要加锁,天然线程安全。
你可能担心重复打开目录的性能开销?其实大可不必——Linux内核的目录缓存(dentry cache + page cache)会把目录内容存在内存里,只要不是第一次读取,后续的opendir()和readdir()基本都是从内存里拿数据,磁盘IO开销可以忽略不计。反而共享DIR*加锁的方案,在高并发下的锁竞争才是真正的性能杀手。
关于Linux内核的单目录并行读取限制
内核根本没有这种限制!相反,内核的设计就是支持多进程/线程并行读取同一个目录的——每个进程/线程打开目录时,内核都会给它分配独立的file结构体,各自维护自己的读取位置,互相完全不干扰。只要你的FUSE文件系统内部为每个readdir请求独立处理目录读取,就能充分利用内核的并行能力,不会有瓶颈。
最后说下readdir_r的弃用
readdir_r被弃用不是因为线程安全问题,而是它本身的设计缺陷:比如你得自己分配缓冲区,但很难确定缓冲区的大小,容易导致溢出或者浪费内存。现在推荐的替代方案就是用普通的readdir(),只要保证每个线程用自己独立的DIR*,就完全是线程安全的,比readdir_r好用多了。
内容的提问来源于stack exchange,提问作者Lapsio

