在学习Java网络编程之前,先来了解一下涉及到的基础概念。
现在操作系统都是采用虚拟存储器,操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟内存划分为两部分,一部分为内核空间,一部分为用户空间。对于32位操作系统,它的寻址空间(虚拟存储空间)为4G(2的32次方),linux操作系统中将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个用户进程使用,称为用户空间。
在 Linux 系统中,内核模块运行在内核空间,对应的进程处于内核态;而用户程序运行在用户空间,对应的进程处于用户态。
内核态进程可以执行任意命令,调用系统的一切资源,而用户态进程只能执行简单的运算,不能直接调用系统资源。那用户态进程如何执行系统调用呢?用户态进程必须通过系统接口(System Call),才能向内核发出指令,完成调用系统资源之类的操作。
CPU 拷贝:由 CPU 直接处理数据的传送,数据拷贝时会一直占用 CPU 的资源。
DMA 拷贝:由 CPU 向DMA控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,从而减轻了 CPU 资源的占有率。
上下文切换:当用户程序向内核发起系统调用时,CPU 将用户进程从用户态切换到内核态;当系统调用返回时,CPU 将用户进程从内核态切换回用户态。
缓存IO又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,数据会先从磁盘/网卡通过DMA方式被拷贝到内核空间的缓冲区中,然后才会从内核空间的缓冲区拷贝到应用程序的地址空间。
读数据:当应用程序调用read()读取数据的时候,如果这块数据存在于内核空间的缓冲区,就直接返回;如果在内核空间缓冲区不存,就从磁盘/网卡中读取到内核空间缓冲区,再拷贝到用户空间缓冲区。
写数据:用户进程调用write(),将数据从用户空间缓冲区复制到内核空间缓冲区,这时候对用户进程来说,写操作已完成,至于数据什么时候从内核缓冲区写入到磁盘/网卡,由操作系统决定,这种输出方式称为延迟写,延迟写减少了磁盘读写次数,但是降低了数据持久化的速度,当系统发生故障时还可能造成数据丢失。也可以通过调用sync()/fsync()/fdatasync()来强制写入磁盘。
缓存 IO 的优点:在一定程度上分离了用户空间和内核空间,保护系统本身运行安全;减少读写磁盘次数,提高性能。
缓存 IO 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
与之对应的还有直接IO,就是应用程序直接访问磁盘/网卡数据,而不经过内核缓冲区,这样可以减少一次内核缓冲区到用户缓冲区的数据拷贝。比如DBMS这类应用,它们比操作系统更了解数据库中存放的数据,可以提供一套更加有效的缓存机制来提高数据库中数据的存取性能,因此更倾向于选择他们自己的缓存机制。
直接 IO 的优点:可以减少一次从内核缓冲区到用户缓冲区的数据拷贝。
直接IO 的缺点:如果访问的数据在应用缓冲区中不存在,那么每次都会从磁盘/网卡中直接读取数据,这种方式读取数据会非常慢,因此直接IO通常和异步IO结合使用才会得到比较好的性能。
同步:进程触发IO操作并等待,或者轮训去查询IO操作是否完成,等待结果,然后才能执行后续的操作;
异步:触发IO操作后,直接返回,继续做后续的操作,IO交给内核来处理,完成后内核通知进程IO完成;
阻塞:进程给CPU传达一个任务后,一直等待CPU处理完成,才继续执行后续操作;
非阻塞:进程给CPU传达任务后,继续执行后续操作,隔段时间再来查询是否完成。
同步和异步是针对于应用程序和内核的交互而言的,更加关注通知的方式,关注的是程序与内核的协作关系;阻塞和非阻塞更关注的是单个进程内部的执行状态,粒度更细,更微观,进程的阻塞是进程自身的一种主动行为。
举个例子:
双11期间,我想知道某个店铺是否有优惠券。
同步:我一直刷新页面,查询是否有优惠券;
异步:我点击了订阅,店铺有优惠券会发短信通知我;
阻塞:无论我不断刷新页面,还是点击订阅,优惠券发放之前我不能做其他事,只能干等着;
非阻塞:无论我不断刷新页面,还是点击订阅,优惠券发放之前我还可以做其他事,比如听歌、喝水。
(1)用户进程发起read,进行recvfrom系统调用。
(2)内核开始准备数据(从网卡拷贝到缓冲区),进程请求的数据并不是一下就能准备好,准备数据是要消耗时间的。与此同时,用户进程阻塞,等待数据ing。
(3)把数据从内核空间拷贝到用户空间。
(4)内核返回结果,用户进程解除阻塞。
(1、3)用户进程发起read,进行recvfrom系统调用,如果数据还没准备好就直接返回。
(2)内核收到用户进程的调用后,开始准备数据。
(4、5)用户进程再次发起read读取数据,如果数据还没准备好,直接返回。
(6)用户进程再次发起read读取数据,这时候数据已经拷贝到内核缓冲区了。
(7)把数据从内核缓冲区拷贝到用户缓冲区,这个过程用户进程阻塞。
(8)内核返回结果,用户进程解除阻塞。
(1)当用户进程调用了select,用户进程会被阻塞。
(2)内核会“监视”所有select负责的socket。
(3)当任何一个socket中的数据准备好了,select就会返回。
(4)这个时候用户进程调用read操作。
(5)将数据从内核拷贝到用户空间缓存,在此期间用户进程会被阻塞。
(6)内核返回结果,用户进程解除阻塞。
所谓信号驱动式I/O(signal-driven I/O),就是预先告知内核,当某个描述符准备发生某件事情的时候,让内核发送一个信号通知应用进程。
(1、2、3)用户进程注册SIGIO信号处理函数,进行sigaction系统调用;内核开启信号驱动式IO并返回调用结果。
(4、5)内核等待数据返回后,会给用户进程发送一个SIGIO信号。
(6)用户进程在信号处理函数中调用recvfrom读取数据,数据返回前,用户进程阻塞。
(7)将数据从内核拷贝到用户空间缓存。
(8)内核返回结果,用户进程解除阻塞。
(1、2)用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。
(3)内核开始准备数据。
(4)把数据从内核缓冲区拷贝到用户缓冲区。
(5)数据拷贝完成后,内核会给用户发送一个signal或者执行一个基于线程的回调函数来完成这次IO处理过程。
转载请注明出处——胡玉洋 《Java网络编程——基础概念》