• 内核通信之Netlink源码分析-用户内核通信原理2


    2017-07-05


    上文以一个简单的案例描述了通过Netlink进行用户、内核通信的流程,本节针对流程中的各个要点进行深入分析

    • sock的创建
    • sock管理结构
    • sendmsg源码分析

     sock的创建

     这点包含用户socket的创建以及内核socket的创建,前者通过socket调用实现,后者通过netlink_kernel_create实现。先看用户层的实现

    SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
    {
        int retval;
        struct socket *sock;
        int flags;
    
        /* Check the SOCK_* constants for consistency.  */
        BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
        BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
        BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
        BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
    
        flags = type & ~SOCK_TYPE_MASK;
        if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
            return -EINVAL;
        type &= SOCK_TYPE_MASK;
    
        if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
            flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
    
        retval = sock_create(family, type, protocol, &sock);
        if (retval < 0)
            goto out;
        retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
        if (retval < 0)
            goto out_release;
    
    out:
        /* It may be already another descriptor 8) Not kernel problem. */
        return retval;
    
    out_release:
        sock_release(sock);
        return retval;
    }

     用户层实现较为简单,其目的就是获取一个socket描述符,分为两步,通过sock_create创建socket,通过sock_map_fd关键一个文件描述符。前者最终要调用到__sock_create,该函数仍然主要分为两部分,调用sock_alloc分配一个socket结构,调用对应协议族的create函数。之前有分析到,针对Netlink协议族,对应netlink_create函数。分配sock是通过其inode得到,inode的分配就和文件系统对应的超级快注册的函数有关。这里通过new_inode_pseudo其实分配的是一个socket_alloc结构,其中包含了socket和inode。分配好以后对其做一些初始化,inode的函数操作表i_op对应sockfs_inode_ops。分配好socket之后,调用对应协议族的create函数,协议族由net_proto_family表示,有一个全局的数组net_families记录所有的的协议族。协议族的number就是下标。我们看下Netlink对应的netlink_create函数,只看核心代码

        if (nl_table[protocol].registered &&
            try_module_get(nl_table[protocol].module))
            module = nl_table[protocol].module;
        else
            err = -EPROTONOSUPPORT;
        cb_mutex = nl_table[protocol].cb_mutex;
        bind = nl_table[protocol].bind;
        netlink_unlock_table();

    这里我们需要注意下if条件,可以看到如果协议对应的netlink_table结构没有在nl_table中注册,就会返回错误,这就可以解释之前没有加载内核模块创建sock之前,运行用户程序出现创建socket失败的情况了。在已经注册的情况下,才会接着往下,调用__netlink_create,设置socket的操作函数表为netlink_ops,然后分配了一个sock结构,注意sock结构作为netlink_sock内嵌结构,一次性分配的是netlink_sock,调用后sock_init_data对sock结构做初始化,然后初始化了netlink_sock的等待队列等。

    创建好socket之后,调用sock_map_fd为其分配一个文件描述符,该函数就比较简单

    static int sock_map_fd(struct socket *sock, int flags)
    {
        struct file *newfile;
        int fd = get_unused_fd_flags(flags);
        if (unlikely(fd < 0))
            return fd;
    
        newfile = sock_alloc_file(sock, flags, NULL);
        if (likely(!IS_ERR(newfile))) {
            fd_install(fd, newfile);
            return fd;
        }
    
        put_unused_fd(fd);
        return PTR_ERR(newfile);
    }

    首先获取一个可用的fd,fd即文件描述符数组中的下标,然后创建一个file结构。file结构的private_data字段指向socket。最后调用fd_install把文件file设置到数组中fd下标处。

    而内核创建socket,通过netlink_kernel_create创建,该函数直接调用了__netlink_kernel_create函数,这里和socket系统调用有些类似,首先是通过sock_create_lite创建socket结构,其中调用了sock_alloc函数 ,该函数前面也有用到。而后也同样调用__netlink_create创建sock结构并和socket建立关联,只是这里如果参数中包含接收函数,会设置接收函数,最后会调用netlink_insert把sock结构插入到链表中。

    if (cfg && cfg->input)//设置接收函数
            nlk_sk(sk)->netlink_rcv = cfg->input;
        /*把sock加入到链表中*/
        if (netlink_insert(sk, net, 0))
            goto out_sock_release;

    由于nl_table是初始化好的,在内核sock加入时无需验证其是否已经注册,所以这里出来后要验证下,如果没有注册,则需要重新注册下,当然在此之前设置netlink_sock的内核sock位NETLINK_KERNEL_SOCKET。

    sock管理结构

    内核中通过一个全局数组nl_table管理各个协议的netlink sock,该数组会在netlink协议族注册的时候进行初始化,每个表项是一个netlink_table结构,在netlink_proto_init函数中

    nl_table = kcalloc(MAX_LINKS, sizeof(*nl_table), GFP_KERNEL);
        if (!nl_table)
            goto panic;

    MAX_LINKS是协议的最大值,定义为32,目前有不少已经使用。

    struct netlink_table {
        struct nl_portid_hash    hash;
        struct hlist_head    mc_list;
        struct listeners __rcu    *listeners;
        unsigned int        flags;
        unsigned int        groups;
        struct mutex        *cb_mutex;
        struct module        *module;
        void            (*bind)(int group);
        int            registered;
    };

    hash是通过数组实现的hash表,其本身是一个nl_portid_hash结构,nl_portid_hash中有一个链表头数组table,记录各个protid对应的链表头,大致结构如下,其中实现表示指针指向,虚线表示内嵌结构。registered表明对应的协议是否已经注册。module一般指向当前模块

     

     下面我们在看下netlink_insert函数

    static int netlink_insert(struct sock *sk, struct net *net, u32 portid)
    {
        struct nl_portid_hash *hash = &nl_table[sk->sk_protocol].hash;
        struct hlist_head *head;
        int err = -EADDRINUSE;
        struct sock *osk;
        int len;
        netlink_table_grab();
        head = nl_portid_hashfn(hash, portid);
        len = 0;
        sk_for_each(osk, head) {
            if (net_eq(sock_net(osk), net) && (nlk_sk(osk)->portid == portid))
                break;
            len++;
        }
        if (osk)
            goto err;
        err = -EBUSY;
        if (nlk_sk(sk)->portid)
            goto err;
        err = -ENOMEM;
        if (BITS_PER_LONG > 32 && unlikely(hash->entries >= UINT_MAX))
            goto err;
        if (len && nl_portid_hash_dilute(hash, len))
            head = nl_portid_hashfn(hash, portid);
        hash->entries++;
        nlk_sk(sk)->portid = portid;
        sk_add_node(sk, head);
        err = 0;
    err:
        netlink_table_ungrab();
        return err;
    }

    首先就根据sock对应的协议在nl_table表中找到对应的netlink_table结构,然后获取nl_portid_hash,然后通过nl_portid_hashfn函数根据portid计算hash值获取在nl_portid_hash中table的下标,具体计算过程不妨看下

    static inline struct hlist_head *nl_portid_hashfn(struct nl_portid_hash *hash, u32 portid)
    {
        return &hash->table[jhash_1word(portid, hash->rnd) & hash->mask];
    }

    可以看到这里通过jhash_1word计算散列值,具体计算过程我们就不深入分析了。获取head之后,对链表进行遍历,通过节点获取 到对应的sock结构,验证是否在同一net下有相同portid的sock存在,如果存在就找到,break,找这个是干什么呢?看下面如,如果最终找到,则goto err,就终止处理了,这也反映了同一命名空间下,portid是不能共享的。如果当前没有相同portid的sock且链表存在,则继续。其实在内核portid统一为0的,如何sock的portid非0则错误。往下走,获取链表头,设置sock的portid,调用sk_add_node把sock加入链表。

    sendmsg源码分析

    下面从内核空间的sendmsg库函数入手,分析下整个处理流程。sendmsg对应的系统调用同样也是sendmsg函数,在socket.c文件中

    SYSCALL_DEFINE3(sendmsg, int, fd, struct msghdr __user *, msg, unsigned int, flags)
    {
        if (flags & MSG_CMSG_COMPAT)
            return -EINVAL;
        return __sys_sendmsg(fd, msg, flags);
    }

    该函数直接调用了__sys_sendmsg()

    long __sys_sendmsg(int fd, struct msghdr __user *msg, unsigned flags)
    {
        int fput_needed, err;
        struct msghdr msg_sys;
        struct socket *sock;
    
        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (!sock)
            goto out;
    
        err = ___sys_sendmsg(sock, msg, &msg_sys, flags, NULL);
    
        fput_light(sock->file, fput_needed);
    out:
        return err;
    }

    这里主要有两步,首先通过sockfd_lookup_light函数根据fd查询文件描述符表,获取对应的socket结构,然后再调用___sys_sendmsg函数。先看下前者

    static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)
    {
        struct file *file;
        struct socket *sock;
    
        *err = -EBADF;
        file = fget_light(fd, fput_needed);
        if (file) {
            sock = sock_from_file(file, err);
            if (sock)
                return sock;
            fput_light(file, *fput_needed);
        }
        return NULL;
    }

    前面我们已经分析创建socket的过程,其中就有和文件描述符建立连接的部分,这里就很容易理解了。通过fget_light查询文件描述符表,其中会检查是否是共享的,如果非共享,则无需枷锁,可以快速的获取,否则需要加rcu lock.其余没什么特殊的,根据文件描述符表结构读取即可。如果找到一个file,则调用sock_from_file函数获取sock,之前提到,socket和file之间的链接是通过file结构的private_data字段联系的,所以这里也很简单

    struct socket *sock_from_file(struct file *file, int *err)
    {
        if (file->f_op == &socket_file_ops)
            return file->private_data;    /* set in sock_map_fd */
    
        *err = -ENOTSOCK;
        return NULL;
    }

    不晓得大家是否还记得,在建立连接的时候,有显示的设置file结构的f_op为socket_file_ops。如果sock不为空,就找到了嘛,返回呗赶紧!接下来就是重头戏___sys_sendmsg,代码比较繁琐就不全局列举了,只列举和介绍核心部分。用户层把msghsr的地址作为参数传递到内核(系统调用机制会把参数从用户栈复制到内核栈,并不是直接通过栈传递),然后需要把用户空间的msghdr的内容复制到内核,这是通过copy_from_user实现的,但是现在msghdr记录的还是iov还是用户空间地址,所以需要也iov也进行替换。接下来略过繁琐的验证机制,接下来同样是核心处理

     err = sock_sendmsg(sock, msg_sys, total_len);

    该函数主要调用了__sock_sendmsg函数,而__sock_sendmsg函数在没有加载安全模块的情况下调用了__sock_sendmsg_nosec函数

    static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock,
                           struct msghdr *msg, size_t size)
    {
        struct sock_iocb *si = kiocb_to_siocb(iocb);
    
        si->sock = sock;
        si->scm = NULL;
        si->msg = msg;
        si->size = size;
        return sock->ops->sendmsg(iocb, sock, msg, size);
    }

    可以看到这里实际上调用的是sock->ops->sendmsg(iocb, sock, msg, size);该函数是什么呢?回想下创建socket的时候,已经设置其ops为netlink_ops了,实际对应的sendmsg函数为netlink_sendmsg(af_netlink.c)该函数中首先获取msghdr中的目标地址结构sockaddr_nl,保存在msghdr的msg_name字段,话说这里意义还真是晦涩难懂,不明白的还以为是名字呢!暂且忽略sock_iocb之类的(我也不懂,以后研究)!

    if (msg->msg_namelen) {
            err = -EINVAL;
            if (addr->nl_family != AF_NETLINK)
                goto out;
            dst_portid = addr->nl_pid;//目标端口
            dst_group = ffs(addr->nl_groups);
            err =  -EPERM;
            if ((dst_group || dst_portid) &&
                !netlink_capable(sock, NL_CFG_F_NONROOT_SEND))
                goto out;
        } else {
            dst_portid = nlk->dst_portid;
            dst_group = nlk->dst_group;
        }

    如果msg->msg_namelen不为空,则获取地址中的目标端口和组播掩码。当然组播掩码一般为0 的。否则设置netlink_sock中的dst_portid和dst_group,如果nlk->port为空,则随机分配一个。接下来分配一个skb,并对其进行设置,主要是设置portid和dst_group。然后调用memcpy_fromiovec把用户空间的消息内容复制到内核skb中

    int memcpy_fromiovec(unsigned char *kdata, struct iovec *iov, int len)
    {
        while (len > 0) {
            if (iov->iov_len) {
                int copy = min_t(unsigned int, len, iov->iov_len);
                if (copy_from_user(kdata, iov->iov_base, copy))
                    return -EFAULT;
                len -= copy;
                kdata += copy;
                iov->iov_base += copy;
                iov->iov_len -= copy;
            }
            iov++;
        }
        return 0;
    }

    根据首篇文章介绍的消息格式,这里理解起来就没问题了,这里len是所有iov向量的总长度,一个循环下来数据就拷贝到内核skb中了。接下来在单播的情况下就调用netlink_unicast函数进行发送了。

    int netlink_unicast(struct sock *ssk, struct sk_buff *skb,
                u32 portid, int nonblock)
    {
        struct sock *sk;
        int err;
        long timeo;
    
        skb = netlink_trim(skb, gfp_any());
    
        timeo = sock_sndtimeo(ssk, nonblock);
    retry:
        /*根据portid获取目标sork*/
        sk = netlink_getsockbyportid(ssk, portid);
        if (IS_ERR(sk)) {
            kfree_skb(skb);
            return PTR_ERR(sk);
        }
        if (netlink_is_kernel(sk))
            return netlink_unicast_kernel(sk, skb, ssk);
    
        if (sk_filter(sk, skb)) {
            err = skb->len;
            kfree_skb(skb);
            sock_put(sk);
            return err;
        }
        err = netlink_attachskb(sk, skb, &timeo, ssk);
        if (err == 1)
            goto retry;
        if (err)
            return err;
        return netlink_sendskb(sk, skb);
    }

    函数中首先遍历nl_table获取sock结构,注意参数中的portid是目标socket的端口,需要要到一个网络命名空间相同且portid和参数中的portid相同的sock.此时如果是内核sock,n那么调用netlink_unicast_kernel,意味着这是发往内核的数据。这里实现就很简单了,直接上代码把

    static int netlink_unicast_kernel(struct sock *sk, struct sk_buff *skb,
                      struct sock *ssk)
    {
        int ret;
        struct netlink_sock *nlk = nlk_sk(sk);
        ret = -ECONNREFUSED;
        if (nlk->netlink_rcv != NULL) {
            ret = skb->len;
            netlink_skb_set_owner_r(skb, sk);
            NETLINK_CB(skb).sk = ssk;
            nlk->netlink_rcv(skb);
            consume_skb(skb);
        } else {
            kfree_skb(skb);
        }
        sock_put(sk);
        return ret;
    }

    总的来说就是交付给内核sock注册的接收函数了,这点在创建内核套接字部分已经介绍,剩下的就看个人设置的如何接收了!然而如果不是发往内核的,那肯定是发往另一个进程的,调用netlink_sendskb函数,该函数中直接调用了__netlink_sendskb。如果不是mmap的skb,则把skb加入到接收端sock的等待队列中,然后调用sock中的sk_data_ready函数,该函数目前还是空函数。剩下的就等对端从其接受队列中获取skb然后处理了O(∩_∩)O~

    以马内利

    参考资料:

    linux内核3.10.1源码

    深入linux内核架构

  • 相关阅读:
    jmeter接口测试--循环获取网页中的html链接
    jmeter接口测试--文件下载
    jmeter接口测试--文件上传
    微信群发消息小工具 v1.0-可定时发送
    xmrig 源码转为vs2015项目--总结
    nginx---max_connections、meme.type、default_type
    字典 dict
    元祖 tuple
    列表list
    字符串常用方法
  • 原文地址:https://www.cnblogs.com/ck1020/p/7122515.html
Copyright © 2020-2023  润新知