[原理分析]linux内核中的链表原理实践[3]

摘要:

本文的第[一,二]系列主题虽然是链表操作,内容还是指针的操作,本文通过链表实例来阐述下指针操作。不仅仅涉及到数据节点指针,也还涉及到函数指针,最后还涉及基于指针的函数体优化。

正文:

本文主要阐述链表中的节点删除操作,并且在删除过程中使用回调函数。回调函数本身也很简单,就是判断当前节点中数据的奇性或者偶性,来判断是否删除该数据节点。

#include "stdafx.h"
#include <memory>

typedef struct node{
	int value;
	node* next;
}node;

typedef bool (* condValid) (node* stu);

bool pickEven(node* n)
{
	if((n->value) % 2 == 0)
		return true;
	return false;
}

bool pickOdd(node* n)
{
	if((n->value) % 2 == 1)
		return true;
	return false;
}

node* addNode(node* head, int value)
{
	if(head != NULL)
	{
		node* n = new node();
		n->value = value;
		n->next = head->next;
		head->next = n;
	}else{
		head = new node();
		head->next = NULL;
		head->value = value;
	}
	return head;
}

node* delNode(node* head,  condValid validRemove)
{
	node* prev;
	node* cur;

	for(prev = NULL, cur = head; cur; ){
		node* const next = cur->next;
		if(validRemove(cur)){
			if(prev){
				prev->next = next;
			}else{
				head = next;
			}
			free(cur);
		}else{
			prev = cur;
		}
		cur = next;
	}
	return head;
}

void showList(node* head)
{
	for(;head; head=head->next)
		printf("%d ", head->value);
	printf("\n");
}

void testAdd()
{
	node* head = NULL;
	head = addNode(head, 2);
	head = addNode(head, 3);
	head = addNode(head, 5);
	head = addNode(head, 6);

	showList(head); //it should show 2, 6, 5, 3;
}

void testDel()
{
	node* head = NULL;

	head = addNode(head, 2);
	head = addNode(head, 3);
	head = addNode(head, 5);
	head = addNode(head, 6);

	showList(head); //it should show 2, 6, 5, 3;
	node* head1 = delNode(head,pickEven);
	printf("after delNode using isEvenSel:\n");
	showList(head1); //it should show 5, 3;

	node* head2 = delNode(head1, pickOdd);
	printf("after delNode using isOddSel:\n");
	showList(head2); //it shoud show nothing.
}

int _tmain(int argc, _TCHAR* argv[])
{
	//testAdd();
	testDel();

	getchar();
	return 0;
}</span>

上面的delNode函数需要注意的是在for语句中:1.for(prev = NULL, cur= head; cur; ){后面并没有马上跟上常见的cur=cur->next;2. 在循环内部一开始就采用将cur->next保存下来;1和2的原因都在于cur当前节点可能在循环中被删除。上述的delNode的唯一不足在于,每次需要返回head节点;如何做到不用返回变更后的head值呢?很直接的我们会想到指针。于是,我们将head的地址作为参数传入,具体的代码如下:

void delNode2(node** head,  condValid validRemove)
{
	node** cur;
	node* entry;

	for(cur=head;*cur; ){
		entry = *cur;
		if(validRemove(entry)){
			*cur = entry->next;
			free(entry);
		}else{
			cur = &entry->next;
		}
	}
}

void testDel2()
{
	node* head = NULL;

	head = addNode(head, 2);
	head = addNode(head, 3);
	head = addNode(head, 5);
	head = addNode(head, 6);

	showList(head); //it should show 2, 6, 5, 3;
	delNode2(&head,pickOdd);
	showList(head); //it should shoe 2, 6;
}

上述代码的关键在于*cur = entry->next是覆盖,而cur=&entry->next则是移动,具体的流程可以参照下图:

上图有四个数据节点:a1, a2, a3, a4;假设我们要删除其中的a1和a3,在每个步骤中所导致的cur的*cur的变化过程。

下面我们再来看个基于指针的优化,所摘的代码来源于linux2.6早期版本的网络部分,此处不需要理解代码背后的网络原理,只需要就代码论代码就可以说明白其中所包含的优化原理,那么首先来看下面的示例代码(linux中原代码的简化实现):

for (ptype = ptype_base; ptype != NULL; ptype = ptype->next)
  {
if ((ptype->type == type || ptype->type == htons(ETH_P_ALL)) && (!ptype->dev || ptype->dev==skb->dev)){
        struct sk_buff *skb2;
        skb2=skb_clone(skb, GFP_ATOMIC);
        if(skb2)
            ptype ->func(skb2, skb->dev, ptype);
    }
}
kfree_skb(skb, FREE_WRITE);

光从代码的表面可以理解,其主要的目的是遍历某个链表,找出其中满足条件的节点进行深度拷贝(skb_clone),将拷贝后的节点作相应的处理,最后再将原始节点skb释放掉;上述代码的主要问题在于skb_clone操作是非常费时的,那有没有办法可以减少clone操作次数呢?可以利用的就是skb,假设这里要拷贝10次,那么最后一次就可以认为是不必要的,可以直接利用skb,因为反正不用白不用,马上skb就会被kfree_skb函数销毁;即使用了,因为skb不会再用作拷贝的原型(最后一次),所以直接利用skb进行ptype->func()操作是合理的。问题是如何免除最后一次的skb2拷贝,直接用skb来实现ptype->func()操作呢?参考linux社区中内核黑客的解决方案:通过增加额外的局部变量,实现延迟type->func()的处理。

修改1: 增加用于暂存ptype的局部变量:pt_prev = NULL;

修改2:对for循环中的if语句进行相应的修改:

if(pt_prev)
{
        struct sk_buff *skb2;
        skb2=skb_clone(skb, GFP_ATOMIC);
        if(skb2)
            pt_prev ->func(skb2, skb->dev,pt_prev);
}
pt_prev = ptype;

修改3:在退出for循环后,处理掉最后的一个ptype:

<span style="font-size:14px;">if(pt_prev)
	pt_prev->func(skb, skb->dev, pt_prev);
else
	kfree_skb(skb, FREE_WRITE);</span>

整合上述的代码修改,最后可以得到:

pt_prev = NULL;
for (ptype = ptype_base; ptype != NULL; ptype = ptype->next)
{
if ((ptype->type == type || ptype->type == htons(ETH_P_ALL)) && (!ptype->dev || ptype->dev==skb->dev))
{
      if(pt_prev)
{
<span style="white-space:pre">	</span>struct sk_buff *skb2;
       skb2=skb_clone(skb, GFP_ATOMIC);
        if(skb2)
           	pt_prev ->func(skb2, skb->dev, pt_prev);
}
pt_prev = ptype;
}
}//end of loop;

if(pt_prev)
	pt_prev->func(skb, skb->dev, pt_prev);
else
kfree_skb(skb, FREE_WRITE);

启发:

1. 上述代码中展示一种延迟处理的代码技巧,虽然扣出性能,但是程序的实现复杂度提升,代码不容易读懂;

2. 由于操作系统优化的需要,linux实现中肯定包含很多上述的代码优化措施,作为学习者需要有一定的代码简化能力和思考追问能力;

参考资料:

http://coolshell.cn/articles/8990.html

http://coolshell.cn/articles/9859.html

时间: 2024-10-08 15:00:44

[原理分析]linux内核中的链表原理实践[3]的相关文章

[原理分析]linux内核中的链表原理实践

摘要: 本文根据linux内核的链表定义,尝试自己来完成相关的接口设计,并且构建测试用例,来体会下链表接口的用法. 正文: 首先来看下linux内核提供的链表接口大致如下: struct head_node{ struct head_node* pre; struct head_node* next; }; 即节点中只有两个指针:一个指向前一个元素,一个指向后一个元素:假设我们现在要使用该接口,跟一个int值联合在一起形成数据节点,那么存在两种可能的方式: case1: typedef stru

[原理分析]linux内核中的链表原理实践[2]

摘要: 本文过程化的演进方式,将自己写的链表结构慢慢地演化到类似linux内核链表的实现. 正文: 在本系列1中,如果将data_node中的信息调换一下,也即value放在前面,将head_node信息放在后面,那么节点数据就不能正常输出. typedef struct data_node{ int value; head_node h; }data_node; 要查找原因,主要还是看list_value函数的实现: void list_value(data_node* d) { data_n

Linux内核中namespace之PID namespace

前面看了LInux PCI设备初始化,看得有点晕,就转手整理下之前写的笔记,同时休息一下!!~(@^_^@)~ 这片文章是之前写的,其中参考了某些大牛们的博客!! PID框架的设计 一个框架的设计会考虑很多因素,相信分析过Linux内核的读者来说会发现,内核的大量数据结构被哈希表和链表链接起来,最最主要的目的就是在于查找.可想而知一个好的框架,应该要考虑到检索速度,还有考虑功能的划分.那么在PID框架中,需要考虑以下几个因素. 如何通过task_struct快速找到对应的pid 如何通过pid快

Linux内核中常见内存分配函数

1.原理说明 Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示.四级页表分别为: l   页全局目录(Page Global Directory) l   页上级目录(Page Upper Directory) l   页中间目录(Page Middle Directory) l   页表(Page Table) 页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录

Linux内核中常见内存分配函数zz

https://blog.csdn.net/wzhwho/article/details/4996510 1.      原理说明 Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示.四级页表分别为: l         页全局目录(Page Global Directory) l         页上级目录(Page Upper Directory) l         页中间目录(

Linux内核中的list用法和实现分析

这些天在思考知识体系的完整性,发现总是对消息队列的实现不满意,索性看看内核里面的链表实现形式,这篇文章就当做是学习的i笔记吧.. 内核代码中有很多的地方使用了list,而这个list的用法又跟我们平时在教科书中常见的用法有很大的不同,所以有必要详细了解下这里面的门道. 内核里面的list(如没有特殊说明,下文说的list都是指内核里面的list)可称之为侵入式链表.这种list最突出的特征就是其节点中不含有任何数据,相反,list节点是嵌入到特定的数据结构中的.大家自然就会问了,这样实现有什么好

linux内核中socket的创建过程源码分析(总结性质)

http://www.jianshu.com/p/5d82a685b5b6 在漫长地分析完socket的创建源码后,发现一片浆糊,所以特此总结,我的博客中同时有另外一篇详细的源码分析,内核版本为3.9,建议在阅读本文后若还有兴趣再去看另外一篇博文.绝对不要单独看另外一篇. 一:调用链: 二:数据结构 一一看一下每个数据结构的意义: 1) socket, sock, inet_sock, tcp_sock的关系创建完sk变量后,回到inet_create函数中: 这里是根据sk变量得到inet_s

Linux内核中的GPIO系统之(3):pin controller driver代码分析--devm_kzalloc使用【转】

转自:http://www.wowotech.net/linux_kenrel/pin-controller-driver.html 一.前言 对于一个嵌入式软件工程师,我们的软件模块经常和硬件打交道,pin control subsystem也不例外,被它驱动的硬件叫做pin controller(一般ARM soc的datasheet会把pin controller的内容放入GPIO controller的章节中),主要功能包括: (1)pin multiplexing.基于ARM core

linux 内核 中链表list

这个结构从list.h 移到了types.h, 可见内核对循环链表的重视 include/linux/types.h中定义 struct list_head {        struct list_head *next, *prev;}; include/linux/list.h 中的宏 初始化 一个叫name的链表节点 #define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \        s