五种I/O模型

五种I/O模型

  • 阻塞I/O
  • 非阻塞I/O
  • I/O多路复用
  • 信号驱动I/O(SIGIO)
  • 异步I/O

一般来说。程序进行输入操作有两步:

  1. 等待有数据可以读;
  2. 将数据从系统内核中拷贝到程序的数据区。

对于一个对嵌套字的输入操作,第一步一般来说是等待数据从网络传到本地。当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中;第二步是从内核中把数据拷贝到程序的数据区中。

1、阻塞I/O模式(blocking I/O)

最普遍的一种I/O模型,大部分程序都是用的是阻塞式I/O。缺省的,一个套接字建立后所处于的模式就是阻塞I/O模式。

对于UDP套接字来说,数据就绪的标志比较简单:

  • 已经收到了一整个数据报
  • 没有收到

而TCP则比较复杂,需要附加一些其他变量。

如下图所示,一个进程调用recvfrom,然后系统调用并不返回知道有数据报到达本地系统,然后系统将数据拷贝到进程的缓存中(如果系统调用收到一个中断信号,则它的调用会被中断)。

我们成这个进程在调用recvfrom一直到从recvfrom返回这段时间是阻塞的。当recvfrom正常返回时。我们的进程继续它的操作。

阻塞I/O

举例:
A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。

典型的阻塞IO模型的例子为:
fd=connect();
write(fd);
read(fd);
close(fd);

2、非阻塞模式I/O(noblocking I/O)

当我们将一个套接字设置为非阻塞模式,相当于告诉系统内核:“当我请求的I/O操作不能马上完成,你想让进行进行休眠等待时,不要这么做,请马上返回一个错误给我”。

如下图用来描述非阻塞式I/O

我们开始对recvfrom的三次调用,因为系统还没有收到网络数据,所以内核马上返回一个EWOULDBLOCK的错误。第四次我们调用recvfrom函数,一个数据报已经到达了,内核将它拷贝到我们的应用程序的缓冲区中,然后recvfrom正常返回,我们就可以对接受的数据进行处理了。

当一个程序使用了非阻塞模式的套接字,它小使用一个循环来不停的测试是否一个文件描述符合有数据可读(称作polling)。应用程序不停地plling内核来检查是否I/O操作已经就绪。这是对CPU资源的极大浪费,这种模式平时使用中不是很普遍。

非阻塞I/O

举例:
B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情,但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。

典型的非阻塞IO模型一般如下:

while(true){
   data = socket.read();
   if(data!= error){
       // 处理数据
       break;
   }
}

3、信号驱动IO(signal blocking I/O)

当我们将一个套接字设置为信号驱动I/O模式,让内核在文件描述符就绪的时候使用SIGIO信号来通知我们。我们将这种模式称之为信号驱动I/O模式。

使用这种模式。我们首先需要允许套接字使用信号驱动I/O,还要安装一个SIGIO的处理函数。在这种模式下,系统调用将会立即返回,然后我们的程序可以继续做爱他的事情。当数据就绪的时候,会向我们的进程大宋一个SIGIO信号。这样我们就可以在SIGIO信号的处理函数中进行I/O操作(或是我们在函数中通知主函数有数据可读)。

对于信号驱动I/O模式,它的现金支出在于他在等待数据的时候不会阻塞,程序可以做自己的事情。当有数据到达的时候,系统内核会向程序发送一个SIGIO信号进行通知,这样哦们的程序就可以获得更大的灵活性,因为我们不必为等待数据进行额外的编码。

信号驱动I/O

信号I/O可以试用内核在某个问价你描述发生改变的时候发信号通知我们的程序。异步I/O可以提高我们程序I/O读写效率。通过使用它,我们的程序进行I/O操作时候,内核尅在初始化I/O操作后立即返回,在进行I/O操作的同时,我们的程序可以处理自己的事情,知道I/O操作结束,系统内核给我们的程序发送消息通知。

为了在一个套接字上使用信号驱动I/O操作,必须有下面三个步骤:

  1. 一个和SIGIO信号的处理函数必须设定
  2. 套接字的拥有者必须被设定,一般使用fcntl函数的F_SETOWN参数来设定拥有着。
  3. 套接字必须被允许使用异步I/O。一般使用fcntl函数的F_SETFL命令,O_ASYNC为参数来实现。

虽然设定套接字为异步I/O非常简单,但是试用起来困难的部分是怎样在程序中判定产生SIGIO信号发送给套接字属主的时候,程序出于什么状态。

  1. UDP套接字的SIGIO信号

    在UDP协议上使用异步I/O非常简单,这个信号将在这时候产生:

    • 套接字接收到一个数据报的数据包
    • 套接字发生了异步错误

    当我们在使用UDP套接字异步I/O时,使用recvfrom()函数来读取数据包数据或异步I/O错误信息。

  2. TCP套接字的SIGIO信号

    异步I/O对TCP套接字没什么作用,因为对于一个TCP套接字来说,SIGIO信号发生几率太高了,所以SIGIO信号并不鞥告诉我们究竟发生了什么。在TCP连接中SIGIO信号会在一下情况产生:

    • 在一个监听某个端口的套接字上成功建立了新的连接
    • 一个断线的请求被成功初始化
    • 一个断线的请求成功的结束
    • 套接字的某个通道被关闭
    • 套接字接收到新数据
    • 添加诶子将数据发送出去
    • 发生一个异步I/O错误

如果一个正在进行读写操作的TCP套接字处于信号驱动I/O状态下,那么当新数据到达本地时,将会产生一个SIGIO信号,当本地套接字发送数据被远程确认后,又会产生一个SIGIO信号,程序无法分别两次信号的差别。在这种情况下使用SIGIO,TCP套接字应当被设置为无阻塞模式来阻止一个阻塞的读写操作。我们可以考虑在一个只进行侦听网络连接操作的套接字上使用异步I/O,遮阳挡一个新链接产生的时候,会有一个SIGIO信号。
举例:

C也在河边钓鱼,但与A、B不同的是,C比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,C就会将鱼钓上来。

4、I/O多路复用(I/O multiplexing)

在使用多路复用I/O技术时,会调用select()函数和poll()函数,在调用他们的时候阻塞,而不是我们来调用recvfrom的时候阻塞。

当调用select()函数阻塞的时候,select函数等待数据报套接字进入读就绪状态。当select函数返回的时候,就是套接字可以读取数据的时候。这是很好就可以调用recvfrom函数来将数据拷贝到缓存区中。

和阻塞模式比较,select()和poll()函数并没有特别的订房。而且,在阻塞模式下只需要调用一个函数:读取或发送,在使用了多路复用技术后,需要调用两个函数:select()或poll()函数,然后才能进行真正的读写。

多路复用的好处在于,能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读取状态,select()函数就可以返回。

在这里插入图片描述

举例:

假设我们运行一个网络客户单程序,要同时很适合用套接字传来的网络数据又要处理本地的标准输入输出。我们在程序处于阻塞状态等待标准输入数据的时候,加入服务器端的程序挂掉了,那么服务器端的TCP协议给客户端的TCP协议发送一个FIN数据代表终止链接。但是我们的程序处于阻塞状态,在他读取套接字数据之前,我们就看不到结束标志,这时候就不能使用阻塞模式。

应用场景:

  • 当一个客户端需要同事处理多个文件描述符的输入输出操作时

  • 当程序需要同时进行多个套接字的操作时

  • 如果一个TCP服务器程序同事处理正在侦听网络连接的套接字和已经连接好的套接字

  • 如果一个服务器程序同事使用TCP和UDP协议

  • 如果一个服务器同事使用过重服务并且每种服务使用协议不同

    4.1、select函数

该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段时间后才唤醒他。

#inlcude <sys/select.h>
#include<sys/times.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

select 函数返回,返回值为检测到的事件个数并且返回哪些I/O发生了事件,遍历这些事件,进而处理事件。
参数的含义:

  • 1.读、写、异常、集合中的文件描述符的最大值+1
  • 2.读集合
  • 3.写集合
  • 4.异常集合
  • 5.超时结构体

select只能并发,无法达到并行。(这里指的是单核)

select的限制

  • 一个进程能打开最大的文件描述符,这个可以通过调整内核的参数
  • select中的fd_set集合容量限制(FD_SETSIZE=1024)这需要重新编译内核。

4.2、poll函数

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds,int timeout);

第一参数是指向结构体数组,每个数组元素都是一个pollfd结构。

**select和poll函数的共同点是:**内核要遍历所有文件描述符,直到找到所有发生事件的文件描述符

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

4.3、epoll 函数

  • 相比于select与poll,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率
  • 内核中的select与poll的实现时采用轮询来处理的,轮询的fd数目越多,自然耗时越多
  • epoll实现是基于回调的,如果fd有期望的事件发生就通过回调函数将其加入epoll就绪队列中,也就说说它只关心“活跃”的fd,与fd的数目无关
  • 内核/用户空间拷贝问题,如何让内核把发fd消息通知给用户空间呢?在这个问题上select/poll采取内存拷贝的方式,而epoll采用的共享内存的方式
  • epoll不仅会告诉应用程序有I/O事件到来,还会告诉应用程序相关的信息,这些信息是应用填充的,因此根据这些消息应用程序就能直接定位到事件,而不必遍历整个fd集合。

4.4、select,poll和epoll三者比较

用通俗的话来讲,假如有三个老师分别是select,poll,epoll,每次当老师要去收全班同学的作业时,也就是当老师被调用时,select和poll老师就会一个一个的检查在这个班的所有的同学的作业并拿走作业,而epoll老师设置了一个讲台,说谁写完了就放在讲台上,那当epoll老师工作的时候,只需要在讲台上拿走完成的作业,而不用全部遍历。

5、异步IO(asynchronous I/O)

当我们运行在异步I/O模式时,只需要告诉内核我们需要进行I/O操作,然后内核就会马上返回,具体的I/O和数据拷贝交给内核来完成,程序继续向下执行。当内核完成所有I/O操作和数据拷贝之后,内核通知我们的程序。

异步I/O与信号驱动I/O区别:

  • 信号驱动I/O模式下,内核在操作可以操作的时候通知给程序发送SIGIO消息
  • 异步I/O模式下,内核在所有操作都被内核操作结束后才会通知给程序

如下图,当我们进行I/O操作时,我们传递给内核我们的文件描述符,我们的缓存区指针和缓存区的大小,一个偏移量offset,以及在内核结束所有操作后和给我们通知的方法,这种调用也是立即返回的,程序不需要阻塞来等待程序的就绪。

在这里插入图片描述
举例:

E也想钓鱼,但E有事情,于是他雇来了F,让F帮他等待鱼上钩,一旦有鱼上钩,F就打电话给E,E就会将鱼钓上去。

6.几种I/O模式比较

在这里插入图片描述
可以看出,阻塞程度:阻塞IO>非阻塞IO>多路复用IO>信号驱动IO>异步IO,效率是由低到高的。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
} @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment信号驱动式I/O模型是一种异步的I/O模型,它可以同时处理多个I/O操作,提高系统的并发性和响应性。下面是详细介绍: 1. 应用程序向内核_music, container, false); Button btnPlay = view.findViewById(R.id.btn_play); Button btnPause = view.findViewById(R.id.btn发起I/O请求,请求在指定的文件描述符上监听I/O事件。 2. 内核在指定的文件_pause); Button btnStop = view.findViewById(R.id.btn_stop); mMediaPlayer = MediaPlayer.create(getContext(), R.raw.music); btn描述符上等待I/O事件发生。 3. 当I/O事件发生时,内核会向应用程序发送Play.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!mMediaPlayer.isPlaying()) { 一个信号,通知应用程序有I/O事件需要处理。 4. 应用程序收到信号后,可以在 mMediaPlayer.start(); } } }); btnPause.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View信号处理函数中处理相应的I/O事件。在信号处理函数中,应用程序可以读写数据,或 v) { if (mMediaPlayer.isPlaying()) { mMediaPlayer.pause(); } } }); btnStop.setOnClickListener(new View者关闭文件描述符等操作。 5. 当应用程序完成对I/O事件的处理后,可以再次向内核.OnClickListener() { @Override public void onClick(View v) { if (mMediaPlayer.isPlaying()) { mMediaPlayer.stop(); 发起I/O请求,等待下一个I/O事件的发生。 信号驱动式I/O模型可以有效地 mMediaPlayer = MediaPlayer.create(getContext(), R.raw.music); } } }); return view; } @Override 避免阻塞等待I/O操作完成的情况,提高系统的并发性和响应性。但是, public void onDestroy() { super.onDestroy(); if (mMediaPlayer != null) { mMediaPlayer.release(); mMediaPlayer =信号驱动式I/O模型需要应用程序处理信号,增加了一定的复杂度。同时, null; } } } ``` 7.其他模块的Fragment和Java代码与音乐模块类似,这里由于信号可能会被其他信号打断,因此需要应用程序进行信号处理函数的重入处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值