Thông tin bộ nhớ vật lý linux

Linux cung cấp cho các tiến trình hệ thống quản lý bộ nhớ ảo, nơi mỗi địa chỉ nhớ ảo có khả năng được ánh xạ tới một địa chỉ vật lý. Với độ dài 32 bit, toàn bộ không gian địa chỉ mỗi tiến trình có khả năng truy nhập là 2^32 ~ 4 Gigabit. Linux chia không gian địa chỉ này thành các trang nhớ [page] có độ dài bằng nhau [4096 bytes], mỗi khi tiến trình yêu cầu một vùng nhớ, cả trang nhớ tương ứng [chứa vùng nhớ] sẽ được cấp cho tiến trình. Bộ nhớ vật lý hệ thống chính là lượng RAM có trong hệ thống, Linux cũng chia bộ nhớ vật lý này thành các trang bằng nhau, gọi là các page frame, mỗi page frame được đánh số thứ tự gọi là các page frame number.

Các địa chỉ ảo có thể sẽ được ánh xạ thành địa chỉ vật lý dựa vào các phần cứng gọi là các MMU [Memory Management Unit] theo một phương pháp gọi là lazy allocation. Theo phương pháp này, mỗi khi một vùng nhớ ảo được cấp phát cho tiến trình, nhân hệ điều hành sẽ chưa ánh xạ nó tới địa chỉ vật lý, vùng này được gọi là vùng unmapped, khi vùng nhớ thực sự được sử dụng để lưu trữ dữ liệu, MMU của hệ thống phát hiện ra vùng nhớ ảo chưa mapped tới một vùng nhớ vật lý nào, MMU sẽ tạo ra một sự kiện gọi là page fault, và raise một ngắt tới CPU, nhân Linux sẽ xử lý sự kiện này và ra lệnh cho khối TLB [Translation Lookaside Buffer] bên trong MMU tiến hành ánh xạ trang nhớ ảo đó với một địa chỉ vật lý, sau đó quyền điều khiển sẽ được trả lại cho tiến trình và quá trình trên sẽ là trong suốt đối với tiến trình. Để hiểu rõ phương pháp này chúng ta xét đoạn mã sau:

char *buffer = malloc[10000];

char *result;

if[0 == func1[]]

    result = func2[];

else

    result = func3[];

memcpy[buffer, result, 10000];

func4[buffer];

free[buffer]

Tại thời điểm đầu tiên, khi tiến trình xin cấp phát 10000 bytes bộ nhớ tương đương với 10000/4096 ~ 3 trang nhớ ảo, hệ điều hành sẽ cấp phát cho tiến trình 3 trang nhớ ảo, 3 trang nhớ này có địa chỉ đầu lưu ở buffer chưa thực sự được ánh xạ tới một vùng nhớ vật lý nào, sau đó tiến trình tiếp tục xử lý nhiệm vụ của nó tới khi nó gọi hàm memcpy, tại đây nó cần ghi dữ liệu tới vùng nhớ nó đã xin cấp phát, khi đó MMU của hệ thống sẽ phát hiện ra vùng nhớ buffer chưa có một địa chỉ vật lý, nó sẽ tạo ra một sự kiện page fault tới CPU, nhân Linux xử lý sự kiện sẽ dừng tiến trình, lưu context của tiến trình lại và xử lý ngắt, hàm phục vụ ngắt tìm kiếm ba page frame [vật lý] chưa được sử dụng và tạo ra ánh xạ giữa ba trang nhớ của tiến trình với ba page frame

Sau khi ba trang nhớ ảo đã được ánh xạ tới địa chỉ vật lý, nhân hệ điều hành sẽ khôi phục lại tiến trình, tiến trình tiếp tục copy dữ liệu vào vùng nhớ ảo và thực hiện tác vụ của mình, tiến trình hoàn toàn không biết những gì đã xảy ra ở nhân

Chúng ta thấy rằng phương pháp lazy allocation sẽ cải thiện hiệu năng cấp phát bộ nhớ, khi mà các tiến trình yêu cầu cấp phát, nó sẽ được trả về các địa chỉ ảo rất nhanh mà chưa quan tâm tới các địa chỉ vật lý, phương pháp này đồng thời cũng sẽ cải thiện hiệu năng sử dụng bộ nhớ, khi các vùng nhớ được cấp phát nhưng không được sử dụng sẽ không bao giờ được ánh xạ tới bộ nhớ vật lý. Đồng thời chúng ta cũng thấy các địa chỉ ảo liên tục không duy trì một địa chỉ vật lý liên tục, việc lựa chọn page frame nào phù hợp để ánh xạ tới bộ nhớ ảo hoàn toàn phụ thuộc vào trạng thái hệ thống.

Bộ nhớ ảo của nhân và tiến trình

Trong mã nguồn của nhân, nhân cũng sử dụng địa chỉ ảo. Tuy nhiên các vùng địa chỉ được phân tách hoàn toàn với tiến trình và tiến trình không thể truy xuất trực tiếp tới các vùng nhớ thuộc về nhân. Ví dụ trong hệ thống 32bit, có 4G RAM, nhân hệ điều hành sẽ sử dụng 1G địa chỉ cao và phần còn lại thuộc về tiến trình.

Vùng nhớ ảo thuộc về nhân, nhân hệ điều hành chia làm hai loại bộ nhớ, bộ nhớ ảo [kernel virtual address] và bộ nhớ logic [kernel logical address].Bộ nhớ logic thực chất là bộ nhớ ảo với một vài khác biệt. Khác biệt lớn nhất là bộ nhớ logic sẽ có địa chỉ vật lý liên tục trong khi bộ nhớ ảo thì không.

Vì có một địa chỉ vật lý liên tục cho nên địa chỉ logic có một số tính chất mà bộ nhớ ảo không có, như offset giữa bộ nhớ logic và bộ nhớ vật lý luôn là một hằng số, do đó việc chuyển đổi giữa bộ nhớ này trở nên dễ dàng. Ví dụ địa chỉ logic là 0xc00000000 ứng với địa chỉ vật lý là 0x00000000 thì 0xc00000001 sẽ ứng với 0x00000001 …

Địa chỉ logic được thiết kế để phù hợp với các hoạt động DMA, khi mà các hoạt động này sẽ sử dụng đến địa chỉ vật lý, giả sử DMA sẽ copy 10000 byte từ ngoại vi vào bộ nhớ tại địa chỉ của page frame number 2, như thế sẽ cần tới 3 page frame và pfn#2, pfn#3, pfn#4  sẽ có dữ liệu sau khi DMA kết thúc, để đọc dữ liệu này CPU sẽ truy vấn tới địa chỉ logic đã được chuyển từ địa chỉ vật lý của pfn#2, như thế dữ liệu sẽ nằm liên tục trong địa chỉ logic và được đọc dễ dàng. Điều này dường như là không khả thi đối với địa chỉ ảo, khi mà các địa chỉ ảo không được ánh xạ liên tục tới địa chỉ vật lý.

Nhân hệ điều hành cung cấp cho chúng ta hàm kmalloc và vmalloc để cấp phát bộ nhớ trong vùng nhớ logical và virtual tương ứng.

Không gian địa chỉ người dùng

Đối với vùng nhớ thuộc về không gian người dùng [user virtual memory], vùng nhớ này được cấp phát tới tiến trình, và tất cả đều là địa chỉ ảo và được ánh xạ không liên tục tới địa chỉ vật lý. Khi một tiến trình được bắt đầu, bộ nhớ của nó được tổ chức như sau

Vùng text segment chứa mã thực thi của tiến trình, vùng data segment chứa biến tĩnh [toàn cục] được khởi tạo, vùng bss segment chứa biến tĩnh [toàn cục] không được khởi tạo, vùng stack chứa tham số cho hàm và biến cục bộ, vùng heap dùng để cấp phát bộ nhớ động, các khoảng trắng giữa các vùng của tiến trình và biên giới bộ nhớ ảo được tạo do lý do security.

Khoảng trắng nằm giữa vùng stack và heap được gọi là vùng Unmapped region, nó hoàn toàn đồng nghĩa với vùng Unmapped region trong bài Quản lý bộ nhớ động. Theo lý thuyết của phương pháp lazy allocation vùng này chưa được ánh xạ tới bất kỳ địa chỉ vật lý nào. Vùng này không chỉ được sử dụng cho mục đích mở rộng stack và heap. Nó còn được dùng cho một kỹ thuật đặc biệt, được gọi là Memory Mapping

Kỹ thuật Memory Mapping

Memory Mapping là một phương pháp hết sức đặc sắc và hữu dụng của các hệ thống Unix/Linux hiện đại. Memory mapping cho phép một tiến trình truy nhập một vùng nhớ thuộc về nhân hệ điều hành hoặc truy nhập không gian địa chỉ của ngoại vi[Input/Output] từ đó chiếm quyền điều khiển các thiết bị này từ hệ điều hành. Với phương pháp này, hiệu năng truyền thông giữa không gian nhân [kernel space] và không gian tiến trình [user space/process space] được cải thiện đáng kể nhờ tránh được việc copy dữ liệu không cần thiết giữa các không gian này.

Cấu trúc hàm của mmap và munmap

       void *mmap[void *addr, size_t length, int prot, int flags, int fd, off_t offset];

       int munmap[void *addr, size_t length];

Các tham số của hàm mmap 

Trường addr là địa chỉ người dùng muốn nơi mapping được bắt đầu, địa chỉ phải nằm trong vùng Unmapped region và trỏ tới bắt đầu của trang nhớ ảo, thông thường người dùng sẽ không biết vùng địa chỉ nào thuộc về Unmapped region hay vị trí nào là đầu trang vì các thông tin này đều do hệ điều hành quản lý, do đó, người dùng sẽ truyền vào NULL để nhân Linux tự quyết định.Tuy nhiên nếu người dùng trước đó đã gọi mmap, nhận về một địa chỉ và đã dùng munmap để unmap nó, bây giờ người sử dụng muốn tái sử dụng địa chỉ đó, thì có thể sử dụng đối addr cho mục đích tái sử dụng    

Trường length mô tả độ dài của mapping, nhân hệ điều hành sẽ tự động roundup trường này tới bội của độ dài một trang [4KB]. Ví dụ như length là 5000 byte thì kernel sẽ roundup nó tới 8192 byte =  2 pages

Trường prot mô tả như quyền truy xuất của mapping, các quyền có thể là đọc ghi và thực thi, các quyền này tương ứng với quyền đối với file được mở cho mapping, thông thường khi chạy binary có mmap, chúng ta chạy ở quyền root [cao nhất]

Trường flags mô tả rất nhiều tùy chọn trong đó hai tùy chọn quan trọng nhất là

MAP_SHARED

            Sử dụng trong các ứng dụng chia sẻ thông tin giữa hai tiến trình hoặc chia sẻ thông tin giữa tiến trình và nhân, khi sử dụng cờ này, giả sử có hai tiến trình đều mapping vào một vùng nhớ mỗi khi một bên ghi nội dung vào vùng đó, bên còn lại có thể đọc nội dung đó

MAP_PRIVATE

            Sử dụng để bảo vệ nội dụng của một bên trong trường hợp có hai bên [hai tiến trình hoặc tiến trình và nhân] cùng mapping vào một vùng nhớ [hai tiến trình là cha-con], khi sử dụng cờ này, nếu một bên ghi nội dung, nhân Linux sẽ sử dụng kỹ thuật copy-on-write copy nội dung mới vào một vùng nhớ vật lý khác, như thế bên còn lại sẽ không thể đọc được nội dung mới được ghi

Trường offset mô tả vị trí trong file mà người dùng muốn mapping nội dung lên, trường này được sử dụng trong các ứng dụng của mmap liên quan tới trải nội dung của file lên một vùng nhớ ảo

Hàm mmap sẽ trả về con trỏ trỏ tới địa chỉ được mapped trong trường hợp thành công, nếu lỗi [void *]-1 sẽ được trả về. Đồng thời errno được thiết lập tương ứng để tìm ra nguyên nhân cụ thể gây lỗi

Các tham số của hàm munmap 

Trường addr chính là địa chỉ muốn thực hiện unmap, trường này chính là con trỏ trả ra của mmap trong trường hợp thành công

Trường length mô tả độ dài muốn unmap.

Hàm munmap trả về 0 nếu thành công, trả về -1 nếu lỗi.

Truyền thông giữa userspace và kernel space sử dụng mmap

Với cách thức quản lý bộ nhớ của hệ điều hành Linux như đã trình bày ở trên, chúng ta sẽ tạo ra một ứng dụng cho phép chia sẻ nội dung giữa tiến trình và nhân Linux, nguyên lý căn bản của ứng dụng sẽ là cho phép một tiến trình tạo ra một mapping mà mapping này có cùng địa chỉ vật lý với vùng địa chỉ logical và virtual trong nhân Linux. Mô hình như sau

Vì mapping trong không gian địa chỉ người dùng có cùng địa chỉ vật lý với các vùng nhớ trong không gian địa chỉ trong nhân, do đó nếu nhân Linux đọc/ghi dữ liệu tới các vùng nhớ được sử dụng để mapping của nó, các nội dung này sẽ đều có thể đọc/ghi được ở tiến trình và ngược lại

Thực hiện bằng mã nguồn

Chúng ta cần viết một loadable module để thực hiện việc mapping ở nhân Linux, loadable module sẽ tạo ra một character driver, ứng dụng ở chạy ở không gian địa chỉ người dùng có thể giao tiếp với loadable module thông qua character driver này

Luồng xử lý ở application đơn giản như sau

Ở lớp ứng dụng chỉ đơn giản là mở character driver file, đây là một file đặc biệt được phục vụ bởi các phương thức đăng ký ở loadable module

Loadable module khi khởi tạo sẽ vừa đăng ký các phương thức cho character driver vừa cấp phát vùng nhớ trong cả hai vùng logical và virtual để tiến hành mapping

Character driver file được khai báo như sau 


    static dev_t mmap_dev; 


    static struct cdev mmap_cdev; 


    static int mmap_open[struct inode *inode, struct file *filp]; 


    static int mmap_release[struct inode *inode, struct file *filp]; 


    static int mmap_mmap[struct file *filp, struct vm_area_struct *vma];      


    static struct file_operations mmap_fops = { 


            .open = mmap_open, 


            .release = mmap_release, 


            .mmap = mmap_mmap, 


            .owner = THIS_MODULE, 

    }; 

    static int mmap_open[struct inode *inode, struct file *filp] 

    { 

            return 0; 

    } 

    static int mmap_release[struct inode *inode, struct file *filp] 

    { 

            return 0; 

    }

Character driver hiện tại có ba phương thức, hai phương thức open và release thực chất không chúng ta cần làm gì vì nhân Linux đã thực hiện mọi thứ cần thiết cho chúng ta, chúng ta chỉ cần implement phương thức thứ ba là mmap.


Các con trỏ global trỏ đến các vùng nhớ logical và virtual cũng được khai báo, số lượng tối đa cho phép tiến hành mapping là 16 trang nhớ


    #define NPAGES 16 


    static int *vmalloc_area;  


    static int *kmalloc_area; 


    static void *kmalloc_ptr;



Khởi tạo loadable module, các vùng nhớ này sẽ được cấp phát và roundup tới đầu trang nhớ ảo, nội dung của chúng cũng được điền trước, chúng ta sẽ đọc thông tin này ở application và có thể chỉnh sửa chúng tùy ý


    static int __init mmap_init_kernel[void] 

    { 

            int ret = 0; 

            int i; 

            unsigned long pfn;

            char *vmalloc_area_ptr = NULL;

            if [[kmalloc_ptr = kmalloc[[NPAGES + 2] * PAGE_SIZE, GFP_KERNEL]] == NULL] { 

                    ret = -ENOMEM; 

                    goto out; 

            }

            kmalloc_area = [int *][[[[unsigned long]kmalloc_ptr] + PAGE_SIZE - 1] & PAGE_MASK];  

            if [[vmalloc_area = [int *]vmalloc[NPAGES * PAGE_SIZE]] == NULL] { 

                    ret = -ENOMEM; 

                    goto out_kfree; 

            }                       

            if [[ret = alloc_chrdev_region[&mmap_dev, 0, 1, "mmap"]] < 0] { 

                    printk[KERN_ERR "could not allocate major number for mmap\n"]; 

                    goto out_vfree; 

            } 

            cdev_init[&mmap_cdev, &mmap_fops]; 

            if [[ret = cdev_add[&mmap_cdev, mmap_dev, 1]] < 0] { 

                    printk[KERN_ERR "could not allocate chrdev for mmap\n"]; 

                    goto out_unalloc_region; 

            } 

            for [i = 0; i < NPAGES * PAGE_SIZE; i+= PAGE_SIZE] { 

                    SetPageReserved[vmalloc_to_page[[void *][[[unsigned long]vmalloc_area] + i]]]; 

                    SetPageReserved[virt_to_page[[[unsigned long]kmalloc_area] + i]]; 

            } 

            for [i = 0; i < [NPAGES * PAGE_SIZE / sizeof[int]]; i += 2] { 

                    vmalloc_area[i] = [0xeeff vm_end - vma->vm_start; 

            unsigned long start = vma->vm_start; 

            char *vmalloc_area_ptr = [char *]vmalloc_area; 

            unsigned long pfn; 

            if [length > NPAGES * PAGE_SIZE] 

                    return -EIO; 

            while [length > 0] { 

                    pfn = vmalloc_to_pfn[vmalloc_area_ptr]; 

                    if [[ret = remap_pfn_range[vma, start, pfn, PAGE_SIZE, 

                                               PAGE_SHARED]] < 0] { 

                            return ret; 

                    } 

                    start += PAGE_SIZE; 

                    vmalloc_area_ptr += PAGE_SIZE; 

                    length -= PAGE_SIZE; 

            } 

            return 0; 

    }  

Với địa chỉ logical chúng ta có công thức quan hệ giữa địa chỉ vật lý và pfn


physical_address = PFN * page_size + offset

Trong đó page_size là 4096, và bởi vì là đầu trang thì offset là 0, do đó ta có thể tính ngược lại PFN thông qua địa chỉ vật lý, Linux cung cấp cho chúng ta hàm chuyển đồi từ địa chỉ logic về địa chỉ vật lý, chúng ta cần dịch địa chỉ này đi 12 bit [chia cho 4096] là sẽ có được pfn.

    int mmap_kmem[struct file *filp, struct vm_area_struct *vma] 

    { 

            int ret, i; 

            long length = vma->vm_end - vma->vm_start; 

            unsigned long pfn;

            if [length > NPAGES * PAGE_SIZE] 

                    return -EIO;

            if [[ret = remap_pfn_range[vma, 

                                       vma->vm_start, 

                                       virt_to_phys[[void *]kmalloc_area] >> PAGE_SHIFT, 

                                       length, 

                                       vma->vm_page_prot]] < 0] { 

                    return ret; 

            } 

             

            return 0; 

    }  

Nhớ rằng địa chỉ logical sẽ có các trang nhớ vật lý liên tục, do đó chúng ta có thể map tất cả các trang chỉ trong một lần gọi

Mã nguồn chương trình ở userpace như sau

    #include  

    #include  

    #include  

    #include  

    #include  

    #include  

    #include  

     

    #define NPAGES 16 

     

    /* this is a test program that opens the mmap driver.

       It reads out values of the kmalloc[] and vmalloc[]

       allocated areas and checks for correctness.

       You need a device special file to access the driver.

       The device special file is called 'node' and searched

       in the current directory.

       To create it

       - load the driver

         'insmod mmap_kernel.ko'

       - find the major number assigned to the driver

         'grep mmap /proc/devices'

       - and create the special file [assuming major number 247]

         'mknod /dev/mmap c 247 0'

    */ 

     

    int main[void] 

    { 

      int fd; 

      unsigned int *vadr, *vadr1; 

      unsigned int *kadr, *kadr1; 


      void *mmap_addr = malloc[NPAGES*4096]; 

     

      int len = NPAGES * getpagesize[]; 

     

      if [[fd=open["/dev/mmap", O_RDWR|O_SYNC]]

Chủ Đề