NIO相关基础篇三

转自:NIO相关基础篇三 https://blog.csdn.net/lirenzuo/article/details/78898430

1、用户空间、内核空间概念

为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
每个进程可以通过系统调用进入内核,因此,【Linux内核由系统内的所有进程共享】。

linux内部结构可以分为三部分,从最底层到最上层依次是:硬件–>内核空间–>用户空间

内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。
不管是内核空间还是用户空间,它们都处于虚拟空间中。

2、Linux 网络 I/O模型

1)为了OS的安全性等的考虑,进程是无法直接操作I/O设备的
其必须【通过系统调用请求内核来协助完成I/O动作】,而内核会为每个I/O设备维护一个buffer

2)请求过程为: 用户进程发起请求,内核接受到请求后,从I/O设备中获取数据到buffer中,再将buffer中的数据copy到用户进程的地址空间,该用户进程获取到数据后再响应客户端。

3)I/O动作五种模式

数据输入至buffer需要时间,从buffer复制数据至用户进程也需要时间。因此根据在这两段时间内等待方式的不同,把I/O分为5中模式。

  • a)阻塞I/O (Blocking I/O)
    在linux中,默认情况下所有的socket都是blocking

    当用户进程调用了recvfrom这个系统调用,内核就开始了IO的第一个阶段:等待数据准备。

    对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。
    而在用户进程这边,整个进程会被阻塞。

    当内核一直等到数据准备好了,就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除block的状态,重新运行起来。
    所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

  • b)非阻塞I/O (Non-Blocking I/O)
    linux下,可以通过设置socket使其变为non-blocking

    当用户进程调用recvfrom时,系统不会阻塞用户进程,而是立刻返回一个ewouldblock错误,从用户进程角度讲 ,并不需要等待,而是马上就得到了一个结果。

    用户进程判断标志是ewouldblock时,就知道数据还没准备好,于是它可以再次发送recvfrom。
    一旦内核中的数据准备好了。并且又再次收到了用户进程的系统调用,那么它马上就将数据拷贝到了用户内存,然后返回。

    用户进程非阻塞的调用recvfrom系统调用,我们称为轮询。应用程序不断轮询内核,看看是否已经准备好了某些操作。这通常是浪费CPU时间,但这种模式偶尔会遇到。

  • c)I/O复用(I/O Multiplexing)
    select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
    它的基本原理就是 select/epoll 这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

    当用户进程调用了select,那么整个进程会被block,而同时,内核会“监视”select负责的所有socket,当任何一个socket中的数据准备好了,select就会返回。
    这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。

    因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。所以耗时要比阻塞I/O(Blocking I/O)长。
    但是,用select的优势在于它可以同时处理多个connection。

    在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,
    但是整个用户的process还是一直被block的,process是被select这个函数block,而不是被socket IO给block。

  • d)信号驱动的I/O (Signal Driven I/O)
    用户进程不是阻塞的。
    首先用户进程建立SIGIO信号处理程序,并通过系统调用sigaction执行一个信号处理函数,这时用户进程便可以做其他的事了。
    一旦数据准备好,系统便为该进程生成一个SIGIO信号,去通知它数据已经准备好了,于是用户进程便调用recvfrom把数据从内核拷贝出来,并返回结果。

  • e)异步I/O (Asynchrnous I/O)
    这些函数通过告诉内核启动操作并在整个操作(包括内核的数据到缓冲区的副本)完成时通知我们。
    这个模型和前面的信号驱动I/O模型的主要区别是,在信号驱动的I/O中,内核告诉我们何时可以启动I/O操作,但是异步I/O时,内核告诉我们何时I/O操作完成。

    当用户进程向内核发起某个操作后,会立刻得到返回,并把所有的任务都交给内核去完成(包括将数据从内核拷贝到用户自己的缓冲区),
    内核完成之后,只需返回一个信号告诉用户进程已经完成就可以了。

4)文件描述符fd
Linux的内核将所有外部设备都可以看做一个文件来操作。
用户进程通过调用内核提供的系统调用对一个文件读写,内核给我们返回一个filede scriptor(fd,文件描述符)。
对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符)
描述符就是一个数字,指向内核中一个结构体(文件路径,数据区,等一些属性),应用程序对文件的读写通过对描述符的读写完成。

5)select
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。
调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。
当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

缺点:

  • [1]select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,32位机默认是1024个,64位机默认是2048。
    一般来说这个数目和系统内存关系很大,”具体数目可以cat /proc/sys/fs/file-max察看”。32位机默认是1024个。64位机默认是2048.

  • [2]对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
    当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。

  • [3]需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

6)poll
poll本质上和select没有区别,将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,
如果设备就绪则在设备等待队列中加入一项并继续遍历,
如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。
这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

  • [1]大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

  • [2]poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。
事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

7)epoll
epoll没有描述符限制。
epoll使用一个文件描述符管理多个描述符,将【用户关系的文件描述符】的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

poll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。
还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,
一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll的优点:

  • [1]没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)

  • [2]效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。
    只有活跃可用的FD才会调用callback函数;
    即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

  • [3]内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

8)使用mmap,让数据传输不需要经过user space

Java mmap()的调用

应用程序调用mmap(),磁盘上的数据会通过DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。

使用mmap是有代价的。当你使用mmap时,你可能会遇到一些隐藏的陷阱。例如,当你的程序map了一个文件,
但是当这个文件被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被SIGBUS信号终止。

解决方案避免这种问题:

  • [1]为SIGBUS信号建立信号处理程序
    当遇到SIGBUS信号时,信号处理程序简单地返回,write系统调用在被中断之前会返回已经写入的字节数,
    并且errno会被设置成success,但是这是一种糟糕的处理办法,因为你并没有解决问题的实质核心。

  • [2]使用文件租借锁
    通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,
    当其它进程想要截断这个文件时,内核会向我们发送一个实时的 RT_SIGNAL_LEASE 信号,告诉我们内核正在破坏你加持在文件上的读写锁。
    这样在程序访问非法内存并且被SIGBUS杀死之前,你的write系统调用会被中断。write会返回已经写入的字节数,并且置errno为success。
    我们应该在mmap文件之前加锁,并且在操作完文件后解锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值