Lab2系统调用
预备知识
系统调用
对于一个同时运行多个程序的系统来说,我们希望某一个应用程序出错时不会影响其他应用和操作系统,并且,操作系统应该可以对其进行清理。为了实现操作系统与应用程序之间的强隔离,应用程序不能修改甚至不能读取操作系统的数据结构和指令以及硬件,只能通过,并且程序不能访问其他进程的内存。
一方面来看,系统调用的存在规定了用户进程进入内核的具体方式,同时也屏蔽了访问硬件的细节而提供了统一的接口。
系统调用过程(以linux系统为例):
(1) 应用程序在用户态通过调用操作系统提供的API来发出系统调用请求,这些API通常由标准库提供,封装了底层的系统调用。
在用户态下执行陷阱指令( int 0x80 指令或者syscall)触发模式切换 ;
(2) CPU 被打断后,执行对应的中断处理函数 ,进入内核态。同时需要保存好用户态的相关寄存器,以便恢复 ;
(3) 通过寄存器传递调用参数和相应的系统调用号,内核验证参数正确性,并根据系统调用号调用对应的系统调用服务 ;
(4) 系统调用处理函数处理完成后,将返回值返回给用户程序
(5)恢复上下文,转换为用户态 ;
下面是有关的一些问题
- 上下文切换
对于一个进程来说,内核在创建进程的时候,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户态,一个内核栈,存在于内核态。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
更详细的堆栈知识:https://cloud.tencent.com/developer/article/2168474 - 参数和系统调用号的传递
用户程序将需要的参数传递给系统调用。这些参数可能包括文件描述符、缓冲区地址、数据长度、系统调用号等信息,具体取决于调用的系统服务。Linux最多允许向系统调用传递6个参数,通过寄存器完成,具体使用的寄存器与架构有关。这6个寄存器可能已经被使用,所以在传参前必须把当前寄存器的状态保存下来,待系统调用返回后再恢复。
系统调用掩码
在操作系统中,每个系统调用被分配一个唯一编号(如 SYS_read 对应编号 5)。在 trace 中,传入的参数是一个整数掩码,掩码的每一位对应一个系统调用号。例如,掩码 32 的二进制是 100000,对应第 5 位为 1,表示跟踪系统调用号为 5 的 read 操作。如果是trace 2147483647,其最低31位全为1,说明追踪1-31的所有系统调用
System call tracing
实现trace来跟踪系统调用,并打印相关信息。以所给例子为例
$ trace 32 grep hello README //追踪实现grep过程中调用的所有掩码为32(即read)的系统调用
3: syscall read -> 1023 //3表示运行grep的进程id,1023表示read的返回值
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
按照提示一步步来
// user/user.h: 各种系统调用在用户态下的声明文件,将系统函数提供给用户程序调用
...
int uptime(void);
int trace(int);//新增
//user/usys.pl 脚本文件,通过entry展开之后通过ecall实现新的系统调用,进入内核态
...
entry("uptime");
entry("trace");//新增
//kernel/syscall.h 添加trace的系统调用号
#define SYS_close 21
#define SYS_trace 22 //新增
//kernel/syscall.c 到达内核态统一系统调用处理函数 syscall(),所有系统调用都会跳到这里来处理。
//根据传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用。
//在这里全局声明系统调用处理函数,并将系统调用号与相应的处理函数对应
...
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void); //全局声明
...
[SYS_close] sys_close,
[SYS_trace] sys_trace, //系统调用号 对应 处理函数
//kernel/proc.h中为进程类proc结构增加一个新变量记录参数以记录要追踪的系统调用
...
//in struct proc
char name[16]; // Process name (debugging)
uint64 call_trace; //新增
//同时需要在 kernel/proc.c中修改
//in fort()
...
release(&np->lock);
np->call_trace = p->call_trace; //子进程需要继续追踪
//kernel/sysproc.c 到达 sys_trace() 函数,执行具体内核操作
//这里完成trace的具体实现,也就是根据传入参数设置追踪掩码
uint64
sys_trace(void)
{
int mask;
if(argint(0, &mask) < 0)
return -1;
myproc()->call_trace = mask;
return 0;
}
//kernel/syscall.c
//系统调用进入内核态后,首先都会集中来这里处理,因此在这里统一输出信息
//首先定义一个数组用来获得系统调用名,实际上就是系统调用号 到 系统调用名的映射数组
const char *syscall_names[] = {
[SYS_fork] "fork",
[SYS_exit] "exit",
[SYS_wait] "wait",
[SYS_pipe] "pipe",
[SYS_read] "read",
[SYS_kill] "kill",
[SYS_exec] "exec",
[SYS_fstat] "fstat",
[SYS_chdir] "chdir",
[SYS_dup] "dup",
[SYS_getpid] "getpid",
[SYS_sbrk] "sbrk",
[SYS_sleep] "sleep",
[SYS_uptime] "uptime",
[SYS_open] "open",
[SYS_write] "write",
[SYS_mknod] "mknod",
[SYS_unlink] "unlink",
[SYS_link] "link",
[SYS_mkdir] "mkdir",
[SYS_close] "close",
[SYS_trace] "trace",
};
void
syscall(void)
{
int num;
struct proc *p = myproc();
//a7寄存器保存了系统调用号
num = p->trapframe->a7;
//判断系统调用号是否有效,以及是否有对应的处理函数
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
//a0寄存器保存返回值
p->trapframe->a0 = syscalls[num]();
//如果当前进程启用了trace
if(( p->call_trace >> num) & 1){
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
最后在makefile文件中添加 $U/_trace\。最后运行测试
== Test trace 32 grep == trace 32 grep: OK (1.8s)
== Test trace all grep == trace all grep: OK (1.1s)
== Test trace nothing == trace nothing: OK (1.0s)
== Test trace children == trace children: OK (7.6s)
Sysinfo
添加一个系统调用,其传入一个指向结构sysinfo的指针,返回空闲的内存、以及已创建的进程数量。在 kernel/sysinfo.h 中已经声明好了结构体sysinfo,其中包含 freemem 空闲内存属性和 nproc 进程数量属性,我们需要把获取的数据赋值在其中,分别是以字节为单位的空闲内存数目,以及状态不为UNUSED的进程数。根据提示还需要返回给用户空间进行使用。使用测试程序sysinfotest进行测试
实现系统调用
首先完成系统调用的注册工作,过程和trace是一样的
// user/user.h: 用户空间的跳板函数
struct sysinfo; //声明结构体,使得用户空间也能使用这个结构体
int sysinfo(struct sysinfo*);
// user/usys.pl
entry("sysinfo");
// kernel/syscall.h 添加系统调用号
#define SYS_sysinfo 23
// kernel/syscall.c 添加系统调用号对应的处理函数
extern uint64 sys_sysinfo(void);
[SYS_sysinfo] sys_sysinfo,
实现获取空内存的字节数
根据kernel/kalloc.c中的代码来看,物理内存以4096字节作为内存页进行分配。空闲内存以链表的形式管理空闲内存页,内存页本身作为链表结点,kmem->freelist指向第一个空余内存页。因此可以通过遍历空闲内存链表的方式计算整个系统的空余内存。
// kernel/kalloc.c
uint64
get_freemem(void)
{
acquire(&kmem.lock); //加锁防止数据不一致
uint64 bytes = 0;
struct run *r = kmem.freelist;
while(r){
bytes += PGSIZE;
r = r->next;
}
release(&kmem.lock);
return bytes;
}
实现获取进程数
根据kernel/proc.c来看,所有的进程被保存在进程表proc[NPROC]里,而根据kernel/proc.h中proc结构体定义可以看到,进程的状态有 UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE五种。因此可以通过遍历进程表的方式来计数。
// kernel/proc.c
uint64
get_procnum(void)
{
uint64 num = 0;
struct proc* p;
for( p = proc; p < &proc[NPROC]; p++){
if(p->state != UNUSED)
num++;
}
return num;
}
实现sysinfo
最后将两个功能在sysinfo中实现,这里需要将内核空间中的数据拷贝到用户空间中。根据提示查看coyput()函数, 在kernel/vm.c中
// 从内核态拷贝到用户态
// 将len字节从src复制到给定页表中的虚拟地址dstva
// 成功则返回 0, 失败返回 -1
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
因此在这里内核空间就是src,根据其他代码的例子,页表可以通过进程结构体的pagetable获得
//别忘在defs.h中声明函数
// kernel/defs.h
// kalloc.c
uint64 et_procnum(void);
// proc.c
uint64 get_procnum(void)
// kernel/sysproc.c 系统调用处理函数
uint64
sys_sysinfo(void)
{
struct sysinfo info;
info.freemem = get_freemem();
info.nproc = get_procnum();
//用户地址
uint64 user_addr;
if(argaddr(0, &user_addr) < 0) //这里一直沿定义链看下去可以发现是使用了a0寄存器
return -1;
//执行拷贝
if( copyout(myproc()->pagetable, user_addr, (char*)&info, sizeof(info) ) <0 )
return -1;
return 0;
}
最后测试,顺利通过。
== Test sysinfotest == sysinfotest: OK (2.2s)

浙公网安备 33010602011771号