目录
虚拟地址转化物理地址
如何理解进程划分资源给线程
再谈什么是页表
几个小问题(重要)
如何理解我们之前的new和malloc
如何理解我们之前学的写时拷贝
申请内存究竟是在干什么
页表什么时候会缺页中断,什么时候是越界,这两个怎么分
线程的优点
线程的缺点
线程异常
线程用途
Linux进程VS线程
我们今天学习线程的概念和控制
虚拟地址转化物理地址
一个可执行程序(ELF格式),要进入CPU执行,需要先将自己程序的入口(程序的首地址)加载到内存中,这个是程序都虚拟地址,我们讲过一个程序内部用的是物理地址,外部用的是虚拟地址,然后物理内存拿到首地址后,需要通过这个首地址映射物理地址然后存起来,所以在映射之前需要将页表结构以及其他的结构进行构建,所以当我们讨论虚拟地址到物理地址的转化时,页表的映射关系已经在加载的时候进行关联了,是进行调度了这个可执行程序让其可以执行的,我们知道转化的工作不涉及进程,转化的工作是在CPU里面进行的,入口的虚拟地址首先被CPU里面的EIP寄存器保存下来,然后CR3寄存器保存页表的地址方便MMU(内存管理单元)进行虚拟地址向物理地址的转化,然后输出CPU填到虚拟地址的页表对应的物理地址位置。我们发现这个过程虚拟地址根本不需要存储在页表里面。
进程调度的可执行程序,那么多个进程怎么知道是哪个进程调度的,所以操作系统中有一个全局变量task_struct* current的指针指向当前正在调度的进程,进程中有该可执行程序的虚拟地址,所以CPU中还有一个寄存器存储current,存储当前进程,CPU要读或者写入到内存通过之间连接的系统总线,当当前的物理地址被解析出来的时候会通过系统总线传给内存,里面还会带着一些比如in/out操作,内存有对应的寄存器进行接收,当操作寄存器识别到对应的指令来的时候,会把对应的这个操作(这个虚拟地址)的命令再通过系统总线传到CPU中存放指令的寄存器IR,CPU 从内存取出指令后,存入 IR 寄存器,随后进行译码和执行,EIP中存放的地址+老命令(IR内指令的长度)的长度就得到下一条指令的地址,接着MMU再解析,再传递整个动作就循环起来了,然后根据上面来看,MMU解析传入(不是写入)内存的是物理地址+操作码。这里就有个问题MMU解析的工作为什么软件不能做要让这个硬件来实现?,首先转化这种工作肯定不能太复杂,其次软件太慢了,MMU不属于外设没有I/O的顾虑,使用的内存访问,不是所有的读取都涉及I/O。CPU的结构决定了系统·必须有页表。
单级⻚表对连续内存要求⾼,于是引⼊了多级⻚表,但是多级⻚表也是⼀把双 刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率。 有没有提升效率的办法呢?计算机科学中的所有问题,都可以通过添加⼀个中间层来解决。 MMU 引⼊ 了新武器,江湖⼈称快表的 TLB (其实,就是缓存) 当 CPU 给 MMU 传新虚拟地址之后, MMU 先去问 TLB 那边有没有,如果有就直接拿到物理地址发到 总线给内存,⻬活。但 TLB 容量⽐较⼩,难免发⽣ Cache Miss ,这时候 MMU 还有保底的⽼武器⻚表,在⻚表中找到之后 MMU 除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录 ⼀下刷新缓存。既然MMU是负责虚拟地址转化为物理地址的,那为什么还要去页表查呢,自己转不就可以了,MMU 之所以需要查页表,是因为地址映射规则是由操作系统(OS)管理的,而不是 MMU 自己决定的。换句话说,MMU 只是个“翻译器”,它不会自己决定虚拟地址该映射到哪个物理地址,而是必须依赖页表,因为页表存储了当前进程的映射关系。
就是每次MMU进行翻译的时候得进去页表里面看一下映射有没有权限什么的,可不可以映射(这些都是操作系统做的),如果都正常才会翻译物理地址然后传给内存,页表不能自己翻译(取代MMU,直接少需一个硬件),这样效率会变低的,页表是软件很慢,现在的 MMU 是硬件级的,速度极快,进程切换时,每个进程都有自己的页表,现在只需要让 MMU 换个页表基地址(CR3),切换非常快。如果让 CPU 负责计算,切换时就要重新加载页表结构,额外的计算量太大。反正就是效率问题,我们再回到TLB上,每次MMU要翻译物理地址的时候会先去访问TLB缓存,TLB 命中表示虚拟地址的映射关系已经缓存在 TLB(比如最近访问过的地址),MMU 直接从 TLB 读取物理地址,不需要查页表,速度极快。TLB 未命中:MMU 在 TLB 里找不到映射关系,它就必须去查页表,找到对应的物理地址,TLB就缓存了物理地址。
32 位系统 通常是 2 级页表,64 位系统 一般是 4 级页表,但实际上页表的级数是可以变化的。
Intel 已经在最新 CPU 里支持 5 级页表(五级分页),用于支持更大的地址空间(最高 57-bit 虚拟地址,能寻址 128PB 的虚拟内存)。
除了虚拟地址和物理地址的映射关系(怎么映射的)需要保存,CPU要调用的代码和数据也需要保存,CPU内部有一个cache缓存缓存的就是CPU要调用的代码和数据,物理地址和操作码被MMU返回时,MMU不会直接通过系统总线向内存要相关操作码的执行代码,而是会先到cache里面查找,如果有这部分操作的多行代码就直接调用了,如果没有就需要内核传入cache然后调用,这个cache里面的信息是根据局部性原理存储可能相关的代码段以及下一步要执行的附近的代码,所以这个也是基于概率的,因为CPU可能不会执行cache里面缓存的内容,cache本质是缓存是预加载。
如何理解进程划分资源给线程
进程是如何做到将线程的代码完成划分的,因为线程的PCB直接分块放在进程的PCB里面那其属性等的肯定是被分好的了。进程其实不需要刻意去做,每个线程都是要执行自己的run方法的,所以每个run方法都是有地址的,每个线程的 run
方法在其 PCB(进程控制块) 里面存的是 虚拟地址,而不是物理地址,主要是因为 线程是进程的一部分,而进程本身就是运行在 虚拟地址空间 上的。所以线程和进程是共享的虚拟空间,每个虚拟地址都不一样,自然页表映射到物理内存的地址空间也不一样,所以每个run函数快就自动的分不同块的映射到来物理内存的不同位置了,这不就自然将进程的物理资源瓜分了吗,只需要创建多个线程的内核数据结构(struct)然后映射到进程的PCB的不同区间就可以了,接着的进程就不需要管了。
再谈什么是页表
页表的结构由页表项和全局页目录项组成加上标记位组成,页表可以认为是由一个unsigned long类型的数组构成的,页表的结构除了物理地址,虚拟地址还有的就是12个标记位,里面有比如是否命中呀,命中就是是否存在页框地址的映射,是否可以映射得到物理地址,还有是否有RWX权限和是否有用户权限和内核权限(U/K),"U/K权限"通常指的是用户(User)和内核(Kernel)之间的权限管理,没有命中说明没有映射物理地址,没有命中页框地址,物理地址没有就会缺页中断,没有写权限,内存就不能写入,一旦写入操作到来,MMU就会来查看这个页表再看是否翻译成新的物理地址,但是MMU发现没有写权限就开始保错,此时CPU发生软中断,查中断向量表,让OS执行对应的方法,所以操作系统怎么知道MMU转化失败的,就是如上办法,像类似这种判断有12位,页表内存放的是页框的地址,但不完全存放,因为页表内的地址是32位的,前20位是映射页框的地址,剩的12位是页表标志位,为什么,因为一个页框的地址32位,假设所有页框都可以被一个页表映射到,所以页框的总地址量是2^32个,那每4KB为一个数据块,这么多页框就被分为了2^32 / 2^12 = 2^20,所以最多只需要20个比特位就可以了,每个bit位标识一个4KB的数据块。
所以虚拟地址到物理地址的转换过程是MMU+OS(负责切换进程页表和设置权限)做的。
几个小问题(重要)
如何理解我们之前的new和malloc
new和malloc只需要在虚拟内存创建一个虚拟地址空间,然后维护好页表的各种关系,然后是否命中是否,物理地址全0,然后等到要开空间的时候才触发缺页中断让OS修改页表的标记为,让MMU映射成功物理地址,就是要用时才向物理内存申请空间,才生成物理地址。
如何理解我们之前学的写时拷贝
原本父子进程的代码是共享的,也就是页表是共享的,那原本子进程对于共享的内存只有读权限,现在子进程非要写入,操作系统就报缺页中断了(无法映射物理地址),操作系统受到错误开始拷贝页表并修改权限使得子进程在新的页表中获得写权限,就是一个MMU能否映射物理地址的问题
写时拷贝是按照4KB为单位进行拷贝的。
申请内存究竟是在干什么
物理地址是MMU+OS联合关联进行自主映射的,所以申请物理地址只需要申请虚拟地址空间+完善好页表,至于映射就不是进程的工作了,不用管反正MMU+OS会帮忙的,那我怎么知道页表有没有指向某个页框的地址呢,这个页框地址有没有人在使用呢,page的flag就是在管理并提示这个的。
页表什么时候会缺页中断,什么时候是越界,这两个怎么分
缺页中断了不一定会直接终止程序,这种检查是由MMU进行的。
就是号是合法的但是页框没有页表映射就是缺页中断,号都不合法,页表都访问不到就是越界。
线程的优点
怎么理解第2条呢,为什么线程切换工作少,主要是TLB,TLB(Translation Lookaside Buffer)缓存的是虚拟地址到物理地址的映射关系,而不是单纯的虚拟地址或物理地址。这是因为TLB的核心作用是加速地址转换过程,而不是简单地存储地址。同一个进程下的多个线程的映射关系都是一样的,所以线程切换时TLB不需要改变,但是进程切换时由于代码数据改变使得不同进程映射关系不一样此时TLB就需要重新加载。
Cache(缓存)是计算机体系结构中的一种高速存储器,用于存储CPU最近访问或可能即将访问的数据和指令,以减少访问主内存(RAM)的延迟,从而提高系统性能。由于所有线程共享代码段和虚拟地址空间,所以他们直接可以互相访问各自的代码区域(物理地址可以互相转化),所以切换线程cache里面的附近代码段的值还是可以用的,不同进程间的代码区域是独立的,所以切换进程时,前一个进程是cache就失效了,需要重新加载。一个进程访问的代码段肯定是同一个PCB下其他线程的附近区域。
使用cat /proc/cpuinfo指令可以查看CPU内的相关数据,可以看到cache的大小如下。
线程的缺点
线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。线程是进程的执⾏分⽀,线程出异常,就类似进程出异常,进⽽触发信号机制,终⽌进程,进程 终⽌,该进程内的所有线程也就随即退出,这个是因为线程的错误就是进程的错误,因为进程是调度的实体,操作系统只能识别进程,然后由于进程里面的所有线程的pid都一样,所以操作系统识别到那个引发错误的pid就会直接将所有pid一样的进程和线程都杀掉。
线程用途
合理的使⽤多线程,能提⾼CPU密集型程序的执⾏效率,合理的使⽤多线程,能提⾼IO密集型程序的⽤⼾体验(如⽣活中我们⼀边写代码⼀边下载开发⼯ 具,就是多线程运⾏的⼀种表现)
Linux进程VS线程
进程是资源分配的基本单位,线程是调度的基本单位,线程共享进程数据,但也拥有⾃⼰的⼀部分数据:
errno是错误码的意思,错误码肯定是自己报自己的,一组寄存器主要是保存各自线程的上下文数据,这个是属于自己的,线程要调用函数就要在栈上开辟栈帧,自己的栈帧是属于自己的所以需要一个线程拥有属于自己的栈结构,但是一个进程只有一个栈,怎么弄的我们之后在线程的控制会说到。
同⼀地址空间,因此Text Segment、Data Segment都是共享的,如果定义⼀个函数,在各线程中都可以调 ⽤,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
凡是进程物理内存中有的东西都是共享的!!!
进程和线程的关系如下图:
他们的关系可以有以上4种,所以我们之前学的单进程是具有⼀个线程执⾏流的进程。