目录

操作系统实验一 - Linux 内核编译及添加系统调用

添加一个系统调用,实现对指定进程的 nice 值的修改或读取功能,并返回进程最新的 nice 值及优先级 prio。

视频教程地址:https://www.bilibili.com/video/av47274857

源码地址:https://github.com/leslievan/Operator_System/tree/master/Operator_System_Lab1

以下内容全部在 Ubuntu 18.04 下操作,使用其他发行版的同学可在此基础上自行修改。

实验内容

  • 添加一个系统调用,实现对指定进程的 nice 值的修改或读取功能,并返回进程最新的 nice 值及优先级 prio。
  • 写一个简单的应用程序测试添加的系统调用。
  • 若程序中调用了 Linux 的内核函数,要求深入阅读相关函数源码。

实验步骤

获取内核源码

The Linux Kernel Archives 获取 Linux 的内核源码,任意选择一个版本,建议使用 longterm 版本。

这里下载 linux-4.19.113.tar.xz

https://cdn.lsvm.xyz/2019/02/2019-01-29-62088c7-1.png!on_blog

解压内核源码

可以通过命令行或图形界面进行解压:

  • 右键单击内核压缩包,点击解压

  • 或在终端键入如下命令检查文件是否存在。(系统语言为中文的请将 Downloads 替换为下载)。

    1
    2
    
    $ cd ~/Downloads
    $ tar xvJf linux-4.19.25.tar.xz
    

    tar 命令参数解释参见附录 1-tar.

修改内核

修改系统调用表

根据上一步内核源码解压目录,定位系统调用表:

1
2
# 将~/Downloads/linux-4.19.113替换为你的内核源码解压目录
$ gedit ~/Downloads/linux-4.19.113/arch/x86/entry/syscalls/syscall_64.tbl

定位到 common/64 的最后一条,此处是334 common rseq __x64_sys_rseq,在下面添加335 64 mysetnice __x64_sys_mysetnice,系统调用号不固定,只需唯一即可。

https://cdn.lsvm.xyz/images/20190129-oslab1-02.gif

申明系统调用服务例程原型

根据内核源码解压目录,定位系统调用头文件:

1
2
# 将~/Downloads/linux-4.19.113替换为你的内核源码解压目录
$ gedit ~/Downloads/linux-4.19.113/include/linux/syscalls.h

定位到最后一行,在 endif 前面添加系统调用原型声明:

1
asmlinkage long sys_mysetnice(pid_t pid, int flag, int nicevaluse, void __user* prio, void __user* nice);

https://cdn.lsvm.xyz/images/20190129-oslab1-03.gif

实现系统调用服务例程

上一步对函数进行了声明,这里给函数一个具体的实现。

步骤分解

需要实现的任务为:

添加一个系统调用,对指定进程的 nice 值的修改及读取的功能,同时返回进程最新的 nice 值及优先级 prio。

分解为四步:

  1. 根据进程号 pid 找到相应的进程控制块 PCB(因为进程控制块中记录了用于描述进程情况及控制进程运行所需要的全部信息,nice 值和优先级正是其中的一部分);
  2. 根据 PCB 对相应进程的 nice 值和优先级 prio 进行读取;
  3. 根据 PCB 对相应进程的 nice 值进行修改;
  4. 重新从 PCB 中读取修改后 nice 值和 prio 值;
  5. 将得到的 nice 值和优先级 prio 进行返回。

根据内核源码解压目录,定位系统调用头文件:

1
2
# 将~/Downloads/linux-4.19.113替换为你的内核源码解压目录
$ gedit ~/Downloads/linux-4.19.113/include/linux/syscalls.h

定位到最后一行,在 endif 前面添加系统调用服务例程实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 置于sys.c的最末端(在‘#endif’之前
SYSCALL_DEFINE5(mysetnice, pid_t, pid, int, flag, int, nicevalue, void __user *,
                prio, void __user *, nice) {
    int cur_prio, cur_nice;
    struct pid *ppid;
    struct task_struct *pcb;

    // 通过进程PID号找到进程的PID结构体,如果ppid为空指针则代表不存在与进程号与pid相同的进程,此时返回EFAULT(
    // 我编译的时候这个if判断并没有加进去,想做出上述判断的可以将注释删去,就逻辑本身而言没有问题-_-
    // 但我无法保证最后是否会出问题,因为我没有自己尝试过
    ppid = find_get_pid(pid);
    /*
    if (ppid == NULL)
        return EFAULT;
    */

    // 通过进程的PID结构体,找到与之对应的进程控制块
    pcb = pid_task(ppid, PIDTYPE_PID);

    // 如果flag=1则修改进程的nice值为nicevalue
    if (flag == 1) {
        set_user_nice(pcb, nicevalue);
    }  // flag既不为1也不为0的时候,即flag出错,此时返回EFAULT
    else if (flag != 0) {
        return EFAULT;
    }

    // 获取进程当前的最新nice值和prio值
    cur_prio = task_prio(pcb);
    cur_nice = task_nice(pcb);

    // 由于系统调用是在内核态下运行的,所有数据均为内核空间的数据,
    // 利用copy_to_user()函数将内核空间的数据复制到用户空间
    copy_to_user(prio, &cur_prio, sizeof(cur_prio));
    copy_to_user(nice, &cur_nice, sizeof(cur_nice));

    return 0;
}

https://cdn.lsvm.xyz/images/20190129-oslab1-04.gif

需要用到的几个内核函数:

  • struct pid *find_get_pid(pid_t nr)

    根据进程标识符号返回相应的进程标识符

  • struct task_struct *pid_task(struct pid *pid, enum pid_type type)

    根据进程标识符和进程类型返回进程控制块

  • int task_prio(const struct task_struct *p)

    返回该 PCB 的 prio 参数

  • static inline int task_nice(const struct task_struct *p)

    返回该 PCB 的 nice 参数

  • void set_user_nice(struct task_struct *p, long nice)

    修改 PCB 的 nice 参数

  • static __always_inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)

    将变量从内核空间复制到用户空间

内核函数具体实现见附录 2

编译内核

安装依赖

以下是一些需要用到的包,用 apt/apt-get 进行安装,命令行输入:

1
$ sudo apt-get install libncurses5-dev make openssl libssl-dev bison flex libelf-dev

确认参数

定位内核源码解压目录,命令行运行:

1
2
# 将~/Downloads/linux-4.19.113替换为你的内核源码解压目录
$ cd ~/Downloads/linux-4.19.113

命令行运行 make,开始编译前的参数确认:

1
$ make menuconfig

出现如下所示界面后,左右键移动下方光标选中 Save,按 Enter 结束。

开始编译

编译内核需要耗费的时间较长,建议通电进行。

定位内核源码解压目录,命令行运行:

1
$ sudo make -j4 2> error.log

-j4 表示使用四线程进行编译,这个过程大概持续一个小时,后面的重定向将错误信息输出到了 error.log 这个文件里面,方便之后进行错误排查。

make 命令默认是指编译所有,包括内核和模块,所以不需要再重新使用 make modules 进行模块的编译(至少我并没有在这个地方受到困扰)。

如果碰到问题请查阅附录 3

安装内核

等待内核、模块均编译完成,开始安装内核,分为两步:

  • 安装模块

    1
    2
    
    # 大约持续十几分钟到几十分钟不等
    $ sudo make modules_install
    
  • 替换内核

    1
    2
    
    # 大约持续几分钟到十几分钟不等
    $ sudo make install
    

替换完成后重启你的电脑,准备下一步的测试,如果电脑打不开可以参考附录 3

测试

完成编译工作后,需要编写一个用户态程序,测试系统调用是否正常工作,这里直接给出 demo.c,请自行查阅理解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// demo.c
#include "unistd.h"
#include "sys/syscall.h"
#include "stdio.h"
#define _SYSCALL_MYSETNICE_ 335
#define EFALUT 14

int main()
{
    int pid, flag, nicevalue;
    int prev_prio, prev_nice, cur_prio, cur_nice;
    int result;

    printf("Please input variable(pid, flag, nicevalue): ");
    scanf("%d%d%d", &pid, &flag, &nicevalue);
    
    result = syscall(_SYSCALL_MYSETNICE_, pid, 0, nicevalue, &prev_prio,
                     &prev_nice);
    if (result == EFALUT)
    {
        printf("ERROR!");
        return 1;
    }
    
    if (flag == 1)
    {
        syscall(_SYSCALL_MYSETNICE_, pid, 1, nicevalue, &cur_prio, &cur_nice);
        printf("Original priority is: [%d], original nice is [%d]\n", prev_prio,
               prev_nice);
        printf("Current priority is : [%d], current nice is [%d]\n", cur_prio,
               cur_nice);
    }
    else if (flag == 0)
    {
        printf("Current priority is : [%d], current nice is [%d]\n", prev_prio,
               prev_nice);
    }
    
    return 0;
}

命令行使用 gcc 进行编译,根据提示信息输入 pidflagnicevalue 进行测试。

1
2
3
# 将~/demo.c替换为你的demo.c所在位置
$ gcc ~/demo.c -o demo
./demo