MIT6.s081/6.828 lectrue1:Introduction and examples
目前课程官网能够查到 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 课程内容简介
课程目标:
- 理解操作系统的设计与实现,设计是一种高层次的结构,实现是代码的样子;这一点和蒋炎岩的课程理念一致,如何设计?如何实现?会花费大量时间讲解
- 在 XV6 上(一个类 UNIX 操作系统)获得动手经验
关于 OS 的目标:
- 管理底层硬件
- 应用程序的并行
- 应用程序的隔离
- 应用程序的数据共享
- 多用户之间的访问控制(Access Control System)/安全性(Security)/权限系统Permission System
- 帮助应用程序获得高性能(Performance)。
- 能够支持大量不同的用户场景:文本编辑器,正在运行游戏,或许你的操作系统需要支持数据库服务器和云计算
1.2 OS 的结构
在这个架构的最上层,运行各种各样的应用程序,这里程序都运行在同一个空间中,这个空间通常会被称为用户空间(Userspace)
计算机有一些硬件资源,包括了CPU,内存,磁盘,网卡。所以硬件资源在最低一层。
区别于用户空间程序,有一个特殊的程序总是会在运行,它称为Kernel。Kernel是计算机资源的守护者。
当你打开计算机时,Kernel总是第一个被启动。Kernel程序只有一个,它维护数据来管理每一个用户空间进程。Kernel中的最重要的两个服务,其中一个服务是文件系统,另一个就是进程管理系统。(在真实的 os 中,kernel 还有很多其他服务:比如 进程间通信服务,比如TCP/IP协议栈,比如支持声卡的软件,比如支持数百种不同磁盘,不同网卡的驱动)
在进程管理系统中,Kernel会管理内存的分配。不同的进程需要不同数量的内存,Kernel会复用内存、划分内存,并为所有的进程分配内存。
应用程序是如何与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难的原因
- 内核的编程环境比较困难,你实际上在提供一个基础设施让别人来运行他们的程序。在这门课程中,我们会使用一个叫做QEMU的硬件模拟器,来模拟CPU和计算机。这会简单一些,但即使这样,编程环境还是比较恶劣。
- 设计 OS 时一些矛盾:
- 高效和易用的矛盾。高效通常意味着操作系统需要在离硬件近的low-level进行操作,而易用则要求操作系统为应用程序提供抽象的high-level可移植接口。所以,提供一个简单可移植,同时又高效的抽象接口需要一定的技巧。
- 简单和强大的矛盾。我们不想程序员看到数量巨多,复杂且难以理解的的内核接口。因为,如果他们不理解这些接口,他们就会很难使用这些接口。想要提供一个非常强大的操作系统服务,这样操作系统才能分担运行应用程序的负担,所以我们需要强大的操作系统服务。
- 灵活和安全的矛盾。我们希望给程序员完全的自由,但是实际上又不能是真正的完全自由,因为我们不想要程序员能直接访问到硬件,干扰到其他的应用程序,或者干扰操作系统的行为。
趣问:子进程是否能够访问到在fork之前创建的文件描述符fd?
子进程可以访问到在fork之前创建的文件描述符fd。在fork之后,子进程会拥有与父进程相同的文件描述符表,包括文件描述符fd
趣问:高级编程语言和系统调用的关系
高阶编程语言通常会对系统调用进行封装,提供更高级、更易用的接口供开发者使用,以简化和抽象编程过程。某种程度上就与系统调用接口隔离了,但是这些语言要完成一些工作,最终肯定是要执行系统调用的,因为编程语言没有资格去调用硬件资源,肯定要和 os 打交道
1.4 课程结构和资源
- 课程网站
- 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或者其他的数字。
O_WRONLY
标志指示文件只能写入。O_CREATE
标志指示如果文件不存在,则创建它。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系统调用,有一些重要的事情,
exec系统调用会保留当前的文件描述符表单。所以任何在exec系统调用之前的文件描述符,例如0,1,2等。它们在新的程序中表示相同的东西。
通常来说exec系统调用不会返回,因为exec会完全替换当前进程的内存,相当于当前进程不复存在了,所以exec系统调用已经没有地方能返回了。所以 shell 不会直接调用 exec,因为用户还想继续使用 shell,这里的一个好方法是 Shell会执行fork,之后fork出的子进程再调用exec系统调用,这是一个非常常见的Unix程序调用风格。
但是如果 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在内核中创建一个文件,然后可以实现两个进程通信
管道分匿名管道和有名管道。匿名管道管道只能用于有亲系关系的进程间通信,即只能在父进程与子进程或兄弟进程间通信。而有名管道可以用于任何进程间通信。
管道的一般用法是,进程在使用fork函数创建子进程前先创建一个管道,该管道用于在父子进程间通信,然后创建子进程,
之后父进程关闭管道的读端,子进程关闭管道的写端。父进程负责向管道写数据而子进程负责读数据。当然父进程可以关闭管道的写端而子进程关闭管道的读端。
这样管道就可以用于父子进程间的通信,也可以用于兄弟进程间的通信。
<1>父进程创建管道
父进程调用pipe开辟管道,得到的两个文件描述符指向管道的两端。
<2>父进程fork出子进程
父进程调用fork创建子进程,则子进程也有两个文件描述符指向同一管道。
<3>父进程关闭fd[0],子进程关闭fd[1]
父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,即实现了进程间通信。
使用pipe创建1个管道。
//pipe()函数创建了管道,并返回了两个描述符:fd[0]用来从管道读数据,fd[1]用来向管道写数据。我们将在父、子进程中使用这两个描述符。
int fd[2];
pipe(int fd[2]);
实现 primes(使用管道)
实现的关键是理解下图的算法:每一个 stage 以当前数集中最小的数字作为素数输出(每个 stage 中数集中最小的数一定是一个素数,因为它没有被任何比它小的数筛掉),并筛掉输入中该素数的所有倍数(必然不是素数),然后将剩下的数传递给下一 stage。
- 进程 1 创建管道 1,fork 出进程 2,使用 write 将 2~11 这些数字输入到管道1中
- 进程 2 输出读到的第一个数(2),然后再创建一个管道2,并 fork 出进程 3,使用 write 将 %2 != 0的数输入到管道 2
- 进程 3 输出读到的第一个数(3),然后再创建一个管道3,并 fork 出进程 4,使用 write 将 %3 != 0的数输入到管道 3
- ...
- 每个进程直到读到-1,exit(0)退出
实现 find
相对来说比较简单,利用 chatGPT 熟读 ls.c的代码,主结构就是递归地遍历所有文件夹,其中需要用到的字符串指针运算需要额外学习一下。
实现xargs
xargs
是一个命令行工具,用于从标准输入或文件中读取数据,并将其作为参数传递给其他命令。它主要用于处理以换行符或空格分隔的数据。
xargs
的基本用法如下:
command | xargs [command]
xargs
将从 command
的输出中读取数据,并将这些数据作为参数传递给 [command]
执行。
这个有些无聊,主要是熟悉一下 c 中的字符串切割和 xargs 的作用
MIT6.s081/6.828 lectrue1:Introduction and examples的更多相关文章
- MIT6.S081/6.828 实验1:Lab Unix Utilities
Mit6.828/6.S081 fall 2019的Lab1是Unix utilities,主要内容为利用xv6的系统调用实现sleep.pingpong.primes.find和xargs等工具.本 ...
- 【MIT6.S081/6.828】手把手教你搭建开发环境
目录 1. 简介 2. 安装ubuntu20.04 3. 更换源 3.1 更换/etc/apt/sources.list文件里的源 3.2 备份源列表 3.3 打开sources.list文件修改 3 ...
- Android API Guides 安卓API指导----第一部分:Introduction(介绍)
第一部分: Introduction(介绍) 目录包含:APP Fundamentals (应用程序基础) Device Compatibility(设备兼容性) System Perm ...
- MIT6.828准备:MacOS下搭建xv6和risc-v环境
本文介绍在MacOS下搭建Mit6.828/6.S081 fall2019实验环境的详细过程,包括riscv工具链.qemu和xv6,对于Linux系统同样可以参考. 介绍 只有了解底层原理才能写好上 ...
- mit-6.828 Lab01:Booting a PC exercise1.1
Lab01:Booting a PC 目录 Lab01:Booting a PC JOS BIOS 背景知识 8086的基本知识 GDB 常用调试指令 Real mode && Pro ...
- Issue 2:Introduction 方法论
1 清晰的思路 论文投稿完毕,彻底完事儿后,原本很反感的论文书写过程也有了些许心得:论文就是科研工作的文字总结,知识再提炼的过程. 想要写好Introduction,脑子里的一条主线必须清楚. 1.1 ...
- Entity Framework 6.0 Tutorials(1):Introduction
以下系统文章为EF6.0知识的介绍,本章是第一篇 原文地址:http://www.entityframeworktutorial.net/entityframework6/introduction.a ...
- Solidity 中文文档 —— 第一章:Introduction to Smart Contracts
第一章:智能合约简介 粗略地翻译了 Ethereum 的智能合约开发语言的文档:Solidity.欢迎转载,注明出处. 有任何问题请联系我,本人微信:wx1076869692,更多详情见文末. 我是 ...
- 一: Introduction(介绍)
Welcome to SQLBackupRestore.com, your online resource for SQL Server backup and recovery issues. Th ...
- SpagoBI 教程 Lesson 1:Introduction and Installation
SapgoBI Lesson 1: Introduction and Installation Downloading and installing SpagoBI. Download SpagoBI ...
随机推荐
- 基础常用API总结2
String java.lang包下 返回值类型 方法 功能 boolean matches(String regex) 如果匹配当前字符串中regex(正则表达式)所表示的字符,如果有返回ture没 ...
- Karmada 多云容器编排引擎支持多调度组,助力成本优化
摘要:Karmada 社区也在持续关注云成本的管理,在最近发布的 v1.5 版本中,支持用户在分发策略 PropagationPolicy/ClusterPropagationPolicy 中设置多个 ...
- AspNetCore 成长杂记(一):JWT授权鉴权之生成JWT(其二)
引子 前面说了用第三方类库生成JWT的故事,给我带来了很大的方便,并且我也承诺要写一篇用常规方法生成JWT的文章(一般都是用微软官方的类库),因此才有了这篇文章. 另外,在前面的文章中,我要纠正一下一 ...
- Python 项目:外星人入侵--第三部分
1.项目内容: 在屏幕左上角添加一个外星人,并指定合适的边框,根据第一个外星人的边距和屏幕尺寸计算屏幕上可容纳多少个外星人. 让外星人群向两边和下方移动,直到外星人被全部击落,有外星人撞到飞船,或有外 ...
- [OpenCV-Python] 16 图像平滑
文章目录 OpenCV-Python:IV OpenCV中的图像处理 16 图像平滑 16.1 平均 16.2 高斯模糊 16.3 中值模糊 16.4 双边滤波 OpenCV-Python:IV Op ...
- 【Python基础】字符串的基本使用
b6f9d807-edb2-4e0a-b554-fae322343bee 字符串是Python中最基本的数据类型之一.它是由一系列字符组成的不可变序列.这意味着一旦创建了一个字符串,就不能直接修改它的 ...
- Django4全栈进阶之路14 项目实战(用户管理):base.html基础模板设计
在 Django 中,我们可以使用模板继承来避免代码的重复.模板继承是指我们可以在一个模板中定义一些公共的 HTML 代码,然后在其他模板中继承这个基础模板,并根据需要添加或覆盖一些内容. 通常情况下 ...
- 记一次,使用python实现一键在爱发电发布带图片的动态
1.背景 本人喜欢转载一些youtube上的视频到b站上面,然后就会有些观众想要视频的封面,那我总不可能一个一个发吧,太麻烦了.故打算将资源发布到爱发电上面.但是爱发电却没有公开对应的api,只能自己 ...
- QT 绘制波形图、频谱图、瀑布图、星座图、眼图、语图
说明 最近在学中频信号处理的一些东西,顺便用 QT 写了一个小工具,可以显示信号的时域波形图.幅度谱.功率谱.二次方谱.四次方谱.八次方谱.瞬时包络.瞬时频率.瞬时相位.非线性瞬时相位.瞬时幅度直方图 ...
- JVM系统参数
JVM(Java虚拟机)是Java程序的运行环境,它可以通过一些系统参数进行配置和优化.以下是一些常用的JVM系统参数: 1. -Xmx: 用于设置JVM堆的最大内存大小.例如,-Xmx1g表示将堆的 ...