Άρθρο 5: Processes μέρος 1

Διαθέσιμα αρχεία:

Το άρθρο δημοσιεύθικε στο linux inside τέυχος 5 (Νοεμβρίου-Δεκεμβρίου 2011).


Εισαγωγή

Το καλοκαίρι πέρασε, και ήρθε ο καιρός να βουτήξουμε ξανά στον κώδικα του πυρήνα, και να γράψουμε ένα από τα πιο βασικά και πιο πολύπλοκα κομμάτια του μέχρι στιγμής. Ήρθε η ώρα να υλοποιήσουμε processes! Το αντικείμενο είναι κάπως μεγάλο και θα χρειαστεί δύο άρθρα για να το καλύψουμε, οπότε ας μην χάνουμε χρόνο, ανοίξτε τον συνοδευτικό κώδικα του άρθρου και ας ξεκινήσουμε αμέσως.

Γενικά περί processes

Σε ένα τυπικό λειτουργικό σύστημα, τα προγράμματα τον χρηστών που εκτελούνται σε ένα σύστημα απαρτίζονται από ένα η περισσότερα processes (διεργασίες). Δουλειά του πυρήνα είναι να παρέχει υπηρεσίες σε αυτά τα processes, να διαχειρίζεται το hardware εκ μέρους τούς, και να διαχειρίζεται τον χρόνο εκτέλεσης που διατίθεται στο καθένα, εναλλάσσοντας πολύ γρήγορα το πιο εκτελείται κάθε φορά, ώστε να δίνεται η ψευδαίσθηση ότι εκτελούνται ταυτόχρονα. Και όλα αυτά ενώ διατηρεί ένα απλό μοντέλο εκτέλεσης, όπου κάθε process θεωρεί ότι του διατίθεται ο υπολογιστής κατ' αποκλειστικότητα

Για να μπορέσει να παρουσιάσει αυτό το απλό μοντέλο εκτέλεσης στα processes, ο πυρήνας πρέπει να κάνει δύο βασικά πράγματα. Κατ' αρχάς πρέπει να αποθηκεύει κάπου στην μνήμη την κατάσταση του επεξεργαστή όταν αποφασίζει να διακόψει την εκτέλεση του ενεργού process, ώστε να μπορεί να τον επαναφέρει ακριβώς στην ίδια κατάσταση όταν έρθει η ώρα να συνεχίσει από εκεί που σταμάτησε. Κατά δεύτερον πρέπει να παρέχει ξεχωριστό address space σε κάθε process ώστε όταν γράφει κάτι στην μνήμη το ένα process να μην επηρεάζει την μνήμη των άλλων.

Η πιο σημαντική ιδέα που πρέπει να κατανοήσουμε για να γίνει ξεκάθαρο το πώς λειτουργεί ο πυρήνας, πώς παρέχει υπηρεσίες στα processes, και πώς τα εναλλάσσει, είναι το εξής: ο πυρήνας δεν εκτελείται στο παρασκήνιο παράλληλα με τα προγράμματα των χρηστών, αντίθετα περνάει την εκτέλεση σε κάποιο user process, και δεν εκτελείται ξανά μέχρι να του δώσει πίσω την σκυτάλη το process ζητώντας κάποια υπηρεσία σηκώνοντας software interrupt, ή εάν πρέπει να χειριστεί κάποιο hardware interrupt.

Αν όμως ο kernel δεν εκτελείται παρά μόνο όταν του ζητηθεί, πώς μπορεί να διακόψει την εκτέλεση ενός process βιαίως για να δώσει χρόνο σε κάποιο άλλο; Μα φυσικά χρησιμοποιώντας τον timer του υπολογιστή. Στο προηγούμενο άρθρο, βάλαμε τον timer να σηκώνει interrupts σε τακτά διαστήματα (250 φορές το δευτερόλεπτο). Όταν συμβαίνει αυτό παίρνει τον έλεγχο ο πυρήνας, και αυτό που τον βάλαμε να κάνει μέχρι στιγμής είναι να αυξάνει απλώς μια μεταβλητή (ticks) κατά ένα. Από εδώ και στο εξής θα χρησιμοποιήσουμε αυτό το timer interrupt για να ελέγχουμε ανά τακτά διαστήματα αν το ενεργό process ξεπέρασε τον χρόνο που θέλουμε να του διαθέσουμε, οπότε και να το σταματάμε για να δώσουμε χρόνο εκτέλεσης σε κάποιο άλλο.

Privilege Levels

Μέχρι στιγμής όλος ο κώδικας που γράψαμε εκτελείται σε privilege level 0, δηλαδή σε kernel mode, που πρακτικά σημαίνει ότι έχει πρόσβαση σε όλη την μνήμη, και μπορεί να διαχειριστεί όλο το hardware κατά βούληση. Αυτό είναι προφανώς ανεπιθύμητο για τα user processes, και γιαυτό θέλουμε να φροντίσουμε να εκτελούνται στο περιορισμένο privilege level 3 του επεξεργαστή. Σε αυτό το επίπεδο απαγορεύεται η εκτέλεση εντολών που μπορεί να επηρεάσουν δομές που διαχειρίζεται ο πυρήνας, όπως page tables, segment descriptors, interrupt vectors, κλπ, όπως επίσης και οι I/O εντολές της οποίες χρησιμοποιούμε για να χειριστούμε τις συσκευές του συστήματως.

Για να το πετύχουμε αυτό κατ' αρχάς πρέπει να τροποποιήσουμε τον GDT (Global Descriptor Table) προσθέτοντας δύο καινούρια segment descriptors για το user code segment, και user data segment. Η ουσιαστική διαφορά μεταξύ αυτών και των kernel code και data segments που έχουμε ήδη, είναι ότι βάζουμε στο πεδίο dpl (descriptor privilege level) την τιμή 3 αντί για 0 (βλ. init_segm στο segm.c).

Οι περιορισμοί που αναφέραμε παραπάνω εφαρμόζονται από τον επεξεργαστή όταν εκτελείται κώδικας με τον cs register να περιέχει selector για segment με dpl 3. Αντίστοιχα όταν γίνει πρόσβαση στην μνήμη μέσω data segment dpl 3, ο επεξεργαστής επιτρέπει την πρόσβαση μόνο σε pages, στο page table entry των οποίων έχουμε θέσει το user bit. Φυσικά ο κώδικας που εκτελείται σε user mode δεν επιτρέπεται να αλλάξει τους segment selectors του, αλλιώς όλα τα παραπάνω δεν θα είχαν ιδιαίτερο νόημα.

Είσοδος και έξοδος από kernel mode

Η αλλαγή του privilege level από 3 σε 0 (user -> kernel) γίνεται αυτόματα όταν σηκωθεί interrupt ενώ είμαστε σε user mode. Η διαδικασία εισόδου στον interrupt handler διαφέρει ελαφρώς αν αυτό γίνει από user mode αντί για kernel mode που είχαμε δει μέχρι τώρα. Η βασική διαφορά είναι ότι κατά την είσοδο στον interrupt handler ο επεξεργαστής αλλάζει αυτόματα τους registers ss (stack segment) και esp (stack pointer) ώστε να χρησιμοποιηθεί από τον kernel διαφορετικό stack από αυτό που χρησιμοποιούσε ο user κώδικας. Στο καινούριο stack γίνονται push επιπλέον οι παλιές τιμές αυτών των δυο registers, πέρα από όλα τα υπόλοιπα που είδαμε στο δεύτερο άρθρο (σχήμα 1).

interrupt frames
Σχήμα 1: Interrupt frame κατά την είσοδο από ίδιο ή διαφορετικό privilege level.

Το interrupt frame περιέχει την προηγούμενη τιμή του cs, και φυσικά και του eip, τα οποία επαναφέρει ο επεξεργαστής όταν γίνει επιστροφή από interrupt (εντολή iret). Αν λοιπών αυτή η σωσμένη τιμή του cs έχει dpl 3, τότε όταν ο επεξεργαστής την επαναφέρει στον cs, γίνεται ξανά μετάβαση σε user mode, και γυρνάμε αυτόματα στο stack του user process (επαναφέρονται οι σωσμένες τιμές των ss και esp).

Την τιμή που θα θέσει στους ss και esp ο επεξεργαστής κατά την είσοδο σε kernel mode, τις παίρνει από ένα ειδικό segment, τον descriptor του οποίου πρέπει φυσικά να τοποθετήσουμε στον GDT, το Task State Segment (TSS). Δεν θα μπούμε σε λεπτομέρειες για τις πιθανές χρήσεις του TSS, αλλά περιληπτικά είναι μια δομή με συγκεκριμένη μορφή όπως φαίνεται στο struct task_state στο αρχείο tss.h, στην οποία περιμένει να βρει ο επεξεργαστής την διεύθυνση και τον selector για το stack που θα χρησιμοποιηθεί κατά την μετάβαση σε οποιοδήποτε privilege level μικρότερο του 3. Εμείς αγνοούμε τα stacks για privilege levels 1 και 2, μιας και δεν μας είναι χρήσιμα, καθώς και όλα τα υπόλοιπα περιεχόμενα της συγκεκριμένης δομής.

Στον κώδικα του interrupt entry (intr-asm.S), οι αλλαγές που χρειάστηκαν, είναι ελάχιστες και δεν έχει νόημα να τον επαναλάβουμε εδώ. Απλά πλέων μπαίνοντας στο interrupt κάνουμε push όλους τους selector registers εκτός από τον cs και τον ss που γίνονται αυτόματα, και τους κάνουμε pop πάλι πριν το iret.

Ουσιαστικότερη είναι η συνάρτηση init_proc στο αρχείο proc.c που κάνει allocate χώρο για το TSS, θέτει σε αυτό την σωστή τιμή για τον ss του kernel, και καλεί την set_tss (segm.c) για να μπει ο descriptor του στον GDT και να εκτελεστεί η εντολή ltr που δίνει στον επεξεργαστή τον selector για το TSS.

static struct task_state *tss;
void init_proc(void)
{
    /* allocate a page for the task state segment */
    int tss_page = pgalloc(1, MEM_KERNEL);
    tss = (void*)PAGE_TO_ADDR(tss_page);

    /* clear the tss and set the correct ss selector */
    memset(tss, 0, sizeof *tss);
    tss->ss0 = selector(SEGM_KDATA, 0);



    init_syscall();
    start_first_proc(); /* never returns */
}

Βλέπουμε ότι η init_proc κάνει επίσης initialize τον μηχανισμό τον system calls καλώντας την init_syscall, και τέλος ξεκινάει το πρώτο user process καλώντας την start_first_proc, την οποία θα δούμε παρακάτω.

System calls

Ο μηχανισμός που θα χρησιμοποιήσουμε για τα system calls είναι πολύ απλός. Όταν ένα process θέλει να ζητήσει κάτι από τον πυρήνα, τοποθετεί στον eax το νούμερο του system call, στους ebx, ecx, edx, esi, και edi τις παραμέτρους (όσες χρειάζονται για το κάθε system call), και σηκώνει το interrupt 128 εκτελώντας την εντολή int. Η εκτέλεση μεταφέρεται στον πυρήνα σε kernel mode, και όπως έχουμε πει σε παλαιότερο άρθρο, καταλήγει να εκτελεστεί η συνάρτηση που έχουμε θέσει για το συγκεκριμένο interrupt. Για το interrupt 128 λοιπών βάζουμε μια συνάρτηση, που χρησιμοποιεί την σωσμένη τιμή του eax από το interrupt frame σαν index σε έναν πίνακα από συναρτήσεις που υλοποιούν το κάθε system call. Όταν επιστέψει η συνάρτηση του system call, παίρνουμε την τιμή που επέστρεψε και την χρησιμοποιούμε για να αντικαταστήσουμε την αποθηκευμένη τιμή του eax στο stack frame, η οποία θα τοποθετηθεί στον eax register λίγο πριν επιστρέψουμε από το interrupt, με την popa που εκτελεί ο interrupt handler πριν το iret.

Τα παραπάνω υλοποιούνται στο αρχείο syscall.c, ενώ οι συμβολικές τιμές που χρησιμοποιούνται για τους αριθμούς τον system calls ορίζονται στο header file syscall.h.

void init_syscall(void)
{
    sys_func[SYS_HELLO] = sys_hello;
    sys_func[SYS_SLEEP] = sys_sleep;
    sys_func[SYS_FORK] = sys_fork;
    sys_func[SYS_GETPID] = sys_getpid;

    interrupt(SYSCALL_INT, syscall);
}

static void syscall(int inum)
{
    struct intr_frame *frm = get_intr_frame();
    int idx = frm->regs.eax
    frm->regs.eax = sys_func[idx](frm->regs.ebx, frm->regs.ecx, frm->regs.edx, frm->regs.esi, frm->regs.edi);
}

Τα system calls που υλοποιήσαμε μέχρι στιγμής είναι τα τέσσερα που φαίνονται στο παραπάνω κομμάτι κώδικα. Όταν κληθεί το syscall hello, τυπώνεται ένα μήνυμα της μορφής: το process τάδε λέει hello, ώστε να βλέπουμε ποιο process τρέχει κάθε φωρά. Η sleep παίρνει σαν παράμετρο έναν αριθμό από δευτερόλεπτα και σταματάει το ενεργό process για τουλάχιστον τόσο χρόνο, αφήνοντας άλλα processes να τρέξουν. Το fork θα το δούμε λεπτομερώς σε λίγο, μιας και είναι η μέθοδος με την οποία δημιουργούνται άλλα processes. Τέλος το getpid, απλά επιστρέφει το id του process που έκανε την κλήση.

Διαχείριση διεργασιών

Υπάρχουν διάφοροι τρόποι να γίνει η διαχείριση, η δημιουργία και το scheduling της εκτέλεσης των processes. Εμείς θα ακολουθήσουμε μια οργάνωση βασισμένη χοντρικά στο μοντέλο του UNIX.

Στο μοντέλο αυτό, ο μόνος τρόπος να δημιουργηθεί καινούριο process είναι μέσο της κλήσης fork που φτιάχνει ένα ακριβές αντίγραφο του process που την κάλεσε. Η δημιουργία process με αυτό τον τρόπο φτιάχνει μια parent-child σχέση μεταξύ των processes. Έτσι δημιουργείται μια ιεραρχία από processes στο σύστημα που έχει σαν ρίζα το πρώτο process (pid 1) το οποίο ξεκινάει ο πυρήνας μετά το initialization του, και ιστορικά λέγεται init.

Για κάθε process κρατάμε ένα structure με πληροφορίες (struct process στο proc.h), που περιέχει μεταξύ άλλων το id του, το id του parent, αν είναι ενεργό και μπορεί να εκτελεστεί, ή περιμένει για κάτι (και τι είναι αυτό που περιμένει) κ.α. Αυτά τα structures τα έχουμε σε έναν πίνακα, και το κάθε process απλά χρησιμοποιεί την θέση του πίνακα που αντιστοιχεί στο id του.

Μια σημαντική λεπτομέρεια όσον αφορά την οργάνωση και την λειτουργία των processes είναι ότι το κάθε process έχει το δικό του kernel stack. Αυτό μας επιτρέπει να σταματάμε την εκτέλεση κάποιου system call που χρειάζεται να περιμένει για κάτι, και να μπορούμε να συνεχίσουμε αργότερα από το σημείο που σταματήσαμε, το οποίο απλοποιεί πολύ την υλοποίηση των system calls. Για παράδειγμα η read όταν διαβάζει από δίσκο, κάτι που θα υλοποιήσουμε σε μεταγενέστερο άρθρο, μπορεί απλά να ξεκινήσει την διαδικασία ανάγνωσης και να σταματήσει μέχρι να γίνουν διαθέσιμα τα δεδομένα από τον δίσκο, τότε μπορεί απλά να συνεχιστεί η εκτέλεση του κώδικα μέσα στον kernel που θα αντιγράψει τα δεδομένα στον buffer που έχει δώσει ο user, και θα επιστρέψει σε user space. Με παρόμοιο τρόπο δουλεύει και η sleep που θα περιγράψουμε λεπτομερώς στο επόμενο άρθρο.

Το πρώτο process (init)

Εφόσον όλα τα processes δημιουργούνται σαν αντίγραφα του parent τους μέσω της fork, το πρώτο process πρέπει να κατασκευαστεί χειροκίνητα από τον kernel. Αυτό το κάνει η συνάρτηση start_first_proc, στο proc.c. Κανονικά θα θέλαμε να φορτώσουμε το image του init process από τον δίσκο, αλλά αφού δεν έχουμε γράψει filesystem ακόμα, πρέπει να αρκεστούμε σε κάποιου είδους πρόχειρο hack που θα μας επιτρέψει να δοκιμάσουμε τον κώδικα που γράφουμε για τα processes πριν αποκτήσουμε filesystem και executable loader.

Η λύση στην οποία κατέληξα είναι να γραφτεί μια μικρή συνάρτηση σε assembly (test_proc.S) που θα γίνει compile μαζί με τον κώδικα του kernel. Σε αυτή την συνάρτηση τοποθετούμε εκτός από το label του entry point, και ένα globally visible label στο τέλος της. Έτσι μπορούμε να υπολογίσουμε το μέγεθος του κώδικα με μια απλή αφαίρεση, και να κάνουμε allocate ένα κομμάτι user μνήμης, να αντιγράψουμε τον κώδικα εκεί, και να αρχίσει η εκτέλεση του πρώτου process από αυτό το σημείο.

/* allocate a chunk of memory for the process image */
proc_size_pg = (test_proc_end - test_proc) / PGSIZE + 1;
img_start_pg = pgalloc(proc_size_pg, MEM_USER);
img_start_addr = PAGE_TO_ADDR(img_start_pg);
memcpy((void*)img_start_addr, test_proc, proc_size_pg * PGSIZE);

Μετά από αυτό, η start_first_proc γεμίζει το process structure και κάνει allocate kernel stack και user stack για το process. Καλεί την add_proc (sched.c) που προσθέτει στην λίστα ενεργών processes του scheduler το συγκεκριμένο process, και καλεί την set_current_pid για να τεθεί η global μεταβλητή που μας λέει ποιο process εκτελείται ανά πάσα στιγμή. Επίσης την διεύθυνση του kernel stack την τοποθετεί στο TSS ώστε να χρησιμοποιηθεί στο επόμενο interrupt από user space.

Το πιο περίεργο κομμάτι είναι πώς ξεκινάει να εκτελείται το process. Αφού ο kernel όπως είπαμε εκτελείται πάντα σαν απάντηση σε interrupt, και επιστρέφει στο user space με επιστροφή από interrupt, η μόνη μέθοδος να ξεκινήσει να εκτελείται το process σε user mode είναι να επιστρέψουμε σε αυτό από interrupt με την εντολή iret! Για αυτό τον λόγο η start_first_proc κατασκευάζει ένα ψεύτικο interrupt frame με όλα τα στοιχεία που θέλουμε να μπουν στους διάφορους registers κατά την επιστροφή από interrupt, και το περνάει σαν παράμετρο στην συνάρτηση intr_ret.

#define FLAGS_INTR_BIT (1 << 9)

struct intr_frame ifrm;
memset(&ifrm, 0, sizeof ifrm);
/* ss:esp after the priviledge switch */
ifrm.esp = PAGE_TO_ADDR(stack_pg) + PGSIZE;
ifrm.ss = selector(SEGM_UDATA, 3);
/* instruction pointer at the beginning of the image */
ifrm.eip = img_start_addr;
ifrm.cs = selector(SEGM_UCODE, 3);
/* make sure the user will run with interrupts enabled */
ifrm.eflags = FLAGS_INTR_BIT;
/* user data selectors should all be the same */
ifrm.ds = ifrm.es = ifrm.fs = ifrm.gs = ifrm.ss;

/* execute an iret with this stack frame */
intr_ret(ifrm);

Η συνάρτηση intr_ret αφού παίρνει σαν παράμετρο το ψεύτικο stack frame structure, αυτό γίνεται push στο stack πριν κληθεί η συνάρτηση. Αυτό σημαίνει ότι αν εξαιρέσουμε το return address που επίσης γίνεται push αυτόματα από την εντολή call, όταν μπαίνουμε στην intr_ret έχουμε ακριβώς τα δεδομένα στο stack που θα είχαμε αν ήμασταν στην intr_entry_common έτοιμοι να ξεκινήσουμε να κάνουμε pop πράγματα και iret. Οπότε η intr_ret αρκεί να ξεφορτωθεί το return address από το stack προσθέτοντας 4 στον esp, πριν κάνει jump στην μέση του interrupt handler, σε ένα label που προσθέσαμε αμέσως μετά την επιστροφή από την dispatch_intr.

    .globl intr_ret
intr_ret:
    add $4, %esp
    jmp intr_ret_local

Context switching

Context switching, δηλαδή αλλαγή του process που εκτελείται, γίνεται όταν κληθεί η συνάρτηση schedule (sched.c). Η συνάρτηση αυτή καλείται από τον handler του timer interrupt (timer_handler στο timer.c) αφού πρώτα μειωθεί το ticks_left στο process structure του ενεργού process. Επίσης καλείται και από την συνάρτηση wait που χρησιμοποιείται όταν κάποιο process θέλει να σταματήσει να εκτελείται περιμένοντας κάτι.

Η schedule διατηρεί μια λίστα με processes που είναι έτοιμα να εκτελεστούν (run-queue), και πάντα διαλέγει το πρώτο σε αυτή τη λίστα προς εκτέλεση. Αν το ticks_left του process που εκτελείται (το πρώτο στην λίστα) έχει φτάσει στο 0, και υπάρχει κι άλλο process στην λίστα που περιμένει να εκτελεστεί, αφαιρεί το process από την αρχή της λίστας, και το τοποθετεί στο πίσω μέρος. Σε κάθε περίπτωση περνάει τελικά το id του πρώτου της λίστας στην συνάρτηση context_switch (proc.c), η οποία αναλαμβάνει να ανταλλάξει το ενεργό process με το καινούριο, εάν είναι διαφορετικά.

Αν δεν υπάρχει κανένα process στο run-queue, τότε καλείται η συνάρτηση idle_proc η οποία απλά κάνει halt τον επεξεργαστή σε ένα loop με ενεργοποιημένα τα interrupts όσο συνεχίζει να είναι άδειο το run-queue.

void context_switch(int pid)
{
    struct process *prev = proc + last_pid;
    struct process *new = proc + pid;
    if(last_pid != pid) {
        set_current_pid(new->id);
        /* switch to the new process' address space */
        set_pgdir_addr(new->ctx.pgtbl_paddr);
        /* set the new kernel stack in tss */
        tss->esp0 = PAGE_TO_ADDR(new->kern_stack_pg) + KERN_STACK_SIZE;

        /* push all registers onto the stack */
        push_regs();
        switch_stack(new->ctx.stack_ptr, &prev->ctx.stack_ptr);
        /* restore registers from the NEW STACK */
        pop_regs();
    } else {
        set_current_pid(new->id);
    }
}

Βλέπουμε ότι η context_switch αλλάζει page tables, και φροντίζει να αλλάξει το kernel stack σε αυτό του καινούριου process καλώντας την switch_stack (proc-asm.S). Επίσης φροντίζει να σώσει την κατάσταση των registers στο παλαιό stack πριν το αλλάξει, και να επαναφέρει τους registers που είχε σώσει την τελευταία φορά που έτρεξε το καινούριο process, κάνοντας pop από το νέο stack.

fork

Η fork (proc.c) όπως είπαμε δημιουργεί ένα καινούριο process, κοινοποιώντας το process που την κάλεσε. Το πρώτο πράγμα που πρέπει να κάνει η fork είναι να εντοπίσει μια ελεύθερη θέση στο process table, και να αναθέσει το αντίστοιχο pid στο καινούριο process.

Μετά πρέπει να κάνει allocate kernel stack για το νέο process. Σε αυτό το kernel stack βάζουμε κατ' αρχάς το υπάρχον interrupt frame, ώστε το καινούριο process να συνεχίσει την εκτέλεση από το ίδιο σημείο που ήταν και ο parent όταν επιστρέψει σε user space. Όμως φροντίζουμε να βάλουμε την τιμή 0 στον eax αυτού του interrupt frame, ώστε να επιστρέψουμε 0 από την fork στο child process (στον parent επιστρέφουμε το pid του καινούριου process).

Τέλος πρέπει να να δημιουργήσει για το καινούριο process ακριβές αντίγραφο της μνήμης του parent, κάτι που αναλαμβάνει να κάνει η clone_vm στο vm.c. Αυτή τη στιγμή η clone_vm δεν αντιγράφει την μνήμη του parent, αλλά κάνει τα δύο processes να μοιράζονται την ίδια μνήμη αντιγράφοντας μόνο τα page tables, καθώς επίσης αλλάζει και τα attributes στα page tables ώστε να είναι read-only η κοινή μνήμη. Αυτό γιατί στο επόμενο άρθρο θα υλοποιήσουμε την αντιγραφή του VM με copy-on-write. Έτσι αν τώρα στο test_proc μετά την κλήση στην fork προσθέσουμε και μια εντολή που γράφει στην μνήμη, π.χ. push %eax, θα δούμε ότι σηκώνεται αμέσως page fault.

Αποτέλεσμα

Εκτελώντας τον κώδικα σε αυτή τη φάση, βλέπουμε ότι εκτελείται κανονικά ο κώδικας του test_proc.S σαν user process, γίνεται το fork, και μετά συνεχώς παίρνουμε μηνύματα από τα δύο processes που καλούν hello, getpid, και sleep σε loop (εικόνα 2).

test process output
Εικόνα 2: Τα δύο πρώτα test processes του kernel.

Στο επόμενο τεύχος θα ολοκληρώσουμε την clone_vm υλοποιώντας copy-on-write, θα εξηγήσουμε πως δουλεύει η sleep, καθώς και οι συναρτήσεις wait και wakeup στον scheduler, και θα φτιάξουμε και μερικά ακόμα βασικά system calls όπως wait και exit. Μέχρι τότε, happy hacking.


Creative Commons License
Copyright © John Tsiombikas 2011
This article is licensed under the Creative Commons Attribution-ShareAlike 3.0 License.