IO模型及select、poll、epoll和kqueue的区别--linux IO 5种方式,linux EPOLL Edge-Triggered Mode
(一)首先,介绍几种常见的I/O模型及其区别,如下:
-
blocking I/O
-
nonblocking I/O
-
I/O multiplexing (select and poll)
-
signal driven I/O (SIGIO)
-
asynchronous I/O (the POSIX aio_functions)—————异步IO模型最大的特点是 完成后发回通知。
阻塞与否,取决于实现IO交换的方式。
异步阻塞是基于select,select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄.
异步非阻塞直接在完成后通知,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。
1 blocking I/O
这个不用多解释吧,阻塞套接字。下图是它调用过程的图示:

重点解释下上图,下面例子都会讲到。首先application调用 recvfrom()转入kernel,注意kernel有2个过程,wait for data和copy data from kernel to user。直到最后copy complete后,recvfrom()才返回。此过程一直是阻塞的。
2 nonblocking I/O:
与blocking I/O对立的,非阻塞套接字,调用过程图如下:

可以看见,如果直接操作它,那就是个轮询。。直到内核缓冲区有数据。
3 I/O multiplexing (select and poll)
最常见的I/O复用模型,select。

select先阻塞,有活动套接字才返回。与blocking I/O相比,select会有两次系统调用,但是select能处理多个套接字。
4 signal driven I/O (SIGIO)
只有UNIX系统支持,感兴趣的课查阅相关资料

与I/O multiplexing (select and poll)相比,它的优势是,免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler处理。
5 asynchronous I/O (the POSIX aio_functions)
很少有*nix系统支持,windows的IOCP则是此模型

完全异步的I/O复用机制,因为纵观上面其它四种模型,至少都会在由kernel copy data to appliction时阻塞。而该模型是当copy完成后才通知application,可见是纯异步的。好像只有windows的完成端口是这个模型,效率也很出色。
6 下面是以上五种模型的比较

可以看出,越往后,阻塞越少,理论上效率也是最优。
=====================分割线==================================
5种模型的比较比较清晰了,剩下的就是把select,epoll,iocp,kqueue按号入座那就OK了。
select和iocp分别对应第3种与第5种模型,那么epoll与kqueue呢?其实也于select属于同一种模型,只是更高级一些,可以看作有了第4种模型的某些特性,如callback机制。
为什么epoll,kqueue比select高级?
答案是,他们无轮询。因为他们用callback取代了。想想看,当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
windows or *nix (IOCP or kqueue/epoll)?
诚然,Windows的IOCP非常出色,目前很少有支持asynchronous I/O的系统,但是由于其系统本身的局限性,大型服务器还是在UNIX下。而且正如上面所述,kqueue/epoll 与 IOCP相比,就是多了一层从内核copy数据到应用层的阻塞,从而不能算作asynchronous I/O类。但是,这层小小的阻塞无足轻重,kqueue与epoll已经做得很优秀了。
提供一致的接口,IO Design Patterns
实际上,不管是哪种模型,都可以抽象一层出来,提供一致的接口,广为人知的有ACE,Libevent(基于reactor模式)这些,他们都是跨平台的,而且他们自动选择最优的I/O复用机制,用户只需调用接口即可。说到这里又得说说2个设计模式,Reactor and Proactor。见:Reactor模式--VS--Proactor模式。Libevent是Reactor模型,ACE提供Proactor模型。实际都是对各种I/O复用机制的封装。
Java nio包是什么I/O机制?
现在可以确定,目前的java本质是select()模型,可以检查/jre/bin/nio.dll得知。至于java服务器为什么效率还不错。。我也不得而知,可能是设计得比较好吧。。-_-。
=====================分割线==================================
总结一些重点:
- 只有IOCP是asynchronous I/O,其他机制或多或少都会有一点阻塞。
- select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
- epoll, kqueue、select是Reacor模式,IOCP是Proactor模式。
- java nio包是select模型。。
(二)epoll 与select的区别
1. 使用多进程或者多线程,但是这种方法会造成程序的复杂,而且对与进程与线程的创建维护也需要很多的开销。(Apache服务器是用的子进程的方式,优点可以隔离用户) (同步阻塞IO)
2.一种较好的方式为I/O多路转接(I/O multiplexing)(貌似也翻译多路复用),先构造一张有关描述符的列表(epoll中为队列),然后调用一个函数,直到这些描述符中的一个准备好时才返回,返回时告诉进程哪些I/O就绪。select和epoll这两个机制都是多路I/O机制的解决方案,select为POSIX标准中的,而epoll为Linux所特有的。
区别(epoll相对select优点)主要有三:
1.select的句柄数目受限,在linux/posix_types.h头文件有这样的声明:#define __FD_SETSIZE 1024 表示select最多同时监听1024个fd。而epoll没有,它的限制是最大的打开文件句柄数目。
2.epoll的最大好处是不会随着FD的数目增长而降低效率,在selec中采用轮询处理,其中的数据结构类似一个数组的数据结构,而epoll是维护一个队列,直接看队列是不是空就可以了。epoll只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数(把这个句柄加入队列),其他idle状态句柄则不会,在这点上,epoll实现了一个"伪"AIO。但是如果绝大部分的I/O都是“活跃的”,每个I/O端口使用率很高的话,epoll效率不一定比select高(可能是要维护队列复杂)。
3.使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。
关于epoll工作模式ET,LT
epoll有两种工作方式
ET:Edge Triggered,边缘触发。仅当状态发生变化时才会通知,epoll_wait返回。换句话,就是对于一个事件,只通知一次。且只支持非阻塞的socket。
LT:Level Triggered,电平触发(默认工作方式)。类似select/poll,只要还有没有处理的事件就会一直通知,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll.支持阻塞和不阻塞的socket。
三 Linux并发网络编程模型
1 Apache 模型,简称 PPC ( Process Per Connection ,):为每个连接分配一个进程。主机分配给每个连接的时间和空间上代价较大,并且随着连接的增多,大量进程间切换开销也增长了。很难应对大量的客户并发连接。
2 TPC 模型( Thread Per Connection ):每个连接一个线程。和PCC类似。
3 select 模型:I/O多路复用技术。
.1 每个连接对应一个描述。select模型受限于 FD_SETSIZE即进程最大打开的描述符数linux2.6.35为1024,实际上linux每个进程所能打开描数字的个数仅受限于内存大小,然而在设计select的系统调用时,却是参考FD_SETSIZE的值。可通过重新编译内核更改此值,但不能根治此问题,对于百万级的用户连接请求 即便增加相应 进程数, 仍显得杯水车薪呀。
.2select每次都会扫描一个文件描述符的集合,这个集合的大小是作为select第一个参数传入的值。但是每个进程所能打开文件描述符若是增加了 ,扫描的效率也将减小。
.3内核到用户空间,采用内存复制传递文件描述上发生的信息。
4 poll 模型:I/O多路复用技术。poll模型将不会受限于FD_SETSIZE,因为内核所扫描的文件 描述符集合的大小是由用户指定的,即poll的第二个参数。但仍有扫描效率和内存拷贝问题。
5 pselect模型:I/O多路复用技术。同select。
6 epoll模型:
.1)无文件描述字大小限制仅与内存大小相关
.2)epoll返回时已经明确的知道哪个socket fd发生了什么事件,不用像select那样再一个个比对。
.3)内核到用户空间采用共享内存方式,传递消息。
四 :FAQ
1、单个epoll并不能解决所有问题,特别是你的每个操作都比较费时的时候,因为epoll是串行处理的。 所以你有还是必要建立线程池来发挥更大的效能。
2、如果fd被注册到两个epoll中时,如果有时间发生则两个epoll都会触发事件。
3、如果注册到epoll中的fd被关闭,则其会自动被清除出epoll监听列表。
4、如果多个事件同时触发epoll,则多个事件会被联合在一起返回。
5、epoll_wait会一直监听epollhup事件发生,所以其不需要添加到events中。
6、为了避免大数据量io时,et模式下只处理一个fd,其他fd被饿死的情况发生。linux建议可以在fd联系到的结构中增加ready位,然后epoll_wait触发事件之后仅将其置位为ready模式,然后在下边轮询ready fd列表。
参考:
http://blog.csdn.net/ysu108/article/details/7570571
http://techbbs.zol.com.cn/1/8_2245.html
io_submit:Linux内核新加入的epoll替代方案
在Linux内核4.18版本中新添加了一种新的内核接口Linux AIO方法IOCB_CMD_POLL。该补丁由Christoph Hellwig补充,将继续将Linux AIO接口配合网络替代等一起使用。
Linux的AIO是最初是设计用于磁盘异步IO的接口。文件与网络替代是大相径庭的东西,可以使用Linux AIO接口,将其统一起来呢?
在这里中,我们介绍如何使用Linux AIO API的优势来编写更好,重启的网络服务器。
Linux AIO简介
我们先来介绍一下Linux AIO。

在Linux上所有磁盘操作都是双重的。无论是open(),read(),write()还是fsync(),如果所需要的数据和元数据还没有在磁盘缓存准备好,则线程就会挂住。通常这不是问题。如果做少量的IO操作或内存足够大,则磁盘syscall会逐渐填充高速缓存,这样整体来说速度还是会很快。
但是对于IO繁重的工作负载(例如数据库或缓存Web代理),IO操作性能会下降很多。在此类应用中,很可能由于一些read()系统调用等待导致磁盘卡顿那是致命的。

要解决这类问题,通常变通使用如下方法:
这就是glibc POSIX AIO(不要与Linux AIO取代)包装器的作用。
posix_fadvise使用预热磁盘缓存,并希望达到最佳效果。
将Linux AIO与XFS文件系统一起使用,使用O_DIRECT打开文件,并避免出现未说明的陷阱。
这些变通的方法都不是完美的。
即使不小心使用Linux AIO,也可能会双重io_submit()调用。
Linux异步I / O(AIO)自从产生以来争议就很多,大多数人希望的至少是异步的,实际上也没有实现。但是由于种种原因,AIO操作可能会在内核中嵌套,从而使AIO在调用线程确实无法承受的情况下难以使用。
最简单的AIO示例
要使用Linux AIO,首先需要定额所需的系统调用。glibc不提供包装函数。要使用Linux AIO,需要:
首先调用io_setup()以设置aio_context数据结构。内核提供了一个不透明的指针。
然后调用io_submit()提交一个“ I / O控制块”矢量结构体iocb进行处理。
最后,调用io_getevents()块并等待一个向量结构体io_event-iocb的完成通知。
一个iocb中可以提交8个命令。4.18内核中约会了两个读取,两个写入,两个fsync变体和一个POLL命令:
IOCB_CMD_PREAD = 0,
IOCB_CMD_PWRITE = 1,
IOCB_CMD_FSYNC = 2
IOCB_CMD_FDSYNC = 3,
IOCB_CMD_POLL = 5,/ * 4.18 * /
IOCB_CMD_NOOP = 6
IOCB_CMD_PREADV = 7
IOCB_CMD_PWRITEV = 8
在iocb结构体传递到io_submit,调整为磁盘IO。这是一个简化的版本:
struct iocb {
__u64数据;/ *用户数据* /
...
__u16 aio_lio_opcode; / * IOCB_CMD_ * /
...
__u32 aio_fildes; / *文件预算* /
__u64 aio_buf; / *缓冲指针* /
__u64 aio_nbytes; / *缓冲大小* /
...
}
从io_getevents以下位置检索完成通知:
struct io_event {
__u64数据;/ *用户数据* /
__u64 obj; / * iocb请求指针* /
__s64 res; / *事件结果码* /
__s64 res2; / *第二结果* /
};
让我们举一个简单的例子,使用Linux AIO API重新读取/ etc / passwd文件:

fd = open(“ / etc / passwd”,O_RDONLY);
aio_context_t ctx = 0;
r = io_setup(128,&ctx);
char buf [4096];
struct iocb cb = {.aio_fildes = fd,
.aio_lio_opcode = IOCB_CMD_PREAD,
.aio_buf =(uint64_t)buf,
.aio_nbytes = sizeof(buf)};
struct iocb * list_of_iocb [1] = {&cb};
r = io_submit(ctx,1,list_of_iocb);
struct io_event events [1] = {{0}};
r = io_getevents(ctx,1,1,events,NULL);
bytes_read = events [0] .res;
printf(“从/ etc / passwd \ n中读取%lld个字节”,bytes_read);
我们用strace追踪程序的执行

这一切都OK!但磁盘重新插入非初始化的,io_submit系统调用被阻止并完成了所有工作!io_getevents通话瞬间完成。
我们可以尝试使磁盘初始化重新,而需要O_DIRECT标志来跳过缓存。
从文件中读取大的1GiB块时显示strace / dev / zero:让举一个更好的说明io_submit普通文件的阻止性质。
io_submit(0x7fe1e800a000,1,[{aio_lio_opcode = IOCB_CMD_PREAD,aio_fildes = 3,aio_buf = 0x7fe1a79f4000,aio_nbytes = 1073741824,aio_offset = 0}])= 1 <0.738380>
io_getevents(0x7fe1e800a000,1,1,[{data = 0,obj = 0x7fffb9588910,res = 1073741824,res2 = 0}],NULL)= 1 <0.000015>
内核中io_submit花费738ms,只有15us io_getevents消耗的。内核的行为与网络众多相同,所有工作都在io_submit中完成。
用Linux AIO处理套接字
io_submit的处理相当保守。除非传递的名义是O_DIRECT文件,否则相邻并发执行请求的操作。
对于双向专有,IOCB_CMD_PREAD将挂起,直到数据包到达为止。
对于非单一专有,IOCB_CMD_PREAD返回-11(EAGAIN)。
这些语法与read()系统调用完全相同。对于网络而言,io_submit并不比旧式读/写调用更好用。
重要的是要注意iob传递给内核的请求是按顺序进行的。
尽管Linux AIO无法帮助异步操作,但它绝对可以用于系统调用批处理。如果有一个Web服务器需要从多个单独网络交替发送和接收数据,那么使用io_submit是个好主意。这可以避免不必要的从用户空间和内核之间来回移位是需要耗时的。

为了说明的io_submit批处理,我们创建一个小程序,将数据从一个TCP转发转发到另一个。最简单的形式,如果没有Linux AIO,该程序将像下面这样简单:
而True:
d = sd1.read(4096)
sd2.write(d)
我们可以使用Linux AIO表达相同的逻辑。该代码将如下:
struct iocb cb [2] = {{.aio_fildes = sd2,
.aio_lio_opcode = IOCB_CMD_PWRITE,
.aio_buf =(uint64_t)&buf [0],
.aio_nbytes = 0},
{.aio_fildes = sd1,
.aio_lio_opcode = IOCB_CMD_PREAD,
.aio_buf =(uint64_t)&buf [0],
.aio_nbytes = BUF_SZ}};
struct iocb * list_of_iocb [2] = {&cb [0],&cb [1]};
while(1){
r = io_submit(ctx,2,list_of_iocb);
struct io_event events [2] = {};
r = io_getevents(ctx,2,2,events,NULL);
cb [0] .aio_nbytes = events [1] .res;
}
以上,代码向io_submit提交两个作业。首先,请求将数据写入sd2,然后从sd1中读取数据。读取完成后,代码将确定写入堆栈的大小并再次循环。该代码用了一个很酷的技巧:第一次写入大小为0。之所以这样做,是因为我们可以在一个io_submit中融合写入+读取(但不能读取+写)。读取完成后,我们必须修复写入正极的大小。
这代码比简单的读/写版本快吗?还不是。
两个版本都有两个系统调用:读+写和io_submit + io_getevents。
但是我们改善它。
出价io_getevents
当运行io_setup()时,内核为该进程分配几页内存。这是此内存块在/ proc / <pid> / maps中的样子:
猫/ proc /`pidof -s aio_passwd` / maps
...
7f7db8f60000-7f7db8f63000 rw-s 00000000 00:12 2314562 / aio(已删除)
...
在大多数情况下,没有任何理由对io_geteventssyscall真正的调用。可以从磁盘上轻松获取完成数据,而无需请求内核。下面是一个无需内核调用的修订版本:
int io_getevents(aio_context_t ctx,long min_nr,long max_nr,
struct io_event *事件,struct timespec *超时)
{
int i = 0;
struct aio_ring * ring =(struct aio_ring *)ctx;
如果(ring == NULL || ring-> magic!= AIO_RING_MAGIC){
转到do_syscall;
}
而(i <max_nr){
无符号头= ring-> head;
if(head == ring-> tail){
打破;
}其他{
/ *还有另一项收获* /
events [i] = ring-> events [head];
read_barrier();
ring-> head =(head +1)%ring-> nr;
i ++;
}
}
if(i == 0 &&超时!= NULL &&超时-> tv_sec == 0 &&超时-> tv_nsec == 0){
返回0;
}
如果(i && i> = min_nr){
返回我
}
do_syscall:
返回系统调用(__NR_io_getevents,ctx,min_nr-i,max_nr-i和&events [i],超时);
}
通过此代码恢复了该io_getevents功能,Linux AIO版本的TCP代理每个循环仅只需要一个系统调用,并且读取版本代码快一点。
替代Epoll
通过在内核4.18中添加IOCB_CMD_POLL,io_submit还可以用作选择/轮询/ epoll。例如,以下代码在套接字监听等待数据:
struct iocb cb = {.aio_fildes = sd,
.aio_lio_opcode = IOCB_CMD_POLL,
.aio_buf = POLLIN};
struct iocb * list_of_iocb [1] = {&cb};
r = io_submit(ctx,1,list_of_iocb);
r = io_getevents(ctx,1,1,events,NULL);
strace追踪显示:
io_submit(0x7fe44bddd000,1,[{aio_lio_opcode = IOCB_CMD_POLL,aio_fildes = 3}])= 1 <0.000015>
io_getevents(0x7fe44bddd000,1,1,[{data = 0,obj = 0x7ffef65c11a8,res = 1,res2 = 0}],NULL)= 1 <1.000377>
如上,该程序“异步”部分运行良好,io_submit立即完成,io_getevents等待数据成功中断了1秒钟。这是非常强大的功能,可以代替epoll_wait()系统调用使用。
另外,通常情况下,epoll杂项处理需要epoll_ctl系统调用。应用程序开发人员竭尽全力避免过多地调用调用。使用io_submit可以解决整个的复杂工作,并且过程中不需要任何虚假的系统调用。需将充足推给iocb请求向量,只调用io_submit一次并等待完成即可。
总结
本文中,我们回顾了Linux AIO接口。甚至最初被认为是仅用于磁盘的接口API,但它似乎与网络替代上的常规读/写系统调用的工作方式相同。但是与读/写不同从4.18版内核开始io_submit,io_getevents可用于等待网络替代上的事件如POLLIN和POLLOUT,用作替代epoll()事件循环。的是,它允许系统调用批处理io_submit,从而可以提高性能。
linux IO 5种方式
1、几个重要概念
用户空间与内核空间: 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
进程切换: 为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换,从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
1)保存处理机上下文,包括程序计数器和其他寄存器。
2)更新PCB信息。
3)把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
4)选择另一个进程执行,并更新其PCB。
5)更新内存管理的数据结构。
6)恢复处理机上下文
文件描述符fd:文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符
缓存 IO:缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 IO 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
2、linux IO模型
1)同步模型(synchronous IO)
2)阻塞IO(bloking IO)
3)非阻塞IO(non-blocking IO)
4)多路复用IO(multiplexing IO)
5)信号驱动式IO(signal-driven IO)
6)异步IO(asynchronous IO)
从内核角度看I/O操作分为两步:用户层API调用;内核层完成系统调用(发起I/O请求)。所以“异步/同步”的是指用户层API调用;“阻塞/非阻塞”是指内核完成I/O调用的模式。同步是指函数完成之前会一直等待; 阻塞 是指系统调用的时候进程会被设置为Sleep状态直到等待的事件发生。
同步:所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。一直占用CPU
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回这个期间客户端浏览器不能干任何事
异步:异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
阻塞: block 就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回),进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,阻塞是不占用CPU资源。

非阻塞: non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式,需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

信号:首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据

IO多路复用:主要是select、poll、epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听;
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

异步IO:异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它read操作完成了Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。

3、select、poll、epoll
Select:
基本原理:select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
缺点:
1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll:
基本原理:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
linux EPOLL Edge-Triggered Mode
https://blog.51cto.com/tener/1671953
什么是epoll
epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll的相关系统调用
epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用。
1. int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
[cpp] view plain copy print ?
-
//保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
-
-
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 :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。
epoll工作原理
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
Epoll的2种工作方式-水平触发(LT)和边缘触发(ET)
假如有这样一个例子:
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)......
Edge Triggered 工作模式:
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
i 基于非阻塞文件句柄
ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
Level Triggered 工作模式
相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。
LT(level triggered)是epoll缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。
因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。
图示说明:

Nginx默认采用ET模式来使用epoll。
epoll的优点:
1.支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2.IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
3.使用mmap加速内核与用户空间的消息传递
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。
4.内核微调
这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小--- 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。
linux下epoll如何实现高效处理百万句柄的
开发高性能网络程序时,windows开发者们言必称iocp,linux开发者们则言必称epoll。大家都明白epoll是一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄,比起以前的select和poll效率高大发了。我们用起epoll来都感觉挺爽,确实快,那么,它到底为什么可以高速处理这么多并发连接呢?
使用起来很清晰,首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。
epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。
epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
从上面的调用方式就可以看到epoll比select/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。
当一个进程调用epoll_creaqte方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关:
[cpp] view plain copy print ?
-
/*
-
-
171 * This structure is stored inside the "private_data" member of the file
-
-
172 * structure and represents the main data structure for the eventpoll
-
-
173 * interface.
-
-
174 */
-
-
175struct eventpoll {
-
-
176 /* Protect the access to this structure */
-
-
177 spinlock_t lock;
-
-
178
-
-
179 /*
-
-
180 * This mutex is used to ensure that files are not removed
-
-
181 * while epoll is using them. This is held during the event
-
-
182 * collection loop, the file cleanup path, the epoll file exit
-
-
183 * code and the ctl operations.
-
-
184 */
-
-
185 struct mutex mtx;
-
-
186
-
-
187 /* Wait queue used by sys_epoll_wait() */
-
-
188 wait_queue_head_t wq;
-
-
189
-
-
190 /* Wait queue used by file->poll() */
-
-
191 wait_queue_head_t poll_wait;
-
-
192
-
-
193 /* List of ready file descriptors */
-
-
194 struct list_head rdllist;
-
-
195
-
-
196 /* RB tree root used to store monitored fd structs */
-
-
197 struct rb_root rbr;//红黑树根节点,这棵树存储着所有添加到epoll中的事件,也就是这个epoll监控的事件
-
198
-
199 /*
-
200 * This is a single linked list that chains all the "struct epitem" that
-
201 * happened while transferring ready events to userspace w/out
-
202 * holding ->lock.
-
203 */
-
204 struct epitem *ovflist;
-
205
-
206 /* wakeup_source used when ep_scan_ready_list is running */
-
207 struct wakeup_source *ws;
-
208
-
209 /* The user that created the eventpoll descriptor */
-
210 struct user_struct *user;
-
211
-
212 struct file *file;
-
213
-
214 /* used to optimize loop detection check */
-
215 int visited;
-
216 struct list_head visited_list_link;//双向链表中保存着将要通过epoll_wait返回给用户的、满足条件的事件
-
217};
每一个epoll对象都有一个独立的eventpoll结构体,这个结构体会在内核空间中创造独立的内存,用于存储使用epoll_ctl方法向epoll对象中添加进来的事件。这样,重复的事件就可以通过红黑树而高效的识别出来。
在epoll中,对于每一个事件都会建立一个epitem结构体:
[cpp] view plain copy print ?
-
/*
-
130 * Each file descriptor added to the eventpoll interface will
-
131 * have an entry of this type linked to the "rbr" RB tree.
-
132 * Avoid increasing the size of this struct, there can be many thousands
-
133 * of these on a server and we do not want this to take another cache line.
-
134 */
-
135struct epitem {
-
136 /* RB tree node used to link this structure to the eventpoll RB tree */
-
137 struct rb_node rbn;
-
138
-
139 /* List header used to link this structure to the eventpoll ready list */
-
140 struct list_head rdllink;
-
141
-
142 /*
-
143 * Works together "struct eventpoll"->ovflist in keeping the
-
144 * single linked chain of items.
-
145 */
-
146 struct epitem *next;
-
147
-
148 /* The file descriptor information this item refers to */
-
149 struct epoll_filefd ffd;
-
150
-
151 /* Number of active wait queue attached to poll operations */
-
152 int nwait;
-
153
-
154 /* List containing poll wait queues */
-
155 struct list_head pwqlist;
-
156
-
157 /* The "container" of this item */
-
158 struct eventpoll *ep;
-
159
-
160 /* List header used to link this item to the "struct file" items list */
-
161 struct list_head fllink;
-
162
-
163 /* wakeup_source used when EPOLLWAKEUP is set */
-
164 struct wakeup_source __rcu *ws;
-
165
-
166 /* The structure that describe the interested events and the source fd */
-
167 struct epoll_event event;
-
168};
此外,epoll还维护了一个双链表,用户存储发生的事件。当epoll_wait调用时,仅仅观察这个list链表里有没有数据即eptime项即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!
那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
epoll的使用方法
那么究竟如何来使用epoll呢?其实非常简单。
通过在包含一个头文件#include <sys/epoll.h> 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。
首先通过create_epoll(int 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程序都使用下面的框架:
[cpp] view plain copy print ?
-
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的程序实例
[cpp] view plain copy print ?
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <errno.h>
-
#include <sys/socket.h>
-
#include <netdb.h>
-
#include <fcntl.h>
-
#include <sys/epoll.h>
-
#include <string.h>
-
-
#define MAXEVENTS 64
-
-
//函数:
-
//功能:创建和绑定一个TCP socket
-
//参数:端口
-
//返回值:创建的socket
-
static int
-
create_and_bind (char *port)
-
{
-
struct addrinfo hints;
-
struct addrinfo *result, *rp;
-
int s, sfd;
-
-
memset (&hints, 0, sizeof (struct addrinfo));
-
hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */
-
hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
-
hints.ai_flags = AI_PASSIVE; /* All interfaces */
-
-
s = getaddrinfo (NULL, port, &hints, &result);
-
if (s != 0)
-
{
-
fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));
-
return -1;
-
}
-
-
for (rp = result; rp != NULL; rp = rp->ai_next)
-
{
-
sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
-
if (sfd == -1)
-
continue;
-
-
s = bind (sfd, rp->ai_addr, rp->ai_addrlen);
-
if (s == 0)
-
{
-
/* We managed to bind successfully! */
-
break;
-
}
-
-
close (sfd);
-
}
-
-
if (rp == NULL)
-
{
-
fprintf (stderr, "Could not bind\n");
-
return -1;
-
}
-
-
freeaddrinfo (result);
-
-
return sfd;
-
}
-
-
-
//函数
-
//功能:设置socket为非阻塞的
-
static int
-
make_socket_non_blocking (int sfd)
-
{
-
int flags, s;
-
-
//得到文件状态标志
-
flags = fcntl (sfd, F_GETFL, 0);
-
if (flags == -1)
-
{
-
perror ("fcntl");
-
return -1;
-
}
-
-
//设置文件状态标志
-
flags |= O_NONBLOCK;
-
s = fcntl (sfd, F_SETFL, flags);
-
if (s == -1)
-
{
-
perror ("fcntl");
-
return -1;
-
}
-
-
return 0;
-
}
-
-
//端口由参数argv[1]指定
-
int
-
main (int argc, char *argv[])
-
{
-
int sfd, s;
-
int efd;
-
struct epoll_event event;
-
struct epoll_event *events;
-
-
if (argc != 2)
-
{
-
fprintf (stderr, "Usage: %s [port]\n", argv[0]);
-
exit (EXIT_FAILURE);
-
}
-
-
sfd = create_and_bind (argv[1]);
-
if (sfd == -1)
-
abort ();
-
-
s = make_socket_non_blocking (sfd);
-
if (s == -1)
-
abort ();
-
-
s = listen (sfd, SOMAXCONN);
-
if (s == -1)
-
{
-
perror ("listen");
-
abort ();
-
}
-
-
//除了参数size被忽略外,此函数和epoll_create完全相同
-
efd = epoll_create1 (0);
-
if (efd == -1)
-
{
-
perror ("epoll_create");
-
abort ();
-
}
-
-
event.data.fd = sfd;
-
event.events = EPOLLIN | EPOLLET;//读入,边缘触发方式
-
s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
-
if (s == -1)
-
{
-
perror ("epoll_ctl");
-
abort ();
-
}
-
-
/* Buffer where events are returned */
-
events = calloc (MAXEVENTS, sizeof event);
-
-
/* The event loop */
-
while (1)
-
{
-
int n, i;
-
-
n = epoll_wait (efd, events, MAXEVENTS, -1);
-
for (i = 0; i < n; i++)
-
{
-
if ((events[i].events & EPOLLERR) ||
-
(events[i].events & EPOLLHUP) ||
-
(!(events[i].events & EPOLLIN)))
-
{
-
/* An error has occured on this fd, or the socket is not
-
ready for reading (why were we notified then?) */
-
fprintf (stderr, "epoll error\n");
-
close (events[i].data.fd);
-
continue;
-
}
-
-
else if (sfd == events[i].data.fd)
-
{
-
/* We have a notification on the listening socket, which
-
means one or more incoming connections. */
-
while (1)
-
{
-
struct sockaddr in_addr;
-
socklen_t in_len;
-
int infd;
-
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
-
-
in_len = sizeof in_addr;
-
infd = accept (sfd, &in_addr, &in_len);
-
if (infd == -1)
-
{
-
if ((errno == EAGAIN) ||
-
(errno == EWOULDBLOCK))
-
{
-
/* We have processed all incoming
-
connections. */
-
break;
-
}
-
else
-
{
-
perror ("accept");
-
break;
-
}
-
}
-
-
//将地址转化为主机名或者服务名
-
s = getnameinfo (&in_addr, in_len,
-
hbuf, sizeof hbuf,
-
sbuf, sizeof sbuf,
-
NI_NUMERICHOST | NI_NUMERICSERV);//flag参数:以数字名返回
-
//主机地址和服务地址
-
-
if (s == 0)
-
{
-
printf("Accepted connection on descriptor %d "
-
"(host=%s, port=%s)\n", infd, hbuf, sbuf);
-
}
-
-
/* Make the incoming socket non-blocking and add it to the
-
list of fds to monitor. */
-
s = make_socket_non_blocking (infd);
-
if (s == -1)
-
abort ();
-
-
event.data.fd = infd;
-
event.events = EPOLLIN | EPOLLET;
-
s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
-
if (s == -1)
-
{
-
perror ("epoll_ctl");
-
abort ();
-
}
-
}
-
continue;
-
}
-
else
-
{
-
/* We have data on the fd waiting to be read. Read and
-
display it. We must read whatever data is available
-
completely, as we are running in edge-triggered mode
-
and won't get a notification again for the same
-
data. */
-
int done = 0;
-
-
while (1)
-
{
-
ssize_t count;
-
char buf[512];
-
-
count = read (events[i].data.fd, buf, sizeof(buf));
-
if (count == -1)
-
{
-
/* If errno == EAGAIN, that means we have read all
-
data. So go back to the main loop. */
-
if (errno != EAGAIN)
-
{
-
perror ("read");
-
done = 1;
-
}
-
break;
-
}
-
else if (count == 0)
-
{
-
/* End of file. The remote has closed the
-
connection. */
-
done = 1;
-
break;
-
}
-
-
/* Write the buffer to standard output */
-
s = write (1, buf, count);
-
if (s == -1)
-
{
-
perror ("write");
-
abort ();
-
}
-
}
-
-
if (done)
-
{
-
printf ("Closed connection on descriptor %d\n",
-
events[i].data.fd);
-
-
/* Closing the descriptor will make epoll remove it
-
from the set of descriptors which are monitored. */
-
close (events[i].data.fd);
-
}
-
}
-
}
-
}
-
-
free (events);
-
-
close (sfd);
-
-
return EXIT_SUCCESS;
-
}
