Linux kernel 分析之十七:设计模式-用C来实现继承和模板

多态实现了,封装呢?基本上,C的结构体是不设防的,谁都可以访问。从这一点来看,C很难实现封装。尽管C中有static关键字,可以保证函数和变量的作用仅限于本文件,尽管内核可以通过控制导出符号表(EXPORT_SYMBOL)来控制提供给下层模块的函数和变量,但这些与C++中的封装相去甚远。好在内核的原则是“相信内核不会自己伤害自己”。所以就不苛求啦。
那么继承呢?这个也基本上很难。不过我们可以通过组合来模拟“继
承”。ext2_inode_info是ext2的inode在内存中的结构体(注意,不是ext2_inode,这个是在硬盘上),有个成员变量是struct inode*,指向inode。使用时,用EXT2_I()宏来实现类型的down_cast。这样很不直观。当然我们也可以理解为ext2_inode_info与inode是两个完全不同的结构体,ext2_inode_info包含了inode。但是这样的理解显然更简单轻巧:ext2_inode_info继承了inode,并且加了自己的私有成员变量。
这是一种实现继承,ext2_inode_info实实在在地继承了inode 的全部“财产”- 成员变量,还有一种接口继承,也使用得非常广泛。如ext2_dir_inode_operations(定义了对ext2的目录节点的操作)对inode_operations的继承。与通常理解的“继承”不同,在C中并没有生成新的结构体,而只是定义了一个inode_operations类型的变量。这种继承只是把里面的函数指针加以实现。这种继承很辛苦,因为接口除了声明,什么也没做。
有了父类子类,自然也少不了类型之间的转换,以及运行中的类型识别(RTTI)。由于内核中只是用组合来模拟继承关系,即用子类包含父类的方式,所以从子类转换到父类就很方便。如子类是struct Derive d里有个成员变量struct Base *b,从子类转换到父类只要d->b就行,但是从父类down cast,向下类型转换到子类就比较麻烦了。然而这种情况非常常见,如设备驱动接口file_operations中
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
如果我们开发一个叫scull的字符设备,通常要定义自己的结构体scull_dev,此外还要继承cdev(表现为组合).可是ioctl接口里没有scull_dev!幸好inode->i_cdev指向的就是cdev。那么如何通过cdev得到scull_dev呢?内核提供了container_of()宏。struct scullc_dev *dev;
dev = container_of(inode->i_cdev, struct scullc_dev, cdev);
这可比down_cast之类复杂多了。里面使用了黑客手段,有兴趣可以看看container_of的实现。
好像还忘记了什么。。。对了,模板呢?
说内核不需要模板是不可能的。光是链表一项,就有很多地方要用到,进程之
间,dentry之间。。。如果为每种情况写一套链表操作,那是很可怕的事。理论上,我们有两种选择,以循环链表,task_struct为例:1.把指针pprev,next放到task_struct中,然后写一套宏,把类型作为参数传进去,实现对循环链表的操作。这个是最自然的思路,最接近C++的模板。但是,问题来了,如果task_struct同时属于好几个链表怎么办(虽然听起来这个想法很怪,但task_struct的确有这样的需求)?
2.对第一种方法的改进:实现一个对最简单的结构体list_head的链表操作。然后把list_head等包含到task_struct 结构体里。如果要对task_struct所在链表进行操作,只要操作对应的list_head就可以了。所以解决了1的问题。至于怎么通过list_head获得task_struct,可以参考container_of()宏的做法。
问题是解决了,但是与前面几种模拟办法相比,这种是最不直观的。因为当我在一个结构体里发现了list_head后,根本不知道它所在的链表究竟放的是什么结构体。事实上只要某个结构体有list_head这个成员,就可以放到链表里。有些类似于MFC的CObjectList。这就有些恐怖了。
不过只要编程者清楚里面放了些什么,就没有问题。“相信内核不会伤害自己”
不过我很好奇,如果换成C++,如何实现list_head的效果呢?能否实现一种新的multi_list模板,它与list的区别在于节点可以属于多个链表。
读核感悟-设计模式-文件系统和设备的继承和接口
接着,我们可以看一下文件系统几个关键的结构体之间的关系。显然,一个inode对应多个dentry,一个dentry对应多个file。任何一本介绍文件系统的书对此都有介绍。那么inode和块设备block_device,字符设备cdev的关系如何呢。我们知道,inode是对很多事物的抽象。在2.4内核中。inode结构体中有一个union,记录了几十种类型,包括各种文件系统的inode,各种设备(块设备,字符设备,socket设备等等)的inode,我们可以把block_device和cdev理解为特殊的节点。他们除了普通节点的一些特性外,还有自己的接口。
如block_device有个成员gendisk,有自己的接口block_device_operations。而对于ide硬盘来说,它是一种特殊的gendisk,它有自己的结构体ide_driver_t。 它还实现了block_device_operations接口,即idedisk_ops。
整体的继承关系:
block_device,gendisk(类)|block_device_operations(接口)| is_a(继承) 
ide_drive_t(类)|idedisk_ops(实现)
这种分析方法有助于理清内核中众多结构体之间的关系。与此类似,我们也可以分析一下两种重要的设备PCI和USB设备:
内核中用来表示pci设备的结构体是pci_dev,此外,还有一个接口pci_driver,定义了一组PCI设备的操作。
内核中用来表示usb设备的结构体是usb_device,此外,还有一个接口usb_driver,定义了一组USB设备的操作。
有趣的是,pci_dev和usb_device有一个共同的成员变量类型为
device,pci_driver和usb_driver有一个共同的成员变量类型为device_driver。
由此,我们可以猜出他们的关系。
pci_dev和usb_device可以看成对device的继承,pci_driver和usb_driver可以看作是从device_driver接口的继承。
在设备驱动中,各种层次显得非常分明。内核通过对usb和pci驱动通用框架的设计,减轻了驱动的开发人员的负担。实际上,内核为驱动开发人员提供的是一个框架(framework)。有些类似于MFC。开发人员只要实现一些接口就可以了。
此外,我们还可以总结出一些有趣的现象:比如,接口该如何定义呢?
Linux把目录和设备看成文件,这使文件操作接口的定义有两种选择:
1.取普通文件,目录文件,设备文件接口的交集,为,把各种“文件”特有的接口放到各种“文件”自定义自己的接口。这样的好处是继承关系比较清楚。不过继承层次比较深。
2. 取普通文件,目录文件,设备文件接口的并集,压缩继承层次。各种“文件”实现自己能实现的接口,把不能实现的函数指针置为NULL
事实上,file_operations就是这么做的。它里面有通用的操作(read write)。有针对目录文件的操作(getdents)。有针对设备的操作(ioctl)。与之类似的还有
inode_operations,包括了普通文件节点(create),设备文件节点(mknod),目录文件节点(mkdir)等等。
通过这样的设计,大大简化了整个层次结构。我们还可以归纳出更多东西:
1. 为了保证扩展性,很多结构体提供一个private指针,如file::private_data2. 如果只是为了代码重用,就提供普通库函数和普通结构体。如果为了提供接口以便继承,就提供接口。在C中,接口和普通成员函数很容易区分,前者一般定义成函数指针。

你可能感兴趣的:(Linux kernel 分析之十七:设计模式-用C来实现继承和模板)