Quản lý bộ nhớ trong driver¶
Driver cần bộ nhớ cho hai việc: chứa dữ liệu của chính nó (cấu trúc điều khiển, buffer) và truy cập thanh ghi của thiết bị. Trang này trình bày bộ nhớ kernel từ góc nhìn của người viết driver, theo thứ tự: không gian địa chỉ và các zone, các API cấp phát (page allocator, kmalloc, vmalloc, kmem_cache, devm_*), cờ GFP chọn theo ngữ cảnh, rồi truy cập vùng I/O bằng ioremap() và readl()/writel(). Bộ nhớ dành cho DMA nằm ở DMA trong Linux kernel. Việc ánh xạ bộ nhớ cho user space nằm ở mmap, UIO, dma-buf.
Địa chỉ ảo và địa chỉ vật lý¶
CPU truy cập bộ nhớ bằng địa chỉ ảo (virtual address), được MMU dịch sang địa chỉ vật lý (physical address) qua page table. Kernel chạy trong không gian địa chỉ ảo riêng, bộ nhớ vật lý được chia thành các page (thường 4 KiB). Vì vậy:
- Con trỏ trong kernel là địa chỉ ảo. Thiết bị DMA dùng địa chỉ khác (bus address), không phải con trỏ kernel.
- Hai vùng ảo liền nhau chưa chắc liền nhau trong bộ nhớ vật lý.
- Thanh ghi của thiết bị có địa chỉ vật lý riêng và phải được ánh xạ vào không gian ảo trước khi truy cập.
Trên kernel 64-bit, kernel ánh xạ toàn bộ RAM vào một vùng ảo cố định (direct map, hay linear map), nên chuyển giữa địa chỉ ảo và vật lý của bộ nhớ thường rất rẻ. Vùng vmalloc và vùng ánh xạ I/O nằm ở các dải ảo khác.
Zone¶
Bộ nhớ vật lý được chia thành các zone theo giới hạn của phần cứng:
| Zone | Ý nghĩa |
|---|---|
ZONE_DMA, ZONE_DMA32 |
Bộ nhớ thiết bị chỉ địa chỉ hóa được bằng 24 hoặc 32 bit |
ZONE_NORMAL |
Bộ nhớ thông thường |
ZONE_HIGHMEM |
Chỉ có trên kernel 32-bit, bộ nhớ không nằm trong direct map |
Driver thường không chọn zone trực tiếp. Giới hạn địa chỉ của thiết bị được khai báo qua DMA mask, và các API DMA chọn bộ nhớ phù hợp.
Page allocator¶
Lớp cấp phát thấp nhất là page allocator (buddy allocator), cấp phát theo khối gồm 2^order page liền nhau:
struct page *pg = alloc_pages(GFP_KERNEL, 2); /* 4 page liền nhau */
void *addr = page_address(pg);
...
__free_pages(pg, 2);
Các hàm tiện dụng: __get_free_pages(), get_zeroed_page() và free_pages(). Khối lớn và liền nhau về vật lý càng khó tìm khi bộ nhớ bị phân mảnh, nên driver ít cấp phát lớn trực tiếp ở mức page.
kmalloc, vmalloc và kmem_cache¶
| API | Bộ nhớ trả về | Giới hạn | Dùng khi |
|---|---|---|---|
kmalloc(), kzalloc() |
Liền nhau về vật lý và ảo | Kích thước tối đa giới hạn (cỡ vài MiB, tùy cấu hình) | Cấu trúc điều khiển, buffer nhỏ và vừa. Lựa chọn mặc định |
vmalloc() |
Liền nhau về ảo, không liền về vật lý | Có thể rất lớn | Buffer lớn mà thiết bị không truy cập trực tiếp |
kvmalloc() |
Thử kmalloc() trước, thất bại thì dùng vmalloc() |
Như trên | Kích thước thay đổi, có thể lớn |
kmem_cache_alloc() |
Đối tượng cùng kích thước từ một cache riêng | Tạo bằng kmem_cache_create() |
Cấp phát và giải phóng rất nhiều đối tượng cùng loại |
Lưu ý:
kmalloc()dựa trên slab allocator, hiện chỉ còn SLUB, xem Linux Kernel Overview. Giải phóng bằngkfree(); bộ nhớ từvmalloc()giải phóng bằngvfree();kvmalloc()giải phóng bằngkvfree().- Bộ nhớ từ
vmalloc()không liền nhau về vật lý, nên không đưa trực tiếp cho thiết bị DMA không có scatter-gather hoặc IOMMU. - Không dùng
kzalloc(a * b)khiavàbcó thể lớn: dùngkcalloc()hoặckmalloc_array()để kiểm tra tràn số. - Sau cấp phát luôn kiểm tra giá trị trả về
NULLvà trả-ENOMEM.
Cờ GFP¶
Mọi hàm cấp phát nhận cờ GFP (Get Free Pages) cho biết ngữ cảnh gọi và cách kernel được phép tìm bộ nhớ:
| Cờ | Ngữ cảnh | Hành vi |
|---|---|---|
GFP_KERNEL |
Process context, được ngủ | Có thể ngủ và thu hồi bộ nhớ. Mặc định cho code trong probe() và syscall |
GFP_ATOMIC |
Ngữ cảnh ngắt, đang giữ spinlock | Không ngủ, có thể dùng phần dự trữ và dễ thất bại hơn |
GFP_NOWAIT |
Không được ngủ nhưng chấp nhận thất bại | Không dùng phần dự trữ khẩn cấp |
GFP_NOIO, GFP_NOFS |
Đang trong đường I/O hoặc filesystem | Cấm thu hồi bộ nhớ bằng cách gọi lại I/O hoặc filesystem, để tránh deadlock |
__GFP_ZERO |
Kèm với cờ khác | Xóa về 0, tương đương kzalloc() |
Chọn cờ theo ngữ cảnh: trong handler ngắt hoặc khi giữ spinlock phải dùng GFP_ATOMIC (xem Interrupt và bottom half và Đồng bộ hóa trong kernel). Dùng GFP_KERNEL trong ngữ cảnh đó gây cảnh báo sleeping function called from invalid context. Nên chuyển việc cấp phát ra ngoài đoạn găng, hoặc cấp phát trước khi lấy lock, thay vì dùng GFP_ATOMIC rộng rãi.
Cấp phát gắn với vòng đời thiết bị¶
Họ hàm devm_* (device-managed) gắn bộ nhớ với một struct device. Kernel tự giải phóng khi probe() thất bại hoặc khi driver gỡ khỏi thiết bị, nên không cần viết đường dọn dẹp thủ công:
Cùng nguyên tắc áp dụng cho devm_ioremap_resource(), devm_request_irq(), devm_clk_get(). Bộ nhớ devm_* sống đến khi driver bị gỡ khỏi thiết bị, không phải đến khi file /dev được đóng. Đối tượng gắn với file mở (có thể còn tồn tại sau khi gỡ driver) cần được quản lý bằng bộ đếm tham chiếu riêng.
Truy cập vùng I/O¶
Thanh ghi của thiết bị nằm trong không gian địa chỉ vật lý của CPU (memory-mapped I/O, MMIO). Để truy cập, driver ánh xạ vùng đó vào không gian ảo của kernel:
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res); /* kiểm tra, request region và ioremap */
if (IS_ERR(base))
return PTR_ERR(base);
writel(CTRL_ENABLE, base + REG_CTRL);
val = readl(base + REG_STATUS);
Hàm gộp devm_platform_ioremap_resource() làm cả hai bước lấy resource và ánh xạ trong probe() của platform driver, xem Platform Device.
Quy tắc khi truy cập MMIO:
- Con trỏ có chú thích
__iomemkhông được dereference trực tiếp (*base). Phải dùngreadb(),readw(),readl(),readq()vàwriteb()...writeq()để đảm bảo đúng độ rộng truy cập, thứ tự và endianness (các hàm này chuyển dữ liệu thiết bị little-endian sang endianness của CPU). memcpy_fromio()vàmemcpy_toio()sao chép khối giữa RAM và vùng I/O.- Vùng ánh xạ bằng
ioremap()có thuộc tính không cache (device memory), nên mỗi truy cập là một giao dịch bus riêng. Truy cập lặp lại vào thanh ghi cần tránh nếu có thể. - Hàm
readl()vàwritel()kèm barrier để thứ tự với truy cập bộ nhớ thường được giữ. Bảnreadl_relaxed()vàwritel_relaxed()bỏ bớt barrier cho đường đi nhanh, người dùng tự thêm barrier ở nơi cần thứ tự, xem Đồng bộ hóa trong kernel. - Một lần ghi vào thanh ghi (posted write) có thể chưa tới thiết bị khi hàm trở về. Khi cần chắc chắn thiết bị đã nhận (ví dụ trước khi chờ một khoảng thời gian), đọc lại một thanh ghi của thiết bị để ép ghi hoàn tất.
- Port I/O (
inb(),outb()) là không gian địa chỉ riêng của x86, ít gặp trên SoC Arm.
Giao tiếp với user space¶
Dữ liệu qua ranh giới user và kernel không được truy cập trực tiếp bằng con trỏ:
| API | Dùng cho |
|---|---|
copy_from_user(), copy_to_user() |
Sao chép khối, trả số byte chưa sao chép được |
get_user(), put_user() |
Giá trị đơn |
Các hàm này có thể ngủ (khi page bị swap) nên không gọi trong ngữ cảnh ngắt hay khi giữ spinlock. Chi tiết xem Device File và ioctl.
Quan sát bộ nhớ¶
/proc/meminfo: tổng quan, gồmSlab,VmallocUsed./proc/slabinfovà công cụslabtop: số đối tượng của từng cache (cần quyền root)./proc/buddyinfo: số khối trống theo order từng zone, cho biết mức phân mảnh./proc/iomem: bản đồ địa chỉ vật lý, gồm RAM và các vùng I/O đã được driver request.- Cấu hình debug:
CONFIG_KASANphát hiện truy cập ngoài biên và use-after-free,CONFIG_DEBUG_KMEMLEAKphát hiện rò rỉ bộ nhớ.
Liên quan¶
- DMA trong Linux kernel: bộ nhớ cho thiết bị truy cập trực tiếp, cache coherency.
- mmap, UIO, dma-buf: ánh xạ bộ nhớ kernel và thiết bị cho user space.
- Kernel Data Structure: các cấu trúc dữ liệu kernel dùng với bộ nhớ cấp phát.
- Platform Device: lấy resource của thiết bị và ánh xạ trong
probe().
Tài liệu tham khảo¶
- Memory Allocation Guide - docs.kernel.org
- Memory Management APIs - docs.kernel.org
- Bus-Independent Device Accesses - docs.kernel.org
- Managed Device Resource (devres) - docs.kernel.org
- Linux Device Drivers, 3rd ed., chương 8 (Allocating Memory) và chương 9 (Communicating with Hardware). Sách viết cho kernel 2.6.10, API đã thay đổi nhiều.