老谭笔记

OSX/iOS中多路I/O复用总结

在OSX/iOS中IO多路复用通常会选择select和kqueue,最近在尝试优化socket改进通信效率,所以总结一下两种模型的用法。

#select
select是socket编程中非常重要的一个函数,并且也是兼容性最好的一种模型,在unix、linux、windows都有对应的实现,其函数原型是:

1
2
3
4
5
6
7
int select(
int nfds,
fd_set* readfds,
fd_set* writefds,
fd_set* errorfds,
const struct timeval* timeout
);

nfds最大的文件描述符加1;
readfds:用于检查可读性的描述符集合,同时也是可读描述符的结果返回;
writefds:用于检查可写性的描述符集合,同时也是可写描述符的结果返回;
errorfds:用于检查异常的描述符集合,同时也是异常描述符的结果返回;
timeout:一个指向timeval结构的指针,用于决定select等待I/O的最长时间,它可以使select处于三种状态:
(1)第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
(2)第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
(3)第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,>返回值同上述。
函数返回值:
负值:select错误
正值:某些文件可读写或出错
零值:等待超时,没有可读写或错误的文件

#kqueue
kqueue是FreeBSD上的一种的多路复用机制,所以刚好能在OSX/iOS中使用。它是针对传统的select处理大量的文件描述符性能较低效而开发出来的。注册一批描述符>到kqueue以后,当其中的描述符状态发生变化时,kqueue将一次性通知应用程序哪些描述符可读、可写或出错了.
kqueue模型最主要的函数就是kevent,它与select类似,提供向内核注册/反注册/修改事件和返回就绪事件或错误事件,函数原型为:

1
2
3
4
5
6
7
8
int kevent(
int kq,
const struct kevent *changelist,
int nchanges,
struct kevent *eventlist,
int nevents,
const struct timespec *timeout
);

kq:由kqueue()返回的一个内核事件队列标识;
changelist:注册/反注册事件的列表;
nchanges:changelist的个数;
eventlist:用于返回有事件发生的列表;
nevents:传入的eventlist的最大长度;
timeout:一个指向timeval结构的指针,指定超时时间;
函数返回值与select类似。

另外比较重要的就是struct kevent这个结构体了,它既是注册、反注册、修改事件的载体,也是事件返回的载体,它的原型为:

1
2
3
4
5
6
7
8
struct kevent {
uintptr_t ident;
short filter;
u_short flags;
u_int fflags;
intptr_t data;
void *udata;
};

由于kqueue不仅仅用于socket,还可用于如文件状态、信号、进程等用途,以下仅仅当在网络编程时这些元素的值及含义:
ident:事件的id,实际应用中,一般设置为文件描述符;
filter:指定你希望内核用于ident成员的过滤器,我们可以指定EVFILT_READ(读状态)和EVFILT_READ(写状态);
flags:告诉内核应当对该事件在队列做何处理,我们可以指定EV_ADD(注册)、EV_DELETE(删除)、EV_ENABLE(开启,默认)、EV_DISABLE(停用),当flags用于eventlist返回事件时,可能包含EV_ERROR的掩码表示描述符错误事情;
fflags:用于指定你想让内核使用的特定于过滤器的标志;
data:用于保存任何特定于过滤器的数据,当filter指定为EVFILT_READ或EVFILT_READ时,data表示可读或可写的数据长度,当事件的描述符出错时,data表示错误>代码;
udata:data成员并不由kqueue使用,kqueue会把它的值不加修改地透传,用于类似于上下文。

#总结
select的缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024(不同平台对FD_SETSIZE设定不一样)
(4)select接口使用并不灵活,无法分步提交集合,也无法将提交和查询分步,必须在一次调用中完成
(4)fd_set不能逆向转换为fd,需要再单独维护一份描述符的列表
而kqueue刚好能弥补这些缺陷,但却无法在其它平台使用

关于两种模型的实现示例下载:
SocketServer_select.zip
SocketServer_kqueue.zip