操作系统总结

进程和线程的区别

  • 本质区别
    • 进程是资源调度以及分配的基本单位
    • 线程是 CPU 调度的基本单位
  • 所属关系:一个线程属于一个进程,一个进程可以拥有多个线程。
  • 地址空间
    • 进程有独立的虚拟地址空间
    • 线程没有独立的虚拟地址空间:线程有调用栈、程序计数器(PC)、本地存储(LS)等少量独立空间。
  • 内存
    • 系统会为每个进程分配不同的内存空间
    • 系统不会为线程分配内存线程所使用的资源来自其所属的进程
  • 并发性
    • 进程的并发性较低
    • 线程的并发性较高
    • 对于单个 CPU,操作系统会把 CPU 的运行时间划分为多个时间段,再将时间段分配给各个线程执行
    • 切换效率:
      • 进程切换效率低(因为所属的资源多),线程切换效率高
      • 都会涉及到上下文切换
  • 健壮性
    • 一个进程崩溃后,不会影响其他进程,进程的健壮性高。
    • 一个线程崩溃后,导致整个进程崩溃,线程的健壮性低。
    • 进程隔离性强一些

进程与线程的上下文切换过程

  • 进程由哪几个部分构成:
    • task_struct
    • 进程的虚拟地址空间。
  • 上下文由哪几个部分构成:
    • 用户级上下文 → 虚拟地址空间:
      • 代码。
      • 数据。
      • 堆栈等。
    • 系统级上下文 → task_struct
      • 进程标识信息。
      • 进程现场信息。
      • 进程控制信息。
      • 系统内核栈
    • 寄存器上下文(硬件上下文)。
      • CPU 各寄存器的内容。
      • 进程的现场信息,存储在系统内核栈(中断栈)
  • 何时发生切换:
    • 主动:系统调用,产生软中断。
    • 被动:时间片到了,时间中断。
    • 由中断来完成切换
  • 进程切换过程:
    • 保存当前进程的硬件上下文。
    • 修改当前进程的状态(存储在 PCB 中),状态由运行态改为就绪态或阻塞态,加入相关队列(就绪队列或阻塞队列)
    • 调度另外一个进程。
    • 修改被调度进程的状态,将其状态改为运行态。
    • 把当前进程的存储管理数据改为被调度进程的存储管理信息(页表、cache TLB → 用户级上下文切换。
    • 恢复被调度进程的硬件上下文,让 PC 指向被调度的进程代码。

进程调度策略有哪几种

  • 调度对象:
    • 线程和进程相对于操作系统而言都是任务(task_struct
    • 线程也被称之为共享用户虚拟地址空间的进程
    • 包含多个线程的进程被称之为线程组
    • 只有一个线程的进程被称之为进程
    • 没有用户虚拟地址空间的进程被称之为内核线程(内核中的所有线程共享内核虚拟地址空间)
  • 进程调度用于决定由谁(哪个或哪几个)获得处理器的执行权
  • 进程的状态:
    • 从新建到就绪:当进程创建完毕,初始化所有必要资源后,它被放入就绪队列。
    • 从就绪到运行:调度程序从就绪队列中选择一个进程并分配 CPU 时间片。
    • 从运行到就绪:当进程的时间片用完但进程尚未完成时,它会被放回就绪队列。
    • 从运行到阻塞:如果进程请求了当前不可用的资源或等待操作完成,它将转入阻塞状态。
    • 从阻塞到就绪:当进程等待的事件发生或资源变得可用时,它将被移回就绪队列。
    • 从运行到退出:进程完成所有操作并自行退出或由于某种原因被操作系统终止。

在这里插入图片描述

  • 调度时机:
    • 主动调度:系统调用等待某个资源。
    • 周期调度:某些进程不主动让出 CPU,内核依靠周期性时钟来抢占调度。
    • 唤醒抢占、创建新进程抢占、内核线程抢占。
  • 进程调度算法:
    • 先来先服务(FCFS):
      • 也称为先进先出。
      • 从就绪队列中选择存在时间最长的任务执行调度。
    • 短作业优先(SJF):
      • 从就绪队列中选择估计运行时间最短的任务执行调度。
    • 高响应比优先
      • FCFSSJF 的综合。
      • 综合考虑等待时间和估计运行时间来选择任务执行调度。
    • 时间片轮转调度:
      • 为任务分配时间片,当执行完时间片后放入就绪队列。
    • 优先级调度:
      • 从就绪队列中选择优先级最高的若干任务执行调度。
      • 优先级用来描述运行的紧迫程度。
    • 多级反馈队列调度
      • 时间片轮转调度和优先级调度的综合。
      • 动态调整任务的优先级和时间片大小,从而兼顾多方面的系统目标。
      • linux(会根据时间片的剩余时间动态调整优先级,更加公平)windows(严格按照优先级取出任务分配时间片) 采用。

后台进程有什么特点

  • 前台进程:
    • 运行在前台的进程。
    • 终端是该进程的控制终端。
    • 如果终端关闭,则向依赖该终端的所有进程发送一个 SIGHUP 信号,然后进程退出。
    • 可接收终端输入,并可在终端输出。
  • 后台进程:
    • 运行在后台的进程,若在终端运行,终端关闭,进程可能退出
    • 不可接收终端输入,可在终端输出。
  • 前后台程序切换:
    • Ctrl + Z:将前台程序切换为后台程序,但是后台程序是挂起状态。
    • jobs:显示所有的后台程序。
    • fg %编号:将后台程序切换为前台程序。
    • bg %编号:让后台程序处于运行状态
    • & = (Ctrl + Z) + bg %编号
    • nohup:忽略 SIGHUP 信号。
    • Ctrl + D:断开终端 session,依赖该终端的所有进程都会收到 SIGHUP 信号。
  • 守护进程
    • 后台进程的延伸,脱离终端的后台进程,不需要考虑 SIGHUP 信号
    • 如何成为守护进程:
      • fork 子进程,并让父进程退出,从而让子进程被 init 进程接管,成为孤儿进程
      • 使用 setsid() 系统调用建立新的进程会话,让孤儿进程成为新建会话的首进程,从而脱离与终端的关联
      • 打开 /dev/null,把 012 重新定向到 /dev/null,从而防止守护进程意外地从终端接收输入或向终端发送输出。
      void daemonize(void) {int fd;if (fork() != 0) exit(0); // 父进程退出setsid(); if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);if (fd > STDERR_FILENO) close(fd);}
      }
      

用户态和内核态

  • 用户态:用户程序运行时的状态。
  • 内核态:操作系统运行时的状态。
  • 为什么要区分用户态和内核态 ?
    • 内核态具备特权,能够操作外部资源
    • 为了系统的安全与稳定。
  • 如何实现用户态和内核态 ?
    • CPU
      • 特权寄存器 → 操作系统使用。
      • 普通寄存器 → 用户程序使用。
    • 内存:
      • 内核空间 → 操作系统使用。
      • 用户空间 → 用户程序使用。
  • 什么时候需要切换 ?
    • 用户态程序在需要操作系统完成特权操作的时候才发生切换。
  • 什么条件引起用户态与内核态之间的切换 ?
    • 系统调用
    • 异常,比如缺页异常
    • 中断
  • 用户态和内核态的区别:
    • 运行实体不同。
    • 是否具备特权。
    • 是否安全和稳定。
    • 实现机制。

描述系统调用整个流程

  • 系统调用是内核给用户程序提供的编程接口
  • 为什么需要系统调用:
    • 内核具有最高的权限,可直接访问所有资源。
    • 而用户只能访问受限资源,不能直接访问内存、网络、磁盘等硬件资源。
  • 中断:使程序从用户态切换到内核态或者从内核态切换到用户态
    • 属性:根据中断号去查询中断向量表,从而找到中断处理程序
      • 中断号。
      • 中断向量表。
      • 中断处理程序。
    • 类别:
      • 硬件中断。
      • 软件中断系统调用对应 int 0x80中断号是 0x80
  • 系统调用是否会引起进程或线程切换 ?
    • 不一定。
    • 如果是阻塞 IO,且 IO 未就绪,将引起线程切换;线程将由运行态转为阻塞态。
    • 如果是非阻塞 IO,且 IO 未就绪,不会引起线程切换,仅仅涉及到用户态和内核态之间的切换。
  • 系统调用引起中断上下文切换
    • 进程状态先由用户态转为内核态,再由内核态转为用户态。
    • 保存运行现场:保存 CPU 寄存器原来的用户态指令。
    • 运行内核代码:更新 CPU 寄存器内容为内核态指令,执行内核代码。
    • 恢复运行现场:更新 CPU 寄存器内容为用户态指令。
  • 系统调用流程:
    • 触发中断:
      • 将系统调用号放入 eax 寄存器
      • 执行 int 0x80
    • 切换堆栈:用户态切换为内核态。
    • 执行中断处理:
      • 根据中断号找到中断处理程序 → 通过 0x80 找到 system_call
      • 根据系统调用号从系统调用表上找到系统调用处理函数并调用
    • 从中断处理程序返回:通过 iret 将返回值返回,并且程序从内核态切换为用户态

CPU 是怎么执行指令的

  • 指令是二进制的机器码,由程序编译产生
  • 程序编译运行过程
    • 编译阶段:通过词法句法分析,源代码 → 汇编代码。
    • 汇编阶段:汇编代码 → 二进制机器码(.o 目标文件)
    • 链接阶段:目标文件 + 库文件 → 可执行程序。
    • 载入阶段:
      • 将可执行程序加载到内存中
        • 给进程分配虚拟内存地址空间。
        • 创建页表。
        • 加载代码段和数据段等数据,并在页表中写入映射关系。
      • 加载器把入口指令地址写入程序计数器
  • CPU 采用流水线的方式执行指令;指令执行过程被称之为指令周期CPU 周而复始地执行指令周期。
  • 指令执行过程涉及到两个组件:CPU 和内存,CPU 和内存之间通过总线进行交互。
    • CPU
      • 寄存器:
        • 通用寄存器 → 存储计算时所需的数据。
        • 程序计数器存储要执行的指令的地址(虚拟内存地址)
        • 指令寄存器存储要执行的指令
      • 控制单元(控制器)。
      • 逻辑运算单元(运算器)。
    • 总线:
      • 地址总线操作内存地址
      • 数据总线读写内存数据
      • 控制总线 → 用来发送或者接收信号。
  • 指令周期:
    • 取得指令
      • 控制单元程序计数器获取指令地址,通过地址总线通知内存准备数据。
      • 如果内存数据准备好了,控制单元通过数据总线获取指令内容,然后写入指令寄存器
    • 指令译码CPU 将指令解析成不同的操作信息。
    • 执行指令CPU 将指令放入逻辑运算单元进行运算。
    • 数据回写控制单元将操作结果通过数据总线回写到内存中。
  • 开启下一个指令周期:控制单元程序计数器中的指令地址 +8(如果是 64 位操作系统)。

内存管理有哪几种方式

  • 背景:
    • 为了多进程之间的内存地址访问不受影响并且相互隔离,操作系统会为每个进程独立分配虚拟地址空间
    • 每个进程都有虚拟地址空间,而物理内存只有一个,当启动程序过多时,实际使用的内存超过物理内存,则会发生内存交换
    • 内存交换:把不常用的内存交换到磁盘中(换出),需要的时候再加载回内存(换入)
    • 虚拟地址需要映射到物理地址。
    • 怎么管理映射关系。
  • 内存管理的方式有三种:分段内存管理、分页内存管理、段页式内存管理。

分段内存管理

  • 程序逻辑分段:代码段、数据段、堆段、文件映射段、栈段。
  • 虚拟地址:段号段内偏移量等构成。
  • 根据段号段表找到段内起始地址加上段内偏移量得到真实的物理地址
  • 优点:程序无需关注具体的物理内存地址。
  • 缺点:
    • 会产生外部内存碎片 → 段与段之间的间隙不足以容纳其他程序。
    • 每次内存交换都会把一个程序的全部内存换出内存交换效率低

分页内存管理

  • 虚拟内存物理内存划分为多个连续且固定大小的页linux 页大小为 4KB
  • 虚拟地址通过查找页表获得物理地址
  • 虚拟内存地址转换为物理内存地址的工作是由 CPU 中的 mmu 来处理的。
  • 若在页表中找不到物理地址,则会触发缺页异常(缺页中断)
  • 优点:
    • 页与页是紧密排列的,不会有外部内存碎片
    • 在内存交换中,只会将若干个页换出内存交换效率高
    • 装载程序时,无需一次性把程序装载到物理内存中,运行中需要时才会加载到内存。
  • 缺点:
    • 分配内存的最小单位是一页,可能没用到一页数据,造成内部内存碎片
    • 单级分页(每一个页表大约 4MB 将耗费更多内存用于存储页表。

段页式内存管理

  • 将程序划分为多个逻辑段
  • 划分为多个
  • 虚拟地址:段号段内页页内偏移
  • 段页式内存管理得到物理地址需要三次内存访问
    • 访问段表,得到页表起始地址
    • 访问页表,得到物理页号
    • 根据物理页号页内偏移值得到物理地址
  • 优点:提高了内存利用率。
  • 缺点:增加了系统开销。

malloc 是如何分配内存的

在这里插入图片描述

  • 背景:
    • 进程的虚拟内存空间分布(由低地址到高地址划分):
      • 代码段:存储二进制可执行代码。
      • 已初始化的数据段:静态常量。
      • 未初始化的数据段:未初始化的静态变量或全局变量。
      • 堆段:动态分配的内存,由低地址往高地址分配。
      • 文件映射段:动态库、共享内存等,由高地址往低地址分配。
      • 栈段:局部变量、函数调用的上下文等。
      • 内核空间
    • malloc 分配的是物理内存还是虚拟内存 ?
      • 分配的是虚拟内存
      • 如果分配的内存没有被访问,则不会映射到物理内存 → 不访问就不会占用物理内存
      • 如果分配的内存被访问了,则通过缺页异常建立虚拟内存到物理内存的映射关系
  • malloc 分配内存的过程:
    • 如果分配内存的大小 < < < 128 KB,则通过 brk 系统调用堆区分配内存。
      • 由低地址往高地址分配。
      • 优先从内存池中进行分配,可以减少系统调用的次数
      • 页表中的映射关系仍然存在,可以减少缺页异常的次数
      • 缺点:频繁 malloc / free 会出现很多的内部内存碎片
    • 如果分配内存的大小 ≥ \geq 128 KB,则通过 mmap 系统调用文件映射区分配内存。
      • 由高地址往低地址分配。
      • 每次都会发生系统调用,进行用户态到内核态的切换。
      • 第一次访问内存时将发生缺页异常
  • free 如何工作:
    • 释放内存后,是否会把内存归还给操作系统 ?
      • 如果是通过 brk 系统调用分配内存,该内存仍然在 malloc 的内存池中下次可以继续使用
      • 如果是通过 mmap 系统调用分配内存free 后会立刻归还给操作系统
    • free(ptr) 传入的是内存地址,如何知道释放多大的内存 ?
      • malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 个字节
      • 16 个字节记录了内存块的描述信息,其中包含了内存块的大小
      • 当释放内存时,会free 传入的内存地址向左偏移 16 个字节从而分析出该释放多大的内存

页面置换算法有哪些

  • 背景:
    • 缺页异常:当 CPU 访问的页面不在物理内存时,便会产生缺页异常,请求操作系统将缺页调入到物理内存。
    • 缺页异常处理流程(页面目前位于磁盘上的交换空间):
      • CPU 中访问 load M 指令,接着查找 M 对应的页表项
      • 若页表项有效,CPU 直接访问物理内存,若无效,则 CPU 发起缺页异常
      • 操作系统收到缺页异常,执行缺页异常处理函数,先查找页面在磁盘中的位置
      • 找到磁盘中的页面后把页面换入到物理内存空闲页中
      • 修改页表中对应页表项的状态位为有效
      • CPU 继续执行访问物理内存。
    • 缺页异常处理流程(页面目前位于尚未初始化的内存区域):
      • 进入内核态分配物理内存更新进程页表映射关系返回到用户态恢复进程的运行
    • 如果没有找到物理内存空闲页,则需要调用页面置换算法将一个物理页换出到磁盘,并将页表项置为无效
  • 页面置换算法:
    • 出现缺页异常,且物理内存无空闲页时,选择被置换物理页面的过程。
    • 选择物理页换出到磁盘,把需要访问的页换入到物理页。
    • 目标:尽可能减少页面的换入换出次数

最佳页面置换算法

  • 置换在未来最长时间不访问的页面。
  • 每个页需要记录访问时间。
  • 实际系统不会实现,代价太大。

先进先出置换算法

  • 选择在内存驻留时间最长的页面进行置换。
  • 实际系统不会使用,可能没法实现目标。

最近最久未使用置换算法

  • 选择最长时间没有被访问的页面进行置换。
  • 代价很高,需要在内存中维护更新所有页面的链表,开销较大。

最不常用置换算法

  • 选择访问次数最少的页面作为置换页。
  • 每个页面都需要记录访问次数。
  • 实际不会使用。

时钟页面置换算法

  • LRU 的一种改进。
  • 把所有页面保存在环形链表中,并用一个指针指向最久未使用的页面检查具体页的访问位
  • 1:访问过。0:未访问。
  • 移动过程中若访问位为 1,则清除访问位(置为 0);遇到 0,选择为置换页
  • 优化:加一个修改位,由访问位和修改位构成元组
    • u = 0, m = 0 → 未访问且未修改。
    • u = 1, m = 0 → 访问过但未修改。
    • u = 0, m = 1 → 未访问但修改过。
    • u = 1, m = 1 → 既访问又修改过。

写文件时进程宕机,数据会丢失吗

在这里插入图片描述

  • 写文件流程:
    • 通过 stdio 函数库选择具体的函数,把数据先写到 stdio 缓冲区中减少系统调用的次数
      • 也可以使用 setbuf 自定义用户缓冲区。
    • 通过 IO 系统调用 write 等,写到内核缓冲区 page cache减少磁盘 IO 的次数
    • 由内核发起的写操作(我们无法控制)。
      • 用户层可以调用 fsyncfdatasyncsync 将内核缓冲区中的数据刷到磁盘。
  • page cache
    • 优点:加快对数据的访问、减少磁盘 IO 的次数。
    • 缺点:
      • 占用了物理内存空间,可能会引发频繁的 swap 操作。
      • 用户层无法优化 page cache 的使用策略,通常数据库需要自己实现 page 管理。
  • 两种磁盘 IO 方式:
    • 缓存文件 IO(小文件使用)
      • 不直接与磁盘交互,而是通过 page cache 进行缓存。
    • 直接文件 IO(大文件使用)
      • 不通过 page cache,直接与磁盘交互。
  • 如果写文件没有调用 fflush/write,则数据丢失,因为数据还在用户态缓冲区。
  • 如果写文件调用了 fflush/write,则数据在内核缓冲区,只要系统不关闭,那么数据就不会丢失。
  • 假设进程宕机后,系统马上也关闭了:
    • 如果系统关闭前调用了 fsyncfdatasyncsync,则数据不会丢失。
    • 否则数据可能丢失,主要看系统是否调用了下面的三个接口。

在这里插入图片描述


磁盘调度算法有哪些

  • 背景:
    • 目的:为了提高磁盘的访问性能。
    • 磁盘访问:
      • 旋转时间:4ms
      • 寻道时间:10ms
      • 数据传输时间:0.3ms
  • 磁盘调度的主要目标:使磁盘的平均寻道时间最短

先来先服务

  • 按照磁盘访问的请求顺序进行调度
  • 优点:是公平的调度方式。
  • 缺点:未做任何优化,平均寻道时间较长。

最短寻道时间优先

  • 优先调度与当前磁道距离最近的磁道
  • 优点:平均寻道时间较短。
  • 缺点:离密集访问磁道较远的请求将出现饥饿现象。

电梯扫描算法

  • 先按一个方向来进行磁盘调度,直到处理完该方向的所有请求后才改变调度方向
  • 优点:解决了最短寻道时间优先的饥饿问题。
  • 缺点:中间调度频率高,两边调度频率低。

循环扫描算法

  • 朝特定方向进行磁盘调度,返回时直接复位磁头,仍朝原来的方向进行调度
  • 磁道只响应一个方向上的请求
  • 相较于电梯扫描算法,各个位置磁道响应频率相对平均。

进程间通信有哪几种方式

管道

  • 父子进程间通信。
  • 半双工。
  • 速度慢、容量有限。
#include <unistd.h>int pipe(int fd[2]);
read(fd[0], buffer, size);
write(fd[1], buffer, size);
// 获取上传后返回的信息 fileid
int fd[2];
if (pipe(fd) < 0) return; // 创建无名管道:fd[0] → read,fd[1] → writepid_t pid;
pid = fork();  // fork 子进程
if (pid < 0) return;if (pid == 0) { // 子进程close(fd[0]); // 关闭读端dup2(fd[1], STDOUT_FILENO); // 将标准输出重定向到写管道 // 往标准输出写的东西都会重定向到 fd 所指向的文件 → 当 fileid 产生时输出到管道 fd[1] // 通过 execlp 执行 fdfs_upload_file → fdfs_upload_file /etc/fdfs/client.conf video.mp4execlp("fdfs_upload_file", "fdfs_upload_file", s_dfs_path_client.c_str(), file_path, NULL); // 如果 execlp 调用成功, 那么新程序将开始执行,原程序的代码则不会再继续执行, execlp 后边的代码也就不会执行了LogError("execlp fdfs_upload_file error"); // 执行失败close(fd[1]);
} else { // 父进程close(fd[1]); // 关闭写端read(fd[0], fileid, TEMP_BUF_MAX_LEN); // 等待管道写入然后读取// 等待子进程结束, 回收其资源wait(NULL); close(fd[0]);
}

FIFO

  • 命名管道。
  • 可以在无关的进程间交换数据。
  • 速度慢。
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char* pathname, mode_t mode);mkfifo("fifo_file", 0600);
int fd = open("./fifo_file");
read(fd, buffer, size);
write(fd, buffer, size);

消息队列

  • 消息的链接表,存储在内核中。
  • 消息队列独立于进程。
  • 速度慢、容量有限。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>// ftok 生产一个 ipc key
key_t ftok(const char* pathname, int proj_id);// 根据 ipc key 获取消息队列唯一标识
int msgget(key_t key, int flag);int msgsnd(int msqid, const void* ptr, size_t size, int flag);int msgrcv(int msqid, void* ptr, size_t size, long type, int flag);
key_t key = ftok(".", 'z');int msgctl(int msqid, int cmd, struct msqid_ds* buf);

共享内存

  • 多个进程共享的一块存储区。
  • 最快的一种 IPC
  • 需要和信号量配合使用。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>// ftok 生产一个 ipc key
key_t ftok(const char* pathname, int proj_id);// 根据 ipc key 获取共享内存唯一标识
int shmget(key_t key, size_t size, int flag);void *shmat(int shm_id, const void* addr, int flag);
int shmdt(void* addr);
int shmctl(int shm_id, int cmd, struct shmid_ds* buf);

信号量

  • 信号量是一个计数器,用于进程间的互斥与同步
#include <semaphore.h>/*pshared == 0 → 一个进程中的多线程之间通信pshared != 0 并且是共享内存的唯一标识 → 多进程之间通信
*/
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_post(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_close(sem_t* sem);
int sem_destroy(sem_t* sem);

信号

  • 信号本质上是一种软中断,一种通知机制
  • 处理方法:忽略、捕获、默认动作。
sighandler_t signal(int signum, sighandler_t handler);
int kill(pid_t pid, int sig);
/* 默认情况下,往一个读端关闭的管道或 socket 连接中写数据将引发 SIGPIPE 信号。我们需要在代码中捕获并处理该信号, 或者至少忽略它。因为程序接收到 SIGPIPE 信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。SIG_IGN 忽略信号的处理程序
*/ 
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号

socket

  • 不会局限是否在同一机器。

线程同步的方式

  • 什么是线程同步 ?
    • 让线程对临界资源的访问按照规定的次序执行。
    • 主要解决线程之间的协同问题。
  • 为什么需要线程同步 ?
    • 在一个进程中的所有线程共享该进程的资源。
    • 如果不干预线程对临界资源的访问可能造成意想不到的错误,甚至导致进程崩溃。
  • 线程同步有哪些方式 ?

互斥锁

  • 确保同一时间只有一个线程访问临界资源
  • 当锁被占用时,其他试图获取该锁的线程都会进入阻塞状态
  • 当锁被释放时,会通过信号通知其他被锁阻塞的线程,被阻塞线程可能修改为就绪状态,也可能直接调度执行。
  • 如何实现互斥锁:
    • 互斥锁首先需要做一个内存标记记录的是线程 ID,因为互斥锁需要进行线程切换。
    • 互斥锁是为了保护临界资源,同时只允许一个线程去访问临界资源,所以会有一个阻塞队列,通过阻塞队列唤醒其它线程去访问临界资源。
    • 要屏蔽中断。
    • 底层硬件自旋锁。
  • 互斥锁的表现:
    • 先在用户态自旋一会儿。
    • 获取失败,把任务挂起(放到阻塞队列中),核心会切换其它线程去执行。
    • 休眠一段时间再次尝试获取锁。
// 声明一个互斥量
pthread_mutex_t mtx;
// 初始化
pthread_mutex_init(&mtx, NULL);
// 加锁
pthread_mutex_lock(&mtx);
// 解锁
pthread_mutex_unlock(&mtx);
// 销毁
pthread_mutex_destroy(&mtx);

自旋锁

  • 当多个线程竞争锁的时候,只允许一个线程去访问临界资源,其他的线程不会处于阻塞态,也就是不会进行线程切换会占用 CPU 核心轮询使用临界资源的线程是否释放锁
  • 在实际自旋锁的实现过程中,它不会一直占用 CPU 不断地去轮询,它可能也会切换线程,但是和互斥锁的切换是有差别的:
    • 互斥锁被切换出去,会处于阻塞态,当互斥锁被释放的时候,会从阻塞队列中取出一个线程,把它转化为就绪态,进入就绪队列,未来被系统调度的时候再把它从就绪队列中取出,转化为运行态去运行。
    • 自旋锁被切换出去,会处于就绪态,进入就绪队列,时刻等待被系统调度。
  • 自旋锁适用于锁持有时间短的场景。
// 声明
pthread_spinlock_t spin;
// 初始化
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
// 加锁
pthread_spin_lock(&spin);
// 解锁
pthread_spin_unlock(&spin);
// 销毁
pthread_spin_destroy(&spin);

条件变量

  • 用户态的条件让当前线程发生阻塞,当另外一个线程判断特定条件为真时,通知当前线程解除阻塞。
  • 对条件的判断需在互斥锁的保护下进行。
  • 条件变量需要和互斥锁一起使用。

读写锁

  • 当加写锁成功时处于写状态,任何试图加读锁或写锁的线程都阻塞
  • 当加读锁成功时处于读状态,其他试图加读锁的线程不阻塞,而试图加写锁的线程阻塞
  • 读写锁适用于读远大于写的场景。
  • MySQL 数据库中的行锁S 锁(读锁)X 锁(写锁)
// 声明一个读写锁
pthread_rwlock_t rwlock;
// 在读之前加读锁
pthread_rwlock_rdlock(&rwlock);
// 读完释放锁
pthread_rwlock_unlock(&rwlock);// 在写之前加写锁
pthread_rwlock_wrlock(&rwlock);
// 写完释放锁
pthread_rwlock_unlock(&rwlock);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);

信号量

  • 非负的整数计数器,用来实现对资源的控制,既可以用于线程之间、也可以用于进程之间。
  • 信号量适用于多份临界资源的访问
  • 只有当信号量大于 0 的时候,才能访问资源。
  • 比如 A 线程调用 sem_wait 将信号量减为 0 了,A 线程发生阻塞;如果 B 线程调用 sem_post 把信号量 + 1,将会解除 A 线程的阻塞,A 线程继续执行。
// 初始化信号的个数
// pshared = 0 → 线程之间通信
// pshared > 0 → 进程之间通信
sem_init(&sem, pshared, value);sem_wait(); // P 操作:--操作
sem_post(); // V 操作:++操作

线程通信的方式

  • 信号:pthread_kill(thread, 0) → 通常用于检测其他线程是否存活 → 返回 0 代表存活,返回错误代表线程不存在。
  • 线程同步的方式。

互斥锁和自旋锁的区别

  • 共同点:
    • 都具备排他性。
    • 谁持有锁谁释放锁。
  • 不同点:
    • 获取锁失败后的行为:
      • 自旋锁轮询检测。
      • 互斥锁进行阻塞。

一个线程加锁了,另一个线程可以解锁吗 ?

  • 只有条件变量是通过另外一个线程来解锁的。

虚假唤醒

  • 可能会有多个线程在 pthread_cond_wait 休眠。
  • pthread_cond_signal唤醒至少一个休眠的线程,也就是可能会唤醒多个休眠的线程
  • 如果使用 if,消费者线程被唤醒后会少了重新检查条件的步骤,所以使用 while
    • 为什么消费者线程被唤醒后要重新检查条件 ? 也就是为什么会出现虚假唤醒 ?
      • 在多线程环境下,可能会发生信号劫持,也就是 pthread_cond_signal 唤醒了多个休眠的线程。
// 消费者线程
pthread_mutex_lock(&mtx);
...
while(condition) { // while 用来解决虚假唤醒pthread_cond_wait(&cond, &mtx);// pthread_cond_wait 内部:// 1. 先 unlock(&mtx)// 2. 在 cond 休眠// --- __add_task 时唤醒// 3. 在 cond 唤醒// 4. 加上 lock(&mtx);
}
... 执行逻辑
pthread_mutex_unlock(&mtx);// 生产者线程
pthread_mutex_lock(&mtx);
... 修改条件
pthread_mutex_unlock(&mtx);
pthread_cond_signal(&cond);
static inline void 
__add_task(task_queue_t *queue, void *task) {// 不限定任务类型,只要该任务的结构起始内存是一个用于链接下一个节点的指针// link 指向 task 的 next 指针void **link = (void**)task;*link = NULL;spinlock_lock(&queue->lock);*queue->tail /* 等价于 queue->tail->next */ = link;queue->tail = link;spinlock_unlock(&queue->lock);// 唤醒休眠的线程pthread_cond_signal(&queue->cond);
}static inline void * 
__pop_task(task_queue_t *queue) {spinlock_lock(&queue->lock);if (queue->head == NULL) {spinlock_unlock(&queue->lock);return NULL;}task_t *task;task = queue->head;void **link = (void**)task;queue->head = *link;if (queue->head == NULL) {queue->tail = &queue->head;}spinlock_unlock(&queue->lock);return task;
}static inline void * 
__get_task(task_queue_t *queue) {task_t *task;// 虚假唤醒while ((task = __pop_task(queue)) == NULL) {pthread_mutex_lock(&queue->mutex);pthread_cond_wait(&queue->cond, &queue->mutex);pthread_mutex_unlock(&queue->mutex);}return task;
}

CAS 是怎样的一种同步机制

  • CAS 属于原子操作。
  • 什么是原子操作 ?
    • 底层级别的同步操作:CPU 指令。
    • 不会被调度机制所打断的操作。
    • 原子性:该原子操作是不可分割的,要么都做完了,要么还没开始,不会被其他线程看到只完成了一部分。
  • 什么时候使用原子操作 ?
    • 背景:锁会造成性能损耗。
    • 操作简单、冲突较少。
  • C++ 中的 CAScompare and swap):
    • CAS 操作组成:
      • 内存中的值 V
      • 期待值 Expect
      • 替换值 New Value
    • CAS 操作流程:
      • 比较内存中的值与期待值是否相等。
      • 如果相等,写入替换值,并返回 true
      • 如果不相等,直接返回 false
      // 基于 CAS 实现自旋锁
      #include <atomic>class SpinLock {
      public:SpinLock() : _flag(false) {}void lock() {bool expect = false;while (!_flag.compare_exchange_weak(expect, true)) {expect = false;// 解决伪失败问题if (_flag.load() == true) {break;}}}void unlock() {_flag.store(false);}
      private:std::atomic<bool> _flag;
      };
      
  • ABA 问题:
    • 线程1 取出 A修改为 B再次修改为 A
    • 线程2 取出 A,由于时间差没有感知到 A 值的变化。
    • 解决:A 增加版本号ABA 问题转变为 1A2B3C 问题
  • 伪失败问题:
    • compare_exchange_weak 允许偶然错误返回(虽然内存中的值与期待值一样,但是返回了 false)。
    • 通过循环解决。
    • compare_exchange_weakcompare_exchange_strong 性能高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/24893.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Day53 动态规划part12

LC309买卖股票的最佳时机含冷冻期 与LC122类似&#xff0c;都是可无限次购买股票&#xff0c;只不过引入了冷冻期的概念dp[i][0] 第i天持有股票收益&#xff1b;dp[i][1] 第i天不持有股票收益;情况一&#xff1a;第i天是冷静期&#xff0c;不能以dp[i-1][1]购买股票,所以以dp[…

性能测试 —— Jmeter对数据库压力测试

Jmeter先要和数据库建立连接&#xff0c;sql语句是在Jmeter中写的&#xff0c;但是语句的执行是在数据库里执行的&#xff0c;数据库再将执行结果返回给Jmeter。 在做jmeter数据库压力测试之前&#xff0c;要先检查是否有mysql-connector-java-5.1.39-bin.jar的这个包&#xf…

flink读取hive写入http接口

目录 0、创建hive数据 1、pom.xml 2、flink代码 3、sink 4、提交任务jar 5、flink-conf.yaml 6、数据接收 flink-1.17.2jdk1.8hive-3.1.3hadoop3.3.6passwordhttp0、创建hive数据 /cluster/hive/bin/beeline !connect jdbc:hive2://ip:10000 create database demo; d…

python 多任务之多进程

多任务 优势 多个任务同时执行可以大大提高程序执行效率&#xff0c;可以充分利用CPU资源&#xff0c;提高程序的执行效率 概念 是指在同一时间内执行多个任务 多进程 概念 进程&#xff08;process&#xff09;是资源分配的最小单位&#xff0c;他是操作系统进行资源分配…

鸿蒙北向开发 IDE DevEco Studio 3.1 傻瓜式安装闭坑指南

首先下载 安装IDE 本体程序 DevEco Studio 下载链接 当前最新版本是3.1.1,下载windows版本的 下载下来后是一个压缩包, 解压解锁包后会出现一个exe安装程序 双击运行安装程序 一路 next ( 这里涉及安装文件目录,我因为C盘够大所以全部默认了,各位根据自己情况选择自己的文件…

前端工程化:基于Vue.js 3.0的设计与实践

这里写目录标题 《前端工程化&#xff1a;基于Vue.js 3.0的设计与实践》书籍引言本书概述主要内容作者简介为什么选择这本书&#xff1f;结语 《前端工程化&#xff1a;基于Vue.js 3.0的设计与实践》书籍 够买连接—>https://item.jd.com/13952512.html 引言 在前端技术日…

菜品信息分页查询——后端SpringBoot

1.分页查询的逻辑&#xff1a; 页面发送ajax请求&#xff0c;将分页查询参数(page&#xff0c;pageSize, name)提交到服务端&#xff0c;获取分页数据&#xff1b; 页面发送请求&#xff0c;请求服务端进行图片下载&#xff0c;用于页面图片展示。 开发菜品信息分页查询功能&a…

数字人的技术实现方案比较

数字人的实现方案通常包括以下几个关键技术领域&#xff0c;数字人的实现是一个跨学科的领域&#xff0c;涉及到计算机图形学、人工智能、机器学习、自然语言处理等多个技术领域&#xff0c;随着技术的不断进步&#xff0c;数字人的能力和应用范围将不断扩大。北京木奇移动技术…

数仓开发:如何计算投放效果?

背景介绍 业务介绍&#xff1a;用户是通过低价引流进来&#xff0c;然后通过复购购买高价商品&#xff0c;可以多次购买。低价商品和高价商品均可以退款&#xff0c;高价商品由于各种复杂的场景&#xff0c;可能会有多次退款。低价商品如果退款是全退&#xff0c;不存在多次退…

使用树莓派和 L298N 来 DIY 小车底盘

树莓派小车可以作为 STEM&#xff08;科学、技术、工程、数学&#xff09;教育的工具&#xff0c;在实际操作中帮助学生理解和学习电子技术、编程和机器人原理。可以培养学生的动手能力、解决问题的能力和创新思维。 随着近年 AI 技术的高速发展&#xff0c;SLAM、VSLAM 甚至带…

2024儿科学中文核心期刊汇总,附投稿信息

第10版《中文核心期刊要目总览》入编了8本儿科学期刊&#xff0c;新入编的期刊是《临床小儿外科杂志》。常笑医学整理了儿科学核心期刊的详细参数&#xff0c;供大家在论文投稿时参考&#xff0c;有需要的赶紧收藏&#xff01; 1.《中华儿科杂志》 &#xff08;详细投稿信息请…

【NI国产替代】高速数据采集模块,最大采样率为 125 Msps,支持 FPGA 定制化

• 双通道高精度数据采集 • 支持 FPGA 定制化 • 双通道高精度采样率 最大采样率为 125 Msps12 位 ADC 分辨率 最大输入电压为 0.9 V -3 dB 带宽为 30 MHz 支持 FPGA 定制化 根据需求编程实现特定功能和性能通过定制 FPGA 实现硬件加速&#xff0c;提高系统的运算速度FPGA…

OpenCV学习(4.8) 图像金字塔

1.目的 在这一章当中&#xff0c; 我们将了解图像金字塔。我们将使用图像金字塔创建一个新的水果&#xff0c;“Orapple”我们将看到这些功能&#xff1a; cv.pyrUp&#xff08;&#xff09; &#xff0c; cv.pyrDown&#xff08;&#xff09; 在通常情况下我们使用大小恒定…

node的安装

node是前端开发环境&#xff0c;所以运行前端程序需要安装和配置node 1. 下载安装node 去node官网选择你需要的版本进行下载 Node.js — Download Node.js (nodejs.org) ​ 下载到本地后一路点击next傻瓜式安装&#xff0c;安装成功后测试是否安装成功 node -v 显示node版…

【Ardiuno】使用ESP32网络功能调用接口数据(图文)

接着上文连通wifi后&#xff0c;我们通过使用HTTPClient库进行网络相关操作&#xff0c;这里我们通过http协议进行接口调用。 为了简化操作&#xff0c;这里使用了本地服务器上的文件作为接口&#xff0c;正常操作时会调用接口后&#xff0c;将服务器返回的数据进行解析&#…

白话解读网络爬虫

网络爬虫&#xff08;Web Crawler&#xff09;&#xff0c;也称为网络蜘蛛、网络机器人或网络蠕虫&#xff0c;是一种自动化程序或脚本&#xff0c;被用来浏览互联网并收集信息。网络爬虫的主要功能是在互联网上自动地浏览网页、抓取内容并将其存储在本地或远程服务器上供后续处…

【模拟-BM99 顺时针旋转矩阵】

题目 BM99 顺时针旋转矩阵 描述 有一个NxN整数矩阵&#xff0c;请编写一个算法&#xff0c;将矩阵顺时针旋转90度。 给定一个NxN的矩阵&#xff0c;和矩阵的阶数N,请返回旋转后的NxN矩阵。 分析 模拟&#xff0c;写几个样例&#xff0c;分析一下新矩阵元素下标与原矩阵元素…

编译原理-语法分析(实验 C语言)

语法分析 1. 实验目的 编制一个递归下降分析程序&#xff0c;实现对词法分析程序所提供的单词序列的语法检查和结构分析 2. 实验要求 利用C语言编制递归下降分析程序&#xff0c;并对简单语言进行语法分析 2.1 待分析的简单语言的语法 用扩充的BNF表示如下&#xff1a; …

牛客NC32 求平方根【简单 二分 Java/Go/C++】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/09fbfb16140b40499951f55113f2166c 思路 Java代码 import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可*** para…

【python报错】TypeError: ‘dict_values‘ Object IsNot Subscriptable

【Python报错】TypeError: ‘dict_values’ object is not subscriptable 在Python中&#xff0c;字典&#xff08;dict&#xff09;提供了几种不同的视图对象&#xff0c;包括dict_keys、dict_values和dict_items。这些视图对象允许你以只读方式遍历字典的键、值或键值对。如果…