Làm thế nào để làm cho kernel bị nén bộ nhớ bị phân mảnh


8

Tôi đang chạy Fedora 26.

Đây là một bài tập rất lạ được đưa ra bởi giáo sư thuật toán của tôi. Bài tập nói:

Phân mảnh bộ nhớ trong C:
Thiết kế, thực hiện và thực hiện chương trình C thực hiện như sau: Nó phân bổ bộ nhớ cho một chuỗi các 3mmảng có kích thước 800.000 phần tử mỗi phần; sau đó, nó giải quyết rõ ràng tất cả các mảng được đánh số chẵn và phân bổ một chuỗi các mmảng có kích thước 900.000 phần tử mỗi mảng. Đo lượng thời gian mà chương trình của bạn yêu cầu để phân bổ chuỗi đầu tiên và cho chuỗi thứ hai. Chọn mlàm cạn kiệt gần như tất cả bộ nhớ chính có sẵn cho chương trình của bạn. "

Mục tiêu tổng thể của việc này là phân mảnh bộ nhớ sau đó yêu cầu nhiều hơn một chút so với những gì có sẵn như một đoạn liền kề, buộc hệ điều hành phải thu gọn hoặc phân mảnh bộ nhớ.

Trong lớp tôi đã hỏi làm thế nào chúng ta nên làm điều này vì bộ nhớ được trực quan hóa và không thực sự tiếp giáp, anh ấy trả lời: "Chà, bạn sẽ phải tắt [bộ nhớ ảo]." Một số sinh viên khác hỏi trong lớp làm thế nào chúng ta nên biết khi nào chúng ta đạt được "bộ sưu tập rác" này và anh ta nói rằng: "Thời gian phân bổ thứ hai nên lớn hơn lần đầu tiên vì thời gian thu gom rác"

Sau khi tìm kiếm xung quanh một chút, điều gần nhất tôi có thể tìm thấy để vô hiệu hóa bộ nhớ ảo là vô hiệu hóa bộ nhớ trao đổi với swapoff -a. Tôi đã vô hiệu hóa môi trường máy tính để bàn của mình và biên dịch và chạy chương trình của tôi từ thiết bị đầu cuối gốc (để tránh sự can thiệp có thể có từ các quá trình khác, đặc biệt là một môi trường nặng như Môi trường máy tính để bàn). Tôi đã làm điều này và chạy chương trình của mình với mức tăng mcho đến khi tôi đạt đến điểm mà thời gian phân bổ thứ hai lớn hơn lần đầu tiên.

Tôi đã chạy chương trình với sự gia tăng mvà cuối cùng tìm thấy một điểm trong đó thời gian cho lần phân bổ thứ hai nhiều hơn thời gian cho lần phân bổ đầu tiên. Trên đường đi, tuy nhiên, tôi đã đạt được một điểm trong đó quy trình đã bị giết trước khi phân bổ thứ hai. Tôi đã kiểm tra dmesgvà thấy rằng nó đã bị giết bởi oom-killer. Tôi đã tìm và đọc một số bài viết về oom-killer và phát hiện ra rằng bạn có thể vô hiệu hóa việc cấp phát bộ nhớ cho kernel.

Tôi đã làm điều này và chạy chương trình của tôi một lần nữa, chỉ lần này tôi không thể tìm thấy msao cho thời gian của lần thứ hai cao hơn lần đầu tiên. Cuối cùng với m lớn hơn và lớn hơn (mặc dù nhỏ hơn nhiều so với khi bật tổng thể), malloc sẽ thất bại và chương trình của tôi sẽ chấm dứt.

Tôi có ba câu hỏi, câu hỏi đầu tiên không thực sự quan trọng:

  1. Là thu gom rác là thuật ngữ chính xác cho điều này? Giáo sư của tôi rất kiên quyết khi nói rằng đây là bộ sưu tập rác, nhưng tôi đã giả định rằng bộ sưu tập rác là thứ được thực hiện bởi các ngôn ngữ lập trình và điều này sẽ được coi là phân mảnh hơn.

  2. Là nén như anh ta muốn có thể trên một hệ thống linux?

  3. Tại sao tôi có thể đạt đến điểm mà thời gian phân bổ thứ hai cao hơn lần đầu tiên khi tôi vô hiệu hóa trao đổi nhưng vẫn kích hoạt tổng thể bộ nhớ? Đã thực sự nén chặt? Nếu vậy tại sao tôi không thể đạt đến điểm mà sự nén xảy ra sau khi tôi vô hiệu hóa tổng thể bộ nhớ?


Bạn không thể "tắt" bộ nhớ ảo. Ngoài ra, hãy lưu ý rằng trong Linux, bạn có thể phân bổ hợp lý nhiều bộ nhớ hơn so với thực tế - hạt nhân sẽ không thực sự phân bổ các trang cho đến khi bạn viết cho chúng.
Andy Dalton

3
Tôi đã mong đợi khác Viết bài tập về nhà của tôi! câu hỏi Nhưng đúng hơn, đây là một bài tập về nhà của tôi dường như được chỉ định rất tệ, thiếu suy nghĩ và không thể. Là nó? câu hỏi Một số trong số này là lãnh thổ Stack Overflow và bạn sẽ tìm thấy rất nhiều câu hỏi và trả lời dọc theo dòng (chỉ để chọn một ví dụ ngẫu nhiên) stackoverflow.com/questions/4039274 .
JdeBP

Câu trả lời:


5

Kudos về nghiên cứu của bạn cho đến nay, đây thực sự là một bộ câu hỏi thú vị.

Nhìn chung, có một khía cạnh quan trọng cần xem xét: phân bổ bộ nhớ là một phần trách nhiệm của hệ điều hành và một phần là trách nhiệm của mỗi quy trình đang chạy (bỏ qua các hệ thống cũ không có bảo vệ bộ nhớ và không gian địa chỉ ảo). Hệ điều hành đảm nhiệm việc cung cấp cho mỗi tiến trình một không gian địa chỉ riêng và phân bổ bộ nhớ vật lý cho các tiến trình khi cần thiết. Mỗi quy trình đảm nhận việc khắc lên không gian địa chỉ của nó (ở một mức độ nào đó) và đảm bảo nó được sử dụng một cách thích hợp. Lưu ý rằng trách nhiệm của một tiến trình sẽ phần lớn là vô hình đối với các lập trình viên, vì môi trường thời gian chạy đảm nhiệm hầu hết mọi thứ.

Bây giờ, để trả lời câu hỏi của bạn ...

  1. Trong tâm trí tôi, bộ sưu tập rác là một bước được loại bỏ khỏi những gì bạn đang làm ở đây. Tôi tưởng tượng bạn đang viết bằng C, sử dụng malloc()free(). Bộ sưu tập rác , được hỗ trợ bởi ngôn ngữ lập trình môi trường thời gian chạy, sẽ chăm sóc phần sau: nó xác định các khối bộ nhớ đã được phân bổ trước đó nhưng không còn được sử dụng (và quan trọng là không bao giờ có thể sử dụng lại) và trả về chúng để người cấp phát. Câu hỏi được liên kết trong bình luận của JdeBP , cung cấp một số thông tin cơ bản, nhưng tôi thấy nó rất thú vị bởi vì nó chứng minh rằng những người khác nhau có ý kiến ​​rất khác nhau về việc thu gom rác và thậm chí cả những gì tạo nên bộ sưu tập rác.

    Trong bối cảnh chúng tôi quan tâm, tôi sẽ sử dụng bộ nhớ nén chặt chẽ của Google để nói về quá trình đang thảo luận.

  2. Từ góc độ lập trình không gian người dùng, điều mà giáo sư của bạn yêu cầu là không thể, trong C, dưới Linux, vì một lý do đơn giản: điều chúng tôi quan tâm ở đây không phải là phân mảnh bộ nhớ vật lý, đó là phân mảnh không gian. Khi bạn phân bổ nhiều khối 800.000 byte của mình, bạn sẽ chỉ có nhiều con trỏ cho mỗi khối. Trên Linux, tại thời điểm này, bản thân hệ điều hành đã không làm được gì nhiều và bạn không nhất thiết phải có bộ nhớ vật lý sao lưu mỗi phân bổ (như một phần, với các phân bổ nhỏ hơn, hệ điều hành sẽ không liên quan gì cả, chỉ là của bạn Phân bổ của thư viện C, nhưng phân bổ ở đây đủ lớn để thư viện C sẽ sử dụngmmap, được xử lý bởi kernel). Khi bạn giải phóng các khối được đánh số lẻ, bạn sẽ lấy lại các khối không gian địa chỉ đó, nhưng bạn không thể thay đổi các con trỏ bạn có sang các khối khác. Nếu bạn in các con trỏ ra khi bạn đi, bạn sẽ thấy sự khác biệt giữa chúng không nhiều hơn yêu cầu phân bổ (802.816 byte trên hệ thống của tôi); không có chỗ giữa hai con trỏ cho khối 900.000 byte. Vì chương trình của bạn có các con trỏ thực tế cho từng khối, thay vì một số giá trị trừu tượng hơn (trong các bối cảnh khác, một điều khiển), môi trường thời gian chạy không thể làm gì về điều đó và vì vậy nó không thể nén bộ nhớ của nó để kết hợp các khối miễn phí.

    Nếu bạn sử dụng ngôn ngữ lập trình trong đó con trỏ không phải là khái niệm có thể nhìn thấy của lập trình viên, thì có thể nén bộ nhớ trong Linux. Một khả năng khác là sử dụng API cấp phát bộ nhớ trong đó các giá trị được trả về không phải là con trỏ; xem ví dụ các hàm phân bổ heap dựa trên tay cầm trong Windows (trong đó các con trỏ chỉ có hiệu lực khi một tay cầm bị khóa).

  3. Bài tập của giáo sư của bạn đang đo hiệu quả hiệu suất của mmapnó, bao gồm thuật toán đi bộ miễn phí. Trước tiên, bạn phân bổ các khối 3 × m , sau đó giải phóng một nửa trong số chúng, sau đó bắt đầu phân bổ lại các khối m ; giải phóng tất cả các khối đó tạo ra một khối lượng lớn các khối miễn phí trên bộ cấp phát của kernel, mà nó cần theo dõi (và thời gian thực hiện bởi các freecuộc gọi cho thấy rằng không có tối ưu hóa nào được thực hiện tại thời điểm này). Nếu bạn theo dõi thời gian phân bổ của từng khối riêng lẻ, thì bạn sẽ thấy rằng phân bổ 900k đầu tiên mất nhiều, nhiềudài hơn các thứ tự khác (ba bậc độ lớn trên hệ thống của tôi), lần thứ hai nhanh hơn nhiều nhưng vẫn mất nhiều thời gian hơn (hai bậc độ lớn) và phân bổ thứ ba trở lại mức hiệu suất điển hình. Vì vậy, có điều gì đó đang xảy ra, nhưng các con trỏ được trả về cho thấy rằng đó không phải là nén bộ nhớ, ít nhất là không được phân bổ nén khối (như đã giải thích ở trên là không thể) - có lẽ là thời gian tương ứng với thời gian xử lý cấu trúc dữ liệu mà hạt nhân sử dụng theo dõi không gian địa chỉ khả dụng trong quy trình (Tôi đang kiểm tra điều này và sẽ cập nhật sau). Các phân bổ dài này có thể phát triển để làm giảm các chuỗi phân bổ tổng thể mà bạn đang đo, đó là khi phân bổ 900k cuối cùng mất nhiều thời gian hơn so với phân bổ 800k.

    Lý do overcommit thay đổi hành vi mà bạn thấy là vì nó thay đổi bài tập từ thao túng hoàn toàn không gian địa chỉ, sang thực sự phân bổ bộ nhớ và do đó làm giảm kích thước sân chơi của bạn. Khi bạn có thể quá mức, hạt nhân chỉ bị giới hạn bởi không gian địa chỉ của quy trình của bạn, vì vậy bạn có thể phân bổ nhiều khối hơn và gây áp lực nhiều hơn cho bộ cấp phát. Khi bạn vô hiệu hóa overcommit, kernel bị giới hạn bởi bộ nhớ khả dụng, điều này làm giảm giá trị bạn có thể có mxuống mức mà bộ cấp phát không đủ căng thẳng để thời gian phân bổ bị nổ tung.


Việc sử dụng calloc () hoặc thực sự ghi vào các mảng được phân bổ có làm nên sự khác biệt nào ở đây không?
Kusalananda

Ghi vào bộ nhớ được phân bổ sẽ hủy bỏ khả năng vượt mức, nhưng thật khó để xử lý vì thất bại sau đó dẫn đến việc kẻ giết người OOM bước vào (và không nhất thiết phải giết quá trình phân bổ quá mức). calloc()với các phân bổ lớn hoạt động giống như malloc()trên Linux, sử dụng mmap()để phân bổ ánh xạ ẩn danh, được lấp đầy bằng 0 khi sử dụng lần đầu (do đó, overcommit vẫn hoạt động).
Stephen Kitt
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.