常见IO模型和epoll O(1)实现原理

来源:互联网 发布:谭浩强c语言第四版答案 编辑:程序博客网 时间:2024/05/02 00:10
同步和阻塞
同步:发送方发送请求后,需要收到响应后才能接着发送下一步请求;
阻塞;通常针对网络套接字socket,调用结果返回前,当前线程一直挂起等待;
同步针对调用方,阻塞针对接受方;
同步非阻塞:发送方发出请求后一直等待,接受方处理请求时立即返回,不用等待执行结果;
异步阻塞:发送方发出请求后马上返回,接受方处理请求期间一直等待,直到返回执行结果;
nginx工作进程采用了异步非阻塞,即执行请求时,工作进程和客户端都无需等待响应;

那么问题来了,当IO请求完成时,如何通知工作进程?
有两种方式:
1 worker定期检查IO运行状态 
2 IO在完成后主动通知worker,即采用事件驱动,允许一个进程同时处理多个请求;


事件驱动
包含事件收集器,事件发送器和事件处理器,重点是事件处理器,通常有3种模式;
基于进程的,基于线程的,基于非阻塞IO的(每接受一个请求,将其放入待处理事件的列表,使用非阻塞IO方式调用事件处理器来处理请求,形成事件驱动处理库)
基于非阻塞IO常见的有select()和epoll(),然而两者皆有不足,尤其在高并发连接时。
select():描述符总数有限制(1024个);读/写/异常各对应一个描述符;O(n)
poll():读/写/异常集合到一个描述符中;描述符总数没有限制;O(n)
而epoll()很大程度上解决了上述问题。


epoll优势 
为了避免每次扫描所有fd,epoll引入两个数据结构,
红黑树--存储socket fd
ready list--当socket上有事件发生时,将其加入此列表
epoll每次只扫描ready list,直接跳过那些不符合条件socketfd,算法复杂度为O(1);
epoll在初始化时调用kmem_cache_create申请内核内存(采用slab机制),用于创建epitem和eppoll_entry;
epoll对外只提供epoll_create/ctl/wait 3个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); 
epoll_create(): 在内核cache里创建上述红黑树和ready list;
epoll_ctl(): 如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上;然后向内核注册回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里;
epoll_wait(): 与select()等效,即返回ready list中的数据,将fd从内核态复制到用户态;
客户端只需调用epoll_ctl(),负责把socket fd加入epoll监控;


sockfd-1
  |
  |
sockfd-2                     e
  |                          p  -----------> wait_queue(process)
  |           ----------->   o
sockfd-3                     o   
  |           ----------->   l
  |                          f  -----------> ready_queue(ready events)
.....                        d 
sockfd-N-2
  |
  |
sockfd-N-1
  |
  |
sockfd-N


伪代码
while true {
active_stream[] = epoll_wait(epollfd)
for i in active_stream[] {
read or write till
}
}
再详细一点
for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i<nfds;++i)< span="" style="word-wrap: break-word;">
        {
            if(events[i].data.fd==listenfd) //有新的连接
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
                ev.data.fd=connfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
            }
            else if( events[i].events&EPOLLIN ) //接收到数据,读socket
            {
                n = read(sockfd, line, MAXLINE)) < 0    //读
                ev.data.ptr = md;     //md为自定义类型,添加数据
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
            {
                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取数据
                sockfd = md->fd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
            }
            else
            {
                //其他的处理
            }
        }
    }


epoll_event数据结构如下
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 */
};
注:events有多种类型,包括EPOLLIN/EPOLLOUT/EPOLLERR/EPOLLET


参考资料
https://gist.github.com/fisheuler/085890c9dff4547b0aa3
http://blog.csdn.net/russell_tao/article/details/7160071
0 0
原创粉丝点击