The Linux Programming Interface Chương 7 · Memory Allocation

Chương 7 · Cấp phát bộ nhớ Chapter 7 · Memory Allocation

Xin thêm bộ nhớ lúc chạy: heap, program break, và lớp malloc đứng giữa Khi chương trình cần thêm chỗ nhớ giữa chừng Asking for memory at run time: the heap, the program break, and the malloc layer in between

Kerrisk tách rõ hai tầng. Ở dưới, tiến trình xin thêm bộ nhớ bằng cách dời program break — ranh giới trên của vùng heap — qua hai lời gọi hệ thống brk() và sbrk().12 Ở trên, thư viện C gói việc đó lại thành bộ đôi quen thuộc malloc()/free(), lo hết phần sổ sách để bạn cấp phát và trả vùng nhớ theo từng mảnh nhỏ.13 Chương còn mổ xẻ bên trong malloc (free list, length word), vì sao free() thường không trả bộ nhớ về nhân, những lỗi cấp phát kinh điển cùng công cụ phát hiện, và cuối cùng là alloca() — xin bộ nhớ ngay trên stack.1

Chương trình lúc chạy nhiều khi cần thêm chỗ để chứa dữ liệu mà lúc viết chưa biết trước bao nhiêu. Phần nhớ "co giãn" đó gọi là heap. Bạn ít khi tự tay dời ranh giới heap; thay vào đó bạn nhờ malloc() cắt cho một miếng, rồi free() trả lại khi xong. Thư viện C lo hết phần rắc rối bên dưới. Nhớ nhé: cần thêm chỗ nhớ thì gọi malloc, xong việc thì free.

Kerrisk separates two layers cleanly. Below, a process asks for more memory by moving the program break — the top boundary of the heap — through two system calls, brk() and sbrk().12 Above, the C library wraps that into the familiar malloc()/free() pair, doing all the bookkeeping so you can allocate and release memory in small pieces.13 The chapter also dissects malloc's internals (the free list, the length word), why free() usually does not return memory to the kernel, the classic allocation bugs and the tools that catch them, and finally alloca() — asking for memory right on the stack.1

Điểm phương pháp: chương 7 nằm đúng trên ranh giới giữa cơ chế hệ thống và tiện ích thư viện. brk()/sbrk() là system call thật sự nói chuyện với nhân để nới vùng địa chỉ của tiến trình; malloc()/free() chỉ là hàm thư viện xây trên chúng. Hiểu được sự phân tầng này giải thích phần lớn các hành vi "khó hiểu" của bộ nhớ động: vì sao bộ nhớ đã free() mà tiến trình vẫn "to" như cũ, vì sao một lần ghi tràn ra ngoài khối lại làm hỏng một khối khác chẳng liên quan, và vì sao alloca() lại nhanh hơn hẳn malloc().1

Luận đề của chương: bộ nhớ động trên heap là một hệ hai tầng — nhân chỉ biết dời program break theo khối lớn, còn thư viện C chia nhỏ vùng đó, ghi sổ bằng length word và free list ngay trong chính bộ nhớ ấy; nắm được tầng dưới (sbrk) và tầng trên (malloc/free) giải thích cả hiệu năng lẫn mọi lỗi bộ nhớ bạn sẽ gặp.1

Ý chính: có hai mức. Mức thấp, chương trình nới ranh giới heap bằng lời gọi hệ thống. Mức cao, thư viện C cắt vùng đó thành từng miếng nhỏ cho bạn dùng qua malloc/free. Hiểu hai mức này là hiểu bộ nhớ động. Nhớ nhé: malloc là người quản kho, nhân chỉ giao đất theo lô lớn.

The chapter's thesis: dynamic heap memory is a two-tier system — the kernel only knows how to move the program break in large chunks, while the C library subdivides that region, keeping its books with a length word and a free list inside the memory itself; mastering the lower tier (sbrk) and the upper tier (malloc/free) explains both performance and every memory bug you will meet.1

Cụm khái niệm 01 Concept cluster 01

Cấp phát trên heap thực chất là dời một ranh giới: program break Nới bộ nhớ heap là dời một ranh giới duy nhất Allocating on the heap is really moving one boundary: the program break

Heap là một vùng bộ nhớ co giãn nằm ngay sau đoạn dữ liệu chưa khởi tạo (uninitialized data, tức BSS) của tiến trình. Ranh giới trên hiện thời của heap được gọi là program break. Cấp phát và giải phóng bộ nhớ trên heap, ở mức thấp nhất, chỉ là việc dời program break lên (lấy thêm) hoặc xuống (trả bớt).12 Khi tiến trình khởi động, program break nằm ngay quá cuối đoạn BSS.1

Hãy hình dung heap như một bãi đất nối dài phía sau nhà, và program break là hàng rào cuối bãi. Muốn thêm chỗ chứa đồ thì đẩy hàng rào ra xa; muốn thu lại thì kéo hàng rào vào gần. Chỉ có đúng một hàng rào để dời. Nhớ nhé: thêm bộ nhớ heap nghĩa là đẩy cái hàng rào program break ra xa.

The heap is a resizable region of memory that begins just after the uninitialized data segment (the BSS) of a process. The current upper limit of the heap is called the program break. At the lowest level, allocating and freeing heap memory is just moving the program break up (taking more) or down (giving some back).12 When a process starts, the program break lies just past the end of the BSS.1

Định nghĩa. Sau khi program break được nâng lên, chương trình có thể truy cập bất kỳ địa chỉ nào trong vùng mới cấp — nhưng lúc này nhân chưa cấp trang bộ nhớ vật lý thật. Nhân chỉ tự động cấp trang vật lý vào lần đầu các địa chỉ đó được chạm tới.1

Nói đơn giản. Đẩy hàng rào ra không có nghĩa là đất đã được san lấp ngay. Chỉ khi bạn thực sự bước chân lên một chỗ, chỗ đó mới được chuẩn bị. Nhớ nhé: nới ranh giới là "đặt gạch", bộ nhớ thật chỉ hiện khi bạn dùng tới.

Definition. After the program break is raised, the program may access any address in the newly allocated region — but at this point the kernel has not yet allocated real physical pages. The kernel allocates physical pages automatically only on the first time those addresses are touched.1

#include <unistd.h>
int  brk(void *end_data_segment);
void *sbrk(intptr_t increment);

top = sbrk(0); /* break hiện thời */
sbrk(increment); /* trả về break cũ */

sbrk(0) --> hỏi ranh giới hiện thời
sbrk(+n) --> đẩy ra n byte
brk(addr) --> đặt ranh giới tới addr

#include <unistd.h>
int  brk(void *end_data_segment);
void *sbrk(intptr_t increment);

top = sbrk(0); /* current break */
sbrk(increment); /* returns old break */

  1. program break

    Program break — một con số, không phải một kho Program break là cái ranh giới, không phải kho đồ The program break — a single number, not a warehouse

    Toàn bộ vùng cấp phát động của một tiến trình được tóm gọn bằng một đại lượng: vị trí hiện thời của program break. Nâng nó lên cho chương trình thêm vùng địa chỉ dùng được; hạ nó xuống trả vùng đó về cho hệ thống. Mọi thứ phức tạp hơn — cấp phát từng mảnh nhỏ, tái sử dụng — đều được xây bên trên nguyên thủy đơn giản này.1

    Tất cả "vùng co giãn" của chương trình chỉ gói trong một con số: hàng rào đang ở đâu. Đẩy ra thì có thêm chỗ, kéo vào thì bớt chỗ. Mọi mẹo cấp phát tinh vi đều dựng trên ý tưởng đơn giản này. Nhớ nhé: cả heap chỉ cần nhớ một mốc — ranh giới đang ở đâu.

    A process's entire dynamic allocation region is captured by one quantity: the current location of the program break. Raising it gives the program more usable address space; lowering it returns that space to the system. Everything more elaborate — allocating small pieces, reusing them — is built on top of this simple primitive.1

    Bạn gặp nó ở đâuWhere you meet it Khi bạn xem /proc/PID/maps hoặc dùng strace thấy một chương trình phát lệnh brk() — chính là nó đang xin nhân nới heap.2 Khi bạn "soi" một chương trình bằng strace và thấy dòng brk(...) — đó là lúc nó đang nới chỗ nhớ. When you inspect /proc/PID/maps or use strace and see a program issue brk() — that is it asking the kernel to grow the heap.2
  2. brk()

    brk() — đặt ranh giới tới một địa chỉ tuyệt đối brk() — đặt hàng rào tới đúng một điểm brk() — set the boundary to an absolute address

    Lời gọi brk() đặt program break tới địa chỉ đã cho trong end_data_segment. Vì trang bộ nhớ chỉ cấp phát theo đơn vị trang, nên trên thực tế break được làm tròn lên biên trang kế tiếp. brk() trả về 0 khi thành công và −1 khi lỗi (đặt errno, thường là ENOMEM).12

    brk() giống như nói "dựng hàng rào ngay tại cột mốc này". Bạn đưa một địa chỉ, nhân cố đặt ranh giới tới đó. Được thì báo 0, không được thì báo −1. Nhớ nhé: brk đặt hàng rào tới đúng một điểm bạn chỉ định.

    The brk() call sets the program break to the address given in end_data_segment. Because memory is allocated in units of pages, the break is in practice rounded up to the next page boundary. brk() returns 0 on success and −1 on error (setting errno, typically ENOMEM).12

    Bạn gặp nó ở đâuWhere you meet it Hầu như không bao giờ gọi trực tiếp trong mã ứng dụng — bạn để malloc() gọi hộ. Biết nó tồn tại giúp đọc hiểu đầu ra strace và thông điệp lỗi "cannot allocate memory". Bạn gần như không bao giờ tự gọi brk — để malloc lo. Nhưng biết nó có giúp hiểu khi chương trình báo "hết bộ nhớ". You almost never call it directly in application code — you let malloc() call it for you. Knowing it exists helps you read strace output and "cannot allocate memory" errors.
  3. sbrk()

    sbrk() — dời tương đối, và trả về ranh giới cũ sbrk() — đẩy thêm một đoạn, cho biết chỗ cũ sbrk() — move relatively, and return the old boundary

    sbrk() điều chỉnh program break thêm increment byte so với vị trí hiện tại, và trả về địa chỉ của program break trước đó — nghĩa là con trỏ tới byte đầu tiên của vùng vừa cấp. Mẹo thường dùng: gọi sbrk(0) để hỏi vị trí break hiện thời mà không thay đổi nó. Khi lỗi, sbrk() trả về (void *) −1.12

    sbrk() nói "đẩy hàng rào thêm chừng này nữa", rồi mách cho bạn hàng rào trước khi đẩy đang ở đâu — đó chính là đầu miếng đất mới. Gọi sbrk(0) là chỉ hỏi "hàng rào đang ở đâu" chứ không dời. Nhớ nhé: sbrk(0) là cách lịch sự để hỏi ranh giới hiện tại.

    sbrk() adjusts the program break by increment bytes from its current position, and returns the address of the previous program break — that is, a pointer to the first byte of the newly allocated region. A common trick: call sbrk(0) to query the current break without changing it. On error, sbrk() returns (void *) −1.12

    Bạn gặp nó ở đâuWhere you meet it Khi muốn đo một chương trình đã nới heap bao nhiêu: so sbrk(0) trước và sau một loạt malloc() cho thấy heap lớn lên thế nào.1 Khi muốn biết heap đã phình to cỡ nào: đo sbrk(0) trước và sau khi gọi malloc nhiều lần. When you want to measure how much a program has grown the heap: comparing sbrk(0) before and after a run of malloc() calls shows how the heap grew.1

    Chi tiết của tác giả

    Cả brk() và sbrk() đều không có trong SUSv3, nhưng tồn tại trên gần như mọi hệ UNIX. SUSv3 cố ý loại chúng vì chúng gắn chặt với một mô hình bộ nhớ heap tuyến tính không còn phổ quát; tài liệu khuyên dùng malloc() thay cho việc gọi trực tiếp.12

    Vì sao điều này quan trọng

    Kiểu intptr_t của increment là kiểu nguyên có dấu đủ rộng để chứa một con trỏ — nhờ vậy sbrk() nhận được cả giá trị âm để hạ program break xuống, trả bộ nhớ về. Đây là nền để hiểu phần sau: vì sao free() hiếm khi làm điều đó.2

Cụm khái niệm 02 Concept cluster 02

malloc() và free(): cấp phát từng mảnh, với hợp đồng sử dụng rõ ràng malloc() xin một miếng, free() trả lại — theo luật malloc() and free(): allocating in pieces, under a clear contract

Trong ứng dụng thực, ta hiếm khi dùng brk()/sbrk(); thay vào đó dùng bộ hàm cấp phát trên heap của thư viện C. malloc() nhận một số byte và trả về con trỏ tới một khối ít nhất bằng chừng ấy byte, đã canh lề phù hợp cho bất kỳ kiểu dữ liệu C nào; nếu không cấp được, nó trả về NULL và đặt errno.13 free() nhận lại con trỏ đó và giải phóng khối.1

Trong thực tế bạn gần như luôn dùng malloc() và free() chứ không tự dời hàng rào. Bạn nói "cho tôi 100 byte", malloc trả về địa chỉ miếng nhớ đó; dùng xong bạn free() trả lại. Nếu kho hết chỗ, malloc trả về NULL — con số 0 đặc biệt báo "không có". Nhớ nhé: luôn kiểm tra malloc có trả về NULL không trước khi dùng.

In real applications we rarely use brk()/sbrk(); instead we use the C library's heap-allocation functions. malloc() takes a number of bytes and returns a pointer to a block of at least that many bytes, suitably aligned for any C data type; if it cannot allocate, it returns NULL and sets errno.13 free() takes that pointer back and releases the block.1

Hai điều phải nhớ. Thứ nhất: bộ nhớ do malloc() trả về không được khởi tạo — nội dung là rác, bạn phải tự đặt giá trị. Thứ hai: khối được canh lề cho mọi kiểu C; trên nhiều kiến trúc nghĩa là canh theo biên 8 hoặc 16 byte.13

Hai điều. Một: miếng nhớ malloc đưa cho bạn chứa toàn rác, phải tự dọn trước khi tin dùng. Hai: địa chỉ của nó luôn "chẵn trang" để chứa được mọi loại dữ liệu. Nhớ nhé: malloc không dọn nhà hộ bạn — nhớ tự khởi tạo.

Two things to remember. First: the memory returned by malloc() is not initialized — its contents are garbage, and you must set the values yourself. Second: the block is aligned for any C type; on many architectures that means an 8- or 16-byte boundary.13

#include <stdlib.h>
void *malloc(size_t size);
void  free(void *ptr);

p = malloc(n);
if (p == NULL) errExit("malloc");
/* ... dùng p ... */
free(p);

p = malloc(n);
if (p == NULL) { /* hết nhớ */ }
/* dùng p */
free(p);

#include <stdlib.h>
void *malloc(size_t size);
void  free(void *ptr);

p = malloc(n);
if (p == NULL) errExit("malloc");
/* ... use p ... */
free(p);

  1. malloc()

    malloc() — một khối byte thô, không hơn malloc() — đưa bạn một miếng byte trống malloc() — a raw block of bytes, no more

    malloc() không biết gì về kiểu dữ liệu; nó chỉ cấp một dải byte liên tục và trả về void * để bạn ép về con trỏ cần dùng. Vì void * tự động chuyển sang mọi kiểu con trỏ trong C, không cần (và không nên) ép kiểu tường minh. Kích thước truyền vào có kiểu size_t — số nguyên không dấu.13

    malloc() chẳng quan tâm bạn định chứa gì trong miếng nhớ đó — nó chỉ giao đất trống. Bạn tự quyết định dùng để làm gì. Nhớ nhé: malloc chỉ giao đất, còn xây gì là việc của bạn.

    malloc() knows nothing of data types; it just hands back a contiguous run of bytes as a void * for you to assign to the pointer you need. Since a void * converts automatically to any pointer type in C, no explicit cast is needed (or wanted). The size argument has type size_t — an unsigned integer.13

    Bạn gặp nó ở đâuWhere you meet it Mỗi lần bạn tạo một mảng động, một chuỗi độ dài chưa biết, hay một nút của danh sách liên kết — gần như luôn qua malloc(). Mỗi khi cần một mảng mà chưa biết trước bao nhiêu phần tử — đó là lúc dùng malloc. Every time you build a dynamic array, a string of unknown length, or a linked-list node — almost always through malloc().
  2. free()

    free() — trả lại khối, không phải xoá nó free() — trả miếng nhớ về, chứ không xoá free() — hand the block back, not wipe it

    free() giải phóng khối mà ptr trỏ tới — ptr phải là một con trỏ do một hàm cấp phát trên heap trả về trước đó. Nói chung, free() không hạ program break; thay vào đó nó thêm khối vừa trả vào một danh sách để các lần malloc() sau tái sử dụng.1 Gọi free(NULL) là hợp lệ và không làm gì cả.3

    free() không hề xoá dữ liệu trong miếng nhớ; nó chỉ báo "tôi dùng xong rồi, cho mượn lại". Miếng đó được cất vào kho để lần sau malloc lấy ra dùng tiếp. Nhớ nhé: free là trả lại chìa khoá, không phải đốt nhà — dữ liệu cũ vẫn còn đó.

    free() releases the block pointed to by ptr — which must be a pointer previously returned by one of the heap-allocation functions. In general, free() does not lower the program break; instead it adds the returned block to a list for future malloc() calls to reuse.1 Calling free(NULL) is valid and does nothing.3

    Bạn gặp nó ở đâuWhere you meet it Khi bạn theo dõi một tiến trình bằng top và thấy cột RSS/bộ nhớ không giảm dù chương trình đã free() rất nhiều — đó là hệ quả trực tiếp của hành vi này. Khi bạn thấy chương trình vẫn "to" dù đã free nhiều — chính vì free không trả đất về ngay. When you watch a process in top and its memory does not shrink even though the program has free()d a lot — that is a direct consequence of this behaviour.
  3. why free at all

    Có cần free() trước khi thoát không? Sắp thoát rồi có cần free() nữa không? Must you free() before exiting?

    Khi một tiến trình kết thúc, mọi bộ nhớ của nó được nhân tự động thu hồi, nên về nguyên tắc không bắt buộc free() ngay trước khi thoát. Dù vậy, vẫn nên giải phóng tường minh: mã sạch và dễ bảo trì hơn; các công cụ dò lỗi bộ nhớ (như valgrind) chỉ hữu ích khi mọi khối đều được giải phóng đúng; và đoạn mã dễ tái sử dụng trong ngữ cảnh khác.110

    Khi chương trình tắt, hệ điều hành dọn sạch mọi bộ nhớ của nó, nên về lý thuyết không cần free ngay trước khi thoát. Nhưng free đàng hoàng vẫn tốt: mã gọn, dễ kiểm lỗi, dễ dùng lại. Nhớ nhé: hệ điều hành dọn hộ khi thoát, nhưng free gọn gàng vẫn là thói quen tốt.

    When a process terminates, all of its memory is reclaimed automatically by the kernel, so freeing right before exit is not strictly required. Even so, freeing explicitly is good practice: cleaner, more maintainable code; memory-checking tools (such as valgrind) are only useful when every block is correctly freed; and the code is easier to reuse in another context.110

    Bạn gặp nó ở đâuWhere you meet it Khi một tiến trình chạy lâu (daemon, máy chủ) mà cứ cấp phát rồi quên free() — bộ nhớ phình mãi không dừng: đó là rò rỉ bộ nhớ (memory leak).1 Với chương trình chạy mãi (như máy chủ), quên free là bộ nhớ cứ phình lên — gọi là rò rỉ bộ nhớ. When a long-running process (a daemon, a server) keeps allocating and forgetting to free() — memory grows without bound: that is a memory leak.1

Cụm khái niệm 03 Concept cluster 03

Bên trong malloc: free list, length word, và vì sao lỗi bộ nhớ lại hiểm Malloc ghi sổ ra sao — và vì sao lỗi bộ nhớ khó tìm Inside malloc: the free list, the length word, and why memory bugs bite

malloc() quét một danh sách các khối trống (free list) để tìm một khối đủ lớn. Nếu khối lớn hơn mức cần, nó được cắt đôi: phần vừa đủ trả cho người gọi, phần dư trả lại free list. Nếu không khối trống nào đủ, malloc() gọi sbrk() để nới heap — và thường xin nhiều hơn mức cần ngay lúc đó, phần thừa đưa vào free list để giảm số lần gọi sbrk() về sau.1

Hãy nghĩ malloc như một người quản kho giữ danh sách "những ngăn còn trống". Khi bạn xin chỗ, anh ta tìm một ngăn vừa đủ trong danh sách; nếu ngăn quá to thì cắt bớt cho bạn phần cần. Hết ngăn trống thì mới đi xin thêm đất, mà mỗi lần xin thì xin dư ra để đỡ phải xin lại. Nhớ nhé: malloc giữ sổ "chỗ trống" để tái dùng, chứ không xin nhân mỗi lần.

malloc() scans a list of free blocks (the free list) to find one large enough. If the block is larger than needed, it is split: the exact piece goes to the caller, the remainder goes back on the free list. If no free block is large enough, malloc() calls sbrk() to grow the heap — and usually asks for more than is needed right then, putting the surplus on the free list to reduce the number of future sbrk() calls.1

Mấu chốt. Khi cấp một khối, malloc() lấy dư vài byte để chứa một số nguyên ghi độ dài của khối (length word), đặt ngay trước vùng trả cho bạn. Nhờ đó free(ptr) chỉ cần nhìn mấy byte ngay trước ptr là biết khối to bao nhiêu — đó là lý do bạn không phải truyền kích thước cho free().1

Mấu chốt. Ngay trước miếng nhớ trao cho bạn, malloc giấu một con số nhỏ ghi "miếng này dài bao nhiêu". Nhờ con số đó mà khi free bạn khỏi cần nói lại kích thước. Nhớ nhé: malloc dán sẵn một nhãn ghi kích thước ngay trước miếng nhớ của bạn.

The key. When it allocates a block, malloc() takes a few extra bytes to hold an integer recording the block's length (the length word), placed just before the region it returns to you. Thanks to this, free(ptr) only needs to look at the few bytes just before ptr to know how big the block is — which is why you never pass a size to free().1

[ length ][ vùng bạn dùng ....... ]
         ^-- ptr do malloc trả về

free list: khối-->khối-->khối
(con trỏ nối nằm trong chính khối trống)

[nhãn độ dài][ phần bạn dùng ]
        ^ ptr trả về
kho trống: khối -> khối -> khối

[ length ][ your usable region ...... ]
         ^-- ptr returned by malloc

free list: block-->block-->block
(links live inside the free blocks themselves)

  1. free list

    Free list sống ngay trong bộ nhớ trống Danh sách chỗ trống nằm trong chính chỗ trống The free list lives inside the free memory itself

    Khi một khối được free(), nó được thêm vào free list. Nhưng free list không dùng bộ nhớ riêng để lưu — nó dùng chính không gian của các khối trống: trong mỗi khối trống, free() ghi một cặp con trỏ nối tới khối trống trước và sau, cùng với length word. Chính vì bộ nhớ "sổ sách" này nằm xen kẽ với bộ nhớ của bạn mà một lỗi ghi tràn ra ngoài khối có thể làm hỏng toàn bộ danh sách.1

    Danh sách "chỗ trống" không được viết vào một cuốn sổ riêng, mà viết lên chính những ngăn đang trống. Nên nếu bạn lỡ viết lan ra ngoài ngăn của mình, bạn có thể bôi bẩn trang sổ của người quản kho — và kho loạn. Nhớ nhé: viết tràn ra ngoài khối là tự phá sổ sách của malloc.

    When a block is free()d, it is added to the free list. But the free list uses no separate memory to store itself — it uses the space of the free blocks: inside each free block, free() writes a pair of pointers linking the previous and next free block, along with the length word. Because this bookkeeping memory is interleaved with your memory, an overrun past a block's bounds can corrupt the whole list.1

    Bạn gặp nó ở đâuWhere you meet it Khi một chương trình đổ vỡ bên trong malloc() hay free() với thông điệp kiểu "corrupted" — thủ phạm thường không ở đó, mà là một lần ghi tràn từ rất sớm trước đó.3 Khi chương trình chết ngay trong malloc/free với chữ "corrupted" — lỗi thật thường đã xảy ra từ trước đó khá lâu. When a program crashes inside malloc() or free() with a "corrupted" message — the culprit is usually not there, but an earlier overrun.3

    Hệ quả: các lỗi kinh điển

    Vì sổ sách nằm trong khối, một loạt lỗi trở nên nguy hiểm: ghi ra ngoài biên khối (buffer overrun/underrun), free() hai lần một con trỏ (double free), free() một con trỏ không do malloc() cấp, hay dùng khối sau khi đã free() (dangling pointer). Tất cả đều có thể âm thầm làm hỏng free list rồi bùng phát ở một chỗ khác hẳn.1

    Quy tắc vàng

    Chỉ free() đúng con trỏ do malloc()/calloc()/realloc() trả về; không bao giờ dùng lại con trỏ sau khi đã giải phóng; và không bao giờ ghi vượt quá số byte đã xin. Đặt con trỏ về NULL ngay sau free() là một thói quen phòng double-free.1

  2. why free keeps it

    Vì sao free() không trả bộ nhớ về nhân Vì sao free() giữ bộ nhớ lại thay vì trả về Why free() keeps memory instead of returning it

    Thường free() không hạ program break vì hai lý do. Một: khối được trả thường nằm đâu đó giữa heap chứ không ở trên đỉnh, nên không thể hạ ranh giới qua nó. Hai: dù khối có nằm trên đỉnh, việc giữ lại giúp giảm số lần gọi sbrk() — một system call tốn kém — vì chương trình thường sẽ sớm xin lại bộ nhớ.1

    Free thường không trả đất về nhân vì: miếng vừa trả hay nằm ở giữa bãi, không phải mép ngoài, nên không kéo hàng rào qua nó được; và kể cả nằm ở mép, giữ lại vẫn lợi hơn vì lát nữa chương trình thường lại xin tiếp — đỡ phải làm phiền nhân nhiều lần. Nhớ nhé: giữ lại để tái dùng thì nhanh hơn trả đi rồi xin lại.

    Usually free() does not lower the program break, for two reasons. One: the returned block is typically somewhere in the middle of the heap, not at the top, so the boundary cannot be lowered past it. Two: even when the block is at the top, keeping it reduces the number of sbrk() calls — an expensive system call — because the program will often soon ask for memory again.1

    Bạn gặp nó ở đâuWhere you meet it Khi một khối trống ở trên đỉnh heap đủ lớn, hiện thực glibc có thể dùng sbrk() hạ program break; ngưỡng này điều chỉnh được qua tham số M_TRIM_THRESHOLD (mặc định 128 kB).4 Nếu miếng trống nằm ngay mép ngoài và đủ lớn, malloc vẫn có thể trả đất về — có một ngưỡng mặc định khoảng 128 kB. When a free block at the top of the heap is large enough, the glibc implementation may use sbrk() to lower the break; this threshold is tunable via M_TRIM_THRESHOLD (default 128 kB).4

    Khối lớn đi đường khác

    Với các yêu cầu cấp phát lớn (mặc định từ 128 kB trở lên, ngưỡng M_MMAP_THRESHOLD), glibc không dùng heap/sbrk() mà cấp hẳn một vùng riêng bằng mmap() — và vùng này được trả thẳng về hệ thống bằng munmap() khi free(). Đó là lý do một số khối lớn có trả bộ nhớ về ngay.4

    Theo dõi và chỉnh gói malloc

    Hàm mallopt() cho phép chỉnh các tham số như M_TRIM_THRESHOLD, M_MMAP_THRESHOLD, M_MMAP_MAX; còn mallinfo() trả về một cấu trúc thống kê về bộ nhớ mà gói malloc đang quản lý. Đây là cửa để quan sát và tinh chỉnh hành vi cấp phát khi hiệu năng quan trọng.45

  3. debugging tools

    Công cụ bắt lỗi malloc: từ trong glibc tới valgrind Công cụ dò lỗi bộ nhớ Tools for malloc bugs: from inside glibc to valgrind

    glibc có sẵn vài trợ thủ. mtrace()/muntrace() bật/tắt việc ghi lại mọi lời gọi cấp phát vào tệp chỉ bởi biến môi trường MALLOC_TRACE, rồi script mtrace phân tích để tìm khối chưa được giải phóng.18 mcheck()/mprobe() (liên kết với -lmcheck) kiểm tra tính nhất quán của heap; biến môi trường MALLOC_CHECK_ cho tác dụng tương tự mà không cần sửa và biên dịch lại mã.19

    Có nhiều công cụ giúp bắt lỗi bộ nhớ thay bạn. Một số nằm sẵn trong thư viện C: bật một công tắc là nó ghi nhật ký cấp phát hoặc tự kiểm tra kho. Bạn không phải tự dò bằng mắt. Nhớ nhé: đừng dò lỗi bộ nhớ bằng mắt — hãy để công cụ làm.

    glibc ships several helpers. mtrace()/muntrace() turn on/off the recording of every allocation call to a file named by the MALLOC_TRACE environment variable, which the mtrace script then analyses to find unreleased blocks.18 mcheck()/mprobe() (linked with -lmcheck) check heap consistency; the MALLOC_CHECK_ environment variable gives similar effect without modifying and recompiling the code.19

    Bạn gặp nó ở đâuWhere you meet it Khi cần truy một lỗi bộ nhớ khó, công cụ ngoài mạnh nhất là Valgrind, phát hiện rò rỉ và truy cập sai; ngoài ra còn Electric Fence (Bruce Perens) đặt bẫy đúng biên khối để bắt ghi tràn ngay tức thì.17 Khi lỗi khó quá, dùng Valgrind — công cụ ngoài rất mạnh, chỉ ra chỗ rò rỉ và truy cập sai. When you need to hunt a hard memory bug, the strongest external tool is Valgrind, which detects leaks and invalid access; there is also Electric Fence (Bruce Perens), which traps exactly at block boundaries to catch an overrun immediately.17

    MALLOC_CHECK_ nhận giá trị nào

    Đặt MALLOC_CHECK_ thành 0 (bỏ qua lỗi), 1 (in thông báo rồi tiếp tục), 2 (gọi abort()), hoặc 3 (in thông báo chi tiết kèm vết gọi rồi abort). Vì lý do an ninh, biến này bị bỏ qua trong các chương trình set-user-ID/set-group-ID.4

    Vì sao cần cả hai hướng

    Công cụ trong glibc nhẹ, chạy nhanh, hợp để bật thường trực lúc phát triển; Valgrind nặng hơn nhiều (chương trình chạy chậm đi đáng kể) nhưng soi được những lỗi mà kiểm tra nội bộ bỏ sót, như đọc một biến chưa khởi tạo. Hai hướng bổ sung cho nhau.17

Cụm khái niệm 04 Concept cluster 04

calloc, realloc, cấp phát canh lề — và alloca() trên stack Các cách cấp phát khác, và cách mượn nhớ trên stack calloc, realloc, aligned allocation — and alloca() on the stack

Quanh malloc() có một họ hàm cho các tình huống riêng: calloc() cấp phát cho một mảng và khởi tạo về 0; realloc() đổi cỡ một khối đã cấp; còn memalign()/posix_memalign() cấp khối canh lề theo biên bạn chỉ định.136 Tách hẳn ra là alloca(): xin bộ nhớ không phải trên heap mà trên stack, và được thu hồi tự động.17

Bên cạnh malloc còn mấy "người anh em": một cái vừa cấp vừa dọn sạch về 0, một cái giúp nới rộng miếng nhớ cũ, một cái cấp miếng có địa chỉ "chẵn" theo yêu cầu. Và một cái rất khác: mượn nhớ ngay trên stack, dùng xong tự biến mất. Nhớ nhé: mỗi nhu cầu có một hàm cấp phát hợp với nó.

Around malloc() is a family of functions for special situations: calloc() allocates for an array and zero-initializes it; realloc() resizes an allocated block; and memalign()/posix_memalign() allocate a block aligned to a boundary you specify.136 Set apart is alloca(): it takes memory not from the heap but from the stack, and it is reclaimed automatically.17

Phân biệt nhanh. calloc(n, size) cấp cho n phần tử mỗi phần size byte và đặt tất cả về 0. realloc(ptr, size) đổi cỡ khối tại ptr — có thể dời khối đi nơi khác, nên phải dùng con trỏ mới nó trả về. posix_memalign(&p, align, size) trả về 0 khi thành công (không đặt errno) và yêu cầu align là lũy thừa của 2 đồng thời là bội của sizeof(void *).136

Phân biệt nhanh. calloc: cấp cho một mảng và lau sạch về 0. realloc: nới/thu miếng cũ, có thể dời chỗ nên phải nhận con trỏ mới. posix_memalign: cấp miếng có địa chỉ "chẵn" theo yêu cầu. Nhớ nhé: realloc có thể dời đồ của bạn — luôn dùng con trỏ nó trả về.

Quick distinctions. calloc(n, size) allocates n items of size bytes each and sets them all to 0. realloc(ptr, size) resizes the block at ptr — it may move the block elsewhere, so you must use the new pointer it returns. posix_memalign(&p, align, size) returns 0 on success (and does not set errno), and requires align to be a power of two and a multiple of sizeof(void *).136

p = calloc(n, size); /* về 0 */
q = realloc(p, newsize);
if (q) p = q; /* có thể đã dời */
posix_memalign(&r, 4096, size);
buf = alloca(len); /* trên stack */

calloc(n, size) -> sạch về 0
realloc(p, n) -> đổi cỡ, nhận con trỏ mới
alloca(n) -> mượn trên stack

p = calloc(n, size); /* zeroed */
q = realloc(p, newsize);
if (q) p = q; /* may have moved */
posix_memalign(&r, 4096, size);
buf = alloca(len); /* on the stack */

  1. calloc() / realloc()

    calloc() dọn sạch, realloc() đổi cỡ calloc dọn về 0, realloc nới rộng calloc() zeroes, realloc() resizes

    calloc() tiện khi cần một mảng khởi đầu toàn 0 — nó còn thay bạn tính n × size và tránh tràn số. realloc() thường dùng để tăng cỡ một khối; khi không nới tại chỗ được, nó cấp khối mới, sao chép dữ liệu cũ sang rồi giải phóng khối cũ. Phần byte tăng thêm (nếu nới rộng) không được khởi tạo.13

    calloc giống malloc nhưng lau sạch miếng nhớ về 0 trước khi đưa. realloc giúp bạn đổi cỡ một miếng cũ: nếu chỗ cũ không đủ, nó tìm chỗ mới, bê đồ sang, rồi trả chỗ cũ. Nhớ nhé: phần mới thêm sau realloc vẫn là rác, phải tự khởi tạo.

    calloc() is handy when you need an array that starts all zero — it also computes n × size for you and guards against overflow. realloc() is commonly used to grow a block; when it cannot resize in place, it allocates a new block, copies the old data across, and frees the old block. The added bytes (when growing) are not initialized.13

    Bạn gặp nó ở đâuWhere you meet it Cạm bẫy kinh điển: viết p = realloc(p, n);. Nếu thất bại, realloc() trả NULL mà khối cũ vẫn còn — bạn vừa mất con trỏ tới nó: rò rỉ. Hãy nhận vào một biến tạm trước.3 Đừng viết p = realloc(p, n) — nếu hỏng, bạn mất luôn miếng cũ. Hãy dùng một biến tạm để hứng kết quả. A classic trap: writing p = realloc(p, n);. If it fails, realloc() returns NULL while the old block still exists — and you have just lost the pointer to it: a leak. Receive it into a temporary first.3
  2. posix_memalign()

    Cấp phát canh lề — khi địa chỉ phải "chẵn" Khi cần miếng nhớ bắt đầu ở địa chỉ "chẵn" Aligned allocation — when the address must be a multiple

    Đôi khi ta cần một khối bắt đầu đúng tại biên lũy thừa của 2 (ví dụ biên trang). memalign() (có trong <malloc.h>, không thuộc mọi hệ UNIX) và posix_memalign() (chuẩn SUSv3 về sau) làm việc đó. posix_memalign() đặt con trỏ vào tham số đầu, trả về 0 nếu thành công; vùng cấp được giải phóng bằng chính free().16

    Có lúc địa chỉ miếng nhớ phải "tròn trịa" — ví dụ chia hết cho 4096. Mấy hàm cấp phát canh lề làm đúng việc đó: bạn nói cần tròn theo mức nào, nó cho miếng bắt đầu đúng chỗ. Dùng xong vẫn free như thường. Nhớ nhé: cần địa chỉ chẵn thì dùng posix_memalign, vẫn free bình thường.

    Sometimes we need a block starting exactly on a power-of-two boundary (a page boundary, say). memalign() (in <malloc.h>, not on every UNIX) and posix_memalign() (later standardized by SUSv3) do this. posix_memalign() stores the pointer in its first argument and returns 0 on success; the region is released with ordinary free().16

    Bạn gặp nó ở đâuWhere you meet it Khi làm I/O trực tiếp (mở tệp với O_DIRECT): nhân đòi vùng đệm canh theo biên khối thiết bị, nên bạn cần một bộ cấp phát canh lề thay vì malloc() thường.6 Khi đọc/ghi "trực tiếp" xuống đĩa, hệ thống đòi vùng đệm có địa chỉ chẵn — lúc đó cần hàm cấp phát canh lề. When doing direct I/O (opening a file with O_DIRECT): the kernel requires a buffer aligned to the device block boundary, so you need an aligned allocator rather than plain malloc().6

    Ràng buộc tham số

    Với posix_memalign(), giá trị alignment phải vừa là lũy thừa của 2 vừa là bội của sizeof(void *); nếu không, hàm trả về mã lỗi EINVAL. Khác với hầu hết hàm thư viện, nó không đặt errno mà trả thẳng số lỗi làm giá trị trả về.6

    Hướng hiện đại

    Trang man ngày nay khuyến nghị dùng aligned_alloc() (chuẩn C11) vì giao diện gọn hơn và cũng cho cùng khả năng — một ví dụ cho thấy API canh lề đã tiến hoá sau thời điểm sách ra đời (2010).6

  3. alloca()

    alloca() — nhanh, tự dọn, nhưng nhiều ràng buộc alloca() — mượn trên stack, tự trả, nhưng cẩn thận alloca() — fast, self-cleaning, but constrained

    alloca() cấp bộ nhớ bằng cách nới khung stack của hàm đang chạy — chỉ việc dời con trỏ stack, nên nhanh hơn malloc(). Bộ nhớ này được tự động giải phóng khi hàm gọi nó trả về; bạn không được gọi free() lên nó. Nó cũng tự thu hồi đúng đắn khi stack bị tháo gỡ bởi longjmp()/siglongjmp() — nơi mà bộ nhớ malloc() sẽ rò rỉ.17

    alloca() mượn chỗ ngay trên stack của hàm hiện tại. Nó nhanh, và khi hàm kết thúc thì chỗ đó biến mất luôn — bạn khỏi phải dọn, và cũng không được gọi free. Rất gọn, nhưng chỉ hợp cho miếng nhỏ. Nhớ nhé: alloca thì không bao giờ free — stack tự dọn khi hàm trả về.

    alloca() allocates memory by enlarging the stack frame of the running function — just by moving the stack pointer, so it is faster than malloc(). This memory is freed automatically when the function that called it returns; you must not call free() on it. It is also reclaimed correctly when the stack is unwound by longjmp()/siglongjmp() — where malloc() memory would leak.17

    Bạn gặp nó ở đâuWhere you meet it Hãy dè chừng: xin quá nhiều bằng alloca() làm tràn stack mà không có cách phát hiện — hành vi không xác định. Và đừng gọi alloca() ngay trong danh sách đối số của một lời gọi hàm.17 Cẩn thận: xin quá nhiều bằng alloca sẽ làm tràn stack mà không báo trước. Chỉ dùng cho miếng nhỏ. Beware: asking for too much with alloca() overflows the stack with no way to detect it — undefined behaviour. And do not call alloca() inside the argument list of a function call.17

Ba ngộ nhận thường gặp Three common misconceptions

“free() trả bộ nhớ lại cho hệ điều hành ngay” “free() gives the memory straight back to the OS”

Ngộ nhận: cứ free() là tiến trình nhỏ lại.

Thực tế: thường free() chỉ đưa khối vào free list để tái dùng; program break thường đứng yên vì khối nằm giữa heap và để giảm số lần gọi sbrk().1

Myth: a free() makes the process shrink.

Reality: usually free() just puts the block on the free list for reuse; the program break typically stays put because the block is mid-heap and to cut the number of sbrk() calls.1

“malloc() trả về bộ nhớ đã sạch về 0” “malloc() returns zeroed memory”

Ngộ nhận: vùng vừa xin có thể tin là toàn số 0.

Thực tế: malloc() trả về bộ nhớ chưa khởi tạo — nội dung là rác; chỉ calloc() mới lau sạch về 0.13

Myth: the freshly requested region can be trusted to be all zeros.

Reality: malloc() returns uninitialized memory — its contents are garbage; only calloc() zeroes it.13

“alloca() cũng phải free() như malloc()” “alloca() memory must be free()d like malloc()”

Ngộ nhận: cấp phát là phải giải phóng, nên alloca() cũng cần free().

Thực tế: alloca() cấp trên khung stack và tự biến mất khi hàm trả về; gọi free() lên nó là lỗi.17

Myth: what is allocated must be freed, so alloca() needs free() too.

Reality: alloca() allocates on the stack frame and vanishes when the function returns; calling free() on it is a bug.17

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm cấp phát bộ nhớ Năm câu hỏi kiểm tra nhanh Five questions to check you grasped memory allocation

Trả lời được những câu này nghĩa là bạn đã nắm cả hai tầng của bộ nhớ động: program break bên dưới và lớp malloc bên trên.

Nếu trả lời được, nghĩa là bạn đã hiểu chương này.

Answering these means you grasp both tiers of dynamic memory: the program break below and the malloc layer above.

  1. Program break là gì, và brk() khác sbrk() ra sao? sbrk(0) làm gì?

    Gợi ý sâu hơn: vì sao bộ nhớ vừa nới bằng sbrk() lại chưa có trang vật lý cho tới lần chạm đầu tiên?

  2. Sau khi malloc() trả về, nội dung khối nhớ là gì, và bạn phải kiểm tra điều gì trước khi dùng?

    Gợi ý sâu hơn: vì sao không nên (và không cần) ép kiểu giá trị void * mà malloc() trả về?

  3. Nêu hai lý do vì sao free() thường không hạ program break để trả bộ nhớ về nhân.

    Gợi ý sâu hơn: trường hợp nào thì bộ nhớ có được trả về ngay (gợi ý: ngưỡng và mmap())?

  4. free() làm sao biết khối cần giải phóng to bao nhiêu, và vì sao một lần ghi tràn ra ngoài khối lại nguy hiểm đến vậy?

    Gợi ý sâu hơn: free list được lưu ở đâu, và điều đó liên hệ thế nào với double free?

  5. Phân biệt malloc(), calloc(), realloc(); và alloca() khác chúng ở điểm cốt lõi nào?

    Gợi ý sâu hơn: vì sao p = realloc(p, n); là một khuôn mẫu nguy hiểm?

  1. Heap là gì, và "nới bộ nhớ heap" nghĩa là làm gì?

  2. Sau khi gọi malloc, bạn phải kiểm tra gì trước khi dùng miếng nhớ?

  3. Vì sao gọi free rồi mà chương trình vẫn không nhỏ lại?

  4. Vì sao viết tràn ra ngoài miếng nhớ đã xin lại nguy hiểm?

  5. alloca khác malloc ở chỗ nào, và vì sao không bao giờ free alloca?

  1. What is the program break, and how does brk() differ from sbrk()? What does sbrk(0) do?

  2. After malloc() returns, what are the block's contents, and what must you check before using it?

  3. Give two reasons why free() usually does not lower the program break to return memory to the kernel.

  4. How does free() know how big the block is, and why is an overrun past a block so dangerous?

  5. Distinguish malloc(), calloc(), realloc(); and how does alloca() fundamentally differ from them?

Khép lại chương Closing the chapter

Bộ nhớ động là một hệ hai tầng: nhân chỉ biết dời program break bằng brk()/sbrk(), còn thư viện C cắt nhỏ heap và ghi sổ bằng length word và free list ngay trong chính bộ nhớ đó. Hiểu điều ấy, bạn hiểu luôn vì sao free() thường giữ lại bộ nhớ, vì sao một lần ghi tràn lại chí mạng, và vì sao alloca() chẳng bao giờ cần free().1

Có hai tầng: nhân chỉ dời một cái hàng rào, còn thư viện C cắt bãi đất thành từng miếng cho bạn. Hiểu hai tầng này là hiểu mọi chuyện lạ của bộ nhớ động. Nhớ nhé: malloc là người quản kho, nhân chỉ giao đất theo lô lớn.

Dynamic memory is a two-tier system: the kernel only knows how to move the program break with brk()/sbrk(), while the C library carves the heap and keeps its books with a length word and free list inside that very memory. Grasp that, and you also grasp why free() usually keeps memory, why one overrun is fatal, and why alloca() never needs free().1