OS 实验一 | linux 内核编译及添加系统调用

设计目的

Linux 是开源操作系统,用户可以根据自身系统需要裁剪、修改内核,定制出功能更加

合适、运行效率更高的系统,因此,编译 linux 内核是进行内核开发的必要基本功。

在系统中根据需要添加新的系统调用是修改内核的一种常用手段,通过本次实验,读

者应理解 linux 系统处理系统调用的流程以及增加系统调用的方法。


内容要求

(1)添加一个系统调用,实现对指定进程的 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。

(2)写一个简单的应用程序测试(1)中添加的系统调用。

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


实验准备

我使用的是美如画的 Deepin 15.5 64 位操作系统,与 Windows10 并存的双系统中。

使用的内核源码是从 The Linux Kernel Archives下载的教老版本的稳定内核 4.4.102。将其解压保存到了主目录下。

注意点:

  • 使用虚拟机的话建议不要吝啬存储空间,建议设置30G~50G大小。太小会导致没有空间存储编译后的文件,编译出现错误。
  • 尽量调高虚拟机的硬件配置,以减少编译所需要的时间。
  • 没必要选择过新的内核版本,会导致编译时间的增长,对于新版本的资料相对也比较少。
  • 如果对 Linux 系统的 boot 分区大小单独进行设置的话,建议选择 3G~5G 的大小,否则会导致新内核编译完成安装时提示 boot 分区大小不足,出现错误。


实验内容

(1)分配系统调用号,修改系统调用表

  • 查看系统调用表(./arch/x86/entry/syscalls/syscall_64.tbl)
  • 在这里我们需要选择一个未使用过的系统调用号进行分配,当前系统使用到325号,所以我们选择新添加的系统调用号为326号。

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

Linux系统调用服务例程的原型申明在文件 (./include/linux/syscalls.h) 中,我们可以在末尾添加如图所示的内容:

每个系统调用都对应一个内核服务例程来实现该系统调用的具体功能,其命名格式都是以“sys_”开头。

其中“asmlinkage”是一个必须的限定词,用于通知编译器仅从堆栈中提取该函数的参数,而不是从寄存器中,因为在执行服务例程之前系统已经将通过寄存器传递过来的参数值压入内核堆栈了。

(3)实现系统调用服务

下面为新调用的 mysetnice 编写服务例程 sys_musetnice, 通常添加在 sys.c 文件中,其路径为 (./kernel/sys.c )。

/*
 * my scall 
 * 添加一个系统调用,实现对指定进程的 nice 值的修改或读取功能,
 * 并返回进程最新的 nice 值及优先级 prio。
 * 参数含义:
 *     pid:进程 ID。
 *     flag:若值为 0,表示读取 nice 值;若值为 1,表示修改 nice 值。
 *     prio、nice:进程当前优先级及 nice 值。
 * 返回值:系统调用成功时返回 0,失败时返回错误码 EFAULT。
 */
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);
	priobef = task_prio(task);
	
	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;
}

这是一个简单的实现读取进程 nice 值和修改进行 nice 值的服务。当参数 flag 为 0 的时候读取 nice 值,并将数据从内核空间拷贝到用户空间。当 flag 为 1 的时候修改 nice 值为 nicevalue 的值。

在新版本的内核中,引入了宏“SYSCALL_DEFINEN(sname)”对服务例程原型进行了封装,其中的“N”是该系统调用所需要参数的个数, 在这里我们使用了 5 个参数所以是 DEFINE5。


内核编译

(1)清除残留的.config 和.o 文件

编译出错需要重新编译或不是第一次编译,都需要清除残留的.config 和.o 文件,方法是进入Linux内核所在的子目录, 执行以下命令:

# make mrproper

这里可能会提醒安装 ncurses 包,在 ubuntu 中 ncurses 库的名字是 libncurses5-dev,所

以安装命令是:

# apt-get install libncurses5-dev

安装完成后,再重新执行清楚编译结果的指令即可。

(2)配置内核

运行的命令是:

# make menuconfig

运行该命令过程中,可能会报错提示缺少一个套件 ncurses devel,那么你需要手动对其进行安装,安装方法同上安装 ncurses 包的过程。

在执行 make menuconfig 命令的时候会出现一个配置对话框,我们一般采用默认值:选择<save>保存配置信息,然后选择<exit>退出对话框。

(3)编译内核,生成启动映像文件

内核配置完后,编译内核,生成启动映像文件 bzlmage ,位于 (./arch/x86_64/boot/bzlmage) 中:

执行命令:

# make 

这里也许会提示你没有安装 openssl,安装的方法如下:

# apt-get install libssl-dev

内核编译时间可能较长,视硬件配置而定,一般情况为2小时左右,可以喝杯茶,看看小说放松放松再来。

(4)编译模块

执行命令:

# make modules

(5)安装内核

安装模块:

# make modules_install

安装内核:

# make install

(6)配置 grub 引导程序

只需要执行命令:

# update-grub2

就会自动修改 grub。

(7)重启系统

重启系统后,我们可以通过终端来查看新内核的版本。输入如下指令

uname -r

观察显示的内核版本是否与我们重新编译的内核版本一致。


编写用户态测试程序

到目前为止,一切看起来都十分美好。

下面,我们就要对我们直接写的系统调用服务进行检验了。如下是我编写的测试程序:

#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define __NR_mysetnice 326 //系统调用号

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 值参数表示的是与执行指令的优先权等级,等级范围为 -20 ~ 19。数值越小,等级越高。只有系统管理者才可以设置负数的等级。更多关于linux进程调度,优先级、进程nice值的讲解可以观看 linux进程调度,优先级、进程nice值 - CSDN博客

我们在测试程序中先后设置了 nice 值为 -5 和 -15 的情况,观察当前进程调度块的 nice 值和 prio 值的变化。显示结果如下:

这里我们没有在控制台显示 printk 所打印的内容。这个情况与你的操作系统本身有关,也许你的系统是可以显示的。如果你也和我一样不能显示 printk 打印的结果,可以使用如下的方法使其进行显示:Ubuntu下查看Printk的输出_Linux教程_Linux公社-Linux系统门户网站

打开另外的一个终端,用一个终端不停地监视并且打印输出当前系统的日志信息:

while true 
do 
    sudo dmesg -c 
    sleep 1 
done 

这样这个终端就会每1秒查看当前系统的日志并清空。

我的显示结果如下:


关于本次实验的全部代码可以查阅:shaonianruntu/OSlab


推荐阅读:

编辑于 2017-12-18

文章被以下专栏收录