뭐요

예제로 알아보는 xv6에서 시스템 호출 구현하기 (1) 본문

Operating System/xv6

예제로 알아보는 xv6에서 시스템 호출 구현하기 (1)

욕심만 많은 사람 2023. 2. 28. 12:38

예제로 알아보는 xv6에서 시스템 호출 구현하기

시스템 호출 구현

xv6에서 시스템 호출을 구현해보자. 직접 시스템 호출을 통해 호출한 프로세스의 메모리 사용량을 출력하는 memsize()를 구현해보려고 한다.

우리의 목표

  • memsize() 시스템 콜 추가 및 호출하는 쉘 프로그램 구현


들어가기 전에

기본 개념

새로운 시스템 호출을 구현하기 위해서, 기존에 있는 시스템 호출이 어떻게 구현되어 있는 지 확인하고 그대로 따라할 예정이다.

시스템 호출 종류는 다음과 같다.

  1. 인자가 없는 시스템 호출 EX) sysproc.c 내 구현된 uptime()
  2. 문자열 및 정수 인자가 있는 시스템 호출 EX) sysfile.c 내 구현된 open()
  3. 구조체 인자가 있는 시스템 호출 EX) fstat()

새롭게 만들 시스템 호출 memsize()와 trace는 각각 인자가 없는 시스템 호출, 문자열 및 정수 인자가 있는 시스템 호출이다. 시스템 호출을 하는 데 있어서, 인수를 전달하기 위해 일반적인 방식으로는 불가능하다. 이는 User-level function에서 Kernel-level function으로 바로 argument를 전달할 수 없기 때문인데 자세한 사항은 아래 결과 설명하면서 구체적으로 설명할 예정이다.

xv6 커널 이해

xv6 커널에 대해 이해해보자. 새로운 시스템 호출을 추가하기 위해서는 “proc.c”, “proc.h”, “syscall.c”, “syscall.h”, “sysproc.c”, “user.h”, “usys.h”에 대해서 수정이 필요하다.

“user.h”
xv6의 시스템 호출 정의

“usys.S”
xv6의 시스템 호출 리스트

“syscall.h”
시스템 호출 번호 매핑 → 새 시스템 호출을 위해 새로운 매핑 추가해야 함

“syscall.c”
시스템 호출 인수를 구문 분석하는 함수 및 실제 시스템 호출 구현에 대한 포인터

“sysproc.c"
프로세스 관련 시스템 호출 구현 → 시스템 호출 코드 추가해야 함

“sysproc.c"
struct proc 구조 정의되어 있음 → 프로세스에 대한 추가 정보를 추적을 위해 구조 변경

“proc.c”
프로세스 간의 스케줄링 및 컨텍스트 전환을 수행하는 함수



memsize() 시스템호출 구현

호출한 프로세스의 메모리 사용량을 출력하는 memsize() 시스템 호출을 구현해보자.

1) user.h에서 memsize() 추가

struct stat;
struct rtcdate;

// system calls
int fork(void);
int exit(void) __attribute__((noreturn));
int wait(void);

int memsize(void);  // 새로운 시스템 호출 추가

2) usys.S에서 memsize() 추가

SYSCALL(getpid)
SYSCALL(sbrk)
SYSCALL(sleep)
SYSCALL(uptime)

SYSCALL(memsize)   // 새로운 시스템 호출 추가

3) syscall.h에서 memsize() 추가

// System call numbers
#define SYS_fork    1
#define SYS_exit    2
#define SYS_wait    3
'
'
'
#define SYS_close  21

#define SYS_memsize 22   // 새로운 시스템 호출 번호 추가

4) syscall.c에서 memsize() 추가

extern int sys_wait(void);
extern int sys_write(void);
extern int sys_uptime(void);

extern int sys_memsize(void);   // 새로운 시스템 호출 추가

'
'
'

[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,

[SYS_memsize]   sys_memsize,   // 새로운 시스템 호출 추가

5) sysproc.c에서 memsize 시스템 콜 구현

// return memory size 
int
sys_memsize(void)
{
  uint size;

  size = myproc()->sz;

  return size;
}

해당 코드를 추가한다.

proc 구조체는 다음과 같이 정의 되어있다.

// Per-process state
struct proc {
  uint sz;                     // Size of process memory (bytes)
  pde_t* pgdir;                // Page table
  char *kstack;                // Bottom of kernel stack for this process
  enum procstate state;        // Process state
  int pid;                     // Process ID
  struct proc *parent;         // Parent process
  struct trapframe *tf;        // Trap frame for current syscall
  struct context *context;     // swtch() here to run process
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

위 과정을 거치면서 memsize() 시스템 호출을 새롭게 구현했다. 이제 memsize()가 정상적으로 동작하는 지 쉘 프로그램을 구현함으로써 확인해본다.

6) memsizetest 쉘 프로그램 구현

memsizetest.c 파일을 만든다.

#include "types.h"
#include "stat.h"
#include "user.h"

#define SIZE 2048

int main(){
    int msize = memsize();
    printf(1, "The process is using %dB\\n", msize);

    char *tmp = (char *)malloc(SIZE * sizeof(char));

    printf(1, "Allocating more memory\\n");
    msize = memsize();
    printf(1, "The process is using %dB\\n", msize);

    free(tmp);
    printf(1, "Freeing memory\\n");
    msize = memsize();
    printf(1, "The process is using %dB\\n", msize);

    exit();
}

Makefile을 수정하여 memsizetest.c 파일도 make 시 컴파일 되도록 변경한다.

⇒ Makefile에서 UPROGS, EXTRA 부분 수정

7) memsizetest 실행 결과 테스트

$ memsizetest

The process is using 12288B
Allocating more memory
The process is using 45056B
Freeing memory
The process is using 45056B

8) 실행 결과

image

정상적으로 작동한다.

지금까지 새로운 시스템 호출인 memsize()를 정의하여 xv6에 추가하고 쉘 프로그램을 작성하여 잘 작동이 되는 지 확인해보았다.



더 나아가기

memsizetest.c 에서 현재 메모리 사용량을 출력하고, malloc을 이용한 동적 메모리 할당 후 메모리 사용량을 한 번 더 출력했다.

#define SIZE 2048

char *tmp = (char *)malloc(SIZE * sizeof(char));

여기서 핵심은 2048 바이트 크기의 동적 메모리 할당을 해주었으므로 malloc 전, 후로는 2048 바이트의 차이가 나야 하지만, 2048이 아닌 4096 바이트 차이가 나온다. ( (45056 - 12288) / 8 = 4096 )

umallo.c의 malloc() 함수를 분석하면서 이유를 알게 되었다.
malloc() 함수는 다음과 같다.

void* malloc(uint nbytes)
{
  Header *p, *prevp;
  uint nunits;

  nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;
  if((prevp = freep) == 0){
    base.s.ptr = freep = prevp = &base;
    base.s.size = 0;
  }
  for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
    if(p->s.size >= nunits){
      if(p->s.size == nunits)
        prevp->s.ptr = p->s.ptr;
      else {
        p->s.size -= nunits;
        p += p->s.size;
        p->s.size = nunits;
      }
      freep = prevp;
      return (void*)(p + 1);
    }
    if(p == freep)
      if((p = morecore(nunits)) == 0)
        return 0;
  }
}

해당 코드 등장하는 morecore()가 핵심이다. 해당 함수는 다음과 같다.

morecore(uint nu)
{
  char *p;
  Header *hp;

  if(nu < 4096)
    nu = 4096;
  p = sbrk(nu * sizeof(Header));
  if(p == (char*)-1)
    return 0;
  hp = (Header*)p;
  hp->s.size = nu;
  free((void*)(hp + 1));
  return freep;
}

malloc() 함수에서 morecore()의 인자로 nunits를 넣어주는데, nunits가 4096이 안되면 4096으로 할당해주기 때문이다! 따라서 우리가 기대하는 2048바이트가 아닌 4096바이트가 나오는 것이고 해당 방식으로 짜여져 있는 이유는 page size 때문이다.
xv6에서 page size는 4096인 걸 알 수 있다. 현재 4096 - 2048 byte만큼의 interner fragmentation이 발생하게 되는데, 무슨 소리인 지 모르겠다면 아래 블로그에서 간단 명료하게 잘 설명이 되어있으니 참고하기 바란다!

내부 단편화