bigsys-gnu / mvcc-os

KhronOS, a scalable operating systems based on sv6 (MIT) with MV-RLU (multi-version concurrency control mechanism)
Other
1 stars 0 forks source link

mvrlu kernel level benchmark memory leak problem #57

Open MadPlayer opened 3 years ago

MadPlayer commented 3 years ago

Sv6에 작성된 mvrlu kernel level benchmark에서 memory leak 문제가 발생하였습니다. memory leak이 일어나는 부분을 찾기위해 다음을 시도했습니다.

list node가 할당해제 안되는 경우가 있지 않을까?

아래의 클레스에 메모리 할당과 삭제에 print 문을 추가했습니다.

struct mvrlu_node {
  int value;
  mvrlu_node *next;

  mvrlu_node(int val): value(val), next(NULL) {}

  static void* operator new(unsigned long nbytes, const std::nothrow_t&) noexcept {
    cprintf("new\n");
    return mvrlu::mvrlu_alloc<mvrlu_node>();
  }

  static void* operator new(unsigned long nbytes) {
    void *p = mvrlu_node::operator new(nbytes, std::nothrow);
    if (p == nullptr)
      throw_bad_alloc();
    return p;
  }

  static void operator delete(void *p, const std::nothrow_t&) noexcept {
    cprintf("delete\n");
    mvrlu::mvrlu_free(p);
  }

  static void operator delete(void *p) {
    mvrlu_node::operator delete(p, std::nothrow);
  }

};

mvrlu_free로 인한 삭제는 즉시 이루어지는 것이 아니라 mvrlu.c log_reclaim 함수에서 free_obj 함수에 의해 실제로 삭제되게 됩니다. free_obj가 실제로 호출되는 곳은 여기뿐이라 그 함수에도 프린트 문을 추가했습니다.

            case TYPE_FREE:
                if (reclaim) {
                    free_obj(chs);
                    printf("delete\n");
                    stat_log_inc(log, n_reclaim_free);
                }
                break;

이렇게 해서 qemu를 이용해 실험한 결과 new와 delete의 갯수가 정확하게 일치하는 것을 알 수 있었습니다.

Benchmark에서 사용하는 데이터를 제대로 할당해제하지 않은 것이 아닐까?

spinlock과 mvrlu는 아래의 코드를 공유합니다.

template <typename T>
void bench(int nb_threads, int initial, int n_buckets, int duration, int update, int range)
{
  bench_init<T>();

  hash_list<T> *hl = new hash_list<T>(n_buckets);

  cprintf("initialize %d nodes...", initial);
  int i = 0;
  unsigned short seed[3];

  rand_init(seed);
  while (i < initial)
  {
    int value = rand_range(range, seed);
    auto list = hl->get_list(value % n_buckets);

    if (list->raw_insert(value))
      i++;
  }
  cprintf("done\n");

  // allocate pointer list
  struct proc **thread_list = new struct proc *[nb_threads];
  thread_param<T> **param_list = new thread_param<T> *[nb_threads];

  // mile sec
  u64 initial_time = nsectime();
  cprintf("Main thread ID: %d\n", myproc()->pid);
  cprintf("Creating %d threads...", nb_threads);

  for (int i = 0; i < nb_threads; i++)
  {
    param_list[i] = new thread_param<T>(n_buckets, nb_threads, update,
                                               range, stop, hl);
  }
  for (int i = 0; i < nb_threads; i++)
  {
    thread_list[i] = threadpin(test<T>, (void*)param_list[i], "test_thread",
                               i%(NCPU-1)+1);
    cprintf("\nThread created %p(c:%d, s:%d)\n", thread_list[i], i%(NCPU-1)+1,
            thread_list[i]->get_state());
  }
  cprintf(" done!\n");

  sleep_usec(initial_time, duration);

  stop = 1;
  cprintf("join %d threads...\n", nb_threads);

  sleep_usec(nsectime(), 4000); // wait for threads

  bench_finish<T>();
  cprintf(" done!\n");

  print_outcome<T>(*hl, param_list, nb_threads, initial, duration);

  // deallocate memory
  delete hl;
  for (int j = 0; j < nb_threads; j++)
  {
    delete param_list[j];
  }
  delete[] thread_list;
  delete[] param_list;
}

Spinlock benchmark 실행시 메모리 누수가 없었습니다.

MV-RLU Thread Data

이제 명시적으로 할당해제하지 않는 mvrlu threaddata가 제대로 할당해제되는지 확인해봤습니다. mvrlu thread data는 include/mvrlu 폴더에 mvrlu.hpp에 class thread_handle이라는 이름으로 정의되어 있습니다.

  template <typename T>
  class thread_handle {
  public:
    thread_handle(void) {
      ::mvrlu_thread_init(&self_);
    }

    ~thread_handle(void) {
      ::mvrlu_thread_finish(&self_);
    }

    // don't deallocate memory right away
    // _qp_thread will take care of.
    static void operator delete(void *, std::size_t) {}

    static void* operator new(unsigned long nbytes, const std::nothrow_t&) noexcept {
      assert(nbytes == sizeof(thread_handle<T>));
      return port_alloc_x(sizeof(thread_handle<T>), 0);
    }

    static void* operator new(unsigned long nbytes) {
      void *p = thread_handle<T>::operator new(nbytes, std::nothrow);
      if (p == nullptr)
        throw_bad_alloc();
      return p;
    }
    /* ... */

thread handle의 소멸자가 mvrlu_thread_finish를 호출하면 mvrlu thread data는 zombie 상태로 남았다가 _qp_thread_main에 의해 차후에 제거되게 됩니다. 그러므로 저는 mvrlu_thread_handle의 메모리가 바로 해제되지 않게 하려고 delete 함수를 비워 놓았습니다.

실제로 mvrlu thread data가 삭제되는 부분은 mvrlu.c 에 qp_reap_zombie_threads 함수에서 처리합니다.

이 부분은 qp_reap_zombie_threads 함수의 마지막 부분입니다.

            /* If it is a dead zombie, reap */
            if (thread->live_status == THREAD_DEAD_ZOMBIE) {
                thread_list_del_unsafe(&g_zombie_threads,
                               thread);
                mvrlu_thread_free(thread);
                printf("thread data delete\n");
            }

mvrlu_thread_free(thread); 가 실행되어야 thread data가 할당해제되는데 아래의 print 문이 실행되지 않는다는 것을 알 수 있었습니다.

kjhnet commented 3 years ago
MadPlayer commented 3 years ago
  • 100번 이상 실행 후 성능이 변화하는 것도 이것과 관련이 있어보이나요?

처음에는 실행횟수가 문제라고 생각했었는데 아니었습니다. 경우에 따라 부팅 후 처음부터 정상적인 성능이 나오는 경우도 있습니다. mvrlu가 어떠한 요인으로 성능이 저하되는지 하드웨어에서 monkstats으로 다시 확인해 보겠습니다.

MadPlayer commented 3 years ago
  • 각 thread가 종료되는 시점에 해제하는 부분에서,

  • 아래 조건이후 종료되는 적절한 시점에 THREAD_DEAD_ZOMBIE 아니라도 해제하는 코드를 넣어줄수 있어요?

          if (thread->live_status == THREAD_DEAD_ZOMBIE)

mvrlu 코드에서 THREAD_DEAD_ZOMBIE를 live_status에 대입하는 함수가 mvrlu_thread_free(thread);인데 이 함수가 실행되는 조건이 live_status가 THREAD_DEAD_ZOMBIE인 경우라서 문제가 되었습니다. 확인해 보고 수정하도록 하겠습니다.

kjhnet commented 3 years ago
  • 100번 이상 실행 후 성능이 변화하는 것도 이것과 관련이 있어보이나요?

처음에는 실행횟수가 문제라고 생각했었는데 아니었습니다. 경우에 따라 부팅 후 처음부터 정상적인 성능이 나오는 경우도 있습니다. mvrlu가 어떠한 요인으로 성능이 저하되는지 하드웨어에서 monkstats으로 다시 확인해 보겠습니다.

네 그래요. Perf 실행이 되면 좋은데 그게 안되서.. user-level 벤치마크 결과와 비교도 해봐야겠어요.

MadPlayer commented 3 years ago

MV-RLU Repository에 bench.c 를 확인해본 결과 아래의 코드가 추가되어야 하는 것으로 확인되어 수정하였습니다.

    ~thread_handle(void) {
      ::mvrlu_thread_finish(&self_);
      ::mvrlu_thread_free(&self_);
    }

원래 bench.c에 있던 코드입니다.

#ifdef IS_MVRLU
    for (i = 0; i < nb_threads; i++) {
        RLU_THREAD_FREE(data[i].p_rlu_td);
    }
#endif

차후에 수정해서 pull request 로 이슈에 링크하겠습니다.

MadPlayer commented 3 years ago

수정후 하드웨어에서 1000번 실행도중 다른곳에서 memory leak이 발생하는 것을 확인했습니다. 추가로 확인하겠습니다.

MadPlayer commented 3 years ago

Sv6에 문제가 있는 것으로 보이는 현상을 발견했습니다.

for i in {1...10000}; do
gdbench 7 10000
done

위의 스크립트를 실행하는 도중 지속적으로 free page를 관찰하였는데 (ctrl + f 로 확인가능) cpu 0의 free page가 지속적으로 감소하더니

kernel trap 14 err 0x11 cpu 0 cs 16 ds 24 ss 24
  rip ffffff0009df9e80 rsp ffffff000591fe98 rbp ffffff000591fec0
  cr2 ffffff0009df9e80 cr3 00000000054fd000 cr4 00000000000006b0
  rdi ffffff000cff4140 rsi ffffff000591ff00 rdx 0000000000000000
  rcx 00000000c0000100 r8  ffffff00054fd000 r9  ffffff001ff68000
  rax ffffff0009df5c40 rbx ffffff0001966ec0 r10 000000000040e25e
  r11 0000000000000246 r12 0000000000000000 r13 0000000000000000
  r14 0000000000000000 r15 0000000000000000 rflags 0000000000000282
  proc: name sh pid 1883 kstack 0xffffff0005918000
  page fault: protection violation reading ffffff0009df9e80 from kernel mode
  ffffffffc0133930
  ffffffffc01587ff

이러한 메시지를 출력하고 종료되었습니다.

MadPlayer commented 3 years ago

https://github.com/bigsys-gnu/mvcc-os/issues/54#issuecomment-919726868 에서도 cpu 0의 free page가 감소하는 문제 발생