1 Bộ nhớ vật lý và bộ nhớ ảo.

Tương tự như các hệ điều hành khác trong thời đại chó lên cung trăng ngày nay, Linux cũng sử dụng một hệ thống gọi là : Hệ thống bộ nhớ ảo - Virtual Memory (VM), tất nhiên là kích thước của VM có thể lớn hơn kích thước của bộ nhớ thật. Trong Linux (mà OS khác cũng thế), mỗi process có một không gian địa chỉ riêng. Các địa chỉ này đều là các địa chỉ ảo, và khi process muốn truy cập đến Memory thì kernel sẽ giúp nó ánh xạ các địa chỉ này sang các địa chỉ vật lý tương ứng, việc này được thực hiện hoàn toàn bởi kernel, process trong Userspace không có ý thức về hành động này. Kể cả trong kernel thì địa chỉ cũng là các địa chỉ ảo, tuy nhiên việc ánh xạ địa chỉ ảo trong kernel sang địa chỉ vật lý đơn giản hơn nhiều so với các process trong User space. Trong kernel phép ánh xạ từ địa chỉ ảo sang địa chỉ vật lý (và ngược lại) đơn giản chỉ là một phép cộng (hoặc trừ) một đơn vị offset nào đó, giá trị của offset phụ thuộc vào platform đang sử dụng.

2 Kmalloc.

Mình đã từng dùng kmalloc trong bài ioctl, tuy nhiên bây giờ mới đến đoạn tìm hiểu về nó (hí hí). Prototype của hàm này như sau:

#include <linux/slab.h>
void *kmalloc(size_t size, int flags);

Hàm kmalloc nhận 2 tham số, tham số đầu tiên là kích thước của vùng nhớ sẽ được cấp phát. Tham số thứ hai, được gọi là allocation flags, là một cờ dùng để điều khiển hoạt động của kmalloc.
Flag phổ biến nhất là GFP_KERNEL, khi dùng flag này, việc cấp phát được thực hiện bằng cách gọi đến hàm __get_free_pages, GFP là tên viết tắt của hàm này. Cờ này dùng trong các process context. Sử dụng GFP_KERNEL có nghĩa là kmalloc có thể đưa process hiện tại vào hàng đợi để chờ đến khi có một page khả dụng (nếu bộ nhớ không còn đủ). Do có thể gây ra sleep nên hàm gọi đến kmalloc sử dụng cờ GFP_KERNEL không được phép chạy trong các atomic context (interrupts,…). Trong lúc process đi ngủ thì kernel sẽ thực hiện các hành động hợp lý để tìm được một vùng nhớ khả dụng, hoặc bằng cách đẩy hết buffer vào đĩa cứng, hoặc swapping mem từ một user-process.

Đôi khi kmalloc cũng có thể được gọi từ bên ngoài process context, điều này có thể xảy ra trong các interrupt handlers, tasklets hay kernel timers. Trong trường hợp này, process hiện thời sẽ đi ngủ và driver code không thể sử dụng GFP_KERNEL được, thay vào đó, GFP_ATOMIC sẽ được dùng. Thông thường kernel sẽ cố gắng giữ một vài free pages nhằm mục đích xử lý các atomic allocation. Khi GFP_ATOMIC được sử dụng, kmalloc có thể sử dụng đến những page này, trong trường hợp không còn page nào khả dụng, thì việc cấp phát bộ nhớ sẽ thất bại.

GFP_KERNELGFP_ATOMIC được gọi là các combinations of flags, hoặc các allocation priorities. Chúng được khai báo trong header linux/gfp bên cạnh các flag thông thường, các flag này thường có prefix là __. Kernel còn có một số allocation priorities cụ thể như sau:
GFP_ATOMIC : ĐƯợc sử dụng để cấp phát mem từ interrupt handler và các code nằm ngoài process context. Không bao giờ sleep, có thể thành công hoặc không.
GFP_KERNEL : Cấp phát bộ nhớ thông thường, có thể ngủ.
GFP_USER : Cấp phát bộ nhớ từ user-space pages; có thể ngủ.
GFP_HIGHUSER : Cấp phát bộ nhớ từ user-spaces pages, sử dụng high memory. GFP_NOIO, GFP_NOFS: Hoạt động giống như GFP_KERNEL nhưng chúng thêm các ràng buộc mà kernel cần làm để thỏa mãn yêu cầu. GFP_NOFS không cho phép thực hiện bất kỳ filesystem call nào, trong khi GFP_NOIO không cho phép khởi tạo bất kỳ I/O nào. Mục đích chính của các cờ này là để sử dụng trong các file system và virtual mem code, nơi mà việc allocation được cho phép ngủ, nhưng việc thực hiện các lời gọi filesystem không phải là ý kiến hay.

Các flag nêu trên có thể được kết hợp với một số flag sau đây bằng phép bitwise OR:
__GFP_DMA Yêu cầu việc cấp phát phải diễn ra trong DMA-capable mem zone.
__GFP_HIGHMEM Chỉ ra rằng bộ nhớ cấp phát có thể được đặt ở high mem.
__GFP_COLD Thông thường, memory allocator cố gắng trả về các page được tìm thấy trong processor cache - cache warm page. Cờ này sẽ yêu cầu các cold page. Cờ này hữu ích trong trường hợp cấp phát page để đọc DMA.
__GFP_NOWARN Đây là cờ hiếm khi được sử dụng. no warning.</br> __GFP_HIGH Đánh dấu request có độ ưu tiên cao, khẩn cấp. __GFP_REPEAT Trong trường hợp cấp phát không thành công, nó sẽ thử lại (nhưng có thể việc cấp phát vẫn lỗi).
__GFP_NOFAIL Việc cấp phát không bao giờ được thất bại, nó sẽ thử lại đến khi nào thành công thì thôi.
__GFP_NORETRY Bỏ cuộc ngay khi vùng nhớ yêu cầu không khả dĩ.

Trong phần giải thích các flag ở trên, có đề cập đến Memory zone, vậy mem zone là gì?
Trong linux kernel có ít nhất 3 mem zone: DMA, normal và high memory. Thông thường việc cấp phát diễn ra ở normal zone, tuy nhiên có thể được setup bằng cách sử dụng một số cờ nhất định. DMA mem là bộ nhớ cho phép thực hiện DMA access. High mem là kỹ thuật sử dụng để cho phép truy cập đến một lượng memory tương đối lớn trên 32-bit platforms.

3 Size argument

Kernel quản lý system’s physical mem, nó chỉ khả dụng trong các page-sized chunk. Kernel sử dụng page-oriented allocation technique để cấp phát bộ nhớ (thay vì head-oriented như malloc).
Linux xử lý các yêu cầu cấp phát bộ nhơ bằng cách tạo ra một tập các pool của các mem objects có kích thước cố định. Các yêu cầu cấp phát được xử lý bằng cách tìm đến một pool đang giữ số lượng object đủ lớn và trao toàn bộ một mem chunk cho người đã request.
Kernel chỉ có thể cấp phát một mảng cố định về kích thước (đã được định nghĩa từ trước) các bytes. Nếu bạn yêu cầu một lượng bộ nhớ bất kỳ, thông thường bạn sẽ nhận được nhiều hơn một ít những gì bạn yêu cầu (fragmentation), có thể lên đến gấp đôi.
memory chunk được cấp phát bởi kmalloc có giới hạn tùy thuộc vào config và arch, lớn nhất là 128KB thì phải. Trong trường hợp cần nhiều bộ nhớ hơn, thì kmalloc không phải là lựa chọn tốt nhất.

4 Lookaside caches.

Lookaside cache là một pool đặc biệt, được sử dụng cho các high-volume object (các object sử dụng nhiều bộ nhớ).
Cache manager trong kernel được gọi là slab allocator, khai báo trong linux/slab.h header. Slab allocator implement các cache có kiểu dữ liệu kmem_cache_t; chúng được tạo ra bằng cách gọi hàm kmem_cache_create. Prototye như sau:

kmem_cache_t *kmem_cache_create(const char *name, size_t size,
								size_t offset,
								unsigned long flags,
								void (*constructor)(void *, kmem_cache_t *, unsigned long flags),
								void (*constructor)(void *, kmem_cache_t *, unsigned long flags));

-Hàm này tạo ra một cache object, cache object này có thể host một số lượng bất kỳ các vùng nhớ có cùng kích thước, được chỉ ra trong tham số size.
-Tham số name hoạt động như một housekeeping cho cache và các function (cons, des). Nó rất hữu ích trong việc theo dõi các vấn đề xảy ra nhờ việc nắm giữ được đầy đủ các thông tin cần thiết. Thông thường, nó là tên của struct được cache. Cache giữ một contror tới name thay vì copy nó, do đó driver nên truyền một pointer vào name trong static storage.
-Tham số offset là offset của object đầu tiên trong page; thông thường sẽ là 0.
-Tham số flags : việc cấp phát sẽ được thực hiện như thế nào và nó là bitmask của các flags sau:
SLAB_NO_REAP Bảo vệ cache khỏi việc bị giảm dung lượng khi system tìm kiếm bộ nhớ cho những requester mới. Việc sử dụng cờ này là ý tồi, vì nó gây ra tình trạng process có mem thì không dùng đến, trong khi các process không có mem thì tìm không ra.
SLAB_HWCACHE_ALIGN
SLAB_CACHE_DMA

Constructor và destructor là optional.
Constructor và destructor có thể hữ ích trong một số trường hợp, nhưng có một số điểm cần lưu ý. Một constructor được gọi khi mem của một tập các objects được allocated; bởi vì mem đó có thể giữ nhiều object nên constructor có thể được gọi nhiều lần, nên ta không thể giả thuyết là constructor sẽ được gọi ngay lập tức sau khi cấp phát bộ nhớ cho một object. Tương tự, destructor có thể được gọi ở bất kỳ thời điểm nào trong tương lai, không nhất thiết phải là ngay lập tức sau khi một object được giải phóng.
Một khi cache of objects đã được tạo, chúng ta có thể cấp phát các mem object từ cache này bằng cách gọi kmem_cache_alloc:

void *kmem_cache_alloc(kmem_cache_t *cache, int flags);

Để free một mem object:

void kmem_cache_free(kmem_cache_t *cache, const void *obj);

Khi driver code làm xong việc với cache, nó phải free cache:

int kmem_cache_destroy(kmem_cache_t *cache);

5 Memory Pools

Đôi lúc, việc cấp phát bộ nhớ bắt buộc phải không được thất bại. Để handle được các trường hợp này, linus cung cấp một abstraction tên là mempool. Một mempool thực tế chỉ là một dạng của lookaside cache, nó luôn luôn có gắng gữ lại một list các vùng nhớ free để dùng cho trường hợp khẩn cấp.
Để tạo một mempool, cần thực hiện như sau:

#include <linux/mempool.h>

mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data);

ở đây min_nr là lượng allocated object ít nhất mà pool cần phải đảm bảo. alloc_fn là hàm thực hiện việc cấp phát thực sự, có prototype như sau:

typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);


free_fn là hàm thực hiện việc giải phóng thực sự, có prototype như sau:

typedef void *(mempool_free_t)(void *element, void *pool_data);


Thông thường, chúng ta sẽ không viết hàm free_fn và alloc_fn làm gì cả, mà sẽ sử dụng các hàm có sẵn của slab allocator:

cache = kmem_cache_create(. . .);
pool = mempool_create(MY_POOL_MINIMUM, mempool_alloc_slab, mempool_free_slab, cache);

Sau khi pool đã được tạo, các object có thể được allocate và free bằng cách sử dụng hai hàm sau:

void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);

Mempool có thể được resize bằng hàm

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);

Khi không còn sử dụng nữa, hãy giải phóng pool cho hệ thống:

void mempool_destroy(mempool_t *pool);

Lưu ý là cần phải return tất các các allocated object (mempool_free) trước khi giải phóng pool, hoặc sẽ gặp kernel oops.

6 get_free_page and Friends.

Nếu một module muốn sử dụng rất nhiều mem, thì tốt nhất là nên sử dụng kỹ thuật page-oriented. Để allocate page, sử dụng một trong các hàm sau:

get_zeroed_page(unsigned int flags);
/*Trả về con trỏ đến page mới với các mem slot đều là 0*/
__get_free_page(unsigned int flags);
/*Trả về con trỏ đến page mới nhưng không clear các mem slot */
__get_free_pages(unsigned int flags, unsigned int order);
/*Trả về con trỏ đến byte đầu tiên của một loạt page mới nhưng không clear các mem slot. Các page này là liên tục trên bộ nhớ vật lý */

Cả 3 hàm trên, tham số flags đều giống kmalloc, còn order ở cuối là số page sẽ get, theo quy tắc số page = 2^order. thường thì ta dùng order bằng 0 hoặc 3, tức là 1 page hoặc 8 page liên tục, việc dùng số order quá cao sẽ khiến việc lấy page thất bại.
Sau khi đã sử dụng xong, cần phải free page, bằng cách dùng một trong 2 hàm sau:

void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);

6. Per-CPU variables.

Leave a Comment