编写简单的内核模块以及实现linux简单链表功能

编写简单的内核模块以及实现linux简单链表功能

什么是模块

模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。

编写模块的过程

  • 模块源代码mmh.c文件
#include <linux/module.h>                    //任何模块程序的编写都要包含这个头文件,它包含了对模块的结构定义以及版本控制
#include <linux/init.h>                      //包含了宏_init(指定初始化函数)和_exit(指定清除函数)
#include <linux/kernel.h>                    //这个头文件包含了常用的内核函数

static int __init mmh_init(void)             //__init函数将mmh_init()标记为初始化函数,在模块装载到内核时调用mmh_init()
{
    printk(KERN_EMERG"Hello Kernel!\n");     //printk()函数由内核定义的,功能与C库中的printf()相近,它把打印的信息发到终端或系统日志
    printk(KERN_ALERT"Hello Kernel!\n");
    printk(KERN_CRIT"Hello Kernel!\n");
    printk(KERN_ERR"Hello Kernel!\n");
    printk(KERN_WARNING"Hello Kernel!\n");
    printk(KERN_NOTICE"Hello Kernel!\n");
    printk(KERN_INFO"Hello Kernel!\n");
    printk(KERN_DEBUG"Hello Kernel!\n");
    return 0;                                //返回非0表示模块初始化失败,无法载入
}

static void __exit mmh_cleanup(void)          //mmh_cleanup()函数是模块退出和清理函数
{
    printk(KERN_ALART"Goodbye!Leaving kernel space...\n");   //在模块卸载前,将这句话打印到日志
}

module_init(mmh_init);                        //模块的加载函数,当通过insmod命令加载模块时,模块的加载函数会自动被内核执行,完成本模块相关初始化工作
module_exit(mmh_cleanup);                     //模块的卸载函数,当通过rmmod命令卸载模块时,模块的卸载函数会自动被内核执行,注销由模块提供的所有功能
MODULE_LICENSE("GPL");                        //模块许可证(GNU general public license),如果不声明,模块加载时将收到内核被污染的警告

  • 上面这个简单的内核模块打印的内容其实是printk的打印等级,源代码在kernel.h中。
#define	KERN_EMERG	"<0>"	/* system is unusable			*/
#define	KERN_ALERT	"<1>"	/* action must be taken immediately	*/
#define	KERN_CRIT	"<2>"	/* critical conditions			*/
#define	KERN_ERR	"<3>"	/* error conditions			*/
#define	KERN_WARNING	"<4>"	/* warning conditions			*/
#define	KERN_NOTICE	"<5>"	/* normal but significant condition	*/
#define	KERN_INFO	"<6>"	/* informational			*/
#define	KERN_DEBUG	"<7>"	/* debug-level messages			*/
  • Makefile文件
obj-m:=mmh.o                                                         //产生mmh模块的目标文件,在这句话中.o的文件名要与编译.c文件名一致 
CURRENT_PATH:=$(shell pwd)                                           //将模块源码路径保存在CURRENT_PATH中  
LINUX_KERNEL:=$(shell uname -r)                                      //将当前内核版本保存在LINUX_KERNEL中
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules       //编译模块    
clean:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean         //清理
  • 编译
    1、在Makefile以及mmh.c.所在目录下,直接make,成功后查看当前目录下有无mmh.ko文件产生,有则内核模块生成成功。
    在这里插入图片描述
    2、使用insmod命令把这个内核模块程序加载到内核中运行,lsmod命令查看内核模块程序是否在内核中正确运行
    在这里插入图片描述3、查看此内核模块程序的打印信息,dmesg命令可查看系统日志打印信息
    在这里插入图片描述4、使用rmmod命令把之前加载的模块卸载,仍然用lsmod命令查看是否成功卸载
    在这里插入图片描述5、重复上面第三步
    在这里插入图片描述

关于链表

我们要知道,数组可以存放一组相同数据类型的数据,系统将为数组分配一片连续的内存空间。数组定义时必须指出数组的大小,但在实际应用中,大小有时很难准确给出,太大会造成系统浪费,太小又无法满足要求,要是能根据实际需要动态的申请空间,需要时就申请,使用完毕就归还给系统就好了。所以说可以利用C语言中动态申请空间malloc函数和释放空间free函数,并通过链表来实现,这里介绍一下malloc函数和free函数(使用这两个函数前要包含#include<stdlib.h>这个系统头文件)

  • malloc函数
    函数格式为:
void *malloc(unsigned int size);

功能:内存的动态存储区分配一块大小为“size”的连续内存空间,若内存有满足大小要求的内存空间,系统则返回一个指向该内存空间首地址的指针,若无法分配,则返回一个NULL指针。因此在调用这个函数时,可以通过判断返回的指针是否为NULL来判断申请空间是否成功。

  • free函数
    函数格式为:
void free(p);

功能:将指针p所指向的内存空间释放,归还给系统。系统的内存空间是有限的,不可能无限的分配下去,编写的程序应尽量节约系统资源,用完后及时释放空间,便于其他变量或程序使用,提高系统内存空间的利用率。

  • 链表概述
    所以说,用malloc函数和free函数可以实现动态的申请和释放空间,那么如果要动态的申请多个相同大小的空间,就要用链表来把这些空间动态的串联起来。链表由结点组成,每个结点代表一块申请的内存空间,每个结点都包含数据部分和指针部分,数据部分存放需要处理的数据,可以是一个变量,也可以是多个变量,而指针部分用来存放其他结点的地址,如此以来通过指针就可以将各个结点连接在一起。
  • 链表的定义
    链表的结点包含的成员不一定是相同数据类型的,所以说定义结点时应使用结构体类型,定义如下:
struct node
{
   int data;                    //数据部分
   struct node *next;           //指针部分
};
  • 链表的建立
#include <stdio.h>
#include <stdlib.h>

struct node               //定义结构体类型,类型名为node
{
   int data;
   struct node *next;
};

struct node* creat()      //creat函数实现构建链表,函数返回值类型为结构体指针型,将创建的表头结点head返回给主调函数
{
   struct node *head,*p,*s;
   int num,mark;
   if(head=(struct node*)malloc(sizeof(struct node)))==NULL)   //head作为链表的表头结点,调用malloc函数申请node类型大小的空间,返回值赋给head,若返回值为NULL,表示申请失败,执行exit函数结束程序
   {
      printf("内存分配失败:\n");
      exit(0);
   }
   head->data=0;           //表头结点内容为0
   head->next=NULL;        //表头指针置空
   
   p=head;                 //指针p指向head指针
   printf("请输入mark的值,申请空间输入1,不申请输入0:");
   scanf("%d",&mark);
   
   while(mark==1)          //循环语句判断mark为1时表示用户需要申请空间
   {
      if(s=(struct node*)malloc(sizeof(struct node)))==NULL)
       {
          printf("内存分配失败");
          exit(0);
       }
       printf("请输入结点的内容:");
       scanf("%d",&num);
       s->data=num;        //为结点赋值
       s->next=NULL;       //结点s的指针置空
       
       p->next=s;          //将s与前一个指针相连
       p=s;                //p指向该结点
       printf("请输入mark的值,申请空间置1,不申请置0:");
       scanf("%d",&mark);
   }
   return head;            //返回表头结点指针
}

int main(void)
{
   struct node *h,*p,*s;
   h=creat();
   printf("链表各结点的值依次是:\n");
   p=h;
   while(p->next!=NULL)    //遍历
   {
      s=p->next;
      printf("the number is ==>%d\n",s->data);
      p=s;
   }
}

在这里插入图片描述

关于linux内核的链表

演化

  • 单链表
    单链表的结点定义如下:
struct node
{
   int data;
   struct node *next;
};
  • 双链表
    相比于单链表,双链表还多了一个指向前一个元素的指针,所以双链表可以同时前后连接,定义如下:
struct node
{
   int data;
   struct node *prev;
   struct node *next;
};
  • 循环链表
    通常链表的最后一个元素不再有下一个元素,所以将链尾元素的后指针置为NULL,以说明它是最后一个元素,但在循环链表中,链表的末尾元素指向链表的首元素,并且在循环双链表中链表的首结点的前驱结点指向尾结点。

    所以说循环双链表提供了最大的灵活性,所以它就成为了linux内核的标准链表。

  • 关于list_head
    Linux内核链表的核心思想是:在用户自定义的结构A中声明list_head类型的成员p,这样每个结构类型为A的变量a中,都拥有同样的成员p,如下:

struct A
{
   int property;
   struct list_head p;   
}

其中,list_head结构类型定义如下:

struct list_head
{
   struct list_head *next,*prev;
}

list_head拥有两个指针成员,其类型都为list_head,分别为前驱指针prev和后驱指针next。

假设:

(1)多个结构类型为A的变量a1…an,其list_head结构类型的成员为p1…pn

(2)一个list_head结构类型的变量head,代表头节点

使:

(1)head.next= p1 ; head.prev = pn

(2) p1.prev = head,p1.next = p2;

(3)p2.prev= p1 , p2.next = p3;

​ …

(n)pn.prev= pn-1 , pn.next = head

以上,则构成了一个循环链表。

因p是嵌入到a中的,p与a的地址偏移量可知,又因为head的地址可知,所以每个结构类型为A的链表节点a1…an的地址也是可以计算出的,从而可实现链表的遍历,在此基础上,则可以实现链表的各种操作。

  • 头文件
#include <linux/init.h>               //包含了宏_init(指定初始化函数)和_exit(指定清除函数)
#include <linux/kernel.h>             //这个头文件包含了常用的内核函数
#include <linux/module.h>             //任何模块程序的编写都要包含这个头文件,它包含了对模块的结构定义以及版本控制
#include <linux/slab.h>               //包含了kmalloc等内存分配函数的定义
#include <linux/list.h>               //linux内核通用链表
  • 建立一个结构体
struct mmh_list
{
   int num;
   struct list_head list;
};
  • 建立一个头结点
struct mmh_list headnode;
  • 初始化头结点
INIT_LIST_HEAD(&headnode.list);
  • 插入结点
for(i=0;i<3;i++)
{
   listnode=(struct mmh_list *)kmalloc(sizeof(struct mmh_list),GFP_KERNEL);
   listnode->num=i+1;
   list_add_tail(&listnode->list,&head.list);
}

其中kmalloc是个功能强大且高速的工具,所分配到的内存在物理内存中连续且保持原有的数据。
原型是:

#include <linux/slab.h> 
static inline void *kmalloc(size_t size, int flags)    

其中static表示这个函数为静态函数,即对函数作用域的一种限制,也就是说函数的作用域仅限于本文件,inline就是编译程序在调用这个函数时就立即展开这个函数。
而GFP_KERNEL 是最常用的标志,意思是这个分配代表运行在内核空间进程运行。内核正常分配内存。当空闲内存较少时,可能进入休眠来等待一个页面。当前进程休眠时,内核会采取适当的动作来获取空闲页。所以使用 GFP_KERNEL 来分配内存的函数必须是可重入,且不能在原子上下文中运行。

  • 遍历链表
    关于遍历链表,list.h中定义了如下遍历链表的宏:
#define list_for_each(pos, head) \
	for (pos = (head)->next, prefetch(pos->next); pos != (head); \
        	pos = pos->next, prefetch(pos->next))

但这种遍历仅仅是找到一个个结点在链表中的偏移位置pos,关键是要通过pos获得结点的起始地址,从来可以引用结点中的域,于是list.h中定义了list_entry()宏:

#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

这里的container_of,它定义在linux/kernel.h中,其源码如下:

#define container_of(ptr, type, member) ({			\
        const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
        (type *)( (char *)__mptr - offsetof(type,member) );})

这里简明的说一下:

const typeof( ((type *)0)->member ) *__mptr = (ptr);	      	

typeof是GNU C对标准C的扩展,它的作用是根据变量获取变量类型。typeof获取了type结构体的member成员的变量类型然后定义了一个指向该变量类型的指针_mptr,并将实际结构体该成员变量的指针的值赋给 _mptr,从而临时变量 _mptr中保存了type结构体成员member在内存中分配的地址值。

(type *)( (char *)__mptr - offsetof(type,member) );

这块代码是利用_mptr的地址,减去type结构体成员变量member相对于结构体首地址的偏移地址,得到的自然是结构体首地址,即该结构体指针。

  • 删除结点
i=1;
list_for_each_safe(pos,n,&headnode.list)
{
   list_del(pos);
   p=list_entry(pos,struct mmh_list,list);
   kfree(p);
}

这里不调用list_for_each()宏而调用list_for_each_safe()进行删除前的遍历。
删除函数的源码如下:

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_for_each(pos,head)中的pos指针在遍历过程中向后移动,即pos=pos->next,如果执行了list_del()操作,pos将指向这个固定位置的next,prev,而此时的next,prev没有任何指向,必然出错,而list_for_each_safe(pos,n,head)宏解决了上面的问题:

#define list_for_each_safe(pos, n, head) \
	for (pos = (head)->next, n = pos->next; pos != (head); \
		pos = n, n = pos->next)

它采用一个同pos同样类型的指针n来暂存将要被删除的结点指针pos,从而使得删除操作不影响pos指针。

linux内核链表的演示

  • list.c源文件
    在这里插入图片描述
  • Makfile文件
    在这里插入图片描述
  • 剩下的部分
    make
    sudo insmod list.ko
    dmesg
    在这里插入图片描述
    sudo rmmod list
    dmesg
    在这里插入图片描述
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
一、linux内核链表 1、普通链表的数据区域的局限性 之前定义数据区域时直接int data,我们认为我们的链表需要存储的是一个int类型的数。但是实际上现实编程链接的节点不可能这么简单,而是多种多样的。 一般实际项目链表,节点存储的数据其实是一个结构体,这个结构体包含若干的成员,这些成员加起来构成了我们的节点数据区域。 2、一般性解决思路:即把数据区封装为一个结构体 (1)因为链表实际解决的问题是多种多样的,所以内部数据区域的结构体构成也是多种多样的。 这样也导致了不同程序当链表总体构成是多种多样的。 我们无法通过一套泛性的、普遍适用的操作函数来访问所有的链表,意味着我们设计一个链表就得写一套链表的操作函数(节点创建、插入、删除、遍历……)。 (2)实际上深层次分析会发现 不同的链表虽然这些方法不能通用需要单独写,但是实际上内部的思路和方法是相同的,只是函数的局部地区有不同。 实际上链表操作是相同的,而涉及到数据区域的操作就有不同 (3)问题 能不能有一种办法把所有链表操作方法里共同的部分提取出来用一套标准方法实现,然后把不同的部分留着让具体链表实现者自己去处理。 3、内核链表的设计思路 (1)内核链表实现一个纯链表的封装,以及纯链表的各种操作函数 纯链表就是没有数据区域,只有前后向指针; 各种操作函数是节点创建、插入、删除、遍历。 这个纯链表本身自己没有任何用处,它的用法是给我们具体链表作为核心来调用。 4、list.h文件简介 (1)内核核心纯链表实现在include/linux/list.h文件 (2)list.h就是一个纯链表的完整封装,包含节点定义和各种链表操作方法。 二、内核链表的基本算法和使用简介 1、内核链表的节点创建、删除、遍历等 2、内核链表的使用实践 (1)问题:内核链表只有纯链表,没有数据区域,怎么使用? 使用方法是将内核链表作为将来整个数据结构的结构体的一个成员内嵌进去。类似于公司收购,实现被收购公司的功能。 这里面要借助container_of宏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值