Linux 用户态与内核态的交互
在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的,例如iprote2网络管理工具,它与内核的交互就全部使用了netlink,著名的内核包过滤框架Netfilter在与用户空间的通读,也在最新版本中改变为netlink,无疑,它将是Linux用户态与内核态交流的主要方法之一。它的通信依据是一个对应于进程的标识,一般定为该进程的 ID。当通信的一端处于中断过程时,该标识为 0。当使用 netlink 套接字进行通信,通信的双方都是用户态进程,则使用方法类似于消息队列。但通信双方有一端是中断过程,使用方法则不同。netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数。
《UNIX Network Programming Volume 1 - 3rd Edition》第18章
讲到BSD UNIX系统中routing socket的应用,这种套接字是按下面方式生成的:
rt_socket = socket(AF_ROUTE, SOCK_RAW, 0);
然后就可以用它跟内核交互,进行网络环境管理的操作,如读取/设置/删除路由表信息,更改网关等等,但书中所列代码只在4.3BSD及以后版本的原始UNIX系统下可用,Linux虽然实现了AF_ROUTE族套接字,但用法却完全不同。由于网上这方面知识的资料想对匮乏,现对Linux下routing socket的使用做一介绍。
由于我现在在Magic Linux1.0下工作,所以以下的讲解全部基于2.4.10内核。Linux从v2.2开始引入这一机制,因此可以肯定从v2.2到v2.4的内核都是适用的,更新的v2.6我没有试过。
Linux下虽然也有AF_ROUTE族套接字,但是这个定义只是个别名,请看
/usr/include/linux/socket.h, line 145:
#define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */
可见在Linux内核当中真正实现routing socket的是AF_NETLINK族套接字。AF_NETLINK族套接字像一个连接用户空间和内核的双工管道,通过它,用户进程可以修改内核运行参数、读取和设置路由信息、控制特定网卡的up/down状态等等,可以说是一个管理网络资源的绝佳途径。
1 生成所需套接字,并绑定一个sockaddr结构
先来看如何生成一个AF_NETLINK族套接字:
sockfd = socket(AF_NETLINK, socket_type, netlink_faimly);
这里socket_type可选SOCK_DGRAM或SOCK_RAW;因为AF_NETLINK族是面向数据报的套接字,所以不能使用SOCK_STREAM。
netlink_family指定要和内核中的哪个子系统进行交互,目前支持:
NETLINK_ROUTE 与路由信息相关,包括查询、设置和删除路由表中的条目等。待会儿我们将以这类family举个实际的例子;
NETLINK_FIREWALL 接收由IPv4防火墙代码发送的包;
NETLINK_ARPD 可以在用户空间进行arp缓存的管理;
NETLINK_ROUTE6 在用户空间发送和接收路由表信息更新;
还有几种虽然没有实现,但已经有了定义,为以后扩展做好了准备。
接下来要给该套接字绑定一个sockaddr结构,实际上是一个sockaddr_nl结构:
struct sockaddr_nl {
sa_family_t nl_family; /*AF_NETLINK*/
unsigned short nl_pad; /* 0 */
pid_t nl_pid; /* 进程pid */
u_32 nl_groups; /* 多播组掩码*/
}nl;
这个结构一般按照注释填好就可以了,nl_groups我也不知道怎么用,一般填零了,表示没有多播。绑定:
bind(sockfd, (struct sockaddr*) &nl, sizeof(nl));
2 填充所需数据结构,并通过sendmsg()/send()等函数写到套接字里去
到此为止,与内核通信的准备工作就完成了,下面要做的工作是,选取适当的数据结构进行填充,并作为sendmsg()的参数发送出去,并recv()收到的消息。这个数据结构就是nlmsghdr,它只是一个信息头,后面可以接任意长的数据,这些数据实际上又是针对某一需求所采用的特定数据结构。先来看nlmsghdr:
struct nlmsghdr {
_u32 nlmsg_len; /* Length of msg including header */
_u32 nlmsg_type; /* 操作命令 */
_u16 nlmsg_flags; /* various flags */
_u32 nlmsg_seq; /* Sequence number */
_u32 nlmsg_pid; /* 进程PID */
};
/* 紧跟着是实际要发送的数据,长度可以任意 */
nlmsg_type决定这次要执行的操作,如查询当前路由表信息,所使用的就是RTM_GETROUTE。标准nlmsg_type包括:NLMSG_NOOP, NLMSG_DONE, NLMSG_ERROR等。根据采用的nlmsg_type不同,还要选取不同的数据结构来填充到nlmsghdr后面:
操作 数据结构
RTM_NEWLINK ifinfomsg
RTM_DELLINK
RTM_GETLINK
RTM_NEWADDR ifaddrmsg
RTM_DELADDR
RTM_GETADDR
RTM_NEWROUTE rtmsg
RTM_DELROUTE
RTM_GETROUTE
RTM_NEWNEIGH ndmsg/nda_chcheinfo
RTM_DELNEIGH
RTM_GETNEIGH
RTM_NEWRULE rtmsg
RTM_DELRULE
RTM_GETRULE
RTM_NEWQDISC tcmsg
RTM_DELQDISC
RTM_GETQDISC
RTM_NEWTCLASS tcmsg
RTM_DELTCLASS
RTM_GETTCLASS
RTM_NEWTFILTER tcmsg
RTM_DELTFILTER
RTM_GETTFILTER
由于情形众多,我从现在开始将用一个特定的例子来说明问题。我们的目的是从内核读取IPV4路由表信息。从上面表看,nlmsg_type一定使用RTM_xxxROUTE操作,对应的数据结构是rtmsg。既然是读取,那么应该是RTM_GETROUTE了。
struct rtmsg {
unsigned char rtm_family; /* 路由表地址族 */
unsigned char rtm_dst_len; /* 目的长度 */
unsigned char rtm_src_len; /* 源长度 */ (2.4.10头文件的注释标反了?)
unsigned char rtm_tos; /* TOS */
unsigned char rtm_table; /* 路由表选取 */
unsigned char rtm_protocol; /* 路由协议 */
unsigned char rtm_scope;
unsigned char rtm_type;
unsigned int rtm_flags;
};
对于RTM_GETROUTE操作来说,我们只需指定两个成员:rtm_family:AF_INET, rtm_table: RT_TABLE_MAIN。其他成员都初始化为0即可。将这个结构体跟nlmsghdr结合起来,得到我们自己的新结构体:
struct {
struct nlmsghdr nl;
struct rtmsg rt;
}req;
填充好rt结构之后,还要调整nl结构相应成员的值。Linux定义了多个宏来处理nlmsghdr成员的值,我们这里用到的是NLMSG_LENGTH(size_t len);
req.nl.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
这将计算nlmsghdr长度与rtmsg长度的和(其中包括了将rtmsg进行4字节边界对齐的调整),并存储到nlmsghdr的nlmsg_len成员中。接下来要做的就是将这个新结构体req放到sendmsg()函数的msghdr.iov处,并调用函数。
sendmsg(sockfd, &msg, 0);
3 接收数据,并进行分析
接下来的操作是recv()操作,从该套接字读取内核返回的数据,并进行分析处理。
recv(sockfd, p, sizeof(buf) - nll, 0);
其中p是指向一个缓冲区buf的指针,nll是已接收到的nlmsghdr数据的长度。
由于内核返回信息是一个字节流,需要调用者检查消息结尾。这是通过检查返回的nlmsghdr的nlmsg_type是否等于NLMSG_DONE来完成的。返回的数据格式如下:
-----------------------------------------------------------
| nlmsghdr+route entry | nlmsghdr+route entry | .........
-----------------------------------------------------------
| 解出route entry
V
-----------------------------------------------------------
| dst_addr | gateway | Output interface| ...............
-----------------------------------------------------------
可以看出,返回消息由多个(nlmsghdr + route entry)组成,当某个nlmsghdr的nlmsg_type == NLMSG_DONE时就表示信息输出已经完毕。而每一个route entry由多个rtattr结构体组成,每个结构体表示该路由项的某个属性,如目的地址,网关等等。根据这个示意图我们就能够轻松解析需要的数据了。
在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的,例如iprote2网络管理工具,它与内核的交互就全部使用了netlink,著名的内核包过滤框架Netfilter在与用户空间的通读,也在最新版本中改变为netlink,无疑,它将是Linux用户态与内核态交流的主要方法之一。它的通信依据是一个对应于进程的标识,一般定为该进程的 ID。当通信的一端处于中断过程时,该标识为 0。当使用 netlink 套接字进行通信,通信的双方都是用户态进程,则使用方法类似于消息队列。但通信双方有一端是中断过程,使用方法则不同。netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数。
《UNIX Network Programming Volume 1 - 3rd Edition》第18章
讲到BSD UNIX系统中routing socket的应用,这种套接字是按下面方式生成的:
rt_socket = socket(AF_ROUTE, SOCK_RAW, 0);
然后就可以用它跟内核交互,进行网络环境管理的操作,如读取/设置/删除路由表信息,更改网关等等,但书中所列代码只在4.3BSD及以后版本的原始UNIX系统下可用,Linux虽然实现了AF_ROUTE族套接字,但用法却完全不同。由于网上这方面知识的资料想对匮乏,现对Linux下routing socket的使用做一介绍。
由于我现在在Magic Linux1.0下工作,所以以下的讲解全部基于2.4.10内核。Linux从v2.2开始引入这一机制,因此可以肯定从v2.2到v2.4的内核都是适用的,更新的v2.6我没有试过。
Linux下虽然也有AF_ROUTE族套接字,但是这个定义只是个别名,请看
/usr/include/linux/socket.h, line 145:
#define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */
可见在Linux内核当中真正实现routing socket的是AF_NETLINK族套接字。AF_NETLINK族套接字像一个连接用户空间和内核的双工管道,通过它,用户进程可以修改内核运行参数、读取和设置路由信息、控制特定网卡的up/down状态等等,可以说是一个管理网络资源的绝佳途径。
1 生成所需套接字,并绑定一个sockaddr结构
先来看如何生成一个AF_NETLINK族套接字:
sockfd = socket(AF_NETLINK, socket_type, netlink_faimly);
这里socket_type可选SOCK_DGRAM或SOCK_RAW;因为AF_NETLINK族是面向数据报的套接字,所以不能使用SOCK_STREAM。
netlink_family指定要和内核中的哪个子系统进行交互,目前支持:
NETLINK_ROUTE 与路由信息相关,包括查询、设置和删除路由表中的条目等。待会儿我们将以这类family举个实际的例子;
NETLINK_FIREWALL 接收由IPv4防火墙代码发送的包;
NETLINK_ARPD 可以在用户空间进行arp缓存的管理;
NETLINK_ROUTE6 在用户空间发送和接收路由表信息更新;
还有几种虽然没有实现,但已经有了定义,为以后扩展做好了准备。
接下来要给该套接字绑定一个sockaddr结构,实际上是一个sockaddr_nl结构:
struct sockaddr_nl {
sa_family_t nl_family; /*AF_NETLINK*/
unsigned short nl_pad; /* 0 */
pid_t nl_pid; /* 进程pid */
u_32 nl_groups; /* 多播组掩码*/
}nl;
这个结构一般按照注释填好就可以了,nl_groups我也不知道怎么用,一般填零了,表示没有多播。绑定:
bind(sockfd, (struct sockaddr*) &nl, sizeof(nl));
2 填充所需数据结构,并通过sendmsg()/send()等函数写到套接字里去
到此为止,与内核通信的准备工作就完成了,下面要做的工作是,选取适当的数据结构进行填充,并作为sendmsg()的参数发送出去,并recv()收到的消息。这个数据结构就是nlmsghdr,它只是一个信息头,后面可以接任意长的数据,这些数据实际上又是针对某一需求所采用的特定数据结构。先来看nlmsghdr:
struct nlmsghdr {
_u32 nlmsg_len; /* Length of msg including header */
_u32 nlmsg_type; /* 操作命令 */
_u16 nlmsg_flags; /* various flags */
_u32 nlmsg_seq; /* Sequence number */
_u32 nlmsg_pid; /* 进程PID */
};
/* 紧跟着是实际要发送的数据,长度可以任意 */
nlmsg_type决定这次要执行的操作,如查询当前路由表信息,所使用的就是RTM_GETROUTE。标准nlmsg_type包括:NLMSG_NOOP, NLMSG_DONE, NLMSG_ERROR等。根据采用的nlmsg_type不同,还要选取不同的数据结构来填充到nlmsghdr后面:
操作 数据结构
RTM_NEWLINK ifinfomsg
RTM_DELLINK
RTM_GETLINK
RTM_NEWADDR ifaddrmsg
RTM_DELADDR
RTM_GETADDR
RTM_NEWROUTE rtmsg
RTM_DELROUTE
RTM_GETROUTE
RTM_NEWNEIGH ndmsg/nda_chcheinfo
RTM_DELNEIGH
RTM_GETNEIGH
RTM_NEWRULE rtmsg
RTM_DELRULE
RTM_GETRULE
RTM_NEWQDISC tcmsg
RTM_DELQDISC
RTM_GETQDISC
RTM_NEWTCLASS tcmsg
RTM_DELTCLASS
RTM_GETTCLASS
RTM_NEWTFILTER tcmsg
RTM_DELTFILTER
RTM_GETTFILTER
由于情形众多,我从现在开始将用一个特定的例子来说明问题。我们的目的是从内核读取IPV4路由表信息。从上面表看,nlmsg_type一定使用RTM_xxxROUTE操作,对应的数据结构是rtmsg。既然是读取,那么应该是RTM_GETROUTE了。
struct rtmsg {
unsigned char rtm_family; /* 路由表地址族 */
unsigned char rtm_dst_len; /* 目的长度 */
unsigned char rtm_src_len; /* 源长度 */ (2.4.10头文件的注释标反了?)
unsigned char rtm_tos; /* TOS */
unsigned char rtm_table; /* 路由表选取 */
unsigned char rtm_protocol; /* 路由协议 */
unsigned char rtm_scope;
unsigned char rtm_type;
unsigned int rtm_flags;
};
对于RTM_GETROUTE操作来说,我们只需指定两个成员:rtm_family:AF_INET, rtm_table: RT_TABLE_MAIN。其他成员都初始化为0即可。将这个结构体跟nlmsghdr结合起来,得到我们自己的新结构体:
struct {
struct nlmsghdr nl;
struct rtmsg rt;
}req;
填充好rt结构之后,还要调整nl结构相应成员的值。Linux定义了多个宏来处理nlmsghdr成员的值,我们这里用到的是NLMSG_LENGTH(size_t len);
req.nl.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
这将计算nlmsghdr长度与rtmsg长度的和(其中包括了将rtmsg进行4字节边界对齐的调整),并存储到nlmsghdr的nlmsg_len成员中。接下来要做的就是将这个新结构体req放到sendmsg()函数的msghdr.iov处,并调用函数。
sendmsg(sockfd, &msg, 0);
3 接收数据,并进行分析
接下来的操作是recv()操作,从该套接字读取内核返回的数据,并进行分析处理。
recv(sockfd, p, sizeof(buf) - nll, 0);
其中p是指向一个缓冲区buf的指针,nll是已接收到的nlmsghdr数据的长度。
由于内核返回信息是一个字节流,需要调用者检查消息结尾。这是通过检查返回的nlmsghdr的nlmsg_type是否等于NLMSG_DONE来完成的。返回的数据格式如下:
-----------------------------------------------------------
| nlmsghdr+route entry | nlmsghdr+route entry | .........
-----------------------------------------------------------
| 解出route entry
V
-----------------------------------------------------------
| dst_addr | gateway | Output interface| ...............
-----------------------------------------------------------
可以看出,返回消息由多个(nlmsghdr + route entry)组成,当某个nlmsghdr的nlmsg_type == NLMSG_DONE时就表示信息输出已经完毕。而每一个route entry由多个rtattr结构体组成,每个结构体表示该路由项的某个属性,如目的地址,网关等等。根据这个示意图我们就能够轻松解析需要的数据了。