Linux是如何发送数据包的

Linux是如何发送数据包的

1. 发包过程概览

高级语言编程:

int main(){
    fd = socket(AF_INET, SOCK_STREAM, 0);
    bind(fd, ...);
    listen(fd, ...);
    cfd = accept(fd, ...);
    read(cfd, ...);
    dosomesting();
    // 发送数据
    send(cfd, buf, sizeof(buf), 0)
}

当执行send时,会发生系统调用,将数据包从用户态拷贝到内核态,然后经过协议栈处理后进入了RingBuffer。随后网卡驱动真正将数据发送了出去。当发送完成的时候,通过硬中断来通知CPU,然后清理RingBuffer

在这里插入图片描述

2. 网卡启动准备

网卡启动时最重要的任务是分配和初始化RingBuffer。

RingBuffer内部不仅仅是一个环形队列数组

  • igb_tx_buffer数组:这个数组是内核使用的,通过vzalloc申请
  • e1000_adv_tx_desc数组:这个数组是网卡硬件使用的,通过dma_alloc_coherent分配

这个时候它们没有什么联系,在发送数据的时候,这两个环形数组中相同位置的指针都将指向同一个skb。这样,内核和硬件就能共同访问同样的数据了,内核往skb写数据,网卡硬件负责发送

在这里插入图片描述

3. 数据从用户进程到网卡的过程

3.1 send系统调用

send系统调用主要干两件事情:

  • 在内核中把真正的socket找出来,在这个对象里记录着各种协议栈的函数地址
  • 构造一个struct msghdr对象,把用户传入的数据,比如buffer地址、数据长度等等都装进去

在这里插入图片描述

3.2 传输层处理

(1)传输层拷贝

在进入协议栈inet_sendmsg以后,内核接着会找到socket上的具体协议发送函数。对于TCP协议来说,那就是tcp_sendmsg,在这个函数中,内核会申请一个内核态的skb内存,将用户发送的数据拷贝进去。这个时候不一定会真正发送,如果没有达到发送条件,很可能在这次调用直接就返回了

在这里插入图片描述

(2)传输层判断

内核什么时候真正把 skb 发送出去。在 tcp_sendmsg 中会进行一些判断。

//file: net/ipv4/tcp.c
int tcp_sendmsg(...)
{
 while(...){
  while(...){
   //申请内核内存并进行拷贝

   //发送判断
   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) 判断的是未发送的数据是否已经超过最大窗口的一半了。

条件都不满足的话,这次的用户要发送的数据只是拷贝到内核就算完事了!

(3)传输层发送

当满足真正发送条件的时候,无论调用的是 __tcp_push_pending_frames 还是 tcp_push_one 最终都实际会执行到 tcp_write_xmit。这个函数处理了传输层的拥塞控制、滑动窗口相关的工作。满足窗口要求的时候,设置一下 TCP 头然后将 skb 传到更低的网络层进行处理。

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
      int push_one, gfp_t gfp)
{
 //循环获取待发送 skb
 while ((skb = tcp_send_head(sk))) 
 {
  //滑动窗口相关
  cwnd_quota = tcp_cwnd_test(tp, skb);
  tcp_snd_wnd_test(tp, skb, mss_now);
  tcp_mss_split_point(...);
  tso_fragment(sk, skb, ...);
  ......

  //真正开启发送
  tcp_transmit_skb(sk, skb, 1, gfp);
 }
}

走到tcp_transmit_skb的时候,真正开启发送了,这个函数主要做三件事:

  • 克隆新的skb
  • 封装TCP头
  • 调用网络层发送接口
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
    gfp_t gfp_mask)
{
 //1.克隆新 skb 出来
 if (likely(clone_it)) {
  skb = skb_clone(skb, gfp_mask);
  ......
 }

 //2.封装 TCP 头
 th = tcp_hdr(skb);
 th->source  = inet->inet_sport;
 th->dest  = inet->inet_dport;
 th->window  = ...;
 th->urg   = ...;
 ......

 //3.调用网络层发送接口
 err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);
}

为什么要复制一个 skb 出来呢?

是因为 skb 后续在调用网络层,最后到达网卡发送完成的时候,这个 skb 会被释放掉。而我们知道 TCP 协议是支持丢失重传的,在收到对方的 ACK 之前,这个 skb 不能被删除。所以内核的做法就是每次调用网卡发送的时候,实际上传递出去的是 skb 的一个拷贝。等收到 ACK 再真正删除

3.3 网络层处理

在网络层里主要处理路由项查找、IP 头设置、netfilter 过滤、skb 切分(大于 MTU 的话)等几项工作,处理完这些工作后会交给更下层的邻居子系统来处理。

int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
 //检查 socket 中是否有缓存的路由表
 rt = (struct rtable *)__sk_dst_check(sk, 0);
 if (rt == NULL) {
  //没有缓存则展开查找
  //则查找路由项, 并缓存到 socket 中
  rt = ip_route_output_ports(...);
  sk_setup_caps(sk, &rt->dst);
 }

 //为 skb 设置路由表
 skb_dst_set_noref(skb, &rt->dst);

 //设置 IP header
 iph = ip_hdr(skb);
 iph->protocol = sk->sk_protocol;
 iph->ttl      = ip_select_ttl(inet, &rt->dst);
 iph->frag_off = ...;

 //发送
 ip_local_out(skb);
}

在 ip_local_out => __ip_local_out => nf_hook 会执行 netfilter 过滤。

如果使用 iptables 配置了一些规则,那么这里将检测是否命中规则。如果设置了非常复杂的 netfilter 规则,在这个函数这里将会导致你的进程 CPU 开销会极大增加

3.4 邻居子系统处理

邻居子系统是位于网络层和数据链路层中间的一个系统,其作用是对网络层提供一个封装,让网络层不必关心下层的地址信息,让下层来决定发送到哪个 MAC 地址

在邻居子系统里主要是查找或者创建邻居项,在创造邻居项的时候,有可能会发出实际的 arp 请求。然后封装一下 MAC 头,将发送过程再传递到更下层的网络设备子系统。

在这里插入图片描述

当获取到硬件 MAC 地址以后,就可以封装 skb 的 MAC 头了。最后将 skb 传递给 Linux 网络设备子系统。

3.5 网络设备子系统处理

在这里插入图片描述

static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
     struct net_device *dev,
     struct netdev_queue *txq)
{
 //1.如果可以绕开排队系统
 if ((q->flags & TCQ_F_CAN_BYPASS) && !qdisc_qlen(q) &&
     qdisc_run_begin(q)) {
  ......
 }

 //2.正常排队
 else {

  //入队
  q->enqueue(skb, q)

  //开始发送
  __qdisc_run(q);
 }
}

上述代码中分两种情况,1 是可以 bypass(绕过)排队系统的,另外一种是正常排队。

第二种情况先调用 q->enqueue 把 skb 添加到队列里。然后调用 __qdisc_run 开始发送。

void __qdisc_run(struct Qdisc *q)
{
 int quota = weight_p;

 //循环从队列取出一个 skb 并发送
 while (qdisc_restart(q)) {
  
  // 如果发生下面情况之一,则延后处理:
  // 1. quota 用尽
  // 2. 其他进程需要 CPU
  if (--quota <= 0 || need_resched()) {
   //将触发一次 NET_TX_SOFTIRQ 类型 softirq
   __netif_schedule(q);
   break;
  }
 }
}

while 循环不断地从队列中取出 skb 并进行发送。这个时候其实都占用的是用户进程的系统态时间(sy)。只有当 quota 用尽或者其它进程需要 CPU 的时候才触发软中断进行发送。

所以这就是为什么一般服务器上查看 /proc/softirqs,一般 NET_RX 都要比 NET_TX 大的多的第二个原因。对于读来说,都是要经过 NET_RX 软中断,而对于发送来说,只有系统态配额用尽才让软中断上。

3.6 软中断调度

如果系统态 CPU 发送网络包不够用的时候,会调用 netif_schedule 触发一个软中断。该函数会进入到 netif_reschedule,由它来实际发出 NET_TX_SOFTIRQ 类型软中断。软中断是由内核线程来运行的,该线程会进入到 net_tx_action 函数,在该函数中能获取到发送队列,并也最终调用到驱动程序里的入口函数 dev_hard_start_xmit。
在这里插入图片描述

用户态进程触发完软中断之后,会有一个软中断内核线程会执行到 net_tx_action。这以后发送数据消耗的 CPU 就都显示在 si 这里了,不会消耗用户进程的系统时间了

static void net_tx_action(struct softirq_action *h)
{
 //通过 softnet_data 获取发送队列
 struct softnet_data *sd = &__get_cpu_var(softnet_data);

 // 如果 output queue 上有 qdisc
 if (sd->output_queue) {

  // 将 head 指向第一个 qdisc
  head = sd->output_queue;

  //遍历 qdsics 列表
  while (head) {
   struct Qdisc *q = head;
   head = head->next_sched;

   //发送数据
   qdisc_run(q);
  }
 }
}

3.7 igb 网卡驱动发送

无论是对于用户进程的内核态,还是对于软中断上下文,都会调用到网络设备子系统中的 dev_hard_start_xmit 函数。在这个函数中,会调用到驱动里的发送函数 igb_xmit_frame。在驱动函数里,将 skb 会挂到 RingBuffer上,驱动调用完毕后,数据包将真正从网卡发送出去。

在这里插入图片描述

3.8 发送完成硬中断

当数据发送完成以后,其实工作并没有结束。因为内存还没有清理。当发送完成的时候,网卡设备会触发一个硬中断来释放内存。
在这里插入图片描述

无论硬中断是因为是有数据要接收,还是说发送完成通知,从硬中断触发的软中断都是 NET_RX_SOFTIRQ,这是软中断统计中 RX 要高于 TX 的一个原因。

4. 总览图

在这里插入图片描述

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值