基于Linux-5.10

一、简介

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_initcall
smpboot_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_initcall
smpboot_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_initcall
smpboot_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_initcall
smpboot_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_init
smpboot_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.hs
struct 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线程的名称中基础的部分。

2. 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=0
td->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);
}

2. 总结

可以看到,所有注册 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
}
}
}

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

2. 其调用路径

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

五、使用方法

既然内核线程函数体没有其它任何,那么只能靠通过 smp_hotplug_thread::store 保存的task_struct结构进行唤醒了,然后再在 smp_hotplug_thread 结构的回调函数中做文章。这也冲服体现了Linux-内核只提供机制,不提供策略的思想了!

六、总结

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

Linux内核机制—smp_hotplug_thread的更多相关文章

  1. Linux内核分析:页回收导致的cpu load瞬间飙高的问题分析与思考--------------蘑菇街技术博客

    http://mogu.io/156-156 摘要 本文一是为了讨论在Linux系统出现问题时我们能够借助哪些工具去协助分析,二是讨论出现问题时大致的可能点以及思路,三是希望能给应用层开发团队介绍一些 ...

  2. 内存管理——linux内核学习

    买了<深入Linux内核架构>这本书准备了解一下linux内核机制.但是最开始看了十几页感觉看着很累,本来都准备弃了 过了段时间看见一个面经有linux内核的内容,于是就照着那个先把内存管 ...

  3. 浅析linux内核中的idr机制

    idr在linux内核中指的就是整数ID管理机制,从本质上来说,这就是一种将整数ID号和特定指针关联在一起的机制.这个机制最早是在2003年2月加入内核的,当时是作为POSIX定时器的一个补丁.现在, ...

  4. Linux 内核中的 Device Mapper 机制

    本文结合具体代码对 Linux 内核中的 device mapper 映射机制进行了介绍.Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机 ...

  5. 解析 Linux 内核可装载模块的版本检查机制

    转自:http://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/ 为保持 Linux 内核的稳定与可持续发展,内核在发展过程中引进了可 ...

  6. linux 内核 RCU机制详解

    RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用.RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数 ...

  7. [内核同步]浅析Linux内核同步机制

    转自:http://blog.csdn.net/fzubbsc/article/details/37736683?utm_source=tuicool&utm_medium=referral ...

  8. Linux内核同步机制--转发自蜗窝科技

    Linux内核同步机制之(一):原子操作 http://www.wowotech.net/linux_kenrel/atomic.html 一.源由 我们的程序逻辑经常遇到这样的操作序列: 1.读一个 ...

  9. Linux 内核通知链机制的原理及实现

    一.概念: 大多数内核子系统都是相互独立的,因此某个子系统可能对其它子系统产生的事件感兴趣.为了满足这个需求,也即是让某个子系统在发生某个事件时通知其它的子 系统,Linux内核提供了通知链的机制.通 ...

  10. Linux 内核的文件 Cache 管理机制介绍

    Linux 内核的文件 Cache 管理机制介绍 http://www.ibm.com/developerworks/cn/linux/l-cache/ 1 前言 自从诞生以来,Linux 就被不断完 ...

随机推荐

  1. 一段简单的对TXT文件的操作代码

    1 string txt = @"C:\DetectFolder\IPV4地址.txt"; 2 string path = ""; 3 4 if (File.E ...

  2. rt-thread模糊到清晰系列: thread切换相关

    // 创建thread tid = rt_thread_create("main", main_thread_entry, RT_NULL, RT_MAIN_THREAD_STAC ...

  3. Zstack 鼎阳SDS6204示波器和Archiver Appliance的重度测试2

    https://blog.csdn.net/weixin_43767046/article/details/113748775 https://blog.csdn.net/weixin_4376704 ...

  4. Kali Linux Web 渗透测试秘籍 中文版

    第一章 配置 Kali Linux 简介 在第一章中,我们会涉及如何准备我们的 Kali 以便能够遵循这本书中的秘籍,并使用虚拟机建立带有存在漏洞的 Web 应用的实验室. 1.1 升级和更新 Kal ...

  5. jupyter环境搭建

    前言 jupyter,一个交互式的笔记本,进行数据分析时比pycharm要好用和专业一些. jupyter安装的正确姿势 在别的教程里,他们讲的是通过pip install jupyter或者brew ...

  6. Linux快照利器:Timeshift,备份和还原效果杠杠的

    转载csdn:Linux快照利器:Timeshift,备份和还原效果杠杠的_网络技术联盟站-CSDN博客

  7. Quill编辑器实现原理初探

    简介 从事前端开发的同学,对富文本编辑器都不是很陌生.但是大多数富文本编辑器都是开箱即用,很少会对其实现原理进行深入的探讨.假如静下心去细细品味,会发现想要做好一款富文本编辑器,需要对整个前端生态有较 ...

  8. .NET AsyncLocal 避坑指南

    目录 AsyncLocal 用法简介 AsyncLocal 实现原理 AsyncLocal 的坑 AsyncLocal 的避坑指南 HttpContextAccessor 的实现原理 AsyncLoc ...

  9. jQuery使用与语法

    jQuery安装 1.从官网Download jQuery | jQuery下载安装: 2.CDN在线加载: statistic CDN:https://cdn.staticfile.org/jque ...

  10. Vue scoped样式

    scoped样式: 作用:让样式在局部生效,防止冲突 写法:<style scoped>