System calls:系统调用
基础背景:操作系统必须满足三个要求:多路复用、隔离和交互。
Unix进程之间的许多交互形式都是通过文件描述符实现的
---------------------------------------------------------------------------------------------------------------------------------
RISC-V有三种CPU可以执行指令的模式:
机器模式(Machine Mode):在机器模式下执行的指令具有完全特权,机器模式主要用于配置计算机
用户模式(User Mode)
管理模式(Supervisor Mode):在管理模式下,CPU被允许执行特权指令
应用程序只能执行用户模式的指令(例如,数字相加等)被称为在用户空间中运行
处于管理模式下的软件可以执行特权指令,并被称为在内核空间中运行。
在内核空间(或管理模式)中运行的软件被称为内核。
用户态和核心态代表了程序执行时所处的两种不同的特权级别,而系统调用则是用户态程序与核心态程序之间的桥梁!!!!!!!!!!!
---------------------------------------------------------------------------------------------------------------------------------
宏内核(monolithic kernel):整个操作系统都驻留在内核中,这样所有系统调用的实现都以管理模式运行。
优点:以完全的硬件特权运行/操作系统的不同部分更容易合作
缺点:操作系统不同部分之间的接口通常很复杂
管理模式中的错误经常会导致内核失败。如果内核失败,计算机停止工作,因此所有应用程序也会失败。计算机必须重启才能再次使用。
微内核(microkernel):最大限度地减少在管理模式下运行的操作系统代码量,并在用户模式下执行大部分操作系统。
补充:单机微内核操作系统中几乎无一例外地都采用客户/服务器模式,将操作系统中最基本的部分放入内核中,而把操作系统的绝大部分功能都放在微内核外面的一组服务器(进程)中实现。
Xv6是作为一个宏内核实现的
---------------------------------------------------------------------------------------------------------------------------------
内核用来实现进程的机制包括用户/管理模式标志、地址空间和线程的时间切片。
一个进程最重要的内核状态片段是它的页表、内核栈区和运行状态
每个进程都有一个执行线程(或简称线程)来执行进程的指令
每个进程有两个栈区:一个用户栈区和一个内核栈区
进程的线程在主动使用它的用户栈和内核栈之间交替。内核栈是独立的(并且不受用户代码的保护),因此即使一个进程破坏了它的用户栈,内核依然可以正常运行。
----------------------------------------------------------------------------------------------两个例子:
调用系统调用:
1.用户程序准备调用exec:
2.执行ecall指令:
3.内核处理系统调用
4.执行sys_exec函数
5.返回用户程序:
6:错误处理:
----------------------------------------------------------------------------------------------
系统调用参数:
系统调用如何安全地处理用户空间传递给内核的参数,特别是当这些参数是内存地址(如指针)时。
系统调用参数传递:内核的陷阱处理代码(trap handler)会保存这些寄存器的值到当前进程的陷阱框架(trap frame)中,内核代码随后可以从陷阱框架中检索这些参数。
用户空间与内核空间:用户空间和内核空间是计算机内存中的两个不同区域,分别用于存放用户程序和操作系统内核的代码和数据
页表映射:页表是操作系统用来将虚拟内存地址映射到物理内存地址的数据结构。用户空间和内核空间有各自的页表,因此它们的虚拟地址到物理地址的映射是不同的
安全的数据传输:用户空间和内核空间之间传输数据:特殊的函数:用户提供的虚拟地址和内核缓冲区之间复制数据,同时确保不会违反内存保护规则。
---------------------------------------------------------------------------------------------------------------------------------
先放两个我遇到的问题:
1.使得ifconfig命令只出现了lo(本地环回)
注意:需要root权限!!!!!
//查看是否有ens33网卡
ifconfig -a
//启动网卡
ifconfig ens33 up
//再输入
dhclient
//最后输入
dhclient ens33
//检查
ifconfig
2.提交了修改,但是只提交了一半,于是be like:
您对下列文件的本地修改将被检出操作覆盖:
Makefile
grade-lab-util
请在切换分支前提交或贮藏您的修
Tips:暂时不想提交这些修改,但又不想丢失它们,可以使用贮藏命令将其保存起来。
于是我选择先git stash
恢复贮藏的修改可以使用git stash apply
命令
_____________________________________________________________________________
正文开始:
根据实验提示:
- 将
$U/_trace
添加到Makefile
的UPROGS
中 - 运行 make qemu , 你将看到编译器无法编译 user/trace.c ,因为系统调用的用户空间存根还不存在
- 将系统调用的原型添加到 user/user.h
第一步:int trace(int);
为什么是整型呢? if (trace(atoi(argv[1])) < 0)
解释:
argv[0]
通常是程序的名称,而argv[1]
、argv[2]
等则包含了传递给程序的命令行参数。argv[1]
被期望包含一个可以转换为整数的字符串,该整数将作为trace
函数的参数,atoi用于将字符串转换为整数,trace
函数执行其操作,并返回一个整数作为结果 - 将存根添加到 user/usys.pl
第二步:entry("trace");
usys.pl
文件是用于定义和实现系统调用用户空间接口的关键组件之一,开发者可以方便地添加新的系统调用,并在用户程序和内核之间建立必要的交互机制。 - 以及将系统调用号添加到 kernel/syscall.h 中 第三步:#define SYS_trace 22
以上三步属于模仿系统文件中的已经存在的代码语句,根据提示可以轻松拿下!!!!
user/usys.S文件中:li a7, SYS_fork
指令就是把 SYS_fork
的系统调用号放入 a7
寄存器,使用 ecall
指令进入系统内核, ecall
指令会跳转到 kernel/syscall.c 中 syscall
函数处执行
第四步:
syscalls[num]();
函数转到定义(F12)
把新增的 trace
系统调用添加到函数指针数组 *syscalls[]
上
在文件开头给内核态的系统调用 trace
加上声明
trace
系统调用应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。
第五步:在 kernel/proc.h 文件的 proc
结构体中,新添加一个变量 mask
在 kernel/sysproc.c 给出 sys_trace
函数的具体实现
uint64
sys_trace(void)
{
int mask;
// 取 a0 寄存器中的值返回给 mask
//argint函数用于安全地从用户态传递的参数中读取整数值
//因为直接读取用户态内存可能会受到恶意用户的影响
if(argint(0, &mask) < 0)
return -1;
// 把 mask 传给现有进程的 mask
//假设myproc()是一个返回当前进程控制块(PCB)指针的函数
//允许系统调用直接修改当前进程的状态,从而实现了动态调整跟踪掩码
myproc()->mask = mask;
return 0;
}
//当需要修改或扩展系统调用时,只需修改相应的函数即可,而无需更改用户态程序。
proc
结构体(见 kernel/proc.h )里的name
是表示与整个proc
结构体(即进程)相关联的名称。name
是进程的名称,用于调试或其他需要引用或标识进程的情况。要自己定义一个数组
第六步:系统调用名字一定要按顺序
系统调用号是从 1
开始,定义第一个为空字符串
static char *syscall_names[] = {
"", "fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat", "chdir",
"dup", "getpid", "sbrk", "sleep", "uptime",
"open", "write", "mknod", "unlink", "link",
"mkdir", "close", "trace"};
检查某个特定的标志(由num
指定)是否在p
指向的进程的mask
字段中被设置
1 << num
:这是一个左移操作
& p->mask
:这是一个按位与(AND)操作
(1 << num) & p->mask
的结果不为0(实际上是1 << num
的结果本身,因为只有在对应位都为1时,按位与操作才会在该位产生1),条件为真。
if((1 << num) & p->mask) {
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
}
第七步:在 kernel/proc.c 中 fork
函数调用时,添加子进程复制父进程的 mask
的代码
np->mask = p->mask;
运行:make qemu
$ trace 32 grep hello README,
$ trace 2147483647 grep hello README,
$ grep hello README,
$ trace 2 usertests forkforkfor
退出 xv6 ,运行单元测试检查结果是否正确:./grade-lab-syscall trace