知识点汇总【一】操作系统三十八问

1. 进程和线程的区别

1.1 进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现操作系统的并发;线程是进程的子任务,是CPU调度和分配的基本单位,用于保证程序的实时性,实现进程内部的并发。

1.2 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。也就是说,资源分配给进程,同一进程的所有线程共享该进程的所有资源。

1.3 线程是操作系统可识别的最小执行和调度单位,每个线程独自占用一个虚拟处理器:独自的寄存器组、指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(动态内存、映射文件和目标代码)、打开的文件队列和其他内核资源。

1.4 一个线程只能属于一个进程,而一个进程可以有多个线程(至少一个)。线程依赖于进程而存在。

1.5 同一进程的多个线程共享代码段(代码和常量)、数据段(全局变量和静态变量)、扩展段(堆存储),但是每个线程拥有自己的栈段(运行时段),用于存放所有局部变量和临时变量。

1.6 创建和撤销进程时,系统要为之分配或回收资源,如内存空间、IO设备等,开销显著大于创建或撤销线程时; 进程切换时的开销也远大于线程切换,前者涉及整个当前CPU环境的保存以及被调度运行的进程的CPU环境的设置。后者只需保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。

1.7 进程编程调试简单,可靠性高,但是创建销毁开销大;线程编程调试复杂,但是开销小,切换速度快。

1.8 进程间不会相互影响,而一个线程挂掉将导致整个进程挂掉。

严格的说没有“线程崩溃”,只是触发了SIGSEGV (Segmentation Violation/Fault)。如果没有设置对应的Signal Handler操作系统就自动终止进程(或者说默认的Signal Handler就是终止进程);如果设置了,理论上可以恢复进程状态继续跑(用longjmp之类的工具)
如果该挂掉的线程改变了其他线程的区域或在共享区域造成了破坏,即使该sig信号被catch,也会导致其他就行执行异常而挂掉。
总体来说,线程没有独立的地址空间,如果崩溃,会发信号,如果没有错误处理的handler,OS一般直接杀死进程。就算是有handler了处理,一般也会导致程序崩溃,因为很有可能其他线程或者进程的数据被破坏了。

1.9 进程适用于多核、多机分布;线程适用于多核。

2. 进程间通信与线程间通信

2.1 进程间通信
2.1.1 管道

  管道包括匿名管道和命名管道,前者可用于具有亲缘关系的进程间通信,后者具有前者的功能并且允许无亲缘关系进程间通信。

匿名管道PIPE

  • 半双工,数据只能在一个方向上流动,单次次数据传递中具有固定的读写端(下一次可能就互换了)
  • 父子进程或者兄弟进程间通信
  • 可视为特殊的文件,对于读写可以使用read/write函数,但是其不属于任何文件系统,只存在与内存中

命名管道FIFP

  • 无关进程间通信
  • 有路径名与之相关联,以一种特殊设备文件形式存在于文件系统中
2.1.2 系统IPC

消息队列

  消息的链接表,存放在内核中,由一个标识符(队列ID)来标记,克服了信号传递信息少和管道只能承载无格式字节流以及缓冲区大小受限的缺点。

  具有写权限的进程可以按照一定规则向消息队列中添加新信息,具有读权限的进程可以从消息队列中读取信息。

  有三个特点:

  • 消息队列面向记录,其中的消息具有特定的格式以及特定的优先级
  • 消息独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除
  • 消息队列可以实现消息的随机查询(不一定先进先出),也可按照消息的类型读取

信号量semaphore

  一个计数器,用于控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而非存储进程间通信数据。

  有四个特点:

  • 信号量用于进程间同步,若要传递数据需要结合共享内存
  • 信号量基于操作系统的PV操作,对信号量的操作都是原子操作
  • 每次的PV操作不仅可加1减1,可以加减任意正整数
  • 信号量组

信号signal

  用于通知接收进程某个事件已经发生。

共享内存

  它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新,需要依靠某种同步操作,比如互斥锁与信号量。

  三个特点:

  • 最快的IPC,因为进程直接对内存进行读取
  • 需要同步
  • 常与信号量一起使用(信号量完成同步)
2.1.3 套接字SOCKET

  可以在本机进程间,也可以不同主机间的进程间通信。

2.2 线程间

临界区 通过多线程的串行化来访问公共资源或者一段代码,速度快,适合控制数据访问。

互斥量Synchronized/Lock 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。

信号量Semphare 为控制具有有限数量的用于资源而设计,它允许多个线程在同一时刻去访问同一个资源,但需要限制同一时刻访问此资源的最大线程数目。

信号Wait/Notify 通过通知操作的方式来保持多线程同步,还可以方便地实现多线程优先级的比较操作。

3. 虚拟内存

  目的是为了防止不同进程同一时刻在物理内存中运行而对物理内存进行争夺和践踏。虚拟内存为每个进程都营造了一种运行时独自占有当前系统内存的假象。

  事实上,每个进程创建加载时,内核只是为进程创建了虚拟内存的布局(即初始化进程控制表中内存相关的链表),实际不立即把虚拟内存对应位置的程序数据和代码拷贝到物理内存中,只是建立好虚拟内存和磁盘文件的映射。等运行到对应程序时,才会通过缺页异常来拷贝数据。动态内存分配比如malloc也只是分配了虚拟内存,即为这块虚拟内存对应的页表项进行设置,等进程真正访问到此数据时,才引发缺页异常。

  使用虚拟内存的好处在于:

  • 扩大地址空间
  • 进程运行在各自的虚拟内存地址空间,互不干扰;并且虚存还对特定的内存地址提供保护,可以防止代码或数据被恶意篡改
  • 公平内存分配,每个进程都有同样大小的虚存空间
  • 进程通信可采用虚存共享的方式实现
  • 当不同的进程使用同样的代码时(比如库文件中的代码),物理内存可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去以节省内存
  • 适用于多道程序设计系统,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用,提高系统并发度
  • 程序需要分配连续的内存空间时,只需要在虚拟内存空间分配连续空间而不需要实际物理内存的连续空间,可以利用碎片

  使用虚拟内存的代价有:

  • 虚存的管理需要建立额外的数据结构,占据内存
  • 虚拟地址到物理地址的转换,增加了指令的执行时间
  • 页面的换入换出需要磁盘IO,耗时
  • 如果一页中只有一部分数据,会浪费内存

4. 缺页中断

  malloc和mmap等内存分配函数分配时知识建立了进程的虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。

  在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在与内存中,若不在,就触发一次缺页中断,此时操作系统根据页表中的外存地址在外存中找到所缺的一页将其调入内存。

  缺页中断和其他中断一样,需要四个步骤:

  • 保护CPU现场
  • 分析中断原因
  • 转入缺页中断处理程序进行处理
  • 恢复CPU现场,继续执行

  但缺页中断也有独特之处,它是由硬件产生:

  • 在指令执行期间产生和处理缺页中断信号
  • 一条指令在执行期间可能产生多次缺页中断
  • 缺页中断返回时,执行产生中断的一条指令(一般中断返回时执行下一条指令)

5. fork()和vfork()

5.1 fork()

  创建一个和当前进程一样的进程,两个进程都会继续运行。在子进程中,成功的fork()调用返回0,父进程中成功的fork()调用返回pid。失败则返回负值。

  常见用法是创建一个新的进程然后使用exec()载入二进制映像进行替换,这样纠缠色很难过了新的进程,即派生+执行。

int main(void)
{
pid_t pid;
signal(SIGCHLD, SIG_IGN);
printf("before fork pid:%d\n", getpid());
int abc = 10;
pid = fork();
// if-else if-else对应pid<0,>0,=0三种情况

  现代Unix系统在调用fork()时不再把所有内部数据结构复制一份然后逐页复制到子进程的地址空间,而是采用了写时复制的方法。

写时复制

  惰性优化,若多个进程要读取它们自己的那部分资源的副本时,每个进程保存一个指向该资源的指针即可,不用复制。如果某一进城要修改那份资源时,才复制给该进程。

  在使用虚拟内存的情况下,写时复制以页为基础。在内核中,与内核页相关的数据结构标记为只读和写时复制,如果由进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断的方式就是对该页进行一次透明复制(过程中清除COW属性,表示不再被共享)

5.2 vfork()

  vfork()调用,会挂起父进程直到子进程终止或者运行了一个新的可执行文件的映像,以避免地址空间的按页复制,但是fork()+写时复制虽然没有vfork()快,但是也足够用了,现在vfork基本都是封装过的fork()。

5.3 fork() v.s. vfork()
  • fork()的子进程拷贝父进程的数据段和代码段,vfork()的子进程与父进程共享数据段
  • fork()的父子进程的执行次序不确定;vfork()保证子进程先运行,在调用exec或exit前与父进程的数据是共享的,调用之后父进程才可能被调度运行。如果调用之前子进程依赖父进程的进一步动作,会导致死锁
  • vfork()当需要改变共享数据中变量的值,则拷贝父进程。

6. 修改文件最大句柄数

  Linux默认最大文件句柄数是1024。当文件并发量较大时会报“too many open files”,修改文件最大句柄数的方法:

  • ulimit -n 2048,但是只对当前进程有效
  • vi /etc/security/limits.conf添加:
*soft nofile 65536
*hard nofile 65536

  然后保存、注销、重新登陆。

7. 并行和并发

  并行:严格物理意义上的同时运行,比如两个程序分别运行在两个核上互不影响。

  并发:宏观上两个程序同时运行,实际上两个程序交织运行,并发不能提供计算机性能,只能提高效率。

8. 页表寻址

  页式内存管理,内存分成固定长度的一个个页片,操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构称为页表。页表的每一项记录了这个页的基地址,通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度有逻辑地址的低位决定。一般情况下该过程由硬件完成。页式内存管理的有点在于灵活,方便内存的换入换出和地址空间扩充(因为以较小的页为单位)

8.1 两级页表

  32位虚拟空间分成三段,低十二位表示页内偏移,高20分成两段,表示两级页表的偏移:

  • Page Global Directory - PGD:最高10位,全局页目录表索引
  • Page Table Entry - PTE:中间10位,页表入口索引
      寻址的过程也就是一级一级深入,称为page table walk。
8.2 三级页表

  高两位-PGD,次高9位-PMD,中间9位-PTE,低12位-页偏移。

  为了兼容二级页表,可以为其虚拟一个中间的PMD。

8.3 四级页表

  PGD:47-39,PUD:38-20,PMD:29-21,PTE:20-12,页偏移:11-0

8.4 TLB

  四级页表需要五次内存IO才能获取一个数据,这样TLB:旁路转换缓存,即将页表尽可能的缓存起来,以备内存管理单元获取然后返回查找对应数据返回CPU。

9. 线程的存在意义

  进程有缺点:

  • 同一时间只能做一件事
  • 执行中被阻塞,整个进程就会挂起,即使进程中有些工作不依赖等待的资源

  线程的优势在于:

  • 创建和销毁代价小
  • 切换效率高
  • 通信快捷
  • 能够使多CPU系统更加有效(操作系统保证当线程数不大于CPU数目时,不多的线程运行与不同的CPU上)
  • 改善程序结构,复杂进程分成独立半独立线程,有利于理解和修改但是不好调试

10. 线程切换

  线程切换过程中需要保存当前线程ID、线程状态、堆栈、寄存器状态等信息,其中寄存器主要包括SP、PC和EAX等:

  • SP:堆栈指针,指向当前栈的栈顶地址
  • PC:程序计数器,存储下一条要执行的指令
  • EAX:累加寄存器,用于加法乘法的缺省寄存器

11. 线程同步

11.1 信号量

  信号量是一种特殊的变量,可用于线程同步。它只取自然数值,并且只支持两种操作:

  • P(SV):如果信号量SV大于0,将它减一;如果SV值为0,则挂起该线程。
  • V(SV):如果有其他进程因为等待SV而挂起,则唤醒,然后将SV+1;否则直接将SV+1。

  系统调用为:

//原子操作信号量减1;若为0则被阻塞直到有非0值
sem_wait(sem_t *sem)
//原子操作信号量加1;若大于0唤醒正在调用sem_wait的线程
sem_post(sem_t *sem)
11.2 互斥量

  当线程进入临界区,需要获得互斥锁并加锁;离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程,系统调用:

pthread_mutex_init//初始化互斥锁
pthread_mutex_destroy//销毁互斥锁
pthread_mutex_lock//原子操作加锁,若已锁则阻塞
pthread_mutex_unlock//原子操作解锁
11.3 条件变量

  又称条件锁,当某个共享数据达到某个值,唤醒等待这个共享数据的一个或多个线程,主要系统调用:

pthread_cond_init//初始化条件变量
pthread_cond_destroy//销毁条件变量
pthread_cond_signal//唤醒一个等待目标变量的线程(取决于优先级和调度策略)
pthread_cond_wait//等待目标天剑变量
11.4 临界区

  通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。

12. 缺页置换算法/缓存置换算法

12.1 OPT:最佳置换算法

  淘汰以后永不使用或者最长时间内不再被访问的页面,这样就能保证最低的缺页率,但是未来的情况无法预知所以无法实现,但是可以用做一个标准评价其他算法。
在这里插入图片描述

12.2 FIFO:页面置换算法

  优先淘汰最早进入内存的页面。但是会出现分配物理块增加但是故障数也增加的Belady异常。
在这里插入图片描述

12.3 LRU:最近最久未使用置换算法

  选择最近最长时间未被访问过的页面淘汰,该算法为每个页面设置一个访问字段,记录该页面字自上次被访问依赖经历的时间,淘汰值最大的。其性能接近OPT但是实现困难开销大。

  有两个问题,一是缓存颠簸【当缓存(1,2,3)满了,之后数据访问(0,3,2,1,0,3,2,1。。。)】,二是缓存污染【突然的偶发性数据访问会让内存中存放大量的冷数据】。
在这里插入图片描述
  改进,LRU-K,最久未使用K次淘汰算法,只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。实现:

  • 数据第一次被访问,加入到访问历史列表
  • 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰
  • 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序
  • 缓存数据队列中被再次访问后,重新排序;
  • 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。
12.4 Clock:时钟置换算法

  简单的Clock算法给每一页设置一个访问位,当页面被访问时,访问位置1,选择淘汰时检查页的访问位,若是0,则换出,若是1,则设置为0,暂不换出,给该页二次驻留内存的机会,又称NRU算法。

  还可以在此基础上增加一个修改位M,每一页就会有四种情况:

  • 最近未被访问,也未被修改(A=0, M=0)
  • 最近被访问,但未被修改(A=1, M=0)
  • 最近未被访问,但被修改(A=0, M=1)
  • 最近被访问,被修改(A=1, M=1)

  算法步骤:

  • 从指针的当前位置开始,扫描循环队列。在这次扫描过程中,对使用位不做任何修改。选择遇到的第一个(A=0, M=0)页,则进行置换
  • 如果第1步失败,则重新扫描,查找(A=0, M=1)页。选择遇到的第一个这样的页则置换。在这个扫描过程中,对每个跳过的页,把它的使用位设置成0
  • 如果第2步失败,指针将回到它的最初位置,并且集合中所有页的使用位均为0。重复第1步,并且如果有必要,重复第2步。这样将可以找到供置换的页
12.5 LFU:最不经常访问淘汰算法

  数据过去被访问很多次,那认为其将来也会被访问很多次。实现时每个数据块一个引用计数,所有数据块按照引用计数排序,相同技术则按照时间排序,每次淘汰队尾。

13. 多进程和多线程的使用场景

  多线程切换效率高,适用于IO密集型工作场景,并且适用于单机多核分布式场景;多进程模型适用于CPU密集型,也适用于多机分布式场景中,易于多机扩展。

14. 死锁

14.1 发生条件

  死锁指两个或两个以上进程执行过程中,因争夺资源而造成的互相等待现象,四个必要条件:

  • 互斥:进程分配到的资源不允许其他进程访问
  • 请求和保持:进程已获得资源并对其他资源发出请求,若资源被占有,此时请求阻塞,但该进程不会释放自己已经占有的资源
  • 不可剥夺:进程已获得资源未完成使用之前不可被剥夺
  • 环路等待:存在进程-资源的环形链
14.2 解决方法
  • 资源一次性分配
  • 可剥夺资源:未得到满足就释放
  • 资源有序分配-每类资源赋予一个序号,进程按编号递增的顺序请求资源,释放则相反
14.3 银行家算法

https://blog.csdn.net/qq_36260974/article/details/84404369

15. 内存对齐

https://mp.weixin.qq.com/s/pMnMBlzrwM1VrILpB4dT8g

16. Linux中的四种锁

16.1 互斥锁

  mutex,用于保证在任何时刻,都只能有一个线程访问该对象,当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。

16.2 读写锁

  rwlock,分为读锁和写锁,出于读操作时,可以允许多个线程同时获得读锁,但是同一时刻只能有一个线程获得写锁,其他获取写锁失败的线程都会进入睡眠状态,知道写锁释放被唤醒。而且写锁也会阻塞后面的读锁。

16.3 自旋锁

  spinlock,任何时刻只有一个线程访问对象,获取锁失败后不进入睡眠状态而是原地自旋,这样节省了唤醒的消耗在加锁时间短暂的环境下可极大提升效率,但如果加锁时间过长,则会浪费CPU资源。

16.4 RCU

  read-copy-update,修改数据时,首先读取数据生成一个副本,对副本修改。修改完成后,再将老数据替换。使用RCU时读者不需要获得锁,但是写者同步开销大(还需要使用锁机制同步并行其他写者的修改操作)。适用于读多写少的环境。

17. 进程状态转换

在这里插入图片描述

17.1 五种基本状态
  • 1)创建状态:进程正在被创建
  • 2)就绪状态:进程被加入到就绪队列中等待CPU调度运行
  • 3)执行状态:进程正在被运行
  • 4)等待阻塞状态:进程因为某种原因,比如等待I/O,等待设备,而暂时不能运行
  • 5)终止状态:进程运行完毕
17.2 交换技术

  当多个进程竞争内存资源时会造成内存资源紧张,并且如果此时没有就绪进程,CPU空闲,而IO操作比CPU要慢很多,可能出现全部进程阻塞等待IO。为解决该问题:

  • 交换技术:换出一部分进程到外存,腾出内存空间
  • 虚拟存储技术:每个进程只装入一部分程序核和数据

  交换技术中,将内存暂时不能运行的进程或者咱叔不用的数据和程序换出到外存,来腾出足够的内存空间,把已经具备运行条件的进程或进程所需的数据和程序换入到内存,从而出现了进程的挂起状态:进程被交换到外存时的进程状态。
  这样又出现了四种新的状态:

  • 活动阻塞:进程在内存,但是由于某种原因被阻塞了
  • 静止阻塞:进程在外存,同时被某种原因阻塞了
  • 活动就绪:进程在内存,处于就绪状态,只要给CPU和调度就可以直接运行
  • 静止就绪:进程在外存,处于就绪状态,只要调度到内存,给CPU和调度就可以运行。

  三种新的状态转换:

  • 活动就绪—静止就绪(内存不够,调到外存)
  • 活动阻塞—静止阻塞(内存不够,调到外存)
  • 执行—静止就绪(时间片用完)

18. 程序内存管理

在这里插入图片描述
  一个可执行程序在存储时(未调入内存)时分为代码段、数据区和未初始化数据区三部分:
BSS段(未初始化数据区):Block Started by Symbol,通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域,BSS段属于静态分配,程序结束后静态变量资源由系统自动释放。内容并不存放在磁盘的程序文件,因为内核在程序开始运行前将他们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。
数据段/data段:存放程序中已初始化的全局变量的一块内存区域,数据段也属于静态内促分配。
代码段:存放程序执行代码的一块内存区域,这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读,也有可能包含一些只读的常数变量。
  一个可执行程序在运行时还有两个区域:
栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用,该函数的返回类型和一些调用的信息被存放到栈中,这个被调用的函数再为他的自动变量和临时变量再分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的 malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
  比如执行A *a=new A;a->i=10;:

  • A * a:a是一个局部变量,类型为指针,故而操作系统在程序栈区开辟4/8字节的空间(0x000m)分配给指针a
  • new A:通过new动态地在兑取申请类A大小的空间(0x000n)
  • a=new A:将指针a的内存区域填入栈中类A申请到的地址的地址:即*(0x000m)=0x000n
  • a->i:找到指针a的地址0x000m,通过a的值0x000n和i在类A中偏移offset,得到a->i的地址为0x000n+offset,进行*(0x000n+offset)=10的操作。即内存0x000n+offset的值为10。

19. static关键字

https://mp.weixin.qq.com/s/-0sb_VZCVUai0DNitM_ppw

20. virtual

  C++多态分为静态多态和动态多态,前者通过重载和模板技术实现,在编译的时候确定;后者通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。

  动态多态实现需要:

  • 虚函数
  • 一个基类的指针或引用指向派生类的对象

  基类指针在调用成员函数时,就会查找该对象的虚函数表,虚函数表在每个对象的首地址,查找该虚函数表中该函数的指针进行调用。

  每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。

  虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。

  如果一个类是局部变量则该类数据存储在栈区,如果一个类是通过new/malloc动态申请的,则该类数据存储在堆区。如果该类是virutal继承而来的子类,则该类的虚函数表指针和该类其他成员一起存储。虚函数表指针指向只读数据段中的类虚函数表,虚函数表中存放着一个个函数指针,函数指针指向代码段中的具体函数。如果类中成员是virtual属性,会隐藏父类对应的属性。
在这里插入图片描述

21.软链接和硬链接

  为了解决文件共享问题,Linux引入了软链接和硬链接。除了为Linux解决文件共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若1个inode号对应多个文件名,则为硬链接,即硬链接就是同一个文件使用了不同的别名,使用ln创建【可以防止误删】。若文件用户数据块中存放的内容是另一个文件的路径名指向,则该文件是软连接。软连接是一个普通文件,有自己独立的inode,但是其数据块内容比较特殊【类似于快捷方式,便于文件的管理,节省空间解决空间不足问题】。

22. 用户态和内核态

  用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。内核态和用户态之间的转换方式主要包括:系统调用,异常和中断。

系统调用

  用户进程通过系统调用申请操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的ine 80h中断。

异常
  当CPU在执行运行在用户态的程序时,发现了某些事件不可知的异常,这是会触发由当前运行进程切换到处理此。异常的内核相关程序中,也就到了内核态,比如缺页异常。

外围设备的中断

  当外围设备完成用户请求的操作之后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条将要执行的指令,转而去执行中断信号的处理程序,如果先执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了有用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

22.1 切换操作

  从出发方式看,可以在认为存在前述3种不同的类型,但是从最终实际完成由用户态到内核态的切换操作上来说,涉及的关键步骤是完全一样的,没有任何区别,都相当于执行了一个中断响应的过程,因为系统调用实际上最终是中断机制实现的,而异常和中断处理机制基本上是一样的,用户态切换到内核态的步骤主要包括:

  • 从当前进程的描述符中提取其内核栈的ss0及esp0信息。
  • 使用ss0和esp0指向的内核栈,将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
  • 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。

23. 源码到可执行程序

1.预编译

  预编译过程主要处理源代码文件中“#”开头的预编译指令,处理规则:

  • 删除所有的#define,展开所有的宏定义。
  • 处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
  • 处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
  • 删除所有的注释,“//”和“/**/”。
  • 保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
  • 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。

2.编译
  把预编译之后生成的xxx.i或xxx.ii文件进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件:

  • 词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
  • 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
  • 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
  • 优化:源代码级别的一个优化过程。
  • 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
  • 目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。

3.汇编

  将汇编代码转变成机器可以执行的指令(机器码文件)。汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。

4.链接

  将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接。

  • 静态链接:

  函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。

  空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;

  更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

  运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

  • 动态链接:

  动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

  共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本

  更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

  性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

  内部链接和外部链接https://mp.weixin.qq.com/s/WsJ4lc7K1DXIUXmsDfDZQw

24. 内存溢出和内存泄漏

内存溢出

  指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。

  原因:

  • 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
  • 集合类中有对对象的引用,使用完后未清空,使得不能回收
  • 代码中存在死循环或循环产生过多重复的对象实体
  • 使用的第三方软件中的BUG
  • 启动参数内存值设定的过小

内存泄漏

  内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

  分类:

  • 堆内存泄漏 (Heap leak):堆内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak
  • 系统资源泄露(Resource Leak):主要指程序使用系统分配的资源比如 Bitmap,handle,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定
  • 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

25. 常用线程模型

25.1 Future模型

  该模型通常在使用的时候需要结合Callable接口配合使用。

  Future是把结果放在将来获取,当前主线程并不急于获取处理结果。允许子线程先进行处理一段时间,处理结束之后就把结果保存下来,当主线程需要使用的时候再向子线程索取。

  Callable是类似于Runnable的接口,其中call方法类似于run方法,所不同的是run方法不能抛出受检异常没有返回值,而call方法则可以抛出受检异常并可设置返回值。两者的方法体都是线程执行体。

25.2 fork&join模型

  该模型包含递归思想和回溯思想,递归用来拆分任务,回溯用合并结果。可以用来处理一些可以进行拆分的大任务。其主要是把一个大任务逐级拆分为多个子任务,然后分别在子线程中执行,当每个子线程执行结束之后逐级回溯,返回结果进行汇总合并,最终得出想要的结果。

  这里模拟一个摘苹果的场景:有100棵苹果树,每棵苹果树有10个苹果,现在要把他们摘下来。为了节约时间,规定每个线程最多只能摘10棵苹树以便于节约时间。各个线程摘完之后汇总计算总苹果树。

25.3 actor模型

  actor模型属于一种基于消息传递机制并行任务处理思想,它以消息的形式来进行线程间数据传输,避免了全局变量的使用,进而避免了数据同步错误的隐患。actor在接受到消息之后可以自己进行处理,也可以继续传递(分发)给其它actor进行处理。在使用actor模型的时候需要使用第三方Akka提供的框架。

25.4 生产者消费者模型

  生产者消费者模型都比较熟悉,其核心是使用一个缓存来保存任务。开启一个/多个线程来生产任务,然后再开启一个/多个来从缓存中取出任务进行处理。这样的好处是任务的生成和处理分隔开,生产者不需要处理任务,只负责向生成任务然后保存到缓存。而消费者只需要从缓存中取出任务进行处理。使用的时候可以根据任务的生成情况和处理情况开启不同的线程来处理。比如,生成的任务速度较快,那么就可以灵活的多开启几个消费者线程进行处理,这样就可以避免任务的处理响应缓慢的问题。

25.5 master-worker模型

  master-worker模型类似于任务分发策略,开启一个master线程接收任务,然后在master中根据任务的具体情况进行分发给其它worker子线程,然后由子线程处理任务。如需返回结果,则worker处理结束之后把处理结果返回给master。

26. 线程的状态

新建状态

  使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

就绪状态

  当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

运行状态

  如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

阻塞状态

  如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

死亡状态

  一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程休眠sleep的目的是使线程让出CPU的最简单的做法之一,线程休眠时候,会将CPU资源交给其他线程,以便能轮换执行,当休眠一定时间后,线程会苏醒,进入就绪状态等待执行。

当线程等待wait时,该线程锁定的资源是释放掉的,这时其它线程是可以锁定这些资源的,当线程被唤醒或者等待时限到时线程重新获取资源才能继续运行;而当线程休眠时线程锁定的资源是不被释放的。

27. 协程

  协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。
在这里插入图片描述
  协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。

  协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。

  一个进程可以包含多个线程,一个线程也可以包含多个协程。简单来说,一个线程内可以由多个这样的特殊函数在运行,但是有一点必须明确的是,一个线程的多个协程的运行是串行的。如果是多核CPU,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内协程却绝对是串行的,无论CPU有多少个核。毕竟协程虽然是一个特殊的函数,但仍然是一个函数。一个线程内可以运行多个函数,但这些函数都是串行运行的。当一个协程运行时,其它协程必须挂起。

28. 微内核与宏内核

28.1 微内核

  内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。优点:稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃。缺点:效率低。典型代表QNX,QNX的文件系统是跑在用户态的进程,称为resmgr的东西,是订阅发布机制,文件系统的错误只会导致这个守护进程挂掉。不过数据吞吐量就比较不乐观了。

28.2 宏内核

  除了最基本的进程、线程管理、内存管理外,将文件系统,驱动,网络协议等等都集成在内核里面,例如linux内核。优点:效率高。缺点:稳定性差,开发过程中的bug经常会导致整个系统挂掉。

29. 僵尸进程

  正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

  unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息,直到父进程通过wait / waitpid来取时才释放。保存信息包括:

  • 进程号the process ID
  • 退出状态the termination status of the process
  • 运行时间the amount of CPU time taken by the process等

  一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

  一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

  僵尸进程是一个进程必然会经过的过程:这是每个子进程在结束时都要经过的阶段。

  如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。

  如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

  如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

  通过kill发送SIGTERM或者SIGKILL信号消灭产生僵尸进程的进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源。

  也可以:

  • 子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
  • fork两次,原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。具体地,父进程一次fork()后产生一个子进程随后立即执行waitpid(子进程pid, NULL, 0)来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵尸进程了。

30. IO模型

30.1 阻塞IO

  调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作

30.2 非阻塞IO

  非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。

30.3 信号驱动IO

  信号驱动IO:linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号。然后处理IO事件。

30.4 IO复用/多路转接IO

  linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数

30.5 异步IO

  linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

31. select、poll、epoll的区别

https://www.cnblogs.com/aspirant/p/9166944.html

32. 线程池的实现

  • 设置一个生产者消费者队列,作为临界资源
  • 初始化n个线程,并让其运行起来,加锁去队列取任务运行
  • 当任务队列为空的时候,所有线程阻塞
  • 当生产者队列来了一个任务后,先对队列加锁,把任务挂在到队列上,然后使用条件变量去通知阻塞中的一个线程

33. 高并发缓存问题

33.1 缓存一致性

  当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象。这就比较依赖缓存的过期和更新策略。一般会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存。
在这里插入图片描述

33.2 缓存并发问题

  缓存过期后将尝试从后端数据库获取数据,这是一个看似合理的流程。但是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 “雪崩”现象。

  此外,当某个缓存key在被更新时,同时也可能被大量请求在获取,这也会导致一致性的问题。那如何避免类似问题呢?我们会想到类似“锁”的机制,在缓存更新或者过期的情况下,先尝试获取到锁,当更新或者从数据库获取完成后再释放锁,其他的请求只需要牺牲一定的等待时间,即可直接从缓存中继续获取数据。

在这里插入图片描述

33.3 缓存穿透问题

  在高并发场景下,如果某一个key被高并发访问,没有被命中,出于对容错性考虑,会尝试去从后端数据库中获取,从而导致了大量请求达到数据库,而当该key对应的数据数据库本身就没有时(或者恶意访问不存在的数据),将导致数据库中并发的去执行了很多不必要的查询操作,从而导致巨大冲击和压力,解决方法:

  • 缓存空对象

  对查询结果为空的对象也进行缓存,如果是集合,可以缓存一个空的集合(非null),如果是缓存单个对象,可以通过字段标识来区分。这样避免请求穿透到后端数据库。同时,也需要保证缓存数据的时效性(设置较短的过期时间)。这种方式实现起来成本较低,比较适合命中不高,但可能被频繁更新的数据。

  • 单独过滤处理

  对所有可能对应数据为空的key进行统一的存放,并在请求前做拦截,这样避免请求穿透到后端数据库。这种方式实现起来相对复杂,比较适合命中不高,但是更新不频繁的数据。

  与之类似的问题还有一个缓存击穿,指的是当一个cache中的key并发访问量非常大形成热点,当这个key过期之后,持续的大并发会跳过缓存,直接访问DB,造成DB的压力.解决这个问题可以对热点的key通过程序实现自动转为永不过期缓存,可以在value中设置超时时间,程序内部校验是否过期,如果过期则异步发起一个县城更新缓存.

33.4 缓存颠簸

  缓存的颠簸问题,有些地方可能被成为“缓存抖动”,可以看做是一种比“雪崩”更轻微的故障,但是也会在一段时间内对系统造成冲击和性能影响。一般是由于缓存节点故障导致。业内推荐的做法是通过一致性Hash算法来解决。

33.5 缓存雪崩

  缓存雪崩就是指由于缓存的原因,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难。

  导致这种现象的原因有很多种,上面提到的“缓存并发”,“缓存穿透”,“缓存颠簸”等问题,其实都可能会导致缓存雪崩现象发生。这些问题也可能会被恶意攻击者所利用。

  还有一种情况,例如某个时间点内,系统预加载的缓存周期性集中失效了,也可能会导致雪崩。为了避免这种周期性失效,可以通过设置不同的过期时间,来错开缓存过期,从而避免缓存集中失效。

  从应用架构角度,我们可以通过限流、降级、熔断等手段来降低影响,也可以通过多级缓存来避免这种灾难。

  此外,从整个研发体系流程的角度,应该加强压力测试,尽量模拟真实场景,尽早的暴露问题从而防范。

33.6 缓存无底洞

  加节点来缓存更多的内容无法降低访问数据库的频率。

33.7 缓存污染

https://segmentfault.com/a/1190000018810255

  扩展-共享缓存
  当仅仅依赖数据库和本地缓存已经无法满足访问量时,也就是IO能力不足的时候,当然可以直接增加内存解决问题.但是Web站点服务器通常不是IO为主的机器,而数据库服务器才是IO密集型服务器.如果只是在Web服务器上增加内存,虽然可以解决一时的IO紧张,但是Web服务一旦重启,缓存被清空,那么所有的请求都将会去数据库服务器请求数据,那么数据库直接垮掉,网站崩溃.
  而且Web服务器是CPU密集型服务器,如果一台计算机顶不住或者连接数顶不住的时候,势必需要增加web服务器,这时候,缓存建立在哪台服务器上也是一个问题.
  所以最好的方法就是吧缓存独立出来,搞一个共享缓存服务器.贡献缓存服务器的时间代价主要是机器之间的通信,虽然相比本地内存拿数据慢了很多,而且还需要对传输的数据再处理,但是成本低而且再慢也比直接从数据库快,毕竟数据库还得除了需要网络传输外还需要磁盘扫描.
  

34. 脏读、不可重复读、幻读

脏读

  (针对未提交数据)如果一个事务中对数据进行了更新,但事务还没有提交,另一个事务可以“看到”该事务没有提交的更新结果,这样造成的问题就是,如果第一个事务回滚,那么,第二个事务在此之前所“看到”的数据就是一笔脏数据。

不可重复读

  (针对其他提交前后,读取数据本身的对比)不可重复读取是指同一个事务在整个事务过程中对同一笔数据进行读取,每次读取结果都不同。如果事务1在事务2的更新操作之前读取一次数据,在事务2的更新操作之后再读取同一笔数据一次,两次结果是不同的,所以,Read Uncommitted也无法避免不可重复读取的问题。【重点是修改】

幻读

  (针对其他提交前后,读取数据条数的对比) 幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。在Read Uncommitted隔离级别下, 不管事务2的插入操作是否提交,事务1在插入操作之前和之后执行相同的查询,取得的结果集是不同的,所以,Read Uncommitted同样无法避免幻读的问题。【重点是条数】

35. 乐观锁和悲观锁

35.1 悲观锁

  悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。

  主要分为共享锁或排他锁:

  • 共享锁【Shared lock】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁【Exclusive lock】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
35.2 乐观锁

  乐观锁机制采取了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

  CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

  ABA问题:比如说一个线程one从数据库中取出库存数3,这时候另一个线程two也从数据库中取出库存数3,并且two进行了一些操作变成了2,然后two又将库存数变成3,这时候线程one进行CAS操作发现数据库中仍然是3,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

  有一个比较好的办法可以解决ABA问题,那就是通过一个单独的可以顺序递增的version字段。

36. CPU调度算法

36.1 先到先服务调度算法(FCFS)

  根据就绪队列的到达时间来服务,此时就绪队列是一个FIFO队列,先到先服务,后到的线程不能抢占前面正在服务的线程。这种算法的优点是实现简单,缺点也很明显,就是CPU进程区间变化很大时,平均等待时间会变化很大。

36.2 最短作业优先调度(SJF)

  顾名思义,就是CPU进程区间最短的先执行,如果两个进程区间具有同样的长度,那么按照FCFS来调度。SJF可以是抢占的,也可以是不抢占的。它的平均等待时间优于FCFS。

36.3 优先级调度

  其实上面的SJF算法就是一种特殊的优先级调度,只不过这里的优先级定义更加广泛一些,SJF算法的优先级是按照CPU进程区间长短来定义的,这里的优先级可以是其他的一些定义。优先级调度可以是抢占的,也可以是非抢占的。优先级调度的一个主要问题是无穷阻塞(也称为饥饿),如果一个线程的优先级很低,可能需要等待很长的时间才能到这个线程执行,甚至永远不执行,一种解决方法是老化(随着时间的增长,增加线程的优先级),另一种是高响应比优先:选择响应比最高的:响应比Rp = (等待时间+要求服务时间) / 要求服务时间

36.4 轮转法调度(RR)

  轮转法调度专门是为分时系统设计的。它类似于FCFS,但是增加了抢占为了切换线程。定义一个较小的时间单元,称为时间片,通常为10-100ms。为了实现RR算法,将就绪队列保存为FIFO队列,新进程增加到就绪队列队尾,CPU调度程序从就绪队列选择第一个进程,设置定时器在一个时间片之后再中断,再分派这个进程。如果该进程的CPU区间小于时间片,进程本身就会释放CPU,调度程序继续处理下一个进程,如果当前进程的CPU区间比时间片长,定时器会产生CPU中断,实行上下文切换,然后将此进程放到就绪队列队尾,继续调度就绪队列第一个进程。

36.5 多级队列调度:

  这里对进程进行分组,在组内使用FCFS和SJF算法,在组间实行优先级调度或者轮转法调度。但是不允许进程在组间切换。

36.6 多级反馈队列调度

  允许进程在组间切换,主要思想是根据不同区间的特点区分进程,如果CPU进程占用过多CPU时间,那么它会被转移到更低优先级队列。这种形式老化阻止饥饿。

37. 磁盘调度算法

37.1 先来先服务算法(FCFS)First Come First Service

  这是一种比较简单的磁盘调度算法。它根据进程请求访问磁盘的先后次序进行调度。此算法的优点是公平、简单,且每个进程的请求都能依次得到处理,不会出现某一进程的请求长期得不到满足的情况。此算法由于未对寻道进行优化,在对磁盘的访问请求比较多的情况下,此算法将降低设备服务的吞吐量,致使平均寻道时间可能较长,但各进程得到服务的响应时间的变化幅度较小。

  先来先服务 (125)86.147.91.177.94.150.102.175.130

37.2 最短寻道时间优先算法(SSTF) Shortest Seek Time First

  该算法选择这样的进程,其要求访问的磁道与当前磁头所在的磁道距离最近,以使每次的寻道时间最短,该算法可以得到比较好的吞吐量,但却不能保证平均寻道时间最短。其缺点是对用户的服务请求的响应机会不是均等的,因而导致响应时间的变化幅度很大。在服务请求很多的情况下,对内外边缘磁道的请求将会无限期的被延迟,有些请求的响应时间将不可预期。

  最短寻道时间优先(125)130.147.150.175.177.102.94.91.86

37.3 扫描算法(SCAN)电梯调度

  扫描算法不仅考虑到欲访问的磁道与当前磁道的距离,更优先考虑的是磁头的当前移动方向。例如,当磁头正在自里向外移动时,扫描算法所选择的下一个访问对象应是其欲访问的磁道既在当前磁道之外,又是距离最近的。这样自里向外地访问,直到再无更外的磁道需要访问才将磁臂换向,自外向里移动。这时,同样也是每次选择这样的进程来调度,即其要访问的磁道,在当前磁道之内,从而避免了饥饿现象的出现。由于这种算法中磁头移动的规律颇似电梯的运行,故又称为电梯调度算法。此算法基本上克服了最短寻道时间优先算法的服务集中于中间磁道和响应时间变化比较大的缺点,而具有最短寻道时间优先算法的优点即吞吐量较大,平均响应时间较小,但由于是摆动式的扫描方法,两侧磁道被访问的频率仍低于中间磁道。

  电梯调度(125)102.94.91.86.130.147.150.175.177

37.4 循环扫描算法(CSCAN)

  循环扫描算法是对扫描算法的改进。如果对磁道的访问请求是均匀分布的,当磁头到达磁盘的一端,并反向运动时落在磁头之后的访问请求相对较少。这是由于这些磁道刚被处理,而磁盘另一端的请求密度相当高,且这些访问请求等待的时间较长,为了解决这种情况,循环扫描算法规定磁头单向移动。例如,只自里向外移动,当磁头移到最外的被访问磁道时,磁头立即返回到最里的欲访磁道,即将最小磁道号紧接着最大磁道号构成循环,进行扫描。

  循环扫描 (125)130.147.150.175.177.86.91.94.102

38. 内存分配

38.1 连续分配

  用户进程在主存中连续存放,但会产生内部碎片,有几种分配方法:

38.1.1 单一连续分配

  分配到内存固定区域,只适合单任务系统;

38.1.2 固定分区分配

  分配到内存中不同的固定区域,分区可以相等也可以不等,可能产生内部碎片

38.1.3 动态分区分配:

  基本概念:按照程序的需要进行动态的划分,常用分配算法:

  • 首次适应:空闲区按地址从小到大为序,分配第一个符合条件的分区;
  • 最佳适应:空闲区按空间大小从小到大排序,分配第一个符合条件的分区;
  • 最坏适应:空闲区按空间从大到小排序,分配第一个符合条件的分区;
  • 邻近适应:空闲区按地址地址递增的次序排列,分配内存时从上次查找结束的位置开始继续查找
38.2 非连续分配

  允许一个程序分散地装入到不相邻的内存分区中,需要额外的空间去存储分散区域的索引。

38.2.1 基本分页

  内存分为固定的块,按物理结构划分,会有内部碎片:

  • 主存、进程都划分为大小固定的块,进程在执行时,以块为单位申请主存中的块空间
  • 进程中的块为页,内存中的块为页框。系统为每个进程建立一张页表,页表记录页面在内存中对应的物理块号,实现从页号到物理块号的地址映射
  • 页式管理中地址空间是一维的;
38.2.2 基本分段

  内存块的大小不固定,按逻辑结构划分,会有外部碎片:

  • 段式管理方式按照用户进程中的自然段划分逻辑空间。段内要求连续,段间不要求连续。段号和段内偏移量必须由用户显示提供。
  • 方便编程、共享、保护、动态链接和增长。
38.2.3 段页式

  基本分段和基本分页的结合,会有内部碎片:

  • 作业的逻辑地址分为:段号、页号和页内偏移量;采用分段方法来分配和管理用户地址空间,采用分页方法来管理物理存储空间;开销大。
38.2.4 请求分页存储管理

  采用虚拟技术,开始运行时不必将作业全部一次性装入内存

38.2.5 多级页表

  将页表的10页空间也进行地址映射,建立上一级页表,用于存储页表的映射关系


欢迎扫描二维码关注微信公众号 深度学习与数学   [每天获取免费的大数据、AI等相关的学习资源、经典和最新的深度学习相关的论文研读,算法和其他互联网技能的学习,概率论、线性代数等高等数学知识的回顾]
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值