נדגים כאן שימוש בפונקציה 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, כפי שתארנו למעלה.
הנה היא שוב לפניכם:
- void str_cli(FILE *fp, int sockfd)
- {
- char sendline[MAXLINE];
- struct args args;
- struct result result;
- while (Fgets(sendline, MAXLINE, fp) != NULL) {
- if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) {
- printf("invalid input: %s", sendline);
- continue;
- }
- Writen(sockfd, &args, sizeof(args));
- if (Readn(sockfd, &result, sizeof(result)) == 0)
- err_quit("str_cli: server terminated prematurely");
- printf("%ld\n", result.sum);
- }
- }
נסתכל על השורות המודגשות בצהוב:
שורה 8: התוכנית "נתקעת" Fgets (פונקציה שעוטפת את fgets בהודעות שגיאה).
שורה 16: התוכנית "נתקעת עם Readn (פונקציה שעוטפת את read ב-loop שקורא n בתים - ראה פרוט בפוסט הקודם + בקבצי המקור שמצורפים לפוסט זה).
והנה המימוש האלטרנטיבי אותו נבחן בפוסט הנוכחי - הסבר בהמשך:
- void
- str_cli(FILE *fp, int sockfd)
- {
- int maxfdp1, stdineof;
- fd_set rset;
- char buf[MAXLINE];
- struct args args;
- int n;
- stdineof = 0;
- FD_ZERO(&rset);
- for ( ; ; ) {
- if (stdineof == 0)
- FD_SET(fileno(fp), &rset);
- FD_SET(sockfd, &rset);
- maxfdp1 = max(fileno(fp), sockfd) + 1;
- Select(maxfdp1, &rset, NULL, NULL, NULL);
- if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
- if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
- if (stdineof == 1)
- return; /* normal termination */
- else
- err_quit("str_cli: server terminated prematurely");
- }
- Write(fileno(stdout), buf,n);
- printf("\n printf %ld %ld n=%d\n", buf[0]);
- }
- if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
- if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
- stdineof = 1;
- Shutdown(sockfd, SHUT_WR); /* send FIN */
- FD_CLR(fileno(fp), &rset);
- continue;
- }
- Writen(sockfd, buf, n);
- }
- }
- }
הפונקציה למעלה מחולקת לשלושה חלקים מודגשים בצבע:
החלק הכתום - הכנת הפרמטרים ל-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.
ליתר דיוק, 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.
הפרמטר הראשון, 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 fd, fd_set *fdset);
ביטול שיוך של descriptor (לא התעכבנו על זה):
void FD_CLR(int fd, fd_set *fdset);
בדיקת מוכנות של descriptor:
int FD_ISSET(int fd, fd_set *fdset);
|
הרצת התוכנית:
מדובר באותה תוכנית שהוצגה בפוסט הקודם, כשרק הפונקציה str_cli ב-client הוחלפה.
הנה קישור להורדת כל קבצי ה-client, כולל קבצי הריצה המקומפלים.
קבצי ה-sever זהים לאלה שנמסרו בפוסט הקודם.
הסבר פשוט מעולה!!!!
השבמחקירדת לפרטי פרטים - מדהים.
תודה רבה