您的位置:首页 > 其它

并发服务器--02(基于I/O复用——运用Select函数)

2015-07-09 21:52 399 查看

I/O模型

  Unix/Linux下有5中可用的I/O模型:

阻塞式I/O

非阻塞式I/O

I/O复用(select、poll、epoll和pselect)

信号驱动式I/O(SIGIO)

异步I/O(POSIX的aio_系列的函数)

  关于这五种详细介绍可参考《UNIX网络编程 卷1》或网上博文

I/O复用

概念

  I/O多路复用(I/O Multiplexing)是指内核一旦发现进程指定的一个或者多个IO条件就绪(也就是说输入已准备好被读取,或者描述符已能承接更多的输出),它就通知该进程的能力。

  I/O多路复用适用如下场合:

  (1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。

  (2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。

  (3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

  (4)如果一个服务器既要处理TCP,又要处理UDP,一般要使用I/O复用。

  (5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

  与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

描述符就绪条件

  1. 满足下列四个条件中的任何一个时,一个套接字准备好读:

  1)该套接字接收缓冲区中的数据字节数大于等于套接字接收缓存区低水位标记的当前大小。对于TCP和UDP套接字而言,缓冲区低水位的值默认为1。那就意味着,默认情况下,只要缓冲区中有数据,那就是可读的。我们可以通过使用SO_RCVLOWAT套接字选项(参见setsockopt函数)来设置该套接字的低水位大小。此种描述符就绪(可读)的情况下,当我们使用read/recv等对该套接字执行读操作的时候,套接字不会阻塞,而是成功返回一个大于0的值(即可读数据的大小)。

  2)该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作,将不会阻塞,而是返回0(也就是EOF)。

  3)该套接字是一个listen的监听套接字,并且目前已经完成的连接数不为0。对这样的套接字进行accept操作通常不会阻塞。

  4)有一个错误套接字待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

  2. 满足下列四个条件中的任何一个时,一个套接字准备好写:

  1)该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓存区低水位标记时,并且该套接字已经成功连接(UDP套接字不需要连接)。对于TCP和UDP而言,这个低水位的值默认为2048,而套接字默认的发送缓冲区大小是8K,这就意味着一般一个套接字连接成功后,就是处于可写状态的。我们可以通过SO_SNDLOWAT套接字选项(参见setsockopt函数)来设置这个低水位。此种情况下,我们设置该套接字为非阻塞,对该套接字进行写操作(如write、send等),将不阻塞,并返回一个正值(例如由传输层接受的字节数,即发送的数据大小)。

  2)该连接的写半部关闭。对这样的套接字的写操作将会产生SIGPIPE信号。所以我们的网络程序基本都要自定义处理SIGPIPE信号。因为SIGPIPE信号的默认处理方式是程序退出。

  3)使用非阻塞的connect套接字已建立连接,或者connect已经以失败告终。

  4)有一个错误的套接字待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

  

  3. 如果一个套接字存在带外数据(out-of-band data)或者仍处于带外标记,那么它有异常条件待处理。 

  带外数据有时也称为经加速数据(expedited data)。其想法是一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端。这里的“迅速”意味着这种通知应该在已经排队等待发送的任何“普通”(有时称为带内)数据之前发送。也就是说,带外数据被认为具有比普通数据更高的优先级。带外数据并不要求在客户和服务器之间再使用一个连接,而是被映射到已有的连接中。

  UDP套接字不存在带外数据。TCP并没有真正的带外数据,不过提供了紧急模式(urgent mode)(TCP数据包头部的紧急指针)。

  注意:当某个套接字上发生错误时,它将由select标记为既可写又可读。

  关于带外数据更详细的请参考《UNIX网络编程 卷1》第24章。

  接收低水位标记和发送低水位标记的目的在于:允许应用进程控制在select返回可读或可写条件之前有多少数据可读或有多大空间可用于写。举例来说,如果我们知道除非至少存在64个字节的数据,否则我们的应用进程没有任何有效工作可做,那么可以把接收低水位标记设置为64,以防少于64个字节的数据准备好读时select唤醒我们。

  任何UDP套接字只要发送低水位标记小于等于发送缓冲区大小(默认应该总是这种关系)就总是可写的,这是因为UDP套接字不需要连接。

  

  下图汇总了导致select返回某个套接字就绪的条件

  

#include "unp.h"

void
str_cli_select(FILE *fp, int sockfd);

int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;

if(argc != 2)
err_quit("usage: tcpcli <IPaddress>");

sockfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr.s_addr);

Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

str_cli_select(stdin, sockfd);

exit(0);
}

void
str_cli_select(FILE *fp, int sockfd)
{
int maxfdp, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;

stdineof = 0;
FD_ZERO(&rset);
for(;;)
{
if(stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp = max(fileno(fp), sockfd) + 1;
Select(maxfdp, &rset, NULL, NULL, NULL);

if(FD_ISSET(sockfd, &rset))    /* socket is readable */
{
if((n == Read(sockfd, buf, MAXLINE)) == 0)
{
if(stdineof == 1)
return;    /* normal termination */
else
err_quit("str_cli_select: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}

if(FD_ISSET(fileno(fp), &rset))    /* input is readable */
{
if((n = Read(fileno(fp), buf, MAXLINE)) == 0)
{
stdineof = 1;
Shutdown(sockfd, SHUT_WR);    /* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}


View Code

运用Select函数存在的问题

  这段摘自博文select、poll、epoll之间的区别总结[整理]

  select的调用过程如下所示:

  



  (图片链接

  1)使用copy_from_user从用户空间拷贝fd_set到内核空间

  2)注册回调函数__pollwait

  3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

  4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

  5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

  6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

  7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

  8)把fd_set从内核空间拷贝到用户空间。

  select的几大缺点:

  1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

  2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

  3)select支持的文件描述符数量太小了,默认是1024

参考资料

  《UNIX网络编程 卷1》

  Linux下的I/O复用

  select、poll、epoll之间的区别总结[整理]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: