MIT-6.S081 XV6 Textbook Chapter1

120 阅读9分钟

Chapter 1 Operating System Interfaces

Overview

The Job of the Operating System

  • 操作系统的工作是在多个程序之间共享一台计算机(硬件),并提供一套比硬件单独支持的更有用的服务。
  • 操作系统管理并抽象出低级别的硬件,因此,例如,一个文字处理器不需要关心正在使用哪种类型的磁盘硬件。
  • 最后,操作系统为程序提供了受控的互动方式,以便它们能够共享数据或共同工作。

Introduction

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

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

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

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

    image-20210116095611198

  • shell:一个普通的程序,其功能是让用户输入命令并执行它们,shell不是内核的一部分

  • Process:每个正在运行的程序,称为一个进程,都有包含指令、数据和堆栈的内存。
    • 这些指令实现了程序的计算。
    • 数据是计算所作用的变量。
    • 堆栈组织了程序的过程调用。
  • Syscalls:当一个进程需要调用内核服务时,它会调用一个系统调用,这是操作系统接口中的一个调用。主要分两个步骤:
    • 首先,系统调用进入了内核;
    • 然后,内核执行该服务并返回;

Processes and Memory

​ 每个进程拥有自己的用户空间内存以及内核空间状态,当进程不再执行时xv6将存储和这些进程相关的CPU寄存器直到下一次运行这些进程。kernel将每一个进程用一个PID(process identifier)指代。

常用syscall

fork:形式:int fork(),其作用是让一个进程生成另外一个和这个进程的内存内容相同的子进程。在父进程中,fork的返回值是这个子进程的PID,在子进程中,返回值是0。

int fork(void)
{
  int i, pid;
  struct proc *np;  //新进程
  struct proc *p = myproc();  //当前进程

  // Allocate process.在进程表中寻找一个未使用的进程。if找到初始化状态并在p->lock的情况下返回,如果没有空闲程序,或者内存分配失败,返回0。
  if((np = allocproc()) == 0){
    return -1;  
  }

  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;  //同样大小

  // copy saved user registers.
  *(np->trapframe) = *(p->trapframe);

  // Cause fork to return 0 in the child.
  np->trapframe->a0 = 0;

  // increment reference counts on open file descriptors.
  for(i = 0; i < NOFILE; i++)
    if(p->ofile[i])
      np->ofile[i] = filedup(p->ofile[i]);
  np->cwd = idup(p->cwd);

  safestrcpy(np->name, p->name, sizeof(p->name));

  pid = np->pid;  //要返回的pid

  release(&np->lock);

  acquire(&wait_lock);
  np->parent = p;
  release(&wait_lock);

  acquire(&np->lock);
  np->state = RUNNABLE;  //状态可运行
  release(&np->lock);

  return pid;
}

exit:形式:void exit(int status)。让调用它的进程停止执行并且将内存等占用的资源全部释放。需要一个整数形式的状态参数,0代表以正常状态退出,1代表以非正常状态退出

void exit(int status)
{
  struct proc *p = myproc(); //cur_proc

  if(p == initproc)
    panic("init exiting");

  // Close all open files.
  for(int fd = 0; fd < NOFILE; fd++){
    if(p->ofile[fd]){
      struct file *f = p->ofile[fd];
      fileclose(f);
      p->ofile[fd] = 0;
    }
  }

  begin_op(); //在每个FS系统调用开始时调用。
  iput(p->cwd);//删除对内存中节点的引用。
  end_op();//在每个FS系统调用结束时调用。如果这是最后一个未完成的操作,则提交。
  p->cwd = 0;//cur dir

  acquire(&wait_lock);

  // Give any children to init.
  reparent(p);

  // Parent might be sleeping in wait().
  wakeup(p->parent);
  
  acquire(&p->lock);

  p->xstate = status;
  p->state = ZOMBIE;

  release(&wait_lock);

  // Jump into the scheduler, never to return.
  sched();
  panic("zombie exit");
}

wait:形式:int wait(int *status)。等待子进程退出,返回子进程PID,子进程的退出状态存储到int *status这个地址中。如果调用者没有子进程,wait将返回-1。If none of the caller’s children has exited, wait waits for one to do so.If the parent doesn’t care about the exit status of a child, it can pass a 0 address to wait.

int wait(uint64 addr)
{
  struct proc *np;
  int havekids, pid;
  struct proc *p = myproc();

  acquire(&wait_lock);

  for(;;){
    // Scan through table looking for exited children.
    havekids = 0;
    for(np = proc; np < &proc[NPROC]; np++){
      if(np->parent == p){
        // make sure the child isn't still in exit() or swtch().
        acquire(&np->lock);

        havekids = 1;
        if(np->state == ZOMBIE){
          // Found one.
          pid = np->pid;
          if(addr != 0 && copyout(p->pagetable, addr, (char *)&np->xstate,
                                  sizeof(np->xstate)) < 0) {
            release(&np->lock);
            release(&wait_lock);
            return -1;
          }
          freeproc(np);
          release(&np->lock);
          release(&wait_lock);
          return pid;
        }
        release(&np->lock);
      }
    }

    // No point waiting if we don't have any children.
    if(!havekids || p->killed){
      release(&wait_lock);
      return -1;
    }
    
    // Wait for a child to exit.
    sleep(p, &wait_lock);  //DOC: wait-sleep
  }
}

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

I/O and File Descriptors

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

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

open:系统调用open是用来打开或创建一个文件进行读或写。

read&writeread(fd,buf,n)最多读n bytes从fd,复制到buf

close:形式是int close(int fd),释放fd,使其能重新使用openpipedup

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

Pipes

问:>、>>、<以及|这几个是什么含义?

答: 1. 首先这几个linux命令都与I/O Redirection相关。

​ 2. 其中>>和>都属于输出重定向,<属于输入重定向,而|则是管道。其中,>会覆盖目标的原有内容。当文件存在时会先删除原文件,再重新创建文件,然后把内容写入该文件,否则直接创建文件。而>>会在目标原有内容后追加内容。当文件存在时直接在文件未尾进行内容追加,不会删除原文件,否则直接创建文件。

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

pipe是一个system call,形式为int pipe(int p[])p[0]为读取的文件描述符,p[1]为写入的文件描述符。

对pipe的理解:

有点像dup,dup是copy一个已经存在的fd,pipe则是先生成一个匿名文件,然后调用两个open,生成两个fd,当然内部还要机制要支持如读写不能同时发生,写了以后读端会收到通知之类的的特性。这么一看,这两者貌似又不是一回事儿。

int p[2]; 
char * argv[2];

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

pipe(p);
// p[0] read side
// p[1] write side
if(fork() == 0) {
	// child process 
	close(0);
	dup(p[0]);
	close(p[0]);
	close(p[1]);
	exec("/bin/wc", argv); 
} else { 
	// parent process
	close(1);
  dup(p[1]);
	close(p[0]);
	close(p[1]);
	write(1, "hello world\n", 12);
}
像echo hello world | wc

|的理解:

|左右两个命令实际上是两个child processes,而|正好就充当了进程间通信的管道pipe,

File System

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,这是不符合常理的。

Lecture Notes

操作系统结构

image-20230504145736392.png

  • 硬件层:硬件资源包括了CPU,内存,磁盘,网卡。在最低一层。
  • 用户空间User Space: 在这个架构的最上层,我们会运行各种各样的应用程序,或许有一个文本编辑器(VI),或许有一个C编译器(CC),就是正在运行的所有程序。这里程序都运行在同一个空间中,这个空间通常会被称为用户空间(User Space)。
  • 内核空间Kernel Space: 有一个特殊的程序总是会在运行,它称为Kernel。Kernel是计算机资源的守护者。当你打开计算机时,Kernel总是第一个被启动。Kernel程序只有一个,它维护数据来管理每一个用户空间进程。Kernel同时还维护了大量的数据结构来帮助它管理各种各样的硬件资源,以供用户空间的程序使用。Kernel同时还有大量内置的服务,例如,Kernel通常会有文件系统实现类似文件名,文件内容,目录的东西,并理解如何将文件存储在磁盘中。所以用户空间的程序会与Kernel中的文件系统交互,文件系统再与磁盘交互。

参考

B站黑色卡坤