Loading

操作系统接口 —— MIT 6S081 FALL 2020

操作系统

操作系统管理计算机上的所有硬件资源,使它们协同工作。并且面对外部应用程序,操作系统要屏蔽不同硬件设备之间的差异,给应用提供一致清晰的接口。

层级

一般的操作系统都提供用户空间内核空间分离的设计模式。一是为了防止用户由于操作失误造成计算机中的一些破坏性损失,二是上面说的,为了给外部应用程序提供一致的接口。

一般的应用程序在用户空间工作,当它需要访问磁盘系统时,它可以不用考虑磁盘是固态硬盘还是机械硬盘,应该采用何种方式访问等细节问题,它只需要通过访问系统在内核空间中提供的文件服务,这是一个操作系统向外部屏蔽硬件细节差异的一个例子。

服务

如上面所说,操作系统会在内核中向外部提供一些服务,比如文件系统服务、网络相关的服务,进程服务等。当工作在用户空间的应用需要访问这些服务时,它便需要使用系统调用,系统调用便是操作系统向外部提供的接口。系统调用会负责进入内核,完成服务的执行并返回。

MIT这门课程所使用的操作系统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[]) 创建一个管道,把write/read文件描述符放在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) 删除一个文件

进程和内存

fork系统调用创建一个和当前进程具有同样内存的子进程,操作系统为每个进程打开的文件维护了一个表,fork后的子进程具有和父进程相同的打开文件。

fork的返回值在刚创建出的子进程中是0,在父进程中是刚创建的子进程的pid。这种区分有必要,因为当你fork后,父进程和子进程要执行的逻辑往往不同。

if(fork() == 0) {
  printf("child");
} else {
  printf("parent");
}

exit系统调用停止当前进程并释放资源(如内存和打开的文件)。exit具有一个参数,传入0则代表进程成功执行,传入1则代表执行失败。

IO和文件描述符

文件描述符是一个整数,它表示进程可以读取或写入的对象,这个对象具体可以是一个文件、设备或者是管道。因为Unix中一切皆文件,所以文件描述符的概念向应用程序屏蔽了不同设备之间的区别,用户程序可以像读写文件一样读写这些设备或者管道。

xv6中的每一个进程都有自己私有的文件描述符空间,从0开始。文件描述符更像操作系统与用户程序之间对于一个可读写对象的契约。当应用程序请求打开这些对象,操作系统颁发一个对于该进程唯一的文件描述符给应用程序,后续该应用程序可以通过这个文件描述符来读写这些对象。

xv6中默认每个进程具有3个描述符,0代表标准输入,1代表标准输出,2代表标准错误。

readwrite都接受三个参数,(fd, buf, len),第一个参数是要读写的文件描述符,第二个是缓冲区,第三个是使用缓冲区的前多少个字节。

fork操作会复制进程的文件描述符表,子级和父级有完全相同的打开的文件,但记住,它们操作的是相同文件,但子级的文件描述符却是父级的副本。

管道

管道是用于进程间通信的一种手段,对于进程来说,它只是一对文件描述符,一个用于读取一个用于写入。

int p[2];
pipe(p);

// 向p[1]写入的数据可以被p[0]读取到

在单进程的情况下,确实没啥用,但如果一个进程创建了一个子进程,那他们将共享管道。你可以在父进程中向p[1]写入,在子进程中读取p[0]

int main() {
    char c, cc;
    int p[2];
    pipe(p);

    if(fork() == 0) {

      read(p[0], &cc, 1);
      printf("I got %c\n", cc);
      exit(0);

    } else {

      printf("SEND TO CHILD : ");
      // 从标准输入读入
      read(0, &c, 1);
      write(p[1], &c, 1);
      wait(0);

    }
    exit(0);
}

上面的程序创建了一个子进程,然后父进程从标准输入读入一个字符,写入到管道的输出端,子进程从管道的输入端中读取该字符并输出。

需要注意的是,管道是一对文件描述符,当你使用fork时,文件描述符变成了两对儿,虽然它们代表了同样的读写对象。

还有,对管道的read操作会挂起,直到管道中有可用的数据或者所有代表该管道的输出端的文件描述符都已关闭(这很重要,因为fork后,连接到管道输出端的文件描述符变成了两个)。

练习,并发获取素数

使用管道编写一个并发版本的素数获取。这个想法来自 Doug McIlroy —— Unix管道的发明者。这篇文章里有这个算法的详细介绍。你需要在user/primes.c中编写这个程序。

刚开始遇到了个坑,怎么也跳不过去,后来照别人代码改了,还是过不去,到最后才发现只是一个声明语句的顺序反了,但是我还是不知道为啥会产生影响。

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

#define read_int(PIPE, NUM) read(PIPE[0], NUM, sizeof(int))
#define write_int(PIPE, NUM) write(PIPE[1], NUM, sizeof(int))



/*
- 当你使用`pipe(p)`创建一个管道,会打开两个文件描述符。p[0]是管道的读取端,p[1]是管道的写入端。管道可以被看作是一个单向数据流,p[0]端所读取的就是p[1]端 所写入的。并且它们的速率不必匹配,当一端请求读取,另一端尚未写入任何信息时,`read`操作会挂起。
- 当使用`fork()`操作创建一个子进程时,该进程和父进程共享打开的文件。所以我们可以先`pipe(p)`,然后在父进程中向`p[1]`写入,在子进程中从`p[0]`读取。
- pipe产生的两个文件描述符在fork之后会变成四个,因为子进程会复制这个,所以你需要全部关闭它们。
- 管道上的`read`操作会挂起,除非它所读取的文件描述符中包含可用数据或者所有指向写入端的文件描述符被关闭。
*/




// traversal启动一个子进程,它从父进程接收消息并将本轮过滤出的数传递到子进程
void
traversal(int *p_pipe)
{
    // 当切换到下面打上注释的那一行时程序出错。我怀疑是p和num也被声明成了数组。就是这个错误让我把我的代码完全改成了别人的。
    int p, num, c_pipe[2];
    // int c_pipe[2], p, num;


    pipe(c_pipe);

    // p_pipe是当前traversal进程与父进程的通信管道
    // c_pipe是当前traversal进程与子进程的通信管道
    // 当前traversal进程需要从父进程的p[0]中读取,并向子进程的p[1]中写入

    // 关闭p_pipe[1],否则read_int将永远无法结束
    close(p_pipe[1]);
    if (read_int(p_pipe, &p)) {
        printf("prime %d\n", p);
        // 创建子进程
        if (fork() == 0) {
            // 这里是子进程的逻辑,子进程需要继续调用traversal过程来遍历它的父进程(也就是本进程)传来的所有数据
            traversal(c_pipe);
            exit(0);
        } else {
            //close(c_pipe[0]); // 关闭,因为在fork完毕后,它不再有用
            // 这里是本进程的逻辑,父进程需要不断地从它的父进程接收数据,并 过滤传递到子进程
            while (read_int(p_pipe, &num)) {
                if (num % p != 0) {
                    write_int(c_pipe, &num);
                }
            }
            // 记得关闭c_pipe[1] 否则子进程的read也永远不会结束
            close(c_pipe[1]);
            wait(0);
        }
    }

    exit(0);
}

int
main(char argc, char *argv)
{
    int p[2];
    pipe(p);

    if (fork() == 0) {
        traversal(p);
        exit(0);
    } else {
        close(p[0]);
        for (int i=2; i<=35; i++) {
            write_int(p, &i);
        }
        close(p[1]);
        wait(0);
    }
    exit(0);
}

该算法已经完全没有我的影子了哈哈,和其他人的几乎一摸一样。

主要的坑是fork后记得关闭所有的管道写入端,否则后面对该管道的读取将一直挂起。

posted @ 2022-02-16 14:00  yudoge  阅读(176)  评论(0编辑  收藏  举报