首页 > 代码库 > Linux Network IO Model Learning
Linux Network IO Model Learning
目录
0. 引言1. IO机制简介2. 阻塞式IO模型(blocking IO model)3. 非阻塞式IO模型(noblocking IO model)4. IO复用式IO模型(IO multiplexing model)5. 信号驱动式IO模型(signal-driven IO model)6. 异步IO式IO模型(asynchronous IO model)7. Linux下IO技术简介8. IO模型编程举例
0. 引言
Linux将所有外部设备都看做一个文件来进行操作。因此,linux对所有外部设备(包括实体设备、以及虚拟设备)的操作都可以看做是文件的操作。文件的操作当然需要有个标示描述它,这就是文件描述符(file descriptor)
而对文件的操作,本质上就是IO的操作,本问将重点讨论IO操作中的网络IO(Network IO)操作,在开始学习之前,我们需要明白一点,不管是对于linux还是window来说,都存在模式和技术方案的关系
1. IO模式所谓是一种理论模型,是一种思想2. IO技术(机制)指linux下具体的IO通信技术
1. IO机制简介
0x1: IO系统的分层
1. File System(文件系统)解决了空间管理的问题,即 1) 数据如何存放 2) 数据如何读取2. Buffer Cache解决数据缓冲的问题 1) 对读,进行cache 缓存经常要用到的数据 2) 对写,进行buffer 缓冲一定数据以后,一次性进行写入3. Vol MgmtVM(Vol Mgmt)其实跟IO没有必然联系。他是处于文件系统和磁盘(存储)中间的一层。VM屏蔽了底层磁盘对上层文件系统的影响。当没有VM的时候,文件系统直接使用存储上的地址空间,因此文件系统直接受限于物理硬盘,这时如果
发生磁盘空间不足的情况,对应用而言将是一场噩梦,不得不新增硬盘,然后重新进行数据复制。而VM则可以实现动态扩展,而对文件系统没有影响。另外,VM也可以把多个磁盘合并成一个磁盘,对文件系统呈现统一的地址空间 /*数据实际存储数据最终会放在这里,因此,效率、数据安全、容灾是这里需要考虑的问题。而提高存储的性能,则可以直接提高物理IO的性能*/4. Device Driver5. IO Channel6. Disk Device
值得主要的是:
//逻辑IO和物理IO不是一一对应的1. Logical IO 逻辑IO是操作系统发起的IO,这个数据可能会放在磁盘上,也可能会放在内存(文件系统的Cache)里2. Physical IO物理IO是设备驱动发起的IO,这个数据最终会落在磁盘上
0x2: IO请求的两个阶段
1. 等待资源阶段(排队)IO请求一般需要请求特殊的资源(如磁盘、RAM、文件),当资源被上一个使用者使用没有被释放时,IO请求就会被阻塞,直到能够使用这个资源(阻塞条件解除)在等待数据阶段,IO分为阻塞IO和非阻塞IO 1.1 阻塞IO 资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时) 1.2 非阻塞IO(返回失败) 资源不可用时,IO请求离开返回,返回数据标识资源不可用 1.3 非阻塞IO(异步回调) 资源不可用时,IO请求离开返回,返回数据标识资源不可用,但使用注册回调机制,当资源可用时由资源方主动向资源请求方发出通知信号(表明资源可用)2. 使用资源阶段(服务)真正进行数据接收和发生在使用资源阶段,IO分为同步IO和异步IO 2.1 同步IO 应用阻塞在发送或接收数据的状态,直到数据成功传输或返回失败 2. 异步IO 应用发送或接收数据后立刻返回,数据写入OS缓存,由OS完成数据发送或接收,并返回成功或失败的信息给应用
0x3: Unix/Linux下的的5个IO模型
1. 阻塞式IO模型(blocking IO model) 2. 非阻塞式IO模型(noblocking IO model)3. IO复用式IO模型(IO multiplexing model)4. 信号驱动式IO模型(signal-driven IO model) 5. 异步IO式IO模型(asynchronous IO model)
在我们详细学习这5种IO模型之前,我们先来对它们的核心概念进行一下区分
0x4: 5个IO模型的区别
值得注意的是,所谓"同步和异步"的说法,更注重的是通信上的问题(数据描述符缓存的读写)、而"阻塞和非阻塞",更注重的是整个执行流的角度(即请求方需要等待被请求方执行完毕之后才能继续)
1. 同步异步标准是数据描述符缓存是由谁来进行读取的 1) 由用户程序读取: 则判断为同步 2) 由内核推送: 判断为异步2. 阻塞非阻塞标准是调用的用户进程是否是阻塞的状态 1) 资源请求方的用户进程处于阻塞状态(发出调用后阻塞等待): 阻塞式IO 2) 资源请求方的用户进程处于非阻塞状态(发出调用后立即返回): 非阻塞式IO
也就是说,本质上来说,"同步异步"和"阻塞非阻塞"是可以两种独立讨论的概念,并不是说异步就一定是非阻塞,它们的评判角度是不同的
0x5: 各种IO模型的特点
1. 阻塞IO使用简单,但随之而来的问题就是会形成阻塞,需要独立线程配合,而这些线程在大多数时候都是没有进行运算的。Java的BIO使用这种方式,问题带来的问题很明显,一个Socket需要一个独立的线程,因此,会造成线程膨胀2. 非阻塞IO采用轮询方式,不会形成线程的阻塞。Java的NIO使用这种方式,对比BIO的优势很明显,可以使用一个线程进行所有Socket的监听(select),大大减少了线程数。 3. 同步IO同步IO保证一个IO操作结束之后才会返回,因此同步IO效率会低一些,但是对应用来说,编程方式会简单。Java的BIO和NIO都是使用这种方式进行数据处理4. 异步IO由于异步IO请求只是写入了缓存,从缓存到硬盘是否成功不可知,因此异步IO相当于把一个IO拆成了两部分,一是发起请求,二是获取处理结果。因此,对应用来说增加了复杂性。但是异步IO的性能是所有很好的,而且异步的思想贯穿了
IT系统方方面面
Relevant Link:
http://pengjiaheng.iteye.com/blog/847588http://pengjiaheng.iteye.com/blog/847615unix网络编程 (1).pdf 6.2节http://www.cnblogs.com/yjf512/archive/2012/05/29/2523692.htmlhttp://www.360doc.com/content/12/0426/15/507289_206688978.shtmlhttp://blog.chinaunix.net/uid-26000296-id-4100620.html
2. 阻塞式IO模型(blocking IO model)
1. 首先application调用recvfrom()转入kernel,注意kernel有2个过程 1) wait for data 2) copy data from kernel to user2. 直到最后copy complete后3. recvfrom()才返回4. 此过程一直是阻塞的
Relevant Link:
http://blog.csdn.net/shallwake/article/details/5265287
3. 非阻塞式IO模型(noblocking IO model)
我们说过,阻塞和非阻塞的区别在于程序流的"连续性",从"阻塞IO"和"非阻塞IO"的图中可以看出来,在系统调用这个环节,非阻塞IO比阻塞IO"连贯",但是就数据通信这个环节,它们都还是同步的,即数据通信是"不连贯"的
可以看出,在非阻塞IO模式下,recvfrom()的系统调用会以轮询的方式进行,每次用户询问内核是否有数据报准备好(文件描述符缓冲区是否就绪),直到数据报准备好的时候(内核缓冲区有数据),就进行拷贝数据报的操作。当数据报没有准备好的时候,也不阻塞程序,内核直接返回未准备就绪的信号,等待用户程序的下一次轮询。
(从这个例子思考阻塞和同步的区别)
Relevant Link:
http://blog.csdn.net/shallwake/article/details/5265287
4. IO复用式IO模型(IO multiplexing model)
IO复用模型是多了一个select函数,select函数有一个参数是文件描述符集合,意思就是对这些的文件描述符进行循环监听,select这是处于阻塞状态,当某个文件描述符就绪的时候(有活动套接字),就对这个文件描述符进行处理。与blocking I/O相比,select会有两次系统调用,但是select能处理多个套接字。所以依然属于阻塞同步IO。但是由于它可以对多个文件描述符进行阻塞监听,所以它的效率比阻塞IO模型高效。
5. 信号驱动式IO模型(signal-driven IO model)
信号驱动IO模型是应用进程告诉内核:当你的数据报准备好的时候,给我发送一个信号哈,并且调用我的信号处理函数来获取数据报。这个模型是由信号进行驱动
与I/O multiplexing (select and poll)相比,它的优势是,免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler处理,但是copy date过程依然是阻塞的,所以属于非阻塞同步IO
6. 异步IO式IO模型(asynchronous IO model)
很少有*nix系统支持,windows的IOCP则是此模型(这里不讨论windows)
当应用程序调用aio_read的时候,内核一方面去取数据报内容返回,另外一方面将程序控制权还给应用进程,应用进程继续处理其他事务。这样应用进程就是一种非阻塞的状态。
当内核的数据报就绪的时候,是由内核将数据报拷贝到应用进程中,返回给aio_read中定义好的函数处理程序。所以这是一种阻塞异步IO模型
Relevant Link:
http://www.cnblogs.com/yjf512/archive/2012/05/31/2527966.htmlhttp://www.blogjava.net/lihao336/archive/2009/12/27/307430.html
7. Linux下IO技术简介
0x1: socket read()流程
我们说网络socket的read()是一个IO操作命令,具体流程是这样的:
1. 应用程序调用read命令,通知内核需要做读取数据操作2. 内核创建一个文件描述符3. 内核从物理层收到读数据的命令,从网络中获取数据包4. 数据包传递到TCP/IP层,解析数据包的头5. 内核将数据包缓存在文件描述符的读缓存区(接受缓存区)中,注意这里的读缓存区是在内核中的6. 当文件描述符读缓存区数据字节数大于应用程序定义的低水位(阈值)的时候(read的一个参数),此时文件描述符处于读就绪的状态7. 将读缓存区中的数据复制到应用程序(用户区)返回
值得注意的是
1. 每个文件描述符都有自己的读缓冲区和写缓冲区,读缓冲区对应的是read操作,写缓冲区对应的就是write操作了2. 读缓冲区和写缓冲区都是在内核区中
0x2: Linux Poll技术
poll IO技术属于IO复用模型(同步阻塞),
0x3: Linux epoll技术
I/O multiplexing
signal driven I/O(callback特性)
0x4: Kqueue技术
I/O multiplexing
0x4: Linux select技术
I/O multiplexing
0x5: javaScript、nodejs中的读取网络(文件)数据
javaScript或者nodejs中的读取网络(文件)数据,然后提供回调函数进行处理,是异步阻塞IO
0x6: IOCP
asynchronous I/O
windows下的IO技术,本文暂不讨论
7. IO模型编程举例
0x1: 同步阻塞模型
在这个模式中,用户空间的应用程序执行一个系统调用,并阻塞,直到系统调用完成为止(数据传输完成或发生错误)
Socket设置为阻塞模式,当socket不能立即完成I/O操作时,进程或线程进入等待状态,直到操作完成
#include <stdio.h>#include <stdlib.h>#include <sys/socket.h>#include <netdb.h>#include <string.h>#define SERVPORT 80#define MAXDATASIZE 100int main(int argc, char *argv[]){ int sockfd, recvbytes; char rcv_buf[MAXDATASIZE]; /*./client 127.0.0.1 hello */ char snd_buf[MAXDATASIZE]; struct hostent *host; /* struct hostent * { * char *h_name; // general hostname * char **h_aliases; // hostname‘s alias * int h_addrtype; // AF_INET * int h_length; * char **h_addr_list; * }; */ struct sockaddr_in server_addr; if (argc < 3) { printf("Usage:%s [ip address] [any string]\n", argv[0]); return 1; } *snd_buf = ‘\0‘; strcat(snd_buf, argv[2]); if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket:"); exit(1); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVPORT); inet_pton(AF_INET, argv[1], &server_addr.sin_addr); memset(&(server_addr.sin_zero), 0, 8); /* create the connection by socket * means that connect "sockfd" to "server_addr" * 同步阻塞模式 */ if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } /* 同步阻塞模式 */ if (send(sockfd, snd_buf, sizeof(snd_buf), 0) == -1) { perror("send:"); exit(1); } printf("send:%s\n", snd_buf); /* 同步阻塞模式 */ if ((recvbytes = recv(sockfd, rcv_buf, MAXDATASIZE, 0)) == -1) { perror("recv:"); exit(1); } rcv_buf[recvbytes] = ‘\0‘; printf("recv:%s\n", rcv_buf); close(sockfd); return 0;}
显然,代码中的connect,send,,recv都是同步阻塞工作模式,在结果没有返回时,程序什么也不做,这种模型非常经典,也被广泛使用
1. 优势在于非常简单,等待的过程中占用的系统资源微乎其微,程序调用返回时,必定可以拿到数据2. 缺点程序在数据到来并准备好以前,不能进行其他操作,需要有一个线程专门用于等待,这种代价对于需要处理大量连接的服务器而言,是很难接受的
0x2: 同步非阻塞模型
同步阻塞I/O的一种效率稍低的变种是同步非阻塞I/O,在这种模型中,系统调用是以非阻塞的形式打开的。这意味着I/O操作不会立即完成, 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN或EWOULDBLOCK),非阻塞的实现是I/O命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。因为数据在内核中变为可用到用户调用read返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低
#include <stdio.h>#include <stdlib.h>#include <sys/socket.h>#include <sys/types.h>#include <errno.h>#include <netdb.h>#include <string.h>#include <unistd.h>#include <fcntl.h>#define SERVPORT 80#define MAXDATASIZE 100int main(int argc, char *argv[]){ int sockfd, recvbytes; char rcv_buf[MAXDATASIZE]; /*./client 127.0.0.1 hello */ char snd_buf[MAXDATASIZE]; struct hostent *host; /* struct hostent * { * char *h_name; // general hostname * char **h_aliases; // hostname‘s alias * int h_addrtype; // AF_INET * int h_length; * char **h_addr_list; * }; */ struct sockaddr_in server_addr; int flags; int addr_len; if (argc < 3) { printf("Usage:%s [ip address] [any string]\n", argv[0]); return 1; } *snd_buf = ‘\0‘; strcat(snd_buf, argv[2]); if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket:"); exit(1); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVPORT); inet_pton(AF_INET, argv[1], &server_addr.sin_addr); memset(&(server_addr.sin_zero), 0, 8); addr_len = sizeof(struct sockaddr_in); /* Setting socket to nonblock */ flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, flags|O_NONBLOCK); /* create the connection by socket * means that connect "sockfd" to "server_addr" * 同步阻塞模式 */ if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } /* 同步非阻塞模式 */ while (send(sockfd, snd_buf, sizeof(snd_buf), MSG_DONTWAIT) == -1) { sleep(10); printf("sleep\n"); } printf("send:%s\n", snd_buf); /* 同步非阻塞模式 */ while ((recvbytes = recv(sockfd, rcv_buf, MAXDATASIZE, MSG_DONTWAIT)) == -1) { sleep(10); printf("sleep\n"); } rcv_buf[recvbytes] = ‘\0‘; printf("recv:%s\n", rcv_buf); close(sockfd); return 0;}
这种模式在没有数据可以接收时,可以进行其他的一些操作,比如有多个socket时,可以去查看其他socket有没有可以接收的数据
实际应用中,这种I/O模型的直接使用并不常见,因为它需要不停的查询,而这些查询大部分会是无必要的调用,白白浪费了系统资源
非阻塞I/O应该算是一个铺垫,为I/O复用和信号驱动奠定了非阻塞使用的基础
while(1){ 非阻塞read(设备1); if(设备1有数据到达) 处理数据; 非阻塞read(设备2); if(设备2有数据到达) 处理数据; ..............................}
0x3: I/O复用(异步阻塞)模式
在这种模型中,配置的是非阻塞I/O,然后使用阻塞select系统调用来确定一个I/O描述符何时有操作。对于select调用来说非常有趣的是它可以用来为多个描述符提供通知,而不仅仅为一个描述符提供通知。于每个提示符来说,我们可以请求这个描述符可以写数据、有读数据可用以及是否发生错误的通知。I/O复用模型能让一个或多个socket可读或可写准备好时,应用能被通知到
#include <stdio.h>#include <stdlib.h>#include <sys/socket.h>#include <sys/select.h>#include <sys/time.h>#include <netdb.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#define SERVPORT 80#define MAXDATASIZE 100#define TFILE "data_from_socket.txt"int main(int argc, char *argv[]){ int sockfd, recvbytes; char rcv_buf[MAXDATASIZE]; /*./client 127.0.0.1 hello */ char snd_buf[MAXDATASIZE]; struct hostent *host; /* struct hostent * { * char *h_name; // general hostname * char **h_aliases; // hostname‘s alias * int h_addrtype; // AF_INET * int h_length; * char **h_addr_list; * }; */ struct sockaddr_in server_addr; /* */ fd_set readset, writeset; int check_timeval = 1; struct timeval timeout={check_timeval,0}; //阻塞式select, 等待1秒,1秒轮询 int maxfd; int fp; int cir_count = 0; int ret; if (argc < 3) { printf("Usage:%s [ip address] [any string]\n", argv[0]); return 1; } *snd_buf = ‘\0‘; strcat(snd_buf, argv[2]); if ((fp = open(TFILE,O_WRONLY)) < 0) //不是用fopen { perror("fopen:"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket:"); exit(1); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVPORT); inet_pton(AF_INET, argv[1], &server_addr.sin_addr); memset(&(server_addr.sin_zero), 0, 8); /* create the connection by socket * means that connect "sockfd" to "server_addr" */ if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } /**/ if (send(sockfd, snd_buf, sizeof(snd_buf), 0) == -1) { perror("send:"); exit(1); } printf("send:%s\n", snd_buf); while (1) { FD_ZERO(&readset); //每次循环都要清空集合,否则不能检测描述符变化 FD_SET(sockfd, &readset); //添加描述符 FD_ZERO(&writeset); FD_SET(fp, &writeset); maxfd = sockfd > fp ? (sockfd+1) : (fp+1); //描述符最大值加1 ret = select(maxfd, &readset, NULL, NULL, NULL); // 阻塞模式 switch( ret) { case -1: exit(-1); break; case 0: break; default: if (FD_ISSET(sockfd, &readset)) //测试sock是否可读,即是否网络上有数据 { recvbytes = recv(sockfd, rcv_buf, MAXDATASIZE, MSG_DONTWAIT); rcv_buf[recvbytes] = ‘\0‘; printf("recv:%s\n", rcv_buf); if (FD_ISSET(fp, &writeset)) { write(fp, rcv_buf, strlen(rcv_buf)); // 不是用fwrite } goto end; } } cir_count++; printf("CNT : %d \n",cir_count); } end: close(fp); close(sockfd); return 0;} perl实现:#! /usr/bin/perl################################################################################ \File# tcp_client.pl# \Descript# send message to server###############################################################################use IO::Socket;use IO::Select; #hash to install IP Port%srv_info =(#"srv_ip" => "61.184.93.197","srv_ip" => "192.168.1.73","srv_port"=> "8080",);my $srv_addr = $srv_info{"srv_ip"};my $srv_port = $srv_info{"srv_port"};my $sock = IO::Socket::INET->new(PeerAddr => "$srv_addr",PeerPort => "$srv_port",Type => SOCK_STREAM,Blocking => 1,# Timeout => 5,Proto => "tcp")or die "Can not create socket connect. $@";$sock->send("Hello server!\n", 0) or warn "send failed: $!, $@";$sock->autoflush(1);my $sel = IO::Select->new($sock);while(my @ready = $sel->can_read){ foreach my $fh(@ready) { if($fh == $sock) { while() { print $_; } $sel->remove($fh); close $fh; } }}$sock->close();
用select来管理多个I/O,当没有数据时select阻塞,如果在超时时间内数据到来则select返回,再调用recv进行数据的复制,recv返回后处理数据
0x4: 信号驱动I/O模型
我们也可以用信号,让内核在描述字就绪时发送SIGIO信号通知我们
1. 首先开启套接口的信号驱动 I/O功能,并通过sigaction系统调用安装一个信号处理函数2. 该系统调用将立即返回,我们的进程继续工作,也就是说没被阻塞3. 当数据报准备好读取时,内核就为该进程产生一个SIGIO信号4. 我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报
服务端
#include <stdio.h> #include <sys/types.h>#include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <fcntl.h> int listenfd1; void do_sigio(int sig){ int clifd, clilen; struct sockaddr_in cli_addr; char buffer[256]; clifd = accept(listenfd1, (struct sockaddr *) &cli_addr, &clilen); bzero(buffer, 256); read(clifd, buffer, 255); printf("Listenfd1 Message%s\r\n", buffer);} int main(int argc, char *argv[]){ //绑定监听7779端口的fd struct sockaddr_in serv_addr1; listenfd1 = socket(AF_INET, SOCK_DGRAM, 0); bzero((char *) &serv_addr1, sizeof(serv_addr1)); serv_addr1.sin_family = AF_INET; serv_addr1.sin_port = htons(7779); serv_addr1.sin_addr.s_addr = INADDR_ANY; struct sigaction sigio_action; memset(&sigio_action, 0, sizeof(sigio_action)); sigio_action.sa_flags = 0; sigio_action.sa_handler = do_sigio; sigaction(SIGIO, &sigio_action, NULL); fcntl(listenfd1, F_SETOWN, getpid()); int flags; flags = fcntl(listenfd1, F_GETFL, 0); flags |= O_ASYNC | O_NONBLOCK; fcntl(listenfd1, F_SETFL, flags); bind(listenfd1, (struct sockaddr *) &serv_addr1, sizeof(serv_addr1)); while(1); close(listenfd1); return 0; }
客户端
//客户端 #include <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <string.h> #include <unistd.h> int main(int argc, char* argv[]){ int socketfd, n; socketfd = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in serv_addr; bzero((char *)&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(7779); connect(socketfd,(struct sockaddr *) &serv_addr, sizeof(serv_addr)); write(socketfd, "client message", 14); return 0; }
0x5: 异步非阻塞模式
linux下的asynchronous IO其实用得很少。与前面的信号驱动模型的主要区别在于
1. 信号驱动I/O是由内核通知我们何时可以启动一个I/O操作2. 异步I/O模型是由内核通知我们I/O操作何时完成(是一种等待模式的完全解放)