计算机网络(4) ——零拷贝专题
计算机网络(4) ——零拷贝专题
1.内核空间和用户空间
虚拟内存被操作系统划分成两块:
内核空间和用户空间,内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态。
划分原因:
内核与业务解耦。内核空间的代码主要管理各种底层资源,而用户空间的代码主要实现业务逻辑。所以内核空间和用户空间在运行时,cpu的权限是不同的。
正是有了不同运行状态的划分,才有了上下文的概念。用户空间的应用程序,如果想要请求系统服务,比如操作一个物理设备,或者映射一段设备空间的地址到用户空间,就必须通过系统调用来(操作系统提供给用户空间的接口函数)实现。
2.进程上下文切换
所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
单核CPU下,当有多个任务同时需要处理怎么办呢?
轮着来,也就是如果有A、B两个程序同时运行,在CPU这里实际上是这样的:A→保存A上下文→读取B上下文→B→保存B上下文→读取A上下文→A……如此循环直到程序结束,由于CPU运行极快,我们并不能感知到A和B中间短暂的中止状态,于是在我们看来就像是在同时运行。
3.缓存IO
缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 IO 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
具体如下:
当应用程序访问某块数据时,操作系统首先会检查内核缓冲区是否有相关数据,如果没有,操作系统首先将磁盘上的数据拷贝的内核缓冲区,这一步目前主要依靠DMA来传输,然后再把内核缓冲区上的内容拷贝到用户缓冲区中,最后将用户缓冲区数据拷贝到socket缓冲区。
改进方法:
(1)mmap+write实现拷贝
先把磁盘的数据拷贝到内核缓冲区,然后将内核缓冲区数据映射到用户缓冲区,socket从用户缓冲区拷贝数据,相当于直接从内核缓冲区里拷,这个过程减少了一次数据拷贝。
(2)sendfile实现零拷贝
把磁盘的数据拷贝到内核缓冲区,然后socket缓冲区直接进行拷。这个过程越过了内存层面的数据拷贝,也叫零拷贝。
4.IO复用模拟
举个栗子:
模拟一个tcp服务器处理30个客户socket。假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
1.第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡主,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。
2.第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。
3.第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。
这种就是IO复用模型。
Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。