세리프 따라잡기

WEEK10 - PintOS Project2 - WIL [team.ver] 본문

SW사관학교 정글

WEEK10 - PintOS Project2 - WIL [team.ver]

맑은 고딕 2022. 6. 5. 18:39

PintOS Project2 - WIL 😎

WIL(Weekly I lerned)

Lock

  • 대부분의 기능들 안에 공유자원을 동기화하기 위한 코드가 필요
int write(int fd, const void *buffer, unsigned size)
{
	check_address(buffer);
	lock_acquire(&filesys_lock);

	...
	file_write(file_obj, buffer, size); // 공유자원(file)에 대한 작업
	...

	lock_release(&filesys_lock);

	return ret;
}
  • file 은 여러 프로세스가 사용할 수 있는 공유 자원이다.
    • ⇒ 파일 관련 system call 처리 함수 안에 lock 추가 필요!

TSS**(task state segment)**

  • tss_update() 함수를 통해 tss 라는 레지스터 정보를 해당 Kernel 스레드의 특정 지점을 가리키게 만들어 놓고, 추후에 syscall 이 불렸을 때 이를 바로 rsp의 값을 해당 지점의 값으로 교체하면 User 영역에서 Kernel 영역으로 옮길 수 있다.
struct task_state {
	...
	uint64_t rsp0;
	uint64_t rsp1;
	uint64_t rsp2;
	...
}__attribute__ ((packed));

struct task_state *tss = tss_get ();
  • 사용자가 syscall 호출 시, Kernel 영역의 어느 부분으로 rsp를 옮겨야 할 지 알려주는 struct
syscall_entry:
	movq %rbx, temp1(%rip)
	movq %r12, temp2(%rip)     /* callee saved registers */
	movq %rsp, %rbx            /* Store userland rsp    */
	movabs $tss, %r12
	movq (%r12), %r12
	movq 4(%r12), %rsp         /* Read ring0 rsp from the tss */\
	
	/* ------ Now we are in the kernel stack ------*/
	push $(SEL_UDSEG)      /* if->ss */
	push %rbx              /* if->rsp */
	push %r11              /* if->eflags */
  • __do_fork(), schedule(), load() 함수에서 process_actiavate() 함수를 호출하여 Context-Switching 될 때마다 tss를 update하여 새롭게 실행되는 스레드의 syscall을 올바르게 handling 할 수 있도록 함.
void tss_update (struct thread *next) {
	ASSERT (tss != NULL);
	tss->rsp0 = (uint64_t) next + PGSIZE;
}


void process_activate(struct thread *next)
{
	/* Activate thread's page tables. */
	pml4_activate(next->pml4);

	/* Set thread's kernel stack for use in processing interrupts. */
	tss_update(next);
}

페이징(Paging)

Page

가상메모리를 사용하는 최소 크기 단위 (PintOS 기준 Page 크기는 4KB)

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT) // 4KB

Page table

프로세스의 Page 번호에 해당하는 Frame 번호를 관리하고 있는 자료구조

  • PCB에 Page table 구조체를 가리키는 주소가 들어있음 (메모리 안에 OS가 관리할 수 있는 영역에 저장되어 있음)
  • 프로세스 구동 시, 해당 Page Table 주소가 별도 레지스터에 저장(CR3)
    • (→ MMU가 Page Table Base 주소에 접근해서, 물리주소를 가져와 CPU에 넘겨준다.
void process_activate(struct thread *next) {
	...
	pml4_activate(next->pml4);	/* Activate thread's page tables. */
	...
}

/* Loads page directory PD into the CPU's page directory base register. */
void pml4_activate (uint64_t *pml4) {
	lcr3 (vtop (pml4 ? pml4 : base_pml4));
}

__attribute__((always_inline))
static __inline void lcr3(uint64_t val) {
	__asm __volatile(
		"movq %0, %%cr3" 
		: 
		: "r" (val)
	);
}
  • 기존의 Page Table은 Linear하게 (가상주소 - 물리주소)쌍이 담겨 있음
  • 이럴 경우, 크기가 작은 프로세스가 4GB 의 주소 공간 중 지극히 일부분만 사용하더라도 Page Table 을 위한 일정 크기의 메모리 할당이 필요

다중 단계 페이징 기법

  • 주소 변환을 위한 페이징 정보를 여러 단계를 나누어 생성
  • 사용하지 않는 주소 공간에 대해서는 외부 페이지 테이블의 항목을 NULL로 설정하며, 여기에 대응하는 내부 페이지 테이블을 생성하지 않는다.
    • ⇒ 필요없는 페이지는 없애고, 공간 절약 가능

  • pml4란 (페이지 맵 level 4)
    • pml4_create():
      • 프로세스마다 Page Table이 존재하므로 load(), __do_fork() 함수에서 해당 프로세스의 Page Table 생성
    • pml4_get_page() (1단계)
      • pml4에서 사용자 가상 주소(uaddr)에 해당하는 실제 주소를 찾는다.
    • pml4e_walk() (2단계)
      • 가상 주소 vaddr에 대한 페이지 테이블 엔트리의 주소를 반환한다.
      • pdpe_walk() (3단계)pgdir_walk() (4단계)

 

 


팀원: ㅎㄷㅇ, ㅈㅇㅅ, me 😏

발표자: ㅎㄷㅇ👦

더보기

8조 WIL 발표 시작하겠습니다.


두 번째 프로젝트 User Program을 다루면서 많은 내용을 배웠는데, 저희 조가 그 중에서도 같이 공유했으면 하는 내용들을 선별해봤습니다.

 

먼저, Lock 과 관련된 내용입니다.
저희가 기존에 무심코 사용했던 많은 기능들 안에도 공유자원을 동기화하기 위한 코드가 필요했습니다.
파일은 여러 프로세스가 사용할 수 있는 공유자원이기 때문에 동시접근을 막기 위해 Locking 기법을 사용했습니다.

 

여기 파일에 데이터를 기록하는 write()함수입니다.
Lock을 획득한 프로세스만이 file에 접근하여 쓰는 작업을 처리하고
작업을 완료하면 Lock을 반납하여 다른 프로세스의 접근을 허용하도록 하였습니다.


이 외에도 저희는 파일 관련된 시스템 콜 처리 함수에 Lock을 사용하여 동기화 문제를 해결했습니다.

 

다음으로 Task State Segment 라고 불리는 TSS와 관련된 내용입니다.

TSS는 사용자 프로세스가 인터럽트가 걸렸을 때나 시스템콜 호출 시에, Kernel 모드로 전환될 때 사용됩니다.
레지스터 스택 포인터라고 불리는 rsp가 Kernel 영역의 어디를 가리켜야 할 지 알려줍니다.
사용자 프로세스는 자신의 가상메모리 위쪽에 실제 물리메모리와 1:1로 mapping된 Kernel 영역이 있습니다.
이렇게되면, 프로세스들끼리 Kernel 영역을 공유하게 되고 그 안에 여러개의 User 프로세스의 Kernel 스레드가 존재하게됩니다.

 

여러개의 Kernel 스레드 중에서 User 프로세스 자신을 실행시킨 Kernel 스레드에 접근해야 하는데
이 때, TSS를 활용하여 올바른 Kernel 스레드에 접근할 수 있습니다.

 

pintOS에서는 task-state라는 구조체를 정의하여 system call 이 요청되었을 때, 해당 구조체 안의 rsp 멤버변수의 값을 레지스터에 넣어 Kernel 모드로 진입할 수 있게 합니다.
예를 들어, syscall-entry.S 함수의 코드입니다. tss 구조체 안의 값을 rsp 레지스터에 넣어 커널모드로 진입하는 것을 볼 수 있습니다.
이렇게, 특정 사용자 프로세스에 대응하는 커널을 하나하나 찾을 필요 없이 TSS 구조체 안의 값을 읽어 바로 커널 스택 끝 주소를 알아낼 수 있습니다.

 

process_activate() 함수는 __do_fork(), schedule(), load() 함수에서 호출되면서 컨텍스트 스위칭 될 때마다 tss값을 업데이트 해줍니다. 이렇게 함으로써 매번 새롭게 실행되는 system call을 handling 할 수 있게 해줍니다.

 

마지막으로, 페이징 기법입니다.
페이지는 가상 메모리를 사용하는 최소 크기 단위로서
pintos skeleton 코드에서는 page의 크기를 4KB라고 정의하고 있습니다.페이지 테이블은 각 프로세스마다 갖고있으며, 가상메모리 Page번호와 매핑되는 물리메모리의 Frame 번호를 관리하고 있는 자료구조입니다.
PCB 안에 이 페이지 테이블을 가리키는 주소가 들어있는데,
프로세스가 실행될 때, 해당 프로세스의 Page Table의 주소가 cr3라고 불리는 별도 레지스터에 저장됩니다.

 

아래 핀토스 코드를 보시면 이해가 되실 겁니다.
Process_activate() 함수를 호출하면 안에 pml4_activate()함수가 호출되고이 함수 안의 Lcr3함수에서 cr3 레지스터에 page table의 주소를 저장하는 것을 볼 수 있습니다.

 

이 Page Table안에 entry들을 Linear하게 [가상주소]-[물리주소] 쌍을 담을 경우,
크기가 작은 프로세스가 총 4GB 가상 메모리의 주소 공간 중 지극히 일부분만 사용하더라도 페이지 테이블을 위한 일정 크기 할당이 필요하게 됩니다.
이렇게되면 사용하지 않는 공간도 페이지를 나누어 할당하게 되면서 메모리를 낭비하게 됩니다.

이러한 메모리 낭비를 막기 위해 다중 단계 페이징 기법을 사용합니다.
페이징 정보를 여러 단계로 나누어 기존의 Page Table을 위한또 다른 Page Table을 만드는 방법입니다.

정말 필요한 영역에 대해서만 페이지 테이블을 생성함으로써, 필요없는 페이지를 만들지 않고 메모리를 아낄 수 있습니다.

현재 pintos에서는 4단계 페이징 기법을 사용하였으며, 핵심적인 역할을 하는 함수를 꼽아 봤습니다.먼저, 프로세스마다 페이지 테이블이 존재하기 때문에 load(), __do_fork()함수에서 pml4_create()함수를 호출하여 해당 프로세스의 페이지 테이블 생성합니다.

주소를 찾는 과정은 4가지 단계를 거치는데 pml4_get_page() -> pml4e_walk() 순으로 단계적으로 접근해서
사용자 가상 주소에 대한 물리 주소를 가져옵니다.

페이징 기법 관련 내용은 바로 다음 프로젝트인 가상메모리에서 주로 다룰 것 같아, 자세한 내용은 다음 WIL때 말씀드리겠습니다.


감사합니다.

    •  
Comments