zoukankan      html  css  js  c++  java
  • 使用sockopt与内核交换数据

    本文档的Copyleft归yfydz所有,使用GPL发布,可以自由拷贝,转载,转载时请保持文档的完整性,严禁用于任何商业用途。
    msn: yfydz_no1@hotmail.com
    来源:http://yfydz.cublog.cn


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

    本文内核代码版本为2.6.19.2。

    2. 基本过程
    首先在内核中要登记相关协议的set/getsockopt的选项命令字和相关的处理函数,然后在用户空间打开该协议的socket后就可以直接调用set/getsockopt来指定命令字执行相关的数据交互操作,常见的TCP、UDP的socket都用这两个系统调用来iptables<->netfilter,ipvsadm<->ip_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,
         char __user *optval, int optlen)
    {
     struct udp_sock *up = udp_sk(sk);
     int val;
     int err = 0;
     if(optlen<sizeof(int))
      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;
       break;
      }
      break;
     default:
      err = -ENOPROTOOPT;
      break;
     };
     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);
     mutex_unlock(&nf_sockopt_mutex);
    }
    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 || optname > (MRT_BASE + 10))
    #endif
        ) {
    // 如果IP中没有这个opt命令字,调用netfilter的sockopt
      lock_sock(sk);
      err = nf_setsockopt(sk, PF_INET, optname, optval, optlen);
      release_sock(sk);
     }
    #endif
     return err;
    }
    /* net/netfilter/nf_sockopt.c */
    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 list_head *i;
     struct nf_sockopt_ops *ops;
     int ret;
     if (mutex_lock_interruptible(&nf_sockopt_mutex) != 0)
      return -EINTR;
    // 扫描netfilter的sockopt链表
     list_for_each(i, &nf_sockopts) {
    // 取出opt操作节点
      ops = (struct nf_sockopt_ops *)i;
    // 根据协议,命令字范围判断是否处理该命令字
      if (ops->pf == pf) {
       if (get) {
    // get操作
        if (val >= ops->get_optmin
            && val < ops->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 < ops->set_optmax) {
         ops->use++;
         mutex_unlock(&nf_sockopt_mutex);
         ret = ops->set(sk, val, opt, *len);
         goto out;
        }
       }
      }
     }
     mutex_unlock(&nf_sockopt_mutex);
     return -ENOPROTOOPT;
     
     out:
     mutex_lock(&nf_sockopt_mutex);
    // 操作完成,opt结构节点使用减一
     ops->use--;
     if (ops->cleanup_task)
      wake_up_process(ops->cleanup_task);
     mutex_unlock(&nf_sockopt_mutex);
     return ret;
    }

    这样,自己定义的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)
    {
     int ret;
     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)
      return -EFAULT;
     /* 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 -1;
     return 0;
    }
     

    5. 结论
     
    用setgetsockopt()在用户空间和内核空间传递数据也是常用方法之一,比较简单方便,而且可以在同一个socket中对不同的命令传送不同的数据结构。
    新命令字的添加可以按新协议添加,也可以添加到现有的实现中,但没有特别需求的话,netfilter提供的动态登记opt命令字可以动态添加删除sockopt操作命令字,而且不用修改内核原有的程序。
  • 相关阅读:
    Java基础教程(15)--枚举类型
    Java基础教程(14)--嵌套类
    Java基础教程(13)--包
    Java基础教程(12)--深入理解类
    Java基础教程(11)--对象
    Java基础教程(10)--类
    Java基础教程(9)--流程控制
    Java基础教程(8)--表达式、语句和块
    Java基础教程(7)--运算符
    Java基础教程(6)--数组
  • 原文地址:https://www.cnblogs.com/qq78292959/p/2546598.html
Copyright © 2011-2022 走看看