几个概念
- CPU、虚拟CPU
- 进程
- 内存、虚拟地址空间
- 物理的CPU被OS虚拟成了多个虚拟的CPU,这些虚拟CPU分别运行各自的程序,这些正在运行的程序被称为进程。
- 物理内存被OS虚拟成了多个虚拟地址空间,每个进程都有独立的、自己的地址空间,程序的指令和数据都在地址空间中
- 磁盘被OS虚拟化为文件系统,文件是被多个程序共享的,它并不是多个虚拟的磁盘,不过也不是无条件共享,涉及到例如互斥共享等多个问题,以后再谈。
1 Virtualizing the CPU
我们在Linux系统上运行C语言程序,体会一下虚拟化的意义。
Windows对多用户的支持不是很好,相关的系统API可能也没有,推荐适用Linux或Unix系统。
// cpu.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <assert.h>int main(int argc,int *argv[]){if(argc != 2){fprintf(stderr,"usage:cpu <string>\n");exit(1);}char *str = argv[1];for(int i = 0;i < 4;i++){sleep(1);printf("%s\n",str);}return 0;
}
这个程序很容易,需要运行的时候输入一个参数,比如一个字符,值得解释的是sleep(1)
,也就是让程序暂停1秒,这非常重要,这意味着物理的CPU在这1s时间可以不用执行该进程,转而执行其他进程。
注意,虚拟后的CPU,最终仍然要在真实物理CPU来执行,要想让每个进程都得到执行,那就应该以合理的方式让他们切换执行。
我们先运行一个进程试试看,输入命令./cpu A1
:
打印了4个A1,并且是每隔1s打印一个,这与我们的预期相符。
接下来,我们同时运行多个进程试试看,输入命令./cpu A1 & ./cpu B2 & ./cpu C3 &
按照直观的理解,不应该是
A1
A1
A1
A1
B2
B2
B2
B2
C3
C3
C3
C3
不应该是这样吗?但是看起来这3个进程并不是顺序执行的,而是并发执行的,也就是它们趁着其他进程在sleep
的时候,抢占了CPU去执行自己了(注意,我们假设计算机只有1个CPU,而且是单核的)。
这样一来,就出现了图中的乱序了。
我们也能充分的感受到,不要让物理CPU闲着的重要理念,同时我们也能想象到,多个进程同时执行,就会涉及到更多的问题,如果是之前的顺序执行,我们只需要进程1执行,其他等待–>进程1执行完成,进程2执行,其他等待–>进程2执行完成,进程3执行–>进程3执行完成。
也就是说,我们只需要等着一个程序执行完,再执行其他程序,这样很简单,但是效率非常低,比如,如果正在执行的程序不使用CPU,去“sleep”了,或者去找I/O设备“玩”了,CPU就只能呆着,其他程序也不能进来执行,CPU利用率很低。
为了避免这种问题,现代OS都采用了类似多道批处理的技术,正在执行的程序不执行时,其他程序会进入CPU执行,而不会允许CPU空闲,要榨干CPU!
就如上面的程序,当一个进程sleep的时候,其他进程就会进入CPU执行,但是,具体如何执行,取决于OS的调度程序,取决于OS设计的策略,所以目前我们还不能得知它具体是如何运作的(也许你可以查看Linux内核,不过如果你有此能力,就不会看见这篇文章了)。
1.1 补充:实例中的C语言知识
以下请自学
1.1.1 main函数参数,argc和argv
1.1.2 fprintf()
1.1.3 sleep()
2 Virtualizing Memory
我们先上代码
// mem.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main(int argc,int *argv[]){int *p = malloc(sizeof(int));
// assert(p != NULL);printf("(%d) memory address of p: %08x\n",getpid(),(unsigned)p);*p = 0;for(int i = 0;i < 4;i++){sleep(1);*p = *p + 1;printf("(%d) p: %d\n",getpid(),*p);}return 0;
}
运行程序./mem
运行多个进程:./mem & ./mem & ./mem &
这里,我们依然能够看到的是虚拟化CPU,不过,虚拟化内存在哪里呢?目前还看不出来,因为Linux默认是启动地址空间随机化的,这样会让系统更安全,不易受到攻击,不过为了展现虚拟化内存,我们应该关掉它。
输入命令sysctl -w kernel.randomize_va_space=0
,再输入./mem & ./mem & ./mem &
我们可以看到,三个进程居然地址完全一样!按理说,1个地址只能对应1个进程,所以,你就能体会到虚拟地址空间的含义了,这并不是真实的物理地址,它会通过某种机制,映射到真实物理地址去。
3 Sharing Disk Information
还记得我们刚才的两个程序吗?他们同时启动了多个进程,并且,这几个进程是同一个程序,也就是说,同一个存储在磁盘的文件,被多次读取到了内存,这也就意味着,磁盘信息是可以被同时多次读取的,我们也可以说,这几个进程共享了一个磁盘文件。
思考:为什么内存和CPU要虚拟化为多个,而磁盘却是共享的?
- 进程是运行中的程序,它是“活的;
- 程序是静止在磁盘中的指令和数据,它是“死的”。
对于正在运行的进程来说,我们需要为其独立地分配一整套生态系统,保证它正常执行,并且每个程序运行时候的结果可能不同,所以,就虚拟地提供了CPU和地址空间,让它们是相互独立的;而对于静止的指令和数据来说,完全没有必要虚拟成多份,那反而是浪费空间,当然这是针对读取而言,写入还需要视情况,不过整体来说,读取信息是及其场景的,将磁盘设为共享也是合理的。
另外要谈的是,磁盘文件必须通过软件和硬件协作的方式,使其持久地保存,而不是很快就消失了,或者被其他数据覆盖掉了。
4 Concurrency
虚拟化对应的是进程,而并发对应的不仅仅是OS的进程,在OS之上的应用程序,也存在并发的问题,他就是多线程编程;虚拟化让一个CPU能并发地执行多个进程,而一个进程,也能并发地执行多个线程。
你一定知道多线程编程,是的,就是那个,我们现在重新审视一下它。
// threads.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>volatile int counter = 0;
int loops;void *worker(void *arg) {int i;for (i = 0; i < loops; i++) {counter++;}return NULL;
}int main(int argc, char *argv[]) {if (argc != 2) {fprintf(stderr, "usage: threads <value>\n");exit(1);}loops = atoi(argv[1]);pthread_t p1, p2;printf("Initial value : %d\n", counter);pthread_create(&p1, NULL, worker, NULL);pthread_create(&p2, NULL, worker, NULL);pthread_join(p1, NULL);pthread_join(p2, NULL);printf("Final value : %d\n", counter);return 0;
}
我们进行编译gcc threads.c -o threads -lpthread
,注意,<pthread.h>
不是Linux默认的库,编译链接需要加上参数-lpthread
,也就是需要链接额外的Import Library:libpthread.a
。
我们进行测试:
对于输入的参数N,输出结果应该是2N(先知道事实,看不懂多线程程序没有关系),但是最后两个,当参数足够大,比如5亿的时候,结果就诡异了。
这是由于计数器的值的更新不是原子操作,他需要:
- 内存–>寄存器
- 寄存器递增
- 寄存器–>内存
3个步骤,但是,这几个步骤可能被其他操作打断,这就造成了结果的诡异。关于原子操作以后再说。
5 小结
我们谈了几件事儿
- 物理CPU – 虚拟化CPU – 多进程并发
- 物理内存 – 虚拟地址空间 – 进程独立地址空间
- 磁盘(持久性) – 文件系统 – 共享磁盘信息
- OS之上的并发:单个进程中的多线程
版权声明
本文是读书笔记,来自于书籍《Operating System:Three Easy Pieces》