본문 바로가기
CS/운영체제

운영체제 스터디 2주차 - 프로세스의 이해

by 데브겸 2023. 9. 26.

3. 1 Process Concept

프로세스(Process)란

프로세스란 실행 중인 프로그램. 스토리지에 있는 프로그램(instruction set)를 메모리에 로드하고, CPU가 fetch하여 execute한 것을 프로세스라고 부른다. (이때 메모리에 로드된 프로그램을 프로세스, program in execution이라고 한다) OS는 프로세스를 태스크의 기본 단위로 생각하며, CPU time, memory, files, I/O device 리소스를 효율적으로 사용하도록 돕는 역할을 수행.

 


프로세스의 메모리 구조

프로그램이 메모리에 로드 되어 프로세스가 되면 왼쪽 같은 메모리 구조를 가짐

- 코드 영역: 실행 코드가 저장 (CPU가 여기에서 명령을 가져와 처리)

- 데이터 영역: 전역 변수, 정적 변수

    - .data: 초기화된 변수 저장

    - .bss: 초기화되지 않은 변수 저장

- 스택 영역:

    - 함수를 호출할 때 임시 데이터 저장장소

    - 함수 지역변수, 매개변수, 복귀 주소 등

    - 컴파일 타임에 크기가 결정됨

- 힙 영역:

    - 프로그램 실행 중에 동적으로 할당되는 메모리

    - 런타임시 크기가 결정됨

    - malloc, new, ...

 

 

 

힙 메모리와 스택 메모리와 관련해서는 아래 자료들을 더 참고해보자

https://dsnight.tistory.com/50

 

[C] 스택(Stack), 힙(Heap), 데이터(Data)영역

C언어의 메모리 구조 프로그램을 실행시키면 운영체제는 우리가 실행시킨 프로그램을 위해 메모리 공간을 할당해준다. 할당되는 메모리 공간은 크게 스택(Stack), 힙(Heap), 데이터(Data)영역으로 나

dsnight.tistory.com

https://helloworld-japan.tistory.com/33

 

기술 면접 힙과 스택의 차이는(heap & stack)?

안녕하세요 도쿄 정대리입니다. 오늘은 기술면접에서 자주 듣게 되는 질문인 힙과 스택의 차이에 대해서 이야기해보도록 하겠습니다. 힙과 스택에 대해서 어렴풋이 개념만 알고 있었다면 이번

helloworld-japan.tistory.com

https://electronic-hwan.tistory.com/entry/%EC%BB%B4%ED%93%A8%ED%84%B0%EA%B5%AC%EC%A1%B0-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%98%81%EC%97%AD-Stack-Heap

 

[컴퓨터구조] 메모리 영역 - Stack, Heap

ARM Cortex에 대해 공부하던 중, Stack, Heap과 같은 메모리구조에 대해 정리해놓을 필요가 있을 것 같아 이번 포스트에서는 메모리구조에 대해 공부한 내용을 정리 해보려고 한다. - 메모리 영역 메모

electronic-hwan.tistory.com

 

여튼 아래 예를 들면

 

왼쪽에 있는 C 코드를 gcc를 통해 컴파일 하여 a.out이라는 프로그램으로 만든다.

a.out을 실행시키면(즉, 메모리에 로드하여 CPU가 작업하게 만들면) 그때 프로세스가 되고, 오른쪽과 같은 메모리 구조를 띄게 되는 것

 


프로세스 상태, 라이프사이클

한국어의 경우 번역이 여러가지라 그냥 영어로 보는게 편한 것 같다

 

1. New:

    - 프로세스가 생성된 상태

    - fork 등의 시스템 콜을 통해 새로운 프로세스가 생성된다

2. Ready:

    - 프로세스가 CPU를 점유할 준비가 되어 대기하고 있는 상태

    - Ready Queue에서 대기한다

3. Running:

    - 프로세스가 CPU를 점유해 instruction을 로드하는 상태

    - 일정 시간 이후 CPU 점유를 다른 프로세스에게 넘겨주고 Ready 상태로 돌아간다

    - dispatch: 스케쥴러가 OS에서 CPU를 assign 해줄 때 사용하는 용어

4. Waiting:

    - I/O나 이벤트가 발생하면 그것이 완료될 때까지 기다리는 상태 (이때 CPU 점유 X) 

    - I/O, 이벤트가 완료되면 다시 Ready 상태가 되어 CPU 점유를 기다린다

5. Terminated:

    - 프로세스 실행을 마친 상태

    - 메모리와 자원이 회수된다

 


PCB (Process Control Block)

Process Control Block 혹은 Task Control Block은 한 프로세스에 대한 정보를 담은 것. 프로세스를 식별하기 위해 사용

아래 정보들을 하나의 구조체로 구현하고, 운영체제는 이 PCB를 통해 프로세스들을 관리

  • 프로세스 ID (PID): 특정 프로세스를 식별하기 위한 고유한 번호 (같은 프로그램이라고 해도 실행되었을 때 각기 다른 PID 지님)
  • 프로그램 카운터 (PC): fetch 해 올 메모리 주소. 명령을 어디에서부터 실행할지 
  • 레지스터 값: 이전까지 사용했던 레지스터의 중간값들 (프로그램 카운터, 메모리 버퍼 레지스터 값 등등)
  • 프로세스 상태: New, Ready, Running, Waiting, Terminated
  • CPU 스케쥴링 정보: 프로세스의 우선순위 (언제 CPU를 할당받을지)
  • 메모리 관리 정보: 프로세스가 어느 메모리에 있는지, 즉 주소를 저장해야 함
  • 사용한 파일과 IO장치 목록: 어떤 IO 장치가 이 프로세스에 할당되었고, 어떤 파일들이 필요한지 

 

 

 

 

 


 

3. 2 Process Scheduling

단일 프로세스 시스템:

- CPU가 하나의 프로세스를 끝날 때까지 주구장창 실행

- I/O 발생하면 잠시 멈추고 기다렸다가 다시 같은 프로세스 실행 == CPU 사용률이 좋지 않음

 

Multiprogramming:

- 프로세서의 자원을 최대한 효율적으로 쓰는 것이 목적

- 여러 개의 프로그램을 메모리에 올려두고 동시에 실행

- I/O 작업이 발생하면 대기하지 않고 다른 프로세스로 바꿔서 처리

 

Multitasking:

- 한 프로세스가 CPU를 아주 짧은 시간(quantum)만 실행하게 하되, 다른 프로세스와 계속해서 번갈아가며 CPU 점유

- 스케쥴러 알고리즘 등을 통해 하나의 CPU가 여러 프로세스를 번갈아가며 처리

=> Time Sharing: CPU의 점유 시간을 여러 프로세스에서 나눠갖자!

 


 

Scheduling Queue & Queueing Diagram

여러 프로세스가 CPU 자원을 나눠갖는 것을 알겠어. 그럼 한 프로세스가 Running하고 있을 동안 다른 프로세스들은 어디에서 기다려? 바로 Ready Queue와 Waiting Queue (아래 오른쪽 그림처럼 linked list로 구현 가능함)

- 만약 처리가 다 끝나지 않았지만 할당된 점유 시간이 끝났을 경우: Ready Queue에서 대기

- I/O 리퀘스트, 이벤트 등의 발생: Waiting Queue에서 I/O, Event Completion을 기다리다가 다시 Ready Queue로 가서 대기

 

 

 


 

Context Switch

프로세스를 교체하여 사용하기 위해서는 그 프로세스가 어디까지 실행했는지를 기억했다가 이후에 다시 사용해야 함. 이 기억을 위해 PCB를 사용하며 이를 문맥(Context)라고 함. 문맥 교환(Context Switch)는 한 프로세스가 다른 프로세스에 CPU 점유를 넘겨주는 과정.

- 인터럽트가 발생했을 때 커널은 실행 중인 프로세스의 상태를 PCB에 저장

- CPU를 새롭게 얻은 프로세스의 상태를 그 프로세스의 PCB에서 읽음

- 이전 프로세스의 상태를 저장하고, 다른 프로세스의 상태를 읽어오는 동안 CPU 점유에 빈 시간이 생길 수 있음

context switch 시간은 하드웨어의 지원에 크게 좌우됨. 사용할 수 있는 레지스터가 상대적으로 적거나(문맥 교환은 현행 레지스터 집합에 대한 포인터를 변경하는 것을 포함), 운영체제가 복잡하여 교환 시 처리해야 할 작업의 양이 많아지면 시간이 늘어날 수 있음.

 

 


 

3. 3  Process Operation

대부분 시스템 내의 프로세스들은 병행 실행될 수 있으며, 반드시 동적으로 생성되고 제거되어야 함. 그러므로 운영체제는 프로세스 생성 및 종료를 위한 기법을 제공해야 함.

 

 

프로세스 생성

하나의 프로세스는 여러 개의 새로운 프로세스들을 생성할 수 있음. 생성하는 프로세스를 부모 프로세스(parent process), 만들어진 프로세스를 자식 프로세스(child process)라고 부름. 이 프로세스는 process id (pid)를 사용하여 구분하며, 자식 프로세스는 또다른 프로세스를 생성할 수 있기 때문에 프로세스의 트리를 형성하게 됨.

 

linux, unix 운영체제에서는 pid가 1인 init 혹은 systemd (최근 배포판에서는 systemd) 먼저 띄워지고, 이 프로세스에서 다른 자식 프로세스가 띄워짐.

 

자식 프로세스는 자원(cpu 시간, 메모리, 파일, 입출력 장치 등)을 운영체제로부터 직접 얻거나, 부모 프로세스가 가진 자원의 부분 집합만 사용하도록 제한될 수 있음. 부모 프로세스는 자원을 분할하여 자식 프로세스들에게 나누어 주거나 메모리나 파일과 같은 자원은 자식 프로세스들과 같이 사용. + 초기화 데이터(입력)도 전달할 수도 있음

 

 

Execution 

(Unix에서) fork() 시스템 콜을 통해 새로운 프로세스 생성 가능. 새로운 프로세스는 원래 프로세스의 주소 공간의 복사본으로 구성됨. 

*fork()로 프로세스의 복사가 일어나면 자식 프로세스는 부모 프로세스의 문맥을 복사하면서 Program Counter도 복사한다. 즉, fork() 다음의 코드 부분부터 실행하게 된다. (https://higunnew.tistory.com/25)

 

5-2강. 프로세스와 관련한 시스템 콜

프로세스 복제, 프로세스 생성에 대해 다시 한번 상기해보자. 프로세스 복제는 프로세스의 문맥(Context)을 모두 복사하는 것이다. 부모 프로세스의 주소 공간 code, data, stack 영역을 그대로 복사하

higunnew.tistory.com

 

- 부모 프로세스는 자식 프로세스와 병행 실행 되거나

- wait()을 사용하여 부모 프로세스가 자식 프로세스가 종료(Terminated)될 때까지 대기하다가 실행하게 할 수도 있다

 

Termination

1. Final Statement가 실행 되면 (주로 return)

2. exit() 시스템 콜로 OS에 자신의 삭제를 요청 (직접 호출해도 되고 언어에서 알아서 불러주는 경우도 있음)

3. 물리 메모리와 가상 메모리, 열린 파일, 입출력 버퍼를 포함한 프로세스의 모든 자원이 할당 헤제되고 운영체제로 반납

기본적으로 특정 프로세스를 종료시키는 시스템 콜은 종료될 프로세스의 부모만이 호출할 수 있음(kill, break 등)

몇몇 시스템에서는 부모가 종료된 이후에 자식이 존재할 수 없기 때문에 부모가 종료되면 모든 자식들이 연쇄로 종료(cascading termination)하는 작업을 운영체제가 실행함 
좀비 프로세스: 자식 프로세스가 terminated 됐는데 부모에서 wait()가 호출되지 않음
고아 프로세스: 부모 프로세스가 fork해서 자식을 생성했는데 wait()를 호출하지 않고 Return 0해서 부모가 terminated된 상태

*데몬 프로세스: PPID가 1인 프로세스, 즉 부모가 pid 1인 프로세스이기 때문에 계속해서 실행

 

 

 


실습

1번

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

int main(void)
{
    pid_t pid;
    pid = fork();
    printf("Hello, Process! %d\n", pid);
}

p0가 실행되고 있다가 fork()를 만나면 자식인 p1을 생성. 부모는 fork()로 자식인 p1의 pid를, 자식인 p1은 fork()로 0을 반환. 

p0가 printf를 만나서 p1의 pid를 출력하고, 이후에 p1이 끝나면서 0을 출력

 

 

2번

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

int main(void)
{
    pid_t pid;
    pid = fork();
    if (pid > 0) // parent process
        wait(NULL);
    printf("Hello, Process! %d\n", pid);
}

pid에 자식 pid가 담기는 (즉, 0이 아닌) 부모는 if문으로 들어가 wait(NULL)을 만남. wait()로 인해 부모는 자식 프로세스가 끝날 때까지 

대기하게 됨. 자식은 pid에 0이 담기기 때문에 if문에 걸리지 않고 먼저 print 하고 main 함수 끝냄. 이후에 부모 프로세스도 print하고 끝냄

 

 

3번

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

int value = 5;

int main()
{
    pid_t pid;
    pid = fork();

    if (pid == 0) {
        value += 15;
        return 0;
    }
    else if (pid > 0) {
        wait(NULL);
        printf("Parent: value = %d\n", value);
    }
}

대부분은 위와 같으나, 전역 변수로 선언된 value에 저장되는 값이 자식과 부모 프로세스가 각각 독립적으로 지니고 처리한다는 것을 알기

 

 

4번

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

int main()
{
    fork();
    fork();
    fork();

    return 0;
}

program counter까지 복제된다는 점 유의

p0에서 첫 번째 fork()를 만나면, fork() 2번이 남은 p1이 만들어짐.

p1이 두 번째 fork()를 만나면, fork() 1번이 남은 p2가 만들어짐.

총 2^3개의 프로세스가 돌아감

 

 

5번

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

int main() 
{
    int i;
    pid_t pid;

    for (i=0; i<4; i++) {
        pid = fork();
        printf("Hello, Fork %d\n", pid);
    }
    return 0;
}

for문도 instruction set의 관점에서 보면 하나 실행 - 하나 실행 - 하나 실행 - 하나 실행

즉 4번과 같이 p0가 fork()를 만나면 fork()가 3번 남은 p1 생성 하는 방식

총 2^4개의 프로세스 (자식은 15개)

 

 

6번

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

int main() 
{
    pid_t pid;
    pid = fork();

    if (pid == 0) {
        execlp("/bin/ls", NULL);
        printf("LINE J\n");
    }
    else if (pid >0) {
        wait(NULL);
        printf("Child Completed\n");
    }

    return

exec()의 경우 복제한 메모리 공간에서 아예 instruction을 바꾸는 것

execlp()를 만나면 기존 부모 프로세스에서 복제한 코드 대신 "/bin/ls"를 실행하는 것으로 바뀜 (따라서 printf 실행 안 됨)