1 of 52

מערכות הפעלה�89-231�תרגול מספר 2

קרדיטים: אבשלום אלמלח, חן חג'ג', שני אלקובי, פריאל לוי, מיכל חבאני, גילעד מדמון, אופיר הניג, שמעון כהן, חן רוזנשטיין

2 of 52

היום

מבוא לתהליכים

1

fork

2

exec

3

wait

4

3 of 52

  • תוכנית: רצף של הוראות בשפת תכנות כלשהי.
  • תהליך (process): אבסטרקציה של תכנית, פעולות שהתכנית מבצעת לפי הסדר.
  • Multiprogramming: ישנם מספר תהליכים שצריכים להתבצע.
  • Scheduling: איזה תהליך ירוץ על המעבד ומתי.
  • PCB: המערכת שומרת לכל תהליך הקשר משלו כדי שתוכל להריץ ולהחליף בין מספר תהליכים במקביל
    • הסביבה של התהליך (הזכרון שלו, זהות והרשאות, מצב הרגיסטרים עבור התהליך) שבה הוא חי.

תהליכים

4 of 52

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

ייצוג של תהליך

5 of 52

  • רשימת משאבים זמינים
      • מערכת ההפעלה מחזיקה רשימה של משאבים שהתהליך ביקש וקיבל גישה אליהם
        • משאבים כמו קבצים, ערוצי תקשורת, התקני קלט/פלט
      • גישה למשאב עוברת דרך מערכת ההפעלה
      • אם לתהליך יש הרשאה לגישה למשאב, מערכת ההפעלה מחזירה לתהליך "מזהה משאב" (File descriptor)
  • מצב תזמון וסטטיסטיקות שונות
      • מערכת ההפעלה זוכרת עבור תהליך שאינו רץ מדוע אינו רץ
      • שומרת נתונים על פעילות התהליך בעבר
        • לצורך קבלת החלטות בנוגע לתזמון התהליך וניהול הזיכרון הווירטואלי שלו
  • זהות והרשאות

ייצוג של תהליך - המשך

6 of 52

  • PID – process identifier, מספר יחודי שמתקבל בזמן יצירת התהליך.

  • UID – user identifier, מספר יחודי של המשתמש שמריץ את התהליך.

  • PPID – parent PID, המזהה של התהליך שיצר את התהליך הנוכחי.

  • Mode – מצב התהליך (מוכן, ממתין, וכו'...)

  • Priority – מספר המסמל את עדיפות הרצת התהליך. הרחבה בהמשך.

ייצוג של תהליך - המשך

7 of 52

ניתן להסתכל על תהליך כישות שיכולה להימצא באחד מהמצבים הבאים:

State של תהליך

8 of 52

Process Table

9 of 52

תהליך הBOOT של מערכת ההפעלה

10 of 52

  • כאשר מערכת ההפעלה נטענת הגרעין (kernel) יוצר 2 תהליכים:
    • swapper: pid=0, אחראי על ניהול זכרון
    • systemd/init: pid=1, כל התהליכים צאצאים שלו.
  • בלינוקס כדי לייצר תהליך משתמשים בפקודה fork()
  • התהליך שנוצר הוא העתק של התהליך שיצר אותו
    • התהליך המקורי, היוצר, נקרא אב (או הורה)
    • התהליך החדש נקרא בן

איך תהליך נוצר

11 of 52

  • קריאת המערכת fork()
    • תחביר:

#include <sys/types.h>

#include <unistd.h>

pid_t fork()

    • פעולה: משכפלת את תהליך האב לתהליך הבן וחוזרת בשני התהליכים: התהליך המקורי שקרא ל-fork והתהליך החדש שנוצר. לתהליכים יש:
      • קוד זהה (נמצאים באותה נקודה בתוכנית, מיד אחרי הקריאה ל fork)
      • זיכרון זהה (משתנים וערכיהם)
      • סביבה זהה (קבצים פתוחים, file descriptors, ספרית עבודה נוכחית)
      • PID שונה
    • פרמטרים: אין
    • ערך מוחזר שונה כדי שאפשר יהיה להבדיל בין שני התהליכים:
      • במקרה של כישלון: -1 לאב (אין בן)
      • במקרה של הצלחה: לבן מוחזר 0 ולאב מוחזר ה-pid של הבן

fork

12 of 52

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

  • מנגנון copy on write
    • ייחודי ליוניקס/לינוקס.
    • מאפשר להשתמש בדפים משותפים לשני התהליכים עד לנקודת הזמן שבה אחד מהתהליכים מבקש לשנות דף, ואז מערכת ההפעלה מעתיקה את הדף הזה כך שלכל תהליך יהיה עותק פרטי משלו, ורק אז מאפשרת לתהליך לכתוב על הדף.

fork - המשך

13 of 52

מנגנון copy on write

14 of 52

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

void main()

{

pid_t pid;

if ((pid = fork()) == 0)

printf("1");

else

printf("2");

printf("3");

}

  • פלט: 2133 או 1233 או 2313 או 1323 או 23 (במידה והפקודה fork נכשלה)

נציין כי 3 לא יוכל להיות מודפס ראשון (לדוגמא, הפלט 3312 אינו אפשרי)

Parent

Child

Print “1”

Print “2”

Print “3”

Print “3”

fork - דוגמא

15 of 52

  • נניח כי קיים תהליך המבצע את הקוד הבא:

fork();

fork();

fork();

כמה תהליכים (בנים) סה"כ ייווצרו?

שאלה

16 of 52

fork();

fork();

fork();

תשובה

17 of 52

  • כאשר אב מסיים את פעולתו והבן עדיין חי אז הבן נחשב orphan.
  • התהליך init מאמץ את כל היתומים (ה-PPID שלהם יהיה 1)

יתומים ואימוץ

18 of 52

int main(){

int i;

for (i = 0; i < 3; i++)

if (fork() == 0)

while(1);

}

gcc -o example2 2_6.c

./example2

ps

UID PID PPID NI STAT TT TIME COMMAND

...

8385 1668 1 20 R pts/0 0:12 example2

8385 1669 1 20 R pts/0 0:14 example2

8385 1670 1 20 R pts/0 0:13 example2

kill -KILL 1668 1669 1670

X

X

X

i

i

i

1668

1669

1670

State of a process:

S – interruptible sleeping (process is waiting for an event to complete).

R - Running or runnable (on run queue)

Z - Zombie (process terminated and parent not waiting)

T - Stopped.

מציג את רשימת התהליכים הפעילים ע"י היוזר המחובר

fork - דוגמא

19 of 52

  • pid_t getpid()

מחזיר את המזהה של התהליך הקורא

  • pid_t getppid()

מחזיר את מזהה תהליך האב של התהליך הקורא

  • uid_t getuid()

מחזיר את מזהה המשתמש של התהליך הקורא

  • uid_t getgid()

מחזיר את מזהה הקבוצה של התהליך הקורא

פונקציות שימושיות

20 of 52

#include<sys/types.h>

#include<unistd.h>

#include<stdio.h>

int main()

{

pid_t val;

printf("PID before fork: %d\n",(int)getpid());

val = fork();

if(val>0)

printf("parent PID: %d\n",(int)getpid());

else if(val ==0)

printf("child PID: %d\n",(int)getpid());

else {

//if val==-1 print error to screen

}

}

דוגמא ל-fork

Output

21 of 52

  • כמה תהליכים ייווצרו לאחר הרצת קטע הקוד הבא? נמקו תשובתכם.

pid_t pid;

pid = getpid();

while (fork() == 0) {� if (pid == getpid())

break;�}

דוגמא

22 of 52

תשובה

  • התהליך המקורי קורא ל- fork(), יוצר בן ולאחר מכן מסיים (כי הערך שחוזר אצלו מה- fork() שונה מאפס).
  • תהליך הבן שנוצר נכנס לתוך הלולאה ואצלו לא מתקיים התנאי pid = getpid(), כיוון שמזהה התהליך שלו שונה מהמזהה של תהליך האב. לכן הוא חוזר לראש הלולאה וקורא שוב ל- fork(). כתוצאה מכך, ייווצר תהליך נכד חדש ותהליך הבן יסתיים (כי שוב הערך שחוזר אצלו מה- fork() שונה מאפס).
  • תהליך הנכד נכנס ללולאה ושוב התנאי pid == getpid() לא מתקיים, לכן הוא יחזור לראש הלולאה, וכך הלאה. בסופו של דבר ייווצרו אינסוף תהליכים.

דוגמא

23 of 52

#include<sys/types.h>

#include<unistd.h>

#include<stdio.h>

#include<stdlib.h>

int glob = 6; /* global variable */

char buf[] = "a write to stdout";

int main(void)

{

int var; /* automatic variable on the stack */

pid_t pid;

var = 88;

if(puts(buf)==EOF)

printf("error in writing to stdout");

printf("before fork\n");

if ( (pid = fork()) < 0)

printf("fork error");

else{

if (pid == 0)

{ /* child */

glob++; /* modify variables */

var++;

}else

{ /* parent */

sleep(2);

}

printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);

exit(0);

}

}

דוגמא (t2_1.c)

Output

24 of 52

  • נניח שנרצה להריץ תוכנית מסוימת מתוך תהליך כלשהו. נוכל ליצור תהליך נוסף, ע"יfork() , אבל התהליך הזה יהיה זהה לתהליך ממנו נעשתה הקריאה.
  • exec טוענת תוכנית חדשה לביצוע במקום התהליך הקורא.
  • פקודת execl:

#include <unistd.h>

int execl(char *pathname, char *arg0,…., NULL)

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

משפחת exec

25 of 52

  • ערך מוחזר:
    • במקרה של כישלון: -1
      • כדי להודיע לתוכנית הקוראת על הכישלון

    • במקרה של הצלחה: הקריאה אינה חוזרת
      • כי exec למעשה החליפה את התוכנית הנוכחית בתוכנית החדשה

משפחת exec

26 of 52

  • פקודה זו מדפיסה את התוכן של ספריה מסויימת.

  • במידה ונריץ את הפקודה ללא פרמטרים, היא תדפיס את תוכנה של הספריה הנוכחית

ls ��

  • שימוש בפורמט הדפסה ארוך.

ls –l

  • מיון לפי תאריך השינוי האחרון.

ls -t

ls - תזכורת

27 of 52

#include<stdio.h>

#include<unistd.h>

int main()

{

execl("/bin/ls","/bin/ls","-l", NULL);

printf("can only get here on error\n");

}

execl - דוגמא

28 of 52

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

  • משתנה הסביבה PATH – הינו מחרוזת המורכבת משמות תיקיות המופרדות בנקודותיים, למשל DIR1:DIR2:DIR3.
  • ה PATH-מיועד לכלול שמות של תיקיות בהן נמצאים קבצי הרצה.
  • בעת ניסיון הרצת פקודה, המערכת בודקת האם קובץ הריצה נמצא באחת מתיקיות הPATH-. במידה וכן, הקובץ יורץ (מבלי שהזנו את הנתיב האבסולוטי שלו).

משתני סביבה (environment variables)

29 of 52

קיימים מספר וריאנטים לפונקציה: exec{l,v}{optional: e,p}

l - command-line arguments are passed individually (a list) to the function.

v - Command-line arguments are passed to the function as an array (vector) of pointers.

e - an array of pointers to environment variables is explicitly passed to the new process image.

p - Uses the PATH environment variable to find the file named in the file argument to be executed.

משפחת exec

30 of 52

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

-look for the command in the path

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

-look for the command in the path

int execle(const char *path, const char *arg , ..., char * const envp[]);

int execve(const char * path, char *const argv [], char *const envp[]);

-pass the environment

משפחת exec

31 of 52

execl("/bin/ls", “/bin/ls", "-r", "-t", "-l", NULL);

execlp("ls", "ls", "-r", "-t", "-l", NULL);

char *args[] = {"/bin/ls", "-r", "-t", "-l", NULL };

execv("/bin/ls", args);

char *args[] = {"ls", "-r", "-t", "-l", NULL };

execvp("ls", args);

משפחת exec – דוגמאות

32 of 52

#include<stdio.h>

#include<unistd.h>

int main() {

char *argv[] = {“date”, NULL};

execv(“/bin/date”, argv);

printf(“hello”);

return 0;

}

execv - דוגמא

Output

33 of 52

#include <unistd.h>

#include <stdio.h>

int main(void)

{

int status;

char *argv[] = { "/bin/env", 0 };

char *envp[] =

{

"HOME=/",

"PATH=/bin:/usr/bin",

"TZ=UTC0",

"USER=beelzebub",

"LOGNAME=tarzan",

0

};

status=execve(argv[0], &argv[0], envp);

printf(“exec failed\n”);

return -1;

}

משפחת exec (t2_2.c)

Output

34 of 52

מה ההורה עושה בזמן שהילד רץ?

ממתין לילד שיסיים.

ממשיך לבצע פעולותיו.

wait

35 of 52

#include <sys/types.h>

#include<sys/wait>

pid_t wait(int *status)

בקריאה ל-wait, התהליך הקורא (האב) ימתין עד שאחד מבניו יסתיים.

מה status יכיל?

מספר ממנו ניתן לחלץ מידע על סטטוס התהליך.

wait

36 of 52

  • WIFEXITED(status) returns true if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning from main().
  • WEXITSTATUS(status) returns the exit status of the child. This consists of the least significant 8 bits of the status argument that the child specified in a call to exit(3) or _exit(2) or as the argument for a return statement in main(). This macro should only be employed if WIFEXITED returned true.
  • WIFSIGNALED(status) returns true if the child process was terminated by a signal.
  • WTERMSIG(status) returns the number of the signal that caused the child process to terminate. This macro should only be employed if WIFSIGNALED returned true.

Inspecting status information

37 of 52

  • WCOREDUMP(status) returns true if the child produced a core dump. This macro should only be employed if WIFSIGNALED returned true.
  • WIFSTOPPED(status) returns true if the child process was stopped by delivery of a signal;
  • WSTOPSIG(status) returns the number of the signal which caused the child to stop. This macro should only be employed if WIFSTOPPED returned true.
  • WIFCONTINUED(status) (since Linux 2.6.10) returns true if the child process was resumed by delivery of SIGCONT.

Inspecting status information

38 of 52

fork()

wait()

exec()

exit()

child

parent

New program execute

Parent waits

signal

Parent resumes

wait flow

39 of 52

#include <unistd.h>

#include <sys/types.h>

#include<stdio.h>

int main()

{

pid_t pid;

int stat;

if ((pid = fork()) == 0)

printf("1\n"); �else �{

wait(&stat);

printf("2\n");

}

}

wait – דוגמא 1

1

2

Output

40 of 52

int main()

{

int status;

pid_t pid, pid1, pid2;

if ((pid1 = fork()) == 0)

printf("in child 1\n");

else

if ((pid2 = fork()) == 0)

printf("in child 2\n");

else

{

pid = wait(&status);

if (pid == pid1)

printf("child 1 finished\n");

if (pid == pid2)

printf("child 2 finished\n");

pid = wait(&status);

if (pid == pid1)

printf("child 1 finished\n");

if (pid == pid2)

printf("child 2 finished\n");

}

}

wait – דוגמא 2

41 of 52

int main(int argc, char* argv[])

{

int stat,waited,ret_code;

pid_t pid;

pid = fork();

if (pid == 0)

{ /* Child */

ret_code = execvp(argv[1],&argv[1]);

if (ret_code == -1)

{

perror("exec failed ");

exit(-1);

}

else

printf(“Banana”);

}

else

{ /* Parent */

printf("Father: after fork, son proc id is %d \n",pid);

waited = wait(&stat); /* stat can tell what happened */

printf("Father: Son proc completed, id is %d \n", waited);

}

}

execvp + wait

42 of 52

execvp + wait

43 of 52

  • תהליך יחיד ראשון systemd/init נוצר ומקבל PID=1.
  • במהלך ריצתו הוא יוצר תהליך נוסף שעתיד להתחרות בו על משאבי המערכת, נאמר PID=2.
  • מערכת ההפעלה תחליף בין התהליך PID=1 וכל הנתונים הנלווים לו לבין PID=2 תוך שמירה על חציצה בין התהליכים.
  • Context Switching - לפני שתהליך "מורדם" נתוניו נשמרים ולפני שהוא הוא "מתעורר" נטענים אותם נתונים לאותם מקומות בדיוק.
  • בשעת ריצת התהליך, הוא רוצה את כל משאבי המערכת שנחוצים לריצתו. אם משאב נחוץ תפוס ע"י תהליך אחר הוא יורדם שוב עד לתור הבא לניסיון ריצתו.
  • תהליך יורדם לרוב לאחר בקשת I/O בשל הצפי למהירות גישה איטית מרכיבי החומרה.

ריבוי תהליכים ו-Context Switching

44 of 52

  • נאמר כי התהליך שיצר הינו ה-"אב" של תהליך ה-"בן"�(ראינו שאב יכול לייצר מספר רב של בנים)
  • תהליך ה-"בן" הינו עותק מלא של האב
  • תהליך בן יכול לבצע כל פקודה ללא קשר לאביו, אך הינו בעל ייחוס זהה
      • למשל, על ידי קריאת המערכת execv()
  • תהליך אב רשאי לפקח על הבנים שלו, אך לא על "הדור הבא" או על "אחים"
    • לדוגמא ע"י שימוש ב-wait() מסוגיו השונים

מבט על fork

45 of 52

#include <sys/types.h>

#include<sys/wait>

pid_t waitpid(pid_t pid, int *status, int options)

pid:

    • > 0 means wait for the child whose process ID is equal to the value of pid.
    • < -1 means wait for any child process whose process group ID is equal to the absolute value of pid.
    • -1 means wait for any child process.
    • 0 means wait for any child process whose process group ID is equal to that of the calling process.
  • Options:
    • WNOHANG returns immediately if no child has exited.
    • WCONTINUED also returns if a stopped child has been resumed by delivery of SIGCONT.

waitpid

46 of 52

int main(void){

pid_t pid;

if ((pid = fork()) < 0)

printf("fork error");

else {

if (pid == 0) { /* first child */

printf("first child\n");

if ((pid = fork()) < 0)

printf ("fork error");

else {

if (pid > 0){

/* parent from second fork == first child */

if (waitpid(pid, NULL, 0) != pid)

printf("waitpid error");

exit(0);

}

/* We're the second child; */

sleep(2);

printf("second child, parent pid = %d\n", getppid());

exit(0);

}

}

if (waitpid(pid, NULL, 0) != pid) /* wait for first child */

printf("waitpid error");

/** We're the parent (the original process); we continue executing, knowing that we're not the parent of the second child.*/

printf("original parent done\n");

exit(0);

}

}

דוגמא - waitpid

47 of 52

#include<stdlib.h>

void _exit(int status); //system call

_exit: סיום התהליך הקורא "מייד", אין חיוב שיעשה flush ל-stdio stream buffers.

void exit(int status); //c library function

exit:

  • יבוצע flush ל-stdio stream buffers
  • ניתן להגדיר פונקציות נוספות (המכונות exit handlers) שיתבצעו בזמן קריאה ל-exit, באמצעות atexit

exit

48 of 52

int atexit(void (*function)(void));

  • פונקציית atexit() "רושמת" פונקצייה נתונה להיקרא בסיום נורמלי של תהליך (או דרך exit(3) או דרך return מה main של התוכנית(.
  • הפונקציות שמוגדרות נקראות בסדר הפוך לסדר הרישום שלהן.
  • אין ארגומנטים שמועברים.
  • אותה פונקציה יכולה להיות "רשומה" מספר רב של פעמים ונקראת פעם אחת לכל "רישום".
  • הפונקציה atexit() מחזירה את הערך 0 אם הצליחה, אחרת היא מחזירה ערך שונה מאפס.
  • _SC_ATEXIT_MAX – קבוע עבור הפונקציה sysconfשמכיל את מספר הפונקציות המקסימלי שאפשר לרשום ל atexit()

atexit

49 of 52

atexit – דוגמא 1

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

void fnExit1 (void) {

puts ("Exit function 1.");

}

void fnExit2 (void) {

puts ("Exit function 2.");

}

int main () {

atexit (fnExit1);

atexit (fnExit2);

atexit (fnExit2);

puts ("Main function.");

return 0;

}

Main function.

Exit function 2.

Exit function 2.

Exit function 1.

Output

50 of 52

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

void bye(void)

{

printf(“Let’s go home, we learned enough today\n");

}

int main(void)

{

long a;

int i;

a = sysconf(_SC_ATEXIT_MAX);

printf("ATEXIT_MAX = %ld\n", a);

i = atexit(bye);

if (i != 0)

{

fprintf(stderr, "cannot set exit function\n");

exit(EXIT_FAILURE);

}

exit(EXIT_SUCCESS);

}

atexit – דוגמא 2

Output

51 of 52

דוגמא מסכמת:�exit vs. _exit (t2_3.c)

void done()

{

printf("see ya!\n");

}

int main()

{

int status;

atexit(done);

if(fork())

{

wait(&status);

printf("parent PID = %d\n",getpid());

printf("exit status= %d\n",WEXITSTATUS(status));

_exit(73);

}

else

{

sleep(2);

printf("child PID = %d\n",getpid());

exit(55);

}

}

52 of 52

כאשר בן מסתיים והאב לא המתין לו, הבן הזה נחשב zombie process.

ה-PCB שלו עדיין נשמר. למה?

כי תהליך האב עדיין יכול לעשות לו wait. רק אז ה-PCB יימחק.

במידה והאב גם יסיים ולא יעשה wait לבן,

הבן יהפוך לתהליך יתום והתהליך init

יאמץ אותו ויעשה לו wait.

זהירות זומבים!