linux内核源代码位于/usr/src/linux目录下,主要分布结构是:


文件描述:

在这里插入图片描述

  • include/子目录包含的是建立内核代码时所需的大部分包含文件
  • init/子目录包含了内核的初始化代码,内核起始点
  • arch/子目录包含了Linux支持的所有硬件结构的内核代码
  • drivers/子目录包含了内核中所有的设备驱动程序,例如字符设备、块设备、scsi设备驱动程序等
  • fs/子目录包含了所有文件系统的代码
  • net/子目录包含了内核中关于网络的代码
  • mm/子目录包含了所有的内存管理代码
  • ipc/子目录包含了进程间通信的代码
  • kernel/子目录包含了主内核代码

Linux内核模块编程

  1. 编写模块
#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. 说明:
    1.
    • module.h头文件中包含对模块的结构定义以及模块的版本控制,任何模块程序的编写都要包含这个头文件;
    • 头文件kernel.h包含了常用的内核函数;
    • 而头文件init.h包含了宏_init和_exit,宏_init告诉编译器程序相关的函数和变量仅用于初始化,编译程序将标有_init的所有代码存储到特殊的内存段中,初始化结束后就释放这段内存。
    1. 函数lkp_init()是模块初始化函数,函数lkp_cleanp()是模块的推出和清理函数。
    2. printk()函数由内核定义,与C库中的printf()类似。字符串<1>是输出的级别,表示立即在终端输出。
    3. module_init()和cleanup_exit()是模块编程中最基本也是最必须的两个函数。module_init()向内核注册模块提供新功能,cleanup_exit()注销由模块所提供的功能。
    4. 最后一句是告诉内核该模块具有GNU公开许可证。
  2. 编译模块

假定程序名为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。

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

  1. 定义
//不包含数据域的链表
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为基本对象,对链表进行插入、删除、合并以及遍历操作。

  1. 声明和初始化宏

链表结构的建立首先需要定义两个宏定义:

#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指向自己。

  1. 链表中增加节点

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函数一般放在头文件中。

  1. 链表遍历

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。
在这里插入图片描述

  1. 链表的应用

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指针。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐