ext2 源代码解析之 “从路径名到目标结点” (一)

时间:2023-10-29 13:21:14

两个主要函数,path_init和path_walk,他们结合在一起根据给定的文件路径名称在内存中找到或者建立代表着目标文件或目录的dentry和inode结构。注意,最终是信息是读取到内存中的。其中有个函数__user_walk()将path_init()和path_walk()包装在一起。本节中的所有代码在fs/namei.c中。

1.分析外包装“__user_walk()”

778/*
779 * namei()
780 *
781 *is used by most simple commands to get the inode of a specified name.
782 *Open, link etc use their own routines, but this is enough for things
783 *like 'chmod' etc.
784 *
785 *namei exists in two versions: namei/lnamei. The only difference is
786 *that namei follows links, while lnamei does not.
787 *SMP-safe
788 */
789int __user_walk(const char *name, unsigned flags, struct nameidata *nd)
790 {
791 char *tmp;
792 int err;
793
794 tmp = getname(name);
795 err = PTR_ERR(tmp);//即使没有出错,仍然会进行转化,所以下文要将err=0
796 if (!IS_ERR(tmp)) {
797 err = 0;
798 if (path_init(tmp, flags, nd))
799 err = path_walk(tmp,nd);
800 putname(tmp);
801 }
802 return err;
803 }

1.1对函数参数的解释:

name是用户空间的路径名称:flags内容是一些标志位,定义在/include/linux/fs.h,如下:

1323 /*
1324 * The bitmask for a lookup event:
1325 * - follow links at the end
1326 * - require a directory
1327 * - ending slashes ok even fornonexistent files
1328 * - internal "there are morepath compnents" flag
1329 */
1330 #define LOOKUP_FOLLOW (1)
//symble link
1331 #define LOOKUP_DIRECTORY (2)
// the target must be a directory
1332 #define LOOKUP_CONTINUE (4)
1333 #define LOOKUP_POSITIVE (8)
1334 #define LOOKUP_PARENT (16)
1335 #define LOOKUP_NOALT (32)

注意第一项,符号链接可以跨越设备,而普通链接不可以。内核提供了不同的系统调用link和symlink,分别用于两种链接的建立关键数据结构.由于符号链接是可以跨越设备的,所以终点有可能悬空;而普通链接的终点是落实的。路径中包含符号链接的时候,是否继续往下搜索,规定如下:

82/* [Feb-Apr 2000 AV] Complete rewrite. Rules for symlinks:
83 * inside the path - always follow.//路径内部
84 * in the last component increation/removal/renaming - never follow.//创建,删除,重命名操作
85 * if LOOKUP_FOLLOW passed - follow.
86 * if the pathname has trailing slashes -follow.
87 * otherwise - don't follow.
88 * (applied in that order).
89 */

最后一个参数,是临时性的,仅仅用于存放返回结果。

655struct nameidata {
656 struct dentry *dentry;
657 struct vfsmount *mnt;
658 struct qstr last;
659 unsigned int flags;
660 int last_type;
661};

dentry记录找到的内存中的dentry和inode信息,mnt记录文件系统安装信息,例如文件系统的安装点,根节点等。

总结:一个函数与外界的信息交互,在数学模型中形如y=f(x),其中形参(一般形参和指针)和全局变量是输入变量;函数返回值,全局变量和指针所指向的变量都是函数输出。对于不改变指向值的指针,往往需要用const关键字。

1.2 执行流程分析:

getname()在系统空间分配一个页面,从用户空间将文件名复制到这个页面中,所以整个路径名字可以长达4K,由于这块空间是动态分配的,所以用完以后要通过putname释放。

关于err的内联函数和解释如下:

1300 /*
1301 * Kernel pointers have redundant information, so we can use a
1302 * scheme where we can return either an error code or a dentry
1303 * pointer with the same return value.
1304 *
1305 * This should be a per-architecture thing, to allow different
1306 * error and pointer decisions.
1307 */
1308 static inline void *ERR_PTR(long error)
1309 {
1310 return (void *) error;
1311 }
1312
1313 static inline long PTR_ERR(const void*ptr)
1314 {
1315 return (long) ptr;
1316 }
1317
1318 static inline long IS_ERR(const void*ptr)
1319 {
1320 return (unsigned long)ptr > (unsigned long)-1000L;
1321 }

注:函数返回值大于0xfffffc18的时候(就是-1000L),就说明出错了。

查看getname函数:

108 static inline int do_getname(const char *filename, char *page)
109 {
110 int retval;
111 unsigned long len = PATH_MAX + 1;
112
113 if ((unsigned long) filename >= TASK_SIZE) {
114 if (!segment_eq(get_fs(), KERNEL_DS))
115 return -EFAULT;
116 } else if (TASK_SIZE - (unsigned long) filename < PAGE_SIZE)
117 len = TASK_SIZE - (unsigned long) filename;
118
119 retval = strncpy_from_user((char *)page, filename, len);
120 if (retval > 0) {
121 if (retval < len)
122 return 0;
123 return -ENAMETOOLONG;
124 } else if (!retval)
125 retval = -ENOENT;
126 return retval;
127 }
129 char * getname(const char * filename)
130 {
131 char *tmp, *result;
132
133 result = ERR_PTR(-ENOMEM);
134 tmp = __getname();
135 if (tmp) {
136 int retval = do_getname(filename, tmp);
137
138 result = tmp;
139 if (retval < 0) {
140 putname(tmp);
141 result = ERR_PTR(retval);
142 }
143 }
144 return result;
145 }

#define ENOMEM 12

#define __getname()     kmem_cache_alloc(names_cachep, SLAB_KERNEL)

涉及内存管理的部分,我们以后再来研究。

2 path_init函数(在fs/namei.c).

[path_init()]
/*SMP-safe */
691int path_init(const char *name, unsigned int flags, struct nameidata *nd)
692 {// 这个函数的作用是:得到路径起点,设置了dentry和mnt两个变量,并进行初始化的相关设置
693 nd->last_type = LAST_ROOT; /* if there are only slashes... */
694 nd->flags = flags;
695 if (*name=='/')
696 return walk_init_root(name,nd);
697 read_lock(current->fs->lock);//
698 nd->mnt = mntget(current->fs->pwdmnt);
699 nd->dentry = dget(current->fs->pwd);
700 read_unlock(current->fs->lock);//
701 return 1;
702 }

函数调用关系图如下:

疑问:flags从何而来?

2.1 nameidata结构中的last_type字段:

首先将nameidata结构中的last_type字段设置成LAST_ROOT,这个字段可能的值定义在fs.h中,如下:

1336 /*
1337 * Type of the last component on LOOKUP_PARENT
1338 */
1339 enum {LAST_NORM, LAST_ROOT, LAST_DOT,LAST_DOTDOT, LAST_BIND};

在搜索过程中,这个字段的值会随着当前搜索结果而改变。例如,如果成功到了目标文件,它就变成LAST_NORM;如果停留在最后一个"."上面,则变成LAST_DOT.

2.2 其他字段的赋值

I)相对路径

if后面的语句,是相对路径的情况:nd->dentry = dget(current->fs->pwd)这句代码有两层意思:1)将nameidata的dentry设置成当前工作目录的dentry结构,表示虚拟绝对路径中,这个节点之前的节点都已经解决了 2)这个具体的dentry结构多了一个用户,所以调用dget递增它的共享计数,即是dentry中d_count的值。关于,Vfsmount结构pwdmnt,可以参考以后的文件系统的安装和拆卸。

233 static __inline__ struct dentry *dget(struct dentry *dentry)
234 {
235 if (dentry) {
236 if(!atomic_read(&dentry->d_count))
237 BUG();//
238 atomic_inc(&dentry->d_count);
239 }
240 return dentry;
241 }

#define atomic_read(v)          ((v)->counter)

d_count 是atomic_t 类型

typedef struct { volatile int counter; } atomic_t;

#define atomic_inc(v) atomic_add(1,(v))

II)绝对路径

if引导的是绝对路径下的情况,通过walk_init_root()从根节点开始查找(fs/namei.c):

[path_init() >>> walk_init_root()]
722/* SMP-safe */
723static inline int
724walk_init_root(const char *name, struct nameidata *nd)
725 {
726 read_lock(current->fs->lock);
727 if (current->fs->altroot &&!(nd->flags & LOOKUP_NOALT)) {
728 nd->mnt =mntget(current->fs->altrootmnt);
729 nd->dentry =dget(current->fs->altroot);
730 read_unlock(¤t->fs->lock);
731 if(__emul_lookup_dentry(name,nd))
732 return 0;
733 read_lock(¤t->fs->lock);
734 }
735 nd->mnt = mntget(current->fs->rootmnt);
736 nd->dentry = dget(current->fs->root);
737 read_unlock(currentt->fs->lock);
738 return 1;
739 }

35 static inline struct vfsmount *mntget(struct vfsmount *mnt)

36 {
 37         if (mnt)
 38                 atomic_inc(&mnt->mnt_count);
 39         return mnt;
 40 }

代码解析:如果当前进程没有通过chroot设置自己的根目录,current->fs->altroot是0,所以nameidata中的两个指针就进行if之外的相关设置;反之,要查看当初调用path_init()的参数flags中的标志位LOOKUP_NOTLT是否为1.通常情况下,这个标志为0,所以如果current->fs->altroot是1,就会通过__emul_lookup_dentry(name,nd)将nameidata中的指针设置成指向“替换”根目录指针。因为linux 在i386处理器上不支持替换根目录,所以current->fs->altroot总是NULL,我们也不在此介绍相关内容,读者可以查看相关书籍。

从path_init()成功返回的时候,nameidata结构中的指针dentry指向当前搜索路径的起点,接下来通过path_walk()顺着路径名的指引进行搜索。下面分段解析这个函数(fs/namei.c):

3.path_walk()函数解析

3.1)这个函数比较长(大概200行),我们进行分段解析,它的完整代码,我们可以查看附录部分。

442 /*
443 *Name resolution.
444 *
445 *This is the basic name resolution function, turning a pathname
446 *into the final dentry.
447 *
448 *We expect 'base' to be positive and a directory.
449 */
450 int path_walk(const char * name, structnameidata *nd)
451 {
452 struct dentry *dentry;
453 struct inode *inode;
454 int err;
455 unsigned int lookup_flags = nd->flags;
456
457 while (*name=='/')// 跳过多个连续的“/”
458 name++;
459 if (!*name)//name仅仅包含一个"/",则解析完成
460 goto return_base;
461
462 inode = nd->dentry->d_inode;//注意dentry已经在init中设置完成
463 if (current->link_count)// current is the type of task_struct
464 lookup_flags = LOOKUP_FOLLOW;
return_base;//return 0;

link_count是task_struct的成员之一,用于防止链接导致的循环搜索。另外,因为path_walk()的起点是一个目录,所以一定有相应的inode存在,而不是空。

小结:以上代码跳过路径前面若干个“/”,并且给inode和flag初始化

3.2)接下来是对路径节点所作的for循环,我们将其拆开来看

466        /* At this point we know we have a real path component. */
/*beforethis point ,we only parse the / or the co*/
467 for(;;) {
468 unsigned long hash;
469 struct qstr this;
470 unsigned int c;
471
472 err = permission(inode,MAY_EXEC);//检查对当前节点的访问权限,注意此处是可执行!!!
473 dentry =ERR_PTR(err);//translate long err into pointer
474 if (err)
475 break;
476
477 this.name = name;
478 c = *(const unsigned char*)name;//name was a pointer to char,c is char name[0] >>> assci
479
//#define init_name_hash() 0
480 hash = init_name_hash();
481 do {
482 name++;
483 hash =partial_name_hash(c, hash);//对c和hash进行2维hash
484 c = *(const unsignedchar *)name;
485 } while (c && (c !='/'));
486 this.len = name - (const char*) this.name;
487 this.hash =end_name_hash(hash);
488
/* remove trailing slashes? */
490 if (!c)
491 goto last_component;
492 while (*++name == '/');
493 if (!*name)
494 gotolast_with_slashes;
495

40 /* Finally: cut down the number of bits to a int value (and try to avoid losing bits) */
 41 static __inline__ unsigned long end_name_hash(unsigned long hash)
 42 {
 43         if (sizeof(hash) > sizeof(unsigned int))
 44                 hash += hash >> 4*sizeof(hash);
 45         return (unsigned int) hash;
 46 }

循环体中的局部变量this是一个qstr结构,用来存放路径名中当前节点的hash值和节点名的长度,定义在include/linux/dcache.h之中

struct qstr {
unsigned int hash;
unsigned int len;
const unsigned char *name;
};

其中,481-485 就是逐个字符计算出当前节点的hash值(hash的过程是,给定一个raw和一个hash函数,返回一个hash值),具体函数如下:

/* partial hash update function. Assumeroughly 4 bits per character */
34static __inline__ unsigned long partial_name_hash(unsigned long c, unsignedlong prevhash)
35 {
36 prevhash = (prevhash << 4) | (prevhash >> (8*sizeof(unsignedlong)-4));
37 return prevhash ^ c;
38 }
最终的hash值,index的计算如下:
40 /*Finally: cut down the number of bits to a int value (and try to avoid losingbits) */
41static __inline__ unsigned long end_name_hash(unsigned long hash)
42 {
43 if (sizeof(hash) > sizeof(unsigned int))
44 hash += hash >>4*sizeof(hash);
45 return (unsigned int) hash;
46 }

路径名的分隔符是“\”,这里紧跟着当前路径名称的字符有两种可能

(1)是“/0”,表示结尾

(2)是“/”,这里又包含几种情况,/后面有字符路径和没有字符路径(比如ls usr/inlude/)

小结:以上代码,对当前的节点进行hash值的计算,并且将指向路径的指针移动到下一个节点所在的地方,如果节点解析完成,将进行跳转。

3.3)接下来查看当前节点

I)当前节点是. or ..,需要进行特殊处理

从前面的分析知道,当前节点一定是目录节点

496                 /*
497 * "." and".." are special - ".." especially so because it has
498 * to be able to know aboutthe current root directory and
499 * parent relationships.
500 */
501 if (this.name[0] == '.')switch (this.len) {
502 default:
503 break;// is itfor the hiden file?no! if the node is directory, it cannot be named like .name
504 case 2:
505 if(this.name[1] != '.')
506 break;
507 follow_dotdot(nd);//如果当前就是“..”,那么需要向上跑到已经到达节点nd->dentry的父目录中去
508 inode =nd->dentry->d_inode;
509 /* fallthrough*/
//nomatter it is .. or ., we go to parse the next node
510 case 1:
511 continue;
512 }

下面,我们来看看 follow_dotdot(nd)

[path_walk >> follow_dotdot]
405 static inline void follow_dotdot(structnameidata *nd)
406 {
407 while(1) {
408 struct vfsmount *parent;
409 struct dentry *dentry;
410 read_lock(current->fs->lock);
411 if (nd->dentry ==current->fs->root &&
412 nd->mnt ==current->fs->rootmnt) {//nd指向的fsmnt结构是否代表根设备
413 read_unlock(¤t->fs->lock);
414 break;
415 }//当前节点就是root节点,所以父节点还是本身,保持不变
 416                read_unlock(¤t->fs->lock);
417 spin_lock(&dcache_lock);
418 if (nd->dentry != nd->mnt->mnt_root){//没有进行设备之间的跳跃
419 dentry =dget(nd->dentry->d_parent);//完成了递增计数器和赋值
420 spin_unlock(&dcache_lock);
421 dput(nd->dentry);
422 nd->dentry =dentry;
423 break;
424 } //下面是跨越了设备的情况
425 parent=nd->mnt->mnt_parent;
 426                 if (parent == nd->mnt) {
427 spin_unlock(&dcache_lock);
428 break;
429 }
430 mntget(parent);
431 dentry=dget(nd->mnt->mnt_mountpoint);
432 spin_unlock(&dcache_lock);
433 dput(nd->dentry);
434 nd->dentry = dentry;
435 mntput(nd->mnt);
436 nd->mnt = parent;
437 }
438 while (d_mountpoint(nd->dentry) &&__follow_down(&nd->mnt, &nd->dentry))
439 ;
440 }
441

这里分三种情况,

第一种,已到达节点就是本进程的根节点,此时,保持不变。对应第一个if语句。

第二种,已到达节点的nd->dentry和它的父节点在同一个设备上。这种情况下,父节点的dentry结构必然已经建立在内存中,而且dentry结构中的指针d_patent就指向其父节点,所以往上走一层是简单的事情。第二个if语句

第三种,已经到达节点nd->dentry就是其所在设备的根节点,再往上就到达另外一个设备中。从文件系统的角度看,安装点和所安装设备的根目录是等价的,而目录树的根目录不等同于设备的根目录,我们在当前设备的根目录中,从这里往上一层,是目录树的上一层目录而不是安装点本身。当将一个存储设备“安装”到另外一个设备的某个节点的时候,内核会分配和设置一个fsmount结构,通过这个结构将两个设备和两个节点连接起来(参见后面“文件系统的安装和拆卸”)。所以,每个安装的设备都有一个fsmount结构,结构中有一个mnt_parent指向父设备,但是根设备的这个指针指向它自己,因为它已经没有父设备了;另外一个指针mnt_mountpoint, 指向代表着安装点的dentry。

先检查当前Vfs结构是否代表着根设备;如果是的话,立即通过423行的break语句结束while循环;反之,进行相关处理。(这里没有解释清楚,我们在学习了下一节以后再回头看)。

回到path_walk的代码之中,我们注意到“case2”没有break语句, 所以会通过case 1的continue语句回到for循环开始的地方执行,继续处理下一个节点。大多数节点不是以.命名的,我们来看看正常节点的处理流程。

我们来看以下dput:

  113  * @dentry: dentry to release 
 114  *
 115  * Release a dentry. This will drop the usage count and if appropriate
 116  * call the dentry unlink method as well as removing it from the queues and
 117  * releasing its resources. If the parent dentries were scheduled for relea     se
 118  * they too may now get deleted.
 119  *
 120  * no dcache lock, please.
 121  */
123 void dput(struct dentry *dentry)
124 {
125 if (!dentry)
126 return;
127
128 repeat:
129 if (!atomic_dec_and_lock(&dentry->d_count, &dcache_lock))
130 return;
131
132 /* dput on a free dentry? */
133 if (!list_empty(&dentry->d_lru))
134 BUG();
135 /*
136 * AV: ->d_delete() is _NOT_ allowed to block now.
137 */
138 if (dentry->d_op && dentry->d_op->d_delete) {
139 if (dentry->d_op->d_delete(dentry))
140 goto unhash_it;
141 }
142 /* Unreachable? Get rid of it */
143 if (list_empty(&dentry->d_hash))
144 goto kill_it;
145 list_add(&dentry->d_lru, &dentry_unused);
146 dentry_stat.nr_unused++;
147 /*
148 * Update the timestamp
149 */
150 dentry->d_reftime = jiffies;
151 spin_unlock(&dcache_lock);
152 return;
153
154 unhash_it:
155 list_del_init(&dentry->d_hash);
156
157 kill_it: {
158 struct dentry *parent;
159 list_del(&dentry->d_child);
160 /* drops the lock, at that point nobody can reach this dentry */
161 dentry_iput(dentry);
162 parent = dentry->d_parent;
163 d_free(dentry);
164 if (dentry == parent)
165                         return;
 166                 dentry = parent;
 167                 goto repeat;
 168         }
 169 }

子函数解析:

 13 int atomic_dec_and_lock(atomic_t *atomic, spinlock_t *lock)
14 {
15 int counter;
16 int newcount;
17
18 repeat:
19 counter = atomic_read(atomic);//读取atomic指向的值
20 newcount = counter-1;
21
22 if (!newcount)
23 goto slow_path;
24
25 asm volatile("lock; cmpxchgl %1,%2"
26 :"=a" (newcount)
27 :"r" (newcount), "m" (atomic->counter), "0" (counter));//what?
28
29 /* If the above failed, "eax" will have changed */
30 if (newcount != counter)//why?
31 goto repeat;
32 return 0;
33
34 slow_path:
35 spin_lock(lock);
36 if (atomic_dec_and_test(atomic))
37 return 1;
38 spin_unlock(lock);
39 return 0;
40 }
 22 #define atomic_read(v)          ((v)->counter)
23 #define atomic_set(v,i) ((v)->counter = (i))
100 #define atomic_dec_return(v) atomic_sub_return(1,(v))
101 #define atomic_inc_return(v) atomic_add_return(1,(v))
102
103 #define atomic_sub_and_test(i,v) (atomic_sub_return((i), (v)) == 0)
104 #define atomic_dec_and_test(v) (atomic_sub_return(1, (v)) == 0)
105
106 #define atomic_inc(v) atomic_add(1,(v))
107 #define atomic_dec(v) atomic_sub(1,(v))
 82 static __inline__ long atomic_sub_return(int i, atomic_t * v)
83 {
84 long temp, result;
85 __asm__ __volatile__(
86 "1: ldl_l %0,%1\n"
87 " subl %0,%3,%2\n"
88 " subl %0,%3,%0\n"
89 " stl_c %0,%1\n"
90 " beq %0,2f\n"
91 " mb\n"
92 ".subsection 2\n"
93 "2: br 1b\n"
94 ".previous"
95 :"=&r" (temp), "=m" (v->counter), "=&r" (result)
96 :"Ir" (i), "m" (v->counter) : "memory");
97 return result;
98 }

II)当前节点是正常节点

/*
514 * See if the low-levelfilesystem might want
515 * to use its own hash..
516 */
517 if (nd->dentry->d_op&& nd->dentry->d_op->d_hash) {
518 err =nd->dentry->d_op->d_hash(nd->dentry, &this);//使用自己的hash函数进行hash
519 if (err < 0)
520 break;
521 }//有些文件系统通过dentry_operations结构中指针d_hash提供专用的hash函数
522 /* This does the actuallookups.. */
523 dentry =cached_lookup(nd->dentry, &this, LOOKUP_CONTINUE);//在内存中寻找该节点已经建立的dentry结构
524 if (!dentry) {//没有在内存中找到相应dentry
525 dentry =real_lookup(nd->dentry, &this, LOOKUP_CONTINUE);//在磁盘上进行寻找
526 err = PTR_ERR(dentry);
527 if (IS_ERR(dentry))
528 break;
}
530 /* Check mountpoints.. */
531 while (d_mountpoint(dentry)&& __follow_down(&nd->mnt, &dentry))
532 ;//what?
533
534 err = -ENOENT;
535 inode = dentry->d_inode;
536 if (!inode)
537 goto out_dput;
538 err = -ENOTDIR;
539 if (!inode->i_op)
540 goto out_dput;
541
542 if(inode->i_op->follow_link) {
543 err =do_follow_link(dentry, nd);
544 dput(dentry);//删除对应dentry条目
545 if (err)
546 gotoreturn_err;
547 err = -ENOENT;
548 inode =nd->dentry->d_inode;
549 if (!inode)
550 break;
551 err = -ENOTDIR;
552 if (!inode->i_op)
553 break;
554 } else {
555 dput(nd->dentry);
556 nd->dentry =dentry;
557 }
558 err = -ENOTDIR;
559 if(!inode->i_op->lookup)
560 break;
561 continue;
562 /* here ends the main loop */

对当前节点的搜索是通过cached_lookup()&real_lookup()两个函数进行的。内核中有个杂凑表,dentry_hashtabel,是一个list_head指针数组,一旦在内存中建立目录节点的dentry结构,就根据其节点名称的hash值挂入hash表的某个队列。当路径名的某个节点变成path_walk()的当前节点时,位于其上游节点的dentry肯定都已经在内存的hash表中,如果在内存的相应结构中找不到相应节点的dentry结构,那么需要通过real_lookup()到磁盘目录寻找,然后载入内存。

内存中还有一个队列dentry_unused,存放共享计数为0,不再使用的dentry结构(有时,也需要把还在使用的dentry结构强制脱链,强迫从磁盘*问),已经脱链的那个数据结构由最后调用dput()使其共享计数变成0的进程负责将其释放。

事实上,dentry中有6个list_head,d_vfsmnt,d_hash,d_lru,d_child,d_subdirs,d_alias.注意,list_head可以作为队列的头部,也可以将其所在的数据结构挂入到某个队列中。d_vfsmnt尽在dentry作为安装点的时候才使用,一个dentry一旦建立就通过d_hash挂入dentry_hashtable的某个队列里,当共享计数变成0的时候,通过d_lru挂入LRU队列dentry_unused之中,dentry通过d_child挂入父节点的d_subdir之中,同时通过d_patrent结构(这不是一个head_list)指向父目录的dentry结构,而它各自的子目录的dentry结构都在它的d_subdir队列中。

一个dentry对应一个inode,但一个inode可能对应多个dentry。所以在inode中有个队列i_dentry,目录项功过dentry结构中的d_alias挂入相应的inode结构中的i_dentry队列。此外dentry结构中还有指针d_sb,指向其锁在设备超级快的super_block结构;指针d_op,指向特定系统的dentry_operations结构。

struct list_head {
struct list_head *next, *prev;
}; //acttrually, it is only a doubly linked list with no content

(未完待续)