קלט פלט ממופה זיכרון מוסבר - MMIO¶
חשוב לפתוח את הפוסט הזה בהבהרה, כי יש כאן בלבול נפוץ. כשמדברים על מיפוי זיכרון של קבצים, מתכוונים לפעולת mmap שמכניסה תוכן של קובץ למרחב הכתובות של תהליך. הפוסט הזה עוסק במשהו אחר לגמרי - איך כתובות זיכרון מסוימות לא מובילות בכלל לזיכרון RAM אמיתי, אלא מחוברות ישירות לרגיסטרים של התקני חומרה. זה נקרא קלט/פלט ממופה זיכרון, או Memory-Mapped I/O, בקיצור MMIO.
איך מעבד בכלל "מדבר" עם חומרה¶
תחשבו רגע איך זה בכלל אמור לעבוד. יש לכם כרטיס רשת, מסך, או דיסק, ולמעבד יש רק דרך אחת בסיסית להתקשר עם כל דבר - לקרוא וכתוב לכתובות. אז איך הוא מעביר פקודות להתקן חומרה פיזי שהוא לא זיכרון בכלל?
הפתרון של MMIO הוא רעיון פשוט וגאוני בו זמנית - מקצים טווח מסוים של כתובות במרחב הכתובות של המערכת, ובמקום שהכתובות האלה יובילו בפועל לתאי זיכרון RAM, הן מחוברות פיזית, ברמת החומרה, לרגיסטרים של ההתקן. כשהמעבד כותב ערך לכתובת כזאת, הוא בעצם לא שומר נתון בזיכרון - הוא שולח פקודה או נתון ישירות להתקן. כשהוא קורא מכתובת כזאת, הוא לא מקבל מה ששמרו שם קודם, הוא מקבל את המצב הנוכחי של ההתקן באותו רגע.
מנקודת המבט של הקוד שרץ, זה נראה בדיוק כמו כתיבה וקריאה רגילה מזיכרון. אין צורך בפקודות מעבד מיוחדות - פשוט מצביע לכתובת המסוימת הזאת, וכותבים או קוראים ממנה כרגיל. כל הקסם קורה ברמת החומרה, שמנתבת את הגישה הזאת להתקן ולא ל-RAM.
ההבדל מגישה ישנה יותר - Port-Mapped I/O¶
לפני ש-MMIO הפכה לגישה הדומיננטית, הייתה גישה אחרת שנקראת Port-Mapped I/O, או קלט/פלט ממופה יציאות. בגישה הזאת, למעבד יש מרחב כתובות נפרד לגמרי, בנוסף למרחב הכתובות הרגיל של הזיכרון, שמיועד אך ורק לתקשורת עם התקנים. כדי לגשת אליו, צריך פקודות מעבד ייעודיות שקיימות רק למטרה הזאת - למשל בארכיטקטורת x86 יש פקודות מיוחדות בדיוק בשביל זה.
ההבדל המרכזי הוא שב-Port-Mapped I/O יש שני "עולמות" נפרדים לגמרי - עולם הזיכרון ועולם ההתקנים, עם פקודות מעבד שונות לכל אחד מהם. ב-MMIO, לעומת זאת, יש עולם אחד - מרחב כתובות יחיד, שחלקים ממנו מובילים לזיכרון אמיתי וחלקים אחרים מובילים להתקנים, אבל כל הגישה אליו נעשית באותן פקודות רגילות של קריאה וכתיבה מזיכרון.
למה MMIO היא הגישה הדומיננטית היום¶
הסיבה המרכזית לכך ש-MMIO ניצחה כמעט לגמרי היא פשטות ונוחות. כשכל הגישה להתקנים נעשית דרך אותן פקודות רגילות של גישה לזיכרון, אין צורך בסט נפרד של פקודות מעבד ייעודיות, וקוד שכתבתם כדי לעבוד עם זיכרון - כולל אופטימיזציות, מבני נתונים, ומצביעים - עובד באותה צורה גם כשהוא בעצם מדבר עם חומרה.
יתרון נוסף הוא שדחיסת כל הגישה למרחב כתובות אחד מפשטת משמעותית את התכנון של המעבד עצמו ושל מערכת ההפעלה שמעליו. זו הסיבה שהארכיטקטורות המודרניות הנפוצות ביותר היום מבוססות בעיקר על MMIO, ו-Port-Mapped I/O נשארה בעיקר כשריד היסטורי לתאימות לאחור.
למה הקומפיילר עלול "לקלקל" גישה ל-MMIO¶
כאן מגיע חלק מעניין במיוחד, כי הוא חושף בעיה שלא הייתם חושבים עליה מלכתחילה. קומפיילרים מבצעים אופטימיזציות שמניחות שקריאה וכתיבה לזיכרון מתנהגות בצורה "רגילה" - למשל, אם קוד קורא את אותה כתובת זיכרון פעמיים ברצף בלי לכתוב לה בין לבין, הקומפיילר עשוי להחליט שאין טעם לקרוא פעמיים, ולהשתמש בערך שכבר קרא בפעם הראשונה.
הבעיה היא שבגישה להתקן חומרה דרך MMIO, ההנחה הזאת פשוט שגויה. כתובת MMIO יכולה להחזיר ערך שונה בכל קריאה, כי היא לא משקפת נתון סטטי בזיכרון אלא מצב חי של חומרה שמשתנה בזמן אמת - למשל רגיסטר שמדווח אם התקן סיים פעולה. אם הקומפיילר "יחסוך" קריאות כאלה בגלל אופטימיזציה, הקוד יפסיק לשקף את המצב האמיתי של ההתקן, ובאגים כאלה קשים במיוחד לאתר כי הם לא נראים כשגיאה בקוד עצמו.
הפתרון - מילת המפתח volatile¶
כדי למנוע את הבעיה הזאת, שפת C מספקת את מילת המפתח volatile. כשמצהירים שמצביע או משתנה הם volatile, בעצם אומרים לקומפיילר "אל תניח שום דבר על הערך הזה - כל קריאה וכתיבה חייבות לקרות בפועל, בדיוק כמו שכתוב בקוד, בלי לדלג ובלי לשנות סדר". ברמה מושגית, זה מבטל את מרבית האופטימיזציות שהקומפיילר היה מבצע סביב הגישה הזאת, כי הוא פשוט לא רשאי להניח שום דבר קבוע לגבי הערך.
בכל קוד שעובד עם MMIO - דרייברים, קוד קרנל, קוד embedded - כמעט תמיד תמצאו את מצביעי ההתקנים מוגדרים כ-volatile, בדיוק בגלל הסיבה הזאת. זו דוגמה טובה לאיך שהבנה של רמת החומרה משפיעה ישירות על החלטות שנראות לכאורה כמו פרט תחבירי קטן בשפת התכנות.
לסיכום¶
ה-MMIO היא דרך אלגנטית לגרום למעבד לדבר עם חומרה בלי לסבך את מערכת הפקודות שלו - היא הופכת גישה להתקן לגישה רגילה לכאורה לזיכרון, אבל בפועל מחברת אתכם ישירות לעולם של רגיסטרים חיים שמשתנים כל הזמן. בקורס ליבת המחשב אנחנו מסבירים איך זה מתחבר לכל שאר הנושאים ברמה הנמוכה - מהקרנל, דרך הדרייברים, ועד לחומרה עצמה.
מוזמנים לשאול בדיסקורד אם משהו לא ברור, במיוחד אם התנסיתם עם קוד דרייברים ונתקלתם בהתנהגות מוזרה שקשורה בדיוק לזה.