Linux内核机制—smp_hotplug_thread

news/2024/10/9 21:18:34

一、简介

  1. 只是一个创建per-cpu线程执行用户提供的回调的机制。
  2. 内核中已存在的注册
static struct smp_hotplug_thread idle_inject_threads = { //drivers/powercap/idle_inject.c.store = &idle_inject_thread.tsk,.setup = idle_inject_setup,.thread_fn = idle_inject_fn,.thread_comm = "idle_inject/%u",.thread_should_run = idle_inject_should_run,
};
early_initcallsmpboot_register_percpu_thread(&idle_inject_threads);static struct smp_hotplug_thread cpu_stop_threads = { //kernel/stop_machine.c.store            = &cpu_stopper.thread,.thread_should_run    = cpu_stop_should_run,.thread_fn        = cpu_stopper_thread,.thread_comm        = "migration/%u",.create            = cpu_stop_create,.park            = cpu_stop_park,.selfparking        = true,
};
early_initcallsmpboot_register_percpu_thread(&cpu_stop_threads)static struct smp_hotplug_thread rcu_cpu_thread_spec = { //kernel/rcu/tree.c.store            = &rcu_data.rcu_cpu_kthread_task,.thread_should_run    = rcu_cpu_kthread_should_run,.thread_fn        = rcu_cpu_kthread,.thread_comm        = "rcuc/%u", //per-cpu的.setup            = rcu_cpu_kthread_setup,.park            = rcu_cpu_kthread_park,
};
early_initcallsmpboot_register_percpu_thread(&rcu_cpu_thread_spec)static struct smp_hotplug_thread softirq_threads = { //kernel/softirq.c.store            = &ksoftirqd,.thread_should_run    = ksoftirqd_should_run,.thread_fn        = run_ksoftirqd,.thread_comm        = "ksoftirqd/%u",
};
early_initcallsmpboot_register_percpu_thread(&softirq_threads)static struct smp_hotplug_thread cpuhp_threads = { //kernel/cpu.c.store            = &cpuhp_state.thread,.create            = &cpuhp_create,.thread_should_run    = cpuhp_should_run,.thread_fn        = cpuhp_thread_fun,.thread_comm        = "cpuhp/%u",.selfparking        = true,
};
kernel_init_freeable //在 do_basic_setup() 时调用,比 early_initcall 调用的还早smp_initsmpboot_register_percpu_thread(&cpuhp_threads)

都是通过 smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread) 函数在内核启动早期调用的。注册线程的函数体都是smpboot_thread_fn()。

二、相关数据结构

  1. struct smp_hotplug_thread
struct smp_hotplug_thread { //include/linux/smpboot.hsstruct task_struct    * __percpu *store;struct list_head    list;int                    (*thread_should_run)(unsigned int cpu);void                (*thread_fn)(unsigned int cpu);void                (*create)(unsigned int cpu);void                (*setup)(unsigned int cpu);void                (*cleanup)(unsigned int cpu, bool online);void                (*park)(unsigned int cpu);void                (*unpark)(unsigned int cpu);bool                selfparking;const char            *thread_comm;
};

CPU hotplug 相关的描述符。

  • store: per-cpu变量,指向每个 cpu 上的 task_struct 结构。smp hotplug thread 在注册时会为每个CPU注册一个内核线程。
  • list: 在初始化时通过它挂在全局 hotplug_threads 链表上,方便 core 进行管理。
  • thread_should_run: 检查线程是否应该运行的回调函数,在禁用抢占的情况下调用。
  • thread_fn: 关联的功能函数,这个是主要的回调,是开着抢占调用的。
  • create: 可选的设置回调函数,在创建线程时调用(不是从线程上下文中调用,TODO: 是在内核启动时调用?)
  • setup: 可选的设置回调函数,当线程第一次运行时调用,可用于设置线程属性。
  • cleanup: 可选的清理回调函数,当线程应该停止时调用(模块退出)
  • park: 可选的 park 回调函数,当线程被 park 时调用(cpu offline)
  • unpark: 可选的 unpark 回调函数,当线程被 unpark 时调用(cpu online)
  • selfparking: 若初始化为true,则创建完线程后线程状态是unpark的,为false则是parked的。
  • thread_comm: 创建的per-cpu线程的名称中基础的部分。
  1. struct smpboot_thread_data
struct smpboot_thread_data {unsigned int            cpu;unsigned int            status;struct smp_hotplug_thread    *ht;
};

是一个辅助结构。

cpu: 判断是哪个CPU的,也就是在哪个CPU上执行。
status: per-cpu的hotplug线程的状态。
ht: 指向用户注册的hotplug结构

三、注册流程

一般内核模块会先初始化一个 smp_hotplug_thread 结构,然后通常在 early_initcall() 或内核启动更早期调用 smpboot_register_percpu_thread() 进行注册。下面使用 stop_machine.c 中的注册进行举例:

static int __init cpu_stop_init(void)
{smpboot_register_percpu_thread(&cpu_stop_threads);
}
early_initcall(cpu_stop_init);
  1. 注册函数执行流程:
int smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread) //smpboot.c
{...for_each_online_cpu(cpu) {__smpboot_create_thread(plug_thread, cpu);smpboot_unpark_thread(plug_thread, cpu);}list_add(&plug_thread->list, &hotplug_threads);
}

1.1. __smpboot_create_thread 函数:

static int __smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu) //smpboot.c
{struct task_struct *tsk = *per_cpu_ptr(ht->store, cpu);struct smpboot_thread_data *td;td = kzalloc_node(sizeof(*td), GFP_KERNEL, cpu_to_node(cpu)); //arg2=0td->cpu = cpu;td->ht = ht;/* 创建的是这个内核线程,执行的函数体是 smpboot_thread_fn() 参数传的是td,td->ht 指向用户注册的结构 */tsk = kthread_create_on_cpu(smpboot_thread_fn, td, cpu, ht->thread_comm);/* 在 kthread->flags |= KTHREAD_IS_PER_CPU 标志 */kthread_set_per_cpu(tsk, cpu);/** 设置tsk的 kthread->flags |= KTHREAD_SHOULD_PARK, 然后tsk会进入到TASK_PARKED状态,* 若tsk!=current则先唤醒它然后让其进入到TASK_PARKED状态。*/kthread_park(tsk);/* 每个CPU上创建的任务由per-cpu的 store 指向 */*per_cpu_ptr(ht->store, cpu) = tsk;/* 若提供了 create 回调则调用,此时内核启动阶段,非进程上下文 */if (ht->create) {wait_task_inactive(tsk, TASK_PARKED);ht->create(cpu);}return 0;
}struct task_struct *kthread_create_on_cpu(int (*threadfn)(void *data),void *data, unsigned int cpu, const char *namefmt)
{/* 在指定的cpu上注册一个CFS 120优先级的内核线程,线程函数体为 smpboot_thread_fn() */struct task_struct p = kthread_create_on_node(threadfn, data, cpu_to_node(cpu), namefmt, cpu);/* * 将创建的线程绑定到这个cpu上,这里会同时设置 p->flags |= PF_NO_SETAFFINITY* 标志位,不允许用户空间设置亲和性。*/kthread_bind(p, cpu);/* 翻译:CPU 热插拔需要在 unparking 线程时再次绑定 */to_kthread(p)->cpu = cpu;return p;
}

1.2 smpboot_unpark_thread 函数:

static void smpboot_unpark_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{struct task_struct *tsk = *per_cpu_ptr(ht->store, cpu);/* 若是使用者没有设置 selfparking= true 则会调用 */if (!ht->selfparking)kthread_unpark(tsk);
}void kthread_unpark(struct task_struct *k)
{struct kthread *kthread = to_kthread(k);/* 翻译:新创建的 kthread 在 CPU 离线时被停放。绑定丢失了,需要重新设置。*/if (test_bit(KTHREAD_IS_PER_CPU, &kthread->flags))__kthread_bind(k, kthread->cpu, TASK_PARKED);clear_bit(KTHREAD_SHOULD_PARK, &kthread->flags);/* 唤醒 parked 状态的任务 */wake_up_state(k, TASK_PARKED);
}
  1. 总结

可以看到,所有注册 smp_hotplug_thread 结构的模块,响应函数都是 smpboot_thread_fn(),默认是CFS 120优先级。

若 smp_hotplug_thread::selfparking = true,则创建完线程后会自动对线程进行unpark操作,创建出来的线程是unparked状态。
为flase则创建出来的线程是parked的状态,使用者还需要自己进行unpark。

若提供了 smp_hotplug_thread::create 回调,则在创建过程中就会调用,此时还是内核启动的 early_init() 或更早的阶段。

线程创建时已经和单个CPU绑定了,且设置了 p->flags |= PF_NO_SETAFFINITY,不允许用户空间设置亲和性了。

四、实现逻辑

  1. smpboot_thread_fn() 实现

既然创建的per-cpu的内核线程执行的是 smpboot_thread_fn(),这个函数是per-cpu的hotplug线程的死循环函数,在它里面会
检查线程是否需要stop、park、unpark、setup、cleanup 并调用用户注册的对应的回到函数。其目前只能返回0。下面看其实现。

static int smpboot_thread_fn(void *data) //smpboot.c
{struct smpboot_thread_data *td = data;struct smp_hotplug_thread *ht = td->ht;while (1) {set_current_state(TASK_INTERRUPTIBLE);preempt_disable();/* * 判断 kthread->flag & KTHREAD_SHOULD_STOP, 判断此 kthread 现* 在是否应该返回。* 当有人对此kthread调用了 kthread_stop() 时,它会被唤醒并返回* true。然后这里应该返回,返回值将被传递给 kthread_stop()。*/if (kthread_should_stop()) {__set_current_state(TASK_RUNNING);preempt_enable();/* cleanup must mirror setup */if (ht->cleanup && td->status != HP_THREAD_NONE)ht->cleanup(td->cpu, cpu_online(td->cpu));kfree(td);return 0;}/* 判断 to_kthread->flags & KTHREAD_SHOULD_PARK, 判断此 kthread* 现在是否应该被park。* 也是先唤醒,然后执行park()回调。*/if (kthread_should_park()) {__set_current_state(TASK_RUNNING);preempt_enable();if (ht->park && td->status == HP_THREAD_ACTIVE) {BUG_ON(td->cpu != smp_processor_id());ht->park(td->cpu);td->status = HP_THREAD_PARKED;}/** 设置 current->state=TASK_PARKED,complete(&self->parked)* 然后将自己切走。*/kthread_parkme();/* We might have been woken for stop */continue;}/* ---- 下面就是不需要stop和不需要park的情况了 ---- */BUG_ON(td->cpu != smp_processor_id());/* Check for state change setup */switch (td->status) {case HP_THREAD_NONE:__set_current_state(TASK_RUNNING);preempt_enable();if (ht->setup)ht->setup(td->cpu);td->status = HP_THREAD_ACTIVE;continue;case HP_THREAD_PARKED:__set_current_state(TASK_RUNNING);preempt_enable();if (ht->unpark)ht->unpark(td->cpu);td->status = HP_THREAD_ACTIVE;continue;}/** 判断注册的回调是否需要运行,为假表示不需要运行,切走。* 若需要运行,则调用 ht->thread_fn() 回调。*/if (!ht->thread_should_run(td->cpu)) {preempt_enable_no_resched();schedule();} else {__set_current_state(TASK_RUNNING);preempt_enable();ht->thread_fn(td->cpu); //例如:cpuhp_thread_fun}}
}

这个函数是个单纯的死循环执行逻辑,没有持任何锁,只是部分函数回调时是关着抢占的。

  1. 其调用路径

上面注册per-cpu的内核线程是作为线程执行实体是其唯一调用路径,没有其它调用路径。

五、使用方法

可以用该函数在每个cpu上创建对应的线程

#include <linux/smpboot.h>
#include <linux/sched.h>
#include <linux/percpu.h>// 定义每CPU线程的数据结构
struct my_thread_info {struct task_struct *task;unsigned long data;
};static DEFINE_PER_CPU(struct my_thread_info, my_thread_info);// 线程函数
static void my_thread_func(unsigned int cpu)
{struct my_thread_info *ti = this_cpu_ptr(&my_thread_info);while (!kthread_should_stop()) {// 执行特定的任务pr_info("Thread running on CPU %d\n", cpu);set_current_state(TASK_INTERRUPTIBLE);schedule_timeout(HZ); // 休眠1秒}
}// 线程启动函数
static int my_thread_start(unsigned int cpu)
{struct my_thread_info *ti = &per_cpu(my_thread_info, cpu);ti->data = cpu * 100; // 示例数据return 0;
}// 线程停止函数
static void my_thread_stop(unsigned int cpu)
{struct my_thread_info *ti = &per_cpu(my_thread_info, cpu);// 清理资源
}// 定义线程控制结构
static struct smp_hotplug_thread my_threads = {.store      = &my_thread_info.task,.thread_fn  = my_thread_func,.setup      = my_thread_start,.cleanup    = my_thread_stop,.park       = NULL,.unpark     = NULL,
};// 初始化函数
static int __init my_init(void)
{int err;err = smpboot_register_percpu_thread(&my_threads);if (err)pr_err("Failed to register per-cpu threads\n");return err;
}// 退出函数
static void __exit my_exit(void)
{smpboot_unregister_percpu_thread(&my_threads);
}module_init(my_init);
module_exit(my_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Example of using smpboot_register_percpu_thread");

六、总结

注册 smp_hotplug_thread 结构,内核只是提供了为每个CPU都创建一个线程执行其回调的机制,线程函数体是 smpboot_thread_fn(),此函数没有任何其它调用路径,因此使用者只能通过唤醒+实现回调来实现自己的功能,执行完回调后进程自动休眠。

七、补充

  1. cpuhotplug回调除了静态指定数组成员外,还可以动态注册,类似于sysrq的实现
cpufreq_register_driverret = cpuhp_setup_state_nocalls_cpuslocked(CPUHP_AP_ONLINE_DYN,"cpufreq:online", cpuhp_cpufreq_online, cpuhp_cpufreq_offline);

此函数会注册到 cpuhp_hp_states[CPUHP_AP_ONLINE_DYN] 对应的位置上。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/69561.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

redis介绍与安装

Redis 简介Redis是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。 Redis与其他 key- value 缓存产品有以下三个特点: Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。 Redis不仅仅支持简单的key-value类型的数据,…

VSCode配置Python(记录)

python安装 官网在线安装或者下载离线包(勾选添加path环境变量)python指定版本运行 把对应版本的python.exe复制一下,粘贴改名加个对应版本,因为添加了环境变量的缘故所以可以直接在命令窗中运行运行测试对应项目创建虚拟环境(包管理) tips:当然了,也可以用anaconda管理…

The Missing Semester of Your CS Education

from pixiv前言 The missing semester of your CS education计算机设计的初衷就是任务自动化,然而学生们却常常陷在大量的重复任务中,或者无法完全发挥出诸如 版本控制、文本编辑器等工具的强大作用。效率低下和浪费时间还是其次,更糟糕的是,这还可能导致数据丢失或 无法完…

《机器学习初步》笔记

第一章 绪论 1.1 引言 机器学习的经典定义:利用经验(数据)改善系统自身的性能 经典的机器学习过程:机器学习最重要的理论模型:PAC(概览近似正确)1.2 基本术语 数据集:一组记录的集合 学习/训练:通过执行某个学习算法,得到模型,学的的模型对应数据的某种潜在规律 示例…

一文搞懂:预提、计提、摊销

本文将透彻解析这三个会计处理方式的异同,以及如何在实务操作中注意其细节,帮助会计人员更好地掌握和应用这些工具。通过实际案例的讲解,使读者能够清晰地区分和运用预提、计提及摊销,确保会计信息的准确性和时效性。 一、预提、计提、摊销 预提、计提、摊销,这三个名词长…

实验2 C语言分支与与循环基础应用编程——1

一、实验目的 1. 能正确使用if语句实现分支结构 2. 能正确使用while语句、do...while语句实现循环结构 3. 能在具体问题场景中正确区分、使用continue和break 4. 能灵活、组合使用c语句编程解决简单应用问题 二、实验准备 1. 分支语句 if 和循环语句 while 、 do...while 的用…

redis安装致命错误jemalloc/jemalloc.h

安装报错 在安装redis的时候,执行 make && make install 发生以下错误解决方案其实可以读一下redis里的Readme.md文件,我截图了其中的部分,使用make MALLOC=libc & make install

鼻托历险记

鼻托历险记我是一个鼻托,和我的双胞胎兄弟共同居住在一副眼镜上。 自打出生起,我就和其它部件们被融合成了一个集体。我们已经工作了快两年了,我的主人很贴心的照顾我们,不隔多久就会给和我们一起工作的镜片兄弟精心擦拭一番,也有时会心血来潮,给整个集体一起洗个凉水澡。…