diff --git a/src/main/java/com/other/OPERATING_SYSTEM.md b/src/main/java/com/other/OPERATING_SYSTEM.md
index d0f211d..c4f7306 100644
--- a/src/main/java/com/other/OPERATING_SYSTEM.md
+++ b/src/main/java/com/other/OPERATING_SYSTEM.md
@@ -973,7 +973,7 @@ Linux 文件系统会为每个文件分配两个数据结构:**索引节点(in
> 软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。
-### 文件IO
+### 文件/网络IO
- 缓冲与非缓冲 I/O
- 直接与非直接 I/O
- 阻塞与非阻塞 I/O VS 同步与异步 I/O
@@ -1050,6 +1050,9 @@ I/O 是分为两个过程的:
`select/poll/epoll` 在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可。
+
+
+
##### select/poll
select 实现多路复用的方式是,将已连接的 Socket 都放到一个**文件描述符集合**,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生。\
@@ -1064,13 +1067,87 @@ poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动
`poll` 和 `select` 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在**用户态与内核态之间拷贝文件描述符集合**,这种方式随着并发数上来,性能的损耗会呈指数级增长。
##### epoll
-epoll 通过两个方面,很好解决了 select/poll 的问题。
+epoll 通过两个方面,很好解决了 select/poll 的问题。epoll 是解决 C10K 问题的利器。
1. 第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,增删改一般时间复杂度是 `O(logn)`。
2. 第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件。给每个fd注册一个回调函数,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中。以此达到O(1)的时间复杂度
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/epoll.png)
+
+**边缘触发和水平触发**
+
+
+- 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
+- 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
+> 水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
+
+简单说——水平触发代表了一种“**状态**”。边沿触发代表了一个“**事件**”。
+
+
+`select/poll` 只有水平触发模式,`epoll` 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
+一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 `epoll_wait` 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
+
+边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。
+**多路复用 API 返回的事件并不一定可读写的**,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。
+
+
+#### Reactor
+
+**单Reactor单进程/线程**
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/singleReactor.png)
+
+进程里有 Reactor、Acceptor、Handler 这三个对象:
+- Reactor 对象的作用是监听和分发事件;
+- Acceptor 对象的作用是获取连接;
+- Handler 对象的作用是处理业务;
+
+介绍下「单 Reactor 单进程」这个方案:
+- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
+- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
+- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
+- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
+
+2 个缺点:
+- 第一个缺点,因为只有一个进程,无法充分利用 **多核 CPU 的性能**;
+- 第二个缺点,Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,**如果业务处理耗时比较长,那么就造成响应的延迟**;
+> 单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
+
+**单 Reactor 多线程 / 多进程**
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/singleReactorMulti.png)
+
+前三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤:
+- Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;
+- 子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
+
+单 Reactor 多线程的方案优势在于能够充分利用**多核 CPU 的性能**,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。
+
+「单 Reactor」的模式还有个问题,因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
+
+
+**多 Reactor 多进程 / 线程**
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/multiReactor.png)
+
+方案详细说明如下:
+- 主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
+- 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
+- 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
+- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
+
+两个开源软件 Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案。
+
+
+#### Proactor
+- **Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件**。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
+- **Proactor 是异步网络模式, 感知的是已完成的读写事件**。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
+> Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」
+
+无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「**待完成**」的 I/O 事件,而 Proactor 模式则是基于「**已完成**」的 I/O 事件。
+
+
### page cache
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/linux-filesystem-cache.png)
diff --git a/src/main/java/com/toc/ALGORITHM.md b/src/main/java/com/toc/ALGORITHM.md
index 216f38e..f29da8c 100644
--- a/src/main/java/com/toc/ALGORITHM.md
+++ b/src/main/java/com/toc/ALGORITHM.md
@@ -656,8 +656,8 @@ public class Solution {
- [移动零](https://leetcode-cn.com/problems/move-zeroes/)
- [删除链表的倒数第 N 个结点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/)
- [下一个排列](https://leetcode-cn.com/problems/next-permutation/): 查找的方法比较巧妙,递减区间的利用
+- [下一个更大元素 III](https://leetcode.cn/problems/next-greater-element-iii/): 与上述类似
- [删除有序数组中的重复项 II](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/): 快慢指针,慢指针为确定的区间
-
### 二分法[Top]
[一道可以考察「二分」本质的面试题](https://mp.weixin.qq.com/s/RW20ob2oO4Bfd-PcukTVJA)
> 「⼆分」的本质是⼆段性,并⾮单调性。只要⼀段满⾜某个性质,另外⼀段不满⾜某个性质,就可以⽤「⼆分」
@@ -1579,7 +1579,7 @@ class Solution {
- [环形子数组的最大和](https://leetcode-cn.com/problems/maximum-sum-circular-subarray/)
- [不同的子序列](https://leetcode-cn.com/problems/distinct-subsequences/): review
- [最长递增子序列的个数](https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/): review
-
+- [最长的斐波那契子序列的长度](https://leetcode.cn/problems/length-of-longest-fibonacci-subsequence/): 子序列状态变形
#### 子数组问题[Top]
- [最长重复子数组](https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/): review
diff --git a/src/main/java/com/toc/OPERATING_SYSTEM.md b/src/main/java/com/toc/OPERATING_SYSTEM.md
index fec661d..e2f3b30 100644
--- a/src/main/java/com/toc/OPERATING_SYSTEM.md
+++ b/src/main/java/com/toc/OPERATING_SYSTEM.md
@@ -21,17 +21,70 @@
3.4. 段页式内存管理
3.5. Linux 内存管理
3.6. 两种分配内存的调用
- 4. 进程管理
- 4.1. 进程
- 4.2. 线程
- 4.3. 线程与进程比较
- 4.4. 处理器调度
- 4.5. 经典同步问题
- 4.5.1. 生产者-消费者问题
- 4.5.2. 读者-写者问题
-计算机组成原理
- 1. 局部性原理
-中间件设计资料
+ 3.7. 内存分配过程
+ 3.7.1. 内存回收类型
+ 3.7.2. 内存回收优化
+ 3.7.3. OOM Killer
+ 3.8. Swap机制
+ 4. 进程管理
+ 4.1. 进程
+ 4.2. 线程
+ 4.3. 线程与进程比较
+ 4.4. 上下文切换
+ 4.4.1. CPU上下文切换
+ 4.4.2. 进程上下文切换
+ 4.4.3. 线程上下文切换
+ 4.5. 处理器调度
+ 4.5.1. 调度基本原则:
+ 4.5.2. 调度算法
+ 4.6. 进程通信
+ 4.6.1. 管道通信
+ 4.6.2. 消息队列
+ 4.6.3. 共享内存
+ 4.6.4. 信号量
+ 4.6.5. 信号
+ 4.6.6. Socket
+ 4.7. 并发
+ 4.8. 经典同步问题
+ 4.8.1. 生产者-消费者问题
+ 4.8.2. 哲学家就餐问题
+ 4.8.3. 读者-写者问题
+ 4.9. 死锁
+ 4.10. 锁
+ 4.10.1. 互斥锁
+ 4.10.2. 自旋锁
+ 4.10.3. 读写锁
+ 4.10.4. 乐观锁与悲观锁
+ 5. 文件系统
+ 5.1. 文件系统组成
+ 5.2. 虚拟文件系统
+ 5.3. 文件使用
+ 5.4. 文件存储
+ 5.5. 文件系统结构
+ 5.6. 软链接和硬链接
+ 5.7. 文件/网络IO
+ 5.7.1. 缓冲与非缓冲 I/O
+ 5.7.2. 直接与非直接 I/O
+ 5.7.3. 阻塞与非阻塞 I/O VS 同步与异步 I/O
+ 5.7.4. IO多路复用
+ 5.7.4.1. select/poll
+ 5.7.4.2. epoll
+ 5.7.5. Reactor
+ 5.7.6. Proactor
+ 5.8. page cache
+ 5.8.1. page与page cache
+ 5.8.2. Page Cache 与 buffer cache
+ 5.8.3. Page Cache 与预读
+ 5.8.4. 持久化的一致性&可靠性
+ 5.8.5. page cache 优劣势
+ 5.8.6. 参考资料
+ 5.9. 直接内存访问
+ 5.10. 零拷贝
+ 5.10.1. mmap + write
+ 5.10.2. sendfile
+计算机组成原理
+ 1. 局部性原理
+中间件设计资料
# 操作系统[Top]
**参考资料:大量参考**=>[小林coding-图解系统介绍](https://xiaolincoding.com/os/)
@@ -97,8 +150,8 @@ CPU 内部还有一些组件,常见的有**寄存器、控制单元和逻辑
**为了避免低效率的串行传输的方式,线路的位宽最好一次就能访问到所有的内存地址。**
CPU 要想操作的内存地址就需要地址总线:
-- 如果地址总线只有 1 条,那每次只能表示 「0 或 1」这两种地址,所以 CPU 能操作的内存地址最大数量为 2(2^1)个(注意,不要理解成同时能操作 2 个内存地址);
-- 如果地址总线有 2 条,那么能表示 00、01、10、11 这四种地址,所以 CPU 能操作的内存地址最大数量为 4(2^2)个。
+- 如果地址总线只有 1 条,那每次只能表示 「0 或 1」这两种地址,所以 CPU 能操作的内存地址最大数量为 2(2^1)个(注意,不要理解成同时能操作 2 个内存地址);
+- 如果地址总线有 2 条,那么能表示 00、01、10、11 这四种地址,所以 CPU 能操作的内存地址最大数量为 4(2^2)个。
> 那么,想要 CPU 操作 4G 大的内存,那么就需要 32 条地址总线,因为 2 ^ 32 = 4G。
> 对于 64 位 CPU 就可以一次性算出加和两个 64 位数字的结果,因为 64 位 CPU 可以一次读入 64 位的数字,并且 64 位 CPU 内部的逻辑运算单元也支持 64 位数字的计算。
@@ -108,13 +161,13 @@ CPU 要想操作的内存地址就需要地址总线:
### 指令[Top]
流水线的方式来执行指令: Fetch -> Decode -> Execution -> Store
-**指令周期(Instruction Cycle)**:CPU 的工作就是一个周期接着一个周期,周而复始。
+**指令周期(Instruction Cycle)**:CPU 的工作就是一个周期接着一个周期,周而复始。
四个阶段的具体含义:
-1. CPU 通过程序计数器读取对应内存地址的指令,这个部分称为 **Fetch(取得指令)**;
-2. CPU 对指令进行解码,这个部分称为 **Decode(指令译码)**;
-3. CPU 执行指令,这个部分称为 **Execution(执行指令)**;
-4. CPU 将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为 **Store(数据回写)**;
+1. CPU 通过程序计数器读取对应内存地址的指令,这个部分称为 **Fetch(取得指令)**;
+2. CPU 对指令进行解码,这个部分称为 **Decode(指令译码)**;
+3. CPU 执行指令,这个部分称为 **Execution(执行指令)**;
+4. CPU 将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为 **Store(数据回写)**;
不同的阶段其实是由计算机中的不同组件完成的:
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/instruction-process.png)
@@ -126,14 +179,14 @@ CPU 要想操作的内存地址就需要地址总线:
**指令的执行速度**
-`程序CPU的运行时间 = CPU 时钟周期数(CPU Cycles) * 时钟周期时间(Clock Cycle Time)`
+`程序CPU的运行时间 = CPU 时钟周期数(CPU Cycles) * 时钟周期时间(Clock Cycle Time)`
时钟周期时间:就是CPU主频,主频越高说明CPU的工作速度就越快。`2.4GHz`就是电脑的主频,**时钟周期时间**就是 `1/2.4G`。
-CPU时钟周期数可以进一步拆解成:`CPU时钟周期数 = 指令数 * 每条指令的平均时钟周期数(Cycles Per Instruction,简称 CPI)`
+CPU时钟周期数可以进一步拆解成:`CPU时钟周期数 = 指令数 * 每条指令的平均时钟周期数(Cycles Per Instruction,简称 CPI)`
- **指令数**:表示执行程序所需要多少条指令,以及哪些指令。这个层面是基本靠编译器来优化。
-- **每条指令的平均时钟周期数 CPI**:表示一条指令需要多少个时钟周期数,现代大多数 CPU 通过流水线技术(Pipeline),让一条指令需要的 CPU 时钟周期数尽可能的少;
+- **每条指令的平均时钟周期数 CPI**:表示一条指令需要多少个时钟周期数,现代大多数 CPU 通过流水线技术(Pipeline),让一条指令需要的 CPU 时钟周期数尽可能的少;
- **时钟周期时间**:表示计算机主频,取决于计算机硬件。
## 操作系统[Top]
@@ -156,6 +209,9 @@ CPU时钟周期数可以进一步拆解成:`CPU时钟周期数 = 指令数 *
3. 虚拟性:操作系统中,虚拟指的是一个物理上的实体变为若干个逻辑上的对应物,前者是实际存在的,而后者是虚拟的。
4. 异步性
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/concurrent-parallel.png)
+
+
操作系统基本功能
1. 处理器管理
2. 存储器管理
@@ -235,10 +291,10 @@ Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用
如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。
于是,这里就引出了两种地址的概念:
-- 我们程序所使用的内存地址叫做**虚拟内存地址**(Virtual Memory Address)
-- 实际存在硬件里面的空间地址叫**物理内存地址**(Physical Memory Address)。
+- 我们程序所使用的内存地址叫做**虚拟内存地址**(Virtual Memory Address)
+- 实际存在硬件里面的空间地址叫**物理内存地址**(Physical Memory Address)。
-操作系统引入了虚拟内存,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。
+操作系统引入了虚拟内存,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/virtual-memory.png)
@@ -291,7 +347,7 @@ Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用
> 由于内存空间都是预先划分好的,也就不会像分段会产生间隙非常小的内存,这正是分段会产生内存碎片的原因。而**采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存。**
- 换出(Swap Out): 如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上。
-- 换入(Swap In): 将从内存页面释放而暂存在硬盘上的数据,重新加载进来。
+- 换入(Swap In): 将从内存页面释放而暂存在硬盘上的数据,重新加载进 来。
内存分页相比内存分段好处:
1. 对于需要内存交换的情况,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
@@ -313,16 +369,16 @@ Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用
- 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。
> 简单分页管理的缺陷?
-> 在 `32` 位的环境下,虚拟地址空间共有 `4GB`,假设一个页的大小是 `4KB(2^12)`,那么就需要大约 `100 万 (2^20)` 个页,每个「页表项」需要 `4` 个字节大小来存储,那么整个 `4GB` 空间的映射就需要有 `4MB` 的内存来存储页表。\
+> 在 `32` 位的环境下,虚拟地址空间共有 `4GB`,假设一个页的大小是 `4KB(2^12)`,那么就需要大约 `100 万 (2^20)` 个页,每个「页表项」需要 `4` 个字节大小来存储,那么整个 `4GB` 空间的映射就需要有 `4MB` 的内存来存储页表。\
> 这 `4MB` 大小的页表,看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。\
> 那么,`100` 个进程的话,就需要 `400MB` 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。
多级页表(Multi-Level Page Table)\
在 32 位和页大小 4KB 的环境下,一个进程的页表需要装下 100 多万个「页表项」,并且每个页表项是占用 4 字节大小的,于是相当于每个页表需占用 4MB 大小的空间。\
-我们把这个 100 多万个「页表项」的单级页表再分页,将页表(一级页表)分为 1024 个页表(二级页表),每个表(二级页表)中包含 1024 个「页表项」,形成二级分页。
+我们把这个 100 多万个「页表项」的单级页表再分页,将页表(一级页表)分为 1024 个页表(二级页表),每个表(二级页表)中包含 1024 个「页表项」,形成二级分页。
进一步节省内存方案:**如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。**
-> 做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB,这对比单级页表的 4MB 是不是一个巨大的节约?
+> 做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB,这对比单级页表的 4MB 是不是一个巨大的节约?
对于 64 位的系统,两级分页肯定不够了,就变成了四级目录,分别是:
- 全局页目录项 PGD(Page Global Directory)
@@ -340,7 +396,7 @@ Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用
`TLB(Translation LookAside Buffer)`: 在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,通常称为页表缓存、转址旁路缓存、快表等。
-在 CPU 芯片里面,封装了内存管理单元(Memory Management Unit)芯片,它用来完成地址转换和 TLB 的访问与交互。 有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
+在 CPU 芯片里面,封装了内存管理单元(Memory Management Unit)芯片,它用来完成地址转换和 TLB 的访问与交互。 有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/TLB.png)
@@ -365,7 +421,7 @@ Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用
Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制。 这主要是Intel 处理器发展历史导致的
-Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。
+Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/linux-memory-1.png)
@@ -393,17 +449,114 @@ malloc 申请的内存,free 释放内存会归还给操作系统吗?
**mmap与brk分配对比**:
mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。
-也就是说,频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
+也就是说,频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
brk的分配方式,调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。\
等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。\
brk的分配方式问题在于,如果申请的空间没办法复用,那么将会导致堆内将产生越来越多不可用的碎片,导致“内存泄露”。
+### 内存分配过程[Top]
+
+> 应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
+当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存,这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的`Page Fault Handler`(缺页中断函数)处理。
+
+缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
+
+如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。
+- **后台内存回收**(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程**异步**的,不会阻塞进程的执行。
+- **直接内存回收**(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是**同步**的,会阻塞进程的执行。
+如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会触发`OOM(Out of Memory)`机制。
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/memory-assign.png)
+
+
+#### 内存回收类型[Top]
+主要有两类内存可以被回收,而且它们的回收方式也不同。
+
+- 文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。
+- 匿名页(Anonymous Page):这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过 Linux 的 Swap 机制,Swap 会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
+
+文件页和匿名页的回收都是基于 LRU 算法,也就是优先回收不常访问的内存。LRU 回收算法,实际上维护着 `active` 和 `inactive` 两个双向链表,其中:
+
+- `active_list` 活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页;
+- `inactive_list` 不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页;
+
+活跃和非活跃的内存页,按照类型的不同,又分别分为文件页和匿名页
+
+- 文件页的回收:对于干净页是直接释放内存,这个操作不会影响性能,而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘 `I/O` 的,这个操作是会影响系统性能的。
+- 匿名页的回收:如果开启了 `Swap` 机制,那么 `Swap` 机制会将不常访问的匿名页换出到磁盘中,下次访问时,再从磁盘换入到内存中,这个操作是会影响系统性能的。
+
+回收内存的操作基本都会发生磁盘 I/O 的,如果回收内存的操作很频繁,意味着磁盘 I/O 次数会很多。文件页的回收操作对系统的影响相比匿名页的回收操作会少一点,因为文件页对于干净页回收是不会发生磁盘 `I/O` 的,而匿名页的 `Swap` 换入换出这两个操作都会发生磁盘 `I/O`。
+
+
+#### 内存回收优化[Top]
+
+> Linux 提供了一个 `/proc/sys/vm/swappiness` 选项,用来调整文件页和匿名页的回收倾向。`swappiness` 的范围是 `0-100`,数值越大,越积极使用 `Swap`,也就是更倾向于回收匿名页;数值越小,越消极使用 `Swap`,也就是更倾向于回收文件页。\
+> 一般建议 swappiness 设置为 0(默认值是 60),这样在回收内存的时候,会更倾向于文件页的回收,但是并不代表不会回收匿名页。
+
+可以通过尽早的触发**后台内存回收**来避免应用程序进行直接内存回收。
+
+内核定义了三个内存阈值(watermark,也称为水位),用来衡量当前剩余内存(pages_free)是否充裕或者紧张,分别是:
+- 页最小阈值(pages_min)
+- 页低阈值(pages_low)
+- 页高阈值(pages_high)
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/memory-watermark.png)
+- 橙色区域:剩余内存(pages_free)在页低阈值(pages_low)和页最小阈值(pages_min)之间,说明内存压力比较大,剩余内存不多了。这时 **`kswapd0` 会执行内存回收,直到剩余内存大于高阈值(pages_high)为止**
+- 红色区域:如果剩余内存(pages_free)小于页最小阈值(pages_min),说明用户可用内存都耗尽了,此时就会**触发直接内存回收**。
-## 进程管理[Top]
+页低阈值(pages_low)可以通过内核选项 `/proc/sys/vm/min_free_kbytes` (该参数代表系统所保留空闲内存的最低限)来间接设置。与其他参数的关系如下:
+```
+pages_min = min_free_kbytes
+pages_low = pages_min*5/4
+pages_high = pages_min*3/2
+```
-### 进程[Top]
+#### OOM Killer[Top]
+
+OOM Killer 机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
+
+`oom_badness()` 函数,它会把系统中可以被杀掉的进程扫描一遍,并对每个进程打分,得分最高的进程就会被首先杀掉。
+
+进程得分的结果受下面这两个方面影响:
+- 第一,进程已经使用的物理内存页面数。
+- 第二,每个进程的 OOM 校准值 oom_score_adj。它是可以通过 `/proc/[pid]/oom_score_adj` 来配置的。我们可以在设置 -1000 到 1000 之间的任意一个数值,调整进程被 OOM Kill 的几率。
+
+### Swap机制[Top]
+Swap 机制指的是当物理内存不够用,内存管理单元(Memory Management Unit,MMU)需要提供调度算法来回收相关内存空间,然后将清理出来的内存空间给当前内存申请方。
+
+Swap 机制存在的本质原因是 Linux 系统提供了虚拟内存管理机制,每一个进程认为其独占内存空间,因此所有进程的内存空间之和远远大于物理内存。所有进程的内存空间之和超过物理内存的部分就需要交换到磁盘上。
+
+**缺页中断**: 操作系统以 page 为单位管理内存,当进程发现需要访问的数据不在内存时,操作系统可能会将数据以页的方式加载到内存中。上述过程被称为缺页中断,当操作系统发生缺页中断时,就会通过系统调用将 page 再次读到内存中。
+
+**页面替换(Page Replacement)**: 主内存的空间是有限的,当主内存中不包含可以使用的空间时,操作系统会从选择合适的物理内存页驱逐回磁盘,为新的内存页让出位置,**选择待驱逐页的过程在操作系统中叫做页面替换(Page Replacement),替换操作又会触发 swap 机制**。
+
+
+> 在 32 位操作系统,因为进程最大只能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。\
+> 在 64 位操作系统,因为进程最大只能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。
+
+
+如果申请物理内存大小超过了空闲物理内存大小,就要看操作系统有没有开启 Swap 机制:
+- 如果没有开启 Swap 机制,程序就会直接 OOM;
+- 如果有开启 Swap 机制,程序可以正常运行。
+
+Swap 就是把一块磁盘空间或者本地文件,当成内存来使用,它包含换出和换入两个过程:
+- 换出(Swap Out) ,是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存;
+- 换入(Swap In),是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来;
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/swap.png)
+
+Linux 中的 Swap 机制会在内存不足和内存闲置的场景下触发:
+- 内存不足:当系统需要的内存超过了可用的物理内存时,内核会将内存中不常使用的内存页交换到磁盘上为当前进程让出内存,保证正在执行的进程的可用性,这个内存回收的过程是强制的直接内存回收(Direct Page Reclaim)。直接内存回收是同步的过程,会阻塞当前申请内存的进程。
+- 内存闲置:应用程序在启动阶段使用的大量内存在启动后往往都不会使用,通过后台运行的守护进程(kSwapd)。我们可以将这部分只使用一次的内存交换到磁盘上为其他内存的申请预留空间。`kSwapd` 是 Linux 负责页面置换(Page replacement)的守护进程,它也是负责交换闲置内存的主要进程,它会在空闲内存低于一定水位(opens new window)时,回收内存页中的空闲内存保证系统中的其他进程可以尽快获得申请的内存。`kSwapd` 是后台进程,所以回收内存的过程是异步的,不会阻塞当前申请内存的进程。
+
+
+
+
+## 进程管理[Top]
+
+### 进程[Top]
进程:在计算机操作系统中,进程是资源分配的基本单元,也是独立运行的基本单元。
进程特征:
@@ -430,14 +583,41 @@ brk的分配方式问题在于,如果申请的空间没办法复用,那么
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/process-state.png)
-### 线程[Top]
+**进程控制块**(process control block,PCB): PCB 是进程存在的唯一标识,用来来描述进程的数据结构。
+
+PCB通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。
+- 将所有处于就绪状态的进程链在一起,称为就绪队列
+- 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列
+
+一个进程最多可以创建多少个线程?
+- 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
+- 64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。
+
+
+### 线程[Top]
线程是进程内一个相对独立的、可调度的执行单元。线程基本上不拥有资源,只拥有在运行时必不可少的资源(如程序计数器、一组寄存器和栈),但它可以和其他线程共享进程拥有的全部资源。
线程的实现:
-1. 内核级线程:依赖于内核,由操作系统内核完成创建和撤销工作的线程。内核维护进程和线程的上下文信息并完成线程切换工作。
+1. 内核级线程(Kernel Thread):依赖于内核,由操作系统内核完成创建和撤销工作的线程。内核维护进程和线程的上下文信息并完成线程切换工作。
> 一个内核级线程由于I/O操作而阻塞时,不会影响其他线程的运行。这时,**处理器时间片分配的对象为线程**。
-2. 用户级线程:不依赖于操作系统核心,由应用程序利用线程库提供创建、同步、调度和管理线程的函数来控制线程。
+2. 用户级线程(User Thread):不依赖于操作系统核心,由应用程序利用线程库提供创建、同步、调度和管理线程的函数来控制线程。
> 用户级线程维护由应用程序完成,内核不需要了解用户级线程存在。用户级线程切换不需要内核特权,通常应用程序的线程调度使用非抢占式或更简单的规则。这时候处理器的时间片是分配给进程的。
+3. 轻量级进程(Light-weight process,LWP):在内核中来支持用户线程;是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持。
+
+
+轻量级进程(Light-weight process,LWP)是内核支持的用户线程,LWP 只能由内核管理并像普通进程一样被调度,Linux 内核是支持 LWP 的典型例子。
+
+> 在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。一般来说,一个进程代表程序的一个实例,而 LWP 代表程序的执行线程,因为一个执行线程不像进程那样需要那么多状态信息,所以 LWP 也不带有这样的信息。
+
+在 LWP 之上也是可以使用用户线程的,那么 LWP 与用户线程的对应关系就有三种:
+- 1 : 1,即一个 LWP 对应 一个用户线程;
+- N : 1,即一个 LWP 对应多个用户线程;
+- M : N,即多个 LWP 对应多个用户线程;
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/lwp.png)
+
+
+
**多线程模型**:
@@ -457,7 +637,10 @@ brk的分配方式问题在于,如果申请的空间没办法复用,那么
> 多个用户线程对应多个内核线程,使得库和操作系统都可以管理线程,用户线程由运行时库调度器管理,内核线程由操作系统调度器管理,可运行的用户线程由运行时库分派并标记为准备好执行的可用线程,操作系统选择用户线程并将它映射到可用内核线程.
-### 线程与进程比较[Top]
+
+
+
+### 线程与进程比较[Top]
- **拥有资源**:进程为拥有系统资源的基本单元,而线程不拥有系统资源,但是线程可以访问隶属进程的系统资源
- **并发性**:进入线程的操作系统,不仅进程可以并发,而且统一进程的线程也可以并发。系统的并发度更高,大大提高了系统的吞吐量。
- **调度**:统一进程中的线程切换不会引起进程切换,而不同进程的线程切换会引起进程切换。
@@ -465,36 +648,194 @@ brk的分配方式问题在于,如果申请的空间没办法复用,那么
- 创建或撤销开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。操作系统所付出的代价远大于创建或撤销线程是的开销。
- 切换开销:进程切换时,涉及整个当前进程CPU环境的保存以及新进程的CPU环境的设置。而线程上下文的切换,只需要保存和设置少量寄存器的内容。多线程的同步和通信因为共享同一进程,甚至无需操作系统干预。
-### 处理器调度[Top]
+线程相比进程能减少开销,体现在:
+- 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
+- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
+- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
+- 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
+
+
+> 线程崩溃,进程一定会崩溃吗?
+
+如果线程是因为非法访问内存引起的崩溃,那么进程肯定会崩溃,为什么系统要让进程崩溃呢,这主要是因为在进程中,**各个线程的地址空间是共享的**,那么某个线程对地址的非法访问就会导致内存的不确定性,进而可能会影响到其他线程。 进程是崩溃主要通过信号机制来实现的。
+
+> 为什么线程崩溃不会导致 JVM 进程崩溃?
+
+原因其实就是虚拟机内部定义了信号处理函数,而在信号处理函数中对这两者做了额外的处理以让 JVM 不崩溃,另一方面也可以看出如果 JVM 不对信号做额外的处理,最后会自己退出并产生 crash 文件 `hs_err_pid_xxx.log`(可以通过`-XX:ErrorFile=/var/log/hs_err.log`这样的方式指定),这个文件记录了虚拟机崩溃的重要原因。
+
+
+
+### 上下文切换[Top]
+
+#### CPU上下文切换[Top]
+CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 **CPU 上下文**。
+> CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)\
+程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
+
+CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
+
+系统内核会存储保持下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
+
+
+上面说到所谓的「**任务**」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:**进程上下文切换、线程上下文切换和中断上下文切换**。
+
+#### 进程上下文切换[Top]
+
+各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,**让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换**。
+
+进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
+所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
+
+进程是由内核管理和调度的,所以进程的切换只能发生在**内核态**。
+
+通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 `PCB` 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行。
+
+
+> 发生进程上下文切换有哪些场景?
+
+为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
+- 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
+- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
+- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
+- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;
+
+
+#### 线程上下文切换[Top]
+线程与进程最大的区别在于:**线程是调度的基本单位,而进程则是资源拥有的基本单位。** 所以,所谓操作系统的任务调度,**实际上的调度对象是线程**,而进程只是给线程提供了虚拟内存、全局变量等资源。
+
+- 当进程只有一个线程时,可以认为进程就等于线程;
+- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;
+
+> 线程上下文切换的是什么?
+
+这还得看线程是不是属于同一个进程:
+
+- 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
+- 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;
+
+
+### 处理器调度[Top]
- 高级调度:即作业调度,按照一定策略将选择磁盘上的程序装入内存,并建立进程。
- 中级调度:即交换调度,按照一定策略在内外存之间进行数据交换。
-- 低级调度:即CPU调度(进程调度),按照一定策略选择就绪进程,占用cpu执行。
+- 低级调度:即CPU调度(进程调度),按照一定策略选择就绪进程,占用cpu执行。
+
+#### 调度基本原则:[Top]
+
+> 原则一:如果运行的程序,发生了 I/O 事件的请求,那 CPU 使用率必然会很低,因为此时进程在阻塞等待硬盘的数据返回。这样的过程,势必会造成 CPU 突然的空闲。所以,为了提高 CPU 利用率,**在这种发送 I/O 事件致使 CPU 空闲的情况下,调度程序需要从就绪队列中选择一个进程来运行**。\
+> 原则二:有的程序执行某个任务花费的时间会比较长,如果这个程序一直占用着 CPU,会造成系统吞吐量(CPU 在单位时间内完成的进程数量)的降低。所以,要提高系统的吞吐率,**调度程序要权衡长任务和短任务进程的运行完成数量**。\
+> 原则三:从进程开始到结束的过程中,实际上是包含两个时间,分别是**进程运行时间和进程等待时间,这两个时间总和就称为周转时间**。进程的周转时间越小越好,**如果进程的等待时间很长而运行时间很短,那周转时间就很长,这不是我们所期望的,调度程序应该避免这种情况发生**。\
+> 原则四:处于就绪队列的进程,也不能等太久,当然希望这个等待的时间越短越好,这样可以使得进程更快的在 CPU 中执行。所以,**就绪队列中进程的等待时间也是调度程序所需要考虑的原则**。\
+> 原则五:对于鼠标、键盘这种交互式比较强的应用,我们当然希望它的响应时间越快越好,否则就会影响用户体验了。所以,**对于交互式比较强的应用,响应时间也是调度程序需要考虑的原则**。
-调度基本原则:
-1. CPU利用率
-2. 系统吞吐量:单位时间内CPU完成的作业数量。
-3. 响应时间:多个用户对系统进行操作,都要求在一定的时间内得到响应。
-4. 周转时间:作业从提交到完成的时间间隔
+1. CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
+2. 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
+3. 周转时间:周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好;
+4. 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
+5. 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。
+#### 调度算法[Top]
-调度算法:
-1. 先来先服务调度算法(first-come first-serverd,FCFS)
-2. 短作业优先调度算法(shortest job first,SJF) : 把处理器分配给最快完成的作业,会导致长作业饿死。
+1. 先来先服务调度算法(`first-come first-serverd,FCFS`)
+2. 短作业优先调度算法(`shortest job first,SJF`) : 把处理器分配给最快完成的作业,会导致长作业饿死。
3. 优先级调度算法:确定优先级进行调度,调度方式还可以分为抢占和非抢占的调度方式
4. 时间片轮转调度算法:一个进程在一个时间片未执行完毕,插入到队尾等待,循环直到处理完成
-5. 高响应比优先算法(highest response ratio first,HSRF):通过设置响应比公式:`响应比=(作业等待时间+估计运行时间)/估计运行时间`,解决长作业饿死问题
+5. 高响应比优先算法(`highest response ratio first,HSRF`):通过设置响应比公式:`响应比=(作业等待时间+估计运行时间)/估计运行时间`,解决长作业饿死问题
6. 多级队列调度算法:多个队列每个队列使用一种调度算法
7. 多级反馈队列调度算法:时间片轮转调度算法和优先级调度算法的综合,动态调整队列优先级和时间片大小。进程所在队列的优先级越高时间片越小。可兼顾多方面的系统目标,如为提高系统吞吐量和缩短平均响应周期而照顾端线程。
-> 适用于作业调度的算法:先来先服务调度算法、短作业优先调度算法、优先级调度算法、高响应比优先算法
-> 适用于进程调度的算法:先来先服务调度算法、短作业优先调度算法、优先级调度算法、时间片轮转调度算法、多级队列调度算法、多级反馈队列调度算法
+> **适用于作业调度的算法**:先来先服务调度算法、短作业优先调度算法、优先级调度算法、高响应比优先算法\
+> **适用于进程调度的算法**:先来先服务调度算法、短作业优先调度算法、优先级调度算法、时间片轮转调度算法、多级队列调度算法、多级反馈队列调度算法
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/multiple-queue-call.png)
-### 经典同步问题[Top]
+### 进程通信[Top]
+#### 管道通信[Top]
+管道通信: 所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。\
+- 管道传输数据是单向的,如果想相互通信,需要创建两个管道才行。
+- 管道这种通信方式效率低,不适合进程间频繁地交换数据
+
+**匿名管道**:对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。\
+匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。
+```shell
+$ ps auxf | grep mysql
+```
+**命名管道**:对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。
+```shell
+mkfifo myPipe
+
+$ echo "hello" > myPipe // 将数据写进管道 block
+
+
+$ cat < myPipe // 读取管道里的数据
+hello
+```
+
+#### 消息队列[Top]
+
+**消息队列**是保存在内核中的消息链表。\
+在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
+
+- 消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在。
+- 消息队列不适合比较大数据的传输。 在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。
+- 消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销
+
+#### 共享内存[Top]
+共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
+> 消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。
+
+#### 信号量[Top]
+
+用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
+
+**了防止多进程竞争共享资源**,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,**信号量就实现了这一保护机制**。
+
+信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
+
+信号量表示资源的数量,控制信号量的方式有两种原子操作:
+- P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
+- V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
+> P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。
+
+- **互斥信号量**,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存,`信号初始化为 1`。使得两个进程互斥访问共享内存。
+- **同步信号量**,它可以保证进程 A 应在进程 B 之前执行,`信号量初始化为0`。进程 A 是负责生产数据,而进程 B 是负责读取数据,这两个进程是相互合作、相互依赖的。
+
+#### 信号[Top]
+管道、消息队列、共享内存、信号量的进程间通信,都是常规状态下的工作模式。**对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。**
-#### 生产者-消费者问题[Top]
-生产者-消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
+> Ctrl+C 产生 SIGINT 信号,表示终止该进程;\
+> Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;\
+> kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号,用来立即结束该进程;
+
+信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,就有下面这几种,用户进程对信号的处理方式。
+1. 执行默认操作。Linux 对每种信号都规定了默认操作,例如,`SIGTERM` 信号,就是终止进程的意思。
+2. 捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
+3. 忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 `SIGKILL` 和 `SEGSTOP`,它们用于在任何时候中断或结束某一进程。
+
+#### Socket[Top]
+前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。
+
+实际上,Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。
+
+### 并发[Top]
+竞争条件(race condition)
+
+不确定性(indeterminate)
+
+临界区(critical section),它是访问共享资源的代码片段,一定不能给多线程同时执行。
+
+互斥(mutual exclusion)的,也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区
+
+同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。
+
+原子操作就是要么全部执行,要么都不执行,不能出现执行到一半的中间状态
+
+- 忙等待锁(自旋锁`spin lock`):当获取不到锁时,线程就会一直 while 循环,不做任何事情。
+- 无等待锁:顾明思议就是获取不到锁的时候,不用自旋。 既然不想自旋,那当没获取到锁的时候,就把当前线程放入到锁的等待队列,然后执行调度程序,把 CPU 让给其他线程执行。
+
+### 经典同步问题[Top]
+
+#### 生产者-消费者问题[Top]
+生产者-消费者问题(`Producer-consumer problem`),也称有限缓冲问题(`Bounded-buffer problem`),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
相关问题点:
- 在缓冲区为空时,消费者不能再进行消费
@@ -502,18 +843,573 @@ brk的分配方式问题在于,如果申请的空间没办法复用,那么
- 在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步
- 注意条件变量与互斥锁的顺序
-#### 读者-写者问题[Top]
+
+#### 哲学家就餐问题[Top]
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/concurrent-classic.png)
+
+
+哲学家就餐的问题描述: 5 个哲学家,围绕着一张圆桌吃面;这个桌子只有 5 支叉子,每两个哲学家之间放一支叉子; 哲学家围在一起先思考,思考中途饿了就会想进餐;\
+**这些哲学家要两支叉子才愿意吃面,也就是需要拿到左右两边的叉子才进餐; 吃完后,会把两支叉子放回原处,继续思考;**
+
+方案1:每个哲学家都先拿左边的叉子,再拿右边的叉子。使用信号量方式实现,充当每个叉子的信号量。
+> 缺陷:假设五位哲学家同时拿起左边的叉子,桌面上就没有叉子了, 这样就没有人能够拿到他们右边的叉子,发生死锁现象。
+
+方案2:只要有一个哲学家进入了「临界区」,也就是准备要拿叉子时,其他哲学家都不能动,只有这位哲学家用完叉子了,才能轮到下一个哲学家进餐。使用互斥信号量,只有一个哲学家获取这个互斥信号量才能去拿叉子。
+> 缺陷:每次进餐只能有一位哲学家,而桌面上是有 5 把叉子,按道理是能可以有两个哲学家同时进餐的,所以从效率角度上,这不是最好的解决方案。
+
+方案3:让偶数编号的哲学家「先拿左边的叉子后拿右边的叉子」,奇数编号的哲学家「先拿右边的叉子后拿左边的叉子」。
+
+方案4:用一个数组 state 来记录每一位哲学家的三个状态,分别是在进餐状态、思考状态、饥饿状态(正在试图拿叉子)。
+
+#### 读者-写者问题[Top]
+读者只会读取数据,不会修改数据,而写者即可以读也可以修改数据。
+
+读者-写者的问题描述:
+- 「读-读」允许:同一时刻,允许多个读者同时读
+- 「读-写」互斥:没有写者时读者才能读,没有读者时写者才能写
+- 「写-写」互斥:没有其他写者时,写者才能写
+
+### 死锁[Top]
+
+在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。
+那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
+
+死锁只有同时满足以下四个条件才会发生:
+- 互斥条件;
+- 持有并等待条件;
+- 不可剥夺条件;
+- 环路等待条件;
+
+1. **互斥条件**是指多个线程不能同时使用同一个资源。
+2. **持有并等待条件**是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
+3. **不可剥夺条件**是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
+4. **环路等待条件**指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
+
+
+**避免死锁**
+
+避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
+> 线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
+
+
+死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。
+
+死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。
+
+所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。
+
+### 锁[Top]
+
+#### 互斥锁[Top]
+**互斥锁**是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。
+
+**对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的**。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。
+
+互斥锁加锁失败时,会从**用户态陷入到内核态**,让内核帮我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本。 那这个开销成本是主要是两次线程上下文切换成本:
+- 当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行;
+- 当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。
+
+线程的上下文切换的是什么?**当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。**
+> 上下切换的耗时据统计,大概在几十纳秒到几微秒之间,如果你锁住的代码执行时间比较短,那可能上下文切换的时间都比你锁住的代码执行时间还要长。
+
+所以,**如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁**。
+
+#### 自旋锁[Top]
+自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。
+
+一般加锁的过程,包含两个步骤:
+- 第一步,查看锁的状态,如果锁是空闲的,则执行第二步;
+- 第二步,将锁设置为当前线程持有;
+CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。
+
+> 在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。\
+> 在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源
+
+#### 读写锁[Top]
+读写锁:由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。读写锁适用于能明确区分读操作和写操作的场景。
+
+读写锁适用于能明确区分读操作和写操作的场景。
+
+- 「读优先锁」:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取写锁。
+- 「写优先锁」:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取写锁。
+- 「公平读写锁」:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。
+
+#### 乐观锁与悲观锁[Top]
+
+悲观锁认为**多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁**。
+
+乐观锁认为假定冲突的概率很低,它的工作方式是:**先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作**。
+
+乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以**只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁**。
+
+
+## 文件系统[Top]
+
+### 文件系统组成[Top]
+Linux 文件系统会为每个文件分配两个数据结构:**索引节点(index node)**和**目录项(directory entry)**,它们主要用来记录文件的元信息和目录层次结构。
+- **索引节点**,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、**数据在磁盘的位置**等等。索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以**索引节点同样占用磁盘空间**。
+- **目录项**,用来记录文件的名字、**索引节点指针**以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,**目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存**。
+> 由于索引节点唯一标识一个文件,而目录项记录着文件的名,所以目录项和索引节点的关系是多对一,也就是说,一个文件可以有多个别字。比如,硬链接的实现就是多个目录项中的索引节点指向同一个文件。
+
+> **目录项和目录**是一个东西吗?\
+> **目录**是个文件,持久化存储在磁盘,而**目录项是内核一个数据结构**,缓存在内存。 如果查询目录频繁从磁盘读,效率会很低,所以内核会把已经读过的目录用目录项这个数据结构缓存在内存,下次再次读到相同的目录时,只需从内存读就可以,大大提高了文件系统的效率。
+
+- **扇区**: 磁盘读写的最小单位是扇区,扇区的大小只有`512B`大小。
+- **逻辑块(数据块)**: 文件系统把多个扇区组成了一个**逻辑块**,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为`4KB`,也就是一次性读写 `8` 个扇区,这将大大提高了磁盘的读写的效率。
+
+
+- **超级块**,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。
+- **索引节点区**,用来存储索引节点;
+- **数据块区**,用来存储文件或目录数据;
+
+> 超级块及索引节点区当需要使用的时候,才将其加载进内存,它们加载进内存的时机是不同超级块:当文件系统挂载时进入内存; 索引节点区:当文件被访问时进入内存;
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/filesystem-os.png)
+
+
+### 虚拟文件系统[Top]
+
+虚拟文件系统:文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为**虚拟文件系统(Virtual File System,VFS)**。
+> VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样使用方不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。
+
+
+**Linux 文件系统中,用户空间、系统调用、虚拟机文件系统、缓存、文件系统以及存储之间的关系**
+> 文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录。
+
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/filesystem-structure.png)
+
+### 文件使用[Top]
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/file-write.png)
+
+打开了一个文件后,操作系统会跟踪进程打开的所有文件,就是操作系统为每个进程维护一个打开文件表,文件表里的每一项代表「**文件描述符**」,所以说文件描述符是打开文件的标识。
+
+操作系统在打开文件表中维护着打开文件的状态和信息:
+- 文件指针:系统跟踪上次读写位置作为当前文件位置指针,这种指针对打开文件的某个进程来说是唯一的;
+- 文件打开计数器:文件关闭时,操作系统必须重用其打开文件表条目,否则表内空间不够用。因为多个进程可能打开同一个文件,所以系统在删除打开文件条目之前,必须等待最后一个进程关闭文件,该计数器跟踪打开和关闭的数量,当该计数为 0 时,系统关闭文件,删除该条目;
+- 文件磁盘位置:绝大多数文件操作都要求系统修改文件数据,该信息保存在内存中,以免每个操作都从磁盘中读取;
+- 访问权限:每个进程打开文件都需要有一个访问模式(创建、只读、读写、添加等),该信息保存在进程的打开文件表中,以便操作系统能允许或拒绝之后的 I/O 请求;
+
+> 用户和操作系统对文件的读写操作是有差异的,用户习惯以**字节**的方式读写文件,而操作系统则是以**数据块**来读写文件,那屏蔽掉这种差异的工作就是文件系统了。
+
+
+读文件和写文件的过程:
+- 当用户进程从文件读取 1 个字节大小的数据时,文件系统则需要获取字节所在的数据块,再返回数据块对应的用户进程所需的数据部分。
+- 当用户进程把 1 个字节大小的数据写进文件时,文件系统则找到需要写入数据的数据块的位置,然后修改数据块中对应的部分,最后再把数据块写回磁盘。
+
+**文件系统的基本操作单位是数据块**
+
+
+### 文件存储[Top]
+连续空间存放方式:文件存放在磁盘「连续的」物理空间中,文件的数据都是紧密相连,读写效率很高,因为一次磁盘寻道就可以读出整个文件。
+> 使用连续存放的方式有一个前提,必须先知道一个文件的大小,这样文件系统才会根据文件的大小在磁盘上找到一块连续的空间分配给文件。 所以,文件头里需要指定「起始块的位置」和「长度」\
+> 缺陷:「磁盘空间碎片」和「文件长度不易扩展」
+
+**非连续空间存放方式分为「链表方式」和「索引方式」**。
+
+**链表方式**
+
+链表的方式存放是离散的,不用连续的,于是就可以消除磁盘碎片,可大大提高磁盘空间的利用率,同时文件的长度可以动态扩展。根据实现的方式的不同,链表可分为「**隐式链表**」和「**显式链接**」两种形式。
+> 「**隐式链表**」的方式存放的话,实现的方式是文件头要包含「**第一块**」和「**最后一块**」的位置,并且每个数据块里面留出一个**指针空间**,用来存放下一个数据块的位置,这样一个数据块连着一个数据块,从链头开始就可以顺着指针找到所有的数据块,所以存放的方式可以是不连续的。
+> 缺点:缺点在于**无法直接访问数据块**,只能通过指针顺序访问文件,以及**数据块指针消耗了一定的存储空间**。隐式链接分配的稳定性较差,系统在运行过程中由于软件或者硬件错误**导致链表中的指针丢失或损坏,会导致文件数据的丢失**。
+
+>「**显式链接**」,它指把用于链接文件各数据块的指针,显式地存放在**内存的一张链接表**中,该表在整个磁盘仅设置一张,每个表项中存放链接指针,指向下一个数据块号。\
+> 查找记录的过程是在内存中进行的,因而不仅显著地提高了检索速度,而且大大减少了访问磁盘的次数。但也正是整个表都存放在内存中的关系,它的主要的缺点是不适用于大磁盘,因为大磁盘需要建立链接表占用内存空间过大。
+
+**索引方式**
+
+索引的实现是为每个文件创建一个「**索引数据块**」,里面存放的是**指向文件数据块的指针列表**。
+
+另外,文件头需要包含指向「索引数据块」的指针,这样就可以通过文件头知道索引数据块的位置,再通过索引数据块里的索引信息找到对应的数据块。
+
+索引的方式优点在于:
+- 文件的创建、增大、缩小很方便;
+- 不会有碎片的问题;
+- 支持顺序读写和随机读写;
+
+缺点:存储索引带来的开销。如果文件很小,但还是需要额外分配一块来存放索引数据
+
+
+> 「链式索引块」,它的实现方式是在索引数据块留出一个存放下一个索引数据块的指针,于是当一个索引数据块的索引信息用完了,就可以通过指针的方式,找到下一个索引数据块的信息。\
+> 「多级索引块」,实现方式是通过一个索引块来存放多个索引数据块,一层套一层索引。
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/file-save-compare.png)
+
+
+
+### 文件系统结构[Top]
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/file-piece.png)
+
+引导块,在系统启动时用于启用引导,接着后面就是一个一个连续的块组了,块组的内容如下:
+- 超级块,包含的是文件系统的重要信息,比如 inode 总个数、块总个数、每个块组的 inode 个数、每个块组的块个数等等。
+- 块组描述符,包含文件系统中各个块组的状态,比如块组中空闲块和 inode 的数目等,每个块组都包含了文件系统中「所有块组的组描述符信息」。
+- 数据位图和 inode 位图, 用于表示对应的数据块或 inode 是空闲的,还是被使用中。
+- inode 列表,包含了块组中所有的 inode,inode 用于保存文件系统中与各个文件和目录相关的所有元数据。
+- 数据块,包含文件的有用数据。
+
+
+### 软链接和硬链接[Top]
+
+给某个文件取个别名,那么在 Linux 中可以通过硬链接(Hard Link) 和软链接(Symbolic Link)的方式来实现
+
+硬链接是多个目录项中的「索引节点」指向一个文件
+> 硬链接是不可用于跨文件系统的。由于多个目录项都是指向一个 inode,那么只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
+
+软链接相当于重新创建一个文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的路径,所以访问软链接的时候,实际上相当于访问到了另外一个文件,
+> 软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。
+
+
+### 文件/网络IO[Top]
+- 缓冲与非缓冲 I/O
+- 直接与非直接 I/O
+- 阻塞与非阻塞 I/O VS 同步与异步 I/O
+
+
+#### 缓冲与非缓冲 I/O[Top]
+文件操作的标准库是可以实现数据的缓存,那么根据「是否利用标准库缓冲」,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O。
+
+- 缓冲 I/O,利用的是**标准库的缓存实现文件的加速访问**,而标准库再通过系统调用访问文件。
+- 非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。
+> 很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来,这样做的目的是,减少系统调用的次数,毕竟**系统调用是有 CPU 上下文切换**的开销的。
+
+#### 直接与非直接 I/O[Top]
+Linux 内核为了减少磁盘 I/O 次数,在系统调用后,会把用户数据拷贝到内核中缓存起来,这个内核缓存空间也就是「**页缓存**」,只有当缓存满足某些条件的时候,才发起磁盘 I/O 的请求。
+
+根据是「否利用操作系统的缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O:
+- 直接 I/O,不会发生**内核缓存和用户程序**之间数据复制,而是直接经过文件系统访问磁盘。
+- 非直接 I/O,读操作时,数据从**内核缓存中拷贝给用户程序**,写操作时,数据从用户程序拷贝给内核缓存,再由**内核决定什么时候写入数据**到磁盘。
+> 如果你在使用文件操作类的系统调用函数时,指定了 O_DIRECT 标志,则表示使用直接 I/O。如果没有设置过,默认使用的是非直接 I/O。
+
+> 如果用了非直接 I/O 进行写数据操作,内核什么情况下才会把缓存数据写入到磁盘?
+
+以下几种场景会触发内核缓存的数据写入磁盘:
+- 在调用 write 的最后,当发现内核缓存的数据太多的时候,内核会把数据写到磁盘上;
+- 用户主动调用 sync,内核缓存会刷到磁盘上;
+- 当内存十分紧张,无法再分配页面时,也会把内核缓存的数据刷到磁盘上;
+- 内核缓存的数据的缓存时间超过某个时间时,也会把数据刷到磁盘上;
+
+
+#### 阻塞与非阻塞 I/O VS 同步与异步 I/O[Top]
+I/O 是分为两个过程的:
+1. 数据准备的过程
+2. 数据从内核空间拷贝到用户进程缓冲区的过程
+
+
+阻塞 I/O: 当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。 \
+阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程。
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/BIO.png)
+
+非阻塞 I/O,非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。
+> **最后一次 read 调用**,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是**内核态的数据拷贝到用户程序的缓存区**这个过程。
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/NIO.png)
+
+为了解决NIO这种轮询方式改善了应用进程对 CPU 的利用率,于是 I/O 多路复用技术就出来了,如 select、poll,它是通过 I/O 事件分发,当内核数据准备好时,再以事件通知应用程序进行操作。
+
+使用 select I/O 多路复用过程。注意,read 获取数据的过程(数据从内核态拷贝到用户态的过程),也是一个同步的过程,需要等待
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/selectIO.png)
+
+
+阻塞I/O、非阻塞I/O,还是基于非阻塞I/O的多路复用都是**同步调用**。**因为它们在 read 调用时,内核将数据从内核空间拷贝到应用程序空间,过程都是需要等待的**,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间。
+
+异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。
+
+当我们发起 aio_read 之后,就立即返回,内核自动将数据从内核空间拷贝到应用程序空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/AIO.png)
+
+
+> 用故事去理解这几种 I/O 模型
+> 举个你去饭堂吃饭的例子,你好比用户程序,饭堂好比操作系统。\
+> 阻塞 I/O 好比,你去饭堂吃饭,但是饭堂的菜还没做好,然后你就一直在那里等啊等,等了好长一段时间终于等到饭堂阿姨把菜端了出来(数据准备的过程),但是你还得继续等阿姨把菜(内核空间)打到你的饭盒里(用户空间),经历完这两个过程,你才可以离开。\
+> 非阻塞 I/O 好比,你去了饭堂,问阿姨菜做好了没有,阿姨告诉你没,你就离开了,过几十分钟,你又来饭堂问阿姨,阿姨说做好了,于是阿姨帮你把菜打到你的饭盒里,这个过程你是得等待的。\
+> 基于非阻塞的 I/O 多路复用好比,你去饭堂吃饭,发现有一排窗口,饭堂阿姨告诉你这些窗口都还没做好菜,等做好了再通知你,于是等啊等(select 调用中),过了一会阿姨通知你菜做好了,但是不知道哪个窗口的菜做好了,你自己看吧。于是你只能一个一个窗口去确认,后面发现 5 号窗口菜做好了,于是你让 5 号窗口的阿姨帮你打菜到饭盒里,这个打菜的过程你是要等待的,虽然时间不长。打完菜后,你自然就可以离开了。\
+> 异步 I/O 好比,你让饭堂阿姨将菜做好并把菜打到饭盒里后,把饭盒送到你面前,整个过程你都不需要任何等待。
+
+
+#### IO多路复用[Top]
+
+一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程,这就是多路复用,这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用。
+
+`select/poll/epoll` 是内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件。
+
+`select/poll/epoll` 在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可。
+
+
+
+
+##### select/poll[Top]
+
+select 实现多路复用的方式是,将已连接的 Socket 都放到一个**文件描述符集合**,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生。\
+检查方式:通过**遍历**文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把**整个文件描述符集合拷贝**回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
+> 对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从**用户空间传入内核空间**,由内核修改后,再传出到用户空间中。
+
+select 使用固定长度的`BitsMap`,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 `FD_SETSIZE` 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
+
+
+poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
+
+`poll` 和 `select` 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在**用户态与内核态之间拷贝文件描述符集合**,这种方式随着并发数上来,性能的损耗会呈指数级增长。
+
+##### epoll[Top]
+epoll 通过两个方面,很好解决了 select/poll 的问题。epoll 是解决 C10K 问题的利器。
+
+1. 第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,增删改一般时间复杂度是 `O(logn)`。
+2. 第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件。给每个fd注册一个回调函数,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中。以此达到O(1)的时间复杂度
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/epoll.png)
+
+
+**边缘触发和水平触发**
+
+
+- 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
+- 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
+> 水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
+
+简单说——水平触发代表了一种“**状态**”。边沿触发代表了一个“**事件**”。
+
+
+`select/poll` 只有水平触发模式,`epoll` 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
+一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 `epoll_wait` 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
+
+边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。
+**多路复用 API 返回的事件并不一定可读写的**,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。
+
+
+#### Reactor[Top]
+
+**单Reactor单进程/线程**
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/singleReactor.png)
+
+进程里有 Reactor、Acceptor、Handler 这三个对象:
+- Reactor 对象的作用是监听和分发事件;
+- Acceptor 对象的作用是获取连接;
+- Handler 对象的作用是处理业务;
+
+介绍下「单 Reactor 单进程」这个方案:
+- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
+- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
+- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
+- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
+
+2 个缺点:
+- 第一个缺点,因为只有一个进程,无法充分利用 **多核 CPU 的性能**;
+- 第二个缺点,Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,**如果业务处理耗时比较长,那么就造成响应的延迟**;
+> 单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
+
+**单 Reactor 多线程 / 多进程**
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/singleReactorMulti.png)
+
+前三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤:
+- Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;
+- 子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
+
+单 Reactor 多线程的方案优势在于能够充分利用**多核 CPU 的性能**,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。
+
+「单 Reactor」的模式还有个问题,因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
+
+
+**多 Reactor 多进程 / 线程**
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/multiReactor.png)
+
+方案详细说明如下:
+- 主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
+- 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
+- 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
+- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
+
+两个开源软件 Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案。
+
+
+#### Proactor[Top]
+- **Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件**。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
+- **Proactor 是异步网络模式, 感知的是已完成的读写事件**。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
+> Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」
+
+无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「**待完成**」的 I/O 事件,而 Proactor 模式则是基于「**已完成**」的 I/O 事件。
+
+
+### page cache[Top]
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/linux-filesystem-cache.png)
+
+红色部分为 Page Cache。 Page Cache 的本质是由 Linux 内核管理的内存区域。我们通过 mmap 以及 buffered I/O 将文件读取到内存空间实际上都是读取到 Page Cache 中。
+
+
+#### page与page cache[Top]
+page 是内存管理分配的基本单位, Page Cache 由多个 page 构成。page 在操作系统中通常为 4KB 大小(32bits/64bits),而 Page Cache 的大小则为 4KB 的整数倍。
+
+Linux 系统上供用户可访问的内存分为两个类型,即:
+- `File-backed pages`:文件备份页也就是 Page Cache 中的 page,对应于磁盘上的若干数据块;对于这些页最大的问题是脏页回盘;
+- `Anonymous pages`:匿名页不对应磁盘上的任何磁盘数据块,它们是进程的运行是内存空间(例如方法栈、局部变量表等属性);
+
+> 内存是一种珍惜资源,当内存不够用时,内存管理单元(Memory Management Unit)需要提供调度算法来回收相关内存空间。内存空间回收的方式通常就是 swap,即交换到持久化存储设备上。
+> - File-backed pages(Page Cache)的内存回收代价较低。Page Cache 通常对应于一个文件上的若干顺序块,因此可以通过顺序 I/O 的方式落盘。另一方面,如果 Page Cache 上没有进行写操作(所谓的没有脏页),甚至不会将 Page Cache 回盘,因为数据的内容完全可以通过再次读取磁盘文件得到。
+> - Anonymous pages 的内存回收代价较高。这是因为 Anonymous pages 通常随机地写入持久化交换设备。另一方面,无论是否有写操作,为了确保数据不丢失,Anonymous pages 在 swap 时必须持久化到磁盘。
+
+#### Page Cache 与 buffer cache[Top]
+
+```
+~ free -m
+ total used free shared buffers cached
+Mem: 128956 96440 32515 0 5368 39900
+-/+ buffers/cache: 51172 77784
+Swap: 16002 0 16001
+```
+cached 列表示当前的页缓存(Page Cache)占用量,buffers 列表示当前的块缓存(buffer cache)占用量。
+
+**Page Cache 用于缓存文件的页数据,buffer cache 用于缓存块设备(如磁盘)的块数据**。页是逻辑上的概念,因此 Page Cache 是与文件系统同级的;块是物理上的概念,因此 buffer cache 是与块设备驱动程序同级的。
+
+> Page Cache 与 buffer cache 的共同目的都是加速数据 I/O:写数据时首先写到缓存,将写入的页标记为 dirty,然后向外部存储 flush,也就是缓存写机制中的 write-back(另一种是 write-through,Linux 默认情况下不采用);读数据时首先读取缓存,如果未命中,再去外部存储读取,并且将读取来的数据也加入缓存。操作系统总是积极地将所有空闲内存都用作 Page Cache 和 buffer cache,当内存不够用时也会用 LRU 等算法淘汰缓存页。
+
+Page Cache 中的每个文件都是一棵基数树(radix tree,本质上是多叉搜索树),树的每个节点都是一个页。根据文件内的偏移量就可以快速定位到所在的页
+> [基数树](https://en.wikipedia.org/wiki/Radix_tree)
+
+#### Page Cache 与预读[Top]
+操作系统为基于 Page Cache 的读缓存机制提供**预读机制**(`PAGE_READAHEAD`),一个例子是:
+- 用户线程仅仅请求读取磁盘上文件 A 的 offset 为 0-3KB 范围内的数据,由于磁盘的基本读写单位为 block(4KB),于是操作系统至少会读 0-4KB 的内容,这恰好可以在一个 page 中装下。
+- 但是操作系统出于局部性原理会选择将磁盘块 offset [4KB,8KB)、[8KB,12KB) 以及 [12KB,16KB) 都加载到内存,于是额外在内存中申请了 3 个 page;
+
+
+#### 持久化的一致性&可靠性[Top]
+任何系统引入缓存,就会引发一致性问题。`文件 = 数据 + 元数据`。元数据用来描述文件的各种属性,也必须存储在磁盘上。因此,保证文件一致性其实包含了两个方面:**数据一致+元数据一致**。
+
+当前 Linux 下以两种方式实现文件一致性:
+- **Write Through(写穿)**:向用户层提供特定接口,应用程序可主动调用接口来保证文件一致性;
+- **Write back(写回)**:系统中存在定期任务(表现形式为内核线程),周期性地同步文件系统中文件脏数据块,这是默认的 Linux 一致性方案;
+
+系统中存在多个回写时机,第一是应用程序主动调用回写接口(fsync,fdatasync 以及 sync 等),第二管理线程周期性地唤醒设备上的回写线程进行回写,第三是某些应用程序/内核任务发现内存不足时要回收部分缓存页面而事先进行脏页面回写,设计一个统一的框架来管理这些回写任务非常有必要。
+
+Write Through 与 Write back 在持久化的可靠性上有所不同:
+- Write Through 以牺牲系统 I/O 吞吐量作为代价,向上层应用确保一旦写入,数据就已经落盘,不会丢失;
+- Write back 在系统发生宕机的情况下无法确保数据已经落盘,因此存在数据丢失的问题。不过,在程序挂了,例如被 kill -9,Page Cache 中的数据操作系统还是会确保落盘;
+
+
+#### page cache 优劣势[Top]
+Page Cache 的优势
+1. 加快数据访问。如果数据能够在内存中进行缓存,那么下一次访问就不需要通过磁盘 I/O 了,直接命中内存缓存即可。
+2. 减少 I/O 次数,提高系统磁盘 I/O 吞吐量。得益于 Page Cache 的缓存以及预读能力,而程序又往往符合局部性原理,因此通过一次 I/O 将多个 page 装入 Page Cache 能够减少磁盘 I/O 次数, 进而提高系统磁盘 I/O 吞吐量。
+
+Page Cache 的劣势
+1. 需要占用额外物理内存空间,物理内存在比较紧俏的时候可能会导致频繁的 swap 操作,最终导致系统的磁盘 I/O 负载的上升。
+2. 在传输大文件(GB 级别的文件)的时候,PageCache 会不起作用。其他「热点」的小文件可能就无法充分使用到 PageCache,于是这样磁盘读写的性能就会下降了;
+3. 对应用层并没有提供很好的管理 API,几乎是透明管理。应用层即使想优化 Page Cache 的使用策略也很难进行。因此一些应用选择在用户空间实现自己的 page 管理,而不使用 page cache,例如 MySQL InnoDB 存储引擎以 16KB 的页进行管理。
+4. Page Cache 最后一个缺陷是在某些应用场景下比 Direct I/O 多一次磁盘读 I/O 以及磁盘写 I/O。
+> - 缓存文件 I/O:用户空间要读写一个文件并不直接与磁盘交互,而是中间夹了一层缓存,即 page cache;
+> - 直接文件 I/O:用户空间读取的文件直接与磁盘交互,没有中间 page cache 层;
+
+Direct I/O :
+- Write 操作:由于其不使用 page cache,所以其进行写文件,如果返回成功,数据就真的落盘了(不考虑磁盘自带的缓存);
+- Read 操作:由于其不使用 page cache,每次读操作是真的从磁盘中读取,不会从文件系统的缓存中读取。
+
+
+#### 参考资料[Top]
+[Linux 的 Page Cache](https://spongecaptain.cool/SimpleClearFileIO/1.%20page%20cache.html)
+
+
+
+### 直接内存访问[Top]
+在没有 DMA (Direct Memory Access)技术前,I/O 的过程是这样的:
+- CPU 发出对应的指令给磁盘控制器,然后返回;
+- 磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断;
+- CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/design/systemdesign/DMA-before.png)
+
+DMA直接内存访问(Direct Memory Access): **在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。**
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/design/systemdesign/DMA-working.png)
+
+具体过程:
+- 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态;
+- 操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务;
+- DMA 进一步将 I/O 请求发送给磁盘;
+- 磁盘收到 DMA 的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA 发起中断信号,告知自己缓冲区已满;
+- DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务;
+- 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU;
+- CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回;
+> 整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。
+
+
+### 零拷贝[Top]
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/design/systemdesign/file-write-read-origin.png)
+
+```
+read(file, tmp_buf, len);
+write(socket, tmp_buf, len);
+```
+期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
+
+发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的:
+- 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
+- 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
+- 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
+- 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。
+
+> 上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。**要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。**
+
+优化的思路:
+1. 要想减少上下文切换的次数,就要减少系统调用的次数。
+2. 因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的。
+
+
+零拷贝技术实现的方式通常有 2 种:
+- `mmap + write`
+- `sendfile`
+
+#### mmap + write[Top]
+```
+buf = mmap(file, len);
+write(sockfd, buf, len);
+```
+通过使用 `mmap()` 来代替 `read()`, 可以减少一次数据拷贝的过程。
+
+`mmap()` 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/design/systemdesign/mmp.png)
+
+具体过程如下:
+- 应用进程调用了 `mmap()` 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;
+- 应用进程再调用 `write()`,操作系统直接将内核缓冲区的数据拷贝到 `socket` 缓冲区中,这一切都发生在内核态,由 `CPU` 来搬运数据;
+- 最后,把内核的 `socket` 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 `DMA` 搬运的。
+
+> 但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。
+
+#### sendfile[Top]
+
+`sendfile()`系统调用不需要将数据拷贝或者映射到应用程序地址空间中去,所以 `sendfile()` **只是适用于应用程序地址空间不需要对所访问数据进行处理的情况**
+
+`sendfile()`系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/design/systemdesign/send-file-v1.png)
+
+
+网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。
+
+![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/design/systemdesign/send-file-v2.png)
+
+零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
+
+零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
+所以,总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上
+零拷贝技术的应用: Kafka。使用了零拷贝能够缩短 65% 的时间,大幅度提升了机器传输数据的吞吐量。
-# 计算机组成原理[Top]
+# 计算机组成原理[Top]
-## 局部性原理[Top]
+## 局部性原理[Top]
// TODO
[计算机组成原理:局部性原理](https://blog.csdn.net/zhizhengguan/article/details/121172704)
-# 中间件设计资料[Top]
+# 中间件设计资料[Top]
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/design/systemdesign/disk-memory.png)
通常在大部分组件设计时,往往会选择一种主要介质来存储、另一种介质作为辅助使用。就拿 redis 来说,它主要采用内存存储数据,磁盘用来做辅助的持久化。拿 RabbitMQ 举例,它也是主要采用内存存储消息,但也支持将消息持久化到磁盘。而 RocketMQ、Kafka、Pulsar 这种,则是数据主要存储在磁盘,通过内存来主力提升系统的性能。关系型数据库例如 mysql 这种组件也是主要采用磁盘组织数据,合理利用内存提升性能。
diff --git a/src/main/java/com/toc/SHARDING_JDBC.md b/src/main/java/com/toc/SHARDING_JDBC.md
index 456b39f..d8d405c 100644
--- a/src/main/java/com/toc/SHARDING_JDBC.md
+++ b/src/main/java/com/toc/SHARDING_JDBC.md
@@ -1,71 +1,102 @@
**Index**
-sharding-jdbc
- 1. 前言
- 1.1. 写在前面的约定内容
- 1.2. sharding-jdbc不支持情况
- 2. Spring参数配置
- 2.1. sharding-jdbc官方参数说明
- 2.1.1. 数据分片
-分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
-用于单分片键的标准分片场景
-用于多分片键的复合分片场景
-行表达式分片策略
-Hint 分片策略
-分表策略,同分库策略
-org.apache.shardingsphere.core.constant.properties.ShardingPropertiesConstant
-org.apache.shardingsphere.core.BaseShardingEngine.shard
- 0.1.2. 读写分离
-省略数据源配置,与数据分片一致
- 0.1.3. 数据加密
-省略数据源配置,与数据分片一致
- 0.1.4. 治理
-省略数据源、数据分片、读写分离和数据脱敏配置
- 0.2. sharding-jdbc官方配置实践
- 0.2.1. 公共配置
-数据源名称(必选)
-多个逗号隔开,改配置名称和下面数据源配置对应
-默认数据源(有单表的情况时,必须配置;若所有表都是分库分表的情景,则可以不配置)
-数据源配置(必选)
-此处ds-1,ds-2与上面数据源名称对应
- 0.2.2. 分库分表配置
- 0.2.2.1. 单库单表配置
-实际逻辑表(必选)
-分表策略(必选):四种策略只能选一种
-主键生成策略,支持:UUID和SNOWFLAKE
-主键生产策略的属性配置:UUID时不需要配置
-使用 SNOWFLAKE 算法,需要配置 worker.id 与 max.tolerate.time.difference.milliseconds 属性。
-若使用此算法生成值作分片值,建议配置 max.vibration.offset 属性
-spring.shardingsphere.sharding.tables.course.key-generator.props.= 1
- 0.2.3. 多库多表配置
------ 实际逻辑表1 user_course_xx
-分库策略
-分表策略
-主键生成策略,支持:UUID和SNOWFLAKE
------ 实际逻辑表2 user_standard_xx
-分库策略
-分表策略
- 0.2.4. 绑定表配置
- 0.2.5. 广播表配置
- 0.2.6. 读写分离配置
-省略数据源配置,与数据分片一致
- 0.2.7. 数据加密
-省略数据源配置,与数据分片一致
- 1. 分库分表策略及思路
- 1.1. 分库策略
- 1.2. 分表策略
- 1.3. 平滑迁移分表解决方案
- 1.4. 一种混合策略思路
-必须搭配sharding-jdbc的混合策略配置
- 0.5. 多租户配置
- 1. 容量预估
-# sharding-jdbc[Top]
+Table of Contents
+sharding-jdbc
+ 1. 前言
+ 1.1. 写在前面的约定内容
+ 1.2. sharding-jdbc不支持情况
+ 2. Spring参数配置
+ 2.1. sharding-jdbc官方参数说明
+ 2.1.1. 数据分片
+分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
+用于单分片键的标准分片场景
+用于多分片键的复合分片场景
+行表达式分片策略
+Hint 分片策略
+分表策略,同分库策略
+org.apache.shardingsphere.core.constant.properties.ShardingPropertiesConstant
+org.apache.shardingsphere.core.BaseShardingEngine.shard
+ 0.1.2. 读写分离
+省略数据源配置,与数据分片一致
+ 0.1.3. 数据加密
+省略数据源配置,与数据分片一致
+ 0.1.4. 治理
+省略数据源、数据分片、读写分离和数据脱敏配置
+ 0.2. sharding-jdbc官方配置实践
+ 0.2.1. 公共配置
+数据源名称(必选)
+多个逗号隔开,改配置名称和下面数据源配置对应
+默认数据源(有单表的情况时,必须配置;若所有表都是分库分表的情景,则可以不配置)
+数据源配置(必选)
+此处ds-1,ds-2与上面数据源名称对应
+ 0.2.2. 分库分表配置
+ 0.2.2.1. 单库单表配置
+实际逻辑表(必选)
+分表策略(必选):四种策略只能选一种
+主键生成策略,支持:UUID和SNOWFLAKE
+主键生产策略的属性配置:UUID时不需要配置
+使用 SNOWFLAKE 算法,需要配置 worker.id 与 max.tolerate.time.difference.milliseconds 属性。
+若使用此算法生成值作分片值,建议配置 max.vibration.offset 属性
+spring.shardingsphere.sharding.tables.course.key-generator.props.= 1
+ 0.2.3. 多库多表配置
+----- 实际逻辑表1 user_course_xx
+分库策略
+分表策略
+主键生成策略,支持:UUID和SNOWFLAKE
+----- 实际逻辑表2 user_standard_xx
+分库策略
+分表策略
+ 0.2.4. 绑定表配置
+ 0.2.5. 广播表配置
+ 0.2.6. 读写分离配置
+省略数据源配置,与数据分片一致
+ 0.2.7. 数据加密
+省略数据源配置,与数据分片一致
+ 1. 分库分表策略及思路
+ 1.1. 分库策略
+ 1.2. 分表策略
+ 1.3. 平滑迁移分表解决方案
+ 1.4. 一种混合策略思路
+必须搭配sharding-jdbc的混合策略配置
+ 0.5. 多租户配置
+ 1. 容量预估
+# Table of Contents[Top]
+
+* [sharding-jdbc](#sharding-jdbc)
+ * [前言](#前言)
+ * [写在前面的约定内容](#写在前面的约定内容)
+ * [sharding-jdbc不支持情况](#sharding-jdbc不支持情况)
+ * [Spring参数配置](#spring参数配置)
+ * [sharding-jdbc官方参数说明](#sharding-jdbc官方参数说明)
+ * [数据分片](#数据分片)
+ * [读写分离](#读写分离)
+ * [数据加密](#数据加密)
+ * [治理](#治理)
+ * [sharding-jdbc官方配置实践](#sharding-jdbc官方配置实践)
+ * [公共配置](#公共配置)
+ * [分库分表配置](#分库分表配置)
+ * [单库单表配置](#单库单表配置)
+ * [多库多表配置](#多库多表配置)
+ * [绑定表配置](#绑定表配置)
+ * [广播表配置](#广播表配置)
+ * [读写分离配置](#读写分离配置)
+ * [数据加密](#数据加密-1)
+ * [分库分表策略及思路](#分库分表策略及思路)
+ * [分库策略](#分库策略)
+ * [分表策略](#分表策略)
+ * [平滑迁移分表解决方案](#平滑迁移分表解决方案)
+ * [一种混合策略思路](#一种混合策略思路)
+ * [多租户配置](#多租户配置)
+ * [容量预估](#容量预估)
+
+
+# sharding-jdbc[Top]
sharding-jdbc分库分表配置说明:
- [官方文档](https://shardingsphere.apache.org/document/current/cn/overview/)
- [官方各版本配置变更历史](https://shardingsphere.apache.org/document/current/cn/reference/api-change-history/shardingsphere-jdbc/spring-boot-starter/)
-## 前言[Top]
-### 写在前面的约定内容[Top]
+## 前言[Top]
+### 写在前面的约定内容[Top]
Spring官方推荐约定大于配置
@@ -76,7 +107,7 @@ Spring官方推荐约定大于配置
* 表名称必须全小写。 比如 account_1,不能写成 ACCOUNT_1
* 以前的单表扩容为分库分表形式,原表名称无需变更表名称带上编号
-### sharding-jdbc不支持情况[Top]
+### sharding-jdbc不支持情况[Top]
* [JDBC不支持选项](https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/unsupported-items/)
* [SQL不支持选项](https://shardingsphere.apache.org/document/current/cn/features/sharding/use-norms/sql/)
@@ -86,7 +117,7 @@ Spring官方推荐约定大于配置
![sharding.jpg](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/sharding.jpg)
-## Spring参数配置[Top]
+## Spring参数配置[Top]
允许一个实体类对应多个表:该参数和Springboot版本有关,非必配
@@ -94,10 +125,10 @@ Spring官方推荐约定大于配置
spring.main.allow-bean-definition-overriding=true
```
-### sharding-jdbc官方参数说明[Top]
+### sharding-jdbc官方参数说明[Top]
[ShardingSphere-4.x](https://shardingsphere.apache.org/document/current/cn/reference/api-change-history/shardingsphere-jdbc/spring-boot-starter/#shardingsphere-4x)
-#### 数据分片[Top]
+#### 数据分片[Top]
```properties
spring.shardingsphere.datasource.names= # 数据源名称,多数据源以逗号分隔
@@ -110,25 +141,25 @@ spring.shardingsphere.datasource..xxx= # 数据库连接池的
spring.shardingsphere.sharding.tables..actual-data-nodes= # 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
-# 分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一[Top]
+# 分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一[Top]
-# 用于单分片键的标准分片场景[Top]
+# 用于单分片键的标准分片场景[Top]
spring.shardingsphere.sharding.tables..database-strategy.standard.sharding-column= # 分片列名称
spring.shardingsphere.sharding.tables..database-strategy.standard.precise-algorithm-class-name= # 精确分片算法类名称,用于 = 和 IN。该类需实现 PreciseShardingAlgorithm 接口并提供无参数的构造器
spring.shardingsphere.sharding.tables..database-strategy.standard.range-algorithm-class-name= # 范围分片算法类名称,用于 BETWEEN,可选。该类需实现 RangeShardingAlgorithm 接口并提供无参数的构造器
-# 用于多分片键的复合分片场景[Top]
+# 用于多分片键的复合分片场景[Top]
spring.shardingsphere.sharding.tables..database-strategy.complex.sharding-columns= # 分片列名称,多个列以逗号分隔
spring.shardingsphere.sharding.tables..database-strategy.complex.algorithm-class-name= # 复合分片算法类名称。该类需实现 ComplexKeysShardingAlgorithm 接口并提供无参数的构造器
-# 行表达式分片策略[Top]
+# 行表达式分片策略[Top]
spring.shardingsphere.sharding.tables..database-strategy.inline.sharding-column= # 分片列名称
spring.shardingsphere.sharding.tables..database-strategy.inline.algorithm-expression= # 分片算法行表达式,需符合 groovy 语法
-# Hint 分片策略[Top]
+# Hint 分片策略[Top]
spring.shardingsphere.sharding.tables..database-strategy.hint.algorithm-class-name= # Hint 分片算法类名称。该类需实现 HintShardingAlgorithm 接口并提供无参数的构造器
-# 分表策略,同分库策略[Top]
+# 分表策略,同分库策略[Top]
spring.shardingsphere.sharding.tables..table-strategy.xxx= # 省略
spring.shardingsphere.sharding.tables..key-generator.column= # 自增列名称,缺省表示不使用自增主键生成器
@@ -156,16 +187,16 @@ spring.shardingsphere.sharding.master-slave-rules..load-balance-algorithm-class-name= # 详见读写分离部分
spring.shardingsphere.sharding.master-slave-rules..load-balance-algorithm-type= # 详见读写分离部分
-# org.apache.shardingsphere.core.constant.properties.ShardingPropertiesConstant[Top]
-# org.apache.shardingsphere.core.BaseShardingEngine.shard[Top]
+# org.apache.shardingsphere.core.constant.properties.ShardingPropertiesConstant[Top]
+# org.apache.shardingsphere.core.BaseShardingEngine.shard[Top]
spring.shardingsphere.props.sql.show= # 是否开启 SQL 显示,默认值: false
spring.shardingsphere.props.executor.size= # 工作线程数量,默认值: CPU 核数
```
-#### 读写分离[Top]
+#### 读写分离[Top]
```properties
-# 省略数据源配置,与数据分片一致[Top]
+# 省略数据源配置,与数据分片一致[Top]
spring.shardingsphere.sharding.master-slave-rules..master-data-source-name= # 主库数据源名称
spring.shardingsphere.sharding.master-slave-rules..slave-data-source-names[0]= # 从库数据源名称列表
@@ -179,10 +210,10 @@ spring.shardingsphere.props.executor.size= # 工作线程数量,默认值: CPU
spring.shardingsphere.props.check.table.metadata.enabled= # 是否在启动时检查分表元数据一致性,默认值: false
```
-#### 数据加密[Top]
+#### 数据加密[Top]
```properties
-# 省略数据源配置,与数据分片一致[Top]
+# 省略数据源配置,与数据分片一致[Top]
spring.shardingsphere.encrypt.encryptors..type= # 加解密器类型,可自定义或选择内置类型:MD5/AES
spring.shardingsphere.encrypt.encryptors..props.= # 属性配置, 注意:使用 AES 加密器,需要配置 AES 加密器的 KEY 属性:aes.key.value
@@ -192,10 +223,10 @@ spring.shardingsphere.encrypt.tables..columns..as
spring.shardingsphere.encrypt.tables..columns..encryptor= # 加密器名字
```
-#### 治理[Top]
+#### 治理[Top]
```properties
-# 省略数据源、数据分片、读写分离和数据脱敏配置[Top]
+# 省略数据源、数据分片、读写分离和数据脱敏配置[Top]
spring.shardingsphere.orchestration.name= # 治理实例名称
spring.shardingsphere.orchestration.overwrite= # 本地配置是否覆盖注册中心配置。如果可覆盖,每次启动都以本地配置为准
@@ -210,26 +241,26 @@ spring.shardingsphere.orchestration.registry.time-to-live-seconds= # 临时节
spring.shardingsphere.orchestration.registry.props= # 配置中心其它属性
```
-### sharding-jdbc官方配置实践[Top]
+### sharding-jdbc官方配置实践[Top]
```properties
druid.enabled=false
```
-#### 公共配置[Top]
+#### 公共配置[Top]
数据源配置
```properties
-# 数据源名称(必选)[Top]
-# 多个逗号隔开,改配置名称和下面数据源配置对应[Top]
+# 数据源名称(必选)[Top]
+# 多个逗号隔开,改配置名称和下面数据源配置对应[Top]
spring.shardingsphere.datasource.names=ds-1,ds-2
-# 默认数据源(有单表的情况时,必须配置;若所有表都是分库分表的情景,则可以不配置)[Top]
+# 默认数据源(有单表的情况时,必须配置;若所有表都是分库分表的情景,则可以不配置)[Top]
spring.shardingsphere.sharding.default-data-source-name=ds-1
-# 数据源配置(必选)[Top]
-# 此处ds-1,ds-2与上面数据源名称对应[Top]
+# 数据源配置(必选)[Top]
+# 此处ds-1,ds-2与上面数据源名称对应[Top]
spring.shardingsphere.datasource.ds-1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds-1.url=jdbc:mysql://xxxx:10032/sanwu_sharding-1?serverTimezone=GMT%2B8
@@ -259,7 +290,7 @@ spring.shardingsphere.props.sql.show=true # 是否开启 SQL 显示,默认值:
spring.shardingsphere.props.executor.size=2 # 工作线程数量,默认值: CPU 核数
```
-#### 分库分表配置[Top]
+#### 分库分表配置[Top]
sharding-jdbc支持4种分库分表策略
@@ -277,59 +308,59 @@ sharding-jdbc支持4种分库分表策略
* UUID
* SNOWFLAKE
-##### 单库单表配置[Top]
+##### 单库单表配置[Top]
course表有两个分表:course_1,couser_2
```properties
-# 实际逻辑表(必选)[Top]
+# 实际逻辑表(必选)[Top]
spring.shardingsphere.sharding.tables.course.actual-data-nodes=ds-1.course_$->{1..2}
-# 分表策略(必选):四种策略只能选一种[Top]
+# 分表策略(必选):四种策略只能选一种[Top]
spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column=cid
spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid % 2 + 1}
-# 主键生成策略,支持:UUID和SNOWFLAKE[Top]
+# 主键生成策略,支持:UUID和SNOWFLAKE[Top]
spring.shardingsphere.sharding.tables.course.key-generator.column=cid
spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
-# 主键生产策略的属性配置:UUID时不需要配置[Top]
-# 使用 SNOWFLAKE 算法,需要配置 worker.id 与 max.tolerate.time.difference.milliseconds 属性。[Top]
-# 若使用此算法生成值作分片值,建议配置 max.vibration.offset 属性[Top]
-# spring.shardingsphere.sharding.tables.course.key-generator.props.= 1[Top]
+# 主键生产策略的属性配置:UUID时不需要配置[Top]
+# 使用 SNOWFLAKE 算法,需要配置 worker.id 与 max.tolerate.time.difference.milliseconds 属性。[Top]
+# 若使用此算法生成值作分片值,建议配置 max.vibration.offset 属性[Top]
+# spring.shardingsphere.sharding.tables.course.key-generator.props.= 1[Top]
```
-#### 多库多表配置[Top]
+#### 多库多表配置[Top]
和单库多表配置相比,只是多了一个数据库路由策略配置
```properties
-# ----- 实际逻辑表1 user_course_xx[Top]
+# ----- 实际逻辑表1 user_course_xx[Top]
spring.shardingsphere.sharding.tables.user_course.actual-data-nodes=ds-$->{1..2}.user_course_$->{1..2}
-# 分库策略[Top]
+# 分库策略[Top]
spring.shardingsphere.sharding.tables.user_course.database-strategy.inline.sharding-column=cid
spring.shardingsphere.sharding.tables.user_course.database-strategy.inline.algorithm-expression=ds-$->{cid % 2 + 1}
-# 分表策略[Top]
+# 分表策略[Top]
spring.shardingsphere.sharding.tables.user_course.table-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.user_course.table-strategy.inline.algorithm-expression=user_course_$->{user_id % 2 + 1}
-# 主键生成策略,支持:UUID和SNOWFLAKE[Top]
+# 主键生成策略,支持:UUID和SNOWFLAKE[Top]
spring.shardingsphere.sharding.tables.user_course.key-generator.column=cid
spring.shardingsphere.sharding.tables.user_course.key-generator.type=UUID
-# ----- 实际逻辑表2 user_standard_xx[Top]
+# ----- 实际逻辑表2 user_standard_xx[Top]
spring.shardingsphere.sharding.tables.user_standard.actual-data-nodes=ds-$->{1..2}.user_standard_$->{1..2}
-# 分库策略[Top]
+# 分库策略[Top]
spring.shardingsphere.sharding.tables.user_standard.database-strategy.standard.sharding-column=cid
spring.shardingsphere.sharding.tables.user_standard.database-strategy.standard.precise-algorithm-class-name=com.sanwu.infra.sharding.DBCidShardingAlgorithm
#spring.shardingsphere.sharding.tables.user_standard.database-strategy.standard.range-algorithm-class-name=ds-$->{cid % 2 + 1}
-# 分表策略[Top]
+# 分表策略[Top]
spring.shardingsphere.sharding.tables.user_standard.table-strategy.standard.sharding-column=user_id
#spring.shardingsphere.sharding.tables.user_standard.table-strategy.inline.algorithm-expression=user_course_$->{user_id % 2 + 1}
spring.shardingsphere.sharding.tables.user_standard.table-strategy.standard.precise-algorithm-class-name=com.sanwu.infra.sharding.TableCidShardingAlgorithm
#spring.shardingsphere.sharding.tables.user_standard.table-strategy.standard.range-algorithm-class-name=user_id
```
-#### 绑定表配置[Top]
+#### 绑定表配置[Top]
[sharding-jdbc 绑定表](https://shardingsphere.apache.org/document/current/cn/features/sharding/concept/table/#绑定表)
绑定表:指分片规则一致的主表和子表,主要解决主表和字表的笛卡尔积问题.
@@ -368,7 +399,7 @@ SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE
其中 t_order 在 FROM 的最左侧,ShardingSphere 将会以它作为整个绑定表的主表。 所有路由计算将会只使用主表的策略,那么 t_order_item 表的分片计算将会使用 t_order 的条件。 因此,绑定表间的分区键需要完全相同。
-#### 广播表配置[Top]
+#### 广播表配置[Top]
[广播表官方说明](https://shardingsphere.apache.org/document/current/cn/features/sharding/concept/table/#广播表)
广播表:指所有的分片数据源中都存在的表,表结构及其数据在每个数据库中均完全一致。 适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
@@ -380,12 +411,12 @@ spring.shardingsphere.sharding.broadcast-tables[1]= # 广播表规则列表
spring.shardingsphere.sharding.broadcast-tables[x]= # 广播表规则列表
```
-#### 读写分离配置[Top]
+#### 读写分离配置[Top]
用于主从模式中,进行数据读写分离:主库写,从库读
```properties
-# 省略数据源配置,与数据分片一致[Top]
+# 省略数据源配置,与数据分片一致[Top]
spring.shardingsphere.sharding.master-slave-rules..master-data-source-name= # 主库数据源名称
spring.shardingsphere.sharding.master-slave-rules..slave-data-source-names[0]= # 从库数据源名称列表
spring.shardingsphere.sharding.master-slave-rules..slave-data-source-names[1]= # 从库数据源名称列表
@@ -394,10 +425,10 @@ spring.shardingsphere.sharding.master-slave-rules..load-balance-algorithm-type= # 从库负载均衡算法类型,可选值:ROUND_ROBIN,RANDOM。若 `load-balance-algorithm-class-name` 存在则忽略该配置
```
-#### 数据加密[Top]
+#### 数据加密[Top]
```properties
-# 省略数据源配置,与数据分片一致[Top]
+# 省略数据源配置,与数据分片一致[Top]
spring.shardingsphere.encrypt.encryptors..type= # 加解密器类型,可自定义或选择内置类型:MD5/AES
spring.shardingsphere.encrypt.encryptors..props.= # 属性配置, 注意:使用 AES 加密器,需要配置 AES 加密器的 KEY 属性:aes.key.value
spring.shardingsphere.encrypt.tables..columns..plainColumn= # 存储明文的字段
@@ -407,25 +438,25 @@ spring.shardingsphere.encrypt.tables..columns..en
```
-## 分库分表策略及思路[Top]
+## 分库分表策略及思路[Top]
sharding-jdbc支持4种分库分表策略:inline、standard、complex、hint。
实际应用中,经常会需要使用hint或complex方式去自定义自己的分库分表的策略实现类继承对应的实现类接口,以支持业务。
-### 分库策略[Top]
+### 分库策略[Top]
1. 根据自ID分库
2. 根据租户字段和主键类型字段分库
3. 根据租户字段分库(限每个租户只有一个库)
4. 根据租户字段和sharding_mapping业务字段分库):
-### 分表策略[Top]
+### 分表策略[Top]
1. 根据主键分表
2. 混合策略分表:依赖sharding_mapping分库分表
3. 根据日期月份和年份分表
-### 平滑迁移分表解决方案[Top]
+### 平滑迁移分表解决方案[Top]
增加配置字段`insert-table-nodes`:该字段用于配置允许插入数据的表节点,非全量表节点,必填字段。
@@ -438,7 +469,7 @@ sharding-jdbc支持4种分库分表策略:inline、standard、complex、hint
需要扩展新表,同时旧表不允许在插入数据(因为已经很多了)。那么此时该参数配置可以插入数据的表。
-### 一种混合策略思路[Top]
+### 一种混合策略思路[Top]
引入映射表`sharding_mapping`,并指定用于映射分表键的业务键:`mappingBizIdColumn`
@@ -464,7 +495,7 @@ sharding-jdbc支持4种分库分表策略:inline、standard、complex、hint
```properties
sanwu.sharding.config.tables.sw_account.mapping-biz-id-column=firm_id
-# 必须搭配sharding-jdbc的混合策略配置[Top]
+# 必须搭配sharding-jdbc的混合策略配置[Top]
sharding.jdbc.config.sharding.tables.sw_account.actual-data-nodes=ds-$->{0..1}.sw_account$->{0..1}
sharding.jdbc.config.sharding.tables.sw_account.table-strategy.complex.sharding-columns=account_no,firm_id
sharding.jdbc.config.sharding.tables.sw_account.table-strategy.complex.algorithm-class-name=com.sanwu.sharding.algorithm.TableByMappingComplexShardingAlgorithm
@@ -488,7 +519,7 @@ create table `sharding_mapping`
) engine=innodb default charset=utf8 COMMENT '分片映射表';
```
-### 多租户配置[Top]
+### 多租户配置[Top]
租户架构情况
@@ -504,7 +535,7 @@ create table `sharding_mapping`
* boss-db-2库有分表:t_user_3,t_user_4
* 根据user_id进行分表,期望user路由到user_db分库,boss用户路由到boss_db分库
-## 容量预估[Top]
+## 容量预估[Top]
分库分表首先是基于**现有的业务量和未来的增量**做出判断。
举个例子,现在我们日单量是10万单,预估一年后可以达到日100万单,根据业务属性,一般我们就支持查询半年内的订单,超过半年的订单需要做归档处理。
@@ -517,4 +548,4 @@ create table `sharding_mapping`
**分表参考公式**:
`(用户数量 * 用户每年产生数据量) / 每个表200-500万数据 = 分表数量`
-参考资料:[百亿级数据分表后怎么分页查询?](https://mp.weixin.qq.com/s/0ir23uCj4-kZfDX6BeoIMg)
\ No newline at end of file
+参考资料:[百亿级数据分表后怎么分页查询?](https://mp.weixin.qq.com/s/0ir23uCj4-kZfDX6BeoIMg)
diff --git a/src/main/java/com/toc/SPRING-SECURITY.md b/src/main/java/com/toc/SPRING-SECURITY.md
index ae9de87..16543ab 100644
--- a/src/main/java/com/toc/SPRING-SECURITY.md
+++ b/src/main/java/com/toc/SPRING-SECURITY.md
@@ -22,7 +22,7 @@
6.2. 基于AbstractAuthenticationProcessingFilter
6.2.1. WebSecurityConfigurerAdapter的配置demo
6.2.2. JWTAuthenticationFilter
- 6.2.3.
+ 6.2.3. JwtAuthenticationProvider
6.3. 基于OncePerRequestFilter
6.3.1. WebSecurityConfigurer
6.3.2. JwtTokenAuthenticationFilter
@@ -35,7 +35,7 @@
9. 相关资料
# Spring security[Top]
-[Spring Security官网][https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/#servlet-architecture]
+[Spring Security官网](https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/#servlet-architecture)
![avatar](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/four/spring-security.jpg)
@@ -453,7 +453,7 @@ public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFil
```
-#### [Top]
+#### JwtAuthenticationProvider[Top]
```java
@Slf4j