lab1前置

发布时间 2023-03-31 19:55:00作者: 白日梦想家-c

1 课程目的

  • 了解操作系统的设计和实现。操作系统应提供的功能:1.多进程支持 2.受控制的进程间通信 3.进程间隔离
  • 为了深入了解具体的工作原理,本实验将通过一个小的叫做XV6的操作系统,获得实际动手经验。通过研究现有的操作系统,并结合课程配套的实验,你可以获得扩展操作系统,修改并提升操作系统的相关经验,并且能够通过操作系统接口,编写系统软件。

在这门课程前面部分的lab中,会要求写一些应用程序来执行系统调用,之后的大部分lab中要求实现基本的操作系统功能或者扩展XV6操作系统。最后一个lab会要求添加一个网络协议栈和一个网络驱动,这样操作系统才能连接到网络上。

2 系统调用

2.1 sleep

C 中的 sleep() 函数允许用户在特定时间内等待当前线程/进程。CPU 的其他操作会正常运行,只有 sleep()函数将在线程指定的时间内使当前可执行文件休眠。在linux平台下,我们需要包含如下头文件来使用:

#include <unistd.h>
unsigned int sleep(unsigned int secs);

使用该函数需要我们传入挂起时间,Linux中的sleep挂起时间是以秒为单位的,当它达到指定挂起时间,它会返回0,若被信号中断,就返回剩余秒数。

2.2 fork

fork产生子进程后,子进程与父进程执行的是相同的代码,父进程在fork之前的所有东西都会拷贝给子进程,但是子进程和父进程是相互独立的变量空间、寄存器以及内存地址,各自的变量互不干扰。
 
其形式为int fork(),头文件为unistd.h。在父进程中,fork返回的是子进程的PID,子进程中返回的是0.

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的值,哪个进程先判断好就先进行输出。
最后一行的输出为

parent: child 1234 is done

这个时候子进程通过exit(0)正常退出,父进程中用wait将子进程回收,最终输出回收成功的信息。

2.3 exit

形式:int exit(int status),让调用它的进程停止执行并且将内存等占用的资源全部释放。需要一个整数形式的状态参数,0代表以正常状态退出,1代表以非正常状态退出.
 
补充:1.return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
2.return用于结束一个函数的执行,将函数的执行信息传出给其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS。通常在主函数最后使用return0或者exit0的现象没有什么区别,但在非主函数中使用时不可随意。

2.4 wait(进程等待函数)

通常用于父进程等待子进程退出并回收资源,防止子进程变成僵尸进程。其形式为int wait(int *status),等待子进程退出,返回子进程PID,子进程的退出状态存储到int *status这个地址中。如果调用者没有子进程,wait将返回-1
 
详述:进程一旦调用了wait,就会立刻阻塞自己,由wait分析当前进程中的某个子进程是否已经退出了,如果让它找到这样一个已经变成僵尸进程的子进程,wait会收集这个子进程的信息,并将它彻底销毁后返回;如果没有找到这样一个子进程,wait会一直阻塞直到有一个出现。参数status用来保存被收集进程退出时的一些状态,它是一个指向int型的指针。
但如果我们对这个子进程是如何死掉的不在乎,只是想要把这个僵尸进程消灭掉,可以将status设置为NULL,如pid = wait(NULL);如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用会失败,wait返回-1。
 
补充:1.当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.
2.wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

2.5 exec

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

3 I/O、文件描述符

3.1文件描述符

  • 文件描述符:文件描述符是唯一标识进程的打开文件的整数。
  • 文件描述符表:文件描述符表是整数数组索引的集合,这些索引是文件描述符,其中元素是指向文件表条目的指针。操作系统中为每个进程提供了一个唯一的文件描述符表。
  • 文件表条目:文件表条目是打开文件的结构内存中代理项,在处理打开文件的请求时创建,这些条目保持文件位置。

 

  • 标准文件描述符:当任何进程启动时,该进程文件描述符表的 fd(文件描述符)0、1、2 会自动打开,(默认情况下)这 3 个 fd 中的每一个都引用名为 /dev/tty 的文件的文件表条目
  • /dev/tty:终端的内存中代理 终端
  • 从 stdin 读取 => 从 fd 0 读取:每当我们从键盘写入任何字符时,它都会从 stdin 读取到 fd 0 并保存到名为 /dev/tty 的文件。
  • 写入 stdout => 写入 fd 1 :每当我们看到任何输出到视频屏幕时,它都来自名为 /dev/tty 的文件,并通过 fd 1 写入屏幕中的 stdout。
  • 写入 stderr => 写入 fd 2:我们看到视频屏幕出现任何错误,它也是从该文件通过 fd 2 写入屏幕中的 stderr。

3.3 read和write

形式int write(int fd, char *buf, int n)int read(int fd, char *bf, int n)。从/向文件描述符fd读/写n字节buf的内容,返回值是实际读取/写入的字节数。每个文件描述符有一个offset,read会从这个offset开始读取内容,读完n个字节之后将这个offset后移n个字节,下一个read将从新的offset开始读取字节。write也有类似的offset

char buf[512];
int n;
  
for (;;) {
    n = read(0, buf, sizeof buf);
    if (n == 0)
        break;
    if (n < 0){
        fprintf(2, "read errot\n");
        exit(1);
    }
    if (write(1, buf, n) != n){
        fprintf(2, "write error\n");
        exit(1);
    }
}

3.3 close/dup

close。形式是int close(int fd),将打开的文件fd释放,使该文件描述符可以被后面的open、pipe等其他system call使用。

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

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相同

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

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

3.3 管道

管道是两个进程之间的连接,使得一个进程的标准输出成为另一个进程的标准输入。在 UNIX 操作系统中,管道对于相关进程之间的通信(进程间通信)很有用。
 
pipe是一个system call,形式为int pipe(int p[]),p[0]为读取的文件描述符,p[1]为写入的文件描述符。
 
当我们在任何进程中使用 fork 时,文件描述符在子进程和父进程中保持打开状态。如果我们在创建管道后调用 fork,那么父子可以通过管道进行通信。

以下程序为例:

#include <stdio.h>
#include <unistd.h>
#define MSGSIZE 16
char* msg1 = "hello, world #1";
char* msg2 = "hello, world #2";
char* msg3 = "hello, world #3";
  
int main()
{
    char inbuf[MSGSIZE];
    int p[2], pid, nbytes;
  
    if (pipe(p) < 0)
        exit(1);
  
    /* continued */
    if ((pid = fork()) > 0) {
        write(p[1], msg1, MSGSIZE);
        write(p[1], msg2, MSGSIZE);
        write(p[1], msg3, MSGSIZE);
  
        // Adding this line will
        // not hang the program
        // close(p[1]);
        wait(NULL);
    }
  
    else {
        // Adding this line will
        // not hang the program
        // close(p[1]);
        while ((nbytes = read(p[0], inbuf, MSGSIZE)) > 0)
            printf("% s\n", inbuf);
        if (nbytes != 0)
            exit(2);
        printf("Finished reading\n");
    }
    return 0;
}

输出为:

hello world, #1
hello world, #2
hello world, #3

在此代码中,完成读取/写入后,父级和子级都会阻止而不是终止进程,这会导致程序挂起。发生这种情况是因为读取系统调用获取与管道请求的数据量或数据量一样多,如果其他某个进程打开管道进行写入,则读取将在预期新数据时阻塞,因此此代码输出挂起,因为此处写入结束父进程,并且子进程不会关闭。