實現一個Linux系統調用,返回相關進程信息

記錄操做系統課程實驗,實驗要求:新增一個系統調用,輸出給定進程的父子進程相關信息,信息包括:進程號(pid),狀態,運行時間,父進程以及第一個子進程pid。該系統調用有三個參數:給定進程pid,存儲這些數據的緩衝,緩衝大小。linux

1、前期準備
安裝VMware和Linux虛擬機的過程網上不少方法,在此不贅述,本人使用的是VMware15pro和ubuntu18.04,這裏有一點須要注意的是給虛擬機分配大一點的磁盤空間用於內核編譯,初始的20G不夠用,本人分配60G可順利完成。到Linux內核官網下載最新穩定版的內核代碼(本人用的是Linux-5.5.7),將內核壓縮包解壓到/usr/src/目錄下。而後安裝好內核編譯的一些依賴:git

sudo apt-get install libncurses5-dev libssl-dev build-essential openssl bision flex

有可能有的虛擬機gcc之類的沒有安裝,看缺什麼補什麼github

2、實驗過程
實驗的基本想法是在用戶空間開闢一個緩存空間傳到內核空間,內核根據進程號獲取進程信息,將信息存放到內核緩衝空間,而後將內核空間信息傳回用戶空間。ubuntu

首先內核如何根據進程號查詢進程的信息參考了部分「鹹魚的自留地」的代碼,修改成系統調用能使用的代碼格式,其核心思想以下api

struct pid *ppid;   //定義pid結構指針
struct task_struct *p;  //定義進程控制塊指針
struct task_struct *pos;

ppid = find_get_pid(_pid);  //_pid是用戶空間傳入的進程號,該函數根據進程號找到對應pid struct
p = pid_task(ppid, PIDTYPE_PID);    //根據pid struct找到對應進程控制塊

//接下來就可使用進程控制塊指針輸出進程信息
printk(KERN_INFO"%s", p->comm);     
//printk是內核信息輸出函數,comm是進程名稱,相似的p->pid是進程號,p->state是進程狀態,p->utime是進程用戶運行時間,p->stime是進程內核運行時間

//獲取父進程信息很簡單,加個real_parent指針便可
printk(KERN_INFO"%s%s", p->real_parent->comm, p->real_parent->pid);

//獲取子進程須要遍歷進程隊列,首先找到第一個子進程,再便利它的兄弟進程便可,將子進程指針賦給pos
list_for_each_entry(pos, &(p->children), sibling)
{
    printk(KERN_INFO"%s%s", pos->comm, pos->pid);
}

有了這些核心信息,就能開始寫系統調用的c文件了,首先cd /usr/src/linux-5.5.7/kernel/目錄下,新建一個show_process_family.c的文件,代碼以下緩存

// show_process_family.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/pid.h>
#include <linux/list.h>
#include <linux/sched.h>
#include <linux/syscalls.h>
#include <linux/slab.h>

#include <uapi/linux/show_process_family.h> //定義了struct pro_info_struct結構體存放進程信息

//SYSCALL_DEFINE3是一個宏定義,第一個參數是函數名稱,3指的是除了函數名稱外後面接3個<類型,參數名>的參數對。參數裏面來自用戶空間的指針須要加上__user標識
SYSCALL_DEFINE3(show_process_family, pid_t, _pid, struct proc_info_struct __user *, buffer, int __user *, len)
{
    long copied;
    struct proc_info_struct *k_buf;     //定義存放進程信息的內核緩衝
    int k_len;      //定義內核緩衝長度

    if(len < 0 || buffer == NULL) return -EINVAL;

    copied = copy_from_user(&k_len, len, sizeof(int));  //複製用戶空間緩衝長度
    if(copied != 0) return -EFAULT;

    k_buf = kcalloc(k_len, sizeof(struct proc_info_struct), GFP_KERNEL);    //根據緩衝大小分配內核緩衝空間
    if(k_buf == NULL) return -ENOMEM;

    copied = copy_from_user(k_buf, buffer, sizeof(struct proc_info_struct) * k_len);    //複製用戶空間緩衝信息
    if(copied != 0) return -EFAULT;

    struct pid *ppid;
    struct task_struct *p;
    struct task_struct *pos;
    char *ptype[3] = {"[I]", "[P]", "[C]"}; //預約義三種進程類型標識,Itself,Parent,Children

    // 經過進程的PID號pid一步步找到進程的進程控制塊p
    ppid = find_get_pid(_pid);
   
    if (ppid == NULL)
    {
        printk("[ShowProcessFamily] Error, PID not exists.\n");
        return -1;
    }
    p = pid_task(ppid, PIDTYPE_PID);

    // 格式化輸出表頭
    printk(KERN_INFO"[I]:process itself [P]:parent process [C]:children process\n");
    printk(KERN_INFO"%-6s%-20s%-6s%-6s%-20s\n", "Type", "Name", "PID", "State", "Running_time");
    printk(KERN_INFO"------------------------------------------\n");

    int buf_idx = 0;

    // Itself
    // 打印自身信息
    unsigned rt_i = (p->utime + p->stime)/1000000;  //將utime和stime相加做爲進程運行時間,除以1000000轉換爲秒單位
    printk(KERN_INFO"%-6s%-20s%-6d%-6ld%us\n", ptype[0], p->comm, p->pid, p->state, rt_i);
    strncpy(k_buf[buf_idx].type, ptype[0], 6);  //如下將信息複製到內核緩衝
    strncpy(k_buf[buf_idx].name, p->comm, 20);
    k_buf[buf_idx].pid = p->pid;
    k_buf[buf_idx].state = p->state;
    k_buf[buf_idx].running_time = rt_i;
    buf_idx++;

    // Parent
    // 打印父進程信息
    unsigned rt_p = (p->real_parent->utime + p->real_parent->stime)/1000000;
    printk(KERN_INFO"%-6s%-20s%-6d%-6ld%us\n", ptype[1], p->real_parent->comm, p->real_parent->pid, p->real_parent->state, rt_p);
    strncpy(k_buf[buf_idx].type, ptype[1], 6);
    strncpy(k_buf[buf_idx].name, p->real_parent->comm, 20);
    k_buf[buf_idx].pid = p->real_parent->pid;
    k_buf[buf_idx].state = p->real_parent->state;
    k_buf[buf_idx].running_time = rt_p;
    buf_idx++;

    // Children
    // 遍歷」我「的子進程,輸出信息
    list_for_each_entry(pos, &(p->children), sibling)
    {
        unsigned rt_c = (pos->utime + pos->stime)/1000000;
        printk(KERN_INFO"%-6s%-20s%-6d%-6ld%us\n", ptype[2], pos->comm, pos->pid, pos->state, rt_c);
        strncpy(k_buf[buf_idx].type, ptype[2], 6);
        strncpy(k_buf[buf_idx].name, pos->comm, 20);
        k_buf[buf_idx].pid = pos->pid;
        k_buf[buf_idx].state = pos->state;
        k_buf[buf_idx].running_time = rt_c;
        buf_idx++;
    }
   
    copied = copy_to_user(buffer, k_buf, sizeof(struct proc_info_struct) * k_len);  //將內核緩衝中的信息複製到用戶空間緩衝區
    if(copied != 0) return -EFAULT;

    kfree(k_buf);  //釋放內核緩衝區
   
    return 0;
}

上面show_process_family的函數中借鑑了jervisfm的想法,使用了三個參數,分別爲pid_t _pid, struct proc_info_struct __user * buffer, int __user len。其中利用proc_info_struct結構體來存放進程信息,該結構體在show_process_family.h中定義。cd /usr/src/linux-5.5.7/include/uapi/linux/目錄下,新建show_process_family.h文件,內容以下函數

#ifndef _SHOW_PROCESS_FAMILY_H_
#define _SHOW_PROCESS_FAMILY_H_

struct proc_info_struct
{
    char type[6];   //父子進程標識
    char name[20];  //進程名稱
    pid_t pid;      //進程號
    long state;     //進程狀態
    unsigned running_time;  //進程運行時間
};

#endif

至於爲何要放在/include/uapi/linux/目錄下卻是吃了很多苦頭才找到的資料,這裏參考了stackoverflow的一個問題。該結構體要在內核中使用,首先要對內核可見,而在c文件裏,該結構體也出如今用戶空間的傳入參數裏面,所以也要對用戶空間可見。要實現這一點就必須將結構體定義放入該目錄下,內核纔會爲用戶空間提供一個可見的api。
而僅僅放在該目錄下還不行,還須要修改Kbuild文件,cd /usr/src/linux-5.5.7/include/uapi/修改該目錄下的Kbuild文件(沒有就新建一個),添加一行,如圖flex

header-y += linux/show_process_family.h

01d0bc54cf627f548d42bbe198e874df.jpg
以後在編譯內核的時候加上一句make headers_install INSTALL_HDR_PATH=/usr就能將該頭文件添加到/usr/include/linux這個用戶可引用的目錄下。ui

回到咱們的系統調用部分,如今完成了c文件和結構體,但系統調用的流程尚未配置完。cd /usr/src/linux-5.5.7/include/linux/目錄,修改該目錄下的syscall.h文件。在文件的末尾添加一句es5

asmlinkage long sys_show_process_family(pid_t _pid, struct proc_info_struct __user * buffer, int __user * len);

adf93e72a4182b2aa821ae0d77dbd561.jpg
asmlinkage告訴編譯器在CPU棧中尋找系統調用函數參數,至關於一個系統調用的聲明,這裏面long是經常使用的返回類型不用管,函數名前面要加上sys_。因爲此處也使用了proc_info_struct,所以在該文件前面頭文件上也要增長該結構體的頭文件,如圖
450dc2b19045c6aeef0bdd652af060fb.jpg

接下來爲咱們自定義的系統調用定義調用號,cd /usr/src/linux-5.5.7/arch/x86/entry/syscall/修改該目錄下的syscall_64.tbl文件,在64位系統調用末尾添加一行,如圖

436 common show_process_family __64_sys_show_process_family

b0f708602a29d216de29f51f493e13f4.jpg
436是順延的系統調用號,common照寫,後面是系統調用函數名,再後面是函數名前面加上__x64_sys_。

到此基本完成新建系統調用的工做,還有一點收尾工做是要將編譯時c文件生成的動態庫連接到內核編譯過程當中,所以cd /usr/src/linux-5.5.7/kernel/也就是c文件所在目錄下,修改Makefile文件,找到obj-y處,在後面加上show_process_family.o,如圖
b282123aab55509f4c10ef6f8717133a.jpg

好了,接下來即可啓動內核編譯工做了,cd到內核代碼根目錄,cd /usr/src/linux-5.5.7/,輸入

sudo make menuconfig

調出編譯內核配置界面,有興趣能夠了解,也能夠不調配置使用默認的,這裏直接選擇Exit而後ok保存默認配置就好
20200307123103.png
接下來輸入

sudo make -j4
sudo make headers_install INSTALL_HDR_PATH=/usr   //輸入一次便可,再次編譯內核可不加
sudo make modules_install -j4
sudo make install -j4

完成內核編譯流程,編譯完後重啓虛擬機輸入uname -r可看到內核版本已更改。此外這裏的4指的是CPU的核心數,看本身電腦來改,越大越快,本人第一次編譯通過了漫長的兩個半小時。後面再改動內核代碼編譯的話就不須要make menuconfig了,直接輸入上面的編譯指令便可,後面的編譯過程本人大概每次編譯要半小時。

3、驗證系統調用
在任意目錄新建一個test.c文件,代碼以下

#include <stdio.h>
#include <stdlib.h>
#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <linux/show_process_family.h>  //此頭文件即是make headers_install後將內核中的結構體提供一份頭文件接口到/usr/include/linux/目錄下

void print_buffer(struct proc_info_struct *buffer, const int len);

int main(){
    int pid;    //定義進程號pid
    struct proc_info_struct *buffer;    //定義用戶空間進程信息緩衝
    int len;    //定義緩衝長度

    printf("Input <pid> <buffer_len>: ");
    scanf("%d%d", &pid, &len);
    printf("\n");

    buffer = calloc(len, sizeof(struct proc_info_struct));  //按緩衝大小分配緩衝空間
    if(buffer == NULL){
        printf("Could not allocate buffer to store processes infomation\n");
        exit(-1);
    }

    int state = syscall(436, pid, buffer, &len);    //調用系統調用
    print_buffer(buffer, len);  //輸出緩衝內容,即進程信息
    return 0;
}

void print_buffer(struct proc_info_struct *buffer, const int len){
    printf("[I]:process itself [P]:parent process [C]:children process\n");
    printf("%-6s%-20s%-6s%-6s%-20s\n", "Type", "Name", "PID", "State", "Running_time");
    printf("---------------------------------------------------\n");
    for(int i = 0; i < len; ++i){
        if(buffer[i].pid != 0){     //空白緩衝區部分不輸出
            printf("%-6s%-20s%-6d%-6ld%us\n", buffer[i].type, buffer[i].name, buffer[i].pid, buffer[i].state, buffer[i].running_time);
        }
    }
}

進程樹以及進程號能夠輸入

pstree -p

來肯定,實驗結果以下
f64e3428e659267d18e5f1e0babac8f2.jpg

bbb20e313a6fc5e229db2f26d5c62d24.jpg
對比進程樹可見結果正確,輸入dmesg查看內核輸入信息
9b536eb39ddc7b610650f294635ce931.jpg

4、參考資料
除正文中介紹的參考資料外,參考瞭如下資料:
Implementing a system call in Linux Kernel 4.7.1
how to pass parameters to linux system call
Tutorial - Write a System Call
給linux系統增長一個系統調用
如何實現一個新的系統調用

相關文章
相關標籤/搜索