1. 程式人生 > >內核通信之Netlink源碼分析-用戶內核通信原理3

內核通信之Netlink源碼分析-用戶內核通信原理3

light needed 其他 oid .cn wal sock irq struct

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內核源碼

內核通信之Netlink源碼分析-用戶內核通信原理3