本lab2 主要是为了熟悉操作系统的调用流程,整个两个实验做下来,大体对这个流程有了一知半解。首先给出系统调用的整个流程说明:
步骤1 用户空间发起系统调用:通常通过库函数(常用的系统调用函数)或者直接使用软件中断指令(比如int syscall指令等),这会调用usys.c中相应的函数(封装了系统调用的细节,比如系统调用编号和必要)
步骤2. 设置系统调用环境:usys.c
中的函数根据不同的操作系统和硬件平台,通过设置寄存器来传递系统调用编号和参数。这些函数还会执行一个特定的指令(比如syscall
指令,在x86-64架构上)来触发操作系统的中断处理。执行出发系统调用的指令后,CPU会产生一个中断,操作系统的中断处理程序接管控制权,使得处理器从用户模式切换到内核模式,并开始执行内核的中断处理程序;
步骤3. 内核中断处理程序进入syscall函数:中断处理程序处理中断,并最终调用syscall
函数。在syscall
函数中,首先会获取当前执行的进程结构体proc(使用myproc()函数,该结构体保存了进程的状态和相关信息);
步骤4. 提取系统调用标号:syscall
函数根据中断时保存的进程状态(trapframe
)来确定发起的系统调用编号(num
)。在RISC-V体系结构中,系统调用编号通常保存在a7
寄存器中,这里通过p->trapframe->a7
获取。
步骤5. 定位到具体的系统调用处理函数(sys_xxx): 一旦获得系统调用编号,syscall
函数查找对应的处理函数。这通常通过一个函数指针数组(如syscalls
数组)实现,数组索引与系统调用编号对应。syscall
函数检查编号有效性,并通过数组索引定位到具体的sys_xxx
函数。其中,这些sys_xxx服务函数通常根据它们管理的资源类型,分散于不同的文件中。比如syspro.c 中的sys_xxx处理与进程相关的调用,而sysfile.c 中的sys_xxx则处理与文件相关的调用。
步骤6. 执行具体的系统调用函数:syscall
函数调用定位到的sys_xxx
函数。这个函数是实际处理系统调用请求的地方。例如,trace
系统调用有对应的sys_trace
函数处理进程请求。
步骤7. 返回值和跟踪输出:sys_xxx
函数执行完毕后,其返回值存入a0
寄存器,以便返回给用户空间程序。如果当前进程的跟踪掩码(trace_mask
)表明需要跟踪这个系统调用,syscall
函数会打印出相关信息。
步骤8. 控制权返回用户空间:系统调用处理完成后,syscall
函数返回,最终控制权也返回给用户空间程序。程序可以通过检查a0
寄存器的值来获取系统调用的返回结果。
根据以上流程,即可以进行实验
1. trace
在内核中实现系统调用之前:
①在Makefile的UPROGS中添加$U/_trace,步骤一
②在user/user.h 添加: int trace(int); //申明,步骤一
③在 user/usys.pl 中添加:entry(“trace”); 步骤二
④在 kernel/syscall.h 中添加:#define SYS_trace 22 步骤四
在内核中实现系统调用之后:
①在kernel/sysproc.c中添加一个sys_trace()函数:
首先在kernel/proc.h 中的proc结构体(记录每个进程状态的结构体)中添加:int trace_mask; 步骤三
然后编写sys_trace函数,也就是给trace_mask赋值
// trace系统调用所执行的函数
uint64
sys_trace(void)
{
int mask; //存储从用户传入的追踪掩码
if(argint(0, &mask) < 0) //获取第一个(索引为0)系统调用参数,并将其值存储到mask变量中,小于0则代表获取参数失败
return -1;
myproc()->trace_mask = mask; //修改进程的属性
return 0;
}
其中,mask
变量中包含的是一个整数值,这个整数值的每一位都可以代表一个不同的系统调用是否应该被追踪。通过将特定的位设置为1,你可以指示操作系统追踪对应编号的系统调用。因此,通过一次sys_trace
系统调用并传递一个合适的mask
参数,你可以控制多个不同系统调用的追踪行为。
②修改syscall.c 中的syscall函数以打印跟踪输出
void
syscall(void)
{
int num;
struct proc *p = myproc(); //获取当前正在执行的进程的proc结构体指针
char *syscall_name[23] = {"fork", "exit", "wait", "pipe", "read",
"kill", "exec", "fstat", "chdir", "dup",
"getpid", "sbrk", "sleep", "uptime", "open",
"write", "mknod", "unlink", "link", "mkdir",
"close", "trace"};
num = p->trapframe->a7; //读取系统调用编号
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num](); // 通过调用对应的系统调用处理函数来执行系统调用。处理函数的返回值存储在 a0 寄存器中,这样当控制权返回给用户空间时,程序可以读取返回值。
if((1 << num) & p->trace_mask)
printf("%d: syscall %s -> %d\n",p->pid, syscall_name[num-1], p->trapframe->a0);
//if(num == 23)
// printf("sysinfo is called\n");
} else { //系统编号不在有效范围内
printf("%d %s: unknown sys call %d\n", p->pid, p->name, num);
p->trapframe->a0 = -1;
}
2. sysinfo
在内核中实现系统调用之前:
①在Makefile的UPROGS中添加$U/_sysinfotest,步骤一
②在user/user.h 添加: struct sysinfo; int sysinfo(struct sysinfo *); //申明,步骤一
③在 user/usys.pl 中添加:entry(“sysinfo”); 步骤二
④在 kernel/syscall.h 中添加:#define SYS_sysinfo 23 步骤四
在内核中实现系统调用之后:
①在kernel/kalloc.c中添加函数freebytes:获得空闲内存量
②在kernel/proc.c中添加获取进程数函数procnum
③编写sys_sysinfo系统调用函数
④在syscall.c中添加新的系统调用
一些Qs:
1. 为什么在fork()
中复制追踪掩码?
当一个进程调用fork()
创建子进程时,它通常希望子进程继承父进程的许多属性,例如打开的文件描述符、环境变量、程序计数器状态等。如果父进程设置了追踪掩码以追踪特定的系统调用,这个行为通常也需要被子进程继承。因此,将追踪掩码从父进程复制到子进程是为了保持行为的一致性,确保任何关于父进程的追踪决定也适用于子进程。
在实现系统调用追踪的情况下,这一步是重要的,因为它确保了父进程设置的追踪配置能传递给子进程,这对于调试复杂的多进程应用程序来说是很有用的。如果你在父进程中启动了追踪,然后父进程调用了fork()
,你很可能希望自动地在子进程中继续这个追踪,而不是手动在每个子进程中重新设置。
2. 为什么打印语句必须放在syscall()
中?
关于在syscall()
函数中打印系统调用追踪信息的问题,我们需要理解系统调用处理的流程。在操作系统中,syscall()
函数是用来处理所有系统调用的通用入口。当程序执行一个系统调用时,控制权会转移到内核模式,syscall()
函数会被触发,然后根据系统调用编号调用相应的处理函数。
打印追踪信息需要在系统调用编号已经确定,且相应的处理函数已经被调用后进行。sys_trace()
函数仅仅是设置追踪掩码的函数,它并不知道何时其他系统调用会发生。因此,打印追踪信息的逻辑需要放在syscall()
中,因为每次系统调用都会经过这里,且此时我们已经具备了所有必要的信息(如调用的系统调用编号、追踪掩码、进程ID等)来决定是否打印追踪信息。
在你提供的syscall()
函数示例中,打印语句检查当前进程的追踪掩码,并且当系统调用编号对应的位在掩码中被设置时,打印出相应的追踪信息。这样确保了每次发生系统调用时,都可以根据追踪掩码决定是否进行追踪打印。
总结一下,复制追踪掩码在fork()
中进行是为了在父进程和子进程之间保持追踪行为的一致性,而追踪信息的打印应该在syscall()
中进行,这样可以在每次系统调用发生时根据追踪掩码来打印相应信息。
参考阅读: