[운체] 오늘의 삽질 - 0717

방법이있지·2025년 7월 17일
post-thumbnail

커널 모드, 사용자 모드

  • 커널 영역 vs 사용자 영역
    • 커널 영역: 운영체제 코드가 적재되는 메모리 영역
    • 사용자 영역: 커널 영역을 제외한 나머지 영역으로, 사용자 프로그램이 적재됨
  • CPU는 명령어를 사용자 모드 or 커널 모드로 실행할 수 있음
  • 사용자 모드: 커널 영역의 코드를 실행할 수 없는 모드
    • 일반적인 응용(사용자) 프로그램은 사용자 모드로 실행됨
    • 사용자 모드로 실행 중인 CPU는, 하드웨어 자원에 접근하는 명령어 실행 불가 (e.g., 입출력)
      • 이 경우 CPU가 예외를 발생시키고, 운영체제가 프로세스를 제거함
  • 커널 모드: 커널 영역의 코드를 실행할 수 있는 모드
    • 시스템 콜: 운영체제의 서비스를 제공받기 위해, 커널 모드로 전환하는 방법
      • CPU가 지금까지의 작업을 백업하고, 커널 영역 내 코드를 실행한 뒤, 기존 사용자 프로그램으로 복귀
  • 시스템 콜 예시: 사용자 프로그램이 하드디스크에 데이터를 저장할 때?
    • 커널 모드에서만 하드디스크에 접근할 수 없음
    • (1) 하드디스크에 데이터를 저장하는 시스템 콜 발생 -> 커널 모드로 전환
    • (2) 커널 영역 내, 하드디스크에 데이터를 저장하는 코드 실행
    • (3) 하드디스크 접근이 끝남 -> 사용자 모드로 전환, 기존 사용자 프로그램으로 복귀
  • 시스템 콜은 소프트웨어 인터럽트
    • (1)에서 시스템 콜을 호출하면 소프트웨어 인터럽트가 발생되고, CPU가 커널 모드로 전환됨
    • (2)에서 커널의 시스템 콜 핸들러가 실행되어, 적절한 커널 함수를 호출
  • Project 1에서 구현한 Threads 코드는 커널 영역의 코드. 즉 사용자 영역은 손댈 일이 없었음
  • Project 2에선 사용자 프로그램을 실행하고, 시스템 콜을 보내는 과정을 구현하게 됨

사용자 프로그램 실행하기

페이지 테이블 개념 복습

  • 페이징: 메모리의 물리 주소 공간을 프레임 단위로, 프로세스의 논리 주소 공간을 페이지 단위로 자른 뒤, 각 페이지를 프레임에 할당

    • 페이지와 프레임의 크기는 동일함 (핀토스 기준 4KB, 4096 바이트)
  • 페이지 테이블: 페이지 번호와 프레임 번호를 짝지어 줌

  • 현재 프로세스의 페이지 테이블 주소는 PTBR(페이지테이블 레지스터)에 저장됨

  • 논리 주소가 물리 주소로 변환되는 과정

    • 논리 주소는 가상 페이지 번호(VPN)offset으로 이루어짐
    • 페이지 테이블을 통해, VPN에 해당하는 물리 프레임 번호(PFN)를 찾음
      • VPN은 페이지 테이블에서 특정 엔트리를 가리키는 인덱스 역할을 함
      • 해당 엔트리엔 PFN이 저장되어 있음.
      • PFN은 말은 번호지만, 실제로는 특정 물리 프레임의 시작 주소라고 이해하는 게 편함
    • 이후 상위 비트에 PFN, 하위 비트에 offset 두고 연결하면 물리 주소로 변환 완료

  • 위 그림엔 물리프레임 대신 물리 페이지, PFN 대신 PPN(물리페이지 번호)라는 용어를 사용했음에 유의.

핀토스에서의 가상 주소

  • 핀토스의 가상 주소는 64비트로 구성됨
    • 하지만 실제로는 하위 48비트만 사용됨
    • Sign Extend (16비트): 64비트 구조 유지를 위한 잉여 비트. 47번째 비트의 값을 단순 복사함.
  63           48 47         39 38         30 29         21 20        12 11         0
+---------------+-------------+-------------+-------------+------------+-------------+
| Sign-Extend   | PML4 Offset | PDP Offset  | PD Offset   | PT Offset  | Page Offset |
+---------------+-------------+-------------+-------------+------------+-------------+
      16 bits         9 bits       9 bits        9 bits       9 bits       12 bits
  • 굉장히 복잡해 보이는데... 4단계의 페이지 테이블 구조라고 생각하면 됨
    • PML4 -> PDP -> PD -> 페이지 테이블을 따라 저장됨.
    • 각 테이블의 엔트리는 64비트 크기로, 상위 52비트엔 다음 단계 테이블의 물리 주소가, 하위 12비트엔 제어 비트가 저장됨
  • 가상 주소 -> 물리 주소가 변환되는 과정
    • CR3 레지스터: 현재 실행 중인 프로세스의 PML4 테이블의 물리 주소가 저장됨.
    • PML4 Offset (9비트): PML4 테이블에서, PD 테이블의 물리 주소를 찾음
      • 엔트리 주소 = PML4 테이블 주소 + (PML4 Offset × 8)
      • 이 엔트리에서 PDP 테이블의 물리 주소를 얻음.
    • PDP Offset (9비트): PDP 테이블에서, PD 테이블의 물리 주소를 찾음
      • 엔트리 주소 = PDP 테이블 주소 + (PDP Offset × 8)
      • 이 엔트리에서 PDP 테이블의 물리 주소를 얻음
    • PD Offset(9비트): PD 테이블에서, 페이지 테이블의 물리 주소를 찾음
      • 페이지 테이블 주소 = PD 테이블 주소 + (PD Offset × 8)
      • 이 엔트리에서 페이지 테이블의 물리 주소를 얻음
    • PT Offset(9비트): 페이지 테이블에서, 페이지 프레임의 물리 주소를 찾음
      • 프레임 주소 = 페이지 테이블 주소 + (PT Offset x 8)
      • 이 엔트리에서 프레임 주소, 즉 PFN를 얻음
    • Page Offset(12비트): 52비트의 FPN에 12비트의 Page Offset을 연결해, 최종 64비트의 물리 주소를 계산.
  • x8을 해 주는 이유: 각 페이지 테이블 엔트리의 크기가 64비트이므로, 8바이트만큼 곱하는 것.

핀토스에서 사용할 매크로

include/threads/vaddr.h

/* Page offset (0:12). */
#define PGSHIFT 0                          /* 첫 비트는 0비트 */
#define PGBITS  12                         /* 오프셋 비트는 총 12비트 */
#define PGSIZE  (1 << PGBITS)              /* 페이지 크기 (바이트) */
  • PGSIZE: 1 << 12 = 2 ^ 12 = 4096. 한 페이지는 4096Byte, 즉 4KB로 구성됨.
#define PGMASK  BITMASK(PGSHIFT, PGBITS)   /* Page offset bits (0:12). */
#define pg_ofs(va) ((uint64_t) (va) & PGMASK) /* 가상 주소의 page offset만 추출 */
#define pg_no(va) ((uint64_t) (va) >> PGBITS) /* 가상 주소의 page number만 추출 */
  • va는 우리가 사용할 가상 주소
  • PGMASK는 Page Offset에 해당하는 하위 12비트는 1, 나머지 52비트는 0으로 설정된 비트마스크
  • pg_ofs: vaPGMASK AND 비트 연산 -> 하위 12비트, 즉 page offset만 추출할 수 있음
  • pg_no: va의 하위 12비트는 page 내 offset이라면, 상위 52비트는 page number로 사용가능
/* 현재 가상 주소를, 페이지 단위로 내림 */
#define pg_round_down(va) (void *) ((uint64_t) (va) & ~PGMASK)
/* 현재 가상 주소를, 페이지 단위로 올림 */
#define pg_round_up(va) ((void *) (((uint64_t) (va) + PGSIZE - 1) & ~PGMASK))
  • pg_round_downva가 속한 페이지의 시작 주소를 반환
  • pg_round_up
    • va가 이미 페이지 시작 주소이면, 시작 주소를 그대로 반환
    • va가 페이지 중간에 있는 경우, va가 속한 다음 페이지의 시작 주소를 반환
/* vaddr이 사용자 영역에 위치하면 true */
#define is_user_vaddr(vaddr) (!is_kernel_vaddr((vaddr)))
/* vaddr이 커널 영역에 위치하면 true  */
#define is_kernel_vaddr(vaddr) ((uint64_t)(vaddr) >= KERN_BASE)
  • KERN_BASE엔 커널 영역의 시작 주소가 저장됨
  • 즉 이 값과 비교하면, 사용자 / 커널 영역 중 어디에 vaddr이 위치해 있는지 확인 가능
/* 물리 주소 paddr에 대응되는 커널 영역의 가상 주소 반환 */
#define ptov(paddr) ((void *) (((uint64_t) paddr) + KERN_BASE))

/* 커널 영역의 가상 주소 vaddr에 대응되는 물리 주소 반환 */
#define vtop(vaddr) \
({ \
	ASSERT(is_kernel_vaddr(vaddr)); \
	((uint64_t) (vaddr) - (uint64_t) KERN_BASE);\
})
  • x86-64에선 물리 주소로 직접 메모리에 접근할 수 없음
  • 핀토스에서는 커널 가상 주소를 물리 주소와 일대일 대응시켜 해결
  • 가상 주소 KERN_BASE -> 물리 주소 0
  • 가상 주소 KERN_BASE + 0x1234 -> 물리 주소 1234와 같은 식...
  • 즉 커널 가상 주소에 KERN_BASE를 빼면 물리 주소가 됨 (vtop)
  • 반대로 물리 주소에 KERN_BASE를 더하면 커널 가상 주소가 됨 (ptov)

include/threads/mmu.h

/* 페이지 테이블 엔트리가 가리키는 페이지에, 사용자 모드 접근이 가능한지 확인 */
#define is_user_pte(pte) (*(pte) & PTE_U)
#define is_kern_pte(pte) (!is_user_pte (pte))

/* 페이지 테이블 엔트리가 가리키는 페이지에, 쓰기가 허용되는지 반환 */
#define is_writable(pte) (*(pte) & PTE_W)
  • 페이지 테이블 엔트리의 하위 12비트는 제어 비트.
  • PTE_U0x4. 하위 3번째 비트가 1인 경우 사용자 접근 가능, 0인 경우 커널만 접근 가능.
  • PTE_W0x2. 하위 2번째 비트가 1인 경우 쓰기 허용, 0인 경우 쓰기 불허.
typedef bool pte_for_each_func (uint64_t *pte, void *va, void *aux);
bool pml4_for_each (uint64_t *pml4, pte_for_each_func *func, void *aux);
  • pml4_for_each
    • 최상위 페이지 테이블 PML4의 주소를 인자로 받음 (uint64_t *pml4)
    • PML4의 엔트리를 순회하며, 각 엔트리가 가리키는 PDP -> PD -> PT...를 재귀적으로 내려감
    • 페이지 테이블까지 내려간 뒤, 각 페이지 테이블 엔트리 pte에 대해 pte_for_each_func 호출
  • pte_for_each_func
    • pte: 페이지 테이블 엔트리의 포인터
    • va: pte가 매핑하는 가상 주소 범위의 시작 주소. 앞선 매크로 함수들의 매개변수로 사용 가능.
    • 해당 함수가 false를 반환하면, pml4_for_each는 반복을 멈추고 false 반환
static bool
stat_page (uint64_t *pte, void *va,  void *aux) {
        if (is_user_vaddr (va))
                printf ("user page: %llx\n", va);
        if (is_writable (va))
                printf ("writable page: %llx\n", va);
        return true;
}
  • 위 함수를 pml4_for_eachfunc에 대입
    • 모든 유저 영역이거나 (is_user_vaddr), 쓰기 가능한 (is_writable) 페이지들의 가상 주소를 출력.

가상 메모리

  • 가상 메모리는 사용자 영역 / 커널 영역으로 나뉨
    • 사용자 영역: 0부터 KERN_BASE-1 (기본값 0x8004000000)
    • 커널 영역: KERN_BASE부터 나머지 영역
  • 사용자 영역은 프로세스 각각 다름
    • 커널이 프로세스 간 전환할시, Page Directory Base Register(앞서 본 CR3 레지스터)의 값을 변경
    • 즉 이후 페이징 시, 전혀 다른 페이지 테이블을 찾아가게 됨
    • struct threaduint64_t *pml4는, 해당 쓰레드의 PML4 테이블 주소를 저장함
  • 커널 영역은 모든 프로세스가 공유
    • 핀토스에선 앞서 봤듯이, 물리 메모리와 커널 영역의 가상 메모리가 1-1 대응됨
    • 가상 주소 KERN_BASE는 물리 주소로 0, KERN_BASE + 0x1234는 물리 주소로 0x1234와 같은 방식
  • 사용자 프로그램은 사용자 영역만, 커널 쓰레드는 사용자 + 커널 영역 모두 접근 가능

메모리의 구조

  • 위 그림과 같음. Project 2에선 사용자스택의 크기가 유지되지만, 3에선 자랄 수 있음

포인터 조심하기

  • 시스템 콜에서, 커널이 사용자 프로그램이 제공한 포인터로 메모리에 접근해야 할 수 있음
  • 이때, 이러한 포인터는 다 빡꾸시켜야 함!! 이후 이런 콜을 보낸 프로세스도 종료시켜야 함.
    • 널 포인터 (더 이상 말할 것도 없음)
    • 매핑되지 않은 가상 메모리 공간
    • 커널 영역 (주소가 KERN_BASE 이상인 경우. is_kernel_vaddr, is_kern_pte 써라!)
  • 제일 간단한 방법은, 포인터를 빡꾸시켜야 하는지 확인 (앞선 함수들 활용)한 뒤, 올바른 값일 때만 참조 연산자를 사용하는 것.
  • 아니면 포인터가 KERN_BASE 미만인지만 확인하고, 다른 경우는 userprog/exception.cpage_fault()에서 처리해도 괜찮음. 하지만 위 방법이 간단해 보여서, 정리는 하지 않았음.

핀토스 파일 시스템

  • 사용자 프로그램을 디스크에서 읽을 때, 시스템 콜을 구현할 때... 파일 시스템 쓸 일 많음.
  • filesys 폴더의 filesys.h, file.h에 많이 있는데, 얘를 수정할 필요는 없음.
  • 하지만 이번 테스트 케이스에는 사용자 프로그램을 만들어 핀토스에 돌릴 일이 있음.
    • make check는 자동으로 해 주는데, 직접 테스트 케이스 돌릴 거면 이런 절차를 진행해야 함.
pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'
  • (1) --fs-disk=10
    • 10MB의 임시 파일 시스템 디스크를 생성한다
    • Pintos 실행이 끝나면 디스크는 자동삭제된다
  • (2) -p tests/userprog/args-single:args-single
    • -- 앞쪽 명령어는, 호스트 머신에서 실행되는 핀토스 스크립트의 옵션
    • -p(put): tests/userprog/args-single 파일을 가상파일 시스템에 복사한다
    • 복사한 파일명은 : 다음에 args-single으로 지정한다
  • (3) -- -q -f run 'args-single onearg'
    • -- 뒤쪽 명령어는, 핀토스 커널에 전달되는 인자.
    • -q(quit): 명령 실행 후 Pintos를 자동 종료한다
    • -f: 가상 파일 시스템을 포맷(초기화)한다
    • run 'args-single onearg': args-single 프로그램을 onearg 인자를 전달해 실행한다

사용자 프로그램 실행하기

  • run 'echo x y z'가 실행된 상황이라 치자.
    • 이때 run_task에서 argv[0]run, argv[1]'echo x y z'가 됨.
    • argv[1]"'echo x y z'"는 계속 내부 함수의 매개변수로 전달되며, 지겹도록 많이 사용됨.
    • 이거를 echo, x, y, z로 파싱해서 알아서 프로그램이 잘 실행되게끔 하는 게 우리 목표.

벨로그에 이미지 업로드가 잘 안 돼서 별도의 글로 남깁니다.

  • [구현 1-1] 현재 process_wait 함수가 정상 실행되지 않음
    • 실행된 사용자 프로그램의 쓰레드가 종료될 때까지 기다려야 함.
    • 일단 임시로 무한 루프를 넣어 동작하게끔 땜빵 처리할 수 있음.
  • [구현 1-2] process_create_initd에서 쓰레드 만들 때, 쓰레드 이름 수정해야 함
    • 현재 위 함수 내 thread_createfile_name 매개변수로는 echo x y z가 전달됨
    • 원본 echo x y zfn_copy에 백업한 뒤, thread_create의 쓰레드 이름으로는 echo만 전달해야 함.
  • [구현 1-3] load에서 사용자 스택 초기화가 완료된 이후, argument passing하기
    • 즉 현재 'echo x y z'echo, x, y, z로 알아서 잘 나눈 뒤, _if (struct intr_frame 구조체: 대충 쓰레드의 레지스터 정보 저장됨) 사용자 스택 (_if->rsp ) 에 전달해야 함
    • 그러면 do_iret_if를 참고해서, 레지스터 정보를 복원한 뒤 사용자 프로그램의 main()로 이동하게 됨.
    • 이건 좀 더 공부해야 할 것 같다.
  • 더 생길 것 같은데, 일단 생략.

목표: 내일은 매개변수 전달 과정에 대해 조금 더 이해해 보는 걸로

profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글