> "File Locking in UNIX" : Bu konuya değinmeden evvel, yardımcı olması açısından, UNIX türevi sistemlerin dosya sistemlerinde kullandığı "i-node" yapısı üzerinde durmak istiyoruz. Anımsanacağı üzere, evvelki konularda, dosya betimleyici tablosunun indislerinde ilgili "file" nesnelerini gösteren göstericilerin bulunduğunu belirtmiştik. İş bu "file" nesneleriyse, Linux kaynak kodlarına bakıldığında, "struct file" türüyle temsil edilmektedir. Hatta ilgili şema aşağıdaki gibidir; ... -> fdtable(fd) -> Elemanları "file*" olan bir dizi -> Bu dizideki her bir eleman "file" türünden. ^ ^ ^ Açtığımız her bir yeni dosyanın bilgilerini tutan, "file" türünden yapı nesneleri. İş bu "file" türünden yapı nesnelerinin tutulduğu dizi. "fdtable" isimli yapı içerisindeki "fd" isimli eleman, iş bu diziyi göstermektedir. İşte diskteki bir dosya kullanılmaya başlandığı zaman, işletim sistemi aynı zamanda "i-node" de oluşturmaktadır. Bu "i-node" nesnesi, farklı prosesler aynı dosyayı açsalar bile, o dosya için toplamda bir tanedir. Bu "i-node" nesnesi içerisinde ise tipik olarak "stat" fonksiyonlarıyla elde ettiğimiz bilgiler bulunmaktadır(örneğin, "fstat" fonksiyonu, hiç disk işlemleriyle uğraşmadan, bilgileri doğrudan bu "i-node" nesnesinin içerisinden almaktaydı). Buradaki anahtar nokta bir dosya için, işletim sisteminin genelinde, toplamda bir adet "i-node" nesnesinin oluşturulmasıdır. Çünkü "i-node" nesneleri, işletim sisteminin diste tekil olan o dosyaya ait olan bilgilerin yerleştirildiği nesnelerdir. O dosya diskte bir tane olduğuna göre, çekirdek tarafındaki temsilinde de tek olmalıdır. Tabii modern işletim sistemleri, dosyalara ilişkin "i-node" elemanlarını bir "cache" sistemiyle saklamaktadır. Yani bir dosya hiç bir proses tarafından kullanılmıyor olsa da o dosyanın bilgileri ilgili "cache" içerisinde bulunuyor olabilir. Linux "i-node" elemanlarının "cache" sistemi için "LRU(Least Recently Used)" stratejisine sahip bir sistem kullanmaktadır. Bu durumda yukarıdaki gösterimin daha gerçekçi gösterimi aşağıdaki gibi olabilir; ... -> fdtable(fd) -> Elemanları "file*" olan bir dizi -> Bu dizideki her bir eleman "file" türünden -> "i-node" nesnesi ^ ^ ^ Açtığımız her bir yeni dosyanın bilgilerini tutan, "file" türünden yapı nesneleri. İş bu "file" türünden yapı nesnelerinin tutulduğu dizi. "fdtable" isimli yapı içerisindeki "fd" isimli eleman, iş bu diziyi göstermektedir. Konuya gelecek olursak; birden fazla prosesin aynı dosyaya erişmek istemesi durumuna gerçekleşecek olaylar silsilesine genel olarak "file locking" denir. UNIX ve türevi sistemlerde "read" ve "write" işlemleri sistem genelinde atomik işlemlerdir. Yani işletim sistemi bu işlemleri sıraya dizer ve bir işlem tamamen bittiğinde diğerini çalıştıracak şekilde tasarlanmıştır. Dolayısıyla bir noktaya aynı anda "read" ve "write" işlemleri yapılmak istendiğinde ya yazma işleminden evvelkiler okunur ya da yazma işleminden sonrakiler. Eğer bir noktaya aynı anda "write" yapmak isteseydik ya birisinin ya da diğerinin yazdığı gözükecektir. Pekiyi işlemler atomik ise neden "file locking" mekanizmasına ihtiyaç duyulsun? Örneğin, şöyle bir senaryo düşünelim; Biz bir dosyanın belli bir yerine bir bilgi yazmak isteyelim. Ancak bu bilgiye ilişkin "hash" değeri de dosyanın başka yerine yazılacak olsun. Burada yazılmak istenen bilgi ile ona ait "hash" değerinin arasındaki bağlantının korunması gözetilmektedir. Dolayısıyla birbiriyle alakalı iki "write" işlemi yapılacaktır. Eğer birinci "write" işlemini tamamladıktan sonra ancak henüz ikinci "write" işlemine başlamadan evvel, yani sadece yazılmak istenen bilginin yazılması fakat ona ilişkin "hash" değerinin henüz yazılmaması durumunda, ikinci bir proses hızlı davranarak iş bu iki "write" işlemini de yaparsa ve bunun üzerine biz kendi ikinci "write" işlemini yaparsak, bilgi ile ona ait "hash" değeri arasındaki ilişkiyi koparmış oluruz. İşte böylesi senaryoların çok olduğu veritabanı yönetim sistemleri uygulamalarında proses önce bilgiyi yazar, daha sonra başka yere ona ait "hash" değerini yazar ve bilgi ile "hash" değeri de tutarlı olur. İşte bu tutarlılığı sağlamak için kullanılacak ilk yöntem senkronizasyon nesnesi kullanmaktır. Yani ilk "write" işlemine başlamadan ilgili "mutex" nesnesi kilitlenir, ikinci "write" işleminden sonra da kilit açılır. Ancak bu çözüm hem yorucu hem de yavaş kalmaktadır. İşte bu tür durumlar için "file locking" mekanizmaları kullanılır. "file locking" mekanizması Bütünsel olarak ve "offset" temelinde(dosyanın o bölümünü kapsayacak şekilde) olacak biçimde iki farklı şekilde kurulur. Bu mekanizmanın bütünsel olarak kurulması pek kullanışlı değildir ve seyrek kullanılır. "offset" temelli olan ise kendi içerisinde isteğe bağlı("advisory") ve zorunlu("mandotary") olmak üzere ikiye ayrılır. >> Bütünsel Kilitleme: Yukarıdaki senaryoyu baz alırsak; iki "write" işlemi sırasında dosyanın hepsi kilitlenir. Dolayısıyla dosyanın bizimle alakası olmayan bölümlerine bile "write" işlemi yapılamaz. "mutex" benzeri bir senkronizasyon nesnesi kullanmak gibidir. Bu mekanizma için "flock" isimli fonksiyon kullanılır. Bu fonksiyon mevcut POSIX standartlarında bulunmamaktadır, Linux sistemlerine özgüdür. >>> "flock" : Fonksiyonun prototipi aşağıdaki gibidir. #include int flock(int fd, int operation); Fonksiyonun birinci parametresi bütünsel kilitlenecek dosyaya ilişkin betimleyiciyi, ikinci parametre ise kilitlemenin nasıl yapılacağını belirtir. İş bu ikinci parametre şunlardır biri olabilir; -> "LOCK_SH" : "read" amacıyla erişmek için. -> "LOCK_EX" : "write" amacıyla erişmek için. -> "LOCK_UN" : Kilidi kaldırmak için. Bu durumda ilk başta ya "LOCK_SH" veya "LOCK_EX" ile kilitlenmeli, işlem bitince "LOCK_UN" ile kilit kaldırılmalıdır. Fonksiyon başarı durumunda "0", hata durumunda "-1" ile geri döner ve "errno" değişkeni uygun değere çekilir. Özetle bu yöntemi aşağıdaki gibi kurabiliriz; // Writer Process flock(fd, LOCK_EX); // Writing... flock(fd, LOCK_UN); //... // Reader Process flock(fd, LOCK_SH); // Reading... flock(fd, LOCK_UN); İşte "Writing" işlemi sırasında bir başka proses "flock" ile "LOCK_SH" veya "LOCK_EX" kullanarak aynı dosyaya erişmek isterse, bloke edilir. Ancak "Reading" işlemi sırasında "flock" ile "LOCK_SH" kullanarak aynı dosyaya erişmek istediğimizde BLOKE GERÇEKLEŞMEZ. Ancak "LOCK_EX" ile erişmeye çalışırsa bloke edilir. Ne zamanki "LOCK_UN" yapılır, o zaman blokeler kaldırılır. Görüldüğü üzere buradaki çalışma mantığı "reader-writer" mekanizmasına benzer. Yani en az bir tanesi "write" yapıyorsa diğerlerinin bekler, yalnızca "read" yapılacaksa buna izin verilir. Eğer yukarıdaki blokelerin gerçekleşmesini istemiyorsak, "LOCK_NB" bayrağını "bitwise-OR" ile yukarıdaki bayraklarla kullanmalıyız. Eğer prosesimiz "fork" yaparsa, bu kilitler de alt prosese aktarılır. Yine "exec" işlemi sırasında da kilit aktarılır. Çünkü kilit bilgisi dosya betimleyicisinin içerisinde saklanır. Dolayısıyla aynı dosyaya ilişkin ikinci bir dosya betimleyicisi oluşturursak, bu kilit bilgisi onda olmayacaktır. Fakat aynı dosya betimleyicisi ile birden fazla kez "flock" çağrısı yapılırsa, en son yapılan çağrıda kullanılan kilit uygulanır ve evvelki kilitlerin etkisi kaldırılır. Eğer kilit hiç açılmazsa, kilitlenen dosyaya ilişkin son dosya betimleyicisi de kapatıldığında, kilit kaldırılır. Diğer yandan bazı programların yalnızca tek bir kopyasının çalışabilir olması istenmektedir. İşte bunu sağlamak için de Bütünsel Kilitleme uygulanır. Bunun için programımız çalışmaya başladığında bir dosyaya Bütünsel Kilitleme uygular. Böylece aynı programı ikinci kez çalıştırmamız durumunda, yine aynı dosyaya Bütünsel Kilitleme uygulayacağından, kilitleme başarısız olacaktır. Bu noktada bloke oluşumunu da pekala engellememiz gerekmektedir. Programın akışını da uygun şekilde düzenlersek, programlarımızın yalnızca bir kopyasının çalışabilir olmasını mümkün kılabiliriz. Şöyleki; #define LOCK_FILE_PATH "lock.dat" //... int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) exit_sys("open"); if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { fprintf(stderr, "only one instance of this program can run...\n"); exit(EXIT_FAILURE); } else { exit_sys("flock"); } } //... Aşağıda bu kullanıma ilişkin bir örnek verilmiştir. * Örnek 1, Bu örnekte "LOCK_NB" bayrağını kullanarak bloke oluşmasının da önüne geçmiş bulunuyoruz. #include #include #include #include #include #include #define LOCK_FILE_PATH "lock.dat" void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) exit_sys("open"); if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { fprintf(stderr, "only one instance of this program can run...\n"); exit(EXIT_FAILURE); } else { exit_sys("flock"); } } sleep(10); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >> "offset" Temelli: Sadece dosyanın belli bir kısmının erişime kapatılmasıdır. Böylelikle bizimle alakası olmayanlar bölümlerde yazma işlemi gerçekleşebilir. Bu mekanizma ise "fcntl" fonksiyonu ile gerçekleştirilir. Anımsanacağı üzere bu fonksiyon bir dosya betimleyicisi ile bazı özel işlemler yapmak için çağrılır. Genel bir fonksiyondur. Prototipi aşağıdaki gibidir. Bir POSIX fonksiyonudur. #include int fcntl(int fildes, int cmd, ...); Fonksiyonun birinci parametresi yine bir dosya betimleyicisidir. İkinci parametreye ise komut denir ve şu değerlerden birisini alır; "F_SETLK", "F_SETLKW", "F_GETLK" Bu değerlerden, -> "F_SETLK" : Blokesiz kilitleme yapmak için kullanılır. Yani "incompatible" kilit isteklerinde ilgili "thread" bloke olmaz ancak "fcntl" fonksiyonunun başarısızlıkla geri döner. -> "F_SETLKW" : Blokeli kilitleme yapmak için kullanılır. Yani "incompatible" kilit isteklerinde ilgili "thread bloke olur, ta ki "incompatible" durum ortadan kaldırılana kadar. Uygulamalarda genellikle bu kullanılır. -> "F_GETLK" : Hedef bölgenin sanki kilitlenecekmiş gibi kontrol edilmesini sağlar. Kilit durumunu istediğimiz bölgede ayrık vaziyette iki farklı kilit de bulunuyor olabilir. Bu durumda "fcntl" fonksiyonu bu kilit bilgilerinden herhangi birisini bize vermektedir. Burada bahsedilen "incompatible" durum ise şudur; örneğin bir bölgeyi yazmaya karşı kilitlemek isteyelim. Eğer ilgili bölge halihazırda kilitlenmişse, ilgili "thread" ya blokede bekler ya da bloke olmaz ve "fcntl" başarısızlıkla geri döner. İşte "F_SETLK" kullanırsak bloke oluşmaz ve "fcntl" başarısızlıkla geri döner. "F_SETLKW" kullanırsak da bloke oluşur, ta ki kilit kaldırılana kadar. Üçüncü parametreye ise "flock" türünden bir yapı nesnesinin adresini geçeriz. Bu yapı aşağıdaki gibi tanımlanmıştır: struct flock { short l_type; // Type of lock; F_RDLCK, F_WRLCK, F_UNLCK. short l_whence; // Flag for starting offset. off_t l_start; // Relative offset in bytes. off_t l_len; // Size; if 0 then until EOF. pid_t l_pid; // Process ID of the process holding the lock; returned with F_GETLK. }; Yapının, -> "l_type" : Kilitlemenin cinsini belirler. "F_RDLCK", "F_WRLCK" veya "F_UNLCK" bayraklarından birisini değer olarak alır. Bu bayraklardan, -> "F_WRLCK" bayrağını kullanırsak, kilitlenen bölge üzerinde ne "read" ne de "write" işlemine izin EDİLMEDİĞİNİ belirtmiş oluruz. Tabii bunun için ilgili dosyanın prosesimiz tarafından yazma modunda açılmış olması gerekmetedir. -> "F_RDLCK" bayrağını kullanırsak, kilitlenen bölge üzerinde "read" için izin verildiği fakat "write" için izin verilmediğini belirtmiş oluruz. Tabii bunun için ilgili dosyanın prosesimiz tarafından okuma modunda açılmış olması gerekmetedir. -> "F_UNLCK" bayrağını kullanırsak, o bölgedeki kilidi kaldırmak istediğimizi belirtmiş oluruz. Görüldüğü üzere bu bağlamda "flock" fonksiyonunu anımsatmaktadır. Son olarak "F_GETLK" kullanılmışsa ve herhangi bir çelişki yoksa, bu elemanın değeri "F_UNLCK" bayrağına çekilir. Eğer çelişki varsa, çelişkiye yol açan kilit bilgileri "flock" yapısına doldurulur. -> "l_whence" : "offset" belirtmektedir. "SEEK_SET", "SEEK_CUR" veya "SEEK_END" değerlerinden birisini değer olarak alır. -> "l_start" : Kilitlenecek bölgenin başlangıç noktasını belirler. -> "l_len" : Kilitlenecek bölgenin uzunluğunu belirler. "0" değerini geçersek, "l_start" değerinden itibaren dosya sonuna kadar kilitlenecektir. Yani bu elemana "0" değeri verirsek, dosya ne kadar büyütülürse büyütülsün, büyütülen tüm alanlar kilitlenecektir. -> "l_pid" : Bu elemanı biz doldurmuyoruz. Eğer fonksiyonun ikinci parametresine "F_SETLK" veya "F_SETLKW" geçilmişse bu yapıyı bizim doldurmamız, "F_GETLK" geçilmişse bu yapıyı fonksiyonun kendisi dolduracaktır. Fonksiyonu ya iki argüman ya da üç argüman ile çağırırız. Üç argümanla çağıracaksak, bu üçüncü argüman her zaman "flock" yapı türünden olmalıdır. Fonksiyon başarı durumunda "0", hata durumunda ise "-1" ile geri döner ve "errno" değişkeni uygun değere çekilir. Öte yandan bu fonksiyonu ve mekanizmayı kullanırken de şu hususlara da dikkat etmeliyiz: -> "fcntl" ile kurulan kilitler "fork" işlemi ile alt proseslere aktarılmazlar. -> "exec" işlemleri sonucunda kilit bilgileri de aktarılır. -> Bir dosyanın kilit bilgileri o dosyanın diskteki varlığı üzerine değil, çekirdek içerisinde oluşturduğu "i-node" nesnesi içerisine yerleştirilmektedir. Dolayısıyla aynı dosyayı açmış olan farklı prosesler, aynı "i-node" nesnesini gördükleri için, aynı kilit bilgilerine sahip olurlar. Genellikle UNIX türevi işletim sistemleri, kilit bilgilerini "i-node" nesnesi içerisinde, bağlı liste kullanarak, "process ID" değerlerine göre sıralı bir biçimde tutar. -> Kilidin kaldırılması için, o kilidi koyan prosesin "F_UNLCK" yapması gerekmektedir. Ancak en kötü olasılıkta, ilgili dosyanın dosyanın kapatılmasıyla birlikte o prosesin yerleştirdiği kilit bilgileri de kaldırılacaktır(açılacaktır). Aynı proses aynı dosyayı birden fazla kez açmış olsa bile, herhangi bir dosya betimleyicisinin kapatılması da kilidin kaldırılması için yeterlidir. Öte yandan proses biterken zaten betimleyiciler de kapatılacağı için, kilitler yine kaldırılacaktır. -> Aynı proses kendisinin yerleştirmiş olduğu bir kilidin türünü değiştirebilir, bu durumda bir çelişki kontrolü yapılmamaktadır. Örneğin bir dosyanın belli bir bölgesinde "F_WRLCK" yerleştirmiş olabiliriz. Sonrasında bunu "F_RDLCK" ile yer değiştirebiliriz. Bu işlem atomik düzeyde YAPILMAKTADIR. Buradaki değiştirme işleminde önce "unlock" sonra yeni türde "lock" DEĞİL, halihazırda "lock" iken başka türden "lock" a çevirme kastedilmiştir. Dolayısıyla "unlock" işlemi "fcntl" içerisinde arka planda yapılmaktadır. -> "deadlock" oluşmamasına dikkat etmeliyiz. Yani biz bir prosesin kilidi açmasını beklerken, o proses de bizi bekliyorsa "deadlock" oluşacaktır. İşte "deadlock" durumu "fcntl" fonksiyonu tarafından otomatik olarak tespit edilmektedir. Örneğin, bir proses "F_SETLKW" ile kilit koymak istesin ve bir şekilde "deadlock" oluşsun. "fcntl" fonksiyonu başarısız olur, "errno" ise "EDEADLK" değerini alır. -> Kilitlenmek istenen bölgede birden fazla ayrık kilit bulunuyor olabilir. Kilitlemeyi yapabilmemiz için, bu ayrık kilitlerin hiç birisinde bir çelişkinin olmaması gerekmektedir. Aksi halde "F_SETLK" komutunda "fcntl" başarısız olur, "F_SETLKW" komutunda ise "fcntl" blokeye yol açar. -> Bir bölgenin bir kilit türüyle kilitlenmiş olduğunu varsayalım. Aynı proses bu bölgenin bir kısmını başka bir kilit türüyle kilitleyebilir ya da o kısmın kilidini kaldırabilir. Bu durumda işletim sistemi otomatik olarak kilitleri birbirinden ayıracak, bağımsız hale getirecektir. Örneğin, 0000000000000000000000000000000000000000000000000000000000000000 biçiminde bir alanımız olsun. Bu alanı aşağıdaki gibi kısmi olarak kilitleyelim. Şöyleki; ...WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW... Daha sonra kilitlenmiş bu bölgenin bir kısmındaki kilit türünü değiştirelim. Şöyleki; ...WWWWWWWWWWWWWRRRRRWWWWWWWWWWWWWWWWWW... Artık üç adet kilitli bölgeye sahip olmuş olacağız. Diğer yandan kilidin kaldırılması, eğer bu işlem kilitleyen proses tarafından yapılıyorsa, her daim başarılı olacaktır. Dolayısıyla yukarıdaki üç bölgeye ilişkin kilitleri tek bir "F_UNLCK" ile kaldırabiliriz. Üç defa "F_UNLCK" yapmaya gerek yoktur. Kilitlenmemiş alana "F_UNLCK" uygulanmasında herhangi bir mahzur yoktur, "fcntl" fonksiyonu başarısız olmayacaktır. -> Eğer kilitleme "F_SETLK" ile yapılıyorsa "EACCESS" ve "EAGAIN" değerlerine, eğer kilitleme "F_SETLKW" ile yapılıyorsa "EDEADLK" değerine karşı "errno" değişkeni sınanmalı ve bu üç hata koduna özel hata mesajları yazdırılmalıdır. Yani bu üç hata kodu özel olarak işlenmelidir. if (fcntl(fd, F_SETLK, &fl) == -1) { if (errno == EACCESS || errno == EAGAIN) { fprintf(stderr, "lock failed!..\n"); //... } else if (errno == EDEADLK) { fprintf(stderr, "deadlock occured!..\n"); //... } else { perror("fcntl"); //... } } Aşağıda kilitleme işlemlerini test edebilmek için örnek programlar verilmiştir. * Örnek 1, Aynı dosyanın iki farklı bölgesi üzerinde birbiriyle ilgili iki güncelleme yapmak isteyelim. Bunun için tipik olarak o iki bölge de kilitlenir. Daha sonra güncellemeler gerçekleştirilir. En sonunda da kilitler açılır. Şöyleki; fcntl(fd, F_SETLKW, ®ion_i); /* F_WRLCK */ fcntl(fd, F_SETLKW, ®ion_ii); /* F_WRLCK */ //... write(fd, region_i, size_i); write(fd, region_ii, size_ii); //... fcntl(fd, F_SETLKW, ®ion_i); /* F_UNLCK */ fcntl(fd, F_SETLKW, ®ion_ii); /* F_UNLCK */ * Örnek 2, Bu programı çalıştırırken komut satırı argümanı olarak kilitlenecek dosyanın yol ifadesini veriniz. Örneğin, "./flock-test test.txt". Program çalıştırıldığında "CSD (55306)>" şeklinde bir "prompt" a düşmektedir. Buradaki "55306" ise "Process ID" ifade etmektedir. Girilecek komutlar ise " " biçiminde olmalıdır, (bkz. "CSD (55306)>F_SETLK F_RDLCK 0 64"). Böylelikle "test.txt" dosyasının "0" numaralı "offset" numarasından başlayan ve "64 byte" uzunluğundaki bölgeye, "F_RDLCK" yerleştirilmek istenmiştir. "F_SETLK" komutu kullanılmıştır böylelikle aksi durumda bloke oluşmayacaktır. İşte başka bir terminal üzerinden yine "CSD (55377)>F_SETLK F_WRLCK 0 64" komutu çalıştırılsın. Bu durumda ekrana "Locked failed!.." yazısı çıkacaktır. Tabii "F_GETLK" komutunu kullanırken kilit türünün belirtilmesi gereksizdir. Ancak bir kilit türünü yine de belirtmek zorundayız. Programdan çıkmak için "quit" komutu çalıştırılmalıdır. /* fclock-test.c */ // ./flock-test #include #include #include #include #include #include #define MAX_CMDLINE 4096 #define MAX_ARGS 64 void parse_cmd(void); int get_cmd(struct flock *fl); void disp_flock(const struct flock *fl); void exit_sys(const char *msg); char g_cmd[MAX_CMDLINE]; int g_count; char *g_args[MAX_ARGS]; int main(int argc, char *argv[]) { int fd; pid_t pid; char *str; struct flock fl; int fcntl_cmd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } pid = getpid(); if ((fd = open(argv[1], O_RDWR)) == -1) exit_sys("open"); for (;;) { printf("CSD (%ld)>", (long)pid), fflush(stdout); fgets(g_cmd, MAX_CMDLINE, stdin); if ((str = strchr(g_cmd, '\n')) != NULL) *str = '\0'; parse_cmd(); if (g_count == 0) continue; if (g_count == 1 && !strcmp(g_args[0], "quit")) break; if (g_count != 4) { printf("invalid command!\n"); continue; } if ((fcntl_cmd = get_cmd(&fl)) == -1) { printf("invalid command!\n"); continue; } if (fcntl(fd, fcntl_cmd, &fl) == -1) if (errno == EACCES || errno == EAGAIN) printf("Locked failed!...\n"); else perror("fcntl"); if (fcntl_cmd == F_GETLK) disp_flock(&fl); } close(fd); return 0; } void parse_cmd(void) { char *str; g_count = 0; for (str = strtok(g_cmd, " \t"); str != NULL; str = strtok(NULL, " \t")) g_args[g_count++] = str; } int get_cmd(struct flock *fl) { int cmd, type; if (!strcmp(g_args[0], "F_SETLK")) cmd = F_SETLK; else if (!strcmp(g_args[0], "F_SETLKW")) cmd = F_SETLKW; else if (!strcmp(g_args[0], "F_GETLK")) cmd = F_GETLK; else return -1; if (!strcmp(g_args[1], "F_RDLCK")) type = F_RDLCK; else if (!strcmp(g_args[1], "F_WRLCK")) type = F_WRLCK; else if (!strcmp(g_args[1], "F_UNLCK")) type = F_UNLCK; else return -1; fl->l_type = type; fl->l_whence = SEEK_SET; fl->l_start = (off_t)strtol(g_args[2], NULL, 10); fl->l_len = (off_t)strtol(g_args[3], NULL, 10); return cmd; } void disp_flock(const struct flock *fl) { switch (fl->l_type) { case F_RDLCK: printf("Read Lock\n"); break; case F_WRLCK: printf("Write Lock\n"); break; case F_UNLCK: printf("Unlocked (can be locked)\n"); } printf("Whence: %d\n", fl->l_whence); printf("Start: %ld\n", (long)fl->l_start); printf("Length: %ld\n", (long)fl->l_len); if (fl->l_type != F_UNLCK) printf("Process Id: %ld\n", (long)fl->l_pid); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi "offset" temelli olan kilitleme mekanizmasındaki isteğe bağlı("advisory") ve zorunlu("mandotary") olma kavramları nedir? Aslında yukarıdaki "fcntl" fonksiyonu aslında isteğe bağlı("advisory") kavramına ilişkindir. Burada yazma ve okuma yapacak olan programların hepsi birbirleriyle koordineli yazıldığı için, onlar da aslında aynı kilit mekanizmasını kullanarak "read" ve "write" fonksiyonlarını çağırmaktadırlar. Buradaki senkronizasyonu "read" ve "write" fonksiyonları DEĞİL, "fcntl" çağrıları yapan o programlar kendi arasında oluşturmaktadır. Yoksa "read" ve "write" fonksiyonlarının kendileri yukarıda anlatılan kilit mekanizmalarını DİKKATE ALMAMAKTADIR. BİR DİĞER DEYİŞLE ALAKASIZ BİR BAŞKA PROGRAM, KİLİT VURULAN ALANA YAZMA VE/VEYA OKUMA YAPABİLİR. Buradaki kritik nokta "fcntl" çağrısını yapan proseslerin aynı kişiler tarafından yazılmış olmaları, bir diğer deyişle birbirlerinden haberdar olmalarıdır. İşte zorunlu("mandotary") olma durumunda "read", "write" fonksiyonları da artık kilidi dikkate ALMAKTADIR. Ancak zorunlu("mandotary") olması sistemi çok yorduğundan(kilitler konulmamış bile olsa kilitlere baktığı için), genellikle isteğe bağlı("advisory") oluş biçimi kullanılır. Pekiyi zorunlu("mandotary") olması nasıl gerçekleştirilir? Aslında isteğe bağlı olması ile zorunlu olması arasında uygulanış biçimi arasında bir farklılık yoktur. Kilitlemenin isteğe bağlı mı yoksa zorunlu mu olacağı "mount" parametrelerine ve o dosyanın erişim haklarına bakılarak belirlenmektedir. Ancak zorunlu("mandotary") olması POSIX standartlarında da yer almaz. Linux sistemlerinde zorunlu("mandotary") kılmak için şu adımları takip etmemiz gerekiyor; -> "df" komutunu argümansız kullanarak hangi dosya sisteminin "mand" parametresiyle "mount" edildiğini tespit edebiliriz. CSD: /home> df Filesystem 1K-blocks Used Available Use% Mounted on /dev/sda5 30297152 23455136 6825632 78% / tmpfs 65536 0 65536 0% /dev tmpfs 4073636 0 4073636 0% /sys/fs/cgroup shm 65536 0 65536 0% /dev/shm /dev/sda1 30297152 23455136 6825632 78% /home tmpfs 524288 0 524288 0% /tmp Daha sonra seçtiğimiz dosya sisteminin parametrelerini görmek için, o dosya sistem için, "mount | grep" komutunu çalıştırmalıyız. CSD: /home>mount | grep /dev/sda5 /dev/sda5 on / type ext4 (rw, relative) Dosyanın içinde bulunduğu dosya sisteminin "-o mand" seçenekleri ile "mount" edilmiş olması gerekir. Halihazırda "mount" edilmiş bir dosya sistemini iş bu seçenek ile "remount" yapmak için, "sudo mount -o mand,remount " komutunu çalıştırmalıyız. Buradaki "device" kısmına dosya sistemini, "mount point" kısmına da "mount" edileceği noktayı yazıyoruz. Böylelikle o dosya sistemi yeniden "mount" edilmiş olacaktır. Örneğin, "sudo mount -o mand,remount dev/sda5 /" komutunu çalıştırırsak "dev/sda5" isimli dosya sistemi "root" a "remount" edilecektir. Sonrasında "remount" işleminin sonucunu kontrol edelim. CSD: /home>mount | grep /dev/sda5 /dev/sda5 on / type ext4 (rw, relative, mand, ...) Tabii bu yöntem de geçicidir. Sistemin yeniden başlatılması durumunda varsayılan ayarlar tekrar devreye girecektir. Kalıcı hale getirmek için bazı "start-up" dosyaları üzerinde oynama yapmak gerekiyor. Örneğin, "/etc/fstab" dosyası bu amaçla elde edilmektedir. -> İlgili dosyanın "set-group-id" bayrağı "set" edilip, varsa gruptaki "x" hakkının kaldırılması gerekmektedir. Bunu, "chmod g+s,g-x test.txt" komutuyla gerçekleştirebiliriz. Yukarıdaki iki ön adımı da tamamladıktan sonra, aşağıdaki zorunlu kilitlemeyi test edebileceğimiz iki programlı bir örneği kullanabiliriz. Bu programlardan "fclock-test.c" olanı yukarıdakinin aynısıdır. "rw-test.c" ise bir dosyanın belli bir noktasından belli bir miktar okuma ya da yazma yapmaktadır. Bu programı, "./rw-test test.txt w 60 10" biçiminde kullanmak "'test.txt' dosyasının altmışıncı baytından itibaren, on baytlık bölümde, 'write' işlemi yapacağız" demektir. Benzer şekilde, ./rw-test test.txt r 0 64 kullanmak, "'test.txt' dosyasının sıfırıncı baytından itibaren, altmışdört baylık bölümde, 'read' işlemi yapacağız" demektir. * Örnek 1, /* fclock-test.c */ // ./flock-test #include #include #include #include #include #include #define MAX_CMDLINE 4096 #define MAX_ARGS 64 void parse_cmd(void); int get_cmd(struct flock *fl); void disp_flock(const struct flock *fl); void exit_sys(const char *msg); char g_cmd[MAX_CMDLINE]; int g_count; char *g_args[MAX_ARGS]; int main(int argc, char *argv[]) { int fd; pid_t pid; char *str; struct flock fl; int fcntl_cmd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } pid = getpid(); if ((fd = open(argv[1], O_RDWR)) == -1) exit_sys("open"); for (;;) { printf("CSD (%ld)>", (long)pid), fflush(stdout); fgets(g_cmd, MAX_CMDLINE, stdin); if ((str = strchr(g_cmd, '\n')) != NULL) *str = '\0'; parse_cmd(); if (g_count == 0) continue; if (g_count == 1 && !strcmp(g_args[0], "quit")) break; if (g_count != 4) { printf("invalid command!\n"); continue; } if ((fcntl_cmd = get_cmd(&fl)) == -1) { printf("invalid command!\n"); continue; } if (fcntl(fd, fcntl_cmd, &fl) == -1) if (errno == EACCES || errno == EAGAIN) printf("Locked failed!...\n"); else perror("fcntl"); if (fcntl_cmd == F_GETLK) disp_flock(&fl); } close(fd); return 0; } void parse_cmd(void) { char *str; g_count = 0; for (str = strtok(g_cmd, " \t"); str != NULL; str = strtok(NULL, " \t")) g_args[g_count++] = str; } int get_cmd(struct flock *fl) { int cmd, type; if (!strcmp(g_args[0], "F_SETLK")) cmd = F_SETLK; else if (!strcmp(g_args[0], "F_SETLKW")) cmd = F_SETLKW; else if (!strcmp(g_args[0], "F_GETLK")) cmd = F_GETLK; else return -1; if (!strcmp(g_args[1], "F_RDLCK")) type = F_RDLCK; else if (!strcmp(g_args[1], "F_WRLCK")) type = F_WRLCK; else if (!strcmp(g_args[1], "F_UNLCK")) type = F_UNLCK; else return -1; fl->l_type = type; fl->l_whence = SEEK_SET; fl->l_start = (off_t)strtol(g_args[2], NULL, 10); fl->l_len = (off_t)strtol(g_args[3], NULL, 10); return cmd; } void disp_flock(const struct flock *fl) { switch (fl->l_type) { case F_RDLCK: printf("Read Lock\n"); break; case F_WRLCK: printf("Write Lock\n"); break; case F_UNLCK: printf("Unlocked (can be locked)\n"); } printf("Whence: %d\n", fl->l_whence); printf("Start: %ld\n", (long)fl->l_start); printf("Length: %ld\n", (long)fl->l_len); if (fl->l_type != F_UNLCK) printf("Process Id: %ld\n", (long)fl->l_pid); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* rw-test.c */ // ./rw-test #include #include #include #include #include void exit_sys(const char *msg); /* ./rwtest */ int main(int argc, char *argv[]) { int fd; int operation; off_t offset; off_t len; char *buf; ssize_t result; if (argc != 5) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (strcmp(argv[2], "r") && strcmp(argv[2], "w")) { fprintf(stderr, "invalid operation!\n"); exit(EXIT_FAILURE); } offset = (off_t)strtol(argv[3], NULL, 10); len = (off_t)strtol(argv[4], NULL, 10); if ((buf = (char *)calloc(len, 1)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], argv[2][0] == 'r' ? O_RDONLY : O_WRONLY)) == -1) exit_sys("open"); lseek(fd, offset, SEEK_SET); if (argv[2][0] == 'r') { if ((result = read(fd, buf, len)) == -1) exit_sys("read"); printf("%ld bytes read\n", (long)result); } else { if ((result = write(fd, buf, len)) == -1) exit_sys("write"); printf("%ld bytes written\n", (long)result); } free(buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Eğer zorunlu("mandotary") kilitleme uygulanmış bir dosyayı açarken "O_NONBLOCK" bayrağını da eklersek, "open" fonksiyonu blokeye yol açmaz. Başarısızlıkla geri döner ve "errno" değişkeninin değeri "EAGAIN" ya çekilir. Öte yandan bir dosyanın bir proses tarafından zorunlu("mandotary") kilitlenmiş olması o dosyanın silinmesini de engellememekte, ancak "truncate" ve "ftruncate" fonksiyonları ile genişletilmesi veya budanması işlemlerini engellemektedir. Çünkü bu fonksiyonlar dosyaya yazma yapıyormuş gibi etki etmektedir. Benzer biçimde zorunlu("mandotary") kilitlenmiş dosyalar, "open" fonksiyonunda "O_TRUNC" modunda da açılamamaktadır. Diğer yandan dosya kilitlerini izlemek için "proc" dosya sistemindeki "locks" dosyasına göz atabiliriz. Örneğin, CSD: /home> cat /proc/locks 13: OFDLCK ADVISORY READ -1 00:06:6 0 EOF ^ ^ ^ ^ ^ ^ ^ ^ | | | | | | | | Kilitlenen alanın uzunluğunu belirtir. | | | | | | | Kilidin başlangıç "offset" noktasını belirtir. | | | | | | Kilitlenen dosyanın "inode" numarasını belirtir. | | | | | Soldan sağa, aygıtın "major" ve "minor" numaralarını belirtir. | | | | Kilidi koyan prosesin ID değeri. | | | Kilidin türünü belirtir. | | Kilidin isteğe bağlı mı zorunlu mu kilitlendiğini belirtir. | Kilidin hangi fonksiyonlarla konulduğunu belirtir. Son olarak "fcntl" fonksiyonunu sarmalayan, "lockf" isminde, bir POSIX fonksiyonu daha vardır. Fonksiyonun prototipi aşağıdaki gibidir. #include int lockf(int fildes, int function, off_t size); Fonksiyonun birinci parametresi ilgili dosyanın betimleyicisini, uygulanacak "lock" işleminin türünü, son parametre ise kilitlenecek alanın uzunluğunu belirtir. Başlangıç "offset" noktasının fonksiyona geçilmediğini DİKKAT ediniz. Dolayısıyla fonksiyon, dosya göstericisinin gösterdiği yerden itibaren, kilitleme yapar. İkinci parametreye ise şu sembolik sabitlerden birisi geçilir; -> "F_ULOCK" : Unlock locked sections, aka 'F_SETLK + F_UNLCK' in "fcntl" function. -> "F_LOCK" : Lock a section for exclusive use, aka 'F_SETLK + F_WRLCK' in "fcntl" function. -> "F_TLOCK" : Test and lock a section for exclusive use, aka 'F_SETLKW + F_WRLCK' in "fcntl" function. -> "F_TEST" : Test a section for locks by other processes, aka 'F_GETLK' in "fcntl" function. Eğer fonksiyonun son parametresine "0" geçilirse, bulunulan noktadan dosya sonuna ve ötesindeki eklemeleri de kapsayacak şekilde kilitleme yapılır. Benzer şekilde bu parametre negatif değer de girilebilir ki bu durumda bulunulan noktadan geriye doğru ilerlenir.