קריאות
מערכת לקלט/פלט
הרבה מתכנתי C מכירים את ה-קלט/פלט של C רק מהרובד גבוה יחסית של printf, fprintf, scanf, fscanf, getc,putc, fgetc,
fputc fopen, fclose וכו'. ב-unix הרוטינות הללו ממומשות בקוד אחר
של C המבצעות את
ה-קלט/פלט באמצעות קריאות מערכת של unix
שהם write, read, open
close ואחרים. במערכות אחרות שמומשו
עבורם קומפילרים של C (כמו DOS, Window , VMS)
קריאות המערכת הללו לפעמים נתמכות ופעמים לא והתוכניות שמשתמשות בהם לפעמים עובדות
ולפעמים לא. אם תרצה להעביר תוכנית מתשתית
אחת לשנייה תצטרך לבדוק זאת.
עקרונות
קריאות המערכת
קריאות
המערכת של ה-קלט/פלט יסקרו על לרמת הפרטים אבל אפשר לתת סקירה כללית של
שיטת העבודה במספר משפטים.
לכל תהליך של תוכנית C יש מספר כזה או אחר של קבצים/
ערוצי קלט/פלט שהם רשאים לפתוח בו זמנית.
פעם זה היה 20 והיום זה תלוי במערכת. הערוצים הללו ממוספרים מאפס עד המספר
המרבי פחות אחד. המוסכמה היא שעם תחילת ריצה של תוכנית, ערוץ 0 מנותב ל-standard input (המקלדת בדרך כלל), ערוץ 1 מנותב ל-standard output (המסך בדרך כלל) ערוץ 2
מנותב ל-standard error
(המסך בדרך כלל). אחד מאמצעי הגמישות של
קוד unix הוא שאפשר לשנות את
הניתוב הזה ללא שינוי הקוד של התוכנית עצמה.
משתמשים שלו unix ממשקים טקסטואליים אחרים מכירים את האמצעי הזה
היטב כאשר הם מריצים פקודות נוסך
ls >filename.txt
ls >> filename.txt
sort < file1.txt > file2.txt
cc prog1.c >& file.txt
ls –l | more
ls | sort | more
וכו'
time > filename.txt
ברמה הזו
קוד הקלט/פלט (כלומר הפקודות שמשתמשים בהם ) הינו כמעט בלתי תלוי באופי הערוץ
עצמו. במלים אחרות הכתיבה למסך או לקובץ או להתקן אחר הוא אותו דבר או כמעט אותו
דבר.
קבצים או ערוצים מסוג אחר נפתחים בדרך כלל ע"י
הפקודה open. קריאה וכתיבה נעשית ע"י קריאות מערכת המעבירות רצפים של
בתים באופן גולמי (ללא עיבוד) בשמות read ו-write לפי מספרי ערוצים.
קריאות
המערכת
קריאת
המערכת open
ל-open יש גרסה ישנה שלא נתייחס אליה
כאן.
open מוגדר
כקריאת מערכת של פתיחת קובץ, אם כי ב-unix
למושג "קובץ" יכול להיות גם משמעות של ספריה או התקן (device).
הגרסה החדש
היותר חדשה היא כלהלן:
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
כאשר pathname הוא שם הקובץ
ומיקומו
flags קובע את צורת הפתיחה של הקובץ. הוא למעשה מערך של דגלים ביטיים שמפורשים
ע"י המערכת. כאשר מתכננת בוחר צורה של פתיחת קובץ, הוא בדרך כלל בונה ערך ל-flags ע"י חישוב של bitwise Or על סדרה של קבועים סמליים שקיימים ב-fcntl.h שהם חזקות של 2. הקבועים הסמליים העיקריים הם:
O_RDONLY – פתח את הקובץ רק לקריאה
O_WRONLY - פתח את הקובץ רק לכתיבה
O_RDWR – פתח גם לקריאה וגם לכתיבה
O_CREAT – אם הקובץ לא קיים צור אותו
O_TRUNC – אם הקובץ קיים, הורד את גודלו לאפס בתים (כלומר רוקן
אותו)
O_EXCL - דלוק – תכשל אם הקובץ קיים כבר O_CREAT אם הביט של
O_APPEND – כתוב מנקודת התחלה של סוף הקובץ
הפרמטר mode קובע את ההרשאות על הקובץ: בדרך
כלל אם הקובץ ביצועי או רגיל, מה מותר לבעליו, למשתמשים חברי קבוצה, ולאחרים. למשל mode = 644
באוקטל פירושו קובץ רגיל, פתוח לקריאה ולכתיבה לבעליו, פתוח לקריאה בלבד לכל
האחרים.
לדוגמא, אם
מתכנת רוצה לפתוח קובץ לקריאה בלבד וליצור את הקובץ אם הוא לא קיים וידרוס את
הקובץ אם הוא כן קיים אזי הוא יבחר פרמטר flags אם הערך
O_WRONLY | O_CREAT | O_TRUNC
הקריאה ל-open יראה משהו כמו
int fd;
……
fd
= open("file.txt", O_WRONLY |
O_CREAT | O_TRUNC, 0644);
אם הוא רוצה
שבמידה והקובץ קיים open לא
יעשה כלום חוץ מלדווח על כשלון הוא יבחר בערך ל-flags
O_WRONLY | O_CREAT | O_EXCL
קריאת המערכת open תחזיר 1- במקרה של כשלון ומספר
שלם אי שלילי במקרה של הצלחה. תכונה
מרכזית שלה היא שהיא מחויבת להשתמש במספר הערוץ הפנוי הקטן ביותר כאשר היא נענית לבקשה.
פקודת
המערכת write
ההכרזה של
פקודת המערכת write היא
ssize_t write(int fd, const void *buf,
size_t count);
כאשר fd זה מספר הערוץ (תשובת של open), count הוא מספר הבתים שנכתבים לערוץ, ו-buff הוא מהיכן לקרוא אותם. תשובת הפונקציה של write תהיה מספר הבתים שנכתבו. הערך
יהיה 1- במקרה של כשלון. בהקשר של קבצים, write תמיד יחזור מיד. מה שיקרה בפועל
הוא שהמערכת תצבור את הנתונים ותכתוב לדיסק כאשר גודל הצבירה עובר סף מסוים, או
בתנאים מסוימים אחרים. אם בהמשך המערכת
נתקלת בבעיה כמו דיסק מלא, התוכנית לא מקבלת הודעה בשלב הזה. בכדי להבין למה זה
צריך לזכור שכתיבה/קריאה לדיסק ולהתקני פלט דומים הם תמיד בגושי נתונים של גודל
קבוע (בדר"כ 4096 בתים או חזקה קרובה
אחרת של 2).
אגב, יש מערכות שאפשר לציין ב-open שפקודות write יחזרו רק אחרי כתיבה פיזית לדיסק,
אבל זה איטי מאד ורק מערכות קריטיות משתמשות בזה.
תשובת
הפונקציה של write תהיה מספר הבתים שנכתבו, או 1-
במקרה של כשלון. ישנם מקרים שבהם מספר הבתים שיכתבו יהיה פחות מ-count או אפילו 0 ולא יחשב ככישלון.
קריאת
המערכת read
הפקודה read היא פקודת הראי של write
וההכרזה שלה היא להלן:
ssize_t read(int fd, void *buf, size_t
count);
כאשר fd הוא מספר הערוץ ממנו לקרוא, count אומר לו כמה לקרוא, ו-buf היכן לשים את המידע שנקרא.
קריאה ל-read תחזור כמעט מיד אם המידע שאמור
להיקרא מהקובץ נקרא במסגרת גוש נתונים שנקרא ב-read קודם. אחרת קריאה ל-read תעצור את התוכנית עד שהמידע יגיע
מהדיסק.
תשובת
הפונקציה של read תהיה מספר הבתים שנקראו בפועל
במקרה של הצלחה כאשר 0 מעיד על ניסיון לקרוא מקובץ שכבר נקרא באופן מלא. הערך המוזר יכול להיות קטן מ-count אם עשינוread סמוך לסוף הקובץ. התשובה של read תהיה 1- במקרה של
כשלון. קריאה ל-read על קובץ שכבר נקרא במלואו או כמעט
במלואו אינם נחשבים לכישלונות.
קריאת
המערכת fsync
קריאת המערכת fsync
גורמת לפקודות write
קודמות להתבצע פיזית לדיסק בלי קשר לשיקולי מילוי של חוצצים. קריאה ל-fsync תעצור את התהליך עד שהכתיבה הפיזית תתבצע. ההכרזה של fsync הינו:
int
fsync(int fd);
כאשר fd הוא כמו תמיד מספר הערוץ. התשובה של fsync תהיה 0 במקרה של הצלחה ו-1- במקרה של כשלון.
קריאת
המערכת close
קריאת המערכת close סוגרת את הערוץ ומשחררת את מספרו להקצאה
מחדש בעתיד. רשמית הוא לא עושה כלום מעבר לכך, הוא מעין פעולת רישום פנימית. כל ניסיון
לקרוא/לכתוב בערוץ סגור יהיה בקשה לא חוקית לקריאת מערכת. ברמה הפורמאלית לפחות אין
מחויבות לבצע בנוסף לכך את מהלכיו של fsync
כולם או חלקם. ברוב המערכות זה אכן קורה,
אבל התקן של unix
אינו מחייב זאת. מי שרצה להיות בטוח מוטב
שיקרא ל-fsync
לפני שהוא קורא ל-close.
ההכרזה על close הינה:
int close(int fd);
כאשר fd הוא כמו תמיד מספר הערוץ. התשובה של close תהיה 0 במקרה של הצלחה ו-1- במקרה
של כשלון.
לדוגמא,
התוכנית הבאה מממשת העתקה של קבצים ששמותיהם מועברים כפרמטרים לתוכנית ראשית:
/*
my_cp.c - copy a single file using unbuffered io */
#include
<fcntl.h>
#include
<sys/stat.h>
#include
<stdio.h>
#include
<stdlib.h>
#define
BUFFSIZE 10
int main(argc, argv)
int argc;
char *argv[];
{
int inpfd, outpfd; /* File descriptors */
char buff[BUFFSIZE]; /* Temporary storage buffer */
int n; /* Number of
characters */
if (argc != 3)
{
printf("Usage: %s orgfilename
copyfilename\n", argv[0]);
exit(1);
}
if ( (inpfd = open(argv[1], O_RDONLY ) ) == -1 )
/* open input file, if cannot */
{
fprintf(stderr,"Cannot open file %s\n",argv[1]);
exit(2);
}
if ( (outpfd =
open(argv[2], O_CREAT | O_WRONLY, 0660 )
) == -1 )
/* open output file, if cannot */
{
fprintf(stderr,"Cannot open file
%s\n",argv[2]);
exit(3);
}
while ( (n = read(inpfd, buff, BUFFSIZE) )
> 0 ) /* Read until end */
write(outpfd, buff, n);
fsync(outpfd);
close(inpfd);
close(outpfd);
return 0;
}
/* main */
דוגמת ריצה:
%
cc my_cp.c
% ./a.out old new
%
cat old
This file was created by a text editor.
%
cat new
This file was created by a text editor.
%
מימוש
ניתובים של קלט/פלט מהמקלדת, מסך
כמעט כל משתמש של הממשק הטקסטואלי של unix, DOS, Windows מכיר את הפטנט של ניתוב
קלט פלט נוסך
% prog > outfile.txt
% prog < infile.txt > outfile.txt
% prog >> outfile.txt
% prog >& outfile.txt (unix בלבד)
כלומר תוכניות
שנכתבו לקבל מידע מהמקלדת וכותבות פלט למסך מנתבים את הקלט ו/או הפלט לקבצים, גם
כאשר אין אפשרות לקמפל מחדש את התוכנית. הדרך שעושים זאת הוא להסתמך על כך ש-fork משכפל לתהליך הבן את הערוצים של
תהליך האב (למעט המקלדת ששם זה מותנה במשהו) וגם ביצוע אחד מפקודות ה- execl לא משנות זאת.
ברמת קוד C אפשר לדמות את מה שעושה ה-schell בשתי צורות. אחד
ע"י שימוש בפונקציה freopen
שהכרזתה היא
FILE *freopen(const char *path, const
char *mode, FILE *stream);
זוהי פונקציה
שדומה ל-fopen רק שהיא סוגרת ערוץ קיים ומנתבת אותו לקובץ המצוין בפקודה. freopen מחזיר פוינטר חדש לערוץ במקרה של הצלחה, ו-NULL במקרה של כשלון.
הדרך לקבל את
האפקט הרצוי הוא לקרוא ל-freopen על stdin, stdout, stderr (לפי הצורך) אחרי ה-fork ולפני ה-execl.
לדוגמא, נניח
שיש לנו תוכנית שמדפיסה למסך:
/*
hello.c */
#include
<stdio.h>
int main()
{
printf("\nHello World!\n");
return 0;
}
/* main */
מה שנעשה
עכשיו הוא לדמות את ה-schell
כאשר הומבצע
%
hello > hello.txt
כלומר, שלמרות
ש-hello.c מתקמפל לתוכנית שאמורה להדפיס למסך, ננתב אותו לכתיבה לקובץ ע"י
תוכנית המעטפת הבאה:
/* redirect1.c */
#include
<stdio.h>
int main()
{
if (fork() == 0) /* Child */
{
if ( freopen("hello.txt",
"wt", stdout) == NULL)
{
perror("freopen");
exit(1);
} /* if */
execl("./hello", "hello",
0);
} /* if */
printf("Parent terminating\n");
}
/* main */
המהלך יהיה
הבא:
%
cc hello.c –o hello
% cc redirect1.c
% a.out
Parent terminating
% cat hello.txt
Hello World!
%
להן דוגמא
לשיטה הקלאסית של תכנות קריאת מערכת של unix: תוכנית שאם אינה מקבלת פרמטרים לתוכנית ראשית, קוראת מהמקלדת
וכותבת למסך. או היא מקבלת פרמטר אחד, היא קוראת אותו כקובץ וכותבת למסך. אם היא
מקבלת 2 פרמטרים לתוכנית ראשית, קוראת את הראשון כקובץ קלט וכותבת את השני כקובץ
פלט. אם היא מורצת עם האופרטורים > או
< אזי כל קומבינציה (עם קובץ פלט אבל ללא קלט למשל) אפשרית:
/*
copy2.c */
#include
<stdio.h>
int main(int argc, char *argv[] )
{
int buff;
if (argc > 1)
if ( freopen(argv[1], "rt",
stdin) == NULL)
{
perror("freopen");
exit(1);
} /* if */
if (argc > 2)
if ( freopen(argv[2], "wt",
stdout) == NULL)
{
perror("freopen");
exit(1);
} /* if */
while ( (buff = getchar()) != EOF ) /* read
until Ctrl-D */
putchar(buff);
}
/* main */
הדרך הישירה יותר לקבל את אותם אפקטים,
שהיא כנראה שיטת המימוש הפנימי של freopen, הוא להשתמש במערכת הקלט/פלט
הגולמית.
ישנן מספר
אפשרויות, נראה כאן את הפשוטה ביותר.
הראשונה הוא
להניח ש-open מחזיר את מספר הערוץ
הפנוי הראשון. לפיכך על מנת לנתב את הערוץ
הבסיסי (0,1,2) של תהליך, פשוט נסגור ב-close את הערוץ ומיד אחר כל ננתב אותו ב-open.
התוכנית הבאה
תעשה פעולה זהה של ניתוב הפלט של hello.c:
/* redirect2.c */
#include
<stdio.h>
#include
<fcntl.h>
void fatal(char str[])
{
fprintf(stderr, "%s\n", str);
exit(1);
}
// fatal
void sys_err(char str[])
{
perror(str);
exit(2);
}
// sys_err
int main()
{
if (fork() == 0) /* Child */
{
int fd;
if( close(1) == -1)
sys_err("close");
if ( (fd = open("hello.txt",
O_WRONLY | O_CREAT |O_TRUNC, 0644)) == -1)
sys_err("open");
if (fd != 1)
fatal("Unexpected dup result");
execl("./hello",
"hello", 0);
} /* if */
printf("Parent terminating\n");
}
/* main */