ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [xv6] - sbrk
    프로젝트/운영체제 만들기 2023. 7. 26. 18:05

    글의 참고

    - https://github.com/mit-pdos/xv6-public/tree/master

    xv6 - DRAFT as of September 4, 2018


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.

    - 페이징 관련 글이 있기 때문에, 이 글을 먼저 일고 오자.


    글의 내용

    : 위키피디아에서 정의되는 `brk` 및 `sbrk` 함수는 다음과 같다. 요약하면, `brk` 및 `sbrk` 함수를 호출한 프로세스에게 할당된 메모리를 사이즈를 늘려주는 함수다.`program break`라는 용어의 의미는 사이즈를 의미한다. 대개는 할당받는 주소의 시작 주소를 많이 사용하는데, 초기 유닉스 계열에서 사용되던 `brk` 및 `sbrk` 함수는 프로그램 사이즈인 `program break`를 사용했다.

    The `brk` and `sbrk` calls dynamically change the amount of space allocated for the data segment of the calling process. The change is made by resetting the `program break of the process`, which determines the maximum space that can be allocated. The program break is the address of the first location beyond the current end of the data region.
    ...

    - 참고 : https://en.wikipedia.org/wiki/Sbrk#Description

     

    : 유저 레벨의 모든 시스템 콜의 정의는 `usys.S` 파일에 정의되어 있다.

    #include "syscall.h"
    #include "traps.h"
    
    #define SYSCALL(name) \
      .globl name; \
      name: \
        movl $SYS_ ## name, %eax; \
        int $T_SYSCALL; \
        ret
    
    SYSCALL(fork)
    SYSCALL(exit)
    SYSCALL(wait)
    SYSCALL(pipe)
    SYSCALL(read)
    SYSCALL(write)
    SYSCALL(close)
    SYSCALL(kill)
    SYSCALL(exec)
    SYSCALL(open)
    SYSCALL(mknod)
    SYSCALL(unlink)
    SYSCALL(fstat)
    SYSCALL(link)
    SYSCALL(mkdir)
    SYSCALL(chdir)
    SYSCALL(dup)
    SYSCALL(getpid)
    SYSCALL(sbrk)
    SYSCALL(sleep)
    SYSCALL(uptime)

     

    : 위의 코드에서 `SYSCALL(sbrk)`는 어떻게 만들어질까? `SYS_sbrk`라는 상수는 시스템 콜을 호출할 때, `eax` 레지스터에 저장되서 시스템 콜 번호로 사용된다. 

    .globl sbrk
    exec:
        movl $SYS_sbrk
        int $T_SYSCALL
        ret
    // syscall.h
    // System call numbers
    #define SYS_fork    1
    #define SYS_exit    2
    #define SYS_wait    3
    #define SYS_pipe    4
    #define SYS_read    5
    #define SYS_kill    6
    #define SYS_exec    7
    #define SYS_fstat   8
    #define SYS_chdir   9
    #define SYS_dup    10
    #define SYS_getpid 11
    #define SYS_sbrk   12
    #define SYS_sleep  13
    #define SYS_uptime 14
    #define SYS_open   15
    #define SYS_write  16
    #define SYS_mknod  17
    #define SYS_unlink 18
    #define SYS_link   19
    #define SYS_mkdir  20
    #define SYS_close  21

     

    : 유저 레벨에서 `sbrk` 함수를 호출하면, 제일 먼저 만나게 되는 함수는 `sys_sbrk` 함수다. `sysproc.c` 파일에 존재하는 함수들은 커널에서 제공해주는 시스템 콜 함수들이다. 즉, 이 함수가 호출됬다는 것은 이미 커널 영역임을 의미한다. 아래에서 `argint` 함수를 통해 첫 번째(0번째) 인자를 `n`에 제공한다. 이 `n`은 현재 사이즈에서 얼마나 더 늘리고 싶은가에 대한 값이다. 예를 들어, 현재 할당받은 사이즈가 0x4000 일 때, `n`이 0x2424라면, `sbrk` 함수를 통해 증가되는 사이즈는 `0x6424`가 된다. `sys_sbrk` 함수는 내부적으로 `growproc` 함수를 통해 사이즈를 늘린다. 그리고, `sys_sbrk` 함수는 이전 사이즈 크기를 반환한다.

    // sysproc.c
    int sys_sbrk(void)
    {
      int addr;
      int n;
    
      if(argint(0, &n) < 0)
        return -1;
      addr = myproc()->sz;
      if(growproc(n) < 0)
        return -1;
      return addr;
    }

     

    : `growproc` 함수는 전달 인자가 양수이냐 음수냐에 따라 사이즈를 늘리거나 줄인다. 예를 들어, 양수라면 페이지 단위로 사이즈가 증가하고, 음수라면 페이지 단위로 사이즈가 감소한다. 먼저, 현재 프로세스를 알아야 한다. 왜? `brk` 및 `sbrk`는 해당 함수를 요청한 프로세스가 할당받은 메모리 사이즈를 늘린다. 그런데, `brk` 및 `sbrk` 함수를 호출한 프로세스와 `growproc` 함수를 호출한 프로세스가 동일하다는 것을 어떻게 보장할 수 있을까? 해당 내용은 이 글을 참고하자.

     

    : `allocuvm` 및 `deallocuvm` 함수 모두 변환된 사이즈를 반환한다. 메모리 확장 및 감소에 대한 요청이 성공하면, 현재 프로세스의 메모리 사이즈를 새로운 사이즈로 교체하고, `switchuvm` 함수를 통해 페이지를 교체한다. 이 함수는 마지막에 알아본다.  

    // proc.c
    // Grow current process's memory by n bytes.
    // Return 0 on success, -1 on failure.
    int
    growproc(int n)
    {
      uint sz;
      struct proc *curproc = myproc();
    
      sz = curproc->sz;
      if(n > 0){
        if((sz = allocuvm(curproc->pgdir, sz, sz + n)) == 0)
          return -1;
      } else if(n < 0){
        if((sz = deallocuvm(curproc->pgdir, sz, sz + n)) == 0)
          return -1;
      }
      curproc->sz = sz;
      switchuvm(curproc);
      return 0;
    }

     

    : `allocuvm` 함수는 `growproc` 함수에 양수가 전달됬을 때, 호출되는 함수다. 즉, `brk` 및 `sbrk` 함수를 호출한 프로세스의 메모리 사이즈를 늘리는 함수다. 첫 조건문부터 중요한 내용이 나온다. `if(newsz >= KERNBASE)`는 왜 검사할까? `KERNBASE`는 참고로, `0x8000_0000`이다. `xv6`의 커널의 시작 위치는 0x8000_0000이다. 그래서, 유저-레벨에서 이 위치보다 높은 곳에는 값을 할당할 수 없다는 조건을 거는 것이다.

     

    : 그런데, `if(newsz >= KERNBASE) 조건만으로 충분할까?`란 생각이 든다. 왜냐면, `newsz`는 사이즈이고, `KERNBASE`는 주소이기 때문이다. 즉, 비교가 되려면 2개 타입이 비슷해야 한다. 물론, 사이즈 요청을 `0x8000_0000`을 했다는 것은 당연히 문제가 된다. 그런데 만약에, 프로세스의 메모리 시작 주소가 0x7FFF_FFFF이고, `newsz`가 0x4000이라면, 이것도 문제아닌가?

     

    : 위의 가정이 될 수가 없다. `xv6`는 가상 주소를 기준으로 모든 유저 프로세스들을 0번지에 복사한다. 최초 커널이 부팅하면서 유저 프로세스(inittproc)를 생성한다(userinit - 사실 `userinit` 함수가 호출되는 시점까지만 해도 이 프로세스는 커널 프로세스다). 그리고 나서 이 프로세스가 스케줄러에 의해 실행되면 `trapret` 프로시저에 의해서 최초의 유저 프로세스가 된다. 이 프로세스는 계속해서 살아있으면서, 자식 프로세스를 만드는데, 이 때 자식 프로세스들은 부모 프로세스의 메모리를 복사한다(fork). 즉, 모든 유저 프로세스는 `init process`의 메모리와 동일한 구조를 같게 된다. 그래서 모든 유저 프로세스는 가상 주소 `0`번지 부터 시작하게 된다.

     

    : 여기서 중요한 변수가 있다. `struct proc.sz` 변수는 단순히 프로세스가 할당받은 메모리 사이즈를 의미하는 것이 아니다. 저 사이즈는 유저 영역에서만 할당받은 사이즈를 기준으로 한다. 즉, 커널에서 얼마나 할당받았는지는 저 변수에 포함되어 있지 않다. 위에서 모든 유저 프로세스가 0 번지를 기준으로 한다고 했다. 그래서 `proc.sz`가 0x4000이라면, 이 프로세스는 `0 ~ 0x4000` 영역을 할당받은 것 이다.

     

    : 그러므로, `if(newsz >= KERNBASE)` 조건만으로도 충분하다. 왜? 사실 저 조건문은 `if(addr + newsz >= KERNBASE)` 여야 하지만, 모든 유저 프로세스의 가상 주소는 `0`이므로 `if(0 + newsz >= KERNBASE)` 이 된다.

     

    : `a`는 이전 주소에 대한 페이지 사이즈를 올림해서 받는 함수다. 예를 들어, oldsz가 0x1352라면 `a`는 0x2000이 된다. 그런데, 이렇게 강제로 페이지 사이즈를 올림하면 저 사이에 접근하면 #PF가 발생하는 것 아닐까? 가상 주소를 할당할 때, 단위를 기억해보자. 가상 주소를 할당 단위는 4KB이다. 즉, 현재 실행되고 있는 주소가 0x1352이고, 이 주소에서 #PF가 발생하지 않았다는 뜻은 0x1000 ~ 0x1FFF까지는 가상 주소를 할당받았다는 뜻이 된다. 결국, 페이지 단위로 가상 주소를 할당받기 때문에, `a`를 페이지 단위로 정렬하는 것이다. 만약, `a`를 정렬하지 않는다면? 0x1352 ~ 0x2351 까지 가상 주소를 할당해야 될 것이다. 이러면, 주소가 겹치게 된다. 이게 큰 문제가 될까? 당연하다. 0x1352 ~ 0x1FFF를 접근할 수 있는 주소가 2개라는 것은 나중에 멀티 태스킹시에 아주 큰 문제를 야기할 수 있다. 그래서 특수한 경우를 제외하고는 최대한 겹치지 않게 하는 것이 정석이다. 그런데, 왜 페이지 올림으로 받을까? 내림으로는 안되나? 내림으로 할 경우, 0x1000 ~ 0x1FFF다. 즉, 완전히 겹처버린다. 정렬을 하지 않았을 때 보다 더 심각한 문제가 생길 수 있다.

     

    : `allowuvm`에는 페이지 단위로 사이즈를 확장하기 때문에, `n`이 값이 양수이기만 하면 최소 1개의 페이지 사이즈만큼은 확장된다.

     

    : 마지막에, `mappages` 함수를 통해 가상 주소(a)와 물리 주소(mem)를 매핑한다. 예를 들어, 위의 예로 본다면, `oldsz`가 0x1352라면, 새로운 사이즈가 할당되는 곳은 시작 주소는 0x2000이된다. 그리고, `kalloc` 함수를 통해 할당받은 메모리의 시작 주소가 `0x9020_C000`이라고 하자. 이 주소는 가상 주소이기 때문에, 물리 주소로 변환이 필요하다. 그래서 `V2P` 매크로를 통해서 이 값은 `0x1020_C000`이 된다. 그러면, 결국 유저 프로세스의 가상 주소 `0x2000`에 물리 주소 `0x1020_C000`이 매핑되는 것이다. 

     

    : 페이지 테이블 엔트리에 `USER`로 설정하는 것도 눈여겨볼 점이다. 그런데, 왜 `kalloc` 함수를 호출할까? 유저 프로세스에서 `sbrk` 함수를 호출했는데, 커널 영역에 메모리를 할당해주고 있다. 유저 프로세스는 유저 영역에서 할당해줄 수는 없을까? 그러고 보니, 유저 영역은 도대체 어디서 사용되고 있는걸까? 

     

    : `xv6`의 프로세스는 유저 프로세스와 커널 프로세스로 나누지 않는다. 프로세스의 종류는 하나다. 그냥 `프로세스`다. 그렇면, 커널과 유저를 어떻게 구별하는가? 프로세스는 자신이 현재 실행되고 있는 주소가 페이지 테이블에서 어느 범위에 있는지와 코드 및 데이터 세그먼테이션을 기준으로 커널인지 유저인지를 나눈다. 

    // vm.c
    // Allocate page tables and physical memory to grow process from oldsz to
    // newsz, which need not be page aligned.  Returns new size or 0 on error.
    int allocuvm(pde_t *pgdir, uint oldsz, uint newsz)
    {
      char *mem;
      uint a;
    
      if(newsz >= KERNBASE)
        return 0;
      if(newsz < oldsz)
        return oldsz;
    
      a = PGROUNDUP(oldsz);
      for(; a < newsz; a += PGSIZE){
        mem = kalloc();
        if(mem == 0){
          cprintf("allocuvm out of memory\n");
          deallocuvm(pgdir, newsz, oldsz);
          return 0;
        }
        memset(mem, 0, PGSIZE);
        if(mappages(pgdir, (char*)a, PGSIZE, V2P(mem), PTE_W|PTE_U) < 0){
          cprintf("allocuvm out of memory (2)\n");
          deallocuvm(pgdir, newsz, oldsz);
          kfree(mem);
          return 0;
        }
      }
      return newsz;
    }

     

    : `deallocuvm` 함수는 기존에 할당받은 영역을 반납하는 함수다. 기존 사이즈가 `oldsz`이고, 줄어드는 사이즈가 `newsz`가 된다. 예를 들어, `freevm` 함수를 보면, 유저 영역 전체를 해제하기 위해 `deallocuvm(pgdir, KERNBASE, 0)`와 같이 호출한다. 

     

    : 최초의 조건문은 직관적으로 이해가 가는 조건이다. `deallocuvm` 함수에서 새로운 사이즈보다 이전 사이즈가 더 작아야 하므로, 새로운 사이즈가 이전 사이즈와 같거나 크다면, 그냥 이전 사이즈를 반환하면 된다. 당연히, `deallocuvm` 함수도 `a = PGROUNDUP(newsz)`를 통해서 새로운 사이즈도 페이지 단위로 올림 정렬을 한다. 상객해보자. 현재 사이즈를 0x12000 할당받았다. 그런데, 0x8300 까지만 사용해도 될듯하다. 그러면, 0x8300을 받았을 때, 페이지 단위로 내려야 할 까, 올려야 할 까? 내릴 경우, 0x8000 까지만 사용이 가능하므로 사용자가 원한 결과와 다를 수 있다. 그러므로, 올림으로 진행해야 한다.

     

    : 루프문의 조건 자체는 이해하기 쉬울 것 이다. 문제는 안에 조건문이다. `walkpgdir` 함수는 페이지 디렉토리와 가상 주소를 받아서 해당 가상 주소에 대한 `PTE`를 반환한다. 그런데, 여기서 세 번째 인자가 중요하다. `walkpgdir` 함수는 세 번째 인자의 값이 `1`이냐 `0`이냐에 따라 기능이 달라진다.

    세 번째 인자 `0` : 페이지 디렉토리와 가상 주소를 받아서 해당 가상 주소에 대한 `PTE`를 반환하는 단순한 함수
    세 번째 인자 `1` : 페이지 디렉토리와 가상 주소를 받아서 해당 가상 주소에 대한 `PTE`를 반환하는 함수. 그런데, 만약에 전달받은 가상 주소에 대응하는 페이지 디렉토리 엔트리 및 페이지 테이블 엔트리가 없을 경우, 새로 생성해서 페이지 테이블 엔트리를 반환한다.

    : `deallocuvm`의 루프문안에 2가지 조건들은 예시를 통해 이해해보자. 만약, 외부에서 유저 메모리를 모두 해제하기 위해 `deallocuvm(pgdir, KERNBASE, 0)`와 같이 호출했다고 치자. 이와같이 호출되면, `deallocuvm`의 루프문을 보면 알 수 있겠지만, 가상 주소 0번지 부터 페이지 사이즈 단위로 더 가면서 페이지 테이블 엔트리를 해제한다. 그런데, 모든 유저 프로세스가 0번지부터 0x8000_0000까지 할당 받았을리는 없을 것이다. 예를 들어, `deallocuvm(pgdir, KERNBASE, 0)`와 같이 호출하고, 이제 `newsz`가 0x0720_3000 까지왔다. 그런데, 0x0720_3000 부터는 가상 주소를 할당받지 않은 상황이다.  그런데, `allocuvm`의 구조를 보면 알겠지만, `xv6`의 유저 스페이스에서 추가 메모리를 할당받으려면, 현재 가상 주소의 마지막 주소부터 무조건 순차적으로 할당받게 된다. 예를 들어, 0x0012_C000 까지 할당받았는데, 0x4000 만큼 더 할당받고 싶다. 그러면, 중간에 할당받는게 아니라, 0x0012_C000 뒤에 붙여서 할당받게 된다. 그래서 최종적으로, 0x0013_0000까지 할당받게 된다. 

     

    : 이 말을 한 이유가 뭘까? 위의 예시로 다시 돌아가보자. `deallocuvm(pgdir, KERNBASE, 0)`와 같이 호출하고, 이제 `newsz`가 0x0720_3000 까지왔다. 그런데, 0x0720_3000 부터는 가상 주소를 할당받지 않은 상황이다. 그러면, 첫 번째 조건문인 `if(!pte)`에 충족되게 된다. 왜냐면, 이건 `walkpgdir`에서 `if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)` 조건문에 걸리게 된다. `0x0720_3000` 주소에서 PTE가 유효하지 않지만, 그 앞인 `0x0720_2000` 까지는 유효했다는 뜻은 PDE 유효하다는 뜻이다.

     

    : `xv6`에서는 4KB 페이징을 사용한다. 그래서, 한 개의 PDE는 4MB를 차지하게 된다. 이 말은 1024개의 PDE면 4GB를 커버할 수 있다는 소리다. `0x0720_3000` 주소는 PDE[28]이다. 즉, 28번 PDE는 0x0700_0000 ~ 0x0740_0000 까지 커버하게 된다. 그런데, `xv6`의 유저 스페이스 메모리는 순차적으로 할당된다고 했다. 그러므로, `0x0720_3000` 주소가 할당되지 않았다면, 그 이후의 주소가 할당되었다고 보기가 어렵다. 그래서 `PGADDR(PDX(a) + 1, 0, 0)` 코드는 페이지 디렉토리 엔트리 단위로 건너뛰는 코드다. `0x0720_3000` 주소에 PTE가 할당되지 않았다면, 다음 페이지 디렉토리 엔트리, 즉, PDE[29] (0x0740_0000)부터 검사를 한다는 것이다. 그런데, 왜 ` - PGSIZE` 페이지 사이즈를 빼는 것일까? 루프문 때문이다. 루프를 돌 때마다, 기본적으로 `PGSIZE`를 더하는 조건이 있어서다. 이럴 경우, 위의 예를 계속 사용하자면, `walkpgdir` 함수가 `0x0740_0000` 주소 부터 검사해야 하는데,  `0x0740_1000` 부터 검사를 하게 될 수 있다.

     

    : 그런데, 유저 스페이스의 메모리가 무조건 연속적으로 할당되는 구조라면, `0x0720_3000`를 기준으로 PTE가 NULL이면, 그냥 루프를 종료하면 안되나?

     

    : 만약, `PTE`가 존재하다면(`else if((*pte & PTE_P) != 0)`), `kfree` 함수를 통해 단순 반복으로 가상 주소들이 해제된다.

    // vm.c
    // Deallocate user pages to bring the process size from oldsz to
    // newsz.  oldsz and newsz need not be page-aligned, nor does newsz
    // need to be less than oldsz.  oldsz can be larger than the actual
    // process size.  Returns the new process size.
    int deallocuvm(pde_t *pgdir, uint oldsz, uint newsz)
    {
      pte_t *pte;
      uint a, pa;
    
      if(newsz >= oldsz)
        return oldsz;
    
      a = PGROUNDUP(newsz);
      for(; a  < oldsz; a += PGSIZE){
        pte = walkpgdir(pgdir, (char*)a, 0);
        if(!pte)
          a = PGADDR(PDX(a) + 1, 0, 0) - PGSIZE;
        else if((*pte & PTE_P) != 0){
          pa = PTE_ADDR(*pte);
          if(pa == 0)
            panic("kfree");
          char *v = P2V(pa);
          kfree(v);
          *pte = 0;
        }
      }
      return newsz;
    }

     

    : `allocuvm` 혹은 `deallocuvm` 함수가 마무리되면, 다시 `growproc`으로 돌아가게 된다. `brk` 및 `sbrk`를 호출한 프로세스는 메모리 사이즈를 새로운 사이즈로 변경하고(curproc->sz = sz), 이제 다시 유저 모드로 돌아갈 준비를 한다(`swithcuvm`).

     

    : `swithcuvm` 함수는 여러 가지 기능을 한다. 그래서 먼저, `xv6` 문서에서 이 함수의 역할을 확인해보자.

    Scheduler (2758) looks for a process with p->state set to RUNNABLE, and there’s only one: initproc. It sets the per-cpu variable proc to the process it found and calls `switchuvm` to tell the hardware to start using the target process’s page table (1879).
    ....
    `switchuvm` also sets up a task state segment SEG_TSS that instructs the hardware to execute system calls and interrupts on the process’s kernel stack. We will re-examine the task state segment in Chapter 3.
    ....
    The function `switchuvm` (1860) stores the address of the top of the kernel stack of the user process into the task segment descriptor.

    : 즉, `swithcuvm` 함수는 아래와 같은 작업을 한다.

    0" TSS를 설정한다. 이건 나중에 다시 시스템 콜이 발생했을 때, 하드웨어가 자동으로 TSS를 참고해서 유저 스택에서 커널 스택으로 바꿔주기 때문에, 유저 모드로 돌아가기전에 반드시 설정해놓고 가야한다.

    1" 현재 사용중이던 커널 페이지 테이블을 유저 페이지 테이블로 변경한다.
    // Switch TSS and h/w page table to correspond to process p.
    void switchuvm(struct proc *p)
    {
      if(p == 0)
        panic("switchuvm: no process");
      if(p->kstack == 0)
        panic("switchuvm: no kstack");
      if(p->pgdir == 0)
        panic("switchuvm: no pgdir");
    
      pushcli();
      mycpu()->gdt[SEG_TSS] = SEG16(STS_T32A, &mycpu()->ts,
                                    sizeof(mycpu()->ts)-1, 0);
      mycpu()->gdt[SEG_TSS].s = 0;
      mycpu()->ts.ss0 = SEG_KDATA << 3;
      mycpu()->ts.esp0 = (uint)p->kstack + KSTACKSIZE;
      // setting IOPL=0 in eflags *and* iomb beyond the tss segment limit
      // forbids I/O instructions (e.g., inb and outb) from user space
      mycpu()->ts.iomb = (ushort) 0xFFFF;
      ltr(SEG_TSS << 3);
      lcr3(V2P(p->pgdir));  // switch to process's address space
      popcli();
    }

     

    : 그런데, `growproc` 함수에서 꼭 `switchuvm` 함수를 호출해야 할까? `xv6` 문서를 볼 때가 됬다. 먼저 `페이지 테이블`관련 내용을 보자. `MOV TO CR3` 방식은 CR4.PCIDE 비트에 따라 달라지긴 하지만, 만약 CR4.PCIDE 비트가 0 이라면, 현재 시스템의모든 TLB들과 `paging-structure caches`를 무효화한다. `MOV TO CR3`를 사용하는 이유는 `paging-structure caches` 들이 남아서 계속 이전 내용을 참조하고 있을 가능성이 있기 때문이다.

    The x86 hardware caches page table entries in a Translation Lookaside Buffer (TLB), and when xv6 changes the page tables, it must invalidate the cached entries. If it didn’t invalidate the cached entries, then at some point later the TLB might use an old mapping, pointing to a physical page that in the mean time has been allocated to another process, and as a result, a process might be able to scribble on some other process’s memory. Xv6 invalidates stale cached entries, by reloading cr3, the register that holds the address of the current page table.

    : `switchuvm` 함수를 호출할 때 마다, 매번 TSS 디스크립터를 새로 만들어서 로딩하는 이유는 ESP를 재설정하기 위해서다. ESP는 정적 필드라서 TSS가 TR에 로드되면, 바꿀 수 가 없다. TSS를 새로 로드하는 방법밖에 없는 것 같다. 그래서 TSS를 재수정한다음에 해당 TSS를 참조하는 TSS 디스크립터를 새로 만들어서 TR에 새로 로딩한다.

     

    : 그런데, 궁금한 게 생겼다. `sbrk` 시스템 콜을 호출하면, `growproc` 함수에서 `swithcuvm` 함수를 호출한다. 여기서 나중에 시스템 콜이 호출될 것을 대비해, TSS의 ESP0을 현재 동작하고 있는 프로세스의 스택으로 설정하고 온다(이 때 동작하는 프로세스를 `A`라고 하자). 그리고 `sbrk` 시스템 콜이 종료되고, 다른 `시스템 콜 B`가 호출됬다고 치자. 그렇면, 유저 레벨에서 커널 레벨로 갈 것이다. `권한 수준`이 달라지니, 유저 레벨 스택에서 커널 레벨 스택으로 변경될 것이다. 이 때, 커널 스택은 TSS의 ESP0에 있는 값으로 설정될 것이다. 그런데, `시스템 콜 B`를 호출한 프로세스가 이전 `sbrk 시스템 콜`을 호출한 커널 프로세스가 설정한 스택을 사용해도 될까?

     

    : 결론부터 말하면, 된다.  왜냐면, `컨택스트 스위칭`이 발생하지 않았기 때문이다. 즉, `시스템 콜 B`를 호출한 프로세스가 이전 `sbrk 시스템 콜` 프로세스와 동일한 프로세스라는 것이다. 왜? 시스템 콜을 호출한다고 해서 컨택스트 스위칭이 발생하지는 않는다. 그리고, 혹시나 `sbrk 시스템 콜`과 `B 시스템 콜` 사이에 `컨택스트 스위칭`이 발생해도 상관없다. 왜냐면, `컨택스트 스위칭`이 발생할 경우, 그 과정안에 `switchuvm` 함수 호출이 들어가 있기 때문이다. 그래서 TSS는 새롭게 실행되는 프로세스의 커널 스택으로 세팅된다. 이러면, 이전 `sbrk 시스템 콜` 종료 시, 세팅했던 프로세스 A의 TSS는 사라지고, 새롭게 컨택스트 스위칭된 프로세스의 스택이 TSS에 세팅된다.

     

    : 그러면, TSS에  설정된 스택은 자신의 스택이기 때문에, 전혀 문제가 될 것이 없다. 즉, 유저 레벨이건 커널 레벨이건 프로세스는 동일하다. 단지, 차이가 있다면, 유저 레벨 스택에서 커널 레벨 스택으로 바뀐 것 뿐이다. 참고로, 이건 싱글 프로세서를 전제로 한다.

    '프로젝트 > 운영체제 만들기' 카테고리의 다른 글

    [xv6] Boot-loader  (0) 2023.07.29
    [xv6] - fork  (0) 2023.07.27
    [xv6] Log  (0) 2023.07.25
    [xv6] Buffer Cache  (0) 2023.07.24
    [xv6] inode  (0) 2023.07.24
Designed by Tistory.