Tại sao bộ nhớ này không thực sự ăn bộ nhớ?


150

Tôi muốn tạo một chương trình mô phỏng tình huống hết bộ nhớ (OOM) trên máy chủ Unix. Tôi đã tạo ra bộ nhớ siêu đơn giản này:

#include <stdio.h>
#include <stdlib.h>

unsigned long long memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
void *memory = NULL;

int eat_kilobyte()
{
    memory = realloc(memory, (eaten_memory * 1024) + 1024);
    if (memory == NULL)
    {
        // realloc failed here - we probably can't allocate more memory for whatever reason
        return 1;
    }
    else
    {
        eaten_memory++;
        return 0;
    }
}

int main(int argc, char **argv)
{
    printf("I will try to eat %i kb of ram\n", memory_to_eat);
    int megabyte = 0;
    while (memory_to_eat > 0)
    {
        memory_to_eat--;
        if (eat_kilobyte())
        {
            printf("Failed to allocate more memory! Stucked at %i kb :(\n", eaten_memory);
            return 200;
        }
        if (megabyte++ >= 1024)
        {
            printf("Eaten 1 MB of ram\n");
            megabyte = 0;
        }
    }
    printf("Successfully eaten requested memory!\n");
    free(memory);
    return 0;
}

Nó ăn nhiều bộ nhớ như được xác định trong memory_to_eatđó bây giờ chính xác là 50 GB RAM. Nó phân bổ bộ nhớ cho 1 MB và in chính xác điểm không thể phân bổ nhiều hơn, để tôi biết giá trị tối đa mà nó quản lý để ăn.

Vấn đề là nó hoạt động. Ngay cả trên một hệ thống có 1 GB bộ nhớ vật lý.

Khi tôi kiểm tra hàng đầu, tôi thấy rằng quá trình này chiếm 50 GB bộ nhớ ảo và chỉ dưới 1 MB bộ nhớ lưu trú. Có cách nào để tạo ra một bộ nhớ ăn mà thực sự tiêu thụ nó?

Thông số kỹ thuật hệ thống: Linux kernel 3.16 ( Debian ) rất có thể được kích hoạt vượt mức (không chắc chắn cách kiểm tra) mà không có trao đổi và ảo hóa.


16
có lẽ bạn phải thực sự sử dụng bộ nhớ này (tức là ghi vào nó)?
ms

4
Tôi không nghĩ trình biên dịch tối ưu hóa nó, nếu đó là sự thật, nó sẽ không phân bổ 50GB bộ nhớ ảo.
Petr

18
@Magisch Tôi không nghĩ đó là trình biên dịch nhưng HĐH giống như copy-on-write.
cadaniluk

4
Bạn nói đúng, tôi đã cố viết thư cho nó và tôi vừa lấy hộp ảo của mình ...
Petr

4
Chương trình ban đầu sẽ hoạt động như bạn mong đợi nếu bạn làm sysctl -w vm.overcommit_memory=2root; xem mjmwired.net/kernel/Documentation/vm/overcommit-accounting . Lưu ý rằng điều này có thể có hậu quả khác; đặc biệt, các chương trình rất lớn (ví dụ: trình duyệt web của bạn) có thể không sinh ra các chương trình trợ giúp (ví dụ: trình đọc PDF).
zwol

Câu trả lời:


221

Khi malloc()việc triển khai của bạn yêu cầu bộ nhớ từ kernel hệ thống (thông qua một cuộc gọi sbrk()hoặc mmap()hệ thống), kernel chỉ ghi chú rằng bạn đã yêu cầu bộ nhớ và vị trí của nó sẽ được đặt trong không gian địa chỉ của bạn. Nó không thực sự ánh xạ các trang đó .

Khi quá trình sau đó truy cập vào bộ nhớ trong vùng mới, phần cứng sẽ nhận ra lỗi phân đoạn và cảnh báo kernel về tình trạng này. Sau đó, kernel tìm kiếm trang trong cấu trúc dữ liệu của chính nó và thấy rằng bạn nên có một trang 0 ở đó, vì vậy nó ánh xạ trong một trang 0 (trước tiên có thể là một trang từ bộ đệm trang) và trả về từ ngắt. Quá trình của bạn không nhận ra rằng bất kỳ điều này đã xảy ra, hoạt động của hạt nhân hoàn toàn trong suốt (ngoại trừ độ trễ ngắn trong khi kernel thực hiện công việc của nó).

Tối ưu hóa này cho phép cuộc gọi hệ thống trở lại rất nhanh và quan trọng nhất là nó tránh mọi tài nguyên được cam kết cho quy trình của bạn khi ánh xạ được thực hiện. Điều này cho phép các quá trình dự trữ bộ đệm khá lớn mà chúng không bao giờ cần trong các trường hợp thông thường, mà không sợ ngấu nghiến quá nhiều bộ nhớ.


Vì vậy, nếu bạn muốn lập trình một bộ nhớ, bạn thực sự phải làm gì đó với bộ nhớ bạn phân bổ. Đối với điều này, bạn chỉ cần thêm một dòng vào mã của bạn:

int eat_kilobyte()
{
    if (memory == NULL)
        memory = malloc(1024);
    else
        memory = realloc(memory, (eaten_memory * 1024) + 1024);
    if (memory == NULL)
    {
        return 1;
    }
    else
    {
        //Force the kernel to map the containing memory page.
        ((char*)memory)[1024*eaten_memory] = 42;

        eaten_memory++;
        return 0;
    }
}

Lưu ý rằng việc ghi vào một byte trong mỗi trang là hoàn toàn đủ (chứa 4096 byte trên X86). Đó là bởi vì tất cả việc cấp phát bộ nhớ từ kernel cho một tiến trình được thực hiện ở mức độ chi tiết của trang bộ nhớ, do đó, do phần cứng không cho phép phân trang ở mức độ chi tiết nhỏ hơn.


6
Cũng có thể cam kết bộ nhớ với mmapMAP_POPULATE(mặc dù lưu ý rằng trang man nói " MAP_POPULATE chỉ được hỗ trợ cho ánh xạ riêng tư kể từ Linux 2.6,23 ").
Toby Speight

2
Điều đó về cơ bản là đúng, nhưng tôi nghĩ rằng các trang đều được sao chép trên bản đồ thành một trang không, thay vì hoàn toàn không xuất hiện trong các bảng trang. Đây là lý do tại sao bạn phải viết, không chỉ đọc, mỗi trang. Ngoài ra, một cách khác để sử dụng hết bộ nhớ vật lý là khóa các trang. vd: gọi mlockall(MCL_FUTURE). (Điều này yêu cầu root, vì ulimit -lchỉ có 64kiB cho tài khoản người dùng trên bản cài đặt mặc định của Debian / Ubuntu.) Tôi vừa thử nó trên Linux 3.19 với sysctl mặc định vm/overcommit_memory = 0và các trang bị khóa sử dụng hết RAM / RAM vật lý.
Peter Cordes

2
@cad Mặc dù X86-64 hỗ trợ hai kích thước trang lớn hơn (2 MiB và 1 GiB), chúng vẫn được xử lý khá đặc biệt bởi kernel linux. Chẳng hạn, chúng chỉ được sử dụng theo yêu cầu rõ ràng và chỉ khi hệ thống đã được cấu hình để cho phép chúng. Ngoài ra, trang 4 kiB vẫn giữ mức độ chi tiết mà tại đó bộ nhớ có thể được ánh xạ. Đó là lý do tại sao tôi không nghĩ rằng việc đề cập đến các trang lớn sẽ thêm bất cứ điều gì vào câu trả lời.
cmaster - phục hồi monica

1
@AlecTeal Vâng, đúng vậy. Đó là lý do tại sao, ít nhất là trên linux, nhiều khả năng một quá trình tiêu tốn quá nhiều bộ nhớ đã bị kẻ giết người hết bộ nhớ bắn ra so với một trong những điều đó malloc()gọi là trả về null. Đó rõ ràng là nhược điểm của phương pháp này trong quản lý bộ nhớ. Tuy nhiên, nó đã tồn tại các bản đồ sao chép trên ghi (nghĩ rằng các thư viện động và fork()) khiến hạt nhân không thể biết cần bao nhiêu bộ nhớ thực sự. Vì vậy, nếu nó không quá bộ nhớ, bạn sẽ hết bộ nhớ có thể lập bản đồ từ lâu trước khi bạn thực sự sử dụng tất cả bộ nhớ vật lý.
cmaster - phục hồi monica

2
@BillBarth Đối với phần cứng, không có sự khác biệt giữa những gì bạn sẽ gọi là lỗi trang và segfault. Phần cứng chỉ nhìn thấy một quyền truy cập vi phạm các hạn chế truy cập được đặt trong các bảng trang và báo hiệu điều kiện đến kernel thông qua lỗi phân đoạn. Đó chỉ là phía phần mềm quyết định xem có nên xử lý lỗi phân đoạn hay không bằng cách cung cấp một trang (cập nhật các bảng trang) hoặc liệu SIGSEGVtín hiệu có được gửi đến quy trình hay không.
cmaster - phục hồi monica

28

Tất cả các trang ảo bắt đầu sao chép trên bản ghi vào cùng một trang vật lý bằng không. Để sử dụng hết các trang vật lý, bạn có thể làm bẩn chúng bằng cách viết một cái gì đó lên từng trang ảo.

Nếu chạy bằng root, bạn có thể sử dụng mlock(2)hoặc mlockall(2)để nhân kernel nối các trang khi chúng được phân bổ mà không phải làm bẩn chúng. (người dùng không root bình thường ulimit -lchỉ có 64kiB.)

Như nhiều người khác đề xuất, có vẻ như nhân Linux không thực sự phân bổ bộ nhớ trừ khi bạn ghi vào nó

Một phiên bản cải tiến của mã, thực hiện những gì OP muốn:

Điều này cũng sửa lỗi chuỗi định dạng printf không khớp với các loại memory_to_eat và eaten_memory, sử dụng %ziđể in size_tcác số nguyên. Kích thước bộ nhớ để ăn, trong kiB, tùy chọn có thể được chỉ định là một dòng lệnh arg.

Thiết kế lộn xộn sử dụng các biến toàn cục và tăng 1k thay vì 4k trang là không thay đổi.

#include <stdio.h>
#include <stdlib.h>

size_t memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
char *memory = NULL;

void write_kilobyte(char *pointer, size_t offset)
{
    int size = 0;
    while (size < 1024)
    {   // writing one byte per page is enough, this is overkill
        pointer[offset + (size_t) size++] = 1;
    }
}

int eat_kilobyte()
{
    if (memory == NULL)
    {
        memory = malloc(1024);
    } else
    {
        memory = realloc(memory, (eaten_memory * 1024) + 1024);
    }
    if (memory == NULL)
    {
        return 1;
    }
    else
    {
        write_kilobyte(memory, eaten_memory * 1024);
        eaten_memory++;
        return 0;
    }
}

int main(int argc, char **argv)
{
    if (argc >= 2)
        memory_to_eat = atoll(argv[1]);

    printf("I will try to eat %zi kb of ram\n", memory_to_eat);
    int megabyte = 0;
    int megabytes = 0;
    while (memory_to_eat-- > 0)
    {
        if (eat_kilobyte())
        {
            printf("Failed to allocate more memory at %zi kb :(\n", eaten_memory);
            return 200;
        }
        if (megabyte++ >= 1024)
        {
            megabytes++;
            printf("Eaten %i  MB of ram\n", megabytes);
            megabyte = 0;
        }
    }
    printf("Successfully eaten requested memory!\n");
    free(memory);
    return 0;
}

Có bạn đúng, đó là lý do, không chắc chắn về nền tảng kỹ thuật, nhưng nó có ý nghĩa. Mặc dù điều đó thật kỳ lạ, nó cho phép tôi phân bổ nhiều bộ nhớ hơn mức tôi có thể sử dụng.
Petr

Tôi nghĩ ở cấp độ hệ điều hành, bộ nhớ chỉ thực sự được sử dụng khi bạn ghi vào nó, điều này hợp lý khi xem xét hệ điều hành không giữ các tab trên tất cả bộ nhớ mà bạn theo lý thuyết có, nhưng chỉ trên những gì bạn thực sự sử dụng.
Magisch

@Petr mind Nếu tôi đánh dấu câu trả lời của mình là wiki cộng đồng và bạn chỉnh sửa mã của mình để người dùng có thể đọc được trong tương lai?
Magisch

@Petr Không có gì lạ cả. Đó là cách quản lý bộ nhớ trên các hệ điều hành ngày nay. Một đặc điểm chính của các quy trình là chúng có các không gian địa chỉ riêng biệt, được thực hiện bằng cách cung cấp cho mỗi chúng một không gian địa chỉ ảo. x86-64 hỗ trợ 48 bit cho một địa chỉ ảo, thậm chí với các trang 1GB, vì vậy, theo lý thuyết, một số Terabyte bộ nhớ cho mỗi quá trình là có thể. Andrew Tanenbaum đã viết một số cuốn sách tuyệt vời về các hệ điều hành. Nếu bạn quan tâm, hãy đọc chúng!
cadaniluk

1
Tôi sẽ không sử dụng từ ngữ "rò rỉ bộ nhớ rõ ràng" Tôi không tin rằng quá mức hoặc công nghệ "sao chép bộ nhớ trên ghi" này đã được phát minh để xử lý rò rỉ bộ nhớ.
Petr

13

Một tối ưu hóa hợp lý đang được thực hiện ở đây. Thời gian chạy không thực sự có được bộ nhớ cho đến khi bạn sử dụng nó.

Một đơn giản memcpysẽ đủ để phá vỡ sự tối ưu hóa này. (Bạn có thể thấy rằng callocvẫn tối ưu hóa việc cấp phát bộ nhớ cho đến thời điểm sử dụng.)


2
Bạn có chắc không? Tôi nghĩ rằng nếu số lượng phân bổ của anh ta đạt đến mức tối đa của bộ nhớ ảo thì malloc sẽ thất bại, không có vấn đề gì. Làm thế nào để malloc () biết rằng không ai sẽ sử dụng bộ nhớ ?? Không thể, vì vậy nó phải gọi sbrk () hoặc bất cứ thứ gì tương đương trong HĐH của anh ta.
Peter - Phục hồi Monica

1
Tôi khá chắc chắn. (malloc không biết nhưng thời gian chạy chắc chắn sẽ). Việc kiểm tra thật đơn giản (mặc dù bây giờ không dễ dàng với tôi: Tôi đang ở trên tàu).
Bathsheba

@Bathsheba Việc viết một byte cho mỗi trang cũng đủ? Giả sử mallocphân bổ trên ranh giới trang những gì dường như rất có thể với tôi.
cadaniluk

2
@doron không có trình biên dịch liên quan ở đây. Đó là hành vi nhân Linux.
el.pescado

1
Tôi nghĩ rằng glibc calloctận dụng mmap (MAP_ANONYMOUS) để tạo ra các trang không, vì vậy nó không nhân đôi công việc không trang của kernel.
Peter Cordes

6

Không chắc chắn về điều này nhưng lời giải thích duy nhất mà tôi có thể nghĩ là linux là một hệ điều hành sao chép trên ghi. Khi một người gọi forkcả hai quá trình trỏ đến cùng một bộ nhớ vật lý. Bộ nhớ chỉ được sao chép khi một quá trình thực sự VIẾT vào bộ nhớ.

Tôi nghĩ ở đây, bộ nhớ vật lý thực tế chỉ được phân bổ khi một người cố gắng viết một cái gì đó cho nó. Gọi điện thoại sbrkhoặc mmapcó thể chỉ cập nhật lưu giữ bộ nhớ của kernel. RAM thực tế chỉ có thể được phân bổ khi chúng ta thực sự cố gắng truy cập bộ nhớ.


forkkhông có gì để làm với điều này. Bạn sẽ thấy hành vi tương tự nếu bạn khởi động Linux với chương trình này như /sbin/init. (tức là PID 1, quá trình chế độ người dùng đầu tiên). Mặc dù vậy, bạn đã có ý tưởng chung đúng với copy-on-write: Cho đến khi bạn làm bẩn chúng, các trang mới được phân bổ đều là bản sao trên ghi được ánh xạ vào cùng một trang zeroed.
Peter Cordes

biết về ngã ba cho phép tôi đoán.
doron
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.