zoukankan      html  css  js  c++  java
  • 内核通信之Netlink源码分析-用户内核通信原理3

    2017-07-06


    上节主讲了用户层通过netlink和内核交互的详细过程,本节分析下用户层接收数据的过程……

    有了之前基础知识的介绍,用户层接收数据只涉及到一个核心调用readmsg(),

    其他的就不多介绍了,不太明白的请参考之前的文章,我们还是重点看下内核究竟在背后做了什么!该函数在内核对应于read_msg系统调用

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

     没什么特殊的,调用了__sys_recvmsg

    long __sys_recvmsg(int fd, struct msghdr __user *msg, unsigned flags)
    {
        int fput_needed, err;
        struct msghdr msg_sys;
        struct socket *sock;
        /*根据找到对应socket结构*/
        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (!sock)
            goto out;
    
        err = ___sys_recvmsg(sock, msg, &msg_sys, flags, 0);
    
        fput_light(sock->file, fput_needed);
    out:
        return err;
    }

     这里主要分为两部分1、根据传递进来的fd找到内核中的socket数据结构。2、调用___sys_recvmsg执行剩余的工作,前者在发送数据的时候已经进行过介绍,这里就不再赘述。直接看___sys_recvmsg,该函数比较长我们还是分部来看。

    if (MSG_CMSG_COMPAT & flags) {
            if (get_compat_msghdr(msg_sys, msg_compat))
                return -EFAULT;
        } else if (copy_from_user(msg_sys, msg, sizeof(struct msghdr)))
            return -EFAULT;
    
        if (msg_sys->msg_iovlen > UIO_FASTIOV) {
            err = -EMSGSIZE;
            if (msg_sys->msg_iovlen > UIO_MAXIOV)
                goto out;
            err = -ENOMEM;
            iov = kmalloc(msg_sys->msg_iovlen * sizeof(struct iovec),
                      GFP_KERNEL);
            if (!iov)
                goto out;
        }

     首先自然是获取头部msghdr信息,注意参数中的msghdr指针为用户空间的地址,所以我们需要在内核中构建一个msghdr,把用户空间结构的内容拷贝到内核中,如果flag字段有MSG_CMSG_COMPAT,则直接使用内核指针访问用户空间的msghdr的属性字段。否则使用copy_from_user把整个结构复制过来。前面文章介绍过msghdr并不直接管理数据,而是通过iov向量。这里如果msg_sys->msg_iovlen超过UIO_FASTIOV,这种情况还有回旋的余地,如果没有超过UIO_MAXIOV,则在内核分配多出UIO_FASTIOV的iov向量,因为前面已经在内核栈中分配好了UIO_FASTIOV数量的内核iov,接下来的工作就比较简单,需要把用户空间iov里记录的信息复制到内核空间的iov中,代码如下,

    uaddr = (__force void __user *)msg_sys->msg_name;
        uaddr_len = COMPAT_NAMELEN(msg);
        /*修改iov信息*/
        if (MSG_CMSG_COMPAT & flags) {
            err = verify_compat_iovec(msg_sys, iov, &addr, VERIFY_WRITE);
        } else
            err = verify_iovec(msg_sys, iov, &addr, VERIFY_WRITE);
        if (err < 0)
            goto out_freeiov;
        total_len = err;

     接下来略过一些控制检查就该处理数据了,

    cmsg_ptr = (unsigned long)msg_sys->msg_control;
        msg_sys->msg_flags = flags & (MSG_CMSG_CLOEXEC|MSG_CMSG_COMPAT);
    
        if (sock->file->f_flags & O_NONBLOCK)
            flags |= MSG_DONTWAIT;
        err = (nosec ? sock_recvmsg_nosec : sock_recvmsg)(sock, msg_sys,
                                  total_len, flags);

     我们分析sock_recvmsg,在快速处理的时候会使用sock_recvmsg_nosec,回想下在发送数据的时候 有个used_address,即如果当前地址和上次发送使用的地址一样,则调用快速处理函数。这里也是同样的道理。在sock_recvmsg中主要调用了__sock_recvmsg继而调用了__sock_recvmsg_nosec,最终调用到sock->ops->recvmsg,针对netlink,这里就是netlink_recvmsg,针对该函数抛开现象看本质的话还是挺清晰的,主要分为两步

    1、从指定的sock接收队列中取出一个skb

    2、把skb中的数据复制到用户空间的内存中

    前者由skb_recv_datagram函数完成,该函数又调用了__skb_recv_datagram,在该函数中涉及到一个MSG_PEEK标志,如果flags字段设置了该标志,则从接收队列中取出skb后不会把skb从队列中删除,所以这样就需要另一个参数off来确定本次需要的是哪个skb.如果没有设置,则得到一个skb后会把skb从队列中删除,这样实际上每次取队列中的首个就可以了,核心代码如下。

    struct sk_buff_head *queue = &sk->sk_receive_queue;
            int _off = *off;
    
            last = (struct sk_buff *)queue;
            spin_lock_irqsave(&queue->lock, cpu_flags);
            /*遍历循环双链表*/
            skb_queue_walk(queue, skb) {
                last = skb;
                *peeked = skb->peeked;
                if (flags & MSG_PEEK) {
                    if (_off >= skb->len && (skb->len || _off ||
                                 skb->peeked)) {
                        _off -= skb->len;
                        continue;
                    }
                    skb->peeked = 1;
                    atomic_inc(&skb->users);
                } else
                    __skb_unlink(skb, queue);
    
                spin_unlock_irqrestore(&queue->lock, cpu_flags);
                *off = _off;
                return skb;
            }

     在获取到skb之后,下面该复制数据了,具体由skb_copy_datagram_iovec函数完成,完成后更新了下msg->msgname和msg_namelen字段。看下skb_copy_datagram_iovec复制函数,该函数也比较清晰,但是涉及到skb的组织方式,又稍显复杂,本节不打算很详细的讲述skb的组织,后面单独开一节来介绍,该函数主要分为三部分:

    1、复制头部

    2、复制分片

    3、复制子skb

     该部分内容在详细介绍skb的时候再做介绍,这样复制到iov中后,接收过程就完成了,用户空间就可以正常读取数据了……

    以马内利

    参考资料

    linux3.10.1内核源码

  • 相关阅读:
    在qt中用tcp传输xml消息
    Response.Redirect 打开新窗体的两种方法
    div:给div加滚动栏 div的滚动栏设置
    高速排序算法
    海量数据处理面试题集锦
    VB中DateDiff 函数解释
    FusionCharts简单教程(一)---建立第一个FusionCharts图形
    mysql 加入列,改动列,删除列。
    Lucene教程具体解释
    Windows7下的免费虚拟机(微软官方虚拟机)
  • 原文地址:https://www.cnblogs.com/ck1020/p/7127323.html
Copyright © 2011-2022 走看看