I/O复用:Select和Poll函数
1.I/O模型
a. Unix下共有五种I/O模型
阻塞I/O
非阻塞I/O
I/O复用(select和poll)
信号驱动I/O(SIGIO)
异步I/O(Posix.1的aio_系列函数)
b.阻塞I/O模型
应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。
如果数据没有准备好,一直等待。。。。
数据准备好了,从内核拷贝到用户空间
IO函数返回成功指示
c.非阻塞I/O模型
我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试 数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。
d. I/O复用模型
I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
e.信号驱动I/O模型
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
f.异步I/O模型
调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。
2.几种I/O模型的比较
前四种模型的区别是第一阶段基本相同,第二阶段基本相同,都是将数据从内核拷贝到调用者的缓冲区。而异步I/O的两个阶段都不同于前四个模型。
3.同步I/O和异步I/O
a.同步I/O操作引起请求进程阻塞,直到I/O操作完成。
异步I/O操作不引起请求进程阻塞。
b.我们的前四个模型都是同步I/O,只有最后一个异步I/O模型是异步I/O。
4.Select函数
a. Select函数可以指示内核等待多个事件中的任一个发生,并仅在任一个事件发生或经某个指定的时间后才返回,才唤醒进程
b. 可以调用select函数,通知内核在下列情况发生时才返回:
集合{1,4,5}中的任何描述符准备好读,或者
集合{2,7}中的任何描述符准备好写,或者
集合{1,4}中任何描述符有异常条件待处理,或者
已经经过了10.2秒
c.描述字可以不受限制与套接字,任意的描述符都可以用select来测试
d. 函数原型
int select(int maxfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct tim *timeout);
参数说明:
timeout:告诉内核等待任一描述符准备好可以花费的时间,这里会有三种情况,
第一种情况是timeout是NULL,这样将一直等待,直到某个描述符准备好;
第二种情况是timeout的值是0,那么将不等待,立即返回;
第三种情况是timeout中的秒或微秒被赋值,那么将等待指定的时间。
此外,如果进程收到一个信号,select也会被中断返回。
readfds,writefds和exceptfds指定了让内核测试读,写和异常条件所需的描述字。
maxfds:说明了被测试的描述符的个数,它的值是要被测试的最大的描述符加1.
返回值:所有描述符集的已准备好的总位数。返回时,描述符集中任何没有准备好的描述符都被清0,我们用FD_ISSET来测试是哪个描述符准备好了。因此,每次调用select时,都要重新将我们关心的描述符在描述符集中置为1.
e. fd_set说明
fd_set是一个整数数组,每个数中的每一位对应一个描述符。例如用32位表示一个整数,那么数组的第一个元素对应于描述字0~31,第二个元素对应于描述字32~63.
四个相关的宏:
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
f.描述符准备好读的条件:
套接口缓冲区中的数据字节大于等于套接口接收缓冲区低潮限度的当前值。套接口将不阻塞并返回一个大于0的值,就是当前准备好读的数据字节数。
套接口收到一个FIN,套接口的读操作将返回一个0.
套接口是一个监听套接口,并且以完成的连接数非0。
有一个套接口错误待处理。套接口的读操作将返回-1.
g.描述符准备好写的条件:
套接口发送缓冲区的可用空间大于等于套接口发送缓冲区低潮限度的当前值。,且或者套接口以连接,套接口不要求连接。
套接口写这一半关闭,这样将产生一个SIGPIPE错误。
有一个套接口错误待处理。
h.描述符异常的条件:
套接口存在带外数据
仍处于带外标记
i. 每个进程可以使用的最大描述符
有一个宏FD_SETSIZE定义了一个进程可以使用的最大描述符数。如果要更改这个值不仅仅要在定义的头文件中改变,还要重新编译内核。
5.使用select函数修改前面的客户-服务器程序
在前面的客户-服务器程序中,客户端采用的是停-等这样的策略来接收来自标准输入的用户输入,这样的好处是可以一对一的完成从用户输入,然后读取从服务器 返回的字符串,这样的弊端是当程序阻塞在等待用户输入时,无法及时的处理来自服务器的FIN等这些消息。现在我们用select函数将客户端程序做一些修 改,使能避免前面提到的问题。
另外,服务器也采用select函数,从而避免产生过多的进程,使用select后可以只有一个进程就可以处理多个客户端。在服务器端建立一个整数型的数 组,用来存放已经完成的客户端连接。每次从见天套接口读到数据后,我们将新的来自客户端的连接加入到这个数组中,并且修改maxfd的值。每次从客户端套 接口读到数据后,将读到的数据重新写回到客户端套接口。
a.服务器从客户套接口读到数据后,返回值有可能为0,这说明客户端已经关闭了写这个方向的连接。在将数据写入客户端套接口后,要将连接关闭。并将客户连接从存放客户连接的数组中移除。
b.采用上面的方案,存在一个潜在的问题就是可能受到拒绝服务的攻击。一个恶意用户和服务器建立连接,发送单个字符,但是没有发送换行符或者终止,这样服 务器将阻塞在read函数中。可能的解决办法是采用非阻塞I/O或者让每个客户用单独的进程来处理或者为I/O操作设置超时。
c.客户端接收到EOF时,只能关闭写这个方向的连接,因为我们仍然希望读取来自服务器的数据。此时是不能用close来关闭连接的,而要用 shutdown来关闭。如果套接字的访问计数大于0,那么close只是将计数减1;如果套接字的访问计数等于0,close将终止套接字的两个方向, 那样我们将不能读取仍然没有从服务器发送回来的数据。
6.shutdown函数
int shutdown(int s, int how);
参数说明:
s: 代表套接字描述字
how:SHUT_RD -- 关闭套接字的读取数据方向的连接
SHUT_WR -- 关闭套接字的写入数据方向的连接
SHUT_RDWR -- 关闭套接字双向的连接
7.pselect函数
int pselect(int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout, const
sigset_t *sigmask);
a.pselect函数采用timespec结构,这个结构支持纳秒
b.sigmask是信号掩码,将禁止递交某些信号
8.poll函数
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
a.参数说明
ufds: 是一个struct pollfd结构体的指针
nfds: 说明我们关心的描述字的个数
timeout: 超时等待的时间,单位是毫秒
b.struct pollfd结构体说明
struct pollfd {
int fd;
short events;
short revents;
};
fd: 是描述字
events: 是在描述字上关心的事件
revents: 是在描述字上返回的事件
poll函数返回后我们要测试revents中的事件是否是我们关心的。