首页 > 代码库 > 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操作何时完成(是一种等待模式的完全解放)