实验要求:添加一个系统调用,实现对指定进程的nice值的修改或读取功能,并返回进程最新的nice以及优先级prio。建议调用原型为:
int mysetnice(pid_t pid,int flag,int nicevalue,void __user* prio,void __user *nice);
参数含义:
pid:进程id
flag:若值为0,表示读取nice值;若值为1,表示修改nice值
prio、nice:指向进程当前优先级以及nice值
系统调用成功时返回0,失败时返回错误码EFAULT
一看到这个实验要求,就产生了很多困惑,通过大量的查阅资料和阅读源码弄懂了个大概
(1)进程nice值是什么?
进程nice值可以调整该进程的优先级。int类型,值从-20到+19,负值表示调整到高优先级,0表示不调整优先级,正值表示调整到低优先级
(2)pid_t是什么类型?
//linux4.16.2源码中是这么写的
typedef __kernel_pid_t pid_t;
typedef int __kernel_pid_t;
那为什么不直接声明为int类型呢?为了在不同平台间可移植性好一些。
(3)void __user* 又是什么类型?
(void __user *)arg 指的是arg值是一个用户空间的地址,不能直接进行拷贝等,要使用例如copy_from_user,copy_to_user等函数。
大概意思就是在内核函数里,拷贝这种类型的数据要用copy_from_user,copy_to_user函数
copy_to_user函数原型:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
(4)EFAULT是什么?
#define EFAULT 14 /* Bad address */
再通过一张图来了解下系统调用的原理:
用户调用系统调用时,会执行一条访管指令,该指令会产生一个访管中断,从而让系统暂停当前进程执行,转入核心态调用处理程序。
接下来进入实际操作
一、下载虚拟机
常用虚拟机软件:Vimware workstation pro或VitualBox
创建虚拟机之前先去下载对应系统的iso文件
虚拟机配置:64位Ubuntu 16.04,磁盘空间100G(建议大于80G,有条件的可以分在固态盘C盘,速度比较快),内存8G,2核
磁盘空间太小会因空间不足而导致编译内核失败。内存太小会影响编译速度,一般编译时间在1.5h-3h。
注:以下操作均在虚拟机内进行
二、网上下载内核
下载内核:点我QAQwww.kernel.org
笔者下载的是最新的4.16.2,点击黄色框框即可下载
三、添加系统调用
切换到root用户,将下载的内核文件复制到/home或者其他空闲目录,在该目录下打开终端,分两步解压缩:
(1)sudo xz -d linux-4.16.2.tar.xz
(2)sudo tar -xvf linux-4.16.2.tar
解压完成后会生成一个linux-4.16.2的文件,进入该文件夹,开始添加系统调用。
添加系统调用需三步:
(1)查看系统调用表(./arch/x86/entry/syscalls/syscall_64.tbl)
每个系统调用在表中占一项,格式为:
<系统调用号> <64/x32/common> <系统调用名> <服务例程入口>
系统调用号非常关键,一旦分配就不能就任何更改。每个系统调用需要一个服务例程完成其具体功能。
选择一个未使用的系统调用号进行分配
如图:
(2)声明系统调用服务例程
进入目录linux-4.16.2/include/linux/syscalls.h中,在文件末尾添加内容
如图:
asmlinkage是一个必须的限定词,用于通知编译器仅从内核中提取该函数的参数,而不是从寄存器中,因为在执行服务例程之前系统已经将通过寄存器传递过来的参数压入内核堆栈了。
(3)实现系统调用服务例程
这里就要编写系统调用实现代码了,进入目录linux-4.16.2/kernel/sys.c,
在该文件中添加以下代码:
SYSCALL_DEFINE5 (mysetnice,pid_t,pid,int,flag,int,nicevalue,void __user*,prio,void __user*,nice){
//SYSCALL_DEFINEN中N表示系统调用所需要的参数个数,我们的是5个
struct task_struct *p;//进程结构体指针
struct pid *id;//pid结构体
int m,n;
id=find_get_pid(pid);//通过传入的pid_t pid得到id结构体
p=pid_task(id,PIDTYPE_PID);//通过id得到指定进程,PIDTYPE_PID指的是进程类型的pid
m=task_nice(p); //通过p得到nice值
n=task_prio(p); //通过p得到prio值(优先级)
if(flag==0){//读取
copy_to_user(nice,(void*)&m,sizeof(m));//将m的地址强转为void *类型
copy_to_user(prio,(void*)&n,sizeof(n));
return 0;
}
else if(flag==1){//修改
printk("nice value before modified:%d\n",m);
set_user_nice(p,nicevalue);//修改nice的值
return 0;
}
printk("syscall failed!");
return EFAULT;
}
如图:
以上用到的函数和结构体可以在linux源码中查阅。
另外一开始很困惑为什么SYSCALL_DEFINE类型的函数参数声明中类型和参数名要用逗号分开,直到查到了这篇文章:
Linux系统调用之SYSCALL_DEFINE
四、开始内核编译
在linux-4.16.2目录下打开终端
step 1.安装编译可能需要的库(血泪教训:没有这些库编译会出更多错)
sudo apt-get install libncurses5-dev libssl-dev
sudo apt-get install build-essential openssl
sudo apt-get install zlibc minizip
sudo apt-get install libidn11-dev libidn11
step 2.库安装完成后执行以下命令
sudo make mrproper //清除以前编译的残留文件
sudo make clean
sudo make menuconfig //配置内核
在该过程中可能会出现一些依赖不存在的问题
只需按照图中的错误提示,sudo apt-get install <文件名> 就可以解决了
例如:
提示说“bison:not found”
解决方法:sudo apt-get install bison
然后会弹出以下窗口:
如图:
不用管它(保留默认值不做修改),save一下然后exit退出即可
step 3.编译内核
sudo make
tips:若虚拟机是多核,可执行sudo make -j N 加快编译速度,其中N是核的数量
该步骤耗时较长,笔者用了2小时(等哭了有木有啊!)
step 4.编译模块
sudo make modules
只用了10分钟。。。网上都说编译内核快,编译模块慢(总觉得在骗我Q_Q)
step 5.安装内核
(1)安装模块:sudo make modules_install
(2)安装内核:sudo make install
step 6.配置grub引导
(顺便提一下引导作用:计算机启动时,引导程序在对计算机系统进行初始化后,把操作系统的核心部分程序装入住存储器)
sudo update-grub2
step 7.重启系统
reboot
重启后应该能够在grub中在新老内核中切换
如图:
进入Ubuntu高级选项
若不能切换,解决方案:
桌面打开终端->sudo update-grub->sudo gedit /etc/default/grub
在该行之前加一个“#”注释掉该行后重启。
五、测试系统调用
新建一个文本文档,文件名为Test.c
在该文档所在目录打开终端,执行以下三步
(1)gcc Test.c
(2)./a.out //gcc默认文件名
(3)dmesg //显示内核信息
a.out用户态程序输出:
dmesg内核程序输出:
比对发现测试成功!