用户态和内核态

用户态和内核态的区别?

  • 内核态:CPU可以执行所有的指令和访问所有的硬件资源,操作具有更高的权限,主要基于操作系统内核运行
  • 用户态:CPU只能执行部分指令集,无法直接访问硬件资源,操作权限较低,主要用于运行用户程序

为什么要分为用户态和内核态?

  1. 安全性:通过对权限的划分,用户程序无法直接访问硬件资源,避免恶意程序对系统资源造成的破坏
  2. 稳定性:用户程序出现问题,不会影响到整个系统
  3. 隔离性:使得操作系统内核与用户程序之间有明确的边界,有利于系统的模块化和维护。

进程管理

进程和线程的区别?

  1. 进程是操作系统分配资源(虚拟内存、文件句柄、信号量…)的基本单位,线程是任务调度的基本单位
  2. 进程切换会有较大的开销;线程切换开销较小
  3. 进程中如果某个线程崩溃了,可能整个进程都会崩溃
  4. 系统在运行时,会为每个进程分配资源;对线程而言,系统不会为线程分配内存,线程的资源来源于进程

进程、线程、协程的区别?

  1. 进程:操作系统分配资源的基本单位,拥有自己独立的内存空间,每个进程都有独立的堆和栈,不与其他进程共享。稳定性和安全性较高,但是上下文切换的开销较大
  2. 线程:CPU调度的基本单位,线程共享进程的内存空间,线程之间通信更高效,因为它们可以直接读取共享内存。线程上下文切换开销小,因为只需要保存和恢复线程的上下文。
  3. 协程:用户态的轻量级线程,调度完全由用户程序控制,不需要内核的参与。协程与其他协程共享堆内存,协程的切换开销非常小,因为只需要保存和恢复协程的上下文,无需进程内核级的上下文切换。协程在处理大量并发任务时具有非常高的效率,需要程序员显示的进行调度和管理。

多线程和单线程的区别?

  1. 多线程可以提高程序的运行效率,可以充分利用多核处理器的资源,同时处理多个任务,加快程序的执行速度
  2. 但是存在多线程数据竞争的问题,需要通过锁机制来保证线程的安全,增加了加锁开销的同时还可能会出现死锁。多线程消耗系统的资源,因为每个线程都需要占用一定的内存和处理时间。

多线程太多会引起什么问题?

多线程不是越多越好,过多的线程也会导致一定的问题。

  1. 切换开销:线程的创建和销毁会消耗系统资源,如果创建太多线程,会占用大量的系统资源,导致系统负载过高,某个线程崩溃后可能会导致进程的崩溃。
  2. 死锁问题

进程切换和线程切换的区别?

进程切换的开销比线程切换的开销大。

线程共享统一进程的地址空间和资源,线程切换只涉及到线程的堆栈、寄存器、程序计数器等,不涉及进程别的资源,避免了进程切换时需要切换内存映射表等大量资源的开销。

线程切换的步骤

  1. 上下文保存:当一个线程切换到另一个线程时,操作系统会先保存当前线程的上下文信息(程序计数器、堆栈指针、寄存器状态)
  2. 切换调度器:操作系统将执行权切换到调度器,调度器负责选择下一个要执行的线程,并根据调度算法做出决策。
  3. 上下文恢复:调度器选择下一个要执行的线程后,他会从线程保存的上下文信息中恢复线程的执行状态。
  4. 切换到新线程:调度器将执行权切换到新线程,使其开始执行。

TCB用来管理线程的数据结构,当发生线程的切换时,通过切换TCB来保存和恢复线程的上下文

线程的状态(五种)如何切换?

884354026fa1417aaf9f200631fd3e35.jpeg

进程间的通讯方式

  1. 管道:

    • 匿名管道:通信方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要建立两个管道,只能存在父子关系的进程通信(通过pipe系统调用创建)
    • 命名管道:需要在文件系统中创建一个类型为p的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。(基于文件系统)

    对于管道来说,进程写入的数据都是缓存在内核中,另一个进程读取数据时也是从内核中获取

  2. 消息队列:消息队列的消息体可以是用户自定义的数据类型,要求发送方与接收方的消息体的数据类型保持一致,每次数据的写入和读取都需要经过用户态和内核态之间的拷贝过程。

  3. 共享内存:直接分配一个共享内存,每个进程都可以直接访问,不需要陷入内核态和用户态之间的数据拷贝,这是最快的进程通信方式,但是带来了一个新的问题,多进程共同竞争共享数据资源会造成数据的混乱,所以就引入了信号量

    • 信号量不仅可以实现访问的互斥性,还可以实现进程间的同步(P、V操作)

    区别信号量和信号:

    • 信号:异步通信机制,用来通知接收进程有某件事要发生,可以在应用进程和内核之间直接交互,但是这个通信机制是基于一台主机,要与不同主机的进程通信只能使用Socket通信

线程间的通讯方式?

  1. 互斥锁:在访问共享资源时堆互斥量进行加锁,访问完成后释放锁。对共享资源加锁以后,其他试图对这个资源加锁的线程将会被阻塞,直到当前线程释放该互斥锁。

  2. 条件变量:一个线程等待“条件变量的条件成立”而挂起;另一个线程使“条件成立”。一般条件变量总是和互斥锁结合在一起

  3. 自旋锁:在用户态完成加锁和解锁的操作,不会主动产生线程上下文切换,相比互斥锁来说快一点。先检查锁的状态,如果锁是空闲的,就将锁设置成当前线程持有,加锁失败的线程会一直忙等,直到它拿到锁。一般会将这两个步骤合并成一条原子指令,这样就保证这两个步骤是不可分割的

  4. 信号量:信号量表示资源的数量,是一种计数器,用来控制对共享资源的访问,有两个原子操作的系统调用函数来控制信号量(P 、V操作)

  5. 读写锁:读写锁适用于能明确区分读操作和写操作的场景,在读多写少的时候能够发挥出优势。

进程调度算法

先来先服务、短作业优先、高响应比优先、时间片轮转、最高优先级调度、多级反馈队列

为什么并发执行线程需要加锁?

主要是为了保护共享数据,防止出现“竞态条件”。

竞态条件:多个线程同时访问和操作同一块数据时,最终结果依赖于线程的执行顺序,可能会导致数据的不一致。

自旋锁是什么

自旋锁加锁失败后,线程会忙等,直到它拿到锁。在用户态完成加锁和解锁操作,不会产生线程的上下文切换。

先查看锁的状态,如果锁是空闲,就把锁设置成当前线程独有。(把这两条操作合并成一条原子指令)

在单核CPU上,需要不断通过时钟中断一个线程获取其他线程,否则自旋锁在单CPU上无法使用,因为一个自旋的线程永不放弃CPU

当加锁失败后,“互斥锁”用线程切换来应对;“自旋锁”用忙等来应对

如果被锁住的代码执行时间很短,就可以选用自旋锁。

死锁的条件

互斥条件、不可剥夺、循环等待、请求和保持

可以通过使用资源有序分配法来破环循环等待的条件

线程A先获取A资源再获取B资源;线程B也是同样先获取A资源再获取B资源。线程A、B都应该以相同的顺序去获取自己想要的资源

银行家算法

可以有效地避免死锁。

在分配资源前,先判断如果分配完资源后,能否让这个线程执行完后释放它所占用的资源,如果可以满足,则分配资源给该线程。

乐观锁和悲观锁

乐观锁:假设多个事务之间很少发生冲突,在读取数据时不会加锁,在更新数据时检查数据的版本,如果版本匹配则执行更新操作,否则就认为发生冲突(适用于读多写少的场景,可以减少锁的竞争)

悲观锁:假设多个事务之间会频发发生冲突,在读取数据时会加上锁,防止其他事务对数据进行修改,当前事务完成操作后才会释放锁(适用于写多的场景,通过加锁保证数据的一致性)

内存管理

操作系统的内存管理是什么?

操作系统设置了虚拟内存,每个进程都有自己独立的虚拟内存,程序不会直接和物理内存打交道。

  1. 虚拟内存可以使进程对运行内存超过物理内存的大小(因为程序的局部性原理,对于不经常使用的内存,可以把它放到磁盘上)
  2. 每个进程都有自己的页表,每个进程的虚拟内存是独立的,解决了多进程之间地址冲突的问题
  3. 页表里的页表项除了一些物理地址外,还有一些标记属性的比特(比如控制一个页的读写权限,标记该页是否存在)

虚拟地址和物理地址之间是通过页表来进行映射的

页表存储在内存里。内存管理单元(MMU):将虚拟内存地址转化为物理内存地址

什么是虚拟内存,什么是物理内存?

  • 虚拟内存:操作系统给每个运行中的程序的一段地址空间,每个内存在运行时认为自己拥有的内存就是虚拟内存,虚拟内存将程序的地址空间划分成固定大小的页,并将这些页映射到物理内存中的不同位置。
  • 物理内存:计算机实际存在的内存。

什么是页表?

分页是把虚拟内存和物理内存空间切成一段段固定尺寸的大小,虚拟内存和物理内存是通过页表来进行映射的。

页表存储在内存里。

内存管理单元(MMU):将虚拟内存地址转化为物理内存地址

当进程访问的虚拟地址在页表中不存在,就会产生缺页异常,操作系统会进行调页处理。

页和页之间是紧密排列的,不会有外部碎片;但是有时程序不足一页,也会给他分配一页,所以会有内部碎片。

虚拟内存地址转成物理内存地址的步骤:

  1. 当访问一个虚拟地址时,MMU会把虚拟内存地址划分成页号 + 页内偏移量
  2. MMU会根据页号,从页表里查询对应的物理页号
  3. MMU将物理页号+偏移量结合得到物理内存地址

堆和栈的区别?

  • 分配方式:堆是动态分配内存,由程序员手动申请和释放,用来存储动态数据结构和对象;栈是静态分配内存,由编译器自动分配和释放,用于存储函数的局部变量。
  • 内存管理:堆内存需要程序员手动管理内存的分配和释放,管理不当可能会出现内存泄漏;栈由编译器自动管理内存,变量的声明周期由作用域决定
  • 大小和速度:堆比栈内存空间较大,动态分配和释放需要时间开销;栈大小有限,内存分配和释放较快。

操作系统内存不足时会发生什么?

应用程序通过malloc函数申请内存时,实际上申请的是虚拟内存,此时不会分配物理内存。

  1. CPU去访问虚拟内存,如果虚拟内存没有映射到物理内存,就会产生缺页中断(从用户态变成内核态)
  2. 缺页中断处理函数会看是否有空闲的物理内存
    • 如果有:就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系
    • 如果没有:就开始进行回收内存的工作(后台内存回收、直接内存回收)
      • 如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,就会触发OOM机制,OOM机制会选择一个占用物理内存较高的进程杀死,直到释放足够的内存。

有两类内存是可以被回收的:

  1. 文件页(内核缓存的磁盘数据、内核缓存的文件数据):回收干净页的方式是直接释放内存;回收脏页的方式是先写回磁盘再释放内存
    • 大部分文件页都可以直接释放内存,以后有需要再从磁盘中读取即可。
    • 被应用程序修改过,还没写回磁盘的数据,需要先写回磁盘,再进行内存释放
  2. 匿名页:没有实际的载体,这部分内存可能还要被再次访问,不能直接释放内存,回收的机制是通过Swap机制,把不经常访问的内存写入磁盘,然后释放内存,给其他更需要的进程使用

文件页和匿名页都是基于LRU算法,优先回收不经常访问的页。

页面置换算法有哪些?

最佳页面置换算法、先进先出置换算法、最近最久未使用(LRU)、时钟页面置换算法、最不常使用算法(LFU)

中断

什么是中断?

中断使得计算机具备应对突发事件的能力,提高了CPU的工作效率,如果没有中断,CPU只能按照原来的程序来运行,轮询查询各个外设操作,工作效率低且不能响应紧急事件。

CPU停下当前的工作,去处理其他事情,处理完后回来继续执行刚才的任务。

  • 外部中断(来源于CPU外部)
    • 可屏蔽中断:主要来自外部设备(硬盘、打印机、网卡…),不会影响系统运行,可以后边再处理。
    • 不可屏蔽中断(电源掉电、硬件线路故障…)
  • 内部中断(来源于CPU内部)
    • 陷阱:有意的、故意安排的异常事件。(print函数,底层的实现中会有一条中断指令,这就是陷阱指令,接下来会使用中断进行系统调用)
    • 故障:如果能处理这个错误,就将程序返回到引起故障的指令,不能处理就报错(缺页,将缺失的物理页从磁盘中重新调入内存,再次执行引起故障的指令便可正常运行)
    • 终止:执行过程中发生了致命错误,不可修复。(不会将控制返回原程序,而是直接终止源程序)

中断的流程?

  1. 发生中断:当外部设备需要处理器的响应时,就会发生中断信号,处理器在收到中断信号后,就会停止当前执行的指令,保存当前执行的现场(程序计数器、寄存器状态..),跳转到中断处理程序中执行
  2. 中断响应:处理器接收到中断信号后,会从中断向量表中找到对应中断程序入口的地址。
  3. 中断处理:处理器跳到中断程序入口地址后开始执行中断处理程序,中断处理程序会根据终端类型进行相应处理。处理完毕后回到原程序继续执行

网络IO

有什么IO模型?

  • 阻塞IO模型:应用程序发起IO操作会被阻塞,操作完成后才返回结果
  • 非阻塞IO模型:应用程序发起IO操作后立即返回,但是需要不断轮询来检查IO操作是否完成
  • IO复用模型:应用程序可以同时等待多个IO操作,当其中任何一个IO操作准备就绪时,应用程序会被通知
  • 信号驱动IO模型:应用程序发起IO操作后,可以继续做其他事,当IO操作完成后,操作系统向应用程序发送信号来通知其完成
  • 异步IO模型:应用程序发起IO操作后可以立即做其他事,等IO操作完成时,应用程序会得到通知

服务器处理并发请求有哪几种方式?

  1. 单线程web服务器方式:web服务器一次处理一个请求,请求结束后读取并处理下一个请求
  2. 多进程/线程web服务器:web服务器生成多个进程或线程并行处理用户请求,但是一旦并发请求数量达到成千上万时,会消耗大量的系统资源。
  3. IO多路复用web服务器:web服务器可以IO多路复用,达到只用一个线程就能监听和处理多个客户端的IO事件
  4. 多路复用多线程web服务器:有多个进程,一个进程里又有多个线程,一个线程处理一个请求

IO多路复用

复用一个线程,处理多个socket中的事件,能够资源复用,防止创建过多的线程导致上下文切换的开销。

什么是零拷贝?

传统的IO工作方式:从磁盘中读取数据,通过网卡向外发送(需要4次上下文切换,4次数据拷贝【2次由DMA完成,2次由CPU完成】)

零拷贝技术:通过一次系统调用合并了磁盘读取和网络发送的操作,降低了上下文切换次数(只需要2次上下文切换,2次数据拷贝【2次的数据拷贝都是由CPU完成】)