(笔记整理自b站小刘说源码课程)
一、DMA
在没有DMA时:CPA首先将内存数据读到缓存,再写到网卡。这样做会降低CPU的速度至网卡层面。
当出现DMA时:首先进行一次CPU复制,将内存数据读到socket内核缓冲区,此时CPU不参与后面的工作,转由DMA接管。DMA读取socket缓冲区的内容,然后将其写入网卡中。
DMA完成手头工作后,DMA中断CPU,此时CPU得知socket空出来了。CPU从用户态切换到内核态,执行中断处理程序,将socket缓冲区阻塞的进程移回到运行队列。
二、系统调用和虚拟内存
系统调用:用户读取硬盘上的文件,发起read调用,read是内核态的库函数api,该库函数会发起系统调用。该库函数里有80中断、软中断、进程切换到内核态。到CPU里存一个系统调用号(表示哪个系统函数,比如read)。把CPU的临时数据都保存到thread_info中(恢复到用户态时使用,包含PC、寄存器、用户程序基地址、CPU cache等),然后执行int 80中断处理程序,搜索到之前存的系统调用号,这里是read,首先检查缓存中有没有对应的数据,没有就去磁盘中加载到内核缓冲区,然后从内核缓冲区拷贝到用户空间,随后恢复到用户态,根据thread_info,用户可以知道从哪继续执行。
缓冲区读写:
读:首先用户态切换到内核态,先判断内核态缓冲区是否有,有的话直接读取,否则交由DMA处理。DMA从外设读,这个过程中CPU可以执行其他进程。DMA将数据加载到内核缓冲区后告诉CPU,CPU把数据拷贝到用户态,将此进程从阻塞队列移到运行队列。
写:缓存区满了之后,写操作产生阻塞。缓冲区有一个等待队列,记录阻塞的进程(java的轻量级进程),DMA将缓存区数据写到网卡后通知CPU,CPU中断,将该进程移动到运行队列。
物理内存:类似于大的数组,可以随机读取。
虚拟内存:早期单核计算机只需要保证数据不写入内核空间即可。多核计算机引入多线程后,每个进程有自己的用户空间,得防止不能访问其他进程空间,所以引入了虚拟内存。进程分配了虚拟内存,CPU的MMU单元可以辅助完成虚拟内存到物理内存的映射。虚拟内存可以大于真实物理内存,MMU可以把不常用的东西从物理内存放到磁盘(SWAP区)。因为可以替换,所以不同进程的虚拟内存空间的地址可以映射到同一个物理内存,利用这个特性,可以把用户空间和内核空间的地址翻译为同一块的物理内存地址,就可以减少拷贝,通常称为零拷贝。
三、IO方式
传统IO:从硬盘读到网卡。进程去读,首先看内核缓冲区是否存在,不存在则告诉DMA去读内核缓冲区,然后把进程放到内核的阻塞队列中,DMA读好后发起中断通知CPU,CPU唤醒阻塞进程,从内核缓冲区读到数据缓冲区,然后再切换到内核态进行写操作,写到socket缓冲区后,告诉DMA把socket缓冲区的数据写到网卡。复制了4次,进程切换了4次。
MMAP:DMA把磁盘上的文件映射到内存,用户空间和内核空间共享同一块物理地址,这样就无需进程用户空间和内核空间的来回复制。 写到网卡的时候,共享空间的内容拷贝到socket缓冲区(CPU复制),然后通知DMA发送到网卡。3次复制,两次DMA,一次CPU。
sendfile:打开文件的fd和传输的socket的fd告诉sendfile,也是和上述一样的3次复制,不过只进行了2次用户态和内核态的切换。
NIO:BUFFER数组,NIO把byte数组的位置和长度发给内核态,内核空间可以访问用户空间,称为跨传输。假设发生了FULL GC,stop the world,线程停止,回收垃圾对象,会进行碎片整理,位置可能改变。NIO选择在堆外创建一个同样大小的buffer,先从用户空间拷贝到堆外空间(CPU拷贝),再发送write系统调用,此时发送堆外的位置和长度,然后再拷贝到内核缓冲区。堆外是不发生GC的。使用NIO时可以直接将buffer拷贝到堆外,可以避免一些拷贝过程。