OS는 할껀데 핵심만 합니다. 2편 프로세스와 프로그램

1

OS

목록 보기
2/17

운영체제 프로세스

1. 프로세스 개요

운영체제에서 프로세스는 하나의 작업 단위이다. 사용자가 더블 클릭하여 프로그램을 실행하면 그 프로그램은 프로세스가 된다.

1.1 프로그램 vs 프로세스

  • 프로그램 : 하드디스크와 같은 저장장치에 보관되어 더블 클릭 시 실행된다.
  • 프로세스 : 프로그램이 메모리에 올라와 작업이 실행되면 프로세스가 된다.

프로그램은 메모리에 저장된 정적인 상태이고, 프로세스는 실행을 위해 메모리에 올라온 동적인 상태이다.

프로그램은 어떤 데이터를 사용하여 어떤 작업을 할지 일련의 절차를 적어 놓은 것이다. 반면 프로세스는 이 프로그램으로 작성된 작업 절차를 실제로 실행에 옮겨진 것을 말한다. 따라서, 누군가 작성한 프로그램이 실행되면 프로세스가 된다.

운영체제는 이러한 프로세스들이 자원을 이용하는데 있어 효율적이고 안전하게 작동하도록 도와준다.

1.2 PCB(Process Control Block)

먼저 운영체제는 프로그램을 메모리에 적재한다. 이와 동시에 프로세스에 대한 정보를 기록한 ```프로세스 제어 블록(process control blcok, PCB)를 만들어낸다.

PCB는 프로세스를 처리하는데 필요한 다양한 정보가 들어있다. 어떤 프로그램이 프로세스가 되었다는 것은 운영체제로부터 PCB를 받았다는 의미이다.


https://binaryterms.com/process-control-block-pcb.html

  1. process-id : 새로운 프로세스에 시스템이 할당해주는 고유 id
  2. process- state : 프로세스의 라이프 타임과 관련된 상태로, waiting, running, ready, blocked, end, suspend-wait, suspend-ready 가 있다.
  3. priority : 프로세스 스케줄링을 위한 우선순위이다.
  4. Process Accounting Information : CPU를 사용한 시간, CPU 할당 시간 등이 있다.
  5. Program Counter : 이전에 배운 PC로 다음 작업할 명령어 위치를 기억한다.
  6. List of Open Files : 실행 중에 프로그램에 필요한 모든 파일의 정보를 포함한다.
  7. Process I/0 status information : 해당 프로세스가 실행 중에 할당을 요구한 I/O 장치에 대한 정보를 담는다.
  8. CPU 레지스터 : context switch가 발생하면 이 때의 레지스터 정보를 기억해서 다시 프로세스가 CPU 할당을 받으면 사용한다. accumulator, index, stack pointer 와 같은 레지스터의 값이 저장된다.
  9. PCB Pointer : 준비중인 다음 프로세스의 주소를 가리킨다. 준비 상태나, 대기 상태의 큐를 구현할 때 다음을 가리키는 포인터로 사용된다.
  10. Memory management information : 메모리 관리 정보로 프로세스가 메모리의 어디에 있는 지 나타내는 메모리 정보와 메모리 보호를 위한 경계 레지스터, 한계 레지스터 값등이 저장된다. 또한, segmentation table , page table 등 정보도 보관한다.
  11. PPID, CPID : 부모 프로세스를 가리키는 PPID, 자식 프로세스를 관리하는 CPID가 저장된다.

PCB에 다양한 정보 중 대표적인 내용은 다음과 같다.

  1. 프로세스 구분자 : process-id에 해당하는 내용으로 메모리에 있는 여러 프로세스들을 구분하기 위해 존재한다.
  2. 메모리 관련 정보 : CPU는 실행하려는 프로세스가 메모리의 어디에 저장되어있는 지 알아야 작업이 가능하다. 이를 위해 PCB에는 프로세스의 메모리 위치 정보가 담겨져 있다. 또한, 메모리 보호를 위한 경계 레지스터와 한계 레지스터도 포함되어 있다.
  3. 각종 중간값 : PCB는 프로세스가 사용했던 중간값을 저장한다. 시분할 시스템에서는 여러 프로세스가 번갈아가며 실행되기 때문에 각 프로세스는 일정 시간 작업을 한 후 다른 프로세스에 CPU를 넘겨준다. 때문에 작업을 그만두어야 하는 프로세스는 다음에 작업해야할 위치가 담긴 레지스터들을 저장한다.

각종 중간값은 다음 실행할 명령어인 program counter와 연산이 저장된 CPU register를 말하는 것이다.

참고로, PCB는 메모리 영역 중에 사용자가 접근할 수 없는 운영체제 영역에 있다.

정리

프로세스 = 프로그램 + PCB

1.3 프로세스 상태

프로세스들은 자신의 상태를 가지고 있는데, 이는 시분할 시스템에서 프로세스에서 time quantum을 만큼 process들이 CPU를 자원을 가지고 있을 수 있기 때문에 context switch(문맥 교환)이 자주 발생하기 때문이다.

  1. New : 프로세스가 메모리에 올라와 실행 준비를 완료한 상태, PCB가 생성된다.
  2. Ready : 생성된 프로세스가 CPU를 얻을 때까지 기다리는 상태이다. 실행 순서가 될 때까지 기다린다. PCB는 ready queue에서 기다리며 CPU 스케줄러에 의해 관리된다.
  3. Running : 준비 상태에 있는 프로세스 중 하나가 CPU를 얻어 실제 작업을 실행하는 상태이다. 만약, 시간(time quantum)을 다 사용하면 timeout이 발생하여 해당 프로세스를 준비 상태로 옮긴다. 만약 time quantum안에 끝나면 exit(end) 상태로 간다. 만약, 프로세스가 입출력을 요구하면 입출력 관리자에게 입출력을 요청하고 waiting(block) 상태로 간다.
  4. Waiting(blcok) : 실행 상태에 있는 프로세스가 입출력을 요청하면 입출력이 완료될 때까지 기다리는 상태이다. 대기 상태의 프로세스는 입출력 장치별로 마련된 큐에서 기다린다. 입출력 완료 시에는 인터럽트가 발생하고, 프로세스를 깨운다. 따라서, 어떤 프로세스가 대기 상태에서 준비 상태로 이동하는 것은 인터럽트 때문이다.
  5. End(terminate) : 프로세스가 종료되는 상태이다. 메모리에서 사용했던 데이터를 삭제하고 PCB를 폐기한다.

위의 다섯 가지 상태를 활성 상태(active status)라고 한다. 프로세스의 상태는 활성 상태 외에 또 다른 상태가 있다.


https://www.gatevidyalay.com/process-states-in-operating-system/

보류 상태(suspend status)는 프로세스가 메모리에서 잠시 쫓겨난 상태로 휴식 상태(메모리에 올라와있고 프로세스 실행이 잠시 멈춘것)와 차이가 있다. 보류 상태는 일시 정지 상태라고도 불리며, 보류 상태와 비교하여 일반적인 프로세스 상태를 활성 상태라고 한다.

보류 상태에 들어간 프로세스는 메모리 밖으로 쫓겨나 스왑 영역에 보관된다. 휴식 상태는 프로세스가 메모리에 있지만 멈춘 상태이고, 보류 상태는 프로세스가 스왑 영역에 있고 멈춘 상태이다.

다음과 같은 경우 보류 상태가 된다.
1. 메모리가 꽉차서 일부 프로세스를 밖으로 내보낼 때
2. 프로그램 오류가 있어 실행을 미루어야 할 때
3. 바이러스와 같은 공격 프로세스일 때
4. 매우 긴 주기로 반복되는 프로세스라 메모리 밖으로 쫒아내도 문제 없을 때
5. 입출력을 기다리는 프로세스의 입출력이 지연될 때

  1. suspend/wait(보류 대기) : 아직 입출력 장치 관리자로 부터 인터럽트를 받지 못한 상태이면서 메모리에 자리가 없는 상태이다. 만약, 메모리에 자리가 생기면 바로 wait 상태로 바뀐다.
  2. suspend/ready(보류 준비) : 입출력 인터럽트를 받았을 때, 바로 wait상태로 가지않고 보류 준비 상태로 간다. 보류 준비 상태에서 메모리에 자리가 날 때까지 기다린다.

2. 문맥 교환(context switching)

context switching은 CPU를 차지(running 상태)하던 프로세스가 나가고 새로운 프로세스를 받아들이는 작업을 말한다. 이 때 두 PCB의 내용이 변경된다.

실행 상태에서 나가는 PCB는 지금까지의 작업 내용을 저장하고, 반대로 실행 상태로 들어오는 프로세스는 PCB 내용을 바탕으로 CPU가 다시 세팅된다.


https://linuxtut.com/en/90efd15a1983c61551a6/

context switching이 발생하는 경우는 매우 다양하다. 일반적으로 한 프로세스가 자신에게 주어진 시간을 다 사용하던지, 인터럽트가 발생하여 실행상태에 벗어날 때도 발생한다.

3. 프로세스 연산

3.1 프로세스의 구조


https://dev.to/ketan_patil/c-c-process-map-242a

프로세스의 구조는 다음과 같이 DATA(BSS), CODE, HEAP, STACK 4가지로 이루어져 있다.

  1. CODE 영역 : 프로그램의 본문이 기술된 곳으로 텍스트 영역이라고도 한다. 프로그래머가 작성한 프로그램은 코드 영역에 탑재되며 읽기 전용으로 처리된다.
  2. 데이터 영역 : 전역 변수와 static(정적) 변수로 선언된 변수가 저장된 곳이다. 참고로 BSS는 data 영역에서 초기화되지 않은 변수들을 말하고, 초기화된 영역은 데이터 영역이라고 나눌 수 있다.
  3. 스택 영역 : 운영체제가 프로세스를 실행하기 위해 부수적으로 필요한 데이터를 모아놓은 곳이다. 가령, 함수 호출로 인한 지역 변수, 반환값, 반환 주소, 파라미터들이 있다.
  4. 힙 영역 : 개발자들이 메모리에 제한되지 않고 프로그램을 개발할 수 있도록 할당된 영역이다.

위 링크를 참조하면 코드로 짜여진 예제를 확인할 수 있으니 들어가보도록 하자

3.2 프로세스의 생성과 복사 fork()

fork() 시스템 호출은 실행 중인 프로세스로부터 새로운 프로세스를 복하는 함수이다. 일종의 커널이 제공하는 시스템 호출이다.

크롬의 탭으로 창을 하나 더 만드는 것(ctrl + n)도 fork()을 일종이다. 새로운 크롬 프로그램을 실행하는 것이 아닌, 현재 프로세스를 복사한 것일 뿐이다.

이때 복사할 때 기존의 프로세스가 부모 프로세스가 되고, 새로 생성된 것이 자식 프로세스가 된다. 따라서 부모 프로세스의 PCB에서 CCIP는 자식 PCB의 process-id가 되고 자식 프로세스의 PCB에서 PPID는 부모 프로세스의 process-id가 된다.

fork()시스템 호출을 하면 프로세스 제어 블록(PCB)을 포함한 부모 프로세스의 대부분이 자식 프로세스에 복사되어 똑같은 프로세스가 만들어진다.

단, id와 메모리 정보, PPID, CCID만 다르다.

3.3 c언어에서의 fork

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

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

    if(pid < 0){
        printf("Error");
        exit(-1);
    }
    else if(pid == 0){
        printf("Child");
        exit(0);
    }
    else {
        printf("Parent");
        exit(0);
    }
}
Parent Child

자식 프로세스는 부모 프로세스의 code 영역도 배낀다고 했다. 따라서 같은 코드가 만들어지게 된다. 그렇다면 계속 fork되므로 무한 반복되는게 아닐까 생각이 들겠지만, 시스템 fork함수의 동작은 다음과 같다.

  1. 부모 프로세스의 fork에는 양수를 반환한다.
  2. 자식 프로세스의 fork에는 0 을 반환하여 fork를 중지한다.
  3. 만약 음수가 나온다면 이는 fork에 실패한 것이다.

따라서, 계속해서 fork가 이루어지진 않는다. 다만, 부모 프로세스와 자식 프로세스가 서로 독립적이기 때문에 "Parent"가 먼저일지 "Child"가 먼저 출력될 지는 아무도 모른다.

3.4 프로세스의 전환( exec() )

exec() 시스템 호출은 기존의 프로세스를 새로운 프로세스로 전환하는 함수이다.

사용하려는 목적은 프로세스의 구조체를 재활용하기 위함이다. 새로운 프로세스를 만들려면 PCB를 만들고, 메모리 자릴르 확보해야하는 과정을 걸친다. 또한, 프로세스 종류 후에 메모리를 청소하기 위해서는 cascading으로 삭제 작업이 이루어지는 부모-자식 상속 관계를 만들어야 한다. 이때 exec() 시스템 호출을 사용하면 이미 만들어진 PCB, 메모리 영역, 부모-자식 관계를 그대로 사용할 수 있어 편리하다.

동작 과정은 매우 단순하다. fork()와는 반대로, process-id, PPID, CPID, 메모리 관련 사항등은 변하지 않지만, 레지스터, 파일 정보 등 모두 리셋된다.

마치 프로세스를 처음 시작하는 것처럼 내용이 정리된다.

심지어 code 영역도 새로운 코드로 적혀지고, 데이터 영역도 새로운 데이터가 할당되며, stack 영역 역시도 리셋된다.

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

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

    if(pid < 0){
        printf("Error");
        exit(-1);
    }
    else if(pid == 0){
        exclp("music_player", "music_player", NULL);
        exit(0);
    }
    else {
        wait(NULL);
        printf("music_player terminated");
        exit(0);
    }
}

fork() 명령어를 거치면서 자식 프로세스는 pid == 0인 곳으로 빠져 exec()함수인 exclp()를 실행한다. exclp()는 음악 플레이어를 실행하는 임의의 함수로 내부적으로 exec()함수라고 하자, 이를 실행하면 이제 자식 프로세스의 code영역이 새로운 코드로 덮어쓰여지므로 음악을 재생하는 코드로 싸악 바뀐다.

또한, 부모 프로세스는 wait()함수에 걸려서 자식이 어떠한 값이라도 return해줄 때까지 기다린다.

자식 프로세스가 음악을 완료하고 나면 return이 실행되고, 이는 wait에서 반응하게되어 printf 코드를 실행한다.

이는 exec() 시스템 호출을 사용하여 새로운 프로세스로 전환하더라도 프로세스 제어 블록의 각종 프로세스 구분자(PID, PPID, CPID)가 변경되지 않았기 때문에 부모 프로세스로 돌아올 수 있던 것이다.

유닉스에서는 이러한 fork()exec()시스템 호출을 이용하여 프로세스를 복사하고, 생성한다. 가령, 사용자가 음악 플레이어를 켜려고 한다면, shell 프로세스는 fork() 호출로 새로운 shell 프로세스를 만들고, exec()으로 뮤직 프로세스로 만든다.

이렇게 프로세스를 계층 구조로 만들면 프로세스 간의 책임 관계가 분명해져 시스템을 관리하기 수월하다. 특히, 자원회수에 아주 편리하다.

3.5 고아 프로세스 ( orphan process, zombie process )

부모 프로세스가 먼저 종료되거나, 자식 프로세스가 비정상적으로 종료되어 부모 프로세스에 연락이 안되는 경우가 있다. 이와 같은 경우 자식 프로세스가 종료되지 않거나, 종료되었음에도 자원이 그대로 남게된다.

이렇게 프로세스가 종료된 후에도 비정상적으로 남아있는 프로세스를 고아 프로세스(orphan process) 또는 좀비 프로세스라고 한다.

고아 프로세스 : 부모가 자식보다 먼저 죽는 경우를 말한다.
좀비 프로세스 : 자식 프로세스가 종료되었지만, 부모 프로세스가 뒤처리가 되지 않아 좀비같이 목적없이 살아있는 경우를 말한다.

고아나 좀비나 둘 다 자원이 낭비됨으로서 효율적인 자원 운영에 방해가 된다.

0개의 댓글