מיפוי משותף מול שינוי חיצוני¶
נניח שאנחנו ממפים קובץ ומחזירים ממנו &[u8] כדי לאפשר קריאה ללא העתקה. הממשק נראה כמו זה של המיפוי האנונימי מהשיעור הקודם, אבל בעלים אחר יכול לכתוב לקובץ או לקצר אותו. עכשיו עצם קיום האובייקט המקומי אינו מבטיח שהטווח עדיין ניתן לקריאה או שתוכנו נשאר מתאים לחוזה ההפניה.
מיפוי משותף מאפשר לנתונים שנכתבים דרך המיפוי להשתתף במצב משותף עם תהליכים וקובץ מגבה לפי סוג המיפוי. הוא אינו מספק אוטומטית פרוטוקול סנכרון. שני משתתפים שכותבים שדות של רשומה יכולים להראות לקורא שילוב של גרסאות גם כאשר כל כתובת עצמה תקפה.
נתחיל בכשל חיים שקל לראות. התוכנית הבאה בשפת סי יוצרת קובץ תרגיל חדש, ממפה אותו, מחכה לקלט ואז קוראת מהמיפוי. היא מיועדת במפורש לניסוי כשל בתהליך נפרד.
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv) {
if (argc != 2) return 1;
long page = sysconf(_SC_PAGESIZE);
if (page <= 0) return 2;
int fd = open(argv[1], O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd < 0 || ftruncate(fd, page) < 0) return 3;
void *mapping = mmap(NULL, (size_t)page, PROT_READ, MAP_SHARED, fd, 0);
if (mapping == MAP_FAILED) return 4;
puts("truncate the test file from another terminal, then press Enter");
getchar();
volatile unsigned char value = *(volatile unsigned char *)mapping;
printf("value=%u\n", (unsigned)value);
munmap(mapping, (size_t)page);
close(fd);
return 0;
}
במסוף אחר קצרו רק את קובץ התרגיל החדש באמצעות truncate -s 0, ואז המשיכו את התוכנית. בלינוקס גישה לעמוד שאינו קיים עוד בטווח הקובץ יכולה להוביל ל־SIGBUS. המתאר עדיין פתוח והמיפוי עדיין מופיע במפות התהליך, אבל אלה אינם מבטיחים שהקובץ נשאר באורך המקורי.
הקריאה ה־volatile אינה מגינה מפני הכשל. היא מבקשת לבצע גישה נצפית לפי כללי השפה, אך אינה נועלת את הקובץ, אינה מאריכה אותו ואינה מסנכרנת עם הכותב. באותו אופן, שימוש במצביע גולמי בראסט אינו הופך כל שינוי חיצוני לחוקי. צריך להגדיר מה מותר לכל משתתף ומתי מותר ליצור הפניה רגילה.
גם בדיקת אורך לפני הגישה אינה פתרון בפני עצמה. התהליך האחר יכול לקצר את הקובץ בין הבדיקה לקריאה. זאת אותה צורה של מרוץ check-then-use שפגשנו בעולם המתארים. נדרש מנגנון שמייצב את ההבטחה לאורך הפעולה, או ממשק שאינו מבטיח הפניה יציבה אל נתון חיצוני משתנה.
אחת האפשרויות היא לעבוד עם קובץ שאי אפשר לשנות בזמן השימוש לפי חוזה מערכת אכיף. אפשרות אחרת היא להעתיק את הנתונים למאגר בבעלות מקומית ואז לפרש אותם. ההעתקה מוסיפה עלות, אבל יוצרת גבול ברור שבו ניתן לבדוק אורך, checksum ותקפות מבנה לפני שמחזירים טיפוסים בטוחים. נעילה advisory מועילה רק אם כל המשתתפים מצייתים לה, ולכן אינה מגינה מול כותב שאינו חלק מהפרוטוקול.
כאשר בונים זיכרון משותף בין תהליכים, כדאי להפריד בין מצב פרוטוקול לבין הפניות. המבנה יכול להכיל מיקומים יחסיים במקום מצביעים וירטואליים, משום שאותו אובייקט יכול להיות ממופה בכתובות שונות בכל תהליך. צריך להגדיר יישור, גרסת פורמט ודרך לזהות רשומה חלקית, ולא להניח שמבנה repr(Rust) הוא פורמט שיתוף יציב.
גם coherence ו־durability הם דברים שונים. העובדה שתהליך אחר רואה עדכון בזיכרון אינה אומרת שהנתון ישרוד הפסקת חשמל. פעולות סנכרון בין משתתפים קובעות מתי מותר לקרוא מצב משותף; פעולות עמידות קובעות אילו שכבות אחסון קיבלו התחייבות מתאימה. ערבוב ביניהן מייצר מערכות שנראות נכונות במבחן עם שני תהליכים אבל מאבדות מידע אחרי אתחול.
הספרייה הבטוחה שלנו צריכה לומר בדיוק איזה סוג מיפוי היא מציעה. מיפוי אנונימי פרטי שנמצא בשליטה בלעדית שונה ממיפוי קובץ חיצוני ומשונה מאזור IPC עם פרוטוקול אטומי. עדיף שלושה ממשקים עם חוזים ברורים על בנאי אחד שמחזיר תמיד &mut [u8] בלי קשר למקור הנתונים.
הסמנטיקה של מיפוי וקיצור מתועדת בעמוד mmap ובעמוד truncate. בסקירת בטיחות שאלו מי עוד יכול לשנות את האחסון, ולא רק מי מחזיק את המצביע המקומי.