最近,我在看linux的内核源码,其中有好多非常优秀的代码写法,今天要描述的就是"函数转数据"这种用法。
"函数转数据"是我自己起的名称,可能不太准确,具体含义是:在一个源文件(模块)内部定义了好多static修饰的静态函数,一般用法源文件中另外一个函数去直接调用这个函数,但是"函数转数据"的用法是这样的:用到了函数指针变量,例如定义了一个结构体类型,结构体成员包含各种函数指针类型的变量,然后另一一个结构体类型的全局变量,全局变量初始化的时候被赋绑定的函数名,最后通过访问这个全局变量里面的成员,来间接实现对函数的访问。
例如,如下操作:
在input.h中定义了一个结构体类型:
cpp
struct input_handler {
void *private;
void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
bool (*match)(struct input_handler *handler, struct input_dev *dev);
int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
void (*disconnect)(struct input_handle *handle);
void (*start)(struct input_handle *handle);
const struct file_operations *fops;
int minor;
const char *name;
const struct input_device_id *id_table;
struct list_head h_list;
struct list_head node;
};
这里有点面向对象思想的写法,结构体类型内部除了需要的数据之外,还包含了方法,方法就是函数指针类型+函数指针名,组成的结构体成员。
第二步:在sysrq.c中定义了一个静态全局变量:类型为struct input_handler类型,变量名为sysrq_handler,变量初始化中变量被绑定了各种sysrq.c中的各个静态函数。
cpp
static struct input_handler sysrq_handler = {
.filter = sysrq_filter,
.connect = sysrq_connect,
.disconnect = sysrq_disconnect,
.name = "sysrq",
.id_table = sysrq_ids,
};
cpp
static bool sysrq_filter(struct input_handle *handle, unsigned int type,
unsigned int code, int value)
{
if (type != EV_KEY)
goto out;
switch (code) {
case KEY_LEFTALT:
case KEY_RIGHTALT:
if (value)
sysrq_alt = code;
else {
if (sysrq_down && code == sysrq_alt_use)
sysrq_down = false;
sysrq_alt = 0;
}
break;
case KEY_SYSRQ:
if (value == 1 && sysrq_alt) {
sysrq_down = true;
sysrq_alt_use = sysrq_alt;
}
break;
default:
if (sysrq_down && value && value != 2)
__handle_sysrq(sysrq_xlate[code], NULL, 1);
break;
}
out:
return sysrq_down;
}
最后,在sysrq.c源文件中直接访问这个全局变量sysrq_handler,如下所示:
cpp
static inline void sysrq_register_handler(void)
{
int error;
error = input_register_handler(&sysrq_handler);
if (error)
pr_err("Failed to register input handler, error %d", error);
else
sysrq_handler_registered = true;
}
函数input_register_handler()调用了全局变量sysrq_handler,间接实现了对绑定函数的访问。
整个过程有点像一个大项目的分工工作,各干各的,各自负责各自工工作,最后再全局变量内部成员进行绑定后就整个线路就通了。
举个例子:有点像城市电网,小区负责小区电网网络建设,这个网络现在是未通电的,市政的国家电网负责城市主要干线的铺设,最终通过小区变压器之类节点实现小区电网连接市电工作。
真正静态函数定义相当于小区网络建设,结构体变量定义相当于"电网节点联网",结构体变量访问相当于"国家电网电网铺设"。
整个过程的好处就是各个各自的,互不干涉,另外就是实现了"隔离",互补干涉。一方改动不会影响第二方。
最后,我们追踪一下结构体变量成员再哪里是真正访问的。技巧就是"追踪"结构体类型定义时的成员变量。
cpp
int input_register_handler(struct input_handler *handler) // 输入注册handler
{
struct input_dev *dev; // 输入设备
int retval; // 返回值
retval = mutex_lock_interruptible(&input_mutex); // 上锁
if (retval) // 判断reval的值,如果不等于0,表示上锁失败了
return retval; // 直接返回
INIT_LIST_HEAD(&handler->h_list); // 初始化handler的h_list链表
if (handler->fops != NULL) { // 校验handler指向的fops是否为空,如果不为空
if (input_table[handler->minor >> 5]) { // struct input_handler * input_table[8] 数量为8表示最多8个input_handler
retval = -EBUSY; // 次设备号之所以要除以32(右移5bit),也就是说每一个input_handler下最多挂接32的设备,ls -l /dev/input
goto out;
}
input_table[handler->minor >> 5] = handler; // 真正干活的,完成注册
}
list_add_tail(&handler->node, &input_handler_list); // 将handler->node添加到input_handler_list中
list_for_each_entry(dev, &input_dev_list, node) // 遍历input_handler_list链表
input_attach_handler(dev, handler); // 将dev和handler进行挂接
input_wakeup_procfs_readers(); // input唤醒proc文件系统的readers
out:
mutex_unlock(&input_mutex);
return retval;
}
进入函数input_register_handler()追踪参数:handler的使用情况:发现这个指针变量使用的几个地方没有用到"方法",最有可能的就是子函数input_attach_handler()的第二个参数中有用的"方法",我们再次追踪input_attach_handler()函数。

其中handler->connect就是结构体成员实现指针变量传参+解引用,真正调用的起始就是最初全局变量内部成员绑定的函数。当然了,input_mach_device()函数中也使用了handler指向的函数指针调用。
启迪:
(1)微观:发明函数本身暗含的重要作用:实现隔离,也就说,只要返回值、函数名、参数列表不变,修改函数内部内容不用修改调用函数。这样调用者和被调用者就形成了隔离。架构如下所示:调用函数---》子函数名(参数列表)---》子函数内容。
(2)宏观:从以上面向对象编程思想考虑,也是存在隔离思想的,真正的函数---》注册/绑定函数指针变量---》调用函数指针变量。无论是修改真正的函数,还是修改调用函数指针,两者任意一方修改都不会影响第二方。
(3)面向对象编程,有助于大项目协同工作,各干各的,互补影响,在一个节点实现双方绑定。这样编写代码好处多多,就不再论述了,但是也带来了一个因为隔离带来的不容易把控整个流程的弊端:需要一定抽象思维来捋清楚整个过程,很难从一方跳转到绑定节点,再跳转到另外一方来理解清楚。