Linux 任务调度简介

吞吐 和 响应 是评估一个计算机系统性能的主要参数,比较典型的就是在IO系统中,Linux为了提高IO吞吐而将IO先缓存到内存中,这在本质上是牺牲了对某个IO的响应过程并以此来提高吞吐率。这种设计思路在Linux调度器的设计也可以看到,Linux并没有追求硬实时系统的高响应,而是在吞吐和响应之间取得一个平衡。

在Linux中,任务(或者说task_struct,由于Linux中进程和线程没有明显的界限,本文使用任务来描述一个可调度实例)一共有0-139个优先级,其中0-99是给RT 任务使用,100-140给普通任务使用,前者即实时调度策略,进一步又分为SCHED_FIFO和SCHED_RR,后者即普通任务,即SCHED_NORMAL。作为优先级更高的实时任务,SCHED_FIFO和SCHED_RR的任务一旦处于RUNNING(就绪)状态,就可以无条件抢占SCHED_NORMAL的CPU,而只要系统中有RT的任务在运行,SCHED_NORMAL就没有运行的机会。当然,即便是RT 任务,其也会有优先级高低的区别,对于不同优先级的RT任务,显然要严格按照高优先级优先运行的原则,而对于相同优先级的两个RT任务,如果使用SCHED_FIFO,就按照”先来后到”的顺序,只要先运行的任务没有放弃CPU,后来的任务不会被调度到; 如果使用SCHED_RR,同优先级的任务会”周期性”的都被执行到。

Linux中每一个SCHED_NORMAL任务都有一个nice值(-20~+19,缺省为0),这个nice值表示这个任务对于其他任务的”容忍”程度,nice值越大,表示这个任务越好说话,结果是这个任务占用的资源就会减少。早期的Linux通过动态的调整一个动态nice来平衡IO消耗型任务和CPU消耗型任务,不过如今这个工作已经被巧妙的CFS替代了

vruntime = pruntime/weight * delta

上图就是CFS的运作公式,pruntime表示当前任务实际运行的时间,weight是一个与nice值相关的权重,share是系统级/cgroup的调整参数。整个CFS基于红黑树实现进程调度。如下图所示:

上图的数字就是每个任务的vruntime,CFS在进行任务调度的时候,总是调度红黑树中vruntime最小的那个任务,由于IO消耗型的任务本身pruntime就比CPU消耗型的pruntime小,所以整体上IO消耗型的任务的vruntime更小,也就能够获得更多的被调度的机会,这样就能提高系统整体的吞吐。

weight与nice的关系如下,可以看到,对于每一个nice值都有其对应weight,nice越大,weight越小,相同的pruntime和share的情况下,任务的vruntime越大,根据CFS算法,被调度到的机会也就越小

/*
 * Nice levels are multiplicative, with a gentle 10% change for every
 * nice level changed. I.e. when a CPU-bound task goes from nice 0 to
 * nice 1, it will get ~10% less CPU time than another CPU-bound task
 * that remained on nice 0.
 *
 * The "10% effect" is relative and cumulative: from _any_ nice level,
 * if you go up 1 level, it's -10% CPU usage, if you go down 1 level
 * it's +10% CPU usage. (to achieve that we use a multiplier of 1.25.
 * If a task goes up by ~10% and another task goes down by ~10% then
 * the relative distance between them is ~25%.)
 */
static const int prio_to_weight[40] = {     
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};

Eg.

下面是一个小demo来验证CFS算法。

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <pthread.h>
void *thread_fun(void *param)
{
	printf("pid:%d,tid:%lu\n",getpid(),pthread_self());
	while(1);
	return NULL;
}
int main(int argc, char * const argv[])
{
	pthread_t tid1, tid2;
	int ret;
	printf("main pid:%d,tid:%lu\n",getpid(),pthread_self());
	if(-1 == pthread_create(&tid1,NULL,thread_fun,NULL)){
		perror("create err");
		return -1;
	}
	if(-1 == pthread_create(&tid2,NULL,thread_fun,NULL)){
		perror("create err");
		return -1;
	}
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	return 0;
}

这里通过上面的小程序使用cgroup来调整系统负载,cgroup是在系统调度器和具体的任务之间加了一层对资源的封装,系统首先按照CFS算法调度每一个group,而后再在group内按照CFS算法调度每一个任务,这种资源封装的一个好处就是防止一个多线程程序占用过多的调度资源,不过cgroup的使用远不止如此,其主要的应用其实就是android和docker,前者通过将后台任务加入到系统资源比较少的group而将前台交互任务加入到系统资源比较多的group来提高用户体验,后者借助cgroup控制了一个容器对系统资源的使用情况。

1. 启动3个任务


Desktop $./threads &
[1] 25213
Desktop $main pid:25213,tid:140358129997568
pid:25213,tid:140358121744128
pid:25213,tid:140358113351424

Desktop $./threads &
[2] 25217
Desktop $main pid:25217,tid:140274541983488
pid:25217,tid:140274533730048
pid:25217,tid:140274525337344

Desktop $./threads &
[3] 25220
Desktop $main pid:25220,tid:139669740836608
pid:25220,tid:139669724190464
pid:25220,tid:139669732583168

系统占用资源如下,PC机是8核,三个任务一共6个线程,各占一个核,每个任务占了2个CPU,所以占用率基本是200%


  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND     
25220 jiang     20   0   22916    756    676 S 196.0  0.0   5:33.29 threads     
25217 jiang     20   0   22916    816    732 S 195.0  0.0   5:36.15 threads     
25213 jiang     20   0   22916    756    676 S 193.4  0.0   5:43.89 threads

2. 将3个线程按照2:1分别加入到两个cgroup中

在cgroup/cpu中,cgroup.procs表述属于该group的PID(严格地说是PGID),如果想按照线程的加入group,类似的将/proc//task/写入到cgroup.tasks即可。


#cd /sys/fs/cgroup/cpu
#mkdir A B
A# ls
cgroup.clone_children  cgroup.procs  cpuacct.stat  cpuacct.usage  cpuacct.usage_percpu  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.shares  cpu.stat  notify_on_release  tasks
A# echo 25213 >cgroup.procs 
A# echo 25217 >cgroup.procs 
A# cd ../B
B# echo 25220 >cgroup.procs

3. 调整相应的cgroup参数

在cgroup/cpu中,cpu.cfs_period_us和cpu.cfs_quota_us用来控制组内任务的CPU使用时间,cpu.shares表示CFS公式中的delta。我们可以通过调节这些参数控制任务的系统资源占用。

比如,调整B group、即任务25220的CPU占用为0.5个核:


B# cat cpu.cfs_period_us 
100000
B# cat cpu.cfs_quota_us 
-1
B# echo 50000 >cpu.cfs_quota_us
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND  
25220 jiang     20   0   22916    756    676 S  49.3  0.0  54:12.29 threads

Bingo!!!

调整CPU占用1.5个


B# echo 150000 >cpu.cfs_quota_us
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND      

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.