一、I/O模型
2、I/O 复用典型用于以下网络场合:
(1)当客户端同时处理多个套接字,这种情况很少出现。
(2)当客户端同时处理多个文件描述符(交互式输入和网络套接字)时,必须使用I/O复用。
(3)如果一个TCP服务器既要处理监听套接口,又要处理连接套接口,一般也用到I/O复用。
3、与多进程和多线程技术相比,I/O复用最大的优势在于系统开销小,系统不必创建进程或线程,同时也不必维护进程或者线程,从而大大减小了系统的开销。
#include<sys/select.h> #include<sys/time.h> int select(int maxfdp1,fd_set* readset, fd_set* writeset, fd_set* execepset, const struct timeval* timeout); //返回:返回值表示所有描述字集中已准备好的描述字个数。如定时到,则返回0;若出错,则返回-1。
在上面的参数中可以看到一个timeval结构,这个结构可以提供秒数和毫秒数成员,形式如下:
struct timeval { long tv_sec; /second*/ long tv_usec; /*microsecond*/ }
(1)设置为空指针NULL,永远等待下去,仅在有一个描述字准备好I/O时才返回。
(2)等待固定时间,再有一个描述字准备好I/O时返回。
(3)成员设置为0,根本不需要等待,检查描述字后立即返回,轮询。
3、满足下列四个条件之一的任何一个时,一个套接字准备好读:
(1)该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对于这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们使用SO_RECVLOWAT套接字选项设置套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1
(2)该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)
(3)该套接字时一个监听套接字且已完成的连接数不为0。
(1)该套接字发送缓冲区中的可用字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞的,写操作将不阻塞并返回一个正值(如由传输层接收的字节数)。我们使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP而言,默认值为2048
(2)该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号
(3)使用非阻塞式connect套接字已建立连接,或者connect已经已失败告终
(1)清空描述符集合;
(2)建立需要监视的描述符与描述符集合的联系;
(3)调用select()函数;
(4)检查所有需要监视的描述符,利用FD_ISSET宏判断是否已经准备好;
select(服务器代码)//同步非阻塞io(模型) 阻塞仅仅在select上 缺点每次使用都需要吧外部的fds拷贝到内核中浪费时间并且不知道具体是哪个fd有数据只知道有几个查找浪费效率(服务器代码)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <assert.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/select.h> #include <sys/time.h> #define MAXSIZE 10 int create_socket(); void fds_init(int fds[])//初始化fds数组 { int i = 0; for( ; i < MAXSIZE; i++ ) { fds[i] = -1; } } void fds_add(int fds[], int fd)//给数组中添加一个新的描述符 { int i = 0; for( ;i < MAXSIZE; i++ ) { if ( fds[i] == -1 ) { fds[i] = fd; break; } } } void fds_del(int fds[], int fd) { int i = 0; for( ; i < MAXSIZE; i++ ) { if ( fds[i] == fd ) { fds[i] = -1; break; } } } int main() { int sockfd = create_socket(); assert( sockfd != -1 ); int fds[MAXSIZE];//设置数组最大可放MAXSIZE个 fds_init(fds); //将数组全部置为-1 fds_add(fds,sockfd);//将监听描述符放到数组中 fd_set fdset;//创建多个文件描述符构成的集合用于存储文件描述符 while( 1 ) { FD_ZERO(&fdset);//清除所有位 int maxfd = -1;//巧妙地方用于存储当前最大的描述符的值 int i = 0; for( ; i < MAXSIZE; i++ ) { if ( fds[i] != -1 ) { FD_SET(fds[i],&fdset);//将数组中的文件描述符放到集合中 if ( maxfd < fds[i] ) { maxfd = fds[i];//最大的描述符用于存储 } } } struct timeval tv = {5,0};//设置等待时间 int n = select( maxfd + 1, &fdset,NULL,NULL,&tv);//获取第一个参数作为fd的最大描述符 if ( n == -1 ) { perror("select error"); continue; } else if ( n == 0 ) { printf("time out\n"); continue; } else { int i = 0; for( ; i < MAXSIZE; i++ ) { if ( fds[i] == -1 ) { continue; } if ( FD_ISSET(fds[i],&fdset) )//一个一个fd去判断是不是该fd有数据 {//用来测试位置是否被设置 if ( fds[i] == sockfd ) { struct sockaddr_in caddr; int len = sizeof(caddr); int c = accept(sockfd,(struct sockaddr*)&caddr,&len); if ( c < 0 ) { perror("accept error"); continue; } printf("accept c = %d\n",c);//创建一个新的描述符让下一步去判断 fds_add(fds,c);//把当前描述符放进数组中 } else { char buff[128] = {0}; int num = recv(fds[i],buff,1,0); if ( num <= 0 ) { close(fds[i]); fds_del(fds,fds[i]); printf("one client over\n"); continue; } printf("buff(%d)=%s\n",fds[i],buff); send(fds[i],"ok",2,0); } } } } } } int create_socket() { int sockfd = socket(AF_INET,SOCK_STREAM,0); if ( sockfd == -1 ) { return -1; } struct sockaddr_in saddr; memset(&saddr,0,sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_port = htons(6000); saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); if ( res == -1 ) { return -1; } listen(sockfd,5); return sockfd; }
8、 Select 模型特点
(1)可监控描述符的个数取决于sizeof(fd_set)的值。
(2)将fd加入select监控集的同时,还要用一个数据结构arr保存存放在select监控集中的fd。
(3)select需要在select前面循环arr(加fd,取maxfd)返回后循环arr(FD_ISSET是否有事件发生)
(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。
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024