Στο συγκεκριμένο φάκελο, περιλαμβάνεται τα εξής:
- Ένα απλό makefile (πατώντας
make, κάνουμε compile όλα τα απαραίτητα προγράμματα, ενώ μεmake cleanμπορούμε να διαγράψουμε τα αντικείμενα αρχεία που δημιουργήσαμε). - Ο φάκελος
Modules, περιλαμβάνει όλα τα κύρια .c αρχεία (τρία από αυτά βρίσκονται μέσα στο foldernfs_manager). - Ο φάκελος
Includes, περιλαμβάνει όλα τα .h αρχεία. - Ο φάκελος
Common, περιλαμβάνει τα αρχεία τα οποία έχουν κάποιες κοινές συναρτήσεις μεταξύ των κυρίων προγραμμάτων.
Για να εκτελέσουμε το πρόγραμμα πατάμε make, και στη συνέχεια ./nfs_manager -l <manager_logfile> -c <config_file> -n <worker_limit> -p <port_number> -b <bufferSize> (όπου το -n είναι optional και αν δεν δοθεί ή δοθεί όχι θετικός αριθμός τίθεται σε 0, ενώ το -b, πρέπει να είναι θετικός αριθμός διαφορετικά θα ξανά ζητηθεί input από τον χρήστη).
Ο "nfs_manager" μπορεί να τρέξει ανεξάρτητα του "nfs_console", αν θελήσουμε να τον τρέξουμε όμως (πατώντας ./nfs_console -l <console-logfile> -h <host_IP> -p <host_port>) επιτυγχάνετε σωστά και ομαλά η επικοινωνία μεταξύ "nfs_manager" - "nfs_console", μέσο των παραμέτρων -h <host_IP> και -p <host_port>.
Ο κάθε client, τον εκτελούμε ./nfs_client -p <port_number> και υποθέτουμε πως τρέχει για το δοσμένο port_number επ αόριστον και έχει ξεκινήσει πριν από την εκκίνηση του nfs_manager. Επίσης θεωρούμε πως ο τερματισμός ενός client γίνεται πατώντας ^C στο τερματικό του.
Απαρτίζεται από το nfs_console.c (και το common.c του φακέλου Common).
- nfs_console.c:
Eκτελεί όλες τις λειτουργίες για αποστολή εντολών (κάνοντας χρήση της συνάρτησης write_all() για σωστή και σίγουρη γραφή όλης της εντολής στο socket) και λήψη μηνυμάτων προς και από τον
nfs_manager. Πριν στείλει κάποια εντολή, ελέγχει αν το "format" της εντολής είναι αποδεκτό, και έπειτα την στέλνει. Από εκεί και στο εξής αν προκύψει κάποιο λάθος, θα το εντοπίσει οnfs_manager. Έπειτα αναμένει να πάρει απάντηση από τονnfs_managerμε το αποτέλεσμα της εντολής, το καταγραφεί στο logfile του (ο nfs_console) και είναι πάλι έτοιμος να καταχωρίσει νέα εντολή.
Απαρτίζεται από τα αρχεία του φακέλου nfs_manager (μέσα στο Modules) και τα header του φακέλου Includes (και το common.c του φακέλου Common).
-
nfs_manager.c: είναι το κύριο αρχείο το οποίο εκτελεί την
main(). -
manager_utils.c/h: περιέχει όλες τις επιμέρους συναρτήσεις για:
- να γράφει τα αντίστοιχα μηνύματα στο
terminal,manager-log-fileκαιconsole socket. - να διαχειρίζεται σωστά
LIST-PULL-PUSHcommands του κάθε client. - να διαχειρίζεται σωστά το
cancelκαιaddcommand του console.
- να γράφει τα αντίστοιχα μηνύματα στο
-
sync_info_mem_store.c/h: περιέχει τις δομές
sync_info(αποθηκεύει τις πληροφορίες κάθε file),buffer_t(είναι ο τύπος του κυκλικού μαςbuffer) καιworker_argv(αποθηκεύονται μόνο τα arguments που πρέπει να λάβει ο κάθε worker). Επίσης περιέχονται και οι συναρτήσεις των δομών για δημιουργία/διαγραφή κάποιουsync_info, αρχικοποίηση/διαγραφή ενόςbuffer_tαλλά και προσθήκης/αφαίρεσης/εύρεσης κάποιου στοιχείουsync_infoστοbuffer.
- Λειτουργία:
Αρχικά, ελέγχει αν έχει γίνει σωστή εκκίνηση με σωστές παραμέτρους. Έπειτα, φτιάχνει το socket επικοινωνίας με τον nfs_console, για να μπορεί αμέσως ο cosnole να γράψει σε αυτό. Ανοίγει το manager-log-file (για να μπορεί να καταγράψει σε αυτό οποιαδήποτε στιγμή) και αρχικοποιεί τον βασικό κυκλικό buffer (ονομασία προγράμματος: sync_buffer). Ο buffer, αποτελεί την ουρά αναμονής των αρχείων που τίθενται για συγχρονισμό. Δημιουργεί τα αντίστοιχα worker threads τα οποία θα είναι όσο είναι και το worker_limit.
Έπειτα ξεκινάει την ανάγνωση του config. Για κάθε μια γραμμή του config, κάνει connect στο socket του αντίστοιχου client και στέλνει την εντολή "LIST". Διαβάζει όσα bytes μπορεί από το socket και λαμβάνει ορθά, "γραμμή-γραμμή", τα files του συγκεκριμένου <source>. Ελέγχει, ξεχωριστά, αν υπάρχουν ήδη στο buffer και αν δεν υπάρχουν τα προσθέτει ή αν είναι γεμάτο το buffer περιμένει μέχρι να έχει χώρο να τα βάλει (ΑΚΑ εκτελεί putitem() στον buffer).
Παράλληλα ο κάθε worker (τα threads που εκτελούν την δουλειά ενός worker) αφαιρεί κάθε φορά από ένα στοιχείο ή αν είναι άδειο το buffer περιμένει μέχρι να υπάρξει στοιχείο για να πάρει (ΑΚΑ εκτελεί getitem() στο buffer) και ξεκινάει αντίστοιχα τον συγχρονισμό του αρχείου.
Κάνει connect στο socket του αντίστοιχου client (για το <source_file>) και στέλνει την εντολή "PULL". Διαβάζει από το socket, μέχρι να βρει τον πρώτο "space" χαρακτήρα για να καθορίσει το filesize. Έπειτα αν δεν προέκυψε κάποιο λάθος και έχει γίνει -1 το filesize(άρα ενημερώνει για το αντίστοιχο λάθος και προχωράει στο επόμενο item), κάνει connect στο socket για το <target_file>.
Στέλνει αρχικά την εντολή PUSH /target_dir/file -1 για να διαγράψει ο,τι υπήρχε στο <target_file> και στη συνέχεια στέλνει συνεχόμενα PUSH /target_dir/file chunk_size data μέχρι, το άθροισμα όλων των chunk_size που έστειλε να είναι ίσο με το filesize. Τέλος ενημερώνει τον client του target, με PUSH /target_dir/file 0 για να τερματίσει την διαδικασία.
Σε όλη την παραπάνω διαδικασία υπάρχουν τα αντίστοιχα μηνύματα με το σωστό format και αν προκύψει κάποιο σφάλμα, ο συγκεκριμένος worker που βλέπει το λάθος, ενημερώνει τον χρήστη, μόνο αν επιτρέπετε με βάση το format των μηνυμάτων και απλά συνεχίζει να λάβει το επόμενο item.
Ο manager, είναι σε θέση να εκτελέσει οποιαδήποτε εντολή έχει λάβει από τον console, αφού τελειώσει την προσθήκη των αρχείων του confing στο buffer. Διαβάζει από το socket του console και εκτελεί την αντίστοιχη εντολή:
-
Υλοποίηση της
add <source> <target>: Αν διαβάζειadd, τότε εκτελεί την ίδια διαδικασία που εκτελεί για κάθε γραμμή του confing. -
Υλοποίηση της
cancel <source>: Αν διαβάζειcancel, τότε πηγαίνει στο κάθε item τουbufferκαι ελέγχει αν έχουν το ίδιοsource nameμε αυτό της εντολήςcancel. Αν ναι, τότε αλλάζει το όνομα τουsourceκαι τουfile, σε μια αδύνατη ονομασία, σε"\0". Έτσι πλέον το συγκεκριμένο item, είναι ένα"special item", το οποίο όταν θα το δει ένας worker θα ξέρει να το διαγράψει άμεσος και να συνεχίσει κατευθείαν στο επόμενο. -
Υλοποίηση της
shutdown: Αν διαβάσειshutdown, τότε σταματάει να λαμβάνει άλλες εντολές. Στη συνέχεια, προσθέτει στοbufferκάποια νέα"special item", ένα για κάθε worker. Στο πρόγραμμα αυτά ισούνται μεNULL. Όταν ένας worker, δει ένα τέτοιο item, γνωρίζει να σταματήσει την εκτέλεση του. Έπειτα, κάνειpthread_join()ταworker threads, απελευθερώνει οποιαδήποτε δεσμευμένη μνήμη και τερματίζει ομαλά.
Απαρτίζεται από το nfs_client.c (και το common.c του φακέλου Common).
- nfs_client.c:
Ο κάθε
nfs_clientενόςdirectoryθα πρέπει να εκτελείτε πριν εκτελέσουμε τονnfs_manager. Δεν προκύπτει κάποιο σφάλμα σε άλλη περίπτωση, απλά δεν θα ληφθεί υπόψη κάποιο από τα αρχεία του για οποιαδήποτε διεργασία. Κάθεnfs_client, δημιουργεί το socket επικοινωνίας του με τονnfs_managerκαι περιμένει μέχρι να κάνει είτε οnfs_managerείτε κάποιοworker threadconnect σε αυτό. Μόλις βρεθεί κάποιο connection, δημιουργεί έναthreadγια την εκτέλεση της εισερχόμενης εντολής (στο πρόγραμμα εκτελείται η συνάρτησηexec_request()) και έπειτα το κάνειpthread_detach()και το αφήνει να εκτελεστεί μόνο του. Καθένα από αυτά τα threads διαβάζει από το socket τουnfs_manager, αρχικά τους 5 πρώτους χαρακτήρες (την εντολή + 1 για το "space") και εκτελεί την αντίστοιχη εντολή:
-
Υλοποίηση της
LIST: Διαβάζει από το socket, όσους χαρακτήρες μπορεί μέχρι να μην έχει άλλους να διαβάσει. Στη συνέχεια, ανοίγει τοdirectoryπου διάβασε, διαβάζει όλα τα ονόματα των αρχείων του περιέχει και γράφει στο socket το κάθεfile_nameπροσθέτοντας'\n'στο τέλος του. Στη συνέχεια, ενημερώνει ότι δεν υπάρχει άλλο αρχείο να πάρει οnfs_manager, γράφοντας στο socket".\n"και τερματίζει η διαδικασία και τοthread. -
Υλοποίηση της
PULL: Διαβάζει από το socket, όσους χαρακτήρες μπορεί μέχρι να μην έχει άλλους να διαβάσει. Στη συνέχεια, αποσπάει το συνολικό μέγεθος του αρχείου (που ζητήθηκε γιαPULL) και το στέλνει στο socket με την μορφήfilesize <space character>. Έπειτα, αφού έχει ενημερώσει τον worker πόσους χαρακτήρες θα διαβάσει, απλά ανοίγει τοfile, διαβάζει κάθε φορά όσους χαρακτήρες μπορεί και τους γράφει όλους στο socket μέχρι να μην υπάρχει αλλος χαρακτήρας να διαβάσει. Τέλος, τερματίζει η διαδικασία και τοthread. -
Υλοποίηση της
PUSH: Διαβάζει από το socket, όσους χαρακτήρες μπορεί μέχρι να βρει τα δύο πρώτα "space characters" (το πρώτο καθορίζει το όνομα τουtarget_fileκαι το δεύτερο τοchunksize). Αφού πάρει τιμή τοchunksizeελέγχουμε την τιμή του.
Αν είναι -1, ανοίγουμε απλά το <target_file> για τις επόμενες επαναλήψεις που θα γράψουμε σε αυτό και ξεκινάμε πάλι από την αρχή την διαδικασία αναγνώρισης του command (γνωρίζουμε ότι θα είναι PUSH ). Υπάρχει περίπτωση να έχει ήδη γίνει η ανάγνωση του επόμενου command από το socket, σε αυτή την περίπτωση, έχουμε φροντίσει να το διατηρήσουμε στη μνήμη, οπότε ελέγχουμε ορθά το επόμενο command κάθε φορά οποία στιγμή και να το έχουμε διαβάσει.
Αν είναι απλά θετικός αριθμός, διαβάζουμε από το socket και γράφουμε στο <target_file>, μέχρι να γράψουμε chunksize χαρακτήρες και όμοια συνεχίζουμε με το επόμενο PUSH.
Αν είναι 0 μόνο τότε κλείνουμε το <target_file> και πηγαίνουμε για να τερματίσει η διαδικασία και το thread.
Τέλος, να αναφέρω πώς σε όλο τον κώδικα υπάρχουν παντού αναλυτικά σχόλια με περισσότερες λεπτομέρειες για την κάθε μια λειτουργία.
Σας ευχαριστώ πάρα πολύ για τον χρόνο που αφιερώσατε!