סנכרון בקרנל מוסבר - למה נעילה רגילה לא תמיד מספיקה¶
בתוכנית רגילה במרחב המשתמש, כשכמה תהליכונים ניגשים למשתנה משותף, פתרון הבעיה נראה יחסית פשוט - נועלים נעילה, נכנסים לאזור קריטי, ומשחררים בסוף. בתוך קרנל לינוקס, אותה בעיה בדיוק מסתבכת פי כמה - כי הקוד לא רק רץ על כמה ליבות מעבד בו זמנית, הוא גם יכול להיקטע באמצע על ידי פסיקת חומרה בכל רגע נתון, כולל בדיוק באמצע עדכון מבנה נתונים משותף.
למה סנכרון בקרנל שונה משמעותית מבמרחב המשתמש¶
בתוכנית רגילה, אם תהליכון נועל נעילה ולא מצליח להיכנס מיד לאזור הקריטי, הפתרון הטבעי הוא להירדם ולתת לתהליכון אחר לרוץ עד שהנעילה תתפנה. בתוך הקרנל, זה לא תמיד אפשרי. אם קוד רץ בתוך הקשר פסיקה - interrupt context, כלומר בזמן שהוא מטפל בפסיקת חומרה, הוא לא יכול פשוט "להירדם" ולחכות - אין למי להעביר שליטה בהקשר הזה, אין תהליך שיתן לו לחכות. זו הסיבה שקרנל לינוקס משתמש בכמה כלי סנכרון שונים, לפי ההקשר שבו הם רצים.
Spinlock - נעילה שממתינה בעמידה¶
spinlock הוא נעילה שכשהיא תפוסה, כל מי שמנסה לתפוס אותה פשוט מסתובב בלולאה צפופה, בודק שוב ושוב אם הנעילה התפנתה, במקום להירדם. זה נשמע כמו בזבוז זמן מעבד, וזה באמת בזבוז אם ההמתנה ארוכה. אבל spinlock בטוח לשימוש גם בתוך הקשר פסיקה, בדיוק כי הוא לא דורש להירדם או להעביר שליטה לתהליך אחר - הוא פשוט מחכה במקום, ברמת המעבד עצמו.
הכלל בקרנל הוא ברור - spinlock מתאים למקרים שבהם הנעילה מוחזקת לזמן קצר מאוד, בערך כמו כמה הוראות מעבד. שימוש בו לתקופת זמן ארוכה מבזבז משאבים בצורה משמעותית, כי כל שאר הליבות שמנסות לגשת פשוט "שורפות" זמן מעבד בהמתנה פעילה.
Mutex - נעילה שממתינה תוך כדי שינה¶
mutex, לעומת זאת, מתאים למצבים שבהם קוד יכול להרשות לעצמו להירדם - כלומר, כשהוא רץ בהקשר תהליך רגיל, לא בתוך פסיקה. אם ה-mutex תפוס, התהליך שמנסה לתפוס אותו נכנס למצב המתנה, מוסר מהתור של המתזמן, ומתעורר רק כשהמוטקס משתחרר. זה חוסך משאבי מעבד בהמתנות ארוכות, אבל הוא לא בטוח לשימוש בהקשר פסיקה, בדיוק כי הוא דורש היכולת להירדם, שלא קיימת שם.
הבחירה בין spinlock למוטקס היא לא עניין של טעם אישי - היא נובעת ישירות מההקשר שבו הקוד רץ, ומכמה זמן צפוי שהנעילה תוחזק.
פעולות אטומיות - כשלא צריך נעילה בכלל¶
הרבה פעמים אין בכלל צורך בנעילה מלאה - מספיק לוודא שפעולה בודדת, כמו הגדלת מונה, מתבצעת בשלמותה בלי הפרעה. פעולות אטומיות - atomic operations, מנצלות הוראות מעבד ייעודיות שמבצעות קריאה, שינוי, וכתיבה חזרה, בפעולה חשמלית אחת בלתי ניתנת לחלוקה. הן מהירות משמעותית מנעילה מלאה, אבל מתאימות רק לפעולות פשוטות מאוד, כמו עדכון מונה יחיד.
RCU - קריאה בלי נעילה בכלל¶
מנגנון מתקדם יותר בקרנל לינוקס נקרא RCU, ראשי תיבות של Read-Copy-Update. הרעיון שלו הוא לאפשר לקוראים לגשת למבנה נתונים משותף בלי שום נעילה בכלל, גם כשמישהו אחר בו זמנית מעדכן אותו. זה מתאפשר בזכות עיקרון פשוט - במקום לעדכן מבנה נתונים קיים ישירות, יוצרים עותק חדש שלו, מעדכנים את העותק, ורק אז מחליפים את המצביע למבנה הישן במצביע למבנה החדש, בפעולה אטומית בודדת. קוראים שכבר התחילו לקרוא מהמבנה הישן ממשיכים לראות אותו עד שהם מסיימים, בלי הפרעה, בעוד קוראים חדשים כבר רואים את הגרסה המעודכנת. המנגנון הזה חיוני בחלקים בקרנל שבהם קריאות הרבה יותר נפוצות מעדכונים.
למה כדאי להבין את השכבה הזאת¶
סנכרון בקרנל הוא דוגמה מצוינת לאיך אילוצי ביצועים אמיתיים מכתיבים בחירת כלים - spinlock, מוטקס, פעולות אטומיות ו-RCU כולם פותרים את אותה בעיה בסיסית, גישה משותפת לזיכרון, אבל כל אחד מתאים להקשר אחר לגמרי.
זה בדיוק סוג ההבנה שאנחנו בונים בקורס ליבת המחשב - איך קוד קרנל מתמודד עם מקביליות ברמה שהרבה יותר מחמירה ממה שרוב המתכנתים פוגשים ביום יום.
ואם ההבדל בין spinlock למוטקס עדיין לא לגמרי ברור, יש קהילה שתשמח לעזור.
לסיכום¶
סנכרון בקרנל מסתבך בדיוק בגלל ההקשרים השונים שבהם קוד יכול לרוץ - ליבות מעבד מקבילות, ופסיקות חומרה שיכולות לקטוע כל רגע. spinlock מתאים לנעילות קצרות שבהן אי אפשר להירדם, מוטקס מתאים כשאפשר להמתין תוך כדי שינה, פעולות אטומיות פותרות עדכונים בודדים בלי נעילה בכלל, ו-RCU מאפשר קריאה בלי שום חסימה גם כשמתבצע עדכון בו זמנית. ביחד, ארבעת הכלים האלה הם מה שמאפשר לקרנל לרוץ בבטחה על מעבדים רב-ליבתיים בלי לקרוס תחת מרוצי נתונים.