一、用户进程缓冲区和内核缓冲区
缓冲区的目的,是为了减少频繁的系统IO调用。大家都知道,系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。
有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区中。等待缓冲区达到一定数量的时候,再进行IO的调用,提升性能。至于什么时候读取和存储则由内核来决定,用户程序不需要关心。
在linux系统中,系统内核也有个缓冲区叫做内核缓冲区。每个进程有自己独立的缓冲区,叫做进程缓冲区。
因此,用户程序的IO读写程序,大多数情况下,并没有进行实际的IO操作,而是在读写自己的进程缓冲区。
用户进程缓冲区
前面提到,用户进程通过系统调用访问系统资源的时候,需要切换到内核态,而这对应一些特殊的堆栈和内存环境,必须在系统调用前建立好。而在系统调用结束后,cpu会从核心模式切回到用户模式,而堆栈又必须恢复成用户进程的上下文。而这种切换就会有大量的耗时。
你看一些程序在读取文件时,会先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节长度的数据,写入buffer。(用较小的次数填满buffer)。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。
所以说:用户缓冲区的目的是为了减少系统调用次数,从而降低操作系统在用户态与核心态切换所耗费的时间。
内核缓冲区
除了在进程中设计缓冲区,内核也有自己的缓冲区。
当一个用户进程要从磁盘读取数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程缓冲区中。
但若是内核缓冲区中没有数据,内核会把对数据块的请求,加入到请求队列,然后把进程挂起,为其它进程提供服务。
**等到数据已经读取到内核缓冲区时,把内核缓冲区中的数据读取到用户进程中,才会通知进程。**当然不同的io模型,在调度和使用内核缓冲区的方式上有所不同,下一小结介绍。
你可以认为,read是把数据从内核缓冲区复制到进程缓冲区。write是把进程缓冲区复制到内核缓冲区。
当然,write并不一定导致内核的写动作,比如os可能会把内核缓冲区的数据积累到一定量后,再一次写入。这也就是为什么断电有时会导致数据丢失。
所以说内核缓冲区,是为了在OS级别,提高磁盘IO效率,优化磁盘写操作。
参考连接:
二、进程空间
进程空间分为:代码区、数据区、堆区、栈区
linux的虚拟空4G,分为内核空间和用户空间,其中最开的(3G)0x00000000-0xBFFFFFFF为各个进程使用,也叫用户空间,最高处的(1G)0xC0000000-0xFFFFFFFF作为内核空间供内核使用,Linux内核空间由系统内的所有进程共享。
于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟地址空间(也叫虚拟内存)。其中每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间则为所有进程以及内核所共享。
- 用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。例外情况只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。
- 用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程变化,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。
- 每个进程的用户空间都是完全独立、互不相干的。
参考连接:
三、进程间通信的原理
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到。所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区。进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制。
主要的过程如下图所示:
数据读写阶段,用户进程能访问内核缓冲区吗?
用户态切换到内核态是通过系统调用实现的,比如 read就是一个系统调用。应用程序通过调用read来读写数据(将内核空间的数据搬到用户空间中,这样我们才能够对数据进行操作)。
四、Reactor模式
要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作 线程(逻辑单元),将 socket 可读可写事件放入请求队列,交给工作线程处理。除此之外,主线程不做 任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步 I/O(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是:
-
主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
-
主线程调用 epoll_wait 等待 socket 上有数据可读。
-
当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
-
睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件。
-
当主线程调用 epoll_wait 等待 socket 可写。
-
当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
-
睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果。 Reactor 模式的工作流程:
为什么要注册读写事件?
因为每个进程有各自的私有用户空间(0~3G),共享1GB的内核空间。内核缓冲区的大小是有限的。 进程想写时,tcp 发送缓冲区缓冲区不一定有空间,需要先注册写事件,由内核判断TCP内核缓冲区有没有写满。如果没有写满,我就可以往里面写。线程同理,差不多。
tcp 或udp收发缓冲区最小值
tcp 或udp接收缓冲区的最小值为 256 bytes,由内核的宏决定;
tcp 或udp发送缓冲区的最小值为 2048 bytes,由内核的宏决定。