操作系统导论(二)----虚拟化内存

#### 那些在自己的领域超凡脱俗的人,比那些相当优秀的人强的不是一点点 ####
#### 努力工作,你也可能会成为这种“以一当百”的人 ####
#### 做不到的话,就和这样的人一起工作,你会明白什么是“听君一席话,胜读十年书” ####
#### 如果都做不到,那就太难过了 ####

一、 虚拟化内存导论
    · 理解硬件和操作系统交互的过程有助于学习虚拟内存
    
    · 基址/界限 ---> TLB和多级页表 ---> 全功能的现代虚拟内存管理程序的工作原理

    · 理解虚拟内存:
        1. 用户程序生成的每个地址都是虚拟地址;
        2. 操作系统只是为每个进程提供一个假象;
        3. 操作系统拥有自己的大量内存; 在硬件的帮助下,操作系统会将这些假的虚拟内存变成真实的物理地址,从而能够找到想要的信息;
    
    · 虚拟内存的意义:  (为了易于使用;隔离;保护)
        1. 操作系统会让每个程序觉得,他有一个很大的连续地址空间来放入其代码和数据;
        2. 作为程序员,不必担心具体在哪存储变量 (数据);
        3. 程序的虚拟地址空间很大,有很多空间可以存代码和数据;
        4. 不希望一个错误的程序能够读取或者覆盖其他程序的内存
    
    · 对于错误的进程行为,正确的操作反应是要 杀死 违规进程;


二、 补充 --- 常见进程API
    · fork():
        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){
                            fprintf(stderr,"fork failed\n");
                            exit(1);
                    }else if (rc==0){
                            printf("i am child (pid:%d)\n",(int) getpid());
                    }else{
                            printf("i am %d 的 father (pid:%d)\n",rc,(int) getpid());
                    }
                    return 0;
            }
        输出为:
            [root@localhost ~]# ./a.out 
            hello world (pid:1902)
            i am 1903 的 father (pid:1902)
            i am child (pid:1903)
            [root@localhost ~]# 
        fork()创建的进程与调用的进程完全一样,
        对操作系统来说,这时看起来有两个完全一样的程序在运行,并且都从fork()系统调用中返回。
        
        新创建的进程被称为子进程,原来的进程被称为父进程;
          子进程不会从main()函数开始执行 (所以没有执行hello world),而是直接从fork()系统调用返回,就像他自己执行了fork()一样;
        子进程不是完全拷贝了父进程。具体来说,它拥有自己的虚拟地址空间、寄存器、程序计数器...
            父进程的fork()返回值是新创建的子进程的pid,子进程的fork()返回值是0。

    · wait()
        wait()用于等待子进程执行完毕;

    · exec()
        exec()可以让子进程执行与父程序不同的程序;
        给定exec()程序名称和需要的参数之后,exec()可以从目标程序加载代码和静态数据,并且用它复写自己的代码段/静态数据;
        堆、栈以及其他内存空间也会被重新初始化。然后执行指定的程序;
        exec()并没有创建新的进程,而是直接将当前的运行的程序替换为目标的程序;
    
    · 为什么这样设置创建新进程的API?
        将fork()和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[])
            {
                printf("hello world (pid:%d)\n",(int) getpid());
                int rc=fork();
                if (rc < 0){
                         fprintf(stderr,"fork failed\n");
                           exit(1);
                }else if (rc==0){
                            close(STDOUT_FILENO);
                            open("./out.txt",O_CREAT|O_WRONLY|O_TRUNC,S_IRWXU);

                            char *myargs[3];
                            myargs[0]=strdup("ls");
                            myargs[1]=strdup("./dict");
                            myargs[2]=NULL;
                            execvp(myargs[0],myargs);

                            printf("i am child (pid:%d)\n",(int) getpid());
                    }else{
                            printf("i am %d 的 father (pid:%d)\n",rc,(int) getpid());
                    }
                    return 0;
            }
        以上代码先fork创建了一个子进程,
          在子进程(rc == 0)中用exec执行之前打开"out.txt"文件,
          再调用exec()执行"ls dict"指令;
          在父进程中调用wait()等待子进程结束再执行;
        输出为:
            [root@localhost ~]# ./a.out 
            hello world (pid:1902)
            i am 1903 的 father (pid:1902)
            [root@localhost ~]# cat out.txt 
            1
            2
            3
            4
            [root@localhost ~]# 
        可以发现fork()的子进程没有修改父进程的数据,exec()调用了"ls"程序,fork+exec的组合可以完成一些看起来复杂的问题;
    
    · 其他API
        kill():向进程发送信号,包括要求睡眠、终止或者其他有用的指令;
        ps:查看当前执行的进程;
        top:查看当前系统中进程消耗资源的情况;
        ...


三、地址空间    (抽象)
    · 虚拟化内存的发展
        1. 最开始的操作系统是一组函数在内存中,运行的程序也在内存中使用剩余的内存;
        2. 为了提高效率,进入了多道程序和时分共享时代;
            一开始实现时分共享的方法:先让进程占据内存并运行一段时间,
                                     然后停止它并将所有状态信息保存在磁盘上,
                                     再加载其他进程的状态信息,再运行一段时间;
            问题是将数据保存到磁盘太慢了,因此将进程信息放到内存中可以更有效的实现时分共享;
            但是当多个程序同时放在内存中,保护变成了重要的问题,因为大家不希望一个进程可以访问其他的进程。
        3. 地址空间
    
    · 隔离原则
        隔离是建立可靠系统的关键原则;
        如果两个实体相互隔离,意味着一个实体的失败不会影响另一个实体;
        操作系统为了防止进程之间相互造成伤害,让进程隔离;
        通过内存隔离,操作系统进一步确保运行程序不会影响底层操作系统的操作;
        现代操作系统通过将操作系统的部分与其他部分隔离开,实现进一步隔离的微内核,这样可以比整体的内核提供更大可靠性;
    
    · 地址空间
        地址空间是物理地址的抽象,是了解内存虚拟化的关键;
        
        地址空间包含运行程序的所有内存状态:
            1. 程序的代码   (code,指令)
            2. 利用栈来保存当前函数的调用信息,分配空间给局部变量、传递参数、函数返回值;
            3. 利用堆来管理动态分配、用户管理的内存;就像从C语言中调用 malloc()和new()获得的内存;
            4. 其他东西:静态初始化获得的变量...
        
        地址空间的分配:
            1. 程序的代码是静态的,放在地址空间的顶部;
            2. 地址空间中还有两个可能会增长,那就是堆和栈,所以将他们放在地址空间的两端 (堆在顶部,栈在底部)
        
        内存虚拟化的关键是 操作系统要在硬件的支持下,将地址空间上的地址映射到正确的物理地址上。
    
    · 虚拟化内存的目标
        1. 透明:运行的程序不应该感知到内存被虚拟化 (透明指的是很难被注意到,而不是公之于众)
        2. 效率:操作系统应该在时间 (不会减慢加载速度)和空间 (不需要太多额外的空间来虚拟化内存)上都要追求高效。
        3. 保护:操作系统应该确保进程收到保护,不会受到其他进程的影响;
                 当进程执行加载、运行或指令提取时,不应该以任何方式访问任何其他进程或操作系统本身的内存内容;
                 为进程之间提供隔离,每个进程都有自己独立的运行环境,避免其他出错或恶意进程的影响;
    
    · 所有能看到的地址都是假的
        在用户层面,可以看到的任何地址都是虚拟地址;
        虚拟地址只是提供地址如何在内存中分布的假象,只有操作系统才知道物理地址
    
    · 问题:
        1. 虚拟化内存的机制
        2. 虚拟化内存的策略
    
    ·总结:
        虚拟内存系统负责为程序提供一个巨大的、稀疏的、私有的地址空间的假象;
          其中保存了程序的所有指令和数据;
        操作系统在硬件的支持下,通过每一个虚拟内存的索引,将其转化为物理地址,
          物理内存根据获得的物理地址取获取所需的信息;
        操作系统会同时堆许多进程执行此操作,并确保程序之间互相不会受到影响,也不会影响操作系统


四、地址转换    (机制)
    · 回顾虚拟化CPU
        采用受限直接访问:
            让程序运行的大部分指令直接访问硬件,只在一些关键点  (如 进程发起系统调用和时钟中断)由操作系统介入;
            来确保 在正确的时间、正确的地点,做正确的事;
        为了实现虚拟化,操作系统应该尽量让程序自己运行,同时通过在关键点的及时介入,
        来保持对硬件的控制;    高效和控制是现代操作系统的两个主要目标。
    
    · 实现虚拟化内存:
        和虚拟化CPU一样,在实现高效和控制的同时,提供期望的虚拟化;
            1. 高效决定了我们要利用硬件的支持,从最开始的使用寄存器,到复杂的 TLB、页表...
            2. 控制意味着操作系统要确保应用程序只能访问他自己的空间;
            3. 最后我们还尽量追求灵活性;即 希望程序能以任何方式访问它自己的地址空间,从而让系统更容易编程
        所以关键问题是:
            1. 如何实现高效的虚拟内存?
            2. 如何提供应用程序所需的灵活性?
            3. 如何控制应用程序可访问的内存位置,从而确保程序的内存访问受到合理的限制?
            4. 如何高效的实现这一切?
    
    · 地址转换
        利用地址转换,硬件对每次内存访问进行处理    (即 获取指令、读取数据、写入)
          将指令中的虚拟地址转换为数据实际存储的物理地址;
          每次内存引用的时候,硬件都会进行地址转换,将应用程序的内存引用重定位到内存中实际的位置;
        仅仅依靠硬件不足以实现虚拟内存;操作系统必须在关键的位置介入,设置好硬件,以便于完成正确的地址转换;
        因此它必须管理内存,记录被占用和空闲的内存位置,并明智而谨慎的介入,保持对内存使用的控制。
    
    · 几个假设:
        用户的地址空间必须连续的放在物理内存中;
        假设地址空间小于物理空间;
        假设每个地址空间大小一样;
    
    · 动态 (基于硬件)重定位
        1. 每个CPU需要两个寄存器:基址 (base)寄存器 和 界限 (bound)寄存器又称限制 (limit)寄存器;
        2. 在程序执行时,将起始地址记录在基址寄存器上,后续该进程的所有内存引用都会加上那个起始地址;
        3. 例如 :
            位于物理地址32KB的程序开始指令 128: movl 0x0 (%ebx), %eax ;
              首先会在基址寄存器上记录32KB,程序计数器(PC)被设置为128,硬件开始执行;
              128被加上基址寄存器中的32KB (32768)得到实际的物理内存地址32896,然后硬件从这个物理地址获取指令;
              接下来处理器处理这个指令,如果需要调用位于栈上16KB处的局部变量,
              处理器同样会将16KB+32KB得到真实的物理地址48KB,从而获取到数据;
        
        将虚拟地址转化为物理地址,这就是所谓的地址转换;
        也就是说,硬件取得进程认为它要访问的地址,将它转换成数据实际位于的物理地址;
        由于这种重定位时在运行时发生的,而且甚至可以在进程开始运行之后改变其地址,这种技术一般称为动态重定位。

        界限寄存器:
            界限寄存器用于提供访问保护,如果界限寄存器被置为16KB,
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值