- 由Heroku创始人Adam Wiggins在2012年发布。
- 是一个开发服务器应用的方法论。开发运行一个服务器程序,应该考虑这些方面。
- 使用于任意语言和后端服务开发的应用程序。
- 最佳阅读人员:服务器开发和运维人员。
- 中心思想是隔离。
用到的docker
都是在ubuntu系统中
docker search时列出tag
使用这个脚本docker-show-repo-tag.sh
1 | #!/bin/sh |
docker精简镜像体积
- 命令尽量都写在一行,docker的命令会产生分层,也就是说,每执行一次命令,都会commit一次,然后继续下一次,这样每一次的执行都会记录下来。
- 找比较小的基础镜像,当然还是ubuntu最好啊哈哈哈。
librdkafka相关
iniparser-C 配置解析
https://github.com/ndevilla/iniparser
特点
- 相当小,4个文件(2个c,2个h),1500行左右
- 不依赖其他库
- 可重入,需要自己加锁实现线程安全
另外,使用也相当简单
数组的二分算法模块--bisect
python配置解析--ConfigParser
配置文件的格式
1 | [server] |
python连接mysql
安装MySQLdb
1 | pip install mysql-python |
创建连接
1 | def db_get_conn(**dbconfig): |
nginx图集
nginx的代码组织相当简洁明了。
命令_nc
shell IO重定向
命令_date
用到的tmux
用到的calibre
逻辑地址,线性地址,物理地址
。。。
首先,我们有1块物理内存,比如4G的DDR3,如果CPU想要找这个内存里边的一块区域存储的东西(0、1),需要知道这块区域在哪里,最简单的就是把物理内存的一个个存储单元都编号,比如位置在最开头的就编号为0x00,接下来一个单元就编号0x01,…,这里每个单元都是1位,也就是只能存1个0或1个1,就像这样:
但是这样每一位都进行编号,整个地址编号就太多了,如果我们约定每8位编一个号,那整个地址就是原先的1/8,就像这样:
为了方便,我们把每8位的一个单元叫做1个字节,0x00指向的就是物理内存中的第1个存储单元,也就是第1个字节,字节是物理内存的最小存储单元。如果想拿到第1个字节第3位中的数字1(标红的位置),那就需要把整个字节都先拿到,再去找里边的数字。
这里给物理内存编的1个号码就是1个物理地址,所有给物理内存编的号码就叫物理地址的地址空间。
CPU如果想拿到某个物理内存中的数字,需要发送一组电信号给物理内存芯片,芯片根据这组电信号查找相应地址,然后返回给CPU,这组电信号编码成数字就是物理地址了,就像这样:
其中CPU到内存控制芯片的这几根线叫地址线,比如常说的某个架构有20根地址线或者32根、36根地址线,就表明CPU可以寻址到的地址空间,比如32根地址线就可以最高寻址到2^32,也就是4G。我们这里画了4根,就可以寻址到2^4,也即是16个字节。
我们需要编写程序控制CPU进行逻辑处理、计算、存储等动作,当需要读写内存中的数据的时候,最简单的当然是用物理地址去指示CPU问内存控制芯片要哪个地址的数据了,比如:
我们想要把0x00中的数据放到寄存器ax中,就把0x00这个地址给CPU,这就可以了,多简单。当然如果真的这么简单,大家都轻松了🙃。事实上,CPU为了能处理多任务,同时在一个任务中区分代码、数据、栈,就需要对内存进行划分,分别存放不同的内容,这样一是为了区分,二是为了保护(比如代码段不允许修改,某些数据只读)。先来想象一下可能的解决办法,我们的目的是想把一段处理逻辑控制在内存0x00~0x07这个段之间,让CPU在运行的时候不能超出这个内存范围去获取代码或数据,超过就要报错。一个简单的方法是设置一个标志位,该标志位表明指令中的地址都需要在0x00~0x07之间,不能超过。在指示0x080x15之间的内存的时候,再设置另一个标志位,起到同样的作用。这样就可以将整个内存分为2个段:0x00\0x07,0x08~0x15。每个段的长度是8个字节。每个段都设置一个标志位的方法有点儿笨,我们用每个段的首地址来表示这个段:0x00表示第1个段,0x08表示第2个段,再用跟段的首地址的偏移量来确认段内某个字节的地址。现在我们确定第1个段中第2个字节的地址就是0x00:1(第1个字节的地址是0)。如下图:
现在我们指令中用到的0x00:1就是逻辑地址,因为它在逻辑上将内存划分为了多个不同的段。我们要做的只是输入0x00:1这个地址,CPU会帮我们把逻辑地址翻译成0x01的物理地址🙂。
除了使用分段的方式划分内存外,还可以将物理内存直接划分为多个不同的页,称为页帧,一个物理页一般是4K大小,这样控制一页内的内存属性相同,多个不同的页可以使用不同的属性,就跟分段类似了,如下图:
这时候会出现个新问题:CPU已经使用逻辑地址来划分段了,如何去实现分页呢?其实肯定也想到了,在逻辑地址和物理地址之间加一个抽象层接口,也就是我们要说的最后一个地址:线性地址,也叫虚拟地址。线性地址的位置在逻辑地址和物理地址之间,比如在讲linux内核的书里边常见有这样的图:
CPU将逻辑地址按照分段逻辑转换成线性地址,再按分页逻辑转换成物理地址。这里的转换都是依靠CPU电路完成的,但是怎样分段,怎样分页是需要内核程序先规定好才行。CPU怎样进行分段和分页的就不细说了,这里讲的真好,可以去看看。
我们来对比一下分段和分页:
分段 | 分页 | |
---|---|---|
目的 | 将内存按照不同的属性分隔,实现隔离和保护 | 将内存按照不同的属性分隔,实现隔离和保护 |
对象 | 对线性地址进行分隔 | 对物理页帧进行分隔 |
方法 | 使用逻辑地址(段基址:偏移)分隔不同的段 | 将线性地址分隔成不同的部分,一步步去寻找页对应的物理页帧,进而找到对应的物理地址 |
程序需要事先做好的准备 | 准备好GDT,LDT,IDT,也就是个个不同的段 | 准备好页目录,页表,也就是各个不同的页 |
大小 | 段大小可变,程序控制 | 页大小固定的几个值,4K,4M,2M,CPU规定好的 |
好了,现在我们的CPU中地址关系如下: | ||
看上去很复杂吧,其实理解了就没那么多东西,我画画的水平有限😅。 |
有个问题很多人都想知道,我一开始看内存这部分的时候也很着急:linux用户程序里边用的是什么地址?我先说答案:是逻辑地址。但很多书里边讲linux虚拟内存的分布,跟程序里用到的地址是一模一样的啊?其实linux用户程序中的地址是逻辑地址中的偏移,只不过linux把所有段的基址都设为0,这样段内偏移就跟线性地址一样了(因为段基址都是0啊,段偏移加上0还是段偏移)。为什么linux要把所有段基址都设为0呢?其实分段和分页都是对内存的分隔和保护,从用途上来说是重复的,分页更简单些,所有linux就只用了分页来实现内存保护。
还有另一个问题:linux中用户进程看到的内存都是一样的,0~4G,这是如何实现的?答案是基于分页,linux给每个进程都维护一个分页映射规则,每个分页规则对应一种线性地址到物理地址的映射,这样每个进程看到的都是同样的线性地址,但是后边对应的物理地址各不相同。
内存管理是linux内核中最复杂的系统之一,除了逻辑地址到线性地址再到物理地址的映射外,还有很多其他的特性辅助操作系统对物理内存实现管理,比如分段逻辑实现时用到的GDT、LDT、IDT、TSS、6个段寄存器,分页逻辑实现时用到的页目录、页表、页帧、CR3、PAE,CPU和物理内存之间的高速缓存等。骨架已经理清楚了,其他的理解就慢慢往里加吧。
参考:
- http://ilinuxkernel.com/?p=1276,讲linux内核的大牛,思路清晰,文笔优美。
- 《Linux内核设计与实现》Robert Love,极好的内核入门书。
- 《深入Linux内核架构》(德)Wolfgang.Mauerer,深入细节1。
- 《Understanding the Linux Kernel 》3rd_Edition,Daniel P. Bovet and Marco Cesati,深入细节2。
《题龙阳县青草湖》 ——唐温如
西风吹老洞庭波,
一夜湘君白发多。
醉后不知天在水,
满船清梦压星河。
IA32常用寄存器
看内核之前需要了解一些体系结构相关的知识,本文介绍IA32常用的几个寄存器。
- 通用寄存器:EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP
- 段寄存器:CS,DS,ES,FS,GS,SS
- EFLAGES寄存器
- EIP寄存器
- 控制寄存器:CR0,CR1,CR2,CR3,CR4
- GDTR,LDTR,IDTR寄存器
- TR寄存器
1. 通用寄存器
用于存放:
1. 算数和逻辑操作的操作数
2. 地址操作的操作数
3. 内存指针
这些寄存器通常可以存放任何东西,但有时候会用作特定用途,比如:
EAX——存放累加操作数和结果,传递参数和结果等
EBX——指向DS数据段中数据的指针
ECX——字符串和循环操作的计数器
EDX——I/O指针
ESI——指向DS数据段中数据的指针;字符串操作的源指针
EDI——跟ES配合,指向目的数据的指针;字符串操作的目的指针
ESP——栈指针(在SS段中)
EBP——指向栈中的数据(SS段中),一般用作函数嵌套调用时的栈帧基址
2. 段寄存器
当用平坦内存模型(flat memory model)的时候,段寄存器都指向0,不分段。
当用段内存模型(segmented memory model)的时候,段寄存器指向各自的段,且有CPU的保护机制。
保护模式下(段内存模型):
CS:保存当前指向代码段的段选择子,配合EIP,可以找到下一个要执行的代码。
DS,ES,FS,GS:保存四个数据段的段选择子,可以让当前任务同时获取四个数据段,比如一个指向当前特权级的数据段,另一个指向更高特权级的数据段,第三个指向动态创建的数据结构,第四个指向跟另一个程序共享的数据段。
SS:保存当前栈的段选择子。
段寄存器中保存着段选择子,根据该选择子可以选择GDTR(或LDTR)指向的GDT(或LDT)表中的某一项(段描述符),然后根据该项可以找到相应的段(选择的时候CPU可以进行权限检查等)。如果CPU每次都从段寄存器拿到段选择子,再根据GDTR找到相应的项,再找到段就太麻烦了,所以这几个段寄存器都有个隐藏的寄存器,不暴露给开发人员,只是CPU内部使用的,用于缓存段地址,加快处理速度。如下图:
3. EFLAGS
存储CPU的一些内部状态。
4. EIP寄存器
用于存放下一个要执行的代码的地址,非常重要,通过特定的指令(JMP,Jcc,CALL,RET,IRET)可以改变值,不能被直接读取,只能在call调用的时候从栈中读取,可以直接赋值,然后用RET或IRET跳转(内核用于向高特权级翻转)。
5. 控制寄存器
CR0——包含控制处理器操作模式和状态的标志
CR1——保留
CR2——包含缺页中断时的线性地址
CR3——包含分页的第一层结构的基址和两个标志位(PCD,PWT)
CR4——包含一些架构扩展,指定特定的处理器兼容。
一些位解释如下:
PG: Paging,是否分页
CD:Cache Disable,是否使用物理缓存
PE:Protection Enable,打开保护模式,该标志位不会启动分页,只会启动基于段的保护(分页和段都是保护,其实是重复的,linux只用了分页保护)。
6. GDTR,LDTR,IDTR
存放GDT,LDT,IDT的基址。
GDT:Global Descriptor Table,全局描述符表,存放全局的段描述符的数组,只有1份,通过该表可以找到所有的段,也只有通过该表才能实现处理器基于段的保护。
LDT:Local Descriptor Table,局部描述符表,存放局部的段描述符的数组,可以有多份,当前的LDT基址存放在LDTR中。另外,每一份LDT都要在GDT中有一个描述符(说明一个LDT本身也是内存中的一个段)。通过LDT可以获取当前任务的相关段。
IDT:Interrupt Descriptor Table,中断描述符,存放处理器的各个中断的地方。
7. TR寄存器
存放当前TSS的基址。
CPU处理多任务的时候,需要在各个任务之间切换,保存当前任务状态,加载下一个任务的状态,然后执行下一个任务,状态保存在多个地方,其中栈的相关信息保存在TSS中。
参考:
- 《Intel® 64 and IA-32 Architectures Software Developer’s Manual
Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D and 4》 - 《Understanding the Linux Kernel 3rd Edition》
《Orange's:一个操作系统的实现》
真的是挺好的一本书,从最基础的boot一直讲到进程调度、文件系统、内存管理,最最重要的是每一步都能自己动手实践,不止理解了概念,还对过程有了自己的感性认识,这是别的操作系统书里很难有的。
经过这些,对计算机如何工作理解的更深刻了。CPU从硬盘中将文件读入到内存,数据和代码都存在在内存,因为涉及到保护之类的,这些内容按段分布,代码控制CPU的逻辑流向。中断让CPU能更及时且准确的处理外来任务。
学习操作系统最大的感受,一个是CPU如何跟各个外设进行交互,另一个就是事无巨细都要一点点去实现,要对整个系统完全掌控才心里有底。
目前我觉得学习操作系统的顺序应该是了解其中的几个基本概念:文件系统、内存管理、进程调度、中断机制,然后开始从boot了解,一步步串起来,才会在整个过程中不那么艰难,看完其实操作系统也就那么回事儿了。
这是对里边涉及到的一些概念的印象,还有待充实。
- IA32 CPU的数据总体结构:
- GDT的作用是用来提供段式存储机制,这种机制是通过段寄存器和GDT中的描述符共同提供的。
- 描述符的种类:
- 代码段描述符
- 数据段描述符
- 系统段描述符
- 门描述符
- IA32的分段机制中,特权级共有4个级别,从高到低分别是0,1,2,3。数字越小表示的特权级越大
- 处理器通过识别CPL,DPL,RPL这3中特权级进行特权级检验
CPL: current privilege level,当前执行的程序或任务的特权级。存储在cs和ss的第0位和第1位上。通常,CPL等于代码所在的段的特权级,当程序转移到不同特权级的代码段时,处理器将改变CPL。一致代码段可以被相同或者更低特权级的代码访问。当处理器访问一个与CPL特权级不同的一致代码段时,CPL不会被改变。
RPL: requested privilege level.存储在段选择子的第0和第1位。处理器通过检查RPL和CPL来确认一个访问请求是否合法。RPL占主导。
DPL: descriptor privilege level,表示段或者门的特权级。存储在段或门描述符的DPL字段。当当前代码段试图访问一个段或门时,DPL将会和CPL以及段或门选择子的RPL相比较,根据段或门类型的不同,DPL将会被区别对待:
- 数据段:DPL规定了可以访问此段的最低特权级。
- 非一致代码段(不使用调用门的情况下):DPL规定访问此段的特权级。比如,一个非一致代码段的特权级为0,那么只有CPL为0的程序才能访问它。
- 调用门:DPL规定了当前执行的程序或者任务可以访问此调用门的最低特权级。
- 一致代码段和通过调用门访问的非一致代码段:DPL规定了访问此段的最高特权级。比如一个一致代码段的DPL是2,那么CPL为0和1的程序将无法访问此段。
- TSS: DPL规定了可以访问此TSS的最低特权级。
- 程序控制转移的发生:
jmp, call, ret, sysenter, sysexit, int n, iret
,或者中断、异常 - jmp、call指令可以实现4种转移:
- 目标操作数包含目标代码段的段选择子
- 目标操作数指向一个包含目标代码段选择子的调用门描述符
- 目标操作数指向一个包含目标代码段选择子的TSS
- 目标操作数指向一个任务门,这个任务门指向一个包含目标代码段选择子的TSS
- 门描述符种类
- 调用门 (call gates)
- 中断门(interrupt gates)
- 陷阱门(trap gates)
- 任务门(task gates)
- 每个任务最多可能在4个特权级间转移,所以每个任务实际上需要4个堆栈,这时候需要TSS(task-state stack)来存储。
- 8259A芯片用于处理外部中断,可设置优先级,可屏蔽
- 一致代码段是给低特权级执行高特权级代码提供的一条通道,没有这条通道,从低到高或者反过来都不能执行
- 数据段都是非一致的,但是高的可以无条件访问低的数据段
- 系统一开始进入的是0,也就是最高特权级,往特权级低的方向移动使用ret,往高处移动使用call,一个任务最多会用到4个特权级,而TSS只是在低特权级到高特权级移动的时候才会用到,所以TSS只会保存前三个。
- 分页是CPU提供的功能,使用两个东西,PDE和PTE,一个PDE和相应的PTE组成一套对应关系,说明一种线性地址和物理地址的对应关系。逻辑地址到线性地址用到的是分段机制。
线性地址提供的相当于一个胶水层,将上层应用(程序使用的逻辑地址)跟下层物理实现(CPU提供的物理地址)分割开来
因为有多套不同的(PDE,PTE),所以形同的线性地址可能就对应不同的物理地址,这也就是常说的操作系统提供的统一内存空间。 - IDT的作用是将每一个中断向量和描述符对应起来。
- C调用约定:后面的参数先入栈,调用者负责清理堆栈。在处理可变参数时C调用约定表现良好,因为只有调用者知道此次调用包含了几个参数,于是可以方便清理堆栈
- 用到的芯片
- 8259A:处理中断
- 8253:PIT(programmable interval timer),可编程定时器
- 敲击键盘有两方面的含义:动作和内容。动作:按下、保持按住的状态、放开;内容:字母键、数字键、回车和其他键。
- tty设备跟普通文件的不同:1.怎样才算“输入结束”,是每次键盘敲击之后都算结束,还是等回车才算结束,或者其他;2. 是否要让文件系统等待输入过程结束。
于渊
《linux内核设计的艺术:图解linux操作系统架构设计与实现原理》(第二版)
要知道,写一本好书并不是说说就那么简单的,思路清晰、知道重点、保持严谨、不厌其烦,应该是写文章做报告写书的人都应该具备的特点,我虽然没写过,但也知道这本书很好的起到了反面例子。但即使没那么好的书,也比某些单独的文章要好得多。
这本书不像Orange’s是个玩具操作系统,而是实实在在讲的linux-0.11——可以实际运行、实际使用的内核。从加电引导,到保护模式启动,内核建立中断,到第0、1、2个进程建立,再到文件系统、内存管理、进程调度,跟着源码一步步将系统带入到“怠速”状态,很神奇的过程。从这里边会明白linux进程调度用到的switch_to(n)
是怎么加载TSS,怎么从一个进程转移到另一个进程;会知道系统调用的代码执行路线;内核中的文件系统是怎么组织的,以前想不明白的dup()/dup2()
函数是怎么个过程,看了源码立马就清晰了;pipe()
原来是个内存页,怪不得最大长度是4096字节(页大小);内存的段页管理机制到底是怎样的。源码之前,了无秘密
。
加电启动过程
1
2
3
41. BIOS:处理器加电启动时会执行0xFFFF0处的代码,也就是BIOS所在区域(直接写在ROM中,不需要加载)。BIOS会加载bootsect进入内存0x007c0位置。
2. bootsect: 规划内核内存使用,将自己复制到合适的位置 (因为占用了0x007c0的位置,这部分是要给后边的程序用的),加载setup、system代码进入内存;跳到seup
3. setup: 利用BIOS的中断服务程序从设备提取内核运行所需的机器系统数据,覆盖掉bootsect程序,关中断;将位于0x10000的内核程序复制到内存地址0x00000处(这里原来放着BIOS的中断向量表和BIOS数据区,在bootsect运行的时候还需要用到中断,所以bootsect一开始不能直接把内核载入到0x00000处),设置中断描述符表和全局描述符表;打开A20,实现32位寻址;建立保护模式下的中断机制,对8259A进行重编程;跳到head
4. head:跟kernel编译在一起,执行本段代码的时候,会覆盖掉本段已经用过的代码(自己吃自己);让所有中断描述符指向ignore_int忽略中断;废除已有的GDT,在新位置重建GDT(原来的GDT是setup建立的,现在setup没用了,这段内存会被覆盖,其中的GDT也会被覆盖。);检验A20是否真正打开;检验数学协处理器;将L6和main地址压栈;建立内核的分页机制;打开分页;调用ret执行main进程要想与块设备进行沟通,必须经过主机内存中的缓冲区。
对物理内存的规划
除了内核代码和数据所占的内存外,其余物理内存主要分为3部分:
1) 主内存区:进程代码运行的空间,也包括内核管理进程的数据结构
2) 缓冲区:主机与外设进行数据交互的中转站
3) 虚拟盘:可选区域,如果选择使用虚拟盘,就可以将外设上的数据先复制到虚拟盘区,然后加以使用,这样可以提高系统执行效率。进程切换,用的是switch_to(n),切换之前会保存当前进程的所有状态,包括下一条指令,再次切换回来后会从先前状态继续执行,沿着自己的函数调用链依次进行或返回。
逻辑地址的形式是CS:DS。实模式下,CS中存放的是段基址,跟DS组成线性地址,线性地址直接映射到物理地址(也就是通过地址线的地址);保护模式下,CS中存放的是段选择子,段选择子会在段描述符表(GDT)中找到相应的段描述符,段描述符中存放的有段基址、段属性、段权限(用于保护)等信息,段描述符中的段基址跟DS组成线性地址,这里如果没有开启分页(只有保护模式才有分页机制),线性地址就直接映射到物理地址;如果开启了分页,线性地址会分成3部分,分别指示处理器找到页目录表、页表、页(页帧),再用线性地址的最后一部分找到页中的某个实际的物理位置。这么麻烦的使用段描述符,是为了保护;这么麻烦的使用分页,一是为了保护,二是为了实现虚拟内存。
新设计团队