epoll 的常用宏和API汇总 - 详解
一、epoll 是干嘛的
Linux 下做高并发 I/O,用 epoll 的套路是:
建一个“事件池”:
epoll_create1得到epfd把你关心的 fd(socket)注册进去:
epoll_ctl(EPOLL_CTL_ADD, fd, events)在主循环里阻塞等事件:
epoll_wait(epfd, evlist, maxevents, timeout)等内核告诉你:“这几个 fd 有事件了(EPOLLIN/EPOLLOUT/错误等)”
二、最重要的三个系统调用
1) epoll_create1
int epfd = epoll_create1(EPOLL_CLOEXEC);
epoll_create1(0) = 正常创建
epoll_create1(EPOLL_CLOEXEC) = 正常创建 + 子进程 exec 时自动关闭,安全一些
返回一个 epoll 的“句柄”(其实就是 fd)
以后所有
epoll_ctl/epoll_wait都用它
宏:EPOLL_CLOEXEC
定义:一个标志位(int),告诉内核:
“如果以后这个进程
exec()启动别的程序,请自动关闭这个 epfd”作用:防止 fd 泄露给子进程
现在的代码:
epfd_ = ::epoll_create1(EPOLL_CLOEXEC);
2) epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* ev);
第二个参数 op 用的是这几个宏:
EPOLL_CTL_ADD
把一个新的 fd 加入 epoll 监控集合(开始被盯着)EPOLL_CTL_MOD
修改已经在 epoll 里的这个 fd 的“关注事件”(比如加上 EPOLLOUT)EPOLL_CTL_DEL
从 epoll 里移除,不再监控(一般 fd 关闭前调用)
现在代码:
epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
epoll_ctl(epfd_, EPOLL_CTL_MOD, fd, &ev);
epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, &ev);
这些就是实际用到的三个 op 宏。
3) epoll_wait
int n = epoll_wait(epfd, evlist, maxevents, timeout);
evlist:epoll_event数组,内核往里面填“就绪事件”maxevents:最多返回多少个timeout:-1:一直阻塞0:不阻塞,立刻返回>0:最多等这么久(毫秒)
现在就是用 -1 无限等:
int n = ::epoll_wait(epfd_, evlist_.data(),
static_cast(evlist_.size()), -1);
三、struct epoll_event 和 “事件宏”
epoll 事件结构:
struct epoll_event {
uint32_t events; // 事件标志(下面这些 EPOLLxxx)
epoll_data_t data; // 用户数据(你随便放,fd 或 ptr)
};
events 是一个“按位或的标志位”,也就是:
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
我的项目里常用的“事件宏”有哪些?
1) 可读 / 可写类
EPOLLIN
定义:一个 bit 标志
作用:
对于
listenfd:表示有新连接可以accept()对于普通 socket:表示这个 fd 上有数据可以
read(),不会阻塞
现在用它:
注册监听:
addFd(listenfd_, EPOLLIN, this)普通连接可读时:
if (events & EPOLLIN) onConnRead(c);
EPOLLOUT
定义:一个 bit 标志
作用:fd 可写(发送缓冲区有空间),写不会阻塞
代码里用它:
reactor_.modFd(fd, EPOLLIN | EPOLLOUT, &c);
...
if (events & EPOLLOUT) onConnWrite(c);
即:
没数据要写 → 只关心 EPOLLIN
有数据要写 → 再加 EPOLLOUT,等内核告诉你“可以写了”
2) 异常 / 关闭类
EPOLLERR
有错误(例如写到一个已 reset 的 socket)
EPOLLHUP
挂起(通常是对端关闭,或者某种异常断开)
现在在 onEvent 里这样用:
if (events & (EPOLLERR | EPOLLHUP)) {
closeConn(fd);
return;
}
意思就是:
如果这个 fd 出错或挂了,就直接关掉连接。
3) 行为控制类
EPOLLET —— 边缘触发(Edge Triggered)
定义:一个 bit 标志,表示 epoll 在这个 fd 上用“边缘触发”
默认是 “电平触发”(LT:Level Triggered),只要缓冲区还有数据,每次 epoll_wait 都会返回
加上
EPOLLET后:只有从“无到有”那一刻会触发一次
结果:
LT:可以适当懒一点,read 一次不读完,下次还会通知
ET:必须循环 read / write 到 EAGAIN / EWOULDBLOCK,否则可能永远收不到下一次通知
现在:
ev.events = events | (useET_ ? EPOLLET : 0);
配合 onConnRead/onConnWrite 里 for(;;) + EAGAIN,这个用法是标准 ET 写法。
EPOLLONESHOT(你暂时没用,知道一下)
定义:一次性事件
作用:事件触发一次后,自动“冻结”这个 fd,不会再给这个 fd 推事件,除非你再次
EPOLL_CTL_MOD激活多线程框架里经常用它保证“一个 fd 的某次事件只被一个线程处理一次”
4) epoll_data 的用法(和 void* user 有关)
data 是一个 union:
union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
现在的策略是:
用
ev.data.fd = fd;真正的 user 指针放在
users_[fd]这个 map 里
在 loop() 里:
int fd = evlist_[i].data.fd;
...
void* user = nullptr;
{
std::lock_guard lk(users_mtx_);
auto it = users_.find(fd);
if (it != users_.end()) user = it->second;
}
dispatcher_(fd, ev, user);
四、配套的几个常见“宏和标志”
虽然不是 epoll_ 开头,但跟它一起用得非常多:
1) O_NONBLOCK(非阻塞)
用在
fcntl(fd, F_SETFL, ...)作用:让 fd 变成非阻塞
非阻塞 + epoll 才是“正确姿势”
在 setNonBlock 里这样用:
int fl = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
2) FD_CLOEXEC
用在
fcntl(fd, F_SETFD, flags | FD_CLOEXEC)作用:exec 新程序时自动关闭这个 fd,防止泄漏到子进程
3) EAGAIN / EWOULDBLOCK
这两个不是宏常量,而是 errno 错误码,用在你循环读 / 写时判断“已经读完/写完了”:
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
对非阻塞 fd 来说:
read 返回 -1 且 errno = EAGAIN/EWOULDBLOCK → 现在没有数据,再读会阻塞
write 同理 → 发送缓冲区暂时满了
五、总表
| 宏名 | 类别 | 用法位置 | 作用简记 |
|---|---|---|---|
EPOLL_CTL_ADD | epoll_ctl op | addFd() | 加入监控 |
EPOLL_CTL_MOD | epoll_ctl op | modFd() | 修改事件 |
EPOLL_CTL_DEL | epoll_ctl op | delFd() | 删除监控 |
EPOLLIN | events 标志 | addFd / onEvent | 可读/有新连接 |
EPOLLOUT | events 标志 | modFd / onEvent | 可写 |
EPOLLERR | events 标志 | onEvent | 出错 |
EPOLLHUP | events 标志 | onEvent | 挂起/关闭 |
EPOLLET | events 标志 | addFd / modFd | 边缘触发 |
EPOLL_CLOEXEC | create 标志 | epoll_create1() | exec 时自动关闭 epfd |
再配合:
O_NONBLOCK:非阻塞 IOFD_CLOEXEC:exec 时自动关闭 fdEAGAIN/EWOULDBLOCK:非阻塞读写结束条件
浙公网安备 33010602011771号