我们在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内核,不过如果你有此能力,就不会看见这篇文章了)。
以下请自学
我们先上代码
// 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个进程,所以,你就能体会到虚拟地址空间的含义了,这并不是真实的物理地址,它会通过某种机制,映射到真实物理地址去。
还记得我们刚才的两个程序吗?他们同时启动了多个进程,并且,这几个进程是同一个程序,也就是说,同一个存储在磁盘的文件,被多次读取到了内存,这也就意味着,磁盘信息是可以被同时多次读取的,我们也可以说,这几个进程共享了一个磁盘文件。
对于正在运行的进程来说,我们需要为其独立地分配一整套生态系统,保证它正常执行,并且每个程序运行时候的结果可能不同,所以,就虚拟地提供了CPU和地址空间,让它们是相互独立的;而对于静止的指令和数据来说,完全没有必要虚拟成多份,那反而是浪费空间,当然这是针对读取而言,写入还需要视情况,不过整体来说,读取信息是及其场景的,将磁盘设为共享也是合理的。
另外要谈的是,磁盘文件必须通过软件和硬件协作的方式,使其持久地保存,而不是很快就消失了,或者被其他数据覆盖掉了。
虚拟化对应的是进程,而并发对应的不仅仅是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个步骤,但是,这几个步骤可能被其他操作打断,这就造成了结果的诡异。关于原子操作以后再说。
我们谈了几件事儿
物理CPU – 虚拟化CPU – 多进程并发物理内存 – 虚拟地址空间 – 进程独立地址空间磁盘(持久性) – 文件系统 – 共享磁盘信息OS之上的并发:单个进程中的多线程本文是读书笔记,来自于书籍《Operating System:Three Easy Pieces》