arthur-zhang / morning-up-up

78 stars 6 forks source link

1123分享:CFS 的问题,sched_autogroup 补丁 #20

Open arthur-zhang opened 3 years ago

arthur-zhang commented 3 years ago

问题 1:新进程的vruntime的初值是不是0啊? 问题 2:CFS 与调度周期(latency target) 问题 3:进程占用的CPU时间片可以无穷小吗?

sched_autogroup 的作用

作业 1、使用 strace 来观察进程和线程 clone 系统调用的区别 2、写一个代码复现 sched_autogroup 的作用

IKNOWLJT commented 3 years ago

进程状态

CFS 的问题

虚拟运行时间 vruntime += 实际运行时间 delta_exec * NICE_0_LOAD / 权重
  1. 新进程的vruntime的初值不是0?

    每个CPU的运行队列cfs_rq都维护一个min_vruntime字段,记录该运行队列中所有进程的vruntime最小值,新进程的初始vruntime值以它所在的运行队列的min_vruntime为基础来计算的,保证新的进程的vruntime与老进程的在合理的范围内。

  2. 新进程的vruntime初值的设置和两个参数有关:

    1. sched_child_runs_first: 规定fork之后让子进程先于父进程运行

      如果 sched_child_runs_first 打开,会选择子进程和父进程中的 vruntime 小的作为子进程的 vruntime,大的是父进程的 vruntime

    2. sched_features 的 START_DEBIT位:规定新进程的第一次运行要有延迟

  3. CFS 与调度周期

    设定一个调度周期(sched_latency_ns),目标是让每个进程在这个周期内至少有机会运行一次,保证每个进程都有机会运行

    查看调度周期的设置:cat /proc/sys/kernel/sched_latency_ns 单位:ns

  4. 假设有两个进程,它们的vruntime初值都是一样的,第一个进程只要一运行,它的vruntime马上就比第二个进程更大了,那么它的CPU会立即被第二个进程抢占吗

    为了避免过于短暂的进程切换造成太大的消耗,CFS设定了进程占用CPU的最小时间值,sched_min_granularity_ns,正在CPU上运行的进程如果不足这个时间是不可以被调离CPU的。

  5. 进程占用的CPU的时间片可以无穷小吗?

    sched_min_granularity_ns 的另一个作用就是:CFS把调度周期sched_latency按照进程的数量平分,给每个进程平均分配CPU时间片(按照nice值加权),但是如果进程数量太多的话,就会造成CPU时间片太小,如果小于sched_min_granularity_ns的话就以sched_min_granularity_ns为准;而调度周期也随之不再遵守sched_latency_ns,而是以 (sched_min_granularity_ns * 进程数量) 的乘积为准

*所以CPU时间片执行的时间:period = max(sched_latency_ns, sched_min_granularity_ns nr_tasks)**

实验1 - 使用 strace 来观察进程和线程 clone 系统调用的区别

clone_flag 含义
CLONE_VM 共享父进程的虚拟内存空间
CLONE_FS|CLONE_FILES 共享父进程的文件描述符和文件系统信息
CLONE_SIGHAND|CLONE_THREAD 共享父进程的异步信号处理函数(即父进程能收到的异步信号,它也能收到并处理)
CLONE_SYSVSEM 共享父进程的System V semaphore(信号)
CLONE_SETTLS 线程支持TLS (Thread Local Storage)。TLS使得变量每一个线程有一份独立实体,各个线程的值互不干扰
CLONE_PARENT_SETTID 父进程和线程会将线程ID保存在内核任务结构体的ptid成员
CLONE_CHILD_CLEARTID 清除内核任务结构体的ctid成员上存储的线程ID。
CLONE_THREAD 将线程放入到父进程的线程组(thread group)里,这样线程在用户态就看不到自己进程ID了,只能看到父进程的进程ID,并且线程共享父进程的异步信号
SIGCHLD 设置这个 flag 以后,子进程退出时,系统会给父进程发送 SIGCHLD 信号,让父进程使用 wait 等函数获取到子进程退出的原因
创建进程的实验
  1. 通过fork方式创建一个进程 process_test.c

    #include <pthread.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sched.h>
    #include <signal.h>
    #include <unistd.h>
    #include <sys/syscall.h>
    #define gettid() syscall(__NR_gettid)
    
    int main() {
       pid_t pid;
       pid = fork();
       if (pid == 0) {
           printf("in child,  pid: %d, tid:%d\n", getpid(), gettid());
       } else {
           printf("in parent, pid: %d, tid:%d\n", getpid(), gettid());
       }
       return 0;
    }
  2. 编译:gcc -o process_test process_test.c -lpthread

  3. strace跟踪:strace ./process_test (进程是资源的封装单位)

    可以看到 当fork创建一个进程的时候的 flags的值: CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,可以看到进程的创建,子进程并没有和父进程共享内存和文件系统资源等等,符合进程是资源的封装单位

    ![image-20201123164329035](/Users/LJTjintao/Library/Application Support/typora-user-images/image-20201123164329035.png)

创建线程的实验:
  1. 通过pthread_create方式创建一个线程 thread_test.c

    #include <pthread.h>
    #include <unistd.h>
    #include <stdio.h>
    
    void *run(void *args) {
       sleep(10000);
    }
    int main() {
       pthread_t t1;
       pthread_create(&t1, NULL, run, NULL);
       pthread_join(t1, NULL);
       return 0;
    } 
  2. 编译:gcc -o thread_test thread_test.c -lpthread

  3. strace跟踪:strace ./thread_test(线程是进程的子单位,共享进程的资源,信号,并属于父进程的线程组)

    可以看到创建一个线程的时候 flags:CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID

    线程创建的本质是共享进程的虚拟内存、文件系统属性、打开的文件列表、信号处理,以及将生成的线程加入父进程所属的线程组中

    image-20201123170809998

实验2 - 复现 sched_autogroup 的作用

  1. 代码 fake_make.c

    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <getopt.h>
    int get_job_num(int argc, char *const *argv);
    void *busy(void *args) {
       while (1);
    }
    int main(int argc, char *argv[]) {
       int num = get_job_num(argc, argv);
       printf("job num: %d\n", num);
       pthread_t threads[num];
       int i;
       for (i = 0; i < num; ++i) {
           pthread_create(&threads[i], NULL, busy, NULL);
       }
       for (i = 0; i < num; ++i) {
           pthread_join(threads[i], NULL);
       }
       return 0;
    }
    int get_job_num(int argc, char *const *argv) {
       if (argc <= 1) {
           printf("illegal args\nusage ./fake_make -j8\n");
           exit(10);
       }
       int num;
    
       const char *optstring = "j:"; // 有三个选项-abc,其中c选项后有冒号,所以后面必须有参数
       int ret;
       ret = getopt(argc, argv, optstring);
    
       if (ret != 'j') {
           printf("illegal args\nusage ./fake_make -j8\n");
           exit(1);
       }
       num = atoi(optarg);
       return num;
    }
  2. 编译:gcc -o fake_make fake_make.c -lpthread

  3. 修改 kernel.sched_autogroup_enabled 参数:sudo sysctl -w kernel.sched_autogroup_enabled=0(注意修改后要退出重新登录)

    启用后,内核会根据进程的线程组来调度CPU,这样CPU的均分就不依赖于线程,而是进程为单位了

    0:禁止 1:开启

  4. 分别起3个终端执行 ./fake_make -j8./fake_make -j4./fake_make -j2

kernel.sched_autogroup_enabled=0

image-20201123173914546

kernel.sched_autogroup_enabled=1

image-20201123174121450

Wolf-ZR commented 3 years ago

问题 1:新进程的vruntime的初值是不是0啊? 答:不是0,新进程的vruntime初始值为当前最小的vruntime,为0会导致优先级过高 问题 2:CFS 与调度周期(latency target)

固定时长内每个线程都会执行一次 cat /proc/sys/kernel/sched_latency_ns 最小执行时间 cat /proc/sys/kernel/sched_min_granularity_ns 问题 3:进程占用的CPU时间片可以无穷小吗? 答:不能,无穷小会导致切换上下文频繁 sched_autogroup 的作用 答:保证每个session下的CPU占用率平均

作业 1、使用 strace 来观察进程和线程 clone 系统调用的区别

2、写一个代码复现 sched_autogroup 的作用

import java.util.concurrent.Executor; import java.util.concurrent.Executors;

public class Test { public static void main(String[] args) { int threadCount = Integer.valueOf(args[0]); Executor executor = Executors.newFixedThreadPool(4); for (int i = 0; i < threadCount; i++) { executor.execute(new MyThread()); } } }

class MyThread implements Runnable { int i = 0; @Override public void run() { while (i < Integer.MAX_VALUE) { i++; if (i > Integer.MAX_VALUE/2) { i = 0; } } } }

image

wolfleave commented 3 years ago

task_struct

进程和线程的关系

进程状态

pid = `pidof sha256sum`
do
  cpulimit -b -l 50 -p pid
  sleep 10
  kill -9 `pidof cpulimit`
  sleep 10
done

进程调度

作业

1、使用 strace 来观察进程和线程 clone 系统调用的区别

1.写一个多进程程序:

#include <unistd.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <stdlib.h>

pid_t gettid() {
    return syscall(__NR_gettid);
}
int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        exit(1);
    } else if (pid == 0) {
        printf("in child, pid: %d, tid:%d\n", getpid(), gettid());
        pause();
        exit(0);
    } else {
        printf("in parent, pid: %d, tid:%d\n", getpid(), gettid());
    }
    return 0;
}

2.编译:gcc -o multiProcess Multi-Process.c

3.跟踪程序系统调用 :strace ./multiProcess

image-20201123210746037

  1. 写一个多线程程序
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
int get_job_num(int argc, char *const *argv);
void *busy(void *args) {
    while (1);
}
int main(int argc, char *argv[]) {
    int num = get_job_num(argc, argv);
    printf("job num: %d\n", num);
    pthread_t threads[num];
    int i;
    for (i = 0; i < num; ++i) {
        pthread_create(&threads[i], NULL, busy, NULL);
    }
    for (i = 0; i < num; ++i) {
        pthread_join(threads[i], NULL);
    }
    return 0;
}
int get_job_num(int argc, char *const *argv) {
    if (argc <= 1) {
        printf("illegal args\nusage ./fake_make -j8\n");
        exit(10);
    }
    int num;

    const char *optstring = "j:"; // 有三个选项-abc,其中c选项后有冒号,所以后面必须有参数
    int ret;
    ret = getopt(argc, argv, optstring);

    if (ret != 'j') {
        printf("illegal args\nusage ./fake_make -j8\n");
        exit(1);
    }
    num = atoi(optarg);
    return num;
}

6.编译:gcc -lpthread autogroup.c

7。跟踪 strace ./a.out -j8

image-20201123211012579

实验可知:

创建进程和创建线程都是调用clone方法。创建进程flags 用的是:CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD。而线程用的flags是:

CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID。线程共用了进程的vm、fs、files、sighand等

2、写一个代码复现 sched_autogroup 的作用

使用autogroup.c 编译得到的a.out 程序,开两个终端,分别执行./a.out -j8、./a.out -j2。得到结果如下:

image-20201123212219901

设置sched_autogroup_enabled :sudo sysctl -w kernel.sched_autogroup_enabled=1.分别执行./a.out -j8、./a.out -j2,得到结果如下:

image-20201123212113729

实验结果可知:

kernel.sched_autogroup_enabled=1 可使cpu 按进程进行公平调度

LinForTracy commented 3 years ago

image

wefun94 commented 3 years ago

(1)概述

进程是程序的执行副本,linux内核通过task_struct结构体来管理进程 进程数量限制:

(2)进程和线程的区别

标识符: pid 是当前进程的ID号 , tgid 当前进程所在线程组的线程组ID 进程和线程都是通过task_struct来管理的,每个线程也是一个task,都有自己的一份 task_struct,而且每个线程都有自己独特的pid 一个进程就是一个线程组,所以每个进程的所有线程都有着相同的tgid。当程序开始运行时,只有一个主线程,主线程的tgid就等于pid。而当其他线程被创建的时候,就继承了主线程的tgid。内核就可以通过tgid知道某个task属于哪个线程组,也就知道属于哪个进程了。

创建方式: fork()创建进程、pthread()创建线程 ,底层都是调用 clone 函数 (系统调用),但是flags参数传值不一样。

(3)进程的状态

task_struct有一个变量 volatile long state 表示进程的状态,包括下面的值

(4)进程调度算法:

优先级: 从调度的角度,Linux把进程分成140个优先等级,其中0级到99级是分给实时进程的,100级到139级是分给非实时进程的。每个优先等级都有 一个运行对列,这样就有140个运行队列。级数越小优先度越高。调度程序从0级到139级依次询问每个运行队列是否有可执行进程

常见的调度算法:

CFS调度器: SCHED_NORMAL调度算法的实现类是struct cfs_rq CFS调度器没有时间片的概念,而是分配cpu使用时间的比例。 例如:2个相同优先级的进程在一个cpu上运行,那么每个进程都将会分配50%的cpu运行时间 CFS调度器的核心:

whx405831799 commented 3 years ago

image