红联Linux门户
Linux帮助

Linux用户态与内核态的交互(1)

发布时间:2006-10-16 00:48:10来源:红联作者:橙丢
参考文档 《Linux 系统内核空间与用户空间通信的实现与分析》 陈鑫 《在 Linux 下用户空间与内核空间数据交换的方式》 杨?
这是一篇学习笔记,主要是对《Linux 系统内核空间与用户空间通信的实现与分析》(此文本站已转载)中的源码imp2的分析。其中的源码,可以到以下URL下载:
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz 理论篇 在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的,例如iprote2网络管理工具,它与内核的交互就全部使用了netlink,著名的内核包过滤框架Netfilter在与用户空间的通读,也在最新版本中改变为netlink,无疑,它将是Linux用户态与内核态交流的主要方法之一。它的通信依据是一个对应于进程的标识,一般定为该进程的 ID。当通信的一端处于中断过程时,该标识为 0。当使用 netlink 套接字进行通信,通信的双方都是用户态进程,则使用方法类似于消息队列。但通信双方有一端是中断过程,使用方法则不同。netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核程,而是通过另一个软中断调用用户事先指定的接收函数。工作原理如图: 如图所示,这里使用了软中断而不是内核线程来接收数据,这样就可以保证数据接收的实时性。 当 netlink 套接字用于内核空间与用户空间的通信时,在用户空间的创建方法和一般套接字使用类似,但内核空间的创建方法则不同,下图是 netlink 套接字实现此类通信时创建的过程: 用户空间 用户态应用使用标准的socket与内核通讯,标准的socket API 的函数, socket(), bind(), sendmsg(), recvmsg() 和 close()很容易地应用到 netlink socket。 为了创建一个 netlink socket,用户需要使用如下参数调用 socket(): [table=400][tr][td]socket(AF_NETLINK, SOCK_RAW, netlink_type)[/td][/tr][/table]netlink对应的协议簇是 AF_NETLINK,第二个参数必须是SOCK_RAW或SOCK_DGRAM, 第三个参数指定netlink协议类型,它可以是一个自定义的类型,也可以使用内核预定义的类型: [table=400][tr][td]#define NETLINK_ROUTE 0 /* Routing/device hook */#define NETLINK_W1 1 /* 1-wire subsystem */#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */#define NETLINK_FIREWALL 3 /* Firewalling hook */#define NETLINK_INET_DIAG 4 /* INET socket monitoring */#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */#define NETLINK_XFRM 6 /* ipsec */#define NETLINK_SELINUX 7 /* SELinux event notifications */#define NETLINK_ISCSI 8 /* Open-iSCSI */#define NETLINK_AUDIT 9 /* auditing */#define NETLINK_FIB_LOOKUP 10#define NETLINK_CONNECTOR 11#define NETLINK_NETFILTER 12 /* netfilter subsystem */#define NETLINK_IP6_FW 13#define NETLINK_DNRTMSG 14 /* DECnet routing messages */#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */#define NETLINK_GENERIC 16
[/td][/tr]
[/table]同样地,socket函数返回的套接字,可以交给bind函数调用: [table=400][tr][td]static int skfd;skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);if(skfd < 0){ printf("can not create a netlink socket\n"); exit(0);}
[/td][/tr]
[/table]bind函数需要绑定协议地址,netlink的socket地址使用struct sockaddr_nl结构描述: [table=400][tr][td]struct sockaddr_nl{ sa_family_t nl_family; unsigned short nl_pad; __u32 nl_pid; __u32 nl_groups;};
[/td][/tr]
[/table]
成员 nl_family为协议簇 AF_NETLINK,成员 nl_pad 当前没有使用,因此要总是设置为 0,成员 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。成员 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组:
[table=400][tr][td]struct sockaddr_nl local;memset(&local, 0, sizeof(local));local.nl_family = AF_NETLINK;local.nl_pid = getpid(); /*设置pid为自己的pid值*/local.nl_groups = 0;/*绑定套接字*/if(bind(skfd, (struct sockaddr*)&local, sizeof(local)) != 0){printf("bind() error\n"); return -1;}[/td][/tr][/table]
用户空间可以调用send函数簇向内核发送消息,如sendto、sendmsg等,同样地,也可以使用struct sockaddr_nl来描述一个对端地址,以待send函数来调用,与本地地址稍不同的是,因为对端为内核,所以nl_pid成员需要设置为0:
[table=400][tr][td]struct sockaddr_nl kpeer;memset(&kpeer, 0,sizeof(kpeer));kpeer.nl_family = AF_NETLINK;kpeer.nl_pid = 0;kpeer.nl_groups = 0;[/td][/tr][/table]另一个问题就是发内核发送的消息的组成,使用我们发送一个IP网络数据包的话,则数据包结构“IP包头+IP数据”,同样地,netlink的消息结构是“netlink消息头部+数据”。Netlink消息头部使用struct nlmsghdr结构来描述: [table=400][tr][td]struct nlmsghdr{ __u32 nlmsg_len; /* Length of message */ __u16 nlmsg_type; /* Message type*/ __u16 nlmsg_flags; /* Additional flags */ __u32 nlmsg_seq; /* Sequence number */ __u32 nlmsg_pid; /* Sending process PID */};
[/td]
[/tr]
[/table]



字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,一般地,我们使用netlink提供的宏NLMSG_LENGTH来计算这个长度,仅需向NLMSG_LENGTH宏提供要发送的数据的长度,它会自动计算对齐后的总长度:
/*计算包含报头的数据报长度*/
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*字节对齐*/
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )


后面还可以看到很多netlink提供的宏,这些宏可以为我们编写netlink宏提供很大的方便。
字段 nlmsg_type 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_flags 用于设置消息标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作),字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID。

struct msg_to_kernel
/*自定义消息首部,它仅包含了netlink的消息首部*/
{
struct nlmsghdr hdr;
};
struct msg_to_kernel message;
memset(&message, 0, sizeof(message));
message.hdr.nlmsg_len = NLMSG_LENGTH(0);
/*计算消息,因为这里只是发送一个请求消息,没有多余的数据,所以,数据长度为0*/
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = IMP2_U_PID; /*设置自定义消息类型*/
message.hdr.nlmsg_pid = local.nl_pid; /*设置发送者的PID*/

这样,有了本地地址、对端地址和发送的数据,就可以调用发送函数将消息发送给内核了:
/*发送一个请求*/
sendto(skfd, &message, message.hdr.nlmsg_len, 0,
(struct sockaddr*)&kpeer, sizeof(kpeer));

当发送完请求后,就可以调用recv函数簇从内核接收数据了,
接收到的数据包含了netlink消息首部和要传输的数据:


/*接收的数据包含了netlink消息首部和自定义数据结构*/
struct u_packet_info
{
struct nlmsghdr hdr;
struct packet_info icmp_info;
};
struct u_packet_info info;
while(1)
{
kpeerlen = sizeof(struct sockaddr_nl);
/*接收内核空间返回的数据*/
rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),
0, (struct sockaddr*)&kpeer, &kpeerlen);

/*处理接收到的数据*/
……
}
文章评论

共有 0 条评论