HDU-OS-Lab1-Linux 内核编译及添加系统调用
还是老样子老原因,因为今年,开设的操作系统课程还有课程设计,对于原本的我来说,其实对于Linux的系统调用还有API都不是很熟悉,所以我就得多多研究这门课程了。
建议大家在做完实验以后,回过头来,再看看接下来这个教程:Linux系统调用(syscall)原理
本文实验均使用root
权限操作,不用每一步都使用sudo
实验一的要求
1、 安装操作系统Linux
2、 查看操作系统内核版本 uname –a
3、 下载操作系统内核源码 /usr/src/kernel/,版本略微高一点,解压
4、 编译新内核,挂载,验证新内核版本
5、 添加一个简单系统API,输出“hello,自己的学号”,编译验证
6、 添加要求的系统API,实现对指定进程的 nice 值的修改或读取功能
7、 自己设计一个新的系统API,实现并验证
对于我来说,前面的几项还是很简单的包括设计一个系统API,但是因为我的编程能力不是太行,所以我就是网上查了好多关于nice值调用的文章,但是基本上,都是只告诉了我们如何调用,但是并没有关于调用的原理是什么的,所以对于我来说,我就只能去研究一下了。
如果大家不知道nice值是干什么的,请大家看看这篇文章哈 :Linux系统进程优先等级调整
计算机分为用户态和内核态两种状态,用户态是上层应用程序的活动空间,被限制不能直接接触底层。内核态直接控制硬件,对用户态提供运行环境。二者之间有访管控制,不允许直接访问。如果从用户态进入内核态,就需要系统调用。系统调用就是一系列的用户态和内核态的接口函数。
接下来就是我做这次实验的过程:
添加系统调用呢,至少需要三步的
第一步:分配系统调用号,修改系统调用表
第二步:服务例程的原型声明的添加
第三步:实现系统调用服务例程
接下来我将从这三个方面,来介绍这次系统的调用过程
安装Linux操作系统
这里我不能过多的赘述,因为Ubuntu Linux系统我已经安装了几十遍了,包括我的腾讯云服务器都使用的是Ubuntu系统,但是为防止初学者看这篇文章,我特意在网上找了方法 传送门
查看安装配置内核
请看我的其他文章:Ubuntu18.04 LTS编译内核
使用Visual Studio Code
远程编辑虚拟机内的文件
首先,我们知道,一般情况下,我们在Ubuntu Linux系统当中的编辑器大概有下列几种:Vim ,gedit,还有nano,当然还有Linux版本的Visual Studio Code。
但是一般情况下,我们的Linux系统,都是安装在虚拟机当中的(不排除有把Linux发行版做主力机的大佬)
为了使用方便,我来给大家推荐一下 使用Visual Studio Code
(以下简称VSCode
) 远程编辑Linux
下的文件
首先我们安装VSCode ,其实安装VSCode很简单的,只需要把对应系统版本的安装包下载下来就可以了下载链接
VSCode
插件安装方法
首先,打开在左边栏点击图标,或者使用Ctrl+Shift+X
打开VSCode
的插件中心(如图)
然后,在搜索框中输入Remote
,安装这个插件就可以了
Ubuntu系统内设置
在Windows
系统的VSCode
下设置成功以后,接下来就在Ubuntu
当中设置一下了
首先我们要安装SSH
来支持远程管理文件,对于Ubuntu
来说,使用如下命令安装SSH
sudo apt-get install openssh-server
然后使用这个连接就可以了,但是我发现一个问题,就是在这个情况下,我无法使用VSCode编辑文件,提示权限不足,然后我就使用root
账户登录,结果登录不了,网上查了一下,才发现是Linux SSH
默认不允许root
账户登录
然后接下来就是解决方法了:
首先配置ssh服务的配置文件
sudo apt-get install vim(如果没有需要先安装)
sudo vim /etc/ssh/sshd_config
找到下面这几句话
#Authentication:
#LoginGraceTime 120
#PermitRootLogin prohibit-password
将#PermitRootLogin prohibit-password前面的#去掉同时将prohibit-password改为yes
接下来就是设置root账户的密码了
首先切换至管理员账户:
sudo su
然后输入:passwd root
设置密码就可以了,设置完root密码以后记得重启ssh的服务
sudo service ssh restart
然后输出“Hello ,自己的学号”
所有系统调用的添加方法都是近似差不多的,所以我就现在这里说一下添加调用的办法
分配系统调用号,修改系统调用表
首先我们找到系统调用表,我的目录是这样的
/usr/src/linux-5.5.5/arch/x86/entry/syscalls/syscall_64.tbl
所以我将VSCode的目录切换到/usr/src/linux-5.5.5/
这个目录下然后我们打开/arch/x86/entry/syscalls/syscall_64.tbl
这个文件看一下
我们可以看到内核调用的是有四组数据的
例如我自己加的#Added By Govzzp
后的
重新加入的hello +学号
还有mysetnice
的系统调用:
系统调用号 | 应用二进制接口 | 系统调用名 | 服务例程入口地址 |
---|---|---|---|
436 | common | hello | __x64_sys_hello |
437 | common | mysetnice | __x64_sys_mysetnice |
应用二进制接口分为三种,分别是:64
、x32
和common
一般情况下64
指的就是在64位程序当中调用的,x32
是供32位程序调用的,common
就是在32位程序和64位程序都可以调用,其他的基本上都可以照着上面照葫芦画瓢,先写上。
对了,经过老师的提醒,我发现我的一个问题,因为我在应用二进制接口这里所填写的是common
所以我在修改调用号的时候应该也要修改32位的调用文件syscall_32.tbl
当中添加一个效果如下:
common
一定要在syscall_32.tbl
添加系统调用号服务例程的原型声明的添加
这个添加服务例程原型声明的文件在这个文件下/usr/src/linux-5.5.5/include/linux/syscalls.h
所以我们先打开这个文件看一下,效果如下
为了方便,我把代码复制下来了
后面的两行代码,是我在想办法设置其他的获取nice的方法,后来我发现还是有点问题的,但是因为已经写进内核了,所以我就没有更改
//Add by Govzzp
asmlinkage long sys_hello(void);
asmlinkage long sys_mysetnice(pid_t pid , int flag , int nicevalue , void __user*prio , void __user*nice);
//Added by Govzzp for twice
//asmlinkage long sys_getusernice(pid_t pid , int nicevalue);
//asmlinkage long sys_setusernice(pid_t pid,void __user *prio,void __user *nice);
特别提示:这段代码一定要加在#endif
后面,不然可能存在不会被识别到的问题(以防万一)
其中asmlinkage
是一个必须的限定词,用于通知编译器仅从堆栈中提取该函数的参数,而不是从寄存器中,因为在执行服务例程之前系统已经将通过寄存器传递过来的参数值压入内核堆栈了。
实现系统调用服务例程
接下来就是很重要的一点了,我们要在系统中添加一个C语言的函数了,这一步与上一步的联系就是这样的,就像是C语言在头文件中的声明(syscall.h)和他的实现了(sys.c)
首先我们从函数mysetnice(pid_t pid , int flag , int nicevalue , void __user*prio , void __user*nice);
当中可以看到,这个函数有五个参数分别是:
参数名 | 参数作用 | 值的作用 |
---|---|---|
pid | 操作线程的pid值 | 获取系统当前运行的进程的pid值 |
flag | 操作码 | 读取nice的值为0,修改nice的值为1 |
nicevalue | 要修改的nice的值 | 用户自定义 |
prio | 优先级 | / |
nice | 当前的nice值 | / |
接下来先提供一部分源码来理解整个实验,建议在实验结束以后,再回头看一下,会有更好的理解
name:find_get_pid
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;
}
name:pid_task
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, pid_links[(type)]);
}
return result;
}
name:task_prio
int task_prio(const struct task_struct *p)
{
return p->prio - MAX_RT_PRIO;
}
name:task_nice
static inline int task_nice(const struct task_struct *p)
{
return PRIO_TO_NICE((p)->static_prio);
}
name:set_user_nice
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);
}
name:copy_to_user
static __always_inline unsigned long __must_check
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;
}
这是sys.c
所在的目录/usr/src/linux-5.5.5/kernal/sys.c
首先看完这些代码,我们先来考虑一下,这个功能如何来实现。根据内容来看:这个系统调用需要具备对指定进程的 nice 值的修改及读取的功能,同时返回进程最新的 nice 值及优先级 prio。
我们慢慢把功能做一个切割,发现我们要做到的有如下几点:
- 根据进程号 pid 找到相应的进程控制块 PCB(因为进程控制块中记录了用于描述进程情况及控制进程运行所需要的全部信息,nice 值和优先级正是其中的一部分);
- 根据 PCB 读取它的 nice 值和优先级 prio;
- 根据 PCB 对相应进程的 nice 值进行修改;
- 将得到的 nice 值和优先级 prio 进行返回。
接下来就是我要写的所有代码,是调用了上面的函数,才可以实现的,代码可能写的不好,还包括了我写关于我的hello+学号
的代码。
首先也要解释一下,关于nice的值修改的过程图:(下面的这一段代码,就是我根据这个图写的)

\#endif /* CONFIG_COMPAT */
//Add by Govzzp
SYSCALL_DEFINE0(hello){
printk("Hello Govzzp!18271441");
return 0;
}
SYSCALL_DEFINE5(mysetnice,pid_t,pid,int,flag,int,nicevalue,void __user*,prio,void __user*,nice)
{
struct pid * kpid;
struct task_struct *task;
kpid = find_get_pid(pid);
task = pid_task(kpid,PIDTYPE_PID);
int NiceBefore = task_nice(task);
int PrioBefore = task_prio(task);
if(flag == 1){
set_user_nice(task,nicevalue);
printk("Nice Before: %d\t Nice Changed:%d\n",NiceBefore,nicevalue);
return 0;
}
else if(flag == 0){
copy_to_user(nice,(const void*)&NiceBefore,sizeof(NiceBefore));
copy_to_user(prio,(const void*)&PrioBefore,sizeof(PrioBefore));
printk("This Value of Nice is:%d\n",NiceBefore);
printk("This Value of prio is:%d\n",PrioBefore);
return 0;
}
printk("Error Flag!Please Retry it!");
return EFAULT;
}
//Govzzp added failed
// SYSCALL_DEFINE2(getusernice,pid_t,pid,int,nicevalue){
// struct pid *ppid;
// struct task_struct *task;
// ppid = find_get_pid(pid);
// task = pid_task(ppid,PIDTYPE_PID);
// }
// SYSCALL_DEFINE3(setusernice,int,nicevalue,int __user,*prio,int __user,*nice){
// get_prio = task_prio(task);
// get_nice = task_nice(task);
// }
下面被注释掉的这一段代码,是我改写这些代码的时候,发现无从下手,所以就写了一半,就再没有写下去。
系统调用的测试
首先是要编译内核,具体内核编译的过程,请见我的另一篇文章:Ubuntu18.04 LTS编译内核
接下来就是要使用一些代码来测试了:
首先我们测试一下我写的第一个系统调用:输出Hello+学号
name:Hello.c
#define _GNU_SOURCE
#include<linux/unistd.h>
#include<sys/syscall.h>
#define __NR_mysyscall 436
int main()
{
syscall(__NR_mysyscall);
}
将上面的代码,写入一个名为Hello.c的文件下面,然后通过代码生成可执行文件,代码如下
gcc -o Hello Hello.c
./Hello
然后在终端当中,输入一下命令查看
dmesg
或者运行dmesg -C
然后再运行 ./Hello
效果如下图所示
接下来测试我们所调用的nice值:
\#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define _GUN_SOURCE
#define __NR_mysetnice 437
int main(){
pid_t tid;
int nicevalue;
int prio = 0;
int nice = 0;
tid = getpid();
syscall(__NR_mysetnice,tid,0,-5,&prio,&nice);//read
printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
syscall(__NR_mysetnice,tid,1,-5,&prio,&nice);//set
printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
syscall(__NR_mysetnice,tid,0,-5,&prio,&nice);//read
printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
printf("*******************************\n");
syscall(__NR_mysetnice,tid,0,-15,&prio,&nice);//read
printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
syscall(__NR_mysetnice,tid,1,-15,&prio,&nice);//set
printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
syscall(__NR_mysetnice,tid,0,-15,&prio,&nice);//read
printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
return 0;
}
然后我们通过具体的实验查看一下测试结果
我们可以看到,前后的nice值发生了变化,所以,这次实验,就算是成功了
实验的改进
经过张祯老师的提醒,我发现在我的程序上面有一个很大的漏洞,就是在我获取nice值的时候,没有去判断当前进程的nice值获取是否成功,因为在我原则上以为,这个nice的值肯定是获取成功的,但是,有同学在实验的时候,也发现了nice值没有获取成功的问题,所以经过改进,我在原来程序上面加了一个,可以判断nice值是否获取成功的判断条件,代码如下:
//Add by Govzzp
SYSCALL_DEFINE0(hello){
printk("Hello Govzzp!18271441");
return 0;
}
SYSCALL_DEFINE5(mysetnice,pid_t,pid,int,flag,int,nicevalue,void __user*,prio,void __user*,nice)
{
struct pid * kpid;
struct task_struct *task;
kpid = find_get_pid(pid);
task = pid_task(kpid,PIDTYPE_PID);
int NiceBefore = task_nice(task);
int PrioBefore = task_prio(task);
if (kpid == NULL)
{
printk("Get pid ERROR!");
return EFAULT;
}else
{
if(flag == 1){
set_user_nice(task,nicevalue);
printk("Nice Before: %d\t Nice Changed:%d\n",NiceBefore,nicevalue);
return 0;
}
else if(flag == 0){
copy_to_user(nice,(const void*)&NiceBefore,sizeof(NiceBefore));
copy_to_user(prio,(const void*)&PrioBefore,sizeof(PrioBefore));
printk("This Value of Nice is:%d\n",NiceBefore);
printk("This Value of prio is:%d\n",PrioBefore);
return 0;
}
printk("Error Flag!Please Retry it!");
return EFAULT;
}
}
SYSCALL_DEFINE3(mysetapi,int,flag,int,NumA,int,NumB){
int NumSwitch;
if (flag == 1)
{
NumSwitch = NumA;
NumA = NumB;
NumB = NumSwitch;
printk("Switch Successful! %d %d",NumA,NumB);
}else
{
NumSwitch = NumA + NumB;
printk("%d+%d=%d\n",NumA,NumB,NumSwitch);
}
后面的代码。因为我自己编程能力不是太好,还要就是,我自己对于Linux内核的系统函数,没有了解过,所以写的一个超级超级简单的API也就是什么呢,就是
在输入的flag的值为1的时候,交换NumA和NumB两个数;
在输入的flag的值为非1的时候,将两个数求和;
验证自己的API的函数文件:
\#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define _GUN_SOURCE
#define __NR_mysetapi 440
int main(){
syscall(__NR_mysetapi,1,3,-6);
syscall(__NR_mysetapi,0,4,9);
return 0;
}
运行结果请看下图:
参考资料
杭州电子科技大学《操作系统》教材
宁宸同学的博客
https://imwjc.xyz/2019/01/os-lab1/
https://xinqiu.gitbooks.io/linux-insides-cn/content/SysCall/linux-syscall-2.html
感谢
感谢张祯老师的指导
感谢我的室友宁宸同学
感谢我的父母的大力支持
感谢我的女朋友的大力支持
微信赞赏
支付宝赞赏
大哥厉害,以后跟着大哥混
⭐⭐⭐⭐⭐五星好评
写的很好呢!真棒,继续加油哦😘
爱你❤️❤️❤️