1. Deferred Memory Allocation là gì?
Đây là một tính năng của Linux kernel giúp tăng hiệu quả sử dụng bộ nhớ.
Mỗi khi một tiến trình (process) gọi malloc() để yêu cầu bộ nhớ, kernel chưa cấp phát ngay một vùng nhớ vật lý. Thay vào đó, kernel cấp phát một địa chỉ ảo (virtual address) và thêm địa chỉ đó vào bảng virtual memory address của tiến trình.
Chỉ khi tiến trình truy cập vào địa chỉ đó (ví dụ: đọc/ghi), kernel sẽ tạm dừng (sleep) tiến trình và tiến hành tìm kiếm một vùng nhớ vật lý để cấp phát. Sau khi tìm thấy, kernel sẽ đính kèm địa chỉ vật lý của vùng nhớ đó vào địa chỉ ảo đã cấp phát lúc trước. Sau đó, kernel sẽ “đánh thức” (wakeup) tiến trình, lúc này tiến trình có thể đọc/ghi dữ liệu vào vùng nhớ đó bình thường.
Tại sao kernel lại trì hoãn (delay) việc cấp phát bộ nhớ vật lý?
Nguyên nhân là do việc trì hoãn này sẽ tăng khả năng đáp ứng bộ nhớ của hệ điều hành (OS).
Ví dụ, một hệ thống đang có 10 byte bộ nhớ trống và 4 tiến trình P1, P2, P3, P4 đang chạy. Cả 4 tiến trình đều xin cấp phát 10 bytes nhưng tại các thời điểm sử dụng khác nhau (t1, t2, t3, t4). Sau khi sử dụng, cả 4 tiến trình đều giải phóng (free) bộ nhớ ngay lập tức.
- Nếu không dùng deferred memory: Chỉ P1 (xin cấp phát trước) là có bộ nhớ. P2, P3, P4 không cấp phát được. Mặc dù P1 xin cấp phát trước nhưng có thể cũng chưa dùng ngay. Việc này gây lãng phí bộ nhớ: P1 có nhưng không dùng, trong khi P2, P3 cần thì lại không có.
- Với deferred allocation: Cả 4 tiến trình đều được cấp phát virtual address ở hàm
malloc. Và do việc dùng bộ nhớ vật lý tại các thời điểm khác nhau, nên cuối cùng kernel vẫn đáp ứng được cho cả 4 tiến trình.
2. Deferred Memory Allocation ảnh hưởng đến chúng ta như thế nào?
Mỗi khi cấp phát bộ nhớ với hàm malloc, sau khi hàm trả về thành công (success), điều đó chỉ có nghĩa là chúng ta đã có một địa chỉ virtual address để truy cập, chứ chưa có bộ nhớ vật lý nào được cấp phát.
Sau đó, khi tiến trình truy cập lần đầu tiên vào địa chỉ đó, nó sẽ bị tạm dừng (sleep). Từ lần truy cập thứ hai trở đi thì sẽ không bị nữa.
3. Các ví dụ lỗi thực tế liên quan đến Deferred Memory
Thực tế cho thấy có khá nhiều lỗi liên quan đến cơ chế này, ví dụ như sau:
Ví dụ 1: Hệ thống Out of Memory (OOM) tại một tiến trình ngẫu nhiên
- Hiện tượng: Hệ thống bị “Out of Memory”, nhưng mỗi lần lại “chết” ở một tiến trình khác nhau. Điều này xảy ra mặc dù ở tất cả các tiến trình, việc cấp phát bộ nhớ đều được kiểm tra cẩn thận:
char *a = malloc(100);
if (NULL == a)
printf(“out of mem”); - Nguyên nhân: Ở tiến trình gây rò rỉ bộ nhớ (memory leak), tại thời điểm hệ thống OOM, nó vẫn “vượt qua” (by pass) được hàm
malloc(vì chỉ nhận virtual address). Sau đó, ở một tiến trình khác, khi truy cập vào buffer mà nó đã cấp phát (từ trước), lúc đó hệ thống tiến hành cấp phát physical memory thì lại không còn, dẫn đến “chết” ở một tiến trình không liên quan. - Cách xử lý: Dự trữ một vùng nhớ (ví dụ 10MB) cho hệ thống. Ngay khi hệ thống chỉ còn 10MB đó, nó sẽ tiến hành báo cáo (log) việc sử dụng memory của tất cả các tiến trình. Từ đó, có thể tìm được tiến trình nào gây leak memory.
Ví dụ 2: Hệ thống Crash ngẫu nhiên khi tương tác với Driver
-
- Hiện tượng: Hệ thống thỉnh thoảng bị crash, không thể tái lập (reproduce) được, với xác suất rất nhỏ và xảy ra một cách ngẫu nhiên.
- Bối cảnh:
- User-mode code (để đọc data dưới hardware):
char *ptr = malloc(100);
read(fd, ptr, 100); - Kernel-mode code (trong driver, để ghi dữ liệu vào buffer):
spin_lock(key);
copy_to_user(ptr);
spin_unlock(key);
- User-mode code (để đọc data dưới hardware):
- Nguyên nhân: Ở dưới driver, trước khi truy cập
ptr,spin_lockđã được gọi. Sau đó, khicopy_to_usertruy cập vàoptr(lần đầu tiên), kernel tạm dừng (sleep) driver để tiến hành cấp phát physical memory. Lúc này,key(biến spinlock) vẫn đang bị driver giữ. Nếu đúng thời điểm đó, có một request đọc khác từ user-mode truyền xuống, driver (ở một CPU khác) lại chiếm CPU và cố gắng lockkeyvới hàmspin_lock(key). Nhưng vìkeyđã bị lock từ trước, tiến trình này sẽ “spin” (chờ) mãi mãi, chiếm dụng CPU và dẫn đến crash hệ thống (deadlock).
Bài học rút ra
- Nên dùng
callocđể cấp phát memory cho các buffer mà sau đó có thể được dùng dưới kernel. - Dưới kernel, nên hạn chế dùng
spin_lock.

