linux-中断下半部

引用

  • preempt

  • softirq

  • tasklet

  • workqueue

  • timer


一. 为什么要有上下半部

中断分成上下半部处理可以提高中断的响应能力,在上半部处理完成后便将cpu中断打开(通常上半部处理越快越好),这样就可以响应其他中断了,等到中断退出的时候再进行下半部的处理。

二. preempt_count

  • task_struct结构体中的thread_info.preempt_count用于记录当前任务所处的context状态;

  • PREEMPT_BITS

  • 用于记录禁止抢占的次数,禁止抢占一次该值就加1,使能抢占该值就减1;

  • SOFTIRQ_BITS

  • 用于同步处理,关掉下半部的时候加1,打开下半部的时候减1;

  • HARDIRQ_BITS

  • 用于表示处于硬件中断上下文中;

  • in_softirq和in_serving_softirq都表示处于softirq上下文,但并不意味着程序正在执行软中断,区别是:

  • in_serving_softirq表示 当前一定有软中断处于执行状态。(bit8 - SOFTIRQ_OFFSET)

  • in_softirq 除了可以表示当前有软中断处于执行状态,还有可能表示当前的context只是disable软中断的thread上下文。(例如:local_bh_disable()下的context)

  • 中断上下文 - interrupt context

  • 我们将 NMI, HARDIRQ, SOFTIRQ 上下文 统称为中断上下文。

  • 可用 in_interrupt() 判断

  • 进程上下文 - process context

  • 与中断上下文相对应。

  • 可用 in_task() 判断

  • 原子上下文 - atomic context

  • 不能发生进程睡眠或者调度的上下文。

  • 处于中断上下文,或者显示地禁止了调度,preempt_count()的值都不为0,都不允许睡眠/调度的发生,这两种场景被统称为atomic上下文。

  • 可用 in_atomic() 来判断当前cpu是否处于atomic上下文。

  • 也就是非 preempt_count 非 0 时,都属于 atomic 上下文,其中包括中断、软中断等中断上下文,还包括进程或者内核线程运行时关中断或者关抢占。

  • 由于该接口在有些场景下不能精确检测,所以 不推荐在driver中使用。

  • 三种上下文的关系

三. softirq

softirq是静态的,不支持动态分配。

  • 相关数据结构

/* 支持的软中断类型,可以认为是软中断号, 其中从上到下优先级递减 */
enum
{
	HI_SOFTIRQ=0,       /* 最高优先级软中断 */
	TIMER_SOFTIRQ,      /* Timer定时器软中断 */
	NET_TX_SOFTIRQ,     /* 发送网络数据包软中断 */
	NET_RX_SOFTIRQ,     /* 接收网络数据包软中断 */
	BLOCK_SOFTIRQ,      /* 块设备软中断 */
	IRQ_POLL_SOFTIRQ,   /* 块设备软中断 */
	TASKLET_SOFTIRQ,    /* tasklet软中断 */
	SCHED_SOFTIRQ,      /* 进程调度及负载均衡的软中断 */
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on thenumbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq, RCU相关的软中断 */

	NR_SOFTIRQS
};

/* 软件中断描述符,只包含一个handler函数指针 */
struct softirq_action {
	void	(*action)(struct softirq_action *);
};

/* 软中断描述符表,实际上就是一个全局的数组 */
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

/* CPU软中断状态描述,当某个软中断触发时,__softirq_pending会置位对应的bit */
typedef struct {
	unsigned int __softirq_pending;
	unsigned int ipi_irqs[NR_IPI];
} ____cacheline_aligned irq_cpustat_t;

/* 每个CPU都会维护一个状态信息结构 */
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

/* 内核为每个CPU都创建了一个软中断处理内核线程 */
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
  • 数据结构关系图

  • softirq_vec[]数组,类比硬件中断描述符表irq_desc[],通过软中断号可以找到对应的handler进行处理,比如图中的tasklet_action就是一个实际的handler函数;

  • 软中断可以在不同的CPU上并行运行,在同一个CPU上只能串行执行;(即软中断不保证重入问题

  • 每个CPU维护irq_cpustat_t状态结构,当某个软中断需要进行处理时,会将该结构体中的__softirq_pending字段或上1UL << XXX_SOFTIRQ

  • 软中断的触发点

  • raise_softirq()/raise_softirq_irqoff() 会设置当前本地cpu的irq_stat中的 __softirq_pending字段,并将相应的软中断号置位,即表明该软中断有处理请求。

  • 软中断执行点

  • 中断处理后;

  • bottom-half enable后;

  • 思考

为什么在使能Bottom-half时要进行软中断处理呢?

==》

在并发处理时,可能已经把Bottom-half进行关闭了,如果此时中断来了后,软中断不会被处理,在进程上下文中打开Bottom-half时,这时候就会检查是否有软中断处理请求了;

四. tasklet

tasklet是软中断的一种类型,那么两者有啥区别呢?

  • 软中断类型内核中都是静态分配,不支持动态分配,而tasklet支持动态和静态分配,也就是驱动程序中能比较方便的进行扩展;

  • 软中断可以在多个CPU上并行运行,因此需要考虑可重入问题,而tasklet会绑定在某个CPU上运行,运行完后再解绑,不要求重入问题,当然它的性能也就会下降一些;

  • DEFINE_PER_CPU(struct tasklet_head, tasklet_vec)为每个CPU都分配了tasklet_head结构,该结构用来维护struct tasklet_struct链表,需要放到该CPU上运行的tasklet将会添加到该结构的链表中,内核中为每个CPU维护了两个链表tasklet_vectasklet_vec_hi,对应两个不同的优先级,本文以tasklet_vec为例;

  • struct tasklet_structtasklet的抽象,几个关键字段如图所示,通过next来链接成链表,通过state字段来标识不同的状态以确保能在CPU上串行执行,func函数指针在调用task_init()接口时进行初始化,并在最终触发软中断时执行;

  • 接口

/* 静态分配tasklet */
DECLARE_TASKLET(name, func, data)

/* 动态分配tasklet */
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

/* 禁止tasklet被执行,本质上是增加tasklet_struct->count值,以便在调度时不满足执行条件 */
void tasklet_disable(struct tasklet_struct *t);

/* 使能tasklet,与tasklet_diable对应 */
void tasklet_enable(struct tasklet_struct *t);

/* 调度tasklet,通常在设备驱动的中断函数里调用 */
void tasklet_schedule(struct tasklet_struct *t);

/* 杀死tasklet,确保不被调度和执行, 主要是设置state状态位 */
void tasklet_kill(struct tasklet_struct *t);

五. workqueue/delay wrokqueue

  • CWMQ (concurrency-managed workqueues)解决了什么问题?

  • 内核线程数量太多。虽然系统中默认有一套工作线程,但有很多工程师系统喜欢自行创建工作线程,对于CPU数量比较多的机器,系统启动完可能就耗尽了PID资源。

  • 并发性比较差。Multi threaded的工作线程和CPU是一一绑定的,某个本地CPU上的工作任务是串行执行的。如某个线程上的工作任务发生了睡眠,之后的工作任务只能等待,前一个任务没有执行完成,则后一个任务永远无法得到执行,也不能迁移到其他空闲CPU上执行。

  • 死锁问题。如果有很多的工作任务运行在系统默认的工作队列上,并且他们有一些数据依赖关系,那么很有可能产生死锁问题。

  • 为了解决上述问题,Linux内核引入了concurrency-managed workqueues(CWMQ),和旧的workqueue接口兼容,明确划分了workqueue的前端接口和后端实现机制。CWMQ提出了工作线程池(worker_pool)概念,不和特定的工作队列关联。工作线程池有两种。一种和具体CPU绑定,为Per-CPU类型,有两个线程池,一个给高优先级的work使用,另一个给低优先级的work使用。另外一种不和具体CPU绑定,可以运行在任意CPU上,工作线程池中的线程是动态分配和管理,线程数量不固定,缺省情况下会创建一个线程来处理工作任务。用户在使用workqueue时,无需关心放在哪个CPU上执行,也无需再创建额外的线程,只需要设置相关flag和优先级,内核会将工作任务放在合适的线程池中执行。当某个工作任务阻塞时,CWMQ会唤醒或创建新的线程执行后续的工作任务,以提高并发效率。

  • linux workqueue机制有多个woker 线程?

  • 内核的CMWQ有多个worker_pool,管理多个worker。

  • 初始化时,会给每个CPU都有创建两个worker_pool,对应不同的优先级,nice值分别为0和-20 <==> 针对bound类型的workqueue,其提交的work会被派发到上面类型的worker_pool中被执行。

  • 初始化时,会默认创建unbound workqueue属性(struct workqueue_attrs),主要描述内核线程的nice,cpumask,no_numa;默认有创建 unbound and ordered (unbound_std_wq_attrs, ordered_wq_attrs) 两种类型的wq attrs,且每种类型有两个实例,区别是nice值不一样,一个是0,一个是-20;<==> 当创建unbound workqueue时,会根据workqueue_attrs来判断是否已经有创建好的worker_pool,如果没有,则创建新的worker_pool并挂入到unbound_pool_hash 中。其中,可指定 __WQ_ORDERED来表明使用 ordered_wq_attrs,否则,使用 unbound_std_wq_attrs;(即相同属性的workqueue使用相同的worker_pool,不同属性的workqueue使用不同的worker_pool,如果woekqueue相关属性没有对应的worker_pool,则会动态创建一个, 即unbound的worker_pool会随着不同属性的workqueue的创建而动态创建。)

  • 在初始化时workqueue_init(), 会为每个worker_pool中创建一个worker,后续会根据需要work的数量和worker的数量进行动态创建和销毁;

  • create_worker函数中,创建的内核线程名字为kworker/XX:YY, kworker/XX:YYH或者kworker/uXX:YY

  • bound: XX表示cpu的编号,YY表示worker的编号,H表示是high prio的worker;

  • unbound: u表示unbound,XX表示worker_pool的编号,YY表示worker的编号;

  • bound和un-bound workqueue的区别?

  • bound:绑定处理器的工作队列,其会被bound的worker_pool服务,该worker_pool是绑定cpu的,即创建的worker内核线程会被绑定到特定的CPU上运行;

  • unbound:不绑定处理器的工作队列,其会被un-bound的worker_pool服务,创建的时候需要指定WQ_UNBOUND标志,内核线程可以在处理器间迁移;

  • 何时创建更多的worker?

  • worker线程worker_thread() 运行时,如果没有空闲idle的worker,会调用manage_workers()接口来创建更多的worker来处理工作;

  • 即一个worker_pool会保证至少有一个idle状态的worker存在,以保证worker_pool的并发能力。

  • 何时销毁多余的worker?

  • 一个worker被创建后,首先进入worker_enter_idle(),里面启动了pool->idle_timer,定时IDLE_WORKER_TIMEOUT即300HZ。如果一个worker进入idle超过300HZ,即会执行idle_worker_timeout(),会根据情况进行销毁多余的worker。

  • 销毁策略 too_many_workers()

  • 满足idle worker数量大于2;

  • 除去两个idle worker线程外的idle worker不能超过busy worker的1/4。

  • 所以每个worker_pool最少两个worker线程。例如,如果worker_pool中有4个worker(3idle+1busy),则3>2,并且(3-2)*4>1,即会选择一个idle销毁。

  • 如何解决一个work阻塞或者死锁了,导致其他的work得不到执行,即各种work之间的互相影响?

  • 在worker线程执行时,会尝试进行worker_pool管理工作 manage_workers() ,即会检查worker_pool中是否有至少一个idle状态的worker,如果没有,则创建一个新的worker。

  • 启动一个100ms的定时器worker_pool->mayday_timer,检查当前worker_pool中是否存在allocation deadlock异常(即因为没有内存等资源,导致该workqueue对应的worker_pool创建新的worker失败),如果有异常,则如果该workqueue指定了WQ_MEM_RECLAIM,则会唤醒该workqueue对应rescuer_thread 进行处理,否则,什么也做不了。

  • 管理worker_pool的内核线程池时,如果有PENDING状态的work,并且发现没有正在运行的工作线程(worker_pool->nr_running == 0),唤醒空闲状态的内核线程,或者动态创建内核线程;

  • 如果work已经在同一个worker_pool的其他worker中执行,不再对该work进行处理(防重入处理);

  • __queue_owrk() 是如何工作的?

  • __queue_owrk() 的工作是将work添加到对应的woker_pool->worklist或者pool_workqueue->delayed_works, 如果没有runing的worker,则唤醒一个idle的worker,详细步骤:

  1. 确定pool_workqueue,一个pool_workqueue对应一个worker_pool,因此确定了pool_workqueue也就确定了worker_pool;

  • pool_workqueue分为三种情况:

  • bound类型的工作队列,直接根据CPU号获取(可指定cpu,如果没有指定,则用当前cpu。);

  • unbound类型的工作队列,根据cpu所在的node号获取,针对unbound类型工作队列,pool_workqueue的释放是异步执行的,需要判断refcnt的计数值,因此在获取pool_workqueue时可能要多次retry;

  • 根据缓存热度,优先选择正在被执行的worker_pool(所以,指定了cpu也不一定生效)

  1. 判断pool_workqueue 中nr_active work数量,如果少于max_active,则将work加入到pool->worklist中,否则,加入到pwq->delayed_works链表中;

  1. 插入work到上面选择的list中,如果没有runing的worker,则唤醒一个idle的worker。

  • WORK_STRUCT_PENDING_BIT何时被设置以及被清0?

  • 当一个work已经加入到workqueue队列中,schedule_work()->queue_work()->queue_work_on()时被设置。

  • 当一个work在工作线程里马上要执行(即执行work之前),worker_thread()->process_on_work()->set_work_pool_and_clear_pend是清0。

  • 上述设置和清0都是在关闭本地中断情况下执行的。

  • 如何判断一个work是否已经全部执行完成?

  • 在目标 work 的后面插入一个新的 work( wq_barrier ),如果 wq_barrier 执行完成,那么目标 work 肯定已经执行完成。

  • 如何指定一个work跑到特定的cpu上?

  • queue_work_on()可以指定cpu id,但是,不一定能指定成功,策略如下:

  • 如果一个work正在一个worker_pool中执行,则本次work也会被放到正在执行的worker_pool中,是为了利用缓存,提高效率。

  • 如果该work此时没有被执行,则会被放到指定的worker_pool->worklist中,等待执行。

  • 如果一个worker_pool中的worker要进入睡眠了,如何保证其余的work能够被执行?

  • worker会标志 该内核线程为workqueue worker。PF_WQ_WORKER

  • 当一个worker线程要被调度器换出时,调度器 __schedule() 会调用wq_worker_sleeping()看看是否需要唤醒同一个线程池中的其它内核线程。

  • workqueue lockup机制是怎么样的?

  • 会用一个timer 检查一个work执行了 30s都没有退出,就会打印warning log。

  • WQ_MEM_RECLAIM 标志表示什么意思?

  • 创建工作队列workqueue时,如果设置了WQ_MEM_RECLAIM标志,则会新建rescuer worker,对应rescuer_thread内核线程。

  • 目的是当内存紧张时,新创建worker可能会失败,这时候由rescuer来处理这种情况;

  • alloc_workqueue()中的max_active是什么意思,作用是什么?

  • 决定该workqueue中,在每个cpu上最多有多少个不同work可以被挂入worker_pool->worklist中,多余的work将被挂入到对应pool_workqueue->delayed_works中,等待延迟处理;例如,在4核的cpu上,max_acitve=2,即表示该workqueue可以在每个cpu的worker_pool上最多挂2个work。

  • 对于BOUND类型的工作队列,max_active最大可以是512,如果max_active被设置为0,则表示指定max_active为256.对于UNBOUND类型工作队列,max_active最大值为512和4*num_possible_cpus之间的最大值。

  • 表示该workqueue中最多能有几个work被挂入到对应的 pwq->pool->worklist中,即能被对应worker_pool的worker同时服务。

  • 当进行 queue_work_on()时,会根据 选择的 pool_workqueue的 pwq->nr_active和pwq->max_active大小来 选择将work挂入到pwq->delayed_works,还是挂入到pwq->pool->worklist中。

  • 如果挂入到pwq->pool->worklist中,则意味着该work进入active状态,worker_pool 的worker就可以进行处理;

  • 如果挂入到pwq->delayed_works,则该work还是pending状态(WORK_STRUCT_DELAYED),不会立即被worker执行;只有当一个 worker处理完一个work (process_one_work())时,会检查该work对应的pwq中 的nr_active和max_active,如果nr_active<max_active,则会激活pwq->delayed_works中的一个work (修改WORK_STRUCT_DELAYED,并移动到pwq->pool->worklist),即该work可以被worker执行;

  • 如何实现order workqueue?

  • 必须是unbound workqueue;

  • max_active设置为1;

  • 怎样保证同一个work不被重复执行?

  • work会有一个 WORK_STRUCT_PENDING_BIT,该标志 会在queue_work_on()是被设定,在执行该work前被清除。所以,作用是 除了该work正在被执行,最多还允许该work被pending在queue中。

  • worker_thread() 执行work时,如果一个worker发现自己要处理的work正在被另外一个worker线程处理,那么本worker线程将不处理该work,只需要挂入正在执行该work的worker线程的scheduled work list即可。

  • 数据结构

  • work_struct:工作队列调度的最小单位,work item

  • workqueue_struct:工作队列,兼容之前的接口;

  • workerwork item的处理者,每个worker对应一个内核线程;

  • worker_poolworker池(内核线程池),是一个共享资源池,提供不同的worker来对work item进行处理;

  • pool_workqueue:充当桥梁纽带的作用,用于连接workqueueworker_pool,建立链接关系;

六. timer

6.1 overall

6.2 源码文件

linux kernel 时间子系统的源文件位于linux/kernel/time/目录下,我们整理如下:

文件名

描述

time.c

timeconv.c

time.c文件是一个向用户空间提供时间接口的模块。具体包括:time, stime, gettimeofday, settimeofday,adjtime。除此之外,该文件还提供一些时间格式转换的接口函数(其他内核模块使用),例如jiffes和微秒之间的转换,日历时间(Gregorian date)和xtime时间的转换。xtime的时间格式就是到linux epoch的秒以及纳秒值。

timeconv.c中包含了从calendar time到broken-down time之间的转换函数接口。

timer.c

传统的低精度timer模块,基本tick的。

time_list.c

timer_status.c

向用户空间提供的调试接口。在用户空间,可以通过/proc/timer_list接口可以获得内核中的时间子系统的相关信息。例如:系统中的当前正在使用的clock source设备、clock event设备和tick device的信息。通过/proc/timer_stats可以获取timer的统计信息。

hrtimer.c

高精度timer模块

itimer.c

interval timer模块

posix-timers.c

posix-cpu-timers.c

posix-clock.c

POSIX timer模块和POSIX clock模块

alarmtimer.c

alarmtimer模块

clocksource.c

jiffies.c

clocksource.c是通用clocksource driver。其实也可以把system tick也看成一个特定的clocksource,其代码在jiffies.c文件中

timekeeping.c

timekeeping_debug.c

timekeeping模块

ntp.c

NTP模块

clockevent.c

clockevent模块

tick-common.c

tick-oneshot.c

tick-sched.c

这三个文件属于tick device layer。

tick-common.c文件是periodic tick模块,用于管理周期性tick事件。

tick-oneshot.c文件是for高精度timer的,用于管理高精度tick时间。

tick-sched.c是用于dynamic tick的。

tick-broadcast.c

tick-broadcast-hrtimer.c

broadcast tick模块。

sched_clock.c

通用sched clock模块。这个模块主要是提供一个sched_clock的接口函数,调用该函数可以获取当前时间点到系统启动之间的纳秒值。

底层的HW counter其实是千差万别的,有些平台可以提供64-bit的HW counter,因此,在那样的平台中,我们可以不使用这个通用sched clock模块(不配置CONFIG_GENERIC_SCHED_CLOCK这个内核选项),而在自己的clock source chip driver中直接提供sched_clock接口。

使用通用sched clock模块的好处是:该模块扩展了64-bit的counter,即使底层的HW counter比特数目不足(有些平台HW counter只有32个bit)。

6.3 常用kernel API

timer.h - include/linux/timer.h - Linux source code (v6.2.5) - Bootlin

#define DEFINE_TIMER(_name, _function)                \
    struct timer_list _name =                \
        __TIMER_INITIALIZER(_function, 0)

#define timer_setup(timer, callback, flags)            \
    __init_timer((timer), (callback), (flags))

#define from_timer(var, callback_timer, timer_fieldname) \
    container_of(callback_timer, typeof(*var), timer_fieldname)

#define timer_setup_on_stack(timer, callback, flags)        \
    __init_timer_on_stack((timer), (callback), (flags))

#define from_timer(var, callback_timer, timer_fieldname) \
    container_of(callback_timer, typeof(*var), timer_fieldname)

static inline int timer_pending(const struct timer_list * timer);

extern void add_timer_on(struct timer_list *timer, int cpu);
extern int mod_timer(struct timer_list *timer, unsigned long expires);
extern int mod_timer_pending(struct timer_list *timer, unsigned long expires);
extern int timer_reduce(struct timer_list *timer, unsigned long expires);

extern void add_timer(struct timer_list *timer);

extern int try_to_del_timer_sync(struct timer_list *timer);
extern int timer_delete_sync(struct timer_list *timer);
extern int timer_delete(struct timer_list *timer);
extern int timer_shutdown_sync(struct timer_list *timer);
extern int timer_shutdown(struct timer_list *timer);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值