實驗概述
1.設計目的
Linux 提供的模塊機制能動態擴充 Linux 功能而無需重新編譯內核,已經廣泛應用在
linux 內核的許多功能的實現中。在本實驗中將學習模塊的基本概念、原理及實現技術,然
後利用內核模塊編程訪問進程的基本信息,加深對進程概念的理解,掌握基本的模塊編程
技術
2.內容要求
(1)設計一個模塊,要求列出系統中所有內核線程的程序名、PID、進程狀態、進程優先級、父進程的 PID。
(2)設計一個帶參數的模塊,其參數爲某個進程的 PID 號,模塊的功能是列出該進程的家族信息,包括父進程、兄弟進程和子進程的程序名、PID 號、進程狀態。
(3)請根據自身情況,進一步閱讀分析程序中用到的相關內核函數的源碼實現。
3、模塊基本概念
Linux 內核是單體式結構,相對於微內核結構而言,其運行效率高,但系統的可維護性及可擴展性較差。爲此,Linux 提供了內核模塊(module)機制,它不僅可以彌補單體式內核相對於微內核的一些不足,而且不影響系統性能。內核模塊的全稱是動態可加載內核模塊(Loadable Kernel Module,KLM),簡稱爲模塊。
模塊是一個目標文件,能完成某種獨立的功能,但其自身不是一個獨立的進程,不能單獨運行,可以動態載入內核,使其成爲內核代碼的一部分,與其他內核代碼的地位完全相同。當不需要某模塊功能時,可以動態卸載。實際上,Linux 中大多數設備驅動程序或文件系統都以模塊方式實現,因爲它們數目繁多,體積龐大,不適合直接編譯在內核中,而是通過模塊機制,需要時臨時加載。使用模塊機制的另一個好處是,修改模塊代碼後只需重新編譯和加載模塊,不必重新編譯內核和引導系統,降低了系統功能的更新難度。
通過看內核編譯能夠深刻體會到這一點。
內核編譯可看:https://zynorl.blog.csdn.net/article/details/105754952
一個模塊通常由一組函數和數據結構組成,用來實現某種功能,如實現一種文件系統、一個驅動程序或其他內核上層的功能。模塊自身不是一個獨立的進程,當前進程運行過程中調用到模塊代碼時,可以認爲該段代碼就代表當前進程在覈心態運行。
將 Makefile 和module01.c module02.c 放在一個文件夾內
一、module01.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/sched/signal.h>
// 初始化函數
static int hello_init(void)
{
struct task_struct *p;
printk("-----------------------------------------------------------------------");
printk(KERN_ALERT"名稱 進程 狀態 優先級 父進程");
for_each_process(p)
{
if(p->mm == NULL){ //內核線程的mm成員爲空
printk(KERN_ALERT"%s\t%d\t%ld\t%d\n",p->comm,p->pid,p->state,p->normal_prio,p->parent->pid);
}
}
return 0;
}
// 清理函數
static void hello_exit(void)
{
printk(KERN_ALERT"goodbye!\n");
}
// 函數註冊
module_init(hello_init);
module_exit(hello_exit);
// 模塊許可申明
MODULE_LICENSE("GPL");
二、module02.c
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include <linux/sched.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("GPL");
static pid_t pid;
module_param(pid,int,0644);
static int hello_init(void)
{
struct task_struct *p;
struct list_head *pp;
struct task_struct *psibling;
printk("--------------------------------------------------------");
//當前進程的 PID
p = pid_task(find_vpid(pid), PIDTYPE_PID);
printk("me:%s %d %ld\n",p->comm, p->pid, p->state);
// 父進程
if(p->parent == NULL) {
printk("No Parent\n");
}
else {
printk("Parent:%s %d %ld\n",p->parent->comm, p->parent->pid, p->parent->state);
}
// 兄弟進程
list_for_each(pp, &p->parent->children)
{
psibling = list_entry(pp, struct task_struct, sibling);
printk("sibling %s %d %ld \n",psibling->comm, psibling->pid, psibling->state);
}
// 子進程
list_for_each(pp, &p->children)
{
psibling = list_entry(pp, struct task_struct, sibling);
printk("children %s %d %ld \n", psibling->comm,psibling->pid, psibling->state);
}
return 0;
三、Makefile
obj-m:=module02.o
KDIR:= /lib/modules/$(shell uname -r)/build
PWD:= $(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
操作相關指令
以下命令除 make 命令外,其他都應以 root 用戶執行:
- 模塊編譯命令 make:
命令格式:make
不帶參數的make命令將默認當前目錄下名爲makefile或者Makefile的文件爲描述文件。 - 加載模塊命令 insmod :
insmod 命令把需要載入的模塊以目標代碼的形式加載到內核中,將自動調用
init_module 宏。其格式爲:
insmod 模塊名.ko
insmod 模塊名.ko pid=223 // 帶參數的模塊編程 本文中對應於module02.c
- 查看已加載模塊命令 lsmod:
列出當前所有已載入系統的模塊信息,包括模塊名、大小、其他模塊的引用計數等信息。
命令格式: lsmod
可以配合 grep 來查看指定模塊是否已經加載:lsmod | grep 模塊名 - 查看指定模塊信息命令 modinfo
查看指定模塊的詳細信息,如模塊名、作者、許可證、參數等信息。
命令格式: modinfo 模塊名.ko - 卸載模塊命令 rmmod:
卸載已經載入內核的指定模塊,命令格式爲:rmmod 模塊名.ko - 查看模塊輸出控制檯日誌
dmesg | tail -100 來輸出“dmesg”命令的最後 100 行日誌。
代碼初步解讀:
module01.c 中通過這段代碼:
module_init(hello_init);
module_exit(hello_exit);
該模塊被載入內核時會向系統日誌文件中寫入“hello,world”;當被卸載時,也會向系統日誌中寫入“goodbye”。
頭文件聲明:
第 1、2 行是模塊編程的必需頭文件。
- init.h 包含了模塊初始化和清理函數的定義。
- module.h 包含了大量加載模塊所需要的函數和符號的定義;
- 如果模塊在加載時允許用戶傳遞參數,模 塊還應該包含moduleparam.h 頭文件。
模塊許可申明:
Linux 內核從 2.4.10 版本內核開始,模塊必須通過MODULE_LICENSE 宏聲明此模塊的許可證,否則在加載此模塊時,會收到內核被污染 “kernel tainted” 的警告。
從 linux/module.h 文件中可以看到,被內核接受的有意義的許可證有 “GPL”,“GPL v2”,“GPL and additional rights”,“Dual BSD/GPL”,“Dual MPL/GPL”,“Proprietary”,其中“GPL” 表示這是 GNU General Public License 的任意版本,其他許可證大家可以查閱資料進一步瞭解。
MODULE_LICENSE 宏聲明可以寫在模塊的任何地方(但必須在函數外面),不過慣例是寫在模塊最後。
初始化與清理函數的註冊:
內核模塊程序中沒有 main 函數,每個模塊必須定義兩個函數:一個函數用來初始化(init),主要完成模塊註冊和申請資源,該函數返回 0,表示初始化成功,其他值表示失敗;另一個函數用來退(exit),主要完成註銷和釋放資源。
Linux 調用宏module_init 和 module_exit 來註冊這兩個函數,module_init 宏標記的函數在加載模塊時調 用,module_exit 宏標記的函數在卸載模塊時調用。
需要注意的是,初始化與清理函數必須在宏module_init 和 module_exit 使用前定義,否則會出現編譯
錯誤。
初始化函數通常定義爲:
static int __init init_func(void)
{
//初始化代碼
}
module_init(init_func);
一般情況下,初始化函數應當申明爲 static,以便它們不會在特定文件之外可見。如果
該函數只是在初始化使用一次,可在聲明語句中加__init 標識,則模塊在加載後會丟棄這個
初始化函數,釋放其內存空間。
清理函數通常定義爲:
static void __exit exit_func(void)
{
//清理代碼
}
module_exit(exit_func); 清理函數沒有返回值,因此被聲明爲 void。聲明語句中的__exit 的含義與初始化函數中的__init 類似,不再重述。
一個基本的內核模塊只要包含上述三個部分就可以正常工作了。
內核模塊組成
模塊組成 | 是否可選 |
---|---|
頭文件: #include<linux/init.h #include<linux/module.h | 必選 |
許可聲明 MODULE_LICENSE(“Dual BSD/GPL”) | 必選 |
加載函數 static int __init hello_init(void) | 必選 |
卸載函數 static void __exit hello_exit(void) | 必選 |
模塊參數 module_param(name,type,perm) | 必選 |
模塊導出符號 EXPORT_SYMBOL(符號名) | 可選 |
模塊作者等信息 MODULE_AUTHOR(“作者名”) | 可選 |
代碼深度解析與數據結構:
struct list_head 雙向循環鏈表詳解
鏈表對每位寫過程序的同學都再熟悉不過了。無非是對鏈表的創建,初始化,插入,刪除,遍歷等操作。但您是否想過,如果針對每一種數據結構都實現一套對鏈表操作的服務原語,是否太浪費時間和精力了。實際上在Linux內核2.4以後,內核開發者對鏈表的結構實現了一個統一的接口,可以利用這些接口實現鏈表,而不用去考慮數據結構的差異。你的興趣是否來了?那就讓我們一睹爲快:
下圖爲鏈表數據結構的定義(include/linux/types.h):
list_head 結構包含兩個list_head結構的指針 *next ,*prev ,咋一看這定義,似乎很普通,其實偉大常常孕育在平凡之中。
我們一般會這樣構造鏈表:
struct list_node{
TYPE data; //鏈表中的數據域
struct list_node *next, *prev;
};
這樣我們把數據嵌入到鏈表節點中之後的示意圖爲:
而,內核開發者寫的結構算法是將鏈表的前後指針所組成的list_head 結構體嵌入到list_node 這整個數據結構中。
struct list_node{
TyPE data;
struct list_head list; //定義一個list_head的節點
};
示意圖爲:
可以看出,鏈表的操作是通過訪問爲一個list_head 來操作的。
在這種鏈表中,所有的鏈表基本操作都是針對list_head 數據結構進行,而不是針對包含list_head的list_node 數據結構。無論無論什麼數據,鏈表操作都得到了統一。
那麼現在碰到一個問題,因爲所有鏈表操作涉及到的指針都是指向list_head數據結構的,而不是包含的list_node數據結構。那麼怎樣從list_head的地址得到包含其list_node數據結構的地址呢?
我們來看linux 內核中(include/linux/list.h)的list_entry(ptr, type, member)這個宏:
- list_entry—獲取該條目的結構體
- @ptr: 是指向list_head 類型鏈表的指針
- @type:一個包含list_head 結構的結構體類型。
- @member:結構體中list_head的名稱
把0 地址轉化爲type類型的指針, 然後獲取該結構中member成員的名稱(sibling-data)。如果data 現在在0 地址上, 那麼由上圖代碼段
offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
(type *)((char *)__mptr - offsetof(type, member))
可知,就是計算0 地址到list_head 的偏移量 (相對長度),說白了就是數據域data (sibling-data) 在此結構中佔多長的空間。這樣如果我們有一個絕對的地址ptr (list_head類型) 那麼 :
ptr 絕對地址 - data 相對地址 = 包含 list_head 結構的task_struct結構體的絕對地址。
試想,如果我們知道鏈表的list_head 的地址,(因爲list_head爲鏈表的節點,我們當然可以知道他的地址) 就可以找到包含這個節點的數據結構的地址,找到這個數據結構的地址順理成章的就可以訪問這個結構中的每個元素了。
list_for_each詳解
Linux系統中的每個進程都有一個父進程(init進程除外);每個進程還有0個或多個子進程。在進程描述符中parent指針指向其父進程,還有一個名爲children的子進程鏈表(父進程task_struct中的children相當於鏈表的表頭)。
task_struct源代碼鏈接:
https://elixir.bootlin.com/linux/v5.6.3/source/include/linux/sched.h#L629
下圖, 爲task_struct 源代碼中的chilren 代碼截圖。可以看出,父進程task_struct中的children相當於鏈表(list_head)的表頭。
而我們可以使用list_for_each(/include/linux/list.h)來依次遍歷訪問子進程:
list_for_each 源代碼鏈接:
https://elixir.bootlin.com/linux/v5.6.3/source/include/linux/list.h#L552
源碼截圖:
- list_for_each——遍歷一個列表
- @pos: &struct list_head用作循環遊標。
- @head:列表的頭部。
現在我們再看下面的module02.c代碼片段截圖:
struct list_head *pp;
struct task_struct *psibling;
// 兄弟進程
list_for_each(pp, &p->parent->children)
{
psibling = list_entry(pp, struct task_struct, sibling);
printk("sibling %s %d %ld \n",psibling->comm, psibling->pid, psibling->state);
}
list_for_each 其實就是一個for 循環, for() 實現的就是一個children 鏈表的遍歷。
首先需要說明,task_struct 指針指向其某個子進程的進程描述符task_struct中的childre的地址而非指向某個子進程的地址,也就是說子進程鏈表中存放的僅僅是各個task_struct成員children的地址。
那麼問題來了,由children的地址如何取到task_struct的地址呢, 它是由list_entry 宏來實現的,關於list_entry 這個宏前面已經講到。
算法總結:
看到這裏,你是否已經恍然大悟,linux 利用list_for_each 這個宏通過雙向循環鏈表這個數據結構算法這個方式找到相對於父進程的children這個進程,但這只是找到了children進程 (task_struct) 中的children成員的地址,並沒有找到children本身的地址。所以就需要 list_entry 這個宏來調節這個“地址差”。
而,list_entry 裏面又是通過container_of() 函數進行偏移(
container_of()思路爲先求出結構體成員member(即children)在結構體(即task_struct)中的偏移量,然後再根據member的地址(即ptr)來求出結構體(即task_struct)的地址。
這裏 ((type *)0)->member,他將地址0強制轉換爲type類型的指針,然後再指向成員member,此時((type )0)->member的地址即爲member成員相對於結構體的位移。),來最終實現的。
如果對你有幫助,麻煩能給個贊嗎,b( ̄▽ ̄)d。
參考於:
http://blog.sina.cn/dpool/blog/s/blog_4cd5d2bb0101525j.html
http://blog.sina.cn/dpool/blog/s/blog_861912cd0100xty9.html