标签 linux内核 下的文章

linux内核中的container_of函数

一、container_of的用途

众所周知,linux内核是用C写的,并且内核中是存在许多数据结构的,栈、链表、哈希表以及红黑树等等。但是C语言中一个致命的缺点就是没有泛型,没有泛型的话所有的数据结构就无法通过一套代码来实现。那有没有办法可以使得这些数据结构成为通用的呢?答案肯定是有的,不然如果每个结构体都要实现一套自己的链表,内核将会变得臃肿、复杂且不好维护。要知道C语言虽然没有泛型,但是它有指针,实现内核的大佬们就通过指针来实现了属于C的泛型

以链表为例,首先定义一个全局通用的链表节点:

struct list_node {
    struct list_node *prev, *next;
}

节点中只包含了prevnext两个成员,不含有任何数据内容。所有的数据内容都要另外再定义结构体,把这个链表节点包含进来,再通过这个链表节点实现链表移动。例如实现一个lru缓存节点的链表:

struct lru_cache {
    int page_addr;
    struct list_node ln;
}
数据节点说的是lru_cache结构类型的节点,链表节点是lru_cache中ln成员节点。

它通过ln元素串起来的数据形式为:

图片看起来很好理解,关键的问题就在于如何通过这个节点来组成链表,以及如何通过这个链表节点找到本身的数据节点呢?这便是container_of发挥作用的时候了。

container_of的作用就是给定结构体类型和数据成员名返回结构体本身的地址,它需要三个参数:

  1. 数据节点指针,表示当前某个数据节点中的链表节点地址,即某个lru_cache节点中ln的地址。
  2. 当前数据节点的数据类型,即struct lru_cache这个结构体。
  3. 链表节点在数据节点中的名字,即ln

假设ptr是某个节点中ln元素的地址,那么通过container_of(ptr, struct lru_cache, ln)就能得到这个节点的地址了。通过它来完成一次节点遍历的过程可以描述为:

struct list_node *p = head;
struct lru_cache *lru_node;
while (p) {
    lur_node = container_of(ptr, struct lru_cache, ln);
    // 处理节点
    // print(lru_node);
    // ...
    
    p = p->next == head ? NULL : p->next;
}

二、container_of的实现

container_of的宏定义:

#define container_of(ptr, type, member) ({          \  
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \  
    (type *)( (char *)__mptr - offsetof(type,member) );})

宏定义有三个变量,展开后一共有两行语句:

  1. const typeof( ((type *)0)->member ) *__mptr = (ptr);
  2. (type *)( (char *)__mptr - offsetof(type,member) );}

这两行语句的解析:

先通过传入的type生成一个该类型的指针,(type *)0表示指向NULL的type类型的指针,假设这个指针为p,语句就变成了:

const typeof( p->member ) *__mptr = (ptr);

然后定义一个链表节点类型的指针__mptr指向ptr,因为不知道ptr的数据类型,所以要通过typeof (p->member)得到数据类型。此时__mptr的指向是:

因此,到这里想要得到lru_cache的地址,只要把__mptr的地址减去ln成员在结构体中的偏移就行了。

这也正是第二个语句的作用:先通过offset_of获取到偏移,再通过(char *)强制转换__mptr的数据类型,使得它的步长是1。最后减去偏移就得到数据节点的地址。

一、问题

使用percpu变量时编译报错:

### make  in drv
make -C /usr/src/linux-wm M=/Packet/ac/module/saas_ctl/drv modules
  CC [M]  /Packet/ac/module/saas_ctl/drv/saas_action.o
  CC [M]  /Packet/ac/module/saas_ctl/drv/saas_main.o
/Packet/ac/module/saas_ctl/drv/saas_main.c: In function 'match_saas_rules':
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: 'per_cpu__g_ssl_cpu_data' undeclared (first use in this function)
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: (Each undeclared identifier is reported only once
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: for each function it appears in.)
cc1: warnings being treated as errors
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: type defaults to 'int' in declaration of 'type name'
/Packet/ac/module/saas_ctl/drv/saas_main.c:395: error: invalid type argument of 'unary *' (have 'int')
make[3]: *** [/Packet/ac/module/saas_ctl/drv/saas_main.o] Error 1
make[2]: *** [_module_/Packet/ac/module/saas_ctl/drv] Error 2
make[1]: *** [all] Error 2
make: *** [drv] Error 2

原因:

percpu变量在其他模块定义的,当前模块使用前要声明:

DECLARE_PER_CPU(type, name);

一、linux内核模块

Linux模块是一些可以作为独立程序来编译的函数和数据类型的集合。之所以提供模块机制,是因为Linux本身是一个单内核。单内核由于所有内容都集成在一起,效率很高,但可扩展性和可维护性相对较差,模块机制可弥补这一缺陷。

Linux模块可以通过静态或动态的方法加载到内核空间,静态加载是指在内核启动过程中加载;动态加载是指在内核运行的过程中随时加载。

一个模块被加载到内核中时,就成为内核代码的一部分。模块加载入系统时,系统修改内核中的符号表,将新加载的模块提供的资源和符号添加到内核符号表中,以便模块间的通信。

内核提供了接口函数来注册我们自己的模块:

// linux/init.h
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 * 
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)    __initcall(x);


/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 * 
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
#define module_exit(x)    __exitcall(x);

module_initmodule_exit分别用于加载和卸载模块,其中的x的函数声明为:

int f(void);

二、编译一个自己的模块

2.1 编写一个模块

首先下载内核源码:yum groupinstall "Development Tools",、安装好后源码在/usr/src/kernels/2.6.32-754.9.1.el6.x86_64/注意最后面的一串内核版本,可能和uname -r不一致

然后准备测试代码test.c

#include <linux/module.h>
#include <linux/init.h>

static int test_init(void) {
    printk("===Test Module Start!===\n");
    return 0;
}

static int test_exit(void) {
    printk("===Test Module Has Been Closed!===");
    return 0;
}

module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("MaQian");

其中linux/module.hlinux/init.h是必须包含的两个问头文件,MODULE_LICENSE是模块许可证声明,一般设置为GPL,可选。MODULE_AUTHOR是模块作者,可选。

2.2 Makefile编写

# test是模块的名字
obj-m := test.o
# 内核代码的位置
KERNEL := /usr/src/kernels/2.6.32-754.9.1.el6.x86_64/
# 当前模块的目录
PWD := $(shell pwd)

modules:
    $(MAKE) -C $(KERNEL) M=$(PWD) modules

.PHONY: clean
clean:
    rm -rf *.o *.ko

如果不出意外,执行make即可生成我们要的模块文件test.ko,但是一般情况下,第一次编译是会失败的,会报错,错误的解决方案在最下面。

2.3 挂载模块到系统

挂载驱动使用insmod,卸载使用rmmod,先开启另外一个窗口开始打印调试信息:

> while :; do dmesg -c; sleep 1; done

插入模块到系统:

> insmod test.ko

输出:

===Test Module Start!===

卸载:

rmmod test.ko

输出:

===Test Module Has Been Closed!===

三、错误处理

3.1 Kernel configuration is invalid

make -C /usr/src/kernels/2.6.32-754.6.3.el6.x86_64/ M=/home/maqian/code/hello_netfilter modules
make[1]: Entering directory `/usr/src/linux-2.6.32'

  ERROR: Kernel configuration is invalid.
         include/linux/autoconf.h or include/config/auto.conf are missing.
         Run 'make oldconfig && make prepare' on kernel src to fix it.


  WARNING: Symbol version dump /usr/src/linux-2.6.32/Module.symvers
           is missing; modules will have no dependencies and modversions.

  Building modules, stage 2.
/usr/src/linux-2.6.32/scripts/Makefile.modpost:42: include/config/auto.conf: No such file or directory
make[2]: *** No rule to make target `include/config/auto.conf'.  Stop.
make[1]: *** [modules] Error 2
make[1]: Leaving directory `/usr/src/linux-2.6.32'
make: *** [modules] Error 2

进入到内核代码目录,执行sudo make oldconfig && sudo make prepare,一路回车确认。

3.2 Symbol version dump xxx is missing

make -C /usr/src/kernels/2.6.32-754.6.3.el6.x86_64/ M=/home/maqian/code/hello_netfilter modules
make[1]: Entering directory `/usr/src/linux-2.6.32'

  WARNING: Symbol version dump /usr/src/linux-2.6.32/Module.symvers
           is missing; modules will have no dependencies and modversions.

  Building modules, stage 2.
  MODPOST 0 modules
/bin/sh: scripts/mod/modpost: No such file or directory
make[2]: *** [__modpost] Error 127
make[1]: *** [modules] Error 2
make[1]: Leaving directory `/usr/src/linux-2.6.32'
make: *** [modules] Error 2

还是在刚刚的目录执行:sudo make scripts

当一个模块编译完成之后,使用insmod加载,使用rmmod加载,lsmod可以查看已经挂载的模块,modinfo查看一个模块的详细信息。

3.3 insmod: error inserting 'test.ko': -1 Invalid module format

插入驱动报错:

insmod: error inserting 'test.ko': -1 Invalid module format

dmesg错误:

test: no symbol version for module_layout

检查驱动内核版本是否一致,最开始出现这个问题是因为内核代码是自己手动下载的,后面改成yum下载就好了。