操作系统

  • 2022-08-11
  • 浏览 (2715)

操作系统

并发和并行 并发是指一个处理器同时处理多个任务。 并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。

并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)。

操作系统的功能

  • 进程管理
  • 内存管理:提高内存利用率和访问速度,从而提高计算机的运行效率
  • 文件管理
  • IO设备管理:设备无关性,将设备抽象成逻辑设备

分成外核与内核模式的原因:

  • 保证操作系统受其他系统异常故障的影响;内核模式只能运行操作系统的程序,用户程序运行在外核中;
  • 确保可能引起系统崩溃的指令(特权指令)只能在内核模式下运行;
  • 为了防止非法IO,将所有IO指令定义为特权指令
  • 设置中断,一定时间后将控制权返回给操作系统
进程和线程的区别

进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位

在执行一些细小任务时,本身无需分配单独资源时(多个任务共享同一组资源即可,比如所有子进程共享父进程的资源),进程的实现机制依然会繁琐的将资源分割,这样造成浪费,而且还消耗时间。后来就有了专门的多任务技术被创造出来——线程。

共同点:在多任务程序中,子进程(子线程)的调度一般与父进程(父线程)平等竞争。在早期的Linux内核中,线程的实现和管理方式就是完全按照进程方式实现的。在2.6版内核以后才有了单独的线程实现。

实现方法的差异:进程的个体间是完全独立的,而线程间是彼此依存的。多进程环境中,任何一个进程的终止,不会影响到其他进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)。而任何一个子线程终止一般不会影响其他线程,除非子线程执行了exit()系统调用。任何一个子线程执行exit(),全部线程同时灭亡。

  • 从系统实现角度讲,进程的实现是调用fork系统调用:pid_t fork(void);线程的实现是调用clone系统调用
  • fork()是将父进程的全部资源复制给了子进程。而线程的clone只是复制了一小部分必要的资源。后来操作系统还进一步优化fork实现——写时复制技术。在子进程需要复制资源(比如子进程执行写入动作更改父进程内存空间)时才复制,否则创建子进程时先不复制。
  • 个体间辈分关系的迥异;进程的备份关系森严,在父进程没有结束前,所有的子进程都尊从父子关系;多线程间的关系没有那么严格,不管是父线程还是子线程创建了新的线程,都是共享父线程的资源,所以,都可以说是父线程的子线程,也就是只存在一个父线程,其余线程都是父线程的子线程。

内核态线程、轻量级进程、用户态线程

它的创建和撤消是由内核的内部需求来决定的,一个内核线程不需要

内核态线程:和一个用户进程联系起来。它共享内核的正文段和全局数据,具有自己的内核堆栈。内核线程的调度由于不需要经过态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。

轻量级进程:轻量级进程是核心支持的用户线程,它在一个单独的进程中提供多线程控制。这些轻量级进程被单独的调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上,而且在它的生命周期这种绑定都是有效的。轻量级进程被独立调度并且共享地址空间和进程中的其它资源,但是每个LWP都应该有自己的程序计数器、寄存器集合、核心栈和用户栈。

用户线程:用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。内核对它们一无所知,而只是调度用户线程下的进程或者轻量级进程,这些进程再通过线程库函数来调度它们的线程。当一个进程被抢占时,它的所有用户线程都被抢占,当一个用户线程被阻塞时,它会阻塞下面的轻量级进程,如果进程只有一个轻量级进程,则它的所有用户线程都会被阻塞。

注意:Linux中,每个线程都有一个task_struct,所以线程和进程可以使用同一调度器调度。如果一个task独占所有的资源,则是一个HWP,如果一个task和其它task共享部分资源,则是LWP。clone系统调用就是一个创建轻量级进程的系统调用,clone的用法和pthread_create有些相似,两者的最根本的差别在于clone是创建一个LWP,对核心是可见的,由核心调度,而pthread_create通常只是创建一个用户线程,对核心是不可见的,由线程库调度。

PCB进程控制模块

  • 记录进程信息:进程标识信息、处理机状态、进程调度信息、资源分配信息
  • 操作系统是根据进程控制块PCB来对并发执行的进程进行控制和管理的。
  • PCB是进程存在的唯一标志
  • 寄存器、堆栈指针、程序计数器、进程状态、优先级、调度的参数、父进程、cpu占用时间

进程和程序的区别

Program 指令的集合、是静态的概念;是持久的

Process 描述的是执行,动态的概念、包含程序、数据以及PCB;是暂时的

进程之间的通信
  • 共享内存

    • 最快的速度进行方便的通信;
  • 信息传递

    • 交换较少的数据;小号时间多
  • 间接通信

    • 每当一个信箱有一个唯一的id
    • 仅当共享一个信箱时,才能通信
  • 共享存储

    • 两个进程对共享空间的存储是互斥的
    • 基于数据结构的:共享速度慢、限制多、是一种低级通信方法
    • 基于存储区的共享,数据的形式、存放位置都是由进程控制的,是一种高级通信方式
  • 管道通信

    • 某个时间只能单行通信,在内存中开辟一个固定大小的缓冲区,但是也是互斥的
    • 需要将缓冲区写满,缓冲区读满的时候才可以
    • 数据不可以重复读,所以读进程只能有一个
  • 消息传递

    • 直接通信:将消息挂到对应线程的缓冲队列上,每个进程都会有自己信息缓冲队列,需要设置一些头
    • 间接通信方式:将信息挂载到中间实体,也被称为“信箱”

线程

引入的原因

  • ​ 进程操作系统开销大;将进程的两个任务分开:分配资源以及调度;对于作为调度和分派的基本单位,不同时作为拥有资源的单位,以做到“轻装上阵”; 对于拥有资源的基本单位,又不对之进行频繁的切换。
  • 因此线程为CPU调度的最小单位,进程为资源分配的最小单位

线程不拥有系统资源,只有其运行所必需的一些数据结构:TCB, a program counter, a register set, and a stack. 它与该进程内其它线程共享该进程所拥有的全部资源

线程和进程的区别

  1. 进程是资源分配的基本单位,所有与该进程有关的资源分配情况,进程也是分配主存的基本单位,它拥有一个完整的虚拟地址空间。而线程与资源分配无关,它属于某一个进程,并与该进程内的其它线程一起共享进程的资源。
  2. 不同的进程拥有不同的虚拟地址空间,而同一进程中的多个线程共享同一地址空间。
  3. 进程调度的切换将涉及到有关资源指针的保存及进程地址空间的转换等问题。而线程的切换将不涉及资源指针的保存和地址空间的变化。所以,线程切换的开销要比进程切换的开销小得多。
  4. 进程的调度与切换都是由操作系统内核完成,而线程则既可由操作系统内核完成,也可由用户程序进行。
  5. 进程可以动态创建进程。被进程创建的线程也可以创建其它线程。
  6. 进程有创建、执行、消亡的生命周期。线程也有类似的生命周期

线程模型

  • 用户线程,一对多
    • 线程在用户态中运行,运行与调度在用户空间中运行,内核无法感知,出问题无法切换,多个线程不能并发执行在多个处理器上(内核中只看到一个进程)
  • 一对一模型
    • 可以并行在多个处理器上
    • 内核开销大
  • 多个多模型

进程同步:对多个相关进程在执行次序上的协调,用于保证这种关系的相应机制称为进程同步。

进程间通信问题

  • 竞争:竞争共享资源,导致运行的结果和进程执行的顺序相关; 解决方法:互斥,某种方法来确保如果一个进程正在使用一个共享的变量或文件,将被其他进程占用不能做同样的事情

四种必要情况去保证互斥

  1. 不能有两个进程同时在临界区中
  2. 不能假设CPU的速度以及数量
  3. 任何运行在关键区域之外的进程都不能阻止另一个进程
  4. 没有进程必须永远等待才能进入关键区域

忙等待的互斥

  1. 屏蔽中断
    1. 每个进程刚进去临界区便屏蔽所有终端
    2. 如果屏蔽中断后忘记打开中断会导致系统的崩溃
    3. 如果系统是多处理器,屏蔽中断只会对单个cpu有效
  2. 锁变量
    1. 可能会有多个进程同时进入到临界区中
  3. 严格轮换法
    1. 不断测试变量直到某一个值的出现为止,称为忙等待;
    2. 在认为等待时间非常短的情况下,用于忙等待的锁,称为自旋锁

睡眠与唤醒:生产者和消费者问题

信号量:检测信号量的数值、修改变量数值都是不可分割的原子操作。在操作完成或者阻塞前,其他进程都是无法访问该信号量的。

管程的引入

信号量的缺点:用信号量可实现进程间的同步,但由于信号量的控制分布在整个序中,其正确性分析很困难

引入管程:把信号量及其操作原语封装在一个对象内部;管程是管理进程间同步的机制,它保证进程互斥地访问共享变量,并方便地阻塞和唤醒进程。

经典的进程通信问题

  • 生产者消费者;等待唤醒机制
  • 哲学家就餐问题;通过增加信号量,保证有一位哲学家可以吃到
  • 读写问题;对资源加锁;对读者之间用锁保证互斥

CPU调度问题

首先是处理机调度算法的共同目标(就与OS的共同目标一样):

  • 资源利用率高:系统中处理机和其他资源都应尽可能的保持忙碌状态,其中最重要的资源是处理机
  • 公平性:诸进程都获得合理的CPU时间,不会发生进程饥饿现象
  • 平衡性:调度算法应当尽可能的保证系统资源使用的平衡性;
  • 策略强制执行:对于所制定的策略,只要需要,就必须执行,即使会造成某些工作的延迟也要执行。

作业(Job):作业是一个比程序更为广泛的概念,它不仅包含了通常的程序和数据,而且还应配有一份作业说明书,系统根据该说明书来对程序的运行进行控制。

作业从进入系统到运行,通常需要经历收容运行完成三个阶段,其对应的作业状态分别为:后备状态(后备队列中)、运行状态(创建进程,进程的生命周期)、完成状态(作业运行结束或提前中断)。

进程调度方式:分为非抢占式和抢占式两种,主要的划分方式就是进程在正常执行的过程中(发生阻塞情况例外),处理机是否可以被抢占。

非抢占式:分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生某事件而阻塞时,才把处理机分配给另一个进程; 抢占式:当一个进程正在运行时,系统可以基于某种原则(优先权原则、短进程优先原则、时间片原则),剥夺已分配给它的处理机,将之分配给其它进程。

调度发生的情况

  • 在创建一个进程后,需要决定是运行父进程还是子进程
  • 在一个进程退出时必须做调度决策
  • 当一个进程阻塞在IO和信号量时,或者由于其他原因,必须选择另一个进程运行
  • 在一个IO中断发生时,必须做出决策调度

批处理中的调度

  • 先来先服务
  • 最短作业优先
  • 最短剩余作业优先(每当新的进程时间比当前的剩余时间段,则挂起当前的);饥饿问题

交互系统中的调度

  • 轮转调度:时间片划分;时间短效率低,时间长交互效果不好
  • 优先级调度:在每个时钟都会降低当前进程的优先级;优先级可以动态(随时间递减)或者静态赋予,
    • 静态非抢占式
    • 抢占式:每次调度时选择当前已到达且优先级最高的进程。当前进程主动放弃处理机时发生调度。就是在运行进程的过程中,放弃当前进行,去进行优先级高的进程。
  • 多级队列
    • image-20210628202032099
  • 保证调度:系统跟踪各个进程自创以来使用过多少CPU的时间
  • 公平分享调度算法:对于上述的保证调度算法,是对诸进程而言体现的一定程度的公平性。但是对于用户来讲就不一定公平了。
  • 彩票调度:权重高的进程拥有更多的票,然后随机
内存管理

无内存抽象:编程时直接写死地址;这样不能同时在系统上运行两份一样程序

地址空间:保护和重定位;是一个进程可用于寻址内存的一套地址集合。每个进程都有自己的地址空间,并且这个地址空间是独立于其他程度的地址空间;动态重定位:用基址寄存器和界限寄存器;

交换技术:处理内存超载问题

  • 直接交换:把程序完整调入内存中,使用该进程一段时间后,把它存到磁盘中
  • 虚拟内存:是程序只有一部分被调入内存的情况下运行
  • 会导致空洞;用内存紧缩技术会耗时
  • 所需空间动态增长问题

空闲内存管理

  • 使用位图来管理;单元越小图越大,检索连续的指定长度的空闲空间是耗时的
  • 使用链表的储存管理:进程结束或者换出链表时块;
    • 下次适配算法
    • 最佳适配算法

虚拟内存

虚拟存储器就是作为主存储器空间扩充的一种方式,存储器管理把进程的全部信息放到辅存中,执行时先将其中的一部分装入主存,以后根据执行行为随用随调入,并且当主存中没有足够的内存空间时,存储器管理依据某些算法(页面置换算法或者是分区淘汰算法)淘汰内存中的页或者是分区。

页面:虚拟地址分为多个单元。

页帧:物理内存中相应的单元。

内存管理单元负责虚拟地址到物理地址的转换

逻辑地址:页号+偏移量;前面几位是页号后面几位是偏移量;用页号去页表(map)去查询得到物理地址+上偏移量就得到物理地址;

页表会保存在内存中,寄存器存页表指针

TLB寄存器:相当于给页表加个缓存,为了解决虚拟地址到物理地址的转换速度

页式管理:页号+偏移量

段式管理:段号+段长度+偏移量

段页式管理:段号+页表长度+页的偏移量(段表【页的长度+页表存放的位置】 需要三次访存:第一次是段表、第二次页表、第三次访目标单元)

​ 可以引用块表,将【段号和页号】作为关键字,这样只要一次访存,依旧是直接访问目标数据

分页的原理

将内存划分成多个小的分区,让一个进程的代码分布在非连续的内存地址中,一个进程按页的大小划分后,不同片段可以分开存储,但是这样就不能使用了之前连续分配的动态重定位的方式,需要额外实现定位的方法;按页的大小划分的一个较大的好处就是减少了进程的内部碎片的问题

局部性原理

  1. 时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
  2. 空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。

针对大内存的页表

  • 多级页表
    • 避免全部页表保存一直保存在内存中
  • 倒排页表
    • 将虚拟地址作hash,然后根据hash值去对应槽找节点,如果有对应的(虚拟页面,页框)则找到

页面置换算法

  • 最近未使用页面置换算法NRU
    • 定时将页面设置为没有被访问
    • 没有被访问,没有被修改
    • 没有被访问,已经被修改
    • 已经被访问,没有修改
    • 已经被访问,已经修改
  • 先进先出
  • 第二次机会页面置换算法
    • 修改FIFO,如果已经被访问,则设置为没被访问,重新进队
  • 时钟置换算法
    • 循环队列,如果R(访问)为0,直接淘汰,如果为1设为0,继续向前走
  • 最近最少使用LRU
    • 可以用老化算法来模拟;
    • 可以理解为每次都访问的放到队头
  • 最近最不常用LFU
    • 计算访问次数

页面小

  • 优点:更少的页框,更少没被使用的
  • 缺点:页表大

分段

采用分页内存管理有一个不可避免的问题:用户视角的内存和实际物理内存的分离。用户通常更愿意将内存看做是一组不同长度的段的集合,这些段之间并没有一定的顺序,因此用户通过两个量来指定地址:段号+偏移

分段和分页的差别

  1. 页式和段式管理策略都不会产生外部碎片,但都有可能产生内部碎片
  2. 页的大小是统一的,而段的大小是可变的
  3. 采用分页会导致用户视角的内存和实际内存的分离,即使用户视角的内存和实际物理内存不一样,而分段正好可以支持用户视角,使用户视角的内存和实际物理内存分布保持一致
  4. 分页对程序员来说是透明的,用户指定一个地址,该地址通过硬件分为页码和偏移,这些程序员是看不见的;而分段对程序员来说通常是可见的,用户通过两个量:段号和偏移来指定地址,这两个量作为组织程序和数据的一种方便手段提供给程序员,程序员可以通过这两个量把程序和数据指定到不同的段(程序员必须清楚段的最大长度)

文件管理

提供文件和目录的抽象,隐藏硬件设施的复杂信息;提供文件保护

同样也有文件控制块的概念

文件名-255字符

目录:包含所有文件信息的节点集合,是根据文件名检索文件的桥梁

通过FCB再次索引,索引中只有文件名,只有真正需要采取读取FCB,再根据FCB来找出文件的存放位置

文件跟踪:实现文件存储就是跟踪哪些磁盘块与哪些文件一起使用。

  • 连续分配
    • 容易实现;读取效率高;文件删除后会留下空洞;文件最大空间在创建时就要确定
  • 链表分配
    • 没有外部碎片;文件检索简单;可以做到增长;随机访问速率低,空间不一定会填满一个空
  • 索引分配
    • 只有在打开相应文件时,才需要将i-node加载到内存中。
    • 支持直接访问;没有外部碎片;索引也会占用空间开销

文件名字的管理

  • 固定长度:浪费空间
  • 线性:删除文件时会留下空洞
  • 堆:需要额外的开销

文件共享

  • 硬链接:都保存的i-node节点
    • 删除源文件时,文件并没有被删除,会导致文件一直存在(按理删了源全部都要删掉)
  • 软链接:只有一个节点是保存了i-node,其他是保存”路径“
    • 需要额外的开销

IO

管理和控制IO操作和IO设备;主要管理IO设备和对应的控制器

设备无关性:应用程序独立于具体使用的物理设备;在系统中引入逻辑设备物理设备;在应用程序中使用逻辑设备名来请求使用某类设备,而系统在实际执行中使用物理设备名;

设备驱动程序层:为内核IO子系统隐藏设别控制器的不同细节

  • 将串行位流转换为字节块
  • 执行必要的错误纠正
  • 方便主存使用

内存映射:CPU如何与设备的控制器和数据缓冲区进行通信

  • 每个控制寄存器都被分配一个IO端口,所有的IO端口形成IO端口空间
    • 直接访问
  • 将所有控制器映射到内存空间中,每个寄存器都被分配唯一的内存空间
    • 不需要特殊的保护机制来保护控制寄存器不被用户直接访问
    • 每一条引用内存的指令也可以引用控制寄存器
    • 但是需要缓存
    • 因为只有一个地址空间,所有主内存模块和所有I/O设备控制器必须检查所有内存引用才能看到该回应哪一个呢

image-20210729213204890

DMA直接存储器存取

在DMA出现之前,CPU与外设之间的数据传送方式有程序传送方式、中断传送方式。CPU是通过系统总线与其他部件连接并进行数据传输。

DMA的出现就是为了解决批量数据的输入/输出问题。DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。这样数据的传送速度就取决于存储器和外设的工作速度。

死锁:一个进程集合中每个程序都在等待只能由该进程集合中其他进程才能引发的事件,那么说明该进程集合是死锁

死锁的四个必要条件

  • 互斥条件
  • 占有和等待
  • 不可抢占
  • 环路等待

处理死锁的四种策略

  • 鸵鸟算法;发生的概率是很低的

  • 检测死锁并且恢复

    • 通过深搜+回溯来找环,如果所有节点都是可以则是安全的
    • 每种类型多个资源的死锁检测(E+R+A矩阵)
    • 恢复地方法
    • 抢占
    • 利用回滚
    • 杀死进程来恢复
  • 死锁避免

    • 资源轨迹图:不安全不代表一定会死锁,只代表有可能死锁
    • 银行家算法;分配然后回收看是否可以全部满足
  • 死锁预防:从四个条件去破坏

    • 破坏互斥条件:避免在非绝对必要时分配资源;申请资源的进程越少越好
    • 破坏占有和等待条件:规定开始执行前必须获得所有资源;当它请求时先释放自己手上的资源
    • 破坏不可抢占条件:如果遭到拒绝就释放自己的资源
    • 破坏环形等待:给资源编号,申请后面的必须先获得前面的资源

两阶段锁

第一阶段:进程试图对所有所需资源的记录进行加锁。如果成功执行第二阶段,完成后释放锁,第一阶段并没有做实际工作

如果第一阶段所需的锁已经被加锁,则释放全部锁,从头再来

通信死锁

通过超时中断死锁

活锁

互相谦让,但是没有进展

饥饿

无限制推后,虽然没有被阻塞

操作系统

进程与线程

进程的创建

  • 系统启动
  • 执行正在进行的进程调用进程的创建
  • 用户申请创建
  • 批处理的初始化

进程的结束

  • 完成任务
  • 发生了错误(自愿)
  • 发生严重错误(非自愿)
  • 被其他进程杀死

三个状态

  • 运行
  • 就绪
  • 阻塞

运行到就绪:调用程序选择另一个进程

进程的内部结构

代码块、数据、堆、栈、PCB进程控制块(用户内用、系统内用、寄存器信息)

切换的步骤

  • 保存PCB
  • 加载PCB
  • 刷新内存缓存
  • 改变地址映射
父进程和子进程

新创建的子进程几乎和父进程完全一样,子进程会获得父进程用户级虚拟地址空间(但是独立)一份副本,包括代码和数据段、堆、共享库以及与用户栈,子进程获得父进程任何打开文件描述符相同的副本,也就是子进程可以读取父进程中打开的任何文件。最大区别是拥有不同的PID;只是在创建的时候是一样的,后续的改变是相互独立的。

僵死进程

发生在子进程回收过程中,当进程由于某种原因终止时,内核不会立刻从系统中删除,而是保持已终止状态,直到父进程回收。所以一个终止了但是还没被回收的进程时僵死进程

如果父进程终止了,但是还没有回收子进程,会有一个init进程成为其养父进程,来回收子进程

fork和execve

fork会创建一个子进程,并且返回两次(用来区别父进程和子进程);execve是在原来的进程的上下文中加载运行另一个新程序,如果成功不会返回

  • 系统中的每个程序都是运行在某个进程的context中的。context是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的通用目的寄存器的内容程序计数器(PC)、环境变量以及打开的文件描述符的集合
  • 每个线程都有它自己的线程context,包括一个唯一的整数线程ID、栈、栈指针、程序计数器(PC)、通用目的寄存器和条件码。
  • 每个线程和运行在同一进程内的其他线程一起共享进程context的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)读/写数据堆以及所有的共享库代码和数据区域组成。线程也同样共享打开文件的集合。
线程的过程

创建线程

  • pthread_create:创建一个新的线程,可以在参数中绑定返回的线程tid,传入函数用于让新创线程执行这个函数,设置可以传入参数改变线程的默认值

线程终止

  • 当顶层的线程例程返回时,线程也就隐式终止
  • 通过调用pthread_exit函数,线程会显示结束。主线程调用pthread_exit会让其他所有对等线程终止
  • 其他对象线程调用exit,该函数会终止进程导致该进程相关的线程都终止
  • 零杠一个对等线程通过以线程id传入pthread_cancel来终止其他线程

回收已终止线程的资源

pthread_join函数可以等待一个函数终止

分离线程

线程是可结合的也可以是分离的,一个结合的线程能够被其他线程收回和杀死;分离的线程是不能被其他线程杀死或者回收,他的资源在它终止的时候由系统自动释放

使用pthread_detach

信号

一个小消息,通知进程系统发生了某一种类型的事件。通知用户进程发生了异常;如通过kill9一个进程向另一个进程来强制终止它;ctrl+c内核向前台进程组发送信号,一般是挂起前台进程;

发送但是还没接受的信号称为待处理信号(pending singal);任何时候,一个类型最多只有一个待处理信号。如果同一个类型的信号收到多个,就会被丢弃。

线程

  • 如何能使多个程序更好地并发执行,同时又尽量减少系统的开销,成为设计操作系统的重要目标。
  • 将进程的两个基本属性分开,由操作系统分开处理:对于作为调度和分派的基本单位,不同时作为拥有资源的单位,可以做到“轻装上阵”;
  • 对于拥有资源的基本单位,又不对之进行频繁的切换。‘’

修改为进程作为资源分配的单位,线程作为调度的单位

线程不拥有系统的资源,但是拥有其运行时必须的数据结构(TCB,程序计数器,寄存器,栈)

当然也有自己独有的部分:程序计数器、寄存器、栈、状态

线程的不同实现方案

纯用户态:线程表放在用户态中,可以自己定制调度算法,阻塞会一起阻塞(某个线程去IO);线程占有CPU,除非资源放弃,其他线程不能保证

内核态中实现:线程表放在内核中,用池化技术循环利用

轻量级进程

与普通进程相比,*LWP*与其它进程共享所有(或大部分)逻辑地址空间和系统资源,一个进程可以创建多个*LWP*,这样它们共享大部分资源;*LWP有它自己的进程标识符,并和其他进程有着父子关系*;*LWP*与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息;

用户线程与内核线程

1)可移植性:因为ULT完全在用户态实现线程,因此也就和具体的内核没有什么关系,可移植性方面ULT略胜一筹;

2)可扩展性:ULT是由用户控制的,因此扩展也就容易;相反,KLT扩展就很不容易,基本上只能受制于具体的操作系统内核;

3)性能:由于ULT的线程是在用户态,对应的内核部分还是一个进程,因此ULT就没有办法利用多处理器的优势,而KLT就可以通过调度将线程分布在多个处理上运行,这样KLT的性能高得多;另外,一个ULT的线程阻塞,所有的线程都阻塞,而KLT一个线程阻塞不会影响其它线程。

4)编程复杂度:ULT的所有管理工作都要由用户来完成,而KLT仅仅需要调用API接口,因此ULT要比KLT复杂的多。

Linux的进程

*Linux*采用的“一对一”的线程模型,即一个*LWP*对应一个线程。这个模型最大的好处是线程调度由内核完成了,而其他线程操作(同步、取消)等都是核外的线程库函数完成的。

在*LinuxThreads*中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。

进程VS线程

  • 进程是资源分配的基本单位,所有与该进程有关的资源分配情况,进程也是分配主存的基本单位,它拥有一个完整的虚拟地址空间。而线程与资源分配无关,它属于某一个进程,并与该进程内的其它线程一起共享进程的资源。
  • 不同的进程拥有不同的虚拟地址空间,而同一进程中的多个线程共享同一地址空间
  • 进程调度的切换将涉及到有关资源指针的保存及进程地址空间的转换等问题。而线程的切换将不涉及资源指针的保存和地址空间的变化。所以,线程切换的开销要比进程切换的开销小得多。
  • 进程的调度与切换都是由操作系统内核完成,而线程则既可由操作系统内核完成,也可由用户程序进行。

进程间的通信

关注点:

  • 进程如何把信息传递给另一个进程(共享同一片空间)
  • 进程在关键点直接不会出现交叉
  • 正确的顺序

进程同步:在多个进程执行次序上的协调;相互合作的一组并发进程在一些关键点上可能需要相互等待与互通信息,保证这种关系的就叫进程同步

竞争条件:多个进程对共享资源,导致最后的结果与操作的顺序有关

互斥:保证进程在使用共享资源的时候,其他进程不能进行相同的操作

保证互斥的四个条件
  • 任何两个进程不能同时处于临济区
  • 不应对cpu的速度和数量作任何假设
  • 临界区域外进程不能阻塞其他进程
  • 不能使进程无限期等待进入临界区
忙等待的互斥

信号屏蔽

  • 信号屏蔽
    • 多核中不好用,是针对操作系统的,忘记开启有问题
  • 锁变量
    • 获取并将其变成1,但是会有问题,因为进程可能随时切换,还是会卡进去
  • 严格轮换法
    • 忙等待,属于自己的就进去,但是违反了条件3,阻塞了别的进程

睡眠与唤醒

  • 信号量:控制进去临界区域的进层数;如果失败就在该位置睡眠,然后等待信号量增加后,系统随机选取一个唤醒

信号量可以实现进程间的通信,但是由于信号量的控制分布在整个程序中,正确性难分析

  • 管程:引入了条件变量,wait和signal,当发现无法继续运行时会在变量上wait,导致进程阻塞;它存在于内存中,进程可以对它进行读写,它提供流控制,保证进程的正确读写,即管道为空时读进程会阻塞,管道为满时写进程会阻塞,以此实现进程之间的通信。
进程间的通信方式

要么是陷入内核,要么是涉及外设

  1. 管道( pipe ): 管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。以内存文件的形式存在
  2. 匿名管道:克服了亲缘的限制,以磁盘文件的形式存在,先进先出,可以任意进程间通信
  3. 信号量(semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  4. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。存在内核中说明其内核重启或者显式删除的时候才被删除
  5. 信号 (sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  6. 共享内存(shared memory ) : 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。就是拿出一块虚拟地址空间来,映射到相同的物理内存中。需要某种同步操作来保证,例如信号量
  7. 套接字(socket ) : 套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。

理解:共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,但是多进程竞争同个共享资源会造成数据的错乱

线程间通信方式
  1. 锁机制:包括互斥锁、条件变量、读写锁:只有拥有互斥对象的线程才有访问公共资源的权限
    • 互斥锁提供了以排他方式防止数据结构被并发修改的方法。
    • 读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
    • 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
  2. 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
  3. 信号机制(Signal):类似进程间的信号处理 线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制

用户态切换到内核态的方式

  1. 系统调用

这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。

  1. 异常

当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常

  1. 外围设备的中断

当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

调度

发生调度的时间

  • 新的进程被创建
  • 进行的进程退出
  • 进行的进程被阻塞
  • IO中断
  • 时钟中断

调度算法

  • FIFO:队列、链表
  • 最短作业优先:优先队列
  • 最短剩余时间优先
  • 优先级调度(优先级的静态和动态)
  • 多级队列(不同队列的时间片不同,2幂增)
  • 保证调度(跟踪已经使用的时间)
  • 彩票调度(优先级高拥有的票多)

存储管理

地址的抽象

多个应用程序处于内存中互补影响,要满足保护和重定位的问题

交换技术和虚拟内存

空间的分配问题(交换中引起了空洞问题)

  • 位图
    • 但是内存很大时,位图也很大
  • 链表的方式
    • 算法
    • 首次适配
    • 最佳适配(要遍历一遍)

虚拟空间

将程序切割成片段,但是前期时由程序员完成,将这个工作交给系统过去完成;

讲程序中的逻辑地址通过内存管理单元MMU进行转换,

虚拟空间中的页面=物理地址中的页框

虚拟地址=虚拟地址页号+偏移量

页表项

image-20210717232227535

通过虚拟地址页号去页表找对应的页框同时判断其在不在内存中

TLB

在页表也很大时时候;以及映射得不够快

相当于给页表再做一个缓存

多级页表

从页号去找二级页表号,从二级页表号根据第二段找到框号,再加上偏移量

倒查页表

根据物理地址页框数量作为哈希数组长度,然后进行哈希

页表置换算法

  • 最近未使用算法NRU:

    • 两位(是否被访问,是否被修改),定期抹除访问标志(变成0)
  • FIFO

  • 第二次机会置换算法

    • FIFO的优化,如果被访问过就将其痕迹抹掉重新入队
  • 时钟置换算法

    • 相当于给第二次置换算法,用循环链表来实现
  • 最近最少使用算法LRU

    • 老化算法
    • 双向链表和HashMap实现
  • LFU最不经常使用

    • 计算频率
分段技术

按照类型把进程的地址空间分为多个,每一种功能对应一个地址空间,独立增长,这就是分段思想,分段使得我们不需要关心如何管理和分割地址空间。分段与分页的区别在于,分段存在于逻辑地址的概念上,是一种划分逻辑地址的思想,而分页是解决逻辑地址到物理地址的映射过程的。

文件系统

文件命名

  • 长度为255
  • windows大小写不敏感,linux是大小写敏感

文件结构

  • 线性+定长
  • 树状结构

目录结构

  • 单层结构
    • 难以查找文件
  • 两层结构
    • 解决了命名冲突问题
    • 提高了搜索效率
    • 可以文件共享和保护
  • 多层次
    • 文件和目录

空间分配问题

  • 顺序存储
    • 容易实现、可以直接访问和顺序访问
    • 外部碎片,文件的长度必须提前知道
  • 链式存储
    • 没有外部碎片、文件变长
    • 随机访问性能差,存在结构性开销
  • i-node
    • 所有的i-node统一存储
    • 每个文件都有自己i-node结点,支持直接访问,不存在外部碎片
    • i-node节点的结构性开销

共享文件

  • i-node
    • 硬链接,每个节点都会指向这个文件
  • 符号链接
    • 其他接待保存路径

IO

分类

  • 块设备:把信息存储在固定的大小中,每个块都有自己的地址,每个块都能独立于其他块而读写
  • 字符设备:不能寻址,也没有任何寻道操作

轮询

cpu的干预非常频繁

image-20210729214538130

中断驱动

容控制器自己完成任务后向cpu发一个中断信号,处理中断过程就是从控制器中读一个字的数据到cpu寄存器中,再写入内存,接着恢复去执行别的,等待下一次中断信号;但是中断次数过多也不好

image-20210729215353822

IO多路复用技术

IO多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部空间,使得流之间共享数据非常容易;

但是并发粒度的缩小会让代码量上升,而且使得如果一个逻辑流在忙于读文本行,其他是无法进展的

DMA

image-20210729220610942

5.主要缺点和主要优点

优点:数据传输以“块”为单位,CPU介入频率进- -步降低。数据的传输不再需要先经过CPU再写入内存,数据传输效率进- -步增加。CPU和/O设备的并行性得到提升。

缺点: CPU每发出一- 条I/0指令,只能读/写一个或多个连续的数据块。

控制器的任务

把串行的位流转换成字节块,并且进行有必要的错误校验

CPU如何与设备控制器何数据缓冲区进行通信

  • 每个控制存储器被分配一个IO端口;只能由操作系统能对其访问;使用特殊的指令去读写
  • 讲IO映射到内存空间中;像访问内存空间一样去访问;不需要特殊的保护机制去组织用户进程执行IO操作

五种IO模型

  1. 同步阻塞IO

preview

  1. 同步非阻塞IO

非阻塞IO是在应用调用recvfrom读取数据时,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。在没有数据的时候会即刻返回错误标识,那也意味着如果应用要读取数据就需要不断的调用recvfrom请求,直到读取到它数据要的数据为止。

  1. 多路复用IO

preview

  • 为什么会产生多路复用IO?

    因为当有多个网络连接,连接到某个进程的时候,我们想要监听这些socket接口,并当这些接口有数据返回的时候,进行处理。一种解决方案就是,对于每一个socket接口,我们都开辟一个线程来侦听,处理。这样做的局限就是,当连接数变大的时候(成千上万),那么我们就要创建多个线程变量。创建线程变量开销很大,而且,线程切换的开销也会变大。所以我们就寻求一种,单线程的情况下监听多个socket接口的方式,所以就会有多路复用IO的诞生。

    本质上还是同步非阻塞IO,但是将阻塞放在了select上

  • 信号驱动IO,继续改良复用IO,思想是发出请求后等你数据准备好了就通知我

  • 异步IO

preview

异步IO需要更强的操作系统支持;当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

死锁

死锁的定义:如果一组进程中每个进程都在等待一个事件,而这个事件只有该集合中的另一个进程才能引起,那么该集合就是死锁的。(都占有资源,然后互相等待)

引起死锁四个条件

  • 互斥条件:资源要么分配给一个进程,要么可用
  • 占有资源和等待
  • 不可抢占
  • 环路等待

死锁检测

  1. 建模之后寻找环路
  2. 多资源的死锁检测,矩阵,先给能满足的,然后释放

死锁恢复

  • 抢占
  • 回滚
  • 杀死进程

死锁避免

  • 资源轨迹图
  • 安全状态和不安全状态
  • 单个银行家算法
  • 多个资源的银行家算法

死锁预防

  • 破坏互斥条件:有些资源根本不能同时访问,如打印机等临界资源只能互斥使用。所以,破坏互斥条件而预防死锁的方法不太可行,而且在有的场合应该保护这种互斥性。

  • 破环占有和等待条件:预先静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不把它投入运行。一旦投入运行后,这些资源就一直归它所有,也不再提出其他资源请求,这样就可以保证系统不会发生死锁。会有“饥饿”

  • 破环不可抢占资源:当一个已保持了某些不可剥夺资源的进程,请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着,一个进程已占有的资源会被暂时释放,或者说是被剥夺了,或从而破坏了不可剥夺条件。

  • 破坏循环等待条件:规定每个进程,必须按编号递增的顺序请求资源

活锁:互相谦让,导致谁都没办法开始

中断

中断主要是一个cpu硬件产生的定时脉冲,为了能通知操作系统是时间片到了,将cpu的使用权限从用户态切换到内核态中,内核态去根据中断信号进行不同的处理;

中断可能来自cpu内部,也可能来自外部;来自内部就是用户申请使用特权指令、时间片到了;外部一般是外设导致,如键盘输入

系统调用:

用户态程序通过系统调用的方法来申请操作系统的服务,因为其可能涉及到一些特权指令才能完成

陷入指令:是唯一一个只能在用户态不能在内核态执行的指令

你可能感兴趣的文章

Vue如何使用G2绘制图片

Docker Compose入门学习

DockerDesktop入门简介

Docker图形化工具Portainer介绍与安装

1.Docker

Docker操作系统之Alpine

如何将镜像推送到阿里云容器镜像服务

对象存储MinIO入门介绍

ElasticSearch安装与介绍

Beats入门简介

1  赞