תרגיל תכנות: שימוש באיתותים (signals)
עדיין דורש עדכון לסביבת חלונות!!!
איתותים (signals) בלינוקס ויוניקס
הינם פסיקות וירטואליות שמערכת ההפעלה שולחת לתהליך במצבים מסויימים,
כמו למשל נסיון לגשת לזיכרון לא ממופה, הודעה שהמערכת עומדת להסגר, ועוד.
תכנית שמעוניינת לטפל באחד המצבים האלה מודיעה למערכת ההפעלה איזו שגרה
להפעיל כאשר קורה המצב הזה. למשל, תכנית יכולה לבקש ממערכת ההפעלה להפעיל
שגרה מסויימת בתכנית כאשר התכנית מנסה לגשת לזיכרון לא ממופה, על מנת
לנסות למפות קובץ לדף שגרם לחריג הדף. כאשר שגרה כזאת מסיימת את פעולתה,
מערכת ההפעלה ממשיכה להריץ את התהליך מאותו מקום שבו קרה המצב המיוחד.
(במקרה של גישה לזיכרון לא ממופה, למשל, מערכת ההפעלה תריץ את התכנית
החל מאותה גישה לזיכרון שנכשלה---אם הזיכרון עדיין אינו ממופה הפעולה
תכשל שוב).
לכל מצב מיוחד כזה מערכת ההפעלה מגדירה התנהגות נורמלית למקרה שתכנית
לא מודיעה על שגרה לטיפול בבעיה. ההתנהגות הנורמלית היא התעלמות ממצבים
לא קריטיים והפסקת פעולת התכנית במקרים קריטיים. יש מצבים בעייתיים במידה
כזו שמערכת ההפעלה לא מרשה לתכנית לשנות את ההתנהגות הנורמלית---העפת
התהליך.
רישום שגרת טיפול באיתות:
#include <signal.h>
...
void (*signal(int signum, void (*handler)(int)))(int);
קריאת המערכת signal מודיעה למערכת ההפעלה על השגרה שיש
להפעיל כאשר מתקבל איתות מסוים. לקריאה יש שני ארגומנטים והיא מחזירה
מצביע. הארגומנט הראשון הוא האיתות. בדרך כלל אין לא מציינים בתכנית
ערך שלם מספרי אלא קבוע סימבולי כגון SIGSEGV או SIGTERM.
שמות כל האיתותים מופיעים ב--man 7 signal. הארגומנט השני
הוא כתובת של שגרת הטיפול באיתות. המצביע שהקריאה מחזירה מצביע לשגרה
שהייתה אחראית לטיפול באיתות עד כה (החזרת ערך זה מאפשרת למודול בתכנית
לשנות את שגרת האיתות בזמן שהמודול פועל ולהחזיר את ההתנהגות הקודמת
עם היציאה מהמודול, ללא שהמודול יצטרך לדעת בדיוק מה הייתה ההתנהגות
הקודמת).
שגרות טיפול באיתות מקבלות ארגומנט אחד, ערך שלם, ואינן מחזירות כל ערך.
הערך שהן מקבלות הוא מספר האיתות, ממשק שמאפשר לשגרה אחת לטפל במספר
איתותים.
במקום כתובת של שגרה ניתן גם להעביר את הערכים SIG_IGN
ו--SIG_DFL. הערך הראשון מודיע למערכת ההפעלה שהתכנית
רוצה להתעלם מהאיתות, והשני מודיע שההתנהגות הרצויה היא התנהגות ברירת
המחדל.
סוגי איתותים.
סוגי האיתותים ושמותיהם מופיעים ב--man 7 signal. בדף זה
גם מצויינת ההתנהגות הרגילה של כל איתות.
לצורך תרגיל זה חשוב לדעת על SIGSEGV, איתות שנשלח כאשר
התכנית מנסה לגשת לכתובת לא ממופה, על SIGTERM שנשלח כאשר
מערכת ההפעלה מעוניינת שהתהליך יסיים את פעולתו, ועל SIGKILL
שנשלח כאשר התהליך חייב להסתיים מייד.
שליחת איתות:
#include <signal.h>
#include <sys/types.h>
...
int kill(pid_t pid, int sig);
קריאת המערכת kill שולחת את האיתות sig לתהליך
שמספרו pid. בדרך כלל נציין את האיתות בעזרת קבוע סימבולי
ולא על ידי מספר.
הפקודה kill מאפשרת לשלוח איתותים לתהליכים באופן אינטראקטיבי.
הפקודה מקבלת פרמטר מספרי אופציונלי שלפניו מופיע הסימן -
המציין את מספר האיתות ואחריו מספרי תהליכים שיש לשלוח להם את האיתות.
הפקודה kill -9 1234, למשל, שולחת איתות מספר 9 (SIGKILL)
לתהליך מספר 4321. כאשר לא מציינים את מספר האיתות, ברירת המחדל היא
SIGTERM, בקשה לסיום פעולת התהליך.
שימוש ב--gdb.
במסגרת התרגיל נכתוב תכנית המפעילה מנפה (debugger) בשם gdb.
על מנת של--gdb יהיה מספיק מידע לניפוי התכנית (שמות משתנים
וכתובותיהם, מספרי שורות, וכו'), יש לקמפל את התכנית עם הדגל -g.
ניתן להפעיל תכנית תחת gdb על ידי מתן הפקודה gdb
PROG (כאשר שם התכנית הוא PROG). ניתן גם להתחיל לנפות
תכנית רצה על ידי מתן הפקודה gdb PROG PID כאשר PID
הוא מספר התהליך של התכנית הרצה. הממשק של gdb הוא ממשק
אלפא-נומרי שכולל מספר פקודות, שהשימושיות ביותר מתוכן הן:
שימוש |
פקודה |
תחילת הרצת התכנית |
run |
המשך ריצה מעצירה |
cont |
היכן אנו בתכנית |
where |
הדפסת ערך משתנה |
print |
הריגת התכנית שבודקים |
kill |
יציאה |
quit |
עזרה |
help |
התרגיל.
זהו תרגיל מורכב יחסית שבו נלמד לטפל באיתותים. טיפול מעניין במיוחד
באיתותים שאותו נתרגל הוא הכנסת התכנית לריצה תחת מנפה כאשר מתעורר מצב
חריג בתכנית.
שימו לב לכך שגיאות תכנות בחלק האחרון של התרגיל (5--6) עשויות לגרום
ליצירת מספר רב מאוד של תהליכים. בשל זאת, יש להשתדל יותר מתמיד להמנע
משגיאות. במקרה של יצירת מספר רב של תהליכים, אנא הרוג אותם מחלון אחר.
אם יש באפשרותך לפתח את התרגיל על מחשב שבשימושך בלבד, אנא עשה זאת.
יש להזהר אך אין צורך לחשוש! דרך אחת להגביל את מספר התהליכים שעשויים
להווצר בלינוקס הוא להקטין את החסם על מספר התהליכים שניתן ליצור בעזרת
הפקודה limit maxproc X, כאשר X הוא מספר
התהליכים שניתן ליצור בו זמנית תחת המעטפת (shell).
מפאת מורכבות התרגיל, יש לפתח את התכנית על פי השלבים הבאים (ולהגיש
גם תשובות לשאלות המופיעות):
-
כתוב תכנית בשם ex-sig.c המדפיסה את המחרוזת continue?
ומחכה לקלט y. במקרה של כל קלט אחר יש להדפיס את השאלה
שוב ולחכות לתשובה. לאחר קבלת הקלט הרצוי התכנית ממשיכה. הפעולה הבאה
שלה היא השמת ערך כלשהו בכתובת 0 שלעולם אינה ממופה (למשל על ידי הפקודה
*pointer=1 כאשר pointer==NULL.
- הפעל את התכנית וכאשר היא מחכה לקלט שלח לה SIGTERM מחלון
אחר בעזרת הפקודה kill. את מספר התהליך ניתן לגלות בעזרת
הפקודה ps aux. האיתות הורג את התהליך.
- כעת הוסף לתוכנית שגרת טיפול באיתותים. השגרה צריכה לבדוק את סוג האיתות,
ובמקרה של SIGTERM פשוט להדפיס I will survive.
בתכנית הראשית הוסף קוד שרושם את שגרת הטיפול הזו לאיתות SIGTERM.
נסה כעת להרוג את התהליך על ידי הפקודה kill. מה קורה?
- נסה להרוג את התהליך על ידי שימוש בפקודה kill -9 ( SIGKILL).
מה קורה כעת? האם ניתן לטפל במקרה זה כמו ב--SIGTERM?
- כעת הוסף קוד שרושם את שגרת הטיפול גם עבור טיפול ב--SIGSEGV
והוסף לשגרה בדיקה שתדפיס את אותה המחרוזת גם עבור איתות זה. מה קורה
כאשר התוכנית מגיעה להשמה לכתובת 0?
- כעת נשנה את התנהגות שגרת הטיפול כך שתפעיל את gdb כאשר
התכנית מקבלת איתות על חריג דף (SIGSEGV).
במקרה כזה השגרה צריכה לברר את מספר התהליך באמצעות קריאה ל--getpid
(מוגדרת ב--unistd.h), וליצור תהליך חדש זהה באמצעות fork.
התהליך החדש (הבן) צריך להפעיל בעזרת execv את הפקודה /usr/bin/gdb
ex-sig PID כאשר במקום PID צריך להופיע מספר התהליך.
התהליך המקורי צריך פשוט לחכות שהמנפה יתחבר אליו בעזרת sleep.
- הפעל את התכנית וגרום לה לחריג דף. באיזה נקודה בתכנית מתחבר אליה המנפה?
מה קורה כאשר מבקשים מהמנפה להמשיך את ריצת התכנית?
Copyright Sivan Toledo 2004