五种I/O模型
程序数据的输入,输出称之为I/O。我们在前面学习的socket套接字阶段,从网络中读取和发送数据,就属于I/O操作。当网络中没有数据可读的时候,进程就会阻塞式的等待,直到有数据准备就绪。socket套接字默认的都是阻塞的方式。
阻塞式I/O
I/O操作分为两部,第一部分是等待,第二部分是进行数据的拷贝。阻塞式I/O的特点就是当没有是数据就绪的时候,程序一直等待底层有数据准备好,然后再进行拷贝。
非阻塞式I/O
阻塞式I/O的特点就是等待效率低下,程序无法进行推进,非阻塞式I/O可以在系统调用的时候,就算底层数据没有准备好,也会进行返回,并返回错误码EWOULDBLOCK,但是非阻塞式I/O需要程序员自己进行不断的轮询调用,对CPU资源来说是一种很大的浪费。
信号驱动I/O
信号驱动I/O的特点是注册SIGIO的处理函数,在底层有数据准备好的时候,内核就给进程发送信号,在信号处理函数中调用recvfrom区拷贝底层数据。
I/O多路转接
前面的I/O操作都是等待一个文件描述符的就绪状态,而I/O多路转接是可以同时等待多个文件描述符的就绪状态,只要一个就绪,那么就返回刻度条件,并且拷贝底层数据。
异步I/O
前面的四种I/O模型都是同步方式,异步I/O是拷贝工作由内核完成,拷贝完成再通知应用程序。
再谈阻塞与非阻塞,当一个文件描述符被打开的时候,它默认是阻塞的I/O,但是我们也可以设置文件描述符的属性,设置为非阻塞式I/O。
这里的cmd操作有5种功能,我们一般使用F_GETFL来获取文件描述符的标记,再使用F_SETFL设置文件描述符,设置的时候加上一个ONONBLOCK就可以把文件描述符设置为非阻塞式。
I/O多路转接之select
前面说过,select是I/O多路转接的接口,它可以同时等待多个文件述,只要有一个或者多个文件描述符的状态发生改变,就会返回,它只完成一件事情那就是等。
参数及返回值:
nfds:表示想要等待的最大文件描述符+1.
readfds,writefds,exceptfds:关心读的文件描述符集,关心写的文件描述符集,关心异常的文件描述符集。这里的fd_set类型表示文件描述符集。它可以理解为一张位图,每一个比特位的下标就表示那一个文件描述符,比特位的内容,1表示关心这个位置上的文件描述符事件。0表示不关心。同时,这三个参数都是输入输出型参数,输入表示关心那些文件描述符上上的那些事件,输出表示关心的文件描述符集上那些事件已经就绪。因此,在每一次调用select之前,都需要重新设置,我们要自己维护一个数组来保存关心的事件。fd_set类型必须用以下函数进行操作。
用来将文件描述符集的指定位清空,判断一个文件描述符是否在这个集合当中,设置一个文件描述符到文件描述符集中,把文件描述符集所有位清空。
timeval结构体:timeval用于描述一段事件,如果等待的文件描述符集在这一段时间之内没有事件发生,那么将超时,select将返回0。
select成功返回表示返回文件描述符改变的个数,返回0表示超时,返回负数代表出错。
一般情况下,我们关心文件描述符的读写情况,那么什么情况下读写条件就绪。
1.读就绪:
(1)在socket内核当中,当接受缓冲区的字节数大于等于低水位时候,这时候就可以非阻塞式的读。
(2)在TCP通信时候,对端关闭连接,此时对该socket读,返回0;
(3)监听套接字上有新的连接请求的时候。
(4)socket上有未处理的错误。
2.写就绪
(1)socket写操作被关闭,那么此时对该socket进行写,就会触发SIGPIPE信号。
(2)在socket内核中,当发送缓冲区字节数大小大于等于低水位的时候,此时可以无阻塞式的写,并且返回值大于0;
(3)socket使用非阻塞的connect连接成功的时候;
(4)socket上有未读取的错误。
最后还有一种方式就是异常就绪:比如在TCP报头中,就有一个紧急指针位,如果触发了紧急指针位,事件也会就绪。
socket的特点以及缺点:
socket可以同时等待多个文件描述符上的一个或者多个文件描述符事件就绪,并返回就绪条件,极大的提高了程序I/O的效率,一个进程能最大等待的文件描述符个数取决于fd_set的大小,不同平台是不一样的。它的缺点是我们要自己维护一个数组来保存想要等待的文件描述符集,并且每次都需要重新设置,而且每次调用select都需要把用户态的fds拷贝到内核,一旦fds大效率就很低,而且我们每次需要遍历的方式去查看寻找fds的最大值。而且select能支持的文件描述符有限制。
select服务器。
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/socket.h>
4 #include<sys/select.h>
5 #include<arpa/inet.h>
6 #include<netinet/in.h>
7 #include<stdlib.h>
8 #include<string.h>
9 int StartUp(int port)
10 {
11 struct sockaddr_in local;
12 local.sin_family=AF_INET;
13 local.sin_port=htons(port);
14 local.sin_addr.s_addr=INADDR_ANY;
15 int ret =socket(AF_INET,SOCK_STREAM,0);
16 if(ret<0)
17 {
18 perror("socket");
19 exit(2);
20 }
21 if(bind(ret,(struct sockaddr*)&local,sizeof(local))<0)
22 {
23 perror("bind");
24 exit(3);
25 }
26 int listen_sock=listen(ret,5);
27 if(listen_sock<0)
28 {
29 perror("listen");
30 exit(4);
31 }
32 return listen_sock;
33 }
34 void Init(int *array,int size)//全部填充为-1表示不关心
35 {
36 int i=0;
37 for( i=0;i<size;i++)
38 {
39
40 array[i]=-1;
41 }
42 }
43
44 void Reload(int listen_sock,int *array,int size,fd_set*r_fds,int *max_fd)
45 {
46 FD_ZERO(r_fds);
47 FD_SET(listen_sock,r_fds);
48 int max=listen_sock;
49 int i=0;
50 for( i=0;i<size;++i)
51 {
52 if(array[i]!=-1)
53 FD_SET(array[i],r_fds);
54 if(array[i]>max)
55 max=array[i];
56 }
57 *max_fd=max;
58
59 }
60 void ADD(int connect_sock,int *array,int size)
61 {
62 int i=0;
63 for( i=0;i<size;i++)
64 {
65
66 if(array[i]==-1)
67 {
68 array[i]=connect_sock;
69 break;
70 }
71 }
72 }
73 void ServerIo(int listen_sock,int* array,int size,fd_set*r_fds,int *max_fd)
74 {
75 if(FD_ISSET(listen_sock,r_fds))//监听套接字准备就绪,获取新连接
76 {
77 struct sockaddr_in client;
78 socklen_t len;
79 int connect_sock=accept(listen_sock,(struct sockaddr*)&client,&len);
80 if(connect_sock<0)
81 {
82 perror("accept");
83 return ;
84 }
85 printf("Get new connect,[%s]:[%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
86 ADD(connect_sock,array,size);
87 }
88 //处理新连接
89 int i=0;
90 for( i=0;i<size;i++)
91 {
92
93 if(array[i]=-1)
94 {
95 return ;
96 }
97 if(!FD_ISSET(array[i],r_fds))
98 return;
99 char buf[1024];
100 ssize_t s=read(array[i],buf,sizeof(buf)-1);
101 if(s<0)
102 {
103 perror("read");
104 return;
105 }
106 if(s==0)
107 {
108 printf("client say good bye\n");
109 close(array[i]);
110 array[i]=-1;
111 }
112 buf[s]=0;
113 printf("client say :%s\n",buf);
114 write(array[i],buf,strlen(buf));
115 }
116 }
117 int main(int argc, char*argv[])
118 {
119 if(argc!=2)
120 {
121
122 printf("Usage:%s:[port]\n",argv[0]);
123 return 1;
124 }
125 int listen_sock=StartUp(atoi(argv[1]));//创建监听套接字
126 fd_set r_fds;
127 int array[1024];//用数组保存我们关系的那些文件描述符中的那些事件。
128 Init(array,sizeof(array)/sizeof(array[0]));
129 for(;;)
130 {
131 int max_fd=listen_sock;
132 Reload(listen_sock,array,sizeof(array)/sizeof(array[0]),&r_fds,&max_fd);//每次将重置
133 int ret=select(max_fd+1,&r_fds,NULL,NULL,0);//r_fds输入输出型参数,输入表示关系那些文件描述符上的事件,输出表示有哪些文件描述符上面的事件以及就绪
134 switch(ret)
135 {
136 case -1:
137 perror("select");
138 exit(5);
139 break;
140 case 0:
141 printf("timeout...\n");
142 break;
143 default:
144 ServerIo(listen_sock,array,sizeof(array)/sizeof(array[0]),&r_fds,&max_fd);
145
146 }
147 }
148
149 return 0;
150 }
epoll
epoll和select一样,都是完成多路复用的一种方式,同时两者也很相似
。
fds是poll监听的一个列表,其中每一个元素是一个文件描述符信息。这里i传一个结构体数组,第二个参数nfds表示数组的长度,timeout位毫秒。函数返回值和selec一样。
下面来看看pollfd这个结构体:
结构体第一个fd表示关心那一个文件描述符,events表示关心这个文件描述符的什么事件,revents表示这个事情返回情况。
这是events和revents取值。
poll的优点:
poll相对于select最大的特点就是将关心的事件和返回的事件封装到一个结构体当中,不需要像select那样每次都要重新进行设置。并且poll并没有最大值的限制。
poll的缺点:
和select一样,每一次poll返回,都需要轮询的方式获取就绪事件的文件描述符。
并且pollfd这个结构体数组也需要我们手动维护,每一次都需要从用户态拷贝到内核态。
随着监视文件描述符的增长,效率还是会下降。