Quantcast
Channel: Sam的技术Blog
Viewing all articles
Browse latest Browse all 158

Linux下IO多路复用select,poll,epoll

$
0
0
作者: Sam (甄峰)  sam_code@hotmail.com

1. I/O多路复用:
IO多路复用使得程序可以同时监听多个文件描述符(socket也是一种文件描述符),这对提高程序性能至关重要。

通常,网络程序在下列情况下需要使用I/O复用技术: 
A. 客户端程序要同时处理多个socket.
B. 客户端程序要同时处理用户输入和网络连接。
C. TCP服务器要同时处理监听Socket连接和socket数据。这是I/O复用最常用的场景。
D. 服务器要同时处理TCP请求和UDP请求。
E: 服务器要同时监听多个端口,或处理多种服务。如xinetd.

Linux下实现多路复用的系统调用主要包括: select, poll, epoll.

I/O多路复用虽然可以监听多路文件描述符,但它本身却是阻塞的。


2. Linux并发网络开发模型:
Linux下设计并发网络程序,方法很多,如PPC(Process Per Connect)模型, TPC(Thread Per Connect)模型, select模型和poll模型。Epoll也在2.6Kernel时被引入。

PPC /TPC模型:
这两种模型思路类似,都是每个连接建立一个Process/Thread. 让他们自我维护。 但Process/Thread都需要资源。再加上Process/Thread切换。 开销会较大。
所以此类模型能够接受的最大连接数都不大,几百个左右。


3. select和poll:
多路复用IO,2.4内核前主要是select和poll。
select(2)函数介绍:
select(2)使我们在SVR4和4.3+BSD之下可以执行I/O多路转接。 

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

select(2)函数告知Kernel: a.我们所关心的描述符。b.对于每个描述符,我们所关心的条件(是否可读,是否可写,是否有异常)。  c. 希望等待的时间。

select(2)返回时,Kernel会负责告诉我们:
a. 已经准备好的描述符数量。
b. 哪一个描述符准备好读,写,或异常条件。

当参数5 timeout为NULL时,select(2)会永远等待,直到所指定的描述符中的一个已经准备好或捕捉到一个信号。
参数2,3,4:readfds, writefds, exceptfds是指向描述符集合的指针。它们分别放置了我们设定的可读,可写,处于异常条件的各描述符。
他们可以为NULL,表明对相应的条件并不关心。
参数1:nfds, 最大的fd+1. 指三个描述符集中找到最高的描述符编号值,然后加1。 有人喜欢把这个值直接设置为:FD_SETSIZE. (sys/types.h中设置的常数,表明最大的描述符数)。但这并不是好办法。因为Kernel会根据这个值在一定范围内查询。如果值太大,效率会降低。 
#define        __FD_SETSIZE            1024
#define    FD_SETSIZE              __FD_SETSIZE

返回值:
-1:以上描述符都没有准备好时就捕捉到一个信号。
0: 以上描述符都没有准备好,但Timeout。
正数:已经准备好的描述符数量。此时,描述符集中打开的对应位就是已经准备好的描述符位。

文件描述符集合的使用
select(2)中的2,3,4三个参数readfds, writefds, exceptfds都是指向描述符集的指针(fd_set*)。这个类型对用户并不透明,需要使用宏进行操作。

void FD_ZERO(fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
       
void FD_ZERO(fd_set *set);
初始化文件描述符集,其中所有位均归零。

void FD_SET(int fd, fd_set *set);
设置文件描述符集中对应fd的位为1。也就是把fd加入了文件描述符集合。

void FD_CLR(int fd, fd_set *set);
把fd移除文件描述符集合。

int  FD_ISSET(int fd, fd_set *set);
查看fd是否存在于文件描述符集合set中。

当关心的文件描述符发生了关心的事件时,我们通过select()返回值获取到个数,但必须使用FD_ISSET()一个一个检查是谁。在关心的文件描述符较大时,效率会很低。

最大并发数限制,因为一个进程所打开的FD(文件描述符)是有限制的
效率问题,select每次调用都会线性扫描全部的FD集合,这样效率就会呈现线性下降
内核/用户空间 内存拷贝问题,如何让内核把FD消息通知给用户空间呢?在这个问题上select采取了内存拷贝方法

poll(2):
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
poll和select用法思路很像, 也是提供一组关心的文件描述符及其事件给Kernel。 当有对应事件发生时,则返回事件个数。
用户也必须逐个查看fds数据中内容。才能确认哪个文件描述符发生了那个Event。


通过注册一堆事件组,当有事件请求时返回,然后仍然需要轮询一遍pollfd才能知道查找到对应的文件描述符,数据也需要在内核空间和用户空间来回拷贝




4. epoll:
Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术。

为何会引入epoll. 肯定是select和poll有什么不足之处。现分析入下:

高并发的核心解决方案是1个线程处理所有连接的“等待消息准备好”。
但select和poll预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select的使用方法是这样的:
      返回的活跃连接 = select(全部待监控的连接)。
      什么时候会调用select方法呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,调用select在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被“频繁”二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select就完全力不从心了。
      此外,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数。
#define __FD_SETSIZE         1024

其次,内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时.

epoll在以上三个问题的优势:
1. epoll没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于2048, 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看
2. 效率提升,Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll.
3. 内存拷贝,Epoll在这点上使用了“共享内存”,这个内存拷贝也省略了





 

Viewing all articles
Browse latest Browse all 158

Trending Articles