Linux内核中的klist分析
分析的内核版本照样是2.6.38.5。
Linux内核中的klist是在神级的双向链表上扩展而形成的。先给出一个图。
很清晰也很简单。 先说表头: K_lock:是一把锁,用来锁表的。这个就不多啰嗦了。 k_list:双向链表,用来联系各节点及链表头。 get、put:两个函数指针,是用来操作链表中的节点接口。 再说节点: n_klist是一个空指针,随便用来指啥,但在我们的klist原语中是用来指向链表头的。另外其最低位用来做标志位。 n_node:双向链表,用来联系各节点及链表头。 n_ref:引用计数。 接下来我们来分析一下我们感兴趣的几个东西。 首先是 WARN_ON(condition) BUG_ON(condition) 这两个宏。 经过分析后其在默认配置中被定义为: #ifndef HAVE_ARCH_BUG_ON #define BUG_ON(condition) do { if (condition) ; } while(0) #endif #ifndef HAVE_ARCH_WARN_ON #define WARN_ON(condition) ({ / int __ret_warn_on = !!(condition); / unlikely(__ret_warn_on); / }) #endif 可以认为这个宏是没用的。因为在klist中并没有对这个返回值作判断。 另外有的朋友可能会说linux内核中怎么会有这样的垃圾代码,会造成不必要的运行开销。其实我想说,你太低估linux内核开发者的水平了,这是一份国际顶尖级的高手写出来的代码,他们对编译器,操作系统,CPU体系结构的认识程度远不是你我所能达到的。说了这么多,让我们来一起仔细领会这些大牛的深厚功力。 我先写了个小例子程序: #include "asm-generic/bug.h" int main() { int aa = 0x0; int condition = 0x77; WARN_ON(condition); aa = 0x88; return 0; } 然后利用命令: gcc test.c -E -I/opt/kernel/linux-2.6.38/linux-2.6.38.5/include/ > test.txt 再vi test.txt 我们可以看到有如下内容: # 1 "test.c" # 1 "<built-in>" # 1 "<command line>" # 1 "test.c" # 1 "/opt/kernel/linux-2.6.38/linux-2.6.38.5/include/asm-generic/bug.h" 1 # 1 "/opt/kernel/linux-2.6.38/linux-2.6.38.5/include/linux/compiler.h" 1 # 5 "/opt/kernel/linux-2.6.38/linux-2.6.38.5/include/asm-generic/bug.h" 2 # 2 "test.c" 2 int main() { int aa = 0x0; int condition = 0x77; ({ int __ret_warn_on = !!(condition); unlikely(__ret_warn_on); }); aa = 0x88; return 0; } 通过编译预处理,我们可以看到宏确实被展开了。 接下来我们反汇编这段代码: 我们输入命令: gcc test.c -c -I/opt/kernel/linux-2.6.38/linux-2.6.38.5/include/ -o test.o –g 再:disassemble main 得到: 0x00000000 <main+0>: lea 0x4(%esp),%ecx 0x00000004 <main+4>: and $0xfffffff0,%esp 0x00000007 <main+7>: pushl -0x4(%ecx) 0x0000000a <main+10>: push %ebp 0x0000000b <main+11>: mov %esp,%ebp 0x0000000d <main+13>: push %ecx 0x0000000e <main+14>: sub $0x14,%esp 0x00000011 <main+17>: movl $0x0,-0x10(%ebp) 0x00000018 <main+24>: movl $0x77,-0xc(%ebp) 0x0000001f <main+31>: cmpl $0x0,-0xc(%ebp) 0x00000023 <main+35>: setne %al 0x00000026 <main+38>: movzbl %al,%eax 0x00000029 <main+41>: mov %eax,-0x8(%ebp) 0x0000002c <main+44>: mov -0x8(%ebp),%eax 0x0000002f <main+47>: mov %eax,(%esp) 0x00000032 <main+50>: call 0x33 <main+51> 0x00000037 <main+55>: movl $0x88,-0x10(%ebp) 0x0000003e <main+62>: mov $0x0,%eax 0x00000043 <main+67>: add $0x14,%esp 0x00000046 <main+70>: pop %ecx 0x00000047 <main+71>: pop %ebp 0x00000048 <main+72>: lea -0x4(%ecx),%esp 多了很多代码。这linux内核的作者这么蠢? 我们再看看inux内核根makefile下面的几行: HOSTCC = gcc HOSTCXX = g++ HOSTCFLAGS = -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-p ointer HOSTCXXFLAGS = -O2 加了个O2! Ok,我们也加个O2。 wwhs_klist]# gcc test.c -c -I/opt/kernel/linux-2.6.38/linux-2.6.38.5/include/ -o test.o -g –O2(后面分别用优化选项-O、-O1、-Os在我们关注的这个地方表现出来的效果是相同的)。 gdb test.o disassemble main 得到: 0x00000000 <main+0>: lea 0x4(%esp),%ecx 0x00000004 <main+4>: and $0xfffffff0,%esp 0x00000007 <main+7>: pushl -0x4(%ecx) 0x0000000a <main+10>: push %ebp 0x0000000b <main+11>: mov %esp,%ebp 0x0000000d <main+13>: push %ecx 0x0000000e <main+14>: sub $0x4,%esp 0x00000011 <main+17>: movl $0x1,(%esp) 0x00000018 <main+24>: call 0x19 <main+25> 0x0000001d <main+29>: add $0x4,%esp 0x00000020 <main+32>: xor %eax,%eax 0x00000022 <main+34>: pop %ecx 0x00000023 <main+35>: pop %ebp 0x00000024 <main+36>: lea -0x4(%ecx),%esp 0x00000027 <main+39>: ret 哈哈。看到了吧,全都被优化掉了,所以加的这些东西对我们的性能不会造成任何影响! 我晕。好像偏题了,不好意思。 不过发现klist除了这个以外没有别的值得讲的。 值得一提的是: 个人觉得klist中有些函数的命名不是非常的好,容易让人误解,经过思考后发现klist的命名虽然是有规律的,但是说实话,确实可以做得更好。 另外有一个值得一讲的是: void klist_remove(struct klist_node *n) { struct klist_waiter waiter; waiter.node = n; waiter.process = current; waiter.woken = 0; spin_lock(&klist_remove_lock); list_add(&waiter.list, &klist_remove_waiters); spin_unlock(&klist_remove_lock); klist_del(n); for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); if (waiter.woken) break; schedule(); } __set_current_state(TASK_RUNNING); } 我们分析一下: struct klist_waiter waiter; waiter.node = n; waiter.process = current; //这是个任务结构体,把当前任务结构体保存起来 waiter.woken = 0; //这是用于唤醒任务的标记 spin_lock(&klist_remove_lock); list_add(&waiter.list, &klist_remove_waiters); //将waiter.list链入klist_remove_waiters spin_unlock(&klist_remove_lock); 接下来: void klist_del(struct klist_node *n) { klist_put(n, true); } static void klist_put(struct klist_node *n, bool kill) { struct klist *k = knode_klist(n); void (*put)(struct klist_node *) = k->put; spin_lock(&k->k_lock); if (kill) knode_kill(n); if (!klist_dec_and_del(n)) put = NULL; spin_unlock(&k->k_lock); if (put) put(n); } 重点在: static int klist_dec_and_del(struct klist_node *n) { return kref_put(&n->n_ref, klist_release); } int kref_put(struct kref *kref, void (*release)(struct kref *kref)) { WARN_ON(release == NULL); WARN_ON(release == (void (*)(struct kref *))kfree); if (atomic_dec_and_test(&kref->refcount)) { //一定要把引用计数消耗完才能调用release()
然后再gdb test.o
release(kref);
return 1;
}
return 0;
}
重点在:
static void klist_release(struct kref *kref)
{
struct klist_waiter *waiter, *tmp;
struct klist_node *n = container_of(kref, struct klist_node, n_ref);
WARN_ON(!knode_dead(n));
list_del(&n->n_node);
spin_lock(&klist_remove_lock);
list_for_each_entry_safe(waiter, tmp, &klist_remove_waiters, list) {
if (waiter->node != n)
continue;
waiter->woken = 1;
mb();
wake_up_process(waiter->process);
list_del(&waiter->list);
}
spin_unlock(&klist_remove_lock);
knode_set_klist(n, NULL);
}
到这里我们可以看到:
waiter->woken = 1;
mb();
标记也打了,内存屏障也设了(多任务的时候用这个玩意可以把寄存器的值回写到内存,防止编译器优化产生BUG,其最主要的函数是编译器内置的,有兴趣的同志可以自行学习一下)。接下就是用wake_up_process(waiter->process)要换醒我们之前设置好的进程了。
回到我们之前的地方:
void klist_remove(struct klist_node *n)
{
struct klist_waiter waiter;
waiter.node = n;
waiter.process = current;
waiter.woken = 0;
spin_lock(&klist_remove_lock);
list_add(&waiter.list, &klist_remove_waiters);
spin_unlock(&klist_remove_lock);
klist_del(n);
for (;;) {
set_current_state(TASK_UNINTERRUPTIBLE);
if (waiter.woken)
break;
schedule();
}
__set_current_state(TASK_RUNNING);
}
跳进循环:
用set_current_state(TASK_UNINTERRUPTIBLE) 设置当前任务为不可中断状态,
接下来就是判断我们之前打的标记了,所以前面的标记如果没打的话,就会继续调用schedule()来切换任务,只有当标记被置了1(也就是说只有引用计数被消耗完了),才会跳出循环。这是设计的好的地方,可以确保在多任务环境下,所有调用者都有机会释放,但也是容易出问题的地方,如果不小心控制引用计数,一个死循环就这么产生了。
接下来再用__set_current_state(TASK_RUNNING)将任务状态切成正在行运状态。