簡單整理下上周做的OS的lab1,前半部分主要介紹Linux內核編譯和添加系統調用的流程,后半部分主要簡要探索一下添加的系統調用中所用到的內核函數的源碼。
首先貼一下這次實驗的要求和我的實驗流程圖:


Linux內核編譯流程
實驗環境
我的實驗環境是VMware虛擬機下的Ubuntu 16.04,使用uname -a
命令查看了自己的內核版本是4.15.0,於是確定待編譯的新內核版本是:4.16.1(這個版本應略高於Linux系統的內核版本)。同時,由於在內核編譯過程中會生成較多的臨時文件,如果磁盤空間預留很小,會出現磁盤空間不足的錯誤而導致內核編譯失敗;內存太小則會影響編譯速度。所以,這里建議虛擬機的配置參數:內存2GB以上,磁盤空間40GB以上。由於我在做內核編譯之前的時候就已經安裝了linux虛擬機,所以這里我需要先擴展磁盤內存,具體參考了以下博文: https://blog.csdn.net/ldzm_edu/article/details/78893721 。
下載內核源碼並解壓
在 https://www.kernel.org/ 中找到對應版本的內核源碼並下載(如上文所說,這里我選擇4.16.10,請視Linux系統的具體情況而定):
在Linux系統中切換至root用戶(隨后的所有步驟都應以root用戶進行操作,無法切換的可以嘗試passwd
命令修改root用戶密碼),然后將壓縮文件復制到/home
或其他比較空閑的目錄中,進入壓縮文件所在子目錄,使用以下命令解壓:
xz -d linux-4.16.10.tar.xz
tar -xvf linux-4.16.10.tar
清楚殘留的.config和.o文件
每次完全重新編譯的時候都應從這一步開始,為了防止出現錯誤,我們先安裝所需要的對應包:
apt-get install libncurses5-dev
隨后執行命令:make mrproper
配置內核
執行命令:make menuconfig
,隨后出現諸如以下界面:
我們選擇默認值:
.config
,最后
編譯內核
先安裝所需的包以防編譯時報錯:
=>安裝openssl:apt-get install libssl-dev
=>安裝bison:apt-get install bison
=>安裝flex:apt-get install flex
隨后執行命令:make
,強烈建議使用命令make -j4
或make -j8
來加快編譯速度,這里第一次進行內核編譯的時候需要比較長的時間,我的電腦大概跑了一個多小時。
(在此過程中,我遇到了swap交換區內存不足的情況,具體通過查閱到博文:https://blog.csdn.net/babybabyup/article/details/79815118 解決)
后續操作
編譯內核之后的操作用時就比較少了,先編譯模塊:make modules
隨后安裝模塊:make modules_install
安裝內核:make install
配置grub引導程序:update-grub2
最后重啟系統:reboot
再次使用uname -a
命令查看內核版本是否變成自己編譯的版本,如果已經成功改變,那么就已經好啦:
添加系統調用的流程
接着,我們修改新編譯的內核源碼,添加自己的系統調用,我們先添加一個最簡單的實現在內核中打印信息的系統調用mysyscall
,以root身份進入Linux內核源碼的目錄/linux-4.16.1
下:
修改系統調用表
修改目錄下arch/x86/entry/syscalls/syscall_64.tbl
文件,在文件的最后為mysyscall
分配一個新的系統調用號(用來唯一標識每一個系統調用的編號,服務例程則是內核具體實現系統調用功能的函數,以sys_
的格式命名),每個系統調用在該系統調用表中占一個表項,具體格式為:
<系統調用號><commmon/x32/64><系統調用名><服務例程入口地址>
具體修改如下:
聲明系統調用服務例程原型
修改目錄下include/linux/syscalls.h
文件,服務例程的原型聲明格式為:
asmlinkage long sys_系統調用名(參數)
這里我們只實現了最簡單的打印功能,所以參數為空,在文件尾添加具體如下:
實現系統調用服務例程
修改目錄下文件kernel/sys.c
,實現系統調用的服務例程,新版本的內核中引入了宏SYSCALL_DEFINEN(sname)
對服務例程的原型進行了封裝(為了防止利用漏洞入侵),其中N是系統調用所需參數的個數,sname則是系統調用名+系統調用各參數,中間以,
分割,具體修改如下:
重新編譯內核
接着,我們按照上述步驟重新進行內核編譯。
測試新系統調用
最后,我們編寫用戶態程序來測試系統調用mysyscall
是否添加成功,這里使用宏定義將我們分配的系統調用號333定義為mysyscall
:
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define mysyscall 333
long hello_syscall()
{
return syscall(mysyscall);
}
int main()
{
long result;
result=hello_syscall();
if (result<0)
{
printf("mysyscall failed");
}
else{}
return 0;
}
編譯,生成可執行文件:
使用命令dmesg
查看內核信息,發現成功在內核中打印了信息:
添加指定要求的系統調用API
該實驗要求我們添加的系統調用需要實現:修改和讀取指定進程的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。
經過上述添加系統調用的流程介紹,我們可以發現在整個流程中最為關鍵的是服務例程的實現,隨后是編寫用戶態程序以測試新系統調用,這里直接給出我所使用的兩部分代碼(內核函數的源碼淺析見下一部分):
補充:在實現服務例程的函數里我使用了find_get_pid()函數(注釋的下一行),在下面的源碼淺析中我們可以看到這個函數會調用get_pid(),從而使該pid的引用次數自增,因此,為了保持引用次數的平衡,我們在退出函數的時候需要同時調用put_pid()函數,使pid的引用次數自減並判斷引用次數是否為0,若為0則回收該pid號。(在我的函數中沒有考慮到這一點,存在bug)
實現系統調用服務例程:
SYSCALL_DEFINE5(mysetnice,pid_t, user_pid, int, flag, int, nicevalue, void __user*, prio, void __user*, nice)
{
int current_prio,current_nice;
struct pid* pid;
struct task_struct* pcb;
//---!>
pid=find_get_pid(user_pid);
if(pid==NULL)
{
return EFAULT;
}
else
{
pcb=pid_task(pid,PIDTYPE_PID);
if(flag==1)
{
set_user_nice(pcb,nicevalue);
}
else if(flag != 0)
{
return EFAULT;
}
current_prio = task_prio(pcb);
current_nice = task_nice(pcb);
copy_to_user(prio, ¤t_prio, sizeof(current_prio));
copy_to_user(nice, ¤t_nice, sizeof(current_nice));
}
return 0;
}
測試新系統調用的用戶態程序:
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#define mysetnice 334
#define EFAULT 14
int main()
{
int flag;
int pid,nicevalue=0;
int prio,nice;
int result;
printf("Input 1 to set nice or 0 to print nice of your pid:");
scanf("%d",&flag);
printf("Give me your pid:");
scanf("%d",&pid);
if (flag == 1)
{
printf("Give me your nicevalue:");
scanf("%d",&nicevalue);
}
result=syscall(mysetnice,pid,flag,nicevalue,&prio,&nice);
if (result == EFAULT)
{
printf("Something Wrong!");
exit(0);
}
printf("OK! Now, the prio of %d is %d, the nice is %d\n",pid,prio,nice);
return 0;
}
測試結果具體如下:
部分內核函數淺析
以下源碼可以在 https://elixir.bootlin.com/linux/latest/source 中得到,由於版本越高內核源碼越難懂復雜,這里我以2.6版本為例,由於我剛剛接觸Linux內核,相應知識匱乏,理解較淺顯且可能有誤,望斧正。
首先我們分析一下實驗要求我們實現的功能:能夠設置用戶指定進程的nice值、能夠給出用戶指定進程的nice值和進程優先級。我們知道,在用戶態可以通過ps命令查看進程的PID號:
而在內核中,使用結構體struct pid
標識進程,我們先看一下源碼include/linux/pid.h
中對struct pid
的描述:
/*
* What is struct pid?
*
* A struct pid is the kernel's internal notion of a process identifier.
* It refers to individual tasks, process groups, and sessions. While
* there are processes attached to it the struct pid lives in a hash
* table, so it and then the processes that it refers to can be found
* quickly from the numeric pid value. The attached processes may be
* quickly accessed by following pointers from struct pid.
*
* Storing pid_t values in the kernel and referring to them later has a
* problem. The process originally with that pid may have exited and the
* pid allocator wrapped, and another process could have come along
* and been assigned that pid.
*
* Referring to user space processes by holding a reference to struct
* task_struct has a problem. When the user space process exits
* the now useless task_struct is still kept. A task_struct plus a
* stack consumes around 10K of low kernel memory. More precisely
* this is THREAD_SIZE + sizeof(struct task_struct). By comparison
* a struct pid is about 64 bytes.
*
* Holding a reference to struct pid solves both of these problems.
* It is small so holding a reference does not consume a lot of
* resources, and since a new struct pid is allocated when the numeric pid
* value is reused (when pids wrap around) we don't mistakenly refer to new
* processes.
*/
...
struct pid
{
atomic_t count;
unsigned int level;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct rcu_head rcu;
struct upid numbers[1];
};
也就是說,struct pid
是內核內部的進程標識符。它指單個任務,進程組和會話。進程號和進程標識符通過hash散列表構成一一對應的關系,通過該數據結構可以根據進程號快速找到進程標識符。使用進程標識符來描述進程可以解決兩大問題:當進程號被重新使用時會分配新的進程標識符,不會錯誤得指向新進程;避免了用戶態進程退出時無用的task_struct
進程描述符占用太多空間,struct pid
結構體只會占用64字節的空間。
簡單了解進程標識符后,我們知道可以根據進程號PID快速找到對應進程的進程標識符,在內核函數中,可以通過find_get_pid
實現,該函數的源碼在/kernel/pid.c
:
struct pid *find_get_pid(pid_t nr)
{
struct pid *pid;
rcu_read_lock();
pid = get_pid(find_vpid(nr));
rcu_read_unlock();
return pid;
}
其中pid_t
最終是int
的宏定義,所以參數就是我們傳入的進程號,rcu是read copy update,是一種鎖機制, 讀者在讀取由RCU保護的共享數據時使用rcu_read_lock
標記它進入讀端臨界區,接着查看find_vpid()
函數:
struct pid *find_vpid(int nr)
{
return find_pid_ns(nr, current->nsproxy->pid_ns);
}
//根據進程號,找到當前進程的pid_namespace
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
struct hlist_node *elem;
struct upid *pnr;
hlist_for_each_entry_rcu(pnr, elem,
&pid_hash[pid_hashfn(nr, ns)], pid_chain)
if (pnr->nr == nr && pnr->ns == ns)
return container_of(pnr, struct pid,
numbers[ns->level]);
return NULL;
}
//在pid hash表中,根據進程號nr和pid_namespace查找對應的進程描述符,需要兩項內容同時相符,然后根據struct pid進程標識符中的level返回進程標識符
這樣我們就得到了對應的進程標識符strcut pid
了,接着,看看get_pid()
函數,在include/linux/pid.h
中:
static inline struct pid *get_pid(struct pid *pid)
{
if (pid)
atomic_inc(&pid->count);
return pid;
}
該函數這里使找到的進程標識符的被引用次數自增1。至此,我們實現了利用內核函數通過進程號得到進程標識符。
同時,我們知道,操作系統管理進程最重要的數據結構就是進程控制塊PCB,也即進程描述符,在Linux內核中是一個task struct
類型的結構體,定義在linux/sched.h
中,用於存放進程所有的描述和控制信息,這里我們也是通過PCB來得到進程的nice值和優先級,修改進程的nice值也需要用到PCB。
那么該如何得到進程描述符?內核提供了pid_task()
函數可以快速通過進程標識符struct pid
得到進程描述符,在kernel/pid.c
中:
struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
struct task_struct *result = NULL;
if (pid) {
struct hlist_node *first;
first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
lockdep_tasklist_lock_is_held());
if (first)
result = hlist_entry(first, struct task_struct, pids[(type)].node);
}
return result;
}
其中,pid_type定義在include/linux/pid.h
中:
enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};
於是我們通過下面這行代碼,即可得到進程描述符pcb:
pcb=pid_task(pid,PIDTYPE_PID);
在2.6版本kernel/sched.c
中可以找到通過pcb得到進程nice值和優先級的內核函數:
/**
* task_prio - return the priority value of a given task.
* @p: the task in question.
*
* This is the priority value as seen by users in /proc.
* RT tasks are offset by -200. Normal tasks are centered
* around 0, value goes from -16 to +15.
*/
int task_prio(const struct task_struct *p)
{
return p->prio - MAX_RT_PRIO;
}
/**
* task_nice - return the nice value of a given task.
* @p: the task in question.
*/
int task_nice(const struct task_struct *p)
{
return TASK_NICE(p);
}
在kernel/sched.c
中也可以找到設置指定進程nice值的內核函數set_user_nice()
:
void set_user_nice(struct task_struct *p, long nice)
{
int old_prio, delta, on_rq;
unsigned long flags;
struct rq *rq;
if (TASK_NICE(p) == nice || nice < -20 || nice > 19)
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, &flags);
/*
* 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_FIFO/SCHED_RR:
*/
if (task_has_rt_policy(p)) {
p->static_prio = NICE_TO_PRIO(nice);
goto out_unlock;
}
on_rq = p->se.on_rq;
if (on_rq)
dequeue_task(rq, p, 0);
p->static_prio = NICE_TO_PRIO(nice);
set_load_weight(p);
old_prio = p->prio;
p->prio = effective_prio(p);
delta = p->prio - old_prio;
if (on_rq) {
enqueue_task(rq, p, 0);
/*
* If the task increased its priority or is running and
* lowered its priority, then reschedule its CPU:
*/
if (delta < 0 || (delta > 0 && task_running(rq, p)))
resched_task(rq->curr);
}
out_unlock:
task_rq_unlock(rq, &flags);
}
最后,用戶空間和內核空間之間不能直接傳遞數據,我們必須使用copy_from_user()
和copy_to_user()
兩個函數實現,這里我們只簡單看下函數原型:
static inline unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
static inline unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
至此,借助上述函數便能夠完成符合要求的系統調用了。
推薦兩個博客:
https://blog.csdn.net/tiantao2012 (這位大佬分析了很多Linux的內核函數)
http://www.wowotech.net/process_management/pid.html (關於Linux內核如何標識進程)