יום שלישי

הפונקציה select

נדגים כאן שימוש בפונקציה select.
מה select עושה?
היא מאפשרת המתנה על מספר events בבת אחת. ליתר דיוק, היא בודקת איזה מבין ה-descriptors שאנו מחכים להם מוכן לקריאה או כתיבה.
אפשר דוגמא?
הנה: ניקח process שרץ על client וממתין לארועים משני מקורות:
1. ממתין להכנסת טקסט מה-stdio (מקלדת). כשהטקסט יוקלד הוא יקלט ואז יכתב ל-socket לצורך שליחתו ל-server.
2. ממתין להודעה מה-server. כשזו תגיע היא תשלח לתצוגה.

ללא שימוש ב-select. יכולנו לבצע את שתי המטלות האלה באופן סינכרוני:
1. המתן להכנסת טקסט מהמקלדת. כשהטקסט יוקלד - שלח אותו ל-server ועבור ל2.
2. המתן להודעה מה-server. כשהיא תגיע, שלח לתצוגה ועבור ל-1.

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

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


נבחן שוב את הטיפול הסינכרוני - ללא select. אח"כ נכניס גם את select לתמונה.
הנה דוגמא מהפוסט הקודם - הפונקציה str_cli שב-client. היא טיפלה בקליטת הודעות מה-stdio ומה-server, כפי שתארנו למעלה.

הנה היא שוב לפניכם:


  1. void str_cli(FILE *fp, int sockfd)
  2. {
  3. char sendline[MAXLINE];
  4. struct args args;
  5. struct result result;

  6. while (Fgets(sendline, MAXLINE, fp) != NULL) {

  7. if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) {
  8. printf("invalid input: %s", sendline);
  9. continue;
  10. }
  11. Writen(sockfd, &args, sizeof(args));

  12. if (Readn(sockfd, &result, sizeof(result)) == 0)
  13. err_quit("str_cli: server terminated prematurely");

  14. printf("%ld\n", result.sum);
  15. }
  16. }


נסתכל על השורות המודגשות בצהוב:

שורה 8: התוכנית "נתקעת" Fgets (פונקציה שעוטפת את fgets בהודעות שגיאה). 
שורה 16: התוכנית "נתקעת עם Readn (פונקציה שעוטפת את read ב-loop שקורא n בתים - ראה פרוט בפוסט הקודם + בקבצי המקור שמצורפים לפוסט זה).


והנה המימוש האלטרנטיבי אותו נבחן בפוסט הנוכחי  - הסבר בהמשך:

  1. void
  2. str_cli(FILE *fp, int sockfd)
  3. {
  4. int maxfdp1, stdineof;
  5. fd_set rset;
  6. char buf[MAXLINE];
  7. struct args args;
  8. int n;

  9. stdineof = 0;
  10. FD_ZERO(&rset);
  11. for ( ; ; ) {
  12. if (stdineof == 0)
  13. FD_SET(fileno(fp), &rset);
  14. FD_SET(sockfd, &rset);
  15. maxfdp1 = max(fileno(fp), sockfd) + 1;
  16. Select(maxfdp1, &rset, NULL, NULL, NULL);

  17. if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
  18. if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
  19. if (stdineof == 1)
  20. return; /* normal termination */
  21. else
  22. err_quit("str_cli: server terminated prematurely");
  23. }
  24. Write(fileno(stdout), buf,n);
  25. printf("\n printf %ld %ld n=%d\n", buf[0]);
  26. }

  27. if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
  28. if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
  29. stdineof = 1;
  30. Shutdown(sockfd, SHUT_WR); /* send FIN */
  31. FD_CLR(fileno(fp), &rset);
  32. continue;
  33. }
  34. Writen(sockfd, buf, n);
  35. }
  36. }
  37. }

הפונקציה למעלה מחולקת לשלושה חלקים מודגשים בצבע:
החלק הכתום - הכנת הפרמטרים ל-select וקריאה ל-select. ה-process נתקע ב-select עד שאחד מהארועים לו הפונקציה מחכה קורה.
שני החלקים הצהובים:
החלק הראשון בשורות 19-28: טיפול בהודעות המתקבלות מה-server:
שורה 19: האם הגיעה הודעה מה-server? נסביר את תהליך הבדיקה הזה מיד. אם כן, המשך בטיפול.
שורה 20: קריאת הודעה מה-socket עם Read. Read  אמנם תוקעת את ה-process אבל היא לא צריכה להמתין כי הרי וידאנו שההודעה מחכה.
שורה 26: הדפסה למשך של תוכן ה-buffer שנקלט.
הערה: Read היא פונקציה שעוטפת את read בטיפול במצב שגיאה - ראה פוסט קודם וגם בקבצים המצורפים כאן.

החלק השני בשורות 30-38: טיפול בהודעות המתקבלות מה-fp - בדוגמא שלנו fp תצביע על stdin. 
התהליך מאד דומה לקריאה מה-socket שתוארה למעלה.
שורה 30: האם הארוע הנ"ל קרה? אם כן המשך בטיפול.
שורה 31: קרא. Read אמנם תוקעת את ה-process, אבל כבר וידאנו שיש נתונים לקריאה ב-buffer.
37: כתיבה ל-socket שמכוונת ל-server.


מנגנון הפעלת ה-select:
 נסתכל ה-prototype של select:
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

select מקבלת 5 פרמטרים.
נתעניין תחילה בפרמטרים 2-4. כולם מהסוג fd_set. כל אחד משלושת הפרמטרים האלה מכיל את רשימת ה-descriptors שנרצה לבדוק את מוכנותם לצורך קריאה, כתיבה או קבלת exception.
ליתר דיוק, fd_set הוא בעצם bitmap:
בתחילת התהליך מדליקים את כל הביטים המשוייכים ל-descriptors אותם נרצה לבדוק.
ביצירה מ-select הביטים ידלקו הביטים של ה-descriptors המוכנים לפעולת IO, כך שמדובר על מנגנון value-result: התוצאה מתקבלת באותו מקום בו נכתבה הקונפיגורציה.

select מחזירה את מספר ה-descriptors המוכנים לפעולת io. 

נבחן את select בדוגמא שלמעלה:
שורה 5 - הגדרת משתנה מסוג fd_set בשם rset.
שורה 17 - rset מופיעה כפרמטר השני בקריאה ל-select. כך שהוא אחראי על ארועי IO מסוג read. 

נסקור את תהליך הביצוע של select לפי שלבים:
שלב 1: הכנסת ה-descriptors ל-fd_set.
לפני שנתחיל לשייך, ננקה את rset עם המאקרו FD_ZERO (שורה 11):
FD_ZERO(&rset);

כעת נשייך ל-rset את descriptors אחריהם נרצה לעקוב. השייוך נעשה ע"י המאקרו FD_SET.

שורה 14: משייכים את fp ל-rset.
FD_SET(fileno(fp), &rset);

שורה 15: משייכים את sockfd ל-rset.
FD_SET(sockfd, &rset);

המאקרו מדליק ביט ב-rset.

נתייחס בהזדמנות זו  לפרמטרים הראשון והחמישי של הפונקציה select:
הפרמטר  הראשון,  maxfdp1,  מציין את ה-descriptor עם הערך הכי גבוה, וכך, ע"י הגבלת תחום המספרים, מייעל את תהליך סריקת ה-descriptors.
למעשה, הערך של הפרמטר הוא מקס +1. זה מודגם בשורה 16:

maxfdp1 = max(fileno(fp), sockfd) + 1;


הפרמטר השישי, struct timaval *timeout.
ה-structure נראה כך:

struct timeval  {
  long   tv_sec;          /* seconds */
  long   tv_usec;         /* microseconds */
};

פרמטר זה קובע את זמן ההמתנה של select.
קיימות 2 אפשרויות:
1. timeout = NULL* - המתנה בלי הגבלה.
2. המתן לפי הערכים ב-structure. במקרה שהם 0, לא תתבצע המתנה בכלל.


שלב 2: select מופעלת.
select תתקע את ה-process,  עד שאחד משני ה-descriptors ששייכנו ל-rset יהיה מוכן לקריאה.
ראה שורה 17. היות שהפרמטרים השלישי והרביעי ב-select הם NULL, הארועים היחידים שיגרמו ל-select לצאת מהתקיעה יהיו מסוג read.

שלב 3: זיהוי ה-descriptor.

הפונקציה select אמנם השתחחרה ממצב התקיעה כי לפחות אחד מה-descriptors ששויכו אליה מוכן. כעת צריך לברר מי הוא ה-descriptor שמוכן.
הבדיקה בעזרת המאקרו FD_ISSET.- ראו שורות 19 ו-30, הבודקות את sockfd ו-fp בהתאמה.


לסיכום, נרכז את 4 המאקרואים לטיפול ב-fd_set:

איפוס:
void FD_ZERO(fd_set *fdset);

הוספת שיוך של descriptor:

void FD_SET(int fdfd_set *fdset);

ביטול שיוך של descriptor (לא התעכבנו על זה):

void FD_CLR(int fdfd_set *fdset);

בדיקת מוכנות של descriptor:

int FD_ISSET(int fdfd_set *fdset);



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

הנה קישור להורדת כל קבצי ה-client, כולל קבצי הריצה המקומפלים.
קבצי ה-sever זהים לאלה שנמסרו בפוסט הקודם.


תגובה 1:

  1. הסבר פשוט מעולה!!!!
    ירדת לפרטי פרטים - מדהים.
    תודה רבה

    השבמחק