MIT 6.S081 (BOOK-RISCV-REV1)教材第一章内容

news2025/1/15 17:39:35

MIT 6.S081 教材第一章内容

  • 引言
  • 第一章 操作系统接口
    • 进程和内存
    • I/O和文件描述符
    • 管道
    • 文件系统
    • 真实世界


引言

MIT 6.S081 2020 操作系统

本文为MIT 6.S081课程第一章教材内容翻译加整理。

本课程前置知识主要涉及:

  • C语言(建议阅读C程序语言设计—第二版)
  • RISK-V汇编
  • 推荐阅读: 程序员的自我修养-装载,链接与库

第一章 操作系统接口

操作系统的任务是在多个程序之间共享一台计算机,并提供比硬件本身支持的更有用的服务。操作系统管理和抽象底层硬件,例如:

  • 文字处理器不需要关心使用哪种类型的磁盘硬件。
  • 一个操作系统在多个程序之间共享硬件,这样它们就可以(或者看起来可以)同时运行。
  • 最后,操作系统为程序提供了可控的交互方式,这样它们就可以共享数据或者一起工作。

操作系统通过接口向用户程序提供服务。设计良好的接口是很困难的。

  • 一方面,我们希望接口简单明了,因为这样更利于正确使用。
  • 另一方面,我们可能倾向于为应用程序提供许多复杂的特性。

解决这个问题的诀窍在于设计接口时,依赖一些可结合的机制,以此来提供更好的通用性。

本书使用单一的操作系统作为具体的例子来说明操作系统的概念。xv6这个操作系统提供了Ken Thompson和Dennis Ritchie的Unix 介绍的基本接口,并且模仿了Unix的内部设计。

Unix提供了一个窄接口,其机制表现突出,提供了令人惊讶的通用程度。这个接口非常成功,甚至现代操作系统BSD、Linux、Mac OSX、Solaris,甚至在一定程度上,Microsoft windows都有类Unix的接口。理解xv6是理解这些系统和其他系统的一个良好开端。

如下图所示,xv6采用传统的内核形式(内核是一个特殊的程序,为正在运行的程序提供服务)。每个正在运行的程序,称为进程,都有包含指令、数据和堆栈的内存。指令实现了程序的运算,数据是计算所依赖的变量,堆栈组织程序的过程调用。一台给定的计算机通常有许多进程,但只有一个内核。

img

当一个进程需要调用一个内核服务时,它会调用一个系统调用,这是操作系统接口中的一个调用。系统调用进入内核;内核执行服务并返回。因此,一个进程在用户空间和内核空间之间交替执行。

内核使用CPU提供的硬件保护机制来确保每个在用户空间执行的进程只能访问它自己的内存。内核程序的执行拥有操控硬件的权限,它需要实现这些保护;而用户程序执行时没有这些特权。当用户程序调用系统调用时,硬件会提升权限级别,并开始执行内核中预先安排好的函数。

内核提供的系统调用集合是用户程序看到的接口。Xv6内核提供了Unix内核传统上提供的服务和系统调用的子集。

下表列出了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表示read/write;返回一个fd(文件描述符)
int write(int fd, char *buf, int n)从buf 写n 个字节到文件描述符fd; 返回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[])创建一个管道,把read/write文件描述符放在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)将打开文件fd的信息放入*st
int stat(char *file, struct stat *st)将指定名称的文件信息放入*st
int link(char *file1, char *file2)为文件file1创建另一个名称(file2)
int unlink(char *file)删除一个文件

xv6系统调用(除非另外声明,这些系统调用返回0表示无误,返回-1表示出错)

本章的其余部分概述了xv6的服务——进程、内存、文件描述符、管道和文件系统——并用代码片段和关于shell(Unix的命令行用户界面)如何使用它们的讨论来阐释。Shell对系统调用的使用说明了它们是如何被精心设计的。

Shell是一个普通的程序,它从用户那里读取命令并执行它们。Shell是一个用户程序,而不是内核的一部分,这一事实说明了系统调用接口的强大之处:shell没有什么特别之处。这也意味着shell很容易替换;因此,现代Unix系统有多种shell可供选择,每种shell都有自己的用户界面和脚本特性。Xv6 Shell是Unix Bourne shell本质的简单实现。它的实现可以在(user/sh.c:1)中找到.


进程和内存

Xv6进程由用户空间内存(指令、数据和堆栈)和对内核私有的每个进程状态组成。Xv6分时进程: 它透明地在等待执行的进程集合中切换可用的CPU。当一个进程没有执行时,xv6保存它的CPU寄存器,并在下一次运行该进程时恢复它们。内核利用进程id或PID标识每个进程。

一个进程可以使用fork系统调用创建一个新的进程。

  • Fork创建了一个新的进程,其内存内容与调用进程(称为父进程)完全相同,称其为子进程。
  • Fork在父子进程中都返回值。
    • 在父进程中,fork返回子类的PID;
    • 在子进程中,fork返回零。

例如,考虑下面用C语言编写的程序片段

// fork()在父进程中返回子进程的PID
// 在子进程中返回0
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。
    • 如果父进程不关心子进程的退出状态,它可以传递一个0地址给wait

在这个例子中,输出:

parent: child=1234
child: exiting

可能以任何一种顺序出来,这取决于父或子谁先到达printf调用。子进程退出后,父进程的wait返回,导致父进程打印

parent: child 1234 is done

之所以可能以任何一种顺序出来,这是因为父进程中将0作为参数传递给wait()表示父进程希望等待任何子进程退出。这称为非阻塞等待,父进程会继续执行,无论是否有任何子进程已经终止。

尽管最初子进程与父进程有着相同的内存内容,但是二者在运行中拥有不同的内存空间和寄存器:

  • 在一个进程中改变变量不会影响到另一个进程。
  • 例如:当wait的返回值存入父进程的变量pid中时,并不会影响子进程中的pid,子进程中pid仍然为0。

exec系统调用使用从文件系统中存储的文件所加载的新内存映像替换调用进程的内存。

(百度百科:根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件)

该文件必须有特殊的格式,它指定文件的哪部分存放指令,哪部分是数据,以及哪一条指令用于启动等等。xv6使用ELF格式(将会在第三章详细讨论)。当exec执行成功,它不向调用进程返回数据,而是使加载自文件的指令在ELF header中声明的程序入口处开始执行。exec有两个参数:可执行文件的文件名和字符串参数数组。

程序的加载,链接部分内容可以阅读: 程序员的自我修养之装载,链接与库进行学习

例如:

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:145*)。

  • 主循环使用getcmd函数从用户的输入中读取一行,然后调用fork创建一个shell进程的副本。
  • 父进程调用wait,子进程执行命令。
    • 例如:当用户向shell输入echo hello时,runcmd(*user/sh.c:58*) 将以echo hello为参数被调用来执行实际命令。
      • 对于“echo hello”,它将调用exec(*user/sh.c:78*)。如果exec成功,那么子进程将从echo而不是runcmd执行命令,在某刻echo会调用exit,这将导致父进程从main(*user/sh.c:78*)中的wait返回。

在这里插入图片描述

你或许想知道为什么execfork没有组合成为一个系统调用,稍后我们将会看到shell在其I/O重定向的实现中利用了这种分离。为了避免创建一个重复的进程然后立即替换它(使用exec)的浪费,操作内核通过使用虚拟内存技术(如copy-on-write)优化 fork 。

Xv6 隐式地分配大多数用户空间内存:

  • fork分配父内存的子副本所需的内存
  • exec 分配足够的内存来保存可执行文件
  • 在运行时需要更多内存的进程(可能是malloc)可以调用 sbrk(n)将其数据内存增加n个字节; sbrk返回新内存的位置。

I/O和文件描述符

文件描述符是一个小整数(small integer),表示进程可以读取或写入的由内核管理的对象。

  • 进程可以通过打开一个文件、目录、设备,或创建一个管道,或复制一个已存在的描述符来获得一个文件描述符。
  • 为了简单起见,我们通常将文件描述符所指的对象称为“文件”;
  • 文件描述符接口将文件、管道和设备之间的差异抽象出来,使它们看起来都像字节流。
  • 我们将输入和输出称为 I/O。

在内部,xv6内核使用文件描述符作为每个进程表的索引,这样每个进程都有一个从零开始的文件描述符的私有空间。

按照惯例,进程从文件描述符0读取(标准输入),将输出写入文件描述符1(标准输出),并将错误消息写入文件描述符2(标准错误)。

  • 正如我们将看到的,shell利用这个约定来实现I/O重定向和管道。
  • shell确保它始终有三个打开的文件描述符(*user/sh.c*:151),这是控制台的默认文件描述符。

readwrite系统调用以字节为单位读取或写入已打开的以文件描述符命名的文件。

  • read(fd,buf,n)从文件描述符fd读取最多n字节,将它们复制到buf,并返回读取的字节数,引用文件的每个文件描述符都有一个与之关联的偏移量。
  • read从当前文件偏移量开始读取数据,然后将该偏移量前进所读取的字节数:
    • (也就是说)后续读取将返回第一次读取返回的字节之后的字节。
    • 当没有更多的字节可读时,read返回0来表示文件的结束。
  • 系统调用write(fd,buf,n)将buf中的n字节写入文件描述符,并返回写入的字节数。
    • 只有发生错误时才会写入小于n字节的数据。
    • 与读一样,write在当前文件偏移量处写入数据,然后将该偏移量向前推进写入的字节数:
      • 每个write从上一个偏移量停止的地方开始写入。

以下程序片段(构成程序cat的本质)将数据从其标准输入复制到其标准输出。如果发生错误,它将消息写入标准错误:

char buf[512];
int n;
for (;;) {
    n = read(0, buf, sizeof buf);
    if (n == 0)
        break;
    if (n < 0) {
        fprintf(2, "read error\n");
        exit(1);
    }
    if (write(1, buf, n) != n) {
        fprintf(2, "write error\n");
        exit(1);
    }
}

代码片段中需要注意的重要一点是,cat不知道它是从文件、控制台还是管道读取。同样也不知道它是打印到控制台、文件还是其他什么地方。文件描述符的使用以及文件描述符0是输入而文件描述符1是输出的约定允许了cat的简单实现。

close系统调用释放一个文件描述符,使其可以被未来使用的openpipedup系统调用重用(见下文)。新分配的文件描述符总是当前进程中编号最小的未使用描述符。

文件描述符和fork相互作用,使I/O重定向更容易实现。

  • fork复制父进程的文件描述符表及其内存,以便子级以与父级在开始时拥有完全相同的打开文件。
  • 系统调用exec替换了调用进程的内存,但保留其文件表。
  • 此行为允许shell通过fork实现I/O重定向,在子进程中重新打开选定的文件描述符,然后调用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);
}

在子进程关闭文件描述符0之后,open保证使用新打开的*input.txt*:0的文件描述符作为最小的可用文件描述符。

cat然后执行文件描述符0(标准输入),但引用的是*input.txt*。父进程的文件描述符不会被这个序列改变,因为它只修改子进程的描述符。


Xv6shell中的I/O重定向代码就是这样工作的(*user/sh.c*:82)。回想一下,在代码执行到这里时,shell已经fork出了子shell,runcmd将调用exec来加载新程序。

open的第二个参数由一组标志组成,这些标志以位表示,用于控制打开的操作。可能的值定义在文件控制(fcntl)头文件(*kernel/fcntl.h*:1-5)中

宏定义功能说明
O_RDONLY只读
O_WRONLY只写
O_RDWR可读可写
O_CREATE如果文件不存在则创建文件
O_TRUNC将文件截断为零长度

现在应该很清楚为什么forkexec分离的用处了:

  • 在这两个调用之间,shell有机会对子进程进行I/O重定向,而不会干扰主shell的I/O设置。
  • 我们可以想象一个假设的forkexec系统调用组合,但是用这样的调用进行I/O重定向是很笨拙的。
  • Shell可以在调用forkexec之前修改自己的I/O设置(然后撤销这些修改);
  • 或者forkexec可以将I/O重定向的指令作为参数;
  • 或者(最不吸引人的是)可以让每个程序(如cat)执行自己的I/O重定向。

尽管fork复制了文件描述符表,但是每个基础文件偏移量在父文件和子文件之间是共享的,比如下面的程序:

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

在这个片段的末尾,附加到文件描述符1的文件将包含数据hello world。父进程中的写操作(由于等待,只有在子进程完成后才运行)在子进程停止写入的位置进行。这种行为有助于从shell命令序列产生顺序输出,比如(echo hello;echo world) >output.txt


dup系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符。两个文件描述符共享一个偏移量,就像fork复制的文件描述符一样。这是另一种将“hello world”写入文件的方法:

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

如果两个文件描述符是通过一系列forkdup调用从同一个原始文件描述符派生出来的,那么它们共享一个偏移量。

否则,文件描述符不会共享偏移量,即使它们来自于对同一文件的打开调用。

dup允许shell执行这样的命令:ls existing-file non-existing-file > tmp1 2>&1

  • 2>&1告诉shell所给命令的文件描述符2是描述符1的副本。
  • 现有文件的名称和不存在文件的错误信息都会显示在tmp1文件中。
  • Xv6 shell不支持错误文件描述符的I/O重定向,但是现在你知道如何实现它了。

在这里插入图片描述

  • >重定向运算符背后的解析逻辑就是将当前子进程的文件描述符数组的1号位替换为指定文件的描述符。
  • 通过 N>&M 的形式,其中 N 和 M 是文件描述符的数字标识符,可以将文件描述符 N 重定向到与文件描述符 M 相同的目标。这样,N 和 M 将引用同一个目标,并共享读写位置和其他属性。
    在这里插入图片描述

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


管道

管道是作为一对文件描述符公开给进程的小型内核缓冲区,一个用于读取,一个用于写入。将数据写入管道的一端使得这些数据可以从管道的另一端读取。管道为进程提供了一种通信方式。

在这里插入图片描述

下面的示例代码使用连接到管道读端的标准输入来运行程序wc

int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
//p数组接收创建得到的管道的输入输出文件描述符号
//p0用于读,p1用于写
//假设父进程此时只打开了0,1,2三个文件描述符,那么p0和p1分别占用3和4号描述符
pipe(p);
if (fork() == 0) {
    //子进程关闭默认的0号标准输入
    close(0);
    //让管道的读端作为新的0号文件描述符关联的文件
    dup(p[0]);
    //释放子进程的3号文件描述符,即p0读端占用的一个描述符,因为此时0号描述符也指向读端
    close(p[0]);
    //释放写端--理由下面有解释
    close(p[1]);
    //执行单词计数程序
    exec("/bin/wc", argv);
} else {
    //父进程关闭读端
    close(p[0]);
    //往写端中写入数据
    write(p[1], "hello world\n", 12);
    //关闭写端
    close(p[1]);
}

程序调用pipe,创建一个新的管道,并在数组p中记录读写文件描述符。

  • fork之后,父子进程都有指向管道的文件描述符。
  • 子进程调用closedup使文件描述符0指向管道的读取端(前面说过优先分配最小的未使用的描述符),然后关闭p中所存的文件描述符,并调用exec运行wc
  • wc从它的标准输入读取时,就是从管道读取。
  • 父进程关闭管道的读取端,写入管道,然后关闭写入端。

如果没有可用的数据,则管道上的read操作将会进入等待,直到有新数据写入或所有指向写入端的文件描述符都被关闭,在后一种情况下,read将返回0,就像到达数据文件的末尾一样。

  • 事实上,read在新数据不可能到达前会一直阻塞,这是子进程在执行上面的wc之前关闭管道的写入端非常重要的一个原因:
    • 如果wc的文件描述符之一指向管道的写入端,wc将永远看不到文件的结束。

Xv6 shell以类似于上面代码(*user/sh.c*:100)的方式实现了诸如grep fork sh.c | wc -l之类的管道。

  • 子进程创建一个管道将管道的左端和右端连接起来。
  • 然后对管道的左端调用forkruncmd,对管道的右端调用forkruncmd,并等待两者都完成。
  • 管道的右端可能是一个命令,该命令本身包含一个管道(例如,a | b | c),该管道本身fork为两个新的子进程(一个用于b,一个用于c)。
  • 因此,shell可以创建一个进程树。这个树的叶子是命令,内部节点是等待左右两个子进程完成的进程。

在这里插入图片描述

原则上,可以让内部节点(上面的父进程)在管道的左端运行,但是正确地这样做会使实现复杂化。考虑进行以下修改:

  • sh.c更改为不对p->left进行fork,并在内部进程中运行runcmd(p->left)
  • 然后,例如: echo hi | wc将不会产生输出,因为当echo hiruncmd中退出时,内部进程将退出,而不会调用fork来运行管道的右端。
    • 这个不正确的行为可以通过不调用内部进程的runcmd中的exit来修复,但是这个修复使代码复杂化:
      • 现在runcmd需要知道它是否是一个内部进程。
      • 同样的,当没有对(p->right)执行fork时也会更加复杂。
      • 例如,只需进行上述的修改,sleep 10 | echo hi将立即打印“hi”,而不是在10秒后,因为echo将立即运行并退出,而不是等待sleep完成。
  • 因为sh.c的目标是尽可能的简单,所以它不会试图避免创建内部进程。

管道看起来并不比临时文件更强大:

下面的管道命令行:

echo hello world | wc

可以不通过管道实现,如下:

echo hello world > /tmp/xyz; wc < /tmp/xyz

在这种情况下,管道相比临时文件至少有四个优势

  • 首先,管道会自动清理自己;在文件重定向时,shell使用完/tmp/xyz后必须小心删除
  • 其次,管道可以任意传递长的数据流,而文件重定向需要磁盘上足够的空闲空间来存储所有的数据。
  • 第三,管道允许并行执行管道阶段,而文件方法要求第一个程序在第二个程序启动之前完成。
  • 第四,如果实现进程间通讯,管道的块读写比文件的非块语义更有效率。

文件系统

Xv6文件系统提供数据文件(包含未解释的字节数组)和目录(包含对数据文件和其他目录的命名引用)。

  • 这些目录形成一个树,从一个叫做根的特殊目录开始。
    • /a/b/c这样的路径是指在根目录/下名为a的目录中名为b的目录中名为c的文件或目录。
  • 不以/开始的路径相对于调用进程的当前工作目录进行计算,当前工作目录可以通过chdir系统调用进行更改。

下面两个代码片段打开相同的文件(假设所有相关的目录都存在)

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

上面代码将进程的当前目录更改为/a/b;下面代码既不引用也不更改进程的当前目录,还有创建新文件和目录的系统调用:

  • mkdir创建一个新目录
  • open中若使用O_CREATE标志将会创建一个新的数据文件
  • mknod创建一个新的设备文件

这个例子说明了这三点:

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

mknod创建一个引用设备的特殊文件。

  • 与设备文件相关联的是主设备号和次设备号(mknod的两个参数),它们唯一地标识了一个内核设备。
  • 当进程稍后打开设备文件时,内核将使用内核设备实现readwrite系统调用,而不是使用文件系统。

一个文件的名字和文件本身是不同的;

  • 同一个底层文件(叫做inode,索引结点)可以有多个名字(叫做link,链接)。
  • 每个链接都由目录中的一个条目组成;
  • 该条目包含一个文件名和一个inode引用。
  • Inode保存有关文件的元数据(用于解释或帮助理解信息的数据),包括其类型(文件/目录/设备)、长度、文件内容在磁盘上的位置以及指向文件的链接数。

fstat系统调用从文件描述符所引用的inode中检索信息。它填充一个stat类型的结构体,struct statstat.h(kernel/stat.h)中定义为:

#define T_DIR 1    // Directory
#define T_FILE 2   // File
#define T_DEVICE 3 // Device
struct stat {
    int dev;     // 文件系统的磁盘设备
    uint ino;    // Inode编号
    short type;  // 文件类型
    short nlink; // 指向文件的链接数
    uint64 size; // 文件字节数
};

link系统调用创建另一个文件名,该文件名指向与现有文件相同的inode。下面的代码片段创建了一个名字既为a又为b的新文件:

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

a读取或写入与从b读取或写入是相同的操作。

  • 每个inode由唯一的inode编号标识。
  • 在上面的代码序列之后,可以通过检查fstat的结果来确定a和b引用相同的底层内容:
    • 两者都将返回相同的inode号(ino),并且nlink计数将被设置为2。

unlink系统调用从文件系统中删除一个名称。只有当文件的链接数为零且没有文件描述符引用时,文件的inode和包含其内容的磁盘空间才会被释放,因此添加:

unlink("a");

最后一行代码序列中会使inode和文件内容可以作为b访问。

此外:

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

是创建没有名称的临时inode的惯用方法,该临时inode将在进程关闭fd或退出时被清理。
在这里插入图片描述

在这里插入图片描述
Unix以用户级程序的形式提供了可从shell调用的文件实用程序,例如mkdirlnrm。这种设计允许任何人通过添加新的用户级程序来扩展命令行接口。事后看来,这个计划似乎是显而易见的,但是在Unix时代设计的其他系统经常将这样的命令构建到shell中(并将shell构建到内核中)。

在这里插入图片描述

在添加新的命令之前,确保将可执行文件放置在适当的位置,并根据需要设置文件的执行权限。这样,当用户在命令行中输入新的命令时,系统就能够找到并执行对应的用户级程序。

一个例外是cd,它是内置在shell(*user/sh.c*:160)。cd必须更改shell本身的当前工作目录。如果cd作为常规命令运行,那么shell将分出一个子进程,子进程将运行cdcd将更改子进程的工作目录。父目录(即shell的)的工作目录不会改变。

在这里插入图片描述


真实世界

Unix将“标准”文件描述符、管道和方便的shell语法结合起来进行操作,这是编写通用可重用程序方面的一大进步。这个想法引发了一种“软件工具”的文化,这种文化对Unix的强大和流行做出了卓越贡献,shell是第一个所谓的“脚本语言”。Unix系统调用接口今天仍然存在于BSD、Linux和MacOSx等系统中。

Unix系统调用接口已经通过便携式操作系统接口(POSIX)标准进行了标准化。Xv6与POSIX不兼容:

  • 它缺少许多系统调用(包括lseek等基本系统调用),并且它提供的许多系统调用与标准不同。
  • 我们xv6的主要目标是简单明了,同时提供一个简单的类unix系统调用接口。
  • 为了运行基本的Unix程序,有些人扩展了xv6,增加了一些系统调用和一个简单的c库。
  • 然而,现代内核比xv6提供了更多的系统调用和更多种类的内核服务。
  • 例如,它们支持网络工作、窗口系统、用户级线程、许多设备的驱动程序等等。
  • 现代内核不断快速发展,提供了许多超越POSIX的特性。

Unix通过一组文件名和文件描述符接口统一访问多种类型的资源(文件、目录和设备)。这个想法可以扩展到更多种类的资源;一个很好的例子是Plan9,它将“资源是文件”的概念应用到网络、图形等等。然而,大多数unix衍生的操作系统并没有遵循这条路。

文件系统和文件描述符是强大的抽象。即便如此,还有其他的操作系统接口模型。Multics,Unix的前身,以一种看起来像内存的方式抽象了文件存储,产生了一种非常不同的接口风格。Multics设计的复杂性直接影响了Unix的设计者,他们试图使设计更简单。

Xv6没有提供一个用户概念或者保护一个用户不受另一个用户的伤害;用Unix的术语来说,所有的Xv6进程都作为root运行。

本书研究了xv6如何实现其类Unix接口,但这些思想和概念不仅仅适用于Unix。任何操作系统都必须在底层硬件上复用进程,彼此隔离进程,并提供受控制的进程间通讯机制。在学习了xv6之后,你应该去看看更复杂的操作系统,以及这些系统中与xv6相同的底层基本概念。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/589675.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript 数据透视表 DHTMLX Pivot Crack

DHTMLX Pivot JavaScript 数据透视表 - 强大的数据汇总和报告 使用我们的高速 JavaScript/HTML5 Pivot 组件可视化您的复杂数据&#xff0c;从而提高您的商业智能。 它可以帮助您以方便的方式汇总大型数据集。 主要特征 纯 JavaScript 库&#xff0c;可轻松与任何服务器端集成…

Linux安装Mysql8,过程详细(离线安装mysql)

因公司需要&#xff0c;需要在服务器上安装一个mysql&#xff0c;但是机器没有外网权限&#xff0c;所以记录一下安装过程&#xff0c;供大家参考。 官网下载安装包 地址&#xff1a;https://dev.mysql.com/downloads/ 如图示&#xff0c;选择MySQL Community Server 如图示&a…

git新手将网页设计代码提交到github上

以下是将代码提交到Github上的一些步骤。如果中途遇到问题或不会的需要我帮忙&#xff0c;可以文章底部联系我。 1. 创建Github账户 首先&#xff0c;您需要在Github上注册一个账户。 如果您已经有账户了&#xff0c;请跳过这一步。 2. 创建一个新的repository 在您的Githu…

格式化数字的实用命令:numfmt

在 Linux 系统中&#xff0c;numfmt 是一个用于格式化数字的实用工具。它可以将数字转换为不同的表示方式&#xff0c;如十进制、二进制、字节单位等。本文将详细介绍 numfmt 命令的使用方法&#xff0c;并提供一些适合初学者的示例。 Numfmt 命令语法 numfmt 命令的基本语法如…

搭建stm32电机控制代码框架(四)——单路PWM生成

STM32中单路PWM的生成一般是基于某一个通用定时器&#xff0c;本次小实验选取TIM2通用定时器&#xff0c;选择PA5作为PWM端口输出。配置步骤如下&#xff1a; 第一步&#xff1a;配置外部晶振与基本的时钟&#xff0c;如下图所示。 &#xff08;1&#xff09;选择外部晶振 &a…

关于java k8s容器环境中的jvm配置与优化

1. 前言 环境 版本 备注 k8s v1.22 配置cpu/mem limit、健康/就绪检查 openjdk 8 openjdk version "1.8.0_342" k8s容器化&#xff08;docker&#xff09;环境更好的解决了 java app 运行环境的封装问题。但存在着一些限制&#xff0c;比如 Java 并不能发现…

流马平台连接数据库

新增数据库配置 在环境管理处对于环境下新增数据库配置 编辑SQL 在用例管理-步骤编辑-逻辑控件处添加前置SQL&#xff0c;编辑SQL 选择数据库名称&#xff0c;查询语句&#xff0c;填写要保存的变量名&#xff0c;并在下方填写查询语句 示例从user表中取出username和passwo…

Semi-supervised Learning(半监督学习)

目录 Introduction Why semi-supervised learning help&#xff1f; Semi-supervised Learning for Generative Model Supervised Generative Model Semi-supervised Generative Model Low-density Separation Assumption Self Training Entropy-based Regularization(基…

docker启动MYSQL8并挂载数据目录

1.创建需要docker挂载的数据目录 mkdir -p /datah/mydata/mysql/conf mkdir -p /datah/mydata/mysql/data mkdir -p /datah/mydata/mysql/log mkdir -p /datah/mydata/mysql/mysql-files 2. 在目录下面 /datah/mydata/mysql/conf 创建配置文件 my.cnf [client] default-charact…

VH6501使用

目录 1.VH6501接口介绍 2.使用场景 2.1当VH6501作为硬件接口卡作通信/监测使用时&#xff0c;使用CH1的任意接口与总线连接即可 2.2针对单节点干扰时&#xff0c;使用CH1的任意接口与被测节点连接即可 2.3针对多节点干扰时&#xff0c;需要通过CH1的两个接口将VH6501串联到…

C++ ---- 模板

目录 泛型编程 函数模板 函数模板语法 模板使用 函数模板原理 函数模板的实例化 隐式实例化 显示实例化 模板参数的匹配原则 类模板 类模板的定义语法 类模板的实例化 非类型模板参数 类模板的特化 全特化 半特化&#xff08;部分特化&#xff09; 两个参数偏…

排查Javascript内存泄漏案例(一)

Chrome DevTools里的Performance面板和Memory面板可以用来定位内存问题。 如何判断应用发生内存泄漏&#xff1f; 为了证明螃蟹的听觉在腿上&#xff0c;一个专家捉了只螃蟹并冲它大吼&#xff0c;螃蟹很快就跑了。然后捉回来再冲它吼&#xff0c;螃蟹又跑了。最后专家把螃蟹的…

浅谈 ChatGPT —— 现代巴别塔

theme: nico 一、用 ChatGPT 一搜就到你这了 ChatGPT 在去年 11 月发布以后&#xff0c;上线 5 天后就有了 100 万用户&#xff0c;上线两个月后已有上亿用户&#xff0c;可谓一炮而红。起初我对 ChatGPT 是没有什么感知的&#xff0c;我单纯认为人工智能还没有发展到完全超越人…

数字经济等相关概念与官方文档

一、数字经济 数字经济&#xff0c;作为一个内涵比较宽泛的概念&#xff0c;凡是直接或间接利用数据来引导资源发挥作用&#xff0c;推动生产力发展的经济形态都可以纳入其范畴。在技术层面&#xff0c;包括大数据、云计算、物联网、区块链、人工智能、5G通信等新兴技术。在应…

美食菜谱类APP小程序开发功能有哪些?

想要开发出一款靠谱好用的美食菜谱APP小程序系统&#xff0c;需要具备哪些基本功能呢&#xff1f; 1、视频教学。对于美食的教学教学方法最直接受用的就是视频教学&#xff0c;用户浏览起来更加方便而且可以直接跟着视频操作&#xff0c;效果更佳。用户也可以自己拍摄制…

JVM垃圾回收篇之相关概念和算法

垃圾回收相关概念 什么是垃圾 垃圾就是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收掉的垃圾,如果不及时进行清理,越积越多就会导致内存溢出. 为什么需要GC 不进行回收,早晚会导致内存溢出,Java自动管理垃圾回收,不需要开发人员手动干预,这就有可能导致开…

【记录】OLAP和OLTP

下面为提炼的重点内容&#xff0c;全部内容参考&#xff1a;OLAP和OLTP OLAP和OLTP的不同设计要求 对于OLTP系统和OLAP系统有哪些不同的要求&#xff1f;要说清楚这个&#xff0c;首先需要分析一下这两个系统的关键特征。 对于OLTP系统来说&#xff0c;最重要的事情是&#…

LIS系统字典模块功能

字典管理模块&#xff1a; 系统参数功能简介&#xff1a;集中设置系统使用过程中所需的参数值&#xff0c;一般由开发或实施人员进行设置。 标本管理功能简介&#xff1a;标本管理是对检验业务中涉及的检验标本类型进行初始化设置&#xff0c; …

Spring Cloud Zuul 是什么?如何使用它来实现 API 网关?

Spring Cloud Zuul 是什么&#xff1f;如何使用它来实现 API 网关&#xff1f; 在微服务架构中&#xff0c;服务之间的通信变得非常频繁和复杂。为了简化服务之间的通信和管理&#xff0c;很多企业都采用了 API 网关的架构模式。API 网关可以帮助我们统一处理服务的入口、路由…

【简单实用框架】【AddressablesMgr】【可移植】

☀️博客主页&#xff1a;CSDN博客主页&#x1f4a8;本文由 萌萌的小木屋 原创&#xff0c;首发于 CSDN&#x1f4a2;&#x1f525;学习专栏推荐&#xff1a;面试汇总❗️游戏框架专栏推荐&#xff1a;游戏实用框架专栏⛅️点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd;&#…