一、虚拟CPU和虚拟内存:
1、虚拟cpu:
利用进程机制,所有的现代操作系统都支持在同一时间来完成多个任务。尽管某个时刻,真实的CPU只能运行一个进程,但是从进程自己的角度来看,它会认为自己在独享CPU(即虚拟CPU),而从用户的角度来看多个进程在一段时间内是同时执行的,即并发执行。在实际的实现中,操作系统会使用调度器来分配CPU资源。调度器会根据策略和优先级来给各个进程一定的时间来占用CPU,进程占用CPU时间的基本单位称为时间片,当进程不应该使用CPU资源时,调度器会抢占CPU的控制权,然后快速地切换CPU的使用进程。这种切换在用户的视角中对程序执行毫无影响,可以认为是透明的。由于切换进程消耗的时间和每个进程实际执行的时间片是非常小的,以至于用户无法分辨,所以在用户看来,多个进程是在同时进行的。
1.1、调度器和优先级:
Linux内核存在一个专门用来调度进程的内核线程,称为调度器。调度器的基本工作是从一组处于可执行状态的进程中选择一个来执行,Linux提供了抢占式的多任务模式。调度器会决定某个进程在什么时候停止运行,并且可以让另一个线程进入执行,这个动作即所谓的抢占。
传统的Linux操作系统采用的时间片轮转法。这种方法的大致流程是:调度器它把所有的可运行的非实时的进程组织在一起,这个数据结构被称为就绪队列。通常调度器取一个固定时间作为调度周期,当一个调度周期开始的时候,调度器回味就绪队列当中的每个进程分配时间片,每个进程能够获取的时间片长度和进程的优先级有关。正在执行的进程会在执行的过程中逐渐消耗它的时间片,当时间片耗尽时,如果该进程仍然处于运行状态,那么它就会被就绪队列中的下一个进程抢占。如果整个调度周期执行完成,调度器就会抢占该进程,并且根据优先级分配新的时间片。
在进程执行过程中,会出现等待IO操作的情况。此时,进程就会从就绪队列当中移除,并且将其放入等待队列,并且将自己的状态调整为等待状态。进程运行终止时,进程也会从就绪队列中移除。
当进程被创建或者被从等待状态唤醒时,调度器会根据优先级分配时间片,再将其插入到就绪队列当中。
二、虚拟内存
在进程本身的视角中,它除了会认为CPU是独占的以外,它还会以为自己是内存空间的独占者,这种从进程视角看到的内存空间被称为虚拟内存空间。当操作系统中有多个进程同时运行时,为了避免真实的物理内存访问在不同进程之间发生冲突,操作系统需要提供一种机制在虚拟内存和真实的物理内存之间建立映射。
二、fork的写时复制:
当执行了fork了以后,父子进程地址空间的内容是完全一致,所以完全可以共享同一片物理内存,也就是父子进程的同一个虚拟地址会对应同一个物理内存字节。通常来说,内存的分配单位是页,我们可以为每一个内存页维持一个引用计数。代码段的部分因为只读,所以完全可以多个进程同时共享。而对于地址空间的其它部分,当进程对某个内存页进行写入操作的时候,我们再真正执行被修改的虚拟内存页分配物理内存并拷贝数据,这就是所谓的写时复制。在执行拷贝以后,同样的虚拟地址就无法对应同样的物理内存字节了。
再探写时复制:
当父进程使用fork创建了一个子进程以后,操作系统需要为子进程拷贝一份父进程的页表到内存当中(基本这是系统调用最令人烦恼的开销)。当子进程的某个可以写入的页被第一次写入的时候,内核会意识到对应的物理页是属于父进程的,此时会为子进程分配一个新的物理页,如果要分配的物理页不在主存储器中,此时会出发一个异常名为缺页异常,当异常处理完成以后,子进程会被分配一个新的物理页,并且物理页内容会是原来页的拷贝。这就是所谓的写时复制。
三、文件映射:
文件映射基本使用
使用mmap系统调用可以实现文件映射功能,也就是将一个磁盘文件直接映射到内存用户态地址空间的一片区域当中,这样的话,内存内容和磁盘文件内容一一对应,也不再使用read和write系统调用就可以进行IO操作,直接读写内存数据即可。
需要注意的是,mmap不能修改文件的大小,所以需要配合函数ftruncate来使用。
#include <sys/mman.h>
void *mmap(void *adr,size_t len,int prot,int flag,int fd,off_t off);
- addr参数用于指定映射存储区的起始位置。这里设置为NULL,这样就由系统自动分配(一般是分配在堆空间里面);
- fd参数是文件描述符,用来指示要建立映射的文件。
- prot参数用来表示权限,其中PROT_READ|PROT_WRITE表示可读可写(open的flag应该填写O_RDWR);
- flag参数在目前是采用MAP_SHARED。
下面是使用mmap的例子:
//假设文件本身的内容是hello#include <func.h>
int main(int argc, char *argv[])
{// ./mmap file1ARGS_CHECK(argc, 2);// 先open文件int fd = open(argv[1], O_RDWR);ERROR_CHECK(fd, -1, "open");// 建立内存和磁盘之间的映射char *p = (char *)mmap(NULL, 5, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);ERROR_CHECK(p, MAP_FAILED, "mmap"); // mmap失败返回不是NULLfor (int i = 0; i < 5; ++i){printf("%c", *(p + i));}printf("\n");*(p + 4) = '0';for (int i = 0; i < 10; ++i){printf("%c", *(p + i));}munmap(p, 5);close(fd);return 0;
}
输出结果如下:
文件映射的底层原理:
一个经常讨论的问题就是使用mmap和read/write的效率到底谁更高?这个问题的答案依赖问题发生所在的场景。从原理上来说,read/write是让数据在内核态的文件对象和用户态内存之间进行来回拷贝,文件对象会和一片由操作系统管理的内存区域(被称为页缓存)相关联,一般来说,操作系统会选择一个合适策略并使用专门的硬件(比如DMA设备)来同步磁盘和页缓存当中的内容,这样read/write操作最终就会影响到磁盘。而mmap的处理就更加简单粗暴,它直接把页缓存的一部分映射到用户态内存,这样在用户态当中的操作就直接对应页缓存的操作。
这样看上去的话,mmap的效率总是会比read/write更加高,因为它避免了一次数据在用户态和内核态之间的拷贝。但是考虑到read/write的特殊性质--它们总是顺序地而不是随机地访问磁盘文件的内容,所以操作系统可以根据这个特点进行优化,比如文件内容的预读等,最终经过测试--read/write在顺序读写的时候性能更好,而mmap在随机访问的时候性能更好。