MIT6.S081学习笔记--lec 1

这篇具有很好参考价值的文章主要介绍了MIT6.S081学习笔记--lec 1。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

引言

操作系统的目标

  • abstract H/W 抽象化硬件
  • multiplex 多路复用
  • isolation 隔离性
  • sharing 共享(进程通信,数据共享)
  • security / access control 安全性/权限控制
  • performance 性能/内核开销
  • range of applications 多应用场景

操作系统概览

操作系统应该提供的功能:1. 多进程支持 2. 进程间隔离 3. 受控制的进程间通信

  • xv6:一种在本课程中使用的类UNIX的教学操作系统,运行在RISC-V指令集处理器上,本课程中将使用QEMU模拟器代替

  • kernel(内核):为运行的程序提供服务的一种特殊程序。每个运行着的程序叫做进程,每个进程的内存中存储指令、数据和堆栈。一个计算机可以拥有多个进程,但是只能有一个内核

    每当进程需要调用内核时,它会触发一个system call(系统调用),system call进入内核执行相应的服务然后返回。

操作系统的组织结构如图1所示

MIT6.S081学习笔记--lec 1

内核提供的一系列系统调用就是用户程序可见的操作系统接口,xv6 内核提供了 Unix 传统系统调用的一部分,它们是:

MIT6.S081学习笔记--lec 1

进程和内存

每个进程拥有自己的用户空间内存以及内核空间状态,当进程不再执行时xv6将存储和这些进程相关的CPU寄存器直到下一次运行这些进程。kernel将每一个进程用一个PID(process identifier)指代。在进程执行中,常常会使用forkexec系统调用来创建新的进程。如下面代码所示:

fork and wait

  • fork:形式:int fork()。其作用是让一个进程生成另外一个和这个进程的内存内容相同的子进程。在父进程中,fork的返回值是这个子进程的PID,在子进程中,返回值是0
  • exit:形式:int exit(int status)。让调用它的进程停止执行并且将内存等占用的资源全部释放。需要一个整数形式的状态参数,0代表以正常状态退出,1代表以非正常状态退出
  • wait:形式:int wait(int *status)。等待子进程退出,返回子进程PID,子进程的退出状态存储到int *status这个地址中。如果调用者没有子进程,wait将返回-1
  • pipe:形式:int pipe(int p[])。创建一个管道,将读/写文件描述符放在p[0]和p[1]中
  • sbrk:形式:char *sbrk(int n)。将进程的内存增加n字节。返回新内存的起始位置。
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");
}

前两行输出可能是

parent: child=1234
child: exiting

也可能是

child: exiting
parent: child=1234

这是因为在fork了之后,父进程和子进程将同时开始判断PID的值,在父进程中,PID为1234,而在子进程中,PID为0。看哪个进程先判断好PID的值,以上输出顺序才会被决定。

最后一行输出为

parent: child 1234 is done

子进程在判断完pid == 0之后将exit,父进程发现子进程exit之后,wait执行完毕,打印输出。

尽管fork了之后子进程和父进程有相同的内存内容,但是内存地址和寄存器是不一样的,也就是说在一个进程中改变变量并不会影响另一个进程。

fork and exec

exec:形式:int exec(char *file, char *argv[])。加载一个文件,获取执行它的参数,执行。如果执行错误返回-1,执行成功则不会返回,而是开始从文件入口位置开始执行命令。文件必须是ELF格式。

xv6 shell使用以上四个system call来为用户执行程序。在shell进程的main中主循环先通过getcmd来从用户获取命令,然后调用fork来运行一个和当前shell进程完全相同的子进程。父进程调用wait等待子进程exec执行完(在runcmd中调用exec

/* sh.c */
int
main(void)
{
  static char buf[100];
  int fd;

  // Ensure that three file descriptors are open.
  while((fd = open("console", O_RDWR)) >= 0){
    if(fd >= 3){
      close(fd);
      break;
    }
  }

  // Read and run input commands.
  while(getcmd(buf, sizeof(buf)) >= 0){
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
      // Chdir must be called by the parent, not the child.
      buf[strlen(buf)-1] = 0;  // chop \n
      if(chdir(buf+3) < 0)
        fprintf(2, "cannot cd %s\n", buf+3);
      continue;
    }
    if(fork1() == 0)
      runcmd(parsecmd(buf));
    // parent wait the child exit
    wait(0);
  }
  exit(0);
}

你可能会想,既然forkexec总是一起使用,为什么不合并成一个呢?实际上我们可以在fork 之后,对子进程进行一些设置,比如输入/输出重定向,然后再执行exec,注意exec并不会改变子进程的file table。

当我们不需要进行额外的设置时,fork 复制内存,exec替换内存,这意味着内存的浪费,有什么办法可以优化这种情况么?答案是肯定的,之后的4.6节我们会讲到 COW (copy-on-write)机制。

I/O 和文件描述符

  • file descriptor:文件描述符,用来表示一个被内核管理的、可以被进程读/写的对象的一个整数,表现形式类似于字节流,通过打开文件、目录、设备等方式获得。一个文件被打开得越早,文件描述符就越小。

    每个进程都拥有自己独立的文件描述符列表,其中0是标准输入,1是标准输出,2是标准错误。shell将保证总是有3个文件描述符是可用的

    while((fd = open("console", O_RDWR)) >= 0)
    {    
        if(fd >= 3)
        {        
            close(fd);        
            break;    
        } 
    }
    
  • readwrite:形式int write(int fd, char *buf, int n)int read(int fd, char *bf, int n)。从/向文件描述符fd读/写n字节bf的内容,返回值是成功读取/写入的字节数。每个文件描述符有一个offset,read会从这个offset开始读取内容,读完n个字节之后将这个offset后移n个字节,下一个read将从新的offset开始读取字节。write也有类似的offset

    /* essence of cat program */ 
    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);    
        } 
    }
    
  • close。形式是int close(int fd),将打开的文件fd释放,使该文件描述符可以被后面的openpipe等其他system call使用。

    使用close来修改file descriptor table能够实现I/O重定向

    /* implementation of I/O redirection, * more specifically, cat < input.txt */ 
    char *argv[2]; 
    argv[0] = "cat"; 
    argv[1] = 0; 
    if (fork() == 0) {   // in the child process    
        close(0);  // this step is to release the stdin file descriptor    
        open("input.txt", O_RDONLY);  // the newly allocated fd for input.txt is 0, since the previous fd 0 is released    
        exec("cat", argv);  // execute the cat program, by default takes in the fd 0 as input, which is input.txt 
    }
    

    父进程的fd table将不会被子进程fd table的变化影响,但是文件中的offset将被共享。

  • dup。形式是int dup(int fd),复制一个新的fd指向的I/O对象,返回这个新fd值,两个I/O对象(文件)的offset相同

    e.g.

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

    除了dupfork之外,其他方式不能使两个I/O对象的offset相同,比如同时open相同的文件

Pipes

pipe:管道,暴露给进程的一对文件描述符,一个文件描述符用来读,另一个文件描述符用来写,将数据从管道的一端写入,将使其能够被从管道的另一端读出。

我们之前有提到过pipe是一个system call,形式为int pipe(int p[])p[0]为读取的文件描述符,p[1]为写入的文件描述符。

xv6中有这样一个例子,通过写管道将参数传递给wc程序

/* run the program wc with stdin connected to the read end of pipe, parent process able to communicate with child process */
int p[2];
char *argv[2];

argv[0] = "wc";
argv[1] = 0;

pipe(p); // read fd put into p[0], write fd put into p[1]
if (fork() == 0) {
    close(0);
    dup(p[0]); // make the fd 0 refer to the read end of pipe
    close(p[0]); // original read end of pipe is closed
    close(p[1]); // fd p[1] is closed in child process, but not closed in the parent process. 注意这里关闭p[1]非常重要,因为如果不关闭p[1],管道的读取端会一直等待读取,wc就永远也无法等到EOF
    exec("/bin/wc", argv); // by default wc will take fd 0 as the input, which is the read end of pipe in this case
} else {
    close(p[0]); // close the read end of pipe in parent process will not affect child process
    write(p[1], "hello world\n", 12); 
    close(p[1]); // write end of pipe closed, the pipe shuts down
}

在xv6的shell实现中即sh.c也是类似的实现,关于pipe系统调用的源码阅读,我也总结了一份代码讲解。

case PIPE:
pcmd = (struct pipecmd*)cmd;
if(pipe(p) < 0)
    panic("pipe");
if(fork1() == 0){
    // in child process
    close(1); // close stdout
    dup(p[1]); // make the fd 1 as the write end of pipe
    close(p[0]);
    close(p[1]);
    runcmd(pcmd->left); // run command in the left side of pipe |, output redirected to the write end of pipe
}
if(fork1() == 0){
    // in child process
    close(0); // close stdin
    dup(p[0]); // make the fd 0 as the read end of pipe
    close(p[0]);
    close(p[1]);
    runcmd(pcmd->right); //  run command in the right side of pipe |, input redirected to the read end of pipe
}
close(p[0]);
close(p[1]);
wait(0); // wait for child process to finish
wait(0); // wait for child process to finish
break;

文件系统

xv6文件系统包含了文件(byte arrays)和目录(对其他文件和目录的引用)。目录生成了一个树,树从根目录/开始。对于不以/开头的路径,认为是是相对路径

  • mknod:创建设备文件,一个设备文件有一个major device #和一个minor device #用来唯一确定这个设备。当一个进程打开了这个设备文件时,内核会将readwrite的system call重新定向到设备上。
  • 一个文件的名称和文件本身是不一样的,文件本身,也叫inode,可以有多个名字,也叫link,每个link包括了一个文件名和一个对inode的引用。一个inode存储了文件的元数据,包括该文件的类型(file, directory or device)、大小、文件在硬盘中的存储位置以及指向这个inode的link的个数
  • fstat。一个system call,形式为int fstat(int fd, struct stat *st),将inode中的相关信息存储到st中。
  • link。一个system call,将创建一个指向同一个inode的文件名。unlink则是将一个文件名从文件系统中移除,只有当指向这个inode的文件名的数量为0时这个inode以及其存储的文件内容才会被从硬盘上移除

注意:Unix提供了许多在用户层面的程序来执行文件系统相关的操作,比如mkdirlnrm等,而不是将其放在shell或kernel内,这样可以使用户比较方便地在这些程序上进行扩展。但是cd是一个例外,它是在shell程序内构建的,因为它必须要改变这个calling shell本身指向的路径位置,如果是一个和shell平行的程序,那么它必须要调用一个子进程,在子进程里起一个新的shell,再进行cd,这是不符合常理的。文章来源地址https://www.toymoban.com/news/detail-576489.html

到了这里,关于MIT6.S081学习笔记--lec 1的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MIT6.828/6.S081 Mac OS下搭建xv6和risc-v

    题外话: 其实我是一名非计算机专业的在校生,因为对软件开发和服务器开发很感兴趣,并且这方面的就业相对我来说资源比较充沛,所以就学习了mit6.828的实验 课程的学习直接跟着官网的schedule走就行,先看Lecture下提供的讲义和手册,然后完成相应的Lab,Lab共计10个,主要

    2024年03月09日
    浏览(39)
  • MIT6.024学习笔记(三)——图论(2)

    科学是使人变得勇敢的最好途径。——布鲁诺 在通信网络中,分为主机和路由器两部分,我们将主机分为输入端和输出端,则构成的图中有三部分:路由器、输入端、输出端,构成了一个有向图。那么,一个N*N规模的通信网络,应该怎么构成才能达到性能最佳呢(假设N总是

    2024年02月09日
    浏览(47)
  • MIT 6.S081学习笔记(第〇章)

    本文涉及 xv6 《第零章 操作系统接口》相关,主要对涉及的进程、I/O、文件描述符、管道、文件等内容产生个人理解,不具有官方权威解释; 文章的目录与书中的目录没有严格的相关性; 文中会有问题 (Question) 字段,这来源于对 xv6 book 的扩展; 文中涉及的代码均能在macOS

    2024年02月09日
    浏览(49)
  • mit6.828 - lab5笔记(上)

    unix的文件系统相关知识 unix将可用的磁盘空间划分为两种主要类型的区域: inode区域 和 数据区域 。 unix为每个文件分配一个inode,其中保存文件的 关键元数据 ,如文件的stat属性和指向文件数据块的指针。 数据区域中的空间会被分成大小相同的数据块(就像内存管理中的分

    2024年02月02日
    浏览(34)
  • MIT 6.S081 Lab Three

    本文为 MIT 6.S081 2020 操作系统 实验三解析。 MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列 在本实验中,您将探索页表并对其进行修改,以简化将数据从用户空间复制到内核空间的函数。 开始编码之前,请阅读xv6手册的第3章和相关文件: * kernel/memlayout.h* ,它捕获了

    2024年02月09日
    浏览(49)
  • MIT6.5830 Lab1-GoDB实验记录(四)

    标签:Golang 读写缓冲区我是一点思路都没有,所以得单独开篇文章记录。 实验补充 了解buffer、序列化与反序列化 这里的序列化,简单来说类似于把一个很长的字符串拆成一个个字符;反序列化就是把这一个个字符拼回成完整的字符串。此处我们需要根据所给的Tuple,转换为

    2024年02月06日
    浏览(51)
  • MIT6.5830 Lab1-GoDB实验记录(五)

    完成了Exercise 1,还有四个Exercise在等着我,慢慢来吧。 实验准备 了解缓冲池 缓冲池,俗称BP。相关的概念还有数据页和缓存页。页(Pages)的概念和操作系统中“分页”的概念是一样的,指的都是把逻辑地址空间分为若干同等大小的页,并从0开始编号。 而缓冲池(Buffer Po

    2024年02月05日
    浏览(47)
  • 【MIT 6.S081】Lab7: Multithreading

    本Lab比较简单,就是为xv6添加一个用户级的多线程功能,然后熟悉一下Linux下多线程编程。 笔者用时约2h 这一部分的代码不涉及内核代码,所以也比较简单,根据提示修改 user/uthread.c 中的代码即可。仿照内核中进程转换函数 swtch 的实现即可。首先,添加一个 context 上下文结

    2023年04月09日
    浏览(37)
  • (MIT6.045)自动机、可计算性和复杂性-图灵机

    有穷自动机(FA)对有限存储量设备是比较好的模型,下推自动机对无限存储设备是较好的模型(但是其存储只能用后进先出的栈模式来使用。)这两个模型过于局限,不能作为通用模型。 和FA相似,但是图灵机有无限的存储。图灵机可以作实际计算机做的所有事情。但是也有图

    2024年02月08日
    浏览(40)
  • MIT 6.S081 教材第八章内容 -- 文件系统 -- 02

    MIT 6.S081 2020 操作系统 本文为MIT 6.S081课程第八章教材内容翻译加整理。 本课程前置知识主要涉及: C语言(建议阅读C程序语言设计—第二版) RISC-V汇编 推荐阅读: 程序员的自我修养-装载,链接与库 术语inode(即索引结点)可以具有两种相关含义之一。它可能是指包含文件大小和

    2024年02月13日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包