כל הזכויות שמורות © לצבי מלמד, 2014-2020

הכנות למעבדה - הורדה ופתיחה של קובץ ה- tar

הורד את הקובץ lab10.tar

לאחר מכן, העתק את הקובץ הזה לתיקיה כלשהי, למשל לתיקיית labs ואז פתח אותו. (הפקודות הבאות מבצעות את הפתיחה)

cd labs
tar -xvf lab10.tar
# The following directories should have been created: # lab10 and lab10/semaphore

cd lab10/semaphore

שימוש בסמפורים בתכנית שמדמה "חנות נעליים" - בריבוי תהליכים

מבוא קצרצר

במעבדה זאת נעסוק בשימוש בסמפורים בתוכנית מרובת תהליכים.

מערכות LINUX ונגזרותיו (וגם סביבת Cygwin) - מספקות לנו API לשימוש בסמפורים,שבו נשתמש בתרגיל הזה.

למעשה, קיימים לפחות שני ממשקים - ממשק ישן יותר שנקרא IPC-V וממשק חדש יותר של POSIX - זהו הממשק שבו נשתמש.

חנות הנעליים

בחנות הנעליים רוצים למכור נעליים (דה...).

לקוחות מגיעים לחנות, נכנסים לתא המדידה (fitting room), ושם מודדים נעליים.

כשלקוח רוצה למדוד זוג נעליים מסוים (שמאופיין על ידי דגם model ומספר size) צריך להביא את הזוג הזה מהמחסן (store). אם הלקוח איננו מעוניין בזוג הזה, צריך להחזיר אותו למחסן.

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

למרבה הצער, המשאבים בחנות הנעליים מוגבלים:

ייצוג בתכנה

הקלט לתכנית:

הקלט לתכנית מתקבל בקובץ טקסט. שם הקובץ מתקבל כארגומנט של שורת ההפעלה.

כל שורה בקובץ הקלט מייצגת לקוח, והמבנה שלה הוא:

<Name> [<model> <size> <buy/drop>]+
לדוגמא:

Avi assics 40 0 assics 42 0 addidas 41 1  
השורה הנ"ל מתארת לקוח בשם Avi, שמדד שני זוגות נעליים של assics (תחילה מספר 40 ולאחר מכן מספר 42) ולא לקח אותן (הערך 0) - כלומר הן הוחזרו למחסן.

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

הדמיית השימוש במשאבים

הסימולציה של חנות הנעליים, מתבצעת ע"י הדפסות שונות.

מודפסות הודעות בכל פעם ש: ההדפסות האלו מדמות את השימוש במשאבים השונים (תא מדידה, מחסן, קופה).

תהליכים

כשאנו מריצים את התכנית הראשית (shop.exe)זה מסמל את פתיחת החנות. כל המשאבים מאותחלים למצבם ההתחלתי - תאי מדידה וקופות - אף אחד איננו בשימוש, המחסן - סגור.

(כלומר, במונחים של משאבים: במצב ההתחלתי ישנם שלושה תאי מדידה פנויים, שתי קופות, ואפס מחסן (המחסן איננו פנוי לשימוש).

בכדי לפתוח את המחסן, נצטרך להפעיל (להריץ) את תכנית המחסנאי (store_start). את זה נוכל לעשות מטרמינל נפרד (נוסף).

התהליך הראשי מתחיל לרוץ הוא פותח את קובץ הקלט, וקורא אותו שורה אחר שורה.

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

הכרת התכנית, הקלט והפלט שמעורבים בתרגיל

  1. קוד התכנית - הקובץ shop.c


  2. התירגול הזה בנוי בשלבים. קובץ המקור העיקרי לתכנית הוא shop.c. לפיכך, הקבצים, בהתאם לשלבים השונים שאנו רוצים להדגים יהיו בעלי השמות shop.c-1 (עבור השלב הראשון של התרגיל), shop.c-2 (לשלב השני) וכו'.

    עיין בקובץ shop.c
    (זה הקובץ ההתחלתי, כך שבנקודה זאת shop.c זהה לקובץ shop.c-1).

    רשימת הפונקציות מופיעה בקטע של ה prototype declaration (שורות 26-35):

    הצג/הסתר את הפרוטוטייפ של הפונקציות



    לגבי רוב הפונקציות, מתוך השם שלהן דיי ברור מה הן עושות למשל, enter_fitting, exit_fitting. הפונקציות הקצת יותר מעניינות הן הבאות:
    • main
    • do_shopper - זאת הפונקציה הראשית של כל תהליך בן שנוצר
    • read_next_try - אולי המינוח try אינו מוצלח כל כך - בכל אופן, הכוונה לכל ניסיון מדידה של זוג נעליים. הפונקציה הזאת לפיכך קוראת "שלשה" של שם-המודל/דגם, מידת-הנעל, והאם לקנות/לוותר.

    עיין בקוד של הפונקציה main.

    שים לב לשתי הלולאות. הראשונה קוראת שורות מהקובץ (שמתקבל כארגומנט לתכנית). לכל שורה שהיא קוראת, יוצרת תהליך בן ש"מבצע" את השורה הזאת.

    הצג/הסתר את הלולאה הראשית בפונקציה main



    עיין בזריזות, בקוד של הפונקציה do_shopper.
    הערה: אין צורך להבין את הפרטים של ה-parsing שמתבצע שם, אלא רק בגדול מה עושה הפונקציה.

    הצג/הסתר את הפונקציה do_shopper



  3. קבצי הקלט

  4. כאמור, שמות קבצי הקלט הם shopper.txt1, shopper.txt2, shopper.txt3.

    עקרונית, ההבדל היחידי ביניהם - כמה שורות (שקול לכמה לקוחות) מכיל כל קובץ.

    הריצו את הפקודות הבאות. הפקודה cat כידוע,"שופכת" את תוכן הקובץ ל- std-out. הדגל n- מוסיף מספר שורה לפלט (מספר השורה איננו חלק מהתוכן של הקובץ).

    ls -l shopper.txt[123]  # you should see 3 files
    wc -l shopper.txt[123]  # show how many lines in each file
    cat -n shopper.txt2
    
  5. קומפילציה והרצה של הקובץ shop.c

  6. המטרה לקמפל ולהריץ את הקובץ shop.c בגרסה ההתחלתית שלו (העתק של הקובץ shop.c-1).

    הפקודות הבאות מבצעות את זה - הרץ אותן:

    cp shop.c-1 shop.c  # just in case you modified this file
    gcc -Wall -pthread shop.c -o shop
    ./shop shopper.txt1
    
    עיין "והתוודע" לפלט שנוצר מהרצת התכנית.

    הרץ את התכנית עם שני קבצי הקלט האחרים... צפה מן הסתם שהפלט יהיה ארוך יותר.

הפרת השימוש במשאבים?

אם נעיין בפלט, נוכל לראות שאין הפרה של שימוש במשאבים (חוץ מאשר שימוש במחסן, לפני שהוא נפתח).

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

בכדי שנוכל לראות את ההפרה של השימוש במשאבים, נכניס השהיות. לאחר מכן, נרצה לתקן את ההפרות (כלומר סינכרון השימוש במשאבים בהתאם לדרישות) באמצעות סמפורים.

הכנסת השהיות

מבוא לחלק זה

המטרה שלנו כעת היא להגיע למצב שבו התהליכים השונים (שמיצגים את הקונים בחנות) מפרים את האילוצים על המשאבים.

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

ההדפסות השונות מתארות התחלה וסיום של שימוש במשאבים.

למשל אתם יכולים לעיין כאן בפלט של הרצת התכנית shop עם קונה בודד (הצג/הסתר)



נכניס השהיות שיאפשרו ליצור ההפרות של האילוצים על השימוש במשאבים.

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

לאחר מכן, בשלב הבא, נראה כיצד ניתן להשתמש בסמפורים בכדי להבטיח עמידה באילוצים ובדרישות הסינכרון.

קבועים והשהיות

הקובץ shop.c-2 זהה לקובץ הקודם, פרט להשהיות שהוכנסו בו.

אין צורך להתעמק בהשהיות שהוכנסו. רק אציין שיש מידה של אקראיות בהשהיות (אנו משיגים זאת ע"י שימוש ב getpid()%3 ). לדוגמא:

#define STORE_GET_TIME (3 * (1 + getpid() % 3))

אם בכל אופן תרצו לשנות את ההשהיות, הקבוע העיקרי שמשפיע על כל ההשהיות הוא DELAY_FACTOR :
#define DELAY_FACTOR 2
הקבוע הזה משפיע על כל ההשהיות - אם למשל תגדילו אותו ל 4 (במקום 2) משך כל השהיה יוכפל.

  • קומפילציה והרצה של הקובץ shop.c-2

  • העתיקו את הקובץ shop.c-2 שיהיה כעת הקובץ shop.c שלנו, קמפלו והריצו. בצעו זאת ע"י הפקודות הבאות:
    cp shop.c-2 shop.c
    gcc -Wall -pthread shop.c -o shop
    ./shop shopper.txt3
    

  • "הפרות" של אילוצי המשאבים

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

    כיצד להבחין בקלות יחסית בשימוש במשאבים

    הפקודות הבאות עשויות לסייע, למשל בשימוש בחדר המדידה fitting room:

    #either use the following:
    # keep it for later comparison:
    ./shop shopper.txt3 > no_sem.log3
    grep --color fitting !$
    
    #alternatively, we could grep the output directly:
    ./shop shopper.txt3 |grep --color fitting
    

    Show/Hide screen-shots










    הוספת סמפורים לסנכרון התכנית

    כעת ניגש להוסיף סמפורים לסנכרון התכנית.

    נשתמש בסמפורים על פי POSIX API. סקירה כללית על הסמפורים האלו (הריצו את הפקודה הבאה):

    man 7 sem_overview
    
    1. Add Semaphor declarations

    2. First Note that we already have in our file the following include:
      #include <semaphore.h>
      

      Add the following declaration and global variables, before the 'struct try_n_buy' (line 24):

      //---------------------------------
      // *** SEMAPHORES
      #define SEM_FITTING 3
      #define SEM_STORE 0
      #define SEM_PAY 2
      //---------------------------------
      // semaphores as global variables
      sem_t* sem_fitting;
      sem_t* sem_store;
      sem_t* sem_pay;
      
      

      The '#define's value serve as initial values for the semaphore.

      The sem_t variables, serve as pointers to the semaphores.

      Note: these variables don't really need to be global. It's just simpler in this demo.

      Question:
      If we don't want them to be global, what do we need to do?

      Two options: (a) we pass them as arguements (b) each son-process opens the semaphores by himself.

      It's fairly intuitive to understand the values of SEM_FITTING and SEM_PAY. However, what's the meaning of " #define SEM_STORE 0 "?

      (We'll get back to this in a little while).

    3. Create/Open the semaphores

    4. We have to 'open' the semaphore, pretty much like we open a file.
      There are two types of semaphores:
      • one is in memory - which we will not use in this lab, because they require shared memory.
      • The other one is "named semaphore" - i.e. the semaphore has a name (again, just like a file has a name). the name is "/<someting>" (slash followed by some name). For example, "/sem_fitting".

      Note: at this phase we will NOT add a semaphore for the storage.

      Modify the code, initialize (create) the semaphores

      We already have this prototype defined: (line 55): [the function itself is empty/dummy)

      //prototype:
      void open_all_sem(void);
      //--------------------------
      

      We need to add the following code of the function open_all_sem().
      This function is at the bottom of the file. It is currently "dummy". Add the following code:

      void open_all_sem(void)
      {
      	sem_fitting = sem_open("/sem_fitting", O_CREAT, S_IRWXU, SEM_FITTING); 
      	if (sem_fitting == SEM_FAILED)
      	{
      		perror("failed to open semaphore /sem_fitting\n");
      		exit(EXIT_FAILURE);
      	}
      
      	sem_pay = sem_open("/sem_pay", O_CREAT, S_IRWXU, SEM_PAY); 
      	if (sem_pay == SEM_FAILED)
      	{
      		perror("failed to open semaphore /sem_pay\n");
      		exit(EXIT_FAILURE);
      	}
      }
      

      Add the call to open_all_sem

      Add the follwing call:

      // call to the function
      open_all_sem();
      //--------------------------
      

      היכן להוסיף את הקריאה הזאת?

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

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

      לסיכום, הוסף את הקריאה הזאת בפונקציה main ממש לפני הלולאה הראשונה שבה האב יוצר את הבנים (שורה 83 בערך, לפני לולאת ה WHILE).

      A short explanation about the arguments to sem_open(...)
      • "/sem_fitting" - the name of the semaphore.
      • O_CREAT - create this semaphore, if it does not exist.
      • S_IRWXU - permissions. Similar to file permissions [ when calling open() ]. Here we give all permissions, i.e. READ-WRITE-EXECUTE to the USER who runs this process(the 'X' for eXecute is meaningless).
      • SEM_FITTING - (integer) initial value.

      Instructions:

      1. Add the prototype of open_all_sem().[ do nothing, it's already there ]
      2. Add the function (e.g. at the bottom of the file).
      3. Add actual calls to the semaphore

      4. There are two basic operations we can do on the semaphore:

        1. wait - call to sem_wait(...)
        2. post - call to sem_post(...) (this is also known sometimes as signal).

        Add the following calls in the "right place":

        	sem_wait(sem_fitting);
        	sem_post(sem_fitting);
        	sem_wait(sem_pay);
        	sem_post(sem_pay);
        

        So, what is the right place?

        we call 'wait' before we use the resource, and we call 'post' or 'signal' after we are done using the resource. So, in our case, call sem_wait(sem_fitting) before we enter the fitting room, and sem_post(sem_fitting) right before we leave this room.

        Note: we are using the global variables here. So we don't have to pass them as arguments.

      5. Compile and Run

      6. Note: We need to add a special flag to the compilation - either -lrt (it didn't work for me) or -pthread.
        gcc -Wall -pthread shop.c -o shop
        ./shop shopper.txt3
        

        Or you can run the following commands:

        # or run the following
        ./shop shopper.txt3 > sem_a.log3
        grep --color fitting sem_a.log3
        grep --color pay sem_a.log3
        

        Verify that at most 3 shoppers are in the fitting room and at most 2 shoppers are paying in parallel.

        Show/Hide screen-shots





      7. Add semaphore for the storage room

      8. Recall that the storage room can serve at most one customer.
        Furthermore, it is not open right away when the shop is open, so the initial status of the semaphore is zero. That's why we have the following #define:
        #define SEM_STORE 0
        


        We need to add the following code to shop.c (but wait a minute with the editing):

        1. In function open_all_sem:

        2. 	if (sem_unlink("/sem_store")==0)
          		fprintf(stderr, "successul unlink of /sem_store\n");
          	sem_store = sem_open("/sem_store", O_CREAT, S_IRWXU, SEM_STORE); 
          	if (sem_store == SEM_FAILED)
          	{
          		perror("failed to open semaphore /sem_store\n");
          		exit(EXIT_FAILURE);
          	}	
          

        3. The function: get_from_store:

        4. Show/Hide the function: get_from_store



        5. The function: return_to_store:

        6. Show/Hide the function: return_to_store


        מה עושה הפונקציה sem_trywait ומה ההבדל בינה לבין הפונקציה sem_wait ?

        ראשית, תשובת בית-הספר היא כמובן לקרוא ב man-page. עשו את זה!

        בקצרה, הקריאה sem_wait היא קריאה חוסמת. אם הערך של הסמפור הוא אפס, אנחנו נחסמים עד שערכו יעלה (כלומר, מישהו עשה sem_post). יתכן, שאנחנו לא רוצים להחסם, עבור זה נועדה הקריאה sem_trywait. אם ערכו של הסמפור גדול מאפס, אז לא נחסם, הסמפור יקטן ב-1, בדיוק כמו wait. לעומת זאת, אם ערכו הוא אפס, אזי נחזור מיד, ונקבל אינדיקציה על כך, באמצעות הערך המוחזר מהפונקציה.

      9. Update the code and compile

      10. You don't need to edit the code of shop.c. Simply copy the file as shown below:
        cp  shop.c-3 shop.c
        gcc -Wall -pthread shop.c -o shop
        

        Question:
        We are going to run the program. How do you expect it to behave?

        Run the following command:

        ./shop shopper.txt3
        

        What's going on?
      11. Examine the 'processes'

      12. While our program seems to be stuck in one terminal, open another terminal , and run the command 'ps'. 'ps' stands for 'process status'. It has tons of options, we'll use the basic form:
        ps u
        # or the following form:
        ps u -H
        
        Do you see something similar to the following screenshot?

        Show/Hide the 'ps' screenshot

        Note: in the past the name of the program was shoe_store , now it is shop, so you'll see something very similar except for this diff.

        Note:
        Arrange the two terminals on the screen so that you can see both of them.

      13. Start the storage guy

      14. The store guy is kind of lazy. We're stuck because we wait for him to start working (open the store).

        In terms of S/W - what is the meaning of that?

        We want him to signal that it's ok to get into the storage. i.e. to post or signal the /store semaphore.

        1. Examine the code of the file start.c (it's short).
        2. compile. Run the following command: (on the second terminal... the first one is still stuck)

        3. gcc -Wall -pthread shop_start.c -o shop_start
          

          Make sure you see both terminals. In one terminal, our program is stuck.. or waiting for some event to happen... On the other terminal, you just compiled start.

        4. Now it's time to run this program:

        5. ./shop_start
          

          Observe what is happening. Explain.

      15. Rerun the program

      16. Suppose we want to rerun the program. Do we need to start the store again?

        First think, and then Rerun and observe.

        Apparently we're stuck again. Rerun the start.

        Qustion: We can run the following command from the bash. Is this going to help?

        ./shop_start ; ./shop shopper.txt2
        
      17. Remove the sem_unlink

      18. Comment out the following lines, in the function open_all_sem (line ~274 and on):
        	if (sem_unlink("/sem_fitting")==0)
        		fprintf(stderr, "successul unlink of /sem_fitting\n");
        	................
        	if (sem_unlink("/sem_store")==0)
        		fprintf(stderr, "successul unlink of /sem_store\n");	
        

        What is going to be the impact of that?

        Save and compile.

        Run again the above combined command, i.e.

        ./shop_start ; ./shop shopper.txt2
        
      19. Removing the semaphores - from another program

      20. If we did not remove the semaphores (by running sem_unlink) they stay in the kernel - for good... Well, not for good, only until we reboot the machine. This will be clarified and demonstrated by the discussion below.

        The program might get stuck now. In such a way, that even if you run ./shop_start will not help.

        If this is the case, compile and run the program unlink_sem:

        # see the code of the program
        cat unlink_sem.c  
        #compile it
        gcc -Wall -pthread unlink_sem.c -o unlink_sem
        #run it
        ./unlink_sem
        

        Now you can rum shop (run shop_start after launching shop).

        You should be able to run shop consecutively.

        This is good. Why it happens? Because the previous run of start left the store semaphore, in the value of 1. So, because we did not remove the semaphore (by calling sem_unlink) we can continue with the same semaphore.

        This raises a question: is this really how we want the program to behave?

      21. Abnormal termination of the program

      22. Another question: suppose the program does not terminate normally... there was "core dump". Or the program was killed by CNTL+C, or by "kill" command... what will happen?

        You can try this - run the program, and kill it with CNTL+C. Now run it again, it's probably stuck. Now, even if you run start - won't help. You'll need the clean up the semaphores (run ./unlink_sem ), and start over again.