C到C++,它们的关系演变过程是怎样的。从Linux的内核代码里面你可以了解到更深的编程层次的面向对象,而不是简单的封装、继承、多态。首先这个题目有点大,而且过于深,而我能了解到的也只是冰山一角,不过我觉得能去做这样的一种有意义的工作,对于提升自己来说,也是很有帮助。
主要分以下几部分:
引言。
C语言中的封装、继承与多态。
从Linux体系设备驱动模型(defs)来看面向对象。
类比插件系统来看内核模块模型中层叠技术的可扩展性。
虚拟函数表与Linux内核符号导出表的面向对象特性优劣分析。
总结
附录
其中,每一部分都会穿插部分语言代码,让整个描述过程更加详实、不空洞。
1 引言:
Linux操作系统架构在合适的硬件之上,诸如 cpu、外存、内存、PCI等设备之上。在Linux这个国家里,任何的硬件设备都作为一个资源,而操作系统就像一个国家机器对他的资源进行管理,国家的公民是运行与操作系统内部的进程,当然这个国家也有公务员(内核线程:注,在内核中没有进程只有线程。关于什么是线程什么是进程,很多书上其实都描述的不在要点。详见附录问题1)。内核线程把持着所有的硬件资源,进程使用需要发起80软中断(INT 0x80)请求内核响应。资源主要分为:内存/虚存、文件(设备)两大类。
2 C语言中的封装、继承与多态:
封装定义是在程序上,隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别;将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。
假设我们有类A, 其内含有a、b、c三个成员变量,并有操作方法 op_a(a)、op_b(b)、op_c(c)、op_ab(a, b)等。在面向对象语言中及其容易实现,在C语言中面临的最大问题是操作方法及其参数传递。
通过函数表可以弥补语言机制上的缺陷:
struct Ops {
void (*op_a) (struct A *, loff_t, int);
void (*op_b) (struct A *, char __user *, size_t, loff_t *);
void (*op_ab) (struct A *, const char __user *, size_t, loff_t *);
…
};
首先这是一个只含有方法的结构体,我们称之为函数表,传递的结构体参数A为c++中默认编译器为类传递的this对象,这样,我们将结构体ops内嵌到结构体A中,如下:
Struct A{
Int a;
Int b;
Int c;
Struct Ops ops;
}
将方法封装到了结构体中。在这里结构体A相当于一个父类,而ops是作为父类公开给子类的接口。抽象出了接口,多态就很容易实现了。从这里模拟封装的过程,可以很容易发现接口的实质是父类提前为子类预留的占位符,是一种“高瞻远瞩”的行为。
最早C++里面的封装是从C中的头文件规则衍生出来的。其关键在于对于一个公共数据操作方法的归类。如果一个公共变量,被很多个C 函数修改,如下图:
后期调试及维护时,带来极大的不便,因为根本不知道系统中到底有多少函数操作的这块数据,这块数据的状态什么时候,被哪个函数改变了状态。所以就演变成了下面的方式:
将操作此数据的函数放到同一个文件中去,任何其它改变或访问此数据的函数都需要通过这个文件中的函数来访问。
所以面向对象中的类,其实是C工程中总结出来的一系列管理方法在语言层面上的实现,并没有太多的新东西。
多态:是允许将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
由多态的定义,最关键的地方在于运行时切换即在编译期不能确定需要执行的代码。这里我们声明两个子操作表 ops1、ops2,当需要使用ops2时,将 A->ops = ops2,即将子类实例化到父类,下面类A的具体操作就会采用ops2操作表中的方法。如图:
代码示例:
Struct A a;
a-> ops = ops1;
a->ops->op_a(a);
//切换
a->ops = ops2;
a->ops->op_a(a);
继承:特殊类(或子类、派生类)的对象拥有其一般类(或称父类、基类)的全部属性与服务,称作特殊类对一般类的继承。
继承的本质是保留父类的一些成员和函数,实质上是保留父类的存储区及跳转函数表,继承在C语言中也是采用内嵌结构体的方式来实现的。这里举内核中的链表结构体来说明,
我们通常所使用链表结构如下:
Struct A_LIST{
Int a;
Int b;
Struct A_LIST* next;
}
在这里我们发现,每一个特定类型的链表机构都需要为其增加一个next或者prev字段来保持链表的结构。还需要为每一个特定的链表类实现脱链、入链操作等。我们需要抽象出一个“基类”来实现链表的功能,其他链表类只需要简单的继承这个链表类就可以了。
Linux 内核的链表结构:
struct hst_head{
struct list_head *next, prey;
);
在这个结构体中,链表及作为一个单独的结构体,不再依附与任何对象。
同上面的例子,我们只需要声明
Struct A_LIST{
Int a;
Int b;
Struct hst_head T* list;
}
这样A_LIST即可作为一个链表对象,其链表操作均由其成员list代为实现,起到了继承基类的可复用方法的作用。实际过程如下图:
其作为一个连接件,只对本身结构体负责,而不需要关注真正归属的结构体。正如继承的特性,父类的方法无法操作也不需要操作子类的成员。(关于从连接件获取宿主结构体方法见附录1.问题2)
至此,C在面向对象的三大特性的实现过程就结束了。
3 从Linux体系设备驱动模型(defs)来看面向对象:
Sysfs:用于将系统中的设备组织成层次结构,并向用户模式程序提供详细的内核数据结构。
Sysfs是一个特殊的文件系统,没有实际存放文件的介质,sysfs信息来源于kobject层次结构,读取sys就是动态的从kobject结构链中提取信息。
两个基础的数据结构kobject和kset
Kobject <linux/kobject.h>
struct kobject
{
const char * k_name; //指向kobject名称的起始位置
char name[KOBJ_NAME_LEN];//不足则重新分配(20)
struct kref kref; /*引用计数*/
struct list_head entry; /*在所挂到链表的连接体*/
struct kobject * parent; //指向kobject的父对象
struct kset * kset; /*指向所属的kset*/
struct kobj_type * ktype; //指向其对象类型描述符的指针
struct dentry * dentry;
//sysfs文件系统中与该对象对应的文件节点目录项
};
引用计数的增加和减小;(操作kref)
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
如果引用计数降为0,调用kobject release() 释放此结构
相关初始化工作
void kobject_init(struct kobject * kobj); kobject初始化函数。
Int kobject_set_name(struct kobject *kobj, const char *format, ...);kobject 名称
int kobject_add(struct kobject * kobj);加入linux设备层次
void kobject_del(struct kobject * kobj);从层次中删除此对象
挂接该kobject对象到kset的list链
增加父目录各级kobject的引用计数
在其parent指向的目录下创建文件节点
注册与注销:
int kobject_register(struct kobject * kobj);
kobject init()
kobject_add()
void kobject_unregister(struct kobject * kobj);
kobject_del()
kobject_put() 如果引用计数降为0,调用kobject release() 释放此结构
kobj_type的目标就是为不同类型的kobject提供不同的属性以及销毁方法。
Kobj type struct kobj_type {
void (*release)(struct kobject *); //kobject销毁时调用的函数kobject_put()调用
struct sysfs_ops * sysfs_ops; //对属性的操作表
struct attribute ** default_attrs; //属性列表
};
操作表:
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *,char *);
ssize_t (*store)(struct kobject *,struct attribute *,const char *, size_t);
};
struct kset {
struct subsystem * subsys; 所在的subsystem的指针
struct kobj type * ktype; 指向该kset对象类型描述符的指针
struct list head list; 用于连接该kset中所有kobject的链表头
struct kobject kobj; 嵌入的kobject
struct kset hotplug ops * hotplug ops; 指向热插拔操作表的指针
};
List为kobject的双向链表头,ktype为对象类型供所有kobject共享。Kobj为所有kobject指向的parent域。Kset的引用计数依赖其成员kobj的引用计数
根据以上数据结构总结如下:
一共存在三条链:
1、子kobj双向链表(有list做链头)
2、 父子链(kobj)
3、 归属链 (kset)
在面向对象中的正反向引用(1对1、1对多)在C中的实现过程如上:
当处理一对多的关系时,“一”需要维护一个“多”的链表,而“多”需要维护一个“一”的指针。
C到C++,它们的关系演变过程是怎样的。从Linux的内核代码里面你可以了解到更深的编程层次的面向对象,而不是简单的封装、继承、多态。首先这个题目有点大,而且过于深,而我能了解到的也只是冰山一角,不过我觉得能去做这样的一种有意义的工作,对于提升自己来说,也是很有帮助。
主要分以下几部分:
引言。
C语言中的封装、继承与多态。
从Linux体系设备驱动模型(defs)来看面向对象。
类比插件系统来看内核模块模型中层叠技术的可扩展性。
虚拟函数表与Linux内核符号导出表的面向对象特性优劣分析。
总结
附录
其中,每一部分都会穿插部分语言代码,让整个描述过程更加详实、不空洞。
1 引言:
Linux操作系统架构在合适的硬件之上,诸如 cpu、外存、内存、PCI等设备之上。在Linux这个国家里,任何的硬件设备都作为一个资源,而操作系统就像一个国家机器对他的资源进行管理,国家的公民是运行与操作系统内部的进程,当然这个国家也有公务员(内核线程:注,在内核中没有进程只有线程。关于什么是线程什么是进程,很多书上其实都描述的不在要点。详见附录问题1)。内核线程把持着所有的硬件资源,进程使用需要发起80软中断(INT 0x80)请求内核响应。资源主要分为:内存/虚存、文件(设备)两大类。
2 C语言中的封装、继承与多态:
封装定义是在程序上,隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别;将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。
假设我们有类A, 其内含有a、b、c三个成员变量,并有操作方法 op_a(a)、op_b(b)、op_c(c)、op_ab(a, b)等。在面向对象语言中及其容易实现,在C语言中面临的最大问题是操作方法及其参数传递。
通过函数表可以弥补语言机制上的缺陷:
struct Ops {
void (*op_a) (struct A *, loff_t, int);
void (*op_b) (struct A *, char __user *, size_t, loff_t *);
void (*op_ab) (struct A *, const char __user *, size_t, loff_t *);
…
};
首先这是一个只含有方法的结构体,我们称之为函数表,传递的结构体参数A为c++中默认编译器为类传递的this对象,这样,我们将结构体ops内嵌到结构体A中,如下:
Struct A{
Int a;
Int b;
Int c;
Struct Ops ops;
}
将方法封装到了结构体中。在这里结构体A相当于一个父类,而ops是作为父类公开给子类的接口。抽象出了接口,多态就很容易实现了。从这里模拟封装的过程,可以很容易发现接口的实质是父类提前为子类预留的占位符,是一种“高瞻远瞩”的行为。
最早C++里面的封装是从C中的头文件规则衍生出来的。其关键在于对于一个公共数据操作方法的归类。如果一个公共变量,被很多个C 函数修改,如下图:
后期调试及维护时,带来极大的不便,因为根本不知道系统中到底有多少函数操作的这块数据,这块数据的状态什么时候,被哪个函数改变了状态。所以就演变成了下面的方式:
将操作此数据的函数放到同一个文件中去,任何其它改变或访问此数据的函数都需要通过这个文件中的函数来访问。
所以面向对象中的类,其实是C工程中总结出来的一系列管理方法在语言层面上的实现,并没有太多的新东西。
多态:是允许将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
由多态的定义,最关键的地方在于运行时切换即在编译期不能确定需要执行的代码。这里我们声明两个子操作表 ops1、ops2,当需要使用ops2时,将 A->ops = ops2,即将子类实例化到父类,下面类A的具体操作就会采用ops2操作表中的方法。如图:
代码示例:
Struct A a;
a-> ops = ops1;
a->ops->op_a(a);
//切换
a->ops = ops2;
a->ops->op_a(a);
继承:特殊类(或子类、派生类)的对象拥有其一般类(或称父类、基类)的全部属性与服务,称作特殊类对一般类的继承。
继承的本质是保留父类的一些成员和函数,实质上是保留父类的存储区及跳转函数表,继承在C语言中也是采用内嵌结构体的方式来实现的。这里举内核中的链表结构体来说明,
我们通常所使用链表结构如下:
Struct A_LIST{
Int a;
Int b;
Struct A_LIST* next;
}
在这里我们发现,每一个特定类型的链表机构都需要为其增加一个next或者prev字段来保持链表的结构。还需要为每一个特定的链表类实现脱链、入链操作等。我们需要抽象出一个“基类”来实现链表的功能,其他链表类只需要简单的继承这个链表类就可以了。
Linux 内核的链表结构:
struct hst_head{
struct list_head *next, prey;
);
在这个结构体中,链表及作为一个单独的结构体,不再依附与任何对象。
同上面的例子,我们只需要声明
Struct A_LIST{
Int a;
Int b;
Struct hst_head T* list;
}
这样A_LIST即可作为一个链表对象,其链表操作均由其成员list代为实现,起到了继承基类的可复用方法的作用。实际过程如下图:
其作为一个连接件,只对本身结构体负责,而不需要关注真正归属的结构体。正如继承的特性,父类的方法无法操作也不需要操作子类的成员。(关于从连接件获取宿主结构体方法见附录1.问题2)
至此,C在面向对象的三大特性的实现过程就结束了。
3 从Linux体系设备驱动模型(defs)来看面向对象:
Sysfs:用于将系统中的设备组织成层次结构,并向用户模式程序提供详细的内核数据结构。
Sysfs是一个特殊的文件系统,没有实际存放文件的介质,sysfs信息来源于kobject层次结构,读取sys就是动态的从kobject结构链中提取信息。
两个基础的数据结构kobject和kset
Kobject <linux/kobject.h>
struct kobject
{
const char * k_name; //指向kobject名称的起始位置
char name[KOBJ_NAME_LEN];//不足则重新分配(20)
struct kref kref; /*引用计数*/
struct list_head entry; /*在所挂到链表的连接体*/
struct kobject * parent; //指向kobject的父对象
struct kset * kset; /*指向所属的kset*/
struct kobj_type * ktype; //指向其对象类型描述符的指针
struct dentry * dentry;
//sysfs文件系统中与该对象对应的文件节点目录项
};
引用计数的增加和减小;(操作kref)
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
如果引用计数降为0,调用kobject release() 释放此结构
相关初始化工作
void kobject_init(struct kobject * kobj); kobject初始化函数。
Int kobject_set_name(struct kobject *kobj, const char *format, ...);kobject 名称
int kobject_add(struct kobject * kobj);加入linux设备层次
void kobject_del(struct kobject * kobj);从层次中删除此对象
挂接该kobject对象到kset的list链
增加父目录各级kobject的引用计数
在其parent指向的目录下创建文件节点
注册与注销:
int kobject_register(struct kobject * kobj);
kobject init()
kobject_add()
void kobject_unregister(struct kobject * kobj);
kobject_del()
kobject_put() 如果引用计数降为0,调用kobject release() 释放此结构
kobj_type的目标就是为不同类型的kobject提供不同的属性以及销毁方法。
Kobj type struct kobj_type {
void (*release)(struct kobject *); //kobject销毁时调用的函数kobject_put()调用
struct sysfs_ops * sysfs_ops; //对属性的操作表
struct attribute ** default_attrs; //属性列表
};
操作表:
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *,char *);
ssize_t (*store)(struct kobject *,struct attribute *,const char *, size_t);
};
struct kset {
struct subsystem * subsys; 所在的subsystem的指针
struct kobj type * ktype; 指向该kset对象类型描述符的指针
struct list head list; 用于连接该kset中所有kobject的链表头
struct kobject kobj; 嵌入的kobject
struct kset hotplug ops * hotplug ops; 指向热插拔操作表的指针
};
List为kobject的双向链表头,ktype为对象类型供所有kobject共享。Kobj为所有kobject指向的parent域。Kset的引用计数依赖其成员kobj的引用计数
根据以上数据结构总结如下:
一共存在三条链:
1、子kobj双向链表(有list做链头)
2、 父子链(kobj)
3、 归属链 (kset)
在面向对象中的正反向引用(1对1、1对多)在C中的实现过程如上:
当处理一对多的关系时,“一”需要维护一个“多”的链表,而“多”需要维护一个“一”的指针。
7 附录
附录1
问题1:进程与线程,32位PC机上可以寻址4GB的虚拟内存空间,在这4GB的内存空间中,各个进程本身占用较低位的3GB虚拟内存并与系统中所有进程及内核线程共享高地址的1GB虚拟内存。要区分是进程和线程的关键在此进/线程是否具有独立的3GB用户空间,如果有则是进程,否则为线程。
问题2:
关于链表结构的宿主指针获取方法,
获取结构类型TYPE里的 成员MEMBER 在结构体内的偏移
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
通过指向成员member的指针ptr获取该成员结构体type的指针
#define container_of(ptr, type, member) ({
const type(((type*) 0)->member)*__mptr = (ptr);
(type*)((char*)__mptr – offsetof(type, member))
})