[OS] 2.Wait for signal (do_wait),task_struct

news2024/11/23 9:08:22

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 获取子进程的退出码。可以使用宏 WIFEXITEDWEXITSTATUSWIFSIGNALED 等来解析子进程的终止状态。
  • 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() 的工作流程

  1. 初始化 wait_opts:父进程调用 wait()waitpid(),内核初始化一个 wait_opts 结构体,将父进程的等待选项、目标子进程、信号信息等填入其中。

  2. 将父进程加入等待队列do_wait() 会调用 add_wait_queue() 将当前父进程加入到等待队列中,更新进程状态为 TASK_INTERRUPTIBLE,等待子进程终止。

  3. 子进程终止后唤醒父进程:当子进程结束时,子进程会调用 wake_up_parent() 来唤醒父进程。父进程从睡眠中被唤醒,并开始处理子进程的终止状态。

  4. 处理子进程的终止状态:父进程通过 wo_stat 获取子进程的退出状态,通过 wo_rusage 获取子进程的资源使用信息。然后根据子进程的终止原因,父进程可以执行后续的逻辑。

  5. 返回结果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;
}

执行流程

  1. 父进程创建子进程fork() 创建一个新的子进程,子进程执行它的任务,例如 sleep(2) 模拟工作,并在结束时返回一个退出状态 42

  2. 父进程等待子进程wait() 调用会让父进程阻塞,直到它的子进程终止。此时,内核就会调用 do_wait() 来处理这个等待请求。

  3. 内核的工作(通过 do_wait()

    • 内核将父进程放入等待队列中,让父进程进入“睡眠”状态,直到子进程结束。
    • 当子进程结束时,内核通过 do_wait() 收集子进程的终止状态,并通过 wake_up_parent() 将父进程从等待队列中唤醒。
  4. 父进程获得子进程状态:父进程被唤醒后,它通过 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 是一个信号集数据类型,用来表示一组信号。它通常用于屏蔽信号,或者表示挂起的信号。

  • blockedtask_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_0SIG_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. 信号处理流程概述

当一个进程接收到信号时,内核会根据以下流程处理信号:

  1. 接收信号:进程接收到信号后,如果信号未被屏蔽,它会被添加到 task_struct 中的 pending 列表中。
  2. 检查屏蔽集:内核会检查 blocked 信号集,判断该信号是否被屏蔽。如果信号被屏蔽,则会保持挂起状态,不会立即处理。
  3. 查找处理方式:如果信号未被屏蔽,内核会查找该信号的处理方式(k_sigaction),然后根据定义的处理程序执行相应的操作:
    • 如果 sa_handler 设置为自定义处理函数,调用该函数。
    • 如果 sa_handler 设置为 SIG_IGN,忽略该信号。
    • 如果 sa_handler 设置为 SIG_DFL,使用信号的默认处理方式。
  4. 唤醒等待的进程:如果有进程在等待该信号(例如 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)逐一处理这些打断。

总结:

  1. 信号:像电话、电子邮件、老板的指示,它们打断进程的正常执行。
  2. 信号处理程序:员工可以选择忽略、响应,或按默认规则处理这些打断。
  3. 屏蔽信号:员工在专注模式时暂时不接电话,但这些电话(信号)会保留在待处理列表中。
  4. 挂起信号:员工暂时没有处理的“未接电话”或“未读邮件”,等专注模式结束后会处理。

内核线程(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. 线程的生命周期总结

  1. 创建线程:调用 kthread_create() 创建一个线程,但线程不会立即开始运行。
  2. 启动线程:通过 wake_up_process() 启动线程,让它开始执行 thread_function()
  3. 线程执行循环:在 thread_function() 中,线程通常会在循环中执行任务,并定期检查 kthread_should_stop() 来判断是否要退出。
  4. 停止线程:如果需要停止线程,可以调用 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() 控制。

代码详细解读:
  1. task_struct *task

    • 定义了一个指向内核线程的 task_struct 指针,用于存储创建的内核线程信息。
  2. func(void *data)

    • 这是内核线程执行的函数。它是一个循环,每次循环输出线程执行的次数 time_count,并且在 kthread_should_stop() 返回 true 或计数达到 30 时退出循环。最后返回 time_count
    • 逻辑:线程在最多循环 30 次时退出,并且每次循环时打印一次日志。
  3. KT_init()

    • 内核模块加载时的初始化函数。这里的主要工作是创建一个内核线程,并在创建成功后启动该线程。
    • kthread_create():用于创建内核线程,传入的参数包括 func(线程执行的函数)、NULL(传递给 func 的数据,未使用)、"MyThread"(线程的名字)。
    • wake_up_process():如果线程创建成功(未返回 IS_ERR()),则调用 wake_up_process() 启动线程。
  4. KT_exit()

    • 内核模块卸载时调用的清理函数。在这里,它只打印一条消息并退出。通常,kthread_stop() 会在这里调用以停止线程,但代码中没有调用它。
  5. module_init()module_exit()

    • 它们分别指示模块加载时应调用的初始化函数 KT_init() 和卸载时应调用的清理函数 KT_exit()

终端输出解释:

  1. insmod KT.ko:这条命令加载内核模块 KT.ko。加载后,模块开始执行 KT_init(),创建并启动内核线程。
  2. lsmod | grep KT:显示内核模块列表,确认 KT.ko 已被加载。
  3. rmmod KT.ko:卸载内核模块,执行 KT_exit() 并打印相应的退出消息。

比喻

你可以把这个内核线程比作一个办公室的自动报告机器人:

  • 办公室(内核环境):这是系统内部的环境,只有授权的人(内核线程)可以在这里运行。
  • 机器人(内核线程):当内核模块加载时(办公室打开),一个机器人被制造出来,它会每隔一段时间记录自己的运行次数,最多记录 30 次。
  • 机器人启动的按钮(wake_up_process():虽然机器人已经被制造出来,但它还不会立即开始工作,只有当有人按下启动按钮(调用 wake_up_process())时,它才会开始执行任务。
  • 机器人检查信号(kthread_should_stop():机器人每次工作时都会检查它的控制面板,看看是否有人要求它停止工作。如果有人按下停止按钮,它会立即停止任务。

在机器人完成了任务(循环 30 次)或者收到停止信号(kthread_should_stop() 返回 true)时,它会自动返回,并停止继续工作。

什么是模块?

在 Linux 内核中,模块(Module)是指一种可以动态加载和卸载的可执行代码。它允许你在不重启系统或重新编译内核的情况下,扩展或修改内核的功能。模块的最常见用途之一就是实现设备驱动或其他特定功能,比如网络协议、文件系统等。

模块的特点:
  1. 动态性

    • 模块可以根据需要动态加载(insmod到内核中,或者动态卸载(rmmod。不需要重新编译或启动内核即可更改系统功能。
  2. 可插拔

    • 你可以将模块想象成一种“插件”,它可以在需要时插入内核,为内核提供某种特定的功能,而当功能不再需要时,你可以安全地将其移除。
  3. 节省资源

    • 由于模块是按需加载的,因此它们可以减少内核的内存使用。在不需要特定功能时,模块可以保持未加载的状态,从而节省系统资源。
模块的组成:
  1. 代码:模块本身是一段可以执行的代码,通常是以 .ko(kernel object) 文件的形式存在。这些代码与内核交互,提供特定的功能,比如设备驱动、文件系统支持等。

  2. 初始化函数 (module_init):模块加载到内核时,会执行初始化函数,这个函数通常用来分配资源或启动某些服务。

  3. 退出函数 (module_exit):当模块被卸载时,会执行退出函数,这个函数通常用来清理资源和终止与模块相关的服务。

模块的工作原理:
  1. 模块加载 (insmod)

    • 当你需要加载某个模块时,使用 insmod 命令加载模块。
    • 例如,insmod my_module.ko 会将模块 my_module 加载到内核中,并调用模块的初始化函数 (module_init)。
  2. 模块运行

    • 一旦加载,模块便会像内核的其他部分一样运行。模块可以调用内核提供的 API,访问内核数据结构,甚至创建线程(例如你之前代码中的 kthread_create)。
  3. 模块卸载 (rmmod)

    • 当模块的功能不再需要时,你可以使用 rmmod 命令将其卸载。
    • 例如,rmmod my_module 会卸载模块,并调用模块的退出函数 (module_exit) 来清理资源。

1. 什么是 LKM(可加载内核模块)?

LKM 是一种独立编译的内核扩展,可以在操作系统启动后动态加载和卸载,允许内核根据需要增加或减少功能。它们通常用于:

  • 添加对新硬件的支持,比如设备驱动程序。
  • 增加对新文件系统的支持,比如挂载某些文件系统格式。
  • 扩展内核功能,比如增加新的系统调用。

LKM 提供了一种模块化的机制来扩展内核功能,避免在添加新功能时必须重新编译整个内核。

2. LKM 的主要用途

  • 硬件支持:内核模块通常用于设备驱动程序,这意味着在引入新硬件(例如 USB 设备、网卡或硬盘控制器)时,不需要重新编译内核,而是通过加载模块来实现支持。
  • 文件系统支持:一些文件系统(如 ext4btrfs)的支持通常作为模块实现,当需要挂载这些文件系统时,可以加载相应的模块来处理。
  • 系统调用:LKM 还可以用于扩展内核的 API,例如在开发调试环境中添加自定义的系统调用。

3. LKM 的跨平台支持

几乎所有现代的类 Unix 操作系统都支持 LKM,虽然它们可能在不同操作系统上使用不同的名称。例如:

  • Linux:LKM 在 Linux 中通常以 .ko 文件的形式存在,可以通过 insmodmodprobe 命令加载。
  • MacOS:在 MacOS 上,LKM 被称为内核扩展(Kernel Extension, kext)。它们通常用于类似的目的,如支持新硬件、文件系统和扩展系统功能。
  • FreeBSDSolaris 也支持类似的模块化内核设计,尽管具体实现有所不同。

4. LKM 的优点

  • 动态加载/卸载:LKM 可以在系统运行时加载和卸载,这使得内核能够根据需求动态增加或减少功能。无需重启系统,节省了大量时间。

  • 模块化开发:开发人员可以编写、测试和调试单独的内核模块,而不需要重新编译整个内核。这为开发和维护内核代码提供了更高的灵活性。

  • 节省内存:只有在需要特定功能时才加载相应的模块,节省了系统资源。

5. LKM 的工作原理

LKM 在运行时被加载到内核空间,与内核共享相同的地址空间。这使得 LKM 可以直接访问内核的内部数据结构和函数,但也需要小心处理,因为 LKM 一旦出现问题(如崩溃),可能会导致整个系统不稳定。

  • 加载模块:通过命令 insmodmodprobe 来加载一个 .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.oKM.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
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2193843.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微软推出针对个人的 “AI伴侣” Copilot 会根据用户的行为模式、习惯自动进化

微软推出了为每个人提供的“AI伴侣”Copilot&#xff0c;它不仅能够理解用户的需求&#xff0c;还能根据用户的日常习惯和偏好进行适应和进化。帮助处理各种任务和复杂的日常生活场景。 它能够根据用户的生活背景提供帮助和建议&#xff0c;保护用户的隐私和数据安全。Copilot…

Crypto虐狗记---”你“和小鱼(八)

前言&#xff1a;剧情八 提示&#xff1a; 下载&#xff1a; 只给了公钥 那么可以用RsaCtfTool去分离公钥---》 得到(e&#xff0c;n)&#xff1a; 如何安装参考&#xff1a; kail下安装RsaCtfTool - 九皋777 - 博客园 (cnblogs.com) 已知n&#xff0c;那么去得到p q 或者使…

智能制造领域的系统都有啥,MES、APS、PLC、SCADA等

提及制造业&#xff0c;大家都能想到工业4.0.那么与工业4.0紧密相连的B端系统都有哪些类型的&#xff0c;本文为大家详细解读下。 在智能制造领域&#xff0c;常见的系统包括MES&#xff08;制造执行系统&#xff09;、APS&#xff08;高级计划与排程系统&#xff09;、PLC&am…

Oracle 数据库安装和配置详解

Oracle 数据库安装和配置详解 Oracle 数据库是一款功能强大、广泛使用的企业级关系数据库管理系统 (RDBMS)&#xff0c;适用于处理大型数据库和复杂事务。本文将介绍如何在 Linux 和 Windows 环境下安装 Oracle 数据库&#xff0c;并对其进行基本配置&#xff0c;帮助开发者快…

maven指定模块快速打包idea插件Quick Maven Package

问题背景描述 在实际开发项目中&#xff0c;我们的maven项目结构可能不是单一maven项目结构&#xff0c;项目一般会用parent方式将各个项目进行规范&#xff1b; 随着组件的数量增加&#xff0c;就会引入一个问题&#xff1a;我们只想打包某一个修改后的组件A时就变得很不方便…

8位单片机与32位单片机

8位单片机与32位单片机 8位与32位指的是什么 单片机的8位或32位说的是什么呢&#xff1f;要搞懂这个问题&#xff0c;首先要搞明白8位或32位说的是单片机上的哪一个部件。 这是单片机的内部框图。单片机内部由这么多部件构成&#xff0c;并不单单是一个CPU&#xff0c;它内部…

NASA:GES DISC 的 ATMOS L1 光谱和运行日志 V3 (ATMOSL1)大气痕量分子光谱(ATMOS)1 级产品

目录 简介 变量 代码 引用 网址推荐 0代码在线构建地图应用 机器学习 ATMOS L1 Spectra and Runlogs V3 (ATMOSL1) at GES DISC 简介 这是第三版大气痕量分子光谱&#xff08;ATMOS&#xff09;1 级产品&#xff0c;包含 netCDF 格式的光谱和运行日志&#xff08;即 &…

Valve通过新的基础架构设计加强对Arch Linux的支持

Valve 宣布与 Arch Linux 发行版开展新的合作。 该公司透露&#xff0c;它将为 Arch Linux 的两项重要计划–构建服务基础设施和安全签名飞地–提供资金支持。这次合作将为 Arch Linux 解决一些老大难问题。 志愿开发人员利用业余时间开发项目。 然而&#xff0c;有些任务需要投…

查缺补漏----信道利用率与数据传输速率(计算题)

1.发送窗口与接收窗口 GBN的发送窗口为&#xff0c;若 大于 &#xff0c;则会造成接收方无法分辨新数据帧和旧数据帧。 这其实是所有重传协议必须满足的要求&#xff0c;即发送窗口为&#xff0c;例如这道题&#xff1a; 发送窗口为就是非常重要的条件&#xff0c;后面有讲解。…

认知杂谈100《穿越顺逆之镜》

内容摘要&#xff1a; 顺境和逆境都是成长的催化剂&#xff0c;但它们的作用并非绝对。顺境有助于积累经验&#xff0c;但可能导致自满和错误估计自身能力。逆境能激发智慧&#xff0c;但也可能使人陷入绝望&#xff0c;且失败的原因可能是不可控的外部因素。 成功可能让我们变…

【C语言】预处理指令详解

目录 一、预定义符号 二、#define 定义常量 三、#define 定义宏 &#xff08;1&#xff09;宏定义的使用 &#xff08;2&#xff09;带副作用的宏参数 &#xff08;3&#xff09;宏替换的规则 &#xff08;4&#xff09;宏与函数对比 &#xff08;5&#xff09;#和## …

Html批量转word工具2.1

2024年10月7日记录&#xff1a; 有客户反馈&#xff0c;2.0刚运行就提示转换完成 有问题就解决。正好国庆假期这几天有空&#xff0c;2.1版就出炉了。 2.1 更新记录&#xff1a; 修复了1个bug&#xff1a;刚运行就提示转换完成 下载地址&#xff1a;Html 转 word 批量处理工具…

基于Springboot+Vue的线上课堂系统(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统中…

分析CppCrash(进程崩溃)(一)

一、Cpp Crash异常检测能力 进程崩溃基于posix信号机制&#xff0c;目前主要支持对以下崩溃异常信号的处理&#xff1a; 信号值(signo)信号解释触发原因4SIGILL非法指令。进程执行了非法、格式错误、未知或特权指令。5SIGTRAP断点或陷阱异常。异常或trap指令发生。6SIGABRT进程…

谷歌发布了日语版的 Gemma2 模型——gemma-2-2b-jpn-it

Gemma 是一系列同类最佳的开放式模型&#xff0c;其灵感和技术源自 Gemini 系列模型。 它们是具有开放权重的文本到文本、纯解码器大型语言模型。 Gemma 模型非常适合各种文本生成任务&#xff0c;包括问题解答、摘要和推理。 Gemma-2-JPN 是一个针对日语文本进行微调的 Gemma…

读数据工程之道:设计和构建健壮的数据系统01数据工程概述

1. 数据工程 1.1. 自从公司开始使用数据做事&#xff0c;数据工程就以某种形式存在了 1.1.1. 预测性分析、描述性分析和报告 1.2. 数据工程师获取数据、存储数据&#xff0c;并准备数据供数据科学家、分析师和其他人使用 1.3. 数据工程是系统和流程的开发、实施和维护&…

No.0 笔记 | 从小白到入门:我的渗透测试笔记

嘿&#xff0c;小伙伴们&#xff01;好久不见啊&#xff0c;是不是都以为我失踪了&#xff1f;&#x1f602; 其实呢&#xff0c;最近一直在埋头苦学&#xff0c;感觉自己就像是在技术的海洋里游泳&#xff0c;每天都在吸收新知识。现在终于有时间冒个泡&#xff0c;跟大家分享…

如何通过 Alt 键打出所有特殊字符?

有时我们需要键入键盘上没有的字符&#xff0c;例如版权符号 ©&#xff0c;怎么办呢&#xff1f; 上一篇文章说过&#xff0c;可以用输入法自带的符号表功能。但除此之外&#xff0c;Windows 官方有一个功能&#xff0c;可以让我们可以通过 Alt 键输入任何特殊符号。 ‍…

Python进阶--函数进阶

目录 1. 函数多返回值 2. 函数多种传参方式 (1). 位置参数 (2). 关键字参数 (3). 缺省参数 (4). 不定长参数 3. 匿名函数 (1). 函数作为参数传递 (2). lambda匿名函数 1. 函数多返回值 def return_num():return 1# 返回1之后就不会再向下继续执行函数体return 2 resu…

《Linux从小白到高手》理论篇:Linux软件安装一篇通

List item 本篇介绍Linux软件安装相关的操作命令&#xff0c;看完本文&#xff0c;有关Linux软件安装相关操作的常用命令你就掌握了99%了。 Linux软件安装 RPM RPM软件的安装、删除、更新只有root权限才能使用&#xff1b;查询功能任何用户都可以操作&#xff1b;如果普通用…