Linux用户态和内核态之间的交互(笔记)

来源:互联网 发布:koka方便面 知乎 编辑:程序博客网 时间:2024/04/26 10:46

Linux用户态和内核态之间的交互:
读书笔记:
          原文:《在 Linux 下用户空间与内核空间数据交换的方式》
          链接:http://www.ibm.com/developerworks/cn/linux/l-kerns-usrs/
      Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。


用户态使用netlink
      用户态应用使用标准的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和 close() 就能很容易地使用 netlink socket。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket 需要的头文件也必不可少,sys/socket.h。

创建netlink socket:
     为了创建一个 netlink socket,用户需要使用如下参数调用 socket():
     socket(AF_NETLINK, SOCK_RAW, netlink_type)
     说明:
     第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink,第二个参数必须是SOCK_RAW或SOCK_DGRAM,第三个参数指定netlink协议类型,如前面讲的用户自定义协议类型NETLINK_MYTEST, NETLINK_GENERIC是一个通用的协议类型,它是专门为用户使用的,因此,用户可以直接使用它,而不必再添加新的协议类型。内核预定义的协议类型有:
#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
      对于每一个netlink协议类型,可以有多达 32多播组,每一个多播组用一个位表示,netlink 的多播特性使得发送消息给同一个组仅需要一次系统调用,因而对于需要多拨消息的应用而言,大大地降低了系统调用的次数。

绑定(bind):
      函数 bind() 用于把一个打开的 netlink socket 与 netlink 源 socket 地址绑定在一起。netlink socket 的地址结构如下:
struct sockaddr_nl
{
  sa_family_t    nl_family;
  unsigned short nl_pad;
  __u32          nl_pid;
  __u32          nl_groups;
};
      说明:
      字段 nl_family 必须设置为 AF_NETLINK 或着 PF_NETLINK,字段 nl_pad 当前没有使用,因此要总是设置为 0,字段 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。字段 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组。
      传递给 bind 函数的地址的 nl_pid 字段应当设置为本进程的进程 ID,这相当于 netlink socket 的本地地址。但是,对于一个进程的多个线程使用 netlink socket 的情况,字段 nl_pid 则可以设置为其它的值,如:
      pthread_self() << 16 | getpid();
      因此字段 nl_pid 实际上未必是进程 ID,它只是用于区分不同的接收者或发送者的一个标识,用户可以根
据自己需要设置该字段。函数 bind 的调用方式如下:
      bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));
      说明:
      fd为前面的 socket 调用返回的文件描述符,参数 nladdr 为 struct sockaddr_nl 类型的地址。为了发送一个 netlink 消息给内核或其他用户态应用,需要填充目标 netlink socket 地址,此时,字段 nl_pid 和 nl_groups 分别表示接收消息者的进程 ID 与多播组。如果字段 nl_pid 设置为 0,表示消息接收者为内核或多播组,如果 nl_groups为 0,表示该消息为单播消息,否则表示多播消息。
   
发送消息:
      使用函数 sendmsg 发送 netlink 消息时还需要引用结构 struct msghdr、struct nlmsghdr 和struct iovec。
      struct msghdr 需如下设置(其中 nladdr 为消息接收者的 netlink 地址):  
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);

      struct nlmsghdr 为 netlink socket 自己的消息头,也被称为netlink控制块,应用在发送 netlink 消息时必须提供该消息头。
      结构体如下:
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 */
};
      说明:
      字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,字段nlmsg_type 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_flags 用于设置消息标志。内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作),字段nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID。
      示例:
#define MAX_MSGSIZE 1024
char buffer[] = "An example message";
struct nlmsghdr nlhdr;
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA(nlhdr),buffer);
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
nlhdr->nlmsg_pid = getpid();  /* self pid */
nlhdr->nlmsg_flags = 0;
   
      struct iovec (用于把多个消息通过一次系统调用来发送):
      示例:
struct iovec iov;
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;

      在完成以上步骤后,消息就可以通过下面语句直接发送:
      sendmsg(fd, &msg, 0);

接收消息:
      应用接收消息时需要首先分配一个足够大的缓存来保存消息头以及消息的数据部分,然后填充消息头,添加完后就可以直接调用函数 recvmsg() 来接收。
      recvmsg(fd, &msg, 0);
      注意:fd为socket调用打开的netlink socket描述符。
      示例:
#define MAX_NL_MSG_LEN 1024
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
struct nlmsghdr * nlhdr;
nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);
iov.iov_base = (void *)nlhdr;
iov.iov_len = MAX_NL_MSG_LEN;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
recvmsg(fd, &msg, 0);
      在消息接收后,nlhdr指向接收到的消息的消息头,nladdr保存了接收到的消息的目标地址,宏NLMSG_DATA(nlhdr)返回指向消息的数据部分的指针。

Linux内核API
内核空间主要完成三件工作:
n 创建netlink套接字
n 接收处理用户空间发送的数据
n 发送数据至用户空间

      netlink的内核实现在.c文件net/core/af_netlink.c中,内核模块要想使用netlink,也必须包含头文件
linux/netlink.h。

增加netlink协议类型:
      增加新的netlink协议类型,仅需增加如下定义到linux/netlink.h就可以:
      #define NETLINK_MYTEST  17
      只要增加这个定义之后,用户就可以在内核的任何地方引用该协议。

创建netlink socket:
      在内核中,为了创建一个netlink socket用户需要调用如下函数:
      struct sock * netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));
      说明:
      参数unit表示netlink协议类型,如NETLINK_MYTEST,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个netlink socket时,该input函数指针就会被引用。函数指针input的参数sk实际上就是函数netlink_kernel_create返回的 struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct sock结构来表示。

发送netlink消息:
      当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过struct sk_buff来管理的,linux/netlink.h中定义了一个宏:
      #define NETLINK_CB(skb)         (*(struct netlink_skb_parms*)&((skb)->cb))
来方便消息的地址设置。下面是一个消息地址设置的例子:
    NETLINK_CB(skb).pid = 0;
    NETLINK_CB(skb).dst_pid = 0;
    NETLINK_CB(skb).dst_group = 1;
      说明:
      字段pid表示消息发送者进程ID,也即源地址,对于内核,它为 0, dst_pid 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则 dst_group 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0。

      在内核中,模块调用函数 netlink_unicast 来发送单播消息:
      int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);
      说明:
      参数sk为函数netlink_kernel_create()返回的socket,参数skb存放消息,它的data字段指向要发送的 netlink消息结构,而skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块,参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。

      内核模块或子系统也可以使用函数netlink_broadcast来发送广播消息:
      void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group,int
allocation);
      说明:
      前面的三个参数与netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或 GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。


释放netlink socket:
      在内核中使用函数sock_release来释放函数netlink_kernel_create()创建的netlink socket:
      void sock_release(struct socket * sock);
      说明:
      注意函数netlink_kernel_create()返回的类型为struct sock,因此函数sock_release应该这种调用:
      sock_release(sk->sk_socket);
      sk为函数netlink_kernel_create()的返回值。


补充:
在linux/netlink.h中定义了一些方便对消息进行处理的宏,这些宏包括:
#define NLMSG_ALIGNTO   4
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值。

#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
宏NLMSG_LENGTH(len)用于计算数据部分长度为len时实际的消息长度。它一般用于分配消息缓存。

#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存。

#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏。

#define NLMSG_NEXT(nlh,len)      ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), /
                      (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址,同时len也减少为剩余消息的总长度,该宏一般在一个消息被分成几个部分发送或接收时使用。

#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && /
                           (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && /
                           (nlh)->nlmsg_len <= (len))
宏NLMSG_OK(nlh,len)用于判断消息是否有len这么长。

#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
宏NLMSG_PAYLOAD(nlh,len)用于返回payload的长度。

/*初始化一个netlink消息首部*/
nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size,sizeof(*nlh));

 

实践代码:

用户态:user.c

 

内核态:kernel.c

 

Makefile:

编译运行:

[root@localhost netlink_liuxl]# uname -r
2.6.26
[root@localhost netlink_liuxl]# make
make -C /lib/modules/2.6.26/build M=/home/liuxltest/netlink/netlink_liuxl
make[1]: Entering directory `/usr/src/linux-2.6.26'
  LD      /home/liuxltest/netlink/netlink_liuxl/built-in.o
  CC [M]  /home/liuxltest/netlink/netlink_liuxl/kernel.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/liuxltest/netlink/netlink_liuxl/kernel.mod.o
  LD [M]  /home/liuxltest/netlink/netlink_liuxl/kernel.ko
make[1]: Leaving directory `/usr/src/linux-2.6.26'
gcc -o user user.c
[root@localhost netlink_liuxl]# ls
built-in.o  kernel.ko     kernel.mod.o  Makefile       Module.symvers  user.c
kernel.c    kernel.mod.c  kernel.o      modules.order  user
[root@localhost netlink_liuxl]# insmod kernel.ko
[root@localhost netlink_liuxl]# ./user
Start send messages!
Wait recv messages!
Received messages!Data part is : Hello , Xiaolei!
[root@localhost netlink_liuxl]#

看看内核情况:
[root@localhost ~]# grep 'KNL' /var/log/messages     
Feb  6 14:17:05 localhost kernel: KNL: Create netlink socket ok!
Feb  6 14:17:26 localhost kernel: KNL: Get data from sk_buff .
Feb  6 14:17:26 localhost kernel: KNL: Recv Hello , Xiaolei! .
Feb  6 14:17:26 localhost kernel: KNL: Pid is 17470
Feb  6 14:17:26 localhost kernel: KNL: Going to send.
Feb  6 14:17:26 localhost kernel: KNL: Send is ok!
[root@localhost ~]#