2017 September 17 —— improvement; android; java; os

Linux 系统

补一波 OS 基础知识:

Unix 的输出哲学: 极简化,文本化,面向流,设备无关格式

Linux

万物皆文本

大道即简,万物皆文本

文本化带来的通用化,对使用者友好,配置文本化,甚至硬件也文本化,只需要看文本遍可知道所有硬件的挂载状态,硬件的配置,硬件特性修改也只需要操作文本便可以完成;

程序间的组合沟通,同样适用通用文本,文本只需要保证编解码一致性则可以保证程序间的独立性,下游不关心上游,上游不关注下游,彼此独立;

文本的解析可能带来一些性能方面的影响,但当无法用数据证明文本的解析对系统造成了影响成为性能瓶颈时,对其的改变便是过早优化 – 用数据说话,而不是猜测 – From 调试九法

文本传输带来的空间尺度问题 - 在高效的数据压缩策略下,文本传输与二进制传输几乎无差别,但数据压缩的压缩算法同样对性能可能造成影响;

Linux 丰富的以文本形式存在的脚本,启动脚本,配置脚本,执行脚本,脚本作为胶水语言,用于弥补设计上的缺陷,事实上无论是自顶向下的设计还是自底向上的设计都存在其对应的缺陷;自顶向下的问题在于可能存在无法实现的下层,自底向上的缺陷在于存在过多与最终目的无关的设计与实现;随之衍生出”正确的自顶向下”以及依靠胶合层实现的双管齐下,上下层独立;

Linux

Linux 有多重分离的发行版本,我常用的 CentOS,Ubuntu,RedHat…, 各版本甚至对于同一功能机制的实现都各不一样,如防火墙控制策略

Linux 中的丰富的 IPC 机制为 Linux 中机制与策略实现上下层分离提供了良好的基础支持: 信号,管道, IO 重定向,共享内存,Socket 等..

Linux 还有用庞杂的配置文件,在新人以及专家手中 Linux 甚至不是同一个系统,当然这也是 Linux 自由的一个体现;

Unix 哲学

  • 每个程序做好一件事 - 简洁

  • 你无法判断程序会在什么地方耗时,瓶颈也经常出现在意想不到的地方,在没有证实时不要随意修改代码;在没有对代码进行估量,找到最耗时的地方前,不要去优化速度,速度与性能的优化要基于实际数据,优化是可量化的;

  • 清晰的设计胜于技巧的设计,设计应该透明可见以便于后期的调试;
  • 透明与简洁带来健壮性;
  • 策略与机制分离: MVC

fork

进程与线程

Unix 进程

初始化进程 - init 进程,执行设置内核,继续完成引导过程所需的剩余步骤,打开系统控制台,挂载根文件系统,运行 init 脚本;这一过程中, init 进程多次自行 fork, 创建运行系统所需的基本进程,init 进程成为系统所有其它进程的祖先.

线程通信依赖信号量,全局变量,以及锁等,而进程依赖 IPC;Linux 的线程实现:NPTL - 原生 POSIX线程库,内核创建与回收线程,线程与内核线程调度实体 1:1;

fork 赋予了进程生命,进程可以向细胞一样分裂扩展;

fork 后子进程继承父进程的资源,复制父进程内存页面, copy到自身所获取的操作系统赋予的内存中.此处的拷贝属于 COW 即为写时拷贝,子进程借此可以完成高效的进程创建以及父进程资源共享,只有到更改时才 copy 父进程的内存页面进而修改,在 copy 内存页面时子进程同时拷贝父进程页面表;

分页和物理内存管理

进程与线程比较: 进程相对而言更加独立通常即使一个进程崩溃也不会造成其他进程的影响,而线程则强调同步与资源共享,而为了资源共享的管理有序又需要引入同步管理机制,导致线程之间撕扯数据,一旦一个线程崩溃通常导致整个进程崩溃,当然也会导致其他线程崩溃;

对于 Linux 而言线程与进程在内核角度而言是基本等同管理的,同时进程特有的fork 引申的主从进程机制,主进程可以很容易的了解到子进程的运行状态,一旦工作子进程崩溃,主进程可以迅速感知并重启子进程,这些都是在系统层面的进程支持;

当然这些都是针对 Linux 系统而言,而对于Windows系统而言,则仿佛是为线程而生的,为工作线程的创建销毁保持了极大的弹性;

IPC

  • 信号: 信号是硬件中断机制在软件层次的模拟机制,信号是 进程 IPC 中唯一的异步通信机制,常用的有 kill 发送信号关闭进程, alarm 指定时间向进程本身发送信号;
  • 管道: 管道实际上是内存中的一个固定大小的缓冲区,管道两端一端读一端写,管道中的数据是一次性的,如同流水一样,一旦有人读取其他人就无法再次获取;同时管道还能被阻塞,写阻塞是指管道中已经写满,在数据被读取进而有空闲空间前管道无法再被写入,同理,读阻塞则是指管道内容全部被读取,在管道有内容写入前,管道一端的读操作将被阻塞;管道的操作为 pipe, 事实上 pipe 线程间通信使用较少,使用更加广泛的是 FIFO 命名管道;

Linux的进程间通信 - 管道

mmap

mmap 在 Android 中的 Binder 传输中大量使用;

mmap 是 Linux 进行虚拟内存管理的核心机制.Linux 内核将其整个内存地址空间看作一系列不同文件的映射;mmap 提供的机制允许用户将某个文件映射到程序地址的某个部分,使用简单的内存访问指令即可进行对该文件的读写;

mmap 操作虚拟内存地址,各个进程可能操作的虚拟地址不一样,但在内核中转后被映射到同一物理内存地址,可以节省copy, 提升系统访问效率;

CPU 提供虚拟内存的硬件特性,该硬件模块成为 MMU 即为内存管理单元,而操作系统完成对虚拟内存的管理;虚拟内存技术方案实现的核心在于地址空间隔离,意在在多任务操作系统中隔离地址空间,完成地址空间的保护;

mmap 的核心是虚拟内存转换为物理内存的过程,也就是物理地址伪装的过程;其主要使用为共享内存,进程间可以通过 mmap 实现对于同一个普通文件的共享内存;

mmap的调用,内核会在进程地址空间中建立新的虚拟地址区域,并回调 mmap 驱动函数时将虚拟地址区域传递给驱动函数;而在用户空间,要对文件进行 mmap ,需要先获取文件描述符,进而调用mmap做内存映射,将文件内容映射到虚拟内存中,完成文件内容的镜像处理,事实上是一次快速 IO;

epoll

epoll 在 Android 中使用参见 MessageQueue实现机制;

epoll 解决的问题: IO 复用;epoll 实际上是 poll 的扩展,而 poll 则是 slelect 的扩展;

select 实现对于 IO 的集合监控,调用者将所关注的 IO 句柄集合传递给 select 程序,同时设定所关注的 IO事件,随后 select 阻塞调用进程(或线程),当对应 IO 句柄有所关注的IO事件发生,select 标记对应的 IO 句柄,并返回,调用者在轮询 check IO事件时受到返回状态进行相应处理;

select 的限制在于句柄数不能超过 1024,这在当前的应用中远远不足,随后 poll 诞生, poll 对于句柄数理论上无限制,但又引出了C10k 问题,随后 epoll 诞生; – 看一个实现,先了解其为何诞生,解决什么问题,这样对于实现才有更加深刻的理解,或许这就是他人所说的 Android 的核心在 Android 之外;

简而言之, C10K 问题就是由于大量操作的消耗与当前连接数量成线性相关,因而在当前连接数较多时,单个任务的资源消耗与当前任务的关系也将成 O(n)关系,进而导致系统吞吐量与机器性能不匹配问题,也就是 机器性能为1的服务器最大连接数为1000,到了机器性能翻倍为2时,最大连接数却不能达到2000并发;这在服务端领域上以万计的连接后资源消耗上是非常恐怖的;

epoll 则摆脱了当前具体操作与当前连接数N之间的线性联系,从而提升了服务器处理能力;其实现机制:

一为维护长期的事件关注列表,进而避免了每次调用 select 时内核用于分析参数建立事件等待结构时的开销
二者避免 select 返回后应用程序扫描整个句柄表的开销,而改为直接返回整个事件列表;

epoll 使用 mmap 加速内核与用户空间的消息传递(mmap 减少不必要的数据拷贝, Binder 效率提升的关键)

epoll 的工作模式: 边缘触发 ET 以及电平触发 LT,对应到数电中的上升沿下降沿与高低电平,电平触发意味着如果关心的是事件没有被处理,后续在这一电平区间内将持续通知调用者,而 ET 触发则不一样,只在电平变换产生上升沿事件发生的那一刻通知调用者,一旦错过就需要等待下次;事件的发生

epoll 的使用 API:

  • epoll_create 返回 epoll 对象句柄
  • epoll_ctl 控制 API, 用于控制 epoll 句柄对象,用于添加修改删除所监控的 IO 句柄以及所关心的事件类型
  • epoll_wait 阻塞等待所监控的 IO 句柄有事件发生

VFS

并发

基于进程的服务器编程,进程实现的优劣性: 其优点在于独立性,实现模型清晰,共享文件表但不共享用户空间;但由于其独立性导致进程间开销相对较大

基于 IO 多路复用机制实现的并发编程,利用单进程逻辑状态的转换实现状态机; 典型如 select 以及 poll: 是并发事件驱动程序的实现基础,在事件驱动程序中,流因为某种事件的出现而前进.服务器事件的监控: 一为添加请求客户端,二为监听可读事件的产生;其优势在于一则 Context 上下文环境单一,流之间的数据共享非常便捷,此外程序行为的控制状态显著增加,可控性增强;但带来的问题也很明显: 逻辑过于复杂;

基于线程实现的并发编程: 线程是运行在进程中的逻辑流.每个线程有自身的线程上下文环境 Context, 独立的栈,栈指针,程序计数器,通用寄存器等,同一进程中的所有线程共享进程的整个虚拟地址空间,即多线程共享进程的单一上下文环境.线程的使用,如果不手动显示回收内存,则需要注意线程导致的内存泄漏问题,小心分离线程,使其在终止执行时其资源可被回收;

将指令逻辑拆分,构建进度图,利用进度图确定线程运行的安全性,互斥保证线程安全性,线程在执行其对于共享虚拟内存中的操作时,对其具有独立唯一性互斥的访问;

互斥的实现 - 信号量同步机制(Java 中 Monitor 监控机制实现同步);

互斥锁加锁判定规则:

如果对于程序中每对互斥锁给所有的锁分配全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放锁,即证明程序实现无死锁;


Quote :

<Unix&Linux 大学教程>

<深入理解计算机系统>
上一篇
下一篇
Loading Disqus comments...
Table of Contents