参考:https://segmentfault.com/a/1190000003063859
Linux下的I/O复用与epoll详解:https://www.cnblogs.com/lojunren/p/3856290.html
彻底学会 epoll 系列:http://blog.chinaunix.net/uid/28541347/sid-193117-list-1.htm
Linux下I/O多路复用系统调用(select, poll, epoll)介绍:https://zhuanlan.zhihu.com/p/22834126
epoll编程,如何实现高并发服务器开发?:https://www.zhihu.com/question/21516827
select、poll、epoll之间的区别总结[整理]:https://www.cnblogs.com/Anker/p/3265058.html
处理大并发之:对epoll的理解,epoll客户端服务端代码:http://blog.csdn.net/zhuxiaoping54532/article/details/56494313
http://blog.csdn.net/feitianxuxue
同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?
--------------------------------------------------------------------------------------------------------------------------------
同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的。所以先限定一下本文的上下文。本文讨论的背景是Linux环境下的network IO。
1. 概念说明
在进行解释之前,首先要说明几个概念:
- 用户空间和内核空间
- 进程切换
- 进程的阻塞
- 文件描述符
- 缓存 I/O
用户空间与内核空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
1. 保存处理机上下文,包括程序计数器和其他寄存器。
2. 更新PCB信息。
3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
4. 选择另一个进程执行,并更新其PCB。
5. 更新内存管理的数据结构。
6. 恢复处理机上下文。
注:总而言之就是很耗资源,具体的可以参考这篇文章:进程切换
进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的
。
文件描述符fd
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
缓存 I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。
在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
socket 简单编程
网络 IPC 套接字socket:http://blog.csdn.net/freeking101/article/details/78889736
1. 关于字节排序 网际协议采用大端字节序,来传输多字节整数。系统提供了转换的宏定义,如果主机与网际协议相同,则宏定义为空。
2. 客户端:socket -> connect(阻塞,三次握手)-> rcv
3. 服务端:socket -> bind -> listen -> accept(阻塞,三次握手)-> send
1. socket
1)函数原型 int socket(int family, int type, int protocol) 。更多可参看 :man socket
2)参数:
family: 协议族AF_INET,IPv4协议 ...
type : type 套接字类型SOCK_STREAM 字节流套接字
protocol: IPPROCO_TCP IPPROCO_UDP IPPROCO_SCTP
3)返回值 成功:返回套接字符 错误:返回INVALID_SOCKET(-1)
4)示例
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
int main()
{ int socketfd; struct sockaddr_in servaddr; if((socketfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { return -1; }
}
2. connect
1)函数原型 int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)。更多可参看:man connect
2)参数:
sockfd: socket 函数返回的套接字描述符
servaddr : 服务器的IP和端口
addrlen: 长度(sizeof(servaddr))
3)返回值:成功:0。错误:返回INVALID_SOCKET(-1)
4)示例
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h> int main()
{ int socketfd; struct sockaddr_in servaddr; if((socketfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("socket error\n"); return -1; } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_addr.s_addr = inet_addr("192.168.0.218"); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(55000); if(connect(socketfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0) { printf("connect error\n"); } return 0;
}
3. bind
1)函数原型 int bind(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)。更多可参看:man bind
2)参数:
sockfd: socket 函数返回的套接字描述符
servaddr : 服务器的IP和端口
addrlen: 长度(sizeof(servaddr))
3)返回值:成功:0。错误:返回INVALID_SOCKET(-1)
4. listen
listen的参数backlog的意义:http://www.cnblogs.com/lit10050528/p/6568357.html
Linux TCP socket 开发中 listen backlog 的含义:http://www.jianshu.com/p/fe2228a77429
1)函数原型 int listen(int sockfd, int backlog)
2)参数:
sockfd: socket 函数返回的套接字描述符
backlog : 内核中套接字排队的最大个数
3)返回值:成功:0。错误:返回INVALID_SOCKET
5. accept
1)函数原型 int accept(int sockfd, const struct sockaddr *servaddr, socklen_t *addrlen)
2)参数: sockfd: socket 函数返回的套接字描述符
3)返回值:成功:从监听套接字返回已连接的套接字的文件描述符。错误:返回 -1,并设置errno。
如果对客户信息不感兴趣,后两个参数可以置空。
servaddr : 客户进程的IP和端口(可设为null)。addrlen: 长度(sizeof(servaddr))(可设为null)
4)示例
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h> int main()
{ int count = 0; int listenfd, socketfd; int nread; struct sockaddr_in servaddr; struct timeval timeoutval; char readbuf[256]; printf("accept started\n"); //socket if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("socket error\n"); return -1; } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(59000); //bind if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { printf("bind error\n"); //return -1; } //listen listen(listenfd, 5); //accept socketfd = accept(listenfd, NULL, NULL); while(1) { printf("start receive %d...\n", count++); memset(readbuf, sizeof(readbuf), 0); nread = recv(socketfd, readbuf, 10, 0); if(nread>0) { readbuf[10] = '\0'; printf("receiveed %s, nread = %d\n\n", readbuf, nread); } } return 0;
}
2. IO模式
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
1. 等待数据准备 (Waiting for the data to be ready)
2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。
1. 阻塞 I/O(blocking IO)
2. 非阻塞 I/O(nonblocking IO)
3. I/O 多路复用( IO multiplexing)
4. 信号驱动 I/O( signal driven IO)
5. 异步 I/O(asynchronous IO)
注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。
阻塞 I/O(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
I/O 多路复用( IO multiplexing)
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
异步 I/O(asynchronous IO)
Linux下的asynchronous IO其实用得很少。先看一下它的流程:
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
总结
blocking和non-blocking的区别
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
synchronous IO和asynchronous IO的区别
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
各个IO Model的比较如图所示:
通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
3. I/O 多路复用之select、poll、epoll详解
目前支持I/O多路复用的系统调用有 select、pselect、poll、epoll。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,pselect,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销
I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过 select/poll/epoll 之类的系统调用来实现,这些函数都可以同时监视多个描述符的读写就绪状况,这样,**多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。
使用场景
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
1)当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。
2)当一个客户同时处理多个套接口时,这种情况是可能的,但很少出现。
3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
epoll 跟 select 都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。
1. select
基本原理:select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
基本流程,如图所示:
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
select的缺陷
高并发的核心解决方案是1个线程处理所有连接的“等待消息准备好”,这一点上epoll和select是无争议的。但select预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select的使用方法是这样的:
返回的活跃连接 ==select(全部待监控的连接)。
什么时候会调用select方法呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,调用select在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被“频繁”二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select就完全力不从心了。
此外,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数。
其次,内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。看到这里,您可能要要问了,你为什么不提poll?笔者认为select与poll在内部机制方面并没有太大的差异。相比于select机制,poll只是取消了最大监控文件描述符数限制,并没有从根本上解决select存在的问题
select本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1.select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
接下来我们看张图,当并发连接为较小时,select与epoll似乎并无多少差距。可是当并发连接上来以后,select就显得力不从心了
select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。poll 和 select 应该被归类为这样的系统调用,它们可以阻塞地同时探测一组支持非阻塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助调用者寻找当前就绪的设备。
下面是select的原理图:
1. 使用copy_from_user从用户空间拷贝fd_set到内核空间
2. 注册回调函数__pollwait
3. 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
4. 以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
5. __pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
6. poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
7. 如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
8. 把fd_set从内核空间拷贝到用户空间。
select 函数定义(man select)
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);void FD_CLR(int fd, fd_set *set); // 清除set中的fd位
int FD_ISSET(int fd, fd_set *set); // 判断set中是否设置了文件描述符fd
void FD_SET(int fd, fd_set *set); // 在set中设置文件描述符fd
void FD_ZERO(fd_set *set); // 清空set中的所有位(在使用文件描述符集前,应该先清空一下)//(注意FD_CLR和FD_ZERO的区别,一个是清除某一位,一个是清除所有位)#include <sys/select.h>int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);各个参数的含义:
1)nfds参数指定被监听的文件描述符的总数。通常被设置为select监听的所有文件描述符中最大值加1;
2)readfds、writefds、exceptfds分别指向可读、可写和异常等事件对应的文件描述符集合。这三个参数都是传入传出型参数,指的是在调用select之前,用户把关心的可读、可写、或异常的文件描述符通过 FD_SET函数 分别添加进readfds、writefds、exceptfds文件描述符集,select将对这些 "文件描述符集合" 中的文件描述符进行监听,如果有就绪文件描述符,select会重置readfds、writefds、exceptfds文件描述符集来通知应用程序哪些文件描述符就绪。这个特性导致select函数返回后,再次调用select之前,必须重置所关心的文件描述符,也就是三个文件描述符集已经不是之前传入的了。
3)timeout参数用来指定select函数的超时时间(下面讲select返回值时还会谈及)。
struct timeval
{long tv_sec; //秒数long tv_usec; //微秒数
};select的返回情况:
1)如果指定timeout为NULL,select会永远等待下去,直到有一个文件描述符就绪,select返回;
2)如果timeout的指定时间为0,select根本不等待,立即返回;
3)如果指定一段固定时间,则在这一段时间内,如果有指定的文件描述符就绪,select函数返回,如果超过指定时间,select同样返回。
4)返回值情况:
a)超时时间内,如果文件描述符就绪,select返回就绪的文件描述符总数(包括可读、可写和异常),没有文件描述符就绪,select返回0;
b)select调用失败时,返回 -1并设置errno,如果收到信号,select返回 -1并设置errno为EINTR。文件描述符的就绪条件:
在网络编程中,
1)下列情况下socket可读:
a) socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT;
b) socket通信的对方关闭连接,此时该socket可读,但是一旦读该socket,会立即返回0(可以用这个方法判断client端是否断开连接);
c) 监听socket上有新的连接请求;
d) socket上有未处理的错误。
2)下列情况下socket可写:
a) socket内核发送缓冲区的可用字节数大于或等于其低水位标记SO_SNDLOWAT;
b) socket的读端关闭,此时该socket可写,一旦对该socket进行操作,该进程会收到SIGPIPE信号;
c) socket使用connect连接成功之后;
d) socket上有未处理的错误。
fd_set 结构体是文件描述符集,该结构体实际上是一个整型数组,数组中的每个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,一般情况下,FD_SETSIZE等于1024,这就限制了select能同时处理的文件描述符的总量。
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低
select 示例代码 1:
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>typedef struct _CLIENT
{int fd;struct sockaddr_in addr; // client address
} CLIENT;#define MYPORT 59000 // 连接端口#define BACKLOG 5 // 最多处理的connect
CLIENT client[BACKLOG];char recvBuf[10]; // 数据接收 buf
int currentClient = 0; // 当前的连接数void showClient() // 显示当前的连接
{int i;printf("client count = %d\n", currentClient);for(i=0; i<BACKLOG; i++){printf("[%d] = %d\n", i, client[i].fd);}printf("\n");
}int main(void)
{int i, ret, receiveLen=0;fd_set readfds, writefds;int sockServer, sockClient, sockMax;struct timeval timeout;struct sockaddr_in clientAddr, serverAddr;// 初始化 fd 都为 -1for(i=0; i<BACKLOG; i++) client[i].fd=-1;if((sockServer=socket(AF_INET, SOCK_STREAM, 0)) < 0){printf("socket creat fail\n");return -1;}printf("sockServer = %d\n", sockServer);// 也可以使用 memset(&serverAddr, 0, siezeof(serverAddr)bzero(&serverAddr, sizeof(serverAddr));serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(MYPORT);serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);int reuse = 1; // 1 毫秒setsockopt(sockServer, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse));// 服务端在没有调用 close(sockServer); 关闭服务端 sock 文件描述符// 时,服务端 会出现 CLOSE_WAITE 状态,导致 必须在2小时后才能释放所占端口// 通过上面设置,可以缩短等待时间。这个一定要在 bind 之前设置。if((bind(sockServer, (struct sockaddr*)&serverAddr, sizeof(serverAddr))) < 0 ){printf("bind fail\n");return -1;}if((listen(sockServer, BACKLOG))<0){printf("listen fail\n");return -1;}/************************ select **************************/while(1){FD_ZERO(&readfds);FD_SET(sockServer, &readfds);sockMax = sockServer;// 加入client的 fdfor(i=0; i<BACKLOG; i++){if(client[i].fd>0){FD_SET(client[i].fd, &readfds);sockMax = sockMax>client[i].fd ? sockMax:client[i].fd;}}timeout.tv_sec = 3;timeout.tv_usec = 0;/* select */ret = select((int)sockMax+1, &readfds, NULL, NULL, &timeout);if(ret<0){printf("select fail\n");break;}else if(ret == 0){printf("select 超时,结束本次循环,继续下次循环\n");continue;}printf("select success and start test data\n");// read datafor(i=0; i<BACKLOG; i++){if(client[i].fd>0 && FD_ISSET(client[i].fd, &readfds)){if(receiveLen != sizeof(recvBuf)){while(1){// recv 数据ret = recv(client[i].fd, (char*)recvBuf+receiveLen, sizeof(recvBuf)-receiveLen, 0);if(0==ret || ret<0){client[i].fd = -1;receiveLen = 0;break;}//数据正常,接受数据receiveLen += ret;if(receiveLen < sizeof(recvBuf)) continue;else{//数据接受完毕printf("%s, buf = %s\n", inet_ntoa(client[i].addr.sin_addr), recvBuf);receiveLen=0;break;}}}}}if(FD_ISSET(sockServer, &readfds)){printf("FD_ISSET sockServer\n");socklen_t clientAddrLen = sizeof(clientAddr);sockClient = accept(sockServer, (struct sockaddr*)&clientAddr, &clientAddrLen);if(sockClient == -1) printf("accept fail\n");else currentClient++;for(i=0; i<BACKLOG; i++){if(client[i].fd < 0){client[i].fd = sockClient;client[i].addr = clientAddr;printf("get a connect from %s\n", inet_ntoa(client[i].addr.sin_addr));break;}}//close(sockServer); }}showClient();return 0;
}
程序运行截图:
使用 nc 模拟 TCP 客户端(Linux nc 命令:http://blog.csdn.net/freeking101/article/details/53289198):
select 示例代码 2:
server端代码,文件名为:select-server.c
/************关于本文档********************************************
*filename: select-server.c
*purpose: 演示网络异步通讯、select用法,这是服务器端程序
*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
* 但请遵循GPL
*Thanks to: Google.com
*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
* 科技站在巨人的肩膀上进步更快!感谢有开源前辈的贡献!
*********************************************************************/#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>#define MAXBUF 1024int main(int argc, char** argv)
{int server_fd, client_fd;socklen_t len;struct sockaddr_in server_Addr, client_Addr;unsigned int port, listen_num;char buf[MAXBUF+1];fd_set readfds;struct timeval tv;int retval, maxfd;if(2 == argc) port=atoi(argv[1]);else port = 12345;if(3 == argc) listen_num=atoi(argv[2]);else listen_num=5;printf("port = %d, listen_num = %d\n", port, listen_num);if((server_fd=socket(PF_INET, SOCK_STREAM, 0))==-1){perror("socket creat fail\n");exit(1);}memset(&server_Addr, 0, sizeof(server_Addr));server_Addr.sin_family=PF_INET;server_Addr.sin_port=htons(port);if(4==argc) server_Addr.sin_addr.s_addr=inet_addr(argv[3]);else server_Addr.sin_addr.s_addr = INADDR_ANY;int reuse=1;setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse));if(bind(server_fd, (struct sockaddr*)&server_Addr, sizeof(struct sockaddr))==-1){perror("bind fail\n");exit(1);}if(listen(server_fd, listen_num) == -1){perror("listen fail\n");exit(1);}while(1){printf("\n等待新的连接到来开始新一轮聊天......\n");len=sizeof(struct sockaddr);if((client_fd=accept(server_fd, (struct sockaddr*)&client_Addr, &len))==-1){perror("accept fail\n");exit(errno);}else{printf("server : got connection from %s, port %d, socket %d\n", inet_ntoa(client_Addr.sin_addr),ntohs(client_Addr.sin_port), client_fd);}/* 开始处理每个新连接上的数据收发 */printf("\n准备就绪,可以开始聊天,直接输入消息回车即可发消息给对方\n");while(1){FD_ZERO(&readfds); // 清空集合FD_SET(0, &readfds); // 把 标准输入(stdin)句柄0加入到集合中maxfd=0;FD_SET(client_fd, &readfds);maxfd=maxfd>client_fd?maxfd:client_fd;/* 设置最大等待时间 */tv.tv_sec=1;tv.tv_usec=0;retval = select(maxfd+1, &readfds, NULL, NULL, &tv);if(-1==retval){printf("select fail and error:%s", strerror(errno));break;}else if(0==retval){/* 没有任何消息到来,也没有按键,继续等待...... */continue;}else{/* 判断当前I/O是否是 stdin */if(FD_ISSET(0, &readfds)){// 用户按键了,则读取用户输入的内容发送出去memset(buf, 0, MAXBUF+1);fgets(buf, MAXBUF, stdin);if(!strncasecmp(buf, "quit", 4)){printf("\n自己请求终止聊天\n");break;}len=send(client_fd, buf, strlen(buf)-1,0);if(len>0) printf("\n消息:%s\t发送成功,共发送%d个字节\n",buf,len);else{printf("\n消息'%s'发送失败,错误代码是%d,错误消息是'%s'\n",buf,errno,strerror(errno));break;}}/* 判断 IO 是否是来自 socket */if(FD_ISSET(client_fd, &readfds)){/*当前连接的socket上有消息到来,则接收对方发过来的消息并显示*/bzero(buf, MAXBUF+1);/*接收客户端的消息*/len=recv(client_fd, buf, MAXBUF,0);if(len>0) printf("\n接收消息成功:'%s',共%d个字节\n",buf,len);else{if(len<0){printf("\n消息接收失败,错误代码是%d,错误信息是'%s'\n",errno, strerror(errno));}else printf("\n对方退出,聊天终止\n");break;}}}}close(client_fd);/* 处理每个新连接上的数据收发结束 */printf("\n还要和其他连接聊天么? (no->退出):");fflush(stdout);bzero(buf, MAXBUF+1);fgets(buf, MAXBUF, stdin);if(!strncasecmp(buf,"no",2)){printf("\n终止聊天\n");break;}}close(server_fd);return 0;
}
程序运行截图(使用 nc 做为客户端):
也可以使用 nc 打开多个 客户端来连接 服务端,但是同一时刻只能有一个客户端和服务端聊天。即如果第二个客户端想和服务端聊天,则必须等待第一个客户端退出,第二个才能和服务端聊天。因为 select 是同步IO,他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。可以自己使用 nc 试下。
client端代码,文件名为select-client.c
/************关于本文档********************************************
*filename: select-client.c
*purpose: 演示网络异步通讯,这是客户端程序
*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
* 但请遵循GPL
*Thanks to: Google.com
*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
* 科技站在巨人的肩膀上进步更快!感谢有开源前辈的贡献!
*********************************************************************/#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>#define MAXBUF 1024int main(int argc, char** argv)
{int sockfd, len;struct sockaddr_in dest;char buf[MAXBUF+1];fd_set readfds;struct timeval tv;int retval, maxfd=-1;if(3!=argc){printf("参数格式错误!正确用法如下:\n\t\t%s IP地址 端口\n\t比如:\t%s 127.0.0.1 80\n\
此程序用来从某个 IP 地址的服务器某个端口接收最多 MAXBUF 个字节的消息\n",\argv[0], argv[0]);exit(0);}/* 创建一个 socket 用于 tcp 通信 */if((sockfd=socket(AF_INET, SOCK_STREAM, 0))<0){perror("socket create fail\n");exit(errno);}/*初始化服务端(对端)*/bzero(&dest, sizeof(dest));dest.sin_family=AF_INET;dest.sin_port=htons(atoi(argv[2]));if(inet_aton(argv[1], (struct in_addr*)&dest.sin_addr.s_addr)==0){perror("argv[1] fail\n");exit(errno);}/*连接服务器*/if(connect(sockfd, (struct sockaddr*)&dest, sizeof(dest))!=0){perror("connect fail\n");exit(errno);}printf("\n准备就绪,可以开始聊天了.直接输入消息回车即可发信息给对方\n");while(1){FD_ZERO(&readfds); /* 把集合清空 */FD_SET(0, &readfds); /* 把标准输入句柄加入到集合中*/maxfd=0;FD_SET(sockfd, &readfds); /* 把当前连接句柄sockfd加入到集合中 */ if(sockfd > maxfd) maxfd=sockfd;tv.tv_sec=1;tv.tv_usec=0;/*开始等待*/retval=select(maxfd+1, &readfds, NULL, NULL, &tv);if (retval == -1){printf("将退出,select出错! %s", strerror(errno));break;}else if(retval == 0){/* printf ("没有任何消息到来,用户也没有按键,继续等待……\n"); */continue;}else{/* */if (FD_ISSET(sockfd, &readfds)){/* 连接的socket上有消息到来则接收对方发过来的消息并显示 */bzero(buf, MAXBUF + 1); /* 接收对方发过来的消息,最多接收 MAXBUF 个字节 */len = recv(sockfd, buf, MAXBUF, 0);if (len > 0) printf("接收消息成功:'%s',共%d个字节的数据\n", buf, len);else{if (len < 0)printf("消息接收失败!错误代码是%d,错误信息是'%s'\n",errno, strerror(errno)); else printf("对方退出了,聊天终止!\n");break;}} if (FD_ISSET(0, &readfds)){/* 用户按键了,则读取用户输入的内容发送出去 */bzero(buf, MAXBUF + 1);fgets(buf, MAXBUF, stdin);if (!strncasecmp(buf, "quit", 4)){printf("自己请求终止聊天!\n");break;}/* 发消息给服务器 */len = send(sockfd, buf, strlen(buf) - 1, 0);if (len < 0) {printf("消息'%s'发送失败!错误代码是%d,错误信息是'%s'\n",buf, errno, strerror(errno));break;}else printf("消息:%s\t发送成功,共发送了%d个字节!\n",buf, len);}}}/* 关闭连接 */close(sockfd);return 0;
}
编译用如下命令:
gcc -Wall select-server.c -o server
gcc -Wall select-client.c -o client
运行截图:
可以开多个客户端验证上面 使用 nc 开多个客户端的情况。
select 示例代码 3:
同时循环读取标准输入和读取有名管道的内容,默认的情况下,标准输入没有内容,read()时会阻塞,同样的,有名管道如果没有内容,read()也会阻塞,我们如何实现循环读取这两者的内容呢?最简单的方法是,开两个线程,一个线程循环读标准输入的内容,一个线程循环读有名管道的内容。而在这里,我们通过 select() 函数实现这个功能:
#include <sys/select.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> int main(int argc, char *argv[])
{ fd_set rfds; struct timeval tv; int ret; int fd; ret = mkfifo("test_fifo", 0666); // 创建有名管道 if(ret != 0){ perror("mkfifo:"); } fd = open("test_fifo", O_RDWR); // 读写方式打开管道 if(fd < 0){ perror("open fifo"); return -1; } ret = 0; while(1){ // 这部分内容,要放在while(1)里面 FD_ZERO(&rfds); // 清空 FD_SET(0, &rfds); // 标准输入描述符 0 加入集合 FD_SET(fd, &rfds); // 有名管道描述符 fd 加入集合 // 超时设置 tv.tv_sec = 1; tv.tv_usec = 0; // 监视并等待多个文件(标准输入,有名管道)描述符的属性变化(是否可读) // 没有属性变化,这个函数会阻塞,直到有变化才往下执行,这里没有设置超时 // FD_SETSIZE 为 <sys/select.h> 的宏定义,值为 1024 ret = select(FD_SETSIZE, &rfds, NULL, NULL, NULL); //ret = select(FD_SETSIZE, &rfds, NULL, NULL, &tv); if(ret == -1){ // 出错 perror("select()"); }else if(ret > 0){ // 准备就绪的文件描述符 char buf[100] = {0}; if( FD_ISSET(0, &rfds) ){ // 标准输入 read(0, buf, sizeof(buf)); printf("stdin buf = %s\n", buf); }else if( FD_ISSET(fd, &rfds) ){ // 有名管道 read(fd, buf, sizeof(buf)); printf("fifo buf = %s\n", buf); } }else if(0 == ret){ // 超时 printf("time out\n"); } } return 0;
}
当前终端运行此程序,另一终端运行一个往有名管道写内容的程序,运行结果如下:
下面为上面例子的往有名管道写内容的示例代码:
#include <sys/select.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h> int main(int argc, char *argv[])
{ //select_demo(8); fd_set rfds; struct timeval tv; int ret; int fd; ret = mkfifo("test_fifo", 0666); // 创建有名管道 if(ret != 0){ perror("mkfifo:"); } fd = open("test_fifo", O_RDWR); // 读写方式打开管道 if(fd < 0){ perror("open fifo"); return -1; } while(1){ char *str = "this is for test"; write(fd, str, strlen(str)); // 往管道里写内容 printf("after write to fifo\n"); sleep(5); } return 0;
}
运行结果:
使用 select 实现类似 QQ
/* * tty1:cat > ./p1* tty2:cat > ./p2* tty3: ./5_select_QQ*/
#include <fcntl.h>
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>int main(void)
{int fds[2]; char buf[4096]; int i, rc, maxfd;fd_set watchset; /* 欲监听read操作的集合 */fd_set inset; /* select()调用后,被修改的集合 */if ((fds[0] = open("p1", O_RDONLY|O_NONBLOCK)) < 0) { perror("open p1"); return 1; }if ((fds[1] = open("p2", O_RDONLY|O_NONBLOCK)) < 0) { perror("open p2"); return 1; }/* 清零集合,将监听描述符加入集合内 */FD_ZERO(&watchset);FD_SET(fds[0], &watchset);FD_SET(fds[1], &watchset);/* 找出最大文件描述符, 用以select第一个参数 */maxfd = fds[0] > fds[1] ? fds[0] : fds[1];/* 循环监视两个文件描述符是否有read条件*/while (FD_ISSET(fds[0], &watchset) || FD_ISSET(fds[1], &watchset)) {inset = watchset;//backup,因为select()调用结束会修改参二(传出参数) if (select(maxfd+1, &inset, NULL,NULL,NULL) < 0) {perror("select"); return 1; } //从监听的两个文件描述符中去判断,哪个还在集合中 for (i = 0; i < 2; i++){if (FD_ISSET(fds[i], &inset)) { //具备read条件rc = read(fds[i], buf, sizeof(buf) - 1);//read之if (rc < 0) {perror("read"); return 1;} else if (!rc) {/* 管道已经被关闭,无需再监听了 */close(fds[i]);FD_CLR(fds[i], &watchset); //清除出监听集合中} else {buf[rc] = '\0';printf("tty%d: %s", i+1, buf);} } } }return 0;
}
2. poll
Poll函数起源于SVR3,最初局限于流设备,SVR4取消了这种限制。
总是来说,poll比select要高效一些,但是,它有可移植性问题,例如,windows就只支持select。
poll 系统调用的原理与原型和 select 基本类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。
基本原理:poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1. 大量的 fd 的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2. poll 还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次 poll 时会再次报告该 fd。
注意:从上面看,select 和 poll 都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
poll系统调用API如下:
poll, ppoll - wait for some event on a file descriptor#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout); // nfds_t 是 unsigned int 类型#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <signal.h>
#include <poll.h>int ppoll(struct pollfd *fds, nfds_t nfds,const struct timespec *tmo_p, const sigset_t *sigmask);各个参数的含义:
1)第一个参数是指向一个结构数组的第一个元素的指针,
即 fds 是一个struct pollfd类型的数组,每个元素都是一个pollfd结构,用于指定测试某个给定描述符的条件。
比如用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;
struct pollfd
{int fd; //指定要监听的文件描述符short events; //指定监听fd上的什么事件short revents; //fd上事件就绪后,用于保存实际发生的事件
};
待监听的事件由events成员指定,函数在相应的revents成员中返回该描述符的状态
(每个文件描述符都有两个事件,一个是传入型的events,一个是传出型的revents,从而避免使用传入传出型参数,注意与select的区别),
从而告知应用程序fd上实际发生了哪些事件。events和revents都可以是多个事件的按位或。
2)第二个参数是要监听的文件描述符的个数,也就是数组fds的元素个数; 即nfds记录数组fds中描述符的总数量;
3)第三个参数意义与select相同。即 timeout是调用poll函数阻塞的超时时间,单位毫秒;
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。
timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;
timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。poll的返回值情况: 与select相同。
返回值和错误代码成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:EBADF 一个或多个结构体中指定的文件描述符无效。EFAULTfds 指针指向的地址超出进程的地址空间。EINTR 请求的事件之前产生一个信号,调用可以重新发起。EINVALnfds 参数超出PLIMIT_NOFILE值。ENOMEM 可用内存不足,无法完成请求。
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd {int fd; /* file descriptor */ /* 文件描述符 */short events; /* requested events to watch */ /* 等待的事件 */short revents; /* returned events witnessed */ /* 实际发生了的事件 */
};
pollfd 结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
poll的事件类型:
在使用POLLRDHUP时,要在代码开始处定义_GNU_SOURCE
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。
每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。
revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
events域中请求的任何事件都可能在revents域中返回。
合法的事件如下:POLLIN 有数据可读。POLLRDNORM 有普通数据可读。POLLRDBAND 有优先数据可读。POLLPRI 有紧迫数据可读。POLLOUT 写数据不会导致阻塞。POLLWRNORM 写普通数据不会导致阻塞。POLLWRBAND 写优先数据不会导致阻塞。POLLMSGSIGPOLL 消息可用。此外,revents域中还可能返回下列事件:POLLER 指定的文件描述符发生错误。POLLHUP 指定的文件描述符挂起事件。POLLNVAL 指定的文件描述符非法。
这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。
使用poll()和select()不一样,你不需要显式地请求异常情况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
poll 示例代码 1:
一个poll的简单例子
#include <stdio.h>
#include <unistd.h>
#include <sys/poll.h>
#include <errno.h>#define TIMEOUT 5 /* poll timeout, in seconds */int main (void)
{struct pollfd fds[2];int ret;/* watch stdin for input */fds[0].fd = STDIN_FILENO;fds[0].events = POLLIN;/* watch stdout for ability to write (almost always true) */fds[1].fd = STDOUT_FILENO;fds[1].events = POLLOUT;/* All set, block! */ret = poll(fds, 2, TIMEOUT * 1000);if (ret == -1) {perror ("poll error\n");return 1;}if (!ret) // 就是判断poll返回值 <=0,上面if判断了等于 -1,这里就是判断返回值是不是0{printf ("%d seconds elapsed.\n", TIMEOUT);return 0;}if (fds[0].revents & POLLIN)printf ("stdin is readable\n");if (fds[1].revents & POLLOUT)printf ("stdout is writable\n");return 0;
}
poll 示例代码 2:
/* 实现功能:通过poll, 处理多个socket * 监听一个端口,监听到有链接时,添加到poll. */
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>typedef struct _CLIENT
{int fd;struct sockaddr_in addr;
} CLIENT;#define PORT 12345 // 端口
#define BACKLOG 5 // 最多处理的连接int currentClient = 0; //当前连接数#define REVLEN 10
char recvBuf[REVLEN]; //数据接收buf#define OPEN_MAX 1024int main(void)
{int i, ret, recvLen=0, timeout;int sockListen, sockServer, sockMax;struct sockaddr_in server_addr, client_addr;socklen_t len = sizeof(client_addr);struct pollfd clientfd[OPEN_MAX];if((sockListen=socket(AF_INET, SOCK_STREAM, 0))<0 ){printf("socket fail\n");return -1;}printf("cocket create success\n");bzero(&server_addr, sizeof(server_addr));server_addr.sin_family=AF_INET;server_addr.sin_port=htons(PORT);server_addr.sin_addr.s_addr=htonl(INADDR_ANY);int reuse=1; setsockopt(sockListen, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)); if(bind(sockListen, (struct sockaddr*)&server_addr, sizeof(server_addr))<0){printf("bind fail\n");return -1;}printf("bind success\n");if(listen(sockListen,BACKLOG)<0){printf("listen fail\n");return -1;}printf("sockListen : %d\n", sockListen);// clientfd 初始化clientfd[0].fd = sockListen;clientfd[0].events=POLLIN;sockMax=0;/****************注意i=1, 初始化剩余其他的fd为 -1 ****************/for(i=1; i<OPEN_MAX; i++) clientfd[i].fd=-1;/****************************************/while(1){timeout=3000; //超时时间,单位 毫秒ret=poll(clientfd, sockMax+1, timeout);if(ret<0){printf("poll fail and errno %d\n", errno);return -1;}else if(0==ret){printf("timeout and start the next.\n");continue;}/****************开始轮询*******************///测试监听描述符是否准备就绪if(clientfd[0].revents & POLLIN){printf("监听描述符准备就绪\n");sockServer = accept(sockListen, (struct sockaddr*)&client_addr, &len);if(-1 == sockServer) printf("accept fail\n");else currentClient++;printf("accept a new client: %s:%d\n",inet_ntoa(client_addr.sin_addr), client_addr.sin_port);for(i=0; i<OPEN_MAX; i++){if(clientfd[i].fd<0){clientfd[i].fd=sockServer;break;}}if(i==OPEN_MAX){printf("too many connections\n");return -1;}clientfd[i].events = POLLIN;if(i>sockMax) sockMax=i;}/*************读取数据*************/for(i=1;i<=sockMax;i++){if(clientfd[i].fd <0) continue;if(clientfd[i].revents & (POLLIN | POLLERR)){if(recvLen != REVLEN){while(1){//recv 数据ret = recv(clientfd[i].fd, (char*)recvBuf+recvLen, REVLEN-recvLen, 0);if(ret <= 0){clientfd[i].fd = -1;recvLen = 0;break;}//数据接收正常recvLen = recvLen+ret;if(recvLen<REVLEN) continue;else{//数据接收完毕printf("buf = %s\n", recvBuf);// close(client[i].fd);// client[i].fd = -1;recvLen = 0;break;}}}}}}return 0;
}
程序运行截图:
poll 示例代码 3:
( https://www.cnblogs.com/Anker/p/3261006.html)
编写一个echo server程序,功能是客户端向服务器发送信息,服务器接收输出并原样发送回给客户端,客户端接收到输出到终端。服务器端程序如下(poll-server.c):
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <poll.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>#define IP_ADDRESS "127.0.0.1" //IP 地址
#define PORT 12345 //端口号
#define MAX_LINE 1024 //缓存大小
#define LISTEN_NUM 5 //最大连接数
#define OPEN_MAX 1000
#define TIME_OUT -1/* 函数声明 */
static int socket_bind(const char* ip, int port); //套接字bind
static void do_poll(int listen_fd); //IO多路复用poll
static void handle_connection(struct pollfd* connect_fds, int num); //处理多个连接int main(int argc, char** argv)
{int listen_fd;listen_fd = socket_bind(IP_ADDRESS, PORT);printf("listen_fd = %d\n", listen_fd);listen(listen_fd, LISTEN_NUM);do_poll(listen_fd);close(listen_fd);return 0;
}static int socket_bind(const char* ip, int port)
{int listen_fd;struct sockaddr_in server_addr;bzero(&server_addr, sizeof(server_addr));server_addr.sin_family=AF_INET; //协议inet_pton(AF_INET, ip, &server_addr.sin_addr); //地址server_addr.sin_port=htons(port); //端口listen_fd = socket(AF_INET, SOCK_STREAM, 0);if(listen_fd<0){printf("socket fail and errno is%d\n",errno);exit(errno);}int reuse=1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)); if(bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))==-1){printf("bind fail and errno %d\n", errno);exit(errno);}return listen_fd;
}static void do_poll(int listen_fd)
{printf("**************start do_poll***************\n");int connect_fd;struct sockaddr_in client_addr;socklen_t len;struct pollfd client_fds[OPEN_MAX];int i, max_i, nready;//添加监听的描述符client_fds[0].fd = listen_fd;client_fds[0].events = POLLIN;//初始化客户端连接描述符for(i=1; i<OPEN_MAX; i++) client_fds[i].fd = -1;max_i = 0;//循环处理for( ; ; ){//获取可用描述符的个数nready = poll(client_fds, max_i + 1, TIME_OUT);if(nready < 0){printf("poll fail and errno %d\n", errno);exit(errno);}else if(nready == 0){printf("timeout and start the next.\n");continue;}printf("测试监听描述符是否准备就绪\n");//测试监听描述符是否准备好if(client_fds[0].revents & POLLIN){printf("监听描述符准备就绪\n");len = sizeof(client_addr);//接收新的连接connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);if(connect_fd<0){if(errno == EINTR) continue;else{printf("accept error and errno %d", errno);exit(errno);}}fprintf(stdout, "accept a new client: %s:%d\n",inet_ntoa(client_addr.sin_addr), client_addr.sin_port);//将新的连接描述符添加到数组中for(i=1; i<OPEN_MAX; i++){if(client_fds[i].fd < 0){client_fds[i].fd = connect_fd;break;}}if(i == OPEN_MAX){fprintf(stdout, "too many clients\n");exit(1);}//将新的描述符添加到读描述符集合中client_fds[i].events = POLLIN;//记录客户连接套接字的个数max_i = (i>max_i ? i : max_i);// 这个判断nready为0就是为了可以提前进入下一次轮询,不需要再判断一次客户端列表。提高效率。完全可以删了。if(--nready <= 0) continue; }//处理客户端连接handle_connection(client_fds, max_i);}
}static void handle_connection(struct pollfd* connect_fds, int num)
{int i, n;char buf[MAX_LINE];memset(buf, 0 ,MAX_LINE);for(i=1; i<=num; i++){if(connect_fds[i].fd < 0) continue;//测试客户端描述符是否准备好if(connect_fds[i].revents & POLLIN){//接收客户端信息n = read(connect_fds[i].fd, buf, MAX_LINE);if(0==n){close(connect_fds[i].fd);connect_fds[i].fd = -1;continue;}//printf("read msg is: ");write(STDOUT_FILENO, buf, n);write(connect_fds[i].fd, buf ,n);}}
}
测试结果截图(使用 nc 作为客户端):
客户端代码如下所示(poll-client.c):
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>#define MAXLINE 1024
#define IPADDRESS "127.0.0.1"
#define SERV_PORT 8787#define max(a,b) (a > b) ? a : bstatic void handle_connection(int sockfd);int main(int argc,char *argv[])
{int sockfd;struct sockaddr_in servaddr;sockfd = socket(AF_INET,SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));//处理连接描述符handle_connection(sockfd);return 0;
}static void handle_connection(int sockfd)
{char sendline[MAXLINE],recvline[MAXLINE];int maxfdp,stdineof;struct pollfd pfds[2];int n;//添加连接描述符pfds[0].fd = sockfd;pfds[0].events = POLLIN;//添加标准输入描述符pfds[1].fd = STDIN_FILENO;pfds[1].events = POLLIN;for (; ;){poll(pfds,2,-1);if (pfds[0].revents & POLLIN){n = read(sockfd,recvline,MAXLINE);if (n == 0){fprintf(stderr,"client: server is closed.\n");close(sockfd);}write(STDOUT_FILENO,recvline,n);}//测试标准输入是否准备好if (pfds[1].revents & POLLIN){n = read(STDIN_FILENO,sendline,MAXLINE);if (n == 0){shutdown(sockfd,SHUT_WR);continue;}write(sockfd,sendline,n);}}
}
poll 示例代码 4:
#include <poll.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> int main(int argc, char *argv[])
{ int ret; int fd; struct pollfd fds[2]; // 监视文件描述符结构体,2 个元素 ret = mkfifo("test_fifo", 0666); // 创建有名管道 if(ret != 0){ perror("mkfifo:"); } fd = open("test_fifo", O_RDWR); // 读写方式打开管道 if(fd < 0){ perror("open fifo"); return -1; } ret = 0; fds[0].fd = 0; // 标准输入 fds[1].fd = fd; // 有名管道 fds[0].events = POLLIN; // 普通或优先级带数据可读 fds[1].events = POLLIN; // 普通或优先级带数据可读 while(1){ // 监视并等待多个文件(标准输入,有名管道)描述符的属性变化(是否可读) // 没有属性变化,这个函数会阻塞,直到有变化才往下执行,这里没有设置超时 ret = poll(fds, 2, -1); //ret = poll(&fd, 2, 1000); if(ret == -1){ // 出错 perror("poll()"); }else if(ret > 0){ // 准备就绪的文件描述符 char buf[100] = {0}; if( ( fds[0].revents & POLLIN ) == POLLIN ){ // 标准输入 read(0, buf, sizeof(buf)); printf("stdin buf = %s\n", buf); }else if( ( fds[1].revents & POLLIN ) == POLLIN ){ // 有名管道 read(fd, buf, sizeof(buf)); printf("fifo buf = %s\n", buf); } }else if(0 == ret){ // 超时 printf("time out\n"); } } return 0;
}
poll() 的实现和 select() 非常相似,只是描述 fd 集合的方式不同,poll() 使用 pollfd 结构而不是 select() 的 fd_set 结构,其他的都差不多。
往有名管道写内容的示例代码:同上面select 往有名管道写内容
#include <sys/select.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h> int main(int argc, char *argv[])
{ //select_demo(8); fd_set rfds; struct timeval tv; int ret; int fd; ret = mkfifo("test_fifo", 0666); // 创建有名管道 if(ret != 0){ perror("mkfifo:"); } fd = open("test_fifo", O_RDWR); // 读写方式打开管道 if(fd < 0){ perror("open fifo"); return -1; } while(1){ char *str = "this is for test"; write(fd, str, strlen(str)); // 往管道里写内容 printf("after write to fifo\n"); sleep(5); } return 0;
}
运行结果:
3. epoll
一 . Epoll 介绍
Epoll 可是当前在 Linux 下开发大规模并发网络程序的热门人选, Epoll 在 Linux2.6 内核中正式引入,和 select 相似,但epoll是select和poll的增强版本。其实都是 I/O 多路复用技术而已 。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。其实在 Linux 下设计并发网络程序,向来不缺少方法,比如典型的 Apache 模型( Process Per Connection ,简称 PPC ), TPC ( Thread Per Connection )模型,以及 select 模型和 poll 模型,那为何还要再引入 Epoll 这个东东呢?
如果不摆出来其他模型的缺点,怎么能对比出 Epoll 的优点呢。
- PPC/TPC 模型。
这两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我 。只是 PPC 是为它开了一个进程,而 TPC 开了一个线程。可是别烦我是有代价的,它要时间和空间啊,连接多了之后,那么多的进程 / 线程切换,这开销就上来了;因此这类模型能接受的最大连接数都不会高,一般在几百个左右。
- select 模型。
1. 最大并发数限制,因为一个进程所打开的 FD (文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024/2048 ,因此 Select 模型的最大并发数就被相应限制了。自己改改这个 FD_SETSIZE ?想法虽好,可是先看看下面吧 …
2. 效率问题, select 每次调用都会线性扫描全部的 FD 集合,这样效率就会呈现线性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢来,什么?都超时了。
3. 内核 / 用户空间 内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法。
总结为:1.连接数受限 2.查找配对速度慢 3.数据由内核拷贝到用户态
- poll 模型。
基本上效率和 select 是相同的, select 缺点的 2 和 3 它都没有改掉。
- epoll的优点如下:
1. 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大 ,具体数目可以 cat /proc/sys/fs/file-max 察看。
2. 效率提升,epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。 所以 epoll 不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数。即 Epoll 最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll 与 select 和 poll 在使用和实现上的区别。
1. epoll使用一组函数来完成,而不是单独的一个函数;
2. epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,无须向select和poll那样每次调用都要重复传入文件描述符集合事件集。
epoll 基本原理:
在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll使用“事件”的就绪通知方式,事先通过epoll_ctl()来注册一个文件描述符(fd),一旦该文件描述符(fd)就绪时,内核就会采用类似callback的回调机制,来激活该 fd,当进程调用epoll_wait()时便可以得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。) 。epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。
epoll对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)。
LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。
LT模式与ET模式:
- LT模式:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。即在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
- ET模式:ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。即就是说当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
注意:如果没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。
epoll为什么要有EPOLLET触发模式?
如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
二 . Epoll 为什么高效
epoll高效的奥秘是什么?就是 epoll 精巧的使用了3个方法来实现select方法要做的事:
1. 新建epoll描述符==epoll_create()
2. epoll_ctrl(epoll描述符,添加或者删除所有待监控的连接)
3. 返回的活跃连接 ==epoll_wait( epoll描述符 )
与select相比,epoll分清了频繁调用和不频繁调用的操作。例如,epoll_ctrl是不太频繁调用的,而epoll_wait是非常频繁调用的。这时,epoll_wait却几乎没有入参,这比select的效率高出一大截,而且,它也不会随着并发连接的增加使得入参越发多起来,导致内核执行效率下降。
要深刻理解epoll,首先得了解epoll的三大关键要素:mmap、红黑树、链表。
epoll是通过内核与用户空间mmap同一块内存实现的。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。
红黑树将存储epoll所监听的套接字。上面mmap出来的内存如何保存epoll所监听的套接字,必然也得有一套数据结构,epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。
Linux下的I/O复用与epoll详解:https://www.cnblogs.com/lojunren/p/3856290.html
epoll 示例程序 1(最简单的 epoll):
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>int main(void)
{int epfd,nfds;struct epoll_event ev,events[5]; // ev用于注册事件,数组用于返回要处理的事件epfd = epoll_create(1); // 只需要监听一个描述符——标准输入ev.data.fd = STDIN_FILENO;ev.events = EPOLLIN|EPOLLET; //监听读状态同时设置ET模式epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件for(;;){nfds = epoll_wait(epfd, events, 5, -1);for(int i = 0; i < nfds; i++){if(events[i].data.fd==STDIN_FILENO)printf("Something happened with stdin!\n");}}
}
程序运行结果截图:
1. 当用户输入一组字符,这组字符被送入buffer,字符停留在buffer中,又因为buffer由空变为不空,所以ET返回读就绪,输出”Something happened with stdin!”。
2. 之后程序再次执行epoll_wait,此时虽然buffer中有内容可读,但是根据我们上节的分析,ET并不返回就绪,导致epoll_wait阻塞。(底层原因是ET下就绪fd的epitem只被放入rdlist一次)。
3. 用户再次输入一组字符,导致buffer中的内容增多,根据我们上节的分析这将导致fd状态的改变,是对应的epitem再次加入rdlist,从而使epoll_wait返回读就绪,再次输出“Something happened with stdin!”。
接下来我们将上面程序的第11行做如下修改:
ev.events=EPOLLIN; //默认使用LT模式
编译并运行,结果如下:
程序陷入死循环,因为用户输入任意数据后,数据被送入buffer且没有被读出,所以LT模式下每次epoll_wait都认为buffer可读返回读就绪。导致每次都会输出”Something happened with stdin!”。
程序 2。修改上面程序:
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>int main(void)
{int epfd,nfds;struct epoll_event ev,events[5]; // ev用于注册事件,数组用于返回要处理的事件epfd = epoll_create(1); // 只需要监听一个描述符——标准输入ev.data.fd = STDIN_FILENO;ev.events = EPOLLIN; // 监听读状态同时设置LT模式epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件for(;;){nfds = epoll_wait(epfd, events, 5, -1);for(int i = 0; i < nfds; i++){if(events[i].data.fd==STDIN_FILENO){char buf[1024] = {0};read(STDIN_FILENO, buf, sizeof(buf)); // 把 buffer 里面的内容读取出来printf("Something happened with stdin!\n");} }}
}
程序运行结果:
本程序依然使用LT模式,但是每次epoll_wait返回读就绪的时候我们都将buffer(缓冲)中的内容read出来,所以导致buffer再次清空,下次调用epoll_wait就会阻塞。所以能够实现我们所想要的功能——当用户从控制台有任何输入操作时,输出”Something happened with stdin!”
程序 3。修改程序:
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>int main(void)
{int epfd,nfds;struct epoll_event ev,events[5]; // ev用于注册事件,数组用于返回要处理的事件epfd = epoll_create(1); // 只需要监听一个描述符——标准输入ev.data.fd = STDIN_FILENO;ev.events = EPOLLIN|EPOLLET; // 监听读状态同时设置ET模式epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件for(;;){nfds = epoll_wait(epfd, events, 5, -1);for(int i = 0; i < nfds; i++){if(events[i].data.fd==STDIN_FILENO){printf("welcome to epoll's word!\n");ev.data.fd = STDIN_FILENO;ev.events = EPOLLIN|EPOLLET; //设置ET模式epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); //重置epoll事件(ADD无效)} }}
}
编译并运行,结果如下:
程序依然使用ET,但是每次读就绪后都主动的再次MOD IN事件,我们发现程序再次出现死循环,也就是每次返回读就绪。但是注意,如果我们将MOD改为ADD,将不会产生任何影响。别忘了每次ADD一个描述符都会在epitem组成的红黑树中添加一个项,我们之前已经ADD过一次,再次ADD将阻止添加,所以在次调用ADD IN事件不会有任何影响。
程序 4。修改程序:
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>int main(void)
{int epfd,nfds;struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件epfd = epoll_create(1); //只需要监听一个描述符——标准输入ev.data.fd = STDOUT_FILENO;ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置ET模式epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件for(;;){nfds = epoll_wait(epfd, events, 5, -1);for(int i = 0; i < nfds; i++){if(events[i].data.fd==STDOUT_FILENO){printf("welcome to epoll's word!\n");} }}
}
编译并运行,结果如下:
这个程序的功能是只要标准输出写就绪,就输出“welcome to epoll's world”。我们发现这将是一个死循环。
下面具体分析一下这个程序的执行过程:
- 首先初始buffer为空,buffer中有空间可写,这时无论是ET还是LT都会将对应的epitem加入rdlist,导致epoll_wait就返回写就绪。
- 程序想标准输出输出”welcome to epoll's world”和换行符,因为标准输出为控制台的时候缓冲是“行缓冲”,所以换行符导致buffer中的内容清空,这就对应第二节中ET模式下写就绪的第二种情况——当有旧数据被发送走时,即buffer中待写的内容变少得时候会触发fd状态的改变。所以下次epoll_wait会返回写就绪。如此循环往复。
程序 5。修改程序:
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>int main(void)
{int epfd,nfds;struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件epfd = epoll_create(1); //只需要监听一个描述符——标准输入ev.data.fd = STDOUT_FILENO;ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置ET模式epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件for(;;){nfds = epoll_wait(epfd, events, 5, -1);for(int i = 0; i < nfds; i++){if(events[i].data.fd==STDOUT_FILENO){printf("welcome to epoll's word!");} }}
}
运行结果截图:
与程序4相比,程序5只是将输出语句的printf的换行符移除。我们看到程序成挂起状态。因为第一次epoll_wait返回写就绪后,程序向标准输出的buffer中写入“welcome to epoll's world!”,但是因为没有输出换行,所以buffer中的内容一直存在,下次epoll_wait的时候,虽然有写空间但是ET模式下不再返回写就绪。回忆第一节关于ET的实现,这种情况原因就是第一次buffer为空,导致epitem加入rdlist,返回一次就绪后移除此epitem,之后虽然buffer仍然可写,但是由于对应epitem已经不再rdlist中,就不会对其就绪fd的events的在检测了。
程序 6:
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>int main(void)
{int epfd,nfds;struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件epfd = epoll_create(1); //只需要监听一个描述符——标准输入ev.data.fd = STDOUT_FILENO;ev.events = EPOLLOUT; //监听读状态同时设置LT模式epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件for(;;){nfds = epoll_wait(epfd, events, 5, -1);for(int i = 0; i < nfds; i++){if(events[i].data.fd==STDOUT_FILENO){printf("welcome to epoll's word!");} }}
}
程序运行截图:
程序六相对程序五仅仅是修改ET模式为默认的LT模式,我们发现程序再次死循环。这时候原因已经很清楚了,因为当向buffer写入”welcome to epoll's world!”后,虽然buffer没有输出清空,但是LT模式下只有buffer有写空间就返回写就绪,所以会一直输出”welcome to epoll's world!”,当buffer满的时候,buffer会自动刷清输出,同样会造成epoll_wait返回写就绪。
程序7:
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>int main(void)
{int epfd,nfds;struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件epfd = epoll_create(1); //只需要监听一个描述符——标准输入ev.data.fd = STDOUT_FILENO;ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置LT模式epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件for(;;){nfds = epoll_wait(epfd, events, 5, -1);for(int i = 0; i < nfds; i++){if(events[i].data.fd==STDOUT_FILENO){printf("welcome to epoll's word!");ev.data.fd = STDOUT_FILENO;ev.events = EPOLLOUT|EPOLLET; //设置ET模式epoll_ctl(epfd, EPOLL_CTL_MOD, STDOUT_FILENO, &ev); //重置epoll事件(ADD无效)} }}
}
编译并运行,结果如下:
程序七相对于程序五在每次向标准输出的buffer输出”welcome to epoll's world!”后,重新MOD OUT事件。所以相当于每次都会返回就绪,导致程序循环输出。
经过前面的案例分析,我们已经了解到,当epoll工作在ET模式下时,对于读操作,如果read一次没有读尽buffer中的数据,那么下次将得不到读就绪的通知,造成buffer中已有的数据无机会读出,除非有新的数据再次到达。对于写操作,主要是因为ET模式下fd通常为非阻塞造成的一个问题——如何保证将用户要求写的数据写完。
要解决上述两个ET模式下的读写问题,我们必须实现:
1. 对于读,只要buffer中还有数据就一直读;
2. 对于写,只要buffer还有空间且用户请求写的数据还未写完,就一直写。
ET模式下的accept问题
请思考以下一种场景:在某一时刻,有多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理。在这种情形下,我们应该如何有效的处理呢?
解决的方法是:解决办法是用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept 返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完。
关于ET的accept问题,这篇博文的参考价值很高,如果有兴趣,可以链接过去围观一下。
ET模式为什么要设置在非阻塞模式下工作
因为ET模式下的读写需要一直读或写直到出错(对于读,当读到的实际字节数小于请求字节数时就可以停止),而如果你的文件描述符如果不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。这样就不能在阻塞在epoll_wait上了,造成其他文件描述符的任务饥饿。
小结
LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
从本质上讲:与LT相比,ET模型是通过减少系统调用来达到提高并行效率的。
《Linux高性能服务器编程》
《彻底学会使用epoll》(系列博文)
《epoll源码分析(全) 》
《linux kernel中epoll的设计和实现》
《poll&&epoll实现分析(二)——epoll实现》
Epoll 的高效和其数据结构的设计是密不可分的,这个下面就会提到。首先回忆一下 select 模型,当有 I/O 事件到来时, select 通知应用程序有事件到了快去处理,而应用程序必须轮询所有的 FD 集合,测试每个 FD 是否有事件发生,并处理事件;代码像下面这样:
int res = select(maxfd+1, &readfds, NULL, NULL, 120);
if (res > 0)
{ for (int i = 0; i < MAX_CONNECTION; i++) { if (FD_ISSET(allConnection[i], &readfds)) { handleEvent(allConnection[i]); } }
}
// if(res == 0) handle timeout, res < 0 handle error
Epoll 不仅会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个FD 集合。
int res = epoll_wait(epfd, events, 20, 120);
for (int i = 0; i < res;i++)
{ handleEvent(events[n]);
}
三 . Epoll 操作过程
epoll操作过程需要三个接口,分别如下:
int epoll_create(int size); //创建一个epoll的句柄,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);
1. int epoll_create(int size);(man epoll_create)
#include <sys/epoll.h>int epoll_create(int size); // 成功则返回 Epoll 专用的文件描述符;
int epoll_create1(int flags);
epoll_create 函数是创建一个文件描述符,即创建一个epoll 的句柄(一个 Epoll 专用的文件描述符),指定内核中的事件表。调用成功返回一个文件描述符,失败返回-1并设置errno。其实是申请一个内核空间,用来存放你想关注的 socket fd 上是否发生以及发生了什么事件 。
size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。即size参数并不起作用,只是给内核一个提示,告诉它事件表需要多大。
该函数返回的文件描述符指定要访问的内核事件表,是其他所有epoll系统调用的句柄。当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);函数描述:epoll_ctl 函数作用就是操作内核事件表,即对指定描述符fd执行op操作。调用成功返回0,调用失败返回-1并设置errno。参数:epfd:是epoll_create()的返回值。即由 epoll_create() 生成的 Epoll 专用的文件描述符。op: 表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。这三个宏分别添加、删除和修改对fd的监听事件。fd: 是需要监听的或者关联的fd(文件描述符)event:指向epoll_event的指针,是告诉内核需要监听的事件。
epoll_ctl 函数就是控制某个 Epoll 文件描述符上的事件:注册、修改、删除。其中参数 epfd 是 epoll_create() 创建 Epoll 专用的文件描述符。相对于 select 模型中的 FD_SET 和 FD_CLR 宏。Epoll 速度快和其数据结构密不可分,其关键数据结构 struct epoll_event 结构如下:
struct epoll_event {__uint32_t events; /* Epoll events : epoll 事件 */epoll_data_t data; /* User data variable : 用户数据 */
};typedef union epoll_data
{void *ptr;int fd;uint32_t u32;uint64_t u64;
}epoll_data;//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里如:struct epoll_event ev;ev.data.fd=listenfd; // 设置与要处理的事件相关的文件描述符ev.events=EPOLLIN|EPOLLET; // 设置要处理的事件类型epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //注册epoll事件
在使用epoll_ctl时,是把fd添加、修改到内核事件表中,或从内核事件表中删除fd的事件。如果是添加事件到事件表中,可以往data中的fd上添加事件events,或者不用data中的fd,而把fd放到用户数据ptr所指的内存中(因为epoll_data 是一个 union 结构体 , 借助于它应用程序可以保存很多类型的信息:fd 、指针等等。有了它,应用程序就可以直接定位目标了。只能使用其中一个数据)然后在设置events。
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);等待 I/O 事件的发生。参数说明:epfd: 由 epoll_create() 生成的 Epoll 专用的文件描述符;epoll_event: 用于回传等待处理事件的数组;maxevents: 每次能处理的事件数;timeout: 等待 I/O 事件发生的超时值;
返回值:函数调用成功返回就绪文件描述符个数,失败返回-1并设置errno。相对于 select 模型中的 select 函数。
epoll系统调用的最关键的一个函数epoll_wait,它在一段时间内等待一个组文件描述符上的事件。即等待epfd上的io事件,最多返回maxevents个事件。函数调用成功返回就绪文件描述符个数,失败返回-1并设置errno。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时
epoll_wait 的运行原理:等待注册在 epfd 上的 socket fd 的事件的发生,如果发生,则将发生的 sokct fd 和 事件类型 放入到events数组中。并且将注册在 epfd 上的 socket fd 的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用 epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。
有的时候会使用两个线程,一个用来监听accept另一个用来监听epoll_wait,如果是这样使用的话,则listen socket fd使用默认的阻塞方式就行了,而如果epoll_wait和accept处于一个线程中,即,全部由epoll_wait进行监听,则,需将listen socket fd也设置成非阻塞的,这样,对accept也应该使用while包起来(类似于上面的recv),因为,epoll_wait返回时只是说有连接到来了,并没有说有几个连接,而且在ET模式下epoll_wait不会再因为上一次的连接还没读完而返回,这种情况确实存在,我因为这个问题而耗费了一天多的时间,这里需要说明的是,每调用一次accept将从内核中的已连接队列中的队头读取一个连接,因为在并发访问的环境下,有可能有多个连接“同时”到达,而epoll_wait只返回了一次。
epoll的典型用法
struct epoll_event ev, *events;
for(;;) {nfds = epoll_wait(kdpfd, events, maxevents, -1);for(n = 0; n < nfds; ++n) {if(events[n].data.fd == listener) {//新的连接client = accept(listener, (struct sockaddr *) &local, &addrlen);if(client < 0){perror("accept");continue;}setnonblocking(client);ev.events = EPOLLIN | EPOLLET;ev.data.fd = client;// 设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {fprintf(stderr, "epoll set insertion error: fd=%d0,client);return -1;}}elsedo_use_fd(events[n].data.fd);}
}
epoll用法比较全面的一个例子的伪代码:
while (1) {nfds = epoll_wait(epfd, events, 20, 500);for (i = 0; i< nfds; i++) { // 被内核IO事件唤醒的fdif (event[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 (event[i].events & EPOLLIN) { // 接受到数据,读socketn = read(sockfd, line, MAXLINE); // 读数据ev.data.ptr = md; // md为自定义的数据,添加数据ev.events = EPOLLOUT | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); // 修改标识符,等待下一个循环时发送数据,异步处理的精髓} else if (event[i].events & EPOLLOUT) { // 有数据要发送,写socketstruct 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 {//其他处理}}
}
四 . 示例程序
示例程序1:
client端代码还是用上面的select-client.c代码,服务器端程序需要改变,这里为epoll-server.c,如下:
/************关于本文档********************************************
*filename: epoll-server.c
*purpose: 演示epoll处理海量socket连接的方法,这是服务器端代码
*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
* 但请遵循GPL
*Thanks to:Google
*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
* 科技站在巨人的肩膀上进步更快!感谢有开源前辈的贡献!
*********************************************************************/#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/resource.h> // struct rlimit 使用这个头文件#define MAXBUF 1024
#define MAXEPOLLSIZE 10000/* setnonblocking - 设置句柄为非阻塞方式 */
int setnonblocking(int sockfd)
{if(fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1) return -1;return 0;
}/* handle_message - 处理每个 socket 上的消息收发 */
int handle_message(int new_fd)
{char buf[MAXBUF + 1];int len;/* 开始处理每个新连接上的数据收发 */bzero(buf, MAXBUF + 1);/* 接收客户端的消息 */len = recv(new_fd, buf, MAXBUF, 0);if (len > 0) printf("%d接收消息成功:'%s',共%d个字节的数据\n", new_fd, buf, len);else{if (len < 0) printf("消息接收失败!错误代码是%d,错误信息是'%s'\n", errno, strerror(errno));close(new_fd);return -1;}/* 处理每个新连接上的数据收发结束 */return len;
}int main(int argc, char **argv)
{int listener, new_fd, kdpfd, nfds, n, ret, curfds;socklen_t len;struct sockaddr_in my_addr, their_addr;unsigned int myport, lisnum;struct epoll_event ev, events[MAXEPOLLSIZE]; //ev用于注册事件,数组用于返回要处理的事件struct rlimit rt;if (2==argc) myport = atoi(argv[1]);else myport = 12345;if (3==argc){myport = atoi(argv[1]);lisnum = atoi(argv[2]);}else lisnum = 5;/* 设置每个进程允许打开的最大文件数 */rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;if (setrlimit(RLIMIT_NOFILE, &rt) == -1){perror("setrlimit");exit(1);}else printf("设置系统资源参数成功!\n");/* 开启 socket 监听 */if ((listener = socket(PF_INET, SOCK_STREAM, 0)) == -1) {perror("socket");exit(1);}else printf("socket 创建成功!\n");setnonblocking(listener);bzero(&my_addr, sizeof(my_addr));my_addr.sin_family = PF_INET;my_addr.sin_port = htons(myport);if (4==argc) my_addr.sin_addr.s_addr = inet_addr(argv[3]);else my_addr.sin_addr.s_addr = INADDR_ANY;if(bind(listener, (struct sockaddr*)&my_addr, sizeof(struct sockaddr))== -1){perror("bind");exit(1);}else printf("IP 地址和端口绑定成功\n");if (listen(listener, lisnum) == -1){perror("listen");exit(1);}else printf("开启服务成功!\n");/* 创建 epoll 句柄,把监听 socket 加入到 epoll 集合里 */kdpfd = epoll_create(MAXEPOLLSIZE);len = sizeof(struct sockaddr_in);ev.events = EPOLLIN | EPOLLET;ev.data.fd = listener;if(epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0){fprintf(stderr, "epoll set insertion error: fd=%d\n", listener);return -1;}else printf("监听 socket 加入 epoll 成功!\n");curfds = 1;while (1) {/* 等待有事件发生 */nfds = epoll_wait(kdpfd, events, curfds, -1);if (nfds == -1) {perror("epoll_wait");break;}/* 处理所有事件 */for (n = 0; n < nfds; ++n) {// 处理监听事件if (events[n].data.fd == listener){new_fd = accept(listener, (struct sockaddr *) &their_addr, &len);if (new_fd < 0){perror("accept");continue;}else printf("有连接来自于: %d:%d, 分配的 socket 为:%d\n", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);setnonblocking(new_fd);ev.events = EPOLLIN | EPOLLET;ev.data.fd = new_fd;if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_fd, &ev) < 0){fprintf(stderr, "把 socket '%d' 加入 epoll 失败!%s\n", new_fd, strerror(errno));return -1;}curfds++;} else //接收客户端的消息数据{ret = handle_message(events[n].data.fd);if (ret < 1 && errno != 11) {epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[n].data.fd, &ev);curfds--;}}}}close(listener);return 0;
}
编译此程序用命令:
gcc -Wall epoll-server.c -o server
gcc -Wall select-client.c -o client
运行此程序需要具有管理员权限!
sudo ./server 7838 1
./client 127.0.0.1 7838
通过测试这一个服务器可能同时处理10000 -3 = 9997 个连接!
如果这是一个在线服务系统,那么它可以支持9997人同时在线,比如游戏、聊天等。
epoll-server与select-server的区别是:
select:server每次都只能和一个client聊天,只有该client断开后,才处理另一个client的连接。
epoll:server可以同时接受多个client端发送来的消息。
示例程序2:
***********************************************************************
一个简单 Echo Server 的例子程序,麻雀虽小,五脏俱全,还包含了一个简单的超时检查机制。
简洁起见没有做错误处理。
(Linux Epoll介绍和程序实例:http://blog.csdn.net/sparkliang/article/details/4770655)
https://www.cppfans.org/1419.html
看下这篇博客示例程序,使用的是 union 结构体 data 即用户数据里面的 ptr指针 保存自定义事件。
可以看下博客下面的评论及讨论。了解思路、框架,以及流程。
***********************************************************************
示例程序3:
epoll 使用详解(精髓):http://blog.csdn.net/ljx0305/article/details/4065058
那么究竟如何来使用epoll呢?其实非常简单。
通过在包含一个头文件#include <sys/epoll.h> 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。
首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则范围。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
epoll_wait范围之后应该是一个循环,遍利所有的事件。
epoll 使用框架:
// 几乎所有的epoll程序都使用下面的框架:
for( ; ; )
{nfds = epoll_wait(epfd, events, 20, 500);for(i=0; i<nfds; ++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在LT和ET模式下的读写方式:http://kimi.it/515.html)
(Windows下完成端口移植Linux下的epoll(续):http://blog.51cto.com/jazka/252620)
(客户端也用 epoll 实现的示例程序:https://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html)
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>using namespace std;#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000void setnonblocking(int sock)
{int opts;opts=fcntl(sock,F_GETFL);if(opts<0){perror("fcntl(sock,GETFL)");exit(1);}opts = opts|O_NONBLOCK;if(fcntl(sock,F_SETFL,opts)<0){perror("fcntl(sock,SETFL,opts)");exit(1);}
}int main(int argc, char* argv[])
{int i, maxi, listenfd, connfd, sockfd, epfd, nfds, portnumber;ssize_t n;char line[MAXLINE];socklen_t clilen;if ( 2 == argc ){if( (portnumber = atoi(argv[1])) < 0 ){fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);return 1;}}else{fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);return 1;}//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件struct epoll_event ev, events[20];//生成用于处理accept的epoll专用的文件描述符epfd=epoll_create(256); struct sockaddr_in clientaddr;struct sockaddr_in serveraddr;listenfd = socket(AF_INET, SOCK_STREAM, 0);//把socket设置为非阻塞方式setnonblocking(listenfd);//设置与要处理的事件相关的文件描述符ev.data.fd=listenfd;//设置要处理的事件类型ev.events=EPOLLIN|EPOLLET;//ev.events=EPOLLIN;//注册epoll事件epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;char *local_addr="127.0.0.1";inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);serveraddr.sin_port=htons(portnumber);bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));listen(listenfd, LISTENQ);maxi = 0;for ( ; ; ){//等待epoll事件的发生nfds=epoll_wait(epfd, events, 20, 500);//处理所发生的所有事件for(i=0; i<nfds; ++i){//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。if(events[i].data.fd==listenfd){connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen);if(connfd<0){perror("connfd<0");exit(1);}//setnonblocking(connfd);char *str = inet_ntoa(clientaddr.sin_addr);cout << "accapt a connection from " << str << endl;//设置用于读操作的文件描述符ev.data.fd=connfd;//设置用于注测的读操作事件ev.events=EPOLLIN|EPOLLET;//ev.events=EPOLLIN;//注册evepoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);}else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。{cout << "EPOLLIN" << endl;if ( (sockfd = events[i].data.fd) < 0) continue;if ( (n = read(sockfd, line, MAXLINE)) < 0) {if (errno == ECONNRESET) {close(sockfd);events[i].data.fd = -1;}else std::cout<<"readline error"<<std::endl;} else if (n == 0) {close(sockfd);events[i].data.fd = -1;}line[n] = '/0';cout << "read " << line << endl;//设置用于写操作的文件描述符ev.data.fd=sockfd;//设置用于注册的写操作事件ev.events=EPOLLOUT|EPOLLET;//修改sockfd上要处理的事件为EPOLLOUTepoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); }else if(events[i].events&EPOLLOUT) // 如果有数据发送{sockfd = events[i].data.fd;write(sockfd, line, n);//设置用于读操作的文件描述符ev.data.fd=sockfd;//设置用于注测的读操作事件ev.events=EPOLLIN|EPOLLET;//修改sockfd上要处理的事件为EPOLINepoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);}}}return 0;
}
示例程序4:
epoll服务器
#include <sys/epoll.h>
#include <netinet/in.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h> #include <errno.h>
#include <stdlib.h>
typedef struct sockaddr_in sockaddr_in ;
typedef struct sockaddr sockaddr ; #define SER_PORT 8080 int nonblock(int fd){ int opt ; opt = fcntl(fd,F_GETFL); opt |= O_NONBLOCK ; return fcntl(fd,F_SETFL,opt);
} int main(int argc,char**argv){ sockaddr_in srv, cli ; int listen_fd ,con_fd ; socklen_t len; int res ,nsize,ws; char buf[255]; int epfd,ers; struct epoll_event evn,events[50]; int i; bzero(&srv,sizeof(srv)); bzero(&cli,sizeof(cli)); srv.sin_port= SER_PORT ; srv.sin_family = AF_INET ; listen_fd = socket(AF_INET,SOCK_STREAM,0); int yes = 1; setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)); if(bind(listen_fd,(sockaddr*)&srv,sizeof(sockaddr))<0) { perror("bind"); exit(0); } listen(listen_fd,100); nonblock(listen_fd); epfd = epoll_create(200); evn.events = EPOLLIN|EPOLLET ; evn.data.fd = listen_fd; epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&evn); static int count ; while(1){ ers = epoll_wait(epfd,events,100,5000); if(ers<0 ){ perror("epoll_wait:");exit(0); }else if(ers==0){ printf("time out:%d\n",count++); continue ; } for(i=0;i<ers;i++){ if(events[i].data.fd == listen_fd){ con_fd = accept(listen_fd,(sockaddr*)&cli ,&len); nonblock(con_fd); printf("connect from:%s\n",inet_ntoa(cli.sin_addr)); evn.data.fd = con_fd; evn.events = EPOLLIN | EPOLLET ; epoll_ctl(epfd,EPOLL_CTL_ADD,con_fd,&evn); }else if(events[i].events & EPOLLIN){ nsize = 0; while((res=read(events[i].data.fd,buf+nsize,sizeof(buf)-1))>0){ nsize+= res; } if(res==0){ epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL); printf("a client over\n"); close(con_fd); continue ; }else if(res<0 && errno!=EAGAIN){ perror("read"); continue ; } buf[nsize]=0; evn.data.fd = events[i].data.fd; evn.events=EPOLLOUT|EPOLLET ; epoll_ctl(epfd,EPOLL_CTL_MOD,events[i].data.fd,&evn); }else if(events[i].events & EPOLLOUT){ nsize = strlen(buf); ws = 0; while(nsize>0){ ws=write(events[i].data.fd,buf,nsize); nsize-=ws; } evn.data.fd = events[i].data.fd; evn.events=EPOLLIN|EPOLLET ; epoll_ctl(epfd,EPOLL_CTL_MOD,events[i].data.fd,&evn); }else{ printf("others\n"); } } } close(listen_fd); return 0;
}
客户端测试代码:
#include <sys/epoll.h>
#include <netinet/in.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct sockaddr_in sockaddr_in ;
typedef struct sockaddr sockaddr ; #define SER_PORT 8080
#define IP_ADDR "10.33.28.230" int main(int argc,char**argv){ sockaddr_in srv, cli ; int listen_fd ,con_fd ; socklen_t len; int res,ws ; char buf[255]; bzero(&srv,sizeof(srv)); bzero(&cli,sizeof(cli)); srv.sin_port= SER_PORT ; srv.sin_family = AF_INET ; inet_pton(AF_INET,IP_ADDR,&srv.sin_addr); listen_fd = socket(AF_INET,SOCK_STREAM,0); if(connect(listen_fd,(sockaddr*)&srv,sizeof(sockaddr))<0){ perror("connect"); exit(0); } while(1){ res = read(STDIN_FILENO,buf,sizeof(buf)-1); ws = write(listen_fd,buf,res); res = read(listen_fd,buf,sizeof(buf)-1); ws = write(STDOUT_FILENO,buf,res); } close(listen_fd); return 0;
}
示例程序5:
一个 epoll 示例,服务端代码:
/************************************************** * 实现功能:通过epoll, 处理多个socket * 监听一个端口,监听到有链接时,添加到epoll_event *************************************************/
#include "select.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <poll.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <netinet/in.h> typedef struct _CLIENT{ int fd; struct sockaddr_in addr; /* client's address information */
} CLIENT; #define MYPORT 59000 //最多处理的connect
#define MAX_EVENTS 500 //当前的连接数
int currentClient = 0; //数据接受 buf
#define REVLEN 10
char recvBuf[REVLEN]; //EPOLL相关
//epoll描述符
int epollfd;
//事件数组
struct epoll_event eventList[MAX_EVENTS]; void AcceptConn(int srvfd);
void RecvData(int fd); int main()
{ int i, ret, sinSize; int recvLen = 0; fd_set readfds, writefds; int sockListen, sockSvr, sockMax; int timeout; struct sockaddr_in server_addr; struct sockaddr_in client_addr; //socket if((sockListen=socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("socket error\n"); return -1; } bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(MYPORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //bind if(bind(sockListen, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { printf("bind error\n"); return -1; } //listen if(listen(sockListen, 5) < 0) { printf("listen error\n"); return -1; } //1. epoll 初始化 epollfd = epoll_create(MAX_EVENTS); struct epoll_event event; event.events = EPOLLIN|EPOLLET; event.data.fd = sockListen; //2. epoll_ctrl if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockListen, &event) < 0) { printf("epoll add fail : fd = %d\n", sockListen); return -1; } //epoll while(1) { timeout=3000; //3. epoll_wait int ret = epoll_wait(epollfd, eventList, MAX_EVENTS, timeout); if(ret < 0) { printf("epoll error\n"); break; } else if(ret == 0) { printf("timeout ...\n"); continue; } //直接获取了事件数量,给出了活动的流,这里是和poll区别的关键 int n = 0; for(n=0; n<ret; n++) { //错误退出 if ((eventList[n].events & EPOLLERR) || (eventList[n].events & EPOLLHUP) || !(eventList[n].events & EPOLLIN)) { printf ( "epoll error\n"); close (eventList[n].data.fd); return -1; } if (eventList[n].data.fd == sockListen) { AcceptConn(sockListen); }else{ RecvData(eventList[n].data.fd); //不删除 // epoll_ctl(epollfd, EPOLL_CTL_DEL, pEvent->data.fd, pEvent); } } } close(epollfd); close(sockListen); printf("test\n"); return 0;
} /**************************************************
函数名:AcceptConn
功能:接受客户端的链接
参数:srvfd:监听SOCKET
***************************************************/
void AcceptConn(int srvfd)
{ struct sockaddr_in sin; socklen_t len = sizeof(struct sockaddr_in); bzero(&sin, len); int confd = accept(srvfd, (struct sockaddr*)&sin, &len); if (confd < 0) { printf("bad accept\n"); return; }else { printf("Accept Connection: %d", confd); } //setnonblocking(confd); //4. epoll_wait //将新建立的连接添加到EPOLL的监听中 struct epoll_event event; event.data.fd = confd; event.events = EPOLLIN|EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event);
} //读取数据
void RecvData(int fd)
{ int ret; int recvLen = 0; memset(recvBuf, 0, REVLEN); printf("RecvData function\n"); if(recvLen != REVLEN) { while(1) { //recv数据 ret = recv(fd, (char *)recvBuf+recvLen, REVLEN-recvLen, 0); if(ret == 0) { recvLen = 0; break; } else if(ret < 0) { recvLen = 0; break; } //数据接受正常 recvLen = recvLen+ret; if(recvLen<REVLEN) { continue; } else { //数据接受完毕 printf("buf = %s\n", recvBuf); recvLen = 0; break; } } } printf("content is %s", recvBuf);
}
示例程序6(使用有名管道):
#include <sys/epoll.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h> int main(int argc, char *argv[])
{ int ret; int fd; ret = mkfifo("test_fifo", 0666); // 创建有名管道 if(ret != 0){ perror("mkfifo:"); } fd = open("test_fifo", O_RDWR); // 读写方式打开管道 if(fd < 0){ perror("open fifo"); return -1; } ret = 0; struct epoll_event event; // 告诉内核要监听什么事件 struct epoll_event wait_event; int epfd = epoll_create(10); // 创建一个 epoll 的句柄,参数要大于 0, 没有太大意义 if( -1 == epfd ){ perror ("epoll_create"); return -1; } event.data.fd = 0; // 标准输入 event.events = EPOLLIN; // 表示对应的文件描述符可以读 // 事件注册函数,将标准输入描述符 0 加入监听事件 ret = epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event); if(-1 == ret){ perror("epoll_ctl"); return -1; } event.data.fd = fd; // 有名管道 event.events = EPOLLIN; // 表示对应的文件描述符可以读 // 事件注册函数,将有名管道描述符 fd 加入监听事件 ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); if(-1 == ret){ perror("epoll_ctl"); return -1; } ret = 0; while(1){ // 监视并等待多个文件(标准输入,有名管道)描述符的属性变化(是否可读) // 没有属性变化,这个函数会阻塞,直到有变化才往下执行,这里没有设置超时 ret = epoll_wait(epfd, &wait_event, 2, -1); //ret = epoll_wait(epfd, &wait_event, 2, 1000); if(ret == -1){ // 出错 close(epfd); perror("epoll"); }else if(ret > 0){ // 准备就绪的文件描述符 char buf[100] = {0}; if( ( 0 == wait_event.data.fd ) && ( EPOLLIN == wait_event.events & EPOLLIN ) ){ // 标准输入 read(0, buf, sizeof(buf)); printf("stdin buf = %s\n", buf); }else if( ( fd == wait_event.data.fd ) && ( EPOLLIN == wait_event.events & EPOLLIN ) ){ // 有名管道 read(fd, buf, sizeof(buf)); printf("fifo buf = %s\n", buf); } }else if(0 == ret){ // 超时 printf("time out\n"); } } close(epfd); return 0;
}
假如有这样一个例子:
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)......
LT模式:
如果是LT模式,那么在第5步调用epoll_wait(2)之后,仍然能受到通知。
ET模式:
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。
当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,
读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:
while(rs)
{buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN){break;}else{return;}}else if(buflen == 0){// 这里表示对端的socket已正常关闭.}if(buflen == sizeof(buf){rs = 1; // 需要再次读取}else{rs = 0;}
}
Linux中的EAGAIN含义
Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。
例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
下面是一段不完整的代码且格式不对,意在表述上面的过程,去掉了一些模板代码。
#define IPADDRESS "127.0.0.1"
#define PORT 8787
#define MAXSIZE 1024
#define LISTENQ 5
#define FDSIZE 1000
#define EPOLLEVENTS 100listenfd = socket_bind(IPADDRESS,PORT);struct epoll_event events[EPOLLEVENTS];//创建一个描述符
epollfd = epoll_create(FDSIZE);//添加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);//循环等待
for ( ; ; ){//该函数返回已经准备好的描述符事件数目ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);//处理接收到的连接handle_events(epollfd,events,ret,listenfd,buf);
}//事件处理函数
static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{int i;int fd;//进行遍历;这里只要遍历已经准备好的io事件。num并不是当初epoll_create时的FDSIZE。for (i = 0;i < num;i++){fd = events[i].data.fd;//根据描述符的类型和事件类型进行处理if ((fd == listenfd) &&(events[i].events & EPOLLIN))handle_accpet(epollfd,listenfd);else if (events[i].events & EPOLLIN)do_read(epollfd,fd,buf);else if (events[i].events & EPOLLOUT)do_write(epollfd,fd,buf);}
}//添加事件
static void add_event(int epollfd,int fd,int state){struct epoll_event ev;ev.events = state;ev.data.fd = fd;epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd){int clifd; struct sockaddr_in cliaddr; socklen_t cliaddrlen; clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen); if (clifd == -1) perror("accpet error:"); else { printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port); //添加一个客户描述符和事件 add_event(epollfd,clifd,EPOLLIN); }
}//读处理
static void do_read(int epollfd,int fd,char *buf){int nread;nread = read(fd,buf,MAXSIZE);if (nread == -1) { perror("read error:"); close(fd); //记住close fd delete_event(epollfd,fd,EPOLLIN); //删除监听 }else if (nread == 0) { fprintf(stderr,"client close.\n");close(fd); //记住close fd delete_event(epollfd,fd,EPOLLIN); //删除监听 } else { printf("read message is : %s",buf); //修改描述符对应的事件,由读改为写 modify_event(epollfd,fd,EPOLLOUT); }
}//写处理
static void do_write(int epollfd,int fd,char *buf) { int nwrite; nwrite = write(fd,buf,strlen(buf)); if (nwrite == -1){ perror("write error:"); close(fd); //记住close fd delete_event(epollfd,fd,EPOLLOUT); //删除监听 }else{modify_event(epollfd,fd,EPOLLIN); } memset(buf,0,MAXSIZE);
}//删除事件
static void delete_event(int epollfd,int fd,int state) {struct epoll_event ev;ev.events = state;ev.data.fd = fd;epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}//修改事件
static void modify_event(int epollfd,int fd,int state){ struct epoll_event ev;ev.events = state;ev.data.fd = fd;epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}//注:另外一端我就省了
udp +epoll+多线程
/*********服务器 udp_epoll_server.c:**********/#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
//#include <openssl/ssl.h>
//#include <openssl/err.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <pthread.h>
#include <assert.h>
//#include "oci_api.h"
#define MAXBUF 1024
#define MAXEPOLLSIZE 100int setnonblocking(int sockfd)
{if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1){return -1;}return 0;
}void* pthread_handle_message(int* sock_fd)
{char recvbuf[MAXBUF + 1];char sendbuf[MAXBUF+1];int ret;int new_fd;struct sockaddr_in client_addr;socklen_t cli_len=sizeof(client_addr);new_fd=*sock_fd;bzero(recvbuf, MAXBUF + 1);bzero(sendbuf, MAXBUF + 1);ret = recvfrom(new_fd, recvbuf, MAXBUF, 0, (struct sockaddr *)&client_addr, &cli_len);if (ret > 0){printf("socket %d 接收到来自:%s:%d的消息成功:’%s’,共%d个字节的数据\n",new_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), recvbuf, ret);}else{printf("消息接收失败!错误代码是%d,错误信息是’%s’\n",errno, strerror(errno));}//printf("pthread exit!");fflush(stdout);pthread_exit(NULL);
}
int main(int argc, char **argv)
{int listener, kdpfd, nfds, n, curfds;socklen_t len;struct sockaddr_in my_addr, their_addr;unsigned int myport;struct epoll_event ev;struct epoll_event events[MAXEPOLLSIZE];struct rlimit rt;myport = 12345;pthread_t thread;pthread_attr_t attr;rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;if (setrlimit(RLIMIT_NOFILE, &rt) == -1){perror("setrlimit");exit(1);}else{printf("设置系统资源参数成功!\n");}if ((listener = socket(PF_INET, SOCK_DGRAM, 0)) == -1){perror("socket 创建失败!");exit(1);}else{printf("socket 创建成功!\n");}int opt=SO_REUSEADDR;setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));setnonblocking(listener);bzero(&my_addr, sizeof(my_addr));my_addr.sin_family = PF_INET;my_addr.sin_port = htons(myport);my_addr.sin_addr.s_addr = INADDR_ANY;if (bind(listener, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1){perror("bind");exit(1);}else{printf("IP 地址和端口绑定成功\n");}kdpfd = epoll_create(MAXEPOLLSIZE);len = sizeof(struct sockaddr_in);ev.events = EPOLLIN | EPOLLET;ev.data.fd = listener;if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0){fprintf(stderr, "epoll set insertion error: fd=%d\n", listener);return -1;}else{printf("监听 socket 加入 epoll 成功!\n");}while (1){nfds = epoll_wait(kdpfd, events, 10000, -1);if (nfds == -1){perror("epoll_wait");break;}for (n = 0; n < nfds; ++n){if (events[n].data.fd == listener){pthread_attr_init(&attr);pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);if(pthread_create(&thread,&attr,(void*)pthread_handle_message,(void*)&(events[n].data.fd))){perror("pthread_creat error!");exit(-1);}}}}close(listener);return 0;
}/*************udp客户端 udp_client.c:***************/#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#define MAXLINE 1024
#define SERV_PORT 12345
void do_cli(FILE *fp, int sockfd, struct sockaddr *pservaddr, socklen_t servlen)
{int n;char sendline[MAXLINE], recvline[MAXLINE + 1];if(connect(sockfd, (struct sockaddr *)pservaddr, servlen) == -1){perror("connect error");exit(1);}while(fgets(sendline, MAXLINE, fp) != NULL){ write(sockfd, sendline, strlen(sendline)-1); n = read(sockfd, recvline, MAXLINE);if(n == -1){perror("read error");exit(1);}recvline[n] = '\0';//fputs(recvline, stdout);printf("receive:%s\n",recvline);fflush(stdout);}
}int main(int argc, char **argv)
{int sockfd;struct sockaddr_in servaddr;if(argc != 2){printf("usage: udpclient <IPaddress>\n");exit(1);}bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){printf("[%s] is not a valid IPaddress\n", argv[1]);exit(1);}sockfd = socket(AF_INET, SOCK_DGRAM, 0);do_cli(stdin, sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));return 0;
}
编译运行截图:
启动两个客户端:
select、poll、epoll区别总结
1、支持一个进程所能打开的最大连接数
2、FD剧增后带来的IO效率问题
3、消息传递方式
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点:
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。
而epoll其实也需要调用 epoll_ wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在 epoll_wait中进入睡眠的进程。
虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的 时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间,这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内 部定义的等待队列),这也能节省不少的开销。
相同点:
1)三者都需要在fd上注册用户关心的事件;
2)三者都要一个timeout参数指定超时时间;
不同点:
1)select:
a)select指定三个文件描述符集,分别是可读、可写和异常事件,所以不能更加细致地区分所有可能发生的事件;
b)select如果检测到就绪事件,会在原来的文件描述符上改动,以告知应用程序,文件描述符上发生了什么时间,所以再次调用select时,必须先重置文件描述符;
c)select采用对所有注册的文件描述符集轮询的方式,会返回整个用户注册的事件集合,所以应用程序索引就绪文件的时间复杂度为O(n);
d)select允许监听的最大文件描述符个数通常有限制,一般是1024,如果大于1024,select的性能会急剧下降;
e)只能工作在LT模式。
2)poll:
a)poll把文件描述符和事件绑定,事件不但可以单独指定,而且可以是多个事件的按位或,这样更加细化了事件的注册,而且poll单独采用一个元素用来保存就绪返回时的结果,这样在下次调用poll时,就不用重置之前注册的事件;
b)poll采用对所有注册的文件描述符集轮询的方式,会返回整个用户注册的事件集合,所以应用程序索引就绪文件的时间复杂度为O(n)。
c)poll用nfds参数指定最多监听多少个文件描述符和事件,这个数能达到系统允许打开的最大文件描述符数目,即65535。
d)只能工作在LT模式。
3)epoll:
a)epoll把用户注册的文件描述符和事件放到内核当中的事件表中,提供了一个独立的系统调用epoll_ctl来管理用户的事件,而且epoll采用回调的方式,一旦有注册的文件描述符就绪,讲触发回调函数,该回调函数将就绪的文件描述符和事件拷贝到用户空间events所管理的内存,这样应用程序索引就绪文件的时间复杂度达到O(1)。
b)epoll_wait使用maxevents来制定最多监听多少个文件描述符和事件,这个数能达到系统允许打开的最大文件描述符数目,即65535;
c)不仅能工作在LT模式,而且还支持ET高效模式(即EPOLLONESHOT事件,读者可以自己查一下这个事件类型,对于epoll的线程安全有很好的帮助)。
select/poll/epoll总结:
select的缺点:
单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)
内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。
拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。
由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。
设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:
1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字
3)调用epoll_wait收集发生的事件的连接
如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
下面来看看Linux内核具体的epoll机制实现思路。
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
epoll数据结构示意图
从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。
OK,讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。
第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。
Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用。
Windows Python:提供: select
Mac Python:提供: select
Linux Python:提供: select、poll、epoll