linux内核源代码位于/usr/src/linux目录下,主要分布结构是:
文件描述:
- include/子目录包含的是建立内核代码时所需的大部分包含文件
- init/子目录包含了内核的初始化代码,内核起始点
- arch/子目录包含了Linux支持的所有硬件结构的内核代码
- drivers/子目录包含了内核中所有的设备驱动程序,例如字符设备、块设备、scsi设备驱动程序等
- fs/子目录包含了所有文件系统的代码
- net/子目录包含了内核中关于网络的代码
- mm/子目录包含了所有的内存管理代码
- ipc/子目录包含了进程间通信的代码
- kernel/子目录包含了主内核代码
Linux内核模块编程
- 编写模块
- #include <linux/module.h>
- #include <linux/kernel.h>
- #include <linux/init.h>
- static int_init lkp_init(void)
- {
- printk("<1>Hello,World!from the kernel space...\n");
- return 0;
- }
- static void_exit lkp_cleanup(void)
- {
- printk("<1>Goodbye,Word!leaving kernel space...\n)";
- }
- modele_init(lkp_init;)
- mode_exir(lkp_cleanup);
- MODELE_LICESEN("GPL");
复制代码- 说明:
1.
- module.h头文件中包含对模块的结构定义以及模块的版本控制,任何模块程序的编写都要包含这个头文件;
- 头文件kernel.h包含了常用的内核函数;
- 而头文件init.h包含了宏_init和_exit,宏_init告诉编译器程序相关的函数和变量仅用于初始化,编译程序将标有_init的所有代码存储到特殊的内存段中,初始化结束后就释放这段内存。
- 函数lkp_init()是模块初始化函数,函数lkp_cleanp()是模块的推出和清理函数。
- printk()函数由内核定义,与C库中的printf()类似。字符串<1>是输出的级别,表示立即在终端输出。
- module_init()和cleanup_exit()是模块编程中最基本也是最必须的两个函数。module_init()向内核注册模块提供新功能,cleanup_exit()注销由模块所提供的功能。
- 最后一句是告诉内核该模块具有GNU公开许可证。
- 编译模块
假定程序名为hellomod.c,只有超级用户才能加载和卸载模块,其Makefile文件基本内容如下
- #Makefile2.6
- obj-m:=hellomod.o #产生hellomod模块的目标文件
- CURRENT_PATH:=$(shell pwd) #模块所在的当前路径
- LINUX_KERNEL:=$(shell uname -r) #Linux内核源代码的当前版本
- LINUX_KERNEL_PATH:= /usr/src/linux-headers-$(LINUX_KERNEL) #Linux内核源代码的绝对路径
- all:
- make -C $(LIN UX_KERNEL_PATH) M=$(CURRENT_PATH) modules #编译模块
- clean:
- make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean #清理
复制代码obj-m :=赋值语句含义:要使用目标文件hellomod.o建立一个模块,最后生成hellomod.ko,如果有一个名为module.ko的模块依赖于两个文件file.o和file2.o,则可以使用module-obj扩展,如下:
- obj-m : = module.o
- module-objs : = file.o file2.o
复制代码make命令运行Makefile。
- 运行模块
- 当编译好模块,要将编译好的模块插入到内核中时可以使用insmod命令实现。
- lsmod命令可以用来检查模块是否正确插入到了内核中。
- 模块的输出由prink()来产生,该函数默认打印系统文件
- 快速浏览该信息可以输入:
- tail/var/log/messages(打印日志文件的最后10行内容)
- 使用rmmod命令,加上insmod中看到的模块名,可以从内核中移除该模块(还可以看到退出时显示的信息)
- 应用程序和内核比较
| 比较内容 | C语言应用程序 | 内核模块程序 |
| — | — | — |
| 使用函数 | libc库 | 内核函数 |
| 运行空间 | 用户空间 | 内核空间 |
| 运行权限 | 普通用户 | 超级用户 |
| 入口函数 | main() | module_init() |
| 出口函数 | exit() | module_exit() |
| 编译 | gcc -c | make |
| 连接 | gcc | insmod |
| 运行 | 直接运行 | insmod |
| 调试 | gdb | kdbug,kdb,kgdb等 |
Linux内核中链表的实现和应用
1.链表的演化
- struct my_list{
- void *mydata;
- struct my_list *next;
- struct my_list *prev;
- };
复制代码如图所示,一个双链表,通过前驱(Prev)和后继(Next)两个指针域,可以从两个方向遍历双链表,使得遍历链表的代价减少。若打乱前驱和后继的依赖关系,就可以构成“二叉树”;如果再让首结点的前驱指向链表的尾结点、尾结点的后继指向首结点,就构成了循环链表;如果设计更多的指针域,就可以构成负责的树状结构。
如果减少一个指针域,就退化成单链表,如果只能对链表的首位进行插入或删除操作,就演变成了队列结构,如果对链表的头进行插入或删除操作就退化为栈结构。
2.链表的定义和操作
- 定义
- //不包含数据域的链表
- struct list_head{
- struct list_head *next,*prev;
- }
- //包含数据域的链表
- struct my_list{
- void * mydata;
- struct list_head list;
- };
复制代码- 1. list域隐藏了链表的指针特性
- 1. struct list_head 可以位于结构的任何位置,可以给其任意名字
- 1. 在一个结构中可以有多个list域
复制代码以struct list_head为基本对象,对链表进行插入、删除、合并以及遍历操作。
- 声明和初始化宏
链表结构的建立首先需要定义两个宏定义:
- #define LIST_HEAD_INIT(name){&(name),&{name}}/*仅初始化*/
- #define LIST_HEAD(name)struct_list_head name = LIST_HEAD_INIT(name)/*声明并初始化*/
复制代码如果要申明并初始化自己的链表头的mylist_head,则直接调用LIST_HEAD:
调用之后,mylist_head的next、prev指针都初始化为指向自己,这样就有了一个空链表,如何判断链表是否为空,就是让头指针的next指向自己。
- 链表中增加节点
list.h中增加节点的函数为:
- static inline void list_add();
- static inline void list_add_tail();
复制代码在内核代码中,函数名前加两个下划线表示内部函数,第一个函数的具体代码如下:
- static inline void _list_add(struct list_head *new,
- struct list_head *prev,
- struct list_head *next)
- {
- next->prev = new;
- new->next = next;
- new->prev =prev;
- prev->next = new;
- }
复制代码调用这个内部函数已分别在链表头和尾增加节点:
- static inline void list_add(struct list_head * new,struct list_head * head)
- {
- _list_add(new,head,head->next);
- }
复制代码该函数向指定链表的head结点后插入new结点,因为链表是循环的,而通常没有首位结点的概念,所以可以将任意结点传递给head。但如果传递最后一个元素给head,则该函数可以实现一个栈。
- static inline void list_tail(struct list_head *new,struct list_head *head)
- {
- _list_add(new,head->prev,head);
- }
复制代码该函数向指定链表的head结点前插入new结点。和list_add()函数类似,因链表是环形的,且可以将任何结点传递给head。但如果传递第一个元素给head,则该函数可以用老实现一个队列。
在函数名前加static inline关键字说明,static加在函数前,表示这个函数是静态函数,对该函数作用域的限制,指该函数的作用域仅限于本文件。static具有隐藏作用。关键字inline加载函数前说明该函数对编译程序是可见的,编译程序在调用这个函数时就立即展开该函数。所以关键字inline必须与函数定义体放在一起才能使函数成为内联。inline函数一般放在头文件中。
- 链表遍历
list.h中定义如何遍历链表的宏:
- #define list_for_each(pos,head)
- for(pos = (head)->next;pos!=(head);
- pos = pos->next)
复制代码这种遍历仅仅是找到一个个结点在链表中的偏移位置pos,如图1.7。
为了获取pos结点的起始地址,从而引用结点中的域,在list.h中定义list_entry()宏:
- #define list_entry(ptr,type,member)
- ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
复制代码指针ptr指向结构体type中的成员member;通过指针ptr,返回结构体type的起始地址,也就是list_entry返回指向type类型的指针,如图1.8。
((unsigned long)(&((type *)0)->member)把0地址转化为type结构的指针,然后获取该结构中member域的指针,也就是获得member在type结构中的偏移量。其中(char *)(ptr)求出的是ptr的绝对地址,二者相减,于是获得type类型结构体的起始地址,如图1.9。
- 链表的应用
Linux内核模块,用以创建、增加、删除和遍历一个双向链表
- #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/slab.h>
- #include <linux/list.h>
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("XIYOU");
- #define N 10 //链表结点数
- static inline void list_tail(struct list_head *new,struct list_head *head)
- {
- _list_add(new,head->prev,head);
- }
- static inline void _list_add(struct list_head *new,
- struct list_head *prev,
- struct list_head *next)
- {
- next->prev = new;
- new->next = next;
- new->prev =prev;
- prev->next = new;
- }
- struct list_head{
- int *next;
- int *prev;
- }
- struct numlist{
- int num; //数据
- srtuct list_head list; //指向双链表前后结点的指针
- };
- struct int _init doublelist_init(void)
- {
- //初始化结点
- struct numlist *listnode; //每次申请链表结点时所用的指针
- struct list_head *pos;
- struct numlist *p;
- int i;
-
- printk("doublelist is starting...\n");
- INIT_LIST_HEAD(&numble.list);
-
- //建立N个结点,依次加入到链表当中
- for(i = 0;i<N;i++)
- {
- //kmalloc()在内核空间申请内存,类似于malloc()
- listnode = (struct numlist *)kmalloc(sizeof(struct numlist),GFP_KERNEL);
- listnode -> num = i+1;
- list_add_tail(&listnode -> list,&numhead.list);
- printk("Node %d has added to the doublelist...\n",i+1);
- }
- //遍历链表
- i=1;
- list_for_each(pos,&numhead.list)
- {
- p = list_entry(pos,struct numlist,list);
- printk("Node %d`s data: %d\n",i,p->num);
- i++;
- }
- return 0;
- }
- ststic void _exit doublelist_exit(void)
- {
- struct list_head *pos,*n;
- struct numlist *p;
- int i;
-
- //依次删除N个结点
- i= 1;
- list_for_each_safe(pos,n,&numhead.list)
- {
- list_del(pos);
- p = list_entry(pos,struct numlist,list);
- kfree(p);
- printk("Node %d has renmove from the doublelist...\n",i++);
- }
- printk("doublelist is exiting..\n");
- }
- module_init(doublelist_init);
- module_exit(doublelist_exit);
- static inline void _list_del(struct list_head * prev,struct list_head *next)
- {
- next->prev = prev;
- prev->next = next;
- }
- static inline void list_del(struct list_head *entry)
- {
- _list_del(entry->prev,entry->next);
- entry->next = LIST_POISON1;
- entry->prev = LIST_POISON2;
- }
复制代码当执行删除操作时,被删除的结点两个指针被指向一个固定的位置(LIST_POISON1和LIST_POISON2是内核空间的两个地址)。而list_for_each(pos,head)中的pos指针在遍历过程中向后移动,即pos = pos->next,如果执行了list_del()操作,pos将指向这个固定位置的next,prev,而此时的next,prev没有任何指向,必然出错。
而list_for_each_safe(p,n,head)宏解决了该问题:
- #define list_for_each_safe(pos,n,head)
- for(pos = (head) -> next,n=pos->next;pos!=(head);
- pos = n,n = pos->next)
复制代码采用同类型的指针n来暂存将要被删除的结点指针pos,从而使得删除操作不影响pos指针。