Linux · Processus de réception de paquets réseau Linux illustré

Parce qu'il est nécessaire de fournir divers services réseau à des millions, des dizaines de millions, voire plus de 100 millions d'utilisateurs, l'une des principales exigences pour interviewer et promouvoir les étudiants en développement back-end dans les entreprises Internet de première ligne est de pouvoir prendre en charge des simultanéité et comprendre la surcharge de performances, seront optimisés pour les performances. Et souvent, si vous n'avez pas une compréhension approfondie de Linux sous-jacent, vous aurez l'impression que vous n'avez aucun moyen de démarrer lorsque vous rencontrez de nombreux goulots d'étranglement de performances en ligne.

Aujourd'hui, nous utilisons une méthode graphique pour comprendre en profondeur le processus de réception des paquets réseau sous Linux. Ou suivez la convention pour emprunter le morceau de code le plus simple pour commencer à réfléchir. Pour plus de simplicité, nous utilisons udp comme exemple, comme suit :

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);
}

Le code ci-dessus est un morceau de logique pour que le serveur udp reçoive des reçus. Du point de vue du développement, tant que le client envoie les données correspondantes, le serveurrecv_from peut les recevoir et les imprimer après exécution . Ce que nous voulons savoir maintenant, c'est que lorsque le paquet réseau atteint la carte réseau, jusqu'à ce que nous recvfromrecevions les données, que s'est-il passé au milieu ?

Grâce à cet article, vous comprendrez en profondeur comment le système de réseau Linux est implémenté en interne et comment chaque partie interagit. Je crois que cela sera d'une grande aide pour votre travail. Cet article est basé sur Linux 3.10. Voir https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/ pour le code source. Le pilote de carte réseau utilise la carte réseau igb d'Intel comme exemple.

Petit rappel amical, cet article est un peu long, vous pouvez d'abord le marquer puis le lire !

Un aperçu de la réception de paquets réseau Linux

Dans le modèle en couches réseau TCP/IP, l'ensemble de la pile de protocoles est divisé en couche physique, couche liaison, couche réseau, couche transport et couche application. La couche physique correspond aux cartes et câbles réseau, et la couche application correspond à nos applications communes Nginx, FTP et autres. Linux implémente trois couches : couche liaison, couche réseau et couche transport.

Dans l'implémentation du noyau Linux, le protocole de couche liaison est implémenté par le pilote de la carte réseau, et la pile de protocoles du noyau implémente la couche réseau et la couche transport. Le noyau fournit une interface de socket à la couche d'application supérieure pour que les processus utilisateur puissent y accéder. Le modèle de couches de réseau TCP/IP que nous voyons du point de vue de Linux devrait ressembler à ceci.

Figure 1 Pile de protocoles réseau du point de vue de Linux

Dans le code source de Linux, la logique correspondant au pilote de périphérique réseau se trouve driver/net/ethernetet le pilote de la carte réseau de la série intel se trouve dans driver/net/ethernet/intelle répertoire. Le code du module de pile de protocoles se trouve dans le répertoire kernelet net.

Les pilotes de périphérique du noyau et du réseau sont gérés au moyen d'interruptions. Lorsque les données arrivent sur l'appareil, cela déclenchera un changement de tension sur la broche correspondante du CPU pour informer le CPU de traiter les données. Pour le module réseau, en raison du traitement complexe et chronophage, si tout le traitement est terminé dans la fonction d'interruption, la fonction de traitement d'interruption (avec une priorité trop élevée) occupera excessivement le CPU, et le CPU ne pourra pas répondre à d'autres appareils, par exemple, les messages de la souris et du clavier. Par conséquent, la fonction de traitement des interruptions Linux est divisée en moitié supérieure et moitié inférieure. La partie supérieure consiste à effectuer le travail le plus simple, à traiter rapidement puis à libérer le CPU, puis le CPU peut permettre à d'autres interruptions d'entrer. La majeure partie du reste du travail est placée dans la moitié inférieure, qui peut être manipulée lentement et calmement. La moitié inférieure de la méthode d'implémentation adoptée par la version du noyau après 2.4 est l'interruption logicielle, qui est entièrement gérée par le thread du noyau ksoftirqd. Contrairement aux interruptions matérielles, les interruptions matérielles appliquent des changements de tension aux broches physiques du processeur, tandis que les interruptions logicielles informent le gestionnaire d'interruption logicielle en donnant une valeur binaire à une variable en mémoire.

Eh bien, après avoir acquis une compréhension générale des pilotes de carte réseau, des interruptions matérielles, des interruptions logicielles et des threads ksoftirqd, nous donnerons un schéma du chemin permettant au noyau de recevoir des paquets basés sur ces concepts :

Figure 2 Vue d'ensemble du réseau du noyau Linux recevant des paquets

Lorsque les données sont reçues sur la carte réseau, le premier module de travail sous Linux est le pilote réseau. Le pilote réseau va écrire la trame reçue sur la carte réseau dans la mémoire par DMA. Initiez ensuite une interruption au CPU pour notifier au CPU que les données sont arrivées. Deuxièmement, lorsque le CPU reçoit une demande d'interruption, il appelle le gestionnaire d'interruption enregistré par le pilote réseau. La fonction de traitement des interruptions de la carte réseau ne fait pas trop de travail, envoie une demande d'interruption logicielle, puis libère le processeur dès que possible. Lorsque ksoftirqd détecte qu'une demande d'interruption logicielle arrive, il appelle poll pour commencer à interroger et à recevoir des paquets, et après l'avoir reçu, il est transmis aux piles de protocoles à tous les niveaux pour traitement. Pour les paquets UDP, ils seront placés dans la file d'attente de réception du socket utilisateur.

À partir de l'image ci-dessus, nous avons saisi le processus de traitement de Linux sur le paquet de données dans son ensemble. Mais si nous voulons comprendre plus de détails sur le fonctionnement du module réseau, nous devons regarder en bas.

Deux démarrage Linux

Le pilote Linux, la pile de protocoles du noyau et d'autres modules doivent faire beaucoup de travail préparatoire avant de pouvoir recevoir les paquets de données de la carte réseau. Par exemple, le thread du noyau ksoftirqd doit être créé à l'avance, les fonctions de traitement correspondant à chaque protocole doivent être enregistrées, le sous-système de périphérique réseau doit être initialisé à l'avance et la carte réseau doit être démarrée. Ce n'est qu'après que ceux-ci sont prêts que nous pouvons réellement commencer à recevoir des paquets. Voyons donc comment ces préparations sont faites.

2.1 Créer le fil du noyau ksoftirqd

Les interruptions logicielles Linux sont toutes effectuées dans un thread de noyau dédié (ksoftirqd), il est donc très nécessaire pour nous de voir comment ces processus sont initialisés, afin que nous puissions comprendre plus précisément le processus de réception des paquets ultérieurement. Le nombre de processus n'est pas 1, mais N, où N est égal au nombre de cœurs de votre machine.

Lorsque le système est initialisé, smpboot_register_percpu_thread est appelé dans kernel/smpboot.c, et cette fonction sera ensuite exécutée sur spawn_ksoftirqd (situé dans kernel/softirq.c) pour créer un processus softirqd.

Figure 3 Créer un thread de noyau ksoftirqd

Le code correspondant est le suivant :

//file: kernel/softirq.c
static struct smp_hotplug_thread softirq_threads = {
    .store          = &ksoftirqd,
    .thread_should_run  = ksoftirqd_should_run,
    .thread_fn      = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/%u",};

static __init int spawn_ksoftirqd(void){
    register_cpu_notifier(&cpu_nfb);

    BUG_ON(smpboot_register_percpu_thread(&softirq_threads));    return 0;
}
early_initcall(spawn_ksoftirqd);

Lorsque ksoftirqd est créé, il entrera sa propre fonction de boucle de thread ksoftirqd_should_run et run_ksoftirqd. Jugez constamment s'il y a une interruption logicielle qui doit être traitée. Une chose à noter ici est que les interruptions logicielles ne sont pas seulement des interruptions logicielles réseau, mais également d'autres types.

//file: include/linux/interrupt.h

enum{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,  
};

2.2 Initialisation du sous-système réseau

Figure 4 Initialisation du sous-système réseau

Le noyau Linux subsys_initcallinitialise divers sous-systèmes via des appels, et vous pouvez gérer plusieurs appels à cette fonction dans le répertoire du code source. Ce dont nous parlons ici est l'initialisation du sous-système réseau, qui exécutera net_dev_initla fonction.

//file: 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);

Dans cette fonction, une softnet_datastructure de données sera appliquée pour chaque CPU. Dans cette structure de données, poll_listil attend que le pilote enregistre sa fonction d'interrogation. Nous pouvons voir ce processus lorsque le pilote de la carte réseau est initialisé plus tard.

De plus, open_softirq enregistre une fonction de traitement pour chaque interruption logicielle. La fonction de traitement de NET_TX_SOFTIRQ est net_tx_action et celle de NET_RX_SOFTIRQ est net_rx_action. Après avoir continué à suivre, open_softirqj'ai constaté que la méthode d'enregistrement est enregistrée dans softirq_vecla variable. Lorsque le thread ksoftirqd recevra ultérieurement une interruption logicielle, il utilisera également cette variable pour trouver la fonction de traitement correspondant à chaque interruption logicielle.

//file: kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *)){
    softirq_vec[nr].action = action;
}

2.3 Enregistrement de la pile de protocoles

Le noyau implémente le protocole ip au niveau de la couche réseau, ainsi que les protocoles tcp et udp au niveau de la couche transport. Les fonctions d'implémentation correspondant à ces protocoles sont respectivement ip_rcv(), tcp_v4_rcv() et udp_rcv(). Contrairement à la façon dont nous écrivons habituellement du code, le noyau est implémenté via l'enregistrement. fs_initcallSemblable à et dans le noyau Linux subsys_initcall, c'est aussi le point d'entrée du module d'initialisation. Démarrez l'enregistrement de la pile de protocoles réseau après fs_initcallavoir appelé . inet_initPassées inet_init, ces fonctions sont enregistrées dans les structures de données inet_protos et ptype_base. Comme indiqué ci-dessous:

Figure 5 Enregistrement de la pile de protocoles AF_INET

Le code correspondant est le suivant

//file: net/ipv4/af_inet.c
static struct packet_type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,};static const struct net_protocol udp_protocol = {
    .handler =  udp_rcv,
    .err_handler =  udp_err,
    .no_policy =    1,
    .netns_ok = 1,};static const struct net_protocol tcp_protocol = {
    .early_demux    =   tcp_v4_early_demux,
    .handler    =   tcp_v4_rcv,
    .err_handler    =   tcp_v4_err,
    .no_policy  =   1,    .netns_ok   =   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)
        pr_crit("%s: Cannot add UDP protocol\n", __func__);
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
        pr_crit("%s: Cannot add TCP protocol\n", __func__);
    ......    dev_add_pack(&ip_packet_type);
}

Dans le code ci-dessus, nous pouvons voir que le gestionnaire dans la structure udp_protocol est udp_rcv, et le gestionnaire dans la structure tcp_protocol est tcp_v4_rcv, qui est initialisé via 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_protocolLa fonction enregistre les fonctions de traitement correspondant à tcp et udp dans le tableau inet_protos. Regardez à dev_add_pack(&ip_packet_type);nouveau cette ligne. Le type dans la structure ip_packet_type est le nom du protocole, et func est la fonction ip_rcv, qui sera enregistrée dans la table de hachage ptype_base dans dev_add_pack.

//file: 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];
}

Ici, nous devons nous rappeler que inet_protos enregistre l'adresse de la fonction de traitement udp et tcp, et ptype_base stocke l'adresse de traitement de la fonction ip_rcv(). Plus tard, nous verrons que le softirq trouvera l'adresse de la fonction ip_rcv via ptype_base, puis enverra correctement le paquet ip à ip_rcv() pour exécution. Dans ip_rcv, la fonction de traitement tcp ou udp sera trouvée via inet_protos, puis le paquet sera transmis à la fonction udp_rcv() ou tcp_v4_rcv().

Pour développer, si vous regardez les codes de fonctions telles que ip_rcv et udp_rcv, vous pouvez voir le traitement de nombreux protocoles. Par exemple, ip_rcv gérera le filtrage netfilter et iptable.Si vous avez des règles netfilter ou iptables nombreuses ou très complexes, ces règles sont exécutées dans le contexte d'interruptions logicielles, ce qui augmentera le délai du réseau. Pour un autre exemple, udp_rcv jugera si la file d'attente de réception du socket est pleine. Les paramètres de noyau associés correspondants sont net.core.rmem_max et net.core.rmem_default. Si cela vous intéresse, je vous suggère de lire inet_initattentivement le code de cette fonction.

2.4 Initialisation du pilote de la carte réseau

Chaque pilote (pas seulement les pilotes de carte réseau) utilisera module_init pour enregistrer une fonction d'initialisation avec le noyau, et le noyau appellera cette fonction lorsque le pilote sera chargé. Par exemple, le code du pilote de la carte réseau igb se trouve dansdrivers/net/ethernet/intel/igb/igb_main.c

//file: drivers/net/ethernet/intel/igb/igb_main.c
static struct pci_driver igb_driver = {
    .name     = igb_driver_name,
    .id_table = igb_pci_tbl,
    .probe    = igb_probe,
    .remove   = igb_remove,    ......
};
static int __init igb_init_module(void){
    ......
    ret = pci_register_driver(&igb_driver);    return ret;
}

Une fois l' pci_register_driverappel du pilote terminé, le noyau Linux connaît les informations pertinentes du pilote, telles que le pilote de la carte réseau igb igb_driver_nameet igb_probel'adresse de la fonction, etc. Lorsque le périphérique de la carte réseau est reconnu, le noyau appellera la méthode de sonde de son pilote (la méthode de sonde de igb_driver est igb_probe). Le but de piloter l'exécution de la méthode de sonde est de rendre le périphérique prêt.Pour la carte réseau igb, il igb_probese trouve sous drivers/net/ethernet/intel/igb/igb_main.c. Les principales opérations réalisées sont les suivantes :

Figure 6 Initialisation du pilote de la carte réseau

À l'étape 5, nous pouvons voir que le pilote de la carte réseau implémente l'interface requise par ethtool, et s'enregistre également ici pour terminer l'enregistrement de l'adresse de la fonction. Quand ethtool initie un appel système, le noyau trouvera la fonction de rappel pour l'opération correspondante. Pour la carte réseau igb, ses fonctions d'implémentation sont toutes sous drivers/net/ethernet/intel/igb/igb_ethtool.c. Je pense que vous pouvez parfaitement comprendre le principe de fonctionnement d'ethtool cette fois, n'est-ce pas ? La raison pour laquelle cette commande peut afficher les statistiques d'envoi et de réception de paquets de la carte réseau, modifier le mode adaptatif de la carte réseau et ajuster le nombre et la taille de la file d'attente RX est que la commande ethtool appelle finalement la méthode correspondante du réseau. pilote de carte, plutôt qu'ethtool lui-même ayant ce super pouvoir.

Le igb_netdev_ops enregistré à l'étape 6 contient des fonctions telles que igb_open, qui seront appelées au démarrage de la carte réseau.

//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,
  .ndo_get_stats64        = igb_get_stats64,
  .ndo_set_rx_mode        = igb_set_rx_mode,
  .ndo_set_mac_address    = igb_set_mac,
  .ndo_change_mtu         = igb_change_mtu,  .ndo_do_ioctl           = igb_ioctl,
 ......

A l'étape 7, lors du processus d'initialisation de igb_probe, il est également appelé igb_alloc_q_vector. Il a enregistré une fonction d'interrogation nécessaire au mécanisme NAPI. Pour le pilote de la carte réseau igb, cette fonction est igb_poll, comme indiqué dans le code suivant.

static int igb_alloc_q_vector(struct igb_adapter *adapter,
                  int v_count, int v_idx,
                  int txr_count, int txr_idx,
                  int rxr_count, int rxr_idx){
    ......
    /* initialize NAPI */
    netif_napi_add(adapter->netdev, &q_vector->napi,               igb_poll, 64);
}

2.5 Démarrer la carte réseau

Lorsque l'initialisation ci-dessus est terminée, vous pouvez démarrer la carte réseau. Rappelant l'initialisation précédente du pilote de carte réseau, nous avons mentionné que le pilote a enregistré la variable de structure net_device_ops avec le noyau, qui contient des fonctions de rappel (pointeurs de fonction) telles que l'activation de la carte réseau, l'envoi de paquets et le réglage de l'adresse mac. Lorsqu'une carte réseau est activée (par exemple, via ifconfig eth0 up), la méthode igb_open dans net_device_ops est appelée. Il effectue généralement les opérations suivantes :

Figure 7 Démarrer la carte réseau

//file: drivers/net/ethernet/intel/igb/igb_main.c
static int __igb_open(struct net_device *netdev, bool resuming){
    /* allocate transmit descriptors */
    err = igb_setup_all_tx_resources(adapter);

    /* allocate receive descriptors */
    err = igb_setup_all_rx_resources(adapter);

    /* 注册中断处理函数 */
    err = igb_request_irq(adapter);
    if (err)
        goto err_req_irq;

    /* 启用NAPI */
    for (i = 0; i < adapter->num_q_vectors; i++)
        napi_enable(&(adapter->q_vector[i]->napi));    ......
}

La __igb_openfonction ci-dessus appelle igb_setup_all_tx_resources et igb_setup_all_rx_resources. Dans igb_setup_all_rx_resourcescette étape, le RingBuffer est alloué et la relation de mappage entre la mémoire et la file d'attente Rx est établie. (Le nombre et la taille des files d'attente Rx Tx peuvent être configurés via ethtool). Regardons à nouveau l'enregistrement de la fonction d'interruption igb_request_irq:

static int igb_request_irq(struct igb_adapter *adapter){
    if (adapter->msix_entries) {
        err = igb_request_msix(adapter);
        if (!err)
            goto request_done;
        ......    }
}
static int igb_request_msix(struct igb_adapter *adapter){
    ......
    for (i = 0; i < adapter->num_q_vectors; i++) {
        ...
        err = request_irq(adapter->msix_entries[vector].vector,
                  igb_msix_ring, 0, q_vector->name,
    }

En suivant l'appel de fonction dans le code ci-dessus, __igb_open=> igb_request_irq=> igb_request_msix, igb_request_msixnous pouvons voir que pour les cartes réseau multi-files d'attente, les interruptions sont enregistrées pour chaque file d'attente, et la fonction de traitement d'interruption correspondante est igb_msix_ring (cette fonction est également dans drivers/ net/ ethernet/intel/igb/igb_main.c). Nous pouvons également voir qu'en mode msix, chaque file d'attente RX a une interruption MSI-X indépendante.Au niveau de l'interruption matérielle de la carte réseau, il peut être défini de sorte que les paquets reçus soient traités par différents processeurs. (Vous pouvez modifier le comportement de liaison avec le CPU via irqbalance, ou modifier /proc/irq/IRQ_NUMBER/smp_affinity).

Lorsque les préparatifs ci-dessus sont terminés, vous pouvez ouvrir la porte pour accueillir les invités (paquets de données) !

Trois saluent l'arrivée des données

3.1 Traitement des interruptions matérielles

Tout d'abord, lorsque la trame de données arrive sur la carte réseau à partir du câble réseau, le premier arrêt est la file d'attente de réception de la carte réseau. La carte réseau recherche un emplacement de mémoire disponible dans le RingBuffer qui lui est alloué. Après l'avoir trouvé, le moteur DMA enverra par DMA les données à la mémoire associée à la carte réseau auparavant. À ce stade, le CPU est insensible. Lorsque l'opération DMA est terminée, la carte réseau lance une interruption matérielle comme le CPU pour informer le CPU que les données sont arrivées.

Figure 8 Processus de traitement des interruptions matérielles des données NIC

Remarque : Lorsque le RingBuffer est plein, les nouveaux paquets de données sont rejetés. Lorsque ifconfig vérifie la carte réseau, il peut y avoir un débordement, indiquant que le paquet a été rejeté car la file d'attente en anneau était pleine. Si une perte de paquets est détectée, vous devrez peut-être utiliser la commande ethtool pour augmenter la longueur de la file d'attente en anneau.

Dans la section sur le démarrage de la carte réseau, nous avons mentionné que la fonction de traitement de l'enregistrement des interruptions matérielles de la carte réseau est igb_msix_ring.

//file: drivers/net/ethernet/intel/igb/igb_main.c
static irqreturn_t igb_msix_ring(int irq, void *data){
    struct igb_q_vector *q_vector = data;

    /* Write the ITR value calculated from the previous interrupt. */
    igb_write_itr(q_vector);

    napi_schedule(&q_vector->napi);    return IRQ_HANDLED;
}

igb_write_itrEnregistrez simplement la fréquence d'interruption matérielle (on dit que le but est de réduire la fréquence d'interruption du CPU). Suivez l'appel napi_schedule jusqu'au bout, __napi_schedule=>____napi_schedule

/* Called with irq disabled */
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);
}

Ici, nous voyons que list_add_tailla poll_list dans la variable CPU softnet_data est modifiée, et la poll_list passée par le pilote napi_struct est ajoutée. La poll_list dans softnet_data est une liste bidirectionnelle dans laquelle les périphériques ont des trames d'entrée en attente de traitement. Ensuite, __raise_softirq_irqoffune interruption logicielle NET_RX_SOFTIRQ est déclenchée.Ce processus dit de déclenchement est juste une opération OU sur une variable.

void __raise_softirq_irqoff(unsigned int nr){
    trace_softirq_raise(nr);    or_softirq_pending(1UL << nr);
}
//file: include/linux/irq_cpustat.h
#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

Nous avons dit que Linux n'effectue que des travaux simples et nécessaires dans les interruptions matérielles, et la plupart du traitement restant est confié aux interruptions logicielles. Comme vous pouvez le voir dans le code ci-dessus, le processus de traitement des interruptions matérielles est vraiment très court. Je viens d'enregistrer un registre, de modifier la poll_list du CPU, puis d'émettre une interruption logicielle. C'est aussi simple que cela, même si le travail d'interruption difficile est terminé.

3.2 Le thread du noyau ksoftirqd gère les interruptions logicielles

Figure 9 thread du noyau ksoftirqd

Lorsque le thread du noyau est initialisé, nous avons introduit deux fonctions de thread ksoftirqd_should_runet dans ksoftirqd run_ksoftirqd. Le code est le ksoftirqd_should_runsuivant :

static int ksoftirqd_should_run(unsigned int cpu){    return local_softirq_pending();
}
#define local_softirq_pending() \    __IRQ_STAT(smp_processor_id(), __softirq_pending)

Ici, nous voyons que la même fonction est appelée dans l'interruption matérielle local_softirq_pending. La différence d'utilisation est que la position d'interruption dure est pour écrire la marque, ici c'est uniquement pour la lecture. S'il est défini dans l'interruption matérielle NET_RX_SOFTIRQ, il peut être lu ici naturellement. Ensuite, il entrera réellement dans la fonction de thread pour run_ksoftirqdle traitement :

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();
}

Dans __do_softirq, jugez en fonction du type d'interruption logicielle du processeur actuel et appelez sa méthode d'action enregistrée.

asmlinkage void __do_softirq(void){
    do {
        if (pending & 1) {
            unsigned int vec_nr = h - softirq_vec;
            int prev_count = preempt_count();
            ...
            trace_softirq_entry(vec_nr);
            h->action(h);
            trace_softirq_exit(vec_nr);
            ...
        }
        h++;
        pending >>= 1;    } while (pending);
}

Dans la section d'initialisation du sous-système réseau, nous avons vu que nous avions enregistré la fonction de gestionnaire net_rx_action pour NET_RX_SOFTIRQ. net_rx_actionLa fonction sera donc exécutée.

Ici, nous devons prêter attention à un détail. Le drapeau d'interruption logicielle est défini dans l'interruption matérielle, et le jugement de ksoftirq s'il y a une interruption logicielle arrive est basé sur smp_processor_id(). Cela signifie que tant que l'interruption matérielle est répondue sur quelle CPU, l'interruption logicielle est également traitée sur cette CPU. Ainsi, si vous constatez que la consommation de votre processeur d'interruption logicielle Linux est concentrée sur un cœur, la méthode consiste à ajuster l'affinité du processeur de l'interruption matérielle pour disperser l'interruption matérielle sur différents cœurs de processeur.

Concentrons-nous à nouveau sur cette fonction principale net_rx_action.

static void net_rx_action(struct softirq_action *h){
    struct softnet_data *sd = &__get_cpu_var(softnet_data);
    unsigned long time_limit = jiffies + 2;
    int budget = netdev_budget;
    void *have;

    local_irq_disable();
    while (!list_empty(&sd->poll_list)) {
        ......
        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) {
            work = n->poll(n, weight);
            trace_napi_poll(n);
        }
        budget -= work;    }
}

Le time_limit et le budget au début de la fonction sont utilisés pour contrôler la sortie active de la fonction net_rx_action, et le but est de s'assurer que la réception de paquets réseau n'occupe pas le CPU. Attendez la prochaine interruption matérielle de la carte réseau, puis traitez les paquets de données reçus restants. Le budget peut être ajusté via les paramètres du noyau. La logique de base restante dans cette fonction consiste à obtenir la variable CPU actuelle softnet_data, à parcourir sa poll_list, puis à exécuter la fonction d'interrogation enregistrée sur le pilote de la carte réseau. Pour la carte réseau igb, c'est igb_pollune fonction de la force motrice igb.

static int igb_poll(struct napi_struct *napi, int budget){
   
   

    ...
    if (q_vector->tx.ring)
        clean_complete = igb_clean_tx_irq(q_vector);

    if (q_vector->rx.ring)
        clean_complete &= igb_clean_rx_irq(q_vector, budget);    ...
}

Dans l'opération de lecture, igb_pollle travail clé est igb_clean_rx_irql'appel à .

static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget){
    ...
    do {
        /* retrieve a buffer from the ring */
        skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb);

        /* fetch next buffer in frame if non-eop */
        if (igb_is_non_eop(rx_ring, rx_desc))
            continue;
        }

        /* verify the packet layout is correct */
        if (igb_cleanup_headers(rx_ring, rx_desc, skb)) {
            skb = NULL;
            continue;
        }

        /* populate checksum, timestamp, VLAN, and protocol */
        igb_process_skb_fields(rx_ring, rx_desc, skb);

        napi_gro_receive(&q_vector->napi, skb);
    }
}

igb_fetch_rx_bufferLa fonction de sum igb_is_non_eopest de supprimer la trame de données de RingBuffer. Pourquoi avez-vous besoin de deux fonctions ? Comme il est possible que la trame occupe plusieurs RingBuffers, elle est acquise en boucle jusqu'à la fin de la trame. Une trame de données obtenue est représentée par un sk_buff. Après avoir reçu les données, effectuez quelques vérifications dessus, puis commencez à définir l'horodatage, l'ID VLAN, le protocole et d'autres champs de la variable sbk. Saisissez ensuite napi_gro_receive :

//file: net/core/dev.c
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb){
    skb_gro_reset_offset(skb);    return napi_skb_finish(dev_gro_receive(napi, skb), skb);
}

dev_gro_receiveCette fonction représente la fonctionnalité GRO de la carte réseau. Elle peut être simplement comprise comme la combinaison de petits paquets liés en un seul gros paquet. Le but est de réduire le nombre de paquets envoyés à la pile réseau, ce qui contribue à réduire l'utilisation du processeur. Ignorons-la pour l'instant, et regardons directement napi_skb_finish, cette fonction est principalement appelée netif_receive_skb.

//file: net/core/dev.c
static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb){
    switch (ret) {
    case GRO_NORMAL:
        if (netif_receive_skb(skb))
            ret = GRO_DROP;
        break;    ......
}

Dans netif_receive_skb, le paquet de données sera envoyé à la pile de protocoles. Déclaration, les 3.3, 3.4, 3.5 suivants appartiennent également au processus de traitement de l'interruption logicielle, mais parce que la longueur est trop longue, elle est extraite séparément en sous-sections.

3.3 Traitement de la pile de protocoles réseau

netif_receive_skbSelon le protocole du paquet, s'il s'agit d'un paquet udp, la fonction enverra le paquet à la fonction de traitement du protocole ip_rcv(), udp_rcv() pour traitement.

Figure 10 Traitement de la pile de protocoles réseau

//file: net/core/dev.c
int netif_receive_skb(struct sk_buff *skb){
    //RPS处理逻辑,先忽略    ......    return __netif_receive_skb(skb);
}
static int __netif_receive_skb(struct sk_buff *skb){
    ......  
    ret = __netif_receive_skb_core(skb, false);}static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc){
    ......

    //pcap逻辑,这里会将数据送入抓包点。tcpdump就是从这个入口获取包的    list_for_each_entry_rcu(ptype, &ptype_all, list) {
        if (!ptype->dev || ptype->dev == skb->dev) {
            if (pt_prev)
                ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = ptype;
        }
    }
    ......
    list_for_each_entry_rcu(ptype,
            &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
        if (ptype->type == type &&
            (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
             ptype->dev == orig_dev)) {
            if (pt_prev)
                ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = ptype;
        }    }
}

Dans __netif_receive_skb_core, j'ai regardé le point de capture de paquets de tcpdump que j'utilisais souvent, et j'étais très excité. Il semble que le temps de lire le code source n'est vraiment pas perdu. Sortez ensuite __netif_receive_skb_corele protocole, il sortira les informations de protocole du paquet de données, puis parcourra la liste des fonctions de rappel enregistrées sur ce protocole. ptype_baseIl s'agit d'une table de hachage, dont nous avons parlé dans la section d'enregistrement du protocole. L'adresse de la fonction ip_rcv est stockée dans cette table de hachage.

//file: 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->funcCette ligne appelle la fonction de gestionnaire enregistrée par la couche de protocole. Pour les paquets ip, il entrera ip_rcv(s'il s'agit d'un paquet arp, il entrera arp_rcv).

3.4 Traitement de la couche de protocole IP

Jetons un coup d'œil général à ce que Linux fait au niveau de la couche de protocole ip et à la manière dont le paquet est ensuite envoyé à la fonction de traitement du protocole udp ou tcp.

//file: 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){
    ......
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,               ip_rcv_finish);
}

Voici NF_HOOKune fonction de hook. Lorsque le hook enregistré est exécuté, la fonction pointée par le dernier paramètre sera exécutée ip_rcv_finish.

static int ip_rcv_finish(struct sk_buff *skb){
    ......
    if (!skb_dst(skb)) {
        int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
                           iph->tos, skb->dev);
        ...
    }
    ......    return dst_input(skb);
}

Après traçage, ip_route_input_norefj'ai vu qu'il s'appelait à nouveau ip_route_input_mc. Dans ip_route_input_mc, la fonction ip_local_deliverest affectée à dst.input, comme suit :

//file: net/ipv4/route.c
static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,u8 tos, struct net_device *dev, int our){
    if (our) {
        rth->dst.input= ip_local_deliver;
        rth->rt_flags |= RTCF_LOCAL;    }
}

Revenons donc à ip_rcv_finishcela return dst_input(skb);.

/* Input packet from network to transport.  */
static inline int dst_input(struct sk_buff *skb){
    return skb_dst(skb)->input(skb);
}

skb_dst(skb)->inputLa méthode d'entrée appelée est l'ip_local_deliver assigné par le sous-système de routage.

//file: net/ipv4/ip_input.c
int ip_local_deliver(struct sk_buff *skb){
    /*     *  Reassemble IP fragments.     */
    if (ip_is_fragment(ip_hdr(skb))) {
        if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
            return 0;
    }

    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,               ip_local_deliver_finish);
}
static int ip_local_deliver_finish(struct sk_buff *skb){
    ......
    int protocol = ip_hdr(skb)->protocol;
    const struct net_protocol *ipprot;

    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot != NULL) {
        ret = ipprot->handler(skb);    }
}

Comme vu dans la section d'enregistrement du protocole, les adresses de fonction de tcp_rcv() et udp_rcv() sont stockées dans inet_protos. Ici, la distribution sera sélectionnée en fonction du type de protocole dans le package. Ici, le package skb sera ensuite distribué au protocole de couche supérieure, udp et tcp.

3.5 Traitement de la couche de protocole UDP

Nous avons dit dans la section d'enregistrement du protocole que la fonction de traitement du protocole udp est udp_rcv.

//file: net/ipv4/udp.c
int udp_rcv(struct sk_buff *skb){
    return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
           int proto){
    sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

    if (sk != NULL) {
        int ret = udp_queue_rcv_skb(sk, skb
    }    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
}

__udp4_lib_lookup_skbIl s'agit de trouver le socket correspondant selon le skb, et quand il est trouvé, de mettre le paquet de données dans la file d'attente du buffer du socket. S'il n'est pas trouvé, un paquet icmp avec destination inaccessible est envoyé.

//file: net/ipv4/udp.c
int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb){  
    ......
    if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))
        goto drop;

    rc = 0;

    ipv4_pktinfo_prepare(skb);
    bh_lock_sock(sk);
    if (!sock_owned_by_user(sk))
        rc = __udp_queue_rcv_skb(sk, skb);
    else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {
        bh_unlock_sock(sk);
        goto drop;
    }
    bh_unlock_sock(sk);    return rc;
}

sock_owned_by_user juge si l'utilisateur fait un appel système sur cette socket (la socket est occupée), sinon, il peut être directement placé dans la file d'attente de réception de la socket. Si c'est le cas, ajoutez sk_add_backlogle paquet à la file d'attente du backlog. Lorsque l'utilisateur libère le socket, le noyau vérifie la file d'attente du backlog et la déplace vers la file d'attente de réception s'il y a des données.

sk_rcvqueues_fullSi la file d'attente de réception est pleine, le paquet sera directement rejeté. La taille de la file d'attente de réception est affectée par les paramètres du noyau net.core.rmem_max et net.core.rmem_default.

Quatre appels système recvfrom

Deux fleurs s'épanouissent, représentant chacune une branche. Ci-dessus, nous avons terminé le processus de réception et de traitement du paquet de données par l'ensemble du noyau Linux, et enfin placé le paquet de données dans la file d'attente de réception du socket. Revenons ensuite sur recvfromce qui s'est passé après l'appel du processus utilisateur. Ce que nous appelons dans le code recvfromest une fonction de la bibliothèque glibc. Une fois la fonction exécutée, l'utilisateur passera en mode noyau et entrera dans l'appel système implémenté par Linux sys_recvfrom. Avant de comprendre la paire Linux sys_revvfrom, examinons brièvement socketcette structure de données de base. Cette structure de données est trop volumineuse, nous ne dessinons aujourd'hui que le contenu lié à notre sujet, comme suit :

Figure 11 Organisation des données du noyau de socket

socketconst struct proto_opsCorrespondant dans la structure de données est l'ensemble des méthodes du protocole. Chaque protocole implémente un ensemble différent de méthodes. Pour la famille de protocoles Internet IPv4, chaque protocole a une méthode de traitement correspondante, comme suit. Pour UDP, il est inet_dgram_opsdéfini par , où les méthodes sont enregistrées inet_recvmsg.

//file: 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,    ......
}

socketUne autre structure de données dans la structure de données struct sock *skest une sous-structure très grande et très importante. Parmi eux, sk_protla fonction de traitement secondaire est définie. Pour le protocole UDP, il sera défini sur l'ensemble de méthodes mis en œuvre par le protocole UDP udp_prot.

//file: 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,    ......
}

Après avoir lu socketles variables, regardons sys_revvfromle processus de mise en œuvre.

Figure 12 Le processus de mise en œuvre interne de la fonction recvfrom

appelle . inet_recvmsg_sk->sk_prot->recvmsg

//file: net/ipv4/af_inet.c
int inet_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,size_t size, int flags){  
    ......
    err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,
                   flags & ~MSG_DONTWAIT, &addr_len);
    if (err >= 0)
        msg->msg_namelen = addr_len;    return err;
}

Nous avons dit plus haut que c'est sk_protle net/ipv4/udp.ccas pour le socket du protocole udp struct proto udp_prot. De cela, nous avons trouvé udp_recvmsgun moyen.

//file:net/core/datagram.c:EXPORT_SYMBOL(__skb_recv_datagram);
struct sk_buff *__skb_recv_datagram(struct sock *sk, unsigned int flags,int *peeked, int *off, int *err){
    ......
    do {
        struct sk_buff_head *queue = &sk->sk_receive_queue;
        skb_queue_walk(queue, skb) {
            ......
        }

        /* User doesn't want to wait */
        error = -EAGAIN;
        if (!timeo)
            goto no_packet;    } while (!wait_for_more_packets(sk, err, &timeo, last));
}

Enfin, nous avons trouvé le point que nous voulions voir, ci-dessus, nous avons vu le soi-disant processus de lecture, qui est l'accès sk->sk_receive_queue. S'il n'y a pas de données et que l'utilisateur autorise l'attente, wait_for_more_packets() sera appelé pour effectuer l'opération d'attente, ce qui mettra le processus utilisateur en veille.

Cinq Résumé

Le module réseau est le module le plus compliqué du noyau Linux. Il semble qu'un simple processus de réception de paquets implique l'interaction entre de nombreux composants du noyau, tels que le pilote de la carte réseau, la pile de protocoles, le thread ksoftirqd du noyau, etc. Cela semble très compliqué.Cet article veut expliquer clairement le processus de réception des paquets du noyau d'une manière facile à comprendre à travers des illustrations. Enchaînons maintenant l'ensemble du processus de collecte de paquets.

Une fois que l'utilisateur a exécuté recvfroml'appel, le processus utilisateur passe en mode noyau via l'appel système. S'il n'y a pas de données dans la file d'attente de réception, le processus se met en veille et est suspendu par le système d'exploitation. Cette pièce est relativement simple, et la plupart des scènes restantes sont réalisées par d'autres modules du noyau Linux.

Tout d'abord, avant de commencer à recevoir des paquets, Linux doit faire beaucoup de travail préparatoire :

  • 1. Créez un thread ksoftirqd, définissez sa propre fonction de thread pour celui-ci, puis comptez sur lui pour gérer les interruptions logicielles
  • 2. Enregistrement de la pile de protocoles, Linux doit implémenter de nombreux protocoles, tels que arp, icmp, ip, udp, tcp, chaque protocole enregistrera sa propre fonction de traitement, de sorte qu'il est pratique de trouver rapidement la fonction de traitement correspondante lorsque le paquet arrive
  • 3. Initialisation du pilote de la carte réseau, chaque pilote a une fonction d'initialisation et le noyau laissera le pilote l'initialiser. Dans ce processus d'initialisation, préparez votre propre DMA et indiquez au noyau l'adresse de la fonction d'interrogation de NAPI
  • 4. Démarrez la carte réseau, allouez les files d'attente RX et TX et enregistrez la fonction de traitement correspondant à l'interruption

Ce qui précède est le travail important avant que le noyau ne soit prêt à recevoir le paquet. Lorsque ce qui précède est prêt, vous pouvez ouvrir l'interruption matérielle et attendre l'arrivée du paquet de données.

Lorsque les données arrivent, la première chose à saluer est la carte réseau (j'y vais, n'est-ce pas un non-sens):

  • 1. La carte réseau DMA envoie la trame de données dans le RingBuffer de la mémoire, puis initie une notification d'interruption au CPU
  • 2. La CPU répond à la demande d'interruption et appelle la fonction de traitement d'interruption enregistrée au démarrage de la carte réseau
  • 3. La fonction de traitement des interruptions ne fait presque rien et lance une demande d'interruption logicielle
  • 4. Le thread ksoftirqd du noyau trouve qu'il y a une demande d'interruption logicielle et ferme d'abord l'interruption matérielle
  • 5. Le thread ksoftirqd commence à appeler la fonction d'interrogation du pilote pour recevoir des paquets
  • 6. La fonction poll envoie le paquet reçu à la fonction ip_rcv enregistrée dans la pile de protocoles
  • 7. La fonction ip_rcv envoie le paquet à la fonction udp_rcv (pour les paquets tcp, il est envoyé à tcp_rcv)

Nous pouvons maintenant revenir à la question du début, la simple ligne que nous avons vue dans la couche utilisateur recvfrom, le noyau Linux doit faire tellement de travail pour nous, afin que nous puissions recevoir les données en douceur. C'est toujours un simple UDP. Si c'est TCP, le noyau doit faire plus de travail. Je ne peux m'empêcher de soupirer que les développeurs du noyau sont vraiment bien intentionnés.

Après avoir compris l'ensemble du processus de réception de paquets, nous pouvons clairement connaître la surcharge CPU de Linux recevant un paquet. Tout d'abord, le premier bloc est la surcharge du processus utilisateur appelant l'appel système et passant en mode noyau. Le deuxième bloc est la surcharge CPU de l'interruption matérielle du paquet de réponse CPU. Le troisième bloc est utilisé par le contexte d'interruption logicielle du thread du noyau ksoftirqd. Plus tard, nous publierons un article dédié pour observer réellement ces dépenses.

De plus, il existe de nombreux détails dans l'envoi et la réception du réseau que nous n'avons pas développés, tels que l'absence de NAPI, GRO, RPS, etc. Parce que je pense que ce que j'ai dit est trop correct affectera la compréhension de l'ensemble du processus par tout le monde, alors essayez de ne garder que le cadre principal, moins c'est plus !

Je suppose que tu aimes

Origine blog.csdn.net/m0_64560763/article/details/131570606
conseillé
Classement