61、Linux 操作系统 CPU 调度与内核同步知识解析

Linux 操作系统 CPU 调度与内核同步知识解析

1. cgroups v2 与 CPU 资源分配

在 Linux 系统中,cgroups v2 是一个强大且实用的内核特性,它已深度嵌入到 Linux 内核之中。当为进程分配 CPU 资源时,若为所有进程赋予相同权重,它们将获得相等的 CPU 资源份额,此时具体分配的值并非关键;而当改变进程间的权重时,这一分配就变得重要起来。

系统d 有助于将 cgroups 自动集成到现代发行版、服务器甚至嵌入式系统中,它能自动且动态地创建和维护各种 cgroups。我们可以利用 cgroups v2 的 CPU 控制器,通过系统d 单元文件或手动方式,按需求为子组中的进程分配 CPU 带宽或利用率。例如,可运行 ch11/cgroups/cpu_constrain/cpu_manual/cgv2_cpu_ctrl.sh Bash 脚本,为进程分配不同的 CPU 带宽利用率,并查看之前 cgroupsv2_explore 脚本的输出结果,以此研究资源分配情况。

2. 实时操作系统(RTOS)概述

主线或普通的 Linux 操作系统并非实时操作系统(RTOS),而是通用操作系统(GPOS),如 Windows、macOS 和 Unix 一样。在 RTOS 中,硬实时特性要求软件不仅要得出正确结果,还必须保证在规定的期限内完成任务。

从实时特性角度可对操作系统进行大致分类,最左侧为非实时操作系统,最右侧为实时操作系统。普通 Linux 操作系统虽不是 RTOS,但在性能方面表现出色,可归为软实时操作系统,能在大多数情况下以“尽力而为”的方式满足期限要求,有时甚至能达到 99.999% 的期限满足率。然而,在一些真正需要硬实时保证的领域,如军事行动、交通运输、机器人技术、电信、工厂自动化、证券交易所和医疗电子等,普通 Linux 作为 GPOS 就无法满足需求。

实时系统的关键特性之一是确定性,即系统对外部事件的响应时间不一定要求极快,重要的是系统可靠且可预测,始终以一致的方式工作并保证期限的满足。响应时间与所需时间的偏差称为抖动,RTOS 致力于将抖动控制在极小甚至可忽略的范围内,而在 GPOS 中,抖动可能会有极大的波动。此外,RT 系统的另一个目标是将最大或最坏情况下的延迟降低到可接受的水平,尽管其最小和平均延迟可能比非 RT 系统更差。

3. 将 Linux 转换为 RTOS

Thomas Gleixner 及其团队长期致力于将普通的非实时 Linux 内核转换为硬实时操作系统。自 2006 年 9 月发布的 2.6.18 内核起,就有可将 Linux 内核转换为 RTOS 的外部补丁。这些补丁可在 https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/ 找到。该项目最初名为“可抢占实时”(PREEMPT_RT),2015 年 10 月内核版本 4.1 起,Linux 基金会接管并将其更名为实时 Linux(RTL)协作项目。

要将主线 6.1 LTS 内核转换为 RTL 内核,可参考 RTL Wiki 网站的教程 ,具体步骤如下:
1. 下载 RTL 补丁集 :从 https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/ 下载适用于给定稳定内核的 RTL 补丁集。补丁集通常有两种形式:一种是单个补丁文件 patch-<kver>-rt<xy>.tar.xz ;另一种是补丁系列文件 patches-<kver>-rt<xy>.tar.xz ,解压后补丁文件位于 patches/ 文件夹中。
2. 提取补丁并应用到内核源码树 :将下载的补丁提取并应用到内核源码树中。
3. 配置内核 :在 make menuconfig 界面中,导航至 General Setup | Preemption Model 。若已下载并应用 RTL 补丁,应会看到 4 个选项(通常为 3 个),选择第 4 个“Fully Preemptible Kernel (Real-Time)”( CONFIG_PREEMPT_RT=y )。若未看到该选项,需先开启 General Setup | Configure standard kernel features (expert users)
4. 编译内核 :按照常规方式编译内核,编译完成后,在 x86_64 系统上执行 sudo make modules_install && sudo make install ,然后重启系统。从引导加载程序菜单中选择新的 RTL 内核。
5. 验证实时内核 :在 shell 中执行 uname -a 命令,若输出包含 PREEMPT_RT 字样,且 /sys/kernel/realtime 文件的值为 1,则表明已成功运行 RTL 内核。

$ uname -a
Linux fedora 6.1.46-rt14-rc1 #1 SMP PREEMPT_RT [ ... ] x86_64 GNU/Linux
$ cat /sys/kernel/realtime 
1
4. 内核调度相关的其他主题
  • 内核例程 :有几个内核例程可用于判断任务是否为实时任务,这些例程位于 include/linux/sched/rt.h 头文件中,可在模块中直接使用。
    • rt_prio() :根据传入的优先级参数,返回一个布尔值,指示该任务是否为实时任务。
    • rt_task() :根据任务的优先级值和任务结构指针,返回一个布尔值,指示该任务是否为实时任务,它是 rt_prio() 的封装。
    • task_is_realtime() :根据任务的调度策略和任务结构指针,返回一个布尔值,指示该任务是否为实时任务。
  • ghOSt OS :随着硬件和软件技术的发展,新的技术领域不断涌现,云工作负载也有特殊需求,现有操作系统调度器面临巨大压力。Google 提出了 ghOSt 系统,旨在为 Linux 调度提供快速灵活的用户空间委托,以满足这些需求。它由内核和用户空间代码组成,内核代码相对稳定,用户空间部分负责策略决策,且以开源形式发布。目前,ghOSt 系统仍处于实验开发阶段。
5. 内核同步简介

在多线程环境编程时,当两个或多个线程可能对共享可写数据项进行操作时,就需要进行同步。若在访问共享数据时缺乏同步或互斥机制,就会出现数据竞争,导致结果不可预测。纯代码本身不存在问题,因为其权限为读 + 执行(r - x),多个 CPU 核心同时读取和执行代码是安全且有益的,可提高吞吐量。但一旦涉及共享可写数据,就必须格外小心,这也是多线程和并发编程复杂的原因。

由于 Linux 内核及其相关驱动和模块是复杂的软件,关于并发及其通过同步进行控制的讨论十分广泛。为方便起见,我们将内核同步这一大型主题分为两部分进行介绍,本部分将涵盖以下内容:
- 临界区、独占执行和原子性
- Linux 内核中的并发问题
- 何时使用互斥锁或自旋锁
- 互斥锁的使用
- 自旋锁的使用
- 锁与中断
- 锁的常见错误和使用准则

综上所述,通过对 CPU 调度和内核同步相关知识的学习,我们能更好地理解 Linux 操作系统的运行机制,并在实际开发中更有效地利用这些特性。

Linux 操作系统 CPU 调度与内核同步知识解析

6. 临界区、独占执行和原子性

在多线程或多进程环境中,临界区是指访问共享资源的代码段。为了避免数据竞争,在同一时间只能有一个线程或进程进入临界区,这就是独占执行。原子性则是指一个操作在执行过程中不会被其他操作中断,要么全部执行完成,要么完全不执行。

例如,在对一个共享变量进行自增操作时,如果这个操作不是原子的,可能会出现多个线程同时读取和修改该变量的情况,导致结果错误。为了保证操作的原子性,可以使用原子操作函数,这些函数在硬件层面保证操作的不可分割性。

7. Linux 内核中的并发问题

Linux 内核是一个高度并发的环境,多个进程、线程和中断处理程序可能同时访问共享资源。常见的并发问题包括数据竞争、死锁和活锁。
- 数据竞争 :如前面所述,当多个线程或进程同时访问共享可写数据且缺乏同步机制时,就会发生数据竞争。
- 死锁 :死锁是指两个或多个进程或线程相互等待对方释放资源,从而导致所有进程或线程都无法继续执行的情况。例如,进程 A 持有资源 X 并请求资源 Y,而进程 B 持有资源 Y 并请求资源 X,此时就会发生死锁。
- 活锁 :活锁是指进程或线程不断地尝试执行某个操作,但由于某些条件始终不满足,导致操作无法完成,从而陷入无限循环。

为了避免这些并发问题,需要使用合适的同步机制。

8. 互斥锁和自旋锁的选择

在 Linux 内核中,互斥锁(mutex)和自旋锁(spinlock)是两种常用的同步机制,它们适用于不同的场景。
| 锁类型 | 适用场景 | 特点 |
| ---- | ---- | ---- |
| 互斥锁 | 当临界区执行时间较长,且可能会导致进程睡眠时使用。例如,在进行磁盘 I/O 操作时,使用互斥锁可以避免浪费 CPU 资源。 | 当锁被占用时,请求锁的进程会进入睡眠状态,直到锁被释放。 |
| 自旋锁 | 当临界区执行时间较短,且不希望进程睡眠时使用。例如,在对内核数据结构进行简单的修改时,使用自旋锁可以快速获取锁。 | 当锁被占用时,请求锁的进程会不断地自旋等待,直到锁被释放。 |

9. 使用互斥锁

互斥锁的使用步骤如下:
1. 初始化互斥锁 :使用 mutex_init() 函数初始化互斥锁。
2. 获取互斥锁 :在进入临界区之前,使用 mutex_lock() 函数获取互斥锁。如果锁已被其他进程占用,当前进程会进入睡眠状态。
3. 释放互斥锁 :在离开临界区之后,使用 mutex_unlock() 函数释放互斥锁,唤醒等待该锁的进程。

以下是一个简单的使用互斥锁的示例:

#include <linux/mutex.h>

struct mutex my_mutex;

// 初始化互斥锁
mutex_init(&my_mutex);

// 获取互斥锁
mutex_lock(&my_mutex);

// 临界区代码
// ...

// 释放互斥锁
mutex_unlock(&my_mutex);
10. 使用自旋锁

自旋锁的使用步骤如下:
1. 初始化自旋锁 :使用 spin_lock_init() 函数初始化自旋锁。
2. 获取自旋锁 :在进入临界区之前,使用 spin_lock() 函数获取自旋锁。如果锁已被其他进程占用,当前进程会不断自旋等待。
3. 释放自旋锁 :在离开临界区之后,使用 spin_unlock() 函数释放自旋锁。

以下是一个简单的使用自旋锁的示例:

#include <linux/spinlock.h>

spinlock_t my_spinlock;

// 初始化自旋锁
spin_lock_init(&my_spinlock);

// 获取自旋锁
spin_lock(&my_spinlock);

// 临界区代码
// ...

// 释放自旋锁
spin_unlock(&my_spinlock);
11. 锁与中断

在使用锁时,需要考虑中断的影响。如果在持有锁的过程中发生中断,而中断处理程序也尝试获取该锁,就会导致死锁。为了避免这种情况,可以使用以下方法:
- 禁止中断 :在获取锁之前禁止中断,在释放锁之后再恢复中断。可以使用 spin_lock_irqsave() spin_unlock_irqrestore() 函数来实现。

#include <linux/spinlock.h>

spinlock_t my_spinlock;
unsigned long flags;

// 初始化自旋锁
spin_lock_init(&my_spinlock);

// 获取自旋锁并禁止中断
spin_lock_irqsave(&my_spinlock, flags);

// 临界区代码
// ...

// 释放自旋锁并恢复中断
spin_unlock_irqrestore(&my_spinlock, flags);
  • 使用中断安全的锁 :有些锁是专门为中断处理程序设计的,如 spin_lock_bh() spin_unlock_bh() 函数,它们可以在禁止软中断的情况下使用。
12. 锁的常见错误和使用准则

在使用锁时,需要注意以下常见错误和使用准则:
- 避免死锁 :确保在获取锁的顺序上保持一致,避免循环等待。
- 减少锁的持有时间 :尽量缩短临界区的执行时间,减少锁的竞争。
- 避免在中断处理程序中使用可能导致睡眠的锁 :中断处理程序应该尽快执行完毕,避免使用互斥锁等可能导致睡眠的锁。
- 检查锁的状态 :在使用锁之前,应该检查锁的状态,避免重复获取或释放锁。

通过遵循这些准则,可以有效地避免锁的使用错误,提高系统的稳定性和性能。

综上所述,内核同步是 Linux 操作系统中一个重要的主题,通过合理使用互斥锁、自旋锁等同步机制,可以避免并发问题,保证系统的正确性和稳定性。在实际开发中,需要根据具体的场景选择合适的同步机制,并遵循相关的使用准则。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值