当前位置: 首页 > news >正文

softirq

linux内核将中断分为上下两部分,在中断发生后马上就需要执行的那部分工作属于中断上半部,比如驱动程序通过request_irq注册的中断处理函数,而稍微延后执行的那部分工作就属于中断下半部,比如softirqtaskletworkqueue等。在中断上半部执行期间,中断通常是被禁用的,为了避免中断被长时间禁用,上半部的代码通常只完成一些必要的处理,之后便会快速的退出。而在中断下半部执行期间,中断通常是开启的,CPU可以响应中断,所以,下半部通常用于处理那些量大又耗时的工作。接下来的篇幅中将简单分析softirq的工作过程。

中断发生后,内核会先保存中断现场,然后再执行设备驱动注册的中断处理函数,如果有必要,中断处理函数可以唤醒softirq。在中断退出的过程中,内核会检查是否有softirq需要执行,如果有,内核就会去执行softirq,直到softirq执行完成或者softirq处理时间超时后,内核才会继续恢复中断现场。大体的过程如下:

产生中断,硬件关闭中断保存中断现场执行中断处理函数有耗时任务,唤醒softirq退出中断处理函数,检查softirq开启中断执行softirqsoftirq执行时间过长,唤醒softirqd线程关闭中断恢复中断现场
中断被开启

softirq类型

内核最多支持32种softirq,目前已有的softirq如下:

// linux_5.10.97/include/linux/interrupt.h
enum
{HI_SOFTIRQ=0, // 高优先级的taskletTIMER_SOFTIRQ, // 定时器NET_TX_SOFTIRQ, // 网卡发包NET_RX_SOFTIRQ, // 网卡收包BLOCK_SOFTIRQ,IRQ_POLL_SOFTIRQ, // irq_pollTASKLET_SOFTIRQ, // 普通优先级的taskletSCHED_SOFTIRQ, // 处理调度相关的事务HRTIMER_SOFTIRQ, // 高精度定时器RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */NR_SOFTIRQS
};

上面那些softirq已经被内核的各个模块使用了,通常我们自己编写驱动都是借助那些模块提供的接口来间接的使用softirq。比如我们的网卡驱动想要通过softirq的机制来收包,就需要向napi模块注册一个回调用于收包,并在网卡接收中断处理函数中调用napi提供的接口来唤醒NET_RX_SOFTIRQ。当NET_RX_SOFTIRQ这个类型的softirq被执行时,napi模块便会调用网卡驱动注册的回调。

如果我们自己的模块需要使用自定义的softirq,那么就需要新增一个类型的softirq(新增一个枚举值),此外,还需编写一个softirq处理函数并在模块初始化时将其注册到内核。

需要注意的是,枚举值越小的softirq优先级越高,多种类型的softirq被唤醒时,优先级高的先被处理。

注册softirq

每种softirq都可以注册一个处理函数,内核在初始化阶段,各个使用softirq的模块便会向内核注册softirq处理函数,比如网络模块:

net_dev_initopen_softirq(NET_TX_SOFTIRQ, net_tx_action);open_softirq(NET_RX_SOFTIRQ, net_rx_action);

通过open_softirq函数就可以向内核注册softirq处理函数,open_softirq函数如下:

// linux_5.10.97/kernel/softirq.c
// open_softirq第一个入参是softirq number,第二个入参是softirq处理函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
{softirq_vec[nr].action = action; // 内核使用了一个数组softirq_vec[]来记录softirq的处理函数
}

唤醒softirq

内核使用了一个irq_cpustat_t类型的结构体变量来记录被唤醒的softirqirq_cpustat_t结果体成员如下,irq_cpustat_t::__softirq_pending的一个bit就代表一种softirq,唤醒某个softirq其实就是把对应的bit置1。

// linux_5.10.97/include/linux/interrupt.h
typedef struct {unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;

内核定义irq_stat时将其定义为PERCPU的变量,那么就可能出现同一类型的softirq在多个CPU被唤醒的情形,自然,同一类型的softirq也就可能同时在多个CPU上执行。

// linux_5.10.97/kernel/softirq.c
DEFINE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);
EXPORT_PER_CPU_SYMBOL(irq_stat);

内核提供了两个函数来唤醒softirq,分别是raise_softirqraise_softirq_irqoffraise_softirqraise_softirq_irqoff的包裹函数,raise_softirq在调用raise_softirq_irqoff之前会关闭当前CPU的中断,如果我们本就处在中断被禁用的上下文中,可以直接调用raise_softirq_irqoff

void raise_softirq(unsigned int nr)
{unsigned long flags;local_irq_save(flags); // 记录本地中断的状态,关闭中断raise_softirq_irqoff(nr);local_irq_restore(flags); // 恢复本地中断
}

我们可以在中断处理函数中唤醒softirq,也可以在进程上下文中唤醒softirq,如果在进程上下文中唤醒softirq,那么内核会使用内核线程来执行softirqraise_softirq_irqoff函数如下:

inline void raise_softirq_irqoff(unsigned int nr)
{__raise_softirq_irqoff(nr); // 将当前CPU的irq_cpustat_t::__softirq_pending的第nr位置1if (!in_interrupt()) // 如果不处于中断上下文,那就唤醒softirqd来执行softirqwakeup_softirqd();
}

softirq执行时机

softirq既可能在中断退出的过程中被执行,也可能在中断线程中执行。
中断退出过程中检查softirq的代码入下:

// linux_5.10.97/kernel/softirq.c
static inline void __irq_exit_rcu(void)
{......preempt_count_sub(HARDIRQ_OFFSET); // 减少hardirq计数,标志着离开中断上半部if (!in_interrupt() && local_softirq_pending()) // in_interrupt用于判断当前是否处于中断上下文,local_softirq_pending用于判断当前cpu是否有softirq被唤醒(判断irq_cpustat_t::__softirq_pending)invoke_softirq(); // 执行softirq......
}

invoke_softirq函数如下:

static inline void invoke_softirq(void)
{if (ksoftirqd_running(local_softirq_pending())) // 如果内核用于处理softirq的线程正在运行,那就直接退出return;if (!force_irqthreads) { // 如果内核没有启动强制线程化softirq,那就直接调用`__do_softirq`来执行softirq......__do_softirq();......} else { // 如果强制要求在线程中执行softirq,那就唤醒用于处理softirq的线程wakeup_softirqd();}
}

内核初始化过程中,softirq模块会为每个CPU都创建一个用于执行softirq的线程,如下:

// linux_5.10.97/kernel/softirq.c
tatic struct smp_hotplug_thread softirq_threads = {.store			= &ksoftirqd,.thread_should_run	= ksoftirqd_should_run, // 当线程被唤醒后,就会调用该函数来判断当前cpu是否有softirq待执行,进而决定是应该睡眠还是应该运行'thread_fn'.thread_fn		= run_ksoftirqd, // ksoftirqd线程调用run_ksoftirqd来执行softirq.thread_comm		= "ksoftirqd/%u", // 线程的名字叫 ksoftirqd/*
};static __init int spawn_ksoftirqd(void) // spawn_ksoftirqd会在内核初始化过程中被调用
{cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,takeover_tasklets);BUG_ON(smpboot_register_percpu_thread(&softirq_threads)); // 创建softirq内核线程return 0;
}
early_initcall(spawn_ksoftirqd);

ksoftirqd被唤醒后调用的run_ksoftirqd如下:

static void run_ksoftirqd(unsigned int cpu)
{local_irq_disable(); // 关闭本地cpu的中断if (local_softirq_pending()) { // 判断是否有有softirq待执行/** We can safely run softirq on inline stack, as we are not deep* in the task stack here.*/__do_softirq(); // 执行softirqlocal_irq_enable(); // 开启本地cpu的中断,这里看似是在关闭中断的环境中执行的softirq,其实不然,__do_softirq函数会打开中断cond_resched(); return;}local_irq_enable();
}

__do_softirq

用于执行softirq__do_softirq函数如下:

asmlinkage __visible void __softirq_entry __do_softirq(void)
{unsigned long end = jiffies + MAX_SOFTIRQ_TIME;unsigned long old_flags = current->flags;int max_restart = MAX_SOFTIRQ_RESTART;struct softirq_action *h;bool in_hardirq;__u32 pending;int softirq_bit;/** Mask out PF_MEMALLOC as the current task context is borrowed for the* softirq. A softirq handled, such as network RX, might set PF_MEMALLOC* again if the socket is related to swapping.*/current->flags &= ~PF_MEMALLOC;//  读取当前cpu有哪些softirq被唤醒了pending = local_softirq_pending();account_irq_enter_time(current);__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);in_hardirq = lockdep_softirq_start();restart:/* Reset the pending bitmask before enabling irqs */// 清除位掩码(irq_cpustat_t::__softirq_pending)set_softirq_pending(0);// 打开中断local_irq_enable();// softirq_vec记录着softirq的处理函数,这里将h指向softirq_vech = softirq_vec;while ((softirq_bit = ffs(pending))) { // 获取pending中第一个不为0的bitunsigned int vec_nr;int prev_count;// 获取softirq处理函数h += softirq_bit - 1;vec_nr = h - softirq_vec;prev_count = preempt_count();kstat_incr_softirqs_this_cpu(vec_nr);trace_softirq_entry(vec_nr);// 调用softirq的处理函数h->action(h);trace_softirq_exit(vec_nr);if (unlikely(prev_count != preempt_count())) {pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",vec_nr, softirq_to_name[vec_nr], h->action,prev_count, preempt_count());preempt_count_set(prev_count);}h++;pending >>= softirq_bit;}if (__this_cpu_read(ksoftirqd) == current)rcu_softirq_qs();// 关闭irqlocal_irq_disable();// 重新查看是否又有新的softirq被唤醒了pending = local_softirq_pending();if (pending) {// 如果此次运行__do_softirq的时间没超过MAX_SOFTIRQ_TIME (2ms)且restart的次数低于MAX_SOFTIRQ_RESTART (10),那就跳转到restart处继续执行softirqif (time_before(jiffies, end) && !need_resched() &&--max_restart)goto restart;wakeup_softirqd(); // 如果__do_softirq已经耗费了太多的时间或者restart的次数过多,那就唤醒ksoftirqd}lockdep_softirq_end(in_hardirq);account_irq_exit_time(current);__local_bh_enable(SOFTIRQ_OFFSET);WARN_ON_ONCE(in_interrupt());current_restore_flags(old_flags, PF_MEMALLOC);
}

__do_softirq函数可以看到,在softirq执行期间,中断是开启的,这期间可能会发生中断,也可能会有新的softirq被唤醒,所以__do_softirq会循环的读取softirq的位掩码并执行softirq,直到没有softirq需要执行或者执行softirq的时间或者次数过多才会退出。如果__do_softirq是因为执行softirq的时间或者次数过多才会退出的,那么它还会唤醒ksoftirqd来继续执行softirq

感谢阅读!

http://www.xdnf.cn/news/100549.html

相关文章:

  • 网页设计规范:从布局到交互的全方位指南
  • axios 在请求拦截器中设置Content-Type无效问题
  • Generative AI for Krita - Krita 生成式 AI 插件
  • 机器学习学习笔记
  • 迭代器模式:统一数据遍历方式的设计模式
  • 基于自适应汉克尔子空间的快速且超高分辨率的弥散磁共振成像(MRI)图像重建|文献速递-深度学习医疗AI最新文献
  • 第七篇:linux之基本权限、进程管理、系统服务
  • FPGA开发流程初识
  • 大数据学习(112)-Analytic函数集
  • (2025最新版)CUDA安装及环境配置
  • 文件上传过程中出现EOFException的解决方案
  • 建筑安全员 A 证与 C 证:差异决定职业方向
  • 【3.1】pod详解——Pod的结构
  • Science Robotics 新型层级化架构实现250个机器人智能组队,“单点故障”系统仍可稳定运行
  • 汽车租赁管理系统分析方案
  • Redis核心技术知识点全集
  • C#语言实现PDF转Excel
  • 【论文阅读】Dual-branch Cross-Patch Attention Learning for Group Affect Recognition
  • Tkinter:Python 3官方轻量级GUI库
  • 常见的五种权限管理模型
  • 用交换机连接两台电脑,电脑A读取/写电脑B的数据
  • openGauss数据库:起源、特性与对比分析
  • CSS内边距、外边距、边框
  • 【C/C++】插件机制:基于工厂函数的动态插件加载
  • 【多线程】二、pthread库 线程控制 线程分离 __thread关键字 线程库封装
  • skynet.cluster 库函数应用
  • update方法
  • Kafka 保证多分区的全局顺序性的设计方案和具体实现
  • 接口访问数据库报错问题记录
  • Java多线程的暗号密码:5分钟掌握wait/notify