linux发行版是centos6.x 64,文中贴的代码段删除了一些预编译处理。
select是一种常见的IO复用技术。仔细思考其与epoll的不同,接口设计的不合理之处,挺有启发的。
函数原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds:关注的最大文件描述符 +1
readfds:可读描述符集
writefds:可写描述符集
exceptfds:异常描述符集
timeout:等待超时
看一下fd_set
typedef long int __fd_mask; // 64位系统 8字节的long int
#define __NFDBITS (8 * (int) sizeof (__fd_mask)) // 64
#define __FD_SETSIZE 1024
typedef struct {
__fd_mask fds_bits[ __FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
}fd_set;
一个struct,由长度为16的INT64数组组成,共有1024个bit位,除掉stdin,stdout,stderr,采用select的话,一个进程能复用的IO文件描述符只有1021个。
与select一起配合使用的有几个宏,里面有一些实用的位操作,当然C++可以用std::bitset:
void FD_ZERO(fd_set *fdset); // 将数组置0
void FD_SET(int fd, fd_set *fdset); // 将fd对应的bit设为1
void FD_CLR(int fd, fd_set *fdset); // 将fd对应的bit设为0
int FD_ISSET(int fd, fd_set *fdset); // 测试fd对应的bit
FD_SET(fd, &fd_set)的展开:
(((&fd_set)->fds_bits)[((fd) / (8 * (int) sizeof (__fd_mask)))] |= ((__fd_mask) 1 << ((fd) % (8 * (int) sizeof (__fd_mask)))))
先看数组下标中的展开:
[((fd) / (8 * (int) sizeof (__fd_mask)))]
fd除64,即0-63的fd,对应数组中的第一个INT64位的bit位,64-127对应第2个INT64的bit位,依次类推。
再看右边的展开:
((__fd_mask) 1 << ((fd) % (8 * (int) sizeof (__fd_mask)))))
先对64取余,数字的范围是[0, 63],正好对应INT64的64个bit位,通过左移将1移动到合适的位置上。
最后通过或操作将指定bit置1。
FD_CLR FD_ISSET类似。
select接口的不合理之处:
1、nfds参数,这意味着每个用户程序必须知道当前已打开的文件描述符中最大的一个,这个完全可以由内核去处理,比如传0时代表当前已打开的最大文件描述符 + 1。这是我最无法理解的接口设计,内核在分配文件描述符时是连续分配的,可以很容易就知道最大的文件描述符,为什么还要在用户程序中去做这个事情。
启发:最小知识原则,接口调用者需要知道的应该越少越好。
2、没有区分不同类型的文件描述符,虽然一切皆文件的抽象很简单清晰,但一个进程内处理多个类型的文件描述符时,要么调用多次select,分别处理。要么一次select,代码糅在一起。epoll可以通过epoll_create多个实例的方式,在一个进程内处理多组文件描述符。
启发:对不同对象提供单一抽象的同时,也要能提供将这些对象分组处理的机制。
3、select是用户轮询所有的文件描述符,epoll是内核通知用户需要处理的文件描述符,主动轮询相较被动处理,编程更复杂,效率也更低(一个进程在打开过多文件描述符时的遍历消耗呈线性递增趋势,在大量连接少数连接活跃时效率较低)。
启发:好莱坞原则。
勘误:都是主动查询(select vs epoll_wait),区别在于select要轮询所有描述符,epoll轮询活跃的链接。