lab2_syscall
系统调用的用户空间代码user/user.h和user/usys.pl
user.h xv6提供的system call
点击查看代码
// system calls
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
点击查看代码
// ulib.c
int stat(const char*, struct stat*);
char* strcpy(char*, const char*);
void *memmove(void*, const void*, int);
char* strchr(const char*, char c);
int strcmp(const char*, const char*);
void fprintf(int, const char*, ...);
void printf(const char*, ...);
char* gets(char*, int max);
uint strlen(const char*);
void* memset(void*, int, uint);
void* malloc(uint);
void free(void*);
int atoi(const char*);
int memcmp(const void *, const void *, uint);
void *memcpy(void *, const void *, uint);
对于系统调用和标准库的区别,从以下几个方面
- 功能层次与来源
系统调用
功能层次:系统调用是操作系统提供给用户程序的接口,是用户程序与操作系统内核进行交互的桥梁。它们直接与操作系统内核进行通信,用于请求内核提供各种底层服务,如进程管理、文件系统操作、设备驱动访问等。
来源:由操作系统内核实现,是操作系统的一部分。不同的操作系统(如 Linux、Windows、macOS 等)提供的系统调用可能有所不同,即使功能类似,其调用方式和参数也可能存在差异。
标准库函数
功能层次:标准库函数是对系统调用和一些通用算法的封装,位于用户空间。它们为程序员提供了更方便、更高级的编程接口,用于实现一些常见的编程任务,如字符串处理、内存管理、输入输出等。
来源:通常由编程语言的标准库提供,例如 C 语言的标准库(如 GNU C Library,即 glibc)。标准库函数的实现不依赖于特定的操作系统,具有较好的可移植性。 - 调用方式与开销
系统调用
调用方式:系统调用的调用过程涉及用户态到内核态的切换。当用户程序调用系统调用时,会触发一个中断或异常,使 CPU 从用户态切换到内核态,然后执行内核中的相应代码。执行完毕后,再从内核态切换回用户态。
开销:由于涉及状态切换,系统调用的开销相对较大。频繁的系统调用会导致性能下降,因此在编写程序时应尽量减少不必要的系统调用。
标准库函数
调用方式:标准库函数的调用是在用户态下进行的,不涉及状态切换。程序直接调用标准库函数的代码,执行完毕后继续在用户态下运行。
开销:标准库函数的调用开销相对较小,因为不需要进行状态切换。但是,有些标准库函数可能会在内部调用系统调用,例如 printf 函数在输出时可能会调用 write 系统调用。 - 错误处理与返回值
系统调用
错误处理:系统调用通常通过返回值来表示调用结果。如果调用成功,返回一个非负的值(如文件描述符、进程 ID 等);如果调用失败,返回 -1,并设置全局变量 errno 来指示具体的错误类型。程序员需要检查返回值并根据 errno 进行相应的错误处理。
返回值:返回值通常具有特定的含义,与操作系统的底层机制相关。例如,open 系统调用返回一个文件描述符,用于后续的文件操作。
标准库函数
错误处理:标准库函数的错误处理方式因函数而异。有些函数通过返回值来表示错误,如 strcmp 函数返回 0 表示字符串相等,非 0 表示不相等;有些函数会设置全局变量或通过指针参数返回错误信息。
返回值:返回值通常是为了方便程序员使用,与具体的编程任务相关。例如,strlen 函数返回字符串的长度。 - 可移植性
系统调用
不同的操作系统提供的系统调用接口和功能可能存在差异,因此系统调用的可移植性较差。如果要编写跨平台的程序,需要针对不同的操作系统进行不同的处理。
标准库函数
标准库函数遵循编程语言的标准规范,具有较好的可移植性。只要使用相同的编程语言和标准库,程序可以在不同的操作系统上运行而无需进行太多修改。
user/usys.pl
点击查看代码
#!/usr/bin/perl -w
# Generate usys.S, the stubs for syscalls.
print "# generated by usys.pl - do not edit\n";
print "#include \"kernel/syscall.h\"\n";
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
entry("fork");
entry("exit");
entry("wait");
entry("pipe");
entry("read");
entry("write");
entry("close");
entry("kill");
entry("exec");
entry("open");
entry("mknod");
entry("unlink");
entry("fstat");
entry("link");
entry("mkdir");
entry("chdir");
entry("dup");
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
entry("trace");
关于stat和fstat
// stat 函数
int stat(const char *pathname, struct stat *statbuf);
pathname:这是一个指向以空字符结尾的字符串的指针,代表要获取状态信息的文件路径名,可以是绝对路径,也可以是相对路径。
statbuf:这是一个指向 struct stat 结构体的指针,函数会把获取到的文件状态信息填充到这个结构体中
// fstat 函数 系统调用
int fstat(int fd, struct stat *statbuf);
fd:这是一个文件描述符,它标识了要获取状态信息的文件。文件描述符通常是通过 open、pipe、socket 等函数返回的。
statbuf:同样是一个指向 struct stat 结构体的指针,用于存储获取到的文件状态信息。
若调用成功,返回 0。
若调用失败,返回 -1,并且会设置 errno 来指示具体的错误原因。
这两个都将内容存在stat里面
struct stat {
dev_t st_dev; /* 文件所在设备的 ID /
ino_t st_ino; / 文件的 inode 编号 /
mode_t st_mode; / 文件的类型和权限 /
nlink_t st_nlink; / 文件的硬链接数 /
uid_t st_uid; / 文件所有者的用户 ID /
gid_t st_gid; / 文件所有者的组 ID /
dev_t st_rdev; / 设备文件的设备 ID /
off_t st_size; / 文件的大小(字节) /
blksize_t st_blksize; / 文件系统块的大小 /
blkcnt_t st_blocks; / 文件占用的块数 /
time_t st_atime; / 文件的最后访问时间 /
time_t st_mtime; / 文件的最后修改时间 /
time_t st_ctime; / 文件状态的最后改变时间 */
};
stat 函数适用于通过文件路径名来获取文件状态信息的场景。
fstat 函数适用于已经有文件描述符,需要获取对应文件状态信息的场景
内核空间syscall
点击查看代码
// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_read 5
#define SYS_kill 6
#define SYS_exec 7
#define SYS_fstat 8
#define SYS_chdir 9
#define SYS_dup 10
#define SYS_getpid 11
#define SYS_sbrk 12
#define SYS_sleep 13
#define SYS_uptime 14
#define SYS_open 15
#define SYS_write 16
#define SYS_mknod 17
#define SYS_unlink 18
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
syscall.c函数
点击查看代码
// Fetch the uint64 at addr from the current process.
int
fetchaddr(uint64 addr, uint64 *ip)
{
struct proc *p = myproc();
if(addr >= p->sz || addr+sizeof(uint64) > p->sz)
return -1;
if(copyin(p->pagetable, (char *)ip, addr, sizeof(*ip)) != 0)
return -1;
return 0;
}
点击查看代码
// Fetch the nul-terminated string at addr from the current process.
// Returns length of string, not including nul, or -1 for error.
int
fetchstr(uint64 addr, char *buf, int max)
{
struct proc *p = myproc();
int err = copyinstr(p->pagetable, buf, addr, max);
if(err < 0)
return err;
return strlen(buf);
}
点击查看代码
static uint64
argraw(int n)
{
struct proc *p = myproc();
switch (n) {
case 0:
return p->trapframe->a0;
case 1:
return p->trapframe->a1;
case 2:
return p->trapframe->a2;
case 3:
return p->trapframe->a3;
case 4:
return p->trapframe->a4;
case 5:
return p->trapframe->a5;
}
panic("argraw");
return -1;
}
点击查看代码
// Fetch the nth 32-bit system call argument.
int
argint(int n, int *ip)
{
*ip = argraw(n);
return 0;
}
从系统调用的参数中获取第 n 个 32 位整数参数,并将其存储到 ip 所指向的内存位置
// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
int
argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
return 0;
}
和上面不同点是取了64位
int
argstr(int n, char *buf, int max)
{
uint64 addr;
if(argaddr(n, &addr) < 0)
return -1;
return fetchstr(addr, buf, max);
}
处理的数据类型:字符串(以空字符结尾的字符序列)。先使用 argaddr 函数获取参数的指针值,然后通过该指针从内存中读取字符串,并复制到指定的字符缓冲区中。
extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern 关键字:用于声明外部函数,告诉编译器这些函数的定义在其他文件中。这里声明的函数是各个系统调用的具体实现,例如 sys_chdir 用于改变当前工作目录,sys_close 用于关闭文件描述符等。
返回值类型:这些函数的返回值类型为 uint64,通常用于返回系统调用的结果,如操作成功返回 0,操作失败返回 -1 等。
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
};
static uint64 (*syscalls[])(void)
uint64:函数的返回值类型,表示系统调用处理函数返回一个64位无符号整数。
syscalls 是一个数组。
syscalls[] 表示数组的每个元素是一个指针。
(syscalls[])(void) 表示数组的每个元素是一个函数指针,指向一个无参数且返回 uint64 的函数。
假设 SYS_fork 的值为 1,sys_fork 是创建新进程的系统调用处理函数。那么 syscalls[SYS_fork] 就相当于 syscalls[1],它存储的是 sys_fork 函数的地址。当操作系统接收到编号为 SYS_fork 的系统调用时,就可以通过 syscallsSYS_fork 来调用 sys_fork 函数进行具体的处理。
函数指针
返回值类型 (*指针变量名)(参数列表);
举例:
点击查看代码
int add(int a, int b) {
return a + b;
}
int main() {
int (*func_ptr)(int, int);
func_ptr = add;
int result = func_ptr(3, 5);
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
进程相关的代码kernel/proc.h和kernel/proc.c。
// Saved registers for kernel context switches.
struct context {
uint64 ra;
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};
保存内核上下文切换时的寄存器值。ra 是返回地址,sp 是栈指针,s0 到 s11 是被调用者保存的寄存器,这些寄存器在函数调用过程中需要被保存和恢复,以确保函数调用前后寄存器状态的一致性,保证程序的正确执行。
// Per-CPU state.
struct cpu {
struct proc *proc; // The process running on this cpu, or null.
struct context context; // swtch() here to enter scheduler().
int noff; // Depth of push_off() nesting.
int intena; // Were interrupts enabled before push_off()?
};
proc 指针指向当前在该 CPU 上运行的进程,如果没有进程运行则为 null。context 用于保存切换到调度器时的上下文。noff 记录了 push_off() 函数嵌套的深度,push_off() 通常用于禁止中断,noff 可以确保中断在正确的时机重新启用。intena 记录了在调用 push_off() 之前中断是否处于启用状态。
struct trapframe {
/* 0 / uint64 kernel_satp; // kernel page table
/ 8 / uint64 kernel_sp; // top of process's kernel stack
/ 16 / uint64 kernel_trap; // usertrap()
/ 24 / uint64 epc; // saved user program counter
/ 32 / uint64 kernel_hartid; // saved kernel tp
/ 40 / uint64 ra;
/ 48 / uint64 sp;
/ 56 / uint64 gp;
/ 64 / uint64 tp;
/ 72 / uint64 t0;
/ 80 / uint64 t1;
/ 88 / uint64 t2;
/ 96 / uint64 s0;
/ 104 / uint64 s1;
/ 112 / uint64 a0;
/ 120 / uint64 a1;
/ 128 / uint64 a2;
/ 136 / uint64 a3;
/ 144 / uint64 a4;
/ 152 / uint64 a5;
/ 160 / uint64 a6;
/ 168 / uint64 a7;
/ 176 / uint64 s2;
/ 184 / uint64 s3;
/ 192 / uint64 s4;
/ 200 / uint64 s5;
/ 208 / uint64 s6;
/ 216 / uint64 s7;
/ 224 / uint64 s8;
/ 232 / uint64 s9;
/ 240 / uint64 s10;
/ 248 / uint64 s11;
/ 256 / uint64 t3;
/ 264 / uint64 t4;
/ 272 / uint64 t5;
/ 280 */ uint64 t6;
};
用于在用户态和内核态切换时保存和恢复寄存器的值。它包含了内核页表指针 kernel_satp、内核栈指针 kernel_sp、用户程序计数器 epc 等关键信息。在发生陷阱(如系统调用、中断等)时,用户态的寄存器值会被保存到这个结构体中,内核处理完陷阱后,再从这个结构体中恢复用户态寄存器值,以便用户程序继续执行
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
lock 是一个自旋锁,用于保护进程相关数据结构的并发访问。state 表示进程的当前状态。parent 指向父进程。chan 用于进程睡眠时等待的通道。killed 表示进程是否被杀死。xstate 是进程退出时返回给父进程的状态。pid 是进程 ID。kstack 是内核栈的虚拟地址,sz 是进程内存大小,pagetable 是用户页表。trapframe 用于陷阱处理,context 用于进程上下文切换。ofile 数组保存进程打开的文件,cwd 是当前工作目录,name 用于调试时标识进程。
proc.c
定义一些全局数据结构
struct cpu cpus[NCPU]; // CPU核心信息数组
struct proc proc[NPROC]; // 进程表(最大NPROC个进程)
struct proc *initproc; // 指向init进程的指针
int nextpid = 1; // PID分配计数器
struct spinlock pid_lock; // PID分配锁
extern char trampoline[]; // 用户/内核切换代码
点击查看代码
// initialize the proc table at boot time.
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++)//使用 for 循环遍历进程表,proc 是进程表的起始地址,&proc[NPROC] 是进程表的结束地址,NPROC 可能是定义的最大进程数。
{
initlock(&p->lock, "proc");
// Allocate a page for the process's kernel stack.调用 kalloc 函数为进程的内核栈分配一个物理页面
// Map it high in memory, followed by an invalid
// guard page.
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));//计算该进程内核栈的虚拟地址
kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);//调用 kvmmap 函数将物理地址 pa 映射到虚拟地址 va。PGSIZE 是页面大小,PTE_R | PTE_W 表示该页面具有读和写权限。
p->kstack = va;//虚拟地址赋值给进程结构体的 kstack 成员,记录该进程内核栈的虚拟地址。
}
kvminithart();
}
int
cpuid()
{
int id = r_tp();
return id;
}
//该函数用于获取当前 CPU 的 ID。
// Return this CPU's cpu struct.
// Interrupts must be disabled.
struct cpu*
mycpu(void) {
int id = cpuid();
struct cpu *c = &cpus[id];
return c;
}
//返回当前 CPU 对应的 struct cpu 结构体指针。struct cpu 结构体通常包含了该 CPU 的各种状态信息,如寄存器状态、当前运行的进程等。
// Return the current struct proc , or zero if none.
struct proc
myproc(void) {
push_off();
struct cpu *c = mycpu();
struct proc *p = c->proc;
pop_off();
return p;
}
//返回当前正在该 CPU 上运行的进程对应的 struct proc 结构体指针
int
allocpid() {
int pid;
acquire(&pid_lock);
pid = nextpid;
nextpid = nextpid + 1;
release(&pid_lock);
return pid;
}
//为新创建的进程分配一个唯一的进程 ID
点击查看代码
static struct proc*
allocproc(void)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);//获取当前进程结构体的锁,确保在检查进程状态时不会被其他线程干扰。
if(p->state == UNUSED) {
goto found;//检查当前进程的状态是否为 UNUSED(未使用)。如果是,则跳转到 found 标签处进行后续初始化操作。
} else {
release(&p->lock);//如果当前进程已被使用,则释放该进程的锁,继续检查下一个进程。
}
}
return 0;
found:
p->pid = allocpid();
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
return p;
}
//为进程初始化分配陷阱帧 页表 上下文 栈指针
点击查看代码
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}//安全地释放一个进程所占用的资源,并将该进程结构体重置为初始的未使用状态,以便后续可以再次分配给其他进程使用。
点击查看代码
// Create a user page table for a given process,
// with no user memory, but with trampoline pages.
pagetable_t
proc_pagetable(struct proc *p)
{
pagetable_t pagetable;
// An empty page table.
pagetable = uvmcreate();
if(pagetable == 0)
return 0;
// map the trampoline code (for system call return)
// at the highest user virtual address.
// only the supervisor uses it, on the way
// to/from user space, so not PTE_U.
if(mappages(pagetable, TRAMPOLINE, PGSIZE,
(uint64)trampoline, PTE_R | PTE_X) < 0){
uvmfree(pagetable, 0);
return 0;
}
// map the trapframe just below TRAMPOLINE, for trampoline.S.
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
fork系统调用
点击查看代码
int
fork(void)
{
int i, pid;
struct proc *np;//指向子进程
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
调用 uvmcopy() 函数将父进程的用户内存(通过页表 p->pagetable 管理)复制到子进程的页表 np->pagetable 中,p->sz 表示父进程的用户内存大小。
若复制失败(uvmcopy() 返回值小于 0),则调用 freeproc() 函数释放之前为子进程分配的资源,接着释放子进程的锁,最后返回 -1 表示 fork 失败。
复制成功后,将子进程的内存大小 np->sz 设置为与父进程相同。
np->sz = p->sz;
np->parent = p;
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
//陷阱帧用于保存进程在陷入内核时的寄存器状态,这样能让子进程继承父进程的寄存器状态。
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
a0 寄存器通常用于存储函数的返回值。把子进程陷阱帧中的 a0 寄存器设为 0,这样当子进程从 fork 系统调用返回时,fork 函数会返回 0
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
git status
git add .
git commmit -m "lab2 syscall"
git checkout syscall
git push github syscall:syscall
cd
783 git config --global --unset http.proxy
784 sudo gedit ~/.gitconfig
785 env | grep -i proxy
786 env | grep -i proxy
787 cd xv6-labs-2020
788 git push github util:util

浙公网安备 33010602011771号