操作系统进程、线程、协程Linux进程状态进程通信线程通信内存分段、分页内存调度磁盘调度进程调度进程切换用户态、内核态调度Linux调度死锁尾递归CAS阻塞非阻塞与同步异步I/O多路复用NIO上下文切换进程内存布局
一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。
1,锁机制(互斥锁提供了以排他方式防止数据结构被并发修改的方法,条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 wait/notify 等待、Volatile 内存共享,CountDownLatch 并发工具);信号量机制(Semaphore);信号机制(Signal)。
2,消息队列(rabbitMQ):当不需要立即获得结果,异步处理:写入消息队列后立即返回客户端,无需等待;限流削峰:请求先入消息队列,而不是由业务处理系统直接处理,做了一次缓冲, 极大地减少了业务处理系统的压力;
点对点模式:点对点的Queue,即Producer发送消息到指定的Queue,接收方从Queue收取消息。
一对多的Topic,即Producer发送消息到指定的Topic,任意多个在线的接收方均可从Topic获得一份完整的消息副本。当Producer想要发送消息的时候,它将消息发送给指定的Exchange,由Exchange将消息根据各种规则投递到一个或多个Queue()路由。送消息时,使用rabbitTemplate.convertAndSend(exchange, routingKey, message)
可以指定Exchange、Routing Key以及消息本身。接收消息时,需要在消息处理的类上标注@RabbitListener
指明要监听的queue,通过多个@RabbitHandler
标注的方法接收不同类型的消息。
逻辑地址(完整、连续、进程隔离、更大内存);物理地址(非连续);地址转换
系统调用将 Linux 整个体系分为用户态和内核态;内核态:控制计算机的硬件资源、稳定安全;用户态:提供应用程序运行的空间、系统调用(os接口)、库函数对系统调用进行封装;给不同的操作给与不同的 “权限”、切换方式(系统调用、异常、外设中断)
(饥饿、长短作业公平性、优先级)先来先服务、短作业优先、时间片轮转、多级队列(新进程优先级高、优先级高时间片越短、抢占式、级别降低)。
内核线程、调度基于线程,nice值越低优先级越高
前提:互斥资源、请求新资源、不可抢占、等待关系闭环。
死锁检测:单资源单向有向图环检测(以每个节点为起点,深度优先、重复)。
死锁恢复:抢占恢复(用完归还)、杀死进程(选择副作用小的进程)。
死锁避免:安全前提(存在非死锁序列)下分配资源(银行家算法)。
死锁预防:非独占(假脱机打印)、非占有等待(一次请求全部资源)、破坏不可抢占(请求失败释放占有资源)。破坏等待(资源编号、请求资源序号升序、无环产生)
传统递归在递归返回后好需要继续运算,保留栈帧;空间O(N);尾递归在递归返回后无后续运算,当前递归结果已被收集(二叉查找树的左右子节点为参数),无需保留栈帧,空间O(1)
乐观锁;根据地址v取值A=get(v)->B=f(A)->A==get(v)->成立则将B写入v,失败则不断重复至成功;重复读取get(v),单变量原子性(封装),ABA问题(版本号比较)
https://www.cnblogs.com/loveer/p/11479249.html
IO分两阶段1.数据准备阶段,2.内核空间复制数据到用户进程缓冲区(用户空间)阶段
通过一种机制,可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,没有就绪事件时,就会阻塞交出cpu。多路是指多个链接,复用指的是复用同一线程。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
https://developer.aliyun.com/article/763247
https://juejin.cn/post/6931543528971436046
select:时间复杂度O(n),通过设置或者检查存放fd标志位的数据结构(fd数组为整型数组,用于保存文件描述符)来进行下一步处理,它仅仅知道有I/O事件发生了,却并不知道是哪那几个流,只能无差别轮询所有流,找出能读出数据,或者写入数据的流,效率较低。单个进程可监视的fd数量被限制,即能监听端口的大小有限。内核需要将消息传递到用户空间时需要内核拷贝动作,每次调用select,都需要把fd集合从用户态拷贝到内核态。
1,用户线程调用select,将fd_set从用户空间拷贝到内核空间 2. 内核在内核空间对fd_set遍历一遍,检查是否有就绪的socket描述符,如果没有的话,就会进入休眠,直到有就绪的socket描述符 3. 内核返回select的结果给用户线程,即就绪的文件描述符数量 4. 用户拿到就绪文件描述符数量后,再次对fd_set进行遍历,找出就绪的文件描述符 5. 用户线程对就绪的文件描述符进行读写操作。
poll:时间复杂度O(n),本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。但是它没有最大连接数的限制,原因是它是基于链表来存储fd的。每次调用poll,都需要把fd集合从用户态拷贝到内核态。
1,用户线程调用poll系统调用,并将文件描述符链表拷贝到内核空间。2,内核对文件描述符遍历一遍,如果没有就绪的描述符,则内核开始休眠,直到有就绪的文件描述符。3,返回给用户线程就绪的文件描述符数量。4,用户线程再遍历一次文件描述符链表,找出就绪的文件描述符。5,用户线程对就绪的文件描述符进行读写操作。
epoll:时间复杂度O(1),epoll可以理解为event poll,给每个fd注册一个回调函数,当fd对应的设备发生IO事件时,就会调用这个回调函数,将该fd放到一个链表中,然后唤醒在epoll_wait中进入睡眠的进程,最后只要判断一下就绪链表是否为空就行了,非空就从该链表中取出一个fd,以此达到O(1)的时间复杂度。效率提升,不是轮询的方式;根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,而跟连接总数无关,会随着fd数量上升而效率下降。使用内存映射(mmap),不需要从用户空间频繁拷贝fd数据到内核空间。
mmap,是将文件/设备映射到内存中,进程可以通过读写内存的方式,实现对被mmap文件的操作。进程通过mmap映射相同的文件,实现共享内存方式的通信。对于大量频繁读写的文件,mmap相对read/write的方式,避免了内核空间->用户空间的数据传输和切换(epoll)。
具体实现:对应着有三个函数:
epoll_create:epoll_create相当于在内核中创建一个存放fd的数据结构。在select和poll方法中,内核都没有为fd准备存放其的数据结构,只是简单粗暴地把数组或者链表复制进来;而epoll则不一样,epoll_create会在内核建立一颗专门用来存放fd结点的红黑树,储监控的文件描述符,后续如果有新增的fd结点,都会注册到这个epoll红黑树上。
epoll_ctr:select和poll会一次性将监听的所有fd都复制到内核中,而epoll不一样,当需要添加一个新的fd时,会调用epoll_ctr,给这个fd注册一个回调函数,然后将该fd结点注册到内核中的红黑树中。当该fd对应的设备活跃时,会调用该fd上的回调函数,将该结点存放在一个就绪链表(存储就绪的文件描述符)中。这也解决了在内核空间和用户空间之间进行来回复制的问题。
epoll_wait:epoll_wait的做法也很简单,其实直接就是从就绪链表中取结点,这也解决了轮询的问题,时间复杂度变成O(1)
Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的时候触发。当缓冲区有数据可取的时候,ET会触发一次事件,之后就不会再触发,而LT只要我们没有取完缓冲区的数据,就会一直触发。
https://tech.meituan.com/2016/11/04/nio.html
https://xie.infoq.cn/article/fb524c4992beea6bb4487af87
https://www.cnblogs.com/loveer/p/11479887.html
1,是一种同步非阻塞的 I/O 模型,在等待就绪阶段都是非阻塞的,真正的 I/O 操作是同步阻塞。是 I/O 多路复用的基础,成为解决高并发与大量连接、I/O 处理问题的有效方式。
2,服务器端同步阻塞 I/O 处理:socket.accept()、socket.read()、socket.write() 三个主要函数都是同步阻塞的,当一个连接在处理 I/O 的时候,系统是阻塞的,所以使用多线程时,就可以让 CPU 去处理更多的事情。低并发下结合线程池使得创建和回收成本相对较低,并且编程模型简单。创建和销毁都是重量级的系统函数,线程本身占用较大内存,线程的切换成本是很高的,无法应对百万级连接。
3,所有的系统 I/O 都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。NIO 里用户最关心” 我可以读了”。NIO的读写函数可以立刻返回而不是柱塞,如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,将用于传输的通道全部注册到选择器上,选择器监控通道,当某一通道就绪后连接继续进行读写,没有必要开启多线程。没有线程切换,只是拼命的读、写、选择事件。
4,Java NIO 实际读写时的核心在于:通道(Channel)和缓冲区(Buffer),选择器。通道表示打开到 IO 设备(文件流、套接字)的连接,对原 I/O 包中的流的模拟,负责传输;缓冲区用于容纳数据,负责存储,Channel的读写必须通过buffer对象,然后操作缓冲区,对数据进行处理。缓存区是双向的,既可以往缓冲区写入数据,也可以从缓冲区读取数据:缓冲区<->然后缓冲区通过通道进行传输<->从缓冲区取数据。选择器:把Channel通道注册到Selector中,通过Selecotr监听Channel中的事件状态,这样就不需要阻塞等待客户端的连接,从主动等待客户端的连接,变成了通过事件驱动,通过事件驱动实现单线程管理多个Channel的目的。
缓冲区根据数据类型的不同,可以进行划分ByteBuffer、CharBuffer等。根据工作方式分:直接缓冲区(磁盘->内核地址空间中->用户地址空间中->读取到应用程序)与非直接缓冲区(将缓冲区建立在物理内存之中,读写数据直接通过物理内存进行),
就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
进程上下文切换 :进程是由内核来管理和调度的,进程的切换只能发生在内核态,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。(用户态-内核态-用户态)
线程上下文切换:当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
中断上下文切换:了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。