Appearance
进程和内存
一个xv6进程由用户空间内存(指令、数据和栈)和内核私有的每个进程的状态组成。 Xv6 分时 处理进程:它透明地在等待执行的进程集合中切换可用的CPU。 当一个进程不执行时,xv6会保存该进程的CPU寄存器,并在下次运行该进程时恢复它们。 内核为每个进程关联一个进程标识符,或 PID
。
xv6 系统调用
系统调用 | 描述 |
---|---|
int fork() | 创建一个进程,返回子进程的PID。 |
int exit(int status) | 终止当前进程;状态报告给wait()。无返回。 |
int wait(int *status) | 等待一个子进程退出;退出状态在*status中;返回子进程PID。 |
int kill(int pid) | 终止进程PID。成功返回0,错误返回-1。 |
int getpid() | 返回当前进程的PID。 |
int sleep(int n) | 暂停n个时钟周期。 |
int exec(char *file, char *argv[]) | 加载一个文件并带参数执行它;仅在出错时返回。 |
char *sbrk(int n) | 将进程的内存增长n个零字节。返回新内存的起始地址。 |
int open(char *file, int flags) | 打开一个文件;flags指示读/写;返回一个fd(文件描述符)。 |
int write(int fd, char *buf, int n) | 从buf向文件描述符fd写入n个字节;返回n。 |
int read(int fd, char *buf, int n) | 读入n个字节到buf;返回读取的字节数;文件末尾返回0。 |
int close(int fd) | 释放打开的文件fd。 |
int dup(int fd) | 返回一个新的文件描述符,引用与fd相同的文件。 |
int pipe(int p[]) | 创建一个管道,将读/写文件描述符放入p[0]和p[1]。 |
int chdir(char *dir) | 更改当前目录。 |
int mkdir(char *dir) | 创建一个新目录。 |
int mknod(char *file, int, int) | 创建一个设备文件。 |
int fstat(int fd, struct stat *st) | 将有关打开文件的信息放入*st。 |
int link(char *file1, char *file2) | 为文件file1创建另一个名称(file2)。 |
int unlink(char *file) | 删除一个文件。 |
Xv6系统调用。如无特别说明,这些调用成功时返回0,出错时返回-1。
一个进程可以使用 fork
系统调用创建一个新进程。 fork
为新进程提供调用进程内存的精确副本:它将调用进程的指令、数据和栈复制到新进程的内存中。 fork
在原始进程和新进程中都会返回。 在原始进程中,fork
返回新进程的PID。 在新进程中,fork
返回零。 原始进程和新进程通常被称为父进程和子进程。
例如,考虑以下用C编程语言编写的程序片段:
c
int pid = fork();
if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait((int *) 0);
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit(0);
} else {
printf("fork error\n");
}
exit
系统调用使调用进程停止执行并释放资源,如内存和打开的文件。 Exit接受一个整数状态参数,通常0表示成功,1表示失败。 wait
系统调用返回当前进程的一个已退出(或被杀死)的子进程的PID,并将子进程的退出状态复制到传递给wait的地址;如果调用者的子进程都没有退出,wait
会等待其中一个退出。 如果调用者没有子进程,wait
立即返回-1。 如果父进程不关心子进程的退出状态,它可以向 wait
传递一个0地址。
在示例中,输出行
parent: child=1234
child: exiting
可能会以任何顺序出现(甚至混合在一起),这取决于父进程或子进程哪个先到达其 printf
调用。 子进程退出后,父进程的 wait
返回,导致父进程打印
parent: child 1234 is done
尽管子进程最初具有与父进程相同的内存内容,但父进程和子进程使用独立的内存和独立的寄存器执行: 在一个进程中更改一个变量不会影响另一个进程。例如,当 wait
的返回值存储到父进程的 pid
中时,它不会改变子进程中的变量 pid
。子进程中的 pid
值仍将为零。
exec
系统调用用从文件系统中加载的新内存映像替换调用进程的内存。 该文件必须具有特定的格式,该格式指定文件的哪一部分包含指令,哪一部分是数据,从哪个指令开始执行等。Xv6使用ELF格式,第3章将对此进行更详细的讨论。 通常,该文件是编译程序源代码的结果。 当 exec
成功时,它不会返回到调用程序;相反,从文件加载的指令在ELF头中声明的入口点开始执行。 exec
接受两个参数:包含可执行文件的文件名和一个字符串参数数组。 例如:
c
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
该片段将调用程序替换为以参数列表 echo hello
运行的程序 /bin/echo
的实例。 大多数程序忽略参数数组的第一个元素,该元素通常是程序的名称。
xv6 shell使用上述调用代表用户运行程序。shell的主要结构很简单;请参见 main
(user/sh.c:/main/)。 主循环使用 getcmd
从用户读取一行输入。 然后它调用 fork
,创建一个shell进程的副本。父进程调用 wait
,而子进程运行命令。例如,如果用户向shell键入 echo hello
,runcmd
将被调用,参数为 echo hello
。 runcmd
(user/sh.c:/runcmd/) 运行实际的命令。对于 echo hello
,它会调用 exec
(user/sh.c:/exec.ecmd/)。 如果 exec
成功,则子进程将执行来自 echo
的指令,而不是 runcmd
。 在某个时候 echo
会调用 exit
,这将导致父进程从 main
(user/sh.c:/main/)中的 wait
返回。
您可能想知道为什么 fork
和 exec
没有合并成一个调用;我们稍后会看到,shell在其I/O重定向的实现中利用了这种分离。 为了避免创建重复进程然后立即替换它(使用 exec
)的浪费,操作系统内核通过使用虚拟内存技术(如写时复制(见第4.4节))来优化 fork
在这种用例下的实现。
Xv6隐式地分配大多数用户空间内存:fork
分配子进程复制父进程内存所需的内存,而 exec
分配足以容纳可执行文件的内存。 在运行时需要更多内存的进程(例如,对于 malloc
)可以调用 sbrk(n)
来将其数据内存增长n个零字节;sbrk
返回新内存的位置。