1.概要:
网络 IO,会涉及到两个系统对象,一个是用户空间调用 IO 的进程或者线程,另一个是内核空间的内核系统,比如发生 IO 操作 read 时,它会经历两个阶段: 1. 等待数据准备就绪 2. 将数据从内核拷贝到进程或者线程中。 因为在以上两个阶段上各有不同的情况,所以出现了多种网络 IO 模型
当用户进程调用了 read 这个系统调用,kernel 就开始了 IO 的第一个阶段:准备数据。对于 network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的数据包), 这个时候 kernel 就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当 kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果, 用户进程才解除 block 的状态,重新运行起来。 所以,blocking IO 的特点就是在 IO 执行的两个阶段(等待数据和拷贝数据两个阶段)都被 block 了,所以服务器端的进程就被阻塞在accept()函数这里,一种解决方法是将accept()后的程序放在另外的线程中处理,让accept()继续监听,但这会涉及到线程的创建与销毁,有性能延迟,而创建一个线程池是对应的方法。但是当有几千个客户端同时连接上来的时候,线程池也无法处理得过来。如果将accept()设置为非阻塞,那么accept()不知道何时会有客户端连接上来。所以,如果内核有一种机制,可以来通知程序何时有客户端连接,那么这个问题就解决了。
2. 什么是网络io多路复用:
在linux系统中,涉及到不同资源之间的信息交换的行为叫做io,比如,内存io,磁盘io,网络io就是用户程序与内核程序之间的网络信息交换。网络io多路复用可以监管用户程序与内核之间的网络报文的交换,通过网络io多路复用,用户程序知道什么时候会有网络报文到达,而不用自己去向内核轮询,可以理解为一个通路,用户程序向这个通路发送和接收网络信息,内核也是;并且有信息的时候,它会提醒用户程序和内核。
linux系统提供了select,poll,epoll三种系统调用去实现网络io多路复用;select,poll的效率低下,因为它们会去轮询已经到达的fd,是否有数据到达,当fd多了的时侯,轮询的开销就变大了;linux系统有了epoll系统调用以后,就变成了真正的服务器os, 因为epoll支持网络并发性能很高,epoll是支持百万并发的。所以,我们也可以看到 io 多路复用只负责检测 io,不负责操作 io。
下面,我将重点讲epoll的原理与应用:
3. epoll
首先,epoll在内核中的实现是两个数据结构,红黑树储存连接事件块,双向链表储存就绪的事件
红黑树的结构体:
struct epoll_event {
__uint32_t events; // 事件类型
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr; // 指针类型的用户数据
int fd; // 文件描述符类型的用户数据
__uint32_t u32; // 32 位整数类型的用户数据
__uint64_t u64; // 64 位整数类型的用户数据
} epoll_data_t;
双向链表的结构体:
struct eventpoll {
// ...
struct rb_root rbr; // 管理 epoll 监听的事件
struct list_head rdllist; // 保存着 epoll_wait
返回满⾜条件的事件
// ...
};
struct epitem {
// ...
struct rb_node rbn; // 红⿊树节点
struct list_head rdllist; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向所属的eventpoll对
象
struct epoll_event event; // 注册的事件类型
// ...
};
epoll提供了三个接口,epoll_create(), epoll_trl((),epoll_wait();
#include <sys/epoll.h>
#include <iostream>
#define POLL_SIZE 1024
int epfd=epoll_create(1);//创建成功,返回epfd,参数非负数 if (epfd<=0) return ;
if(epfd <=0) return ;
struct epoll_event events[POLL_SIZE] = {0};//传入传出参数,将数据从内核态拷贝到用户态
struct epoll_event ev; //刚开始注册的epoll event
ev.events = EPOLLIN;
ev.data.fd = listenfd;
int ret=epoll_ctl(epfd, EPOLL_CTL_ADD,listenfd,&ev);
if(ret==-1){
std::cout<<"epoll_ctl failed"; //也可以使用strerror(errno);
return;
}
while(1){
/*epoll_wait成功时,返回发生的事件描述符fd,失败时返回-1;
第二个参数events是一个传入传出函数,POLL_SIZE是events的大小
最后一个参数timeout 为-1时,表面epoll_wait是阻塞的,没有事件到达时,将阻塞程序
如果 timeout的值 为 0,表示立即返回,无论是否有事件发生。
如果 timeout的值 大于 0,表示等待指定的毫秒数后返回,无论是否有事件发生。*/
int nready=epoll_wait(epfd,events,POLL_SIZE,-1);
if(nready==-1){
std::cout<<"epoll_ctl failed"; //也可以使用strerror(errno);
continue;
}
for(int i=0; i<nready;i++){
int clientfd =events[i].data.fd;
if (clientfd == listenfd) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}else if (events[i].events & EPOLLIN) {
n = recv(clientfd, buff, MAXLNE, 0);
}
}
调用 epoll_create 会创建一个 epoll 对象;调用 epoll_ctl 添加到 epoll 中的事件都会与网卡驱动程序建立回调关系,相应事件触发时会调用回调函数 (ep_poll_callback),将触发的事件拷贝到 rdlist 双向链表中;调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到用户态中;
总结:其实在使用epoll的时候,是要配合事件触发来设计相应的事件结构体,也就是reactor模式的epoll,这个时候epoll就能发挥最大的性能;另外,在使用的时候,都是one epoll per thread,下篇文章将分析reactor模型的epoll封装。
----------------------------------------------------------------- -end!