天天看點

使用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操作指令字,而且不用修改核心原有的程式。

繼續閱讀