首页 > 系统相关 >Linux内核机制—smp_hotplug_thread

Linux内核机制—smp_hotplug_thread

时间:2024-10-09 21:12:10浏览次数:1  
标签:kthread struct thread hotplug smp 线程 smpboot cpu

一、简介

  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线程的名称中基础的部分。
  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=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);
}
  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_driver
    ret = cpuhp_setup_state_nocalls_cpuslocked(CPUHP_AP_ONLINE_DYN,
            "cpufreq:online", cpuhp_cpufreq_online, cpuhp_cpufreq_offline);

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

标签:kthread,struct,thread,hotplug,smp,线程,smpboot,cpu
From: https://www.cnblogs.com/linhaostudy/p/18455158

相关文章

  • `std::packaged_task`、`std::thread` 和 `std::async` 的区别与联系
    std::packaged_task、std::thread和std::async的区别与联系std::packaged_task、std::thread和std::async都是C++11中提供的并发工具,用于执行任务并处理多线程操作。虽然它们都有类似的作用(并发执行任务),但在功能和使用方式上有显著区别。下面分别解释它们的特点,并说明它......
  • 【国产化】RT-THREAD ENV介绍和应用
    原创zgrxmmlinux源码阅读前言作为一款优秀的国产实时操作系统,RT-THREAD以其轻量级、高可靠性和丰富的生态系统,在众多嵌入式开发项目中崭露头角。今天,基于RT-THREAD探索如何利用其强大的ENV工具链快速搭建和配置STM32F429-ATK-APOLLO开发板的工程项目。通过这篇教程......
  • 【多线程奇妙屋】 Java 的 Thread类必会小技巧,教你如何用多种方式快速创建线程,学并发
    本篇会加入个人的所谓鱼式疯言❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言而是理解过并总结出来通俗易懂的大白话,小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.......
  • 【linux】linux unable to create new native thread
    1.概述今天遇到一个问题unabletocreatenewnativethread但是看着代码这里应该不会报错,出现这个问题该怎么排查呢?当在Linux系统中出现“unabletocreatenewnativethread”错误时,通常是由于达到了系统可用的进程或线程数量限制导致的。这个错误表示系统无法......
  • OpenOCD 代码学习(5)继续 openocd_thread()
    目录前言1server_init()2执行init命令2.1targetinit2.2adapter_init()2.3其它命令3server_loop()前言1)前面几节我们学习了①~③中的解析命令行参数与解析配置文件,接下来我们来看一下剩余的④~⑥部分:server_init()函数、init命令和server_loop()函数:......
  • 2024.9.26 ThreadLocal
    在使用ThreadLocal的情况下,并发量很高时不会产生冲突,原因如下:1.线程隔离:ThreadLocal为每个线程提供独立的存储空间。每个线程都可以安全地设置和获取其自己的变量值,而不会影响其他线程。即使在高并发环境下,线程间的数据是隔离的。2.并发安全:ThreadLocal本身是线程安......
  • MySQL variables:thread_handling
    在使用MySQL数据库时,我们经常会遇到多个客户端同时访问数据库的情况。为了处理并发请求,MySQL提供了thread_handling参数,用于控制线程的管理方式thread_handling参数的作用thread_handling参数用于控制MySQL如何处理客户端的连接请求。它可以影响数据库的性能、吞吐量以......
  • READ_ONCE/WRITE_ONCE/ACCESS_ONCE和smp_store_release/smp_load_acquire作用
    READ_ONCE,WRITE_ONCE和ACCESS_ONCE宏在linux内核中出现的频率极高。那么这三个宏到底起了什么样的作用呢?smp_store_release/smp_load_acquire又如何呢?1、宏定义我们先看下READ_ONCE和WRITE_ONCE的宏定义:#defineREAD_ONCE(x)\ ({union{typeof(x)__val;char__......
  • 进程和线程的区别;线程的多种创建方式;Thread 类及常见方法;线程的状态
    文章目录进程和线程的区别线程的创建方式继承Thread,重写run(创建单独的类/匿名内部类)实现Runnable,重写run(创建单独的类/匿名内部类)实现Callable,重写call(创建单独的类/匿名内部类)使用lambda表达式ThreadFactory线程工厂线程池Thread类及常见方法Thread的常见属性启动一......
  • Thread , ThreadLocal , ThreadLocalMap , Entry 之间的关系?
    Thread,ThreadLocal,ThreadLocalMap,Entry之间的关系?首先ThradLocal是线程的本地副本,怎么理解这句话呢?一个Thread都有一个它自己的ThreadLocalMap。ThreadLocalMap不是HashMap的结构,而是一个Entry数组,里面存放了一个一个的Entry。而Entry中存放的就是key和value,这个......