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