操作系统八股文合集

什么是操作系统
操作系统是管理计算机硬件和软件资源的计算机程序,提供一个计算机用户与计算机硬件系统之间的接口,向上对用户程序提供接口,向下接管硬件资源,操作系统本质上也是一个软件,作为最接近硬件的系统软件,负责处理器管理,存储器管理,设备管理,文件管理和提供用户接口

请介绍一下操作系统中的中断
中断是指CPU对系统发生的某个事件做出的一种反应,CPU暂停正在执行的程序,保存现场后自动去执行相应的处理程序,处理完该事件后再返回中断处继续执行原来的程序。中断一般三类,一类是由CPU外部引起的,如I/O中断,时钟中断,一种是来自CPU内部事件或程序执行引起的中断,例如程序非法操作,地址越界,浮点溢出,最后一种是在程序中使用系统调用引起的。而中断处理一般分为中断响应和中断处理两个步骤,中断响应由硬件实施,中断处理主要由软件实施

系统调用
概念:在计算机中,系统调用指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供了用户程序和操作系统之间的接口

操作系统中的状态分为管态(核心态)和目态(用户态)。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。特权指令:一类只能在核心态下运行而不能在用户态下运行的特殊指令。不同的操作系统特权指令会有所差异,但是一般来说主要是和硬件相关的一些指令。用户程序只在用户态下运行,有时需要访问系统核心功能,这时通过系统调用接口使用系统调用。

应用程序有时会需要一些危险的、权限很高的指令,如果把这些权限放心地交给用户程序是很危险的(比如一个进程可能修改另一个进程的内存区,导致其不能运行),但是又不能完全不给这些权限。于是有了系统调用,危险的指令被包装成系统调用,用户程序只能调用而无权自己运行那些危险的指令。另外,计算机硬件的资源是有限的,为了更好的管理这些资源,所有的资源都由操作系统控制,进程只能向操作系统请求这些资源。操作系统是这些资源的唯一入口,这个入口就是系统调用。

举例:
对文件进行写操作,程序向打开的文件写入字符串“hello world”,open和write都是系统调用。如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(int argc, char *argv[])
{
    if (argc<2)
        return 0;
    //用读写追加方式打开一个已经存在的文件
    int fd = open(argv[1], O_RDWR | O_APPEND);
    if (fd == -1)
    {
        printf("error is %s\n", strerror(errno));
    }
    else
    {
        //打印文件描述符号
        printf("success fd = %d\n", fd);
        char buf[100];
        memset(buf, 0, sizeof(buf));
        strcpy(buf, "hello world\n");
        write(fd, buf, strlen(buf));
        close(fd);
    }
    return 0;
}

还有写数据write,创建进程fork,vfork等都是系统调用。

主机字节序和网络字节序
主机字节序:
又名CPU字节序,其不是由操作系统决定的,而是由CPU指令集架构决定的,主机字节序分为两种
1.大端字节序:高序字节存储在低位地址,低序字节存储在高位地址(符合人)
2.小端字节序:高序字节存储在高位地址,低序字节存储在低位地址(符合机器)

网络字节序:
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型,操作系统等无关,从而保证数据在不同主机之间传输能够被正确解释
网络字节序采用大端的排列方式

什么是内核态什么是用户态
用户态和内核态是操作系统两种运行级别,两者最大的区别是特权级不同。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。内核态和用户态之间的转换方式主要包括:系统调用,异常和中断

操作系统为什么要分用户态和内核态
为了安全性,在CPU的一些指令中,有的指令如果用错,会导致整个系统崩溃。分了内核态和用户态后,当用户需要操作这些指令的时候,内核为其提供了API,可以通过系统调用陷入内核,让内核去执行这些操作

并发和并行的区别
1.并发:指宏观上看起来两个程序在同时运行,比如说在单核CPU上的多任务。但是从微观上看两个程序的指令是交织着运行的,指令之间的交错执行,在单个运行期内只运行了一个指令。这种并发并不能提高计算机的性能,只会提高效率
2.并行:指严格物理意义上的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样来说并行确实提高了计算机的效率,所以现在的CPU都是往多核方面发展

子进程继承了父进程的哪些东西
1.用户号UID和用户组号GID
2.环境Environment
3.堆栈
4.共享内存
5.打开文件的描述符
6.执行时关闭标志
7.信号控制设定
8.进程组号
9.当前工作目录
10.根目录
11.文件方式创建屏蔽字
12.资源限制
13.控制终端

子进程独有
1.进程号PID
2.不同的父进程号
3.自己的文件描述符和目录流的拷贝
4.子进程不继承父进程的进程正文,数据和其他锁定内存
5.不继承异步的输入输出
父进程和子进程拥有独立的地址空间和PID参数

** fork和vfork**
fork:创建一个和当前进程映像一样的进程可以通过fork()系统调用

#include<sys/types.h>
#include<unistd.h>

pid_t fork(void)

成功调用fork()会创建一个新的进程,它几乎与调用fork()的进程一摸一样。这两个进程都会继续运行,在子进程中,成功的fork()调用会返回0。在父进程中fork()返回子进程的pid。如果出现错误,fork()返回一个负值

最常见的fork()用法是创建一个新的进程,然后使用exec()载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像,这种派生加执行的方法很是常见

在早期的unix系统中,创建进程比较原始,当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中,但从内核角度来说,逐页复制的方式是十分耗时的。现代Unix系统采用了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制

vfork的基础知识:
在实现写时复制之前,Unix的设计者们就一直很关注在fork后立即执行exec所造成的地址空间的浪费

#include<sys/types.h>
#include<unistd.h>

pid_t vfork(void);

除了子进程必须要立刻执行一次对exec的系统调用,或者调用_exit()退出,对vfork()的成功调用所产生的结果和fork()一样,vfork()会挂起父进程直到子进程终止或者运行了一个新的可执行文件的映像。通过这样的方式,vfork()避免了地址空间的按页复制。在这个过程中,父进程和子进程共享相同的地址空间和页表项目,实际上vfork()只完成了一件事:复制内部的内核数据结构,因此,紫禁城也就不能修改地址空间中的任何内存

vfork( )是一个历史遗留产物,Linux本不应该实现它。需要注意的是,即使增加了写时复制,vfork( )也要比fork( )快,因为它没有进行页表项的复制。然而,写时复制的出现减少了对于替换fork( )争论。实际上,直到2.2.0内核,vfork( )只是一个封装过的fork( )。因为对vfork( )的需求要小于fork( ),所以vfork( )的这种实现方式是可行的。

写时复制:
Linux采用了写时复制的方法,以减少fork时对父进程空间进程整体复制带来的消耗

写时复制是一种采用了惰性优化方法来避免复制时的系统开销,它的前提很简单:如果有多个进程要读取他们自己的那部分资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。只要没有进程要去修改自己的“副本”,就存在这样的幻觉:每个进程好像独占那个资源,从而避免了复制到来的负担,。如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。所以这就是名称的由来:在写入时进行复制。

在使用虚拟内存的情况下,写时复制(Copy-On-Write)是以页为基础进行的。所以,只要进程不修改它全部的地址空间,那么就不必复制整个地址空间。在fork( )调用结束后,父进程和子进程都相信它们有一个自己的地址空间,但实际上它们共享父进程的原始页,接下来这些页又可以被其他的父进程或子进程共享。

写时复制在内核中的实现非常简单。与内核页相关的数据结构可以被标记为只读和写时复制。如果有进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断的方式就是对该页进行一次透明复制。这时会清除页面的COW属性,表示着它不再被共享。

现代的计算机系统结构中都在内存管理单元(MMU)提供了硬件级别的写时复制支持,所以实现是很容易的。

在调用fork( )时,写时复制是有很大优势的。因为大量的fork之后都会跟着执行exec,那么复制整个父进程地址空间中的内容到子进程的地址空间完全是在浪费时间:如果子进程立刻执行一个新的二进制可执行文件的映像,它先前的地址空间就会被交换出去。写时复制可以对这种情况进行优化。

fork和vfork的区别:
1.fork()的子进程拷贝父进程的数据段和代码段;vfork()的子进程与父进程共享数据段
2.fork()的父子进程的执行次序不确定;vfork()保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行
3.vfork()保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程进一步动作,则会导致死锁
4.当需要改变共享数据段中变量的值,则拷贝父进程

进程间通信方法
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程间要交换数据必须要通过内核在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区中把数据读走,内核提供这种机制称为进程间通信

不同进程间通信的本质:进程之间可以看到一份公共资源,而提供这份资源的形式或者提供者不同造成了通信方式不同

进程间通信主要包括管道,系统IPC(包括消息队列,信号量,信号,共享内存等)以及套接字socket

管道
1.无名管道
他是半双工的,具有固定的读端和写端;它只能用于具有亲缘关系的进程间通信(父子进程或者兄弟进程);它可以看成一种特殊的文件,对于它的读写也可以使用普通的read,write等函数,但是它不是普通的文件,并不属于其他任何文件系统,并且只存于内存中
优点:简单方便
缺点:局限于单向通信,只能创建在它的进程以及其有亲缘关系的进程之间,缓冲区有限

2.FIFO,也成为命名管道
FIFO可以在无关的进程之间交换数据,与无名管道不同,FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
优点:可以实现任意关系的进程间通信
缺点:长期存于系统中,使用不当容易出错;缓冲区有限

3.消息队列:
是消息的链接表,存放于内核中,一个消息队列由一个标识符(队列ID)来标识,具有写权限的进程可以按照一定的规则向消息队列中添加新信息,对消息队列有读权限的可以从消息队列中读取信息

特点:
1.消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2…消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
3.消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

优点:可以实现任意进程间的通信,并且通过系统调用函数来实现消息发送和接收之间的同步,无需考虑同步问题,方便
缺点:信息的复制需要额外消耗CPU的时间,不适宜于信息量大或者操作频繁的场合

4.信号量
与之前介绍的IPC结构不同,他是一个计数器,可以用来控制多个线程对共享资源的访问。信号量用于实现进程间的互斥和同步,而不是用于存储进程间通信数据

特点:
1.信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2.信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
3.每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
4.支持信号量组。

优点:可以同步进程
缺点:信号量有限

5.信号:
一种比较复杂的通信方式,用于通知接受进程某个事件已经发生

6.套接字:socket也是一种进程间通信机制,与其他通信机制不同的是,它可以用于不同主机之间的进程通信

优点:
1.传输数据为字节级,传输数据可自定义,数据量小效率高
2.传输数据时间短,性能高
3.适合于客户端和服务器端之间信息实时交互
4.可以加密,数据安全性强

缺点:需对传输的数据进行解析,转化成应用级的数据

7.共享内存
它使得多个进程可以访问同一块内存空间(这段共享内存由一个进程创建)不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等

1.共享内存是最快的一种IPC,因为进程是直接对内存进行存取
2.因为多个进程可以同时操作,所以需要进行同步
3.信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

优点:无需复制,快捷,信息量大
缺点:通信是通过将共享空间缓冲区直接附加到进程的虚拟地址空间中来实现的,未提供同步机制,因此进程间的读写操作同步问题
利用内存缓冲区直接交换信息,内存的实体存在于计算机中,只能同一个计算机系统中的诸多进程共享,不方便网络通信

进程的状态与状态转换
进程的五种状态
1.创建状态:进程正在被创建
2.就绪状态:进程被加入到就绪队列中等待CPU调度运行
3.执行状态:进程正在被运行
4.等待阻塞状态:进程因为某种原因,比如等待I/O,等待设备,而暂时不能运行
5.终止状态:进程运行完毕

2.交换技术
当多个进程竞争内部资源时,会造成内存资源紧张,并且,如果此时没有就绪进程,处理机会很空闲,I/O速度比处理机慢得多,可能出现全部进程阻塞等待I/O

针对以上问题:提出了两种解决方法:
1.交换技术:换出一部分进程到外存,腾出内存空间
2.虚拟存储技术:每个进程只能装入一部分数据和程序
在交换技术上,将内存暂时不能运行的进程,或者暂时不用的数据和程序,换出到外存,来腾出足够的内存空间,把已经具备运行条件的进程,或进程所需的数据和程序换入到内存。

从而出现了进程的挂起状态:进程被交换到外存,进程状态就成为了挂起状态。

3.活动阻塞,静止阻塞,活动就绪,静止就绪
活动阻塞:进程在内存,但是由于某种原因被阻塞了
静止阻塞:进程在外存,同时被某种原因阻塞了
活动就绪:进程在内存,处于就绪状态,只要给CPU和调度就可以直接运行
静止就绪:进程在外存,处于就绪状态,只要调度到内存,给CPU和调度就可以运行

进程在运行时有三种基本状态:就绪态、运行态和阻塞态。

运行(running)态:进程占有处理器正在运行的状态。进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态; 在多处理机系统中,则有多个进程处于执行状态。

就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行的状态。 当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。

阻塞(wait)态:又称等待态或睡眠态,指进程不具备运行条件,正在等待某个时间完成的状态。

各状态之间的转换:

就绪→执行 处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。
执行→就绪 处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。
执行→阻塞 正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。
阻塞→就绪 处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态。

孤儿进程,僵尸进程,守护进程
正常进程
正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步的过程,即父进程永远无法预测子进程到底什么时候结束,当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态

unix提供了一种机制可以保证只要父进程想要知道子进程结束时的状态信息,就可以得到每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息,知道父进程通过wait/waitpid来取时才释放,保存的信息包括
1.进程号
2.退出状态
3.运行时间

孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作

僵尸进程
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程叫做僵尸进程
僵尸进程是一个进程必然会经历的过程,这是每个子进程在结束时都要经过的阶段
如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就可以看到子进程状态时"Z"。如果父进程能及时处理,可能用ps命令都来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸进程
如果父进程在子进程结束之前推出,则子进程将有init接管,init将会以父进程的身份对僵尸状态的子进程进行处理

危害:
如果进程不调用wait/wiatpid的话,那么保留的那段信息就不会被释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程

外部消灭:
通过kill发送sigterm或者sigkill信号消灭产生僵尸进程的进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程所接管,init进程会wait()z这些孤儿进程,释放它们占用的系统进程表中的资源

内部解决
1.子进程退出时向父进程发送sigchild信号,父进程处理sigchild信号。在信号处理函数中调用wait进行处理僵尸进程
2.fork两次,原理是将子进程成为孤儿进程,从而其父进程变成init进程,通过init进程可以处理僵尸进程

创建守护进程的步骤
调用fork(),创建新进程,它会是将来的守护进程.
在父进程中调用exit,保证子进程不是进程组长
调用setsid()创建新的会话区
将当前目录改成跟目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)
将标准输入,标注输出,标准错误重定向到/dev/null

进程的同步
同步:
同步亦称直接制约关系,它是指完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待,传递信息所产生的制约关系,进程间的直接制约关系就是源于它们之间的相互合作

例如:输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才能唤醒进程A

互斥:
互斥亦称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此次临界资源

例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时,系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变成就绪状态。

为禁止两个进程同时进入临界区,同步机制应遵循以下准则
空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
有限等待。对请求访问的进程,应保证能在有限时间内进入临界区。
让权等待。当进程不能进入临界区时,应立即释放处理器,防止进程忙等待

进程同步的方法:
操作系统中,进程是具有不同的地址空间的,两个进程是不能感知到对方的存在的,有时候,需要多个进程来协同完成一些任务

当多个进程需要对同一内核资源进行操作时,这些进程便是竞争关系,操作系统必须协调每个进程对资源的占用,进程的互斥是解决进程间竞争关系的方法。进程互斥指若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源

当多个进程协同完成一些任务时,不同进程的执行进度不一致,这便产生了进程的同步问题。需要操作系统干预,在特定的同步点对所有进程进行同步,这种协作进程之间相互等待对方消息或信号的协调关系称为进程同步。进程互斥本质上也是一种进程同步

进程的同步办法:
1.互斥锁
2.读写锁
3.条件变量
4.记录锁
5.信号量
6.屏障

线程的同步
当多个线程访问一个对象时,如果不用额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的

操作系统中,属于同一进程的线程之间具有相同的地址空间,线程之间共享数据变得简单高效,遇到竞争的线程同时修改同一数据或是协作的线程设置同步点的问题时,需要使用一些线程同步的方法来解决这些问题

使用同步和互斥来保证线程的安全
互斥:通过保证同一时间只有一个执行流可以对临界资源进行访问(一个执行流访问期间,其它执行流不能访问),来保证数据访问的安全性。

同步:通过一些条件判断来实现多个执行流对临界资源访问的合理性(有资源则访问,没有资源则等待,等有了资源再被唤醒)。

线程同步的方法:
1.互斥锁
2.读写锁
3.条件变量
4.信号量
5.自旋锁
6.屏障(屏障允许每个线程等待,直到所有合作线程都到达某一点,然后从该点开始执行)

进程同步和线程同步的区别
进程之间地址空间不同,不能感知对方的存在,同步时需要将锁放在多进程共享的空间,而线程之间共享同一地址空间,同步时把锁放在所属的同一进程空间即可

多进程和多线程的选择
对比:

对比维度多进程多线程总结
数据共享,同步数据共享复杂,需要用IPC,数据是分开的,同步简单因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂各有优势
内存、CPU占用内存多,切换复杂,CPU利用率低占用内存少,切换简单,CPU利用率高线程占优
创建销毁、切换创建销毁、切换复杂,速度慢创建销毁,切换简单,速度很快线程占优
编程、调试编程简单,调试简单编程复杂,调试复杂进程占优
可靠性进程间不会互相影响一个线程挂掉将导致整个进程挂掉进程占优
分布式适用于多核多机分布式;如果一台机器不够,扩展到多台机器比较简单适应于多核分布式进程占优

优劣:

优劣多进程多线程
优点编程,调试简单,可靠性高创建,销毁,切换速度快,内存,资源占用小
缺点创建、销毁,切换速度慢,内存,资源占用大编程,调试复杂,可靠性差

选择:
需要频繁创建销毁的优先用线程
需要进行大量计算的优先使用线程
强相关的处理用线程,弱相关的处理用进程
可能要扩展到多机分布的用进程,多核分布的用线程
都满足需求的情况下,用你最熟悉最拿手的方式

什么是虚拟地址,什么是物理地址
地址空间是一个非负整数地址的有序集合

在一个带虚拟内存的系统中,CPU从一个有N= pow(2,n)个地址的地址空间中生成虚拟地址,这个地址空间成为虚拟地址空间,现在系统通常支持32位或64位虚拟地址空间

一个系统还有一个物理地址空间,对应于系统中物理内存的M个字节
地址空间的概念是很重要的,因为它清楚地区分了数据对象和它们的属性
一旦认识到这种区别,那么我们就可以将其推广,允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间。这就是虚拟内存的思想
主存中的每字节都有一个选自虚拟地址空间和一个选自物理地址空间的物理地址

Linux虚拟地址空间
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存,虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。事实上,在每个进程创建加载时,内核只是为进程”创建“了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的的程序数据和代码拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好,等到运行到对应的程序时,才会通过缺页异常来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常

请求分页系统,请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换

什么是虚拟内存
为了更加有效地管理内存并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存,虚拟内存是硬件异常,硬件地址翻译,主存,磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的,一致的和私有的地址空间。通过一个很清晰的机制,虚拟内存提供了三个重要的能力:

1.它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效的使用了主存
2.它为每个进程提供了一致的地址空间,从而简化了内存管理
3.它保护了每个进程的地址空间不被其他进程破坏

为什么要引入虚拟内存
在系统中所有的进程之间是共享CPU和主存这些内存资源的。当进程数量变多时,所需要的内存资源就会相应的增加。可能会导致部分程序没有主存空间可用,此外,由于资源是共享的,那么就有可能导致某个进程不小心写了另一个进程所使用的内存,进而导致程序运行不符合正常逻辑

虚拟内存提供了三个重要的能力:缓存,内存管理,内存保护

1.虚拟内存作为缓存的工具
虚拟内存被组织为一个由存放在磁盘上N个连续的字节大小的单元组成的数组
虚拟内存利用DRAM缓存来自通常更大的虚拟地址空间的页面

2.虚拟内存作为内存管理的工具。操作系统为每个进程提供了一个独立的页表,也就是独立的虚拟地址空间。多个虚拟页面可以映射到一个物理页面上

简化链接: 独立的地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据实际存放在物理内存的何处。
例如:一个给定的 linux 系统上的每个进程都是用类似的内存格式,对于64为地址空间,代码段总是从虚拟地址) 0x400000 开始,数据段,代码段,栈,堆等等。
简化加载: 虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。要把目标文件中.text和.data节加载到一个新创建的进程中,Linux加载器为代码和数据段分配虚拟页VP,把他们标记为无效(未被缓存) ,将页表条目指向目标文件的起始位置。
加载器从不在磁盘到内存实际复制任何数据,在每个页初次被引用时,虚拟内存系统会按照需要自动的调入数据页。
简化共享: 独立地址空间为OS提供了一个管理用户进程和操作系统自身之间共享的一致机制。
一般:每个进程有各自私有的代码,数据,堆栈,是不和其他进程共享的,这样OS创建页表,将虚拟页映射到不连续的物理页面。
某些情况下,需要进程来共享代码和数据。例如每个进程调用相同的操作系统内核代码,或者C标准库函数。OS会把不同进程中适当的虚拟页面映射到相同的物理页面。
简化内存分配: 虚拟内存向用户提供一个简单的分配额外内存的机制。当一个运行在用户进程中的程序要求额外的堆空间时(如 malloc ),OS分配一个适当k大小个连续的虚拟内存页面,并且将他们映射到物理内存中任意位置的k个任意物理页面,因此操作系统没有必要分配k个连续的物理内存页面,页面可以随机的分散在物理内存中。
3.虚拟内存作为内存保护的工具,不应该允许一个用户进程修改它的只读段,也不允许它修改任何内核代码和数据结构,不允许读写其他进程的私有内存,不允许修改任何与其他进程共享的虚拟页面。每次CPU生成一个地址时,MMU会读一个PTE,通过PTE上添加一些额外的许可位来控制一个对虚拟页面内容的访问十分简单
4.虚拟内存很适合多道程序设计系统中使用,许多程序的片段保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高

虚拟内存的代价
1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
2.虚拟地址到物理地址的转换,增加了指令执行的时间
3.页面的换入换出需要磁盘的I/O,这是很耗时的
4.如果一页中只有一部分数据,会浪费内存

常见的页面置换算法
当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘兑换区,替换一个页,这种现象叫做缺页置换。当前操作系统最常采用的缺页置换算法如下:

1.全局置换:在整个内存空间置换
工作集算法
缺页率置换算法
2.局部置换:在本进程中进行置换
先进先出(FIFO)算法:
思路:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面
实现:按照进入内存的先后次序排列成队列,从队尾进入,从队首删除
特点:实现简单
弊端:性能较差,调出的页面可能是经常访问的

最不常用算法(LFU)
思路:缺页时,置换访问次数最少的页面
实现:每个页面设置一个访问计数,访问页面时,访问计数加一,缺页时,置换计数最小的页面
特点:算法开销大(排序),开始时频繁使用,但以后不适用的页面很难置换(缓存颠簸)

最近最少使用(LRU)算法
思路:置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;较长时间内没有被访问的页面,可能最近不会被访问
实现:缺页时,计算内存中每个逻辑页面上的上一次访问时间,选择上一次使用到当前时间最长的页面,使用一个栈,新页面或者命中的页面则将该页面移动到栈底,每次替换栈顶的缓存页面
特点:可能达到最优的效果,LRU算法对热点数据命中率是很高的
缺陷:
维护这样的访问链表开销比较大
缓存颠簸,当缓存(1,2,3)满了,之后数据访问(0,3,2,1,0,3,2,1…)
缓存污染,突然大量偶发性的数据访问,会让内存中存放大量冷数据

缺页中断
mollc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常,缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存

缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤
1.保护CPU现场
2.分析中断原因
3.转入缺页中断处理程序进行处理
4.恢复CPU现场,继续执行
但是缺页中断是由于索要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般中断存在区别:
1.在指令执行期间产生和处理缺页中断信号
2.一条指令在执行期间,可能产生多次缺页中断
3.缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令

  • 3
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值