编译失败,虽千万次,吾往矣
什么!你编译内核...也...又失败了?看过无数篇实验教程却还是手足无措?得不到想要的结果却觉得是和指导书一样的操作?你感动天,感动地,却没能感动编译器。是不是有点想放弃?试图把第一个实验水过去。
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 | mkdir folder # 创建文件夹 |
编辑器 Vim
本文的编辑工作交给 Vim
1
2 sudo apt install vim # 安装最新版Vim
vim hello.c # 在当前目录新建.c文件测试Vim
进入Vim,编辑一段 hello world吧!
Vim 基础操作:
- 按下
i
开启编辑模式 - 按下
Esc
开启命令模式 - 按下
:
开始编辑命令 - 输入
wq
保存并退出 - 输入
q!
不保存并强行退出
1 | // /home/user/hello.c |
[*] Caution! 从/向终端复制/粘贴快捷键为 Ctrl+Shift+C
/ Ctrl+Shift+V
编译器 gcc
1 | sudo apt install gcc # 安装最新版编译器gcc |
[*] Caution! 运行当前目录下的文件的命令是 ./filename
内核解压缩&移动
与此同时应该已经下载完成了
1 | cd /home/user/下载 # 用cd命令进入 下载 |
修改系统调用表
[*] 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
__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
27SYSCALL_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 | apt-get install libncurses5-dev # 安装必备软件包 |
弹出配置内核界面
[*] Caution! 可按下 y
/ m
/ n
进行修改
继续向下查看
按
ENTER
进入按
ENTER
进入如图所示按
y
调整成*
,Save
并Exit
按
ENTER
进入如图所示按
y
调整成*
,Save
并Exit
编译内核
1 | apt-get install libssl-dev # 猜测你可能会缺少这个软件包 |
编译模块
1 | make modules # 试图编译模块 |
安装模块&内核
1 | uname -a # 检查旧内核版本 4.15.0 |
测试系统调用
[*] 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
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 | // ./include/linux/sched.h |
pid
在计算机领域,进程标识符(英语:process identifier,又略称为进程ID(英语:process ID)、PID)是大多数操作系统的内核用于唯一标识进程的一个数值。这一数值可以作为许多函数调用的参数,以使调整进程优先级、杀死进程之类的进程控制行为成为可能。
1 | // ./include/linux/pid.h |
pid_t
1 | // ./usr/include/sys/types.h |
1 | // ./include/uapi/asm-generic/posix_types.h |
兜兜转转之后,可知 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 | // ./kernel/pid.c |
pid_task()
1 | // ./kernel/pid.c |
set_user_nice()
1 | // ./kernel/sched/core.c |
这个函数很重要,肥肠之重要,重要到需要专门为其写一篇文章,当然了,我还没写QAQ
请务必点击上一个set_user_nice()
超链接,在线层层阅读源码并深入思考。暂且再放一个仅供参考的文章链接吧!
copy_to_user()
1 | // ./include/linux/uaccess.h |
不知上述解释有没有帮助到你,希望你看到这里时已经彻底击败这个实验了!
如果发现文章中的纰漏也请及时联系我,右侧边栏的站点概览有我的联系方式,我们一起进步!
暂且撤了,更细节的的内容随时补充,拜拜!
Remarks
关于题目的要求: 实现对指定进程的 nice 值的修改或读取功能,并获取进程最新的 nice 值及优先级 prio
,目前所知有如下两种理解:
<甲>
- 读取功能:打印
nice
、prio
的值 - 修改功能:仅修改
nice
值
<乙>
- 读取功能:打印
nice
、prio
的值 - 修改功能:修改
nice
值,并打印最新的nice
、prio
的值
笔者文中采用的是第一种写法,我粗浅地认为功能要纯粹,代码少重复。当然这一环节在实验中并不占什么分量,没必要花大心思。有其他想法的朋友可根据自身需求和实际情况随机应变。比如
<乙>
可以有如下改法:
1 | ...... |
部分源码 改编校正 自GitHub上前人的贡献,点击查看出处
更详细的实验操作流程点这里,45张实验截图包你满意
更透彻的实验指导点这里,笔者能力有限,直接祭出外援,假装@Liolok