1、 概念
什么是操作系统
- 操作系统是系统资源的管理者
- 向上层提供方便易用的服务
- 把硬件功能封装成简单易用的服务,使用户能更加方便地使用计算机
- 用户无需关心底层硬件的原理,只需要对操作系统发出命令即可
- 是最接近硬件的一层软件
特征
并发
共享
这里同时共享也可以是真正同时
虚拟
背景知识:一个程序需要放入内存并给它分配CPU才能执行
运行内存只有4GB,如果多个程序同时运行的内存远大于4GB,那为什么它们还可以同时运行呢? -- 虚拟存储器技术(空分复用技术)
既然一个程序需要分配CPU才能正常执行,那为什么单核CPU的电脑中能同时运行多个程序呢? -- 虚拟处理器技术(时分复用技术)
异步
总结
运行机制
内核程序和应用程序
程序运行过程其实就是CPU执行一条一条指令的过程
特权指令和非特权指令
应用程序只能执行非特权指令
特权指令只能由操作系统内核来使用
- 在CPU设计和生产的时候就划分了特权指令和非特权指令,因此CPU执行一条指令前就能判断出其类型
CPU能判断出特权指令和非特权指令,但是它是怎么区分此时运行的是内核程序还是应用程序呢?
内核态、用户态的切换
中断是操作系统夺回CPU使用权的唯一途径
中断
内中断
与当前执行的指令有关
中断信号来自于CPU内部
发生内中断的情况:
- 试图在用户态下执行特权指令
- 若当前执行的指令是非法的,则会引发一个中断信号;比如执行除法指令时发现除数为 0
- 当应用程序想请求操作系统内部的服务,此时会执行一条特殊指令——陷入指令,该指令会引发一个内部中断信号
外中断
与当前执行的指令无关,
中断信号来源于CPU外部
比如:
- 时钟中断——由时钟部件发来的中断信号
- IO中断请求(指由外部io设备引起的中断)
分类
中断机制基本原理
系统调用
总结
操作系统的内核
2. 进程与线程
进程的组成
PCB
程序段、数据段
程序是如何运行的
进程状态的转换
进程控制
实现线程状态转换是原子操作
如何实现原子性
进程创建
终止进程
进程的阻塞和唤醒
进程的切换
程序是怎样运行的
总结
进程间的通信
共享存储
缺点:存在进程安全问题
消息传递
直接通信方式
需要点名我是谁、我要发给谁
间接通信方式
管道通信
本质上是个队列
与共享存储的区别在于
管道通信是先到先出,只能按这个顺序读取数据
而共享内存是可以直接读取任意位置的数据
线程
系统资源是分配给进程的,线程是共享进程的资源
线程的实现方式
用户级线程
内核级线程
-
多线程模型之一对一模型
-
多线程模型之多对一模型
-
多线程模型之多对多模型
总结
Python和Java区别之一
造成Python没有真正意义上的多线程的原因:
即不管有多少的处理器,任何时候都总是只有一个线程在执行
因为Python是多对一模型【还未搞懂】
线程控制块
进程调度
调度算法
先来先到服务
短作业优先
高响应比优先
总结
3. 进程同步与互斥
进程互斥
进程互斥:
- 把一个时间段内只允许一个进程使用的资源叫做临界资源
- 进程互斥是指 当一个进程访问临界资源时,其它想要访问该临界资源的进程必须等待
总结:
进程互斥的软件实现方法
单标志法
双标志先检查法
双标志后检查法
Peterson算法
进程互斥的硬件实现方法
中断屏蔽方法
互斥锁
解释:
多个核的进程需要使用某个临界资源,其中一个核释放资源后,另一个核立马能使用临界资源
但是在单核中,没有其它进程来释放资源,不会解锁
信号量机制
整型信号量
记录型信号量
信号量机制实现线程同步
4. 死锁
死锁:各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象
死锁、饥饿、死循环的区别
发生死锁的情况
预防死锁
检测死锁
5. 内存
内存管理
覆盖与交换
虚拟内存
传统存储管理方式的特征、缺点:
虚拟内存的定义和特征:
内存映射文件
逻辑结构与物理结构
5. IO设备
io控制器
io控制方式
程序直接控制方式
中断驱动方式
DMA方式
通道控制方式
输入输出应用接口
IO模型
同步阻塞io
一个线程只能接收一个客户端的 io 请求
应用程序发起read调用(系统调用)后,如果没有事件就绪,会一直阻塞,直到内核把数据拷贝到用户空间
同步非阻塞io
解释:
在用户空间调用某个fd的read函数时,首先会将这个fd拷贝一份到内核空间,接着内核空间会判断这个fd对应的socket是不是已经有数据到达,如果没数据到达,则马上返回一个-1,然后去检查下一个fd
也是会一直发起read调用,但是与阻塞io的区别在于,发起系统调用时,如果内核中没有就绪事件,非阻塞io会立即返回,然后去接受其它客户端的连接
当有就绪事件,就会将将数据由内核缓冲区拷贝到用户缓冲区,此时是阻塞的
缺点是:在用户空间需要不断的遍历发起read调用检查是否有数据到来,而系统调用会导致用户态和内核态的一个切换,当socket比较多时,开销较大
io多路复用
核心思想是让单个线程去监视多个连接,一旦某个连接就绪,也就是触发了读/写事件,就会去通知对应的应用程序去主动获取这个就绪的连接,从而进行读写操作,也就是在应用程序里使用单个线程同时去处理多个客户端连接
优点:系统资源消耗小,提升了服务端的连接处理数量
目的:减少系统调用次数
常见的io多路复用机制实现包括:
select -- 基于轮询
当服务端(的用户空间)监听了多个socket,那会将这多个socket的文件描述符信息放到用户空间的数组中,然后通过一次select系统调用,会将所有文件描述符信息发送到内核态,(好处是可以减少内核的调用次数,不用建立一个连接就发送一个文件描述符到内核态)
然后由内核态去遍历检查哪个socket上有数据到达,当某个socket有数据就绪了,就会通知给用户空间,但是用户空间并不知道是哪一个socket数据就绪了,它会再遍历一遍
如果遍历一遍,发现没有就绪事件,就会把当前线程改为阻塞状态
当客户端把数据发送到服务端的网卡上时,网卡设备就会通过中断信号告诉CPU有数据到达,(执行:通过DMA将数据从网卡拷贝到内核缓冲区--将文件描述符状态改为就绪状态)
简单地说就是:一次把多个连接事件发送到内核去处理,从而减少系统调用
缺点:
- 返回的是已就绪的文件描述符的个数,具体哪个不知道
- 监听的文件描述符数量有限制
poll -- 基于轮询
跟select基本类似,主要是优化了监听的文件描述符的数量,突破了1024限制
因为在内核里使用的是链表
epoll -- 基于事件驱动
前面存在一个问题,用户态是不知道具体哪个文件描述符有事件就绪的
- 请描述epoll的执行原理
- 通过红黑树记录了需要监视的文件描述符
- 有一个就绪队列记录了就绪的节点
- 当执行epoll_wait方法时,会将就绪队列中的元素拷贝到一个数组中,然后拷贝到用户态
- epoll为什么比select/poll快,快在哪里
- 因为epoll只有在调用epoll_ctl方法时,在监听事件时,才会把数据从用户态拷贝到内核态
- 而select和poll每次执行时,都需要将所有事件从用户态拷贝到内核态
- epoll采用回调机制将已就绪的文件描述符加入到就绪队列中,而不是采用轮询的方式
- 因为epoll只有在调用epoll_ctl方法时,在监听事件时,才会把数据从用户态拷贝到内核态
- epoll为什么采用红黑树存储需要关注的事件
- 为什么不用hash呢?
- hash的底层使用了数组,无法确定要创建多大的数组
- 为什么不用hash呢?
- epoll一定比poll快吗
- epoll适合高并发场景
零拷贝
传统拷贝
read:
- 当使用read调用来读取数据,此时会将用户态转化为内核态
- CPU对DMA控制器发起一个调用命令
- DMA将数据从硬盘拷贝到内核缓冲区中,拷贝完后通知CPU数据准备好了
- CPU将数据从内核缓冲区中拷贝到用户缓冲区中
- 在4过程中需要将内核态切换为用户态
- 拷贝完后,read调用结束,并唤醒已阻塞的进程
write:
- 当用户线程使用write调用来写入数据,此时会将用户态转化为内核态
- 将用户缓冲区中的数据拷贝到socket缓冲区
- CPU对DMA控制器发起一个调用命令
- DMA控制将数据从socket缓冲区拷贝到网卡设备上(拷贝到网卡,消费者才能消费到数据)
- 拷贝完成后,发送中断信号通知CPU数据已拷贝完成,CPU收到信号后,将内核态切换为用户态,系统调用返回,并唤醒阻塞写的进程
mmap(内存地址映射) + write
mmap:内存地址映射,将内核空间的PageCache映射到用户缓冲区
- 发现传统拷贝过程中,将数据拷贝到用户空间里并没有做任何修改,所以其实没必要拷贝到这里
- 因此这里采用内存映射的方式,将内核缓冲区映射到用户缓冲区(使用的是虚拟内存,不会占用物理内存),就可以减少一次CPU拷贝过程
- 用户进程发起mmp系统调用,此时用户态切换为内核态
- CPU对DMA控制器发起一个调用命令
- DMA将数据从硬盘拷贝到内核缓冲区中
- 拷贝完成后,mmap方法返回调用,此时内核态切换为用户态
- 用户线程使用write调用来写入数据,此时会将用户态转化为内核态
- CPU直接把数据从内核缓冲区拷贝到socket缓冲区
- DMA控制将数据从socket缓冲区拷贝到网卡设备上
- 拷贝完成后,发送中断信号通知CPU数据已拷贝完成,CPU收到信号后,将内核态切换为用户态,系统调用返回,并唤醒阻塞写的进程
-
总结:mmap读取数据快的原因就是,因为建立了pageCache到用户进程的虚拟地址映射,避免了把数据从pageCache拷贝到用户进程的过程,从而减少了一次CPU拷贝
-
注意,依然是两次系统调用,四次上下文切换,但只有三次数据拷贝,其中一次CPU拷贝,两次 DMA 拷贝
kafka生态系统组成
sendfile
与mmap不同在于:
- 数据是完全不经过用户空间,它是直接把数据从内核缓冲区拷贝到socket缓冲区
- 减少了一次系统调用,sendfile系统调用它是包括了把数据从磁盘拷贝到内核态再拷贝到网卡设备这整个过程,而mmap系统调用不包括把数据从内核态拷贝到网卡设备,这一步需要使用write系统调用
总结:包括一次系统调用,三次数据拷贝,其中一次CPU拷贝、两次DMA拷贝
kafka底层代码调用的是:
这个方法使用了sendfile系统调用
sendfile + DMA gather copy
Linux2.4内核版本开始,对sendfile进行了改进
- 引入了gather操作
- 可以实现了无CPU拷贝
与sendfile区别:
- 它是把内核缓冲区中的数据的数据描述信息(包括:内存地址、地址偏移量等)读取到socket缓冲区中,然后就会发生一次DMA gather copy,DMA会根据数据的内存地址和偏移量等信息把数据批量的从内核缓冲区读取到网卡设备上,读取完毕后,发送信号,sendfile系统调用返回,上下文切换到用户态
- 省去了一次CPU拷贝过程
真正的零拷贝:指没有CPU拷贝的过程
splice + DMA copy
与sendfile的区别:
- 在内核缓冲区与socket缓冲区之间搭建了一个环形的管道,将内核缓冲区绑定到管道的写端,把socket缓冲区绑定到管道的读端,所以数据将通过这个管道被读取到socket缓冲区中,然后通过DMA将数据拷贝到网卡设备上,拷贝完成后,splice函数返回,上下文切换到用户态
- 省去了一次CPU拷贝过程