虚拟化
操作系统的基本抽象——进程。
人们希望同时运行多个程序,但CPU核心往往是屈指可数的。为了使得每个程序都有自己的CPU可用(至少看起来是这样的),系统将CPU虚拟化,让一个进程只运行一个“时间片”,然后切换到其他进程——即时分共享CPU计数
但是很明显进程共享CPU,进程间的切换会有额外的性能消耗
与“时分共享”相对的,磁盘空间就是一个“空分共享”资源
- 低级机制:实现所需功能的方法或协议,“如何实现?”
- 高级智能:操作系统内做出某种决定的算法,“更明智的决策”
进程
进程是操作系统为正在运行的程序提供的抽象
包括了但不限于诸如:
内存、寄存器、IO信息(当前打开的文件列表)
创建
程序如何被转化为一个进程?
-
首先,要把代码和所有的静态数据从磁盘加载到内存(进程的地址空间)中
现代操作系统一般惰性加载(分页和交换机制)
-
为程序运行时栈、堆分配一些内存(也可能是程序自己申请并初始化的:
malloc()
) -
IO相关的初始化操作
比如UNIX系统中的每个进程默认有三个文件描述符:标准输入、输出和错误
-
最后,通过跳转到
main()
例程,OS将CPU的控制权转移到新创建的进程中,从而程序开始执行
状态
- 就绪
- 运行
- 阻塞
只有“就绪”和“运行”可以相互切换,进程的切换需要额外的数据结构保存其上下文信息(进程列表、PCB)
API
fork()
// 这是关于调用系统API创建新进程的简单例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello world (pid:%d)n", (int)getpid());
int rc = fork();
if (rc < 0)
{
fprintf(stderr, "fork failedn");
exit(1);
}
else if (rc == 0)
{
printf("hello,I am child (pid:%d)n", (int)getpid());
}
else
{
printf("hello,I am parent of %d (pid:%d)n", rc, (int)getpid());
}
return 0;
}
注意这个程序要在Linux(Unix)环境下运行,不然
<unistd.h>
头文件要报错,因为windows下不提供这个
这边运行的输出长这样:
hello world (pid:3125265)
hello,I am parent of 3125266 (pid:3125265)
hello,I am child (pid:3125266)
程序的主要内容就是在主程序中fork()
了一个子进程
注意fork()
之后两个进程是同时运行的,并不存在绝对的顺序(由CPU调度程序决定)
所以可能是parent语句先输出,也可能child语句先输出
另外,新创建的子进程虽然是和父进程完全一样的,但它并不从main入口开始执行,而是从fork()
位置开始,这也是为什么没有再输出一遍hello world
的原因
还有就是,父进程和子进程获取到返回值是不一样的,父进程获取到的是子进程的PID,而子进程获取到的fork()
返回值是0,这也是为什么两个进程运行了不同的分支的原因
wait()
父进程需要等待子进程完成的情况
// 这是关于调用系统API让父进程等待子进程执行完毕的简单例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
printf("hello world (pid:%d)n", (int)getpid());
int rc = fork();
if (rc < 0)
{
fprintf(stderr, "fork failedn");
exit(1);
}
else if (rc == 0)
{
printf("hello,I am child (pid:%d)n", (int)getpid());
}
else
{
int wc = wait(NULL);// 这里等待子进程执行完成
printf("hello,I am parent of %d (wc:%d) (pid:%d)n", rc, wc, (int)getpid());
}
return 0;
}
# 程序输出
hello world (pid:3129576)
hello,I am child (pid:3129577)
hello,I am parent of 3129577 (wc:3129577) (pid:3129576)
exec()
fork()
只能运行与父进程相同的拷贝进程,而exec()
用来运行不同的进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main()
{
printf("hello world (pid:%d)n", (int)getpid());
int rc = fork();
if (rc < 0)
{
fprintf(stderr, "fork failedn");
exit(1);
}
else if (rc == 0)
{
printf("hello,I am child (pid:%d)n", (int)getpid());
char *myargs[3];
myargs[0] = strdup("wc");
myargs[1] = strdup("testExec.c");
myargs[2] = NULL;
execvp(myargs[0], myargs);
printf("this shouldn't print out");
}
else
{
int wc = wait(NULL);
printf("hello,I am parent of %d (wc:%d) (pid:%d)n", rc, wc, (int)getpid());
}
return 0;
}
# 程序输出
hello world (pid:3131031)
hello,I am child (pid:3131032)
33 80 730 testExec.c
hello,I am parent of 3131032 (wc:3131032) (pid:3131031)
让我们看看发生了什么:
首先主程序运行,然后创建了一个子进程并等待子进程执行完毕
在子进程中调用了execvp()
来运行字符计数程序wc(基于本文件,打印出有多少行、多少单词、字节)
exec()
有诸多变体
很奇怪,exec()
并没有创建新进程,而是直接将当前运行的程序替换为不同的运行程序
“对
exec()
的成功调用永远不会返回”?什么意思
为什么这么设计?把fork()
和exec()
分开?
给了shell在
fork()
之后,exec()
之前运行代码的机会,在运行新程序前改变环境,比如:输出重定向,以下是个示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
int rc = fork();
if (rc < 0)
{
fprintf(stderr, "fork failedn");
exit(1);
}
else if (rc == 0)
{
// 重定向标准输出到指定文件
close(STDOUT_FILENO);
open("./test4.output", O_CREAT | O_WRONLY | O_TRUNC | S_IRWXU);
char *myargs[3];
myargs[0] = strdup("wc");
myargs[1] = strdup("test4.c");
myargs[2] = NULL;
execvp(myargs[0], myargs);
}
else
{
int wc = wait(NULL);
}
return 0;
}
# 输出到test4.output文件中
cat test4.output
32 68 604 test4.c
机制:受限直接执行
前面我们说了,CPU的虚拟化可以通过时间片轮转的而方式,但是这样的做法同样会带来一些挑战:
-
首先是进程切换的性能开销,不可避免,但是必须控制在尽量小的范围
-
控制权,操作系统不能将CPU等系统资源的控制权完全放出
防止可能的程序恶意行为,以及需要保留能够对运行中的程序进行调度的能力
即:高效、可控