Skip to content

mishozhghenti/OS_FINAL

Repository files navigation

OS_FINAL: Network Filesystem

Mikheil Zhghenti mzhgh14@freeuni.edu.ge


Network file system supports Raid 1 and Raid 5 distributions. Implemented client and server sides written in C, both from scratches. Uses Fuse library for implementing kernel system calls in user space. In this way client is able to distribute a data among the servers. The project includes errors handling during the system calls and logging support.

პროექტი კომპილირდება make-ს საშუალებით.
ქსელურ ფაილურ სისტემას აქვს ორი ძირითადი კომპონენტი - ესენია: კლიენტი და სერვერი.

თითოეული მათგანის გაშვების სინოფსისის მაგალითები:
კლიენტი ./net_raid_client client_config.txt და
სერვერი ./net_raid_server 127.0.0.1 10001 /home/misho/Desktop/storage1

სერვერის ზოგადი მიმოხილვა
სერვერი argv-დან შესაბამის პარამეტრებს იღებს და bind/listen-ით სტარტავს კონკრეტულ გადმოცემულ პორტზე "მომსახურებას". სერვერის ეფექტურ და უსაფრთხო ფუნქციონირებას(იმ გაგებით, რომ თუ კონკრეტულ request-ზე დაიქრაშა ჩვენი პროგრამა, სერვერი არ წყვეტს მუშაობას და აქტიური ახალი რექვესტის მისაღებად. ეს იმით, რომ თითოეულ მოთხოვნას ცალკე პროცესში ემსახურება.) სერვერის მხარეს შედარებით ნაკლები "საქმეა"(ლოგიკა მის მხარეს არაა დიდი რაოდენობით გატანილი), მას უფრო "კონკრეტული დავალებების" დამუშავება უწევს. მივმართავთ სერვერს, რომ შეასრულოს A სამუშაო: მიიღებს სერვერი A სამუშაობს, ასრულებს და შედეგს უკან გვიბრუნებს.

კლიენტის ზოგადი მიმოხილვა
კლიენტის მთავარი მიზანი, არის მოცემული task-ის მართვა, მენეჯმენტი. უდიდესი ლოგიკაა სწორედ მის მხარესაა გატანილი. იგი იღებს გადაწყვეტილებას, თუ როგორ მოექცეს data-ს, როგორ შეინახოს და მაავე დროს performance-ზე ფიქრიც უწევს.

პროექტის არქიტექტურა
თითოეულ ეტაპზე, main()-ის argv-დან დაწყებული სერვერის პასუხებიდან დასრულებული ერორ კოდების დაბრუნება ადეკვატურად ხდება. მაშინაც კი, როდესაც კონფიგ ფაილის გაპარსვის დროს მოხდება "ექსეფშენი" თუ ფაილი ვერ იპოვა, ვერ გახსნა ან ასე შემდეგ, ეს ადექვატურად ილოგება, როგორც გადმოცემულ LOG ფაილში ასევე სტანდარტულ output-ზე(ტერმინალში).
Server-თან "საუბრის" დროს მაგალითად syscall read-ის გამოძახების დროს მას გადმოეცემა path,buf,size, offset. სადაც buf-ში უნდა ჩაისეტოს ფაილიდან (path-იდან) წაკითხული მონაცემები. ცხადია ეს სიტემური ბრძანება იყენებს ჯერ გახსნის ბრძანებას, ხოლო შემდეგ კი უშუალოდ წაკითხვის ბრძანებას. ამ შემთხვევაში სავსებიშ შესაძლებელია არასწორი მისამართის გადმოცემა, ან ამ ფაილის გახსნის უფლება არ გვქონდეს, ან უკვე "in used"-ში იმყოფებოდეს და არ გვაძლევდეს ვინმე, ჩვენგან დამოუკიდებელი რამ მასზე წვდომას. ცხადია syscall-ი open() ამ შემთხვევაში უკვე წარუმატებლად დაბრუნდება და ამის გამო Error handling-ი იმაში მდგომარეობს, რომ თითოეული ეტაპის სტატუს კოდს ვითხოვ კლიენტში. თუკი read()-ში, მის მიმდინარეობისას open()-მა ვერ იმუშავა ამას ვამოწმებ კლიენტის მხარეს და თუ საჭიროა შეჩდომით ვასრულებ პროცედურას, წინააღმდეგ შემთხვევაში მივუყვები ბოლომდე read()-ს.

დეტალურად კლიენტი
კლიენტის გამოძახებისას, მას პარამეტრად გადმოეცემა configuration-ის მისამართი, სადაც ჩამოწერილია ყელა სერვერის მისამართი, სტორიჯების სახელები, ლოგირების ფაილის სახელი, ქეშის ზომა, თაიმაუთი, დასამაუნთებელი დირექტორიები და სხვა.

პროექტის სტრუქტურის მოწყობა ისე გადავწყვიტე, რომ თითოეულ Storage-ზე კონკრეტული 1 პროცესი მუშაობდეს. ეს უზრუნველყოფს იმას, რომ (ზემოთ უკვე ავღნიშნეთ), დამოუკიდებელი პროცესები საერთოდ "პრობლემას" უფრო ნაკლებად წარმოქმნიან. და თითოეული პროცესის მუშაობა საერთო ჯამში უფრო ეფექტური გამოვა.

აღსანიშნავია, რომ ყოველ პროცესში "საზიარო" მონაცემბს წარმოადგენს: errorlog, cache_size, cache_replacment, timeout. ხოლო მათ გარდა, კონკრეტულ Storage-ს მონაცემები, მხოლოდ კონკრეტულ პროცესს აინტერესებს. სწორედ ამიტომ, თითოეული Storage-ს სრული მონაცემების წაკითხვის შემდეგ (diskname, mountpoint, raid, servers, hotswap) მშობელი პროცესი fork()-ს აკეთებს და შვილი პროცესი მხოლოდ კონკრეტულ 1 პროცესსზე იქნება პასუხისმგებელი.

struct Client {
   char* error_log;
   int cache_size;
   char* cache_replacment;
   int timeout;
};

მაქვს Client-ის ტიპის სტრუქტურა და მასში ვინახავ ამ მონაცემებს. (იხ. client.h)

fork()-ი იძახება მაშინ როცა კონკრეტულ Storage-ზე სრული ინფორმაცია გვაქვს, ანუ ვიცით ყველა სერვერის მისამართი. თითოეული სერვერის შესახებ მონაცემს ასეთ სტრუქტურაში ვინახავ:

struct Server{
	char* ip;
	char* port;
};

ხოლო ყველა წაკითხულს ერთად კი მასივში ვინახავ(გლობალური ცვლადი net_raid_client.c-ში):

struct Server servers[10];

უკვე მზად არის, რომ სერვერთან "ინიციალიზაცია" მოვახდინო კავშირის და descriptor-ები "მოვიპოვო". ამისთვის ვიყენებ socket()-ს:

socket(AF_INET, SOCK_STREAM, 0);

და მის დაბრუნებულ sfd-ს ასევე გლობალურ მასივში ვინახავ, რომ შემდეგ ამის მეშვეობით კომუნიკაცია ვიქონიო სერვერთან.

int servers_sfd [10];

connect()-ის გამოძახების შემდეგ ვნახულობ თუ "კავშირზეა" სერვერი. თუ წარმატებით განხორციელდა დაკავშირება LOG-ში იწერება შესაბამისი მონაცემ. აღსანიშნავია, რომ აქვე ხდება დაკავშირება hotswap server-თან.

სერვერებთან "შეხების" შემდეგ გადავდივართ mount-ზე. ვიძახებთ

fuse_main(argc, new_argv, &all_methods, NULL); 

თუკი რაიმე პრობლემაა, დირექტორია ვერ იპოვნა, ან იპოვნა და უკვე დამაუნთებულია და სხვა პროცესი იყენებს მაშინ მეთოდი არანულოვანი მნიშვნელობით ბრუნდება და ესეც ილოგება შესაბამისი მესიჯით LOG ფაილში. და იდეაში, პროგრამა ამით ასრულებ მუშაობას, რადგან სამუშაოს ასე ვერ შეასრულებს.

აქ ჩამოთვლილია ყველა ის syscall-ი, რომელიც გადატვირთულია სისტემაში:

static struct fuse_operations all_methods = {
	.getattr	= my_getattr,
	.readdir	= my_readdir,
	.open		= my_open,
	.read		= my_read,
	.write      = my_write,
	.rename     = my_rename,
	.release    = my_release,
	.releasedir = my_releasedir,
	.rmdir      = my_rmdir,
	.mkdir      = my_mkdir,
	.unlink     = my_unlink,
	.create     = my_create,
	.utimens    = my_utimens,
	.opendir    = my_opendir,
};

სერვერთან კომუნიკაციის პროტოკოლი
თითოეულ გადატვირთულ მეთოდში printf()-ით console-ში ვბეჭდავ კონკრეტული მეთოდის სახელს, პროცესის ID-ს და სხვა პარამეტრებს, რომლებიც გადმოეცემა. მაგალითისთვის ასეთი ფორმტატით იქნება getattr-ის შემთხვევაში:

printf("Process ID:%d Diskname:%s Method:%s PATH:%s\n",getpid(), diskname, "getattr",path);

(ეს მიმარტივებდა DEBUG-ის პროცესს და ლოგირებაშიც მეხმარებოდა)

თითოეული syscall-ის გამოძახებისას კლიენტი ადგენს Request-ს, სადაც წერია syscall-ის სახელი და path. ამას ვაკეთებ შემდეგ ნაირიად, მაგალითისთვის:

	char request [strlen("getattr")+strlen(path)+2]; // size of the request
	sprintf(request, "%s %s", "getattr", path); // sets values

და ამ request-ს ვგზავნი პირველ ეტაპზე.

Server-ზე არის ამ მონაცემის parser-ი, რომელიც ადგენს თუ რომელი მეთოდი გამოვიძახოთ და რა პარამეტრით. (path-ის გადაცემა request-ში იმიტომ გადავწყვიტე, რომ ყველა იყენებს, ხოლო სხვა კონკრეტული პარამეტრები კი კომპლექსურია და ზოგი იყენებს, ზოგი - არა, და ამიტომ მათ ეტაპობრივად საჭიროემისამებრ ვგზავნი).

Server-ზე გაწერილია "დიდი if/else if/else" სადაც კონკრეტულ syscall-ს ასრულებს და შედეგს უკან კლიენტს უბრუნებს.

Server-თან კავშირის დამყარებისას თუ რაიმე პრობლემაა და ვერ მოხერხდა, იგი ავტომატურად გათიშულად არ ცხადდება. sleep()-ის შემდეგ კლიენტი კიდევ ცდილობს კავშირის დამყარებას.

თითოეული syscall-ის შესრულება ყველა საჭირო სერვერზე for() ციკლით ვითხოვ. გადავუვლი მოცემულ სერვერებს და რადგან ვიცი რომ მათზე იდენტური რამ უნდა მოხდეს, სანამ ყველა არ მიპასუხებს OK-ს. თუ კი რაიმე პრობლემაა და მართლა რეალურად სერვერი გამორთულია, მაშინ შესაბამისი კოდი ბრუნდება თითოეული syscall-ის შესრულების წარმატებულობის შესახებ.

დამაუნთებულ დირექტორიაში "ძრომიალით", სისტემური ბრძანებების გამოძახებით, როდესაც ეს ყველაფერი უკვე "გადატვირთულია" და ჩვენი სურვილის მიხედვით ხდება ყველაფერი ამ დირექტორიაში, ავრომატურად გამოდის სტორიჯის "გადმოტანა" და ცხადია იერარქიული დირექტორიის სტრუქტურად ავტომატურად ნარჩუნდება.

Write()-ს დროს, როდესაც კონკრეტული Storage არის Raid 1 ტიპის, ამ შემთხვევაში ჩაწერის თანმიმდევრობ არის რიგითობის მიხედვით. პირველ სერვერზე ჩაწერის დასრულების შემთხვევაში შემდეგ "გადადის" მეორე სერვერზე და იქაც ანალოგიურ/კოპიო მონაცემს ინახავს მირორინგს აკეთებს.
ასევეა წაკითხვაზე. რიგითობა შენარჩუებულია კონფიგის მიხედვით. ჯერ პირველი სერვერიდან წაიკითხავს შესაბამის მონაცემს და შემდეგ მეორე სერვერიდან წაიკითხავს იდენტურ მონაცემს.

Write() Raid 5 ტიპის Storage-ს შემთხვევაში უფრო customized flow გვაქვს. ამ შემთხვევაში ჩვენი მიზანია, რომ მოცემული მონაცემი ჩენ ხელთ არსებულ სერვერებში ისე გავანაწილოთ, რომ უფრო "მდგრადი" იყოს და ასევე წარმადობის კუთხით უკეთესიც. ამის იდეა მდგომარეობს Parity-ს არსებობაში. მონაცემებს ისე ვანაწილებთ რამდენიმე სერვერზე ისე, რომ უფრო მეტი სერვერის წარმადობას ვღებულობთ რეალურად. კონფიგის დამუშავების შემდეგ რეალურად ვადგენ თუ რამდენ სერვერზე უნდა გადანაწილდეს მონაცემი. შემდეგ, შემოსულ data-ს შემთხვევაში ვცდილობ რომ ეს მონაცემები თანაბრად გადავანაწილო თითოეულ სერვერზე.ისე ომ მხოლოდ თითო-თითო ჩანქი იყოს შენახული თითოეულ სერვერზე. ცხადია, რაც მეტია თითოეულ სერვერზე ჩანქების რაოდენობა მით უფრო დაცული/მდგრადია Storage-ს ფუნქციონირება. თითოეული Stipes თითოეულ სერვერზე ვგზავნი ვინახავ მათ კლიენტის მხარეს და პარალელურად ვითვლი გაგზავნილი მონაცემების ერთმანეთზე XOR-ს. გადანაწილების შემდეგ კი მიღებულ XOR-ს კი ერთ შერჩეულ სერვერზე ვგზავნი და ვინახავ.

უტილ კლასები (იხ. "utils.h")
აქ გატანილი მაქვს ყველა ის დამხმარე ფუნქციები, რომლებიც მეხმარება კლიენტის კონფიგის დაპარსვაში, მონაცემების კონვერტაციაში, კლიენტიდან სერვერთან გაგზავნილი data-ს ამოღებაში, დაპარსვასა, ანალიზში, sub string-ების ამოღებაში და სხვა. მაგალითად, კლიენტის მხარეს IP:PORT-ის გახლეჩვა და ცალცალკე სტრუქტურაში შენახვა. string-ების int-ად დაკასტვა. ასევე, სერვერის მხარეს syscall-ის ამოღება request-იდან, path-ის ამოღება request-იდან და

char* get_time(); 

რომელიც დროს აბრუნებს სტრინგად.

ასევე იმპლემენტირებულია XOR-ის დათვლა String-ებისთვის. (Raid 5-ის შემთხვევაში საჭიროა Parity-ს დათვლა, რომელსაც სწორედ ამ მეთოდების გამოყენებით ვაკეთებ)

char* two_strings_xor(char* s1,char* s2);
char* XOR(char* arg1, ...);

პირველი ფუნქცია იღებს ორ სტრინგს და აბრუნდებს მათ bitwise XOR-ს.
ხოლო მეორეს კი წინასწარ განუსაზღვრელი რაოდენობის string გადმოეცემა და ყველა მათგანის XOR-ს აბრუნებს. (ცხადია XOR-ის იმპლემენტაციაში two_strings_xor() მონაწილეობს).

ლოგირება / კლასების დეკომპოზიცაა (იხ. "logger.h")
ძალიან გადატვირთული რომ არ ყოფილიყო "utils.h" ფაილი დეკომპოზიციით კიდევ უფრო განვტვირთე დამხმარე კლასების ფუნქციონალი. აქ აღწერილი მეთოდებით ხდება ლოგირება. სულ არის 3 მეთოდი, ესენია:

int logger_init(char* file_name);
void log_message(char* msg);
void logger_deinit();

პირველი init მეთოდი ხსნის/ქმნის ფაილს სახელწოდებით file_name. თუკი ეს ოპერაცია შეუძლებელია, წარმატებით ვერ განხორციელდა მაშინ შესაბამისი ერორ კოდით სრულდება მეთოდი და ტერმინალში ილოგება "Can not open the logger file". თუკი ყველაფერი კარგად დასრულდა მაშინ ბრუნდება 0 და მის "private" ცვლაში ინახება ეს ფაილი. ხოლო წარმატებულად დაბრუნებული კოდის შემდეგ კი შეგვიძლია, გამოვიძახოთ log_message(char* msg)-რომელიც თავის მხრივ იყენებს init()-ის მიერ "მოპოვებულ" ფაილს და იქ წერს გადმოცემულ მესიჯს. აღსანიშნავია, რომ ყოველ log_message-ის გამოძახებისას გადმოეცემა მხოლოდ ტექსტი, და ეს მეთოდი კი ავტომატურად ლოგერ ფაილში ინახავს აღნიშნული ფორმატით თითოეულ ლოგს(თარიღი, Storage-ს სახელი და მესიჯი). მაგალითად:

[Wed Aug 15 03:24:46 2018] STORAGE1 mountpointing to: /home/misho/Desktop/a1

logger_deinit() - კი დესტრუქტორის ფუნქციას ასრულებს. გამოყოფილ მეხსიერებას ასუფთავებს.

Releases

No releases published

Packages

No packages published