用户空间和内核空间
现在操作系统都是采用虚拟存储器,那么对于32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间分为两部分,内核空间,用户空间。针对linux系统而言,将最高的1G(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(0x00000000到0xBFFFFFFF)供各个进程使用,称为用户空间。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。从一个进程的运行转到另一个进程上运行,这个过程中经过下面的变化:
1.保存处理机上下文,包括程序计数器和其他寄存器。
2.更新PCB(进程管理块)信息。
3.把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
4.选择另一个进程执行,并更新PCB。
5.更新内存管理的数据结构。
6.恢复处理机上下文。
总而言之就是很耗资源。
进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其自身转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
Linux IO模式
基本概念
文件描述符fd(file descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。在形式上是一个非负整数。实际上,是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。
缓存IO
缓存IO又被称作标准IO,大多数文件系统的默认IO操作都是缓存IO。在Linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
IO模式
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核缓冲区拷贝到应用程序的地址空间。所以说,当一次read操作发生时它会经历两个阶段:
1.等待数据准备
2.将数据从内核拷贝到进程中
正因为这两个阶段,Linux系统产生了下面五种网络模式的方案。
1.阻塞IO(blocking IO)
2.非阻塞IO(nonblocking IO)
3.IO多路复用(IO multiplexing)
4.信号驱动IO(signal driven IO)
5.异步IO(asynchronous IO)
信号驱动IO在实际中并不常用,不再介绍。
阻塞IO
在Linux中默认所有的socket都是blocking,一个典型的读操作流程大概如下:当用户进程调用recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
特点是在IO执行的两个阶段都被block了。
非阻塞IO
Linux下,可以通过设置socket使其变为non-blocking。当对non-blocking socket执行读操作时,流程是这样:当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到用户进程的system call,那么它马上就将数据拷贝到用户内存,然后返回。
特点是用户进程需要不断的主动询问kernel数据准备好了没有。
异步IO
用户进程发起read操作后,立刻就可以开始去做其他的事。而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

IO多路复用
IO多路复用就是我们通常说的select、poll、epoll。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select、poll、epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会监视所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
IO多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接的处理更快,而是在于能处理更多的连接。
在IO编程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者IO多路复用技术进行处理。IO多路复用技术通过把多个IO的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统多线程/多进程模型比,IO多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或线程,也不需要维护这些进程和线程的运行,降低系统的维护工作量,节省了系统资源,IO多路复用的主要应用场景如下:
1.服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
2.服务器需要同时处理多种网络协议的套接字


select、poll、epoll的区别
select最大缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对于那些需要支持上万个TCP连接的大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降。我们也可以通过选择多进程的方案(传统Apache方案)解决问题,不过虽然在Linux上创建进程的代价比较小,但仍旧不可忽略,另外进程间的数据交换非常麻烦,对于java由于没有共享内存,需要通过socket通信或者其他方式进行数据同步,这带来了额外的性能损耗,增加了程序复杂度,所以也不是一种完美的解决方案。
epoll没有FD限制,它所支持的FD上限就是操作系统的最大文件句柄数,这个树远远大于1024,。例如,在1GB内存的机器上大约是10万个句柄左右,具体的值可以通过cat /proc/sys/fs/file- max查看,通常这个值跟系统内存关系比较大。
epoll的IO效率不会随着FD数目的增加而线性下降
select、poll另一个致命弱点是当socket集合很多时,由于网络延迟或链路空闲,任一时刻只有少部分的socket是活跃的,但select、poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对活跃的socket操作,这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的,那么只有活跃的socket才会触发callback函数。
epoll使用mmap加速内核与用户空间的消息传递
无论是select、poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap(将一个文件或者其他对象映射进内存)同一块内存实现的。
epoll的API更加简单
包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生,关闭epoll描述符等等。
值得说明的是,用来克服select、poll缺点的方法不只有epoll,epoll只是一种Linux的实现方案。在FreeBSD下有kqueue。
epoll边缘触发(ET)和水平触发(LT)
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率比LT模式高。epoll工作在ET模式时,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读、阻塞写操作把处理多个文件描述符的任务饿死。

posted on 2019-09-26 01:08  caohongchang  阅读(597)  评论(0编辑  收藏  举报