一、栈介绍
栈保存了一个函数调用所需要的维护信息,常被称为:堆栈帧(Stack Frame
)或活动记录。堆栈帧一般包括如下内容:
- 函数的返回地址和参数
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
- 保存的上下文:包括在函数调用前后需要保持不变的寄存器
Linux 系统调用是用中断门来实现的,通过软中断指令 int 来主动发起中断信息。Linux 只占用一个中断向量号,即 0x80
,处理器执行指令 int 0x80
时便触发了系统调用。为了让用户程序可以通过这一个中断门调用多种系统功能,在系统调用之前,Linux 在寄存器 eax 中写入子功能号。当用户程序通过 int 0x80
进行系统调用时,对应的中断处理例程会根据 eax 的值来判断用户进程申请那种系统调用。
我们来看 syscall 的 man 手册
1 | syscall - indirect system call |
这里的 syscall 是间接的(indirect system call
)。就是 C 库函数
exit 时进程主动退出,结束运行。main 函数执行结束后程序流程会回到 C 运行时库,C 运行库的结束代码处会调用 exit。结束程序运行始终是通过主动调用 exit 系统调用实现的,因为这是唯一让系统重新拿回处理器控制权的机会。
exit 由子进程调用,表面功能是使子进程结束运行并传递返回值给内核,本质上是内核会将进程除 pcb 以外的所有资源都回收。
wait 一是可以使调用进程阻塞,二是可以获得子进程的返回值。
如果调用进程没有子进程,那么 wait 返回 -1。如果有子进程,调用进程将被阻塞,内核再去遍历其所有子进程,查找那个子进程退出了,并将子进程退出时的返回值传递给父进程,随后将父进程唤醒。
wait 可以用来同步父子进程,协调父子进程的执行次序。
如何得到当前线程的 PCB 地址?各个线程所用的 0 级栈都是在自己的 PCB 中,因此取当前栈指针的高 20 位作为当前运行线程的 PCB。因为 PCB 有一页,PCB 的地址都对齐到 4KB 上。
线程执行用户线程函数时,一定要开中断?他是任务调度的保证,我们的任务调度机制基于时钟中断,由时钟中断这种 “不可抗力” 来中断所有任务的执行,借此将控制权交到内核手中,由内核的任务调度器考虑将处理器使用权发放到某个任务的手中,下次中断再发生时,权利将再被回收。这样保证了操作系统不会被 “架空”,而且保证所有任务都有运行的机会。什么时候关的中断呢?线程首次运行是由时钟中断处理函数调用任务调度器完成的,进入中断后处理器会自动关中断,因此在执行用户线程函数前要打开中断。
调度器的主要任务就是读写就绪队列,增删里面的节点,节点是线程 PCB 中的 general_tag,相当于线程的 PCB。
线程每次在处理器上的执行时间是由其 ticks 决定的,我们在初始化线程的时候,已经将线程 PCB 中的 ticks 赋值为 prio,优先级越高,ticks 越大。每发生一次时钟中断,时钟中断的处理程序便将当前运行的线程的 ticks 减一。当 ticks 为 0 时,时钟的中断处理程序调用调度器 schedule,也就是把当前线程换下处理器,让调度器选择另一个线程上处理器。
我们的调度机制是 “轮询调度”,称为 RR(Round-Robin Scheduling)。
任何寄存器中的内容才是任务的最新状态。采取轮流使用 CPU 的方式运行多任务,当前任务在被换下 CPU 时,任务的最新状态,也就是寄存器中的内容应该找个地方保存起来,以便下次重新将此任务调度到 CPU 上时可以恢复此任务的最新状态,这样任务才能继续执行。
Intel 的建议是给每个任务 “关联” 一个任务状态段,就是 TSS(Task State Segment),用它来表示任务。TSS 是由程序员提供,也就是说是程序员为任务单独定义的一个结构体遍历。TSS 由 CPU 来维护,也就是说 CPU 自动用此结构体变量保存任务的状态(任务的上下文环境、寄存器组的值)和自动从此结构体变量中载入任务的状态。当加载新任务时,CPU 自动把当前任务(旧任务)的状态存入当前任务的 TSS,然后将新任务 TSS 中的数据载入到对应的寄存器中,这就实现了任务切换。TSS 就是任务的代表,CPU 用不同的 TSS 区分不同的任务,因此任务切换的本质就是 TSS 的换来换去。
在 CPU 中有一个专门存储 TSS 信息的寄存器,称为 TR 寄存器,他始终指向当前正在运行的任务。因此在 CPU 眼里,任务切换的实质就是 TR 寄存器指向不同的 TSS。
TSS 本质上是一片存储数据的内存区域,使用 TSS 描述符结构来描述,TSS 描述符也要在 GDT 中注册,这样才可以找到。
对于操作系统来说,线程是 CPU 调度的最小单元,进程是资源管理的最小单元。
在多核场景下,如果是 IO 密集型场景,就算开多个线程来处理,也未必能提升 CPU 的利用率,反而会增加线程切换的开销。另外,多线程之间假如存在临界区或者共享数据,那么同步的开销也可能比较大。
那么协程可以解决如上的问题。
协程是一种比线程更加轻量化的存在,一个线程中可以拥有多个协程。协程的调度由用户实现的调度器来控制。协程拥有自己的寄存器上下文和栈。协程在做调度切换时,一般会将寄存器上下文和栈保存到某个指定地方,在切回来时,恢复先前保存的寄存器上下文和栈,这个过程都是在用户空间完成的,不需要内核的参与。所以切换过程是非常快的。
对于 CPU 密集型场景,使用协程可能还会增加调度切换的开销。