Λειτουργικά Συστήματα (ΗΥ-345)
Χειμερινό Εξάμηνο 2012
Άσκηση 5η
Προσθήκη νέου system call στο λειτουργικό σύστημα Linux
Στην 5η άσκηση του μαθήματος θα προσθέσετε ένα καινούργιο system call στον πυρήνα (kernel)
του λειτουργικού συστήματος (OS) Linux.
Το νέο system call θα επιστρέφει τον χρόνο εκτέλεσης διεργασιών, πότε ξεκίνησαν να τρέχουν αυτές οι διεργασίες,
και κάποιες γενικές πληροφορίες για αυτές και τις συγγενικές τους διεργασίες,
όπως θα σας εξηγήσουμε παρακάτω.
Έπειτα θα το δοκιμάσετε χρησιμοποιώντας τον προσομοιωτή (emulator) QEMU:
αφού κάνετε compile τον Linux kernel με τις δικές σας αλλαγές,
θα τρέξετε το Linux με τον καινούργιο kernel στον προσομοιωτή QEMU,
και εκεί θα γράψετε και θα τρέξετε κάποια user-level test προγράμματα που
θα χρησιμοποιούν το καινούργιο 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 call που περιγράφεται στο βήμα 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 call
$ 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 call στον Linux kernel
Η αλλαγή που θα κάνετε στον Linux kernel 2.6.38.1 είναι να προσθέσετε ένα
καινούργιο system call που θα λέγεται getproctimes
,
το οποίο θα επιστρέφει πληροφορίες σχετικά με τον χρόνο εκτέλεσης της τρέχουσας
ή οποιασδήποτε άλλης διεργασίας, και των συγγενικών της διεργασιών.
Αυτό system call θα παίρνει δυο ορίσματα:
int getproctimes(int pid, struct proctimes *pt);
Το πρώτο argument (pid
) είναι το pid της διεργασίας για την οποία θα επιστρέψετε
τις πληροφορίες .
Αν το 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.
Το δεύτερο argument (pt
) είναι ένας pointer σε ένα struct τύπου
struct proctimes
.
Για αυτό το struct θα πρέπει να δεσμεύει μνήμη το user-level πρόγραμμα πριν καλέσει το system call.
Στην συνέχεια ο kernel θα πρέπει να το γεμίζει με όλες τις πληροφορίες που χρειάζονται για
την διεργασία που ζητήθηκε.
Έτσι θα επιστρέφει αυτές τις πληροφορίες
(by reference) στο user-level πρόγραμμα που κάλεσε το getproctimes
system call.
Αν το αυτό το δεύτερο όρισμα είναι NULL, τότε το getproctimes
θα πρέπει να επιστρέφει ξανά
το error value EINVAL.
Όπως επίσης αν συμβεί οποιοδήποτε άλλο λάθος (π.χ. αν ο kernel δεν μπορεί να αντιγράψει τις ζητούμενες πληροφορίες
σε user space), θα επιστρέφεται το ίδιο error value.
Αν όλα τα πάνε καλά και το system call τρέξει επιτυχώς, αυτό θα πρέπει να επιστρέφει 0.
Αντίστοιχα, το user-level πρόγραμμα που χρησιμοποιεί το system call θα πρέπει να ελέγχει την
επιστρεφόμενη τιμή για ενδεχόμενο λάθους.
Όπως είπαμε, αν το return value του system call είναι 0 (success), τότε τα πραγματικά αποτελέσματα
θα επιστραφούν μέσω του struct proctimes *pt
.
θα πρέπει εσείς να ορίσετε το struct proctimes
στο καινούργιο αρχείο linux-2.6.38.1/include/proctimes.h που θα δημιουργήσετε.
Όπως είπαμε, αυτό το struct θα περιέχει πληροφορίες σχετικές με την διεργασία
που έχει το pid
που δίνετε στο πρώτο όρισμα του getproctimes
,
ή την τρέχουσα διεργασία αν το pid
είναι ίσο με -1.
Επίσης, θα περιέχει τις ίδιες πληροφορίες για την πατρική της διεργασία (parent process),
την παλαιότερη θυγατρική της διεργασία (oldest children process),
και την παλαιότερη αδελφική της διεργασία (oldest sibling process).
Αδελφικές (siblings) είναι οι διεργασίες που έχουν την ίδια πατρική διεργασία (parent's child processes).
Συνολικά, το struct proctimes
θα περιέχει πληροφορίες για τέσσερις διαφορετικές
συγγενικές διεργασίες.
Οι πληροφορίες για κάθε μια από αυτές τις διεργασίες θα αποθηκεύονται
ξεχωριστά σε ένα struct τύπου struct proc_time
,
οπότε το struct proctimes
θα περιέχει απλά τέσσερα struct proc_time
.
Θα πρέπει επίσης να ορίσετε το struct proc_time
στο καινούργιο αρχείο
linux-2.6.38.1/include/proctimes.h που θα φτιάξετε πριν
από το struct proctimes
.
Το struct proc_time
θα περιέχει τις πληροφορίες που θέλουμε για κάθε μια από
τις διεργασίες που αναφέραμε:
κάποιες γενικές πληροφορίες (pid, όνομα διεργασίας),
την χρονική στιγμή που ξεκίνησε να τρέχει αυτή η διεργασία (start time),
και τους χρόνους εκτέλεσης της διεργασίας (real time, user time, και system time).
Αναλυτικά, το struct proc_time
και το struct proctimes
θα πρέπει να έχουν τα παρακάτω πεδία:
struct proc_time { // info and times about a single process
pid_t pid; // pid of the process
char name[16]; // file name of the program executed
unsigned long start_time; // start time of the process
unsigned long real_time; // real time of the process execution
unsigned long user_time; // user time of the process
unsigned long sys_time; // system time of the process
}
struct proctimes { // info and times about all processes we need
struct proc_time proc; // process with given pid or current process
struct proc_time parent_proc; // parent process
struct proc_time oldest_child_proc; // oldest child process
struct proc_time oldest_sibling_proc; // oldest sibling process
}
Το system call getproctimes
θα πρέπει να γεμίζει όλα αυτά τα πεδία
για όλες τις παραπάνω διεργασίες.
Αν δεν υπάρχουν θυγατρικές ή αδελφικές διεργασίες, τα αντίστοιχα πεδία θα πρέπει να είναι όλα μηδέν.
Το pid_t ορίζετε στο linux-2.6.38.1/include/linux/types.h.
Τέλος, κάθε φορά που καλείται το system call getproctimes
στο επίπεδο του πυρήνα,
θα πρέπει να τυπώνετε με την συνάρτηση printk
μια γραμμή με το όνομα σας και τον Α.Μ. σας.
Τα μηνύματα που τυπώνετε στον kernel με την συνάρτηση printk
μπορείτε να βλέπετε όταν έχετε φορτώσει το Linux με τον συγκεκριμένο kernel
τρέχοντας το dmesg
ή κάνοντας cat /var/log/messages.
Έτσι, κάθε φορά που καλείται το συγκεκριμένο system call από ένα 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
, αλλά εσείς θα χρειαστείτε για το getproctimes
).
Έπειτα φτιάχνουμε ένα καινούργιο αρχείο 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 (π.χ. στο getproctimes
)
θα πρέπει να τα αντιγράψετε: αφού καλέσετε το 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 call.
Ο Linux kernel αποθηκεύει τις αναλυτικές πληροφορίες
(στις οποίες συμπεριλαμβάνονται και αυτές που ζητάμε για το getproctimes
)
για όλες τις τρέχουσες διεργασίες σε ένα doubly linked list
από task_struct
structs.
Το struct task_struct
ορίζεται στο αρχείο linux-2.6.38.1/include/linux/sched.h.
Εκεί μπορείτε να βρείτε όλες τις πληροφορίες που χρειάζεστε για κάθε διεργασία:
το pid, το όνομα της διεργασίας, τον χρόνο που ξεκίνησε η εκτέλεση της,
και τα user, system time κάθε διεργασίας.
Επίσης, θα βρείτε έναν δείκτη στην πατρική διεργασία και δυο λίστες
με τις θυγατρικές και τις αδελφικές διεργασίες αντίστοιχα (για κάθε διεργασία).
Αν υπάρχουν θυγατρικές και αδελφικές διεργασίες θα πρέπει να διατρέξετε αυτές τις λίστες
για να βρείτε τις παλιότερες από αυτές τις διεργασίες, όπως σας ζητάμε,
και τα πεδία αυτών των διεργασιών αντίστοιχα (από το κατάλληλο task_struct
entry).
Για να βρείτε την τρέχουσα διεργασία κοιτάξτε στο αρχείο linux-2.6.38.1/include/asm/current.h.
Εκεί υπάρχει το inline function current που επιστρέφει το task_struct
entry
της τρέχουσας διεργασίας.
Θα παρατηρήσετε ότι το start_time στο task_struct
έιναι της μορφής struct timespec
, ενώ τα utime
και stime
(user time και system time) είναι σε cputime_t
.
Θα πρέπει όλα αυτά να τα μετατρέψετε σε unsigned long
πριν τα επιστρέψετε με το getproctimes
.
Επίσης θα δείτε ότι δεν υπάρχει κάποιο πεδίο που να δίνει το real time, δηλαδή τον πραγματικό χρόνο
που εκτελείται η διεργασία, ή με άλλα λόγια, πόσος χρόνος πέρασε από την στιγμή που ξεκίνησε η διεργασία
να εκτελείται μέχρι τώρα (που εκτελείται το getproctimes
system call).
Οπότε για να βρείτε το real time της κάθε διεργασίας θα πρέπει να
διαβάσετε πρώτα την τρέχουσα ώρα του συστήματος, και έπειτα από αυτήν να αφαιρέσετε το start_time της διεργασίας:
process_real_time = current_time - process_start_time
.
Για να δείτε πως μπορείτε να βρείτε την τρέχουσα ώρα του συστήματος (π.χ. να διαβάσετε την μεταβλητή xtime
του kernel)
μπορείτε να δείτε το αρχείο linux-2.6.38.1/include/linux/time.h π.χ. για συναρτήσεις που
μπορούν να επιστρέψουν την τιμή που ψάχνετε. Επίσης μπορείτε να κοιτάξετε παρόμοια system calls
(όπως το gettimeofday
) που διαβάζουν την τρέχουσα ώρα του συστήματος.
Τέλος, για να βρείτε περισσότερες πληροφορίες για το πώς δουλεύει ένα system call στον Linux kernel,
που θα σας βοηθήσει πολύ και στην δικιά σας υλοποίηση, μπορείτε να ψάχνετε στον source code του Linux kernel
για άλλα παρόμοια system calls που υπάρχουν ήδη στον kernel, όπως π.χ. το getpid
ή το times
.
Για παράδειγμα, το getpid
χρησιμοποιεί επίσης την συνάρτηση current
για να βρεί
την τρέχουσα διεργασία, ενώ το times
επιστρέφει παρόμοιους χρόνους εκτέλεσης με το getproctimes
για την τρέχουσα διεργασία.
5. Δοκιμή του νέου system call
Στο τελευταίο βήμα της άσκησης θα πρέπει να δοκιμάσετε το καινούργιο system call.
Αφού έχετε κάνει compile με επιτυχία τον kernel με το system call που φτιάξατε,
και έχετε ξεκινήσει τον QEMU με τον καινούργιο Linux kernel,
θα πρέπει να γράψετε κάποια test προγράμματα που να χρησιμοποιούν το proctimes
στο 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 ένα macro για το getproctimes
,
ώστε το syscall να μοιάζει με function call,
είτε να φτιάξετε ένα wrapper function getproctimes
(με τα ορίσματα που δέχεται αυτό το system call)
που να καλεί εσωτερικά το syscall.
Έτσι θα καλείτε το getproctimes
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).
Για το getproctimes
σας προτείνουμε να προσθέσετε τα definitions για τα
struct proctimes
και struct proc_time
, μαζί με το
macro ή inline wrapper function του getproctimes
,
στο unistd.h
header file που υπάρχει στο guest OS,
και το οποίο θα κάνετε include σε όλα τα test προγράμματα.
Τα test προγράμματα που θα φτιάξετε θα πρέπει να δείχνουν ότι τα βασικά features
του καινούργιου system call δουλεύουν σωστά.
Τρία χαρακτηριστικά test προγράμματα που θέλουμε να κάνετε είναι τα παρακάτω:
- Test program 1: Θα φτιάξετε ένα πρόγραμμα το οποίο θα καλεί αρχικά το
getproctimes
και θα τυπώνει όλα τα πεδία του struct proc_time
για την τρέχουσα διεργασία (proc
).
Έπειτα θα κάνετε ένα εκατομμύριο πολλαπλασιασμούς και θα καλείτε ξανά το getproctimes
τυπώνοντας τα ίδια πεδία.
Τέλος, θα κάνετε ένα sleep για 5 δευτερόλεπτα (sleep(5)
) και θα τυπώσετε
ξανά τα πεδία του struct proc_time
για την τρέχουσα διεργασία, αφού τρέξετε ξανά
το getproctime
system call.
- Test program 2: Το δεύτερο πρόγραμμα θα πρέπει να καλεί την συνάρτηση fork() δύο φορές.
Η κάθε καινούργια διεργασία θα πρέπει να κάνει άλλο ένα fork() μετά από 5 δευτερόλεπτα.
Σε όλες τις διεργασίες θα καλείτε το
getproctimes
πριν και μετά την fork()
και να τυπώνετε τα πεδία του proc_time
για όλες τις διεργασίες (τρέχουσα, πατρική,
παλαιότερη θυγατρική, και παλαιότερη αδελφική). Θα πρέπει να ελέγξετε ότι τα πεδία που τυπώνετε
συμφωνούν μεταξύ τους. Για ευκολία μπορείτε να τυπώνετε μόνο τα pid, name, και start_time.
- Test program 3: Το τρίτο πρόγραμμα θα πρέπει να παίρνει σαν είσοδο από command line
ένα pid και να καλεί το
getproctimes
τυπώνοντας όλα τα πεδία για όλες τις σχετικές διεργασίες.
Δοκιμάστε να τρέξετε αυτό το πρόγραμμα για αρκετά pid που εμφανίζονται με την εντολή ps.
Εκτός από αυτά τα τρία προγράμματα, μπορείτε να φτιάξετε όσα περισσότερα θέλετε ώστε να
βεβαιωθείτε ότι το getproctimes
δουλεύει σωστά.
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 call.
Δηλαδή όλα τα αρχεία .c, .h, Makefile κτλ που κάνατε κάποια αλλαγή, ή δημιουργήσατε εσείς.
Μην παραδώσετε αρχεία που δεν χρειάστηκε να τα τροποποιήσετε για την υλοποίησή σας.
- Τον source code από όλα τα test προγράμματα που γράψατε και τρέξατε μέσα στο guest Linux OS για να δοκιμάσετε
το system call που υλοποιήσατε.
Και επίσης ότι header files χρησιμοποιήσατε για type και function definitions (π.χ. το
unist.h
).
Δηλαδή τα αρχεία .c, .h και Makefile και ότι άλλο αρχείο δημιουργήσατε στο
guest OS για να δοκιμάσετε το system call (εκτός από τα executables).
- Ένα README file στο οποίο να περιγράφετε συνοπτικά (αλλά περιεκτικά και ξεκάθαρα) όλα τα βήματα που ακολουθήσατε
για την δημιουργία του καινούργιου system call.
Επίσης πρέπει να σχολιάσετε τι παρατηρήσατε από τα 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) σε όλα τα αρχεία.
- Τοποθετήστε σε ένα κατάλογο όλα τα
αρχεία προς παράδοση για την άσκηση 5. Παραδώστε τα
παραπάνω αρχεία χρησιμοποιώντας το πρόγραμμα submit (πληκτρολογήστε
submit assignment_5@hy345 directory_name από τον κατάλογο
που περιέχει τον κατάλογο directory_name με τα αρχέια της άσκησης).