3.4 虚拟内存
直接使⽤物理内存会产⽣⼀些问题
1. 内存空间利⽤率的问题:各个进程对内存的使⽤会导致内存碎⽚化,当要⽤ malloc 分配⼀块很⼤的内存空间时,可能会出现虽然有⾜够多的空闲物理内存,却没有⾜够⼤的连续空闲内存这种情况
2. 读写内存的安全性问题:物理内存本身是不限制访问的,任何地址都可以读写
3. 进程间的安全问题:各个进程之间没有独⽴的地址空间,⼀个进程由于执⾏错误指令或是恶意代码都可以直接修改其它进程的数据,甚⾄修改内核地址空间的数据,这是操作系统所不愿看到的
4. 内存读写的效率问题:当多个进程同时运⾏,需要分配给进程的内存总和⼤于实际可⽤的物理内存时,需要将其他程序暂时拷⻉到硬盘当中,然后将新的程序装⼊内存运⾏。由于⼤量的数据频繁装⼊装出,内存的使⽤效率会⾮常低虚拟内存 虚拟内存是计算机系统内存管理的⼀种技术。它使得应⽤程序认为它拥有连续可⽤的内存,⽽实际上,它通常是被分隔成多个物理内存碎⽚,还有部分暂时存储在外部磁盘存储器上,在需要时进⾏数据交换。 [1] 虚拟内存有什么作⽤?
● 解决了多进程之间地址冲突的问题。由于每个进程都有⾃⼰的⻚表,进程也没有办法访问其他进程的⻚表,所以每个进程的虚拟内存空间就是相互独⽴的。
● 在内存访问⽅⾯,操作系统提供了更好的安全性。⻚表⾥的⻚表项中除了物理地址之外,还有⼀些标记属性,⽐如控制⼀个⻚的读写权限,标记该⻚是否存在等。
虚拟地址空间
对于⼀个单⼀进程的概念,这个进程看到的将是地址从0开始的整个内存空间。虚拟存储器是⼀个抽象概念,它为每⼀个进程提供了⼀个假象,好像每个进程都在独占主存,每个进程看到的存储器是⼀致的,称为虚拟地址空间。在 Linux 操作系统中,虚拟地址空间的内部⼜被分为内核空间和⽤户空间两部分,不同位数的系统,地址空间的范围也不同。⽐如最常⻅的 32 位和 64 位系统,如下所示。虽然每个进程都各⾃有独⽴的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很⽅便地访问内核空间内存。
⽤户空间内存,从低到⾼分别是 6 种不同的内存段:
● 程序⽂件段(.text),包括⼆进制可执⾏代码;
● 已初始化数据段(.data),包括静态常量;
● 未初始化数据段(.bss),包括未初始化的静态变量;
● 堆段,包括动态分配的内存,从低地址开始向上增⻓;
● ⽂件映射段,包括动态库、共享内存等,从低地址开始向上增⻓;
● 栈段,包括局部变量和函数调⽤的上下⽂等。栈的⼤⼩是固定的,⼀般是 8 MB。当然系统也提供了参数,以便我们⾃定义⼤⼩;
虚拟内存实现思路
1. 在系统中为每个程序定义⼀个虚拟地址空间,虚拟地址空间中的地址都是连续的
2. 虚拟地址空间被分割成多个块,每块称为⼀个⻚或者⻚⾯
3. 物理内存被分成和⻚⾯⼤⼩相同的多个区域,称作⻚框
4. 程序加载时,可将任意⼀个⻚⾯放⼊内存中的任意⼀个⻚框
5. CPU 的硬件负责将虚拟地址映射到物理内存中的地址(⻚⾯ -> ⻚框)
6. 程序的整个地址空间⽆需全部载⼊物理内存,还有部分暂时存储在外存上,需要时再换⼊内存
7. 如果程序引⽤到⼀部分不在虚拟地址时,会发⽣缺⻚中断,由操作系统负责将缺失的⻚⾯加载⼊⻚框中,并重新执⾏失败的指令
内存管理单元(MMU):将CPU中 负责地址转换 的部分统称为内存管理单元。利⽤地址转换,操作系统就可以控制进程的所有内存访问,确保访问在地址空间的界限内。这个技术的关键是硬件⽀持,硬件可以快速地将内存访问操作中的虚拟地址转换为物理地址。对于⼀个⻚式内存地址转换,有三个步骤:
● 把虚拟内存地址,切分成⻚号和偏移量;
● 根据⻚号,从⻚表⾥⾯,查询对应的物理⻚号;
● 直接拿物理⻚号,加上前⾯的偏移量,就得到了物理内存地址。
TLB 的原理
现代操作系统中,⻚表的个数是很多的,⽽每次执⾏语句时都需要先查找⻚表,将虚拟地址转换为物理内存地址。这部分切换的时间开销是很⼤的。因此,解决⽅案是为计算机设置⼀个转换检测缓冲区(TLB,translation-lookaside buffer),是 MMU(内存管理单元)的⼀部分。它提供⼀个缓冲区, 89 记录虚拟⻚⾯号到物理⻚框号的映射,这样可以在 O(1) 的时间⾥直接将虚拟⻚⾯映射到物理⻚框,不需要访问⻚表,从⽽节省时间。
⼯作流程:如果⻚⾯号在 TLB 中,得到⻚框号,访问内存;否则,从内存中的⻚表中得到⻚框号,将其存⼊ TLB,访问内存。
内存管理⽅法
段式内存管理:按照逻辑意义将程序分成若⼲个段,每个段独⽴载⼊到内存的不同区间中,通过段表对物理地址进⾏映射
优点:按照逻辑关系划分,能产⽣连续的内存空间。
缺点:每个段必须连续、全部加载到内存中,会产⽣内存碎⽚;连续空间不⾜,与硬盘交互导致内存交换的效率低的问题。
⻚式内存管理:⻚式管理把内存空间按⻚的⼤⼩划分成若⼲⻚⾯,然后把⻚式虚拟地址与内存地址建⽴ ⼀⼀对应的⻚表。⽽当进程访问的虚拟地址在⻚表中查不到时,系统会产⽣⼀个缺⻚异常,进⼊系统内核空间分配物理内存、更新进程⻚表,最后再返回⽤户空间,恢复进程的运⾏。
优点:不存在外部碎⽚,只在每个进程的最后⼀个⻚中存在内部碎⽚
缺点:程序全部装⼊内存,要求有相应的硬件⽀持。例如地址转换模块缺⻚中断的产⽣和选择淘汰⻚⾯等都要求有相应的硬件⽀持。这增加了机器成本和系统开销。
段⻚式内存管理:把分段和分⻚两种⽅式结合,先把程序按照逻辑意义分成段,然后每个段再分成固定⼤⼩的⻚。这样,地址结构就由段号、段内⻚号和⻚内位移三部分组成。Linux 系统主要采⽤了分⻚管理,但是由于 Intel 处理器的发展史,Linux 系统⽆法避免分段管理。于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被⽤于访问控制和内存保护。
⻚⾯放置算法
空闲链表与内存池:https://zhuanlan.zhihu.com/p/73468738
1. 最佳适应算法:检查所有空闲分区,选择和新进程申请内存⼤⼩最接近的空闲分区
优点:该算法保留⼤的空闲区
缺点:检查所有空闲分区需要时间。外部碎⽚多:会留下许多难以利⽤的,很⼩的空闲分区,称为外部碎⽚。可以采⽤内存紧凑的⽅法,将被使⽤的分区都移动到⼀起,减少外部碎⽚。但是移动内存中的代码和数据也需要很多时间
2. 最差适应算法:每次为进程分配分区时,都选择最⼤的空闲分区分配。最差适应算法使链表中的结点⼤⼩趋于均匀,适⽤于请求分配的内存⼤⼩范围较窄的系统
优点:该算法保留⼩的空闲区,尽量减少外部碎⽚的产⽣
缺点:检查⽐较所有的空闲区间需要时间;系统中不会存在⾯积很⼤的空闲区间,难满⾜⼤进程的要求
3. ⾸次适应法:只要发现能⽤的分区就分配。这种⽅法⽬的在于减少查找时间。为适应这种算法,空闲分区表(空闲区链)中的空闲分区要按地址由低到⾼进⾏排序。该算法优先使⽤低址部分空闲
区,在低址空间造成许多⼩的空闲区,在⾼地址空间保留⼤的空闲区
优点:可以剩下⼤的分区
缺点:外部碎⽚多,集中在低址部分;并且每次查找都是从低址部分开始的,这⽆疑⼜会增加查找可⽤空闲分区时的开销
4. 下次适应法:操作系统记住接下来该检查的空闲分区的位置,给进程分配分区时,系统从记录的分区开始依次向后查找直到碰到能⽤的分区为⽌,如果到链表尾还没有找到,就再从头开始
缺点:很难剩下⾯积很⼤的区间,会使剩余分区的⼤⼩⽐较平均。
⻚⾯置换算法
当出现缺⻚异常需调⼊新⻚⾯⽽内存已满时,选择被置换的物理⻚⾯,也就是说选择⼀个物理⻚⾯换出到磁盘,然后把需要访问的⻚⾯换⼊到物理⻚。
1. 最佳⻚⾯置换算法:置换在「未来」最⻓时间不访问的⻚⾯。最佳⻚⾯置换算法作⽤是为了衡量你的算法的效率,你的算法效率越接近该算法的效率,那么说明你的算法是⾼效的。
2. 先进先出置换算法(FIFO):直接换出最早装⼊的⻚⾯。
优点:简单
缺点:性能不是很好,因为它淘汰的可能是常⽤的⻚⾯
适⽤场景:数据只⽤⼀次,将来不太可能使⽤;
3. 时钟置换法(Clock):将⻚⾯保存在环形链表中,只需要后移队头指针。如果该位为 0,淘汰该⻚;如果该位为 1,将该位设为 0
优点:避免了移动链表节点的开销
4. 最近最少使⽤法(LRU:Least Recently Used):优先淘汰最久未被访问的⻚⾯。根据局部性原理,⼀个进程在⼀段时间内要访问的指令和数据都集中在⼀起。如果⼀个⻚⾯很久没有被访问,那么将来被访问的可能性也⽐较⼩。
优点:实验证明 LRU 的性能较好,能够降低置换频率
缺点:存在缓存污染问题,即由于偶发性或周期性的冷数据批量查询,热点数据被挤出去,导致缓存命中率下降
适⽤场景:访问分布未知的情况
5. 最少频率使⽤法(LFU:Least Frequently Used):优先淘汰最近访问频率最少的数据。
优点:能够避免缓存污染问题对 LRU 的命中影响
缺点:存在访问模式问题,即如果访问内容发⽣较⼤变化,LFU 需要⽤更⻓的时间来适应,导致缓存命中率下降;维护相关数据结构的开销⼤
6. 随机淘汰法(Random):实现简单,不需要保留有关访问历史记录的任何信息
适⽤场景:如果应⽤对于缓存的访问概率相等,则可以使⽤这个算法。
磁盘调度算法
为了提⾼磁盘的访问性能,⼀般是通过优化磁盘的访问请求顺序来做到的。寻道的时间是磁盘访问最 耗时的部分,如果请求顺序优化得当,必然可以节省⼀些不必要的寻道时间,从⽽提⾼磁盘的访问性能。
1. 先来先服务
2. 最短寻道时间优先(Shortest Seek First,SSF)算法:优先选择从当前磁头位置所需寻道时间最短的请求。但这个算法可能存在某些请求的饥饿
3. 扫描算法:为了防⽌上述饥饿问题,可以规定:磁头在⼀个⽅向上移动,访问所有未完成的请求,直到磁头到达该⽅向上的最后的磁道,才调换⽅向,这就是扫描(Scan)算法。
4. 循环扫描(Circular Scan, CSCAN ):只有磁头朝某个特定⽅向移动时,才处理磁道访问请求,⽽返回时直接快速移动⾄最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且返回中途不处理任何请求,该算法的特点,就是磁道只响应⼀个⽅向上的请求。
5. LOOK 与 C-LOOK算法:扫描算法和循环扫描算法,都是磁头移动到磁盘「最始端或最末端」才开始调换⽅向。优化的思路就是磁头在移动到「最远的请求」位置,然后⽴即反向移动。
内存不⾜会发⽣什么
主要有两类内存可以被回收,⽽且它们的回收⽅式也不同。⽂件⻚和匿名⻚的回收都是基于 LRU 算法,也就是优先回收不常访问的内存。
● ⽂件⻚:内核缓存的磁盘数据(Buffer)和内核缓存的⽂件数据(Cache)都叫作⽂件⻚。⼤部分⽂件⻚,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。⽽那些被应⽤程序修改过,并且暂时还没写⼊磁盘的数据(也就是脏⻚),就得先写⼊磁盘,然后才能进⾏内存释放。所以,回收⼲净⻚的⽅式是直接释放内存,回收脏⻚的⽅式是先写回磁盘后再释放内存。
● 匿名⻚:这部分内存没有实际载体,不像⽂件缓存有硬盘⽂件这样⼀个载体,⽐如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的⽅式是通过 Linux 的
Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存。再次访问这些内存 时,重新从磁盘读⼊内存就可以了。
在 4GB 物理内存的机器上,申请 8G 内存会怎么样?
这个问题要考虑三个前置条件:
● 操作系统是 32 位的,还是 64 位的?因为 32 位操作系统,进程最多只能申请 3 GB ⼤⼩的虚拟
内存空间,所以进程申请 8GB 内存的话,在申请虚拟内存阶段就会失败
● 申请完 8G 内存后会不会被使⽤?程序申请的是虚拟内存,如果没有被使⽤,它是不会占⽤物理空间的。当访问这块虚拟内存后,操作系统才会进⾏物理内存分配。
● 操作系统有没有使⽤ Swap 机制?如果没有开启 Swap 机制,程序就会直接 OOM(内存溢出简称OOM,是指应⽤系统中存在⽆法回收的内存或使⽤的内存过多,最终使得程序运⾏要⽤到的内存⼤于能提供的最⼤内存);如果有开启 Swap 机制,程序可以正常运⾏。
3.5 缓存区
缓存区溢出
C 语⾔使⽤运⾏时栈来存储过程信息。每个函数的信息存储在⼀个栈帧中,包括寄存器、局部变量、参数、返回地址等。C 对于数组引⽤不进⾏任何边界检查,因此对越界的数组元素的写操作会破坏存储在栈中的状态信息,这种现象称为缓冲区溢出。缓冲区溢出会破坏程序运⾏,也可以被⽤来进⾏攻击计算机,如使⽤⼀个指向攻击代码的指针覆盖返回地址。
防范缓冲区溢出攻击的机制:随机化、栈保护和限制可执⾏代码区域。
随机化
使⽤缓冲区溢出进⾏攻击,需要知道攻击代码的地址。因此常⻅的防范⽅法有:
1. 栈随机化:程序开始时在栈上分配⼀段随机⼤⼩的空间
2. 地址空间布局随机化(Address-Space Layout Randomization,ASLR):每次运⾏时程序的不
同部分,包括代码段、数据段、栈、堆等都会被加载到内存空间的不同区域
但是攻击者依然可以使⽤蛮⼒克服随机化,这种⽅式称为“空操作雪橇(nop sled)”,即在实际的攻击代码前插⼊很⻓的⼀段 nop 指令序列,执⾏这条指令只会移动到下⼀条指令。因此只要攻击者能够猜中这段序列的某个地址,程序就会最终经过这段序列,到达攻击代码。因此栈随机化和 ASLR 只能增加攻击⼀个系统的难度,但不能完全保证安全。
栈保护
在发⽣缓冲区溢出、造成任何有害结果之前,尝试检测到它。即在每个函数的栈帧的局部变量和栈状态之间存储⼀个随机产⽣的特殊的值,称为⾦丝雀值(canary)。在恢复寄存器状态和函数返回之前,程序检测这个⾦丝雀值是否被改变了,如果是,那么程序异常终⽌。
限制可执⾏代码区域
内存⻚的访问形式有三种:可读、可写、可执⾏。只有编译器产⽣的那部分代码所处的内存才是可执⾏的,其他⻚应当限制为只允许读和写。
3.6 I/O 模型
为什么 Redis 单线程模型依然效率很⾼:多线程技术是为了充分利⽤ CPU 的计算资源,适⽤于下层存储慢速的场景。⽽ redis 是纯内存操作,读写速度⾮常快。所有的操作都会在内存中完成,不涉及任何I/O 操作,因此多线程频繁的上下⽂切换反⽽是⼀种负优化。Redis 选择基于⾮阻塞 I/O 的 I/O 多路复⽤机制,在单线程⾥并发处理客户端的多个连接,减少多线程带来的系统开销,同时也有更好的可维护性,⽅便开发和调试。
I/O 模型类型
I/O 同步和异步的区别在于:将数据从内核复制到⽤户空间时,⽤户进程是否会阻塞
I/O 阻塞和⾮阻塞的区别在于:进程发起系统调⽤后,是会被挂起直到收到数据后再返回、还是⽴即返 回成功或失败
I/O 模型类别:阻塞IO、⾮阻塞IO、IO复⽤模型、信号驱动IO模型、异步IO
⽔平触发(LT,Level Trigger):当⽂件描述符就绪时,会触发通知,如果⽤户程序没有⼀次性把数据读/写完,下次还会发出通知。select 只⽀持⽔平触发。
边缘触发(ET,Edge Trigger):仅当描述符从未就绪变为就绪时,通知⼀次,之后不会再通知。
epoll ⽀持⽔平触发和边缘触发。当循环读取 recv 到没有数据时会出现 eagain 现象(LT 是多线程时可能会出现),默认跳过处理。
针对⽹络IO的操作,可以分成两个阶段:
● 准备阶段(阻塞):等待数据是否可⽤,是在内核进程中完成的;
● 操作阶段(同步):执⾏实际的IO调⽤,数据从内核缓冲区拷⻉到⽤户进程缓冲区。
阻塞IO:应⽤进程调⽤I/O操作时阻塞,只有等待要操作的数据准备好,并复制到应⽤进程的缓冲区中才返回;
⾮阻塞IO:应⽤进程调⽤I/O操作导致该进程进⼊阻塞状态时,该I/O调⽤返回⼀个错误。⼀般情况下,应⽤进程需要利⽤轮询的⽅式来检测某个操作是否就绪。数据就绪后,会等待数据复制到应⽤进程的缓冲区中以后才返回;
IO复⽤:多路IO共⽤同⼀个同步阻塞接⼝,此时阻塞发⽣在 select/poll 的系统调⽤上,⽽不是阻塞在实际的I/O系统调⽤上。IO多路复⽤的⾼级之处在于,它能同时等待多个⽂件描述符,⽽这些⽂件描述符其中的任意⼀个进⼊读就绪状态,select等函数就可以返回。
信号驱动IO:注册⼀个IO信号事件,在数据可操作时通过信号通知线程。
异步IO: 应⽤进程通知内核开始⼀个异步I/O操作,并让内核在整个操作(包含将数据从内核复制到应⽤进程的缓冲区)完成后通知应⽤进程。
I/O 复⽤模型
select 缺点:
1. 性能开销⼤:调⽤ select 时会陷⼊内核,这时需要将参数中的 fd_set 从⽤户空间拷⻉到内核空
间。内核需要遍历传递进来的 fd_set 的每⼀位,不管它们是否就绪
2. 能够监听的⽂件描述符数量太少:受限于 sizeof(fd_set) 的⼤⼩,在编译内核时就确定了且⽆法更改,⼀般是 1024。 poll 和 select ⼏乎没有区别。poll 在⽤户态通过数组⽅式传递⽂件描述符,在内核会转为链表⽅式存 储,没有最⼤数量的限制
epoll ,select、poll 模型都只使⽤⼀个函数,⽽ epoll 模型使⽤三个函数:epoll_create、epoll_ctl(监听事件) 和 epoll_wait(相当于select,返回就绪数量)。
特点:
1. 使⽤红⿊树存储⽂件描述符集合
2. 使⽤队列存储就绪的⽂件描述符
3. 每个⽂件描述符只需在添加时传⼊⼀次,之后调⽤ epoll_wait 不需要再次传递,提⾼了效率。
4. 通过事件更改⽂件描述符状态。epoll_ctl 中为每个⽂件描述符指定了回调函数,并在就绪时将其加⼊到就绪列表,因此只需要判断就绪列表是否为空即可。
三者区别:
1. select、poll 都是在⽤户态维护⽂件描述符集合,因此每次需要将完整集合传给内核;epoll 由操作系统在内核中维护⽂件描述符集合,因此只需要在创建的时候传⼊⽂件描述符。
2. 当连接数较多并且有很多的不活跃连接时,epoll 的效率⾼很多;当连接数较少并且都⼗分活跃的情况下,由于 epoll 需要很多回调,因此性能可能低于其它两者。
3. epoll 不⽀持跨平台,仅在 linux 上使⽤,⽽ select 可以在windows 和 linux 同时使⽤。
Reactor 与 Proactor 模式
Reactor 是 ⾮阻塞 同步 ⽹络模式,感知的是 就绪可读写 事件。在每次感知到有IO事件发⽣(⽐如可读就绪事件)后,就需要应⽤进程主动调⽤ read ⽅法来完成数据的读取,也就是要应⽤进程主动将socket 接收缓存中的数据读到应⽤进程内存中,这个过程是同步的,读取完数据后应⽤进程才能处理数据。
Proactor 是 异步 ⽹络模式, 感知的是 已完成的读写 事件。在发起异步读写请求时,需要传⼊数据缓 冲区的地址(⽤来存放结果数据)等信息,这样系统内核才可以⾃动帮我们把数据的读写⼯作完成,这⾥的读写⼯作全程由操作系统来做,并不需要像 Reactor 那样还需要应⽤进程主动发起 read/write 来读写数据,操作系统完成读写⼯作后,就会通知应⽤进程直接处理数据。因此,Reactor 可以理解为「来了事件操作系统通知应⽤进程,让应⽤进程来处理」,⽽ Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应⽤进程」。这⾥的「事件」就是有新连接、有数据可读、有数据可写的 I/O 事件;这⾥的「处理」包含从驱动读取到内核以及从内核读取到⽤户空间。⽆论是 Reactor,还是 Proactor,都是⼀种基于「事件分发」的⽹络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,⽽ Proactor 模式则是基于「已完成」的 I/O 事件。
3.7 Copy on Write
写时复制(Copy-on-write,COW),有时也称为隐式共享(implicit sharing)。COW 将复制操作推迟到第⼀次写⼊时进⾏:在创建⼀个新副本(只复制其⻚表)时,不会⽴即复制资源,⽽是共享原始副本的资源;当修改时再执⾏复制操作。通过这种⽅式共享资源,可以显著减少创建副本时的开销,节省资源。
实现原理:
● fork() 之后,内核会把⽗进程的所有内存⻚都标记为只读
● ⼀旦其中⼀个进程尝试写⼊某个内存⻚,就会触发⼀个保护故障(缺⻚异常),此时会陷⼊内核
● 内核将写⼊操作拦截,并为尝试写⼊的进程创建这个⻚⾯的⼀个新副本,恢复这个⻚⾯的可写权
限,然后重新执⾏这个写操作,这时就可以正常执⾏了
● 内核会保留每个内存⻚⾯的引⽤数。每次复制某个⻚⾯后,该⻚⾯的引⽤数减⼀;如果该⻚⾯只有⼀个引⽤,就可以跳过分配,直接修改
3.8 Linux
启动过程
BIOS开机⾃检:当打开电源后,⾸先是BIOS开机⾃检,按照BIOS中设置的启动设备(通常是硬盘)来启动。操作系统接管硬件以后,⾸先读⼊ /boot ⽬录下的内核⽂件。
运⾏ init:init 进程是系统所有进程的起点,没有这个进程,系统中任何进程都不会启动。init 程序⾸先是需要读取配置⽂件 /etc/inittab。许多程序需要开机启动。它们在Windows叫做"服务"(service),在Linux就叫做"守护进程"(daemon)。init进程的⼀⼤任务,就是去运⾏这些开机启动的程序。Linux启动时根据"运⾏级别",确定要运⾏哪些程序。Linux系统有7个运⾏级别(runlevel):
● 运⾏级别0:系统停机状态,系统默认运⾏级别不能设为0,否则不能正常启动
● 运⾏级别1:单⽤户⼯作状态,root权限,⽤于系统维护,禁⽌远程登陆
● 运⾏级别2:多⽤户状态(没有NFS)
● 运⾏级别3:完全的多⽤户状态(有NFS),登陆后进⼊控制台命令⾏模式
● 运⾏级别4:系统未使⽤,保留
● 运⾏级别5:X11控制台,登陆后进⼊图形GUI模式
● 运⾏级别6:系统正常关闭并重启,默认运⾏级别不能设为6,否则不能正常启动
系统初始化:执⾏ rc.sysinit 脚本,它主要是完成⼀些系统初始化的⼯作,rc.sysinit是每⼀个运⾏级别都要⾸先运⾏的重要脚本。它主要完成的⼯作有:激活交换分区,检查磁盘,加载硬件模块以及其它⼀些需要优先执⾏的任务。
建⽴终端并登录:这时系统环境基本已经设置好了,各种守护进程也已经启动了。init接下来会打开6个终端,以便⽤户登录系统。
常⽤命令
⽂件管理:
● ls: 显示当前⽬录下⽂件。
● pwd:显示当前⼯作⽬录的绝对路径
● cd + 路径:切换当前⼯作⽬录。(路径可以为相对路径也可以为绝对路径)
● touch:创建普通⽂件。
● rm:删除普通⽂件 rm -r:删除⽬录⽂件 -r(递归的意思)
● mkdir:创建⽬录⽂件
● rmdir:删除空⽬录
● mv:剪切、重命名
● cp:拷⻉ cp -r
● chmod:修改⽂件权限
● tar 压缩⽂件
进程管理:
● ps :显示进程信息快照(-e 显示所有进程。 -f 全格式)
● kill pid :结束进程
● kill -9 pid :强制结束pid进程
● kill -stop pid :挂起进程
● &:在后台运⾏进程
系统管理:
● uptime:查询系统负载
● top:实时显示系统中各个进程的资源占⽤状况,类似 于Windows 的任务管理器
● free:可以显示当前系统未使⽤的和已使⽤的内存数⽬,还可以显示被内核使⽤的内存缓冲区
⽹络通讯命令:
● ping
ifconfig
● tcpdump:抓包⼯具
● netstat:打印本地⽹卡接⼝上的全部连接、路由表信息、⽹卡接⼝信息。常⽤:显示tcp连接
以及状态。
3.9 硬件结构
CPU 缓存⼀致性
解决⽅法
1. 写传播:当某个 CPU 核⼼发⽣写⼊操作时,需要把该事件⼴播通知给其他核⼼;
2. 事务的串⾏化:只有保证了这个,才能保障数据是真正⼀致的,程序在各个不同的核⼼上运⾏的结 果也是⼀致的
MESI 协议
基于总线嗅探机制实现了事务串⾏化,也⽤状态机机制降低了总线带宽压⼒,这个协议就是 MESI 协议,这个协议就做到了 CPU 缓存⼀致性。MESI 协议其实是 4 个状态单词的开头字⺟缩写,分别是:
● Modified,已修改
● Exclusive,独占。「独占」和「共享」的差别在于,独占状态的时候,数据只存储在⼀个 CPU 核⼼的 Cache ⾥,⽽其他 CPU 核⼼的 Cache 没有该数据。另外,在「独占」状态下的数据,如果 有其他核⼼从内存读取了相同的数据到各⾃的 Cache ,那么这个时候,独占状态下的数据就会变成共享状态。
● Shared,共享。「共享」状态代表着相同的数据在多个 CPU 核⼼的 Cache ⾥都有,所以当要更 新 Cache ⾥⾯的数据的时候,不能直接修改,⽽是要先向所有的其他 CPU 核⼼⼴播⼀个请求,要求先把其他核⼼的 Cache 中对应的 Cache Line 标记为「⽆效」状态,然后再更新当前 Cache ⾥⾯的数据。
● Invalidated,已失效
这四个状态来标记 Cache Line 四个不同的状态。
伪共享
多个线程同时读写同⼀个 Cache Line 的不同变量时,⽽导致 CPU Cache 失效的现象称为伪共享
(False Sharing)。解决⽅法:
● Cache Line ⼤⼩字节对⻬:利⽤宏,以空间换时间的思想,浪费⼀部分 Cache 空间,从⽽换来性能的提升。
● 字节填充:如在类中增加占位的字符
⼀致性哈希
⼀致性哈希算法解决了分布式系统在扩容或者缩容时,发⽣过多的数据迁移的问题。
⼀致性哈希是指将「存储节点」和「数据」都映射到⼀个⾸尾相连的哈希环上,如果增加或者移除⼀个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响。但是⼀致性哈希算法不能够均匀的分布节点,会出现⼤量请求都集中在⼀个节点的情况,在这种情况下进⾏容灾与扩容时,容易出现雪崩的连锁反应。为了解决⼀致性哈希算法不能够均匀的分布节点的问题,就需要引⼊虚拟节点,对⼀个真实节点做多个副本。不再将真实节点映射到哈希环上,⽽是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这⾥有「两层」映射关系。引⼊虚拟节点后,可以会提⾼节点的均衡度,还会提⾼系统的稳定 性。所以,带虚拟节点的⼀致性哈希⽅法不仅适合硬件配置不同的节点的场景,⽽且适合节点规模会 ⽣变化的场景。