시스템 콜과 자바에서의 시스템 콜 사용례
Last updated
Last updated
참고 링크
시스템 콜은 사용자 프로세스가 커널 프로세스에게 어떠한 문맥을 요청하면서 발생하는 것이다.
이를 알기 전에 약간의 운영체제 지식이 필요하니 운영체제에 대해서 조금 훑고 지나가자.
운영 체제의 목적은 다음과 같다.
사용자가 편리하게 컴퓨터 시스템을 사용할 수 있는 환경을 제공
컴퓨터 시스템 안의 하드웨어를 효율적으로 관리하기 위함
그렇다면 어떻게 편리한 사용 환경을 만들고, 하드웨어를 효율적으로 관리할까?
오늘날 대부분의 운영체제는 시분할 시스템이다. 컴퓨터 내에서 돌아가는 수 많은 프로세스들은 운영체제 스케줄링에 의해서 자원을 할당받아 실행된다.
스케줄링을 통해 할당 받는 시간 단위가 매우 짧기 때문에, 여러 프로그램을 실행해도, 동시에 실행되는 것 처럼 느껴진다.
즉, 프로세스는 운영체제 위에서 실행 중인 프로그램이라고 볼 수 있다. 당연히 이러한 환경이다보니 어떤 프로세스의 자원 처리나 하드웨어 작업 등의 처리가 나날이 복잡해졌다.
우리가 C 언어를 사용할 때, 어떠한 메모리를 할당받았으면 반드시 프로그래머는 해당 구문이 필요가 없어지면 free()
를 사용해 자원을 반납해주어야 했다.
하지만, Java 와 같은 언어들은 GC 를 지원하면서 자원 반납에 대한 프로그래머의 부담을 해소시켜주었다.
운영체제도 우리에게 Java 와 같은 편리함을 제공해준다고 생각하면 될 것 같다. (우리는 컴퓨터를 사용하면서 프로세스 메모리에 대한 고민을 하지 않는다..)
여기서 하나의 궁금증이 생길 수 있는데, C 언어에서 메모리 할당을 받는 것처럼 운영체제도 그와 같은 기능을 하지 않을까?
운영체제는 크게 2가지 모드로 프로세스를 동작시킨다. (더 세분화된 모드들도 있다)
사용자 모드 (User mode)
사용자 모드가 사용하는 대부분의 프로그램들이 동작하는 모드
커널 모드(Kernel mode)
커널 모드는 운영체제 내부의 커널이 관리하는 프로세스 모드
커널 모드를 통해 외부의 접근을 최소화해야하는 영역(보안)을 지정해두고 보안성을 높였다.
사용자 모드가 커널 영역에 접근하는 것이 아니라, 운영체제에게 요청을 하면 해당 처리를 운영체제에 위임해 처리하도록 하는데, 이 명령을 바로 시스템 콜이라고 한다.
우리가 C 를 사용하면서 malloc()
같은 명령어를 수행하면 내부적으로 시스템 콜이 발생해서 프로세스는 커널 모드로 변경되고 운영체제에게 이 요청을 위임한다.
운영체제는 해당 명령어를 해석하고 할당해서 완료가 되면 프로세스에게 알려주고 다시 프로세스는 사용자 모드로 변경된다.
중요한 부분은 단순하게 메모리 용량 할당 뿐 아니라, I/O 작업이나 네트워크 작업 등 커널 영역이 필요한 모든 곳에서는 시스템 콜이 필요하다.
즉, 우리가 사용하는 프로세스는 수 없이 많이 사용자모드와 커널모드를 왔다갔다 하면서 작업을 수행하는 것이다.
그렇다면 자바에서 시스템 콜이 가장 자주 일어나는 I/O 에 대해서 알아보자.
자바는 기본적으로 JVM 이라는 가상화 머신을 사용해 동작한다. 그렇기 때문에, C 프로세스보다 한 단계를 더 거쳐 운영체제에 접근하게 된다.
핵심은 C 프로세스의 경우 시스템 콜을 직접 사용할 수 있지만, 자바의 경우에는 간접적으로 사용할 수 있다는 것이다.
만약 C 로 I/O 를 한다면 아래와 같은 흐름으로 시스템 콜이 발생할 것이다.
C 프로세스 -> 시스템 콜 -> 커널 -> 디스크 컨트롤러 -> 데이터 복사
자바는 아래와 같은 흐름으로 시스템 콜이 발생한다.
JVM -> 시스템 콜 -> 커널 -> 디스크 컨트롤러 -> 커널 버퍼 복사 -> JVM 버퍼 복사
때문에, C 프로세스에 비해서 Java 프로세스의 IO 처리 속도가 느릴 수 밖에 없다..
자바 혹은 다른 언어를 사용하더라도, 결국은 시스템 콜을 사용하는 I/O 는 느릴 수 밖에 없다. 그래서 운영체제는 I/O 속도 향상을 위한 기술들을 제공하는데 다음과 같다.
버퍼 (Buffer)
Scatter/Gather
가상메모리 (Virtual Memory)
메모리 맵 파일
파일 락
버퍼를 설명하기 전에 앞서 시스템 콜 영역을 좀 더 세부적으로 그려보면 다음과 같다.
유저 영역과 커널 영역에서 버퍼를 사용하는 모습을 볼 수 있다. DMA, Disk Controller 는 운영체제 내요이니 패스~
버퍼는 무엇이고, 왜 사용해야 할까?
여러번 반복적으로 전달하는 것 보다. 중간에 버퍼에 값을 쌓아두었다가 일정량이 모이면 한번에 전달하는 것이 효율적이다.
I/O 비용은 비싸다..
버퍼는 효율적으로 데이터를 전달하는 객체이다. 따라서 데이터를 전송하는 곳에서 대부분 버퍼를 사용하는데 운영체제도 예외적인 아니다.
버퍼를 사용하고 안하고의 속도 차이를 보고 싶으면 다음 글을 참고하자. 차이가 상당하다;; I/O 기본1
만약 내가 버퍼를 N 개를 만들어서 사용하는데, 동시에 I/O 작업이 이뤄진다고 가정해보자. 그렇다면 N 번의 시스템 콜이 일어날 수 있다고 추론할 수 있다.
시스템 콜은 컨텍스트 스위칭과 비교해서 상대적으로 낮지만 여전히 성능에 영향을 줄 수 있다.
이렇게 N 번의 시스템 콜을 요청하는 경우 당연히 비효율적이라고 볼 수 있다. 이러한 문제 때문에 운영체제는 Scatter 와 Gather 를 제공해준다.
Scatter 와 Gather 의 흐름은 아래 그림과 같다. 기존과 달리 버퍼에 대한 메타데이터(주소, 크기 등) 를 포함하는 구조체를 사용하여, 시스템 콜(시스템 콜이 1번만 발생한다) 시 이 정보를 함께 전달한다.
메타데이터를 통해서 주어진 버퍼들을 순차적으로 읽거나 쓴다.
자바에서는 이런 기능을 이용하기 위해서 java.nio.channel
패키지에 ScatteringByteChannel
과 GatheringByteChannel
을 제공해준다.
I/O 관점에서 가상메모리를 사용함으로 얻는 장점은 다음과 같다.
실제 물리 메모리 크기보다 큰 가상 메모리 공간 사용 가능
여러 개의 가상 주소가 하나의 물리적 메모리 주소를 참조함으로써 메모리를 유연하게 사용 가능
가상 메모리를 사용하면 2개의 버퍼를 사용하더라도 뒤에서 볼 메모리 맵 파일을 통해서 동일한 영역에 접근이 가능해진다.
따라서, 커널 영역 -> 유저 영역으로 데이터를 복사하지 않아도 된다.
위에서 가상메모리를 설명할 때 유저 가상 메모리와 커널 가상 메모리가 매핑되려면 메모리 맵 파일을 사용한다고 했는데, 이번에 메모리 맵 파일에 대해 살펴보자.
우리가 인텔리제이(인텔리제이는 자바를 통해 만들어졌다) 를 통해서 코드를 입력하게 되면 I/O 시스템 콜이 발생할 것이다. 그리고 입력된 값을 다시 버퍼에 옮기는 작업이 이뤄질 것이고, 복사를 한 후에 가비지가 생기고 이를 또 가비지 콜렉터가 처리할 것이다.
가비지 콜렉터가 가비지를 수거하는 것은 상당히 느린 작업이고, 많은 기업들이 GC 튜닝하는데 공을 들이는 이유일 것이다.
이러한 문제점을 해결하기 위해 운영체제에서 지원하는 것이 MMIO(Memory-mapped I/O) 이다.
파일 매핑
메모리 맵 파일을 사용할 때, 프로그램은 파일을 특정 메모리 주소에 매핑한다. 즉, 파일의 내용을 특정 주소 영역과 연결하여, 파일 데이터를 메모리처럼 접근할 수 있게 만든다.
메모리에 매핑된 파일은 프로그램의 메모리처럼 접근할 수 있어서, 파일을 읽거나 쓰는 동작을 메모리 조작만으로 처리할 수 있다.
가상 메모리와 페이지 관리
메모리 맵 파일은 가상 메모리를 사용하여 페이지 단위로 파일을 매핑한다. 필요할 때만 해당 페이지를 실제 메모리에 로드하는 방식으로 온디맨드 로딩(demand paging) 을 한다.
파일의 특정 부분에 접근할 대만 운영체제가 페이지 단위로 메모리에 올리므로, 모든 파일을 한꺼번에 메모리에 올릴 필요가 없다.
동기화
파일을 변경할 때, 수정된 내용은 메모리에 쓰여지고, 운영체제가 적절한 시점에 디스크로 플러시한다.
MappedByteBuffer
의 force()
메서드를 사용해 명시적으로 디스크에 기록할 수도 있지만, 기본적으로는 운영체제가 자동으로 변경된 데이터를 파일에 반영합니다.
I/O 시스템 콜 감소
메모리 맵 파일을 사용하면 파일을 메모리처럼 직접 조작하기 때문에, 파일을 읽고 쓸 때마다 시스템 콜을 호출할 필요가 없다.
보통 파일 I/O 는 read()
, write()
메서드에서 시스템 콜을 반복해서 사용하지만, 메모리 맵 파일은 단 한번의 매핑하는 시스템 콜 만으로 메모리에 파일을 연결하여 효율을 높인다.
페이지 폴트 기반 온디맨드 로딩
메모리 맵 파일은 필요할 때만 페이지 단위로 메모리에 올리는 방식이므로, 필요한 부분만 메모리에 로드됩니다. 특히 대형 파일을 처리할 때 유용하며, 전체 파일을 읽는 대신 특정 부분만 메모리에 올려 효율을 극대화할 수 있습니다.
디스크와 메모리 간 복사 제거
일반적인 파일 I/O는 파일 데이터를 커널 버퍼에서 사용자 버퍼로 복사하는 과정이 필요하지만, 메모리 맵 파일은 파일을 메모리에 매핑하여 복사 과정을 생략합니다.
이로 인해, I/O 작업이 CPU 메모리 접근처럼 이루어지므로 데이터 복사에 드는 시간이 절약됩니다.
원래 자바 1.4 이전에는 파일락 기능을 제공하지 않았다. 이 부분도 운영체제의 기능 중 하나였기 때문이다.
또한 파일락은 프로세스들의 접근 자체를 제한하거나, 접근하는 방법에 제한을 두어야 했어서 JVM 에서 처리가 불가능했다.
NIO 패키지에서 이러한 파일 락 기능을 제공하기 시작했다.
java.nio.channels.FileChannel.lock()