Λειτουργικά Συστήματα (ΗΥ-345)
Χειμερινό Εξάμηνο 2013-2014
Άσκηση 3



Φροντιστήριο: 2/12/2013
Παράδοση: 17/12/2013

Προσθήκη νέων system calls στο λειτουργικό σύστημα Linux


Στην 3η άσκηση του μαθήματος θα προσθέσετε δύο καινούργια system calls στον πυρήνα (kernel) του λειτουργικού συστήματος (OS) Linux. Το πρώτο system call (setproclimit()) θα ορίζει μια τιμή για το μέγιστο ποσοστό χρόνου που μια διεργασία, μαζί με όλες τις θυγατρικές της διεργασίες (process family), θα μπορούν να τρέξουν στον επεξεργαστή. Το δεύτερο system call (getproclimit) θα επιστρέφει το παραπάνω όριο για το process family μιας διεργασίας (αν έχει οριστεί) και τον συνολικό χρόνο (μαζί με κάποια άλλα στατιστικά) που έχουν τρέξει αυτές οι διεργασίες στον επεξεργαστή, όπως θα σας εξηγήσουμε παρακάτω. Έπειτα θα τα δοκιμάσετε χρησιμοποιώντας τον προσομοιωτή (emulator) QEMU: αφού κάνετε compile τον Linux kernel με τις δικές σας αλλαγές, θα τρέξετε το Linux με τον καινούργιο kernel στον προσομοιωτή QEMU, και εκεί θα γράψετε και θα τρέξετε κάποια user-level test προγράμματα που θα χρησιμοποιούν τα καινούργια system calls.

Σε αυτήν την άσκηση θα πρέπει να υλοποιήσετε και να δοκιμάσετε τα δυο καινούργια system calls. Αυτά τα system calls θα τα χρησιμοποιήσετε και στην επόμενη άσκηση του μαθήματος (4η άσκηση) όπου θα τροποποιήσετε τον scheduler του Linux kernel έτσι ώστε να εφαρμόσετε τελικά το όριο στον χρόνο εκτέλεσης των διεργασιών που θα δηλώνετε με την βοήθεια του setproclimit system call.

Στόχος της άσκησης είναι να εξοικειωθείτε με τον source code του Linux kernel, με τον τρόπο που είναι δομημένο ένα system call στο Linux, με την μεταγλώττιση του kernel, και με την χρήση ενός προσομοιωτή.


1. Εκτέλεση Linux στον QEMU Emulator

Σε αυτήν την άσκηση θα χρησιμοποιήσετε τον QEMU emulator για να τρέξετε ένα λειτουργικό σύστημα Linux μέσα από κάποιο άλλο λειτουργικό σύστημα. Οι emulators είναι πολύ διαδεδομένοι για πολλούς λόγους. Για παράδειγμα, μας επιτρέπουν να εγκαταστήσουμε και να τρέξουμε ένα λειτουργικό σύστημα σαν απλοί χρήστες σε έναν υπολογιστή που έχει κάποιο άλλο λειτουργικό σύστημα, χωρίς να χρειαστεί να αλλάξουμε κάτι σε αυτό. Αυτόν τον υπολογιστή μπορεί να τον χρησιμοποιούν αρκετοί χρήστες, και κάθε ένας μπορεί να τρέχει διαφορετικό λειτουργικό σύστημα με έναν emulator χωρίς να επηρεάζονται οι υπόλοιποι χρήστες. Ιδιαίτερα όταν θέλουμε να δοκιμάσουμε κάποιες αλλαγές στον kernel ενός λειτουργικού συστήματος, όπως θα κάνουμε σε αυτήν την άσκηση, ο emulator είναι αρκετά χρήσιμος για έναν ακόμα λόγο: αν λόγω κάποιου προγραμματιστικού λάθους στον kernel το σύστημα καταρρεύσει (π.χ. kernel panic) μπορούμε εύκολα και γρήγορα να ξεκινήσουμε ξανά το λειτουργικό σύστημα με κάποια αλλαγή (debugging) χωρίς να επηρεαστεί το βασικό λειτουργικό σύστημα του υπολογιστή (host operating system).

Για αυτήν την άσκηση θα χρησιμοποιήσετε τον QEMU emulator. Ο QEMU υπάρχει ήδη εγκατεστημένος στα μηχανήματα του τμήματος (man qemu για περισσότερες πληροφορίες). Ο QEMU emulator μπορεί να δημιουργήσει και να διαβάσει έναν εικονικό δίσκο (virtual disk image) και σε αυτόν μπορούμε να εγκαταστήσουμε ένα οποιοδήποτε λειτουργικό σύστημα (π.χ. από ένα εικονικό cd rom). Για τους σκοπούς της άσκησης έχουμε εγκαταστήσει για σας ένα απλό Linux OS (ttylinux distribution) για αρχιτεκτονική 32-bit x86 (i386) σε ένα virtual disk image που θα πρέπει να χρησιμοποιήσετε για την άσκηση σας. Εσείς θα πρέπει αρχικά να αντιγράψετε αυτό το disk image (63 MB) από την περιοχή του μαθήματος (~hy345/qemu-linux/hy345-linux.img) σε έναν κατάλογο στο /spare του μηχανήματος (π.χ. /spare/[username]) που χρησιμοποιείτε, ώστε να μην έχετε πρόβλημα χώρου (π.χ. quota exceeded) με την περιοχή σας:
$ cp ~hy345/qemu-linux/hy345-linux.img /spare/[username]/
Προσέξτε να έχετε τα κατάλληλα permissions στον κατάλογο [username] ώστε να έχετε μόνο εσείς read/write access (chmod 700 /spare/[username]).

Το παραπάνω disk image έχει εγκατεστημένο το Linux OS που θα χρησιμοποιήσετε. Περιέχει το root filesystem (/) στο οποίο υπάρχουν τα βασικά προγράμματα και tools του συστήματος. Περιέχει επίσης τον αρχικό Linux kernel 2.6.38.1 που χρησιμοποιεί για να ξεκινήσει το λειτουργικό σύστημα. Οπότε χρησιμοποιώντας το image αυτό μπορείτε να δοκιμάσετε να ξεκινήσετε αυτό το Linux OS με τον QEMU emulator, απλά με την παρακάτω εντολή:
$ qemu -hda hy345-linux.img
Η παράμετρος -hda hy345-linux.img κάνει τον QEMU να χρησιμοποιεί το αρχείο hy345-linux.img σαν virtual disk image, το οποίο θα φαίνεται σαν το device /dev/hda στο emulated OS (guest operating system). Τρέχοντας την παραπάνω εντολή θα δείτε να ξεκινάει το Linux OS που προσομοιώνουμε. Όταν σας ζητήσει να κάνετε login χρησιμοποιήστε το account με username "user" και password "csd-hy345", όπως θα σας τυπώσει και το σύστημα. Επίσης μπορείτε να κάνετε login και με το account του "root" με password "hy345".


2. Μεταγλώττιση του αλλαγμένου Linux kernel

Το επόμενο βήμα είναι να αλλάξετε τον Linux kernel, να τον μεταγλωττίσετε (compile), και να φτιάξετε ένα καινούργιο kernel image με το οποίο θα μπορείτε να ξεκινήσετε το guest Linux OS με τον QEMU, αντί για το original kernel image που υπάρχει στο disk image που σας δίνουμε. Θα μπορούσατε να αλλάξετε και να μεταγλωττίσετε τον kernel μέσα από το guest OS. Για ευκολία όμως θα δουλέψετε στο host OS, δηλαδή κατευθείαν στο μηχάνημα του τμήματος που χρησιμοποιείτε για την άσκηση. Αρχικά θα χρειαστείτε τον source code του Linux kernel 2.6.38.1 για να κάνετε τις απαιτούμενες αλλαγές και να τον κάνετε compile. Οπότε θα πρέπει να αντιγράψετε τον source code από την περιοχή του μαθήματος (~hy345/qemu-linux/linux-2.6.38.1.tar.bz2) στον κατάλογο που χρησιμοποιείτε στο /spare/[username] του μηχανήματος.

Αφού αντιγράψετε και κάνετε decompress τον source code του Linux kernel 2.6.38.1, μπορείτε να κάνετε ότι αλλαγές απαιτούνται για την υλοποίηση των καινούργιων system calls που περιγράφεται στο βήμα 4 παρακάτω. Έπειτα υπάρχουν δύο απλά βήματα που πρέπει να ακολουθήσετε για να κάνετε compile τον αλλαγμένο kernel και να φτιάξετε ένα καινούργιο kernel image: configure και make.

Υπάρχουν διάφοροι τρόποι για να κάνει κάποιος configure τον Linux kernel (π.χ. make menuconfig, make config, κτλ). Τελικά το configuration του kernel γράφεται στο αρχέιο .config μέσα στον κατάλογο linux-2.6.38.1. Επειδή υπάρχουν πολλές επιλογές για το configuration του Linux kernel, σας δίνουμε εμείς έτοιμο ένα configuration που είναι συμβατό με το Linux OS που έχουμε εγκαταστήσει στο disk image. Θα αντιγράψετε από την περιοχή του μαθήματος το configuration file (~hy345/qemu-linux/.config) ώστε να το χρησιμοποιήσετε για τον kernel που θα φτιάξετε. Το μόνο option που πρέπει να αλλάξετε στο .config είναι το CONFIG_LOCALVERSION. Εκεί πρέπει να προσθέσετε μια κατάληξη για το όνομα (version) του καινούργιου kernel που θα φτιάξετε, έτσι ώστε να είστε σίγουροι ότι χρησιμοποιείτε τον δικό σας kernel όταν θα τον φορτώσετε με τον QEMU (και όχι τον original kernel). Επίσης, αν επαναλάβετε την διαδικασία περισσότερες φορές, θα μπορείτε να ξεχωρίζετε τα διαφορετικά revisions του kernel που έχετε δοκιμάσει, αλλάζοντας το kernel version πριν από κάθε μεταγλώττιση. Οπότε στο CONFIG_LOCALVERSION θα πρέπει να βάλετε το username σας, και αν θέλετε και ένα revision number. Το version του kernel θα μπορείτε να το δείτε όταν ξεκινάει το OS, ή αφού έχετε κάνει login με την εντολή uname -a.

Τέλος, για να γίνει compile o kernel με τις δικές σας αλλαγές, θα τρέξετε make ARCH=i386 bzImage. Το καινούργιο kernel image (bzImage) θα είναι το αρχείο "linux-2.6.38.1/arch/x86/boot/bzImage". (Αφού τον καινούργιο kernel δεν θα τον χρησιμοποιήσετε στο host OS, δεν χρειάζεται να κάνετε make install). Παρακάτω περιγράφουμε συνοπτικά τα βήματα για να κάνετε compile τον kernel.
$ cp ~hy345/qemu-linux/linux-2.6.38.1.tar.bz2 /spare/[username]/
$ tar -jxvf linux-2.6.38.1.tar.bz2
$ cd linux-2.6.38.1
 Edit kernel source code to implement the new system calls
$ cp ~hy345/qemu-linux/.config
 Edit .config, find CONFIG_LOCALVERSION="-hy345", and append to the kernel's version name your username and a revision number
$ make ARCH=i386 bzImage



3. Εκτέλεση του αλλαγμένου Linux kernel με το QEMU

Το επόμενο βήμα είναι να χρησιμοποιήσετε το καινούργιο kernel image (linux-2.6.38.1/arch/x86/boot/bzImage) με τις αλλαγές που κάνατε στον kernel για να ξεκινήσετε το Linux με το QEMU. Θα χρησιμοποιήσετε ξανά το virtual disk image που σας δώσαμε, αλλά θα δώσετε επίσης στο QEMU το kernel image που θα τρέξει:
$ qemu -hda hy345-linux.img  -append "root=/dev/hda" -kernel linux-2.6.38.1/arch/x86/boot/bzImage
Με το -kernel linux-2.6.38.1/arch/x86/boot/bzImage το QEMU θα ξεκινήσει με το καινούργιο kernel image. Με το -append "root=/dev/hda" το QEMU θα κάνει mount to root filesystem από το /dev/hda, που είναι το disk image που φορτώνετε όπως και πριν. Αφού κάνετε login, με την εντολή uname -a βλέπετε την έκδοση του kernel που τρέχει το σύστημα.


4. Προσθήκη νέων system calls στον Linux kernel

Η αλλαγή που θα κάνετε στον Linux kernel 2.6.38.1 είναι να προσθέσετε δύο καινούργια system calls που θα λέγονται setproclimit και getproclimit. To setproclimit Θα δίνει δύο τιμές σε δύο καινούργια πεδία που θα προσθέσετε σε κάθε διεργασία, ενώ το getproclimit θα επιστρέφει τις τιμές αυτές και επίσης τον χρόνο εκτέλεσης (user+system time) ενός συνόλου διεργασιών (συνολικός χρόνος εκτέλεσης, μέγιστος και ελάχιστος). Επίσης, θα πρέπει να προσθέσετε τρία καινούργια πεδία στην δομή task_struct που αναπαριστά μια διεργασία στον Linux kernel. Η τιμή σε αυτά τα τρία πεδία θα αλλάζει από το system call setproclimit, ενώ το system call getproclimit θα διαβάζει αυτές τις τιμές και θα επιστρέφει τα αντίστοιχα αποτελέσματα.

Αρχικά, θα προσθέσετε τα παρακάτω τρία καινούργια πεδία στην δομή task_struct (που είναι ορισμένη στο αρχείο linux-2.6.38.1/include/linux/sched.h):
 
pid_t root_pid;
int time_limit;
int time_interval;
 
Αυτά τα τρία πεδία θα πρέπει να αρχικοποιούνται με -1 (στο αρχείο linux-2.6.38.1/include/linux/init_task.h). Έτσι, αν για κάποια διεργασία (και για την πατρικής της και τους προγόνους της) δεν έχει καλεστεί το system call setproclimit, οι τιμές που θα έχουν τα παραπάνω πεδία θα πρέπει να είναι -1 (που σημαίνει οτι δεν υπάρχει κάποιο όριο για την εκτέλεσή τους και δεν έχει οριστεί κάποιο process family για αυτήν την διεργασία αν το root_pid είναι ίσο με -1).

To system call setproclimit θα παίρνει τρία ορίσματα:
 
int setproclimit(int pid, int limit, int interval);
 
Το πρώτο argument (pid) είναι το pid της διεργασίας που θα αλλάξετε τις τιμές root_pid, time_limit και time_inteval. Αν το pid είναι -1, τότε μας ενδιαφέρει η τρέχουσα διεργασία που καλεί το system call. Αν έχει άλλη τιμή, τότε θα πρέπει να βρείτε την διεργασία που έχει αυτό το pid και έπειτα να αλλάξετε τα παραπάνω τρία πεδία σε αυτήν την διεργασία. Αν η τιμή του pid είναι αρνητική (αλλά όχι -1), ή αν δεν αντιστοιχεί σε κάποια τρέχουσα διεργασία, τότε το system call θα πρέπει επιστρέφει το error value EINVAL. Το EINVAL και τα υπόλοιπα error values είναι ορισμένα στο linux-2.6.38.1/include/asm-generic/errno-base.h.

Μόλις βρείτε την σωστή διεργασία (τρέχουσα διεργασία ή διεργασία με pid=pid), ο kernel θα πρέπει να θέτει στα πεδία time_limit και time_inteval τις αντίστοιχες τιμές από το δεύτερο και τρίτο όρισμα του system call (limit και inteval). Επίσης, το πεδίο root_pid θα γίνετε ίσο με το pid της συγεκριμένης διεργασίας. Επιπλέον, αν αυτή η διεργασία έχει θυγατικές διεργασίες (και αντίστοιχα αν οι θυγατρικές της διεργασίες έχουν άλλα παιδιά) θα πρέπει να διατρέχετε όλες αυτές τις διεργασίες και να αλλάζετε τα πεδία τους root_pid, time_limit και time_inteval με τα αντίστοιχα τρία ορίσματα του system call. Το root_pid θα πρέπει να είναι το pid της αρχικής διεργασίας για την οποία έγινε το system call (το πρώτο όρισμα του system call ή το pid της τρέχουσας διεργασίας αν το πρώτο όρισμα είναι -1).

Έτσι, θα έχουμε ορίσει ένα process family (αρχίζοντας από την διεργασία με pid=root_pid) και δύο τιμές limit και interval για αυτό το process family. Αυτές οι δύο τιμές έχουν στόχο να περιορίσουν το ποσοστό χρόνου εκτέλεσης του συγκεκριμένου process family, έτσι ώστε ο συνολικός χρόνος εκτέλεσης αυτών τον διεργασιών μέσα σε ένα χρονικό διάστημα time_interval milliseconds να μην ξεπερνάει τα limit milliseconds. Αυτό θα το υλοποιήσετε στην επόμενη άσκηση.

Αν το δεύτερο όρισμα limit είναι μεγαλύτερο από το τρίτο όρισμα interval, τότε το setproclimit θα πρέπει να επιστρέφει με το error value EINVAL, καθώς αυτό δεν είναι αποδεκτό. Αν όλα τα πάνε καλά και το system call τρέξει επιτυχώς, αυτό θα πρέπει να επιστρέφει 0. Αλλιώς θα πρέπει να επιστρέφει το error value EINVAL. Το user-level πρόγραμμα που χρησιμοποιεί το system call θα πρέπει να ελέγχει την επιστρεφόμενη τιμή για ενδεχόμενο λάθους.

To system call getproclimit θα παίρνει δύο ορίσματα:
 
int getproclimit(int pid, struct proclimit *pl);
 
Το πρώτο argument (pid) είναι το pid της διεργασίας για της οποίας το process family θα επιστρέψετε τις πληροφορίες που θέλουμε. Αν το pid είναι -1, τότε μας ενδιαφέρει το process family της τρέχουσας διεργασίας που καλεί το system call. Αν έχει άλλη τιμή, τότε θα πρέπει να βρείτε την διεργασία που έχει αυτό το pid και έπειτα τα στοιχεία που σας ζητάμε για το process family αυτής της διεργασίας. Αν η τιμή του pid είναι αρνητική (αλλά όχι -1), ή αν δεν αντιστοιχεί σε κάποια τρέχουσα διεργασία, τότε το system call θα πρέπει επιστρέφει το error value EINVAL. Το EINVAL και τα υπόλοιπα error values είναι ορισμένα στο linux-2.6.38.1/include/asm-generic/errno-base.h.

Το δεύτερο argument (pl) είναι ένας pointer σε ένα struct τύπου struct proclimit. Για αυτό το struct θα πρέπει να δεσμεύει μνήμη το user-level πρόγραμμα πριν καλέσει το system call. Στην συνέχεια ο kernel θα πρέπει να το γεμίζει με όλες τις πληροφορίες που χρειάζονται για το process family της διεργασίας που ζητήθηκε. Έτσι θα επιστρέφει αυτές τις πληροφορίες (by reference) στο user-level πρόγραμμα που κάλεσε το getproclimit system call. Αν το αυτό το δεύτερο όρισμα είναι NULL, τότε το getproclimit θα πρέπει να επιστρέφει ξανά το error value EINVAL. Όπως επίσης αν συμβεί οποιοδήποτε άλλο λάθος (π.χ. αν ο kernel δεν μπορεί να αντιγράψει τις ζητούμενες πληροφορίες σε user space), θα επιστρέφεται το ίδιο error value. Αν όλα τα πάνε καλά και το system call τρέξει επιτυχώς, αυτό θα πρέπει να επιστρέφει 0. Αντίστοιχα, το user-level πρόγραμμα που χρησιμοποιεί το system call θα πρέπει να ελέγχει την επιστρεφόμενη τιμή για ενδεχόμενο λάθους. Όπως είπαμε, αν το return value του system call είναι 0 (success), τότε τα πραγματικά αποτελέσματα θα επιστραφούν μέσω του struct proclimit *pl.

θα πρέπει εσείς να ορίσετε το struct proclimit στο καινούργιο αρχείο linux-2.6.38.1/include/proclimit.h που θα δημιουργήσετε. Όπως είπαμε, αυτό το struct θα περιέχει τις πληροφορίες που χρειαζόμαστε για το process family της διεργασίας με το pid που δίνετε στο πρώτο όρισμα του getproclimit, ή της τρέχουσα διεργασίας αν αυτό είναι ίσο με -1. Αυτές οι πληροφορίες θα είναι το root_pid του process family (ή -1 αν δεν υπάρχει), οι τιμές για τα πεδία time_limit και time_interval που έχουν δωθεί για αυτό το process family με το setproclimit() system call (ή -1 αν δεν έχουν δωθεί τιμές), o συνολικός χρόνος εκτέλεσης όλων των διεργασιών στο process family, ο μεγαλύτερος και ο μικρότερος χρόνος εκτέλεσης από τις διεργασίες που ανήκουν στο process family. Αν η διεργασία δεν ανήκει σε κάποιο process family, όλες αυτές οι τιμές θα πρέπει να είναι ίσες με -1. Ο χρόνος εκτέλεσης μιας διεργασίας ισούται με το άθροισμα user time και system time για αυτήν την διεργασία, και μπορείτε να τον βρείτε κατευθείαν από τα αντίστοιχα πεδία στην δομή task_struct του kernel. Για να βρείτε τον συνολικό χρόνο εκτέλεσης και τον μεγαλύτερο/μικρότερο χρόνο εκτέλεσης από όλες τις διεργασίες στο process family μιας διεργασίας με το pid που δίνεται, θα πρέπει να διατρέξετε και όλες τις θυγατρικές της διεργασίες (εφόσον υπάρχουν) και όλες τις πατρικές διεργασίες μέχρι να φτάσετε στην διεργασία με root_pid. Οι δομές για να βρείτε τις θυγατρικές και πατρικές διεργασίες υπάρχουν επίσης στο task_struct. Εναλλακτικά μπορείτε να διατρέξετε την λίστα με όλες τις διεργασίες και να βρείτε αυτές που ανήκουν στο συγεκριμένο process family (ίδιο root_pid).

Αναλυτικά, το struct proclimit θα πρέπει να έχει τα παρακάτω πεδία:
  
struct proclimit {			// info and times about the process family of a given process
   pid_t root_pid;			// pid of the root process of the process family
   int time_limit;			// time limit for this process family
   int time_interval;			// time interval for enforcing the time limit
   unsigned long total_cpu_time; 	// total cpu time of the process family
   unsigned long max_cpu_time;  	// max cpu time of a process in this family
   unsigned long min_cpu_time;  	// min cpu time of a process in this family
}
 
Το system call getproclimit θα πρέπει να γεμίζει όλα αυτά τα πεδία για ένα process family μιας διεργασίας. Το pid_t ορίζετε στο linux-2.6.38.1/include/linux/types.h.

Τέλος, κάθε φορά που καλούνται τα system calls setproclimit και getproclimit στο επίπεδο του πυρήνα, θα πρέπει να τυπώνετε με την συνάρτηση printk μια γραμμή με το όνομα σας, τον Α.Μ. σας, και το όνομα του system call. Τα μηνύματα που τυπώνετε στον kernel με την συνάρτηση printk μπορείτε να βλέπετε όταν έχετε φορτώσει το Linux με τον συγκεκριμένο kernel τρέχοντας το dmesg ή κάνοντας cat /var/log/messages. Έτσι, κάθε φορά που καλούνται τα συγκεκριμένα system calls από ένα user-level πρόγραμμα, θα πρέπει με αυτόν τον τρόπο να βλέπουμε το μήνυμα που εκτυπώσατε με το όνομά σας από τον kernel. Με τον ίδιο τρόπο (printk) μπορείτε να εκτυπώνετε ότι άλλα μηνύματα θέλετε από τον kernel (π.χ. ένας απλός τρόπος να κάνετε debugging το system call που φτιάχνετε) και να τα βλέπετε όταν τρέχει ο kernel, αφού καλέσετε το system call, από το dmesg.

Hint 1: Γενικές οδηγίες για την προσθήκη νέου system call στον Linux kernel.

Γενικές πληροφορίες για τα βήματα που πρέπει να ακολουθήσετε και τα αρχεία που πρέπει να αλλάξετε ή να δημιουργήσετε για να προσθέσετε ένα system call στον Linux kernel 2.6 μπορείτε να βρείτε εδώ . Ο παραπάνω οδηγός περιγράφει συνοπτικά πως είναι δομημένο ένα system call στον Linux kernel και πως μπορείτε να προσθέσετε ένα καινούργιο. Επίσης μπορείτε να βρείτε αρκετά σχετικά tutorials στο Web. Θα σας αναφέρουμε και εμείς συνοπτικά τα βασικά βήματα που χρειάζονται και τα αρχεία που πρέπει να τροποποιήσετε (ή να δημιουργήσετε) σε γενικές γραμμές για να προσθέσετε ένα καινούργιο system call στον Linux kernel 2.6.

Υπάρχουν τρία βασικά βήματα για να υλοποιήσετε ένα καινούργιο system call στον Linux kernel:
  1. Πρέπει να προσθέσετε ένα καινούργιο system call number στον kernel για το δικό σας system call.
  2. Πρέπει να προσθέσετε ένα entry στον system call table του kernel με το system call number του δικού σας καινούργιου system call. Αυτό το entry θα καθορίσει ποια συνάρτηση του kernel θα εκτελεστεί όταν συμβεί ένα trap με το δικό σας system call number (όταν δηλαδή καλεστεί το system call από user level και ο έλεγχος μεταβεί στον kernel για την εκτέλεση του συγκεκριμένου system call, που ξεχωρίζει από το system call number).
  3. Πρέπει να προσθέσετε κώδικα στον kernel που να υλοποιεί την λειτουργικότητα που θα προσφέρει το system call. Για αυτό πρέπει επίσης να προσθέσετε τα κατάλληλα header files, για να ορίσετε καινούργιους τύπους και structs που χρησιμοποιεί το system call για να μεταφέρει πληροφορία μεταξύ kernel και user space. Ακόμα, θα πρέπει να αντιγράφετε arguments και αποτελέσματα μεταξύ kernel και user space με τις αντίστοιχες συναρτήσεις που υπάρχουν στον kernel.

Θα σας εξηγήσουμε λίγο πιο αναλυτικά τα τρία παραπάνω βήματα με ένα απλό παράδειγμα. Έστω ότι θέλουμε να προσθέσουμε ένα system call με όνομα dummy_sys το οποίο παίρνει ένα όρισμα από το user-level πρόγραμμα που τον κάλεσε: έναν ακέραιο αριθμό. Το dummy_sys system call θα τυπώνει απλά αυτόν τον αριθμό που δόθηκε σαν όρισμα και θα επιστρέφει τον διπλάσιο του στο user-level πρόγραμμα.
  1. Ανοίγουμε το linux-2.6.38.1/arch/x86/include/asm/unistd_32.h με κάποιον editor, βρίσκουμε τα system call numbers για τα system calls που υπάρχουν ήδη στον kernel, και μετά το τελευταίο system call number (π.χ. 340 στον δικό μας kernel) προσθέτουμε μία γραμμή με τον επόμενο αριθμό, π.χ. 341 στον δικό μας kernel:
     
    #define __NR_dummy_sys 341
    
    Επίσης αυξάνουμε το NR_syscalls κατά ένα (π.χ. από 341 σε 342 στον δικό μας kernel). Έτσι ορίσαμε το system call number 341 για το system call dummy_sys. Αυτός ο αριθμός θα χρησιμοποιηθεί μετά από ένα trap ώστε να βρεί ο kernel στον system call table την κατάλληλη συνάρτηση (system call function pointer) που υλοποιεί το system call.
  2. Στο δεύτερο βήμα ανοίγουμε το αρχείο linux-2.6.38.1/arch/x86/kernel/syscall_table_32.S και προσθέτουμε στην τελευταία γραμμή το όνομα της συνάρτησης που υλοποιεί το καινούργιο system call (system call function pointer):
     
    .long sys_dummy_sys /* 341 */
    
  3. Στο τρίτο βήμα θα υλοποιήσουμε το system call dummy_sys. Στο τέλος του αρχείου linux-2.6.38.1/include/asm-generic/syscalls.h θα προσθέσουμε το function prototype του system call:
    
    asmlinkage long sys_dummy_sys(int arg0);
    
    Αν έχετε να προσθέσετε type definitions πρέπει να φτιάξετε και ένα header file στο linux-2.6.38.1/include/linux/ το οποίο θα κάνετε include όπου χρειάζεται (δεν χρειαζόμαστε για το dummy_sys, αλλά εσείς θα χρειαστείτε για το getproclimit). Έπειτα φτιάχνουμε ένα καινούργιο αρχείο dummy_sys.c στο linux-2.6.38.1/kernel, π.χ. το linux-source-2.6.38.1/kernel/dummy_sys.c το οποίο περιέχει τον κώδικα του system call:
    
    #include <linux/kernel.h>
    #include <asm/uaccess.h>   
    #include <linux/syscalls.h>
    
    asmlinkage long sys_dummy_sys(int arg0)
    {
      printk("Called system call dummy_sys with argument: %d\n",arg0);
      return((long)arg0*2);
    }
    
    Αν έχετε arguments που περνάνε by reference από user space σε kernel space (π.χ. στο getproclimit) θα πρέπει να τα αντιγράψετε: αφού καλέσετε το access_ok(), θα τα αντιγράψετε καλώντας την συνάρτηση copy_from_user(). Αντίστοιχη διαδικασία θα κάνετε για να αντιγράψετε τα δεδομένα πίσω στο user space (access_ok και copy_to_user). Τέλος, θα πρέπει να αλλάξουμε το linux-source-2.6.38.1/kernel/Makefile για να συμπεριλάβει και να κάνει compile το καινούργιο source code αρχείο προσθέτοντας μια γραμμή στο κατάλληλο σημείο:
    
    obj-y += dummy_sys.o
    
Πιστεύουμε οτι σας φανούν ιδιαίτερα χρήσιμες οι εντολές grep και find, και ενδεχομένως το πρόγραμμα ctags αν χρησιμοποιείτε τον vim editor, για να μπορείτε να ψάχνετε funtion prototypes, definitions, structs, και οτι άλλο χρειάζεστε στον source code του Linux kernel. Επίσης είναι σημαντικό να δείτε πως έχουν υλοποιηθεί κάποια παρόμοια system calls που υπάρχουν ήδη στον Linux kernel (π.χ. gettimeofday, times, getpid και άλλα.)

Hint 2: Για τα συγκεκριμένα system calls.

Ο Linux kernel αποθηκεύει τις αναλυτικές πληροφορίες για όλες τις τρέχουσες διεργασίες σε ένα doubly linked list από task_struct structs. Μπορείτε να χρησιμοποιήσετε το macro for_each_process() για να προσπελάσετε όλες τις διεργασίες του συστήματος (task_struct) που είναι σε αυτήν την λίστα μια προς μια. Το struct task_struct ορίζεται στο αρχείο linux-2.6.38.1/include/linux/sched.h. Εκεί μπορείτε να βρείτε όλες τις πληροφορίες για κάθε διεργασία: το pid, το όνομα της διεργασίας, τον χρόνο που ξεκίνησε η εκτέλεση της, και τα user, system time κάθε διεργασίας. Επίσης, θα βρείτε έναν δείκτη στην πατρική διεργασία και μία λίστα με τις θυγατρικές διεργασίες αντίστοιχα (για κάθε διεργασία).

Για να βρείτε την τρέχουσα διεργασία κοιτάξτε στο αρχείο linux-2.6.38.1/include/asm/current.h. Εκεί υπάρχει το inline function current που επιστρέφει το task_struct entry της τρέχουσας διεργασίας.

Θα παρατηρήσετε ότι τα utime και stime (user time και system time) στο task_struct έιναι της μορφής cputime_t. Θα πρέπει αυτό να το μετατρέψετε σε unsigned long πριν επιστρέψετε τους χρόνους εκτέλεσης με το getproclimit. Μπορείτε να χρησιμοποιήσετε την συνάρτηση cputime_to_usecs() για αυτό το σκοπό.

Για να διατρέξετε τις λίστες του kernel (π.χ. θυγατρικές διεργασίες) θα σας φανούν χρήσιμα τα macros list_for_each() και list_entry(). Για παράδειγμα μπορείτε να διατρέξετε όλες τις θυγατρικές διεργασίες του current process με τον παρακάτω τρόπο:

struct list_head *list;
struct task_struct *process, *i;

process=current;

list_for_each(list, &process->children) {
	i=list_entry(list, struct task_struct, children);
	//do something with i
}
Εναλλακτικά μπορείτε να διατρέξετε ολες τις διεργασίες του συστήματος για να βρείτε της πληροφορίες που θέλετε. Για αυτό θα σας φανεί χρήσιμο το macro for_each_process:

struct task_struct *i;

for_each_process(i) {
	//do something with i
}
Τέλος, για να βρείτε περισσότερες πληροφορίες για το πώς δουλεύει ένα system call στον Linux kernel, που θα σας βοηθήσει πολύ και στην δικιά σας υλοποίηση, μπορείτε να ψάχνετε στον source code του Linux kernel για άλλα παρόμοια system calls που υπάρχουν ήδη στον kernel, όπως π.χ. το getpid ή το times. Για παράδειγμα, το getpid χρησιμοποιεί επίσης την συνάρτηση current για να βρεί την τρέχουσα διεργασία, ενώ το times επιστρέφει παρόμοιους χρόνους εκτέλεσης για την τρέχουσα διεργασία.




5. Δοκιμή του νέου system call

Στο τελευταίο βήμα της άσκησης θα πρέπει να δοκιμάσετε τα καινούργια system calls. Αφού έχετε κάνει compile με επιτυχία τον kernel με τα system calls που φτιάξατε, και έχετε ξεκινήσει τον QEMU με τον καινούργιο Linux kernel, θα πρέπει να γράψετε κάποια test προγράμματα που να χρησιμοποιούν τα setproclimit και getproclimit στο guest Linux OS. Συνήθως ένα system call καλείται μέσω κάποιας συνάρτησης που τρέχει σε user level και υπάρχει σε κάποια βιβλιοθήκη (π.χ. libc). Στην συνέχεια, αυτή η user-level συνάρτηση καλεί το macro syscall με το system call number του συγκεκριμένου system call για να μεταβιβάσει τον έλεγχο στον kernel (trap) και εκεί να τρέξει αυτό το system call. Αν δεν έχει υλοποιηθεί αυτή η συνάρτηση σε κάποια user-level library (θα πρέπει αυτό να γίνει μέσα στο guest Linux OS) ένα test προγράμμα μπορεί να τρέχει κατευθείαν το syscall macro με το system call number που έχει οριστεί για το system call στον αλλαγμένο kernel. Για παράδειγμα, το παρακάτω test πρόγραμμα καλεί το dummy_test system call με το system call number 341 που δείξαμε στο παραπάνω παράδειγμα, δίνοντας σαν όρισμα τον αριθμό 42. Μπορείτε να κάνετε compile το test πρόγραμμα κανονικά με τον gcc.
 
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

#define __NR_dummy_sys 341

int main()
{
  printf("Trap to kernel level\n");
  syscall(__NR_dummy_sys, 42);
  //you should check return value for errors
  printf("Back to user level\n");
}
Εσείς θα πρέπει είτε να κάνετε define δύο macros για τα setproclimit και getproclimit, ώστε τα syscalls να μοιάζουν με function calls, είτε να φτιάξετε δύο wrapper functions setproclimit και getproclimit (με τα ορίσματα που δέχονται αυτά τα system calls) που να καλούν εσωτερικά το αντίστοιχο syscall. Έτσι θα καλείτε το setproclimit ή getproclimit macro ή wrapper function αντί για το syscall μέσα στο πρόγραμμά σας, σύμφωνα με το function prototype που ορίσαμε. Για παράδειγμα, για το dummy_sys:

//either macro
#define dummy_sys(arg1) syscall(341, arg1)

//or wrapper function
long dummy_sys(int arg1) {
	syscall(341, arg1);
}

//and in the test program we just call:
dummy_sys(42);

Αν έχετε header files με definitions για νέους τύπους και structs, πρέπει επίσης να τα κάνετε include στο test πρόγραμμα (θα χρειαστεί να τα μεταφέρετε στο guest OS και ίσως χρειαστεί να δώσετε το path με αυτά με τα header files στον gcc). Για το getproclimit σας προτείνουμε να προσθέσετε τα definitions για τo struct proclimit, μαζί με τα macros ή inline wrapper functions των setproclimit και getproclimit, στο unistd.h header file που υπάρχει στο guest OS, και το οποίο θα κάνετε include σε όλα τα test προγράμματα.

Τα test προγράμματα που θα φτιάξετε θα πρέπει να δείχνουν ότι τα βασικά features των καινούργιων system calls δουλεύουν σωστά. Τρία χαρακτηριστικά test προγράμματα που θέλουμε να κάνετε είναι τα παρακάτω: Εκτός από αυτά τα τρία προγράμματα, μπορείτε να φτιάξετε όσα περισσότερα θέλετε ώστε να βεβαιωθείτε ότι τα system calls δουλεύουν σωστά.


X11 Forwarding - Για να δουλεύετε remotely με QEMU

Αν δουλεύετε remotely σε κάποιο μηχάνημα <host> του τμήματος, για να ξεκινήσετε το QEMU στο remote μηχάνημα θα πρέπει να συνδεθείτε με X11 forwarding από τον δικό σας υπολογιστή. Αν χρησιμοποιέιτε μηχάνημα Linux, όταν κάνετε enable <host> από το gate1/gate2 θα σας επιστρέψει το command που χρειάζεται να εκτελέσετε για να συνδεθείτε στο <host>, π.χ.
 ssh username@gate1.csd.uoc.gr -p 17724 
Για να κάνετε X11 forwarding και να μπορεί να ξεκινήσει το QEMU θα πρέπει επίσης να προσθέστε ένα flag -Y, π.χ.
 ssh username@gate1.csd.uoc.gr -p 17724 -Y 
Δοκιμάστε να τρέξετε xterm, θα πρέπει να σας ανοίξει το xterm. Εφόσον λειτουργεί το xterm, μπορείτε να χρησιμοποιήσετε το QEMU.

Αν έχετε Windows, πρέπει να γίνει κάποια αντίστοιχη διαδικασία.
  1. Κατεβάστε το Xming , εγκαταστήστε το και τρέξτε το.
  2. Kατεβάστε, εφόσον δεν το έχετε, το putty .
  3. Στο putty εφαρμόστε τις παρακάτω ρυθμίσεις: Αν θέλετε κάνετε save το session για να μην χρειαστεί να επαναλάβετε τις ρυθμίσεις την επομενη φορα.
  4. Τέλος πατήστε Open στο putty για να συνδεθείτε. Σε περίπτωση που έχετε firewall, βεβαιωθείτε ότι το Xming είναι unblocked. Δοκιμάστε τρέχοντας το xterm. Εφόσον λειτουργεί το xterm, μπορείτε να χρησιμοποιήσετε το QEMU. Από το σπίτι ενδέχεται να παρατηρήσετε delays, δοκιμάστε σε αυτή την περίπτωση αν από VPN είναι ποιο γρήγορο.


Εναλακτικά, μπορείτε απλά να τρέχετε το QEMU χωρίς γραφικό περιβάλλον με την βιβλιοθήκη ncurses:
 qemu -hda hy345-linux.img -curses 
Είτε να αντιγράψετε το kernel image (αφού έχετε αλλάξει και έχετε κάνει compile τον Linux kernel σε κάποιο μηχάνημα του τμήματος) και το disk image, και έπειτα να τρέξετε το QEMU (αφού το εγκαταστήσετε) τοπικά στον υπολογιστή σας.


Τι πρέπει να παραδώσετε

Αφού κάνετε αυτήν την άσκηση θα πρέπει να παραδώσετε τα παρακάτω:
  1. Το καινούργιο kernel image, δηλαδή το αρχείο linux-2.6.38.1/arch/x86/boot/bzImage.
  2. Όλα τα αρχεία που τροποποιήσατε ή δημιουργήσατε στον source code του Linux kernel 2.6.38.1 για να υλοποιήσετε τα system calls. Δηλαδή όλα τα αρχεία .c, .h, Makefile κτλ που κάνατε κάποια αλλαγή, ή δημιουργήσατε εσείς. Μην παραδώσετε αρχεία που δεν χρειάστηκε να τα τροποποιήσετε για την υλοποίησή σας.
  3. Τον source code από όλα τα test προγράμματα που γράψατε και τρέξατε μέσα στο guest Linux OS για να δοκιμάσετε τα system calls που υλοποιήσατε. Και επίσης ότι header files χρησιμοποιήσατε για type και function definitions (π.χ. το unist.h). Δηλαδή τα αρχεία .c, .h και Makefile και ότι άλλο αρχείο δημιουργήσατε στο guest OS για να δοκιμάσετε τα system calls (εκτός από τα executables).
  4. Ένα README file στο οποίο να περιγράφετε συνοπτικά (αλλά περιεκτικά και ξεκάθαρα) όλα τα βήματα που ακολουθήσατε για την δημιουργία των καινούργιων system calls. Επίσης πρέπει να σχολιάσετε τι παρατηρήσατε από τα test προγράμματα που τρέξατε. Αν έχετε κάνει κάτι διαφορετικό ή παραπάνω από όσα αναφέρουμε στην εκφώνηση της άσκησης σε οποιοδήποτε βήμα μπορείτε επίσης να το αναφέρετε στο README. Καλό θα ήταν το README να είναι από 20 μέχρι 30 γραμμές.
Για να μεταφέρετε αρχεία από το guest OS (που τρέχετε με το QEMU) στο host OS (που κάνετε την βασική σας υλοποίηση) και αντίστροφα, μπορείτε να χρησιμοποιήσετε το πρόγραμμα scp. Μέσα από το guest OS μπορείτε να προσπελάσετε το host OS με την (virtual) IP address 10.0.2.2. Για παράδειγμα, για να μεταφέρετε το αρχείο test1.c από το guest OS στο host OS στην περιοχή σας σε έναν κατάλογο hy345 μπορείτε απλά να κάνετε:
scp test1.c [username]@10.0.2.2:~/hy345
μέσα από το QEMU (guest OS). To [username] είναι το username που έχετε στα μηχανήματα του τμήματος. Θα χρειαστεί να δώσετε το password που έχετε στα μηχανήματα του τμήματος για να ολοκληρωθεί η αντιγραφή με το scp. Αντίστοιχα, για να αντιγράψετε από το host OS (π.χ. ένα μηχάνημα του τμήματος) το αρχείο test1.c από τον κατάλογο hy345 που είναι στην περιοχή σας στο Linux OS που τρέχει στο QEMU, θα τρέξετε μέσα από το QEMU την εντολή:
scp [username]@10.0.2.2:~/hy345/test1.c .
Προσοχή: δεν χρειάζεται να παραδώσετε το disk image (hy345-linux.img) ακόμα και αν αυτό έχει τροποποιηθεί (όντως, το disk image μπορεί να αλλάξει όσο χρησιμοποιείτε το guest OS, σαν ένας κανονικός δίσκος, αλλά δεν χρειάζεται να το παραδώσετε). Δεν χρειάζεται επίσης να παραδώσετε κάποιο αρχείο με ολόκληρο τον source code του Linux kernel, πρέπει να σημειώσετε και να παραδώσετε μόνο τα αρχεία που τροποποιήσατε ή δημιουργήσατε. To kernel image (bzImage), τα header files, και τα test προγράμματα που θα παραδώσετε θα πρέπει να είναι αρκετά ώστε η άσκησή σας να μπορεί να τρέξει με το αρχικό disk image και το QEMU, έτσι ώστε να φαίνετε η σωστή υλοποίηση του ζητούμενου system call.

Μπορείτε να φτιάξετε έναν κατάλογο με τα τροποποιημένα source code αρχεία του kernel (αν θέλετε θα είναι καλό να κρατήσετε την δομή καταλόγων που υπάρχει στον linux kernel), έναν κατάλογο με τα test προγράμματα και header files από το guest OS, και τέλος να τους μεταφέρετε σε ένα κατάλογο μαζί το bzImage και το README file για να παραδώσετε την άσκηση με τον γνωστό τρόπο.


Παρατηρήσεις

  1. Η άσκηση είναι ατομική. Τυχόν αντιγραφές μπορούν να ανιχνευθούν εύκολα από κατάλληλο πρόγραμμα και θα μηδενιστούν. Συμπεριλάβετε το όνομα σας και το λογαριασμό σας (account) σε όλα τα αρχεία.
  2. Τοποθετήστε σε ένα κατάλογο όλα τα αρχεία προς παράδοση για την άσκηση 3. Παραδώστε τα παραπάνω αρχεία χρησιμοποιώντας το πρόγραμμα submit (πληκτρολογήστε submit assignment_3@hy345 directory_name από τον κατάλογο που περιέχει τον κατάλογο directory_name με τα αρχέια της άσκησης).