Linux內核模塊編程以及內核鏈表

在前一次完成了內核的編譯和安裝以及添加了一個很奇怪的系統調用,系統調用不加參數,void可以正常輸出內容,但是加上參數之後就亂了,像是溢出的樣子,可是找不到任何可能出問題的地方,先放着吧,實在不行就用helloworld了。這次是另一個實驗,基於上次編譯內核,添加一個內核模塊,在編譯內核的過程中有一個步驟是安裝模塊,這次我們自己寫一個,然後安裝卸載,並且查看輸出,這個還是相對簡單的。

準備

沒什麼好準備的,環境是

  • Ubuntu 18.04

這次主要的一個難點是寫Makefile,然後就是我直接把那堆英文翻譯拿來解釋得了。

編寫.c文件

在這裏我們需要先編寫一個.c文件,就相當於是我們模塊的主體,要執行什麼樣的功能,直接放代碼吧,如下

File:simple.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
int simple_init(void){
	printk(KERN_INFO "Loading Modules\n");
	return 0;
}
void simple_exit(void){
	printk(KERN_INFO "Removing Modules\n");
}
module_init(simple_init);
module_exit(simple_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple Module");
MODULE_AUTHOR("RBK");

上面就是一個完整的simple.c文件,也就是我們一會要生成模塊的源文件,下面解釋一下里面的東西
頭文件就不用說了。
simple_init()是模塊的入口函數,當模塊在裝載的時候將會執行這個函數,同樣的,simple_exit是模塊的出口函數,當模塊在卸載的時候將會執行這個函數。
入口函數必須返回一個整型,0表示成功,其他任意值表示失敗。出口函數返回值必須爲void,兩者都不能傳入任何參數,使用module_init()和module_exit()兩個函數將我們寫的兩個函數註冊到內核中。
其中的printk()函數也不用說了,之前用過的,第一個KERN_INFO表示這是一個通知信息內容。
最後三行分別表示遵循的軟件證書、模塊描述和作者,在此我們並不需要這三個信息,但是如果真正開發的話這麼寫是一個標準。

編譯

在進行這一步的時候不能單純的手動用 gcc編譯器進行編譯鏈接操作,需要編寫一個Makefile,指定我們需要產生的文件和編譯所需的一些信息,使用起來非常方便,我的文件內容如下

File:Makefile

ifneq ($(KERNELRELEASE),)
obj-m :=simple.o
else
KDIR :=/usr/src/linux-$(shell uname -r)/
PWD:=$(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules
clean:
	rm -rf *.ko *.o *.symvers *.cmd *.cmd.o
endif

保存後退出,執行命令

make

即可進行模塊的編譯,編譯完成之後可以看到有一個simple.ko文件,這就是模塊文件了,這樣編譯步驟就結束了
image
如果代碼有什麼報錯的話,就會在make這一步體現出來。

模塊的裝載和卸載

上面得到了simple.ko文件,使用命令裝載模塊到內核

sudo insmod simple.ko

查看模塊是否裝載成功,可以輸入命令lsmod查看列表中是否有我們的模塊,也可以執行命令

dmesg

這個命令之前也用過,用於查看輸出的調試信息,因爲我們使用的是printk()函數,所以輸出的信息會出現在這裏
看到輸出的調試信息,我們的模塊就裝載完成了。
使用命令也可以卸載模塊

sudo rmmod simple

就可以成功卸載模塊了,當然同樣可以使用dmesg查看我們輸出的調試信息。
不僅是我們自定義的模塊可以進行這樣的裝載和卸載,對弈其他系統模塊也可以這樣操作,比如我之前安裝網卡驅動,就是用上面的卸載模塊命令卸載了系統原有的網卡驅動模塊

鏈表

在內核模塊裏面我們同樣可以使用鏈表這個東西,在內核裏,它提供了一個非常獨特的結構list_head,這個結構只有兩個元素,分別爲prevnext,分別指向上一個結點和下一個結點的list_head,因此建立的鏈表是一個雙向鏈表。
鏈表的建立和C語言相差無幾,思想都是先建立一個頭結點,然後新建一個結點,進行插入,在此也可以進行頭插法和尾插法,先一個一個來看吧。

需要知道的一些知識

內核的一些函數和平時用的C語言是不太一樣的,就比如我們使用的printk()函數,在這裏再說一下其他有關的我們需要使用的函數,在此只進行說明,具體用法看後面的代碼就知道了
在內核的鏈表中,特有的結構list_head可以獲取該節點的所有內容,因此以下操作幾乎都是建立在已知list_head的基礎上的

  • void *kmalloc(size_t size,int flags); 參數分別爲需要分配的內存,字節爲單位,以及內存的類型,這個函數特別之處在於他分配內存是物理上連續的,除此之外還有vmalloc,分配的內存物理上不一定連續
  • kfree() 用於釋放內存,就相當於常用的free()函數,對應的有vfree()函數
  • static LIST_HEAD(Node) 聲明一個list_head類型的變量Node,初始化
  • INIT_LIST_HEAD(&First->list) 初始化Node結點的list(list_head類型)
  • list_add_tail(&first->list,&Node) 將first結點添加在Node結點後
  • list_del(struct list_head *element) 刪除結點element
  • list_for_each_entry(ptr,&birthday_list,list) 遍歷鏈表,具體的一些問題後面會說到

頭文件

使用的頭文件如下,如果文件不正確,不僅你不知道錯哪了,而且他的報錯是建議你換一個函數之類的,會更亂,比如如果沒有引用相應頭文件,他會讓你把kmalloc換成vmalloc等等

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/modules.h>
#include <linux/types.h>
#include <linux/slab.h>

先建立一個結構體

建立一個結構體,方便我們後面的說明,代碼裏也會直接用這個

struct birthday{
	int day;
	int month;
	int year;
	struct list_head list;
}

建立一個頭節點

在內核有兩種方法建立頭節點
像C語言一樣建立一個正常的結點–方式1

struct birthday *birthday_list;
birthday_list=kmalloc(sizeof(*birthday_list),GFP_KERNEL);
INIT_LIST_HEAD(&(birthday_list->list));

單純建立一個list_head結構–方式2

static LIST_HEAD(birthday_list);

頭結點是不放數據的,所以即便是建立了一個birthday,也最好不放數據。
這樣頭結點就好了。

插入結點

順序也是先分配一個結點,賦值,然後添加到鏈表中,一定記得初始化list_head,相關代碼如下所示

struct brithday *person;
person=kmalloc(sizeof(*person),GFP_KERNEL);
person->year=1990;
person->month=12;
person->day=21;
INIT_LIST_HEAD(&person->list);
//如果使用的是方式2建立的頭結點
list_add_tail(&(person->list),&birthday_list);
//如果使用方式1建立頭結點
list_add_tail(&(person->list),&(birthday_list->list));

遍歷鏈表

這個操作是遍歷鏈表操作,其實我還不太清楚能不能單獨直接用list_head去獲取當前結點的數據。而且這個東西看似是個普通函數,其實還要大括號呢,至於遍歷的順序似乎好像還不太正常

struct birthday *p=NULL;
list_for_each_entry(p,&birthday_list,list){
	printk("%d\t%d\t%d\n",p->year,p->month,p->day);
}

刪除結點

刪除結點我暫時也是用遍歷的方法去尋找符合刪除條件的結點,理由如上,我不清楚如何根據當前list_head獲取結點的結構,在此有一點要注意
雖然說是遍歷,但是使用的函數和上面的遍歷有點不太一樣
先來說一說爲什麼吧,然後在示例
我在使用list_for_each_entry()進行遍歷然後刪除特定結點,安裝模塊後會提示段錯誤,核心已轉儲,然後呢,內核提示這種錯誤一般是類似內存溢出之類的問題。
list_for_each()的原型爲

#define list_for_each(pos, head) \
for (pos = (head)->next; prefetch(pos->next), pos != (head); \
pos = pos->next)

由定義可知,list_del(pos)(將pos的前後指針指向undefined state)panic,list_del_init(pos)(將pos前後指針指向自身)導致死循環.–當刪除時,鏈表指針變爲特殊類型,所以報錯了。
list_for_each_safe()的原型爲

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

由定義可知,safe函數首先將pos的後指針緩存到n,處理一個流程後再賦回pos,避免了這種情況的發生。
因此只遍歷鏈表不刪除節點時可以使用前者,若有刪除節點的操作,則要使用後者。
由safe的說明可知,是專門爲刪除節點時準備的:iterate over a list safe against removal of list entry。
其他帶safe的處理也基本源於這個原因。
好了,問題清楚了,下面我們來完成我們的要求,刪除一個結點,我們需要一個變量作爲參數n,保存臨時變量,使用的函數爲list_del(&p)kfree(&p),在刪除結點之後需要釋放該結點,如果用錯了kfree或者list_del,運氣好了報錯,運氣差了關機都管不了

struct birthday *n=NULL,*p=NULL;
list_for_each_entry_safe(p,n,&birthday_list,list){
	if (p->year==1995){
		list_del(&p->list);
		printk("deleted successfullt\n");
		kfree(p);
	}
}

一個栗子

然後寫一段代碼,把這些函數都用上,可以封裝成函數的,可是我就用一次,懶得弄了。來看看具體的效果,代碼如下

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/slab.h>
struct birthday{
	int day;
	int month;
	int year;
	struct list_head list;
};
void list_print(struct list_head *head){
	struct birthday *p=NULL;
	list_for_each_entry(p,head,list){
		printk("%d\n",p->year);
	}
}
void list_insert(struct list_head *head,int year,int month,int day){
	struct birthday *n;
	n=kmalloc(sizeof(*n),GFP_KERNEL);
	n->year=year;
	n->month=month;
	n->day=day;
	INIT_LIST_HEAD(&n->list);
	list_add_tail(&n->list,head);
}
void list_delete_by_year(struct list_head *head,int year){
	struct birthday *n,*p;
	list_for_each_entry_safe(p,n,head,list){
		if (p->year==year){
			list_del(&p->list);
			kfree(p);
		}
	}
	printk("刪除元素(year==2018))成功\n");
}
int simple_init(void)
{
	printk("Loaded Modules\n");
	static LIST_HEAD(head);

	list_insert(&head,2018,11,3);
	printk("初始元素爲:2018\t11\t3\n");
	printk("添加元素:2000\t10\t10\n");
	list_insert(&head,2000,10,10);
	printk("遍歷鏈表中的元素\n");
	list_print(&head);
	printk("刪除year=2018的結點\n");
	list_delete_by_year(&head,2018);
	printk("遍歷鏈表中的元素\n");
	list_print(&head);
	return 0;
}
void simple_exit(void)
{
	printk(KERN_INFO "Removing Modules\n");
}
module_init(simple_init);
module_exit(simple_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple Module");
MODULE_AUTHOR("RBK");

執行下列命令,然後查看輸出的信息

make
sudo insmod simple.ko
dmesg

image

OK,THANKS FOR READING.BYE BYE~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章