Linux网络编程 五种I/O 模式及select、epoll方法的理解

近期一次面试机会让我觉得有很多地方理解可能不到位,翻翻谷歌的资料加深对一些技术的理解

五种I/O 模式:
【1】 阻塞 I/O (Linux下的I/O操作默认是阻塞I/O,即open和socket创建的I/O都是阻塞I/O)
【2】 非阻塞 I/O (可以通过fcntl或者open时使用O_NONBLOCK参数,将fd设置为非阻塞的I/O)
【3】 I/O 多路复用 (I/O多路复用,通常需要非阻塞I/O配合使用)
【4】 信号驱动 I/O (SIGIO)
【5】 异步 I/O
一般来说,程序进行输入操作有两步:
1.等待有数据可以读
2.将数据从系统内核中拷贝到程序的数据区。
对于sock编程来说:
第一步: 一般来说是等待数据从网络上传到本地。当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中;
第二步: 是从内核中把数据拷贝到程序的数据区中。

阻塞I/O模式 //进程处于阻塞模式时,让出CPU,进入休眠状态
阻塞 I/O 模式是最普遍使用的 I/O 模式。是Linux系统下缺省的IO模式。
大部分程序使用的都是阻塞模式的 I/O 。
一个套接字建立后所处于的模式就是阻塞 I/O 模式。(因为Linux系统默认的IO模式是阻塞模式)

对于一个 UDP 套接字来说,数据就绪的标志比较简单:
(1)已经收到了一整个数据报
(2)没有收到。
而 TCP 这个概念就比较复杂,需要附加一些其他的变量。
一个进程调用 recvfrom ,然后系统调用并不返回知道有数据报到达本地系统,然后系统将数据拷贝到进程的缓存中。 (如果系统调用收到一个中断信号,则它的调用会被中断)
我们称这个进程在调用recvfrom一直到从recvfrom返回这段时间是阻塞的。当recvfrom正常返回时,我们的进程继续它的操作。

非阻塞模式I/O //非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源。
当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核: “当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
我们开始对 recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返回一个 EWOULDBLOCK的错误。
第四次我们调用 recvfrom 函数,一个数据报已经到达了,内核将它拷贝到我们的应用程序的缓冲区中,然后 recvfrom 正常返回,我们就可以对接收到的数据进行处理了。
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做 polling(轮询))。应用程序不停的 polling 内核来检查是否 I/O操作已经就绪。这将是一个极浪费 CPU资源的操作。这种模式使用中不是很普遍。

例如:
对管道的操作,最好使用非阻塞方式!
I/O多路复用 //针对批量IP操作时,使用I/O多路复用,非常有好。
在使用 I/O 多路技术的时候,我们调用 select()函数和 poll()函数或epoll函数(2.6内核开始支持),在调用它们的时候阻塞,而不是我们来调用 recvfrom(或recv)的时候阻塞。
当我们调用 select函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当select函数返回的时候, 也就是套接字可以读取数据的时候。 这时候我们就可以调用 recvfrom函数来将数据拷贝到我们的程序缓冲区中。
对于单个I/O操作,和阻塞模式相比较,select()和poll()或epoll并没有什么高级的地方。
而且,在阻塞模式下只需要调用一个函数:
读取或发送函数。
在使用了多路复用技术后,我们需要调用两个函数了:
先调用 select()函数或poll()函数,然后才能进行真正的读写。

多路复用的高级之处在于::
它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

IO 多路技术一般在下面这些情况中被使用:
1、当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字),I/O 多路复用技术将会有机会得到使用。
2、当程序需要同时进行多个套接字的操作的时候。
3、如果一个 TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。
4、如果一个服务器程序同时使用 TCP 和 UDP 协议。
5、如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如 inetd就是这样的)。
异步IO模式有::
1、信号驱动I/O模式
2、异步I/O模式
信号驱动I/O模式 //自己没有用过。

我们可以使用信号,让内核在文件描述符就绪的时候使用 SIGIO 信号来通知我们。我们将这种模式称为信号驱动 I/O 模式。

为了在一个套接字上使用信号驱动 I/O 操作,下面这三步是所必须的。
(1)一个和 SIGIO信号的处理函数必须设定。
(2)套接字的拥有者必须被设定。一般来说是使用 fcntl 函数的 F_SETOWN 参数来
进行设定拥有者。
(3)套接字必须被允许使用异步 I/O。一般是通过调用 fcntl 函数的 F_SETFL 命令,O_ASYNC为参数来实现。
虽然设定套接字为异步 I/O 非常简单,但是使用起来困难的部分是怎样在程序中断定产生 SIGIO信号发送给套接字属主的时候,程序处在什么状态。
1.UDP 套接字的 SIGIO 信号 (比较简单)
在 UDP 协议上使用异步 I/O 非常简单.这个信号将会在这个时候产生:
1、套接字收到了一个数据报的数据包。
2、套接字发生了异步错误。
当我们在使用 UDP 套接字异步 I/O 的时候,我们使用 recvfrom()函数来读取数据报数据或是异步 I/O 错误信息。
2.TCP 套接字的 SIGIO 信号 (不会使用)
不幸的是,异步 I/O 几乎对 TCP 套接字而言没有什么作用。因为对于一个 TCP 套接字来说,SIGIO 信号发生的几率太高了,所以 SIGIO 信号并不能告诉我们究竟发生了什么事情。
在 TCP 连接中, SIGIO 信号将会在这个时候产生:
l 在一个监听某个端口的套接字上成功的建立了一个新连接。
l 一个断线的请求被成功的初始化。
l 一个断线的请求成功的结束。
l 套接字的某一个通道(发送通道或是接收通道)被关闭。
l 套接字接收到新数据。
l 套接字将数据发送出去。
l 发生了一个异步 I/O 的错误。
一个对信号驱动 I/O 比较实用的方面是 NTP(网络时间协议 Network Time Protocol)服务器,它使用 UDP。这个服务器的主循环用来接收从客户端发送过来的数据报数据包,然后再发送请求。对于这个服务器来说,记录下收到每一个数据包的具体时间是很重要的。
因为那将是返回给客户端的值,客户端要使用这个数据来计算数据报在网络上来回所花费的时间。图 6-8 表示了怎样建立这样的一个 UDP 服务器。

异步I/O模式 //比如写操作,只需用写,不一定写入磁盘(这就是异步I/O)的好处。异步IO的好处效率高。
当我们运行在异步 I/O 模式下时,我们如果想进行 I/O 操作,只需要告诉内核我们要进行 I/O 操作,然后内核会马上返回。具体的 I/O 和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的 I/O 操作和数据拷贝后,内核将通知我们的程序。
异步 I/O 和 信号驱动I/O的区别是:
1、信号驱动 I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送SIGIO 消息。
2、异步 I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。

=================EPOLL 模型===============================================
Epoll模型详解
Linux 2.6内核中提高网络I/O性能的新方法-epoll I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。

1、为什么select落后
首先,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在 我用的2.6.15-25-386内核中,该值是1024,搜索内核源代码得到:
include/linux/posix_types.h:
#define __FD_SETSIZE 1024
也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的。或者 同时检测1025个句柄的可写状态也是不可能的。其次,内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。当然,在前文中我并没有提及poll方法,事实上用select的朋友一定也试过poll,我个人觉得 select和poll大同小异,个人偏好于用select而已。

2、内核中提高I/O性能的新方法epoll
epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。要使用epoll只需要这三个系统调 用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。
当然,这不是2.6内核才有的,它是在 2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)

Linux2.6 内核epoll介绍
先介绍2本书《The Linux Networking Architecture–Design and Implementation of Network Protocols in the Linux Kernel》,以2.4内核讲解Linux TCP/IP实现,相当不错.作为一个现实世界中的实现,很多时候你必须作很多权衡,这时候参考一个久经考验的系统更有实际意义。举个例子,linux内 核中sk_buff结构为了追求速度和安全,牺牲了部分内存,所以在发送TCP包的时候,无论应用层数据多大,sk_buff最小也有272的字节.其实 对于socket应用层程序来说,另外一本书《UNIX Network Programming Volume 1》意义更大一点.2003年的时候,这本书出了最新的第3版本,不过主要还是修订第2版本。其中第6章《I/O Multiplexing》是最重要的。Stevens给出了网络IO的基本模型。在这里最重要的莫过于select模型和Asynchronous I/O模型.从理论上说,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告诉你IO操作完成。但是一直以来,如何实现就没有一个完美的方 案。最著名的windows完成端口实现的AIO,实际上也是内部用线程池实现的罢了,最后的结果是IO有个线程池,你应用也需要一个线程池…… 很多文档其实已经指出了这带来的线程context-switch带来的代价。在linux 平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有很多AIO内核patch,最著名的应该算是SGI那个。但是一直到2.6内核发布,网络 模块的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差不多 了)。2.6内核所支持的AIO特指磁盘的AIO—支持io_submit(),io_getevents()以及对Direct IO的支持(就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有相当帮助)。
所以,剩下的select模型基本上就是我们在linux上面的唯一选择,其实,如果加上no-block socket的配置,可以完成一个”伪”AIO的实现,只不过推动力在于你而不是os而已。不过传统的select/poll函数有着一些无法忍受的缺 点,所以改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案

3、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网 卡驱动架构。

4、epoll的工作模式
令人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦 是epoll有2种工作方式:LT和ET。
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致 了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html ,在http://www.kegel.com/rn/也有一个完整的例子,大家一看就知道如何使用了
Leader/follower模式线程 pool实现,以及和epoll的配合。

5、 epoll的使用方法
首先通过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模型主要负责对大量并发用户的请求进行及时处理,完成服务器与客户端的数据交互。其具体的实现步骤如下:
(a) 使用epoll_create()函数创建文件描述,设定将可管理的最大socket描述符数目。
(b) 创建与epoll关联的接收线程,应用程序可以创建多个接收线程来处理epoll上的读通知事件,线程的数量依赖于程序的具体需要。
(c) 创建一个侦听socket描述符ListenSock;将该描述符设定为非阻塞模式,调用Listen()函数在套接字上侦听有无新的连接请求,在 epoll_event结构中设置要处理的事件类型EPOLLIN,工作方式为 epoll_ET,以提高工作效率,同时使用epoll_ctl()注册事件,最后启动网络监视线程。
(d) 网络监视线程启动循环,epoll_wait()等待epoll事件发生。
(e) 如果epoll事件表明有新的连接请求,则调用accept()函数,将用户socket描述符添加到epoll_data联合体,同时设定该描述符为非 阻塞,并在epoll_event结构中设置要处理的事件类型为读和写,工作方式为epoll_ET.
(f) 如果epoll事件表明socket描述符上有数据可读,则将该socket描述符加入可读队列,通知接收线程读入数据,并将接收到的数据放入到接收数据 的链表中,经逻辑处理后,将反馈的数据包放入到发送数据链表中,等待由发送线程发送。
C++语言: epoll使用方法101 //epoll_wait范围之后应该是一个循环,遍利所有的事件:
02 for (n = 0; n < nfds; ++n)
03 {
04 if (events[n].data.fd == listener)
05 {//如果是主socket的事件的话,则表示有新连接进入了,进行新连接的处理。
06 client = accept (listener, (struct sockaddr *) &local, &addrlen);
07 if (client < 0)
08 {
09 perror ("accept");
10 continue;
11 }
12 setnonblocking (client); // 将新连接置于非阻塞模式
13 ev.events = EPOLLIN | EPOLLET; // 并且将新连接也加入EPOLL的监听队列。
14 //注意,这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,
15 //如果有写操作的话,这个时候epoll是不会返回事件的,
16 //如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET
17 ev.data.fd = client;
18 if (epoll_ctl (kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)
19 {
20 /*
21 设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,
22 这里用EPOLL_CTL_ADD来加一个新的epoll事件,通过EPOLL_CTL_DEL来减少一个epoll事件,通
23 过EPOLL_CTL_MOD来改变一个事件的监听方式.
24 */
25 fprintf (stderr, "epoll set insertion error: fd=%d", client);
26 return -1;
27 }
28 }
29 else // 如果不是主socket的事件的话,则代表是一个用户socket的事件,
30 do_use_fd (events[n].data.fd); //则来处理这个用户socket的事情,比如说read(fd,xxx)之类的,或者一些其他的处理。
31 }
对,epoll的操作就这么简单,总共不过4个 API:epoll_create, epoll_ctl, epoll_wait和close。
如果您对epoll的效率还不太了解,请参考我 之前关于网络游戏的网络编程等相关的文章。

以前公司的服务器都是使用HTTP连接,但是这样的话,在手机目前的网络情况下不但显得速度较慢,而且不稳定。因此大家一致同意用 SOCKET来进行连接。虽然使用SOCKET之后,对于用户的费用可能会增加(由于是用了CMNET而非CMWAP),但是,秉着用户体验至上的原则, 相信大家还是能够接受的(希望那些玩家月末收到帐单不后能够保持克制...)。
这次的服务器设计中,最重要的一个突破,是使用了EPOLL模型, 虽然对之也是一知半解,但是既然在各大PC网游中已经经过了如此严酷的考验,相信他不会让我们失望,使用后的结果,确实也是表现相当不错。在这里,我还是 主要大致介绍一下这个模型的结构。
6、Linux下EPOll编程实例
EPOLL模型似乎只有一种格式,所以大家只要参考我下面的代码, 就能够对EPOLL有所了解了,代码的解释都已经在注释中:

C++语言: Codee#1176301 while (TRUE)
02 {
03 int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT); //等待EPOLL时间的发生,相当于监听,
04 //至于相关的端口,需要在初始化EPOLL的时候绑定。
05 if (nfds <= 0)
06 continue;
07 m_bOnTimeChecking = FALSE;
08 G_CurTime = time (NULL);
09 for (int i = 0; i < nfds; i++)
10 {
11 try
12 {
13 if (m_events[i].data.fd == m_listen_http_fd) //如果新监测到一个HTTP用户连接到绑定的HTTP端口,
14 //建立新的连接。由于我们新采用了SOCKET连接,所以基本没用。
15 {
16 OnAcceptHttpEpoll ();
17 }
18 else if (m_events[i].data.fd == m_listen_sock_fd) //如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,
19 //建立新的连接。
20 {
21 OnAcceptSockEpoll ();
22 }
23 else if (m_events[i].events & EPOLLIN) //如果是已经连接的用户,并且收到数据,那么进行读入。
24 {
25 OnReadEpoll (i);
26 }
27
28 OnWriteEpoll (i); //查看当前的活动连接是否有需要写出的数据。
29 }
30 catch (int)
31 {
32 PRINTF ("CATCH捕获错误\n");
33 continue;
34 }
35 }
36 m_bOnTimeChecking = TRUE;
37 OnTimer (); //进行一些定时的操作,主要就是删除一些短线用户等。
38 }

epoll优势 <1>支持一个进程打开大数 目的socket描述符(FD)
<2>IO 效率不随FD数目增加而线性下降
<3>使用mmap加速内核 与用户空间的消息传递。
<4>内核微调
工作模式 LT ET 在通知次数上区别(重复直到IO操作/只通知一次)

你可能感兴趣的:(网络编程)