Dank Netty habe ich einige Kenntnisse über asynchrones IO in JAVA erhalten, das eine Ergänzung zum ursprünglichen IO darstellt. Dieser Artikel beschreibt hauptsächlich die zugrunde liegenden Implementierungsprinzipien von IO in JAVA und stellt die Zerocopy-Technologie vor.
IO bedeutet eigentlich: Daten werden ständig in den Puffer hinein und aus ihm heraus verschoben (der Puffer wird verwendet). Wenn das Benutzerprogramm beispielsweise einen Lesevorgang initiiert, der zu einem Systemaufruf „Syscall Read“ führt, werden die Daten in einen Puffer verschoben, wenn der Benutzer einen Schreibvorgang initiiert, der zu einem Systemaufruf „Syscall Write“ führt Daten in einem Puffer werden verschoben (An das Netzwerk senden oder in eine Festplattendatei schreiben)
Der obige Prozess scheint einfach, aber die Implementierung des zugrunde liegenden Betriebssystems und die Details der Implementierung sind sehr kompliziert. Gerade aufgrund der unterschiedlichen Implementierungsmethoden gibt es Implementierungsmethoden für die Dateiübertragung unter normalen Umständen (nennen wir es vorerst normales E/A), und es gibt auch Implementierungsmethoden für die Übertragung großer Dateien oder die Übertragung großer Datenmengen im Batch, wie z. B. die Zerocopy-Technologie .
Der Ablauf des gesamten IO-Prozesses ist wie folgt:
1) Der Programmierer schreibt Code, um einen Puffer zu erstellen (dieser Puffer ist der Benutzerpuffer): Haha. Rufen Sie dann die Methode read() in einer While-Schleife auf, um die Daten zu lesen (was den Systemaufruf „syscall read“ auslöst).
byte[] b = new byte[4096]; while((read = inputStream.read(b))>=0) { total = total + read; // other code…. }
2) Wenn die Methode read() ausgeführt wird, werden viele Vorgänge ausgeführt passieren tatsächlich ganz unten:
①Der Kernel sendet einen Befehl an den Festplattencontroller mit der Meldung: Ich möchte die Daten auf einem bestimmten Festplattenblock auf der Festplatte lesen. –Kernel gibt einen Befehl an die Festplatten-Controller-Hardware aus, um die Daten von der Festplatte abzurufen.
② Lesen Sie unter der Kontrolle von DMA die Daten auf der Festplatte in den Kernel-Puffer. –Der Festplattencontroller schreibt die Daten per DMA direkt in einen Kernel-Speicherpuffer
③Der Kernel kopiert die Daten aus dem Kernel-Puffer in den Benutzerpuffer. –kernel kopiert die Daten aus dem temporären Puffer im Kernelraum
Der Benutzerpuffer hier sollte das Byte[]-Array von new in dem von uns geschriebenen Code sein.
Was kann aus den oben genannten Schritten analysiert werden?
ⓐFür das Betriebssystem ist die JVM nur ein Benutzerprozess, der sich im Benutzermodusbereich befindet. Prozesse im Benutzerbereich können die zugrunde liegende Hardware nicht direkt bedienen. E/A-Vorgänge erfordern den Betrieb der zugrunde liegenden Hardware, z. B. Festplatten. Daher müssen E/A-Vorgänge mit Hilfe des Kernels (Interrupt, Trap) abgeschlossen werden, d. h. es erfolgt ein Wechsel vom Benutzermodus in den Kernelmodus.
ⓑWenn wir Code für ein neues Byte[]-Array schreiben, erstellen wir normalerweise ein Array „beliebiger Größe“ „nach Belieben“. Zum Beispiel neues Byte[128], neues Byte[1024], neues Byte[4096]....
Beim Lesen von Festplattenblöcken ist dies jedoch nicht der Fall, wenn Sie auf die Festplatte zugreifen, um Daten zu lesen Lesen von Daten beliebiger Größe, aber: Lesen eines Plattenblocks oder mehrerer Plattenblöcke gleichzeitig (dies liegt daran, dass die Kosten für den Zugriff auf den Plattenvorgang sehr hoch sind und wir auch an das Prinzip der Lokalität glauben). Daher besteht ein Bedarf für einen „Zwischenpuffer“ – also Kernel-Puffer. Lesen Sie zuerst die Daten von der Festplatte in den Kernel-Puffer und verschieben Sie sie dann vom Kernel-Puffer in den Benutzerpuffer.
Aus diesem Grund haben wir immer das Gefühl, dass der erste Lesevorgang langsam ist, die nachfolgenden Lesevorgänge jedoch sehr schnell. Denn für nachfolgende Lesevorgänge befinden sich die zu lesenden Daten wahrscheinlich im Kernel-Puffer. Zu diesem Zeitpunkt müssen Sie nur die Daten im Kernel-Puffer in den Benutzerpuffer kopieren, die zugrunde liegenden Daten sind nicht beteiligt. Das Lesen von Festplattenvorgängen ist natürlich schnell.
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.
Wenn die Daten nicht verfügbar sind, wird der Prozess angehalten und muss warten, bis der Kernel die Daten von der Festplatte in den Kernel-Puffer holt.
Dann könnten wir sagen: Warum liest DMA die Daten auf der Festplatte nicht direkt in den Benutzerpuffer? Zum einen handelt es sich um den in ⓑ erwähnten Kernel-Puffer als Zwischenpuffer. Wird verwendet, um die „willkürliche Größe“ des Benutzerpuffers und die feste Größe jedes gelesenen Plattenblocks „anzupassen“. Andererseits befindet sich der Benutzerpuffer im Benutzermodusbereich und der Vorgang des DMA-Lesens von Daten betrifft die zugrunde liegende Hardware. Die Hardware kann im Allgemeinen nicht direkt auf den Benutzermodusbereich zugreifen (wahrscheinlich aufgrund des Betriebssystems)
Zusammenfassend lässt sich sagen, dass normale E/A-Vorgänge Daten zwischen dem Benutzerpuffer und dem Kernelpuffer hin- und herbewegen müssen, da DMA nicht direkt auf den Benutzerbereich (Benutzerpuffer) zugreifen kann, was sich auf die E/A-Geschwindigkeit in bestimmten Programmen auswirkt. Gibt es eine entsprechende Lösung?
Das ist Direct Memory Mapped IO, also die in JAVA NIO erwähnte Memory Mapped-Datei, oder Direct Memory ... Kurz gesagt, sie drücken ähnliche Bedeutungen aus. Kernel-Space-Puffer und User-Space-Puffer werden demselben physischen Speicherbereich zugeordnet.
Seine Hauptmerkmale sind wie folgt:
① Es sind keine Lese- oder Schreibsystemaufrufe mehr erforderlich, um die Datei zu bearbeiten – der Benutzerprozess sieht die Dateidaten im Speicher, sodass keine Ausgabe erforderlich ist Systemaufrufe read() oder write().
②Wenn der Benutzerprozess auf die Adresse der „speicherzugeordneten Datei“ zugreift, wird automatisch ein Seitenfehler generiert, und dann ist das zugrunde liegende Betriebssystem für das Senden der Daten auf die Festplatte verantwortlich zur Erinnerung. Informationen zur Seitenspeicherverwaltung finden Sie unter: Einige Erkenntnisse zur Speicherzuweisung und Speicherverwaltung
As 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???
下面这段话,非常值得一读:这里再一次提到了为什么需要内核缓冲区。
Code kopieren
Die Verwendung des Kernel-Zwischenpuffers (anstelle einer direkten Übertragung der Daten
in den Benutzerpuffer) scheint ineffizient zu sein, aber Kernel-Zwischenpuffer wurden
in den Prozess eingeführt, um die Leistung zu verbessern . Durch die Verwendung des Zwischenpuffers auf der Leseseite kann der Kernelpuffer als „Readahead-Cache“ fungieren,
wenn die Anwendung nicht so viele Daten angefordert hat, wie der Kernelpuffer enthält.
Dies verbessert sich erheblich Leistung, wenn die angeforderte Datenmenge kleiner ist als die Kernel-Puffergröße. Der Zwischenpuffer auf der Schreibseite ermöglicht den asynchronen Abschluss des Schreibvorgangs.
Kopieren von Code
Ein zentraler Punkt ist: Der Kernel-Puffer verbessert die Leistung. Hä? Ist es nicht seltsam? Denn es wurde bereits gesagt, dass gerade durch die Einführung des Kernel-Puffers (Zwischenpuffers) die Daten hin- und herkopiert werden, was die Effizienz verringert.
Werfen wir zunächst einen Blick darauf, warum es heißt, dass der Kernel-Puffer die Leistung verbessert.
Für Lesevorgänge entspricht der Kernel-Puffer einem „Readahead-Cache“. Wenn das Benutzerprogramm jeweils nur eine kleine Datenmenge lesen muss, liest das Betriebssystem zunächst eine große Datenmenge aus von der Festplatte zum Kernel. Das Benutzerprogramm nimmt nur einen kleinen Teil des Puffers weg (ich kann einfach ein 128-B-Byte-Array neu erstellen! Neues Byte[128]). Wenn das Benutzerprogramm das nächste Mal Daten liest, kann es diese direkt aus dem Kernel-Puffer übernehmen und das Betriebssystem muss nicht erneut auf die Festplatte zugreifen! Denn die Daten, die der Benutzer lesen möchte, befinden sich bereits im Kernel-Puffer! Dies ist auch der Grund, warum nachfolgende Lesevorgänge (read()-Methodenaufrufe) wie bereits erwähnt deutlich schneller sind als beim ersten Mal. Aus dieser Perspektive verbessert der Kernel-Puffer tatsächlich die Leistung von Lesevorgängen.
Schauen wir uns den Schreibvorgang an: Er kann „asynchron schreiben“ erfolgen. Das heißt: Beim Schreiben (dest[]) weist das Benutzerprogramm das Betriebssystem an, den Inhalt des dest[]-Arrays in die XX-Datei zu schreiben, sodass die Schreibmethode zurückkehrt. Das Betriebssystem kopiert im Hintergrund stillschweigend den Inhalt des Benutzerpuffers (dest[]) in den Kernel-Puffer und schreibt dann die Daten im Kernel-Puffer auf die Festplatte. Solange der Kernelpuffer nicht voll ist, kann der Schreibvorgang des Benutzers dann schnell zurückkehren. Dies sollte die asynchrone Disk-Brushing-Strategie sein.
(Eigentlich ist es das. Ein kompliziertes Problem in der Vergangenheit war, dass der Unterschied zwischen synchronem IO, asynchronem IO, blockierendem IO und nicht blockierendem IO nicht mehr viel Sinn macht. Diese Konzepte sind nur zum Anschauen da Beim Problem handelt es sich nur um unterschiedliche Perspektiven. Blockieren und Nichtblockieren gelten für den Thread selbst und asynchron für den Thread und die externen Ereignisse, die ihn beeinflussen zu dieser Artikelserie: Zwischen Systemkommunikation (3) – IO-Kommunikationsmodell und JAVA-Praxis Teil 1】
Da Sie sagten, der Kernel-Puffer sei so leistungsstark und perfekt, warum brauchen Sie Zerocopy? ? ?
Leider kann dieser Ansatz selbst zu einem Leistungsengpass führen, wenn die Größe der angeforderten Daten
beträchtlich größer ist als die Kernel-Puffergröße. Die Daten werden mehrmals zwischen der Festplatte, dem Kernel-Puffer usw. kopiert >und Benutzerpuffer, bevor es schließlich an die Anwendung übermittelt wirdZerocopy verbessert die Leistung durch Eliminierung dieser redundanten Datenkopien
Nun ist Zerocopy an der Reihe. Wenn die zu übertragenden Daten viel größer sind als die Größe des Kernel-Puffers, wird der Kernel-Puffer zu einem Engpass. Aus diesem Grund eignet sich die Zerocopy-Technologie für große Dateiübertragungen. Warum ist der Kernel-Puffer zum Engpass geworden? —Ich denke, ein wichtiger Grund ist, dass es nicht mehr als „Puffer“ fungieren kann. Schließlich ist die übertragene Datenmenge zu groß.
Werfen wir einen Blick darauf, wie die Zerocopy-Technologie die Dateiübertragung handhabt.
von vier auf drei (von denen nur eine die CPU betrifft)
Wenn die Zerocopy-Technologie nur so weit kommen kann, dann ist es einfach so.
Wir können die vom Kernel durchgeführte Datenduplizierung weiter reduzieren, wenn die zugrunde liegende Netzwerkschnittstellenkarte
Sammelvorgänge unterstützt. In Linux-Kerneln 2.4 und höher wurde der Socket-Puffer-Deskriptor geändert, um dieser Anforderung gerecht zu werden reduziert nicht nur mehrere Kontextwechsel, sondern eliminiert auch die doppelten Datenkopien, die
eine CPU-Beteiligung erfordern
Mit anderen Worten, wenn die zugrunde liegende Netzwerkhardware und das Betriebssystem dies unterstützen, verringert sich die Anzahl der Datenkopien und die Anzahl der CPU-Eingriffe weiter reduziert werden kann.
Zusammenfassung der nicht blockierenden E/A- und Ereignisschleife in Node.js_node.js
Node. Diskussion zur asynchronen IO-Leistung von js_node.js
Das obige ist der detaillierte Inhalt vonVerständnis von JAVA IO und NIO. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!