layout: post
title: 八股总结(三)操作系统内存管理、进程线程、进程同步与通信、中断与异常、常用命令
description: 八股总结(三)操作系统内存管理、进程线程、进程同步与通信、中断与异常、常用命令
tag: 八股总结
操作系统概述
冯诺依曼计算机体系结构?
存储器、计算器、控制器、输入输出设备
内存分层体系
地址空间与地址生成
逻辑地址与物理地址的区别与联系
- 物理地址:是硬件支持的、固定的地址空间
- 逻辑地址:一个运行的程序所拥有的内存范围
- 联系:两者可以通过CPU中的MMU(Memory Management Unit,内存管理单元)进行映射。应用程序中的变量和代码中的逻辑地址通过编译器分配和生成,这期间可能经过了多种编译转换,如C语言编译器,汇编语言编译器,等等。
操作系统在对内存进行管理的时候需要做些什么?
- 操作系统负责内存空间的分配与回收
- 操作系统需要提供某种技术从逻辑上对内存空间进行扩充
- 操作系统需要提供地址转换功能,负责程序的逻辑地址与物理地址间的转换。
- 操作系统需要提供内存保护功能,保证各个进程在各自存储空间内运行,互不干扰。
虚拟内存的目的是什么?
-
虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。
-
为了更好的管理内存,操作系统将内存抽象成地址空间,每个程序拥有自己的地址空间,这些地址空间被分割为多个块,每一块称为一页。
-
这些页被映射到物理内存,但是不需要映射到连续的物理内存,也不需要所有的页都在物理内存中,当程序引用到不在物理内存中的页时,再由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行访问指令。
内存覆盖技术是什么?有什么特点?
由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可以把用户空间分成为一个固定区和若干个覆盖区。将经常活跃的部分放在固定区,其余部分按照调用关系分段,首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统将其调入覆盖区,替换覆盖区中原有的段。
覆盖技术的特点:是打破了必须将一个进程的全部信息装入内存后才能运行的限制,但当同时运行程序的代码量大于主存时仍不能运行,再而,大家要注意到,内存中能够更新的地方只有覆盖区的段,不在覆盖区的段会常驻内存。
内存交换技术是什么?有什么特点?什么时候会进行内存的交换?
交换(对换)技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)
换入:把准备好竞争CPU运行的程序从辅存移到内存。 换出:把处于等待状态(或CPU调度原则下被剥夺运行权力)的程序从内存移到辅存,把内存空间腾出来。
内存交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程;如果缺页率明显下降,就可以暂停换出。
虚拟内存管理技术(虚存技术)是什么?
- 虚存技术能像覆盖技术那样不把所有内容都放在内存中,但它无须程序员干涉。
- 能像交互技术那样,实现进程在内存与外存之间的交换,可用做得更好,只对进程的的部分内容在内存和外存之间交换。
大部分虚拟存储系统都采用虚拟页表存储管理技术,在基本页式管理基础上,增加了请求调页和页面置换功能。其基本思路是:
当一个用户程序调入内存运行时,不是将该程序的所有页面都装入内存,而是装入部分的页面即可启动程序。运行过程中,如果发现运行程序要访问的数据不在内存,则向系统发出缺页中断请求,系统在处理这个中断时,将外存中相应的页面调入内存。
虚存管理的常用页面置换算法有:
- 最优页面置换算
- 先进先出算
- 最久未使用算法(LRU缓存)
- 时钟页面置换算法
抖动是什么?或者说颠簸现象
刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为,会持续触发缺页中断,使得进程运行的速度变得很慢,这种状态被称为抖动,或颠簸。产生抖动的主要原因是进程频繁访问页面数目高于可用物理块数(分配给进程的物理块不够)
为进程分配的物理块太少会使得进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率。
什么是快表?
快表,又称 转换转换后备缓冲表(Translation Look-aside Buffer,TLB),是一种访问速度比内存快很多的高速缓冲存储器,用来存放当前访问的若干表项,以加速地址转换的过程,与此对应,内存中的页表常称为慢表。
页表可能非常大,导致访问一个内存单元需要两次内存访问(一次用于获取页表项(因为页表也在内存中),一次用于访问数据)。
TLB快表,利用缓存的思想,将经常使用的页表项存在TLB缓冲(动态内存),从而缩减了页表查询的耗用时间。
地址转换中,有快表和没快表的区别是什么?
内存碎片与外部碎片
- 内碎片:分配给某些进程的内存区域中有些部分没用上,常见于固定分配方式
- 外碎片:内存中某些空闲区,由于比较小,而难以利用上。
如何消除碎片文件
对于外部碎片,通过紧凑技术消除,就是操作系统不时地对进程进行移动和整理,但是这需要动态重定位寄存器的支持,且相对费时,紧凑地过程实际上类似于Windows系统中磁盘整理程序,只不过后者是对外存空间的紧凑。
解决外部内存碎片问题的方法就是内存交换
例如可以把音乐程序占用的那256MB内存写到硬盘上,然后再从硬盘上读回来到内存里。不过再读回的时候,我们不能装载回原来的位置,而是紧紧跟着已经被占用的512MB内存后边,这样就能空缺出连续的256MB空间,于是新的200MB程序就可以转载进来。
回收内存时,尽可能将相邻的空闲空间合并。
为什么分段式存储管理有外部碎片而无内部碎片?为什么固定分区分配有内部碎片而不会有外部碎片?
分段式分配是按需分配,而固定式分配是固定分配的方式。
进程、线程、协程
概念
进程的生命周期
- 进程创建:引起进程创建的3个主要方式:
- 系统初始化
- 用户请求创建一个新进程
- 正在运行的进程执行了创建进程的系统调用
- 进程运行:CPU内核选择一个就绪的进程,让他占用处理机并执行
- 进程等待(阻塞):在某些情况下,进程可能没有办法直接运行,
进程只能自己阻塞直接,因为只有进程自身才能知道何时需要等待某种事件的发生。
- 请求并等待系统服务,因为无法马上完成
- 启动某中操作,因为无法马上完成
- 需要的数据,没有到达
- 进程唤醒:被阻塞的进程需要的资源可被满足或者阻塞进程等待的事件到达时,进程被唤醒(
进程只能被别的进程或OS唤醒
) - 进程结束:以下情况进程结束
- 正常退出(自愿的)
- 错误退出(自愿的)
- 致命错误(强制性的)
- 被其他进程杀死(强制性的)
进程、线程和协程的区别和联系
- 进程:是资源调度的基本单位,一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序。
- 线程:是程序执行的基本单位,是轻量级的进程,每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束,进程也会结束。
- 协程:是用户态的轻量级线程,是线程内部调度的基本单位,使用协程无操作系统切换,只需要保存自己的寄存器和栈的上下文,因此切换速度非常快。
线程和进程的区别
-
资源分配:进程是操作系统中的一个独立执行单位,拥有独立的地址空间、文件描述符、内存空间等系统资源。每个进程都有自己的代码、数据和堆栈空间。而线程是进程的子任务,共享进程的资源,包括地址空间和文件描述符等。线程之间可以直接访问同一进程的数据。
-
执行单位:进程是程序在执行过程中的一个实例,是资源分配和调度的基本单位。每个进程都是独立运行的,有自己的程序计数器(PC)、寄存器集合和栈。而线程是进程的执行单元,一个进程可以有多个线程同时执行不同的任务,共享进程的上下文。
-
切换开销:由于进程拥有独立的地址空间和系统资源,进程间的切换开销相对较大。在进程切换时,需要保存和恢复进程的上下文信息,切换涉及到页表的切换和缓存失效等开销。而线程切换的开销较小,因为线程共享相同的地址空间和系统资源,切换时只需要保存和恢复线程的上下文信息。
-
并发性和多核利用:由于线程共享进程的资源,多个线程可以在同一时间内并发执行,提高了系统的并发性。而进程之间的并发性较低,进程间通信需要额外的机制。另外,多个线程可以在多核系统上并行执行,充分利用多核处理器的性能。
进程切换和线程切换涉及到的内容可以具体分为以下几个方面:
-
上下文切换:切换是指从一个执行单位(进程或线程)切换到另一个执行单位的过程。在切换之前,需要保存当前执行单位的上下文信息,包括程序计数器(PC)、寄存器状态、栈指针等。然后,加载下一个执行单位的上下文信息,使其能够继续执行。
-
内存管理:在进程切换时,操作系统需要将当前进程的内存空间映射(包括页表等)切换为下一个进程的内存空间,这涉及到内存管理机制。而线程切换时,不需要切换内存空间,因为线程共享进程的地址空间。
-
调度和优先级:切换过程中,操作系统的调度器需要选择下一个要执行的进程或线程。调度算法通常根据一定的策略来决定选择哪个执行单位。优先级也可以用于确定执行单位的执行顺序。
-
缓存刷新:在切换过程中,由于不同的执行单位可能访问不同的数据,可能需要刷新处理器缓存,以确保新执行单位访问的是最新的数据。
-
同步和通信:在切换过程中,操作系统需要处理进程或线程间的同步和通信机制。例如,在进程切换时,需要考虑进程间通信机制,如管道、共享内存等。
这些内容共同构成了进程切换和线程切换的复杂性和开销。由于进程切换涉及到更多的资源切换和内存管理,因此进程切换的开销通常比线程切换大。线程切换的开销相对较小,因为它们共享相同的地址空间和系统资源,切换时只需要切换线程的上下文信息即可。
一个进程可以创建多少线程,和什么有关?
这个要分不同系统去看:
- 如果是32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
- 如果是64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。
顺便多说一句,过多的线程将会导致大量的时间浪费在线程切换上,给程序运行效率带来负面影响,无用线程要及时销毁。
什么时候用多线程,什么时候用多进程?
- 频繁修改:需要频繁创建和销毁的优先使用多线程
- 计算量:需要大量计算的优先使用多线程,因为需要消耗大量的CPU资源且切换频繁,所以多线程好一点。
- 相关性:任务相关性比较强的用多线程,相关性比较弱的用多进程,因为线程之间的数据共享和同步比较简单。
- 多分布:可能要拓展到多级分布的用多进程,多核分布的用多线程。
实际中更常见的是进程加线程结合的方式,并不是非此即彼的。
怎么回收线程?有哪些方法?
-
等待线程结束:主线程调用
int pthread_join(pthread_t tid, void* retval)
,等待子线程退出并回收其资源,类似于进程中wait/waitpid回收僵尸进程,调用pthread_join的线程会被阻塞- tid:创建线程时通过指针得到的tid的值
- retval:指向返回值的指针
-
结束线程:子线程执行
void pthread_exit(void* retval)
,用来结束当前线程并通过retval传递返回值,该返回值可以通过pthread_join获得。 -
分离线程:主线程、子线程均可调用
int pthread_detach(pthread_t tid)
主线程、子线程均可调用。其中主线程的参数是要分离的子线程的id,子线程结束的是自己本身。调用后和主线程分离,子线程结束后立即回收资源。
服务器高并发的解决方案你知道多少?
- 应用数据与静态资源分离:将静态资源(图片、视频、js、css等)单独保存到专门的静态资源服务器中,在客户端访问的时候从静态资源服务器
中返回静态资源,从主服务器中返回应用数据。 - 客户端缓存:因为效率最高,消耗资源最小的就是纯静态的HTML页面,所以可以把网站上的页面尽可能用静态的来实现,在页面过期或者有数据更新之后再将页面重新缓存。或者先生成静态页面,然后用ajax异步请求获取动态数据。
- 集群和分布式:(集群是所有服务器都有相同的功能,请求哪台都可以,主要起分流的作用,分布式是将不同的业务放到不同的服务器中,处理一个请求可能需要使用多台服务器,起到加快请求处理的速度)。使用服务器集群和分布式架构,同时加快请求的处理速度
- 反向代理:在访问服务器的时候,服务器通过别的服务器获取资源或结果返回给客户端。
进程调度与通信
进程调度算法
-
先来先服务:
非抢占式的调度算法,按照请求的顺序进行调度。
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。 -
短作业优先
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 -
最短剩余时间优先
最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 -
时间片轮转
将所有就绪进程按 FCFS(先来先服务) 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系:
因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
而如果时间片过长,那么实时性就不能得到保证。
-
优先级调度
为每个进程分配一个优先级,按优先级进行调度。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。 -
多级反馈队列
一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。
多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,…。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
优先级反转
在下图的例子中,T1-T3优先级依次下降,T3从t1到t2后执行了访问共享资源,但此时到t3时刻被优先级更高的T1进程打断,转而执行T1,T1执行到t4时刻,发现自己也需要刚刚T1访问的那个恭喜资源,于是切换为T3,让它释放那个占用的共享资源,但是在释放期间t5时刻,T1被优先级更高的T2再次打断,转而执行T2,等待T2执行完毕,才会到T3释放共享资源,释放完毕后,执行T1。这期间,优先级更低的T2抢在了优先级更高的T1之前完成了任务,称为优先级反转。造成优先级反转的主要原因是低进程影响了高优先级进程的运行,解决方案是T3在释放共享资源时应该继承T1的优先级,以避免被进程T2抢占。
惊群效应是什么?怎么解决?
惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。
Linux解决方案之accept
Linux 2.6 版本之前,监听同一个 socket 的进程会挂在同一个等待队列上,当请求到来时,会唤醒所有等待的进程。这就会有惊群现象的出现
Linux 2.6 版本之后,通过引入一个标记位 WQ_FLAG_EXCLUSIVE,解决掉了 accept 惊群效应。
具体实现: 在Linux 2.6版本中,维护了一个等待队列,队列中的元素就是进程,非exclusive属性的元素会加在等待队列的前面,而exclusive属性的元素会加在等待队列的末尾,当子进程调用阻塞accept时,该进程会被打上WQ_FLAG_EXCLUSIVE标志位,从而成为exclusive属性的元素被加到等待队列中。当有TCP连接请求到达时,该等待队列会被遍历,非exclusive属性的进程会被不断地唤醒,直到出现第一个exclusive属性的进程,该进程会被唤醒,同时遍历结束。
Nginx解决方案之锁的设计
Nginx采用锁的机制避免了对资源的抢夺,只有一个连接会得到锁,没有得到锁的只能等待。
Linux下进程间的通信方式(IPC)?
- 管道:
- 无名管道(内存文件):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程之间使用。进程的亲缘关系通常是指父子进程关系。
- 有名管道(FIFO文件,借助文件系统):有名管道也是半双工的通信方式,但是允许在没有亲缘关系的进程之间使用,管道是先进先出的通信方式。
- 共享内存:共享内存就是映射一段能够被其他进程所访问的内存,这段共享内存由一个进程创建,但是多个进程都可以访问。共享内存是最快的IPC(Innerprocess Communication 进程间通信)方式,它是针对其他进程间通信方式效率低而专门设计的,它往往与信号量配合使用,从而实现进程间的同步和通信
- 消息队列:消息队列是有消息的链表,存放在内核中并由消息队列标识符,标识,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 套接字(socket):适用于不同机器间的进程通信,也可以在本机作为两个进程的通信方式。
- 信号:用于通知接收进程某个事件已经发生,比如Ctrl + C就是信号。
- 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问,它常作为一种锁机制,实现进程、线程的对临界区的同步及互斥访问。
- 管程:信号量是一种进程同步机制,但是每个要访问临界资源的进程都必须自备同步操作wait(S)和signal(S),这样大量同步操作分散到各个进程中,可能会导致系统管理问题和死锁,在解决上述问题的过程上,提出了新的进程间同步工具——管程。
管程相对于锁又增加了很多的条件变量,用于确定某些共享资源是否得到满足。进入管程后就可以操作各种条件变量,当某些条件无法得到满足时,会进入等待(wait),当再次满足时会唤醒(signal)。
守护进程、僵尸进程和孤儿进程
- 守护进程
也称为精灵进程,是运行在后台的一种特殊进程,他独立于控制终端,并且可以周期性的执行某种任务或者等待处理某些发生的事件。
当前大多数web服务器用的都是守护进程。
可以Linux指针ps ajx
来查看系统中的进程
a代表不仅列出当前用户的进程并且其他用户的进程也会被列出
参数x表示不仅列出所有控制终端的进程也列出所有无控制终端的进程
参数j表示列出也作业控制相关的进程
-
孤儿进程
如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程(注:任何一个进程都必须有哦父进程)。
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程,孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。 -
僵尸进程
如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。
设置僵尸进程的目的是为了维护子进程的信息,以便父进程在以后的某个时刻获取,这些信息至少包括进程ID、进程的终止状态,以及该进程使用的CPU时间,所有当子进程的父进程调用wait或waitpid时就可以得到这些信息,如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们。
Linux下同步机制?
- POSIX信号量:可以用于进程同步,也可以用于线程同步
- POSIX互斥锁 + 条件变量:只能用于线程同步
哲学家进餐问题
为了防止死锁的发生,可以设置两个条件:
- 必须同时拿起左右两根筷子;
- 只有在两个邻居都没有进餐的情况下才允许进餐。
介绍一下几种典型的锁?
-
读写锁
-
互斥锁
-
条件变量
-
自旋锁
你知道哪几种锁?
-
互斥锁(mutex)
一次只能一个线程拥有互斥锁,其他线程只有等待
互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态,直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。
互斥锁的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能实现是先自旋一段时间,当自旋时间超过阈值之后再将线程投入到睡眠中,因此在并发运算中使用互斥锁的效果可能不亚于自旋锁。
-
条件变量(cond)
互斥锁的一个明显的缺点是他只有两种状态,锁定和非锁定,而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法,弥补了互斥锁的不足。
它常和互斥锁一起使用,以免出现竟态条件,当条件不满足时,线程往往解开相应的互斥锁并阻塞线程,然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正在被此条件变量堵塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制
-
自旋锁(spin)
如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止,如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用在加锁时间很短的场景,这个时候效率比较高,因为自旋锁通过CPU提供的CAS(compare and swap)函数,在用户态完成加锁和解锁,不会产生线程的上下文切换,相对于互斥锁而言,会更快,开销更小。 -
读写锁
包含读锁和写锁两部分。- 如果只读取共享资源,用读锁,读锁是共享锁,读锁能够并发地被多个线程持有,多个线程同时持有也不会破坏共享资源的数据。
- 如果要修改共享数据,则使用写锁,写锁是独占锁,任何时候只能有一个线程持有写锁,类似互斥锁和自旋锁,没有获取到写锁的线程会被阻塞,其他线程获取读锁的操作也会被阻塞。
-
乐观锁与悲观锁
- 前边提到的互斥锁、自旋锁、读写锁都是属于悲观锁,悲观锁比较悲观,认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
- 相反地,如果认为多线程同时修改共享资源的概率比较低,就可以采用乐观锁。
乐观锁,假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
乐观锁全程并没有加锁,它也叫无锁编程。实际上我们常见的SVN和Git也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否发生了冲突,发生冲突后,需要我们自己修改后,再重新提交。乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本高的场景时,考虑使用乐观锁
原子操作是如何实现的
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
首先处理器会自动保证基本的内存操作的原子性,处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。
CPU1 CPU2
i=1 i=1
i+1 i+1
i=2 i=2
使用总线锁保证原子性
:如果多个处理器同时对共享变量进行读改写操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。所谓总线锁,就是处理器提供的一个LOCK信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存
2. 使用缓存锁保证原子性
:频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁。所谓缓存锁定是指内存区域如果被缓存在处理器的缓存中,并且在lock操作期间被锁定,那么它执行锁操作回写到内存时,处理器不在总线上声明lock信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使得缓存行无效,在如上图所示的例子中,当CPU1修改缓存行中的i时使用了缓存锁定,那么CPU2就不能使用同时缓存i的缓存行
死锁产生原因和条件
两个线程A和B,两个数据1和2。线程A在执行过程中,首先对资源1加锁,然后再去给资源2加锁,但是由于线程的切换,导致线程A没能给资源2加锁。线程切换到B后,线程B先对资源2加锁,然后再去给资源1加锁,由于资源1已经被线程A加锁,因此线程B无法加锁成功,当线程切换为A时,A也无法成功对资源2加锁,由此就造成了线程AB双方相互对一个已加锁资源的等待,死锁产生。
理论上认为死锁产生有以下四个必要条件,缺一不可:
- 互斥条件:进程对所需求的资源具有排他性,若有其他进程请求该资源,请求进程只能等待。
- 不剥夺条件:进程在获得资源未释放前,不能被其他进程强行夺走,只能自己释放。
- 请求和保持条件:进程当前所拥有的资源在进程请求其他新资源时,由该进程继续占用。
- 循环等待条件:存在一种进程资源循环等待链,链中每个进程已获得的资源同时被链中下一个进程所请求。
死锁处理方法
-
死锁预防:确保系统永远不会进入死锁
从死锁产生的特征出发来避免死锁发生,比如限制资源的申请方式,破坏互斥条件、不抢占资源,破坏保持条件,如果不能立即分配所需的全部资源,则释放当前已经占有的资源;对所有资源类型进行排序,并要求每个进程按照资源的顺序进行申请,破坏循环等待的条件。
-
死锁恢复:运行系统进入死锁,寻找恢复方法
-
忽略死锁:忽略该问题
银行家算法
银行家算法(Banker’s Algorithm)是一个死锁避免的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础。判断并保证系统的安全运行。
死锁检测
上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。
图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
注意:死锁一定有环,有环不一定死锁
死锁恢复
- 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
中断与异常
Linux中异常和中断的区别与联系
- 中断:是由硬件设备产生的,通过中断控制器发送给CPU,接着CPU判断收到的中断来自于哪个硬件设备(这在内核中有定义),最后由CPU发送给内核,有内核处理中断。
-
异常:例如,CPU处理程序的时候,一旦程序不在内存中,就会产生缺页异常;当运行除法程序时,除数为0时,又会产生0异常。因此异常是由CPU产生的,同时,它会发送给内核,要求内核处理这些异常
-
相同点:最后都是由CPU发送给内核,交由内核处理;处理程序的流程设计上是相似的
-
不同点:
- 产生的源不同,异常是由CPU产生的,而中断是由硬件设备产生的;内核需要根据是异常还是中断调用不同的处理程序。
- 内核根据是异常还是中断,调用不同的处理程序
- 中断不是时钟同步的,这意味着中断可能随时到来;异常由于是CPU产生的,所以他是时钟同步的。
- 当处理中断时,处于中断上下文中,当处理异常时,处于进程的上下文中。
Linux指令
网络与防火墙
网络端口
显示或设置网络设备
ifconfig
ip address
持续监控网络设置状态
ip monitor
查看数据报到主机的路由线路
tracerouter www.baidu.com
查看网络层是否可达目的地址
ping www.baidu.com
查看域名相关信息
dig www.baidu.com
获取特定主机的ip地址
host www.baidu.com
也可以通过ip获取主机名
host 8.8.4.4
列出所有监听和非监听的TCP连接
ss -at
列出所有监听和非监听的UDP连接
ss- au
netstat列出端口
netstat -a
列出所有TCP端口
netstat -at
列出所有UDP端口
netstat -au
列出所有Unix端口
netstae -ax
端口占用查看
netstat -lnpt
防火墙
Ubuntu
显示或设置网络设备
ifconfig
ip address
持续监控网络设置状态
ip monitor
查看数据报到主机的路由线路
tracerouter www.baidu.com
查看网络层是否可达目的地址
ping www.baidu.com
查看域名相关信息
dig www.baidu.com
获取特定主机的ip地址
host www.baidu.com
也可以通过ip获取主机名
host 8.8.4.4
列出所有监听和非监听的TCP连接
ss -at
列出所有监听和非监听的UDP连接
ss- au
netstat列出端口
netstat -a
列出所有TCP端口
netstat -at
列出所有UDP端口
netstat -au
列出所有Unix端口
netstae -ax
端口占用查看
netstat -lnpt
iptables系列
查看防火墙状态
service iptables status
启动|停止|重启防火墙
service iptables statrt|stop|restart
防火墙开发指定端口(以80为例)
iptables -I INPUT -p tcp --dport 80 -j ACCEPT
service iptables save
设置指定ip访问指定端口
进程管理
查看进程状态
ps(process status)查看进程状态
– 查看进程状态,默认列出当前用户的进程
– 选项 a:列出带有终端的所有用户进程
– 选项 x:列出当前用户的所有进程,包括没有终端的进程
– 选项 u:面向用户友好的显示风格
– 选项 -e:列出所有进程
– 选项 -u:列出某个用户关联的所有进程
– 选项 -f:显示完整格式的进程列表
ps [选项]
– 常用命令
– 查看系统中的所有进程
ps aux
– 查看子父进程之间的关系
ps -ef
杀死进程kill
kill终止进程
– 通过进程ID终止进程
– 选项 -9:强迫进程立即停止
kill [选项] 进程ID
– 通过进程名称终止所有相关进程,支持通配符
killall 进程名称
端口号查询进程号(PID)netstat 或 lsof
根据端口号查看进程号
1.使用lsof命令
lsof 是“list open files”的缩写,在linux系统中,一切可是为文件,网络连接也可视为文件,因此,我们可以通过lsof命令查看占用某个端口号的进程。
示例:
lsof -i :8080
或者
lsof -i | grep 8080
参数解释:-i 是Internet的缩写,它表示选择网络地址与[i]表示的内容匹配的所有文件(网络连接)。
2. 使用netstat命令
netstat是用于打印网络连接、路由表、接口统计信息等。目前官方文档是这是一个过时的命令,因此推荐使用其他命令,如netstat大部分的功能都可以通过ip命令实现。在这里,我们仍给出使用netstat命令实现通过端口号查找进程号的示例。
示例:
netstat -nlp | grep 8080
参数解释:
-n --numeric的缩写,即通过数值展示ip地址
-l --listening的缩写,只打印正在监听中的网络连接
-p --program,打印相应端口号对应进程的进程号
进程监控TOP
将进程号为1303的系统赋值每20s刷新一次
top -d 20 -p 1303
每隔20s,一共执行3次,统计结果写入test.txt
top -d 20 -n 3 -b -> test.txt
小林coding操作系统
硬件结构
内存对齐的问题与总线宽度有关!!!
-
总线:⽤于 CPU 和内存以及其他设备之间的通信,总线可分为 3种:
- 地址总线,用于指定CPU将要操作的内存地址
- 数据总线,用于读写内存数据;
- 控制总线,⽤于发送和接收信号,⽐如中断、设备复位等信号,CPU 收到信号后⾃然进
⾏响应,这时也需要控制总线;
-
存储器金字塔
CPU ⾥的寄存器和 Cache,是整个计算机存储器中价格最贵的,虽然存储空间很⼩,但是读写速度是极快的,⽽相对⽐较便宜的内存和硬盘,速度肯定⽐不上 CPU 内部的存储器,但是能弥补存储空间的不⾜。
3. CPU 缓存一致性
写直达:保存内存与cache一致性的最简单的方式是:把数据同时写入内存和Cache中,这种方式称为写直达。
如果数据在cache中,先把数据更新到cache,再写入到内存。
如果不在cache中,就直接把数据更新到内存。
写回: 既然写直达由于每次写操作都会把数据写回到内存,而导致影响性能,于是为了减少数据写回内存的频率,出现了写回
的方法。
在写回机制中,当发生写操作时,新的数据仅仅被写入到Cache Block里,只有当修改过的Cahe Block被替换时才需要写到内存中,减少了数据写回内存的频率。
写回这个⽅法,在把数据写⼊到 Cache 的时候,只有在缓存不命中(需要读取内存),同时数据对应的 Cache 中的 Cache Block 为脏标记的情况下,才会将数据写到内存(更新内存)中,而在缓冲命中时,只需要把数据写入到cache 并标记为脏,不必写入到内存中。
- 缓存一致性
现在CPU都是多核的,由于L1/L2 cache 是多个核心各自独有的,那么会带来多核心的缓存一致性(cache coherence)
的问题,如果不能保证缓存一致性,就可能造成结果错误。
假设A核和B核同时运行两个线程,都操作共同的变量i(初始值为0),这时如果A核执行了++i,为了考虑性能,使用写回策略,先把值为1的数据写入到cache中,并标记为脏,这个时候并没有同步到内存中,因为写回策略,只有A核中的这个cache block 要被替换时,才会写入到内存中。
这时如果B核尝试读取i变量的值,则会读到错误的值,因为B核无法访问A核的cache,而A核的修改还没写入到内存中。
为了解决这一问题,就需要一种机制,来同步两个不同核心里边的缓存数据,要实现这个机制,需要保证下边2点:
-
第一点,某个CPU核心里的cache数据更新时,必须要传播到其他核心的cache,这个称为
写传播
-
第二点,某个CPU核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为
事务的串行化
-
写传播的原则就是当某个CPU核心更新了cache中的数据,要把该事件广播通知到其他核心,最常见的方式就是
总线嗅探
-
实现事务串行化,要做到2点,CPU核心对于cache中数据的操作,需要同步到其他CPU核心,需引入锁的概念,如果两个CPU核心里有相同数据的cache,那么这个cache数据的更新,只有拿到’锁’,才能进行对应的数据更新。
总线嗅探的方式:CPU需要每时每刻监听总线上的一切活动,但是不管别的核心的cache是否缓存了相同的数据,都需要发出一个广播事件,这无疑会加重总线负载。
软中断
中断:
在计算机中,中断是系统⽤来响应硬件设备请求的⼀种机制,操作系统收到硬件的中断请求,会打断正在执⾏的进程,然后调⽤内核中的中断处理程序来响应请求
软中断:
Linux 系统为了解决中断处理程序执⾏过⻓和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」。
- 上半部⽤来快速处理中断,⼀般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。
- 下半部⽤来延迟处理上半部未完成的⼯作,⼀般以「内核线程」的⽅式运⾏
举⼀个计算机中的例⼦,常⻅的⽹卡接收⽹络包的例⼦。
⽹卡收到⽹络包后,会通过硬件中断通知内核有新的数据到了,于是内核就会调⽤对应的中断处理程序来响应该事件,这个事件的处理也是会分成上半部和下半部。
上部分要做到快速处理,所以只要把⽹卡的数据读到内存中,然后更新⼀下硬件寄存器的状态,⽐如把状态更新为表示数据已经读到内存中的状态值。
接着,内核会触发⼀个软中断
,把⼀些处理⽐较耗时且复杂的事情,交给「软中断处理程序」去做,也就是中断的下半部,其主要是需要从内存中找到⽹络数据,再按照⽹络协议栈,对⽹络数据进⾏逐层解析和处理,最后把数据送给应⽤程序。
所以,中断处理程序的上部分和下部分可以理解为:
- 上半部直接处理硬件请求,也就是
硬中断
,主要是负责耗时短的⼯作,特点是快速执⾏
; - 下半部是由内核触发,也就说
软中断
,主要是负责上半部未完成的⼯作,通常都是耗时⽐较⻓的事情,特点是延迟执⾏
;
系统中有哪些软中断?
Linux中的软中断包括网络收发,定时器,调度,RCU锁(read copy update)等类型。可以通过查看/proc/softirqs
来观察软中断的累计次数情况。
如何定位软中断CPU使用率过高的问题
要想知道当前的系统的软中断情况,我们可以使⽤ top 命令查看,下⾯是⼀台服务器上的top 的数据:
上图中的⻩⾊部分 si (soft interrupt 软中断),就是 CPU 在软中断上的使⽤率,⽽且可以发现,每个 CPU 使⽤率都不⾼,两个 CPU 的使⽤率虽然只有 3% 和 4% 左右,但是都是⽤在软中断上了。
浮点数是什么含义
计算机是以浮点数的形式存储⼩数的,包含三个部分:
符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数:
指数位:指定了⼩数点在数据中的位置,指数可以是负数,也可以是正数,指数位的⻓度越⻓则数值的表达范围就越⼤;
尾数位:⼩数点右侧的数字,也就是⼩数部分,⽐如⼆进制 1.0011 x 2^(-2),尾数部分就是 0011,⽽且尾数的⻓度决定了这个数的精度,因此如果要表示精度更⾼的⼩数,则就要提⾼尾数位的⻓度。
网络系统
Linux接收网络包的流程
当有⽹络包到达时,⽹卡发起硬件中断,于是会执⾏⽹卡硬件中断处理函数,中断处理函数处理完需要「暂时屏蔽中断」,然后唤醒「软中断」来轮询处理数据,直到没有新数据时才恢复中断,这样⼀次中断处理多个⽹络包,于是就可以降低⽹卡中断带来的性能开销。
软中断处理流程:从Ring Buffer中拷贝数据到内核struct sk_buff缓冲区中,从而可以作为一个网络包交给网络协议栈进行逐层处理。
零拷贝
磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存10倍以上,所以针对优化磁盘的技术非常多,比如零拷贝、直接I/O、异步I/O等。这些优化的目的都是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效减少磁盘的访问次数。
DMA(Direct Memory Access,直接内存访问)
在进行I/O设备和内存的数据传输的时候,数据搬运的工作全部交给DMA控制器
,而CPU不再参与任何与数据搬运相关的事情。
传统的文件传输过程
传统 I/O 的⼯作⽅式是,数据读取和写⼊是从⽤户空间到内核空间来回复制,⽽内核空间的数据是通过操作系统层⾯的 I/O 接⼝从磁盘读取或写⼊。
代码只有简单的两行:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
期间共发生了4次用户态与内核态的上下文切换,因为发生了两次系统调用,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换为用户态。
期间还发生了4次数据拷贝,其中两次是DMA的拷贝,另外两次是通过CPU拷贝到用户区。
如何实现零拷贝?
零拷贝技术的实现方式通常有2种:
- mmap + write
- sendfile
mmap + write
read()系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,为了减少这一开销,可以用mmap()替换read()系统调用函数。
buf = mmap(file, len);
write(sockfd, buf, len);
mmap()系统调用函数会直接把内核缓冲区里的数据映射
到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
通过mmap()
来代替read()
,我们减少了一次数据拷贝的过程。
但这还不算是最理想的零拷贝,因为系统调用还是2次,需要4次上下文切换。
sendfile
Linux内核2.1版本中,提供了一个专门发送文件的系统调用函数sendfile()
,函数形式如下:
#include<sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t * offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后边两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
该系统调用,可以直接把内核缓冲区里的数据拷贝到socket缓冲区,不再拷贝到用户态,这样就只有2次上下文切换和3次数据拷贝:
真正的零拷贝
senfile相比mmap + write,又减少了1次系统调用,也就是2次上下文切换。但仍然不是真正的零拷贝。
如果网卡支持SG-DMA(the Scatter-Gather Direct Memory Access)技术,我们可以进一步减少通过CPU把内核缓冲区里的数据拷贝到socket缓冲区的过程。
可以通过下边的命令,查看网卡是否支持scatter-gather特性:
ethtool -k eth0 | grep scatter-gather
scatter-gather: on
在网卡支持SG-DMA
技术的情况下,sendfile()
系统调用的过程发生了点变化,具体过程如下:
- 第一步,通过DMA将磁盘上的数据拷贝到内核缓冲区;
- 第二步,缓冲区描述符和数据长度传到socket缓冲区,这样网卡的SG-DMA控制器就可以直接将内核缓冲中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到socket缓冲区中,这样就减少了一次数据拷贝:
这就是所谓的零拷贝
技术,因为没有在内存层面去拷贝数据,所有的数据都是通过DMA进行传输,不需CPU来搬运,与传统的文件传输方式相比,减少了2次上下文切换,2次数据拷贝,且2次都是DMA拷贝搬运,CPU是零拷贝
。
总体来看,零拷贝技术可以把文件传输性能提高至少一倍以上。