Analyse du problème de l'alignement des grands et petits boutiens et octets dans le développement de la messagerie instantanée

Grand et petit

Qu'est-ce que le big endian?

Dans différentes architectures informatiques, les mécanismes de stockage et de transmission des données (bits, octets, mots, etc.) sont différents; à l'heure actuelle, il existe deux principaux mécanismes de stockage d'octets utilisés dans les ordinateurs de différentes architectures: Big-endian et Little-endian.
Endianness, également connu sous le nom d'endianness, endianness, anglais: Endianness. Dans le domaine de l'informatique, l'endianité fait référence à l'ordre des octets (octets) stockant des données multi-octets. La situation typique est la méthode de stockage des entiers en mémoire (petit boutien) et l'ordre de transmission de la transmission réseau (grand boutien) . L'endianness peut également être utilisée dans l'ordre des bits.

Original: http://smilejay.com/2012/01/big-endian_little-endian/

  • Little-Endian: Les systèmes d'exploitation courants sont le stockage little-endian , et divers langages de programmation sont également little-endian stockés en mémoire, tels que C / C ++. La caractéristique est la suivante: l'octet haut de données est dans l'adresse basse, et l'octet bas de données est dans l'adresse haute. Autrement dit, l'adresse passe de petite à grande, et les données sont placées de haut en bas.
  • Big-Endian: L'ordre des octets du réseau est un stockage big-endian . La caractéristique est: l'octet haut des données est dans l'adresse haute et l'octet bas est dans l'adresse basse
    Insérez la description de l'image ici

Dans quels scénarios rencontrerez-vous des problèmes petits et grands?

Programmation réseau C / C ++

Regardons un exemple plus spécifique (nous devons juste nous rappeler que les entiers sont stockés en tant que little-endian dans l'architecture commune du processeur x86, et C / C ++ doit être converti en big-endian lorsqu'il est envoyé au réseau).

En vs 2017, voyons à quoi ressemble uint32_t (entier non signé 32 bits 4 octets) en mémoire:
Insérez la description de l'image ici

Cette chaîne: 07 00 00 00 , c'est donc le mode petit-boutiste.

Et s'il était converti en big endian?
Insérez la description de l'image ici

Il devient: 00 00 00 07 .

Quelle est la différence entre les deux images avant et après? Le premier consiste à imprimer directement l'adresse mémoire de uint32_t, et le second est d'appeler indirectement htonl pour convertir le type long non signé de l'ordre de l'hôte (little endian sous x86 cpu) en ordre d'octet du réseau (big endian) via la fonction AppendInt32 du Classe de tampon de evpp .

Les détails des deux fonctions de conversion dans evpp (bibliothèque réseau multiplateforme C ++) sont les suivants:

namespace evpp {
    
    
class EVPP_EXPORT Buffer {
    
    
public:
    // ...

    // 往缓冲区中追加一个有符号32位整数
    void AppendInt32(int32_t x) {
    
    
        int32_t be32 = htonl(x); // htonl,转换成网络字节序(大端)后再写入内存
        Write(&be32, sizeof be32);
    }
	
    // ...

    // 取的时候,调用ntohl再从大端转换回来
    int32_t PeekInt32() const {
    
    
        assert(length() >= sizeof(int32_t));
        int32_t be32 = 0;
        ::memcpy(&be32, data(), sizeof be32);
        return ntohl(be32);
    }

PS:
Il existe les quatre fonctions de conversion couramment utilisées suivantes en C / C ++. Ces quatre fonctions prennent effet dans le système little endian. Le système big endian n'a pas besoin d'être converti car il a le même ordre d'octets que le réseau.
htons ——
Conversion de type court non signé de l'ordre d'hôte en ordre d'octet réseau ntohs —— Conversion de type court non signé de l'ordre d'octet réseau en ordre d'hôte
htonl —— Conversion de type long non signé de l'ordre d'hôte en ordre d'octet réseau
ntohl - - Conversion de type long non signé à partir de Ordre des octets du réseau à l'ordre des hôtes
Lorsque vous utilisez la fonction ci-dessus, vous devez inclure le fichier d'en-tête correspondant, comme suit:

#if defined(linux) || defined(__linux__)
	#include <netinet/in.h>
#endif
 
#ifdef WIN32
	#include <WINSOCK2.H>
#endif

Grand et petit boutian dans Go

Comme mentionné ci-dessus: En général, les systèmes d'exploitation courants sont petit-boutistes, tandis que l'ordre des octets du réseau est gros-boutiste . En règle générale, le protocole de la couche application de la transmission basée sur TCP adoptera le format TLV, c'est-à-dire la longueur de stockage d'octet fixe et le type de message de l'en-tête, tels que:

type ImHeader struct {
    
    
	Length    uint32 // the whole pdu length
	Version   uint16 // pdu version number
	Flag      uint16 // not used
	ServiceId uint16 //
	CommandId uint16 //
	SeqNum    uint16 // 包序号
	Reversed  uint16 // 保留

	pbMessage proto.Message // 消息体
}

Comme l'en-tête ci-dessus, 16 octets sont fixes. Les 4 premiers octets stockent la longueur. En supposant que mon serveur est développé avec go , quelles seront les conséquences si je l' analysais en fonction du big-endian?
Insérez la description de l'image ici

Les 67 premières lignes conformément à la grande fin de l'analyse du client avaient envoyé un numéro 7 (la longueur des données représentant la partie 7) , la ligne latérale 66 analysée naturellement pas à droite, en 117 440 512. Par conséquent, il n'y a aucun moyen d'obtenir les données correctement dans le département des données plus tard, et cela ne peut être considéré que comme un mauvais paquet.

Regardons à nouveau la mémoire:
Insérez la description de l'image ici

Nous voyons le début: 07 00 00 00, qui est en effet le petit boutien , et le bon gros boutien devrait être: 00 00 00 07.

Grand et petit boutian à Java

Citation:

Tous les fichiers binaires dans JAVA sont stockés en tant que big-endian , et cette méthode de stockage est également appelée ordre du réseau. C'est-à-dire que l'exécution de JAVA sur toutes les plates-formes, telles que Mac, PC, UNIX, etc., n'a pas besoin de prendre en compte le problème des grands et des petits bouts. Le problème est que les programmes développés dans différentes langues échangent des données. Par exemple, dans le projet récent de l'auteur, le fichier binaire est généré par C et envoyé au format Json via le canal de message redis. Le langage C utilise par défaut le mode little-endian , qui implique à la fois le grand et le petit boutiste. Certaines plates-formes (telles que Mac, IBM 390) ont un mode big-endian intégré, tandis que d'autres ont un mode little-endian intégré (comme Intel). JAVA vous aide à protéger la différence dans l'ordre des octets de chaque plate-forme.

Par conséquent, Java n'a généralement pas besoin de traiter le problème des grandes et petites extrémités, à moins qu'il ne doive communiquer avec d'autres langages tels que C / C ++.

en conclusion

  • Big endian: ordre des octets du réseau, Java.
  • Little-endian: En général, les systèmes d'exploitation courants sont little-endian, et C / C ++ est également un stockage little-endian, et Go peut spécifier le big-endian.

La machine virtuelle en Java protège le problème des petites et grandes extrémités, et il n'est pas nécessaire de la considérer s'il s'agit d'une communication entre Java.

Matériel de référence:

Alignement d'octets

Qu'est-ce que l'alignement d'octets?

Pour citer une phrase de l'Encyclopédie Baidu:

L'espace mémoire des ordinateurs modernes est divisé en octets. En théorie, il semble que l'accès à n'importe quel type de variable puisse commencer à partir de n'importe quelle adresse, mais la situation réelle est que lors de l'accès à un type spécifique de variable, il est souvent accédé à un adresse mémoire spécifique. Il est nécessaire que différents types de données soient disposés dans l'espace selon certaines règles, plutôt que séquentiellement les uns après les autres. Il s'agit de l'alignement

Examinons deux autres concepts ci-dessous pour approfondir votre compréhension.

Granularité d'accès à la mémoire

définition

Habituellement aux yeux des programmeurs C / C ++, char * signifie ubiquitairement "un morceau de mémoire", et même Java a un type byte [] qui représente la mémoire brute. Est-ce vraiment aussi simple que cela?

La mémoire aux yeux des programmeurs:
Insérez la description de l'image ici

La mémoire aux yeux du CPU:
Insérez la description de l'image ici

Le processeur de l'ordinateur ne lit et n'écrit pas la mémoire sur un seul octet. Au lieu de cela, la mémoire est accessible par blocs de 2, 4, 8, 16 ou même 32 octets (afin d'améliorer les performances). Nous appelons la taille de l' accès du processeur à la mémoire la granularité d'accès à la mémoire .

Alors, il y a une question: supposons que je définis une structure avec une taille de 5 (sauf 2)? Lisons d'abord les bases de l'alignement, et il y aura des réponses dans la section suivante: Alignement de la structure.

Base d'alignement

Granularité d'accès à la mémoire sur un octet

Insérez la description de l'image ici

Ceci est conforme à la perception générale du programmeur du modèle de fonctionnement de la mémoire: la lecture à partir de l'adresse 0 n'est pas différente de la lecture à partir de l'adresse 1. Maintenant, regardez ce qui se passe sur un processeur avec une granularité de deux octets, comme le 68000 d'origine;

Granularité d'accès à la mémoire sur deux octets

Insérez la description de l'image ici
Lors de la lecture à partir de l'adresse 0, le nombre d'accès mémoire occupés par un processeur avec une granularité de 2 octets est la moitié de celui d'un processeur avec une granularité de 1 octet. Étant donné que chaque accès mémoire nécessite une surcharge fixe, la réduction du nombre d'accès peut en effet améliorer les performances.

Cependant, veuillez faire attention à ce qui se passe lorsque vous lisez à partir de l'adresse 1. Étant donné que l'adresse ne tombe pas uniformément sur la limite d'accès mémoire du processeur, le processeur a encore beaucoup de travail à faire. De telles adresses sont appelées adresses non alignées (donc l'instruction #pragma pack ralentira le programme?). Étant donné que l'adresse 1 n'est pas alignée, un processeur avec une granularité de deux octets doit effectuer des accès mémoire supplémentaires, ce qui ralentit l'opération .

Enfin, vérifiez ce qui se passe sur un processeur avec une granularité d'accès mémoire de quatre octets, tel que 68030 ou PowerPC®601;

Granularité d'accès à la mémoire sur quatre octets

Insérez la description de l'image ici

Un processeur avec une granularité de quatre octets peut extraire quatre octets d'une adresse alignée en une seule lecture. Notez également que la lecture à partir d'une adresse non alignée doublera le nombre d'accès.

C'est précisément parce que l'accès mémoire du processeur n'est pas conforme à notre compréhension de la lecture de 1 octet 1 octet, de sorte que le compilateur est obligé de faire quelque chose pour que nous optimisions les performances. Ensuite, examinons l'alignement de la structure et comprenons une partie du travail effectué par le compilateur dans les coulisses.

Alignement de la structure

Cette fois, je vais expérimenter avec macbook pro 2017.

Une structure innocente:

struct Header {
    
    
    char a; // 1个字节
    int b;  // 4个字节
    char c; // 1个字节
};

Quelle est la taille (en octets) de cette structure? Intuitivement, il devrait être 1 + 4 + 1 égal à 6. Est-ce vrai? Jetons un coup d'œil à sa disposition en mémoire:
Insérez la description de l'image ici

L'image de droite est la disposition de la mémoire du compilateur clang sous macOS. Nous avons constaté que la taille réelle n'est pas de 6 octets, mais alignée selon la plus grande valeur de la structure (int = 4 octets), c'est-à-dire 3 * 4 = 12. Puis redondant. Nous utilisons clion pour déboguer (flottant sur la variable -> clic droit -> Afficher en vue mémoire) pour confirmer:
Insérez la description de l'image ici

C'est vrai, cela posera 2 problèmes:

  • La mémoire devient volumineuse et inutile, 6 octets sont inutiles
  • Tromper, il est très probable qu'une compréhension incohérente entre les deux parties pendant la transmission du réseau conduira à des résultats analytiques différents.

Que faire alors?

  1. [Pratique recommandée] Ajustez l'ordre des champs et remplissez manuellement les champs inférieurs à la valeur maximale de la structure pour éviter que le compilateur ne se remplisse automatiquement et ne cause des problèmes potentiels.
struct Header {
    
    
    int b;  // 4字节,放在最前面
    char a; // 1个字节
    char c; // 1个字节,还没满4*2的倍数=8,怎么办?
    int16_t reserved;// 我来显示定义一个保留位,占2字节
};
  1. Forcer le compilateur à s'aligner sur 1 octet
#pragma pack(1)

Insérez la description de l'image ici

Pourquoi envisager l'alignement des octets

Citation :

Si vous ne comprenez pas et ne résolvez pas le problème d'alignement dans le logiciel, les conditions suivantes sont possibles d'une gravité élevée à faible:

  • Votre logiciel ralentira.
  • Votre application sera verrouillée.
  • Votre système d'exploitation va planter.
  • Votre logiciel échouera silencieusement, produisant des résultats incorrects.

Pour faire simple, un alignement incorrect entraînera un gaspillage de mémoire, une dégradation des performances et même des plantages causés par des erreurs d'analyse.

en conclusion

vérifié:

  1. La valeur d'auto-alignement d'une structure ou d'une classe: la valeur avec la plus grande valeur d'auto-alignement parmi ses membres.
  2. Spécifiez la valeur d'alignement: #pragma pack (value) spécifie la valeur de la valeur d'alignement.

Non vérifié:

  1. La valeur d'alignement du type de données lui-même:
      pour les données de type char, sa valeur d'alignement est 1, pour le type court c'est 2, pour le type int, float, sa valeur d'alignement est 4, pour le type double, sa valeur d'alignement est 8, unité octet.
  2. Valeurs d'alignement valides des membres de données, des structures et des classes: la plus petite entre la valeur d'auto-alignement et la valeur d'alignement spécifiée.

La bonne approche, exemple 1:

struct Header {
    
    
	int b;  // 4个字节
	char a; // 1个字节
	char c; // 1个字节
	int16_t reserved; // 2个字节,对齐
};

Exemple 2 (Ainsi, lorsque nous personnalisons le protocole, en particulier les types de champs dans l'en-tête métier ne peuvent pas être définis arbitrairement, mais doivent être placés en fonction du multiple entier de la plus grande valeur):

type ImHeader struct {
    
    
	Length    uint32 // the whole pdu length
	Version   uint16 // pdu version number
	Flag      uint16 // not used
	ServiceId uint16 //
	CommandId uint16 //
	SeqNum    uint16 // 包序号
	Reversed  uint16 // 保留

	pbMessage proto.Message // 消息体
}

Matériel de référence:

Je suppose que tu aimes

Origine blog.csdn.net/xmcy001122/article/details/111215358
conseillé
Classement