Linux内核中链表的使用

上一篇 / 下一篇  2012-08-09 10:23:40 / 个人分类:Linux

51Testing软件测试网XS"?o;u+H;f*X

  链表是存放和操作可变数量元素的数据结构,它可在需要时动态创建结点并插入链表中,在编译时不需知道包含多少个元素,而且它在内存中也无须占用连续内存区。51Testing软件测试网 cc6OF3lv,N:Y.Fyol

"r.HKRFtfI0  内核有许多链表的实现,而且还有其官方内核实现,所以在内核中使用链表时只要使用官方实现即可,可以说是方便、快捷、高效、安全。链表的基础知 识可见K&R经典C程序设计语言。LKD3e对于内核数据结构的讲解无疑是经典的,但没有更实际的例子以至看后印象不是很深,现通过LKD3e为 指明方向,增加实际例子讲解内核链表是如何操作的。51Testing软件测试网5@tP]g'zW

t H\x&V0  1、链表数据结构51Testing软件测试网1T%_S;p M vtoH3L

51Testing软件测试网 kKZ t$A2RK

_&Q o PdoD,G@'n0
头文件<linux/list.h>51Testing软件测试网&y ~._5z*r,?V
    数据结构:  51Testing软件测试网V-}sn.zZ&K2P
    struct list_head {51Testing软件测试网,l"S8N(}@
      struct list_head *next;
iYAp"l[1U6E&Z,[0      struct list_head  *prev;
(A HX"sBl s\0    };
51Testing软件测试网3L c"gAm]2fU&W

  需要理解的是内核不是将数据结构插进链表,而是将链表节点插入数据结构,也就是说我们自己定义的每个结构体里面都含有一个struct list_head结构体成员,用其指向链表的下一个节点和上一个节点,它并没有把数据本身插入到链表中。而我们其实只对数据感兴趣,对于struct list_head来说只是一个链接前后节点的工具,那么如何取其数据呢?内核实现是把每个struct list_head和一个数据节点挂勾(也就每个节点中包含struct list_head的原因),通过list_entry()宏即可通过struct list_head结构取包含其的节点数据。51Testing软件测试网,zgZ&J)?!p

Ku(mQ`\'N[0 51Testing软件测试网P C.HM pC

51Testing软件测试网*LBQ9yW~.A+A1T

#define list_entry(ptr, type, member) \
%}(Bg|U8}s\0    container_of(ptr, type, member)
51Testing软件测试网 ^!{8Keh x:G(`)h

51Testing软件测试网iJ;Zh(_l5h6K!Q

    #define container_of(ptr, type, member) ({          \
?7fIg8L0    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \51Testing软件测试网T6z)sR%N n#]+n
    (type *)( (char *)__mptr - offsetof(type,member) );})

0k.aZ U;R{Y h0
51Testing软件测试网 T$\.OsXC1BM

  LKD3e上描述说:在C语言中,一个给定结构中的变量偏移在编译时地址就被ABI固定下来。也就是说定义的一个结构体变量地址再加上偏移地址就能知道各成员的地址了。51Testing软件测试网!N?:E1`Z2r7]B+Q

'Cu7Ok*]c+e4Pn e0  2、定义一个链表51Testing软件测试网9IU3Z!L%^${xO,nk'h

51Testing软件测试网/p$|v,G2k)H`L

  链表使用之前需要初始化。一般使用链表的情况是定义一个全局链表头,并将其初始化。ip地址和时间挂钩的情况很多时候都可用到,所以现以一个包 含ip地址和一个时间的结构体为例演示内核链表是如何操作使用的。题外话,把存放ip地址的成员定义成4个__u32大小的数组是为了支持ipv6地址, 因为ipv6地址是128bit,4*32刚好是128bit,如果是ipv4地址只用数组的第0个成员足够了,本文只讲解v4地址的比较。51Testing软件测试网z{]p%|L!c

$K!V-Nkm0

-L_/u3j!\?6N j0
51Testing软件测试网fnD5|-s|*C.Kv9N

typedef unsigned int __u32

h.fTW%tj2NN(A1e0 51Testing软件测试网`@j/Q5m3K2pM

    struct ipstore{51Testing软件测试网ElIeI*Jg2e
    unsigned long time;51Testing软件测试网x%_z_(M
    __u32 addr[4];
't'`H y Veeo5R0    struct list_head list;51Testing软件测试网:B6pIk0A1aS]
    };

B4j^2K"l(U'OH0]#\0

y%jJ3q8o0    static LIST_HEAD(addr_list);51Testing软件测试网AA;mzVj1l

51Testing软件测试网d-pZ5} Q j0D&fdv8X

  这样就定义了一个名为addr_list链表头,LIST_HEAD本身是宏,它已对addr_list进行了初始化:51Testing软件测试网F+n[z@8g%G_ v

51Testing软件测试网~]a$@ a9v

51Testing软件测试网`G9hA%v:W5M I:B5Y

p @F4n5WL&^Pg0#define LIST_HEAD_INIT(name) { &(name), &(name) }51Testing软件测试网5c%[H6fa2W g

P8q)?8|-` l;SHc0    #define LIST_HEAD(name) \51Testing软件测试网 i"lL qy~%[
        struct list_head name = LIST_HEAD_INIT(name)
51Testing软件测试网~[5@c9L

3k(_8|%z0~d"{4a0  如果是动态创建链表结点,使用运行时初始化:51Testing软件测试网3f%F B#VeN+{

51Testing软件测试网|hk.X7ZKMp

tw7[%n`?0
struct ipstore *ist;
'Kq&d8s Bo0    ist = kmalloc(sizeof (*ist), GFP_ATOMIC);
SSJ3s9y!_8V-U y^4o0    if(!ist)51Testing软件测试网I*c(j$j,i5l2v
    {51Testing软件测试网~p3b?.b2D
        printk("kmalloc failed.\n");
B5S5k0Bu.f0        return -1;
*zFx8]t`;G@0    }51Testing软件测试网.t,Q5{_9m
   
4w\]0|`R(Z:bjSP$U0    ist->time = jiffies+time*HZ;51Testing软件测试网0erc'W g(Qne!pnk
    ist->addr[0] = 0xc0a80101;//IP:192.168.1.1
TY:}C.IB`0    INIT_LIST_HEAD(&ist->list);
51Testing软件测试网(J J4Vaw

  此时ist就是一个指向已初始化完成并有数据的ipstore结构体结点,其struct list_head成员list已初始化。

P5eEb,p#i0b3Q k;n0
*I Z!Ih S.O0
51Testing软件测试网]&F"x8d_~@

  3、在链表中增加一个节点

G;bf/GAC0 51Testing软件测试网 h Q8BBeLh

%ei1P4O5KE#a0
static inline void list_add(struct list_head *new, struct list_head *head)
"{4e$f$U bM0    {   51Testing软件测试网^*Q~J x%CHL
        __list_add(new, head, head->next);51Testing软件测试网f-S&l+Qb"P5{5J5t G
    }
51Testing软件测试网RI?7^/`8JH m"q

  要调用此函数,只要在上一步后面接上:51Testing软件测试网 P$d E6C:gN

+Zbox$~bg\#c}0

+~.i3s/g7kXM"K0
list_add(&ist->list, &addr_list);

gJ(w9]1?-p]5o RO0  完整的增加操作:51Testing软件测试网WNkOi!s.@.\X

ZOp_5Qz;}/YF0 51Testing软件测试网1|m(nv-u

struct ipstore *ist;
l+R/n-og(G0    ist = kmalloc(sizeof (*ist), GFP_ATOMIC);51Testing软件测试网 v+tpE2i%~H
    if(!ist)
s(zZV6R O(y]8O0    {
za#` QEe_E0        printk("kmalloc failed.\n");51Testing软件测试网g3z-pktE]Zv
        return -1;51Testing软件测试网v+Dgpl3\8f)st
    }51Testing软件测试网-L{R,p9W1r$c
   
7r7Y xJ$i^ a4a0    ist->time = jiffies+time*HZ;51Testing软件测试网3x3K okP] e7s:]5G
    ist->addr[0] = 0xc0a80101;//IP:192.168.1.151Testing软件测试网6|4y~G!E$X
    INIT_LIST_HEAD(&ist->list);
&ZJ0L6RY"?4?0    list_add(&ist->list, &addr_list);
51Testing软件测试网(Vj STl$w2PO

  这样在链表addr_list中就增加了新的链表节点。

WnTY"i t6Z0

"_4K-M0u#o0b!]0  4、遍历链表

:T,~j&j\r0q4R [0

TD/]]+h{3|-q0  为什么不先讲删除节点,因为很多情况下都是先遍历链表,找到匹配的节点后再去删除的,所以先讲遍历链表,遍历链表分普通遍历和安全删除遍历,可见如果要删除链表结点我们就需要使用安全删除遍历。

?1u;S,A0V0

'F Kak!o`5@0  普通遍历,主要使用在当加入节点时判断是否有相同的结点存在,如果已存在就不需要再往下操作了,如下判断是否有相同的IP地址存在。51Testing软件测试网$z$iS$`q {1[

_ wt{ |4s N ` ^0 51Testing软件测试网5E7c-\5Fj

51Testing软件测试网#n S.z8uc

#define list_for_each(pos, head) \
5BPh'Yc%C:P UX%w j0        for (pos = (head)->next; prefetch(pos->next), pos != (head); \
&lx1j:}@ `B+g0                pos = pos->next)

6f8|j5X!w;v"db%^8h7]ln0

U5A6Am*|!A$MT0    struct list_head *p;
B6Or!T0A h0    struct ipstore *store;
(Q&|P7}/i)T0    list_for_each(p, &addr_list)51Testing软件测试网2j X`|b~%h
    {
K)h#F3g*[g#\%[j)J{P/a J[0      store = list_entry(p, struct ipstore, list);51Testing软件测试网A+zTO'XEB
      if(store->addr[0] == ist->addr[0])
/tyV n_~#O0      {51Testing软件测试网 bqy9RV*@C7i
          if(ist)
#qh;m2GVa1b*{0              kfree(ist);
0~n-f'Q5p7_ f/f0          break;51Testing软件测试网3zg9i)tq?7B'V7yAG*I}
      }51Testing软件测试网[mE@TS
    }
f jr1r)Y6MZO0    INIT_LIST_HEAD(&ist->list);51Testing软件测试网9SJ}1r*fm jg1l
    list_add(&ist->list, &addr_list);

x!ba+zit0

`/R{ K$} O4]'|Egn0  ist就是上一步的链表结点指针,如果链表中有IP和新增节点IP地址相同的节点则释放刚分配的新节点空间,不进行任何操作,否则加入addr_list链表中。

6E!U,rE|0 51Testing软件测试网6e|(Z X'kF"w

  内核提供了一个比较简单的接口,可以在遍历的同时取出节点,此函数内部封装了list_entry():51Testing软件测试网?8rG3jX*UM

51Testing软件测试网6I(ia3L9pUw~

Z*Evr4o)|%I0 #define list_for_each_entry(pos, head, member)              \
"J\k2o4S;IY H0    for (pos = list_entry((head)->next, typeof(*pos), member);  \51Testing软件测试网W ]x)Th!yf}m
         prefetch(pos->member.next), &pos->member != (head);    \
$\+s5W;|~ R*f0         pos = list_entry(pos->member.next, typeof(*pos), member))51Testing软件测试网d(DDS#l9IS8{
51Testing软件测试网7g;Ph%uqX8b\

  所以以上例子可改成如下,这样就比较简洁了。51Testing软件测试网_+TN1qN)?i

p^,J"T&G0 51Testing软件测试网UD}5D8VbG

struct ipstore *store;51Testing软件测试网kZ j Z^W$F u
    list_for_each_entry(store, &addr_list, list)
K^^6ZF|OLTd&P0    {51Testing软件测试网,Xbo~4Gl"J$w,W
      if(store->addr[0] == ist->addr[0])
l~iO8iV:m4D0      {
]i-f!~ uM0          if(ist)
e5Svc!cy_"lS0              kfree(ist);51Testing软件测试网!glqi4l3d
     
s#gNG d0          return 0;
hjKk*d(@zTE)Z0      }
PE.o^F0    }
51Testing软件测试网(j9^$fC](\;g&G?

  再来看安全删除遍历接口:51Testing软件测试网:|X g7hL"qy6f

!EH,j,HE0F0

'h elr"QN*S0E ^0
#define list_for_each_entry_safe(pos, n, head, member)          \51Testing软件测试网 C3a}\RUv:?%D
    for (pos = list_entry((head)->next, typeof(*pos), member),  \51Testing软件测试网sKsMT
        n = list_entry(pos->member.next, typeof(*pos), member); \
2l^:y&~*Fh#I+XQm0         &pos->member != (head);                    \
{g|s&Ag5{UsT0         pos = n, n = list_entry(n->member.next, typeof(*n), member))
51Testing软件测试网ays7p2g

  同样把上面的例子进行修改,这样删除时就不会破坏原有链表的结构出现“使用在释放后”的程序崩溃问题:51Testing软件测试网R} amf

51Testing软件测试网9Nu*D'GOC;onC+t

51Testing软件测试网M}RjC E bH@N

struct ipstore *v, *n;
k(vS I6ak{?7SL6V,q0    list_for_each_entry_safe(v, n, &addr_list, list)
Y G h_E @0    {
Em+SW:R^6^0        if(iph->saddr == v->addr[0])
Ty-Ao`walq!@0        {51Testing软件测试网"F(? [3c3d1JBc)P
              list_del(&v->list);//删除节点
{G1F*@ xa0              kfree(v);//还需要注意的是list_del()只是把节点从链表中摘除,它并不释放其占用的内存,所以需要手动释放内存。51Testing软件测试网DwAD]fF
        }
'T#L3v)@e(h C`0    }
51Testing软件测试网$Z5Xm+U1c:Z7w

  5、判空操作

tq5? |~}0

7GQZR9z M5N0  判空操作也比较重要,如果为空咱就什么都不操作,直接返回

&Bg d ~I_ Kl M(U1w0

nI&EfT+\3c5tm0

#imv V,z-TO0
if(list_empty(&addr_list))51Testing软件测试网.a6fEf+Y
    {51Testing软件测试网/zD Lyb3` K
        return 0;51Testing软件测试网RUsr{1D`pn
    }

}G!G _2H/BB0  总结:到此为止对于内核链表的操作基本上可以满足日常需求了,当然内核还提供移动和合并链表操作、反向遍历链表操作的接口,具体可参考LKD3e.51Testing软件测试网pp8?Uj t

51Testing软件测试网-ce}2c8o#X.v

  对链表的操作中一不小心就会出现“使用在释放后”的程序崩溃问题,请看如下使用普通遍历并删除的代码:51Testing软件测试网u|1V+J3c\

Oj:^'kn d;[#`-{0

8h3S[\3b8N8m7~0
51Testing软件测试网ppF8m%Ly YP

DEFINE_RWLOCK(v4_rwlock);51Testing软件测试网|z6g(h#j}8X/\P
    struct list_head *p;51Testing软件测试网;|,F0f#WHrU
    struct ipstore *store;51Testing软件测试网!O fR#os
    list_for_each(p, &addr_list)51Testing软件测试网G.R \jC`.K$c Q
    {
-TT1A4c [`0        store = list_entry(p, struct ipstore, list);51Testing软件测试网)xQW.C u\-K
        if(iph->saddr == store->addr[0])51Testing软件测试网|^-ZB L ~ O
        {51Testing软件测试网K*ku!{i:c#SG
            read_lock(&v4_rwlock);51Testing软件测试网:} V8I1h Y&W@
            list_del(&store->list);
O cH$bj+z f/gg0            read_unlock(&v4_rwlock);

(rHG;c:Ca0 51Testing软件测试网;t1m A'c2J W0I

            kfree(store);

Tf$L2V#i*d0

5MWp0jG.L3@0            return 0;51Testing软件测试网.{3n.d`h/wBHef+}8i
        }51Testing软件测试网m)BT*y~0Pv8M
    }
51Testing软件测试网a5l$V2PE

NJpFlm J*y0N0  这段代码虽然用了普通非安全遍历删除操作,但不会引起程序崩溃,但它已经埋下了安全隐患,不会崩溃是因为在删除链表并释放内存后直接 return 0了,没再对目前的链表进行遍历移动操作,所以不会出现问题,万一哪天没return呢?。所以很多时间程序出现莫名崩溃问题都发现不了,我想这也许是个 例子。

l;e\;Qm o }0 51Testing软件测试网?.S4G2{Q1A0V

  在操作写数据时的好习惯是加一把锁,前面例子为了突出链表操作没把加解锁的代码贴出,这个例子中就有体现,首先定义了一把读写锁,在删除链表结点时加锁,删除完毕解锁。51Testing软件测试网&kK-Iw;y_5`


uDngpK0

TAG:

 

评分:0

我来说两句

Open Toolbar