Memory Mapping (continue)
Bài học trước đã giới thiệu cơ bản về kỹ thuật memory mapping cũng như cách sử dụng system call mmap() để tạo ra 1 vùng nhớ memory mapping. Ngoài ra, bài trước cũng đã đề cập về các loại memory mapping với chế độ private và shared tương ứng. Trong bài này, chúng ta sẽ đi sâu vào bản chất, các bước tạo ra memory mapping cũng như công dụng của các kỹ thuật mapping này.
File mapping
Để tạo ra một vùng nhớ file mapping, chúng ta cần thực hiện 2 bước sau:
-
Lấy giá trị fd của file cần map, thường dùng system call open()
-
Truyền fd vào đối số của system call mmap()
Sau 2 bước này, mmap() sẽ ánh xạ nội dung của file vào địa chỉ bộ nhớ ảo của tiến trình gọi. Lúc này, chúng ta có thể đóng file lại mà không ảnh hưởng đến vùng nhớ mapping. Đối số offset trong mmap() giúp chúng ta xác định được byte đầu tiên của vùng nhớ trong file được map. Nếu offset được truyền giá trị 0, nghĩa là mapping từ đầu file. Đối số length chỉ ra số byte được map. Sử dụng offset và length sẽ giúp chúng ta map đúng vùng nhớ trong file.
Ví dụ, 1 file có nội dung là “Hello I am vimentor”, Nếu muốn map vùng nhớ có nội dung là từ “Hello”, ta truyền offset là 0, length là 5; nếu muốn map vùng nhớ có nội dung “Vimentor” thì ta truyền offset là 11 và length là 8.
Để hình dung dễ hơn về file mapping, chúng ta xem lại hình dưới đây:
Hình 1: File memory mapping
Private file mapping
Nếu đối số “flags” của system call mmap() được truyền vào cờ MAP_PRIVATE, vùng nhớ mapping được thiết lập chế độ private. Sau khi tạo ra private file mapping, nội dung của vùng nhớ mapping được khởi tạo từ vùng nhớ tương ứng trong file. Nhiều tiến trình có thể cùng map đến 1 file, lúc này các tiến trình sẽ cùng map đến địa chỉ vật lý của file, nhưng kỹ thuật copy-on-write sẽ được áp dụng. Nghĩa là nếu 1 tiến trình thay đổi vùng nhớ map đến file, kernel sẽ tạo ra vùng nhớ riêng biệt cho tiến trình đó, và sự thay đổi này sẽ không làm thay đổi nội dung của file trong bộ nhớ vật lý. Do đó, các tiến trình khác không nhìn thấy và không chịu sự thay đổi này.
Private file mapping được ứng dụng chủ yếu để khởi tạo 1 vùng nhớ của tiến trình với nội dung có sẵn của 1 file. Ví dụ, có thể khởi tạo text segment hoặc initialized data segment của 1 tiến trình từ 1 file thực thi được hoặc 1 file thư viện.
Bây giờ, hãy xét 1 ví dụ để giải thích rõ hơn về tính chất của private file mapping. Đầu tiên, bạn tạo ra 1 file hello.txt có nội dung trong đường dẫn chứa code (trong ví dụ này, file hello.txt có nội dung là “Hello vimentor guys”). Chương trình dưới đây thực hiện tạo ra 1 vùng nhớ memory mapping đến file hello.txt với cờ MAP_PRIVATE. Sau đó thử ghi đè 1 nội dung khác vào địa chỉ memory mapping này, hãy cùng xem kết quả nhé.
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(void)
{
char *addr;
int fd;
struct stat sb;
char str[16] = "Overwrite file";
fd = open("hello.txt", O_RDWR);
if (-1 == fd)
{
printf("Open file error\n");
}
/*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 error\n");
}
/*Create memory mapping to fd with flag MAP_PRIVATE*/
addr = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (MAP_FAILED == addr)
{
printf("mmap error\n");
}
if (write(STDOUT_FILENO, addr, sb.st_size) != sb.st_size)
{
printf("write to stdout error\n");
}
/*Overwrite content of string “str” to addr*/
memset(addr, 0x0, sizeof(addr));
sprintf(addr, "%s", str);
printf("Mapped memory content: %s\n", addr);
exit(EXIT_SUCCESS);
}
Sau khi complie và chạy chương trình, ta được kết quả sau:
Bây giờ mở file hello.txt ra, ta sẽ thấy nội dung của file không bị thay đổi khi mmap() sử dụng cờ MAP_PRIVATE.
Shared file mapping
Khi nhiều tiến trình tạo ra vùng nhớ mapping với cờ MAP_SHARED từ 1 file, chúng dùng chung địa chỉ vật lý của RAM. Nếu 1 tiến trình thay đổi vùng nhớ này, nó sẽ được nhìn thấy bởi các tiến trình còn lại, và nội dung file cũng bị thay đổi. Để hiểu rõ hơn về bản chất của shared file mapping, chúng ta xem hình dưới đây:
Hình 2: Shared file memory mapping
Shared file mapping được ứng dụng trong 2 trường hợp:
-
Memory-mapped I/O
Nội dung của một file được tải vào 1 vùng nhớ trong bộ nhớ ảo của tiến trình; và chỉ cần thay đổi nội dung vùng nhớ thì sẽ tự động thay đổi được nội dung của file. Vì vậy, kỹ thuật này tạo ra cơ chế tương đương thay thế cho file I/O với các system call read() và write() mà chúng ta đã học trong bài file I/O.
Ngoài ra, trong 1 số trường hợp, kỹ thuật memory-mapped I/O tỏ ra tối ưu về hiệu năng hơn là các hàm file I/O thông thường vì các nguyên nhân sau:
-
Một lần gọi read() hoặc write(), kernel phải thực hiện thêm 2 bước copy phụ: giữa RAM và bộ nhớ đệm của kernel và giữa bộ nhớ đệm kernel và bộ nhớ đệm tầng user. Sử dụng mmap() sẽ loại bỏ được bước copy thứ 2. Khi đọc, dữ liệu được copy thẳng từ RAM đến tầng user của tiến trình; còn khi ghi, dữ liệu từ tầng user sẽ copy thẳng đến địa chỉ của file trên RAM.
-
Khi dùng read() hoặc write(), dữ liệu sẽ được lưu trữ tại 2 nơi: bộ nhớ đệm kernel và bộ nhớ đệm tầng user. Nhưng khi sử dụng mmap(), chỉ có 1 vùng nhớ đệm dùng chung giữa kernel và user, qua đó giúp tiết kiệm bộ nhớ. Ưu điểm hiệu năng này sẽ càng thấy rõ nếu nhiều tiến trình cùng truy cập vào các vùng nhớ rời rạc nhau trên RAM.
-
Giao tiếp liên tiến trình (inter process communication - IPC)
Các tiến trình khác nhau có thể liên lạc với nhau thông qua các biến trong vùng nhớ chia sẻ mapping đến cùng 1 file. Tính năng này hiệu quả khi vùng nhớ không những được chia sẻ giữa các tiến trình mà còn được giữ trong file khi tiến trình kết thúc hoặc hệ thống khởi động lại.
Bây giờ lại quay trở lại ví dụ trên để thấy được sự khác nhau giữa private file mapping và shared file mapping. Chúng ta sử dụng lại source code và file hello.txt của chương trình trên, trừ việc sử dụng cờ MAP_SHARED thay vì MAP_PRIVATE để tạo ra vùng nhớ shared memory mapping như sau:
addr = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
Sau đó, hãy compile và chạy chương trình. Chúng ta sẽ thấy log do chương trình xuất ra giống hệt trường hợp của private file mapping, nhưng có sự khác biệt là nội dung của file hello.txt đã được tự thay đổi theo nội dung của vùng nhớ địa chỉ “addr”.
Anonymous mapping
Một anonymous mapping sẽ tạo ra 1 vùng nhớ nhưng không ánh xạ đến 1 file cụ thể nào. Trong Linux, ta dùng 1 trong 2 cách sau đây để tạo ra vùng nhớ anonymous mapping:
-
Sử dụng cờ MAP_ANONYMOUS và fd = -1 trong mmap(). Thực ra khi dùng cơ MAP_ANONYMOUS, đối số fd sẽ bị bỏ qua, nhưng 1 vài phiên bản UNIX vẫn yêu cầu set fd = -1 đi kèm với cờ MAP_ANONYMOUS.
-
Mở device file /dev/zero và truyền mô tả file fd này vào mmap()
Các byte trong vùng anonymous mapping sau khi được tạo ra sẽ được khởi tạo giá trị 0. Với cả 2 cách trên, đối số “offset” sẽ bị bỏ qua do không ánh xạ đến 1 file cụ thể nào.
Tương tự như file mapping, chúng ta có thể dùng 2 cờ MAP_PRIVATE và MAP_SHARED trong system call mmap() để tạo ra private anonymous mapping và shared anonymous mapping. Cả private và shared mapping đều tạo ra vùng nhớ mapping trên bộ nhớ ảo của tiến trình gọi, chỉ khác nhau về cơ chế xử lý ghi vào vùng nhớ. Private anonymous mapping sử dụng cơ chế copy-on-write, nghĩa là nếu 1 tiến trình khác ghi vào các trang của vùng nhớ này, kernel sẽ tạo ra các trang mới cho riêng tiến trình đó. Chúng ta đã thấy được kỹ thuật mapping này trong bài học về tiến trình khi tiến trình cha tạo ra 1 tiến trình con bằng system call fork(). Còn với shared anonymous mapping, các tiến trình khi ghi vào vùng nhớ này sẽ được nhìn thấy bởi các tiến trình khác.
Hãy cùng xét 1 ví dụ để hiểu rõ hơn về anonymous mapping. Chương trình dưới đây dùng cờ MAP_SHARED để tạo ra 1 vùng nhớ shared anonymous mapping có địa chỉ bắt đầu là “addr”trước khi dùng system call fork() để tạo ra 1 tiến trình con. Lúc này, tiến trình con sẽ dùng chung vùng nhớ mapping với tiến trình cha, do đó khi tiến trình con thay đổi giá trị của địa chỉ addr, tiến trình cha cũng sẽ được cập nhật.
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int *addr; /* Địa chỉ trỏ đến shared memory mapping */
/*Dùng mmap() với cờ MAP_SHARED tạo ra shared anonymous mapping*/
addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
{
printf("mmap fail\n");
}
*addr = 1; /* Khởi tạo giá trị của addr */
switch (fork())
{
case -1:
printf("fork fail\n");
case 0: /* Trong tiến trình con, tăng *addr 1 đơn vị */
printf("Child started, value = %d\n", *addr);
(*addr)++;
if (munmap(addr, sizeof(int)) == -1)
{
printf("munmap fail\n");
}
exit(EXIT_SUCCESS);
default: /* Trong tiến trình cha, chờ con kết thúc rồi truy xuất *addr */
if (wait(NULL) == -1)
{
printf("wait fail\n");
}
printf("In parent, value = %d\n", *addr);
if (munmap(addr, sizeof(int)) == -1)
{
printf("munmap fail\n");
}
exit(EXIT_SUCCESS);
}
}
Bây giờ compile và chạy chương trình, bạn sẽ thấy tiến trình cha đã cập nhật giá trị *addr sau khi tiến trình con thay đổi.
Tiếp theo, hãy cùng xem điều gì sẽ xảy ra nếu chúng ta dùng cờ MAP_PRIVATE để tạo ra private anonymous mapping với cùng chương trình trên? Bạn chỉ cần thay đổi hàm mmap() như sau:
addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Lúc này, tiến trình cha sẽ không cập nhật giá trị *addr sau khi tiến trình con thay đổi do cơ chế copy-on-write. Kết quả của chương trình sẽ như sau:
Kết luận
Qua bài này, chúng ta đã đi sâu vào 4 kỹ thuật memory mapping: private file mapping, shared file mapping, private anonymous mapping và shared anonymous mapping. Memory mapping được sử dụng rất nhiều trong Linux như cấp phát bộ nhớ private cho tiến trình, khởi tạo nội dung của text hoặc initialized segment cho tiến trình… Bài học này cũng kết thúc khóa học giới thiệu và hướng dẫn các nội dung cơ sở trong lập trình Linux, qua đó làm tiền đề cho các khóa học sau để trở thành 1 kỹ sư phần mềm hệ thống Linux.