操作系统

367 阅读13分钟

一、并发

1 协程、线程、进程

1.1 什么是

  • 进程是资源分配和拥有的基本单位。运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序
  • 线程是程序执行的基本单位,是轻量级的进程。每个进程都有唯一的主线程,主线程和进程是相互依存的关系,主线程结束进程也会结束。同一进程的线程共享的有:堆、全局变量、静态变量、文件,独自占有栈和局部变量。
  • 协程是用户态的轻量级线程,是线程内部调度的基本单位。

1.2 什么时候用多线程,什么时候用多进程

  • 多线程的优点:
    1. 共享内存:当任务需要共享相同的内存空间时,使用多线程更方便。因为线程共享相同的地址空间,数据传递和通信更容易。
    2. 轻量级任务:线程的创建和销毁比进程更轻量级,适用于需要快速启动和销毁的任务。
    3. 资源效率:线程的切换通常比进程更快,所以在需要频繁切换的情况下,多线程可能更高效。
  • 多进程的优点:
    1. 独立性:每个进程都有自己独立的内存空间,不容易受到其他进程的影响。
    2. 稳定性:每个进程有独立的内存空间,一个进程的崩溃不会影响其他进程,但是一个线程的崩溃可能会导致整个进程都崩溃
    3. 跨平台性:多进程通常更容易实现跨平台,因为进程之间的通信通常基于进程间通信机制,而不涉及直接的内存共享。

1.3 线程切换为什么比进程切换快,节省了什么资源

线程共享同一进程的地址空间和资源,线程切换时只需要切换堆栈和程序计数器等少量信息,不需要切换地址空间,避免了进程切换的时候需要切换内存映射表等大量资源开销,节约了时间。

2 进程调度算法

  • 先到先服务:FCFS
  • 短作业优先
  • 最短剩余时间优先
  • 时间片轮转
    • 所有进程按到达时间排队,每次分配一个时间片给队首进程,执行完放到队尾。
    • 时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
    • 时间片太长,实时性不能得到保证
  • 优先级调度
    • 每个进程分配一个优先级,按优先级进行调度。
    • 为了防止饿死,随着时间的推移增加等待进程的优先级
  • 多级反馈队列
    • 多个队列,1,2,4,8,...个时间片。进程在第一个队列没执行完,就会被移到下一个队列。
    • 最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
    • 能解决时间片多的进程的切换成本。

3 阻塞IO、非阻塞IO、多路复用IO。

blog.csdn.net/Chen4852010…

  • 阻塞IO

    • 当用户线程发出IO请求后,内核会去查看数据是否就绪,未就绪的话就会等待。用户线程处于阻塞状态,用户线程交出CPU。
  • 非阻塞IO

    • 用户线程不断询问内核,数据是否就绪,不会交出CPU,而是一直占用CPU
  • 多路复用IO

    • 单个线程就可以同时处理多个IO请求,单个线程可以监视多个文件句柄,一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作。没有文件句柄就绪时,会阻塞应用程序,交出cpu。
  • 如何实现多路复用IO

    • 在linux中有三种机制可以实现多路复用IO,select,poll,epoll

4 select、poll、epoll(IO多路复用)

  • select、poll、epoll都用于实现I/O多路复用,允许一个进程或线程同时管理多个文件描述符的I/O事件。
  • epoll的性能比select和poll要高。
  • 事件驱动:epoll是事件驱动的,它能够监视文件描述符上的事件,只有在有事件发生时才会通知应用程序,而不是像selectpoll那样需要轮询。
  • epoll没有描述符数量的限制,用红黑树来存储和管理需要监视的文件描述符,用双向链表存储已经就绪的事件。在文件描述符增加时,select和poll的性能下降更加明显。(select和poll的监视描述符数量有上限)
  • epoll除了支持水平触发,还支持边缘触发,只有在文件描述符状态发生变化时才会通知应用程序。(select和poll只支持水平触发)
  • epoll是Linux特有的
  • epoll的水平触发和边缘触发
    • 边沿触发:
      1. socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
      2. socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
      3. 仅在缓冲区状态变化时触发事件
    • 水平触发:
      1. socket接收缓冲区不为空,有数据可读,则读事件一直触发
      2. socket发送缓冲区不满可以继续写入数据,则写一直触发

5 进程间通信方式

  1. 管道:用于具有亲缘关系的进程之间的通信。
  2. 有名管道:遵循先进先出。以磁盘文件的方式存在,可以实现本机任意两个进程通信。
  3. 共享内存:不同进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。需要依靠同步操作,如互斥锁和信号量。
  4. 消息队列:消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。也是先进先出。
  5. 信号:用于通知接收进程某个事件已经发生
  6. 信号量:信号量是一个计数器,用于控制多个进程对共享数据的访问。
  7. 套接字:用于在客户端和服务器之间通过网络进行通信。

同一台机器进程通信最快的方式是什么,为什么。

  • 共享内存通信最快,共享内存的消息复制只有两次。

6 条件变量和互斥锁的区别

  • 互斥锁的明显缺点:只有两种状态,锁定和非锁定
  • 条件变量,允许线程阻塞,和,等待另一个线程发送信号,的方法弥补了互斥锁的不足
  • 条件变量和互斥锁一起使用,以免出现竞态条件
  • 条件不满足的时候
    • 线程往往解开相应的互斥锁,并阻塞线程,然后等待条件发生变化
    • 一旦其他某个线程改变了条件变量
    • 它将通知相应的条件变量,唤醒一个或多个,正在被这个条件变量阻塞的线程
  • 互斥锁是线程间互斥的机制,条件变量是同步机制

7 死锁的必要条件

  1. 互斥,(解决方法是:允许多个线程同时访问资源,例如读写锁,或者真正请求物理打印机的是守护进程)
  2. 请求和保持,(解决方法是:在开始执行前,请求所需要的全部资源)
  3. 不可抢占,(解决方法是:允许抢占资源)
  4. 循环等待,(解决方法是:给资源统一编号,只能按照编号顺序来请求资源)

8 进程状态

  • 创建(Created):进程被创建但还没有开始执行。
  • 就绪(Ready):进程已经准备好执行,但尚未分配到CPU时间。
  • 运行(Running):进程正在CPU上执行指令。
  • 阻塞(Blocked):进程在等待某些事件发生,例如等待输入/输出完成、等待资源分配等。
  • 终止(Terminated):进程执行完毕或被强制终止。
  • (僵尸态)

9 用户态和内核态

  • 内核态指的是cpu可以访问内存的所有数据,包括外围设备,例如硬盘、网卡,可以将自己从一个程序切换到另一个程序
  • 用户态只能受限的访问内存,不允许访问外围设备,cpu资源可以被其他程序获取

需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者访问外围设备的数据。

  • 如何避免频繁切换用户态和内核态
    1. 减少线程切换,释放锁和加锁会引起较多上下文切换
    2. 用CAS算法,避免阻塞现场
    3. 使用协程

10 多个线程竞争如何解决

  • 互斥锁:可以让一个线程获取锁的时候,其他线程阻塞直到锁被释放
  • 读写锁:在读多写少的时候,可以让多个读线程同时访问资源,写操作排他
  • 原子操作:用golang的atomic保证操作的原子性,避免竞争条件
  • 信号量:可以控制多个线程对共享数据的访问。

11 孤儿进程、僵尸进程

孤儿进程

  • 父进程先于子进程结束。孤儿进程不会自动消失,会被操作系统pid为1的init进程接管,无需手动处理

僵尸进程

  • 子进程已经结束,但是父进程没有调用wait或waitpid,子进程的进程控制块还在系统中

二、内存

1 页面置换算法

  • 最佳页面置换算法:OPT
    • 选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。无法实现,是衡量其他算法的参考。
  • 先进先出页面置换算法:FIFO
    • 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
  • 最近最久未使用页面置换算法:LRU
    • 记录每个页面上一次被访问到现在的时间,选最久未被使用的淘汰。
  • 最少使用页面置换算法:LFU
    • 选择之前使用次数最少的页面进行淘汰
  • 时钟置换算法:CLOCK

最佳置换算法性OPT能最好,但无法实现;
先进先出置换算法FIFO实现简单,但算法性能差;
最近最久未使用置换算法LRU性能好,但是实现起来需要专门的硬件支持,算法开销大。

2 栈和堆

2.1 栈上分配内存快还是堆上分配内存快

栈上分配内存更快,因为栈上只需要移动栈指针

  1. 操作系统会在底层对栈提供支持,会分配专门的寄存器,存放栈的地址
  2. 栈的入栈出栈操作简单,有专门的指令执行,栈效率高
  3. 堆生长空间向上,地址越来越大,栈的生长空间向下,地址越来越小

2.2 堆和栈的区别

  • 申请方式不同
    • 栈是系统自动分配的
    • 堆是自己申请和释放的
  • 申请大小限制不同
    • 栈顶和栈底是之前预设好的,大小固定,可以通过ulimit -a查看,通过ulimit -s修改。
    • 堆向高地址扩展,是不连续的内存区域,大小可以灵活调整
  • 申请效率不同
    • 栈由系统分配,速度快,不会有碎片。
    • 堆由程序员分配,速度慢,并且会有碎片。

3 程序的内存模型/内存分区

程序的内存模型从高地址到低地址,分别是栈区、(未使用的内存)、堆区、全局数据区、常量区和代码区。

  • 栈:一般存放的是函数内的局部变量。栈的内存分配运算,是内置在处理器的指令集里的,效率很高,但是内存容量有限。在golang里会出现栈逃逸的情况。
  • 堆:主要通过make和new分配内存。golang的垃圾回收机制可以自动管理堆区。
  • 全局存储区:存储的是全局变量和静态变量,在程序执行过程中是持久存在的,在程序启动的时候被分配,会被自动初始化。
  • 常量存储区:存放的是常量,不允许修改
  • 代码区:存放的是程序代码,位于比较低的内存地址

4 内存分段分页

  • 分段
    • 将程序分为代码段、数据段、堆栈段等。
  • 分页
    • 把虚拟内存地址,切分成页号和偏移量
    • 根据页号,从页表里面,查询对应的物理页号
    • 直接拿物理页号,加上偏移量,得到了物理内存地址

5 虚拟地址

5.1 什么是虚拟地址

操作系统把内存抽象成地址空间,将物理内存抽象成一个连续的地址空间。使得每个进程都有自己独立的虚拟地址空间。操作系统负责管理虚拟地址到物理地址的映射。

5.2 虚拟地址空间的好处

  • 虚拟内存可以让进程运行的内存超过物理内存大小,对于不会经常使用到的内存,可以换到硬盘上的swap区域
  • 每个进程有自己的页表,可以解决多进程之间地址冲突的问题

5.3 虚拟地址怎么转换到物理地址的

虚拟地址到物理地址的转换是通过页表实现的。
页表可以将虚拟地址映射到物理地址。
分页可以将【段】分成多个大小相等的页,可以将整个进程,的多个段的页,映射到真实的物理地址空间上了。

6 cow写时复制copy on write

  • 在fork的时候,子进程不需要复制父进程的物理内存,只需要复制页表。父进程和子进程的页表指向的是共享的物理内存。
  • 当父进程或者子进程任何一方对共享的物理内存发生了操作修改,才会触发写的机制。
  • 可以节省物理内存的资源

7 大小端存储是什么意思,如何区分

  • 大端存储:字数据的高字节存储在低地址中,
  • 小端存储:字数据的低字节存储在低地址中
  • 所以在socket编程中,需要将操作系统用的小端存储的IP地址转换为大端存储,才能进行网络传输

8 内存泄漏和内存溢出

  • 内存泄漏指的是堆内存的泄漏,偏向于内存被浪费,已经分配的内存没有释放,不会立即造成崩溃
  • 内存溢出是out of memory是内存不够用,程序需求的内存超过系统限制,会引发崩溃
  • 可以通过pprof来分析内存情况
  • 内存泄漏常见的原因
    • 没有释放的goroutine过多会导致一直占用内存
    • 没有清除不再使用的引用,长时间存活或者越来越大的slice
  • 内存泄漏的解决方法
    • 定期清理全局变量
    • 限制长时间存活对象的数量
    • 确保goroutine正确推出
  • 内存溢出的解决方法
    • 限制goroutine的并发量