用户态和内核态
根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
- 用户态(User Mode) : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入内核态。
- 内核态(Kernel Mode):内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
不过,由于切换内核态需要付出较高的开销(需要进行一系列的上下文切换和权限检查),应该尽量减少进入内核态的次数,以提高系统的性能和稳定性。
用户态和内核态如何切换
用户态切换到内核态的 3 种方式:
- 系统调用(Trap):用户态进程 主动 要求切换到内核态的一种方式,主要是为了使用内核态才能做的事情比如读取磁盘资源。系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现。
- 中断(Interrupt):当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
- 异常(Exception):当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
进程和线程
- 进程(Process) 是指计算机中正在运行的一个程序实例。比如打开某个应用。
- 线程(Thread) 轻量级进程,多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。
- 协程是一种用户态的轻量级线程,其调度完全由用户程序控制,而不需要内核的参与。协程拥有自己的寄存器上下文和栈,但与其他协程共享堆内存。协程的切换开销非常小,因为只需要保存和恢复协程的上下文,而无需进行内核级的上下文切换。这使得协程在处理大量并发任务时具有非常高的效率。然而,协程需要程序员显式地进行调度和管理,相对于线程和进程来说,其编程模型更为复杂。
进程的几种状态
和线程的状态很像
- 创建状态(new):进程正在被创建,尚未到就绪状态。
- 就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
- 运行状态(running):进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
- 阻塞状态(waiting):又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态(terminated):进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
DMA(direct memory access)
在没有DMA技术之前,I/O过程是这样的:
- 用户进程发起read调用,切换用户态到内核态
- CPU发出I/O请求给磁盘控制器,然后返回
- 磁盘控制器收到请求开始准备数据,把数据放到磁盘控制器的内部缓冲区,然后产生一个中断
- CPU收到中断信号,停下手头的工作,把磁盘控制器缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的
由上可见整个数据传输过程中,都需要CPU亲自参与,期间cpu不能做其他事,非常拉低性能。于是有了DMA。
传统的文件传输
进程文件传输,最简单的方式就是把磁盘文件读取出来,通过网络协议发出去。
期间发生四次用户态和内核态的切换,因为发生了两次系统调用,一次是
read()
,一次是write(),
每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
如何优化文件传输性能
在读取磁盘数据的时候之所以要发生上下文切换,是因为用户空间没有权限操作磁盘或网卡,需要切换到内核态来完成,而一次系统调用必然发生两次上下文切换:用户到内核、内核到用户,所以要减少上下文切换次数,就要减少系统调用次数!
在前面我们知道了,传统的文件传输方式会历经4次数据拷贝,而且这里面,从内核
的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到socket的缓冲区里,
这个过程是没有必要的。因为文件传输的应用场景中,在用户空间我们并不会对数据再加工,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的。
如何实现零拷贝
mmap + write
在前面我们知道,read()系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,可以用mmap()替换read()系统调用函数。
mmap()
系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
这种方式仍然是两次系统调用,四次上下文切换,只不过少了1次拷贝。
sendfile
linux内核2.1中的sendfile是专门发送文件的系统调用函数,仅有一次系统调用,可以代替read和write两次系统调用。该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。
linux 内核
2.4
,在网卡支持SG-DMA的情况下,可以用sendfile直接把文件从内核缓冲区拷贝到网卡。这才是真正的零拷贝,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
PageCache
回顾前面说道文件传输过程,其中第一步都是先需要先把磁盘文件数据拷贝「内核缓冲
区」里,这个「内核缓冲区」实际上是磁盘高速缓存(PageCache)。
PageCache会缓存最近被访问的数据(把磁盘数据读到缓存中),来提高读写性能。同时会进行预读,比如实际要从磁盘读取32kb数据,那么内核会把32-64kb的数据也提前读到缓存。