Open arthur-zhang opened 3 years ago
虚拟运行时间 vruntime += 实际运行时间 delta_exec * NICE_0_LOAD / 权重
新进程的vruntime的初值不是0?
每个CPU的运行队列cfs_rq都维护一个min_vruntime字段,记录该运行队列中所有进程的vruntime最小值,新进程的初始vruntime值以它所在的运行队列的min_vruntime为基础来计算的,保证新的进程的vruntime与老进程的在合理的范围内。
新进程的vruntime初值的设置和两个参数有关:
sched_child_runs_first: 规定fork之后让子进程先于父进程运行
如果 sched_child_runs_first 打开,会选择子进程和父进程中的 vruntime 小的作为子进程的 vruntime,大的是父进程的 vruntime
sched_features 的 START_DEBIT位:规定新进程的第一次运行要有延迟
CFS 与调度周期
设定一个调度周期(sched_latency_ns),目标是让每个进程在这个周期内至少有机会运行一次,保证每个进程都有机会运行
查看调度周期的设置:
cat /proc/sys/kernel/sched_latency_ns
单位:ns
假设有两个进程,它们的vruntime初值都是一样的,第一个进程只要一运行,它的vruntime马上就比第二个进程更大了,那么它的CPU会立即被第二个进程抢占吗
为了避免过于短暂的进程切换造成太大的消耗,CFS设定了进程占用CPU的最小时间值,sched_min_granularity_ns,正在CPU上运行的进程如果不足这个时间是不可以被调离CPU的。
进程占用的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)**
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 等函数获取到子进程退出的原因 |
通过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;
}
编译:gcc -o process_test process_test.c -lpthread
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)
通过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;
}
编译:gcc -o thread_test thread_test.c -lpthread
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
线程创建的本质是共享进程的虚拟内存、文件系统属性、打开的文件列表、信号处理,以及将生成的线程加入父进程所属的线程组中
代码 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;
}
编译:gcc -o fake_make fake_make.c -lpthread
修改 kernel.sched_autogroup_enabled 参数:sudo sysctl -w kernel.sched_autogroup_enabled=0
(注意修改后要退出重新登录)
启用后,内核会根据进程的线程组来调度CPU,这样CPU的均分就不依赖于线程,而是进程为单位了
0:禁止 1:开启
分别起3个终端执行 ./fake_make -j8
、./fake_make -j4
、./fake_make -j2
kernel.sched_autogroup_enabled=0
kernel.sched_autogroup_enabled=1
问题 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; } } } }
pid = `pidof sha256sum`
do
cpulimit -b -l 50 -p pid
sleep 10
kill -9 `pidof cpulimit`
sleep 10
done
作业
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
#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
创建进程和创建线程都是调用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等
使用autogroup.c 编译得到的a.out 程序,开两个终端,分别执行./a.out -j8、./a.out -j2。得到结果如下:
设置sched_autogroup_enabled :sudo sysctl -w kernel.sched_autogroup_enabled=1.分别执行./a.out -j8、./a.out -j2,得到结果如下:
kernel.sched_autogroup_enabled=1 可使cpu 按进程进行公平调度
进程是程序的执行副本,linux内核通过task_struct结构体来管理进程 进程数量限制:
标识符: pid 是当前进程的ID号 , tgid 当前进程所在线程组的线程组ID 进程和线程都是通过task_struct来管理的,每个线程也是一个task,都有自己的一份 task_struct,而且每个线程都有自己独特的pid 一个进程就是一个线程组,所以每个进程的所有线程都有着相同的tgid。当程序开始运行时,只有一个主线程,主线程的tgid就等于pid。而当其他线程被创建的时候,就继承了主线程的tgid。内核就可以通过tgid知道某个task属于哪个线程组,也就知道属于哪个进程了。
创建方式: fork()创建进程、pthread()创建线程 ,底层都是调用 clone 函数 (系统调用),但是flags参数传值不一样。
task_struct有一个变量 volatile long state 表示进程的状态,包括下面的值
优先级: 从调度的角度,Linux把进程分成140个优先等级,其中0级到99级是分给实时进程的,100级到139级是分给非实时进程的。每个优先等级都有 一个运行对列,这样就有140个运行队列。级数越小优先度越高。调度程序从0级到139级依次询问每个运行队列是否有可执行进程
常见的调度算法:
CFS调度器: SCHED_NORMAL调度算法的实现类是struct cfs_rq CFS调度器没有时间片的概念,而是分配cpu使用时间的比例。 例如:2个相同优先级的进程在一个cpu上运行,那么每个进程都将会分配50%的cpu运行时间 CFS调度器的核心:
问题 1:新进程的vruntime的初值是不是0啊? 问题 2:CFS 与调度周期(latency target) 问题 3:进程占用的CPU时间片可以无穷小吗?
sched_autogroup 的作用
作业 1、使用 strace 来观察进程和线程 clone 系统调用的区别 2、写一个代码复现 sched_autogroup 的作用