프로세스의 추상화 과정 (The Abstraction: A Process)

프로세스를 구현하기 위해 수많은 추상화 과정들이 있다.

 

프로세스가 실행될 땐 무언가 읽거나 써야지

  • 주소 공간 : 프로세스가 처리할 수 있는 메모리
  • 레지스터 : 명령어를 읽거나 업데이트할 수 있는 저장공간 (i.e. 프로그램 카운터, 스택 포인터)
  • 입출력 정보 : 프로세스가 현재 열어놓은 파일 목록

 

프로세스도 상호작용을 해야한다 (Process API)

아무런 상호작용을 하지 못하는 프로세스는 쓸 이유가 없지.

  • Create : 사용자가 프로세스를 생성할 수 있어야 한다. 예시로 리눅스에서 특정 명령어를 입력했을 때 특정한 프로세스가 실행되어야 한다. 그것이 안 된다면 의미가 없는 명령어이다.
  • Destroy : 프로세스에서 오류가 발생했을 때 사용자는 프로그램을 임의로 종료할 할 수 있어야 한다.
  • Wait :실행이 중단/종료 될 때까지 기다릴 수 있어야 한다. -> 보통 다른 프로세스 때문에 영향 받음 
    • e.g. 파일탐색기에서는 파일을 복사하는 프로세스가 종료될 때까지 기다려야 하는 상황이 있을 수 있다.
  • Miscellaneous Control : 프로세스를 일시중지하고 필요할 경우 다시 시작할 수 있어야 한다.
  • Status : 운영체제는 윈도우 작업관리자와 같은 맥락으로 Status를 확인할 수 있어야한다.

참고로 State, Status를 모두 상태로 해석하면 골치 아프다.

Status - State의 일부분을 관찰하는 전제에서 사용

State - 본질적인 어떠한 상태

 

프로세스의 생성과정은 어떻게 되는가?

 

1. Load : Code & any Static data (e.g., 전역변수)를 메모리에 적재한다. (정확히는 메모리 내부 프로세스 -> 주소 공간)

 

Loading : 시스템 버스를 통해서 디스크 -> 메모리로 로드되는 과정

다음과 같은 로딩 방법이 있다. 

  • Eager loading (비교적 옛 OS들) : 프로그램 실행 전 프로그램 정보를 로딩한다.
  • Lazy loading (비교적 최신 OS들) : 코드의 일부나 데이터 일부를 필요할 때 로딩한다. (cf. paging, swapping 기법)

Eager loading은 디스크를 읽을 필요가 없으니 빠르게 실행된다. 하지만 처음 로딩이 느릴 수 있다.

Lazy loading은 디스크를 계속 읽어야 할 가능성이 생긴다. 느려질 수 있다.

 

그럼에도 불구하고 최신 OS는 lazy 로딩을 많이 채택하는데

-> 실행을 하지 않는 부분을 가져오지 않을 수 있다는 이점을 가질 수 있다.

 

ex) Nvidia Graphic Driver같은 프로그램은 exe 파일 하나가 300mb 이상으로 큰 편이다. 처음에 프로그램을 실행할 때 인터페이스를 통해 다운로드할 기능들을 결정할 수 있는데, 모두 설치하는 경우를 제외하고는 lazy loading이 더 효율적이고 빠르다. 

 

 

2. Stack -> run-time stack을 초기화하고 공간을 할당

c언어의 경우 프로그램이 가장 먼저 실행되는 함수는 main이다. main 함수의 경우 argc, argv의 2개 인자를 가지고 있는데, 시작 전에는 shell에서부터 받은 인자를 들고 실행시켜야 한다. 때로는 인자가 리턴 주소일 수 있다.

 

3. Heap -> 특정 메모리 공간을 동적할당

 

4. Initialization(초기화) -> 유닉스 경우 3가지 입출력 파일 디스크립터(file descriptors)를 개별적으로 할당

0 : 표준 입력 (standard input)

1 : 표준 출력 (standard output)

2. 표준 에러 (standard error)

Process list 공간은 운영체제가 관리한다. PCB에 file descriptor에 대한 정보를 기재하게 되어있음.

 

5. CPU control을 newly-created process 전송

rip로 비유하면 이해가 쉽다.

 

Process States

운영체제가 관리하는, 즉 운영체제 입장에서 프로세스의 (아주 요약된) 상태

process states라는 용어는 일반적으로 프로세스에 관련된 모든 상태값들을 모아놓은걸 의미할 수 있고,

스케쥴링과 관련된 부분에서는 프로세스의 스케쥴러와 관련된 상태에 해당하는 부분을 의미한다.

 

Process States 간단한 예시

 

 

  • Running : CPU에 의해 명령어가 차례대로 실행 중에 있다.
  • Ready : 프로세스가 준비되었지만, 아직 실행을 하고 있지 않은 상태
  • Blocked : 프로세스가 실행할 준비가 되지 않은 상태 (e.g., I/O를 기다리고 있는 상태)

Process State  추적 예시

[Fig 4.3] Evnet

1. Process 0이 실행되고 있기 때문에 Process 1은 Ready 중이다.

2. Process 0이 exit되면 Process 1은 Running된다.

 

[Fig 4.4] Event 

1. Process 0이 실행되다 I/O 요청을 하게 되어 Blocked상태로 옮겨짐

2. Process 0이 Blocked되었으므로 Process 1이 Running된다.

3. Process 0이 I/O 요청이 끝났지만, Process 1이 아직 Running 중이기에 Process 0은 Ready 한다. (Process 1이 선점)

4. Process 1이 exit되면 Process 0이 Running된다.

 

운영체제의 자료구조

운영체제는 앞 개념들을 실현하기 위해 자료구조를 여러개 갖고 있도록 설계되어있다.

 

운영체제는 프로세스의 상태를 유지하기 위해서 process list(task list)를 가지고 있기도 한다.

I/O 이벤트가 끝났거나 특정 프로세스의 점유가 끝났을 경우 process list를 통해 blocked process 정보를 모아서 wake 해주기도 해야한다.

Process Control Block or process descriptor

프로세스에 대한 자료구조를 때때로 PCB 또는 process descriptor로 부르기도 한다.

주로 C 구조체로 작성된다.

 

xv6 proc 구조체

// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
    int eip;
    int esp;
    int ebx;
    int ecx;
    int edx;
    int esi;
    int edi;
    int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
		RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
    char *mem; // Start of process memory
    uint sz; // Size of process memory
    char *kstack; // Bottom of kernel stack
    // for this process
    enum proc_state state; // Process state
    int pid; // Process ID
    struct proc *parent; // Parent process
    void *chan; // If !zero, sleeping on chan
    int killed; // If !zero, has been killed
    struct file *ofile[NOFILE]; // Open files
    struct inode *cwd; // Current directory
    struct context context; // Switch here to run process
    struct trapframe *tf; // Trap frame for the
    // current interrupt
};

 

proc 구조체엔 상태들이 저장된다.

proc_state는 enum 타입으로 프로세스 상태를 6가지로 나타낸다.

 

struct context -> Register context는 프로세스가 종료될 경우 eip, esp, ebx와 같이 중요한 레지스트리 값들을 저장하도록 설계가 되어있다. (int 형으로 구현된걸 보아 이 운영체제는 32bit임을 짐작할 수 있다.)

* context switch : 어떤 프로세스가 정지되었을 경우 그 레지스터 값들을 메모리에 저장한다.

 

또한 앞에서 소개된 3개의 states외에도 다음과 같은 states가 있을 수 있다.

INITIAL : 프로세스가 생성되었음을 알림

FINAL (UNIX-based : ZOMBIE) : 프로세스가 종료되었지만 처리가 되지 않음

 

Final state를 필요로하는 이유

다른 프로세스(특히 부모 프로세스)가 현재 프로세스의 리턴 코드를 확인하는게 가능하다.

 

예를들어 파일복사 프로세스를 구현할 때, 파일복사가 제대로 되었는지 확인하는 체크하는 로직을 포함할 수 있다.

(파일 복사와 관련된 프로세스 리턴 값을 확인)

 

-> 프로세스가 종료되었어도 상태값을 유지할 필요가 있다.

만약 종료되었다면 부모 프로세스는 운영체제에 final call(e.g., wait())를 통해 관련 값들을 clean한다.

 

Summary

운영체제의 기본적인 추상적 개념 프로세스는 컴퓨터에 구현하기 위해 아주 많은 생각이 필요하다.

이런 프로세스를 구현하기 위한 low level mechanisms과 higher-level policies에 대해 공부할 필요가 있다.