OS内存进程

总论 现代操作系统,内核一般会提供 4 个基本能力: 管理进程、线程,决定哪个进程、线程使用 CPU,也是进程调度的能力; 管理内存,决定内存的分配和回收,也是内存管理的能力; 管理硬件设备,为进程与硬件设备之间提供通信能力,也是硬件通信能力; 提供系统调用,如果应用程序要运行更高权限运行的服务,就需要有系统调用,是用户程序与操作系统之间的接口 内核具有很高的权限,可控制 cpu、内存、硬盘等硬件,而应用程序具有的权限很小。 因此大多数操作系统,把内存分成了两个区域 内核空间:这个内存空间只有内核程序可以访问 用户空间:这个内存空间专门给应用程序使用 用户空间的代码只能访问一个局部的内存空间,而内核空间的代码可以访问所有内存空间 当程序使用用户空间时,我们常说该程序在用户态执行 当程序使内核空间时,程序则在内核态执行 应用程序如果需要进入内核空间,就需要通过系统调用 内核程序执行在内核态,用户程序执行在用户态 应用程序使用系统调用时,会产生一个中断。发生中断后, CPU 会中断当前在执行的用户程序,转而跳转到中断处理程序(开始执行内核程序) 内核处理完后,主动触发中断,把 CPU 执行权限交回给用户程序,回到用户态继续工作 现代操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响 内存管理 虚拟内存 单片机的 CPU 是直接操作内存的「物理地址」 想在内存中同时运行两个程序是不可能的。如果第一个程序在 2000 的位置写入一个新的值,将会擦掉第二个程序存放在相同位置上的所有内容,所以同时运行两个程序是根本行不通的 解决方法? 把进程所使用的地址「隔离」开来,即让操作系统为每个进程分配独立的一套「虚拟地址」,人人都有,大家自己玩自己的地址就行,互不干涉 有个前提每个进程都不能访问物理地址,至于虚拟地址最终怎么落到物理内存里,对进程来说是透明的 理解:之所以不能同时运行,就是因为两个程序引用相同物理地址,也就是不能让其使用相同的物理地址,加一层映射过去 操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来 程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了 引出两种地址概念: 我们程序所使用的内存地址叫做虚拟内存地址 实际存在硬件里面的空间地址叫物理内存地址 操作系统引入了虚拟内存,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存 虚拟内存作用: 第一,虚拟内存使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域 理解:加载一个很大的存储,原本只能一次性,现在可分按需批 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性 内存分段 程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来 分段机制下,虚拟地址的物理地址的映射 分段机制下的虚拟地址由两部分:段选择因子和段内偏移量 段选择子就保存在段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。 虚拟地址中的段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址 虚拟地址是通过段表与物理地址进行映射的,分段机制会把程序的虚拟地址分成 4 个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址 问题: 第一个是内存碎片 内存分段管理可以做到段根据实际需求分配内存,有多少需求就分配多大的段,所以不会出现内部内存碎片,但是由于每个段的长度不固定,所以多个段未必能恰好使用所有的内存空间,会产生了多个不连续的小物理内存,导致新的程序无法被装载,所以会出现外部内存碎片 解决「外部内存碎片」的是内存交换 内存交换空间,在Linux中,是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,用于内存与硬盘的空间交换 第二个是内存交换的效率低 如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿 内存分页 分段的好处就是能产生连续的内存空间 但会出现「外部内存碎片和内存交换的空间太大」的问题 解决这些问题,那么就要想出能少出现一些内存碎片的办法 分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小,这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB 虚拟地址与物理地址之间通过页表来映射 页表存储在内存里,内存管理单元 (MMU)将虚拟内存地址转换成物理地址工作 当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行 解决分段问题 内存分页由于内存空间都是预先划分好的,采用了分页,页与页之间是紧密排列的,所以不会有外部碎片 内存分页机制分配内存的最小单位是一页,即程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,所以针对内存分页机制会有内部内存碎片 内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高 分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中,只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去 分页机制,虚拟地址与物理地址的映射 分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址 总结一下,对于一个内存地址转换,就是这样三个步骤: 把虚拟内存地址,切分成页号和偏移量; 根据页号,从页表里面,查询对应的物理页号; 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址 缺陷: 操作系统是可以同时运行非常多的进程的,那这不就意味着页表会非常的庞大 在 32 位的环境,虚拟地址空间共 4GB,假设一个页大小是 4KB(2^12),就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节(4B)来存储,那整个 4GB 空间的映射就需要 4MB 的内存来存储页表 100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存 多级页表 解决分页的缺陷 把这 100 多万个「页表项」的单级页表再分页,将页表(一级页表)分为 1024 页表(二级页表),每个表(二级页表)中包含 1024 个「页表项」,形成二级分页 算机组成原理里面无处不在的局部性原理 每个进程都有 4GB 的虚拟地址空间,而显然对于大多数程序来说,其使用到的空间远未达到 4GB,因为会存在部分对应的页表项都是空的,根本没有分配,对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存 一级页表就可以覆盖整个 4GB 虚拟地址空间,但如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表 深入理解思想: 从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址 页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项 此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建 TLB 多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,带来了时间上的开销 程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域 最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB 有TLB 后,那么 CPU 在寻址时,先查 TLB,如果没找到,才会继续查常规的页表 TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个 段页式内存管理 内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为段页式内存管理 段页式内存管理实现方式: 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制 再把每个段划分为多个页,也就对分段划分出来的连续空间,再划分固定大小的页 这样,地址结构就由段号、段内页号和页内位移三部分组成 段页式地址变换中要得到物理地址须经过三次内存访问: 第一次访问段表,得到页表起始地址; 第二次访问页表,得到物理页号; 第三次将物理页号与页内位移组合,得到物理地址 Linux内存结构 Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护 Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制 每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存 分配内存 应用程序通过 申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存...

Network(TCP)

TCP握手挥手 TCP TCP 是面向连接的、可靠的、基于字节流的传输层通信协议 面向连接:一定是「一对一」才能连接,不能像 UDP 协议一个主机同时向多个主机发送消息 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序不知道「消息的边界」,无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃 TCP连接 用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接 Socket:由 IP 地址和端口号组成 序列号:用来解决乱序问题等 窗口大小:用来做流量控制 TCP与UDP区别 1. 连接 TCP 是面向连接的传输层协议,传输数据前先要建立连接。 UDP 是不需要连接,即刻传输数据。 2. 服务对象 TCP 是一对一的两点服务,即一条连接只有两个端点。 UDP 支持一对一、一对多、多对多的交互通信 3. 可靠性 TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。 UDP 是尽最大努力交付,不保证可靠交付数据。但是可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议 4. 拥塞控制、流量控制 TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。 UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。 5. 传输方式 TCP 是流式传输,没有边界,但保证顺序和可靠。 UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。 6. 分片不同 TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。 UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层 传输层的「端口号」是为了区分同一个主机上不同应用程序的数据包 传输层两个传输协议 TCP 和 UDP,在内核中是两个完全独立的软件模块...

网络基础

网络模型 同一台设备上的进程间通信,有很多种方式,比如有管道、消息队列、共享内存、信号等方式 不同设备上的进程间通信,需要网络通信,而设备是多样性的,协商出了一套通用的网络协议 TCP/IP网络模型 应用层 最上层的,也是我们能直接接触到的,应用就把应用数据传给下一层,就是传输层 应用层为用户提供应用功能,比如 HTTP、FTP等 传输层 传输层会有两个传输协议,分别是 TCP 和 UDP TCP:大部分应用使用的正是 TCP 传输层协议,比如HTTP。TCP有流量控制、超时重传、拥塞控制等,为保证数据包能可靠地传输给对方 UDP:只负责发送数据包,不保证数据包是否能抵达对方,它实时性相对更好,传输效率也高 应用需要传输的数据可能会非常大,当传输层的数据包大小超过 MSS(TCP 最大报文段长度),将数据包分块,这样即使中途有一个分块丢失或损坏了,只需要重新发送这一个分块 MTU:一个网络包最大长度,以太网中一般为 1500 字节。 MSS:除去 IP 和 TCP 头部后,一个网络包所能容纳的 TCP 数据的最大长度。 传输层负责把数据包传给应用,但一台设备上会有很多应用接收或者传输数据,用编号将应用区分开来,这个编号是端口 80 端口通常是 Web 服务器用的,22 端口通常是远程登录服务器用的 对于浏览器(客户端)中的每个标签栏都是一个独立的进程,操作系统会为这些进程分配临时的端口号 网络层 传输层协议处理太多的事情,只需要服务好应用即可;而实际的传输功能就交给下一层,也就网络层,负责将数据从一个设备传输到另一个设备 IP 协议将传输层的报文作为数据部分,再加上 IP 包头组装成 IP 报文;如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会再次进行分片,得到一个即将发送到网络的 IP 报文 IP 地址给设备进行编号 IPv4 协议, IP 地址共 32 位,分成了四段(比如192.168.100.1),每段是8位 将 IP 地址分成两种意义: 网络号,负责标识该 IP 地址是属于哪个「子网」的; 主机号,负责标识同一「子网」下的不同主机 配合子网掩码才能算出 IP 地址 的网络号和主机号 比如 10....

Java细知识

单元测试 单元测试:针对最小的功能单元编写测试代码 Java程序最小的功能单元是方法,对Java程序进行单元测试就是针对单个方法测试 测试驱动开发 先编写接口,紧接着编写测试。编写完测试后,我们才开始真正编写实现代码 举例子 public class Factorial { public static long fact(long n) { long r = 1; for (long i = 1; i <= n; i++) { r = r * i; } return r; } } 测试这方法,一个很自然的想法是编写一个main()方法,然后运行一些测试代码: public class Test { public static void main(String[] args) { if (fact(10) == 3628800) { System.out.println("pass"); } else { System.out.println("fail"); } } } 只能有一个main()方法,不能把测试代码分离 是没有打印出测试结果和期望结果,例如,expected: 3628800, but actual: 123456 很难编写一组通用的测试代码 编写JUnit测试 JUnit是一个开源的Java语言的单元测试框架,专门针对Java设计...

Java高级

异常处理 Java的异常 调用方获取调用失败的信息 约定返回错误码 处理一个文件,如果返回0,表示成功,返回其他整数,表示约定的错误码: int code = processFile("C:\\test.txt"); if (code == 0) { // ok: } else { // error: switch (code) { case 1: // file not found: case 2: // no read permission: default: // unknown error: } } 语言层面上提供一个异常处理机制、 try { String s = processFile(“C:\\test.txt”); // ok: } catch (FileNotFoundException e) { // file not found: } catch (SecurityException e) { // no read permission: } catch (IOException e) { // io error: } catch (Exception e) { // other error: } 异常是class,它的继承关系如下:...

Java中级

面向对象基础 理解: class是一种对象模版,定义了如何创建实例,class本身就是一种数据类型 instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同 定义class 创建一个类,例如,给这个类命名为Person,就是定义一个class: class Person { public String name; public int age; } class Book { public String name; public String author; public String isbn; public double price; } 创建实例 定义了class,只是定义了对象模版,而要根据对象模版创建出真正的对象实例,必须用new操作符 Person ming = new Person(); 区分Person ming是定义Person类型的变量ming,而new Person()是创建Person实例 ming.name = "Xiao Ming"; // 对字段name赋值 ming.age = 12; // 对字段age赋值 System.out.println(ming.name); // 访问字段name Person hong = new Person(); hong.name = "Xiao Hong"; hong.age = 15; 上述两个变量分别指向两个不同的实例,它们在内存中的结构如下: ┌──────────────────┐ ming ──────▶│Person instance │ ├──────────────────┤ │name = "Xiao Ming"│ │age = 12 │ └──────────────────┘ ┌──────────────────┐ hong ──────▶│Person instance │ ├──────────────────┤ │name = "Xiao Hong"│ │age = 15 │ └──────────────────┘ 方法 意义:...

Python高级

Python高级 面向对象编程 以一个例子来说明面向过程和面向对象在程序流程上的不同之处 假设我们要处理学生的成绩表,表示学生的成绩,面向过程的程序可以用dict表示: std1 = { 'name': 'Michael', 'score': 98 } std2 = { 'name': 'Bob', 'score': 81 } 而处理学生成绩可以通过函数实现,比如打印学生的成绩: def print_score(std): print('%s: %s' % (std['name'], std['score'])) 面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来 class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print('%s: %s' % (self.name, self.score)) 给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向对象的程序写出来就像这样: bart = Student('Bart Simpson', 59) lisa = Student('Lisa Simpson', 87) bart.print_score() lisa.print_score() 类和实例 类是抽象的模板,实例是类创建的一个个具体的“对象” 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数 定义类是通过class关键字: class Student(object): pass class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,通常,没有合适的继承类,就使用object类,是所有类最终都会继承的类 由于类可以起到模板的作用,因此,在创建实例的时候,把我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把name,score等属性绑上去:...

Python中级

Python中级使用 函数 调用函数 参数可以多个 >>> max(1, 2) 2 >>> max(2, 3, 1, -5) 3 参数类型需要一致 >>> abs('a') TypeError: bad operand type for abs(): 'str' 函数调用 >>> abs(100) 100 >>> abs(-20) 20 数据类型转换 >>> int('123') 123 >>> int(12.34) 12 >>> float('12.34') 12.34 >>> str(1.23) '1.23' >>> str(100) '100' >>> bool(1) True >>> bool('') False 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量 >>> a = abs # 变量a指向abs函数 >>> a(-1) # 所以也可以通过a调用abs函数 1 定义函数 要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回 def my_abs(x): if x >= 0: return x else: return -x print(my_abs(-99)) 空函数:定义一个什么事也不做的空函数,可以用pass语句...

Git实战

Git使用 分布式工作流程 集中式工作流——————–单点协作 两个开发者从中心仓库克隆代码下来,同时作了一些修改,只有第一个开发者可以顺利地把数据推送回共享服务器。第二个开发者在推送修改之前,必须先将第一个人的工作合并进来,这样才不会覆盖第一个人的修改 集成管理工作流 项目维护者推送到主仓库。 贡献者克隆此仓库,做出修改。 贡献者将数据推送到自己的公开仓库。 贡献者给维护者发送邮件,请求拉取自己的更新。 维护者在自己本地的仓库中,将贡献者的仓库加为远程仓库并合并修改。 维护者将合并后的修改推送到主仓库。 司令官与副官工作流——-大型软件:Linux 普通开发者在自己的特性分支上工作,并根据 master 分支进行变基。 这里是司令官的master分支。 副官将普通开发者的特性分支合并到自己的 master 分支中。 司令官将所有副官的 master 分支并入自己的 master 分支中。 司令官将集成后的 master 分支推送到参考仓库中,以便所有其他开发者以此为基础进行变基。 向项目贡献 影响因素 : 活跃贡献者的数量,项目的工作流程,提交权限 提交准则 git diff --check,它将会找到可能的空白错误并将它们为你列出来 空白错误是指行尾的空格、Tab 制表符,和行首空格后跟 Tab 制表符的行为 每一个提交成为一个逻辑上的独立变更集 不要一次提交解决好多问题 每个提交写清楚,让你的同事工作容易些 有一个优秀的提交信息 私有小团队 私有 : 只有这几个开发者有仓库的推送权限 主采用SVN 区别:合并发生在客户端这边而不是在提交时发生在服务器那边 工作流程: 你通常在一个特性分支工作一会儿,当它准备好整合时合并回你的 master 分支。 当想要共享工作时,将其合并回你自己的 master 分支,如果有他人改动的话然后抓取并合并 origin/master,最终推送到服务器上的 master 分支 私有管理团队 公司使用了一种整合-管理者工作流程,独立小组的工作只能被特定的工程师整合,主仓库的 master 分支只能被那些工程师更新 所有的工作都是在基于团队的分支上完成的并且稍后会被整合者拉到一起 新建一个分支,与他人一起工作,可以加-u 也要注意 -u 标记;这是 --set-upstream 的简写,该标记会为之后轻松地推送与拉取配置分支...

Python基本

Python基础语法 Python基础 # print absolute value of an integer: a = 100 if a >= 0: print(a) else: print(-a) Python的语法比较简单 #开头的语句是注释 当语句以冒号:结尾时,缩进的语句视为代码块 缩进实用四个空格的缩进 议input 用input()读取用户的输入 birth = input('birth: ') if birth < 2000: print('00前') TypeError: unorderable types: str() > int() 因为input()返回的数据类型是str,str不能直接和整数比较,必须先把str转换成整数 s = input('birth: ') birth = int(s) if birth < 2000: print('00前') 可以得到正确地结果 议不可变对象:str是不变对象,list是可变对象 对于可变对象,比如list >>> a = ['c', 'b', 'a'] >>> a.sort() >>> a ['a', 'b', 'c'] 对于不可变对象,比如str...