1.用户空间和内核空间
虽然每个进程都有自己的虚拟地址空间,但是为了进一步保障系统运行安全,虚拟地址空间被划分为用户空间和内核空间。操作系统运行在内核空间,用户程序运行在用户空间。内核空间由所有进程的地址空间共享,但是用户程序不能直接访问内核空间。
操作系统保存的进程控制块PCB自然是在内核空间,这里除了页目录以外还可以找到很多重要的内容,例如进程和父进程的ID,状态,打开文件句柄表等等。
2.线程是怎样的存在?
线程就是进程中的执行体,它要有指定的执行入口,通常是某个函数的指令入口,线程执行时要使用从进程虚拟地址空间中分配的栈空间来存储数据,这被称为“线程栈”。在创建线程时,操作系统会在用户空间和内核空间分别分配两段栈,就是通常所说的用户栈和内核栈。
线程切换到内核态执行时会使用内核栈,为的是不允许用户代码对其进行修改以保证安全。操作系统也会记录每个线程的控制信息,例如执行入口,线程栈,线程ID等等,在windows中线程控制块对应TCB,在PCB中可以找到进程拥有的线程列表,同一个进程内的线程会共享进程的地址空间和句柄表等资源。
而在Linux中只用了一个task_struct结构体,进程在创建子进程时会指定它和自己使用同一套地址空间和句柄表等资源,用这种方法来实现多线程的效果。
如果接下来要执行进程A中的线程a1,CPU的指令指针寄存器就会指向线程的执行入口,当前指向用户空间的程序指令,所以栈基和栈指针寄存器会记录用户栈的位置,可以看到程序执行时,CPU面向的是某个线程,所以才说线程是操作系统执行与调度的基本单位。一个进程中至少要有一个线程,它要从这个线程开始执行,这被称为它的主线程,可以认为主线程是进程中的第一个线程,一般是由父进程或操作系统创建的,而进程中的其他线程一般都是由主线程创建的。
3.系统调用是怎么实现的?
线程中发生函数调用时就会在线程栈中分配函数调用栈,而虚拟内存分配,文件io操作,网络读写等很多功能,都是由操作系统来实现,再向用户程序暴露接口,所以线程免不了要调用操作系统提供的系统服务,也就是少不了进行系统调用。
CPU中会有一个特权级标志,用于记录当前程序执行在用户态还是内核态,只有标记为内核态时才可以访问内核空间。而目前线程a1处在用户态,还不能访问内核空间,所以系统调用发生时就得切换到内核态。使用线程的内核栈,执行内核空间的系统函数,这被称为从“用户态”切换到“内核态”。
最初系统调用是通过软中断触发的,所谓软中断,简单来说就是通过指令模拟中断。与软中断对应的就是硬件中断,操作系统会按照CPU硬件要求,在内存中存一张中断向量表,用来把各个中断标号映射到相应的处理程序,例如在Linux系统中,系统调用中断对应的编号为0x80,对应的处理程序就是用来派发系统调用(os提供的函数)的,为什么说派发系统调用呢?因为操作系统提供了数百个系统调用,不能为每一个都分配一个中断号,所以操作系统又实现了一张系统调用表,用于通过系统调用编号,找到对应的系统函数入口。
所以用户程序这里,会把要调用的系统函数编号存入特定寄存器,通过寄存器或用户栈来传递其他所需参数,然后用int 0x80来触发系统调用中断。而硬件层面,CPU有一个中断控制器,它负责接收中断信号,切换到内核态。保存用户态执行现场,一部分寄存器的值会通过硬件机制保存起来,还有一部分通用寄存器的值会被压入内核栈内,然后去中断向量表这里查询0x80对应的系统调用派发程序入口,而系统调用的派发程序会根据指定的系统调用编号,去系统调用表这里查询对应的系统调用入口(addr2)并执行。
后来为了优化系统调用的性能,改为通过特殊指令触发系统调用,例如x86的sysenter,和amd64平台下的syscall。当cpu执行到这些指令时,就会进入内核态,从专用寄存器拿到派发入口地址,省去了查询中断向量表的过程。
等系统调用结束后,再利用之前保存的信息,恢复线程在用户态的执行现场,继续执行后面的指令,这样就完成了一次系统调用。
ok,对线程的执行有一个大致的了解。接下来就可以看看线程切换是怎么回事了。
4.线程切换和进程切换
我们知道现代操作系统中,CPU的执行权被划分为不同的时间片,只有获得CPU时间片的程序才能运行,由于时间片很短,所以用户感觉不到程序的切换过程,又因为CPU执行的很快,所以即使很短的时间片,也足够它执行很多很多的指令了。
一个线程获得的时间片用完时(时间片轮转只是触发线程或进程切换的多种场景之一),CPU硬件时钟会触发一次时钟中断,对应的中断处理程序,会从已经就绪的线程中挑选一个来执行,我们暂不展开线程调度的问题,只关注切换过程
例如接下来要从线程a1切换到线程a2,而这两个线程属于进程A,那么就只涉及到线程切换,只需要把线程a1的执行现场保存起来,后续再把指令指针,栈指针这些寄存器的值修改为线程a2的信息,修改一下内存中调度相关的数据结构,一次同进程间的线程切换就算完成了。
等到线程a1再次获得时间片时,会根据之前保存的信息,恢复到切换前的执行现场(上下文),继续完成它的任务
假如线程a1要切换到另一个进程B的线程b1,那么除了线程切换外还要切换进程。CPU这里保存的页目录地址要切换到进程B。所以进程切换与线程切换的区别就是进程切换会导致地址空间等进程资源发生变化,会导致TLB缓存失效,切换代价相应的会更大