本文将讨论网络编程中的高级I/O复用技术,将从下面几个方面进行展开:
a. 什么是复用技术呢?
b. 什么情况下需要使用复用技术呢?
c. I/O的复用技术的工作原理是什么?
d. select, poll and epoll的实现机制,以及他们之间的区别。
下面我们以一个背景问题来开始:
包括在以前的文章中我们讨论的案例都是阻塞式的I/O包括(fgetc/getc, fgets/gets),即当输入条件未满足时进程会阻塞直到满足之后进行读取,但是这样导致的一个
问题是如果此时进程还有别的I/O信息需要读取那么这些信息将会被进程忽略掉。如何解决这个问题呢?
可能这么说还是有点抽象,我们针对之前的回射程序举个例子。程序在:http://blog.csdn.net/michael_kong_nju/article/details/43457393
在正常连接的情况下,客户端阻塞于fgets函数,此时如果服务器终止我们发现客户端没有得到消息,仍然阻塞:
这个时候看下网络状态,发现服务器已经结束,而客户端处于CLOSE_WAIT状态。
而客户端此时得不到这个FIN的消息,一直阻塞。而这是我们不希望看到,那么这个问题该怎么解决呢?
可以从下面几个条件来考虑:
1.使用多进程或者多线程,让不同的线程或者进程阻塞在不同的描述符上。
但是这种方法会造成程序的复杂,而且进程和线程也需要OS资源的消耗,如果访问请求过大的话,那么很可能造成服务器的崩溃。Apache服务器是用的子进程的方式,
其中的优点是在于不同的线程服务于不同的用户可以隔离用户。
2.用一个进程,但是使用的是非阻塞的I/O读取数据,
当一个I/O不可读的时候立刻返回,检查下一个是否可读,这种形式的循环为轮询(polling),这种方法比较浪费CPU时间,因为大多数时间是不可读,但是仍花费时间不断反复执行read系统调用。
使用这种方法,进程不会阻塞,而是设置一个信号处理函数,当I/O条件满足时由内核通知进程进行数据读取。但是这也会有一个问题,如果请求很多的话,那么需要的信号也很多。
4. 使用异步I/O(asynchronous I/O)技术
和信号驱动式类似,异步I/O技术也是使用信号进行通知进程,但是不同的是这里只有一个阶段,即当内核完成i/o操作之后会通知进程而不是就绪的时候。
关于2,3,4是另外的几种高级I/O技术,我们将在后面的文章分别进行详细的讨论。
还有一种方法就是我们即将讨论的I/O多路复用技术,下面先回答第一个问题
I/O复用技术是一种预先告知内核此进程需要进行哪些I/O,并且当任何指定一个或多个I/O条件就绪时内核通知进程去进行处理的一种技术。他使得一个进程在不阻塞的
情况下处理多个描述符I/O.
针对上面的背景问题,我们可以回答我们开始的第二个疑问,
(1)当客户处理多个描述符时,即上面的这种情况,同时处理交互式输入和网络套接字。
(2)当客户需要处理多个套接字时。
(3)当服务器需要处理多个套接字时,即并发服务器模型。
(4)当服务器需要同时处理TCP和UDP等不同的传输协议时也需要使用多路复用技术。
上面大概是几种需要使用多路复用技术的场景,下面我们来讨论复用技术的实现原理。回答开头的第三个问题:
下面这幅图是复用技术的工作模型,可以看到这里是使用select来实现的,当然也可以用epoll和poll来实现只是其中的具体细节不一样罢了。
下面我们开始回答最后一个问题:
该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:
#include <sys/select.h> #include <sys/time.h> int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就绪描述符的数目,超时返回0,出错返回-1
函数参数介绍如下:
(1)第一个参数maxfdp1指定待测试的描述字个数,注意这里是个数是实际的最大的描述符加1,和数组下标类似从0到maxfdp 共maxfd + 1个,所以这里是maxfd plus 1. 描述字0、1、2...maxfdp1-1均将被测试。在linux中,头文件<sys/select.h>定义了最大的描述符是1024,所以这里最大的maxfdp1也就是1025,在互联网没有快速发展的时候这个值可能已经很大了,但是在现在看来很容易就会实现这么大的并发,所以select在很多条件下已经不能满足服务器的要求了,所以出现了epoll这种无限制的机制。
(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合 例如: fd_set rset; FD_ZERO(&set) ; 初始化,将所有位置0
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中 FD_SET(1, &rset); 1bit开启。
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写。
在select中集合是使用整数数组实现的,数组中的每一个位都是一个int可以表示32bit,即fd_set[0]可以用来表示0-31号描述符,下面依次。
(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
下面看看描述有哪些就绪条件;准备好读:
1,套接字接收缓冲区的数据字节数大于等于,套接字接收缓冲区低水位线,可以用SO_RCVLOWAT套接选项来设置低水位线,对于TCP和UDP套按字,默认值为1
2,该连接的读半部分关闭(接收到了FIN的TCP连接).对这样的套接字读操作,返回0(EOF)
3,该套接字是一个监听套接字且已经完成的连接数不为0.对这样的套按字的accept通常不会阻塞
4,其上有一个套接字错误街处理.对这样的套按字的读操作将不阻塞并返回-1(错误),同时把errno设置成错误条件,这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取.
准备好写:
1,该套接字发送缓冲区的可用字节数大于等于套接字发送缓冲区低水位线的当前大小.并且或者该套接已经连接,或者套按字不需要连接(UDP),如果我们把这套接字设置成非阻塞,写操作将不阻塞并返回一个正值.可以使用SO_SNDLOWAT设置一个该套接字的低水位标记.对于TCP和UDP默认值通常为2048.
2,该连接的写半部关闭.对这样的套接写的写操作将产生SIGPIPE信号.
3使用非阻塞式的connect的套按字已经建立连接,或者connect已经失败.
4,其上有一个套接字错误等处理,对这样的套接字进行写操作会返回-,且,把ERROR设置成错误条件,可以通过指定SO_ERROR套按选项调用getsockopt获取并清除.
上面都是理论的知识,我们现在来看一个例子,我们用select重写http://blog.csdn.net/michael_kong_nju/article/details/43457393 中的echo_tcp_client.c中的str_cli函数:
void str_cli(FILE *fp, int sockfd) { int maxfdp1; fd_set rset; char sendline[MAXLINE], recvline[MAXLINE]; FD_ZERO(&rset); for ( ; ; ) { FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1; select(maxfdp1, &rset, NULL, NULL, NULL); if (FD_ISSET(sockfd, &rset)) { /* socket is readable */ if (read(sockfd, recvline, MAXLINE) == 0) { perror("str_cli: server terminated prematurely"); exit(1); } fputs(recvline, stdout); } if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */ if (fgets(sendline, MAXLINE, fp) == NULL) return; /* all done */ write(sockfd, sendline, strlen(sendline)); } } }
限于篇幅的原因这里不给出客户端的main函数,但是我们建议你去我的github中下载这个我已经展开过的源码去调试运行一下:
https://github.com/michaelnju/UNPV-Relaxing-Code/blob/master/Chaper6_Select_Test/select_echo_tcp_cli.c
服务器程序还是用上一个连接中的。
这时候你会看到在客户端和服务器正常连接的过程中,如果这时候服务器断开了,那么客户端会立马被告知,而不像我们刚开始的时候那样会阻塞。
所以我们看到了select的作用。下篇文章我们将看到select在并发服务器中的作用。