天天看点

使用sockopt与内核交换数据

1. 前言

打开一个网络socket后可以使用set/getsockopt(2)可实现用户空间与内核的通信,本质和ioctl差不多,区别在于set /getsockopt不用新建设备,直接利用系统已有的socket类型就可以进行,可用setsockopt函数向内核写数据,用 getsockopt向内核读数据。

本文内核代码版本为2.6.19.2。

2. 基本过程

首先在内核中要登记相关协议的set/getsockopt的选项命令字和相关的处理函数,然后在用户空间打开该协议的socket后就可以直接调用set/getsockopt来指定命令字执行相关的数据交互操作,常见的TCP、UDP的socket都用这两个系统调用来 iptablesnetfilter,ipvsadmip_vs就是这么实现的。

3. set/getsockopt(2)

set/getsockopt(2)函数的基本使用格式为:

int setsockopt(int sockfd, int proto, int cmd, void *data, int datalen)

int getsockopt(int sockfd, int proto, int cmd, void *data, int datalen)

第一个参数是socket描述符;第2个参数proto是sock协议,IP RAW的就用SOL_SOCKET/SOL_IP等,TCP/UDP socket的可用SOL_SOCKET/SOL_IP/SOL_TCP/SOL_UDP等,即高层的socket是都可以使用低层socket的命令字的;第3个参数cmd是操作命令字,由自己定义;第4个参数是数据缓冲区起始位置指针,set操作时是将缓冲区数据写入内核,get的时候是将内核中的数据读入该缓冲区;第5个参数数据长度。

4. 内核实现

内核实现新的sockopt命令字有两类,一类是添加完整的新的协议后引入,一类是在原有协议命令集的基础上增加新的命令字。

sockopt命令字定义没有什么特别之处,就是一个整数,只要对这个协议内部是一个唯一的的即可,不象ioctl的命令字还有一定格式要求。

4.1 完整协议

每个协议都是用struct proto结构(include/net/sock.h)来描述的,Linux内核中缺省定义了三种:TCP、UDP和RAW,所有非TCP、UDP的都用RAW来描述。在net/core/sock.c的sock_get/setsockopt()函数中内核实现了一个所有socket共同的 sockopt读写命令集合,在各个协议的内部再单独定义各自协议的独有命令字。

struct proto中有setsockopt和getsocket成员函数,用来定义每个协议的独有相关的命令字。

如对于UDP协议的setsockopt成员函数:

static int udp_setsockopt(struct sock *sk, int level, int optname,

     char __user *optval, int optlen)

{

// 先判断是否是UDP层,不是的话调IP层的sockopt处理

 if (level != SOL_UDP)

  return ip_setsockopt(sk, level, optname, optval, optlen);

// 是UDP级别命令,调用UDP协议本身的sockopt处理

 return do_udp_setsockopt(sk, level, optname, optval, optlen);

}

static int do_udp_setsockopt(struct sock *sk, int level, int optname,

 struct udp_sock *up = udp_sk(sk);

 int val;

 int err = 0;

 if(optlen  return -EINVAL;

 if (get_user(val, (int __user *)optval))

  return -EFAULT;

// 实际UDP独有的命令字就这两个

 switch(optname) {

 case UDP_CORK:

  if (val != 0) {

   up->corkflag = 1;

  } else {

   up->corkflag = 0;

   lock_sock(sk);

   udp_push_pending_frames(sk, up);

   release_sock(sk);

  }

  break;

// UDP封装,在IPSEC的NAT-T时使用 

 case UDP_ENCAP:

  switch (val) {

  case 0:

  case UDP_ENCAP_ESPINUDP:

  case UDP_ENCAP_ESPINUDP_NON_IKE:

   up->encap_type = val;

   break;

  default:

   err = -ENOPROTOOPT;

 default:

  err = -ENOPROTOOPT;

 };

 return err;

所以要实现一个新协议的sockopt控制,只需要类似方法处理即可,定义好struct proto结构后将其注册到系统中即可,对于IP族内协议用inet_register_protosw()函数,其他协议族可类似处理。

4.2 命令扩充

实际使用中单独定义新协议的可能性不是很大,通常只是添加新的命令字即可,对于TCP、UDP的新命令字的添加,需要自己修改内核tcp/udp实现代码,把自己的命令字添加进去后重新编译内核才能生效。

对于IP RAW级别的命令字,netfilter提供了nf_register_sockopt()和nf_unregister_sockopt()来动态登记或取消sockopt命令字,这样可以不用修改内核原来的代码。方法是将netfilter的sockopt操作集合定义为一个链表,要定义新的opt操作就定义一个新的opt操作节点挂接到该链表中,在系统sockopt调用时,会依次查找链表中的命令字,匹配上了就可以成功调用,因此opt的命令字不能和原来IP RAW中定义相同,不过命令字是个32位的数,取值范围很大,只要稍微注意一点是不会冲突的。

netfilter的sock是RAW级别的。

sockopt操作节点结构,结构比较简单明了,就是定义各自命令字的范围空间和相关的处理函数:

/* include/linux/netfilter.h */

struct nf_sockopt_ops

// 链表节点

 struct list_head list;

// 协议族

 int pf;

 /* Non-inclusive ranges: use 0/0/NULL to never get called. */

// set命令的最小值

 int set_optmin;

// set命令的最大值

 int set_optmax;

// set函数实现

 int (*set)(struct sock *sk, int optval, void __user *user, unsigned int len);

 int (*compat_set)(struct sock *sk, int optval,

   void __user *user, unsigned int len);

// get命令的最小值

 int get_optmin;

// get命令的最大值

 int get_optmax;

// get函数实现

 int (*get)(struct sock *sk, int optval, void __user *user, int *len);

 int (*compat_get)(struct sock *sk, int optval,

   void __user *user, int *len);

 /* Number of users inside set() or get(). */

 unsigned int use;

 struct task_struct *cleanup_task;

};

opt操作结构登记和撤销函数:

/* net/netfilter/nf_sockopt.c */

// nf的sockopt的链表,所有sockopt命令处理都挂接到这个链表

static LIST_HEAD(nf_sockopts);

/* Functions to register sockopt ranges (exclusive). */

int nf_register_sockopt(struct nf_sockopt_ops *reg)

 struct list_head *i;

 int ret = 0;

// 加锁

 if (mutex_lock_interruptible(&nf_sockopt_mutex) != 0)

  return -EINTR;

// 检查当前链表中是否已经挂接了该sockopt操作节点

 list_for_each(i, &nf_sockopts) {

  struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;

  if (ops->pf == reg->pf

      && (overlap(ops->set_optmin, ops->set_optmax,

    reg->set_optmin, reg->set_optmax)

   || overlap(ops->get_optmin, ops->get_optmax,

       reg->get_optmin, reg->get_optmax))) {

   NFDEBUG("nf_sock overlap: %u-%u/%u-%u v %u-%u/%u-%u\n",

    ops->set_optmin, ops->set_optmax,

    ops->get_optmin, ops->get_optmax,

    reg->set_optmin, reg->set_optmax,

    reg->get_optmin, reg->get_optmax);

   ret = -EBUSY;

   goto out;

 }

// 新节点,添加到opt链表中

 list_add(&reg->list, &nf_sockopts);

out:

// 解锁

 mutex_unlock(&nf_sockopt_mutex);

 return ret;

EXPORT_SYMBOL(nf_register_sockopt);

void nf_unregister_sockopt(struct nf_sockopt_ops *reg)

 /* No point being interruptible: we're probably in cleanup_module() */

 restart:

 mutex_lock(&nf_sockopt_mutex);

 if (reg->use != 0) {

// 操作节点还在使用中,阻塞进程直到所有操作完成

  /* To be woken by nf_sockopt call... */

  /* FIXME: Stuart Young's name appears gratuitously. */

  set_current_state(TASK_UNINTERRUPTIBLE);

  reg->cleanup_task = current;

  mutex_unlock(&nf_sockopt_mutex);

  schedule();

  goto restart;

// 从链表中删除

 list_del(&reg->list);

EXPORT_SYMBOL(nf_unregister_sockopt);

下面来看一下具体调用流程是如何进行的,首先打开的socket是一个RAW类型的IP socket,对这类socket的setsockopt操作会调用到ip_setsockopt()函数:

/* net/ipv4/ip_sockglue.c */

int ip_setsockopt(struct sock *sk, int level,

  int optname, char __user *optval, int optlen)

 int err;

 if (level != SOL_IP)

  return -ENOPROTOOPT;

// 先按普通IP的sockopt操作执行

 err = do_ip_setsockopt(sk, level, optname, optval, optlen);

#ifdef CONFIG_NETFILTER

// 内核要支持netfilter

 /* we need to exclude all possible ENOPROTOOPTs except default case */

 if (err == -ENOPROTOOPT && optname != IP_HDRINCL &&

  optname != IP_IPSEC_POLICY && optname != IP_XFRM_POLICY

#ifdef CONFIG_IP_MROUTE

  && (optname (MRT_BASE + 10))

#endif

    ) {

// 如果IP中没有这个opt命令字,调用netfilter的sockopt

  lock_sock(sk);

  err = nf_setsockopt(sk, PF_INET, optname, optval, optlen);

  release_sock(sk);

int nf_setsockopt(struct sock *sk, int pf, int val, char __user *opt,

    int len)

// 实际调用nf_sockopt函数

 return nf_sockopt(sk, pf, val, opt, &len, 0);

static int nf_sockopt(struct sock *sk, int pf, int val,

        char __user *opt, int *len, int get)

 struct nf_sockopt_ops *ops;

 int ret;

// 扫描netfilter的sockopt链表

// 取出opt操作节点

  ops = (struct nf_sockopt_ops *)i;

// 根据协议,命令字范围判断是否处理该命令字

  if (ops->pf == pf) {

   if (get) {

// get操作

    if (val >= ops->get_optmin

        && val get_optmax) {

// opt结构节点使用计数加1

     ops->use++;

     mutex_unlock(&nf_sockopt_mutex);

     ret = ops->get(sk, val, opt, len);

     goto out;

    }

   } else {

// set操作

    if (val >= ops->set_optmin

        && val set_optmax) {

     ret = ops->set(sk, val, opt, *len);

   }

 return -ENOPROTOOPT;

 out:

// 操作完成,opt结构节点使用减一

 ops->use--;

 if (ops->cleanup_task)

  wake_up_process(ops->cleanup_task);

这样,自己定义的nf的opt节点就可以被遍历到,操作也就有效.

具体实例, ip_vs opt操作节点:

/* net/ipv4/ipvs/ip_vs_ctl.c */

static struct nf_sockopt_ops ip_vs_sockopts = {

 .pf  = PF_INET,

// 定义set命令字范围

 .set_optmin = IP_VS_BASE_CTL,

 .set_optmax = IP_VS_SO_SET_MAX+1,

 .set  = do_ip_vs_set_ctl,

// get命令字范围

 .get_optmin = IP_VS_BASE_CTL,

 .get_optmax = IP_VS_SO_GET_MAX+1,

 .get  = do_ip_vs_get_ctl,

set/get函数就很简单了,就进行一些合法性检查,然后根据命令字进行相关处理即可:

static int

do_ip_vs_set_ctl(struct sock *sk, int cmd, void __user *user, unsigned int len)

 unsigned char arg[MAX_ARG_LEN];

 struct ip_vs_service_user *usvc;

 struct ip_vs_service *svc;

 struct ip_vs_dest_user *udest;

// 用户权限检查

 if (!capable(CAP_NET_ADMIN))

  return -EPERM;

// 数据长度检查

 if (len != set_arglen[SET_CMDID(cmd)]) {

  IP_VS_ERR("set_ctl: len %u != %u\n",

     len, set_arglen[SET_CMDID(cmd)]);

  return -EINVAL;

// 拷贝数据

 if (copy_from_user(arg, user, len) != 0)

 /* increase the module use count */

// ipvs模块使用计数

 ip_vs_use_count_inc();

 if (mutex_lock_interruptible(&__ip_vs_mutex)) {

  ret = -ERESTARTSYS;

  goto out_dec;

// 以下进行具体的命令实现操作:

 if (cmd == IP_VS_SO_SET_FLUSH) {

  /* Flush the virtual service */

  ret = ip_vs_flush();

  goto out_unlock;

......

5. 用户空间

用户空间的操作很简单,就是用socket(2)打开相关协议类型的socket,直接调用set/getsockopt函数就可以进行操作了.

实例: ipvsadm

int ipvs_init(void)

 socklen_t len;

 len = sizeof(ipvs_info);

// 打开RAW类型的socket

 if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == -1)

  return -1;

// 读取ipvs基本信息

 if (getsockopt(sockfd, IPPROTO_IP, IP_VS_SO_GET_INFO,

         (char *)&ipvs_info, &len))

 return 0;

5. 结论

用setgetsockopt()在用户空间和内核空间传递数据也是常用方法之一,比较简单方便,而且可以在同一个socket中对不同的命令传送不同的数据结构。

新命令字的添加可以按新协议添加,也可以添加到现有的实现中,但没有特别需求的话,netfilter提供的动态登记opt命令字可以动态添加删除sockopt操作命令字,而且不用修改内核原有的程序。

继续阅读