기존 IO  2가지 효율적인 부분



1. 커널 영역 버퍼에서 프로세스 영역 안의 버퍼로 데이터를 복사

[디스크]에서 [커널 영역 버퍼] 데이터를 저장하는 것은 디스크 컨트롤러가 DMA 기술을 사용하기 때문에 CPU 사용 하지 않는다. 하지만 [커널 영역]에서 [프로세스 영역 버퍼]으로 데이터를 전달하는 것은 CPU 사용한다. 만약 커널 영역의 버퍼에 저장된 데이터를 직접 사용한다면 복사하는 시간을 단축 있고, 복사 대상인 데이터의 가비지 컬렉션도 필요 없다. 그리고 CPU자원도 최소화 있다.

 

2. 디스크 컨트롤러에서 커널 영역의 버퍼로 데이터를 복사하는 동안 프로세스 영역의 블록킹

운영체제는 효율을 높이기 위해 최대한 많은 양의 데이터를 커널 영역의 버퍼에 저장한 프로세스 영역의 버퍼로 전달한다. 따라서 디스크의 파일 데이터를 커널 영역 안의 버퍼로 모두 복사할 까지 자바 프로세스가 블록킹 된다. 정확하게는 파일 읽기를 요청한 자바 스레드가 블록킹 된다.

 

요약하면  OS에서 관리하는 커널 버퍼에 직접 접근할 없고, 기본의 IO Blocking I/O여서 매우 비효율적이었다는 것이다. 커널에서 JVM내부 메모리(사용자 프로세스 영역) 복사한는 의미는 디스크에서 커널로 데이터를 읽어들이는 것은 CPU 개입없이 DMA 처리해주지만 커널에서 사용자 프로세스 내부로 데이터를 복사하는 것은 CPU 포함한 많은 Resource 사용 댓가가 필요하다. 복사용으로 사용된  Buffer 활용 GC 대상이 되므로 이것 또한 부담으로 남는다.

 


NIO에서 도입된 3가지 기술


버퍼(Buffer)

NIO에서 Buffer클래스를 도입했다. 1.4이전 버전에서 모든 메모리는 JVM 영역을 통해서 관리했다. 결국, IO 과정에서 항상 비효율적인 보사 과장을 거치고, 블록킹이 된다. 하지만 1.4 부터는 시스템 메모리를 직접 사용할 있는 Buffer 클래스가 도입 되었다(물론, DirectByteBuffer 한정된 것이지만).

 

채널(Channel)

NIO에서는 스트림의 향상된 버전인 Channel 도입했다. 채널은 스트림처럼 읽거나 쓰는 단방향에서부터, 읽고 쓰는 양방향 통신이 가능한 세가지 형식이 존재한다. ㄸㅎ한 운영체제에서 제공해주는 다양한 네이티브IO 서비스들을 이용할 있게 해준다.

 

셀럭터(Selector)

셀럭터는 네트워크 프로그래밍의 효율을 높이기 위한 것이다. 클라이언트 하나당 스레드 하나를 생성해서 처리해야 했는데, 사용자가 늘어나면 스레드가 많이 생성됨으로 인해 급격한 성능 저하를 가져왔다. 따라서 셀렉터는 하나의 스레드에서 다수의 동시 사용자를 처리할 있는 기술이다.

 

 

NIO에서는 커널 버퍼에 직접 접근할 있는 클래스를 제공해주는데. Buffer클래스들이 그것인데, 내부적으로 커널버퍼를 직접 참조하도록 하고있다. 운영체제가 제공해주는 효율적인 I/O 핸들링 서비스를 이용 있게 해주며 위에서 발생한 복사문제로 인해 CPU자원의 비효율성, I/O 요청 Thread Blocking 되는 문제점 등이 해결될 있다.

 

 


그림을 살펴보면 Buffer에는 여러가지 자료형을 지원하는데Direct Buffer ByteBuffer 지원한다. 따라서 커널 버퍼를 직접 사용하고 싶다면 불편하더라도 ByteBuffer 사용해야 한다. Buffer 만드는 방법은 다음과 같다.

1. ByteBuffer buf = ByteBuffer.allocate(SIZE);

2. ByteBuffer directBuf = ByteBuffer.allocateDirect(SIZE);

  

아랫줄의 코드와 같이 사용하여야 커널 버퍼를 직접 이용하는 것이며. 줄은 기존 방식과 같은 것으로  JVM내부에 메모리가 할당 된다.



ByteBuffer


ByteBuffer 중요한 속성

 

1. pos

pos 현재 버퍼 포인터의 위치이고, lim 버퍼에 저장할수 있는 최대 , 이라고 생각하면 된다. cap 원래 수용할 있는 최대 공간이다. pos put 통해 데이터를 입력할때마다 데이터 개수만큼 뒤로 가서 새로은 pos 가르키게 된다.

예를 들어 put(4바이트) 하게되면 포스는 4 가르키게 된다.

 

2. lim

lim 현재 입력받을 있는 최대 buffer 양이다. buffer.limit() 을통해 현재 limit값을 리턴받을수도있고 buffer.limit(int num) 통해 원하는 값으로 lim 수정할 수도 있다.

lim 초과하는 put 들어오면 에러를 뿜어낸다. 현재의 버퍼 크기라고 생각하면 된다.

 

3. cap

버퍼의 절대적인 수치이다. 처음 선언시 만들어 주는 크기이며 변화 시킬 없는 값이다. lim역시 당연히 cap 크기를 넘어 설수는 없다.

 


ByteBuffer 중요한 Method


1. clear() 

해당 ByteBuffer instance 생성되었을 상태로 만들어 준다.

 

2. limit() 

쓰기 가능 영역 크기를 조정할 있다. 파라미터로 넘기는 값이 allocate 지정한 capacity 초과할 없다


3. flip() 

filp 메서드를 사용하게 되면 lim = pos 되고 pos= 0 된다. 이것이 어떤 의미를 가지게 되냐면 버퍼 내용을 프린터할때 lim 까지 프린터 할수도 있고, 그리고 get 통해 byteArray 옮기게 될때 pos부터 lim까지의 양을 (유효한 ) 옮길 있다.

 

filp 보통 쓰기와, 읽기를 오갈때 써준다고 생각하면 된다. 쓰고 나면 flip -> 읽기 하는 방식으로 사용한다.

 

4. put*() 

put* 메소드들을 이용해서 데이터를 ByteBuffer 넣어준다.

 

5. get*() 

원하는 데이터 크기만큼 데이터를 읽을 있다. get* 메소드를 이용해서 데이터를 읽으면 다음 get* 함수 이용시 이전 get* 메소드를 이용해 읽은 다음 부분부터 값을 읽는다.

 

6. put*() 

값을 쓰거나 SocketChannel read 함수로 ByteBuffer instance 값을 쓰고 나서 데이터를 읽기 전에 ByteBuffer 클래스의 flip 함수를 한번 호출해야 한다.

 

7. compact() 

compact메소드 socket 통신의 경우 buffer 읽을 있는 양이 때까지 read 함수로 buffer 채우고 buffer 충분히 차면 의미있는 범위까지 데이터를 읽고 buffer 남은 부분을 유지하고 남은 부분 다음부터 데이터를 채우는 작업을 반복하는 경우가 있다.

이때 데이터의 남은 부분을 buffer 앞으로 이동시키고 남은 부분 다음부터 있게 하는 메소드가가 compact 메소드이다


Posted by Steven J.S Min
,