0%

Linux实时线程和进程调度

实时线程

  • 在Linux中,实时线程是一种特殊类型的线程,它们的调度策略和优先级可以被设置为实时的。这意味着,相比于普通的线程,实时线程有更高的优先级,而且它们的执行不会被低优先级的线程打断

    实时线程的调度策略

  • SCHED_FIFO先入先出策略
    • 线程会一直运行,直到它自己放弃CPU时间,或者有更高优先级的线程需要运行
  • SCHED_RR时间片轮转调度方案
    • 类似于SCHED_FIFO,但是每个线程会有一个固定的时间片来运行。当一个线程的时间片用完时,它会被放到同优先级线程的队列尾部

      C语言设置线程优先级的方法

  • 使用pthread多线程库
    #include <pthread.h>
    #include <sched.h>

    void *thread_func(void *arg) {
    // 这里是线程的代码
    return NULL;
    }

    int main() {
    pthread_t thread;
    pthread_attr_t attr;
    struct sched_param param;

    // 初始化线程属性
    pthread_attr_init(&attr);

    // 设置线程为分离状态,这样当线程结束时会自动释放所有资源
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // 设置线程的调度策略为实时调度策略SCHED_FIFO
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);

    // 设置线程的优先级为最高
    param.sched_priority = sched_get_priority_max(SCHED_FIFO);
    pthread_attr_setschedparam(&attr, &param);

    // 创建线程
    pthread_create(&thread, &attr, thread_func, NULL);

    // 销毁线程属性对象
    pthread_attr_destroy(&attr);

    return 0;
    }

  • 请注意,只有具有适当权限的用户(通常是root用户)才能创建实时线程。此外,过度使用实时线程可能会导致系统响应变慢,甚至完全无响应, 也可能导致系统直接崩溃。请谨慎使用。

    time命令

  • 使用time命令运行程序的时候,Ctrl+C可以计算程序在系统空间(sys)总时间(包括多个CPU核心)、用户空间(user)(包括多个CPU核心)总时间以及人类视角(real)运行了多长时间
  • picture 8

实时进程

  • 实时进程和实施线程类似,因为Linux在进行CPU调度的时候线程和进程是平等的
  • 调度方式也是有SCHED_FIFOSCHED_RR两种方式
  • 设置一个进程为实时进程
    sudo chrt -f -a -p 99 pid
  • 其中pid是需要控制的线程的进程id(Linux中每个线程都有单独的进程ID,inux中每个线程都有单独的进程ID。在Linux中,线程其实是通过轻量级进程(LWP)实现的,因此Linux中每个线程都是一个进程,都拥有一个PID。换句话说,操作系统原理中的线程,对应的其实是Linux中的进程)
  • -a是进程的所有线程,可以不用这个选项,针对每个线程单独设置
  • 在C程序中设置一个进程的调度方式
    #include <sched.h>

    int main() {
    struct sched_param param;
    int policy = SCHED_FIFO; // 这里可以改为你想要的调度策略

    param.sched_priority = sched_get_priority_max(policy);
    if (sched_setscheduler(0, policy, &param) == -1) {
    perror("sched_setscheduler");
    return 1;
    }

    // 这里是进程的代码

    return 0;
    }
  • 在shell脚本中设置进程的调度方式
    sudo chrt -f 99 pid
    # -f是FIFO调度方式
    sudo chrt -m
    # 上面这句命令可以显示所有可用的调度策略和优先级

    进程和CPU的关系

  • 将一个进程绑定到一个特定的CPU
    taskset -cp <cpuID> pid
    # 或者
    taskset -cp <cpu-list> pid
    #其中cpu-list是数字化的cpu列表,从1开始。多个不连续的cpu可用逗号连接,连续的可用短现连接,比如1,2,5-11等
  • pid是需要改变的进程的ID,cpuID是需要绑定的CPU核心id
  • 或者使用掩码的方式设置
  • picture 10
  • taskset -a -p <掩码> <pid>
  • 在C程序中将进程绑定到某个特定的CPU
    #define _GNU_SOURCE
    #include <sched.h>
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>

    int main(int argc, char **argv) {
    cpu_set_t cpuset;

    CPU_ZERO(&cpuset); // 初始化CPU集合,将cpuset置为空
    CPU_SET(2, &cpuset); // 将本进程绑定到CPU2上

    // 设置进程的CPU亲和性
    if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) {
    printf("Set CPU affinity failed, error: %s\n", strerror(errno));
    return -1;
    }

    return 0;
    }
  • 线程绑定到某个CPU
  • picture 9
  • Linux中线程可以在不同CPU核心之间来回移动
  • 中断也可以设置CPU亲和性
  • picture 11
  • picture 12
    • 上述补丁的意义是将软中断负载均衡到每个核心

      进程群

  • picture 13
  • 设置进程群的CPU使用比率,先调度进程群,再调度其中的进程
  • 现决定一个进程群内部所有进程能用的所有CPU时间,再考虑分配给谁

    如何创建

  • 进入/sys/fs/cgroup/cpu/下创建目录dir
  • picture 14
  • 进入创建的目录查看cpu.shares权重
  • 添加某个进程到进程组
    • 将进程的pid添加到cgroup.procs文件中
  • cpu.cfs_period_uscpu.cfs_quota_us是一个进程组中的cfs进程在一个period范围内能运行最多quota微秒
  • quota可以大于period,因为是多核的,可以设置为period*核心数

硬实时

  • 从创建一个任务到他开始被调度,不会超过一个截止期限
  • Linux是一个软实时的系统,因此可能会超过这个时间
  • Linux无法实现硬实时
  • picture 15
    • Linux下假如你休眠10ms,因为Linux的调度抖动,可能会导致进程就绪之后无法被调度,因此两次调度会间隔大于10ms,会随着系统负载变大而延迟变大,具有不确定性

      Linux不可调度区域

  • picture 16
    • 打上硬实时补丁之后可以进一步减小不可调度的范围,将系统变为硬实时的
  • picture 17
    • 中断,软中断和自旋锁执行时都不能被调度,不能被抢占
    • 一个CPU拿到spin lock的时候,这个核心就不能被调度了
    • 软中断中可以嵌套中断,硬中断不行
    • 如果需要抢占的任务发生在上述三种情况中,则只能在上述三种状态执行结束的瞬间立即抢占
  • picture 18
    • 因为当一个进程占有不可打断的任务的时候,中断无法抢占他导致中断被延迟处理,不满足实时性,延迟的长度是橙色箭头

      实时补丁

  • picture 19
  • preempt_rt补丁
  • 第四个选项就是完全硬实时补丁的选项

    自旋锁和互斥锁的区别

  • 自旋锁是CPU一个核心拿到锁,开始处理,另一个核心拿不到则原地自旋
  • 互斥锁是一个进程拿到锁开始执行,另一个线程没拿到,则睡眠直到上一个进程释放锁唤醒

    优先级继承

  • 在低优先级的进程持有高优先级的进程试图获取的锁的时候,临时提高这个低优先级进程的优先级到跟高优先级进程一样,使得他能够在调度上获得优势从而赶快执行完,释放锁给高优先级进程使用,防止高优先级进程因为争抢锁等待低优先级进程

    Linux的进程调度策略

  • 进程调度参考1
  • 进程调度参考2

    进程调度的基础知识

  • 进程调度本身所需要的时间很短,基本就是更改一些寄存器等等,但是因为这个原因导致的上下文变化引起的CPU内部高速缓存的不命中可能在更大程度上导致程序执行时间受到影响
  • 调度的基本单位是线程
  • picture 0
    • Linux内核的抢占设置
    • 服务器一般讲究的是吞吐量而不是响应速度,但是桌面电脑和手机必须讲究响应速度否则导致卡顿
    • 第一个的话操作系统几乎没有抢占调度
    • 内核不能被抢占
    • 第三个内核也可以被抢占
  • 进程的特性
    • CPU消耗型和IO消耗型
    • IO消耗型任务得到CPU要求的较为及时,因为不及时的话会导致IO速度下降,用户体验下降,但是CPU性能对其影响不大

      早期调度器设计

  • picture 2
  • 内核优先级0-139,内核数字越小优先级越高
  • 0-99算是实时线程,99-139是非实时线程,0-99之间的数字越大优先级越高,内核实际计算的时候是99-用户设置的优先级
  • 调度看的就是从高优先级到低优先级,谁先有进程就绪就调度谁
  • 优先级高的进程可以抢占优先级低的进程
  • picture 3
  • 以上是优先级再0-99期间的进程的调度策略,二者的区别是FIFO同等优先级是先进先出,RR是时间片轮转方式(同等优先级)
  • 所有前面的进程都跑完了,才会跑100-139的进程
  • 普通进程的优先级是nice,也就是-20-19,值越大优先级越低
  • 普通进程优先级高不会形成对低优先级的绝对优势
  • 前面不会堵着后面
  • picture 4
    • RT的门限:上面一条设置的是实时进程在一个sched_rt_period中能运行的最多时间是sched_rt_runtime
    • 修改上述门限的方法:
    • 直接消除门限:sudo sysctl -w kernel.sched_rt_runtime_us=-1
      • picture 21
      • 看得出解除门限之后确实CPU使用不受限制了
    • sudo sh -c 'echo "1000000" > /proc/sys/kernel/sched_rt_runtime_us'
    • 因此,在一些情况下猛然将一个进程从普通进程转换为实时进程可能导致进程运行速度下降,因为实时进程的运行时间比率是有限的,但是普通进程没有这个限制
    • 但是可能会导致系统运行明显卡顿,因为实时进程的优先级太高了,比很多系统进程优先级都高
    • RT进程创建的线程也是RT的
    • RT类型的进程应该尽可能的小

      Linux系统的实时进程

  • 使用命令ps -eo pid,rtprio,cmd可以查看系统中所有具有实时优先级的进程
  • 非实时进程没有实时优先级,因此是-,实时进程则会显示实时优先级
  • picture 20
  • 但是系统中也有一些其他自身的实时进程,比如/usr/libexec/tracker-miner-fs-3优先级是0,irq/24-pciehpwatchdogd, idle_inject等的实时优先级是50,migration/9等是99
  • 不建议超过50,否则可能导致看门狗进程无法正常运行,使得系统崩溃

    CFS调度算法(也就是所谓的OTHER调度算法)

  • 针对非实时进程的调度算法,实时进程的优先级是大于这些进程的
  • 追求的是每个进程的vruntime接近
  • 线程才有nice,线程是调度单位,进程资源分配单位(不包括CPU)
  • 默认的线程的nice是0
  • 无法设置进程是IO消耗还是CPU消耗
  • picture 5
  • CFS能在真实硬件上模拟出一种“公平的、精确的任务多处理CPU”
  • 参考
  • vruntime += 实际运行时间(time process run) * 1024 / 进程权重(load weight of this process)
    • picture 6
  • 调度的是虚拟运行时间最短的进程(利用红黑树排序)
  • vruntime并不是无限小的,有一个最小值来限定。假如新进程的vruntime初值为0的话,比老进程的值小很多,那么它在相当长的时间内都会保持抢占CPU的优势,老进程就要饿死了
  • 每个CPU的运行队列cfs_rq都维护一个min_vruntime字段,记录该运行队列中所有进程的vruntime最小值,新进程的初始vruntime值就以它所在运行队列的min_vruntime为基础来设置,与老进程保持在合理的差距范围内
  • 唤醒抢占
    • 休眠进程在唤醒时会获得vruntime的补偿,它在醒来的时候有能力抢占CPU是大概率事件,这也是CFS调度算法的本意,即保证交互式进程的响应速度,因为交互式进程等待用户输入会频繁休眠

      调度API

  • picture 7