您的位置:首页 > 运维架构 > Linux

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

2011-06-28 00:29 821 查看
多数的 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】



其中相关的系统调用是需要用户自行编写并载入内核。 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】



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

3.2.2 netlink 套接字

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

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

图【3】



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

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

图【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-<receive_queue)) != NULL)
10:          {
11:          {
12:              struct nlmsghdr *nlh = NULL;
13:        if(skb-<len <= sizeof(struct nlmsghdr))
14:              {
15:                nlh = (struct nlmsghdr *)skb-<data;
16:                if((nlh-<nlmsg_len <= sizeof(struct nlmsghdr))
17:                   && (skb-<len <= nlh-<nlmsg_len))
18:                  {
19:                    if(nlh-<nlmsg_type == IMP2_U_PID)
20:                      {
21:                        write_lock_bh(&user_proc.pid);
22:                        user_proc.pid = nlh-<nlmsg_pid;
23:                write_unlock_bh(&user_proc.pid);
24:                      }
25:                    else if(nlh-<nlmsg_type == IMP2_CLOSE)
26:                      {
27:                        write_lock_bh(&user_proc.pid);
28:               if(nlh-<nlmsg_pid == user_proc.pid) user_proc.pid = 0;
29:                        write_unlock_bh(&user_proc.pid);
30:                      }
31:                  }
32:             }
33:          }
34:        kfree_skb(skb);
35:        }
36:        up(&receive_sem);
37:      }while(nlfd && nlfd-<receive_queue.qlen);
38:  }

如果读者看过 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 套接字是专为此类通信定制的。

作者: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

sys_mlock如果调用成功返回,这其中所有的包含具体内存区域的页必须是常驻内存的,或者说在调用munlock 或 munlockall之前这部分被锁住的页面必须保留在内存。当然,如果调用mlock的进程终止或者调用exec执行其他程序,则这部分被锁住的页面被 释放。通过fork()调用所创建的子进程不能够继承由父进程调用mlock锁住的页面。
内存屏蔽主要有两个方面的应用:实时算法和高度机密数据的处理。实时应用要求严格的分时,比如调度,调度页面是程序执行延时的一个主要因素。保密安全软件 经常处理关键字节,比如密码或者密钥等数据结构。页面调度的结果是有可能将这些重要字节写到外存(如硬盘)中去。这样一些黑客就有可能在这些安全软件删除 这些在内存中的数据后还能访问部分在硬盘中的数据。 而对内存进行加锁完全可以解决上述难题。
内存加锁不使用压栈技术,即那些通过调用mlock或者mlockall被锁住多次的页面可以通过调用一次munlock或者munlockall释放相应的页面
mlock的返回值分析:若调用mlock成功,则返回0;若不成功,则返回-1,并且errno被置位,进程的地址空间保持原来的状态。返回错误代码分析如下:
ENOMEM:部分具体地址区域没有相应的进程地址空间与之对应或者超出了进程所允许的最大可锁页面。
EPERM:调用mlock的进程没有正确的优先权。只有root进程才允许锁住要求的页面。
EINVAL:输入参数len不是个合法的正数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: