57、Linux CPU调度器深入解析

Linux CPU调度器深入解析

1. CPU调度器入口点总结

CPU调度器的 schedule() 函数在以下几种情况下会被调用:
- 显式阻塞调用 :当进程或线程进行显式的阻塞调用时,由于调用者需要等待某个事件,阻塞API最终会调用 schedule() 。当事件发生时,内核或使其进入睡眠状态的驱动程序会将其唤醒。
- 用户模式定时器管理抢占 :满足以下三个条件之一时会触发:
- 当前任务是否超过了其时间片(有效时间片),超过量达到 min_granularity_ns
- 新创建或刚唤醒的任务优先级是否高于当前正在执行内核代码路径的任务。
- CFS红黑树中是否有任务的虚拟运行时间(vruntime)低于当前任务,即当前任务是否不再是该树的最左叶子节点。
- 定时器(软中断)管理 :在定时器软中断上下文函数 scheduler_tick() 中检查是否需要设置 TIF_NEED_RESCHED 位,但不会在此处调用 schedule()
- 机会点检查 :在以下进程上下文的“机会点”检查 TIF_NEED_RESCHED 位是否设置:
- 系统调用返回路径。
- 中断返回路径。
- 其他调度激活点
- 任何对 schedule() 的调用。
- 任何可能导致调用 schedule() cond_resched*() 调用。
- 内核中从不可抢占模式切换到可抢占模式的情况。
- 任务唤醒 :当有任务被添加到运行队列时,判断该任务是否需要抢占当前任务。如果需要,设置 TIF_NEED_RESCHED ,并在下次机会点调用 schedule()
- 可抢占内核 :在任何内核抢占点(如调用 preempt_enable() ,如解锁自旋锁),包括系统调用返回和硬件中断返回时。
- 不可抢占内核 :在系统调用返回用户空间、中断返回用户空间、任何 cond_resched*() 调用或显式调用 schedule() 时。

以下是这些情况的总结表格:
| 触发情况 | 详细描述 |
| — | — |
| 显式阻塞调用 | 进程或线程进行阻塞调用,等待事件时调用 schedule() |
| 用户模式定时器管理抢占 | 满足三个特定条件之一时触发 |
| 定时器(软中断)管理 | 检查 TIF_NEED_RESCHED 位,但不调用 schedule() |
| 机会点检查 | 系统调用和中断返回路径检查 TIF_NEED_RESCHED 位 |
| 其他调度激活点 | 调用 schedule() cond_resched*() 或模式切换时 |
| 任务唤醒 | 根据内核类型在不同时机调用 schedule() |

2. 核心调度器代码简述

核心调度器代码由当前线程在进程上下文中运行,最终通过上下文切换将自己从CPU上移除。 __schedule() 函数中有两个重要的局部变量 prev next prev 指向当前任务, next 指向即将进行上下文切换并运行的下一个任务。以下是 __schedule() 函数的部分代码:

static void __sched notrace __schedule(unsigned int sched_mode)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    unsigned long prev_state;
    struct rq_flags rf;
    struct rq *rq;
    int cpu;
    cpu = smp_processor_id();
    rq = cpu_rq(cpu);
    prev = rq->curr;         << this is current ! >>
    [ ... ]
    next = pick_next_task(rq, prev, &rf);
        << Here we 'pick' the task to run next in an 'object- 
            oriented' manner, as discussed earlier in detail... >>
    clear_tsk_need_resched(prev);
    clear_preempt_need_resched();
    [ ... ]
    if (likely(prev != next)) {   << switching to another thread's likely… 
>>
        [ ... ]
        /* Also unlocks the rq: */
        rq = context_switch(rq, prev, next, &rf);
    [ ... ]
}
3. 上下文切换

上下文切换的工作是在切换到下一个任务之前,保存当前正在执行任务的状态。在Linux中, kernel/sched/core.c:context_switch() 内联函数完成上下文切换工作,由当前任务(即“前一个”任务)运行其代码。上下文切换主要分为两个与架构密切相关的阶段:
- 内存(MM)切换 :将特定架构的CPU寄存器指向 next 任务的内存描述符结构( struct mm_struct )。在x86[_64]架构中,该寄存器是CR3(控制寄存器3);在ARM(AArch32)架构中,是TTBR0(转换表基址寄存器0)。这样做是为了让MMU在 next 任务执行地址转换时引用其页表。
- 实际CPU切换 :通过保存 prev 任务的栈和CPU寄存器状态,并将 next 任务的栈和CPU寄存器状态恢复到处理器,在 switch_to() 宏中完成。这使得 next 任务能够从上次中断的位置继续执行。

以下是上下文切换的mermaid流程图:

graph TD;
    A[开始] --> B[内存(MM)切换];
    B --> C[实际CPU切换];
    C --> D[结束];
4. 处理器隔离

内核提供了一种方法,通过指定内核参数 isolcpus=[flag-list,]<cpu-list> 来隔离一组处理器,使其不受调度器类和SMP负载均衡的影响。 flag-list 默认值为 domain ,指定后, cpu-list 中的所有CPU核心将与“通用SMP平衡和调度算法”隔离,这对某些实时应用程序很有用。不过, isolcpus= 内核参数现已被弃用,建议使用更强大灵活的cpusets cgroups(v2)控制器。

5. CPU亲和性掩码的理解、查询和设置
5.1 CPU亲和性掩码概述

任务结构中有一个重要成员——CPU亲和性位掩码( cpumask_t *cpus_ptr ),它表示线程被允许运行的CPU核心。默认情况下,所有CPU亲和性掩码位都被设置,即线程可以在任何核心上运行。例如,在一个有8个CPU核心的系统中,每个线程的默认CPU亲和性位掩码为二进制 1111 1111 (十六进制 0xff )。

以下是一个8核系统中CPU亲和性位掩码的示例表格:
| CPU核心编号 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| — | — | — | — | — | — | — | — | — |
| 亲和性位 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |

此示例中,CPU位掩码值为 0x3f (二进制 0011 1111 ),表示该线程可以在CPU核心0到5上调度,但不能在核心6和7上运行。

5.2 显式设置CPU亲和性掩码的好处
  • 减少缓存失效 :确保线程始终在同一CPU核心上运行,可大大减少频繁的缓存失效和不愉快的缓存“跳动”。
  • 消除线程迁移成本 :有效消除核心之间的线程迁移成本。
  • CPU预留 :保证某个核心专门用于一个线程,确保其他线程不能在该核心上执行,这在一些对时间要求严格的实时系统中很有用。
5.3 查询和设置CPU亲和性掩码的代码实现

可以使用系统调用 sched_getaffinity() sched_setaffinity() 来查询和设置线程的CPU亲和性掩码。以下是示例代码:

#define _GNU_SOURCE
#include <sched.h>
int sched_getaffinity(pid_t pid, size_t cpusetsize,
                      cpu_set_t *mask);
int sched_setaffinity(pid_t pid, size_t cpusetsize,
                      const cpu_set_t *mask);

cpu_set_t 类型用于表示CPU亲和性位掩码,需要先使用 CPU_ZERO() 宏将其初始化为零。以下是查询和设置CPU亲和性掩码的具体代码:

// 查询CPU亲和性掩码
static int query_cpu_affinity(pid_t pid)
{
    cpu_set_t cpumask;
    CPU_ZERO(&cpumask);
    if (sched_getaffinity(pid, sizeof(cpu_set_t), &cpumask) < 0) {
        perror("sched_getaffinity() failed");
        return -1;
    }
    disp_cpumask(pid, &cpumask, numcores);
    return 0;
}

// 设置CPU亲和性掩码
static int set_cpu_affinity(pid_t pid, unsigned long bitmask)
{
    cpu_set_t cpumask;
    int i;
    printf("\nSetting CPU affinity mask for PID %d now...\n", pid);
    CPU_ZERO(&cpumask);
    for (i = 0; i < sizeof(unsigned long) * 8; i++) {
        if ((bitmask >> i) & 1)
            CPU_SET(i, &cpumask);
    }
    if (sched_setaffinity(pid, sizeof(cpu_set_t), &cpumask) < 0) {
        perror("sched_setaffinity() failed");
        return -1;
    }
    disp_cpumask(pid, &cpumask, numcores);
    return 0;
}

需要注意的是,任何人都可以查询任何任务的CPU亲和性掩码,但只有拥有任务所有权、具有root权限或 CAP_SYS_NICE 能力的用户才能设置它。

Linux CPU调度器深入解析

6. 线程调度策略和优先级的查询与设置

在Linux系统中,线程的调度策略和优先级是影响其执行顺序和资源分配的重要因素。以下是关于查询和设置线程调度策略及优先级的详细介绍。

6.1 相关系统调用

Linux提供了一系列系统调用用于查询和设置线程的调度策略和优先级,其中常用的有 sched_getscheduler sched_setscheduler

#include <sched.h>

// 获取线程的调度策略和优先级
int sched_getscheduler(pid_t pid);

// 设置线程的调度策略和优先级
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
  • sched_getscheduler :该函数用于获取指定线程(通过 pid 指定)的当前调度策略。
  • sched_setscheduler :该函数用于设置指定线程的调度策略和优先级。 policy 参数指定调度策略, param 参数是一个指向 sched_param 结构体的指针,该结构体包含线程的优先级信息。
6.2 调度策略和优先级的操作步骤

以下是查询和设置线程调度策略及优先级的具体操作步骤:

  1. 查询线程的调度策略和优先级
    • 调用 sched_getscheduler 函数获取线程的当前调度策略。
    • 根据返回的调度策略值,进一步处理和显示相关信息。
#include <stdio.h>
#include <sched.h>

int main() {
    pid_t pid = getpid(); // 获取当前线程的PID
    int policy = sched_getscheduler(pid);
    if (policy == -1) {
        perror("sched_getscheduler");
        return 1;
    }
    printf("Current scheduling policy: %d\n", policy);
    return 0;
}
  1. 设置线程的调度策略和优先级
    • 定义 sched_param 结构体,并设置所需的优先级。
    • 调用 sched_setscheduler 函数设置线程的调度策略和优先级。
#include <stdio.h>
#include <sched.h>

int main() {
    pid_t pid = getpid(); // 获取当前线程的PID
    struct sched_param param;
    param.sched_priority = 50; // 设置优先级为50
    int policy = SCHED_FIFO; // 设置调度策略为SCHED_FIFO

    if (sched_setscheduler(pid, policy, &param) == -1) {
        perror("sched_setscheduler");
        return 1;
    }
    printf("Scheduling policy and priority set successfully.\n");
    return 0;
}
7. cgroups简介

cgroups(Control Groups)是Linux内核提供的一种机制,用于限制和监控一组进程的资源使用情况,如CPU、内存、磁盘I/O等。通过cgroups,可以对不同的进程组进行资源分配和限制,从而提高系统的稳定性和性能。

7.1 cgroups的基本概念
  • 控制组(Control Group) :一组进程的集合,这些进程可以共享相同的资源限制和监控信息。
  • 子系统(Subsystem) :用于实现特定资源管理功能的模块,如CPU子系统、内存子系统等。
  • 层级(Hierarchy) :cgroups以层级结构组织,每个层级可以包含多个控制组,子控制组可以继承父控制组的资源限制。
7.2 cgroups的使用步骤

以下是使用cgroups进行资源限制的基本步骤:

  1. 创建cgroup层级
    • 使用 cgcreate 命令创建一个新的cgroup层级。
cgcreate -g cpu,memory:/mygroup

上述命令创建了一个名为 mygroup 的cgroup层级,同时启用了CPU和内存子系统。

  1. 将进程添加到cgroup
    • 使用 cgclassify 命令将指定进程添加到创建的cgroup中。
cgclassify -g cpu,memory:/mygroup <pid>

其中 <pid> 是要添加到cgroup的进程的PID。

  1. 设置资源限制
    • 通过修改cgroup目录下的相应文件来设置资源限制。例如,设置CPU使用率限制:
echo 50000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us

上述命令将 mygroup 控制组的CPU使用率限制为50%(假设CPU周期为100000微秒)。

8. 运行Linux作为实时操作系统(RTOS)简介

在某些对时间要求严格的应用场景中,需要将Linux系统作为实时操作系统(RTOS)来运行,以确保任务能够在规定的时间内完成。以下是关于将Linux作为RTOS运行的一些关键要点。

8.1 实时调度策略

Linux提供了几种实时调度策略,如 SCHED_FIFO SCHED_RR ,用于满足实时任务的需求。

  • SCHED_FIFO :先进先出调度策略,具有相同优先级的任务按照到达顺序依次执行,高优先级任务可以抢占低优先级任务。
  • SCHED_RR :轮转调度策略,具有相同优先级的任务在时间片内轮流执行,时间片用完后,任务会被放到队列尾部。
8.2 实时内核配置

为了使Linux系统具备更好的实时性能,需要进行一些内核配置。以下是一些常见的配置选项:

  • 启用实时补丁 :使用PREEMPT_RT补丁可以将Linux内核转换为实时内核,增强其抢占能力。
  • 调整调度参数 :通过修改内核参数,如 kernel.sched_rt_runtime_us kernel.sched_rt_period_us ,可以调整实时任务的时间片和周期。
9. 杂项调度相关主题

除了上述内容外,还有一些其他的调度相关主题值得关注。

9.1 调度器的负载均衡

在多CPU系统中,调度器的负载均衡功能非常重要,它可以确保各个CPU核心的负载均匀分布。调度器会根据任务的优先级、CPU使用率等因素,动态地将任务分配到不同的CPU核心上。

9.2 调度器的性能优化

为了提高调度器的性能,可以采取以下措施:

  • 减少调度开销 :通过优化调度算法和数据结构,减少调度过程中的时间和空间开销。
  • 提高缓存命中率 :合理安排任务的执行顺序,提高CPU缓存的命中率,减少缓存失效带来的性能损失。
10. 总结

本文深入探讨了Linux CPU调度器的多个方面,包括CPU调度器的入口点、核心调度器代码、上下文切换、处理器隔离、CPU亲和性掩码的查询和设置、线程调度策略和优先级的操作、cgroups的使用、将Linux作为RTOS运行以及杂项调度相关主题。通过对这些内容的学习,我们可以更好地理解Linux系统的调度机制,优化系统性能,满足不同应用场景的需求。

在实际应用中,我们可以根据具体需求选择合适的调度策略和配置,以提高系统的稳定性、实时性和性能。同时,不断关注Linux内核的发展和更新,学习新的调度技术和优化方法,将有助于我们更好地应对各种挑战。

以下是一个总结表格,概括了本文的主要内容:
| 主题 | 主要内容 |
| — | — |
| CPU调度器入口点 | 包括显式阻塞调用、用户模式定时器管理抢占等多种情况 |
| 核心调度器代码 | __schedule() 函数的工作原理 |
| 上下文切换 | 内存切换和实际CPU切换两个阶段 |
| 处理器隔离 | 使用 isolcpus 参数或cpusets cgroups控制器 |
| CPU亲和性掩码 | 查询和设置线程的CPU亲和性掩码 |
| 线程调度策略和优先级 | 使用系统调用进行查询和设置 |
| cgroups | 限制和监控进程的资源使用 |
| 运行Linux作为RTOS | 实时调度策略和内核配置 |
| 杂项调度相关主题 | 负载均衡和性能优化 |

希望本文对您理解Linux CPU调度器有所帮助,如有任何疑问或建议,请随时留言。

标题中提及的“BOE-B2-154-240-JD9851-Gamma2.2_190903.rar”标识了一款由京东方公司生产的液晶显示单元,属于B2产品线,物理规格为154毫米乘以240毫米,适配于JD9851型号设备,并采用Gamma2.2标准进行色彩校正,文档生成日期为2019年9月3日。该压缩文件内包含的代码资源主要涉及液晶模块的底层控制程序,采用C/C++语言编写,用于管理显示屏的基础运行功能。 液晶模块驱动作为嵌入式系统的核心软件组成部分,承担着直接操控显示硬件的任务,其关键作用在于通过寄存器读写机制来调整屏幕的各项视觉参数,包括亮度、对比度及色彩表现,同时负责屏幕的启动与关闭流程。在C/C++环境下开发此类驱动需掌握若干关键技术要素: 首先,硬件寄存器的访问依赖于输入输出操作,常借助内存映射技术实现,例如在Linux平台使用`mmap()`函数将寄存器地址映射至用户内存空间,进而通过指针进行直接操控。 其次,驱动需处理可能产生的中断信号,如帧缓冲区更新完成事件,因此需注册相应的中断服务例程以实时响应硬件事件。 第三,为确保多线程或进程环境下共享资源(如寄存器)的安全访问,必须引入互斥锁、信号量等同步机制来避免数据竞争。 第四,在基于设备树的嵌入式Linux系统中,驱动需依据设备树节点中定义的硬件配置信息完成初始化与参数设置。 第五,帧缓冲区的管理至关重要,驱动需维护该内存区域,保证图像数据准确写入并及时刷新至显示面板。 第六,为优化能耗,驱动应集成电源管理功能,通过寄存器控制实现屏幕的休眠与唤醒状态切换。 第七,针对不同显示设备支持的色彩格式差异,驱动可能需执行色彩空间转换运算以适配目标设备的色彩输出要求。 第八,驱动开发需熟悉液晶显示控制器与主处理器间的通信接口协议,如SPI、I2C或LVDS等串行或并行传输标准。 最后,完成代码编写后需进行系统化验证,包括基础显示功能测试、性能评估及异常处理能力检验,确保驱动稳定可靠。 该源代码集合为深入理解液晶显示控制原理及底层驱动开发实践提供了重要参考,通过剖析代码结构可掌握硬件驱动设计的具体方法与技术细节。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值