לדלג לתוכן

סיגנלים בלינוקס מוסברים - איך תוכנה מקבלת הודעה שהיא לא ביקשה

לחצתם Ctrl+C בטרמינל ותוכנית נעצרה מיד, גם באמצע לולאה אינסופית. הרצתם kill על תהליך תקוע והוא נעלם. תוכנית קרסה עם "Segmentation fault" בלי שאף שורת קוד קראה לזה במפורש. כל אלה קורים דרך מנגנון אחד בקרנל לינוקס - סיגנלים, אחת הדרכים הכי בסיסיות שבהן מערכת ההפעלה מתקשרת עם תהליכים בזמן ריצה, בלי שהם ביקשו את זה.

אז מה זה בעצם סיגנל

סיגנל הוא הודעה אסינכרונית שהקרנל שולח לתהליך, כדי להודיע לו שקרה משהו שדורש תשומת לב. "אסינכרוני" זה המילה החשובה כאן - הסיגנל לא מחכה שהתהליך יבדוק אם משהו קרה, הוא פשוט מפסיק את התהליך באמצע כל מה שהוא עושה, ומעביר לו שליטה כדי לטפל באירוע. זה שונה לגמרי מקריאת פונקציה רגילה, שהתהליך יוזם בעצמו.

לכל סיגנל יש מספר ושם - SIGINT, SIGTERM, SIGKILL, SIGSEGV, ועוד עשרות נוספים, כל אחד עם משמעות משלו. חלקם נשלחים על ידי הקרנל עצמו, כמו SIGSEGV שנשלח כשתוכנית מנסה לגשת לזיכרון שהיא לא אמורה לגעת בו. חלקם נשלחים על ידי תהליכים אחרים או משתמשים, כמו SIGINT שנשלח כשלוחצים Ctrl+C.

מה תוכנית עושה עם סיגנל שהיא מקבלת

לכל סיגנל יש התנהגות ברירת מחדל - action ברירת מחדל שהקרנל מבצע אם התהליך לא הגדיר משהו אחר. עבור רוב הסיגנלים, ברירת המחדל היא סיום התהליך. עבור חלקם, כמו SIGSTOP, ברירת המחדל היא להקפיא את התהליך זמנית.

אבל תוכנית יכולה, במקרים רבים, להירשם על סיגנל ולהגדיר handler משלה - פונקציה שתרוץ במקום ברירת המחדל כשהסיגנל מגיע. זו הדרך שבה שרתים סוגרים חיבורים בעדינות כשמקבלים SIGTERM, במקום פשוט להיעלם באמצע פעולה. הרישום הזה נעשה היום בעיקר עם sigaction, שמחליפה את הפונקציה הישנה יותר signal, כי היא נותנת שליטה עדינה יותר על התנהגות ה-handler.

הסיגנלים שאי אפשר לתפוס בשום מקרה

שני סיגנלים, SIGKILL ו-SIGSTOP, לא ניתן לתפוס, לחסום, או להתעלם מהם בשום צורה. זו החלטת עיצוב מכוונת - היא מבטיחה שלמשתמש עם הרשאות מתאימות תמיד תהיה דרך ודאית לעצור תהליך, גם אם הוא כתוב בכוונה או בטעות כך שהוא מתעלם מכל שאר הסיגנלים. זו הסיבה ש-kill -9, שמשלח SIGKILL, נחשב ל"אקדח" האחרון - כשכלום אחר לא עובד, הפקודה הזאת תמיד תסיים את התהליך.

מסיכת סיגנלים ובטיחות בתוך handler

תהליך יכול לחסום זמנית קבלה של סיגנלים מסוימים - לא להתעלם מהם לצמיתות, אלא לדחות את הטיפול בהם עד לרגע נוח יותר. זה חשוב במיוחד כשמבצעים פעולה קריטית שאסור להפריע לה באמצע.

וגם בתוך handler עצמו יש כלל זהב חשוב - לא כל פונקציה בטוחה לקריאה שם. אם סיגנל מגיע בדיוק באמצע קריאה ל-printf או malloc, וה-handler עצמו קורא לאותה פונקציה, אפשר להגיע למצב לא עקבי ולקריסה. פונקציות שבטוח לקרוא להן בתוך handler נקראות async-signal-safe, ורשימתן מוגבלת בכוונה - בעיקר קריאות מערכת ישירות ומעטות פונקציות ספרייה נוספות.

איפה זה פוגש אתכם ביום יום

מעבר לCtrl+C ולפקודת kill, סיגנלים עומדים מאחורי מנגנונים רבים שנראים "מובנים מאליהם" - איך שרת web יודע לסגור חיבורים בעדינות לפני שהוא נופל, איך דיבאגר כמו gdb עוצר תוכנית באמצע ריצה כדי לבדוק אותה, ואיך תוכנית מתקדמת יכולה לזהות שהיא קרסה בגלל גישה לא חוקית לזיכרון, ולתעד את זה לפני שהיא נעלמת.

זה בדיוק אחד המנגנונים שאנחנו מפרקים בקורס ליבת המחשב - איך הקרנל מתקשר עם תהליכים בזמן אמת, ואיך כותבים קוד שמתמודד עם זה נכון.

ואם סיגנל גרם לכם לבאג מוזר שקשה לשחזר, יש קהילה שכבר נתקלה בזה.

הצטרפו לקהילה בדיסקורד

לסיכום

סיגנל הוא הדרך של הקרנל להפריע לתהליך ולהגיד לו "קרה משהו, תתייחס לזה עכשיו". רוב הסיגנלים אפשר לתפוס ולטפל בהם בעדינות, שניים מהם, SIGKILL ו-SIGSTOP, לא ניתנים לעצירה בשום צורה, וכתיבת handler בטוח דורשת זהירות לגבי אילו פונקציות מותר לקרוא בתוכו. ברגע שמבינים את המנגנון הזה, גם התנהגויות שנראו מסתוריות, כמו קריסה מיידית של תוכנית שנתקעה בלולאה, הופכות להיגיוניות לגמרי.