Bài học trước đã giới thiệu về các kỹ thuật cũng như cách sử dụng các API cấp phát bộ nhớ cho tiến trình. Trong bài này, chúng ta sẽ bàn luận về phương pháp ánh xạ bộ nhớ (memory mapping), vốn được sử dụng trong các kỹ thuật rất quan trọng như giao tiếp giữa các tiến trình hoặc cấp phát bộ nhớ. Thuật ngữ tiếng anh memory mapping rất phổ biến trong lập trình Linux, nên từ thời điểm này chúng ta sẽ dùng thuật ngữ này khi nói về ánh xạ bộ nhớ.
Cơ bản về memory mapping
Memory mapping là kỹ thuật tạo ra một vùng nhớ ánh xạ trên không gian bộ nhớ ảo của tiến trình. Vùng nhớ này có thể được ánh xạ đến 1 file cụ thể (file mapping) hoặc không đến 1 file nào (anonymous mapping), có thể được tạo ra với chế độ riêng tư (private) hoặc chia sẻ được để thực hiện nhiều mục đích khác nhau.
-
File mapping: Kỹ thuật file mapping ánh xạ trực tiếp 1 phân vùng của đường dẫn file vào bộ nhớ ảo của tiến trình thực hiện. Sau khi file mapping thành công, các trang của vùng nhớ ánh xạ đó sẽ tự động được tải vào từ file tương tứng. Và nội dung của vùng nhớ file đó có thể được truy cập hoặc sửa đổi bằng cách thao tác trên vùng nhớ ánh xạ đó. Kỹ thuật này còn được gọi là file-base mapping hoặc memory-mapped file.
-
Anonymous mapping: Kỹ thuật này tạo ra vùng nhớ ánh xạ không map với 1 file cụ thể nào. Vì vậy, nội dung của vùng nhớ sau đó sẽ không được tải vào từ file nào mà sẽ được khởi tạo giá trị 0.
Trong Linux, memory mapping có thể thực hiện bằng system call mmap(), chúng ta sẽ tìm hiểu về system call này sau. Vùng nhớ ánh xạ của tiến trình này có thể được chia sẻ với vùng nhớ ánh xạ của tiến trình khác (cùng trỏ đến các vùng nhớ giống nhau trên RAM). Khi 2 hoặc nhiều tiến trình cùng chia sẻ vùng nhớ ánh xạ, tiến trình này có thể nhìn thấy sự thay đổi của vùng nhớ khi tiến trình khác thay đổi hay không phụ thuộc vào việc chế độ mapping là private hay shared.
-
Private mapping: Sự thay đổi nội dung của vùng nhớ mapping không được nhìn thấy bởi tiến trình khác. Nếu là file mapping, nội dung của file cũng sẽ không thay đổi. Kernel làm được việc này bằng kỹ thuật copy-on-write. Nghĩa là trước khi thay đổi thì 2 tiến trình chia sẻ vùng nhớ mapping, nhưng nếu 1 tiến trình thay đổi nội dung vùng nhớ đó, kernel trước hết sẽ tạo ra 1 bản copy của các trang thay đổi đó trên vùng nhớ riêng biệt của bộ nhớ ảo. Vì vậy, kỹ thuật mapping với quyền private còn được gọi là copy-on-write mapping.
-
Shared mapping: Sự thay đổi nội dung của vùng nhớ mapping bởi 1 tiến trình sẽ được nhìn thấy bởi các tiền trình khác cùng chia sẻ vùng nhớ đó. Nếu là file mapping, nội dung file cũng sẽ được cập nhật thay đổi.
Trong không gian bộ nhớ ảo của tiến trình, vùng nhớ memory mapping được đặt tại 1 vùng nhớ chưa được cấp phát riêng biệt, không thuộc stack cũng như heap của tiến trình. Để hiểu rõ hơn về kỹ thuật memory maping, chúng ta xem hình vẽ minh họa dưới đây:
Hình 1: File memory mapping
Để ý trong hình vẽ trên, tiến trình tạo ra 1 vùng nhớ ảo trong không gian “Mapped Memory” được ánh xạ đến 1 file thực trong hệ thống. Nhưng tiến trình không mapp toàn bộ nội dung file, mà map 1 vùng nhớ kích thước “Length” bắt đầu từ “offset” trong file.
Tạo memory mapping bằng mmap()
System call mmap() được dùng để tạo ra memory mapping trong tiến trình gọi có prototype như sau:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*Trả về địa chỉ đầu tiên của mapping, hoặc MAP_FAILED nếu lỗi*/
Đối số truyền vào “addr” chỉ định địa chỉ đầu tiên của vùng nhớ mapping trong bộ nhớ ảo của tiến trình. Nếu “addr” được truyền NULL (cách này được sử dụng rất thường xuyên), kernel sẽ chọn 1 địa chỉ thích hợp để đặt vùng nhớ mapping này.
Đối số “length” chỉ định kích thước của vùng nhớ mapping theo đơn vị byte. Lưu ý rằng, kernel tạo memory mapping theo đơn vị là trang (page). Như bài trước đã nói, mỗi bộ nhớ của tiến trình được chia thành nhiều page với kích cỡ cố định (thường là 4096 byte). Nếu đối số “length” không phải là bội số của page size, kernel sẽ tạo ra memory mapping với kích thước làm tròn lên bội số của page size vừa đủ để bao phủ kích thước length. Ví dụ, nếu bạn dùng mmap() tạo ra vùng mapping với length là 4000 byte, kernel sẽ tạo ra vùng mapping với kích thước 1 page (4096 byte).
Đối số “prot” là bit mask chỉ định chế độ bảo vệ truy cập của vùng mapping. Nó có thể là PROT_NONE hoặc kết hợp (toán tử OR) với các cờ sau đây:
Giá trị | Mô tả |
PROT_NONE | Vùng mapping không cho phép truy cập |
PROT_READ
PROT_WRITE PROT_EXEC |
Vùng mapping cho phép đọc
Vùng mapping cho phép ghi Vùng mapping cho phép execute |
Đối số “flag” là bit mask để thiết lập vùng nhớ mapping là private hay shared, cách hoạt động của 2 chế độ này như đã nói ở trên.
Hai đối số fd và offset tương ứng là mô tả file và giá trị offset tính từ đầu file của file muốn mapping. Hai đối số này chỉ được sử dụng với file mapping, do anonymous mapping không map với 1 file nào cả.
Ví dụ về mmap()
Hãy cùng xét 1 ví dụ cụ thể để hiểu rõ hơn về system call mmap(). Chương trình dưới đây là 1 chương trình đơn giản của câu lệnh “cat” (đọc nội dung của 1 file đưa ra màn hình stdout). Đầu tiên, chúng ta tạo ra 1 file có nội dung và nằm cùng đường dẫn với code (ví dụ file hello.txt). Chương trình sử dụng mmap() để ánh xạ nội dung của file hello.txt lên vùng nhớ của tiến trình. Do vậy, khi đọc nội dung của vùng nhớ đó, cũng giống như đọc nội dung của file mà không cần dùng các system call file I/O nữa. Trong chương trình, chúng ta gọi hàm fstat() để lấy được thông tin độ lớn của file (được lưu vào biến “sb.st_size”)
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
char *addr;
int fd;
struct stat sb;
fd = open("hello.txt", O_RDONLY);
if (-1 == fd)
{
printf("Open file errorn");
}
/*Obtain the size of the file and save to sb structure*/
/*Use it to specify the size of the mapping*/
if (-1 == fstat(fd, &sb))
{
printf("Stat file errorn");
}
addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (MAP_FAILED == addr)
{
printf("mmap errorn");
}
if (write(STDOUT_FILENO, addr, sb.st_size) != sb.st_size)
{
printf("write to stdout errorn");
}
exit(EXIT_SUCCESS);
}
Bây giờ compile và chạy chương trình, nội dung của file “hello.txt” sẽ được in ra:
Kết luận
Qua bài này, chúng ta đã nắm được ý tưởng và cách tạo ra vùng nhớ memory mapping cho tiến trình. Kỹ thuật memory mapping với 2 chế độ file mapping và anonymous mapping rất hữu ích trong lập trình hệ thống Linux mà chúng ta sẽ tìm hiểu cụ thể trong bài sau.