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ếp | Vùng cấp phát động |
|---|---|---|
| Ai cấp phát | Trình biên dịch tự động | Bạn, bằng malloc |
| Ai thu hồi | Trình biên dịch tự động | Bạn, bằng free |
| Chi phí cấp phát | Một lệnh máy | Hàng chục tới hàng trăm chu kỳ |
| Kích thước biết khi nào | Lúc biên dịch | Lúc chạy |
| Dung lượng | Vài megabyte | Gần bằng bộ nhớ hệ thống |
| Thứ tự thu hồi | Bắt buộc ngược thứ tự cấp | Tùy ý |
| Hướng phát triển | Xuống địa chỉ thấp | Lên địa chỉ cao |
| Thân thiện bộ nhớ đệm | Rất, vì tái dùng cùng vùng | Kém hơn, các khối nằm rải rác |
| Lỗi có thể xảy ra | Tràn ngăn xếp | Rò 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
| Phần | Ai dùng | Bạn có thấy không |
|---|---|---|
| Đầu khối | Bộ cấp phát, để biết khối rộng bao nhiêu và đã trống chưa | Không, và không được đụng vào |
| Vùng dữ liệu | Bạn | Có, đây chính là chỗ con trỏ trỏ tới |
| Phần đệm | Bộ 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ỉnh | Tổng thật sự chiếm |
|---|---|---|---|
| 1 byte | 8 hoặc 16 | tới bội của 16 | 32 byte |
| 10 byte | 8 hoặc 16 | tới bội của 16 | 32 byte |
| 100 byte | 8 hoặc 16 | tới bội của 16 | 112 byte |
| 1 000 000 byte | 8 hoặc 16 | khô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ước | Chuyện xảy ra |
|---|---|
| Lần đầu chương trình gọi malloc | Bộ 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 theo | Chia 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 kilobyte | Xin 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ên | Dùng ở đâu | Đặc điểm |
|---|---|---|
| ptmalloc2 | glibc, tức mặc định trên Linux | Có nhiều vùng riêng cho từng luồng để giảm tranh chấp |
| jemalloc | FreeBSD, và nhiều dịch vụ lớn | Chống phân mảnh tốt, thống kê chi tiết |
| tcmalloc | Của Google, dùng trong nhiều dự án | Rấ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 đủ 120phan-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ảnh | Giải thích |
|---|---|
| Cấp phát các khối cùng kích thước | Khố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ùng | Khô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ảnh | jemalloc 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ử
- 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.
- Ghi tràn một byte ra ngoài vùng vừa cấp, chạy có và không có
-fsanitize=addressrồi so hai thông báo. - 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.
- Dùng
straceđể xem chương trình của bạn gọibrkvàmmapbao nhiêu lần khi gọimallocmột nghìn lần. - Cài chương trình gây phân mảnh như ví dụ trong bài, in
malloc_statstrước và sau. - 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.
mallocxin hệ điều hành một vùng lớn rồi tự chia, vàfreethườ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.