[CS/OS] 스터디 week9

2rlokr·2025년 6월 1일

cs-knowledge

목록 보기
8/12
post-thumbnail

⚙️ 시스템 콜 (System Call)

시스템 콜이란, 운영체제의 커널이 제공하는 서비스에 대해, 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스이다. 시스템 콜은 사용자 프로그램이 운영체제(OS)의 기능을 사용할 수 있도록 해준다.

  • 각 시스템 콜에는 번호가 할당되고, 시스템 콜 인터페이스는 시스템 콜 번호 + 시스템 콜 핸들러 함수 주소로 구성되는 시스템 콜 테이블을 유지한다.
  • 운영체제는 자신의 커널 영역에서 해당 인덱스가 가리키는 주소에 저장되어 있는 루틴을 수행한다.
  • 작업이 완료되면 CPU에게 인터럽트를 발생시켜 수행이 완료되었음을 알린다.

🗒️ 시스템 콜 예시

사용자 행동시스템 콜 이름설명
파일 열기open()파일 시스템에 접근해서 파일을 염
파일 읽기read()파일에서 데이터를 읽어옴
새 프로세스 만들기fork()새로운 프로세스 생성
다른 프로그램 실행exec()다른 프로그램으로 실행 코드 바꿈
종료하기exit()현재 프로그램 종료
기다리기wait()자식 프로세스가 끝날 때까지 대기

🗂️ 시스템 콜의 유형

프로세스 제어

  • 프로세스 생성, 종료, 대기 등
  • 메모리에 로드, 실행
  • 프로세스 속성 값 확인, 지정
  • wait 이벤트, signal 이벤트
  • 메모리 할당
  • EX) fork(), wait(), exec(), exit()

파일 매니지먼트

  • 파일 생성, 파일 삭제
  • 파일 열기, 닫기 및 읽기, 쓰기
  • EX) open(), read(), write(), close()

디바이스 매니지먼트

  • 하드웨어 입출력 요청
  • EX) ioctl(), read(), wrtie()

정보 유지/관리

  • 시간 확인, 시간 지정
  • 시스템 데이터 확인, 지정
  • 프로세스, 파일, 디바이스 속성 가져오기 및 설정하기
  • EX) getpid(), alarm()

통신

  • 프로세스 간 데이터 교환
  • 커뮤니케이션 연결 생성 및 삭제
  • 메시지 송신, 수신
  • 상태 정보 전달
  • remote 디바이스 해제 및 장착
  • EX) pipe(), shmget(), send(), recv()

↪️ 시스템 콜 실행 과정

✏️ C언어에서 printf을 사용하는 예시

...
movl 5, %eax
int $0x80

1️⃣ 컴파일 단계

  1. 사용자 응용 프로그램에서 printf을 사용한다.
    -> printf()는 내부적으로 표준 출력을 통해 문자열을 출력하기 위해 write() 시스템콜을 사용한다.
  • 사용자 프로그램이 시스템 콜을 호출 (read(), write()등)
  1. 매개변수가 있다면 매개변수들을 저장한다.
    -> write 안에 들어가는 함수의 인자들이 레지스터나 스택에 저장된다.
  2. 트랩이 발동되기 전에 어떤 시스템 콜 때문에 트랩이 발동됐는지 알려주기 위한 정보를 기록한다.
    ->write 시스템 콜 함수에 대응하는 sys_call_table의 index인 4를 EAX 레지스터에 저장한다.
  • 운영체제가 제공한 시스템 콜 번호를 사용 → 이 번호로 어떤 기능인지 구분한다.
  1. int $0x80 같이 인터럽트 트랩을 발동하는 시스템콜을 호출하여 커널 모드로 전환한다.
  • 특별한 명령어(trap)를 사용해 커널 모드로 전환

2️⃣ 런타임 단계

  1. 운영체제가 요청을 처리한다.
  • sys_call_Table에서 EAX 레지스터에 저장된 번호를 읽어 어떤 시스템 콜인지 확인한다.
  1. 운영체제는 커널 내부의 sys_write() 함수를 실행한다.
  • 커널 모드에서 요청 처리
  1. 실행 결과가 EAX에 저장되어 사용자 프로그램에 반환된다.
  2. 커널 모드 -> 유저 모드로 돌아간다.

🔢 시스템 콜 번호

커널은 시스템 콜 번호(system call number)로 시스템콜을 구분할 수 있다.

  • read=0, write = 1 처럼 각 시스템 콜마다 고유한 번호가 있다.
  • 유저 프로그램이 시스템 콜을 호출할 때, 이 번호를 함께 전달하고, 커널은 이 번호를 확인하고 어떤 시스템콜 요청인지 알고 처리할 수 있다.

❓ 그렇다면 시스템 콜은 왜 필요할까?

시스템 콜이 필요한 이유

우리가 일반적으로 사용하는 프로그램은 '응용 프로그램'이다. 유저 레벨의 프로그램은 유저레벨의 함수들만으로는 많은 기능을 구현하기 힘들기 때문에, 커널(kernal)의 도움을 반드시 받아야 한다. 이러한 작업은 응용 프로그램으로 대표되는 유저 프로세스 (User Process)에서 유저모드로는 수행할 수 없다.

반드시 ! kernal에 관련된 것은 커널 모드로 전환한 후에야, 해당 작업을 수행할 권한이 생긴다.

권한은 왜 필요할까?

  • 권한이 없을 때, 해커가 피해를 입히기 위해 악의적으로 시스템 콜을 사용할 수도 있다.
  • 초보 사용자가 하드웨어 명령어를 잘 몰라서 아무렇게나 함수를 호출했을 경우, 시스템에게 치명적일 수 있다.

-> 이러한 명령어들은 특별하게 커널 모드에서만 실행할 수 있도록 설계되었고, 만약 유저 모드에서 시스템 콜을 호출할 경우에는 OS에서 불법적인 접근이라 여기고 trap을 발생시킨다.

🔁 Dual Mode(이중 모드)

운영체제는 보안을 위해 2가지 모드를 제공한다.

  • 유저 모드(User Mode) : 일반 프로그램이 실행되는 모드
  • 커널 모드(Kernal Mode) : 운영체제가 실행되는 모드 (모든 자원에 접근 가능)
  • CPU에는 현재 어떤 모드인지 나타내는 모드 비트(Mode Bit)가 있다.
    • 커널 모드 : 0
    • 유저 모드 : 1
  • 시스템 콜이 실행되면, 유저모드 -> 커널모드로 전환되어 운영체제가 요청을 처리할 수 있게 된다.

👩‍💻 유저 모드
PC Register가 사용자 프로그램이 올라가 있는 메모리 위치를 가리키고 있을 때는 현재 사용자 프로그램의 명령어를 수행중이라고 여기고 CPU가 유저모드에서 수행중이라고 말한다.

💻 커널 모드
PC Register가 운영체제가 존재하는 부분을 가리키고 있다면 현재 운영체제의 코드를 실행중이라고 여기고, CPU가 커널 모드에서 수행중이라고 말한다.

서로 다른 시스템 콜을 어떻게 구분할 수 있을까요?,


⚡️ 인터럽트

인터럽트(Interrupt)란, 컴퓨터 시스템에서 발생하는 중단 신호로, CPU가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치나 예외 상황이 발생하여 처리가 필요할 경우에 잠시 프로그램을 중단하고 발생한 일을 처리한 후, 다시 실행 중인 작업으로 돌아오는 것을 말한다.

🗂️ 인터럽트 종류

하드웨어 인터럽트

  • 하드웨어 장치에서 발생하는 인터럽트로, 주변 장치와의 상호작용을 관리한다.
  • 예를 들어, 키보드나 마우스와 같은 입력 장치, 타이머나 외부 신호에 의한 인터럽트가 여기에 해당한다.

소프트웨어 인터럽트

  • 소프트웨어 명령에 의해 발생하는 인터럽트로, 프로그램의 명령에 따라 명시적으로 발생시킬 수 있다.
  • 주로 시스템콜을 위항 int명령, 프로세스 간 통신이나 예외 처리에서 사용된다.

예외 인터럽트

  • 프로세서가 실행 중에 예외 상황을 감지할 때 발생하는 인터럽트이다.
  • 프로그램 에러, 메모리 오류, 연산 오버플로우 등과 같은 예외 상황을 처리하기 위해 사용된다.

외부 인터럽트

  • 외부 장치나 외부 시스템에서 발생하는 인터럽트이다.
  • 주로 다른 컴퓨터 시스템과의 통신에서 사용된다.

HW 인터럽트 & SW 인터럽트

1️⃣ 하드웨어 인터럽트

CPU 외부로부터 인터럽트 요구 신호에 의해 발생되는 인터럽트로, Maskable Interrupt, Non-maskable Interrupt 가 있다.

Maskable Interrupt

  • Interrupt Mask가 가능하다.

Non Maskable Interrupt

  • Interrupt Mask가 불가능하다.
  • 거부하거나 무시할 수 없다.
  • 정전, 하드웨어 고장 등의 문제로 발생하는 어쩔 수 없는 오류에 의해 발생한다.

하드웨어 인터럽트 종류

  • 입출력 인터럽트 (I/O interrupt)
    • 입출력 작업의 종료나 입출력 오류에 의해 CPU의 기능이 요청됨
  • 정전,전원 이상 인터럽트(Power fail interrupt)
    • 전원 공급의 이상
  • 기계 착오 인터럽트(Machine check interrupt)
    • CPU의 기능적인 오류
  • 외부 신호 인터럽트(External interrupt)
    • I/O 장치가 아닌 오퍼레이터나 타이머에 의해 의도적으로 프로그램이 중단된 경우

2️⃣ 소프트웨어 인터럽트

CPU 내부에서 실행한 명령이나 CPU의 명령 실행에 관련된 모듈이 변화하는 경우 발생한다. trap 또는 exception 이라고도 한다.

소프트웨어 인터럽트 종류

  • 프로그램 검사 인터럽트 (Program Check Interrupt)
    • 0으로 나누는 경우
    • OverFlow/UnderFlow
    • 페이지 부재
  • SVC (Supervisor Call: 감시 프로그램 호출) 인터럽트
    • 사용자가 프로그램을 실행시키거나 supervisor을 호출하는 동작을 수행하는 경우
    • 프로그래머에 의해 코드로 짜인 감시 프로그램을 호출하는 방식

🧑‍🔧 인터럽트 처리 과정

1. ⚡️ 인터럽트 요청 발생 (Interrupt Request, IRQ)

  • 예) 키보드를 누름, 하드디스크에서 데이터 읽기 완료, 0으로 나누기 등
  • 이 신호는 Interrupt Controller (PIC/APIC)을 통해 CPU에 전달된다.

2. 진행중인 프로그램의 수행을 중단한다.

  • 원자성을 보장하기 위해 현재 수행중인 명령의 수행이 완료된 시점에서 중단한다.

3. 문맥 저장 (Context Save) : CPU는 중단된 프로그램이 다시 이어서 실행할 수 있도록 다음 정보를 스택(Stack)에 저장한다.

  • 만약, 인터럽트로 Context Switching이 발생한다면, 커널 스택에 저장한 상태를 PCB에 복사해 저장하여 새로운 프로세스의 PCB에서 정보를 꺼내 복원하도록 한다.
  • 저장하는 정보들
    • PC (Program Counter) : 다음에 실행할 명령어 주소
    • 레지스터 값들 : 계산 도중의 변수들
    • 상태 레지스터 : 조건/플래그 정보

4. 인터럽트 전처리 실행

  • 인터럽트 벡터 번호로 어떤 장치에서, 어떤 이유로 인터럽트가 발생했는지 확인한다.
  • 인터럽트 벡터 테이블에서 해당 인터럽트의 서비스 루틴 주소를 찾는다.

5. 인터럽트 서비스 루틴(ISR) 실행 (인터럽트 핸들러고도 부른다.)

  • 인터럽트를 실제로 처리하는 코드가 실행된다.
  • 프로그램의 상태를 안전한 곳에 기억시켜 보존한다.
  • 예) 키보드 인터럽트 -> 어떤 키를 눌렀는지 확인하고, 버퍼에 저장
    디스크 인터럽트 -> 읽은 데이터를 메모리로 복사

6. 인터럽트 처리가 끝나면, 스택에 저장해둔 정보(PC, 레지스터 등)를 다시 복원한다.

  • 다시 중단된 시점으로 점프해서 프로그램을 이어서 계속 실행한다.

💡 관련 용어

인터럽트 핸들러 (인터럽트 서비스 루틴, ISR)
인터럽트가 발생했을 때, 그 인터럽트를 처리하기 위해 운영체제가 미리 정해놓은 함수/코드 블록

인터럽트 벡터
인터럽트가 발생했을 때, 어떤 핸들러(ISR)를 실행할지 주소를 정리해놓은 테이블

PCB (Process Control Block)
운영체제가 하나의 프로세스를 관리하기 위해 저장해두는 정보 묶음

🔢 인터럽트 우선순위

여러 장치에서 인터럽트가 동시에 발생하거나, 인터럽트 서비스 루틴(ISR) 수행 중에 인터럽트가 발생했을 경우, 우선순위를 따져서 처리한다.


  • 일반적으로 하드웨어 인터럽트가 소프트웨어 인터럽트보다 우선순위가 높다.
  • 또한, 내부 인터럽트보다 외부 인터럽트가 우선순위가 높다.

📚 인터럽트 순서 처리 방식

인터럽트 마스킹 (Interrupt Masking)

  • 어떤 인터럽트를 잠시 막아둘 수 있다.

중첩 처리 (Nested Interrupt)

  • 인터럽트 처리 중에 더 우선순위가 높은 인터럽트가 오면, 현재 인터럽트도 중단하고 더 급한 것을 먼저 처리하는 방식이다.

⏲️ Polling 방식

CPU가 주기적으로 특정 장치나 상태를 확인하여 이벤트가 발생했는지 직접 체크하는 방식으로, 장치 상태를 확인하는 루프를 실행하며 이벤트가 발생했을 때 작업을 처리한다. -> 반복 검사 기반 방식 (Loop-based)

  • 구현이 간단하고, 인터럽트 없이 작동이 가능하다.
  • 짧고 일정한 간격으로 장치를 확인해야 할 때는, 인터럽트보다 안정적으로 작동한다.
  • ⚠️ 다만, 이벤트가 자주 발생하지 않으면 불필요하게 루프를 실행하므로 CPU 자원이 낭비되고 주기가 있으므로 이벤트 감지에 지연이 발생할 수 있다.

🌱 Spring 에서 Polling 방식 구현

@Component
public class PollingService {

    @Scheduled(fixedRate = 5000) // 5초마다 실행
    public void pollExternalService() {
        System.out.println("Checking external service...");
        // 외부 서비스 상태 확인 로직 추가
    }
}
  • Spring의 @Scheduled를 활용해 Polling 방식으로 데이터를 주기적으로 가져오도록 구현할 수 있다.
  • SpringBoot 애플리케이션에서 @EnableScheduling을 추가하여 스케쥴링을 활성화한다.

🔄 Polling vs Interrupt

항목PollingInterrupt
CPU 방식직접 계속 확인알림이 올 때만 처리
CPU 효율낭비 심함효율적 (쓸데없는 반복 없음)
사용 예시간단한 장치, 타이머실시간 반응 필요한 장치 (키보드 등)

🖥️ 프로세스

프로세스는 실행 중인 프로그램을 의미한다. 프로세스는 운영체제에 의해 관리되며, 독립적으로 실행되고 자원을 할당 받을 수 있는 단위이다. 운영체제는 프로세스들에게 적절히 자원들을 분배하여 여러가지 작업을 수행할 수 있게 한다.

  • 독립된 메모리 공간을 가진다. (코드/데이터/스택/힙)
  • 운영체제에 의해 관리된다. (PID, 상태 등)
  • 최소 1개 이상의 스레드를 포함한다.

❓ 프로그램과 프로세스, 스레드의 차이는 뭘까?

구분정의메모리독립성
프로그램실행되지 않은 정적 파일없음N/A
프로세스실행 중인 프로그램독립된 전체 메모리다른 프로세스와 독립
스레드프로세스 내부의 실행 단위코드/데이터 공유, 스택은 개별같은 프로세스 내 다른 스레드와 협력 가능

🔅 프로세스의 상태

1. 프로세스 생성 (new)

  • 프로세스가 생성된 상태로, 생성만 되었고, 아직 실행되기 위한 자원을 할당받지 못한 상태이다.

2. 실행 가능(Ready)

  • 프로세스가 실행을 기다리는 상태로, 프로세스가 실행되기 위해 필요한 자원을 모두 할당받았으며, 실행을 위한 준비가 완료되었지만, CPU를 할당받지 못한 상태이다.
  • 이 상태에서는 CPU를 할당받기 위해 스케줄링 대기열(Queue)에 들어가게 된다.

3. 실행 상태(Running)

  • 프로세스가 CPU를 할당받아 실제로 코드를 실행하는 상태이다.
  • 프로세스가 작업을 처리하고, 결과를 만들어낸다.

4. 대기 (Blocked)

  • 프로세스 처리 중에 작업 시간이 초과되거나 자원 사용을 위해 대기해야 하는 이벤트가 발생하여 프로세스가 잠시 멈춘 상태이다.
  • 이 상태에서는 CPU를 사용하지 않으며, 특정 자원을 사용할 수 있을 때까지 실행을 멈추고, 다시 대기열(Ready Queue)로 들어가게 되며, 프로세스 처리가 가능한 상태가 되면 실행(Running) 상태로 변경된다.

5. 종료(Terminated, exit)

  • 프로세스의 실행이 완료되어 종료된 상태이다. 이 상태에서는 할당된 자원이 해제되고, 프로세스의 메모리 공간은 운영체제에 반환된다.

ℹ️ PCB (Process Control Block)

운영체제가 프로세스를 관리하기 위해 사용하는 모든 정보(데이터)이다.

  • PCB는 각 프로세스마다 유지되며, 해당 프로세스의 상태 정보와 제어 정보를 저장한다.
  • 프로세스가 생성되면 OS는 PCB를 할당하고, 프로세스가 종료되면 해당 PCB를 해제한다.

프로그램이 실행되면 프로세스의 정보를 저장하는 별도의 공간이 따로 생기게 된다. 운영체제의 커널도 하나의 프로그램이기 때문에 프로세스와 같이 정보를 저장할 수 있는 공간 (stack, data, heap, ...)이 생성된다. 이 때, 커널의 데이터(Data) 영역에서는 각 프로세스의 상태, CPU 사용 정보, 메모리 사용 정보 등 각종 자원을 관리하기 위해 PCB라는 공간을 둔다.

🗒️ PCB에 저장되는 정보

1️⃣ 운영체제가 관리상 사용하는 정보

1. 프로세스 상태

  • 프로세스의 현재 상태를 나타낸다.
  • 예를 들면, 실행(Running), 준비(Ready), 대기(Waiting) 등의 상태가 있다.
  • 이 상태 정보는 프로세스 스케줄링과 상호작용하여 프로세스의 실행을 관리한다.

2. 프로세스 ID (Process ID)

  • 운영체제가 각 프로세스를 고유하게 식별하기 위해 부여하는 번호

3. 스케줄링 정보 (Scheduling Information)

  • 프로세스의 우선순위, 할당된 CPU시간, 스케줄링 알고리즘과 관련된 정보 등 스케줄링에 필요한 정보를 포함한다.

4. 우선순위

  • 운영체제가 어떤 프로세스를 먼저 실행할지 결정할 때 참고하는 값으로, 우선순위가 높을수록 CPU를 먼저 차지할 가능성이 높다.

2️⃣ CPU 수행 관련 하드웨어 값

1. 프로그램 카운터 (Program Counter, PC)

  • 프로세스가 다음에 실행할 명령어(코드)의 주소를 가리키는 포인터이다.
  • PC는 프로세스가 중단되었을 때 다시 시작할 명령어 주소를 저장하고 있다.

2. 레지스터

  • 프로세스가 인터럽트 이후 올바르게 작업을 이어나가기 위해 참조하는 CPU 레지스터 값

3️⃣ 메모리 관련

1. 메모리 관리 정보

  • 프로세스가 사용하는 메모리 공간의 주소 범위, 페이지 테이블, 메모리 할당 정보 등과 같이 메모리 관리에 필요한 정보를 저장한다.

4️⃣ 파일 관련

1. 입출력 상태

  • 프로세스가 현재 사용 중인 입출력 장치와 관련된 정보를 포함한다. 예를 들어, 어떤 입출력 요청을 보내고 있는지, 어떤 파일을 열어두었는지 등의 정보를 저장한다.

❓ 그렇다면, 스레드는 PCB를 갖고 있을까?

스레드는 PCB를 가지지 않는다 ! 대신 TCB(Thread Control Block)를 가진다.

TCB는 운영체제가 스레드를 관리하기 위해 사용하는 자료구조로, 프로세스를 관리하기 위해 PCB 가 존재하는 것처럼, 스레드를 관리하기 위해 운영체제가 TCB를 만들어서 관리한다.

스레드는 프로세스 내에서 메모리를 공유하기 때문에, PCB와 같은 독립적인 메모리 정보는 필요하지 않다 !

🍴 리눅스에서 프로세스, 스레드 생성

1. 프로세스 생성

pid_t pid = fork();
  • fork() 명령어로 프로세스를 생성한다.
  • 현재 프로세스를 복제하여 새로운 자식 프로세스를 생성하는 방식이다.
  • 부모와 자식은 완전히 독립된 메모리 공간을 가진다.

2. 스레드 생성

pthread_create(&tid, NULL, func, NULL);
  • clone() 또는 pthread_create()로 스레드를 생성한다.
  • 새로운 실행 단위를 만들되, 메모리 공간은 공유하게 된다.

🧟‍♀️ 부모 프로세스 / 자식 프로세스 사망 시?

자식이 먼저 죽는 경우

  • 자식은 좀비(zombie)상태가 된다.
  • 부모가 wait() 시스템 콜로 자식 종료 상태를 수거하면 소멸된다.
    • wait() 또는 waitpid()를 호출하면, 자식의 종료 상태를 확인하고, 좀비 프로세스가 완전히 정리되도록 한다. (리소스를 회수한다.)
    • 자식 프로세스의 종료를 부모 프로세스가 감지하는 데에는 비동기, 동기적 방법이 있다.
  • 만약, 부모가 wait()를 호출하지 않으면, 커널은 종료된 자식의 정보를 PCB 형태로 보존하고, 자식의 부모는 init 프로세스(보통 PID 1)로 변경된다. 이 경우 init 프로세스가 자동으로 wait()를 호출해서 좀비를 수거해줌.
    • 하지만, 부모가 wait()를 안 하면 수거까지 지연되고, 그 사이에 좀비들이 PCB를 차지하게 되니까 시스템 자원이 낭비된다.

부모가 먼저 죽는 경우

  • 자식은 고아 프로세스(Orphan)가 된다.
  • 리눅스에서의 init 프로세스(PID 1)가 고아 자식의 새 부모가 된다.
  • 나중에 자식이 죽으면, init프로세스가 대신 wait()를 호출해서 자식 프로세스가 정리된다.

📖 프로세스 구분

프로세스는 눈에 보이는 프로세스(foreground process)와 눈에 보이지 않은 프로세스(background process) 로 구분된다.

👀 포그라운드 프로세스

  • 사용자와 상호작용하는 프로세스
  • 최소화를 하더라도 눈에 보이는 프로세스를 포그라운드 프로세스
  • 인터넷, 카카오톡, 메모장, 그림판 등 눈에 보이는 프로그램 

🙄 백그라운드 프로세스

  • 프로세스는 사용자와 상호작용하지 않고, 시스템에서 실행되는 프로세스
  • 눈에 보이지 않지만 뒤에서 일을 하고 있는 프로세스를 백그라운드 프로세스
  • 백신 프로그램, 그래픽 드라이버, 마이크 드라이버 등 눈에 보이지 않는 프로그램

😈 데몬 프로세스(Daemon)란?

백그라운드에서 계속 실행되며, 사용자가 직접적으로 제어하지 않고 백그라운드에서 돌면서 여러 작업을 하는 프로그램을 말한다.

  • 사용자의 요청을 기다리고 있다가 요청이 발생하면 이에 적절히 대응하는 리스너와 같은 역할을 한다.
    즉, 메모리에 상주하면서 특정 요청이 오면 즉시 대응 할 수 있도록 대기중인 프로세스를 말한다. 

🌳 리눅스의 프로세스 트리

리눅스에서 모든 프로세스가 부모-자식 구조(Tree)로 연결된다.

트리의 루트 : PID 1번, init 또는 systemd

  • 시스템 부팅 시 제일 먼저 실행된다.
  • 다른 모든 프로세스들의 조상이며, 고아 프로세스를 Adopt하는 역할도 한다.

❓ fork()로 자식 프로세스를 만들지 않아도 부모-자식 트리로 관리할까?

fork()를 직접 호출하지 않아도, 운영체제가 프로세스를 만들 때는 내부적으로 fork() 또는 clone() 같은 시스템콜을 자동으로 사용하여 처리하기 때문에 항상 부모 프로세스가 존재한다.

운영체제는 계층 구조(Tree)로 프로세스를 관리하기 때문에, 프로세스를 생성하려면, 항상 이미 존재하는 다른 프로세스를 통해서 만들어야 한다. 즉, 부모가 없는 프로세스는 PID 1번 하나 뿐이다.

💿 프로세스 주소공간

운영체제가 해당 프로세스에 할당해 준 메모리 공간으로, 하나의 프로세스는 자신만의 고유한 가상 메모리 공간을 가지고 있으며, 다른 프로세스와 격리되어 있다.

프로세스 구조

프로세스를 실행시키기 위해서는 코드의 데이터를 메모리에 올려 실행시켜야 한다. 이 때, 프로세스마다 고유한 가상 메모리 공간을 제공하며, 이 공간은 4개로 나눌 수 있다.

1. code 영역 (Text Segment)

  • 작성한 코드가 저장되는 공간으로, 코드는 컴파일되어 0과 1로 변환된 기계어로 저장된다.

2. Data 영역 (Data Segment)

  • 코드에서 선언된 전역 변수, 정적 변수, 상수 등을 저장한다.
  • 초기화된 변수와 초기화되지 않은 변수들이 나눠서 저장된다.
    • 초기값이 있는 전역 변수, 정적 변수는 Data 영역에 저장된다.
    • 초기값이 없는 전역 변수, 정적 변수는 BSS(Blocked Started By Symbol) 영역에 저장된다.
  • 데이터 영역은 프로그램의 시작 시 초기화되며, 프로세스가 종료될 때까지 유지된다.

3. Stack

  • 지역변수, 매개변수, return 주소들을 저장한다.
  • 프로세스마다 독립적인 stack을 가질 수 있고, stack 포인터를 통해 스택의 관리를 관리한다.
  • 함수가 호출되면 stack 공간이 생성되며, 함수가 종료되면 제거된다.

4. Heap

  • 코드에서 동적으로 생성되는 데이터 구조나 객체들을 저장한다.
  • 힙은 동적으로 할당되는 메모리 영역으로, 프로세스가 실행 중에 동적으로 메모리를 할당받고 해제하는데 사용된다.
  • 힙은 프로세스의 주소 공간의 나머지 영역에 위치하며, 크기는 동적으로 확장될 수 있다.

❓ 공간을 이렇게 분할한 이유 ?

✅ 서로 반대 방향으로 자라게 하면 충돌 없이 유연하게 메모리 사용이 가능하기 때문이다.

  • 스택은 함수 호출 시 자동 생성/소멸되며, 위에서 아래로 자란다.
  • 힙은 개발자가 직접 메모리 할당/해제하며, 아래에서 위로 자란다.

프로세스 주소 공간

❓ Stack 와 Heap

📦 크기 관점

  • Stack의 크기는 미리 정해진 크기 내에서 유동적으로 사용된다.
  • Heap의 크기는 동적으로 결정된다.

stack

  • Stack의 크기는 운영체제가 프로그램 시작 시 기본으로 설정해둔 크기로 컴파일 타임에 결정된다.
  • ulimit -s 명령어나 운영체제 설정으로 사전에 설정할 수 있다.
  • 즉, 전체 크기는 고정되어 있고, 내부 사용만 유동적이다.

heap

  • Heap의 크기는 운영체제가 초기 최소값으로 시작한다.
  • 이후, 필요할 때마다 malloc(), `new 같은 함수로 운영체제에 요청하여 동적으로 늘릴 수 있다.
  • 즉, 런타임에 동적으로 크기가 결정된다.

스택과 힙의 크기가 너무 크면, stack overflow, out of memory 등의 문제가 발생한다.

🏎️ 접근 속도 관점

  • Stack이 더 빠르다.

stack

  • CPU에 의해 효율적으로 관리되고, 메모리 단편화가 적게 일어나기 때문에 접근이 매우 빠르다.
  • 메모리의 연속된 공간을 사용하며, 함수 호출/리턴 시 주소 계산이 단순하고 빠르다.

heap

  • 메모리 관리자(allocator)가 메모리 할당 및 해제를 담당하며, 할당된 메모리의 위치가 동적으로 결정된다. 이로 인해 힙 영역에 접근하려면 메모리 관리자를 통해 할당된 메모리의 주소를 찾아가야 하므로 접근 속도가 느리다.

🧱 자료구조 관점

stack

  • LIFO(Last-In-First-Out) 원칙에 따라 메모리의 함수 호출과 관련된 변수, 함수 호출 정보, 복귀 주소 등을 입출력하는 측면에서 자료구조의 스택과 관련이 있다.

heap

  • 프로그램의 실행 중에 사용자에 의해 동적으로 할당되고 해제되는 방식으로 객체를 관리하는 특징이 있으며, 자료구조 힙이랑은 관련이 없다.

🗣️ IPC (Inter-Process Communication)

하나의 컴퓨터 안에서 실행 중인 서로 다른 프로세스 간에 발생하는 통신

공유 메모리 방식 (Shared Memory)

공유 메모리 방식은 프로세스들이 주소 공간의 일부를 공유하는 방식이다.

원칙적으로는 서로 다른 프로세스는 각자 독립적인 주소 공간을 가지므로 자신이 아닌 다른 프로세스의 메모리에 대한 접근이 불가하다.

하지만 ! 운영체제는 공유 메모리를 사용하는 시스템 콜을 지원해서 서로 다른 프로세스들이 그들이 주소 공간 중 일부를 공유할 수 있도록 하고 있다.

  • 프로세스 주소 공간 중 Data 영역에 할당되어 메모리 주소를 직접 공유하는 방식으로 데이터를 교환할 수 있게 해준다.
  • IPC의 기본 방식은 커널을 통해 데이터를 전달하는 반면 (Message Passing), 공유 메모리에 읽고 쓰기 때문에 데이터 전달이 필요없어 빠른 데이터 공유가 가능하며 효율적이다.
    즉, 중개자 없이 곧바로 메모리에 접근할 수 있기 때문에 빠르게 작동할 수 있다.
  • 여러 프로세스에 공유 메모리 주소만 공유하게 되면 접근이 가능하기 때문에 유연성이 좋다.
  • 여러 프로세스가 동시에 접근하여 수정할 수 있기 때문에 동기화 문제, 공유 메모리의 낭비가 발생할 수 있다.

0개의 댓글