月度归档:2018年12月

深入理解计算机系统(第三版)/ CSAPP 杂谈,第8章:异常控制流

Published / by sickworm / Leave a Comment
  • 异常控制流(Exceptional Control Flow,ECF)是操作系统为应用提供的一种访问处理器资源之外的能力,对应于嵌入式和CPU等硬件的中断概念。

  • 系统调用,进程管理,并发,IO 访问都属于异常控制流。

  • 异常(exception)是控制流的突变,用来处理处理器状态中的某些变化。异常通过事件(event)触发,有专门的异常表(exception table)用于事件的跳转。

  • 每种类型的异常都有唯一的异常号(exception number),有可能是处理器设计时分配的零除,缺页,内存访问违例,断点和算术运算溢出;也可能是操作系统分配的系统调用,外部IO设备信号。操作系统的异常号是在系统启动时初始化的。

  • 异常表的格式是:异常号->地址。

  • 异常的处理是在内核空间内,拥有访问所有资源的权限。

  • 异常分为,中断(interrupt,异步),陷阱(trap),故障(fault),终止(abort)。

  • 系统调用属于陷阱异常,用户程序想要调用服务n时,就执行处理器提供的特殊指令 syscall n。这会触发异常处理程序,程序会解析参数,并调用合适的内核程序。普通函数调用则无法进入内核空间,也就无法访问特殊的资源。

  • 终止异常是硬件问题,如 RAM 校验等不可恢复的错误,只能直接终止程序

  • 段故障(segment fault)通常因为程序访问了受限的内存块导致的,如读未定义的虚拟内存,写只读内存等。

  • 并发流(concurrent flow)与处理器核心数无关,只要时间上重叠即可,多个进程以并发流形式运行成为多任务(multitasking);并行流(parallel flow)必须是在不同的处理器或计算机上同时运行。也就是说,并行流一定是并发流,反之不一定。

  • 模式位(mode bit)在处理器层上提供内核模式和用户模式的内存空间访问范围控制。没有设置模式位时,就处于用户模式,此时不允许使用特权指令(privileged instruction),如停止处理器,改变模式位,或发起一个 I/O 操作。

  • 上下文切换(context switch)是操作系统基于异常来实现的异常控制流,用于实现多任务。内核为每一个进程(process)维持一个上下文,具体内容包括寄存器,用户栈,内核栈,和各种内核数据结构如页表,当前进程信息的进程表,当前进程打开的文件的文件表。

  • 内核使用调度器(scheduler)对进程做上下文切换,从而切换当前运行的进程。切换时机由调度器决定,比如时间片用完,sleep,请求磁盘数据后。

  • init 进程是所有进程的祖先,如果父进程在回收它的僵尸子进程前就终止了,则由 init 进程回收它们。waitpid 可以用来等待自己的子进程终止或停止。

——- 20190101 ——–

  • fork 创建一个新的进程,execve 在原有进程上执行新的程序的 main 函数。

  • Linux 信号允许进程和内核中断其他进程。信号可以理解为一条消息,一个事件。

  • 发送信号的程序可以是内核,另一个程序,自己

  • 接收信号可以用 signal handler 来捕获这个信号(相当于嵌入式的中断函数),或者忽略。SIGKILL 和 SIGSTOP 信号无法被捕获和忽略。发出但没有被接收的信号叫 pending signal。一个类型最多只能有一个待处理信号,后面的同类型信号将会被丢弃。这个特性可以让程序有选择性的阻塞接收特定信号,做到不重复处理的功能。

  • 可以大量向 process group 进程组发送信号,父进程和子进程属于同一个进程组。使用 setpgid 设置自己的 pid 进程组

  • 阻塞信号分为两种:隐式和显式。隐式是指处理信号 s 的程序正在运行且未返回时新的信号 s 会变成待处理而没有被接收(注意最多只能有一个 s 待处理信号)。显式是使用 sigprocmask 明确的阻塞和接触阻塞选定的信号。

  • 信号处理程序需要:

    1. 尽可能简单,如设置一个flag。否则会被阻塞;
    2. 只调用异步信号安全的函数,他们有可重入(例如只访问局部变量),或不能被中断的特性
    3. 保存和回复 errno,在进入处理程序时吧 errno 保存在局部变量中,并在返回时恢复它(如果你的处理程序会调用会修改 errno 的方法的话)
    4. 访问全局变量时,阻塞所有信号。否则会出现竞争导致不可预知的结果
    5. 使用 volatile 声明全局变量。优化编译器有可能会优化掉周期读取全局变量的代码,进而使用缓存值。vollatile 会让编译器不要缓存这个变量;并强迫代码每次引用全局变量时,强制从内存中读取。
    6. 使用 sig_atomic_t 来声明标志,它是原子读写的(不可中断的)。这样就不需要暂时阻塞信号。原子性只能保证单个读写,不能保证 falg++ flag+=10 这样的操作。
  • signal 函数不同系统的语义可能不一样。比如每次触发信号处理之后可能需要再次调用 signal 函数,否则不会再次触发。

  • setjmp 和 longjmp 函数可以实现非本地跳转 nonlocal jump,即从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。

  • setjmp 在 env 缓冲区保存当前调用环境,以供后面的 longjmp 使用,返回 0。longjmp 调用后 setjmp 再次返回,此时返回值非 0(类似 fork)。该功能可用于捕获异常信号

  • C++ 和 Java 的异常机制是 setjmp 和 longjmp 的更加结构化的版本。try catch 类似 setjmp,throw 类似 longjmp。

深入理解计算机系统(第三版)/ CSAPP 杂谈,第7章:链接

Published / by sickworm / Leave a Comment
  • 链接器主要完成符号解析和重定位两个任务。

  • 目标文件有三种形式:可重定位目标文件(.so);可执行目标文件(.exe),共享目标文件(.so)。

  • linux x86-64 的可重定位目标文件使用 ELF 格式。ELF 头的前 16 字节描述文件对应系统的字的大小和字节顺序,后面还有头的大小,目标文件类型,机汽类型,各 section header 的文件偏移,以及它们的大小和数量。

  • 一般 ELF 包含以下几种 section:

    • .text:可执行机器码
    • .rodata:只读数据,如字符串常量和 switch 跳转表
    • .data:已初始化的全局和静态变量
    • :bss:未初始化或初始化为 0 的全局和静态变量。不占空间,仅仅为占位符。运行时在内存动态生成。
    • .symtab:符号表,存放定义和引用的函数与全局变量的信息。使用 STRIP 命令可以去掉符号表。
    • .rel.text:.text 中位置的列表,是重定位信息。链接器把目标文件组合起来的时候,需要修改这些位置,以让各个目标文件链接起来。一般来说,修改的是外部函数或者引用全局变量的位置,调用的本地函数的位置则不需要修改。
    • .rel.data: 引用或定义的所有全局变量的重定位信息。
    • .debug:调试符号表。用 -g 选项编译的时候才会得到这张表。
    • .line:源程序的行号与 .text 机器码的对应关系。用 -g 选项编译的时候才会得到这张表。
    • .strtab:字符串表,包括 .symtab,.debug,节头名字。以 null 结尾的字符串序列。

—- 2018.12.14 —-

  • .symtab 存储各 section 重定位信息

  • 弱全局符号是未赋值的全局符号,会被强全局符号覆盖,包括类型,所以容易导致类型不匹配的细微的bug;强全局符号是初始化的全局符号,会互相冲突。弱全局符号分配在 COMMON section 中,强全局符号分配在 .bss 中。

  • 静态库用于共享重复的代码,链接器仅会拷贝需要的函数。也可以通过参数拷贝所有函数。

  • gcc 的静态库链接是按顺序进行的。遇到目标文件 .o 时会把未定义和已定义的符号保存起来,遇到存档文件 .a 时,除了前面的操作,还会把 .a 的成员符号与未定义的符号比较,把匹配的成员符号对应的 .o 链接起来。这样的话因为是顺序的,如果把静态库放在前面,则会错过后面目标文件的匹配,从而在链接完所有文件,却还是有未定义符号,结果编译报错。

  • 所以一般做法是静态库文件放在最后。如果 .a 库相互之间也有依赖,则需要将 .a 库排序。

版权所有,转载请注明出处:
https://sickworm.com/?p=500

深入理解计算机系统(第三版)/ CSAPP 杂谈,第6章:储存器层次结构

Published / by sickworm / Leave a Comment
  • SRAM 贵,稳定,集成度低,用于高速缓存存储器

  • DRAM 较便宜,不稳定,集成度高,需要定时重新读写和纠错码,用于主存和帧缓冲区

  • DRAM 的存储单元(超单元)以二元阵列排列而不是线性排列,这样可以节省管脚。请求某个超单元先发送行,此时会将行缓存到内部行缓冲区;然后发送列,此时将该行该列的超单元数据返回给请求者。传统的 DRAM 会将剩余的数据丢掉,而 FPM DRAM会缓存整行。这两种DRAM早就已经停产了,现在主流是 DDR3/4 SDRAM。

  • 可擦写编程器 EEPROM 掉电数据不丢失,主要用于存储数据,如闪存(U盘)

  • 一般的程序都具有良好的局部性,即访问的数据都是在一个较密集的区间内,这样可以提高访问效率。但局部性也导致容易受到栈溢出攻击。印象中新版本的 Linux 内核有降低局部性,防止栈溢出攻击的策略。

  • 存储器层次结构的本质是,每一层存储设备都是较低一层的缓存。

  • 高速缓存步骤:1. 组选择(一般用内存中间的位避免相邻的内存分到同一个组) * 2.行匹配 3. 字抽取(返回)

  • 直接映射高速缓存命中遵循定的策略,在真实的程序中很常见,会导致令人困惑的性能问题。例如反复调用两个相邻的内存块(两个数组),而两个内存块映射的组索引恰好一样(块大小为2的幂时尤为容易出现),就会导致冲突不命中,只能每次重新加载。解决方案可以是稍微扩充一下内存块大小。直接映射高速缓存限制是只能有1行。

  • 组相联高速缓存每组可以有多行,冲突不命中的问题得到缓解。

  • 缓存经常会用到 write-back 机制,即更新的数据不会立刻写入到下一层,而是每隔一段时间写一次,以获得更高的性能。L1 对 RAM,RAM 对 ROM 都会用到。write-allocate 是当需要写的时候,如果没有匹配缓存,就先加载进来,而不是 write-through 直接写进去。write-back 和 write-allocate 经常一起使用。

  • i7 每个核心都有独立的 L1 i-cache (instructment) d-cache(data) 和 L2 unified cache。所有核心共享 L3 cache。L1 4周期,32kb,64组64块大小;L2 10周期,256KB,8行512组64块大小;L3 40-75 周期,8mb,16行8192组64块大小

版权所有,转载请注明出处:
https://sickworm.com/?p=494