send过程分析
应用层
send函数首先调用__sys_sendto函数:
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
unsigned int, flags, struct sockaddr __user *, addr,
int, addr_len)
{
return __sys_sendto(fd, buff, len, flags, addr, addr_len);
}
__sys_sendto创建并初始化了struct socket和struct msghdr,然后把它们传递给sock_sendmsg,socket_sendmsg调用sock_sendmsg_nosec。
/*
* Send a datagram to a given address. We move the address into kernel
* space and check the user space data area is readable before invoking
* the protocol.
*/
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
struct sockaddr __user *addr, int addr_len)
/**
* sock_sendmsg - send a message through @sock
* @sock: socket
* @msg: message to send
*
* Sends @msg through @sock, passing through LSM.
* Returns the number of bytes sent, or an error code.
*/
int sock_sendmsg(struct socket *sock, struct msghdr *msg)
sock_sendmsg_nosec有3个函数调用,其中sock->ops->sendmsg是根据不同的协议调用不同的sendmsg函数,op的类型是struct proto_ops ,在文件af_inet.c中定义了sendmsg对应的函数。最终它调用的是inet_sendmsg函数。
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg,
inet_sendmsg, sock, msg,
msg_data_left(msg));
BUG_ON(ret == -EIOCBQUEUED);
return ret;
}
inet_sendmsg调用的是tcp_sendmsg,属于传输层的函数。
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock->sk;
if (unlikely(inet_send_prepare(sk)))
return -EAGAIN;
return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg, udp_sendmsg,
sk, msg, size);
}
传输层
tcp_sendmsg调用了tcp_sendmsg_locked
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
int ret;
lock_sock(sk);
ret = tcp_sendmsg_locked(sk, msg, size);
release_sock(sk);
return ret;
}
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
在 tcp_sendmsg_locked 中, 完成的是将所有的数据组织成发送队列,这个发送队列是struct sock 结构中的一个域 sk_write_queue, 这个队列的每一个元素是一个 skb, 里面存放的就是待发送的数据。 然后调用了 tcp_push()函数。
static void tcp_push(struct sock *sk, int flags, int mss_now,
int nonagle, int size_goal)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
skb = tcp_write_queue_tail(sk);
if (!skb)
return;
if (!(flags & MSG_MORE) || forced_push(tp))
tcp_mark_push(tp, skb);
tcp_mark_urg(tp, flags);
if (tcp_should_autocork(sk, skb, size_goal)) {
/* avoid atomic op if TSQ_THROTTLED bit is already set */
if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);
}
/* It is possible TX completion already happened
* before we set TSQ_THROTTLED.
*/
if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)
return;
}
if (flags & MSG_MORE)
nonagle = TCP_NAGLE_CORK;
__tcp_push_pending_frames(sk, mss_now, nonagle);
}
在 tcp 协议的头部有几个标志字段: URG、 ACK、 RSH、 RST、 SYN、 FIN, tcp_push 中会判断这个 skb 的元素是否需要 push, 如果需要就将 tcp 头部字段的 push 置一 。
tcp_push调用__tcp_push_pending_frames ,__tcp_push_pending_frames 调用tcp_write_xmit 发送数据。
tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb传输数据,实际调用的是__tcp_transmit_skb。
__tcp_transmit_skb的icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
,在tcp_ipv4.c文件,可以看到该语句调用的queue_xmit绑定的是ip_queue_xmit函数。接下来数据就进入到了网络层。
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
...
网络层
ip_queue_xmit调用__ip_queue_xmit完成所需的功能。
int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl,
__u8 tos)
{
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options_rcu *inet_opt;
struct flowi4 *fl4;
struct rtable *rt;
struct iphdr *iph;
int res;
/* Skip all of this if the packet is already routed,
* f.e. by something like SCTP.
*/
rcu_read_lock();
/*
* 如果待输出的数据包已准备好路由缓存,
* 则无需再查找路由,直接跳转到packet_routed
* 处作处理。
*/
inet_opt = rcu_dereference(inet->inet_opt);
fl4 = &fl->u.ip4;
rt = skb_rtable(skb);
if (rt)
goto packet_routed;
/* Make sure we can route this packet. */
/*
* 如果输出该数据包的传输控制块中
* 缓存了输出路由缓存项,则需检测
* 该路由缓存项是否过期。
* 如果过期,重新通过输出网络设备、
* 目的地址、源地址等信息查找输出
* 路由缓存项。如果查找到对应的路
* 由缓存项,则将其缓存到传输控制
* 块中,否则丢弃该数据包。
* 如果未过期,则直接使用缓存在
* 传输控制块中的路由缓存项。
*/
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (!rt) { /* 缓存过期 */
__be32 daddr;
/* Use correct destination address if we have options. */
daddr = inet->inet_daddr; /* 目的地址 */
if (inet_opt && inet_opt->opt.srr)
daddr = inet_opt->opt.faddr; /* 严格路由选项 */
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/ /* 查找路由缓存 */
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet->inet_saddr,
inet->inet_dport,
inet->inet_sport,
sk->sk_protocol,
RT_CONN_FLAGS(sk),
sk->sk_bound_dev_if);
if (IS_ERR(rt))
goto no_route;
sk_setup_caps(sk, &rt->dst); /* 设置控制块的路由缓存 */
}
skb_dst_set_noref(skb, &rt->dst);/* 将路由设置到skb中 */
packet_routed:
if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
goto no_route;
/* OK, we know where to send it, allocate and build IP header. */
/*
* 设置IP首部中各字段的值。如果存在IP选项,
* 则在IP数据包首部中构建IP选项。
*/
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);/* 构造ip头 */
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl(inet, &rt->dst);
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
/* Transport layer set skb->h.foo itself. */
/* 构造ip选项 */
if (inet_opt && inet_opt->opt.optlen) {
iph->ihl += inet_opt->opt.optlen >> 2;
ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
}
ip_select_ident_segs(net, skb, sk,
skb_shinfo(skb)->gso_segs ?: 1);
/* TODO : should we use skb->sk here instead of sk ? */
/*
* 设置输出数据包的QoS类型。
*/
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
res = ip_local_out(net, sk, skb); /* 输出 */
rcu_read_unlock();
return res;
no_route:
rcu_read_unlock();
/*
* 如果查找不到对应的路由缓存项,
* 在此处理,将该数据包丢弃。
*/
IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}
最后调用 ip_local_out 发送数据包 ,再调用__ip_local_out
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
ip_send_check(iph);
/* if egress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
skb = l3mdev_ip_out(sk, skb);
if (unlikely(!skb))
return 0;
skb->protocol = htons(ETH_P_IP);
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
net, sk, skb, NULL, skb_dst(skb)->dev,
dst_output);
}
最后调用dst_output(),实际调用了skb_dst(skb)->output(skb),随后调用ip_finish_output,观察数据报长度是否大于MTU,若大于,则调用ip_fragment分片,否则调用ip_finish_output2输出。在ip_finish_output2函数中会检测skb的前部空间是否还能存储链路层首部。如果不够,就会申请更大的存储空间,最终会调用邻居子系统的输出函数neigh_output进行输出,输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。
static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
unsigned int hh_alen = 0;
unsigned int seq;
unsigned int hh_len;
do {
seq = read_seqbegin(&hh->hh_lock);
hh_len = READ_ONCE(hh->hh_len);
if (likely(hh_len <= HH_DATA_MOD)) {
hh_alen = HH_DATA_MOD;
/* skb_push() would proceed silently if we have room for
* the unaligned size but not for the aligned size:
* check headroom explicitly.
*/
if (likely(skb_headroom(skb) >= HH_DATA_MOD)) {
/* this is inlined by gcc */
memcpy(skb->data - HH_DATA_MOD, hh->hh_data,
HH_DATA_MOD);
}
} else {
hh_alen = HH_DATA_ALIGN(hh_len);
if (likely(skb_headroom(skb) >= hh_alen)) {
memcpy(skb->data - hh_alen, hh->hh_data,
hh_alen);
}
}
} while (read_seqretry(&hh->hh_lock, seq));
if (WARN_ON_ONCE(skb_headroom(skb) < hh_alen)) {
kfree_skb(skb);
return NET_XMIT_DROP;
}
__skb_push(skb, hh_len);
return dev_queue_xmit(skb);
}
最后调用dev_queue_xmit进入数据链路层。
数据链路层
__dev_queue_xmit最终会调用dev_hard_start_xmit函数,该函数最终会调用xmit_one来发送一到多个数据包。
最后xmit_one调用__netdev_start_xmit向物理层发送数据,调用各网络设备实现的 ndo_start_xmit 回调函数,从而把数据发送给网卡,物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部 RAM(buffer) 之中。 在数据拷贝中, 同时加入符合以太网协议的相关 header, IFG、 前导符和 CRC。 对于以太网网络,物理层发送采用 CSMA/CD,即在发送过程中侦听链路冲突。一旦网卡完成报文发送, 将产生中断通知 CPU, 然后驱动层中的中断处理程序就可以删除保存的 skb 了。
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops,
struct sk_buff *skb, struct net_device *dev,
bool more)
{
__this_cpu_write(softnet_data.xmit.more, more);
return ops->ndo_start_xmit(skb, dev);
}
recv过程分析
应用层
recv函数调用__sys_recvfrom
/*
* Receive a frame from the socket and optionally record the address of the
* sender. We verify the buffers are writable and if needed move the
* sender address from kernel to user space.
*/
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
struct socket *sock;
struct iovec iov;
struct msghdr msg;
struct sockaddr_storage address;
int err, err2;
int fput_needed;
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
msg.msg_control = NULL;
msg.msg_controllen = 0;
/* Save some cycles and don‘t copy the address if not needed */
msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
/* We assume all kernel code knows the size of sockaddr_storage */
msg.msg_namelen = 0;
msg.msg_iocb = NULL;
msg.msg_flags = 0;
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
err = sock_recvmsg(sock, &msg, flags);
if (err >= 0 && addr != NULL) {
err2 = move_addr_to_user(&address,
msg.msg_namelen, addr, addr_len);
if (err2 < 0)
err = err2;
}
fput_light(sock->file, fput_needed);
out:
return err;
}
和send类似,调用路径为__sys_recvfrom -> sock_recvmsg ->sock-recvmsg_nosec -> inet_recvmsg
。
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
int flags)
{
struct sock *sk = sock->sk;
int addr_len = 0;
int err;
if (likely(!(flags & MSG_ERRQUEUE)))
sock_rps_record_flow(sk);
err = INDIRECT_CALL_2(sk->sk_prot->recvmsg, tcp_recvmsg, udp_recvmsg,
sk, msg, size, flags & MSG_DONTWAIT,
flags & ~MSG_DONTWAIT, &addr_len);
if (err >= 0)
msg->msg_namelen = addr_len;
return err;
}
最后inet_recvmsg调用tcp_recvmsg,进入到传输层。
传输层
传输层 TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。
调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态。
如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。
网络层
\1. IP 层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果
需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing
netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
\2. ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用
ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是
丢弃:
\3.
如果是发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP
packet),然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的
protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),
icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而
处理流程进入 TCP 栈。
如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用 dst_input 函
数。该函数会 处理 Netfilter Hook;执行 IP fragmentation;调用 dev_queue_xmit,进入
链路层处理流程。
ip_rcv_finish函数如下所示:
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
int ret;
/* if ingress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
skb = l3mdev_ip_rcv(skb);
if (!skb)
return NET_RX_SUCCESS;
ret = ip_rcv_finish_core(net, sk, skb, dev, NULL);
if (ret != NET_RX_DROP)
ret = dst_input(skb);
return ret;
}
ip_rcv_finish 函数最终会调用ip_route_input函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
如果是发到本机的话,调用ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用ip_local_deliver函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。
如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用dst_input函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用 dev_queue_xmit,进入链路层处理流程
数据链路层
一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过
DMA 传送到位于 linux kernel 内存中的 rx_ring。
网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括
分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从
数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例
如skb->protocol;
终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据
帧。
内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:
(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的
netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。
该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从
rx_ring 中被删除,进入 netif _receive_skb 处理流程。