资源调度

一、CPU 调度的概念

在 Linux 系统中,CPU 调度是指操作系统决定在多个等待执行的进程或线程中,选择哪一个在 CPU 上运行的过程。因为系统中通常会同时存在多个进程竞争 CPU 资源,而 CPU 在某一时刻只能处理一个任务,所以需要合理的调度算法来分配 CPU 时间,以确保系统的高效运行、响应性以及公平性 。这就好比在一个繁忙的工厂里,有许多工人(进程)都需要使用同一台重要的机器(CPU),调度算法就是安排每个工人何时使用这台机器的规则。

(一)调度算法

先来先服务(FCFS, First-Come, First-Served):这是一种最简单的调度算法,按照进程进入就绪队列的先后顺序来分配 CPU。先进入的进程先执行,直到它完成任务或者因为等待某些资源而阻塞。例如,进程 A 先到达就绪队列,随后进程 B 到达。那么 A 会先获得 CPU 资源开始执行,B 需要等待 A 执行完毕或阻塞后才有机会运行 。这种算法的优点是实现简单、公平,缺点是如果有一个长进程先到达,后面的短进程可能需要等待很长时间,导致整体效率不高,平均等待时间较长 。

短作业优先(SJF, Shortest Job First):该算法优先调度预计执行时间最短的进程。它能有效减少平均等待时间,提高系统吞吐量 。例如,进程 A 预计执行时间为 10 秒,进程 B 预计执行时间为 2 秒。如果采用 SJF 算法,B 会先于 A 执行。但它的问题在于难以准确预估进程的执行时间,而且可能导致长进程长时间得不到执行机会,产生 “饥饿” 现象 。

优先级调度:为每个进程分配一个优先级,调度程序总是选择优先级最高的进程执行。优先级可以根据进程的类型(如系统进程优先级通常较高)、资源需求等因素确定 。比如,实时进程需要及时响应,会被赋予较高优先级。不过,如果高优先级进程不断产生,低优先级进程可能会长期得不到执行,同样出现 “饥饿” 问题 。

时间片轮转调度:将 CPU 的时间划分成固定长度的时间片,每个进程轮流在一个时间片内执行。当时间片用完后,即使进程尚未完成,也会被暂停,调度程序将 CPU 分配给下一个就绪进程 。例如,时间片设置为 100 毫秒,进程 A 在时间片内未执行完,就会被暂停,进程 B 接着执行。这种算法保证了每个进程都能在一定时间内获得 CPU 资源,响应性较好,常用于分时系统。但如果时间片设置过短,会导致进程上下文切换过于频繁,增加系统开销;时间片设置过长,又会退化为 FCFS 算法,影响响应性 。

多级反馈队列调度:综合了多种调度算法的优点,设置多个就绪队列,每个队列有不同的优先级和时间片。新进程首先进入最高优先级队列,按时间片轮转方式执行。如果在一个时间片内未完成,该进程会被移到下一级队列,优先级降低 。低优先级队列的时间片会逐渐增大。这种算法既能照顾短进程,又能保证长进程最终也能得到执行机会,在实际应用中表现良好 。

(二)Linux 的 CFS 调度器(完全公平调度器)

在现代 Linux 内核中,CFS(Completely Fair Scheduler)是主要的调度器。

设计理念:CFS 旨在为每个进程提供公平的 CPU 使用机会,避免进程 “饥饿” 现象。它摒弃了传统的优先级概念,而是基于 “时间记账” 的方式。每个进程都有一个虚拟运行时间(vruntime),这个时间反映了进程已经使用的 CPU 时间。CFS 总是选择虚拟运行时间最短的进程执行,这意味着那些之前获得 CPU 时间较少的进程会优先得到调度 。

实现方式:CFS 使用红黑树(一种自平衡二叉搜索树)来管理就绪进程队列。树中的节点就是就绪进程,按照虚拟运行时间从小到大排序。在调度时,直接选择红黑树最左边的节点(即虚拟运行时间最短的进程)运行 。当一个进程的时间片用完后,它会被重新插入到红黑树中合适的位置。同时,CFS 会根据系统中运行的进程数量动态调整时间片的大小,以保证公平性和系统效率 。

(三)影响 CPU 调度的因素

进程类型:不同类型的进程对 CPU 调度有不同的需求。例如,交互式进程(如用户正在操作的图形界面程序)需要快速响应,否则会让用户感觉系统卡顿。这类进程希望能优先获得 CPU 资源,以提供流畅的用户体验。而批处理进程(如一些后台数据处理任务)对响应时间要求不高,更注重系统的整体吞吐量,可以在系统空闲时执行 。

系统负载:系统中运行的进程数量和它们的资源需求会影响 CPU 调度。当系统负载较高,即有大量进程竞争 CPU 资源时,调度器需要更加精细地分配 CPU 时间,以平衡各个进程的需求。例如,在服务器上同时运行多个服务进程和大量用户请求处理进程时,调度器要确保关键服务的正常运行,同时尽量满足用户请求的响应 。

硬件资源:CPU 的核心数、缓存大小等硬件因素也会影响调度效果。多核心 CPU 可以同时处理多个进程,调度器需要合理分配进程到不同核心上,以充分利用硬件资源。缓存大小会影响进程访问数据的速度,如果频繁调度的进程数据经常能在缓存中命中,会提高系统性能,因此调度器可能会考虑将相关进程尽量调度到同一核心上,以利用缓存 。

(四)taskset 指令的CPU调度方法

taskset 是 Linux 系统中用于设置或查看进程或线程的 CPU 亲和性的工具。CPU 亲和性是指将进程或线程绑定到特定的 CPU 核心上运行,这样可以提高缓存命中率,减少 CPU 之间的上下文切换开销,从而提升系统性能。以下是 taskset 指令的常见使用方法:

1.查看进程的 CPU 亲和性

使用 -p 选项可以查看指定进程 ID(PID)的 CPU 亲和性掩码。CPU 亲和性掩码是一个二进制数,每一位代表一个 CPU 核心,1 表示该进程可以在对应的 CPU 核心上运行,0 表示不能。

taskset -p <PID>

要查看 PID 为 1234 的进程的 CPU 亲和性:

taskset -p 1

输出示例:

pid 1234's current affinity mask: f

这里的 f 是十六进制表示,转换为二进制是 1111,表示该进程可以在所有 4 个 CPU 核心上运行。

2.设置进程的 CPU 亲和性

使用 -p-c 选项可以将指定进程绑定到特定的 CPU 核心上。-c 选项后面可以跟一个或多个 CPU 核心编号,多个编号之间用逗号分隔。

taskset -p -c <CPU列> <PID>

例如,将 PID 为 1234 的进程绑定到 CPU 核心 02 上:

taskset -p -c 0,2 1234

3.启动新进程并指定 CPU 亲和性

如果要在启动新进程时就指定其 CPU 亲和性,可以不使用 -p 选项。

taskset -c <CPU列> <>

例如,启动一个 top 进程并将其绑定到 CPU 核心 1 上:

taskset -c 1 top

4.查看 PID 当前 CPU 位置

使用 /proc/<PID>/stat 文件

在 Linux 系统中,每个进程在 /proc 目录下都有一个以其 PID 命名的目录,其中的 stat 文件包含了进程的各种状态信息,包括当前运行的 CPU 核心编号。可以使用以下命令查看:

grep '^cpu' /proc/stat
cat /proc/<PID>/stat | cut -d ' ' -f 39

例如,要查看 PID 为 1234 的进程当前运行的 CPU 核心编号:

cat /proc/1234/stat | cut -d ' ' -f 39

输出的数字就是 CPU 核心编号(从 0 开始计数)。

5.使用 ps 命令

ps 命令可以结合 -o 选项来查看进程的 CPU 亲和性和当前运行的 CPU 核心编号。

ps -o pid,psr,args -p <PID>

例如,查看 PID 为 1234 的进程的相关信息:

ps -o pid,psr,args -p 1234

其中 psr 列显示的就是当前运行的 CPU 核心编号。

6.以下是一个完整的示例脚本,结合了 taskset 的使用和查看 PID 当前 CPU 位置的方法:

#!/bin/bash
 
# 启动一个 sleep 进程
sleep 100 &
PID=$!
 
# 查看进程初始的 CPU 亲和性
echo "进程 $PID 初始的 CPU 亲和性:"
taskset -p $PID
 
# 将进程绑定到 CPU 核心 0
echo "将进程 $PID 绑定到 CPU 核心 0"
taskset -p -c 0 $PID
 
# 查看进程绑定后的 CPU 亲和性
echo "进程 $PID 绑定后的 CPU 亲和性:"
taskset -p $PID
 
# 查看进程当前运行的 CPU 核心编号
echo "进程 $PID 当前运行的 CPU 核心编号:"
ps -o pid,psr,args -p $PID
 
# 等待进程结束
wait $PID   

将上述脚本保存为 cpu_affinity_example.sh,并赋予执行权限:

chmod +x cpu_affinity_example.sh
./cpu_affinity_example.sh

通过这个脚本,你可以清晰地看到 taskset 指令的使用和如何查看进程当前运行的 CPU 核心编号。

二、cgroup 的概念

cgroup(Control Group)是 Linux 内核提供的一种机制,用于对一组进程的资源使用进行限制和监控。它可以控制进程对 CPU、内存、磁盘 I/O、网络带宽等资源的使用情况,从而实现对系统资源的精细化管理,提高系统的稳定性和资源利用率。例如,在一个服务器上同时运行多个服务,使用 cgroup 可以确保每个服务不会过度占用资源,避免一个服务的异常导致整个系统性能下降。

作用

资源限制:可以为不同的进程组设置资源使用上限,如限制某个进程组使用的 CPU 时间、内存大小等。

优先级分配:通过调整资源分配的优先级,确保重要的进程组能优先获得所需资源。

资源统计:可以统计进程组对各种资源的使用情况,方便进行性能分析和监控。

进程控制:可以对进程组进行统一的管理,如暂停、恢复、终止等操作。

相关术语

任务(Task):即系统中的进程。

控制组(Control Group):一组按照某种标准划分的进程,一个进程可以属于多个控制组。

层级(Hierarchy):由多个控制组组成的树状结构,每个层级都关联一组子系统。

子系统(Subsystem):是一组对资源进行调度和限制的内核模块,如 CPU 子系统、内存子系统等。

cgroup 的使用示例

在 Linux 系统中,有两种主要的 cgroup 版本:cgroup v1cgroup v2。以下分别给出它们的使用示例。

cgroup v1 使用示例

在使用 cgroup v1 之前,需要确保系统已经挂载了相应的 cgroup 文件系统。通常,cgroup 文件系统会挂载在 /sys/fs/cgroup 目录下。

  1. 创建一个 CPU 控制组
# 创建一个名为 mycpu 的 CPU 控制组
sudo mkdir /sys/fs/cgroup/cpu/mycpu
# 查看新创建的控制组的属性文件
ls /sys/fs/cgroup/cpu/mycpu
  1. 限制 CPU 使用率

假设系统有 4 个 CPU 核心,要将某个进程组的 CPU 使用率限制在 25%(即相当于 1 个核心的使用率),可以通过修改 cpu.cfs_quota_uscpu.cfs_period_us 文件来实现。

# 设置 CPU 周期为 100000 微秒(即 100 毫秒)
echo 100000 | sudo tee /sys/fs/cgroup/cpu/mycpu/cpu.cfs_period_us
# 设置 CPU 配额为 25000 微秒(即 25 毫秒),表示在 100 毫秒的周期内,该控制组只能使用 25 毫秒的 CPU 时间
echo 25000 | sudo tee /sys/fs/cgroup/cpu/mycpu/cpu.cfs_quota_us
  1. 将进程加入控制组
# 启动一个 CPU 密集型进程
yes > /dev/null &
PID=$!
 
# 将进程的 PID 写入控制组的 tasks 文件,将该进程加入控制组
echo $PID | sudo tee /sys/fs/cgroup/cpu/mycpu/tasks
  1. 查看 CPU 使用情况

可以使用 tophtop 等工具查看进程的 CPU 使用情况,会发现该进程的 CPU 使用率被限制在 25% 左右。

  1. 结束进程并删除控制组
# 终止进程
kill $PID
# 删除控制组
sudo rmdir /sys/fs/cgroup/cpu/mycpu

cgroup v2 使用示例

cgroup v2cgroup 的新一代版本,提供了更简洁的接口和更强大的功能。

  1. 挂载 cgroup v2 文件系统

如果系统默认使用的是 cgroup v1,需要手动挂载 cgroup v2 文件系统。

sudo mount -t cgroup2 none /sys/fs/cgroup
  1. 创建一个控制组
# 创建一个名为 mygroup 的控制组
sudo mkdir /sys/fs/cgroup/mygroup
# 查看新创建的控制组的属性文件
ls /sys/fs/cgroup/mygroup
  1. 限制内存使用
# 设置内存使用上限为 100MB
echo 100M | sudo tee /sys/fs/cgroup/mygroup/memory.max
  1. 将进程加入控制组
# 启动一个内存密集型进程
python -c "x = 'a' * 1024 * 1024 * 200; while True: pass" &
PID=$!
# 将进程的 PID 写入控制组的 cgroup.procs 文件,将该进程加入控制组
echo $PID | sudo tee /sys/fs/cgroup/mygroup/cgroup.procs

由于设置了 100MB 的内存上限,该进程会因为内存不足而被终止。

  1. 删除控制组
# 终止进程
kill $PID
# 删除控制组
sudo rmdir /sys/fs/cgroup/mygroup

通过以上示例,可以看到 cgroup 能够方便地对进程的资源使用进行限制和管理。在实际应用中,可以根据具体需求创建不同的控制组,并设置相应的资源限制规则。