Wait for signal (do_wait)
Linux 内核中 do_wait() 函数的实现,该函数是父进程等待子进程结束的系统调用的一部分。它通过在内核模式下等待信号,处理进程终止以及与父子进程相关的机制。让我们详细解读您提供的信息,涉及 do_wait() 的执行过程、内核模块的加载和等待队列的机制。
1. do_wait() 函数的作用
 
-  定义位置: do_wait()函数的实现位于/kernel/exit.c文件中。这个函数是系统调用wait()和waitpid()的核心处理部分,它让父进程等待一个或多个子进程的终止并收集子进程的退出状态。
-  执行流程:当父进程调用 wait()或waitpid()等系统调用时,内核会进入do_wait()函数,进行子进程状态的收集和处理。具体来说,do_wait()会阻塞父进程,直到子进程终止或收到相应信号。
2. 创建 wait_opts 结构体并加入等待队列
 
-  wait_opts结构体:wait_opts是一个结构体,包含了等待操作所需的选项和参数。这个结构体用于保存wait()操作的选项,例如应该等待哪个子进程、是否是非阻塞的等待等。
-  加入等待队列:通过调用 add_wait_queue(),将当前任务(父进程)加入到等待队列中。等待队列是内核的一种数据结构,用来管理等待某个事件(如子进程结束)的进程。当子进程终止时,等待队列中的进程将被唤醒。
do_wait() 函数 
 
long do_wait (struct wait_opts *wo);
-  作用: do_wait()是内核中的一个函数,它处理与wait()和waitpid()系统调用相关的逻辑。父进程通过wait()或waitpid()调用,内核通过do_wait()实现等待子进程的结束,并收集子进程的退出状态。
-  参数 wo:该参数是一个struct wait_opts结构体的指针,包含了等待子进程终止的各种选项和信息。
-  struct wait_opts { enum pid_type wo_type; // 用于标识 PID 的类型 int wo_flags; // 等待选项 (WNOHANG, WEXITED 等) struct pid *wo_pid; // 内核中进程标识符 struct siginfo *wo_info; // 信号信息 int *wo_stat; // 子进程的终止状态 struct rusage *wo_rusage; // 子进程资源使用信息 wait_queue_entry_t child_wait; // 等待队列 int notask_error; // 错误码 };结构体成员解释:
-  enum pid_type wo_type- 定义位置:pid_type在/include/linux/pid.h中定义,表示内核中 PID 的类型。
- 作用:wo_type表示要等待的进程类型(可以是PIDTYPE_PID,PIDTYPE_PGID,PIDTYPE_SID等),它决定了等待的进程是单个进程、进程组还是会话。
 
- 定义位置:
-  int wo_flags- 作用:wo_flags是等待选项的标志。常见的标志有:- WNOHANG:如果没有子进程结束,- wait()不会阻塞,而是立即返回。
- WEXITED:等待已经终止的子进程。
- 还有其他等待标志如 WUNTRACED(等待暂停的子进程)、WCONTINUED(等待恢复运行的子进程)。
 
 
- 作用:
-  struct pid *wo_pid- 作用:wo_pid是内核中表示进程 ID 的数据结构。它用于标识要等待的子进程。可以通过find_get_pid()函数获取对应的pid结构体。
- find_get_pid():这是内核中一个函数,用于从 PID 表中查找并获取与给定进程 ID 对应的- pid结构体。
 
- 作用:
-  struct siginfo *wo_info- 作用:wo_info是一个指向struct siginfo的指针,用于保存进程接收到的信号信息。当子进程终止时,这个结构体包含关于终止信号的详细信息,例如终止原因、信号编号等。
 
- 作用:
-  int *wo_stat- 作用:wo_stat是一个指向整数的指针,表示子进程的终止状态。父进程通过wo_stat获取子进程的退出码。可以使用宏WIFEXITED、WEXITSTATUS、WIFSIGNALED等来解析子进程的终止状态。
 
- 作用:
-  struct rusage *wo_rusage- 作用:wo_rusage用于保存子进程的资源使用情况。它是一个指向struct rusage的指针,记录了子进程的 CPU 时间、内存使用等资源信息。父进程可以通过它来了解子进程的资源消耗。
 
- 作用:
-  wait_queue_entry_t child_wait- 作用:child_wait是一个等待队列条目,它用于让当前任务(即父进程)在等待子进程结束时进入睡眠状态。当子进程结束时,父进程会被唤醒,继续执行。
 
- 作用:
-  int notask_error- 作用:notask_error用于保存出错信息。如果没有可用的子进程或者发生其他错误,notask_error会保存相应的错误码,以便父进程可以进行错误处理。
 
- 作用:
do_wait() 的工作流程
 
-  初始化 wait_opts:父进程调用wait()或waitpid(),内核初始化一个wait_opts结构体,将父进程的等待选项、目标子进程、信号信息等填入其中。
-  将父进程加入等待队列: do_wait()会调用add_wait_queue()将当前父进程加入到等待队列中,更新进程状态为TASK_INTERRUPTIBLE,等待子进程终止。
-  子进程终止后唤醒父进程:当子进程结束时,子进程会调用 wake_up_parent()来唤醒父进程。父进程从睡眠中被唤醒,并开始处理子进程的终止状态。
-  处理子进程的终止状态:父进程通过 wo_stat获取子进程的退出状态,通过wo_rusage获取子进程的资源使用信息。然后根据子进程的终止原因,父进程可以执行后续的逻辑。
-  返回结果: do_wait()返回子进程的退出状态或错误信息。
相关函数
-  find_get_pid():用于查找并获取pid结构体。它会从 PID 表中根据指定的 PID 找到对应的内核进程结构。
-  set_current_state():在父进程进入等待状态时,set_current_state()会将其状态设置为TASK_INTERRUPTIBLE,即可中断的睡眠状态。
-  wake_up_parent():当子进程终止时,内核会调用该函数唤醒等待的父进程。
这个函数怎么用?
do_wait() 本身是内核代码的一部分,应用程序不会直接使用它。用户态程序会通过 wait() 或 waitpid() 系统调用来等待子进程结束,内核在处理这些系统调用时,会间接调用 do_wait()。所以,作为开发者,你只需要在用户态程序里用 wait() 或 waitpid() 来等待子进程。
wait() 和 waitpid() 用法示例
 
假设你有一个程序,它创建了一个子进程,然后父进程需要等待子进程执行完毕,收集子进程的退出状态。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
    pid_t pid = fork();  // 创建子进程
    if (pid == 0) {
        // 子进程
        printf("Child process running...\n");
        sleep(2);  // 模拟子进程执行的工作
        exit(42);  // 子进程退出,返回码为42
    } else if (pid > 0) {
        // 父进程
        int status;
        pid_t child_pid = wait(&status);  // 父进程等待子进程结束
        
        if (WIFEXITED(status)) {  // 检查子进程是否正常退出
            printf("Child process %d terminated with exit status %d\n", child_pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {  // 检查子进程是否被信号终止
            printf("Child process %d was killed by signal %d\n", child_pid, WTERMSIG(status));
        }
    } else {
        // 如果 fork 失败
        perror("Fork failed");
        exit(1);
    }
    return 0;
}
执行流程
-  父进程创建子进程: fork()创建一个新的子进程,子进程执行它的任务,例如sleep(2)模拟工作,并在结束时返回一个退出状态42。
-  父进程等待子进程: wait()调用会让父进程阻塞,直到它的子进程终止。此时,内核就会调用do_wait()来处理这个等待请求。
-  内核的工作(通过 do_wait()):- 内核将父进程放入等待队列中,让父进程进入“睡眠”状态,直到子进程结束。
- 当子进程结束时,内核通过 do_wait()收集子进程的终止状态,并通过wake_up_parent()将父进程从等待队列中唤醒。
 
-  父进程获得子进程状态:父进程被唤醒后,它通过 wait()得到子进程的 PID 和退出状态,并根据WIFEXITED()和WEXITSTATUS()检查子进程是否正常结束,以及结束的状态码。
wait() 和 waitpid() 的区别:
 
- wait():等待任意子进程结束。父进程会一直阻塞,直到一个子进程结束。
- waitpid():可以指定等待特定的子进程,也可以通过选项(如- WNOHANG)实现非阻塞等待。
比喻
可以把 do_wait() 想象成一个工厂中的“工头”(内核),当父进程(工厂经理)创建了子进程(工人)去干活时,工头会一直盯着这些工人,直到他们的任务完成。如果工人完成了任务(子进程终止),工头就会立即告诉经理(唤醒父进程),并报告工人的工作结果(子进程的退出状态)。do_wait() 就是这个“工头”,负责监视和报告工人(子进程)状态。在比喻中,wait() 函数可以被比喻成“工厂经理”的耳朵。它负责监听“工头”(也就是 do_wait(),内核中的实际处理机制)是否有反馈,也就是子进程(工人)的工作是否完成。
1. 进程描述符 (task_struct):
 
每个进程在内核中都有一个对应的 task_struct,这个结构体包含了与该进程相关的所有信息,包括信号处理的信息。task_struct 是进程的核心数据结构,内核通过它跟踪每个进程的状态。
在 task_struct 中,有一些与信号处理相关的字段和数据结构,比如:
- pending:挂起的信号列表。用于记录进程当前有哪些信号正在等待处理。
- blocked:被屏蔽的信号集。表示进程当前屏蔽了哪些信号,这些信号暂时不会被处理。
- sighand:信号处理程序的指针。指向- struct sighand_struct,保存了进程当前的信号处理程序。
2. struct sigpending:
 
sigpending 结构体用于保存挂起信号的集合。进程或线程在某些情况下接收到信号,但还不能处理(例如被屏蔽),这些信号会被存储在 sigpending 中。挂起信号一旦条件允许,会被取出并处理。
struct sigpending {
    struct list_head list;   // 挂起的信号链表
    sigset_t signal;         // 信号集,表示哪些信号挂起
};
- list:这是一个链表,保存了所有挂起的信号。
- signal:这是一个位图,用来表示有哪些信号处于挂起状态。
3. sigset_t:
 
sigset_t 是一个信号集数据类型,用来表示一组信号。它通常用于屏蔽信号,或者表示挂起的信号。
- blocked:- task_struct中的- blocked是一个- sigset_t类型的字段,用来表示进程当前屏蔽的信号集。被屏蔽的信号不会被立即处理,直到它们从屏蔽集中移除。- sigset_t blocked; // 屏蔽的信号
- 操作函数:通过 sigprocmask()系统调用,用户进程可以设置或修改进程的信号屏蔽集。
- 忽略信号
- 捕获信号并调用自定义处理函数
- 使用默认处理方式(如终止进程)
-  4.struct sighand_struct:struct sighand_struct保存了进程的信号处理程序的信息。它包含每个信号对应的处理函数(sigaction),每个信号都有一个处理方式,比如:struct sighand_struct { atomic_t count; // 引用计数,表示有多少线程共享这个处理程序 struct k_sigaction action[_NSIG]; // 信号处理数组,保存每个信号的处理方式 };
- action[_NSIG]:这是一个数组,存储每个信号的处理程序。每个信号(从- SIG_0到- SIG_64)都有一个对应的- k_sigaction结构,用于描述该信号的处理方式。
5. struct k_sigaction:
 
k_sigaction 结构体用于保存信号的具体处理方式。每个信号都有一个对应的 k_sigaction 结构,其中包含该信号的处理程序以及其他相关的信息。
struct k_sigaction {
    struct sigaction sa;    // 信号处理动作,用户层的处理信息
    unsigned long sa_flags; // 信号处理的标志
};
- sa:这是一个- sigaction结构体,保存用户空间定义的信号处理程序。- sa_handler:指向信号处理函数的指针,或者表示对该信号的默认处理或忽略。
- sa_mask:处理该信号时要屏蔽的其他信号。
- sa_flags:信号处理的标志,控制信号的具体处理行为,例如- SA_RESTART(自动重启被中断的系统调用)。
 
- sa_flags:指定信号处理的行为,比如是否在处理信号时屏蔽其他信号,是否自动重启被信号中断的系统调用。
6. 信号处理流程概述:
当一个进程接收到信号时,内核会根据以下流程处理信号:
- 接收信号:进程接收到信号后,如果信号未被屏蔽,它会被添加到 task_struct中的pending列表中。
- 检查屏蔽集:内核会检查 blocked信号集,判断该信号是否被屏蔽。如果信号被屏蔽,则会保持挂起状态,不会立即处理。
- 查找处理方式:如果信号未被屏蔽,内核会查找该信号的处理方式(k_sigaction),然后根据定义的处理程序执行相应的操作:- 如果 sa_handler设置为自定义处理函数,调用该函数。
- 如果 sa_handler设置为SIG_IGN,忽略该信号。
- 如果 sa_handler设置为SIG_DFL,使用信号的默认处理方式。
 
- 如果 
- 唤醒等待的进程:如果有进程在等待该信号(例如 wait()或select()),信号处理完成后,等待中的进程会被唤醒。
7. 信号相关的系统调用:
- kill():向指定的进程发送信号。
- sigaction():设置或获取信号的处理程序。
- sigprocmask():设置或获取当前进程的信号屏蔽集。

我们有一个办公室场景:
1. 办公室的员工 = 进程
每个员工(进程)都在忙于自己的工作(运行程序)。有时,外部的事情需要打断他们的工作,像电话铃声、电子邮件,或者老板的指令,这些都是“信号”。
2. 信号 = 电话、电子邮件、老板的指示
信号就是这些打断员工的事件,比如:
- 电话响了(信号 SIGINT),可能要求员工立即停止工作。
- 收到了一封重要邮件(信号 SIGTERM),要求员工保存工作并退出。
- 老板发来了一条消息(信号 SIGHUP),可能要求员工重新启动项目。
3. 信号处理的配置 = 员工对待打断的态度
每个员工都可以有不同的态度来处理这些“打断事件”(信号)。这就是信号的“处理方式”:
- 忽略:员工可以选择不接电话(忽略信号)。
- 响应:员工也可以设置一个特定的响应动作,比如接听电话并按老板的要求去做(自定义信号处理函数)。
- 默认反应:如果员工没有特别设定,他们可能会根据习惯来应对(默认信号处理)。比如,有些员工听到电话响了就会直接挂断(默认终止进程)。
4. 信号屏蔽集 = 员工的专注模式
有时员工进入了“专注模式”,不想被任何事情打扰。这相当于信号屏蔽集(blocked)。当员工处于这种状态时,他们会暂时不接电话、也不处理任何电子邮件(屏蔽信号)。这些未处理的信号被“挂起”了。
5. 挂起信号 = 等待处理的电话/邮件
如果一个信号到来了,但员工正在专注工作而无法处理(屏蔽信号),信号会进入“挂起状态”。相当于电话在“未接电话列表”中,或者电子邮件在“收件箱”里没有被处理。
6. 信号处理程序 = 员工的应对策略
每个员工可以提前为某些“信号”设定好应对策略。就像在办公室里,如果电话响了:
- 默认处理方式:员工可以让电话铃响几下后自动挂断(SIG_DFL)。
- 忽略处理方式:员工可能设置手机为静音,永远不接电话(SIG_IGN)。
- 自定义处理方式:员工可以设定某个电话铃声响起时,他们会暂停手上的工作,然后处理电话。
7. k_sigaction = 员工的备忘录
每个员工都有一本“备忘录”(k_sigaction),记录了如果电话来了该怎么做。如果是老板来电,他们可能会停止当前的工作立即执行老板的任务。如果是垃圾邮件,他们可能会忽略。这本备忘录告诉员工该如何处理不同的打断事件。
8. 信号挂起时处理的过程:
当电话(信号)响起,而员工正在专注模式中无法处理时,电话被挂起。等员工解除专注模式后,检查未接电话(挂起的信号),并根据备忘录(k_sigaction)逐一处理这些打断。
总结:
- 信号:像电话、电子邮件、老板的指示,它们打断进程的正常执行。
- 信号处理程序:员工可以选择忽略、响应,或按默认规则处理这些打断。
- 屏蔽信号:员工在专注模式时暂时不接电话,但这些电话(信号)会保留在待处理列表中。
- 挂起信号:员工暂时没有处理的“未接电话”或“未读邮件”,等专注模式结束后会处理。
内核线程(kthread)
 
在 Linux 内核中,内核线程(kthread)是内核中独立执行的任务,类似于用户空间的线程,但运行在内核态。kthread 的创建和执行涉及到几个关键函数,包括 kthread_create() 用于创建线程,以及线程的执行函数 thread_function()。
1. kthread_create() - 创建内核线程
struct task_struct *kthread_create(
    int (*threadfn)(void *data),
    void *data,
    const char *namefmt, ...);
-  说明: - threadfn:这是线程的主函数,当线程开始运行时,会执行这个函数。函数的原型是- int thread_function(void *data)。
- data:传递给线程函数- threadfn的参数,它是一个- void *指针,允许传递任何类型的数据给线程函数。
- namefmt:线程的名字,可以通过格式化字符串(类似- printf())为线程命名。
 
-  返回值: - 返回的是一个指向 struct task_struct的指针,表示内核中创建的线程。task_struct是 Linux 内核中表示一个进程或线程的主要数据结构。
 
- 返回的是一个指向 
-  线程不立即运行: - 调用 kthread_create()创建线程后,线程不会立即运行。线程只有在调用wake_up_process()函数时才会真正开始执行。struct task_struct *my_kthread; my_kthread = kthread_create(thread_function, NULL, "my_kthread"); if (my_kthread) { wake_up_process(my_kthread); // 唤醒并启动线程 }在上面的代码中, kthread_create()创建了一个名为"my_kthread"的内核线程,但它不会立即执行。随后通过wake_up_process()来启动这个线程。
-  thread_function()- 线程执行函数
-  这是内核线程的实际执行代码,它是由 kthread_create()传递的函数。函数接受一个void *data参数,通常是用于传递给线程的一些上下文或数据。int thread_function(void *data) { // 线程的实际工作代码 while (!kthread_should_stop()) { // 线程的主要工作循环 // 执行一些工作 schedule(); // 手动调用调度器,让出 CPU } // 如果 `kthread_should_stop()` 返回 true,表示要停止线程 return 0; // 返回时线程结束 }
-  kthread_should_stop():这是一个检查函数,判断内核线程是否应该停止。当kthread_stop()被调用时,kthread_should_stop()会返回true,这时线程应该退出执行循环并结束。
-  直接调用 do_exit():- 对于某些“独立”的线程,可能没有调用 kthread_stop()的需求,这时线程可以直接调用do_exit()函数来终止自己。
 
- 对于某些“独立”的线程,可能没有调用 
-  返回机制: - 内核线程可以通过返回值来结束,当 kthread_should_stop()返回true时,线程可以通过简单返回结束自己。
 如果线程是独立的、不需要停止控制(例如,系统中没有其他地方会调用 kthread_stop()来停止它),可以直接调用do_exit()来退出线程。
- 内核线程可以通过返回值来结束,当 
-  3.kthread_stop()- 停止内核线程如果您想停止一个内核线程,可以调用 kthread_stop(),这会通知线程应该停止,并等待线程的退出。int kthread_stop(struct task_struct *k);
 
- 调用 
- kthread_stop()的作用:当您调用- kthread_stop()时,会设置线程的内部状态,让- kthread_should_stop()函数返回- true。线程一旦检测到- kthread_should_stop()为- true,应该尽快退出。- kthread_stop()会阻塞,直到线程退出。
4. 线程的生命周期总结
- 创建线程:调用 kthread_create()创建一个线程,但线程不会立即开始运行。
- 启动线程:通过 wake_up_process()启动线程,让它开始执行thread_function()。
- 线程执行循环:在 thread_function()中,线程通常会在循环中执行任务,并定期检查kthread_should_stop()来判断是否要退出。
- 停止线程:如果需要停止线程,可以调用 kthread_stop(),这会通知线程停止,并等待线程安全退出。
5. 示例 - 完整的内核线程创建与停止
#include <linux/kthread.h>
#include <linux/delay.h>
struct task_struct *my_thread;
// 线程函数
int thread_function(void *data) {
    while (!kthread_should_stop()) {
        printk(KERN_INFO "Thread running...\n");
        ssleep(5);  // 模拟工作,睡眠5秒
    }
    printk(KERN_INFO "Thread stopping...\n");
    return 0;
}
// 模块加载时创建线程
static int __init my_module_init(void) {
    my_thread = kthread_create(thread_function, NULL, "my_thread");
    if (my_thread) {
        wake_up_process(my_thread);  // 启动线程
        printk(KERN_INFO "Thread created and started.\n");
    } else {
        printk(KERN_ERR "Failed to create thread.\n");
    }
    return 0;
}
// 模块卸载时停止线程
static void __exit my_module_exit(void) {
    if (my_thread) {
        kthread_stop(my_thread);  // 停止线程
        printk(KERN_INFO "Thread stopped.\n");
    }
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example Kernel Thread");
MODULE_AUTHOR("Your Name");
6. 关键步骤总结:
- 创建线程:通过 kthread_create()创建一个线程,返回task_struct指针。
- 启动线程:调用 wake_up_process()启动线程。
- 线程循环:线程通过检查 kthread_should_stop()确定是否需要停止。
- 停止线程:通过 kthread_stop()来停止线程,并等待线程安全退出。
通过这些步骤,您可以在 Linux 内核中创建、运行和停止内核线程,使其执行特定的任务并安全退出。
这张图片进一步解释了内核线程创建、启动和运行的函数以及其返回值。
1. 返回值 (task_struct)
 
-  成功时:如果内核线程创建成功, kthread_create()会返回一个指向内核线程的task_struct结构体的指针。task_struct是 Linux 内核中描述线程或进程的主要数据结构,它包含了线程的状态、PID 以及其他与线程相关的信息。
-  失败时:如果创建失败, kthread_create()会返回ERR_PTR,这是一个错误指针,用来表示具体的错误。
2. 启动线程 (wake_up_process())
 
-  wake_up_process(struct task_struct *p):创建线程后,使用wake_up_process()来启动线程。- 该函数会将线程从创建状态变为可运行状态,这意味着它已经准备好被调度执行了。
- 传递的参数是指向内核线程的 task_struct指针。
 
-  ERR_PTR:如果wake_up_process()失败,它可能会返回错误指针(ERR_PTR),指示出错的原因。
3. kthread_run() - 创建并启动线程的快捷函数
 
 
2. 运行环境的差异
-  kthread_run()是一个方便的内核 API,它相当于kthread_create()和wake_up_process()的组合。
-  参数: - threadfn:要在线程中执行的函数。
- data:传递给- threadfn的数据。
- namefmt:线程的名称,类似- printf()格式化字符串。
 
-  功能: - kthread_run()直接创建并启动一个线程,相当于调用- kthread_create()后立刻调用- wake_up_process()。
- 这样,您不需要单独调用 wake_up_process(),使得代码更加简洁。struct task_struct *my_kthread; // 线程函数 int my_thread_function(void *data) { while (!kthread_should_stop()) { // 执行线程的主要任务 printk(KERN_INFO "Thread is running...\n"); ssleep(1); // 模拟工作,睡眠1秒 } return 0; } // 创建并启动线程 my_kthread = kthread_run(my_thread_function, NULL, "my_thread"); if (IS_ERR(my_kthread)) { printk(KERN_ERR "Failed to create thread.\n"); } else { printk(KERN_INFO "Thread started successfully.\n"); }在这个示例中, kthread_run()创建了一个内核线程,并立刻启动它执行my_thread_function()函数。线程的名字是"my_thread",线程会一直运行,直到kthread_stop()被调用,kthread_should_stop()返回true。总结:
- kthread_create()用于创建线程,返回一个- task_struct指针,表示线程的结构。
- wake_up_process()用于启动线程。
- kthread_run()是一个简化的 API,它可以同时创建并启动线程,使代码更加简洁。
 
-  kthread_create()和用户空间的fork()调用之间的区别主要在于内核线程和用户线程的不同。它们的使用场景、运行环境、以及实现方式都不同。下面我会详细对比这两者的区别。1. 内核线程 (kthread_create()) 和用户进程 (fork()) 的本质区别
- 内核线程: 
  - 定义:内核线程是运行在 内核空间 的线程,专门用于执行内核的任务。
- 创建方式:内核线程通过 kthread_create()创建,运行在内核态,不受用户空间的直接影响。
- 执行环境:内核线程不属于任何用户空间进程,它只存在于内核中,用于处理内核任务(例如调度器、内核服务等)。
- 内存空间:内核线程使用的是内核的地址空间,它没有自己独立的虚拟地址空间,和内核共享同一地址空间。
- 功能:内核线程通常用于处理不与用户空间直接交互的后台任务,如设备驱动、内核模块的调度任务等。
 
- 用户进程: 
  - 定义:用户进程是在 用户空间 运行的进程,它们是用户程序(如文本编辑器、浏览器等)的实例。
- 创建方式:用户进程通过 fork()来创建,fork()会复制调用进程的地址空间,生成一个新的用户进程(子进程)。
- 执行环境:用户进程在用户态运行,且可以通过系统调用请求内核服务。它有自己独立的地址空间和权限。
- 内存空间:fork()创建的子进程有独立的虚拟地址空间,和父进程的地址空间是分离的(虽然开始时它们是相同的,但写时复制技术确保了之后它们独立修改内存)。
- 功能:用户进程用于运行用户应用程序,并通过系统调用与内核进行交互。
 
- 内核线程: 
  - 运行在内核态,它们可以直接访问和操作内核的数据结构和硬件资源。
- 没有用户空间的限制(如访问控制、权限限制等),可以执行特权操作。
- 内核线程的调度由内核直接控制,通常用于执行系统底层操作。
 
- 用户进程: 
  - 运行在用户态,与内核隔离。用户进程需要通过系统调用来与内核交互。
- 受到内核的限制,不能直接访问内核的内存或硬件资源。
- 通过用户态的调度机制进行调度,用户态进程可以被内核挂起、切换或终止。
-  总结特性 内核线程 ( kthread_create())用户进程 ( fork())执行空间 内核空间 用户空间 内存空间 与内核共享内存地址空间 拥有独立的虚拟内存地址空间 创建方式 通过 kthread_create()创建,不自动启动通过 fork()创建,自动开始执行启动方式 需要手动调用 wake_up_process()创建后立即执行 使用场景 内核模块、驱动程序、后台管理任务 用户应用程序,如服务器、并行任务 调度 由内核调度器调度,始终运行在内核态 由内核调度器调度,运行在用户态 
 

#include <linux/init.h>      // 包含模块初始化和退出的头文件
#include <linux/module.h>    // 包含模块的基本定义
#include <linux/kthread.h>   // 包含内核线程相关的函数和定义
MODULE_LICENSE("GPL");  // 模块的许可证声明
static struct task_struct *task;  // 定义一个内核线程的任务结构体指针,用来存储线程信息
// 实现测试函数,供内核线程执行
int func(void *data) {
    int time_count = 0;  // 计数器,记录线程执行次数
    do {
        printk(KERN_INFO "thread_function: %d times", ++time_count);  // 每次循环打印线程运行次数
    } while (!kthread_should_stop() && time_count < 30);  // 如果没有收到停止信号,并且执行次数小于30,则继续循环
    return time_count;  // 返回执行次数
}
// 模块初始化函数,在模块加载时调用
static int __init KT_init(void) {
    printk("KT module create kthread start\n");  // 打印模块加载信息
    // 创建内核线程,线程的执行函数是 func,线程名为 "MyThread"
    task = kthread_create(&func, NULL, "MyThread");
    // 如果线程创建成功,启动线程
    if (!IS_ERR(task)) {
        printk("kthread starts\n");  // 打印线程启动信息
        wake_up_process(task);  // 唤醒并启动线程,线程开始执行
    }
    return 0;  // 初始化函数返回 0 表示成功
}
// 模块退出函数,在模块卸载时调用
static void __exit KT_exit(void) {
    printk("KT module exits! \n");  // 打印模块退出信息
    // 漏掉的步骤:停止线程
    if (task) {
        kthread_stop(task);  // 调用 kthread_stop() 停止内核线程
    }
}
// 指定模块初始化和退出时调用的函数
module_init(KT_init);  // 指定 KT_init 为模块加载时调用的函数
module_exit(KT_exit);  // 指定 KT_exit 为模块卸载时调用的函数
代码解释
这段代码是一个用于创建和启动内核线程的 Linux 内核模块。它通过 kthread_create() 创建一个线程,并通过 wake_up_process() 启动该线程执行 func() 函数。内核模块的生命周期由 KT_init() 和 KT_exit() 控制。
代码详细解读:
-  task_struct *task:- 定义了一个指向内核线程的 task_struct指针,用于存储创建的内核线程信息。
 
- 定义了一个指向内核线程的 
-  func(void *data):- 这是内核线程执行的函数。它是一个循环,每次循环输出线程执行的次数 time_count,并且在kthread_should_stop()返回true或计数达到 30 时退出循环。最后返回time_count。
- 逻辑:线程在最多循环 30 次时退出,并且每次循环时打印一次日志。
 
- 这是内核线程执行的函数。它是一个循环,每次循环输出线程执行的次数 
-  KT_init():- 内核模块加载时的初始化函数。这里的主要工作是创建一个内核线程,并在创建成功后启动该线程。
- kthread_create():用于创建内核线程,传入的参数包括- func(线程执行的函数)、- NULL(传递给- func的数据,未使用)、- "MyThread"(线程的名字)。
- wake_up_process():如果线程创建成功(未返回- IS_ERR()),则调用- wake_up_process()启动线程。
 
-  KT_exit():- 内核模块卸载时调用的清理函数。在这里,它只打印一条消息并退出。通常,kthread_stop()会在这里调用以停止线程,但代码中没有调用它。
 
- 内核模块卸载时调用的清理函数。在这里,它只打印一条消息并退出。通常,
-  module_init()和module_exit():- 它们分别指示模块加载时应调用的初始化函数 KT_init()和卸载时应调用的清理函数KT_exit()。
 
- 它们分别指示模块加载时应调用的初始化函数 
终端输出解释:
- insmod KT.ko:这条命令加载内核模块- KT.ko。加载后,模块开始执行- KT_init(),创建并启动内核线程。
- lsmod | grep KT:显示内核模块列表,确认- KT.ko已被加载。
- rmmod KT.ko:卸载内核模块,执行- KT_exit()并打印相应的退出消息。
比喻
你可以把这个内核线程比作一个办公室的自动报告机器人:
- 办公室(内核环境):这是系统内部的环境,只有授权的人(内核线程)可以在这里运行。
- 机器人(内核线程):当内核模块加载时(办公室打开),一个机器人被制造出来,它会每隔一段时间记录自己的运行次数,最多记录 30 次。
- 机器人启动的按钮(wake_up_process()):虽然机器人已经被制造出来,但它还不会立即开始工作,只有当有人按下启动按钮(调用wake_up_process())时,它才会开始执行任务。
- 机器人检查信号(kthread_should_stop()):机器人每次工作时都会检查它的控制面板,看看是否有人要求它停止工作。如果有人按下停止按钮,它会立即停止任务。
在机器人完成了任务(循环 30 次)或者收到停止信号(kthread_should_stop() 返回 true)时,它会自动返回,并停止继续工作。
什么是模块?
在 Linux 内核中,模块(Module)是指一种可以动态加载和卸载的可执行代码。它允许你在不重启系统或重新编译内核的情况下,扩展或修改内核的功能。模块的最常见用途之一就是实现设备驱动或其他特定功能,比如网络协议、文件系统等。
模块的特点:
-  动态性: - 模块可以根据需要动态加载(insmod)到内核中,或者动态卸载(rmmod)。不需要重新编译或启动内核即可更改系统功能。
 
- 模块可以根据需要动态加载(
-  可插拔: - 你可以将模块想象成一种“插件”,它可以在需要时插入内核,为内核提供某种特定的功能,而当功能不再需要时,你可以安全地将其移除。
 
-  节省资源: - 由于模块是按需加载的,因此它们可以减少内核的内存使用。在不需要特定功能时,模块可以保持未加载的状态,从而节省系统资源。
 
模块的组成:
-  代码:模块本身是一段可以执行的代码,通常是以 .ko(kernel object) 文件的形式存在。这些代码与内核交互,提供特定的功能,比如设备驱动、文件系统支持等。
-  初始化函数 ( module_init):模块加载到内核时,会执行初始化函数,这个函数通常用来分配资源或启动某些服务。
-  退出函数 ( module_exit):当模块被卸载时,会执行退出函数,这个函数通常用来清理资源和终止与模块相关的服务。
模块的工作原理:
-  模块加载 ( insmod):- 当你需要加载某个模块时,使用 insmod命令加载模块。
- 例如,insmod my_module.ko会将模块my_module加载到内核中,并调用模块的初始化函数 (module_init)。
 
- 当你需要加载某个模块时,使用 
-  模块运行: - 一旦加载,模块便会像内核的其他部分一样运行。模块可以调用内核提供的 API,访问内核数据结构,甚至创建线程(例如你之前代码中的 kthread_create)。
 
- 一旦加载,模块便会像内核的其他部分一样运行。模块可以调用内核提供的 API,访问内核数据结构,甚至创建线程(例如你之前代码中的 
-  模块卸载 ( rmmod):- 当模块的功能不再需要时,你可以使用 rmmod命令将其卸载。
- 例如,rmmod my_module会卸载模块,并调用模块的退出函数 (module_exit) 来清理资源。
 
- 当模块的功能不再需要时,你可以使用 

1. 什么是 LKM(可加载内核模块)?
LKM 是一种独立编译的内核扩展,可以在操作系统启动后动态加载和卸载,允许内核根据需要增加或减少功能。它们通常用于:
- 添加对新硬件的支持,比如设备驱动程序。
- 增加对新文件系统的支持,比如挂载某些文件系统格式。
- 扩展内核功能,比如增加新的系统调用。
LKM 提供了一种模块化的机制来扩展内核功能,避免在添加新功能时必须重新编译整个内核。
2. LKM 的主要用途
- 硬件支持:内核模块通常用于设备驱动程序,这意味着在引入新硬件(例如 USB 设备、网卡或硬盘控制器)时,不需要重新编译内核,而是通过加载模块来实现支持。
- 文件系统支持:一些文件系统(如 ext4、btrfs)的支持通常作为模块实现,当需要挂载这些文件系统时,可以加载相应的模块来处理。
- 系统调用:LKM 还可以用于扩展内核的 API,例如在开发调试环境中添加自定义的系统调用。
3. LKM 的跨平台支持
几乎所有现代的类 Unix 操作系统都支持 LKM,虽然它们可能在不同操作系统上使用不同的名称。例如:
- Linux:LKM 在 Linux 中通常以 .ko文件的形式存在,可以通过insmod或modprobe命令加载。
- MacOS:在 MacOS 上,LKM 被称为内核扩展(Kernel Extension, kext)。它们通常用于类似的目的,如支持新硬件、文件系统和扩展系统功能。
- FreeBSD 和 Solaris 也支持类似的模块化内核设计,尽管具体实现有所不同。
4. LKM 的优点
-  动态加载/卸载:LKM 可以在系统运行时加载和卸载,这使得内核能够根据需求动态增加或减少功能。无需重启系统,节省了大量时间。 
-  模块化开发:开发人员可以编写、测试和调试单独的内核模块,而不需要重新编译整个内核。这为开发和维护内核代码提供了更高的灵活性。 
-  节省内存:只有在需要特定功能时才加载相应的模块,节省了系统资源。 
5. LKM 的工作原理
LKM 在运行时被加载到内核空间,与内核共享相同的地址空间。这使得 LKM 可以直接访问内核的内部数据结构和函数,但也需要小心处理,因为 LKM 一旦出现问题(如崩溃),可能会导致整个系统不稳定。
-  加载模块:通过命令 insmod或modprobe来加载一个.ko文件(内核模块)。例如,在 Linux 中,加载模块的命令:  
1. Makefile
Makefile 是一种自动化工具,用于编译程序。在 Linux 内核模块编译中,Makefile 扮演着非常重要的角色。它帮助我们定义了如何构建内核模块以及如何清理生成的文件。
Makefile 的内容解释:
obj-m := KM.o          # 指定要编译的目标模块对象,KM.o 是内核模块的名称
KVERSION := $(shell uname -r)  # 获取当前运行的内核版本
PWD := $(shell pwd)           # 获取当前工作目录的路径
all:
    $(MAKE) -C /lib/modules/$(KVERSION)/build M=$(PWD) modules
    # 使用内核的 build 系统,编译模块。-C 选项指定内核源代码路径,M=$(PWD) 表示模块的路径
clean:
    $(MAKE) -C /lib/modules/$(KVERSION)/build M=$(PWD) clean
    # 清理生成的中间文件
-  obj-m := KM.o:这行指定了要生成的模块名称为KM.o。这是模块的目标文件,KM.o最终会被生成为KM.ko文件,这就是可加载的内核模块文件。
-  KVERSION := $(shell uname -r):使用uname -r命令获取当前内核的版本号,存储在KVERSION变量中。这对于确保我们编译的模块与当前正在运行的内核兼容非常重要。
-  PWD := $(shell pwd):pwd命令获取当前的工作目录,$(PWD)用来表示当前路径。
-  all目标:这定义了编译内核模块的步骤。通过调用$(MAKE),并使用-C /lib/modules/$(KVERSION)/build,告诉make工具去内核源代码目录下找到适当的编译规则,并在M=$(PWD)目录下执行模块编译。
-  clean目标:make clean用于清理编译生成的临时文件或中间文件。调用的指令类似于all,但是带有clean选项,告诉系统清理文件。
2. 编译内核模块
-  输入命令 make:- 当你在终端输入 make命令时,Makefile 会根据all目标来自动编译模块。它会生成.o文件以及最终的.ko文件,即可加载的内核模块。
- 在文件夹中你会看到类似于 KM.o,KM.ko,KM.mod.o, 和KM.mod.c这些文件。
 
- 当你在终端输入 
-  输入命令 make clean:- 当你执行 make clean命令时,Makefile 中的clean目标会被执行。它会删除编译生成的中间文件和目标文件,只保留源代码和 Makefile。这样可以保持工作目录的整洁。
 
- 当你执行 
3. Makefile 的工作流程概述
-  make:当你运行make命令时,它会根据 Makefile 中的规则,调用内核的构建系统,编译你指定的内核模块文件(KM.o),最终生成可加载的内核模块文件(KM.ko)。
-  make clean:用于清理生成的目标文件,移除.o和.ko文件,以便你可以重新编译模块。
4. 目录结构
- 图片中展示了在 LoadModule目录下,生成了几个重要的文件:- KM.c:模块的源代码文件,包含内核模块的实现。
- KM.o和- KM.ko:目标文件和可加载模块文件。
- Makefile:控制模块编译的文件。
 
当执行 make 命令后,KM.ko 就是最终生成的模块文件,你可以使用 insmod 命令将其加载到内核中,或用 rmmod 卸载。
关于如何管理 Linux 内核模块的具体步骤说明。让我们逐步解释一下每个命令的作用,以及它们如何操作内核模块。
1. 切换到 root 账户
$ sudo su
 作用:Linux 内核模块的加载和卸载需要超级用户权限,因此你必须以 root 用户身份进行操作。sudo su 命令允许你从普通用户切换到 root 账户,从而获得系统的最高权限,能够进行内核模块的操作。
2. 插入内核模块
$ insmod MODULE_NAME.ko
-  作用: insmod命令用于将内核模块插入(加载)到正在运行的 Linux 内核中。你需要指定你要加载的模块文件名称,通常是一个以.ko结尾的文件。
-  示例: insmod my_module.ko



















