340 likes | 433 Views
מערכות הפעלה. תרגול 6 – חוטים ב- Linux. תוכן התרגול. מבוא לחוטים ב- Linux כיצד גרעין Linux תומך בחוטים עבודה עם חוטים ב- Linux ספריות המממשות תמיכה בחוטים ב- Linux Linux Threads POSIX Threads API – סקירה בסיסית
E N D
מערכות הפעלה תרגול 6 – חוטים ב-Linux
תוכן התרגול • מבוא לחוטים ב-Linux • כיצד גרעין Linux תומך בחוטים • עבודה עם חוטים ב-Linux • ספריות המממשות תמיכה בחוטים ב-Linux • Linux Threads • POSIX Threads API – סקירה בסיסית • הקשר בין חוטים ותהליכים – כיצד השימוש בחוטים משפיע על פקודות שלמדנו בנושא תהליכים כגון fork() • מבוא לסינכרוניזציה: סינכרוניזציה בגרעין של Linux מערכות הפעלה - תרגול 6
מבוא לחוטים ב-Linux (1) • חוט הוא יחידת ביצוע עצמאית בתוך תהליך. • תהליך ב-Linux יכול לכלול מספר חוטים המשתפים ביניהם את כל משאבי התהליך : מרחב הזיכרון, גישה לקבצים והתקני חומרה, מנגנונים שונים של מערכת ההפעלה. • כל חוט בתהליך מהווה הקשר ביצוע נפרד – לכל חוט מחסנית ורגיסטרים משלו. מערכות הפעלה - תרגול 6
מבוא לחוטים ב-Linux (2) • החוטים נועדו לאפשר ביצוע בלתי תלוי של חלקים מהמשימה של אותו תהליך. • מספר חוטים של אותו תהליך יכולים לרוץ במקביל על מעבדים שונים. • ניתן לשפר את ביצוע תהליך באמצעות שימוש בריבוי חוטים גם על מעבד יחיד – חוט אחד יכול לבצע הוראה חוסמת וחוט אחר ימשיך בביצוע חלק אחר של התכנית. מערכות הפעלה - תרגול 6
מבוא לחוטים ב-Linux (3) • כשתהליך נוצר לראשונה עם חוט יחיד, החוט הראשי (primary thread), חוטים נוספים נוצרים באמצעות קריאת המערכת clone(). • התקשורת בין חוטים של אותו תהליך היא פשוטה ביותר: קריאה וכתיבה למשתנים משותפים. • זהו גם חסרון: יש לתאם את הפעולות בין חוטים הניגשים לאותם משתנים על-מנת למנוע את שיבוש הנתונים • הוספת חוט לביצוע תכנית זולה בהרבה מהוספת תהליך לאותה מטרה, מפני שאינה כרוכה בהקצאת משאבים נוספים כגון זיכרון, גישה לחומרה וכו'. מערכות הפעלה - תרגול 6
מבוא לחוטים ב-Linux (4) • יישומים שמתאימים במיוחד לריבוי חוטים: • תכניות המכילות מספר משימות בלתי תלויות, כגון הדפסת מסמך במקביל לעריכתו. • שרתים המטפלים במספר בקשות בו-זמנית: עדיף להפעיל חוט לכל בקשה מאשר תהליך לכל בקשה. • יישומים המכילים חישובים "כבדים" הניתנים למיקבול, במערכת מרובת-מעבדים: ניצול החומרה לשיפור הביצועים. • יישומים שאינם מתאימים לריבוי חוטים: • תכניות קטנות ופשוטות: תקורה מיותרת מבלי לשפר ביצועים. • יישומים עתירי חישוב במערכת מעבד יחיד: אין שיפור בביצועים. מערכות הפעלה - תרגול 6
תמיכה בחוטים בגרעין Linux (1) • Linux תומכת בחוטים ברמת גרעין מערכת ההפעלה. חוטי המערכת הם למעשה תהליכים רגילים המשתפים ביניהם משאבים כגון זיכרון ,גישה לקבצים וחומרה. • התמיכה בחוטים ב-Linux שואפת להתאים לתקן הכללי של מימוש חוטים במערכות Unix הקרוי POSIX Threads או POSIX 1003.1c • לכל חוט, בהיותו תהליך רגיל, יש מתאר תהליך משלו ו-PID משלו. • עם זאת, המתכנת, בהתאם לתקן POSIX, מצפה שלכל החוטים השייכים לאותו תהליך ניתן יהיה להתייחס דרך PID יחיד – של התהליך המכיל אותם. • פעולות על ה-PID של התהליך צריכות להשפיע על כל החוטים בתהליך • פעולת getpid() בכל חוט צריכה להחזיר אותו PID – של התהליך המכיל את החוט מערכות הפעלה - תרגול 6
תמיכה בחוטים בגרעין Linux (2) • כדי לאפשר את ההתייחסות לכל החוטים באותו תהליך, מוגדר בגרעין של Linux (מגרסת 2.4.X ומעלה) העיקרון של קבוצת חוטים (thread group): • כל החוטים השייכים לאותו תהליך נמצאים בקבוצה אחת. • מתארי התהליכים של כל החוטים באותה קבוצה מקושרים באמצעות שדה thread_group במתאר התהליך. • שדה tgid במתאר התהליך מכיל את ה-PID המשותף לכל החוטים באותה קבוצה. למעשה, זהו ערך ה-PID של החוט הראשון של התהליך. • פעולת getpid() מחזירה את current->tgid. • פעולות על ה-PID המשותף מתורגמות לפעולה על קבוצת החוטים המתאימה ל-PID. • אם לחוט כלשהו יש בנים (תהליכים), הם הופכים להיות בנים של חוט אחר בקבוצת האב לאחר מותו. מערכות הפעלה - תרגול 6
קריאת המערכת clone() (1) תמיכה בחוטים בגרעין Linux • קריאת המערכת clone() מאפשרת לתהליך ליצור תהליך נוסף המשתף איתו משאבים ונתונים לפי בחירה • קריאת מערכת זאת היא הבסיס לספריות התמיכה בחוטים מתוך user mode • תחביר: int clone(int (*fn)(void*), void *child_stack, int flags, void *arg); • פרמטרים: • fn – מצביע לפונקציה שתהווה את הקוד הראשי של התהליך החדש. • כשביצוע הפונקציה fn(arg) מסתיים, נגמר התהליך החדש • child_stack – מצביע לסוף בלוק זיכרון המוקצה לטובת מחסנית של התהליך החדש • תזכורת: המחסנית גדלה לכיוון הכתובות הנמוכות מערכות הפעלה - תרגול 6
קריאת המערכת clone() (2) תמיכה בחוטים בגרעין Linux • flags – מסכת דגלים (OR) הקובעת את צורת השיתוף בין התהליך הקורא והתהליך החדש. להלן מספר דגלים אופייניים: • arg – הפרמטר המועבר לפונקציה fn() בתחילת ביצוע התהליך החדש. • ערך מוחזר: במקרה של הצלחה, התהליך הקורא מקבל את ה-PID של התהליך החדש, אחרת -1. מערכות הפעלה - תרגול 6
קריאת המערכת clone() (3) תמיכה בחוטים בגרעין Linux • בתוך הגרעין, sys_clone() (קובץ גרעין arch/i386/kernel/process.c) משתמשת למעשה בפונקציה do_fork() עליה למדנו בתרגול קודם, ומעבירה לה את הדגלים על-מנת לקבוע לכל משאב אם לשתף אותו או ליצור אותו כחדש. • עם שיפור התמיכה בחוטים ב-Linux, צפויה הוספת דגלים ושינויים ב-clone(). מערכות הפעלה - תרגול 6
עבודה עם חוטים ב-Linux • הספרייה הנפוצה לעבודה עם חוטים ב-Linux נקראת Linux Threads. • המנגנון תואם (באופן מסורבל, חלקי ועם הרבה בעיות) את תקן POSIX Threads. • לדוגמה, מנגנון זה עדיין חושף PID נפרד לכל חוט בקריאה ל-getpid(). • Linux Threads אינה מנצלת את התמיכה הקיימת בגרעין 2.4.X לחוטים, אלא מפעילה מנגנון ותיק (שקיים כבר בגרסאות ישנות של הגרעין) של שיתוף משאבים בין תהליכים המהווים חוטים של אותו יישום. מערכות הפעלה - תרגול 6
חוטים ב-Linux: הדור הבא • Linux Threads תומך, בגרסאות הנוכחיות, בחוטי מערכת Kernel Threads/Lightweight Processes)) בלבד, כלומר חוטים המנוהלים ומתוזמנים ע"י גרעין מערכת ההפעלה. • בגרסאות החדשות התמיכה בחוטים השתפרה משמעותית באמצעות הספריה החדשה: NPTL (Native POSIX Thread Library) • תאימות מלאה ומסודרת ב-POSIX Threads • שיפורים ניכרים בביצועים ביחס ל-Linux Threads • בהמשך, אנו נתמקד במימוש הקיים לפי Linux Threads בלבד. מערכות הפעלה - תרגול 6
שימוש ב POSIX Threads • כדי להשתמש ב-POSIX Threads דרך ספריית ה-Linux Threads יש לבצע את הפעולות הבאות: • בתחילת קוד התכנית (C) יש להוסיף #include <pthread.h> • יש לחבר את התכנית עם הספריה pthread, לדוגמה: gcc –g –o myprog myprog.c –lpthread • הספריה Linux Threads מוסיפה לתוכנית שני שינויים: • מממשת ממשק לעבודה עם חוטים הקרוי POSIX Threads API • משנה את פעולתן של מספר קריאות מערכת הקשורות לעבודה עם תהליכים, כפי שנראה בהמשך • הספרייה מחליפה את ה-wrapper functions של קריאות המערכת הרלוונטיות מערכות הפעלה - תרגול 6
POSIX Threads API (1) • יצירת חוט:pthread_create()- יוצרת חוט חדש המתבצע במקביל לחוט הקורא בתוך אותו תהליך. החוט החדש מתחיל לבצע את הפונקציה המופיעה בפרמטר start_routine ונהרג בסיום ביצוע הפונקציה. • תחביר: int pthread_create(pthread_t *thread, pthread_attr_t *attr, void* (*start_routine)(void*), void *arg); • ערך מוחזר: 0 במקרה של הצלחה, ערך אחר במקרה של כישלון. כמו כן, במקרה של הצלחה מוכנס מזהה החוט החדש למקום המוצבע ע"י thread. מערכות הפעלה - תרגול 6
POSIX Threads API (2) • פרמטרים: • thread – מצביע למקום בו יאוחסן מזהה החוט החדש במקרה של סיום הפונקציה בהצלחה. • attr – מאפיינים המתארים את תכונות החוט החדש, כגון האם החוט הוא חוט מערכת או חוט משתמש, האם ניתן לבצע לו join, כלומר להמתין לסיומו, וכו'. בד"כ נספק ערך NULL המציין חוט מערכת שניתן להמתין לסיומו. • start_routine – מצביע לפונקציה שתהווה את קוד החוט. הערך המוחזר מפונקציה זו במקרה של סיומה הטבעי הינו ערך הסיום של החוט. • arg – פרמטר שיסופק לפונקציה עם הפעלתה. מערכות הפעלה - תרגול 6
POSIX Threads API (3) • סיום חוט:pthread_exit() - החוט הקורא מסיים את פעולתו. ערך הסיום יוחזר לחוט שימתין לסיום חוט זה. • תחביר: void pthread_exit(void *retval); • פרמטרים: • retval– ערך סיום (בדומה לזה של exit()) • ערך מוחזר: אין • סיום פעולת החוט הראשי ע"י pthread_exit()אינו מסיים את כל החוטים בתהליך. מערכות הפעלה - תרגול 6
POSIX Threads API (4) • חוט יכול להסתיים כתוצאה ממספר אפשרויות שונות: • חזרה מהפונקציה הראשית של החוט • קריאה ל-pthread_exit() בתוך קוד החוט • קריאה ל-exit() ע"י חוט כלשהו בקבוצה של החוט המדובר (כולל סיום "טבעי" של החוט הראשי) • הריגת החוט ע"י קריאה ל-pthread_cancel() מחוט אחר כלשהו ביישום מערכות הפעלה - תרגול 6
POSIX Threads API (5) • קבלת מזהה החוט : pthread_self() - החוט הקורא מקבל את המזהה של עצמו. מזהה זה הוא פנימי לספרייה Linux Threads ואינו קשור במישרין ל-PID של החוט. • תחביר: pthread_t pthread_self(); • פרמטרים: אין • ערך מוחזר: מזהה החוט מערכות הפעלה - תרגול 6
POSIX Threads API (6) • המתנה לסיום חוט:pthread_join() - החוט הקורא ממתין לסיום החוט המזוהה ע"י th. • תחביר: int pthread_join(pthread_t th, void **thread_return); • פרמטרים: • th – מזהה החוט שממתינים לסיומו • לא ניתן להמתין ל"סיום חוט כלשהו" בדומה ל-wait(). • thread_return – מצביע למקום בו יאוחסן ערך הסיום של החוט עבורו ממתינים • ניתן לציין NULL כדי להתעלם מערך הסיום מערכות הפעלה - תרגול 6
POSIX Threads API (7) • ערך מוחזר: 0 במקרה של הצלחה, וערך שונה מ-0 במקרה כישלון. כמו כן, במקרה הצלחה מוחזר ערך הסיום מוצבע מ-thread_return (אם אינו NULL). • תכונות: • ניתן להמתין על סיום אותו חוט פעם אחת לכל היותר • ביצוע pthread_join על אותו חוט יותר מפעם אחת ייכשל • כל חוט יכול להמתין לסיום כל חוט אחר באותו תהליך • ההמתנה על סיום החוט משחררת את מידע הניהול של החוט ברמת Linux Threads וברמת הגרעין מערכות הפעלה - תרגול 6
POSIX Threads API (8) • הריגת חוט:pthread_cancel() - סיום ביצוע החוט המזוהה ע"י thread. • ערך סיום הביצוע של החוט שנהרג יהיה PTHREAD_CANCELED • תחביר: int pthread_cancel(pthread_t thread); • פרמטרים: • thread – מזהה החוט המיועד לסיום • ערך מוחזר: 0 במקרה של הצלחה, וערך אחר במקרה כישלון מערכות הפעלה - תרגול 6
ביצוע fork() בתוך חוט חוטים ותהליכים ב-Linux Threads • כאשר חוט קורא ל-fork(), נוצר תהליך חדש שהוא הבן של החוט הקורא בלבד • חוט אחר בקבוצה של החוט הקורא לא יכול לבצע wait() על תהליך הבן שנוצר • לתהליך הבן החדש יש חוטים משלו. בהתחלה, חוט יחיד – החוט הראשי • חוטים נוספים יכולים להיווצר בהמשך בתהליך הבן • גם אם תהליך הבן מכיל יותר מחוט אחד, חוט האב יכול לבצע wait() על תהליך הבן פעם אחת בלבד – להמתין לסיום תהליך הבן, כפי שנראה בהמשך מערכות הפעלה - תרגול 6
ביצוע execv() בתוך חוט אם קריאה ל-execv() מצליחה, החוט הקורא מתחיל מחדש בתור החוט הראשי (והבלעדי) של התהליך כולל הקצאת משאבים מחדש: זיכרון וכו' כל החוטים האחרים מופסקים חוטים ותהליכים ב-Linux Threads (c) ארז חדד 2003 מערכות הפעלה - תרגול 6 מערכות הפעלה - תרגול 6 24
סיום ביצוע תהליך חוטים ותהליכים ב-Linux Threads • אם חוט כלשהו מתוך תהליך קורא ל-exit() או שביצוע אחד החוטים גורם לתקלה לא-מטופלת, מתבצע סיום ביצוע התהליך כולו • כל החוטים בקבוצה מופסקים • כמו כן, כזכור מהתרגול הקודם, לאחר סיום ביצוע קוד החוט הראשי מתבצעת קריאה אוטומטית –לexit() הגורמת לסיום ביצוע התהליך • אם כל החוטים בתהליך מסיימים באמצעות pthread_exit(), אזי סיום החוט האחרון הוא סיום התהליך מערכות הפעלה - תרגול 6
מסלולי בקרה – Control Paths • גרעין מערכת ההפעלה מטפל בבקשות מסוגים שונים(פסיקות, חריגות) ע“י ביצוע סדרת פקודות (פעולות). • סדרת פקודות המתבצעת מהרגע שמתקבלת בקשהועד שמסיימים לטפל בה נקראת מסלול בקרה בגרעין(kernel control path). • לדוגמה מסלול בקרה המטפל בקריאת מערכת של תהליך ב- user mode מתחיל ב-system_call() ומסתיים ב-ret_from_sys_call(). • למסלול בקרה אין descriptor כי הוא לא חוט או תהליך, אלא מסלול ביצוע הוראות בתוך הגרעין. מערכות הפעלה - תרגול 6
מיתוג בין מסלולי בקרה • המעבד יכול למתג בין מספר מסלולי בקרה בתוך הגרעין, למשל בגלל: • ביצוע החלפת הקשר בין תהליכים בגרעין. • תוך-כדי ביצוע מסלול בקרה המטפל בפסיקה כלשהי, יש פסיקה נוספת אשר מחייבת לבצע מסלול בקרה נוסף. • כמו כן, במערכת מרובת מעבדים, מספר מסלולי בקרה מתבצעים במקביל בגרעין. • מבני הנתונים בגרעין עלולים להשתבש כתוצאה מגישה לא מתואמת ממספר מסלולי בקרה, לכן צריך להגן עליהם ע"י סנכרון. מערכות הפעלה - תרגול 6
מערכות עם מעבד בודד-מתי ועל מה צריך להגן? • קריאת מערכת לא-חוסמת מתבצעת בצורה אטומית ביחס לקריאות מערכת אחרות, כך שאם היא לא משתמשת במבני נתונים הנגישים ממסלולי בקרה של פסיקות אחרות, היא יכולה לגשת לנתונים בבטחה. • קריאת מערכת חוסמת (מוותרת על המעבד) חייבת להשאיר את מבני הנתונים בגרעין תקינים לפני שתוותר על המעבד, ובחזרה לביצוע עליה לבדוק שהנתונים לא שונו ע"י מסלול בקרה אחר. • לא תהיה חריגה תוך כדי טיפול בחריגה אחרת (למעט חריגת דף – page fault), אבל יכולה להיות פסיקה תוך כדי טיפול בחריגה. • פסיקה אף פעם לא גורמת לחריגה. • ניתן לחלק את מבני הנתונים לשניים: • נגישים מחריגות בלבד – אין צורך להגן אם משחררים את המשאב לפני קריאה ל-schedule. • נגישים גם מפסיקות וגם מחריגות – יש צורך להגן. מערכות הפעלה - תרגול 6
סנכרון גישה למבני הנתונים • הגנה על מבני הנתונים נעשית ע“י הגדרת קטעים קריטיים וסנכרון גישה אליהם ממסלולי בקרה שונים. • קטע קריטי בגרעין הוא קטע קוד שחייב להתבצע בשלמותו ע"י מסלול הבקרה שנכנס אליו לפני שכל מסלול בקרה אחר יוכל לבצע קטע אשר ניגש לאותם נתונים. • יש מגוון אמצעים להגן על קטעים קריטיים... מערכות הפעלה - תרגול 6
אמצעי סנכרון: הוראות אטומיות • הוראות אטומיות (atomic operations): פעולות עדכון נתונים (read-modify-write) המבוצעות באופן אטומי ברמת המכונה ביחס לכל המעבדים. • הקטע הקריטי מוכל בהוראה האטומית. • הוראות אטומיות מנצלות תמיכה של החומרה, כגון הוראת lock (IA32) הנועלת את ערוץ הגישה של כל המעבדים לזיכרון (memory bus) עד לסיום ההוראה שאחריה. • דוגמה להוראה אטומית: test_and_set_bit(), המדליקה ביט ומחזירה את ערכו הקודם מערכות הפעלה - תרגול 6
אמצעי סנכרון: חסימת פסיקות • חסימת פסיקות מקומית (Local Interrupt Disabling): חסימת הפסיקות במעבד בו מתבצע מסלול הבקרה • אחד האמצעים החשובים ביותר להגנה על קטע קריטי • מאפשר למסלול בקרה להתקדם ללא חשש מקטיעה ע"י מסלולי בקרה של פסיקות אחרות • חסימת הפסיקות לזמן רב עלולה לגרום לפגיעה בביצועים ולאבדן פסיקות חיוניות • בסיום הקטע הקריטי משוחזר ערך דגל הפסיקות מלפני כיבויו, מפני שהדגל לא דלק בהכרח לפני הכיבוי. מערכות הפעלה - תרגול 6
אמצעי סנכרון: סמפורים כללים • סוג של מנעול המיועד לשימוש פנימי בגרעין בלבד: • המתנה של מסלול בקרה לסמפור גורמת להמתנת התהליך שרץ כרגע. • לכן, סמפורים משמשים בקריאות מערכת ואינם משמשים בטיפול בפסיקות אחרות (אחרת תהליך נכנס להמתנה מסיבה שאינה תלויה בו) • טיפוס struct semaphore בקובץ גרעין include/asm-i386/semaphore.h מערכות הפעלה - תרגול 6
מערכות מרובות מעבדים • קריאות מערכת אינן אטומיות לעולם במערכת מרובת מעבדים. • הסנכרון מסובך יותר, בדרך כלל דורש שילוב של אחד ממנגנוני סנכרון שהזכרנו קודם עם מנגנון סנכרון הנקרא Spin Lock - למעשה, מנעול הממומש כ-busy wait. • קיים רק במערכת מרובת מעבדים, מפני שאילולי כן, במערכת מעבד יחיד מסלול בקרה היה נתקל ב-spin lock נעול, והיה עלול להמתין לנצח – כי המסלול שהחזיק את המנעול נקטע ואינו יכול לחזור לרוץ עד שהממתין יסיים. • למעשה, busy wait הוא המתנה יעילה כאשר מדובר בנעילות קצרות מאוד כפי שקורה בגרעין, מפני שאין את התקורה של של כניסה ויציאה מהמתנה. • טיפוס spinlock_t בקובץ גרעין include/linux/spinlock.h מערכות הפעלה - תרגול 6
סיכום: אמצעי הסנכרון עבור מבני נתונים הנגישים מפעולות שונות מערכות הפעלה - תרגול 6