赞
踩
C是面向过程的语言,但是linux内核却用C实现了一套面向对象的设计模式,linux内核中处处体现着面向对象的思想。
我们在语法书上学到的链表都是在原数据结构的基础上增加指针域next(或者prev),从而使各个节点能链接在一起。
比如
typedef struct node
{
int data;
/*...*/
struct node *prev;
struct node *next;
} Node;
传统的链表有个最大的缺点就是不好通用化,linux内核的实现可以说是独树一帜,它实现了一种更通用的链表。
struct list_head
{
struct list_head *prev;
struct list_head *next;
};
typedef struct node
{
int data;
/*...*/
struct list_head list;
}Node;
在数据结构中塞入list_head,那么当组成链表的时候,所有的Node节点的list域串联在一起组成链表,我们一次遍历其实就是遍历所有的list_head域。
这里有一个小技巧:当我们知道了一个结构体对象Node中某个成员(比如list_head成员list)的地址,通过偏移就可以计算出整个结构体对象起始位置的地址。
linux内核提供了list_entry的宏,来通过成员对象指针来获取到整个对象的指针。
这到底是怎么实现的呢?
#define list_entry(ptr, type, member) /
container_of(ptr, type, member)
#define container_of(ptr, type, member) /
({ /
const typeof( ((type *)0)->member ) *__mptr = (ptr);/
(type *)( (char *)__mptr - offsetof(type,member) ); /
})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
我们将宏完全展开,就得到如下的代码:
#define list_entry(ptr, type, member) /
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
这个宏返回指向type结构的指针。
生产者和消费者模型中,生产者创造数据,而消费者读取消息和处理包,或者以其他方式消费这些数据。
最简单的实现方法是队列,生产者将数据推进队列,消费者从队列中读取数据。消费者获取数据的顺序和生产者推入队列的顺序一致。
队列,即FIFO(First in first out,先进先出)。Linux内核通用队列实现称为kfifo,在kernel/kfifo.c文件中实现,声明在<linux/kfifo.h>文件中。
首先看一下kfifo的数据结构:
struct kfifo {
unsigned char *buffer; /* the buffer holding the data */
unsigned int size; /* the size of the allocated buffer */
unsigned int in; /* data is added at offset (in % size) */
unsigned int out; /* data is extracted from off. (out % size) */
spinlock_t *lock; /* protects concurrent modifications */
};
定义自旋锁的目的为了防止多进程/线程并发使用kfifo。因为in和out在每次get和out时,发生改变。
kfifo的巧妙之处在于in和out定义为无符号类型,在put和get时,in和out都是增加,当达到最大值时,产生溢出,使得从0开始,进行循环使用。
3. get
代码如下:
static inline unsigned int kfifo_put(struct kfifo *fifo, const unsigned char *buffer, unsigned int len) { unsigned long flags; unsigned int ret; spin_lock_irqsave(fifo->lock, flags); ret = __kfifo_put(fifo, buffer, len); spin_unlock_irqrestore(fifo->lock, flags); return ret; } static inline unsigned int kfifo_get(struct kfifo *fifo, unsigned char *buffer, unsigned int len) { unsigned long flags; unsigned int ret; spin_lock_irqsave(fifo->lock, flags); ret = __kfifo_get(fifo, buffer, len); //当fifo->in == fifo->out时,buufer为空 if (fifo->in == fifo->out) fifo->in = fifo->out = 0; spin_unlock_irqrestore(fifo->lock, flags); return ret; } unsigned int __kfifo_put(struct kfifo *fifo, const unsigned char *buffer, unsigned int len) { unsigned int l; //buffer中空的长度 len = min(len, fifo->size - fifo->in + fifo->out); /* * Ensure that we sample the fifo->out index -before- we * start putting bytes into the kfifo. */ smp_mb(); /* first put the data starting from fifo->in to buffer end */ l = min(len, fifo->size - (fifo->in & (fifo->size - 1))); memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l); /* then put the rest (if any) at the beginning of the buffer */ memcpy(fifo->buffer, buffer + l, len - l); /* * Ensure that we add the bytes to the kfifo -before- * we update the fifo->in index. */ smp_wmb(); fifo->in += len; //每次累加,到达最大值后溢出,自动转为0 return len; } unsigned int __kfifo_get(struct kfifo *fifo, unsigned char *buffer, unsigned int len) { unsigned int l; //有数据的缓冲区的长度 len = min(len, fifo->in - fifo->out); /* * Ensure that we sample the fifo->in index -before- we * start removing bytes from the kfifo. */ smp_rmb(); /* first get the data from fifo->out until the end of the buffer */ l = min(len, fifo->size - (fifo->out & (fifo->size - 1))); memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l); /* then get the rest (if any) from the beginning of the buffer */ memcpy(buffer + l, fifo->buffer, len - l); /* * Ensure that we remove the bytes from the kfifo -before- * we update the fifo->out index. */ smp_mb(); fifo->out += len; //每次累加,到达最大值后溢出,自动转为0 return len; }
hash 最重要的是选择适当的hash函数,从而平均的分配关键字在桶中的位置,从而优化查找 插入和删除所用的时间。
然而任何hash函数都会出现冲突问题。内核采用的解决哈希冲突的方法是:拉链法,拉链法解决冲突的做法是:将所有关键字为同义词的 结点链接在同一个链表中。
内核哈希数据结构:
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
因为哈希链表并不需要双向循环的技能,它一般适用于单向散列的场景。
所以,为了减少开销,并没有用struct hlist_node{}来代表哈希表头,而是重新设计struct hlist_head{}这个数据结构。此时,一个哈希表头就只需要4Byte了,相比于struct hlist_node{}来说,存储空间已经减少了一半。
这样一来,在需要大量用到哈希链表的场景,其存储空间的节约是非常明显的,特别是在嵌入式设备领域。
在hlist中,表头中没有prev,只有一个first,为了能统一地修改表头的first指针hlist就设计了pprev。
node节点里的pprev其实指向的是其前一个节点里的第一个指针元素的地址。对于hlist_head来说,它里面只有一个指针元素,就是first指针;而对于hlist_node来说,第一个指针元素就是next。
所以,当在代码中看到类似与 *(hlist_node->pprev) 这样的代码时,表示此时正在哈希表里操作当前节点前一个节点里的第一个指针元素所指向的内存地址。
教课书上的hash函数一般都是对模数取余,模数一般就是hash表的长度(桶的个数),通常为了较好的散列性,还把模数调整为一个质数.
那么内核中的hash函数是什么样呢?
其实不同的使用场景,需要用到的hash函数是不同的。
比如linux netfilter中对会话的hash 函数
static u_int32_t __hash_conntrack(const struct nf_conntrack_tuple *tuple, unsigned int size, unsigned int rnd) { unsigned int a, b; a = jhash((void *)tuple->src.u3.all, sizeof(tuple->src.u3.all), ((tuple->src.l3num) << 16) | tuple->dst.protonum); b = jhash((void *)tuple->dst.u3.all, sizeof(tuple->dst.u3.all), (tuple->src.u.all << 16) | tuple->dst.u.all); return jhash_2words(a, b, rnd) % size; } static inline u_int32_t hash_conntrack(const struct nf_conntrack_tuple *tuple) { return __hash_conntrack(tuple, nf_conntrack_htable_size, nf_conntrack_hash_rnd); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。