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这个核心数据结构 。这个数据结构太大了,我们只把对和我们今天主题相关的内容画出来,如下:

文章插图
图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的实现过程 。
文章插图
图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线程等 。看起来很复杂,本文想通过图示的方式,尽量以容易理解的方式来将内核收包过程讲清楚 。现在让我们再串一串整个收包过程 。
推荐阅读
- 三种使用AI攻击网络安全的方法
- 理解了Linux I/O机制,才能真的明白“什么是多线程”
- Linux网络配置
- 带你重新认识Linux系统的inode
- 古剑奇谭网络版野外pvp 古剑奇谭ol剧情
- 从Linux源码看Socket的listen及连接队列
- linux网络编程常见API详解
- 了解神经网络和模型泛化
- TCP/IP 基础知识总结
- 如何在电脑上安装网络打印机?详细教程全部教给你
