参考这篇博文,总结的非常全面:
先看上面这篇,看完之后,再看下面这篇作为补充:
io多路复用的原理和实现_彻底理解 IO 多路复用实现机制
操作系统
- 用户态切换到内核态有几种方式
有三种:系统调用、中断、异常。
1、系统调用:系统调用将Linux整个体系分为用户态和内核态,为了使应用程序访问到内核的资源,如CPU、内存、I/O,内核必须提供一组通用的访问接口,这些接口就叫系统调用。
用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用,系统调用的机制核心使用了操作系统为用户特别开放的一个中断来实现,如Linux 的 int 80h 中断,也可以称为软中断
2、异常:当 C P U 在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常
3、中断:当 C P U 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 C P U 发出相应的中断信号,这时 C P U 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等。参考:https://segmentfault.com/a/1190000039774784
- 用户态和内核态在内存资源上的分配
以32位linux操作系统为例。一共有内存资源:2^32即内存资源:4G。
寻址空间范围是 4G(2的32次方),而操作系统会把虚拟控制地址划分为两部分,一部分为内核空间,另一部分为用户空间。
内核态使用:高位的 1G(从虚拟地址 0xC0000000 到 0xFFFFFFFF)
用户态使用:低位的 3G:(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用。
- 什么是用户态和内核态
通关了C P U 指令集权限,现在再说用户态与内核态就十分简单了,用户态与内核态的概念就是C P U 指令集权限的区别,进程中要读写 I O,必然会用到 ring 0 级别的 C P U 指令集,而此时 C P U 的指令集操作权限只有 ring 3,为了可以操作ring 0 级别的 C P U 指令集, C P U 切换指令集操作权限级别为 ring 0,C P U再执行相应的ring 0 级别的 C P U 指令集(内核代码),执行的内核代码会使用当前进程的内核栈。
PS:每个进程都有两个栈,分别是用户栈与内核栈,对应用户态与内核态的使用
备注:处于对权限的考虑,将内存资源分为用户态和内核态。
进程管理
- 进程和线程的区别
- 进程有哪些调度算法?
- 先来先服务算法:队列实现,长短作业都进队列。缺点:不利于短作业,因为前面长作业执行时间可能很长。
- 短作业优先:按估计运行时间最短的作业进行调度。缺点:长作业可能饿死。
- 优先级调度:每个进程分配一个优先级。低优先级可以随着时间推移,增加优先级。
- 时间片轮转:所有进程按先来先服务思想进队列,CPU每次给队首进程执行一个时间片。时间片用完后该进程再到队列末尾进行排队。效率:和CPU时间片有关系。太大:实时性无法保证;太小:进程上下文切换太频繁。
- 进程管理:
https://github.com/CyC2018/CS-Notes/blob/master/notes/Linux.md#%E5%8D%81%E8%BF%9B%E7%A8%8B%E7%AE%A1%E7%90%86
- 为什么虚拟机地址空间切换会比较耗时?/ 为什么进程上下文切换比较耗时?
- 每个进程都有自己的虚拟地址空间
- 虚拟地址空间转换为物理地址空间方法:查找页表。
- 耗时原因:查找页表是一个很慢的过程。
- 总结:每个进程都有自己的虚拟地址空间,相应的每个进程都有自己的页表。当进程切换后页表也需要进行切换。页表切换后TLB就失效了。缓存失效导致命中率降低。那么虚拟地址转换为物理地址就会变慢,表现出来就是程序运行会变慢。
- 线程切换相对于进程切换,为什么耗时少?
- 原因:线程切换,如果是同一个进程内的线程,则这些线程共享虚拟地址空间,线程切换无需切换地址空间。
- 进程与线程的切换流程是怎么样的?
进程切换分2步:
- 切换页表,以使用新的地址空间。
- 切换内核栈和硬件上下文。
线程切换就1步:
- 切换内核栈和硬件上下文。
linux来说,线程和进程切换的最大区别是:虚拟地址空间。对于线程,不用做第1步。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
- 进程和线程的区别?
- 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。
- 切换:线程上下文切换比进程上下文切换要快很多。
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源。但是可以访问隶属于进程的资源。
- 系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。
- 进程都拥有哪些资源,线程呢?
进程拥有的资源:
下面介绍进程地址空间的划分中属于进程资源的:
- 代码区:保存写的代码,即编译后可执行的机器指令。
- 数据区:这里存放全局变量
- 堆区:c/c++中用malloc或者new出来的数据就存放在这个区域。任何一个线程都可以通过变量的指针即变量的地址,访问其指向的数据。
线程私有的资源:
- 线程的栈区
- 程序计数器
- 栈指针
- 函数运行使用的寄存器
参考:https://cloud.tencent.com/developer/article/1768025
- 线程和协程的区别?
什么是协程?
协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。
一句话:协程是用户态的线程。
好处:协程占用内存更少,一般几十kb。协程的创建、销毁和调度不需要内核来实现,由用户态代码来完成,避免频繁上下文切换带来的开销。可以支持更多并发。
参考:
1.https://xie.infoq.cn/article/c62beed510625a8e128aa9001
2. https://zhuanlan.zhihu.com/p/70256971
内存管理
- 虚拟地址和物理地址通过页表进行关联
页表的理解:
- 什么是内存分页?
内存分页:将虚拟地址、物理地址分成大小相等、连续的固定尺寸大小,每一段称之为页。
页大小:Linux 页大小:4kb
- 访问分页系统中数据需要几次内存才能得到数据?
需要2次内存访问:
1、第一次:从内存中访问页表,通过页表得到指定的物理页号。
2、第二次:通过物理页号,访问物理内存获取到数据
- 什么是多级页表?
多级页表解决问题:解决多进程使用一个页表造成的页表庞大问题。
什么是多级页表:把单级页表再次进行分页,分为不同级别页表。
多级页表优点:
1、其他级别页表在需要时候进行创建,省内存。
2、内存不足时,可以将其他级别页表置换到磁盘中。
- 什么是块表?
- 称之为页表缓存
- 专门存放程序最经常访问的页表
- 分页和分段区别
大小不同:段大小不固定,页大小固定
目的不同:段为根据用户需要划分,对用户可见;页为管理主内存方便划分,对用户透明。
单位:段是信息的逻辑单位;页是信息的物理单位。
- 什么是交换空间?
- 页:操作系统把物理内存划分为一块一块小内存,称之为:页
- 交换空间:是磁盘上的一块空间。
- 交换:当物理内存资源不足时,Linux操作系统会把部分页内容转移到交换空间,这一过程被称之为:交换。
- 虚拟内存总容量=物理内存容量+交换空间容量
- 页面置换算法有哪些?
- 先进先出置换算法:链表实现,将内存页用链表实现FIFO,链表头部页面待时间最长,最先被置换出去,内存新页加到链表尾部即可。
- 最近最久未使用置换算法(LRU):思想:置换最长时间没有被访问的页面。也是链表实现,LRU算法。
- 时钟页面置换算法:思想:每次置换表指针指向页面,指针前进1。实现:环形链表,表指针始终指向的是最老的页面。
- 最不常用置换算法:思想:每个页面都有自己的计算器,每访问一次就+1。缺页中断时,将计数器最小的进行置换。
- 颠簸
- 局部性原理是什么?
- 时间上的局部性:最近被访问的页在不久的将来还会被访问;
- 空间上的局部性:内存中被访问的页周围的页也很可能被访问。
参考:https://blog.youkuaiyun.com/justloveyou_/article/details/78304294
IO
- 零拷贝了解吗?
程序数据读取,写入底层操作:
- 读取:内核空间中数据是通过操作系统I/O接口从磁盘读取,用户空间去内核空间拿数据。
- 写入:将数据放到内核空间,内核空间调用操作系统IO写入到磁盘。
问题:
- 用户态和内核态发生多次上下文切换;
- 用户空间和内核空间发生多次数据拷贝
解决思路:
- 减少用户态和内核态的上下文切换
- 减少用户空间和内核空间的数据拷贝
解决方法:零拷贝技术。该技术有两种实现方法。
- 方法1: mmap()+write:mmap()函数是个系统调用函数。作用是:将内核空间数据映射到用户空间,让内核空间和用户空间不再进行数据拷贝。
- 方法2:sendfile:专门发送文件系统调用函数。替代read和write系统调用,可以完成两个功能。减少了一次系统调用。且可以直接将内核态数据直接拷贝到Socket缓冲区中,不再拷贝到用户态。
应用:很多开源组件:Kakfa、RocketMQ,采用零拷贝技术,提升I/O效率。
- IO多路复用
- IO多路复用理解:一个线程/进程维护多个Socket
- 实现机制:有3种。分别是:select/poll/epoll
- select:
1.select作用:将文件描述符集合fd_set拷贝到内核空间,然后内核遍历fd_set集合,来检查是否有网络事件发生,发生的话标记为可读或可写。然后再把fd_set集合拷贝到用户空间。那么在用户空间,用户态还需要遍历一遍找到内核空间标记的可读或可写的socket,进行处理。
- 文件描述符集合实现:BitsMap。固定长度,默认为:FDSET_SIZE=1024。即只能监听1024个描述符。
- 缺点:
(1) 用户态内核态来回拷贝,用户态内核态都需要遍历—> 开销大,效率低
(2) BitsMap大小固定。不容易修改
- poll:
- 文件描述符集合实现:动态数组,链表形式组织。
- 比select优秀地点:突破了select 的⽂件描述符个数限制,当然还会受到系统⽂件描述符限制。
- 和select其他区别:poll 和 select 并没有太⼤的本质区别,都是使⽤线性结构存储进程关注的Socket集合,因此都需要遍历⽂件描述符集合来找到可读或可写的Socke,时间复杂度为O(n),⽽且也需要在⽤户态与内核态之间拷⻉⽂件描述符集合,这种⽅式随着并发数上来,性能的损耗会呈指数级增⻓。
- epoll. 2个方面解决select和poll问题
- 文件描述符底层数据结构:内核使用红黑树来跟踪进程所有的fd。增删查时间复杂度:O(logn)
- epoll使用事件驱动机制。内核里维护一个就绪事件链表:来存储就绪事件。
(1)事件加入链表时机:socket有事件发生时,通过回调函数将Socket加入到链表。- 具体实现机制:
epoll_create:创建一个eventPoll事件对象,底层是红黑树和就绪链表
epoll_ctl:将待检测的socket事件加入到红黑树。所有添加到红黑树中的事件都会和设备(网卡)驱动程序建立回调关系。当相应事件发生时,会进行调用回调方法,这个方法叫:eppoll_callback,它会将发生的事件添加到rdlist双链表中。
epoll_wait:返回就绪事件链表中就绪的事件个数+就绪事件列表。
从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。 讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。 讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲:
第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。
- epoll 水平触发(LT)与 边缘触发(ET)的区别?
epoll 有 EPOLLLT 和 EPOLLET 两种触发模式:
- LT 是默认的模式:LT 模式下,只要这个 fd 还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作;
- ET 是 “高速” 模式:ET 模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论 fd 中是否还有数据可读。所以在 ET 模式下,read 一个 fd 的时候一定要把它的 buffer 读完,或者遇到 EAGIN 错误。
epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
- select/poll/epoll 支持一个进程所能打开的最大连接数是多少?
- select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,32位机器:32个,64位机器:64个
- poll:无最大连接数限制。原因:基于链表实现。
- epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。
- epoll 缺点
缺点:epoll只能工作在 linux 下
- FD剧增后带来的IO效率问题
- select:用户态向内核态拷贝,遍历等,fd增多有线性性能下降问题。
- poll:同上。
- epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
- select/poll/epoll 的消息传递方式
- select:内核需要将消息传递到用户空间,都需要内核拷贝动作
- poll:同上
- epoll:epoll通过内核和用户空间共享一块内存来实现的。
- Redis IO多路复用技术
redis 是一个单线程却性能非常好的内存数据库, 主要用来作为缓存系统。 redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。
- 为什么 Redis 中要使用 I/O 多路复用这种技术呢?
原因:
- Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务。
- I/O 多路复用就是为了解决这个问题而出现的。redis的io模型主要是基于epoll实现的,不过它也提供了 select和kqueue的实现,默认采用epoll。
- 什么是fd(文件描述符)
定义:
- 形式上:非负整数
- 意思上:代表内核为进程创建文件描述符。它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
- BIO/NIO/AIO是什么?
参考文章:https://segmentfault.com/a/1190000037714804
简单理解:
BIO:
- 同步阻塞IO,是传统IO。
- 实现模式:一个连接一个线程。
- 问题:如果连接不做任何事情,造成不必要的开销。
- 解决方法:通过线程池机制解决
NIO:
- 同步非阻塞IO,多个连接一个线程。基于事件模型。
- 实现模式:服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。
AIO:
- 异步非阻塞IO,有效的请求才启动线程。