Linux核心機制—smp_hotplug_thread

yooooooo發表於2024-10-09

一、簡介

  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] 對應的位置上。

相關文章