内核链表中list_entry的实现原理
先使用内核链表实现数据的基本使用。
#include <stdio.h>
#include <stdlib.h>
#include "kernel_list.h" //内核链表的头文件
struct node //大结构体
{
int data;
struct list_head list; //小结构体
};
struct node *list_init()
{
struct node *head = calloc(1, sizeof(struct node));
if (NULL == head)
{
perror("Initialization failure !");
}
INIT_LIST_HEAD(&head->list);
return head;
}
void list_show(struct node *head)
{
struct list_head *pos = NULL;
list_for_each(pos, &head->list)
{
struct node *tmp = list_entry(pos, struct node, list);
printf("data = %d\t", tmp->data);
}
printf("\n");
}
void list_destory(struct node *head)
{
struct list_head *pos, *n;
list_for_each_safe(pos, n, &head->list)
{
struct node *tmp = list_entry(pos, struct node, list);
free(tmp);
tmp = NULL;
}
free(head);
head = NULL;
}
struct node *new_node(int data)
{
struct node *new = calloc(1, sizeof(struct node));
if (NULL == new)
{
perror("Node creation failure!");
}
new->data = data;
INIT_LIST_HEAD(&new->list);
return new;
}
int main(int argc, char const *argv[])
{
struct node * head = list_init();
for (int i = 0; i < 10; i++)
{
struct node *new = new_node(i);
list_add_tail(&new->list,&head->list);
}
list_show(head);
list_destory(head);
return 0;
}
在该程序中我们使用for循环,使用尾插法将数据存储到结点中,从上面的图片可以看到我们成功执行程序,并且将节点申请的内存空间完全释放。
在自定义的list_show()和list_destory()函数中均使用到内核链表中的list_entry()。我们先来看一下该函数在链表中是怎么样去定义的。
list_entry
/**
* list_entry – get the struct for this entry
* @ptr: the &struct list_head pointer.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the struct.
*/
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
在kernel_list.h中我们能看到list_entry()有三个参数分别是小结构体的指针,大结构体的类型和小结构体的变量名。在这段宏定义中看起来似乎是有点难以理解的,这里的功能其实就是通过传入小结构体指针去找到返回所被大结构体数据包围的大结构体的指针变量,当然我们得到了该节点的大结构体的指针变量后,自然而然的能通过这个大结构体指针去指向所在节点的其它成员变量。这样,就能对其它成员变量的值进行输出。这样说感觉还是有点抽象,接下来我带大家,进一步地研究。
首先给大家展示一段代码:
#include <stdio.h>
struct node
{
int a;
char b;
int c;
};
int main(int argc, char const *argv[])
{
struct node p = {1,'w',3};
printf("a = %d\n",p.a);
printf("b = %c\n",p.b);
printf("c = %d\n",p.c);
char *pb = &p.b;
printf("pb = %c\n",*pb);
printf("pb地址为%ld\n",(long)pb);
printf("pb = %c\n",*(char *)(4294954000));//这种方法不建议使用
return 0;
}
上面的代码可以帮助我们输出成员变量的值,最下面的输出语句是不建议这样使用的,因为如果强制类型转化不当,就会访问到未定义的虚拟内存空间,极大可能出现段错误的情况,这里我是想为下面执行的代码程序铺垫。printf("pb = %c\n",*(char *)(4294954000));这一行代码上面的地址数字是先通过前面的printf语句输出得到pb所指向的地址空间的,在不同电脑运行,或者你对代码的其它语句进行修改它是会不一样的,会改变的,所以在你自己的编程环境中编译这段代码时,不要一味地复制,自己先调试一下。
我们其实知道一串数字代表的含义是什么在于我们怎么样去使用,去定义他。如果上面的数字4294954000代表的不是人口数量,也不是代表我们需要背诵的英语单词数量,而是代表p.b的虚拟地址。这样我们就能对这个地址,通过其它成员变量对其的相对偏移量,进行加减就能得到我们所想要的值。如下图所示:
我们在这里暂停思考一下,我们上面的实验只是有这个指向char类型的p.b地址就能得到其它成员变量的值。
我们假如有这一行代码,int num;这是什么是意思呢?
实质上应该这么解释,在栈空间中连续申请了四个字节的地址空间,并使用变量名num去间接访问使用这片空间。假如我们不使用变量名去间接访问,而是自己定义一个数字为地址去使用这块区域空间呢?
(struct node *)5 //自己假定以0为起始基地址的大结构体地址
&((struct node *)5)->b //以0为大结构体的基地址上b的地址
(long)(&((struct node *)5)->b) - 5 //减去基地址的值得到成员b在大结构体中的偏移量
<==> (long)(&((struct node *)0)->b) - 0
<==> (long)(&((struct node *)0)->b) //偏移量
为什么要费劲心思去得到该成员变量的偏移量,如果5(或者0)是大结构体的地址,为什么不直接让它输出成员变量的值呢?我们是不能够通过自己虚拟出来的地址直接得到成员变量的值的,因为我能struct node p = {1,'w',3};的p的内存地址并不是真的在5(或者0)上面,之所以是求出b的偏移量,是因为我们前面的代码有一个char型指针指向成员变量b,我们可以通过借助b的偏移量,改变char型指针的指向,并对它进行强制类型转化为大结构体指针(struct node *),我们就能通过大结构体的指针指向我们需要访问成员变量了,我通过下面的代码和图示来帮助大家理解:
#include <stdio.h>
struct node
{
int a;
char b;
int c;
};
int main(int argc, char const *argv[])
{
struct node q = {1, 'w', 3};
printf("a = %d\n", q.a);
printf("b = %c\n", q.b);
printf("c = %d\n", q.c);
char *pb = &q.b;
printf("pb = %c\n", *pb);
printf("pb地址为%ld\n", (long)pb);
printf("pb = %c\n", *(char *)(4294953984)); //这种方法不建议使用
printf("a = %d\n", *(char *)(4294953980)); //这种方法不建议使用
printf("c = %d\n", *(char *)(4294953988)); //这种方法不建议使用
// // int offset = (long)(&((struct node *)5)->b ) - 5;
int offset = (long)(&((struct node *)5)->b) - 5; // ==> (long)(&((struct node *)0)->b )
struct node *p = (struct node *)((long)pb - offset);
// // (struct node *)((long)pb - offset);
// // (struct node *)((long)pb - (long)(&((struct node *)0)->b ));
printf("a : %d\n", p->a);
printf("b : %c\n", p->b);
printf("c : %d\n", p->c);
return 0;
}
struct node *p = (struct node *)((long)pb - offset);
// // (struct node *)((long)pb - offset);
// // (struct node *)((long)pb - (long)(&((struct node *)0)->b));
逐步展开然后回过头来与我们的list_entry定义做对比
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))