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);

对于系统调用和标准库的区别,从以下几个方面

  1. 功能层次与来源
    系统调用
    功能层次:系统调用是操作系统提供给用户程序的接口,是用户程序与操作系统内核进行交互的桥梁。它们直接与操作系统内核进行通信,用于请求内核提供各种底层服务,如进程管理、文件系统操作、设备驱动访问等。
    来源:由操作系统内核实现,是操作系统的一部分。不同的操作系统(如 Linux、Windows、macOS 等)提供的系统调用可能有所不同,即使功能类似,其调用方式和参数也可能存在差异。
    标准库函数
    功能层次:标准库函数是对系统调用和一些通用算法的封装,位于用户空间。它们为程序员提供了更方便、更高级的编程接口,用于实现一些常见的编程任务,如字符串处理、内存管理、输入输出等。
    来源:通常由编程语言的标准库提供,例如 C 语言的标准库(如 GNU C Library,即 glibc)。标准库函数的实现不依赖于特定的操作系统,具有较好的可移植性。
  2. 调用方式与开销
    系统调用
    调用方式:系统调用的调用过程涉及用户态到内核态的切换。当用户程序调用系统调用时,会触发一个中断或异常,使 CPU 从用户态切换到内核态,然后执行内核中的相应代码。执行完毕后,再从内核态切换回用户态。
    开销:由于涉及状态切换,系统调用的开销相对较大。频繁的系统调用会导致性能下降,因此在编写程序时应尽量减少不必要的系统调用。
    标准库函数
    调用方式:标准库函数的调用是在用户态下进行的,不涉及状态切换。程序直接调用标准库函数的代码,执行完毕后继续在用户态下运行。
    开销:标准库函数的调用开销相对较小,因为不需要进行状态切换。但是,有些标准库函数可能会在内部调用系统调用,例如 printf 函数在输出时可能会调用 write 系统调用。
  3. 错误处理与返回值
    系统调用
    错误处理:系统调用通常通过返回值来表示调用结果。如果调用成功,返回一个非负的值(如文件描述符、进程 ID 等);如果调用失败,返回 -1,并设置全局变量 errno 来指示具体的错误类型。程序员需要检查返回值并根据 errno 进行相应的错误处理。
    返回值:返回值通常具有特定的含义,与操作系统的底层机制相关。例如,open 系统调用返回一个文件描述符,用于后续的文件操作。
    标准库函数
    错误处理:标准库函数的错误处理方式因函数而异。有些函数通过返回值来表示错误,如 strcmp 函数返回 0 表示字符串相等,非 0 表示不相等;有些函数会设置全局变量或通过指针参数返回错误信息。
    返回值:返回值通常是为了方便程序员使用,与具体的编程任务相关。例如,strlen 函数返回字符串的长度。
  4. 可移植性
    系统调用
    不同的操作系统提供的系统调用接口和功能可能存在差异,因此系统调用的可移植性较差。如果要编写跨平台的程序,需要针对不同的操作系统进行不同的处理。
    标准库函数
    标准库函数遵循编程语言的标准规范,具有较好的可移植性。只要使用相同的编程语言和标准库,程序可以在不同的操作系统上运行而无需进行太多修改。

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");

这是一个 Perl 脚本,文件开头的 #!/usr/bin/perl -w 声明了使用 Perl 解释器执行脚本,-w 开启了警告模式。 脚本的核心目的是生成一个汇编语言文件 usys.S,该文件包含了一系列系统调用的存根函数。这些存根函数作为用户程序与内核之间的桥梁,负责将用户程序的系统调用请求传递给内核。 print会把输出送进usys.s里面, .global $name: 声明全局符号,允许外部调用 $name:: 定义标签作为入口点 li a7, SYS_$name: 将系统调用号加载到寄存器a7 ecall: 触发软中断进入内核态 ret: 从系统调用返回 什么是系统调用存根? 是用户空间程序与操作系统内核之间的桥梁。它是一段特殊的代码,负责将用户空间的请求传递给内核,并将内核的处理结果返回给用户程序。 其主要任务是: 1准备参数:将用户空间的参数按照内核约定的方式传递。 2触发内核模式:通过特定的指令(如 ecall 或 int 0x80)切换到内核模式。 3返回结果:将内核的处理结果返回给用户程序。 为用户态到内核态提供一个接口,例如以 fork() 为例 1用户程序调用 fork(),这是C标准库提供的函数。 2进入存根代码:fork() 的实现会跳转到系统调用存根(如 usys.S 中的 fork 标签)。 3准备系统调用号:存根代码将系统调用号(如 SYS_fork)加载到指定的寄存器(如 RISC-V 的 a7)。 4触发内核模式:存根代码执行 ecall(RISC-V)或 int 0x80(x86),触发软中断,切换到内核模式。 5内核处理:内核根据系统调用号调用对应的处理函数(如 sys_fork())。 6返回用户空间:内核处理完成后,将结果返回给存根代码。存根代码通过 ret 指令返回到用户程序

关于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;
}
从当前进程的指定地址 addr 处获取一个 64 位无符号整数,并将其存储到 ip 所指向的内存位置。 struct proc *p = myproc();:调用 myproc() 函数获取当前进程的进程控制块指针 p。myproc() 函数通常会返回指向当前正在执行的进程的结构体指针。 addr 必须在进程内存范围 [0, p->sz) 内,且 addr + 8 不越界,当前进程的地址空间大小 if(copyin(p->pagetable, (char *)ip, addr, sizeof(*ip)) != 0):调用 copyin 函数将指定地址处的数据复制到 ip 指向的内存中。p->pagetable 是当前进程的页表,用于地址转换。如果 copyin 函数返回非零值,表示复制操作失败,返回 -1。
点击查看代码

// 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);
}
从当前进程的指定地址 addr 处获取一个以空字符('\0')结尾的字符串,并将其复制到 buf 缓冲区中,返回字符串的长度(不包括结尾的空字符) addr:要获取字符串的起始地址,是一个 64 位无符号整数。 buf:一个字符指针,指向用于存储字符串的缓冲区。 max:缓冲区的最大容量,用于限制int err = copyinstr(p->pagetable, buf, addr, max);:调用 copyinstr 函数将从指定地址开始的以空字符结尾的字符串复制到 buf 缓冲区中。copyinstr 函数会在遇到空字符或达到最大复制长度时停止复制。返回值 err 表示复制操作的结果,如果为负数,表示出现错误。复制的字符数量,防止缓冲区溢出。
点击查看代码

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;
}
argraw主要在于处理系统调用时,根据传入的来找出当前进程的寄存器某些状态 p->trapframe 指向当前进程的陷阱帧。陷阱帧是在发生系统调用、中断或异常时,操作系统保存当前进程的寄存器状态的一种数据结构。当用户程序发起系统调用时,会触发陷阱(trap)机制,此时操作系统会将用户程序的寄存器值保存到陷阱帧中,以便在系统调用处理完成后恢复这些值,使进程能够继续正常执行。 在 RISC - V 架构中,a0 到 a5 是用于传递函数参数的寄存器。因此,根据传入的参数编号 n,函数从陷阱帧中提取对应的寄存器值并返回。例如,当 n 为 0 时,返回 p->trapframe->a0,即第一个系统调用参数的值。 问题是,为什么在argraw这个函数里面没有系统调用还是有陷阱帧呢。 用户程序会执行 ecall 指令,这会触发一个陷阱(trap),使 CPU 从用户态切换到内核态。在这个过程中,操作系统会进行以下关键操作: 保存寄存器状态:操作系统会将用户程序当前的寄存器状态保存到一个特定的数据结构中,这个数据结构就是陷阱帧(trap frame)。在 RISC - V 架构里,用于传递函数参数的寄存器(如 a0 - a5)中的值也会被保存到陷阱帧中。这些寄存器的值就是用户程序传递给系统调用的参数。 跳转到系统调用处理程序:操作系统会根据系统调用号跳转到相应的系统调用处理程序开始执行。 argraw 函数通常是在系统调用处理程序内部被调用的。以下是具体的流程: 进入系统调用处理程序:当触发系统调用进入内核态后,操作系统会根据系统调用号找到对应的处理函数。在这个处理函数中,可能需要获取用户程序传递的参数。 调用 argraw 函数:为了获取这些参数,系统调用处理程序会调用 argraw 函数,并传入参数的编号(从 0 开始)。 从陷阱帧中获取参数:argraw 函数通过 myproc() 函数获取当前进程的进程控制块指针,然后访问其中的陷阱帧。根据传入的参数编号,从陷阱帧中提取对应的寄存器值(如 a0 - a5)并返回。 虽然 argraw 函数本身不发起系统调用,但它依赖于系统调用触发时保存的陷阱帧信息。在系统调用处理程序中,argraw 函数通过访问陷阱帧,能够准确获取用户程序传递给系统调用的参数,从而为系统调用的处理提供必要的数据。
点击查看代码

// 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;
  }
}
从用户程序接收系统调用请求,根据系统调用号分派到对应的内核处理函数,并将结果返回给用户程序 1.num=p->trapframe->a7; 从当前进程的陷阱帧(trapframe)中读取寄存器 a7 的值(RISC-V约定 a7 存放系统调用号)。 2.p->trapframe->a0 = syscalls[num]();根据系统调用号 num,从 syscalls[] 数组中查找对应的处理函数并调用。 将系统调用处理函数的返回值存入 a0 寄存器。 RISC-V约定,a0 寄存器用于存放系统调用的返回值

进程相关的代码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();
}
procinit 函数的主要功能是在系统启动时为每个进程分配内核栈的物理内存,并将其映射到虚拟地址空间,同时初始化相关的锁,最后完成内核页表的初始化。

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

posted @ 2025-03-16 17:42  amaoamaoamao  阅读(53)  评论(0)    收藏  举报