Skip to content

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ằng kfree(); bộ nhớ từ vmalloc() giải phóng bằng vfree(); kvmalloc() giải phóng bằng kvfree().
  • 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) khi a và b có thể lớn: dùng kcalloc() hoặc kmalloc_array() để kiểm tra tràn số.
  • Sau cấp phát luôn kiểm tra giá trị trả về NULL và 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:

md = devm_kzalloc(&pdev->dev, sizeof(*md), GFP_KERNEL);
if (!md)
    return -ENOMEM;

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 __iomem không được dereference trực tiếp (*base). Phải dùng readb(), 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ản readl_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ồm Slab, VmallocUsed.
  • /proc/slabinfo và 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_KASAN phát hiện truy cập ngoài biên và use-after-free, CONFIG_DEBUG_KMEMLEAK phát hiện rò rỉ bộ nhớ.

Liên quan

Tài liệu tham khảo