Linux 系统内核空间与用户空间通信的实现与分析

Linux内核和用户空间通信的方法(二)— 使用netlink

 
作者:Kendo
2006-9-3

这是一篇学习笔记,主要是对《Linux 系统内核空间与用户空间通信的实现与分析》中的源码imp2的分析。其中的源码,可以到以下URL下载:
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/imp2.tar.gz

参考文档
《Linux 系统内核空间与用户空间通信的实现与分析》                陈鑫
http://www-128.ibm.com/developerworks/cn/linux/l-netlink/?ca=dwcn-newsletter-linux
《在 Linux 下用户空间与内核空间数据交换的方式》                杨燚
http://www-128.ibm.com/developerworks/cn/linux/l-kerns-usrs/

理论篇
     在 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():
 

socket(AF_NETLINK, SOCK_RAW, netlink_type)

netlink对应的协议簇是 AF_NETLINK,第二个参数必须是SOCK_RAW或SOCK_DGRAM, 第三个参数指定netlink协议类型,它可以是一个自定义的类型,也可以使用内核预定义的类型:

#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

同样地,socket函数返回的套接字,可以交给bing等函数调用:

static int skfd;
skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
if(skfd < 0)
{
      printf("can not create a netlink socket/n");
      exit(0);
}

bind函数需要绑定协议地址,netlink的socket地址使用struct sockaddr_nl结构描述:

struct sockaddr_nl
{
  sa_family_t nl_family;
  unsigned short nl_pad;
  __u32 nl_pid;
  __u32 nl_groups;
};

成员 nl_family为协议簇 AF_NETLINK,成员 nl_pad 当前没有使用,因此要总是设置为 0,成员 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。成员 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组:

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;
}

用户空间可以调用send函数簇向内核发送消息,如sendto、sendmsg等,同样地,也可以使用struct sockaddr_nl来描述一个对端地址,以待send函数来调用,与本地地址稍不同的是,因为对端为内核,所以nl_pid成员需要设置为0:

struct sockaddr_nl kpeer;
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;

另一个问题就是发内核发送的消息的组成,使用我们发送一个IP网络数据包的话,则数据包结构为“IP包头+IP数据”,同样地,netlink的消息结构是“netlink消息头部+数据”。Netlink消息头部使用struct nlmsghdr结构来描述:

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 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,一般地,我们使用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);
                  
       /*处理接收到的数据*/
……
}

同样地,函数close用于关闭打开的netlink socket。程序中,因为程序一直循环接收处理内核的消息,需要收到用户的关闭信号才会退出,所以关闭套接字的工作放在了自定义的信号函数sig_int中处理:

/*这个信号函数,处理一些程序退出时的动作*/
static void sig_int(int signo)
{
  struct sockaddr_nl kpeer;
  struct msg_to_kernel message;

  memset(&kpeer, 0, sizeof(kpeer));
  kpeer.nl_family = AF_NETLINK;
  kpeer.nl_pid = 0;
  kpeer.nl_groups = 0;

  memset(&message, 0, sizeof(message));
  message.hdr.nlmsg_len = NLMSG_LENGTH(0);
  message.hdr.nlmsg_flags = 0;
  message.hdr.nlmsg_type = IMP2_CLOSE;
  message.hdr.nlmsg_pid = getpid();

  /*向内核发送一个消息,由nlmsg_type表明,应用程序将关闭*/
  sendto(skfd, &message, message.hdr.nlmsg_len, 0, (struct sockaddr *)(&kpeer), sizeof(kpeer));

  close(skfd);
  exit(0);
}

这个结束函数中,向内核发送一个“我已经退出了”的消息,然后调用close函数关闭netlink套接字,退出程序。

内核空间

与应用程序内核,内核空间也主要完成三件工作:
n        创建netlink套接字
n        接收处理用户空间发送的数据
n        发送数据至用户空间

API函数netlink_kernel_create用于创建一个netlink socket,同时,注册一个回调函数,用于接收处理用户空间的消息:

struct sock *
netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));

参数unit表示netlink协议类型,如NL_IMP2,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个netlink socket时,该input函数指针就会被引用。函数指针input的参数sk实际上就是函数netlink_kernel_create返回的struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct sock结构来表示。

static int __init init(void)
{
  rwlock_init(&user_proc.lock); /*初始化读写锁*/

  /*创建一个netlink socket,协议类型是自定义的ML_IMP2,kernel_reveive为接受处理函数*/
  nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);
  if(!nlfd) /*创建失败*/
  {
      printk("can not create a netlink socket/n");
      return -1;
  }

  /*注册一个Netfilter 钩子*/
  return nf_register_hook(&imp2_ops);
}


module_init(init);

用户空间向内核发送了两种自定义消息类型:IMP2_U_PID和IMP2_CLOSE,分别是请求和关闭。kernel_receive 函数分别处理这两种消息:

DECLARE_MUTEX(receive_sem); /*初始化信号量*/
static void kernel_receive(struct sock *sk, int len)
{
        do
    {
                struct sk_buff *skb;
                if(down_trylock(&receive_sem)) /*获取信号量*/
                        return;
                /*从接收队列中取得skb,然后进行一些基本的长度的合法性校验*/
                while((skb = skb_dequeue(&sk->receive_queue)) != NULL)
        {
                        {
                                struct nlmsghdr *nlh = NULL;
                                
                                if(skb->len >= sizeof(struct nlmsghdr))
                                {
                                        /*获取数据中的nlmsghdr 结构的报头*/
                                        nlh = (struct nlmsghdr *)skb->data;
                                        if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))
                                                && (skb->len >= nlh->nlmsg_len))
                                        {
                                                /*长度的全法性校验完成后,处理应用程序自定义消息类型,主要是对用户PID的保存,即为内核保存“把消息发送给谁”*/
                                                if(nlh->nlmsg_type == IMP2_U_PID) /*请求*/
                                                {
                                                        write_lock_bh(&user_proc.pid);
                                                        user_proc.pid = nlh->nlmsg_pid;
                                                        write_unlock_bh(&user_proc.pid);
                                                }
                                                else if(nlh->nlmsg_type == IMP2_CLOSE) /*应用程序关闭*/
                                                {
                                                        write_lock_bh(&user_proc.pid);
                                                        if(nlh->nlmsg_pid == user_proc.pid)
                                                                user_proc.pid = 0;
                                                        write_unlock_bh(&user_proc.pid);
                                                }
                                        }
                                }
                        }
                        kfree_skb(skb);
        }
                up(&receive_sem); /*返回信号量*/
    }while(nlfd && nlfd->receive_queue.qlen);
}

 
因为内核模块可能同时被多个进程同时调用,所以函数中使用了信号量和锁来进行互斥。skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收队列上的消息,返回为一个struct sk_buff的结构,skb->data指向实际的netlink消息。

程序中注册了一个Netfilter钩子,钩子函数是get_icmp,它截获ICMP数据包,然后调用send_to_user函数将数据发送给应用空间进程。发送的数据是info结构变量,它是struct packet_info结构,这个结构包含了来源/目的地址两个成员。Netfilter Hook不是本文描述的重点,略过。
send_to_user 用于将数据发送给用户空间进程,发送调用的是API函数netlink_unicast 完成的:

int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);

参数sk为函数netlink_kernel_create()返回的套接字,参数skb存放待发送的消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息, 参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。
向用户空间进程发送的消息包含三个部份:netlink 消息头部、数据部份和控制字段,控制字段包含了内核发送netlink消息时,需要设置的目标地址与源地址,内核中消息是通过sk_buff来管理的, linux/netlink.h中定义了NETLINK_CB宏来方便消息的地址设置:

#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。

static int send_to_user(struct packet_info *info)
{
int ret;
int size;
unsigned char *old_tail;
struct sk_buff *skb;
struct nlmsghdr *nlh;
struct packet_info *packet;

/*计算消息总长:消息首部加上数据加度*/
size = NLMSG_SPACE(sizeof(*info));

/*分配一个新的套接字缓存*/
skb = alloc_skb(size, GFP_ATOMIC);
old_tail = skb->tail;

/*初始化一个netlink消息首部*/
nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
/*跳过消息首部,指向数据区*/
packet = NLMSG_DATA(nlh);
/*初始化数据区*/
memset(packet, 0, sizeof(struct packet_info));
/*填充待发送的数据*/
packet->src = info->src;
packet->dest = info->dest;

/*计算skb两次长度之差,即netlink的长度总和*/
nlh->nlmsg_len = skb->tail - old_tail;
/*设置控制字段*/
NETLINK_CB(skb).dst_groups = 0;

/*发送数据*/
read_lock_bh(&user_proc.lock);
ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
read_unlock_bh(&user_proc.lock);


}

函数初始化netlink 消息首部,填充数据区,然后设置控制字段,这三部份都包含在skb_buff中,最后调用netlink_unicast函数把数据发送出去。
函数中调用了netlink的一个重要的宏NLMSG_PUT,它用于初始化netlink 消息首部:

 

#define NLMSG_PUT(skb, pid, seq, type, len) /
({ if (skb_tailroom(skb) < (int)NLMSG_SPACE(len)) goto nlmsg_failure; /
   __nlmsg_put(skb, pid, seq, type, len); })
static __inline__ struct nlmsghdr *
__nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq, int type, int len)
{
        struct nlmsghdr *nlh;
        int size = NLMSG_LENGTH(len);

        nlh = (struct nlmsghdr*)skb_put(skb, NLMSG_ALIGN(size));
        nlh->nlmsg_type = type;
        nlh->nlmsg_len = size;
        nlh->nlmsg_flags = 0;
        nlh->nlmsg_pid = pid;
        nlh->nlmsg_seq = seq;
        return nlh;
}


这个宏一个需要注意的地方是调用了nlmsg_failure标签,所以在程序中应该定义这个标签。

在内核中使用函数sock_release来释放函数netlink_kernel_create()创建的netlink socket:

void sock_release(struct socket * sock);

程序在退出模块中释放netlink sockets和netfilter hook:

static void __exit fini(void)
{
  if(nlfd)
    {
      sock_release(nlfd->socket); /*释放netlink socket*/
    }
  nf_unregister_hook(&imp2_ops); /*撤锁netfilter 钩子*/
}

 原文地址 http://linux.chinaunix.net/bbs/thread-822500-1-1.html

 

 

 

 

 

 

 

 

 

 

 

 

 

Linux 系统内核空间与用户空间通信的实现与分析

 

 

 

未显示需要 JavaScript 的文档选项


级别: 初级

陈鑫 ([email protected]), 自由软件爱好者, 南京邮电学院电子工程系

2004 年 7 月 01 日

 

多数的 Linux 内核态程序都需要和用户空间的进程交换数据,但 Linux 内核态无法对传统的 Linux 进程间同步和通信的方法提供足够的支持。本文总结并比较了几种内核态与用户态进程通信的实现方法,并推荐使用 netlink 套接字实现中断环境与用户态进程通信。

1 引言

Linux 是一个源码开放的操作系统,无论是普通用户还是企业用户都可以编写自己的内核代码,再加上对标准内核的裁剪从而制作出适合自己的操作系统。目前有很多中低端用户使用的网络设备的操作系统是从标准 Linux 改进而来的,这也说明了有越来越多的人正在加入到 Linux 内核开发团体中。

一个或多个内核模块的实现并不能满足一般 Linux 系统软件的需要,因为内核的局限性太大,如不能在终端上打印,不能做大延时的处理等等。当我们需要做这些的时候,就需要将在内核态采集到的数据传送到用户态的一个或多个进程中进行处理。这样,内核态与用户空间进程通信的方法就显得尤为重要。在 Linux 的内核发行版本中没有对该类通信方法的详细介绍,也没有其他文章对此进行总结,所以本文将列举几种内核态与用户态进程通信的方法并详细分析它们的实现和适用环境。





回页首


2 Linux 内核模块的运行环境与传统进程间通信

在一台运行 Linux 的计算机中,CPU 在任何时候只会有如下四种状态:

【1】 在处理一个硬中断。

【2】 在处理一个软中断,如 softirq、tasklet 和 bh。

【3】 运行于内核态,但有进程上下文,即与一个进程相关。

【4】 运行一个用户态进程。

其中,【1】、【2】和【3】是运行于内核空间的,而【4】是在用户空间。其中除了【4】,其他状态只可以被在其之上的状态抢占。比如,软中断只可以被硬中断抢占。

Linux 内核模块是一段可以动态在内核装载和卸载的代码,装载进内核的代码便立即在内核中工作起来。Linux 内核代码的运行环境有三种:用户上下文环境、硬中断环境和软中断环境。但三种环境的局限性分两种,因为软中断环境只是硬中断环境的延续。比较如表【1】。

表【1】

内核态环境 介绍 局限性
用户上下文 内核态代码的运行与一用户空间进程相关,如系统调用中代码的运行环境。 不可直接将本地变量传递给用户态的内存区,因为内核态和用户态的内存映射机制不同。
硬中断和软中断环境 硬中断或软中断过程中代码的运行环境,如 IP 数据报的接收代码的运行环境,网络设备的驱动程序等。 不可直接向用户态内存区传递数据;
代码在运行过程中不可阻塞。

Linux 传统的进程间通信有很多,如各类管道、消息队列、内存共享、信号量等等。但它们都无法介于内核态与用户态使用,原因如表【2】。

表【2】

通信方法 无法介于内核态与用户态的原因
管道(不包括命名管道) 局限于父子进程间的通信。
消息队列 在硬、软中断中无法无阻塞地接收数据。
信号量 无法介于内核态和用户态使用。
内存共享 需要信号量辅助,而信号量又无法使用。
套接字 在硬、软中断中无法无阻塞地接收数据。




回页首


3 Linux内核态与用户态进程通信方法的提出与实现

3.1 用户上下文环境

运行在用户上下文环境中的代码是可以阻塞的,这样,便可以使用消息队列和 UNIX 域套接字来实现内核态与用户态的通信。但这些方法的数据传输效率较低,Linux 内核提供 copy_from_user()/copy_to_user() 函数来实现内核态与用户态数据的拷贝,但这两个函数会引发阻塞,所以不能用在硬、软中断中。一般将这两个特殊拷贝函数用在类似于系统调用一类的函数中,此类函数在使用中往往"穿梭"于内核态与用户态。此类方法的工作原理路如图【1】。


图【1】
Linux 系统内核空间与用户空间通信的实现与分析_第1张图片

其中相关的系统调用是需要用户自行编写并载入内核。 imp1.tar.gz是一个示例,内核模块注册了一组设置套接字选项的函数使得用户空间进程可以调用此组函数对内核态数据进行读写。源码包含三个文件,imp1.h 是通用头文件,定义了用户态和内核态都要用到的宏。imp1_k.c 是内核模块的源代码。imp1_u.c 是用户态进程的源代码。整个示例演示了由一个用户态进程向用户上下文环境发送一个字符串,内容为"a message from userspace/n"。然后再由用户上下文环境向用户态进程发送一个字符串,内容为"a message from kernel/n"。

3.2 硬、软中断环境

比起用户上下文环境,硬中断和软中断环境与用户态进程无丝毫关系,而且运行过程不能阻塞。

3.2.1 使用一般进程间通信的方法

我们无法直接使用传统的进程间通信的方法实现。但硬、软中断中也有一套同步机制--自旋锁(spinlock),可以通过自旋锁来实现中断环境与中断环境,中断环境与内核线程的同步,而内核线程是运行在有进程上下文环境中的,这样便可以在内核线程中使用套接字或消息队列来取得用户空间的数据,然后再将数据通过临界区传递给中断过程。基本思路如图【2】。


图【2】
Linux 系统内核空间与用户空间通信的实现与分析_第2张图片

因为中断过程不可能无休止地等待用户态进程发送数据,所以要通过一个内核线程来接收用户空间的数据,再通过临界区传给中断过程。中断过程向用户空间的数据发送必须是无阻塞的。这样的通信模型并不令人满意,因为内核线程是和其他用户态进程竞争CPU接收数据的,效率很低,这样中断过程便不能实时地接收来自用户空间的数据。

3.2.2 netlink 套接字

在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的,同时还使用 netlink 实现了 ip queue 工具,但 ip queue 的使用有其局限性,不能自由地用于各种中断过程。内核的帮助文档和其他一些 Linux 相关文章都没有对 netlink 套接字在中断过程和用户空间通信的应用上作详细的说明,使得很多用户对此只有一个模糊的概念。

netlink 套接字的通信依据是一个对应于进程的标识,一般定为该进程的 ID。当通信的一端处于中断过程时,该标识为 0。当使用 netlink 套接字进行通信,通信的双方都是用户态进程,则使用方法类似于消息队列。但通信双方有一端是中断过程,使用方法则不同。netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数。工作原理如图【3】。


图【3】
Linux 系统内核空间与用户空间通信的实现与分析_第3张图片

很明显,这里使用了软中断而不是内核线程来接收数据,这样就可以保证数据接收的实时性。

当 netlink 套接字用于内核空间与用户空间的通信时,在用户空间的创建方法和一般套接字使用类似,但内核空间的创建方法则不同。图【4】是 netlink 套接字实现此类通信时创建的过程。


图【4】
Linux 系统内核空间与用户空间通信的实现与分析_第4张图片

以下举一个 netlink 套接字的应用示例。示例实现了从 netfilter 的 NF_IP_PRE_ROUTING 点截获的 ICMP 数据报,在将数据报的相关信息传递到一个用户态进程,由用户态进程将信息打印在终端上。源码在文件 imp2.tar.gz中。内核模块代码(分段详解):


(一)模块初始化与卸载
static struct sock *nlfd;
struct
{
  __u32 pid;
  rwlock_t lock;
}user_proc;
/*挂接在 netfilter 框架的 NF_IP_PRE_ROUTING 点上的函数为 get_icmp()*/
static struct nf_hook_ops imp2_ops =
{
  .hook = get_icmp,		/*netfilter 钩子函数*/
  .pf = PF_INET,
  .hooknum = NF_IP_PRE_ROUTING,
  .priority = NF_IP_PRI_FILTER -1,
};
static int __init init(void)
{
  rwlock_init(&user_proc.lock);
  /*在内核创建一个 netlink socket,并注明由 kernel_recieve() 函数接收数据
    这里协议 NL_IMP2 是自定的*/
  nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);
  if(!nlfd)
    {
      printk("can not create a netlink socket/n");
      return -1;
    }
  /*向 netfilter 的 NF_IP_PRE_ROUTING 点挂接函数*/
  return nf_register_hook(&imp2_ops);
}
static void __exit fini(void)
{
  if(nlfd)
    {
      sock_release(nlfd->socket);
	}
  nf_unregister_hook(&imp2_ops);
}
module_init(init);
module_exit(fini);

其实片断(一)的工作很简单,模块加载阶段先在内核空间创建一个 netlink 套接字,再将一个函数挂接在 netfilter 框架的 NF_IP_PRE_ROUTING 钩子点上。卸载时释放套接字所占的资源并注销之前在 netfilter 上挂接的函数。


(二)接收用户空间的数据
DECLARE_MUTEX(receive_sem);
01:	static void kernel_receive(struct sock *sk, int len)
02:	{
03: 	 do
04:    	{
05:     	struct sk_buff *skb;
06:      	if(down_trylock(&receive_sem))
07:        		return;
08:
09:      	while((skb = skb_dequeue(&sk-

如果读者看过 ip_queue.c 或 rtnetlink.c中的源码会发现片断(二)中的 03~18 和 31~38 是 netlink socket 在内核空间接收数据的框架。在框架中主要是从套接字缓存中取出全部的数据,然后分析是不是合法的数据报,合法的 netlink 数据报必须有nlmsghdr 结构的报头。在这里笔者使用了自己定义的消息类型:IMP2_U_PID(消息为用户空间进程的ID),IMP2_CLOSE(用户空间进程关闭)。因为考虑到 SMP,所以在这里使用了读写锁来避免不同 CPU 访问临界区的问题。kernel_receive() 函数的运行在软中断环境。


(三)截获 IP 数据报
static unsigned int get_icmp(unsigned int hook,
                             struct sk_buff **pskb,
                             const struct net_device *in,
                             const struct net_device *out,
                             int (*okfn)(struct sk_buff *))
{
  struct iphdr *iph = (*pskb)->nh.iph;
  struct packet_info info;
  if(iph->protocol == IPPROTO_ICMP)	/*若传输层协议为 ICMP*/
    {
      read_lock_bh(&user_proc.lock);
      if(user_proc.pid != 0)
	    {
          read_unlock_bh(&user_proc.lock);
          info.src = iph->saddr;	/*记录源地址*/
          info.dest = iph->daddr;	/*记录目的地址*/
          send_to_user(&info);		/*发送数据*/
        }
      else
        read_unlock_bh(&user_proc.lock);
    }
  return NF_ACCEPT;
}



(四)发送数据
static int send_to_user(struct packet_info *info)
{
  int ret;
  int size;
  unsigned char *old_tail;
  struct sk_buff *skb;
  struct nlmsghdr *nlh;
  struct packet_info *packet;
  size = NLMSG_SPACE(sizeof(*info));
  /*开辟一个新的套接字缓存*/
  skb = alloc_skb(size, GFP_ATOMIC);
  old_tail = skb->tail;
  /*填写数据报相关信息*/
  nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
  packet = NLMSG_DATA(nlh);
  memset(packet, 0, sizeof(struct packet_info));
  /*传输到用户空间的数据*/
  packet->src = info->src;
  packet->dest = info->dest;
  /*计算经过字节对其后的数据实际长度*/
  nlh->nlmsg_len = skb->tail - old_tail;
  NETLINK_CB(skb).dst_groups = 0;
  read_lock_bh(&user_proc.lock);
  ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT); /*发送数据*/
  read_unlock_bh(&user_proc.lock);
  return ret;
 nlmsg_failure: /*若发送失败,则撤销套接字缓存*/
  if(skb)
    kfree_skb(skb);
  return -1;
}



片断(四)中所使用的宏参考如下:
/*字节对齐*/
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/*计算包含报头的数据报长度*/
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*字节对齐后的数据报长度*/
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/*填写相关报头信息,这里使用了nlmsg_failure标签,所以在程序中要定义*/
#define NLMSG_PUT(skb, pid, seq, type, len) /
({ if (skb_tailroom(skb) < (int)NLMSG_SPACE(len)) goto nlmsg_failure; /
   __nlmsg_put(skb, pid, seq, type, len); })
static __inline__ struct nlmsghdr *
__nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq, int type, int len)
{
	struct nlmsghdr *nlh;
	int size = NLMSG_LENGTH(len);
	nlh = (struct nlmsghdr*)skb_put(skb, NLMSG_ALIGN(size));
	nlh->nlmsg_type = type;
	nlh->nlmsg_len = size;
	nlh->nlmsg_flags = 0;
	nlh->nlmsg_pid = pid;
	nlh->nlmsg_seq = seq;
	return nlh;
}
/*跳过报头取实际数据*/
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/*取 netlink 控制字段*/
#define NETLINK_CB(skb)		(*(struct netlink_skb_parms*)&((skb)->cb))

运行示例时,先编译 imp2_k.c 模块,然后使用 insmod 将模块加载入内核。再运行编译好的 imp2_u 命令,此时就会显示出本机当前接收的 ICMP 数据报的源地址和目的地址。用户可以使用 Ctrl+C 来终止用户空间的进程,再次启动也不会带来问题。





回页首


4 总结

本文从内核态代码的不同运行环境来实现不同方法的内核空间与用户空间的通信,并分析了它们的实际效果。最后推荐使用 netlink 套接字实现中断环境与用户态进程通信,因为 netlink 套接字是专为此类通信定制的。



参考资料

  • Linux 2.4 及后续版本内核源代码;

  • www.netfilter.org

  • RFC 3549;


关于作者

陈鑫:南京邮电学院电子工程系 2000 级本科生,自由软件爱好者。喜欢阅读 Linux 内核源代码,目前从事 Linux 系统的网络模块部分的分析工作。联系方式: [email protected]

 

 

 

 

 

 

 

 

在 Linux 下用户空间与内核空间数据交换的方式,第 1 部分: 内核启动参数、模块参数与sysfs、sysctl、系统调用和netlink

developerWorks
文档选项
将打印机的版面设置成横向打印模式

打印本页

将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


级别: 初级

燚 杨 ([email protected]), 计算机科学硕士

2006 年 2 月 16 日

本系列文章包括两篇,它们文详细地介绍了 Linux 系统下用户空间与内核空间数据交换的九种方式,包括内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,并给出具体的例子帮助读者掌握这些技术的使用。
本文是该系列文章的第一篇,它介绍了内核启动参数、模块参数与sysfs、sysctl、系统调用和netlink,并结合给出的例子程序详细地说明了它们如何使用。

一、引言

一般地,在使用虚拟内存技术的多任务系统上,内核和应用有不同的地址空间,因此,在内核和应用之间以及在应用与应用之间进行数据交换需要专门的机制来实现,众所周知,进程间通信(IPC)机制就是为实现应用与应用之间的数据交换而专门实现的,大部分读者可能对进程间通信比较了解,但对应用与内核之间的数据交换机制可能了解甚少,本文将详细介绍 Linux 系统下内核与应用进行数据交换的各种方式,包括内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs 和 relayfs。





回页首


二、内核启动参数

Linux 提供了一种通过 bootloader 向其传输启动参数的功能,内核开发者可以通过这种方式来向内核传输数据,从而控制内核启动行为。

通常的使用方式是,定义一个分析参数的函数,而后使用内核提供的宏 __setup把它注册到内核中,该宏定义在 linux/init.h 中,因此要使用它必须包含该头文件:


__setup("para_name=", parse_func)

para_name 为参数名,parse_func 为分析参数值的函数,它负责把该参数的值转换成相应的内核变量的值并设置那个内核变量。内核为整数参数值的分析提供了函数 get_option 和 get_options,前者用于分析参数值为一个整数的情况,而后者用于分析参数值为逗号分割的一系列整数的情况,对于参数值为字符串的情况,需要开发者自定义相应的分析函数。在源代码包中的内核程序kern-boot-params.c 说明了三种情况的使用。该程序列举了参数为一个整数、逗号分割的整数串以及字符串三种情况,读者要想测试该程序,需要把该程序拷贝到要使用的内核的源码目录树的一个目录下,为了避免与内核其他部分混淆,作者建议在内核源码树的根目录下创建一个新目录,如 examples,然后把该程序拷贝到 examples 目录下并重新命名为 setup_example.c,并且为该目录创建一个 Makefile 文件:


obj-y = setup_example.o

Makefile 仅许这一行就足够了,然后需要修改源码树的根目录下的 Makefile文件的一行,把下面行


core-y          := usr/

修改为


core-y          := usr/ examples/

注意:如果读者创建的新目录和重新命名的文件名与上面不同,需要修改上面所说 Makefile 文件相应的位置。做完以上工作就可以按照内核构建步骤去构建新的内核,在构建好内核并设置好lilo或grub为该内核的启动条目后,就可以启动该内核,然后使用lilo或grub的编辑功能为该内核的启动参数行增加如下参数串:

setup_example_int=1234 setup_example_int_array=100,200,300,400 setup_example_string=Thisisatest

当然,该参数串也可以直接写入到lilo或grub的配置文件中对应于该新内核的内核命令行参数串中。读者可以使用其它参数值来测试该功能。

下面是作者系统上使用上面参数行的输出:


setup_example_int=1234
setup_example_int_array=100,200,300,400
setup_example_int_array includes 4 intergers
setup_example_string=Thisisatest

读者可以使用


dmesg | grep setup

来查看该程序的输出。





回页首


三、模块参数与sysfs

内核子系统或设备驱动可以直接编译到内核,也可以编译成模块,如果编译到内核,可以使用前一节介绍的方法通过内核启动参数来向它们传递参数,如果编译成模块,则可以通过命令行在插入模块时传递参数,或者在运行时,通过sysfs来设置或读取模块数据。

Sysfs是一个基于内存的文件系统,实际上它基于ramfs,sysfs提供了一种把内核数据结构,它们的属性以及属性与数据结构的联系开放给用户态的方式,它与kobject子系统紧密地结合在一起,因此内核开发者不需要直接使用它,而是内核的各个子系统使用它。用户要想使用 sysfs 读取和设置内核参数,仅需装载 sysfs 就可以通过文件操作应用来读取和设置内核通过 sysfs 开放给用户的各个参数:


$ mkdir -p /sysfs
$ mount -t sysfs sysfs /sysfs

注意,不要把 sysfs 和 sysctl 混淆,sysctl 是内核的一些控制参数,其目的是方便用户对内核的行为进行控制,而 sysfs 仅仅是把内核的 kobject 对象的层次关系与属性开放给用户查看,因此 sysfs 的绝大部分是只读的,模块作为一个 kobject 也被出口到 sysfs,模块参数则是作为模块属性出口的,内核实现者为模块的使用提供了更灵活的方式,允许用户设置模块参数在 sysfs 的可见性并允许用户在编写模块时设置这些参数在 sysfs 下的访问权限,然后用户就可以通过sysfs 来查看和设置模块参数,从而使得用户能在模块运行时控制模块行为。

对于模块而言,声明为 static 的变量都可以通过命令行来设置,但要想在 sysfs下可见,必须通过宏 module_param 来显式声明,该宏有三个参数,第一个为参数名,即已经定义的变量名,第二个参数则为变量类型,可用的类型有 byte, short, ushort, int, uint, long, ulong, charp 和 bool 或 invbool,分别对应于 c 类型 char, short, unsigned short, int, unsigned int, long, unsigned long, char * 和 int,用户也可以自定义类型 XXX(如果用户自己定义了 param_get_XXX,param_set_XXX 和 param_check_XXX)。该宏的第三个参数用于指定访问权限,如果为 0,该参数将不出现在 sysfs 文件系统中,允许的访问权限为 S_IRUSR, S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH 和 S_IWOTH 的组合,它们分别对应于用户读,用户写,用户组读,用户组写,其他用户读和其他用户写,因此用文件的访问权限设置是一致的。

源代码包中的内核模块 module-param-exam.c 是一个利用模块参数和sysfs来进行用户态与内核态数据交互的例子。该模块有三个参数可以通过命令行设置,下面是作者系统上的运行结果示例:


$ insmod ./module-param-exam.ko my_invisible_int=10 my_visible_int=20 mystring="Hello,World"

               my_invisible_int = 10
my_visible_int = 20
mystring = 'Hello,World'
$ ls /sys/module/module_param_exam/parameters/
mystring  my_visible_int
$ cat /sys/module/module_param_exam/parameters/mystring
Hello,World
$ cat /sys/module/module_param_exam/parameters/my_visible_int
20
$ echo 2000 > /sys/module/module_param_exam/parameters/my_visible_int
$ cat /sys/module/module_param_exam/parameters/my_visible_int
2000
$ echo "abc" > /sys/module/module_param_exam/parameters/mystring
$ cat /sys/module/module_param_exam/parameters/mystring
abc
$ rmmod module_param_exam
my_invisible_int = 10
my_visible_int = 2000
mystring = 'abc'





回页首


四、sysctl

Sysctl是一种用户应用来设置和获得运行时内核的配置参数的一种有效方式,通过这种方式,用户应用可以在内核运行的任何时刻来改变内核的配置参数,也可以在任何时候获得内核的配置参数,通常,内核的这些配置参数也出现在proc文件系统的/proc/sys目录下,用户应用可以直接通过这个目录下的文件来实现内核配置的读写操作,例如,用户可以通过


Cat /proc/sys/net/ipv4/ip_forward

来得知内核IP层是否允许转发IP包,用户可以通过


echo 1 > /proc/sys/net/ipv4/ip_forward

把内核 IP 层设置为允许转发 IP 包,即把该机器配置成一个路由器或网关。一般地,所有的 Linux 发布也提供了一个系统工具 sysctl,它可以设置和读取内核的配置参数,但是该工具依赖于 proc 文件系统,为了使用该工具,内核必须支持 proc 文件系统。下面是使用 sysctl 工具来获取和设置内核配置参数的例子:


$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
$ sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

注意,参数 net.ipv4.ip_forward 实际被转换到对应的 proc 文件/proc/sys/net/ipv4/ip_forward,选项 -w 表示设置该内核配置参数,没有选项表示读内核配置参数,用户可以使用 sysctl -a 来读取所有的内核配置参数,对应更多的 sysctl 工具的信息,请参考手册页 sysctl(8)。

但是 proc 文件系统对 sysctl 不是必须的,在没有 proc 文件系统的情况下,仍然可以,这时需要使用内核提供的系统调用 sysctl 来实现对内核配置参数的设置和读取。

源代码包中给出了一个实际例子程序,它说明了如何在内核和用户态使用sysctl。头文件 sysctl-exam.h 定义了 sysctl 条目 ID,用户态应用和内核模块需要这些 ID 来操作和注册 sysctl 条目。内核模块在文件 sysctl-exam-kern.c 中实现,在该内核模块中,每一个 sysctl 条目对应一个 struct ctl_table 结构,该结构定义了要注册的 sysctl 条目的 ID(字段 ctl_name),在 proc 下的名称(字段procname),对应的内核变量(字段data,注意该该字段的赋值必须是指针),条目允许的最大长度(字段maxlen,它主要用于字符串内核变量,以便在对该条目设置时,对超过该最大长度的字符串截掉后面超长的部分),条目在proc文件系统下的访问权限(字段mode),在通过proc设置时的处理函数(字段proc_handler,对于整型内核变量,应当设置为&proc_dointvec,而对于字符串内核变量,则设置为 &proc_dostring),字符串处理策略(字段strategy,一般这是为&sysctl_string)。

Sysctl 条目可以是目录,此时 mode 字段应当设置为 0555,否则通过 sysctl 系统调用将无法访问它下面的 sysctl 条目,child 则指向该目录条目下面的所有条目,对于在同一目录下的多个条目,不必一一注册,用户可以把它们组织成一个 struct ctl_table 类型的数组,然后一次注册就可以,但此时必须把数组的最后一个结构设置为NULL,即


{
	.ctl_name = 0
}

注册sysctl条目使用函数register_sysctl_table(struct ctl_table *, int),第一个参数为定义的struct ctl_table结构的sysctl条目或条目数组指针,第二个参数为插入到sysctl条目表中的位置,如果插入到末尾,应当为0,如果插入到开头,则为非0。内核把所有的sysctl条目都组织成sysctl表。

当模块卸载时,需要使用函数unregister_sysctl_table(struct ctl_table_header *)解注册通过函数register_sysctl_table注册的sysctl条目,函数register_sysctl_table在调用成功时返回结构struct ctl_table_header,它就是sysctl表的表头,解注册函数使用它来卸载相应的sysctl条目。用户态应用sysctl-exam-user.c通过sysctl系统调用来查看和设置前面内核模块注册的sysctl条目(当然如果用户的系统内核已经支持proc文件系统,可以直接使用文件操作应用如cat, echo等直接查看和设置这些sysctl条目)。

下面是作者运行该模块与应用的输出结果示例:


$ insmod ./sysctl-exam-kern.ko
$ cat /proc/sys/mysysctl/myint
0
$ cat /proc/sys/mysysctl/mystring
$ ./sysctl-exam-user
mysysctl.myint = 0
mysysctl.mystring = ""
$ ./sysctl-exam-user 100 "Hello, World"
old value: mysysctl.myint = 0
new value: mysysctl.myint = 100
old vale: mysysctl.mystring = ""
new value: mysysctl.mystring = "Hello, World"
$ cat /proc/sys/mysysctl/myint
100
$ cat /proc/sys/mysysctl/mystring
Hello, World
$





回页首


五、系统调用

系统调用是内核提供给应用程序的接口,应用对底层硬件的操作大部分都是通过调用系统调用来完成的,例如得到和设置系统时间,就需要分别调用 gettimeofday 和 settimeofday 来实现。事实上,所有的系统调用都涉及到内核与应用之间的数据交换,如文件系统操作函数 read 和 write,设置和读取网络协议栈的 setsockopt 和 getsockopt。本节并不是讲解如何增加新的系统调用,而是讲解如何利用现有系统调用来实现用户的数据传输需求。

一般地,用户可以建立一个伪设备来作为应用与内核之间进行数据交换的渠道,最通常的做法是使用伪字符设备,具体实现方法是:

1.定义对字符设备进行操作的必要函数并设置结构 struct file_operations

结构 struct file_operations 非常大,对于一般的数据交换需求,只定义 open, read, write, ioctl, mmap 和 release 函数就足够了,它们实际上对应于用户态的文件系统操作函数 open, read, write, ioctl, mmap 和 close。这些函数的原型示例如下:


ssize_t exam_read (struct file * file, char __user * buf, size_t count, loff_t * ppos)
{
…
}
ssize_t exam_write(struct file * file, const char __user * buf, size_t count, loff_t * ppos)

               {
…
}
int exam_ioctl(struct inode * inode, struct file * file, unsigned int cmd, unsigned long argv)

               {
…
}
int exam_mmap(struct file *, struct vm_area_struct *)
{
…
}
int exam_open(struct inode * inode, struct file * file)
{
…
}
int exam_release(struct inode * inode, struct file * file)
{
…
}

在定义了这些操作函数后需要定义并设置结构struct file_operations


struct file_operations exam_file_ops = {
	.owner = THIS_MODULE,
	.read = exam_read,
	.write = exam_write,
	.ioctl = exam_ioctl,
	.mmap = exam_mmap,
	.open = exam_open,
	.release = exam_release,
};

2. 注册定义的伪字符设备并把它和上面的 struct file_operations 关联起来:


int exam_char_dev_major;
exam_char_dev_major = register_chrdev(0, "exam_char_dev", &exam_file_ops);

注意,函数 register_chrdev 的第一个参数如果为 0,表示由内核来确定该注册伪字符设备的主设备号,这是该函数的返回为实际分配的主设备号,如果返回小于 0,表示注册失败。因此,用户在使用该函数时必须判断返回值以便处理失败情况。为了使用该函数必须包含头文件 linux/fs.h。

在源代码包中给出了一个使用这种方式实现用户态与内核态数据交换的典型例子,它包含了三个文件:头文件 syscall-exam.h 定义了 ioctl 命令,.c 文件 syscall-exam-user.c为用户态应用,它通过文件系统操作函数 mmap 和 ioctl 来与内核态模块交换数据,.c 文件 syscall-exam-kern.c 为内核模块,它实现了一个伪字符设备,以便与用户态应用进行数据交换。为了正确运行应用程序 syscall-exam-user,需要在插入模块 syscall-exam-kern 后创建该实现的伪字符设备,用户可以使用下面命令来正确创建设备:


$ mknod /dev/mychrdev c `dmesg | grep "char device mychrdev" | sed 's/.*major is //g'` 0

然后用户可以通过 cat 来读写 /dev/mychrdev,应用程序 syscall-exam-user则使用 mmap 来读数据并使用 ioctl 来得到该字符设备的信息以及裁减数据内容,它只是示例如何使用现有的系统调用来实现用户需要的数据交互操作。

下面是作者运行该模块的结果示例:


$ insmod ./syscall-exam-kern.ko
char device mychrdev is registered, major is 254
$ mknod /dev/mychrdev c `dmesg | grep "char device mychrdev" | sed 's/.*major is //g'` 0
$ cat /dev/mychrdev
$ echo "abcdefghijklmnopqrstuvwxyz" > /dev/mychrdev
$ cat /dev/mychrdev
abcdefghijklmnopqrstuvwxyz
$ ./syscall-exam-user
User process: syscall-exam-us(1433)
Available space: 65509 bytes
Data len: 27 bytes
Offset in physical: cc0 bytes
mychrdev content by mmap:
abcdefghijklmnopqrstuvwxyz
$ cat /dev/mychrdev
abcde
$





回页首


六、netlink

Netlink 是一种特殊的 socket,它是 Linux 所特有的,类似于 BSD 中的AF_ROUTE 但又远比它的功能强大,目前在最新的 Linux 内核(2.6.14)中使用netlink 进行应用与内核通信的应用很多,包括:路由 daemon(NETLINK_ROUTE),1-wire 子系统(NETLINK_W1),用户态 socket 协议(NETLINK_USERSOCK),防火墙(NETLINK_FIREWALL),socket 监视(NETLINK_INET_DIAG),netfilter 日志(NETLINK_NFLOG),ipsec 安全策略(NETLINK_XFRM),SELinux 事件通知(NETLINK_SELINUX),iSCSI 子系统(NETLINK_ISCSI),进程审计(NETLINK_AUDIT),转发信息表查询(NETLINK_FIB_LOOKUP),netlink connector(NETLINK_CONNECTOR),netfilter 子系统(NETLINK_NETFILTER),IPv6 防火墙(NETLINK_IP6_FW),DECnet 路由信息(NETLINK_DNRTMSG),内核事件向用户态通知(NETLINK_KOBJECT_UEVENT),通用 netlink(NETLINK_GENERIC)。

Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。

Netlink 相对于系统调用,ioctl 以及 /proc 文件系统而言具有以下优点:

1,为了使用 netlink,用户仅需要在 include/linux/netlink.h 中增加一个新类型的 netlink 协议定义即可, 如 #define NETLINK_MYTEST 17 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换。但系统调用需要增加新的系统调用,ioctl 则需要增加设备或文件, 那需要不少代码,proc 文件系统则需要在 /proc 下添加新的文件或目录,那将使本来就混乱的 /proc 更加混乱。

2. netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息,但系统调用与 ioctl 则是同步通信机制,如果传递的数据太长,将影响调度粒度。

3.使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖,但系统调用就有依赖,而且新的系统调用的实现必须静态地连接到内核中,它无法在模块中实现,使用新系统调用的应用在编译时需要依赖内核。

4.netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性,任何对内核事件感兴趣的应用都能收到该子系统发送的内核事件,在后面的文章中将介绍这一机制的使用。

5.内核可以使用 netlink 首先发起会话,但系统调用和 ioctl 只能由用户应用发起调用。

6.netlink 使用标准的 socket API,因此很容易使用,但系统调用和 ioctl则需要专门的培训才能使用。

用户态使用 netlink

用户态应用使用标准的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和 close() 就能很容易地使用 netlink socket,查询手册页可以了解这些函数的使用细节,本文只是讲解使用 netlink 的用户应该如何使用这些函数。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket 需要的头文件也必不可少,sys/socket.h。

为了创建一个 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() 用于把一个打开的 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 需如下设置:


struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);

其中 nladdr 为消息接收者的 netlink 地址。

struct nlmsghdr 为 netlink socket 自己的消息头,这用于多路复用和多路分解 netlink 定义的所有协议类型以及其它一些控制,netlink 的内核实现将利用这个消息头来多路复用和多路分解已经其它的一些控制,因此它也被称为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 用于设置消息标志,可用的标志包括:


/* Flags values */
#define NLM_F_REQUEST           1       /* It is request message.       */
#define NLM_F_MULTI             2       /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK               4       /* Reply with ack, with zero or error code */
#define NLM_F_ECHO              8       /* Echo this request            */
/* Modifiers to GET request */
#define NLM_F_ROOT      0x100   /* specify tree root    */
#define NLM_F_MATCH     0x200   /* return all matching  */
#define NLM_F_ATOMIC    0x400   /* atomic GET           */
#define NLM_F_DUMP      (NLM_F_ROOT|NLM_F_MATCH)
/* Modifiers to NEW request */
#define NLM_F_REPLACE   0x100   /* Override existing            */
#define NLM_F_EXCL      0x200   /* Do not touch, if it exists   */
#define NLM_F_CREATE    0x400   /* Create, if it does not exist */
#define NLM_F_APPEND    0x800   /* Add to end of list           */

标志NLM_F_REQUEST用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。

标志NLM_F_MULTI 用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得。

宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来。

标志NLM_F_ECHO表示该消息是相关的一个包的回传。

标志NLM_F_ROOT 被许多 netlink 协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型。

标志 NLM_F_MATCH 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。

标志 NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。

标志 NLM_F_DUMP 未实现。

标志 NLM_F_REPLACE 用于取代在数据表中的现有条目。

标志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败。

标志 NLM_F_CREATE 指示应当在指定的表中创建一个条目。

标志 NLM_F_APPEND 指示在表末尾添加新的条目。

内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 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() 来接收。


#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); 

注意:fd为socket调用打开的netlink socket描述符。

在消息接收后,nlhdr指向接收到的消息的消息头,nladdr保存了接收到的消息的目标地址,宏NLMSG_DATA(nlhdr)返回指向消息的数据部分的指针。

在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的长度。

函数close用于关闭打开的netlink socket。

netlink内核API

netlink的内核实现在.c文件net/core/af_netlink.c中,内核模块要想使用netlink,也必须包含头文件linux/netlink.h。内核使用netlink需要专门的API,这完全不同于用户态应用对netlink的使用。如果用户需要增加新的netlink协议类型,必须通过修改linux/netlink.h来实现,当然,目前的netlink实现已经包含了一个通用的协议类型NETLINK_GENERIC以方便用户使用,用户可以直接使用它而不必增加新的协议类型。前面讲到,为了增加新的netlink协议类型,用户仅需增加如下定义到linux/netlink.h就可以:


#define NETLINK_MYTEST  17

只要增加这个定义之后,用户就可以在内核的任何地方引用该协议。

在内核中,为了创建一个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结构来表示。下面是一个input函数的示例:


void input (struct sock *sk, int len)
{
 struct sk_buff *skb;
 struct nlmsghdr *nlh = NULL;
 u8 *data = NULL;
 while ((skb = skb_dequeue(&sk->receive_queue)) 
       != NULL) {
 /* process netlink message pointed by skb->data */
 nlh = (struct nlmsghdr *)skb->data;
 data = NLMSG_DATA(nlh);
 /* process netlink message with header pointed by 
  * nlh and data pointed by data
  */
 }   
}

函数input()会在发送进程执行sendmsg()时被调用,这样处理消息比较及时,但是,如果消息特别长时,这样处理将增加系统调用sendmsg()的执行时间,对于这种情况,可以定义一个内核线程专门负责消息接收,而函数input的工作只是唤醒该内核线程,这样sendmsg将很快返回。

函数skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收队列上的消息,返回为一个struct sk_buff的结构,skb->data指向实际的netlink消息。

函数skb_recv_datagram(nl_sk)也用于在netlink socket nl_sk上接收消息,与skb_dequeue的不同指出是,如果socket的接收队列上没有消息,它将导致调用进程睡眠在等待队列nl_sk->sk_sleep,因此它必须在进程上下文使用,刚才讲的内核线程就可以采用这种方式来接收消息。

下面的函数input就是这种使用的示例:


void input (struct sock *sk, int len)
{
  wake_up_interruptible(sk->sk_sleep);
}

当内核中发送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用于非原子上下文。

在内核中使用函数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()的返回值。

源代码包中给出了一个使用 netlink 的示例,它包括一个内核模块 netlink-exam-kern.c 和两个应用程序 netlink-exam-user-recv.c, netlink-exam-user-send.c。内核模块必须先插入到内核,然后在一个终端上运行用户态接收程序,在另一个终端上运行用户态发送程序,发送程序读取参数指定的文本文件并把它作为 netlink 消息的内容发送给内核模块,内核模块接受该消息保存到内核缓存中,它也通过proc接口出口到 procfs,因此用户也能够通过 /proc/netlink_exam_buffer 看到全部的内容,同时内核也把该消息发送给用户态接收程序,用户态接收程序将把接收到的内容输出到屏幕上。





回页首


小结

本文是系列文章的第一篇,它详细介绍了五种用户空间与内核空间的数据交换方式,并通过实际例子程序向读者讲解了如何在内核开发中使用这些技术,其中内核启动参数方式是单向的,即只能向内核传递,而不能从内核获取,其余的均可以进行双向数据交换,即既可以从用户应用传递给内核,有可以从内核传递给应用态应用。netlink 是一种双向的数据交换方式,它使用起来非常简单高效,特别是它的广播特性在一些应用中非常方便。作者认为,它是所有这些用户态与内核态数据交换方式中最有效的最强大的方式。

该系列文章的第二篇将详细地讲解另外三种用户态与内核态的数据交换方式,包括 procfs、seq_file、debugfs 和 relayfs,有兴趣的读者请参看该系列文章第二篇。



参考资料

  1. Linux 2.6.13的内核源码,http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.13.tar.bz2
  2. 内核文档,Documentation/filesystems/sysfs.txt。
  3. Linux Device Drivers, Third Edition, http://lwn.net/Kernel/LDD3
  4. 内核文档,Documentation/sysctl/*。
  5. Linux Kernel Module Programming Guide, http://ldp.icenet.is/LDP/lkmpg/2.6/html/lkmpg.html
  6. [6] Linux 2.6.14的内核源码,http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.14.tar.bz2
  7. http://www.wlug.org.nz/LinuxNetlinkSockets
  8. Linux Device Drivers, Third Edition, Kernel Korner - Why and How to Use Netlink Socket, http://www.linuxjournal.com/article/7356
  9. netlink手册, netlink(7)。
  10. Extending netlink, http://lwn.net/Articles/131802/


关于作者

杨燚,计算机科学硕士,毕业于中科院计算技术研究所,有4年的Linux内核编程经验,目前从事嵌入式实时Linux的开发与性能测试。您可以通过 [email protected] 与作者联系。

 

 

 

 

 

 

 

 

 

在 Linux 下用户空间与内核空间数据交换的方式,第 2 部分: procfs、seq_file、debugfs和relayfs

developerWorks
文档选项
将打印机的版面设置成横向打印模式

打印本页

将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


级别: 初级

燚 杨 ([email protected]), 计算机科学硕士

2006 年 2 月 16 日

本系列文章包括两篇,它们文详细地地介绍了Linux系统下用户空间与内核空间数据交换的九种方式,包括内核启动参数、模块参数与sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,并给出具体的例子帮助读者掌握这些技术的使用。
本文是该系列文章的第二篇,它介绍了procfs、seq_file、debugfs和relayfs,并结合给出的例子程序详细地说明了它们如何使用。

一、procfs

procfs是比较老的一种用户态与内核态的数据交换方式,内核的很多数据都是通过这种方式出口给用户的,内核的很多参数也是通过这种方式来让用户方便设置的。除了sysctl出口到/proc下的参数,procfs提供的大部分内核参数是只读的。实际上,很多应用严重地依赖于procfs,因此它几乎是必不可少的组件。前面部分的几个例子实际上已经使用它来出口内核数据,但是并没有讲解如何使用,本节将讲解如何使用procfs。

Procfs提供了如下API:


struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,
                                          struct proc_dir_entry *parent)

该函数用于创建一个正常的proc条目,参数name给出要建立的proc条目的名称,参数mode给出了建立的该proc条目的访问权限,参数parent指定建立的proc条目所在的目录。如果要在/proc下建立proc条目,parent应当为NULL。否则它应当为proc_mkdir返回的struct proc_dir_entry结构的指针。


extern void remove_proc_entry(const char *name, struct proc_dir_entry *parent)

该函数用于删除上面函数创建的proc条目,参数name给出要删除的proc条目的名称,参数parent指定建立的proc条目所在的目录。


struct proc_dir_entry *proc_mkdir(const char * name, struct proc_dir_entry *parent)

该函数用于创建一个proc目录,参数name指定要创建的proc目录的名称,参数parent为该proc目录所在的目录。


extern struct proc_dir_entry *proc_mkdir_mode(const char *name, mode_t mode,
                        struct proc_dir_entry *parent);
struct proc_dir_entry *proc_symlink(const char * name,
                struct proc_dir_entry * parent, const char * dest)

该函数用于建立一个proc条目的符号链接,参数name给出要建立的符号链接proc条目的名称,参数parent指定符号连接所在的目录,参数dest指定链接到的proc条目名称。


struct proc_dir_entry *create_proc_read_entry(const char *name,
        mode_t mode, struct proc_dir_entry *base,
        read_proc_t *read_proc, void * data)

该函数用于建立一个规则的只读proc条目,参数name给出要建立的proc条目的名称,参数mode给出了建立的该proc条目的访问权限,参数base指定建立的proc条目所在的目录,参数read_proc给出读去该proc条目的操作函数,参数data为该proc条目的专用数据,它将保存在该proc条目对应的struct file结构的private_data字段中。


struct proc_dir_entry *create_proc_info_entry(const char *name,
        mode_t mode, struct proc_dir_entry *base, get_info_t *get_info)

该函数用于创建一个info型的proc条目,参数name给出要建立的proc条目的名称,参数mode给出了建立的该proc条目的访问权限,参数base指定建立的proc条目所在的目录,参数get_info指定该proc条目的get_info操作函数。实际上get_info等同于read_proc,如果proc条目没有定义个read_proc,对该proc条目的read操作将使用get_info取代,因此它在功能上非常类似于函数create_proc_read_entry。


struct proc_dir_entry *proc_net_create(const char *name,
        mode_t mode, get_info_t *get_info)

该函数用于在/proc/net目录下创建一个proc条目,参数name给出要建立的proc条目的名称,参数mode给出了建立的该proc条目的访问权限,参数get_info指定该proc条目的get_info操作函数。


struct proc_dir_entry *proc_net_fops_create(const char *name,
        mode_t mode, struct file_operations *fops)

该函数也用于在/proc/net下创建proc条目,但是它也同时指定了对该proc条目的文件操作函数。


void proc_net_remove(const char *name)

该函数用于删除前面两个函数在/proc/net目录下创建的proc条目。参数name指定要删除的proc名称。

除了这些函数,值得一提的是结构struct proc_dir_entry,为了创建一了可写的proc条目并指定该proc条目的写操作函数,必须设置上面的这些创建proc条目的函数返回的指针指向的struct proc_dir_entry结构的write_proc字段,并指定该proc条目的访问权限有写权限。

为了使用这些接口函数以及结构struct proc_dir_entry,用户必须在模块中包含头文件linux/proc_fs.h。

在源代码包中给出了procfs示例程序procfs_exam.c,它定义了三个proc文件条目和一个proc目录条目,读者在插入该模块后应当看到如下结构:


$ ls /proc/myproctest
aint		astring		bigprocfile
$

读者可以通过cat和echo等文件操作函数来查看和设置这些proc文件。特别需要指出,bigprocfile是一个大文件(超过一个内存页),对于这种大文件,procfs有一些限制,因为它提供的缓存,只有一个页,因此必须特别小心,并对超过页的部分做特别的考虑,处理起来比较复杂并且很容易出错,所有procfs并不适合于大数据量的输入输出,后面一节seq_file就是因为这一缺陷而设计的,当然seq_file依赖于procfs的一些基础功能。





回页首


二、seq_file

一般地,内核通过在procfs文件系统下建立文件来向用户空间提供输出信息,用户空间可以通过任何文本阅读应用查看该文件信息,但是procfs有一个缺陷,如果输出内容大于1个内存页,需要多次读,因此处理起来很难,另外,如果输出太大,速度比较慢,有时会出现一些意想不到的情况,Alexander Viro实现了一套新的功能,使得内核输出大文件信息更容易,该功能出现在2.4.15(包括2.4.15)以后的所有2.4内核以及2.6内核中,尤其是在2.6内核中,已经大量地使用了该功能。

要想使用seq_file功能,开发者需要包含头文件linux/seq_file.h,并定义与设置一个seq_operations结构(类似于file_operations结构):


struct seq_operations {
        void * (*start) (struct seq_file *m, loff_t *pos);
        void (*stop) (struct seq_file *m, void *v);
        void * (*next) (struct seq_file *m, void *v, loff_t *pos);
        int (*show) (struct seq_file *m, void *v);
};

start函数用于指定seq_file文件的读开始位置,返回实际读开始位置,如果指定的位置超过文件末尾,应当返回NULL,start函数可以有一个特殊的返回SEQ_START_TOKEN,它用于让show函数输出文件头,但这只能在pos为0时使用,next函数用于把seq_file文件的当前读位置移动到下一个读位置,返回实际的下一个读位置,如果已经到达文件末尾,返回NULL,stop函数用于在读完seq_file文件后调用,它类似于文件操作close,用于做一些必要的清理,如释放内存等,show函数用于格式化输出,如果成功返回0,否则返回出错码。

Seq_file也定义了一些辅助函数用于格式化输出:


int seq_putc(struct seq_file *m, char c);

函数seq_putc用于把一个字符输出到seq_file文件。


int seq_puts(struct seq_file *m, const char *s);

函数seq_puts则用于把一个字符串输出到seq_file文件。


int seq_escape(struct seq_file *, const char *, const char *);

函数seq_escape类似于seq_puts,只是,它将把第一个字符串参数中出现的包含在第二个字符串参数中的字符按照八进制形式输出,也即对这些字符进行转义处理。


int seq_printf(struct seq_file *, const char *, ...)
        __attribute__ ((format (printf,2,3)));

函数seq_printf是最常用的输出函数,它用于把给定参数按照给定的格式输出到seq_file文件。


int seq_path(struct seq_file *, struct vfsmount *, struct dentry *, char *);

函数seq_path则用于输出文件名,字符串参数提供需要转义的文件名字符,它主要供文件系统使用。

在定义了结构struct seq_operations之后,用户还需要把打开seq_file文件的open函数,以便该结构与对应于seq_file文件的struct file结构关联起来,例如,struct seq_operations定义为:


struct seq_operations exam_seq_ops = {
	.start = exam_seq_start,
   .stop = exam_seq_stop,
   .next = exam_seq_next,
   .show = exam_seq_show
};

那么,open函数应该如下定义:


static int exam_seq_open(struct inode *inode, struct file *file)
{
        return seq_open(file, &exam_seq_ops);
};

注意,函数seq_open是seq_file提供的函数,它用于把struct seq_operations结构与seq_file文件关联起来。最后,用户需要如下设置struct file_operations结构:


struct file_operations exam_seq_file_ops = {
        .owner   = THIS_MODULE,
        .open    = exm_seq_open,
        .read    = seq_read,
        .llseek  = seq_lseek,
        .release = seq_release
};

注意,用户仅需要设置open函数,其它的都是seq_file提供的函数。

然后,用户创建一个/proc文件并把它的文件操作设置为exam_seq_file_ops即可:


struct proc_dir_entry *entry;
entry = create_proc_entry("exam_seq_file", 0, NULL);
if (entry)
entry->proc_fops = &exam_seq_file_ops;

对于简单的输出,seq_file用户并不需要定义和设置这么多函数与结构,它仅需定义一个show函数,然后使用single_open来定义open函数就可以,以下是使用这种简单形式的一般步骤:

1.定义一个show函数


int exam_show(struct seq_file *p, void *v)
{
…
}

2. 定义open函数


int exam_single_open(struct inode *inode, struct file *file)
{
        return(single_open(file, exam_show, NULL));
}

注意要使用single_open而不是seq_open。

3. 定义struct file_operations结构


struct file_operations exam_single_seq_file_operations = {
        .open           = exam_single_open,
        .read           = seq_read,
        .llseek         = seq_lseek,
        .release        = single_release,
};

注意,如果open函数使用了single_open,release函数必须为single_release,而不是seq_release。在源代码包中给出了一个使用seq_file的具体例子seqfile_exam.c,它使用seq_file提供了一个查看当前系统运行的所有进程的/proc接口,在编译并插入该模块后,用户通过命令"cat /proc/ exam_esq_file"可以查看系统的所有进程。





回页首


三、debugfs

内核开发者经常需要向用户空间应用输出一些调试信息,在稳定的系统中可能根本不需要这些调试信息,但是在开发过程中,为了搞清楚内核的行为,调试信息非常必要,printk可能是用的最多的,但它并不是最好的,调试信息只是在开发中用于调试,而printk将一直输出,因此开发完毕后需要清除不必要的printk语句,另外如果开发者希望用户空间应用能够改变内核行为时,printk就无法实现。因此,需要一种新的机制,那只有在需要的时候使用,它在需要时通过在一个虚拟文件系统中创建一个或多个文件来向用户空间应用提供调试信息。

有几种方式可以实现上述要求:

使用procfs,在/proc创建文件输出调试信息,但是procfs对于大于一个内存页(对于x86是4K)的输出比较麻烦,而且速度慢,有时回出现一些意想不到的问题。

使用sysfs(2.6内核引入的新的虚拟文件系统),在很多情况下,调试信息可以存放在那里,但是sysfs主要用于系统管理,它希望每一个文件对应内核的一个变量,如果使用它输出复杂的数据结构或调试信息是非常困难的。

使用libfs创建一个新的文件系统,该方法极其灵活,开发者可以为新文件系统设置一些规则,使用libfs使得创建新文件系统更加简单,但是仍然超出了一个开发者的想象。

为了使得开发者更加容易使用这样的机制,Greg Kroah-Hartman开发了debugfs(在2.6.11中第一次引入),它是一个虚拟文件系统,专门用于输出调试信息,该文件系统非常小,很容易使用,可以在配置内核时选择是否构件到内核中,在不选择它的情况下,使用它提供的API的内核部分不需要做任何改动。

使用debugfs的开发者首先需要在文件系统中创建一个目录,下面函数用于在debugfs文件系统下创建一个目录:


        struct dentry *debugfs_create_dir(const char *name, struct dentry *parent);
    

参数name是要创建的目录名,参数parent指定创建目录的父目录的dentry,如果为NULL,目录将创建在debugfs文件系统的根目录下。如果返回为-ENODEV,表示内核没有把debugfs编译到其中,如果返回为NULL,表示其他类型的创建失败,如果创建目录成功,返回指向该目录对应的dentry条目的指针。

下面函数用于在debugfs文件系统中创建一个文件:


        struct dentry *debugfs_create_file(const char *name, mode_t mode,
                                       struct dentry *parent, void *data,
                                       struct file_operations *fops);
    

参数name指定要创建的文件名,参数mode指定该文件的访问许可,参数parent指向该文件所在目录,参数data为该文件特定的一些数据,参数fops为实现在该文件上进行文件操作的fiel_operations结构指针,在很多情况下,由seq_file(前面章节已经讲过)提供的文件操作实现就足够了,因此使用debugfs很容易,当然,在一些情况下,开发者可能仅需要使用用户应用可以控制的变量来调试,debugfs也提供了4个这样的API方便开发者使用:


    struct dentry *debugfs_create_u8(const char *name, mode_t mode, 
                                     struct dentry *parent, u8 *value);
    struct dentry *debugfs_create_u16(const char *name, mode_t mode, 
                                      struct dentry *parent, u16 *value);
    struct dentry *debugfs_create_u32(const char *name, mode_t mode, 
                                      struct dentry *parent, u32 *value);
    struct dentry *debugfs_create_bool(const char *name, mode_t mode, 
struct dentry *parent, u32 *value);

参数name和mode指定文件名和访问许可,参数value为需要让用户应用控制的内核变量指针。

当内核模块卸载时,Debugfs并不会自动清除该模块创建的目录或文件,因此对于创建的每一个文件或目录,开发者必须调用下面函数清除:


        void debugfs_remove(struct dentry *dentry);
    

参数dentry为上面创建文件和目录的函数返回的dentry指针。

在源代码包中给出了一个使用debufs的示例模块debugfs_exam.c,为了保证该模块正确运行,必须让内核支持debugfs,debugfs是一个调试功能,因此它位于主菜单Kernel hacking,并且必须选择Kernel debugging选项才能选择,它的选项名称为Debug Filesystem。为了在用户态使用debugfs,用户必须mount它,下面是在作者系统上的使用输出:


$ mkdir -p /debugfs
$ mount -t debugfs debugfs /debugfs
$ insmod ./debugfs_exam.ko
$ ls /debugfs
debugfs-exam
$ ls /debugfs/debugfs-exam
u8_var		u16_var		u32_var		bool_var
$ cd /debugfs/debugfs-exam
$ cat u8_var
0
$ echo 200 > u8_var
$ cat u8_var
200
$ cat bool_var
N
$ echo 1 > bool_var
$ cat bool_var
Y





回页首


四、relayfs

relayfs是一个快速的转发(relay)数据的文件系统,它以其功能而得名。它为那些需要从内核空间转发大量数据到用户空间的工具和应用提供了快速有效的转发机制。

Channel是relayfs文件系统定义的一个主要概念,每一个channel由一组内核缓存组成,每一个CPU有一个对应于该channel的内核缓存,每一个内核缓存用一个在relayfs文件系统中的文件文件表示,内核使用relayfs提供的写函数把需要转发给用户空间的数据快速地写入当前CPU上的channel内核缓存,用户空间应用通过标准的文件I/O函数在对应的channel文件中可以快速地取得这些被转发出的数据mmap来。写入到channel中的数据的格式完全取决于内核中创建channel的模块或子系统。

relayfs的用户空间API:

relayfs实现了四个标准的文件I/O函数,open、mmap、poll和close

  • open(),打开一个channel在某一个CPU上的缓存对应的文件。
  • mmap(),把打开的channel缓存映射到调用者进程的内存空间。
  • read(),读取channel缓存,随后的读操作将看不到被该函数消耗的字节,如果channel的操作模式为非覆盖写,那么用户空间应用在有内核模块写时仍可以读取,但是如果channel的操作模式为覆盖式,那么在读操作期间如果有内核模块进行写,结果将无法预知,因此对于覆盖式写的channel,用户应当在确认在channel的写完全结束后再进行读。
  • poll(),用于通知用户空间应用转发数据跨越了子缓存的边界,支持的轮询标志有POLLIN、POLLRDNORM和POLLERR。
  • close(),关闭open函数返回的文件描述符,如果没有进程或内核模块打开该channel缓存,close函数将释放该channel缓存。

注意:用户态应用在使用上述API时必须保证已经挂载了relayfs文件系统,但内核在创建和使用channel时不需要relayfs已经挂载。下面命令将把relayfs文件系统挂载到/mnt/relay。


		mount -t relayfs relayfs /mnt/relay
	

relayfs内核API:

relayfs提供给内核的API包括四类:channel管理、写函数、回调函数和辅助函数。

Channel管理函数包括:

  • relay_open(base_filename, parent, subbuf_size, n_subbufs, overwrite, callbacks)
  • relay_close(chan)
  • relay_flush(chan)
  • relay_reset(chan)
  • relayfs_create_dir(name, parent)
  • relayfs_remove_dir(dentry)
  • relay_commit(buf, reserved, count)
  • relay_subbufs_consumed(chan, cpu, subbufs_consumed)

写函数包括:

  • relay_write(chan, data, length)
  • __relay_write(chan, data, length)
  • relay_reserve(chan, length)

回调函数包括:

  • subbuf_start(buf, subbuf, prev_subbuf_idx, prev_subbuf)
  • buf_mapped(buf, filp)
  • buf_unmapped(buf, filp)

辅助函数包括:

  • relay_buf_full(buf)
  • subbuf_start_reserve(buf, length)

前面已经讲过,每一个channel由一组channel缓存组成,每个CPU对应一个该channel的缓存,每一个缓存又由一个或多个子缓存组成,每一个缓存是子缓存组成的一个环型缓存。

函数relay_open用于创建一个channel并分配对应于每一个CPU的缓存,用户空间应用通过在relayfs文件系统中对应的文件可以访问channel缓存,参数base_filename用于指定channel的文件名,relay_open函数将在relayfs文件系统中创建base_filename0..base_filenameN-1,即每一个CPU对应一个channel文件,其中N为CPU数,缺省情况下,这些文件将建立在relayfs文件系统的根目录下,但如果参数parent非空,该函数将把channel文件创建于parent目录下,parent目录使用函数relay_create_dir创建,函数relay_remove_dir用于删除由函数relay_create_dir创建的目录,谁创建的目录,谁就负责在不用时负责删除。参数subbuf_size用于指定channel缓存中每一个子缓存的大小,参数n_subbufs用于指定channel缓存包含的子缓存数,因此实际的channel缓存大小为(subbuf_size x n_subbufs),参数overwrite用于指定该channel的操作模式,relayfs提供了两种写模式,一种是覆盖式写,另一种是非覆盖式写。使用哪一种模式完全取决于函数subbuf_start的实现,覆盖写将在缓存已满的情况下无条件地继续从缓存的开始写数据,而不管这些数据是否已经被用户应用读取,因此写操作决不失败。在非覆盖写模式下,如果缓存满了,写将失败,但内核将在用户空间应用读取缓存数据时通过函数relay_subbufs_consumed()通知relayfs。如果用户空间应用没来得及消耗缓存中的数据或缓存已满,两种模式都将导致数据丢失,唯一的区别是,前者丢失数据在缓存开头,而后者丢失数据在缓存末尾。一旦内核再次调用函数relay_subbufs_consumed(),已满的缓存将不再满,因而可以继续写该缓存。当缓存满了以后,relayfs将调用回调函数buf_full()来通知内核模块或子系统。当新的数据太大无法写入当前子缓存剩余的空间时,relayfs将调用回调函数subbuf_start()来通知内核模块或子系统将需要使用新的子缓存。内核模块需要在该回调函数中实现下述功能:

初始化新的子缓存;

如果1正确,完成当前子缓存;

如果2正确,返回是否正确完成子缓存切换;

在非覆盖写模式下,回调函数subbuf_start()应该如下实现:


static int subbuf_start(struct rchan_buf *buf,
                        void *subbuf,
			void *prev_subbuf,
			unsigned int prev_padding)
{
	if (prev_subbuf)
		*((unsigned *)prev_subbuf) = prev_padding;
	if (relay_buf_full(buf))
		return 0;
	subbuf_start_reserve(buf, sizeof(unsigned int));
	return 1;
}

如果当前缓存满,即所有的子缓存都没读取,该函数返回0,指示子缓存切换没有成功。当子缓存通过函数relay_subbufs_consumed()被读取后,读取者将负责通知relayfs,函数relay_buf_full()在已经有读者读取子缓存数据后返回0,在这种情况下,子缓存切换成功进行。

在覆盖写模式下, subbuf_start()的实现与非覆盖模式类似:


static int subbuf_start(struct rchan_buf *buf,
                        void *subbuf,
			void *prev_subbuf,
			unsigned int prev_padding)
{
	if (prev_subbuf)
		*((unsigned *)prev_subbuf) = prev_padding;
	subbuf_start_reserve(buf, sizeof(unsigned int));
	return 1;
}

只是不做relay_buf_full()检查,因为此模式下,缓存是环行的,可以无条件地写。因此在此模式下,子缓存切换必定成功,函数relay_subbufs_consumed() 也无须调用。如果channel写者没有定义subbuf_start(),缺省的实现将被使用。可以通过在回调函数subbuf_start()中调用辅助函数subbuf_start_reserve()在子缓存中预留头空间,预留空间可以保存任何需要的信息,如上面例子中,预留空间用于保存子缓存填充字节数,在subbuf_start()实现中,前一个子缓存的填充值被设置。前一个子缓存的填充值和指向前一个子缓存的指针一道作为subbuf_start()的参数传递给subbuf_start(),只有在子缓存完成后,才能知道填充值。subbuf_start()也被在channel创建时分配每一个channel缓存的第一个子缓存时调用,以便预留头空间,但在这种情况下,前一个子缓存指针为NULL。

内核模块使用函数relay_write()或__relay_write()往channel缓存中写需要转发的数据,它们的区别是前者失效了本地中断,而后者只抢占失效,因此前者可以在任何内核上下文安全使用,而后者应当在没有任何中断上下文将写channel缓存的情况下使用。这两个函数没有返回值,因此用户不能直接确定写操作是否失败,在缓存满且写模式为非覆盖模式时,relayfs将通过回调函数buf_full来通知内核模块。

函数relay_reserve()用于在channel缓存中预留一段空间以便以后写入,在那些没有临时缓存而直接写入channel缓存的内核模块可能需要该函数,使用该函数的内核模块在实际写这段预留的空间时可以通过调用relay_commit()来通知relayfs。当所有预留的空间全部写完并通过relay_commit通知relayfs后,relayfs将调用回调函数deliver()通知内核模块一个完整的子缓存已经填满。由于预留空间的操作并不在写channel的内核模块完全控制之下,因此relay_reserve()不能很好地保护缓存,因此当内核模块调用relay_reserve()时必须采取恰当的同步机制。

当内核模块结束对channel的使用后需要调用relay_close() 来关闭channel,如果没有任何用户在引用该channel,它将和对应的缓存全部被释放。

函数relay_flush()强制在所有的channel缓存上做一个子缓存切换,它在channel被关闭前使用来终止和处理最后的子缓存。

函数relay_reset()用于将一个channel恢复到初始状态,因而不必释放现存的内存映射并重新分配新的channel缓存就可以使用channel,但是该调用只有在该channel没有任何用户在写的情况下才可以安全使用。

回调函数buf_mapped() 在channel缓存被映射到用户空间时被调用。

回调函数buf_unmapped()在释放该映射时被调用。内核模块可以通过它们触发一些内核操作,如开始或结束channel写操作。

在源代码包中给出了一个使用relayfs的示例程序relayfs_exam.c,它只包含一个内核模块,对于复杂的使用,需要应用程序配合。该模块实现了类似于文章中seq_file示例实现的功能。

当然为了使用relayfs,用户必须让内核支持relayfs,并且要mount它,下面是作者系统上的使用该模块的输出信息:


$ mkdir -p /relayfs
$ insmod ./relayfs-exam.ko
$ mount -t relayfs relayfs /relayfs
$ cat /relayfs/example0
…
$

relayfs是一种比较复杂的内核态与用户态的数据交换方式,本例子程序只提供了一个较简单的使用方式,对于复杂的使用,请参考relayfs用例页面http://relayfs.sourceforge.net/examples.html





回页首


小结

本文是该系列文章最后一篇,它详细地讲解了其余四种用户空间与内核空间的数据交换方式,并通过实际例子程序向读者讲解了如何在内核开发中使用这些技术,其中seq_file是单向的,即只能向内核传递,而不能从内核获取,而另外三种方式均可以进行双向数据交换,即既可以从用户应用传递给内核,又可以从内核传递给应用态应用。procfs一般用于向用户出口少量的数据信息,或用户通过它设置内核变量从而控制内核行为。seq_file实际上依赖于procfs,因此为了使用seq_file,必须使内核支持procfs。debugfs用于内核开发者调试使用,它比其他集中方式都方便,但是仅用于简单类型的变量处理。relayfs是一种非常复杂的数据交换方式,要想准确使用并不容易,但是如果使用得当,它远比procfs和seq_file功能强大。



参考资料

  1. Linux 2.6.13的内核源码,http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.13.tar.bz2
  2. Linux 2.6.14的内核源码,http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.14.tar.bz2
  3. 内核文档,Documentation/filesystems/proc.txt。
  4. LWN, Driver porting: The seq_file interface, http://lwn.net/Articles/22355/
  5. LWN, debugfs, http://lwn.net/Articles/115405/
  6. Linux kernel procfs guide, http://kernelnewbies.org/documents/kdoc/procfs-guide/lkprocfsguide.html
  7. 内核文档,Documentation/filesystems/relayfs.txt。
  8. Relayfs project homepage, http://relayfs.sourceforge.net/
  9. relayfs usage examples, http://relayfs.sourceforge.net/examples.html
  10. relayfs source, http://prdownloads.sourceforge.net/relayfs/old-relayfs-051205-kernel-2.6.11.patch.bz2?download
  11. relayfs example applications, http://prdownloads.sourceforge.net/relayfs/relay-apps-0.91.tar.gz?download


关于作者

杨燚,计算机科学硕士,毕业于中科院计算技术研究所,有4年的Linux内核编程经验,目前从事嵌入式实时Linux的开发与性能测试。您可以通过 [email protected] 与作者联系。

 

 

 

 

推荐:Linux用户态与内核态的交互

http://www.souzz.net/html/server/Linux/15615_9.html

你可能感兴趣的:(Linux 系统内核空间与用户空间通信的实现与分析)