实验3— 系统调用
实验目的
- 建立对系统调用接口的深入认识;
- 掌握系统调用的基本过程;
- 能完成系统调用的全面控制;
- 为后续实验做准备。
实验内容
此次实验的基本内容是:在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。
(1)iam()
第一个系统调用是 iam()
,其原型为:
int iam(const char * name);
完成的功能是将字符串参数 name
的内容拷贝到内核中保存下来。要求 name
的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name
的字符个数超过了 23,则返回 “-1”,并置 errno
为 EINVAL
。
在 kernal/who.c
中实现此系统调用。
(2)whoami()
第二个系统调用是 whoami()
,其原型为:
int whoami(char* name, unsigned int size);
它将内核中由iam()
保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name
越界访存(name
的大小由 size
说明)。返回值是拷贝的字符数。如果 size
小于需要的空间,则返回“-1”,并置 ``errno 为 EINVAL
。
也是在 kernal/who.c
中实现。
(3)测试程序
运行添加过新系统调用的 Linux 0.11
,在其环境下编写两个测试程序iam.c
和 whoami.c
。最终的运行结果是:
$ ./iam lizhijun
$ ./whoami
lizhijun
实验过程
操作系统实现系统调用的基本过程:
- 应用程序调用库函数(API);
- API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
- 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
- 中断处理函数返回到 API 中;
- API 将 EAX 返回给应用程序。
1.在include/unistd.h设置系统调用号
2.修改了系统调用号之后,系统调用号数增加了2个,总共为74个,因此还需要在kernel/system_call.s中和include/linux/sys.h中修改有关系统调用的代码
3.在kernel下建立who.c,并编写who.c,完成whoami和iam的系统调用函数
#include<string.h>
#include<errno.h>
#include<asm/segment.h>
char _name[30]; //_name用来保存iam()中的name
int sys_whoami(char* name,unsigned int size){
int len=strlen(_name);//获得_name长度
printk("%d===>%s\n",len,_name);//打印_name长度及内容
//如果 `size `小于需要的空间,则返回“-1”,并置 ``errno 为 `EINVAL`。
if(size<len){
errno=EINVAL;
return -1;
}else{
//否则返回拷贝字符数
int i=0;
while(i<len){
//逐字节保存_name到name数组中,将内核中由` iam() `保存的名字拷贝到 name 指向的用户地址空间中
puts_fs_byte(_name[i],name+i);
i++;
}
}
return len;
}
int sys_iam(const char* name){
char str[30];//定义一个数组str
int i=0;
for(i=0;i<=30&&name[i];i++){
//逐字节获得用户空间的内容
str[i]=get_fs_byte(name+i);
}
//如果` name` 的字符个数超过了 23,则返回 “-1”,并置 `errno` 为 `EINVAL`
if(i>=23){
errno=EINVAL;
return -1;
}else{
//调用strcpy 把用户空间的数据拷贝到_name中,供whoami()使用
strcpy(_name,str);
}
return i;
}
4.修改kernel/Makefile
5.在oslab目录下编写用户程序whoami.c和iam.c来调用刚刚设置的系统调用
iam.c
#define __LIBRARY__
#include<errno.h>
#include<asm/segment.h>
#include<unistd.h>
#include<linux/kernel.h>
_syscall1(int, iam, const char*, name);
int main(int argc,const char*argv[]){
iam(argv[1]);
return 0;
}
whoami.c
#define __LIBRARY__
#include<errno.h>
#include<asm/segment.h>
#include<unistd.h>
#include<linux/kernel.h>
#include<stdio.h>
_syscall2(int, whoami,char*,name,unsigned int,size);
int main(int argc,const char*argv[]){
char name[40];
whoami(name,24);
printf("%s\n",name);
return 0;
}
挂载hdc
在oslab目录下执行sudo ./mount-hdc
执行cp whoami.c iam.c ./hdc/usr/root
,实现共享文件
接着卸载这个hdc sudo umount hdc
接着运行./run
脚本,调出bochs调试器,执行以下两句代码
gcc -o whoami whoami.c
gcc -o iam iam.c
发现出错
于是在hdc/usr/include/unistd.h中查看,unistd是否同步
于是在里面增加这两个系统调用号,在运行
小结
系统调用实质就是函数调用,只是调用的函数是系统函数,处于内核态而已。用户在调用系统调用时会向内核传递一个系统调用号,然后系统调用处理程序通过此号从系统调用表中找到相应的内核函数执行(系统调用服务例程),最后返回。
系统调用号
为了唯一的标识每一个系统调用,Linux为每-一个系统调用定义了一个唯一的编号,此编号称为系统调用号。系统调用号的另一个目的是作为系统调用表的下标,当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。系统调用号相当关键,一旦分配好就不能再有任何改变
### 系统调用表
为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统调用表,这个表存放在sys_ call_table 数组中,它是一个函数指针数组,每一个函数指针都指向其系统调用的封装例程,有NR_syscalls个表项,第n个表项包含系统调用号为n的服务例程的地址。
每一个系统调用foo()在内核态都有一个对应的内核函数sys_ foo(),这个内核函数就是系统调用foo()的实现,也就是说在用户态调用foo(),最终会由内核函数sys_ foo()为用户服务,这里的sys_foo()就是系统调用服务例程。
提问
-
从 Linux 0.11 现在的机制看,它的系统调用最多能传递几个参数?你能想出办法来扩大这个限制吗?
从Linux0.11现在的机制来看,最多只能传递3个参数,因为通过查看include/unistd.h,可以看到
#define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name)); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ } #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; \ } #define _syscall2(type,name,atype,a,btype,b) \ type name(atype a,btype b) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b))); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ } #define _syscall3(type,name,atype,a,btype,b,ctype,c) \ type name(atype a,btype b,ctype c) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \ if (__res>=0) \ return (type) __res; \ errno=-__res; \ return -1; \ }
在当前Linux版本中定义了这4个宏,最多只有三个参数,其中EAX用于保存系统调用号,EBX、ECX、EDX用于保存参数。
解决办法:
- 结构体参数:将所有的参数封装到一个结构体中,通过指针来传递结构体的地址作为参数。
- 嵌套调用:将一个系统调用作为另一个系统调用的参数,在内部系统调用中获取外部系统调用的参数。
-
用文字简要描述向 Linux 0.11 添加一个系统调用 foo() 的步骤
1.首先在include/unistd.h
中添加系统调用号 #define __NR_foo num
2.在kernel/system_call.s
中修改系统调用总个数
3.在include/linux/sys.h
中修改系统调用表,把sys_foo添加到表后面
4.实现sys_foo()的系统调用服务例程,其路径为kernel/foo.c
5.重新编译内核
6.编写用户程序,调用foo()。