← All Posts

[pintos]userprograms 개념을 알아보자

스레드에 이어 유저프로그램 흐름까지

Pintos Project 2: 유저 프로그램은 어떻게 실행될까?#

같은 과목을 듣는 분들을 위해 쓴 글입니다. 개념과 그림 위주로 정리했고, 과제 해답 코드는 포함하지 않았습니다.

1. 유저 프로그램이란?#

운영체제를 공부하기 전에는 프로그램을 실행한다는 것이 그냥 실행 파일을 실행하는 과정이라고 생각했었다.

하지만 운영체제 관점에서 프로그램의 실행은 조금 더 복잡하다. 프로그램이 실행되기 위해서는 메모리 공간을 준비해야 하고, 실행에 필요한 인자를 전달해야 하며, 파일이나 메모리 같은 시스템 자원도 관리해야 한다.

Pintos의 User Programs 프로젝트에서는 이러한 과정을 직접 구현하면서 유저 프로그램과 커널이 어떻게 상호작용하는지 학습한다.

수업에서 본 것처럼 운영체제는 하드웨어 위에서 응용 프로그램과 하드웨어 사이를 잇는 계층이다. 응용 프로그램은 하드웨어를 직접 제어하지 않고, 시스템 콜이라는 창구를 통해서만 커널에 요청한다.

유저 프로그램은 사용자 권한으로 실행되는 프로그램이며, 운영체제의 핵심 기능에 직접 접근할 수 없다. 수업에서 배운 사용자 모드와 커널 모드의 구분이 바로 이 제한을 만든다. 파일을 읽거나 프로세스를 생성하는 등의 작업이 필요하면 시스템 콜을 통해 커널에 요청해야 한다.

유저 프로그램은 독립적으로 실행되는 것처럼 보이지만, 실제로는 필요한 순간마다 커널의 도움을 받는다.

유저계층구조


2. 유저 프로그램의 전체 실행 흐름#

유저 프로그램의 실행 과정을 이해하기 위해서는 프로그램이 생성되는 시점부터 종료되는 시점까지의 흐름을 살펴볼 필요가 있다.

  1. 부모 프로세스가 exec로 실행을 요청한다
  2. 커널이 실행 파일을 열고, 메모리 공간과 사용자 스택을 준비한다
  3. 유저 프로그램이 실행되고, 필요할 때 시스템 콜로 커널에 요청한다
  4. 커널 모드에서 요청을 처리하고 결과를 돌려준다
  5. 프로그램이 종료되면 자원을 정리하고 종료 상태를 전달한다
  6. 부모 프로세스가 wait로 종료 상태를 확인한다

이 흐름에서 중요한 점은 프로그램 실행, 시스템 콜 처리, 프로세스 종료가 서로 분리된 작업이 아니라는 것이다.

프로그램이 실행되기 위해서는 실행 환경이 먼저 준비되어야 한다. 실행 중에는 커널에 작업을 요청할 수 있어야 하고, 종료된 뒤에는 자원을 정리하고 필요한 종료 정보를 전달해야 한다.


3. 프로세스 실행: exec#

exec는 새로운 프로그램을 실행하기 위한 과정이다.

프로그램 이름만 전달하면 실행이 끝나는 것이 아니라, 커널은 실행 파일을 확인하고 해당 프로그램이 실행될 수 있는 환경을 준비해야 한다.

이 과정에서 중요한 개념은 다음과 같다.

  • 실행 파일 로딩: 실행 파일의 내용을 메모리에 배치한다.
  • 주소 공간 구성: 프로그램이 사용할 가상 메모리 공간을 준비한다.
  • 사용자 스택 구성: 프로그램 실행에 필요한 인자와 초기 실행 환경을 구성한다.
  • 유저 모드 진입: 준비된 실행 환경에서 프로그램이 실행되도록 한다.

수업에서 본 것처럼 프로그램이 쓰는 주소는 가상 주소이고, 페이지 테이블을 통해 실제 물리 메모리 주소로 변환된다. 그래서 실행 파일을 올릴 때도 메모리 전체를 바로 채울 필요는 없다. 실제로 접근하는 페이지가 생길 때 할당하는 방식이 가능하다.

높은 주소
+----------------+
| 사용자 스택 | ← 함수 호출 정보, 인자
+----------------+
| (빈 공간) |
+----------------+
| 힙 | ← 동적으로 할당되는 영역
+----------------+
| 데이터 영역 | ← 전역 변수 등
+----------------+
| 텍스트 (코드) | ← 실행되는 명령어
+----------------+
낮은 주소

특히 명령행 인자를 전달하는 과정에서는 문자열을 단순히 저장하는 것만으로는 충분하지 않다. 프로그램이 시작될 때 인자에 올바르게 접근할 수 있도록 메모리 배치와 스택 구성을 함께 고려해야 한다.

수업에서 본 스택의 원리도 여기서 그대로 이어진다. 함수 호출 정보와 지역 변수가 스택에 쌓이기 때문에, 프로그램이 시작할 때 인자를 스택 위에 올바른 형태로 놓아 두어야 프로그램의 main이 값을 읽을 수 있다.

이를 통해 프로그램 실행은 단순히 파일을 읽는 작업이 아니라, 실행 가능한 환경을 구성하는 작업이라는 점을 이해할 수 있다.


4. 시스템 콜: 유저 프로그램과 커널의 연결#

유저 프로그램은 파일을 읽거나 쓰고, 프로세스를 종료하는 등의 작업을 수행해야 한다. 하지만 이러한 작업을 직접 처리할 수는 없다.

이때 사용하는 인터페이스가 시스템 콜(System Call) 이다.

시스템 콜은 유저 프로그램이 커널에 특정 작업을 요청할 수 있도록 제공되는 인터페이스다.

예를 들어 파일을 읽는 경우 다음과 같은 흐름으로 동작한다.

시스템콜흐름

시스템 콜 진입은 사용자 모드에서 커널 모드로 바뀌는 순간이다. 수업에서 보았듯이 커널 모드에서만 실행할 수 있는 명령과 자원은 사용자 모드에서 직접 쓸 수 없고, CPU가 권한 수준으로 이를 구분한다. 시스템 콜은 이 권한 전환을 정해진 창구로만 허용하는 장치다.

커널모드전환

수업에서 본 인터럽트 처리도 같은 구조다. 처리가 끝나면 원래 실행하던 위치로 돌아와야 하듯, 시스템 콜이 끝나면 유저 프로그램은 요청한 다음 명령부터 다시 실행된다.

여기서 중요한 것은 시스템 콜을 통해 커널로 진입했다는 사실만으로 요청이 안전해지는 것은 아니라는 점이다.

유저 프로그램은 잘못된 메모리 주소를 전달할 수 있다. 따라서 커널은 유저가 전달한 주소를 검증하고, 안전하게 접근할 수 있는지 확인해야 한다. 수업에서 본 페이지 테이블의 접근 권한 bit가 이 검사의 기반이 된다. 사용자 프로그램이 접근해서는 안 되는 커널 영역이나 권한이 없는 페이지를 가리키면, 커널은 그 요청을 거부해야 한다.

결국 시스템 콜 구현에서는 요청을 처리하는 기능뿐 아니라 커널이 신뢰할 수 없는 유저 입력을 어떻게 다룰 것인지도 중요한 설계 대상이 된다.


5. 파일 디스크립터: 열린 파일을 식별하는 방법#

유저 프로그램이 파일을 열면 이후 읽기나 쓰기 작업에서 해당 파일을 식별할 방법이 필요하다.

이를 위해 사용하는 것이 파일 디스크립터(File Descriptor, FD)다.

파일 디스크립터는 프로세스가 열어 둔 파일 등의 자원을 식별하는 정수형 값이다. 유저 프로그램은 파일 자체의 내부 구조를 직접 다루는 대신, 파일 디스크립터를 이용해 커널에 작업을 요청한다.

수업에서 본 것처럼 스토리지는 블록 단위로 데이터를 저장하고, 파일은 경로 이름과 그 파일을 구성하는 블록 정보로 관리된다. 유저 프로그램이 이 구조를 직접 알 필요가 없도록, 커널은 파일을 하나의 형태로 보여 주고 파일 디스크립터라는 번호로 식별하게 한다.

파일을 사용하는 일반적인 흐름은 다음과 같다.

open()
|
v
파일 열기 성공
|
v
파일 디스크립터 반환
|
+----> read()
|
+----> write()
|
+----> close()

여기서 파일 디스크립터는 파일 자체와 동일한 개념이 아니라는 점을 구분해야 한다. 파일 디스크립터는 프로세스가 열린 파일을 참조하는 데 사용하는 식별자다.

따라서 파일 디스크립터를 관리할 때는 유효한 번호인지, 실제로 열린 파일과 연결되어 있는지 등을 확인해야 한다.

또한 프로세스가 종료될 때는 더 이상 필요하지 않은 파일 자원을 적절히 정리해야 한다.


6. 프로세스 생성과 종료: fork, wait, exit#

유저 프로그램에서는 하나의 프로세스가 다른 프로세스를 생성하거나, 자식 프로세스의 실행 결과를 확인하는 상황도 고려해야 한다.

수업에서 본 것처럼 각 프로세스는 독립된 주소 공간과 자원을 가진다. 그래서 프로세스 사이의 관계를 다룰 때는 “누가 어떤 자원을 소유하는가”를 먼저 정해야 한다.

fork: 자식 프로세스 생성#

fork는 기존 프로세스를 바탕으로 자식 프로세스를 생성하는 기능이다.

부모 프로세스와 자식 프로세스는 서로 다른 실행 흐름을 갖게 된다. 따라서 자식 프로세스가 생성되었다는 사실만으로 모든 초기화가 완료되었다고 볼 수는 없다.

부모 프로세스가 자식 프로세스의 실행 결과나 초기화 상태를 확인해야 하는 경우에는 프로세스 사이의 동기화가 필요하다.

wait: 자식 프로세스의 종료 대기#

wait는 부모 프로세스가 특정 자식 프로세스의 종료를 기다리고 종료 상태를 확인할 수 있도록 하는 기능이다.

여기서 고려해야 할 상황은 다양하다.

  • 아직 실행 중인 자식을 기다리는 경우
  • 이미 종료된 자식의 결과를 확인하는 경우
  • 존재하지 않는 자식의 종료를 기다리는 경우
  • 동일한 자식에 대해 중복으로 기다리는 경우

이러한 상황을 올바르게 처리하려면 부모와 자식 사이에 종료 상태를 전달하고 관리하는 구조가 필요하다.

부모자식

exit: 프로세스 종료#

프로세스가 종료될 때는 단순히 실행을 멈추는 것만으로 충분하지 않다.

열린 파일과 같은 자원을 정리하고, 종료 상태를 기록하며, 부모 프로세스가 필요한 정보를 확인할 수 있도록 해야 한다.

특히 자식 프로세스의 종료와 부모 프로세스의 wait는 서로 연결되어 있다. 자식이 먼저 종료될 수도 있고, 부모가 먼저 기다리기 시작할 수도 있기 때문이다.

따라서 프로세스의 실행 순서가 달라져도 올바르게 동작하도록 상태 관리와 동기화 방법을 설계해야 한다.


7. 구현 과정에서 중요했던 동시성과 자원 관리#

User Programs 프로젝트를 구현하면서 중요하게 생각해야 할 부분은 개별 기능의 동작만이 아니다. 여러 기능이 서로 어떤 관계를 맺고 있는지 함께 고려해야 한다.

예를 들어 자식 프로세스의 종료 상태를 부모에게 전달한다고 생각해 보자.

자식 프로세스가 종료 정보를 기록하기 전에 부모가 해당 정보를 읽으려고 한다면 문제가 발생할 수 있다. 반대로 부모가 기다리지 않는 상황에서도 자식의 종료 정보를 안전하게 관리할 수 있어야 한다.

이 문제는 수업에서 본 저널링과 같은 구조를 가진다. 여러 단계로 나뉜 쓰기 도중에 전원이 나가면 데이터가 중간 상태로 남을 수 있기 때문에, 중간 상태를 누가 어떻게 보게 할지를 설계해야 했다. 종료 정보를 기록하는 중간 단계를 부모가 읽는 상황도 같은 종류의 문제다.

여러단계

이러한 문제는 단순히 조건문을 추가하는 것으로 해결되지 않을 수 있다. 공유 데이터의 생명주기와 접근 시점을 함께 고려해야 하기 때문이다.

또한 자원 정리 과정에서도 주의해야 한다. 파일을 닫거나 종료 상태를 전달하는 과정이 서로 연결되어 있다면, 한 부분의 변경이 다른 부분에 영향을 줄 수 있다.

따라서 기능을 구현할 때는 다음과 같은 질문을 던지는 것이 중요하다.

  • 이 데이터는 누가 소유하는가?
  • 어떤 시점에 생성되고 해제되는가?
  • 여러 실행 흐름이 동시에 접근할 수 있는가?
  • 작업이 실패하면 어떤 상태로 남는가?
  • 부모와 자식의 실행 순서가 달라져도 올바르게 동작하는가?

이러한 질문은 특정 테스트 케이스를 통과하는 것뿐 아니라 전체 시스템의 안정성을 생각하는 데 도움이 된다.


마무리#

Pintos의 User Programs 프로젝트를 통해 프로그램 실행이 단순히 실행 파일을 불러오는 작업이 아니라는 점을 배울 수 있었다.

프로세스를 실행하기 위해서는 메모리와 스택을 준비해야 하고, 실행 중에는 시스템 콜을 통해 커널과 상호작용해야 한다. 또한 파일 디스크립터를 통해 자원을 관리하고, 프로세스가 종료될 때는 다른 프로세스와 종료 상태를 주고받아야 한다.

수업에서 강조한 것처럼 추상화는 세부 사항을 숨겨 프로그램을 쉽게 만들지만, 커널을 구현하는 사람은 그 세부 사항을 정확히 알아야 한다. 이번 과제에서는 유저 프로그램이 보는 단순한 인터페이스 뒤에서 커널이 주소 공간, 권한, 자원을 함께 관리하고 있었다.

무엇보다 개별 기능이 정상적으로 동작하는 것만큼이나 기능 사이의 관계를 이해하는 것이 중요했다.

exec, fork, wait, exit는 각각 독립적인 기능처럼 보이지만 실제로는 프로세스의 생성부터 종료까지 하나의 흐름으로 연결되어 있다.

이번 프로젝트는 기능을 구현하는 방법뿐 아니라 실행 흐름, 자원의 소유권, 프로세스 간 동기화, 예외 상황까지 고려하며 시스템을 설계하는 방법을 배울 수 있는 과정이었다.


참고#