Iinux 中 select 函数的使用 阻塞式 I/O 编程有两个特点 : 一 如果一个发现 I\O 有输入, 读取的过程中, 另外一个也有了输入, 这时候不会产生任何反应, 也就是需要你的程序语句去 select 的时候才知道有数据输入 二 程序去 select 的时候, 如果没有数据输入, 程序会一直等待, 直到有数据位置, 也就是程序中无需循环和 sleep Select 在 Socket 编程中还是比较重要的, 可是对于初学 Socket 的人来说都不太爱用 Select 写程序, 他们只是习惯写诸如 connect accept recv 或 recvfrom 这样的阻塞程序 ( 所谓阻塞方式 block, 顾名思义, 就是进程或是线程执行到这些函数时必须等待某个事件的发生, 如果事件没有发生, 进程或线程就被阻塞, 函数不能立即返回 ) 可是使用 Select 就可以完成非阻塞 ( 所谓非阻塞方式 non-block, 就是进程或线程执行此函数时不必非要等待事件的发生, 一旦执行肯定返回, 以返回值的不同来反映函数的执行情况, 如果事件发生则与阻塞方式相同, 若事件没有发生则返回一个代码来告知事件未发生, 而进程或线程继续执行, 所以效率较高 ) 方式工作的程序, 它能够监视我们需要监视的文件描述符的变化情况 读写或是异常 下面详细介绍一下! Select 的函数格式 ( 我所说的是 Unix 系统下的伯克利 socket 编程, 和 windows 下的有区别, 一会儿说明 ): int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout); 先说明两个结构体 : 第一,struct fd_set 可以理解为一个集合, 这个集合中存放的是文件描述符 (file descriptor), 即文件句柄, 这可以是我们所说的普通意义的文件, 当然 Unix 下任何设备 管道 FIFO 等都是文件形式, 全部包括在内, 所以毫无疑问一个 socket 就是一个文件,socket 句柄就是一个文件描述符 fd_set 集合可以通过一些宏由人为来操作, 比如清空集合 FD_ZERO(fd_set *), 将一个给定的文件描述符加入集合之中 FD_SET(int,fd_set *), 将一个给定的文件描述符从集合中删除 FD_CLR(int,fd_set*), 检查集合中指定的文件描述符是否可以读写 FD_ISSET(int,fd_set* ) 一会儿举例说明 第二,struct timeval 是一个大家常用的结构, 用来代表时间值, 有两个成员, 一个是秒数, 另一个是毫秒数 具体解释 select 的参数 : int maxfdp 是一个整数值, 是指集合中所有文件描述符的范围, 即所有文件描述符的最大值加 1, 不能错! 在 Windows 中这个参数的值无所谓, 可以设置不正确
fd_set *readfds 是指向 fd_set 结构的指针, 这个集合中应该包括文件描述符, 我们是要监视这些文件描述符的读变化的, 即我们关心是否可以从这些文件中读取数据了, 如果这个集合中有一个文件可读,select 就会返回一个大于 0 的值, 表示有文件可读, 如果没有可读的文件, 则根据 timeout 参数再判断是否超时, 若超出 timeout 的时间,select 返回 0, 若发生错误返回负值 可以传入 NULL 值, 表示不关心任何文件的读变化 fd_set *writefds 是指向 fd_set 结构的指针, 这个集合中应该包括文件描述符, 我们是要监视这些文件描述符的写变化的, 即我们关心是否可以向这些文件中写入数据了, 如果这个集合中有一个文件可写,select 就会返回一个大于 0 的值, 表示有文件可写, 如果没有可写的文件, 则根据 timeout 参数再判断是否超时, 若超出 timeout 的时间,select 返回 0, 若发生错误返回负值 可以传入 NULL 值, 表示不关心任何文件的写变化 fd_set *errorfds 同上面两个参数的意图, 用来监视文件错误异常 struct timeval* timeout 是 select 的超时时间, 这个参数至关重要, 它可以使 select 处于三种状态, 第一, 若将 NULL 以形参传入, 即不传入时间结构, 就是将 select 置于阻塞状态, 一定等到监视文件描述符集合中某个文件描述符发生变化为止 ; 第二, 若将时间值设为 0 秒 0 毫秒, 就变成一个纯粹的非阻塞函数, 不管文件描述符是否有变化, 都立刻返回继续执行, 文件无变化返回 0, 有变化返回一个正值 ; 第三,timeout 的值大于 0, 这就是等待的超时时间, 即 select 在 timeout 时间内阻塞, 超时时间之内有事件到来就返回了, 否则在超时后不管怎样一定返回, 返回值同上述 返回值 : 负值 :select 错误正值 : 某些文件可读写或出错 0: 等待超时, 没有可读写或错误的文件 在有了 select 后可以写出像样的网络程序来! 举个简单的例子, 就是从网络上接受数据写入一个文件中 例子 : main() int sock; FILE *fp; struct fd_set fds; struct timeval timeout=3,0}; //select 等待 3 秒,3 秒轮询, 要非阻塞就置 0
char buffer[256]=0}; //256 字节的接收缓冲区 /* 假定已经建立 UDP 连接, 具体过程不写, 简单, 当然 TCP 也同理, 主机 ip 和 port 都已经给定, 要写的文件已经打开 sock=socket(...); bind(...); fp=fopen(...); */ while(1) FD_ZERO(&fds); // 每次循环都要清空集合, 否则不能检测描述符变化 FD_SET(sock,&fds); // 添加描述符 FD_SET(fp,&fds); // 同上 maxfdp=sock>fp?sock+1:fp+1; // 描述符最大值加 1 switch(select(maxfdp,&fds,&fds,null,&timeout)) //select 使用 case -1: exit(-1);break; //select 错误, 退出程序 case 0:break; // 再次轮询 default: if(fd_isset(sock,&fds)) // 测试 sock 是否可读, 即是否网络上有数据 recvfrom(sock,buffer,256,...);// 接受网络数据 if(fd_isset(fp,&fds)) // 测试文件是否可写 fwrite(fp,buffer...);// 写入文件 buffer 清空 ;
}// end if break; }// end switch }//end while }//end main select() 的机制中提供一 fd_set 的数据结构, 实际上是一 long 类型的数组, 每一个数组元素都能与一打开的文件句柄 ( 不管是 Socket 句柄, 还是其他文件或命名管道或设备句柄 ) 建立联系, 建立联系的工作由程序员完成, 当调用 select() 时, 由内核根据 IO 状态修改 fd_set 的内容, 由此来通知执行了 select() 的进程哪一 Socket 或文件可读, 下面具体解释 : #include <sys/types.h> #include <sys/times.h> #include <sys/select.h> int select(nfds, readfds, writefds, exceptfds, timeout) int nfds; fd_set *readfds, *writefds, *exceptfds; struct timeval *timeout; ndfs:select 监视的文件句柄数, 视进程中打开的文件数而定, 一般设为呢要监视各文件中的最大文件号加一 readfds:select 监视的可读文件句柄集合 writefds: select 监视的可写文件句柄集合 exceptfds:select 监视的异常文件句柄集合 timeout: 本次 select() 的超时结束时间 ( 见 /usr/sys/select.h, 可精确至百万分之一秒!) 当 readfds 或 writefds 中映象的文件可读或可写或超时, 本次 select() 就结束返回 程序员利用一组系统提供的宏在 select() 结束时便可判断哪一文件可读或可写 对 Socket 编程特别有用的就是 readfds 几只相关的宏解释如下 : FD_ZERO(fd_set *fdset): 清空 fdset 与所有文件句柄的联系 FD_SET(int fd, fd_set *fdset): 建立文件句柄 fd 与 fdset 的联系 FD_CLR(int fd, fd_set *fdset): 清除文件句柄 fd 与 fdset 的联系 FD_ISSET(int fd, fdset *fdset): 检查 fdset 联系的文件句柄 fd 是否可读写,>0 表示可读写 ( 关于 fd_set 及相关宏的定义见 /usr/include/sys/types.h)
这样, 你的 socket 只需在有东东读的时候才读入, 大致如下 :... int sockfd; fd_set fdr; struct timeval timeout =..;... for(;;) FD_ZERO(&fdR); FD_SET(sockfd, &fdr); switch (select(sockfd + 1, &fdr, NULL, &timeout)) case -1: error handled by u; case 0: timeout hanled by u; default: if (FD_ISSET(sockfd)) now u read or recv something; /* if sockfd is father and server socket, u can now accept() */ } } } 所以一个 FD_ISSET(sockfd) 就相当通知了 sockfd 可读 至于 struct timeval 在此的功能, 请 man select 不同的 timeval 设置使使 select() 表现出超时结束 无超时阻塞和轮询三种特性 由于 timeval 可精确至百万分之一秒, 所以 Windows 的 SetTimer() 根本不算什么 你可以用 select() 做一个超级时钟 FD_ACCEPT 的实现? 依然如上, 因为客户方 socket 请求连接时, 会发送连接请求报文, 此时 select() 当然会结束,FD_ISSET(sockfd) 当然大于零, 因为有报文可读嘛! 至于这方面的应用, 主要在于服务方的父 Socket, 你若不喜欢主动 accept(), 可改为如上机制来 accept() 至于 FD_CLOSE 的实现及处理, 颇费了一堆 cpu 处理时间, 未完待续 -- 讨论关于利用 select() 检测对方 Socket 关闭的问题 : 仍然是本地 Socket 有东东可读, 因为对方 Socket 关闭时, 会发一个关闭连接通知报文, 会马上被 select() 检测到的 关于 TCP 的连接 ( 三次握手 ) 和关闭 ( 二次握手 ) 机制, 敬请参考有关 TCP/IP 的书籍
不知是什么原因,UNIX 好象没有提供通知进程关于 Socket 或 Pipe 对方关闭的信号, 也可能是 cpu 所知有限 总之, 当对方关闭, 一执行 recv() 或 read(), 马上回返回 -1, 此时全局变量 errno 的值是 115, 相应的 sys_errlist[errno] 为 "Connect refused"( 请参考 /usr/include/sys/errno.h) 所以, 在上篇的 for(;;)...select() 程序块中, 当有东西可读时, 一定要检查 recv() 或 read() 的返回值, 返回 -1 时要作出关断本地 Socket 的处理, 否则 select() 会一直认为有东西读, 其结果曾几令 cpu 伤心欲断针脚 不信你可以试试 : 不检查 recv() 返回结果, 且将收到的东东 ( 实际没收到 ) 写至标准输出... 在有名管道的编程中也有类似问题出现 具体处理详见拙作 : 发布一个有用的 Socket 客户方原码 至于主动写 Socket 时对方突然关闭的处理则可以简单地捕捉信号 SIGPIPE 并作出相应关断本地 Socket 等等的处理 SIGPIPE 的解释是 : 写入无读者方的管道 在此不作赘述, 请详 man signal 以上是 cpu 在作 tcp/ip 数据传输实验积累的经验, 若有错漏, 请狂炮击之 唉, 昨天在 hacker 区被一帮孙子轰得差点儿没短路 ren cpu( 奔腾的心 ) z80 补充关于 select 在异步 ( 非阻塞 )connect 中的应用, 刚开始搞 socket 编程的时候我一直都用阻塞式的 connect, 非阻塞 connect 的问题是由于当时搞 proxy scan 而提出的呵呵通过在网上与网友们的交流及查找相关 FAQ, 总算知道了怎么解决这一问题. 同样用 select 可以很好地解决这一问题. 大致过程是这样的 : 1. 将打开的 socket 设为非阻塞的, 可以用 fcntl(socket, F_SETFL, O_NDELAY) 完成 ( 有的系统用 FNEDLAY 也可 ). 2. 发 connect 调用, 这时返回 -1, 但是 errno 被设为 EINPROGRESS, 意即 connect 仍旧在进行还没有完成. 3. 将打开的 socket 设进被监视的可写 ( 注意不是可读 ) 文件集合用 select 进行监视, 如果可写, 用 getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int)); 来得到 error 的值, 如果为零, 则 connect 成功. 在许多 unix 版本的 proxyscan 程序你都可以看到类似的过程, 另外在 solaris 精华区 -> 编程技巧中有一个通用的带超时参数的 connect 模块.