Linux高性能服务器编程第九章IO复用
Linux高性能服务器编程第九章IO复用
Hoshea ZhangI/O复用使得程序能够同时监听多个文件描述符,这对提高程序的性能至关重要
- 客户端程序程序要同时处理多个socket
- 要同时处理用户输入和网络连接
- TCP服务器要同时处理监听socket和监听
需要指出的是,I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。
本章只讨论epoll
epoll系列系统调用
内核事件表
epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll有很大差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传人文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用如下epoll_create函数来创建:
1 |
|
size参数告诉内核事件表需要多大,该函数返回的文件描述符用作其他所有的epoll系统调用的第一个参数,以指定要访问的内核事件表:
1 |
|
epfd 事件表的文件描述符
op 指定操作类型
- EPOLL_CTL_ADD 往事件表注册fd事件
- EPOLL_CTL_MOD 修改fd上注册事件
- EPOLL_CTL_DEL 删除fd上注册事件
fd 要操作的文件描述符
event 指定事件,定义如下:
1
2
3
4
5struct epoll_event
{
_uint32_t events;/*epoll事件*/
epoll_data_t data;/*用户数据* /
};其中events成员描述事件类型。epoll支持的事件类型和poll基本相同。表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如 epoll 的数据可读事件是EPOLLIN。但epoll有两个额外的事件类型——EPOLLET和 EPOLLONESHOT。它们对于epoll的高效运作非常关键,我们将在后面讨论它们。data成员用于存储用户数据,其类型epoll_data_t的定义如下:
1
2
3
4
5
6
7typedef union epoll_data
{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;注意这是个联合体,只能使用一个成员。
epoll_wait
epoll系列系统调用的主要接口是epoll_wait函数
1 |
|
- timeout 指定超时值,timeout为-1时,将永远阻塞直到某个事件发生
- maxevents 最多监听多少个事件
- *events 事件数组
LT和ET模式
epoll对文件描述符的操作有两种模式:LT电平触发和ET边沿触发。
LT模式是默认的模式,ET需要注册EPOLLET事件。
LT检测到有事件发生,应用程序可以不立即处理,下一次调用epoll_wait时还会再次通告,直到该事件被处理。
ET检测到有事件发生必须立即处理该事件,后续的epoll_wait调用不再向应用程序通知这一事件
EPOLLONESHOT
即使我们使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程(或进程,下同〉在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个socket的局面。这当然不是我们期望的。我们期望的是一个socket连接在任一时刻都只被一个线程处理。这一点可以使用epoll 的 EPOLLONESHOT事件实现。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT 事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。但反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的 EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。