C++面试题汇总 操作系统
-
- 进程与线程的概念,以及为什么要有进程线程,其中有什么区别,他们各自又是怎么同步的
- 进程间通信的方式:
- `Linux`虚拟地址空间
- 操作系统中的程序的内存结构
- 操作系统中的缺页中断
- `fork`和`vfork`的区别
- 如何修改文件最大句柄数?
- 并发(concurrency)和并行(parallelism)
- `MySQL`的端口号是多少,如何修改这个端口号
- 操作系统中的页表寻址
- 有了进程,为什么还要有线程?
- 单核机器上写多线程程序,是否需要考虑加锁,为什么?
- 线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的
- 线程间的同步方式,最好说出具体的系统调用
- 多线程和多进程的不同
- 进程和线程的区别
- 游戏服务器应该为每个用户开辟一个线程还是一个进程,为什么?
- `OS`缺页置换算法
- 多进程和多线程的使用场景
- 死锁发生的条件以及如何解决死锁
- 虚拟内存和物理内存怎么对应
- 操作系统中的结构体对齐,字节对齐
- 进程间怎么通信
- 虚拟内存置换的方式
- 多线程,线程同步的几种方式
- 互斥锁(mutex)机制,以及互斥锁和读写锁的区别
- 进程状态转换图,动态就绪,静态就绪,动态阻塞,静态阻塞
- `A* a = new A; a->i = 10`;在内核中的内存分配上发生了什么?
- 给你一个类,里面有static,virtual,之类的,说一说这个类的内存分布
- 软链接和硬链接区别
- 什么是大端小端以及如何判断大端小端
- 静态变量什么时候初始化
- 用户态和内核态区别
- 如何设计`server`,使得能够接收多个客户端的请求
- 死循环+来连接时新建线程的方法效率有点低,怎么改进?
- 怎么唤醒被阻塞的`socket`线程?
- 怎样确定当前线程是繁忙还是阻塞?
- 请问就绪状态的进程在等待什么?
- 多线程的同步,锁的机制
- 两个进程访问临界区资源,会不会出现都获得自旋锁的情况?
- `windows`消息机制知道吗,请说一说
- C++的锁你知道几种?
- 说一说你用到的锁
- 请你说一说死锁产生的必要条件?
- 内存溢出和内存泄漏
- 进程和线程的区别,你都使用什么线程模型
- 请你来说一说协程
- 系统调用是什么,你用过哪些系统调用
- 请你来手写一下`fork`调用示例
- 用户态到内核态的转化原理
- 源码到可执行文件的过程
- 微内核与宏内核
- 僵尸进程
- 请问GDB调试用过吗,什么是条件断点
- 介绍一下5种IO模型
- 异步编程的事件循环
- 操作系统为什么要分内核态和用户态
- 为什么要有`page cache`,操作系统怎么设计的`page cache`
- `server`端监听端口,但还没有客户端连接进来,此时进程处于什么状态?
- 怎么实现线程池
- `Linux`下怎么得到一个文件的`100`到`200`行
- 请你来说一下`awk`的使用
- `linux`内核中的`Timer`定时器机制
进程与线程的概念,以及为什么要有进程线程,其中有什么区别,他们各自又是怎么同步的
-
基本概念:
- 进程
- 是对运行时程序的封装,
- 是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
- 线程
- 是进程的子任务,是
CPU调度
和分派
的基本单位,用于保证程序的实时性,实现进程内部的并发; - 线程是操作系统可识别的
最小执行和调度单位
。 - 每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态(栈也是独立的)。
- 每个线程完成不同的任务,但是共享
- 同一地址空间(也就是同样的动态内存(堆区),映射文件(映射区),目标代码等等)
- 打开的文件队列和其他内核资源。(例如
Socket
, 文件句柄什么的)
- 是进程的子任务,是
- 进程
-
区别:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
- 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。
- 但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
- 进程是资源分配的最小单位,线程是CPU调度的最小单位;
- 系统开销:进程切换的开销也远大于线程切换的开销。
- 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。
- 在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。
- 而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。
- 通信:
- 同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
- 而
进程间通信IPC
主要包括: 管道,系统IPC
(包括消息队列、信号量、信号、共享内存等) 以及套接字socket
。
- 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
- 进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
- 进程适应于多核、多机分布;线程适用于多核
进程间通信的方式:
-
进程间通信主要包括
管道
、系统IPC
(包括消息队列
、信号量
、信号
、共享内存
等)、以及套接字socket
。- 管道:管道主要包括无名管道和命名管道, 管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信
- 普通管道
PIPE
:- 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
- 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的
read
、write
等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
- 命名管道
FIFO
:FIFO
可以在无关的进程之间交换数据FIFO
有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
- 普通管道
- 系统
IPC
- 消息队列
- 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即
队列ID
)来标记。 (消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;
- 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即
- 特点:
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
- 消息队列
- 信号量
semaphore
- 信号量(semaphore)与已经介绍过的
IPC
结构不同,它是一个计数器
,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。 - 特点:
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量基于操作系统的
PV
操作,程序对信号量的操作都是原子操作。 - 每次对信号量的
PV
操作不仅限于对信号量值加 1
或减 1
,而且可以加减任意正整数。 - 支持信号量组。
- 信号量(semaphore)与已经介绍过的
信号signal
: 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。共享内存(Shared Memory)
:它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等- 特点:
- 共享内存是最快的一种
IPC
,因为进程是直接对内存进行存取 - 因为多个进程可以同时操作,所以需要进行同步
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
- 共享内存是最快的一种
- 特点:
- 套接字
SOCKET
:socket
也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。
- 管道:管道主要包括无名管道和命名管道, 管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信
-
线程间通信的方式:
临界区
:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;互斥量Synchronized/Lock
:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问信号量Semphare
:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。事件(信号) Wait/Notify
:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作
Linux
虚拟地址空间
- 虚拟内存技术
- 目的是为了解决进程地址空间隔离的问题, 采用了虚拟内存。
- 虚拟内存技术为每个进程提供一个大的, 一致的和私有的地址空间, 就好像独占整个内存空间一样.
- 所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。
- 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。
- 还有进程运行过程中,要动态分配内存,比如
malloc
时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。 - 请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
- 扩大地址空间; (最简单的例子不就是64位的系统么, 逻辑地址空间大的逆天 !!)
- 虚拟内存的好处:
- 扩大地址空间; (最简单的例子不就是64位的系统么, 逻辑地址空间大的逆天,
物理地址
远小于逻辑地址
!!) - 扩大了可寻址空间 (例如
32位逻辑地址
寻址36位的物理地址
) - 内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
- 公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
- (内存共享)当进程通信时,可采用虚存共享的方式实现。
- 当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
- (虚拟储存技术)虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
- 在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用内存碎片
- 好处:
- 扩大
寻址空间
和可寻址空间
- 内存保护
- 内存共享
- 和缺页中断配合实现虚拟储存技术(参考: 进程状态转换图,动态就绪,静态就绪,动态阻塞,静态阻塞)
- 扩大
- 扩大地址空间; (最简单的例子不就是64位的系统么, 逻辑地址空间大的逆天,
- 虚拟内存的代价:
- 虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
- 虚拟地址到物理地址的转换,增加了指令的执行时间。
- 页面的换入换出需要
磁盘I/O
,这是很耗时的 - 如果一页中只有一部分数据,会浪费内存。
操作系统中的程序的内存结构
-
任何程序程序本质上都是由
BSS段
、data段
、text段
三个组成的。BSS段(未初始化数据区)
:通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段
属于静态分配,程序结束后静态变量资源由系统自动释放。数据(data)段
:存放程序中已初始化的全局变量, 静态变量以及常量数据的一块内存区域。数据段也属于静态内存分配。代码(text)段
:存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量(例如字节型字面值常量)
-
可执行程序在存储(没有调入内存)时分为
代码段
、数据区
和未初始化数据区
三部分。text段
和data段
在编译时已经分配了空间,而BSS段
并不占用可执行文件的大小(因为没有初值),它是由链接器来获取内存的。bss段
(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0
。需要存放在程序文件中的只有正文段和初始化数据段。data段
(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。数据段
包含经过初始化的全局变量以及它们的值。BSS段
的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。- 包含
数据段
和BSS段
的整个区段此时通常称为数据区(静态区)
。
-
可执行程序在运行时又多出两个区域:
栈区
和堆区
。- 栈区:
- 由编译器自动释放,存放函数的参数值、局部变量等。
- 每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。
- 然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。
- 每调用一个函数一个新的栈就会被使用。
- 栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量(
1M
)是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
- 堆区:
- 用于动态分配内存,位于
BSS
和栈
中间的地址区域。由程序员申请分配和释放。 - 堆是从低地址位向高地址位增长,采用链式存储结构。
- 频繁的
malloc/free
造成内存空间的不连续,产生碎片。 - 申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
- 用于动态分配内存,位于
- 栈区:
操作系统中的缺页中断
malloc()
和mmap()
等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存.- 当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
- 缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
- 缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
保护 CPU 现场
- 分析中断原因
- 转入缺页中断处理程序进行处理
恢复 CPU 现场
,继续执行
- 中断的处理右两个处理步骤: 中断响应和中断处理
- 中断响应由硬件实现主要高阔: 识别中断源, 保护现场, 引入中断处理函数入口地址(检查中断向量表)
- 中断处理就是执行中断处理函数
- 但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:
- 在指令执行期间产生和处理缺页中断信号
- 一条指令在执行期间,可能产生多次缺页中断
- 缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。
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
所造成的地址空间的浪费。BSD
的开发者们在3.0
的BSD
系统中引入了vfork( )
系统调用。#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
的区别:fork( )
的子进程拷贝父进程的数据段和代码段;vfork( )
的子进程与父进程共享数据段fork( )
的父子进程的执行次序不确定;vfork( )
保证子进程先运行,在调用exec
或exit
之前与父进程数据是共享的,在它调用exec
或exit
之后父进程才可能被调度运行。vfork( )
保证子进程先运行,在它调用exec
或exit
之后父进程才可能被调度运行 。 如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会 导致死锁。- 当需要改变共享数据段中变量的值,则拷贝父进程。
如何修改文件最大句柄数?
-
linux
默认最大文件句柄数是1024
个,在linux
服务器文件并发量比较大的情况下,系统会报"too many open files"
的错误。故在linux
服务器高并发调优时,往往需要预先调优Linux
参数,修改Linux
最大文件句柄数。 -
有两种方法:
ulimit -n <可以同时打开的文件数>
,将当前进程的最大句柄数修改为指定的参数- 注:该方法只针对当前进程有效,重新打开一个
shell
或者重新开启一个进程,参数还是之前的值首先用 ulimit -a 查询`Linux`相关的参数,如下所示: core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 94739 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 94739 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited 其中,open files就是最大文件句柄数,默认是1024个。 修改Linux最大文件句柄数: ulimit -n 2048, 将最大句柄数修改为 2048个。
- 注:该方法只针对当前进程有效,重新打开一个
- 对所有进程都有效的方法,修改
Linux系统参数
vi /etc/security/limits.conf
添加* soft nofile 65536 * hard nofile 65536 // 将最大句柄数改为 65536
- 修改以后保存,注销当前用户,重新登录,修改后的参数就生效了
并发(concurrency)和并行(parallelism)
并发(concurrency)
:指宏观上看起来两个程序在同时运行,比如说在单核cpu
上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率。并行(parallelism)
:指严格物理意义上的同时运行,比如多核cpu
,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu
都是往多核方面发展。
MySQL
的端口号是多少,如何修改这个端口号
- 查看端口号:
- 使用命令
show global variables like 'port';
查看端口号 mysql
的默认端口是3306
。- 补充:
sqlserver
默认端口号为:1433
;oracle
默认端口号为:1521
;DB2
默认端口号为:5000
;PostgreSQL
默认端口号为:5432
- 使用命令
- 修改端口号:
- 修改端口号:编辑
/etc/my.cnf
文件,早期版本有可能是my.conf
文件名,增加端口参数,并且设定端口,注意该端口未被使用,保存退出。
- 修改端口号:编辑
操作系统中的页表寻址
-
页式内存管理,内存分成固定长度的一个个页片。
- 操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。
- 通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。
- 页式内存管理的优点就是比较灵活,内存管理以较小的页为单位(内存碎片的最小单位为页),方便内存换入换出和扩充地址空间。
- 优点:
- 页面都用固定的长度, 页表简单,调如方便
- 缺点:
- 程序不可能正好是页面的整数倍, 所以最后一页的零头将会浪费
- 页不是逻辑上的独立实体,所以处理, 保护和共享都不级段式虚拟存储
-
Linux
最初的两级页表机制:- 两级分页机制将
32位
的虚拟空间分成三段,分别表示页目录表项
,页表项
,内页偏移
, 虚拟地址高10位表示页目录表偏移, 中间10位表示页表偏移, 低12位表示页内偏移 - 低十二位表示
页内偏移
,高20分成两段分别表示两级页表的偏移。PGD(Page Global Directory)
: 最高10位,全局页目录表索引PTE(Page Table Entry)
:中间10位,页表入口索引- 其中的计算关系为:
- 一页的大小为
4K
, 页表项索引的大小为4bites
, 所以一页中可以存放1024(2 10 ^{10} 10)个页表项- 对于页目录表 就是一页存放了
1024
个页表项索引 - 对于页表就是 一页可以存放
1024
页索引
- 对于页目录表 就是一页存放了
- 一页的大小为
- 当在进行地址转换时,
- 首先
CR3寄存器
中存放的全局页目录表(page directory, PGD)
的这一页的物理地址 - 再加上从虚拟地址中抽出
高10位
叫做页目录表项(内核也称这为pgd
)的部分作为偏移, 即定位到可以描述该地址的pgd
;从该pgd
中可以获取可以描述该地址的页表的物理地址, - 再加上从虚拟地址中
抽取中间10位
作为偏移, 即定位到可以描述该地址的pte
; - 在这个
pte
中即可获取该地址对应的页的物理地址, 加上从虚拟地址中抽取的最后12位
,即形成该页的页内偏移, 即可最终完成从虚拟地址到物理地址的转换。 - 从上述过程中,可以看出,对虚拟地址的分级解析过程,实际上就是不断深入页表层次,逐渐定位到最终地址的过程,所以这一过程被叫做
page talbe walk
。
- 首先
- 两级分页机制将
-
Linux
的三级页表机制:- 当
X86
引入物理地址扩展(Pisycal Addrress Extension, PAE)
后,可以支持大于4G
的物理内存(36位),但虚拟地址依然是32位,原先的页表项不适用,它实际多4 bytes
被扩充到8 bytes
,这意味着,每一页现在能存放的pte
数目从1024
变成512了(4k/8)
。相应地,页表层级发生了变化,Linux新增加了一个层级,叫做页中间目录(page middle directory, PMD)
, 变成:
段 描述 位数 r3 指向一个PDPT crs寄存器存储 GD 指向PDPT中4个项中的一个 位31~30 MD 指向页目录中512项中的一个 位29~21 TE 指向页表中512项中的一个 位20~12 age offset 4KB页中的偏移 位11~0 - 现在就同时存在2级页表和3级页表,在代码管理上肯定不方便。巧妙的是,
Linux
采取了一种抽象方法:所有架构全部使用3级页表: 即PGD -> PMD -> PTE
。 - 那只使用2级页表(如非PAE的X86)怎么办?
- 办法是针对使用2级页表的架构,把
PMD
抽象掉,即虚设一个PMD表项
。这样在page table walk
过程中,PGD
本直接指向PTE
的,现在不了,指向一个虚拟的PMD
,然后再由PMD
指向PTE
。这种抽象保持了代码结构的统一。
- 办法是针对使用2级页表的架构,把
- 当
-
Linux
的四级页表机制:- 硬件在发展,3级页表很快又捉襟见肘了,原因是
64位CPU
出现了, 比如X86_64
, 它的硬件是实实在在支持4级页表的。它支持48位
的虚拟地址空间。如下:
段 - 硬件在发展,3级页表很快又捉襟见肘了,原因是