*內核版本linux-5.25.0
第一章 概述
1.1 Linux操作系統概述
user->application->os->hardware
os目標:1.提高資源利用率 2.方便用戶的使用
Linux系統的整體結構:
Linux內核的設計理念:機制與策略分離 ( Linux內核提供的是機制 )
系統調用機制->隔離變化
Linux學習:
入門:Linux內核設計與實現
深入理解:深入理解Linux內核
動手:Linux設備驅動程序
1.2 內核結構&模塊編程
- 1.單核(可維護性較差)與微內核(效率較低)
- 2.內核源代碼目錄結構:mm,fs等
- 3.可加載的Linux內核模塊LKM
- 內核模塊編程入門:
printf->printk(輸出到日誌文件中)
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
/*
* 模塊的初始化函數
* __init 爲作用於初始化的修飾符
* /
static int __init lkp_init(void){
printk("Hello,world!from the kernel sapce...\n");
}
/*
* 模塊的退出函數
* __exit爲作用於退出的修飾符
* /
static void __exit lkp_exit(void){
printk("Goodbye,world!form the kernel space...\n");
}
module_init(lkp_init);
module_exit(lkp_exit);
/*
* 模塊的許可證聲明GPL
*/
MODULE_LICENSE("GPL");
- makefile文件編寫:
obj-m:=hello.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
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
-
查看printk打印信息,日誌信息存放在 /proc/kmsg文件中,通過dmesg命令查看。
-
模塊插入內核:sudo insmod + 模塊名.ko
-
查看當前系統中的模塊:lsmod
-
通過demsg查看系統日誌:
-
模塊從內核卸載:sudo rmmod + 模塊名.ko
dmesg查看系統日誌:
1.3 內核中的雙鏈表結構
- 雙鏈表可以轉換爲:單鏈表,隊列,棧,二叉樹。
- 雙鏈表的定義存在於源代碼目錄下的/include/linux/types.h中
// 雙鏈表的定義
struct list_head{
struct list_head *next,*prev;
};
- 鏈表的聲明與初始化
內核位置:/include/linux/list.h文件中
其中#define LIST_HEAD_INIT(name) {&(name),&(name)}
宏僅進行鏈表的初始化,而#define LIST_HEAD(name) strcut list_head name = LIST_HEAD_INIT(name)
宏完成了鏈表的聲明與初始化。(其實就是把雙鏈表頭節點中的next指針和prev指針,指向自己的地址) - 判斷空鏈表
即判斷頭節點的next指針 - 在鏈表中增加節點
內核位置:/include/linux/list.h文件中
1.插入節點到指定節點之後:
static inline void list_add(struct list_head *new, struct list_head *head);
2.插入節點到指定節點之前
static inline void list_add_tail();
這兩個函數均調用了
static inline void __list_add(strcut list_head *new, struct list_head *pre, struct list_head *next);
- 遍歷鏈表
內核位置:/include/linux/list.h文件中
#define list_for_each(pos, head)\ for(pos = (head)->next; pos != (head); pos = pos->next)
- 獲得節點的起始地址
內核位置:/include/linux/list.h文件中
#define list_entry(ptr, type, member) container_of(ptr, type, member)
其中container_of(ptr, type, member)
函數用來求member成員所在的type類型的結構體的首地址。具體實現如下:
其中((type *)0)->member
爲member成員在函數體中的相對地址,(char *)__mptr
爲member成員的絕對地址,用絕對地址減去相對地址,得到其所在結構體的首地址。
內核中的hash表結構
哈希表原理:此處略過,可以閱讀數據結構方面的書籍
hash表的數據結構
內核位置/include/linux/types.h
struct hlist_head{
struct hlist_node *first;
};
struct hlist_node{
struct hlist_node *next, **prev;
};
1.那麼爲什麼哈希表中頭節點hlist_head與其他節點hlist_node不同,而沒有采用相同的結構體呢?
由於哈希表中一般採用單散列的形式,並不需要雙鏈表的雙向循環功能,所以Linux內核爲了減少開銷,並沒有用hlist_node來指定哈希表頭結點,而是採用了hlist_head結構,以減少存儲空間的佔用。頭結點的數量與數據的總量在同一個數量級。
2.爲什麼在hlist_node中prev採用了二級指針,而沒有采用單鏈表,或雙向鏈表的形式呢?
(1)若使用單鏈表結構,在插入節點的時候可以採用在哈希表的頭結點之後插入節點,此時時間複雜度爲O(1)。但在刪除節點的時候必須要遍歷鏈表來尋找待刪除節點的前一個節點,此時效率較低。
(2)若prev採用一級指針,則鏈表的形式如下:
- a.向鏈表中插入結點
當往鏈表中插入節點node1的時候,插入方式如下:
my_list.first = node1;
node1->pprev = (struct hlist_node*)&my_hlist;
再繼續插入節點node2,node3,node4的時候,插入的方式:
頭插法:
my_list.first = node[x];
node[x]->pprev = (struct hlist_node*)&my_hlist;
node[x]->next = node[x-1];
尾插法:
node[x]->next = node[x-1]->next;
node[x]->pprev = node[x-1];
- b.從鏈表中刪除結點
刪除node1節點時
(struct hlist_head*)node1->pprev->first = node1->next;
node1->next->pprev = (struct hlist_node*)&my_hlist / node1->pprev;
當刪除node2~4其他節點時,
node[x]->pprev->next = node[x]->next;
node[x]->next->pprev = node[x]->pprev;
從節點的插入與刪除中,我們可以看到若pprev採用一級指針,則第一個節點的插入與刪除操作與其他節點的操作方式是不一樣的,同時還需要進行hlist_dead和hlist_node之間的類型強制轉換。因此Linux內核中爲了統一節點的插入與刪除操作方式,將pprev指針設置爲了二級指針。
Linux內核中將pprev指針設計爲二級指針有如下好處
爲了間接改變表頭中hlist_node類型first指針的值,使用了二級指針,因此在node節點中pprev中保存的爲前一個結點中第一個元素的地址,頭結點中即爲first指針,其餘節點則爲next。
- 因此刪除第一個節點的操作方式如下
*(node1->pprev) = node1->next;
node1->next->pprev = node1->pprev;
- 刪除其餘節點的操作方式如下(以node2節點爲例):
*(node2->pprev) = node2->next;
node2->next->pprev = node2->pprev;
- 插入第一個節點
node1->pprev = &(my_list->first);
node1->next = my_list->first;//初始化時,first指針指向NULL
mylist->first = node1;
- 插入其餘節點(以node2爲例)
頭插法
node2->pprev = &(my_list->first);
node2->next = my_list->first; // 或node1,爲了與插入第一個節點的操作保持一致
my_list->first = node2;
尾插法
node2->pprev = &(node1->next);
node2->next = node1->next;
node1->next = node2;
這樣一來所有節點的刪除和插入操作方式都是完全一樣的。
哈希表中的宏定義
//初始化頭結點
#define HLIST_HEAD_INIT { .first = NULL }
//聲明並初始化頭結點
#define HLIST_HEAD(name) struct hlist_head name = { .first = NULL }
//初始化頭結點
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)
//初始化其他節點
#define INIT_HLIST_NODE(ptr) ((ptr)->next = NULL, (ptr)->pprev = NULL)
- 其中
{ .first = NULL }
這個代碼段的作用是,將hlist_head結構體中的first元素初始化爲0。是根據結構體中的變量名來區分元素的,自己做了以下實驗:
打印結果爲:
注:該方法只能在GNU編譯器下使用,在vs中測試無效。