מרחב כתובות והתחייבות לזיכרון¶
נניח שביקשנו ארבעה גיגה־בתים באמצעות VirtualAlloc והפעולה הצליחה כמעט מיד. האם ארבעה גיגה־בתים של RAM כבר שייכים לנו? בלי לדעת אילו flags העברנו, אי אפשר לענות. שמירת כתובות, התחייבות לאחסון ותושבות פיזית הן שלוש שאלות שונות.
מרחב הכתובות הוא המפה שבה היישום רואה מצביעים. פעולת reserve שומרת טווח במפה כדי שהקצאה אחרת לא תשתמש בו. פעולת commit מבקשת ממערכת ההפעלה להתחייב לאחסון בהתאם למדיניות הזיכרון. הדפים הפיזיים שמשרתים את הטווח אינם חייבים להיות מוקצים ותושבים באותו רגע.
עוקבים אחר ארבעה דפים¶
התוכנית הבאה שומרת טווח קטן, מבצעת commit לדף אחד, נוגעת בו ואז מבטלת את ה־commit ומשחררת את ההזמנה. היא מדפיסה את מצב האזור דרך VirtualQuery.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cstddef>
#include <cstdio>
bool show(const char* label, const void* address) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(address, &info, sizeof(info)) == 0) return false;
std::printf("%s base=%p allocation=%p size=%zu state=%lx protect=%lx\n",
label, info.BaseAddress, info.AllocationBase,
static_cast<std::size_t>(info.RegionSize), info.State, info.Protect);
return true;
}
int main() {
SYSTEM_INFO system{};
GetSystemInfo(&system);
const SIZE_T page_size = system.dwPageSize;
std::printf("page=%lu granularity=%lu\n",
system.dwPageSize, system.dwAllocationGranularity);
void* base = VirtualAlloc(nullptr, page_size * 4,
MEM_RESERVE, PAGE_NOACCESS);
if (!base) return 1;
auto* selected = static_cast<unsigned char*>(base) + page_size;
show("reserved", selected);
if (!VirtualAlloc(selected, page_size, MEM_COMMIT, PAGE_READWRITE)) {
const DWORD error = GetLastError();
VirtualFree(base, 0, MEM_RELEASE);
std::printf("commit error=%lu\n", error);
return 2;
}
show("committed", selected);
*static_cast<volatile unsigned char*>(selected) = 42;
show("touched", selected);
if (!VirtualFree(selected, page_size, MEM_DECOMMIT)) {
const DWORD error = GetLastError();
VirtualFree(base, 0, MEM_RELEASE);
std::printf("decommit error=%lu\n", error);
return 3;
}
show("decommitted", selected);
if (!VirtualFree(base, 0, MEM_RELEASE)) return 4;
}
הכתיבה ב־volatile נועדה לכך שהניסוי יבצע גישה מפורשת ולא יסלק אותה כאופטימיזציה. היא אינה מנגנון סנכרון בין threads. בקוד מוצר נשתמש בחוזי אובייקטים ובסנכרון מתאימים, ולא ב־volatile כפתרון כללי.
גודל דף וגודל הזמנה אינם אותו יישור¶
ה־page size קובע יחידת תרגום והגנה בסיסית, ואילו allocation granularity משפיעה על כתובת ההתחלה של הזמנה או מיפוי מסוים. ב־Windows x64 נפוצים ערכים מוכרים, אך התוכנית שואלת את המערכת במקום לקבע אותם.
ה־commit לדף הפנימי משתמש בכתובת שכבר נמצאת בתוך ההזמנה. אין צורך לשמור את אותו טווח שוב. מערכת ההפעלה יכולה לעגל טווחים לגבולות דף לפי החוזה, ולכן חישוב גודל צריך להבין אילו דפים נכללים בפועל.
הכתיבה לדף שנמצא רק במצב reserved אינה חוקית. הטווח השמור אינו buffer שמותר להשתמש בו; הוא מקום עתידי במפה. גם pointer שאינו null אינו מוכיח שדף committed או בעל הרשאה מתאימה.
התחייבות לזיכרון - commit¶
המערכת מנהלת commit charge ו־commit limit. הבקשה יכולה להיכשל גם אם היישום רואה הרבה כתובות פנויות, משום שמשאבי הגיבוי והמדיניות אינם מספיקים. לכן מרחב כתובות ויכולת להתחייב לזיכרון הם גבולות נפרדים.
קובץ ההחלפה יכול להשתתף בגיבוי, אך זיכרון וירטואלי אינו שם אחר ל־pagefile. מיפוי קובץ, דף משותף ודף פרטי דורשים הסברים שונים. גם מערכת בלי pagefile עדיין משתמשת בזיכרון וירטואלי ובטבלאות דפים.
ה־commit אינו מבטיח שהדף כבר נמצא ב־working set של התהליך. גישה ראשונה יכולה לגרום ל־page fault שמערכת ההפעלה פותרת. בשיעור הבא נמדוד את ההבדל בין הכנת הטווח לבין נגיעה בו.
תיאור אזורים באמצעות VirtualQuery¶
ה־RegionSize שחוזר אינו בהכרח גודל הבקשה המקורית. הפונקציה מתארת רצף דפים בעל מאפיינים תואמים החל מהכתובת שנבדקה. התחייבות לדף אחד באמצע הזמנה יכול לפצל את התצוגה למספר אזורים.
ה־AllocationBase מאפשר לקשר חלקים להזמנה המקורית. השדה State אומר אם מדובר ב־MEM_FREE, MEM_RESERVE או MEM_COMMIT. השדות אינם אומרים לבדם כמה RAM פרטי צורך כל דף ברגע המדידה.
לכן אין לבנות כלי מדידת RAM על סכימת RegionSize של כל אזור committed בלבד. זה יכול לספור הזמנות מחויבות, שיתוף ומיפויים בצורה שאינה תואמת למדד שהתכוונו אליו.
ביטול התחייבות תוך שמירת הכתובת - decommit¶
אחרי MEM_DECOMMIT הטווח נשאר reserved, אך אין יותר זכות לגשת לתוכן הקודם. התחייבות מחדש לדף מחזיר אחסון מאופס לפי החוזה הרלוונטי. אין להשתמש בו כאילו הנתונים שרדו מפני שהכתובת נשארה זהה.
ב־MEM_RELEASE חייבים להעביר את בסיס ההזמנה המקורי וגודל אפס. אי אפשר להשתמש באותה צורת קריאה כדי לשחרר חצי reservation רגיל. טעות כזו יכולה לגרום לכשל שקט אם לא בודקים return value.
מחברים את המודל ל־C++¶
קבלת אחסון ממערכת ההפעלה אינה מריצה constructor של מחלקה. אם נרצה לשים בו אובייקט מורכב, צריך לדאוג ליישור, לבנייה, להשמדה ולשחרור לפי הסדר. בפרקים הקודמים הפרדנו storage מ־lifetime; כאן רואים שגם ל־storage עצמו יש כמה מצבי מערכת.
לקריאה נוספת: החוזה של VirtualAlloc וההבדל בין decommit ל־release.