목차
- 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로 작성) 전체를 링커 등을 통해 한번에 번역하여 목적 파일(기계어로 작성)로 만들어 메모리상에 적재한다.
인터프리터는 소스 코드를 한 행씩 중간 코드로 번역 후 실행한다.
- 컴파일러는 소스코드 전체를 컴퓨터 프로세서가 실행할 수 있도록 바로 기계어로 변환한다. 인터프리터는 고레벨 언어를 중간 코드(intermediate code)로 변환하고 이를 각 행마다 실행한다.
- 일반적으로 컴파일러가 각 행마다 실행하는 특성을 가진 인터프리터보다는 실행시간이 빠르다.
- 컴파일러는 전체 소스코드를 변환 한 뒤 에러를 보고하지만 인터프리터는 각 행마다 실행하는 도중 에러가 보고되면 이후 작성된 코드를 살펴보지 않는다. 이는 보안적인 관점에서 도움이 된다.
 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 에 올려주는 역할
- Executable 의 Header 를 읽어, Text 와 Data 의 크기를 결정
- 프로그램을 위한 Address Space 를 생성
- 실행 명령어와 Data 들을 Executable 로부터 생성한 Address Space 로 복사
- 프로그램의 argument 들을 stack 으로 복사
- 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():
- 커널에서 사용자 공간으로 데이터를 복사.
- 명령줄 인자 문자열을 사용자 스택에 복사.
 
- 스택에 추가로 다음 데이터를 초기화
- ustack[0]: 가짜 반환 주소 (return PC). 함수 호출에서 돌아오지 않도록 설정.
- ustack[1]: 명령줄 인자 개수 (argc).
- ustack[2]: 명령줄 인자 배열의 포인터 (argv).
- 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 차이
- CISC
- 복잡한 명령어 지원
- 실행 속도가 느릴 수 있음
 ex) Intel Pentium Processor
 
- 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 비용이 낮은 반면, 엣지 디바이스에서는 비용이 높음
 => 워크로드 특성에 따라 오버헤드가 달라짐
 
- 클라우드에서는 context switching 비용이 낮은 반면, 엣지 디바이스에서는 비용이 높음

위 그래프는 lmbench라는 벤치마크 도구를 사용하여 문맥 전환(Context Switch) 및 비문맥 전환(Non-Context Switch)의 오버헤드를 평가한 결과를 나타낸다. 그래프는 데이터 크기와 쓰레드 수에 따라 오버헤드를 분석하며, 문맥 전환과 관련된 성능 영향을 비교한다.
- Non-Context Switch의 경우 데이터 크기가 크면 일정한 오버헤드가 발생하며, 쓰레드 수 증가에 따른 영향은 미미
- Context Switch의 오버헤드는 데이터 크기와 쓰레드 수가 증가함에 따라 선형적으로 증가한다.
- 데이터 크기가 클수록 쓰레드 수 증가에 따른 오버헤드가 더 명확하게 나타난다.

(a) Image Recognition Applications
- Process switch (파란 선):
- 문맥 전환 빈도가 시간에 따라 크게 변화하며, 특정 시간 구간에서 18,000/s 이상의 높은 빈도를 기록.
- AI 워크로드가 다양한 프로세스 간 전환을 자주 요구하는 경우, 문맥 전환 횟수가 급증.
- 예를 들어, MobileNet v2와 같은 워크로드에서는 문맥 전환 빈도가 낮고, VGG16과 같은 더 복잡한 네트워크에서는 문맥 전환 빈도가 크게 증가.
 
- 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)과 관련.
 
전반적인 결론
- 문맥 전환(Process Switch):
- AI 워크로드가 복잡할수록 문맥 전환 빈도가 크게 증가.
- 특히, 대형 네트워크(VGG16, PSPNet, DeepLab)에서 초당 문맥 전환 횟수가 약 18,000/s로 최고치를 기록.
 
- CPU 사용률(CPU Utilization):
- 문맥 전환과 큰 상관없이 약 30% ~ 50%로 일정하게 유지.
- 이는 AI 연산이 주로 GPU에서 수행되고, CPU는 보조 작업을 수행하기 때문.
 
- 프로세스 수와 인터럽트:
- 실행 중인 프로세스 수는 워크로드에 상관없이 일정.
- 인터럽트 빈도는 특정 작업(I/O, 메모리 관리)에 따라 급격히 변동하며, 최대 10000 IRQ/s까지 상승.
 
- 최적화 방향:
- 복잡한 워크로드에서 문맥 전환 오버헤드를 줄이기 위해 스케줄링 최적화가 필요.
- 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는 유지되지만, 메모리와 코드 상태는 완전히 변경된다.
 
 
- execl()은 현재 실행 중인 프로세스의 메모리 공간 전체를 새로운 프로그램으로 교체한다.
 
- 종료:
- 자식 프로세스가 종료되면, 부모 프로세스가 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 프로세스에서 시작된다.
- root 프로세스:
- 최상위 프로세스.
- 모든 프로세스는 root 프로세스에서 직접 또는 간접적으로 파생
 
- init 프로세스:
- 시스템 부팅 시 생성되는 첫 번째 프로세스.
- 모든 사용자 프로세스의 부모 프로세스로 동작.
- 사용자 로그인 및 세션 관리.
 
- 시스템 프로세스 (pagedaemon, swapper):
- 시스템 자원 관리에 사용.
- 예:
- pagedaemon: 메모리 페이지 관리를 담당.
- swapper: 메모리 스와핑(교체)을 담당.
 
 
- 사용자 프로세스 (user 1, user 2, user 3):
- 사용자가 직접 실행하는 애플리케이션 또는 명령어.
- init 프로세스에서 생성.
 
Process Termination
- 프로세스가 실행을 완료하거나 강제 종료될 때 운영 체제는 해당 프로세스를 제거
- 정상 종료
 - exit 시스템 호출:
- 프로세스가 마지막 명령을 실행한 후 스스로 종료를 요청.
- 부모 프로세스는 **wait()**를 통해 자식 프로세스의 종료 상태를 수집.
- 종료 시, 운영 체제는 프로세스가 점유한 자원을 회수(메모리, 파일 디스크립터 등).
 
 
- exit 시스템 호출:
- 비정상 종료
- abort() 함수:
- 프로세스를 비정상적으로 종료.
- SIGABRT 시그널을 해당 프로세스에 전달.
- 종료 시 코어 덤프(메모리 상태 저장)를 생성하여 디버깅 가능.
 
 
- abort() 함수:
Cooperating Processes
- 독립 프로세스:
- 다른 프로세스의 실행에 영향을 받지 않음.
 
- 협력 프로세스:
- 프로세스 간 상호작용이 존재하며, 다른 프로세스의 실행에 의존적.
- 장점
- 정보 공유:
- 여러 프로세스가 데이터를 공유하여 효율적으로 작업.
 
- 계산 속도 향상:
- 여러 프로세스가 동시에 작업을 수행하여 처리 속도 증가.
 
- 모듈성:
- 큰 작업을 여러 프로세스가 나누어 처리.
 
- 편리성:
- 프로세스 간 작업 분담으로 복잡한 작업을 쉽게 처리.
 
- 예:
- DBMS
- 공유 메모리를 통해 협력하는 세 개의 프로세스(P1, P2, P3) 가 나타남
- 모든 프로세스는 읽기 / 쓰기 작업을 통해 데이터베이스 작업을 병렬적으로 수행
 
 
- DBMS
 
- 정보 공유:
 
- 프로세스 간 상호작용이 존재하며, 다른 프로세스의 실행에 의존적.

'3학년 2학기 학사 > 운영체제' 카테고리의 다른 글
| [운영체제] #9. 동기화 (1) (0) | 2024.11.27 | 
|---|---|
| [운영체제] #7. InterProcess Communication (IPC) (0) | 2024.11.26 | 
| [운영체제] #5. Computer Architecture (0) | 2024.11.24 | 
| [운영체제] #3. 운영체제 구조 (2) (0) | 2024.11.19 | 
| [운영체제] #2. 운영체제 구조 (1) (0) | 2024.11.19 | 
 
                    
                   
                    
                   
                    
                  