Βασικές έννοιες του Unix
Βασικές έννοιες του Unix Σύστημα αρχείων μία ιεραρχική δομή αρχείων και καταλόγων Η «βάση» είναι ο πρωταρχικός κατάλογος (root directory) που συμβολίζεται με / Ένας κατάλογος είναι ένα «αρχείο» που περιέχει καταχωρήσεις καταλόγου Μία καταχώρηση καταλόγου από λογική άποψη είναι μία συλλογή πληροφοριών (όνομα, είδος, μέγεθος, ιδιοκτήτης, δικαιώματα, ημερομηνία τροποποίησης κ.λπ.) Οι πληροφορίες αυτές είναι προσπελάσιμες με τις συναρτήσεις stat και fstat
Βασικές έννοιες του Unix Διαδρομή Μία ακολουθία από μηδέν ή περισσότερα ονόματα αρχείων, διαχωριζόμενα με καθέτους (/) και που ενδεχομένως ξεκινά με κάθετο καλείται διαδρομή Αν ξεκινά με κάθετο καλείται απόλυτη, αν όχι καλείται σχετική Τα ονόματα των αρχείων φυλάσσονται στους καταλόγους και διαβάζονται με καθορισμένες συναρτήσεις και τύπους δεδομένων
Βασικές έννοιες του Unix Παράδειγμα [readdir.c] #include <sys/types.h> #include <dirent.h> #include <stdio.h> void err_die(char *err) { fprintf(stderr, "%s\n", err); exit(1); } int main(int argc, char *argv[]) { DIR *dp; struct dirent *dirp; if (argc != 2) err_die("provide the directory name"); if ((dp = opendir(argv[1])) == NULL) err_die("Directory not found"); while ((dirp = readdir(dp)) != NULL) printf("%s\n", dirp->d_name); closedir(dp); return 0;
Κατάλογος εργασίας και εστιακός κατάλογος Κάθε εκτελούμενη διεργασία έχει έναν κατάλογο εργασίας, βάσει του οποίου διερμηνεύονται οι σχετικές διαδρομές Η διεργασία μπορεί να αλλάξει τον κατάλογο εργασίας της μέσω της κλήσης συστήματος chdir Όταν συνδεόμαστε με το σύστημα, ο τρέχων κατάλογος είναι ο εστιακός κατάλογος (home directory), όπως καθορίζεται στο αρχείο passwd
Είσοδος και έξοδος Περιγραφείς αρχείων Ακέραιοι με μικρές τιμές (0-256 ή 1024) με τους οποίους ο πυρήνας του λειτουργικού αναγνωρίζει τα αρχεία που έχει ανοικτά μία διεργασία. Με το άνοιγμα ενός αρχείου, ο πυρήνας επιστρέφει στη διεργασία έναν περιγραφέα αρχείου Κατά σύμβαση, όταν ξεκινά μία διεργασία υπάρχουν τρεις διαθέσιμοι περιγραφείς αρχείων Κανονική είσοδος (συνήθως 0) Κανονική έξοδος (συνήθως 1) Κανονική έξοδος σφαλμάτων (συνήθως 2)
Είσοδος και έξοδος Χωρίς χρήση ενδιάμεσης μνήμης Παρέχεται από τις συναρτήσεις open, read, write, lseek, close, που λειτουργούν με περιγραφείς αρχείων Παράδειγμα [copyfile.c]: #include <fcntl.h> #include <stdio.h> #define BUFSIZE 8192 int main(int argc, char *argv[]) { int infd, outfd, nbytes; char buf[BUFSIZE]; if ((infd = open(argv[1], O_RDONLY)) == -1) { fprintf(stderr, "Cannot open input file\n"); exit(1); } if ((outfd = open(argv[2], O_WRONLY | O_CREAT, 0644)) == -1) { fprintf(stderr, "Cannot open output file\n"); exit(1);} while ((nbytes = read(infd, buf, BUFSIZE)) > 0) if (write(outfd, buf, nbytes) != nbytes) { fprintf(stderr, "Cannot write bytes\n"); break; } if (nbytes < 0) fprintf(stderr, "Input error\n"); close(infd); close(outfd); return 0; }
Είσοδος και έξοδος Με χρήση ενδιάμεσης μνήμης Τυπική βιβλιοθήκη της C (printf, scanf, gets, getc, putc, fread, fwrite, …) Βολική για επεξεργασία αρχείων κειμένου Παράδειγμα [copyfile-std.c] #include <stdio.h> int main(int argc, char *argv[]) { FILE *infp, *outfp; int c; if ((infp = fopen(argv[1], "rb")) == NULL) { fprintf(stderr, "Cannot open input file\n"); exit(1); } if ((outfp = fopen(argv[2], "wb")) == NULL) { fprintf(stderr, "Cannot open output file\n"); exit(1);} while ((c = fgetc(infp)) != EOF) if (fputc(c, outfp) == EOF) { fprintf(stderr, "Cannot write bytes\n"); break; } if (!feof(infp)) fprintf(stderr, "Input error\n"); fclose(infp); fclose(outfp); return 0; }
Προγράμματα και διεργασίες Πρόγραμμα Ένα εκτελέσιμο πρόγραμμα σε ένα αρχείο δίσκου. Το πρόγραμμα διαβάζεται στη μνήμη και εκτελείται από τον πυρήνα, μέσω κατάλληλων κλήσεων συστήματος Διεργασία (ή λειτουργία (task)) Ένα εκτελούμενο στιγμιότυπο ενός προγράμματος. Κάθε διεργασία αναγνωρίζεται μοναδικά από έναν προσδιοριστή διεργασίας (process identifier) που είναι ένας μη αρνητικός ακέραιος Το πιο κάτω πρόγραμμα τυπώνει τον προσδιοριστή διεργασίας του [print_pid.c] #include <unistd.h> #include <stdio.h> int main(void) { printf("Hello from process %d\n", getpid()); return 0; }
Έλεγχος διεργασιών Γίνεται κυρίως με τρεις συναρτήσεις, fork, exec και waitpid Η συνάρτηση fork δημιουργεί ένα αντίγραφο της εκτελούμενης διεργασίας, με διαφορετικό προσδιοριστή διεργασίας Η συνάρτηση exec ξεκινά στη θέση της τρέχουσας διεργασίας ένα πρόγραμμα που καθορίζεται ως παράμετρος Η συνάρτηση waitpid αναμένει μέχρι τον τερματισμό της καθορισμένης διεργασίας Δημιουργία νέας διεργασίας, εκτέλεση του προγράμματος p: fork() για δημιουργία αντιγράφου Το ένα αντίγραφο εκτελεί την exec για να αντικαταστήσει τον εαυτό του με ένα εκτελούμενο στιγμιότυπο του p Το άλλο αντίγραφο (συνήθως) περιμένει να τελειώσει το p και κατόπιν συνεχίζει
Έλεγχος διεργασιών – Παράδειγμα [newproc.c] #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> int main(void) { char buf[1024], *rd_status; pid_t pid; int status; for ( ; ; ) { printf("Enter command (no params): "); rd_status = gets(buf); if ((rd_status == NULL) || (buf[0] == 0)) break; if ((pid = fork()) < 0) { fprintf(stderr, "fork error"); exit(1); } else if (pid == 0) { /* child here */ execlp(buf, buf, NULL); fprintf(stderr, "Cannot execute %s\n", buf); exit(2); } /* parent here */ if ((pid = waitpid(pid, &status, 0)) < 0) { fprintf(stderr, "waitpid error\n"); exit(1); } return 0;
Έλεγχος διεργασιών – Εποπτικά (1/3) int fork_result = -----; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(fork_result, NULL, 0); dp = opendir("."); while ((dent = readdir(dp)) != NULL) printf(…); Πρόγραμμα sshell Πρόγραμμα ls To sshell εκτελείται και δημιουργείται μία διεργασία int fork_result = -----; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(fork_result , NULL, 0); pid: 1982 PC
Έλεγχος διεργασιών – Εποπτικά (2/3) Εκτελείται η fork και δημιουργείται μία ακόμη διεργασία, η 2178 int fork_result = 2178; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(pid, NULL, 0); int fork_result = 0; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(pid, NULL, 0); pid: 1982 pid: 2178 PC PC Βάσει του if επιλέγονται διαφορετικά μονοπάτια εκτέλεσης για κάθε διεργασία int fork_result = 2178; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(pid, NULL, 0); int fork_result = 0; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(pid, NULL, 0); pid: 1982 pid: 2178 PC PC
Έλεγχος διεργασιών – Εποπτικά (3/3) Η θυγατρική διεργασία εκτελεί την exec – ο κώδικάς της αντικαθίσταται από την ls û int fork_result = 2178; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(pid, NULL, 0); int fork_result = 0; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(pid, NULL, 0); dp = opendir("."); while ((dent = readdir(dp)) != NULL) printf(…); û pid: 1982 pid: 2178 pid: 2178 PC PC PC Η θυγατρική εκτελείται & ολοκληρώνεται – η γονική διεργασία «ξυπνάει» από τη waitpid ü int fork_result = 2178; printf("Starting…"); fork_result = fork(); if (fork_result == 0) execlp("ls", "ls", NULL); else waitpid(pid, NULL, 0); dp = opendir("."); while ((dent = readdir(dp)) != NULL) printf(…); pid: 2178 pid: 1982 PC PC
Διαχείριση σφαλμάτων Όταν συμβεί κάποιο σφάλμα, οι συναρτήσεις του UNIX επιστρέφουν –1 και θέτουν την καθολική μεταβλητή errno να υποδεικνύει το συγκεκριμένο σφάλμα Το αρχείο ενσωμάτωσης errno.h ορίζει συμβολικές σταθερές για τα διάφορα δυνατά σφάλματα (ENOENT, EBADF, ΕΝΟΜΕΜ, ENOTDIR κ.λπ.) Η τιμή του errno δεν τίθεται ποτέ σε μηδέν από καμία συνάρτηση Η τιμή του errno που προκύπτει από ένα σφάλμα επικαλύπτει τις πρότερες τιμές Η συνάρτηση strerror(int errnum) επιστρέφει (ένα δείκτη σε) μία συμβολοσειρά που περιγράφει το σφάλμα Η συνάρτηση perror(const char *msg) τυπώνει στην κανονική έξοδο σφαλμάτων τη συμβολοσειρά msg ακολουθούμενη από το μήνυμα που αντιστοιχεί στο τελευταίο σφάλμα
Διαχείριση σφαλμάτων – παράδειγμα [errhandle.c] #include <errno.h> #include <stdio.h> #include <fcntl.h> int main(int argc, char *argv[]) { char buff[256], *bp; int fd; for ( ; ; ) { printf("Enter filename to open: "); if (((bp = gets(buff)) == NULL) || (*bp == '\0')) break; fd = open(buff, O_RDONLY); if (fd == -1) { printf("Error %d: %s\n", errno, strerror(errno)); perror(buff); } else close(fd); return 0;
Ταυτότητες χρηστών Κάθε χρήστης προσδιορίζεται μοναδικά από έναν αριθμό, την ταυτότητα χρήστη που αποδίδεται από τον διαχειριστή και αποθηκεύεται στο αρχείο /etc/passwd Η ταυτότητα χρήστη χρησιμοποιείται από τον πυρήνα για διακρίβωση του αν ο χρήστης έχει τα απαραίτητα δικαιώματα για τις ενέργειές του Ο χρήστης με ταυτότητα 0 είναι ο υπερ-χρήστης (συνήθως ονομάζεται root) και γι’ αυτόν παραλείπονται αρκετοί έλεγχοι για δικαιώματα στο σύστημα αρχείων Για κάθε χρήστη καθορίζεται επίσης μία αριθμητική ταυτότητα ομάδας τυπικά, πολλοί χρήστες έχουν την ίδια ταυτότητα ομάδας Η αντιστοιχία ταυτοτήτων ομάδων σε ονόματα ομάδων καθορίζεται στο αρχείο /etc/group Στο ίδιο αρχείο καθορίζονται και συμπληρωματικές συμμετοχές σε ομάδες για τους χρήστες Οι συμμετοχές σε ομάδες χρησιμοποιούνται επίσης για διακρίβωση κατοχής δικαιωμάτων
Ταυτότητες χρηστών – Παράδειγμα [userid.c] Οι συναρτήσεις getuid(), getgid(), geteuid(), getegid(), getgroups(), setuid(), seteuid(), setgid(), setegid(), setgroups() χρησιμοποιούνται για την αναφορά και την αλλαγή των ταυτοτήτων χρήστη και ομάδων #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(void) { gid_t theGroups[16]; int ngroups, ctr; printf("Uid = %d, Primary Gid = %d\n", getuid(), getgid()); ngroups = getgroups(16, theGroups); printf("All groups: "); for (ctr = 0 ; ctr < ngroups; ctr++) printf("%d\t", theGroups[ctr]); putchar('\n'); return 0; }
Σήματα Τα σήματα είναι ένας γενικός μηχανισμός αναφοράς σε μία διεργασία ότι κάτι έχει συμβεί (συνήθως κακό) Διαίρεση με το μηδέν, πάτημα CTRL-C, πάτημα CTRL-Z, σφάλμα διευθυνσιοδότησης, αλλαγή στην κατάσταση θυγατρικής διεργασίας κ.λπ. Όταν φθάνει ένα σήμα σε μία διεργασία, αυτή έχει τρεις επιλογές: Να αγνοήσει το σήμα Να εκτελέσει την εξ ορισμού ενέργεια για το συγκεκριμένο σήμα Να παράσχει τη δική της συνάρτηση που θα εκτελεστεί Η αποστολή συγκεκριμένων σημάτων σε μία διεργασία μπορεί προσωρινά να ανασταλεί και να ενεργοποιηθεί ξανά όταν η διεργασία βρίσκεται σε πιο βολικό σημείο
Σήματα – Παράδειγμα [signals.c] #include <signal.h> #include <stdio.h> #include <setjmp.h> sigjmp_buf buf; void sig_fpe(int siginfo) { signal(SIGFPE, SIG_IGN); fprintf(stderr, "Floating point error - cannot compute result\n"); siglongjmp(buf, 1); signal(SIGFPE, sig_fpe); } void sig_int(int siginfo) { signal(SIGINT, SIG_IGN); printf("Ctrl-C pressed. Bye-Bye!\n"); exit(0); int main(void) { int a, b, c; signal(SIGINT, sig_int); signal(SIGFPE, sig_fpe); sigsetjmp(buf, 0); for ( ; ; ) { printf("Enter a: "); scanf("%d", &a); printf("Enter b: "); scanf("%d", &b); c = a / b; printf("Result = %d\n", c); }
Χρονικές ενδείξεις Τα συστήματα UNIX παρέχουν δύο ειδών χρονικές ενδείξεις: Ημερολογιακός χρόνος, που ακολουθεί το «ρολόι τοίχου». Αναπαρίσταται ως το πλήθος των δευτερολέπτων που έχουν περάσει από την 00:00:00 01/01/1970 UTC Ο τύπος δεδομένων που φυλάσσει τιμές τέτοιου τύπου είναι ο time_t Χρόνος διεργασίας, που επίσης καλείται και χρόνος ΚΜΕ, και μετρά τον χρόνο που καταναλώνει μία διεργασία στην KME. Ο χρόνος αυτός μετριέται σε κτύπους ρολογιού, συνήθως 50, 60 ή 100 κτύπους ανά δευτερόλεπτο Ο τύπος δεδομένων που φυλάσσει τιμές τέτοιου τύπου είναι o clock_t Για μία διεργασία έχουμε τρεις χρόνους: χρόνος ρολογιού, χρόνος χρήστη ΚΜΕ, χρόνος συστήματος ΚΜΕ Οι χρόνοι μπορούν να μετρηθούν με την εντολή time cd /usr/include; time grep POSIX */* > /dev/null real 0m6.921s user 0m0.240s sys 0m0.340s Μία διεργασία μπορεί να αιτείται αποστολή σημάτων εγρήγορσης όταν περνάει ένα χρονικό διάστημα (ρολογιού, χρήστη, χρήστη + συστήματος)
Κλήσεις συστήματος και διαδικασίες βιβλιοθήκης (1/2) Τα λειτουργικά συστήματα παρέχουν σημεία εξυπηρέτησης στα οποία τα προγράμματα ζητούν κάτι από τον πυρήνα τα σημεία αυτά είναι οι κλήσεις συστήματος, που ποικίλουν σε αριθμό (Unix V7 50, 4.3 BSD 110, SVR4 120) Η διασύνδεση αυτή τεκμηριώνεται στα σχετικά εγχειρίδια, συνήθως σε γλώσσα C, ανεξάρτητα από την πραγματική γλώσσα υλοποίησης (συνήθως C ή συμβολική γλώσσα) Συνήθως στο Unix κάθε κλήση συστήματος έχει και μία αντίστοιχη συνάρτηση βιβλιοθήκης που την καλεί με τον κατάλληλο τρόπο (π.χ. τοποθετώντας τιμές σε καταχωρητές και προκαλώντας μία παγίδευση στον πυρήνα) Το Unix παρέχει επίσης και συναρτήσεις βιβλιοθήκης που παρέχουν υπηρεσίες στον χρήση Μία συνάρτηση βιβλιοθήκης μπορεί να καλεί μία κλήση συστήματος (π.χ. printf write) μπορεί όμως και όχι (π.χ. strlen) Οι χρήστες δεν ενδιαφέρονται για τη διαφορά, για όσους υλοποιούν όμως το Unix είναι κρίσιμο Οι συναρτήσεις βιβλιοθήκης αλλάζουν, οι κλήσεις συστήματος όμως όχι (π.χ. fread, fscanf, fgetc read)
Κλήσεις συστήματος και διαδικασίες βιβλιοθήκης (2/2) κώδικας εφαρμογής Διεργασία χρήστη διαδικασίες βιβλιοθήκης (fread, fscanf, fgetc, fgets) κλήση συστήματος (read) Πυρήνας
Πρότυπα και υλοποιήσεις του Unix
Γιατί πρότυπα; Τα προγράμματα του Unix ήταν πάντα αρκετά μεταφέρσιμα Τα πρότυπα αποσκοπούν στην πλήρη μεταφερσιμότητα Πρότυπα για: Τη γλώσσα προγραμματισμού (ANSI C) Τις κλήσεις συστήματος, τις παραμέτρους κάθε μίας και τη σημασιολογία αυτών Τις συναρτήσεις επιπέδου βιβλιοθήκης που πρέπει να παρέχονται Τα αρχεία ενσωμάτωσης, τους τύπους δεδομένων και τις συμβολικές σταθερές που ορίζουν Τα όρια που τίθενται στα προγράμματα Τις ενδείξεις που πρέπει να υποστηρίζουν τα προγράμματα που ενσωματώνονται στη διανομή και τις παραμέτρους που πρέπει να δέχονται Τη «γλώσσα» και το συντακτικό των φλοιών Το υποσύστημα διαχείρισης του συστήματος
Το πρότυπο IEEE POSIX Portable Operating System Interface for Computer Environments (ή for UNIX) Περιέχει το 1003.1/1988 (διεπαφή λειτουργικού συστήματος) αλλά αναπτύσσονται και άλλα (π.χ. 1003.2 για τους φλοιούς, 1003.7 για τη διαχείριση κ.ο.κ.) Το 1003.1 ορίζει τις υπηρεσίες που πρέπει να παρέχει ένα σύστημα για να «συμμορφώνεται με το POSIX» Οι περισσότεροι κατασκευαστές το υιοθετούν Δεν περιορίζεται αποκλειστικά σε συστήματα UNIX To 1003.1 είναι διεπαφή και όχι υλοποίηση, συνεπώς δεν διαχωρίζει μεταξύ κλήσης συστήματος και διαδικασίας βιβλιοθήκης Το 1990 κυκλοφόρησε η έκδοση POSIX.1 που είναι και διεθνές πρότυπο (ISO/EIC 9945-1:1990) To 1993 κυκλοφόρησε η έκδοση 1003.1a από την IEEE που ουσιαστικά χαρακτήρισε υποχρεωτική την υποστήριξη συμβολικών συνδέσμων
Τα πρότυπα X/Open XPG3 και FIPS Έχει εκδώσει ένα επτάτομο σύγγραμμα καλούμενο X/Open Portability Guide Ο δεύτερος τόμος «XSI System Interface and Headers» ορίζει μία διεπαφή για συστήματα τύπου Unix βασισμένη στο IEEE 1003.1/88 περιέχει χαρακτηριστικά που δεν ορίζονται στο 1003.1/88, π.χ. τη δυνατότητα παρουσίασης μηνυμάτων σε διαφορετικές γλώσσες Υπάρχει ήδη η έκδοση XPG4 με πρόσθετα στοιχεία διεπαφής FIPS: Federal Information Processing Standard Ορίσθηκε από την κυβέρνηση των ΗΠΑ, χρησιμοποιείται για τα συστήματα των κυβερνητικών οργανισμών Βασίζεται στο IEEE 1003.1/88 με βασική διαφορά τον χαρακτηρισμό ως «υποχρεωτικών» στοιχείων που το IEEE 1003.1/88 χαρακτηρίζει «προαιρετικά»
Οι υλοποιήσεις του UNIX http://www.levenez.com/unix/history.html Βασικοί εκπρόσωποι: SVR4 από την AT&T Unix Systems Laboratories Συγκερασμός των AT&T SVR3.2, SunOS, 4.3 BSD και MS Xenix Ο κώδικας είναι διαθέσιμος σε όλους από το 1990 Συμμορφώνεται με το POSIX 1003.1 και X/Open XPG3 Η ΑΤ&Τ έχει δημοσιεύσει επίσης το SVID (System V interface definition) που -ως πρότυπο διεπαφής- δεν διαχωρίζει μεταξύ κλήσεων συστήματος και διαδικασιών βιβλιοθήκης 4.4BSD Berkeley Software Distributions από το πανεπιστήμιο Berkeley Διάδοχος του 4.3BSD που υποστήριζε τα περισσότερα από χαρακτηριστικά του POSIX 1003.1 Η παραλλαγή 386 BSD εκτελείται σε υπολογιστές βασισμένους σε Intel 386
Σχέση μεταξύ προτύπων και υλοποιήσεων Τα πρότυπα ορίζουν ένα υποσύνολο των πραγματικών υλοποιήσεων Αρχείο ενσωμάτωσης Πρότυπο Υλοποίηση ANSI C POSIX 1 XPG 3 SVR4 4.4 BSD <float.h> <cpio.h> <nl_types.h> <unistd.h> <sys/ipc.h> <sys/msg.h>
Όρια Υπάρχουν πολλοί «μαγικοί αριθμοί» και σταθερές που ορίζονται από τις υλοποιήσεις. Σε πολλά προγράμματα ενσωματώνονται ως τιμές στον κώδικα, αλλά η προτυποποίηση έχει οδηγήσει σε ορισμό σταθερών και μεθόδων για πιο μεταφέρσιμο καθορισμό των «μαγικών αριθμών» και ορίων σε σχέση με: Ενδείξεις χρόνου μεταγλώττισης (το σύστημα υποστηρίζει έλεγχο εργασιών; πώς γίνεται η διαχείριση των τερματικών; κ.λπ.) Ορίζουν ποιες διεπαφές είναι διαθέσιμες στο σύστημα Όρια χρόνου μεταγλώττισης (ποια η μέγιστη τιμή ενός ακεραίου; ποια η μέγιστη ακρίβεια σε πραγματικούς διπλής ακρίβειας;) Σχετίζονται κυρίως με την αρχιτεκτονική της μηχανής, απίθανο να αλλάξουν σε επόμενη εκτέλεση Όρια χρόνου εκτέλεσης (μέγιστο μήκος διαδρομής αρχείου, μέγιστο πλήθος θυγατρικών διεργασιών κοκ) Όρια που σχετίζονται με την τρέχουσα υλοποίηση ή ρύθμιση, μπορεί να αλλάξουν σε επόμενη εκτέλεση
Αντιμετώπιση ζητημάτων ορίων Με τρεις τρόπους: Ενδείξεις χρόνου μεταγλώττισης και αρχεία ενσωμάτωσης π.χ. –DHAS_CURSES, -D_POSIX_JOB_CONTROL, SHRT_MAX συνάρτηση sysconf για όρια χρόνου εκτέλεσης που δεν σχετίζονται με αρχεία ή καταλόγους sysconf(_SC_OPEN_MAX); sysconf(_SC_SEMAPHORES); συναρτήσεις pathconf και fpathconf για όρια χρόνου εκτέλεσης που σχετίζονται με αρχεία ή καταλόγους result = pathconf("/", _PC_NAME_MAX); μέγιστο μέγεθος ονόματος αρχείου για το σύστημα αρχείων του πρωταρχικού καταλόγου Για περισσότερη διασκέδαση: αν ένα όριο χρόνου εκτέλεσης (αναμένεται να) είναι σταθερό σε ένα σύστημα ΕΙΝΑΙ ΠΙΘΑΝΟ να ορίζεται με σταθερά και αν δεν ορίζεται με σταθερά, τότε χρησιμοποιείται η κατάλληλη συνάρτηση
Όρια της ANSI C Όλα τα όρια της ANSI C είναι όρια χρόνου μεταγλώττισης και ορίζονται σε αρχεία ενσωμάτωσης limits.h, που ορίζει ελάχιστες και μέγιστες τιμές καθώς και άλλα πληροφοριακά στοιχεία για τους ακέραιους τύπους δεδομένων CHAR_BIT, INT_MIN, LONG_MAX, ULONG_MAX float.h, που ορίζει ελάχιστες και μέγιστες τιμές καθώς και άλλα πληροφοριακά στοιχεία για τους τύπους δεδομένων κινητής υποδιαστολής FTL_DIG, FLT_MIN_10_EXP, FLT_MAX, FLT_EPSILON stdio.h, ειδικότερα η σταθερά FOPEN_MAX (στο POSIX ορίζεται η STREAM_MAX που πρέπει να έχει την ίδια τιμή με την FOPEN_MAX)
Όρια του POSIX Αντίστοιχα όρια ορίζονται και από το XPG3 Αμετάβλητες ελάχιστες τιμές (δεν αλλάζουν από σύστημα σε σύστημα) – αν και το όνομά τους τελειώνει σε MAX! π.χ. _POSIX_ARG_MAX, _POSIX_LINK_MAX, _POSIX_PIPE_BUF, POSIX_SSIZE_MAX Τιμές που ορίζονται από την υλοποίηση ARG_MAX, LINK_MAX, PIPE_BUF, SSIZE_MAX Στη sysconf πρέπει να προτάσσεται η συμβολοσειρά _SC_, στην pathconf η _PC_ Τιμές που ορίζονται κατά τη μεταγλώττιση _POSIX_VERSION, _POSIX_JOB_CONTROL Τιμές που μπορεί να αλλάξουν, ανάλογα με τις συνθήκες (π.χ. το σύστημα αρχείων στο οποίο αναφερόμαστε) _POSIX_CHOWN_RESTRICTED, _POSIX_NO_TRUNC Κάποια υλοποίηση μπορεί να μην θέτει κάποιο όριο (π.χ. απεριόριστος αριθμός ανοικτών αρχείων). Σ’ αυτή την περίπτωση οι sysconf/pathconf επιστρέφουν –1 και ΔΕΝ ΑΛΛΑΖΟΥΝ την τιμή του errno Αντίστοιχα όρια ορίζονται και από το XPG3
Παράδειγμα ελέγχου ορίων της ANSI C #if (FLT_DIG >= 8) && (FLT_EPSILON < 1e-8f) #define FLT_READ_FMT "%f" typedef flt_type float; #else #define FLT_READ_FMT "%lf" typedef flt_type double; #endif … int main(int argc, char *argv[]) { flt_type params[10]; int i; for (i = 0; i < 10; i++) { printf("Enter parameter #%d", i); scanf(FLT_READ_FMT, params[i]); }
Παράδειγμα Ελέγχου Δυνατοτήτων με #ifdef void play_tic_tac_toe_game () { #ifdef HAVE_CURSES initscr(); cbreak(); noecho(); game_window = newwin(6 /*lines */, 6 /* columns */, 2 /* x */, 2 /* y */); #elsifdef __WINDOWS__ clrscr(); gotoxy(2, 2); game_window = NULL; #else printf("\033[2J"); /* ANSI code for clearing screen */; printf("\033[2;2H"); #endif
Παράδειγμα ελέγχου βάσει προτύπου #ifdef SYSV # include <termio.h> # define TTYSTRUCT termio # define stty(fd,buf) ioctl((fd),TCSETA,(buf)) # define gtty(fd,buf) ioctl((fd),TCGETA,(buf)) struct termio tty; struct termio oldtty; #else /* not system V */ # include <sgtty.h> #define stty(fd,buf) ioctl((fd),TIOCSETN,(buf)) #define gtty(fd,buf) ioctl((fd),TIOCGETP,(buf)) struct sgttyb tty; struct sgttyb oldtty; #endif
Παράδειγμα ελέγχου ορίων [sysconf.c] #include <unistd.h> #include <errno.h> int main (void) { long res; errno = 0; if ((res = sysconf(_SC_ARG_MAX)) == -1) if (errno == 0) printf("_SC_ARG_MAX: no limit defined\n"); else perror("_SC_ARG_MAX constant:"); else printf("_SC_ARG_MAX = %ld\n", res); if ((res = pathconf("/", _PC_LINK_MAX)) == -1) printf("_PC_LINK_MAX: no limit defined\n"); perror("_PC_LINK_MAX constant:"); else printf("_PC_LINK_MAX = %ld\n", res); return 0; }
Παράδειγμα ελέγχου ορίων [guess-sysconf.c] #include <unistd.h> #include <errno.h> #include <limits.h> #ifdef PATH_MAX size_t max_path_len = PATH_MAX; #else size_t max_path_len = 0; #endif #define MAX_PATH_GUESS 8192 size_t get_maxpath(void) { if (max_path_len == 0) { errno = 0; if ((max_path_len = pathconf("/", _PC_PATH_MAX)) == -1) { if (errno == 0) max_path_len = MAX_PATH_GUESS; else { perror("_PC_PATH_MAX: "); exit(1); } return max_path_len;
Παραδείγματα ελέγχου δυνατοτήτων Ο προεπεξεργαστής της C ορίζει αυτόματα κάποια σύμβολα (π.χ. __STDC__, _POSIX_SOURCE, __STDC_VERSION__, κ.λπ.) ενώ κάποια άλλα μπορούν να ορίζονται από όσους κάνουν compile το πρόγραμμα (π.χ. HAVE_POSIX_SIGNALS, HAVE_MEMSET κ.ο.κ.) Τα σύμβολα αυτά είναι χρήσιμα για τη διαμόρφωση του προγράμματός μας #ifdef __STDC__ void *myalloc(int numbytes); #else char *myalloc(); #endif #ifndef HAVE_MEMSET char *memset(char *ptr, int c, size_t nbytes) { char *retval = ptr; while (nbytes-- > 0) *(ptr++) = c; return retval; }
Μεταγλώττιση και ενδείξεις γραμμής εντολών
Μεταγλώττιση προγραμμάτων Πολλά σφάλματα σε προγράμματα οφείλονται στο ότι αγνοούμε μηνύματα προειδοποίησης ή χρησιμοποιούμε επισφαλείς συναρτήσεις της C ή δεν εφαρμόζουμε σωστές πρακτικές, π.χ.: Δεν ελέγχουμε τιμές επιστροφής για σφάλματα Χρήση της printf με λάθος πλήθος ή τύπο ορισμάτων Χρήση μεταβλητών που δεν έχουν αρχικοποιηθεί Χρήση συναρτήσεων που μπορούν να οδηγήσουν σε υπερχείλιση, π.χ. gets
Εντοπισμός και διόρθωση ζητημάτων Ο μεταγλωττιστής! gcc –pedantic –Wall –o output filename filename(s).c –llib(s) Οι ενδείξεις –pedantic και –Wall ενεργοποιούν την αναφορά όλων των προειδοποιήσεων, τις οποίες πρέπει στη συνέχεια να διορθώσουμε Οι προειδοποιήσεις περιλαμβάνουν και άλλες χρήσιμες για την ποιότητα του κώδικα πληροφορίες, όπως π.χ. μεταβλητές που δηλώνονται μεν αλλά δεν χρησιμοποιούνται
Εντοπισμός και διόρθωση ζητημάτων Εργαλείο splint - Secure Programming lint Αναφέρει προβλήματα όπως: Χρήση δεικτών με τιμή NULL Χρήση μνήμης που δεν έχει δεσμευτεί Ασυμφωνία τύπων, με μεγαλύτερη ακρίβεια απ’ ό,τι οι περισσότεροι μεταγλωττιστές Ατέρμονους βρόχους, μη ελεγχόμενες περιπτώσεις σε switch, κ.ο.κ. Χρήση συναρτήσεων που οδηγούν σε υπερχείλιση μνήμης Μη χρήση return σε διαδρομές συναρτήσεων Μη έλεγχος αποτελεσμάτων συνάρτησης
Splint: παραδείγματα Κώδικας: Προειδοποίηση splint: Διόρθωση: dup2(fd2, fd) Προειδοποίηση splint: a.c:9:1: Return value (type int) ignored: dup2(fd2, fd) Διόρθωση: if (dup2(fd2, fd) == -1) { /* error handle */ } Δυνατότητα 2: αν είμαστε ΑΠΟΛΥΤΑ ΒΕΒΑΙΟΙ ότι δεν μας ενδιαφέρει η τιμή επιστροφής, κάνουμε type cast το αποτέλεσμα σε void: (void) free(somePointer);
Splint: παραδείγματα Κώδικας: Προειδοποίηση splint: Διόρθωση: char s[20]; gets(s); Προειδοποίηση splint: a.c:8:1: Use of gets leads to a buffer overflow vulnerability. Use fgets instead: gets Διόρθωση: (void) fgets(s, 19, stdin); /* Προσοχή: η fgets ΑΦΗΝΕΙ το \n στη συμβολοσειρά – μάλλον θα θέλουμε να το αφαιρέσουμε */ if (s[strlen(s) – 1] == ‘\n’) s[strlen(s)] = ‘\0’;
Splint: παραδείγματα Κώδικας: Προειδοποίηση splint: Διόρθωση: fp = fopen("thefile", "r"); fscanf(fp, "%d", &i); Προειδοποίηση splint: a.c:10:8: Possibly null storage fp passed as non-null param: fscanf (fp, ...) a.c:10:1: Return value (type int) ignored: fscanf(fp, "%d", &i) Διόρθωση: if ((fp = fopen("thefile", "r")) == NULL) { /* error handle */ } else { if (fscanf(fp, "%d", &i) == 0) { /* error reading */ } else { /* normal read */ }
Ενδείξεις & ορίσματα γραμμής εντολών Πολλές εντολές δέχονται προαιρετικές ενδείξεις λειτουργίας στη γραμμή εντολών όπως και ορίσματα Μπορούμε να διατρέξουμε τον πίνακα argv και να τα επεξεργαστούμε ένα προς ένα Μπορούμε επίσης να χρησιμοποιήσουμε τη συνάρτηση getopt
Συνάρτηση getopt int getopt (int argc, char **argv, const char *options); argc και argv είναι οι σχετικές παράμετροι της main options είναι οι πιθανές ενδείξεις που μπορούν να παρατεθούν στη συγκεκριμένη εντολή Η getopt επιστρέφει τον χαρακτήρα ένδειξης που εντοπίστηκε ή -1, αν δεν υπάρχουν επιπλέον ενδείξεις Τυπικά χρησιμοποιούμε έναν βρόχο για να διατρέξουμε όλες τις ενδείξεις – και έναν δεύτερο για να προσπελάσουμε τα ορίσματα που δεν είναι ενδείξεις Οι μεταβλητές optarg και optopt χρησιμοποιούνται για να έχουμε πρόσβαση σε ορίσματα που αφορούν τη συγκεκριμένη ένδειξη ή στην ένδειξη που εντοπίστηκε, αντίστοιχα
getopt - Παράδειγμα Έστω ότι θέλουμε να υλοποιήσουμε έναν μεταγλωττιστή C με τις ενδείξεις: gcc [-gc] [-o output] f1.c [f2.c … ] Ο σχετικός κώδικας με χρήση της getopt θα είναι: (επόμενη διαφάνεια)
getopt – Παράδειγμα (1/2) [optsgcc.c] int main(int argc, char *argv[]) { int i, hasC = 0, hasG = 0, hasO = 0; char arg, *oArg = NULL; while ((arg = getopt (argc, argv, "cgo:")) != -1) { switch (arg) { case 'c': hasC = 1; break; case 'g': hasG = 1; break; case 'o': hasO = 1; oArg = optarg; break; case '?': if (optopt == 'o') fprintf (stderr, "Option -%c requires an argument.\n", optopt); else if (isprint (optopt)) fprintf (stderr, "Unknown option '-%c'.\n", optopt); else fprintf (stderr, "Unknown option character '\\x%x'.\n", optopt); break; default: abort (); }
getopt – Παράδειγμα (2/2) printf("hasC = %d, hasG = %d, hasO = %d, oArg = %s\n", hasC, hasG, hasO, (hasO == 1) ? oArg : "NULL"); for (i = optind; i< argc; i++) printf ("Non-option argument %s\n", argv[i]); }
Συνάρτηση getopt_long Π.χ. --help που (μπορεί να) θεωρείται ισοδύναμη με τη συνοπτική μορφή -h Πρότυπο: int getopt_long (int argc, char *const *argv, const char *shortopts, const struct option *longopts, int *indexptr); Ο πίνακας longopts με δομές τύπου struct option περιγράφει τις εκτεταμένες μορφές και ορίζει πως χειριζόμαστε την κάθε μία
Συνάρτηση getopt_long – struct option Η δομή έχει τα κάτωθι πεδία: const char *name; Το όνομα της ένδειξης int has_arg; Ορίζει αν η ένδειξη δέχεται πρόσθετο όρισμα. no_argument (0) όχι όρισμα, required_argument (1) υποχρεωτική παρουσία ορίσματος, optional_argument (2) προαιρετική παρουσία ορίσματος int *flag, int val;: Αν flag == NULL, τότε όταν εντοπισθεί η ένδειξη επιστρέφεται η τιμή val (προφανώς οι τιμές val πρέπει να είναι διαφορετική για διαφορετικές ενδείξεις). Αν flag != NULL, τότε όταν εντοπισθεί η ένδειξη εκτελείται *flag = val; και επιστρέφεται 0. Η τελευταία καταχώρηση του πίνακα longopts πρέπει να έχει την τιμή {NULL, 0, NULL, 0}
Παράδειγμα getopt_long (optsgcc-long) Στο παράδειγμα του μεταγλωττιστή προσθέτουμε τις εκτεταμένες ενδείξεις --debug ισοδύναμο προς το -g --compile ισοδύναμο προς το -c --output ισοδύναμο προς το -o Ο πίνακας longopts θα οριστεί ως εξής: const struct option longopts[] = { {"debug", no_argument, NULL, 'g'}, {"compile", no_argument, NULL, 'c'}, {"output", required_argument, NULL, 'o'}, {NULL, 0, NULL, 0}}; και η κλήση arg = getopt (argc, argv, "cgo:"); θα αντικατασταθεί με arg = getopt_long(argc, argv, "cgo:", longopts, &nextArg);