[유혁 교수의 운영체제] 5.프로세스

프로세스

### Scope of Operating System

  • 프로세스 관리
  • 메모리 관리
  • 파일 관리
  • I/O 시스템 관리
  • 네트워킹
  • 보안

프로그램과 프로세스

  • 소스코드를 컴파일하면, Object File이 만들어진다. 이것들을 Linker로 묶으면 Executable이 된다. 이것을 Loader로 메모리에 물리적으로 올라오면 프로세스가 되는 것이다. 왼쪽 끝에 있는 소스코드들이 프로그램이고, 오른쪽이 프로세스인 것이다.

  • a.c / b.c / c.c 라는 각각의 소스코드를 컴파일하면 a.o / b.o / c.o 파일들이 만들어지고, 그 파일들에 대하여 상대적인 주소(물리주소x, 일련번호에 가까움)가 부여된다. 이 주소를 갖고 Link를 하면 하나의 Executable 파일이 만들어지고, 여기에 포함된 모든 함수들은 절대적인 주소들이 붙는다(resolve). 절대 주소가 resolve되지 않은 함수들은 Library 뿐이다.

소스코드

  • 소스코드 (.c file)
    • 프로그램이 수행하고자 하는 작업이 프로그래밍 언어로 표현되어 있다.
  • 컴파일러 (Complier)
    • 사람이 이해할 수 있는 프로그래밍 언어로 작성된 소스 코드를 컴퓨터 (CPU)가 이해할 수 있는 기계어로 표현된 오브젝트 파일로 변환
  • 오브젝트 파일 (.o file)
    • 컴퓨터가 이해할 수 있는 기계어로 구성된 파일
    • 이 파일 자체로는 수행이 이루어지지 못하며, 프로세스로 변환되기 위한 정보가 삽입되어야 함
    • 소스코드 하나에 대하여 오브젝트 파일 하나 생성
      • 10개 소스코드 -> 10개 오브젝트 파일
  • 링커 (Linker, Linkage editor)
    • 관련된 여러 오브젝트 파일들과 라이브러리들을 연결하여 메모리로 로드 될 수 있는 하나의 실행파일로 작성
  • 실행파일 (executable file, 윈도우의 경우 : .exe)
    • 특정한 환경(OS)에서 수행될 수 있는 파일
    • 프로세스로의 변환을 위한 header, 작업 내용인 text, 필요한 데이터인 data를 포함한다
    • 오브젝트 파일은 여러 개이더라도 실행 파일은 하나임
      • 이 안의 함수들의 주소는 오브젝트 파일과 마찬가지로 절대 주소
    • Header 형식: COFF, a.out
  • 컴파일러와 링커는 결과물이 수행될 OS와 CPU에 따라 다른 형태의 파일을 만든다

Dynamic Linking vs Static Linking

  • Static Linking : 각각의 소스코드 파일들을 컴파일 한 후 라이브러리들과 함께 Link되는데, 이때, 라이브러리들의 주소가 모두 resolve된다.
    • executable 파일에 있는 Symbol Table에서 라이브러리들을 포함한 모든 함수들과 주소가 매핑되어있다. 커널을 디버깅할 때, 이 Symbol Table을 확인하면서 하면 편리하다.
    • C에서 제공되는 모든 라이브러리들에 대한 주소 정보가 Executable 파일에 저장된다면 파일의 크기가 커진다.
  • Dynamic Linking : Linking 과정에서 라이브러리의 주소를 resolve하지 않고, 메모리에 executable 파일이 올라갔을 때 RTS(RunTime System)에 의해서 동적으로 필요한 라이브러리 함수만 호출
    • Executable 파일에서는 메모리 위에서 라이브러리를 호출하기 위한 Relocation 정보를 포함한다.
    • 상대적으로 Static Linking보다 Executable 파일의 크기가 작다.

Runtime System (RTS)

  • Programming language defines execution model
    • RTS implements it : 프로그래밍 언어가 정의한 수행 모델을 구현하는 역할
    • Behavior not attributable to the program
      • Process memory layout : 프로세스 메모리의 구성
      • Parameter passing between procedures : 파라미터가 어떤 절차에 의해 넘어가느냐
  • Every language has its own RTS
    • ART: android runtime
    • Node.js: Javascript
  • Provide runtime environment
    • In C language
      • Call main() : C에서 존재하는 제일 상위 함수로, C의 RTS가 이 함수를 호출함으로써 프로그램이 수행되기 시작한다.
      • Environment variables : argc, argv - main()이 호출되면서 해당 환경변수들을 넘겨준다.

Process “crafted” from Program

프로그램이 프로세스로 crafted되는 과정을 설명한다. 여기서 crafted란, 아주 정교하게 분리되는 과정을 통해서 다른 무언가로 전환되는 것을 말한다.

  • 프로그램 (executable 파일)
    • header : a.out 형식의 헤더
    • text : 개발자가 작성한 소스코드를 어셈블리어로 번역한 것
    • data : global data
  • 프로그램이 프로세스가 되는 과정
    1. header에서 Text size을 알아낸 다음, 그만큼의 메모리를 확보한다.
    2. 프로그램의 text를 갖고 Text segment를 만든다.
    3. header에서 Data size을 알아낸 다음, 그만큼의 메모리를 확보한다.
    4. 프로그램의 data를 복사하여 메모리에 올린다.
    5. 프로그램에는 없는 bss(initialize되지 않은 전역 변수)의 size을 header에서 알아낸 다음, 그만큼의 메모리를 확보한다. 이렇게 프로세스에서 확보된 Data와 bss까지가 Data Segment이다.
    6. heap(사용자에 의해 동적으로 할당되는 메모리, malloc(3c)를 호출하면 이 힙에서 메모리가 할당된다) / Stack(함수의 호출과 관계되는 지역 변수와 매개변수가 저장되는 영역, 가장 하위 층에서 쌓아올라가는 형태)영역이 메모리 안에서 확보된다.

Process 정의

  • Process – abstraction for
    • Execution unit: 스케줄링의 단위 - CPU에서 동작하는 작업의 단위
    • Protection domain: 서로 침범하지 못함 - 서로 다른 Protectio Domain을 부여받으므로, 서로가 서로를 호출할 수는 없다.
  • Process – implemented with (프로세스는 무엇을 갖고 구현되는가?)
    • Program counter
    • Stack
    • Data section
  • 프로세스는 디스크에 저장된 프로그램으로부터 변환되어 메모리에서 만들어진다

Protection Domain

Protection Domain은 프로세스는 서로 다른 프로세스의 주소를 알더라도 접근할 수가 없도록 보장하는 기술이다.

Protection Domain을 돌파하는 해킹 시도

이 제한을 돌파하려고 하는 행동을 해킹이라고 할 수 있는데, 가장 대표적인 것으로는 Text나 data에 해킹 코드를 심어놓고, 이 프로그램이 Text의 수행 영역을 빠져나와 Data에 있는 코드를 수행하도록 만드는 등의 방법들이 있다. 혹은 해킹 코드를 Heap에 심어놓고 나올 수도 있다. melloc(3c)을 통해 동적으로 메모리를 할당한 후 free()를 호출하지 않고 빠져나오면, 프로세스가 종료된 이후에도 이 영역은 반납되지 않고 그대로 남아있게 되는 데 이것을 memory leak이라고 한다. 이 영역에 해킹 코드를 심어놓음으로써 커널에 문제를 일으킬 수가 있는 것이다.

Process from Program

  • 윈도우에서는 작업관리자 창에 들어가면 각각의 프로세스들을 모두 볼 수가 있다.
  • 리눅스에서는 “ps aux”라는 명령어를 입력하면 현재 수행되는 프로세스들의 목록이 출력된다.
    • 각각의 Command 하나에 Process 하나가 할당된다.
    • Process 1번은 Init이라는 특수한 Command를 수행한다.
    • Command 중에서 k로 시작하는 것들은 커널과 관련된 프로세스들이다.

Q : Command 하나하나가 프로세스인가요? A : 그렇다. 사용자가 명령어를 호출하면 명령어에 해당되는 프로세스가 생성된다.

문맥 전환 (context switch)

  • CPU에서 수행되는 프로세스가 바뀌는 것
    • A 프로세스가 수행되는 도중에 preempt(CPU를 강제로 빼앗는 것) 혹은 voluntary한 전환이 발생하여 B 프로세스가 수행됨
  • 언제 발생하나
    • Time Quantum expires - preempt
      • Time 인터럽트가 발생하여 context를 저장하고, 다른 프로세스를 동작시킴
    • I/O 호출 - voluntary
  • 프로세스 old가 위의 조건에 해당하면 old의 동작을 멈추고, 프로세스 new를 수행한다.
  • 커널에서 문맥 교환을 담아하는 부분을 dispatcher라고 한다.

OS 용어 뒤에 붙는 (1), (2), (3)의 의미

  • (1) : 사용자가 입력하는 명령어 ex) ps(1)
  • (2) : System Call ex) Open(2)
  • (3) : C 라이브러리 ex) init(3)

Context Switch

Context Switch의 동작 원리를 구체적으로 살펴보자.

  1. 프로세스 Old를 수행하다가 Context Switch 인터럽트가 들어오면 state를 PCB라는 공간에 저장한 후, idle 상태가 된다.
  2. 프로세스 new의 state를 PCB로부터 로드한다.
  3. idle하고 있던 프로세스 new가 수행된다.
  4. 프로세스 new의 수행되는 도중에 다시 Context Switch 인터럽트가 들어오면, 다시 state를 PCB에 저장한 후, idle 상태가 된다.
  5. 프로세스 old의 state를 PCB로부터 로드한다.
  6. idle하고 있던 프로세스 old가 다시 수행된다.

이 동작 과정은 concurrent하다. 한 순간에 하나의 프로세스만 수행되고 있지만, 사용자의 눈은 마치 두프로세스가 동시에 수행되는 것처럼 느껴진다.

Q : 세 개의 프로세스가 있다면? A : 특정 스케줄링 Policy가 도입된다.

Q : context switch는 프로세스 중간에 다른 프로세스로 들어간다는 점에서 protection domain이 위반된다고 볼 수 없나요? A : 프로세스 old가 프로세스 new를 호출하는 것이 아니라, 커널이 Old를 CPU에서 내리고 new를 다시 올리는 것이다. Protection Domain 위반은 프로세스가 다른 프로세스를 호출하는 경우를 말한다.

Q : 저 save state와 reload state 과정 사이에 ISR이 있는 건가요? A : 그렇다. Time expire 인터럽트가 들어와서 ISR이 수행되고, 이 ISR에서 디스패치가 호출되고 ISR을 끝난다. 즉, ISR이 직접 Context Switch를 주관하는 것이 아니라, 디스패처를 호출하는 것이다. ISR의 수행 시간은 짧아야하기 때문이다.

Q : idle은 정확히 무슨 상태인가? A : 프로세스가 ready queue에 있을 때의 상태를 말한다.

Q : 멀티프로그래밍과 타임 쉐어링에서 발생하는 모든 프로세스 전환들은 Context Switch라고 볼 수 있나요? A : 그렇다.

Process Control Block

  • Process Control Block(PCB)
    • Each process is represented by PCB
    • Located in kernel memory
    • 프로세스의 정보가 PCB에 저장,로드되는 과정에서 오버헤드가 존재한다.
  • Information associated with each process
    • Process state
    • Program counter
    • CPU registers

Q : 그림에 서술되어 있는 pointer의 의미는 해당 프로세스가 메모리 어느 주소에 위치하는지 나타내는게 맞나요? A : 그렇다.

시스템콜 - 문맥교환 없음

프로세스가 시스템콜을 호출하면 이에 해당하는 트랩이 수행되어 커널 모드 안에서 System call Handler가 동작하는 과정은 동기적인 이벤트이므로 문맥 교환이 발생하지 않는다. 따라서 오버헤드가 적을 수 밖에 없다.

Q : PCB의 크기는 가변적인가요? A : 가변적일 수는 있으나 크기 제한이 있다. PCB는 커널 메모리에 존재하며, 커널 메모리를 initialize를 할 때에 최대 사용공간을 정해야하는 configuration이 존재한다. 커널 메모리는 사용할 영역의 크기를 동적으로 할당하기가 기술적으로 매우 어렵다. 따라서 커널 메모리를 사용하는 PCB도 그런식으로 최대 사용 공간을 정해야 하고, 만약 그 크기를 넘어선다면 Linked List와 같은 자료구조를 통해서 확장하는 수 밖에 없다.

프로세서 구조에 따른 문맥 전환의 차이

  • CISC
    • 복잡한 명령어 셋 구성 -> 효율 높임, 클럭 속도 저하
    • 복잡한 회로 -> 물리적인 공간 차지 -> 레지스터 용량 저하
    • ex) Intel pentium preocessor
  • RISC
    • 간단한 명령어 셋 구성 -> 클럭 속도 높임 -> 빠른 수행 속도
      • 대신 같은 작업을 하는데 많은 명령어가 필요
      • 컴파일러 최적화에 어려움이 있음
    • 절약된 물리적 공간에 보다 많은 레지스터 장착
      • 복잡한 작업을 여러번의 간단한 명령어로 처리하려면 그만큼 많은 레지스터 영역이 필요하기 때문
      • 문맥 전환 시 레지스터 내용 변경에 보다 큰 오버헤드가 생김
        • PCB에 저장되는 레지스터의 개수가 많음 - PCB의 크기가 커짐
      • 예) Sparc, Arm Processor
  • RISC에서 문맥교환 시 발생하는 오버헤드를 줄이기 위한 방법
    1. 하드웨어적으로 레지스터를 dump시키는 instruction을 만든다.
    2. Register Window (Berkeley RICS design) : 레지스터 복사의 과정을 생략하기 위해 더 많은 레지스터를 사용하는 기술

프로세서의 아키텍쳐 종류 - CISC(Complex Instruction Set Computer) vs RICS(Reduced Instruction Set Computer)

  • CISC : 복잡하고 기능이 많은 명령어로 구성된 프로세서로, 복합 명령을 가짐으로써 하위 호환성을 충분히 확보했다. 그러나 성능이 좋지 않고, 전력 소모가 크며 속도가 느리다.
  • RISC : 가장 자주 사용되는 적은 수의 컴퓨터 명령어를 수행하도록 설계된 프로세서로, 좀 더 빠른 속도로 동작한다. 대단히 효율적이고 하드웨어도 간단하지만, 소프트웨어가 복잡하고 크기가 커야 한다. 호환성이 부족하다.

Process State

  • New: process is created
  • Running: process is being executed
    • run queue
  • Sleep: process is waiting for some event to occur such as I/O completion
    • sleep queue
  • Ready: process is watiing to be dispatched to a processor
    • ready queue
  • Terminated: process has finished execution
  • 각 상태마다 큐가 있음

Transition of Process State

각 상태에서 어떤 상태로 전환이 가능한지 확인해라.

  • sleep 상태의 프로세스는 바로 Running 상태가 될 수 없다. 반드시 ready 상태를 거친 후 running 상태가 된다. 즉 I/O가 완료되어 인터럽트가 발생하면 ISR은 해당 프로세스를 sleep queue에서 ready queue로 가져다가 놓는다.

Process Creation

  • 프로세스 생성을 용이하게 하기 위하여
    • 프로그램에서 프로세스가 craft되는 과정에서 본 것처럼 프로세스 생성 과정은 아주 복잡함
    • 기존에 있는 프로세스를 갖고 다른 프로세스를 생성함(fork)으로써 프로세스 생성 과정을 간단히 할 수 있다.
      • fork(2) 라는 시스템 콜을 사용
    • init : 사용자에 의해 생성되는 모든 프로세스들은 이 init을 조상으로 하고 있다.
  • Process Creation by fork(2)
    • Parent process vs. child process
      • 부모 프로세스(Parent Process) : fork을 호출하는 기존 프로세스 자식 프로세스(Child Process) : fork를 통해 새로 생성되는 프로세스로, 부모 프로세스의 복사본
    • Some resources are shared
      • because initially PCB is copied
    • Execution
      • Parent and children execute concurrently - 부모 프로세스와 자식 프로세스는 서로 독립적으로 수행된다.
      • Parent can wait until children termnate (SIGCHLD) - 다만 자식 프로세스가 종료될 때, 부모 프로세스는 이를 알리는 시그널을 받는다.

Process Creation in Memory View

우리가 fork를 하는 이유는 부모 프로세스와 똑같은 자식 프로세스를 생성하려는 것이 아니라, 부모 프로세스에 기반한 새로운 프로세스를 만들려고 하는 것이다. 따라서 단순히 fork(2) 시스템 콜을 통해서 부모 프로세스를 복제하는 것 뿐만 아니라, exec(2) 시스템 콜을 통해 복제된 프로세스에 새로운 프로그램을 로딩해와야 한다.

  1. A new process created by fork system call
  2. A new process (child process) consists of a copy of the memory of the original process(parent process)
  3. Exec(2): to replace the memory with a new program
    • 메모리 안에 있는 프로세스 정보를 다시 작성한다.
      • Text segment, Data 부분에 새로운 프로그램 정보를 가져온다.
      • bss 영역의 크기를 조정한다.
      • heap, stack의 포인터를 초기화한다.

fork의 보안상 취약점

fork가 되었지만 exec이 덜된 복제된 프로세스의 heap과 stack 등에서 기존 프로세스의 정보가 남아있을 수가 있다. 이전 프로세스가 호출한 함수과 아규먼트를 볼 수 있는 잠재적인 여지가 있다.

Process Creation Hierarchy in Linux

각 프로세스들의 부모, 자식 관계를 도식화한 것을 Process Hierarchy 라고 한다. Linux가 boot되고 난 후, 존재하는 프로세스들의 Hierarchy는 다음과 같다.

그림에서 보이는 pagedaemon, swapper, init과 같은 프로세스들은 커널 프로세스로 Linux가 boot되면서 커널이 생성한다. 이후에 사용자에 의해서 만들어지는 프로세스들은 모두 init 프로세스에서부터 fork된다. 또한 이렇게 만들어진 프로세스들 중에서도 몇몇 프로세스들은 필요에 따라 fork가 발생하여 Child process가 생성될 수도 있다.

이렇게 fork된 Child process들은 하나의 process group을 형성한다. process group들은 서로 isolated되어있다.

Q : 프로세스들이 init에서 fork된다면 init은 memory를 거의 차지하지 않는 빈 프로세스의 형태인건가요? A : 비어있는 것은 아니지만, 가장 기본적인 기능은 들어가있는 단순한 프로세스이다.

Q : 프로세스 그룹을 형성한다고 하셨는데 그러면 child 프로세스가 모두 종료되기 전에 parent 프로세스가 종료될 순 없나요? A : 종료될 수 있다. 다만, parent가 종료되면 child도 함께 종료되므로, 왠만하면 parent가 모든 child가 종료될 때까지 기다린다.

Q : fork를 함으로써 프로세스를 생성하는 것이 용이하다고 하였는데 왜 그러한지 궁금합니다. A : 프로세스가 생성될 때에는 프로세스와 관련된 수많은 자료구조들을 하나하나 직접 만들어줘야하는 번거로움 이 있다. 이에 반해 프로세스를 fork하면 자료구조를 만드는 과정을 생략하고, 기존의 있는 자료구조들을 싹 다 복사하기 때문에 메모리 관리면에서 효율적이다. 그런데 복사 과정도 오버헤드가 있을 수 있으므로 실제로는 복사를 아주 일부만 하고 나머지는 exec를 하는 식으로 동작한다.

Q : init프로세스가 exit되면 다른 프로세스들도 모두 exit되는건가요? A : 그렇다.

Process creation in Linux

  • C 프로그래밍을 통해서 간단히 자식 프로세스를 fork할 수가 있다.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
  int counter = 0;
  pid_t pid;
  printf("Creating Child Process\n");
  pid = fork();
  if(pid < 0) { // Error in fork
    fprintf(stderr, "fork failed, errno: %d\n", errno);
    exit(EXIT_FAILURE);
  }
  else if (pid > 0) { // This is Parents Process
    int i;
    printf("Parents(%d) made Child(%d)\n", getpid(), pid);
    for (i=0; i<10; i++) {
      printf("Counter: %d\n", counter++);
    }
  }
  else if (pid == 0) { // This is Child Process
    int i;
    printf("I am Child Process %d!\n", getpid());
    exel("/bin/ls", "ls", "-1", NULL); // Run 'ls -1' at /bin/ls
    for (i=0; i<10; i++) { // Cannot be run
      printf("Counter: %d\n", counter++);
    }
  }
  wait(NULL); // wait for child termination
  return EXIT_SUCCESS;
}
  • 위에서 pid_t는 Process Identifier, 즉 프로세스 식별자로 하나씩 값이 증가하는 일련 번호가 들어간다.
    • init 프로세스의 pid는 1이다.
  • fork()라는 함수는 실패시, -1라는 음수값을 반환하고, 성공시에는 부모 프로세스에서는 자식 프로세스의 pid 값을 반환하며, 자식 프로세스에서는 0을 반환한다.
    • 시스템에 메모리가 부족하거나 하는 등의 특수한 상황을 제외하고는 fork()가 실패할 확률은 극히 적다.
    • 위의 코드에서 fork가 성공했다면, fork()가 리턴되는 순간부터 해당 코드를 똑같이 수행하는 또 하나의 자식 프로세스가 생성될 것이다.
    • 동시에 수행되고 있는 두 개의 프로세스 중, 부모 프로세스에서 변수 pid 값은 자식 프로세스의 pid일 것이므로 해당 조건문을 수행할 것이며, 자식 프로세스에서 변수 pid 값은 0이므로 그에 맞는 조건문을 수행할 것이다.
  • getpid()는 현재 이 코드를 수행하는 프로세스의 pid를 반환한다.
  • execl() 함수는 exec 관련 작업을 수행하는 여러 함수 중 하나이다.
    • 위의 코드에서 execl은 자식 프로세스를 디렉토리 안의 파일 목록을 출력하는 프로세스로 초기화한다.
    • 따라서 자식 프로세스에서는 execl() 함수 호출 다음에 이어 나오는 코드는 수행되지 않는다.
  • 부모 프로세스에서는 wait(NULL)이 호출되는데, 이는 자식프로세스의 종료 시그널을 받을 때까지 다음 코드를 수행하지 않고 기다리는 것을 의미한다. 이를 통해서 자식 프로세스가 모두 끝나고 난 후 부모 프로세스도 이어 종료할 수 있도록 한다.

Q : fork(2)라는 syscall은 값을 하나만 리턴해줄텐데 왜 fork()라는 함수의 반환 값은 parent 프로세스와 child 프로세스에서 각각 다른 것인지 궁급합니다. A : syscall의 값을 리턴해주는 것은 커널인데, 커널은 각각의 프로세스들 중 무엇이 부모이고 자식인지 알고 있기 때문에 각각의 문맥에 필요한 값을 리턴해줄 수가 있다.

Process creation in Windows

윈도우에서 리눅스에서의 fork 역할을 하는 system call은 CreateProcess이다.

  • CreateProcess Prototype

윈도우에서 fork를 하기 위한 간단한 C 프로그래밍 예제도 살펴보자.

#include <stdio.h>
#include <windows.h>
enum{loop = 100};
int main(int argc, char *argv[]) {
  STARTUPINFO si; // 구조체 정보를 전달하기 위한 변수
  PROCESS_INFORMATION pi; // 구조체 정보를 반환받기 위한 변수
  ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
  ZeroMemory(&si, sizeof(STARTIPINFO));
  si.cb = sizeof(si);
  si.lpReserved = NULL;
  si.lpTitle = L"ChildProcess.exe";
  si.lpDesktop = NULL;
  si.dwx = 0;
  printf("Parent Process ID: %d\n", GetCurrentProcessId());
  if(!CreateProcess(L"ChildProcess.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { // fork 실패시 실행됨
    
    fprintf(stderr, "Create Process failed (%d)\n", GetLastError());
    return EXIT_FAILURE;
  }
  
  // fork 성공시 부모 프로세스에서 실행됨
  printf("Child Process ID: %d\n", pi.dwProcessId);
  {
    int i;
    for(i=0;i<loop;++i){
      printf("PID: %d, i: %d\n", GetCurrentProcessId(), i);
    }
  }
  WaitForSingleObject(pi.hProcess, INFINITE);
  printf("child process exit\n");
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
  return EXIT_SUCCESS;
}
  • CreateProcess() 함수는 아주 많은 아규먼트를 필요로 하며, 자식 프로세스가 무엇을 수행할지(exec)를 모두 아규먼트를 통해 한번에 정한다.
    • 이때 “ChildProcess.exe”는 자식 프로세스가 수행할 코드로 구성된 executable 파일의 이름이다.
    • 리눅스에서는 fork() 호출 후에 exec() 함수에서 아규먼트로 넘겨주었던 코드의 path를, 여기서는 CreateProcess() 함수에서 한번에 넘겨준다.
    • CreateProcess()의 반환값이 false이면 fork가 실패했음을 의미힌다.
  • STARTUPINFO 구조체 변수인 si의 속성을 초기화한 다음 이 정보를 인자로 전달하여 CreateProcess를 호출하면 프로세스가 생성된다. 이 생성된 프로세스의 정보를 PROCES_INFORMATION 구조체에 담는다.
  • fork가 성공하면 fork 실패시 실행되는 조건문 다음에 이어오는 코드가 실행된다. 이 때 자식 프로세스가 종료될 때까지 기다리는 것을 Object 개념과 관련된 WaitForSingleObject() 함수 호출을 통해 구현한다. 그 이어서 오는 CloseHandle()의 호출도 필요하다.

STARTUPINFO vs PROCESS_INFORMATION

  • STARTUPINFO : 생성하는 프로세스의 속성을 지정하기 위한 구조체 변수로, 사용자는 이 변수의 속성을 초기화한 다음, 이 변수의 포인터를 인자로 전달한다.
  • PROCESS_INFORMATION : 생성하는 프로세스의 정보를 얻기 위해 사용되는 인자로, 전달된 다른 변수의 주소값이 가리키는 변수에 프로세스 정보가 채워진다.

다음은 ChildProcess.c 파일의 소스이고, 이것을 컴파일한 결과는 ChildProcess.exe가 되어 자식 프로세스가 이를 수행하게 될 것이다.

#include <stdio.h>
#include <windows.h>
enum{loop = 100};
void ProcExit(void) {
  printf("process exit\n");
}
int main(int argc, char *argv[]) {
  int i;
  atexit(ProcExit);
  for(i=0;i<loop;++i) {
    printf("PID: %d, i: %d\n", GetCurrentProcessId(), i);
  }
  return EXIT_SUCESS;
}
  • 리눅스와 윈도우의 차이점 : 리눅스와 윈도우에서 fork 과정은 비슷할 수 있으나, 각 과정에 대한 Semantics가 다르다고 할 수 있다. 이러한 Semantics의 차이점은 각 OS의 근본적인 설계에서의 차이를 가져온다.
    • 리눅스는 fork()의 결과 값에 따라 조건문으로 부모 프로세스와 자식 프로세스를 구분하고, 자식프로세스에 직접 exec() 함수를 통해 수행할 작업을 정해주었다면, 윈도우는 CreateProcess()에서 한 번에 자식 프로세스에서 수행할 작업을 정해준다는 차이점이 있다.
    • 리눅스에서는 자식 프로세스의 종료를 기다리는 기능을 단순히 wait() 함수의 호출을 통해 구현할 수 있지만, 윈도우는 WaitForSingleObject(), CloseHandle() 함수의 호출들이 조금더 복잡하게 필요하다. 그렇다고 해서 내부에서 일어나는 과정이 윈도우가 리눅스보다 복잡하다는 의미는 절대 아니다.

Process Termination

프로세스 사이클에서 termination이 중요한 이유는 프로세스가 사용하고 있는 자원, 그 중에서도 특히 커널 안에서 사용되는 자료구조를 반납하지 않으면 커널 메모리가 소진되기 때문이다.

  • Process Termination
    • A process terminates when it finishes executing its final statement and asks kernel to delete it by using exit system call.
      • 그렇다면 왜 우리는 직접 exit을 호출하지 않는데도 프로세스들이 정상적으로 종료되는 것일까?
        • RTS(Runtime System)이 프로세스에 대한 Control 권한을 받고, main()를 호출해준 후, main이 리턴되었을 때 exit을 호출해준다.
      • Output data from child to parent(via wait)
      • Process’ resources are deallocated by kernel
      • 만약 exit(2)이 호출되지 않는다면
    • The abort function causes abnormal process termination.
      • 이 함수는 비정상적인 프로세스의 종료가 일어났을 때, 이를 부모 프로세스에게 알려주는 역할을 한다.
      • The SIGABRT signal is sent to the calling process.
      • core dump is made.

Q : c에서는 동적할당된 메모리를 반환하지 않으면 안되는것처럼 exit을 하지 않으면 커널메모리를 쓸수 없나요? 아니면 os에도 python이나 java처럼 커널메모리를 관리하는 방법이 있나요? A : RTS이 커널 메모리를 자동적으로 반납해준다.

Cooperating Processes

자식 프로세스를 fork하는 이유 중 하나는 부모 프로세스에서 필요한 일들을 자식 프로세스에게 나눠줌으로써 여러개의 프로세스가 한 작업을 위해 서로 협력하도록 하기 위함이다.

딥러닝과 같이 높은 효율을 필요로 하는 시스템에서는 Cooperating process 개념을 활용하며, 각 프로세스는 worker라고 불린다.

  • Independent process cannot affect or be affected by the execution of another process.
  • Adventages of process cooperation.
    • Information sharing
    • Computation speed-up
    • Modularity
    • Convenience
  • 서로 다른 프로세스가 서로 협력하기 위해서는 공통적으로 필요한 데이터를 서로 공유할 수 있어야 하는데, protection domain으로 인해 프로세스들은 서로 데이터를 주고 받지 못한다. 이를 해결하기 위한 방법들이 존재하며 나중에 배우게 될 것이다.

Process and Container

  • Process has
    • pid: process id
    • uid: user id - important in file access
      • 규모가 큰 하드웨어를 여러 명의 사용자가 나눠 사용해야 할 때에 uid가 매우 중요하다.
      • 특정 file에 대한 접근 권한을 uid를 통해 확인한다.
    • Originally, kernel generates them from “a single namespace”
      • 하나의 namespace 안에서 특정 번호를 가진 프로세스는 그것이 유일하다
        • Single namespace를 원칙으로 하는 커널에서 발생하는 프로세스들은 모두 같은 namespace 안에서 동작하며, 모든 프로세스는 각각 유일한 pid와 uid를 갖는다.
      • multiple namespace : 특정 번호를 가진 프로세스가 여러 곳에 분산되어 존재할 수가 있다. 이를 가능하게 해주는 기술이 Container이다.
  • Now, each process is associated with a namespace - 하나의 프로세스는 오로지 하나의 namespace 안에 존재한다.
    • can only see or use the resources associated with that namespace - 따라서 프로세스는 자신이 속한 namespace에 있는 자원에만 접근이 가능하다.
  • Container is a process
    • 컨테이너는 multiple namespace를 구현 - 각 컨테이너마다 같은 번호를 가진 프로세스가 존재할 수 있다.
    • with its own namespaces
      • pid, uid
      • Network, mount
    • 가상화를 통한 isolation - So a process is isolated from other (sibling) processes
      • In addition, resources can be limited via control group

Q : multi name space에서도 같은 namespace에서는 같은 pid를 가질 수 없게 되는가요? A : 그렇다. 같은 namespace에서는 pid가 중복될 수 없다.

Q : container라는 개념이 만들어 지면서 process별로 다른 namespace를 가질 수 있게 된건가요 아니면 process별로 다른 namespace를 가질 수 있게 되면서 container라는게 만들어 진건가요? A : Container와 namespace라는 개념은 별도로 발전했고, 리눅스에서 두 개념이 연결되었다고 생각하면 된다. 연구적인 관점에서는 엄밀히는, namespace라는 개념이 더 먼저 발전했다고 할 수 있다.

Comments