https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzkxNTYwMTIyMg==&action=getalbum&album_id=3164991115087904771&subscene=159&subscene=189&scenenote=https%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzkxNTYwMTIyMg%3D%3D%26mid%3D2247483915%26idx%3D1%26sn%3D12a6c27ef80fabfb652b3c299f2e5eb8%26chksm%3Dc15de022f62a69343c9affd6eae91e2b54e80c5fe78a532171044e19ae9ddbe6e94a06572974%26cur_album_id%3D3164991115087904771%26scene%3D189%23wechat_redirect&nolastread=1#wechat_redirect
原创 陆小易 Linux内核网络 2023-10-25 21:30 发表于陕西
“ 江流宛转绕芳甸,月照花林皆似霰。”
单例设计模式是一种对象创建型的设计模式,它保证一个类只有一个实例,并且提供一个全局访问点来访问该实例。
单例设计模式包括以下角色:
1. 单例类(Singleton):包含一个静态的实例变量和一个静态的方法,用于 创建单例实例。
2. 客户端(Client):通过单例类的静态方法来访问单例实例。
在Linux内核中,单例模式被广泛使用,例如在设备驱动程序中。设备驱动程序通常需要访问硬件设备,而硬件设备只有一个实例,因此可以使用单例模式来管理设备的访问。
以下简单示例说明在Linux内核中如何使用单例模式
内核实例(一)
首先,我们需要定义一个设备结构体,包含设备的属性和方法:
然后,我们可以定义一个单例函数 get_device,用于获取设备实例:
在这个函数中,我们使用了一个静态变量instance来保存设备实例。如果设备实例不存在,则使用kzalloc函数分配内存来创建一个新的实例。然后,我们初始化设备的属性,并使用device_initialize函数来初始化设备结构体。
最后,我们可以通过调用get_device函数来获取设备实例,并使用设备实例来访问设备的属性和方法。
这种方式可以确保在Linux内核中只有一个设备实例,并提供一个全局访问点来访问该实例。
内核实例(二)
静态的变量或者全局变量包括结构体对象,函数对象,都是单例模式实现.
使用对象 DATA存储实例,并且提供了唯一访问点 get_data().
全局变量包括结构体变量,函数指针,数组等,也包括静态局部变量。这些都算是C语言的单例模式实现。
或者
或者
总之:单例模式,在Linux 内核中主要还是以全局变量或者静态局部变量形式存在,提供一个函数作为全局访问点貌似更简单合理一点,特别是当这个对象是临界资源可能多个线程要竞争访问时,容易加锁处理。
原型设计模式是一种创建型设计模式,它通过复制现有的对象来创建新的对象,而不是从头开始创建,增加了代码的灵活性和可维护性。
原型设计模式包括以下角色:
1. 原型(Prototype):定义了复制接口和基本操作。
2. 具体原型(Concrete Prototype):实现了原型接口的具体实现,包含具体的操作和状态。
使用原型设计模式,可以避免从头开始创建对象,而是通过复制现有的对象来创建新的对象,从而节省了创建对象的时间和成本。同时,原型设计模式还可以提供对对象的透明访问,使得客户端程序可以直接访问对象,而不需要通过接口进行访问。
优点:性能提高,减少复杂创建的对象的逻辑,逃过了构造函数的初始操作;
缺点:可能引起的引用问题;需要提供一个clone接口,实现一个clone()方法。
内核实例(二)
在Linux内核网络中,原型模式(Prototype Pattern)是一种创建对象的设计模式,它通过复制现有的对象来创建新对象,避免了逐个创建对象的过程。
在Linux内核网络中,原型模式被广泛应用于网络数据包的(sk Buffer)的处理中。sk Buffer是网络数据包的的一个内存缓冲区,包含数据包的各种信息,如数据包头、数据包内容等。
当网络应用程序发送数据包时,Linux内核会为该数据包分配一个skb结构体,skb包含了网络数据包的相关信息,如协议、数据包头等。skb结构体中还包含一个数据包的指针,指向数据包的内容。
当网络数据包到达Linux内核时,内核会根据skb结构体中的信息对数据包进行各种处理,如路由、过滤等。在处理过程中,有时需要复制一个现有的skb结构体,以便在不同的处理路径上使用。
为了方便复制skb结构体,Linux内核提供了skb_clone函数(浅拷贝),skb_copy()函数(深拷贝)该函数将现有的skb结构体复制一份,并返回新的skb结构体的指针。skb_clone函数会复制skb结构体中的所有信息,包括数据包的指针,因此新的skb结构体和原来的skb结构体完全独立,可以分别进行处理。
通过使用原型模式,Linux内核网络可以高效地处理网络数据包,并且避免了逐个创建skb结构体的开销,提高了系统的性能和响应速度。
skb_clone()
skb_copy() 和原来的skb完全独立。
skb在linux 内核网络桥接的代码中,未知的单播或者广播会在桥下洪泛,洪泛到桥下所有端口,此时报文会skb_clone(); 因为此时并不会修改skb的内容,只是增加一个引用计数;atomic_inc(&(skb_shinfo(skb)->dataref))。比如端口镜像,同样只是报文的clone即可,比skb_copy() 性能会好点。
下图是组播处理,同样是skb_clone().
有些场景,比如报文交给另一个任务去处理,不清楚这个任务会有何操作,可能会修改其中的内容,或者持有这个报文会较长时间,所以skb_copy此处更合适。
抽象工厂模式也是一种创建型设计模式,它提供了一个工厂接口,用于创建相关或依赖对象的集合,而不需要指定具体的产品类。通过将具体的工厂实现封装在具体工厂类中,可以在不改变客户端代码的情况下,灵活地更换具体工厂,从而创建不同的对象集合。
其实就是抽象出共性,收敛变化,灵活应对变化。
对 c 语言来讲,数据类型就是类,声明一个变量就是对象的创建,这些对象初始化就是构造函数,free就是析构函数;比如 int , char ,struct ,union, enum,void *; void*(*func), 数组等等这些。
举例:int a = 100;此处创建了一个整形对象a,并且初始化为 100。c语言是个开放式的编程语言都是public,没有面向对象JAVA、C++ 那么多private,protected 的权限设定。这个意义上讲c 语言符号表都是开放式的,符号地址也是全局可以随机访问的,这其实也是不安全的,除了用户地址和内核地址保护模式区分外。
抽象工厂设计模式包括以下角色:
1. 抽象工厂(Abstract Factory):定义了创建对象集合的接口。
2. 具体工厂(Concrete Factory):实现了抽象工厂接口,具体负责创建对象集合。
3. 抽象产品(Abstract Product):定义了产品的接口和基本操作。
4. 具体产品(Concrete Product):实现了产品接口的具体实现。
以下简单示例说明在Linux内核中如何使用抽象工厂模式
内核实例(一)
在Linux内核网络中,抽象工厂模式被用于创建网络协议单元(protocol object)。网络协议单元是Linux内核中用于管理网络协议的一种数据结构,它包含了与特定协议相关的方法和属性。
下面是一个简单的抽象工厂模式的示例代码,用于创建Linux内核网络中的协议对象:
在这个示例中,net_proto是一个抽象协议对象接口,定义了所有协议对象必须实现的方法和属性。net_proto_ipv4和net_proto_ipv6是具体的协议对象实现,它们实现了net_proto接口中的所有方法。
net_proto_get函数是一个工厂方法,根据传入的协议族(family参数),返回相应的协议对象。这个函数使用了抽象工厂模式,它返回一个指向具体协议对象的指针,而不是返回一个具体的协议对象实例。这样,调用方可以使用返回的指针来调用协议对象的方法,实现了对不同协议的支持。
通过使用抽象工厂模式,Linux内核网络可以灵活地创建不同类型的协议对象,而不需要硬编码具体的协议对象类型。这使得内核可以轻松地添加新的协议支持,而不需要修改现有的代码。
实例 (二)
Linux内核中的抽象工厂模式主要是通过提供一个接口和一系列的抽象产品,将具体的实现细节隐藏在工厂类中,从而达到代码的高内聚性和低耦合性。
在面向过程的C 编程语言中,实现抽象工厂模式可以通过以下步骤:
1. 定义产品接口
首先,需要定义一个产品接口,它规定了所有产品应该具有的方法和属性。这个接口可以被多个具体产品实现,从而让它们能够在客户端以相同的方式使用。
例如,我们可以定义一个“图形”接口,其中包含绘制、移动等图形对象的方法。
2. 实现具体产品
接下来,我们需要实现多个具体产品,这些产品都实现了产品接口中定义的方法和属性。
例如,我们可以实现一个圆形和一个矩形,它们都实现了“图形”接口中的方法。
3. 定义工厂接口
接下来,我们需要定义一个工厂接口,它规定了创建产品的方法。
例如,我们可以定义一个“图形工厂”接口,其中包含一个创建图形的方法。
4. 实现具体工厂
接下来,我们需要实现多个具体工厂,这些工厂都实现了工厂接口中定义的方法。
例如,我们可以实现一个圆形工厂和一个矩形工厂,它们都实现了“图形工厂”接口中的方法。
5. 客户端代码
最后,客户端代码可以通过调用工厂接口中的方法来创建具体的产品。
例如,客户端代码可以调用“图形工厂”接口中的方法来创建一个圆形或矩形对象,并调用这些对象的方法来绘制和移动它们。
以下是示例代码:
如上两个示例说明在Linux c 场景中使用抽象工厂模式的代码。
建造者设计模式是一种创建型设计模式,它将复杂的对象的分解为更小的部分,并按照一定的顺序构建这些部分,以创建最终的对象,实现代码的灵活性和可维护性。
建造者设计模式包括以下角色:
1. 抽象建造者:定义了构建对象的接口。
2. 具体建造者:实现了抽象建造者接口,具体负责构建对象的部分。
3. 抽象产品:定义了最终要构建的对象的接口和基本操作。
4. 具体产品:实现了最终要构建的对象的接口和具体实。
5. 建造者管理器:负责使用具体的建造者来构建最终的对象。
使用建造者设计模式,可以将复杂的对象的分解为更小的部分,并按照一定的顺序构建这些部分,以创建最终的对象。这样可以提高代码的复用性和可维护性,同时还可以将构建逻辑封装在建造者中,使得代码更加清晰和易于维护。
以下简单示例说明在Linux内核中如何使用建造者模式
内核实例
在Linux内核网络中,建造者模式可以用于创建网络协议栈或网络设备驱动程序。下面是一个简单的示例代码,展示了如何使用建造者模式创建网络协议栈:
在这个示例中,我们定义了三个不同的创建函数:create_net_device、create_net_protocol 和 create_net。这些函数分别用于创建网络设备、网络协议和网络协议栈。
create_net_device 函数首先使用 alloc_netdev_mq 函数分配一个新的网络设备结构体,然后设置一些设备的属性,最后注册设备并返回。如果注册失败,它将释放设备结构体并返回 NULL。
create_net_protocol 函数使用 kzalloc 函数分配一个新的网络协议结构体,然后设置一些协议的属性,最后返回协议结构体。
最后,create_net 函数首先使用 alloc_net_ns 函数分配一个新的网络命名空间,然后设置一些网络的属性,最后创建一个网络设备并将其添加到网络命名空间中。如果设备创建失败,它将释放设备结构体和网络命名空间,并返回 NULL。
这个示例展示了如何使用建造者模式来创建一个复杂的网络协议栈。通过将创建过程分解为多个步骤,我们可以更轻松地管理和维护代码。
命令设计模式是一种行为设计模式,它允许你将请求封装为对象,从而使你可以使用不同的请求和响应来参数化不同的行为。
命令设计模式的组成部分包括:
1. 命令:这是一个抽象类或接口,定义了一个执行请求的方法。
2. 具体命令:这是一个实现命令类的对象,它包含执行请求所需的所有数据和方法。
3. 请求者:这是一个对象,它包含一个指向具体命令对象的引用,并调用其执行方法。
4. 接收者:这是一个对象,它知道如何执行请求。具体命令将请求委托给接收者。
5. 客户端:这是一个对象,它将具体命令对象传递给请求者,并触发请求。
使用命令设计模式的好处包括:
1. 将请求和响应解耦,使你可以在不同的请求和响应之间进行切换。
2. 使得你可以容易地添加新命令,因为你不必更改现有的代码。
3. 使得你可以将命令对象作为参数传递,从而可以使用不同的命令对象来实现不同的行为。
4. 使得你可以轻松地撤销 命令,从而可以轻松地回滚操作。
以下简单示例说明在Linux内核中如何使用命令模式
Linux内核网络栈中使用了多种命令模式,其中比较常见的有以下几种:
Netlink模式:Netlink是Linux内核中用于进程间通信的一种机制,它可以用于网络管理和配置。在Netlink模式下,用户空间进程通过Netlink套接字向内核发送命令,如路由信息、接口配置等。
Socket模式:Socket是Linux内核中用于进程间通信的另一种机制。在Socket模式下,用户空间进程使用socket API(如socket、bind、connect、send、recv等)来与内核进行通信,内核通过处理这些请求来实现网络功能。
ioctl模式:ioctl是Linux内核中用于控制设备参数的一种命令机制。在网络设备中,可以使用ioctl命令来设置网络接口的参数,如接口类型、MTU等。
ioctl+Socket模式:这种模式结合了ioctl和Socket两种模式的优点。用户空间进程首先通过ioctl命令设置网络接口参数,然后使用Socket API进行数据传输。
这些命令模式可以根据不同的应用场景和需求进行选择和组合使用。
用户态iproute2 工具ip addr命令 Netlink 模式实例
具体命令:
客户端:
请求者:
接收者:
状态机设计模式是网络协议常用的软件设计模式,它允许一个对象在其内部状态改变时改变其行为。状态机通常由三个部分组成:状态(state)、事件(event),行为(action)。
状态表示对象的一种状态,它包含一组特定的属性和行为。状态迁移表示一个事件发生后,对象执行某种行为从当前状态转换到下一种状态的过程。
状态机设计模式的核心思想是将对象的行为与其状态分离,使得对象在不同的状态下具有不同的行为。这样,当对象的状态改变时,其行为也会相应地改变。
一句话描述FSM就是:某个对象在当前状态下,接收到某个事件之后执行某个动作,之后,对象的状态切换为下一个状态。
"Cur_State -- Event -- Action -- Next State"
当前所讲的是有限状态机( Finite-State Machine, FSM),就是说输入和输出都是有限种状态。FSM 最常见的使用场景就是实现网络通信协议,比如TCP,SSH,Telnet,BGP,Capwap控制协议等。
状态机设计模式的实现框架通常有三种:switch-case 法, 函数指针法,二维表驱动法。这三种方式其实就是对状态机模式逻辑表示的三种数据结构,是一种符号描述和抽象。
一个程序的算法和复杂性首先依赖于数据结构,数据结构的设计是否合理在很大程度上决定了程序结构是否清晰,操作是否灵活,是否具备可扩展性和可维护性。
在Linux 内核中,设计模式集中体现在抽象的数据结构设计,并无复杂的算法逻辑。
下面使用案例描述下三种状态机实现方法。
1. Switch-case 法:
以下是一个使用C语言实现状态机设计模式的示例代码,用于实现网络数据包的接收和传输:状态用switch-case 表示,事件也可以用switch-case 表示; 状态和事件是多对多的关系,所以可以状态嵌套事件,也可以事件嵌套状态。
后续维护,可以扩展case 变量,switch-case 还是比较简单。
在上述代码中,我们定义了一个状态枚举类型State,用于表示当前的网络状态。我们还定义了两个函数process_packet和transmit_packet,分别用于处理和传输数据包。
在transition函数中,我们根据当前状态和接收到的数据包长度,进行状态转移。在不同的状态下,我们可以执行不同的操作,例如验证数据包的有效性、处理数据包、传输数据包等。
在主函数中,我们使用一个无限循环来模拟网络的持续运行。每次循环中,我们调用transition函数来更新当前状态,并根据当前状态执行相应的操作。
2. 函数指针法:
以SSH(Secure Shell)协议为例分析
ssh2s_state:
ssh2s_event:
二维函数指针数组 ssh2s_action[state][event];
SSH_FSM 的初始化函数:
SSH_FSM 的运转函数:
用枚举变量表示状态与事件,用函数指针数组表述行为,用函数返回值表示下一个状态。使用函数指针的表现形式也是比较容易的,只是要注意整个FSM的初始化,以及在状态和事件非法时的处理逻辑,否则容易引发堆栈。当然这种函数指针实现方式效率应该比swich-case高不少。
3. 二维表驱动法:
以TELNET协议为例分析,这里面有两层表示,还有一个子状态机迁移过程。下面表格是通用表示方法。
当前状态 |
输入事件 |
下一个状态 |
动作 |
s1 |
c1 |
s2 |
a1 |
s2 |
c2 |
s3 |
a2 |
s3 |
c1 |
s2 |
a3 |
s2 |
c2 |
s3 |
a2 |
s3 |
c1 |
s2 |
a3 |
s2 |
c1 |
s_trap |
a_trap |
s_trap |
c1 |
s_trap |
a_trap |
基本数据结构表示Fsm_trans.
状态机二维表驱动表示如下:
FSM初始化
状态机的运转
TELNET与SSH协议不同的是:
(1)telnet的下一个状态同样写到状态机二维数组里面,而ssh协议没有
写,将action的返回值作为状态机的下一个状态;
(2)telnet专门写了一个状态转移的结构FSM_TRANS,action在结构里面,而ssh是写了一个
SSH2S_DO_EVENT ssh2s_action[ssh2s_s_last][ssh2s_e_last]; state是一个枚举,event也是一个枚举。
总之,三种状态机模式:
从实现难易度上说:三者差不多,都比较容易实现。
从执行效率上讲:二维表驱动法 >= 函数指针法 > switch-case 法
从代码描述上看:二维表驱动法比函数指针可读性更强, switch-case属于入门级实现,有些简单相对稳定的协议场景可以用。
综上,通过使用状态机设计模式,我们可以将网络的不同状态和状态之间的转移逻辑封装在不同的函数中,从而使代码更易于维护和扩展。同时,这种设计模式还提高了代码的可重用性,可以方便地应用于其他网络应用程序。
原创 陆小易 Linux内核网络 2023-11-13 23:39 发表于陕西
“ 可怜楼上月裴回,应照离人妆镜台”
观察者设计模式是一种行为型设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题(subject)对象,当主题对象状态发生变化时,观察者对象都会收到通知并更新。
观察者充当一个订阅者的角色, 正如微信订阅号一样,账号发表一个文章,所有关注者都会收到文章推送。关注者可以定义回调函数,比如阅读后收藏的动作。这些关注者是相互独立的,之间没有没有任何关系,也没有任何优先级。
在linux内核网络中,最常用的就是通知链功能。linux 内核中的各个子系统既相互依赖又是一个组合的整体,当其中一个子系统状态发生改变时,内核使用时间通知链机制来告知其他子系统做相应的操作,目的各系统之间状态同步。
事件通知链表是一个事件处理函数的列表,每个通知链都与某个或某些事件有关,当特定的事件发生时,就调用相应的事件通知链中的回调函数,进行相应的处理。
Linux的网络子系统一共有3个通知链:表示ipv4地址发生变化时的inetaddr_chain;表示ipv6地址发生变化的inet6addr_chain;还有表示设备注册、状态变化的netdev_chain。
通知链节点 notifier_block
notifier_call:当相应事件发生时应该调用的函数,由被通知方提供;
notifier_block *next:用于链接成链表的指针;
通知链的调用
子系统0 产生事件,观察者分别是子系统1,2,3.
某对象发生变化,需要通知其他对象时,可以遍历链表里各子系统,执行注册的回调函数。这个机制在内核中比较常见。
综上所述,观察者设计模式在Linux内核中被广泛使用,用于通知其他组件关于系统状态的变化。通过创建观察者对象并订阅主题对象,观察者对象可以在主题对象状态发生变化时收到通知,并进行相应的处理。这样可以提高代码的复用性和可维护性,同时也可以将主题对象的状态封装在具体主题中,使得代码更加清晰和易于维护。
原创 陆小易 Linux内核网络 2023-11-14 23:10 发表于陕西
“ 玉户帘中卷不去,捣衣砧上拂还来。”
责任链设计模式是一种行为型设计模式,它允许开发者将多个对象串联起来,形成一个处理请求的责任链。每个对象只关心它能够处理的请求,如果不能处理则将其传递给下一个对象。
这个结构可以是链表,树形结构,也可以是其他数据结构形式。
链式结构:可以是一维,二维,也可以是三维链表。
树形结构:
责任链时序图:
在Linux内核网络中,责任链设计模式被用于实现网络协议的处理和网络数据包的处理。
1. 网络协议处理
每个网络协议都被实现为一个协议族。协议族通过注册到内核中的套接字实现。责任链设计模式被用于将多个协议族串联起来,形成一个处理网络协议的链条。当收到一个网络协议请求时,内核会从协议族链条的头开始依次遍历每个协议族,直到找到能够处理该请求的协议族。
2. 网络数据包处理
网络数据包的处理是通过在linux内核网络datapath的几个必经点上实现的。典型的就是netfilter的实现。其主要功能就是对报文的处理,对注册到nf_hooks[proto][hook_num]上的nf_hook_ops 进行处理,如上图形成一个处理网络数据包的责任链。当收到一个网络数据包时,内核会从nf_hooks链表的头开始依次遍历每个nf_hook对象,直到遍历完成。
下面以一个网络安全监测模块botnet举例说明:
0. 总体结构如下:
1. checkpoint 结构
2. checkmodule结构
3. 全局的责任链
4. checkpoint的注册
5. checkmodule的注册
6. 函数调用流程
如上实现:在botnet模块,checkpoint就是挂接点,相当于netfilter的hooknum,但是botnet实际都是在一个挂载点上搞,只不过做了简单分类,从这个意义上讲,这个checkpoint可以理解为netfilter里面节点nf_hook_ops里面的pf协议族(ipv4,arp,桥,所有协议等),做了分类;netfilter是一个三维节点图,botnet就是一个二维点图!
综上:责任链貌似和上一个观察者模式有点雷同,其实有个明显的不同就是,责任链的节点是有优先级的,并且与主程序是相关联的,而观察者节点是相互独立的,主程序事件会通知到每个观察者,而责任链是挨个按照优先级处理,并且会对这个事件做相应的处理。
原创 陆小易 Linux内核网络 2023-11-16 22:34 发表于陕西
"此时相望不相闻,愿逐月华刘照君"
策略设计模式是一种对象行为型设计模式,它允许在运行时动态地改变对象的行为。该模式将算法封装到独立的策略类中,使得对象可以根据不同的策略类来选择不同的算法。
实现了对象行为和上下文的解耦,这使得策略类可以独立于上下文进行操作,从而增加了代码的灵活性和可维护性。此处的上下文是指封装了策略的组件。
策略设计模式时序图
上图的client 是客户端使用者,context 是封装了策略的组件, Strategy A 和B是两种应用策略。client 使用策略A,则Context 将操作交给策略A,策略A执行操作并返回;client 改变策略,使用策略B,则Context 将操作交给策略B, 策略将结果返回给Context, Context再将结果返回给client。
下面以一个网络协议过滤模块举例说明:
Strategy
在上述代码中,我们定义了一个Filter 类,它包含一个报文过滤方法。我们定义了三个不同的过滤对象,filter_ip、filter_tcp和filter_udp,它们分别对应不同的网络协议。这些过滤函数接收一个指向数据包的数据的指针和数据包的长度,并返回一个整数值,表示数据包是否被过滤。
在主函数中,我们创建了一个包含三个过滤器的数组filters,并且每个过滤器对应一个协议。我们传递一个数据包给每个过滤器,并检查每个过滤器的过滤函数是否过滤了数据包。如果数据包被过滤,我们打印出使用该过滤器的过滤函数的名称。
command
上面函数将主函数的for 语句封装为Context函数,这个实现看起来就是命令模式, 因为不同的协议返回了不同的对象,不同的对象去过滤报文。 而策略模式更像是做同一件事情的不同方法。这是两者的不同之处。
原创 LinuxNetworking Linux内核网络 2023-11-18 22:43 发表于陕西
"鸿雁长飞光不度,鱼龙潜跃水成文"
模板方法设计模式是一种行为型设计模式,它定义了一个算法的框架,包含一些具体步骤的抽象方法,子类必须实现这些抽象方法以完成算法的实现。模板方法基于继承,子类也可以扩展更改这些算法。
设计模式只是技巧,就是在大量重复编写冗余代码之后总结分析出一种设计模式和编码套路,并不是某个语言(比如面向对象语言)诞生后就有的模式。所以在这个意义上讲,设计模式和设计原则是 "技" 。
试想某个场景,某开发团队老大在review大家代码的时候,察觉到一个严重的问题:发现码农张三李四王五三人写的各子系统的代码有些是重复的,都在重复造轮子,于是老大从整个系统软件架构的宏观角度看这些代码,发现了太多的重复和冗余,既缺乏模块化的设计,又没有扩展性、复用性和维护性方面的考量。最后老大提出一种解决方案:重构整套系统,将三者的代码抽象、分离、解耦。抽象出共性,分离出特性。共性的逻辑完全可以复用一套逻辑,特性的逻辑可以通过多态化的方式或者单独处理。以这种思路总结出的这套方法就是设计模式和设计原则。
每种设计模式都有其侧重点,见名知义,本文要讲的模板方法侧重点就是复用性。
如上图,我们要把某个对象装进某个容器;step1: 打开容器门;step2: 把对象放进去;step3 :关上容器门。 这些就是模板的操作, 无论是把大象装进冰箱还是把现金存到保险柜里面都是可以复用这个逻辑的。
大象装冰箱模板方法时序图
内核网络实例:
Linux内核网络中使用了模板方法设计模式来定义网络设备操作的基本框架,具体实现可以通过继承和重写父类中的函数来完成。
在上述代码中,struct net_device 结构体定义了网络设备的属性和方法,其中 ops 指向一个 net_device_operations 结构体,该结构体定义了一组网络设备操作的方法,例如打开设备、停止设备、发送数据包和初始化设备等。
通过定义这些基本方法,Linux内核网络框架为网络设备提供了一个通用的操作接口。设备驱动程序可以继承这些方法,并实现自己的设备操作。例如,一个具体的设备驱动程序可以定义如下的操作方法:
在上述代码中,定义了一个my_net_device 结构体,它包含一个 net_device 结构体和一个自定义的操作方法(这个需要自己实现)。具体的设备驱动程序实现了 my_net_device_operations 结构体中的方法,并通过 netdev_priv() 函数获取设备私有的数据结构 my_net_device,然后调用相应的设备操作方法。
在Linux 中:"一切皆文件"。无论是设备文件还是文件系统,网络socket 只是稍微特别一点,其实都是file_operations的对象,都可以直接当做一个文件来open(), read(), write(), close()。 这其实就是模板方法设计逻辑。
当然有些设计模式最初提出来主要是在面向对象的编程环境中,所以有些模式直接用 C 来解释,貌似有点生搬硬套。但是无论面向过程还是面向对象:设计思想是一样的,软件编码的目标是一致的, 设计模式是通用的,设计原则也都是要遵守的。