栈与调用惯例

一、栈介绍

栈保存了一个函数调用所需要的维护信息,常被称为:堆栈帧(Stack Frame)或活动记录。堆栈帧一般包括如下内容:

  • 函数的返回地址和参数
  • 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • 保存的上下文:包括在函数调用前后需要保持不变的寄存器
查看更多

fork的原理与实现

一、fork 的实现

fork 利用老进程克隆出一个新进程并使新进程执行,新进程之所以能够执行,本质上是它具备程序体,这其中包括代码和数据等资源。因此 fork 就是把某个进程的全部资源复制了一份,然后让处理器的 cs:eip 寄存器指向新进程的指令部分。

因此 fork 要分两步,先复制进程资源,然后跳过去执行。

如下列举出我们的操作系统中进程的资源:

  • 进程的 pcb,即 task_struct
查看更多

系统调用概念

一、Linux 系统调用

Linux 系统调用是用中断门来实现的,通过软中断指令 int 来主动发起中断信息。Linux 只占用一个中断向量号,即 0x80,处理器执行指令 int 0x80 时便触发了系统调用。为了让用户程序可以通过这一个中断门调用多种系统功能,在系统调用之前,Linux 在寄存器 eax 中写入子功能号。当用户程序通过 int 0x80 进行系统调用时,对应的中断处理例程会根据 eax 的值来判断用户进程申请那种系统调用。

我们来看 syscall 的 man 手册

1
2
3
4
syscall - indirect system call
#define SYS_getpid __NR_getpid
#define __NR_getpid 172
__SYSCALL(__NR_getpid, sys_getpid)

这里的 syscall 是间接的(indirect system call)。就是 C 库函数

查看更多

wait 和 exit 的原理

一、wait 和 exit 的作用

1. exit 浅析

exit 时进程主动退出,结束运行。main 函数执行结束后程序流程会回到 C 运行时库,C 运行库的结束代码处会调用 exit。结束程序运行始终是通过主动调用 exit 系统调用实现的,因为这是唯一让系统重新拿回处理器控制权的机会。

exit 由子进程调用,表面功能是使子进程结束运行并传递返回值给内核,本质上是内核会将进程除 pcb 以外的所有资源都回收。

2. wait 浅析

wait 一是可以使调用进程阻塞,二是可以获得子进程的返回值。

如果调用进程没有子进程,那么 wait 返回 -1。如果有子进程,调用进程将被阻塞,内核再去遍历其所有子进程,查找那个子进程退出了,并将子进程退出时的返回值传递给父进程,随后将父进程唤醒。

wait 可以用来同步父子进程,协调父子进程的执行次序。

查看更多

概念

  1. 什么是任务调度器?

任务调度器就是操作系统中用于把任务轮流调度上处理器运行的一个软件模块,他是操作系统的一部分。调度器在内核中维护一个任务表(也称为进程表、线程表或调度表),然后按照一定的算法,从任务表中选择一个任务,然后把该任务放到处理器上执行,当任务运行的时间片到期后,再从任务表中找另外一个任务放到处理器上执行。

查看更多

实现线程的方式

实现线程的两种方式 — 内核或用户进程

线程的实现:

  • 由操作系统原生支持,用户进程通过系统调用使用线程。线程在 0 特权级的内核空间中实现(并不是线程所运行的代码也必须是 0 特权级的内核级代码,也可以是 3 特权级的用户级代码)
  • 进程自己实现线程,线程在 3 特权级的用户空间实现。通常情况下,标准库提供了用户级线程库,我们直接调用即可
查看更多

多线程调度

如何得到当前线程的 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)。

查看更多

进程实现

一、概念

1. 任务状态段 TSS

任何寄存器中的内容才是任务的最新状态。采取轮流使用 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。

<1>. TSS 描述符结构

TSS 本质上是一片存储数据的内存区域,使用 TSS 描述符结构来描述,TSS 描述符也要在 GDT 中注册,这样才可以找到。

查看更多

协程的实现

一、为什么要有协程

对于操作系统来说,线程是 CPU 调度的最小单元,进程是资源管理的最小单元。

在多核场景下,如果是 IO 密集型场景,就算开多个线程来处理,也未必能提升 CPU 的利用率,反而会增加线程切换的开销。另外,多线程之间假如存在临界区或者共享数据,那么同步的开销也可能比较大。

那么协程可以解决如上的问题。

协程是一种比线程更加轻量化的存在,一个线程中可以拥有多个协程。协程的调度由用户实现的调度器来控制。协程拥有自己的寄存器上下文和栈。协程在做调度切换时,一般会将寄存器上下文和栈保存到某个指定地方,在切回来时,恢复先前保存的寄存器上下文和栈,这个过程都是在用户空间完成的,不需要内核的参与。所以切换过程是非常快的。

对于 CPU 密集型场景,使用协程可能还会增加调度切换的开销。

查看更多