1、网络发包总览
网络发包总流程图如下:
从上图中可以看到用户数据被拷贝到内核态,然后经过协议栈处理后进入RingBuffer。随后网卡驱动真正的将数据发送了出去。当发送完成的时候,是通过硬中断来通知CPU,然后清理RingBuffer。
下面从源码的角度给出一个流程图。
2、网卡启动准备
现在服务器上的网卡一般都是支持多队列的。每一个队列是由一个RingBuffer表示的,开启了多队列以后的网卡就会对应有多个RingBuffer。网卡启动时的最重要任务之一就是分配和初始化RingBuffer.
在网卡启动时,会调用__igb_open函数,RingBuffer就是在这里分配的。
static int __igb_open(struct net_device *netdev, bool resuming)
{
struct igb_adapter *adapter = netdev_priv(netdev);
struct e1000_hw *hw = &adapter->hw;
struct pci_dev *pdev = adapter->pdev;
//分配传输描述符数组
err = igb_setup_all_tx_resources(adapter);
//分配接收描述符数组
err = igb_setup_all_rx_resources(adapter);
//中断注册,igb_msix_ring就是在这里进行注册的
err = igb_request_irq(adapter);
//开启全部队列
netif_tx_start_all_queues(netdev);
..
}
static int igb_setup_all_tx_resources(struct igb_adapter *adapter)
{
struct pci_dev *pdev = adapter->pdev;
int i, err = 0;
for (i = 0; i < adapter->num_tx_queues; i++) {
//有几个队列就构造几个RingBuffer
err = igb_setup_tx_resources(adapter->tx_ring[i]);
if (err) {
dev_err(&pdev->dev,
"Allocation for Tx Queue %u failed\n", i);
for (i--; i >= 0; i--)
igb_free_tx_resources(adapter->tx_ring[i]);
break;
}
}
return err;
}
int igb_setup_tx_resources(struct igb_ring *tx_ring)
{
//申请igb_tx_buffer数组内存
size = sizeof(struct igb_tx_buffer) * tx_ring->count;
tx_ring->tx_buffer_info = vmalloc(size);
if (!tx_ring->tx_buffer_info)
goto err;
//申请e1000_adv_tx_desc DMA数组内存
tx_ring->size = tx_ring->count * sizeof(union e1000_adv_tx_desc);
tx_ring->size = ALIGN(tx_ring->size, 4096);
tx_ring->desc = dma_alloc_coherent(dev, tx_ring->size,
&tx_ring->dma, GFP_KERNEL);
//初始化队列成员
tx_ring->next_to_use = 0;
tx_ring->next_to_clean = 0;
...
}
上面__igb_open调用igb_setup_all_tx_resources分配所有传输的RingBuffer,调用igb_setup_all_rx_resources创建所有的接收RingBuffer。真正的RingBuffer构建是在igb_setup_tx_resources完成的。从上述源码可以看到一个传输RingBuffer的内部包含两个环形数组:igb_tx_buffer数组是内核使用;e1000_adv_tx_desc数组是硬件网卡使用。这两个数组在发送的时候,相同位置的指针都将指向同一个skb。这样内核和硬件就能共同访问同样的数据了,内核往skb写数据,网卡硬件负责发送。
硬中断的处理函数igb_msix_ring也是在__igb_open中注册的。
3、数据从用户进程到网卡的详细过程
3.1 send系统调用实现
send系统调用源码位于net/socket.c中,其内部实际使用的是sendto系统调用。该函数主要干了两件事:在内核中将真正的socket找出来,在这个对象里记录了各种协议栈的函数地址;构造一个struct msghdr对象,把用户传入的数据copy进去。之后就调用inet_sendmsg了。大致流程如下图:
源码如下:
SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
unsigned int, flags)
{
return sys_sendto(fd, buff, len, flags, NULL, 0);
}
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
unsigned int, flags, struct sockaddr __user *, addr,
int, addr_len)
{
struct socket *sock;
struct msghdr msg;
err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
//1.根据fd找到socket
sock = sockfd_lookup_light(fd, &err, &fput_needed);
//2.构造msghdr
msg.msg_name = NULL;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
err = move_addr_to_kernel(addr, addr_len, &address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
//3.发送数据
err = sock_sendmsg(sock, &msg);
}
从源码可以看到,send只是sendto封装出来的。在sendto系统调用里,首先根据用户传进来的句柄号来查找真正的socket对象,接着将用户请求的buf、len、flag等参数打包成一个msghdr对象。接着调用了sock_sendmsg==>sock_sendmsg_nosec,在sock_sendmsg_nosec中进入协议栈。
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
//实际调用的是inet_sendmsg
int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
BUG_ON(ret == -EIOCBQUEUED);
return ret;
}
3.2 传输层处理
传输层发送流程大致如下:
3.2.1 传输层拷贝
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
...
return sk->sk_prot->sendmsg(sk, msg, size);
}
对于TCP的socket来说,sk->sk_prot->sendmsg实际上是指向tcp_sendmsg(对于UDP的socket来说实际上是udp_sendmsg)。
由于这个函数比较长,下面分开进行理解。
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
// 开始发送数据
copied = 0;
restart:
mss_now = tcp_send_mss(sk, &size_goal, flags); // 获取当前 MSS 和目标大小
while (msg_data_left(msg)) {
int copy = 0;
int max = size_goal;
skb = tcp_write_queue_tail(sk); // 获取发送队列尾部的 sk_buff
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:
// 分配新的 sk_buff
if (!sk_stream_memory_free(sk))
goto wait_for_sndbuf;
if (process_backlog && sk_flush_backlog(sk)) {
process_backlog = false;
goto restart;
}
first_skb = skb_queue_empty(&sk->sk_write_queue);
//申请skb
skb = sk_stream_alloc_skb(sk,
select_size(sk, sg, first_skb),
sk->sk_allocation,
first_skb);
if (!skb)
goto wait_for_memory;
process_backlog = true;
if (sk_check_csum_caps(sk))
skb->ip_summed = CHECKSUM_PARTIAL;
//把skb挂到socket的发送队列末尾
skb_entail(sk, skb);
copy = size_goal;
max = size_goal;
if (tp->repair)
TCP_SKB_CB(skb)->sacked |= TCPCB_REPAIRED;
}
if (copy > msg_data_left(msg))
copy = msg_data_left(msg);
//如果skb有空余空间,则将msg存储的数据copy到skb中
if (skb_availroom(skb) > 0) {
copy = min_t(int, copy, skb_availroom(skb));
//将用户空间的数据拷贝到内核空间,同时计算教育和
err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
if (err)
goto do_fault;
}
...
//更新seq
tp->write_seq += copy;
TCP_SKB_CB(skb)->end_seq += copy;
tcp_skb_pcount_set(skb, 0);
wait_for_sndbuf:
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
if (copied)
tcp_push(sk, flags & ~MSG_MORE, mss_now,
TCP_NAGLE_PUSH, size_goal);
//socket发送缓存不足时,如果是阻塞套接字会陷入等待
err = sk_stream_wait_memory(sk, &timeo);
if (err != 0)
goto do_error;
mss_now = tcp_send_mss(sk, &size_goal, flags);
}
out:
if (copied) {
tcp_tx_timestamp(sk, sockc.tsflags, tcp_write_queue_tail(sk));
tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
}
out_nopush:
release_sock(sk);
//返回已复制的长度
return copied + copied_syn;
}
上面的源码主要是将用户层的数据拷贝到socket的发送缓存队列末尾,如果socket缓存空间不够,而socket又是阻塞模式的就会陷入等待直到超时或者条件满足。在这个copy步骤中,如果用户层发送的数据长度超过mss,则会进行多次分割copy。
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
while (msg_data_left(msg)) {
//发送判断
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;
}
...
}
在发送只有满足forced_push(tp)或skb == tcp_send_head(sk)成立时,内核才会启动发送数据包。其中forced_push(tp)判断的时未发送的数据是否超过最大窗口的一半,skb == tcp_send_head(sk)判断的是队列最末尾的skb是不是待发送的skb。
条件不满足的话只是将用户数据拷贝到socket的发送队列。
3.2.2 传输层发送
tcp_write_xmit 是 Linux 内核 TCP 协议栈中用于处理数据包发送的核心函数。它负责从 TCP 套接字的发送队列中取出数据包,并在满足特定条件时通过 IP 层发送它们。这个函数处理了多种情况,包括 MTU 探测、拥塞窗口测试、发送窗口测试、Nagle 算法、TSO(TCP Segmentation Offload)处理等。
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); // 获取 TCP 特定的 sock 结构
struct sk_buff *skb; // 指向待发送的 sk_buff 结构
unsigned int tso_segs, sent_pkts; // TSO 分段数,已发送数据包数
int cwnd_quota; // 拥塞窗口配额
int result; // 用于存储函数返回值
bool is_cwnd_limited = false, is_rwnd_limited = false; // 拥塞窗口和发送窗口限制标志
u32 max_segs; // 最大分段数
sent_pkts = 0; // 初始化已发送数据包数
if (!push_one) {
// 执行 MTU 探测
result = tcp_mtu_probe(sk);
if (!result) {
return false; // 如果探测失败,返回 false
} else if (result > 0) {
sent_pkts = 1; // 如果探测成功,增加已发送数据包数
}
}
max_segs = tcp_tso_segs(sk, mss_now); // 计算 TSO 分段数
while ((skb = tcp_send_head(sk))) {
// 循环处理发送队列头部的数据包
unsigned int limit; // 发送限制
tso_segs = tcp_init_tso_segs(skb, mss_now); // 初始化 TSO 分段数
BUG_ON(!tso_segs); // 确保 TSO 分段数有效
if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
// 如果需要修复,跳过网络传输
skb_mstamp_get(&skb->skb_mstamp);
goto repair;
}
// 测试拥塞窗口是否足够
cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota) {
if (push_one == 2) {
// 强制发送一个丢包探测数据包
cwnd_quota = 1;
} else {
break; // 如果拥塞窗口不足,退出循环
}
}
// 测试发送窗口是否足够
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
is_rwnd_limited = true; // 标记发送窗口限制
break;
}
// 处理 Nagle 算法
if (tso_segs == 1) {
if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
(tcp_skb_is_last(sk, skb) ?
nonagle : TCP_NAGLE_PUSH)))) {
break; // 如果 Nagle 算法不允许发送,退出循环
}
} else {
// 如果 TSO 分段数大于 1,检查是否应该推迟发送
if (!push_one &&
tcp_tso_should_defer(sk, skb, &is_cwnd_limited,
max_segs)) {
break; // 如果应该推迟,退出循环
}
}
// 计算最大发送大小
limit = mss_now;
if