Linux线程调度策略和优先级

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

关于LINUX下线程调度的测试于思考

  • 线程调度和优先级的相关问题比较少,这里把遇到的问题做一个总结
  • 一、测试代码
    • 调度策略
      • 1.通过线程属性设置
        • 注意事项:
      • 2.子线程自己更改自己的调度策略和优先级
        • 注意事项
      • 3.获取线程调度策略和优先级
        • 注意事项
  • 二、优先级大小,内核和用户
    • 1.从内核角度看
    • 2.从应用层角度看
  • 三、CPU亲和性,单核测试
  • 四、实时进程/线程的适用场景
      • 1.硬实时
      • 2.软实时
      • 衡量实时性的标准
      • 实时任务和普通任务
      • 测试代码
  • 总结


线程调度和优先级的相关问题比较少,这里把遇到的问题做一个总结

包括线程的调度函数,遇到的BUG,以及优先级的测试计算等,由于纯靠自己测试以及一些博客的参考,可能存在一些问题。


一、测试代码

代码如下(示例):

///
/// @file    thread.cc
/// @author  fenglang
/// @date    2023-09-01 09:31:52
///

#include 
#include 
#include 
#include 

void *thread_function(void *arg) {
	//	获取当前线程的线程号
	pthread_t thread = pthread_self();
	//设置线程优先级相关的参数变量
	int policy;
	struct sched_param param;
//	param.sched_priority = 22;

	// 获取当前线程的调度参数
//	int result = pthread_setschedparam(thread,SCHED_RR,¶m);//能够设置正在运行的线程,更偏好param参数设置。

//	int result = sched_setscheduler(0, SCHED_FIFO, ¶m);//能够设置正在运行的进程,需要和线程调度函数分开
//	printf("result = %d \n",result);//获取返回值结果,返回0成功,其他为错误码
//	if(result == -1)
//	{
//		perror("Error");
//	}
	int rett = pthread_getschedparam(thread, &policy, &param);//获取当前线程的调度策略和优先级
	if(rett != 0)
	{
		perror("Error");
	}
//	while(1)
//	{
//		sleep(3);
	printf("rett = %d , policy = %d , priority = %d ,thread = %lu \n",rett, policy, param.sched_priority,(unsigned long)thread);
//	}

	return NULL;

}

int main() {
	//设置线程属性
	pthread_attr_t attr;
    pthread_attr_init(&attr);

    // 设置线程属性的调度策略为 SCHED_RR
	int ret = pthread_attr_setschedpolicy(&attr, SCHED_RR);
	pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);//设置线程属性是否继承父线程属性,必须设置该函数为不继承父线程属性,才能够成功设置子线程属性

	int policy;
	struct sched_param param;
	pthread_t thread;

	param.sched_priority = 50;
	pthread_attr_setschedparam(&attr,&param);//设置线程属性的优先级

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

	//打印RR策略的最大优先级与最小优先级
//	int min = sched_get_priority_min(SCHED_RR);
//	int max = sched_get_priority_max(SCHED_RR);
//	printf("min_priority = %d,  max_priority = %d \n",min,max);

//	int result = pthread_getschedparam(thread,&policy,¶m);	
//	printf("result = %d \n",result);
//	if(result != 0)
//	{
//		perror("Error");
//	}

	//获取线程属性的策略和优先级参数
	pthread_attr_getschedpolicy(&attr, &policy);
	pthread_attr_getschedparam(&attr, &param);
	printf("ret = %d , policy = %d , priority = %d  thread = %lu \n ",ret,policy, param.sched_priority,(unsigned long)thread);
	
	pthread_join(thread,NULL);
	return 0;

}

调度策略

  • SCHED_FIFO :实时调度策略,先到先服务。一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃 (非实时线程会有一小部分资源分配到)
  • SCHED_RR :实时调度策略,时间片轮转。当进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平 (非实时线程会有一小部分资源分配到)
  • SCHED_OTHER :分时调度策略 只能作用于非实时线程,由系统自动分配时间片,并且根据运行状态自动分配动态优先级

关于调度策略设置代码如下(示例):

1.通过线程属性设置

//1.通过线程属性设置
pthread_attr_t attr;
pthread_attr_init(&attr);
int ret = pthread_attr_setschedpolicy(&attr, SCHED_RR);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
int policy;
struct sched_param param;
param.sched_priority = 50;
pthread_attr_setschedparam(&attr,&param);//设置线程属性的优先级
注意事项:
  1. 是否继承父线程属性

.在线程初始化时使用pthread_attr_setschedpolicy函数设置线程调度策略,必须使用pthread_attr_setinheritsched函数设置不继承父线程的属性,否则仅设置属性,创建时默认继承父线程属性,使用该函数修改线程调度策略时,在终端执行程序需要使用sudo用管理员权限执行,不然子线程无法成功执行。

	#include  
	int pthread_attr_setinheritsched(pthread_attr_t *attr,int inheritsched);

inheritsched参数:

  • PTHREAD_EXPLICIT_SCHED:不继承,只有不继承父线程的调度策略才可以设置线程的调度策略
  • PTHREAD_INHERIT_SCHED:继承父进程的调度策略,默认是继承
  1. 线程优先级参数设置

    param.sched_priority = 50;
    pthread_attr_setschedparam(&attr,¶m);
    

设置实时调度策略,FIFO和RR必须对param.sched_priority参数进行赋值,取值范围[1,99],否则在后续获取参数线程策略和优先级时会产生参数错误或未知错误。

2.子线程自己更改自己的调度策略和优先级

//1.通过pthread_setschedparam函数设置
pthread_t thread = pthread_self();
int policy;
struct sched_param param;
param.sched_priority = 22;
pthread_setschedparam(thread,SCHED_RR,&param);
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param);
注意事项

设置实时调度策略时,需要对优先级参数param.sched_priority进行设置,否则无法设置调度策略和优先级。同样需要使用管理员权限执行。

3.获取线程调度策略和优先级

//获取线程的调度策略和优先级
pthread_getschedparam(thread, &policy, &param);

//获取线程属性的调度策略和优先级
pthread_attr_getschedpolicy(&attr, &policy);
pthread_attr_getschedparam(&attr, &param);
注意事项

1.参数值需要有效,在设置实时调度策略(FIFO和RR)时,需要对参数param赋值,否则使用pthread_getschedparam函数获取参数时会出现未知错误。
2.pthread_attr_getschedparam最好在子线程内部获取线程的调度策略和优先级,在主线程中调用该函数会出现一个奇怪错误,目前尚不知晓原因,如下图所示

	int result = pthread_getschedparam(thread,&policy,&param);	
	printf("result = %d \n",result);
	if(result != 0)
	{
		perror("Error");
	}

二、优先级大小,内核和用户

1.从内核角度看

数值越小,优先级越高,在内核中可以这样理解
Linux线程调度策略和优先级_第1张图片

2.从应用层角度看

  1. 实时任务
    内核通过下面这个公式来计算真正的优先级数值:

    kernel priority = 100 - 1 - param.sched_priority
    

如果应用层传入数值 0,那么在内核中优先级数值就是 99(100 - 1 - 0 = 99),在所有实时任务中,它的优先级是最低的,和普通任务没啥区别,所以在优先级参数设置的时候无法将实时调度策略的参数设置为0。
如果应用层传输数值 99,那么在内核中优先级数值就是 0(100 - 1 - 99 = 0),在所有实时任务中,它的优先级是最高的。
能够看出,与内核角度相反,所以在我们手动设置参数时,param.sched_priority的值越高,优先级越高

  1. 普通任务
    调整普通任务的优先级,是通过 nice 值来实现的,内核中也有一个公式来把应用层传入的 nice 值,转成内核角度的优先级数值:

    kernel prifoity = 100 + 20 + nice
    

nice 的合法数值是:-20 ~ 19。
如果应用层设置线程 nice 数值为 -20,那么在内核中优先级数值就是 100(100 + 20 + (-20) = 100),在所有的普通任务中,它的优先级是最高的。
如果应用层设置线程 nice 数值为 19,那么在内核中优先级数值就是 139(100 +20 +19 = 139),在所有的普通任务中,它的优先级是最低的。
能够看出,在设置普通任务的优先级时,nice值越小,优先级就越高


三、CPU亲和性,单核测试

在线程调度策略和优先级测试时,如果是多核CPU,可能多个线程同时执行线程,多线程并发执行,很难看出测试结果,需要设置CPU亲和性,将线程绑定到单一CPU上进行测试,才能够看出测试效果。


四、实时进程/线程的适用场景

1.硬实时

硬实时的要求:可重复,决定性时间内做出响应。
个人理解:结果可预期,触发概率100%。
适用场景:机器人控制,传感器采集,汽车安全气囊,紧急制动等
很明显Linux无法做到硬实时需求,无论中断程序的不可嵌套,自旋锁,内存管理机制都决定了Linux无法做到硬实时

2.软实时

并不要求完全在规定时间内100%触发,95%情况能触发就能够接受。
其目的只是根据不同的优先级尽快的完成任务目标。
适用场景:DVD播放,鼠标键盘响应

衡量实时性的标准

  1. 中断延迟:中断程序一般都是优先级较高的,当中断函数来临时的响应速度可以作为衡量实时性的标准。中断延迟就是从一个外部事件发生到相应的中断处理函数的第一条指令开始执行所需要的时间。

  2. 抢占延迟:有时也称调度延迟,系统中运行的任务很多,当优先级高的任务抢占当前任务时所需要的速度也是衡量实时性的标准。抢占延迟就是从一个外部事件发生到相应的处理该事件的任务的第一条命令开始执行的时间。

实时任务和普通任务

  1. 实时任务:什么样的任务适合作为实时任务,第一个想到的一般都是中断程序,首先他需要紧急完成,其次的触发频率不能太高。简单来说就是如果一个程序需要高效率,但是任务数量不多,那就适合设置为实时任务。
  2. 普通任务:优先级不需要高,但是又一直在跑,例如后台任务。
    举个例子,就像道路上的私家车和救护车,救护车就属于实时任务,别人要给他让路,私家车就是普通任务,慢慢跑就行。 在线程实时性设置的时候可以参考需求设置实时性。

测试代码

#include   
#include 
#include 
#include 
#include 
#include 

// 用来打印当前的线程信息:调度策略是什么?优先级是多少?
void get_thread_info(const int thread_index)
{
	int policy;
	struct sched_param param;

	printf("\n====> thread_index = %d \n", thread_index);

	pthread_getschedparam(pthread_self(), &policy, &param);
	if (SCHED_OTHER == policy)
		printf("thread_index %d: SCHED_OTHER \n", thread_index);
	else if (SCHED_FIFO == policy)
		printf("thread_index %d: SCHED_FIFO \n", thread_index);
	else if (SCHED_RR == policy)
		printf("thread_index %d: SCHED_RR \n", thread_index);

	printf("thread_index %d: priority = %d \n", thread_index, param.sched_priority);
}

// 线程函数,
void *thread_routine(void *args)
{
	cpu_set_t mask;
	//int cpus = sysconf(_SC_NPROCESSORS_CONF);
	CPU_ZERO(&mask);
	CPU_SET(0, &mask);
	if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) < 0)
	{
		printf("set thread affinity failed! \n");
	}
	//printf("cpus = %d \n",cpus);

	// 参数是:线程索引号。四个线程,索引号从 1 到 4,打印信息中使用。
	int thread_index = *(int *)args;

	// 为了确保所有的线程都创建完毕,让线程睡眠1秒。
	sleep(1);

	// 打印一下线程相关信息:调度策略、优先级。
	get_thread_info(thread_index);
	
	long num = 0;
	for (int i = 0; i < 5; i++)
	{
		for (int j = 0; j < 5000000; j++)
		{
			// 没什么意义,纯粹是模拟 CPU 密集计算。
			float f1 = ((i+1) * 345.45) * 12.3 * 45.6 / 78.9 / ((j+1) * 4567.89);
			float f2 = (i+1) * 12.3 * 45.6 / 78.9 * (j+1);
			float f3 = f1 / f2;
		}

		// 打印计数信息,为了能看到某个线程正在执行
		printf("thread_index %d: num = %ld \n", thread_index, num++);
	}

	// 线程执行结束
	printf("thread_index %d: exit \n", thread_index);
	return 0;
}
int  main(void)
{
	// 一共创建四个线程:0和1-实时线程,2和3-普通线程(非实时)
	int thread_num = 4;

	// 分配的线程索引号,会传递给线程参数
	int index[4] = {1, 2, 3, 4};

	// 用来保存 4 个线程的 id 号
	pthread_t ppid[4];

	// 用来设置 2 个实时线程的属性:调度策略和优先级
	pthread_attr_t attr[2];
	struct sched_param param[2];
	
	int priority = getpriority(PRIO_PROCESS, 0);
	printf("Main thread priority: %d\n", priority);
	int cpus = sysconf(_SC_NPROCESSORS_CONF);
	printf("cpus = %d \n",cpus);
	// 实时线程,必须由 root 用户才能创建
	if (0 != getuid())
	{
		printf("Please run as root \n");
		exit(0);
	}

	// 创建 4 个线程
	for (int i = 0; i < thread_num; i++)
	{
		if (i <= 1)    // 前2个创建实时线程
		{
			// 初始化线程属性
			pthread_attr_init(&attr[i]);

			// 设置调度策略为:SCHED_FIFO
			pthread_attr_setschedpolicy(&attr[i], SCHED_FIFO);

			// 设置优先级为 51,52。
			param[i].__sched_priority = 51 + i;
			pthread_attr_setschedparam(&attr[i], &param[i]);

			// 设置线程属性:不要继承 main 线程的调度策略和优先级。
			pthread_attr_setinheritsched(&attr[i], PTHREAD_EXPLICIT_SCHED);

			// 创建线程
			pthread_create(&ppid[i], &attr[i],thread_routine, (void *)&index[i]);
		}
		else        // 后两个创建普通线程
		{
			pthread_create(&ppid[i], 0, thread_routine, (void *)&index[i]);
		}

	}

	// 等待 4 个线程执行结束
	for (int i = 0; i < 4; i++)
	{
		pthread_join(ppid[i], 0);
	}

	for (int i = 0; i < 2; i++)
		pthread_attr_destroy(&attr[i]);
	return 0;
}

输出结果如下:同一段代码,输出结果有所不同,这里经过分析是linux实时进程的门限问题,就是说在 一段时间周期内,实时进程最多只能跑 95%的时间 ,剩下的5%需要让给普通进程,也能避免RT进程有bug, 把系统挂死了。这个就是 RT进程的熔断机制。从左图中可以看出线程1跑到一半,被线程2抢占了,线程2运行完退出,继续线程1,然后线程4和线程3又把1抢断了(这里就是5%的时间让给普通线程)。
Linux线程调度策略和优先级_第2张图片Linux线程调度策略和优先级_第3张图片

总结

linux线程调度碰到的一些基本问题在这里做一个总结,还有更多东西后续学习或接触到再进行补充。


参考:https://zhuanlan.zhihu.com/p/387806696
https://blog.csdn.net/Adrian503/article/details/109714119

你可能感兴趣的:(Linux,linux)