linux kernel
1.1 内核常见的数据类型
linux内核中包含许多对象和数据结构。使用链表和二叉搜索树先将这些对象分组放入一个容器中,然后再以某种有效的方式查找单个元素。
1.1.1 链表
在计算机科学中,链表是一种常见的数据类型。在linux内核中常以循环双向链表的形式出现。因此,给定链表中的任意节点,都可以找到其下一节点和上一节点。链表的完整代码放在头文件include/linux/list.h.
1 #ifndef _LINUX_LIST_H 2 #define _LINUX_LIST_H 3 4 #include <linux/stddef.h> 5 #include <linux/poison.h> 6 #include <linux/prefetch.h> 7 #include <asm/system.h> 8 9 struct list head { 10 struct list_head *next,*prev; 11 } 12 13 #define LIST_HEAD_INIT(name) {&(name),&(name)} 14 15 #define LIST_HEAD(name) \ 16 struct list_head name = LIST_HEAD_INIT(name)
17 static inline void INIT_LIST_HEAD(struct list_head *list)
18 {
19 list->prev = list;
20 list->next = list;
21 }
22
23 #ifndef CONFIG_DEBUG_LIST
24 static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next)
25 {
26 next->prev = new;
27 prev->next = new;
28 new->prev = prev;
29 new->next = next;
30 }
31 #else
32 extern void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next);
33 #endif
34
35 static inline void list_add(struct list_head *new, struct list_head *head)
36 {
37 __list_add(new,head,head->next);
38 }
39
40 static inline list_add_tail(struct list_head *new, struct list_head *head)
41 {
42 __list_add(new,head->prev,head);
43 }
44
45 static inline void __list_del(struct list_head *prev, struct list_head *next)
46 {
47 next->prev = prev;
48 prev->next = next;
49 }
50
51 #ifndef CONFIG_DEBUG_LIST
52 static inline void list_del(struct list_head *entry)
53 {
54 __list_del(entry->prev,entry->next);
55 entry->next = LIST_POISON1;
56 entry->prev = LIST_POISON2;
57 }
1.1.2 查找
有序表根据链表中每个元素的关键值进行排序。如果想要找到某个特定元素,可以从表头开始,顺序查找整个链表,比较当前节点的关键值与给定的关键值。如果不相等,就继续比较下一个元素,直到找到匹配的值为止。
对于查找算法而言,大O表示法常用语从理论上来衡量一个算法找到某给定关键值的执行时间,他代表对于一个给定值n在最坏情况下所花费的查找时间。线性查找的效率是O(n/2),这就意味着,平均来算,这到一个给定的关键之必须与链表中的一般元素进行比较。
1.1.3 树
树用于Linux内存管理中,能够高效地访问并操作数据。此时,其效率就是用存储和检索数据的快慢程度来衡量的。
1.1.3.1 二叉树
前面我们用线性查找来查找关键值,在每次循环中比较关键值的大小。如果每次比较都可以将有序表中待处理的节点数目减半,效率会提高很多。
二叉树和链表不同,它是一种非线性的扥层数据结构。在二叉树中,每个元素或借点志向一主要规则就是左子节点的关键值小于父节点的关键值,而右子节点的关键值大于或等于父节点的关键值。因此,对于一个给定几点的关键值,其左子树上所有的关键值上所有节点的关键值杜晓宇该节点,而其右子树上所有节点的关键值都大于或等于该节点。
在均高二叉树中,根节点带任意叶节点的距离都是一样远的。节点添加到二叉树中后,为了保证查找的效率,必须进行平衡化处理,这可以通过旋转来实现。插入一个节点后,给定节点e,如果他有一个比任何其它叶节点高两层的左子树,就必须右旋转,如果每次插入节点后都进行了平衡化处理,那么,每次最多只需要作一次旋转。满足平衡规则的二叉树被称为AVL树。
1.1.3.2 红黑树
红黑树类似于AVL树,用于Linux内存管理。红黑树就是平衡二叉树,其每个节点都有红或黑的属性。
红黑树的规则如下:
- 每个节点要么是红色的,要么是黑色的。
- 如果一个节点是红色的,那么它的所有子节点都是黑色的。
- 所有叶节点都是黑色的。
- 从根节点到叶节点的每条路径包含同样多的黑色节点。
1.2 内联汇编
gcc编译器支持的另一种编码形式是内联汇编代码。内联汇编不需要调用单独编译好的汇编程序。我们可以通过特定的结构告诉编译器将代码块组合到一起,而不是要编译该代码块。
以下就是内联汇编程序的机构:
------------------------------------------------------------------------------------------------------------
1 asm(assembler instruction(s)
2 :output operands (optional)
3 :input operands (optional)
4 :clobbered registers (optional)
5 );
例如:
asm("movl %eax, %ebx");
1.2.1 asm
由于与其他结构同名,关键字asm也许会在编译时引发错误。读者常常会看到写成__asm__的表达式,两者完全相同。
1.2.2 __volatile__
另一个常用的修饰符是__volatile__,该修饰符对对会变大吗的意义非同寻常,它告诉编译器不要优化这个内联汇编例程。
static __inline__ unsigned long xchg_u32(volatile void *p, unsigned long cal) { unsigned long prev; __asm__ __volatile__("\n 1: lwarx %0,0,%2\n" "stwcx %3,0,%2 \n bne 1b\"); return prev; }
1.3 特殊的C语言用法
Linux内核中的许多规范都需要经过反复查找和阅读才能发现器最终的意义和目的。
1.3.1 asmlinkage
asmlinkage告诉编译器将参数存入局部栈,这就涉及宏FASTCALL,它通知编译器将参数传给通用寄存器。
include/asm/linkage.h 4 #define asmlinkage CPP_ASMLINKAGE __attribute__ ((regparm(0))) 5 #define FASTCALL(x) x__attribute__((regparm(3))) 6 #define fastcall __attribute__((regparm(3)))
1.3.2 UL
UL常用在数值常数之后,表明该常数无符号长整型。这样有助于编写出于体系无关的代码。
1.3.3 内联
关键字inline表明要优化函数的可执行代码,这可以通过将函数的代码合并到调用程序的代码中实现。一个声明为“static inline”的函数促使编译器尝试着将其代码合并到所有调用它的程序中。
1.3.3 const和volatile
const表示只读的意思。const int *x表示x是一个指向const整数的指针,int const *x表示x是一个const指针指向一个整数。
volatile表明变量无序经过高就可以被修改。他通知编译器每次使用这个标记的变量时都要重新加载其值,而不是存储和访问一个副本。中断处理,硬件寄存器以及并发进程之间共享的变量都是典型的被标记为volatile的例子。
1.4 内核探索工具一览
成功编译并构建自己的Linux内核后,我们要使用分析内核文件的工具。
1.4.1 objdump/readelf
objdump和readelf实用程序可分别用于显示目标文件和ELF文件中的任何信息。我们可以借助于命令行参数使用命令来查看给定目标文件的文件头、文件大小的结构。
1.4.2 hexdump
命令hexdump可以显示给定十六进制/ASCII码/八进制格式文件的内容。
1.4.3 nm
实用程序nm可以列出指定文件中的符号,他能够显示符号的值,类型和名字,虽然不如其他使用程序一样有用,单挑是库文件却能大显身手。
1.4.4 objcopy
当你想要复制一个目标文件而忽略或改变其某方面的内容时,可以使用objcopy命令。objcopy的常见用法是去掉调试后正式运行的目标文件中的调试符号。
1.4.5 ar
ar或archive命令有助于维护连接程序使用索引函数库。ar命令可以将一个或多个目标文件合并到一个链接库中,也可以从单个链接库中将目标文件分离出来。
1.5 内核发言:倾听来自内核的信息
1.5.1 printk()
1.5.2 dmesg
linux内核有多种方式可以用于存储日志和信息。sysklogd()是syslogd()和klogd()的组合。linux内核通过klogd()发送消息,并给他表上适当的警告级。所有级别的信息都存储在/proc/kmsg中。
1.5.3 /var/log/message
1.6 其他奥秘
1.6.1 __init
宏__init告诉编译器相关的函数或变量仅用于初始化。编译器将标有__init所有代码存储到特俗的内存段中,初始化结束后就释放这段内存。
类似的,如果某些数据只在初始化时才用到,这些数据必须用__initdata标记。同样,宏__exit和__exitdata仅用于退出和关闭例程。
1.6.2 likely()和unlikely
1.6.3 IS_ERR和PTR_ERR
宏IS_ERR将负的错误号编码成指针,而宏PTR_ERR则将指针恢复成错误号。
1.6.4 通知程序链
通知程序链机制为内核提供它感兴趣的可变异步事件的相关信息。这个通用接口将其可用性扩展到了内核的所有子系统和组件中。
通告程序链(notifier chain)就是一个notifier_block对象的单链表。
include/linux/notifier.h struct notifier_block{ int (*notifier_call)(struct notifier_block *self, unsigned long, void *); struct notifier_block_next; int priority; };
notifier_block包含指向函数notifier_call的指针。当时间发生时调用该函数,其参数包括指向保存信息notifier_block对象的指针、相应事件代码和标志的值,以及指向特定于子系统的数据类型的指针。notifier_block结构还包含指向链中下一个notifier_block的指针和优先级声明。
例程notifier_block_register和notifier_block_unregister()分别用于向特定通告程序链注册或注销notifier_block对象。
1.7 总结
1.7.1 写一个字符设备驱动程序
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> MODULE_LICENSE("GPL"); /* initalize */ static int __init hello_init(void) { printk("<1>: hello world.\n"); return 0; } /* exit */ static void __exit hello_exit(void) { printk("Goodbye, world!"); } module_init(hello_init); module_exit(hello_exit);
1.7.2 编译模块(makefile 书写)
#Makefile for Linux kernel primer module skeletom(linux version) obj-m += hello.o make -c /kernel/linux version SUBDIRS=$(PWD) modules
1.7.3 运行代码
;加载模块到内核中
# insmod hello.ko
;从内核中移除模块
#rmmod hello