Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1071253
  • 博文数量: 139
  • 博客积分: 1823
  • 博客等级: 上尉
  • 技术积分: 3403
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-05 09:54
文章存档

2014年(7)

2013年(16)

2012年(48)

2011年(68)

分类: 嵌入式

2011-07-29 17:04:04

非阻塞 I/O 经常使用 poll(System V)、select(BSD Unix)、 epoll(linux2.5.45开始)系统调用。

select系统调用

select()的调用形式为:
    #include
    #include
    int select(int maxfd,                   /*
要被检测的比特数,待检测的最大文件描述符大1*/
                 fd_set *readfds,             /*
被读监控的文件描述符集*/
                 fd_set *writefds,            /*
被写监控的文件描述符集*/
                 fd_set *exceptfds,         /*
被例外条件监控(异常监控)的文件描述符集*/
                 const struct timeval *timeout);/*
定时器*/

   参数timeout到了指定的时间,无论是否有设备准备好,都返回调用。timeval的结构定义如下:
    struct timeval{
        long tv_sec; //秒
        long tv_usec; //微秒
    }

    timeout取不同的值,该调用就表现不同的性质:
    timeout为0,调用立即返回
    timeout为NULL,select()调用就阻塞,直到知道有文件描述符就绪;
    timeout为正整数,就是一般的定时器

    select调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writefds和exceptfds中的所有没有就绪的描述符。select的返回值有如下情况:
    正常情况下返回就绪的文件描述符个数;
    经过了timeout时长后仍无设备准备好,返回值为0;
    如果select被某个信号中断,它将返回-1并设置errno为EINTR。
    如果出错,返回-1并设置相应的errno。

    系统提供了4个宏对描述符集进行操作:
    #include
    #include
    void FD_SET(int fd, fd_set *fdset);
    void FD_CLR(int fd, fd_set *fdset);
    void FD_ISSET(int fd, fd_set *fdset);
    void FD_ZERO(fd_set *fdset);

    FD_SET    设置文件描述符集fdset中对应于文件描述符fd的位(设置为1)
    FD_CLR    清除文件描述符集fdset中对应于文件描述符fd的位(设置为 0)
    FD_ZERO   清除文件描述符集fdset中的所有位(既把所有位都设置为0)。
    使用这3个宏在调用select前设置描述符屏蔽位

    在调用select后使用
    FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。

fd_set
---------------------------------------------------------
typedef __kernel_fd_set     fd_set;

typedef struct {
    unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;

#define __FDSET_LONGS   (__FD_SETSIZE/__NFDBITS)
#define __FD_SETSIZE    1024
#define __NFDBITS       (8 * sizeof(unsigned long))


    过去,描述符集被作为一个整数位屏蔽码得到实现,但是这种实现对于多于32个的文件描述符将无法工作。描述符集现在通常用整数数组中的位域表示,数组元素的每一位对应一个文件描述符。例如,一个整数占32位,那么整数数组的第一个元素代表文件描述符0到31,数组的第二个元素代表文件描述符32到63,以此类推。宏FD_SET设置整数数组中对应于fd文件描述符的位为1,宏FD_CLR设置整数数组中对应于fd文件描述符的位为0,宏FD_ZERO设置整数数组中的所有位都为0。

    假设执行如下程序后:
    #include
    #include
    fd_set readset;
    FD_ZERO(&readset);
    FD_SET(5, &readset);
    FD_SET(33, &readset);

    再执行如下程序后:
    FD_CLR(5, &readset);

    通常,操作系统通过宏_FD_SETSIZE来声明在一个进程中select所能操作的文件描述符的最大数目。例如:
    #define _FD_SETSIZE 1024
    既定义_FD_SETSIZE为1024,一个整数占4个字节,既32位,那么就是用包含32个元素的整数数组来表示文件描述符集。我们可以在头文件中修改这个值来改变select使用的文件描述符集的大小,但是必须重新编译内核才能使修改后的值有效。当前版本的unix操作系统没有限制 _FD_SETSIZE的最大值,通常只受内存以及系统管理上的限制。

select的中间三个指向描述符集的参数若全部为空指针,则select提供了较sleep(等待整数秒)更为精确的计时器。

poll系统调用
poll()系统调用是System V的多元I/O解决方案。它解决了select()的几个不足,尽管select()仍然经常使用(多数还是出于习惯,或者打着可移植的名义):
用户空间调用的poll函数定义如下:
#include <sys/poll.h>
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

和select()不一样,poll()没有使用低效的三个基于位的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组。pollfd结构体定义如下:

#include <sys/poll.h>

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
 

每一个pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码。内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。
合法的事件如下:
POLLIN                     有数据可读。
POLLRDNORM        有普通数据可读。
POLLRDBAND         有优先数据可读。
POLLPRI                  有紧迫数据可读。
POLLOUT                写数据不会导致阻塞。
POLLWRNORM       写普通数据不会导致阻塞。
POLLWRBAND        写优先数据不会导致阻塞。
POLLMSG                SIGPOLL消息可用。

此外,revents域中还可能返回下列事件:
POLLER                 指定的文件描述符发生错误。
POLLHUP              指定的文件描述符挂起事件。
POLLNVAL            指定的文件描述符非法。

timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时;timeout为0指示 poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF
一个或多个结构体中指定的文件描述符无效。
EFAULT
fds指针指向的地址超出进程的地址空间。
EINTR
请求的事件之前产生一个信号,调用可以重新发起。
EINVAL
nfds参数超出PLIMIT_NOFILE值。
ENOMEM
可用内存不足,无法完成请求。


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


内核驱动对以上系统调用的支持

对于以上系统调用的支持需要来自设驱动程序的相应支持。所有三个系统调用均通过驱动程序的poll方法提供。

而在内核的文件接口poll定义如下:
中声明:
unsigned int (*poll) (struct file *filp, poll_table *wait);
当用户空间程序在驱动程序关联的文件描述符上执行poll、select或epoll系统调用时,该驱动方法将被调用。

驱动通过调用函数 poll_wait增加一个等待队列到 poll_table 结构:
void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
poll_table在中声明,驱动程序开发员将其当成一个不透明的对象使用,不必了解该结构细节。


2. 返回一个位掩码:描述可能不必阻塞就立刻进行的操作,几个标志(通过 定义)用来指示可能的操作:

 标志

                          含义

 POLLIN

 如果设备无阻塞的读取,就返回该值

 POLLRDNORM

 通常的数据已经准备好可以读了,就返回该值。一个可读设备返回(POLLLIN | POLLRDNORM)

 POLLRDBAND

 如果可以从设备读出带外数据,就返回该值,它只可在与套接字相关的的文件描述符中使用,通常不用在设备驱动程序中

 POLLPRI

 如果可以无阻塞的读取高优先级(oob带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select把带外数据当作异常处理

 POLLHUP

 当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。

 POLLERR

 如果设备发生错误,就返回该值。

 POLLOUT

 如果设备可以无阻塞地写入,就返回该值

 POLLWRNORM

 设备已经准备好,可以写了,就返回该值。一个可写设备返回(POLLOUT | POLLNORM)

 POLLWRBAND

 于POLLRDBAND类似

阅读(1929) | 评论(0) | 转发(4) |
给主人留下些什么吧!~~