實驗環境
Ubuntu18.04.1,linux內核:5.6.3,VMware15,gcc編譯器,內存:3G,CPU:4×2=8核,外存:100G。
Ubuntu18.04.1.iso鏡像網盤下載地址:
鏈接:
https://pan.baidu.com/s/1CYVD-2ZlU2lfYD5bisBIvA
提取碼:1ork
設計目的
Linux 是開源操作系統,用戶可以根據自身系統需要裁剪、修改內核,定製出功能更加
合適、運行效率更高的系統,因此,編譯 Linux 內核是進行內核開發的必要基本功。
在系統中根據需要添加新的系統調用是修改內核的一種常用手段,通過本次實驗,讀
者應理解 Linux 系統處理系統調用的流程以及增加系統調用的方法。
內容要求
1、添加一個系統調用,實現對指定進程的 nice 值的修改或讀取功能,並返回進程最
新的 nice 值及優先級 prio。建議調用原型爲:
int mysetnice(pid_t pid, int flag, int nicevalue,void __user * prio, void __user * nice);
參數含義:
- pid:進程 ID。
- flag:若值爲 0,表示讀取 nice 值;若值爲 1,表示修改 nice 值。
- nicevalue:爲指定進程設置的新 nice 值。
- prio、nice:指向進程當前優先級 prio 及 nice 值。
- 返回值:系統調用成功時返回 0,失敗時返回錯誤碼 EFAULT。
2、寫一個簡單的應用程序測試1、中添加的系統調用。
3、若程序中調用了 Linux 的內核函數,要求深入閱讀相關函數源碼。
nice與prio的關係
Linux 系統調用基本概念
系統調用的實質是調用內核函數,於內核態中運行。Linux 系統中用戶(或封裝例程)通過執行一條訪管指令“int $0x80”來調用系統調用,該指令會產生一個訪管中斷,從而讓系統暫停當前進程的執行,而轉去執行系統調用處理程序,通過用戶態傳入的系統調用號從系統調用表中找到相應服務例程的入口並執行,完成後返回。
1、系統調用號與系統調用表
Linux 系統提供了多達幾百種的系統調用,爲了唯一的標識每一個系統調用,Linux 爲每個系統調用都設置了一個唯一的編號,稱爲系統調用號;同時每個系統調用需要一個服務例程完成其具體功能。Linux 內核中設置了一張系統調用表,用於關聯繫統調用號及其相對應的服務例程入口地址,定義在./arch/x86/entry/syscalls/syscall_64.tbl 文件中(32 位系統是 syscall_32.tbl),每個系統調用佔一表項,比如大家比較熟悉的幾個系統調用的調用號如表所示:
系統調用號非常關鍵,一旦分配就不能再有任何變更,否則之前編譯好的應用程序就會崩潰。在 x86 中,系統調用號是通過 eax 寄存器傳遞給內核的。在陷人內核之前,先將系統調用號存入eax 中,這樣系統調用處理程序一旦運行,就可以從 eax 中得到調用號。
2、系統調用服務例程
每個系統調用都對應一個內核服務例程來實現該系統調用的具體功能,其命名格式都是以“sys_”開頭,如 sys_read 等,其代碼實現通常存放在./kernel/sys.c 文件中。服務例程的原型聲明則是在./include/linux/syscalls.h 中,通常都有固定的格式,如 sys_open 的原型爲:asmlinkage long sys_open(const char __user *filename,int flags, int mode);
其中“asmlinkage”是一個必須的限定詞,用於通知編譯器僅從堆棧中提取該函數的參
數,而不是從寄存器中,因爲在執行服務例程之前系統已經將通過寄存器傳遞過來的參數
值壓入內核堆棧了。在新版本的內核中,引入了宏“SYSCALL_DEFINEN(sname)”對服務例程
原型進行封裝,其中的“N”是該系統調用所需要參數的個數,如上述 sys_open 調用
在./kernel/sys.c 文件中的實現格式爲:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)
3、系統調用參數傳遞
與普通函數一樣,系統調用通常也需要輸入/輸出參數。在 x86 上,Linux 通過 6 個寄存器來傳入參數值,其中 eax 傳遞系統調用號,後面 5 個寄存器 ebx, ecx, edx, esi 和 edi 按照順序存放前五個參數,需要六個或六個以上參數的情況不多見,此時,應該用一個單獨的寄存器存放指向所有這些參數在用戶空間地址的指針。服務例程的返回值通過 eax 寄存器傳遞,這是在執行 rutern 指令時由 C 編譯器自動完成的。
當系統調用執行成功時,將返回服務例程的返回值,通常是 0。但如果執行失敗,爲防止和正常的返回值混淆,系統調用並不直接返回錯誤碼,而是將錯誤碼放入一個名爲 errno的全局變量中,通常是一個負值,通過調用 perror()庫函數,可以把 errno 翻譯成用戶可以理解的錯誤信息描述。
4.、系統調用參數驗證
系統調用必須仔細檢查用戶傳入的參數是否合法有效,比如與進程相關的調用必須檢查用戶提供的 PID 等是否有效。最重要的是要檢查用戶提供的指針是否有效,以防止用戶進程非法訪問數據。內核提供了兩個函數來完成必須的檢查以及內核空間與用戶空間之間數據的來回拷貝:
copy_to_user()和 copy_from_user(),對 2.6.24 內核,在/include/asm-x86/uaccess_64.h 文件中
申 明 原型 , 在 ./arch/x86/lib/usercopy_32.c 文 件 中實 現 函數 ; 對於 內核 4.12 ,定 義 在./include/linux/uaccess.h 文件中。
下載內核源碼
linux內核:5.6.3獲取:
- 百度網盤下載
鏈接:
https://pan.baidu.com/s/1nQV6jmwoMkCUy51EFgtVPw
提取碼:fo1u - CSDN下載地址
linux-5.6.3 - 官網下載,Linux 的內核源代碼是完全公開的
https://www.kernel.org/ - 特別提醒:
我是在Windows上下載好的,然後通過SSH上傳到了Linux的用戶指定的Downloads目錄下。
如果,不知道SSH上傳,可看我之前的博客(當然也可以通過Ubuntu自帶的瀏覽器,官網下載):
Xshell 連接 Ubuntu 教程(超詳細),並解決二個常見問題(一直連不上、root用戶拒絕密碼)
解壓下載好的linux5.6.3內核
首先切換到 root 用戶(後面所有操作都必須以 root 用戶進行),將下載的新內核壓縮文
件複製到/home 或其他比較空閒的目錄中,然後進入壓縮文件所在子目錄,分兩步解壓縮:
(1)xz -d linux-5.6.3.tar.xz,大概執行 1 分鐘左右,中間沒有任何信息顯示。
(2)tar –xvf linux-5.6.3.tar
下圖,是我解壓縮好的文件,以後操作都是以root身份進入linux-5.6.3文件進行操作。
注意:由於編譯過程中會生成很多臨時文件,所以要確保壓縮文件所在子目錄有足夠的
空閒空間,最好能有 15-20GB。筆者在建立虛擬機時預留了 100GB 磁盤空間。
Linux 添加系統調用的步驟:
注意:必須以 root身份才能完成下述操作。進入解壓後的linux-5.6.3文件中
1、分配系統調用號,修改系統調用表
vim ./arch/x86/entry/syscalls/syscall_64.tbl
下圖,是我添加的439調用號,64和common不用深究,調用號,在末尾累加。
2、 申明系統調用服務例程原型
Linux 系統調用服務例程的原型聲明在文件 linux-5.6.3/include/linux/syscalls.h 中,可在
文件末尾添加如圖 :
vim ./include/linux/syscalls.h
asmlinkage long sys_zynorlsyscall(pid_t pid, int flag, int nicevalue);
3.實現系統調用服務例程
下面爲新調用 zynorlsyscall 編寫服務例程 sys_zynorlsyscall,通常添加在 sys.c 文件中(我放到了尾端,用快捷鍵:shift+g),其完整路徑爲:linux-5.6.3/kernel/sys.c:
注意:SYSCALL_DEFINE3字段中的3不要隨意更該。
vim ./kernel/sys.c
SYSCALL_DEFINE3(zynorlsyscall, pid_t, pid, int, flag, int, nicevalue)
{
int error = 0;
struct task_struct *p;
for(p = &init_task;(p = next_task(p)) != &init_task;){
if(p->pid == pid){
if(flag == 0){
printk("this process's nice value is %d\n",task_nice(p));
printk("this process's prio value is %d\n",task_prio(p));
}else if(flag == 1){
set_user_nice(p,nicevalue);
printk("this process's nice now changed to %d\n",task_nice(p));
printk("this process's prio now changed to %d\n",task_prio(p));
}else{
error = -EFAULT;
}
}
}
return error;
}
重新編譯內核
上面三個步驟已經完成添加一個新系統調用的所有工作,但是要讓這個系統調用真正
在內核中運行起來,還需要重新編譯內核。
作爲自由軟件,Linux 內核版本不斷更新,新內核會修訂舊內核的 bug,並增加若干新
特性,如支持更多的硬件、具備更好的系統管理能力、運行速度更快、更穩定等。用戶若
想使用這些新特性。而我們是想添加新的系統調用,需要編譯內核。
1、預先安裝一些輔助工具包,如果沒有編譯過程中會出錯:
apt-get install libncurses5-dev
apt-get install libssl-dev
apt-get install bison
apt-get install flex
apt-get install pkg-config
2、清除殘留的.config 和.o 文件
在開始完全重新編譯之前,需要清除殘留的.config 和.o 文件,後續如果編譯過程中出
現錯誤,再次開始完全重新編譯之前也需要如此清理。方法是進入 linux-5.6.3 子目錄,
執行以下命令:
#make mrproper
3、 配置內核
make menuconfig
此命令將打開如圖 所示配置對話框,對於每一個配置選項,用戶可以回答"y"、“m"或"n”:中"y"表示將相應特性的支持或設備驅動程序編譯進內核;"m"表示將相應特性的支持或設備驅動程序編譯成可加載模塊,在需要時,可由系統或用戶自行加入到內核中去;"n"表示內核不提供相特性或驅動程序的支持。一般採用默認值即可:選擇保存配置信息,文件名採用默認的.config,然後選擇退出。
4、 編譯內核,生成啓動映像文件
內核配置完成後,執行 make 命令開始編譯內核,如果編譯成功,則生成 Linux 啓動映
像文件 bzImage(位於./arch/x86_64/boot/bzImage):
make
可使用 make -j2(雙核 CPU)或 make -j4(4 核 CPU)來加快編譯速度。編譯過程中,可能會出現一些錯誤,通常都是因爲缺少某個庫,一般根據相應的錯誤提示,安裝相應的包即可,然後重新編譯。
我的虛擬機是8核的,所以我用1如下命令:
make -j8
我大約用了不到20分鐘,如果你是一核的,用make去編譯,會花費幾個小時。
5、 編譯模塊
make modules -j8
第一次編譯模塊需要時間比較長,但沒有make長,我用了不到10分鐘。(同樣-j8會加快速度)
6、安裝內核
安裝模塊:make modules_install
安裝內核:make install
7、配置 grub 引導程序
update-grub2
該命令會自動修改 grub
8、重啓系統
reboot
9、將使用新內核啓動 linux。啓動完成後進入終端查看內核版本,如圖 :
uname -a
編寫用戶態程序測試新系統調用
1、編寫測試C程序:
#include <linux/unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a,b,c;
printf("Please input pid:");
scanf("%d",&a);
printf("Please select operation \"0\" as read \"1\" as change:");
scanf("%d",&b);
if (b == 0)
{
c=0;
}
else{
printf("Please input nice value you want:");
scanf("%d",&c);
}
syscall(439,a,b,c);
printf("Program execute successfully! Please use dmesg to see the log.\n");
return 0;
}
2、編譯程序
gcc -o zynorlsyscall zynorlsyscall.c
3、執行程序:
./zynorlsyscall
4、程序說明:
syscall(439,a,b,c);
函數 syscall()宏調用新添加的系統調用,它是 Linux 提供給用戶態程序直接調用系統調用的一種方法,其格式爲:int syscall(int number, …);其中 number 是系統調用號,number 後面應順序接上該系統調用的所有參數。
5、執行用戶態程序,使用top指令隨意找一個進程
top
如圖:
我任意選擇了PID爲360的進程(這裏有一個錯誤,我是想截圖360,截成了370,因爲,在我截圖的時候,畫面在動。但他們的nice都爲-20):
6、運行截圖:
7、編譯該程序並運行後,使用 dmesg -T 命令查看輸出內容,如圖 :
dmesg -T
8、然後再轉到top查看360pid的nice值,已經變成了-19,說明我們測試成功了。但,因爲,top運行出來的圖,一直在動,不方便截圖。這裏就沒有圖片演示了。
系統調用所涉及的源碼講解
以下代碼是在Linux內核官方文檔copy而來,註釋是自己的理解,個人能力有限,有錯誤的地方還請大家見諒。
set_user_nice源碼鏈接:https://elixir.bootlin.com/linux/v5.6.3/source/kernel/sched/core.c#L4503
在講代碼之前,我先上一些預備知識:
-
在由task_struct定義的進程控制塊PCB定義中,每個進程有4個優先級成員變量,如下:
prio(動態優先級)、normal_prio(歸一化優先級)、static_priority(靜態優先級1)和rt_priority(實時優先級)。
關於進程優先級的更多解讀:
Linux調度器 ——進程優先級 -
我們知道,在調度時使用了prio,其數值0對應最高優先級,99爲最低實時優先級。Prio和normal_prio 數值越大優先級越小,而rt_priority的數值越大優先級越大。這就是爲什麼有人說實時進程優先級數值越小優先級越高,也有人說實時進程優先級數值越大優先級越高的原因。
-
對於普通進程而言,進程優先級就是nice value,從-20(優先級最高)~19(優先級最低),通過修改nice value可以改變普通進程獲取cpu資源的比例。nice只針對普通進程有效,對實時進程無效,nice可以被user設置其相關的優先級(-20~19)來輔助進程調度。nice,並不直接影響實際的調度策略(prio動態優先級)。
具體可參考我的博客:
進程優先級,進程nice值和%nice的解釋 -
task_truct中的policy成員記錄了該線程的調度策略,有DL(deadline)調度器 與 RT(rt_priority)調度器。
具體可參考我的博客:
實時調度器之 DL(deadline)調度器 與 RT(rt_priority)調度器 詳解
set_user_nice
/*
* 1、從上面的系統調用,和set_user_nice命名可以看出,該方法是設置user即普通進程的優先級,說白了
* 不是實時進程。
* 2、NICE是反應進程優先級的一個值,範圍是[-19,+20],一共有40個值,值越小,代表的優先級更高;反 * 之,值越大,優先級越低。
* 3、task_struct結構體是Linux下的進程控制塊PCB,PCB裏包含着一個進程的所有信息。*p可以簡單地理* 解爲就是我們要set的進程。
*/
void set_user_nice(struct task_struct *p, long nice)
{
bool queued, running;//布爾值 就緒隊列狀態或運行狀態
int old_prio;//原優先級
struct rq_flags rf;//運行隊列的標誌
struct rq *rq;//運行隊列
//如果傳入的要設置的nice與原本的nice相同就不用再次設置,直接return,退出該函數。
//如果傳入的要設置nice不在規定的[-19,+20]的範圍內也直接return.
if (task_nice(p) == nice || nice < MIN_NICE || nice > MAX_NICE)
return;
/*
* We have to be careful, if called from sys_setpriority(),
* the task might be in the middle of scheduling on another CPU.
*/
rq = task_rq_lock(p, &rf);//上鎖,進程在訪問一個臨界資源時,要有加鎖操作。
update_rq_clock(rq);//更新運行隊列時鐘
/*
* The RT priorities are set via sched_setscheduler(), but we still
* allow the 'normal' nice value to be set - but as expected
* it wont have any effect on scheduling until the task is
* SCHED_DEADLINE, SCHED_FIFO or SCHED_RR:
*/
/*
* 1、task_has_policy(),task_has_rt_policy() 分別判斷當前進程 p 是不是實時進程
*(DL(deadline)+ RT(rt_priorit));在PCB中的policy成員記錄了該線程的調度策略。
* 2、如果是實時進程,nice就會不起作用,也不會實際改變調度器行爲,但這裏還是將傳入的nice賦
* 值給了p->static_prio靜態優先級。
* 3、但,值得注意的是,如果是實時進程就沒有了入隊和出隊操作(queued,running),直接釋放
* 之前佔用的鎖,然後return。
*/
if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
p->static_prio = NICE_TO_PRIO(nice);
goto out_unlock;
}
//執行到這裏,只可能是普通進程了
queued = task_on_rq_queued(p);//排隊狀態
running = task_current(rq, p);//運行狀態
if (queued)
dequeue_task(rq, p, DEQUEUE_SAVE | DEQUEUE_NOCLOCK);//dequeue_task 出隊
if (running)
put_prev_task(rq, p);//用另一個進程代替當前運行的進程之前調用,將切換出去的進程插入到隊尾
p->static_prio = NICE_TO_PRIO(nice);//將進程的靜態優先級賦值
/*
* 負責根據非實時進程類型極其靜態優先級計算符合權重(cpu資源),CFS調度器在計算進程的虛擬運
* 行時間或者調度延遲時都是使用的權重(cpu資源)。
* 當系統中沒有實時進程或者deadline進程的時候,所有的runnable的進程一起來瓜分cpu資源,以此
* 不同的進程分享一個特定比例的cpu資源,我們稱之load weight。不同的nice value對應不同的
* cpu load weight,因此,當更改nice value的時候,也必須通過set_load_weight來更新該進程
* 的cpu load weight。除了load weight,該線程的動態優先級也需要更新,這是通過p->prio =
* effective_prio;來完成的
*/
set_load_weight(p, true);
/*
* task struct中的prio成員表示了該線程的動態優先級,也就是調度器在進行調度時候使用的那個優
* 先級。動態優先級在運行時可以被修改,例如在處理優先級翻轉問題的時候,系統可能會臨時調升一個
* 普通進程的優先級。
* */
//通過effective_prio()更新進程p的動態優先級(prio).
old_prio = p->prio;
p->prio = effective_prio(p);
//
if (queued)
enqueue_task(rq, p, ENQUEUE_RESTORE | ENQUEUE_NOCLOCK);//enqueue_task:入隊
if (running)
set_next_task(rq, p);
/*
* If the task increased its priority or is running and
* lowered its priority, then reschedule its CPU:
*/
//如上文英文註釋代碼所說的,在當前進程的調度策略發生變化時調用,那麼需要調用這個函數改變CPU
p->sched_class->prio_changed(rq, p, old_prio);
out_unlock:
task_rq_unlock(rq, p, &rf);//解鎖
}
effective_prio
static int effective_prio(struct task_struct *p)
{
p->normal_prio = normal_prio(p);
/*
* If we are RT tasks or we were boosted to RT priority,
* keep the priority unchanged. Otherwise, update priority
* to the normal priority:
* 如果是實時進程,keep the priority unchanged,直接return該進程的動態優先級.
* 如不是,更新動態優先級爲normal_prio(歸一化優先級),那什麼是歸一化優先級請看下段代碼
*/
if (!rt_prio(p->prio))
return p->normal_prio;
return p->prio;
}
normal_prio
static inline int normal_prio(struct task_struct *p)
{
int prio;
/*
* MAX_RT_PRIO-1是99,MAX_RT_PRIO-1 - p->rt_priority則翻轉了實時進程的scheduling
* priority,最高優先級是0,最低是98。
* */
//如果該優先級是基於deadline調度策略實時優先級,那麼動態優先級是最大的DL-1=-1
//因此,deadline的進程比RT進程和normal進程的優先級還要高
if (task_has_dl_policy(p))
prio = MAX_DL_PRIO-1;
else if (task_has_rt_policy(p))
//MAX_RT_PRIO-1 - p->rt_priority ; 由這條語句可以看出Prio與rt_priority的優先級與數值的關係成反比
prio = MAX_RT_PRIO-1 - p->rt_priority;
else
//prio和normal_prio是等似的,都與rt_priority的優先級與數值的關係正好相反。
prio = __normal_prio(p);
return prio;
}
對於普通進程,set_user_nice()順下來:
p->static_prio = NICE_TO_PRIO(nice);
p->prio=p->normal_prio;
最後我們也可以得出一個這樣的結論:
對於非實時進程的prio和normal_prio 一直保持相同
參考於: