首先谈一谈为什么要引入I/O复用这个概念。当我们必须从两个文件描述符中读取时,我们不能从任一个描述符上进行阻塞读,否则可能会因为被阻塞在一个文件描述符的读操作上而导致另一个描述符即使有数据也无法处理。当然我们也可以通过以下几种方法解决。下面进行简单介绍且指出各个方法的不足。
<1>我们可以通过fork进程的方法,用每个进程处理一个文件的读。但是操作的终止时间也是一个问题。如果子进程先读到EOF,那么子进程终止,然后有wait函数父进程可以接受到子进程终止的信号。但是如果父进程先于子进程死亡,那么父进程需要通过发送SIGUSR1信号通知子进程停止,这使得程序变得复杂。
<2>我们可以通过创建两个线程,这使得我们需要做线程同步工作。
<3>我们也可以使用一个进程完成该读写过程,使用非阻塞I/O读取数据。但大多数时候我们无数据可读。浪费CPU时间。
这就使得我们引入I/O复用,我将在我的博客里介绍select,poll和epoll模型。本文主要谈谈我对select理解。
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1;
nfds是我们监听的最大文件描述符加1,加1是保证我们监听到所有的文件描述符。
中间三个参数readfds,writefds,exceptfds是指向描述符集的指针。这三个描述符说明我们关心的可读,可写,异常的描述符集合。
最后一个参数是愿意等待的时间长度。
timeout == NULL,永远等待,直至捕捉到一个信号中断此无限期等待。当所指定的描述符中的一个已经准备好或者捕捉到一个信号则返回。
timeout->tv_sec == 0 || timeout->tv_usec == 0;根本不等待。测试所有指定的描述符并立即返回。可用于轮询不阻塞select函数方法。
timeout->tv_sec != 0 || timeout->tv_usec != 0;等待指定的秒数,如果在等待期间有准备好的描述符或者等待时间已到则返回。超时返回0。
下面展示一个select的例子。
(不知道linux里如何截长图,查也没查到合适的,有知道的还请指教)
fd_set结构体仅包含一个整型数组,该数组的每一位标记一个文件描述符。我们将介绍下列宏来访问结构体中的位:
FD_ZERO(fd_set *fdset); 清零所有位
FD_SET(int fd,fd_set *fdset); 设置fdset的位fd
FD_CLR(int fd,fd_set *fdset); 清除fdset的位fd
FD_ISSET(int fd,fd_set *fdset);测试fdset的fd位是否设置
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0010,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0110
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0110。注意:没有事件发生的fd=5被清空。
注:fd_set有128字节可以设置所有1024个文件描述符。
文件描述符的就绪条件
![]()
在了解select所需的基础知识后,接下来我们分析select的程序逻辑。
conn_num是客户端的个数,max_sock最大的文件描述符。second_sock第二大文件描述符,client_fd数组用来存客户端的文件描述符。
如果select返回值是大于0的判断是新的连接还是进行其他收发操作。如果sockSer在readset里面我们进行客户端与服务器的连接,否则我们进行轮询查找对应文件描述符进行数据收发。
如此循环我们每次把服务器的套结字和客户端的文件描述符重新置到readset中,利用select来监测。
如有错误之处,还请多多指教。