linux操作系统:发送网络包

从VFS层到IP层是如何发送网络包的

解析socket的write操作

socket对于用户来讲,是一个文件一样的存在,拥有一个文件描述符。因而对于网络包的发送,我们可以使用对于socket文件的写入系统调用,也就是write系统调用。

对于每一个打开的文件都有一个struct file结构,write系统调用会最终调用struct file结构指向的file_operations操作。

对于socket来讲,它的file_operations定义如下:

static const struct file_operations socket_file_ops = {
    
    
	.owner =	THIS_MODULE,
	.llseek =	no_llseek,
	.read_iter =	sock_read_iter,
	.write_iter =	sock_write_iter,
	.poll =		sock_poll,
	.unlocked_ioctl = sock_ioctl,
	.mmap =		sock_mmap,
	.release =	sock_close,
	.fasync =	sock_fasync,
	.sendpage =	sock_sendpage,
	.splice_write = generic_splice_sendpage,
	.splice_read =	sock_splice_read,
};

按照文件系统的写入流程,调用的是sock_write_iter

static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    
    
	struct file *file = iocb->ki_filp;
	struct socket *sock = file->private_data;
	struct msghdr msg = {
    
    .msg_iter = *from,
			     .msg_iocb = iocb};
	ssize_t res;
......
	res = sock_sendmsg(sock, &msg);
	*from = msg.msg_iter;
	return res;
}

在 sock_write_iter 中,我们通过 VFS 中的 struct file,将创建好的 socket 结构拿出来,然后调用 sock_sendmsg。而 sock_sendmsg 会调用 sock_sendmsg_nosec。

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
    
    
	int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
......
}

这里调用了 socket 的 ops 的 sendmsg。根据 inet_stream_ops 的定义,我们这里调用的是 inet_sendmsg。

int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
    
    
	struct sock *sk = sock->sk;
......
	return sk->sk_prot->sendmsg(sk, msg, size);
}

这里面,从 socket 结构中,我们可以得到更底层的 sock 结构,然后调用 sk_prot 的 sendmsg 方法。

解析 tcp_sendmsg 函数

根据 tcp_prot 的定义,我们调用的是 tcp_sendmsg。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    
    
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;
	int flags, err, copied = 0;
	int mss_now = 0, size_goal, copied_syn = 0;
	long timeo;
......
	/* Ok commence sending. */
	copied = 0;
restart:
	mss_now = tcp_send_mss(sk, &size_goal, flags);
 
	while (msg_data_left(msg)) {
    
    
		int copy = 0;
		int max = size_goal;
 
		skb = tcp_write_queue_tail(sk);
		if (tcp_send_head(sk)) {
    
    
			if (skb->ip_summed == CHECKSUM_NONE)
				max = mss_now;
			copy = max - skb->len;
		}
 
		if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
    
    
			bool first_skb;
 
new_segment:
			/* Allocate new segment. If the interface is SG,
			 * allocate skb fitting to single page.
			 */
			if (!sk_stream_memory_free(sk))
				goto wait_for_sndbuf;
......
			first_skb = skb_queue_empty(&sk->sk_write_queue);
			skb = sk_stream_alloc_skb(sk,
						  select_size(sk, sg, first_skb),
						  sk->sk_allocation,
						  first_skb);
......
			skb_entail(sk, skb);
			copy = size_goal;
			max = size_goal;
......
		}
 
		/* Try to append data to the end of skb. */
		if (copy > msg_data_left(msg))
			copy = msg_data_left(msg);
 
		/* Where to copy to? */
		if (skb_availroom(skb) > 0) {
    
    
			/* We have some space in skb head. Superb! */
			copy = min_t(int, copy, skb_availroom(skb));
			err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
......
		} else {
    
    
			bool merge = true;
			int i = skb_shinfo(skb)->nr_frags;
			struct page_frag *pfrag = sk_page_frag(sk);
......
			copy = min_t(int, copy, pfrag->size - pfrag->offset);
......
			err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
						       pfrag->page,
						       pfrag->offset,
						       copy);
......
			pfrag->offset += copy;
		}
 
......
		tp->write_seq += copy;
		TCP_SKB_CB(skb)->end_seq += copy;
		tcp_skb_pcount_set(skb, 0);
 
		copied += copy;
		if (!msg_data_left(msg)) {
    
    
			if (unlikely(flags & MSG_EOR))
				TCP_SKB_CB(skb)->eor = 1;
			goto out;
		}
 
		if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
			continue;
 
		if (forced_push(tp)) {
    
    
			tcp_mark_push(tp, skb);
			__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
		} else if (skb == tcp_send_head(sk))
			tcp_push_one(sk, mss_now);
		continue;
......
	}
......
}

tcp_sendmsg 的实现还是很复杂的,这里面做了这样几件事情。

  • msg是用户要写入的数据,这个数据要拷贝到内核协议栈里面去发送;在内核协议栈里面,网络包的数据都是由struct sk_buff维护的,因而第一件事情就是找到一个空闲的内存空间,将用户要写入的数据,拷贝到struct sk_buff的管辖范围内。而第二件事情就是发送struct sk_buff。
  • 在tcp_sendmsg中,我们首先通过强制类型转换,将sock结构转换为struct tcp_sock,这个是维护TCP连接状态的重要数据结构
  • 接下来是tcp_sendmsg的第一件事情,把数据拷贝到struct sk_buff
  • 我们先声明一个变量copied,初始化为0,这表示拷贝了多少数据。紧接着是一个循环,while(msg_data_left(msg)),也即是用户的数据没有发送完毕,就一直循环。循环里声明了一个copy变量,表示这次拷贝的数值,在循环的最后有copied += copy,将每次拷贝的数量都加起来

我们这里只需要看一次循环做了哪些事情。
(1) 第一步: tcp_write_queue_tail 从TCP写入队列sk_write_queue中拿出最后一个struct sk_buff,在这个写入队列中排满了要发送的struct sk_buff,为什么要拿最后一个呢?这里面只有最后一个,可能会因为上次用户给的数据太少,而没有填满
(2) 第二步,tcp_send_mss会计算MSS。这是什么呢?这个意思是说,我们在网络上传输的网络包的大小是有限制的,而这个限制在最底层就有

  • MTU(Maximum Transmission Unit,最大传输单元)是二层的一个定义。以以太网为例,MTU 为 1500 个 Byte,前面有 6 个 Byte 的目标 MAC 地址,6 个 Byte 的源 MAC 地址,2 个 Byte 的类型,后面有 4 个 Byte 的 CRC 校验,共 1518 个 Byte。
  • 在IP层,一个IP数据报在以太网中传输,如果它的长度大于该MTU值,就要进行分片传输
  • 在TCP层有一个MSS(Maximum Segment Size,最大分段大小),等于 MTU 减去 IP 头,再减去 TCP 头。也就是,也就是,在不分片的情况下,TCP里面放的最大内容
  • 在这里,max 是 struct sk_buff 的最大数据长度,skb->len 是当前已经占用的 skb 的数据长度,相减得到当前 skb 的剩余数据空间。

(3)第三步,如果copy小于0,说明最后一个struct sk_buff已经没地方可以存放了,需要调用sk_stream_alloc_skb,重新分配struct sk_buff,然后调用skb_entail,将新分配的sk_buff放到队列尾部

  • struct sk_buff是存储网络包的重要数据结构,在应用层数据包叫做data,在TCP层叫做segment,在IP层叫做packet,将数据链路层叫做frame。在struct sk_buff,首先是一个链表,将struc sk_buff结构串起来
  • 接下来,我们从headers_start开始,到headers_end结束,里面都是各层次的头的位置。这里面有二层的mac_header、三层的network_header和四层的transport_header
struct sk_buff {
    
    
	union {
    
    
		struct {
    
    
			/* These two members must be first. */
			struct sk_buff		*next;
			struct sk_buff		*prev;
......
		};
		struct rb_node	rbnode; /* used in netem & tcp stack */
	};
......
	/* private: */
	__u32			headers_start[0];
	/* public: */
......
	__u32			priority;
	int			skb_iif;
	__u32			hash;
	__be16			vlan_proto;
	__u16			vlan_tci;
......
	union {
    
    
		__u32		mark;
		__u32		reserved_tailroom;
	};
 
	union {
    
    
		__be16		inner_protocol;
		__u8		inner_ipproto;
	};
 
	__u16			inner_transport_header;
	__u16			inner_network_header;
	__u16			inner_mac_header;
 
	__be16			protocol;
	__u16			transport_header;
	__u16			network_header;
	__u16			mac_header;
 
	/* private: */
	__u32			headers_end[0];
	/* public: */
 
	/* These elements must be at the end, see alloc_skb() for details.  */
	sk_buff_data_t		tail;
	sk_buff_data_t		end;
	unsigned char		*head,
				*data;
	unsigned int		truesize;
	refcount_t		users;
};
  • 最后几项,head执行分配的内存块起始地址。data这个指针指向的位置是可变的。它有可能随着报文所处的层次而变动。
    • 当接收报文时,从网卡驱动开始,通过协议层层层往上传送数据报,通过增加skb->data的值,来逐步剥离协议首部。
    • 而要发送报文时,各协议会创建sk_buff{},在经过各下层协议时,通过减少skb_data的值来增加协议首部。
  • tail指向数据的结尾,end指向分配的内存块的结束地址。

要分配一个这样的结构,sk_stream_alloc_skb会最终调用到__alloc_skb。在这个函数里面,除了分配一个sk_buff结构之外,还要分配sk_buff指向的数据区域。这段数据区域分为下面这几个部分

  • 第一部分是连续的数据区域。
  • 紧接着是第二部分,一个 struct skb_shared_info 结构。
    • 这个结构是对于网络包发送过程的一个优化,因为传输层之上就是应用层了。按照 TCP 的定义,应用层感受不到下面的网络层的 IP 包是一个个独立的包的存在的。反正就是一个流,往里写就是了,可能一下子写多了,超过了一个 IP 包的承载能力,就会出现上面 MSS 的定义,拆分成一个个的 Segment 放在一个个的 IP 包里面,也可能一次写一点,一次写一点,这样数据是分散的,在 IP 层还要通过内存拷贝合成一个 IP 包。
    • 为了减少内存拷贝的代价,有的网络设备支持分散聚合(Scatter/Gather)I/O,顾名思义,就是 IP 层没必要通过内存拷贝进行聚合,让散的数据零散的放在原处,在设备层进行聚合。如果使用这种模式,网络包的数据就不会放在连续的数据区域,而是放在 struct skb_shared_info 结构里面指向的离散数据,skb_shared_info 的成员变量 skb_frag_t frags[MAX_SKB_FRAGS],会指向一个数组的页面,就不能保证连续了。
      在这里插入图片描述

(4)于是我们就有了第四步,在注释/* Where to copy to? */后面有个 if-else 分支。if 分支就是 skb_add_data_nocache 将数据拷贝到连续的数据区域。else 分支就是 skb_copy_to_page_nocache 将数据拷贝到 struct skb_shared_info 结构指向的不需要连续的页面区域。

(5)第五步,将要发送网络包。

  • 第一种情况是累积的数据报数目太多了,因而我们需要通过调用__tcp_push_pending_frames发送网络包
  • 第二种情况是,这是第一个网络包,需要马上发送,调用tcp_push_one。

无论 __tcp_push_pending_frames 还是 tcp_push_one,都会调用 tcp_write_xmit 发送网络包。

至此,tcp_sendmsg 解析完了。

解析tcp_write_xmit函数

接下来我们来看,tcp_write_xmit 是如何发送网络包的。

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp)
{
    
    
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;
	unsigned int tso_segs, sent_pkts;
	int cwnd_quota;
......
	max_segs = tcp_tso_segs(sk, mss_now);
	while ((skb = tcp_send_head(sk))) {
    
    
		unsigned int limit;
......
		tso_segs = tcp_init_tso_segs(skb, mss_now);
......
		cwnd_quota = tcp_cwnd_test(tp, skb);
......
		if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
    
    
			is_rwnd_limited = true;
			break;
		}
......
		limit = mss_now;
        if (tso_segs > 1 && !tcp_urg_mode(tp))
            limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle);
 
		if (skb->len > limit &&
		    unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
			break;
......
		if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
			break;
 
repair:
		/* Advance the send_head.  This one is sent out.
		 * This call will increment packets_out.
		 */
		tcp_event_new_data_sent(sk, skb);
 
		tcp_minshall_update(tp, mss_now, skb);
		sent_pkts += tcp_skb_pcount(skb);
 
		if (push_one)
			break;
	}
......
}

这里面主要的逻辑是一个循环,用来处理发送队列,只要队列不空,就会发送。

在一个循环中,涉及 TCP 层的很多传输算法,我们来一一解析。

(1)TSO(TCP Segmentation Offload)。如果发送的网络包非常大,那就要进行分段。分段这个事情可以由协议栈在内核做,缺点是比较耗CPU,另一种方式是延迟到硬件网卡中去做,需要网卡支持对大数据包进行自动分段,可以降低CPU负载

  • 在代码中,tcp_init_tso_segs 会调用tcp_set_skb_tso_segs。这里面有这样的语句:DIV_ROUND_UP(skb->len, mss_now)。也就是sk_buff的长度除以mss_now,应该分成几个段。如果算出来要分成多个段,接下来就是要看,是在这里(协议栈的代码里面)分好,还是等待到了底层网卡再分
  • 于是,调用函数tcp_mss_split_point,开始计算切分的limit。这里面会计算max_len = mss_now * max_segs,根据现在不切分来计算limit,所以下一步的判断中,大部分情况下tso_fragment不会被调用,等待到了底层网卡来切分

(2)拥塞窗口(cwnd,congestion window),也就是说为了避免拼命发送,把网络塞满了,定义一个窗口的概念,在这个窗口之内的才能发送,超过这个窗口的就不能发送,来控制发送的频率。那窗口大小是多少呢?就是遵循下面这个著名的拥塞窗口变化图。
在这里插入图片描述

  • 一开始的窗口只有一个mss大小叫做slow start(慢启动)。一开始的增长速度是很快的,翻倍增长。一旦到达一个临界值ssthresh,就变成线性增长,这个叫做拥塞避免。什么时候算真正拥塞呢?就是出现了丢包。一旦丢包,一种方法是马上降回到一个mss,然后重复先翻倍再线性对的过程。如果觉得太激进,也可以有第二种方法,就是降到当前cwnd的一半,然后进行线性增长。
  • 在代码中,tcp_cwnd_test 会将当前的 snd_cwnd,减去已经在窗口里面尚未发送完毕的网络包,那就是剩下的窗口大小 cwnd_quota,也即就能发送这么多了。

(3)接收窗口rwnd(receive window),也叫做滑动窗口。如果说拥塞窗口是为了避免把网络塞满,在出现丢包的时候减少发送速度,那么滑动窗口就是为了避免把接收方塞满,而控制发送速度。
在这里插入图片描述
滑动窗口,其实就是接收方告诉发送方自己的网络包的接收能力,超过这个能力,我就受不了了。因为滑动窗口的存在,将发送方的缓存分成了四个部分。

  • 第一部分:发送了并且已经确认的。这部分是已经发送完毕的网络包,这部分没有用了,可以回收。
  • 第二部分:发送了但尚未确认的。这部分,发送方要等待,万一发送不成功,还要重新发送,所以不能删除。
  • 第三部分:没有发送,但是已经等待发送的。这部分是接收方空闲的能力,可以马上发送,接收方收得了。
  • 第四部分:没有发送,并且暂时还不会发送的。这部分已经超过了接收方的接收能力,再发送接收方就收不了了。

因为滑动窗口的存在,接收方的缓存也要分成了三个部分。

  • 第一部分:接受并且确认过的任务。这部分完全接收成功了,可以交给应用层了
  • 第二部分:还没接收,但是马上就能接收的任务。这部分有的网络包到达了,但是还没确认,不算完全完毕,有的还没有到达,那就是接收方能够接受的最大的网络包数量。
  • 第三部分:还没接收,也没法接收的任务。这部分已经超出接收方能力。
    在这里插入图片描述

在网络包的交互过程中,接收方会将第二部分的大小,作为AdvertisedWindow 发送给发送方,发送方就可以根据它来调整发送速度了。

在tcp_snd_wnd_test 函数中,会判断sk_buff中的end_seq和tcp_wnd_end(tp)之间的关系,也就是sk_buff是否在滑动窗口的允许范围之内。如果不再范围内,说明发送要受到限制了,我们就要把is_rwnd_limited 设置为true。

接下来,tcp_mss_split_point 函数就要被调用了

static unsigned int tcp_mss_split_point(const struct sock *sk,
                                        const struct sk_buff *skb,
                                        unsigned int mss_now,
                                        unsigned int max_segs,
                                        int nonagle)
{
    
    
        const struct tcp_sock *tp = tcp_sk(sk);
        u32 partial, needed, window, max_len;
 
        window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
        max_len = mss_now * max_segs;
 
        if (likely(max_len <= window && skb != tcp_write_queue_tail(sk)))
                return max_len;
 
        needed = min(skb->len, window);
 
        if (max_len <= needed)
                return max_len;
......
        return needed;
}

这里面除了会判断上面讲的,是否会因为超出mss而分段,还会判断另一个条件,就是是否在滑动窗口的运行范围之内,如果小于窗口的大小,也需要分段,也就是需要调用tso_fragment。

在一个循环的最后,是调用tcp_transmit_skb,真正去发送一个网络包

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
                gfp_t gfp_mask)
{
    
    
    const struct inet_connection_sock *icsk = inet_csk(sk);
    struct inet_sock *inet;
    struct tcp_sock *tp;
    struct tcp_skb_cb *tcb;
    struct tcphdr *th;
    int err;
 
    tp = tcp_sk(sk);
 
    skb->skb_mstamp = tp->tcp_mstamp;
    inet = inet_sk(sk);
    tcb = TCP_SKB_CB(skb);
    memset(&opts, 0, sizeof(opts));
 
    tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
    skb_push(skb, tcp_header_size);
 
    /* Build TCP header and checksum it. */
    th = (struct tcphdr *)skb->data;
    th->source      = inet->inet_sport;
    th->dest        = inet->inet_dport;
    th->seq         = htonl(tcb->seq);
    th->ack_seq     = htonl(tp->rcv_nxt);
    *(((__be16 *)th) + 6)   = htons(((tcp_header_size >> 2) << 12) |
                    tcb->tcp_flags);
 
    th->check       = 0;
    th->urg_ptr     = 0;
......
    tcp_options_write((__be32 *)(th + 1), tp, &opts);
    th->window  = htons(min(tp->rcv_wnd, 65535U));
......
    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
......
}

tcp_transmit_skb 这个函数比较长,主要做了两件事情,第一件事情就填充TCP头
在这里插入图片描述
这里面有源端口,设置为 inet_sport,有目标端口,设置为 inet_dport;有序列号,设置为 tcb->seq;有确认序列号,设置为 tp->rcv_nxt。我们把所有的 flags 设置为 tcb->tcp_flags。设置选项为 opts。设置窗口大小为 tp->rcv_wnd。

全部设置完毕之后,就会调用 icsk_af_ops 的 queue_xmit 方法,icsk_af_ops 指向 ipv4_specific,也即调用的是 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,
        .conn_request      = tcp_v4_conn_request,
        .syn_recv_sock     = tcp_v4_syn_recv_sock,
        .net_header_len    = sizeof(struct iphdr),
        .setsockopt        = ip_setsockopt,
        .getsockopt        = ip_getsockopt,
        .addr2sockaddr     = inet_csk_addr2sockaddr,
        .sockaddr_len      = sizeof(struct sockaddr_in),
        .mtu_reduced       = tcp_v4_mtu_reduced,
};

小结

上文解析了发送一个网络包的一部分过程,如下图:

在这里插入图片描述
这个过程分成几个层次。

  • VFS 层:write 系统调用找到 struct file,根据里面的 file_operations 的定义,调用 sock_write_iter 函数。sock_write_iter 函数调用 sock_sendmsg 函数。
  • Socket 层:从 struct file 里面的 private_data 得到 struct socket,根据里面 ops 的定义,调用 inet_sendmsg 函数。
  • Sock 层:从 struct socket 里面的 sk 得到 struct sock,根据里面 sk_prot 的定义,调用 tcp_sendmsg 函数。
  • TCP 层:tcp_sendmsg 函数会调用 tcp_write_xmit 函数,tcp_write_xmit 函数会调用 tcp_transmit_skb,在这里实现了 TCP 层面向连接的逻辑。
  • IP 层:扩展 struct sock,得到 struct inet_connection_sock,根据里面 icsk_af_ops 的定义,调用 ip_queue_xmit 函数。

从IP层到MAC层

解析ip_queue_xmit函数

从ip_queue_xmit函数开始,我们就要进入IP层的发送逻辑了

int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
    
    
    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;
 
    inet_opt = rcu_dereference(inet->inet_opt);
    fl4 = &fl->u.ip4;
    rt = skb_rtable(skb);
    /* 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;
 ......
        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);
 
packet_routed:
    /* OK, we know where to send it, allocate and build IP header. */
    skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
    skb_reset_network_header(skb);
    iph = ip_hdr(skb);
    *((__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. */
 
    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 ? */
    skb->priority = sk->sk_priority;
    skb->mark = sk->sk_mark;
 
    res = ip_local_out(net, sk, skb);
......
}

在ip_queue_xmit中,也就是IP层的发送函数中,有三部分逻辑。

(1)第一部分,选取路由,也就是我要发送的这个包应该从哪个网卡出去。

这件事情主要由 ip_route_output_ports 函数完成。接下来的调用链为:ip_route_output_ports->ip_route_output_flow->__ip_route_output_key->ip_route_output_key_hash->ip_route_output_key_hash_rcu。

struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4, struct fib_result *res, const struct sk_buff *skb)
{
    
    
	struct net_device *dev_out = NULL;
	int orig_oif = fl4->flowi4_oif;
	unsigned int flags = 0;
	struct rtable *rth;
......
    err = fib_lookup(net, fl4, res, 0);
......
make_route:
	rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
......
}

ip_route_output_key_hash_rcu 先会调用 fib_lookup。

FIB全称是 Forwarding Information Base,转发信息表。其实就是咱们常说的路由表。

static inline int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res, unsigned int flags)
{
    
    	struct fib_table *tb;
......
	tb = fib_get_table(net, RT_TABLE_MAIN);
	if (tb)
		err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
......
}
 

路由表可以有多个,一般会有一个主表,RT_TABLE_MAIN。然后fib_table_lookup函数在这个表里面进行查找。

路由表是一个怎么样的结构呢?

路由就是在linux服务器上的路由表中配置的一条一条规则。这些规则大概就是这样的:向访问某个网段,从某个网卡出去,下一跳就是某个IP。

比如,下面拓扑图,里面的三台 Linux 机器的路由表都可以通过 ip route 命令查看。
在这里插入图片描述

# Linux 服务器 A
default via 192.168.1.1 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100 metric 100
 
# Linux 服务器 B
default via 192.168.2.1 dev eth0
192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.100 metric 100
 
# Linux 服务器做路由器
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1  
192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1  

其实,对于两端的服务器来讲,我们没有太多的路由可以选,但是对于中间的linux服务器做路由,这里有两台路可以选,一个是往左边转发,一个是往右边转发,就需要路由表的查找。

fib_table_lookup的代码逻辑比较复杂,好在注释比较清楚。因为路由表要按照前缀进行查询,希望找到最长匹配的那一个,比如192.168.2.0/24 和 192.168.0.0/16 都能匹配 192.168.2.100/24。但是,我们应该使用 192.168.2.0/24 的这一条。

为了更加方便的做这个事情,我们使用了trie树这种结构。比如我们有一系列的字符串:{bcs#, badge#, baby#, back#, badger#, badness#}。之所以每个字符串都加上#,是希望不要一个字符串成为另一个字符串的前缀。然后我们把它们放在trie树中,如下图:
在这里插入图片描述
对于将IP地址转成二进制放入trie树,也是同样的道理,可以很快进行路由的查询。

找到了路由,就知道了应该从哪个网卡发出去。

然后,ip_route_output_key_hash_rcu 会调用__mkroute_output,创建一个struct rtable,表示找到的路由表项。这个结构是由rt_dst_alloc 函数分配的

struct rtable *rt_dst_alloc(struct net_device *dev,
			    unsigned int flags, u16 type,
			    bool nopolicy, bool noxfrm, bool will_cache)
{
    
    
	struct rtable *rt;
 
	rt = dst_alloc(&ipv4_dst_ops, dev, 1, DST_OBSOLETE_FORCE_CHK,
		       (will_cache ? 0 : DST_HOST) |
		       (nopolicy ? DST_NOPOLICY : 0) |
		       (noxfrm ? DST_NOXFRM : 0));
 
	if (rt) {
    
    
		rt->rt_genid = rt_genid_ipv4(dev_net(dev));
		rt->rt_flags = flags;
		rt->rt_type = type;
		rt->rt_is_input = 0;
		rt->rt_iif = 0;
		rt->rt_pmtu = 0;
		rt->rt_gateway = 0;
		rt->rt_uses_gateway = 0;
		rt->rt_table_id = 0;
		INIT_LIST_HEAD(&rt->rt_uncached);
 
		rt->dst.output = ip_output;
		if (flags & RTCF_LOCAL)
			rt->dst.input = ip_local_deliver;
	}
 
	return rt;
}

最终返回struct rtable实例,第一部分也就完成了

(2)第二部分,就是准备IP层的头,往里面填充内容。
在这里插入图片描述
在这里面,服务类型设置为tos,标识位里面设置是否允许分片frag_off。如果不允许,而遇到MTU太小过不去的情况,就发送ICMP报错。TTL是这个包的存活时间,为了防止一个IP包迷路以后一直存活下去,每经过一个路由器TTL都减少一,减为零则“死去”。设置protocol,指的是更上层的协议,这里是TCP。源地址和目的地址由ip_copy_addrs设置。最后,设置options。

(3)第三部分,就是调用ip_local_out 发送IP包。

int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    
    
	int err;
 
	err = __ip_local_out(net, sk, skb);
	if (likely(err == 1))
		err = dst_output(net, sk, skb);
 
	return err;
}
 
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);
	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);
}

ip_local_out 先是调用__ip_local_out,然后里面调用了nf_hook。这是什么呢?nf的意思是Netfilter,这是Linux内核的一个机制,用于在网络发送和转发的关键节点加上hook函数,这些函数可以截获数据包,对数据包进行干预。

一个著名的实现,就是内核模块ip_tables。在用户态,还有一个客户端程序iptables,用命令行来干预内核的规则。

在这里插入图片描述
iptables 有表和链的概念,最终要的是两个表。

filter 表处理过滤功能,主要包含以下三个链。

  • INPUT 链:过滤所有目标地址是本机的数据包
  • FORWARD 链:过滤所有路过本机的数据包
  • OUTPUT 链:过滤所有由本机产生的数据包

nat 表主要处理网络地址转换,可以进行 SNAT(改变源地址)、DNAT(改变目标地址),包含以下三个链。

  • PREROUTING 链:可以在数据包到达时改变目标地址
  • OUTPUT 链:可以改变本地产生的数据包的目标地址
  • POSTROUTING 链:在数据包离开时改变数据包的源地址

在这里插入图片描述
在这里,网络包马上就要发出去了,因而是 NF_INET_LOCAL_OUT,也即 ouput 链,如果用户曾经在 iptables 里面写过某些规则,就会在 nf_hook 这个函数里面起作用。

ip_local_out 再调用 dst_output,就是真正的发送数据。

/* Output packet to network from transport.  */
static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    
    
	return skb_dst(skb)->output(net, sk, skb);
}

这里调用的就是 struct rtable 成员 dst 的 ouput 函数。在 rt_dst_alloc 中,我们可以看到,output 函数指向的是 ip_output。

int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    
    
	struct net_device *dev = skb_dst(skb)->dev;
	skb->dev = dev;
	skb->protocol = htons(ETH_P_IP);
 
	return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
			    net, sk, skb, NULL, dev,
			    ip_finish_output,
			    !(IPCB(skb)->flags & IPSKB_REROUTED));
}

在 ip_output 里面,我们又看到了熟悉的 NF_HOOK。这一次是 NF_INET_POST_ROUTING,也即 POSTROUTING 链,处理完之后,调用 ip_finish_output。

解析 ip_finish_output 函数

从ip_finish_output 函数开始,发送网络包的逻辑由第三层到第二层。ip_finish_output 最终调用ip_finish_output2

static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    
    
	struct dst_entry *dst = skb_dst(skb);
	struct rtable *rt = (struct rtable *)dst;
	struct net_device *dev = dst->dev;
	unsigned int hh_len = LL_RESERVED_SPACE(dev);
	struct neighbour *neigh;
	u32 nexthop;
......
	nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
	neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
	if (unlikely(!neigh))
		neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
	if (!IS_ERR(neigh)) {
    
    
		int res;
		sock_confirm_neigh(skb, neigh);
		res = neigh_output(neigh, skb);
		return res;
	}
......
}

在ip_finish_output2中,先找到struct rtable路由表里面的下一跳,下一跳一定和本机在同一个局域网中,可以通过二层进行通信,因而通过__ipv4_neigh_lookup_noref,查找如何通过二层访问下一跳。

static inline struct neighbour *__ipv4_neigh_lookup_noref(struct net_device *dev, u32 key)
{
    
    
	return ___neigh_lookup_noref(&arp_tbl, neigh_key_eq32, arp_hashfn, &key, dev);
}

__ipv4_neigh_lookup_noref是从本地的ARP表中查找下一跳的MAC地址。ARP表的定义如下:

struct neigh_table arp_tbl = {
    
    
    .family     = AF_INET,
    .key_len    = 4,    
    .protocol   = cpu_to_be16(ETH_P_IP),
    .hash       = arp_hash,
    .key_eq     = arp_key_eq,
    .constructor    = arp_constructor,
    .proxy_redo = parp_redo,
    .id     = "arp_cache",
......
    .gc_interval    = 30 * HZ, 
    .gc_thresh1 = 128,  
    .gc_thresh2 = 512,  
    .gc_thresh3 = 1024,
};

如果在ARP表中没有找到相应的项,那么调用__neigh_create 进行创建。

struct neighbour *__neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev, bool want_ref)
{
    
    
    u32 hash_val;
    int key_len = tbl->key_len;
    int error;
    struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);
    struct neigh_hash_table *nht;
 
    memcpy(n->primary_key, pkey, key_len);
    n->dev = dev;
    dev_hold(dev);
 
    /* Protocol specific setup. */
    if (tbl->constructor && (error = tbl->constructor(n)) < 0) {
    
    
......
    }
......
    if (atomic_read(&tbl->entries) > (1 << nht->hash_shift))
        nht = neigh_hash_grow(tbl, nht->hash_shift + 1);
 
    hash_val = tbl->hash(pkey, dev, nht->hash_rnd) >> (32 - nht->hash_shift);
 
    for (n1 = rcu_dereference_protected(nht->hash_buckets[hash_val],
                        lockdep_is_held(&tbl->lock));
         n1 != NULL;
         n1 = rcu_dereference_protected(n1->next,
            lockdep_is_held(&tbl->lock))) {
    
    
        if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) {
    
    
            if (want_ref)
                neigh_hold(n1);
            rc = n1;
            goto out_tbl_unlock;
        }
    }
......
    rcu_assign_pointer(n->next,
               rcu_dereference_protected(nht->hash_buckets[hash_val],
                             lockdep_is_held(&tbl->lock)));
    rcu_assign_pointer(nht->hash_buckets[hash_val], n);
......
}

__neigh_create先调用negih_alloc,创建一个struct negihbour结构,用于维护MAC地址和ARP相关的信息。这个名字也很好理解,大家都是在一个局域网里面,可以通过MACD地址访问到,当然是邻居了。

static struct neighbour *neigh_alloc(struct neigh_table *tbl, struct net_device *dev)
{
    
    
	struct neighbour *n = NULL;
	unsigned long now = jiffies;
	int entries;
......
	n = kzalloc(tbl->entry_size + dev->neigh_priv_len, GFP_ATOMIC);
	if (!n)
		goto out_entries;
 
	__skb_queue_head_init(&n->arp_queue);
	rwlock_init(&n->lock);
	seqlock_init(&n->ha_lock);
	n->updated	  = n->used = now;
	n->nud_state	  = NUD_NONE;
	n->output	  = neigh_blackhole;
	seqlock_init(&n->hh.hh_lock);
	n->parms	  = neigh_parms_clone(&tbl->parms);
	setup_timer(&n->timer, neigh_timer_handler, (unsigned long)n);
 
	NEIGH_CACHE_STAT_INC(tbl, allocs);
	n->tbl		  = tbl;
	refcount_set(&n->refcnt, 1);
	n->dead		  = 1;
......
}

在 neigh_alloc 中,我们先分配一个 struct neighbour 结构并且初始化。这里面比较重要的有两个成员,一个是arp_queue,所以上层想通过ARP获取MAC地址的任务,都放在这个队列里面。另一个是timer定时器,我们设置成,过一段时间就调用neigh_timer_handler,来处理这些ARP任务。

__neigh_create然后调用了arp_tbl的constructor函数,也就是调用arp_constructor,在这里面定义了ARP的操作arp_hh_ops

static int arp_constructor(struct neighbour *neigh)
{
    
    
	__be32 addr = *(__be32 *)neigh->primary_key;
	struct net_device *dev = neigh->dev;
	struct in_device *in_dev;
	struct neigh_parms *parms;
......
	neigh->type = inet_addr_type_dev_table(dev_net(dev), dev, addr);
 
	parms = in_dev->arp_parms;
	__neigh_parms_put(neigh->parms);
	neigh->parms = neigh_parms_clone(parms);
......
	neigh->ops = &arp_hh_ops;
......
	neigh->output = neigh->ops->output;
......
}
 
static const struct neigh_ops arp_hh_ops = {
    
    
	.family =		AF_INET,
	.solicit =		arp_solicit,
	.error_report =		arp_error_report,
	.output =		neigh_resolve_output,
	.connected_output =	neigh_resolve_output,
};

__neigh_create 最后是将创建的struct neighbour结构放入一个哈希表,从里面的代码逻辑比较容易看出,这是一个数组加链表的链式哈希表,先计算出哈希值hash_val,得到相应的链表,然后循环这个链表找到对应的项,如果找不到就在最后插入一项。

我们回到 ip_finish_output2,在 __neigh_create 之后,会调用 neigh_output 发送网络包。

static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
    
    
......
	return n->output(n, skb);
}

按照上面对于 struct neighbour 的操作函数 arp_hh_ops 的定义,output 调用的是 neigh_resolve_output。

int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
    
    
	if (!neigh_event_send(neigh, skb)) {
    
    
......
		rc = dev_queue_xmit(skb);
	}
......
}

在 neigh_resolve_output 里面,首先 neigh_event_send 触发一个事件,看能否激活 ARP。

int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
    
    
	int rc;
	bool immediate_probe = false;
 
	if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {
    
    
		if (NEIGH_VAR(neigh->parms, MCAST_PROBES) +
		    NEIGH_VAR(neigh->parms, APP_PROBES)) {
    
    
			unsigned long next, now = jiffies;
 
			atomic_set(&neigh->probes,
				   NEIGH_VAR(neigh->parms, UCAST_PROBES));
			neigh->nud_state     = NUD_INCOMPLETE;
			neigh->updated = now;
			next = now + max(NEIGH_VAR(neigh->parms, RETRANS_TIME),
					 HZ/2);
			neigh_add_timer(neigh, next);
			immediate_probe = true;
		} 
......
	} else if (neigh->nud_state & NUD_STALE) {
    
    
		neigh_dbg(2, "neigh %p is delayed\n", neigh);
		neigh->nud_state = NUD_DELAY;
		neigh->updated = jiffies;
		neigh_add_timer(neigh, jiffies +
				NEIGH_VAR(neigh->parms, DELAY_PROBE_TIME));
	}
 
	if (neigh->nud_state == NUD_INCOMPLETE) {
    
    
		if (skb) {
    
    
.......
			__skb_queue_tail(&neigh->arp_queue, skb);
			neigh->arp_queue_len_Bytes += skb->truesize;
		}
		rc = 1;
	}
out_unlock_bh:
	if (immediate_probe)
		neigh_probe(neigh);
.......
}

在__neigh_event_send 中,激活ARP分两种情况,第一种情况是马上激活,也就是immediate_probe。另一种情况是延迟激活则仅仅设置一个timer。然后将ARP包放在arp_queue上。如果马上激活,就直接调用neigh_probe;如果延迟激活,则定时器到了就会触发neigh_timer_handler,在这里面还是会调用neigh_probe。

我们就来看 neigh_probe 的实现,在这里面会从 arp_queue 中拿出 ARP 包来,然后调用 struct neighbour 的 solicit 操作。

static void neigh_probe(struct neighbour *neigh)
        __releases(neigh->lock)
{
    
    
        struct sk_buff *skb = skb_peek_tail(&neigh->arp_queue);
......
        if (neigh->ops->solicit)
                neigh->ops->solicit(neigh, skb);
......
}

按照上面对于 struct neighbour 的操作函数 arp_hh_ops 的定义,solicit 调用的是 arp_solicit,在这里我们可以找到对于 arp_send_dst 的调用,创建并发送一个 arp 包,得到结果放在 struct dst_entry 里面。

static void arp_send_dst(int type, int ptype, __be32 dest_ip,
                         struct net_device *dev, __be32 src_ip,
                         const unsigned char *dest_hw,
                         const unsigned char *src_hw,
                         const unsigned char *target_hw,
                         struct dst_entry *dst)
{
    
    
        struct sk_buff *skb;
......
        skb = arp_create(type, ptype, dest_ip, dev, src_ip,
                         dest_hw, src_hw, target_hw);
......
        skb_dst_set(skb, dst_clone(dst));
        arp_xmit(skb);
}
 

我们回到 neigh_resolve_output 中,当 ARP 发送完毕,就可以调用 dev_queue_xmit 发送二层网络包了。

/**
 *	__dev_queue_xmit - transmit a buffer
 *	@skb: buffer to transmit
 *	@accel_priv: private data used for L2 forwarding offload
 *
 *	Queue a buffer for transmission to a network device. 
 */
static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv)
{
    
    
	struct net_device *dev = skb->dev;
	struct netdev_queue *txq;
	struct Qdisc *q;
......
	txq = netdev_pick_tx(dev, skb, accel_priv);
	q = rcu_dereference_bh(txq->qdisc);
 
	if (q->enqueue) {
    
    
		rc = __dev_xmit_skb(skb, q, dev, txq);
		goto out;
	}
......
}

网络设备同硬盘块设备一样,每个块设备都有队列,用于将内核的数据放到队列里面,然后设备驱动从队列里面取出后,将数据根据具体设备的特性发送给设备。对于发送来说,有一个发送队列struct netdev_queue *txq。

这里还有另一个变量叫做 struct Qdisc,这个是什么呢?如果我们在一台 Linux 机器上运行 ip addr,我们能看到对于一个网卡,都有下面的输出。

# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc pfifo_fast state UP group default qlen 1000
    link/ether fa:16:3e:75:99:08 brd ff:ff:ff:ff:ff:ff
    inet 10.173.32.47/21 brd 10.173.39.255 scope global noprefixroute dynamic eth0
       valid_lft 67104sec preferred_lft 67104sec
    inet6 fe80::f816:3eff:fe75:9908/64 scope link 
       valid_lft forever preferred_lft forever

这里面有个关键字 qdisc pfifo_fast 是什么意思呢?qdisc 全称是 queueing discipline,中文叫排队规则。内核如果需要通过某个网络接口发送数据包,都需要按照这个接口配置的qdisc(排队规则)把数据包加入队列。

最简单的 qdisc 是 pfifo,它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列。pfifo_fast 稍微复杂一些,它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。

三个波段的优先级也不相同。band 0 的优先级最高,band 2 的最低。如果 band 0 里面有数据包,系统就不会处理 band 1 里面的数据包,band 1 和 band 2 之间也是一样。

数据包是按照服务类型(Type of Service,TOS)被分配到三个波段里面的。TOS 是 IP 头里面的一个字段,代表了当前的包是高优先级的,还是低优先级的。

pfifo_fast 分为三个先入先出的队列,我们能称为三个 Band。根据网络包里面的 TOS,看这个包到底应该进入哪个队列。TOS 总共四位,每一位表示的意思不同,总共十六种类型。

在这里插入图片描述
通过命令行 tc qdisc show dev eth0,我们可以输出结果 priomap,也是十六个数字。在 0 到 2 之间,和 TOS 的十六种类型对应起来。不同的 TOS 对应不同的队列。其中 Band 0 优先级最高,发送完毕后才轮到 Band 1 发送,最后才是 Band 2。

# tc qdisc show dev eth0
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

接下来,__dev_xmit_skb 开始进行网络包发送。

static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
                 struct net_device *dev,
                 struct netdev_queue *txq)
{
    
    
......
    rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK;
    if (qdisc_run_begin(q)) {
    
    
......
        __qdisc_run(q);
    }
......    
}
 
void __qdisc_run(struct Qdisc *q)
{
    
    
    int quota = dev_tx_weight;
    int packets;
     while (qdisc_restart(q, &packets)) {
    
    
        /*
         * Ordered by possible occurrence: Postpone processing if
         * 1. we've exceeded packet quota
         * 2. another process needs the CPU;
         */
        quota -= packets;
        if (quota <= 0 || need_resched()) {
    
    
            __netif_schedule(q);
            break;
        }
     }
     qdisc_run_end(q);
}

__dev_xmit_skb 会将请求放入队列,然后调用__qdisc_run 处理队列中的数据。qdisc_restart 用于数据的发送。根据注释,qdisc的另一个功能是用于控制网络包的发送速度,如果超过速度,就需要调用__netif_schedule重新调度

static void __netif_reschedule(struct Qdisc *q)
{
    
    
    struct softnet_data *sd;
    unsigned long flags;
    local_irq_save(flags);
    sd = this_cpu_ptr(&softnet_data);
    q->next_sched = NULL;
    *sd->output_queue_tailp = q;
    sd->output_queue_tailp = &q->next_sched;
    raise_softirq_irqoff(NET_TX_SOFTIRQ);
    local_irq_restore(flags);
}

__netif_schedule 会调用 __netif_reschedule,发起一个软中断 NET_TX_SOFTIRQ。咱们讲设备驱动程序的时候讲过,设备驱动程序处理中断,分两个过程,一个是屏蔽中断的关键处理逻辑,一个是延迟处理逻辑。当时说工作队列是延迟处理逻辑的处理方案,软中断也是一种方案。

在系统初始化的时候,我们会定义软中断的处理函数。例如,NET_TX_SOFTIRQ 的处理函数是 net_tx_action,用于发送网络包。还有一个 NET_RX_SOFTIRQ 的处理函数是 net_rx_action,用于接收网络包

open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

这里我们来解析一下 net_tx_action。

static __latent_entropy void net_tx_action(struct softirq_action *h)
{
    
    
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
......
    if (sd->output_queue) {
    
    
        struct Qdisc *head;
 
        local_irq_disable();
        head = sd->output_queue;
        sd->output_queue = NULL;
        sd->output_queue_tailp = &sd->output_queue;
        local_irq_enable();
 
        while (head) {
    
    
            struct Qdisc *q = head;
            spinlock_t *root_lock;
 
            head = head->next_sched;
......
            qdisc_run(q);
        }
    }
}

我们会发现,net_tx_action 还是调用了 qdisc_run,还是会调用 __qdisc_run,然后调用 qdisc_restart 发送网络包。

我们来看一下 qdisc_restart 的实现。

static inline int qdisc_restart(struct Qdisc *q, int *packets)
{
    
    
        struct netdev_queue *txq;
        struct net_device *dev;
        spinlock_t *root_lock;
        struct sk_buff *skb;
        bool validate;
 
        /* Dequeue packet */
        skb = dequeue_skb(q, &validate, packets);
        if (unlikely(!skb))
                return 0;
 
        root_lock = qdisc_lock(q);
        dev = qdisc_dev(q);
        txq = skb_get_tx_queue(dev, skb);
 
        return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
}

qdisc_restart 将网络包从 Qdisc 的队列中拿下来,然后调用 sch_direct_xmit 进行发送。

int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
            struct net_device *dev, struct netdev_queue *txq,
            spinlock_t *root_lock, bool validate)
{
    
    
    int ret = NETDEV_TX_BUSY;
 
    if (likely(skb)) {
    
    
        if (!netif_xmit_frozen_or_stopped(txq))
            skb = dev_hard_start_xmit(skb, dev, txq, &ret); 
    } 
......
    if (dev_xmit_complete(ret)) {
    
    
        /* Driver sent out skb successfully or skb was consumed */
        ret = qdisc_qlen(q);
    } else {
    
    
        /* Driver returned NETDEV_TX_BUSY - requeue skb */
        ret = dev_requeue_skb(skb, q);
    }   
......
}

在 sch_direct_xmit 中,调用 dev_hard_start_xmit 进行发送,如果发送不成功,会返回 NETDEV_TX_BUSY。这说明网络卡很忙,于是就调用 dev_requeue_skb,重新放入队列。

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev, struct netdev_queue *txq, int *ret) 
{
    
    
    struct sk_buff *skb = first;
    int rc = NETDEV_TX_OK;
 
    while (skb) {
    
    
        struct sk_buff *next = skb->next;
        rc = xmit_one(skb, dev, txq, next != NULL);
        skb = next; 
        if (netif_xmit_stopped(txq) && skb) {
    
    
            rc = NETDEV_TX_BUSY;
            break;      
        }       
    }   
......
}

在 dev_hard_start_xmit 中,是一个 while 循环。每次在队列中取出一个 sk_buff,调用 xmit_one 发送。

接下来的调用链为:xmit_one->netdev_start_xmit->__netdev_start_xmit。

static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops, struct sk_buff *skb, struct net_device *dev, bool more)          
{
    
    
    skb->xmit_more = more ? 1 : 0;
    return ops->ndo_start_xmit(skb, dev);
}

这个时候,已经到了设备驱动层了。我们能看到,drivers/net/ethernet/intel/ixgb/ixgb_main.c 里面有对于这个网卡的操作的定义。

static const struct net_device_ops ixgb_netdev_ops = {
    
    
        .ndo_open               = ixgb_open,
        .ndo_stop               = ixgb_close,
        .ndo_start_xmit         = ixgb_xmit_frame,
        .ndo_set_rx_mode        = ixgb_set_multi,
        .ndo_validate_addr      = eth_validate_addr,
        .ndo_set_mac_address    = ixgb_set_mac,
        .ndo_change_mtu         = ixgb_change_mtu,
        .ndo_tx_timeout         = ixgb_tx_timeout,
        .ndo_vlan_rx_add_vid    = ixgb_vlan_rx_add_vid,
        .ndo_vlan_rx_kill_vid   = ixgb_vlan_rx_kill_vid,
        .ndo_fix_features       = ixgb_fix_features,
        .ndo_set_features       = ixgb_set_features,
};

在这里面,我们可以找到对于 ndo_start_xmit 的定义,调用 ixgb_xmit_frame。

static netdev_tx_t
ixgb_xmit_frame(struct sk_buff *skb, struct net_device *netdev)
{
    
    
    struct ixgb_adapter *adapter = netdev_priv(netdev);
......
    if (count) {
    
    
        ixgb_tx_queue(adapter, count, vlan_id, tx_flags);
        /* Make sure there is space in the ring for the next send. */
        ixgb_maybe_stop_tx(netdev, &adapter->tx_ring, DESC_NEEDED);
 
    } 
......
    return NETDEV_TX_OK;
}

在 ixgb_xmit_frame 中,我们会得到这个网卡对应的适配器,然后将其放入硬件网卡的队列中。

至此,整个发送才算结束。

总结

在这里插入图片描述
这个过程分成几个层次。

  • VFS 层:write 系统调用找到 struct file,根据里面的 file_operations 的定义,调用 sock_write_iter 函数。sock_write_iter 函数调用 sock_sendmsg 函数。
  • Socket 层:从 struct file 里面的 private_data 得到 struct socket,根据里面 ops 的定义,调用 inet_sendmsg 函数。
  • Sock 层:从 struct socket 里面的 sk 得到 struct sock,根据里面 sk_prot 的定义,调用 tcp_sendmsg 函数。
  • TCP 层:tcp_sendmsg 函数会调用 tcp_write_xmit 函数,tcp_write_xmit 函数会调用 tcp_transmit_skb,在这里实现了 TCP 层面向连接的逻辑。
  • IP 层:扩展 struct sock,得到 struct inet_connection_sock,根据里面 icsk_af_ops 的定义,调用 ip_queue_xmit 函数。
  • IP 层:ip_route_output_ports 函数里面会调用 fib_lookup 查找路由表。FIB 全称是 Forwarding Information Base,转发信息表,也就是路由表。
  • 在 IP 层里面要做的另一个事情是填写 IP 层的头。
  • 在 IP 层还要做的一件事情就是通过 iptables 规则。
  • MAC 层:IP 层调用 ip_finish_output 进行 MAC 层。
    MAC 层需要 ARP 获得 MAC 地址,因而要调用 ___neigh_lookup_noref 查找属于同一个网段的邻居,他会调用 neigh_probe 发送 ARP。
  • 有了 MAC 地址,就可以调用 dev_queue_xmit 发送二层网络包了,它会调用 __dev_xmit_skb 会将请求放入队列。
  • 设备层:网络包的发送回触发一个软中断 -NET_TX_SOFTIRQ 来处理队列中的数据。这个软中断的处理函数是 net_tx_action。
  • 在软中断处理函数中,会将网络包从队列上拿下来,调用网络设备的传输函数 ixgb_xmit_frame,将网络包发的设备的队列上去。

猜你喜欢

转载自blog.csdn.net/zhizhengguan/article/details/121682328
今日推荐