实验名称:实验6:编译内核及增加Linux系统调用

实验目的

  • 1、熟悉Linux编译内核的流程与方法
  • 2、熟悉系统调用的流程

实验内容

本次实验由两部分组成。
第一部分仅仅要求编译一个干净的内核且加载成功,并不需要对内核修改。
第二部分是修改已经编译成功的内核,为其增加新的系统调用,扩充系统服务,提供给用户使用。
实现系统调用psta,获取进程的若干信息。其原型如下:int psta(struct pinfo *buf);
参数buf指向一个缓冲区,用于存放进程信息。结构 pinfo定义如下:

struct pinfo {
int nice;/*进程的nice值*/
pid_t pid;/*进程ID*/
uid_t uid;/*进程拥有者的用户ID*/
};

系统调用成功时的返回值为0,系统调用失败时应返回错误码EFAULT,表示 buf 指向非法地址空间。错误码定义见内核头文件asm-generic/errnoasm-generic/errno-base.h。

实验环境

  1. VMware
  2. Fedora7


实验作业

一、Fedora下内核编译

第一步、下载内核

首先要决定编译哪个版本的内核,一般情况下,待编译的内核(以下一律称新内核)的版本不低于当前正在运行的内核版本,如果两者版本差距较大,则可能需要更新如 gcc,binutils和 modutils之类的编译工具。我们只需要采用Linux发行版所对应的内核版本即可,这样所有内核编译工具都是现成的,无须更新,无疑减少了不必要的麻烦。先查看当前环境下的内核版本号:
![]([object Object]&originHeight=252&originWidth=426&originalType=binary&ratio=1&status=done&style=none)
可知本机内核版本号为2.6.23.17,在http://www.kernel/org/pub/linux/kernel/v2.6下载相应的内核压缩包。
![]([object Object]&originHeight=858&originWidth=914&originalType=binary&ratio=1&status=done&style=none)本次实验采用2.6.21.tar.gz,下载到桌面之后,移动到/usr/src,并进行解压。
![]([object Object]&originHeight=192&originWidth=376&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=483&originWidth=631&originalType=binary&ratio=1&status=done&style=none)
解压完成后,进入目录观察:
![]([object Object]&originHeight=307&originWidth=605&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=577&originWidth=779&originalType=binary&ratio=1&status=done&style=none)

第二步、生成内核配置文件.config

Linux.内核代码非常庞大,适用于许多体系结构,包含了大量驱动程序。用户在生成内核的时候要根据实际情况进行配置。所有的配置将保存在内核代码树顶级目录下的一个名为.config 的配置文件中。生成正确的配置文件是内核编译过程中至关重要的一步。
配置文件可以从头开始生成,但是这没有必要。因为当前正在运行的内核已经有对应的配置文件,该文件在/boot目录下,利用它作为新内核配置文件的模板无疑是更好的做法。因此我们把该配置文件复制到/usr/src/linux-2.6.21.7目录下,命令如下:

make mrproper
cp /boot/config-`uname -r` ./.config
介绍:`uname -r`

uname 可显示电脑以及操作系统的相关信息。 -r或–release 显示操作系统的发行编号。 通过此命令便于我们进入相关的目录之下。
第一个命令“make mrproper”用来保证内核树是干净的,如果内核树已经编译过,该命令有效,如果内核树是第一次编译则可以省略该命令。
![]([object Object]&originHeight=83&originWidth=554&originalType=binary&ratio=1&status=done&style=none)
现在虽然有了模板,但是.config文件的配置并不一定囊括了新内核的所有编译选项(因为新内核可能是更新的版本,如2.6.23),可以使用下面的命令:

make oldconfig

![]([object Object]&originHeight=61&originWidth=639&originalType=binary&ratio=1&status=done&style=none)
该命令读取.config文件并根据新内核版本更新它。具体过程是这样的,该命令输出新内核所有的配置项,如果配置项已经在.config中有设置,则输出设置值;如果是新项,程序会停下来要求用户输入设置值,值的具体含义见附录C。用户输入值后程序继续运行直到所有配置项处理完毕。用户也可以使用如下命令:

make silentoldconfig

该命令的功能和“make oldconfig”相似,不过它不输出信息,除非是新选项需要用户输入的时候。到现在为止,生成的.config 文件就可以使用了,进入第3步。
![]([object Object]&originHeight=80&originWidth=423&originalType=binary&ratio=1&status=done&style=none)

第三步、编译和安装新内核

在编译内核之前,还可以定义用户自己的内核版本号,这样做是为了便于识别。在内核代码树的根目录下有文件Makefile
![]([object Object]&originHeight=425&originWidth=636&originalType=binary&ratio=1&status=done&style=none)
在编译内核之前,还可以定义用户自己的内核版本号,这样做是为了便于识别。在内核代码树的根目录下有文件Makefile,它的前4行是:

VERSION =2
PATCHLEVEL=6
SUBLEVEL =21
EXTRAVERSION =

把第4行改成EXTRAVERSION = .7-ywl,这样新内核版本号就是2.6.21.7-ywl。
![]([object Object]&originHeight=147&originWidth=285&originalType=binary&ratio=1&status=done&style=none)

然后执行下面三个命令:

make all
make modules_install
make install

make all将生成期望的内核映像及模块
![]([object Object]&originHeight=66&originWidth=363&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=332&originWidth=350&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=136&originWidth=500&originalType=binary&ratio=1&status=done&style=none)
make modules_install将安装模块到“默认目录/lib/modules/<内核版本号>”下面。
![]([object Object]&originHeight=174&originWidth=671&originalType=binary&ratio=1&status=done&style=none)
make install最终将内核映像等几个文件复制到“/boot”目录,并修改引导程序的配置以启用该新内核。
![]([object Object]&originHeight=115&originWidth=635&originalType=binary&ratio=1&status=done&style=none)


如果上述三个命令均执行成功,可以观察到引导程序grub的配置文件/boot/grub/menu.Ist的文件内容:
![]([object Object]&originHeight=444&originWidth=626&originalType=binary&ratio=1&status=done&style=none)
为了以后能直接操作菜单,把 hiddenmenu那一行注释掉(行的最前面加一个“#”字符即可)或删除,为了方便反应,我将菜单出现的时间调整为20。
![]([object Object]&originHeight=407&originWidth=603&originalType=binary&ratio=1&status=done&style=none)
重启,启动新内核
![]([object Object]&originHeight=83&originWidth=494&originalType=binary&ratio=1&status=done&style=none)

发现新内核在其中,选择并启用
![]([object Object]&originHeight=282&originWidth=598&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=566&originWidth=778&originalType=binary&ratio=1&status=done&style=none)
启动完毕,正常打开
![]([object Object]&originHeight=435&originWidth=638&originalType=binary&ratio=1&status=done&style=none)

二、添加past系统调用

若系统调用名为xxx,则内核对应的实现函数名一般为sys_xxx,据此我们把 psta系统调用对应的内核函数命名为 sys_psta。下面假定当前工作目录是/usr/src/linux-2.6.21.7,给出添加系统调用psta的基本过程。

(1)调整arch/i386/kernel/syscall_table.S

在文件 arch/i386/kernel/syscall_table.S 的尾部加上要新增的系统调用函数名称,如阴影行所示,注释中320表示它的系统调用号。
进入目录:
![]([object Object]&originHeight=184&originWidth=573&originalType=binary&ratio=1&status=done&style=none)
打开文件:
![]([object Object]&originHeight=49&originWidth=507&originalType=binary&ratio=1&status=done&style=none)
进行修改

![]([object Object]&originHeight=204&originWidth=476&originalType=binary&ratio=1&status=done&style=none)

(2)在include/linux目录下添加头文件psta.h

#ifndef _LINUX_PSTA_H
#define _LINUX_PSTA_H
struct pinfo
{
	int nice;
	pid_t pid;
	uid_t uid;
};
#endif

![]([object Object]&originHeight=124&originWidth=623&originalType=binary&ratio=1&status=done&style=none)

(3)在kernel目录下新建文件psta.c

在该文件中实现sys_psta函数

#include <linux/linkage.h>
#include <linux/psta.h>
#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/kernel.h>

asmlinkage int sys_psta( struct pinfo* buf)
{
	buf->pid=current->pid;
	buf->uid=current->uid;
	buf->nice=10;
	return 0; 
}

asmlinkage定义在文件 linux/linkage.h中,表示函数的参数通过栈传递,而不是通过寄存器,所有系统调用都遵循这种参数传递方式。
sys_psta的实现非常简单,无非就是把task_struct结构中的几个成员复制到用户态空间。值得一提的是,nice表示进程的优先级,取值范围为[-20,19],数值越低表示优先级越高。内核没有直接存储nice值,而是通过一个简单的变换后将它存放在 task_struct结构的static_prio成员中。两者之间的转换关系见下面几个宏(位于文件 kernel/sched.c中),其中MAX_RT_PRIO值为100。

#define NICE_TO_PRIO(nice)	 	(MAX_RT_PRIO +(nice)+ 20)
#define PRIO_TO_NICE(prio)		((prio) - MAX_RT_PRIO - 20)
#define TASK_NICE(p)			PRIO_TO_NICE((p)->static_prio)

(4)修改文件 kernel/Makefile

使psta.c能在内核编译时可见。Makefile 中有如下一行:

obj-y= sched.o fork.o exec_domain.o panic.o printk.o profile.o\

添加psta.o到该行中:

obj-y = psta.o sched.o fork.o exec_domain.o panic.o printk.o profile.o\

![]([object Object]&originHeight=506&originWidth=669&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=466&originWidth=631&originalType=binary&ratio=1&status=done&style=none)
值得说明的是,第2步中sys_psta的实现不一定要放在一个新文件中,例如文件kernel/sys.c也许就是添加sys_psta系统调用的合适位置,这样第3步就没有必要了。

(5)修改include/asm-i386/unistd.h

在include/asm-i386/unistd.h里面加上系统调用号的宏定义,在该文件中有如下几行:

#define_NR_epoll_pwait319
#ifdef _KERNEL__
#define NR_syscalls 320

可以看到,按照惯例系统调用号的宏名以“_NR_”开头,而其后跟着的数值则是系统调用号。此外,NR_syscalls表示的值应该是最大的系统调用号加一。所以修改后的内容如下:
![]([object Object]&originHeight=50&originWidth=331&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=452&originWidth=672&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=491&originWidth=666&originalType=binary&ratio=1&status=done&style=none)

(6)修改include/linux/syscalls.h

加上函数sys_psta的声明。在该文件的首部添加一行:
#include <linux/psta.h> 在该文件的最后一行“#endif”之前添加一行:
asmlinkage int sys_psta(struct pinfo *buf); ![]([object Object]&originHeight=78&originWidth=314&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=235&originWidth=336&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=346&originWidth=521&originalType=binary&ratio=1&status=done&style=none)

(7)重新编译内核。

这里特别要提醒初学者, 因为我们已经有了正确的.config,最好备份一份到别的目录下以防被删除。上一次编译内核时已经在内核目录下生成了许多中间文件,所以本次编译内核之前要删除这些文件。这可以使用如下命令:

make mrproper

该命令连.config文件都会删除,所以等命令执行完后需要把备份的.config文件复制回来,然后执行前面的第3步就可以了。因为新内核包含了用户自己的代码,所以很可能会在“ makeall”时因编译出错而停止,根据错误提示信息修改错误后,可以重复本步骤。
清空上次编译的中间文件:
![]([object Object]&originHeight=402&originWidth=616&originalType=binary&ratio=1&status=done&style=none)



![]([object Object]&originHeight=362&originWidth=525&originalType=binary&ratio=1&status=done&style=none)

三、简化实现

测试一

调用系统函数输出hello,
操作与之前一样:

测试程序:

#include<unistd.h>
#include<sys/syscall.h>
#include<assert.h>
#include<errno.h>

struct pinfo
{
	int nice;
	pid_t pid;
	uid_t uid;
};

int main(void)
{
	assert(6==syscall(__NR_write,1,"hello",6));
	return 0;
}

运行图:
![]([object Object]&originHeight=354&originWidth=580&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=102&originWidth=311&originalType=binary&ratio=1&status=done&style=none)

测试二

添加的系统调用会输出hello world,
操作与之前一样:
头文件:

#ifndef _LINUX_PSTA_H
#define _LINUX_PSTA_H
struct pinfo
{
	int nice;
	pid_t pid;
	uid_t uid;
};
#endif

.c文件

#include <linux/linkage.h>
#include <linux/psta.h>
#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/kernel.h>

asmlinkage int psta( struct pinfo* buf)
{
	//buf->pid=current->pid;
	//buf->uid=current->uid;
	//buf->nice=10;
    printf("hello world");
	return 0; 
}

测试程序:

#include<unistd.h>
#include<sys/syscall.h>
#include<assert.h>
#include<errno.h>

struct pinfo
{
	int nice;
	pid_t pid;
	uid_t uid;
};

int main(void)
{
	struct pinfo info;
	int ret;
	ret=syscall(320,&info);
	return 0;
}

运行图:

![]([object Object]&originHeight=82&originWidth=330&originalType=binary&ratio=1&status=done&style=none)

实验结果

实现系统调用psta,获取进程的若干信息。
实验代码:
头文件与之前相同
.c文件

#include <linux/linkage.h>
#include <linux/psta.h>
#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/kernel.h>

asmlinkage int sys_psta( struct pinfo* buf)
{
	buf->pid=current->pid;
	buf->uid=current->uid;
	buf->nice=10;
	return 0; 
}
测试程序:
#include<unistd.h>
#include<sys/syscall.h>
#include<assert.h>
#include<error.h>
#include<stdio.h>

struct pinfo{
	int nice;
	pid_t pid;
	uid_t uid;
};

int main(void)
{
	struct pinfo info;
	int ret;
	ret=syscall(320,&info);
	printf("%d\n%d\n%d\n",info.pid,info.uid,info.nice);
	return 0;

}

运行图:
![]([object Object]&originHeight=417&originWidth=603&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=282&originWidth=519&originalType=binary&ratio=1&status=done&style=none)
![]([object Object]&originHeight=401&originWidth=579&originalType=binary&ratio=1&status=done&style=none)
测试结果如下:
![]([object Object]&originHeight=214&originWidth=424&originalType=binary&ratio=1&status=done&style=none)
至此实验结束.

实验总结

这是操作系统的最后一次必选实验了,通过这次实验课我对操作系统的了解更深刻一步,在前几次的基础上我掌握了Linux文件系统的基本原理、结构和实现方法,并且掌握了Linux文件系统中文件的建立、打开、读/写、执行、属性等系统调用的使用,学会设计简单的文件系统并实现一组操作。学会利用系统调用完自己需要的功能。但还有很多需要改进的地方,比如写入文件的大小限制,还有容错性检验等等,还有很多不足之处。