面试题
进程和线程
- 进程是计算机资源分配的最小单位,线程是CPU执行的最小单位
- 线程属于进程
- 进程之间的内存一般很难共享,线程之间的内存容易共享
- 进程挂掉之后一般不会影响其他进程,线程挂掉之后很可能导致其他线程挂掉
- 进程的切换、创建、销毁的开销比线程大
线程的生命周期
- 创建:新创建的一个线程对象
- 就绪:线程创建后,该线程处于可运行状态等待别调度
- 运行:获得CPU正在运行的线程
- 阻塞:线程需要等待资源或IO阻塞
- 死亡:线程执行结束
内核态和用户态是什么
操作系统根据进程对资源的访问级别划分了两个级别
用户态:用户态的进程只能访问用户程序的数据,访问内存也只能访问分配的部分
内核态:内核态的进程几乎可以访问任何计算机资源,包括外围设备等等
什么是操作系统
- 操作系统是计算机的基石,主要负责管理软件和硬件资源
- 它屏蔽了硬件的复杂性,向上层软件提供各种接口
- 控制程序的调度,CPU的使用、内存的分配等
分段和分页
分段是根据程序逻辑划分的所以每段的大小可以不同,进程在执行时通过段表和偏移量来找到物理地址。分段存在外碎片
分页是直接根据内存将内存划分成等大小的页面,分页存在内碎片
为什么有虚拟内存
- 虚拟内存可以将进程隔离,程序使用逻辑地址由操作系统完成逻辑地址到虚拟地址的转换,保证了一个进程不会访问到其他进程的数据
- 虚拟内存的出现使得进程可以以低的内存执行高内存的程序(比如可以通过页面置换来实现)
分段和分页的区别
- 分段和分页都能减少内存空间碎片化
- 分页和分段都是可以离散存储的
- 页的大小固定,段的大小不固定有当前运行的进程决定
NIO
NIO是同步非阻塞。NIO在发起IO时,在数据从内核空间拷贝到用户空间这段时间内线程是非阻塞的,线程通过不断轮询检测数据是否到达。
并且NIO基于多路复用模型:一个线程可以处理多个链接,BIO不能一个线程处理多个IO的原因是BIO中线程在等待数据到达时是需要阻塞的
- Netty是基于NIO的
AIO
AIO是NIO的增强版,异步非阻塞的,基于事件和回调机制实现的,应用程序在发起IO调用后可以直接返回,当数据处理完成之后操作系统会主动通知程序进行后续执行。需要操作系统的支持
进程和线程
- 进程是计算机资源分配的最小单位、线程是CPU执行的最小单位
- 进程拥有完整的资源,线程一般只独享部分资源
- 线程属于进程
- 进程是程序的一次执行,进程之间的内存是很难共享,而线程之间的内存很容易共享
- 线程挂掉后可能会导致其他线程挂掉,一个进程挂掉后不会影响到其他进程
为什么Java处理并发采用多线程而不采用多进程
- 由于进程是资源分配的最小单位,每个进程的创建、销毁、切换都会销毁大量的时间和空间所以进程的数量不能太多
- 而线程基本不拥有系统资源,只有一些运行时必不可少的资源,可以减少程序并发执行的时间和空间,具有更好的并发性
进程
进程的状态
- 创建状态
- 就绪态
- 运行态
- 阻塞态
- 结束状态
- 挂起状态
进程被阻塞后是不能主动恢复就绪的,必须是由其他进程唤醒
进程控制块PCB
PCB中的信息
- 进程唯一标识
- 进程当前状态
- 持有资源
- 内存空间
进程终止流程
- 查找进程的PCB
- 如果进程处于执行态,则立即终止
- 终止其子进程
- 将该进程拥有的资源归还给父进程和操作系统
- 删除其PCB
Linux进程调度算法
先来先服务
先到的进程先执行,不论执行时间长短
短作业优先
系统按当前等待执行的进程需要执行的时间来排,作业执行时间越短优先级越高,先执行
时间片轮转
给每个进程分配等量的时间片,进程
优先级调度算法
优先级调度算法是指作业根据优先级依次执行。
优先级调度算法有两种,一种是非抢占式的(CPU允许当前在执行的进程优先级低于等待的进程)
另一种是抢占式的(优先级高的进程可以抢占正在执行的低优先级进程)
多级反馈队列
有多个就绪队列,最上层的就绪队列时间片较小,下层就绪队列时间片依次增大且队列优先级依次降低。进程都会先进入上层就绪队列,进程执行一次后如果没有结束将会进入下一级队列等待。最底层队列采用有时间片轮转依次执行
进程间通信方式
进程是独立的,每个进程都有自己独立的虚拟空间
进程间的通信方式主要分两种:本地进程通信、远程进程通信
本地进程通信
本地进程通信又主要分为如下几种:匿名管道、命名管道、消息队列、共享内存、信号量
管道通信
- 管道通信是
半双工
的(数据是单向传输的),双方都需要通信时需要建立两个管道。 - 匿名管道只能用于父子进程和兄弟进程之间、命名管道可以用于本机上任何进程之间
- 管道实际上就是在内核中的一块数据缓冲区,传输方每次将数据置于管道尾部,接收方每次从管道头部获取数据
- 管道其实是一个特殊的文件但是它只存在于内存不存在与文件系统中(内核中)
- 管道的生命周期是随进程的,进程销毁管道则销毁
管道的缺点
- 只支持单向数据流动
- 缓冲区大小有限
- 管道传输的数据是无格式的字节流,双方要约定数据格式
- 需要从内核中读取数据
- 不适合频繁交互
消息队列
- 消息队列是存放在内核中的一个消息链表
- 消息队列是全双工的,两端都可以对数据进行读写
- 消息队列也是放在内核中的
- 数据是有格式的
- 消息队列的生命周期是随内核的换句话说就是会一直存在
- 适合频繁沟通
缺点
- 不是大数据传输
- 需要从内核中读取数据,会频繁的从用户态到内核态进行切换
共享内存
-
共享内存就是在内存中映射一块区域到两个进程中(两个进程能看到同一块内存区域)
进程A在这块区域写的东西进程B也可以直接访问到
-
不需要像消息队列一样从内核中拷贝到用户态
缺点
- 需要控制同步
信号量
- 信号量就是一个多个进程可以同时访问的整形变量,进程通过修改这个变量的值来进行同步
- 信号量有两个原子操作(P-V操作)
信号
信号是进程间的通信的一种机制,进程接受到信号后可以对信号选择:执行默认操作、捕捉、忽略信号
以 kill -9 pid命令来说,-9 就是一个信号
或者-15也是一个信号,进程在接受到信号之后需要进行相应的处理,而其中 -9和-19是不可忽略的,这就是为什么在kill指令中可以使用-9来强制杀死进程,而使用默认-15不一定能杀死进程的原因
远程进程通信
Socket套接字
- 前面的几种进程间通信方式都有个缺点,只能在同一台机器上通信,而socket就是借助网络通信来实现跨机器通信
- socket也可以用于同一台机器之间通信
线程
线程是比进程更小的执行单位,线程是属于进程的。其作用就是使得程序能同时执行多个任务。
为什么不采用多进程
因为进程本身比线程更庞大,CPU在切换上下文时需要消耗的时间更长,并且普通进程之间是不共享内存空间的。当一个程序需要协作完成任务时,多进程就很困难了。而线程不仅需要的资源更小,CPU切换线程时消耗的时间也小并且线程之间是可以共享内存空间的。
线程的缺点
线程崩溃后可能会影响其他线程从而导致整个进程崩溃
线程切换
线程和进程一样也是有状态的。线程在切换时如果切换的两个线程属于同一个进程,只需要切换部分线程不共享的数据,所属进程的资源不需要切换。如果切换的两个线程属于不同进程,切换过程等于进程切换。
线程通信
这里主要讨论java中的线程通信方式
-
共享变量
类似于volatile关键字定义的变量
-
等待/通知机制
wait/notify等
-
队列
Linux
Linux命令
Kill
作用:关闭进程
kill -9和kill区别
kill默认信号为 kill -15
kill -15: 系统像程序发送一个信号,程序接收到信号后如何处理取决于程序自身。程序的处理方式有三种
- 立即停止
- 释放资源后停止
- 忽略信号,继续执行
kill -9:强制程序退出,这种退出程序是没有办法处理退出工作的,很可能造成数据丢失和无法恢复等情况
Linux进程状态
Linux2.6之后进程有7种状态,PS指令中可以查看到对应的进程状态
-
D (TASK_UNINTERRUPTIBLE):不可中断睡眠
进程处于睡眠状态,但不可被中断。一般是由IO引起的
kill -9
指令不能关闭该进程,只能通过重启。但是Sleep状态的进程不会占用CPU资源
-
S (TASK_INTERRUPTIBLE):可中断睡眠
与不可中断睡眠相似,此状态也处于Sleep状态不占用CPU资源,但是是可以被中断的,当等待的事件到达后会被唤醒并进入R状态
-
R (TASK_RUNNING):可执行
进程处于可执行队列或正在执行状态
-
T(TASK_STOPPED):暂停态
进程处于运行停止状态,暂停态一般是因为收到SIGTOP、SIGTSTP、SIGTTIN、SIGTTOUT四种信号
暂停态会释放所有资源
-
t(TASK_TRACED):跟踪态
进程处于运行停止状态,与暂停态不同的是跟踪态是因为被其他进程跟踪导致的,会释放所有资源
-
X(EXIT_DEAD):死亡态
进程结束状态,一般不会被操作系统捕捉到
-
Z(EXIT_ZOMBIE):僵尸态
进程已经结束,但父进程还未收回子进程资源,僵尸进程会释放除程序入口以外的资源
CPU&LOAD飙高排查
CPU飙高,系统性能问题如何排查? - 知乎 (zhihu.com)
CPU占用率和Load是系统性能最直观的衡量标准
Linux对Load的计算
一般操作系统的计算公式
Load = (处于TASK_RUNNING的进程)/ (等待运行的进程数)
Linux的计算方式
Load = (TASK_RUNNING的进程+TASK_UNINTERRUPTIBLE的进程)/ (等待运行的进程数)
从公式可以看出来,Linux的Load负载计算的是整个系统的负载(CPU负载+Disk负载+网络负载+其余外设负载)
Linux对CPU使用率的计算
CPU时间可以分为四大类:用户进程运行时间 - User Time, 系统内核运行时间 - System Time, 空闲时间 - Idle Time, 被抢占时间 - Steal Time
除了Idle Time CPU处于空闲状态,其余状态CPU都处于运行状态
CPU高Load高
如果是CPU sys高则可能是线程上下文切换频繁导致的
如果是CPU si高则可能是软中断导致的
如果是CPU us高则可能是用户进程代码出现问题(死循环、内存不足、CPU计算密集等问题)
CPU低Load高
这种情况主要就是处于不可中断的进程太多导致Load飙高,可以进一步检查是网络IO还是磁盘IO导致的
IO
BIO、NIO、AIO
BIO
BIO:同步阻塞IO,用户线程发起读写数据的IO请求后,用户线程会被阻塞并交出CPU。
只有当操作系统将内核数据拷贝到用户空间时,用户线程才会脱离阻塞继续执行。如果数据一直不到达,线程将会被持续阻塞。
一个链接一个线程,所以当某个链接长时间不请求时会导致服务器资源浪费
NIO
NIO:同步非阻塞IO,用户线程发起读写数据的IO请求后,用户线程不用等待可以直接返回。此时返回的结果有两种情况。①若数据未到达将直接返回未就绪的状态(用户线程可以继续执行,不会阻塞用户线程)②若数据到达后,则进行读写数据。
这里的非阻塞是指用户线程在数据未到达时仍然持有CPU
这里的同步是指用户线程在数据到达后需要去进行数据的读写(指将内核态数据复制到用户态)
一个请求一个线程,客户端链接后将请求注册到多路复用器上,轮询到IO请求时才会进行处理
使用于链接多但链接时间较短的情况
AIO
AIO:异步非阻塞IO,用户线程发起IO请求后,用户线程不等待直接返回。内核中的数据准备完成后会将数据拷贝到用户态。再通知用户线程数据准备完成。
一个有效请求一个线程,IO由操作系统完成后才会通知服务器启动线程来处理
IO多路复用
IO多路复用:是指一个线程来管理多个socket。只有当socket真正发生IO操作时采取IO操作。
使每个socket发生读写时不需要产生新的进程或线程来处理,大大的减少了资源占用。
IO多路复用下涉及到三种读写数据的方式
select、poll、epoll
select
轮询所有socket,缺点socket数组有上限1024。并且当有socket发生IO时,select需要去遍历所有socket来找到真正需要执行IO的socket。线程监听多个socket,调用select时会如果有数据则返回,如果没有数据则阻塞。所以当从select方法返回时表明一定是有socket的数据是到达了的,select是不知道具体哪个socket发生了调用的,只能通过遍历来找到。
poll
poll整体来说和select没有区别,poll的socket没有最大连接数限制,因为采用的是链表
epoll
(91条消息) Epoll原理解析_~~ LINUX ~~-CSDN博客_epoll
epoll只会对真正发生IO的socket进行处理,epoll之所以能知道哪个socket产生了数据是因为,内核维护了一个等待队列,当socket产生数据后会将socket放入等待队列,此时用户线程直接从等待队列中的socket读取数据即可。
DMA
DMA 的中文名称是直接内存访问,它意味着 CPU 授予 I/O 模块权限在不涉及 CPU 的情况下读取或写入内存。也就是 DMA 可以不需要 CPU 的参与。这个过程由称为 DMA 控制器(DMAC)的芯片管理。由于 DMA 设备可以直接在内存之间传输数据,而不是使用 CPU 作为中介,因此可以缓解总线上的拥塞。DMA 通过允许 CPU 执行任务,同时 DMA 系统通过系统和内存总线传输数据来提高系统并发性。
DMA的作用就是减少CPU对I/O的处理,使得用户进程需要进行I/O时可以直接由DMA对数据进行拷贝到内核态,数据读取完成后再通知CPU处理,数据拷贝期间CPU就可以执行其他任务。
- 用户进程向操作系统发起一次read读取请求
- 开始系统调用(用户态切换到内核态)
- 用户进程进入阻塞态
- 操作系统接收到I/O请求后交给DMA控制器
- DMA控制器将请求发给磁盘请求读取数据
- 磁盘将数据拷贝到磁盘缓冲区中并通知DMA
- DMA将数据拷贝内核缓冲区中
- 拷贝完成后发送中断信号给CPU
- CPU再将数据拷贝到用户态
- 系统调用结束(内核态切换回用户态)
零拷贝技术
问题引入
服务端如果提供了文件传输功能,传统的工作流程是
- 将磁盘上的文件读取出来
- 通过网络协议发送到客户端
- DMA将磁盘数据拷贝到内核中
- CPU将内核中的数据拷贝到用户态
- CPU将用户态的数据拷贝到内核态
- DMA将数据拷贝到网卡中发送
整个流程中会涉及到两次系统调用(两次系统调用对应四次上下文切换),四次数据拷贝。
我们可以发现从CPU将内核数据拷贝到用户数据,再将用户数据拷贝内核,这两次拷贝是没有必要的
零拷贝实现方式
使用零拷贝技术可以减少数据拷贝的次数来提高数据的发送效率。
零拷贝技术实现的方式通常有2种
- mmap+write
- sendfile
mmap+write实现方式
从前面可以知道,之所以有四次上下文切换是因为有两次系统调用,两次系统调用分别是read()和write()
而使用mmap()来替换read()后,系统可以直接将内核缓冲区中的数据映射到用户空间,这样操作系统的内核态和用户态之间就不需要再进行任何的数据拷贝
具体过程如下
- 应用进程调用mmap(),DMA会把磁盘的数据拷贝到内核态的缓冲区中,应用程序和操作系统内核态共享这个缓冲区
- 应用进程再调用write(),操作系统直接将内核态缓冲区的数据拷贝到socket缓冲区,这里由CPU执行
- 把内核中socket的数据拷贝到网卡的缓冲区中,这里由DMA执行
从上面可以看出,基于mmap+write的零拷贝技术,可以减少一次数据拷贝,但仍然会有两次系统调用和四次上下文切换
sendfile实现方式
Linux内核版本2.1开始提供了一种专门用于发送文件的系统调用函数sendfile()
用户进程发送一次文件只需要一次系统调用。
具体过程如下
- DMA将文件从磁盘中拷贝到内核态缓冲区
- CPU将内核态缓冲区中的数据拷贝到内核态Socket缓冲区中
- DMA将内核态Socket缓冲区中的数据拷贝到网卡中
这种方式只需要发起一次系统调用(SendFile())、两次上下文切换、三次数据拷贝。
如果网卡支持DMA技术的话,甚至只需要两次数据拷贝即可完成
具体过程如下
- DMA将磁盘文件拷贝到内核态中
- SG-DMA将数据从内核态中直接拷贝到网卡中
可以通过查看网卡是否支持
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
零拷贝技术缺点
零拷贝技术不适合用来传输大文件(GB级别)
大文件传输
零拷贝技术会使用PageCache(磁盘数据的缓存),而PageCache使用了预读功能
预读功能
由于空间局部性原理,PageCache在读取数据时,会多读一部分数据到PageCache中,这样如果后续使用到了这部分数据,就可以减少一次IO,并且这个优化在大部分情况下是有效的。
但是在传输大文件时,因为文件本身很大,PageCache被大文件占满,导致其他热点文件无法使用PageCache,这样会降低磁盘读写性能。所以在传输大文件时避免使用零拷贝
大文件传输如何实现
使用异步IO+直接IO
异步IO就是指用户发起一次系统调用时指定数据准备好后需要完成的操作,内核直接将磁盘数据拷贝到用户空间中。
内存管理
内存管理主要分两种管理方式:连续分配和非连续分配
连续分配
连续分配是指操作系统将内存分成几个大小固定的块(也叫块式分配),每次为进程分配的时候分配一块空间
缺点:分配空间不会正好合适,多余的空间就浪费了,造成内碎片
虚拟内存(非连续分配)
虚拟内存是指,程序中使用的内存地址为虚拟的内存地址,程序执行时由操作系统来将程序中的逻辑地址映射为真实的物理内存地址
这样的好处就是将操作系统中多个进程的内存空间隔离。
虚拟内存中地址映射主要由两种方式 内存分段、内存分页
分段
分段是指将内存按逻辑进行分段,每段大小不固定,通过段表加偏移量实现地址映射
缺点
因为段的大小不固定,所以分段是有外碎片的
分页
将内存按固定大小分成一页一页的,每页空间比较小。
缺点
分页没有外碎片但因为每页大小相同而需要分配的内存不固定,所以存在内碎片。分页通过页表+偏移量实现内存地址映射
快表
快表相当于是页表地址映射的缓存,将最近映射过的地址保存起来,下次查询时直接由快表进行映射
多级页表
如果维护的页表过大就操作系统就需要耗费大量的内存来保存页表,多级页表是对页表再次进行分页,可以使用更小的内存空间来表示更大的内存地址
多级页表是典型的以空间换时间
页面置换算法
FIFO先进先出
每次淘汰进入时间最早的页面(相当于维护一个先进先出队列),这种算法不好因为先进来的页面后面使用的概率比较大。
LRU最近最少使用
每次淘汰最近最少使用的页面。(维护一个链表,节点被访问后移动到链表尾部,需要置换页面时只需要将头部的页面移除即可)
缺点:维护开销大
LFU最不常用
每个页面一个计数器,被访问时计数器+1。需要置换页面时只需要将计数器值最小的页面置换出去
时钟置换
它是LRU和FIFO的折衷。内存中所有页面都通过链接指针链接成一个循环队列,需要置换时,循环检测队列中是否存在未被访问的页面。1