分类: LINUX
2015-02-28 22:26:37
七月 21, 2013 | 阅读次数(4,385) |
本地安装的是drbd-8.3.5版本,下载相应的源码包。两个子目录涉及源代码,其中drbd目录为内核态的源码,user目录为用户态工具的源码。所有的业务都是在内核态完成,用户态只是提供工具安装、配置、维护内核模块的工作。
drbd架构图,在官方网站的主页上面就能看到,非常显眼,这是内核态的架构示意图:
可以很清晰的看到,drbd在文件系统之下,直接操纵物理磁盘(块设备),在网络模型中,基于传输层之上建立虚拟设备。通过TCP/IP协议与远端设备交互。
drbd内核模块的名字为:
[root@Shentar /opt/drbd-8.3.5]# modprobe -all|grep drbd /lib/modules/2.6.25-14.fc9.i686/kernel/drivers/block/drbd.ko [root@Shentar /opt/drbd-8.3.5]#
首先找代码的入口,内核模块的初始化定义:module_init宏定义。在drbd_main.c文件中。
module_init(drbd_init) module_exit(drbd_cleanup)
下面贴出初始化函数:drbd_init(void):
整个初始化分如下几个步骤:
int __init drbd_init(void) { int err; if (sizeof(struct p_handshake) != 80) { printk(KERN_ERR "drbd: never change the size or layout " "of the HandShake packet.\n"); return -EINVAL; } if (1 > minor_count || minor_count > 255) { printk(KERN_ERR "drbd: invalid minor_count (%d)\n", minor_count); #ifdef MODULE return -EINVAL; #else minor_count = 8; #endif } err = drbd_nl_init(); if (err) return err; err = register_blkdev(DRBD_MAJOR, "drbd"); if (err) { printk(KERN_ERR "drbd: unable to register block device major %d\n", DRBD_MAJOR); return err; } register_reboot_notifier(&drbd_notifier); /* * allocate all necessary structs */ err = -ENOMEM; init_waitqueue_head(&drbd_pp_wait); drbd_proc = NULL; /* play safe for drbd_cleanup */ minor_table = kzalloc(sizeof(struct drbd_conf *) * minor_count, GFP_KERNEL); if (!minor_table) goto Enomem; err = drbd_create_mempools(); if (err) goto Enomem; drbd_proc = proc_create("drbd", S_IFREG | S_IRUGO, NULL, &drbd_proc_fops); if (!drbd_proc) { printk(KERN_ERR "drbd: unable to register proc file\n"); goto Enomem; } rwlock_init(&global_state_lock); printk(KERN_INFO "drbd: initialized. " "Version: " REL_VERSION " (api:%d/proto:%d-%d)\n", API_VERSION, PRO_VERSION_MIN, PRO_VERSION_MAX); printk(KERN_INFO "drbd: %s\n", drbd_buildtag()); printk(KERN_INFO "drbd: registered as block device major %d\n", DRBD_MAJOR); printk(KERN_INFO "drbd: minor_table @ 0x%p\n", minor_table); return 0; /* Success! */ Enomem : drbd_cleanup(); if (err == -ENOMEM) /* currently always the case */ printk(KERN_ERR "drbd: ran out of memory\n"); else printk(KERN_ERR "drbd: initialization failure\n"); return err; }
初始化网络,在内核2.6.16之前的版本中,还没有内核连接器的封装,还是直接调用原始的netlink套接字,因此初始化时需要有一系列的初始化netlink的动作。新的内核版本中集成了connector的封装,相对来说网络初始化的过程就简单多了。在分析代码时,需要注意,drbd目录下也有一个connector.c,这是为老版本准备的。新版本中根本没有编译该文件,因此只需要知道cn_add_callback这个接口的作用,而不需要去看connector.c中该函数的定义。昨天误分析到connector.c中去了,发现cn_add_callback函数最终结束时会往自己生成的任务队列中注册一个任务,但是怎么也找不到之后谁来等待执行该任务,试图加日志查看该函数流程时,才发现原来并没有运行到该代码。这才发现上述connector封装的问题。
关于连接器,这里简要说明一下,连接器封装了内核态和用户态的通讯过程。提供了简单的几个接口:
int cn_add_callback(struct cb_id*, char*, void (*callback) (void*)); void cn_del_callback(struct cb_id*); int cn_netlink_send(struct cn_msg*, u32, gfp_t); int cn_queue_add_callback(struct cn_queue_dev* dev, char* name, struct cb_id* id, void (*callback) (void*)); void cn_queue_del_callback(struct cn_queue_dev* dev, struct cb_id* id); struct cn_queue_dev* cn_queue_alloc_dev(char* name, struct sock*); void cn_queue_free_dev(struct cn_queue_dev* dev); int cn_cb_equal(struct cb_id*, struct cb_id*); void cn_queue_wrapper(void* data);
这里只分析cn_add_callback和cn_netlink_send两个,在内核创建连接器时,需要调用cn_add_callback方法,其中callback函数指针参数指定了连接器接收到数据时的回调函数,数据到达时将由该函数来处理。连接器既可以用于接收数据也可以用于发送数据。接收数据使用回调来处理。发送数据直接使用cn_netlink_send方法即可。该方法支持单播、广播和组播。一般单播和广播会用到。cn_add_callback方法无误返回,也就意味着网络初始化好了,比起之前版本中的复杂创建、绑定和监听套接字等流程简单多了。
这个方法是内核系统调用,用于注册一个块设备,需要指定主设备号,如果指定的设备号为0,则会由系统自动分配一个。该方法调用之后,就可以在/proc/devices文件中看到drbd块设备。drbd设备的设备号在代码中写死,为147。需要注意的是,如果块设备号已被占用,会导致注册失败。
见左图。
目前注册了一个空函数,委婉的说以后会实现,其实没有任何需要做的事情:
等待队列可以是生产者和消费者之间的共享队列,后面的业务分析中应该会遇到该结构,到时会仔细分析该队列中所存放的内容。应该与具体的读写数据业务有关。
这块还没有仔细分析,drbd中需要使用的内存池特别多,数据块的,接收缓冲区的,发送缓冲区的。
内核允许模块在/proc目录下创建自己的虚拟文件和虚拟文件夹。并且指定该文件的操作回调函数。其中,drbd指定了open, read, write和close方法的回调函数即/proc/drbd的四个回调函数:
struct file_operations drbd_proc_fops =
{
.owner = THIS_MODULE, .open = drbd_proc_open, .read = seq_read,
.llseek = seq_lseek, .release = single_release,
};
在打开该文件时,内核会调用drbd_proc_open函数来呈现相应的内容。也可以说这里是drbd内核服务向用户态反馈信息的一个通道。所有用户态的工具也是基于这个来判断当前drbd的状态。
整个内核模块的初始化到这里就结束了,总结一下,主要涉及下面几个系统调用:cn_add_callback,cn_netlink_send,register_blkdev,register_reboot_notifier,init_waitqueue_head,mempool_create,create_proc_entry。随着linux 内核的不断壮大,驱动编程似乎也在越来越简单。
内核链接器的说明:
连接器(Netlink Connector)及其应用 - IBM(转载)
一、引言
连接器是一种新的用户态与内核态的通信方式,它使用起来非常方便。本质上,连接器是一种netlink,它的 netlink 协议号为 NETLINK_CONNECTOR,与一般的 netlink 相比,它提供了更容易的使用接口,使用起来更方便。目前,最新稳定内核有两个连接器应用实例,一个是进程事件连接器,另一个是 CIFS 文件系统。连接器核心实现代码在内核源码树的driver/connector/connector.c 和 drivers/connector/cn_queue.c 文件中,文件 drivers/connector/cn_proc.c 是进程事件连接器的实现代码,而 CIFS 连接器的实现则在该文件系统的实现代码中。连接器是一个可选模块,用户可以在配置内核时在设备驱动(Device drivers)菜单中选择或不选它。
任何内核模块要想使用连接器,必须先注册一个标识 ID 和回调函数,当连接器收到 netlink 消息后,会根据消息对应的标识 ID 调用相应该 ID 的回调函数。
对用户态而言,连接器的使用跟普通的 netlink 没有差别,只要指定 netlink 协议类型为NETLINK_CONNECTOR 就可以了。
回页首
二、连接器相关数据结构和 API
下面是连接器的 API 以及相关的数据结构
:
struct cb_id
{
__u32 idx;
__u32 val;
};
struct cn_msg
{
struct cb_id id;
__u32 seq;
__u32 ack;
__u32 len; /* Length of the following data */
__u8 data[0];
};
int cn_add_callback(struct cb_id *id, char *name, void (*callback) (void *));
void cn_del_callback(struct cb_id *id);
void cn_netlink_send(struct cn_msg *msg, u32 __group, int gfp_mask);
结构 cb_id 是连接器实例的标识 ID,它用于确定 netlink 消息与回调函数的对应关系。当连接器接收到标识 ID 为 {idx,val} 的 netlink 消息时,注册的回调函数 void (*callback) (void *) 将被调用。该回调函数的参数为结构 struct cn_msg 的指针。
接口函数 cn_add_callback 用于向连接器注册新的连接器实例以及相应的回调函数,参数 id 指定注册的标识 ID,参数 name 指定连接器回调函数的符号名,参数 callback 为回调函数。
接口函数 cn_del_callback 用于卸载回调函数,参数 id 为注册函数 cn_add_callback 注册的连接器标识 ID。
接口函数 cn_netlink_send 用于向给定的组发送消息,它可以在任何上下文安全地调用。但是,如果内存不足,可能会发送失败。在具体的连接器实例中,该函数用于向用户态发送 netlink 消息。
参数 msg 为发送的 netlink 消息的消息头。参数 __group 为接收消息的组,如果它为 0,那么连接器将搜索所有注册的连接器用户,最终将发送给用户 ID 与在 msg 中的 ID 相同的组,但如果 __group 不为 0,消息将发送给 __group 指定的组。参数 gfp_mask 指定页分配标志。
注意:当注册新的回调函数时,连接器将指定它的组为 id.idx。
cn_msg 是连接器定义的消息头,字段 seq 和 ack 用于确保消息的可靠传输,刚才已经提到,netlink 在内存紧张的情况下可能丢失消息,因此该头使用顺序号和响应号来满足要求可靠传输用户的需求。当发送消息时,用户需要设置独一无二的顺序号和随机的响应号,顺序号也应当设置到 nlmsghdr->nlmsg_seq。注意 nlmsghdr 是类型为结构 struct nlmsghdr 的变量,它用于设置或保存 netlink 的消息头。每发送一个消息,顺序号应当加 1,如果需要发送响应消息,那么响应消息 的顺序号应当与被响应的消息的顺序号相同,同时响应消息的响应号应当为被响应消息的顺序号加1。如果接收到的消息的顺序号不是期望的顺序号,那表明该消息是一个新的消息,如果接收到的消息的顺序号是期望的顺序号,但它的响应号不等于上次发送消息的顺序号加1,那么它也是新消息。
回页首
三、用户态如何使用连接器
内核 2.6.14 对 netlink 套接字有新的实现,它缺省情况下不允许用户态应用发送给组号非 1 的netlink 组,因此用户态应用要想使用非1的组,必须先加入到该组,这可以通过如下代码实现:
#ifndef SOL_NETLINK
#define SOL_NETLINK 270
#endif
#ifndef NETLINK_DROP_MEMBERSHIP
#define NETLINK_DROP_MEMBERSHIP 0
#endif
#ifndef NETLINK_ADD_MEMBERSHIP
#define NETLINK_ADD_MEMBERSHIP 1
#endif
int group = 5;
s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
l_local.nl_family = AF_NETLINK;
l_local.nl_groups = group;
l_local.nl_pid = getpid();
if (bind(s, (struct sockaddr *)&l_local, sizeof(struct sockaddr_nl)) == -1) {
perror("bind");
close(s);
return -1;
}
setsockopt(s, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group));
在不需要使用该连接器时使用语句
setsockopt(s, SOL_NETLINK, NETLINK_DROP_MEMBERSHIP, &group, sizeof(group));
退出NETLINK_CONNECTOR的group组。
宏 SOL_NETLINK、NETLINK_ADD_MEMBERSHIP 和 NETLINK_DROP_MEMBERSHIP 在旧的系统中并没有定义,因此需要用户显式定义。
内核 2.6.14 的 netlink 代码只允许选择一个小于或等于最大组号的组,对于连接器,最大的组号为CN_NETLINK_USERS + 0xf, 即16,因此如果想使用更大的组号,必须修改CN_NETLINK_USERS 到该大值。增加的 0xf 个号码供非内核态用户使用。因此,组 0xffffffff目前不能使用。