Linux 基础知识回顾
用户空间和内核空间
现在操作系统都采用虚拟寻址,处理器先产生一个虚拟地址,通过地址翻译成物理地址(内存的地址),再通过总线的传递,最后处理器拿到某个物理地址返回的字节。
对 32 位操作系统而言,它的寻址空间(虚拟存储空间)为 4G(2 的 32 次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
针对 linux 操作系统而言:将最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)供内核使用,称为内核空间。而将较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)供各个进程使用,称为用户空间。
直接 I/O 和缓存 I/O
文件系统 IO 分为 DirectIO(直接I/O)和 BufferIO(缓存 I/O),其中 BufferIO 也叫 Normal IO(标准 I/O)。大多数文件系统的默认 I/O 操作都是缓存 I/O。
缓存 I/O
读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。
写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令。
以 write 为例,数据会先被拷贝进程缓冲区,在拷贝到操作系统内核的缓冲区中,然后才会写到存储设备中。
直接 I/O(少了拷贝到应用进程缓冲区这一步)
阻塞与同步
阻塞(Block) / 非租塞(NonBlock)
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,比如当数据没有准备就绪的时候。
阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。
非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。
同步(Synchronization) / 异步(Asynchronization)
同步和异步都是基于应用程序私操作系统处理 IO 事件所采用的方式,比如:
同步:是应用程序要直接参与 IO 读写的操作。
异步:所有的 IO 读写交给操作系统去处理,应用程序只需要等待通知。
同步方式在处理 IO 事件的时候,必须阻塞在某个方法上面等待我们的 IO 事件完成(阻塞 IO 事件或者通过轮询 IO 事件的方式)。
对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的 IO 操作,当操作完成 IO 后会给我们的应用程序一个通知。
常见 IO 模型
对于一次 IO 访问,它会经历两个阶段:等待数据准备就绪 (Waiting for the data to be ready);将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
举例来说:
读函数:分为等待系统可读和真正的读。
写函数:分为等待网卡可以写和真正的写。
说明:
等待就绪的阻塞是不使用 CPU 的,是在“空等”。而真正的读写操作的阻塞是使用 CPU 的,真正在“干活”,而且这个过程非常快,属于 memory copy,宽带通常在 1GB/s 级别以上,可以理解为基本不耗时。
下图是几种常见 I/O 模型的对比:
以 socket.read() 为例子:
传统的 BIO 里面 socket.read(),如果 TCP RecvBuffer 里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
对于 NIO,如果 TCP RecvBuffer 有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回 0,永远不会阻塞。
最新的 AIO(Async I/O) 里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
换句话说,BIO 里用户最关心“我要读”,NIO 里用户最关心"我可以读了",在 AIO 模型里用户更需要关注的是“读完了”。
NIO 一个重要的特点是:socket 主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的 I/O 操作是同步阻塞的(消耗 CPU 但性能非常高)。