暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

进程管理与调度--内核线程

二进制人生 2020-05-15
1733

微信公众号:二进制人生
专注于嵌入式linux开发。问题或建议,请发邮件至hjhvictory@163.com。
更新日期:2020/05/07,内容整理自网络,转载请注明出处。

参考:
https://blog.csdn.net/gatieme/article/details/51589205

目录

内核线程概述为什么需要内核线程内核线程的进程描述符线程私有数据2号进程kthreadd--内核线程父母内核线程的创建(1)kernel_thread(2)kthread_create(3)kthread_run内核线程的退出内核线程的停车技术内核线程的cpu绑定技术内核线程的冷冻技术

内核线程概述

内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程。

内核线程主要有两种类型:

1、线程启动后一直等待,直至内核请求线程执行某一特定操作。

2、线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。

内核线程由内核自身生成,其特点在于:

它们在内核态执行,而不是用户态。

它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间。

我们后面会写另外一篇文章,专门介绍内核的几个知名线程。

为什么需要内核线程

Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。

内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。

内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。

这与用户线程是不一样的。因为内核线程只运行在内核态。

因此,它只能使用大于PAGE_OFFSET(传统的x86_32上是3G)的虚拟地址空间。

内核线程的进程描述符

内核线程也是使用task_struct作为描述符,因为在内核层面,内核线程也是当做进程来看的,只不过有一些区别,那就是内核线程只访问内核空间,而用户进程需要访问全部空间。

task_struct进程描述符中包含两个跟进程地址空间相关的字段mm,active_mm:

struct task_struct
{

    // ...
    struct mm_struct *mm;
    struct mm_struct *avtive_mm;
    //...
};

大多数计算机上系统的全部虚拟地址空间分为两个部分: 供用户态程序访问的虚拟地址空间和供内核访问的内核空间。每当内核执行上下文切换时, 虚拟地址空间的用户层部分都会切换, 以便当前运行的进程匹配, 而内核空间不会放生切换。

对于普通用户进程来说,mm指向虚拟地址空间的用户空间部分,而对于内核线程,mm为NULL。

这为优化提供了一些余地,可遵循所谓的惰性TLB处理(lazy TLB handing)。active_mm主要用于优化,由于内核线程不与任何特定的用户层进程相关,内核并不需要倒换虚拟地址空间的用户层部分,保留旧设置即可。由于内核线程之前可能是任何用户层进程在执行,故用户空间部分的内容本质上是随机的,内核线程决不能修改其内容,故将mm设置为NULL,同时如果切换出去的是用户进程,内核将原来进程的mm存放在新内核线程的active_mm中,因为某些时候内核必须知道用户空间当前包含了什么。

为什么没有mm指针的进程称为惰性TLB进程?

假如内核线程之后运行的进程与之前是同一个,在这种情况下,内核并不需要修改用户空间地址表。地址转换后备缓冲器(即TLB)中的信息仍然有效。只有在内核线程之后,执行的进程是与此前不同的用户层进程时,才需要切换(并对应清除TLB数据)。

内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm指针被设置为NULL;它只在 内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。

线程私有数据

除了用task_struct来描述一个线程外,相对于用户进程,内核线程还有一些私有数据,这些私有数据封装成了结构体:

struct kthread {
    unsigned long flags;//线程的一些标志位
    unsigned int cpu;   //绑定的cpu
    void *data;         //任务函数参数
    struct completion parked; //实现停车技术的完成量
    struct completion exited; //实现即时退出的完成量
#ifdef CONFIG_BLK_CGROUP
    struct cgroup_subsys_state *blkcg_css;
#endif
};

线程标志位:

enum KTHREAD_BITS {
    KTHREAD_IS_PER_CPU = 0,
    KTHREAD_SHOULD_STOP,
    KTHREAD_SHOULD_PARK,
};

task_struct有一指针成员指向该私有数据,

    /* CLONE_CHILD_SETTID: */
    int __user          *set_child_tid;

所以可以通过task_struct访问到该线程私有数据:

static inline struct kthread *to_kthread(struct task_struct *k)
{
    WARN_ON(!(k->flags & PF_KTHREAD));
    return (__force void *)k->set_child_tid;
}

按照我的预测,这一块后续应该会有较大改动,set_child_tid这个名字取得也不是很合理。

2号进程kthreadd--内核线程父母

kthreadd:它就是我们前面说的2号进程,严格来讲它是一个内核线程,它的作用是创建待创建的线程。

其他任务或代码(我们把它叫做调用者)想创建内核线程时需要调用kthread_create(或kthread_create_on_node)创建一个kthread,该kthread会被加入到kthread_create_list链表中,同时kthread_create会唤醒 kthreadd_task(即kthreadd),然后调用者自己阻塞等待线程创建完成。

kthreadd被唤醒后,更新状态为TASK_RUNNING,kthreadd会从链表中取出待创建线程,调用kernel_thread创建kthread线程,kthread线程在做了一些初始化工作后通知调用者创建完成,即刻起调用者解除了阻塞状态,而kthread更新自己的状态为TASK_UNINTERRUPTIBLE,调用schedule()让出了cpu,在下一次得到cpu时,它就会真正执行任务实体。

在链表为空时kthreadd会更新状态为TASK_INTERRUPTIBLE,并调用scheduler 让出CPU。详细的细节可以见下面内核线程的创建。

内核线程会出现在系统进程列表中,但是在ps的输出中进程名command由方括号包围,以便与普通进程区分。

如下图所示,我们可以看到系统中,所有内核线程都用[]标识,而且这些进程父进程id均是2,而2号进程kthreadd的父进程是0号进程
使用ps -eo pid,ppid,comm:

root@AI-Machine:~# ps -eo pid,ppid,comm
  PID  PPID COMMAND
    1     0 systemd
    2     0 kthreadd
    4     2 kworker/0:0H
    6     2 mm_percpu_wq
    7     2 ksoftirqd/0
    8     2 rcu_sched
    9     2 rcu_bh
   10     2 migration/0
   11     2 watchdog/0
   ...

可以看到,很多内核线程的ppid都等于2,也就是说都是kthreadd的子进程。
内核线程会用方括号圈起来(上面使用-o选项指令了ps命令的输出格式,所以就没有方括号):

root@AI-Machine:~# ps -aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  24060  4312 ?        Ss   4月23   0:08 /sbin/init splash
root         2  0.0  0.0      0     0 ?        S    4月23   0:00 [kthreadd]
root         4  0.0  0.0      0     0 ?        I<   4月23   0:00 [kworker/0:0H]
root         6  0.0  0.0      0     0 ?        I<   4月23   0:00 [mm_percpu_wq]
root         7  0.0  0.0      0     0 ?        S    4月23   0:02 [ksoftirqd/0]

内核线程的创建

  • kthead_create

  • kthread_run

内核线程有两个常用的创建接口:
kthead_create和kthread_run。

kthread_create创建一个新的内核线程。最初线程是停止的,需要使用wake_up_process启动它。

使用kthread_run,与kthread_create不同的是,其创建新线程后立即唤醒它,其本质就是先用kthread_create创建一个内核线程,然后通过wake_up_process唤醒它。

(1)kernel_thread

但是在kthreadd还没创建之前,我们只能通过kernel_thread这种方式去创建,kernel_thread由_do_fork来实现,参见kernel/fork.c:

pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
    struct kernel_clone_args args = {
        .flags      = ((flags | CLONE_VM | CLONE_UNTRACED) & ~CSIGNAL),
        .exit_signal    = (flags & CSIGNAL),
        .stack      = (unsigned long)fn,
        .stack_size = (unsigned long)arg,
    };

    return _do_fork(&args);
}

实际上这是创建内核线程的唯一方式,后面的kthread_create接口最终还是间接的调用了kernel_thread。
建立新的内核线程时默认采用了如下标志:

  • CLONE_VM:置此标志在进程间共享地址空间。因此内核线程和调用的进程(current)具备相同的进程空间,因为调用者运行在进程的内核态,所以进程在内核态时共享内核空间。

  • CLONE_UNTRACED:保证即使父进程正在被调试,内核线程也不会被调试。

(2)kthread_create

#define kthread_create(threadfn, data, namefmt, arg...) \
       kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

我们看下线程创建的细节:
这些步骤有点类似于我们实现一个线程池一样,

  • 1、先分配一个线程结构体

struct kthread_create_info
{

    /* Information passed to kthread() from kthreadd. */
    int (*threadfn)(void *data);//执行实体
    void *data;//参数
    int node;

    /* Result passed back to kthread_create() from kthreadd. */
    struct task_struct *result;
    struct completion *done;

    struct list_head list;
};

  • 2、初始化线程结构体

  • 3、将新创建的线程结构体添加到线程链表kthread_create_list的尾部,唤醒线程守护者kthreadd,kthreadd可以理解成一个工人,没事的时候就睡眠,流水线上有零件来了就取出来组装成产品--即内核线程,线程守护者创建线程使用的还是kthread_create接口。

  • 4、阻塞等待线程守护者创建进程描述符,得到进程描述符后,设置调度参数、线程名字,返回。

struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
                            void *data, int node,
                            const char namefmt[],
                            va_list args)
{
    DECLARE_COMPLETION_ONSTACK(done);
    struct task_struct *task;
    struct kthread_create_info *create = kmalloc(sizeof(*create),
                             GFP_KERNEL);

    if (!create)
        return ERR_PTR(-ENOMEM);
    create->threadfn = threadfn;
    create->data = data;
    create->node = node;
    create->done = &done;

    spin_lock(&kthread_create_lock);
    list_add_tail(&create->list, &kthread_create_list);//添加到链表尾部
    spin_unlock(&kthread_create_lock);

    wake_up_process(kthreadd_task);//唤醒线程守护者
    /*
     * Wait for completion in killable state, for I might be chosen by
     * the OOM killer while kthreadd is trying to allocate memory for
     * new kernel thread.
     */

    if (unlikely(wait_for_completion_killable(&done))) {//等待内核线程创建完成
        /*
         * If I was SIGKILLed before kthreadd (or new kernel thread)
         * calls complete(), leave the cleanup of this structure to
         * that thread.
         */

        if (xchg(&create->done, NULL))
            return ERR_PTR(-EINTR);
        /*
         * kthreadd (or new kernel thread) will call complete()
         * shortly.
         */

        wait_for_completion(&done);
    }
    task = create->result;
    if (!IS_ERR(task)) {
        static const struct sched_param param = { .sched_priority = 0 };
        char name[TASK_COMM_LEN];

        /*
         * task is already visible to other tasks, so updating
         * COMM must be protected.
         */

        vsnprintf(name, sizeof(name), namefmt, args);
        set_task_comm(task, name);
        /*
         * root may have changed our (kthreadd's) priority or CPU mask.
         * The kernel thread should not inherit these properties.
         */

        sched_setscheduler_nocheck(task, SCHED_NORMAL, &param);//设置调度策略为普通调度
        set_cpus_allowed_ptr(task, cpu_all_mask);//允许在任何cpu上执行
    }
    kfree(create);
    return task;
}

我们来看下线程守护者(2号进程)的主体:

int kthreadd(void *unused)
{
    struct task_struct *tsk = current;

    /* Setup a clean context for our children to inherit. */
    set_task_comm(tsk, "kthreadd");
    ignore_signals(tsk);
    set_cpus_allowed_ptr(tsk, cpu_all_mask);
    set_mems_allowed(node_states[N_MEMORY]);

    current->flags |= PF_NOFREEZE;
    cgroup_init_kthreadd();

    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE);
        if (list_empty(&kthread_create_list))//链表为空,让出cpu
            schedule();
        __set_current_state(TASK_RUNNING);

        spin_lock(&kthread_create_lock);
        while (!list_empty(&kthread_create_list)) {//非空
            struct kthread_create_info *create;

            create = list_entry(kthread_create_list.next,
                        struct kthread_create_info, list);
            list_del_init(&create->list);
            spin_unlock(&kthread_create_lock);

            create_kthread(create);//取出订单,创建内核线程

            spin_lock(&kthread_create_lock);
        }
        spin_unlock(&kthread_create_lock);
    }

    return 0;
}

create_kthread最终调用了kernel_thread。

static void create_kthread(struct kthread_create_info *create)
{
    int pid;

#ifdef CONFIG_NUMA
    current->pref_node_fork = create->node;
#endif
    /* We want our own signal handler (we take no signals by default). */
        //创建线程实体
    pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
    if (pid < 0) {//线程创建失败,需要通知线程创建者
        /* If user was SIGKILLed, I release the structure. */
        struct completion *done = xchg(&create->doneNULL);

        if (!done) {
            kfree(create);
            return;
        }
        create->result = ERR_PTR(pid);
        complete(done);//通知线程创建者线程创建完成,解除阻塞。
    }
}

线程实体kthread:
kthread第一次获得调度后就会通知调用者线程创建完成(在此之前,调用者处于阻塞状态),更新自身状态为TASK_UNINTERRUPTIBLE,然后让出cpu,下一次获得调度时才会真正执行任务实体。

static int kthread(void *_create)
{
    /* Copy data: it's on kthread's stack */
    struct kthread_create_info *create = _create;
    int (*threadfn)(void *data) = create->threadfn;
    void *data = create->data;
    struct completion *done;
    struct kthread *self;
    int ret;

    self = kzalloc(sizeof(*self), GFP_KERNEL);
    set_kthread_struct(self);

    /* If user was SIGKILLed, I release the structure. */
    done = xchg(&create->done, NULL);
    if (!done) {
        kfree(create);
        do_exit(-EINTR);
    }

    if (!self) {
        create->result = ERR_PTR(-ENOMEM);
        complete(done);
        do_exit(-ENOMEM);
    }

    self->data = data;
    init_completion(&self->exited);
    init_completion(&self->parked);
    current->vfork_done = &self->exited;

    /* OK, tell user we're spawned, wait for stop or wakeup */
    __set_current_state(TASK_UNINTERRUPTIBLE);
    create->result = current;
    complete(done);//通知调用者,线程创建完成
    schedule();//让出调度

    ret = -EINTR;
    if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {
        cgroup_kthread_ready();
        __kthread_parkme(self);
        ret = threadfn(data);//执行实体任务
    }
    do_exit(ret);
}

可以看到引入了守护者线程这个机制,其实只是将线程的创建操作进行了推迟而已。即将内核线程的创建工作交给kthreadd 2号进程来做。为何要搞得这么麻烦,让想创建线程的人直接调用 kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);不行吗?

(3)kthread_run

/**
 * kthread_run - create and wake a thread.
 * @threadfn: the function to run until signal_pending(current).
 * @data: data ptr for @threadfn.
 * @namefmt: printf-style name for the thread.
 *
 * Description: Convenient wrapper for kthread_create() followed by
 * wake_up_process().  Returns the kthread or ERR_PTR(-ENOMEM).
 */

#define kthread_run(threadfn, data, namefmt, ...)                          \
({                                                                         \
    struct task_struct *__k                                            \
            = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
    if (!IS_ERR(__k))                                                  \
            wake_up_process(__k);                                      \
    __k;                                                               \
})

使用kthread_run,与kthread_create不同的是,其创建新线程后立即唤醒它,其本质就是先用kthread_create创建一个内核线程,然后通过wake_up_process唤醒它。

内核线程的退出

线程一旦启动起来后,会一直运行,除非该线程主动调用do_exit函数,或者线程执行结束。当然如果线程函数永远不返回并且不检查信号,它将永远都不会停止。

内核给我们设计了几个接口,让线程能做到随时退出。

int kthread_stop(struct task_struct *k);

kthread_stop() 通过发送信号给线程告知其退出(说是发送信号,其实就是置位某个标志位而已)。

在执行kthread_stop的时候,目标线程必须没有退出,否则会Oops。原因很容易理解,当目标线程退出的时候,其对应的task结构也变得无效,kthread_stop引用该无效task结构就会出错。

我们的线程主体函数应该设计成这样:

thread_func()//线程
{
    // do your work here
    // wait to exit
    while(!kthread_should_stop())
    {
         // do your work here  
    }
}

或者这样:

thread_func()//线程
{
    // do your work here
    while(1)
    {
         if(kthread_should_stop())
             break;
         // do your work here  
    }
}

想让线程退出的时候就调用kthread_stop(_task)。

kthread_should_stop其实就是读取线程退出标志KTHREAD_SHOULD_STOP是否置位:

bool kthread_should_stop(void)
{
    return test_bit(KTHREAD_SHOULD_STOP, &to_kthread(current)->flags);
}

而kthread_stop其实就是置位线程退出标志KTHREAD_SHOULD_STOP,另外它还可以取得线程返回值。

int kthread_stop(struct task_struct *k)
{
    struct kthread *kthread;
    int ret;

    trace_sched_kthread_stop(k);

    get_task_struct(k);
    kthread = to_kthread(k);
    set_bit(KTHREAD_SHOULD_STOP, &kthread->flags);//置位KTHREAD_SHOULD_STOP
    kthread_unpark(k);
    wake_up_process(k);
    wait_for_completion(&kthread->exited);//等待线程退出,会在do_exit里发送该信号
    ret = k->exit_code;
    put_task_struct(k);

    trace_sched_kthread_stop_ret(ret);
    return ret;
}

put_task_struct会将task_struct的引用计数减1,并判断是否为0,为0则销毁task_struct。

static inline void put_task_struct(struct task_struct *t)
{
    if (refcount_dec_and_test(&t->usage))
        __put_task_struct(t);
}

关于销毁的细节我们会放到其他章节来介绍。

内核线程的停车技术

所谓的停车技术就是让内核线程随时停止执行,所用的伎俩和线程退出技术一样。编程范式如下:

//线程主体
thread_fun()
{
    //do your work
    while(1)
    {
         if(kthread_should_park())//有人叫我停车了
             kthread_parkme();//那我就只好停车了
         //do your work
    }
}

可以调用kthread_park让线程停车。

kthread_park用于置位线程停车标志位KTHREAD_SHOULD_PARK:

int kthread_park(struct task_struct *k)
{
    struct kthread *kthread = to_kthread(k);

    if (WARN_ON(k->flags & PF_EXITING))
        return -ENOSYS;

    if (WARN_ON_ONCE(test_bit(KTHREAD_SHOULD_PARK, &kthread->flags)))
        return -EBUSY;

    set_bit(KTHREAD_SHOULD_PARK, &kthread->flags);
    if (k != current) {
        wake_up_process(k);//唤醒线程,让它停车
        /*
         * Wait for __kthread_parkme() to complete(), this means we
         * _will_ have TASK_PARKED and are about to call schedule().
         */

        wait_for_completion(&kthread->parked);//等线程停好车,kthread_parkme里会触发停车完成信号
        /*
         * Now wait for that schedule() to complete and the task to
         * get scheduled out.
         */

        WARN_ON_ONCE(!wait_task_inactive(k, TASK_PARKED));
    }

    return 0;
}

kthread_parkme用于实现线程停车操作:

void kthread_parkme(void)
{
    __kthread_parkme(to_kthread(current));
}

static void __kthread_parkme(struct kthread *self)
{
    for (;;) {
        /*
         * TASK_PARKED is a special state; we must serialize against
         * possible pending wakeups to avoid store-store collisions on
         * task->state.
         *
         * Such a collision might possibly result in the task state
         * changin from TASK_PARKED and us failing the
         * wait_task_inactive() in kthread_park().
         */

        set_special_state(TASK_PARKED);//设置为停车状态,哈哈,进程的新增状态之一
        if (!test_bit(KTHREAD_SHOULD_PARK, &self->flags))//再次确认是不是真的要我停车
            break;

        complete(&self->parked);//告诉叫你停车的人,我的车停好了
        schedule();//车停好了,让出cpu
    }
    __set_current_state(TASK_RUNNING);//再次获得调度时,更新为执行态
}

车挺久了,可以用kthread_unpark解除停车状态:

void kthread_unpark(struct task_struct *k)
{
    struct kthread *kthread = to_kthread(k);

    /*
     * Newly created kthread was parked when the CPU was offline.
     * The binding was lost and we need to set it again.
     */

    if (test_bit(KTHREAD_IS_PER_CPU, &kthread->flags))
        __kthread_bind(k, kthread->cpu, TASK_PARKED);

    clear_bit(KTHREAD_SHOULD_PARK, &kthread->flags);
    /*
     * __kthread_parkme() will either see !SHOULD_PARK or get the wakeup.
     */

    wake_up_state(k, TASK_PARKED);
}

内核线程的cpu绑定技术

kthread_bind将创建好的线程绑定在指定的CPU核心上运行。

void kthread_bind(struct task_struct *p, unsigned int cpu)
{
    __kthread_bind(p, cpu, TASK_UNINTERRUPTIBLE);
}

另外可以直接在创建线程的时候就和cpu绑定,使用接口:

struct task_struct *kthread_create_on_cpu(int (*threadfn)(void *data),
                   void *data,
                   unsigned int cpu,
                   const char *namefmt);

关于cpu绑定技术会放到其他章节深入介绍。

内核线程的冷冻技术

进程(线程)冻结技术(freezing of tasks)是指在系统hibernate或者suspend的时候,将用户进程和部分内核线程置于“可控”的暂停状态。

当系统进入hibernate或者suspend的时候,线程如果要响应,那么线程需要使用相应接口将线程冻结。

待补充
冷冻技术会独立一篇介绍。


愿你有所收获…

图:二进制人生公众号



文章转载自二进制人生,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论