CPU
cpu是计算机的中央运算单元,从内存里面读取一条一条的代码指令,然后根据指令来执行运算(加,减,乘,除,复制数据等)。
CPU在运算的过程中一些数据存放在CPU的寄存器和内存里面。
CPU里面有各种寄存器,指令指针寄存器存放当前执行到那条代码指令(写完程序后被编译器编译成二进制指令代码)。
内核与虚拟内存
启动代码后运行OS内核,内核里也有线程,这个我们把它叫做内核态。内核启动以后,内核将物理内存管理起来,内核提供虚拟内存管理机制给每个进程内存服务。
每个进程都有自己的虚拟内存空间,这里的空间只是一个数字空间,没有划分实际的物理内存。多个进程内存都是独立的相互不影响,物理内存只有一个,多个进程不会因为直接使用物理内存而冲突。
物理内存管理
进程需要内存的时候,OS分配一块虚拟内存,然后OS在从自己管理的物理内存里面分配出来物理内存页,然后通过一个MMU的单元,将分配的虚拟内存与物理内存页映射起来。读写虚拟内存地址最终通过映射来使用物理内存地址,这样每个进程之间的内存是独立的,安全的。每个进程会把虚拟内存空间分成4个段(代码段, 数据端,堆,栈)。
- 代码段:用来存放进程的代码指令。
- 数据端:用来存放全局变量的内存。
- 堆:调用os的malloc/free 来动态分配的内存。
- 栈:用来存放局部变量,函数参数,函数调用与跳转。
进程与线程关系
每个进程相当于一个容器,所有代码里面需要的资源和机制都在进程里面。线程是OS独立调度执行的单元,OS调度执行的单位就是线程,线程需要以进程作为容器和使用进程相关的环境。
- 每个线程共享进程的代码段内存空间,所以我们编写多线程代码的时候,可以在任何线程调用任何函数。
- 每个线程共享进程的数据段内存空间,所以我们编写多线程代码的时候,可以在任何线程访问全局变量。
- 每个线程共享进程的堆,所以我们编写多线程代码的时候,可以在一个线程访问另外一个线程new/malloc出来的内存对象。
- 每个线程都有自己的栈的空间,所以可以独立调用执行函数(参数,局部变量,函数跳转)相互之间不受影响。
OS调度线程
CPU一般会有多个核心,每个核心都调度一个线程执行,最多同时可调度几个线程。
OS的功能就是要在合适的时候分配CPU核心来调度合适的线程。为了能实现多任务并发,OS不允许一个OS核心长期固定调度一个线程。
OS会根据线程的优先级分配每次调度最多执行的时间片,这个时间一到,无论如何都要重新调度一次线程(也许还是调度到这个线程)。
除了时间片以外,线程会等待某些条件(磁盘读取文件,网卡发送完数据,线程休眠, 等待用户操作)这样也会把这个线程挂起,OS会重新找一个新的线程继续执行,直到挂起的这个线程的条件满足了,重新把这个线程放到可调度队列里面,这个线程又有机会被OS调度CPU核心来执行。
每个线程“随时随地”都可能被OS中断执行,并调度到其它的线程执行。这样每个线程都会有一个运行时的环境(运行时CPU的每个寄存器的值、栈独立。栈的内存数据不会变。数据段、堆共用,可能调度回来会变)。
调度流程
当OS要把某个CPU核心调度出去给其它线程的时候,首先会把当前线程的运行环境(寄存器的值等)保存到内存,然后调度到其它线程,等再次调度回来的时候,再把原来保存到内存的寄存器的值,再设置会CPU核心的寄存器里面,这样就回到了调度出去之前的进度。
因为多线程之间共用了代码段(代码段只读,不会改),数据段(全局变量调度回来后,可能被其它线程篡改,不是调度之前的那个值了),堆(调度回来后,动态内存分配的对象内存数据可能被其它线程出篡改),调度回来后,栈上的数据是不变的,因为每个线程都有自己的栈空间。
线程调度的开销就是:保存上下文执行环境,内核态运行算法决定接下来调度那个线程,切换这个线程的上下文环境。