>  기사  >  类库下载  >  JAVA IO 및 NIO 이해

JAVA IO 및 NIO 이해

坏嘻嘻
坏嘻嘻원래의
2018-09-14 09:23:241998검색

Netty 덕분에 JAVA의 NIO는 원래 IO의 보충 자료입니다. 이 기사에서는 주로 JAVA의 IO 구현 원리와 Zerocopy 기술에 대해 설명합니다.

IO는 실제로 데이터가 버퍼 안팎으로 끊임없이 이동한다는 의미입니다(버퍼가 사용됨). 예를 들어, 사용자 프로그램이 읽기 작업을 시작하여 "syscall read" 시스템 호출이 발생하고 사용자가 쓰기 작업을 시작하여 "syscall write" 시스템 호출이 발생하면 데이터가 버퍼로 이동됩니다. 버퍼에 있는 데이터가 밖으로 이동됩니다(네트워크로 보내기 또는 디스크 파일에 쓰기)

위 프로세스는 간단해 보이지만 기본 운영 체제가 어떻게 구현되고 구현 세부 사항이 매우 중요합니다. 복잡한. 일반적인 상황(당분간 일반 IO라고 부르겠습니다)에서 파일 전송을 위한 구현 방법이 있고 다음과 같은 대용량 파일 전송 또는 일괄 빅데이터 전송을 위한 구현 방법도 있는 것은 바로 구현 방법이 다르기 때문입니다. 제로카피 기술.

전체 IO 프로세스의 흐름은 다음과 같습니다.

1) 프로그래머는 버퍼를 생성하는 코드를 작성합니다(이 버퍼는 사용자 버퍼입니다): 하하. 그런 다음 while 루프에서 read() 메서드를 호출하여 데이터를 읽습니다("syscall read" 시스템 호출 트리거)

byte[] b = new byte[4096];
while((read = inputStream.read(b))>=0) { 
        total = total + read; 
            // other code…. 
        }


2) read() 메서드가 실행되면 어떻게 됩니까? 실제로는 맨 아래에서 발생합니다. 많은 작업이 있습니다.

①커널은 디스크 컨트롤러에 다음과 같은 명령을 보냅니다. 디스크의 특정 디스크 블록에 있는 데이터를 읽고 싶습니다. –kernel은 디스크 컨트롤러 하드웨어에 명령을 내려 디스크에서 데이터를 가져옵니다.

② DMA의 제어 하에 디스크의 데이터를 커널 버퍼로 읽어옵니다. – 디스크 컨트롤러는 DMA

에 의해 데이터를 커널 메모리 버퍼에 직접 씁니다. ③커널은 커널 버퍼의 데이터를 사용자 버퍼에 복사합니다. –kernel은 커널 공간의 임시 버퍼에서 데이터를 복사합니다

여기서 사용자 버퍼는 우리가 작성한 코드에서 new의 byte[] 배열이어야 합니다.

위 단계에서 무엇을 분석할 수 있나요?

ⓐ운영 체제의 경우 JVM은 사용자 모드 공간에 위치한 사용자 프로세스일 뿐입니다. 사용자 공간의 프로세스는 기본 하드웨어를 직접 작동할 수 없습니다. IO 작업에는 디스크와 같은 기본 하드웨어를 작동해야 합니다. 따라서 IO 작업은 커널(인터럽트, 트랩)의 도움으로 완료되어야 합니다. 즉, 사용자 모드에서 커널 모드로 전환됩니다.

ⓑ새로운 byte[] 배열 코드를 작성할 때 일반적으로 "임의로" "모든 크기"의 배열을 만듭니다. 예를 들어 새 바이트[128], 새 바이트[1024], 새 바이트[4096]….

그러나 디스크 블록을 읽는 경우 데이터를 읽기 위해 디스크에 액세스할 때마다 모든 크기의 데이터를 읽는 것은 한 번에 하나의 디스크 블록 또는 여러 디스크 블록을 읽는 것입니다(이는 디스크 작업에 액세스하는 데 드는 비용이 매우 높기 때문이며 우리는 또한 지역성의 원칙을 믿습니다). "중간" 버퍼” – 즉, 커널 버퍼. 먼저 디스크의 데이터를 커널 버퍼로 읽은 다음 커널 버퍼의 데이터를 사용자 버퍼로 이동합니다.

이것이 우리가 항상 첫 번째 읽기 작업이 느리다고 느끼는 이유이지만 후속 읽기 작업은 매우 빠릅니다. 후속 읽기 작업의 경우 읽어야 하는 데이터가 커널 버퍼에 있을 가능성이 높기 때문에 이때 커널 버퍼의 데이터만 사용자 버퍼에 복사하면 되며 기본 데이터는 관련되지 않습니다. 디스크 작업을 읽는 것은 물론 빠릅니다.

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.


데이터를 사용할 수 없는 경우 프로세스가 일시 중지되고 커널이 디스크의 데이터를 커널 버퍼로 가져올 때까지 기다려야 합니다.

그러면 다음과 같이 말할 수 있습니다. DMA는 왜 디스크의 데이터를 사용자 버퍼로 직접 읽지 않습니까? 한편으로는 ⓑ에서 중간버퍼로 언급한 커널버퍼가 있다. 사용자 버퍼의 "임의 크기"와 읽은 각 디스크 블록의 고정 크기를 "맞춤"하는 데 사용됩니다. 반면, 사용자 버퍼는 사용자 모드 공간에 위치하며, DMA 데이터 읽기 작업에는 기본 하드웨어가 포함됩니다. 하드웨어는 일반적으로 사용자 모드 공간에 직접 액세스할 수 없습니다(아마도 OS 때문일 것입니다)

# 🎜 🎜#요약하자면, DMA는 사용자 공간(사용자 버퍼)에 직접 접근할 수 없기 때문에 일반 IO 작업에서는 사용자 버퍼와 커널 버퍼 사이에서 데이터를 앞뒤로 이동해야 하며 이는 특정 프로그램의 IO 속도에 영향을 미칩니다. 해당하는 솔루션이 있나요?

그것은 JAVA NIO에서 언급하는 메모리 매핑 파일인 다이렉트 메모리 매핑 IO, 즉 다이렉트 메모리... 한마디로 비슷한 의미를 표현합니다. 커널 공간 버퍼와 사용자 공간 버퍼는 모두 동일한 물리적 메모리 영역에 매핑됩니다.

주요 기능은 다음과 같습니다.

① 파일을 작동하기 위해 더 이상 읽기 또는 쓰기 시스템 호출이 필요하지 않습니다. — 사용자 프로세스는 파일 데이터를 메모리로 보기 때문에 read() 또는 write() 시스템 호출을 실행할 필요가 없습니다.

②사용자 프로세스가 "메모리 매핑 파일" 주소에 액세스하면 페이지 폴트가 자동으로 발생하고 기본 OS가 저장을 담당합니다. 데이터가 메모리로 전송됩니다. 페이지 저장 관리에 대해서는 메모리 할당 및 메모리 관리에 대한 이해

를 참조하세요.

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???

下面这段话,非常值得一读:这里再一次提到了为什么需要内核缓冲区。

코드 복사
(데이터를 사용자 버퍼로 직접 전송하는 대신
) 중간 커널 버퍼를 사용하는 것이 비효율적으로 보일 수 있습니다. 그러나 성능을 향상시키기 위해 중간 커널 버퍼가
도입되었습니다. 읽기 측에서는 애플리케이션이 커널 버퍼가 보유하는 만큼의 데이터를 요청하지 않은 경우
커널 버퍼가 "미리 읽기 캐시" 역할을 할 수 있습니다.
이는 요청된 데이터 양이 커널 버퍼 크기보다
적을 때 성능을 크게 향상시킵니다. . 쓰기 측의 중간 버퍼를 사용하면 쓰기가 비동기적으로 완료될 수 있습니다.
코드 복사
핵심 포인트는 커널 버퍼가 성능을 향상한다는 것입니다. 뭐? 이상하지 않나요? 왜냐하면 앞서 말했듯이 바로 커널 버퍼(중간 버퍼)의 도입으로 인해 데이터가 앞뒤로 복사되어 효율성이 떨어지기 때문입니다.

커널 버퍼가 성능을 향상시킨다고 하는 이유부터 먼저 살펴보겠습니다.

읽기 작업의 경우 커널 버퍼는 "미리 읽기 캐시"와 동일합니다. 사용자 프로그램이 한 번에 적은 양의 데이터만 읽어야 하는 경우 운영 체제는 먼저 디스크에서 대용량 데이터를 읽습니다. 커널 버퍼. 프로그램은 작은 부분만 가져옵니다(128B 바이트 배열을 새로 만들 수 있습니다! 새 바이트[128]). 사용자 프로그램이 다음에 데이터를 읽을 때 커널 버퍼에서 직접 가져올 수 있으며 운영 체제는 디스크에 다시 액세스할 필요가 없습니다! 사용자가 읽으려는 데이터가 이미 커널 버퍼에 있기 때문입니다! 이는 앞서 언급한 이유이기도 합니다. 후속 읽기 작업(read() 메서드 호출)이 처음보다 확실히 빠른 이유입니다. 이러한 관점에서 커널 버퍼는 읽기 작업의 성능을 향상시킵니다.

쓰기 작업을 살펴보겠습니다. "비동기적으로 쓰기"를 수행할 수 있습니다. 즉, write(dest[])를 수행할 때 사용자 프로그램은 운영 체제에 dest[] 배열의 내용을 XX 파일에 쓰라고 지시하므로 write 메서드가 반환됩니다. 운영 체제는 백그라운드에서 사용자 버퍼(dest[])의 내용을 커널 버퍼에 자동으로 복사한 다음 커널 버퍼의 데이터를 디스크에 씁니다. 그런 다음 커널 버퍼가 가득 차지 않는 한 사용자의 쓰기 작업이 빠르게 반환될 수 있습니다. 이는 비동기 디스크 브러싱 전략이어야 합니다.

(사실 그렇습니다. 과거에 얽힌 문제는 동기식 IO, 비동기식 IO, 차단 IO, 비차단 IO의 차이가 더 이상 의미가 없다는 것이었습니다. 이러한 개념은 문제에 대한 다른 관점일 뿐입니다. 차단 및 비차단은 스레드 자체에 대한 것이며, 동기화 및 비동기는 스레드와 이에 영향을 미치는 외부 이벤트에 대한 것입니다...) [보다 완벽하고 예리한 설명은 다음 기사 시리즈를 참조하십시오. -시스템 통신(3) ——IO 통신 모델과 JAVA 실습 1편】

커널 버퍼가 이렇게 강력하고 완벽하다고 했는데 왜 제로카피가 필요한가요? ? ?

안타깝게도 요청된 데이터의 크기가

커널 버퍼 크기보다 상당히 큰 경우 이 접근 방식 자체가 성능 병목 현상이 될 수 있습니다. 데이터가 최종적으로 복사되기 전에 디스크, 커널 버퍼
및 사용자 버퍼 간에 여러 번 복사됩니다.
Zero copy는 중복된 데이터 복사본을 제거하여 성능을 향상시킵니다.
마지막으로 zerocopy가 등장할 차례입니다. 전송해야 하는 데이터가 커널 버퍼 크기보다 훨씬 크면 커널 버퍼에 병목 현상이 발생합니다. 이것이 제로카피 기술이 대용량 파일 전송에 적합한 이유입니다. 커널 버퍼가 병목 현상을 일으키는 이유는 무엇입니까? —큰 이유는 더 이상 "버퍼" 역할을 할 수 없기 때문이라고 생각합니다. 전송되는 데이터의 양이 너무 많기 때문입니다.

제로카피 기술이 파일 전송을 처리하는 방법을 살펴보겠습니다.

transferTo() 메서드가 호출되면 사용자 모드에서 커널 모드로 전환됩니다. 완료된 작업은 다음과 같습니다. DMA가 디스크의 데이터를 읽기 버퍼로 읽습니다(첫 번째 데이터 복사). 그런 다음 여전히 커널 공간에서 데이터가 읽기 버퍼에서 소켓 버퍼로 복사되고(두 번째 데이터 복사본), 마지막으로 데이터가 소켓 버퍼에서 NIC 버퍼로 복사됩니다(세 번째 데이터 복사본). 그런 다음 커널 모드에서 사용자 모드로 돌아갑니다.

위의 전체 프로세스에는 세 개의 데이터 복사와 두 개의 컨텍스트 전환만 포함됩니다. 데이터 복사본이 하나만 저장되는 느낌입니다. 그러나 사용자 공간 버퍼는 더 이상 여기에 포함되지 않습니다.

세 개의 데이터 복사본 중 단 하나의 복사본에만 CPU 개입이 필요합니다. (두 번째 복사본) 이전의 기존 데이터 복사에는 4번의 복사본이 필요하고 3번의 복사본에는 CPU 개입이 필요합니다.

이것은 개선 사항입니다. 컨텍스트 전환 수를 4개에서 2개로 줄이고 데이터 복사 수를

4개에서 3개로 줄였습니다(그 중 하나만 CPU와 관련됨)

제로 복사 기술만 완료할 수 있는 경우 이 시점에서는 그저 그렇습니다.

기본 네트워크 인터페이스 카드가
gather 작업을 지원하는 경우 커널에 의해 수행되는 데이터 중복을 더 줄일 수 있습니다. #🎜🎜 Linux 커널 2.4 이상에서는 소켓 버퍼 설명자가 수정되었습니다. #이 접근 방식은 다중 컨텍스트 전환을 줄일 뿐만 아니라
CPU 개입이 필요한 중복된 데이터 복사본을 제거합니다.
즉, 기본 네트워크 하드웨어와 운영 체제가 이를 지원하는 경우 데이터 복사본의 수는 더욱 줄어들고 CPU 개입 횟수도 줄어듭니다.

두 개의 복사본과 두 개의 컨텍스트 스위치만 있습니다. 더욱이 이 두 복사본은 DMA 복사본이며 CPU 개입이 필요하지 않습니다. 더 엄격하게 말하면 완전히 필요한 것은 아닙니다.

전체 과정은 다음과 같습니다.

사용자 프로그램은 transferTo() 메서드를 실행하여 시스템 호출이 발생하고 사용자 모드에서 커널 모드로 전환됩니다. 완료된 작업은 다음과 같습니다. DMA는 디스크의 데이터를 읽기 버퍼로 복사합니다

설명자를 사용하여 전송할 데이터의 주소와 길이를 표시하고 DMA는 읽기 버퍼에서 데이터를 직접 전송합니다. NIC 버퍼에 버퍼를 추가합니다. 데이터 복사 프로세스에는 CPU 개입이 필요하지 않습니다.

관련 권장 사항:


Node.js_node.js의 비차단 IO 및 이벤트 루프 요약

Node.js_node.js의 비동기 IO 성능 토론

위 내용은 JAVA IO 및 NIO 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.