1. 前言
假设要写一个echo服务器,用于对用户从标准输入键入的交互命令做出相应,在这种情况下,服务器必须相应两个独立的I/O事件:1)网络客户端发起连接请求,2)用户在键盘上键入命令行。我们先等待哪个事件呢?没有哪个选择是理想的,如果使用accept等待一个连接请求,就不能相应输入命令,如果read等待一个输入命令,就不能相应任何连接请求。
2. 基于进程的并发编程
我们可以为每个连接开启一个新的线程,也就是accpet到新的连接后,开启一个新的线程完成对用户指令的回显。但是当连接数过多的时候,线程之间的上下文切换将极大的消耗cpu资源。
3. select多路复用
当然我们也可以基于I/O多路复用进行编程,其基本思路就是使用select函数,要求内核挂起进程,只有当一个或多个I/O事件发生后,才将控制权返回给应用程序。简单的讲,select函数有两个输入:一个称为读集合的描述符集合(fdset)和该读集合的基数(n)。select会一直阻塞,直到读集合中至少有一个描述符准备好了可以读。select有个副作用,当其返回时,会修改参数fdset指向读集合的一个子集,我们称为准备好集合,因此我们每次调用select时都要更新读集合。示例代码如下所示:
void echo(int connfd);
void command(void);
int main(int argc, char **argv){
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storange clientaddr;
fd_set read_st,ready_set;
if(argc != 2){
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
listenfd = Open_listenfd(argv[1]);
FD_ZERO(&read_set); // 清空
FD_SET(STDIN_FILNO, &read_set); // 将标准输入添加到读集合
FD_SET(listenfd , &read_set); // 将监听描述符添加到读集合
while(1){
ready_set = read_set;
select(listenfd+1,&ready_set,NULL,NULL,NULL);
if(FD_ISSET(SETDIN_FILENO, &ready_set))
command();
if(FD_ISSET(listenfd , &ready_set)){
clientlen = sizeof(stuct sockaddr_storage);
connfd = accept(listenfd , (SA *)&clientaddr, &clientlen);
echo(connfd);
close(connfd);
}
}
void command(void){
char buf[MAXLINE];
if(!fgets(buf, MAXLINE , stdin))
exit(0);
printf("%s",buf);
}
}
4. epoll多路复用
我们可以看到select多路复用是有一些弊端的,比如它需要将fdset从用户态内存空间贝到内核内存空间,返回时有需要将fdset从内核用户空间拷贝回用户内存空间;另外当select返回后,我们仍需要遍历一遍fdset判断哪些监听符准备好了,然后开启连接。为了解决以上两个弊端,诞生了epoll多路复用器。
epoll多路复用通过3个系统调用为用户提供服务。
- epoll_create系统调用
- epoll_ctl系统调用
- epoll_wait系统调用
当调用epoll_create时,会在内核空间中开辟一个eventpoll结构体,这个结构体中有两个成员与epoll的使用密切相关
struct eventpoll{
/*红黑树的根节点,存储着添加到epoll中的事件,即监听事件*/
struct rb_root rbr;
/*双向链表保存着ready的监听事件,即epoll_wait返回给用户的,满足条件的事件*/
struct list_head rellist;
}
当我们使用epoll_ctl添加监听事件的时候,会向红黑树中添加新的节点,并且会与设备驱动建立回调关系,当相应的事件发生时,会把该事件放到rdllist双向链表中,当调用epoll_wait时,只需要将rdllist复制到用户内存空间即可,因此epoll_wait的效率非常高。
总结一下epoll高效的本质在于:
- 减少了用户态和内核态的文件句柄拷贝
- 使用红黑树存储fd,插入、查找、删除性能不错
- 不需要遍历fdset得到ready fd
当然,也并不是epoll在所有情况下由于select,例如在如果有少于1024个文件描述符监听,且大多数socket都是出于活跃繁忙的状态,这种情况下,select要比epoll更为高效,因为epoll会有更多次的系统调用,用户态和内核态会有更加频繁的切换。