[WIL] PintOS 프로젝트와 함께 알아보는: Interrupt Frame, TCB, PCB

@developer/takealittle.time·2024년 11월 20일
1

KAIST PintOS Project

목록 보기
8/9
post-thumbnail


00. 들어가며

  • PintOS Project를 하면서 느끼는 즐거움 (보상)은 어디서 나오는가?
    → 책으로만 읽었던 / 개념론적으로만 학습했던 내용들을 직접 코드로 옮겨가며 머릿속 퍼즐들이 들어맞춰질 때 오는 지적 쾌감 🧩

    ex) 멀티 스레딩 방식으로 코드 구현을 할 때, 공유 자원을 위해 사용되었던 Semaphore, Lock 등의 동기화 도구가 실제 운영체제 커널 안에서 이런 식으로 동작하는구나!

  • 이번 주차에 궁금했던 것?
    학습한 개념론적인 내용에서는 Context Switch가 발생할 때 해당 Context 정보를 TCB, PCB에 저장한다고 했다.
    PintOS 안에서 TCB, PCB는 어디에 있을까? 🤔

    TCB, PCB라고 명명된 구조체를 확인하지 못했는데, 2주차 프로젝트 중에 문맥 정보를 저장하는 interrupt frame 구조체의 등장으로 머릿속이 혼잡해졌었다.

01. TCB (Thread Control Block)와 PCB(Process Control Block)

01-1. TCB (Thread Control Block)

스레드 단위로 관리되는 구조체로, 각 스레드의 문맥과 제어 정보를 저장.

  • 하나의 PCB(Process Control Block)에 여러 TCB가 있을 수 있다.
    (즉, 프로세스 내의 스레드 단위로 관리.)


* 위 그림은 Linux에서의 TCB와 PCB의 구조이다.

<주요 특징>

1. 구조

  • 스레드 ID: 스레드의 고유 식별자.
  • 상태 정보: 스레드가 실행 중인지, 대기 중인지 등.
  • 스택 포인터: 스레드의 스택 위치.
  • 레지스터 상태: 실행 중이던 레지스터 값들.
  • 우선순위: 스레드 스케줄링 우선순위.
    ...

2. 역할

  • 스레드 간 문맥 전환 시 상태를 저장하고 복구.
  • 스레드별 자원(스택, 로컬 변수 등)을 관리.

3. 사용 사례:

  • 멀티스레드 프로그래밍에서 각 스레드의 상태를 관리.
  • 문맥 전환 시 현재 스레드의 상태를 저장하고, 다음 스레드의 상태를 복구.

"이런 비슷한 구조, 어디서 봤다!" 💡

  • PintOS 내 threads/thread.h 의 struct thread

/* threads/thread.c */

struct thread {
	/* Owned by thread.c. */
	tid_t tid;                          /* Thread identifier. */
	enum thread_status status;          /* Thread state. */
	char name[16];                      /* Name (for debugging purposes). */
	int priority;                       /* Priority. */
	int init_priority;									/* 해당 스레드의 '원래' Priority. */
	int nice;														/* nice값: 클수록 CPU 양보 ↑ */
	int recent_cpu;											/* 해당 스레드가 최근 얼마나 많은 CPU time을 사용 했는지 */
	struct list donations;							/* 해당 스레드가 가지고 있는 lock을 필요로 하는 스레드들 */
	struct list_elem d_elem;						/* donations 리스트를 쓰기 위한 리스트 요소 */
	struct lock * wait_on_lock;					/* 해당 스레드가 기다리고 있는 lock을 가리키는 포인터 */ 
	int64_t wake_tick; /* 스레드가 깨어날 시각*/

	/* Shared between thread.c and synch.c. */
	struct list_elem elem;              /* List element. */

	struct list_elem allelem;						/* List element. */

#ifdef USERPROG
	/* Owned by userprog/process.c. */
	uint64_t *pml4;                     /* Page map level 4 */

	/* Project 2: System Call 구현 */
	int exit_status; // exit 상태를 나타내는 정수형 변수

	struct file **fdt;						// fd 테이블
	int fd_idx;										// fd 인덱스
	struct file *runn_file;				// 실행중인 file

	struct intr_frame parent_if; 	// 해당 스레드의 interrupt frame
	struct list child_list; 			// 자식 프로세스 리스트
	struct list_elem child_elem; 	// 자식 프로세스 리스트 요소

	struct semaphore fork_sema; 	// fork가 완료될 때 sgnal
	struct semaphore exit_sema; 	// 자식 프로세스 종료 signal
	struct semaphore wait_sema; 	//exit_sema를 기다릴 때 사용
#endif

	/* Owned by thread.c. */
	struct intr_frame tf;               /* Information for switching */
	unsigned magic;                     /* Detects stack overflow. */
	
};
  • 위에서 얘기했던 스레드 ID, 상태 정보, 스택 포인터, 레지스터 상태, 우선 순위 등은 모두 struct thread 자료구조에 포함된다.

  • 사실상 struct thread가 TCB에 해당하는 정보들을 가지고 있고, 스레드가 스케줄링 되고 실행 상태가 변화하면서 스레드 간 Context Switching 동작을 수행한다.


01-2. PCB (Process Control Block)

프로세스 단위로 관리되는 구조체로, 각 프로세스의 상태와 자원 정보를 저장.

  • PCB는 스레드 단위의 정보를 포함하는 TCB를 관리하며, 프로세스 자원을 책임짐.


* 위 그림은 Linux에서의 TCB와 PCB의 구조이다.

<주요 특징>

1. 구조

  • 프로세스 ID: 프로세스의 고유 식별자.
  • 프로세스 상태: 실행 중, 대기 중, 종료 등.
  • 프로세스 메모리 정보: 코드, 데이터, 힙, 스택 위치.
  • 파일 디스크립터 테이블: 프로세스가 열고 있는 파일 목록.
  • 스레드 정보: 하나의 PCB에 여러 TCB가 포함될 수 있음.
    ...

2. 역할

  • 프로세스 간 문맥 전환 시 상태를 저장하고 복구.
  • 프로세스 실행에 필요한 자원(메모리, 파일 등)을 관리.

3. 사용 사례

  • 멀티태스킹에서 각 프로세스의 자원과 상태를 관리.
  • 커널이 프로세스 간 문맥 전환을 할 때 참조.

  • 우리가 작성하고 있는 PintOS는 기본적으로 단일 스레드 프로세스를 전제한다.
    즉, 멀티 스레딩을 지원하지 않는다.

    → 스레드의 관리 (TCB)와 프로세스의 관리(PCB)를 구분할 필요가 크지 않다!

    따라서, 단일 자료구조인 struct thread 만으로 TCB와 PCB를 분리하지 않고 구현이 가능하다.

  • 보통 멀티스레드를 지원하는 시스템에서 각 스레드가 TCB를 통해 관리되고, 이러한 스레드가 속한 프로세스는 PCB를 통해 관리된다.
    PintOS와 같은 프로젝트에서는 struct thread로 이러한 구조를 단순화 시키는 것이 효율적이다!


02. Interrupt Frame

/* include/threads/interrupt.h */

struct intr_frame {
    /* intr-stubs.S의 intr_entry에 의해 푸시됨.
       중단된 작업의 저장된 레지스터입니다. */
    struct gp_registers R;  // 정수 레지스터 구간
    uint16_t es;            // Extra Segment - Extra Data 영역
    uint16_t __pad1;
    uint32_t __pad2;
    uint16_t ds;  // Data Segment - 데이터 영역
    uint16_t __pad3;
    uint32_t __pad4;
    /* intr-stubs.S의 intrNN_stub에 의해 푸시됨. */
    uint64_t vec_no; /* Interrupt vector number. */
                     /* 때로는 CPU에 의해 푸시되고, 그렇지 않으면 일관성을 위해 intrNN_stub에 의해 0으로 푸시됩니다.
                        CPU는 이를 'EIP' (Extended) Instruction Pointer 바로 아래에 두지만 우리는 여기로 옮깁니다. */
    uint64_t error_code;
    /* CPU에 의해 푸시됨.
       중단된 작업의 저장된 레지스터입니다.. */
    uintptr_t rip;  // Instruction Pointer = Program Counter 다음에 실행될 명령의 주소
    uint16_t cs;    // Code Segment - 명령어 영역
    uint16_t __pad5;
    uint32_t __pad6;
    uint64_t eflags;  // Extended Flags - 상태, 제어, 시스템 플래그 -> 레지스터가 어떤 일을 수행하는 지
    uintptr_t rsp;    // Stack Pointer - 스택 포인터
    uint16_t ss;      // Stack Segment - 임시 Stack 영역
    uint16_t __pad7;
    uint32_t __pad8;
} __attribute__((packed));

Interrupt 또는 System Call 발생 시 CPU의 실행 상태(레지스터, 스택 포인터, 플래그 등)를 임시 저장.

  • Interrupt, System Call이 발생했을 때, 기존의 실행 상태를 보존하여 작업이 끝난 후 복구할 수 있게 한다.

<주요 특징>

1.구조

  • CPU의 레지스터 값: rax, rbx, rsp, rip 등.
  • 플래그 값: 인터럽트 상태 플래그(rflags).
  • 세그먼트 레지스터: cs, ss 등.
  • 메모리와 레지스터 상태를 테이블처럼 저장.

2. 저장 시점

  • 인터럽트 발생 시 하드웨어가 자동으로 저장하거나, 소프트웨어가 수동으로 저장.
  • 저장된 후 Interrupt Handler / Systemcall Handler가 실행.

3. 사용 사례

  • Interrupt, System Call 등 예외 상황 발생 시 커널이 사용자 프로그램 상태를 임시 저장.
  • 커널과 사용자 영역 간의 임시 전환을 지원

※ PintOS에서 Interrupt Frame의 1번 쓰임

→ Interrupt, System Call 발생 시 레지스터 상태 임시 저장 및 복원 (의미, 개념론적인 interrupt frame으로 사용)

※ PintOS에서 Interrupt Frame의 2번 쓰임

struct thread 구조체 안에 위치하면서, 스레드 간 문맥 전환이 발생할 때에도 레지스터 상태 정보를 저장. (레지스터 상태를 저장하는 자료구조로서 사용)

" 스레드의 레지스터 정보도 저장해야하는데, 이를 어떻게 저장할까? "
" → 이미 레지스터를 저장할 수 있는 구조체가 있으니, 이를 재사용하자! "

/* threads/thread.h */

struct thread {
	/* Owned by thread.c. */
	tid_t tid;                          /* Thread identifier. */
  enum thread_status status;          /* Thread state. */
  char name[16];                      /* Name (for debugging purposes). */
  int priority;                       /* Priority. */
  int origin_priority;                //* 본래 priority

 ...
 
#ifdef USERPROG
	/* Owned by userprog/process.c. */
	uint64_t *pml4;                     /* Page map level 4 */

	/* Project 2: System Call 구현 */
	int exit_status; // exit 상태를 나타내는 정수형 변수

	struct file **fdt;						// fd 테이블
	int fd_idx;										// fd 인덱스
	struct file *runn_file;				// 실행중인 file

	struct intr_frame parent_if; 	// interrupt frame
	struct list child_list; 			// 자식 프로세스 리스트
	struct list_elem child_elem; 	// 자식 프로세스 리스트 요소

	struct semaphore fork_sema; 	// fork가 완료될 때 sgnal
	struct semaphore exit_sema; 	// 자식 프로세스 종료 signal
	struct semaphore wait_sema; 	//exit_sema를 기다릴 때 사용
#endif

  ...
  
  
	/* Owned by thread.c. */
	struct intr_frame tf;               /* Information for switching */
	unsigned magic;                     /* Detects stack overflow. */
};
  • 위에서 parent_if, tf가 말하자면 각각 interrupt frame, thread frame.
    • if는 system call의 처리나 interrupt의 처리에 사용
    • tf는 thread 간 context switch에 사용

03. 차이점 요약


04. 결론.

  • PintOS에서 TCB, PCB라는 이름은 사용되지 않지만, 스레드 관리를 위해 struct thread라는 자료구조를 사용하며, 이는 TCB의 역할을 수행한다.

    • 이 때, 우리가 구현한 PintOS는 기본적으로 단일 스레드 프로세스를 전제하기 때문에, struct thread 만으로 사실상 PCB의 역할까지도 구현할 수 있다.

  • Interrupt Frame은 스레드 간 Context Switch가 아닌 Interrupt, System Call 등 예외 상황이 발생했을 때 커널과 사용자 모드 간 전환을 지원하기 위해 사용된다.
    but! PintOS 안의 thread 정보를 저장할 때 어차피 레지스터 정보도 저장해야 하므로, interrupt framestruct thread 안에서도 재사용 한다.

→ 위 내용의 이해를 바탕으로, PintOS에서 사용자 프로그램 실행 시의 간단한 예시 시나리오

1. 사용자 프로그램 시작

  • 프로세스 실행 요청 → PintOS에서는 struct thread 자료구조에 사용자 프로그램을 실행할 스레드 정보를 포함. → 프로세스 생성 시 해당 스레드에 프로그램의 실행 파일 적재
    • ex) 프로그램의 코드, 데이터, 스택을 메모리에 로드하고 사용자 스택의 초기 상태를 설정.

2. 사용자 프로그램이 실행 상태로 전환

  • 스케줄러가 실행 가능한 스레드를 선택, 해당 스레드로 CPU 제어권 부여 → 스레드가 사용자 모드에서 실행 시작, 프로그램의 명령어들이 순차적으로 실행

3. '특정 상황'에서 발생하는 이벤트

  1. 타이머 인터럽트에 의한 문맥 교환
    • 현재 실행 중인 사용자 프로그램이 일정 시간을 사용하면 → 타이머 인터럽트 발생 → CPU는 현재 실행 상태를 인터럽트 프레임에 저장 → 타이머 인터럽트 핸들러로 제어권 부여 → 스케줄러 호출: 현재 스레드를 계속 실행할 지 결정 → 다른 스레드로 전환이 필요하다면 → 현재 스레드의 실행 상태를 struct thread에 저장하고, 다음 실행할 스레드의 상태를 복원해 실행. (즉, Context Switch) → Context Switch 이후 interrupt handler는 intr_frame을 복구하고 프로그램의 실행을 재개.

++ 시리즈의 이전 글


* 참고자료 / 이미지 출처

profile
능동적으로 사고하고, 성장하기 위한. 🌱

0개의 댓글

관련 채용 정보