大家好,我是程栩,一个专注于性能的大厂程序员,分享包括但不限于计算机体系结构、性能优化、云原生的知识。
引
前文我们说到进程结构体task_struct中有兄弟进程、子进程链表的指针,但是该指针类型是list_head,并不是我们平常见到的那种以一个同样的数据结构,然后用一个next指针记录后续指针的链表。那么,内核是如何基于这个指针寻找相关的数据结构的呢?
今天主要基于《Linux内核设计与实现》的6.1章及内核6.3源码编写。
list_head与task_struct
首先我们来看list_head类型,
// include/linux/types.h L178
struct list_head {
struct list_head *next, *prev;
};
可以看出这是一个双向链表,既可以往前寻找也可以向后寻找,这是一个环形双向链表:
list_head与task_struct关系如下图所示:
我们将这两者结合:
可以看到,我们可以很容易在获取到task_struct指针后就通过children指针访问到list_head结构体:
那么,现在的问题就演变成,我们如何在访问到list_head后回过头来访问task_struct了:
指针
当我们访问一个结构体指针的时候,其实我们只是访问的这个结构体的起始地址:
而如果我们有这个结构体的声明的话,那么每一个结构体成员对应的偏移在编译的时候就已经固定了:
当我们去访问结构体中成员的时候,我们通过偏移进行寻找:
那么反过来说,如果我们有20偏移的位置对应的地址的话,那么,我们就可以反过来寻找:
这样的话,我们就可以通过list_head反向访问到task_struct ,从而可以遍历全部的task_struct了:
访问过程如下所示:
内核实现
在内核中,内核通过宏定义来实现从list_head到task_struct的访问过程:
// include/linux/container_of.h L10
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
* WARNING: any const qualifier of @ptr is lost.
*/
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
static_assert(__same_type(*(ptr), ((type *)0)->member) || \
__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
/**
* container_of_const - cast a member of a structure out to the containing
* structure and preserve the const-ness of the pointer
* @ptr: the pointer to the member
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*/
#define container_of_const(ptr, type, member) \
_Generic(ptr, \
const typeof(*(ptr)) *: ((const type *)container_of(ptr, type, member)),\
default: ((type *)container_of(ptr, type, member)) \
)
#endif /* _LINUX_CONTAINER_OF_H */
我们首先来看container_of函数,这是一个宏定义:
#define container_of(ptr, type, member)
简单来说,就是针对ptr指针,返回一个type类型的指针。在我们的例子中,就是传入list_head类型指针,list_head被嵌入到了task_struct指针中,也即ptr是list_head,type是task_struct,而member就是list_head在task_struct中的成员名,例如在这里就是children。
首先计算出结构体成员member在task_struct的偏移,也即:
offsetof(type, member)
接着,将当前的指针地址减去偏移量:
__mptr - offsetof(type, member)
这样我们就找到了这个结构体的起始位置,但是我们要注意返回一个合适的类型,也即:
((type *)(__mptr - offsetof(type, member)))
这样,我们就从list_head找到了task_struct。
小结
相比于将数据结构放到链表中的常见实现,内核采用了将链表节点放到数据结构中的方式实现。这样的话,当我们需要一个新的链表的时候,我们只需要将list_head嵌入到我们声明的结构体中即可,并且链表的相关操作,都可以复用当前的这一套,这些链表操作在include/linux/list.h中。
今天的内容更多的在于介绍list_head到task_struct,相对而言会简短一些,理解实现原理比较重要: