任务切换:进程切换,线程切换的区别

开始之前,我们暂时先简单的定义一个概念,CPU任务,是指在CPU中运行的一段程序。根据任务的大小,粒度不同,一个任务可以是一个进程,一个线程,或者是一个中断处理程序等。

一台冯诺依曼体系结构的计算机是有这五个部门构成:运算器、控制器、存储器、输入设备、输出设备。其中运算器、控制器组成CPU。而输入输出设备和任务切换基本没什么关系,所以可以简单的认为任务切换主要包括CPU切换和内存切换(地址空间)两部分。

而在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好CPU 寄存器程序计数器。CPU 寄存器和程序计数器就是 CPU 上下文,因为它们是任务运行时必须的依赖环境。

任务切换的本质

对于CPU来讲,任务切换的本质就是CPU状态的切换,就是指CPU从一个任务中的状态切换到另一个任务的状态。再具体一点,CPU状态指的就是CPU寄存器和程序计数器的状态。而寄存器和程序计数器又可以称为CPU上下文。

对于任务切换的方案,一个最简单的方法就是:

1,保存当前状态,把当前CPU的状态保存到一个地方。

2,重新加载CPU状态,把新任务的状态加载进CPU运行。

假设计算机运行着A,B两个任务,当前运行A任务,准备要切换到B任务,然后再切换回A任务,这个过程是这样:

1,先把CPU当前的状态,也就是把任务A的状态保存到一个指定的内存地址,这个地址我们叫它C;

2,假设任务B的状态保存在内存地址D中,那我们就去内存地址D中获取任务B的状态并把它加载进CPU中运行,到此第一次任务切换就已经完成了。

3,当再次切换会任务A的时候,那就先把当前任务B的状态保存到内存地址D中,然后从内存地址C中获取任务A的状态然后重新加载到CPU中运行,这时第二次任务切换已经完成,CPU有恢复到了任务A的状态。

关于保存任务状态的那一块内存,在x86架构中叫做TSS任务状态段。

CPU 上下文切换

CPU上下文切换是指把当前的 CPU 上下文( 寄存器和程序计数器)保存起来,然后加载下一个要运行的任务的上下文到CPU寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

对于CPU上下文切换,我们先来看一下英特尔x86提供的硬件解决方案,然后再看Linux的方案。

英特尔进程切换方案

在 x86 体系结构中,提供了一种以硬件的方式进行CPU上下文切换的模式,对于每个任务(进程),x86 希望在内存里面维护一个 TSS(Task State Segment,任务状态段)结构。在里面保存所有的寄存器。另外,还有一个特殊的寄存器 TR(Task Register,任务寄存器),指向某个进程的 TSS。更改 TR 的值,将会触发硬件电路保存 CPU 所有寄存器的值到当前进程的 TSS 中,然后从新进程的 TSS 中读出所有寄存器值,加载到 CPU 对应的寄存器中。32 位的 TSS 结构如下图所示。

图片来自 Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes

 英特尔的方案有个缺点:每次切换都要先保存所有的寄存器状态,加载也是全量加载寄存器。但是实际上进程切换的时候,根本没必要每个寄存器都切换,因为并不是所有的寄存器都和进程切换相关。

Linux进程切换方案

Linux 操作系统在系统初始化的时候,在cpu_init 中会给每一个 CPU 关联一个 TSS,然后将 TR 指向这个 TSS,然后在操作系统的运行过程中,TR 寄存器是不会切换的,永远指向这个 TSS。


void cpu_init(void)
{
  int cpu = smp_processor_id();
  struct task_struct *curr = current;
  struct tss_struct *t = &per_cpu(cpu_tss, cpu);
    ......
    load_sp0(t, thread);
  set_tss_desc(cpu, t);
  load_TR_desc();
    ......
}

TSS 用数据结构 tss_struct 表示,在 x86_hw_tss 中可以看到和上图相应的结构。

struct tss_struct {
  /*
   * The hardware state:
   */
  struct x86_hw_tss  x86_tss;
  unsigned long    io_bitmap[IO_BITMAP_LONGS + 1];
} 

在 Linux 中,真正参与进程切换的寄存器不多,主要的就是栈顶寄存器。于是,在 进程的task_struct 里面,用成员变量 thread来保留了要切换进程的时候需要修改的寄存器。 进行进程切换的时候,就将某个进程的 thread_struct 里面的寄存器的值,写入到 CPU 的 TR 指向的 tss_struct,对于 CPU 来讲,这就算是完成了切换。

地址空间切换 

我们知道不同的进程的虚拟地址空间是完全独立的,地址空间分为内核地址空间和用户地址空间。对于内核空间,不同进程是共用同一个内核空间,只有用户空间才是各用各的。所以在进程切换时,地址空间切换的时候只需要切换用户空间就可以了。

程序要访问物理内存,需要通过MMU内存管理单元来将虚拟地址转换成物理地址, 而MMU需要通过分页机制和页表来完成虚拟地址到物理地址的转换。每个进程都有独立的地址空间,为了这个进程独立完成映射,这就需要每个进程都有独立的进程页表。也就是说进程切换的时候需要进行页表切换。这里需要引入一个寄存器,在英特尔CPU中有个CR3寄存器,这个寄存器又叫PDBR(Page Directory Base Register)页表基址寄存器。顾名思义,这个寄存器指向的是进程的顶级PGD(Page Global Directory页面目录),注意,这个寄存器保存的是真实的物理地址。每个任务的TSS段就有自己的CR3的物理地址。每次进行任务切换的时候,CR3的内容都会被替换改为新任务的CR3域中的物理地址。这里我们已经可以看到,页表切换其实也是通过寄存器切换来完成的。

当某个进程被调度到某个 CPU 上运行的时候,要调用 context_switch 进行上下文切换。对于内存方面的切换会调用 switch_mm_irqs_off,这里面会调用  load_new_mm_cr3来重新加载CR3寄存器。 

至此,进程切换就可以告一段落了。当然对于地址空间的切换,还有一些细节没讲,比如内核栈和用户栈的切换没有讲,这些要放到另一篇文章来讲。

线程切换 

当然这里的线程指的是同一个进程中的线程。每个进程都有一个独立的虚拟地址空间,进程内的线程都是共享同一个地址空间。所以很明了,线程切换不需要进行地址空间切换,这个就是线程切换和进程切换的最大的区别。那么这个区别会带来什么影响呢?

上面讲到,任务切换的时候要把新任务的一些寄存器加载到CPU,其中就保持CR3寄存器。每当用MOV指令重置CR3的值时,会导致分页机制高速缓冲区的内容无效。在任务切换时,CR3要被改变,但是如果新任务中CR3的值与原任务中CR3的值相同,那么处理器不刷新分页高速缓存,以便当任务共享页表时有较快的执行速度。

我们知道,要访问物理地址需要去查页表,而页表往往是多级的,比如说64位的Linux操作系统默认是采用4级页表,也就是说这个过程需要多次访问内存才能找到对应的物理地址。为了加快页表查找,操作系统会把一些页表项加入到一个高速缓存--TLB(translation Lookaside Buffer)。只有在TLB中找不到对应的页表项,才会到内存中查询页表,这样就减少了由于页表查询导致的处理器性能下降。但是在进程切换的时候,当我们进行地址空间切换后(修改CR3寄存器),TLB高速缓存中的数据就会失效。缓存失效就会导致命中率降低(cache miss),那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢。同样的道理,除了TLB,由于地址空间切换也会导致CPU其它的高速缓存(L1,L2等)失效。

而线程切换并不需要切换地址空间,所以就不会有缓存失效这个问题。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值