一、并发
1 协程、线程、进程
1.1 什么是
- 进程是资源分配和拥有的基本单位。运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序
- 线程是程序执行的基本单位,是轻量级的进程。每个进程都有唯一的主线程,主线程和进程是相互依存的关系,主线程结束进程也会结束。同一进程的线程共享的有:堆、全局变量、静态变量、文件,独自占有栈和局部变量。
- 协程是用户态的轻量级线程,是线程内部调度的基本单位。
1.2 什么时候用多线程,什么时候用多进程
- 多线程的优点:
- 共享内存:当任务需要共享相同的内存空间时,使用多线程更方便。因为线程共享相同的地址空间,数据传递和通信更容易。
- 轻量级任务:线程的创建和销毁比进程更轻量级,适用于需要快速启动和销毁的任务。
- 资源效率:线程的切换通常比进程更快,所以在需要频繁切换的情况下,多线程可能更高效。
- 多进程的优点:
- 独立性:每个进程都有自己独立的内存空间,不容易受到其他进程的影响。
- 稳定性:每个进程有独立的内存空间,一个进程的崩溃不会影响其他进程,但是一个线程的崩溃可能会导致整个进程都崩溃
- 跨平台性:多进程通常更容易实现跨平台,因为进程之间的通信通常基于进程间通信机制,而不涉及直接的内存共享。
1.3 线程切换为什么比进程切换快,节省了什么资源
线程共享同一进程的地址空间和资源,线程切换时只需要切换堆栈和程序计数器等少量信息,不需要切换地址空间,避免了进程切换的时候需要切换内存映射表等大量资源开销,节约了时间。
2 进程调度算法
- 先到先服务:FCFS
- 短作业优先
- 最短剩余时间优先
- 时间片轮转
- 所有进程按到达时间排队,每次分配一个时间片给队首进程,执行完放到队尾。
- 时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
- 时间片太长,实时性不能得到保证
- 优先级调度
- 每个进程分配一个优先级,按优先级进行调度。
- 为了防止饿死,随着时间的推移增加等待进程的优先级
- 多级反馈队列
- 多个队列,1,2,4,8,...个时间片。进程在第一个队列没执行完,就会被移到下一个队列。
- 最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
- 能解决时间片多的进程的切换成本。
3 阻塞IO、非阻塞IO、多路复用IO。
-
阻塞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是事件驱动的,它能够监视文件描述符上的事件,只有在有事件发生时才会通知应用程序,而不是像select和poll那样需要轮询。 - epoll没有描述符数量的限制,用红黑树来存储和管理需要监视的文件描述符,用双向链表存储已经就绪的事件。在文件描述符增加时,select和poll的性能下降更加明显。(select和poll的监视描述符数量有上限)
- epoll除了支持水平触发,还支持边缘触发,只有在文件描述符状态发生变化时才会通知应用程序。(select和poll只支持水平触发)
- epoll是Linux特有的
- epoll的水平触发和边缘触发
- 边沿触发:
- socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
- socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
- 仅在缓冲区状态变化时触发事件
- 水平触发:
- socket接收缓冲区不为空,有数据可读,则读事件一直触发
- socket发送缓冲区不满可以继续写入数据,则写一直触发
- 边沿触发:
5 进程间通信方式
- 管道:用于具有亲缘关系的进程之间的通信。
- 有名管道:遵循先进先出。以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 共享内存:不同进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。需要依靠同步操作,如互斥锁和信号量。
- 消息队列:消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。也是先进先出。
- 信号:用于通知接收进程某个事件已经发生
- 信号量:信号量是一个计数器,用于控制多个进程对共享数据的访问。
- 套接字:用于在客户端和服务器之间通过网络进行通信。
同一台机器进程通信最快的方式是什么,为什么。
- 共享内存通信最快,共享内存的消息复制只有两次。
6 条件变量和互斥锁的区别
- 互斥锁的明显缺点:只有两种状态,锁定和非锁定
- 条件变量,允许线程阻塞,和,等待另一个线程发送信号,的方法弥补了互斥锁的不足
- 条件变量和互斥锁一起使用,以免出现竞态条件
- 条件不满足的时候
- 线程往往解开相应的互斥锁,并阻塞线程,然后等待条件发生变化
- 一旦其他某个线程改变了条件变量
- 它将通知相应的条件变量,唤醒一个或多个,正在被这个条件变量阻塞的线程
- 互斥锁是线程间互斥的机制,条件变量是同步机制
7 死锁的必要条件
- 互斥,(解决方法是:允许多个线程同时访问资源,例如读写锁,或者真正请求物理打印机的是守护进程)
- 请求和保持,(解决方法是:在开始执行前,请求所需要的全部资源)
- 不可抢占,(解决方法是:允许抢占资源)
- 循环等待,(解决方法是:给资源统一编号,只能按照编号顺序来请求资源)
8 进程状态
- 创建(Created):进程被创建但还没有开始执行。
- 就绪(Ready):进程已经准备好执行,但尚未分配到CPU时间。
- 运行(Running):进程正在CPU上执行指令。
- 阻塞(Blocked):进程在等待某些事件发生,例如等待输入/输出完成、等待资源分配等。
- 终止(Terminated):进程执行完毕或被强制终止。
- (僵尸态)
9 用户态和内核态
- 内核态指的是cpu可以访问内存的所有数据,包括外围设备,例如硬盘、网卡,可以将自己从一个程序切换到另一个程序
- 用户态只能受限的访问内存,不允许访问外围设备,cpu资源可以被其他程序获取
需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者访问外围设备的数据。
- 如何避免频繁切换用户态和内核态
- 减少线程切换,释放锁和加锁会引起较多上下文切换
- 用CAS算法,避免阻塞现场
- 使用协程
10 多个线程竞争如何解决
- 互斥锁:可以让一个线程获取锁的时候,其他线程阻塞直到锁被释放
- 读写锁:在读多写少的时候,可以让多个读线程同时访问资源,写操作排他
- 原子操作:用golang的atomic保证操作的原子性,避免竞争条件
- 信号量:可以控制多个线程对共享数据的访问。
11 孤儿进程、僵尸进程
孤儿进程
- 父进程先于子进程结束。孤儿进程不会自动消失,会被操作系统pid为1的init进程接管,无需手动处理
僵尸进程
- 子进程已经结束,但是父进程没有调用wait或waitpid,子进程的进程控制块还在系统中
二、内存
1 页面置换算法
- 最佳页面置换算法:OPT
- 选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。无法实现,是衡量其他算法的参考。
- 先进先出页面置换算法:FIFO
- 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
- 最近最久未使用页面置换算法:LRU
- 记录每个页面上一次被访问到现在的时间,选最久未被使用的淘汰。
- 最少使用页面置换算法:LFU
- 选择之前使用次数最少的页面进行淘汰
- 时钟置换算法:CLOCK
- 又叫最近未用算法:NRU
- blog.csdn.net/Gu_fCSDN/ar…
最佳置换算法性OPT能最好,但无法实现;
先进先出置换算法FIFO实现简单,但算法性能差;
最近最久未使用置换算法LRU性能好,但是实现起来需要专门的硬件支持,算法开销大。
2 栈和堆
2.1 栈上分配内存快还是堆上分配内存快
栈上分配内存更快,因为栈上只需要移动栈指针
- 操作系统会在底层对栈提供支持,会分配专门的寄存器,存放栈的地址
- 栈的入栈出栈操作简单,有专门的指令执行,栈效率高
- 堆生长空间向上,地址越来越大,栈的生长空间向下,地址越来越小
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的并发量