在冯诺依曼体系结构下,输入输出是重要的两部分。当然,在网络中,输入输出也是重要的两环。通常,在网络通信的效率中,IO的比重占的很大。
通常情况下,IO分为两步,第一步是等,第二步是搬迁数据。搬迁数据的时间是一定的,但是,等的时间是可以改变的。我们要想提高IO的效率,就必须提高等的效率,即降低等的比重。
1. IO模型的分类
首先IO模型分为两大类,一类是同步IO,另一类是异步IO;
同步IO包括四类:阻塞IO,非阻塞IO,信号驱动IO,多路转接IO
(1)阻塞IO
在内核将数据准备好之前,系统调用会一直等待,即操作系统将进程挂起来。所有的套接字默认方式下都是阻塞方式。
(2)非阻塞IO
如果内核没有将数据准备好,系统调用仍然会直接返回去做别的事情,并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复去读写文件描述符,不断去检测是否有事件就绪。
(3)信号驱动IO
用户在代码中设置信号处理程序,在内核中有数据来时,内核发送SIGIO信号通知应用程序进行IO操作。
(4)多路转接IO
多路转接IO最本质的地方在于能同时等待多个文件描述符就绪。
异步IO
在内核中有数据来临时,直接将数据在内核拷贝,然后交给应用程序。
2. 认识几个函数
(1)fcntl 函数
函数原型:
fcntl函数的5种功能:
复制一个现有的描述符(cmd=F_DUPFD);
获得/设置文件描述符标记(cmd=FGETFD或FSETFD);
获得/设置文件状态标记(cmd=FGETFL或FSETFL);
获得/设置异步IO所有权(cmd=FGETOWN或FSETOWN);
获得/设置记录锁(cmd=FGETLK或FSETLKW);
(2)重定向dup/dup2函数
函数原型:利用重定向这个特点,可以改变文件描述符的指向及复制文件描述符。
注:dup分配文件描述符是从最小的开始分配。dup2是将新文件描述符拷贝到旧文件描述符上去。
3. I/O多路转接之select
(1)select函数
函数原型:
函数功能:监视多个文件描述符的状态变化,在IO中负责IO的第一步——等,等待事件的就绪。
函数参数:参数nfds表示要监视的文件描述符的最大值+1;
中间三个参数表示三个事件集,分别是读事件集,写事件集,异常事件集。
timeout的取值来表示select等的方式。
函数返回值:
执行成功返回文件描述符状态改变(事件就绪)的个数。
执行失败返回-1,错误原因存于错误码。
如果返回0,表示timeout时间一到,超时返回。
注意:使用select多路转接时,我们需要一个数组来保存我们所要关心的文件描述符,在调用select之前,需要对fd_set进行重新设置,依据就是数组。
(2)关于fd_set
fd_set是一个输入输出型参数,当输入参数时,表示我要告诉操作系统我所关心哪些文件描述符的哪个事件;当输出参数时,表示操作系统告诉我我所关心的文件描述符的什么事件就绪了。
关心哪个事件,就将对应的参数设置为事件集,如果不关心,直接置NULL。
fd_set底层是一个位图,此位图对应的下标表示文件描述符,内容表示是否关心此文件描述符。
fd_set的大小由系统决定,是有上限的,所以表示的文件描述符也是有上限的。
由于fd_set也是系统维护,所以要将文件描述符的状态设置进位图,需要系统调用:
(3)关于timeval
timeval结构用于设置时间长度,有两个数量级——秒和毫秒。
(4)关于timeout取值
timeout取值有三种:
NULL,表示select阻塞式的等待;
0,仅检测文件描述符的状态,然后立即返回,并不等待事件发生;
特定时间值,如果在时间段里没有事件发生,则超时返回。
4. 套接字socket就绪条件
读就绪
(1)socket内核中,接收缓冲区的字节数大于等于低水位标记(数据数量一定时),此时可以无阻塞的读,并且返回值大于0。
(2)socketTCP通信中,对端关闭连接,此时读,返回0.
(3) 监听socket上有新的连接请求。
(4) socket上有未处理的错误。
写就绪
(1) socket内核中,发送缓冲区的空闲位置大于等于低水位时,可以无阻塞的写,并且返回值大于0;
(2)socket的写操作被关闭,触发SIGPIPE信号。
(3)socket使用connect连接时。
(4)socket上有未读取的错误。
异常就绪
socket上收到带外数据。
5. select网络服务器的实现
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/socket.h>
4 #include <unistd.h>
5 #include <netinet/in.h>
6 #include <arpa/inet.h>
7 #include <stdlib.h>
8 #include <sys/select.h>
9 #include <string.h>
10
11 #define MAX_FD 1024
12 #define INIT_DATA -1
13 void Initfdarray(int fd_array[],int num) //数组初始化
14 {
15 int i=0;
16 for(i=0;i<num;i++)
17 {
18 fd_array[i]=INIT_DATA;
19 }
20 }
22 int startup(int port)//创建监听套接字
23 {
24 int sock=socket(AF_INET,SOCK_STREAM,0);
25 if(sock<0)
26 {
27 perror("socket");
28 exit(2);
29 }
30 int opt=1;
31 setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
32
33 struct sockaddr_in local;
34 local.sin_family=AF_INET;
35 local.sin_addr.s_addr=htonl(INADDR_ANY);
36 local.sin_port=htons(port);
37
38 if(bind(sock,(struct sockaddr*)&local,sizeof(struct sockaddr_in))<0)
39 {
40 perror("bind");
41 exit(3);
42 }
43 if(listen(sock,5)<0)
44 {
45 perror("listen");
46 exit(4);
47 }
48 return sock;
49 }
50 int AddsockToArray(int fd,int array[],int num) //将要关心的文件描述符保存在数组中
51 {
52 int i=0;
53 for(;i<num;i++)
54 {
55 if(array[i]==INIT_DATA)
56 {
57 array[i]=fd;
58 return i;
59 }
60 }
61 return -1; //full
62 }
63 int AddfdArrayTofdset(int array[],int num,fd_set* rfds) //将所要关心的文件描述符添加到fd_set里面
64 {
65 int i=0;
66 int Maxfd=INIT_DATA;
67 for( i=0;i<num;i++)
68 {
69 if(array[i]>INIT_DATA)
70 {
71 FD_SET(array[i],rfds);
72 if(Maxfd<array[i])
73 Maxfd=array[i];
74 }
75 }
76 return Maxfd;
77 }
78 void service_select(int arr[],int num,fd_set *rfds) //IO服务函数
79 {
80 int i=0;
81 for(i=0;i<num;i++)
82 {
83 if(arr[i]>INIT_DATA)
84 {
85 int fd=arr[i];
86 if(i==0&&FD_ISSET(arr[i],rfds)) //监听套接字绪
87 {
88 //listen_sock
89 struct sockaddr_in client; //建立连接
90 int size=sizeof(client);
91
92 int sock=accept(fd,(struct sockaddr*)&client,&size);
93 if(sock<0)
94 {
95 perror("accept");
96 continue;
97 }
98 printf("get a connect:%d",ntohs(client.sin_port));
99 int ret= AddsockToArray(sock,arr,num);
100 if(ret==-1)
101 close(sock);
102
103 }
104 else if(i!=0&&FD_ISSET(arr[i],rfds)) //其他套接字读写数据
105 {
106 //nomal fd
107
108 printf("please read!\n");
109 char buf[1024];
110 ssize_t s=read(fd,buf,sizeof(buf)-1);
111 if(s>0)
112 {
113 printf("please read!!\n");
114 buf[s]=0;
115 printf("client> %s\n",buf);
116 }
117 else if(s==0)
118 {
119 printf("read finish!");
120 close(fd);
121 arr[i]=INIT_DATA;
122 }
123 else
124 {
125 perror("read");
126 close(fd);
127 arr[i]=INIT_DATA;
128
129 }
130
131 }
132
133 else
134 {
135 //perror FD_ISSET
136 //do nothing
137 }
138
139
140 }
141
142 }
143 }
144 int main(int argc,char*argv[])
145 {
146 if(argc!=2){
147 printf("Usage:%s[port]\n",argv[0]);
148 return 1;
149 }
150 int listen_sock=startup(atoi(argv[1]));
151
152 int fd_list[MAX_FD];
153 int size=sizeof(fd_list)/sizeof(fd_list[0]);
154 Initfdarray(fd_list,size);
155 AddsockToArray(listen_sock,fd_list,size);
156
157 fd_set rfds;
158 for(;;){
159
160 FD_ZERO(&rfds);
161 int maxfd=AddfdArrayTofdset(fd_list,size,&rfds);
162 int ret=select(maxfd+1,&rfds,NULL,NULL,NULL);
163 switch(ret)
164 {
165 case -1:
166 {
167 perror("select");
168 break;
169 }
170 case 0:
171
172 {
173 printf("timeout");
174 break;
175 }
176
177 default:
178 {
179 service_select(fd_list,size,&rfds);
180 break;
181 }
182 }
183
184 }
6. 总结select的特点
(1)select上可监控的文件描述符个数取决于sizeof(fd_set)的大小,是有上限的。
(2)将要关心的fd加入到select监控集的同时,还需要一个数组来保存fd,并且数组需要自己维护。
一是用于select返回后,数组作为原数据和fd_set进行FD_ISSET 判断,看是否是自己关心的文件描述符或者是哪个。
二是select返回后,会把以前加入的但是没有事件就绪的fd清空,则每次开始select之前都要重新从array中取得fd加入到fd_set中。
7. select的优缺点
优点:
select的优点是相对于多线程多进程来讲的,效率较高。
缺点:
select的缺点是相对于poll和epoll来讲的:
(1)每次调用select,都需要手动设置fd集合,使用不便。
(2)每次调用select,都需要把fd集合从用户态拷贝到内核态,开销较大。
(3)每次调用select,都需要在内核遍历传递进去的fd,开销较大。
(4)select支持的文件描述符数量有上限。