Hi,欢迎来到中国嵌入式培训高端品牌 - 华清远见嵌入式学院<北京总部官网>,专注嵌入式工程师培养13年!
当前位置: > 嵌入式学院 > 嵌入式学习 > 讲师博文 > 2.6.2 IO模式介绍
2.6.2 IO模式介绍
时间:2017-07-21作者:华清远见
数据的拷贝是从硬件设备拷到我们应用程序开辟的缓存buffer中,写的方式就是反过来。其实数据不是直接由硬件设备上拷到我们应用程序开辟的那段缓存之中,其中要经过内核开辟的一段缓存。然后再拷贝到我们的应用程序开辟的缓存之中。而我们的io模型主要就是针对这两个缓存的阶段来进行操作的。
    其中只有io多路复用是可以针对多个设备的其他四个都是针对单一设备而进行的。
1.阻塞I/O 模式
例如UDP函数recvfrom的内核到应用层、应用层到内核的调用过程是这样的:首先把描述符、接受数据缓冲地址、大小传递给内核,但是如果此时该与该套接口相应的缓冲区没有数据,这个时候就recvfrom就会卡(阻塞)在这里,知道数据到来的时候,再把数据拷贝到应用层,也就是传进来的地址空间,如果没有数据到来,就会使该函数阻塞在那里,这就叫做阻塞I/O模型。这种模型利用一个进程很无法实现多路IO同时跑。(应用程序调用函数之后,就等待内核准备好数据。内核从硬件设备上等待数据读取。如果是网络的哈要等待数据从网络上传递过来。当数据从外设拷到内核的缓存区之后。就再将数据从内核拷到我们应用程序的缓存区中.在这两个阶段中,数据准备阶段和从内核拷到应用程序的缓存。我们的都在等待当中。程序没有运行一直阻塞在哪里) 
提高:
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
2解释:每个TCP套接口有一个发送缓冲区,可以用SO_SNDBUF套接口选项来改变这一缓冲区的大小。当应用进程调用write往套接口写数据时,内核从应用进程缓冲区中拷贝所有数据到套接口的发送缓冲区,如果套接口发送缓冲区容不下应用程序的所有数据,或者是应用进程的缓冲区大于套接口的发送缓冲区,或者是套接口的发送缓冲区中有别的数据,应用进程将被挂起。内核将不从write返回。直到应用进程缓冲区中的所有数据都拷贝到套接口发送缓冲区。所以,从写一个TCP套接口的write调用成功返回仅仅表示我们可以重新使用应用进程缓冲区,它并不是告诉我们对方收到数据。TCP发给对方的数据,对方在收到数据时必须给于确认,只有在收到对方的确认时,本方TCP才会把TCP发送缓冲区中的数据删除。
UDP因为是不可靠连接,不必保存应用进程的数据拷贝,应用进程中的数据在沿协议栈向下传递时,以某种形式拷贝到内核缓冲区,当数据链路层把数据传出后就把内核缓冲区中数据拷贝删除。因此它不需要一个发送缓冲区。写UDP套接口的write返回表示应用程序的数据或数据分片已经进入链路层的输出队列,如果输出队列没有足够的空间存放数据,将返回错误ENOBUFS.)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2.非阻塞模式I/O
非阻塞方式实质上是用户高速内核,如果你没准备好就立刻返回,别把我阻塞。但如果内核看到数据已经就绪,read执行还是阻塞方式的。注意在6.2图上要突出非阻塞方式之所以效率低,在于会在内核态和用户态之间反复进出。所以引入第三点多路复用。(数据在第一阶段,准备数据的阶段由原来的等待转变成了过一段时间问一下我们的内核是否准备完成。以轮询的形式来操作第一阶段,第二阶段还是相似的)
普通的进程间通信我们使用open函数的时候我们直接在open函数中设定。
O_NONBLOCK 以不可阻断的方式打开文件, 也就是无论有无数据读取或等待, 都会立即返回进程之中.
O_NDELAY 同O_NONBLOCK.
而对于网络的状况我们使用fcntl函数来修改文件描述符本身激活他的非阻塞功能。 
fcntl()函数 当你一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。 可以使用函数fcntl()设置一个套接字的标志为O_NONBLOCK 来实现非阻塞。 
int fcntl(int fd, int cmd, long arg);       
int flag;       
flag = fcntl(sockfd, F_GETFL, 0);    
flag |= O_NONBLOCK;       
fcntl(sockfd, F_SETFL, flag);  
3.多路复用I/O
最后讲到多路IO复用时强调和非阻塞IO的对比。将retry的逻辑放到了内核态,用户态采用注册后等通知的模式。避免用户态和内核态的反复切换。其主要的意思就是可以同时等待多个设备,只要其中有一个设备准备完毕就可以执行。
注:select等待仍然是阻塞的,而且后继的read一般也是阻塞的方式。但由于此时read必定能读出数据,阻塞的时间也不会很长,可以一定程度上实现一些并行的效果。我们后面的实验可以看到这个效果 。
多路复用是依靠我们的select函数来完成的,在本文的最后会讲解以下select的用法。
4. 信号驱动I/O模型
当内核为我们准备好数据的时候,就会发送 SIGIO 信号,可以调用 signal注册SIGIO信号的处理函数,这个时候就可以在 SIGIO 信号处理函数中进行 recvfrom 函数来接受数据报。
这个需要内核的配合,就是我们等待内核从外部设备接受数据的这个部分有我们的内核本身完成当内核准备好就会发个信号给我们应用程序,这时候我们在处理这部分信息。
(将第一阶段进一步改进将原有的一次次访问的模式改为有内核发出信号,再由我们的函数调用内核将数据拷贝到应用程序的缓存)
 
5. 异步I/O模型
是让内核拷贝完之后通知我们。信号驱动I/O是当内核准备好数据的时候,通知我们可以调用recvfrom了,而异步I/O模型是内核通知我们I/O操作完成的时候通知我们。类比上面四中IO模型,只有这种可以称之为真正的异步IO,即用户发出IO请求后不用管,完全由内核并行执行所有操作,包括数据从内核态到用户态的拷贝。(将第一和第二阶段同时优化,由内核完成第一和第二阶段,最后直接将数据递交到我们应用程序的缓存中)
6. Select函数的使用
int select(int maxfdl,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct ti
    多端口复用函数select在调用前要首先设置监听的端口数目,FD_ZERO是清空端口集,FD_SET是设置端口集。
    select()函数常常用在用一个进程监听多个服务器端socket。
    有时,select()也被当作延时函数使用。sleep()延时会释放CPU,select()的话,可以在占用CPU的情况下延时。
    select()函数主要是建立在fd_set类型的基础上的。fd_set(它比较重要所以先介绍一下)是一组文件描述字(fd)的集合,它用一位来表示一个fd(下面会仔细介绍),对于fd_set类型通过下面四个宏来操作:
   void FD_ZERO(fd_set *fdset) 
void FD_SET(int fd,fd_set *fdset)  
void FD_CLR(int fd,fd_set *fdset)  
int FD_ISSET(int fd,fd_set *fdset) 
[cpp] view plain copy
1. fd_set  set;  
2.   FD_ZERO(&set);        /*将set清零使集合中不含任何fd*/  
3.   FD_SET(fd, &set);      /*将fd加入set集合*/  
4.   FD_CLR(fd, &set);      /*将fd从set集合中清除*/  
5.   FD_ISSET(fd, &set);   /*测试fd是否在set集合中*/  
    过去,一个fd_set通常只能包含<32的fd(文件描述字),因为fd_set其实只用了一个32位矢量来表示fd; 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,其值通常是1024,这样就能表示<1024的fd。根据fd_set的位矢量实现,我们可以重新理解操作fd_set的四个宏:    
[cpp] view plain copy
1. fd_set  set;  
2.    FD_ZERO(&set);        /*将set的所有位置0,如set在内存中占8位则将set置为00000000*/  
3.    FD_SET(0, &set);       /*将set的第0位置1,如set原来是00000000,则现在变为100000000,这样fd==1的文件描述字就被加进set中了*/  
4.    FD_CLR(4, &set);       /*将set的第4位置0,如set原来是10001000,则现在变为10000000,这样fd==4的文件描述字就被从set中清除了*/  
5.    FD_ISSET(5, &set);     /*测试set的第5位是否为1,如果原来set是10000100,则返回非零,表明fd==5的文件描述符在set中,否则返回0*/  
    注意:fd的最大值必须<FD_SETSIZE。
    select函数的接口比较简单:    
[cpp] view plain copy
1. int select(int nfds,  fd_set* readset,  fd_set* writeset,  fe_set* exceptset,  struct timeval* timeout);  
    功能:
    测试指定的fd可读?可写?有异常条件待处理?
    参数:
    nfds: 需要检查的文件描述字个数(即检查到fd_set的第几位),数值应该比三组fd_set中所含的最大fd值更大,一般设为三组fd_set中所含的最大fd值加1(如在readset, writeset, exceptset中所含最大的fd为5,则nfds=6,因为fd是从0开始的 )。设这个值是为了提高效率,使函数不必检查fd_set的所有1024位。
    readset: 用来检查可读性的一组文件描述字。
    writeset: 用来检查可写性的一组文件描述字。
    exceptset: 用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
    timeout: 有三种可能:
    1.  timeout = NULL (阻塞:直到有一个fd位被置为1函数才返回)
    2.  timeout所指向的结构设为非零时间(等待固定时间:有一个fd位被置为1或者时间耗尽,函数均返回)
    3.  timeout所指向的结构,时间设为0(非阻塞:函数检查完每一个fd后立即返回)
    返回值:返回对应位仍然为1的fd的总数。
    Remark:
    三组fd_set均将某些fd位置0,只有那些可读,可写以及有异常条件待处理的fd位仍然为1。
    使用select函数的过程一般是:
    先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1。

发表评论

全国咨询电话:400-611-6270,双休日及节假日请致电值班手机:15010390966

在线咨询: 曹老师QQ(3337544669), 徐老师QQ(1462495461), 刘老师 QQ(3108687497)

企业培训洽谈专线:010-82600901,院校合作洽谈专线:010-82600350,在线咨询:QQ(248856300)

Copyright 2004-2017 华清远见教育集团 版权所有 ,沪ICP备10038863号,京公海网安备110108001117号