在 Unix 哲学中,“一切皆文件”。但在 I/O 多路复用的世界里,文件和文件是不一样的。Socket、Pipe、TTY 和普通磁盘文件在 Libevent 中的待遇天差地别。
1. 管道 (Pipe) 与 FIFO
管道(匿名管道 pipe 或命名管道
mkfifo)在内核中是有缓冲区的。 *
支持度: 完美支持。 * 机制:
它们表现得就像 Socket
一样。当缓冲区有数据时可读,有空间时可写。 *
用法: 直接使用 event_new 或
bufferevent_socket_new 即可。
int fds[2];
pipe(fds);
// 监听读端
struct event *ev = event_new(base, fds[0], EV_READ | EV_PERSIST, read_cb, NULL);
event_add(ev, NULL);2. 普通磁盘文件 (Regular Files)
这是新手最容易踩的坑:你不能用 epoll
去监听一个普通文件(如
/var/log/syslog)。
2.1. 为什么 epoll 不支持?
epoll(以及 select,
poll)的设计初衷是监控“可能会阻塞”的 I/O。
对于普通磁盘文件,Linux
内核认为它总是就绪的(Always Ready)。 *
读: 即使数据不在 Page Cache 中,内核也会发起磁盘 I/O
并挂起进程,但这被视为“阻塞读”,而不是“未就绪”。 * 写:
只要不超过磁盘配额,写入总是立即成功的(写入 Page
Cache)。
如果你把一个文件 fd 加入 epoll,epoll_wait
会立即返回,导致你的 Event Loop 变成 100%
CPU 的死循环(Busy Loop)。
2.2. 解决方案
既然不能用 Reactor 模式处理文件 I/O,我们该怎么办?
方案 A: 阻塞 I/O (简单粗暴)
如果文件读写量很小(如读取配置文件),直接在 Event Loop 中阻塞读取也无伤大雅。但如果是大文件,绝对禁止。
方案 B: 线程池 (Thread Pool)
这是最通用的做法。 1.
主线程将文件读写任务投递给工作线程池。 2.
工作线程阻塞读写文件。 3. 完成后,通过管道或
event_active 通知主线程。
方案 C:
零拷贝发送 (evbuffer_add_file)
如果你只是想把文件发送给网络对端(如 HTTP
Server),Libevent 提供了 evbuffer_add_file。
它利用 sendfile 系统调用,在内核层面完成“磁盘
-> 网卡”的数据传输,完全不占用 Event Loop 的 CPU
时间。
3. 文件变更监控
除了读写内容,我们有时还需要监控文件元数据的变化(如文件被修改、删除)。
3.1. Linux: inotify
Linux 提供了 inotify
机制。inotify_init 返回的是一个文件描述符。
惊喜: 这个 fd 是支持 epoll 的!
所以,你可以把 inotify 的 fd 注册到 Libevent
中,当文件发生变化时,回调函数会被触发。
3.2. BSD/macOS: kqueue
kqueue 原生支持
EVFILT_VNODE,可以直接监控文件系统的变化,无需额外的
fd。
4. 总结
- Pipe/Socket/TTY: 放心使用 Libevent。
- Disk File (Read/Write):
禁止直接加入 Event Loop。请使用线程池或
sendfile。 - Disk File (Watch): 可以通过
inotify(Linux) 或kqueue(BSD) 的 fd 集成到 Libevent 中。
上一篇: 03-events/signal.md - 信号处理 下一篇: 04-architecture/threading.md - 线程安全与锁