哈工大操作系统实验3 系统调用(System Call)

哈工大操作系统实验3 系统调用(System Call)

该篇文章是哈工大操作系统实验3的完成笔记,其中包含了详细的步骤和相关代码,并有截图说明。实验内容我都成功通过了,但是因为内容较多,记录中难免会有疏忽,如有发现错误,欢迎大家留言和我联系。

实验内容

该实验的内容其实就是编写两个系统调用:

  • 第一个系统调用来记录我是谁,接收一个name参数,关键词 iam。
  • 第二个系统调用来显示第一个系统调用记录的name。关键词 whoami。

image

最后运行的结果如下图:

image

这两个系统调用功能非常简单,实验的目的主要是要了解系统调用的原理。

系统调用原理

用户程序调用系统内核库函数称为系统调用。关于系统调用原理,实验内容里解释的很详细,我这里针对其中的基本过程补充了一些说明和相关的文件。

基本过程

系统调用的基本过程:

  • 应用程序调用库函数(API);
  • API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
  • 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
  • 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
  • 中断处理函数返回到 API 中;
  • API 将 EAX 返回给应用程序。

这个基本过程咋一看难以理解,我顺着源码查看,画了一流程图方便理解。

image

一些说明:

  • 定义80中断处理函数:这个是在系统运行起来就处理好了,详细流程可以参考:main.c -> kernel/sched.c中的sched_init函数 -> include/asm/system.h中的set_system_gate函数,将0x80号中断的处理例程设置为 kernel/system_call.s 的 system_call例程。
  • include/unistd.h:该文件中定义了内核系统调用号和系统调用嵌入式汇编宏函数,参考《Linux内核完全注释》第11.14章节。
  • kernel/system_call.s:该文件实现系统调用(system_call)中断 int 0x80 的入口处理过程,参考《Linux内核完全注释》第5.6章节。
  • include/linux/sys.h:该文件列出了内核中所有系统调用函数的原型,以及系统调用函数指针表,参考《Linux内核完全注释》第11.30章节。
  • kernel目录:该目录放置内核进程调度、信号处理、系统调用等程序,参考《Linux内核完全注释》第2.8章节和第5章。最终要编写的两个系统调用实现就是放在这个目录

开发过程

知道基本过程后,那么实验的过程也就容易理解了。大体的过程分为两步:

  1. 内核实现:需要增加iam、whoami两个函数,并修改相关的地方;
  2. 用户程序测试:内核编写好后,编写用户程序 iam.c 和 whoami.c 进行测试。

当然在实验前我们要准备好相关源码,实验后进行测试评分、编写实验报告等。

实验前准备

恢复Linux0.11源码,这步非常简单,参照实验内容做即可。

# 删除原来的文件
$ cd ~/oslab
$ sudo rm -rf ./*

# 重新拷贝
$ cp -r /home/teacher/oslab/* ./

image

内核实现

既然用户程序要调用内核库函数,那么内核就需要先定义并实现这些库函数,涉及的相关地方也需要调整。

  1. include/unistd.h 增加两个系统调用编号72和73,从71接着开始。
// include/unistd.h 文件
#define __NR_setreuid	70
#define __NR_setregid	71

#define __NR_iam    72      // iam 系统调用号
#define __NR_whoami 73      // whoami 系统调用号

include/unistd.h 这个文件内核程序有用,用户程序也有用,只是本案中,因为内核程序并没有使用到这两个系统调用,所以这里不更改也行

关于内核程序也有使用系统调用的理解,可以参考实验的说明6.1章节。内容说明如下:linux-0.11 的 lib 目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。

具体的文件可以参考 init/main.c ,也引用了 include/unistd.h 这个文件。

  1. 修改系统调用总数。

在 kernel/system_call.s 中,_system_call例程是0x80的入口处理过程,在_system_call中,会先检测调用号是否超出系统调用总数(nr_system_calls),所以需要修改这个系统调用总数。

# kernel/system_call.s 文件中

# offsets within sigaction
sa_handler = 0
sa_mask = 4
sa_flags = 8
sa_restorer = 12

nr_system_calls = 74    # 系统调用总数,原先为72,增加2个就是74

_system_call例程中后面会通过 _sys_call_table 表格定位到具体的内核库函数进行后续的处理,接下来就要修改这个表。

# kernel/system_call.s 文件中

_system_call:	
	......
	call _sys_call_table(,%eax,4)	# 这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。对应的 sys_call_table 在 include/linux/sys.h 中,其中定义了一个包括 72 个系统调用 C 处理函数的地址数组表。
    ......
  1. 修改系统调用表(sys_call_table)。

sys_call_table 是一个函数指针数组的起始地址,它定义在 include/linux/sys.h 中。增加实验要求的系统调用,需要在这个函数表中增加两个函数引用: sys_iam 和 sys_whoami。当然该函数在 sys_call_table 数组中的位置必须和 __NR_xxxxxx 的值对应上。

同时还要仿照此文件中前面各个系统调用的写法,加上 extern int sys_iam(); 以及 extern int sys_whoami();

image

  1. 实现内核函数。

sys_call_table只是定义了内核库函数的索引表格,现在就要实现具体的库函数。

在 linux 0.11的设计中,库函数放在 kernel目录。在 kernel 目录增加 who.c 文件,编写 sys_iam 和 sys_whoami 两个接口。

#define __LIBRARY__      // 定义一个符号常量,见下行说明。
#include <unistd.h>      // Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。如果定义了__LIBRARY__,则还含系统调用号和内嵌汇编 syscall0()等。
#include <errno.h>       // 错误号头文件。包含系统中各种出错号。(Linus 从 minix 中引进的)。包含了 EINVAL 错误码的定义。
#include <string.h>      // 字符串头文件。主要定义了一些有关字符串操作的嵌入函数。其中包含了 strlen、strcpy 函数。
#include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。其中包含了 get_fs_byte 和 put_fs_byte 函数

char buffer[24]; // 声明一个缓冲区,用来保存用户程序输入的name,23个字符 +'\0' = 24

/*
 * 将name的内容拷贝到buffer,name的长度不超过23个字符。
 * 返回:拷贝的字符数。如果name的字符个数超过了23,则返回-1,并置errno为EINVAL。
 */
int sys_iam(const char *name)
{
    /*
        一开始我想直接用strlen计算name的长度,用strcpy复制name到buffer中。
        但是不能这样操作,因为name的位置在用户程序段,用户程序段需要用fs进行操作。
        strlen和strcpy都是默认用内核数据段ds和es进行操作。
    */
    // int len = strlen(name);
    // strcpy(buffer, name);

    /*
        直接打印name也是不行的,因为printk只能打印内核数据段内的数据,而name的位置是在用户程序段。
     */
    // printk("name=%s\n", name);

    /* 下面将name复制到tmp中 */
    int i;        // i用作字符串索引
    char tmp[25]; // 临时存储输入字符串,操作失败时不影响buffer,比buffer的长度多1,即可校验长度是否超出。
    for (i = 0; i < 25; i++)
    {
        tmp[i] = get_fs_byte(name + i); // 从用户程序内存取得数据,fs指向用户程序段,name是在用户程序段中的起始偏移位置
        if (tmp[i] == '\0')             // 字符串结束
            break;
    }
    printk("tmp=%s\n", tmp); // 调试打印tmp内容

    /* 判断输入的长度是否超过23 */
    int len = strlen(tmp); // tmp位于内核中,所以可以直接用strlen计算长度。
    if (len > 23)          // 字符长度大于23个
    {
        printk("The length of name is greater than 23!\n");
        return -EINVAL; // 置errno为EINVAL,返回“­-1”。具体实现见include/unistd.h中的_syscalln宏。
    }

    /* 将tmp复制到buffer中 */
    strcpy(buffer, tmp);
    printk("buffer=%s\n", buffer); // 调试打印buffer内容。不知道为什么这里打印不出来,但实际上buffer是有内容的。

    /* 返回长度 */
    return len;
}

/*
 * 将buffer拷贝到name指向的用户地址空间中,确保不会对name越界访存(name的大小由size说明)
 * 返回:拷贝的字符数。如果size小于需要的空间,则返回“­-1”,并置errno为EINVAL。
 */
int sys_whoami(char *name, unsigned int size)
{
    /* 先校验buffer长度是否超过size,size为name的长度 */
    int len = strlen(buffer);
    if (len > size)
    {
        return -EINVAL; // 置errno为EINVAL,返回“­-1”。具体实现见include/unistd.h中的_syscalln宏。
    }

    /* 把 buffer 输出至 name */
    int i;
    for (i = 0; i < size; i++)
    {
        put_fs_byte(buffer[i], name + i); // 将buffer逐个字节写入到用户程序段内name起始的位置。fs指向用户程序段,name是在用户程序段中的起始偏移位置
        if (buffer[i] == '\0')            // 字符串结束
            break;
    }

    return len;
}
  1. 修改kernal/Makefile文件。

上面增加了who.c的文件,在Makefile文件中就要增加这个文件的编译规则,这样在make all的时候就会自动编译链接这个文件了。修改参考下图:

image

  1. 重新编译内核并运行。

现在重新编译内核,进入 linux-0.11 目录:

# 当前的工作路径为 /home/shiyanlou/oslab/linux-0.11/

# 编译内核
$ make all 

# 执行 oslab 目录中的 run 脚本
$ ../run

如果出现错误啥的,那么说明我们的修改生效了,根据错误提示进行排查即可。如果一切正常,那么就OK了。

用户程序测试

内核好了,那么就要编写用户程序测试一下新编写的系统调用。

因为linux0.11启动后,进入shell界面,只有vi编辑器,加上早年的编辑器功能比较少,如果在shell环境下进行编写非常麻烦。推荐使用挂载Linux0.11文件系统的方法,在宿主机(Ubuntu)上编辑文件就方便多了。

如果感觉Ubuntu编写还不太方便,蓝桥的Ubuntu系统可以上传本地编写好的代码文件,上传统一放在 /home/shiyanlou/Code 这个文件里。

挂载Linux0.11文件系统的方式:可以参考实验1的文件交换章节,我把相关命令列在这里,方便使用。

# 当前的工作路径为 /home/shiyanlou/oslab/

# 启动挂载脚本
$ sudo ./mount-hdc

# 之后即可在 hdc 目录查看到Linux0.11文件

# 卸载
$ sudo umount hdc
  1. /usr/include/unistd.h 增加两个系统调用编号72和73,从71接着开始。
// include/unistd.h 文件
#define __NR_setreuid	70
#define __NR_setregid	71

#define __NR_iam    72      // iam 系统调用号
#define __NR_whoami 73      // whoami 系统调用号

用户测试程序会引用这个文件,发起iam和whoami系统调用。

  1. 编写iam.c 和 whoami.c 两个用户程序进行测试。
  • iam.c 用来测试 iam 系统调用。
  • whoami.c 用来测试 whoami 系统调用。

iam.c源码:

/* 用户程序测试iam系统调用 */
/* Linux0.11的用户端C语言程序,不能用 "//" 进行注释,会报如下错误:parse error before "/" */
#define __LIBRARY__ /* 定义一个符号常量,见下行说明。unistd.h文件中会用到。 */
#include <stdio.h>  /* 使用其中的 printf 打印字符串。 */
#include <unistd.h> /* Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。如果定义了__LIBRARY__,则还含系统调用号和内嵌汇编 syscall0()等。 */

/* _syscall1宏展开后是一个调用系统函数的函数,接收1个参数。参考 include/unistd.h 文件 */
_syscall1(int, iam, const char *, name);

int main(int argc, char **argv) /* argc表示参数个数。argv参数数组。如执行:./iam cjb,那么 argc=2, argv[0]=./iam、argv[1]=cjb */
{
    int result = 0;
    /* 判断参数个数,如果小于1,表示没有输入名称,则提示错误 */
    if (argc < 1)
    {
        printf("Not enough arguments!\n");
        return -1;
    }
    /* 通过系统调用告知内核我是谁,即第2个参数的内容。 */
    result = iam(argv[1]); /* result表示argv[1]的长度 */
    printf("The result is %d\n", result);
    return 0;
}

whoami.c文件:

#define __LIBRARY__     /* 定义一个符号常量,见下行说明。unistd.h文件中会用到。 */
#include <unistd.h>     /* Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。如果定义了__LIBRARY__,则还含系统调用号和内嵌汇编 syscall0()等。 */
#include <stdio.h>      /* 使用其中的 printf 打印字符串。 */

/* _syscall2是宏,宏展开后是一个调用系统函数的函数,接收2个参数。参考 include/unistd.h 文件 */
_syscall2(int, whoami, char *, name, unsigned int, size);

int main()
{
    int len = 0;
    char s[30]; /* 声明一个字符串,接收whoami返回的字符内容 */
    len = whoami(s, 30); /* 调用了_syscall2定义的whoami函数 */
    printf("Name is %s, the length of name is %d\n", s, len);
    return 0;
}
  1. 编译文件
# /usr/root 目录

$ gcc -o iam iam.c -Wall
$ gcc -o whoami whoami.c -Wall

一些说明:

  • -o:表示输出可执行文件,后面接可执行文件名;
  • -Wall:表示输出尽可能多的警告信息。
  1. 运行
# /usr/root 目录

$ ./iam QingYun
$ ./whoami 

运行结果:

image

如果能成功看到上图输出的内容,那么系统调用就成功了。恭喜,终于又在操作系统内核上更进了一步。

运行时碰到的一个错误,记录在这里。错误信息如下:

general protection:0000
EIP :0008:00008821
EFLAGS:00010206
ESP:0004:00004000
fs: 0010
base:10000000,limit:04000000
Pid:12,process nr:4 
f2 ae f7 d1 49 85 d2 78 06 39
Segmentation fault

排查情况:因为在内核库函数sys_iam里打印收到的name参数,而name参数是在用户程序段里、而对应的段指向在内核数据段,所以就报这个段错误了。

int sys_iam(const char *name)
{
    ... 
    /*
        直接打印name也是不行的,因为printk只能打印内核数据段内的数据,而name的位置是在用户程序段。
     */
    // printk("name=%s\n", name);
    ...
}

测试评分

测试脚本实验已经提供了,它的功能是测试 iam.c 和 whoami.c。将 testlab2.sh(在 /home/teacher 目录下) 拷贝到和 iam.c 和 whoiam.c 的同一个目录,并增加可执行权限,这步在宿主机(Ubuntu)上完成。

# 当前的工作路径为 /home/shiyanlou/oslab/

# 挂载文件系统
$ sudo ./mount-hdc 

# 复制 testlab2.sh 到和 iam.c 和 whoiam.c 的同一个目录
$ cp /home/teacher/testlab2.sh hdc/usr/root/

# 增加可执行权限
$ chmod +x hdc/usr/root/testlab2.sh

编译运行linux0.11,执行 testlab2.sh,发现居然是0分:

image

后来去查看 testlab2.sh 源码,才发现原来是 testlab2.sh 是去运行 iam 和 whoami 程序,然后判断 whoami 输出进行得分,前面写的 whoami.c 输出的内容不匹配,那么修改一下就可以了。

whoami.c 只要输出一个名字即可:

#define __LIBRARY__     /* 定义一个符号常量,见下行说明。unistd.h文件中会用到。 */
#include <unistd.h>     /* Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。如果定义了__LIBRARY__,则还含系统调用号和内嵌汇编 syscall0()等。 */
#include <stdio.h>      /* 使用其中的 printf 打印字符串。 */

/* _syscall2是宏,宏展开后是一个调用系统函数的函数,接收2个参数。参考 include/unistd.h 文件 */
_syscall2(int, whoami, char *, name, unsigned int, size);

int main()
{
    int len = 0;
    char s[30]; /* 声明一个字符串,接收whoami返回的字符内容 */
    len = whoami(s, 30); /* 调用了_syscall2定义的whoami函数 */
    printf("%s\n", s);  /* 就是这行输出只要输出一个名字就可以了 */
    return 0;
}

再次运行成功通过:

image

实验报告

  1. 从 Linux 0.11 现在的机制看,它的系统调用最多能传递几个参数?你能想出办法来扩大这个限制吗?

最大传递的参数:在Linux 0.11中,系统调用的参数主要通过寄存器来传递。从include/unistd.h可以看出,系统调用宏定义(如_syscall0、_syscall1、_syscall2、_syscall3)表明,系统调用可以无参数,或者有1个、2个、3个参数。这些参数分别通过寄存器ebx、ecx、edx来传递。因此,在Linux 0.11的当前机制下,如果不考虑系统调用编号和其他技术手段,系统调用最多能直接传递3个参数。

改进的方法:使用数据结构的方式。将需要传递的多个参数保存在一段自定义的数据结构中。将该用户态地址空间的首地址作为参数传递给系统调用。在系统调用内部,通过寄存器间接寻址的方式访问该数据结构,从而获取所有参数。

  1. 用文字简要描述向 Linux 0.11 添加一个系统调用 foo() 的步骤。

主要步骤如下:

  • 在 include/unistd.h 定义系统调用号: #define __NR_foo 72;
  • kernel/system_call.s 中最大系统调用数 + 1;
  • 修改系统调用表(sys_call_table),增加系统调用 foo 的地址索引;
  • 实现foo函数;
  • 编译内核并运行测试。

系统调用编写完后,还需要编写一个用户程序进行测试一下。

参考资料

  • 实验课本身的内容:这个翻来覆去多看几遍,结合《Linux内核完全注释》很多就理解了。
  • 赵炯博士的《Linux内核完全注释》:代码的完全注释,通过阅读代码了解了很多细节问题。

完。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晴空闲雲

感谢家人们的投喂

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值