Linux笔记

174 阅读15分钟

内容全部来自各种摘抄, 遇到一点就记录一点吧, 记录的东西非常的枯燥, 切忌漫无目的的看

逻辑地址、线性地址、物理地址和虚拟地址理解

进程间的通信方式(一):共享内存

Android匿名共享内存(Ashmem)原理

一、物理内存

1.1 物理内存的概念

  1、真实的内存, 就是我们常说的那个4G、8G、16G的内存条

  2、物理内存是CPU可以直接进行寻址的内存空间.

1.2 物理内存的缺点

  最初的操作系统并没有现在那么完善, 刚开始的时候, 程序是直接装载到物理内存中的, 这就导致了下面的一些问题:

1、程序编写困难

2、修改内存数据导致程序崩溃

1.3 程序编写困难

  1、操作系统同时运行多个程序, 编写程序是直接操作物理内存的, 而物理内存的大小又是很有限的, 比如8086只有20根地址线, 那么它的寻址空间就是1MB, 我们就说8086能支持1MB的物理内存, 即使我们安装了128M的内存条在主板上, 我们也只能说8086拥有1MB的物理内存空间, 同理我们现在大部分使用的是32位的机子, 32位的386以上CPU就可以支持最大4GB的物理内存空间了.

1.4 RAM

  RAM就是高速缓存, 通电就具有记忆功能, 断电就失去, 也就是运行内存, 用来存放临时文件, 而"内存"就是RAM的一种物理硬件, 广义来说两者也可以是同一概念, 而虚拟内存是系统利用硬盘分出来的具有辅助内存工作的虚拟RAM, 不是硬件, 但又依靠硬盘.

  内存就是RAM, 虚拟内存并不是把硬盘当作内存, 而是在硬盘上创建的交换文件. 当物理内存用完后, 虚拟内存管理器选择最近没有用过的, 低优先级的内存部分写到交换文件(页面文件)上, 并将需要访问内存的程序的内容从页面文件中换入到物理内存.

二、虚拟内存

2.1 概念

  虚拟内存也叫内存交换区, 不要把虚拟内存跟真实的插在主板上的内存条相挂钩, 虚拟内存它是"虚拟的"不存在的, 并不是实际的内存, 它只是内存管理的一种抽象.

  对于4G的Linux系统来说, 虚拟内存也为4G, 其中1G为系统的内存, 剩下的3G为应用程序的内存, 就是有1G虚拟内存是编写的应用程序操作不到的, 应用程序最多只能操作3G的虚拟内存空间.

三、物理内存与虚拟内存关联

  计算机的CPU操作的是物理内存的地址, 而现在编写的程序操作的都是虚拟内存地址.

3.1 分页机制

  现在的操作系统虚拟地址和物理地址建立对应关系采用的是页映射的方式.

  一般一页是4096个字节, 通过某种方式将虚拟地址映射到物理地址, 映射的关系是通过一张表实现的, 也就是页表.

  分页机制的思想是通过映射, 可以使连续的线性地址与物理地址相关联, 逻辑上连续的线性地址对应的物理地址可以不连续, 分页的作用 - 将线性地址转换为物理地址, 用大小相同的页替换大小不同的段.

  以块为单位的方法, 因为每次都需要占用一整块内存, 难免会造成在使用阶段, 出现大量程序没有使用, 但是内存却仍然被占用的弊端, 所以现在操作系统就使用了更加精细的划分方法, 将虚拟地址和物理地址进行分割, 就是结合一一对应和整块对应的中间点, 让内存区域不在以元器件和程序为单位, 而是以切割的小内存块为单位, 这样就会为内存腾出大量不需要的空间, 也不用把一整块程序都放进来. 小块内存之间有很多可以操作的余地, 我们把这种均匀切割的内存看做一页, 整体称作分页机制, 这种地址转换的过程称为页表机制.

3.2 地址隔离

  操作系统将虚拟内存映射到物理内存, 这种映射并不是简单的一个内存映射至另一内存, 它们是以块为单位划分, 在系统上运行的程序所使用的空间看做一整块, 在经过映射后, 物理内存中也将按块划分, 如果虚拟地址中, 程序访问的地址超过了自己的使用范畴, 那么系统就会将其认为成非法访问, 直接在虚拟层面上禁止了访问物理内存, 这样就不会存在内存相互占用的错误, 我们将计算机的这种行为称为**地址隔离.**

四、逻辑、线性、物理、虚拟地址

4.1 逻辑地址

  是指由程式产生的和段相关的偏移地址部分. 例如在进行C语音指针编程中, 能读取指针变量本身值, 这个值就是逻辑地址.

  CPU将一个虚拟内存空间中的地址(逻辑地址)转换为物理地址, 需要进行两部: 首先将给定一个逻辑地址, CPU要利用其段氏内存管理单元, 先将每个逻辑地址转换成一个线性地址, 再利用其页式内存管理单元, 转换为最终物理地址. 这样做两次转换, 的确是非常麻烦而且没有必要的, 因为直接可以把线性地址抽象给进程, 之所以这样冗余, Inte完全是为了兼容而已.

4.2 物理地址

  目前CPU外部地址总线上的寻址物理内存的地址信号, 是地址变换的最终结果地址, 如果启用了分页机制, 那么线性地址会使用页目录和页表中的项变换成物理地址, 如果没有启用分页机制, 那么线性地址就直接成为物理地址了.

4.3 虚拟地址

  进程使用虚拟内存中的地址, 由操作系统协助相关硬件, 把它"转换"成真正的物理地址, 这个**转换是所有问题讨论的关键. 有了这样的抽象, 一个程序, 就可以使用比真实物理地址大得多的地址空间. 甚至多个进程可以使用相同的地址, 因为转换后的物理地址并非相同的.

  逻辑地址也称为虚拟地址,** 因为和虚拟内存空间的概念类似, 逻辑地址也是和实际物理内存容量无关的. 逻辑地址和物理地址的差距是0xC0000000, 是由于虚拟地址 -> 线性地址 -> 物理地址映射正好差这个值, 这个值是由操作系统指定的.

  逻辑地址到线性地址是由CPU的段机制自动转换的. 如果没有开启分页管理, 则线性地址就是物理地址.

4.4 线性地址

  逻辑地址到物理地址变换之间的中间层, 程式代码会产生逻辑地址, 或者说是段中的偏移地址, 加上相应段的基地址就生成了一个线性地址. 如果启用了分页机制, 那么线性地址能再经变换以产生一个物理地址. 如果没有启用分页机制, 那么线性地址直接就是物理地址.

五、内存共享

5.1 内存共享

  共享内存允许两个或更多进程访问同一块内存, 就如同malloc()函数向不同进程返回了指向同一个物理内存区域的指针. 当一个进程改变了这块地址中的内容的时候, 其他进程都会察觉到这个更改.   共享内存实际上是进程通过shmget(shared memory ge获取共享内存)来分配一个共享内存块, 然后每个进程通过shmat(shared memory attach绑定到共享内存), 将进程的逻辑虚拟地址空间指向共享内存块中. 随后需要访问这个共享内存块的进程都必须将这个共享内存绑定到自己的地址空间中去. 当一个进程往一个共享内存块中写入数据, 共享这个内存区域的所有进程都可以看到其中的内容.

5.1.1 共享内存的特点
  • 1、共享内存是进程间共享数据的一种最快的方法. 一个进程向共享的内存区域写入了数据, 共享这个内存区域的所有进程就可以立刻看到其中的内容.
  • 2、使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥. 若一个进程正在向共享内存区写数据, 则在它做完这一步操作前, 别的进程不应当去读、写这些数据.
5.1.2 共享内存的通信

  因为所有进程共享同一块内存, 共享内存在各种进行间通信方式中具有最高的效率. 访问共享内存区域和访问进程独有的内存区域一样快, 并不需要通过系统调用或者其他需要切入内核的过程来完成. 同时它也避免了对数据的各种不必要的复制.

  因为系统内核没有对访问共享内存进行同步, 需要提供自己的同步措施, 例如在数据被写入之前不允许进程从共享内存中读取信息、不允许两个进程同时向同一个共享内存地址写入数据等. 解决这些问题的常用方法是通过使用信号量进行同步.

5.1.3 共享内存的内存模型

  要使用一块共享内存, 进程必须首先分配它, 随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中. 当完成通信之后, 所有进程都将脱离共享内存, 并且由一个进程释放该共享内存块.

  理解Linux系统内存模型可以有助于解释这个绑定的过程. 在Linux系统中, 每个进程的虚拟内存是被分为许多个页面的. 这些内存页面中包含了实际的数据. 每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系. 尽管每个进程都有自己的内存地址, 不同的进程可以同时将同一个内存页面映射到自己的地址空间中, 从而达到共享内存的目的.

  分配一个新的共享内存块会创建新的内存页面. 因为所有进程都希望共享对同一个块内存的访问, 只应由一个进程创建一块新的共享内存. 再次分配一块已经存在的内存块不会创建新的页面, 而只是会返回一个标识该内存块的标识符. 一个进程如需使用这个共享内存块, 则首先需要将它绑定到自己的地址空间中. 这样会创建一个从进程本身虚拟地址到共享页面的映射关系. 当对共享内存的使用结束之后, 这个映射关系将被删除. 当再也没有进程需要使用这个共享内存块的时候, 必须有一个(且只能是一个)进程负责释放这个被共享的内存页面

5.2 内存映射

  指应用进程将文件映射到内存中, 应用进程可直接使用(读取和修改), 可省略将磁盘文件读入应用进程的步骤.

  要让一个进程获取对一块共享内存的访问, 这个进程必须先调用shmat. 将shmget返回的共享内存标识符SHMID传递给这个函数作为第一个参数. 该函数的第二个参数是一个指针, 指向你希望用于映射该共享内存块的进程虚拟内存地址, 如果你指定NULL则Linux会自动选择一个合适的地址用于映射. 第三个参数是一个标志位.

5.3 匿名内存共享

  Android可以使用Linux的一切IPC通信方式, 包括共享内存, 不过Android主要使用的方式是匿名共享内存Ashmem, 跟原生的不太一样, 比如它在自己的驱动中添加了互斥锁, 另外通过fd的传递来实现共享内存的传递.

六、Linux函数

6.1 shmget
#include <sys/ipc.h>
#include <sys/shm.h>

/**
 * @desc:         创建或打开一块共享内存区
 * @param key:    进程间通信键值
 * @param size:   共享存储段的长度(字节)
 * @param shmflg: 标识函数的行为及共享内存的权限, 其取值如下:
 *				  IPC_CREATE: 如果不存在就创建
 *				  IPC_EXCAL:  如果已经存在则返回失败
 * @return: 	  成功: 共享内存标识符;
 *				  失败: -1
 */
int shmget(key_t key, size_t size, int shmflg)
6.2 shmat
#include <sys/types.h>
#include <sys/shm.h>

/**
 * @desc: 将一个共享内存段映射到调用进程的数据段中, 简单来说说就说让进程和共享内存建立一种联系, 
 *		  让进程某个指针指向此共享内存.
 * @param shmid:   共享内存标识符, shmget()的返回值
 * @param shmaddr: 共享内存映射地址(若为NULL则由系统自动指定), 推荐使用NULL
 * @param shmflg:  共享内存段的访问权限和映射条件(通常为0), 具体取值如下:
 *				   0: 共享内存具有可读可写权限
 *				   SHM_RDONLY: 只读
 * @return: 成功: 共享内存段映射地址(相当于这个指针就指向此共享内存)
 *			失败: -1
 *
void *shmat(int shmid, const void *shmaddr, int shmflg)
6.3 mmap

  共享内存允许两个或多个进程共享一个给定的存储区, 因为数据不需要来回复制, 所以是最快的一种进程间通信机制, 共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用**匿名映射**)机制实现, 也可以通过系统V共享内存机制实现.

6.3.1 mmap机制

  在磁盘上建立一个文件, 每个进程存储器里面单独开辟一个空间来进行映射, 如果多进程的话, 那么不会对实际的物理存储器(主存)消耗太大.

  shm的机制: 每个进程共享内存都直接映射到实际物理存储器里面

6.3.2 两者区别:
  • 1、mmap保存到实际硬盘, 实际存储并没有反映到主存上, 优点: 存储量可以很大(多于主存), 缺点: 进程间读写速度要比主存慢
  • 2、shm保存到物理存储器(主存), 实际的存储量直接反映到主存上. **优点: **进程间读写速度要比磁盘快, 缺点: 存储量不能非常大
  • 3、**使用上看: **如果分配的存储量不大, 那么使用shm; 如果存储量大, 那么使用mmap
6.3.3 mmap参数注释
/**
 * @param fd:  为即将映射到进程空间的文件描述符, 一般由open()返回, 同时fd可以指定为-1, 此时
 *             须指定flag参数中的MAP_ANON, 表明进行的是匿名映射(不涉及具体的文件名, 避免了
 			   文件的创建与打开, 只能用于具有亲缘关系的进程间通信)
 * @param len: 映射到调用进程地址空间的字节数, 它从被映射文件开头offset个字节开始算起
 */
void* mmap(void* addr, size_t len, int prot, int flags, int fd, off_t offset)
6.3.4 匿名内存映射

  使用特殊文件提供匿名内存映射, 适用于具有亲缘关系的进程之间, 由于父子进程特殊的关系, 在父进程中先调用mmap(), 然后调用fork(). 那么在调用fork()之后, 子进程继承父进程匿名映射后的地址空间, 同样也继承mmap()返回的地址, 这样, 父子进程就可以通过映射区域进行通信了. **注意: **这里不是一般的继承关系, 一般来说, 子进程单独维护从父进程继承下来的一些变量, 而mmap()返回的地址, 却由父子进程共同维护.

6.4 malloc
6.4.1 虚拟内存工作流程

  当每个进程创建的时候, 内核会为进程分配4G的虚拟内存, 当进程还没有开始运行时, 这只是内存布局. 实际上并不立即就把虚拟内存对应位置的程序数据和代码拷贝到物理内存中, 只是建立好虚拟内存和磁盘文件之间的映射关系, 这个时候数据和代码还是在磁盘上的, 当运行到对应的程序时, 进程去寻找页表, 发现页表中地址没有存放在物理内存上, 而是在磁盘上, 于是发生缺页异常, 于是将磁盘上的数据拷贝到物理内存中

6.4.2 malloc

  进程运行过程中, 通过malloc来动态分配内存时, 只是分配了虚拟内存, 即为这块虚拟内存对于的页表项作相应设置, 当进程真正访问到此数据时, 才引发缺页异常.