Chuyển Đổi Địa Chỉ Ảo
Phương pháp triển khai bộ nhớ ảo phổ biến nhất hiện nay dựa trên kỹ thuật phân trang (paging). Không gian địa chỉ ảo được chia thành các đơn vị có kích thước cố định gọi là trang (page), thường là 4KB. Không gian địa chỉ vật lý cũng được phân chia tương ứng thành các khung (frame) có cùng kích thước.
Trong kiến trúc RISC-V được sử dụng bởi XV6, địa chỉ ảo (Virtual Address - VA) có độ dài 39 bit. Cấu trúc của VA được phân tách như sau:
- Page Offset (VA[11:0]): 12 bit thấp nhất xác định vị trí byte cụ thể bên trong một trang.
- Virtual Page Number - VPN (VA[38:12]): Các bit còn lại xác định số thứ tự của trang ảo.
Tương tự, địa chỉ vật lý (Physical Address - PA) bao gồm Frame Offset (PA[11:0]) và Physical Frame Number - PFN. Quá trình chuyển đổi từ địa chỉ ảo sang địa chỉ vật lý thực chất là ánh xạ từ VPN sang PFN, trong khi phần Offset được giữ nguyên vẹn.
Kiến Trúc Bảng Trang Đa Cấp
Hệ thống sử dụng một cấu trúc dữ liệu gọi là Bảng trang (Page Table) để lưu trữ mối quan hệ ánh xạ giữa VPN và PFN. Bảng trang được lưu trữ trong bộ nhớ vật lý và được truy xuất thông qua địa chỉ ảo. Mỗi tiến trình sở hữu một bảng trang riêng biệt. Để phần cứng biết được vị trí của bảng trang hiện tại, RISC-V cung cấp thanh ghi satp (Supervisor Address Translation and Protection).
XV6 áp dụng mô hình bảng trang 3 cấp (Sv39), bao gồm:
- Cấp 2 (L2): Bảng trang gốc (Root Page Table).
- Cấp 1 (L1): Bảng trang trung gian.
- Cấp 0 (L0): Bảng trang cuối cùng chứa các mục ánh xạ trực tiếp đến PFN.
Khi khởi tạo tiến trình, hệ điều hành cấp phát một trang 4KB liên tục để làm bảng trang L2 và nạp địa chỉ vật lý của nó vào thanh ghi satp. Trong quá trình thực thi, các cấp bảng trang thấp hơn sẽ được tạo động. Quá trình phần cứng duyệt qua các cấp này để tìm địa chỉ vật lý được gọi là Page Table Walk. Để giảm thiểu độ trễ do phải truy cập bộ nhớ nhiều lần (tối đa 3 lần cho việc dịch địa chỉ và 1 lần để lấy dữ liệu), CPU tích hợp một bộ nhớ đệm phần cứng chuyên dụng gọi là TLB (Translation Lookaside Buffer).
Khởi Tạo Không Gian Địa Chỉ Kernel
Không gian địa chỉ của kernel cần được ánh xạ trực tiếp (direct-map) để quản lý các tài nguyên phần cứng. Cấu trúc dữ liệu pagetable_t đóng vai trò là con trỏ trỏ đến bảng trang gốc của RISC-V. Các thiết bị ngoại vi quan trọng như PLIC (Bộ điều khiển ngắt cấp nền tảng) và CLINT (Bộ điều khiển ngắt cục bộ lõi) đều được ánh xạ vào không gian này.
Thay vì gọi hàm ánh xạ rời rạc, quá trình khởi tạo có thể được tối ưu hóa bằng cách duyệt qua một mảng cấu hình các vùng nhớ:
typedef struct {
uint64 v_addr;
uint64 p_addr;
uint64 region_size;
int permissions;
} memory_mapping_t;
pagetable_t g_kernel_pagetable;
void initialize_kernel_vm() {
// Cấp phát trang vật lý cho bảng trang gốc và khởi tạo bằng 0
g_kernel_pagetable = (pagetable_t) allocate_physical_page();
clear_memory(g_kernel_pagetable, PAGE_SIZE);
// Định nghĩa các vùng nhớ cần ánh xạ cho Kernel
memory_mapping_t hw_maps[] = {
{ UART0_BASE, UART0_BASE, PAGE_SIZE, PTE_READ | PTE_WRITE },
{ VIRTIO_BASE, VIRTIO_BASE, PAGE_SIZE, PTE_READ | PTE_WRITE },
{ CLINT_BASE, CLINT_BASE, 0x10000, PTE_READ | PTE_WRITE },
{ PLIC_BASE, PLIC_BASE, 0x400000, PTE_READ | PTE_WRITE },
{ KERNEL_BASE, KERNEL_BASE, (uint64)kernel_text_end - KERNEL_BASE, PTE_READ | PTE_EXEC },
{ (uint64)kernel_text_end, (uint64)kernel_text_end, PHYSICAL_MEM_TOP - (uint64)kernel_text_end, PTE_READ | PTE_WRITE },
{ TRAMPOLINE_VADDR, (uint64)trampoline_code, PAGE_SIZE, PTE_READ | PTE_EXEC }
};
int num_maps = sizeof(hw_maps) / sizeof(hw_maps[0]);
for (int i = 0; i < num_maps; i++) {
if (map_kernel_region(hw_maps[i].v_addr, hw_maps[i].p_addr,
hw_maps[i].region_size, hw_maps[i].permissions) != 0) {
kernel_panic("Lỗi ánh xạ bộ nhớ kernel");
}
}
}
Cơ Chế Duyệt Và Ánh Xạ Trang
Hàm map_kernel_region thực chất là một wrapper gọi đến logic cốt lõi apply_mappings. Logic này duyệt qua dải địa chỉ ảo và thiết lập các Page Table Entry (PTE).
int apply_mappings(pagetable_t pt, uint64 start_va, uint64 map_size, uint64 start_pa, int perms) {
uint64 current_va = ALIGN_DOWN(start_va, PAGE_SIZE);
uint64 end_va = ALIGN_DOWN(start_va + map_size - 1, PAGE_SIZE);
while (1) {
pte_t *target_pte = traverse_pagetable(pt, current_va, true);
if (!target_pte) return -1;
if (*target_pte & PTE_VALID) {
kernel_panic("Địa chỉ đã được ánh xạ trước đó");
}
*target_pte = PA_TO_PTE(start_pa) | perms | PTE_VALID;
if (current_va == end_va) break;
current_va += PAGE_SIZE;
start_pa += PAGE_SIZE;
}
return 0;
}
Hàm traverse_pagetable đảm nhận việc tìm kiếm hoặc tự động cấp phát các bảng trang con khi duyệt qua 3 cấp (L2, L1, L0):
pte_t * traverse_pagetable(pagetable_t pt_root, uint64 v_addr, bool should_alloc) {
if (v_addr >= MAX_VIRTUAL_ADDR) return NULL;
for (int lvl = 2; lvl > 0; lvl--) {
pte_t *entry = &pt_root[GET_LEVEL_INDEX(lvl, v_addr)];
if (*entry & PTE_VALID) {
pt_root = (pagetable_t)ENTRY_TO_PA(*entry);
} else {
if (!should_alloc) return NULL;
pagetable_t new_pt = (pagetable_t)allocate_physical_page();
if (!new_pt) return NULL;
clear_memory(new_pt, PAGE_SIZE);
*entry = PA_TO_ENTRY(new_pt) | PTE_VALID;
pt_root = new_pt;
}
}
return &pt_root[GET_LEVEL_INDEX(0, v_addr)];
}
Sau khi bảng trang được thiết lập, kernel sử dụng chỉ thị sfence.vma để đồng bộ hóa và nạp địa chỉ gốc vào phần cứng:
void activate_kernel_pagetable() {
write_satp_register(MAKE_SATP(g_kernel_pagetable));
flush_tlb(); // Thực thi sfence.vma
}
Quản Lý Stack Kernel Cho Tiến Trình
Mỗi tiến trình cần một không gian stack riêng khi thực thi ở chế độ kernel. Các stack này được phân tách bằng các trang bảo vệ (guard page) chưa được ánh xạ để phát hiện tràn stack.
#define MAX_PROCESSES 64
process_t process_table[MAX_PROCESSES];
void setup_process_table() {
init_spinlock(&global_pid_lock, "pid_sync");
for (int idx = 0; idx < MAX_PROCESSES; idx++) {
process_t *current = &process_table[idx];
init_spinlock(¤t->proc_lock, "proc_sync");
char *phys_stack = allocate_physical_page();
if (!phys_stack) kernel_panic("Không đủ bộ nhớ cho kernel stack");
// Tính toán địa chỉ ảo tương ứng cho stack
uint64 stack_va = CALCULATE_KSTACK_VADDR(idx);
// Ánh xạ trang vật lý vào không gian ảo của kernel
map_kernel_region(stack_va, (uint64)phys_stack, PAGE_SIZE, PTE_READ | PTE_WRITE);
current->kernel_stack_base = stack_va;
}
activate_kernel_pagetable();
}
Bộ Cấp Phát Bộ Nhớ Vật Lý
XV6 duy trì một danh sách liên kết đơn chứa các trang vật lý trống nằm giữa vùng kết thúc của kernel và giới hạn bộ nhớ vật lý (PHYSICAL_MEM_TOP). Cấu trúc free_node_t tận dụng chính 8 byte đầu tiên của trang trống để lưu con trỏ kế tiếp. Toàn bộ danh sách được bảo vệ bởi một spinlock.
typedef struct free_node {
struct free_node *next_page;
} free_node_t;
struct {
spinlock_t mem_lock;
free_node_t *head;
} physical_memory_pool;
void initialize_allocator() {
init_spinlock(&physical_memory_pool.mem_lock, "mem_pool");
register_free_pages(kernel_end_addr, (void*)PHYSICAL_MEM_TOP);
}
void register_free_pages(void *start, void *end) {
char *page_ptr = (char*)ALIGN_UP((uint64)start, PAGE_SIZE);
while (page_ptr + PAGE_SIZE <= (char*)end) {
release_physical_page(page_ptr);
page_ptr += PAGE_SIZE;
}
}
void release_physical_page(void *page_addr) {
if ((uint64)page_addr % PAGE_SIZE != 0) kernel_panic("Địa chỉ không hợp lệ");
acquire_spinlock(&physical_memory_pool.mem_lock);
free_node_t *node = (free_node_t*)page_addr;
node->next_page = physical_memory_pool.head;
physical_memory_pool.head = node;
release_spinlock(&physical_memory_pool.mem_lock);
}
Thiết Lập Không Gian Người Dùng và ELF
Mỗi tiến trình người dùng sở hữu một không gian địa chỉ ảo độc lập từ 0 đến MAXVA. Khi gọi hệ thống exec, kernel sẽ phân tích tệp thực thi định dạng ELF, cấp phát bộ nhớ vật lý cho từng phân đoạn (segment), thiết lập stack người dùng và đẩy các đối số (argc, argv) vào stack này. Đáy stack được bảo vệ bởi một guard page.
int load_and_execute(const char *file_path, char **arguments) {
inode_t *file_inode = NULL;
pagetable_t new_pagetable = NULL;
elf_header_t elf_hdr;
uint64 alloc_size = 0, stack_ptr = 0;
int arg_count = 0;
begin_filesystem_op();
file_inode = resolve_path(file_path);
if (!file_inode) goto cleanup_error;
lock_inode(file_inode);
if (read_inode_data(file_inode, 0, (uint64)&elf_hdr, sizeof(elf_hdr)) != sizeof(elf_hdr) ||
elf_hdr.magic != ELF_MAGIC) {
goto cleanup_error;
}
new_pagetable = create_user_pagetable();
if (!new_pagetable) goto cleanup_error;
// Tải các phân đoạn chương trình
uint64 hdr_offset = elf_hdr.phoff;
for (int i = 0; i < elf_hdr.phnum; i++, hdr_offset += sizeof(prog_header_t)) {
prog_header_t seg_hdr;
if (read_inode_data(file_inode, 0, (uint64)&seg_hdr, hdr_offset, sizeof(seg_hdr)) != sizeof(seg_hdr))
goto cleanup_error;
if (seg_hdr.type == ELF_PROG_LOAD) {
if (seg_hdr.mem_size < seg_hdr.file_size || seg_hdr.v_addr + seg_hdr.mem_size < seg_hdr.v_addr)
goto cleanup_error;
alloc_size = allocate_user_memory(new_pagetable, alloc_size, seg_hdr.v_addr + seg_hdr.mem_size);
if (!alloc_size) goto cleanup_error;
if (load_segment_data(new_pagetable, seg_hdr.v_addr, file_inode, seg_hdr.offset, seg_hdr.file_size) < 0)
goto cleanup_error;
}
}
unlock_and_put_inode(file_inode);
end_filesystem_op();
file_inode = NULL;
// Khởi tạo stack người dùng
alloc_size = ALIGN_UP(alloc_size, PAGE_SIZE);
alloc_size = allocate_user_memory(new_pagetable, alloc_size, alloc_size + 2 * PAGE_SIZE);
if (!alloc_size) goto cleanup_error;
clear_user_memory(new_pagetable, alloc_size - 2 * PAGE_SIZE);
stack_ptr = alloc_size;
uint64 stack_base = stack_ptr - PAGE_SIZE;
uint64 arg_pointers[MAX_ARGS + 1];
for (arg_count = 0; arguments[arg_count]; arg_count++) {
if (arg_count >= MAX_ARGS) goto cleanup_error;
stack_ptr -= string_length(arguments[arg_count]) + 1;
stack_ptr -= stack_ptr % 16; // Căn chỉnh 16 byte cho RISC-V
if (stack_ptr < stack_base) goto cleanup_error;
if (copy_to_user(new_pagetable, stack_ptr, arguments[arg_count], string_length(arguments[arg_count]) + 1) < 0)
goto cleanup_error;
arg_pointers[arg_count] = stack_ptr;
}
arg_pointers[arg_count] = 0;
stack_ptr -= (arg_count + 1) * sizeof(uint64);
stack_ptr -= stack_ptr % 16;
if (stack_ptr < stack_base || copy_to_user(new_pagetable, stack_ptr, (char*)arg_pointers, (arg_count + 1) * sizeof(uint64)) < 0)
goto cleanup_error;
// Chuyển giao quyền kiểm soát cho tiến trình
process_t *curr_proc = get_current_process();
pagetable_t old_pt = curr_proc->page_table;
curr_proc->page_table = new_pagetable;
curr_proc->memory_size = alloc_size;
curr_proc->trap_frame->epc = elf_hdr.entry_point;
curr_proc->trap_frame->sp = stack_ptr;
curr_proc->trap_frame->a1 = stack_ptr; // Con trỏ argv
destroy_user_pagetable(old_pt, curr_proc->memory_size);
return arg_count; // Trả về argc qua thanh ghi a0
cleanup_error:
if (new_pagetable) destroy_user_pagetable(new_pagetable, alloc_size);
if (file_inode) {
unlock_and_put_inode(file_inode);
end_filesystem_op();
}
return -1;
}
Trong kiến trúc RISC-V, giá trị trả về của exec (số lượng đối số argc) được lưu trữ mặc định vào thanh ghi a0, trong khi thanh ghi a1 được gán trực tiếp địa chỉ của mảng argv trên stack người dùng. Khi chương trình mới bắt đầu thực thi tại hàm main, nó sẽ nhận các tham số này một cách liền mạch từ môi trường mà kernel đã chuẩn bị sẵn.