操作系统导论

操作系统-笔记2

2020-03-19  本文已影响0人  KyoDante

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

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

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

如何创建进程?

进程状态:
运行(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)下处理。

上一篇 下一篇

猜你喜欢

热点阅读