운영체제(Operating System) - 프로세스와 프로세스 관리

InAnarchy·2022년 4월 18일
0

Operating System

목록 보기
2/4

프로세스 개요

프로그램과 프로세스

프로그램: 저장장치에 저장된 실행가능한 파일

프로세스: 프로그램이 메모리에 적재되어 실행중일 때 프로세스라고 부르며,
프로세스들은 서로 독립적인 메모리 공간을 가지므로 다른 프로세스 영역에 접근할 수 없다. 또 운영체제는 프로세스마다 고유한 번호(프로세스 ID)를 할당하고, 프로세스에 대한 정보는 운영체제 커널에 의해 관리된다.

프로세스 관리

커널은 프로세스를 위해 사용자 공간에 메모리를 할당하고, 프로세스 ID를 부여하며, 커널 영역에 프로세스 테이블을 만들고, 이 테이블을 이용하여 생성된 모든 프로세스의 정보를 관리한다.

프로그램의 다중 인스턴스

하나의 프로그램을 여러번 실행시키면, 실행될 때마다 독립된 프로세스가 생성되며, 이 프로세스를 프로그램의 다중 인스턴스라고 부른다.

프로세스의 구성

프로세스는 4가지 메모리 영역으로 구성된다.
코드 영역: 프로세스 코드가 적재되는 영역
데이터 영역: 프로세스의 전역 변수들과 정적 변수들이 적재되는 영역
힙 영역: 프로세스가 실행 중에 동적 할당받는 영역, 아래번지로 내려가면서 할당
스택 영역: 함수가 호출될 때 지역변수, 매개변수, 함수의 리턴 값 등이 저장되는 영역

프로세스 주소 공간

프로세스가 실행 중에 접근할 수 있도록 허용된 주소의 최대 범위(사실상 CPU주소 공간과 같음)
사용자 공간 + 커널 공간(프로세스가 실행 중에 시스템 호출을 통해 커널에 진입하여 커널 함수를 실행하기 때문)

사용자 공간: 프로세스의 코드, 데이터, 힙, 스택 영역이 순서대로 할당되는 공간.
힙은 데이터 영역 바로 다음부터 시작하고, 스택은 사용자 공간의 바닥에서 시작하여 거꾸로 자람.
힙 영역은 높은 번지로 자라고, 스택은 낮은 번지로 자람.

커널 공간: 프로세스가 시스템 호출을 통해 이용하는 커널 공간. 커널 코드를 실행하는 것은 사용자 프로세스
커널 코드, 커널 데이터, 커널 스택 존재

결론

  • 프로세스의 코드와 데이터 영역은 실행 파일에 결정된 상태로 적재-> 실행 중 크기가 변하지 않음
  • 프로세스는 사용자 공간의 최대 범위까지 동적 할당 받으면서 힙 영역과 스택 영역을 늘려갈 수 있음
  • 프로세스마다 각각 사용자 주소 공간이 있음
  • 시스템 전체에는 하나의 커널 주소 공간이 있음
  • 모든 프로세스는 커널 주소 공간을 공유

프로세스의 주소 공간은 가상 공간이다.

운영체제는 각 프로세스마다 프로세스의 가상 주소 공간과 물리 메모리의 물리 주소 공간을 연결하는 매핑 테이블을 두고 관리한다. 가상 주소 데이터는 메모리에 분산되어 있는데, 운영체제는 물리 메모리의 비어 있는 공간에 나누어 적재하고 매핑 테이블에 적는다.

Q. 프로세스 사이 가상 주소 공간은 충돌하는가?
A. 아니다. 프로세스는 자신의 매핑 테이블을 통해 물리 메모리에 접근하고, 각 프로세스 영역은 운영체제에 의해 물리 메모리의 서로 다른 공간에 배치되므로 프로세스들 사이의 가사 주소 공간은 충돌하지 않는다.

커널의 프로세스 관리

프로세스 테이블과 프로세스 제어 블록

운영체제 커널은 시스템 전체에 하나의 프로세스 테이블을 두고 모든 프로세스의 정보를 관리하며,
프로세스를 생성할 때마다 프로세스 제어 블록(PCB)을 생성하여 프로세스의 정보를 저장하고, 테이블의 비어 있는 항목에 프로세스 번호(PID)와 함께 PCB를 연결한다.

프로세스 제어 블록

프로세스당 하나씩 존재하고, 프로세스가 생성될 때 만들어지고 종료되면 삭제

프로세스 번호(PID): 프로세스를 식별하는 고유 번호이다.

부모 프로세스 번호(PPID): 프로세스는 프로세스에 의해 생성되며 이들 사이에는 부모-자식 관계가 형성된다. 최상위 프로세스를 제외하고 모든 프로세스는 부모 프로세스를 가지며, PCB에는 부모 프로세스 ID가 저장된다.

프로세스 상태 정보(Process State): 생성 초기 상태, 실행되고 있는 상태, 준비 상태, 블록 상태가 있다. 커널은 프로세스의 상태를 바꿀 때마다 PCB에 상태 정보를 저장한다.

프로세스 컨텍스트 정보: 커널은 프로세스 컨텍스트(PC,SP,범용 레지스터 등)를 PCB에 저장한다.

스케줄링 정보: PCB에는 커널이 스케줄링 시 참고하는 우선순위, 프로세스가 사용한 CPU시간, 할당받아 실행한 시간 등이 저장된다.

종료 코드: 프로세스가 종료할 때 종료 이유를 부모 프로세스에게 전달하기 위한 정수 값으로 종료한 프로세스의 PCB에 저장된다. 종료코드는 eixt 시스템 호출의 매개변수 값이나 main()함수의 리턴 값이다. 종료되었는데 부모가 종료코드를 읽어가지 않은 상태의 프로세스를 좀비 프로세스라고 부른다.

프로세스의 오픈 파일 테이블: 실행 중에 열어놓은 파일에 관한 정보들

회계 정보: CPU사용 총 시간, 실행을 시작하여 경과한 총 시간 등을 통해 컴퓨터 사용료를 계산하거나 성능 통계를 낼 때 사용된다.

프로세스의 소유자 정보

프로세스의 생명 주기와 상태 변이

New 상태: 커널은 새 프로세스의 코드와 데이터를 메모리에 적재하고 PCB를 만들어 프로세스 테이블의 빈 항목에 등록하고, 프로세스 상태를 New로 기록한다. 실행 준비를 마쳤을 때는 Ready상태로 만든다.

Ready 상태: 커널에 있는 준비 큐에 들어간다. 실행 중인 프로세스가 종료되거나 중단되면 커널은 준비 큐에서 한 개의 프로세스를 선택한다. 이 과정을 프로세스 스케줄링 또는 CPU 스케줄링이라고 부르는데, 스케줄링이 잘못 되면 준비 큐에 오래 머무르는 기아 프로세스가 생기기도 한다.

Running 상태: 프로세스의 PCB에 상태를 Running으로 기록하고 CPU에게 프로세스를 실행하게 한다. 시간 할당량(time slice)이 지나면 다시 Ready상태로 바뀐다.

Blocked 또는 Wait 상태: Blocked는 자원을 요청하거나 입출력을 요청하고 완료를 기다리는 상태이다. 프로세스가 실행 상태에서 시스템 호출을 일으켰을 경우 커널은 현재 프로세스를 Blocked 또는 Wait 상태로 만들고, 스케줄링을 통해 Ready 상태의 프로세스를 선택하여 현재 프로세스와 컨텍스트를 스위칭한다.

Terminated/Zombie 상태: 프로세스가 종료하면 커널은 프로세스가 차지하던 메모리와 할당받은 자원들을 모두 반환하고 닫은 후 Terminated/Zombie 상태로 표기한다. 프로세스는 종료할 때 종료코드를 남기는데 PCB에 저장된 채 남아있게 된다.

Terminated/Out 상태: 부모 상태가 좀비 상태의 자식 프로세스의 PCB에서 종료코드를 읽어갈 때 커널은 좀비 상태의 PCB를 시스템에서 제거하고, 좀비 프로세스는 시스템에서 완전히 사라진다.

프로세스 스케줄링

스레드를 대상으로 스케줄링하고, 프로세스는 스레드에게 공유 자원을 제공하는 컨테이너의 역할을 한다.

C프로그램으로 프로세스 정보 얻기

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

int main(){
    pid_t pid, ppid; //sys/types.h 헤더 파일에 pid_t타입 선언(int로 선언된 것과 같음)
    pid = getpid(); //현재 프로세스 pid
    ppid = getppid(); //부모프로세스 pid
    printf("프로세스 pid = %d, 부모 프로세스 pid = %d\n", pid, ppid);
}

프로세스의 계층 구조

프로세스의 부모-자식 관계

생성한 프로세스를 부모 프로세스, 생성된 프로세스를 자식 프로세스라고 한다.

리눅스 사례
#0 프로세스: swapper/idle
#1 프로세스: init - 사용자 모드에서 실행되는 모든 프로세스의 조상
#2 프로세스: kthreadd - 부팅 때 생성되어 커널 모드로 실행되면서 커널의 기능을 돕는 프로세스(응용프로그램과 관계없이 실행)

리눅스에서 실행 중인 프로세스의 계층 구조 보기

pstree 0 #0번 프로세스부터 프로세스들의 계층 구조를 트리 형태로 출력

#0과 #1 프로세스: idle, init 프로세스

리눅스에서 idle프로세스는 부팅 관여 없이 아무 일도 하지 않고 루프, 우선순위가 가장 낮은 프로세스

부모 자식 프로세스의 실행 관계

fork() - 자식 프로세스를 생성하는 시스템 호출 함수
exit() - 현재 프로세스의 종료를 처리하는 시스템 호출 함수
wait() - 부모가 자식 프로세스가 종료할 때 까지 커널 내에서 대기하는 시스템 호출 함수


부모가 자식을 생성한 후 자식의 종료를 기다리는 경우


자식이 부모보다 먼저 종료한 경우

좀비 프로세스: 종료 후 방치된 자식 프로세스

자식프로세스가 exit()시스템 호출을 실행하면
exit()는 부모 프로세스에게 자식의 종료를 알리는 SIGCHLD신호를 전달한다.
부모가 이 신호를 받았을 때 wait()시스템 호출을 부르도록 작성되어 있지 않다면 죽은 자식 프로세스는 계속 좀비 상태로 남게 된다.
wait()를 실행하지 않는다면 자식 프로세스는 종료하였지만 좀비 상태로 PCB와 프로세스 테이블에 항목이 남아있어 살아있는 것 처럼 보여져, 쉘에 ps명령을 입력하면 좀비 프로세스도 프로세스 리스트에 나타난다.

좀비 프로세스를 제거하기 위해 부모 프로세스의 SIGCHILD 신호 핸들러가 wait()를 호출하거나,
부모 프로세스를 강제 종료하여 좀비는 init프로세스의 부모가 되고, init프로세스는 주기적으로 wait() 시스템을 호출하여 자식인 좀비 프로세스가 제거된다.

고아 프로세스와 입양

고아 프로세스: 부모가 먼저 종료한 자식 프로세스

백그라운드 프로세스와 포그라운드 프로세스

백그라운드 프로세스: 사용자와의 대화를 필요하지 않는 프로세스
포그라운드 프로세스: 터미널 사용자로부터 입출력을 독점하는 프로세스로 운영체제는 터미널에서의 사용자 입력을 모두 포그라운드 프로세스로 보낸다.

CPU 집중 프로세스와 I/O 집중 프로세스

CPU 집중 프로세스: 대부분의 시간을 계산 중심의 일을 하느라 보내는 프로세스, CPU 속도가 성능 좌우
I/O 집중 프로세스: 입출력 작업을 하느라 대부분의 시간을 보내는 프로세스, 입출력 장치나 시스템의 속도가 성능 좌우
운영체제의 스케줄링 우선 순위: I/O 집중 프로세스 > CPU 집중 프로세스

프로세스 제어

프로세스 생성과 fork()

프로세스가 생성되는 경우
시스템 부팅 과정에서 프로세스 생성
로그인 시 쉘 프로세스 생성
사용자 명령에 따라 응용프로그램 프로세스 생성
배치 프로세스 생성
응용프로그램이 다중처리를 위해 자식 프로세스 생성

프로세스는 fork(), CreateProcess()같은 시스템 호출을 통해 생성되고,
새로운 PID번호 할당, PCB 구조체 생성, 프로세스 테이블에서 새 항목 할당, 새로 할당된 프로세스 테이블 항목에 PCB 연결, 새로운 프로세스를 위한 메모리 공간 할당, PCB에 프로세스 정보 기록 등의 작업이 진행된다.

#include <unistd.h>
pid_t pid; //pid 변수 선언

pid = fork(); //자식 프로세스 생성
if(pid >0) { /*부모 프로세스가 실행할 코드*/
}
else if (pid ==0){ /*자식 프로세스가 실행할 코드*/
}
else{ /*fork()오류를 처리하는 코드*/
}

fork()를 이용한 자식 프로세스 생성

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

#include <unistd.h>

int main(){
    pid_t pid;
    int i, sum = 0;

    pid = fork(); //자식 프로세스 생성
    if(pid >0) { //부모 프로세스에 의해 실행되는 코드
    printf("부모 프로세스: fork()의 리턴 값 = 자식 프로세스 pid = %d\n", pid);
    printf("부모 프로세스: pid = %d\n", getpid());
    wait(NULL); //자식 프로세스가 종료할 때 까지 대기
    printf("부모 프로세스 종료\n");
    return 0;
    }
    else if(pid ==0){ //자식 프로세스에 의해 실행되는 코드
    printf("자식 프로세스: fork()의 리턴 값 pid = %d\n", pid);
    printf("자식 프로세스: pid = %d, 부모 프로세스: %d\n", getpid(), getppid());
    for(i=i;  i<=100; i++)
    sum+=i;
    printf("자식 프로세스: sum = %d\n", sum);
    return 0;
    }
    else { //fork() 오류
    printf("fork 오류");
    return 0;
    }
    }

프로세스 오버레이와 exec()

프로세스 오버레이: 현재 실행 중인 프로세스의 주소 공간에 새로운 응용 프로그램을 적재하여 실행시키는 기법
프로세스가 execlp(), execv(), execvp() 등으로 시스템 호출을 하면 exec()는 호출한 프로세스의 주소 공간에 새로운 응용프로그램의 코드,데이터,힙,스택을 올리게 되어, 호출 프로세스의 모든 영역들이 사라진다.

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

int main(){
    pid_t pid;

    pid = fork(); //자식 프로세스 생성
    if(pid >0) { //부모 프로세스에 의해 실행되는 코드
    printf("부모 프로세스: fork()의 리턴 값 = 자식 프로세스 pid = %d\n", pid);
    printf("부모 프로세스: pid = %d\n", getpid());
    wait(NULL); //자식 프로세스가 종료할 때 까지 대기
    return 0;
    }
    else if(pid ==0){ //자식 프로세스에 의해 실행되는 코드
    printf("자식 프로세스: fork()의 리턴 값 pid = %d\n", pid);
    printf("자식 프로세스: pid = %d, 부모 프로세스: %d\n", getpid(), getppid());
    execlp("/bin/ls", "ls", "-l", NULL);
    }
    else { //fork() 오류
    printf("fork 오류");
    return 0;
    }
    }

프로세스 종료와 프로세스 종료 대기

프로세스 종료: exit() 시스템 호출, C프로그램의 main()에서 리턴
종료 코드: 부모 프로세스에게 전달하는 값, 정상 종료는 0, 1~255는 종료 이유를 임의로 정해서 사용

int main(){
	return 300; //return 44;와 같음
}
void func(){
	exit(300); //exit(44)와 같음
}

exit() 시스템 호출로 프로세스 종료 과정
프로세스의 모든 자원 반환->PCB에 프로세스 상태를 Terminated로 변경->PCB에 종료 코드 저장->자식 프로세스들이 있으면 이들을 init에 입양->부모 프로세스에게 SIGCHLD신호 전달

wait() 시스템 호출로 자식 프로세스 종료 대기

좀비 프로세스

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

int main(){
    pid_t pid, zompid;
    int status;

    pid = fork(); //자식 프로세스 생성
    if(pid >0) { //부모 프로세스에 의해 실행되는 코드
    sleep(10); //10초동안 잠자기
    zompid = wait(&status); //자식 프로세스 종료 대기
    printf("부모 프로세스: 자식 pid = %d, 종료 코드 = %d\n", zompid, WEXITSTATUS(status));
        return 0;
    }
    else if(pid ==0){ //자식 프로세스에 의해 실행되는 코드
    printf("자식 프로세스: %d 종료합니다.\n", getpid());
    exit(100); //자식이 종료하여 부모가 wait() 호출할 때 까지 좀비 프로세스. 종료코드 100 전달
    }
    else { //fork() 오류
    printf("fork 오류");
    return 0;
    }
    }
profile
github blog 쓰다가 관리하기 귀찮아서 돌아왔다

0개의 댓글