首页 > 其他分享 >MIT6.s081/6.828 lectrue1:Introduction and examples

MIT6.s081/6.828 lectrue1:Introduction and examples

时间:2023-07-18 21:00:14浏览次数:47  
标签:fork 文件 调用 MIT6 管道 6.828 lectrue1 描述符 进程

目前课程官网能够查到 2020,2021.2022 秋季的课程表,但是视频都是 2020 年录制的那一版

简单复习+回顾下自己的 OS 学习之旅

参考资料:

官网:https://pdos.csail.mit.edu/6.828/2022/schedule.html

视频翻译:https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/

教材英文版:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf

教材中文版:https://th0ar.gitbooks.io/xv6-chinese/content/index.html


lab参考 :

浙大 fanixao: https://fanxiao.tech/posts/MIT-6S081-notes/

无废话代码:https://github.com/ZachVec/MIT-6.S081

课程内容回顾

1.1 课程内容简介

课程目标

  1. 理解操作系统的设计与实现,设计是一种高层次的结构,实现是代码的样子;这一点和蒋炎岩的课程理念一致,如何设计?如何实现?会花费大量时间讲解
  2. 在 XV6 上(一个类 UNIX 操作系统)获得动手经验

关于 OS 的目标

  1. 管理底层硬件
  2. 应用程序的并行
  3. 应用程序的隔离
  4. 应用程序的数据共享
  5. 多用户之间的访问控制(Access Control System)/安全性(Security)/权限系统Permission System
  6. 帮助应用程序获得高性能(Performance)。
  7. 能够支持大量不同的用户场景:文本编辑器,正在运行游戏,或许你的操作系统需要支持数据库服务器和云计算

1.2 OS 的结构

在这个架构的最上层,运行各种各样的应用程序,这里程序都运行在同一个空间中,这个空间通常会被称为用户空间(Userspace)

计算机有一些硬件资源,包括了CPU,内存,磁盘,网卡。所以硬件资源在最低一层。

区别于用户空间程序,有一个特殊的程序总是会在运行,它称为Kernel。Kernel是计算机资源的守护者。

当你打开计算机时,Kernel总是第一个被启动。Kernel程序只有一个,它维护数据来管理每一个用户空间进程。Kernel中的最重要的两个服务,其中一个服务是文件系统,另一个就是进程管理系统。(在真实的 os 中,kernel 还有很多其他服务:比如 进程间通信服务,比如TCP/IP协议栈,比如支持声卡的软件,比如支持数百种不同磁盘,不同网卡的驱动

在进程管理系统中,Kernel会管理内存的分配。不同的进程需要不同数量的内存,Kernel会复用内存、划分内存,并为所有的进程分配内存。

image-20221027104428425

应用程序是如何与Kernel交互

这里是通过所谓的系统调用(System Call)来完成。系统调用与程序中的函数调用看起来是一样的,但区别是系统调用会实际运行到系统内核中,并执行内核中对于系统调用的实现。

系统调用举例

open-应用程序需要打开一个文件

//现在要打开一个名为“out”的文件,那么会将文件名“out”作为参数传入。同时我们还希望写入数据,那么还会有一个额外的参数,在这里这个参数的值是1,表明我想要写文件。
int fd = open("out", 1);

fd全称就是file descriptor。之后,应用程序可以使用这个文件描述符作为handle,来表示相应打开的文件

write-向文件写入数据

//向write传递一个由open返回的文件描述符作为参数。你还需要向write传递一个指向要写入数据的指针(数据通常是char型序列),在C语言中,可以简单传递一个双引号表示的字符串(下图中的\n表示是换行)。第三个参数是你想要写入字符的数量。
write(fd, "hello\n", 6)

fork-创建新的进程

//更有意思的系统调用是fork。fork是一个这样的系统调用,它创建了一个与调用进程一模一样的新的进程,并返回新进程的process ID/pid。
int pid = fork();

1.3 OS难的原因

  1. 内核的编程环境比较困难,你实际上在提供一个基础设施让别人来运行他们的程序。在这门课程中,我们会使用一个叫做QEMU的硬件模拟器,来模拟CPU和计算机。这会简单一些,但即使这样,编程环境还是比较恶劣。
  2. 设计 OS 时一些矛盾:
    • 高效和易用的矛盾。高效通常意味着操作系统需要在离硬件近的low-level进行操作,而易用则要求操作系统为应用程序提供抽象的high-level可移植接口。所以,提供一个简单可移植,同时又高效的抽象接口需要一定的技巧。
    • 简单和强大的矛盾。我们不想程序员看到数量巨多,复杂且难以理解的的内核接口。因为,如果他们不理解这些接口,他们就会很难使用这些接口。想要提供一个非常强大的操作系统服务,这样操作系统才能分担运行应用程序的负担,所以我们需要强大的操作系统服务。
    • 灵活和安全的矛盾。我们希望给程序员完全的自由,但是实际上又不能是真正的完全自由,因为我们不想要程序员能直接访问到硬件,干扰到其他的应用程序,或者干扰操作系统的行为。

趣问:子进程是否能够访问到在fork之前创建的文件描述符fd?

子进程可以访问到在fork之前创建的文件描述符fd。在fork之后,子进程会拥有与父进程相同的文件描述符表,包括文件描述符fd

趣问:高级编程语言和系统调用的关系

高阶编程语言通常会对系统调用进行封装,提供更高级、更易用的接口供开发者使用,以简化和抽象编程过程。某种程度上就与系统调用接口隔离了,但是这些语言要完成一些工作,最终肯定是要执行系统调用的,因为编程语言没有资格去调用硬件资源,肯定要和 os 打交道

1.4 课程结构和资源

  1. 课程网站
  2. Piazza

1.5 read,write,exit 系统调用

系统调用是操作系统提供的服务的接口,因为 OS 是服务应用程序

XV6是一个简化的类似Unix的操作系统,XV6运行在一个RISC-V微处理器上,实际上,XV6运行在QEMU模拟器之上。(QEMU不仅能模拟CPU, 还提供了极为完善的外部硬件模拟,包括处理器、内存、磁盘、网络接口、外设等)

(它足够简单,所以你们极有可能在几周内很直观的读完所有的代码,同时也把相应的书也看完,这样你们就能理解XV6内部发生的一切事情了。)

这个命令会编译XV6,而XV6是用C语言写的。我首先执行一下make clean,这样你们就能看到完整的编译过程。

make qemu

read:读文件

char buf[64];
int n = read(0, buf, sizeof(buf));
  • 第一个参数是文件描述符,指向一个之前打开的文件。Shell会确保默认情况下,当一个程序启动时,文件描述符0连接到console的输入,文件描述符1连接到了console的输出。
  • read的第二个参数是指向某段内存的指针
  • read的第三个参数是代码想读取的最大长度,sizeof(buf)表示,最多读取64字节的数据

read的返回值可能是读到的字节数(包括结束符)

解释:read 函数从文件 fd 中读取size 字节数据,保存到 buf 指针指向的内存中

write:写文件

char buf[64];
int n = write(1, buf, sizeof(buf));
  • 第一个参数是文件描述符,指向一个之前打开的文件。Shell会确保默认情况下,当一个程序启动时,文件描述符0连接到console的输入,文件描述符1连接到了console的输出。
  • write 的第二个参数是指向某段内存的指针
  • write 的第三个参数是要写入的最大长度,sizeof(buf)表示,最多写入64字节的数据

write 的返回值可能是写入的字节数(包括结束符?)

解释:write 函数从内存地址 buf 中读取size 字节数据,然后写到文件 fd 中,

1.6 open

最直接的创建文件描述符的方法是open系统调用

int fd = open("output.txt", O_WRONLY | O_CREATE | O_TRUNC);
write(fd, "xxx\n", 4)
exit(0)

这个叫做open的程序,会创建一个叫做output.txt的新文件,并向它写入数据:"ooo",最后退出。

第二个参数是一些标志位,用来告诉open系统调用在内核中的实现:我们将要创建并写入一个文件。open系统调用会返回一个新分配的文件描述符,这里的文件描述符是一个小的数字,可能是2,3,4或者其他的数字。

  1. O_WRONLY 标志指示文件只能写入。
  2. O_CREATE 标志指示如果文件不存在,则创建它。
  3. O_TRUNC 标志指示如果文件存在,则截断它,即将其长度设置为 0。

内核为每个进程都维护了一个独立的文件描述符表,这个表描述了这个进程已打开的文件

  • key是整数值,代表文件描述符

  • value是指向文件表项(File Table Entry)的指针或索引,它记录了文件的相关信息,如文件状态、位置指针、访问权限等。

1.7 shell

这里的Shell通常也是人们说的命令行接口

当我输入ls时,我是在要求Shell运行位于文件ls内的这些计算机指令

除了运行程序以外,Shell还会做一些其他的事情,比如,它允许你能重定向IO。比如,我输入 ls > out:我要求Shell运行ls命令,但是将输出重定向到一个叫做out的文件中

趣问:编译器如何处理系统调用?生成的汇编语言是不是会调用一些由操作系统定义的代码段?

OS 定义的代码只能在内核中执行,在RISC-V中,系统调用会被编译为ecall指令,这个特殊的指令将控制权转给内核。之后内核检查进程的内存和寄存器,并确定相应的参数。

1.8 fork:拷贝当前进程的内存

fork会拷贝当前进程的内存,并创建一个新的进程,这里的内存包含了进程的指令和数据。之后,我们就有了两个拥有完全一样内存的进程

在原始的进程(父进程)中,fork系统调用会返回大于0的整数(子进程 ID)。而在新创建的进程(子进程)中,fork系统调用会返回0

fork创建了一个新的进程。当我们在Shell中运行东西的时候,Shell实际上会创建一个新的进程来运行你输入的每一个指令。所以,当我输入ls时,我们需要Shell通过fork创建一个进程来运行ls,这里需要某种方式来让这个新的进程来运行ls程序中的指令,加载名为ls的文件中的指令(也就是后面的exec系统调用)。

shell 中执行的所有进程都是 shell fork 出来的子进程

1.9 exec:执行指令 wait:等待子进程退出

exec系统调用,这个系统调用会从指定的文件(第一个参数)中读取并加载指令,并替代当前调用进程的指令。

从某种程度上来说,这样相当于丢弃了调用进程的内存,并开始执行新加载的指令。

char *argv[] = { "echo", "this", "is", "echo", 0 };
//操作系统从名为echo的文件中加载指令到当前的进程中,并替换了当前进程的内存,之后开始执行这些新加载的指令
exec("echo", argv);

有关exec系统调用,有一些重要的事情,

  1. exec系统调用会保留当前的文件描述符表单。所以任何在exec系统调用之前的文件描述符,例如0,1,2等。它们在新的程序中表示相同的东西。

  2. 通常来说exec系统调用不会返回,因为exec会完全替换当前进程的内存,相当于当前进程不复存在了,所以exec系统调用已经没有地方能返回了。所以 shell 不会直接调用 exec,因为用户还想继续使用 shell,这里的一个好方法是 Shell会执行fork,之后fork出的子进程再调用exec系统调用,这是一个非常常见的Unix程序调用风格。

  3. 但是如果 exec 命令执行失败,比如 exec 一个不存在的指令,就会返回,在下面的第 6 行中,如果 exec 不存在的指令,会执行第 7、8 行;如果运行一个存在的指令,就不会执行第 7、8 行

Unix提供了一个wait系统调用。这里的父进程是 shell,shell 会执行 wait,等待之前创建的子进程退出

子进程退出后,父进程的 wait 函数才会返回,wait 函数返回的是子进程的PID(注意,status 是子进程的退出状态,而不是 PID)

int main() {
  int pid, status;
	pid = fork();
  if(pid == 0) {
    char argv = { "echo", "THIS", "IS", "ECHO", 0}:
    exec("'echo", argv);
    printf("exec failed !\n");
    exit(1);
  } else {
    printf("parent waiting\n");
    wait (&status);
    printf("the child exited with status sdln" , status);
  }
  exit(0);
}


这里wait的参数status,刚传入时是初始值 0。当子进程退出后,其 exit 值会传递给 status,相当于父进程拿到了子进程的退出状态

Unix中的风格是,如果一个程序成功的退出了,那么exit的参数会是0,如果出现了错误,那么就会像第8行一样,会向exit传递1。

NULL的意思是退出状态不关注。如果要获取退出状态应该写成wait(&status);

优化:

常用的写法:先调用fork,再在子进程中调用exec。由于fork首先拷贝了整个父进程的内存,但是之后exec整个将这个拷贝丢弃了,所以这里的拷贝显得有些浪费。

在这门课程的后面,会实现一些优化,比如说copy-on-write fork,这种方式会消除fork的几乎所有的明显的低效,而只拷贝执行exec所需要的内存

1.10 I/O Redirect

Shell提供了方便的I/O重定向工具。如果我运行下面的指令:

echo hello > out

Shell会将echo的输出送到文件out。之后我们可以运行cat指令,并将out文件作为输入

cat < out

IO 重定向是如何实现的?

#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fcntl.h"

// redirect.c: run a command with output redirected

int
main()
{
  int pid;

  pid = fork();
  if(pid == 0){
    //文件描述符1通常是进程用来作为输出的(也就是console的输出文件符),关闭文件描述符
    close(1);
    //打开 out 文件,为 out 分配一个没有使用的、最小的文件描述符1
    open("out", O_WRONLY | O_CREATE | O_TRUNC);

    char *argv[] = { "echo", "this", "is", "redirected", "echo", 0 };
    //echo 的代码中有:write(1, argv[i], strlen(argv[i]));所以 echo 是写到文件描述符 1 中的
    exec("echo", argv);
    printf("exec failed!\n");
    exit(1);
  } else {
    wait((int *) 0);
  }

  exit(0);
}

所以 fork()、close()、open()、echo() 这些系统调用结合起来,实现了I/O重定向。

lab 回顾:lab1 util lab

实现 sleep:

这个很简单,直接调用系统调用 sleep 就可以了,如果想再深究一下,sleep 在 kernel 中是这么实现的:维护一个全局变量 ticks,用来记录 OS 启动后过了多久,调用 sleep 时记录该 ticks 的值为 tick0,然后不断循环判断 ticks - tick0 是否超过阈值,否则就继续 sleep

实现 pingpong

这个函数主要是理解管道,管道就是一个文件、一个缓冲区,可以用来暂存数据

调用 pipe 函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端

半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。之所说管道是半双工,因为父进程关闭读端,子进程关闭写端就可以实现父->子,而反过来就可以实现子->父,所以说是半双工的。

通过pipe在内核中创建一个文件,然后可以实现两个进程通信

img

管道分匿名管道和有名管道。匿名管道管道只能用于有亲系关系的进程间通信,即只能在父进程与子进程或兄弟进程间通信。而有名管道可以用于任何进程间通信。


管道的一般用法是,进程在使用fork函数创建子进程前先创建一个管道,该管道用于在父子进程间通信,然后创建子进程,

之后父进程关闭管道的读端,子进程关闭管道的写端。父进程负责向管道写数据而子进程负责读数据。当然父进程可以关闭管道的写端而子进程关闭管道的读端。

这样管道就可以用于父子进程间的通信,也可以用于兄弟进程间的通信。

<1>父进程创建管道

父进程调用pipe开辟管道,得到的两个文件描述符指向管道的两端。

add2a3b465f42785921f17db1732fd99.png

<2>父进程fork出子进程

父进程调用fork创建子进程,则子进程也有两个文件描述符指向同一管道。

5b70bbfd06646157d0319e71400f35c7.png

<3>父进程关闭fd[0],子进程关闭fd[1]

父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,即实现了进程间通信。

bfc229e921bb734ba15f06a8c1eaea1c.png

使用pipe创建1个管道。

//pipe()函数创建了管道,并返回了两个描述符:fd[0]用来从管道读数据,fd[1]用来向管道写数据。我们将在父、子进程中使用这两个描述符。
int fd[2];

pipe(int fd[2]);

实现 primes(使用管道)

实现的关键是理解下图的算法:每一个 stage 以当前数集中最小的数字作为素数输出(每个 stage 中数集中最小的数一定是一个素数,因为它没有被任何比它小的数筛掉),并筛掉输入中该素数的所有倍数(必然不是素数),然后将剩下的数传递给下一 stage。

image-20230718192619016

  1. 进程 1 创建管道 1,fork 出进程 2,使用 write 将 2~11 这些数字输入到管道1
  2. 进程 2 输出读到的第一个数(2),然后再创建一个管道2,并 fork 出进程 3,使用 write 将 %2 != 0的数输入到管道 2
  3. 进程 3 输出读到的第一个数(3),然后再创建一个管道3,并 fork 出进程 4,使用 write 将 %3 != 0的数输入到管道 3
  4. ...
  5. 每个进程直到读到-1,exit(0)退出

实现 find

相对来说比较简单,利用 chatGPT 熟读 ls.c的代码,主结构就是递归地遍历所有文件夹,其中需要用到的字符串指针运算需要额外学习一下。

实现xargs

xargs 是一个命令行工具,用于从标准输入或文件中读取数据,并将其作为参数传递给其他命令。它主要用于处理以换行符或空格分隔的数据。

xargs 的基本用法如下:

command | xargs [command]

xargs 将从 command 的输出中读取数据,并将这些数据作为参数传递给 [command] 执行。

这个有些无聊,主要是熟悉一下 c 中的字符串切割和 xargs 的作用

标签:fork,文件,调用,MIT6,管道,6.828,lectrue1,描述符,进程
From: https://www.cnblogs.com/looking-for-zihuatanejo/p/17564124.html

相关文章

  • MIT6.S081学习笔记--lec 1
    引言操作系统的目标abstractH/W抽象化硬件multiplex多路复用isolation隔离性sharing共享(进程通信,数据共享)security/accesscontrol安全性/权限控制performance性能/内核开销rangeofapplications多应用场景操作系统概览操作系统应该提供的功能:1.多进程支......
  • MIT6.5840 lab2,3 记录
    参考链接课程地址如何Debug:没有它可怎么活,几万行的日志怎么看Students'GuidetoRaftraft算法可视化:直观展示raft可视化简单入门raft讲解视频:强烈推荐感想感觉理论+实践来学一个东西才学的深刻,特别是对于我这样对抽象理解不太行的,每次见识了一个算法或系统真正如何运行......
  • mit6.s081 lab2: system calls
    1.systemcalltracing(moderate)要求:创建一个系统调用来实现跟踪特性,它采用一个参数来指定跟踪哪一个系统调用,例如:跟踪fork系统调用,程序调用trace(1<<SYS_fork),其中SYS_fork是kernel/syscall.h中的系统调用号。如果在掩码中设置了系统调用的编号,则必须修改须xv6的内核,以便在每......
  • MIT6.5830-2022 Lab 1: SimpleDB
    SimpleDB组成:classes:表示fields,tuples,tupleschemas。classes:作用于tuples的谓词和条件类。methods:在硬盘上存储关系(如heapfiles),处理并发控制和事务。classes:处理tuples的操作类(select,join,insert,delete等)。bufferpool:在内存中缓存活跃的......
  • mit6.s081 lab1:Unix Utilities
    1sleep(easy)要求:为xv6实现UNIX程序睡眠;睡眠需要暂停一段用户指定的时间。刻度是由xv6内核定义的时间概念,即定时器芯片两次中断之间的时间。解决的程序应该在user/sleep.c文件中。 一些小提示:查看user/中的其它程序,如echo.c,grep.c或rm.c,明白如何获取传递给程序的......
  • MIT6.1810的学习笔记
    Chapter0OperatingsysteminterfacesProcessesandmemory这一节主要了解一下基础的xv6中的systemcall其中fork是对进程本身进行操作的它复制当前进程的全部内容以及当前进程的fd表也就是说子进程会做和原进程相同的事且对相同的file进行操作。(需要注意,子进程......
  • xv6 page fault —— MIT6.S081操作系统工程
    当硬件对用户使用的虚拟地址进行翻译时,若该虚拟地址不正确,比如尚未映射、权限不足等,硬件会产生一个pagefault陷阱给操作系统,就是这样一个看似简单平常的机制,却给了操作系......
  • MIT6.828_锁
    JOS中的锁JOS中只有自旋锁,用于大内核锁的实现:staticinlinevoidlock_kernel(void){ spin_lock(&kernel_lock);}自旋锁结构如下:structspinlock{ unsignedlock......
  • MIT6.828_进程切换和进程调度
    MIT6.828_JOS进程切换进程状态JOS共有5种进程状态:enum{ ENV_FREE=0, ENV_DYING, ENV_RUNNABLE, ENV_RUNNING, ENV_NOT_RUNNABLE}我个人觉得ENV_FREE这个状态......
  • mit6.824 raft
    lab2A这部分的内容是用来实现leaderelection过程,按照解释,每个节点创建之初都为follower都会有一个超时时间,超时之后我们进入leaderelection状态。过程需要currentTIme+......