之前有一个刷 squid 的 purge 程序,大约要刷20台机器,当时采用多线程 + 非阻塞(select)模式,发现一个很奇怪的问题,当线程数多于52时,select 经常返回出错,
22 Invalid argument
9 Bad file descriptor
不得其解。
最近温习unip,读到select时,终于明白了。select 最多可以检查FD_SETSIZE(通常是1024)个描述符。结合fd_set 的实现(FD_SET 在 描述符(大小)对应的位置1),明白了,select 可以检查的描述符最大为FD_SETSIZE,而不是最多FD_SETSIZE个描述符(虽然通常是这样,因为unix总是先用最小的)。
在 purge 程序中,20台机器,52个线程 52 × 20 = 1020,线程多于52,肯定就有描述符大于1024。虽然每个select 检查的描述符个数小于1024,但是整个进程的描述符个数已经大于1024(个数大于1024,那么描述符的大小肯定大于1024)。对于大于1024的描述符,FD_SET的处理方式是拿描述符对1024取模。假设描述符为1029,FD_SET(1029,&set)相当于FD_SET(5,&set),而 5 可能不是一个有效的描述符,当 select 去检查描述符 5 时,因为 5 不是有效的描述符,所以出 Invalid argument 或 Bad file descriptor 就比较合理了。
select 的正确语义应该时,整个进程中,select 最多可以检查1024个描述符,而不管用了多少 select 函数。更精确的描述是,select 只能检查描述符大小小于1024的描述符,当描述符个数大约1024,描述符大小就肯定大于1024。
可以epoll,或者poll解决这个问题,用libevent库(libevent,用选择平台上最好的I/O多路转接函数),更好。