5:linux内核调度的机制taskletworkqueuekthread_workerk。。。
前⾔:
linux下的sleep函数⼀直就感觉linux下⾯的任务调度机制太丰富了,由于各种调度机制平时⼯作中只是要⽤,理解并不是那么深刻,所有有时候说不上道道来,只知道这个要⽤softirq/tasklet/workqueue/thread/, workqueue的优先级要设置成system_wq,system_highpri_wq,
system_unbound_wq 或者thread 的SCHED_RR/SCHED_FIFO这样⼦,说实话,现在我也不能保证说概述的很全很准确(有不对地⽅,欢迎⼤家指出)。就期待后⾯可以慢慢完善,读者如果有建议补充的可以提建议,我们⼀起不断更新这篇⽂章,⼀起努⼒可以把linux 线程相关的东西吃懂吃透,⽬的是争取为社区贡献⼀篇好⽂章。
闲话少说:先简述吧
第1章:linux常见的任务调度的机制
1.1 softirq(不允许休眠阻塞,中断上下⽂)
软终端⽀持SMP,同⼀个softirq可以在不同CPU同时运⾏,必须是可重⼊的,是编译期间静态分配的,
不想tasklet⼀样能被动态注册,删除(个⼈感觉因为不⽅便,所以使⽤较少)。kernel/softirq.c⽂件中定义了⼀个包含32个softirq_action结构体的数组,每个被注册的软终端都占据该数组的⼀项,因此最多可能有32个软中断。
特性:
1)⼀个软中断不会抢占另⼀个软中断
2)唯⼀可以抢占软中断的是ISR(中断服务程序)
3)其它软中断可以在其它处理器同时执⾏
4)⼀个注册的软中断必须被标记后才执⾏
5)软中断不可以⾃⼰休眠(不能⾃⼰调⽤sleep,wait等函数)
6)索引号⼩的软中断在索引号⼤的软中断之前执⾏
1.2 tasklet(不允许休眠阻塞,中断上下⽂)
中断服务程序⼀般都是在中断请求关闭的条件下执⾏的,以避免嵌套⽽使中断控制复杂化。但是,中
断是⼀个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从⽽造成中断的丢失。因此,Linux内核的⽬标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。例如,假设⼀个数据块到达触发中断时,中断控制器接受到这个中断请求信号时,Linux内核只是简单地标志数据到来了,然后让处理器恢复到它以前运⾏的状态,其余的处理稍后再进⾏(如把数据移⼊⼀个缓冲区,接受数据的进程就可以在缓冲区到数据)。因此,内核把中断处理分为两部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中断服务程序)内核⽴即执⾏,⽽下半部(就是⼀些内核函数)留着稍后处理,⾸先,⼀个快速的“上半部”来处理硬件发出的请求,它必须在⼀个新的中断产⽣之前终⽌。通常,除了在设备和⼀些内存缓冲区(如果你的设备⽤到了DMA,就不⽌这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这⼀部分做的⼯作很少。下半部运⾏时是允许中断请求的,⽽上半部运⾏时是关中断的,这是⼆者之间的主要区别
中断bai处理的tasklet(⼩任务)机制
特性:
1)不允许两个相同的tasklet绝对不会同时执⾏,即使在不同CPU上。
2)从softirq衍⽣,但是使⽤简单,效率⾼(较softirq低,较workqueue⾼),常⽤
3)在中断期间运⾏时, 即使被多次调⽤也只会执⾏⼀次
4)SMP系统上,可以确保在第⼀个调⽤它的CPU执⾏
1.3 workqueue(允许休眠阻塞,进程上下⽂)
⼯作队列(work queue)是另外⼀种将⼯作推后执⾏的形式,它和前⾯讨论的tasklet有所不同。⼯作队列可以把⼯作推后,交由⼀个内核线程去执⾏,也就是说,这个下半部分可以在进程上下⽂中执⾏。这样,通过⼯作队列执⾏的代码能占尽进程上下⽂的所有优势。最重要的就是⼯作队列允许被重新调度甚⾄是睡眠。
1)⼯作队列会在进程上下⽂中执⾏
2)可以阻塞
3)可以重新调度
4)缺省⼯作者线程(kthrerad worker && kthread work)
5)在⼯作队列和其它内核间⽤锁和其它进程上下⽂⼀样
6)默认允许响应中断
7)默认不持有任何锁
那么,什么情况下使⽤⼯作队列,什么情况下使⽤tasklet。如果推后执⾏的任务需要睡眠,那么就选择⼯作队列。如果推后执⾏的任务不需要睡眠,那么就选择tasklet。另外,如果需要⽤⼀个可以重新调度的实体来执⾏你的下半部处理,也应该使⽤⼯作队列。它是唯⼀能在进程上下⽂运⾏的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得⼤量的内存时、在需要获取信号量时,在需要执⾏阻塞式的
I/O操作时,它都会⾮常有⽤。如果不需要⽤⼀个内核线程来推后执⾏⼯作,那么就考虑使⽤tasklet。
1.4 kthread
Linux内核可以看作⼀个服务进程(管理软硬件资源,响应⽤户进程的种种合理以及不合理的请求)。内核需要多个执⾏流并⾏,为了防⽌可能的阻塞,⽀持多线程是必要的。内核线程就是内核的分⾝,⼀个分⾝可以处理⼀件特定事情。内核线程的调度由内核负责,⼀个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。这与⽤户线程是不⼀样的。因为内核线程只运⾏在内核态,因此,它只能使⽤⼤于PAGE_OFFSET(3G)的地址空间。内核线程和普通的进程间的区别在于内核线程没有独⽴的地址空间,mm指针被设置为NULL;它只在 内核空间运⾏,从来不切换到⽤户空间去;并且和普通进程⼀样,可以被调度,也可以被抢占。
内核线程(thread)或叫守护进程(daemon),在操作系统中占据相当⼤的⽐例,当Linux操作系统启动以后,你可以⽤”ps -ef”命令查看系统中的进程,这时会发现很多以”d”结尾的进程名,确切说名称显⽰⾥⾯加 "[]"的,这些进程就是内核线程。
内核线程和普通的进程间的区别在于内核线程没有独⽴的地址空间,它只在 内核空间运⾏,从来不切换到⽤户空间去;并且和普通进程⼀样,可以被调度,也可以被抢占。让模块在加载后能⼀直运⾏下去的⽅法——内核线程。要创建⼀个内核线程有许多种⽅法。
第2章:linux常见的任务调度的优先级
2.1 workqueue(include/linux/workqueue.h)与waitequeue协作(include/linux/wait.h)
使⽤⽰例
定义workqueue
struct work_struct retreive_frame_work;
初始化workqueue和waitqueue及workqueue的callback的定义
INIT_WORK(&retreive_frame_work, retrieve_desc_task_callback);
init_waitqueue_head(&frame_wq);
void retrieve_desc_task_callback(struct work_struct *work){
add_desc(iav,0);//⽣产frame_desc
wake_up_interruptible_all(&frame_wq);//通知消费者,frame_desc已经⽣产完毕,可以去取了
}
中断ISR中:唤醒workqueue
queue_work(system_wq,&retreive_frame_work);
消费由 retrieve_desc_task_callback ⽣产的 frame_desc(blocking 的⽅式)
wait_event_interruptible(frame_wq,(desc =find_frame_desc()));
2.2 kthread(include/linux/kthread.h)
常见的优先级如下:SCHED_OTHER、SCHED_RR、SCHED_FIFO
本⼩节会主要介绍SCHED_RR和SCHED_FIFO这两种调度策略,因为SCHED_OTHER其实就是默认的调度机制,也就是说该咋咋滴,能被⾼优先级抢占,其实优先级就是最低的,属于⾦字塔⾷物链底端,谁都可以欺负它。谁优先级⾼就谁吃cpu cycle,同优先级就是linux来调度,轮着吃⾼优先级schedule 出来的cpu cycle。
SCHED_RR、SCHED_FIFO对⽐
对⽐才有伤害,是骡⼦是马都得见婆婆,丑媳妇也得拉出来溜溜。先上图再解析,直截了当。
说实话,我是真的懒,看到好图直接盗图了,开源精神有。如作者不允许到请联系我,我⾃⼰画⼀个哈。
参考链接: .
图参考链接: .
SCHED_RR的任务调度时间⽚如下图1
SCHED_FIFO任务调度时间⽚如下图2
剖析:这⾥内核创建了四个优先级相同的线程,对于SCHED_RR⽽⾔是每个任务有⼀个特定的时间⽚,轮转依次运⾏,⽽SCHED_FIFO是⼀个任务执⾏完再去执⾏下⼀个任务(优先级⾼),其执⾏顺序是创建的先后。SCHED_RR是依据时间⽚来调度线程的,当时间⽚⽤完后,⽆论该线程优先级多⾼,都不会再执⾏,⽽是进⼊就绪队列,等待下⼀个时间⽚到来。只是图1显⽰,在thread5798时间⽚⽤完时,该线程紧接着进⾏了⼀次抢占preemption。⼜获得了⼀个时间⽚。顺便提⼀句时间⽚长度的定位是linux凭经验来的。即选择尽可能长、同⼀时候能保持良好对应时间的⼀个时间⽚。
SCHED_FIFO:先进先出调度
SCHED_FIFO线程的优先级必须⼤于0,当它运⾏时,⼀定会抢占正在运⾏的普通策略的线程(SCHED_OTHER, SCHED_IDLE,
SCHED_BATCH);SCHED_FIFO策略是没有时间⽚的算法,需要遵循以下规则:
1)如果⼀个SCHED_FIFO线程被⾼优先级线程抢占了,那么它将会被添加到该优先级等待列表的⾸部,以便当所有⾼优先级的线程阻塞的时候得到继续运⾏;
2)当⼀个阻塞的SCHED_FIFO线程变为可运⾏时,它将被加⼊到同优先级列表的尾部;
3)如果通过系统调⽤改变线程的优先级,则根据不同情况有不同的处理⽅式:
a)如果优先级提⾼了,那么线程会被添加到所对应新优先级的尾部,因此,这个线程有可能会抢占当前运⾏的同优先级的线程;
b)如果优先级没变,那么线程在列表中的位置不变;
c)如果优先级降低了,那么它将被加⼊到新优先级列表的⾸部;
根据POSIX.1-2008规定,除了使⽤pthread_setschedprio(3)以外,通过使⽤其他⽅式改变策略或者优先级会使得线程加⼊到对应优先级列表的尾部;
4)如果线程调⽤了sched_yield(2),那么它将被加⼊到列表的尾部;
SCHED_FIFO会⼀直运⾏,直到它被IO请求阻塞,或者被更⾼优先级的线程抢占,亦或者调⽤了sched_yield();
5) 处于可运⾏状态的SCHED_FIFO级的进程会⽐任何SCHED_NORMAL级的进程都先得到调⽤
6) ⼀旦⼀个SCHED_FIFO级进程处于可执⾏状态,就会⼀直执⾏,直到它⾃⼰受阻塞或显式地释放处理器为⽌,它不基于时间⽚,可以⼀直执⾏下去
7) 只有更⾼优先级的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务
如果有两个或者更多的同优先级的SCHED_FIFO级进程,它们会轮流执⾏,但是依然只有在它们愿意让出处理器时才会退出
8) 只要有SCHED_FIFO级进程在执⾏,其他级别较低的进程就只能等待它变为不可运⾏态后才有机会执⾏
SCHED_RR:轮转调度
1. SCHED_RR是SCHED_FIFO的简单增强,除了对于线程占⽤的时间总量之外,对于SCHED_FIFO适⽤的规则对于SCHED_RR同样
适⽤;如果SCHED_RR线程的运⾏时间⼤于等于时间总量,那么它将被加⼊到对应优先级列表的尾部;如果SCHED_RR线程被抢占了,当它继续运⾏时它只运⾏剩余的时间量;时间总量可以通过sched_rr_get_interval()函数获取;
2. 当SCHED_RR任务耗尽它的时间⽚时,在同⼀优先级的其他实时进程被轮流调度
3. 时间⽚只⽤来重新调度同⼀优先级的进程
4. 对于SCHED_FIFO进程, 优先级总是⽴即抢占低优先级,但低优先级进程决不能抢占SCHED_RR任务,即使它的时间⽚耗尽SCHED_OTHER:默认Linux时间共享调度
SCHED_OTHER只能⽤于优先级为0的线程,SCHED_OTHER策略是所有不需要实时调度线程的统⼀标准策略;调度器通过动态优先级来决定调⽤哪个SCHED_OTHER线程,动态优先级是基于nice值的,nice值随着等待运⾏但是未被调度执⾏的时间总量的增长⽽增加;这样的机制保证了所有SCHED_OTHER线程调度的公平性
MAX_RT_PRIO:实时优先级
1) 实时优先级范围从0到MAX_RT_PRIO减1
2) 默认情况下,MAC_RT_RTIO为100——所以默认的实时优先级范围从0到99
3) SCHED_NORMAL级进程的noce值共享了这个取值空间。它的取值范围从MAC_RT_PRIO到(MAX_RT_PRIO+40)。也就是说,在默认情况下,nice值从-20到+19直接对应的是从100到139的实时优先级范围
4) struct sched_param param = { .sched_priority = MAX_RT_PRIO },最后优先级priority是越⼩越好,最后的值会转换为priority = n - sched_priority,所以sched_priority 越⼤,则其实优先级越⾼。
限制实时线程的CPU使⽤时间
SCHED_FIFO, SCHED_RR的线程如果内部是⼀个⾮阻塞的死循环,那么它将⼀直占⽤CPU,使得其它线程没有机会运⾏;
在2.6.25以后出现了限制实时线程运⾏时间的新⽅式,可以使⽤RLIMIT_RTTIME来限制实时线程的CPU占⽤时间;Linux也提供了两个proc⽂件,⽤于控制为⾮实时线程运⾏预留CPU时间;
/proc/sys/kernel/sched_rt_period_us
这个⽂件中的数值指定了总CPU(100%)时间的宽度值,默认值是1,000,000;
/proc/sys/kernel/sched_rt_runtime_us
这个⽂件中的数值指定了实时线程可以运⾏的CPU时间宽度,如果设置为-1,则认为不给⾮实时线程预留任何运⾏时间,默认值是
950,000,因为第⼀个⽂件的总量是1,000,000,也就是说默认配置为⾮实时线程预留了5%的CPU时间;
使⽤⽰例
现在有⼀个需求kernel中,共⼀个SOF(start of frame)中断唤醒thread A, thread A 去唤醒 thread B, thread A 和 B 执⾏完就分别进休眠状态,从中断到thread B 被唤醒必须在4ms内(即使在arm的loading很重的时候)(具体原因是码流的
idsp/iso/shutter/again/dgain等参数必须要在⼀帧内下进去,⽽sensor信号时I2C发送的,如果fps = 120,⼀帧数据传输+I2C stop 和start信号之间的间隙约8ms,所以要尽可能间断其时从SOF 中断进来,到更新参数thread被唤醒的时间,现预估4ms内,实际应该更⼩),thread A 去apply参数,thread B 去update参数,具体流程是:
1. ⼀开始有⼀个准备好的参数-》 SOF中断trigger唤醒thread A 去apply 参数-》 thread A 去唤醒thread B 去更新参数:
2. 同时有其它⼀个线程C⼀直更新参数,thread B 唤醒时,可以去拿参数。
初始化线程
inline void vin_create_kthreads(struct vin_device *vdev)
{
struct sched_param sched_param ={.sched_priority = MAX_RT_PRIO /2};
if(!thread_B.kthread){
sema_init(&sem_b,0);
thread_B.kthread =kthread_run(update_sht_agc_task, vdev,"update_sht_agc");
sched_setscheduler(thread_B.kthread, SCHED_FIFO,&sched_param);
}
if(!thread_A.kthread){
sema_init(&sem_a,0);
thread_A.kthread =kthread_run(apply_sht_agc, vdev,"apply_sht_agc");
sched_setscheduler(thread_A.kthread, SCHED_FIFO,&sched_param);
}
return;
}
apply参数线程的结构体
struct apply_sht_agc {
struct task_struct *kthread;
struct semaphore sem;
u32 exit_kthread :1;
u32 reserved0 :31;
};
SOF中断唤醒apply 参数线程
#include<linux/time.h>
long ns_irq =0;
struct timespec64 tstart_irq;
idsp_sof_irq {
atomic_set(&vinc->wait_sof,0);
ktime_get_real_ts64(&tstart_irq);
ns_irq = tstart_irq.tv_nsec;
up(&vsem_a);
}
apply参数线程,执⾏结束唤醒更新参数线程
#include<linux/time.h>
extern struct timespec64 tstart_irq;
extern long ns_irq;
long ns_task =0;
u8 debug_times =0;
int apply_shutter_agc_task(void*arg)
{
while(!kthread_should_stop()){
if(it_kthread){
continue;
}
ktime_get_real_ts64(&tstart_irq);
ns_task = tstart_irq.tv_nsec;
if(ns_task - ns_irq >2000000|| debug_times <2){
iav_debug("ns_task = %ld, ns_irq = %ld\n", ns_task, ns_irq); debug_times++;
}
if(down_interruptible(&sem_a)){
continue;
}
//apply 参数
up(&sem_b);
wake_up_interruptible_all(&a->sht_agc_wq);
return0;
}
更新参数线程
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论