Krafton Jungle/2. Keywords

[WEEK11] USERPROG 관련 키워드 정리

munsik22 2025. 5. 27. 11:09

🔹 User mode vs Kernel mode

더보기

User mode vs Kernel mode

1️⃣ 정의 및 역할

구분 User Mode Kernel Mode
목적 사용자 응용 프로그램이 실행되는 모드 운영체제의 핵심 기능(OS 커널 코드)이 실행되는 모드
권한 수준 제한적 (하드웨어 직접 접근 불가) 전권 보유 (모든 메모리/디바이스 접근 가능)
실행 코드 사용자 코드 (예: ls, gcc, 사용자 작성 프로그램 등) 커널 코드 (예: 파일 시스템, 스케줄러, 메모리 관리자 등)

 2️⃣ 주요 차이점

항목 User Mode Kernel Mode
하드웨어 접근 제한됨 (예: I/O 포트, 메모리 직접 접근 불가) 가능 (모든 하드웨어 제어 가능)
시스템 호출 필요 시스템 콜 통해 커널 기능 요청 필요 없음, 직접 커널 함수 호출 가능
메모리 보호 사용자 메모리만 접근 가능 전체 시스템 메모리 접근 가능
예외 발생 시 커널에 제어권 이양 (trap) 커널 자체 예외 처리 루틴 수행

3️⃣ 모드 전환 (User → Kernel) 예시

  • 사용자 프로그램이 파일을 열려고 open() 호출 → 커널에 시스템 콜 trap → 커널 모드로 전환
  • 페이지 폴트, 인터럽트 발생 → 커널이 이를 처리하므로 커널 모드 진입
  • 커널 작업 끝나면 다시 사용자 모드로 복귀 (iretq 등으로 레지스터 복구)

4️⃣ 모드 전환 예시 (코드)

사용자 프로그램에서 write() 같은 시스템 콜 호출 시,

  1. Trap 발생 → 커널 진입
  2. syscall_entry() 실행
  3. syscall_handler()에서 실제 시스템 콜 처리
  4. 작업 끝나면 커널에서 유저 모드로 복귀

① 사용자 프로그램의 시스템 콜 요청

  • 사용자 프로그램에서는 보통 라이브러리 함수를 호출하지만, 결국 어셈블리 수준에서는 시스템 콜 넘버와 인자를 레지스터에 넣고 syscall 명령을 실행한다.
write(fd, buffer, size);  // 사용자 코드
  • 내부적으로는 아래와 같이 syscall 넘버를 세팅:
mov rax, SYS_WRITE     # 시스템 콜 번호
mov rdi, fd
mov rsi, buffer
mov rdx, size
syscall                # 커널 모드 진입

syscall_entry()

void
syscall_entry (void) {
  struct thread *cur = thread_current ();
  ASSERT (is_user_vaddr (f->rsp));  // 사용자 스택인지 확인
  syscall_handler (f);              // 실제 처리 담당 함수로 이동
}
  • syscall_entry()는 유저 → 커널 모드로 전환된 trap 진입점
  • intr_frame *f를 통해 유저 레지스터 값을 커널에 전달함

syscall_handler(): 시스템 콜 실제 처리

static void
syscall_handler (struct intr_frame *f) {
  uint64_t syscall_num = f->R.rax; // syscall 넘버 추출
  switch (syscall_num) {
    case SYS_WRITE:
      f->R.rax = sys_write(f->R.rdi, f->R.rsi, f->R.rdx);
      break;
    ...
  }
}
  • 레지스터로 전달된 인자(rdi, rsi, rdx)를 사용해 해당 syscall 수행
  • 결과값은 다시 rax에 저장 → 유저 모드 복귀 시 return값으로 전달됨

④ 커널 → 사용자 모드 복귀

  • 시스템 콜 처리 끝나면 intr_exit() 혹은 syscall_return 루틴을 통해 사용자 모드로 돌아감
  • rax 값이 그대로 사용자 코드에 return 값이 됨

🔹 관련 소스 정리

파일 함수 설명
syscall.c syscall_entry() 유저 → 커널 진입점
syscall.c syscall_handler() 시스템 콜 분기 처리
syscall.c sys_write() 실제 시스템 콜 함수
interrupt.c intr_frame 구조체 유저 레지스터 백업 구조체

🔹 요약

  1. 유저 코드가 syscall 명령 → 커널 진입
  2. syscall_entry()syscall_handler() 경유
  3. 커널 모드에서 요청 처리
  4. rax에 return 값을 담아 사용자로 복귀

5️⃣ 왜 두 가지 모드가 필요한가?

  • 보안(Security): 사용자 프로그램이 시스템 자원에 직접 접근하지 못하도록 보호
  • 안정성(Stability): 오류가 커널에 영향을 주지 않도록 격리
  • 제어(Control): 자원 할당, 스케줄링 등은 커널만 처리하도록 제한

6️⃣ 요약

  • User Mode: 제한된 권한, 안전하지만 기능 제약 있음
  • Kernel Mode: 모든 기능 가능, 단 실수 시 시스템 전체에 영향

이러한 모드 전환을 사용함으로써 User Mode와 Kernel Mode의 책임 분리와 보안이 가능하다.

🔹 Register vs Memory

더보기

Register vs Memory

1️⃣ 개념 정리

  • Register
    • CPU 내부에 존재하는 초고속 저장 장치
    • 보통 몇 개(수십 개)만 존재하며 이름이 정해져 있음 (예: rax, rsp, rip 등)
    • 연산에 직접 사용되는 데이터를 담고 있음
    • 접근 속도는 CPU 내부에서 가장 빠름 (한 사이클 내 접근 가능)
  • Memory
    • 일반적으로 말하는 RAM (주기억장치)
    • 프로그램 실행 시 코드, 데이터, 스택 등이 올라감
    • CPU 외부에 위치 → 속도는 느리지만 용량은 큼 (GB 단위)

2️⃣ 기능적 차이

항목 Register Memory
위치 CPU 내부 CPU 외부 (RAM 칩)
용량 매우 작음 (수십~수백 바이트 수준) 크고 확장 가능 (GB 단위)
속도 매우 빠름 (1 CPU cycle) 상대적으로 느림 (수십~수백 cycle)
접근 방식 명시적인 레지스터 사용 (mov rax) 주소 기반 간접 접근 (mov rax, [addr])
용도 연산 처리 중간 결과 저장 코드, 데이터, 실행 스택 저장

3️⃣ 사용 예시

# 레지스터 사용 예시
mov rax, 5        # 레지스터 rax에 5 저장
add rax, 3        # rax = rax + 3

# 메모리 접근 예시
mov rbx, [0x601000] # 메모리 주소 0x601000의 값을 rbx에 로드
  • 레지스터는 직접 지정해서 사용함
  • 메모리는 주소를 통해 간접적으로 접근해야 함

4️⃣ 사용 예시 (코드)

🔹 레지스터 사용 예시: 시스템 콜 진입 시 intr_frame

void syscall_handler (struct intr_frame *f) {
  uint64_t syscall_number = f->R.rax;
  switch (syscall_number) {
    case SYS_WRITE:
      f->R.rax = sys_write(f->R.rdi, f->R.rsi, f->R.rdx);
      break;
    ...
  }
}
  • 시스템 콜 진입 시, 사용자 레지스터 값은 struct intr_frame 구조체를 통해 전달됨
  • f->R.rax, f->R.rdi 등은 사용자 모드에서 시스템 콜 진입 시의 레지스터 값을 의미
  • 실제 syscall 인자들이 레지스터를 통해 전달된다는 의미
  • 여기서 f는 스택에 저장된 레지스터 백업본이고, 이를 통해 syscall을 처리함

🔹 메모리 사용 예시: 사용자 스택에서 인자 읽기 (User memory 접근)

bool is_valid(const void *addr) {
	if (addr == NULL) return false;
	if (!is_user_vaddr(addr)) return false;
	if (pml4_get_page(thread_current()->pml4, addr) == NULL) return false;
	return true;
}
  • 사용자 인자인 포인터 addr이 올바른 사용자 메모리 영역에 속해 있는지 확인
  • 실제 데이터는 RAM에 있고, pml4_get_page()를 통해 매핑 여부 확인
char *user_str = (char *)f->R.rdi;
if(!is_valid((user_str)) exit(-1);
  • rdi는 레지스터에서 받지만,
  • user_str은 실제로 사용자 메모리 주소를 가리킴

 🔹 실제 흐름 요약

  1. 사용자 코드에서 syscall 호출 (write(fd, buffer, size))
  2. 인자들은 레지스터(rdi, rsi, rdx)에 저장됨 → 커널 진입
  3. 커널은 f->R.rdi 등 레지스터에서 인자를 꺼냄
  4. 해당 인자가 메모리 주소일 경우, pml4_get_page() 등으로 메모리 접근

5️⃣ 보안 및 시스템 설계 관점

  • 레지스터는 context switch 시 커널이 백업/복원함
  • 메모리는 사용자/커널 영역이 나뉘며, 보호 메커니즘(Paging, MMU 등)이 적용됨
  • 운영체제는 메모리 접근을 제한할 수 있지만, 레지스터는 제한하지 않음 (사용자 프로세스는 자기 레지스터만 사용)

6️⃣ 요약

  • Register: 작고 빠르며 CPU 연산에 직접 사용되는 저장소
  • Memory: 크고 느리지만 다양한 데이터를 저장할 수 있는 저장소

🔹 User stack

더보기

[출처]

1️⃣ User Stack이란?

  • 유저 프로그램(사용자 프로세스)의 함수 호출, 로컬 변수, 리턴 주소 등을 저장하는 후입선출(LIFO) 구조의 메모리 영역
  • 커널 스택과는 분리되어 있으며, 유저 모드에서만 접근 가능함
  • 보통 프로세스마다 하나씩 생성되고, 가상 메모리 상에서 높은 주소부터 낮은 방향으로 성장함

2️⃣ 리눅스에서의 User Stack 사용

  • 구조 및 위치
    • 리눅스는 유저 프로세스 생성 시 고정된 크기의 스택 공간을 프로세스 주소 공간 상단에 할당한다.
    • 예시: 0x7ffffffff000에서 아래로 내려가는 방향
  • 사용 방식
    • 함수 호출 시 리턴 주소와 인자, 로컬 변수가 스택에 push
    • main()의 인자 (argc, argv, envp)도 실행 시점에 스택에 올라감
    • execve() 시스템 콜이 새로운 프로세스를 실행할 때, 유저 스택에 프로그램 인자를 세팅함
  • 보호
    • Stack Overflow 방지를 위한 guard page 사용
    • 유저는 자기 스택만 접근 가능 (MMU, 페이지 테이블이 보호)

3️⃣ PintOS에서의 User Stack 사용

🔹 구조 및 할당 방식

  • PintOS는 load_segment() 함수에서 스택 초기 페이지를 명시적으로 할당함
if (!setup_stack (if_)) FAIL;
  • 내부적으로는 palloc_get_page(PAL_USER)를 사용해 USER 영역에 한 페이지 할당
bool setup_stack (struct intr_frame *if_) {
  uint8_t *kpage;
  bool success = false;
  kpage = palloc_get_page (PAL_USER | PAL_ZERO);
  if (kpage != NULL) {
    success = install_page (stack_bottom_addr, kpage, true);
    if (success)
      if_->rsp = USER_STACK_BOTTOM;
    else
      palloc_free_page (kpage);
  }
  return success;
}
  • 스택의 초기 위치는 0x47480000 - PGSIZE (STACK_BOTTOM)에서 시작
  • 위와 동일하게 아래 방향으로 성장

🔹 시스템 콜 인자 전달에도 사용됨

  • 시스템 콜 호출 시 사용자 코드에서 syscall 명령이 실행되면, CPU가 자동으로 user stack에 레지스터와 return 주소 등을 저장함
  • 그 후 커널 모드로 진입

🔹 System Call

더보기

System Call

1️⃣ 시스템 콜이란?

  • 사용자 프로그램이 운영체제 커널의 기능을 사용하기 위해 호출하는 인터페이스 함수
  • 사용자 프로그램은 보안상 커널 영역에 직접 접근할 수 없기 때문에, 파일 입출력, 메모리 할당, 프로세스 생성, 네트워크 통신 등 OS의 서비스가 필요한 경우 반드시 시스템 콜을 통해 요청해야 함

2️⃣ 시스템 콜이 필요한 이유

  1. 보안(Security)
    • 사용자 프로그램이 커널 메모리, 하드웨어 자원에 직접 접근하면 시스템 전체가 불안정해짐
    • 커널은 사용자 접근을 차단하고, 간접 호출 방식(syscall)을 제공
  2. 권한 분리(Privilege Separation)
    • CPU는 User Mode와 Kernel Mode를 구분함
    • 시스템 콜은 User Mode → Kernel Mode 전환의 유일한 공식 경로
  3. 운영체제의 제어 유지
    • 사용자 코드가 운영체제의 제어 없이 자원을 사용하면 스케줄링/보호/공유 관리가 어려워짐
    • 시스템 콜을 통해 OS가 모든 요청을 관리

3️⃣ 시스템 콜의 특징

항목 설명
인터페이스 보통 libc가 제공 (read(), write(), fork() 등)
호출 방식 syscall 명령어 또는 int 0x80 (x86)
인자 전달 레지스터 (x86-64 기준: rdi, rsi, rdx, ...)
리턴 값 rax에 저장되어 사용자에게 전달
오버헤드 유저 → 커널 전환 비용 존재

 4️⃣ 시스템 콜 호출 흐름

1. 사용자 프로그램이 C 라이브러리 함수 호출

write(1, "hello", 5);

2. 내부적으로 시스템 콜 번호와 인자를 레지스터에 세팅

mov rax, 1        # SYS_write
mov rdi, 1        # fd = 1 (stdout)
mov rsi, buf      # buf = "hello"
mov rdx, 5        # size = 5
syscall           # 시스템 콜 진입

3. 커널 진입 → 시스템 콜 번호 해석 → 해당 커널 함수 실행
4. 반환값은 다시 rax에 담아 사용자 코드로 복귀


5️⃣ 요약

  • 시스템 콜은 사용자와 커널 사이의 게이트웨이
  • 하드웨어 접근, 자원 요청 등은 항상 커널이 대신 수행
  • 보안을 유지하면서도 필요한 기능을 사용자에게 제공하기 위한 필수 구조

🔹 32-bit OS vs 64-bit OS

더보기

32비트 OS vs 64비트 OS

1️⃣ 기본 개념

운영체제에서 32비트와 64비트의 차이는 주로 CPU 아키텍처와 관련된 주소 처리 능력과 데이터 처리 폭을 의미한다.

항목 32비트 OS 64비트 OS
주소 폭 32비트 (4바이트) 64비트 (8바이트)
최대 메모리 접근 약 4GB 이론상 16EB (Exabytes), 실제는 수 TB
레지스터 크기 32비트 64비트
처리 속도 상대적으로 느림 더 많은 연산 가능 (부동소수점, 암호화 등)
호환성 구형 시스템, 저사양 PC 최신 시스템, 대부분 현대 OS

2️⃣ 메모리 주소 공간 차이

  • 32비트: 주소 공간이 2³² = 4GB
    • 보통 커널이 12GB 차지 → 사용자 공간은 최대 23GB
  • 64비트: 이론상 2⁶⁴ 바이트 = 16 Exabytes(EiB)
    • 현실적으로는 하드웨어/OS가 제한하여 수십 테라바이트(TB) 수준 사용

3️⃣ 주요 차이점 비교

항목 32비트 OS 64비트 OS
주소 범위 0x00000000 ~ 0xFFFFFFFF 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF
CPU 레지스터 32비트 (eax, ebx, ...) 64비트 (rax, rbx, ...)
프로그램 지원 32비트 앱만 실행 가능 32비트 & 64비트 앱 대부분 실행 가능
드라이버 32비트 드라이버만 호환 64비트 전용 드라이버 필요
성능 연산량이 적은 경우 유리 연산량이 많거나 메모리 집약 작업에 유리
보안 기능 제한적 (예: ASLR 범위 좁음) 강화된 보안 가능 (NX bit, SMEP, etc)

🔹 프로그램 및 라이브러리에서의 차이

항목 32비트 64비트
실행 파일 ELF32 ELF64
포인터 크기 4바이트 8바이트
sizeof(void*) 4 8
ABI 차이 존재 (함수 호출 시 인자 전달 방식 등) 다름

🔹 개발 관점에서의 차이

  • 포인터 연산, 자료구조 설계 시 크기 차이를 고려해야 함
  • 커널 개발, 시스템 콜 인터페이스 구현 시 ABI(응용 바이너리 인터페이스) 차이에 주의 필요
  • 호환성 문제: 64비트 OS에서 32비트 앱을 실행하려면 추가 라이브러리 필요 (lib32, WoW64 등)

[읽어보면 좋을 이야기] 64비트 게임에서 32비트 매크로가 돌아가지 않는 이유는?

 

💿 KAIST:PINTOS | Concept | 32 bit OS vs 64 bit OS

32비트와 64비트의 차이는 ‘한 번에 처리할 수 있는 정보량’과 ‘다룰 수 있는 메모리 크기’의 차이이며, 프로그램의 성능과 범위를 결정짓는 아주 중요한 요소이다.컴퓨터는 모든 데이터를

velog.io

🔹 Atomic Operation

더보기

Atomic Operation

  • 정의 : 딱 한 번만에 끝나는 연산, 도중에 끼어들 수 없고 실패하거나 중단되지 않는 연산
  • 예시
x = x + 1; // ❌메모리에서 x를 읽고, 1을 더하고, 다시 메모리에 쓰는 중간에 다른 쓰레드가 끼어들 수 있다.
__sync_fetch_and_add(&x, 1);  // ✅CPU에서 하나의 명령어로 처리되어 도중에 다른 쓰레드가 간섭할 수 없다.
  • 왜 중요할까?
    • 멀티쓰레드 환경에서 데이터 손상 방지
    • 락(lock)을 쓰지 않고도 간단한 동기화 가능 (ex. 카운터 증가)
    • 세마포어, 스핀락, 뮤텍스 등의 기본이 되는 개념

🔹 lock_acquire()

void lock_acquire (struct lock *lock) {
  ASSERT (lock != NULL);
  ASSERT (!intr_context ());
  ASSERT (!lock_held_by_current_thread (lock));

  sema_down (&lock->semaphore);   // 세마포어 획득
  lock->holder = thread_current ();  // 락 소유자 설정
}
  • 여기서 사용되는 sema_down() 함수는 내부적으로 원자적 연산을 통해 세마포어 값을 감소시키고, 필요 시 대기한다.

🔹 sema_down()

void sema_down (struct semaphore *sema) {
  enum intr_level old_level;

  ASSERT (sema != NULL);
  ASSERT (!intr_context ());

  old_level = intr_disable (); // 인터럽트 비활성화 → Atomic 보호

  while (sema->value == 0) {
    list_push_back (&sema->waiters, &thread_current ()->elem);
    thread_block ();
  }
  sema->value--;               // 공유 자원 접근 전 원자적 감소
  intr_set_level (old_level); // 인터럽트 복구
}
  • intr_disable() → 인터럽트를 끄고
  • sema->value-- 수행 → 도중에 스케줄러나 인터럽트가 끼어들 수 없음
  • 즉, sema->value를 감소시키는 이 구간이 Atomic Section이 되는 것

🔹 Interrupt

더보기

Interrupt

  • 정의
    • CPU가 프로그램을 실행하는 도중에, 예상치 못한 외부 또는 내부 이벤트가 발생했음을 알리는 신호
    • CPU가 예상치 못한 사건에 즉각 반응하도록 도와주는 메커니즘
    • CPU는 이 신호를 받으면 현재 작업을 잠시 중단하고, 해당 이벤트를 처리한 뒤 다시 돌아오는 구조로 동작함
  • 필요성
    • 효율성: CPU가 계속 디바이스 상태를 감시하지 않아도 됨 (Polling 불필요)
    • 실시간성: 긴급 이벤트 발생 시 즉시 대응 가능 (예: 키보드 입력)
    • 멀티태스킹 기반: 스케줄링, 시분할 처리 등에 필수
  • 종류
종류 설명 예시
하드웨어 인터럽트 외부 장치에서 발생 키보드 입력, 디스크 I/O 완료, 타이머
소프트웨어 인터럽트 명령어 또는 예외 발생 시 시스템 콜(int 0x80), 0으로 나누기
타이머 인터럽트 주기적 신호로 CPU 제어 시분할 스케줄링의 기반
  • 인터럽트 발생 시 흐름
    1. 인터럽트 발생 (예: 키보드 입력, 타이머 틱)
    2. 현재 작업 저장 (레지스터, PC 등 → 스택에 저장)
    3. 인터럽트 핸들러 호출
    4. 이벤트 처리
    5. 저장된 상태 복원 → 원래 하던 코드로 복귀
  • 인터럽트와 커널 보호
    • 크리티컬 섹션 보호: 멀티스레드 환경에서 공유 자원을 보호하기 위해, 잠시 인터럽트를 끄고 작업하는 경우가 많음
    • Atomic Operation을 보장하기 위한 일반적인 방식
enum intr_level old_level = intr_disable();
/* Critical Section */
intr_set_level(old_level);
  • 인터럽트 벡터와 핸들러
    • 운영체제는 인터럽트 번호(벡터)마다 핸들러 함수를 등록해두고,
    • 인터럽트가 발생하면 해당 번호에 맞는 함수가 실행된다.
인터럽트 번호 설명  핸들러
0x20 타이머 timer_interrupt()
0x21 키보드 keyboard_interrupt()
0x80 시스템 콜 (x86 리눅스) syscall_handler()

🔹 인터럽트 vs 시스템콜

  • “인터럽트는 시스템이 주도, 시스템 콜은 사용자가 요청”
항목 Interrupt System Call
정의 CPU 외부/내부 이벤트로 인해 실행 흐름이 강제로 전환되는 신호 사용자 프로그램이 커널 기능을 요청하는 명령
발생 원인 하드웨어 이벤트 (예: 키보드 입력, 타이머), 예외 프로그래머가 명시적으로 호출 (예: write())
발생 주체 외부 장치 or CPU 내부 하드웨어 사용자 프로그램
목적 예기치 않은 사건 처리 (비동기적) 커널 기능 요청 (동기적)
전환 방향 일반적으로 커널 → 커널 (또는 유저 → 커널) 항상 유저 → 커널
예시 명령 하드웨어: IRQ, int 0x20 (타이머) / 예외: 0 나누기 int 0x80, syscall
공통점 둘 다 커널 모드 진입 지점이며 보호 모드 전환 발생
대표 핸들러 timer_interrupt(), keyboard_interrupt() syscall_handler()

🔹 Segmentation Fault

더보기

Segmentation Fault

  • 정의
    • 프로그램이 허용되지 않은 메모리 영역에 접근했을 때 발생하는 오류
    • 하지 말라는 메모리 주소에 접근했다가 터지는 오류
    • 운영체제는 각 프로세스에 대해 고유한 메모리 공간을 할당하고, 이 공간 외의 영역에 접근하려고 하면 보호 차원에서 강제로 프로그램을 종료시킨다 → 세그폴트(Segfault)
  • 주요 원인들
원인 설명 예시
널 포인터 역참조 NULL을 잘못 사용 *ptr = 5; where ptr = NULL
초기화되지 않은 포인터 포인터 선언만 하고 사용 char *str; strcpy(str, "hi");
배열/버퍼 오버플로우 인덱스 범위 초과 int arr[3]; arr[5] = 10;
해제된 메모리 접근 free() 후 접근 free(p); *p = 3;
읽기 전용 영역 쓰기 문자열 리터럴 수정 시도 char *s = "hello"; s[0] = 'H';
스택 오버플로우 재귀 무한 호출 f() { f(); }
  • 작동 원리 (OS 관점)
    • 프로세스는 가상 주소 공간을 갖는다.
    • OS는 이 공간을 코드, 데이터, 힙, 스택 등으로 구분하고, 접근 가능한 페이지를 페이지 테이블(PTE)로 관리한다.
    • 잘못된 주소 접근 시 MMU가 trap을 발생시키고, OS는 SIGSEGV 시그널을 발생시켜 해당 프로세스를 종료시킨다.

PintOS에서의 Segfault

핀토스에서도 유저 프로세스가 잘못된 포인터를 사용하거나, 허용되지 않은 커널 메모리를 읽으려 하면 세그폴트가 발생하고, 이는 커널에서 페이지 폴트(Page Fault) 예외로 처리된다.

 

🔸 잘못된 유저 포인터 사용

int write(int fd, const void *buffer, unsigned size) {
    if (!is_valid(buffer)) exit(-1);
    ...
}
  • buffer가 유효한 유저 주소가 아니면 → is_valid()가 false를 리턴하고 프로세스를 exit(-1) 시킴.
  • 만약 검사를 빼먹고 잘못된 주소를 putbuf() 등에 넘기면, 커널은 실제로 존재하지 않는 주소를 접근하게 되어 page fault → kernel panic 발생 (Segmentation Fault 유사)

🔸 사용자 코드에서 NULL 또는 unmapped 주소 접근

char *p = NULL;
*p = 'A';  // in user program
  • load() 이후 실행되는 유저 프로그램에서 이런 코드를 실행하면, CPU는 해당 주소를 페이지 테이블에서 찾지 못하고 → page fault exception 0x0e (#PF) 발생
  • userprog/exception.cpage_fault() 함수에서 trap 처리

🔹 PintOS의 Segfault 처리 흐름

1. 사용자 프로세스가 잘못된 주소 접근 → 하드웨어가 Page Fault 예외 발생 (#PF)

2. exception.cpage_fault() 함수 호출

void page_fault (struct intr_frame *f) {
    void *fault_addr = rcr2();  // fault 발생한 주소
    ...
    if (not_valid_user_addr(fault_addr))
        exit(-1);  // 잘못된 접근: 프로세스 종료
}

3. 접근 주소가 유저 메모리 아니거나, 매핑 안 되어 있으면 → 프로세스를 종료 (exit(-1))

🔹 MLFQS

더보기

Multi-Level Feedback Queue Scheduler #

  • 정의
    • 운영체제에서 사용하는 CPU 스케줄링 알고리즘 중 하나
    • 다단계 큐(Multi-Level Queue) 기반에 피드백(우선순위 조정) 기능을 결합한 방식
    • 다단계 큐로 작업을 나누고, 각 작업의 행동에 따라 우선순위를 동적으로 조절하는 똑똑한 스케줄러
  • 주요 특징
특징 설명
큐 여러 개 여러 개의 우선순위 큐 사용 (보통 0~N단계)
우선순위 기반 실행 높은 우선순위 큐가 먼저 실행됨
피드백 적용 CPU 사용 시간, 입출력 여부 등을 바탕으로 우선순위 조정
비선점/선점 모두 가능 구현 방식에 따라 유동적
  • 동작 방식
    1. 처음 프로세스 생성 시 중간 우선순위 큐에 배치
    2. CPU를 오래 점유하면 → 낮은 큐로 강등
    3. 입출력 중심, 짧게 실행하는 프로세스 → 높은 큐로 승급
    4. 정기적으로 전체 우선순위 재조정 (aging 등으로 starvation 방지)
  • 장점
장점 설명
반응성 우수 짧고 빈번한 작업(인터랙티브)은 빠르게 처리됨
CPU 점유 작업 억제 무거운 연산 작업은 자동으로 우선순위 낮아짐
자동 우선순위 조절 사용자가 우선순위를 설정할 필요 없이 운영체제가 관리
  • 단점 : 구현 복잡도 ↑, 연산량 증가
  • 핵심 지표
    1. nice value (nice)
      • 프로세스가 자발적으로 낮은 우선순위를 가지도록 하는 값
      • 값이 높을수록 CPU 사용을 덜 하겠다는 의미
    2. recent_cpu
      • 해당 스레드가 최근에 CPU를 얼마나 사용했는지 나타내는 값
      • 높을수록 우선순위 낮아짐
    3. load_avg
      • 전체 시스템의 평균적인 CPU 부하량
      • 프로세스가 많을수록 증가함
  • 우선순위 계산식
    priority = PRI_MAX - (recent_cpu / 4) - (nice * 2)
    • recent_cpu가 클수록 → 우선순위 낮아짐
    • nice가 클수록 → 우선순위 낮아짐
    • 결국 짧게 실행되고 nice가 낮은 프로세스가 우선순위 높음
  • 주기적 업데이트
시점 동작
매 틱(timer tick) recent_cpu 증가 recent_cpu += 1
매 4틱 우선순위 재계산
매 1초 load_avg 재계산 load_avg = (59/60) * load_avg + (1/60) * ready_threads
recent_cpu 재계산 recent_cpu = (2*load_avg)/(2*load_avg + 1) * recent_cpu + nice

실제 구현 흐름

🔹 thread_tick()

void thread_tick(void) {
  if (thread_mlfqs) {
    mlfqs_increment();  // recent_cpu++
    if (timer_ticks() % TIMER_FREQ == 0)
      mlfqs_update_load_avg_and_recent_cpu();
    if (timer_ticks() % 4 == 0)
      mlfqs_update_priority(thread_current());
  }
}
  • mlfqs_increment(): 현재 실행 중 스레드의 recent_cpu 증가
  • mlfqs_update_load_avg_and_recent_cpu(): load_avg 및 전체 스레드 recent_cpu 갱신
  • mlfqs_update_priority(): 우선순위 계산

🔹 thread_create() 시 초기값 설정

if (thread_mlfqs) {
  t->nice = thread_current()->nice;
  t->recent_cpu = thread_current()->recent_cpu;
  mlfqs_update_priority(t);
}