HDU-OS-1 Linux内核编译及添加系统调用

编译失败,虽千万次,吾往矣

什么!你编译内核......又失败了?看过无数篇实验教程却还是手足无措?得不到想要的结果却觉得是和指导书一样的操作?你感动天,感动地,却没能感动编译器。是不是有点想放弃?试图把第一个实验水过去。

Hey! Man! Don't do it before looking here! 本文中,笔者将从零开始,和你一起完成第一个实验,以一个重修os的老学长的姿态和你并肩前行,避免你误入不必要的坑。要自信!毕竟我在折腾了大半个月后都成功了,而且论手残,我是专业的。

实验要求

(1) 编译一个干净的 linux 内核并加载成功,不需要对内核进行修改。

(2) 在 (1) 中新编译的内核中,添加一个系统调用,实现对指定进程的 nice 值的修改或读取功能,并获取进程最新的 nice 值及优先级 prio
建议调用原型为:

1
Int mysetnice(pid_t pid, int flag, int nicevalue, void __user * prio,void __user * nice);

参数含义:
pid:进程 ID
flag:若值为 0,功能是读取 nice 值;若值为 1,功能是设置 nice 值。
Prio:进程的优先级。
返回值:系统调用成功时返回 0,进程最新的优先级(prio),失败时返回错误码 EFAULT

(3) 若程序中调用了 linux 的内核函数,要求深入阅读函数源码。

准备

Linux分区&下载内核

笔者的笔电很垃圾,全机械磁盘 + 陈年CPU。放弃虚拟机,采用双系统的方式安装Ubuntu-16,却依然要面对无限网卡驱动无效 + 多核编译死机的问题。几经折腾后采用了如下不够科学却足够稳妥的分区方式:

Type Sign Proportion
主分区 / 30GB/100GB
交换空间 swap 8GB/100GB
逻辑分区 /home 52GB/100GB
引导分区 /boot 10GB/100GB

[*] Caution!/boot 分区不要吝啬!我吃过亏!编译过程中空间不足最为致命!使用双系统的朋友要尤为注意!

官网下载最新版Linux内核 4.18.8,默认保存到 /home/user/下载
下载过程中,我们练习几条命令为接下来的操作准备

开启 root

快捷键打开终端 Ctrl+Alt+T

1
2
3
$ sudo passwd root # 设置/更新root权限密码
$ su root # 切换到root权限
# su user # 切换到user权限

[*] Caution! 具体执行时 user 是你的用户名,后面出现的所有 user 亦然,当然 变量类型 __user 除外 QAQ

文件管理

1
2
3
4
5
6
7
$ mkdir folder # 创建文件夹
$ rmdir null_folder # 删除空文件夹
$ rm file1 file2 # 删除文件
$ rm -rf notnull_floder # 删除非空文件夹下的一切
$ touch file # 创建文件
$ mv file1 file2 # 重命名
$ mv file folder # 移动

编辑器 Vim

本文的编辑工作交给 Vim

1
2
$ sudo apt install vim # 安装最新版Vim
$ vim hello.c # 在当前目录新建.c文件测试Vim

进入Vim,编辑一段 hello world吧!
Vim 基础操作:

  • 按下 i 开启编辑模式
  • 按下 Esc 开启命令模式
  • 按下 : 开始编辑命令
  • 输入 wq 保存并退出
  • 输入 q! 不保存并强行退出
1
2
3
4
5
6
7
8
// /home/user/hello.c
#include<stdio.h>

int main()
{
printf("Hello World!\n");
return 0;
}

[*] Caution! 从/向终端复制/粘贴快捷键为 Ctrl+Shift+C/ Ctrl+Shift+V

编译器 gcc

1
2
3
4
$ sudo apt install gcc # 安装最新版编译器gcc
$ gcc hello.c -o hello # 编译已经编辑好的hello.c生成目标文件hello
$ ./hello # 运行 可执行文件 hello
Hello World!

[*] Caution! 运行当前目录下的文件的命令是 ./filename

内核解压缩&移动

与此同时应该已经下载完成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cd /home/user/下载 # 用cd命令进入 下载
$ ls # 查看下载完的内核
linux-4.18.8.tar.xz
$ mv linux-4.18.8.tar.xz linux.tar.xz # 名字太长,我选择改名,你随意
$ xz -d linux.tar.xz # 解压第一步
$ tar -xvf linux.tar # 解压第二步
$ ls # 查看解压结果
linux-4.18.8 linux.tar
$ mv linux-4.18.8 linux # 日常改名
$ mv linux /usr/src # 将内核移至Ubuntu专门存放内核的目录下
mv 无法创建目录'/usr/src/linux': 权限不够 # 发现权限不够
$ su root # 开启root权限
# mv linux /usr/src # 继续移动
# ls /usr/src # 查看结果 发现新内核与现存的其他版本内核
linux linux-headers-4.13.0-36
linux-headers-4.13.0-45-generic linux-headers-4.13.0-45

修改系统调用表

[*] Caution! 如不加强调,接下来所有操作均在 /usr/src/linux 下进行

1
2
# cd /usr/src/linux # 进入内核文件根目录
# vim + ./arch/x86/entry/syscalls/syscall_64.tbl # 打开系统调用表所在的文件

可以看到系统调用表的格式


[*] Caution! 黄线框内为此版本内核的不同之处

添加一条系统调用

1
335     64      mysetnice       __x64_sys_mysetnice


[*] Caution! 注意序列号数字的变化,实际操作时以你的版本为主,随机应变,还要关注下缩进

分配系统调用号

1
# vim + ./include/uapi/asm-generic/unistd.h # 打开目标文件的最后一行


[*] Caution! 末尾两处宏定义的变化

添加如下代码设置系统调用号

1
2
#define __NR_mysetnice 335		//分配系统调用号
__SYSCALL(__NR_mysetnice,sys_mysetnice)

申明系统调用原型

1
# vim + ./include/linux/syscalls.h # 打开目标文件的最后一行

添加函数原型的申明

1
2
//添加系统调用函数原型的申明
asmlinkage long sys_mysetnice(pid_t pid,int flag,int nicevalue,void __user * prio,void __user * nice);

[*] Caution! 原型申明插入在 #endif 的前一行

实现系统调用服务例程

1
# vim + ./kernel/sys.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
SYSCALL_DEFINE5(mysetnice, pid_t, pid, int, flag, int, nicevalue, void __user *, prio, void __user *, nice)
{
struct pid * kpid;
struct task_struct * task;
int nicebef;
int priobef;
kpid = find_get_pid(pid); // 根据 进程号返回 pid 类型的一个结构体
task = pid_task(kpid, PIDTYPE_PID); // 返回 task_struct 类型的结构体
nicebef = task_nice(task); // 修改前的nice值
priobef = task_prio(task); // 修改前的priority值

if(flag == 1){
set_user_nice(task, nicevalue);
printk("修改前的nice值:%d\t修改后的nice值:%d\n", nicebef, nicevalue); // 日志级别打印,此行可有可无
return 0;
}
else if(flag == 0){
copy_to_user(nice, (const void*)&nicebef, sizeof(nicebef)); // 内核态 到 用户态
copy_to_user(prio, (const void*)&priobef, sizeof(priobef)); // 同上
printk("该进程的nice值:%d\n", nicebef); // 日志级别打印, 此行可有可无
printk("该进程的prio值:%d\n", priobef); // 日志级别打印, 此行可有可无
return 0;
}

printk("flag 有误,重新输入!\n");
return EFAULT;
}

[*] Caution! 代码插入在 #endif 前一行

编译&安装(内核&模块)

配置内核

1
2
3
4
5
# apt-get install libncurses5-dev # 安装必备软件包
# make menuconfig # 试图配置内核,为何试图?因为很难一次性成功
# apt-get install XXX # 仔细看报错&提示,缺啥补啥,这个过程可能会多次进行
# make menuconfig # 再次尝试配置,祝你成功
......

弹出配置内核界面

[*] Caution! 可按下 y/ m / n 进行修改

  1. 继续向下查看

  2. ENTER 进入

  3. ENTER 进入

  4. 如图所示按 y 调整成 *SaveExit

  5. ENTER 进入

  6. 如图所示按 y 调整成 *SaveExit

编译内核

1
2
3
4
5
6
# apt-get install libssl-dev # 猜测你可能会缺少这个软件包
# apt-get install libelf-dev # 以及这个
# make -jX # 多线程编译,X为线程数,以你硬件资源的实际情况为主。笔者多线程直接死机,所以使用 make 单线程进行
# apt-get install XXX # 若编译无法进行,仔细看报错,结合网上资料,缺啥补啥
# make mrproper # 每次编译失败都要清除内核残留,且重新配置内核
# make menuconfig # 重复配置内核的过程

编译模块

1
2
# make modules # 试图编译模块
# apt-get install XXX # 若编译无法进行,仔细看报错,缺啥补啥

安装模块&内核

1
2
3
4
5
6
# uname -a # 检查旧内核版本 4.15.0
Linux user-XLI 4.15.0-34-generic #37~16.04.1-Ubuntu SMP Tue Aug 28 11:18:27 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
# make modules_install # 安装模块
# make install # 安装内核
# update-grub2 # 自动修改 grub
$ reboot # 重启

测试系统调用

[*] Caution! 接下来操作在 /home/user/ 下进行即可

1
2
3
4
5
$ uname -a # 检查内核版本 4.18.8
Linux user-XLI 4.18.8 #1 SMP Sep 16 11: 07:51 CST 2018 x86_64 x86_64 x86_64 GNU/Linu
$ mkdir os_1 # 新建文件夹
$ vim os_1/test_syscall.c # 编写测试系统调用的程序
$ gcc os_1/test_syscall.c -o os_1/test # 编译生成可执行文件

测试程序具体代码

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
// /home/user/os_1/test_syacall.c
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define __NR_mysyscall 335

int main()
{
int pid = 0;
int flag = 0;
int nicevalue = 0;
int prio = 0;
int nice = 0;
int result;

printf("please input: pid, flag, nicevalue\n");
scanf("%d %d %d", &pid, &flag, &nicevalue);
result = syscall(__NR_mysyscall, pid, flag, nicevalue, (void *)&prio, (void *)&nice);
if (result == 0)
{
printf("pid:%d, flag:%d, nicevalue:%d, prio:%d, nice:%d\n", pid, flag, nicevalue, prio, nice);
return 0;
}
printf("some wrong, maybe pid is not exist\n");
return 0;
}

打开两个终端

1
$ top # 终端1:top 命令查看进程表

1
$ ./os_1/test # 终端2:测试系统调用

测试详情:

若你的测试效果大该如此这般,那么恭喜你,成功了大半!

至于另一半?你得深入了解一下Linux下系统调用的实质

实验详解

[*] Caution! 下面所有源码的引用均以 v4.18.12 为例,均以 linux 内核所在的目录为根目录

process

进程是具有一定独立功能的程序关于某个数据集合的一次运算过程,是系统进行资源分配和调度的独立单位。进程的两个基本元素:一个或一组可执行的程序、与程序有关的数据集。

PCB

进程控制块,是进程映像的重要组成部分,其中记录了用于描述进程情况即控制进程运行的全部信息。是具有 task_struct 结构的结构体。

task_struct

task_struct是Linux内核的一种数据结构,它会被装载到RAM中并且包含着进程的信息。每个进程都把它的信息放在 task_struct 这个数据结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ./include/linux/sched.h
struct task_struct {

......
volatile long state; // 602 lines
......
int prio; // 639 lines
int static_prio;
int normal_prio;
unsigned int rt_priority;
......
struct mm_struct *mm; // 688 lines
struct mm_struct *active_mm;
......
pid_t pid; // 742 lines
......
struct sched_entity *parent; // 759 lines
......
struct list_head children; // 764 lines
struct list_head sibling;
......
char comm[TASK_COMM_LEN]; // 842 lines
......
}

pid

在计算机领域,进程标识符(英语:process identifier,又略称为进程ID(英语:process ID)、PID)是大多数操作系统的内核用于唯一标识进程的一个数值。这一数值可以作为许多函数调用的参数,以使调整进程优先级、杀死进程之类的进程控制行为成为可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ./include/linux/pid.h
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];
};
/*
这里的 pid 是一个结构体类型,绝不仅仅代表一个 进程号
进程号 只是一个整形数
pid 包含更多信息
*/

pid_t

1
2
// ./usr/include/sys/types.h
typedef __kernel_pid_t pid_t;
1
2
// ./include/uapi/asm-generic/posix_types.h
typedef int __kernel_pid_t;

兜兜转转之后,可知 pid_t 就是 int 类型

nice & priority

Nice值是类UNIX操作系统中表示静态优先级的数值。每个进程都有自己的静态优先级,优先级高的进程得以优先运行。Nice值的范围是-20~+19,拥有Nice值越大的进程的实际优先级越小,默认的Nice值是0。由于Nice值是静态优先级,所以一经设定,就不会再被内核修改,直到被重新设定。Nice值只起干预CPU时间分配的作用,实际中的细节,由动态优先级决定。

The difference is that PR is a real priority of a process at the moment inside of the kernel and NI is just a hint for the kernel what the priority the process should have.
区别在于Priority是内核内部进程的真正优先级,Nice只是内核提示进程应具有的优先级。

一般情况下,Priority = Nice + 20,也就是Priority的值为0~39。但是上述理论仅仅适用于调度策略是SCHED_OTHER的进程。

find_get_pid()

1
2
3
4
5
6
7
8
9
10
11
12
// ./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 类型的结构体

pid_task()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ./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;
}
EXPORT_SYMBOL(pid_task);
// 根据 pid 和 pid_type 确定一个 task_struct 类型的结构体,并返回他

set_user_nice()

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// ./kernel/sched/core.c
void set_user_nice(struct task_struct *p, long nice)
{
bool queued, running;
int old_prio, delta;
struct rq_flags rf;
struct rq *rq;

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:
*/
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);
if (running)
put_prev_task(rq, p);

p->static_prio = NICE_TO_PRIO(nice);
set_load_weight(p, true);
old_prio = p->prio;
p->prio = effective_prio(p);
delta = p->prio - old_prio;

if (queued) {
enqueue_task(rq, p, ENQUEUE_RESTORE | ENQUEUE_NOCLOCK);
/*
* 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_curr(rq);
}
if (running)
set_curr_task(rq, p);
out_unlock:
task_rq_unlock(rq, p, &rf);
}
EXPORT_SYMBOL(set_user_nice);
// 简言之,根据 task_struct 确定一个进程,并改变进程 nice 值

这个函数很重要,肥肠之重要,重要到需要专门为其写一篇文章,当然了,我还没写QAQ
请务必点击上一个 set_user_nice() 超链接,在线层层阅读源码并深入思考。暂且再放一个仅供参考的文章链接吧!

copy_to_user()

1
2
3
4
5
6
7
8
9
10
11
12
13
// ./include/linux/uaccess.h
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(check_copy_size(from, n, true)))
n = _copy_to_user(to, from, n);
return n;
}
/*
内核空间与用户空间的内存 不能 直接互访
to 目标地址 用户空间的地址;
from 源地址 内核空间的地址;
n 将要拷贝的数据的字节数。
*/


不知上述解释有没有帮助到你,希望你看到这里时已经彻底击败这个实验了!
如果发现文章中的纰漏也请及时联系我,右侧边栏的站点概览有我的联系方式,我们一起进步!
暂且撤了,更细节的的内容随时补充,拜拜!

Remarks

关于题目的要求: 实现对指定进程的 nice 值的修改或读取功能,并获取进程最新的 nice 值及优先级 prio ,目前所知有如下两种理解:

<甲>

  • 读取功能:打印 niceprio 的值
  • 修改功能:仅修改nice

<乙>

  • 读取功能:打印 niceprio 的值
  • 修改功能:修改nice 值,并打印最新的 niceprio 的值

笔者文中采用的是第一种写法,我粗浅地认为功能要纯粹,代码少重复。当然这一环节在实验中并不占什么分量,没必要花大心思。有其他想法的朋友可根据自身需求和实际情况随机应变。比如 <乙>可以有如下改法:

1
2
3
4
5
6
7
8
9
10
11
12
13
......
if(flag == 1){
set_user_nice(task, nicevalue);
copy_to_user(nice, (const void*)&nicebef, sizeof(nicebef)); // 内核态 到 用户态
copy_to_user(prio, (const void*)&priobef, sizeof(priobef)); // 同上
return 0;
}
else if(flag == 0){ // flag=0时,执行读取并打印功能
copy_to_user(nice, (const void*)&nicebef, sizeof(nicebef)); // 内核态 到 用户态
copy_to_user(prio, (const void*)&priobef, sizeof(priobef)); // 同上
return 0;
}
......

部分源码 改编校正 自GitHub上前人的贡献,点击查看出处
更详细的实验操作流程点这里,45张实验截图包你满意
更透彻的实验指导点这里,笔者能力有限,直接祭出外援,假装@Liolok


END