【Linux内核九】进程管理模块:list_head钩子构造双向列表和一些宏定义

接上篇:【Linux内核八】进程管理模块:进程调度队列Struct rq

上篇将进程的等待队列rq结构简单的记录了一下。提到队列,内核中还有一个比较常用的结构:队列钩子结构------list_head。

队列钩子结构:list_head

在内核中,很多都是采用了双向列表。而双向列表的主要结构就是list_head

c 复制代码
struct list_head {
	struct list_head *next, *prev;
};

这个结构非常的简单,实际上这各结构就是一个钩子,连接前面和后面的两个实体,任何的一个实体都可以拥有这样一个钩子,用来挂在一个队列上。

task_struct中的tasks,使用list_head构造全进程双向链表

task_struct中的tasks成员变量,配合上面的list_head结构一起,就可以将所有的进程结构串起来,形成一个双向链表。

每个task都可以通过list_head结构的tasks钩子,很方便的找到前一个和后一个链表成员。

c 复制代码
struct list_head		tasks;

对于这样一个钩子结构,内核中的任何结构体,如果需要形成一个双向列表,都可以再结构体中增加一个list_head XXX的成员即可实现。

利用钩子结构看current进程的前后进程信息

c 复制代码
// 辅助函数:输出进程核心信息
static void print_task_info(struct task_struct *task, const char *desc)
{
    printk(KERN_INFO "===== %s =====\n", desc);
    printk(KERN_INFO "进程PID:%d\n", task->pid);          // 进程ID
    printk(KERN_INFO "进程名:%s\n", task->comm);         // 进程名
    printk(KERN_INFO "进程状态:%ld\n", task->__state);   // 进程状态(0=运行态,1=睡眠态等)
}

// 1. 模块初始化函数(加载模块时执行,对应insmod命令)
// __init:告诉内核这个函数只在初始化时用,执行后可释放内存
static int __init hello_kernel_init(void)
{
    struct task_struct *prev_task, *next_task;
    // 内核打印用printk,KERN_INFO是日志级别(对应dmesg的info级)
    // current是内核全局指针,指向当前运行的进程
    printk(KERN_INFO "=== Hello Kernel! ===\n");
    printk(KERN_INFO "Current PID: %d\n", current->pid);
    printk(KERN_INFO "Current process name: %s\n", current->comm);

    prev_task = list_prev_entry(current, tasks);
    print_task_info(prev_task, "current的前一个进程");
    next_task = list_next_entry(current, tasks);
    print_task_info(next_task, "current的后一个进程");
    // 返回0表示初始化成功,非0则加载失败
    return 0;
}

dmesg的输出为:

bash 复制代码
[  923.813769] Current PID: 5628
[  923.813770] Current process name: insmod
[  923.813771] ===== current的前一个进程 =====
[  923.813772] 进程PID:5627
[  923.813773] 进程名:systemd-udevd
[  923.813774] 进程状态:1
[  923.813775] ===== current的后一个进程 =====
[  923.813775] 进程PID:0
[  923.813776] 进程名:swapper/0
[  923.813776] 进程状态:0

list_prev_entry和list_next_entry宏定义

在上面的代码中,list_prev_entry和list_next_entry是比较奇怪的用法,在整个函数中没有tasks的变量,如果list_prev_entry和list_next_entry是函数的话,是会报错的。

但是list_prev_entry和list_next_entry这两个货是一个宏定义,只是把tasks这个串作为宏定义的参数传入宏,真正的函数执行在后面。

这两个宏定义在include/linux/list.h中:

c 复制代码
/**
 * list_next_entry - get the next element in list
 * @pos:	the type * to cursor
 * @member:	the name of the list_head within the struct.
 */
#define list_next_entry(pos, member) \
	list_entry((pos)->member.next, typeof(*(pos)), member)

/**
 * list_prev_entry - get the prev element in list
 * @pos:	the type * to cursor
 * @member:	the name of the list_head within the struct.
 */
#define list_prev_entry(pos, member) \
	list_entry((pos)->member.prev, typeof(*(pos)), member)

把上面的代码list_next_entry(current, tasks)展开后就是:

c 复制代码
list_entry(current->tasks.prev, typeof(*(current)), tasks)

再看看list_entry。

真正实现双向链表遍历:list_entry宏

list_entry也是一个宏定义:

c 复制代码
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

而container_of是内核的 "地址偏移神器":

c 复制代码
#define container_of(ptr, type, member) ({          \
    // 1. 计算member在type结构体中的偏移量
    const typeof(((type *)0)->member) *__mptr = (ptr); \
    // 2. 用节点地址 - 偏移量 = 结构体首地址
    (type *)((char *)__mptr - offsetof(type, member)); \
})

根据list_entry传入的内容,也就是找到current进程结构体(task_struct)的tasks的地址。

假设:

  • current(当前进程)的task_struct首地址是0x1000;
  • tasks在task_struct中的偏移是0x14(即offsetof(task_struct, tasks)=0x14);
  • current->tasks.next指向的下一个list_head节点地址是0x2014(属于另一个进程的tasks节点)。
  • 执行list_entry(current->tasks.next, struct task_struct, tasks):
  • offsetof(struct task_struct, tasks) → 计算出tasks在task_struct中的偏移0x14;
    (char *)0x2014 - 0x14 → 得到0x2000(下一个task_struct的首地址);
  • 强制转换为struct task_struct * → 拿到下一个进程的完整结构体。

这一段有点绕,我画一张图来说明一下:

对应到上面的宏定义里面去,container_of(ptr, type, member):

  • ptr就是tasks->next,也就是上图中的0x2014。

  • 代码const typeof(((type *)0)->member) *__mptr = (ptr); 就是获得这个指针的类型,也就是member, 也就是tasks,也就是类型是list_head的结构体类型的指针,此时的__mptr也还是0x2014。

  • 代码offsetof(type, member)),就是计算这个member到这个type(task_struct)的偏移量,也就是图中的0x14.

    • offsetof也是一个宏定义:

      c 复制代码
      #define offsetof(type, member)  ((size_t)&((type *)0)->member)
    • (type *)0:将数字0强制转换为type类型的结构体指针(比如struct task_struct *),相当于 "假设结构体首地址在内存 0 地址处";

    • ((type *)0)->member:访问这个 "0 地址结构体" 的member成员(比如tasks);

    • &((type *)0)->member:取这个成员的地址 ------ 因为结构体首地址是 0,成员地址就是它在结构体中的偏移量;

    • (size_t):将地址值转换为size_t(无符号整数),得到最终的偏移量(字节数)。

  • 最后再用0x2014 - 0x14就可以得到下一个结构体的首地址了。

  • 最后在调用list_entry的最外层,强制转换成tast_struct的结构体就可以了。

  • 所有的过程都是通过宏定义来实现所有结构体类型的适配,所以任何结构体想形成一个双向链表,只需要在结构体中定义一个list_head的成员变量就可以了。

学习使用offsetof的方式

之前没有想到过有这种方式来求一个结构体中的偏移量,试了一个:

c 复制代码
struct demo_s{
    int a;
    int b;
    int c;
};

size_t s = ((size_t)&((demo_s *)0)->c);

cout << s << endl;

输出就是8。

又学习到了一些奇奇怪怪的知识。

之前代码中的一些宏定义函数

在内核中还定义了一些常用的宏定义

其中有一个就是和这个list_head的双向列表有关:init_task。也就是这个双向列表的第一个元素。

可以直接通过代码验证一下:

c 复制代码
 extern struct task_struct init_task;  // 是一个在其他地方定义的全局变量,导入即可。
 printk(KERN_INFO "init task name: %s\n", init_task.comm);
bash 复制代码
[59801.979963] init task name: swapper/0

0号进程

系统启动时,内核会先创建init_task(0 号进程):swapper。

在init_task.h中有一个宏定义:

c 复制代码
#define INIT_TASK_COMM "swapper"

简单了解了一下这个0号进程:swapper:

0 号进程(init_task)的进程名在x86 架构下默认是 swapper,在 ARM 等架构下可能是 idle------

本质上它是内核的空闲进程,是 Linux 系统中最基础、最特殊的内核态进程,核心作用是CPU 空闲时 "兜底运行",同时也是系统启动的

"创世进程"。下面从名字由来、核心功能、运行机制三个维度讲透: 一、为什么叫 swapper?名字的历史渊源 swapper

这个名字是Linux 内核的 "历史遗留产物",背后对应了它早期的核心功能: 早期内核的内存交换职责 在 Linux 0.1x

等早期版本中,0 号进程不仅是空闲进程,还负责内存页面的换入 / 换出(swap)------

当内存不足时,它会把不活跃的进程数据写到磁盘交换分区,腾出内存给活跃进程。因此被命名为 swapper。 现代内核的职责分离

随着内核发展,内存交换功能被拆分给了专门的内核线程(如 kswapd0),0 号进程不再负责 swap。但 swapper 这个名字在

x86 架构中被保留了下来,成为一种历史习惯。 不同架构的命名差异 x86/x86_64:沿用历史名称 swapper;

ARM/ARM64:更贴合现代功能,命名为 idle(直接体现 "空闲" 属性); 本质上两者是同一个进程,只是名字不同。 二、0

号进程(swapper)的核心作用 它是内核态进程,没有用户空间代码,也不执行任何业务逻辑,核心职责有 3 个:

  1. CPU 空闲时的 "兜底进程"------ 避免 CPU "无事可做" 这是 0 号进程最核心的功能。 调度逻辑:当 CPU 上没有任何可运行的进程(包括用户进程、其他内核线程)时,调度器会选择 0 号进程运行; 核心动作:0 号进程执行 cpu_idle()
    函数,让 CPU 进入低功耗状态(如 x86 的 HALT 指令、ARM 的 WFI 指令),直到有新的进程被唤醒(如中断触发);
    性能意义:如果没有这个兜底进程,CPU 会陷入 "空循环",导致功耗飙升、温度升高。0 号进程的存在是 Linux 系统节能的关键机制之一。
相关推荐
小周学学学1 小时前
ESXI故障处理-重启后数据存储丢失
linux·运维·服务器
哎哟喂呢哈2 小时前
ddns 免费 ipv6
linux
Flash.kkl2 小时前
Linux——线程的同步和互斥
linux·开发语言·c++
sunfove2 小时前
Python 面向对象编程:从过程式思维到对象模型
linux·开发语言·python
王九思2 小时前
Ansible 自动化运维介绍
运维·自动化·ansible
三不原则2 小时前
实战:基于 GitOps 实现 AI 应用的自动化部署与发布
运维·人工智能·自动化
云和数据.ChenGuang2 小时前
达梦数据库安装服务故障四
linux·服务器·数据库·达梦数据库·达梦数据
PPPPPaPeR.2 小时前
使用vim实现进度条(初级)
linux·编辑器·vim
纵有疾風起3 小时前
【Linux 系统开发】基础开发工具详解:自动化构建、版本控制与调试器开发实战
linux·服务器·开发语言·c++·经验分享·开源·bash