














采用传统形式的“内核”(kernel),一种特殊的程序,提供运行程序的服务。
运行的程序被称为“进程”(process),具有内存包含了指令,数据,栈。
多对一:多个进程,一个内核。
进程调用一个内核服务——唤起“系统调用”(system call),是操作系统进程中的一个调用。
系统调用进入到内核:内核将执行服务并且返回。
因此进程在用户空间和内核空间中交替执行。

内核使用硬件提供的保护机制,来保证每个用户空间的进程只会读/写自己的内存。
内核以实现这些保护所exi需的硬件权限执行。用户程序则不带有这些特权。
用户程序唤起一个系统调用的时候,硬件将提升特权级,开始执行已经设计好的内核函数。
系统调用的集合就是用户端能看见的接口。
shell是一个读取用户指令并执行他们的程序。shell是用户态进程,不是内核的一部分。
shell很容易替换。因此现代操作系统中有大量的不同种类的shell(bash, zsh, fish...)
xv6的shell实现在user/sh.c中。
PID 来关联程序。fork 可以创建一个新的进程。fork 会直接拷贝调用者进程给新进程的内存:指令、数据与调用者进程的栈。fork 将会在调用者和新进程内都有返回。
PID。exit 调用将会导致调用程序停止执行,并且释放资源(内存,文件)。exit 将会采用一个整数状态参数,一般0为成功,1为失败。wait 系统调用将返回一个退出(或被杀死的)当前进程的子进程的 PID。
并且将子进程的退出状态传到传入 wait 函数的地址上。
如果没有一个调用者的子程序退出,wait 则等待其中一个退出。
如果调用者没有子程序,立刻返回 -1。
如果父进程不关心子进程的退出状态,可以传递一个 0 地址。
即使子进程是父进程的完全拷贝,两者的执行使用不同的内存/寄存器。修改一个进程变量不会影响到另一个进程。
exec 系统调用会用从文件系统中加载的文件的新内存映像替换调用进程的内存。exec 将不会返回到调用程序,而是从文件中加载指令,并从ELF中指定的入口开始执行。getcmd 读取一行输入fork,创建一个shell进程的拷贝wait 等待子进程执行程序。子进程通过 runcmd 执行真正的指令。// main loop.
while(getcmd(buf, sizeof(buf)) >= 0){ // getcmd 读取一行输入。
char *cmd = buf;
while (*cmd == ' ' || *cmd == '\t')
cmd++;
if (*cmd == '\n') // is a blank command
continue;
if(cmd[0] == 'c' && cmd[1] == 'd' && cmd[2] == ' '){
// Chdir must be called by the parent, not the child.
cmd[strlen(cmd)-1] = 0; // chop \n
if(chdir(cmd+3) < 0)
fprintf(2, "cannot cd %s\n", cmd+3);
} else {
if(fork1() == 0) // fork 创建进程的拷贝。
runcmd(parsecmd(cmd)); // 子进程通过 runcmd 执行。
wait(0);
}
}
fork 需要分配内存来满足子进程对父进程的拷贝。exec 分配足够的内存来载入可执行文件。sbrk(n) 可以用于进程在运行时分配更多内存(增加 n 个0字节),并返回新内存的地址。文件描述符(file descriptor, fd)是一个小的整数,代表了内核管理的对象。进程可以读取或者写入。
一个进程可能会通过:(1) 打开文件/文件夹/设备;(2) 复制一个存在的文件描述符来获取fd。
fd 抽象了文件/管道/设备成为字节流。
在内部,xv6内核将 fd 用作每个进程表的下标。
// Ensure that three file descriptors are open.
while((fd = open("console", O_RDWR)) >= 0){
if(fd >= 3){
close(fd);
break;
}
}
read 和 write 系统调用从已经和打开的文件中读取/写入字节。
read(fd, buf, n) 从fd中读取至多n个字节到buf中并且返回读取的字节数。read 从当前偏移开始读取数据,并前进偏移。前进的幅度为读取的字节数目。write(fd, buf, n) 系统调用将向文件写入 n 字节buf的内容。read 一样,从当前偏移开始写入数据,并前进偏移。前进的幅度为写入的字节数目。我们可以来看如下的 user/cat.c。需要注意的是 cat 无视读取的来源是文件/终端/管道,也不知道写入的目的是文件/终端/管道。
char buf[512];
void
cat()
{
int n;
// 从标准输入中读取 buf 大小的数据。大于零则读取成功。否则读取失败。
while((n = read(0, buf, sizeof(buf))) > 0) {
// 随后写入标准输出。
if (write(1, buf, n) != n) {
fprintf(2, "cat: write error\n");
exit(1);
}
}
if(n < 0){
fprintf(2, "cat: read error\n");
exit(1);
}
}
close 系统调用释放一个fd,使其可以被未来的 open/pipe/dup 重用。fork 复制父进程的fd与内存,因此子进程将从与父进程相同的文件开始。exec 替换调用进程的内存但是保留了文件表。fork 实现IO重定向,在子进程中开启/关闭文件描述符,exec 执行新的程序。char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) { // 子进程执行if语句
close(0); // 将fd绑定到新的 `input.txt`
open("input.txt", O_RDONLY);
exec("cat", argv); // 替换内存镜像执行程序
}
在 user/sh.c 中也是相同的。在如下的代码片段中,注意执行流程:在main中执行 fork 后,子进程执行 runcmd 部分,并通过exec替换了需要执行的程序。
case EXEC:
ecmd = (struct execcmd*)cmd;
if(ecmd->argv[0] == 0)
exit(1);
exec(ecmd->argv[0], ecmd->argv);
fprintf(2, "exec %s failed\n", ecmd->argv[0]);
break;
case REDIR:
rcmd = (struct redircmd*)cmd;
close(rcmd->fd);
if(open(rcmd->file, rcmd->mode) < 0){
fprintf(2, "open %s failed\n", rcmd->file);
exit(1);
}
runcmd(rcmd->cmd);
break;
open 的第二个参数flag,表明了 open 的控制方式。
O_RDONLY 只读O_WRONLY 只写O_RDWR 读写O_CREATE 创建新的文件O_TRUNC 截断到0---- fork ---- fd不变,继续执行
|---- close ---- open ---- exec
(此处实现重定向)
(撤销操作)
-- close --open -- forkexec -- close -- open
|-- exec --
dup 系统调用复制一个已经存在的文件描述符。这个文件描述符与原先的文件描述符将指代相同的IO对象。可以看下面的例子:fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);
这样写入的是 hello world\n。这和下面的例子是相同的。因为 fork 出来的fd将指代相同的IO对象,共享偏移量。
if(fork() == 0) {
write(1, "hello ", 6);
exit(0);
} else {
wait(0);
write(1, "world\n", 6);
}
pipe 是一个小的kernel 缓冲区,提供给进程一对文件描述符,分别用于读写。一端写入,另一端即可读取。pipe 提供了进程通信的能力。以下是一个 wc 的例子:
int p[2]; // 文件描述符数组。
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p); // 创建管道的两端:p[0] 读端,p[1] 写端
if(fork() == 0) {
// 子进程关闭fd = 0,并且通过dup将 p[0] 关联上 stdcin. 也就是标准输入重定向到读端。
close(0);
dup(p[0]);
close(p[0]);
close(p[1]); // 关闭写端防止阻塞。
exec("/bin/wc", argv); // 执行wc,通过标准输入读取管道。
} else {
close(p[0]); // 关闭读端。
write(p[1], "hello world\n", 12); // 向写端写入数据。这部分数据将会被 wc 读取。
close(p[1]);
}
xv6 以如下的方式实现 pipe 命令的执行:
// 在main中通过fork进入子程序。
case PIPE:
pcmd = (struct pipecmd*)cmd;
if(pipe(p) < 0) // 子程序打开pipe链接左右两端。
panic("pipe");
if(fork1() == 0){ // fork+runcmd 执行管道左边的任务。管道左端需要关联标准输出(stdout,1)与写入端(p[1])。
close(1);
dup(p[1]);
close(p[0]);
close(p[1]);
runcmd(pcmd->left);
}
if(fork1() == 0){ // fork+runcmd 执行管道右边的任务。管道右端需要关联标准输入(stdin,0)与写入端(p[0])。
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
runcmd(pcmd->right);
}
close(p[0]);
close(p[1]);
wait(0);
wait(0);
break;
/ 的路径则为相对于当前路径的相对路径。chdir 用于切换当前进程所在的文件夹。mkdir 用于创建新的文件夹。open 通过 O_CREATE 创建新的数据文件。mknod 创建新的设备文件。两个数字参数分别为设备的major与minor号。
这里我们可以看看 xv6 中的 inode 的结构如下:
// in kernel/fs.h
// On-disk inode structure
struct dinode {
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // Data block addresses
};
// This is the directory structure.
struct dirent {
ushort inum; // inodes' count.
char name[DIRSIZ]; // the name of the directory.
};
fstat 系统调用将从 fd 指向的 inode 中取回 stat 结构体信息。定义如下所示:#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEVICE 3 // Device
struct stat {
int dev; // File system's disk device
uint ino; // Inode number
short type; // Type of file
short nlink; // Number of links to file
uint64 size; // Size of file in bytes
};
link 系统调用将创建另一个文件系统名,作为存在的文件,指向相同的 inode。创建了link后,对该link的读写等同于对原文件的读写。fstat 将会返回 ino (inode number) 来区分文件。unlink 将从文件系统中移除一个文件名。nlink=0 的时候才会释放。因此我们具有如下的方式来保证临时文件在程序结束后会被回收:fd = open("/tmp/xyz", O_CREATE|O_RDWR); unlink("/tmp/xyz");
cd 例外。cd 需要改变进程的文件夹,因此 不能fork后执行,而是直接在shell内执行。相关的lab已经写完了。近日内将会补充笔记。

此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。