Linux·Ilustrando o processo de envio de pacotes de rede

Por favor, pense em algumas pequenas questões.

  • Questão 1: Quando olhamos para a CPU consumida pelo kernel enviando dados, devemos olhar para sy ou si?
  • Pergunta 2: Por que NET_RX é muito maior que NET_TX em /proc/softirqs em seu servidor ?
  • P3: Quais operações de cópia de memória estão envolvidas no envio de dados de rede?

Embora esses problemas sejam frequentemente vistos online, parece que raramente nos aprofundamos neles. Se pudermos realmente entender essas questões completamente, nossa capacidade de controlar o desempenho se tornará mais forte.

Com essas três perguntas, começamos a análise aprofundada de hoje do processo de envio de rede do kernel Linux. Ainda de acordo com nossa tradição anterior, comece com um simples trecho de código. O código a seguir é um microcódigo típico de um programa de servidor típico:

int main(){
 fd = socket(AF_INET, SOCK_STREAM, 0);
 bind(fd, ...);
 listen(fd, ...);

 cfd = accept(fd, ...);

 // 接收用户请求
 read(cfd, ...);

 // 用户请求处理
 dosometing(); 

 // 给用户返回结果
 send(cfd, buf, sizeof(buf), 0);
}

Hoje vamos discutir como o kernel envia o pacote de dados após chamar send no código acima. Este artigo é baseado no Linux 3.10, e o driver da placa de rede usa a placa de rede igb da Intel como exemplo.

Aviso: Este artigo tem mais de 10.000 palavras e 25 imagens, cuidado com artigos longos!

1. Visão geral do processo de envio de rede Linux

Acho que a coisa mais importante a se observar no código-fonte do Linux é ter uma compreensão geral, em vez de ficar atolado em vários detalhes desde o início.

Preparei um fluxograma geral para você aqui e expliquei brevemente como os dados enviados por send são enviados para a placa de rede passo a passo.

Nesta figura, vemos que os dados do usuário são copiados para o estado do kernel e, em seguida, inseridos no RingBuffer após serem processados ​​pela pilha de protocolos. Em seguida, o driver da placa de rede realmente envia os dados. Quando a transmissão é concluída, a CPU é notificada por meio de uma interrupção de hardware e, em seguida, o RingBuffer é limpo.

Como o código-fonte será inserido posteriormente no artigo, forneceremos um fluxograma da perspectiva do código-fonte.

Embora os dados tenham sido enviados neste momento, na verdade há uma coisa importante que não foi feita, que é liberar a memória, como a fila de cache.

Como o kernel sabe quando liberar a memória, claro, depois que a rede é enviada. Quando a placa de rede terminar de enviar, ela enviará uma interrupção de hardware para a UCP para notificar a UCP. Veja o diagrama para um processo mais completo:

Observe que, embora nosso tópico hoje seja o envio de dados, a interrupção suave acionada pela interrupção física é NET_RX_SOFTIRQ em vez de NET_TX_SOFTIRQ! ! ! (T é a abreviação de transmitir, R significa receber)

É uma surpresa, é uma surpresa? ? ?

Então, isso é parte do motivo para abrir a pergunta 1 (observe, é apenas parte do motivo).

Pergunta 1: Verifique /proc/softirqs no servidor, por que NET_RX é muito maior que NET_TX?

A conclusão da transferência eventualmente aciona NET_RX, não NET_TX. Então, naturalmente, você pode ver mais NET_RX observando /proc/softirqs.

Ok, agora você tem uma visão geral de como o kernel envia pacotes de rede. Não seja complacente, os detalhes que precisamos saber são mais valiosos, vamos continuar! !

2. Preparação da inicialização da placa de rede

As placas de rede nos servidores atuais geralmente suportam várias filas. Cada fila é representada por um RingBuffer, e a placa de rede com várias filas habilitadas terá vários RingBuffers.

Uma das tarefas mais importantes quando a placa de rede é iniciada é alocar e inicializar o RingBuffer. Entender o RingBuffer será muito útil para dominarmos o envio posteriormente. Como o assunto de hoje é envio, vamos pegar como exemplo a fila de transmissão, vamos ver o processo real de alocação do RingBuffer quando a placa de rede inicia.

Quando a placa de rede for iniciada, a função __igb_open será chamada e o RingBuffer será alocado aqui.

//file: drivers/net/ethernet/intel/igb/igb_main.c
static int __igb_open(struct net_device *netdev, bool resuming)
{
 struct igb_adapter *adapter = netdev_priv(netdev);

 //分配传输描述符数组
 err = igb_setup_all_tx_resources(adapter);

 //分配接收描述符数组
 err = igb_setup_all_rx_resources(adapter);

 //开启全部队列
 netif_tx_start_all_queues(netdev);
}

Na função __igb_open acima, chame igb_setup_all_tx_resources para alocar todos os RingBuffers de transmissão e chame igb_setup_all_rx_resources para criar todos os RingBuffers de recebimento.

//file: drivers/net/ethernet/intel/igb/igb_main.c
static int igb_setup_all_tx_resources(struct igb_adapter *adapter)
{
 //有几个队列就构造几个 RingBuffer
 for (i = 0; i < adapter->num_tx_queues; i++) {
  igb_setup_tx_resources(adapter->tx_ring[i]);
 }
}

O processo de construção real do RingBuffer é concluído em igb_setup_tx_resources.

//file: drivers/net/ethernet/intel/igb/igb_main.c
int igb_setup_tx_resources(struct igb_ring *tx_ring)
{
 //1.申请 igb_tx_buffer 数组内存
 size = sizeof(struct igb_tx_buffer) * tx_ring->count;
 tx_ring->tx_buffer_info = vzalloc(size);

 //2.申请 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);

 //3.初始化队列成员
 tx_ring->next_to_use = 0;
 tx_ring->next_to_clean = 0;
}

Como pode ser visto no código-fonte acima, na verdade, um RingBuffer não tem apenas um array de fila de anel, mas dois.

1) array igb_tx_buffer: Este array é usado pelo kernel e aplicado através do vzalloc. 2) array e1000_adv_tx_desc: Este array é utilizado pelo hardware da placa de rede, o hardware pode acessar diretamente esta memória através do DMA e alocá-la através do dma_alloc_coherent.

Neste momento não há conexão entre eles. Ao enviar no futuro, os ponteiros na mesma posição nas duas matrizes de anel apontarão para o mesmo skb. Dessa forma, o kernel e o hardware podem acessar os mesmos dados em conjunto, o kernel grava os dados no skb e o hardware da placa de rede é responsável por enviá-los.

Finalmente, chame netif_tx_start_all_queues para iniciar a fila. Além disso, a função de processamento igb_msix_ring para interrupção de hardware é realmente registrada em __igb_open.

Três, aceite criar um novo soquete

Antes de enviar dados, geralmente precisamos de um soquete que já tenha estabelecido uma conexão.

Vamos tomar como exemplo o aceite mencionado no código-fonte da microforma do servidor. Depois de aceitar, o processo criará um novo soquete e o colocará na lista de arquivos abertos do processo atual, que é usado especialmente para se comunicar com o cliente correspondente .comunicação.

Supondo que o processo do servidor tenha estabelecido duas conexões com o cliente por meio da aceitação, vamos dar uma breve olhada no relacionamento entre essas duas conexões e o processo.

O diagrama estrutural mais específico do objeto do kernel do soquete que representa uma conexão é o seguinte.

Para evitar sobrecarregar, o processo de aceitação do código-fonte detalhado não será apresentado aqui. Se você estiver interessado, consulte "Ilustração | Desmistificação detalhada de como a epoll realiza a multiplexação IO!" " . A primeira parte do artigo.

Hoje ainda nos concentramos no processo de envio de dados.

4. O envio de dados realmente começa

4.1 enviar implementação de chamada de sistema

O código-fonte para a chamada de sistema de envio está no arquivo net/socket.c. Nesta chamada de sistema, a chamada de sistema sendto é realmente usada internamente. Embora toda a cadeia de chamadas não seja curta, na verdade ela só faz duas coisas simples,

  • A primeira é descobrir o soquete real no kernel, e os endereços de função de várias pilhas de protocolos são registrados nesse objeto.
  • A segunda é construir um objeto struct msghdr e colocar todos os dados passados ​​pelo usuário, como endereço de buffer, comprimento de dados, etc., nele.

O restante é entregue à próxima camada, a função inet_sendmsg na pilha de protocolos, onde o endereço da função inet_sendmsg é encontrado por meio do membro ops no objeto do kernel do soquete. O processo geral é mostrado na figura.

Com o entendimento acima, será muito mais fácil olharmos para o código-fonte. O código fonte é o seguinte:

//file: net/socket.c
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(......)
{
 //1.根据 fd 查找到 socket
 sock = sockfd_lookup_light(fd, &err, &fput_needed);

 //2.构造 msghdr
 struct msghdr msg;
 struct iovec iov;

 iov.iov_base = buff;
 iov.iov_len = len;
 msg.msg_iovlen = 1;

 msg.msg_iov = &iov;
 msg.msg_flags = flags;
 ......

 //3.发送数据
 sock_sendmsg(sock, &msg, len);
}

Como pode ser visto no código-fonte, a função send e a função sendto que usamos no modo de usuário são realmente implementadas pela chamada de sistema sendto. send é apenas uma maneira mais fácil de chamar encapsulada por conveniência.

Na chamada de sistema sendto, o objeto real do kernel do soquete é primeiro pesquisado de acordo com o número do identificador do soquete passado pelo usuário. Em seguida, empacote o buff, len, flag e outros parâmetros solicitados pelo usuário em um objeto struct msghdr.

Em seguida, chamado sock_sendmsg => __sock_sendmsg ==> __sock_sendmsg_nosec. Em __sock_sendmsg_nosec, a chamada entrará na pilha de protocolos da chamada do sistema, vejamos seu código-fonte.

//file: net/socket.c
static inline int __sock_sendmsg_nosec(...)
{
 ......
 return sock->ops->sendmsg(iocb, sock, msg, size);
}

Por meio do diagrama de estrutura de objeto do kernel do soquete na terceira seção, podemos ver que o que é chamado aqui é sock->ops->sendmsg e inet_sendmsg é realmente executado. Esta função é uma função de envio geral fornecida pela família de protocolos AF_INET.

4.2 Processamento da camada de transporte

1) Cópia da camada de transporte

Depois de entrar na pilha de protocolo inet_sendmsg, o kernel encontrará a função de envio de protocolo específico no soquete. Para o protocolo TCP, é tcp_sendmsg (também encontrado por meio do objeto do kernel do soquete).

Nesta função, o kernel solicitará uma memória skb no modo kernel e copiará os dados a serem enviados pelo usuário para ela. Observe que o envio pode não começar neste momento. Se a condição de envio não for atendida, é provável que a chamada seja retornada diretamente. O processo aproximado é mostrado na figura:

Vejamos o código-fonte da função inet_sendmsg.

//file: net/ipv4/af_inet.c
int inet_sendmsg(......)
{
 ......
 return sk->sk_prot->sendmsg(iocb, sk, msg, size);
}

Nesta função será chamada a função de envio do protocolo específico. Consulte também o diagrama de estrutura de objeto do kernel do soquete na terceira seção, vemos que para o soquete sob o protocolo TCP, sk->sk_prot->sendmsg aponta para tcp_sendmsg (para UPD, é udp_sendmsg).

A função tcp_sendmsg é relativamente longa, então vamos examiná-la várias vezes. olha isso primeiro

//file: net/ipv4/tcp.c
int tcp_sendmsg(...)
{
 while(...){
  while(...){
   //获取发送队列
   skb = tcp_write_queue_tail(sk);

   //申请skb 并拷贝
   ......
  }
 }
}

//file: include/net/tcp.h
static inline struct sk_buff *tcp_write_queue_tail(const struct sock *sk)
{
 return skb_peek_tail(&sk->sk_write_queue);
}

Entender a chamada de tcp_write_queue_tail no soquete é um pré-requisito para entender o envio. Conforme mostrado acima, esta função é para obter o último skb na fila de envio do soquete. skb é a abreviação do objeto struct sk_buff e a fila de envio do usuário é uma lista vinculada composta por esse objeto.

Vejamos as outras partes do tcp_sendmsg.

//file: net/ipv4/tcp.c
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
  size_t size)
{
 //获取用户传递过来的数据和标志
 iov = msg->msg_iov; //用户数据地址
 iovlen = msg->msg_iovlen; //数据块数为1
 flags = msg->msg_flags; //各种标志

 //遍历用户层的数据块
 while (--iovlen >= 0) {

  //待发送数据块的地址
  unsigned char __user *from = iov->iov_base;

  while (seglen > 0) {

   //需要申请新的 skb
   if (copy <= 0) {

    //申请 skb,并添加到发送队列的尾部
    skb = sk_stream_alloc_skb(sk,
         select_size(sk, sg),
         sk->sk_allocation);

    //把 skb 挂到socket的发送队列上
    skb_entail(sk, skb);
   }

   // skb 中有足够的空间
   if (skb_availroom(skb) > 0) {
    //拷贝用户空间的数据到内核空间,同时计算校验和
    //from是用户空间的数据地址 
    skb_add_data_nocache(sk, skb, from, copy);
   } 
   ......

Esta função é relativamente longa, mas a lógica não é complicada. Dentre eles, msg->msg_iov armazena o buffer dos dados a serem enviados na memória do modo usuário. Em seguida, solicite a memória do kernel no estado do kernel, como skb, e copie os dados da memória do usuário para a memória do estado do kernel. Isso envolverá a sobrecarga de uma ou várias cópias de memória .

Quanto a quando o kernel realmente envia o skb. Alguns julgamentos serão feitos em 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;
  }
 }
}

Somente quando forced_push(tp) ou skb == tcp_send_head(sk) for satisfeito, o kernel realmente começará a enviar pacotes. Entre eles, force_push(tp) julga se os dados não enviados excederam a metade da janela máxima.

Se as condições não forem atendidas, os dados que o usuário deseja enviar desta vez são apenas copiados para o kernel e o trabalho está encerrado!

2) Envio da camada de transporte

Assumindo que as condições de envio do kernel foram atendidas agora, vamos acompanhar o processo de envio real. Para as funções na seção anterior, quando as condições de envio reais são atendidas, não importa se __tcp_push_pending_frames ou tcp_push_one é chamado, ele realmente executará tcp_write_xmit no final.

Portanto, olhamos diretamente de tcp_write_xmit, esta função lida com o controle de congestionamento da camada de transporte e o trabalho relacionado à janela deslizante. Quando os requisitos da janela forem atendidos, defina o cabeçalho TCP e passe o skb para a camada de rede inferior para processamento.

Vejamos o código-fonte de tcp_write_xmit.

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

Você pode ver que a janela deslizante e o controle de congestionamento que aprendemos no protocolo de rede são concluídos nesta função e esta parte não será muito expandida. Os alunos interessados ​​podem encontrar este código-fonte para ler por conta própria. Olhamos apenas para o processo principal de envio hoje e depois chegamos a tcp_transmit_skb.

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

A primeira coisa é clonar um novo skb primeiro. Aqui vamos nos concentrar em por que precisamos copiar um skb?

É porque o skb está chamando a camada de rede posteriormente, e quando a placa de rede finalmente for enviada, o skb será liberado. E sabemos que o protocolo TCP suporta retransmissão perdida, e este skb não pode ser excluído antes de receber o ACK da outra parte. Portanto, o método do kernel é que toda vez que a placa de rede é chamada para enviar, o que realmente é distribuído é uma cópia do skb. Aguarde até que o ACK seja recebido antes de realmente excluir.

A segunda coisa é modificar o cabeçalho TCP no skb e definir o cabeçalho TCP de acordo com a situação real. Aqui está um pequeno truque para introduzir, skb na verdade contém todos os cabeçalhos no protocolo de rede. Ao definir o cabeçalho TCP, apenas aponte o ponteiro para a posição correta de skb. Ao definir o cabeçalho IP posteriormente, basta mover o ponteiro para evitar aplicação frequente de memória e cópia, o que é muito eficiente.

tcp_transmit_skb é a última etapa no envio de dados na camada de transporte e, em seguida, pode entrar na camada de rede para a próxima camada de operações. A interface de envio icsk->icsk_af_ops->queue_xmit() fornecida pela camada de rede é chamada.

No código-fonte a seguir, sabemos que queue_xmit na verdade aponta para a função ip_queue_xmit.

//file: net/ipv4/tcp_ipv4.c
const struct inet_connection_sock_af_ops ipv4_specific = {
 .queue_xmit    = ip_queue_xmit,
 .send_check    = tcp_v4_send_check,
 ...
}

Desde então, o trabalho da camada de transporte foi concluído. Os dados saem da camada de transporte e entrarão na implementação do kernel na camada de rede.

4.3 Processamento de envio da camada de rede

A implementação do envio na camada de rede do kernel do Linux está localizada no arquivo net/ipv4/ip_output.c. O ip_queue_xmit chamado pela camada de transporte também está aqui. (Também pode ser visto pelo nome do arquivo que ele entrou na camada IP e o nome do arquivo de origem mudou de tcp_xxx para ip_xxx.)

Na camada de rede, ele lida principalmente com várias tarefas, como pesquisa de item de roteamento, configuração de cabeçalho IP, filtragem netfilter, segmentação skb (se for maior que MTU), etc. Após o processamento dessas tarefas, ele será entregue ao vizinho inferior subsistema para processamento.

Vejamos o código-fonte da função de entrada da camada de rede ip_queue_xmit:

//file: net/ipv4/ip_output.c
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_queue_xmit alcançou a camada de rede. Nesta função, vemos a função de pesquisa de item de roteamento relacionada à camada de rede. Se for encontrada, será definida como skb (se não houver rota, relatará diretamente um erro e retornará ).

No Linux, você pode ver a configuração de roteamento de sua máquina local por meio do comando route.

Na tabela de roteamento, você pode descobrir por qual Iface (placa de rede) e por qual Gateway (placa de rede) uma rede de destino deve enviar. Depois que a pesquisa é encontrada, ela é armazenada em cache no soquete e, na próxima vez que os dados forem enviados, não há necessidade de verificá-los.

Em seguida, coloque o endereço da tabela de roteamento em skb também.

//file: include/linux/skbuff.h
struct sk_buff {
 //保存了一些路由相关信息
 unsigned long  _skb_refdst;
}

A próxima etapa é localizar a posição do cabeçalho IP no skb e começar a definir o cabeçalho IP de acordo com a especificação do protocolo.

Em seguida, vá para a próxima etapa por meio de ip_local_out.

//file: net/ipv4/ip_output.c  
int ip_local_out(struct sk_buff *skb)
{
 //执行 netfilter 过滤
 err = __ip_local_out(skb);

 //开始发送数据
 if (likely(err == 1))
  err = dst_output(skb);
 ......

Em ip_local_out => __ip_local_out => nf_hook executará a filtragem netfilter. Se você usar o iptables para configurar algumas regras, aqui irá verificar se as regras foram atingidas. Se você definir uma regra netfilter muito complicada, esta função fará com que a sobrecarga da CPU do processo aumente muito .

Ainda não falo muito sobre isso, apenas continue falando sobre o processo relacionado ao envio de dst_output.

//file: include/net/dst.h
static inline int dst_output(struct sk_buff *skb)
{
 return skb_dst(skb)->output(skb);
}

Esta função encontra a tabela de roteamento (entrada dst) para este skb e então chama o método de saída da tabela de roteamento. Este é novamente um ponteiro de função, apontando para o método ip_output.

//file: net/ipv4/ip_output.c
int ip_output(struct sk_buff *skb)
{
 //统计
 .....

 //再次交给 netfilter,完毕后回调 ip_finish_output
 return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
    ip_finish_output,
    !(IPCB(skb)->flags & IPSKB_REROUTED));
}

Faça algum trabalho estatístico simples em ip_output e execute a filtragem netfilter novamente. Chame de volta ip_finish_output após a filtragem.

//file: net/ipv4/ip_output.c
static int ip_finish_output(struct sk_buff *skb)
{
 //大于 mtu 的话就要进行分片了
 if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
  return ip_fragment(skb, ip_finish_output2);
 else
  return ip_finish_output2(skb);
}

Em ip_finish_output, vemos que se os dados forem maiores que o MTU, a fragmentação será realizada.

O tamanho real da MTU é determinado pela descoberta da MTU e o quadro Ethernet é de 1500 bytes. Nos primeiros dias, a equipe QQ tentava controlar o tamanho de seus pacotes de dados para serem menores que o MTU e otimizar o desempenho da rede dessa maneira. Porque a fragmentação trará dois problemas: 1. Processamento de segmentação adicional é necessário, o que acarreta sobrecarga de desempenho adicional. 2. Enquanto um fragmento for perdido, todo o pacote deverá ser retransmitido. Portanto, evitar a fragmentação não apenas elimina a sobrecarga de fragmentação, mas também reduz bastante a taxa de retransmissão.

Em ip_finish_output2, o processo de envio final entrará na próxima camada, o subsistema vizinho.

//file: net/ipv4/ip_output.c
static inline int ip_finish_output2(struct sk_buff *skb)
{
 //根据下一跳 IP 地址查找邻居项,找不到就创建一个
 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);

 //继续向下层传递
 int res = dst_neigh_output(dst, neigh, skb);
}

4.4 Subsistema de Vizinhança

O subsistema vizinho é um sistema localizado entre a camada de rede e a camada de enlace de dados, cuja função é fornecer um encapsulamento para a camada de rede, de modo que a camada de rede não precise se preocupar com as informações de endereço da camada inferior e deixe a camada inferior decide para qual endereço MAC enviar.

E esse subsistema vizinho não está localizado no diretório net/ipv4/ da pilha de protocolos, mas em net/core/neighbor.c. Porque este módulo é necessário para IPv4 e IPv6.

No subsistema vizinho, é principalmente para encontrar ou criar uma entrada vizinha.Ao criar uma entrada vizinha, uma solicitação arp real pode ser enviada. Em seguida, encapsule o cabeçalho MAC e passe o processo de envio para o subsistema do dispositivo de rede de nível inferior. O processo geral é mostrado na figura.

Depois de entender o processo geral, vamos voltar ao código-fonte. __ipv4_neigh_lookup_noref é chamado no código-fonte de ip_finish_output2 na seção acima. Ele procura no cache arp e seu segundo parâmetro é a informação IP do próximo salto da rota.

//file: include/net/arp.h
extern struct neigh_table arp_tbl;
static inline struct neighbour *__ipv4_neigh_lookup_noref(
 struct net_device *dev, u32 key)
{
 struct neigh_hash_table *nht = rcu_dereference_bh(arp_tbl.nht);

 //计算 hash 值,加速查找
 hash_val = arp_hashfn(......);
 for (n = rcu_dereference_bh(nht->hash_buckets[hash_val]);
   n != NULL;
   n = rcu_dereference_bh(n->next)) {
  if (n->dev == dev && *(u32 *)n->primary_key == key)
   return n;
 }
}

Se não for encontrado, chame __neigh_create para criar um vizinho.

//file: net/core/neighbour.c
struct neighbour *__neigh_create(......)
{
 //申请邻居表项
 struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);

 //构造赋值
 memcpy(n->primary_key, pkey, key_len);
 n->dev = dev;
 n->parms->neigh_setup(n);

 //最后添加到邻居 hashtable 中
 rcu_assign_pointer(nht->hash_buckets[hash_val], n);
 ......

Depois de ter a entrada do vizinho, ele ainda não tem a capacidade de enviar pacotes IP neste momento, porque o endereço MAC de destino ainda não foi obtido. Chame dst_neigh_output para continuar passando skb.

//file: include/net/dst.h
static inline int dst_neigh_output(struct dst_entry *dst, 
     struct neighbour *n, struct sk_buff *skb)
{
 ......
 return n->output(n, skb);
}

Chamar a saída, na verdade, aponta para neigh_resolve_output. Dentro desta função é possível emitir uma requisição de rede arp.

//file: net/core/neighbour.c
int neigh_resolve_output(){

 //注意:这里可能会触发 arp 请求
 if (!neigh_event_send(neigh, skb)) {

  //neigh->ha 是 MAC 地址
  dev_hard_header(skb, dev, ntohs(skb->protocol),
           neigh->ha, NULL, skb->len);
  //发送
  dev_queue_xmit(skb);
 }
}

Depois de obter o endereço MAC do hardware, você pode encapsular o cabeçalho MAC do skb. Por fim, chame dev_queue_xmit para passar o skb para o subsistema do dispositivo de rede Linux.

4.5 Subsistema de Equipamentos de Rede

O subsistema vizinho entra no subsistema do dispositivo de rede por meio de dev_queue_xmit.

//file: net/core/dev.c 
int dev_queue_xmit(struct sk_buff *skb)
{
 //选择发送队列
 txq = netdev_pick_tx(dev, skb);

 //获取与此队列关联的排队规则
 q = rcu_dereference_bh(txq->qdisc);

 //如果有队列,则调用__dev_xmit_skb 继续处理数据
 if (q->enqueue) {
  rc = __dev_xmit_skb(skb, q, dev, txq);
  goto out;
 }

 //没有队列的是回环设备和隧道设备
 ......
}

Na segunda seção do capítulo de abertura, dissemos na preparação para o início da placa de rede que a placa de rede possui várias filas de envio (especialmente a placa de rede atual). A chamada acima para a função netdev_pick_tx é para selecionar uma fila para enviar.

A seleção da fila de envio netdev_pick_tx é afetada por configurações como XPS, e também há um cache, que também é um conjunto de lógica pequena e complicada. Aqui focamos apenas em duas lógicas: primeiro, será obtida a configuração XPS do usuário, caso contrário, será calculada automaticamente. Veja netdev_pick_tx => __netdev_pick_tx para o código.

//file: net/core/flow_dissector.c
u16 __netdev_pick_tx(struct net_device *dev, struct sk_buff *skb)
{
 //获取 XPS 配置
 int new_index = get_xps_queue(dev, skb);

 //自动计算队列
 if (new_index < 0)
  new_index = skb_tx_hash(dev, skb);}

Em seguida, obtenha o qdisc associado a esta fila. O tipo qdisc pode ser visto através do comando tc no linux, por exemplo, é mq disc em uma das minhas máquinas de placa de rede multi-fila.

#tc qdisc
qdisc mq 0: dev eth0 root

A maioria dos dispositivos tem filas (exceto dispositivos de loopback e túnel), então agora vamos para __dev_xmit_skb.

//file: net/core/dev.c
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);
 }
}

Existem duas situações no código acima, uma é que o sistema de filas pode ser ignorado e a outra é a fila normal. Vejamos apenas o segundo caso.

Primeiro chame q->enqueue para adicionar skb à fila. Em seguida, chame __qdisc_run para iniciar o envio.

//file: net/sched/sch_generic.c
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;
  }
 }
}

No código acima, vemos que o loop while busca continuamente skb da fila e os envia. Observe que esse tempo realmente ocupa o tempo de estado do sistema (sy) do processo do usuário. Somente quando a cota estiver esgotada ou outros processos precisarem da CPU, a interrupção suave será acionada para enviar.

Portanto, esta é a segunda razão pela qual NET_RX geralmente é muito maior que NET_TX ao visualizar /proc/softirqs em um servidor geral . Para leitura, precisa passar pela interrupção suave NET_RX, e para envio, a interrupção suave só é permitida quando a cota de estado do sistema for esgotada.

Vamos nos concentrar em qdisc_restart e continuar a ver o processo de envio.

static inline int qdisc_restart(struct Qdisc *q)
{
 //从 qdisc 中取出要发送的 skb
 skb = dequeue_skb(q);
 ...

 return sch_direct_xmit(skb, q, dev, txq, root_lock);
}

qdisc_restart pega um skb da fila e chama sch_direct_xmit para continuar enviando.

//file: net/sched/sch_generic.c
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
   struct net_device *dev, struct netdev_queue *txq,
   spinlock_t *root_lock)
{
 //调用驱动程序来发送数据
 ret = dev_hard_start_xmit(skb, dev, txq);
}

4.6 Agendamento de interrupção suave

Em 4.5, vimos que se a CPU no estado do sistema não for suficiente para enviar pacotes de rede, ela chamará __netif_schedule para acionar uma interrupção suave. Essa função entrará em __netif_reschedule, que na verdade emitirá uma interrupção suave do tipo NET_TX_SOFTIRQ.

A interrupção suave é executada pelo thread do kernel, que entrará na função net_tx_action, na qual a fila de envio pode ser obtida e, finalmente, a função de entrada dev_hard_start_xmit no driver é chamada.

//file: net/core/dev.c
static inline void __netif_reschedule(struct Qdisc *q)
{
 sd = &__get_cpu_var(softnet_data);
 q->next_sched = NULL;
 *sd->output_queue_tailp = q;
 sd->output_queue_tailp = &q->next_sched;

 ......
 raise_softirq_irqoff(NET_TX_SOFTIRQ);
}

Nesta função, a fila de dados a ser enviada é definida no softnet_data que pode ser acessado pelo softirq, e adicionada ao output_queue. Em seguida, uma interrupção suave do tipo NET_TX_SOFTIRQ é acionada. (T significa transmissão de transmissão)

Não entrarei em detalhes sobre o código de entrada do softirq aqui. Os alunos interessados ​​podem consultar a Seção 3.2 no artigo "Processo ilustrado de recebimento de pacotes de rede Linux" - ksoftirqd processos de thread do kernel softirq.

Começamos diretamente da função de retorno de chamada net_tx_action registrada por NET_TX_SOFTIRQ softirq. Depois que o processo do modo de usuário acionar a interrupção suave, um thread de kernel de interrupção suave executará net_tx_action.

Lembre-se de que a CPU consumida pelo envio de dados será exibida em si a partir de agora e o tempo do sistema do processo do usuário não será consumido .

//file: net/core/dev.c
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);
  }
 }
}

A interrupção suave obterá softnet_data aqui. Anteriormente, vimos que o modo do kernel do processo gravava a fila de envio na output_queue de softnet_data ao chamar __netif_reschedule. A interrupção suave percorre sd->output_queue para enviar quadros de dados.

Vejamos qdisc_run, que, como o modo de usuário do processo, também chamará __qdisc_run.

//file: include/net/pkt_sched.h
static inline void qdisc_run(struct Qdisc *q)
{
 if (qdisc_run_begin(q))
  __qdisc_run(q);
}

Então o mesmo é digitar qdisc_restart => sch_direct_xmit até a função do driver dev_hard_start_xmit.

Envio de driver de placa de rede 4.7 igb

Como vimos anteriormente, seja para o estado do kernel do processo do usuário ou para o contexto de interrupção suave, a função dev_hard_start_xmit no subsistema do dispositivo de rede será chamada. Nesta função será chamada a função de envio igb_xmit_frame no driver.

Na função do driver, o skb ficará pendurado no RingBuffer, depois que o driver for chamado, o pacote de dados será realmente enviado pela placa de rede.

Vamos dar uma olhada no código-fonte real:

//file: net/core/dev.c
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
   struct netdev_queue *txq)
{
 //获取设备的回调函数集合 ops
 const struct net_device_ops *ops = dev->netdev_ops;

 //获取设备支持的功能列表
 features = netif_skb_features(skb);

 //调用驱动的 ops 里面的发送回调函数 ndo_start_xmit 将数据包传给网卡设备
 skb_len = skb->len;
 rc = ops->ndo_start_xmit(skb, dev);
}

Dentre elas, ndo_start_xmit é uma função a ser implementada pelo driver da placa de rede, que é definida em net_device_ops.

//file: include/linux/netdevice.h
struct net_device_ops {
 netdev_tx_t  (*ndo_start_xmit) (struct sk_buff *skb,
         struct net_device *dev);

}

No código-fonte do driver da placa de rede igb, nós o encontramos.

//file: drivers/net/ethernet/intel/igb/igb_main.c
static const struct net_device_ops igb_netdev_ops = {
 .ndo_open  = igb_open,
 .ndo_stop  = igb_close,
 .ndo_start_xmit  = igb_xmit_frame, 
 ...
};

Ou seja, para ndo_start_xmit definido pela camada de dispositivo de rede, a função de implementação de igb é igb_xmit_frame. Esta função é atribuída quando o driver da placa de rede é inicializado. Para o processo de inicialização específico, consulte a Seção 2.4 do artigo "Processo ilustrado de recebimento de pacotes de rede Linux" , inicialização do driver da placa de rede.

Portanto, ao chamar ops->ndo_start_xmit na camada de dispositivo de rede acima, ele realmente entrará na função igb_xmit_frame. Vamos entrar nesta função para ver como o driver funciona.

//file: drivers/net/ethernet/intel/igb/igb_main.c
static netdev_tx_t igb_xmit_frame(struct sk_buff *skb,
      struct net_device *netdev)
{
 ......
 return igb_xmit_frame_ring(skb, igb_tx_queue_mapping(adapter, skb));
}

netdev_tx_t igb_xmit_frame_ring(struct sk_buff *skb,
    struct igb_ring *tx_ring)
{
 //获取TX Queue 中下一个可用缓冲区信息
 first = &tx_ring->tx_buffer_info[tx_ring->next_to_use];
 first->skb = skb;
 first->bytecount = skb->len;
 first->gso_segs = 1;

 //igb_tx_map 函数准备给设备发送的数据。
 igb_tx_map(tx_ring, first, hdr_len);
}

Aqui, um elemento é retirado do RingBuffer da fila de envio da placa de rede e o skb é anexado ao elemento.

A função igb_tx_map lida com o mapeamento dos dados skb em uma área de memória DMA acessível pela placa de rede.

//file: drivers/net/ethernet/intel/igb/igb_main.c
static void igb_tx_map(struct igb_ring *tx_ring,
      struct igb_tx_buffer *first,
      const u8 hdr_len)
{
 //获取下一个可用描述符指针
 tx_desc = IGB_TX_DESC(tx_ring, i);

 //为 skb->data 构造内存映射,以允许设备通过 DMA 从 RAM 中读取数据
 dma = dma_map_single(tx_ring->dev, skb->data, size, DMA_TO_DEVICE);

 //遍历该数据包的所有分片,为 skb 的每个分片生成有效映射
 for (frag = &skb_shinfo(skb)->frags[0];; frag++) {

  tx_desc->read.buffer_addr = cpu_to_le64(dma);
  tx_desc->read.cmd_type_len = ...;
  tx_desc->read.olinfo_status = 0;
 }

 //设置最后一个descriptor
 cmd_type |= size | IGB_TXD_DCMD;
 tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type);

 /* Force memory writes to complete before letting h/w know there
  * are new descriptors to fetch
  */
 wmb();
}

Quando todos os descritores necessários tiverem sido construídos e todos os dados no skb tiverem sido mapeados para endereços DMA, o driver vai para sua etapa final, acionando o envio real.

4.8 Enviar interrupção total completa

Quando os dados são enviados, o trabalho ainda não acabou. Porque a memória não foi limpa. Quando a transmissão estiver concluída, o dispositivo da placa de rede acionará uma interrupção de hardware para liberar a memória.

Nas seções 3.1 e 3.2 do artigo "Processo ilustrado de recebimento de pacotes de rede Linux" , descrevemos o processo de processamento de interrupções físicas e interrupções suaves em detalhes.

Na interrupção hard de conclusão de envio, será realizada a limpeza da memória do RingBuffer, conforme a figura.

Reveja o código-fonte da interrupção suave acionada pela interrupção rígida.

//file: drivers/net/ethernet/intel/igb/igb_main.c
static inline void ____napi_schedule(...){
 list_add_tail(&napi->poll_list, &sd->poll_list);
 __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

Existe um detalhe bem interessante aqui, seja a hard interrupt é porque há dados a serem recebidos, ou a notificação de conclusão do envio, a soft interrupt acionada a partir da hard interrupt é NET_RX_SOFTIRQ . Dissemos isso na primeira seção, essa é uma das razões pelas quais RX é maior que TX nas estatísticas de interrupção suave.

Ok, vamos para a função de retorno de chamada igb_poll da interrupção suave. Nesta função notamos que existe uma linha igb_clean_tx_irq, veja o código fonte:

//file: drivers/net/ethernet/intel/igb/igb_main.c
static int igb_poll(struct napi_struct *napi, int budget)
{
 //performs the transmit completion operations
 if (q_vector->tx.ring)
  clean_complete = igb_clean_tx_irq(q_vector);
 ...
}

Vamos dar uma olhada no que igb_clean_tx_irq faz quando a transmissão é concluída.

//file: drivers/net/ethernet/intel/igb/igb_main.c
static bool igb_clean_tx_irq(struct igb_q_vector *q_vector)
{
 //free the skb
 dev_kfree_skb_any(tx_buffer->skb);

 //clear tx_buffer data
 tx_buffer->skb = NULL;
 dma_unmap_len_set(tx_buffer, len, 0);

 // clear last DMA location and unmap remaining buffers */
 while (tx_desc != eop_desc) {
 }
}

Nada mais é do que limpar o skb, liberar o mapeamento DMA e etc. Neste ponto, a transferência está basicamente completa.

Por que digo que está basicamente concluído, não completamente concluído? Como a camada de transporte precisa garantir a confiabilidade, o skb não foi realmente excluído. Não será excluído até que receba o ACK da outra parte, momento em que será enviado por completo.

afinal

Use uma imagem para resumir todo o processo de envio

Depois de entender todo o processo de envio, vamos voltar e revisar as perguntas mencionadas no início.

1. Quando monitoramos a CPU consumida pelo kernel enviando dados, devemos olhar para sy ou si?

No processo de envio de pacotes de rede, o processo do usuário (no estado do kernel) conclui a maior parte do trabalho, até mesmo a chamada do driver. Uma interrupção suave é iniciada somente antes que o processo do modo kernel seja interrompido. Durante o processo de envio, a maior parte (90%) da sobrecarga é consumida no modo kernel do processo do usuário.

A interrupção suave (tipo NET_TX) é acionada apenas em alguns casos e é enviada pelo processo do kernel ksoftirqd de interrupção suave.

Portanto, ao monitorar a sobrecarga da CPU causada pela rede IO para o servidor, não devemos apenas olhar para si, mas devemos levar em consideração si e sy.

2. Verifique /proc/softirqs no servidor, por que NET_RX é muito maior que NET_TX?

Anteriormente, pensei que NET_RX fosse lido e NET_TX fosse transmitido. Para um Servidor que não apenas recebe as requisições dos usuários, mas também as devolve aos usuários. Os números dessas duas peças devem ser aproximadamente iguais, pelo menos não haverá diferença de ordem de grandeza. Mas, na verdade, um dos servidores de Fei Ge se parece com isso:

Após a análise do código-fonte de hoje, descobriu-se que existem dois motivos para esse problema.

A primeira razão é que, quando a transmissão de dados é concluída, o driver é notificado da conclusão da transmissão por meio de uma interrupção de hardware. No entanto, se a interrupção de hardware tiver recepção de dados ou a conclusão do envio, a interrupção de software acionada é NET_RX_SOFTIRQ, não NET_TX_SOFTIRQ.

A segunda razão é que, para leitura, tudo precisa passar pela interrupção suave NET_RX e passar pelo processo do kernel ksoftirqd. Para envio, a maior parte do trabalho é processada no modo kernel do processo do usuário e somente quando a cota do modo do sistema estiver esgotada, o NET_TX será enviado para liberar a interrupção suave.

Com base nos dois motivos acima, não é difícil entender que NET_RX é muito maior que NET_TX na máquina.

3. Quais operações de cópia de memória estão envolvidas no envio de dados de rede?

A cópia de memória aqui, nos referimos apenas à cópia de memória dos dados a serem enviados.

A primeira operação de cópia ocorre após o kernel ter solicitado o skb. Nesse momento, o conteúdo dos dados no buffer passado pelo usuário será copiado para o skb. Se a quantidade de dados a serem enviados for relativamente grande, a sobrecarga dessa operação de cópia não será pequena.

A segunda operação de cópia é ao entrar na camada de rede a partir da camada de transporte, e cada skb será clonado em uma nova cópia. A camada de rede e os drivers subjacentes, interrupções suaves e outros componentes excluirão esta cópia quando a transmissão for concluída. A camada de transporte salva o skb original e pode reenviá-lo quando o outro lado da rede não tiver ACK, de modo a realizar a transmissão confiável exigida no TCP.

A terceira cópia não é necessária, apenas quando a camada IP constatar que o skb é maior que o MTU. Ele solicitará skb adicionais e copiará o skb original em vários skb pequenos.

Insira aqui uma digressão, a cópia zero que todo mundo costuma ouvir em otimização de desempenho de rede, acho isso um pouco exagerado. Para garantir a confiabilidade do TCP, a segunda cópia não pode ser salva. Se o pacote for maior que o MTU, a cópia durante a fragmentação também será inevitável.

Vendo isso, acredito que o kernel enviando pacotes de dados não é mais uma caixa preta que você não entende nada.

Acho que você gosta

Origin blog.csdn.net/m0_64560763/article/details/131570295
Recomendado
Clasificación