虚拟内存 & I/O & 零拷贝( 四 )


对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码 。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行 。
如何从用户空间进入内核空间?
其实所有的系统资源管理都是在内核空间中完成的 。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等 。我们的应用程序是无法直接进行这样的操作的 。但是我们可以通过内核提供的接口来完成这样的任务 。比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用” 告诉内核:“我要读取磁盘上的某某文件” 。
其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据 。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态 。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以开开心心的往下执行了 。简单说就是应用程序把高科技的事情(从磁盘读取文件)外包给了系统内核,系统内核做这些事情既专业又高效 。
三、 IO
在进行 IO 操作时,通常需要经过如下两个阶段:

  • 数据准备阶段:数据从硬件到内核空间
  • 数据拷贝阶段:数据从内核空间到用户空间

虚拟内存 & I/O & 零拷贝

文章插图
通常我们所说的 IO 的阻塞/非阻塞以及同步/异步,和这两个阶段关系密切:
  • 阻塞 IO 和非阻塞 IO 判定标准:数据准备阶段,应用程序是否阻塞等待操作系统将数据从硬件加载到内核空间;
  • 同步 IO 和异步 IO 判定标准:数据拷贝阶段,数据是否备好后直接通知应用程序使用,无需等待拷贝;
3.1 (同步)阻塞 IO阻塞 IO :当用户发生了系统调用后,如果数据未从网卡到达内核态,内核态数据未准备好,此时会一直阻塞 。直到数据就绪,然后从内核态拷贝到用户态再返回 。
虚拟内存 & I/O & 零拷贝

文章插图
阻塞 IO 每个连接一个单独的线程进行处理,通常搭配多线程来应对大流量,但是,开辟线程的开销比较大,一个程序可以开辟的线程是有限的,面对百万连接的情况,是无法处理 。非阻塞 IO 可以一定程度上解决上述问题 。
3.2 (同步)非阻塞 IO
非阻塞 IO :在第一阶段(网卡-内核态)数据未到达时不等待,然后直接返回 。数据就绪后,从内核态拷贝到用户态再返回 。
虚拟内存 & I/O & 零拷贝

文章插图
非阻塞 IO 解决了阻塞 IO每个连接一个线程处理的问题,所以其最大的优点就是 一个线程可以处理多个连接 。然而,非阻塞 IO 需要用户多次发起系统调用 。频繁的系统调用是比较消耗系统资源的 。
3.3 IO 多路复用
为了解决非阻塞 IO 存在的频繁的系统调用这个问题,随着内核的发展,出现了 IO 多路复用模型 。
IO 多路复用:通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,就可以返回 。
虚拟内存 & I/O & 零拷贝

文章插图
IO 多路复用本质上复用了系统调用,使多个文件的状态可以复用一个系统调用获取,有效减少了系统调用 。select、poll、epoll均是基于 IO 多路复用思想实现的 。
虚拟内存 & I/O & 零拷贝

文章插图

虚拟内存 & I/O & 零拷贝

文章插图
select 和 poll 的工作原理比较相似,通过 select或者 poll将多个 socket fds 批量通过系统调用传递给内核,由内核进行循环遍历判断哪些 fd 上数据就绪了,然后将就绪的 readyfds 返回给用户 。再由用户进行挨个遍历就绪好的 fd,读取或者写入数据 。所以通过 IO 多路复用+非阻塞 IO,一方面降低了系统调用次数,另一方面可以用极少的线程来处理多个网络连接 。select 和 poll 的最大区别是:select 默认能处理的最大连接是 1024 个,可以通过修改配置来改变,但终究是有限个;而 poll 理论上可以支持无限个 。而 select 和 poll 则面临相似的问题在管理海量的连接时,会频繁的从用户态拷贝到内核态,比较消耗资源 。


推荐阅读