同步和阻塞
同步:发送方发送请求后,需要收到响应后才能接着发送下一步请求;
阻塞;通常针对网络套接字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
{
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