深入理解Linux网络(二):UDP接收内核探究

一、UDP 协议处理

udp 协议的处理函数是 udp_rcv。

//file: net/ipv4/udp.c
int udp_rcv(struct sk_buff *skb)
{
 return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable, int proto)
{
 sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
 if (sk != NULL) {
   int ret = udp_queue_rcv_skb(sk, skb);
 }
 icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
}

__udp4_lib_lookup_skb 是根据 skb 来寻找对应的socket,当找到以后将数据包放到
socket 的缓存队列⾥。如果没有找到,则发送⼀个⽬标不可达的 icmp 包。

//file: net/ipv4/udp.c
int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{ 
 ......
 if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))
   goto drop;
 rc = 0;
 ipv4_pktinfo_prepare(skb);
 bh_lock_sock(sk);
 if (!sock_owned_by_user(sk))
   rc = __udp_queue_rcv_skb(sk, skb);
 else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {
   bh_unlock_sock(sk);
   goto drop;
 }
 bh_unlock_sock(sk);
 return rc;
}

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

二、recvfrom 系统调⽤实现

代码⾥调⽤的 recvfrom 是⼀个 glibc 的库函数,该函数在执⾏后会将⽤户进⾏陷⼊到内核态,进⼊到 Linux 实现的系统调⽤ sys_recvfrom 。
在这里插入图片描述
socket 数据结构中的 const struct proto_ops 对应的是协议的⽅法集合。每个协议都会实现不同的⽅法集,对于IPv4 Internet 协议族来说,每种协议都有对应的处理⽅法,如下:
对于 udp 来说,是通过 inet_dgram_ops 来定义的,其中注册了 inet_recvmsg ⽅法。

//file: net/ipv4/af_inet.c
const 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.c
struct 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_recvfrom 的实现过程。
在这里插入图片描述
在 inet_recvmsg 调⽤了 sk->sk_prot->recvmsg 。

//file: net/ipv4/af_inet.c
int 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;
}

//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() 执⾏等待操作,它加⼊会让⽤户进程进⼊睡眠状态。
具体是怎么进⼊睡眠状态的,和 TCP 的实现一样,属于进程的基本知识了。

再次推荐飞哥的 《深入理解Linux网络》。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部