(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验2-系统调用

操作系统实验2:系统调用


实验基本内容:

一、在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。

1.第一个系统调用是 iam(),完成的功能是将字符串参数 name 的内容拷贝到内核中保存下来。kernal/who.c 中实现。函数原型是int iam(const char * name);

2.第二个系统调用是 whoami(),其原型为:int whoami(char* name, unsigned int size);,它将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name 越界访存(name 的大小由 size 说明)

3.测试程序。运行添加过新系统调用的 Linux 0.11,在其环境下编写两个测试程序iam.cwhoami.c。最终返回正确的字符串。

二、完成老师提供的测试程序testlab2.ctestlab.sh

三、实验问题

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

实验问题2。用文字简要描述向 Linux 0.11 添加一个系统调用 foo() 的步骤。


零、本章架构

思维导图如下:(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验2-系统调用_第1张图片


一、实验内容1

1.在 Linux 0.11 上添加两个系统调用iamwhoami,并编写两个简单的应用程序测试它们。

应用程序如何调用系统调用:应调用系统库中为该系统编写的一个接口函数API,API并不能完成系统调用的真正功能,它要做的是调用真正的系统调用。

1.应用程序添加宏与头文件,以调用API

我们要做的在自己编写的函数中,也要加上这几句,才能调用系统API。比如lib/close.c中close()函数调用include/unistd.h下的_syscall1(int, close, int, fd)

/*
而在应用程序中,要有:
*/

/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__        

/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"    

/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);    

/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);    

因此,自己编写的两个函数代码如下:

// iam.c
#define __LIBRARY__
#include 
#include 
#include 
#include 

_syscall1(int,iam,char*,name);

int main(int argc,char* argv[]){
	if(argc<=1){
		printf("No input!\n");
		return -1;	
	}
	else{
		if(iam(argv[1])<0){
			printf("sys_call wrong!\n");
			return -1;	
		}
	}
	return 0;
}

//whoami.c
#define __LIBRARY__
#include 
#include 
#include 
#include 

_syscall2(int, whoami, char*,name,unsigned int,size);

int main(){
	int num;
	char a[128] = {0};

	num = whoami(a,80);
	if(num >= 0){
		printf("%s\n",a);
	}
	else{
		printf("sys_call exception");
		return -1;
	}
	return 0;
}

编写完成后,通过ubuntu宿主机与虚拟机的文件交换,交换方式如下,执行,才可将编写好的文件放到虚拟机中。路径推荐放在/home/shiyanlou/oslab/hdc/usr/root。可sys_call的应用程序编写完毕

补充:实验楼的文件交换。

在linux0.11没有运行时,通过cd ~/oslab/sudo ./mount-hdc。挂载内核的根文件系统镜像文件到ubuntu

然后通过cd /oslab/hdc进入目录读写文件。

读写完毕,结束挂载通过cd ~/oslab/sudo umount hdc。这时才可开启Bochs虚拟机,关闭虚拟机前用sync存盘。

2.在内核的include/unistd.h添加系统调用号

系统API会为我们进行宏展开:调用系统中断,系统调用号存%eax,其余寄存器存函数其他参数。它先将宏 __NR_close 存入 %eax,将参数fd存入 %ebx,然后进行 0x80 中断调用。调用返回后,从 %eax取出返回值,存入 __res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值。

//syscall1 函数原型
#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    : "=a" (__res) \
    : "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
    return (type) __res; \
errno = -__res; \
return -1; \
}
//宏展开后的_syscall1(int,close,int,fd)
int close(int fd) 
{ 
    long __res;      
    __asm__ volatile ("int $0x80" 
        : "=a" (__res) 
        : "0" (__NR_close),"b" ((long)(fd)));      
    if (__res >= 0)
        return (int) __res; 
    errno = -__res; 
    return -1; 
}

因此要实现自己的系统调用外,系统终端和寄存器存参已帮我们实现。我们还需要手动实现 __NR_close 。就是系统调用的编号,别急着停止挂载,在 /home/shiyanlou/oslab/hdc/include/unistd.h 中操作。操作结束后,可以短暂的停止挂载了。

#define __NR_close    6
/*
所以添加系统调用时需要修改include/unistd.h文件,
使其包含__NR_whoami和__NR_iam。
*/
//我的添加:
#define __NR_whoami   72
#define __NR_iam      73

3.修改系统调用表和调用总数

sys_call_table ,为系统调用表。在linux-0.11/kernel/system_call.s中,将nr_system_calls加1,修改系统调用总数

显然,sys_call_table 一定是一个函数指针数组的起始地址,它定义在 include/linux/sys.h 中:

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...

增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iamsys_whoami。当然该函数在 sys_call_table 数组中的位置必须和 __NR_xxxxxx的值对应上。

同时还要仿照此文件中前面各个系统调用的写法,加上:

extern int sys_whoami();
extern int sys_iam();

不然,编译会出错的。

4.实现内核函数

添加系统调用的最后一步,是在内核中实现函数 sys_iam()sys_whoami()

每个系统调用都有一个 sys_xxxxxx() 与之对应,它们都是我们学习和模仿的好对象。

比如在 fs/open.c 中的 sys_close(int fd)

int sys_close(unsigned int fd)
{
//    ……
    return (0);
}

它没有什么特别的,都是实实在在地做 close() 该做的事情。

所以只要自己创建一个文件:kernel/who.c,然后实现两个函数就万事大吉了。

注意:

1.头文件!

2.如何在用户态和核心态直接传递数据?

内核访问用户应用程序提供的地址,其实是应用程序所在地址空间的逻辑地址,由指针参数传递方式决定。这使得内核态访问到的永远是内核空间的数据,因此需要特殊工作。

内核态获取用户态的数据:内核由fs寄存器控制用户段,因此用 get_fs_byte() 获得一个字节的用户空间中的数据。

同理,用户态获取内核态的数据:用put_fs_byte(),也同样定义在 include/asm/segment.h 下。

3.errno

errno 是一种传统的错误代码返回机制。当一个函数调用出错时,通常会返回 -1 给调用者。但 -1 只能说明出错,不能说明错是什么。为解决此问题,全局变量 errno 登场了。错误值被存放到errno中,于是调用者就可以通过判断 errno来决定如何应对错误了。

各种系统对 errno的值的含义都有标准定义。Linux 下用“man errno”可以看到这些定义。

代码如下:

//who.c
#define __LIBRARY__
#include 
#include 
#include 

char a[80] = {0};
int sys_iam(const char* name){
	int i = 0;
	while( (get_fs_byte(name + i)) != '\0'){
		i++;
	}
	if( i >= 24){
		return -EINVAL;
	}
	printk("len(a):%d\n",i);
	i = 0;
	while( (get_fs_byte(name + i)) != '\0'){
		a[i] = get_fs_byte(name + i);
		i++;
	}
	return i;
}

int sys_whoami(char* name, unsigned int size){
	int i = 0;
	while( (a[i]) != '\0'){
		i++;
	}
	if( i > size){
		return -1;
	}
	i = 0;
	while( (a[i]) != '\0'){
		put_fs_byte(a[i],name + i);		
		i++;
	}
	return i;
}

5.修改Makefile文件

要想让我们添加的 kernel/who.c 可以和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。

Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是 kernel/Makefile。需要修改两处。

第一处:

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
        panic.o printk.o vsprintf.o sys.o exit.o \
        signal.o mktime.o

改为:

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
        panic.o printk.o vsprintf.o sys.o exit.o \
        signal.o mktime.o who.o

添加了 who.o

第二处:

### Dependencies:
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

改为:

### Dependencies:
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

添加了 who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h

Makefile 修改后,和往常一样 make all 就能自动把 who.c 加入到内核中了。

如果编译时提示 who.c 有错误,就说明修改生效了。所以,有意或无意地制造一两个错误也不完全是坏事,至少能证明 Makefile是对的。

6.编译运行程序

比如在 sys_iam() 中向终端 printk() 一些信息,让应用程序调用 iam(),从结果可以看出系统调用是否被真的调用到了。

可以直接在 Linux 0.11 环境下用 vi 编写(别忘了经常执行“sync”以确保内存缓冲区的数据写入磁盘),也可以在 Ubuntu 或 Windows 下编完后再传到 Linux 0.11 下。无论如何,最终都必须在 Linux 0.11 下编译。编译命令是:

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

gcc的 “-Wall” 参数是给出所有的编译警告信息,“-o” 参数指定生成的执行文件名是 iam,用下面命令运行它:

$ ./iam XXX

如果编译没报错,且如愿输出了你的信息,就说明你添加的系统调用生效了。否则,就还要继续调试,祝你好运!

实验结果:

(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验2-系统调用_第2张图片


二、实验内容2

将 testlab2.c(在 /home/teacher 目录下) 在修改过的 Linux 0.11 上编译运行,显示的结果即内核程序的得分。满分 50%。将脚本 testlab2.sh(在 /home/teacher 目录下) 在修改过的 Linux 0.11 上运行,显示的结果即应用程序的得分。满分 30%

Linux 的一大特色是可以编写功能强大的 shell 脚本,提高工作效率。本实验的部分评分工作由testlab2.c和脚本 testlab2.sh 完成。它的功能是测试 iam.cwhoami.c

$ gcc -o test testlab2.c -Wall

首先将 iam.cwhoami.c 在linux0.11下分别编译成 iamwhoami(如果实验1做过就不用了),然后将 testlab2.sh(在 /home/teacher 目录下) 拷贝到同一目录下,记得挂载,要移动到的是虚拟机目录。

运行:

$ ./test

结果如图:

(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验2-系统调用_第3张图片
8个pass,这50分拿到了。

再执行testlab2.sh

用下面命令为此脚本增加执行权限:

$ chmod +x testlab2.sh

然后运行之:

$ ./testlab2.sh

根据输出,可知 iam.cwhoami.c 的得分。

运行结果:

(浓缩+精华)哈工大-操作系统-MOOC-李治军教授-实验2-系统调用_第4张图片

最后一个死活过不去。看了一下脚本代码

#/bin/sh
 
string1="Sunner"
string2="Richard Stallman"
string3="This is a very very long string!"
 
score1=10
score2=10
score3=10
 
expected1="Sunner"
expected2="Richard Stallman"
expected3="Richard Stallman"
 
echo Testing string:$string1
./iam "$string1"
result=`./whoami`
if [ "$result" = "$expected1" ]; then
	echo PASS.
else
	score1=0
	echo FAILED.
fi
score=$score1
 
echo Testing string:$string2
./iam "$string2"
result=`./whoami`
if [ "$result" = "$expected2" ]; then
	echo PASS.
else
	score2=0
	echo FAILED.
fi
score=$score+$score2
 
echo Testing string:$string3
./iam "$string3"
result=`./whoami`
if [ "$result" = "$expected3" ]; then
	echo PASS.
else
	score3=0
	echo FAILED.
fi
score=$score+$score3
 
let "totalscore=$score"
echo Score: $score = $totalscore%

发现注意字符串过长的时候,不要覆盖之前保存的字符串,就是说所保存的字符串是之前存的字符串就好。

但我如果把数组改成全局变量的话会出现指针覆盖的情况,因此如果有更好的解决方案欢迎大家评论区探讨。

先拿90分了。


3.实验报告,满分20%
  • Q1:从 Linux 0.11 现在的机制看,它的系统调用最多能传递几个参数?你能想出办法来扩大这个限制吗?
    • 最多传递8个参数。从_syscall3(type,name,atype,a,btype,b,ctype,c)可看出。
    • 我觉得可以通过向寄存器传递逻辑地址,然后用函数get_fs_byte找到逻辑地址对应的真实地址解决这一问题
  • Q2:用文字简要描述向 Linux 0.11 添加一个系统调用 foo() 的步骤
    • 应用程序调用库函数foo(API);
    • API 将系统调用号存入 %eax,把函数参数存给其他通用寄存器,然后通过中断调用使系统进入内核态;
    • 内核中的中断处理函数根据系统调用号,调用对应的内核函数sys_foo(系统调用);
    • 系统调用完成相应功能,将返回值存入 %eax,返回到中断处理函数;
    • 中断处理函数返回到 API 中,API 将 %eax 返回给应用程序。

最后补番外链接:
1.实验楼(操作系统原理与实践)
https://www.shiyanlou.com/courses/115
2.网易云课堂:哈尔滨工业大学,国家级精品课程,操作系统
https://mooc.study.163.com/course/1000002004#/info
3.推荐markdown神器,本文由此写成
https://typora.io/
(完)

你可能感兴趣的:(操作系统)