xv6操作系统接口

操作系统接口

这里是参阅xv6中文文档的第0章

内核是什么??

内核是一个想其他程序提供服务的特殊程序,每一个运行中程序(称之为进程)都拥有包含指令、数据、栈的内存空间。指令实现了程序的运算,数据是用于运算过程的变量,栈管理了程序的过程调用。

执行简单的shell命令

shell 是一个普通的程序,它接受用户输入的命令并且执行它们,它也是传统 Unix 系统中最基本的用户界面。shell 作为一个普通程序,而不是内核的一部分,充分说明了系统调用接口的强大:shell 并不是一个特别的用户程序。这也意味着 shell 是很容易被替代的,实际上这导致了现代 Unix 系统有着各种各样的 shell,每一个都有着自己的用户界面和脚本特性。
一个简单的shell脚本:

/*
vim t.sh
*/
ls > y
cat < y | sort | uniq | wc > y1 cat y1 rm y1 ls |  sort | uniq | wc rm y

xv6实现shell的程序执行后的情况如下:

这里写图片描述

可以看到这个代码是不完整的,需要我们自补充代码。在runcmd里。

/* vim shell.c gcc shell.c */
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>

// Simplifed xv6 shell.

#define MAXARGS 10

// All commands have at least a type. Have looked at the type, the code
// typically casts the *cmd to some specific cmd type.
struct cmd {
  int type;          // ' ' (exec), | (pipe), '<' or '>' for redirection
};

struct execcmd {
  int type;              // ' '
  char *argv[MAXARGS];   // arguments to the command to be exec-ed
};

struct redircmd {
  int type;          // < or > 
  struct cmd *cmd;   // the command to be run (e.g., an execcmd)
  char *file;        // the input/output file
  int flags;         // flags for open() indicating read or write
  int fd;            // the file descriptor number to use for the file
};

struct pipecmd {
  int type;          // |
  struct cmd *left;  // left side of pipe
  struct cmd *right; // right side of pipe
};

int fork1(void);  // Fork but exits on failure.
struct cmd *parsecmd(char*);

// Execute cmd. Never returns.
void
runcmd(struct cmd *cmd)
{
  int p[2], r;
  struct execcmd *ecmd;
  struct pipecmd *pcmd;
  struct redircmd *rcmd;

  if(cmd == 0)
    _exit(0);

  switch(cmd->type){
  default:
    fprintf(stderr, "unknown runcmd\n");
    _exit(-1);
//普通指令
  case ' ':
    ecmd = (struct execcmd*)cmd;
    if(ecmd->argv[0] == 0)
      _exit(0);
    fprintf(stderr, "exec start\n");
    // Your code here ...这是要我们补充代码的地方
    break;
//重定向指令
  case '>':
  case '<':
    rcmd = (struct redircmd*)cmd;
    fprintf(stderr, "redir not implemented\n");
    // Your code here ...
    runcmd(rcmd->cmd);
    break;
//管道指令
  case '|':
    pcmd = (struct pipecmd*)cmd;
    fprintf(stderr, "pipe not implemented\n");
    // Your code here ...
    break;
  }    
  _exit(0);
}

int
getcmd(char *buf, int nbuf)
{
  if (isatty(fileno(stdin)))
    fprintf(stdout, "6.828$ ");
  memset(buf, 0, nbuf);
  if(fgets(buf, nbuf, stdin) == 0)
    return -1; // EOF
  return 0;
}

int
main(void)
{
  static char buf[100];
  int fd, r;

  // 读取控制台输入的命令并执行
  while(getcmd(buf, sizeof(buf)) >= 0){
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
      // Clumsy but will have to do for now.
      // Chdir has no effect on the parent if run in the child.
      buf[strlen(buf)-1] = 0;  // chop \n
      if(chdir(buf+3) < 0)
        fprintf(stderr, "cannot cd %s\n", buf+3);
      continue;
    }
    if(fork1() == 0)
      runcmd(parsecmd(buf));
    wait(&r);
  }
  exit(0);
}

int fork1(void)
{
  int pid;

  pid = fork();
  if(pid == -1)
    perror("fork");
  return pid;
}

struct cmd*
execcmd(void)
{
  struct execcmd *cmd;

  cmd = malloc(sizeof(*cmd));
  memset(cmd, 0, sizeof(*cmd));
  cmd->type = ' ';
  return (struct cmd*)cmd;
}
..............
}

进程和内存

xv6的一个进程由用户内存空间(指令,数据,栈)和对内核课件的状态组成。xv6提供了分时特性:他在可用CPU之间不断切换。进程不在执行时(就绪或者阻塞状态),xv6保存它的CPU的寄存器,当他们再次执行时恢复这些寄存器的值。

int
main(void)
{
  static char buf[100];
  int fd, r;

  // 读取控制台输入的命令并执行
  while(getcmd(buf, sizeof(buf)) >= 0){
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
      // Clumsy but will have to do for now.
      // Chdir has no effect on the parent if run in the child.
      buf[strlen(buf)-1] = 0;  // chop \n
      if(chdir(buf+3) < 0)
        fprintf(stderr, "cannot cd %s\n", buf+3);
      continue;
    }
    //用子进程来执行shell命令
    if(fork1() == 0)
      runcmd(parsecmd(buf));//执行cmd命令
    wait(&r);//等待子进程退出
  }
  exit(0);
}

这里是把创建进程和加载程序分开

int fork1(void)
{
  int pid;
  //执行fork 函数
  pid = fork();
  if(pid == -1)
    perror("fork");
  return pid;
}

I/O和文件描述符

文件描述符是一个整数,它代表了一个进程可以读写的被内核管理的对象。进程可以通过多种方式获得一个文件描述符,如打开文件、目录、设备,或者创建一个管道(pipe),或者复制已经存在的文件描述符。
简单起见,我们常常把文件描述符指向的对象称为“文件”。文件描述符的接口是对文件、管道、设备等的抽象,这种抽象使得它们看上去就是字节流。

0标准输入
1标准输出
2标准错误输出

char buf[512];
int n;

for(;;){
    n = read(0, buf, sizeof buf);//读取完成返回值为0
    if(n == 0)
        break;
    if(n < 0){//非正常返回
        fprintf(2, "read error\n");
        exit();
    }
    if(write(1, buf, n) != n){//正常返回值为写入长度
        fprintf(2, "write error\n");
        exit();
    }
}

fork 和 exec 是单独调用。用这种区分使得 shell 可以在子进程执行指定程序之前对子进程进行修改。

/*
cat < input.txt
*/
char *argv[2];
argv[0] = "cat";
### argv[1] = 0;
if(fork() == 0) {
    close(0);//子进程关闭文件描述符
    open("input.txt", O_RDONLY);
    exec("cat", argv);
}

虽然 fork 复制了文件描述符,但每一个文件当前的偏移仍然是在父子进程之间共享的,考虑下面这个例子:

if(fork() == 0) {
    write(1, "hello ", 6);
    exit();
} else {
    wait();
    write(1, "world\n", 6);
}
/*
hello world
*/

dup 复制一个已有的文件描述符,返回一个指向同一个输入/输出对象的新描述符。这两个描述符共享一个文件偏移,正如被 fork 复制的文件描述符一样。这里有另一种打印 “hello world” 的办法:

fd = dup(1);
write(1, "hello", 6);
write(fd, "world\n", 6);

从同一个原初文件描述符通过一系列 fork 和 dup 调用产生的文件描述符都共享同一个文件偏移,而其他情况下产生的文件描述符就不是这样了,即使他们打开的都是同一份文件。

文件描述符是一个强大的抽象,因为他们将他们所连接的细节隐藏起来了:一个进程向描述符1写出,它有可能是写到一份文件,一个设备(如控制台),或一个管道。

管道

管道是一个小型的内核缓冲区,他以文件描述符对的形式提供给进程,一边用于写操作,一边用于读操作。
从管道的一端写入的数据可以从管道的另一端读取。

int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);//创建一个新的管道并且将读写描述符记录在数组 p
if(fork() == 0) {
    close(0);
    dup(p[0]);//子进程将管道的读端口拷贝在描述符0上
    close(p[0]);//关闭 p 中的描述符
    close(p[1]);
    exec("/bin/wc", argv);//执行 wc
} else {
    write(p[1], "hello world\n", 12);//向管道的写端口写入
    close(p[0]);//关闭两个文件描述符
    close(p[1]);
}

如果数据没有准备好,那么对管道执行的read会一直等待,直到有数据了或者其他绑定在这个管道写端口的描述符都已经关闭了。在后一种情况中,read 会返回 0,就像是一份文件读到了最后。读操作会一直阻塞直到不可能再有新数据到来了,这就是为什么我们在执行 wc 之前要关闭子进程的写端口。如果 wc 指向了一个管道的写端口,那么 wc 就永远看不到 eof 了。
如下的代码用管道就很方便的实现

echo hello world > /tmp/xyz; wc < /tmp/xyz
➜  code echo hello world | wc
       1       2      12

wc程序从标准输入流或文件列表读取文件,并生成一个或多个下列统计信息: 文件包含的字节数、单词数以及文件的行数

但管道和临时文件起码有三个关键的不同点。首先,管道会进行自我清扫,如果是 shell 重定向的话,我们必须要在任务完成后删除 /tmp/xyz。第二,管道可以传输任意长度的数据。第三,管道允许同步:两个进程可以使用一对管道来进行二者之间的信息传递,每一个读操作都阻塞调用进程,直到另一个进程用 write 完成数据的发送。

文件系统

xv6 文件系统提供文件和目录,文件就是一个简单的字节数组,而目录包含指向文件和其他目录的引用。以root为根目录的树状性结构。

以下两种方式都是打开同一个文件

chdir("/a");
chdir("b");
open("c", O_RDONLY);

open("/a/b/c", O_RDONLY);

有很多的系统调用可以创建一个新的文件或者目录:mkdir 创建一个新的目录,open 加上 O_CREATE 标志打开一个新的文件,mknod 创建一个新的设备文件。下面这个例子说明了这3种调用:

mkdir("/dir");
fd = open("/dir/file", O_CREATE|O_WRONGLY);
close(fd);
mknod("/console", 1, 1);

mknod 在文件系统中创建一个文件,但是这个文件没有任何内容。相反,这个文件的元信息标志它是一个设备文件,并且记录主设备号和辅设备号(mknod 的两个参数),这两个设备号唯一确定一个内核设备。当一个进程之后打开这个文件的时候,内核将读、写的系统调用转发到内核设备的实现上,而不是传递给文件系统。(这里可能出现漏洞)

fstat 可以获取一个文件描述符指向的文件的信息。它填充一个名为 stat 的结构体,它在 stat.h 中定义为:

#define T_DIR 1
#define T_FILE 2
#define T_DEV 3
// Directory
// File
// Device
     struct stat {
       short type;  // Type of file
       int dev;     // File system’s disk device
       uint ino;    // Inode number
       short nlink; // Number of links to file连接
       uint size;   // Size of file in bytes
};

文件名和这个文件本身是有很大的区别。同一个文件(称为 inode)可能有多个名字,称为连接 (links)。系统调用 link 创建另一个文件系统的名称,它指向同一个 inode。下面的代码创建了一个既叫做 a 又叫做 b 的新文件。

open("a", O_CREATE|O_WRONGLY);
link("a", "b");

读写 a 就相当于读写 b。每一个 inode 都由一个唯一的 inode 号 直接确定。在上面这段代码中,我们可以通过 fstat 知道 a 和 b 都指向同样的内容:a 和 b 都会返回同样的 inode 号(ino),并且 nlink 数会设置为2。

unlink("a")

系统调用 unlink 从文件系统移除一个文件名。一个文件的 inode 和磁盘空间只有当它的链接数变为 0 的时候才会被清空,也就是没有一个文件再指向它。因此在上面的代码最后加上

我们同样可以通过 b 访问到它。另外

fd = open("/tmp/xyz", O_CREATE|O_RDWR);
unlink("/tmp/xyz");

是创建一个临时 inode 的最佳方式,这个 inode 会在进程关闭 fd 或者退出的时候被清空。

xv6 关于文件系统的操作都被实现为用户程序,诸如 mkdir,ln,rm 等等。这种设计允许任何人都可以通过用户命令拓展 shell 。现在看起来这种设计是很显然的,但是 Unix 时代的其他系统的设计都将这样的命令内置在了 shell 中,而 shell 又是内置在内核中的。

有一个例外,那就是 cd,它是在 shell 中实现的(8016)。cd 必须改变 shell 自身的当前工作目录。如果 cd 作为一个普通命令执行,那么 shell 就会 fork 一个子进程,而子进程会运行 cd,cd 只会改变子进程的当前工作目录。父进程的工作目录保持原样。

现实情况

UNIX 将“标准”的文件描述符,管道,和便于操作它们的 shell 命令整合在一起,这是编写通用、可重用程序的重大进步。这个想法激发了 UNIX 强大和流行的“软件工具”文化,而且 shell 也是首个所谓的“脚本语言”。UNIX 的系统调用接口在今天仍然存在于许多操作系统中,诸如 BSD,Linux,以及 Mac OS X。

现代内核提供了比 xv6 要多的多的系统调用和内核服务。最重要的一点,现代基于 Unix 的操作系统并不遵循早期 Unix 将设备暴露为特殊文件的设计,比如刚才所说的控制台文件。Unix 的作者继续打造Plan 9 项目,它将“资源是文件”的概念应用到现代设备上,将网络、图形和其他资源都视作文件或者文件树。

文件系统抽象是一个强大的想法,它被以万维网的形式广泛的应用在互联网资源上。即使如此,也存在着其他的操作系统接口的模型。Multics,一个 Unix 的前辈,将文件抽象为一种类似内存的概念,产生了十分不同的系统接口。Multics 的设计的复杂性对 Unix 的设计者们产生了直接的影响,他们因此想把文件系统的设计做的更简单。

这本书考察 xv6 是如何实现类似 Unix 的接口的,但涉及的想法和概念可以运用到 Unix 之外很多地方上。任何一个操作系统都需要让多个进程复用硬件,实现进程之间的相互隔离,并提供进程间通讯的机制。在学习 xv6 之后,你应该了解一些其他的更加复杂的操作系统,看一下他们当中蕴含的 xv6 的概念。

相关文章
相关标签/搜索