操作系统-笔记2

作者: KyoDante | 来源:发表于2020-03-19 01:25 被阅读0次

    虚拟化:
    进程(Process):是一个运行的程序。程序本身是呆在磁盘上,一堆指令或者可能有一些静态的数据,等待被OS运行。
    我们是想要一次运行多个进程的,尽管我们实际上只有几个CPU能用,OS如何提供这种近似无限的CPU供应?
    运行一个进程->停止一个进程->运行另一个进程;然后不断重复,以达到CPU的时分(time sharing),但是越多进程会带来性能上的影响。与时分相对应的是空分(space sharing),例如:硬盘的某个块一旦给了某个文件,那除非文件删除,否则这个块就不会给其他的文件。
    而要完成这种运行多进程的要求,需要有底层和上层的智慧:
    底层的机制(low-level mechanisms),比如:上下文切换(context switch)。
    而上层的智慧(high-level intelligence),是调度策略(scheduling policies)。策略可能会利用一些历史信息(比如:上一分钟哪个程序运行的时间多)、或负载信息(比如:是什么类型的程序在运行)、或性能指标(比如:交互性能或者吞吐量是否优化?)来做出程序上的调度决定。

    为了明白什么组成了进程,需要知道它的机器状态(machine state):

    • 一个是内存,指令在内存中,数据也在内存中。因此,进程可以访问的内存(地址空间,address space)是进程的一部分。
    • 另一部分是寄存器,很多指令显性地读和更新寄存器。还有一些特殊的寄存器,比如,程序计数器(PC, short for program counter)有时也被叫做指令指针(IP,short for instruction pointer),它告诉我们程序的下一条执行的指令是什么;栈指针(stack pointer)和相关的帧指针(frame pointer)被用来管理栈(包含函数参数,局部变量和返回地址)。
    • 最后,可能还有程序访问的存储设备。这些I/O的可能包括一系列进程已经打开的文件。

    Process API:
    Create:双击应用打开,或者在shell里面使用指令,OS都会创建新的进程来执行程序。
    Destroy:有创建就得有销毁。
    Wait:等待进程结束运行。
    Miscellaneous Control:暂停进程,然后恢复。
    Status: 进程的状态信息,比如:运行了多久了,或者当前进程处于什么状态。

    如何创建进程?

    • 首先,OS把代码和静态数据(比如,已初始化的变量)加载到内存(进程的地址空间)。这些程序最初以可执行格式存在于硬盘。
      早期的OS,以积极的(eagerly)方式加载,而现在的OS,以懒惰的(lazily)方式加载,需要执行的时候再加载,而不是一次性都先加载。(这部分涉及到页(paging)和交换(swapping)。)
    • 然后,需要初始化运行时的栈(run-time stack)。比如前面提到的C语言的栈,包含局部变量,函数参数,返回地址。OS可能把argc和argv填充给main()函数了。
    • 然后,需要分配内存给程序的堆(heap)。C程序中,使用malloc和free函数来动态请求和释放空间。一些有趣的数据结构比如链表、哈希表、树等需要用到heap。
    • OS还会做其他的初始化任务,比如相关的I/O。例如,UNIX系统中,每个进程默认有三个打开的文件描述符(file descriptors),标准输入,输出和错误。(standard input, output and error)。这些描述符容易让程序从终端中读取输入和输出到屏幕。
    • 做完这些,OS要设置程序执行的阶段。将程序开始在main()函数,跳到main()对应的路线(routine),然后把CPU的控制权交给新建的进程,进程就开始执行。

    进程状态:
    运行(running):正在处理器运行。
    准备(ready):资源什么的都准备好,但是OS没选该进程运行。
    阻塞(blocked):因为I/O请求而阻塞进程,而将处理器资源给其他的进程。
    ……可能还有初始(initial)状态(刚被创建)、最终(final)状态(退出但没被OS清理,也叫僵尸(zombie)状态)等。父(parent)进程检查进程的状态,如果进程返回的值正常,则说明子进程成功完成了任务。父进程应该等待(wait())子进程的完成,然后清理进程的相关数据结构。

    OS持有的进程列表包含了所有进程的信息。每一项有时被叫做进程控制块(PCB,short for process control block),其实就是包含特定进程信息的结构。

    仿真:IO阻塞的时候是否切换别的进程?别的进程切换之后,是否优先运行IO之前阻塞的进程?这些都会影响到最终耗费的CPU时间。

    系统调用:fork()用来创建一个新的进程。尝试运行以下程序。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
        printf("hello world (pid:%d)\n", (int)getpid());
        int rc = fork();
        if (rc < 0)
        {
            // fork failed
            fprintf(stderr, "fork failed\n");
            exit(1);
        }
        else if (rc == 0)
        {
            // child (new process)
            printf("hello, I am child (pid:%d)\n", (int)getpid());
        }
        else
        {
            // parent goes down this path (main)
            printf("hello, I am parent of %d (pid:%d)\n",
                   rc, (int)getpid());
        }
        return 0;
    }
    

    首先,打印hello world和当前的pid(short for process identifier)。然后调用fork()新建进程,奇怪的是,新建的进程几乎是被调用进程的确切(exact)拷贝。但是,是从fork这里开始,而不是从main开始的。

    hello world (pid:14622)
    hello, I am parent of 14623 (pid:14622)
    hello, I am child (pid:14623)
    

    但是,fork返回值在两种进程是不同的,新建(子)进程为0,而父为正,所以可以像上面的程序一样处理不同进程的逻辑。而且可能子进程比父进程先打印。执行的顺序不是预先决定好的(non-determinism),会带来一些有趣的问题,特别是在多线程(multi-threaded)的程序中。而想预先决定好,下面的wait()可以试试。

    系统调用:wait()可以做一些等待的操作。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    int main(int argc, char *argv[])
    {
        printf("hello world (pid:%d)\n", (int)getpid());
        int rc = fork();
        if (rc < 0)
        { // fork failed; exit
            fprintf(stderr, "fork failed\n");
            exit(1);
        }
        else if (rc == 0)
        { // child (new process)
            printf("hello, I am child (pid:%d)\n", (int)getpid());
        }
        else
        { // parent goes down this path (main)
            int rc_wait = wait(NULL);
            printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n",
                   rc, rc_wait, (int)getpid());
        }
        return 0;
    }
    

    依然是打印,但是父进程的先wait(),然后打印wait的返回值。

    hello world (pid:15222)
    hello, I am child (pid:15223)
    hello, I am parent of 15223 (rc_wait:15223) (pid:15222)
    

    如果子进程先运行,那child先打印;如果父进程先运行,会调用wait(),等待子进程结束后再运行。因此,总是子进程先打印。(有一些例子导致wait()比子进程早返回,需要使用man查看相关细节。)

    最后是exec():在linux,有很多种变种execl(), execlp(), execle(), execv(), execvp(), and execvpe()。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        printf("hello world (pid:%d)\n", (int)getpid());
        int rc = fork();
        if (rc < 0)
        { // fork failed; exit
            fprintf(stderr, "fork failed\n");
            exit(1);
        }
        else if (rc == 0)
        { // child (new process)
            printf("hello, I am child (pid:%d)\n", (int)getpid());
            char *myargs[3];
            myargs[0] = strdup("wc");   // program: "wc" (word count)
            myargs[1] = strdup("p3.c"); // argument: file to count
            myargs[2] = NULL;           // marks end of array
            execvp(myargs[0], myargs);  // runs word count
            printf("this shouldn’t print out");
        }
        else
        { // parent goes down this path (main)
            int rc_wait = wait(NULL);
            printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n",
                   rc, rc_wait, (int)getpid());
        }
        return 0;
    }
    

    exec()并不产生新的进程,而是把代码和静态数据从新的执行中覆盖。程序内存空间的堆和栈重新初始化了。而且也一去不复返,不会继续后面的程序内容了("this shouldn’t print out"不会执行并打印了)。

    为什么这个接口会这么奇怪?fork()和exec()的区分,可以帮助构建一个UNIX shell程序。

    prompt> wc p3.c > newfile.txt
    

    wc之后,重定向输出到newfile.txt。shell可以很轻易的做到:fork()进程之后,在执行exec()之前,将标准输出关闭,并打开文件newfile.txt,这样,执行exec()时候的输出就都导向文件了。
    比如下面你的函数,就是这么干的:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        int rc = fork();
        if (rc < 0)
        {
            // fork failed
            fprintf(stderr, "fork failed\n");
            exit(1);
        }
        else if (rc == 0)
        {
            // child: redirect standard output to a file
            close(STDOUT_FILENO);
            open("./p4.output", O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);
    
            // now exec "wc"...
            char *myargs[3];
            myargs[0] = strdup("wc");   // program: wc (word count)
            myargs[1] = strdup("p4.c"); // arg: file to count
            myargs[2] = NULL;           // mark end of array
            execvp(myargs[0], myargs);  // runs word count
        }
        else
        {
            // parent goes down this path (main)
            int rc_wait = wait(NULL);
        }
        return 0;
    }
    

    当open()被调用,STDOUT FILENO是第一个可用的文件描述符。
    UNIX的管道(pipes)也是类似的,但是使用的是pipe()系统调用。一个进程的输出连接到内核的管道,这部分可以无缝地作为另一个进程的输入。比如在shell中使用"|"来达到进程连接管道:

    grep -o foo file | wc -l
    

    除了这几个API,还有kill()系统调用(听说killall更容易用),是用来发送任意信号(signals)给进程的,进而改变进程的状态。而一些按键的组合比如ctrl-c发送了SIGINT(中断,interrupt)给当前运行的进程。ctrl-z发送SIGTSTP(停止,stop)信号,来暂停进程。后续可以通过“fg”或别的命令来恢复进程。一个进程需要使用signal()系统调用来捕捉多种信号,才能对特定的信号做出响应。
    谁能发信号?现代的OS强调用户(user)的概念,当用户登录之后,OS把资源分不同的份给每个用户,每个用户可以控制自己的进程。

    "ps"可以看当前的processes。
    "top"可以看processes和对应使用的资源,比如CPU。

    超级用户(superuser, root):一个系统需要有管理的人,不会受到大部分系统用户的的限制。可以控制其他用户的进程,也可以执行一些命令比如“shutdown”,但是为了安全性,尽可能在普通用户(regular user)下处理。

    相关文章

      网友评论

        本文标题:操作系统-笔记2

        本文链接:https://www.haomeiwen.com/subject/zpkxyhtx.html