網絡數據包的接收過程

這裏深度理解一下在 Linux 下網絡包的接收過程,爲了簡單起見,我們用 udp 來舉例,如下:

int main(){
    int serverSocketFd = socket(AF_INET, SOCK_DGRAM, 0);
    bind(serverSocketFd, ...);

    char buff[BUFFSIZE];
    int readCount = recvfrom(serverSocketFd, buff, BUFFSIZE, 0, ...);
    buff[readCount] = '\0';
    printf("Receive from client:%s\n", buff);

}

上面代碼是一段 udp server 接收收據的邏輯。只要客戶端有對應的數據發送過來,服務器端執行 recv_from 後就能收到它,並把它打印出來。那麼當網絡包達到網卡,直到 recvfrom 收到數據,這中間究竟都發生過什麼?

Linux 網絡架構

在 Linux 內核實現中,鏈路層協議靠網卡驅動來實現,內核協議棧來實現網絡層和傳輸層。內核對更上層的應用層提供 socket 接口來供用戶進程訪問。我們用 Linux 的視角來看到的 TCP/IP 網絡分層模型應該是下面這個樣子的。

Linux 網絡初始化

網絡設備子系統初始化

linux 內核通過調用 subsys_initcall 來初始化各個子系統,其中網絡子系統的初始化會執行到 net_dev_init 函數:

//net/core/dev.c

static int __init net_dev_init(void){

    ......

    for_each_possible_cpu(i) {
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        memset(sd, 0, sizeof(*sd));
        skb_queue_head_init(&sd->input_pkt_queue);
        skb_queue_head_init(&sd->process_queue);
        sd->completion_queue = NULL;
        INIT_LIST_HEAD(&sd->poll_list);
        ......
    }
    ......
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);

}

subsys_initcall(net_dev_init);

首先爲每個 CPU 都申請一個 softnet_data 數據結構,在這個數據結構裏的 poll_list 是等待驅動程序將其 poll 函數註冊進來,稍後網卡驅動初始化的時候我們可以看到這一過程。

然後 open_softirq 爲每一種軟中斷都註冊一個處理函數。NET_TX_SOFTIRQ 的處理函數爲 net_tx_action,NET_RX_SOFTIRQ 的爲 net_rx_action。

//kernel/softirq.c

void open_softirq(int nr, void (*action)(struct softirq_action *)){

    softirq_vec[nr].action = action;

}

open_softirq 會把不同的 action 記錄在 softirq_vec 變量裏的。後面 ksoftirqd 線程收到軟中斷的時候,也會使用這個變量來找到每一種軟中斷對應的處理函數。

網卡驅動初始化

這裏以 FSL 系列網卡爲例,其驅動位於:drivers/net/ethernet/freescale/fec_main.c

static struct platform_driver fec_driver = {
 .driver = {
  .name = DRIVER_NAME,
  .pm = &fec_pm_ops,
  .of_match_table = fec_dt_ids,
  .suppress_bind_attrs = true,
 },
 .id_table = fec_devtype,
 .probe = fec_probe,
 .remove = fec_drv_remove,
};
static int
fec_probe(struct platform_device *pdev)
{
  fec_enet_clk_enable
  fec_reset_phy      //使用gpio 復位phy 芯片
  fec_enet_init      //設置netdev_ops、設置ethtool_ops
  for (i = 0; i < irq_cnt; i++) {
    devm_request_irq(..., irq, fec_enet_interrupt, ...);
  }
  fec_enet_mii_init  //讀取dts mdio節點下phy子節點,並註冊phy_device
  register_netdev    //註冊網絡設備
}

Linux 以太網驅動會向上層提供 net_device_ops ,方便應用層控制網卡,比如網卡被啓動(例如,通過 ifconfig eth0 up)的時候會被調用 fec_enet_open,此外它還包含着網卡發包、設置 mac 地址等回調函數。

static const struct net_device_ops fec_netdev_ops = {
 .ndo_open  = fec_enet_open,
 .ndo_stop  = fec_enet_close,
 .ndo_start_xmit  = fec_enet_start_xmit,
 .ndo_select_queue       = fec_enet_select_queue,
 .ndo_set_rx_mode = set_multicast_list,
 .ndo_validate_addr = eth_validate_addr,
 .ndo_tx_timeout  = fec_timeout,
 .ndo_set_mac_address = fec_set_mac_address,
 .ndo_eth_ioctl  = fec_enet_ioctl,
#ifdef CONFIG_NET_POLL_CONTROLLER
 .ndo_poll_controller = fec_poll_controller,
#endif
 .ndo_set_features = fec_set_features,
 .ndo_bpf  = fec_enet_bpf,
 .ndo_xdp_xmit  = fec_enet_xdp_xmit,
};

此外,網卡驅動實現了 ethtool 所需要的接口,當 ethtool 發起一個系統調用之後,內核會找到對應操作的回調函數。可以看到 ethtool 這個命令之所以能查看網卡收發包統計、能修改網卡自適應模式、能調整 RX 隊列的數量和大小,是因爲 ethtool 命令最終調用到了網卡驅動的相應方法。

static const struct ethtool_ops fec_enet_ethtool_ops = {
 .supported_coalesce_params = ETHTOOL_COALESCE_USECS |
         ETHTOOL_COALESCE_MAX_FRAMES,
 .get_drvinfo  = fec_enet_get_drvinfo,
 .get_regs_len  = fec_enet_get_regs_len,
 .get_regs  = fec_enet_get_regs,
 .nway_reset  = phy_ethtool_nway_reset,
 .get_link  = ethtool_op_get_link,
 .get_coalesce  = fec_enet_get_coalesce,
 .set_coalesce  = fec_enet_set_coalesce,
#ifndef CONFIG_M5272
 .get_pauseparam  = fec_enet_get_pauseparam,
 .set_pauseparam  = fec_enet_set_pauseparam,
 .get_strings  = fec_enet_get_strings,
 .get_ethtool_stats = fec_enet_get_ethtool_stats,
 .get_sset_count  = fec_enet_get_sset_count,
#endif
 .get_ts_info  = fec_enet_get_ts_info,
 .get_tunable  = fec_enet_get_tunable,
 .set_tunable  = fec_enet_set_tunable,
 .get_wol  = fec_enet_get_wol,
 .set_wol  = fec_enet_set_wol,
 .get_eee  = fec_enet_get_eee,
 .set_eee  = fec_enet_set_eee,
 .get_link_ksettings = phy_ethtool_get_link_ksettings,
 .set_link_ksettings = phy_ethtool_set_link_ksettings,
 .self_test  = net_selftest,
};

協議棧初始化

內核實現了網絡層的 ip 協議,也實現了傳輸層的 tcp 協議和 udp 協議。這些協議對應的實現函數分別是 ip_rcv(),tcp_v4_rcv() 和 udp_rcv()。

網絡協議棧是通過函數 inet_init() 註冊的,通過 inet_init,將這些函數註冊到了 inet_protos 和 ptype_base 數據結構中了。如下圖:

相關代碼如下:

//net/ipv4/af_inet.c
static struct packet_type ip_packet_type __read_mostly = {
 .type = cpu_to_be16(ETH_P_IP),
 .func = ip_rcv,
 .list_func = ip_list_rcv,
};

static const struct net_protocol tcp_protocol = {
 .handler = tcp_v4_rcv,
 .err_handler = tcp_v4_err,
 .no_policy = 1,
 .icmp_strict_tag_validation = 1,
};

static const struct net_protocol udp_protocol = {
 .handler = udp_rcv,
 .err_handler = udp_err,
 .no_policy = 1,
};

static int __init inet_init(void){

    ......
    if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
        pr_crit("%s: Cannot add ICMP protocol\n", __func__);
    if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)  //註冊 udp_rcv()
        pr_crit("%s: Cannot add UDP protocol\n", __func__);
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)  //註冊 tcp_v4_rcv()
        pr_crit("%s: Cannot add TCP protocol\n", __func__);
    ......
    dev_add_pack(&ip_packet_type);  /註冊 ip_rcv()

}

上面的代碼中我們可以看到,udp_protocol 結構體中的 handler 是 udp_rcv,tcp_protocol 結構體中的 handler 是 tcp_v4_rcv,通過 inet_add_protocol 被初始化了進來。

int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol){
    if (!prot->netns_ok) {
        pr_err("Protocol %u is not namespace aware, cannot register.\n",
            protocol);
        return -EINVAL;
    }

    return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
            NULL, prot) ? 0 : -1;

}

inet_add_protocol 函數將 tcp 和 udp 對應的處理函數都註冊到了 inet_protos 數組中了。

再看 dev_add_pack(&ip_packet_type); 這一行,ip_packet_type 結構體中的 type 是協議名,func 是 ip_rcv 函數,在 dev_add_pack 中會被註冊到 ptype_base 哈希表中。

//net/core/dev.c
void dev_add_pack(struct packet_type *pt){

    struct list_head *head = ptype_head(pt);
    ......

}

static inline struct list_head *ptype_head(const struct packet_type *pt){

    if (pt->type == htons(ETH_P_ALL))
        return &ptype_all;
    else
        return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];

}

這裏我們需要記住 inet_protos 記錄着 udp,tcp 的處理函數地址,ptype_base 存儲着 ip_rcv() 函數的處理地址。後面我們會看到軟中斷中會通過 ptype_base 找到 ip_rcv 函數地址,進而將 ip 包正確地送到 ip_rcv() 中執行。在 ip_rcv 中將會通過 inet_protos 找到 tcp 或者 udp 的處理函數,再而把包轉發給 udp_rcv() 或 tcp_v4_rcv() 函數。

數據包的接收過程

硬中斷處理

首先當數據幀從網線到達網卡,網卡在分配給自己的 ringBuffer 中尋找可用的內存位置,找到後 DMA 會把數據拷貝到網卡之前關聯的內存裏。當 DMA 操作完成以後,網卡會向 CPU 發起一個硬中斷,通知 CPU 有數據到達。

中斷處理函數爲:

//drivers/net/ethernet/freescale/fec_main.c
static irqreturn_t
fec_enet_interrupt(int irq, void *dev_id)
{
 struct net_device *ndev = dev_id;
 struct fec_enet_private *fep = netdev_priv(ndev);
 irqreturn_t ret = IRQ_NONE;

 if (fec_enet_collect_events(fep) && fep->link) {
  ret = IRQ_HANDLED;

  if (napi_schedule_prep(&fep->napi)) {
   /* Disable interrupts */
   writel(0, fep->hwp + FEC_IMASK);
   __napi_schedule(&fep->napi);
  }
 }

 return ret;
}
//net/core/dev.c
__napi_schedule->____napi_schedule
static inline void ____napi_schedule(struct softnet_data *sd,

                     struct napi_struct *napi){
    list_add_tail(&napi->poll_list, &sd->poll_list);
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);

}

這裏我們看到,list_add_tail 修改了 CPU 變量 softnet_data 裏的 poll_list,將驅動 napi_struct 傳過來的 poll_list 添加了進來。其中 softnet_data 中的 poll_list 是一個雙向列表,其中的設備都帶有輸入幀等着被處理。緊接着 __raise_softirq_irqoff 觸發了一個軟中斷 NET_RX_SOFTIRQ。

注意:當 RingBuffer 滿的時候,新來的數據包將給丟棄。ifconfig 查看網卡的時候,可以裏面有個 overruns,表示因爲環形隊列滿被丟棄的包。如果發現有丟包,可能需要通過 ethtool 命令來加大環形隊列的長度。

ksoftirqd 軟中斷處理

接下來進入軟中斷處理函數:

//kernel/softirq.c
static void run_ksoftirqd(unsigned int cpu){
    local_irq_disable();
    if (local_softirq_pending()) {
        __do_softirq();
        rcu_note_context_switch(cpu);
        local_irq_enable();
        cond_resched();
        return;
    }
    local_irq_enable();

}

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
  while ((softirq_bit = ffs(pending))) {
    h->action(h);
  }
}

在網絡設備子系統初始化中,講到爲 NET_RX_SOFTIRQ 註冊了處理函數 net_rx_action。所以 net_rx_action 函數就會被執行到了。

//net/core/dev.c
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
  struct softnet_data *sd = this_cpu_ptr(&softnet_data);
  
  list_splice_init(&sd->poll_list, &list);
  
  for (;;) {
    ...
    n = list_first_entry(&list, struct napi_struct, poll_list);
    budget -= napi_poll(n, &repoll);
    ...
  }
  ...
}

napi_poll->__napi_poll->work = n->poll(n, weight)

首先獲取到當前 CPU 變量 softnet_data,對其 poll_list 進行遍歷, 然後執行到網卡驅動註冊到的 poll 函數。對於 FSL 網卡來說,其驅動對應的 poll 函數就是 fec_enet_rx_napi。

//drivers/net/ethernet/freescale/fec_main.c
static int fec_enet_rx_napi(struct napi_struct *napi, int budget)
{
 struct net_device *ndev = napi->dev;
 struct fec_enet_private *fep = netdev_priv(ndev);
 int done = 0;

 do {
  done += fec_enet_rx(ndev, budget - done);
  fec_enet_tx(ndev);
 } while ((done < budget) && fec_enet_collect_events(fep));

 if (done < budget) {
  napi_complete_done(napi, done);
  writel(FEC_DEFAULT_IMASK, fep->hwp + FEC_IMASK);
 }

 return done;
}
fec_enet_rx->fec_enet_rx_queue

然後進入 GRO 處理,流程如下:

napi_gro_receive->napi_skb_finish->gro_normal_one->gro_normal_list->netif_receive_skb_list_internal

最終通過函數 netif_receive_skb_list_internal() 進入內核協議棧。

協議棧處理

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
        struct packet_type **ppt_prev)
{
  ......
  //抓包
  list_for_each_entry_rcu(ptype, &ptype_all, list) {
      if (pt_prev)
          ret = deliver_skb(skb, pt_prev, orig_dev);
      pt_prev = ptype;
  }

  list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
      if (pt_prev)
          ret = deliver_skb(skb, pt_prev, orig_dev);
      pt_prev = ptype;
  }
  ......
  if (likely(!deliver_exact)) {
      deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                     &ptype_base[ntohs(type) &
                         PTYPE_HASH_MASK]);
  }
  ......
}

static inline void deliver_ptype_list_skb(struct sk_buff *skb,
       struct packet_type **pt,
       struct net_device *orig_dev,
       __be16 type,
       struct list_head *ptype_list)
{
 struct packet_type *ptype, *pt_prev = *pt;

 list_for_each_entry_rcu(ptype, ptype_list, list) {
  if (ptype->type != type)
   continue;
  if (pt_prev)
   deliver_skb(skb, pt_prev, orig_dev);
  pt_prev = ptype;
 }
 *pt = pt_prev;
}

函數 deliver_ptype_list_skb 會從數據包中取出協議信息,然後遍歷註冊在這個協議上的回調函數列表。ptype_base 是一個 hash table,在協議初始化小節我們提到過,ip_rcv 函數地址就是存在這個 hash table 中的。

//net/core/dev.c
static inline int deliver_skb(struct sk_buff *skb,
                  struct packet_type *pt_prev,
                  struct net_device *orig_dev)
{
    ......
    return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}

pt_prev->func 這一行就調用到了協議層註冊的處理函數了。對於 ip 包來講,就會進入到 ip_rcv(如果是 arp 包的話,會進入到 arp_rcv)。

//net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
    struct net_device *orig_dev)
{
 struct net *net = dev_net(dev);

 skb = ip_rcv_core(skb, net);
 if (skb == NULL)
  return NET_RX_DROP;

 return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
         net, NULL, skb, dev, NULL,
         ip_rcv_finish);
}

這裏 NF_HOOK 是一個鉤子函數,當執行完註冊的鉤子後就會執行到最後一個參數指向的函數 ip_rcv_finish。

ip_rcv_finish->dst_input->ip_local_deliver->ip_local_deliver_finish
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
 skb_clear_delivery_time(skb);
 __skb_pull(skb, skb_network_header_len(skb));

 rcu_read_lock();
 ip_protocol_deliver_rcu(net, skb, ip_hdr(skb)->protocol);
 rcu_read_unlock();

 return 0;
}
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
  ......
  ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
          skb);
  ......
}

在這裏 skb 包將會進一步被派送到更上層的協議中,udp 和 tcp。

//net/ipv4/udp.c
int udp_rcv(struct sk_buff *skb)
{
 return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}

應用層處理

通過開頭的應用程序,我們知道應用層的數據接收函數是 recvfrom,recvfrom 是一個 glibc 的庫函數,該函數在執行後會將用戶進行陷入到內核態,進入到 Linux 實現的系統調用 sys_recvfrom。

在理解 sys_revvfrom 之前,我們先來簡單看一下 socket 這個核心數據結構。

socket 數據結構中的 const struct proto_ops 對應的是協議的方法集合。每個協議都會實現不同的方法集,對於 IPv4 Internet 協議族來說, 每種協議都有對應的處理方法,如下。對於 udp 來說,是通過 inet_dgram_ops 來定義的,其中註冊了 inet_recvmsg 方法。

//net/ipv4/af_inet.c
const struct proto_ops inet_stream_ops = {
    ......
    .recvmsg       = inet_recvmsg,
    .mmap          = sock_no_mmap,
    ......
}

const struct proto_ops inet_dgram_ops = {
    ......
    .sendmsg       = inet_sendmsg,
    .recvmsg       = inet_recvmsg,
    ......
}

socket 數據結構中的另一個數據結構 struct sock *sk 是一個非常大,非常重要的子結構體。其中的 sk_prot 又定義了二級處理函數。對於 UDP 協議來說,會被設置成 UDP 協議實現的方法集 udp_prot。

//net/ipv4/udp.c
struct proto udp_prot = {
    .name          = "UDP",
    .owner         = THIS_MODULE,
    .close         = udp_lib_close,
    .connect       = ip4_datagram_connect,
    ......
    .sendmsg       = udp_sendmsg,
    .recvmsg       = udp_recvmsg,
    .sendpage      = udp_sendpage,
    ......
}

看完了 socket 變量之後,我們再來看 sys_revvfrom 的實現過程。

總結

首先在開始收包之前,Linux 要做許多的準備工作:

  1. 創建 ksoftirqd 線程,爲它設置好它自己的線程函數,後面指望着它來處理軟中斷呢

  2. 協議棧註冊,linux 要實現許多協議,比如 arp,icmp,ip,udp,tcp,每一個協議都會將自己的處理函數註冊一下,方便包來了迅速找到對應的處理函數

  3. 網卡驅動初始化,每個驅動都有一個初始化函數,內核會讓驅動也初始化一下。在這個初始化過程中,把自己的 DMA 準備好,把 NAPI 的 poll 函數地址告訴內核

  4. 啓動網卡,分配 RX,TX 隊列,註冊中斷對應的處理函數

當上面都 ready 之後,就可以打開硬中斷,等待數據包的到來了:

  1. 網卡將數據幀 DMA 到內存的 RingBuffer 中,然後向 CPU 發起中斷通知

  2. CPU 響應中斷請求,調用網卡啓動時註冊的中斷處理函數

  3. 中斷處理函數幾乎沒幹啥,就發起了軟中斷請求

  4. 內核線程 ksoftirqd 線程發現有軟中斷請求到來,先關閉硬中斷

  5. ksoftirqd 線程開始調用驅動的 poll 函數收包

  6. poll 函數將收到的包送到協議棧註冊的 ip_rcv 函數中

  7. ip_rcv 函數再講包送到 udp_rcv 函數中(對於 tcp 包就送到 tcp_rcv)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OI53ckc-u_NmYE28-w7YNA