http://blog.csdn.net/seekerzhou/article/details/8146616
1. 绪论
本文对linux下几种常用的多路复用模型进行总结,不涉及其他操作系统如freeBSD等的多路复用技术。
2. 多路复用模型介绍
一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。
3. 常用的多路复用模型
3.1 select
1) 相关API
- int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
对fd_set的一些操作宏:
- FD_ZERO(fd_set *set) - clears a file descriptor set
- FD_SET(int fd, fd_set *set) - adds fd to the set
- FD_CLR(int fd, fd_set *set) - removes fd from the set
- FD_ISSET(int fd, fd_set *set) - tests to see if fd is in the set
FD_ZERO(fd_set *set) - clears a file descriptor set
FD_SET(int fd, fd_set *set) - adds fd to the set
FD_CLR(int fd, fd_set *set) - removes fd from the set
FD_ISSET(int fd, fd_set *set) - tests to see if fd is in the set
2) select机制的原理介绍
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
3) select服务器伪码:
- array[slect_len]; nSock=0; array[nSock++]=listen_fd; maxfd=listen_fd;
- while {
- FD_ZERO(&set);
- foreach (fd in array) {
- fd大于maxfd,则maxfd=fd
- FD_SET(fd,&set)
- }
- res=select(maxfd+1,&set,0,0,0);
- if(FD_ISSET(listen_fd,&set)) {
- newfd=accept(listen_fd);
- array[nsock++]=newfd;
- if(--res<=0) continue
- }
- foreach 下标1开始 (fd in array) {
- if(FD_ISSET(fd,&set))
- 执行读等相关操作,如果错误或者关闭,则要删除该fd,将array中相应位置和最后一个元素互换就好,nsock减一
- if(--res<=0) continue
- }
- }
array[slect_len]; nSock=0; array[nSock++]=listen_fd; maxfd=listen_fd;
while {
FD_ZERO(&set);
foreach (fd in array) {
fd大于maxfd,则maxfd=fd
FD_SET(fd,&set)
}
res=select(maxfd+1,&set,0,0,0);
if(FD_ISSET(listen_fd,&set)) {
newfd=accept(listen_fd);
array[nsock++]=newfd;
if(--res<=0) continue
}
foreach 下标1开始 (fd in array) {
if(FD_ISSET(fd,&set))
执行读等相关操作,如果错误或者关闭,则要删除该fd,将array中相应位置和最后一个元素互换就好,nsock减一
if(--res<=0) continue
}
}
4)select的特点:
•可监控的fd个数取决与sizeof(fd_set)的值,同是还会受限于内核的设置,系统默认支持1024,可以通过命令修改,但治标不治本。
•select每次只返回有事件的fd,没有事件的fd被清空,需另保存所有注册的fd,在每次select调用前清理fd_set,重新注册,同时取出最大的fd+1作为每一个参数。
•需要遍历所有注册的fd判断是否有事件发生
3.2 poll
1) 相关API
- int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
其中struct pollfd的定义如下:
- struct pollfd {
- int fd; /* file descriptor */
- short events; /* requested events */
- short revents; /* returned events */
- };
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
2) poll实现的服务器伪代码:
- struct pollfd fds[POLL_LEN];
- unsigned int nfds=0;
- fds[0].fd=server_sockfd;
- fds[0].events=POLLIN|POLLPRI;
- nfds++;
- while {
- res=poll(fds,nfds,-1);
- if(fds[0].revents&(POLLIN|POLLPRI)) {
- 执行accept并加入fds中,nfds++
- if(--res<=0) continue
- }
- 循环之后的fds,if(fds[i].revents&(POLLIN|POLLERR )) {
- 读操作或处理异常等
- if(--res<=0) continue
- }
- }
struct pollfd fds[POLL_LEN];
unsigned int nfds=0;
fds[0].fd=server_sockfd;
fds[0].events=POLLIN|POLLPRI;
nfds++;
while {
res=poll(fds,nfds,-1);
if(fds[0].revents&(POLLIN|POLLPRI)) {
执行accept并加入fds中,nfds++
if(--res<=0) continue
}
循环之后的fds,if(fds[i].revents&(POLLIN|POLLERR )) {
读操作或处理异常等
if(--res<=0) continue
}
}
3) poll机制的特点
•fd个数不再有上限限制,可以将参数ufds想象成栈底指针,nfds是栈中元素个数,该栈可以无限制增长。
•pollfd结构将fd信息、需要监控的事件、返回的事件分开保存,则poll返回后不会丢失fd信息和需要监控的事件信息,也就省略了select模型中前面的循环操作,但返回后的循环仍然不可避免。另外每次poll操作会自动把上次的revents清空,不需要再清理。
3.3 epoll
1) 相关API
- 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);
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);
- typedef union epoll_data {
- void *ptr;
- int fd;
- __uint32_t u32;
- __uint64_t u64;
- } epoll_data_t;
- struct epoll_event {
- __uint32_t events; /* Epoll events */
- epoll_data_t data; /* User data variable */
- };
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
其中op的定义如下:
2) epoll机制的特点
- op: EPOLL_CTL_ADD、EPOLL_CTL_DEL
op: EPOLL_CTL_ADD、EPOLL_CTL_DEL
2) epoll机制的特点
•它保留了poll的两个相对与select的优点。
•epoll_wait的参数events作为出参,直接返回了有事件发生的fd,epoll_wait的返回值既是发生事件的个数,省略了poll中返回之后的循环操作。
•不再象select、poll一样将标识符局限于fd,epoll中可以将标识符扩大为指针,大大增加了epoll模型下的灵活性。
3) epoll机制的优点:
•支付FD的数目大(1GB内存的机器上大约是10万左右),epoll用红黑树来存储事件。
•只响应活跃的FD,活跃的FD会主动callback。
•使用mmap加速内核与用户空间的消息传递。
epoll的设计思路,是把select/poll单个的操作拆分为1个epoll_create+多个epoll_ctl+一个epoll_wait。 由于在执行epoll_create和epoll_ctrl时,已经把用户态的信息保存到内核态了,所以之后即使反复地调用epoll_wait,也不会重复地拷贝参数,扫描文件描述符,反复地把当前进程放入/放出等待队列。
4) epoll实现的服务器伪码
- ....
- nfds = epoll_wait(kdpfd, events, maxfds, timeout);
- if (-1 == nfds) { ErrorLog("epoll_wait return -1!"); }
- for (i = 0, cevents = events; i < nfds; i++, cevents++)
- {
- iFDTemp = cevents->data.fd;
- ......
- if (0 != (EPOLLERR & cevents->events)) { ...... }
- if (0 == (EPOLLIN & cevents->events)) { ...... }
- if (LISTEN_SOCKET == m_pSocketInfo->m_iSocketType)
- {
- iNewSocket = accept(iFDTemp, (struct sockaddr *)&m_stSockAddr, (socklen_t *)&iSockAddrSize);
- ......
- }
- else
- {
- RecvClientData(iFDTemp);
- }
....
nfds = epoll_wait(kdpfd, events, maxfds, timeout);
if (-1 == nfds) { ErrorLog("epoll_wait return -1!"); }
for (i = 0, cevents = events; i < nfds; i++, cevents++)
{
iFDTemp = cevents->data.fd;
......
if (0 != (EPOLLERR & cevents->events)) { ...... }
if (0 == (EPOLLIN & cevents->events)) { ...... }
if (LISTEN_SOCKET == m_pSocketInfo->m_iSocketType)
{
iNewSocket = accept(iFDTemp, (struct sockaddr *)&m_stSockAddr, (socklen_t *)&iSockAddrSize);
......
}
else
{
RecvClientData(iFDTemp);
}
5) epoll的工作模式
•LT(Level Triggered)缺省,支持block和no-block socket。水平触发,只要buffer里有未被处理的事件,内核会不断通知你就绪未处理的FD。
•ET(Edge Triggered),只支持no-block socket。边缘触发,只在buffer大小发生变化时通知,只通知一次,控制不当有可能丢失。
4 总结
select->poll->epoll的修改主要体现在如下几个方面:
•从readset、writeset等分离到将读写事件集中到统一的结构。
•从阻塞操作前后的两次循环到之后的一次循环,再到精确返回有事件发生的fd。
•从只能绑定fd信息,到可以绑定指针结构信息。
下图展示了其在性能方面的测试情况:
性能对比图