כן, עוד פוסט טכני.
כאשר מבצעים פעולות קבצים/sockets ביוניקס, נהוג להבדיל בין מספר סוגי פעולות. השאלה הראשונה שיש לשאול היא “האם הפעולה חוסמת” (blocking). פעולה חוסמת היא פעולה שלא חוזרת עד שהפעולה הושלמה. לצורך הדוגמא, דמיינו את המצב הבא. אנחנו מבקשים לכתוב לחיבור TCP/IP, מה שדורש שחלק מה-buffer שלנו ייכתב לחיבור. הבעיה היא שהצד השני של התקשורת מגיב לאט, מה שאומר שמידע קודם שכתבנו עוד לא קיבל אישור מהצד השני, והוא ממתין בצד שלנו. במצב כזה עלול להיות מצב שבו אין לשכבת ה-TCP/IP שלנו מקום לנתונים הנוספים שאנחנו מנסים לכתוב. אם אנחנו משתמשים בפעולה חוסמת מה שיקרה בשלב הזה זה שהתוכנית שלנו תמתין עד שהצד השני ייאשר קצת נתונים קודמים, מה שיפנה חוצצים, מה שיאפשר לקבל את הנתונים החדשים שאנחנו מנסים לכתוב ולשחרר את פעולת הכתיבה שלנו. אנחנו חסומים מלהמשיך בביצוע התוכנית עד שהמידע (לפחות חלקו) מתקבל.
בעוד שפעולות חוסמות הן מצב ברירת המחדל (ביוניקס וגם בחלונות), הן לא הדרך המומלצת לפעול. אפילו אם התוכנה שלנו מנהלת רק חיבור יחיד, עלול להיות מצב שהצד שלנו כותב את כל המידע שהוא יכול לפני שהוא קורא מהצד השני, והצד השני עושה את אותו הדבר. התוצאה תהיה ששני הצדדים ייחסמו בכתיבה, ויווצר dead lock. כדי להמנע מהמצב הזה, מומלץ להשתמש בפעולות לא חוסמות.
פעולות לא חוסמות עובדות כך. אנחנו מנסים לכתוב לחיבור שלנו. המערכת מחזירה ערך של כישלון ומחזירה שגיאה שאומרת “היינו חוסמים”. אנחנו אמורים להבין מהשגיאה שאין בעיה ספציפית בחיבור, אלא שהחוצצים של המערכת מלאים. יש אמצעים אחרים שבהם אנחנו יכולים לדעת מתי כבר כן אפשר לנסות לכתוב שוב. סגנון הכתיבה הזה מאפשר לטפל בכמות מאוד גדולה של חיבורים במקביל באמצעות thread יחיד, והיא צורת התכנון המומלצת כדי לקבל ביצועים מירביים.
בעקבות שאלה של חבר, ניסיתי לראות עד כמה יהיה קשה לממש שרת מסויים על חלונות. בעודי מסתכל על הממשקים של חלונות ל-TCP/IP, גיליתי עובדה מטרידה. מערכת ה-TCP/IP של חלונות הועתקה, ברובה, מהמערכת של יוניקס, ויש דמיון רב בשמות הפונקציות ובפרמטרים שלהם. דמיון רב, אבל לא מושלם. בעוד שהפונקציה send של יוניקס תומכת בדגל שאומר “אל תחסום”, לפונקציה המקבילה של חלונות פשוט אין את אותו הדגל. ביוניקס אפשר גם לבקש שכל הפעולות על אותו החיבור יהיו לא חוסמות באמצעות הפונקציה fcntl, שפשוט לא קיימת בחלונות.
נסיון לחפש את המילים “non blocking” בתיעוד של מיקרוסופט הביא אותי לדף הבא. להלן ציטוט נבחר:
Therefore, it was strongly recommended that programmers use the nonblocking (asynchronous) operations if at all possible with Windows Sockets 1.1.
במילים אחרות, הדף מתייחס לפעולה לא חוסמת כאל פעולה אסינכרונית. הבדל של סמנטיקה? ממש לא.
פעולות אסינכרוניות הן פעולות שמסתיימות בלי לחסום. בניגוד לפעולות לא חוסמות, שלא מתחילות אם הן עלולות לחסום אבל מתבצעות סינכרונית אם הן מתחילות, פעולות אסינכרוניות מתחילות ביצוע בכל מקרה ומייד. במילים אחרות, אתם אומרים לפעולה להתחיל, והיא מסתיימת מתי שהוא. כאשר היא מסתיימת אפשר לבקש לקבל הודעה (אסינכרונית) או לבצע פעולה מסויימת (בצורה אסינכרונית). פעולות אסינכרוניות הינן פעולות שמאוד קשה לתכנת ללא שגיאות, בין השאר בגלל הקומפילר לא יודע איך באמת איך לייצר קוד שיודע להתמודד עם הפרעות לא צפויות. אפשר לכתוב בצורה אסינכרונית, ולפעמים אפילו משתלם לכתוב כך, אבל לא רצוי אם אפשר להמנע מכך.
מעבר לתחושת חוסר האונים מול העדר כלי תכנותי שאני זקוק לו, מה שהפריע לי פה היה הזהות המוחלטת שבה הכותבים מתייחסים לפעולות לא חוסמות ולפעולות אסינכרוניות. לצורך ההבהרה: אין לי בעיה עם המשפט “פעולה אסינכרונית היא פעולה לא חוסמת”. הבעיה שלי היא עם המשפט “פעולה לא חוסמת היא פעולה אסינכרונית”.
כמובן שבסופו של דבר מצאתי את המאמר הבא שאומר בדיוק את זה, ואפילו מפנה לפעולה הרלוונטית בחלונות שעושה את מה שאני צריך. במילים אחרות, לא מדובר פה בפעולה שחסרה בחלונות כמו בחוסר מודעות מספקת מצד מי שכתב את התיעוד להבדל בין השניים.
או, כמו שכתבתי בכותרת, לא מבדילים בין פעולה לא חוסמת לפעולה אסינכרונית.
שחר
הערה:
ביוניקס ההתייחסות לחיבורי TCP/IP הינה, ברובה, במסגרת ההתייחסות הכללית לקבצים. כתוצאה מכך יש לנו שלוש פונקציות שונות (וייתכן שמישהו בתגובות יציין שיש אפילו יותר) שמשנות את ההתנהגות שלהן. הראשונות הן ioctl ו-fcntl, שפועלות על כל סוגי הקבצים, והשלישית היא setsockopt, שרלוונטית רק ל-sockets.
בחלונות ייבאו את מערכת ה-sockets של יוניקס, אבל שם היא רלוונטית רק לתקשורות, ולא לקבצים בכלל. כתוצאה מכך יש התנגשויות וחוסרי בהירויות פוטנציאלים בשמות פונקציות בין פעילויות רשת לבין פעילויות כלליות. התוצאה היא שבעיקרון אפשר להעביר תוכנות ללא שינוי בין המערכות, אבל בפועל יש הרבה פעולות של שינויים סמנטיים ושֵמִיִים לבצע. לצורך הדוגמא, הפונקציה setsockopt קיימת גם בחלונות תחת אותו השם, אבל הפונקציה ioctl נקראת בחלונות ioctlsocket. הפונקציה fcntl לא קיימת בחלונות בכלל, אבל חלק מהדברים שמתבצעים דרכה (כמו לבקש פעולות לא חוסמות) מתבצעים בחלונות דרך ioctlsocket.
שחר
מספר רב של הערות:
זה לא ממש נכון. שוב, צריך לזכור שבד”כ אתה לא ממש צריך פעולות לא חוסמות כשאתה עובד עם select. בד”כ הוא מספיק לך. אם כי זה **מומלץ** לעבוד עם פעולות לא חוסמות. לדוגמה חייבים לעשות פעולה לא חוסמת כדי לעשות connect א־סיכנרוני.
עכשיו מה שכתבת זה לא נכון. ההפיך הוא נכון. אם יש לך פעול לא חוסמת אתה יכול להפוך אותה לא־סיכנרונית בעזרת select או api דומה. זה לא נכון עבור פעולות א־סיכנרוניות. לא ניתן להפוך אותן לפעולות לא חוסמות.
אומנם שיניהם ב־UNIX עובדים עם file dscriptor ועל שניהם ניתן לבצע read/write יש הבדל מהותי בכל הנושא של פעילות לא חוסמות לבין socketים. למעשה, כל הטריקים שאתה יודע על socketים לא יעבדו על קבצים. לדוגמה, select תמיד ידווח על readability של קובץ או read לא ייצא עם EWOULDBLOCK כשאתה מנסה לקרוא מהקובץ, אפילו שהמידע נמצא בדיסק.
פועל, התהליך ייעצר עד שהמידע יובא מהדיסק ואז תצא מ־read. בגלל זה, קשה מאוד לבנות מנגנון א־סיכנרוני שעובד הן עם קבצים והן עם socketים.
כדי לעשות את זה אתה צריך להשתמש ב־aio_read/aio_write וסיגנלים. שזה מממממש לא נחמד.
עכשיו כשמדברים על חלונות, אני מסכים, שמימוש של berkley api ב־windows מה שנקרא “defected by design”. אבל, בחלונות, כמו לכל דבר אחר יש API משלו עבור קבצים, סוקטים ועוד. ה־API הטבעי של חלונות הוא אחיד גם כן, רק עובד בצורה אחרת לחלוטין. יש לו טכניקות שונות לחלוטין כמו Complition Port (שאגב עובד הן על קבצים והן על socketים).
בקיצור. עזוב, חלונות זה עולם אחר, אל תבוא אליו עם דרישות של UNIX… או עדיף בכלל, אל תבוא.
אפשר לעבוד בצורה **מאוד** זהירה עם berkeley sockets ב־Windows אבל לא לצפות ליותר מידי ולהתרגל ל־ifdefים רבים.
במקרה כזה, עדיף:
1. לא לתמוך בחלונות
2. לקחת איזשהו cross platform toolkit כמו Boost.Asio ו… גם לא לתמוך בחלונות 😉
פעולה אסינכרונית חוזרת מייד, ועל כן היא פעולה לא חוסמת. מצד שני, לא כל פעולה לא חוסמת היא אסינכרונית. ההבדל בין fcntl לבין aio_read הוא בדיוק ההבדל שאני מדבר עליו. ההבדל שהתיעוד של מיקרוסופט לא מקפיד עליו.
לא רק connect מתנהג אחרת בפעולות לא חוסמות. אני לא בטוח פה ב-100%, אבל לדעתי אפילו write מתנהג אחרת. במצב לא חוסם הוא לא יחכה לאישור מהצד השני שהדברים התקבלו. שוב, צריך לבדוק את זה – זה מזכרון עמום.
בכל מקרה, מכיוון ש-select הוא level trigger, אכן מותר לפעמים לעבוד עם פעולות חוסמות. עדיין, יש דברים שלא היית רוצה ליפול עליהם. עבודה עם פונקציות לא חוסמות מאוד מאוד מפשטת את הלוגיקה של התוכנית שלך. יש מירוצים שעלולים לגרום לך לקרוא לפונקציה חוסמת למרות ה-select. בכל מקרה, בעבודה עם Edge trigger (כמו חלק מהמודים של epoll), אין לך ברירה אלא לעבוד בצורה לא חוסמת.
לגבי completion ports – מדובר בפעולות אסינכרוניות, ולהן יש את הבעיות שלהן (כפי שציינת). בכל מקרה, לא האשמנו את חלונות שהוא לא יודע לעבוד אסינכרונית. האשמתי אותו שהוא ממליץ על עבודה אסינכרונית במקומות שבהם מה שאתה באמת רוצה זה לעבוד לא חוסם.
שחר
לפני תשע שנים, כתבתי תוכנית proxy שרצה תחת Windows (אז 98). לא התעמקתי כמוך בהבדלים בין המערכות, אלא השתמשתי בפונקציות ה-UNIX (כלומר fcntl ו-setsockopt) ע”י שמוש ב-cygwin. למרות כמה בעיות (למשל – ה-sockets לא עברו לבנים אחרי fork) התוכנית עבדה כרצוי ואפילו איפשרה לי לדבג בעיות עם winsock בתוכניות שנכתבו (ע”י אחרים).
אהוד,
התלונה שלי לא היתה על הטכנולוגיה, אלא על התיעוד ועל ההתייחסות. כמו שכתבתי בגוף הפוסט עצמו, אני יודע שחלונות כן תומך בפעולות חוסמות.
לגבי Cygwin – למרבה הצער זו לא טכנולוגיה שכדאי לבסס עליה יותר מידי. היא עושה את העבודה, אבל כל כך לאט….
למעשה, Cygwin ביחד ללינוקס הרבה יותר איטית מאשר Wine ביחס לחלונות, וזאת על אף שמקמפלים מחדש את התוכנה. עם Wine יש תוכנות מסויימות (למשל כאלו שמתבססות על מנהל הזכרון הרבה) שממש ירוצו יותר מהר מאשר על חלונות.
שחר