操作系统-笔记2

虚拟化:
进程(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)下处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容