线程与进程

没创建线程的进程 == 完全独享整个进程资源的单线程。

  • 线程不能看做独立应用,而进程可看做独立应用
  • 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
  • 线程没有独立的地址空间,多进程的程序比多线程程序健壮
  • 进程的切换比线程的切换开销大

虚拟地址映射

一、物理地址空间是什么

理解虚拟地址空间还得从物理地址空间开始说起。我们知道内存就像一个数组,每个存储单元被分配了一个地址,这个地址就是物理地址,所有物理地址构成的集合就是物理地址空间。物理地址也就是真实的地址,对应真实的那个内存条。

二、虚拟地址空间是什么

引入虚拟地址之后,对于每一个进程,操作系统提供一种假象,让每个进程感觉自己拥有一个巨大的连续的内存可以使用,这个虚拟的空间甚至还可以比内存的容量还大。这个“假象”就是虚拟地址空间。虚拟地址是面向每个进程的,只是一个“假象”罢了。CPU使用虚拟地址向内存寻址,通过专用的内存管理单元(MMU)硬件把虚拟地址转换为真实的物理地址。

用户态与内核态

image.png

32位系统中,一个linux进程最大可以申请4GB地址空间,其中3g被划分为用户空间,1g被划分为内核空间。内核空间的数据是进程间共享的(所有进程的内核空间映射的内存地址是一样的)。 :::success

  • 当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。
  • 当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。
  • Ring3状态不能访问Ring0的地址空间,包括代码和数据(运行在用户态下的程序不能直接访问操作系统内核数据结构和程序); ::: 用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,然后进入3G-4G中的内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据。

    用户态到内核态的切换

    以下的三种情况会导致用户态到内核态的切换

  • 系统调用 (用户进程主动发起)

  • 异常(被动)
  • 外围设备的中断(被动)

java的线程运行时是内核态还是用户态?

其实站在java程序员的角度只需要关注系统调用,因为系统调用可以认为是用户进程主动发起的,比如调用线程的park()方法会对应到一个os的一个函数,从而使当前线程进入了内核态;再比如遇到synchronized关键字如果是重量锁则会调用pthread_mutex_lock()这样我们的线程也会切换到内核态;当执行完系统调用切换到用户态;


CPU 上下文

  1. 在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好CPU 寄存器和程序计数器<br />其实和spring上下文差不多,CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境。<br />CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。<br />程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。

什么是 CPU 上下文切换

就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行

CPU 上下文切换的类型

CPU 上下文切换有四种

  • 进程内部发生了系统调用,从而导致用户态与内核态之间的切换
  • 进程与进程之间的切换

  • 进程内部线程与线程的切换

  • 线程与外部线程之间的切换

根据任务的不同,和java并发编程相关的我们只关心以下两种类型: 进程上下文切换 ,线程上下文切换

一、进程上下文切换

1、进程上下文切换之系统调用

进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。

在这个过程中就发生了 CPU 上下文切换,整个过程是这样的:

  1. 保存 CPU 寄存器里原来用户态的指令位
  2. 为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。
  3. 跳转到内核态运行内核任务。
  4. 当系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。

所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这跟我们通常所说的进程上下文切换是不一样的:进程上下文切换,是指从一个进程切换到另一个进程运行;而系统调用过程中一直是同一个进程在运行。所以,系统调用过程通常称为特权模式切换,而不是上下文切换。系统调用属于同进程内的 CPU 上下
文切换

2、真正的进程上下文切换和系统调用有什么区别呢?

进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
因此,进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
发生进程上下文切换的场景:

  • 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待CPU 的进程运行。
  • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
  • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
  • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行

    二、线程上下文切换

    特点以及场景

  1. 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
    2. 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以切换过程就跟进程上下文切换是一样。
    2. 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源
    就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

:::info cas不会升级内核态,他仅仅是处理器提供的一个指令,速度非常快 :::


转自:https://www.yuque.com/wanghuaihoho/gobbon/aov0w8