Grâce à Netty, j'ai acquis quelques connaissances sur les IO asynchrones en JAVA est un complément à l'IO original. Cet article enregistre principalement les principes sous-jacents d'implémentation des IO en JAVA et présente la technologie Zerocopy.
IO signifie en fait : les données sont constamment déplacées dans et hors du tampon (le tampon est utilisé). Par exemple, si le programme utilisateur lance une opération de lecture, entraînant un appel système « syscall read », les données seront déplacées dans un tampon ; si l'utilisateur lance une opération d'écriture, entraînant un appel système « syscall write », le les données dans un tampon seront déplacées (envoyer au réseau ou écrire dans un fichier disque)
Le processus ci-dessus semble simple, mais la façon dont le système d'exploitation sous-jacent est implémenté et les détails de la mise en œuvre sont très compliqués. Précisément en raison des différentes méthodes d'implémentation, il existe des méthodes d'implémentation pour le transfert de fichiers dans des circonstances ordinaires (appelons cela des IO ordinaires pour le moment), et il existe également des méthodes d'implémentation pour le transfert de fichiers volumineux ou le transfert de Big Data par lots, telles que la technologie zéro copie. .
Le déroulement de l'ensemble du processus IO est le suivant :
1) Le programmeur écrit du code pour créer un tampon (ce tampon est un tampon utilisateur) : Haha. Appelez ensuite la méthode read() dans une boucle while pour lire les données (déclenchant l'appel système "syscall read")
byte[] b = new byte[4096]; while((read = inputStream.read(b))>=0) { total = total + read; // other code…. }
2) Lorsque la méthode read() est exécutée, qu'est-ce que réellement se passe en bas De nombreuses opérations :
①Le noyau envoie une commande au contrôleur de disque disant : Je veux lire les données sur un certain bloc de disque sur le disque. –kernel émettant une commande au matériel du contrôleur de disque pour récupérer les données du disque
② Sous le contrôle de DMA, lisez les données sur le disque dans le tampon du noyau. –Le contrôleur de disque écrit les données directement dans un tampon mémoire du noyau par DMA
③Le noyau copie les données du tampon du noyau vers le tampon utilisateur. –kernel copie les données du tampon temporaire dans l'espace du noyau
Le tampon utilisateur ici devrait être le tableau byte[] de new dans le code que nous avons écrit.
Que peut-on analyser à partir des étapes ci-dessus ?
ⓐPour le système d'exploitation, la JVM n'est qu'un processus utilisateur, situé dans l'espace du mode utilisateur. Les processus dans l’espace utilisateur ne peuvent pas faire fonctionner directement le matériel sous-jacent. Les opérations d'E/S nécessitent l'exploitation du matériel sous-jacent, tel que les disques. Par conséquent, les opérations d'E/S doivent être effectuées à l'aide du noyau (interruption, trap), c'est-à-dire qu'il y aura un passage du mode utilisateur au mode noyau.
ⓑLorsque nous écrivons du code dans un nouveau tableau d'octets[], nous créons généralement un tableau de "n'importe quelle taille" "à volonté". Par exemple, nouvel octet[128], nouvel octet[1024], nouvel octet[4096]....
Cependant, pour lire des blocs de disque, chaque fois que vous accédez au disque pour lire des données, vous n'êtes pas lire n'importe quelle taille de données, mais : lire un bloc de disque ou plusieurs blocs de disque à la fois (en effet, le coût d'accès au fonctionnement du disque est très élevé et nous croyons également au principe de localité) Par conséquent, il est nécessaire pour un "tampon intermédiaire" » – c'est-à-dire le tampon du noyau. Lisez d'abord les données du disque dans le tampon du noyau, puis déplacez les données du tampon du noyau vers le tampon utilisateur.
C'est pourquoi nous avons toujours l'impression que la première opération de lecture est lente, mais que les opérations de lecture suivantes sont très rapides. Parce que, pour les opérations de lecture ultérieures, les données qu'il doit lire se trouvent probablement dans le tampon du noyau. À ce stade, il vous suffit de copier les données du tampon du noyau dans le tampon utilisateur, et les données sous-jacentes ne sont pas impliquées. La lecture des opérations sur disque est bien entendu rapide.
The kernel tries to cache and/or prefetch data, so the data being requested by the process may already be available in kernel space. If so, the data requested by the process is copied out. If the data isn’t available, the process is suspended while the kernel goes about bringing the data into memory.
Si les données ne sont pas disponibles, le processus sera suspendu et devra attendre que le noyau récupère les données du disque dans le tampon du noyau.
Alors nous pourrions dire : Pourquoi DMA ne lit-il pas directement les données sur le disque dans le tampon utilisateur ? D'une part se trouve le tampon du noyau mentionné en ⓑ comme tampon intermédiaire. Utilisé pour « ajuster » la « taille arbitraire » du tampon utilisateur et la taille fixe de chaque bloc de disque lu. D'un autre côté, le tampon utilisateur est situé dans l'espace du mode utilisateur et l'opération de lecture des données DMA implique le matériel sous-jacent. Le matériel ne peut généralement pas accéder directement à l'espace du mode utilisateur (probablement à cause du système d'exploitation)
<🎜. > En résumé, étant donné que DMA ne peut pas accéder directement à l'espace utilisateur (tampon utilisateur), les opérations d'E/S ordinaires doivent déplacer les données entre le tampon utilisateur et le tampon noyau, ce qui affecte la vitesse d'E/S dans certains programmes. Existe-t-il une solution correspondante ? Il s'agit d'IO mappées en mémoire directe, qui est le fichier mappé en mémoire mentionné dans JAVA NIO, ou mémoire directe... En bref, ils expriment des significations similaires. Le tampon d'espace noyau et le tampon d'espace utilisateur sont mappés sur la même zone de mémoire physique. Ses principales caractéristiques sont les suivantes : ① Plus aucun appel système de lecture ou d'écriture n'est nécessaire pour faire fonctionner le fichier. Le processus utilisateur voit les données du fichier comme de la mémoire, il n'est donc pas nécessaire d'émettre Appels système read() ou write().②Lorsque le processus utilisateur accède à l'adresse du "fichier mappé en mémoire", une erreur de page est automatiquement générée, puis le système d'exploitation sous-jacent est responsable de l'envoi des données sur le disque. à la mémoire. Concernant la gestion du stockage des pages, veuillez vous référer à : Quelques compréhensions de l'allocation de mémoire et de la gestion de la mémoireAs the user process touches the mapped memory space, page faults will be generated automatically to bring in the file data from disk. If the user modifies the mapped memory space, the affected page is automatically marked as dirty and will be subsequently flushed to disk to update the file.
这就是是JAVA NIO中提到的内存映射缓冲区(Memory-Mapped-Buffer)它类似于JAVA NIO中的直接缓冲区(Directed Buffer)。MemoryMappedBuffer可以通过java.nio.channels.FileChannel.java(通道)的 map方法创建。
使用内存映射缓冲区来操作文件,它比普通的IO操作读文件要快得多。甚至比使用文件通道(FileChannel)操作文件 还要快。因为,使用内存映射缓冲区操作文件时,没有显示的系统调用(read,write),而且OS还会自动缓存一些文件页(memory page)
zerocopy技术介绍
看完了上面的IO操作的底层实现过程,再来了解zerocopy技术就很easy了。IBM有一篇名为《Efficient data transfer through zero copy》的论文对zerocopy做了完整的介绍。感觉非常好,下面就基于这篇文来记录下自己的一些理解。
zerocopy技术的目标就是提高IO密集型JAVA应用程序的性能。在本文的前面部分介绍了:IO操作需要数据频繁地在内核缓冲区和用户缓冲区之间拷贝,而zerocopy技术可以减少这种拷贝的次数,同时也降低了上下文切换(用户态与内核态之间的切换)的次数。
比如,大多数WEB应用程序执行的一项操作就是:接受用户请求—>从本地磁盘读数据—>数据进入内核缓冲区—>用户缓冲区—>内核缓冲区—>用户缓冲区—>socket发送
数据每次在内核缓冲区与用户缓冲区之间的拷贝会消耗CPU以及内存的带宽。而zerocopy有效减少了这种拷贝次数。
Each time data traverses the user-kernel boundary, it must be copied, which consumes CPU cycles and memory bandwidth.
Fortunately, you can eliminate these copies through a technique called—appropriately enough —zero copy
那它是怎么做到的呢?
我们知道,JVM(JAVA虚拟机)为JAVA语言提供了跨平台的一致性,屏蔽了底层操作系统的具体实现细节,因此,JAVA语言也很难直接使用底层操作系统提供的一些“奇技淫巧”。
而要实现zerocopy,首先得有操作系统的支持。其次,JDK类库也要提供相应的接口支持。幸运的是,自JDK1.4以来,JDK提供了对NIO的支持,通过java.nio.channels.FileChannel类的transferTo()方法可以直接将字节传送到可写的通道中(Writable Channel),并不需要将字节送入用户程序空间(用户缓冲区)
You can use the transferTo()method to transfer bytes directly from the channel on which it is invoked to
another writable byte channel, without requiring data to flow through the application
下面就来详细分析一下经典的web服务器(比如文件服务器)干的活:从磁盘中中读文件,并把文件通过网络(socket)发送给Client。
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
从代码上看,就是两步操作。第一步:将文件读入buf;第二步:将 buf 中的数据通过socket发送出去。但是,这两步操作需要四次上下文切换(用户态与内核态之间的切换) 和 四次拷贝操作才能完成。
①第一次上下文切换发生在 read()方法执行,表示服务器要去磁盘上读文件了,这会导致一个 sys_read()的系统调用。此时由用户态切换到内核态,完成的动作是:DMA把磁盘上的数据读入到内核缓冲区中(这也是第一次拷贝)。
②第二次上下文切换发生在read()方法的返回(这也说明read()是一个阻塞调用),表示数据已经成功从磁盘上读到内核缓冲区了。此时,由内核态返回到用户态,完成的动作是:将内核缓冲区中的数据拷贝到用户缓冲区(这是第二次拷贝)。
③第三次上下文切换发生在 send()方法执行,表示服务器准备把数据发送出去了。此时,由用户态切换到内核态,完成的动作是:将用户缓冲区中的数据拷贝到内核缓冲区(这是第三次拷贝)
④第四次上下文切换发生在 send()方法的返回【这里的send()方法可以异步返回,所谓异步返回就是:线程执行了send()之后立即从send()返回,剩下的数据拷贝及发送就交给底层操作系统实现了】。此时,由内核态返回到用户态,完成的动作是:将内核缓冲区中的数据送到 protocol engine.(这是第四次拷贝)
这里对 protocol engine不是太了解,但是从上面的示例图来看:它是NIC(NetWork Interface Card) buffer。网卡的buffer???
下面这段话,非常值得一读:这里再一次提到了为什么需要内核缓冲区。
Copier le code
L'utilisation du tampon intermédiaire du noyau (plutôt qu'un transfert direct des données
dans le tampon utilisateur) peut sembler inefficace, mais des tampons intermédiaires du noyau ont été
introduits dans le processus pour améliorer les performances. . L'utilisation du
tampon intermédiaire côté lecture permet au tampon du noyau d'agir comme un « cache de lecture anticipée »
lorsque l'application n'a pas demandé autant de données que le tampon du noyau en contient
Cela améliore considérablement. performances lorsque la quantité de données demandée est inférieure
à la taille du tampon du noyau. Le tampon intermédiaire côté écriture permet à l'écriture de se terminer de manière asynchrone
Copier le code
Un point essentiel est le suivant : le tampon du noyau améliore les performances. Hein? N'est-ce pas étrange ? Car il a déjà été dit que c'est précisément à cause de l'introduction du tampon noyau (tampon intermédiaire) que les données sont copiées dans les deux sens, ce qui réduit l'efficacité.
Voyons d'abord pourquoi il est indiqué que le tampon du noyau améliore les performances.
Pour les opérations de lecture, le tampon du noyau est équivalent à un "cache de lecture anticipée". Lorsque le programme utilisateur n'a besoin de lire qu'une petite quantité de données à la fois, le système d'exploitation lit d'abord une grande partie des données. le disque au noyau. Le programme utilisateur n'enlève qu'une petite partie du tampon (je peux simplement créer un nouveau tableau de 128 octets ! nouvel octet[128]). Lorsque le programme utilisateur lira les données la prochaine fois, il pourra les extraire directement du tampon du noyau, et le système d'exploitation n'aura pas besoin d'accéder à nouveau au disque ! Parce que les données que l'utilisateur souhaite lire sont déjà dans le tampon du noyau ! C'est également la raison pour laquelle les opérations de lecture ultérieures (appels de méthode read()) sont nettement plus rapides que la première fois, comme mentionné précédemment. De ce point de vue, le tampon du noyau améliore les performances des opérations de lecture.
Regardons l'opération d'écriture : elle peut être effectuée « écrire de manière asynchrone ». Autrement dit : lorsque write(dest[]), le programme utilisateur demande au système d'exploitation d'écrire le contenu du tableau dest[] dans le fichier XX, donc la méthode d'écriture revient. Le système d'exploitation copie silencieusement le contenu du tampon utilisateur (dest[]) dans le tampon du noyau en arrière-plan, puis écrit les données du tampon du noyau sur le disque. Ensuite, tant que le tampon du noyau n'est pas plein, l'opération d'écriture de l'utilisateur peut revenir rapidement. Cela devrait être la stratégie de brossage de disque asynchrone.
(En fait, c'est ça. Un problème complexe dans le passé était que la différence entre les E/S synchrones, les E/S asynchrones, les E/S bloquantes et les E/S non bloquantes n'a plus beaucoup de sens. Ces concepts sont juste pour regarder au problème. Les perspectives sont simplement différentes. Le blocage et le non-blocage concernent le thread lui-même ; la synchronisation et l'asynchrone concernent le thread et les événements externes qui l'affectent...) [Pour une explication plus parfaite et incisive, veuillez vous référer. à cette série d'articles : Between Systems Communication (3) - IO Communication Model and JAVA Practice Part 1】
Puisque vous avez dit que le tampon du noyau est si puissant et parfait, pourquoi avez-vous besoin de zérocopie ? ? ?
Malheureusement, cette approche elle-même peut devenir un goulot d'étranglement en termes de performances si la taille des données demandées
est considérablement supérieure à la taille du tampon du noyau. Les données sont copiées plusieurs fois sur le disque, le tampon du noyau, et le tampon utilisateur avant qu'il ne soit finalement livré à l'application.
Zéro copie améliore les performances en éliminant ces copies de données redondantes
C'est enfin au tour de zérocopie de faire ses débuts. Lorsque les données à transférer sont beaucoup plus volumineuses que la taille du tampon du noyau, celui-ci devient un goulot d'étranglement. C'est pourquoi la technologie Zerocopy est adaptée aux transferts de fichiers volumineux. Pourquoi le tampon du noyau est-il devenu un goulot d’étranglement ? —Je pense qu'une des principales raisons est qu'il ne peut plus fonctionner comme un « tampon ». Après tout, la quantité de données transmises est trop importante.
de quatre à trois (dont une seule implique le CPU)
Nous pouvons réduire davantage la duplication de données effectuée par le noyau si la carte d'interface réseau sous-jacente prend en charge
les opérations de collecte. Dans les noyaux Linux 2.4 et versions ultérieures, le descripteur de tampon de socket a été modifié pour répondre à cette exigence. non seulement réduit les multiples changements de contexte, mais élimine également les copies de données en double qui
nécessitent l'implication du processeur
En d'autres termes, si le matériel réseau sous-jacent et le système d'exploitation le prennent en charge, le nombre de copies de données et le nombre d'interventions du processeur. peut être encore réduite.
Résumé des E/S non bloquantes et de la boucle d'événements dans Node.js_node.js
Node. Discussion sur les performances des E/S asynchrones de js_node.js
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!