闲聊一下CPU时序和现代操作系统二三事

时分系统和Linux

首先我们补习一下时分系统,时分系统是一个非常重要的操作系统概念,它最大限度地提高了运算机的利用率,是实现多道程序并发执行的重要手段。 我们日常工作用到的Linux系统 内核也采用了时分系统的思想,主要体现在以下几个方面:

时间片:

Linux 使用时间片机制对 CPU 进行时间分割,每个进程只能执行一个时间片的时间,然后交出 CPU 给其他进程运行。这实现了 CPU 时间的共享与公平分配。

上下文切换:

当时间片用完或进程主动放弃 CPU 时,会进行上下文切换,保存当前进程上下文并恢复下一个进程上下文。这使得 CPU 可以高效地在不同进程间切换。

进程调度:

Linux 使用 CFS 调度器根据每个进程的时间片选择最适合运行的进程,这是时分系统思想的体现。不同的调度策略可以实现不同的时分效果。

中断机制:

Linux 使用中断机制实现对时间的管理与调度。时钟中断可以在时间片用完时通知内核进行上下文切换与调度。这为时分系统提供了动力基础。

时钟事件:

Linux 基于时分系统管理各种时间事件,如定时器、睡眠唤醒等。这需要内核根据时钟中断来进行管理与调度。

除此之外,时分系统的思想在 Linux 中还体现在:

  1. 多道程序设计 Linux 支持多道程序并发运行,这也依赖于时分系统实现的 CPU 时间共享机制。
  2. 实时性 通过设置实时调度策略和对中断处理的优化,Linux 可以提供较好的实时响应性能。这也需要时分系统的支持。
  3. 睡眠唤醒 进程可以主动睡眠释放 CPU,这需要时分系统在其唤醒后重新调度其 CPU 时间。
  4. 同步机制 Linux 提供多种同步机制,这都需要时分系统来实现进程之间的协调与调度。

时分系统是 Linux 实现多道程序、并发执行、实时响应、时间管理等功能的基础。它使 Linux 能够充分利用 CPU 资源,实现高效率与公平的调度。时分系统的思想贯穿 Linux 内核的方方面面,是理解 Linux 调度与实现并发执行的重要概念。

聊一下Linux的时序

接下来我们需要提前了解几个概念:

Some Words

Jiffies

有更多兴趣的可以看看《 内核时钟问题

jiffies是一个非常重要的Linux内核变量,它代表了自系统启动以来的时间戳,以时钟中断的个数来表示。它有以下特点:

  1. jiffies以时钟中断的个数来衡量时间,所以它的精度由时钟中断频率决定。一般的1000HZ时钟中断,jiffies的精度为1ms。
  2. jiffies是一个无符号的长整型数值,会随着时钟中断不断累加。所以它的取值范围决定了Linux可以持续运行的最大时间。
  3. jiffies的值在溢出后会重新从0开始累加。所以它只能用于衡量从某个时刻起的较短时间,不能直接用于表示系统的绝对时间。
  4. 内核中的很多时间相关参数都使用jiffies作为单位,如时间片的大小、时钟中断的间隔等。这使得Linux可以根据不同的时钟源进行 Migration。
  5. jiffies可以用于比较两个时间戳之间的时间差,判断某个事件是否超时等。但在比较绝对时间时要特别注意jiffies的溢出问题。
  6. 可以通过jiffies的变化速度来粗略判断Linux内核的负载情况。jiffies变化越快,说明时钟中断越频繁,系统负载可能越重。
  7. 在用户空间,可以通过/proc/uptime文件来获取以秒为单位的系统启动时间与当前jiffies值。这可以用于计算系统的绝对时间等。

所以,jiffies作为Linux内核中的时间戳变量,具有以下重要意义:

  1. 它使Linux的时间单位由具体的时钟源独立,可以根据不同的时钟进行Migration。
  2. 它用于衡量较短的时间区间,判断超时等,但由于可能的溢出,不适用于表示绝对时间。
  3. 它的变化情况可以反映系统的负载情况,用于粗略判断系统性能。
  4. 通过它可以在内核与用户空间translating不同时间单位,如HZ与秒。
  5. 许多内核定时与时序相关的参数都基于jiffies,所以它是Linux时间管理的基础。

理解jiffies的含义与作用,可以帮助我们更深入理解Linux内核的时间管理机制。它使Linux的时间单位由硬件时钟源独立,是Linux灵活管理时间的基石。

CPU时间片

CPU时间片是操作系统为实现CPU调度而引入的一种机制。它的主要作用是:

  1. 实现CPU资源的公平共享,通过定期中断当前运行进程,让其他进程也有机会运行,避免优先级高的进程独占CPU。
  2. 避免任何一个进程长时间占用CPU,影响其他进程运行与系统的响应速度。
  3. 为CPU调度器提供时机,定期重新评估进程优先级,选择最适宜运行的进程。

CPU时间片的原理是:

  1. 操作系统根据时钟中断来对CPU的使用进行隔离与限制。当时间片用完时,会暂停当前运行进程,让CPU调度器选择其他进程运行。
  2. CFS调度器会根据进程的优先级与其他因素为每个进程分配一个时间片,决定其可以运行的时间。
  3. 进程运行时会消耗其时间片,当时间片用完时会被调度出CPU,腾出时间让其他进程运行。
  4. 进程如果在时间片用完前主动放弃CPU(如IO阻塞),其剩余时间片会被保存,在下次获得CPU时继续使用。
  5. 实时进程可以设置固定的时间片,不受CFS调度器影响,以保证其实时响应需求。

CPU时间片的大小(时钟中断频率)直接影响着调度的频率与公平性。时间片较小,可以增强公平性与响应速度;但也会增加上下文切换开销。时间片的设置需要平衡公平性与效率。

在Linux内核中,时钟中断频率为1000HZ,时间片默认为1ms。CFS调度器会根据各进程的漂移值动态调整时间片大小,但不会超过默认最大时间片。这可以很好地平衡调度的公平性与开销。

CPU队列

CPU队列是Linux内核用于管理可运行进程( TASK_RUNNING状态)的一种数据结构。它主要有以下作用:

  1. 临时保存可运行但未被当前CPU选择的进程,等待下次调度。
  2. 根据进程的优先级、调度策略对进程进行排序,为CPU调度提供候选进程。
  3. 实现公平的CPU资源共享,防止优先级高的进程独占CPU。

Linux内核的CPU队列主要有以下几种:

  1. 运行队列(runqueue):每CPU一个,保存正在该CPU上运行或准备运行的进程。CFS调度器直接从该队列中选择进程调度运行。
  2. 备选队列(backup queue):全局只有一个,保存从其他CPU上被推出的进程,等待重新调度。主要防止优先级高的进程被starvation。
  3. 过期队列(expired queue):全局只有一个,保存时间片用完但优先级较高未立即调度出CPU的进程。等待下次调度考虑。
  4. 唤醒队列(wakeup queue):全局只有一个,保存从睡眠状态被唤醒的进程,等待下次调度。

这些CPU队列间的数据流转如下:

新创建的进程 –> 唤醒队列 –> 备选队列 –> 过期队列 –> 运行队列

处于RUNNING状态的进程会被添加到各自CPU的运行队列等待调度运行;如果被调度出CPU会进入备选队列或过期队列;从睡眠唤醒会进入唤醒队列。这些队列围绕 RUNNABLE和RUNNING两种状态对进程进行管理与排序。

CFS调度器会从运行队列和备选队列选择进程进行调度。过期队列和唤醒队列的进程也会在适当情况下被提前调度运行。这些队列的使用与进程之间的流转,确保了每个CPU资源可以充分并公平地被需要使用的进程利用,实现了Linux的时间共享调度策略。

SO?

在Linux内核中,CPU时间片用于实现CPU调度中的时间共享,它的处理逻辑如下:

  1. 内核定期根据时钟中断来隔离进程或线程与CPU的执行,这就是CPU上下文切换,目的是为了重新选择其他进程或线程运行。
  2. 时钟中断频率决定了时间片的大小,时钟越快,时间片越小,CPU调度越频繁。早期通过PIT提供100HZ(10ms)的时钟中断,现代内核主要使用HPET提供的1000HZ(1ms)中断。
  3. 时间片的分配由CFS调度器决定,原则上每个进程获得的时间片大小应该与它的优先级和漂移值成正比。CFS会实时计算每个进程的vruntime来判断其获得的时间片。
  4. 内核通过托管进程上下文来记录进程运行的时间片使用情况,包括:时间片大小、已经使用的时间片、是否在运行等,这些信息保存在task_struct结构体中。
  5. 当进程获得CPU执行资源时,会从time_slice中减去使用的时间片,如果time_slice用完,则意味着时间片用完,应进行调度并重新选择其他进程运行。
  6. 如果在时间片用完之前,进程主动放弃CPU(如睡眠、IO等),则剩余的时间片会保留,在下次获得CPU执行时继续使用。但切换超过一定次数后,原有时间片会作废,从新分配。
  7. 内核会根据时钟中断周期不断检查正在运行的进程时间片使用情况,一旦时间片用完,就会将该进程置为可调度状态,并选择其他进程运行。
  8. 针对实时进程,内核可以设置静态的时间片,让其不受CFS调度器的管理,获得定期的CPU执行时间。这可以满足实时进程的需求。

所以,在Linux内核的CPU调度中,时间片起着非常重要的作用。它可以实现公平的CPU共享,满足不同进程的调度需求。内核通过时钟中断、CFS调度器和进程上下文来实现时间片的分配、管理与使用检查,以控制每个CPU运算周期中各个进程获得的执行时间,实现CPU资源的分配与调度。

那时钟中断频率和时间片有什么关系和区别?

时钟中断频率和时间片是两个相关但不同的概念:

  1. 时钟中断频率决定了时钟中断的次数,即CPU上下文切换的频率。时钟中断越频繁,上下文切换越频繁,时间片就越小。
  2. 时间片决定了每个进程在获得CPU时间后可以占用CPU的最大时间长度。时间片的大小由操作系统根据时钟中断频率与调度策略来确定。
  3. 时钟中断是一种事件,用于通知操作系统隔离当前运行的进程,选择其他进程运行。它的频率反映了这个隔离动作的次数。
  4. 时间片是一种资源分配机制,它决定了每个进程获得CPU时间后可以独占使用CPU的时间长短。

它们的关系是:

  1. 时钟中断频率越高,时间片就越小。比如100HZ对应10ms时间片,1000HZ对应1ms时间片。
  2. 时间片的大小限制了一个进程可以独占CPU运行的最长时间。时钟中断用于在时间片用完时强制隔离当前进程,让其他进程运行。
  3. 时钟中断必须大于等于时间片大小。如果时钟中断频率为10HZ,但时间片为5ms,将是不合理的,这会导致定时器在时间片内无法生效。
  4. 较小的时间片可以增强调度的公平性与响应速度,但会增加上下文切换开销。时钟中断频率与时间片的设置需要在这两个方面进行权衡。

所以,总结来说:

  • 时钟中断频率决定了CPU上下文切换的频率,影响操作系统的调度频率。
  • 时间片决定了每个进程CPU占用时间的大小,影响调度的公平性与进程等待时间。
  • 时钟中断频率越高,时间片越小;但也增加上下文切换开销。
  • 它们需要根据系统需求进行权衡,设置合适的参数。

正确理解时钟中断与时间片的关系与区别,对学习操作系统的调度机制非常重要。时钟中断提供了调度的动力,时间片实现了调度的公平策略。两者相互配合,才能完成操作系统高效而合理的CPU调度。 所以,总的来说,时钟中断频率决定了调度的节奏,时间片决定了每个进程的时间分配。它们共同构成操作系统CPU调度的基本机制。

补习了这些概念有啥用?

我们开发出来的程序和产品跑在现代操作系统上,也包括Kubernetes等,知名的大佬都在研究时间的处理问题,有兴趣可以点击下面的链接看看: https://github.com/kubernetes/kubernetes/pull/111520 https://github.com/zalando-incubator/kubernetes-on-aws/pull/923 甚至包括知名理论《 排队论

排队论

排队论是运筹学的一个重要分支,主要研究的是排队系统的性能分析与优化。它通过建立数学模型来分析客户的到达模式、服务模式、排队discipline等因素对系统性能的影响,帮助我们理解和优化复杂的排队系统。 排队论可以很好地用来描述和分析CPU的时序问题。我们可以将CPU看作一个服务器,任务或进程作为客户,建立数学模型来分析不同调度策略对CPU性能的影响。 具体来说,我们可以这样建模:

  1. 客户:CPU上的任务或进程,它们会不断地到达与等待CPU服务。这些任务的到达模式可以看作是随机的。
  2. 服务器:CPU内核,它按照一定的规则为任务提供计算服务。CPU的服务时间也可以看作是一个随机变量。
  3. 排队区:就绪队列(ready queue),保存已就绪等待运行的任务。不同的调度规则对应不同的排队规则。
  4. 服务机制:CPU调度器,它从就绪队列中选择任务并调度到CPU上运行。不同的调度算法对应不同的服务机制。

基于该模型,我们可以分析不同调度策略对CPU性能的影响:

  1. FCFS:就绪队列相当于FIFO,会对短任务不公平,长任务的响应会很长。
  2. SJF:可以优化平均响应时间,但可能导致长任务的starvation。
  3. 轮转法:间接限制每个任务的CPU时间,可以提高公平性但可能会降低CPU利用率。
  4. 优先级:为高优先级任务提供更快服务,可以提高效率,但也可能出现priority inversion等问题。
  5. 反馈队列:根据任务的CPU使用情况动态调整其优先级,在公平性与效率间达到平衡。
  6. 增加CPU数量:可以提高系统吞吐量,但也增加硬件成本与管理难度。
  7. 亲和性:将任务与特定CPU绑定,可以提高缓存利用率与性能,需要权衡任务移动开销。

所以,我们可以看到,CPU调度中的许多问题如公平性、吞吐量、响应性、优先级等,都可以借助排队论的模型与分析方法来研究。通过调整服务模式、排队规则与 CPU 数量等参数,可以实现不同的调度策略,优化 CPU 性能。

排队论为我们理解和分析复杂的 CPU 调度机制提供了非常有用的工具和思路。它可以将调度问题数学化与模型化,帮助我们从更高的角度去思考与优化问题。所以,排队论是我们研究操作系统与学习 CPU 调度机制的重要基础理论之一

提外话也聊一下

观测CPU截流

CPU截流的原理是控制进程对CPU的访问,以保证系统的稳定性和公平性。它的主要思想是:当CPU超过某个利用阈值时,限制进程对CPU的访问,从而防止CPU过度占用。

CPU截流通常用于以下场景:

  1. 防止CPU过载。当系统CPU利用率过高时,通过CPU截流可以限制个别进程的CPU使用,避免CPU过载导致系统崩溃或响应缓慢。
  2. 保证服务质量。可以通过CPU截流来保证关键业务进程获取足够的CPU资源,避免被其他进程挤占。这样可以为关键业务提供稳定的服务质量。
  3. 防止进程CPU starvation。通过CPU截流可以避免个别进程长期无法获取CPU时间片的情况,保证每一个进程都有机会运行。

CPU截流通常有两种主要方式:

  1. 时间片截流。限制进程在每个时钟中断周期内可以使用的CPU时间片。例如可以限制为原来的80%。
  2. IO速率限制。限制进程的IO吞吐速率,间接限制其CPU使用。因为CPU使用通常伴随着IO操作,限制IO可以减少CPU占用。

在Linux系统中,可以使用ulimit、cpulimit等工具来设置进程的CPU截流。另外,cgroups也提供了CPU限制功能,可以更精细地控制容器/进程的CPU使用。

CPU截流是一个非常有用的技术,它可以帮助系统管理员更好地管理CPU资源,提供更加稳定可靠的服务。但如果使用不当,也会影响系统和业务的性能,所以需要慎重设置各个进程的CPU限制。

CPU截流的计算逻辑主要涉及两个方面:

  1. CPU利用率计算。需要实时计算系统和各个进程的CPU利用率,作为CPU截流的判断标准。

    CPU利用率的计算公式为:CPU使用时间/测量间隔时间。其中,CPU使用时间可以从/proc/stat中获取,测量间隔时间一般选取1秒。

    • 系统CPU利用率 = (user + nice + system + idle) / 总时间
    • 进程CPU利用率 = 进程使用CPU时间 / 总时间
  2. CPU截流判断与执行。需要根据CPU利用率判断是否需要执行CPU截流,如果需要则计算每个进程的CPU限制并执行限制。

    判断逻辑可以为:

    • 如果系统CPU利用率 > 阈值(例如80%)则触发系统级CPU截流
    • 如果关键进程CPU利用率 < 最小阈值(例如50%)则触发针对该进程的CPU截流
    • 如果普通进程CPU利用率 > 最大阈值(例如20%)则触发针对该进程的CPU截流

    CPU限制的计算可以基于进程原有的CPU份额来计算,例如:

    • 系统级别:每个进程CPU限制 = 进程原有CPU份额 * 系统阈值(例如80%)
    • 进程级别:该进程CPU限制 = 进程原有CPU份额 * 进程阈值(例如50%或20%)
    • 手动设置某些关键进程的CPU限制,其余进程按比例分配

Prometheus和CPU截流的小故事:

一般主机场景

Prometheus可以很方便地实现CPU截流监控与预警。主要可以通过以下几个方面:

  1. CPU利用率指标:

    • 计算系统总体CPU利用率:rate(node_cpu_seconds_total{mode=“system”}[1m]) / 60
    • 计算每个进程的CPU利用率:rate(process_cpu_seconds_total{name=“过程名”}[1m]) / 60
  2. 根据CPU利用率判断是否超过阈值,如果超过则触发CPU截流或预警:

    1
    2
    
    alert: CPUUtilizationTooHigh 
      expr: rate(node_cpu_seconds_total{mode="system"}[1m]) / 60 > 0.8 
    
    1
    2
    
    alert: ProcessCPUOveruse
      expr: rate(process_cpu_seconds_total{name="过程名"}[1m]) / 60 > 0.5
    
  3. 如果触发CPU截流,则需要计算每个进程的CPU限制并设置:

    进程CPU限制 = 进程原有CPU使用量 * (1 - 系统超出阈值的cpu使用比例)

    举例:

    • 系统CPU利用率阈值:80%
    • 当前系统CPU利用率:90%
    • 某进程原有CPU使用量:20%

    则:进程CPU限制 = 20% * (1 - (90% - 80%) / 90%) = 10%

    也就是该进程的CPU利用率限制会从20%降低到10%,实现对其CPU使用量的限制,达到CPU截流的目的。

  4. 根据CPU限制值,调用cgroup或其他机制设置进程的CPU限额:

    1
    
    cgroup_cpu_limit{process_name="进程名",cpu_limit="10%"}
    

    此时Prometheus通过步骤1计算cpu利用率,步骤2判断是否超阈值需要CPU截流,步骤3计算每个进程的CPU限制,步骤4设置cgroup对进程的CPU限额来完成整个CPU截流的 workflow。

Kubernetes集群

在Kubernetes中,可以通过Prometheus监控Pod的CPU利用率并进行CPU截流。主要步骤如下:

  1. 监控Pod的CPU利用率指标:

    1
    
    rate(container_cpu_usage_seconds_total{pod=~"pod_name", container=~"container_name"}[1m]) 
    
  2. 设置CPU利用率阈值,超过阈值触发报警(表示需要对该Pod进行CPU截流):

    1
    2
    
    alert: PodCPUOveruse 
      expr: rate(container_cpu_usage_seconds_total{pod=~"pod_name", container=~"container_name"}[1m]) > 0.8
    
  3. 计算Pod的CPU限制值:

    Pod CPU限制 = 总CPU限额 * (1 - 超出阈值的CPU使用比例)

    例如,Pod总CPU限额为2个CPU,当前CPU利用率为90%,阈值为80%。则:

    Pod CPU限制 = 2 * (1 - (90% - 80%) / 90%) = 1 个CPU

  4. 设置Pod的CPU限额,有两种主要方式:

    1
    
    kubectl patch pod pod-name -p '{"spec":{"containers":[{"name":"container-name","resources":{"limits":{"cpu": "1"}}}]} }'