内核通信之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内核架构

posted @ 2017-07-06 10:59  jack.chen  Views(1723)  Comments(0Edit  收藏  举报

以马内利