Triển Khai Cơ Chế Copy-on-Write (COW) trong Hệ Điều Hành xv6

Bộ nhớ ảo là một thành phần cốt lõi của các hệ điều hành hiện đại, mang lại một lớp gián tiếp quan trọng giữa các chương trình và phần cứng. Kernel có thể kiểm soát các truy cập bộ nhớ bằng cách đánh dấu các Page Table Entry (PTE) là không hợp lệ hoặc chỉ đọc, từ đó kích hoạt các lỗi trang (page fault). Khả năng này cũng cho phép kernel thay đổi ý nghĩa của một địa chỉ ảo bằng cách sửa đổi PTE tương ứng.

Trong hệ điều hành xv6, lệnh gọi hệ thống fork() truyền thống thực hiện việc sao chép toàn bộ không gian bộ nhớ người dùng của tiến trình cha sang tiến trình con. Nếu tiến trình cha chiếm một lượng lớn bộ nhớ, quá trình sao chép này có thể tốn rất nhiều thời gian và tài nguyên. Điều tệ hơn là công sức này thường bị lãng phí đáng kể: fork() thường được theo sau bởi exec() trong tiến trình con, lúc này phần lớn dữ liệu vừa được sao chép sẽ bị loại bỏ. Tuy nhiên, việc sao chép là cần thiết nếu cả tiến trình cha và con đều cần ghi vào các trang bộ nhớ đã chia sẻ.

Để giải quyết vấn đề này, cơ chế Copy-on-Write (COW) được triển khai cho fork(). Với COW, hệ thống chỉ tạo một bảng trang mới cho tiến trình con và sao chép các entry từ bảng trang của tiến trình cha. Thay vì sao chép dữ liệu vật lý ngay lập tức, các PTE của tất cả các trang bộ nhớ người dùng được chia sẻ giữa cha và con sẽ được đánh dấu là chỉ đọc (read-only), đồng thời một cờ đặc biệt (ví dụ: PTE_COW) được thiết lập. Khi bất kỳ tiến trình nào (cha hoặc con) cố gắng ghi vào một trong các trang COW này, CPU sẽ phát sinh một lỗi trang (store page fault).

Lúc này, trình xử lý lỗi trang trong kernel (usertrap) sẽ được kích hoạt. Nó nhận diện lỗi này (r_scause() = 15) là một lỗi ghi vào trang COW. Kernel sẽ cấp phát một trang vật lý mới, sao chép nội dung từ trang gốc (chỉ đọc) sang trang mới này, và sau đó sửa đổi PTE của tiến trình gây lỗi để trỏ đến trang mới, đánh dấu nó là có thể ghi. Khi trình xử lý lỗi trang trả về, tiến trình người dùng sẽ có thể ghi vào bản sao riêng của mình, và lệnh gây lỗi ban đầu sẽ được thực thi lại thành công.

Các Vấn Đề và Giải Pháp Khi Triển Khai COW

Việc thiết kế và triển khai COW đặt ra một số thách thức chính:

  1. Làm thế nào để nhận biết một trang có đang sử dụng COW hay không?

    Giải pháp: Chúng ta có thể sử dụng một bit dự trữ trong cấu trúc PTE (ví dụ, bit 8) để định nghĩa một cờ mới, PTE_COW. Cờ này sẽ được thiết lập cho các trang đang ở chế độ Copy-on-Write.

  2. Làm thế nào để quản lý việc cấp phát và giải phóng các trang vật lý khi nhiều tiến trình cùng chia sẻ một trang?

    Giải pháp: Duy trì một bộ đếm tham chiếu (reference count) cho mỗi trang vật lý. Trang vật lý chỉ được giải phóng khi bộ đếm tham chiếu của nó giảm về 0.

  3. Làm thế nào để cập nhật bộ đếm tham chiếu một cách an toàn trong môi trường đa luồng/đa tiến trình?

    Giải pháp: Sử dụng một mảng toàn cục để lưu trữ bộ đếm tham chiếu, với chỉ số mảng được tính từ địa chỉ vật lý của trang (ví dụ: pa / PGSIZE). Mọi thao tác truy cập và cập nhật bộ đếm tham chiếu phải được bảo vệ bởi một spinlock để ngăn ngừa các điều kiện tranh chấp dữ liệu.

Các Thay Đổi Mã Nguồn Cần Thiết

Để hỗ trợ COW, cần sửa đổi một số thành phần chính trong hệ thống xv6:

1. Các Định Nghĩa và Cấu Trúc Toàn Cục

Trong riscv.h, định nghĩa cờ PTE_COW và macro để tính chỉ số trang:

// riscv.h
#define PTE_COW (1L << 8) // Cờ Copy-on-Write
#define CHISO_TRANG(pa) (((pa)) >> PGSHIFT) // Chuyển đổi địa chỉ vật lý thành chỉ số trang

Trong kalloc.c, thêm mảng bộ đếm tham chiếu và khóa bảo vệ:

// kalloc.c
uint page_ref_counts[PHYSTOP >> PGSHIFT]; // Mảng lưu trữ bộ đếm tham chiếu cho mỗi trang vật lý
struct spinlock ref_count_lock; // Khóa bảo vệ mảng page_ref_counts

void tang_dem_ref(uint64 dia_chi_vat_ly) {
  uint idx = CHISO_TRANG(dia_chi_vat_ly);
  acquire(&ref_count_lock);
  page_ref_counts[idx]++;
  release(&ref_count_lock);
}

void dat_dem_ref(uint64 dia_chi_vat_ly, uint so_luong) {
  uint idx = CHISO_TRANG(dia_chi_vat_ly);
  acquire(&ref_count_lock);
  page_ref_counts[idx] = so_luong;
  release(&ref_count_lock);
}

void giam_dem_ref(uint64 dia_chi_vat_ly) {
  uint idx = CHISO_TRANG(dia_chi_vat_ly);
  acquire(&ref_count_lock);
  page_ref_counts[idx]--;
  release(&ref_count_lock);
}

2. Hàm usertrap() và Trình Xử Lý Lỗi COW

Trong trap.c, hàm usertrap() cần kiểm tra lỗi ghi trang (store page fault, r_scause() == 15) và gọi một hàm xử lý đặc biệt cho COW.

// trap.c (trong hàm usertrap)
else if (r_scause() == 15) { // Xử lý lỗi ghi/lưu trữ trang
    int ket_qua = xu_ly_cow_page_fault(r_stval());
    if (ket_qua < 0) {
        printf("usertrap(): Lỗi xử lý COW không mong đợi %p pid=%d\n", ket_qua, p->pid);
        printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
        setkilled(p);
    }
}

Hàm xu_ly_cow_page_fault sẽ thực hiện logic chính của COW khi có lỗi trang:

// trap.c (hàm mới)
int xu_ly_cow_page_fault(uint64 dia_chi_ao_loi) {
  pte_t *con_tro_pte = walk(myproc()->pagetable, dia_chi_ao_loi, 0);

  // Kiểm tra PTE hợp lệ, có mặt, là trang người dùng và có cờ COW
  if (con_tro_pte == 0 || !(*con_tro_pte & PTE_V) || !(*con_tro_pte & PTE_U) || !(*con_tro_pte & PTE_COW))
    return -1; // Không phải lỗi COW hợp lệ hoặc PTE không tồn tại/không hợp lệ

  acquire(&ref_count_lock);
  uint64 dia_chi_vat_ly_goc = PTE2PA(*con_tro_pte);
  uint dem_ref_hien_tai = page_ref_counts[CHISO_TRANG(dia_chi_vat_ly_goc)];

  if (dem_ref_hien_tai == 1) { // Nếu chỉ còn một tham chiếu duy nhất đến trang này
    *con_tro_pte &= ~PTE_COW; // Bỏ cờ COW
    *con_tro_pte |= PTE_W;    // Đánh dấu là có thể ghi
    release(&ref_count_lock);
    return 0; // Không cần sao chép, chỉ cần thay đổi quyền
  }
  release(&ref_count_lock);

  // Cấp phát một trang vật lý mới
  char* trang_moi = kalloc();
  if (trang_moi == 0)
    return -2; // Không đủ bộ nhớ

  // Sao chép nội dung từ trang gốc sang trang mới
  memmove(trang_moi, (void*)dia_chi_vat_ly_goc, PGSIZE);

  // Giảm số tham chiếu của trang gốc. Hàm kfree() trong xv6 thường 
  // chịu trách nhiệm kiểm tra và giải phóng trang nếu bộ đếm tham chiếu về 0.
  // Việc gọi kfree tại đây đảm bảo trang gốc được quản lý đúng cách.
  kfree((void*)dia_chi_vat_ly_goc);

  // Cập nhật PTE của tiến trình hiện tại để trỏ đến trang mới, đánh dấu là có thể ghi
  *con_tro_pte = PA2PTE(trang_moi) | PTE_W | PTE_FLAGS(*con_tro_pte);
  *con_tro_pte &= ~PTE_COW; // Xóa cờ COW
  return 0;
}

3. Hàm uvmcopy()

Trong vm.c, hàm uvmcopy() (thường được gọi bởi fork()) cần được điều chỉnh để hỗ trợ COW. Thay vì sao chép toàn bộ dữ liệu, nó sẽ thiết lập các PTE để chia sẻ trang:

// vm.c (hàm uvmcopy được sửa đổi cho COW)
int uvmcopy_cow(pagetable_t bang_trang_cu, pagetable_t bang_trang_moi, uint64 kich_thuoc) {
  pte_t *con_tro_pte;
  uint64 dia_chi_vat_ly, i;
  uint cac_co_hieu;

  for (i = 0; i < kich_thuoc; i += PGSIZE) {
    if ((con_tro_pte = walk(bang_trang_cu, i, 0)) == 0)
      panic("uvmcopy_cow: PTE không tồn tại");
    if (!(*con_tro_pte & PTE_V))
      panic("uvmcopy_cow: Trang không có mặt");

    dia_chi_vat_ly = PTE2PA(*con_tro_pte);

    // Đánh dấu PTE của cả tiến trình cha (bang_trang_cu) là chỉ đọc và COW.
    // Nếu nó đã là PTE_W hoặc PTE_COW, thì điều chỉnh lại.
    if ((*con_tro_pte & PTE_W) || (*con_tro_pte & PTE_COW)) {
      *con_tro_pte &= ~PTE_W;     // Bỏ cờ ghi
      *con_tro_pte |= PTE_COW;    // Đặt cờ COW
    }
    cac_co_hieu = PTE_FLAGS(*con_tro_pte); // Lấy các cờ hiện tại của PTE

    // Ánh xạ trang trong bảng trang mới (bang_trang_moi) với các cờ tương tự
    // (bao gồm PTE_COW và chỉ đọc)
    if (mappages(bang_trang_moi, i, PGSIZE, dia_chi_vat_ly, cac_co_hieu) != 0) {
      goto loi_uvmcopy;
    }
    tang_dem_ref(dia_chi_vat_ly); // Tăng bộ đếm tham chiếu cho trang vật lý
  }
  return 0;

loi_uvmcopy:
  uvmunmap(bang_trang_moi, 0, i / PGSIZE, 1);
  return -1;
}

4. Hàm copyout()

Hàm copyout() được sử dụng để sao chép dữ liệu từ không gian kernel sang không gian người dùng. Khi sử dụng COW, việc ghi vào một trang người dùng có thể kích hoạt lỗi trang nếu trang đó là COW và chỉ đọc. Do đó, copyout() cũng cần kiểm tra và xử lý trường hợp này.

// vm.c (hàm copyout được sửa đổi)
int ghi_ra_vung_nguoi_dung(pagetable_t bang_trang, uint64 dia_chi_dich_ao, char *nguon, uint64 do_dai) {
  uint64 kich_thuoc_chun, va_tron, pa_vat_ly;
  pte_t *con_tro_pte;

  while (do_dai > 0) {
    va_tron = PGROUNDDOWN(dia_chi_dich_ao);
    if (va_tron >= MAXVA)
      return -1;

    con_tro_pte = walk(bang_trang, va_tron, 0);
    if (con_tro_pte == 0 || !(*con_tro_pte & PTE_V) || !(*con_tro_pte & PTE_U))
      return -1;

    // Nếu trang đích không có cờ ghi (PTE_W), có thể đó là một trang COW
    if (!(*con_tro_pte & PTE_W)) {
      if (xu_ly_cow_page_fault(va_tron) < 0) // Gọi trình xử lý COW
        return -1;
    }

    pa_vat_ly = PTE2PA(*con_tro_pte);
    kich_thuoc_chun = PGSIZE - (dia_chi_dich_ao - va_tron);
    if (kich_thuoc_chun > do_dai)
      kich_thuoc_chun = do_dai;

    memmove((void *)(pa_vat_ly + (dia_chi_dich_ao - va_tron)), nguon, kich_thuoc_chun);

    do_dai -= kich_thuoc_chun;
    nguon += kich_thuoc_chun;
    dia_chi_dich_ao = va_tron + PGSIZE;
  }
  return 0;
}

Thẻ: xv6 operating-system copy-on-write virtual-memory memory-management

Đăng vào ngày 8 tháng 11 lúc 15:21