1. 程式人生 > >linux核心與使用者之間的通訊方式——虛擬檔案系統、ioctl以及netlink

linux核心與使用者之間的通訊方式——虛擬檔案系統、ioctl以及netlink

    本文嘗試去闡述核心與使用者空間之間的通訊介面:虛擬檔案系統、ioctl以及netlink.文中所有的結構及程式碼全來自於Linux kernel 2.6.34.

一、虛擬檔案系統

      proc檔案系統,通常是掛載在/proc,允許核心以檔案型別形式向用戶提供內部資訊,但是值得注意的是裡面的檔案目錄不能被寫入,即使用者不能新增或者刪除目錄中的任何目錄。同時,核心也提供了一個可供使用者配置核心變數的方法,那就是sysctl系統呼叫以及在/proc中新增一個特殊目錄:sys,為每個由sysctl所輸出的核心變數引入一個檔案。其中procfs主要是輸出只讀資料,而大多數sysctl資訊都是可寫的。

      procfs: 當用戶需要讀取proc目錄中檔案內容時,就會間接引起一組註冊函式的執行,生成並返回需要顯示的內容。proc中的目錄由核心中的proc_mkdir函式建立,/proc/net中的檔案可以在proc_fs.h中定義的proc_net_fops_create和proc_net_remove函式進行註冊與刪除。以arp為例,首先會在/proc/net目錄下建立arp檔案(只讀),然後初始化其檔案操作處理函式,具體如下:

static const struct file_operations arp_seq_fops = {
    .open       = arp_seq_open,
    .read       = seq_read,
    .llseek     = seq_lseek,
    .release    = seq_release_net,
    .owner      = THIS_MODULE
};

      sysctl(目錄/proc/sys):/proc/sys目錄下的每一個檔案實質上是一個核心變數,這些檔案的訪問規則是:任何人均可讀,但是隻有超級使用者才能修改,修改方法主要有兩種:直接修改指定檔案,或者可以直接用sysctl系統呼叫進行修改(當然也有許可權限制了)。

      sysctl net.ipv4.ip_forward

      sysctl -w net.ipv4.ip_forward=1  //修改值

      這些目錄或者檔案既有可能是在引導期間建立的,也有可能是在執行期間生成的,在執行期間生成的情況主要有:

1.     當核心模組實現一個新功能,或者一個協議載入或者解除安裝時;

2.     當一個新的網路裝置被註冊或者刪除時,

      /proc/sys下的目錄或者檔案均是以ctl_table結構定義的。而要想註冊或者刪除ctl_table結構則是通過register_sysctl_tableunregister_sysctl_table函式來實現(kernel/sysctl.c)。

struct ctl_table 
{
    const char *procname;  //proc/sys中所用的檔名
    void *data;
    int maxlen; //輸出的核心變數的尺寸大小
    mode_t mode; //設定/proc/sys中相關聯的檔案或者目錄的訪問許可權
    struct ctl_table *child; //用於建立目錄與檔案之間的父子關係
    struct ctl_table *parent;   /* Automatically set */
    proc_handler *proc_handler; //完成讀取或者寫入操作的函式
    void *extra1;
    void *extra2; //兩個可選引數,通常用於定義變數的最小值和最大值ֵ
};

      一般來講,/proc/sys下定義了以下幾個主目錄(kernel, vm, fs, debug, dev),其以及它的子目錄定義如下:

static struct ctl_table root_table[] = {
	{
		.procname	= "kernel",
		.mode		= 0555,
		.child		= kern_table,
	},
	{
		.procname	= "vm",
		.mode		= 0555,
		.child		= vm_table,
	},
	{
		.procname	= "fs",
		.mode		= 0555,
		.child		= fs_table,
	},
	{
		.procname	= "debug",
		.mode		= 0555,
		.child		= debug_table,
	},
	{
		.procname	= "dev",
		.mode		= 0555,
		.child		= dev_table,
	},
/*
 * NOTE: do not add new entries to this table unless you have read
 * Documentation/sysctl/ctl_unnumbered.txt
 */
	{ }
};

二、ioctl(網路相關部分)

      一切均從系統呼叫開始,當用戶呼叫ioctl函式時,會呼叫核心中的SYSCALL_DEFINE3函式,它是一個巨集定義,如下:

 #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
【注意】巨集定義中的第一個#代表替換, 則代表使用’-’強制連線。

      SYSCALL_DEFINEx之後呼叫__SYSCALL_DEFINEx函式,而__SYSCALL_DEFINEx同樣是一個巨集定義,如下:

    #define __SYSCALL_DEFINEx(x, name, ...)                 \
    asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__));       \
    static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__));   \
    asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__))        \
    {                               \
        __SC_TEST##x(__VA_ARGS__);              \
        return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__));    \
    }                               \
    SYSCALL_ALIAS(sys##name, SyS##name);                \
    static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))

      中間會呼叫紅色部分的巨集定義,asmlinkage會通知編譯器僅從中提取該函式的引數,所有的系統呼叫都會用到這個標誌。

       上面只是闡述了系統呼叫的一般過程,值得注意的是,上面這種系統呼叫過程的應用於最新的核心程式碼中,老版本中的這些過程是在syscall函式中完成的。我們就以在網路程式設計中ioctl系統呼叫為例介紹整個呼叫過程。當用戶呼叫ioctl試圖去從核心中獲取某些值時,會觸發呼叫:

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
    struct file *filp;
    int error = -EBADF;
    int fput_needed;

    filp = fget_light(fd, &fput_needed); //根據程序描述符獲取對應的檔案物件
    if (!filp)
        goto out;

    error = security_file_ioctl(filp, cmd, arg);
    if (error)
        goto out_fput;

    error = do_vfs_ioctl(filp, fd, cmd, arg);
 out_fput:
    fput_light(filp, fput_needed);
 out:
    return error;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
}

      之後依次經過file_ioctl---->vfs_ioctl找到對應的與socket相對應的ioctl,即sock_ioctl.

static long vfs_ioctl(struct file *filp, unsigned int cmd,
              unsigned long arg)
{
    ........
    if (filp->f_op->unlocked_ioctl) {
        error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
        if (error == -ENOIOCTLCMD)
            error = -EINVAL;
        goto out;
    } else if (filp->f_op->ioctl) {
        lock_kernel();
        error = filp->f_op->ioctl(filp->f_path.dentry->d_inode,
                      filp, cmd, arg);
        unlock_kernel();
    }
    .......
}

      從上面程式碼片段中可以看出,根據對應的檔案指標呼叫對應的ioctl,那麼socket對應的檔案指標的初始化是在哪完成的呢?可以參考socket.c檔案下sock_alloc_file函式:

static int sock_alloc_file(struct socket *sock, struct file **f, int flags)
{
    struct qstr name = { .name = "" };
    struct path path;
    struct file *file;
    int fd;
    ..............
    file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
          &socket_file_ops);
    if (unlikely(!file)) {
        /* drop dentry, keep inode */
        atomic_inc(&path.dentry->d_inode->i_count);
        path_put(&path);
        put_unused_fd(fd);
        return -ENFILE;
    }
    .............
}

     alloc_file函式將socket_file_ops指標賦值給socket中的f_op.同時注意file->private_data = sock這條語句。

struct file *alloc_file(struct path *path, fmode_t mode,const struct file_operations *fop)
{
    .........

    file->f_path = *path;
    file->f_mapping = path->dentry->d_inode->i_mapping;
    file->f_mode = mode;
file->f_op = fop;

file->private_data = sock;
    ........
}

      而socket_file_ops是在socket.c檔案中定義的一個靜態結構體變數,它的定義如下:

static const struct file_operations socket_file_ops = {
    .owner =    THIS_MODULE,
    .llseek =   no_llseek,
    .aio_read = sock_aio_read,
    .aio_write =    sock_aio_write,
    .poll =     sock_poll,
    .unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_sock_ioctl,
#endif
    .mmap =     sock_mmap,
    .open =     sock_no_open,   /* special open code to disallow open via /proc */
    .release =  sock_close,
    .fasync =   sock_fasync,
    .sendpage = sock_sendpage,
    .splice_write = generic_splice_sendpage,
    .splice_read =  sock_splice_read,
};

      從上面分析可以看出,filp->f_op->unlocked_ioctl實質呼叫的是sock_ioctl。

      OK,再從sock_ioctl程式碼開始,如下:

static long sock_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
    .......
    sock = file->private_data;
    sk = sock->sk;
    net = sock_net(sk);
    if (cmd >= SIOCDEVPRIVATE && cmd <= (SIOCDEVPRIVATE + 15)) {
        err = dev_ioctl(net, cmd, argp);
    } else
#ifdef CONFIG_WEXT_CORE
    if (cmd >= SIOCIWFIRST && cmd <= SIOCIWLAST) {
        err = dev_ioctl(net, cmd, argp);
    } else
#endif
     .......
        default:
            err = sock_do_ioctl(net, sock, cmd, arg);
            break;
        }
    return err;
}

      首先通過file變數的private_date成員將socket從sys_ioctl傳遞過來,最後通過執行sock_do_ioctl函式完成相應操作。

static long sock_do_ioctl(struct net *net, struct socket *sock,
                 unsigned int cmd, unsigned long arg)
{
    ........
    err = sock->ops->ioctl(sock, cmd, arg);
    .........
}

      那麼此時的socket中的ops成員又是從哪來的呢?我們以IPV4為例,都知道在建立socket時,都會需要設定相應的協議型別,此處的ops也是socket在建立inet_create函式中遍歷inetsw列表得到的。

static int inet_create(struct net *net, struct socket *sock, int protocol,
               int kern)
{
	struct inet_protosw *answer;
    ........
    sock->ops = answer->ops;
    answer_prot = answer->prot;
    answer_no_check = answer->no_check;
    answer_flags = answer->flags;
    rcu_read_unlock();
    .........
}

      那麼inetsw列表又是在何處生成的呢?那是在協議初始化函式inet_init中呼叫inet_register_protosw(將全域性結構體陣列inetsw_array陣列初始化)來實現的,

static struct inet_protosw inetsw_array[] =
{
    {
        .type =       SOCK_STREAM,
        .protocol =   IPPROTO_TCP,
        .prot =       &tcp_prot,
        .ops =        &inet_stream_ops,
        .no_check =   0,
        .flags =      INET_PROTOSW_PERMANENT |
                  INET_PROTOSW_ICSK,
    },
    {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_UDP,
        .prot =       &udp_prot,
        .ops =        &inet_dgram_ops,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_PERMANENT,
    },
    {
           .type =       SOCK_RAW,
           .protocol =   IPPROTO_IP,    /* wild card */
           .prot =       &raw_prot,
           .ops =        &inet_sockraw_ops,
           .no_check =   UDP_CSUM_DEFAULT,
           .flags =      INET_PROTOSW_REUSE,
   }
};

      很明顯可以看到,sock->ops->ioctl需要根據具體的協議找到需要呼叫的ioctl函式。我們就以TCP協議為例,就需要呼叫inet_stream_ops中的ioctl函式——inet_ioctl,結構如下:

int inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
{
    struct sock *sk = sock->sk;
    int err = 0;
    struct net *net = sock_net(sk);

    switch (cmd) {
    case SIOCGSTAMP:
        err = sock_get_timestamp(sk, (struct timeval __user *)arg);
        break;
    case SIOCGSTAMPNS:
        err = sock_get_timestampns(sk, (struct timespec __user *)arg);
        break;
    case SIOCADDRT:  //增加路由
    case SIOCDELRT:  //刪除路由
    case SIOCRTMSG:
        err = ip_rt_ioctl(net, cmd, (void __user *)arg);  //IP路由配置
        break;
    case SIOCDARP: //刪除ARP項
    case SIOCGARP: //獲取ARP項
    case SIOCSARP: //建立或者修改ARP項
        err = arp_ioctl(net, cmd, (void __user *)arg); //ARP配置
        break;
    case SIOCGIFADDR: //獲取介面地址
    case SIOCSIFADDR: //設定介面地址
    case SIOCGIFBRDADDR:  //獲取廣播地址
    case SIOCSIFBRDADDR:  //設定廣播地址
    case SIOCGIFNETMASK:  //獲取網路掩碼
    case SIOCSIFNETMASK:  //設定網路掩碼
    case SIOCGIFDSTADDR:  //獲取某個介面的點對點地址
    case SIOCSIFDSTADDR:  //設定每個介面的點對點地址
    case SIOCSIFPFLAGS:
    case SIOCGIFPFLAGS:
    case SIOCSIFFLAGS: //設定介面標誌
        err = devinet_ioctl(net, cmd, (void __user *)arg); //網路介面配置相關
        break;
    default:
        if (sk->sk_prot->ioctl)
        err = sk->sk_prot->ioctl(sk, cmd, arg);
        else
        err = -ENOIOCTLCMD;
        break;
    }
    return err;
}

      到此,基本上找到了socket所對應的ioctl處理程式碼片段。整個流程大致可以用下面圖進行概括(來自《深入理解Linux網路技術內幕》):



     三、Netlink

      netlink已經成為使用者空間與核心的IP網路配置之間的首選介面,同時它也可以作為核心內部與多個使用者空間程序之間的訊息傳輸系統.在實現netlink用於核心空間與使用者空間之間的通訊時,使用者空間的建立方法和一般的套接字的建立使用類似,但核心的建立方法則有所不同,下圖是netlink實現此類通訊時的建立過程:


下面分別詳細介紹核心空間與使用者空間在實現此類通訊時的建立方法:

● 使用者空間:

      建立流程大體如下:

①     建立socket套接字

②     呼叫bind函式完成地址的繫結,不過同通常意義下server端的繫結還是存在一定的差別的,server端通常繫結某個埠或者地址,而此處的繫結則是socket套介面與本程序的pid進行繫結

③     通過sendto或者sendmsg函式傳送訊息;

④     通過recvfrom或者rcvmsg函式接受訊息。

【說明】

◆       netlink對應的協議簇是AF_NETLINK,協議型別可以是自定義的型別,也可以是核心預定義的型別;

	#define NETLINK_ROUTE       0   /* Routing/device hook              */
	#define NETLINK_UNUSED      1   /* Unused number                */
	#define NETLINK_USERSOCK    2   /* Reserved for user mode socket protocols  */
	#define NETLINK_FIREWALL    3   /* Firewalling hook             */
	#define NETLINK_INET_DIAG   4   /* INET socket monitoring           */
	#define NETLINK_NFLOG       5   /* netfilter/iptables ULOG */
	#define NETLINK_XFRM        6   /* ipsec */
	#define NETLINK_SELINUX     7   /* SELinux event notifications */
	#define NETLINK_ISCSI       8   /* Open-iSCSI */
	#define NETLINK_AUDIT       9   /* auditing */
	#define NETLINK_FIB_LOOKUP  10  
	#define NETLINK_CONNECTOR   11
	#define NETLINK_NETFILTER   12  /* netfilter subsystem */
	#define NETLINK_IP6_FW      13
	#define NETLINK_DNRTMSG     14  /* DECnet routing messages */
	#define NETLINK_KOBJECT_UEVENT  15  /* Kernel messages to userspace */
	#define NETLINK_GENERIC     16
	/* leave room for NETLINK_DM (DM Events) */
	#define NETLINK_SCSITRANSPORT   18  /* SCSI Transports */
	#define NETLINK_ECRYPTFS    19

     上面是核心預定義的20種類型,當然也可以自定義一些。

◆       前面說過,netlink處的繫結有著自己的特殊性,其需要繫結的協議地址可用以下結構來描述:

    struct sockaddr_nl {
    sa_family_t nl_family;  /* AF_NETLINK   */
    unsigned short  nl_pad;     /* zero     */
    __u32       nl_pid;     /* port ID  */
    __u32       nl_groups;  /* multicast groups mask */
};

      其中成員nl_family為AF_NETLINK,nl_pad當前未使用,需設定為0,成員nl_pid為接收或傳送訊息的程序的 ID,如果希望核心處理訊息或多播訊息,就把該欄位設定為 0,否則設定為處理訊息的程序 ID.,不過在此特別需要說明的是,此處是以程序為單位,倘若程序存在多個執行緒,那在與netlink通訊的過程中如何準確找到對方執行緒呢?此時nl_pid可以這樣表示:

pthread_self() << 16 | getpid()

      pthread_self函式是用來獲取執行緒ID,總之能夠區分各自執行緒目的即可。成員 nl_groups用於指定多播組,bind 函式用於把呼叫程序加入到該欄位指定的多播組,如果設定為 0,表示呼叫者不加入任何多播組

◆  通過netlink傳送的訊息結構:

    struct nlmsghdr {
    __u32       nlmsg_len;  /* Length of message including header */
    __u16       nlmsg_type; /* Message type */
    __u16       nlmsg_flags;    /* Additional flags */
    __u32       nlmsg_seq;  /* Sequence number */
    __u32       nlmsg_pid;  /* Sending process port ID */
};

      其中nlmsg_len指的是訊息長度,nlmsg_type指的是訊息型別,使用者可以自己定義。欄位nlmsg_flags 用於設定訊息標誌,對於一般的使用,使用者把它設定為0 就可以,只是一些高階應用(如netfilter 和路由daemon 需要它進行一些複雜的操作),欄位 nlmsg_seq nlmsg_pid 用於應用追蹤訊息,前者表示順序號,後者為訊息來源程序 ID

●  核心空間:

核心空間主要完成以下三方面的工作:

①    建立netlinksocket,並註冊回撥函式,註冊函式將會在有訊息到達netlinksocket時會執行;

②    根據使用者請求型別,處理使用者空間的資料;

③    將資料傳送回用戶。   

【說明】

◆       netlink中利用netlink_kernel_create函式建立一個netlink socket.

struct sock *netlink_kernel_create(struct net *net, int unit, unsigned int groups,void (*input)(struct sk_buff 								*skb),struct mutex *cb_mutex, struct module *module)

      net欄位指的是網路的名稱空間,一般用&init_net替代;unit欄位實質是netlink協議型別,值得注意的是,此值一定要與使用者空間建立socket時的第三個引數值保持一致;groups欄位指的是socket的組名,一般置為0即可;input欄位是回撥函式,當netlink收到訊息時會被觸發;cb_mutex一般置為NULL;module一般置為THIS_MODULE巨集。

◆       netlink是通過呼叫API函式netlink_unicast或者netlink_broadcast將資料返回給使用者的.

	int netlink_unicast(struct sock *ssk, struct sk_buff *skb,u32 pid, int nonblock)

       ssk欄位正是由netlink_kernel_create函式所返回的socket;引數skb指向的是socket快取,它的data欄位用來指向要傳送的netlink訊息結構;引數pid為接收訊息程序的pid,引數nonblock表示該函式是否為非阻塞,如果為1,該函式將在沒有接收快取可利用時立即返回,而如果為0,該函式在沒有接收快取可利用時睡眠

【引申】核心傳送的netlink訊息是通過struct sk_buffer結構來管理的,即socket快取,linux/netlink.h中定義了

#define NETLINK_CB(skb)   (*(struct netlink_skb_parms*)&((skb)->cb))

來方便訊息的地址設定。

    struct netlink_skb_parms {
    struct ucred        creds;      /* Skb credentials  */
    __u32           pid;
    __u32           dst_group;
    kernel_cap_t        eff_cap;
    __u32           loginuid;   /* Login (audit) uid */
    __u32           sessionid;  /* Session id (audit) */
    __u32           sid;        /* SELinux security id */
};

      其中pid指的是傳送者的程序ID,如:

NETLINK_CB(skb).pid = 0; /*from kernel */

◆  netlink API函式sock_release可以用來釋放所建立的socket.

【宣告】轉載時請註明出處和作者聯絡方式
  文章出處:http://blog.csdn.net/chenlilong84/article/details/8235060
  作者聯絡方式:[email protected]

相關推薦

no