操作系统学习
材料
- 深入了解计算机系统
- 王道前一个月
- apue
- linux内核设计与实现
- 408操作系统(可选)
预期
todo
深入了解计算机系统
time
6.29 - ?
第一章计算机系统漫游
trips
- 系统中的所有信息都是由一串比特表示的,区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。
- 在每个微处理器芯片中:每个CPU都有自己的L1、L2缓存,所有CPU共享L3缓存,L3缓存与内存通信。
- 这是个总纲,后面详细内容理解了,在依赖总纲去整理总结
代码运行生命周期
系统的硬件组成
- 总线:携带信息字节在各个部件间传递 --> 32位4个字节、64位8个字节。
- IO设备:all need IO总线
- 主存:临时存储设备,存放程序和程序处理的数据。物理上DRAM芯片。逻辑上是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。
- 处理器:执行存储在主存中指令的引擎。
核心是程序计数器(PC),在任何时刻,PC都指向主存的某条机器语言指令。
处理器一直在不断的执行程序计数器指向的指令,执行完一条指令后,再更新程序计数器,使其指向下一条指令。
CPU在指令中可能会执行这些操作:
加载:主存到寄存器
存储:寄存器到主存
操作:计算
跳转:从指令本身抽取下一个指令的地址,覆盖到PC中
运行前编译:
- 预处理器(预处理 -> .i)
- 编译器(编译 -> .s)
- 汇编器(汇编 -> .o)
- 链接器(链接 -> .exe)
运行时
- DMA把应用程序的数据和代码加载到主存(通过IO总线)
- CPU在主存中取数据开始运行(通过IO总线)
- 运行后的事情后序总结
操作系统三要素
需求
操作系统的两个基本功能:
(1)防止硬件被失控的程序滥用;
(2)向应用程序提供简单一致的机制来控制复杂又不同的低级硬件设备。操作系统通过几个基本的抽象概念(进程、虚拟内存、和文件)来实现这两个功能。
进程
进程与并发的故事
- 假象:程序看上去是独占的使用处理器、主存和IO设备;处理器看上去就像在不间断的一条接一条的执行程序中的指令,即该程序的代码和数据是系统在内存中唯一的对象(可以举个case)。这些假象是通过进程的概念来实现的。(需求分析->多道执行流的管理)
- 进程是操作系统对一个正在运行的一个程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占的使用硬件。(概念,独占与应用系统资源管理)
CPU独占
- 在任何一个时刻,单处理器系统都只能执行一个进程的代码。(前提背景,进程时执行程序的最小单位,分配资源,内存)
- 并发通过处理器在进程间切换来实现的,这种交错执行叫上下文切换(cpu独占实现)
- 上下文:是操作系统保持跟踪进程运行所需的所有状态信息,包括:PC和寄存器文件的当前值,以及主存的内容(后序继续补充)
- 当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从它上次停止的地方开始运行(实现具体过程)
内存独占
- 主存独占后序补充
内核的故事
- 内核是操作系统代码常驻主存的部分(最上面,每个进程都默认的),内核不是一个独立的进程,相反,它是系统管理全部进程所用代码和数据结构的集合(操作系统的sdk,哦不,系统调用 代码常驻本地的sdk -> commonapi ?)
- 当应用程序需要操作系统的某些操作时,比如进程切换、读写文件,它就执行一条特殊的系统调用指令,将控制权传递给内核(权限检查),然后内核执行被请求的操作并返回因应用程序(虽然没有进程的上下文切换,但是是用户态和内核态的切换,需要权限检查,信号处理,代码多余检查等等额外开销,执行内核代码比执行用户态消耗大)
- 我的理解是:用户态代码只能用cpu的操作来对内存上的数据进行管理,对于其他内容如操作IO设备、管理线程、进程数据都不可以直接做,想操作可以,需要调系统调用来执行这些(私有数据成员与共有接口),pid,线程id是只能读的,不能改,优先级也需要调系统调用来改,与内核相关的东西全部由内核代码来维护管理,用户态只能用这个sdk来获取或者管理。
- 但是用户态代码要尽量减少系统调用,虽然系统调用没有进程切换,但是陷入内核态,到切换回用户态需要很多冗余的操作,一个demo就是fread和read的对比,读磁盘的速度能差10倍以上。
- 系统调用消耗在于:跳转前:权限检查,特权改变。跳转后:处理异步信号、改权限、判断是否需要调度。调用中:调用前后的代码都在相同的虚拟地址空间中(地址空间并没有切换),(栈不同)运行内核代码时使用的栈是内核栈,系统调用时需要进行栈的切换。(参数传递)普通函数调用是通过栈来传递参数的;而系统调用是通过寄存器来传递参数。因为栈要切换,参数传递起来不那么简单。(CPU执行内核代码和执行用户程序代码区别)做了大量全面的检查。
- 参考:https://blog.csdn.net/jibing57/article/details/7566541
线程
- 一个进程实际上可以有多个称为线程的执行单元组成,!!!每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。 -> 多线程比多进程间更易共享数据。
虚拟内存
- 为每个进程提供了一个假象,即每个进程都在独占的使用主存,每个进程看到的内存都是一只的(内存独占)
- 包含:程序代码和数据(全局和静态)、堆、共享库、栈、内核虚拟内存
- 共享库:大约在地址的中间部分是一块用来存放像C标准库和数学库这样的共享的代码和数据的区域
并发和并行
- 需求:想要计算机做的更多,想要计算机运行的更快
线程级并发
- 传统单核这种并发是模拟出来的,通过一台计算机在它执行的进程间快速切换来实现的
- 超线程:允许一个CPU执行多个控制流的技术。常规的处理器需要大约2000个时钟周期做不同线程的切换(可以通过空间换时间省掉这部分,存储相关的可以多份,某些单元仅一份,充分利用资源实现),而超线程的处理器可以在单个周期的基础上决定要执行哪一个线程,使得CPU能更高效的利用资源。如4核⑧线程
指令级并行(听过)
- 现代处理器可以同时执行多条指令
单指令、多数据并行(听过)
第二章 信息的表示和处理
trips(这章没啥意思)
- 大多数(all)使用字节作为最小的可寻址的内存单位(8位)
- 机器级编程将内存视为一个非常大的字节数组,称为虚拟内存,实际实现是将动态随机访问存储器(DRAM)、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组
- C语言中一个指针的值(无论指向整数、结构体。。)都是个存储块的第一个字节的虚拟地址,C编译器还把每个指针和类型信息联席起来。
- 每台计算机都有一个字长,指明指针数据的大小。字长决定了虚拟内存的大小,32位为4GB,64位为16EB
- 对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么(首地址)、以在内存中如何排列这些字节(大小端)
- unicode编码包含了ascii编码
- 由于整数乘法比移位和加法的代价要大的多,许多C语言编译器试图以移位、加法和减法的组合来消除很多整数乘以常数的情况,例如:x*14 ; 14 = 2
3 + 2
2 + 2`1; --> (x<<3)+(x<<2)+(x<<1) - 除法比乘法还慢优化为移位操作
第三章 程序的机器级表示
trips
- 大多数ISA,包括x86-64,将程序的行为描述成好像每条都是按顺序执行的,一条指令结束后,下一条指令再开始。
- 程序内存包含:程序的可执行机器代码、操作系统需要的一些信息、用来管理函数调用和返回的运行时栈。以及用户分配的内存块。
- 64位的虚拟地址中,这些地址的高16位必须设置位0,实际能够指定的是256TB,操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器内存中的物理地址。
- 一个64位的中央处理单元包含一组16个存储8个字节的通用目的寄存器,这些寄存器用来存储整型数据和指针。
- 局部变量通常保存在寄存器中(不够用了还是得用栈),而不是内存中。
寄存器
- 编译过程中,编译器会把代码转化成处理器执行的非常基本的指令。
- CPU的寄存器很多,除了这些还有很多,这些只是书上介绍的:PC、16个整数寄存器(可存储指针、整形数据、临时数据如局部变量、参数和返回值)、条件玛寄存器、一组向量寄存器。
过程(函数)
函数调用过程:
- 原函数P调用新函数Q(寄存器要先给Q用了,用完再还回来)
- 进去Q之前把PC设置位Q的代码的起始地址,返回时PC设置为调用Q之后那条指令的地址。
- 当调用函数Q需要的存储空间超出寄存器所能存放的大小时,就会在栈上分配空间,
- 调用前,Q的栈里需要包含的信息(寄存器会有参数和临时变量,寄存器不够用再用Q的栈):局部变量、被保存的寄存器的值(原P的控制的寄存器有很多)、Q的下一条指令的地址、寄存器放不下的参数(寄存器能放6个)。还有可能会动态增加栈(用到的时候再增加),前面这些在进入Q的函数控制前就已经分配好了。(准备好,这时寄存器已经准备给Q用了)
- 进入Q函数时,控制转移到Q,用属于Q的寄存器、栈(前面准备好的),过程中可能还会增加栈(额外)
- 返回:把返回值给 P,P在Q的栈中恢复寄存器的现场(未控制转移),清除Q的栈空间(地址偏移即可)栈的自动管理内存,控制权返回到 P。
结构体
- 类似于数组的实现,结构的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址,编译器维护关于每个结构类型的信息,指示每个字段的字节偏移。
- 数据对齐:总线8个字节
对抗缓冲区溢出攻击
栈随机化
目的:栈的位置在程序每次运行时都有变化。
实现:程序开始时,在栈上分配一段0~n字节之间的随机大小的空间,程序不使用这段空间,但是它会导致程序每次执行时后续的栈位置发生了变化
栈破坏检测
栈的后面生成一些哨兵,调用新函数返回后观察哨兵是否发生变化