本文摘自写给应用开发的 Android Framework 教程,完整教程请查阅 yuandaimaahao.github.io/AndroidFram… 更为详细的视频教程与答疑服务,请联系微信 zzh0838
什么是 IO 多路复用
在 Linux 中:
- IO 就是对文件的读写操作
- 多路是指同时读写多个文件
- 复用是指使用一个程序处理多个文件的同时读写
问题来了,为什么需要多路复用,为了快,要给每一个 fd 通道最快的感受,要让每一个 fd 觉得,你只在给他一个人跑腿。
为了更快的处理多路 IO,大体有两种方案:
- 一种方案是:一个 IO 请求(比如 write )对应一个线程来处理,但是线程数多了,性能反倒会差。
- 另外一种方案是: IO 多路复用
接下来,我们就来看看 IO 多路复用:
我不用任何其他系统调用,能否实现 IO 多路复用?
可以的,写个 for 循环,每次都尝试 IO 一下,读/写到了就处理,读/写不到就 sleep 下。
while(true)
{
foreach fd数组
{
read/write(fd, /* 参数 */)
}
sleep(1s)
}
默认情况下,我们没有加任何参数 create 出的 fd 是阻塞类型的。我们读数据的时候,如果数据还没准备好,是会需要等待的,当我们写数据的时候,如果还没准备好,默认也会卡住等待。所以,在上面伪代码中的 read/write
是可能被直接卡死,而导致整个线程都得到不到运行。只需要把 fd 都设置成非阻塞模式。这样 read/write
的时候,如果数据没准备好,返回 EAGIN 的错误即可,不会卡住线程,从而整个系统就运转起来了。
这个实现只是为了帮助我们理解 IO 多路复用,实际上,上面的实现在性能上有很大的缺陷。for 循环每次要定期 sleep 1s,这个会导致吞吐能力极差,因为很可能在刚好要 sleep 的时候,所有的 fd 都准备好 IO 数据,而这个时候却要硬生生的等待 1s。
IO 多路复用就是 1 个线程处理 多个 fd 的模式。我们的要求是:这个 “1” 就要尽可能的快,避免一切无效工作,要把所有的时间都用在处理句柄的 IO 上,不能有任何空转,sleep 的时间浪费。
为了实现上诉的功能,内核提供了 3 种系统调用 select,poll,epoll。
这 3 种系统调用都能够管理 fd 的可读可写事件,在所有 fd 不可读不可写无所事事的时候,可以阻塞线程,切走 cpu 。fd 可读写的时候,对应线程会被唤醒。
三者的差异主要是在性能上,epoll 的性能是强于 select 和 poll 的,我们接下来就来看看 epoll 的具体使用。
epoll 的使用
使用 epoll 需要以下三个系统调用:
//头文件
#include
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- epollcreate 负责创建一个池子,一个监控和管理句柄 fd 的池子;
- epollctl 负责管理这个池子里的 fd 增、删、改;
- epollwait 就是负责打盹的,让出 CPU 调度,但是只要有“事”,立马会从这里唤醒;
接下来我们看个示例程序:
使用 epoll_create 创建一个管理 fd 的池子
epollfd = epoll_create(1024);
if (epollfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
}
这个池子对我们来说是黑盒,这个黑盒是用来装 fd 的,我们暂不纠结其中细节。我们拿到了一个 epollfd ,这个 epollfd 就能唯一代表这个 epoll 池。
然后,我们就要往这个 epoll 池里放 fd 了,这就要用到 epoll_ctl 了
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
我们就把 fd 放到这个池子里了,EPOLL_CTL_ADD 表明操作是增加 fd,最后一个参数是 epoll_event 结构体:
struct epoll_event {
__uint32_t events; /* Epoll 事件 */
epoll_data_t data; /* 用户数据 */
};
epoll_event 的第一个成员 events 用于指定我们监听的 fd 事件类型,常见的值有:
- EPOLLIN:可读事件
- EPOLLOUT:可写事件
多个值可以通过或操作同时生效:
epoll_event event;
// 同时监听可读可写事件
event.events = EPOLLIN | EPOLLOUT;
最后,我们需要调用 epoll_wait 进入休眠状态,可读或可写事件到来时,醒休眠中的程序从 epoll_wait 处被唤醒。
其使用方法,通常如下:
while (true)
{
// epollfd 是 epoll_create 的返回值
// events 是一个 epoll_event 的数组,用于存储收到的多个事件
// EPOLL_SIZE 用于设定最多监听多少个事件
// 最后一个参数 -1 用于指定阻塞时间上限,-1:表示调用将一直阻塞
int count = epoll_wait(epollfd, events, EPOLL_SIZE, -1);
if (count < 0)
{
perror("epoll failed");
break;
}
for (int i=0;i < count;i++)
{
//处理可读或可写事件
}
}
参考资料
- 深入理解 Linux 的 epoll 机制
- Linux下的I/O复用技术 之 epoll为什么更高效 ?
- epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析
- epoll LT 模式和 ET 模式详解