J'ai déjà lu un article de discussion sur la cohérence séquentielle et la cohérence du cache, et j'ai une compréhension plus claire de la différence et du lien entre ces deux concepts. Dans le noyau Linux, il existe de nombreux mécanismes de synchronisation et de barrière, que je voudrais résumer ici.
Avant, j'ai toujours pensé que de nombreux mécanismes sous Linux étaient destinés à assurer la cohérence du cache, mais en fait, l'essentiel de la cohérence du cache est obtenu par des mécanismes matériels. Ce n'est que lors de l'utilisation d'instructions avec le préfixe de verrouillage que cela a quelque chose à voir avec la mise en cache (bien que ce ne soit certainement pas strict, mais du point de vue actuel, c'est le cas dans la plupart des cas). La plupart du temps, nous souhaitons garantir une cohérence séquentielle.
La cohérence du cache signifie que dans un système multiprocesseur, chaque processeur possède son propre cache L1. Étant donné que le contenu du même morceau de mémoire peut être mis en cache dans le cache L1 de différents processeurs, lorsqu'un processeur modifie son contenu mis en cache, il doit garantir qu'un autre processeur peut également lire le dernier contenu lors de la lecture de ces données. Mais ne vous inquiétez pas, ce travail complexe est entièrement effectué par le matériel. En implémentant le protocole MESI, le matériel peut facilement réaliser le travail de cohérence du cache. Même si plusieurs processeurs écrivent en même temps, il n'y aura aucun problème. Que ce soit dans son propre cache, dans le cache d'autres processeurs ou en mémoire, un processeur peut toujours lire les dernières données. C'est ainsi que fonctionne la cohérence du cache.
La cohérence dite séquentielle fait référence à un concept complètement différent de la cohérence du cache, bien qu'elles soient toutes deux des produits du développement des processeurs. La technologie du compilateur continuant d'évoluer, elle peut modifier l'ordre de certaines opérations afin d'optimiser votre code. Les concepts d'exécution multi-problèmes et dans le désordre sont présents depuis longtemps dans les processeurs. Le résultat est que l’ordre réel des instructions exécutées sera légèrement différent de l’ordre d’exécution du code lors de la programmation. Bien sûr, cela n'est rien sous un seul processeur. Après tout, tant que votre propre code ne passe pas, personne ne s'en souciera de perturber l'ordre d'exécution tout en garantissant que leur propre code ne peut pas être découvert. Mais ce n'est pas le cas des multiprocesseurs. L'ordre dans lequel les instructions sont exécutées sur un processeur peut avoir un impact important sur le code exécuté sur les autres processeurs. Il existe donc le concept de cohérence séquentielle, qui garantit que l'ordre d'exécution des threads sur un processeur est le même du point de vue des threads sur d'autres processeurs. La solution à ce problème ne peut pas être résolue par le seul processeur ou le compilateur, mais nécessite une intervention logicielle.
La méthode d'intervention logicielle est également très simple, c'est-à-dire l'insertion d'une barrière mémoire. En fait, le terme barrière de mémoire a été inventé par les développeurs de processeurs, ce qui le rend difficile à comprendre. Les barrières de mémoire peuvent facilement nous conduire à la cohérence du cache, et même douter que nous puissions le faire pour permettre à d'autres processeurs de voir le cache modifié. C'est une erreur de le penser. Du point de vue du processeur, la barrière mémoire est utilisée pour sérialiser les opérations de lecture et d'écriture. D'un point de vue logiciel, elle est utilisée pour résoudre le problème de cohérence séquentielle. Le compilateur ne veut-il pas perturber l'ordre d'exécution du code ? Le processeur ne veut-il pas exécuter le code dans le désordre ? Lorsque vous insérez une barrière mémoire, cela équivaut à indiquer au compilateur l'ordre des instructions avant et après. la barrière ne peut pas être inversée. Il indique au processeur qu'il ne peut attendre les instructions avant la barrière qu'une fois l'instruction exécutée, les instructions derrière la barrière peuvent commencer à être exécutées. Bien sûr, les barrières de mémoire peuvent empêcher le compilateur de jouer, mais le processeur a encore un moyen. N'y a-t-il pas un concept d'exécution multi-problèmes, dans le désordre et d'achèvement séquentiel dans le processeur, pendant la barrière mémoire, il suffit de s'assurer que les opérations de lecture et d'écriture des instructions précédentes doivent être terminées avant le ? les opérations de lecture et d'écriture des instructions suivantes sont terminées. Par conséquent, il existe trois types de barrières mémoire : les barrières en lecture, les barrières en écriture et les barrières en lecture-écriture. Par exemple, avant x86, les opérations d'écriture étaient garanties d'être effectuées dans l'ordre, donc les barrières d'écriture n'étaient pas nécessaires. Cependant, maintenant, les opérations d'écriture de certains processeurs ia32 sont effectuées dans le désordre, des barrières d'écriture sont donc également nécessaires.
En fait, en plus des instructions spéciales de barrière en lecture-écriture, il existe de nombreuses instructions qui sont exécutées avec des fonctions de barrière en lecture-écriture, telles que les instructions avec un préfixe de verrouillage. Avant l’émergence d’instructions spéciales de barrière de lecture et d’écriture, Linux comptait sur le verrouillage pour survivre.
Quant à l’endroit où insérer les barrières de lecture et d’écriture, cela dépend des besoins du logiciel. La barrière de lecture-écriture ne peut pas atteindre pleinement la cohérence séquentielle, mais le thread sur le multiprocesseur ne regardera pas toujours votre ordre d'exécution tant qu'il garantit que lorsqu'il examine, il pense que vous respectez la cohérence séquentielle et. l'exécution ne vous amènera pas à Il n'y a pas de situations inattendues dans le code. Dans la situation dite inattendue, par exemple, votre thread attribue d'abord une valeur à la variable a, puis attribue une valeur à la variable b. En conséquence, les threads exécutés sur d'autres processeurs examinent et découvrent que b s'est vu attribuer une valeur. mais aucune valeur n'a été attribuée à a. (Remarque : cette incohérence n'est pas causée par une incohérence du cache, mais par l'incohérence dans l'ordre dans lequel les opérations d'écriture du processeur sont terminées. Dans ce cas, une barrière d'écriture doit être ajoutée entre les affectations). de a et l’affectation de b.
Avec SMP, les threads commencent à s'exécuter sur plusieurs processeurs en même temps. Tant qu'il s'agit d'un thread, il existe des exigences de communication et de synchronisation. Heureusement, le système SMP utilise une mémoire partagée, ce qui signifie que tous les processeurs voient le même contenu de mémoire. Bien qu'il existe un cache L1 indépendant, le traitement de la cohérence du cache est toujours géré par le matériel. Si les threads sur différents processeurs souhaitent accéder aux mêmes données, ils ont besoin de sections critiques et d'une synchronisation. De quoi dépend la synchronisation ? Dans le système UP auparavant, nous nous appuyions sur des sémaphores en haut et désactivions les interruptions et lisions, modifiions et écrivions les instructions en bas. Désormais, dans les systèmes SMP, la désactivation des interruptions a été supprimée. Même s'il est toujours nécessaire de synchroniser les threads sur le même processeur, il ne suffit plus de s'en remettre uniquement. Lire les instructions de modification et d'écriture ? Pas plus. Lorsque l'opération de lecture dans votre instruction est terminée et que l'opération d'écriture n'est pas effectuée, un autre processeur peut effectuer une opération de lecture ou d'écriture. Le protocole de cohérence du cache est avancé, mais il n’est pas encore suffisamment avancé pour prédire quelle instruction a émis cette opération de lecture. Ainsi, x86 a inventé des instructions avec un préfixe de verrouillage. Lorsque cette instruction est exécutée, toutes les lignes de cache contenant les adresses de lecture et d'écriture dans l'instruction seront invalidées et le bus mémoire sera verrouillé. De cette façon, si d'autres processeurs veulent lire ou écrire la même adresse ou l'adresse sur la même ligne de cache, ils ne peuvent le faire ni depuis le cache (la ligne correspondante dans le cache a expiré), ni depuis le cache. bus mémoire (l'ensemble du bus mémoire est en panne), atteignant enfin l'objectif de l'exécution atomique. Bien sûr, à partir du processeur P6, si l'adresse à laquelle accéder par l'instruction de préfixe de verrouillage est déjà dans le cache, il n'est pas nécessaire de verrouiller le bus mémoire et l'opération atomique peut être terminée (même si je soupçonne que cela est dû à l'ajout du commun interne multiprocesseur (en raison du cache L2).
Étant donné que le bus mémoire sera verrouillé, les opérations de lecture et d'écriture inachevées seront terminées avant d'exécuter les instructions avec le préfixe de verrouillage, qui sert également de barrière mémoire.
De nos jours, pour la synchronisation des threads entre multiprocesseurs, les verrous tournants sont utilisés en haut et les instructions de lecture, de modification et d'écriture avec le préfixe de verrouillage sont utilisées en bas. Bien entendu, la synchronisation proprement dite comprend également la désactivation de la planification des tâches du processeur, l'ajout d'interruptions de tâche et l'ajout d'un sémaphore à l'extérieur. La mise en œuvre de ce type de verrouillage tournant sous Linux a traversé quatre générations de développement et est devenue plus efficace et plus puissante.
\#ifdef CONFIG_SMP \#define smp_mb() mb() \#define smp_rmb() rmb() \#define smp_wmb() wmb() \#else \#define smp_mb() barrier() \#define smp_rmb() barrier() \#define smp_wmb() barrier() \#endif
CONFIG_SMP就是用来支持多处理器的。如果是UP(uniprocessor)系统,就会翻译成barrier()。
#define barrier() asm volatile(“”: : :”memory”)
barrier()的作用,就是告诉编译器,内存的变量值都改变了,之前存在寄存器里的变量副本无效,要访问变量还需再访问内存。这样做足以满足UP中所有的内存屏障。
\#ifdef CONFIG_X86_32 /* \* Some non-Intel clones support out of order store. wmb() ceases to be a \* nop for these. */ \#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2) \#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2) \#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM) \#else \#define mb() asm volatile("mfence":::"memory") \#define rmb() asm volatile("lfence":::"memory") \#define wmb() asm volatile("sfence" ::: "memory") \#endif
如果是SMP系统,内存屏障就会翻译成对应的mb()、rmb()和wmb()。这里CONFIG_X86_32的意思是说这是一个32位x86系统,否则就是64位的x86系统。现在的linux内核将32位x86和64位x86融合在同一个x86目录,所以需要增加这个配置选项。
可以看到,如果是64位x86,肯定有mfence、lfence和sfence三条指令,而32位的x86系统则不一定,所以需要进一步查看cpu是否支持这三条新的指令,不行则用加锁的方式来增加内存屏障。
SFENCE,LFENCE,MFENCE指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。 SFENCE——串行化发生在SFENCE指令之前的写操作但是不影响读操作。 LFENCE——串行化发生在SFENCE指令之前的读操作但是不影响写操作。 MFENCE——串行化发生在MFENCE指令之前的读写操作。 sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。 lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。 mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
至于带lock的内存操作,会在锁内存总线之前,就把之前的读写操作结束,功能相当于mfence,当然执行效率上要差一些。
说起来,现在写点底层代码真不容易,既要注意SMP问题,又要注意cpu乱序读写问题,还要注意cache问题,还有设备DMA问题,等等。
多处理器间同步的实现
多处理器间同步所使用的自旋锁实现,已经有专门的文章介绍
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!