아무것도 몰라요

실험실 (커널 오류)

[kernel panic] unable to handle kernel paging request at ~~

telomere37 2021. 7. 21. 14:02

1. 도입

오늘 밀워키 벅스가 NBA 우승을 했다. 매년 이 순간에 내가 뭘 했는지를 돌아보곤 하는데, 올해는 AS관에 앉아서 디버깅을 하고 있다.

오늘의 BUG

기존에는 항상 NULL을 참조해서 panic이 발생했지만, 이번에는 kernel paging request를 다룰 수 없다는 bug이다. write path에서 dedup queue에 write entry를 push 하는 함수를 호출하였고, dedup system call에서 이 queue안의 write entry를 꺼내는 방식을 구현해보았다. 

// fs/nova/dedup.c

// Get next Dedup queue entry
int nova_dedup_queue_get_next_entry(struct nova_file_write_entry *next_entry){
  struct nova_dedup_queue *ptr;
  
  if(nova_dedup_queue_head.list.next){
    ptr = list_entry(nova_dedup_queue_head.list.next, struct nova_dedup_queue, list);
    printk("checking~ %lld\n",ptr->entry_data->pgoff);
    next_entry = ptr->entry_data;
    return 1;
  }
  else{
    return 0;
  }
}

// Insert Write Entries to Dedup Queue
int nova_dedup_queue_push(struct nova_file_write_entry *new_entry){
  struct nova_dedup_queue *new_data;
  new_data = kmalloc(sizeof(struct nova_dedup_queue), GFP_KERNEL);
  list_add_tail(&new_data->list, &nova_dedup_queue_head.list);
  new_data->entry_data = new_entry;
  return 0;
}
   // fs/nova/file.c
   
   nova_init_file_write_entry(sb, sih, &entry_data, epoch_id,
          start_blk, allocated, blocknr, time,
          file_size);

    ret = nova_append_file_write_entry(sb, pi, inode,
          &entry_data, &update);

    /* NOVA DEDUP KHJ */
    nova_dedup_queue_init();
    printk("Dedup Queue init\n");
    nova_dedup_queue_push(&entry_data);
    printk("Pushed Entry %lld\n",entry_data.pgoff);

2. Test 1

어느 부분에서 오류가 발생하는지 확인하기 위해, dedup.c의 함수들에 주소와 실제 넘긴 write entry의 값들을 비교해보았다. 

// fs/nova/dedup.c

// Insert Write Entries to Dedup Queue
int nova_dedup_queue_push(struct nova_file_write_entry *new_entry){
  struct nova_dedup_queue *new_data;
  new_data = kmalloc(sizeof(struct nova_dedup_queue), GFP_KERNEL);
  list_add_tail(&new_data->list, &nova_dedup_queue_head.list);
  new_data->entry_data = new_entry;
  printk("push print %p\n",new_data->entry_data);
  return 0;
}

// Get next Dedup queue entry
int nova_dedup_queue_get_next_entry(struct nova_file_write_entry *next_entry){
  struct nova_dedup_queue *ptr;
  
  if(nova_dedup_queue_head.list.next){
    ptr = list_entry(nova_dedup_queue_head.list.next, struct nova_dedup_queue, list);
    printk("checking~ %lld\n",ptr->entry_data->pgoff);
    next_entry = ptr->entry_data;
    return 1;
  }
  else{
    return 0;
  }
}

분석 결과, write path에서 entry_data의 주소를 저장하면 안 되었다. 이는 잠깐 dram에 두는 임시 주소일 뿐 실제로 pmeme에 저장되는 주소는 완전히 다르기 때문이다. 이 주소를 알기 위해서는 nova_append_file_write_entry를 따라 들어가야 됐다. 즉, 실제 copy가 어디서 이루어지는지, 그리고 그 주소가 정해지는 시점이 어디인지를 알고 그 주소를 dedup queue에 push 해줘야 됐다. 

log update 흐름도

3. Test 2

static int nova_append_log_entry(struct super_block *sb, 
  struct nova_inode *pi, struct inode *inode,
  struct nova_inode_info_header *sih,
  struct nova_log_entry_info *entry_info)
{
  void *entry, *alter_entry;
  enum nova_entry_type type = entry_info->type;
  struct nova_inode_update *update = entry_info->update;
  u64 tail, alter_tail;
  u64 curr_p, alter_curr_p;
  size_t size;
  int extended = 0; 
  unsigned long irq_flags = 0; 

  if (type == DIR_LOG)
    size = entry_info->file_size;
  else 
    size = nova_get_log_entry_size(sb, type);

  tail = update->tail;
  alter_tail = update->alter_tail;

  curr_p = nova_get_append_head(sb, pi, sih, tail, size,
            MAIN_LOG, 0, &extended);
  if (curr_p == 0)
    return -ENOSPC;


  /* DEDUP NOVA KHJ */
  if(type == FILE_WRITE){
    nova_dedup_queue_push(curr_p);
  }
  /*****************/

nova_append_log_entry에서 'curr_p'가 새로 append할 주소인 것 같다. 이 값을 queue에 저장해뒀다가 나중에 nova_get_block(sb, curr_p)로 읽어오면 될 것 같다. 이 자료형은 u64이니까 출력해보자! 

Write Entry는 64B

오~ 64B씩 증가하는 것을 볼 수 있다! 그렇다면 이 주소를 저장하는 것이 맞을 것 같다.

 

4. Test 3

// fs/nova/dedup.h

/* nova_dedup_queue
   queue of entries that needs to be deduplicated
   */
struct nova_dedup_queue{
  u64 write_entry_address;
  struct list_head list;
};
// fs/nova/dedup.c

// Insert Write Entries to Dedup Queue
int nova_dedup_queue_push(u64 new_address){
  struct nova_dedup_queue *new_data;
  new_data = kmalloc(sizeof(struct nova_dedup_queue), GFP_KERNEL);
  list_add_tail(&new_data->list, &nova_dedup_queue_head.list);
  new_data->write_entry_address = new_address;
  printk("push print %llu\n",new_data->write_entry_address);
  return 0;
}

// Get next Dedup queue entry
u64 nova_dedup_queue_get_next_entry(void){
  struct nova_dedup_queue *ptr;
  
  if(nova_dedup_queue_head.list.next){
    ptr = list_entry(nova_dedup_queue_head.list.next, struct nova_dedup_queue, list);
    printk("checking~ %llu\n",ptr->write_entry_address);
    return 1;
  }
  else{
    return 0;
  }
}
// fs/nova/log.c

static int nova_append_log_entry(...){
...
  /* DEDUP NOVA KHJ */
  if(type == FILE_WRITE){
    printk("%llu\n",curr_p);
    nova_dedup_queue_push(curr_p);
  }
  /*****************/

...
}

dedup queue의 맴버 변수를 u64로 바꾸서 주소를 저장했다. 

checking~에서 잘 뜬다!

결과 설명

우선 test파일은 'KHJ'를 총 4096번 반복해서 쓰는 fprintf문이었다. fprintf와 buffer의 구조에 따라서 총 3번의 write만 호출하는 것 같다. (더 깊이 깔린 지식은 정확히는 모르겠다) 아무튼 연속적인 write였기 때문에 그렇지 않을까 싶고 nova에서 flush 하는 단위가 4KB였을 수 있다. 중요한 것은 1298... 이 주소 값이 한 번은 nova_append_log_entry에서 찍히고 한 번은 test2.c를 돌렸을 때 dedup system call에서 찍힌다는 것이다. 아직 queue를 어디에 선언할지 정하지 않아서 매번 init 해주기 때문에 가장 마지막에 저장된 주소가 pop-out이 된다. 이제 queue의 pop-out에서 삭제를 구현해줘야 되고 어디서 선언할지를 정한 다음 fingerprinting으로 들어가야겠다.

 

오류 결론

다른 코드 영역의 공간을 참조하려고해서(?) , 암튼 초반 발상 자체가 잘못됨.