0%

[toc]

来源:
Redis IO多路复用技术以及epoll实现原理
精通Redis!epoll?IO的同/异步、阻塞/非阻塞?都懂了吗?


Q: select/poll的缺点?#

A:

  • select的本质是采用32个整数的32位,即3232= 1024来标识,fd值为1-1024。当fd的值超过1024限制时,就必须修改FD_SETSIZE的大小。这个时候就可以标识32max值范围的fd。
  • poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
  • select/poll的几大缺点:
  1. 每次调用select/poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select/poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. 针对select支持的文件描述符数量太小了,默认是1024
  4. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  5. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
  6. 相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制 ,但其他三个缺点依然存在。

而用了epoll,上面select的缺点都不复存在了。
他们三者的对比如下:
af606199b857d67784fc708b01ad193a0ae5b2ab


epoll的设计要点#

A:

  1. 首选Epoll在Linux内核中构建了一个文件系统,该文件系统采用红黑树来构建,红黑树在查询、新增、删除的效率极高,保障了在存在大量活跃连接的情况下的性能。 即新连接通过红黑树方式插入和更新

  2. 其次Epoll红黑树上采用事件异步唤醒,内核监听I/O,事件发生后内核搜索红黑树并将对应节点数据放入异步唤醒的事件队列中。这就避免了无差别的轮询,不会因为连接数增加而导致性能的快速下降。

  3. 最后Epoll的数据从用户空间到内核空间采用mmap存储I/O映射来加速。该方法是目前Linux进程间通信中传递最快,消耗最小,传递数据过程不涉及系统调用的方法。这点大大提升了存在大量FD时数据拷贝的消耗

详细解释:epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象

398cc1e5fdaa0abf90cd8e577fd826ade07c3ecf

[toc]

来源参考

DMA#

Q:DMA的作用是什么?#

A:
IO中断,需要CPU响应,需要CPU参与,因此效率比较低
用户进程需要读取磁盘数据,需要CPU中断,发起IO请求,每次的IO中断,都带来CPU的上下文切换。
DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU 的大量中断负载。


实际因此IO读取,涉及两个过程:

  1. DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区;
  2. 用户进程,将内核缓冲区的数据copy到用户空间。
    这两个过程,都是阻塞的,占用时间。
    9bc71cd4950d1563b41e145c834a7a02714125b7

传统数据传送#

Q: 传统数据传送的缺点是什么?

A: 如果是把文件二进制数据直接通过网络传输, 会涉及4次传输。

  1. 第一次:将磁盘文件,读取到操作系统内核缓冲区;
  2. 第二次:将内核缓冲区的数据,copy到application应用程序的buffer;
  3. 第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
  4. 第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。

b81d9302d0571857e766209319deeb824473bd49

如果不对数据做特殊处理的话, 那么2和3是没有必要的。

零拷贝原理#

什么是零拷贝?#

零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

  • 零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
  • 零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销

可以看出没有说不需要拷贝,只是说减少冗余[不必要]的拷贝。

目的:减少IO流程中不必要的拷贝
零拷贝需要OS支持,也就是需要kernel暴露api。虚拟机不能操作内核,

一、mmap内存映射#

DMA加载磁盘数据到kernel buffer后,应用程序缓冲区(application buffers)和内核缓冲区(kernel buffer)进行映射,数据再应用缓冲区和内核缓存区的改变就能省略。
3fb0ac7085f297bc9ebc376ca0b813313c0b703b

mmap内存映射将会经历:3次拷贝: 1次cpu copy,2次DMA copy;

epoll中事件的传递用的是mmap

二、sendfile#

当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer直接拷贝到socket buffer;
一旦数据全都拷贝到socket buffer,sendfile()系统调用将会return、代表数据转化的完成。
socket buffer里的数据就能在网络传输了。
6bc1d3912c82b88534cca1582fb77446d1a45916
sendfile会经历:3次拷贝,1次CPU copy 2次DMA copy;
以及2次上下文切换

三、、Sendfile With DMA Scatter/Gather Copy#

Scatter/Gather可以看作是sendfile的增强版,批量sendfile。

dae3d6b62f5de10f215226d8a8790ecc7ee6db5b
49ca15a47fae3cad2a776b8fdd5c4b5099f1cf5e
浅谈scatter-gather DMA
Scatter/Gather会经历2次拷贝: 0次cpu copy,2次DMA copy

为什么都是sendfile, scatter/gather不需要cpu拷贝呢?
我理解是copy过程借助的是DMA的gathercopy, 不再需要cpu的参与, 他可以一次性全部拷贝完,不用分多次。
7941f2f9868b88a70c970e955c7ceff5495734cd

四、splice#

数据从磁盘读取到OS内核缓冲区后,在内核缓冲区直接可将其转成内核空间其他数据buffer,而不需要拷贝到用户空间。
如下图所示,从磁盘读取到内核buffer后,在内核空间直接与socket buffer建立pipe管道。
和sendfile()不同的是,splice()不需要硬件支持。
d24a7b4f5d20e931f5a4b6bd681f556b98a012c5
splice会经历 2次拷贝: 0次cpu copy 2次DMA copy;

注意splice和sendfile的不同,sendfile是将磁盘数据加载到kernel buffer后,需要一次CPU copy,拷贝到socket buffer。
而splice是更进一步,连这个CPU copy也不需要了,直接将两个内核空间的buffer进行set up pipe。

linux 零拷贝机制对比#

e44c722b8b1949c80cd6267f9abc23e5e78bd4f0

零拷贝的应用#

Q: 知道零拷贝用在哪些地方吗?
A:

  • NIO提供的内存映射 MappedByteBuffer ——Linux mmap()
  • NIO FileChannel.transferTo() —— Linux sendfile()
  • Kafka Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入;
  • Kafka Customer从broker读取数据,采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。

  • mmap 适合小数据量读写,sendFile 适合大文件传输。
  • mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
  • sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
  • 在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

[toc]

整体流程#

  1. 加载BIOS(CPU相关信息)

  2. 读取磁道第一个扇区MBR(预启动信息)

  3. boot loader(操作系统运行之前运行的小程序)

  4. 加载内核

  5. 设定运行等级

  6. 启动内核

  7. 启动脚本登录界面

启动第一步--加载BIOS#

当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它。这是因为BIOS中包含了CPU的相关信息、设备启动顺序信息、硬盘信息、内存信息、时钟信息、PnP特性等等。在此之后,计算机心里就有谱了,知道应该去读取哪个硬件设备了。

启动第二步--读取MBR#

众所周知,硬盘上第0磁道第一个扇区被称为MBR,也就是Master Boot Record,即主引导记录,它的大小是512字节,别看地方不大,可里面却存放了预启动信息、分区表信息。

系统找到BIOS所指定的硬盘的MBR后,就会将其复制到0×7c00地址所在的物理内存中。其实被复制到物理内存的内容就是Boot Loader,而具体到你的电脑,那就是lilo或者grub了。

启动第三步--Boot Loader#

Boot Loader 就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核做好一切准备。

Boot Loader有若干种,其中Grub、Lilo和spfdisk是常见的Loader。

我们以Grub为例来讲解吧,毕竟用lilo和spfdisk的人并不多。

系统读取内存中的grub配置信息(一般为menu.lst或grub.lst),并依照此配置信息来启动不同的操作系统。

启动第四步--加载内核#

根据grub设定的内核映像所在路径,系统读取内存映像,并进行解压缩操作。此时,屏幕一般会输出“Uncompressing Linux”的提示。当解压缩内核完成后,屏幕输出“OK, booting the kernel”。

系统将解压后的内核放置在内存之中,并调用start_kernel()函数来启动一系列的初始化函数并初始化各种设备,完成Linux核心环境的建立。至此,Linux内核已经建立起来了,基于Linux的程序应该可以正常运行了。

启动第五步--用户层init依据inittab文件来设定运行等级#

内核被加载后,第一个运行的程序便是/sbin/init,该文件会读取/etc/inittab文件,并依据此文件来进行初始化工作。

其实/etc/inittab文件最主要的作用就是设定Linux的运行等级,其设定形式是“:id:5:initdefault:”,这就表明Linux需要运行在等级5上。Linux的运行等级设定如下:

0:关机

1:单用户模式

2:无网络支持的多用户模式

3:有网络支持的多用户模式

4:保留,未使用

5:有网络支持有X-Window支持的多用户模式

6:重新引导系统,即重启

关于/etc/inittab文件的学问,其实还有很多

启动第六步--init进程执行rc.sysinit#

在设定了运行等级后,Linux系统执行的第一个用户层文件就是/etc/rc.d/rc.sysinit脚本程序,它做的工作非常多,包括设定PATH、设定网络配置(/etc/sysconfig/network)、启动swap分区、设定/proc等等。如果你有兴趣,可以到/etc/rc.d中查看一下rc.sysinit文件,里面的脚本够你看几天的

启动第七步--启动内核模块#

具体是依据/etc/modules.conf文件或/etc/modules.d目录下的文件来装载内核模块。

启动第八步--执行不同运行级别的脚本程序#

根据运行级别的不同,系统会运行rc0.d到rc6.d中的相应的脚本程序,来完成相应的初始化工作和启动相应的服务。

启动第九步--执行/etc/rc.d/rc.local#

你如果打开了此文件,里面有一句话,读过之后,你就会对此命令的作用一目了然:

# This script will be executed after all the other init scripts.

# You can put your own initialization stuff in here if you don’t

# want to do the full Sys V style init stuff.

rc.local就是在一切初始化工作后,Linux留给用户进行个性化的地方。你可以把你想设置和启动的东西放到这里。

启动第十步--执行/bin/login程序,进入登录状态#

此时,系统已经进入到了等待用户输入username和password的时候了,你已经可以用自己的帐号登入系统了。😃

===

漫长的启动过程结束了,一切都清静了…

其实在这背后,还有着更加复杂的底层函数调用,等待着你去研究…本文就算抛砖引玉了:)

[toc]

man 命令输出该命令的使用手册
./ 当前目录…/ 父目录
ls常用参数-a 显示所有文件(包括隐藏的,all)-i 列出索引节点(inode)-l 输出长列表(即包含文件的各种信息,long)-s 输出每个文件的块大小(size)-F 区分各个文件,目录后加/,可执行文件后加*(区分file)
ls 参数 匹配串则可以只显示匹配字符串的文件,可以用*和?


touch 文件名创建一个空文件
touch -t 200101011200 文件名 这样可以修改文件的时间


cp 源文件 目标文件cp test1 test2 在当前目录中,将test1复制为新的test2cp test1 dir 将test1复制到目录dir中cp dir/test1 . 将dir/test1复制到当前目录中
cp -R dir1 dir2 将dir1中的所有内容进行复制,复制成dir2cp -f 强制覆盖,即如果有重复直接覆盖,不提示用户
cp -l test1 testlink 创建一个硬链接testlink,索引节点号与test1相同,类似“复制”。不可跨文件系统或挂载点cp -s test1 soft_test 创建一个软连接soft_test,节点号不同,可以跨文件系统或挂载点


mv test1 test_new_name 将test1重命名为test_new_name当重命名时,test1的软连接会变成无效链接,硬链接则仍然有效
rm 删除文件删除test1后,硬连接仍然显示内容,软连接则已经无效。
mkdirmkdir -m=MODE 设定模式,rwx-umaskmkdir -p parents 若路径上的目录不存在,自己建1个父目录mkdir -v 建目录时要显示信息
rmdir dir1 默认情况下,rmdir只能删除空目录rm -f dir1 ,可删除dir1中的所有文件加目录,但是会有提示rm -rf dir1, 删除所有且无提示
stat test1 可查看test1的状态信息,但没有文件类型file test1 可查看文件的类型(文本文件,可执行文件,数据文件)


当前目录中有a和bls>c则先生成c,再执行ls,再执行传输进C
tar:压缩命令z 创建tar.gz格式文件x 解压 c压缩vf一般都跟在最后,f指把压缩文件输出到某个目录下tar 命令符 所得到的压缩文件名字 要被压缩的目录
cat 直接跳最后一页less 可以上下查看more 只能向下查看


cat -n test1 给所有行加上“行号”cat -b test1 给“非空行”加上“行号”cat -s test1 压缩连续空白行为一行cat -T
tail 查看文件末尾10行,可用-n指定行数head 查看文件头,也可用-n指定行数


ps 查看当前进程
ps-e 查看系统所有进程
参数含义UID:启动用户PID:进程的进程号PPID:父进程号C: CPU利用率STIME:进程启动的系统时间TTY:进程启动时的终端设备TIME:运行进程需要的CPU时间CMD:程序名称
top:实时显示进程
w命令显示当前登录的用户和其进行的操作
uptime查看当前系统的启动时间


kill 进程号 相当于kill-TERM 尽可能地杀死进程,如果进程跑飞则可能无法杀死
kill -s HUP 进程号 先停止进程再结束进程
killall 进程名(可使用通配符)
mount 输出挂载设备


df 查看磁盘空间情况
du 了解哪个磁盘没有空间了cat /proc/cpuinfo:查看一台linux机器的CPU信息
du:查看该目录占用文件系统数据块的情况 cat /proc/swaps:查看SWAP分区信息df -lh:查看硬盘信息
sort file 对文本中的字符串进行排序
sort -n file 对文本中的数字进行排序
grep 字符串 文件 查找字符串在文件中的位置
tar 压缩文件

[toc]

解释器模式#

https://www.runoob.com/design-pattern/interpreter-pattern.html

这种模式实现了一个表达式接口,该接口解释一个特定的上下文。

项目中的expresion里的evalute本质上就是解释器模式。

模板方法模式#

抽象类定义n个抽象方法,提供一个对外接口确定抽象方法的调用顺序, 子类负责实现这几个内部的抽象方法。

责任链模式#

if/else太多了判断起来很烦, 用setNext设置一个责任链,谁完不成就交给下一个,这样后续直接添加setNext就好,不用再加else。

适用于需要“动态”添加判断条件情况。

命令模式#

把命令作为一个commond类看待,这样可以把命令放到队列或者栈里去处理。

迭代器模式#

提供给外界一个iterator()专门用于next遍历, 这样外界不用关心内部的存储逻辑。

备忘录模式#

https://www.runoob.com/design-pattern/memento-pattern.html

备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态

观察者模式#

A做了一件事之后,把自己的状态通知 B\C\D这3个观察者(需要绑定观察者)

注意和访问者不是同一个模式。

仲裁者#

A\B\C\D 这几个类对象每当做完一件事,就调用 E.update(xxx), E会负责更新整体状态,或者作为中间人去调度其他几个类。

状态模式#

状态不同, doAction时发生的事情也不同,类似于项目中做互信时,不同状态有不同的doAction处理逻辑

d146ad3e20bcc846133a5bb32f2bef82890f29d6

和策略模式的区别: 状态模式在于状态之间是由关联的,是可以切换状态的,且状态A到状态B可能有限制。
但是策略模式的话,不同的策略之间可能是独立的,仅仅是有同一个基类,但不存在可以切换的状态图。

策略模式#

装备了不同的武器或者策略,就会有不同的战斗结果。 我只要不断切换自己装备的武器子类即可。

访问者模式#

同一份数据或者树型结构, 可以用不同的vistor去访问,得到不同的结果或者展示。

[toc]

享元模式#

当有几种对象, 可能会被经常创建,且属性变化不大,也许就几种(例如常量1-100经常用)

一种方式是调用者经常自己手动创建这些对象

还有一种是封装到一个factory中, 调用者取factory中拿。

factory中会维护一个map, 缓存这些对象,这样就不用经常创建了。所以叫享元。
fbcb7c5c2bb6242e758efb7b42bfcf7bb57b731e


和其他的区别:#

  • 和原型模式的区别: 原型模式的重点在于提供clone方法去创建对象。 而享元模式不存在创建,更多是一个缓存结构(所以是结构型设计模式)
  • 和工厂模式的区别: 享元模式其实类似于单例+工厂的结合。 工厂模式不一定是享元模式,因为工厂模式可能真的是创建新的。 而享元模式基本会提供类似工厂方法的get方法,给调用者使用,本质上不生成新的对象了。

[toc]

外观模式#

也叫门面模式。

  • 当我们有很多的类,各种类之间的方法互相关联,存在复杂的调用先后顺序时
  1. 一种方法是告诉别人 这些类的用法, 让他们自己注意使用顺序。(比如你要先调A再调B才有用)
  2. 另一种是提供一个统一对外的接口, 在接口里去组装这些顺序。

这就是facade模式,看起来像一个窗口或者客户端, 外界不知道这些类的关系,但是只需要调门面的几个方法即可。
a4624283bf8c71587ad996a7279f4891ab4f1cf3
也不一定是为了复杂的调用顺序,可能就是为了让调用者不用关心用哪个类,只关心调哪个方法,如下:
2a2f769be48fb696ae6d25a31f79e378ae6d6cdd

  • 因为是将很多类作为成员封装进一个门面类里, 所以是结构型的设计模式。

举例: 做饭(买菜、洗碗、烧菜,希望封装到一个仆人类里来做)


代理模式#

在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。

代理模式中, 提供的行为和被代理对象是基本一致的,只是会额外做一些处理(例如打日志、监控),再调用被代理对象的方法。


代理模式和其他模式的区别#

  • 和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
  • 和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
  • 和外观模式的区别: 外观模式是可以封装好几个类,提供特定的单方法或者组合方法。 而代理模式大多是针对一个类做代理,且对外方法与被代理对象 基本一致。
  • 和仲裁者模式的区别: 仲裁者更多是三方 ABC交互,由B承担沟通工作,A不需要直接调用C。 代理模式则只涉及代理和被代理者。

[toc]

组合模式和装饰者模式#

一、组合模式#

该模式的意义:
用于树类, 或者支持将多个A对象合成1个A对象的 情形下
8953af0abe9db9e2badbc02eacbeb5a0325e0ab6
该模式的意义:
用于树类, 或者支持将多个A对象合成1个A对象的 情形下

主要方法:

  • 基类( 包含add或insert方法 )
  • 子类 包括 叶子类 和 非叶类
  • 非叶类 即可以add 叶子, 叶可以 add非叶。
  • 其实就是 子类里可以包含父类容器,不断添加、延伸。

计算引擎中的GNODE、logicalNode就是这种。

32a2333013afb0eb27642c817eb4e8de9d0f1cc7

优点#

  1. 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。

  2. 可以形成复杂的树形结构。

  3. 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。

缺点#

使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性
而且不是所有的方法都与叶子对象子类都有关联
要注意使用场景

二、装饰者模式#

如果我们 对某个 类, 以后可能会有很多小的修改加上去, 而且都是“装饰”性质的, 即加“功能”,而且这些功能可以一层层叠加
如果加功能频率很多,我们可以用装饰者模式。 经典应用就是java中的stream流
new XXXStream(new BufferStream(new FileOutputStream()))
d96605fa973db16ce93300c7e89582aa219dec2f

Q: 装饰模式和组合模式都是成员里会包含自己这个类或者子类,区别是?#

A:
组合模式, 重点体现在用成员类组成一个树形或者图型结构(重点不在于做事情), 使用者会关心成员类的内容且可能会遍历。
装饰者模式,重点体现在用成员类的方法做相同的事情,自己再做一些补充, 且可以不断叠加。 叠加后只提供一个最终叠加后的对象给别人使用, 别人不用关心里面叠加的那些中间对象。


Q: 装饰着模式和proxy代理模式又有什么区别? 代理模式也是用成员的方法调用之后,再做一些额外处理#

A:

  • 使用代理模式,代理和真实对象之间的的关系通常在编译时就已经确定了。 注意AOP里的所有代理对象其实都是编译时定好了用哪个代理
  • 而 装饰者能够在运行时递归地被构造,即运行期可以设置不同的装饰对象做装饰, 但是代理的话,代理是固定的,只不过可能会选用不同的代理罢了。