OS 實驗二 | Linux 內核模塊編程
設計目的
Linux 提供的模塊機制能動態擴充 linux 功能而無需重新編譯內核,已經廣泛應用在 linux內核的許多功能的實現中。在本實驗中將學習模塊的基本概念、原理及實現技術,然後利用內核模塊編程訪問進程的基本信息,從而加深對進程概念的理解、對模塊編程技術的掌握。
內容要求
(1) 設計一個模塊,要求列出系統中所有內核線程的程序名、PID 號、進程狀態及進程優先順序。
(2) 設計一個帶參數的模塊,其參數為某個進程的 PID 號,該模塊的功能是列出該進程的家族信息,包括父進程、兄弟進程和子進程的程序名、PID 號。
(3) 請根據自身情況,進一步閱讀分析程序中用到的相關內核函數的源碼實現。
實驗內容一
(1)查找內核進程和系統進程的區別
Linux 進程描述符 task_struct 結構定義在/usr/src/linux4.4.19/include/linux/sched.h 中,包
含眾多的成員項,部分與本實驗相關的成員項有:
#define TASK_RUNNING 0#define TASK_INTERRUPTIBLE 1#define TASK_UNINTERRUPTIBLE 2#define __TASK_STOPPED 4#define __TASK_TRACED 8/* in tsk->exit_state */#define EXIT_DEAD 16#define EXIT_ZOMBIE 32/* in tsk->state again */#define TASK_DEAD 64#define TASK_WAKEKILL 128#define TASK_WAKING 256struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack; int prio, static_prio, normal_prio; unsigned int policy; struct list_head tasks; /*線程組長鏈表,是節點*/ struct mm_struct *mm, *active_mm; pid_t pid; pid_t tgid; struct task_struct __rcu *real_parent; /* real parent process */ struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */ struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parents children list */ cputime_t utime, stime, utimescaled, stimescaled; char comm[TASK_COMM_LEN]; /* executable name excluding path */ ......};
Linux 的進程和輕量級進程/線程均有相應的 task_struct 結構和 PID 號,而 POSIX 要求同一組線程有統一的 PID,為此 linux 引入了 tgid(thread group identifer),tgid 實際上是線程組第一個線程的 pid 值,該線程稱為線程組長。對於普通進程,其 pid 與 tgid 是相同的。此外,linux 系統的進程包含一種特殊的類型——內核線程(kernel thread),完成內核的一些特定任務,並始終在核心態運行,沒有用戶態地址空間,其 task_struct 結構的 mm 成員項為NULL,如交換進程。
於是可以根據核心態的 mm 值為 NULL 的特徵,來內核進程和用戶進程進行區別。
關於更多 mm 值的介紹可以查閱:Linux進程管理之task_struct結構體(下) - CSDN博客
可以編寫出相應的識別代碼如下:
if(p->mm == NULL){} //內核線程的mm成員為空
(2)需要列印的數據的查找方法
其實在 task_struct 結構體中,已經記錄了列印的程序名、PID 號、進程狀態及進程優先順序。我們可以查閱如下的兩篇文檔,來對task_struct結構體進行更好的理解。
- Linux進程管理之task_struct結構體(上) - CSDN博客
- Linux進程管理之task_struct結構體(下) - CSDN博客
對應的變數名定義如下:
- 相應的程序名 :
char comm[TASK_COMM_LEN];
- 進程標識符(PID):
pid_t pid;
- 進程狀態
volatile long state;
- 常規靜態優先順序
int normal_prio;
於是我們可以編寫代碼如下:
if(p->mm == NULL) { printk(KERN_ALERT"%s %d %d %d
",p->comm,p->pid, p->state,p->normal_prio);}
(3)遍歷所有的進程。
這裡我們可以利用內核的線程組長鏈表實現,每個線程組長通過 task_struct 結構的 tasks 成員加入該鏈表。 Linux 內核提供了宏 for_each_process()訪問該鏈表中的每個進程:
#define for_each_process(p) for (p = &init_task ; (p = next_task(p)) != &init_task ; )
宏 for_each_process 定義在/include/linux/sched.h 文件中。
(4)編寫模塊代碼
#include <linux/init.h>#include <linux/module.h>#include <linux/kernel.h>#include <linux/sched.h>// 初始化函數static int hello_init(void){ struct task_struct *p; printk(KERN_ALERT"名稱 進程號 狀態 優先順序 "); for_each_process(p) { if(p->mm == NULL){ //內核線程的mm成員為空 printk(KERN_ALERT"%s %d %ld %d
",p->comm,p->pid, p->state,p->normal_prio); } } return 0;}// 清理函數static void hello_exit(void){ printk(KERN_ALERT"goodbye!
");}// 函數註冊module_init(hello_init); module_exit(hello_exit); // 模塊許可申明MODULE_LICENSE("GPL");
(5)編寫 Makefile
obj-m:=module1.o KDIR:= /lib/modules/$(shell uname -r)/buildPWD:= $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean
(6)編譯模塊
# make
(7)添加模塊
# insmod module1.ko
(8)輸出日誌
# dmesg
輸出結果如下:
(輸出的結果太長,這裡只截取一部分顯示給大家看:)
(知乎的圖片壓縮效果太慘不忍睹了,大家意思意思,下面貼一下這部分的輸出結果。)
[ 80.324201] 名稱 進程號 狀態 優先順序 [ 80.324207] kthreadd 2 1 120[ 80.324218] kworker/0:0 3 1 120[ 80.324222] kworker/0:0H 4 1 100[ 80.324227] kworker/u16:0 5 1 120[ 80.324231] ksoftirqd/0 6 1 120[ 80.324235] rcu_sched 7 1 120[ 80.324240] rcu_bh 8 1 120[ 80.324244] migration/0 9 1 0[ 80.324248] lru-add-drain 10 1 100[ 80.324253] watchdog/0 11 1 0[ 80.324257] cpuhp/0 12 1 120......
(9)結果驗證
為了比較我們輸出的內核進程結果時候正確,我們可以使用如下命令顯示 root 用戶的全部信息,比較是否與我們自己列印出的結果一致。
$ ps -u root
該命令的輸出結果如下:
我們可以通過對比發現我們的操作是正確的。
關於更多 ps 進程查看器的操作可以查閱:Linux Tools Quick Tutorial
實驗內容二
(1)了解 linux 進程家族的組織情況
所以的進程都是 PID 為 1 的 init 進程的後代,內核在系統啟動的最後階段創建 init 進程,並由其完成後續啟動工作。系統中的每個進程必有一個父進程,相應的,每個進程也可以擁有零個或多個子進程。父進程(task_struct 中的 parent 成員)相同的所有進程稱為兄弟進程,由 task_struct 中的 sibling 成員鏈接成父進程的 children 鏈表,它們間的關係如圖所示:
對子進程鏈表和兄弟進程鏈表的訪問,都可以通過宏 list_for_each()和 list_entry()以及list_for_each_entry()來實現。對於指定的 pid,可以通過函數 pid_task()和 find_vpid()(或者find_get_pid())配合使用找到其相應的 task_atruct 結構,位於 linux/kernel/pid.c 文件中:
struct task_struct *result = NULL;if (pid) { struct hlist_node *first; first = rcu_dereference_check(pid->tasks[type].first, rcu_read_lock_held() || lockdep_tasklist_lock_is_held()); if (first) { result = hlist_entry(first, struct task_struct, pids[(type)].node); } return result;}struct pid *find_vpid(int nr) { return find_pid_ns(nr, current->nsproxy->pid_ns);}struct pid *find_get_pid(pid_t nr) { struct pid *pid; pid = get_pid(find_vpid(nr)); rcu_read_unlock(); return pid;}
(2)尋找指定進程的 PID
p = pid_task(find_vpid(pid), PIDTYPE_PID);
更多關於進程 ID 的操作可以查閱:Linux進程ID號--Linux進程的管理與調度(三) - CSDN博客
(3)查找當前進程的父進程
每個進程必有一個父進程。
if(p->parent == NULL) { printk("No Parent
");}else { printk("Parent: %d %s
", p->parent->pid, p->parent->comm);}
(4)查找當前進程的兄弟進程
list_for_each(pp, &p->parent->children){ psibling = list_entry(pp, struct task_struct, sibling); printk("sibling %d %s
", psibling->pid, psibling->comm);}
(5)查找當前進程的子進程
list_for_each(pp, &p->children){ psibling = list_entry(pp, struct task_struct, sibling); printk("children %d %s
", psibling->pid, psibling->comm);}
(6)編寫模塊代碼
#include<linux/init.h>#include<linux/module.h>#include<linux/kernel.h>#include <linux/sched.h>#include <linux/moduleparam.h>static pid_t pid=1;module_param(pid,int,0644);static int hello_init(void){ struct task_struct *p; struct list_head *pp; struct task_struct *psibling; // 當前進程的 PID p = pid_task(find_vpid(pid), PIDTYPE_PID); printk("me: %d %s
", p->pid, p->comm); // 父進程 if(p->parent == NULL) { printk("No Parent
"); } else { printk("Parent: %d %s
", p->parent->pid, p->parent->comm); } // 兄弟進程 list_for_each(pp, &p->parent->children) { psibling = list_entry(pp, struct task_struct, sibling); printk("sibling %d %s
", psibling->pid, psibling->comm); } // 子進程 list_for_each(pp, &p->children) { psibling = list_entry(pp, struct task_struct, sibling); printk("children %d %s
", psibling->pid, psibling->comm); } return 0;}static void hello_exit(void){ printk(KERN_ALERT"goodbye!
");}module_init(hello_init);module_exit(hello_exit); MODULE_LICENSE("GPL");
(7)編寫 Makefile
obj-m:=module2.o KDIR:= /lib/modules/$(shell uname -r)/buildPWD:= $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean
(8)編譯模塊
# make
(9)添加模塊
# insmod module2.ko
(10)輸出日誌
# dmesg
輸出結果如下:
(11)結果驗證
對於父進程 PID 的確認我們可以使用如下命令顯示所有進程信息,其中 PPID 顯示的就是其上級父程序的ID。
$ ps -ef
顯示結果如下:
對於子進程的查詢,我們可以使用 pstree -p 命令來完成。但是為了同時顯示父進程、兄弟進程和子進程,我們可以查詢當前進程的父進程的進程樹。
$ pstree -p 0
得到結果如下:
由於進程樹太大,這裡只截取了一部分顯示當前進程子進程樹的部分。顯示結果與我們自己列印出的結果一致。
關於更多 pstree 查看進程樹的操作可以查閱 pstree - Linux命令大全 。
關於本次實驗的全部代碼可以查閱:shaonianruntu/OSlab
推薦閱讀:
- Linux進程管理之task_struct結構體(上) - CSDN博客
- Linux進程管理之task_struct結構體(下) - CSDN博客
- Linux3.0.6內核task_struct注釋 - CSDN博客
- Linux Tools Quick Tutorial
- Linux進程描述符task_struct結構體詳解--Linux進程的管理與調度(一) - CSDN博客
- Linux的命名空間詳解--Linux進程的管理與調度(二) - CSDN博客
- Linux進程ID號--Linux進程的管理與調度(三) - CSDN博客
- pstree - Linux命令大全
- linux內核模塊編程之列出系統中所有內核線程的程序名、PID號、進程狀態和優先順序 - 後台君
- linux內核根據pid,列出家族信息中的程序名和PID號 - 後台君
推薦閱讀:
※《深入理解計算機系統》配套實驗:Shell Lab
※win7系統安裝教程
※諾基亞有哪些操作系統?
※如何減少電腦中病毒的幾率?
※蘋果操作系統適用什麼人群?