개발자 끄적끄적

프로세스와 프로세스 관리 본문

운영체제

프로세스와 프로세스 관리

햏치 2024. 3. 22. 01:28

<프로그램 vs 프로세스>
- 프로그램(Program)
  - 보조저장장치에 저장된 실행 가능한 파일
  - 수동적 존재

- 프로세스(Process)
  - 메모리에 적재되어 CPU에 의해 실행중인 프로그램
    - 실행 중에 필요한 자원을 할당 받음
    - CPU를 할당 받기 위해 대기하거나 I/O 처리가 끝나기를 대기하는 경우 포함
    - 실행중인 프로그램
    - 오늘날 시분할 시스템에서 작업의 단위
 
  - PC와 관계되는 자원들의 집합을 갖는 능동적 실체
    - 생성, 실행, 일시 중단, 재개

- 시스템 = 프로세스들의 집합체
  - OS에 의해 CPU는 프로세스들간에 다중화(Multiplex)하여 시스템 효율성을 높임




<프로세스의 특징>
- OS는 실행되는 프로그램을 메모리에 잭재
  - 실행에 필요한 메모리 할당
    - 코드, 데이터, 스택...

- 각 프로세스들은 독립적인 메모리 공간을 가진다
  - 기본적으로 다른 프로세스의 영역에는 접근 불가
 
- 커널은 각 프로세스의 정보를 관리



<커널의 프로세스 관리>
- 프로세스는 OS가 프로그램의 실행을 위해 생성한 것
  - 커널은 각 프로세스의 정보 관리
    - PCB(Process Control Block)
    - 커널 영역에 프로세스 테이블을 통해 프로세스 목록 관리

- 관리내용
  - 현재 각종 상태 정보 관리
    - 프로세스 ID 할당
    - 사용자 공간에 할당된 메모리 위치와 크기

  - 프로세스 간 통신, 동기화
  - 스케줄링 및 컨텍스트 스위칭




<프로그램의 다중 인스턴스>
- 한 프로그램이 여러 번 실행된 경우 OS는 독립적인 프로세스를 생성
  - 기본적으로 각 프로세스에게 독립된 메모리 공간 할당
  - OS는 다중 인스턴스 프로세스들을 별개로 취급




<CPU 주소 공간(CPU address space)>
- CPU가 '접근할 수 있는' 전체 메모리 공간
  - 최대 크기 : 주소 버스 수에 의해 결정
    - 32비트 CPU -> 32개의 주소선 -> 2^32 -> 4GB
      - 주소 공간은 0번지부터 시작
      - 하나의 주소가 가리키는 공간은 1바이트 영역

  - CPU 주소 공간보다, 
    - 큰 메모리가 장착되어 있어도 접근 불가
    - 작은 메모리가 장착되어 있는 경우엔 접근 가능
      - 단, CPU가 설치된 메모리의 주소 영역을 넘어 접근하면 시스템 오류 발생



<프로세스의 메모리 구성>
- 프로세스는 프로그램과 달리 주기억장치에 주소 공간을 가지며,
- 현재는 활동 요소를 갖는 능동적인 개체이다
  - 프로세서(processor)의 레지스터에 의해 표현

low address
코드 : 실행 코드를 저장
정적 데이터 : 정적 변수(전역, 지역)를 저장 -> static, 고정된 크기의 공간
힙 : 동적 할당 영역 -> malloc
...
스택 : 함수의 복귀 주소 및 지역 변수를 저장하는 영역(매개변수, 반환값 포함)
high address



<프로세스 주소 공간>
- 프로세스가 실행 중에 '접근할 수 있도록 허용된' 주소의 최대 범위
  - 가상적인 논리 공간
    - 0번지부터 시작해서 연속적
    - 사용자 공간 + 커널 공간(모드의 변화가 필요)

  - CPU 주소 공간과 같다

  - 프로세스 주소 공간의 크기
    - CPU가 접근할 수 있는 최대 크기 : 32비트 CPU -> 4GB
    - 단, 프로세스의 '현재 크기'와는 다름
      - 코드/데이터 영역 크기 + '현재 할당된' 힙/스택 영역의 크기
      - 실행 중에 계속 변한다



<프로세스 주소 공간의 구성>
- 사용자 공간
  - 코드 + 데이터 + 힙 + 스택이 할당된 공간
    - 코드와 데이터 영역의 크기는 프로세스 시작시 결정됨
    - 힙과 스택 영역의 크기는 정해져 있지 않다
      - 힙은 아래로, 스택은 위로 자람

  - 각 프로세스마다 고유하게 가진다

- 커널 공간
  - 커널코드 + 커널 데이터 + 커널 스택(커널 코드 실행 시)
    - 프로세스가 시스템 호출을 통해 이용하는 커널 공간

  - 커널 공간은 모든 사용자 프로세스에 의해 공유된다



<커널 공간의 의미>
- 프로세스가 사용자 코드에서 '시스템 호출'을 통해 커널 코드를 실행할 때 사용되는 공간
  - 사용자 프로세스가 커널 모드에서 실행되고 있다고 함
    - 커널 코드를 실행하는 것은 사용자 프로세스 
  - 사용자 영역과 커널 영역을 하나의 가상 주소 영역으로 다룬다
    - 커널 코드가 적재된 물리 메모리 위치 역시 사용자 프로세스가 소유한 매핑 테이블 사용




<프로세스 주소 공간의 특징>
- 프로세스 주소 공간은 각 프로세스 별로 할당된 가상 공간
  - 사용자나 개발자가 보는 관점
    - 자신이 작성한 프로그램이 0번지부터 시작하여 연속적인 메모리 공간에 형성된다고 상상
    - CPU가 접근할 수 있는 최대 크기의 메모리가 설치되어 있다고 상상

- 실제 상황
  - 설치된 물리 메모리의 크기는 프로세스 주소 공간보다 작을 수 있다
  - 코드, 데이터, 힙, 스택은 물리 메모리에 흩어져 저장된다
    - 연속 할당이 아니다



<커널의 프로세스 관리>
- 각 프로세스는 상태를 가지며, 실행되면서 상태가 변한다

- 프로세스 생명 주기와 상태 변이
New(프로세스가 생성된 직후)
New -> Ready : CPU에게 할당받기를 기다리는 상태
Ready -> Running : CPU를 할당받아서 스케줄링 될 때
  Running-> Blocked : 입출력을 실행시켰거나, sleep() 호출로 알람을 기다릴 때, 자원이 사용가능하기를 기다릴 때
  Blocked -> Ready : 입출력 완료. 타임 아웃, 혹은 요청 자원을 얻게 되었을 때
Running -> Terminated/Zombie : 프로세스 완료, 혹은 외부로부터 강제 종료될 때
Terminated/Zombie -> Terminated/Out : 부모 프로세스에 의해 프로세스의 종료가 확인될 때
*Running 상태에는 한 개의 프로세스만 놓일 수 있다



<프로세스 제어 블록(PCB, Process Control Block)
- 한 프로세스에 관한 모든 정보를 저장하는 '구조체'
  - 각 프로세스마다 하나씩 존재
  - OS마다 다르다

- 커널에 의해 생성, 저장, 읽혀지는 등 관리
  - 프로세스 생성 시 만들어지고, 종료 시 삭제된다

ex)
  프로세스 번호(PID)
  부모프로세스 번호(PPID)
  프로세스 상태(process state) 
  프로세스 컨텍스트(PC, SP, 범용 레지스터 등 CPU 레지스터들)
  스케줄링 정보(priority, nice 값, 프로세스가 사용한 CPU 시간 등)
   프로세스의 종료 코드(exit code)
  프로세스의 오픈 파일 테이블, 메로리 관리를 위한 정보들(페이지 테이블 주소 등)
  프로세스 사이의 통신 정보들
  회계 정보(accounting info)
  프로세스 소유자 이름(user name)
  기타 프로세스가 사용중인 입출력 장치 목록 등




<프로세스 테이블(Process Table)> 
- OS가 모든 프로세스를 관리하기 위한 배열
  - 시스템에 한 개 존재
  - 구현 방식은 OS마다 다르다

- PCB와 프로세스 테이블은 '커널 영역'에 위치
  - 커널 코드(커널 모드)만이 접근 가능
  - PCB에 빠르게 접근 할 수 있도록 한다

- 프로세스가 종료하면
  - 프로세스 테이블에서 해당 프로세스를 제거





<프로세스 스케줄링(Process Scheduling)>
- 다중 프로그래밍 OS에서 실행 중인 프로세스들중 CPU를 '할당할 프로세스'를 결정하는 과정
  - 과거 OS : 프로세스가 실행 단위
  - 오늘날 OS : 스레드가 실행 단위

- 스레드(thread)
  - 1개의 프로세스는 1개 이상의 스레드로 구성된다
    - 각 작업(task)를 구성하는 단위
  - 오늘날 OS에서 실행 단위
    - 프로세스 스케줄링 -> 스레드 스케줄링
    - 프로세스는 스레들 간에 공유 자원을 제공하는 컨테이너 역할로 바뀐다





<프로세스 정보 보기>
- 프로세스 테이블의 PCB로부터 얻어올 수 있다
- #0(시조 프로세스) : swapper/idle process



<리눅스의 프로세스 목록>
- $ps -eal : 모든 사용자의 전체 프로세스 목록들 출력



<프로세스 계층 구조>
1. 부모-자식 관계를 갖는 프로세스
  - OS는 프로세스들을 부모-자식 관계로 분리

2. #0 프로세스
  - 유닉스의 #0 프로세스(swapper)
    - 부팅 과정 담당, 메모리 부족 시 스와핑 담당
    - #1(init) 프로세스 생성
      - 부팅 후 시스템을 시작시킴,
      - 시스템 종료시 마무리 작업 수행

  - 리눅스의 #0 프로세스(idle process)와 윈도우의 #0 프로세스(system idle process)
    - 유닉스 경우와 달리 부팅에 관여하지 않는다
    - 우선 순위가 가장 낮으며 아무 일도 하지 않는다



<리눅스의 프로세스 관련 시스템 호출>
- '시스템 호출'을 통해서만 프로세스 생성/종료
  - fork()
    - 부모의 복사본을 갖는 자식 프로세스 생성

  - exit()
    - 프로세스 종료를 커널에 알린다 
    - wait() 호출로 대기 중인 부모 프로세스를 깨운다

  - wait()
    - 부모가 자식 프로세스가 종료되기를 기다린다
    - 자식이 종료할 때까지 반환되지 않고 커널 내에서 대기시킨다




<부모는 자식의 종료를 확인하고 마무리>
- 부모는 wait() 시스템 호출을 통해 자식의 종료 코드를 읽어야 한다




<고아 프로세스의 입양>
- 자식프로세스보다 부모프로세스가 먼저 종료(exit() 또는 비정상 종료)되었을 때 고아 프로세스가 생성되고
  #1 init 프로세스가 고아 프로세스를 입양. 즉, init이 고아 프로세스의 양부모가 된다




<프로세스의 종류(1)>
- 사용자와의 상호작용 여부에 따라

  - 포그라운드 프로세스(foreground process)
    - 실행되는 동안 사용자 입력을 독점

  - 백그라운드 프로세스(background process)
    - 사용자와 상호작용을 하지 않으면서 실행
    - 사용자 입력을 필요로 하지 않으면서 처리 기간을 긴 경우




<프로세스의 종류(2)>
- 처리 내용의 유형에 따라

  - CPU 집중 프로세스
    - 대부분의 시간이 CPU 계산인 경우 : 배열 곱, 인공지능 관련 연산, 이미지 처리 등
  - CPU 성능이 시스템의 성능 좌우(CPU bound)

  - I/O 집중 프로세스
    - 입출력 작업을 하느라 대부분의 시간을 보내는 경우 : 파일 입출력, 네트워크 전송
    - 입출력 장치 시스템의 속도가 시스템 성능 좌우(I/O bound)
    - OS는 이 유형의 프로세스를 우선 스케줄링 : 사용자 응답시간은 줄어들고, 시스템의 처리율은 높아진다




<프로세스 생성 과정>
- 시스템 호출을 통해 생성
  - 새로운 PID 번호 할당
  - PCB 구조체 생성
  - 프로세스 테이블에 새 항목 할당하고 PCB 연결
  - 메모리 공간을 할당하고 코드와 데이터 적재
  - PCB에 프로세스 정보 기록
  - 프로세스 상태를 ready로 기록, 준비 큐에 넣어 스케줄 대상이 되게 한다
  - 자식의 PID를 부모에게 알림
    - 부모는 자식에게 신호를 보내거나
    - 자식이 종료되기를 대기





<프로세스가 생성되는 경우>
- 부팅 과정에서 필요한 프로세스 생성
- 로그시 사용자 대화를 위한 프로세스 생성 : 쉘 프로그램
- 사용자 명령 실행 시 
- 배치 작업 실행 시 : at, batch





<fork() 시스템 호출>
- 현재 프로세스를 '복사'해서 자식 프로세스 생성
  - int pid = fork();
    - 부모(현재 프로세스)의 모든 환경, 메모리, PCB 복사
    - 부모와 동일한 내용이나 독립된 주소 공간 소유

ex)
pid = fork(); //자식 프로세스 생성

if(pid >0){  
  //부모 프로세스가 계속 실행할 코드 작성

else if(pid == 0){ 
  //자식 프로세스가 실행할 코드 작성
 } 
else {  
  //fork() 호출의 오류를 처리하는 코드 작성
}
  - 반환값
    - 부모 : 자식의 PID
    - 자식 : 자식 0





<프로세스 오버레이(Process Overlay)>
- 현재 실행중인 프로세스의 주소 공간에 새로운 프로그램을 적재하여 실행
- exec 패밀리 시스템 호출 
  - execlp(), execv(), execvp() : 다양한 형태의 시스템 호출 함수 존재
  - 실행 파일을 로딩하여 '현재'프로세스의 주소공간에 '덮어씀'
    - 새로운 프로세스 생성 과정을 거치지 않는다 : PID 변경 없음
  - 보통 fork()로 생성된 자식에 exec() 실행




<exec() 시스템 호출의 예>
- 내 프로그램에서 /bin/ls 를 실행하는 경우
부모 프로세스 -> fork() -> 자식프로세스 생성 -> wait() (자식 종료 대기) -> 자식의 종료 확인
자식프로세스 -> exec() (적재되며 /bin/ls 실행 파일) -> /bin/ls 프로세스 -> exit() (프로세스 종료) -> 종료 



<프로세스 종료>
- 'exit()'시스템 호출

- C 프로그램의 main()에서 return
  - 컴파일 시 exit() 시스템 호출이 실행되도록 처리됨

- 이 때 부모에게 종료 코드를 전달함
  - 시스템 호출 : exit(종료코드);
  - main() 함수의 반환값 : return, 종료코드;




<종료 코드(exit code)>
- 현재 프로세스가 종료한 상태나 이유를 부모에게 전달하기 위해 사용되는 값
  - POSIX 표준에서 '0~255의 1'바이트 데이터
    - 관례적으로 0은 정상 종료로 간주 : C언어 C++의 return 0
    - 1~255의 값은 각 개발자가 종료 이유를 임의로 정해 사용
    - 255이상의 값이 사용된 경우 256으로 나눈 나머지가 전달 : 300 -> 300% 256 = 44
    - -1(음수 값)을 전달하는 경우 255가 전달 : -1 -> 0xFF -> +255 즉, return -1 = return 255(비정상적으로 프로그램이 종료되었다)




<프로세스 종료 과정>
1. 종료 발생
- main()의 종류나 'exit()'호출을 통한 정상 종료
- 다른 프로세스에 의한 강제 종료(kill)

2. 종료되는 프로세스에게 할당된 모든 '메모리'반환
- 할당된 모든 메모리 반환, 열어 놓은 파일/소켓 닫음
- 단, PCB는 프로세스 테이블에서 제거되지 않음
  - PCB에 프로세스 상태를 terminated로 변경
  - PCB에 종료 코드 저장

3. 부모가 '시스템 호출'을 통해 자식이 남긴 정보를 읽어간 후, 자식의 PCB가 완전히 제거

'운영체제' 카테고리의 다른 글

스레드와 멀티태스킹  (0) 2024.03.28
운영체제와 인터럽트(Interrupt)  (0) 2024.03.13
커널(Kernel)과 시스템 호출(System Call)  (0) 2024.03.13