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命令实现。
- 例:insmod hellomod.ko
- lsmod命令可以用来检查模块是否正确插入到了内核中。
- 模块的输出由prink()来产生,该函数默认打印系统文件
- /var/log/messages
- 快速浏览该信息可以输入:
- tail/var/log/messages(打印日志文件的最后10行内容)
- 使用rmmod命令,加上insmod中看到的模块名,可以从内核中移除该模块(还可以看到退出时显示的信息)
- 当编译好模块,要将编译好的模块插入到内核中时可以使用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:
LIST_HEAD(mylist_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指针。