Λειτουργικά Συστήματα (ΗΥ-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:
- Πρέπει να προσθέσετε ένα καινούργιο system call number στον kernel για το δικό σας system call.
- Πρέπει να προσθέσετε ένα 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).
- Πρέπει να προσθέσετε κώδικα στον 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 πρόγραμμα.
- Ανοίγουμε το 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.
- Στο δεύτερο βήμα ανοίγουμε το αρχείο linux-2.6.38.1/arch/x86/kernel/syscall_table_32.S
και προσθέτουμε στην τελευταία γραμμή το όνομα της συνάρτησης που υλοποιεί το
καινούργιο system call (system call function pointer):
.long sys_dummy_sys /* 341 */
- Στο τρίτο βήμα θα υλοποιήσουμε το 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 προγράμματα που θέλουμε να κάνετε είναι τα παρακάτω:
- Test program 1:
Θα φτιάξετε ένα πρόγραμμα το οποίο θα καλεί αρχικά το
setproclimit
για την τρέχουσα διεργασία με κάποιες τιμές
για τα πεδία limit και interval, και έπειτα θα καλεί το getproclimit
και θα τυπώνει όλα τα πεδία του struct proclimit
για την τρέχουσα διεργασία.
Έπειτα θα κάνετε ένα εκατομμύριο πολλαπλασιασμούς και θα καλείτε ξανά το getproclimit
τυπώνοντας τα ίδια πεδία.
Τέλος, θα κάνετε ένα sleep για 5 δευτερόλεπτα (sleep(5)
) και θα τυπώσετε
ξανά τα πεδία του struct proclimiy
για την τρέχουσα διεργασία, αφού τρέξετε ξανά
το getproclimit
system call.
- Test program 2: Το δεύτερο πρόγραμμα θα πρέπει να καλεί αρχικά το
setproclimit
για την τρέχουσα διεργασία (pid -1) με κάποιες τιμές για limit και interval,
και έπειτα την fork() δύο φορές.
Η κάθε καινούργια διεργασία θα πρέπει να κάνει άλλο ένα fork() μετά από 5 δευτερόλεπτα.
Σε όλες τις διεργασίες θα καλείτε το getproclimit
πριν και μετά την fork()
και θα τυπώνετε τα πεδία του struct proclimit
.
- Test program 3: Το τρίτο πρόγραμμα θα πρέπει να παίρνει σαν είσοδο από command line
ένα pid και ένα limit (σε milliseconds, που θα πρέπει να είναι μικρότερο απο 1000).
Έπειτα θα καλεί τo
setproclimit
με αυτό το pid και limit, με το interval να είναι ίσο με 1000 (δηλαδή 1 second).
Τέλος, θα καλεί το getproclimit
για το ίδιο pid τυπώνοντας όλα τα πεδία του.
Δοκιμάστε να τρέξετε αυτό το πρόγραμμα για αρκετά pid που εμφανίζονται με την εντολή ps.
Εκτός από αυτά τα τρία προγράμματα, μπορείτε να φτιάξετε όσα περισσότερα θέλετε ώστε να
βεβαιωθείτε ότι τα 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, πρέπει να γίνει κάποια αντίστοιχη διαδικασία.
- Κατεβάστε το Xming ,
εγκαταστήστε το και τρέξτε το.
- Kατεβάστε, εφόσον δεν το έχετε, το putty .
- Στο putty εφαρμόστε τις παρακάτω ρυθμίσεις:
- Session -> Hostname: gate1.csd.uoc.gr / gate2.csd.uoc.gr
- Session -> Port: το port που θα σας επιστρέψει το "enable", π.χ. 17724
- Connection -> ssh -> X11: tick στο enable X11 forwarding.
Αν θέλετε κάνετε save το session για να μην χρειαστεί να επαναλάβετε τις
ρυθμίσεις την επομενη φορα.
- Τέλος πατήστε 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 (αφού το εγκαταστήσετε)
τοπικά στον υπολογιστή σας.
Τι πρέπει να παραδώσετε
Αφού κάνετε αυτήν την άσκηση θα πρέπει να παραδώσετε τα παρακάτω:
- Το καινούργιο kernel image, δηλαδή το αρχείο linux-2.6.38.1/arch/x86/boot/bzImage.
- Όλα τα αρχεία που τροποποιήσατε ή δημιουργήσατε στον source code του Linux kernel 2.6.38.1 για να υλοποιήσετε τα system calls.
Δηλαδή όλα τα αρχεία .c, .h, Makefile κτλ που κάνατε κάποια αλλαγή, ή δημιουργήσατε εσείς.
Μην παραδώσετε αρχεία που δεν χρειάστηκε να τα τροποποιήσετε για την υλοποίησή σας.
- Τον source code από όλα τα test προγράμματα που γράψατε και τρέξατε μέσα στο guest Linux OS για να δοκιμάσετε
τα system calls που υλοποιήσατε.
Και επίσης ότι header files χρησιμοποιήσατε για type και function definitions (π.χ. το
unist.h
).
Δηλαδή τα αρχεία .c, .h και Makefile και ότι άλλο αρχείο δημιουργήσατε στο
guest OS για να δοκιμάσετε τα system calls (εκτός από τα executables).
- Ένα 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
για να παραδώσετε την άσκηση με τον γνωστό τρόπο.
Παρατηρήσεις
- Η άσκηση είναι ατομική. Τυχόν αντιγραφές μπορούν να ανιχνευθούν εύκολα
από κατάλληλο πρόγραμμα και θα μηδενιστούν. Συμπεριλάβετε το όνομα σας και
το λογαριασμό σας (account) σε όλα τα αρχεία.
- Τοποθετήστε σε ένα κατάλογο όλα τα
αρχεία προς παράδοση για την άσκηση 3. Παραδώστε τα
παραπάνω αρχεία χρησιμοποιώντας το πρόγραμμα submit (πληκτρολογήστε
submit assignment_3@hy345 directory_name από τον κατάλογο
που περιέχει τον κατάλογο directory_name με τα αρχέια της άσκησης).