什么是用户态和内核态?

用户态和内核态是操作系统的两种运行状态。

1、用户态和内核态

内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。0 - 4G 范围的虚拟空间地址都可以操作,尤其是对 3-4G 范围的高位虚拟空间地址必须由内核态去操作

用户态:用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。 内核必须提供一组通用的访问接口,这些接口就叫系统调用(下文会介绍)。用户态只能操作 0 - 3G 范围的低位虚拟空间地址

补充:3G - 4G 部分大家是共享的(指所有进程的内核态逻辑地址是共享同一块内存地址),是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据
每个进程的 4G 虚拟空间地址,高位 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用,换句话说就是, 高位 1G 的内核空间是被所有进程共享的!
我们通过指令集权限区分用户态和内核态,还限制了内存资源的使用,操作系统为用户态与内核态划分了两块内存空间,给它们对应的指令集使用

2、形态的切换相关

2.1、什么样的操作会切换形态

从用户态到内核态切换可以通过三种方式,或者说会导致从用户态切换到内核态的操作:

系统调用:其实系统调用本身就是中断,但是软件中断,跟硬中断不同。系统调用机制是使用了操作系统为用户特别开放的一个中断来实现,如 Linux 的 int 80h 中断。因为部分操作是软件无法提供的,是系统提供的操作接口给软件去调用,这个时候会切换到内核态。

系统调用有哪些呢?
进程控制相关: process control。指令:exit, fork
文件管理相关: file system control。指令:chmod,chown,open
设备、IO 操作相关:device control。指令:read,write
信息相关:比如说要获取 cpu相关的信息等。指令:getcpu
通信相关:比如说进程间的通信等。比如说管道就是一个系统调用。指令:pipe, mmap。

异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,会触发由当前运行进程切换到处理此异常的内核相关进程中

外围设备中断:外围设备完成用户请求的操作之后,会向CPU发出中断信号,这时CPU会转去处理对应的中断处理程序。

2.2、切换的代价是什么

当发生用户态到内核态的切换时,会发生如下过程(本质上是从“用户程序”切换到“内核程序”)

  • 设置处理器至内核态。
  • 保存当前寄存器(栈指针、程序计数器、通用寄存器)。
  • 将栈指针设置指向内核栈地址。
  • 将程序计数器设置为一个事先约定的地址上,该地址上存放的是系统调用处理程序的起始地址。

而之后从内核态返回用户态时,又会进行类似的工作。

2.3、如何避免频繁切换

减少线程、进程切换
可以多使用"协程"
用户进程缓冲区
内核缓冲区

3、实际场景有什么

3.1、使用锁和无锁操作

因为线程的切换会导致用户态和内核态之间的切换,所以减少线程切换也会减少用户态和内核态之间的切换。那么如何减少线程切换呢?

  • 无锁并发编程。多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换。(为什么加锁和释放锁会导致上下文切换,看文末的补充解释)
  • CAS算法。使用CAS避免加锁,避免阻塞线程
  • 使用最少的线程。避免创建不需要的线程
  • 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

3.2、架构设计

这里比较抽象就不详细举例了
比如:

Disruptor 高性能队列设计 论文是《LMAX Disruptor: High performance alternative to bounded queues for exchanging data between concurrent threads》

4 补充

为什么加锁和释放锁会导致上下文切换

比如 Java 的 Synchronized 同步所是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock 来实现的。但是由于使用 Mutex Lock 需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。