Linux-进程的管理与调度10(基于6.1内核)---Linux内核线程
一、内核线程
1.1、为什么需要内核线程?
Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。
内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。
内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。
原因如下:
1. 实现内核功能和服务
内核线程通常用来处理操作系统内核级的任务,例如:
- 硬件驱动程序:操作系统需要与硬件设备进行交互,这通常需要在内核空间运行的线程。比如,网络设备、磁盘驱动、USB设备驱动等,都会使用内核线程来处理设备的输入输出操作。
- 中断处理:中断是硬件或外部设备发送到 CPU 的信号,操作系统需要快速响应并处理。内核线程可以用来延续中断处理,处理完成后的任务。
- 系统管理任务:操作系统需要管理资源,例如内存管理、调度管理、进程管理等,许多这类任务需要在内核线程中运行。
2. 与硬件交互
内核线程有直接的硬件访问权限,可以执行诸如内存分配、设备控制和中断处理等需要高权限的任务。用户空间的应用程序无法直接与硬件交互,因为它们受到内核的保护。内核线程则可以运行在内核空间,拥有直接操作硬件的能力。
3. 内存和资源管理
内核线程负责管理系统资源的分配和回收,例如:
- 内存管理:内核线程参与内存的分配、回收和垃圾回收机制,确保系统的稳定性和效率。
- 进程调度:内核线程负责调度用户进程的执行,它们决定哪些进程应该运行,以及何时切换任务。
4. 支持并发和多任务处理
内核线程使得操作系统能够支持并发和多任务处理。操作系统需要在多个线程之间进行调度,以保证每个任务都能适时得到执行。例如,内核线程可能会定期执行定时任务(如任务调度、时钟滴答),确保系统的实时性。
5. 内核的守护线程(Daemon Threads)
许多操作系统内核任务需要长期运行并且不依赖用户进程的生命周期。内核线程可以充当守护线程(daemon threads),它们持续在后台运行,处理一些持续性的任务,如磁盘同步、网络连接管理、日志记录等。
6. 独立于用户空间的执行
内核线程的一个重要特点是它们可以独立于用户空间程序运行,不会受到用户进程崩溃的影响。因为它们运行在内核空间,即使所有的用户进程都崩溃或退出,内核线程依然能够持续运行,保证操作系统的稳定性和可靠性。
7. 提升系统响应性和性能
内核线程有助于提升系统的响应性和性能。例如,在 Linux 中,内核线程可以用来处理 I/O 请求,帮助避免阻塞其他进程。内核线程可以在多核处理器上并行运行,提升整体系统的效率。
8. 处理低级任务(如系统调用)
内核线程也参与处理低级系统调用的执行,例如进程的创建、终止、资源分配等操作。用户进程通过系统调用与内核交互,而这些操作往往需要内核线程来执行具体的工作。
9. 提供高优先级的任务处理
内核线程可以具有高优先级,这使得它们能够比普通的用户进程更早被调度执行。这对于一些紧急的或重要的任务(如中断处理、硬件故障恢复等)非常重要。
1.2、内核线程概述
内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程。
他们执行下列任务:
-
周期性地将修改的内存页与页来源块设备同步。
-
如果内存页很少使用,则写入交换区。
-
管理延时动作, 如2号进程接手内核进程的创建。
-
实现文件系统的事务日志。
内核线程主要有两种类型:
-
线程启动后一直等待,直至内核请求线程执行某一特定操作。
-
线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。
内核线程由内核自身生成,其特点在于:
-
它们在CPU的管态执行,而不是用户态。
-
它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间
1.3、内核线程的进程描述符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 Process)这一术语通常指的是一个没有 mm 指针(指向内存管理结构)的进程。在Linux等操作系统中,mm 指针是指向当前进程的内存管理结构(mm_struct),它包含了进程的虚拟内存布局、页表、内存映射等信息。
TLB(Translation Lookaside Buffer)
TLB(转换后备缓冲区)是一个CPU缓存,用于加速虚拟地址到物理地址的映射。每当进程访问一个虚拟地址时,CPU会首先检查TLB缓存,如果找到有效的映射,它就可以直接使用,而无需查找完整的页表。TLB命中会大大加速地址转换过程。如果TLB中没有找到对应的虚拟地址映射(TLB未命中),CPU会触发一个页面错误,并通过查找页表来进行映射。
这位优化提供了一些余地, 可遵循所谓的惰性TLB处理(lazy TLB handing)。active_mm主要用于优化,由于内核线程不与任何特定的用户层进程相关,内核并不需要倒换虚拟地址空间的用户层部分,保留旧设置即可。由于内核线程之前可能是任何用户层进程在执行,故用户空间部分的内容本质上是随机的,内核线程决不能修改其内容,故将mm设置为NULL,同时如果切换出去的是用户进程,内核将原来进程的mm存放在新内核线程的active_mm中,因为某些时候内核必须知道用户空间当前包含了什么。
为什么没有mm指针的进程称为惰性TLB进程?
假如内核线程之后运行的进程与之前是同一个, 在这种情况下, 内核并不需要修改用户空间地址表。地址转换后备缓冲器(即TLB)中的信息仍然有效。只有在内核线程之后, 执行的进程是与此前不同的用户层进程时, 才需要切换(并对应清除TLB数据)。
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm指针被设置为NULL;它只在内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。
二、内核线程的创建
2.1、创建内核线程接口
创建内核更常用的方法是辅助函数kthread_create,该函数创建一个新的内核线程。最初线程是停止的,需要使用wake_up_process启动它。
使用kthread_run,与kthread_create不同的是,其创建新线程后立即唤醒它,其本质就是先用kthread_create创建一个内核线程,然后通过wake_up_process唤醒。include/linux/kthread.h
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data,
int node,
const char namefmt[], ...);
/**
* kthread_create - create a kthread on the current node
* @threadfn: the function to run in the thread
* @data: data pointer for @threadfn()
* @namefmt: printf-style format string for the thread name
* @arg: arguments for @namefmt.
*
* This macro will create a kthread on the current node, leaving it in
* the stopped state. This is just a helper for kthread_create_on_node();
* see the documentation there for more details.
*/
#define kthread_create(threadfn, data, namefmt, arg...) \
kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
/**
* 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; \
})
2.2、2号进程kthreadd的诞生
2号进程kthreadd
kthreadd进程, 并随后演变为2号进程, 它在系统初始化时同1号进程一起被创建(当然肯定是通过kernel_thread), 参见rest_init函数, 并随后演变为创建内核线程的真正建造师, 参见kthreadd和kthreadd函数, 它会循环的是查询工作链表static LIST_HEAD(kthread_create_list);中是否有需要被创建的内核线程, 而我们的通过kthread_create执行的操作, 只是在内核线程任务队列kthread_create_list中增加了一个create任务, 然后会唤醒kthreadd进程来执行真正的创建操作。
kernel/kthread.c
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, housekeeping_cpumask(HK_TYPE_KTHREAD));
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))
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;
}
内核线程会出现在系统进程列表中, 但是在ps的输出中进程名command由方括号包围, 以便与普通进程区分。
如下图所示, 我们可以看到系统中, 所有内核线程都用[]标识, 而且这些进程父进程id均是2, 而2号进程kthreadd的父进程是0号进程

2.3、kernel_thread
linux6.1内核的创建工作, 将内核线程的创建工作交给一个内核线程来做, 即kthreadd 2号进程
但是在kthreadd还没创建之前, 我们只能通过kernel_thread这种方式去创建,
同时kernel_thread的实现也改为由kernel_clone来实现,kernel/fork.c
/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, const char *name,
unsigned long flags)
{
struct kernel_clone_args args = {
.flags = ((lower_32_bits(flags) | CLONE_VM |
CLONE_UNTRACED) & ~CSIGNAL),
.exit_signal = (lower_32_bits(flags) & CSIGNAL),
.fn = fn,
.fn_arg = arg,
.name = name,
.kthread = 1,
};
return kernel_clone(&args);
}
2.4、kthread_create
/**
* kthread_create - create a kthread on the current node
* @threadfn: the function to run in the thread
* @data: data pointer for @threadfn()
* @namefmt: printf-style format string for the thread name
* @arg: arguments for @namefmt.
*
* This macro will create a kthread on the current node, leaving it in
* the stopped state. This is just a helper for kthread_create_on_node();
* see the documentation there for more details.
*/
#define kthread_create(threadfn, data, namefmt, arg...) \
kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
struct task_struct *kthread_create_on_cpu(int (*threadfn)(void *data),
void *data,
unsigned int cpu,
const char *namefmt);
创建内核更常用的方法是辅助函数kthread_create,该函数创建一个新的内核线程。最初线程是停止的,需要使用wake_up_process启动它。
2.5、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函数,或者其他的进程调用kthread_stop函数,结束线程的运行。
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(!thread_could_stop())
{
wait();
}
}
exit_code()
{
kthread_stop(_task); //发信号给task,通知其可以退出了
}
这种退出机制很温和,一切尽在thread_func()的掌控之中,线程在退出时可以从容地释放资源,而不是莫名其妙地被人“暗杀”。
867

被折叠的 条评论
为什么被折叠?



