Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 14.220 phút đọc

Vùng cấp phát động

Sau bài này bạn sẽ làm được

  • Giải thích ba lý do cần vùng cấp phát động
  • So sánh heap với ngăn xếp trên bảy tiêu chí
  • Hiểu sơ lược cách bộ cấp phát quản lý khối trống
  • Nhận ra hiện tượng phân mảnh bộ nhớ

Vùng cấp phát động, thường gọi là heap, là nơi bạn tự quyết định khi nào lấy bộ nhớ và khi nào trả lại. Tự do đó đổi lấy trách nhiệm, và bốn bài cuối chương này nói về những gì xảy ra khi bạn quên trách nhiệm đó.

#Ba lý do cần vùng cấp phát động

Lý do 1: kích thước chỉ biết lúc chạy

/* Đọc một file có kích thước bất kỳ */
long co = kich_thuoc_file(f);

char *noi_dung = malloc((size_t)co + 1);      /* co chỉ biết lúc chạy */

/* Đọc số phần tử do người dùng nhập */
size_t n;

if (scanf("%zu", &n) == 1 && n > 0) {
    int *a = malloc(n * sizeof *a);
    ...
}

Lý do 2: dữ liệu phải sống lâu hơn hàm tạo ra nó

Trả về địa chỉ biến cục bộ
char *tao_chuoi(void)
{
    char s[64] = "xin chao";

    return s;      /* s chết khi hàm kết thúc */
}
Cấp phát động
char *tao_chuoi(void)
{
    char *s = malloc(64);

    if (s == NULL) return NULL;

    snprintf(s, 64, "xin chao");

    return s;      /* sống tới khi ai đó gọi free */
}

char *t = tao_chuoi();

if (t != NULL) {
    printf("%s\n", t);
    free(t);
}

Lý do 3: dữ liệu lớn hơn ngăn xếp

/* Ma trận 1000 nhân 1000 số thực là 8 megabyte, đúng bằng cả ngăn xếp */
double *m = malloc(1000 * 1000 * sizeof *m);

/* Vùng cấp phát động không có giới hạn cứng như vậy.
   Nó chỉ dừng khi hệ điều hành không còn bộ nhớ để cấp. */

#So sánh với ngăn xếp

Tiêu chíNgăn xếpVùng cấp phát động
Ai cấp phátTrình biên dịch tự độngBạn, bằng malloc
Ai thu hồiTrình biên dịch tự độngBạn, bằng free
Chi phí cấp phátMột lệnh máyHàng chục tới hàng trăm chu kỳ
Kích thước biết khi nàoLúc biên dịchLúc chạy
Dung lượngVài megabyteGần bằng bộ nhớ hệ thống
Thứ tự thu hồiBắt buộc ngược thứ tự cấpTùy ý
Hướng phát triểnXuống địa chỉ thấpLên địa chỉ cao
Thân thiện bộ nhớ đệmRất, vì tái dùng cùng vùngKém hơn, các khối nằm rải rác
Lỗi có thể xảy raTràn ngăn xếpRò rỉ, treo, giải phóng hai lần
hai-vung.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int  cuc_bo = 1;
    int *dong = malloc(sizeof *dong);

    if (dong == NULL) return 1;

    printf("ngan xep : %p\n", (void *)&cuc_bo);
    printf("cap phat : %p\n", (void *)dong);
    printf("chenh    : %.1f GB\n",
           ((char *)&cuc_bo - (char *)dong) / 1073741824.0);

    free(dong);

    return 0;
}
terminal
./hai-vung
ngan xep : 0x7ffd8c2a1e4c
cap phat : 0x55d3f52b62a0
chenh    : 43174.5 GB

Khoảng cách khổng lồ đó là chủ ý. Hai vùng nằm ở hai đầu không gian địa chỉ ảo và cùng phát triển hướng vào giữa, nên cả hai đều có chỗ để lớn mà không đụng nhau. Bài 14.5 sẽ vẽ đầy đủ bản đồ.

#Bên trong một khối cấp phát

Bộ cấp phát lưu kích thước và vài cờ ngay trước vùng bạn nhận được. Con trỏ malloc trả về đã bỏ qua phần đầu đó.
PhầnAi dùngBạn có thấy không
Đầu khốiBộ cấp phát, để biết khối rộng bao nhiêu và đã trống chưaKhông, và không được đụng vào
Vùng dữ liệuBạnCó, đây chính là chỗ con trỏ trỏ tới
Phần đệmBộ cấp phát, để căn chỉnh địa chỉKhông

Chi phí thật của một khối nhỏ

Bạn xinĐầu khốiĐệm căn chỉnhTổng thật sự chiếm
1 byte8 hoặc 16tới bội của 1632 byte
10 byte8 hoặc 16tới bội của 1632 byte
100 byte8 hoặc 16tới bội của 16112 byte
1 000 000 byte8 hoặc 16không đáng kểkhoảng 1 000 016 byte

#Bộ cấp phát làm việc thế nào

Hàm malloc không xin hệ điều hành từng lần. Nó xin một vùng lớn rồi tự chia nhỏ, vì gọi hệ điều hành tốn hàng nghìn lần nhiều hơn.

BướcChuyện xảy ra
Lần đầu chương trình gọi mallocBộ cấp phát xin hệ điều hành một vùng lớn, thường 132 kilobyte
Các lần gọi tiếp theoChia nhỏ từ vùng đã có, không đụng tới hệ điều hành
Khi gọi freeĐánh dấu khối là trống, và ghép với khối trống liền kề nếu có
Khi hết chỗ trong vùng đã cóXin hệ điều hành thêm, bằng lời gọi brk hoặc mmap
Khối rất lớn, thường trên 128 kilobyteXin riêng bằng mmap, và trả lại ngay khi free
terminal
# Xem lời gọi hệ thống thật của một chương trình gọi malloc
strace -e trace=brk,mmap ./chuong-trinh 2>&1 | head
brk(NULL)                    = 0x55d3f52b6000
brk(0x55d3f52d7000)          = 0x55d3f52d7000
mmap(NULL, 1052672, ...)     = 0x7f2a1c000000

Ba bộ cấp phát bạn có thể gặp

TênDùng ở đâuĐặc điểm
ptmalloc2glibc, tức mặc định trên LinuxCó nhiều vùng riêng cho từng luồng để giảm tranh chấp
jemallocFreeBSD, và nhiều dịch vụ lớnChống phân mảnh tốt, thống kê chi tiết
tcmallocCủa Google, dùng trong nhiều dự ánRất nhanh với chương trình nhiều luồng
terminal
# Thay bộ cấp phát mà không cần biên dịch lại
LD_PRELOAD=/usr/lib/libjemalloc.so ./chuong-trinh

#Phân mảnh bộ nhớ

Phân mảnh ngoài
Tình trạng tổng bộ nhớ trống còn nhiều nhưng không có khối liền nhau nào đủ lớn cho yêu cầu mới.
Bộ nhớ sau một loạt cấp phát và giải phóng xen kẽ:

[dùng 100][trống 50][dùng 100][trống 50][dùng 100][trống 50]

Tổng trống: 150 byte
Xin 120 byte: THẤT BẠI, vì không có khối liền nào đủ 120
phan-manh.c
/* Mẫu gây phân mảnh nặng: cấp xen kẽ hai kích thước rồi giải phóng một loại. */
#define N 100000

char *nho[N];
char *lon[N];

for (size_t i = 0; i < N; ++i) {
    nho[i] = malloc(16);
    lon[i] = malloc(1024);
}

for (size_t i = 0; i < N; ++i)
    free(lon[i]);      /* giải phóng loại lớn, để lại các khối nhỏ rải rác */

/* Giờ có khoảng 100 MB trống nhưng chia thành 100 000 mảnh 1 KB. */
Cách giảm phân mảnhGiải thích
Cấp phát các khối cùng kích thướcKhối trống luôn vừa khít yêu cầu tiếp theo
Cấp một lần, chia nhỏ tự quản lýBộ nhớ gộp, phù hợp với nhiều đối tượng cùng loại
Giải phóng theo lôCấp phát theo vùng, hủy cả vùng một lượt
Cấp trước và tái dùngKhông trả lại mà giữ trong danh sách để dùng lại
Dùng bộ cấp phát chống phân mảnhjemalloc và tcmalloc được thiết kế cho việc này
terminal
# Xem thống kê bộ cấp phát trong chương trình của bạn
man malloc_stats
void malloc_stats(void);      /* in ra stderr, phần mở rộng của glibc */

Tự làm thử

  1. In địa chỉ một biến cục bộ và một vùng vừa cấp phát, tính khoảng cách giữa hai vùng.
  2. Ghi tràn một byte ra ngoài vùng vừa cấp, chạy có và không có -fsanitize=address rồi so hai thông báo.
  3. Cấp phát một triệu khối 8 byte, đo bộ nhớ tiến trình thật sự dùng bằng công cụ hệ điều hành.
  4. Dùng strace để xem chương trình của bạn gọi brk mmap bao nhiêu lần khi gọi malloc một nghìn lần.
  5. Cài chương trình gây phân mảnh như ví dụ trong bài, in malloc_stats trước và sau.
  6. Giải phóng toàn bộ rồi kiểm tra bộ nhớ tiến trình có giảm không, giải thích kết quả.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Ba lý do cần vùng cấp phát động: kích thước chỉ biết lúc chạy, dữ liệu phải sống lâu hơn hàm, và dữ liệu lớn hơn ngăn xếp.
  • Bộ cấp phát lưu phần đầu ẩn ngay trước vùng bạn nhận được, nên free không cần biết kích thước.
  • Ghi tràn dù chỉ một byte cũng phá phần đầu của khối kế tiếp, và triệu chứng xuất hiện ở một lời gọi hoàn toàn khác.
  • malloc xin hệ điều hành một vùng lớn rồi tự chia, và free thường không trả bộ nhớ về hệ điều hành.
  • Phân mảnh là tình trạng còn nhiều bộ nhớ trống nhưng không có khối liền nào đủ lớn. Nó chỉ đáng lo với chương trình chạy dài ngày.