在LINUX中内核中有很多C语言的奇技淫巧,比如container_of()这个宏, 在很多地方会用到,比如在定义宏to_i2c_driver, to_spi_driver时都用到这个,有些就直接在函数中调用 container_of宏。
1. 作用
这个宏的作用就是通过一个结构体的某个成员的指针,反推出这个结构体的首地址。
这种操作非常有用,尤其在C语言中写面向对象的程序时,相当模拟了C++多态的效果。
以LINUX内核中I2C子系统为例, i2c驱动结构体如下:
[C] 纯文本查看 复制代码 struct i2c_driver {
unsigned int class;
/* Standard driver model interfaces */
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
void (*remove)(struct i2c_client *client);
/* New driver model interface to aid the seamless removal of the
* current probe()'s, more commonly unused than used second parameter.
*/
int (*probe_new)(struct i2c_client *client);
/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *client);
/* Alert callback, for example for the SMBus alert protocol.
* The format and meaning of the data value depends on the protocol.
* For the SMBus alert protocol, there is a single bit of data passed
* as the alert response's low bit ("event flag").
* For the SMBus Host Notify protocol, the data corresponds to the
* 16-bit payload data reported by the slave device acting as master.
*/
void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol,
unsigned int data);
/* a ioctl like command that can be used to perform specific functions
* with the device.
*/
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
[color=#ff0000] struct device_driver driver;[/color]
const struct i2c_device_id *id_table;
/* Device detection callback for automatic device creation */
int (*detect)(struct i2c_client *client, struct i2c_board_info *info);
const unsigned short *address_list;
struct list_head clients;
u32 flags;
};
在struct i2c_driver 中有一个成员struct device_driver driver, 这相当于“is-a”的关系,即 i2c_driver 是一种 device_driver. 这种操作实际就是继承。
紧接着定义了一个i2c专用宏:
[C] 纯文本查看 复制代码 #define to_i2c_driver(d) container_of(d, struct i2c_driver, driver)
在i2c_device_match函数中使用这个宏:
[C] 纯文本查看 复制代码 static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
/* Attempt an OF style match */
if (i2c_of_match_device(drv->of_match_table, client))
return 1;
/* Then ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
[color=#ff0000] driver = to_i2c_driver(drv);[/color]
/* Finally an I2C match */
if (i2c_match_id(driver->id_table, client))
return 1;
return 0;
}
在此函数中会调用driver = to_i2c_driver(drv); 相当于:driver = container_of(drv, struct i2c_driver, driver)
注意在传入函数的参数中有一个参数是struct device_driver 类型的引用( *drv ),通过这个宏就可以通过drv的地址
反推出drv所在结构体struct i2c_driver 对象driver 的首地址,从而可以通过driver去操作其他各个成员。即可达到通过基类指针寻址到派生类地址的效果。
2. 宏实现
来看看内核中是如何定义和实现这个container_of宏的。
[C] 纯文本查看 复制代码
/tools/include/linux/kernel.h
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
RT-Thread 中也有类似的操作:
[C] 纯文本查看 复制代码 /**
* rt_container_of - return the start address of struct type, while ptr is the
* member of struct type.
*/
#define rt_container_of(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
从注释上可以清楚的看出这个宏的作用就是:当ptr是结构体的一个成员时,可以反推出结构体的首地址。
先写个小程序做个实验,验证一下(&((type *)0)->member)到底是什么结果:
[C] 纯文本查看 复制代码
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
typedef struct {
char a;
char b;
char c;
char d;
}test_t;
void main(void)
{
printf("offsetof: %d\n",offsetof(test_t, a));
printf("offsetof: %d\n",offsetof(test_t, b));
printf("offsetof: %d\n",offsetof(test_t, c));
printf("offsetof: %d\n",offsetof(test_t, d));
}
运行:
当把结构体成员类型换一下,如下:
typedef struct {
int a;
int b;
int c;
int d;
}test_t;
运行:
现在我们可以知道(&((type *)0)->member) 这种操作,就是返回member在type这种类型结构体中的相对地址偏移,注意是相对偏移而不是成员在内存中的地址。
理解了以上内容后再看offsetof(),它的作用是获取结构体中某个成员相对于该结构体首元素地址的偏移量。
[C] 纯文本查看 复制代码 #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
test_t 对象内存布局:
从图中可以看出,只要获得了某个成员的地址,再减去这个成员在结构体中的偏移就到到顶了,即反推出了结构体的地址。
先看RTT内核中的宏定义,比较明了一些。
[C] 纯文本查看 复制代码 #define rt_container_of(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
(char *)(ptr)强转换以将移动步进控制为字节,ptr地址减掉这个偏移,就回到结构体首地址了,真TM神奇!
3.原理
在linux内核中的宏定义是这样的:
[C] 纯文本查看 复制代码 #define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
这里面还出现了一个typeof关键字.
那么,typeof(((type *)0)->member), (&((type *)0)->member) 到底是什么样的一种操作了?
原来,ANSI C标准允许值为0的常量被强制转换成任何一种类型的指针,并且转换的结果是个NULL,因此((type *)0)的结果就是一个类型为type *的NULL指针.
如果利用这个NULL指针来访问type的成员当然是非法的,但typeof( ((type *)0)->member )是想取该成员的类型,所以编译器不会生成访问type成员的代码,
类似的代码&( ((type *)0)->member )在最前面有个取地址符&,它的意图是想取member的相对于type类型首地址的偏移,所以编译器同样会优化为直接取地址偏移。
typeof()关键字
这个关键字有点像C++11中的decltype关键字,用来获取对象的类型
typeof()关键字常见用法一共有以下几种。
1.不用知道函数返回什么类型,可以使用typeof()定义一个用于接收该函数返回值的变量。
[C] 纯文本查看 复制代码 int func()
{
return 0;
}
typeof(func()) r1;//定义一个变量r1,用于接收函数func()返回的值,由于该函数返回的类型是:int,所以变量r1也是该类型。注意,函数不会执行。
r1 = func();
2. 在宏定义中动态获取相关结构体成员的类型
如下代码,定义一个和变量x相同类型的临时变量_max1,定义一个和变量y相同类型的临时变量_max2,再判断两者类型是否一致,不一致给出一个警告,最后比较两者。
看一段内核中的代码 :
[C] 纯文本查看 复制代码
#ifndef max
#define max(x, y) ({ \
typeof(x) _max1 = (x); \
typeof(y) _max2 = (y); \
(void) (&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; })
#endif
#ifndef min
#define min(x, y) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void) (&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2; })
#endif
再回头看看在linux内核中的container_of宏定义:
[C] 纯文本查看 复制代码 #define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
const typeof(((type *)0)->member) * __mptr 这句是不是也是定义一个临时变量 __mptr?
3. 也可直接取得已知类型
如下代码,定义了一个int类型的指针p,不过这纯属脱库子放P,像这种用法没什么太大的意义了。
[C] 纯文本查看 复制代码 int a = 2;
typeof (int *) p;
p = &a;
printf("%d\n", *p);
注意:
typeof构造中的类型名不能包含存储类说明符,如extern或static。不过允许包含类型限定符,如const或volatile。
例如,下列代码是无效的,因为它在typeof构造中声明了extern:
typeof(extern int) a;
强制转换
再回头想想,有没有另外一种办法了可以通过结构体成员的地址反推结构体地址的呢? 当然是有的: 把基类对象放在结构体的第一个位置即可。
以struct i2c_drive为例,如下:
[C] 纯文本查看 复制代码 struct i2c_driver {
struct device_driver driver;
unsigned int class;
....
u32 flags;
};
在函数中调用时,只要强制转换一下就行:
[C] 纯文本查看 复制代码
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
/* Attempt an OF style match */
if (i2c_of_match_device(drv->of_match_table, client))
return 1;
/* Then ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
//driver = to_i2c_driver(drv);
driver = (struct i2c_driver*)drv;
/* Finally an I2C match */
if (i2c_match_id(driver->id_table, client))
return 1;
return 0;
}
将 driver = to_i2c_driver(drv); 改为: driver = (struct i2c_driver*)drv; 即可。
因为在一个系统上指针大小都一样的,对指针的引用本质是对指针所指向的地址的有效作用域的引用,通过强制类型转换可以改变这种作用域的范围,
struct device_driver 对象处在struct i2c_driver结构体的第一个成员地址,因此struct device_driver 对象的地址即是其所在结构体的地址。
通过将struct device_driver 类型的指针强制转换为struct i2c_driver类型,也就重新定义了该指针的作用范围,即该指针的作用域从struct device_driver
扩大到了struct i2c_driver。这种方式更加简单,但也因此丧失了一定的灵活性。
//=============================END====================================================================
|