图解Linux网络包接收过程( 八 )

sock_owned_by_user判断的是用户是不是正在这个socker上进行系统调用(socket被占用),如果没有,那就可以直接放到socket的接收队列中 。如果有,那就通过sk_add_backlog把数据包添加到backlog队列 。当用户释放的socket的时候,内核会检查backlog队列,如果有数据再移动到接收队列中 。
sk_rcvqueues_full接收队列如果满了的话,将直接把包丢弃 。接收队列大小受内核参数net.core.rmem_max和net.core.rmem_default影响 。

recvfrom系统调用
花开两朵,各表一枝 。上面我们说完了整个Linux内核对数据包的接收和处理过程,最后把数据包放到socket的接收队列中了 。那么我们再回头看用户进程调用recvfrom后是发生了什么 。我们在代码里调用的recvfrom是一个glibc的库函数,该函数在执行后会将用户进行陷入到内核态,进入到Linux实现的系统调用sys_recvfrom 。在理解Linux对sys_revvfrom之前,我们先来简单看一下socket这个核心数据结构 。这个数据结构太大了,我们只把对和我们今天主题相关的内容画出来,如下:

图解Linux网络包接收过程

文章插图
 
图11 socket内核数据机构
socket数据结构中的const struct proto_ops对应的是协议的方法集合 。每个协议都会实现不同的方法集,对于IPv4 Internet协议族来说,每种协议都有对应的处理方法,如下 。对于udp来说,是通过inet_dgram_ops来定义的,其中注册了inet_recvmsg方法 。
//file: net/ipv4/af_inet.cconst struct proto_ops inet_stream_ops = {    ......    .recvmsg       = inet_recvmsg,    .mmap          = sock_no_mmap,    ......}const struct proto_ops inet_dgram_ops = {    ......    .sendmsg       = inet_sendmsg,    .recvmsg       = inet_recvmsg,    ......}socket数据结构中的另一个数据结构struct sock *sk是一个非常大,非常重要的子结构体 。其中的sk_prot又定义了二级处理函数 。对于UDP协议来说,会被设置成UDP协议实现的方法集udp_prot 。
//file: net/ipv4/udp.cstruct proto udp_prot = {    .name          = "UDP",    .owner         = THIS_MODULE,    .close         = udp_lib_close,    .connect       = ip4_datagram_connect,    ......    .sendmsg       = udp_sendmsg,    .recvmsg       = udp_recvmsg,    .sendpage      = udp_sendpage,    ......}看完了socket变量之后,我们再来看sys_revvfrom的实现过程 。
图解Linux网络包接收过程

文章插图
 
图12 recvfrom函数内部实现过程
在inet_recvmsg调用了sk->sk_prot->recvmsg 。
//file: net/ipv4/af_inet.cint inet_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,size_t size, int flags){      ......    err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,                   flags & ~MSG_DONTWAIT, &addr_len);    if (err >= 0)        msg->msg_namelen = addr_len;    return err;}上面我们说过这个对于udp协议的socket来说,这个sk_prot就是net/ipv4/udp.c下的struct proto udp_prot 。由此我们找到了udp_recvmsg方法 。
//file:net/core/datagram.c:EXPORT_SYMBOL(__skb_recv_datagram);struct sk_buff *__skb_recv_datagram(struct sock *sk, unsigned int flags,int *peeked, int *off, int *err){    ......    do {        struct sk_buff_head *queue = &sk->sk_receive_queue;        skb_queue_walk(queue, skb) {            ......        }        /* User doesn't want to wait */        error = -EAGAIN;        if (!timeo)            goto no_packet;    } while (!wait_for_more_packets(sk, err, &timeo, last));}终于我们找到了我们想要看的重点,在上面我们看到了所谓的读取过程,就是访问sk->sk_receive_queue 。如果没有数据,且用户也允许等待,则将调用wait_for_more_packets()执行等待操作,它加入会让用户进程进入睡眠状态 。

总结
网络模块是Linux内核中最复杂的模块了,看起来一个简简单单的收包过程就涉及到许多内核组件之间的交互,如网卡驱动、协议栈,内核ksoftirqd线程等 。看起来很复杂,本文想通过图示的方式,尽量以容易理解的方式来将内核收包过程讲清楚 。现在让我们再串一串整个收包过程 。


推荐阅读