공부해요/운영체제

프로세스와 스레드

yenas0 2024. 9. 26. 09:00
반응형

1. 프로세스 개념과 상태 변화

1.1. 프로세스와 프로그램의 차이

  • 프로그램은 하드디스크와 같은 저장 매체에 저장된 실행 파일임. 우리가 컴퓨터에서 실행할 수 있는 파일들(예: .exe 파일, .out 파일) 등이 프로그램임. 이 프로그램은 단순히 저장된 상태로는 동작하지 않음.
  • 프로세스는 프로그램이 메모리에 올라와서 실제로 실행 중인 상태를 의미함. 즉, 프로그램이 실행되면 운영체제가 그 프로그램을 메모리에 올리고, 메모리에 올라간 프로그램을 CPU가 실행하면서 프로세스가 됨.
  • 프로세스는 실행 중인 프로그램에 대한 여러 정보들을 포함함. 예를 들어, 메모리 상태, 실행 중인 명령어, 열려 있는 파일 목록 등을 관리함.
  • 프로세스의 메모리 컨텍스트: 프로세스가 실행될 때 사용되는 메모리는 크게 네 가지로 나눌 수 있음.
    1. 코드 영역: 실행할 명령어가 저장되는 곳임. 프로그램의 코드가 여기에 적재됨.
    2. 데이터 영역: 전역 변수와 같이 프로그램 시작부터 종료까지 지속적으로 사용되는 데이터들이 저장됨.
    3. 힙(Heap) 영역: 프로세스가 동적으로 할당한 메모리가 여기에 저장됨. 프로그램이 실행 중에 메모리를 요청하면 힙 영역에서 할당됨.
    4. 스택(Stack) 영역: 함수 호출 시 사용되는 지역 변수나 함수의 반환 주소 등이 저장됨. 함수가 호출되면 해당 함수에 대한 정보가 스택에 저장되고, 함수가 종료되면 스택에서 제거됨.

1.2. 프로세스 상태 변화

프로세스는 여러 상태를 거치면서 실행됨. 각 상태는 아래와 같음:

  • New (생성): 프로세스가 막 생성된 상태임. 프로그램이 실행되면서 운영체제가 프로세스를 생성하고 메모리에 로드한 후, 아직 실행하지 않은 상태를 의미함.
  • Ready (준비): 프로세스가 실행 준비 상태에 있는 것을 말함. CPU가 할당되기를 기다리는 상태임. 이때 프로세스는 실행할 준비는 다 되어 있지만 CPU를 아직 할당받지 않은 상태임.
  • Running (실행 중): 프로세스가 실제로 CPU를 할당받아 실행되고 있는 상태임. 이 상태에서는 프로세스가 명령어를 처리 중임.
  • Waiting (대기) / Blocked (보류): 어떤 이벤트나 I/O 작업을 기다리고 있는 상태임. 예를 들어, 프로세스가 파일을 읽거나 네트워크로부터 데이터를 받아오는 등의 작업을 요청한 후, 해당 작업이 완료되기를 기다리는 상태임.
  • Terminated (종료): 프로세스가 실행을 마치고 종료된 상태임. 이때 운영체제는 프로세스에 할당된 메모리와 자원을 해제함.

1.3. 프로세스 메모리 구조

프로세스는 가상 메모리라는 개념을 사용함. 가상 메모리는 물리 메모리보다 더 큰 메모리 공간을 제공할 수 있도록 해줌. 프로세스는 가상 메모리 공간에서 동작하며, 각 프로세스는 독립적인 메모리 공간을 가지고 있어서 서로의 메모리 영역을 침범할 수 없음.

  • 코드 영역: 프로그램의 실행 명령어들이 저장됨.
  • 데이터 영역: 초기화된 전역 변수와 정적 변수가 저장됨.
  • 힙 영역: 동적으로 할당된 메모리가 저장됨. 예를 들어, 프로그래머가 malloc() 함수로 메모리를 할당하면 그 메모리가 힙 영역에 할당됨.
  • 스택 영역: 함수 호출 시 사용되는 지역 변수, 함수의 매개변수, 그리고 반환 주소 등이 저장됨. 함수 호출이 끝나면 스택에서 해당 함수의 정보는 사라짐.

 

 

2. 시스템 호출과 API

2.1. 주요 시스템 호출 (System Calls)

  • 시스템 호출은 프로세스가 운영체제의 기능을 사용할 수 있도록 해주는 인터페이스임. 예를 들어, 파일을 읽거나 쓰기 위해서, 또는 새로운 프로세스를 생성하기 위해서 시스템 호출을 사용함.
  • fork(): 새로운 자식 프로세스를 생성하는 시스템 호출임. fork() 호출 시 부모 프로세스는 그대로 유지되면서 새로운 자식 프로세스가 생성됨. 자식 프로세스는 부모 프로세스의 복사본이지만, 독립적인 메모리 공간을 가짐. 즉, 부모와 자식 프로세스는 별개의 프로세스로 동작함. fork()가 성공하면 부모 프로세스에는 자식 프로세스의 PID가 반환되고, 자식 프로세스에는 0이 반환됨.
  • exec(): exec()는 프로세스의 메모리 공간을 새로운 프로그램으로 덮어쓰는 시스템 호출임. 보통 fork()로 자식 프로세스를 만든 후, 자식 프로세스에서 exec()를 호출해 다른 프로그램을 실행함. exec() 계열의 함수는 여러 가지 변형이 있지만, 공통적으로 현재 프로세스의 메모리 내용을 새로운 프로그램으로 대체함.
  • wait(): 부모 프로세스가 자식 프로세스의 종료를 기다리도록 하는 시스템 호출임. 자식 프로세스가 종료되면 부모 프로세스는 자식 프로세스의 종료 상태를 확인하고 계속 실행됨.

2.2. 프로세스 API와 예제 코드

fork() 예제:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]){
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else {
        printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
    }
    return 0;
}

이 코드에서는 fork()를 호출해서 자식 프로세스를 생성하고, 부모와 자식 프로세스가 각각 다른 메시지를 출력함. 자식 프로세스는 0을 반환받고, 부모 프로세스는 자식의 PID를 반환받음.

exec() 예제:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, char *argv[]){
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc == 0) {
        char *myargs[3];
        myargs[0] = strdup("wc");  // "wc" 프로그램 (단어 개수 세는 명령어)
        myargs[1] = strdup("p3.c"); // wc 명령어의 대상 파일
        myargs[2] = NULL; // 배열 끝
        execvp(myargs[0], myargs);  // wc 실행
        printf("이 메시지는 출력되지 않음");
    } else {
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }
    return 0;
}

 

 

 

3. 추가로 알아야 할 내용

3.1. 프로세스 제어 블록 (PCB)

  • **PCB (Process Control Block)**는 운영체제가 각 프로세스의 상태를 관리하는 데 필요한 정보를 담고 있는 데이터 구조임. 프로세스가 생성되면 PCB가 생성되고, 프로세스가 종료되면 PCB가 삭제됨.
  • PCB에는 프로세스의 ID(PID), 프로세스 상태, 레지스터 값, 프로세스에 할당된 메모리 정보, 열려 있는 파일 목록 등이 포함됨. 즉, 운영체제는 PCB를 통해 프로세스를 관리하고 추적함.

3.2. 컨텍스트 스위칭

  • 컨텍스트 스위칭은 CPU가 하나의 프로세스에서 다른 프로세스로 전환될 때 필요한 과정임. CPU는 현재 실행 중인 프로세스의 상태(레지스터 값, 메모리 상태 등)를 PCB에 저장하고, 새로운 프로세스의 PCB에서 상태를 불러와 실행을 이어감. 이 과정에서 프로세스는 CPU를 공유하므로, 효율적인 컨텍스트 스위칭이 중요함.

3.3. 프로세스 생성과 종료

  • 프로세스 생성: 프로세스는 운영체제의 시스템 호출을 통해 생성됨. 대표적인 시스템 호출로는 fork()와 exec()가 있음. fork()는 부모 프로세스가 자식 프로세스를 생성하는데, 이때 자식 프로세스는 부모 프로세스의 복사본으로 만들어짐. 하지만 자식 프로세스는 독립적으로 동작함. 이후 exec()를 통해 자식 프로세스가 새로운 프로그램을 실행하게 만들 수 있음.
  • 프로세스 종료: 프로세스가 할 일을 다 끝내면 exit() 시스템 호출을 통해 종료됨. 프로세스가 종료되면 운영체제는 해당 프로세스의 자원을 회수하고, 부모 프로세스는 wait() 시스템 호출을 통해 자식 프로세스의 종료를 확인함. 만약 부모가 자식 프로세스의 종료를 기다리지 않으면, 자식 프로세스는 좀비 프로세스가 됨.

3.4. 좀비 프로세스와 고아 프로세스

  • 좀비 프로세스: 자식 프로세스가 종료되었지만, 부모 프로세스가 그 종료 상태를 아직 확인하지 않으면 좀비 프로세스가 됨. 좀비 프로세스는 실행은 끝났지만, 여전히 PCB가 남아있는 상태임. 부모 프로세스가 wait()를 호출해서 자식 프로세스의 종료를 확인해야 좀비 상태에서 완전히 제거됨.
  • 고아 프로세스: 부모 프로세스가 먼저 종료되고 자식 프로세스가 남아 있는 상태임. 이때 고아 프로세스는 시스템의 init 프로세스가 대신 관리하게 됨. init 프로세스는 모든 고아 프로세스를 자동으로 수용해서 처리함.

3.5. 프로세스 스케줄링

  • 운영체제는 여러 프로세스가 동시에 실행될 때, 각 프로세스가 CPU를 공유하도록 해야 함. 이때 어느 프로세스가 언제 실행될지 결정하는 역할을 프로세스 스케줄러가 함.
  • 스케줄링 알고리즘:
    1. FCFS (First-Come, First-Served): 먼저 도착한 프로세스가 먼저 실행되는 방식임. 단순하지만, 후반에 도착한 프로세스는 오래 기다려야 할 수 있음.
    2. SJF (Shortest Job First): 실행 시간이 짧은 프로세스부터 실행함. CPU 자원을 효율적으로 사용할 수 있지만, 실행 시간이 긴 프로세스가 오래 기다릴 수 있음.
    3. Round Robin (라운드 로빈): 각 프로세스가 일정 시간 동안 CPU를 할당받아 순서대로 실행됨. 할당 시간이 지나면 다음 프로세스로 넘어가고, 다시 자신의 차례가 오면 실행됨. 공평한 방식이지만, 할당 시간이 너무 길거나 짧으면 효율이 떨어질 수 있음.
    4. Priority Scheduling (우선순위 스케줄링): 우선순위가 높은 프로세스가 먼저 실행됨. 우선순위는 프로세스의 중요도나 긴급성을 기반으로 할당됨. 단, 우선순위가 낮은 프로세스는 실행되지 않고 무한 대기 상태에 빠질 수 있음.

 

 

4. 멀티스레드와 스레드 관리

  • 프로세스는 하나 이상의 스레드로 구성될 수 있음. 스레드는 프로세스 내에서 실행되는 작은 실행 단위임. 예를 들어, 한 프로세스가 여러 작업을 동시에 처리해야 할 때, 각 작업을 다른 스레드로 나누어 병렬적으로 처리할 수 있음.
  • 스레드의 장점: 스레드는 프로세스 내에서 공유 자원을 사용하기 때문에, 같은 메모리 공간을 공유하며 통신 비용이 적음. 또한, 스레드 간의 전환은 프로세스 전환보다 더 빠름.
  • 스레드의 상태 변화: 스레드도 프로세스와 마찬가지로 생성, 실행, 대기, 종료 상태로 변함. 다만 스레드는 프로세스 내에서 실행되므로, 스레드 간의 동기화 문제가 발생할 수 있음. 이를 해결하기 위해 **뮤텍스(Mutex)**나 세마포어(Semaphore) 같은 동기화 도구를 사용함.

4.1. 스레드 생성과 관리

  • 스레드 생성: pthread_create()와 같은 함수로 새로운 스레드를 생성할 수 있음. 각 스레드는 독립적인 실행 경로를 가짐. 스레드를 생성할 때는 실행할 함수와 그 함수에 전달할 매개변수를 함께 넘김.
  • 스레드 종료: 스레드가 할 일을 다 끝내면 pthread_exit()을 호출해서 종료됨. 이때 다른 스레드들이 종료된 스레드를 기다릴 수 있도록 pthread_join()을 사용해 동기화함.

 

 

5. 프로세스와 스레드 관련 질문들

  • Q1: 프로세스와 스레드의 차이점은?
    • 프로세스는 독립적인 실행 단위로, 각각의 메모리 공간을 가짐. 반면, 스레드는 프로세스 내에서 실행되며, 같은 메모리 공간을 공유함. 즉, 프로세스는 서로 간섭할 수 없지만, 스레드는 자원을 공유하기 때문에 더 빠른 통신이 가능하지만 동기화 문제가 발생할 수 있음.
  • Q2: 왜 fork()와 exec()가 분리되어 있을까?
    • fork()와 exec()를 분리함으로써 프로세스 복사와 프로그램 실행을 유연하게 처리할 수 있음. 즉, fork()로 자식 프로세스를 만든 후, 자식 프로세스가 어떤 작업을 수행한 뒤에 exec()를 호출해서 프로그램을 실행할 수 있음. 이를 통해 프로그램 실행 전후에 필요한 작업을 처리할 수 있음.
  • Q3: 좀비 프로세스를 방지하려면 어떻게 해야 할까?
    • 좀비 프로세스는 자식 프로세스가 종료되었지만, 부모 프로세스가 이를 확인하지 않아서 발생하는 문제임. 이를 방지하기 위해 부모 프로세스는 반드시 wait() 시스템 호출을 통해 자식 프로세스의 종료 상태를 확인해야 함. 또는 부모 프로세스가 종료될 때 자동으로 init 프로세스가 좀비 프로세스를 처리하게 할 수 있음.
반응형

'공부해요 > 운영체제' 카테고리의 다른 글

프로세스와 스레드(2)  (1) 2024.10.03
프로세스와 스레드 퀴즈  (0) 2024.09.26
프로그램 수행과 예외처리 및 시스템 콜 퀴즈  (0) 2024.09.19
시스템 콜  (0) 2024.09.19
프로그램 수행과 예외처리  (1) 2024.09.19