zoukankan      html  css  js  c++  java
  • 事无巨细 | 使用 printf 的时候,都发生了什么 (施工中)

    执行 printf("num: %d", nums[4200]); 的时候,都发生了什么事情。

    本文并不是源码阅读文章,只是对原理进行梳理。

    补全参数

    如果我们要输出一个格式化字符串,负责把参数填入原有字符串的并不是最后一步的打印函数,而是在进入内核态前就已经先把整个格式化字符串转换成全字符串。

    实现这样一个功能并不难,只需要一个状态机即可。

    对数字的寻址

    当我们要获取 nums[4200] 的值的时候,我们会取出 nums 的起始地址,然后尝试获取偏移量 4200 的地址的值。但是,尽管 nums 的地址值大概是有缓存在 CPU 里,但是对于 4200 这么大的偏移量,很可能会发现缓存中没有这个值,于是我们就需要到内存中去取值。而由于虚拟地址的原因,我们还并不能简单地靠一次读写得到值。所以接下来我们就要看,如何去获取一个指定地址上的值。

    虚拟地址空间

    每一个进程都拥有自己的虚拟地址空间。

    不能直接操作物理地址是为了防止不同进程内存冲突,也是为了避免难以为进程分配内存空间。同时虚拟内存的做法还有利于扩充可用的内存空间,因为内存内容可以页面机制,在外存中存取,来间接扩大可用内存空间。

    虚拟地址空间的分配方式是这样的(以32位4G内存空间为例):

    • 0xC0000000以上是kernel memory,这部分是由操作系统内核自己操作的。

    • 从0xBFFFFFF以下是栈,栈的延伸方向是从大到小的。

    • 0x40000000 以上会被用于共享内存的挂载。

    • 从0x8048000以上,是堆和从可执行文件中加载进来的内容。

      • 0x8048000 开始是只读数据和代码区,包含 .data .text 段的数据
      • 0x8049000 以上是静态数据区,包括已初始化过的,和未初始化预先置 0 的。
      • 静态数据区之上就是堆,堆的从小到大延伸的。注意堆区和数据结构堆没有联系。
    • 0x8048000之下没有使用。

    要得到一个地址真正指向内存的地址,需要将其从逻辑地址(vaddr)转换为线性地址(laddr)转换为物理地址(paddr)

    逻辑地址 -> 线性地址

    虚拟内存中未经加工过的地址就是逻辑地址。

    段机制

    分段式存储是为了让用户在编程时更好地对数据进行模块化管理。

    在启用了分段式存储之后,访问不同的数据就需要先确定数据所在的段,然后检查段的属性来把虚拟地址转换为线性地址。

    每个进程可拥有的段,总共有六个,它们的描述信息分别存在六个段寄存器内,分别是 CS, SS, DS, ES, FS, GS。

    对于我们取值这样的操作,就需要通过取出数据段 DS 内的信息来查找数据段。

    寄存器内的信息是段选择符。段选择符的内容有段表指定,特权级和索引。

    • TI 指定了段是在全局描述符表(GDT)还是局部描述符表(LDT)内。
    • RPL 指定了访问段所需的权限(用户或内核)
    • Index 指定了段描述符在表中的哪一项。

    例如,假设我们的数据在全局描述符表,那么我们就从用户不可见的“GDT寄存器” GDTR 内取出 GDT 的起始地址,然后根据索引找到段描述符。

    段描述符最重要的信息是它的基址 (base) 和限界 (limit)。

    • 通过 base,我们能知道给定的逻辑地址应该加上多少的偏移量得到线性地址。
    • 通过 limit,我们能知道一个地址是否超出段的空间。

    总之,我们通过段得到了 laddr = base + vaddr。

    线性地址 -> 物理地址

    由于每个进程都享有 4G 的虚拟内存,因此当多个进程同时存在于系统中时,必然会有地址冲突的问题。而解决的办法就是分页机制,同一个位置被进程 1 利用时,装载进程 1 的数据。当进程 1 空闲,进程 2 要使用该位置时,就可以从磁盘中把进程 2 的数据加载到主存。总的来说,就是同一个主存下,不同的页位置给予不同进程使用。

    页机制

    两级页表

    分页机制需要一个页表将虚拟地址的页转换成物理地址的页。例如 0x8048000 占虚拟 0x8048 号页面,那么可能就会通过查找页表,得到 0x1234 这样的物理页。

    考虑把一个线性地址拆分成页表项和页内偏移量。当我们偏移量取 12 位,那么页表索引就有 20 位,也就是 2^20 项,这会占用很大一块的内存空间,所以实际上我们分成两级页表,一个是页目录表(DIR),DIR 的每一项指向一个页表(PAGE)。每张表的大小都是 1024 项,而当其他 1023 个页表不使用时,我们可以将其从主存中卸载。

    页表项的基地址也叫页框,它加上原有的偏移量,即可得到物理地址。

    缺页中断

    如果某个页不在主存中,据需要通过缺页中断,让系统从外部存储中调入数据。

    页面的替换有 FIFO、LRU、LFU、CLOCK 等机制,替换页面之后,就需要修改页表。因此,上一次的物理页可能是 0x1234,下一次可能就被放到了 0x5678。

    快表

    每次页表查询都会需要一定时间,如果有一个缓存机制,让我们直接能获得页框,就可以省下很多时间。

    这样的缓存称作快表(TLB)。如果查询快表没有命中,那么接下来页表项获取无论是否成功,查到的页都会记在到快表中。

    快表的替换的随机的,并没有页面替换那么多的机制。

    物理地址 -> 数据

    得到物理地址之后,还不会马上去主存读取数据,而是先检查缓存。

    缓存

    多级缓存

    CPU 的速度是很快的,因此如果每次访存都要走主存,那么反复 IO 的时间会拖累 CPU。因此通常基于局部性,读出一整段数据到缓存中。

    现在的机器一般会具有多级缓存,比如L1,L2,L3等。这几个内存的访问速度依次减慢,大小也依次增加。

    以 Intel Core i7 为例,每个核私有 L1 和 L2 缓存,L1 缓存包括了数据和指令缓存,两个 L1 缓存共用一个 L2 缓存,所有核共享一个 L3 缓存。

    L1 缓存存取为 4 个时钟周期,而 L3 就达到了 30 - 40 个时钟周期。

    缓存映射

    缓存有三种映射方式。

    • 直接映射:逻辑地址被拆分成 Tag、cache 行号和块内地址。行号的取值空间取决于缓存大小。如果地址对应的缓存行和地址有一致的 Tag,那么就认为缓存命中。
    • 全相连映射:逻辑地址拆分成 Tag 和块内地址。遍历缓存,如果 Tag 相同就命中,否则任意挑选位置,将数据从主存提取到缓存内。
    • 组相连映射:逻辑地址拆分成 Tag,cache 组号和块内地址。每一组是有固定的行数的,比如每组 8 行称为 8 路组相连。算出组号以后,遍历该组的所有行,流程就和全相连映射差不多了。

    如果缓存命中的话,那么直接就可以从缓存中根据 offset 读出数据。

  • 相关阅读:
    单例模式
    iOS宏定义
    WKWebView基本使用
    文件操作(NSFileManager)
    iOS 字典和NSData之间转换
    iOS 身份证,邮箱,手机号验证
    iOS自定义数字键盘
    iOS指纹识别
    JavaScript表单
    JavaScript数组操作方法集合(2)
  • 原文地址:https://www.cnblogs.com/KakagouLT/p/13654666.html
Copyright © 2011-2022 走看看