select
select函数原型如下:
select(int maxfdp,fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout)
fd_set
是一种数据结构,该数据结构中存放着文件描述符,即文件句柄。该数据结构有大小限制,受到内核参数FD_SETSIZE
的影响,一般为1024。该结构可以通过一些宏由人来操控。
fd_set set;
FD_ZERO(&set); //将set清零
FD_SET(fd, &set); //将fd加入set中
FD_CLR(fd, &set); //不再监控fd
FD_ISSET(fd, &set); //fd在set中是否就绪,即是否为1
timeval
是一种用来表示时间值的数据结构,它有两个成员,一个是秒数,另一个是毫秒数。
struct timeval{
long tv_sec;
long tv_usec;
}
select
函数中,maxfdp
要监控的文件描述符的最大值加1。三个fd_set
是文件描述符的集合,它们既代表输入参数,也代表输出参数。
readfds
代表可读的文件描述符集合,作为输入,select
要去检查它每一个为1的位是否可读;作为输出,调用者要使用FD_ISSET
去检查自己关注的句柄是否就绪,即是否为1。其他同理。
timeout
是超时时间,它可以设置为三种状态:
- NULL:代表该函数是阻塞的,只有当有句柄就绪时才返回
- 0:代表函数是非阻塞的,调用即返回
- 大于0:代表超时时间,在超时时间内阻塞,一旦有就绪句柄,立马返回;否则等到超时时间到才返回。
select的返回值是集合中就绪的句柄数目。如果小于0,代表发生错误;如果等于0,代表超时。
这里有一些关于select的小问题:
- 为什么select有最大文件描述符个数的限制?
答:注意,这里的最大文件描述符个数指的是单进程的最大限制。主要是因为select采用了fd_set这一数据结构,从该数据结构的定义中可以看到,fd_set
其实应该是一个数据类型为long
的fds_bits
数组,该数组的大小由一个howmany
宏来控制,而观察其参数可知,一个取决于FD_SETSIZE
;另一个取决于fd_mask
,即long
的大小,该值在32位系统中是4,64位系统中是8。计算可知,如果long
大小为8,那么fds_bits
数组大小为16,16Byte*8*8bit/Byte=1024bit
,数组共有1024个bit
;若long
大小为4,那么fds_bits
数组大小为32,数组的bit
数为32Byte*4*8bit/Byte=1024bit
,还是1024。所以,系统位数对该数组的比特数没有影响。
而fd_set
被宏视为一个位图进行操作,1位可以代表一个句柄,所以总共只能放1024个句柄。
#ifndef FD_SETSIZE
#define FD_SETSIZE 1024
#endif
#define NBBY 8 /* number of bits in a byte */
typedef long fd_mask;
#define NFDBITS (sizeof (fd_mask) * NBBY) /* bits per mask */
#define howmany(x,y) (((x)+((y)-1))/(y))
typedef struct _types_fd_set {
fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)];
} _types_fd_set;
#define fd_set _types_fd_set
作者:用心阁
链接:https://www.zhihu.com/question/37219281/answer/74003967
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
如何突破该限制?
答:如上所说,该限制针对的是单进程,所以一个方法就是使用多进程;另一个方法就是修改FD_SETSIZE
这个参数,并重新编译内核;还有就是使用poll和epoll。FD_SETSIZE
限制了什么?
答:起码在linux上,既限制了监视的文件描述符数目,又限制了最大的文件描述符。因为select的第一个参数,绝对不能大于FD_SETSIZE
,否则访问fd_set数组时会越界。其他复用方式为什么没有这种限制?
答:因为它们没有采用FD_SET
数据结构啊。
poll
poll和select类似,本质上没有太大区别,查询就绪事件时还是使用轮询,但是它没有最大文件描述符的限制。
函数原型如下:
int poll(struct pollfd*fds, unsigned int nfds,int timeout)
其中,pollfd结构体如下:
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
由原型可以发现,poll可以监听多个pollfd,且没有数目的限制,events是监视该文件描述符的事件掩码,由用户设置,revents是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events中请求的任何事件都有可能在revents中返回。合法事件如下:
POLLIN 有数据可读。
POLLRDNORM 有普通数据可读。
POLLRDBAND 有优先数据可读。
POLLPRI 有紧迫数据可读。
POLLOUT 写数据不会导致阻塞。
POLLWRNORM 写普通数据不会导致阻塞。
POLLWRBAND 写优先数据不会导致阻塞。
POLLMSGSIGPOLL 消息可用
函数返回值和select一样,成功时返回revents中不为0的事件数,超时返回0,失败返回-1。
epoll
1. int epoll_create(int size);
size是内核能够正确处理的最大句柄数目,返回值是一个epollfd。
epoll在内核中申请了一个简易的文件系统。该函数创建了一个epoll对象,并在epoll文件系统中为这个句柄对象分配资源。具体是创建了一个eventpoll结构体:
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每个epoll对象(即fd)都有一个结构体,用于存放epoll_ctl方法向epoll对象中添加进来的事件。这些事件会挂到红黑树中。利用红黑树来维护事件。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd就是第一个函数的返回值,op是要进行的操作(EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL
),fd是要进行操作的句柄,event是监听事件的集合。在event结构体中有一个epoll_data的union,一定要填写这个域来表明是哪个fd在监听事件。
struct epoll_event
结构体如下:
typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event{
__uint32_t events;
epoll_data_t data;
};
该函数将要监控的事件放到内核cache里的红黑树上,并向中断处理程序注册一个回调函数,当设备就绪的时候,就将该事件添加到就绪列表中。
具体点说,如果有一个事件被添加到epoll中,那么内核会为它生成一个对应的epitem结构对象,epitem会被添加到eventpoll的红黑树中。
3. int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)
epfd同上,events用来从内核获得事件的集合,maxevents告诉内核这个events有多大,timeout是超时时间。调用这个函数会直接访问eventpoll结构的rdlist就绪链表,如果不为空,就把发生的事件拷贝到用户空间(即填写events参数),并将事件数量返回给用户。用户可以访问events,来获取就绪的fd及对应的事件。
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
比较
- poll要比select好,因为没有文件描述符大小的限制;而且在应付大数目的文件描述符时更快,不用像select一样去对比fd_set中的每一个比特位。
- select中输入输出使用同一个fd_set,所以该fd_set会一直变化,在每一次调用select时都需要重新设置该值。而poll将输入输出事件分开,允许复用被监控的文件数组。
- select每次返回时超时参数也是未定义的,所以每次调用都需要对其进行初始化。
- select的优点就是可移植性好,超时精度较高。
- epoll主要有如下优点:支持进程打开大数目的文件描述符;IO效率不随fd的增大而降低,因为它只关注活跃的fd;使用mmap加速内核和用户空间的消息传递,主要是因为epoll中内核与用户空间mmap同一块内存。
内容来自:
IO多路复用之poll总结