用户线程和内核线程
内核态和用户态
介绍
Inter把 C P U 指令集 操作的权限由高到低划为4级
- ring 0
- ring 1
- ring 2
- ring 3
其中 ring 0 权限最高,可以使用所有 C P U 指令集,ring 3 权限最低,仅能使用常规CPU指令集,不能使用操作硬件资源的CPU指令集,比如IO读写、网卡访问、申请内存都不行,Linux系统仅采用ring 0 和 ring 3 这2个权限,ring0被叫做内核态,完全在操作系统内核中运行,ring3被叫做用户态,在应用程序中运行
内核态权限大可以复制IO读写,网卡,内存等操作,用户态只能操作应用程序分配的空间
当需要进行高权限操作的时候,就需要从用户态切到内核态,比如,要读取文件,就需要从用户态切换到内核态(通过调用系统函数),进行文件读写,读写完成后再切换回来(普通IO时如果文件过大会进行多次用户和内核态的切换)
操作系统通过CS:IP来查找执行命令,其中CS的最低两位来表示内核和用户态(0:表示内核态,3:表示用户态,刚好符合inter权限)
用户态和内核态切换的开销大
切换时需要:
- 保留用户态现场(上下文、寄存器、用户栈等)
- 复制用户态参数,用户栈切到内核栈,进入内核态
- 额外的检查(因为内核代码对用户不信任)
- 执行内核态代码
- 复制内核态代码执行结果,回到用户态
- 恢复用户态现场(上下文、寄存器、用户栈等)
什么情况会导致用户态到内核态切换
- 系统调用:用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用,系统调用的机制核心使用了操作系统为用户特别开放的一个中断来实现,如Linux 的 int 80h 中断,也可以称为软中断
- 异常:当 C P U 在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常
- 中断:当 C P U 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 C P U 发出相应的中断信号,这时 C P U 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等
零拷贝
DMA 技术
在没有DMA时
- CPU 发出对应的指令给磁盘控制器,然后返回;
- 磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断;
- CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。
整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的
所以,DMA就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
普通IO拷贝
普通拷贝,byte[] b = read(),write(b);
image.png
- 发生了 4 次用户态与内核态的上下文切换
- 发生了 4 次数据拷贝(
C++拷贝是4次,但对于Java语言其实是拷贝6次,从内核到用户态的拷贝的时候只是拷贝到直接内存(堆外内存),还有一步是从堆外内存拷贝到堆内内存中,同理用户态到内核态的拷贝也需要经历堆外内存
)
文件大的话会循环进行更多次操作
零拷贝--mmap + write
mmap + write文件传输过程
image.png
- 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区(PageCache)
- 仍然需要 4 次上下文切换
- 拷贝少了一次
零拷贝--sendfile
sendfile文件传输过程
image.png
- 这样就只有 2 次上下文切换,和 3 次数据拷贝
零拷贝--网卡支持 SG-DMA
image.png- 零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的
- 只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运
@Overridepublic
long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}
Java中通过transferTo来实现零拷贝,如果 Linux 系统支持 sendfile() 系统调用,那么 transferTo() 实际上最后就会使用到 sendfile() 系统调用函数
文件传输过程,其中第一步都是先需要先把磁盘文件数据拷贝「内核缓冲区」里,这个「内核缓冲区」实际上是磁盘高速缓存(PageCache)读取磁盘数据的时候,需要找到数据所在的位置,但是对于机械磁盘来说,就是通过磁头旋转到数据所在的扇区,再开始「顺序」读取数据,但是旋转磁头这个物理动作是非常耗时的,为了降低它的影响,PageCache 使用了预读功能(在传输大文件(GB 级别的文件)的时候,PageCache 会不起作用,那就白白浪费 DMA 多做的一次数据拷贝,造成性能的降低,即使使用了 PageCache 的零拷贝也会损失性能,PageCache 由于长时间被大文件占据,其他「热点」的小文件可能就无法充分使用到 PageCache,于是这样磁盘读写的性能就会下降了)在高并发的场景下,针对大文件的传输的方式,应该使用「异步 I/O + 直接 I/O」来替代零拷贝技术
用户线程
什么是用户线程
- 用户空间运行线程库,任何应用程序都可以通过使用线程库被设计成多线程程序。线程库是用于用户级线程管理的一个例程包,它提供多线程应用程序的开发和运行的支撑环境,包含:用于创建和销毁线程的代码、在线程间传递数据和消息的代码、调度线程执行的代码以及保存和恢复线程上下文的代码。
- 所以线程的创建、消息传递、线程调度、保存/恢复上下文都由线程库来完成。内核感知不到多线程的存在。内核继续以进程为调度单位,并且给该进程指定一个执行状态(就绪、运行、阻塞等)
用户线程:使用Java,开启一个线程,在这个线程里实现线程的切换(不太准确,其实Java开启一个线程,就是创建了一个用户线程,同时创建了一个内核线程),所以对于内核来说感知不到用户线程的存在,如果多个用户线程切换到某个线程在执行过程中IO阻塞,此时主线程(进程)就进入阻塞态,那么所有的用户现场都会阻塞
协程就是用户态的线程(好像在java线程里面自己实现的线程)
用户线程的特点:创建销毁快,支持大量的用户线程(创建用户线程比内核线程需要的空间要小的多),但是不能利用CPU多核,同时需要自己实现阻塞调度,否则会影响其他用户线程的执行
内核线程
什么是内核线程
- 线程管理的所有工作(创建和撤销)由操作系统内核完成
- 操作系统内核提供一个应用程序设计接口API,供开发者使用KLT
进程中的一个线程被阻塞,内核能调度同一进程的其他线程(就绪态)占有处理器运行,可以利用多核,但是线程数量不能过多
用户和内核线程模型
1:1
image.pngn:1
image.pngn:m
image.pngjava使用的线程模型
-
JDK 1.8 Thread.java 中 Thread#start 方法的实现,实际上是通过 Native 调用 start0 方法实现的;在 Linux 下, JVM Thread 的实现是基于 pthread_create 实现的,而 pthread_create 实际上是调用了 clone() 完成系统调用创建线程的。
-
所以,目前 Java 在 Linux 操作系统下采用的是用户线程加轻量级线程,一个用户线程映射到一个内核线程,即 1:1 线程模型。由于线程是通过内核调度,从一个线程切换到另一个线程就涉及到了上下文切换(对线程的操作也会进行用户和内核态的切换,如thread.yeld())
-
Linux的线程模型是1:1模型,而 Go 语言是使用了 N:M 线程模型实现了自己的调度器,它在 N 个内核线程上多路复用(或调度)M 个协程,协程的上下文切换是在用户态由协程调度器完成的,因此不需要陷入内核,相比之下,这个代价就很小了。
用户和内核线程如何关联
- 程序是存储在内存中的指令,用户态线程是可以准备好程序让内核态线程执行的。
- 用户线程把命令准备好,放入相应的指令空间中,内核线程会从里面取来进行执行
- 执行1+1只需要用户线程,在用户空间执行就行,把指令考入相应的内存中,内核线程获取时间片后取指执行