操作系统概念学习笔记

tag: #ComputerScience/basic

课程

Stanford CS140使用操作系统概念

知识联系

最基础:[[《深入理解计算机系统》笔记]]
操作系统实践阶段知识:[[操作系统设计与原理笔记]]]
具有联想: [[计算机组成原理]]

基础

操作系统发展史

原始操作系统

image-20230820150441945

在原始操作系统中,程序更多的是与硬件进行绑定,是一个无保护的标准服务库(为了方便用户或开发者使用而提供的一系列标准服务、函数或API)。

系统一次只能运行一个程序

多任务处理

image-20230820152546823

系统可以同时运行多个进程,当一个进程阻塞(等待硬盘,网络或用户输入)block时,运行另一个进程。

系统对进程操作不当会出现的问题:

  1. 死锁,一个进程永远占据CPU资源
  2. 使其他进程的私有资源遭到破坏

多用户操作系统

image-20230820152628694

通过保护机制来决定系统为不同的用户提供什么资源,以及实时为每个用户实际分配所需的硬件资源。

可能出现的问题:

  1. 当某个用户恶意占有过多的CPU资源时,其他用户无法使用,或被分配较少CPU资源
  2. 总内存使用量大于机器RAM。
  3. 避免因需求增加而导致响应时间的超线性增加

保护机制:

image-20230820155420047

通过设置抢占来避免发生死锁。通过介入和调解来控制程序和管理程序(其中使用了一种表格的数据结构,该表格包含有资源标识符、访问权限、时间限制,并发访问等其他策略)。利用角色管理,来控制权限和实际运行环境。

特权级

指在内核模式下执行系统操作的权限。

CPU抢占

防止CPU被独占的机制,操作系统通过设置一个定时器(hardware),当定时器计时到达时,触发一个中断。操作系统将在中断处理程序中进行进程切换,将CPU分配给优先级更高的进程。

例如:内核将定时器编程为每10ms中断一次。

定时器

定时器设置为指定周期后中断计算机。指定周期可以是固定的或者固定速率(可变定时器)的。

内存保护

地址翻译

其用于保护程序间免受对方操作影响。

  • 地址空间:程序可以命名的所有内存位置

  • 虚拟地址:进程地址空间中的地址

  • 物理地址:实际内存地址

  • 翻译:将虚拟地址映射到物理地址

其他内存保护
  1. 内核只允许内核虚拟地址:CPU允许操作系统内核只使用特定的内核虚拟地址,这些地址在用户态程序中不可见。操作系统的内核代码和数据通常是存在于所有进程的地址空间中的,这使得系统调用等操作可以在同一个地址空间中处理,但通过限制内核虚拟地址的访问,确保了用户态程序无法直接操作内核内存,从而保护了内核的安全性。
  2. 禁用特定虚拟地址:CPU允许操作系统禁用(失效)特定的虚拟地址,用于捕捉并停止出现野指针等错误访问的程序。这种机制可以防止出现错误的程序访问,同时也可以通过只在实际访问时从磁盘中加载页面来使虚拟内存看起来比物理内存更大。
  3. 强制只读虚拟地址:CPU提供了强制只读虚拟地址的机制,这对于代码页面在多个进程之间的共享非常有用。通过共享只读的代码页,多个进程可以共享同一份代码,从而节省内存和提高性能,同时也保护了代码免受不必要的修改。
  4. 执行禁止虚拟地址:CPU提供了强制执行禁止的虚拟地址机制,可以使某些恶意的代码注入攻击变得更加困难。通过禁止在某些虚拟地址上执行代码,CPU可以防止恶意程序在其中注入并执行恶意代码,从而增强了系统的安全性。

上下文

描述正在运行的程序、进程或任务的状态及其所需的环境信息。

举例来说,在操作系统中,每个进程都有自己的上下文,包括了进程的程序计数器、寄存器值、内存映像等信息。当操作系统从一个进程切换到另一个进程时,它会保存当前进程的上下文,并加载下一个进程的上下文,以保证进程能够继续执行而不丢失状态。

在多任务系统中用于实现任务处理、状态保存和恢复等功能。

上下文种类:

  • 用户级别:CPU在用户模式下运行应用程序。
  • 内核进程上下文:在特定进程的内核代码上执行,代表该进程执行内核代码,例如执行系统调用。还包括异常处理(如内存错误、数值异常等)以及执行内核-only 进程(如网络文件服务器)。
  • 未与进程关联的内核代码:包括计时器中断(hardclock)、设备中断以及一些特定于操作系统的概念如“软中断”(Softirqs)和“任务队列”(Tasklets)(Linux 中的术语)。

上下文切换

指操作系统切换不同的上下文的过程。通常涉及到修改当前的地址空间和寄存器状态。

  • 用户级别到内核进程上下文:当应用程序需要执行系统调用或发生异常时,CPU从用户级别切换到内核进程上下文,执行内核代码以处理相关操作。
  • 内核进程上下文到用户级别:当内核进程完成任务或系统调用处理后,CPU可以切换回用户级别,继续运行应用程序。
  • 进程之间的切换:操作系统需要在不同进程之间切换CPU的执行,以实现多任务处理。这包括保存当前进程的状态,加载下一个进程的状态,切换地址空间等操作。

中断

通过设置CPU的中断引脚,当软硬件触发中断事件,由事件的严重程度将信号通过对应引脚进行传输中断信号(在8086中为INTR和NMI引脚,通过EFLAGS/IF判断该中断是否有效),在以前的操作系统中,查找IVT(中断向量表:0x00~0xff地址存储各种中断事件,1KB个事件)对应的事件,然后根据IVT给出的事务处理方法对中断信号进行处理。

IVT(中断向量表):

其中:0x00~0x07为错误/异常中断

0x08~0x0f为硬件中断

0x10~0x1A为BIOS中断

image-20230821113415668

硬件:

硬件传送信号给CPU触发中断。

软件:

软件通过系统调用(监督程序调用)触发中断。

通常来说CPU被中断时,它需要转到固定位置(中断服务程序的开始地址)继续执行。

方法:
1.调用一个通用程序以检查中断信息
2.通过中断处理程序的指针表.

其中,指针表位于低地址内存(前100左右的位置)。

中断向量:及每个设备的中断处理程序的地址且通过唯一设备号来索引

3.中断体系结构

保存了中断指令的地址,且在现代体系结构将返回地址保存在系统堆栈上。

执行过程

如果中断程序需要修改处理器状态,如修改寄存器的值,则应明确保存当前状态,并在返回之前恢复该状态。在处理完中断之后,保存的返回地址会加载到程序计数器,被中断的计算可以重新开始,就好像中断没有发生过一样。

  1. 保存返回地址:在发生中断之前,处理器会将当前程序计数器(PC)的内容(即下一条要执行的指令地址)保存到堆栈中。这是为了以后从中断处理程序返回到原来的执行位置。这个保存的操作可以通过将PC的值压入堆栈来完成。
  2. 切换到中断处理程序地址:然后,处理器会将PC的值替换为中断处理程序的入口地址,以便跳转到中断处理程序的执行。这个中断处理程序的地址通常存储在一个特定的位置,如中断向量表。
  3. 处理中断事务:中断处理程序现在开始执行,它会处理与中断相关的任务、事件或数据。中断处理程序的任务取决于中断类型和系统的设计。
  4. 从中断返回:当中断处理程序完成后,它需要从中断返回到原来的执行位置。为了实现这一点,处理器会从堆栈中弹出之前保存的返回地址,并将PC的值设置为该地址,从而回到中断发生前的状态。

开销

在计算机领域,"开销"(Overhead)通常指的是执行某项操作所需的额外时间、资源或成本。这些额外的开销可能是由于操作本身带来的,也可能是由于执行操作所需的附加操作造成的。

开销种类:
  • 时间开销:执行一个操作所需的额外时间。这可能涉及指令的执行、数据的传输、上下文切换等。
  • 资源开销:执行一个操作所需的额外资源,如内存、处理器时间、带宽等。
  • 空间开销:执行一个操作所需的额外存储空间,如数据结构、寄存器保存等。
  • 能源开销:执行一个操作所需的额外能源消耗,如电池能量、电力消耗等。
  • 复杂性开销:执行一个操作所需的额外复杂性或代码增加。例如,添加额外的逻辑或控制流可能导致代码更复杂。

典型的操作系统结构

image-20230820161427572

操作系统被分为用户态和内核态运行环境:

  • 程序在用户级的进程中实例化运行
  • 而操作系统内核需要在特权模式下运行:
    • 创建/删除进程
    • 对硬件进行访问

系统调用

通俗:是用户程序能够调用系统提供的某些操作的接口。

官方解释:是操作系统提供给用户程序的一组接口,允许用户程序在运行过程中请求操作系统执行特权的操作。

比如:文件流的读写,进程的创建和删除,网络通信等操作

该功能及确保了用户的进程可访问系统提供的底层功能,又保障了操作系统的安全和稳定性。

系统调用涉及以下相关概念:

  • 系统调用接口: 操作系统通过一组预定义的接口(通常是函数或指令)来暴露系统调用功能给用户程序。这些接口由操作系统提供,用户程序通过调用这些接口来请求特定操作。
  • 用户态和内核态: 大多数操作系统采用特权级别(例如用户态和内核态)来区分用户程序和操作系统的执行权限。用户态下运行的用户程序只能执行受限操作,而系统调用需要从用户态切换到内核态,以便操作系统执行特权操作。
  • 上下文切换: 当用户程序发起系统调用时,操作系统需要进行上下文切换,将用户程序的执行环境切换到内核态。这包括保存用户程序的状态、加载内核的状态,并执行所请求的系统调用。完成后,操作系统再将控制返回给用户程序。
  • 返回结果: 用户程序在执行系统调用时,请求操作系统完成某个任务,操作系统执行后会将结果返回给用户程序。这个结果可能是执行成功与否的标志,以及相关数据或状态信息。
  • 权限和安全性: 系统调用允许用户程序访问操作系统提供的底层功能,但操作系统会对这些功能进行权限管理。操作系统会检查用户程序的请求是否合法,以及用户是否有足够的权限执行该操作。

Unix文件系统调用

  • Applications “open” files (or devices) by name

    • I/O happens through open files
  • int open(char path, int flags, /int mode*/...);

    • flags: O_RDONLY, O_WRONLY, O_RDWR

    • O_CREAT: create the file if non-existent

    • O_EXCL: (w. O_CREAT) create if file exists already

    • O_TRUNC: Truncate the file

    • O_APPEND: Start writing from end of file

    • mode: final argument with O_CREAT

  • Returns file descriptor—used for all I/O to file

该系统调用发生错误时,返回:-1:打开失败;-2:没有这样的文件或目录;-13:权限被拒绝

使用perror函数打印人类可读类型的错误信息。

文件描述符(file description)

文件描述符,是指操作系统对某个文件进行操作时分配的唯一标识符。用于区别其他正在操作的文件。文件描述符仅描述已打开的文件。

在Unix的存储系统中,文件的inode号可能是文件的唯一标识。这个唯一标识用于在文件系统层级上定位和管理文件。

  • read:返回读取的字节数
  • write:返回写入的字节数,错误时返回-1
  • off_t lseek:0开始,1当前,2结束,当前在文件中读取的指针位置
  • close

文件描述符由进程继承:即当一个进程创建另一个进程时,默认情况下具有相同fd。

操作系统在计算机结构扮演角色:

image-20230820144142141

从资源共享的角度看问题:

我们从上图可知,操作系统将物理资源抽象化,其应用程序通过操作系统提供的系统调用,能够间接系统操作。它们都共享一个物理资源。

隔离

操作系统将运行的程序(或者进程)其执行相互隔离,每个程序都获得属于自己那部分的计算机资源的切片(若所有程序在物理机上直接执行,则会出现类似独占情况,是计算机资源无法给其他程序使用。而若建立于操作系统之上,且每个程序由操作系统分配基本资源且互相间运行环境隔离)。其主要作用是:防止其他进程的故障影响到实际的物理机本身或者是其他进程的运行。

通信

在不同的进程之间,有时候会使用到共同的资源,以及在相互间可能会需要进行部分操作的通信,来达成联动的操作。比如:在进行网页搜索时,一个进行广告搜索,一个进行真正内容搜索,另一个用于合并搜索结合准备返回数据其他所需资源。

虚拟化

即操作系统利用虚拟化技术,将一个物理资源虚拟出许多个虚拟机资源,供应用程序运行(让应用程序以为每个都是独占整个物理机)。

比如:VMware虚拟机监视器,它可以创建许多虚拟操作系统,它在操作系统的基础上,又虚拟化一遍资源和虚拟一个操作系统。从而有了所谓的物理机和虚拟机。

又或者说虚拟内存,其容量一般由寻址空间,寻址空间一般由机器字长决定大小,机器字长也就是CPU字长:一次性处理二进制数据的位数。虚拟内存的容量实际比物理内存的容量大得多。

通用服务

也就是每个服务都可以使用到的公共服务。其跨越进程,能够为不同进程传递消息和共享内容提供了标准方法。(比如:剪贴板功能)

总结

也就是操作系统提供资源的分配、资源的虚拟化和公共服务等。

引出技术

并行应用程序

执行的任务多于处理器时,系统如何决定先执行那些任务;运行时系统如何向程序员隐藏硬件细节;高并发数据结构的使用。

不平衡问题:

  1. 80/20原则:Skew现象在编程中常被描述为 "80/20" 原则,也就是指系统中的20%的代码占用了80%的执行时间。这表示少数核心部分的代码会占据大部分的系统执行时间。
  2. 内存引用的不平衡:Skew现象还可以在内存使用中观察到。大约10%的内存区域会占用90%的引用次数。这就是所谓的 "90/10" 规律。这意味着少数内存区域会被频繁访问,而大部分内存区域可能很少被访问。
  3. 缓存的应用:Skew现象也是缓存系统设计的基础之一。系统将频繁访问的数据放入快速缓存中,而将不经常访问的数据放在较慢的缓存中。这样可以在系统内部创造出一个似乎是一个统一的大型快速缓存,提高数据访问速度。
  4. 过去预测未来(时局性局部性):这一现象指出,过去发生的访问模式往往可以预测未来的访问模式。在缓存替换策略中,最近最少使用(LRU)是一种基于这一原则的策略。如果过去的访问模式与未来相似,那么被最近访问最少的数据很可能在未来也不会被访问,因此可以被替换出缓存。
  5. 公平与吞吐量之间的冲突:尽管提高吞吐量(例如减少缓存失效等)可以使某一进程继续运行,但这可能会导致公平性问题。从公平性的角度来看,应该定期抢占CPU资源,将其分配给下一个进程。然而,这可能降低吞吐量,导致性能下降。

操作系统中重要的功能

*进程

定义

是指正在运行的程序的实例。是操作系统最基本的执行单位。

特点

  1. 每个进程拥有属于自己的独立的内存空间和资源

    • 独立的内存空间:即每个进程拥有独立的地址空间(通过不同的寻址方式来查找),每个进程仅看到自己的处的地址空间
    • 独立的打开文件:每个进程可以独立的打开文件,但需要注意的是当不同进程打开同一个文件时,会引起数据冲突
    • 独立的虚拟CPU:操作系统通过抢占多任务处理机制,通过CPU时间片的分配,让每个进程认为自己独享整个CPU。
  2. 并行执行更能有利于提高执行速度,在操作系统的调度下交替执行,给用户以多任务并行体验。并且也简化了开发人员需要关心的对象,只需关注对应的进程即可。

image-20230821160903308

进程间通信

  1. 通过内核传递消息

image-20230821165239581

  1. 通过共享物理内存区域

image-20230821165300688

  1. 通过异步信号或报警(异常处理)

相关进程在linux中API

创建进程

此函数用于创建进程,创建与当前进程完全相同的进程,在父进程中返回新进程的pid,在子进程中返回0

image-20230821170215609

进程等待

image-20230821170222452

删除进程

  • void exit (int status);
    • Current process ceases to exist
    • status shows up in waitpid (shifted)
    • By convention, status of 0 is success, non-zero error
  • int kill (int pid, int sig);
    • Sends signal sig to process pid
    • SIGTERM most common value, kills process by default(but application can catch it for “cleanup”)
    • SIGKILL stronger, kills process always

运行程序

image-20230821170200480

C语言中分配内存的函数

void* malloc:分配堆栈

void* calloc(size_t nmemb, size_t size);:分配堆栈

void* readloc(void *ptr, size_t size);:分配堆栈

void* allocasize_t size);:分配函数栈帧

进程控制块

进程的PCB(进程控制块)是操作系统中用于管理和维护进程信息的数据结构。每个正在运行或被挂起的进程都有一个对应的PCB,它包含了进程的所有必要信息,以便操作系统能够管理和控制进程的执行。

PCB通常包含信息:

  1. 进程状态:表示进程当前的状态,例如运行、挂起(可能挂起在内存中等待I/O或异常事件,也可能挂起在外部存储中)、就绪等。
  2. 程序计数器(Program Counter,PC):指向进程在内存中下一条要执行的指令的地址。
  3. 寄存器集合:保存了进程的寄存器状态,包括通用寄存器、堆栈指针、基址寄存器等。
  4. 内存管理信息:包括页表、地址空间信息等,用于虚拟内存管理。
  5. 打开文件列表:记录进程当前打开的文件以及文件描述符。
  6. 进程优先级:用于调度算法,决定进程被调度的优先级。
  7. 进程ID:唯一标识进程的标识符。
  8. 父进程ID:标识创建当前进程的父进程。
  9. 资源分配信息:例如已使用的CPU时间、内存占用等。
  10. 信号处理信息:记录进程注册的信号处理函数。
  11. 进程的等待队列:如果进程正在等待某个事件,如I/O完成,它可以在等待队列中等待。
  12. 其他调度和状态信息:例如进程的创建时间、运行时间、调度策略等。

总之,PCB是操作系统管理进程的关键数据结构,它允许操作系统跟踪、管理和控制进程的各种信息,以便有效地进行进程调度、资源分配和各种操作.

PCB字段举例

  1. 进程标识符(Process ID): 示例:PID = 123 解释:唯一标识一个进程的数字。
  2. 程序计数器(Program Counter): 示例:PC = 0x12345678 解释:指向进程下一条要执行的指令地址。
  3. 寄存器状态(Register State): 示例:Registers = {AX: 42, BX: 100, ...} 解释:保存进程当前的寄存器值,包括通用寄存器、条件码等。
  4. 栈指针(Stack Pointer): 示例:SP = 0x789ABC00 解释:指向进程当前的堆栈顶部。
  5. 堆栈区域(Stack Area): 示例:Stack Memory = [0x789ABC00, 0x789ABFFF] 解释:存储进程的函数调用、局部变量等数据。
  6. 状态信息(State Information): 示例:State = Ready 解释:表示进程的当前状态,如就绪、运行、等待等。
  7. 优先级(Priority): 示例:Priority = 2 解释:标识进程的调度优先级,影响调度顺序。
  8. 打开文件列表(Open File List): 示例:Files = [file1, file2, ...] 解释:记录进程打开的文件和文件描述符。
  9. 父子关系(Parent-Child Relationship): 示例:Parent = 101, Children = [105, 110, ...] 解释:记录进程之间的父子关系。
  10. 资源分配信息(Resource Allocation Information): 示例:Memory = 512 MB, CPU Usage = 80% 解释:记录分配给进程的资源信息,如内存和CPU使用情况。

linux的简要PCB图

image-20230822091220375

也就是说,哪个用户创建的进程,并且这个进程的状态是什么,以及当前执行使用到了哪些文件,在虚拟内存中的地址空间,以及下一条指令地址,在调度策略中,它的优先级是多少。

进程状态

image-20230822092528198

状态列表:

  1. 新建状态(New):进程刚刚被创建,但还没有开始执行。在此状态下,操作系统正在为进程分配必要的资源和初始化进程的PCB。
  2. 就绪状态(Ready):进程已经准备好执行,但由于其他进程正在执行,它需要等待CPU的分配。处于就绪状态的进程通常被放置在就绪队列中,等待调度执行。
  3. 运行状态(Running):进程正在CPU上执行指令。在任何时刻,只有一个进程能够处于运行状态,因为一台计算机只有一个CPU核心。
  4. 阻塞状态(Blocked):进程因等待某些事件(如I/O操作、资源分配等)而被阻塞。进程在等待事件完成时暂时无法继续执行,被移到阻塞队列中等待(线程于FIFO被调度)。
  5. 挂起状态(Suspended):进程被挂起,即从主存移到外部存储,以释放主存资源。进程可以是就绪、阻塞或运行状态中的一个,被挂起的进程不会占用主存。
  6. 终止状态(Terminated):进程执行完毕或因某些原因被操作系统终止。在这个状态下,进程的资源将被释放,PCB 会被清除。

当进程为0个时,CPU会停止或进入空闲状态,而进程为一个时就执行它。当进程有多个时,需要调度算法进行分配时间片。

调度策略

调度策略是操作系统用于决定哪个进程在给定时间片内获得CPU时间的算法或规则。不同的调度策略可以影响进程的执行顺序、响应时间、吞吐量等性能指标。

常见的调度策略:

  1. 先来先服务(First-Come, First-Served,FCFS): 按照进程到达的顺序分配CPU时间。适用于短作业,但可能导致长作业等待时间过长("饥饿"问题)。
  2. 最短作业优先(Shortest Job Next,SJN): 选择下一个预计运行时间最短的进程。可以最小化平均等待时间,但对长作业可能不公平。
  3. 最短剩余时间优先(Shortest Remaining Time First,SRTF): 动态版本的SJN,考虑进程的实际剩余执行时间,使得新到达的作业也有机会。
  4. 轮转(Round Robin,RR): 每个进程被分配一个时间片(时间量),然后切换到下一个进程。适用于多任务环境,但可能导致上下文切换开销增加。
  5. 优先级调度: 每个进程都有优先级,优先级高的进程先执行。可以是静态或动态调度,但可能导致低优先级进程饥饿。
  6. 多级反馈队列(Multilevel Feedback Queue,MLFQ): 将进程分配到多个队列中,根据优先级和时间片大小进行调度。优先级可能在队列之间调整,适用于多种作业长度。
  7. 最高响应比优先(Highest Response Ratio Next,HRRN): 以等待时间和服务时间的比值为依据选择下一个进程,具有较高响应比的进程被优先调度。
  8. 多处理器调度: 在多个处理器上并行调度多个进程,可能涉及负载均衡等问题。
  9. 实时调度: 保证实时任务在规定的时间内得到响应,根据截止时间等调度。

抢占

抢占(Preemption)指的是操作系统可以在一个正在执行的进程没有自愿释放CPU的情况下,强制地从该进程手中取回CPU,并分配给另一个进程执行。

这种方式允许操作系统在任何时候都可以中断正在运行的进程,将CPU分配给其他高优先级的进程,以满足紧急任务或高优先级任务的需要。

抢占可以用于实现多任务并发、实时任务响应等需求。它有助于保证高优先级的任务在合适的时间内得到执行,而不会被低优先级的任务长时间阻塞。

常见的抢占场景包括:

  1. 时间片用尽:在轮转调度算法中,每个进程被分配一个固定的时间片。当时间片用尽时,操作系统会抢占正在执行的进程,将CPU分配给下一个就绪的进程。
  2. 优先级提升:如果一个高优先级的进程到达就绪态,并且当前正在执行的进程优先级较低,操作系统可以选择抢占正在执行的进程,以便让高优先级进程优先执行。
  3. 实时任务响应:在实时操作系统中,一些任务必须在特定的时间内得到响应。如果一个实时任务到达,并且当前正在执行的任务无法在要求的时间内完成,操作系统可以抢占当前任务,确保实时任务得到及时执行。

抢占的实现通常需要考虑上下文切换的开销,因为当操作系统从一个进程切换到另一个进程时,需要保存和恢复进程的上下文信息(如寄存器状态)。因此,在选择抢占策略时,需要权衡抢占的频率和上下文切换的开销,以保持系统的效率和响应性。

上下文信息

上下文信息(Context Information)是进程的PCB一部分,描述进程的当前执行状态

上下文信息包括了进程或线程在执行过程中的各种状态、数据和寄存器值,这些信息允许操作系统在进行进程切换时保存当前进程的状态(封锁现场),然后将控制权转移到另一个进程,最后再恢复原进程的状态(释放现场)。

常见的上下文信息包括:

  1. 程序计数器(Program Counter,PC):指向当前进程或线程要执行的下一条指令的地址。
  2. 寄存器状态:包括通用寄存器、状态寄存器(如条件码寄存器、PSW等)以及其他特定于体系结构的寄存器。
  3. 栈指针(Stack Pointer):指向进程或线程的堆栈顶部,用于管理函数调用和局部变量。
  4. 堆栈内容:进程或线程的当前堆栈帧,包括局部变量、返回地址等。
  5. 程序状态字(Program Status Word,PSW):包含一些标志位和控制信息,用于指导指令的执行。
  6. 内存管理信息:包括页表、地址空间信息等,用于虚拟内存管理。
  7. 文件描述符表:指向打开文件和其他I/O资源的指针或索引。
  8. 信号处理信息:注册的信号处理程序和相关状态。
  9. 优先级和调度信息:进程或线程的优先级、调度策略等信息。

当操作系统决定切换到另一个进程时,它会保存当前进程的上下文信息,然后将控制权转移到新的进程,并恢复其上下文信息,使得新进程能够继续执行。

上下文切换的过程涉及寄存器值的保存和恢复,堆栈的切换,以及其他必要的操作,因此是一个开销较大的操作。

上下文信息的管理对于多任务并发和进程调度至关重要。

上下文切换

上下文切换(Context Switching)是操作系统在多任务环境下进行任务调度时的一个重要概念。它指的是操作系统从一个正在执行的进程(或线程)切换到另一个进程(或线程)时,保存当前进程的上下文信息,并恢复下一个进程的上下文信息,以便让新进程能够继续执行。上下文切换是实现多任务并发的关键机制之一。

:上下文信息包括进程(或线程)的寄存器值程序计数器(PC)程序状态字(PSW)等,这些信息共同构成了进程的状态

💡image-20230822111336484

上下文切换的过程步骤:

  1. 保存当前进程的上下文信息:操作系统将当前进程的寄存器值、PC、PSW等上下文信息保存到该进程的PCB(进程控制块)中,以便将来恢复。
  2. 选择下一个要执行的进程:操作系统从就绪队列中选择下一个要执行的进程,即将要切换到的进程。
  3. 恢复下一个进程的上下文信息:操作系统从选定的进程的PCB中恢复上下文信息,将寄存器值、PC、PSW等恢复到适当的状态,以便该进程能够继续执行。
  4. 开始新进程的执行:一旦下一个进程的上下文信息恢复完毕,操作系统会将控制权转移到该进程,让其继续执行。

上下文切换是一个开销较大的操作,因为它涉及到多次寄存器值的读取和写入,以及从内存中读取和写入PCB等。过多频繁的上下文切换可能会导致系统的效率下降,因此在设计调度策略时,需要平衡抢占的频率和上下文切换的开销。

空闲(Idle)

Idle 进程或线程在系统中没有实际的工作负载,它只是占用CPU时间,以防止系统进入空闲状态,确保系统始终保持活跃状态。

上下文切换的关键点

  1. 保存寄存器:保存当前进程的程序计数器和整数寄存器,以便它在再次获得CPU时间时能够继续执行。
  2. 特殊寄存器:如果进程使用了浮点运算或其他特殊寄存器,这些寄存器的状态也可能需要保存。可以根据需要优化,避免不必要的开销。
  3. 条件码:保存和恢复条件码(标志位),这些标志位用于表示操作的结果,影响程序的执行流程。
  4. 虚拟地址映射:切换进程需要更新内存映射表,确保新进程的内存访问得以正确指向。
  5. 切换成本:上下文切换具有一定的开销,涉及到寄存器和设置的保存和恢复。特别是涉及到浮点寄存器时,成本可能更高。
  6. 优化:为了减少开销,可以采用优化措施。例如,只在进程使用浮点操作时保存和恢复浮点寄存器,减少不必要的开销。
  7. TLB清空:切换可能需要清空TLB(内存翻译硬件),但可以通过硬件优化避免清空不必要的数据。
  8. 缓存未命中:上下文切换可能导致更多的缓存未命中,特别是进程的工作集不同。切换进程可能需要重新加载缓存行,影响性能。

线程

是操作系统的最小可执行单元,每个进程至少使用一个线程,是进程中的一个可调度的执行上下文的执行流程(或者是进程的执行单元)。

一个进程可以包含多个线程,它们📗共享进程的资源(如内存空间、文件描述符等),📗但每个线程有自己的执行栈和局部变量(TCB)。

特点

  1. 共享进程资源:在同一进程内的所有线程共享相同的全局变量、堆内存、打开的文件、其他资源等。
  2. 独立的执行栈:每个线程都有自己的执行栈,用于存储局部变量、函数调用信息等。这使得线程可以独立运行,并在切换时保持自己的执行上下文。
  3. 轻量级:相对于进程,线程更轻量级,创建、销毁和切换线程的开销通常比进程小得多。
  4. 并发执行:多个线程可以并发执行,提高了系统的资源利用率。不同的线程可以执行不同的任务,从而实现并行计算。
  5. 通信和同步:线程之间可以通过共享内存进行通信,但同时也需要考虑线程同步,以避免数据竞争和冲突。

为什么要使用线程?

是相对于进程更加轻量的抽象表示,能够共享进程中的所有字段。

提高多任务的并发能力和响应能力。通过对进程的管理,就能够间接管理线程。

用途:
  1. 并发执行:线程允许多个任务在同一时间内并发执行,从而充分利用多核处理器和计算资源,加速任务完成。
  2. 提高性能:在并行计算中,多线程可以同时处理任务,加快计算速度,尤其对于计算密集型任务效果显著。
  3. 改善响应性:使用多线程可以使程序对用户输入和外部事件更敏感,提高系统的响应能力。
  4. 任务分解:将一个大任务分解成多个线程,每个线程负责一部分工作,从而简化问题的解决和设计。
  5. 协作任务:不同线程可以协作完成复杂的任务,例如一个线程负责计算,另一个线程负责I/O。
使用场景:
  • 并行计算:在多核处理器上,不同线程可以并行执行,加速计算密集型任务。
  • 多任务并发:不同线程可以处理不同的任务,例如一个线程处理用户输入,另一个线程处理网络请求。
  • GUI应用:图形用户界面(GUI)应用程序通常使用主线程来处理用户输入和界面更新。
  • 服务器应用:服务器程序可以使用多线程来同时处理多个客户端请求,提高响应性能。

内核级线程

内核级线程(Kernel-level thread)是由操作系统内核直接支持和管理的线程。每个内核级线程都有自己的上下文、寄存器集合和调度信息,这些线程由操作系统内核调度和管理,不依赖于用户空间的支持。

如下图:一个内核级线程可对应一个用户级线程,每个内核级线程可并发处理同一个程序内的多个子任务,提高处理效率。

image-20230822141922408

与用户级线程相比,内核级线程的主要优点在于更高的并发性、更好的资源管理和更强的稳定性。然而,由于操作系统介入线程的创建、销毁和切换,内核级线程可能会带来较高的开销。

每个线程的操作都必须经过内核,不需要额外设置内核级线程的优先级。

特点:

  1. 并发性: 内核级线程由操作系统内核直接管理,因此可以在多个处理器核心上并发执行,提高并发性。
  2. 稳定性: 内核级线程的管理由操作系统负责,能够更好地处理线程的创建、销毁和调度,有助于避免用户级线程可能引起的一些问题。
  3. 资源管理: 操作系统可以更精确地分配和管理内核级线程的资源,如内存、CPU时间等。
  4. 多任务性: 内核级线程可以在不同的进程中运行,允许多任务并发执行。
  5. 阻塞: 内核级线程可以进行阻塞操作,如等待 I/O 完成,而不会影响其他线程的执行。

用户级线程

用户级线程(User-level thread)是在用户空间中创建、管理和调度的线程。与内核级线程不同,用户级线程的创建和管理不需要操作系统内核的直接干预。

这意味着用户级线程是在用户程序内部实现的,操作系统并不感知这些线程的存在

如下图:多个用户级线程共享一个内核级线程,一个用户级线程故障,其他会受到影响。

image-20230822142741395

特点:

  1. 轻量级: 由于不需要操作系统内核的支持,用户级线程的创建和销毁开销较小,通常比内核级线程更轻量级。
  2. 灵活性: 用户级线程的调度由用户程序实现,可以根据需要自定义调度算法和策略。
  3. 资源共享: 用户级线程可以共享进程内的资源,如内存和文件,但需要自行处理同步和数据共享。
  4. 阻塞问题: 用户级线程在执行过程中如果发生阻塞,可能会影响同一进程内的其他线程,因为操作系统对线程的调度不可见。
  5. 并发性: 由于用户级线程是在单个内核级线程中运行,如果一个用户级线程被阻塞,其他用户级线程也会受到影响。
  6. 通常需要库支持: 用户级线程的创建和管理通常需要程序员使用线程库(如POSIX pthreads、Windows Thread API)来进行编程。

实现

需要用户自行设计保留运行用户级线程的的队列。

以一个多线程的Web服务器为例:

  1. 为每个 thread_create 分配新的栈: 当使用 thread_create 函数创建一个新线程时,会为该线程分配一个独立的栈。栈用于存储局部变量、函数调用信息等线程执行所需的数据。
  2. 维护可运行线程队列: 维护一个队列或列表,以跟踪所有准备运行的线程。这个队列由线程调度器管理,用于决定下一个要执行的线程。
  3. 替代网络系统调用: 在多线程环境中,如果执行一个网络系统调用(如 readwrite)会阻塞,系统可以切换到其他线程,而不是等待操作完成。这种方法允许服务器在某些线程等待I/O操作时继续处理其他线程。
  4. 定时器信号调度: 使用定时器信号(如 POSIX 系统中的 setitimer)可以实现抢占式调度。当接收到定时器信号时,当前线程的执行被中断,另一个线程被调度运行。这提供了一种时间切片机制,确保线程之间的公平执行。
  5. 多线程Web服务器示例: 在这个场景中,使用一个多线程的Web服务器作为示例。线程用于同时处理客户端连接和请求。
    • 处理连接的线程可能调用 read 来从远程Web浏览器获取数据。
    • 可以以非阻塞模式实现一个“虚假”的 read 函数。如果没有立即可用的数据,线程将不会阻塞,而是安排运行另一个线程。
    • 可以使用定时器信号定期切换执行到不同的线程,确保没有线程独占CPU。

内核级和用户级的主要区别:

主要就两个大点:

1)前者有操作系统管理和创建,而后者为用户程序在其内部创建和管理;

2)前者当发生阻塞时,由操作系统自动进行调度,而后者需要用户自己进行设置阻塞调度算法。

3)前者若出现故障,其影响不到其他线程,而后者因为共用同一个内核级线程,所以一个故障时,会影响到其余用户级线程。

详细:

1. 创建和管理方式:

  • 用户级线程: 用户级线程是在用户程序内部创建和管理的。这意味着线程的创建、销毁和切换都由应用程序的线程库负责,而操作系统内核对于这些线程是不可见的。
  • 内核级线程: 内核级线程是由操作系统内核直接支持和管理的。操作系统负责线程的创建、销毁、调度和资源管理。

2. 调度控制:

  • 用户级线程: 用户级线程的调度由应用程序实现,通常使用一些自定义的调度算法。操作系统对于用户级线程的调度没有直接控制权,因此用户级线程可能在发生阻塞时会影响同一进程中的其他线程。
  • 内核级线程: 内核级线程的调度由操作系统内核控制。操作系统可以在不同的处理器核心上并发地调度内核级线程,提高并发性和资源管理。

3. 并发性:

  • 用户级线程: 用户级线程是在单个内核级线程中运行的。如果一个用户级线程被阻塞,其他用户级线程也会受到影响,因为它们共享同一个内核级线程。
  • 内核级线程: 内核级线程可以在多个处理器核心上并发执行,从而提高并发性。

4. 阻塞问题:

  • 用户级线程: 用户级线程在执行过程中如果发生阻塞,可能会导致整个进程内的所有用户级线程受阻,因为它们共享同一个内核级线程。
  • 内核级线程: 内核级线程可以独立地进行阻塞操作,不会影响其他线程。

5. 开销:

  • 用户级线程: 用户级线程的创建和销毁开销相对较小,因为它们不涉及操作系统内核的干预。
  • 内核级线程: 内核级线程的创建和销毁可能会带来较高的开销,因为它们涉及到操作系统内核的调度和资源管理

线程实现的细节

在线程和进程中都是使用栈帧进行实现,只不过两者实现的栈帧所拥有的字段个数不同(包含的信息数不同)。结合计组的函数调用栈帧那块理解。

下图为线程的栈帧实现:

High Address							expression
+--------------------------+
|   Call arguments         |		表示函数调用时传递的参数
+--------------------------+
|   Return Address (RA)    |		以及PC在上一层函数栈帧运行的位置
+--------------------------+
|   Previous Frame Pointer |		此处保存了上一层栈帧的基址
|						   |		
+--------------------------+此处为fp栈基址指针指向的位置
|   Local Variables        |
|   and Temporary Data     |		表示局部变量和临时数据
+--------------------------+
|   Callee-saved Registers |		表示被调用函数需要保存和恢复的寄存器
+--------------------------+
|   Function Parameters    |		表示函数参数和调用时传递的参数值
|   and Arguments          |
+--------------------------+此处为sp栈顶指针指向位置
|   (Unused/Padding)       |
+--------------------------+
Low Address

Callee-saved 寄存器位于局部变量和临时数据之后,参数之前。在函数调用时,被调用函数需要将先将这些寄存器的值保存到当前栈帧,然后在函数返回时恢复这些寄存器的值,以确保调用者的寄存器状态不被破坏。

调用过程描述:

创建被调用函数栈帧:
  1. 先将调用函数的参数压入栈中。
  2. 再将当前PC指向的地址压入被调用栈帧中
  3. 保存调用者栈帧的基址(即栈底地址)
  4. 将被调用函数的局部变量和临时数据入栈
  5. 保存调用函数需要保存的寄存器的值,以便能够后续被调用函数执行结束后恢复调用函数的现场
  6. 将函数参数和调用时传递参数入栈
释放被调用函数栈帧

其执行过程与上述相反。

其中需要注意的是:

  1. 保护调用函数的现场所压入的元素:返回地址;调用函数的栈帧栈底地址;调用函数的寄存器值
  2. 而被调用者在运行完毕后,对应于调用函数中,若将其修改的部分将会被恢复。(调用者使用到的通用寄存器和当前栈顶指针sp需要恢复,全局变量的修改不需要恢复)

线程交换的汇编示例

cur 是一个指向线程控制块(TCB)数据结构的指针。

将%edx保存堆栈在栈中的偏移量,也是堆栈在TCB中的偏移量。

cur->stack表示当前线程控制块中堆栈字段

pushl %ebx; pushl %ebp 				#Save callee-saved regs 
pushl %esi; pushl %edi				# 保存调用者寄存器值
mov thread_stack_ofs, %edx 			# %edx = offset of stack field 
									# in thread struct 将%edx保存堆栈在栈中的偏移量,也是堆栈的起始地址
movl 20(%esp), %eax 				# %eax = cur #将%eax保存当前线程指针,在上下文切换中,cur一般指向线程控制块的首地址
movl %esp, (%eax,%edx,1) 			# cur->stack = %esp #cur->stack表示访问当前线程控制块中堆栈字段,因为前面添加了是个寄存器的值,此处相当于赋值,堆栈的首地址向下移动。
movl 24(%esp), %ecx 				# %ecx = next 指向下一个栈帧TCB
movl (%ecx,%edx,1), %esp			# %esp = next->stack 指向下一个栈帧TCB的堆栈
popl %edi; popl %esi 				# Restore calle-saved regs
popl %ebp; popl %ebx				#
ret 								# Resume execution

代码部分

句柄

句柄(Handle)通常用于表示对某个资源的引用或标识。句柄在操作系统和编程中被广泛使用,用于管理和操作各种资源,如文件、内存、图形界面元素等。句柄可以被视为一种抽象的数据结构,用于访问底层资源,而不需要直接操作资源的实际内容

句柄的优点包括:

  1. 封装性: 句柄可以隐藏底层资源的实现细节,提供了一种封装机制,使得程序员可以通过句柄来操作资源,而无需了解资源的内部结构。
  2. 安全性: 使用句柄可以提高资源的安全性,因为程序无法直接访问资源的底层数据。只有通过句柄才能进行操作,从而减少了直接访问资源可能引发的错误或不安全的情况。
  3. 灵活性: 句柄允许系统在资源的生命周期内进行动态管理和重用。资源的实际内容可以在句柄之间共享,从而提高资源的利用效率。
  4. 跨进程通信: 在多进程或多线程环境中,句柄可以被用于在不同的进程或线程之间共享资源,实现跨进程通信

在不同的上下文中,句柄可能指向不同类型的资源。例如,在Windows操作系统中,句柄可以用来表示文件、窗口、图像、事件等。在编程中,句柄常常作为一种数据类型来使用,用于引用和操作底层资源

并发

互斥

有时也称为临界区

posted @ 2023-08-20 17:18  duuuuu17  阅读(24)  评论(0编辑  收藏  举报