본문 바로가기
3학년 학사/운영체제

[운영체제] #4. Process

by whiteTommy 2024. 11. 20.
반응형
더보기

목차

  • Process Management
  • Memory Management
  • File Management
  • I/O System Management
  • Networking
  • Security

 

프로그램이 만들어지는 과정은 아래와 같다.

 

Compiler

: 사람이 이해할 수 있는 프로그래밍 언어로 작성된 Source Code 를 컴퓨터(CPU) 가 이해할 수 있는 기계어로 표현된 Object 파일로 변환

  • Source Code (c file) : 프로그램이 수행하고자 하는 작업이 프로그래밍 언어로 표현되어 있음
  • Object file (o file) : 컴퓨터 (CPU) 가 이해할 수 있는 기계어로 구성된 파일
    • 자체로는 수행이 안됨
    • 프로세스로 변환되기 위한 정보가 삽입되어야 함
    • Relocatable Addresses (Relative Address) 로 표현 : symbol 주소가 상대적인 값으로 표현됨
      ex) 시작 주소로부터 26 byte 지점

  • Cross Compiler : 한 platform 에서 다른 platform 용 실행 파일을 생성하는 compiler
    ex) x86 기반 컴퓨터에서 ARM 기반 기기의 실행 파일을 생성

Source Code -> Cross Compiler -> Target Platform executable file (예 : a.out)

 

Compilation vs. Translation

컴파일러와 인터프리터 모두 high-level language를 machine language로 번역한다. 컴파일러는 소스 코드(high-level language로 작성) 전체를 링커 등을 통해 한번에 번역하여 목적 파일(기계어로 작성)로 만들어 메모리상에 적재한다.

인터프리터는 소스 코드를 한 행씩 중간 코드로 번역 후 실행한다.

  1. 컴파일러는 소스코드 전체를 컴퓨터 프로세서가 실행할 수 있도록 바로 기계어로 변환한다. 인터프리터는 고레벨 언어를 중간 코드(intermediate code)로 변환하고 이를 각 행마다 실행한다.
  2. 일반적으로 컴파일러가 각 행마다 실행하는 특성을 가진 인터프리터보다는 실행시간이 빠르다.
  3. 컴파일러전체 소스코드를 변환 한 뒤 에러를 보고하지만 인터프리터각 행마다 실행하는 도중 에러가 보고되면 이후 작성된 코드를 살펴보지 않는다. 이는 보안적인 관점에서 도움이 된다.
    ex) 파이썬은 인터프리트 언어이고 C, C++는 컴파일 언어이다. 자바는 컴파일러와 인터프리터 모두 사용

  • Rosetta 의 번역 한계
    • 대부분의 intel 기반 앱과 JIT 컴파일러를 포함하는 앱을 번역할 수 있지만, kernel extension 과 x86_64 컴퓨터 platform 을 virtualize 하는 가상 머신 앱을 번역하지는 못한다.

 

Linker

: Object file 들과 library 들을 연결하여, memory 로 load 될 수 있는 하나의 executable file 로 변환한다.

  • Executable (exe file)
    • 특정한 환경(OS) 에서 수행될 수 있음
    • process 로의 변환을 위한 Header, 작업 내용인 Text, 필요한 데이터인 Data 를 포함
    • Absolute Addresses 로 표현

 

Loader

: Executable file 을 실제 memory 에 올려주는 역할

  1. Executable 의 Header 를 읽어, Text 와 Data 의 크기를 결정
  2. 프로그램을 위한 Address Space 를 생성
  3. 실행 명령어와 Data 들을 Executable 로부터 생성한 Address Space 로 복사
  4. 프로그램의 argument 들을 stack 으로 복사
  5. CPU 내 Register 를 초기화하고, Start-up Routine (프로그램 시작 지점) 으로 Jump 하여 실행 시작

 

 

Program 이 process 가 되어 memory 에 load 되기 위해서 process 는 input queue 에서 대기한다. input queue 에서 대기하던 process 중 선택된 process 는 main memory 에 load 되고, process 가 execute 할 때, 해당 main memory 에서 명령어와 데이터를 사용한다.

 

Process 의 address 를 어느 시점에 어떻게 할당해줄 것인지에 (Symbolic address -> Logical Address -> Physical Address) 에 대해 결정해주는 것이 Address Binding 이다. 이에 대한 자세한 설명은 이후에 다시 다루기로 한다.

 

 

Runtime System

: 프로그램의 실행을 지원하고, 효율성을 보장하기 위해, 프로그램과 운영 체제 간의 연결을 담당한다.

  • C Runtime System Program Execution
    • GCC 컴파일러는 Start-up Code Object file 을 추가하여 program 을 comfile 하며, 이 때 기본 library 들도 동적으로 링킹된다.
    • process 를 시작하기 위해 kernel 은 program counter (다음에 실행될 명령어의 주소를 가지고 있는 register)를 _start 함수의 주소로 지정한다. // queue 구조를 사용
    • _start 함수는 동적으로 링킹된 C library 및 thread 환경을 초기화하기 위해 _libc_start_main 함수를 호출한다.
    • library 초기화를 진행한 이후, 프로그램의 main 함수가 호출된다.

 

Process

  • Execution Unit : 스케줄링의 단위
  • Protection Domain : 서로 침범하지 못함
  • Program Counter, Stack, Data section 을 통해 구현된다.
  • disk 에 저장된 program 으로부터 변환되어 memory 로 loading 된다.

  • Text Segment
    • 실행 명령어가 저장된 공간
    • PC 와  Process 의 레지스터 내용을 포함
    • 읽기 전용으로 설정되어 변경을 방지
  • Data Segment
    • 초기화된 전역 변수와 상수가 저장
    • 읽기-쓰기 권한을 가짐
  • BSS (Block Started by Symbol)
    • 정적으로 할당된 초기화되지 않은 변수를 포함
  • Heap
    • 동적 메모리 할당(malloc, new)에 사용
  • Stack
    • 함수 호출 및 지역 변수 저장

각 process 는 protection domain 을 통해 memory 접근이 제한된다. 

 

 

Process Execution in xv6

  • Xv6 code exec.c
    exec() 시스템 호출은 사용자의 주소 공간을 초기화하고, 파일 시스템에 저장된 실행 파일을 읽어와 메모리에 로드하는 작업을 수행한다. 아래 코드를 살펴보자.
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "defs.h"
#include "x86.h"
#include "elf.h"

int
exec(char *path, char **argv)
{
  char *s, *last;
  int i, off;
  uint argc, sz, sp, ustack[3+MAXARG+1];
  struct elfhdr elf;
  struct inode *ip;
  struct proghdr ph;
  pde_t *pgdir, *oldpgdir;
  struct proc *curproc = myproc();

  begin_op();

  if((ip = namei(path)) == 0){
    end_op();
    cprintf("exec: fail\n");
    return -1;
  }
  ilock(ip);
  pgdir = 0;

  // Check ELF header
  if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf))
    goto bad;
  if(elf.magic != ELF_MAGIC)
    goto bad;

  if((pgdir = setupkvm()) == 0)
    goto bad;

  // Load program into memory.
  sz = 0;
  for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
    if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph))
      goto bad;
    if(ph.type != ELF_PROG_LOAD)
      continue;
    if(ph.memsz < ph.filesz)
      goto bad;
    if(ph.vaddr + ph.memsz < ph.vaddr)
      goto bad;
    if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;
    if(ph.vaddr % PGSIZE != 0)
      goto bad;
    if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
      goto bad;
  }
  iunlockput(ip);
  end_op();
  ip = 0;

  // Allocate two pages at the next page boundary.
  // Make the first inaccessible.  Use the second as the user stack.
  sz = PGROUNDUP(sz);
  if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
    goto bad;
  clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
  sp = sz;

  // Push argument strings, prepare rest of stack in ustack.
  for(argc = 0; argv[argc]; argc++) {
    if(argc >= MAXARG)
      goto bad;
    sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
    if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
      goto bad;
    ustack[3+argc] = sp;
  }
  ustack[3+argc] = 0;

  ustack[0] = 0xffffffff;  // fake return PC
  ustack[1] = argc;
  ustack[2] = sp - (argc+1)*4;  // argv pointer

  sp -= (3+argc+1) * 4;
  if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
    goto bad;

  // Save program name for debugging.
  for(last=s=path; *s; s++)
    if(*s == '/')
      last = s+1;
  safestrcpy(curproc->name, last, sizeof(curproc->name));

  // Commit to the user image.
  oldpgdir = curproc->pgdir;
  curproc->pgdir = pgdir;
  curproc->sz = sz;
  curproc->tf->eip = elf.entry;  // main
  curproc->tf->esp = sp;
  switchuvm(curproc);
  freevm(oldpgdir);
  return 0;

 bad:
  if(pgdir)
    freevm(pgdir);
  if(ip){
    iunlockput(ip);
    end_op();
  }
  return -1;
}

 

  • 주요 변수
    • path : 실행할 binary 파일 경로
    • argv : 실행 시 전달할 명령어 인자
    • elf : ELF 파일의 헤더를 저장하는 구조체
    • elfhdr : 실행 파일의 ELF 헤더를 저장
    • progphr : ELF 프로그램의 헤더를 저장
    • ustack : 사용자 스택 초기화에 사용되는 임시 배열
    • ph : 프로그램 헤더(program header) 를 저장하는 구조체
    • pgdir : 페이지 디렉터리
    • curproc : 현재 실행 중인 프로세스를 나타내는 구조
    • phoff : 프로그램 헤더 테이블의 시작 위치(오프셋)
    • phnum : 프로그램 헤더 개수

  • 코드 흐름 분석
    • 파일 검색 및 잠금
begin_op();

if((ip = namei(path)) == 0) {
  end_op();
  cprintf("exec: fail\n");
  return -1;
}
ilock(ip);
pgdir = 0;
  • namei(path) : 주어진 경로를 통해 해당 파일의 inode 를 가져온다. 파일이 존재하지 않을 경우 NULL 반환
  • ilock(ip) : 파일 시스템에서 해당 파일을 잠근다. 다른 process 가 이 파일을 수정하거나 삭제하지 못하게 한다.

    • ELF 헤더 검증
if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf))
  goto bad;
if(elf.magic != ELF_MAGIC)
  goto bad;
  • readi (ip, (char*)&elf, 0, sizeof(elf)) : 실행 파일에서 ELF 헤더를 읽어 elf 구조체에 저장
  • elf.magic != ELF_MAGIC : ELF 헤더의 매직 넘버를 확인하여 올바른 ELF 파일인지 검증

    • 새로운 페이지 디렉터리 생성
if((pgdir = setupkvm()) == 0)
  goto bad;
  • setupkvm() : 새로운 커널 가상 메모리 페이지 디렉터리를 생성해서 사용자 주소 공간의 초기화에 사용한다.

    • 프로그램 메모리 로드
for(i = 0, off = elf.phoff; i < elf.phnum; i++, off += sizeof(ph)) {
  if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph))
    goto bad;
  if(ph.type != ELF_PROG_LOAD)
    continue;
  if(ph.memsz < ph.filesz)
    goto bad;
  if(ph.vaddr + ph.memsz < ph.vaddr)
    goto bad;
  if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
    goto bad;
  if(ph.vaddr % PGSIZE != 0)
    goto bad;
  if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
    goto bad;
}
  • 작업
    • 반복문에서 ELF 파일에 포함된 여러 프로그램 헤더를 순회하며 각 세그먼트를 메모리에 적재하는 데 사용한다. 
    • ELF 프로그램 헤더를 읽어 프로그램의 각 segment 를 메모리에 적재
    • allocuvm() : 사용자 메모리 공간을 할당
    • loaduvm() : 파일 내용을 사용자 메모리에 복사
  • 조건 검사
    • ph.type != ELF_PROG_LOAD: 실행 가능한 세그먼트만 메모리에 로드
    • segment 크기 (memsz >= filesz), 정렬 조건(vaddr % PGSIZE == 0) 등을 검증

  • 사용자 스택 설정
sz = PGROUNDUP(sz);
if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
  goto bad;
clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
sp = sz;
  • clearpteu() : 페이지 테이블 엔트리에서 사용자 접근 권한을 제거
  • sp = sz : 스택 포인터를 두 번째 페이지의 최상단으로 설정

 

  • 명령줄 인자 복사
for(argc = 0; argv[argc]; argc++) {
  if(argc >= MAXARG)
    goto bad;
  sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
  if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
    goto bad;
  ustack[3+argc] = sp;
}
ustack[3+argc] = 0;

ustack[0] = 0xffffffff;  // fake return PC
ustack[1] = argc;        // 인자 개수
ustack[2] = sp - (argc+1)*4;  // argv 배열 포인터

 

  • sp - (strlen(argv[argc]) + 1):
    • 문자열의 길이만큼 스택 포인터를 아래로 이동.
    • 추가로 +1을 사용해 문자열 종료 문자(\0) 포함.
  • & ~3:
    • 스택을 4바이트 정렬(Alignment)합니다. 메모리 정렬을 통해 성능을 최적화.
  • copyout():
    • 커널에서 사용자 공간으로 데이터를 복사.
    • 명령줄 인자 문자열을 사용자 스택에 복사.
  • 스택에 추가로 다음 데이터를 초기화
    1. ustack[0]: 가짜 반환 주소 (return PC). 함수 호출에서 돌아오지 않도록 설정.
    2. ustack[1]: 명령줄 인자 개수 (argc).
    3. ustack[2]: 명령줄 인자 배열의 포인터 (argv).
    4. ustack[3+argc] : argv 배열의 끝을 나타내는 NULL 포인터 (0으로 무한 루프 방지)

 

  • 스택 복사
sp -= (3+argc+1) * 4;
if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
  goto bad;

 

  • sp -= (3 + argc + 1) * 4
    • 스택 포인터(sp)를 현재 위치에서 데이터 크기만큼 감소시켜, 새로운 데이터가 저장될 공간을 확보.
    • 데이터는 스택의 최상단에 저장되므로, 스택 포인터를 아래로 이동시킴(스택은 위에서 아래로 성장).
  • (3 + argc + 1) * 4: 스택에 저장할 데이터의 전체 크기를 계산
    • 예제
./program arg1 arg2
  • ustack[0] : 0xffffffff
  • ustack[1] : 3 (argc)
  • ustack[2] : argv 배열의 시작 주소.
  • ustack[3] : "./program"의 주소
  • ustack[4] : "arg1"의 주소
  • ustack[5] : "arg2"의 주소
  • ustack[6] : NULL (배열 끝)
(3 + argc + 1) * 4
= (3 + 3 + 1) * 4
= 7 * 4
= 28 바이트
+---------------------+ <- 스택 최상단 (sp 초기값)
| NULL                | <- `ustack[6]`
+---------------------+
| "arg2" 주소         | <- `ustack[5]`
+---------------------+
| "arg1" 주소         | <- `ustack[4]`
+---------------------+
| "./program" 주소    | <- `ustack[3]`
+---------------------+
| argv 시작 주소      | <- `ustack[2]`
+---------------------+
| argc 값             | <- `ustack[1]`
+---------------------+
| Fake Return PC      | <- `ustack[0]`
+---------------------+

 

  • 프로그램 이름 저장
  // Save program name for debugging.
  for(last=s=path; *s; s++)
    if(*s == '/')
      last = s+1;
  safestrcpy(curproc->name, last, sizeof(curproc->name));
  • 작업
    • 문자열 path를 순회하며 가장 마지막에 등장하는 / 문자를 찾는다. 마지막 / 이후의 문자열이 프로그램의 이름이 된다. 루프가 끝나면 last 는 프로그램 이름의 시작 위치를 가리킨다.
    • sfaestrcpy (curproc->name, last, sizeof(curproc->name)) : 복사할 문자열의 크기를 초과하지 않도록 안전하게 복사한다.

  • 사용자 이미지 커밋
  // Commit to the user image.
  oldpgdir = curproc->pgdir;
  curproc->pgdir = pgdir;
  curproc->sz = sz;
  curproc->tf->eip = elf.entry;  // main
  curproc->tf->esp = sp;
  switchuvm(curproc);
  freevm(oldpgdir);
  return 0;
  • 오류 처리 블록
 bad:
  if(pgdir)
    freevm(pgdir);
  if(ip){
    iunlockput(ip);
    end_op();
  }
  return -1;


Process State

  • New : Process 생성 중
  • Running : CPU 에서 실행 중
  • Waiting : 특정 이벤트(I/O 완료 등)를 대기 중
  • Ready : 실행 준비 완료
  • Terminated : 실행 종료

 

위의 그림과 같이 kernel 내에 Ready Queue, Waiting Queue, Running Queue 를 두고 Process 들을 상태에 따라 관리.

  • 기본 흐름 : new -> ready -> ready queue -> scheduler dispatch -> running 

 

Process State in xv6

  • xv6 proc.h
 enumprocstate { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
 // Per-process state
     struct proc {
     uint sz; // Size of process memory (bytes)
     pde_t* pgdir; // Page table
     char *kstack; // Bottom of kernel stack for this process
     enumprocstate state; // Process state
     int pid; // Process ID
     struct proc *parent; // Parent process
     struct trapframe *tf; // Trap frame for current syscall
     struct context *context; // swtch() here to run process
     void *chan; // If non-zero, sleeping on chan
     int killed; // If non-zero, have been killed
     struct file *ofile[NOFILE]; // Open files
     struct inode *cwd; // Current directory
     char name[16]; // Process name (debugging)
 };
  • enum procstate
    • UNUSED : 사용되지 않은 상태
    • EMBRYO : 프로세스가 생성 중인 상태
    • SLEEPING : 실행을 멈추고 이벤트를 기다리는 상태
    • RUNNABLE : 실행 가능하지만 CPU 가 아직 할당되지 않은 상태
    • RUNNING : 현재 CPU 에서 실행 중인 상태
    • ZOMBIE : 종료되었지만 부모 프로세스가 아직 리소스를 회수하지 않은 상태
  • proc 구조체
    • sz : 프로세스 메모리 크기
    • pgdir : 페이지 디렉토리
    • kstack : kernel 스택
    • state : 현재 프로세스 상태
    • pid : 프로세스 id
    • parent : 부모 프로세스
    • tf : CPU 레지스터 상태 저장
    • context : kernel thread 에서의 실행 상태 저장
    • chan : 프로세스가 대기하는 채널(이벤트)
    • name : 디버깅용으로 프로세스 이름 저장

 

Process Control Block (PCB)

  • OS 가 각 프로세스를 관리하기 위해 사용하는 데이터 구조
  • xv6 에서는 struct proc 가 PCB 역할을 한다

  • PCB 에 저장되는 정보
    • 프로세스 상태
    • 프로그램 카운터 : 다음에 실행될 명령어의 주소를 가지고 있는 register
    • CPU 레지스터 값
    • CPU 스케줄링 정보
    • 메모리 관리 정보
    • 입출력 상태 정보
    • 열린 파일 목록

 

Context Switch

  • CPU 가 한 프로세스에서 다른 프로세스로 전환될 때 발생
  • 현재 프로세스의 실행 상태를 저장(기존 프로세스 상태 저장)하고, 새 프로세스의 저장된 상태를 복구 (새로운 프로세스 로드)
  • context switching 오버헤드
    • CPU 가 유용한 작업을 수행하지 못하는 시간
    • 하드웨어 지원 여부에 따라 속도가 결정됨

 

프로세서 구조에 따른 Context Switching 차이

  1. CISC
    • 복잡한 명령어 지원
    • 실행 속도가 느릴 수 있음
      ex) Intel Pentium Processor
  2. RISC
    • 간단한 명령어 집합
    • 실행 속도가 빠름
    • 레지스터 윈도우를 사용하여 context switching 속도를 높임
      ex) ARM Processor

물리적으로 존재하는 모든 레지스터

 

 

위의 2가지 개념은 CPU(중앙처리장치) 를 설계하는 방식이다. CPU가 작동하려면 프로그램이 있어야 하고 명령어를 주입해서 설계를 한다.

  • 일반적인 레지스터 교체 방식 (CISC):
    • 기존 CISC 프로세서에서는 함수 호출 시 레지스터 값을 스택에 저장하고, 복구할 때 다시 로드해야 한다.
    • 이 과정은 시간이 많이 소요되고, 문맥 전환 오버헤드의 주요 원인이 된다.
  • 레지스터 윈도우 방식 (RISC):
    • 레지스터 윈도우를 사용하면 스택에 데이터를 저장하지 않고, 물리적 레지스터에서 윈도우 포인터만 변경하여 새로운 레지스터 세트를 활성화한다.
    • 그림에서 보이는 슬라이딩 윈도우 방식으로, 함수 호출 시 새로운 레지스터 세트를 활성화하고, 이전 세트와 중첩 영역을 통해 데이터를 전달한다.

 

 

Context Switch 도식

 

  • 동작 흐름
    • 프로세스 $P_{0}$ 실행 중 시스템 콜 또는 인터럽트 발생
    • 프로세스 $P_{0}$ 의 상태 저장 (PCB 에 저장)
    • 프로세스 $P_{1}$ 의 상태 복구 (PCB 에서 복구)
    • 프로세스 $P_{1}$ 실행
    • 반복 . . .

 

Quantifying Context Switch Overhead

 

 

  • 인공지능 워크로드(클라우드와 엣지 환경)에서 문맥 전환의 성능 영향을 분석.
  • 클라우드와 엣지 환경에서 문맥 전환 비용이 다르게 나타남.

  • Cloud vs. Edge
    • 클라우드에서는 context switching 비용이 낮은 반면, 엣지 디바이스에서는 비용이 높음
      => 워크로드 특성에 따라 오버헤드가 달라짐

 

 

위 그래프는 lmbench라는 벤치마크 도구를 사용하여 문맥 전환(Context Switch) 및 비문맥 전환(Non-Context Switch)의 오버헤드를 평가한 결과를 나타낸다. 그래프는 데이터 크기와 쓰레드 수에 따라 오버헤드를 분석하며, 문맥 전환과 관련된 성능 영향을 비교한다.

 

  • Non-Context Switch의 경우 데이터 크기가 크면 일정한 오버헤드가 발생하며, 쓰레드 수 증가에 따른 영향은 미미
  • Context Switch의 오버헤드는 데이터 크기와 쓰레드 수가 증가함에 따라 선형적으로 증가한다.
  • 데이터 크기가 클수록 쓰레드 수 증가에 따른 오버헤드가 더 명확하게 나타난다.

 


(a) Image Recognition Applications

  1. Process switch (파란 선):
    • 문맥 전환 빈도가 시간에 따라 크게 변화하며, 특정 시간 구간에서 18,000/s 이상의 높은 빈도를 기록.
    • AI 워크로드가 다양한 프로세스 간 전환을 자주 요구하는 경우, 문맥 전환 횟수가 급증.
    • 예를 들어, MobileNet v2와 같은 워크로드에서는 문맥 전환 빈도가 낮고, VGG16과 같은 더 복잡한 네트워크에서는 문맥 전환 빈도가 크게 증가.
  2. CPU utilization (주황 선):
    • CPU 사용률은 문맥 전환과 상관없이 일정하게 유지되며, 약 30% ~ 50% 사이.
    • 복잡한 네트워크가 실행될 때도 CPU 사용률이 상대적으로 낮게 유지됨. 이는 AI 워크로드가 GPU 중심적이고, CPU는 주로 제어 및 관리 작업을 수행하기 때문.

(b) Image Processing and Object Segmentation Applications

 

  • Process switch (파란 선):
    • 문맥 전환 빈도가 구간별로 큰 변화를 보이며, 특정 시간대에는 0/s에 가까운 빈도로 감소.
    • SRCNN과 같은 가벼운 네트워크는 문맥 전환 빈도가 낮고, PSPNet과 같은 복잡한 네트워크에서는 빈도가 크게 증가.
    • 특히, DeepLab과 같은 고급 객체 분할 네트워크에서는 문맥 전환 빈도가 약 18,000/s로 치솟음.
  • CPU utilization (주황 선):
    • 문맥 전환 빈도와 다르게 CPU 사용률은 상대적으로 일정하게 유지.
    • 객체 분할 워크로드는 GPU에서 실행되는 연산량이 크기 때문에, CPU 사용률은 여전히 낮음 (20% ~ 40% 사이).

(C) Process Number and Interrupt on Nvidia Jetson TX2

 

  • Process number (녹색 선):
    • 실행 중인 프로세스 수는 약 530 ~ 570 사이에서 일정하게 유지됨.
    • AI 워크로드의 종류에 상관없이 Jetson TX2에서 기본적으로 실행되는 프로세스 수가 크게 변하지 않음을 시사.
  • Interrupt (빨간 선):
    • 초당 발생하는 인터럽트 수는 2000 ~ 10000 IRQ/s 사이에서 변동.
    • 특정 시간 구간에서는 인터럽트가 급격히 증가하며, 이는 하드웨어 자원 관리 또는 입출력 작업(I/O)과 관련.

 

전반적인 결론

  1. 문맥 전환(Process Switch):
    • AI 워크로드가 복잡할수록 문맥 전환 빈도가 크게 증가.
    • 특히, 대형 네트워크(VGG16, PSPNet, DeepLab)에서 초당 문맥 전환 횟수가 약 18,000/s로 최고치를 기록.
  2. CPU 사용률(CPU Utilization):
    • 문맥 전환과 큰 상관없이 약 30% ~ 50%로 일정하게 유지.
    • 이는 AI 연산이 주로 GPU에서 수행되고, CPU는 보조 작업을 수행하기 때문.
  3. 프로세스 수와 인터럽트:
    • 실행 중인 프로세스 수는 워크로드에 상관없이 일정.
    • 인터럽트 빈도는 특정 작업(I/O, 메모리 관리)에 따라 급격히 변동하며, 최대 10000 IRQ/s까지 상승.
  4. 최적화 방향:
    • 복잡한 워크로드에서 문맥 전환 오버헤드를 줄이기 위해 스케줄링 최적화가 필요.
    • I/O 작업과 인터럽트 빈도를 줄이기 위해 워크로드와 시스템 간의 데이터 전송 경로를 최적화해야 함.

 

Process Creation

  • OS 는 프로세스를 생성하고 종료하며, 여러 프로세스가 Concurrently 하게 실행될 수 있으며, 동적으로 생성/종료
  • OS 는 process creation 과 process termination 메커니즘을 제공한다.

  • 생성은 fork() 시스템 호출을 통해 이루어진다.
  • 부모와 자식 프로세스는 모든 리소스를 공유한다.
  • 자식 프로세스는 부모 프로세스의 리소스의 부분집합을 공유한다.
  • 부모와 자식은 병렬적으로 실행되며 부모는 자식 프로세스가 종료될 때까지 기다린다.

 

Process Creation in Memory View

 

  • 자식 프로세스는 부모 프로세스의 복사본으로 생성된다.
  • 자식 프로세스는 독립적인 프로그램을 실행하거나 부모의 메모리를 그대로 사용할 수 있음.

 

 

 

 

  • 부모 프로세스 (Parent Process):
    • 텍스트 섹션, 데이터 섹션, 힙, 스택을 가지고 있음.
  • 자식 프로세스 (Child Process):
    • fork() 호출 후 부모의 메모리를 복사.
    • exec() 호출 시 기존 메모리를 새로운 프로그램 메모리로 대체.

 

 

Process Creation in UNIX

#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 faild, 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());
    	execl("/bin/ls", "ls", "-l", NULL); // Run 'ls -l' at /bin/ls
    	for(i=0; i<10; i++){ // Cannot be run
    		printf("Counter: %d\n", counter++);
    	}
    }
    
    wait(NULL);
    return EXIT_SUCCESS;
    }
 }

 

 

  • pid = fork():
    • 새로운 프로세스를 생성
    • 이후부터 부모와 자식은 다른 실행 흐름을 따른다
  • 부모 프로세스 (pid > 0):
    • 부모 프로세스는 자식 프로세스의 PID를 출력하고, counter 값을 10번 출력
    • wait() 호출로 자식 프로세스가 종료될 때까지 대기
  • 자식 프로세스 (pid == 0):
    • 자식 프로세스는 자신의 PID를 출력하고, /bin/ls 명령어를 실행
    • execl() 호출 이후의 코드는 실행되지 않음
      • execl()은 현재 실행 중인 프로세스의 메모리 공간 전체를 새로운 프로그램으로 교체한다.
        • 현재 프로세스가 실행 중이던 프로그램의 코드, 데이터, 스택 섹션은 삭제된다.
        • 대신, 지정된 새로운 프로그램(/bin/ls 등)이 프로세스 메모리 공간에 로드된다.
        • 기존 프로세스의 PID는 유지되지만, 메모리와 코드 상태는 완전히 변경된다.
  • 종료:
    • 자식 프로세스가 종료되면, 부모 프로세스가 wait()에서 깨어나 정상적으로 종료

 

Process Creation in XV6

 // Create a new process copying p as the parent.
 // Sets up stack to return as if from system call.
 // Caller must set state of returned proc to RUNNABLE.
 int
 fork(void)
 {
 int i, pid;
 struct proc *np;
 struct proc *curproc = myproc();
 ...
 // Copy process state from proc.
 if((np->pgdir = copyuvm(curproc->pgdir, curproc->sz)) ==
 0){
 kfree(np->kstack);
 np->kstack = 0;
 np->state = UNUSED;
 return-1;
 }
 np->sz = curproc->sz;
 np->parent = curproc;
 *np->tf = *curproc->tf;
 // Clear %eax so that fork returns 0 in the child.
 np->tf->eax = 0;
 for(i = 0; i < NOFILE; i++)
 if(curproc->ofile[i])
 np->ofile[i] = filedup(curproc->ofile[i]);
 np->cwd = idup(curproc->cwd);
 safestrcpy(np->name, curproc->name, sizeof(curproc->name));
 pid = np->pid;
 acquire(&ptable.lock);
 np->state = RUNNABLE;
 release(&ptable.lock);
 return pid;
 }

 

 

 

Process Creation Hierarchy in UNIX

 

  • UNIX에서 프로세스는 트리 구조로 관리된다.
  • 모든 프로세스는 최상위 부모 프로세스인 root 프로세스에서 시작된다.

 

  1. root 프로세스:
    • 최상위 프로세스.
    • 모든 프로세스는 root 프로세스에서 직접 또는 간접적으로 파생
  2. init 프로세스:
    • 시스템 부팅 시 생성되는 첫 번째 프로세스.
    • 모든 사용자 프로세스의 부모 프로세스로 동작.
    • 사용자 로그인 및 세션 관리.
  3. 시스템 프로세스 (pagedaemon, swapper):
    • 시스템 자원 관리에 사용.
    • 예:
      • pagedaemon: 메모리 페이지 관리를 담당.
      • swapper: 메모리 스와핑(교체)을 담당.
  4. 사용자 프로세스 (user 1, user 2, user 3):
    • 사용자가 직접 실행하는 애플리케이션 또는 명령어.
    • init 프로세스에서 생성.

 

Process Termination

  • 프로세스가 실행을 완료하거나 강제 종료될 때 운영 체제는 해당 프로세스를 제거

  • 정상 종료
    • exit 시스템 호출:
      • 프로세스가 마지막 명령을 실행한 후 스스로 종료를 요청.
      • 부모 프로세스는 **wait()**를 통해 자식 프로세스의 종료 상태를 수집.
      • 종료 시, 운영 체제는 프로세스가 점유한 자원을 회수(메모리, 파일 디스크립터 등).
  • 비정상 종료
    • abort() 함수:
      • 프로세스를 비정상적으로 종료.
      • SIGABRT 시그널을 해당 프로세스에 전달.
      • 종료 시 코어 덤프(메모리 상태 저장)를 생성하여 디버깅 가능.

 

Cooperating Processes

  • 독립 프로세스:
    • 다른 프로세스의 실행에 영향을 받지 않음.
  • 협력 프로세스:
    • 프로세스 간 상호작용이 존재하며, 다른 프로세스의 실행에 의존적.

    • 장점
      • 정보 공유:
        • 여러 프로세스가 데이터를 공유하여 효율적으로 작업.
      • 계산 속도 향상:
        • 여러 프로세스가 동시에 작업을 수행하여 처리 속도 증가.
      • 모듈성:
        • 큰 작업을 여러 프로세스가 나누어 처리.
      • 편리성:
        • 프로세스 간 작업 분담으로 복잡한 작업을 쉽게 처리.
      • 예:
        • DBMS
          • 공유 메모리를 통해 협력하는 세 개의 프로세스(P1, P2, P3) 가 나타남
          • 모든 프로세스는 읽기 / 쓰기 작업을 통해 데이터베이스 작업을 병렬적으로 수행

 

 

반응형