

沉淀、分享、成长,让自己和他人都能有所收获!😄
📢在面向对象编程中,封装和继承其实是不分开的:封装就是为了更好地继承。我们将几个类共同的一些属性和方法抽取出来,封装成一个类,就是为了通过继承最大化地实现代码复用。通过继承,子类可以直接使用父类中的属性和方法。

为了更好地使用OOP思想理解内核源码,我们可以把继承的概念定义得更宽松一点,除了内嵌结构体,C语言还可以有其他方法来模拟类的继承,如通过私有指针。我们可以把使用结构体类型定义各个不同的结构体变量,也可以看作继承,各个结构体变量就是子类,然后各个子类通过私有指针扩展各自的属性或方法。
这种继承方法主要适用于父类和子类差别不大的场合。如Linux内核中的网卡设备,不同厂家的网卡、不同速度的网卡,以及相同厂家不同品牌的网卡,它们的读写操作基本上都是一样的,都通过标准的网络协议传输数据,唯一不同的就是不同网卡之间存在一些差异,如I/O寄存器、I/O内存地址、中断号等硬件资源不相同。
遇到这些设备,我们完全不必给每个类型的网卡都实现一个结构体。我们可以将各个网卡一些相同的属性抽取出来,构建一个通用的结构体net_device,然后通过一个私有指针,指向每个网卡各自不同的属性和方法,通过这种设计可以最大程度地实现代码复用。如Linux内核中的net_device结构体。
structnet_device{char name[IFNAMSIZ];structhlist_node name_hlist;char*ifalias;/* mid-layer private */union{void*ml_priv;structpcpu_lstats __percpu *lstats;structpcpu_sw_netstats __percpu *tstats;structpcpu_dstats __percpu *dstats;structpcpu_vstats __percpu *vstats;};在net_device结构体定义中,我们可以看到一个私有指针成员变量:ml_priv。当我们使用该结构体类型定义不同的变量来表示不同型号的网卡设备时,这个私有指针就会指向各个网卡自身扩展的一些属性。如在bfin_can.c文件中,bfin_can这种类型的网卡自定义了一个结构体,用来保存自己的I/O内存地址、接收中断号、发送中断号等。
/* * bfin can private data */structbfin_can_priv{structcan_priv can;/* must be the first member */structnet_device*dev;void __iomem *membase;int rx_irq;int tx_irq;int err_irq;unsignedshort*pin_list;};每个使用net_device类型定义的结构体变量,都可以被看作是基类net_device的一个子类,各个子类可以通过自定义的结构体类型(如bfin_can_priv)在父类的基础上扩展自己的属性或方法,然后将结构体变量中的私有指针ml_priv指向它们即可。
含有纯虚函数的类,我们一般称之为抽象类。抽象类不能被实例化,实例化也没有意义,如animal类,它只能被子类继承。
抽象类的作用,主要就是实现分层:实现抽象层。当父类和子类之间的差别太大时,很难通过继承来实现代码复用,如生物类和狗类,我们\可以在它们之间添加一个animal抽象类。抽象类主要用来管理父类和子类的继承关系,通过分层来提高代码的复用性。如上面设备模型中的device类,位于kobj类和usb_device类之间,通过分层,可以更好地实现代码复用。
什么是接口呢?一个类支持的行为和方法就是接口。一个类封装好以后,留出API函数供别的对象使用,这些API就是接口。不同的对象之间可以通过接口进行通信,而不需要关心各自内部的实现,只要接口不变,内部实现即使改变了也不会影响接口的使用。接口就像山地车的手刹一样,无论是油刹还是碟刹,对于骑手来说都不需要关心,骑手关心的是通过控制手刹这个接口,自行车可以停就行。
接口与抽象类相比,两者有很多相似的地方。如两者都不能实例化对象,都是为了实现多态。不同点在于接口是对一些方法的封装,在类中不允许有数据成员,而抽象类中则允许有数据成员存在。除此之外,抽象类一般被子类继承,而接口一般要被类实现。我们可以把接口看作一个退化了的多重继承。接口简化了继承关系,解决了多重继承的冲突,可以将两个不相关的类建立关联。
在我们使用面向对象编程思想分析Linux内核的过程中,如果遇到多重继承让我们的分析变得复杂时,我们也可以考虑化繁为简,将多重继承简化为单继承,另一个继承使用接口代替。通过这种方法,我们可以把复杂问题降维分析,将复杂问题拆解简单化。如USB网卡驱动,既有USB子系统,又有网络驱动模块,放在一起分析比较复杂,我们可以通过接口,将多重继承改为单继承,就能将整个驱动的架构和分层关系简单化

以Linux内核中的RTL8150 USB网卡驱动源码为例:我们把以usb_device为基类的这条继承分支当作一个接口来处理,USB网卡通过usb_device封装的接口可以实现USB网卡设备的插拔检测、底层数据传输等功能。而对于以net_device为基类的这路继承,我们把它看作一个普通的单继承关系,USB网卡以kobject为基类,实现多级继承,每一级的基类都扩展了各自的方法或封装了接口,供其子类RTL8150调用。RTL8150网卡通过调用祖父类kobject的方法kobject_add()将设备注册到系统;通过调用device类的probe()完成驱动和设备的匹配及设备的suspend、shutdown等功能;通过调用net_device类实现的open、xmit、stop等接口完成网络设备的打开、数据发送、数据停止发送等功能。