CS/운영체제

JVM과 프로세스, 스레드

Ynghan 2024. 3. 31. 23:13
들어가며

프로세스와 스레드에 대한 개념은 개발에 있어 기초이자 필수이다. 그러나 계속해서 제대로 이해되지 않는 부분이 존재하고 또한 JVM과 연관지어 생각해본 경험이 없었다. 이번 포스팅을 통해 JVM과 프로세스가 어떻게 관련지어 동작하는지 알아보자.

 

목차
  • 프로세스와 스레드
  • fork()와 exec()
  • 데몬
  • JVM과 프로세스

 

프로세스란?

프로그램이 CPU를 점유하고 있을 때 프로세스가 된다. OS는 여러 프로세스를 실행하고 관리한다.

프로그램과 프로세스

프로그램은 레시피, 프로세스는 실제 레시피에 대한 요리

 

프로세스 상태

  • new - 프로세스가 생성되는 상태
  • ready - 프로세스가 CPU에 할당되어, 처리되기를 기다리는 상태
  • running - 프로세스가 CPU에 할당되어, 명령어들이 실행되는 상태
  • waiting - 어떤 이벤트의 발생으로 인해 프로세스가 기다리는 상태
  • terminated - 프로세스가 종료되는 상태

 

프로그램이 프로세스가 될때 어떤 일이 일어날까?

메모리 영역

프로세스가 필요로 하는 재료들이 메모리에 올라가야 한다.

하나의 프로세스는 각각 독립된 메모리 영역(Code, Data, Stack, Heap)을 할당 받는다.

Code

코드 영역은 실행할 프로그램의 코드매크로 상수가 기계어 형태로 저장되는 영역

CPU는 코드 영역에 저장된 명령어를 하나씩 처리한다.

Data

데이터 영역은 코드에서 선언한 Global 변수Static 변수가 저장되는 영역

데이터 영역은 프로그램의 시작과 함께 할당되어 종료될 때 소멸된다.

Stack

스택영역은 함수 안에서 선언된 지역변수, 매개변수, 리턴값 등이 저장되고 함수 호출시 기록하고 종료되면 제거한다.

흔히 재귀함수를 통해 너무 많은 함수를 호출하게 되는 경우 스택 영역이 초과하면서 StackOverFlow가 발생한다.

Heap

동적 메모리 할당 영역

 

PCB(Process Control Block)

특정 프로세스에 대한 메타 정보를 포함하는 운영체제 커널의 자료구조

특정 프로세스가 Context Switching이 가능하도록 프로세스에 대한 정보 및 상태를 저장/복원할 수 있다.

Pointer, Process State, Process Number, Program Counter 등에 대한 정보를 가진다.

 

프로세스들은 서로 어떻게 통신할까?

IPC (Inter-process Communication)

프로세스는 통신을 직접적으로 할 수가 없고, 서로의 공간을 접근할 수가 없다. 그래도 프로세스 간의 커뮤니케이션은 필요하기 때문에 나온 기법이라 볼 수 있다. pipe, message queue, shared memory 등이 있다

리눅스는 프로세스 공간이 완전히 분리되어 있다. 리눅스는 모든 프로세스가 커널 공간을 공유하고 커널 공간에서 프로세스간 공유(통신)이 가능하다.


프로세스에 대해 알아보았다. 다음으로 프로세스와 비교되는 개념인 쓰레드를 알아보자.

쓰레드

프로세스를 Context Switching 하는 것은 무거운 작업이다. 쓰레드는 같이 사용하는 자원은 그대로 공유하며 독립된 코드만을 Context Switching하여 경량화 시킨 것이다. 때문에 Context Switching이 일어날 때 캐시 적중률이 올라간다.

운영체제 입장에서 작업의 최소 단위는 Process이고, CPU 입장에서는 Thread가 최소단위이다.

 

TCB(Thread Contol Block)

PCB가 Process의 Context Switching을 가능하도록 하는 것처럼 TCB도 하나의 Thread를 메타 정보를 담아 쓰레드간의 Context Switching을 가능하게 한다. 프로세스의 상태를 관리하는 PCB보다 적은 양의 정보가 담겨있다. Context Switching을 할 때 CPU scheduling을 하는 최소 단위이다.

 

Context Switching

CPU의 코어가 1개라면 동시에 단 하나의 프로세스만 실행이 가능하다. CPU scheduling을 통해서 하나의 CPU를 여러 작업들이 공유할 수 있게 CPU 시간을 나누어 작업을 수행한다.

이때, 프로세서가 지금까지 실행되던 프로세스(A)를 중지하고 다른 프로세스(B)의 PCB 정보를 바탕으로 프로세스(B)를 실행하는 것을 Process Context Switching이라고 한다.

여기서 동일한 프로세스 속에서 하나의 쓰레드(a)를 중지하고 다른 쓰레드(b)의 TCB 정보를 바탕으로 쓰레드(b)를 실행하는 것을 Thread Context Switching의 최소 단위는 TCB이다.


JVM(Java Virtual Machine)

일반 프로그램은 프로그램 → 운영체제 → 하드웨어 순으로 동작한다.
자바 프로그램은 프로그램 → JVM → 운영체제 → 하드웨어 순으로 동작한다.
그러면 프로세스가 생성되는 방식도 다른 프로그램과 조금은 다를 것인데, 이를 알아보도록 하자.

 

JVM 동작 방식

JVM

Java Complier를 통해 Java 코드가 Byte 코드로 변환된다.

개발자가 Run(실행) 하면 JVM이 해당 클래스 파일에 대한 프로세스를 생성한다.

  • OS로부터 Runtime Data Area 메모리를 할당받고, Class Loader가 Class 파일을 메모리에 저장한다.

프로세스가 생성되면 main thread를 생성하고, main thread를 통해 다른 thread들을 생성하며 기능을 수행한다.

 

Runtime Data Area

Runtime Data Area

  • Method Area
    • Class Loader가 Class 파일을 읽어오면, 클래스 정보를 파싱해서 해당 공간에 저장한다.
    • Class 정보, 전역변수, Static 변수 정보가 저장되는 공간이다.
  • Heap
    • 프로그램을 실행하면서 생성한 모든 객체 인스턴스를 저장하는 공간이다.  
    • new 연산자로 생성된 객체, Array와 같은 동적으로 생성된 데이터가 저장되는 공간
    • Reference Type의 데이터가 저장되는 공간
  • Stack
    • 쓰레드마다 하나씩 존재한다.
    • 스택 프레임은 메소드가 호출될 때마다 생성된다.
      • 메소드 실행이 끝나면 pop되어 제거된다.
    • Stack Frame
      • 메서드 호출 시, 생성되어 스택에 push
      • Local variables array
      • Operand Stack
      • Frame Data
        • Constant Pool
        • 이전 스택 프레임에 대한 정보
        • 현재 메서드가 속한 클래스
        • 객체에 대한 참조
    • 지역변수, 메소드의 매개변수 등 일시적으로 필요한 데이터를 저장한다.
  • PC Register
    • 각 쓰레드는 메서드를 실행하고, PC는 그 메서드 안에서 현재 위치(바이트코드의 몇 번째 줄을 실행해야 하는지)를 저장하는 역할을 한다.
    • 쓰레드가 생성되면서 생성됨
  • Native Method Stack 
    • Java가 아닌 다른 언어로 구성된 메소드를 실행이 필요할 때 사용되는 공간

JVM 스레드 종류

데몬 스레드와 비데몬 스레드로 나눌 수 있습니다.

데몬 스레드

  • 데몬 스레드는 다른 비데몬 스레드가 없다면 동작을 중지합니다.
  • JVM의 가비지 컬렉션, 모니터링 등 애플리케이션 운용을 위해 사용하다가 main 스레드가 종료되면 함께 종료되어야 할 경우, 데몬으로 설정하면 유용합니다.

비데몬 스레드

  • 프로그램 실행 진입점인 main thread는 (static void main(String[] args) 비데몬 스레드로 생성되어, 이 스레드가 동작을 중지하거나 종료하면 다른 데몬 스레드들도 함께 중지하게 됩니다.

데몬

데몬이란?

멀티 태스킹 운영체제에서 데몬은 사용자가 직접적으로 제어하지 않고, 백그라운드에서 돌면서 여러 작업을 하는 프로그램을 말한다.

 

UNIX 계열에서의 데몬

: 시스템의 기능을 제공하거나 백그라운드에서 항시 실행되는 프로그램
: 다른 운영체제에서는 ‘시스템 프로세스’라 불림
: 대부분 시스템의 시작과 끝을 함께 함
: 대개 관리자 권한으로 실행되어 네트워크 요청이나 하드웨어 동작 등 여러 기능을 담당하며 다양한 목적으로 사용됨

 

데몬의 특징

  • 부모 프로세스가 없음
  • PPID(부모 프로세스 ID)는 1
    • 시스템의 첫 프로세스인 init의 바로 하위에 위치
    • init은 자바의 Object 클래스처럼 모든 프로세스의 직, 간접 부모인 데몬이며 이것이 없다면 커널 패닉이 발생하게 된다.
  • 데몬은 일반적으로 자식 프로세스를 fork하여 자기 자신을 복사, 생성한 후 자기 자신은 삭제하여 해당 프로세스를 고아 프로세스로 만든 후 이를 init이 자신의 자식 프로세스로 받아들이도록 하는 과정을 통해 만들어지며 이를 ‘fork off and die’라 표현한다.

 

데몬의 동작방식

메일 서버는 메일이 언제 도착하고 나갈 지 모르기에 서버 관리자가 항시 대기할 수는 없는데 메일 서버 프로그램이 데몬으로 항시 실행되고 있다면 원활한 운영이 가능할 것이다.

데몬은 크게 두 가직 방법으로 동작하게 된다.

  1. Standalone, 스탠드얼론
  2. Super daemon(xinetd), 슈퍼 데몬

 

스탠드얼론

위에서 언급한 메일 서버, 웹 서버처럼 혼자서 요청을 받아 처리하는 데몬(httpd 등)을 스탠드얼론이라고 한다. 서버 서비스의 대부분이 스탠드얼론 방식으로 수행되는데 이 방식으로 네트워크 서비스를 수행한다면 클라이언트의 요청이 언제 들어올지 모르는 상태에서 항상 서비스가 가능하고 다른 메커니즘 없이 바로 데몬이 클라이언트의 요청을 처리할 수 있다.

물론 항상 동작하며 메모리를 상주해야 하기 때문에 서버의 메모리를 많이 소모하고 클라이언트의 요청이 들어올 때마다 처리를 위해 새로운 메모리를 소모한다는 점 등의 문제점이 있다. 따라서 클라이언트의 요청이 많지 않은 네트워크 서비스의 경우 스탠드얼론 방식은 비효율적이라 할 수 있다.

스탠드얼론은 /etc/init.d/ 나 /etc/rc/d/init.d/ 에 있는 스크립트 파일로 실행된다. 대표적으로 SSH 서비스를 찾아볼 수 있으며 이는 ‘해당 서비스 절대경로’ + start/stop/restart 나 ‘service 서비스 이름’ + start/stop/restart로 데몬을 시작하거나 멈추는 등 관리할 수 있다.

 

슈퍼 데몬

두 번째 방법인 슈퍼 데몬(xinetd)은 데몬 자기 자신이 직접 서비스를 수행하지 않고 클라이언트 등에서 들어온 요청에 따라 해당 데몬을 실행시키는 방식이다. inetd라는 데몬(현재는 보안상의 이유로 xinetd)이 받은 메시지에 따라 해당되는 데몬을 메모리에 적재함으로써 실행하여 서비스를 처리한다.

필요에 따라 데몬을 메모리에 올리기 때문에 대부분의 경우 xinetd를 사용하는 것이 자원 관리 면에서 이득이 있지만 스탠드얼론에 비해서 상대적으로 처리가 늦기 때문에 클라이언트의 요청에 대해 빠른 네트워크 서비스를 제공하기 위해서는 스탠드얼론 방식이 적합할 것이다.

슈퍼 데몬은 xinetd에 의해 다른 데몬을 실행시키지만 xinetd도 데몬이기 때문에 시스템이 켜질 때 스탠드얼론으로 실행된다.

슈퍼 데몬은 여러 네트워크 서비스를 한 곳에서 관리할 수 있게 해주는 역할을 합니다.

슈퍼 데몬은 여러 서비스의 요청을 감시하고, 해당 요청에 따라 적절한 서비스 데몬을 시작하는 관리자의 역할을 합니다. 이는 시스템 자원을 효율적으로 관리하고, 필요할 때만 서비스 데몬을 시작하여 자원을 절약할 수 있게 해줍니다.


프로세스 생성과 복사 fork()와 exec()

fork()와 exec()는 모두 한 프로세스가 다른 프로세스를 실행시키기 위해 사용하게 된다.

exec에는 execl, execv 등 여러가지 함수군을 가지고 있다. exec의 함수군에 대해서는 아래쪽에서 차이를 간단히 정리하고자 한다.

 

fork(), exec()의 차이점

fork() 시스템 호출은 새로운 프로세스를 위한 메모리를 할당한다는 것이다. 그리고 fork()를 호출한 프로세스를 새로운 공간으로 전부 복사하게 되고, 원래 프로세스는 원래 프로세스대로 작업을 실행하고 fork()를 이용해서 생성된 프로세스도 그 나름대로 fork() 시스템 콜이 수행된 라인의 다음 라인부터 실행이 된다. 새로 생성된 프로세스는 원래의 프로세스와 똑같은 코드를 가지고 있다.

반면, exec()는 fork()처럼 새로운 프로세스를 위한 메모리를 할당하지 않고, exec()를 호출한 프로세스가 아닌 exec()에 의해 호출된 프로세스만 메모리에 남게 된다.

간단히 정리하면, fork()의 결과는 프로세스가 하나 더 생기는 것(PID가 다른 또 하나의 프로세스가 생기는 것).

반면 exec() 실행의 결과로 생성되는 새로운 프로세스는 없고, exec()를 호출한 프로세스의 PID가 그대로 새로운 프로세스에 적용이 되며, exec()를 호출한 프로세스는 새로운 프로세스에 의해 덮어 쓰여지게 된다.

exec로 호출되는 프로그램이 현재 메모리에 올라와 있는 프로그램을 덮어서 로딩된다.

 

 

출처

1. https://www.youtube.com/watch?v=UzaGOXKVhwU

2. https://www.youtube.com/watch?v=1grtWKqTn50

3. https://jaewoo2233.tistory.com/93

4. https://jerry92k.tistory.com/58

5. https://woochan-autobiography.tistory.com/207

6. https://spidyweb.tistory.com/222

7. https://developer-jinnie.tistory.com/28