面试整理——操作系统

操作系统

一. 基本特征

问:并发与并行?

并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。

  • 并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。
  1. 并发(Concurrency)

    • 定义:并发是指多个任务在同一时间段内交替执行。任务可能是独立的,也可能需要共享资源。并发更强调的是任务之间的逻辑顺序,而不是同时运行

    • 特点

      • 多个任务交替进行,给人一种“同时进行”的感觉。

      • 通常在单个CPU核心上,通过任务切换实现。操作系统通过引入进程和线程,使得程序能够并发运行。

      • 强调任务之间的协调与资源管理。

    • 示例

      • 在单核CPU上运行一个视频播放器:音频解码、视频解码、用户输入处理交替执行。
      • Java中使用Thread或线程池时,多个线程并发地执行。
  2. 并行(Parallelism)

    • 定义:并行是指多个任务在同一时刻真正地同时运行。并行需要硬件支持,例如多核处理器或分布式系统。
    • 特点
      • 任务在物理上同时运行。
      • 通常依赖于多核CPU或多台机器。
      • 强调计算任务的真正并行处理。
    • 示例
      • 在多核CPU上,多个线程分别运行在不同的核心上。
      • 大型分布式计算(如MapReduce)中的数据分片并行处理。

并发 vs. 并行:对比总结

特性 并发 并行
定义 任务交替进行 任务同时运行
硬件依赖 不依赖多核硬件,可单核实现 依赖多核或多机器
重点 任务之间的逻辑切换和协调 任务真正同时运行
示例 多线程任务调度 多线程在多核上运行

Java 中的体现

  • 并发java.util.concurrent包提供了线程池、锁等并发编程工具。
  • 并行Fork/Join框架和Stream API中的并行流(Parallel Stream)利用多核CPU实现并行计算。

问:什么是同步?什么是异步?

面试中回答时可以根据实际项目经验,说明在什么场景选择同步或异步,以及如何权衡两者的优缺点。如果面试官深入提问,可以提到:

  • Java 中的具体实现(如 FutureCompletableFuture)。
  • 异步编程可能引入的问题(如线程安全、死锁等)。
  • 应用异步的设计模式(如生产者-消费者、回调、事件驱动)。

同步异步描述了任务执行过程中线程或进程之间的交互方式。

  1. 同步(Synchronous)

    • 定义:同步是指任务按照顺序执行,一个任务必须等前一个任务完成后才能开始。同步分为进程/线程同步和数据同步,进程同步指多个进程在特定点会和使操作序列有序,数据同步则指一份数据集的多份拷贝保持一致以维护完整性。(一般通过进程同步来实现数据同步)
    • 特点
      • 阻塞:调用方需要等待被调用的任务完成,才能继续执行。
      • 线程占用:调用方在等待期间一直占用线程或资源。
      • 顺序性:严格按照任务的启动顺序完成。
    • 优点

      • 逻辑简单,容易理解和实现。
      • 适用于对任务完成顺序要求严格的场景。
    • 缺点:效率较低,调用方在等待时无法处理其他任务。

    • 示例

      1
      2
      3
      4
      5
      6
      public void synchronousExample() {
      System.out.println("任务1开始");
      System.out.println("任务1完成");
      System.out.println("任务2开始");
      System.out.println("任务2完成");
      }

      执行结果是按顺序输出“任务1开始”、“任务1完成”、“任务2开始”、“任务2完成”。

  2. 异步(Asynchronous)

    • 定义:异步是指任务可以独立于前一个任务执行,调用方在发起任务后无需等待,可以继续处理其他任务。异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。
    • 特点
      • 非阻塞:调用方无需等待任务完成。
      • 回调/事件驱动:任务完成后,通过回调函数、Promise 或 Future 通知调用方。
      • 并发性:可以并发地执行多个任务。
    • 优点

      • 提高系统的效率和资源利用率。
      • 适用于需要处理大量 I/O 或长时间等待的场景。
    • 缺点

      • 实现逻辑较复杂,可能引入回调地狱或竞争问题。
      • 调试和错误处理相对困难。
    • 示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      import java.util.concurrent.CompletableFuture;

      public void asynchronousExample() {
      System.out.println("任务1开始");
      CompletableFuture.runAsync(() -> {
      System.out.println("任务2完成");
      });
      System.out.println("任务3开始");
      }

      可能的输出:

      1
      2
      3
      任务1开始
      任务3开始
      任务2完成

      任务2在后台异步执行,不阻塞任务3的执行。

  3. 同步与异步的对比

    特性 同步 异步
    任务依赖 后续任务需等待前一任务完成 任务独立,不依赖完成顺序
    调用行为 阻塞调用方,必须等待结果 非阻塞调用方,可继续处理其他任务
    执行效率 效率较低 更高效,适合并发任务
    实现难度 简单 复杂
    应用场景 对执行顺序有严格要求的场景 网络请求、大量 I/O 操作等场景
  4. 典型应用场景

    同步场景

    • 数据库事务:需要保证操作的原子性和一致性。
    • 文件写入:确保顺序性。

    异步场景

    • 网络请求:例如 REST API 调用。
    • 消息队列:如 RabbitMQ、Kafka,用于异步解耦。
    • 用户界面:避免界面卡顿,异步处理后台任务。

问:虚拟技术?

虚拟技术(Virtualization Technology)通过软件模拟硬件资源,使得多个操作系统或应用程序能够共享同一套物理硬件资源。

  • 定义:虚拟技术是指在一台物理设备上创建多个逻辑设备(虚拟机、虚拟资源)的技术,把一个物理实体转换为多个逻辑实体。它使得硬件资源如CPU、内存、存储和网络能够被抽象化,并分配给不同的用户或系统。

  • 分类:主要有两种虚拟技术:时(时间)分复用技术、空(空间)分复用技术。

    多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片并快速切换。

    虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。

    1. 硬件虚拟化
      • 将物理硬件抽象为多个虚拟硬件实例。
      • 应用:虚拟机(Virtual Machine),如 VMware、KVM、Hyper-V 等。
      • 分类:
        • 全虚拟化:完全模拟硬件环境,不需要修改操作系统(如 VMware ESXi)。
        • 半虚拟化:部分硬件由软件模拟,操作系统需要进行修改(如 Xen)。
        • 硬件辅助虚拟化:利用CPU指令集支持虚拟化(如 Intel VT-x, AMD-V)。
    2. 操作系统虚拟化
      • 在单一操作系统内运行多个隔离的用户空间实例。
      • 应用:容器技术(如 Docker、LXC)。
      • 优势:比硬件虚拟化更轻量级,启动速度快,占用资源少。
    3. 存储虚拟化
      • 将物理存储设备抽象为逻辑存储资源,提供更高的存储灵活性。
      • 应用:存储池化、分布式存储(如 VMware vSAN)。
    4. 网络虚拟化
      • 把网络硬件和网络功能抽象为逻辑组件(如虚拟交换机、虚拟路由器)。
      • 应用:软件定义网络(SDN),虚拟局域网(VLAN)。
  • 核心组件:

    1. 虚拟机监控器(Hypervisor)
      • 在硬件和虚拟机之间的核心层。
      • 分类:
        • Type 1(裸金属型):直接运行在硬件上,性能高(如 VMware ESXi)。
        • Type 2(托管型):运行在主机操作系统之上,灵活性高(如 VMware Workstation)。
    2. 虚拟机(Virtual Machine)
      • 虚拟硬件环境中的操作系统实例。
    3. 容器(Container)
      • 操作系统层的虚拟化技术,提供隔离的运行环境。
  • 优点:

    1. 资源利用率提升:多个虚拟机或容器共享硬件资源,减少资源闲置。
    2. 隔离性强:不同虚拟机和容器之间相互隔离,安全性高。
    3. 灵活性和可扩展性:快速部署新环境,按需扩展资源。
    4. 成本节约:减少硬件投入,降低能耗。
    5. 高可用性:支持迁移、快照等功能,减少宕机时间。
  • 常见应用场景:

    1. 服务器整合
      • 将多个服务器整合到一个虚拟化平台,减少硬件使用。
    2. 云计算
      • 云服务提供商(如 AWS、Azure)利用虚拟化技术提供弹性计算资源。
    3. 开发与测试
      • 快速创建隔离的测试环境。
    4. 容器化部署
      • 使用 Docker 或 Kubernetes 实现微服务架构的部署和管理。
    5. 桌面虚拟化
      • 通过虚拟桌面基础架构(VDI),远程访问桌面环境。
  • 相关技术及工具:

    • 虚拟机工具:VMware、KVM、Hyper-V、VirtualBox。
    • 容器工具:Docker、Podman、LXC。
    • 编排工具:Kubernetes、Docker Swarm。
    • 存储虚拟化工具:Ceph、GlusterFS。
    • 网络虚拟化工具:Open vSwitch、SDN 控制器。

二. 基本功能

问:说一下操作系统的基本功能包括?

  1. 进程管理
    *
  2. 内存管理
    *
  3. 文件管理
    *
  4. 设备管理:
    *

操作系统(Operating System, OS)是管理计算机硬件和软件资源的核心软件,其主要功能是为用户和应用程序提供一个方便、有效的运行环境。以下是操作系统的基本功能分类和详细说明:

  1. 进程管理

    定义:操作系统负责对进程进行创建、调度、终止,并确保多进程环境下的资源共享和独立性。

    关键任务:进程控制、进程同步、进程通信、死锁处理、处理机调度等

    • 进程调度:决定哪个进程优先执行(如先来先服务、时间片轮转)。
    • 进程同步:协调多个进程之间的运行顺序,防止资源竞争。
    • 进程通信:提供进程之间的数据交换机制(如共享内存、消息队列、管道等)。
    • 进程状态管理:维护进程的创建、就绪、运行、等待、终止等状态转换。
  2. 内存管理

    定义:操作系统负责为每个进程分配和管理内存,同时确保内存的高效使用和安全性。

    关键任务:内存分配、地址映射、内存保护与共享、虚拟内存等。

    • 内存分配和回收:为进程分配内存空间,释放未使用的空间。
    • 地址空间管理:实现虚拟内存机制,使进程看到的是逻辑地址,而非物理地址。
    • 分页与分段:通过分页或分段技术实现内存管理。
    • 内存保护:确保一个进程的内存不会被其他进程访问。
  3. 文件管理

    定义
    操作系统提供了对存储设备上文件的创建、读取、写入、删除等操作的支持。

    关键任务:文件存储空间的管理、目录管理、文件读写管理和保护等。

    • 文件系统管理:定义文件的存储结构(如 FAT32、NTFS、EXT4)。
    • 目录管理:支持文件的组织和分类。
    • 文件访问控制:确保文件访问的权限(如读、写、执行权限)。
    • 文件共享和保护:支持多用户对文件的共享,并保证安全性。
  4. 设备管理

    定义
    操作系统负责管理各种硬件设备(如磁盘、打印机、键盘、网络设备等)并提供统一的接口。

    关键任务:完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。主要包括缓冲管理、设备分配、设备处理、虛拟设备等。

    • 设备驱动程序:通过驱动程序与硬件交互。
    • 设备分配:在多个进程请求使用设备时,合理分配资源。
    • I/O 管理:提供同步和异步的输入输出操作。
    • 缓冲与缓存:提高设备操作的效率。
  5. 存储管理

    定义
    操作系统管理存储设备(如硬盘、SSD)以及存储层次结构(如主存、缓存、外存)。

    关键任务

    • 数据组织与分配:决定如何将数据存储在存储设备上。
    • 磁盘调度:如电梯调度算法,用于优化磁盘的读取效率。
    • 空间管理:分配和回收存储空间。
    • 备份与恢复:支持数据的备份和故障恢复。
  6. 网络管理

    定义
    操作系统提供网络通信的支持,实现进程之间的远程通信。

    关键任务

    • 协议支持:如 TCP/IP 协议栈的实现。
    • 数据传输:在网络设备间高效传输数据。
    • 资源共享:通过网络共享文件和设备。
  7. 用户接口管理

    定义
    为用户和操作系统的交互提供界面,包括图形用户界面(GUI)和命令行界面(CLI)。

    关键任务

    • 图形用户界面:提供窗口、菜单、按钮等可视化组件。
    • 命令行界面:支持用户通过命令与系统交互。
    • 多用户支持:支持不同用户登录并访问资源。
  8. 安全与保护

    定义
    操作系统提供对系统和用户数据的保护,防止非法访问和恶意破坏。

    关键任务

    • 用户认证:通过密码、指纹等手段验证用户身份。
    • 访问控制:限制对资源(文件、进程)的访问权限。
    • 数据加密:对敏感数据进行加密存储和传输。
    • 防御机制:防范病毒、木马、网络攻击等威胁。
  9. 错误检测与恢复

    定义
    操作系统监测和处理运行中的错误,以保证系统稳定性和可靠性。

    关键任务

    • 硬件错误:如内存故障、磁盘损坏。
    • 软件错误:如进程死锁、程序异常退出。
    • 容错机制:通过备份、冗余设计,提高系统可靠性。

总结

操作系统的基本功能可以归纳为:

  1. 资源管理(进程、内存、设备、存储)。
  2. 任务调度(进程调度、I/O调度)。
  3. 安全保障(权限控制、数据保护)。
  4. 用户交互(提供友好的用户界面)。

面试建议
在回答操作系统功能时,结合实际项目或经验说明某个功能的使用场景,例如在 Java 项目中如何利用多线程进行进程管理,或如何通过虚拟内存优化内存使用。这样可以体现对理论和实践的理解。

1.1 进程管理

问:什么是进程?什么是线程?线程和进程的区别?

  1. 进程(Process)

    • 定义:进程是程序在内存中的一次运行实例,是资源分配的基本单位每个进程都有独立的地址空间,包含代码、数据、堆和栈。进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
    • 特点
      • 独立性:进程之间相互隔离,每个进程有独立的地址空间。
      • 资源独占:每个进程独立持有资源(如内存、文件句柄)。
      • 开销大:进程切换需要保存和恢复上下文(CPU寄存器、内存页表等)。
    • 示例:在操作系统中运行的每个程序(如浏览器、文本编辑器)都是一个进程。
  2. 线程(Thread)

    • 定义:线程是进程中的一个执行单元,是操作系统调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源(如内存、文件句柄)。
    • 特点
      • 轻量级:线程的开销比进程小,线程切换效率更高。
      • 资源共享:同一进程中的线程共享代码段、数据段和打开的文件句柄,但有独立的栈和寄存器。
      • 并发执行:线程可以通过多核CPU实现并行。
    • 示例:一个浏览器进程可能包含多个线程:QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
      • 渲染线程负责页面显示。
      • 网络线程负责数据下载。
      • 主线程处理用户输入。
  3. 进程与线程的区别

    特性 进程 线程
    定义 程序的独立运行实例,是资源分配的基本单位,开销大切换慢 进程中的执行单元,是调度的基本单位,开销小切换快
    内容 地址空间、全局变量、打开文件、子进程、即将发生的报警、信号与信号处理程序、账户信号、同步互斥信号量 程序计数器、寄存器、堆栈、状态
    地址空间 独立的地址空间,每个进程有自己的代码、数据和堆栈 同一进程内的线程共享地址空间(代码段、数据段、堆)
    资源共享 进程间资源独立,不共享资源 线程本身并不拥有资源但可以访问同一进程内的线程共享资源(内存、文件句柄等)
    执行开销 进程创建、切换和销毁的开销较大(需切换上下文)。创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销,进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置 线程创建、切换和销毁的开销较小(上下文切换更快)。线程切换时只需保存和设置少量寄存器内容,开销很小
    通信方式 通过进程间通信(IPC),如管道,信号,消息队列,共享内存,套接字等通信机制 通过共享内存或全局变量实现线程间通信
    独立性 进程相互独立,一个进程崩溃不会影响其他进程 同一进程内的线程相互依赖,一个线程崩溃可能导致整个进程崩溃
    并发性 不同进程可以并发执行 同一进程内的多个线程可以并发执行

问:进程的数据段和内存?进程和线程的内存有什么区别?

进程的数据段和内存?

一个进程的内存空间通常分为以下几个主要区域(进程的内存布局):

  1. 代码段(Text Segment)
    • 存储进程的可执行代码,也称为指令区。
    • 代码段是只读的,多个进程可以共享相同的代码段(如共享库)。
  2. 数据段(Data Segment)
    • 存储进程的全局变量和静态变量。
    • 数据段分为两部分:
      • 已初始化数据段:存储初始化的全局变量和静态变量。
      • 未初始化数据段(BSS Segment):存储未初始化的全局变量和静态变量,默认初始化为 0。
  3. 堆(Heap)
    • 用于动态分配内存,分配的内存空间可以在程序运行时通过 malloc(C)或 new(Java)等方式获得。
    • 堆的大小可以动态增长或缩减。
  4. 栈(Stack)
    • 每个线程都有独立的栈,用于存储局部变量、函数调用信息(如返回地址、参数)等。
    • 栈是后进先出的数据结构,随着函数调用不断增长和缩减。
  5. 内核区(Kernel Space)
    • 包含与操作系统交互所需的数据和函数,例如文件描述符、进程控制块(PCB)。
    • 普通进程无法直接访问内核区。

进程和线程的内存区别?

进程和线程在内存的使用上有显著的区别,主要体现在共享与隔离的层面。

特性 进程 线程
地址空间 每个进程有独立的地址空间,互相隔离。 线程共享进程的地址空间(代码段、数据段、堆等)。
数据段 数据段独立,每个进程有自己的全局变量和静态变量。 线程共享进程的数据段,全局变量和静态变量对所有线程可见。
每个进程有独立的堆区域,用于动态内存分配。 线程共享进程的堆区域,但需要通过同步机制避免竞争。
每个进程有独立的栈,存储其自身的函数调用和局部变量。 每个线程有独立的栈,存储线程的局部变量和调用栈信息。
内核对象 进程有独立的内核对象,如文件描述符和资源句柄。 线程共享进程的内核对象,但线程可以独立使用这些对象。

进程与线程内存使用的关键点

1. 数据共享

  • 进程:进程之间的数据共享需要通过进程间通信(IPC)机制实现,如共享内存、管道、消息队列、套接字等。
  • 线程:同一进程内的线程共享内存,数据共享较为直接,但需要处理同步问题,防止数据竞争。

2. 独立性与隔离性

  • 进程:由于进程的地址空间独立,崩溃的进程不会影响其他进程。
  • 线程:线程共享地址空间,一个线程崩溃可能会导致整个进程崩溃。

3. 并发与同步

  • 进程:进程间的同步开销较大,因为需要通过操作系统提供的 IPC。
  • 线程:线程间的同步更高效,但也更容易出现竞争条件和死锁。

问:进程、线程间的通信方式?

进程间通信(Inter-Process Communication, IPC):进程间通信的目的是在独立的进程间传递数据或协调操作,因为每个进程都有自己的独立地址空间。以下是常见的进程间通信方式:

  1. 管道(Pipe)

    • 描述:管道是一种半双工的单向通信机制,数据从一端写入,从另一端读取,且只能在具有亲缘关系的进程间使用(通常指父子进程)。

    • 特点:

      • 数据流是单向的,除非使用命名管道(Named Pipe)。
      • 命名管道(FIFO):有路径名关联,允许无亲缘关系进程间进行通信
      • 只能在有亲缘关系的进程间通信(如父子进程)。
      • 操作系统提供管道的创建和销毁。
    • 示例(Linux Shell):

      1
      2
      3
      ls | grep ".txt"
      ls
      grep

      通过管道实现通信。

  2. 信号(Signal)

    • 描述:信号是一种异步通信机制,用于通知进程某些事件的发生。

    • 特点:

      • 信号处理机制简单,用于事件通知。
      • 常见信号:SIGKILL(终止进程)、SIGINT(中断进程)。
      • 不适合传递大量数据。
    • 示例: 使用 kill -SIGINT pid 向进程发送信号。

  3. 套接字(Socket)

    • 描述:套接字是一种通用的通信机制,可用于同一主机或分布式系统中不同机器的进程间通信。

    • 特点:

      • 支持跨网络通信。
      • 支持多种协议(如 TCP、UDP)。
      • 广泛用于客户端-服务器架构。
    • 示例: HTTP 请求通过套接字在客户端和服务器间传输数据。

  4. 消息队列(Message Queue)

    • 描述:即消息的链接表,存放在内核中并由消息队列标识符(队列ID)标识,有写权限的进程向队列添加消息,有读权限的进程则可以读走。通过消息队列,进程可以以消息的形式发送和接收数据。

    • 消息队列解决了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。相比于管道的优点:

      • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
      • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
      • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
    • 特点:

      • 可以在无亲缘关系的进程之间通信。
      • 消息队列由操作系统维护,有容量限制。
      • 支持同步或异步通信。
    • 应用场景: 用于多进程服务器的任务调度。

  5. 共享内存(Shared Memory)

    • 描述:即映射一段能被其他进程所访问的内存,由一个进程创建,但多个进程都可访问。共享内存允许多个进程直接访问一段共享的内存区域。因为数据不需要在进程之间复制,这种IPC方式效率最高,往往与其他通信机制配合使用(需要使用信号量用来同步对共享存储的访问)。

    • 特点:

      • 最快的通信方式,因为数据无需在内核和用户空间之间拷贝。
      • 需要同步机制(如信号量)来防止数据竞争。
      • 通信前需要设置共享内存区域。
    • 示例: 在 Linux 中,可以通过 shmgetshmat 系统调用创建和访问共享内存。

  6. 信号量(Semaphore)

    • 描述:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。常作为一种锁机制,防止某进程访问共享资源时被其他进程访问。

    • 信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作:

      • down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
      • up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。
      • 如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。
    • 特点:

      • 用于解决共享内存中的数据竞争问题
      • 提供计数器机制,允许多个进程共享资源。
    • 示例: POSIX 信号量 API 提供 sem_waitsem_post 操作。

  7. 文件(File)

    • 描述:通过读写文件,进程可以交换数据。

    • 特点:

      • 简单易用,跨平台支持。
      • 性能较低,因为涉及磁盘 I/O。
    • 应用场景: 日志文件、配置文件的共享访问。

线程间通信(Thread Communication) (线程间通信主要用于同步,所以没有进程那种用于数据交换的通信机制)线程之间共享进程的地址空间,因此通信相对简单。以下是线程间常用的通信和同步机制:

  1. 全局变量

    • 描述:线程共享进程的内存空间,直接通过全局变量或静态变量共享数据。
    • 注意:需要同步机制防止数据竞争。
  2. 锁(Lock)

    • 描述:通过锁机制,线程可以同步访问共享资源。

    • 常见类型:

      • 互斥锁(Mutex):确保同一时间只有一个线程访问共享资源。
      • 读写锁(Read-Write Lock):写锁获取时阻塞所有尝试获取锁的线程,读锁获取时只阻塞写锁。读模式共享,写模式互斥。
      • 自旋锁:获取锁失败并不阻塞,而是循环轮询尝试,因为没有线程切换所以没有相关开销,但因为占用CPU所以可能造成资源浪费。适用于并行结构或锁占用时间较短切换频繁的场景。
  3. 条件变量(Condition Variable)

    • 描述:线程通过条件变量实现等待和通知机制。以原子的方式阻塞线程,直到某个特定的条件为真。条件变量一直与互斥锁配套使用。

    • 示例(Java):

      1
      2
      3
      4
      5
      6
      7
      synchronized (lock) {
      while (!condition) {
      lock.wait();
      }
      // 执行操作
      lock.notifyAll();
      }
  4. 信号量(Semaphore)

    • 描述:包括无名和命名信号量。类似于进程的信号量,可以高效完成基于线程的资源计数,是一个整数计数器,每当公共资源增加或减少就相应变化,当信号量大于0时可以访问其所代表的资源。

    • 示例(Java):

      1
      2
      3
      4
      5
      6
      7
      Semaphore semaphore = new Semaphore(3); // 最多允许3个线程访问
      semaphore.acquire();
      try {
      // 访问共享资源
      } finally {
      semaphore.release();
      }
  5. volatile共享内存:TODO

  6. 阻塞唤醒机制:TODO

  7. 阻塞队列(Blocking Queue)

    • 描述:线程通过阻塞队列实现生产者-消费者模型。

    • 示例(Java):

      1
      2
      3
      BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
      queue.put(1); // 阻塞直到队列有空间
      queue.take(); // 阻塞直到队列有数据

进程与线程通信方式对比

特性 进程间通信 线程间通信
地址空间 各进程独立,需要显式通信机制。 共享进程地址空间,通信更简单。
效率 较低(如管道、消息队列有内核开销)。 较高,直接操作共享内存。
数据同步 需要同步机制(如信号量、共享内存)。 需要同步机制(如锁、条件变量)。
通信复杂性 高,需要显式建立通信渠道。 低,数据共享天然支持。
适用场景 独立进程间的协作或分布式通信。 同一任务下的并发操作。

问:生产者消费者问题?

  1. 问题描述

    • 生产者消费者问题,又叫有限缓冲区问题,是多进程同步的经典案例。典型的同步问题,一个共享固定大小的缓冲区,多个生产者线程或进程负责向缓冲区生产数据,多个消费者进程则消耗缓冲区的数据。生产者需要在缓冲区有空闲空间时才能生产,消费者需要在缓冲区有数据时才能消费。问题的关键就是保证生产者不会在缓冲区满时加入数据,消费者不会在缓冲区空时消耗数据。
  2. 核心难点

    • 数据竞争:生产者和消费者同时访问共享缓冲区,可能造成数据混乱。
    • 同步与互斥:需要确保生产者在缓冲区未满时生产,消费者在缓冲区非空时消费。
    • 效率:避免线程或进程因资源竞争而忙等待,提升系统效率。
  3. 常见解决方案,解决生产者-消费者问题需要借助进程间或线程间的同步机制

    基于线程的解决方案(Java 示例)

    1. 使用阻塞队列(推荐方式)

    Java 提供的 BlockingQueue 是生产者-消费者问题的理想解决方案。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;

    public class ProducerConsumer {
    public static void main(String[] args) {
    BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);

    // 生产者线程
    Runnable producer = () -> {
    try {
    for (int i = 0; i < 10; i++) {
    queue.put(i); // 阻塞直到队列有空位
    System.out.println("Produced: " + i);
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    };

    // 消费者线程
    Runnable consumer = () -> {
    try {
    while (true) {
    Integer item = queue.take(); // 阻塞直到队列有数据
    System.out.println("Consumed: " + item);
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    };

    new Thread(producer).start();
    new Thread(consumer).start();
    }
    }

    2. 使用 wait()notify()

    手动实现同步逻辑,适合学习线程间通信。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    import java.util.LinkedList;
    import java.util.Queue;

    public class ProducerConsumer {
    private static final int CAPACITY = 5;
    private final Queue<Integer> queue = new LinkedList<>();

    public static void main(String[] args) {
    ProducerConsumer pc = new ProducerConsumer();

    // 生产者线程
    Runnable producer = () -> {
    try {
    for (int i = 0; i < 10; i++) {
    pc.produce(i);
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    };

    // 消费者线程
    Runnable consumer = () -> {
    try {
    while (true) {
    pc.consume();
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    };

    new Thread(producer).start();
    new Thread(consumer).start();
    }

    public synchronized void produce(int value) throws InterruptedException {
    while (queue.size() == CAPACITY) {
    wait(); // 等待队列有空位
    }
    queue.add(value);
    System.out.println("Produced: " + value);
    notifyAll(); // 通知消费者
    }

    public synchronized void consume() throws InterruptedException {
    while (queue.isEmpty()) {
    wait(); // 等待队列有数据
    }
    int value = queue.poll();
    System.out.println("Consumed: " + value);
    notifyAll(); // 通知生产者
    }
    }

    基于进程的解决方案:使用共享内存 + 信号量

    • 因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
    • 为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:
      • empty:记录空缓冲区的数量,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;
      • full:记录满缓冲区的数量,full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
    • 生产者:检查可用空位信号量,写入共享内存后增加数据信号量。
    • 消费者:检查数据信号量,读取共享内存后增加空位信号量。
    • 注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。

问:哲学家进餐问题?

  1. 问题描述:由计算机科学家 Edsger Dijkstra 提出,是典型的同步问题,用于说明如何避免死锁、饥饿等问题。

    • 场景:五个哲学家围坐在圆桌旁,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。生活的循环是“思考”和“吃饭”。

    • 条件:

      • 每个哲学家吃饭时需要两只筷子(左手和右手)。
      • 筷子放在哲学家之间的两侧,共有五根筷子。
      • 筷子是共享资源,最多只能被一个哲学家使用。
    • 问题:如何设计一种机制,保证哲学家不会出现死锁或饥饿现象,同时能有效地使用资源。

  2. 问题的核心挑战

    • 死锁(Deadlock):如果每个哲学家同时拿起左手的筷子,所有人都会等待右手的筷子,从而陷入死锁。
    • 饥饿(Starvation):如果某个哲学家长期无法获取两只筷子,他将永远无法进餐。
    • 并发性:如何设计高效的机制,使多个哲学家可以尽量同时进餐,避免资源浪费。
  3. 解决方案:

    为了防止死锁的发生,可以设置两个条件:

    • 必须同时拿起左右两根筷子;
    • 只有在两个邻居都没有进餐的情况下才允许进餐。

    方法 1:资源分配顺序

    • 规则:哲学家只有在同时拿到两只筷子时才可以进餐,否则必须放下已拿的筷子。
    • 实现: 每个哲学家尝试同时获取左右两只筷子(互斥锁或信号量),失败时放下筷子并稍后重试。

    伪代码:

    1
    2
    3
    4
    5
    6
    7
    while True:
    think() # 思考
    wait(left_fork) # 拿左边的筷子
    if try_acquire(right_fork): # 尝试拿右边的筷子
    eat() # 进餐
    release(right_fork)
    release(left_fork)

    方法 2:限制同时进餐的哲学家

    • 规则:限制最多只能有4个哲学家同时尝试拿筷子,保证至少一个哲学家能够成功拿到两只筷子并进餐。
    • 实现:使用一个计数信号量,限制最多允许4个哲学家同时进入“拿筷子”阶段。

    伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    semaphore dining = 4  # 限制同时进入拿筷子的哲学家数量

    while True:
    think() # 思考
    wait(dining) # 尝试进入拿筷子阶段
    wait(left_fork) # 拿左边的筷子
    wait(right_fork) # 拿右边的筷子
    eat() # 进餐
    release(right_fork) # 放下右边的筷子
    release(left_fork) # 放下左边的筷子
    signal(dining) # 离开拿筷子阶段

    方法 3:改变资源分配顺序

    • 规则:所有哲学家以相同的顺序拿筷子(如先拿左筷子,再拿右筷子),除了最后一个哲学家,他需先拿右筷子再拿左筷子。
    • 优点:避免形成资源分配的环路,从而预防死锁。

    伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    if (philosopher_index == n - 1):  # 最后一个哲学家
    wait(right_fork)
    wait(left_fork)
    else:
    wait(left_fork)
    wait(right_fork)

    eat()

    release(left_fork)
    release(right_fork)

    方法 4:使用条件变量

    • 规则:哲学家需要检查邻居是否正在用餐,只有邻居都没有用餐时才可以拿筷子。
    • 实现:通过条件变量管理哲学家的状态(思考、饥饿、用餐),并保证同时最多只有两个相邻的哲学家进餐。

    伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    enum State {THINKING, HUNGRY, EATING};
    State[] states = new State[5]; # 每个哲学家的状态
    Condition[] self = new Condition[5]; # 条件变量

    lock.acquire();
    while (true) {
    think();
    states[i] = HUNGRY;
    test(i);
    if (states[i] != EATING)
    self[i].await(); # 等待
    eat();
    states[i] = THINKING;
    test((i - 1 + 5) % 5); # 通知左邻居
    test((i + 1) % 5); # 通知右邻居
    lock.release();
    }

    void test(int i) {
    if (states[i] == HUNGRY &&
    states[(i - 1 + 5) % 5] != EATING &&
    states[(i + 1) % 5] != EATING) {
    states[i] = EATING;
    self[i].signal(); # 唤醒哲学家
    }
    }

问:读者写者问题?

允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。

  1. 问题描述

    读者-写者问题(Readers-Writers Problem)是经典的同步问题,主要描述多个读者和写者如何安全地访问共享资源(如文件、数据库等):

    • 读者:可以同时访问资源(共享读锁)。
    • 写者:独占资源访问,不能与任何其他读者或写者同时操作(独占写锁)。
  2. 问题的核心

    • 数据一致性:确保写操作与读操作、写操作与写操作之间不会引起数据冲突。

    • 公平性:避免某一类(读者或写者)长时间无法获得资源,造成饥饿现象。

    • 效率:尽量允许更多的并发(如多个读者同时读),提升系统性能。

  3. 常见场景

    • 数据库系统中,读操作和写操作的并发控制。
    • 文件系统中的读写访问。
    • 缓存系统中的共享和更新。
  4. 问题的变体

    • 第一类读者-写者问题:优先读者:当读者想访问时,写者必须等待,可能造成写者饥饿。

    • 第二类读者-写者问题:优先写者:当写者想访问时,新的读者必须等待,可能造成读者饥饿。

    • 第三类读者-写者问题(公平性):读者和写者按照先后顺序访问资源,不优先考虑某一方,避免饥饿。

  5. 解决方案

    一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。

    优先读者的实现

    读者可以在其他读者访问时加入,写者必须等待。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    import java.util.concurrent.locks.ReentrantReadWriteLock;

    public class ReaderWriter {
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    public static void main(String[] args) {
    Runnable reader = () -> {
    readLock.lock();
    try {
    System.out.println(Thread.currentThread().getName() + " is reading");
    Thread.sleep(1000); // 模拟读操作
    System.out.println(Thread.currentThread().getName() + " finished reading");
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    } finally {
    readLock.unlock();
    }
    };

    Runnable writer = () -> {
    writeLock.lock();
    try {
    System.out.println(Thread.currentThread().getName() + " is writing");
    Thread.sleep(2000); // 模拟写操作
    System.out.println(Thread.currentThread().getName() + " finished writing");
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    } finally {
    writeLock.unlock();
    }
    };

    // 创建并启动读者和写者线程
    for (int i = 0; i < 3; i++) {
    new Thread(reader, "Reader-" + i).start();
    }
    for (int i = 0; i < 2; i++) {
    new Thread(writer, "Writer-" + i).start();
    }
    }
    }

    优先写者的实现

    写者优先,当有写者想访问时,读者必须等待。

    伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    class ReaderWriterPriorityWrite {
    private int readCount = 0;
    private boolean writeFlag = false;
    private final Object lock = new Object();

    public void read() throws InterruptedException {
    synchronized (lock) {
    while (writeFlag) {
    lock.wait(); // 等待写操作完成
    }
    readCount++;
    }

    // 读操作
    System.out.println(Thread.currentThread().getName() + " is reading");
    Thread.sleep(1000);

    synchronized (lock) {
    readCount--;
    if (readCount == 0) {
    lock.notifyAll();
    }
    }
    }

    public void write() throws InterruptedException {
    synchronized (lock) {
    while (writeFlag || readCount > 0) {
    lock.wait(); // 等待读者或写者释放资源
    }
    writeFlag = true;
    }

    // 写操作
    System.out.println(Thread.currentThread().getName() + " is writing");
    Thread.sleep(2000);

    synchronized (lock) {
    writeFlag = false;
    lock.notifyAll();
    }
    }
    }

    公平性解决方案

    通过信号量(Semaphore)或公平锁实现读者与写者的交替访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    import java.util.concurrent.Semaphore;

    public class FairReaderWriter {
    private static final Semaphore resource = new Semaphore(1);
    private static final Semaphore readLock = new Semaphore(1);
    private static int readCount = 0;

    public static void main(String[] args) {
    Runnable reader = () -> {
    try {
    readLock.acquire();
    if (readCount == 0) {
    resource.acquire();
    }
    readCount++;
    readLock.release();

    System.out.println(Thread.currentThread().getName() + " is reading");
    Thread.sleep(1000);

    readLock.acquire();
    readCount--;
    if (readCount == 0) {
    resource.release();
    }
    readLock.release();
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    };

    Runnable writer = () -> {
    try {
    resource.acquire();
    System.out.println(Thread.currentThread().getName() + " is writing");
    Thread.sleep(2000);
    resource.release();
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    };

    for (int i = 0; i < 3; i++) {
    new Thread(reader, "Reader-" + i).start();
    }
    for (int i = 0; i < 2; i++) {
    new Thread(writer, "Writer-" + i).start();
    }
    }
    }

问:进程调度算法?

  1. 什么是进程调度?

    • 进程调度是操作系统为多个进程分配 CPU 资源的机制,目的是在多任务系统中高效地管理和执行进程。
    • 调度策略的目标:
      • 最大化 CPU 利用率
      • 最小化等待时间、周转时间和响应时间。
      • 确保系统的公平性和效率。
  2. 常见的进程调度算法

    不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法:

    • 批处理系统:批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。

      • 先来先服务(FCFS, First-Come, First-Served)

        • 描述:按照进程到达的顺序分配 CPU,先到先执行。非抢占式的调度算法,有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。

        • 特点:

          • 实现简单。
          • 不支持抢占。
        • 优点:

          • 简单易用。
          • 对长进程有利。
        • 缺点:

          • 平均等待时间可能较长。
          • 容易出现“长进程阻塞短进程”的问题(即“会车现象”)。

        :假设有以下进程及其到达时间和运行时间:

        进程 到达时间 执行时间
        P1 0 5
        P2 1 3
        P3 2 8
        • 调度顺序:P1 → P2 → P3
        • 平均等待时间:(0 + 5 + 8) / 3 = 4.33 秒。
      • 短作业优先(SJF, Shortest Job First)

        • 描述:优先调度执行时间最短的进程。按估计运行时间最短的顺序进行调度。长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。

        • 特点:

          • 可以是非抢占式,也可以是抢占式(抢占式又称最短剩余时间优先,SRTF)。
        • 优点:

          • 能够最小化平均等待时间。
        • 缺点:

          • 可能导致饥饿问题(长进程可能永远得不到调度)。
          • 实现依赖于对执行时间的准确预测。

        :与 FCFS 相同的进程:

        • 调度顺序:P2 → P1 → P3
        • 平均等待时间:(0 + 3 + 5) / 3 = 2.67 秒。
      • 最短剩余时间优先(SRTN):最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。

    • 交互式系统:交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应

      • 时间片轮转(RR, Round-Robin)

        • 描述:将 CPU 时间片分为固定长度,每个进程按照循环顺序分配 CPU 时间片。将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。

        • 特点:

          • 是抢占式调度。
          • 时间片的大小对性能影响很大:因为进程切换都要保存进程的信息并且载入新进程的信息。
            • 时间片太长:退化为 FCFS。实时性就不能得到保证。
            • 时间片太短:上下文切换开销大。会导致进程切换得太频繁,在进程切换上就会花过多时间。
        • 优点:公平性好,适合时间共享系统。

        • 缺点:平均等待时间可能较高。

        :假设时间片为 2 秒:

        • 调度顺序:P1(2) → P2(2) → P3(2) → P1(3) → P3(6)
        • 平均等待时间:(0 + 2 + 4 + 6 + 8) / 3 = 5 秒。
      • 优先级调度(Priority Scheduling):为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级

        • 描述:根据进程的优先级调度 CPU,优先级高的进程优先执行。

        • 特点:

          • 可以是抢占式,也可以是非抢占式。
          • 优先级可以动态调整(如老化技术)。
        • 优点:高优先级任务响应快。

        • 缺点:可能导致饥饿问题(低优先级进程可能永远得不到调度)。

        :假设时间片为 2 秒:

        进程 优先级 执行时间
        P1 2 5
        P2 1 3
        P3 3 8
        • 调度顺序:P3 → P1 → P2
        • 平均等待时间:(0 + 8 + 13) / 3 = 7 秒。
      • 多级队列调度(Multilevel Queue Scheduling)

        • 描述:将进程按照不同类别(如前台交互进程、后台批处理进程)分到不同队列,不同队列有不同的优先级或调度策略。一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。

        • 特点:

          • 每个队列可以有自己的调度算法
          • 进程一般不跨队列。
        • 优点:实现了不同类型任务的分级调度。

        • 缺点:队列优先级的配置和算法设计较复杂。

      • 多级反馈队列调度(Multilevel Feedback Queue Scheduling)

        • 描述:在多级队列的基础上,允许进程根据其表现(如 CPU 时间使用量)在队列间动态调整。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

        • 特点:

          • 动态调整优先级,避免饥饿。
          • 适合多种任务的混合负载。
        • 优点:兼顾了效率和公平性。

        • 缺点:实现复杂。

    • 实时系统:实时系统要求一个请求在一个确定时间内得到响应。分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。

  3. 选择调度算法的考量

    • 任务类型:

      • 短任务较多:选择 SJF 或多级反馈队列。
      • 时间共享系统:选择 RR。
    • 实时性需求:

      • 实时系统中,需要优先级调度或多级队列。
    • 系统目标:

      • 要求最大化吞吐量:选择 SJF。
      • 要求公平性:选择 RR 或多级反馈队列。

问:进程中的响应时间指?

到达时间和进程首次获取CPU的时间之间的差异称为响应时间。

问:fork和exec有深入了解吗?父进程有多个线程在运行,调用fork后,产生的子进程中有多少个线程?

  1. 什么是 fork 和 exec?

    • **fork()**:

      • 用于创建一个新的子进程。

      • 子进程是父进程的副本:拥有父进程的数据段的副本(在现代操作系统中通常通过写时拷贝机制实现)。

      • 子进程的PID不同,返回值不同:

        • 父进程fork() 返回子进程的 PID。
        • 子进程fork() 返回 0
      • 子进程从父进程的fork 调用点开始执行

    • **exec()**:

      • 用于用一个新程序替换当前进程的代码段。
      • 执行 exec() 后,当前进程的代码段、数据段等被新程序替换,PID 不变。
  2. fork 的多线程行为

    当一个多线程进程调用 fork() 时,子进程只有一个线程,即调用 fork() 的线程会复制到子进程中。其余线程不会复制到子进程中。

    • 原因
      • 子进程会从父进程的 fork 调用点开始执行。
      • 如果复制了所有线程,线程的状态可能在复制后变得不一致,例如线程的锁状态、堆内存状态等。
      • 为了避免这些问题,POSIX 标准要求在多线程进程中调用 fork 后,子进程只保留调用 fork 的线程。
    • 解决方法:如果需要保留所有线程,通常会立即调用 exec() 替换进程映像,这样子进程就不需要处理线程状态的问题。
  3. 需要注意的问题

    • fork 与多线程的不兼容性
      • 由于子进程只复制调用 fork() 的线程,其他线程的状态可能会导致不一致问题。
      • 例如,锁的状态在子进程中可能处于锁定状态,但没有线程能解锁。
    • 推荐的最佳实践
      • 多线程环境下,如果需要使用 fork(),建议立即调用 exec()
      • 在单线程环境下使用 fork() 更安全。
  4. 结论

    • 调用 fork() 后,子进程中只有一个线程,即调用 fork() 的线程。
    • 如果需要在子进程中运行多线程程序,应在 fork() 后调用 exec() 加载新的程序。
    • 深入了解线程状态、锁、信号处理等,可以帮助在多线程和 fork 的结合场景下避免陷阱。

问:死锁的必要条件?死锁的处理方法?

根据 Coffman 条件,死锁的发生需要满足以下 4 个必要条件,若任何一个条件不成立,就不会发生死锁:

  1. 互斥条件(Mutual Exclusion):资源一次只能被一个进程占用。
  2. 占有并等待条件(Hold and Wait):进程已经持有至少一个资源,同时等待其他资源,而这些资源被其他进程占用。
  3. 不可抢占条件(No Preemption):资源不能被强制抢占,只能由持有资源的进程主动释放。
  4. 环路等待条件(Circular Wait):存在一个进程集合 {P1, P2, ..., Pn},其中每个进程都等待下一个进程所占用的资源,形成环形等待链。

问:死锁的处理方法?死锁的检测与恢复?死锁预防?死锁避免?

主要有以下四种方法:

  • 鸵鸟策略:把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。

  • 死锁检测与恢复:允许死锁发生,系统周期性检测是否出现死锁,并通过某种策略恢复系统。

    1. 死锁检测

      • 使用资源分配图或等待图(Wait-for Graph)检测环路。从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
      • 若图中存在环路,则表示发生死锁
    2. 死锁恢复

      • 资源抢占:强制抢占资源并分配给其他进程。

      • 利用回滚恢复

      • 终止进程:

        • 按一定顺序终止死锁中的进程(例如优先终止低优先级进程)。
        • 一次终止一个进程,直到解除死锁。

    优点:提高资源利用率,只有在发生死锁时才采取措施。

    缺点

    • 死锁检测算法开销较高。
    • 进程终止或资源抢占可能导致计算丢失或不一致。
  • 死锁预防:通过破坏死锁的必要条件之一,来防止死锁的发生。

    • 破坏互斥条件:

      • 尽量避免资源的独占使用,例如使用共享锁(但对某些资源,如打印机,无法完全避免互斥)。
    • 破坏占有并等待条件:

      • 要求进程在开始执行前一次性申请所需的所有资源,避免持有部分资源再申请。
    • 破坏不可抢占条件:

      • 允许操作系统在必要时强制抢占资源并分配给其他进程。
    • 破坏环路等待条件:

      • 对资源进行全局编号,要求进程按照编号递增的顺序申请资源。

    缺点

    • 资源利用率低。
    • 可能导致系统运行效率降低。
  • 死锁避免:

    动态检测可能的死锁情况,确保系统不会进入死锁状态。

    1. 安全状态(Safe State)

      定义

      • 系统处于安全状态是指:存在一种资源分配顺序,使每个进程都能够在有限时间内完成并释放资源
  • 如果不存在这种顺序,则系统处于不安全状态,可能导致死锁。

判定条件

 - 系统在进行资源分配时,必须确保分配后仍然处于安全状态。
  • 如果分配资源后,导致系统处于不安全状态,则拒绝分配。

安全序列

 - 安全序列

    是一种进程执行顺序(如 P1,P2,…,Pn),满足以下条件:

   - 每个进程在运行时,系统有足够资源满足其需求。
  • 进程完成后释放资源,可供后续进程使用。

安全状态与死锁

 - 安全状态 ≠ 无死锁:

   - 不安全状态并不一定会导致死锁,但可能存在死锁风险。

 - 安全状态 → 一定无死锁:

   - 系统始终保持安全状态,则死锁一定不会发生。
  1. 单个资源的银行家算法(Banker’s Algorithm)

    银行家算法是一个经典的死锁避免算法,模拟银行对贷款的分配,确保系统始终处于安全状态。

    • 系统根据当前资源分配情况,动态评估进程申请资源后的系统状态是否安全。
  • 只有在分配资源后系统仍处于安全状态时,才允许分配资源。

    • 安全状态:系统能够按照某种顺序为每个进程分配其最大需求资源并完成执行。

    优点

    • 相比死锁预防更灵活,资源利用率较高。

    缺点

    • 算法复杂度高。
    • 系统开销大。

基本概念

 1. Available(可用资源向量):系统当前可用的每种资源数量。
 2. Maximum(最大需求矩阵):每个进程对每种资源的最大需求。
 3. Allocation(已分配矩阵):每个进程当前已经获得的每种资源数量。
 4. Need(需求矩阵):
  - 每个进程还需要的每种资源数量。
 - 计算公式:Need\[i\]\[j\]=Maximum\[i\]\[j\]−Allocation\[i\]\[j\]。

假设条件

  • 系统中只有一种资源类型。

    • 每个进程的最大需求量已知。

    算法步骤

    1. 初始状态:记录系统可用资源数量 Available 和每个进程的最大需求 Max。

    2. 安全性检查:

      • 遍历所有进程,找到一个满足 Need[i]≤Available 的进程。
    • 如果找到:
      • 假设该进程完成,释放资源(即 Available+=Allocation[i])。
      • 标记该进程为已完成。
      • 如果没有找到:
        • 系统处于不安全状态。
    1. 判断安全状态:
      • 如果所有进程都能完成,则系统处于安全状态。
  • 否则,不安全。

一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。

上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。

  1. 多个资源的银行家算法

    假设条件

    • 系统中有多种资源类型。
  • 每个进程的最大需求和已分配资源已知。

算法步骤

  1. 初始化数据结构
    • 可用资源向量 Available。
    • 最大需求矩阵 Maximum。
    • 已分配矩阵 Allocation。
      • 需求矩阵 Need,计算公式: Need[i][j]=Maximum[i][j]−Allocation[i][j]
  2. 安全性检查算法
    • 设置工作向量 Work: Work=Available
    • 初始化完成标志向量 Finish: Finish[i]=false(i=1,2,…,n)
    • 遍历所有进程,找到满足以下条件的进程 Pi: Need[i] ≤ Work and Finish[i] = false
    • 如果找到:
      • 假设该进程完成,将其资源释放给系统: Work=Work+Allocation[i]
      • 标记为完成: Finish[i]=true
    • 如果没有找到:
      • 停止检查,判断系统是否处于安全状态。
      1. 判断安全状态
    • 如果所有 Finish[i]=true,则系统处于安全状态。
    • 如果存在未完成的进程,系统处于不安全状态。
 检查一个状态是否安全的算法如下:
  
 - 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。
 - 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
 - 重复以上两步,直到所有进程都标记为终止,则状态时安全的。
   
 如果一个状态不是安全的,需要拒绝进入这个状态。

 **示例:多个资源的银行家算法**

 **问题描述**
  
 系统有以下资源:

 - 可用资源:[3,3,2]。
  • 进程 P0 到 P4 的资源分配和需求:

| 进程 | Allocation | Maximum | Need |
| —- | ———- | ———————– | ———————– |
| P0 | [0,1,0] | [7,5,3] | [7,4,3] |
| P1 | [2,0,0] | [3,2,2] | [1,2,2] |
| P2 | [3,0,2] | [9,0,2] | [6,0,0] |
| P3 | [2,1,1] | [2,2,2] | [0,1,1] |
| P4 | [0,0,2] | [4,3,3] | [4,3,1] |

 **步骤**
  
 1. 初始状态:Work=[3,3,2]。
  
 2. 遍历寻找满足条件的进程:
  
    - P1满足 Need[1]≤Work:

       - 假设完成,释放资源:Work=[3,3,2]+[2,0,0]=[5,3,2]。

    - P3满足 Need[3]≤Work:

       - 假设完成,释放资源:Work=[5,3,2]+[2,1,1]=[7,4,3]。

    - 继续寻找,最终得出安全序列 [P1,P3,…]。

 **结果**:系统处于安全状态。
  • 死锁预防、避免、检测对比
方法 主要特点 优点 缺点
死锁预防 破坏死锁必要条件,防止死锁发生 实现简单,避免死锁 资源利用率低,效率可能降低
死锁避免 动态检测状态,避免进入死锁状态 灵活,资源利用率较高 算法复杂,系统开销大
死锁检测与恢复 允许死锁发生,周期性检测并恢复 提高资源利用率,适合批处理系统 检测开销大,可能丢失计算或数据

1.2 内存管理

问:说一下操作系统的内存管理机制?

内存管理机制包括:内存分配、地址映射、内存保护与共享、虚拟内存等。

操作系统的内存管理机制负责高效地分配、使用和回收计算机内存,为程序提供稳定的运行环境,同时避免内存冲突和资源浪费。

  1. 内存管理的目标

    • 分配内存:确保每个进程都能获得所需的内存资源。
    • 保护内存:防止进程互相访问彼此的内存空间。
    • 高效利用:最大化内存利用率,减少内存碎片。
    • 地址空间管理:为每个进程提供独立的逻辑地址空间。
  2. 内存管理的主要功能

    • 地址映射
      • 将逻辑地址转换为物理地址。
      • 实现方式:重定位寄存器页表
    • 内存分配
      • 静态分配:在程序运行前分配固定大小的内存。
      • 动态分配:在程序运行时根据需求动态分配内存。
    • 内存保护:使用基址寄存器(Base Register)和界限寄存器(Limit Register)防止越界访问。
    • 内存回收:回收已终止进程占用的内存,供其他进程使用。
  3. 内存管理的关键机制

    (1) 分段(Segmentation)

    • 概念:将程序的内存分为逻辑段(如代码段、数据段、栈段)。

    • 优点:

      • 更符合程序逻辑结构。
      • 提供更灵活的访问控制。
    • 缺点:会产生外部碎片

(2) 分页(Paging)

  • 概念:

    • 将内存分为固定大小的块(称为页),逻辑地址分为页号和页内偏移量。
    • 页表(Page Table)负责映射逻辑地址到物理地址。
  • 优点:

    • 避免外部碎片。
    • 更容易管理内存。
  • 缺点:页表可能占用大量内存(可使用多级页表或反向页表优化)。

(3) 分段与分页结合

  • 概念:将分段和分页结合使用,段进一步划分为固定大小的页。

  • 优点:结合两者优点,既支持逻辑结构,又能避免碎片问题。

  1. 内存管理的高级机制

    (1) 虚拟内存(Virtual Memory)

    • 概念
      • 提供比物理内存更大的逻辑地址空间。
      • 使用磁盘作为扩展内存。
    • 主要技术
      1. 请求分页:仅在需要时将页面加载到内存中。
      2. 页面置换算法:决定哪些页面被换出内存(如 FIFO、LRU、LFU)。
      3. 缺页中断:当访问未加载的页面时触发中断,操作系统加载页面。
    • 优点
      • 提供独立的地址空间,支持更多并发进程。
      • 提高内存利用率。
    • 缺点:磁盘 I/O 速度远慢于内存,频繁换页会导致性能下降(抖动现象)。

    (2) 缓存机制

    • 概念:

      • 利用缓存(如 CPU 缓存、磁盘缓存)减少内存和外部存储之间的访问时间差。
      • 缓存管理策略:LRU(最近最少使用)、MRU(最近最多使用)。

    (3) 内存映射文件

    • 概念:

      • 将文件的内容映射到内存地址中,方便访问和修改。
      • 应用:内存数据库、共享内存。
  2. 内存分配策略

    • 连续分配
      • 首次适配(First Fit):从头开始寻找第一个能容纳进程的空闲块。
      • 最佳适配(Best Fit):选择能刚好容纳进程的最小空闲块。
      • 最差适配(Worst Fit):选择最大的空闲块。
    • 非连续分配
      • 使用分页或分段机制,允许进程分散在内存的多个区域中。
  3. 内存碎片问题

    1. 外部碎片
      • 内存中存在大量零散的小空闲块,无法容纳大进程。
      • 解决方案:分页、分段与分页结合。
    2. 内部碎片
      • 分配的内存块比实际需要的内存大,导致多余的部分浪费。
      • 解决方案:适当调整页面大小。
  4. 面试重点总结

    • 基本概念
      • 分段、分页和虚拟内存的区别和应用场景。
      • 分页避免外部碎片,而分段更符合逻辑结构。
    • 虚拟内存
      • 如何处理缺页中断。
      • 常用的页面置换算法(如 LRU)。
    • 内存保护
      • 如何防止进程间的地址冲突。
    • 实际应用
      • 操作系统如何管理共享内存。
      • 分页机制对性能的影响。

问:虚拟内存?

虚拟内存(Virtual Memory) 是操作系统的一种内存管理技术,通过将物理内存与磁盘结合,提供给每个进程一个连续的、逻辑上的内存地址空间使得程序可以运行在比实际物理内存更大的地址空间中,让程序获得更多的可用内存。为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。

从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。

  1. 虚拟内存的核心概念

    • 逻辑地址与物理地址
      • 逻辑地址:进程看到的地址,是由编译器或程序生成的。
      • 物理地址:内存硬件中的实际地址。
      • 地址转换:通过 MMU(内存管理单元)将逻辑地址映射到物理地址。
    • 按需加载(Demand Paging):不需要一次性将整个程序加载到内存,只在需要时加载对应的页面。
    • 分页与分段支持:虚拟内存通常结合分页(Paging)和分段(Segmentation)技术。
  2. 虚拟内存的优点

    • 扩展内存空间:程序可以运行在比物理内存更大的虚拟地址空间中。
    • 多任务支持:每个进程都有独立的虚拟地址空间,避免相互干扰。
    • 提高内存利用率:仅加载实际使用的部分内存页面,未使用的部分保存在磁盘中。
    • 实现进程隔离:防止一个进程访问另一个进程的内存空间。
  3. 虚拟内存的主要机制

    (1) 页表(Page Table)

    • 作用:存储逻辑页与物理页之间的映射关系。

    • 结构:

      • 每个进程有自己的页表。
      • 页表条目包含:
        • 页面号。
        • 页面在物理内存中的帧号。
        • 是否有效(页面是否在内存中)。
        • 访问权限等。
    • 优化方式:

      • 多级页表:减少页表占用的内存空间。
      • 反向页表(Inverted Page Table):使用全局页表减少内存占用。

    (2) 缺页中断(Page Fault)

    • 触发条件:进程访问的页面不在内存中。

    • 处理过程:

      1. 暂停进程。
      2. 操作系统查找页面对应的磁盘地址。
      3. 将页面加载到内存中。
      4. 更新页表。
      5. 恢复进程执行。

    (3) 页面置换算法

    • 目的:当内存不足时,选择一个页面替换出去,为新的页面腾出空间。

    • 常见算法:

      • FIFO(先进先出):优先置换最早加载的页面。
      • LRU(最近最少使用):优先置换最近最少访问的页面。
      • LFU(最不常用):优先置换访问次数最少的页面。
      • CLOCK(时钟算法):FIFO 的改进版,通过使用访问位来减少开销。

    (4) 虚拟内存分配策略

    • 静态分配:固定为进程分配一定数量的页面。
    • 动态分配:根据进程的需求动态调整页面分配。
  4. 虚拟内存的实现技术

    (1) 分页(Paging)

    • 将虚拟地址分为固定大小的页面(Page),内存分为相同大小的帧(Frame)。
    • 页表记录页面与帧的映射。

    (2) 分段(Segmentation)

    • 将虚拟地址按逻辑划分为多个段(如代码段、数据段、堆栈段)。
    • 每个段可以有不同的大小和权限。

    (3) 分段与分页结合

    • 先将虚拟地址分为段,再对每段进行分页,兼顾逻辑性和避免外部碎片。
  5. 虚拟内存的性能问题

    1. 缺页率
      • 频繁的缺页中断会导致性能下降,称为抖动(Thrashing)
      • 解决方法:
        • 增加物理内存。
        • 调整页面大小。
        • 使用更优的页面置换算法。
    2. 磁盘 I/O 开销:虚拟内存频繁依赖磁盘访问会降低性能。
  6. 示例问题及回答建议

    (1) 问题:虚拟内存是如何工作的?

    回答要点

    • 提到逻辑地址到物理地址的映射。
    • 解释按需加载和缺页中断处理过程。
    • 简要介绍分页或页面置换算法。

    (2) 问题:虚拟内存和物理内存的区别?

    虚拟内存 物理内存
    是操作系统提供的抽象概念。 是实际的硬件资源。
    可以比物理内存大,使用磁盘作为扩展。 有固定大小,受硬件限制。
    通过地址映射机制转换到物理地址。 直接存储数据和指令。

    (3) 问题:LRU 页面置换算法的实现原理?

    • 通过记录页面最近使用的时间或访问顺序,选择最近最少使用的页面进行替换。
    • 可使用链表、栈或硬件支持的访问位实现。

问:分页系统地址映射?

面试重点:

  1. 基本概念
    • 页号、页内偏移、页表、页框。
  2. 计算过程
    • 如何根据虚拟地址和页表得到物理地址。
  3. 优化机制
    • 多级页表的优点。
    • TLB 的作用和性能提升。
  4. 常见问题
    • 缺页中断的处理流程。
    • 页表大小的计算:根据虚拟地址位数、页大小和页表结构。

内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。在分页系统中,虚拟地址(逻辑地址)通过页表(Page Table)映射到物理地址。这种机制让操作系统能够高效管理内存并支持虚拟内存。

  • 分页系统的基本概念

    1. 页面(Page):虚拟地址空间被分为固定大小的块,称为页面(如 4KB 或 8KB)。
    2. 页框(Frame):物理内存同样被划分为与页面大小相同的块,称为页框。
    3. 页表(Page Table):每个进程都有一个页表,用来存储虚拟页面到物理页框的映射关系。
  • 虚拟地址结构

    虚拟地址在分页系统中通常分为两部分:

    1. 页号(Page Number,P):决定虚拟地址对应的页。

    2. 页内偏移量(Page Offset,d):决定页面内的具体地址。

    虚拟地址大小 = 页号位数 + 页内偏移量位数

  • 地址映射过程

    1. 虚拟地址分解
      • 操作系统将虚拟地址分解为:
        • 页号 PP
        • 页内偏移 dd
    2. 查找页表
      • 根据页号 PP,在页表中找到对应的页表项(Page Table Entry,PTE)。
      • 页表项包含页框号 FF 和其他信息(如有效位、权限位等)。
    3. 计算物理地址
      • 将页框号 FF 与页内偏移 dd 组合,形成物理地址。

    公式:物理地址=(页框号×页大小)+页内偏移

  • 示例

    假设:

    • 虚拟地址为 16 位,页大小为 4KB(2^12 = 4096 字节)。
    • 页号位数 = 4(16 位 - 12 位)。
    • 页内偏移位数 = 12。

    虚拟地址:0x1234

    • 页号 P=0x1
    • 页内偏移 d=0x234

    页表:

    • 页号 0 映射到页框号 5。
    • 页号 1 映射到页框号 3。

    物理地址计算:

    • 物理地址=(页框号×4KB)+页内偏移
    • 物理地址=(3×4096)+0x234
    • 物理地址=0xC234
  • 多级页表

    对于大虚拟地址空间,单级页表可能过于庞大,无法高效存储。因此使用多级页表

    机制

    1. 将页表进一步划分为多级结构:
      • 顶级页表(目录)。
      • 中间页表(部分实现可有)。
      • 叶级页表。
    2. 虚拟地址分为多个字段:
      • 页目录索引。
      • 页表索引。
      • 页内偏移。

    优点

    • 减少内存占用。
    • 仅为需要的页表分配内存。
  • 硬件支持

    分页地址映射通常依赖硬件支持的 MMU(内存管理单元),它能快速完成逻辑地址到物理地址的转换。

    加速技术

    1. TLB(Translation Lookaside Buffer):

      • 页表缓存,用于存储最近使用的页表项。
      • 避免频繁访问内存中的页表。
    2. 硬件页表查找:许多现代处理器支持硬件级别的页表遍历。

问:分页和分段的区别?

分页(Paging)和分段(Segmentation)是操作系统中两种不同的内存管理方式,它们在实现目的管理单位实现细节等方面都有显著区别。虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。

  • 分页(Paging)

    1.定义

    • 分页是一种将进程的逻辑地址空间分割为固定大小的块(称为页,Page)的内存管理方式。主存被分割为与页大小相同的块(称为页框,Frame)。
      • 页面(Page):虚拟地址空间的固定大小单位。
      • 页框(Frame):物理地址空间的固定大小单位,与页面大小相等。
    • 页和页框通过页表(Page Table)实现地址映射。

    2. 特点

    • 固定大小:页的大小固定(如 4KB、8KB),由硬件或系统决定。
    • 解决外部碎片问题:由于页是固定大小,内存分配时不会产生外部碎片。
    • 地址连续性要求低:页可以存储在非连续的物理内存位置。
    • 需要页表:通过页表维护逻辑页号和物理页框的映射关系。

    3. 地址结构

    • 逻辑地址分为两部分:逻辑地址=(P,D)
      1. 页号(Page Number,P):标识逻辑地址对应的页。
      2. 页内偏移量(Page Offset,D):标识逻辑地址在页内的位置。
    • 地址转换:通过页表将页号映射到物理页框。

    优点

    • 有效利用内存,减少内存碎片(只需分配所需的页数)。
    • 支持虚拟内存,允许进程使用比物理内存更大的地址空间。

    缺点

    • 内部碎片:页面可能无法完全填满页框。
    • 需要硬件支持(如 MMU)进行地址转换。
  • 分段(Segmentation)

    1. 定义

    • 分段是一种将进程的逻辑地址空间划分为具有逻辑意义的段(Segment)的内存管理方式。
    • 每个段可以表示逻辑上程序的不同部分:
      • 代码段(Code Segment)。
      • 数据段(Data Segment)。
      • 堆栈段(Stack Segment)。

    2. 特点

    • 可变大小:段的大小由程序需求决定。
    • 逻辑划分:段是逻辑单元,具有明确的意义(如一个函数、数组等)。
    • 可能产生外部碎片:由于段的大小可变,内存分配后可能存在空闲空间无法利用。
    • 需要段表:通过段表(Segment Table)记录段号与段基址、段长度的映射关系。

    3. 地址结构

    • 逻辑地址分为两部分:逻辑地址=(S,D)
      1. 段号(Segment Number,S):标识逻辑地址对应的段。
      2. 段内偏移量(Segment Offset,D):标识逻辑地址在段内的位置。
    • 地址转换:通过段表将段号映射到物理地址,检查偏移量是否超出段长度。

    优点

    • 更符合程序的逻辑结构,便于内存保护和共享。
    • 灵活分配内存,无需固定的分割方式。

    缺点

    • 外部碎片:由于段大小不固定,可能会出现大量小块未使用的物理内存。
    • 地址计算较复杂。
  • 分页与分段的对比

    • 对程序员的透明性:分页透明,但是分段需要程序员显式划分每个段。
    • 地址空间的维度:分页是一维地址空间,分段是二维的。
    • 大小是否可以改变:页的大小不可变,段的大小可以动态改变。
    • 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
特性 分页(Paging) 分段(Segmentation)
管理单位 页(Page),大小固定 段(Segment),大小可变
划分依据 固定大小划分 逻辑功能划分
地址空间 一维地址空间;页号 + 页内偏移 多维地址空间,分为多个段;段号 + 段内偏移
碎片问题 解决内部碎片问题,但存在外部碎片问题 容易产生外部碎片,但无内部碎片问题
地址映射方式 使用页表将逻辑页号映射到物理页框 使用段表将段号映射到段基址,并检查段长度
连续性要求 页框存储在物理内存中可以不连续 每个段必须连续分配物理内存
效率 地址映射效率高(使用固定大小页表) 地址映射需要额外检查段长度,效率相对较低
硬件支持 需要页表硬件支持 需要段表硬件支持
适用场景 适用于解决内存管理的低碎片需求 适用于程序逻辑划分需求
保护和共享 保护性差,每页权限一致 段粒度更大,保护和共享更灵活
实现复杂度 较低 较高
示例 分配逻辑页号给用户程序的内存空间 将代码段、数据段、堆栈段等独立划分为不同的段

分页和分段的结合:段页式存储管理

在实际操作系统中,分页分段可以结合使用,形成段页式存储管理:程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。

  1. 首先将进程划分为若干段(Segmentation)。
  2. 每个段再划分为若干固定大小的页(Paging)。
  3. 地址映射需要段表和页表联合完成:
    • 段表:找到段的基址。
    • 页表:找到段内页的物理地址。

优点

  • 既支持逻辑划分(通过段表)又支持内存管理效率(通过页表)。
  • 避免了分段中可能产生的外部碎片问题。

地址结构

  • 虚拟地址 = 段号 + 页号 + 页内偏移
  • 段号用于定位段表,页号用于定位页表,页内偏移定位具体物理内存。

问题?

  1. 分页和分段的核心区别是什么?
    • 分页是物理上的固定划分,分段是逻辑上的划分。
  2. 分页和分段分别如何解决内存碎片问题?
    • 分页减少了外部碎片,但会产生内部碎片。
    • 分段减少了内部碎片,但可能产生外部碎片。
  3. 分页和分段的结合有哪些优点?
    • 高效内存利用和灵活内存管理的结合。
  4. 页表和段表如何管理?
    • 页表存储页面映射关系,段表存储段的起始地址和长度。

问:虚拟内存的缺页中断处理过程?

虚拟内存的缺页中断(Page Fault)是指当程序访问的虚拟地址未被映射到物理内存时硬件和操作系统协同处理的一种异常情况。以下是缺页中断的详细处理过程:

  • 缺页中断的触发条件

    • 当程序执行时,访问某个虚拟地址。如果该虚拟地址的页面没有加载到物理内存中,MMU(内存管理单元)检测到该地址的页表项无效(缺页标志位未设置),会触发缺页中断。
  • 缺页中断的处理流程

    1. 硬件检测并触发缺页中断

    • 硬件(MMU)检测到访问的页面不在物理内存中,产生一个缺页异常(Page Fault Exception)。
    • 当前的CPU将控制权转交给操作系统内核中的缺页中断处理程序。

    2. 保存CPU的上下文信息

    • 操作系统中断处理程序会保存当前进程的上下文(如寄存器状态),以便稍后恢复程序的执行。
    • 确保中断处理过程对程序透明。

    3. 检查访问合法性

    • 操作系统通过页表或其他数据结构,检查引发缺页中断的虚拟地址是否有效:
      • 地址合法:进程有权访问该地址,继续后续处理。
      • 地址非法:进程试图访问未分配的虚拟地址,操作系统会向进程发送异常信号(如 SIGSEGV)或直接终止进程。

    4.确定页面所在位置

    如果虚拟地址合法,操作系统需要确定页面的数据是否在二级存储(如硬盘的交换分区)中:

    • 页面已存在于磁盘中:页面在交换分区或程序文件中,准备加载到物理内存。
    • 页面不在磁盘中:页面需要初始化(如为堆栈或数据段分配新页面),分配一个新的空页面。

    5.分配物理内存页面

    操作系统在物理内存中分配一个空闲的页框(Page Frame)用于加载页面:

    • 内存不足:如果没有可用的物理内存页面,需要触发页面置换算法(如 LRU、FIFO)。
      • 替换掉一个不常用的页面(如未修改的页面直接丢弃,已修改的页面写回磁盘)。
    • 内存充足:直接分配一个新的页框。

    6.加载页面到物理内存

    • 如果页面在磁盘中:操作系统从磁盘中读取页面数据,加载到刚分配的页框中。
    • 如果页面需要初始化:分配的物理内存页框被清零或初始化为默认值。

    7.更新页表

    页表项被更新,标记:

    • 页面已在物理内存中。
    • 设置页面的物理地址(页框号)。
    • 更新访问位、修改位、有效位等。

    8.恢复进程执行

    • 恢复之前保存的CPU上下文。
    • 重新尝试访问引发缺页中断的虚拟地址。
    • 由于页面已加载到物理内存,这次访问不会再引发缺页中断。
  • 缺页中断处理的流程图(概述)

    1. 检测到缺页中断。
    2. 保存进程上下文。
    3. 检查地址合法性。
    4. 找到页面的存储位置。
    5. 分配或置换物理内存页框。
    6. 加载页面数据到内存。
    7. 更新页表。
    8. 恢复进程执行。
  • 性能优化

    • 页面置换算法:选择合适的页面置换算法(如 LRU)降低缺页率。
    • 预取机制:根据程序访问的局部性,预先加载可能使用的页面。
    • 减少磁盘IO:
      • 使用 SSD 提升磁盘读写速度。
      • 利用缓存机制减少实际的磁盘访问次数。

问:页面置换算法如何选择?页面置换算法有哪些,介绍一下?

为什么需要页面置换算法?

  • 在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
  • 页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。

页面置换算法的目标

  1. 减少缺页率(Page Fault Rate):尽量减少内存中页面被替换的频率。使页面置换频率最低。
  2. 提高系统性能:通过高效的算法减少页面置换的开销。
  3. 适应系统环境:根据进程的内存访问模式选择合适的算法。

常见页面置换算法

  1. 最优页面置换算法(Optimal Page Replacement, OPT)

    • 思想:替换未来最久不会被访问的页面,最长时间内不再被访问。

    • 优点:理论上缺页率最低,作为衡量其他算法性能的基准。

    • 缺点:实现困难,需提前知道页面访问序列(仅用于模拟或离线分析)。

    • 举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列:

      1
      70120304230321201701

      开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。

  2. 先进先出(FIFO, First-In-First-Out)

    • 思想:替换最早进入内存的页面。
    • 优点:简单易实现,使用队列即可。
    • 缺点:容易出现Belady 异常(增加页面数却导致缺页率增加)。该算法会将那些经常被访问的页面换出,导致缺页率升高。
    • 适用场景:小规模内存,简单场景下的页面置换。
  3. 最近最久未使用(LRU, Least Recently Used)

    • 思想:替换最近最久未被使用的页面。虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。
    • 优点:缺页率低,性能接近最优算法。
    • 缺点:实现复杂、硬件支持成本高,需要维护访问时间记录或使用链表/栈。为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高
    • 适用场景:页面访问局部性强的应用。
  4. 最近未使用(NRU, Not Recently Used)

    • 思想:利用页面的访问位和修改位,优先替换“最近未被使用且未修改”的页面。每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。

      使用硬件提供的位标志:

      1. 访问位(Referenced Bit):页面是否被访问。
      2. 修改位(Modified Bit):页面是否被修改。

      其中 R 位会定时被清零。可以将页面分成以下四类:

      • R=0,M=0
      • R=0,M=1
      • R=1,M=0
      • R=1,M=1

      当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。

      NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。

    • 优点:适中复杂度,能较好地平衡效率和性能。

    • 缺点:需要硬件支持访问位和修改位。

    • 适用场景:硬件支持的虚拟内存环境。

  5. 第二次机会算法(Second-Chance)

    • 思想:基于FIFO,同时检查页面的访问位,访问位为1的页面获得第二次机会。FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。优点:改进了FIFO,性能更优。
    • 缺点:实现稍复杂,需要维护一个环形队列。
    • 适用场景:缓存管理,硬件支持访问位的场景。
  6. 时钟算法(Clock Algorithm)

    • 思想:是第二次机会算法的优化版本,使用指针实现循环检查页面的访问位。第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。使用一个指针指向“时钟”的当前位置。如果页面的访问位为 0,淘汰该页面;如果为 1,清除访问位并移动指针。
    • 优点:高效,适合实际操作系统实现。
    • 缺点:在访问位全为1时效果一般。
    • 适用场景:操作系统中常用的实际算法。
  7. 最近未使用访问频率(LFU, Least Frequently Used)

    • 思想:替换访问频率最低的页面。记录每个页面的访问次数,选择访问次数最少的页面淘汰。
    • 优点:适合访问模式稳定的场景。可以保留频繁使用的页面。
    • 缺点:不适合访问模式变化快的场景,可能导致热点页面频繁替换。可能导致“历史遗留问题”(某些页面长期占用内存)。
    • 适用场景:数据库缓存等场景。
  8. 最少修改页面置换(MFU, Most Frequently Used)

    • 思想:替换访问频率最高的页面,假设频繁使用的页面即将被淘汰。
    • 优点:针对某些特殊模式,性能优于LFU。
    • 缺点:一般适用性不强。

算法性能对比

算法 优点 缺点 适用场景
FIFO 简单易实现 Belady 异常,性能差 资源受限的简单场景
LRU 性能接近最优 实现复杂,硬件支持要求高 高性能系统,硬件支持较好
OPT 理论最优性能 实际不可实现 理论分析和性能比较
NRU 性能较好,开销小 需硬件支持访问位和修改位 通用系统
CLOCK 实现简单,性能接近 LRU 不如 LRU 精确 性能需求较高但资源有限的系统
LFU 频繁使用的页面保留时间长 历史问题,计算复杂 特殊应用,访问频率较稳定时
RANDOM 实现简单 缺页率高 资源受限的特殊场景

如何选择页面置换算法?

  1. 系统支持的硬件功能
    • 如果硬件支持访问位和修改位:选择 LRU、NRU 或时钟算法
    • 如果硬件功能有限:选择 FIFO 或其改进版本(如第二次机会算法)
  2. 应用场景
    • 工作负载较轻:FIFO 简单高效。
    • 工作负载局部性强:LRU 或其近似算法(如 CLOCK)。
    • 实时系统或性能敏感场景:时钟算法因效率高而常用。
    • 内存访问模式稳定:LFU 性能较好。
    • 内存需求动态变化大:NRU 平衡性能和复杂度。
  3. 性能优先还是实现优先
    • 性能优先:LRU 或最优页面置换算法(OPT,理论分析用)。
    • 实现优先:FIFO 或时钟算法(较简单易实现)。

总结与推荐

  1. 理论参考:使用 最优算法(OPT) 测试并分析缺页率。
  2. 实际实现:
    • 如果硬件资源充足,优先考虑 LRU 或 CLOCK。在实际系统中,LRUCLOCK 是最常用的页面置换算法。
    • 如果硬件受限且需要简单实现,选择 FIFO 或第二次机会算法
  3. 应用场景:
    • 数据库缓存中使用 LFU 或其改进版本。
    • 操作系统中的内存管理多采用 时钟算法

面试重点问题

(1) FIFO 和 LRU 的区别?

  • FIFO:优先淘汰最早加载的页面。
  • LRU:优先淘汰最近最少使用的页面,性能更优。

(2) 如何实现 LRU?

  • 计数器法:维护时间戳记录页面最近访问时间。
  • 栈法:使用链表或栈实现页面访问的排序。

(3) 时钟算法如何改进 FIFO?

  • 引入访问位减少不必要的页面淘汰,避免淘汰仍然活跃的页面。

1.3 文件管理

1.4 设备管理

问:磁盘结构?

  • 结构

    1. 磁盘盘片(Platter):每个磁盘包含一个或多个盘片,通常是金属或玻璃制成,并涂有磁性材料,用于存储数据。
    2. 磁道(Track):每个盘片表面划分为若干同心圆环,称为磁道。
    3. 扇区(Sector):每个磁道被进一步划分为多个扇形区域,称为扇区,是磁盘的最小存储单位(如 512 字节或 4 KB)。
    4. 柱面(Cylinder):不同盘片上相同半径的磁道集合称为柱面。
    5. 磁头(Read/Write Head):每个盘片有一个磁头,与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写),用于读取或写入数据。
    6. 制动手臂(Actuator arm):用于在磁道之间移动磁头;负责磁头移动、数据传输以及和操作系统的通信。
    7. 旋转轴(Spindle):磁盘的盘片通过旋转轴固定,并以一定速度旋转(如 7200 RPM)。
  • 磁盘寻址方式

    磁盘通过CHS(Cylinder, Head, Sector)逻辑块地址(LBA, Logical Block Addressing)进行寻址:

    • CHS:柱面号 + 磁头号 + 扇区号。
    • LBA:将磁盘的扇区逻辑编号,简化寻址。

问:磁盘调度算法?

读写一个磁盘块的时间的影响因素有:

  • 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
  • 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
  • 实际的数据传输时间

其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短

磁盘调度算法用于优化磁头移动的顺序,减少寻道时间,提高磁盘访问效率。

  1. 先来先服务(FCFS, First-Come, First-Served)

    思想

    • 按磁盘请求到达的顺序依次服务。

    优点

    • 简单易实现、公平。

    缺点

    • 未对寻道做任何优化,使平均寻道时间可能较长。可能导致磁头频繁跳动,增加寻道时间。
  2. 最短寻道时间优先(SSTF, Shortest Seek Time First)

    思想

    • 优先服务离磁头所在磁道距离最近的请求。

    优点

    • 减少总寻道时间。虽然平均寻道时间比较低,但是不够公平。

    缺点

    • 饥饿问题:距离远的请求可能长期得不到服务。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。
  3. 电梯算法(SCAN)

    思想

    • 电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。和电梯的运行过程类似,磁头按一个方向移动,直到到达最远端,再反向移动,服务沿途的请求。
    • 类似于电梯运行。

    优点

    • 减少饥饿问题,较均衡。因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。

    缺点

    • 每次换向可能导致等待时间增加。
  4. 循环扫描算法(C-SCAN, Circular SCAN)

    思想

    • 磁头始终向一个方向移动,服务沿途的请求,到达最远端后快速返回起始端(不服务沿途请求)。

    优点

    • 为所有请求提供公平的响应时间。
    • 均衡服务,减少饥饿问题。
  5. LOOK 和 C-LOOK

    LOOK 和 SCAN 的改进

    • LOOK:

      • 磁头只移动到有请求的最远位置,然后反向移动。
    • C-LOOK:

      • 磁头只服务有请求的最远位置后快速返回起始端。

    优点

    • 减少不必要的磁头移动。
  6. EDF(Earliest Deadline First)

    思想

    • 按照请求的截止时间优先级排序,优先处理最紧急的任务。

    优点

    • 适用于实时系统。

    缺点

    • 需要额外的时间和资源管理。
  7. 算法性能对比

算法 优点 缺点 适用场景
FCFS 简单易实现 寻道时间可能较长 请求数量少,顺序性强的场景
SSTF 总寻道时间短 饥饿问题 磁盘负载较轻
SCAN 减少饥饿问题,较均衡 换向时增加等待时间 请求负载中等,响应时间要求较低
C-SCAN 公平性更高,响应时间更均匀 不服务回程请求,增加寻道距离 请求较多,需要公平响应的场景
LOOK 减少磁头的无效移动 换向等待时间增加 改进 SCAN 场景
C-LOOK 减少无效移动,公平性较好 实现复杂性稍高 改进 C-SCAN 场景
EDF 优先处理紧急任务 需额外管理任务优先级 实时系统

面试中的关键点

  1. 如何选择调度算法?
    • 根据任务特性:
      • 实时性:EDF。
      • 公平性:C-SCAN、C-LOOK。
      • 性能:SSTF、LOOK。
  2. SCAN 和 C-SCAN 的区别?
    • SCAN:磁头来回移动。
    • C-SCAN:磁头始终一个方向移动,公平性更高。
  3. LOOK 系列的优势?
    • 减少无效的磁头移动,性能更优。
  4. 实际中如何优化磁盘调度?
    • 结合多种算法。
    • 考虑缓存(如磁盘缓存)和硬件支持(如 SSD)。

三. 系统调用

问:讲讲Linux你知道的系统调用?

在 Linux 操作系统中,系统调用(System Call)是用户程序与内核进行交互的接口,用于完成底层资源管理和硬件操作。系统调用是 Linux 内核的重要组成部分,用户态程序通过它们向内核请求服务。

以下是一些常见的 Linux 系统调用,根据其功能分类介绍:

  1. 文件操作

    这些系统调用用于文件的创建、读取、写入和管理。

    设备操作:ioctl(); read(); write();

    系统调用 功能 示例
    open 打开文件或创建新文件 fd = open("file.txt", O_RDONLY);
    close 关闭文件描述符 close(fd);
    read 从文件中读取数据 read(fd, buf, count);
    write 向文件中写入数据 write(fd, buf, count);
    lseek 移动文件指针 lseek(fd, offset, SEEK_SET);
    stat 获取文件元数据 stat("file.txt", &statbuf);
    mkdir 创建目录 mkdir("newdir", 0755);
    unlink 删除文件 unlink("file.txt");
    rename 重命名文件 rename("old.txt", "new.txt");
    fsync 将文件的缓冲区内容同步到磁盘 fsync(fd);
  2. 进程控制

    这些系统调用用于创建、管理和终止进程。

    进程通信:pipe(); shmget(); mmap();

    系统调用 功能 示例
    fork 创建子进程 pid = fork();
    exec 执行新程序,替换当前进程 execl("/bin/ls", "ls", NULL);
    exit 终止当前进程 exit(0);
    wait 等待子进程退出 wait(&status);
    getpid 获取当前进程 ID pid = getpid();
    getppid 获取父进程 ID ppid = getppid();
    kill 向进程发送信号 kill(pid, SIGKILL);
    nice 设置进程优先级 nice(-5);
  3. 内存管理相关系统调用

    这些系统调用用于分配和管理内存。

    系统调用 功能 示例
    brk 修改进程的堆边界 brk(new_brk);
    mmap 将文件或设备映射到内存 mmap(addr, len, prot, flags, fd, 0);
    munmap 解除内存映射 munmap(addr, len);
    mprotect 修改内存区域的保护属性 mprotect(addr, len, PROT_READ);
    shmget 获取共享内存标识符 shmget(key, size, IPC_CREAT);
    shmat 将共享内存段附加到进程的地址空间 shmat(shmid, NULL, 0);
  4. 网络相关系统调用

    这些系统调用用于套接字操作和网络通信。

    系统调用 功能 示例
    socket 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bind 绑定套接字到地址 bind(sockfd, &addr, sizeof(addr));
    listen 监听连接请求 listen(sockfd, backlog);
    accept 接受连接请求 connfd = accept(sockfd, &addr, &len);
    connect 发起连接请求 connect(sockfd, &addr, sizeof(addr));
    send 发送数据 send(sockfd, buf, len, flags);
    recv 接收数据 recv(sockfd, buf, len, flags);
    setsockopt 设置套接字选项 setsockopt(sockfd, level, optname, &optval, len);
  5. 时间管理相关系统调用

    这些系统调用用于获取或设置系统时间。

    信息维护:getpid(); alarm(); sleep();

    系统调用 功能 示例
    time 获取当前时间(秒级) time(&t);
    gettimeofday 获取当前时间(微秒级) gettimeofday(&tv, NULL);
    clock_gettime 获取时钟时间 clock_gettime(CLOCK_REALTIME, &ts);
    alarm 设置闹钟,触发信号 alarm(seconds);
    sleep 挂起进程一段时间 sleep(seconds);
  6. 信号相关系统调用

    这些系统调用用于处理进程间的异步信号。

    系统调用 功能 示例
    signal 注册信号处理程序 signal(SIGINT, handler);
    sigaction 更灵活地设置信号处理程序 sigaction(SIGTERM, &act, NULL);
    kill 向进程发送信号 kill(pid, SIGKILL);
    pause 挂起进程,直到收到信号 pause();
  7. 安全:chmod(); umask(); chown();

  8. 常见的高级系统调用

    • **ioctl**:控制设备的 I/O 操作。
    • **poll/select**:监听多个文件描述符的事件。
    • **epoll**:高效处理多路 I/O 事件。
    • **clone**:创建轻量级进程或线程(常用于实现线程库)。
    • **sysinfo**:获取系统信息,如总内存、空闲内存等。
  9. 面试考点

    • 系统调用和库函数的区别:系统调用直接与内核交互,而库函数是对系统调用的封装。

    • 常用系统调用的调用流程:系统调用通过软中断(如 x86 的 int 0x80syscall 指令)进入内核。

    • 高频考察点:

      • 文件操作(openreadwrite)。
      • 进程管理(forkexecwait)。
      • 网络通信(socketconnectbind)。
    • 系统调用性能优化:

      • 使用批量操作(如 readvwritev)。
      • 使用异步 I/O 减少阻塞。

四. 中断


五. Linux

5.1 基础操作

问:linux中有哪些常见的指令?

1. 文件和目录操作

指令 功能 示例
ls 列出目录内容;列出文件或者目录的信息,目录的信息就是其中包含的文件。 ls -l
cd 切换目录 cd /home/user
pwd 显示当前工作目录 pwd
mkdir 创建目录 mkdir new_folder
rmdir 删除空目录 rmdir empty_folder
rm 删除文件或目录 rm -r folder
cp 复制文件或目录 cp source.txt dest.txt
mv 移动或重命名文件 mv oldname.txt newname.txt
touch 创建空文件 touch newfile.txt
find 查找文件 find / -name "file.txt"
locate 快速查找文件(需安装并更新数据库) locate filename
stat 查看文件详细信息 stat file.txt
file 查看文件类型 file file.txt
cat 获取文件内容
  • grep:正则表达式

    • ```bash
      $ grep [-acinv] [–color=auto] 搜寻字符串 filename
      -c : 统计匹配到行的个数
      -i : 忽略大小写
      -n : 输出行号
      -v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行
      –color=auto :找到的关键字加颜色显示
      1
      2
      3
      4
      5
      6
      7
      8

      * ```bash
      $ grep -n 'the' regular_express.txt
      8:I can't finish the test.
      12:the symbol '*' is represented as start.
      15:You are the best is mean you are the no. 1.
      16:The world Happy is the same with "glad".
      18:google is the best tools for search keyword
  • cut:对数据进行切分,取出想要的部分。

    • ```bash
      $ cut
      -d :分隔符
      -f :经过 -d 分隔后,使用 -f n 取出第 n 个区间
      -c :以字符为单位取出区间
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189

      * 切分过程一行一行地进行

      **2. 文件内容查看**

      | **指令** | **功能** | **示例** |
      | -------- | ------------------------------ | --------------------- |
      | `cat` | 查看文件内容 | `cat file.txt` |
      | `tac` | 反向查看文件内容 | `tac file.txt` |
      | `less` | 分页查看文件 | `less file.txt` |
      | `more` | 分页查看文件 | `more file.txt` |
      | `head` | 查看文件的前几行 | `head -n 10 file.txt` |
      | `tail` | 查看文件的后几行 | `tail -n 10 file.txt` |
      | `nl` | 查看文件并显示行号 | `nl file.txt` |
      | `wc` | 统计文件的行数、单词数和字符数 | `wc -l file.txt` |

      **3. 权限管理**

      | **指令** | **功能** | **示例** |
      | -------- | ------------------ | ---------------------- |
      | `chmod` | 修改文件或目录权限 | `chmod 755 file.txt` |
      | `chown` | 修改文件所有者 | `chown user file.txt` |
      | `chgrp` | 修改文件所属组 | `chgrp group file.txt` |
      | `umask` | 设置默认权限掩码 | `umask 022` |

      **4. 压缩与解压**

      | **指令** | **功能** | **示例** |
      | -------- | --------------------- | ------------------------------ |
      | `tar` | 打包或解包 | `tar -cvf archive.tar folder/` |
      | `gzip` | 压缩文件 | `gzip file.txt` |
      | `gunzip` | 解压 `.gz` 文件 | `gunzip file.txt.gz` |
      | `zip` | 压缩为 `.zip` 文件 | `zip archive.zip file1 file2` |
      | `unzip` | 解压 `.zip` 文件 | `unzip archive.zip` |
      | `xz` | 压缩文件为 `.xz` 格式 | `xz file.txt` |
      | `unxz` | 解压 `.xz` 文件 | `unxz file.txt.xz` |

      **5. 系统管理**

      关机:

      * who:先用 who 命令查看有没有其它用户在线。
      * sync:为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘,因此关机之前需要先进行 sync 同步操作。
      * shutdown:`shutdown [-krhc] 时间 [信息]` 。

      | **指令** | **功能** | **示例** |
      | -------- | ---------------------- | --------------- |
      | `df` | 查看磁盘使用情况 | `df -h` |
      | `du` | 查看目录或文件大小 | `du -sh folder` |
      | `free` | 查看内存使用情况 | `free -h` |
      | `top` | 实时查看系统进程 | `top` |
      | `htop` | 彩色增强版进程查看工具 | `htop` |
      | `ps` | 查看当前进程 | `ps aux` |
      | `kill` | 终止进程 | `kill -9 PID` |
      | `uptime` | 查看系统运行时间 | `uptime` |
      | `uname` | 查看系统信息 | `uname -a` |
      | `who` | 查看当前登录用户 | `who` |
      | `whoami` | 查看当前用户身份 | `whoami` |

      **6. 网络相关**

      | **指令** | **功能** | **示例** |
      | ---------- | ------------------------------ | ----------------------------------- |
      | `ping` | 测试网络连通性 | `ping www.google.com` |
      | `ifconfig` | 查看或配置网络接口(旧版工具) | `ifconfig` |
      | `ip` | 查看或配置网络接口(推荐工具) | `ip addr` |
      | `netstat` | 查看网络连接和端口使用情况 | `netstat -tuln` |
      | `ss` | 替代 `netstat` 的现代网络工具 | `ss -tuln` |
      | `curl` | 下载或发送网络请求 | `curl https://example.com` |
      | `wget` | 下载文件 | `wget https://example.com/file.zip` |
      | `scp` | 通过 SSH 传输文件 | `scp file.txt user@host:/path/` |
      | `ssh` | 远程登录 | `ssh user@host` |

      **7. 用户管理**

      | **指令** | **功能** | **示例** |
      | --------- | ------------------------------------------------------------ | ------------------ |
      | `adduser` | 添加用户 | `adduser username` |
      | `deluser` | 删除用户 | `deluser username` |
      | `passwd` | 修改用户密码 | `passwd username` |
      | `su` | 切换用户 | `su username` |
      | `sudo` | 提升权限执行命令,允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令 | `sudo apt update` |
      | `id` | 查看用户 ID 和组 ID | `id username` |

      **8. 日志管理**

      | **指令** | **功能** | **示例** |
      | ------------ | ------------ | ------------------------- |
      | `dmesg` | 查看内核日志 | `dmesg |
      | `journalctl` | 查看系统日志 | `journalctl -xe` |
      | `tail` | 实时查看日志 | `tail -f /var/log/syslog` |

      **9. 权限与SELinux**

      | **指令** | **功能** | **示例** |
      | ------------ | ----------------- | -------------- |
      | `getenforce` | 查看 SELinux 状态 | `getenforce` |
      | `setenforce` | 设置 SELinux 模式 | `setenforce 0` |

      **10. 包管理(以 Ubuntu/Debian 为例)**

      | **指令** | **功能** | **示例** |
      | ------------- | ------------------ | ------------------------------- |
      | `apt update` | 更新软件包列表 | `sudo apt update` |
      | `apt upgrade` | 升级已安装的软件包 | `sudo apt upgrade` |
      | `apt install` | 安装软件包 | `sudo apt install package_name` |
      | `apt remove` | 删除软件包 | `sudo apt remove package_name` |





      #### 问:说一下Linux的启动方式?



      1. **加电启动(BIOS/UEFI 阶段)开机BIOS自检**
      - **BIOS (Basic Input/Output System)****UEFI (Unified Extensible Firmware Interface)** 是硬件的初始化程序。

      - 主要任务:

      1. **初始化硬件设备**(CPU、内存、硬盘等)。
      2. **检测并加载启动设备**
      3. **将控制权交给引导加载程序**(如 GRUB)。
      2. **引导加载程序(Bootloader 阶段)MBR引导、grub引导菜单**
      - **引导加载程序**是存储在**硬盘 MBR(主引导记录)**或 EFI 分区的一个小程序,用于加载操作系统内核。

      - 常见引导加载程序:

      - GRUB(GRUB Legacy 或 GRUB2)。
      - LILO(已过时)。
      - systemd-boot(适用于 systemd 系统)。

      - 主要任务:

      1. **显示启动菜单,让用户选择内核或操作系统**
      2. **加载内核(Kernel)和 initramfs/initrd(临时文件系统,用于初始化硬件)**
      3. **将控制权移交给内核**
      3. **内核启动(Kernel 阶段)**
      - 内核是 Linux 操作系统的核心部分,负责硬件管理和提供系统功能。

      - 主要任务:

      1. **解压并加载内核到内存中。**
      2. **初始化内核数据结构和设备驱动。**
      3. **挂载根文件系统(通常由 initramfs 提供临时支持,稍后切换到实际的根文件系统)。**
      4. **启动第一个用户空间进程(通常是 `/sbin/init` 或等效进程)。**
      4. 初始化进程(Init System 阶段)启动init进程、读取inittab文件
      - 在 Linux 系统中,`init` 是第一个用户空间进程,进程号始终为 `1`。

      - 常见的初始化系统:

      1. **SysVinit**:传统的 init 系统,通过运行级别(runlevel)管理服务。
      2. **Upstart**:改进版 init 系统,支持事件驱动(已逐步被淘汰)。
      3. **systemd**(主流):现代化的初始化系统,启动速度快,支持并行服务启动。

      - 主要任务:

      1. **读取配置文件(如 `/etc/inittab` 或 `/etc/systemd/system`)。**
      2. **根据运行级别或目标(target),启动相关服务和守护进程。**
      3. **准备用户登录环境。**
      5. 启动用户空间服务(User Space Services 阶段)启动mingetty进程
      - systemd 的作用:

      - **加载必要的系统服务和守护进程(如网络管理、日志记录)。**
      - **启动目标单元(target),如多用户目标(multi-user.target)或图形界面目标(graphical.target)。**

      - 常见服务:

      - 网络服务(`networkd`、`NetworkManager`)。
      - 日志服务(`rsyslog`、`journald`)。
      - 文件系统服务(挂载 NFS、自动挂载设备)。
      6. 提供登录界面(Login Prompt 阶段)登录系统
      - 启动完成后,系统提供用户登录界面,允许用户登录使用。

      - 登录方式:

      1. **TTY 控制台**:文本模式的登录界面,通常在 `Ctrl + Alt + F1~F6` 提供。
      2. **图形界面**:如 GNOME、KDE 等桌面环境提供的 GUI 登录界面。

      - 登录后执行:

      - 加载用户环境变量(如 `.bashrc`、`.profile`)。
      - 提供交互式 shell 或桌面环境。

      **Linux 启动过程总结**

      ```plaintext
      BIOS/UEFI → Bootloader (如 GRUB) → Kernel → Init (如 systemd) → User Services → Login Prompt

Linux 常见启动方式

  1. 正常启动:默认引导加载程序按配置启动系统。

  2. 单用户模式 (Single-User Mode):

    • 用于维护或修复系统,只启动必要的服务。
    • 在 GRUB 中编辑内核参数,添加 singleinit=/bin/bash
  3. 救援模式 (Rescue Mode):用于修复无法正常启动的系统,加载最少的服务。

  4. 网络启动 (PXE Boot):从网络服务器加载操作系统,常用于无盘工作站或批量部署。

  5. 安全模式:限制系统功能,仅启动核心服务,用于排查问题。

  6. Live 模式:从可启动介质(如 USB 或光盘)直接运行操作系统,不影响现有系统。

问:select、poll、epoll有没有了解过,讲解一下?区别?

在操作系统和网络编程中,**selectpoll** 和 epoll 是三种常见的 I/O 多路复用机制,用于在一个线程中同时监控多个文件描述符(如网络套接字)的状态。这些机制广泛用于实现高并发服务器。

总结

  • select

    • 简单易用,但性能和扩展性较差。
    • 适用于文件描述符较少的应用。
  • poll

    • 相比 select 更灵活,但性能依然受限于线性扫描。
    • 适用于文件描述符数量中等的场景。
  • epoll

    • 提供最高性能,适合高并发、大规模连接的服务器。
    • 是现代 Linux 网络编程的首选。

选择哪种机制取决于场景和需求,推荐在高并发场景中使用 **epoll**。

  1. `select``

    概念

    • 是最早期的 I/O 多路复用机制。
    • 使用一个固定大小的文件描述符集合(fd_set),通过检查文件描述符的读、写、异常状态来决定是否可以进行 I/O 操作。

    特点

    1. 文件描述符限制

      • 在大多数系统中,select 能处理的文件描述符数量有限(通常是 1024,FD_SETSIZE 可调整)。
    2. 阻塞和非阻塞

      • 可以设为阻塞或指定超时时间(timeout 参数)。
    3. 效率低下

      • 每次调用时,需要将文件描述符集合从用户态复制到内核态(开销较大)。
      • 内核需要遍历整个文件描述符集合,效率较低。
    4. 不支持边缘触发

      • 只能通过轮询检查状态,效率较差。

    调用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(sock_fd, &readfds);
    struct timeval timeout = {5, 0}; // 超时时间 5 秒

    int ret = select(sock_fd + 1, &readfds, NULL, NULL, &timeout);
    if (ret > 0) {
    if (FD_ISSET(sock_fd, &readfds)) {
    // 处理可读事件
    }
    }

  2. ``poll`

    概念

    • select 的改进版本,没有文件描述符数量限制。
    • 使用一个数组(pollfd)代替 fd_set,数组中每个元素都包含文件描述符及其事件信息。

    特点

    1. 无限制文件描述符

      • 不再受 FD_SETSIZE 限制。
    2. 依然线性扫描

      • 内核仍需遍历所有文件描述符来检查状态,效率仍不高。
    3. 动态数组

      • 文件描述符集合是动态分配的,不像 select 使用固定大小的集合。

    调用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct pollfd fds[2];
    fds[0].fd = sock_fd1;
    fds[0].events = POLLIN; // 关注可读事件
    fds[1].fd = sock_fd2;
    fds[1].events = POLLIN;

    int ret = poll(fds, 2, 5000); // 超时 5 秒
    if (ret > 0) {
    if (fds[0].revents & POLLIN) {
    // 处理 sock_fd1 的可读事件
    }
    }
  3. epoll

    概念

    • poll 的进一步优化,由 Linux 2.6 引入,专为高效 I/O 处理设计。
    • 提供了更高效的事件通知机制,支持大规模文件描述符监控。

    特点

    1. 无上限

      • 文件描述符数量不受限制(仅受系统资源限制)。
    2. 事件驱动

      • 支持 水平触发(Level-Triggered, LT)边缘触发(Edge-Triggered, ET)
      • 边缘触发可以避免重复通知,适合高性能场景。
    3. 更高性能

      • 文件描述符状态变化后,内核通过事件通知机制将就绪事件加入一个就绪队列,避免重复扫描。
    4. 分两步操作

      • **epoll_create**:创建 epoll 实例。
      • **epoll_ctl**:注册、修改、删除监控的文件描述符。
      • **epoll_wait**:等待事件发生。

    调用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int epfd = epoll_create(1); // 创建 epoll 实例
    struct epoll_event ev, events[10];
    ev.events = EPOLLIN;
    ev.data.fd = sock_fd;

    epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ev); // 注册文件描述符

    int ret = epoll_wait(epfd, events, 10, 5000); // 等待事件
    for (int i = 0; i < ret; i++) {
    if (events[i].events & EPOLLIN) {
    // 处理可读事件
    }
    }
  4. 区别

特性 select poll epoll
文件描述符限制 固定大小(1024 或更高) 无限制(取决于系统资源) 无限制(取决于系统资源)
性能 扫描所有文件描述符 扫描所有文件描述符 事件驱动,无需遍历
事件触发类型 水平触发 水平触发 水平触发和边缘触发
适用场景 适用于少量描述符 适用于中等数量描述符 适用于大规模描述符监控
内存复制 每次调用复制 fd_set 每次调用复制 pollfd 数组 一次注册后无需重复操作
内核支持 所有 Unix 系统 所有 Unix 系统 Linux 2.6 及以上

问:如何查看Linux系统运行状态?

1. 系统整体状态

  • **top**:

    • 显示实时的系统运行状态,包括 CPU、内存使用情况以及进程。
    • 交互操作:
      • q 退出。
      • M 按内存排序。
      • P 按 CPU 使用率排序。
  • **htop**:

    • 类似 top,但界面更美观,操作更直观。
    • 需要安装:sudo apt install htopsudo yum install htop
  • **uptime**:

    • 显示系统运行时间、当前用户数量、系统平均负载。

    • 示例输出:

      1
      13:05:18 up 2 days,  5:30,  3 users,  load average: 0.15, 0.08, 0.02
  • **vmstat**:

    • 提供 CPU、内存、I/O、进程的详细信息。
    • 示例:vmstat 1 5 每秒刷新一次,共显示 5 次。

2. CPU 相关状态

  • **lscpu**:
    • 显示 CPU 的架构、核心数量、线程数、频率等详细信息。
  • **mpstat**(需安装 sysstat 包):
    • 显示每个 CPU 的使用率。
    • 示例:mpstat -P ALL 1 5 列出每个CPU的信息,每秒刷新一次,共显示 5 次。
  • **cat /proc/cpuinfo**:
    • 查看 CPU 的详细信息,如型号、频率等。

3. 内存状态

  • **free**:

    • 显示系统内存使用情况。

    • 示例:统计已用和空闲的内存

      1
      free -h

      (以人类可读格式显示)。

      1
      2
      total        used        free      shared  buff/cache   available
      Mem: 15Gi 2Gi 11Gi 142Mi 1.3Gi 12Gi
  • **cat /proc/meminfo**:

    • 查看内存的详细信息,包括总量、空闲量、缓存等。
  • $ vmstat 5 # 每隔5秒刷新
    • 统计CPU,内存及虚拟内存使用状态。

4. 磁盘状态

  • **df**:

    • 显示磁盘分区的使用情况。

    • 示例:

      1
      df -h

      (以人类可读格式显示)。

      1
      2
      Filesystem      Size  Used Avail Use% Mounted on
      /dev/sda1 50G 20G 30G 40% /
  • **du [OPTION] [DIR]**:

    • 查看文件或目录占用的磁盘空间。查看指定目录下各子目录及文件的大小
    • 示例:du -sh /var/log 查看 /var/log 目录的大小。
  • **lsblk**:

    • 显示系统中的块设备(磁盘及其分区)信息。
  • **iostat [OPTION] [INTERVAL]**(需安装 sysstat 包):

    • 查看磁盘 I/O 读写情况。统计CPU及IO操作信息
    • 示例:iostat -d 1 5 每秒刷新一次,共显示 5 次。

5. 网络状态

  • ifconfig 或 **ip a**:
    • 显示网络接口的状态和 IP 地址。
  • netstat 或 **ss**:
    • 查看网络连接,端口,Socket信息,监听状态。
    • 示例:ss -tuln 显示监听的 TCP 和 UDP 端口。
  • **ping**:
    • 测试网络连通性。
    • 示例:ping www.google.com
  • **traceroute**:
    • 显示数据包到目标主机的路由。
    • 示例:traceroute www.google.com
  • **iftop**:
    • 显示实时网络流量。
    • 需要安装:sudo apt install iftop

6. 进程状态

  • **ps**:
    • 查看当前运行的进程,列出进程信息。
    • 示例:ps aux | grep apache 查找与 apache 相关的进程。
  • **pidstat**(需安装 sysstat 包):
    • 查看进程的资源使用情况。
    • 示例:pidstat -u 1 每秒刷新一次,显示 CPU 使用率。
  • **pgrep**:
    • 按名称查找进程。
    • 示例:pgrep sshd
  • $ pstree [OPTION] :显示进程树信息
  • $ pmap [OPTION] [PID] :显示每个进程的内存映射信息
  • $ top [OPTION] :动态显示进程列表

7. 系统日志

  • **journalctl**:
    • 查看系统日志(适用于 systemd 系统)。
    • 示例:journalctl -xe 查看最近的错误日志。
  • **dmesg**:
    • 查看内核启动消息和硬件相关信息。

8. 用户和登录信息

  • **who**:
    • 显示当前登录用户。
  • **w**:
    • 显示登录用户及其活动。
  • **last**:
    • 查看用户登录历史。

9. 其他综合工具

  • **sar**(需安装 sysstat 包):
    • 收集和查看系统性能数据。
    • 示例:sar -u 1 5 查看 CPU 使用率。
  • **glances**:
    • 综合性能监控工具,类似于 htop,但功能更强大。
    • 需要安装:sudo apt install glances

问:说一下Linux如何查找CPU故障?

在 Linux 系统中,查找 CPU 故障需要结合系统日志、硬件状态检查工具以及性能监控工具。

1. 检查系统日志

系统日志通常是排查 CPU 故障的第一步,因为 CPU 故障可能导致内核报错或系统异常,日志中会记录相关信息。

  • 查看内核消息

    • 使用 dmesg 查看内核启动和运行过程中的信息:

      1
      dmesg | grep -i cpu

      搜索与 CPU 相关的错误,例如 “CPU stuck”, “Machine Check Exception” (MCE) 等。

  • 查看系统日志

    • 使用 journalctl 查看系统日志:

      1
      journalctl -k | grep -i cpu

      查找与 CPU 相关的警告或错误消息。

  • 检查硬件日志

    • 某些系统可能有硬件特定日志(如 /var/log/messages/var/log/syslog),可以查找异常信息。

2. 检查 CPU 温度和状态

过热或硬件故障可能导致 CPU 异常。

  • lm-sensors 工具

    • 安装并使用 sensors 检查 CPU 温度:

      1
      2
      sudo apt install lm-sensors
      sensors

    查看是否有温度过高或异常的情况。

  • hwinfo 工具

    • 获取硬件详细信息,检查 CPU 的健康状况:

      1
      2
      sudo apt install hwinfo
      hwinfo --cpu

3. 检查 CPU 使用率和负载

CPU 负载过高可能是某些问题的原因或结果。

  • **tophtop**:

    • 查看实时 CPU 使用率,检查是否有异常的进程占用大量 CPU。
  • sar 工具

    • 查看 CPU 使用的历史记录:

      1
      sar -u 1 5

      检查是否有 CPU 使用率异常高的情况。

  • **mpstat**:

    • 监控每个 CPU 核心的使用率:

      1
      mpstat -P ALL 1
  • 检查平均负载

    • 使用 uptimetop检查系统平均负载是否异常:如果负载长期超过 CPU 核心数,可能存在性能问题。

4. 检查 CPU 硬件错误

  • mcelog 工具

    • 检查 CPU 硬件错误(如机器检查异常 - Machine Check Exception)。

    • 安装并使用:

      1
      2
      sudo apt install mcelog
      sudo mcelog --ascii

      输出 CPU 错误日志,检测是否有硬件问题。

  • **/proc/cpuinfo**:

    • 查看 CPU 详细信息,确认硬件特性:

      1
      cat /proc/cpuinfo

      检查是否有与硬件配置不符的地方。

  • BIOS/UEFI 检查

    • 某些硬件故障可能需要检查 BIOS/UEFI 的日志或设置,确保 CPU 配置正常。

5. 性能分析和瓶颈排查

  • perf 工具

    • 性能分析工具,可深入分析 CPU 性能问题。

    • 示例:记录一段时间的 CPU 性能数据并生成报告:

      1
      2
      perf record -a -g sleep 10
      perf report
  • **straceltrace**:

    • 跟踪特定进程的系统调用,排查导致 CPU 高负载的原因。

6. 硬件健康检测

  • stress 工具

    • 用于压力测试 CPU,验证 CPU 是否在高负载下运行正常。

    • 安装并运行:

      1
      2
      sudo apt install stress
      stress --cpu 4 --timeout 60

      观察是否在高负载下系统会出现错误或崩溃。

  • **memtest86+**:

    • CPU 的问题可能与内存有关,可以运行内存测试工具检查。

7. 内核参数和补丁检查

  • 检查是否有未应用的内核补丁,尤其是与 CPU 微码相关的更新:

    1
    2
    sudo apt install intel-microcode  # Intel CPU
    sudo apt install amd64-microcode # AMD CPU
  • 查看当前内核参数:

    1
    cat /proc/cmdline

8. 硬件故障处理

如果怀疑是硬件故障,可以采取以下措施:

  1. 清理和检查硬件连接:

    • 检查 CPU 风扇和散热片是否正常工作,清除灰尘。
  2. 更换硬件:

    • 更换 CPU、主板,或者将 CPU 移至其他主机测试。
  3. 联系厂商:

    • 如果 CPU 在保修期内,可联系厂商进行进一步的诊断和更换。

5.2 磁盘

5.3 分区

5.4 文件系统

问:Linux文件系统有哪些?

Linux 支持多种文件系统,不同的文件系统有其特定的设计目标、性能特点和使用场景。以下是 Linux 中常见的文件系统及其特点:

1. 传统文件系统

1.1 ext(Extended File System)系列

  • ext2:
    • 全称:Second Extended File System。
    • 特点:
      • 是 Linux 的第一个商业文件系统。
      • 不支持日志功能(Journal)。
      • 适用于闪存等不需要日志的存储设备。
    • 使用场景:早期 Linux 系统。
  • ext3:
    • 全称:Third Extended File System。
    • 特点:
      • 基于 ext2 增加了日志功能。
      • 支持快速恢复,防止数据丢失。
      • 向下兼容 ext2。
    • 使用场景:主流 Linux 系统(过时)。
  • ext4:
    • 全称:Fourth Extended File System。
    • 特点:
      • 支持大文件(单文件大小可达 16TB)。
      • 更快的性能,延迟分配(Delayed Allocation)。
      • 向后兼容 ext3。
    • 使用场景:现代 Linux 系统默认文件系统之一。

1.2 XFS

  • XFS:

    • 由 SGI 开发,适用于高性能场景。
    • 特点:
      • 支持并行 I/O,性能优异。
      • 支持大文件和大分区。
      • 使用 B+ 树加速目录操作。
    • 使用场景:高性能和大规模存储系统,如媒体处理、数据库。

1.3 ReiserFS

  • ReiserFS:

    • 特点:
      • 优化小文件存储。
      • 高效的磁盘空间利用率。
      • 支持日志功能。
    • 使用场景:曾经流行,但因开发停止逐渐被淘汰。

1.4 JFS (Journaled File System)

  • JFS:

    • IBM 开发的日志文件系统。
    • 特点:
      • 高性能,适合大文件存储。
      • 使用较少的 CPU 资源。
    • 使用场景:一些企业级应用。

2. 新型文件系统

2.1 Btrfs (B-tree File System)

  • Btrfs:

    • 由 Oracle 开发,现代文件系统。
    • 特点:
      • 支持快照、子卷、数据校验。
      • 高度灵活,支持在线扩展和缩减。
      • 原生支持 RAID。
    • 使用场景:需要快照和数据校验的场景,如云存储。

2.2 ZFS

  • ZFS:

    • 由 Sun Microsystems 开发,现归 Oracle。
    • 特点:
      • 支持快照、压缩、数据校验和自愈。
      • 原生支持 RAID 功能。
      • 需要更多内存资源。
    • 使用场景:存储服务器、大数据处理。

3. 特殊用途文件系统

3.1 tmpfs

  • tmpfs:

    • 基于内存的文件系统,数据存储在内存中。
    • 特点:
      • 高速读写。
      • 数据随系统重启丢失。
    • 使用场景:临时文件存储,如 /tmp

3.2 procfs

  • procfs:

    • 虚拟文件系统,挂载在 /proc
    • 特点:
      • 提供系统和进程信息。
      • 数据动态生成,不占用实际磁盘空间。
    • 使用场景:查看系统状态和配置。

3.3 sysfs

  • sysfs:

    • 虚拟文件系统,挂载在 /sys
    • 特点:
      • 提供内核和硬件设备信息。
      • procfs 补充使用。
    • 使用场景:设备和驱动程序调试。

3.4 cgroupfs

  • cgroupfs:

    • 提供控制组(Control Groups)信息。
    • 特点:
      • 用于资源限制和监控(CPU、内存等)。
    • 使用场景:容器管理(如 Docker)。

4. 网络文件系统

4.1 NFS (Network File System)

  • NFS:

    • 由 Sun Microsystems 开发,用于网络文件共享。
    • 特点:
      • 支持远程文件访问。
      • 适用于多台机器共享数据。
    • 使用场景:企业网络存储。

4.2 CIFS/SMB

  • CIFS/SMB:

    • Windows 网络文件共享协议,Linux 支持通过 samba 实现。
    • 特点:
      • 支持 Windows 文件共享。
    • 使用场景:混合环境文件共享。

4.3 GlusterFS 和 CephFS

  • 分布式文件系统:

    • GlusterFS 和 CephFS 是两种流行的分布式文件系统。
    • 特点:
      • 支持大规模存储和高可用性。
    • 使用场景:云存储、分布式存储。

5. FAT 系列文件系统

  • FAT32 和 exFAT
    • 特点:
      • FAT32 支持最大 4GB 文件。
      • exFAT 支持更大文件且兼容性好。
    • 使用场景:USB、SD 卡等便携式存储。
  • NTFS:
    • Windows 文件系统,Linux 支持通过 ntfs-3g 工具读写。
    • 使用场景:跨平台存储访问。

6. 文件系统对比

文件系统 日志支持 文件大小限制 特点 使用场景
ext4 16TB 稳定、高性能 通用场景
XFS 8EB 高性能并行 I/O 高性能、大数据
Btrfs 16EB 快照、RAID 支持,灵活管理 云存储、高可靠性
ZFS 16EB 数据校验、快照、自愈 存储服务器、大数据
tmpfs 取决于内存 基于内存的高速临时存储 临时文件
NFS 依赖底层 FS 网络文件共享 企业网络存储

问:文件系统的组成?inode 和 block ?

对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中查找文件内容所在的所有 block,然后把所有 block 的内容读出来。而对于 FAT 文件系统,它没有 inode,每个 block 中存储着下一个 block 的编号。

总结

  • inode 是 Linux 文件系统管理文件的核心结构,用于存储文件元数据及指向数据块的指针。
  • block 是存储文件实际数据的基本单元。
  • 两者相辅相成,inode 管理文件结构和定位 block,而 block 负责实际的数据存储。

Linux 文件系统的组成是一个复杂但高效的架构,主要包括元数据和数据两部分。关键概念如 inodeblock 是理解文件系统工作的核心。以下是详细的解释:

文件系统的组成

  1. 超级块(Superblock):记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;

    • 保存文件系统的全局信息。
    • 包括:
      • 文件系统的类型(如 ext4、XFS)。
      • 文件系统的大小。
      • 已用和未用的 inode 和 block 数量。
      • 文件系统的状态(是否干净)。
      • 文件系统挂载点等信息。
  2. inode(索引节点):一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号;

    • 是文件的元数据结构,描述文件的属性。
    • 每个文件或目录在文件系统中都有一个唯一的 inode。
    • 存储信息:
      • 文件类型(普通文件、目录、符号链接等)。
      • 权限(如 rwx)。
      • 文件所有者(用户 ID 和组 ID)。
      • 文件大小。
      • 文件创建、修改、访问时间戳。
      • 指向数据 block 的指针。
    • 不存储文件名:文件名与 inode 的映射由目录项存储。
  3. 数据块(Data Block):block:记录文件的内容,文件太大时,会占用多个 block。

    • 用于存储文件的实际内容(数据)。
    • 每个文件的数据分为若干个 block 存储,大小通常是文件系统初始化时定义的(如 4KB)。
    • inode 中保存指向这些数据 block 的指针。
  4. 目录结构

    • 目录本质上是一个特殊的文件,记录文件名与 inode 的映射关系。

    • 例如:

      1
      文件名: test.txt -> inode: 1234

      目录项中存储了文件名和对应的 inode 号。

  5. 日志(Journal)(仅限日志型文件系统):

    • 用于记录文件系统的元数据操作(如文件创建、删除)。
    • 提供崩溃恢复能力,防止因断电或系统崩溃导致数据损坏。
  6. 位图(Bitmap):block bitmap:记录 block 是否被使用的位图。

    • inode 位图:记录 inode 的分配状态。
    • block 位图:记录数据 block 的分配状态。

inode 和 block 的详细解析

1. inode(索引节点)

  • 结构: inode 是一个固定大小的结构体,文件系统中所有的文件和目录都通过 inode 号进行标识和管理。

  • inode 包含的元数据

    属性 描述
    文件类型 文件、目录、符号链接、设备等
    权限 读、写、执行权限(rwx)
    用户 ID 和组 ID 文件的所有者和所属组
    文件大小 以字节为单位的文件大小
    时间戳 创建、修改、访问时间
    链接计数 文件的硬链接数量
    数据 block 指针 指向实际存储数据的 block
  • 指针结构

    • 直接指针:指向实际数据块。
    • 间接指针:如果文件很大,inode 会使用间接指针(一级、二级、三级)指向更多的数据块。

2. block(数据块)

  • 作用
    • 数据块是用于存储文件数据的基本单元。
    • 一个文件可能占用多个数据块。
  • 块大小
    • 文件系统初始化时设置(常见值为 1KB、2KB、4KB)。
    • 块大小影响性能和空间利用率:
      • 小块:适合小文件,减少空间浪费。
      • 大块:适合大文件,提高访问性能。
  • block 的分配
    • 文件系统使用 inode 中的指针管理数据块。
    • 如果数据块不足,文件会通过间接指针扩展数据存储。

inode 和 block 的关系

  1. inode 中指向 block
    • inode 中的指针用于定位存储文件数据的 block。
    • 每个文件的所有数据 block 都由 inode 通过直接或间接指针管理。
  2. block 存储实际数据
    • 数据 block 存储文件的实际内容。
    • 目录文件的 block 存储文件名与 inode 号的映射。

inode 和 block 的区别

属性 inode block
定义 文件的元数据 文件的实际数据存储区域
功能 描述文件属性并定位数据 block 存储文件的内容或目录的映射
大小 固定大小(如 128 字节、256 字节) 可配置(如 1KB、4KB)
数量 由文件系统设置,固定总数 根据磁盘大小和块大小计算得出

inode 和 block 的重要概念

  1. inode 表
    • 文件系统中所有 inode 的集合。
    • 每个 inode 占用固定空间。
  2. 文件存储原理
    • 创建文件时:
      • 分配一个 inode。
      • 分配数据块并更新 inode 的指针。
  3. 磁盘空间浪费(碎片化)
    • 文件大小小于块大小时,未用部分会浪费(内部碎片)。
    • 小文件占用一个 inode 和一个 block,即使数据很少。

问:如何进行数据恢复?

数据恢复是指在数据丢失后,通过技术手段恢复丢失的文件或数据的过程。数据恢复的方法依赖于数据丢失的原因以及存储介质的类型。在操作系统层面或硬件存储层面,数据恢复通常包括以下几种场景和方法:

一、数据丢失的常见原因

  1. 人为误操作:

    • 删除文件(rm 命令误操作)。
    • 格式化存储设备。
  2. 文件系统损坏:

    • 文件系统被破坏导致数据不可见。
  3. 硬盘损坏:

    • 磁盘出现坏道。
    • 硬盘机械故障。
  4. 病毒或恶意软件攻击:

    • 文件被加密、删除或损坏。
  5. 断电或系统崩溃:

    • 数据未写入磁盘或写入不完整。
  6. 分区表损坏:

    • 分区表丢失或被覆盖。

二、数据恢复的方法

根据数据丢失的原因,可以选择适当的恢复方法。以下是具体操作方法:

1. 从回收站或备份中恢复

  • 如果文件被删除,但存储在回收站或其他临时存储区域,可以直接恢复。
  • 如果有系统备份,可以通过备份工具恢复丢失的数据。

2. 文件删除后的恢复

  • 在 Linux 中,文件被删除后,实际数据仍存储在硬盘上,除非被覆盖。

  • 恢复方法:

    1. 停止写操作:删除文件后立即停止对磁盘的任何写操作,以防止数据被覆盖。

    2. 使用恢复工具:

      • extundelete

        (针对 ext 文件系统):

        1
        sudo extundelete /dev/sdX --restore-file <filename>
      • TestDisk

        (支持多种文件系统):

        • 功能:恢复文件、修复分区表。

        • 安装:

          1
          sudo apt install testdisk
        • 运行:

          1
          sudo testdisk
    3. dd 命令:

      • 用于创建磁盘的原始副本,便于后续数据分析。

        1
        sudo dd if=/dev/sdX of=/path/to/backup.img

3. 文件系统损坏后的恢复

  • 文件系统损坏可能导致文件不可访问,但数据仍然存在。

  • 恢复工具:

    • fsck(文件系统检查工具):

      • 修复文件系统损坏。

      • 使用:

        1
        sudo fsck /dev/sdX
    • e2fsck(针对 ext 文件系统):

      • 检查和修复文件系统。

      • 使用:

        1
        sudo e2fsck -y /dev/sdX
    • photorec(基于文件签名恢复数据):

      • 可恢复文件内容,即使文件系统损坏。
  • 适合恢复图片、文档等已知类型文件。

4. 硬盘坏道或物理损坏

  • 坏道检测与修复:

    • 使用 badblocks 工具检测坏道:

      1
      sudo badblocks -v /dev/sdX
    • 使用 e2fsck 标记坏道:

      1
      sudo e2fsck -c /dev/sdX
  • 硬盘镜像与克隆:

    • 通过 ddrescue 工具创建硬盘镜像:

      1
      sudo ddrescue /dev/sdX /path/to/backup.img /path/to/logfile
  • 专业服务:

    • 如果硬盘出现严重物理损坏(如磁头损坏、盘片划伤),需要联系专业数据恢复机构。

5. 分区表丢失后的恢复

  • TestDisk:

    • 功能:扫描并恢复丢失的分区表。
    • 使用:
      1. 启动 TestDisk。
      2. 选择磁盘。
      3. 搜索丢失的分区。
      4. 修复分区表并保存。

6. RAID 数据恢复

  • RAID 数据恢复比普通硬盘更复杂。

  • 方法:

    • 使用 RAID 重建工具(如 mdadm):

      1
      sudo mdadm --assemble --scan
    • 如果 RAID 阵列崩溃,需依赖专业数据恢复软件(如 R-Studio)。

三、数据恢复的原则

  1. 及时停止写操作:

    • 文件被删除后,数据仍可能存在,写入新数据可能覆盖原始数据。
  2. 备份磁盘镜像:

    • 使用 ddddrescue 备份磁盘镜像以保护原始数据。
  3. 不要轻易修复原始磁盘:

    • 在修复之前,应备份数据,以防修复失败导致进一步损坏。
  4. 优先尝试逻辑恢复:

    • 如果数据丢失是因误操作或文件系统损坏导致,应尝试逻辑恢复工具。

四、常用数据恢复工具总结

工具名称 功能 支持文件系统
extundelete 恢复 ext 文件系统删除文件 ext2/ext3/ext4
TestDisk 分区表恢复、文件恢复 多种文件系统
PhotoRec 基于文件签名恢复文件 多种文件系统
fsck 修复文件系统错误 ext 系列、xfs 等
ddrescue 镜像与坏道数据恢复 通用

五、总结

  • 数据恢复是一个技术性较强的过程,取决于数据丢失原因和存储设备状况。
  • 小型数据丢失可通过工具如 extundeleteTestDisk 恢复。
  • 硬件故障或大规模损坏情况下,优先备份磁盘镜像,再尝试恢复工具或联系专业机构。
  • 最重要的是:防患于未然,定期备份数据!

5.5 文件

问:Linux文件类型有哪些?

常见的文件类型及其含义有:

  • d:目录
  • -:文件
  • l:链接文件

在 Linux 中,文件类型是操作系统用于区分不同文件的类别。这些文件类型可以通过命令行工具(如 ls)查看,也可以通过文件系统元数据识别。以下是 Linux 中的主要文件类型及其说明:

Linux 文件类型

1. 普通文件(Regular File)

  • 特征:

    • 包含普通数据,如文本、二进制文件、图像等。

    • 文件类型标识:-(减号)。

    • 例如:

      1
      -rw-r--r--  1 user group 1024 Jan 12 10:00 example.txt
  • 子分类:

    • 文本文件:如 .txt.log
    • 二进制文件:如可执行文件、程序。
    • 压缩文件:如 .zip.tar.gz

2. 目录文件(Directory File)

  • 特征:

    • 用于存储其他文件和目录。

    • 文件类型标识:d

    • 例如:

      1
      drwxr-xr-x  2 user group 4096 Jan 12 10:00 mydir
  • 说明:

    • 目录是一个特殊文件,存储文件名和对应 inode 的映射。
  • 可以通过 cd 命令访问。

3. 链接文件(Link File)

  • 类型:

    • 硬链接(Hard Link):

      • 指向文件的 inode 号。
      • 和原文件共享同一数据,不占用额外空间。
      • 删除原文件,硬链接仍然有效。
    • 符号链接(Symbolic Link 或 Soft Link):

      • 指向文件的路径。

      • 文件类型标识:l

      • 例如:

        1
        lrwxrwxrwx  1 user group   10 Jan 12 10:00 symlink -> example.txt

4. 字符设备文件(Character Device File)

  • 特征:

    • 用于与字符设备交互。

    • 文件类型标识:c

    • 例如:

      1
      crw-rw-rw-  1 root root 5, 1 Jan 12 10:00 /dev/tty
  • 作用:

    • 提供逐字符输入/输出的设备,如终端、键盘。

5. 块设备文件(Block Device File)

  • 特征:

    • 用于与块设备交互。

    • 文件类型标识:b

    • 例如:

      1
      brw-rw----  1 root disk 8, 0 Jan 12 10:00 /dev/sda
  • 作用:提供随机访问数据的设备,如硬盘、USB 驱动器。

6. 套接字文件(Socket File)

  • 特征:

    • 用于进程间通信(IPC)。

    • 文件类型标识:s

    • 例如:

      1
      srwxrwxrwx  1 user group 0 Jan 12 10:00 my_socket
  • 作用:

    • 允许两个进程通过网络或本地通信。

7. 管道文件(FIFO,命名管道)

  • 特征:

    • 用于进程间通信,通过先入先出的方式传递数据。

    • 文件类型标识:p

    • 例如:

      1
      prw-r--r--  1 user group 0 Jan 12 10:00 my_pipe
  • 作用:

    • 实现简单的消息传递或数据流动。

查看文件类型的方法

1. 使用 ls 命令

  • 使用 ls -l 查看文件的类型标识:

    输出示例:

    1
    2
    3
    -rw-r--r--  1 user group  1024 Jan 12 10:00 file.txt
    drwxr-xr-x 2 user group 4096 Jan 12 10:00 mydir
    lrwxrwxrwx 1 user group 10 Jan 12 10:00 symlink -> file.txt

2. 使用 file 命令

  • file 命令通过分析文件内容判断类型:file filename

    输出示例:

    1
    2
    3
    file.txt: ASCII text
    mydir: directory
    symlink: symbolic link to file.txt

3. 使用 stat 命令

  • stat 提供详细的文件信息:
    1
    stat filename

文件类型标识对照表

文件类型 标识符 说明
普通文件 - 普通文本或二进制文件
目录文件 d 用于存储文件的目录
符号链接文件 l 指向其他文件的链接
字符设备文件 c 与字符设备交互
块设备文件 b 与块设备交互
套接字文件 s 进程间通信通道
管道文件 p FIFO 命名管道

总结

Linux 文件系统中,文件不仅仅指常规文件,还包括目录、链接、设备文件等特殊类型。通过 lsfile 等命令,我们可以轻松查看和区分文件类型。了解这些类型对于高效管理和使用 Linux 系统至关重要。

问:说一下Linux软链接以及和硬链接的区别?

Linux的文件链接分:

  • 硬链接:通过索引节点(inode)来识别文件。在 Linux 中,多个文件名指向同一索引节点是存在的,所以硬连接指通过索引节点来进行的连接,即每一个硬链接都是一个指向对应区域的文件。
  • 软链接:软链接又叫符号链接,这个文件包含了另一个文件的路径名,软连接可以是任意文件或目录,可以链接不同文件系统的文件,在对符号文件进行读或写操作的时候,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身,这一点类似于 Windows 操作系统下的快捷方式。
软链接 硬链接
inode 原文件&链接文件拥有不同的inode号,表明是不同文件 原文件和链接文件共用一个inode号,表示是同一个文件
文件属性 明确写出是链接文件 未写出,因为本质上硬链接文件和原文件相同
跨越文件系统建立 支持 不支持
链接数目 链接数目不会增加,文件大小不一样 显示的大小与原文件相同

在 Linux 文件系统中,软链接和硬链接是两种文件链接方式,用于指向文件或目录。以下是软链接和硬链接的详细说明及其区别:

1. 软链接(Symbolic Link)

概念

  • 软链接类似于 Windows 中的快捷方式,是指向目标文件路径的特殊文件。
  • 如果目标文件被删除或移动,软链接将失效。

特点

  1. 独立文件:软链接是一个独立的文件,保存目标文件的路径信息。

  2. 跨文件系统:可以在不同的文件系统或分区之间创建软链接。

  3. 指向目录:软链接可以指向文件或目录。

  4. 删除目标文件的影响:如果目标文件被删除,软链接会变成“断链”,无法访问。

创建软链接

1
ln -s <目标文件> <软链接名>

示例:

1
ln -s /home/user/file.txt link_to_file

查看软链接

  • 使用 ls -l 查看软链接:

    1
    ls -l link_to_file

    输出示例:

    1
    lrwxrwxrwx 1 user group 10 Jan 12 10:00 link_to_file -> /home/user/file.txt

2. 硬链接(Hard Link)

概念

  • 硬链接是指向文件数据(inode)的另一个文件名,与原文件共享相同的 inode。

特点

  1. 共享 inode:硬链接和目标文件共享相同的 inode 号,因此指向相同的文件数据。

  2. 不能跨文件系统:硬链接只能在同一个文件系统内创建。

  3. 不能指向目录:硬链接只能指向文件,不能指向目录。

  4. 删除目标文件的影响:即使原文件被删除,硬链接仍然可以访问文件数据,文件不会丢失,直到最后一个硬链接被删除。

创建硬链接

1
ln <目标文件> <硬链接名>

示例:

1
ln /home/user/file.txt link_to_file

查看硬链接

  • 使用 ls -l 查看文件的链接数:

    1
    ls -l

    输出示例:

    1
    2
    -rw-r--r-- 2 user group 1024 Jan 12 10:00 file.txt
    -rw-r--r-- 2 user group 1024 Jan 12 10:00 link_to_file
  • 第二列的 2 表示文件有两个硬链接。

3. 软链接与硬链接的区别

特性 软链接(Symbolic Link) 硬链接(Hard Link)
指向 文件路径 文件数据(inode)
跨文件系统 支持 不支持
目录支持 可以指向目录 不支持
目标文件删除 软链接失效 硬链接仍然有效
存储位置 创建一个新文件保存路径信息 与目标文件共享 inode
文件大小 软链接文件大小是路径字符串的长度 硬链接与原文件大小相同

4. 示例对比

创建软链接

1
2
ln -s /home/user/file.txt soft_link
ls -l soft_link

输出:

1
lrwxrwxrwx 1 user group 10 Jan 12 10:00 soft_link -> /home/user/file.txt

创建硬链接

1
2
ln /home/user/file.txt hard_link
ls -l

输出:

1
2
-rw-r--r-- 2 user group 1024 Jan 12 10:00 file.txt
-rw-r--r-- 2 user group 1024 Jan 12 10:00 hard_link

删除目标文件

  • 如果删除

    1
    /home/user/file.txt

    • 软链接soft_link 失效,无法访问。
    • 硬链接hard_link 可正常访问文件数据。

5. 应用场景

软链接适用场景

  • 快捷访问文件或目录。
  • 跨分区、跨文件系统链接。
  • 创建动态路径指向目标文件(路径会更新)。

硬链接适用场景

  • 实现文件备份。
  • 确保文件数据不会因误删而丢失(至少保留一个硬链接)。

总结

软链接和硬链接各有优缺点,具体使用哪种取决于应用场景。软链接更灵活,适用于跨文件系统或目录指向;硬链接更安全,适用于数据备份或文件共享。了解两者的特性和差异,有助于在 Linux 文件管理中高效使用这两种工具。

5.6 压缩与打包

5.7 Bash

5.8 管道指令

问:说一下命名管道和匿名管道的特点和区别?

在 Linux 和其他类 Unix 系统中,管道(Pipe)是一种进程间通信(IPC)机制,用于在两个进程之间传递数据。根据是否具备名称,管道分为 命名管道(Named Pipe,FIFO)匿名管道(Anonymous Pipe)。以下是两者的特点及区别:

1. 匿名管道

特点

  1. 无名称:匿名管道没有名字,仅存在于创建它的进程和相关进程之间。

  2. 父子进程之间通信:通常用于有亲缘关系的进程之间通信,例如父进程与子进程之间。

  3. 单向通信:数据流是单向的,从一端写入,另一端读取。

  4. 生命周期:当创建匿名管道的进程终止时,管道也随之销毁。

  5. 效率高:因为只在内存中存在,匿名管道通信效率较高。

创建与使用

  • 通过系统调用 pipe() 创建:

    1
    2
    int pipefd[2];
    pipe(pipefd); // 创建一个管道
  • pipefd[0] 用于读,pipefd[1] 用于写。

2. 命名管道(FIFO)

特点

  1. 有名称:命名管道以文件形式存在于文件系统中,可通过路径访问。

  2. 任意进程间通信:不要求进程之间存在亲缘关系,任何两个进程都可以通过命名管道通信。

  3. 单向或双向通信:默认单向通信,但可以通过多个管道实现双向通信。

  4. 持续存在:命名管道的文件在文件系统中持续存在,除非被显式删除。

  5. 易于调试:因为文件名可见,便于调试和使用。

创建与使用

  • 使用 mkfifo 命令创建命名管道:

    1
    mkfifo my_fifo
  • 使用程序操作命名管道:

    • 通过标准文件操作(如 openreadwrite)对管道进行读写。

3. 区别对比

特性 匿名管道 命名管道(FIFO)
是否具备名称 无名称,只能在创建时使用文件描述符访问 有名称,可以通过路径访问
通信进程 仅限有亲缘关系的进程间通信 任意进程之间都可通信
存在形式 仅存在于内存中 文件系统中的特殊文件
通信方向 单向通信 默认单向通信,可通过多个管道实现双向
生命周期 随进程终止而销毁 管道文件持续存在,需手动删除
创建方式 使用 pipe() 系统调用 使用 mkfifo 命令或 mkfifo() 函数
适用场景 父子进程或兄弟进程间的简单通信 需要长时间存在的进程间通信
性能 更高,因为只在内存中操作 稍低,因为涉及文件系统操作

4. 示例

匿名管道示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <unistd.h>
#include <stdio.h>

int main() {
int pipefd[2];
char buffer[100];

if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}

pid_t pid = fork();
if (pid == 0) {
// 子进程 - 写入管道
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello from child!", 18);
close(pipefd[1]);
} else {
// 父进程 - 从管道读取
close(pipefd[1]); // 关闭写端
read(pipefd[0], buffer, sizeof(buffer));
printf("Parent received: %s\n", buffer);
close(pipefd[0]);
}
return 0;
}

命名管道示例

创建命名管道

1
mkfifo my_fifo

写入数据

1
echo "Hello from writer!" > my_fifo

读取数据

1
cat my_fifo

5. 应用场景

匿名管道

  • 父子进程间的数据交换。
  • 简单任务中,效率更高的临时通信。

命名管道

  • 不同进程之间的数据传递。
  • 需要长时间运行的进程间通信。
  • 数据流处理(如日志记录、实时监控)。

总结

匿名管道适用于简单的、短期的父子进程通信,而命名管道适合复杂场景中的进程间通信。根据应用场景选择合适的管道方式,可以有效提高程序的效率和灵活性。

5.9 正则表达式

5.10 进程管理

进程管理相关,僵尸进程与孤儿进程,SIGCHLD 。

进程管理是操作系统中的重要部分,其中 僵尸进程孤儿进程 是需要关注的特殊情况。此外,信号 SIGCHLD 是处理子进程状态的重要工具。以下是详细解释:

1. 僵尸进程(Zombie Process)

定义

  • 僵尸进程是指子进程已经终止,但其退出状态尚未被父进程回收的进程。
  • 僵尸进程会在系统中占用一个进程表项,但不会占用内存或其他资源。

成因

  1. 子进程终止后,会发送一个 SIGCHLD 信号通知父进程。
  2. 父进程未及时调用 wait()waitpid() 回收子进程的退出状态。
  3. 系统无法清除子进程的进程表项,导致其变为僵尸状态。

危害

  • 如果父进程未回收大量僵尸进程,进程表会被占满,导致系统无法创建新的进程。

解决方法

  1. 父进程回收子进程:

    • 使用 wait()waitpid() 明确回收子进程的退出状态。
  2. 忽略 SIGCHLD 信号:

    • 父进程可以通过设置 SIGCHLD 信号的处理方式为 SIG_IGN,让系统自动回收子进程。

    • 示例代码:

      1
      signal(SIGCHLD, SIG_IGN);
  3. 编写守护进程:

    • 如果父进程不负责直接管理子进程,可以将僵尸进程交由 init 进程(PID=1)处理。

2. 孤儿进程(Orphan Process)

定义

  • 孤儿进程是指其父进程已经终止,但子进程仍在运行的进程。

处理机制

  • 孤儿进程会被操作系统自动托管,由 init 进程(PID=1)接管,并负责回收它的退出状态。
  • 孤儿进程不会对系统资源造成危害,因为操作系统会自动处理它。

3. SIGCHLD 信号

定义

  • SIGCHLD 是子进程状态发生变化时,向父进程发送的信号。
  • 子进程终止、暂停或恢复运行都会触发此信号。

父进程对 SIGCHLD 的处理

  1. 默认行为:默认情况下,SIGCHLD 信号会被忽略,但子进程的状态依然需要父进程显式回收。

  2. 自定义信号处理:

    • 父进程可以捕获 SIGCHLD 信号,并在处理函数中调用 wait()waitpid()

    • 示例代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      #include <signal.h>
      #include <sys/types.h>
      #include <sys/wait.h>
      #include <unistd.h>
      #include <stdio.h>

      void sigchld_handler(int signum) {
      int status;
      // 回收子进程
      while (waitpid(-1, &status, WNOHANG) > 0) {
      printf("Child process terminated\n");
      }
      }

      int main() {
      signal(SIGCHLD, sigchld_handler); // 注册 SIGCHLD 信号处理函数

      if (fork() == 0) {
      // 子进程
      printf("Child process running\n");
      sleep(2);
      return 0;
      } else {
      // 父进程
      printf("Parent process waiting\n");
      while (1) {
      sleep(1); // 模拟长时间运行
      }
      }
      }

4. 僵尸进程和孤儿进程的对比

特性 僵尸进程 孤儿进程
定义 子进程已终止,但未被父进程回收 父进程已终止,但子进程仍在运行
处理机制 需要父进程调用 wait()waitpid() 回收 自动由 init 进程接管,无需人工干预
危害 占用进程表项,可能导致系统无法创建新进程 不会造成危害
解决方法 父进程主动回收或忽略 SIGCHLD 信号 操作系统自动处理

5. 示例场景

僵尸进程示例

父进程未回收子进程的退出状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>
#include <stdio.h>

int main() {
if (fork() == 0) {
// 子进程
printf("Child process exiting\n");
return 0;
} else {
// 父进程
sleep(10); // 父进程没有及时回收子进程
printf("Parent process exiting\n");
}
return 0;
}

运行 ps 命令可以看到 Z 状态的僵尸进程。

孤儿进程示例

父进程终止后,子进程由 init 进程接管:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>
#include <stdio.h>

int main() {
if (fork() == 0) {
// 子进程
sleep(5);
printf("Orphan process running\n");
} else {
// 父进程
printf("Parent process exiting\n");
}
return 0;
}

通过 ps 查看子进程的 PPID,会发现它变为 1init 进程的 PID)。

总结

  • 僵尸进程需要主动回收,否则可能占用系统资源。
  • 孤儿进程由系统自动接管,不需人工干预。
  • 使用 SIGCHLD 信号和相关函数可以有效管理子进程的生命周期,避免资源浪费。

问:说一下Linux进程间通信的方式?

答:管道,信号量,消息队列,共享内存,套接字。

中断与系统调用的概念

中断系统调用 是操作系统中两种重要的机制,主要用于处理硬件事件和提供内核服务。以下是它们的详细概念和区别:

1. 中断(Interrupt)

定义

  • 中断 是一种机制,允许计算机硬件或软件通过向 CPU 发送信号打断当前的程序执行,并切换到特定的中断处理程序。
  • 主要用于响应外部事件(硬件中断)或处理异常情况(软件中断)。

分类

  1. 硬件中断:

    • 由外部硬件设备(如键盘、鼠标、网卡等)触发。
    • 示例:按下键盘、硬盘完成数据读写。
  2. 软件中断:

    • 由指令触发,通常是程序主动请求操作系统的服务。
  • 示例:使用 int 指令在 x86 系统中触发系统调用。

工作流程

  1. 中断发生:

    • 硬件设备或软件产生中断信号。
  2. CPU保存上下文:

    • 保存当前程序的状态(如寄存器值、程序计数器)。
  3. 调用中断处理程序:

    • 操作系统内核根据中断向量表跳转到对应的中断服务程序(ISR)。
  4. 处理完成并恢复:

    • 恢复之前保存的状态,继续执行被打断的程序。

特点

  • 异步性:中断可以随时发生,不受当前程序控制。
  • 优先级:中断有不同优先级,高优先级的中断会抢占低优先级的中断。
  • 硬件驱动:硬件设备依赖中断通知操作系统完成事件处理。

2. 系统调用(System Call)

定义

  • 系统调用是用户程序向操作系统内核请求服务的接口,是操作系统功能对外的唯一入口。
  • 用户态程序通过系统调用访问内核态资源,例如文件、进程、内存等。

常见分类

  1. 文件操作:

    • 打开/关闭文件:open()close()
    • 读写文件:read()write()
  2. 进程管理:

    • 创建/结束进程:fork()exit()
    • 等待进程:wait()
  3. 内存管理:

    • 分配/释放内存:mmap()munmap()
  4. 设备管理:

    • 设备读写:ioctl()read()write()
  5. 网络通信:

    • 套接字操作:socket()connect()bind()

工作流程

  1. 用户程序发起请求:

    • 调用系统提供的库函数(如 C 标准库中的 printf(),底层会调用 write())。
  2. 陷入内核态:

    • 用户态程序通过 trap 指令切换到内核态。
  3. 内核处理请求:

    • 根据系统调用号定位相应的内核函数,执行服务。
  4. 返回用户态:

    • 返回结果到用户态程序。

特点

  • 同步性:系统调用是用户程序主动发起的,请求完成后继续执行。
  • 受控访问:通过系统调用访问内核资源,保证安全性和隔离性。
  • 跨越权限边界:从用户态切换到内核态,执行需要特权的操作。

3. 中断与系统调用的区别

特性 中断 系统调用
触发方式 由硬件事件或特殊指令触发 用户程序主动调用
触发时机 异步发生,与程序执行无直接关系 同步发生,由程序显式发起
作用 响应硬件事件或异常 提供内核服务,用户态程序访问操作系统资源
执行环境 中断服务程序运行在内核态 系统调用切换到内核态后运行
优先级 通常优先级较高,会抢占当前任务 按调用顺序执行,无优先级干预
性能开销 较小,因为只处理特定硬件事件 较大,因为涉及用户态与内核态切换

4. 示例

中断示例

假设一个键盘输入触发中断:

  1. 用户按下键盘。
  2. 键盘控制器向 CPU 发出中断信号。
  3. CPU 暂停当前任务,调用对应的中断处理程序。
  4. 中断处理程序读取按键数据,并将其写入缓冲区。
  5. 恢复执行被打断的任务。

系统调用示例

用户程序调用 write() 写入数据:

  1. 用户程序调用 write(fd, buffer, size)
  2. 通过 trap 指令切换到内核态。
  3. 操作系统根据文件描述符 fd 找到对应文件,并将数据写入。
  4. 返回写入字节数到用户态程序。

5. 总结

概念 中断 系统调用
核心区别 响应硬件或异常事件,主要用于异步处理 用户态程序请求内核服务的接口,用于受控访问操作系统资源
执行路径 硬件/软件中断 → CPU → 中断向量表 → 中断处理程序 用户程序 → 系统调用库函数 → trap → 内核处理
典型场景 硬盘读写完成、网络请求到达、中断处理异常等 打开文件、读写文件、创建进程、分配内存等

通过区分 中断系统调用 的触发方式和作用场景,可以更好地理解操作系统对硬件和软件的管理机制。

问:用户态和内核态?为什么要有用户态和内核态?

通过系统调用将Linux整个体系分为用户态和内核态(或者说内核空间和用户空间)。那内核态到底是什么呢?其实从本质上说就是我们所说的内核,它是一种特殊的软件程序,特殊在哪儿呢?控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行

用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。内核必须提供一组通用的访问接口,这些接口就叫系统调用。

内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。

用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。

为什么要有用户态和内核态?由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。

用户态和内核态概念

用户态(User Mode)内核态(Kernel Mode) 是操作系统中两种不同的运行权限级别。它们的存在是为了保障系统的稳定性、安全性和资源隔离。

1. 用户态(User Mode)

定义

  • 用户态是程序运行在非特权级别下的一种状态。
  • 在用户态中,程序不能直接访问硬件资源或执行特权指令,必须通过系统调用请求操作系统内核的服务。

特点

  1. 受限的权限:

    • 用户态的程序只能访问受保护的虚拟地址空间,无法直接操作底层硬件。
  2. 安全性高:

    • 如果用户态程序崩溃,影响范围仅限于当前进程,不会导致系统崩溃。
  3. 主要用途:

    • 用于运行用户应用程序(如浏览器、文本编辑器)。

2. 内核态(Kernel Mode)

定义

  • 内核态是操作系统运行在特权级别下的一种状态。
  • 在内核态中,操作系统内核可以直接访问硬件资源,执行特权指令,以及管理系统资源。

特点

  1. 高权限:

    • 内核态程序可以直接操作内存、CPU寄存器以及外围设备。
  2. 潜在风险:

    • 内核态的错误可能导致整个系统崩溃。
  3. 主要用途:

    • 用于操作系统内核运行,处理硬件中断、执行系统调用等任务。

3. 用户态与内核态的区别

特性 用户态 内核态
权限级别 非特权级别 高特权级别
访问范围 受限,仅能访问用户空间内存 可访问所有硬件资源和内存区域
执行指令 无法执行特权指令 可执行特权指令
崩溃影响 程序崩溃不会影响系统运行 内核崩溃可能导致整个系统崩溃
用途 运行用户应用程序 运行操作系统内核和处理底层任务

4. 为什么需要用户态和内核态?

  1. 安全性
    • 将应用程序限制在用户态运行,防止其直接操作硬件资源或执行特权指令。
    • 通过权限分离,避免用户程序的错误影响操作系统核心的稳定性。
  2. 稳定性
    • 如果程序在用户态崩溃,操作系统内核仍能正常运行。
    • 系统可以通过隔离机制保护关键资源,避免资源竞争或恶意操作。
  3. 系统资源管理
    • 内核态负责管理和调度硬件资源(CPU、内存、硬盘等)。
    • 用户态通过系统调用请求资源,而不是直接控制资源,从而确保资源分配的公平性和高效性。
  4. 抽象和简化
    • 用户态程序通过系统调用访问底层功能,无需关心硬件细节。
    • 操作系统内核提供统一的接口,简化开发者对底层资源的访问和使用。

5. 用户态与内核态的切换

切换场景

  1. 用户态 → 内核态

    • 系统调用:

      • 用户程序请求操作系统服务,例如文件操作、内存分配。
    • 中断:

      • 硬件设备触发中断信号(如键盘输入、网络数据到达)。
    • 异常:

      • 用户程序执行非法指令或出现除零等异常情况。
  2. 内核态 → 用户态

    • 系统调用返回:

      • 操作系统完成请求服务后,将控制权交还给用户程序。
    • 进程调度:

      • 操作系统将 CPU 控制权交给下一个用户程序。

切换过程

  • 用户态切换到内核态的步骤:

    1. 保存当前用户态程序的状态(寄存器、程序计数器等)。
    2. 切换到内核态,进入内核指定的入口点。
    3. 执行内核代码,完成中断处理或系统调用。
  • 内核态切换回用户态的步骤:

    1. 恢复用户态程序的状态。
  1. 切换到用户态,继续执行用户程序。

切换开销

  • 用户态与内核态的切换需要保存和恢复上下文,涉及硬件操作和内存访问,存在一定的性能开销。
  • 为减少切换次数,部分系统调用(如 Linux 的 getpid())通过用户态实现部分逻辑。

6. 示例:用户态与内核态切换

系统调用示例

用户程序调用 read() 读取文件:

  1. 用户程序执行 read(),触发系统调用。
  2. 通过 trap 指令切换到内核态。
  3. 操作系统内核读取文件数据,返回结果。
  4. 内核态切换回用户态,返回结果到用户程序。

中断示例

键盘按下触发中断:

  1. 键盘控制器发出中断信号。
  2. CPU 响应中断,切换到内核态。
  3. 内核执行中断处理程序,将按键数据存入缓冲区。
  4. 中断处理完成后,切换回用户态程序。

7. 总结

  • 用户态和内核态 是操作系统实现安全性和稳定性的重要机制。
  • 通过 权限分离,用户程序无法直接操作硬件资源,有效防止错误或恶意操作的扩散。
  • 切换代价 是用户态与内核态设计需要权衡的关键因素,高效的切换机制对系统性能至关重要。