מבוא ל-io_uring - איך לינוקס פתר את הבעיה שאף אחד לא שם לב אליה¶
כל קריאת מערכת, גם read פשוטה, עולה זמן - יש מעבר מלא ממרחב המשתמש למרחב הקרנל וחזרה, וזה קורה גם אם הבקשה עצמה זעירה. בשרת רגיל, זה זניח. אבל בשרת שמבצע מיליוני פעולות קלט ופלט בשנייה, העלות הזאת מצטברת לנתח משמעותי מהזמן הכולל. io_uring הוא מנגנון חדש יחסית בקרנל לינוקס שנועד לצמצם את העלות הזאת כמעט לגמרי, בעזרת רעיון פשוט להפליא - לתת לתהליך ולקרנל לדבר דרך זיכרון משותף, במקום דרך קריאות מערכת בודדות.
הבעיה עם המודל הישן¶
בגישה המסורתית, כל פעולת קלט ופלט היא קריאת מערכת נפרדת - read אחת, write אחת, כל אחת עם מעבר מלא בין מרחב המשתמש למרחב הקרנל. גם מנגנונים ותיקים שנועדו לטפל בהרבה חיבורים בו זמנית, כמו epoll, עדיין דורשים קריאת מערכת נפרדת כדי לבדוק אילו תיאורי קבצים מוכנים, ועוד קריאה נפרדת כדי לבצע בפועל את הפעולה עצמה. לינוקס אמנם כבר הציע מנגנון קלט ופלט אסינכרוני קודם, POSIX AIO, אבל הוא סבל ממגבלות משמעותיות ולא תמך היטב בכל סוגי הפעולות.
הרעיון המרכזי - שני מעגלים בזיכרון משותף¶
io_uring פותר את הבעיה בעזרת שני מבני נתונים שהתהליך והקרנל שניהם ניגשים אליהם ישירות, דרך אותו מיפוי זיכרון שכבר הכרנו - תור הגשה, submission queue, ותור השלמה, completion queue. במקום לקרוא לקריאת מערכת בכל פעם שרוצים לבצע פעולה, התוכנית פשוט כותבת בקשה ישירות לתור ההגשה, בזיכרון משותף, בלי מעבר לקרנל בכלל. הקרנל, מצידו, קורא מהתור הזה, מבצע את הפעולות, וכותב את התוצאות לתור ההשלמה - שגם אותו התוכנית קוראת ישירות מהזיכרון, שוב בלי צורך בקריאת מערכת נפרדת.
התוצאה היא שאפשר להגיש כמות גדולה של בקשות קלט ופלט, ולקבל את התוצאות שלהן, כמעט בלי לשלם את מחיר המעבר בין מרחב המשתמש למרחב הקרנל על כל פעולה בנפרד - רק על קבוצות שלמות של פעולות בבת אחת.
הגשה בקבוצות במקום אחת אחת¶
כיוון שהבקשות נכתבות לתור משותף בזיכרון, תוכנית יכולה להצטבר כמה בקשות רצופות - למשל, לקרוא מכמה קבצים שונים במקביל - ולהודיע לקרנל פעם אחת שיש קבוצה שלמה של עבודה חדשה לבצע. הקרנל מטפל בכולן, ומחזיר את כל התוצאות דרך תור ההשלמה. מספר קריאות המערכת הכולל יכול לרדת בסדרי גודל, במיוחד בעומסים כבדים עם הרבה פעולות קטנות.
למי בכלל זה מיועד¶
io_uring לא נועד לתוכנית רגילה שקוראת קובץ קונפיגורציה פעם אחת בהפעלה. היתרון שלו מורגש בעיקר במערכות שמבצעות נפח עצום של פעולות קלט ופלט - שרתי מסדי נתונים, שרתי web בעומס גבוה, ומערכות אחסון מבוזרות, שבהן כל מיקרו-שנייה שנחסכת בכל פעולה מצטברת למשהו משמעותי כשמכפילים אותה במיליוני פעולות בשנייה. בשביל רוב התוכנות היום יומיות, המודל המסורתי עדיין פשוט ומספיק לגמרי.
איך משתמשים בזה בפועל¶
לרוב לא כותבים קוד שמדבר ישירות עם תורי ה-io_uring הגולמיים - זה מסובך ודורש זהירות רבה. הדרך הנפוצה היא דרך ספרייה בשם liburing, שעוטפת את המנגנון הגולמי בממשק נוח ומוכן יותר, ומטפלת בפרטים העדינים של ניהול התורים בזיכרון המשותף. מערכות ותשתיות רבות כבר החלו לאמץ אותה כאופציה לביצועים גבוהים במיוחד, לצד המנגנונים המסורתיים.
למה כדאי להכיר את זה¶
io_uring הוא דוגמה מצוינת לאיך פתרון מקורי לבעיה ישנה, מעבר בין מרחב משתמש לקרנל, יכול להוציא ביצועים משמעותיים בלי לשנות את החומרה בכלל, רק את הדרך שבה תוכנה ותוכנה מדברות זו עם זו.
זה בדיוק סוג המנגנונים המודרניים שאנחנו חושפים בקורס ליבת המחשב - לא רק איך המערכת עובדת מסורתית, אלא גם לאן היא מתקדמת.
ואם io_uring נשמע לכם כמו הרבה מידע בבת אחת, יש קהילה שתשמח לפרק את זה יחד איתכם.
לסיכום¶
io_uring מצמצם את עלות קריאות המערכת המסורתיות על ידי שיתוף תורי הגשה והשלמה בזיכרון משותף בין תהליך לקרנל, במקום לעבור דרך קריאת מערכת נפרדת על כל פעולת קלט ופלט. זה מאפשר הגשת בקשות בקבוצות גדולות, וחיסכון משמעותי בעומסים כבדים. זה לא כלי שכל תוכנית זקוקה לו, אבל למערכות שמתמודדות עם נפח עצום של קלט ופלט, הוא בדיוק סוג החדשנות שממשיכה לדחוף את לינוקס קדימה.