Operating System Concepts - 운영체제부터 프로그램이 실행되기까지
컴퓨터에서 프로그램이 실행되기까지 과정을 정리해보고자 한다. 개발자 입장에서 직접 작성한 고급언어 코드 프로그램이 어떻게 작동하게 되는지 알아보자. 운영체제별 차이점까지 깊게 다루진 않았다. 특히 Java 개발자 측면에서 찾아보며 정리해보았다.
프로그램 실행 과정 전체 개요
C/C++ 실행 과정
1
소스 코드(.c, .cpp) -> 컴파일 -> 링킹 -> 실행 파일 -> 로딩 -> 프로세스 생성 -> 실행 -> 종료
Java 실행 과정
1
소스코드(.java) → javac → 바이트코드(.class) → JVM 로딩 → JVM 프로세스 → 바이트코드 실행
1. 소스 코드 작성 및 컴파일
개발자는 C, C++, Java, Python 등 **고급 프로그래밍 언어로 사람이 이해할 수 있는 형태의 소스 코드를 작성한다.
이 소스코드는 컴파일러 에 의해 기계어 or 중간 언어로 번역된다. 컴파일 과정에서 코드 최적화, 오류 검사 등이 수행된다. 컴파일 오류가 발생하지 않으면 실행 가능한 프로그램 (실행 파일) 이 생성된다.
Java의 경우 바이트코드와 같은 중간 형태로 컴파일되어, 가상 머신에서 실행될 수 있다.
2. 링킹
컴파일된 코드는 필요한 라이브러리나 다른 모듈과 함께 링킹된다.
- 정적 링킹 : 컴파일 시간에 외부 코드나 라이브러리를 실행 파일에 포함시킴
- 동적 링킹 : 프로그램이 실행될 때 필요한 코드를 불러오는 방식
최종적으로, 링킹 과정을 거쳐 완전한 실행 파일이 완성된다.
3. 실행 파일 로딩
사용자가 프로그램을 실행시키면, 운영 시스템은 실행 파일을 메모리로 로드한다.
이 과정에서 운영 시스템은 파일 시스템에서 실행 파일을 찾아 메모리에 적재한다.
4. 프로세스 생성
메모리에 로드 된 실행 파일은 프로세스(실행중인 프로그램의 인스턴스)로 생성된다. 프로세스는 고유 메모리 공간(code, data, stack 등) 과 운영 시스템 자원(파일 핸들, 스레드 등) 을 할당받는다.
운영 시스템은 프로세스를 관리하고, CPU 스케줄링을 통해 프로세스가 실행될 수 있도록 한다.
5. 실행
프로세스가 CPU 시간을 할당받으면, 프로세스의 코드가 실행된다. 이때 명령어 실행 사이클 (Fetch, Decode, Execute 등) 이 반복되며, 프로그램의 로직에 따라 처리가 수행된다.
프로그램 실행중에는 메모리 접근, 입출력 작업, 네트워크 통신 등 다양한 시스템 호출이 이루어질 수 있다.
6. 종료
프로그램이 완료되면 운영 시스템은 프로세스를 종료시키고 사용했던 자원을 회수한다. 프로세스 종료는 정상 종료, 사용자에 의한 강제 종료, 오류로 인한 비정상 종료 등 여러 방식이 있다.
Java 예시를 통한 자세한 과정
int res = a + b;
라는 java 코드가 어떻게 실행되는지 살펴보자.
컴파일 단계 (javac)
1. 소스코드 -> 바이트 코드
1
2
3
4
5
6
7
8
public class APlusB {
public static void main(String[] args) {
int a = 5;
int b = 3;
int res = a + b;
System.out.println(res);
}
}
javac 컴파일러가 .java 파일을 .class 파일(바이트코드)로 변환한다.
이 바이트코드는 특정 OS에 종속되지 않는 중간 코드로, JVM만 있으면 어디서든 실행 가능하다.
1
javac APlusB.java # APlusB.class 생성
2. JVM 프로세스 시작
1
java APlusB
java 명령어를 실행하면 JVM이라는 프로세스가 OS에서 시작된다. JVM 자체는 C/C++로 만들어진 네이티브 프로그램이므로 일반적인 프로세스 생성 과정을 거친다.
3. 클래스 로딩 - JVM 내부의 과정
JVM 내부에서 클래스 로더가 3단계 작업
-
로딩(Loading)
- 첫번째로 로딩 단계는 .class 파일을 읽어와 JVM 내부에서 사용할 수 있는 자료구조인 메모리에 적재한다.
- 읽어온 .class 파일은 바이트코드(Bytecode) 형태로 변환된다.
-
링크(Linking)
- 링크 단계는 클래스가 메모리에 로딩된 후에 실행되는 단계다. 클래스 파일의 정보를 분석하여 해당 클래스가 참조하고 있는 다른 클래스, 메서드, 변수 등의 레퍼런스(참조)를 연결하는 과정이다.
- 링크단계는 검증(Verification), 준비(Preparation), 해석(Resolution) 세 가지 단계로 나뉜다.
- 검증 : 로딩된 클래스 파일이 올바른 자바 클래스 파일인지 검증하는 과정
- 준비 : 클래스가 필요로 하는 메모리 공간을 할당
- 해석 : 클래스의 상수 풀(constant pool)에서 필요한 심볼릭 참조(symbolic reference)를 실제 메모리상의 레퍼런스로 교체하는 과정
1
2
3
4
5
6
7
8
9
10
- 상수 풀:
클래스 파일 내부에 있는 상수들을 모아 놓은 것
클래스 파일 내부에서 사용되는 모든 상수들이 저장되어 있다.
- 심볼릭 참조:
클래스나 인터페이스의 이름, 필드의 이름, 메서드의 이름 등을 나타내는 것입니다.
일종의 자바 코드 상의 식별자 (예시 APlusB 같은것)
-
초기화
-
초기화 단계는 클래스의 정적변수(static variable)와 클래스의 정적블록(static block)이 초기화 되는 단계다.
- 정적변수는 클래스가 로딩되는 과정에서 메모리에 할당된다. 이 변수들은 초기화 전에 기본값으로 초기화 된다.
- 정수형 변수는 0, boolean 타입의 변수는 false
- 정적블록은 클래스가 로딩될 때 실행되는 코드 블록이다. 이 블록에서는 클래스의 정적 변수를 초기화하거나, 클래스의 정적 메소드를 호출하거나, 예외 처리 등의 작업을 수행할 수 있다.
-
4. 바이트코드 실행 - 인터프리터 + JIT
JVM 실행엔진이 바이트코드를 실행하는 두 가지 방식:
- 인터프리터 방식: 바이트코드를 한 줄씩 해석해서 실행
1
int res = a + b; → iload_1, iload_2, iadd, istore_3
- JIT 컴파일러: 자주 실행되는 코드를 기계어로 컴파일해서 캐시에 저장
1 2 3 4
// 반복문 같은 자주 실행되는 코드 for(int i = 0; i < 1000000; i++) { res = a + b; // JIT 컴파일 대상 }
5. 메모리 구조 - JVM 내부 영역들
JVM 프로세스 내부의 여러 메모리 영역:
JVM 프로세스
├── Method Area
: 클래스 정보, static 변수
├── Heap
: new로 생성한 객체들
├── Stack
: 지역변수, 메서드 호출 정보
└── PC Register
: 현재 실행 중인 명령어 위치
1
2
3
4
5
6
7
8
9
@RestController
public class UserController { // Method Area에 클래스 정보
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { // Stack에 id 변수
User user = new User(id); // Heap에 User 객체 생성
return user;
}
}
6. GC와 메모리 관리
Java의 가장 큰 특징은 자동 메모리 관리
C/C++
1
2
3
4
5
char* buffer = malloc(1024); // 수동 할당(size_t size; 입력 인자로 필요한 형식의 메모리 크기)
...
free(buffer); // 수동 해제 (없으면 메모리 누수)
Java
1
2
3
4
5
List<String> list = new ArrayList<>(); // 자동 할당
...
// 자동으로 GC가 메모리 해제 (개발자가 신경 쓸 필요 없음)