5.5 מתארי קבצים פתרון
פתרונות - מתארי קבצים - file descriptors¶
פתרון 1 - כתיבה לקובץ עם write¶
כתבו תוכנית שפותחת קובץ בשם hello.txt לכתיבה, כותבת אליו את המחרוזת "hello from fd!\n" באמצעות הsyscall write, וסוגרת את הקובץ.
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("hello.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
write(2, "open failed\n", 12);
return 1;
}
const char *msg = "hello from fd!\n";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
פתרון 2 - הפניית stdout לקובץ עם dup2¶
כתבו תוכנית שמפנה את stdout לקובץ בשם redirected.txt באמצעות dup2, ואז משתמשת ב-printf כדי לכתוב כמה שורות.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("redirected.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}
// מפנים את stdout (fd 1) לקובץ
dup2(fd, 1);
close(fd); // כבר לא צריכים את הfd המקורי
// מעכשיו printf כותב לקובץ
printf("this line goes to the file\n");
printf("this line also goes to the file\n");
printf("nothing appears on screen!\n");
return 0;
}
אחרי ההרצה, אף שורה לא תודפס למסך. כל הפלט יהיה בתוך redirected.txt.
פתרון 3 - קריאת קובץ בית-אחר-בית¶
כתבו תוכנית שקוראת קובץ טקסט בית אחרי בית באמצעות הsyscall read, ומדפיסה כל תו למסך באמצעות write לfd 1.
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("hello.txt", O_RDONLY);
if (fd < 0) {
write(2, "open failed\n", 12);
return 1;
}
char c;
while (read(fd, &c, 1) > 0) {
write(1, &c, 1); // כותבים בית אחד ל-stdout
}
close(fd);
return 0;
}
שימו לב: read מחזירה 0 כשמגיעים לסוף הקובץ (EOF), ולכן הלולאה עוצרת. בכל איטרציה אנחנו קוראים בדיוק בית אחד וכותבים אותו ל-stdout.
פתרון 4 - אב ובן כותבים לאותו קובץ¶
כתבו תוכנית שפותחת קובץ, קוראת ל-fork, ואז גם האב וגם הבן כותבים את הPID שלהם לקובץ.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int fd = open("pids.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}
pid_t pid = fork();
if (pid == 0) {
// תהליך הבן
char buf[64];
sprintf(buf, "child PID: %d\n", getpid());
write(fd, buf, strlen(buf));
close(fd);
_exit(0);
} else if (pid > 0) {
// תהליך האב
char buf[64];
sprintf(buf, "parent PID: %d\n", getpid());
write(fd, buf, strlen(buf));
wait(NULL); // מחכים שהבן יסיים
close(fd);
} else {
perror("fork");
close(fd);
return 1;
}
return 0;
}
נקודות חשובות:
- הקובץ נפתח לפני הfork, ולכן שני התהליכים חולקים את אותו fd
- שני התהליכים כותבים לאותו קובץ - הסדר יכול להשתנות בין הרצות
- האב מחכה לבן עם wait כדי למנוע zombie
פתרון 5 - שאלה תיאורטית¶
מה קורה למתארי קבצים הפתוחים כאשר תהליך קורא ל-execve?
כברירת מחדל, מתארי קבצים פתוחים נשארים פתוחים לאחר קריאה ל-execve. כלומר, התוכנה החדשה שנטענת יורשת את כל הfd-ים שהיו פתוחים לפני הexecve.
זה בדיוק מה שמאפשר לshell לבצע הפניית פלט (redirection) עם תוכנות חיצוניות. כשהshell מריץ ls > output.txt, הוא:
- קורא
forkכדי ליצור תהליך בן - בתהליך הבן: פותח את
output.txtומבצעdup2כדי להפנות את stdout (fd 1) לקובץ - בתהליך הבן: קורא
execve("/bin/ls", ...)כדי להריץ את ls - התוכנה ls מתחילה לרוץ - היא כותבת ל-stdout (fd 1) שמצביע על הקובץ
אם execve היה סוגר את כל הfd-ים, ההפניה הייתה נשברת ו-ls היה כותב למסך במקום לקובץ.
הערה: ניתן לסמן fd ספציפי כ-"close-on-exec" עם הדגל O_CLOEXEC בopen, או עם הsyscall fcntl. fd שמסומן כך כן ייסגר אוטומטית בexecve. אבל ברירת המחדל היא שהfd נשאר פתוח.