/* * Hizalama kuralları 3. dersteki gibi olmalıdır. * Komut, fonksiyon imzası gibi şeyler yeni bir satıra yazılmalı, öncesindeki ve devamındaki satırlar boş bırakılmalı ve * ilgili yazı bir "tab" karakteri kadar içeride olmalıdır. * Fonksiyon isimleri, dosya isimleri ve yabancı dildeki kelimeler tırnak işareti içerisinde yazılmalıdır. */ /*================================================================================================================================*/ (01_22_10_2022) /*================================================================================================================================*/ (02_23_10_2022) /*================================================================================================================================*/ (03_30_10_2022) > Linux Source Codes : https://elixir.bootlin.com/linux/latest/source > POSIX : https://pubs.opengroup.org/onlinepubs/9699919799/ > UNIX/Linux Sistemler, C programlarının derlenerek çalıştırılması. >> Dünyanın en yaygın C derleyicilerinden bir tanesi "gcc" derleyicisidir. GNU projesi kapsamında geliştirilmiş olup , "GNU C Compiler" şeklinde ismi açılabilir. >> MacOS sistemlerde ise varsayılan derleyici olarak "clang" derleyicisi kullanılmaktadır. >> UNIX/Linux sistemlerde "clang" derleyicisini yüklemek için aşağıdaki komutu kullanabiliriz; "sudo apt-get install clang" >> Gerek "gcc" gerek "clang" derleyicilerinde yapacağımız komut satırı işlemleri birbirinden farklılık göstermemektedir. >> "gcc" derleyicisini kullanarak bir C kodunun derlenmesi; >>> Herhangi bir editör kullanılarak bir C programı yazılır ve ".c" uzantısı ile kaydedilir. Örneğin, dosyamızın ismi "sample.c" olsun. >>> Daha sonra "shell" programı üzerinden, ilgili C programını yazdığımız dizinin içerisine gireriz ve aşağıdaki komutu çalıştırırız; "gcc sample.c" Bu işlem sonrasında ilgili dosyamızda herhangi bir hata yok ise ÖNCE DERLENİR, SONRASINDA DA BAĞLAMA işlemi yapılarak "executable" bir dosya elde edilir. İş bu dosyaya herhangi bir isim vermediğimiz için ismi varsayılan olarak "a.out" olur. >>> GNU sistemlerde derleme sonrası oluşturulan "object file", başarılı bir bağlama aşamasından sonra silinir. >>> GNU tip sistemlerde "executable" dosyalar için özel bir dosya uzantısı mevcut değildir. Sadece biz isim vermediğimiz zaman oluşturulan "a.out" isimli dosya için ".out" uzantısı eklenir. >>> Oluşturulan "executable" dosyaya isim vermek için, yukarıda yazdığımız "shell" komutuna aşağıdaki seçeneği ilave etmeliyiz; "-o file_name" Böylelikle "sample.c" isimli dosyayı derlemek ve çalıştırılabilir dosyanın da ismini "my_file_name" yapmak istiyorsak aşağıdaki komutu "shell" programında çalıştırmamız gerekiyor; "gcc sample.c -o my_file_name" GENEL OLARAK OLUŞTURULAN "executable" DOSYALAR İÇİN BİR UZANTI GİRİLMESİ TAVSİYE EDİLMEZ. >>> Windows sistemlerde ortaya çıkan "executable" dosyayı çalıştırmak için dosyanın adını direkt olarak "shell" terminaline yazmamız yeterliyken, GNU sistemlerde ise önce "./" ifadesini devamında ise ilgili dosyanın adını yazmamız gerekiyor. Dolayısıyla GNU sistemlerde "sample" isimli bir dosyayı çalıştırmak için aşağıdaki komutu "shell" programından çalıştırmalıyız; "./sample" >>> "gcc" derleyicisindeki bazı seçenekler; >>>> "-Wall" seçeneği, derleyicinin bütün uyarıları vermesine olanak sağlar. >>> 64-bit sistemlerde 32-bit derleme yapabilmek için, öncelikle aşağıdaki komutu "shell" programına girerek bir paket yüklemesi yapmamız gerekmektedir; "sudo apt-get install g++-multilib libc6-dev-i386" Devamında ise "-m32" seçeneceğini yazacağımız komuta eklememiz gerekmektedir. Böylelikle komutumuzun son hali aşağıdaki gibi olacaktır; "gcc sample.c -o my_file_name -m32" Böylelikle, ortaya çıkan "executable" dosya 32-bit bir dosya olacaktır. >>> Derleme sonrasında Sembolik Makine Dili çıktısı da görmek için "-S" seçeneği kullanılır. Bu durumda ek olarak ortaya ".s" bir dosya daha çıkacaktır. İlaveten bu seçenek ile birlikte "-masm=intel" seçeneği de kullanılsın ki ortaya çıkan dosyanın dili daha anlaşılır olsun. >> "shell" programındaki bazı komutlar; >>> "-c" seçeneği, ilgili dosyanın sadece derlenmesi için kullanılmalıdır. Bu seçenek girildiğinde BAĞLAMA İŞLEMİ YAPILMAYACAKTIR. Bu işlem sonucunda, GNU sistemlerde meydana getirilen dosyanın uzantısı ".o" şeklinde fakat Windows sistemlerinde bu dosyalar ".obj" uzantısına sahiptirler. >>> "pwd" komutu, "print working directory" manasına gelmekte olup, o anki çalışma dizinini ekrana yazdıracaktır. >>> "ls" komutu, o an bulunduğumuz dizindeki dosyaların isimlerini ekrana yazdıracaktır. >>> "ls -l" komutu, o an bulunduğumuz dizindeki dosyaların özelliklerini ekrana yazdıracaktır. >> Windows işletim sistemi üzerinde koşarken, "bash" uygulamasını çalıştırdığımız zaman o anki çalışma dizinimiz aşağıdaki gibi olacaktır; "ahmopasha@DESKTOP-OHF04Q3:/mnt/c/Windows/system32$" Bu noktada "cd .." komutunu tekrar tekrar kullanarak, dizinimizi aşağıdaki hale getirmeliyiz; "ahmopasha@DESKTOP-OHF04Q3:/mnt/c" Daha sonra sırasıyla "cd Users", "cd ahmet" ve "cd Desktop" komutlarını kullanarak dizinimizi aşağıdaki hale getirmeliyiz; "ahmopasha@DESKTOP-OHF04Q3:/mnt/c/Users/ahmet/Desktop$" Artık Windows işletim sistemindeki masaüstündeyiz. >> "mkdir folder_name" komutu ile "folder_name" isminde yeni bir dizin oluştururuz. >> "touch file_name" komutu ile "file_name" isminde yeni bir dosya oluşturuyoruz. /*================================================================================================================================*/ (04_05_11_2022) > GNU stili komut satırı argümanları: >> Bir takım belirlemeleri de gerçekleştirmek için "-" karakteri ve bir harf karakterini kullanıyoruz. Bunlara da genel olarak "option" denmektedir. Örneğin, "ls" komutu ilgili dizindeki dosyaların isimlerini ekrana basmaktadır. Bu komutu "-l" seçeneği ile kullanırsak ilgili dosyaların detaylarını da ekrana yazdıracaktır (bkz. "ls -l"). >> Ekseriyet ile bu komutlar birer programlardır da. >> GNU stili komut satırı argümanlarını üç ana başlıkta incelememiz mümkündür. Bunlar, >>> Argümansız Seçenekler (Options without an Argument) : "ls -l" komutunu ele alırsak, buradaki "-l" seçeneği argümansız bir seçenektir. >>> Argumanlı Seçenekler (Options with an Argument) : İş bu seçeneğin yanına ek olarak ekstra bir argüman geçmemiz gereken seçeneklerdir. Örneğin, "gcc sample.c -o my_file_name" komutundaki "-o" argümanlı bir seçenek olup, "my_file_name" ise bunun argümanıdır. Argümanlı bir seçenek girdiğimiz zaman devamında yazacaklarımız onun argümanını teşkil etmektedir. >>> Seçeneksiz Argümanlar (Arguments without an Option) : Hiç bir seçeneğe ait olmayan argümanlardır. >>> Özetle, "gcc sample.c -o my_file_name -S" komutundaki, "sample.c" Seçeneksiz Argüman, "-o" Argümanlı Seçeneği ve "-S" ise Argümansız Seçeneği belirtmektedir. Buradaki "my_file_name" ise "-o" seçeneğine ait bir argüman olarak değerlendirilecektir. >> Bazı özel durumlar haricinde, seçenekler arasındaki sıranın bir önemi yoktur. Örneğin, aşağıdaki her iki komut da aynı çıktıyı üretecektir; "ls -l -i" "ls -i -l" >> Yine GNU stilinde seçenekler "Case-Sensitive" bir özelliğe sahiptirler. >> Birden fazla argümansız seçeneği, aralarında boşluk ve "-" karakteri olmadan, yazmamız mümkündür. Örneğin, aşağıdaki komutlar aynı çıktıyı üretecektir; "ls -l -i" "ls -i -l" "ls -il" "ls -li" >> Fakat argümanlı seçenekler ile argümansız seçenekleri birleştirerek yazmaktan kaçınmalıyız. Her ne kadar programlar bunu kabul ediyor olsalar da iyi bir teknik olduğu söylenemez. Örneğin, "./sample -a -b ali" "./sample -ab ali" >> Belirli bir dönemden sonra Uzun Seçenek terimi de bu stile dahil edildi. Birden fazla karakter içeren seçenekler Uzun Seçenek olarak geçmektedir. Bu tip seçenekleri kullanmak için "-" karakteri yerine "--" karakterini kullanmalıyız. Örneğin, "--ali" seçeneğinde "ali" seçeneğin kendi ismidir. Uzun Seçenekler ise kendi bünyesinde üçe ayrılmaktadır. Bunlar, >>> Argümansız Seçenekler (Options without an Argument) : "./sample --wrap" komutunu ele aldığımızda, buradaki "--wrap" seçeneği argümansız seçenektir. >>> Argumanlı Seçenekler (Options with an Argument) : "./sample --size 100" komutundaki "--size 100" ikilisi ise argümanlı seçeneği işaret etmektedir. >>> İsteğe Bağlı Argüman İçeren Uzun Seçenek (Options with an Optional Argument): "./sample --myWrap=100" komutundaki "--myWrap=100" ise iş bu seçeneği işaret etmektedir. İsteğe bağlı olduğu için, "./sample --myWrap" şeklinde de kullanabiliriz. Eğer bu şekil yerine "--myWrap 100" yazarsak Argümanlı Seçenek gibi algılanacaktır. >>>> Ayrıca bir uzun seçeneği "=" karakterini kullanarak da yazabiliriz. Örneğin, aşağıdaki komutlar aynı çıktıyı üretecektir; "head --lines 3 sample.c" "head --lines=3 sample.c" Fakat İsteğe Bağlı Argüman İçeren Uzun Seçeneklere argüman geçeceksek, "=" KULLANMAK ZORUNDAYIZ. >> Artık yer yer kısa seçenek ve uzun seçenek birbirinin alternatifi haline gelmiştir. Örneğin, "-m" seçeneği ile "--mode" seçeneği aynı şeyi yapmaktadır. >> İsteğe Bağlı Argüman İçeren Kısa Seçenek YOKTUR. Fakat kendi programlarımızda böyle bir uygulamaya gidebiliriz. >> Ama kısa seçeneği ama uzun seçeneği, varsa, argümanı izlemek zorundadır. >> Argümanlı kısa seçeneği, argümanı ile kombine bir şekilde yazabiliriz. Örneğin, aşağıdaki iki komut da aynı çıktıyı verecektir; "gcc sample.c -o my_file_name" "gcc sample.c -omy_file_name" Fakat bu kurala uymayanlarda da vardır. >> Aynı seçenekleri, ama argümanlı ama argümansız, tek bir komut içerisinde girmek herhangi bir hata mesajının ekrana yazılmasına neden olmaz. Çünkü GNU stilinde buna karşın bir kontrol mekanizması yazılmamıştır. > Komut satırı argümanlarının ayrıştırılması (bkz. "parsing"): >> Komut satırı argümanlarının ekrana bastırılması, * Örnek 1, programa geçilen komut satırı argümanlarının ekrana yazdırılması: #include "stdio.h" #include "unistd.h" int main(int argc, char **argv) { /* # INPUT # ./ex_01 -a -b --ceyhan denizli */ /* # OUTPUT # ./ex_01 -a -b --ceyhan denizli */ for (size_t i = 0; i < argc; i++) { puts(argv[i]); } } >> GNU stili bir ayrıştırma yapmak için iki adet "getopt" ve "getoptlong" POSIX fonksiyonu kullanılmaktadır. Burada "getoptlong" fonksiyonu, "getopt" fonksiyonunu KAPSAMAKTADIR. >>> "getopt" fonksiyonu, aşağıdaki parametrik yapıya sahiptir; #include int getopt(int argc, char * const argv[], const char *optstring); Fonksiyonun birinci ve ikinci parametreleri, "main" fonksiyonuna geçilen parametrelerdir. Üçüncü parametre olan "optstring" ise bizim belirlediğimiz seçeneklerin oluşturduğu bir karakter dizisidir. Bu diziye yazacağımız karakterlerin sıralamasının bir önemi yoktur fakat eşleştirme sırasında bu sıranın gözetilmesi tavsiye edilmektedir. Ek olarak "unistd.h" başlık dosyası bir çok önemli POSIX fonksiyonunun imzasını barındırmaktadır. Fakat unutmamak gerekir ki bu başlık dosyası standart C kütüphanesi değil, bir POSIX kütüphanesidir. İş bu fonksiyonun geri dönüş değeri, bulmuş olduğu ilk argümandır fakat "int" türündendir. Fakat Argümansız Seçenekler gördüğünde geriye o karakterin "int" halini, bütün argümanlar "parse" edildiğinde ise geriye "-1" değerini döndürmektedir. Bu durumda, her çağırmada bulunan bir seçenek döndürülüyorsa, bir "while" döngüsü içerisinde bu fonksiyonu kullanabiliriz. * Örnek 1, "getopt()" fonksiyonunun çağrılması: #include "stdio.h" #include "unistd.h" int main(int argc, char **argv) { /* # INPUT # ./ex_01 -a -b -c Ahmo -d -e Memo */ /* # OUTPUT # -a option is given. -b option is given. -c option is given. -d option is given. -e option is given. */ int result; /* * @param a, Argümansız Seçenek olan "-a" yı temsil etmektedir. * @param b, Argümansız Seçenek olan "-b" yi temsil etmektedir. * @param c, Argümanlı Seçenek olan "-c" yi temsil etmektedir. * @param d, Argümansız Seçenek olan "-d" yi temsil etmektedir. * @param e, Argümanlı Seçenek olan "-e" yi temsil etmektedir. */ const char* myArgumentList = "abc:de:"; while ((result = getopt(argc, argv, myArgumentList)) != -1) { switch (result) { case 'a': fprintf(stdout, "-a option is given.\n"); break; case 'b': fprintf(stdout, "-b option is given.\n"); break; case 'c': fprintf(stdout, "-c option is given.\n"); break; case 'd': fprintf(stdout, "-d option is given.\n"); break; case 'e': fprintf(stdout, "-e option is given.\n"); break; } } } İş bu fonksiyon, Seçeneksiz Argümanları, ilgili "buffer" ın sonuna ötelemektedir. İş bu "buffer", fonksiyonun gövdesinde hayata gelmektedir. "optind" isimli "int" türden değişken ise iş bu Seçeneksiz Argümanların, öteleme işleminden sonraki, başladığı yeri göstermektedir. Bu "optind" isimli değişken de yine "unistd.h" isimli başlık dosyasında bildirilmiştir. Fakat öteleme işlemi sonrasında iş bu Seçeneksiz Argümanların kendi içindeki sıralaması, bizim girdiğimiz sıralamaya göre olacağı GARANTİ DEĞİLDİR. * Örnek 1, #include "stdio.h" #include "unistd.h" int main(int argc, char **argv) { /* # INPUT # ./ex_01 -a -b -c Ahmo -d -e Memo TheOptionWithoutAnArgument */ /* # OUTPUT # -a option is given. -b option is given. -c option is given. -d option is given. -e option is given. List of Options without an Argument: TheOptionWithoutAnArgument */ /* * @param a, Argümansız Seçenek olan "-a" yı temsil etmektedir. * @param b, Argümansız Seçenek olan "-b" yi temsil etmektedir. * @param c, Argümanlı Seçenek olan "-c" yi temsil etmektedir. * @param d, Argümansız Seçenek olan "-d" yi temsil etmektedir. * @param e, Argümanlı Seçenek olan "-e" yi temsil etmektedir. */ const char* myArgumentList = "abc:de:"; int result; while ((result = getopt(argc, argv, myArgumentList)) != -1) { switch (result) { case 'a': fprintf(stdout, "-a option is given.\n"); break; case 'b': fprintf(stdout, "-b option is given.\n"); break; case 'c': fprintf(stdout, "-c option is given.\n"); break; case 'd': fprintf(stdout, "-d option is given.\n"); break; case 'e': fprintf(stdout, "-e option is given.\n"); break; } } /* * Buradaki "optind" isimli değişken, Seçeneksiz Argümanların "buffer" * içerisindeki indisini belirtmektedir. "argc" ise toplam eleman sayısıdır. */ fprintf(stdout,"List of Options without an Argument:\n"); for (int i = optind; i < argc; i++) { puts(argv[i]); } } Argümanlı Seçeneklerde ise devreye "optarg" isimli gösterici girmektedir. İş bu gösterici yine "unistd.h" isimli başlık dosyasında bildirilmiş olup, "getopt" fonksiyonu ile böyle bir seçeneğe denk geldiğinizde, ilgili göstericimiz, bu seçeneğin argümanını gösterir hale gelmektedir. Eğer "getopt" başka bir argümanlı seçenek yakalarsa, bu sefer "optarg" isimli göstericimiz onun argümanını gösterecektir. "optarg" isimli göstericinin gösterdiği yer "static" bir alan olduğundan, "strcpy" isimli fonksiyonu çağırmamıza gerek yoktur. * Örnek 1, Argümanlı Seçeneklerde ilgili seçeneğe ait argümanın tutulması: #include "stdio.h" #include "unistd.h" int main(int argc, char **argv) { /* # INPUT # ./ex_01 -a -b -c Ahmo -d -e Memo TheOptionWithoutAnArgument */ /* # OUTPUT # -a option is given. -b option is given. -c option is given. -d option is given. -e option is given. List of Options without an Argument: TheOptionWithoutAnArgument List of Arguments that belong to Options: Argument [Ahmo] belongs to the option -c Argument [Memo] belongs to the option -e */ /* * @param a, Argümansız Seçenek olan "-a" yı temsil etmektedir. * @param b, Argümansız Seçenek olan "-b" yi temsil etmektedir. * @param c, Argümanlı Seçenek olan "-c" yi temsil etmektedir. * @param d, Argümansız Seçenek olan "-d" yi temsil etmektedir. * @param e, Argümanlı Seçenek olan "-e" yi temsil etmektedir. */ const char* myArgumentList = "abc:de:"; const char* c_arg, *e_arg; int result; while ((result = getopt(argc, argv, myArgumentList)) != -1) { switch (result) { case 'a': fprintf(stdout, "-a option is given.\n"); break; case 'b': fprintf(stdout, "-b option is given.\n"); break; case 'c': fprintf(stdout, "-c option is given.\n"); c_arg = optarg; break; case 'd': fprintf(stdout, "-d option is given.\n"); break; case 'e': fprintf(stdout, "-e option is given.\n"); e_arg = optarg; break; } } fprintf(stdout,"List of Options without an Argument:\n"); for (int i = optind; i < argc; i++) { puts(argv[i]); } fprintf(stdout, "List of Arguments that belong to Options:\n"); fprintf(stdout, "Argument [%s] belongs to the option -c\n", c_arg); fprintf(stdout, "Argument [%s] belongs to the option -e\n", e_arg); } Bütün eşleme işlemleri tamamlandıktan sonra, yapacağımız şeyleri ilgili "while" döngüsü dışında yapmamız tavsiye olunur. Bunu gerçekleştirmek için de her bir seçeneğe karşılık gelen birer "flag" tanımlamamız yeterlidir. * Örnek 1, #include "stdio.h" #include "unistd.h" int main(int argc, char **argv) { /* # INPUT # ./ex_01 -a -b -c Ahmo -d -e Memo TheOptionWithoutAnArgument */ /* # OUTPUT # -a option is given. -b option is given. -c option is given with an argument [Ahmo] -d option is given. -e option is given with an argument [Memo]. List of Options without an Argument: TheOptionWithoutAnArgument */ /* * @param a, Argümansız Seçenek olan "-a" yı temsil etmektedir. * @param b, Argümansız Seçenek olan "-b" yi temsil etmektedir. * @param c, Argümanlı Seçenek olan "-c" yi temsil etmektedir. * @param d, Argümansız Seçenek olan "-d" yi temsil etmektedir. * @param e, Argümanlı Seçenek olan "-e" yi temsil etmektedir. */ const char* myArgumentList = "abc:de:"; // Harflerin sırasını unutmayalım. const char* c_arg, *e_arg; // Seçeneklerimizin sıralamasını unutmayalım. int a_flag, b_flag, c_flag, d_flag, e_flag; // Seçeneklerimizin sıralamasını unutmayalım. a_flag = b_flag = c_flag = d_flag = e_flag = 0; int result; while ((result = getopt(argc, argv, myArgumentList)) != -1) { switch (result) { // YİNE SEÇENEKLERİMİZİN SIRALAMASINI GÖZ ÖNÜNDE BULUNDURDUK. case 'a': a_flag = 1; break; case 'b': b_flag = 1; break; case 'c': c_flag = 1; c_arg = optarg; break; case 'd': d_flag = 1; break; case 'e': e_flag = 1; e_arg = optarg; break; } } // YİNE SEÇENEKLERİMİZİN SIRALAMASINI GÖZ ÖNÜNDE BULUNDURDUK. if (a_flag) { fprintf(stdout, "-a option is given.\n"); } if (b_flag) { fprintf(stdout, "-b option is given.\n"); } if (c_flag) { fprintf(stdout, "-c option is given with an argument [%s]\n", c_arg); } if (d_flag) { fprintf(stdout, "-d option is given.\n"); } if (e_flag) { fprintf(stdout, "-e option is given with an argument [%s].\n", e_arg); } fprintf(stdout,"List of Options without an Argument:\n"); for (int i = optind; i < argc; i++) { puts(argv[i]); } } Geçersiz bir seçenek ya da argümanı girilmeyen argümanlı seçenek girildiğinde "stderr" akımına hata mesajı yazarlar. Fakat bu hata mesajının işlenmesini bizler üzerimize almak istiyorsak "opterr" isimli değişkenin değerini sıfıra çekmemiz gerekiyor. Yine bu değişken de "unistd.h" isimli başlık dosyasında bildirilmiştir. Eğer bizler de unutursak herhangi bir mesaj yazılmayacaktır. "getopt" fonksiyonu ise iş bu durumda "?" karakterini döndürür. İş bu mesajı düzenlemek için de "optopt" isimli göstericiyi kullanacağız. Sorun çıkartan hangi seçenek ise o turda bu gösterici onu gösterecektir. * Örnek 1, hata mesajının bizler tarafından ele alınması: #include "stdio.h" #include "unistd.h" int main(int argc, char **argv) { /* # INPUT # ./ex_01 -a -b -f -c */ /* # OUTPUT # [-f] is an invalid option! [-c] must have an argument! -a option is given. -b option is given. */ /* * @param a, Argümansız Seçenek olan "-a" yı temsil etmektedir. * @param b, Argümansız Seçenek olan "-b" yi temsil etmektedir. * @param c, Argümanlı Seçenek olan "-c" yi temsil etmektedir. * @param d, Argümansız Seçenek olan "-d" yi temsil etmektedir. * @param e, Argümanlı Seçenek olan "-e" yi temsil etmektedir. */ const char* myArgumentList = "abc:de:"; // Harflerin sırasını unutmayalım. const char* c_arg, *e_arg; // Seçeneklerimizin sıralamasını göz önünde bulundurduk. int a_flag, b_flag, c_flag, d_flag, e_flag; // Seçeneklerimizin sıralamasını göz önünde bulundurduk. a_flag = b_flag = c_flag = d_flag = e_flag = 0; opterr = 0; // Geçersiz girişlerde hata mesajının stili bize bağlıdır. int result; while ((result = getopt(argc, argv, myArgumentList)) != -1) { switch (result) { // YİNE SEÇENEKLERİMİZİN SIRALAMASINI GÖZ ÖNÜNDE BULUNDURDUK. case 'a': a_flag = 1; break; case 'b': b_flag = 1; break; case 'c': c_flag = 1; c_arg = optarg; break; case 'd': d_flag = 1; break; case 'e': e_flag = 1; e_arg = optarg; break; case '?': if (optopt == 'c' || optopt == 'e') fprintf(stderr, "[-%c] must have an argument!\n", optopt); else fprintf(stderr, "[-%c] is an invalid option!\n", optopt); break; } } // YİNE SEÇENEKLERİMİZİN SIRALAMASINI GÖZ ÖNÜNDE BULUNDURDUK. if (a_flag) { fprintf(stdout, "-a option is given.\n"); } if (b_flag) { fprintf(stdout, "-b option is given.\n"); } if (c_flag) { fprintf(stdout, "-c option is given with an argument [%s]\n", c_arg); } if (d_flag) { fprintf(stdout, "-d option is given.\n"); } if (e_flag) { fprintf(stdout, "-e option is given with an argument [%s].\n", e_arg); } /* * "optind", Seçeneksiz Argümanların indis bilgisini tutmaktadır. "argc" ise ilgili * "buffer" alanının toplam eleman sayısını. Bu ikisinin birbirine eşit olmaması, * en az bir Seçeneksiz Argüman olduğunu göstermektedir. Eğer her ikisi de eşitse * programın akışı hem "if" hem "for" gövdesine girmeyecektir. */ if (optind != argc) fprintf(stdout,"List of Options without an Argument:\n"); for (int i = optind; i < argc; i++) { puts(argv[i]); } } Bir hata ile karşılaşıldığında programın "parsing" işleminin tamamlanmasının ardından sonlandırılması gerekiyor. Geçerli seçeneklerin işlenmesi TAVSİYE EDİLMEZ. * Örnek 1, AŞAĞIDAKİ KALIP DİĞER PROGRAMLARDA KULLANMAK İÇİN UYGUNDUR. #include "stdio.h" #include "unistd.h" #include "stdlib.h" int main(int argc, char **argv) { /* # INPUT # ./ex_01 -a -b -f -c */ /* # OUTPUT # [-f] is an invalid option! [-c] must have an argument! */ /* * @param a, Argümansız Seçenek olan "-a" yı temsil etmektedir. * @param b, Argümansız Seçenek olan "-b" yi temsil etmektedir. * @param c, Argümanlı Seçenek olan "-c" yi temsil etmektedir. * @param d, Argümansız Seçenek olan "-d" yi temsil etmektedir. * @param e, Argümanlı Seçenek olan "-e" yi temsil etmektedir. */ const char* myArgumentList = "abc:de:"; // Harflerin sırasını unutmayalım. const char* c_arg, *e_arg; // Seçeneklerimizin sıralamasını göz önünde bulundurduk. int a_flag, b_flag, c_flag, d_flag, e_flag, err_flag; // Seçeneklerimizin sıralamasını göz önünde bulundurduk. a_flag = b_flag = c_flag = d_flag = e_flag = err_flag = 0; opterr = 0; // Geçersiz girişlerde hata mesajının stili bize bağlıdır. int result; while ((result = getopt(argc, argv, myArgumentList)) != -1) { switch (result) { // YİNE SEÇENEKLERİMİZİN SIRALAMASINI GÖZ ÖNÜNDE BULUNDURDUK. case 'a': a_flag = 1; break; case 'b': b_flag = 1; break; case 'c': c_flag = 1; c_arg = optarg; break; case 'd': d_flag = 1; break; case 'e': e_flag = 1; e_arg = optarg; break; case '?': if (optopt == 'c' || optopt == 'e') fprintf(stderr, "[-%c] must have an argument!\n", optopt); else fprintf(stderr, "[-%c] is an invalid option!\n", optopt); err_flag = 1; } } /* * Seçenekleri ayırt ettikten sonra gördük ki bir hata durumu var. Artık bu aşamadan * sonra ilerlemek TAVSİYE EDİLMEZ. Yani her şey hatasız olmalıdır. */ if (err_flag) { exit(EXIT_FAILURE); } // YİNE SEÇENEKLERİMİZİN SIRALAMASINI GÖZ ÖNÜNDE BULUNDURDUK. if (a_flag) { fprintf(stdout, "-a option is given.\n"); } if (b_flag) { fprintf(stdout, "-b option is given.\n"); } if (c_flag) { fprintf(stdout, "-c option is given with an argument [%s]\n", c_arg); } if (d_flag) { fprintf(stdout, "-d option is given.\n"); } if (e_flag) { fprintf(stdout, "-e option is given with an argument [%s].\n", e_arg); } if (optind != argc) fprintf(stdout,"List of Options without an Argument:\n"); for (int i = optind; i < argc; i++) { puts(argv[i]); } } Pekiştirici bir örnek: Sırasıyla "-a", "-m", "-d" ve "-s" seçenekleri için toplama, çarpma, bölme ve çıkarma işlemlerini gerçekleştiren bir program yazalım. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "unistd.h" int main(int argc, char** argv) { /* # INPUT # i. ./ex_01 -a 10 20 ii. ./ex_01 -m 10 20 iii. ./ex_01 -d 10 20 iv. ./ex_01 -s 10 20 v. ./ex_01 -m 10 20 -M result */ /* # OUTPUT # i. 30.000000 ii. 200.000000 iii. 0.500000 iv. -10.000000 v. result: 200.000000 */ int a_flag, m_flag, d_flag, s_flag, M_flag, err_flag; a_flag = m_flag = d_flag = s_flag = M_flag = err_flag = 0; const char* M_arg; opterr = 0; int result; while ((result = getopt(argc, argv, "amdsM:")) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'm': m_flag = 1; break; case 'd': d_flag = 1; break; case 's': s_flag = 1; break; case 'M': M_flag = 1; M_arg = optarg; break; case '?': err_flag = 1; if (optopt == 'M') fprintf(stderr, "[-M] has no argument!\n"); else fprintf(stderr, "[-%c] is an invalid option!\n", optopt); } } if (err_flag) { exit(EXIT_FAILURE); } if (a_flag + m_flag + d_flag + s_flag == 0) { fprintf(stderr, "At least one -[amds] option must be passed.!\n"); exit(EXIT_FAILURE); } if (a_flag + m_flag + d_flag + s_flag > 1) { fprintf(stderr, "Only one option must be passed!\n"); exit(EXIT_FAILURE); } if (argc - optind != 2) { fprintf(stderr, "Two number must be passed as argument!\n"); exit(EXIT_FAILURE); } double argOne, argTwo; argOne = atof(argv[optind]); argTwo = atof(argv[optind + 1]); double calc_result; if (a_flag) { calc_result = argOne + argTwo; } else if (m_flag) { calc_result = argOne * argTwo; } else if (d_flag) { calc_result = argOne / argTwo; } else { calc_result = argOne - argTwo; } if (M_flag) { fprintf(stdout, "%s: %f", M_arg, calc_result); } else { fprintf(stdout, "%f\n", calc_result); } } > Hatırlatıcı notlar; >> POSIX standartlarına göre, POSIX kütüphanelerindeki global değişken ya da fonksiyon isimlerini kullanarak başka bir değişken ya da fonksiyon oluşturmanız "Tanımsız Davranış" meydana getirir. Bu tip isimlerin hangi isimler olduğunu bilmek bizim sorumluluğumuzdadır. >> C standartlarında ise ilk karakteri "_" olan ve ikinci karakteri büyük harfle başlayanlar ile ilk iki karakteri "__" olan değişken isimleri rezerve edilmiş isimlerdir. Böyle isimlerini kullanılması yine "Tanımsız Davranış" meydana getirir. İş bu sebepten ötürü C dilinde kod yazarken "__" ile başlayan ve ilk karakteri "_" olup ikinci karakteri büyük harf olan değişken ismi kullanmamalıyız. /*================================================================================================================================*/ (05_06_11_2022) > Komut satırı argümanlarının ayrıştırılması (bkz. "parsing") (devam): >> GNU stili bir ayrıştırma yapmak için iki adet "getopt" ve "getoptlong" POSIX fonksiyonu bulunduğunu ve "getoptlong" fonksiyonunun, "getopt" fonksiyonunu kapsadığını daha önce açıklamıştık. >>> "getopt_long" fonksiyonu, aşağıdaki parametrik yapıya sahiptir; #include int getopt_long( int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex ); Fonksiyonun ilk üç parametresi "getopt" fonksiyonunun parametresiyle birebir aynıdır. Dördüncü parametre ise elemanları "struct option" türden olan bir dizinin başlangıç adresidir ve bu dizinin son elemanı olan yapı nesnesinin bütün elemanları NULL değerinde olmalıdır. Bu yapı ise aşağıdaki biçimdedir; #include struct option { const char *name; int has_arg; int *flag; int val; }; -> "name", "const char*" türünden bir değişken olup, ilgili uzun seçeneğin adını tutmaktadır. -> "has_arg", "int" türden bir değişken olup, argüman bilgisini tutmaktadır. Burada "no_argument", "required_argument" ve "optional_argumen" isimli sembolik sabitler kullanabiliriz. Bu isimler sırasıyla ilgili seçeneğin argümansız, argümanlı ya da isteğe bağlı argümanlı olduğunu belirtmektedir. -> "flag", "int*" türden bir değişken olup, uzun seçeneğin bulunması durumunda, bizim belirlediğimiz rakamı iş bu adrese yazmak için kullanılmaktadır. NULL geçilmesi durumunda bu mekanizma devre dışı kalacaktır. -> "val", "int" türden bir değişken olup, uzun seçeneğin bulunması durumunda, iş bu "getopt_long" fonksiyonunun ne ile döndüreceğini belirtmek için kullanılır. Eğer üçüncü parametreye bir adres bilgisi geçersek, iş bu değer, üçüncü parametreye geçilen adrese yazılacaktır. Açıkçası bu dördüncü parametre pek kullanılmaz, dolayısıyla fonksiyonun bu parametresine NULL değerini geçebiliriz. Öte yandan, aynı anda ya "getopt" ya da "getopt_long" kullanamayız. Her ikisini de kullandığımız zaman bir karışma söz konusu olacaktır. İş bu "getopt_long" fonksiyonunun kullanılması, "getopt" fonksiyonuna nazaran daha karmaşık olduğu için, uzun seçenek kullanmadığımız zamanlarda direkt "getopt" fonksiyonunu kullanabiliriz. Çünkü aslında kısa seçenekler söz konusu olduğunda her iki fonksiyon da aynı sonucu üretecektir. Fonksiyonun beşinci parametresine ilerleyen vakitlerde değinilecektir. * Örnek 1, "getopt_long" fonksiyonunu, "getopt" fonksiyonu gibi kullanmak: #include "stdio.h" #include "stdlib.h" #include "unistd.h" #include "getopt.h" int main(int argc, char** argv) { /* # INPUT # */ /* # OUTPUT # */ // Aşağıdaki kullanım şekilleri aynı sonucu üretecektir; //getopt(argc, argv, "ab:"); getopt_long(argc, argv, "ab:"); } "getopt" fonksiyonunu çağırma şeklimiz, iş bu fonksiyon için de geçerlidir. "-1" değeri dönene kadar iş bu fonksiyon çağrılacaktır. Kısa seçenek buldu ise o kısa seçeneğin kendisini, uzun seçenek buldu ise "struct option" türünden değişkenin dördüncü parametresini döndürecektir. Burada "struct option" türden yapı nesnesinin üçüncü parametresine NULL geçilmiştir. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "unistd.h" #include "getopt.h" int main(int argc, char** argv) { /* # INPUT # ./ex_01 -a -b TheArgumentOfb --size --length 2000 --number=31 */ /* # OUTPUT # -a option was given. -b option was given with an argument of [TheArgumentOfb]. --size option was given. --length option was given with an argument of [2000]. --number option was given with an argument of [31]. */ /**type "struct option": * @param name, "const char*" türünden bir değişken olup, ilgili uzun seçeneğin adını tutmaktadır. * @param has_arg, "int" türden bir değişken olup, argüman bilgisini tutmaktadır. Burada no_argument", * "required_argument" ve "optional_argumen" isimli sembolik sabitler kullanabiliriz. Bu isimler sırasıyla * argümansız, argümanlı ya da isteğe bağlı argümanlı olduğunu belirtmektedir. * @param flag, "int*" türden bir değişken olup, uzun seçeneğin bulunması durumunda, bizim belirlediğimiz * rakamı iş bu adrese yazmak için kullanılmaktadır. NULL geçilmesi durumunda bu mekanizma devre dışı kalacaktır. * @param val, "int" türden bir değişken olup, uzun seçeneğin bulunması durumunda, iş bu "getopt_long" fonksiyonunun * ne ile döneceğini belirtmek için kullanılır. Eğer üçüncü parametreye bir adres bilgisi de geçersek, iş bu değer, * üçüncü parametreye geçilen adrese yazılacaktır. ****************************************************************************************************************** * Aşağıdaki "options" dizisinin son elemanı olan yapı nesnesinin bütün elemanları NULL değerinde olmalıdır. */ struct option options[] = { {"size", no_argument, NULL, 1}, {"length", required_argument, NULL, 2}, {"number", optional_argument, NULL, 3}, {0, 0, 0, 0} }; char* b_arg, *length_arg, *number_arg = NULL; opterr = 0; int result; while ((result = getopt_long(argc, argv, "ab:", options, NULL)) != -1) { switch (result) { case 'a': fprintf(stdout, "-a option was given.\n"); break; case 'b': b_arg = optarg; fprintf(stdout, "-b option was given with an argument of [%s].\n", b_arg); break; case 1: fprintf(stdout, "--size option was given.\n"); break; case 2: length_arg = optarg; fprintf(stdout, "--length option was given with an argument of [%s].\n", length_arg); break; case 3: number_arg = optarg; fprintf(stdout, "--number option was given with an argument of [%s].\n", number_arg); break; } } } İş bu fonksiyonu kullanırken aşağıdaki hata durumlarında ilgili "getopt_long" fonksiyonu "?" karakteri döndürmektedir. Bu hata durumları, >>>> Olmayan bir kısa seçeneğin girilmesi durumunda "optopt", olmayan bu kısa seçeneği bize vermektedir. >>>> Olmayan bir uzun seçeneğin girilmesi durumunda "optopt" değeri sıfıra çekiliyor. Fakat girilen bu seçeneğin ne olduğunu bize verememektedir. >>>> Argümanlı bir kısa seçeneğin argümanının girilmemiş olması durumunda "optopt", olmayan bu kısa seçeneği bize vermektedir. >>>> Argümanlı bir uzun seçeneğin argümanının girilmemiş olması durumunda ise "struct option" yapı nesnesinin dördüncü veri elemanını bize döndürecektir. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "unistd.h" #include "getopt.h" int main(int argc, char** argv) { /* # INPUT # ./ex_01 -a -b TheArgumentOfb --all --length 3000 --number=4000 5000 6000 */ /* # OUTPUT # -a option was given. -b option was given with an argument of [TheArgumentOfb]. --all option was given. --length option was given with an argument of [3000]. --number option was given with an argument of [4000]. Arguments without an options 5000 6000 */ /**type "struct option": * @param name, "const char*" türünden bir değişken olup, ilgili uzun seçeneğin adını tutmaktadır. * @param has_arg, "int" türden bir değişken olup, argüman bilgisini tutmaktadır. Burada no_argument", * "required_argument" ve "optional_argumen" isimli sembolik sabitler kullanabiliriz. Bu isimler sırasıyla * argümansız, argümanlı ya da isteğe bağlı argümanlı olduğunu belirtmektedir. * @param flag, "int*" türden bir değişken olup, uzun seçeneğin bulunması durumunda, bizim belirlediğimiz * rakamı iş bu adrese yazmak için kullanılmaktadır. NULL geçilmesi durumunda bu mekanizma devre dışı kalacaktır. * @param val, "int" türden bir değişken olup, uzun seçeneğin bulunması durumunda, iş bu "getopt_long" fonksiyonunun * ne ile döneceğini belirtmek için kullanılır. Eğer üçüncü parametreye bir adres bilgisi de geçersek, iş bu değer, * üçüncü parametreye geçilen adrese yazılacaktır. */ struct option options[] = { {"all", no_argument, NULL, 1}, {"length", required_argument, NULL, 2}, {"number", optional_argument, NULL, 3}, {0, 0, 0, 0} }; char* b_arg, *length_arg, *number_arg = NULL; opterr = 0; int result; while ((result = getopt_long(argc, argv, "ab:", options, NULL)) != -1) { switch (result) { case 'a': fprintf(stdout, "-a option was given.\n"); break; case 'b': b_arg = optarg; fprintf(stdout, "-b option was given with an argument of [%s].\n", b_arg); break; case 1: fprintf(stdout, "--all option was given.\n"); break; case 2: length_arg = optarg; fprintf(stdout, "--length option was given with an argument of [%s].\n", length_arg); break; case 3: number_arg = optarg; fprintf(stdout, "--number option was given with an argument of [%s].\n", number_arg); break; case '?': { if (optopt == 'b') { fprintf(stderr, "-b option was given WITHOUT an argument!.\n"); } else if(optopt == 2) { fprintf(stderr, "--length option was given WITHOUT an argument\n"); } else if(optopt != 0) { fprintf(stderr, "invalid option: -%c\n", optopt); } else { fprintf(stderr, "invalid long option!\n"); } } } } if (optind != argc) { fprintf(stdout, "Arguments without an options\n"); for (size_t i = optind; i < argc; i++) { puts(argv[i]); } puts("\n"); } } * Örnek 2, #include "stdio.h" #include "stdlib.h" #include "unistd.h" #include "getopt.h" int main(int argc, char** argv) { /* # INPUT # ./ex_01 -a -b TheArgumentOfb --all --length 3000 --number=4000 5000 6000 */ /* # OUTPUT # -a option was given. -b option was given with an argument of [TheArgumentOfb]. --all option was given. --length option was given with an argument of [3000]. --number option was given with an argument of [4000]. Arguments without an options 5000 6000 */ /**type "struct option": * @param name, "const char*" türünden bir değişken olup, ilgili uzun seçeneğin adını tutmaktadır. * @param has_arg, "int" türden bir değişken olup, argüman bilgisini tutmaktadır. Burada no_argument", * "required_argument" ve "optional_argumen" isimli sembolik sabitler kullanabiliriz. Bu isimler sırasıyla * argümansız, argümanlı ya da isteğe bağlı argümanlı olduğunu belirtmektedir. * @param flag, "int*" türden bir değişken olup, uzun seçeneğin bulunması durumunda, bizim belirlediğimiz * rakamı iş bu adrese yazmak için kullanılmaktadır. NULL geçilmesi durumunda bu mekanizma devre dışı kalacaktır. * @param val, "int" türden bir değişken olup, uzun seçeneğin bulunması durumunda, iş bu "getopt_long" fonksiyonunun * ne ile döneceğini belirtmek için kullanılır. Eğer üçüncü parametreye bir adres bilgisi de geçersek, iş bu değer, * üçüncü parametreye geçilen adrese yazılacaktır. */ struct option options[] = { {"all", no_argument, NULL, 1}, {"length", required_argument, NULL, 2}, {"number", optional_argument, NULL, 3}, {0, 0, 0, 0} }; int a_flag, b_flag, all_flag, length_flag, number_flag, err_flag; a_flag = b_flag = all_flag = length_flag = number_flag = err_flag = 0; char* b_arg, *length_arg, *number_arg = NULL; opterr = 0; int result; while ((result = getopt_long(argc, argv, "ab:", options, NULL)) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'b': b_flag = 1; b_arg = optarg; break; case 1: all_flag = 1; break; case 2: length_flag = 1; length_arg = optarg; break; case 3: number_flag = 1; number_arg = optarg; break; case '?': { err_flag = 1; if (optopt == 'b') { fprintf(stderr, "[ERROR] -b option was given WITHOUT an argument!.\n"); } else if(optopt == 2) { fprintf(stderr, "[ERROR] --length option was given WITHOUT an argument\n"); } else if(optopt != 0) { fprintf(stderr, "[ERROR] invalid option: -%c\n", optopt); } else { fprintf(stderr, "[ERROR] invalid long option!\n"); } } } } if (err_flag) { exit(EXIT_FAILURE); } if (a_flag) { fprintf(stdout, "-a option was given.\n"); } if (b_flag) { fprintf(stdout, "-b option was given with an argument of [%s].\n", b_arg); } if (all_flag) { fprintf(stdout, "--all option was given.\n"); } if (length_flag) { fprintf(stdout, "--length option was given with an argument of [%s].\n", length_arg); } if (number_flag) { if (number_arg != NULL) { fprintf(stdout, "--number option was given with an argument of [%s].\n", number_arg); } else { fprintf(stdout, "--number option was given without an argument of.\n"); } } if (optind != argc) { fprintf(stdout, "Arguments without an options\n"); for (size_t i = optind; i < argc; i++) { puts(argv[i]); } puts("\n"); } } "getopt_long" fonksiyonunun dördüncü parametresi olan "struct option" türünden değişkenin "flag" isimli veri elemanına NULL değeri geçilmediğinde, yani "int" türden bir değişkenin ADRESİ geçildiğinde, ilgili uzun seçenek bulunduğunda, geçilen adrese bilgiyi işliyor ve fonksiyon sıfır değeri ile geri dönüyor. Böylelikle ilgili örnekteki "while" bloğuna hiç girmeden ilgili uzun seçeneğin var bilgisini temin ediyoruz. Fakat seçeneğimiz bir argüman alıyorsa, aldığı argümanı temin etme şansımız KALMIYOR. Bu yöntem argüman almayan seçeneklerde kullanabiliriz. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "unistd.h" #include "getopt.h" int main(int argc, char** argv) { /* # INPUT # ./ex_01 -a -b TheArgumentOfb --all --length 3000 --number=4000 5000 6000 */ /* # OUTPUT # -a option was given. -b option was given with an argument of [TheArgumentOfb]. --all option was given. --length option was given with an argument of [3000]. --number option was given with an argument of [4000]. Arguments without an options 5000 6000 */ int a_flag, b_flag, all_flag, length_flag, number_flag, err_flag; a_flag = b_flag = all_flag = length_flag = number_flag = err_flag = 0; /**type "struct option": * @param name, "const char*" türünden bir değişken olup, ilgili uzun seçeneğin adını tutmaktadır. * @param has_arg, "int" türden bir değişken olup, argüman bilgisini tutmaktadır. Burada no_argument", * "required_argument" ve "optional_argumen" isimli sembolik sabitler kullanabiliriz. Bu isimler sırasıyla * argümansız, argümanlı ya da isteğe bağlı argümanlı olduğunu belirtmektedir. * @param flag, "int*" türden bir değişken olup, uzun seçeneğin bulunması durumunda, bizim belirlediğimiz * rakamı iş bu adrese yazmak için kullanılmaktadır. NULL geçilmesi durumunda bu mekanizma devre dışı kalacaktır. * @param val, "int" türden bir değişken olup, uzun seçeneğin bulunması durumunda, iş bu "getopt_long" fonksiyonunun * ne ile döneceğini belirtmek için kullanılır. Eğer üçüncü parametreye bir adres bilgisi de geçersek, iş bu değer, * üçüncü parametreye geçilen adrese yazılacaktır. */ struct option options[] = { {"all", no_argument, &all_flag, 1}, {"length", required_argument, NULL, 'l'}, {"number", optional_argument, NULL, 3}, {0, 0, 0, 0} }; char* b_arg, *length_arg, *number_arg = NULL; opterr = 0; /* * Aşağıdaki "while" döngüsünde; * 'a' karakteri ile karşılaştığında 'case a', * 'b' karakteri ile karşılaştığında 'case b', * 'l' karakteri ile karşılaştığında 'case l' bölümlerindeki kodlar çalışacaktır. * Eğer "--all" ile karşılaştığında döngüye girmeyecek ve "all_flag" değişkeninin * adresine "1" yazacaktır. Böylelikle ilgili bayrak da "set" edilmiş olacaktır. * Zaten "--length" seçeneğini 'case l' bölümünde halletmişti. * "--number" gördüğünde de "3" ile geri dönecektir. */ int result; while ((result = getopt_long(argc, argv, "ab:l:", options, NULL)) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'b': b_flag = 1; b_arg = optarg; break; case 'l': length_flag = 1; length_arg = optarg; break; case 3: number_flag = 1; number_arg = optarg; break; case '?': { err_flag = 1; if (optopt == 'b') { fprintf(stderr, "[ERROR] -b option was given WITHOUT an argument!.\n"); } else if(optopt == 'l') { fprintf(stderr, "[ERROR] --length option was given WITHOUT an argument\n"); } else if(optopt != 0) { fprintf(stderr, "[ERROR] invalid option: -%c\n", optopt); } else { fprintf(stderr, "[ERROR] invalid long option!\n"); } } } } if (err_flag) { exit(EXIT_FAILURE); } if (a_flag) { fprintf(stdout, "-a option was given.\n"); } if (b_flag) { fprintf(stdout, "-b option was given with an argument of [%s].\n", b_arg); } if (all_flag) { fprintf(stdout, "--all option was given.\n"); } if (length_flag) { fprintf(stdout, "--length option was given with an argument of [%s].\n", length_arg); } if (number_flag) { if (number_arg != NULL) { fprintf(stdout, "--number option was given with an argument of [%s].\n", number_arg); } else { fprintf(stdout, "--number option was given without an argument of.\n"); } } if (optind != argc) { fprintf(stdout, "Arguments without an options\n"); for (size_t i = optind; i < argc; i++) { puts(argv[i]); } puts("\n"); } } Hem kısa seçeğin hem de uzun seçeneğin aynı çıktıyı üretmesi için yapılması gereken yöntemler: * Örnek 1, "switch" merdiveni içerisinde "fallthrough" meydana getirmek. Artık "-l" ve "--length" aynı şeyi yapacaktır. #include "stdio.h" #include "stdlib.h" #include "unistd.h" #include "getopt.h" int main(int argc, char** argv) { /* # INPUT # ./ex_01 -a -b TheArgumentOfb --all --length 3000 --number=4000 5000 6000 */ /* # OUTPUT # -a option was given. -b option was given with an argument of [TheArgumentOfb]. --all option was given. --length option was given with an argument of [3000]. --number option was given with an argument of [4000]. Arguments without an options 5000 6000 */ /**type "struct option": * @param name, "const char*" türünden bir değişken olup, ilgili uzun seçeneğin adını tutmaktadır. * @param has_arg, "int" türden bir değişken olup, argüman bilgisini tutmaktadır. Burada no_argument", * "required_argument" ve "optional_argumen" isimli sembolik sabitler kullanabiliriz. Bu isimler sırasıyla * argümansız, argümanlı ya da isteğe bağlı argümanlı olduğunu belirtmektedir. * @param flag, "int*" türden bir değişken olup, uzun seçeneğin bulunması durumunda, bizim belirlediğimiz * rakamı iş bu adrese yazmak için kullanılmaktadır. NULL geçilmesi durumunda bu mekanizma devre dışı kalacaktır. * @param val, "int" türden bir değişken olup, uzun seçeneğin bulunması durumunda, iş bu "getopt_long" fonksiyonunun * ne ile döneceğini belirtmek için kullanılır. Eğer üçüncü parametreye bir adres bilgisi de geçersek, iş bu değer, * üçüncü parametreye geçilen adrese yazılacaktır. */ struct option options[] = { {"all", no_argument, NULL, 1}, {"length", required_argument, NULL, 2}, {"number", optional_argument, NULL, 3}, {0, 0, 0, 0} }; int a_flag, b_flag, all_flag, length_flag, number_flag, err_flag; a_flag = b_flag = all_flag = length_flag = number_flag = err_flag = 0; char* b_arg, *length_arg, *number_arg = NULL; opterr = 0; int result; while ((result = getopt_long(argc, argv, "ab:l:", options, NULL)) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'b': b_flag = 1; b_arg = optarg; break; case 1: all_flag = 1; break; case 'l': // fallthrough case 2: length_flag = 1; length_arg = optarg; break; case 3: number_flag = 1; number_arg = optarg; break; case '?': { err_flag = 1; if (optopt == 'b') { fprintf(stderr, "[ERROR] -b option was given WITHOUT an argument!.\n"); } else if(optopt == 2) { fprintf(stderr, "[ERROR] --length option was given WITHOUT an argument\n"); } else if(optopt != 0) { fprintf(stderr, "[ERROR] invalid option: -%c\n", optopt); } else { fprintf(stderr, "[ERROR] invalid long option!\n"); } } } } if (err_flag) { exit(EXIT_FAILURE); } if (a_flag) { fprintf(stdout, "-a option was given.\n"); } if (b_flag) { fprintf(stdout, "-b option was given with an argument of [%s].\n", b_arg); } if (all_flag) { fprintf(stdout, "--all option was given.\n"); } if (length_flag) { fprintf(stdout, "--length option was given with an argument of [%s].\n", length_arg); } if (number_flag) { if (number_arg != NULL) { fprintf(stdout, "--number option was given with an argument of [%s].\n", number_arg); } else { fprintf(stdout, "--number option was given without an argument of.\n"); } } if (optind != argc) { fprintf(stdout, "Arguments without an options\n"); for (size_t i = optind; i < argc; i++) { puts(argv[i]); } puts("\n"); } } * Örnek 2, Uzun seçenek bulunması durumunda, ilgili fonksiyon kısa seçenek olan harfini döndürecektir. BU YAKLAŞIM BİÇİMİ TAVSİYE EDİLMEKTEDİR. #include "stdio.h" #include "stdlib.h" #include "unistd.h" #include "getopt.h" int main(int argc, char** argv) { /* # INPUT # ./ex_01 -a -b TheArgumentOfb --all --length 3000 --number=4000 5000 6000 */ /* # OUTPUT # -a option was given. -b option was given with an argument of [TheArgumentOfb]. --all option was given. --length option was given with an argument of [3000]. --number option was given with an argument of [4000]. Arguments without an options 5000 6000 */ /**type "struct option": * @param name, "const char*" türünden bir değişken olup, ilgili uzun seçeneğin adını tutmaktadır. * @param has_arg, "int" türden bir değişken olup, argüman bilgisini tutmaktadır. Burada no_argument", * "required_argument" ve "optional_argumen" isimli sembolik sabitler kullanabiliriz. Bu isimler sırasıyla * argümansız, argümanlı ya da isteğe bağlı argümanlı olduğunu belirtmektedir. * @param flag, "int*" türden bir değişken olup, uzun seçeneğin bulunması durumunda, bizim belirlediğimiz * rakamı iş bu adrese yazmak için kullanılmaktadır. NULL geçilmesi durumunda bu mekanizma devre dışı kalacaktır. * @param val, "int" türden bir değişken olup, uzun seçeneğin bulunması durumunda, iş bu "getopt_long" fonksiyonunun * ne ile döneceğini belirtmek için kullanılır. Eğer üçüncü parametreye bir adres bilgisi de geçersek, iş bu değer, * üçüncü parametreye geçilen adrese yazılacaktır. */ struct option options[] = { {"all", no_argument, NULL, 1}, // İş bu uzun seçenek bulunduğunda, kısa seçenek versiyonu dönecektir. {"length", required_argument, NULL, 'l'}, {"number", optional_argument, NULL, 3}, {0, 0, 0, 0} }; int a_flag, b_flag, all_flag, length_flag, number_flag, err_flag; a_flag = b_flag = all_flag = length_flag = number_flag = err_flag = 0; char* b_arg, *length_arg, *number_arg = NULL; opterr = 0; int result; while ((result = getopt_long(argc, argv, "ab:l:", options, NULL)) != -1) { switch (result) { case 'a': a_flag = 1; break; case 'b': b_flag = 1; b_arg = optarg; break; case 1: all_flag = 1; break; case 'l': length_flag = 1; length_arg = optarg; break; case 3: number_flag = 1; number_arg = optarg; break; case '?': { err_flag = 1; if (optopt == 'b') { fprintf(stderr, "[ERROR] -b option was given WITHOUT an argument!.\n"); } else if(optopt == 'l') { fprintf(stderr, "[ERROR] --length option was given WITHOUT an argument\n"); } else if(optopt != 0) { fprintf(stderr, "[ERROR] invalid option: -%c\n", optopt); } else { fprintf(stderr, "[ERROR] invalid long option!\n"); } } } } if (err_flag) { exit(EXIT_FAILURE); } if (a_flag) { fprintf(stdout, "-a option was given.\n"); } if (b_flag) { fprintf(stdout, "-b option was given with an argument of [%s].\n", b_arg); } if (all_flag) { fprintf(stdout, "--all option was given.\n"); } if (length_flag) { fprintf(stdout, "--length option was given with an argument of [%s].\n", length_arg); } if (number_flag) { if (number_arg != NULL) { fprintf(stdout, "--number option was given with an argument of [%s].\n", number_arg); } else { fprintf(stdout, "--number option was given without an argument of.\n"); } } if (optind != argc) { fprintf(stdout, "Arguments without an options\n"); for (size_t i = optind; i < argc; i++) { puts(argv[i]); } puts("\n"); } } >>> ASCII tablosundaki ilk 32 karakter ekrana basılamayan karakterdir. "getopt_long" fonksiyonunun üçüncü parametresine geçtiğimiz karakterin ASCII tablosundaki karşılığı olan sayıyı, "struct option" türündeki değişkenin dördüncü veri elemanına geçmememiz gerekiyor. >>> "getopt_long" fonksiyonunun beşinci parametresi olan "int*" türden değişkene NULL geçmezsek, yani "int" türden bir değişkenin adresini geçersek, bir uzun seçenek bulunduğunda, iş bu uzun seçeneğin "struct option" dizisinde denk gelen elemanın indeks bilgisini bu adrese yazmaktadır. Fakat pek ihtiyaç duyulan bir yöntem değildir. > UNIX/Linux Sistemlerinde Temel Dizin Yapısı: >> "bin" dizini, komut satırında kullanabileceğimiz komutların "executable" hallerini barındırmaktadır. Örneğin, "ls" komutu bu dizinin de altındadır. >> "boot" dizini, sistemin "boot" edilebilmesi için gereken dosyaları barındırmaktadır. Bizler, "kernel" i derlediğimiz zaman, kernel'e ait "image" dosyasını buraya yerleştireceğiz. >> "cdrom" dizini, CD-Rom'un "mount" edildiği yer. >> "dev" dizini, "device driver" dosyalarının bulunduğu dizin. Bu konuya ilerleyen zamanlarda değineceğiz. >> "etc" dizini, sistemle ilgili önemli konfigürasyon dosyalarının bulunduğu dizin. >> "home" dizini bizim için en önemli dizinlerden bir tanesidir. Sistemde oluşturulan her bir kullanıcı için, bu dizin altında yeni bir klasör oluşturmaktadır. >> "lib" dizinleri ise sistemin kullandığı ama statik ama dinamik kütüphane dosyalarını içerir. >> "media" dizini tipik olarak "media" aygıtlarının "mount" edildiği yer olarak kullanılmakta. >> "mnt" dizini ise genel olarak bir "mount point" şeklinde kullanılıyor. "mount" işlemine de yine ileride değineceğiz. >> "opt", üçüncü parti yazılımların yüklendiği alternatif dizinlerden bir tanesidir. >> "proc", "proc" dosya sistemine ait bir dizindir. İleride değineceğiz. >> "root" dizinin kendisi ise bir "super-user" dizinidir. Aslında "root" ismindeki kullanıcının "home" dizinidir. >> "sbin" dizisinde ise "super-user" kişilerin kullanabileceği komutları içeren dizindir. >> "sys" ise daha modern bir dosya sistemini bünyesinde barındırmaktadır. >> "tmp" dizini ise "temporary" dosya açan uygulamalar için varsayılan bir dizin olarak kullanılmakta. >> "usr" ise kullanıcının yüklediği programların bulunduğu en genel dizinlerden bir tanesidir. > Proses kavramı ve proseslerin kontrol blokları: >> İşletim sistemlerindeki en önemli kavramlardan bir tanesi de "process" kavramıdır ve o an çalışmakta olan programlara verilen isimdir. Pek çok işletim sistemi "process" ile "task" sözcüklerini eş değer kullanmaktadır. >> İşletim sistemi bir programı çalıştırdığında onu sürekli olarak izlemektedir. Dolayısıyla işletim sistemi, o programın ne yaptığı hakkında sürekli olarak fikir sahibi olmaktadır. >> İşletim sistemleri, bir program çalıştırıldığında, iş bu programın bilgilerini "Process Control Block" adı verilen bir veri yapısında tutmaktadır. Bu blok "kernel" tarafından oluşturulmaktadır. Linux kaynak kodlarında bu yapı, "task_struct" adında, C dilindeki "struct" ile temsil edilmiştir. >>> İş bu kontrol blokları bünyesinde prosesin yetki derecesi, proesesin "Current Working Directory" bilgisi, prosesin o an bellekteki yeri, prosesler arası geçiş için bir takım bilgiler, prosesin o anki durum bilgisi, prosesin açmış olduğu dosyalar, prosesin akışları(thread) vb. bilgileri barındırır. "task_struct" bünyesinde bazen bilgi içeren veri elemanları, bazen başka veri yapısını gösteren göstericiler bulunmaktadır. Dolayısıyla dallı budaklı bir ağaç şeklindedir. Bağlı Listeler şeklinde örneklendirilebilir. İşte bu ağacın genelini "task_struct" olarak görebiliriz. > Proseslerin ID değerleri: >> UNIX/Linux sistemlerinde her proses eşsiz bir ID değerine sahiptir. Bu ID değerleri tam sayısal değerdir. İş bu ID değeri o anlıktır. Dolayısıyla belli bir zaman sonra, sona eren proseslerin ID'leri başka proseslere de atanabilir. >> Bu ID değerleri "kernel" tarafından o prosesin kontrol bloğuna erişmede kullanılır. UNIX/Linux sistemlerde bu ID değerleri bir "hash-table" veri yapısında tutmaktadır. Bu ID değerleri "pid_t" türü olarak "typedef" edilmiştir. Bazı sistemlerde bu "signed int", bazı sistemlerde "long" türüne tekabül etmektedir. İşletim sistemini yazanlar bunun kararını vermektedirler. Bütün sistemlerde ortak arayüz "pid_t" türünün kullanılmasıdır. > Prosesler arasındaki "altlık/üstlük" ilişkisi: >> Unutulmamalıdır ki bir programı her zaman bir başka program çalıştırır. Örneğin, komut satırından bir program çalıştırdığımız zaman bu program aslında "shell" programı tarafından çalıştırılmaktadır. >> Proseslerin kontrol bloklarında da kimin çağrılan kimin çağıran olduğu bilgisi de tutulmaktadır. Çağıran program üst("parent"), çağrılan ise alt("child") programdır. "parent" olan bir proses yeni bir proses oluşturduğunda, kendi "Process Control Block" içerisindeki bazı bilgileri "child" olan prosese kopyalamaktadır ya da bu bilgiler kopyalanmaktadır. > Hatırlatıcı notlar: >> C dilinde sonu "_t" ile biten isimler genelde "typedef" edilmiş isimlerdir. POSIX standartlarında da benzer yaklaşım kullanılmıştır. POSIX sistemlerde "typedef" edilmiş isimlerin detaylarına "sys/types.h" isimli başlık dosyasından temin edebiliriz. C dilinde ise "stddef.h" isimli başlık dosyasında. Dolayısıyla gerçek türleri merak etmemiz gerek yok. /*================================================================================================================================*/ (06_12_11_2022) > Proseslerin ID değerleri (devam): >> "ps" komutu ile proseslerin ID değerlerini görüntüleyebiliriz. >> Proseslerin alabileceği maksimum ID değerine "shell" programına aşağıdaki kodu yazarak ulaşabiliriz; "cat /proc/sys/kernel/pid_max" İşletim sistemi bu değere ulaştıktan sonra tekrardan en başa geçiyor ve tamamlanan proseslerin ID değerlerini yeni oluşturulan proseslere atıyor. Böylelikle sistem genelinde tıkanma olasılığı bir hayli düşük. > Proseslerin Kullanıcı ve Grup ID değerleri: >> Proseslerin Kullanıcı ve Grup ID değerleri yetki derecesini belirtmekte kullanılmaktadır ve her proses Kullanıcı ID ve Grup ID değerini bünyesinde barındırır. >> Kullanıcı ve Grup ID değerleri de şu şekilde gruplanmıştır; >>> Kullanıcı ID Değerleri: >>>> Gerçek Kullanıcı ID Değerleri ("Real User ID"): >>>> Etkin Kullanıcı ID Değerleri ("Effective User ID"): >>> Grup ID Değerleri: >>>> Gerçek Grup ID Değerleri ("Real Group ID"): >>>> Etkin Grup ID Değerleri ("Effective Group ID"): Fakat genel olarak yukarıdaki ID değerleri sayısal olarak birbirinin aynısıdır(kendi içerisinde). İleride işlenecek konularda da göreceğimiz üzere, her ne kadar nadiren de olsa, proseslerin Etkin Kullanıcı ID değeri ile gerçek kullanıcı ID değeri, aynı şekilde Etkin Grup ID değeri ile gerçek Grup ID değeri birbirinden ayrılabiliyor. İleride anlatılacak bir takım test işlemlerinde proseslerin etkin ID değerleri işleme sokulmaktadır. >> Proseslerin iş bu ID değerleri tam sayı şeklindedir ve bu değerler de "typedef" edilmişlerdir ki böylelikle sistemler arasında taşınabilirlik korunsun. İşte bu nedenden dolayı, >>> Gerçek Kullanıcı ID ve Etkin Kullanıcı ID değerlerini "uid_t" isimli tür cinsinden yapmışlar. >>> Gerçek Grup ID ve Etkin Grup ID değerlerlerini de "gid_t" isimli tür cinsinden yapmışlardır. >> Prosesler bu dört ID değerini de üst prosesten alıyor, yani kendisini çalıştıran prosesten. Örneğin, bizler "Bash" isimli programı çalıştırıyor olalım. Çalıştığı için kendisi artık bir proses halindedir, dolayısıyla kendisinin de kullanıcı ve grup ID değeri mevcuttur. İş bu programın Kullanıcı ID değerlerinin 100 olduğunu varsayalım. Bizler ilgili "Bash" programından "./sample" komutu ile "sample" isimli bir programı çalıştırdığımız zaman, yeni oluşturulan bu "sample" programının kullanıcı ID değerleri de 100 olacaktır. Bu yaklaşım proseslerin Grup ID değerleri için de geçerlidir. * Örnek 1: "Bash" programının ID değerleri, #include "stdio.h" int main(int argc, char** argv) { /* # INPUT # id */ /* # OUTPUT # uid=1000(ahmopasa) gid=1000(ahmopasa) groups=1000(ahmopasa),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare) */ /** * İş bu programın Kullanıcı ID değerleri, 1000. * İş bu programın Grup ID değerleri, 1000. */ } >> Proseslerin ID değerlerine karşılık bir isim de atanmıştır. Böylelikle konuşma dilindeki anlaşılırlığı arttırmayı hedeflemişlerdir. * Örnek 1, #include "stdio.h" int main(int argc, char** argv) { /* # INPUT # id */ /* # OUTPUT # uid=1000(ahmopasa) gid=1000(ahmopasa) groups=1000(ahmopasa),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare) */ /** * İş bu programın Kullanıcı ID değerleri olan 1000 sayısına karşılık "ahmopasha" ismi verilmiştir. * İş bu programın Grup ID değerleri olan 1000 sayısına karşılık "ahmopasha" ismi verilmiştir. */ } >> Proseslerin ID değeri prosesi betimlerken, Kullanıcı ID'ler ise biz kullanıcıları betimlemektedir. >> İşletim sisteminin kendisi bir proses statüsünde olmadığından kullanıcı ve Grup ID değerleri mevcut DEĞİLDİR!. >> İşletim sistemi proseslere ID verirken bazı sayıları bünyesinde rezerv ediyor. Böylelikle, en kötü senaryoda sistemin admini proses çalıştırabilsin. Örneğin, sistemimizde en fazla 32.000 adet ID değeri atanabilir olsun. İşletim sistemi bunun 100 tanesini bize vermiyor, kendisine saklıyor. Çünkü bütün ID'lerin dolu olması durumunda YENİ BİR PROSES HAYATA GETİREMEYİZ. > Unix/Linux sistemlerinde "Login" süreci: >> "ctrl+alt+F1...F7" tuş kombinasyonlarını kullanarak siyah ekran üzerinden de oturum açabiliriz. >> Bilgisayarı ilk açtığımız zaman, bir takım işlemler arka planda çalıştırılır. Daha sonra işletim sistemi çalıştırılır ve işletim sistemi bir noktada kendini prosese dönüştürür. İş bu prosesin Kullanıcı ID değeri ise "0" şeklindedir. Bu prosesin adı "swapper" ya da "pager" ismindedir. Bu proses ise "init" isminde bir prosesi çalıştırıyor ki onun Kullanıcı ID değeri "1" şeklindedir. Daha sonra "swapper" ya da "pager" isimli proses bekletiliyor/devreden çıkıyor. "init" prosesinin hayata gelmesinden sonra "Kernel" kodlarının bir önemi kalmıyor çünkü "init" fonksiyonunun kodları "kernel" içerisinde DEĞİL. Artık hayatta olan iki proses vardır. Bunlar "swapper" isimli proses ile "init" isimli proses. "init" isimli proses ise bir takım işlemlerden sonra "login" isimli prosesi oluşturuyor. Bu "login" isimli program ise biz kullanıcılardan "username" ve "password" bilgilerini alıyor ve girilen bu bilgileri "etc/passwd" isimli dosya içerisindekiler ile karşılaştırıyor. BİR SİSTEMİ KULLANAN KULLANICILARIN BÜTÜN BİLGİLERİ "etc/passwd" İSİMLİ DOSYA İÇERİSİNDE SAKLAMAKTADIR. Herhangi bir kullanıcı, dosyayı görüntüleyebilir fakat sadece sistem yöneticileri burada değişiklik yapma hakkına sahiptir. >>> "etc/passwd" dosyası bildiğimiz bir text dosyasıdır. Yetkimiz olduğunda yeni bir kullanıcı oluşturmak için elle de giriş yapabiliriz. Bu dosyadaki metinler ":" ayrılmış alanlarda oluşmaktadır. Her satır bir kullanıcı için ayrılmıştır. ":" ile ayrılan toplam 7 adet de alan vardır. UNIX/Linux sistemlerinde Kullanıcı ID ve Kullanıcı İsminin eşleştirildiği tek dosya bu dosyadır. Aşağıda, "etc/passwd" dosyasının bir satırı yazılmıştır: // ... ahmopasa:x:1000:1000:Ahmet Kandemir PEHLİVANLI,,,:/home/ahmopasa:/bin/bash ^ ^ ^ ^ ^ ^ ^ I II III IV V VI VII I: Kullanıcının ismi II: Kullanıcının şifresi. 'x' karakterinin yazılması durumunda, ilgili şifrenin kripte edilmiş hali "etc/shadow" isimli dosya içerisine yazıyoruz. 'x' karakterini kullanmayacaksak, şifrenin kripte edilmiş halini buraya yazıyoruz. İlgili program şifreleri karşılaştırırken kripte edilmiş hallerini karşılaştırdığından şifrenin ne olduğunu sistem de bilmemektedir. Bu tip şifreleme yapan algoritmalara ise tek yönlü şifreleme algoritmaları denmektedir. Şifrelemeyi yapan algoritmanın kaynak kodları da verilmiştir, incelenebilir. UNIX/Linux sistemlerinde "crypt" isimli program şifreleme için oluşturulmuştur. Bu alanın boş bırakılması oturum açarken "Enter" tuşuna basıp geçebileceğimiz anlamına gelmektedir. III: Oluşturduğumuz kullanıcının Kullanıcı ID değeri. IV: Oluşturduğumuz kullanıcının Grup ID değeri. Grupların ismi de "etc/group" isimli dosya içerisindedir. V: Kullanıcıya ait olan iletişim bilgierinin olduğu yer. VI: Oturum açtıktan sonra kendimizi bulacağımız dizin. "Current Working Directory". Bir zorunluluk olmamasına karşın, yeni bir kullanıcı eklerken, ek olarak "home" dizinin içerisine de yeni bir klasör oluşturmalıyız. VII: Oturum açtıktan sonra çalıştırılacak ilk programı belirtir. Bu kısma da kendimizin yazacağı bir programı geçebiliriz. Görüldüğü üzere ":" ile ayrılan her bir kısım aslında başka dosyaları işaret etmektedir. Bu dosyalardan; >>>> "etc/shadow" içerisindekiler: // ... ahmopasa:$6$DcUtZx5u4mUVFQfA$wSGIC2szvgP5zeos4YoaVEbVPbnAZxsSuydIUl9K0xy8DhfPo4hWROBFGm7.xWu5hq/uPVKDIvADYsX6q905Q0:18736:0:99999:7::: ^ ^ ^ ^ ^ ^ I II I: Kullanıcı ismi II: Kullanıcı şifresinin kripte edilmiş hali ... >>>> "etc/group" içerisindekiler: // ... adm:x:4:syslog,ahmopasa ^ ^ ^ ^ // ... ahmopasa:x:1000: ^ ^ ^ I II III sambashare:x:132:ahmopasa ^ ^ ^ ^ I II III IV I: Grubun ismi II: Grubun şifresi III: Grubun ID değeri IV: Bu gruba dahil olanlarınların kullanıcı adları Manuel olarak bir yeni kullanıcının eklenmesi için, ilk önce aşağıdaki komut "shell" programına girilir; "sudo vim /etc/passwd" Buradaki "sudo" kelimesi, bizlerin sistem yöneticisi olduğunu betimlemek içindir. "vim" kullandığımız için "vim" programı karşımıza gelecektir ve açılan text dosyasına yeni bir satır ekliyoruz. Bu yeni satırı eklerken, bilgileri ":" ile ayırarak eklemeliyiz. Dolayısıyla, I : Kısma kullanıcı adı olarak "student" yazısını yazıyoruz. II : Kısmı boş geçiyoruz. Böylelikle şifresiz bir şekilde oturum açabileceğiz. III: Kısma Kullanıcı ID olarak, 1001 sayısını giriyoruz. Genel olarak 1000'den başlatırlar sayıları. IV : Kısma Grup ID olarka, 10001 sayısını giriyoruz. V : Kısma "MyStudent,,," yazısını ekliyoruz. Buradaki "," atomları başka anlama gelmektedir. VI : Kısma "/home/student" yazısını ekliyoruz ki iş bu kullanıcı kendini bu dizinde bulsun. VII: Kısma "/bin/bash" yazısını ekliyoruz ki ilgili program oturum açıldıktan sonra çalıştırılsın. Daha sonra "etc/passwd" dosyasını kaydetip kapatıyoruz. Artık yeni bir kullanıcı oluşturuldu. Fakat bizler henüz "/home" dizini içerisinde "student" isminde yeni bir dizin oluşturmadık. Komut satırından "home" dizinine geliyoruz ve aşağıdaki komutu koşturarak yeni bir dizin oluşturuyoruz; "sudo mkdir student" Artık sistemimizde yeni bir kullanıcımız var. Yeni oluşturulan bu kullanıcıya bir şifre atamak için de yine "shell" programına şu aşağıdaki kodu yazıyoruz; "sudo passwd student" Daha sonra iş bu "student" kullanıcısına ait olacak bir şifreyi giriyoruz. Komut satırından, komutlar ile yeni bir kullanıcı eklemek için, şu aşağıdaki komutları kullanabiliriz; "sudo useradd" "sudo adduser" Bu iki komutun nasıl işlediğine, nasıl kullanabileceğimize internetten bakabiliriz. Bir kullanıcıyı silmek için ilk önce "etc/passwd" içerisindeki ilgili satır komple silinir. Daha sonra "etc/group" içerisindeki satırlardan, silinen kullanıcının ismi de silinir. Ayrıca, bu işlemleri gerçekleştiren "deluser" ve "userdel" komutlarını da kullanabiliriz. > Proseslerin Kullanıcı ID ve Grup ID Değerleri ile Kullanıcıların Kullanıcı ID ve Grup ID değerleri arasındaki ilişki şu şekildedir; "login" isimli program bizden, oturum açma sırasında kullanıcı adı ve şifreyi aldıktan sonra, "etc/passwd" dosyasında aramaya başlıyor. Dışarıdan aldığı kullanıcı adı ve şifre ile iş bu dosyadaki kullanıcı adı ve şifre kombinasyonunu doğrulandığında, "login" fonksiyonu "etc/passwd" içerisinde belirtilen programı çalıştırıyor. Haliyle oradaki program prosese dönüşüyor. Prosese dönüşürken Prosesin Kullanıcı ID Değerleri ve Prosesin Grup ID değerlerini, "etc/passwd" içerisinde belirtilen Kullanıcının Kullanıcı ID Değeri ve Kullanıcının Grup ID Değerinden alıyor. Artık bizim prosesimizin User ID ve Group ID değerleri, "login" programı vasıtasıyla, "etc/passwd" içerisinde belirtilen değerlerden alınıyor. Daha sonra "login" prosesinin işi bittiği için sonlanıyor. Özetle bu ID bilgilerinin ilk çıkış noktası "etc/passwd" isimli dosya. ID değerleri de kalıtım yoluyla aktarıldığından, ilgili "Bash" üzerinden çalıştırılan bütün proseslerin Kullanıcı ID ve Grup ID değerleri yine "etc/passwd" içerisindekiler. > Oturum ilk açıldığında çalıştırılması istenen programa alternatif, yani "/bin/bash" yerine, başka bir programın çalıştırılması: >> İlk önce şu aşağıdaki programı derliyoruz. * Örnek 1, Our Bash Program (version 1) #include "stdio.h" #include "stdlib.h" #include "string.h" #include "unistd.h" #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 char parse_cmd_line(void); void dir_proc(void); void clear_proc(void); void pwd_proc(void); typedef struct tagCMD{ char* name; void (*proc)(void); } CMD; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {NULL, NULL} }; char g_cmdline[MAX_CMD_LINE]; char* g_params[MAX_CMD_PARAMS]; int g_nparams; int main(void) { /* # INPUT # CSD>pwd CSD>ls CSD>dir */ /* # OUTPUT # /home/ahmopasa/Desktop/LearnLinux/Examples/wd bad command: ls dir command executing... */ char* str; int i; for (;;) { fprintf(stdout, "CSD>"); /* * Komut satırından girilenleri "g_cmdline" dizisine * kopyalıyoruz. */ if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; /* * "g_cmdline" dizisinin sonundaki "\n" karakterini * "\0" karakteriyle değiştiriyoruz. */ if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; /* * Daha sonra "g_cmdline" dizisindekileri "g_params" * dizisine aktarıyoruz. Aktarılan her bir kelime için * "g_nparams" bir arttırılacaktır. */ parse_cmd_line(); /* * Eğer hiç kelime aktarılmamışsa, komut satırından bir şey * girilmediği manasındadır. */ if(g_nparams == 0) continue; if(!strcmp(g_params[0], "exit")) break; for(i = 0; g_cmds[i].name != NULL; ++i) if(!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if(g_cmds[i].name == NULL) fprintf(stderr, "bad command: %s\n", g_params[0]); } return 0; } char parse_cmd_line(void) { char* str; g_nparams = 0; for(str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) { g_params[g_nparams++] = str; } } void dir_proc(void) { fprintf(stdout, "dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { if (g_nparams > 1) { fprintf(stdout, "pwd command must be used w/o an argument!\n"); return; } char cwd[4096]; getcwd(cwd, 4096); fprintf(stdout, "%s\n", cwd); } >> Daha sonra komut satırından ki prosesin o anki çalışma dizini yukarıdaki kodun derlenmiş halinin bulunduğu dizin olması lazım, aşağıdaki kodu çalıştırıyoruz; "sudo cp wd /bin/wd" Böylelikle yukarıdaki program "/bin/wd" dizini içerisine kopyalanmış oluyor. Sonrasında da "etc/passwd" isimli dosyayı açarak, yeni eklenen kullanıcının olduğu satırın yedinci bloğuna "/bin/wd" yazısını ekliyoruz. Böylelikle ilgili kullanıcı oturum açtığında bizim yazdığımız program çalıştırılacaktır. > POSIX Fonksiyonlarında Hata Kontrolleri: >> İş bu fonksiyonların %70'e yakını "int" türden değer döndürmektedir. Böyle fonksiyonlarda "0" değeri başarı, "-1" değeri ise başarısızlık manasına gelmektedir. Diğer yandan bazı fonksiyonlar ise gösterici türünden değer döndürmektedir. Bu durumda da NULL değeri başarısızlık anlamına gelmektedir. > Hatırlatıcı notlar: >> Unix/Linux sistemlerinde "typedef" edilen türler "sys/types.h" isimli başlık dosyası içerisindedir. >> Bu zamana kadar ID değerine sahip olan şeyler şunlardır: >>> Proseslerin ID değeri. >>> Proseslerin Kullanıcı ID Değerleri: >>>> Proseslerin Gerçek Kullanıcı ID Değeri. >>>> Proseslerin Etkin Kullanıcı ID Değeri. >>> Proseslerin Grup ID Değerleri: >>>> Proseslerin Gerçek Grup ID Değeri. >>>> Proseslerin Etkin Grup ID Değeri. >>> Sistemi kullanan kişilerin: >>>> Kullanıcı Adı. >>>> Kullanıcı ID değeri. >>>> Grup Adı. >>>> Grup ID değeri. >> Pareto Kuralına göre bir materyalin ya da şeyin %80'nini ve %20'sini ayırsak; >>> İlgili %20'lik kısım, işin %80'lik kısmını yapmaktadır. >>> Geriye kalan %80'lik kısım ise, işin geriye kalan %20'lik kısmını yapıyor. >>> Örneğin, bir "Word" programının %20'lik kısmını bilseniz, bizden istenen şeylerin %80'nini yapabiliriz. Dolayısıyla gücümüzü en önemli %20'lik kısım için harcamalıyız. >> "cd .." komutu bir önceki dizine, "cd /" komutu ise "root" dizinine dönmenizi sağlar. /*================================================================================================================================*/ (07_13_11_2022) > POSIX Fonksiyonlarında Hata Kontrolleri (devam): >> İlgili fonksiyonlar başarısız olduklarında, yukarıdakilere ek olarak, "errno" isimli değişkenin de değerini değiştirmektedir. Bu "errno" değişkeni "errno.h" başlık dosyası içerisinde bildirilmiş olup, "int" türden bir değişken ya da bu türe açılabilen bir makrodur. Dolayısıyla bizlerin "errno" isminde bir makro yazması ya da "errno" isminde bir değişkeni kullanması "Tanımsız Davranış" oluşturacaktır. Ek olarak birden fazla POSIX fonksiyonu "errno" değişkeninin değerini değiştirdiğinde, en son değiştireninki geçerli olacaktır. Bu nedenden dolayı hangi POSIX fonksiyonlarının "errno" değişkenini değiştirdiğini bilirsek, hataların nedenlerini daha sağlıklı bulabiliriz. Son olarak "errno" değişkeninin almış olduğu değerler de "#define" edilmiştir. Hangi değerleri aldığı ve bu değerlerin açıklamalarını gerek ilgili POSIX fonksiyonunun dökümanında gerek şu https://pubs.opengroup.org/onlinepubs/9699919799/ adresinden öğrenebiliriz. Bu değerlerin anlamları ve değerlerin kendileri standart haldedir fakat bazı sistemler kendilerine özgü değerler de tanımlamış olabilir. Aşağıda bu konuya bir örnek verilmiştir. * Örnek 1, //.. int main() { //.. if(ThePosixFunc() == -1) { //.. /* * Buradaki "EPERM", UNIX sistemlerinde ortak bir arayüz oluşturmak için * tanımlanmıştır ve "Operation not permitted" anlamındadır. */ if(errno == EPERM) { //.. } //.. } //.. } Bütün bunların yanı sıra, hiç bir fonksiyon "errno" değişkeninin değerini sıfıra çekmez. Çünkü standartlarca bu garanti altındadır. Yine unutmamalıyız ki bazı POSIX fonksiyonları başarılı olduklarında bile "errno" değişkeninin değerini değiştirmektedirler. Bunların hangi fonksiyon olduklarına ileride değineceğiz. Dolayısıyla tavsiye edilen yöntem, sadece başarısız olduğu bilinen fonksiyonlar için "errno" değişkenine bakmak olacaktır. Son olarak bazı POSIX fonksiyonları "errno" değişkeninin değerini hiç değiştirmeden, direkt olarak hata kodu ile dönerler. Böyle fonksiyonlar için, "0" ile dönmek demek başarılı olduğu anlamındadır. Öte yandan bu "errno" değişkeni "thread" lere özgü olduğundan, ayrı "thread" içerisindekiler birbirlerininkini "set" edemezler. Pekiyi bizler bu "errno" değişkeninin almış olduğu değerleri, daha doğrusu vermek istediği mesajı ekrana nasıl yazdırırız? Tabiki "strerror" ya da "perror" isimli fonksiyonları kullanarak: >>> "strerror" fonksiyonu, aşağıdaki parametrik yapıya sahiptir; #include char *strerror(int errnum); Bu fonksiyon, "errno" değişkenini argüman olarak almakta ve bu değere karşılık gelen yazıyı da geri döndürmektedir. Döndürülen yazı statik ömürlü bir yazı olduğundan, "free()" fonksiyonu ile yazının tuttuğu alanı tekrardan geri vermeye lüzum yoktur.Ek olarak bu fonksiyon standart bir C fonksiyonudur. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "string.h" #include "fcntl.h" #include "errno.h" int main(void) { /* # INPUT # ./wd */ /* # OUTPUT # Error opening file: No such file or directory */ int fd; /* * Buradaki "open" fonksiyonu bir POSIX fonksiyonu olup, * başarısız olduğunda "-1" ile geri dönmektedir. */ if((fd = open("xxx.txt", O_RDONLY)) == -1) { fprintf(stderr, "Error opening file: %s\n", strerror(errno)); exit(EXIT_FAILURE); } fprintf(stdout, "\n********************************\n"); return 0; } * Örnek 2, Merak ettiğimiz herhangi bir "errno" değişkeninin mesajını da yazdırabiliriz: #include "stdio.h" #include "stdlib.h" #include "string.h" #include "fcntl.h" #include "errno.h" int main(void) { /* # INPUT # ./wd */ /* # OUTPUT # Operation not permitted ******************************** */ fprintf(stdout, "%s\n", strerror(EPERM)); fprintf(stdout, "\n********************************\n"); return 0; } >>> "perror" fonksiyonu, aşağıdaki parametrik yapıya sahiptir; #include void perror(const char *s); Bu fonksiyon ise parametre olarak bir yazı almaktadır. Daha sonra bu yazının sonuna ": " karakterlerini eklemektedir. Devamında da o anki "errno" değişkeninin sahip olduğu değerin yazısal karşılığı eklenmektedir. Son olarak birleştirilen bu yazı, "stderr" akımına yazılmaktadır. Yine bu fonksiyon da standart bir C fonksiyonudur. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "fcntl.h" int main(void) { /* # INPUT # ./wd */ /* # OUTPUT # [Error opening file: ]_xxx.txt: No such file or directory */ int fd; /* * Buradaki "open" fonksiyonu bir POSIX fonksiyonu olup, * başarısız olduğunda "-1" ile geri dönmektedir. */ if((fd = open("xxx.txt", O_RDONLY)) == -1) { fprintf(stderr, "[Error opening file:]_"); perror("xxx.txt"); exit(EXIT_FAILURE); } fprintf(stdout, "\n********************************\n"); return 0; } * Örnek 2, "perror" fonksiyonunun temsili implementasyonu: #include "stdio.h" #include "stdlib.h" #include "fcntl.h" #include "errno.h" #include "string.h" void MyErrno(const char* msg) { fprintf(stderr, "%s: %s\n", msg, strerror(errno)); } int main(void) { /* # INPUT # ./wd */ /* # OUTPUT # [Error opening file:]_xxx.txt: No such file or directory */ int fd; /* * Buradaki "open" fonksiyonu bir POSIX fonksiyonu olup, * başarısız olduğunda "-1" ile geri dönmektedir. */ if((fd = open("xxx.txt", O_RDONLY)) == -1) { fprintf(stderr, "[Error opening file:]_"); MyErrno("xxx.txt"); exit(EXIT_FAILURE); } fprintf(stdout, "\n********************************\n"); return 0; } * Örnek 3, İlgili fonksiyonu bir başka fonksiyon ile sarmalamak: #include "stdio.h" #include "stdlib.h" #include "fcntl.h" void exit_system(const char* msg); int main(void) { /* # INPUT # ./wd */ /* # OUTPUT # Error opening: : No such file or directory */ int fd; /* * Buradaki "open" fonksiyonu bir POSIX fonksiyonu olup, * başarısız olduğunda "-1" ile geri dönmektedir. */ if((fd = open("xxx.txt", O_RDONLY)) == -1) exit_system("Error opening: "); fprintf(stdout, "\n********************************\n"); return 0; } void exit_system(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Değişken adetli argüman alan bir fonksiyon kullanarak hata mesajının yazdırılması: #include "stdio.h" #include "stdlib.h" #include "string.h" #include "stdarg.h" #include "errno.h" #include "fcntl.h" void exit_system(const char* msg, ...); int main(void) { /* # INPUT # ./wd */ /* # OUTPUT # Error opening xxx.txt: : No such file or directory */ int fd; char path[] = "xxx.txt"; /* * Buradaki "open" fonksiyonu bir POSIX fonksiyonu olup, * başarısız olduğunda "-1" ile geri dönmektedir. */ if((fd = open(path, O_RDONLY)) == -1) exit_system("Error opening %s: ", path); fprintf(stdout, "\n********************************\n"); return 0; } void exit_system(const char* msg, ...) { va_list ap; va_start(ap, msg); vfprintf(stderr, msg, ap); fprintf(stderr, ": %s\n\n", strerror(errno)); va_end(ap); exit(EXIT_FAILURE); } >> UNIX/Linux sistemlerde, standart C fonksiyonları da aynı zamanda POSIX fonksiyonu kabul edilmektedirler. Dolayısıyla bu fonksiyonlar başarısızlık durumunda "errno" değişkeninin değerini değiştirebilmektedir. Her ne kadar standart C fonksiyonları özünde böyle bir işlem YAPMASALARDA. Örneğin, Standart C fonksiyonu olan "fopen" ile bunun UNIX/Linux dünyasındaki karşılığını aşağıda görebiliriz. >>> Standart C fonksiyonu olan "fopen" => https://en.cppreference.com/w/cpp/io/c/fopen >>> POSIX fonksiyonu olan Standart C fonksiyonu olan "fopen" => https://pubs.opengroup.org/onlinepubs/9699919799/ > "ls -l" komutu ile ekrana çıkan yazıların incelenmesi: "ls -l" komutunu "shell" programı ile çalıştırdığımız zaman, aşağıdaki gibi sonuçlar ekranda belirecektir. Bu sonuçlar, o dizinde bulunan dosyaların detaylarına ilişkindir. -rwxrwxr-x 1 ahmopasa ahmopasa 17128 Kas 17 21:39 wd -rw-rw-r-- 1 ahmopasa ahmopasa 741 Kas 17 21:39 wd.c ^ ^ ^ ^ ^ ^ ^ I II III IV V VI VII Görüldüğü üzere bu bilgiler 7 sütun altında toplanmıştır. Bu bilgilerden, I : İlgili dosyaya, o anki kullanıcının sahip olduğu, erişim hakları. II : İlgili dosyanın yapmış olduğu "hard-link" adedi. III: İlgili dosyanın User ID değeri. Rakamsal karşılığı yine "etc/passwd" dosyası içerisindedir. IV : İlgili dosyanın Group ID değeri. Rakamsal karşılığı yine "etc/passwd" dosyası içerisindedir. V : İlgili dosyanın byte bilgisi. VI : İlgili dosyanın en son değiştirilme tarihi. VII: İlgili dosyanın adı. Sağdan sola doğru olmak üzere, bu bilgileri detaylı bir şekilde incelersek; >> VII: İlgili dosyanın adını belirtmektedir. >> VI : İlgili dosyanın en son değiştirilme tarihini belirtmektedir. >> V : İlgili dosyanın kaç "byte" olduğunu belirtmektedir. >> IV : İlgili Dosyanın Grup ID bilgisini belirtmektedir. UNIX/Linux sistemlerinde dosya oluşturan sadece bir adet POSIX fonksiyonu vardır ki onun adı da "open". Kendisi, arka planda, "sys_open" isimli sistem fonksiyonunu çağırmaktadır. İş bu "open" fonksiyonunu çağıran şey sonuçta bir proses olduğu için, yeni oluşturulan dosyanın Grup ID bilgisi iki farklı şekilde atanabilir: >>> İlki, bu dosyayı meydana getiren prosesin Etkin Grup ID bilgisinin, dosyaya aktarılması. >>> İkincisi ise ilgili dosyanın içinde bulunduğu dizinin Grup ID bilgisinin, yeni oluşturulan bu dosyaya aktarılması. Varsayılan ayar olarak Linux sistemlerinde birincil yaklaşım sergilenmektedir fakat bir takım ayarlamalar ile ikincil yaklaşımın sergilemesi de mümkündür. Son olarak DOSYALAR BİRDEN FAZLA GRUPLARA DAHİL OLAMAZLAR. >> III : İlgili dosyanın Kullanıcı ID bilgisini belirtmektedir. Grup ID bilgisinin elde edilişinden farklı olarak, burada, "open" fonksiyonunu çağıran prosesin Etkin Kullanıcı ID bilgisi, yeni oluşturulan dosyaya aktarılmaktadır. >> II : İlgili dosyanın sahip olduğu "hard-link" sayısı. Bu konunun detaylarına ilerleyen dönemlerde değinilecektir. >> I : Bu kısma dosyanın erişim hakları denmektedir. 10 adet karakterden meydana gelmektedir. Dosya açılmadan evvel öncelikle erişim haklarının kontrolü yapılmaktadır. Yani prosesimizin bir dosya üzerinde yazma hakkı yoksa dosyayı hiç açamıyoruz. İlk aşama olarak, dosyaya erişmeye çalışan prosesin sadece Etkin Kullanıcı ID bilgisinin "0" olup olmadığına bakılıyor. Geleneksel olarak Etkin Kullanıcı ID bilgisi sıfır olan prosesler POSIX standartlarında "root", "super-user" vb. isimler ile anılmakta. Eğer ilgili ID sıfır ise başka hiç bir kontrol yapmadan bütün yetkiler prosese veriliyor. >>> "sudo" kelimesini program isimlerinin başına yazarak bir program çalıştırdığımız zaman, yeni oluşturulan bu prosesin Etkin Kullanıcı ID bilgisi sıfıra oluyor. Örneğin, aşağıdaki komutu "shell" programı ile çalıştırdığımızda, Etkin Kullanıcı ID değeri sıfır olan yeni bir "Bash" prosesini hayata getirmiş oluruz. "sudo /bin/bash" Bundan dolayıdır ki "id" komutunu "shell" üzerinde çalıştırırsak, aşağıdaki çıktıyı elde ederiz; "uid=0(root) gid=0(root) groups=0(root)" Bu da demektir ki "shell" ("Bash") içerisinde yeni bir "shell" programı çalıştırıyoruz. İçerideki "Bash" prosesinin Etkin Kullanıcı ID değeri sıfır, dışarıdaki "Bash" prosesinin Etkin Kullanıcı ID değeri ise "ahmopasa" şeklindedir. İçerideki "Bash" üzerinden oluşturacağımız bütün dosyaların Kullanıcı ID bilgileri ve Grup ID bilgileri artık sıfır değerinde olacaktır. Eğer "exit" komutunu çalıştırırsak, önce içerideki "Bash" prosesi sona erecek ve böylelikle "Bash" prosesine geri döneceğiz. Eğer prosesin Etkin Kullanıcı ID değeri "0" değil ise, bu kısımdaki karakterler şu anlamlara gelmektedir; >>> En soldaki karakter, erişim haklarından ziyade, ilgili dosyanın türü hakkında bilgi vermektedir. Bu karakter şu aşağıdaki karakterlerden birisi olabilir; >>>> "-" karakteri ise, ilgili dosyanın "regular file" olduğunu belirtir. >>>> "d" karakteri ise, ilgili dosyanın bir dizin olduğunu belirtir. Yani "directory", anlamına gelmektedir. >>>> "p" karakteri ise, ilgili dosyanın bir "pipe folder" olduğunu belirtir. >>>> "s" karakteri ise, ilgili dosyanın bir "socket folder" olduğunu belirtir. >>>> "l" karakteri ise, ilgili dosyanın bir "symbolic link" olduğunu belirtir. >>> Geri kalan dokuz karakter ise üç gruba ayrılmıştır: >>>> Soldaki ilk üçlü parçaya "owner" denmekte olup, sahiplik bilgisinin saklandığı yerdir. O an dosyayı kullanacak olan prosesin Etkin Kullanıcı ID bilgisi ile o dosyanın Kullanıcı ID bilgisinin aynı olması durumudur. Eğer ID bilgileri eşit ise geriye kalan diğer üçlü parçalara bakılmıyor ve bu alandaki hakları sorgulamaya başlıyor. Eğer ID bilgileri farklı ise ikinci üçlü parçaya geçiyor. >>>> Soldan ikinci üçlü parçaya "group" denmekte olup, grup hakkında bilgi vermektedir. Eğer "owner" grubundaki ID sorgulaması başarısız olmuş ise sıra bu parçaya geçiyor ve iş bu dosyanın Grup ID bilgisi ile bu dosyada işlem yapacak prosesin Etkin Grup ID bilgisini karşılaştırıyor. Her iki ID bilgisinin de aynı olması durumunda, son üçlü gruba hiç bakmadan, sadece bu gruptaki hakları sorgulamaya başlıyor. >>>> Soldan üçüncü üçlü parçaya ise "other" denmekte olup, ne "owner" ne de "group" dahilinde olanlar hakkında bilgi vermektedir. Bu dosyaya herhangi bir proses tarafından erişildiğini varsaymakta ve buradaki hakları sorgulamaya başlamaktadır. >>>> Her bir grup kendi içerisinde üç karakter barındırmaktadır. Bu karakterlerden en soldaki genellikle "r", ortadaki "w" ve en sağdaki ise genelde "x" karakteri alır. "-" olması durumunda ise o hakkın verilmediği anlamına gelmektedir. * Örnek 1, Aşağıdaki hakları inceleyelim; "-rw-rw-r--" Yukarıdaki hakları sunan dosyanın ID değerleri aşağıdaki gibi olsun; Kullanıcı ID bilgisi: kaan Grup ID bilgisi: study Bu dosyada yazma yapacak prosesin ID değerleri de aşağıdaki gibi olsun; Etkin Kullanıcı ID bilgisi: ali Etkin Grup ID bilgisi: test Bu durumda işletim sistemi en sağdaki ilk üç grupta hak sorgulaması yapacak. Çünkü en soldaki tekil karakteri es geçersek ikinci, üçüncü ve dördüncü karakterlere prosesin Etkin Kullanıcı ID bilgisi ile dosyanın Kullanıcı ID bilgisinin eşit olması durumunda bakılacaktı. Örneğimizde böyle bir eşitlik olmadığından beşinci, altıncı ve yedinci karaktere bakılacak. Fakat buraya bakılabilmesi için ilgili dosyanın Grup ID bilgisi ile işlem yapacak prosesin Etkin Grup ID bilgisinin eşit olması gerekiyor. Bizim örneğimizde böyle bir eşitlik olmadığından, en sağdaki üçlü grupta hak incelemesi yapılacak. Bu durumda "r" karakteri "read-only", "w" karakteri "write" ve "x" karakteri ise çalıştırılabilir bir dosya ise çalıştırma yetkisi vermektedir. Bizim örneğimizdeki karakterler "r--" olması hasebiyle, ilgili proses dosyamız üzerinde SADECE okuma yapabilir. * Örnek 2, Aşağıdaki hakları inceleyelim; "-rw-rw-r--" İlgili dosyanın Kullanıcı ID bilgisi "kaan", Grup ID bilgisi ise "study" olsun. Bu dosyada yazma yapacak prosesin Etkin Kullanıcı ID bilgisi "ali", Etkin Grup ID bilgisi ise "study" olsun. En soldaki karakteri yine es geçiyoruz. Devamında gelen ilk üçlü karakter grubunu, dosyanın Kullanıcı ID bilgisi ile prosesin Etkin Kullanıcı ID bilgisi aynı olmadığı için, es geçiyoruz. Fakat dosyanın Grup ID bilgisi ile prosesin Etkin Grup ID bilgisi aynı olduğu için ikinci üçlü karakter üzerinde hak sorgulaması yapılacaktır. Bu durumda beşinci, altıncı ve yedinci karakter incelenecektir. Yani "rw-" üçlüsüne bakılacaktır. Burada "read-only" ve "write" yetkileri verildiğinden, prosesimiz ilgili dosyada YAZMA işlemi ve OKUMA işlemi yapabilecektir. >>> Çok karşılaşılan bir arayüz ise ilgili dosyanın "-rw-r--r--" şeklinde erişim haklarının olması durumudur. >>> Proseslerin "supplementary" grupları da mevcut olduğundan, bu grupların Etkin Grup ID bilgileri de dosyaların Grup ID bilgileri ile karşılaştırılır. * Örnek 1, "ls -l" komutunu "shell" programında çalıştırdığımız zaman aşağıdaki yazılar ekrana çıkmaktadır; total 24 -rwxrwxr-x 1 ahmopasa ahmopasa 17128 Kas 17 21:39 wd -rw-rw-r-- 1 ahmopasa ahmopasa 741 Kas 17 21:39 wd.c Bu durumda gerek "wd" gerek "wd.c" dosyalarının Kullanıcı ID bilgileri ve Grup ID bilgileri "ahmopasa" şeklindedir. Yine "id" komutunu çalıştırdığımız zaman aşağıdaki yazılar ekrana çıkmaktadır; uid=1000(ahmopasa) gid=1000(ahmopasa) ... Buradaki ID değerleri, ilgili prosesin Etkin ID değerleridir. Yani Etkin Kullanıcı ID, Etkin Grup ID vs. Buradaki Etkin Kullanıcı ID ve Etkin Grup ID bilgileri ise "etc/passwd" dosyasından alınmıştır. Buna göre bizler "shell" üzerinden "Vim" programını çalıştırsak, hayata gelen "Vim" prosesinin de Etkin Kullanıcı ID ve Etkin Grup ID değerleri yine "ahmopasa" olacak. Bu "Vim" prosesi ile yukarıdaki "wd.c" dosyası üzerinde işlem yapmak istediğimiz vakit, prosesin Etkin Kullanıcı ID değeri ile dosyanın Kullanıcı ID değeri birbirine eşit olduğundan, dosyanın sahibi gibi muamele göreceğiz ve soldan ikinci, üçüncü ve dördüncü karakterler üzerinde hak sorgulamasına tabii tutulacağız. Yani "rw-". Bu da demektir ki "Vim" prosesi üzerinden "wd.c" dosyasını okuma ve yazma amacı ile açabiliriz. * Örnek 2, "ls -l /etc/passwd" komutunu "shell" programında çalıştırdığımız zaman aşağıdaki yazılar ekrana çıkmaktadır; -rw-r--r-- 1 root root 2923 Haz 28 2021 /etc/passwd "etc" dizini içindeki "passwd" dosyasının Kullanıcı ID bilgisi ve Grup ID bilgisi "root" şeklindedir. Yine "id" komutunu çalıştırdığımız zaman karşımıza etkin ID bilgileri ekrana basılır; uid=1000(ahmopasa) gid=1000(ahmopasa) ... Prosesimizin Etkin Kullanıcı ID bilgisi ile Etkin Grup ID bilgisi, dosyanın Kullanıcı ID bilgisi ve Grup ID bilgisine EŞİT OLMADIĞI için sekizinci, dokuzuncu ve onuncu karakterler bazında hak sorgulamasına tabii tutulacağız. Yani "r--". Bu durumda bizler "Vim" üzerinden bu dosyaya erişmek istediğimiz zaman sadece okuma hakkımız olacak. * Örnek 3, "ls -l wd" komutunu "shell" programında çalıştırdığımız zaman aşağıdaki yazılar ekrana çıkmaktadır; -rwxrwxr-x 1 ahmopasa ahmopasa 17128 Kas 17 21:39 wd Yine "id" komutunu çalıştırdığımız zaman karşımıza etkin ID bilgileri ekrana basılır; uid=1000(ahmopasa) gid=1000(ahmopasa) ... Buradan anlaşılıyor ki "owner", "group" ve "other" kısmındaki prosesler bu programı çalıştırma yetkisine sahipler. Sırasıyla şu kodları "shell" programında çalıştıralım; "chmod o-x wd" "ls -l wd" Şimdi ekranda çıkacak en son yazı şu şekilde olacaktır; -rwxrwxr-- 1 ahmopasa ahmopasa 17128 Kas 17 21:39 wd Her ne kadar "other" isimli kısımdaki prosesler bu programı çalıştıramasalar da, bizler hala çalıştırabiliriz çünkü bizler hem "owner" hem de "group" kısımlarında yetkiliyiz. Şimdi de şu aşağıdaki kodları "shell" programında çalıştıralım; "chmod g-x wd" "ls -l wd" Şimdi ekranda çıkacak en son yazı şu şekilde olacaktır; -rwxrw-r-- 1 ahmopasa ahmopasa 17128 Kas 17 21:39 wd Artık "other" kısmına ek olarak "group" kısımdaki prosesler de bu programı çalıştıramazlar. Fakat biz hala çalıştırabiliriz çünkü bizler hala "owner" kısmındayız. Şimdi de şu aşağıdaki kodları "shell" programında çalıştıralım; "chmod u-x wd" "ls -l wd" Şimdi ekranda çıkacak en son yazı şu şekilde olacaktır; -rw-rw-r-- 1 ahmopasa ahmopasa 17080 Kas 19 04:01 wd Artık bizlerin de çalıştırma yetkisi yoktur. Eğer çalıştırmayı denersek, aşağıdaki gibi bir hata alacağız; "bash: ./wd: Permission denied" Bu durumda bizler ancak kendimizi "root" yaparsak çalıştırabiliriz. * Örnek 4, "Executable" olmayan bir dosyaya, örneğin bir metin dosyası, "x" hakkı verdiğimiz zaman işletim sisteminin sistem fonksiyonları bu dosyayı bir "Script" dosyası olarak değerlendiriyor ve ona göre aksiyon alıp çalıştırıyor. Bu konunun nasıl olduğuna ilerleyen derslerde de değinilecektir. > Unix/Linux sistemlerinde dosya işlemleri "Case-Sensitive" şeklindedirler. Yani "study" ile "Study" isimli dosyalar birbirinden farklı dosyalardır. Fakat Windows sistemler "Case-Sensitive" değillerdir. > Hatırlatıcı Notlar: >> Aslında sistem fonksiyonları, "errno" değişkeninin değerini değiştirmezler. Negatif değer ile dönen sistem fonksiyonları, hata kodunun kendisinin negatif değerini geri döndürürler. Onu çağıran kod ise bu negatif değeri pozitif hale getirip, "errno" değişkenine atarlar. /*================================================================================================================================*/ (08_19_11_2022) > Prosesler bir dosya üzerinde işlem yapmak istediklerinde, işletim sisteminin aşağıdaki yönergeleri SIRASIYLA takip etmesi gerekmektedir; >> İşlem yapmak isteyen prosesin Etkin Kullanıcı ID bilgisi sıfır ise, işletim sistemine göre bu proses bir "root" proses veya "super-user" proses veya "priviledged" proses olarak değerlendirilirler ve başka herhangi bir sınamaya tabii tutulmazlar. Prosesin yapacak olduğu işe bakmaksızın direkt olarak işleme onay verir. Bunun tek istisnası, "x" hakkı içindir. Şöyle ki; eğer ilgili dosya en az bir kimseye ("user", "group", "other"), "x" hakkı tanımamış ise artık iş bu "root" proses de ilgili dosyayı ÇALIŞTIRAMAZLAR. Örneğin, aşağıdaki özelliklere haiz bir dosyamız olsun; -rw-rw-r-- 1 ahmopasa ahmopasa 17080 Kas 19 04:01 wd Dosyayı "sudo ./wd" ile çalıştırmak istediğimiz zaman aşağıdaki hata mesajını alacağız; sudo: ./wd: command not found Eğer ilgili dosyamızın özellikleri şu şekilde olsaydı; -rwxrw-r-- 1 ahmopasa ahmopasa 17080 Kas 19 04:01 wd Dosyayı "sudo ./wd" ile çalıştırmak istediğimiz zaman aşağıdaki hata mesajını alacaktık; ******************************** Buradan hareketle diyebiliriz ki "root" prosesin de bir dosyayı çalıştırabilmesi için ilgili dosyanın en az bir kimseye "x" hakkı tanıması gerekiyor. Bu kimse ama "user" ama "group" ama "other" olsun. >> Yukarıdaki adım başarısız olmuş ise bu adım izlenir; eğer işlem yapmak isteyen prosesin Etkin Kullanıcı ID bilgisi ile ilgili dosyanın Kullanıcı ID bilgisi birbirinin aynısıysa, işletim sistemi bu prosesi ilgili dosyanın "sahibi" olarak yorumlar. Bu durumda dosyanın yetkilerinden soldan ikinci, üçüncü ve dördüncü yetkiler ile yapılmak istenen iş karşılaştırılır. Eğer yetkiler, bu işlemi destekliyor ise işleme onay verilir. DESTEKLEMİYOR İSE İŞLEM BAŞARISIZLIKLA SONUÇLANIR. >> Yukarıdaki adım da başarısız olmuş ise bu adım izlenir; eğer işlem yapmak isteyen prosesin Etkin Grup ID bilgisi veya ek gruplarının Etkin Grup ID bilgileri, dosyanın Grup ID bilgisiyle aynıysa, işletim sistemi bu prosesi, ilgili dosya ile aynı grupta olan biri olarak yorumlar. Bu durumda dosyanın yetkilerinden soldan beşinci, altıncı ve yedinci yetkiler ile yapılmak istenen iş karşılaştırılır. Eğer yetkiler bu işlemi destekliyor ise işleme onay verilir. DESTEKLEMİYOR İSE İŞLEM BAŞARISIZLIKLA SONUÇLANIR. >> Yukarıdaki adım başarısız olmuş ise, işletim sistemi ilgili prosesi herhangi bir proses olarak görür ve dosyanın sekizinci, dokuzuncu ve onuncu yetkileri ile yapılacak olan işi karşılaştırılır. Eğer ilgili yetkiler, bu işlemi destekliyorsa işleme onay verilir. DEĞİLSE İŞLEM BAŞARISIZLIKLA SONUÇLANIR. Örneğin, aşağıdaki özelliklere haiz bir dosyamız olsun; -rw-r--r-- 1 kaan study 20 Kas 13 13:54 test.txt Dosyaya erişim yapmak isteyen isteyen proses ise "okuma ve yazma" amacı taşısın. Bu durumda, -> Eğer prosesin Etkin Kullanıcı ID bilgisi sıfır ise BU İŞLEM ONAYLANACAKTIR. -> Eğer prosesin Etkin Kullanıcı ID bilgisi "kaan" ise BU İŞLEM YİNE ONAYLANACAKTIR. -> Eğer prosesin Etkin Kullanıcı ID bilgisi veya Etkin Grup ID bilgisi ya da ek gruplarının Etkin Grup ID bilgileri, yukarıdakilerden farklı ise, işlem yine ONAYLANMAYACAKTIR. -> Eğer prosesin Etkin Grup ID bilgisi veya ek gruplarının Etkin Grup ID bilgileri "study" ise BU İŞLEM YİNE ONAYLANMAYACAKTIR. Çünkü dosya ile aynı Grup ID bilgilerine sahip olanlar için sadece "okuma" hakkı tanınmıştır. Öte yandan dosyanın sahibine verilmeyen bir hakkın, grup üyelerine veya diğerlerine verilmesi uygun bir davranış değildir. Örneğin, aşağıdaki özelliklere haiz bir dosyamız olsun; -r--rw-r-- 1 kaan study 20 Kas 13 13:54 test.txt Böylesi bir durumda dosyanın sahibi olan bir kişi sadece okuma yapabilecekken, dosya ile aynı grupta olan veya diğerleri dosyadan okuma ve yazma yapabilecektir. > Bir dosyanın Kullanıcı ID bilgisi "kaan" olsun. Bizler de "etc/passwd" içerisinden "kaan" kullanıcısının bulunduğu satırı komple silelim. Artık bu durumda o dosyanın detaylarını "ls -l" komutu ile incelediğimiz zaman, "kaan" yerine, rastgele sayılar göreceğiz. Bir proses ile bu dosya üzerinde işlem yapmak istediğimiz zaman, prosesin Etkin Kullanıcı ID bilgisi ile iş bu dosyanın rastgele rakamlardan oluşan Kullanıcı ID bilgisi karşılaştırılacaktır. Bu durum sistemin çalışmasında bir bozulmaya yol açmaz fakat tavsiye edilen yöntem, "Bash" programı vasıtasıyla, kullanıcı silen komut satırı argümanlarının kullanılmasıdır. Çünkü bu komutlar ilgili kullanıcının id bilgilerini içeren bütün dosyaları ve programları da silmektedir. Bu komutlar "deluser", "userdel", "delgroup" ve "groupdel" komutlarıdır. Yani elle kullanıcı silmemeliyiz. > "sudo", "su" ve "runuser" komutlarını İNCELEMELİYİZ. Bu komutlar, "Bash" programı üzerinden başka kullanıcılara geçmemizi sağlar. Yani o kullanıcı olarak işleme devam ediyoruz. Örneğin, normal bir şekilde kendi oturumuzu açıyoruz. "Bash" programını çalıştırdığımız zaman, iş bu prosesin id bilgileri, "etc/passwd" dosyasından temin ediliyor. Dolayısıyla bu "Bash" programı üzerinden oluşturduğumuz bütün dosyalar ve çalıştırdığımız diğer prosesler, bizim kullanıcımızınki ile aynı id bilgilerine sahip olacaktır. Eğer "Bash" programı üzerinden "sudo bin/bash" dersek artık "root" kullanıcısı olarak "Bash" programını çalıştırmış oluyoruz. Bu andan sonra çalıştıracağımız bütün prosesler ve oluşturacağımız bütün dosyaların id bilgileri "root" kullanıcısının id bilgileri ile aynı olacaktır. Bir diğer örnek de şu olabilir; Normal bir şekilde kendi oturumuzu açıyoruz. "Bash" programını çalıştırdığımız zaman, iş bu prosesin id bilgileri, "etc/passwd" dosyasından temin ediliyor. Dolayısıyla bu "Bash" programı üzerinden oluşturduğumuz bütün dosyalar ve çalıştırdığımız diğer prosesler, bizim kullanıcımızınki ile aynı id bilgilerine sahip olacaktır. Eğer "Bash" programı üzerinden "su - veli" komutunu çalıştırırsak ve "veli" kullanıcısının şifresini de biliyorsak, artık ilgili "Bash" programının id bilgileri "veli" ninki ile aynı olacaktır. Bu andan sonra çalıştıracağımız bütün prosesler ve oluşturacağımız bütün dosyaların id bilgileri "veli" kullanıcısının id bilgileri ile aynı olacaktır. > Yol İfadeleri (Path Names): Bir dosyanın hangi dizin içerisinde olduğunu belirten ifadelere denmektedir. >> UNIX/Linux dünyasında yol ifadeleri "Case-Sensitive" şeklindedir. Örneğin, "home/kaan/Study/sample.c" ile "home/kaan/study/sample.c" farklı yol ifadeleridir ya da "Sample.c" ile "sample.c" birbirinden farklı dosyalardır. >> UNIX/Linux dünyasında yol ifadelerindeki "/" sembolleri, Windows dünyasındaki yol ifadelerindeki "\" sembolleri arasındakiler yol ifadelerinin bileşenleri olarak adlandırılmıştır. Örneğin, yukarıdaki yol ifadesinde "home", "kaan" vs. birer bileşendir. Yol ifadeleri içerisinde iş bu sembolleri birden fazla kullanmakta herhangi bir sakınca yoktur. >> Yol ifadeleri, temelde ikiye ayrılmaktadırlar; >>> İfadenin başında "/" olması durumunda, ilgili yol ifadesi Mutlak Yol İfadesi olarak adlandırılır. Kök dizinden itibaren yer belirtmektedirler. Yani "/" ifadesi aslında kök dosyadan itibaren aranmayı sağlamaktadır. Unutulmamalıdır ki bir prosesin kökü varsayılan olarak "root" dizinidir. "/" ifadesi ile "root" dizini içerisinde aramaya başlar. Eğer bu kök dizini "home/kaan" yaparsak, artık "kaan" dizini içerisinden aramaya başlayacaktır. İlgili prosesin kök bilgisi de yine "Process Control Block" içerisinde saklanmaktadır. >>> İfadenin başında "/" olmaması durumunda ise ilgili ifade Göreli Yol İfadesi olarak adlandırılır. Her bir prosesin kontrol bloğunda (bkz. Process Control Block) "Current Working Directory" bilgisi tutulmaktadır. Türkçesi o anki çalışma dizini. İş bu Göreli Yol İfadeleri ise ilgili prosesin "Current Working Directory" konumundan itibaren yer belirtiyor. Örneğin, bizim prosesimizin o anki çalışma dizini "home/kaan" olsun. "ali/veli/sample.txt" yol ifadesi aslında "home/kaan/ali/veli/sample.txt" anlamına gelmektedir. Bir diğer örnek, "test.txt" şeklindeki yol ifadesi Göreli Yol İfadesi olduğundan, prosesin o anki çalışma dizini içerisinde aranacaktır. >>>> Proseslerin "Current Working Directory" bilgisi de yine üst prosesten alt prosese aktarılmaktadır. "pwd" komutu ile prosesin o anki çalışma dizin bilgisini temin edebiliriz. "Bash" programındaki yol ifadesinin başında yer alan "~" sembolü sadece "Bash" programı içindir ve "home/kaan" dizinini işaret eder. Bizler yol ifadesi verirken "home/kaan" şeklinde yazmalıyız. * Örnek 1, "ahmopasa@ahmopasa:~/Desktop/LearnLinux/Examples/wd$ pwd" şeklindeki satırın çıktısı "/home/ahmopasa/Desktop/LearnLinux/Examples/wd" şeklinde. Görüldüğü üzere "~" sembölü "home/ahmopasha" ifadesine denk gelmektedir. * Örnek 2, Aşağıdaki örnekte ilgili dosya "Current Working Directory" içerisinde arandığı gösterilmiştir: #include "stdio.h" #include "stdlib.h" int main(void) { /* # INPUT # ~/Desktop/LearnLinux/Examples/wd$ ./wd ~/Desktop/LearnLinux/Examples$ wd/wd */ /* # OUTPUT # Success!.. Error opening file! */ FILE *f; if((f = fopen("test.txt", "r") ) == NULL) { fprintf(stderr, "Error opening file!\n"); exit(EXIT_FAILURE); } printf("Success!..\n"); return 0; } * Örnek 3, Aşağıdaki örnekte ise "Current Working Directory" konumundan itibaren arama sağlayacak şekilde bir yol ifadesi geçilmiştir: #include "stdio.h" #include "stdlib.h" int main(void) { /* # INPUT # ~/Desktop/LearnLinux/Examples$ wd/wd */ /* # OUTPUT # Success!.. */ FILE *f; if((f = fopen("wd/test.txt", "r") ) == NULL) { fprintf(stderr, "Error opening file!\n"); exit(EXIT_FAILURE); } printf("Success!..\n"); return 0; } >>>> Bir prosesin "Current Working Directory" bilgisini "getcwd" fonksiyonu ile temin edebiliriz. Bu bir POSIX fonksiyonudur. Birincil parametre olarak yazının yazılacağı adres, ikincil parametre olarak ise karakter adedi. İş bu fonksiyon başarısız olması halinde "errno" değişkeninin değerini değiştiriyor ve NULL değeri ile geri dönüyor. Başarı durumunda ise ilk parametredeki adrese yazıyı yazmaktadır. Yeteri kadar büyük karakter adedi için Linux sistemlerinde tanımlı "PATH_MAX" sembolik sabitini kullanabiliriz ki bu sabit "limits.h" başlık dosyasında bildirilmiştir. Yazının sonuna gelecek olan '\0' karakteri de bu adetlere dahildir. Bu sembolik 4096 rakamına tekabül etmektedir. Fakat UNIX sistemlerinde bu PATH_MAX sembolik sabitinin tanımlanması zorunlu değildir. Böyle bir senaryoda "pathconf()" fonksiyonunu çağırabiliriz. İş bu fonksiyon "Absolute Path" bilgisini vermektedir, yani "Mutlak Yol İfadesi". * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "limits.h" #include "unistd.h" void exit_sys(const char* msg); int main(void) { /* # INPUT # ~/Desktop/LearnLinux/Examples/wd$ ./wd */ /* # OUTPUT # /home/ahmopasa/Desktop/LearnLinux/Examples/wd */ char buffer[PATH_MAX]; /* * @param PATH_MAX, UNIX sistemlerinde tanımlı olması bir zorunluluk olmadığından, * ilgili fonksiyonun geri dönüş değerini kontrol etmeliyiz. */ if (getcwd(buffer, PATH_MAX) == NULL) { exit_sys("getcwd"); } puts(buffer); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Our Bash Program(version 2): #include "stdio.h" #include "stdlib.h" #include "string.h" #include "unistd.h" #include "limits.h" #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 char parse_cmd_line(void); void dir_proc(void); void clear_proc(void); void pwd_proc(void); void exit_sys(const char* msg); typedef struct tagCMD{ char* name; void (*proc)(void); } CMD; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {NULL, NULL} }; char g_cmdline[MAX_CMD_LINE]; char* g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; int main(void) { /* # INPUT # ~/Desktop/LearnLinux/Examples/wd$ ./wd */ /* # OUTPUT # CSD: /home/ahmopasa/Desktop/LearnLinux/Examples/wd> */ char* str; int i; if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); for (;;) { fprintf(stdout, "CSD: %s> ", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(); if(g_nparams == 0) continue; if(!strcmp(g_params[0], "exit")) break; for(i = 0; g_cmds[i].name != NULL; ++i) if(!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if(g_cmds[i].name == NULL) fprintf(stderr, "bad command: %s\n", g_params[0]); } return 0; } char parse_cmd_line(void) { char* str; g_nparams = 0; for(str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) { g_params[g_nparams++] = str; } } void dir_proc(void) { fprintf(stdout, "dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { if (g_nparams > 1) { fprintf(stdout, "pwd command must be used w/o an argument!\n"); return; } char cwd[4096]; getcwd(cwd, 4096); fprintf(stdout, "%s\n", cwd); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> Bir prosesin "Current Working Directory" bilgisini değiştirmek için "chdir" isimli POSIX fonksiyonunu kullanabiliriz. "Bash" programı üzerinden "cd" ya da "chdir" komutlarını da kullanabiliriz. İş bu fonksiyona göreli ya da mutlak yol ifadesi geçebiliriz. Başarı durumunda sıfıra, başarısızlık durumunda eksi bir değerini döndürmektedir. İlgili yol ifadesini bulamadığı zaman başarısız olacaktır. * Örnek 1, "chdir" fonksiyonunun başarısız olması: #include "stdio.h" #include "stdlib.h" #include "limits.h" #include "unistd.h" void exit_sys(const char* msg); int main(void) { /* # INPUT # ~/Desktop/LearnLinux/Examples/wd$ ./wd */ /* # OUTPUT # /home/ahmopasa/Desktop/LearnLinux/Examples/wd chdir: No such file or directory */ char buffer[PATH_MAX]; if (getcwd(buffer, PATH_MAX) == NULL) { exit_sys("getcwd"); } puts(buffer); if (chdir("/usr/binxxx") == -1) { exit_sys("chdir"); } puts(buffer); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Our Bash Program(version 3): #include "stdio.h" #include "stdlib.h" #include "string.h" #include "unistd.h" #include "limits.h" #include "errno.h" #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 char parse_cmd_line(void); void dir_proc(void); void clear_proc(void); void pwd_proc(void); void cd_proc(void); void exit_sys(const char* msg); typedef struct tagCMD{ char* name; void (*proc)(void); } CMD; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {"cd", cd_proc}, {NULL, NULL} }; char g_cmdline[MAX_CMD_LINE]; char* g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; int main(void) { /* # INPUT # ~/Desktop/LearnLinux/Examples/wd$ ./wd CSD: /home/ahmopasa/Desktop/LearnLinux/Examples/wd> cd .. CSD: /home/ahmopasa/Desktop/LearnLinux/Examples> cd /usr/bin CSD: /usr/bin> cd / */ /* # OUTPUT # CSD: /home/ahmopasa/Desktop/LearnLinux/Examples/wd> CSD: /home/ahmopasa/Desktop/LearnLinux/Examples> CSD: /usr/bin> CSD: /usr/bin> cd / */ char* str; int i; if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); for (;;) { fprintf(stdout, "CSD: %s> ", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(); if(g_nparams == 0) continue; if(!strcmp(g_params[0], "exit")) break; for(i = 0; g_cmds[i].name != NULL; ++i) if(!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if(g_cmds[i].name == NULL) fprintf(stderr, "bad command: %s\n", g_params[0]); } return 0; } char parse_cmd_line(void) { char* str; g_nparams = 0; for(str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) { g_params[g_nparams++] = str; } } void dir_proc(void) { fprintf(stdout, "dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { if (g_nparams > 1) { fprintf(stdout, "pwd command must be used w/o an argument!\n"); return; } fprintf(stdout, "%s\n", g_cwd); } void cd_proc(void) { // DEFAULT if (g_nparams > 2) { printf("Too mang arguments!..\n"); return; } // APPROACH - I if (g_nparams == 1) { printf("Too few arguments!..\n"); return; } if (chdir(g_params[1]) == -1) { printf("%s!\n", strerror(errno)); return; } // APPROACH - II /* char* dir; if (g_nparams == 1) { if((dir = getenv("HOME")) == NULL) exit_sys("getenv"); } else dir = g_params[1]; if (chdir(dir) == -1) { printf("%s!\n", strerror(errno)); return; } */ // DEFAULT if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> Pek çok dosya sistemi, boşluklu dosya isimlerine izin vermemektedir. * Örnek 1, "~/Desktop/LearnLinux/Examples/wd$ ls" komutu verildiğinde aşağıdaki çıktıyı almaktayız; 'ali veli' test.txt wd wd.c Fakat, aşağıdaki komutu çağırdığımız zaman "~/Desktop/LearnLinux/Examples/wd$ cd "ali veli" ", "~/Desktop/LearnLinux/Examples/wd/ali veli$ " şeklinde çıktı alıyoruz. Demek ki bizim dosya sistemimiz boşluklu dosya isimlerine izin veriyor. Gördüğünüz gibi komut satırı argümanlarını tırnak işareti içerisine aldığımız zaman tek bir argüman olarak değerlendirmektedir. >> Yol ifadelerinde "." ve ".." karakterlerinin anlamları sırasıyla o anki çalışma dizini ve bir üst dizin şeklindedir. Örneğin, "/ali/../veli/./selami" şeklinde bir yol ifademiz olsun. "/" ile başladığı için "root" dizini içindeki "ali" dizini, ".." geldiği için tekrardan bir üst dizine dönüyoruz. Yani yine "root" içine geldik. Devamında "/veli" olduğu için, "root" içindeki "veli" dizinine geçiyoruz. "/." ise o anki dizin anlamındadır, yani hala "veli" dizini içindeyiz. En son "selami" olduğundan, "veli" içindeki "selami" dizinine geçiyoruz. Özetle, yukarıdaki o yol ifadesi "/veli/selami" şeklindedir. Bu iki karakter hem göreli hem de mutlak yol ifadesinde kullanılabilir. * Örnek 1, "ali/.." yol ifadesi göreli bir yol ifadesidir. O anki çalışma dizininden bir üst dizine çıkacaktır. * Örnek 2, "/home/kaan/../veli" yol ifadesi ise mutlak bir yol ifadesidir. Önce "home" içindeki "kaan" dizinine geçilir. "/.." ile karşılaşınca bir üst dizine geri döner, yani tekrardan "home" içine. Sonrasında da "home" içerisinde "veli" dizinine geçer. > "Internal Command" ve "External Command" : "External Command", "/bin" dizini içerisinde bulunan programlardır. "Internal Command" ise, bizim yazdığımız "Bash" programındaki fonksiyonlar gibi düşünülebilir. /*================================================================================================================================*/ (09_20_11_2022) > "ls" komutuna "-a" seçeneğini geçtiğimiz zaman ekrana "." ve ".." ile başlayan dosyaları da bastıracaktır. Aksi halde "ls" komutu "." ve ".." ile başlayan dosyaları ekrana yazdırmamaktadır. > Yeni bir dizin oluşturduğumuz zaman, "root" dizini hariç, bütün dizinlerin içerisinde "." ve ".." isminde birer adet dizin oluşturulur. Bu dizinler sırasıyla o anki dizinin yerini ve bir üst dizinin yerini tutarlar. Ek olarak bu iki dosya SİLİNEMEZ. > Hata mesajlarının yerelleştirilmesi için "locale" bilgisinin değiştirilmesi gerekiyor. "setlocal" fonksiyonu ile "locale" bilgisini değiştirebiliriz. Varsayılan lokal dili İngilizce dilidir. Lokallerin yazım biçiminde önce ülke, sonra o ülke içindeki lehçe ve "encoding" ayarına dair bilgi gelir. Aşağıdaki örnekte "tr" ülkeyi, "TR" o ülke içindeki şiveyi, "UTF-8" ise "encoding" bilgisini içerir. Bir program başladığında ise varsayılan lokal bilgisi "C" şeklindedir. Minimal, hiç bir ülkeyle ve dil ile alakası olmayan bir lokaldir. Yani "tr_TR.UTF-8" yerine "C" gelir. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "string.h" #include "errno.h" #include "locale.h" void exit_sys(const char* msg); int main(void) { if (setlocale(LC_ALL, "tr_TR.UTF-8") == NULL) { fprintf(stderr, "cannot set locale!...\n"); exit_sys("setlocale"); } puts("Success!..."); puts(strerror(EPERM)); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Dizinlerin erişim hakları; >> Normal dosyalarda "owner", "group" ve "other" ne manaya geliyorsa dizinler için de aynı manaya gelmektedir. >> Dizinlerde de, tıpkı dosyalarda olduğu gibi, erişim hakları vardır. "w" hakkı demek ilgili dizinde bir dosya oluşturulabileceği ya da silinebileceği anlamına gelmektedir. Bir dosyayı silebilmek için bu dosya üzerinde "w" hakkına sahip olmamıza gerek yoktur, iş bu dosyanın içinde bulunduğu dizin üzerinde "w" hakkında sahip olmamız gerekiyor. Bir dosya oluşturabilmek, ismini değiştirebilmek için de keza aynı şey geçerlidir. Çünkü işletim sistemi tarafından dizinler, o dizin içerisindeki dosya bilgilerini barındıran, birer dosya olarak değerlendiriliyorlar. * Örnek 1, "~/Desktop/LearnLinux$ ls -l Examples" komutunu çalıştırdığımı zaman karşımıza "Examples" dizini içindekilerin detaylı bilgileri çıkacaktır. Eğer, "~/Desktop/LearnLinux$ ls -l -d Examples" komutunu çalıştırırsak, işte bu sefer "Examples" dizininin kendisi hakkında detaylı bilgileri çıkartacaktır. >> İlgili dizinde "r" hakkına sahip olmamız demek, o dizinin içeriğinin okunabilmesi anlamına gelmektedir (bkz. ls komutu). >> İlgili dizinde "x" hakkında sahip olmak demek, ilgili dizinin içerisine girebilmek demektir. Bir yol ifadesini argüman olarak bir programa geçtiğimiz zaman, ilgili ifade önce bileşenlerin ayrılıyor ve bu bileşenlerin de birer dizin olduğu teyit ediliyor. Buna "Path Name Resolution" denmektedir. Bunun için de ilgili bileşenin "x" hakkını bizlere sunması gerekiyor ki o dizinin içine girebilelim. Eğer bir yol ifadesindeki bir dizinde "x" hakkını kaldırırsanız, o dizinden öteye "Path Name Resolution" yapılamaz, o dizinden öteye gidilemez. Dolayısıyla ilgili yol ifadesindeki bütün dizinler, ilgili prosese, "x" hakkını vermeli ki hedeflenen dosyaya erişim sağlanabilsin. Linux dökümanlarında bu hakka "Searc Permission" da denmektedir. Öte yandan "mkdir" komutu ile bir dizin oluşturduğumuz vakit bu hak otomatik olarak sunulur. Zaman zaman da bu hak dizin ağacında duvar örmek, ötesine geçişi engellemek için de kullanılabilinir. Dosyalarda kimseye "x" hakkı vermediğimiz zaman "root" kullanıcısı bile dosyayı çalıştıramıyor idi fakat dizinlerde böyle bir şey söz konusu değil. Hiç kimseye dahi bu hakkı vermeseniz, "root" kullanıcısı yine müdahale edebilir. * Örnek 1, "home/kaan/Study/C/sample.c" şeklinde bir yol ifademiz olsun. Hedeflenen dosya ise "sample.c" olsun. Hedefe ulaşabilmek için prosesimizin kim olduğu önce belirlenir. Yani "owner", "group" ya da "other" dan birisiyizdir. Sonrasında iş bu proses, sırasıyla "home", "kaan", "Study", "C" dizinlerinde "x" hakkına sahip olmalı ki "Path Name Resolution" başarıyla tamamlansın ve hedefe erişebilelim. > İşletim Sistemlerinin Dosya Sistemleri; >> İki gruba ayrılmıştır. Bir grup, disk üzerindeki dosya organizasyonundan sorumlu iken diğer grup işletim sistemi çalıştırıldığında, çekirdek alanının içerisindeki organizasyonlardan sorumludur. Yani işletim sistemlerinde dosya sistemi iki ayrı ayaktan meydana gelmektedir. >> Beş adet sistem fonksiyonları ile işletilmektedir. Bunlar "sys_open", "sys_close", "sys_read", "sys_write" ve "sys_lseek" isimli sistem fonksiyonlarıdır. Bunlara karşılık gelen POSIX fonksiyonları ise sırasıyla "open", "close", "read", "write" ve "lseek" fonksiyonlarıdır. İş bu fonksiyonlar ise sırasıyla dosya açmak için, dosya kapatmak için, dosyadan okuma yapmak için, dosyaya yazmak için ve dosya göstericisini konumlandırmak için kullanılırlar. Hangi programlama dilini kullanırsak kullanalım, günün sonunda dosya işlemleri iş bu beş fonksiyon çağrılarak işimiz görülmektedir. Bu POSIX fonksiyonlarının kaynak kodlarına da "elixir.bootlin.com" adresinden ulaşabiliriz. >> Bir dosya açıldığı zaman, bu dosyayı açan prosesin "Process Control Block" unun içerisinde "task_struct" türünden bir gösterici bulunmaktadır. İş bu gösterici tarafından gösterilen nesne ise bünyesinde "files_struct" türünden gösterici barındırır. Yine bu gösterici tarafından gösterilen nesne ise bünyesinde "fdtable" türünden başka bir yapı bulundurur. İş bu "fdtable" türünden yapı ise içerisinde bir adet gösterici-dizisi barındırmaktadır, yani göstericiyi gösteren gösterici. Bu gösterici ise elemanları "file" türden olan bir dizinin başlangıç adresini göstermektedir. Yani bu dizinin her bir elemanı olan gösterici ise "file" türünden adreslerdir. Bu göstericiyi gösteren göstericinin ismi ise "fd" biçimindedir. İş bu gösterici-dizisine ise Dosya Betimleyici Tablosu denmektedir. İşte, Linux çekirdeği tarafından bir dosya açıldığında, diskten ilgili dosyanın bilgilerini alıp, RAM içerisinde, ilgili prosesin kontrol bloğuna yerleştiriyor. Böylelikle açık dosyanın bilgileri disk yerine memory'de saklanıyor. İş bu "file" türü ise bünyesinde dosyanın açış moduna dair bilgiler, dosya göstericisinin konum bilgisi vb. bir sürü bilgiyi barındırmaktadır. İşin özü, dosyaya ait bilgiler iş bu "file" türünden olan "struct" içerisindedir. "Linux Kernel" kursunda bu konunun detayları açıklanmaktadır fakat şu an için bizim bilmemiz gereken şey şudur: bir dosya açıldığında işletim sistemi, diskten ilgili dosyanın bilgilerini çekerek, "file" türünden olan yapının içerisine yerleştiriyor. Özetle, "task_struct(a) --> files_struct(b) --> fdtable(c) --> file*[](d) --> file(e)" a: https://elixir.bootlin.com/linux/v6.3.1/source/include/linux/sched.h#L737 b: https://elixir.bootlin.com/linux/v6.3.1/source/include/linux/fdtable.h#L57 c: https://elixir.bootlin.com/linux/v6.3.1/source/include/linux/fdtable.h#L57 d: https://elixir.bootlin.com/linux/v6.3.1/source/include/linux/fdtable.h#L57 e: https://elixir.bootlin.com/linux/v6.3.1/source/include/linux/fs.h#L942 Kısca, "task_struct" > "file*[]" > "file" >> Bir dosya kapatıldığında ise "file*[]" olan, yani Dosya Betimleyici Tablosu, dizinin elemanları boşaltılıyor ve ilgili "file" türden değişken de yok ediliyor. Buradan da diyebiliriz ki her bir prosesin Dosya Betimleyici Tablosu birbirinden farklıdır. Her dosya açışımızda ayrı bir "file" türden değişken hayata gelir. Aynı dosyayı iki defa da açsak yine iki adet "file" türünden değişkenimiz olacaktır. > "open" isimli POSIX fonksiyonu, UNIX türevi sistemlerde bir dosyayı açmak için kullanılmaktadır. Prototipi şu şekildedir; #include int open(const char* path, int flags, ...); Her ne kadar imzasından istediğimiz kadar argüman ile çağırabileceğimiz anlaşılsa da, işin özünde bizler bu fonksiyonu ya iki argüman ile ya da üç argüman ile çağırmalıyız. Eğer üç argüman ile çağıracaksak, bu üçüncü argüman "mode_t" türünden olmak zorundadır. "mode_t" türü herhangi bir "integral" tür olabilir. Bu fonksiyonu üçten fazla argüman ile çağırmak "Tanımsız Davranış" a neden olur. Bu fonksiyonun birinci parametresi, açılacak olan dosyaya ilişkin yol ifadesidir. İkinci parametre ise dosyayı açış bayraklarını belirtmektedir. Yani dosyayı hangi amaç için açtığımızın bilgisini bu argüman ile geçiyoruz. Bu bayrak, TEK BİTİ bir olan sayılar şeklindedir. Dolayısıyla birden fazla bayrağı "|" (Bit-wise OR) işlemine soktuğumuz zaman, iki farklı amaca dair bilgiyi argüman olarak geçebiliriz. C dilindeki maskeler gibi. Bu bayraklar "O_" ile başlamakta olup, önemlilerden bazıları şu isimdedirler; "O_RDONLY", "O_WRONLY", "O_RDWR". Fakat bu üç bayraktan yalnızca BİR TANESİNİ argüman olarak geçebiliriz ve bu bayrakların anlamları ise sırasıyla şu şekildedir; "Okuma", "Yazma" ve "Okuma & Yazma". Fakat unutulmamalıdır ki ilgili dosyanın, bu dosyayı açacak prosese, sırasıyla şu hakları da vermesi gerekmektedir; "r", "w" veya "rw" Bu hak konusunda bir uyuşmazlık olursa "open" fonksiyonu BAŞARISIZ OLUR. Bunlara ek olarak, "open" fonksiyonu yeni bir dosya oluşturmak için de kullanılabilinir fakat bunun için "O_CREAT" bayrağını kullanmalıyız. Bu bayrağı, yukarıdaki üç bayraktan bir tanesi ile "|" (Bit-wise OR) işlemine sokarsak, ilgili dosyanın olmaması durumunda, yeni bir dosya oluşturulacak fakat dosyanın olması durumunda OLAN dosya açılacak eğer yetki konusunda da bir terslik yoksa. Fakat bu "O_CREAT" bayrağını kullanmazsak, dosyanın olmaması durumunda "open" fonksiyonu başarısız olacaktır. Buradaki "O_CREAT" bayrağı, dosyanın var olması durumunda ilgili dosyanın sıfırlanacağı ANLAMINA GELMEMEKTEDİR. Sadece "Dosya yok ise yeni bir dosya oluştur" anlamına gelmektedir. Eğer dosya var ise bu "O_CREAT" bayrağı işlevsiz hale gelir. Fonksiyonun üçüncü parametresi ise ilgili dosyanın sunacağı erişim haklarının bilgisidir. Üçüncü argümanı geçmemiz, dosyanın var olması durumunda, ilgili dosyanın erişim haklarının değişeceği anlamına gelmez. SADECE YENİ BİR DOSYA OLUŞTURDUĞUMUZ ZAMAN İŞ BU DOSYANIN HAKLARINI BELİRTMEKTEDİR. Dolayısıyla "O_CREAT" BAYRAĞINI KULLANIYORSAK, ÜÇÜNCÜ PARAMETRE OLARAK BU HAKLARI DA fonksiyona geçmeliyiz. Eğer "O_CREAT" bayrağı girilmemiş ise bu üçüncü parametreyi geçmemiz anlamsız olacaktır. Yine bu üçüncü parametre de, tıpkı ikinci parametrede olduğu gibi bir takım bayraklardan meydana gelmektedir ve "|" (Bit-wise OR) işlemi uygulayabiliriz. Bu bayraklar ise "sys/stat.h" başlık dosyasında bildirilmiş olup isimleri "S_" ile başlamaktadır. Bu isimlerin isimlendirme konvensiyonu şu şekildedir; -> "S_" karakterlerinden sonra ya "R" ya "W" ya da "X" karakteri gelmekte. -> İş bu karakterden sonra ya "USR", ya "GRP" ya da "OTH" kelimelerinden bir tanesi gelmektedir. Bu şekilde dokuz adet bayrak tanımlanmıştır. Bu bayrak ise şu şekildedir; "S_IRUSR", "S_IWUSR", "S_IXUSR", "S_IRGRP", "S_IWGRP", "S_IXGRP", "S_IROTH", "S_IWOTH", ve "S_IXOTH" Örneğin, "rw-r--r--" haklarını veren bayrak kombinasyonu şu şekildedir; (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) Bu dokuz bayrağa ek olarak üç tane de bayrak daha vardır ki bu dokuz bayrağın kombine edilmiş halledir; "S_IRWXU", "S_IRWXG" ve "S_IRWXO". EĞER "O_CREAT" BAYRAĞINI İKİNCİ PARAMETRE OLARAK KULLANMIŞSAK FAKAT ÜÇÜNCÜ PARAMETRE OLARAK "S_I" İLE BAŞLAYAN ARGÜMANLARI KULLANMAMIŞSAK "Tanımsız Davranış" OLUŞACAKTIR. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "string.h" #include "fcntl.h" #include "sys/stat.h" void exit_sys(const char* msg); int main(void) { /* # INPUT # ~/Desktop/LearnLinux/Examples/wd$ ls /Desktop/LearnLinux/Examples/wd$ ./wd */ /* # OUTPUT # test.txt wd wd.c Success!.. */ if (open("test.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) == -1) { exit_sys("open"); } puts("Success!.."); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (10_26_11_2022) > "open" isimli POSIX fonksiyonu (devam), >> "O_TRUNC" isimli bir açış moduna da sahiptir. Bu açış modunu "O_WRONLY" ya da "O_RDWR" ile birlikte kullanmalıyız, fakat "O_RDONLY" modu ile birlikte KULLANAMAYIZ. Bu mod, açılan dosyanın sıfırlanacağı anlamına da gelmektedir. "O_TRUNC" bayrağını kullanabilmek için dosyanın illaki yeniden oluşturulması gerekmez, halihazırda var olan dosyaları da açabiliriz. Fakat böyle bir dosya mevcut değil ise fonksiyon başarısız olacaktır. >>> Aşağıdaki tabloda ise Standart C fonksiyonlarındaki açış modları ile POSIX fonksiyonlarının açış modlarının karşılaştırmasını bulabilirsiniz: Standart C <=> POSIX "w" O_WRONLY | O_CREAT | O_TRUNC "W+" O_RDWR | O_CREAT | O_TRUNC "r" O_RDONLY "r+" O_RDWR >> "O_APPEND" bayrağı, yazma işleminin dosyanın sonuna yapılacağı anlamına gelmektedir. Tıpkı "O_TRUNC" bayrağında olduğu gibi, bu bayrağı kullanabilmek için bizlerin "O_WRONLY" ya da "O_RDWR" bayraklarını da kullanmamız gerekmektedir. Bu bayrak geçildiğinde işletim sistemi, dosya göstericisini EOF konumuna kaydırarak, yazma yapmaktadır. >>> Aşağıdaki tabloda ise Standart C fonksiyonlarındaki açış modları ile POSIX fonksiyonlarının açış modlarının karşılaştırmasını bulabilirsiniz: Standart C <=> POSIX "a" O_WRONLY | O_CREAT | O_APPEND "a+" O_RDWR | O_CREAT | O_APPEND >> "O_EXCL" bayrağı ise "dosya yok ise yeni bir dosya oluştur, var ise bir şey yapma ve başarısız ol" anlamına gelmektedir. Dosya halihazırda mevcut ise fonksiyon başarısız olmaktadır. Dolayısıyla bu bayrak, "O_CREAT" bayrağı ile birlikte kullanılmalıdır. "O_CREAT" olmadan bu bayrağın kullanılması "Tanımsız Davranış" oluşturur. >> Geri dönüş değeri "int" türden olup, ismine Dosya Betimleyicisi("File Descriptor") de denmektedir. Bu geri dönüş değerini "write" ve "read" işlemi yapan diğer POSIX fonksiyonlarına argüman olarak geçiyoruz ki hangi dosya üzerinde işlem yapıldığı belli olsun. "open" fonksiyonunun başarısız olması durumunda, -1 ile geri dönülmektedir ve "errno" değişkeninin değeri o hataya ilişkin bir değere çekilir. Örneğin, dosyamızı "O_RDWR" modunda açmak isteyelim. Bu durumda bu işi yapacak olan prosesin, bu dosya üzerinde "r" ve "w" haklarına sahip olması gerekmektedir. Aksi halde "open" fonksiyonu başarısız olacak ve "errno" nun değeri "EACCESS" değerine çekilecektir. Buradaki kilit nokta, kontrolün ilk başta yapılmasıdır. Yani, prosesimiz ilgili dosya üzerinde "r" ve "w" haklarına sahip değilse, dosya hiç açılmamaktadır, "read" veya "write" fonksiyonlarına daha sıra gelmemiştir. Çünkü prosesimiz dosya üzerinde ne "r" hakkına ne de "w" hakkına sahiptir. AÇMA İŞLEMİNİN BAŞARI DURUMU KESİNLİKLE KONTROL EDİLMELİDİR. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "fcntl.h" #include "sys/stat.h" void exit_sys(const char* msg); int main(void) { /* # INPUT # //.. */ /* # OUTPUT # //.. */ int fd; if ((fd = open("test.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) { exit_sys("open"); } puts("Success!.."); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> İşletim sistemi veya POSIX nezdinde "text_mode"/"binary_mode" şeklinde bir kavram yoktur. Bu kavramlar C dili nezdinde oluşturulan kavramlardır. >> Önceki günlerde de açıklandığı üzere proseslerin kontrol bloğu içerisinde("Process Control Block"), dolaylı yoldan, Dosya Betimleyici Tablosunun("File Descriptor Table") adresi tutulmaktadır. Bu tablonun her bir indeksinde de "file" türünden dosya nesnelerinin adresleri tutulmaktadır. İşte "open" fonksiyonu çağrıldığında sırasıyla şu işlemleri yapmaktadır; ilk önce "file" türünden bir dosya nesnesi hayata getiriyor ve diskten aldığı bilgileri bu nesnenin içerisine işliyor. Sonrasında bu nesnenin adresini de Dosya Betimleyici Tablosundaki boş bir indise yazar. İlgili indis numarasını da geri döndürüyor eğer başarılı olmuş ise. İşte bu geri dönüş değeri bir nevi "handle" olarak kullanılmaktadır ki ilgili dosyaya erişebilelim. Geri döndürülen bu indis bilgisinin adı da "File Description" olarak geçmektedir. Fakat işin esasında bu işlemler, bir sistem fonksiyonu olan "sys_open" isimli Linux sistem fonksiyonu tarafından yapılmaktadır. "open" sadece bu fonksiyonu çevrelemektedir. >>> Dosya Betimleyici Tablosunun ilk üç indisi her zaman için bizlere dolu olarak sunulmaktadır. Sıfırıncı indis "stdin", birinci indis "stdout", ikinci indis ise "stderr" isimli dosya nesnelerini göstermektedir. POSIX standartlarına göre "open" fonksiyonunun Dosya Betimleyici Tablosundaki ilk boş indisi vermesi garanti edilmiştir. >>> Dosya Betimleyici Tablosu ise proseslere özgüdür, yani her prosesinki farklıdır. Buradan hareketle diyebiliriz ki "open" fonksiyonunun geri döndürdüğü File Descriptor("fd") değeri, kendi prosesinde anlamlıdır. Bu indis bilgisi, başka prosesinkinde bambaşka bir "file" nesnesine hitap ediyor olabilir. >>> Bütün bu süreç aslında şöyle de özetlenebilir; >>>> İlk evvel parametrelerin geçerliliği kontrol edilir. Geçerli parametre girilmediğinde herhangi bir dosya açma işlemi yapmadan süreç sonlandırılır. >>>> Daha sonra Dosya Betimleyici Tablosunda boş yer olup olmadığına, var ise en düşük indisinin kaçıncı indis olduğuna bakarız. Eğer bu tabloda yer yok ise herhangi bir dosya açma işlemi yapmadan süreç sonlandırılır. >>>>> Bütün tabloyu tekrar tekrar silbaştan dolaşmak yerine, ilgili tablodaki toplam indis adedince bitlerden oluşan yeni bir dizi tahsis edilmiş. Bu yeni dizinin her bir indisi aslında Dosya Betimleyici Tablosundaki aynı indisi göstermekte. Eğer Dosya Betimleyici Tablosundaki 10 numaralı indis kullanımdaysa, bitlerden oluşan dizinin de on numaralı indisinin değeri "1" olarak değiştirilmekte. Eğer 15 numaralı indis boş olsaydı, bitlerden oluşan dizinin 15 numaralı indisinin değeri "0" olarak değiştirilmekte. Bunun avantajı ise şundan kaynaklanmaktadır; işlemcilerin bir takım komutları bir bit dizisindeki ilk "1" değerine sahip indisi döndürür. Bu komutlar ile bit-dizisindeki ilk dolu olan indisi öğrenip, Dosya Betimleyici Tablosundaki aynı indise erişebiliyoruz. "find_next_zero_bit" isimli kernel fonksiyonu ilk boş biti bize döndürmektedir. Bu veri yapısına da "bitset" veri yapısı denmektedir. >>>> Devamında ise "file" türden bir dosya nesnesi tahsis edip, diske gideceğiz. Diskten uygun bilgileri alıp, bu nesnenin içerisine yazacağız. >>>> Sonrasında da bu "file" türden dosya nesnesinin adresini de Dosya Betimleyici Tablosundaki uygun indise yazacağız. >>>> İşletim sistemi, o an çalışmakta olan prosesin yani "open" fonksiyonunu çağıran prosesin, "Process Control Block" adresini her zaman bir gösterici ile tutmaktadır. Linux sistemlerinde bu göstericinin adı "current" ismindedir. >>>>> "current" isimli göstericinin tanımlandığı yere ise aşağıdaki linktek ulaşabiliriz: https://elixir.bootlin.com/linux/1.2.5/source/kernel/sched.c#L88 > Dosyanın Kapatılması: >> Bir prosesin sonlanması hasebiyle işletim sistemi, bu prosesin açmış olduğu bütün dosyaları, Dosya Betimleyici Tablosundan hareketle kapatmaktadır. Dosya Betimleyici Tablosunun maksimum indis sayısı 1024 tanedir, yani bir proses ile en fazla 1024 adet dosya açabiliriz. >> Bir dosya açıkken hem Kernel içerisinde hem de Dosya Betimleyici Tablosunda yer kaplamaktadır. Dolayısıyla işimizin bittiği dosyaları biz kapatmalıyız ki hem Kernel rahatlasın hem de Dosya Betimleyici Tablosunda boş yer sayısı çoğalsın. Nesne Yönelimli programlama dillerinde dosyalar bir sınıf ile temsil edildiklerinden, örneğin C++ dili için "RAII idiom", C# & Java dillerinde "Reference Counter" üzerinden çalışan "Garbage Collector", dosyaların kapatılması otomatik olarak gerçekleştirilmekte. Fakat C dilinde bizler manuel olarak kapatmalıyız. >> Dosya Betimleyici Tablosundaki her bir indiste gösterilen dosya nesneleri ("file"), bünyesinde bir sayaç barındırmaktadır. İlgili tablodaki farklı indislerin aynı "file" nesnesini göstermesi durumunda, iş bu sayacın değeri arttırılır. Eğer bu sayacın değeri sıfır olmuş ise dosya artık kapatılır(önce "file" nesnesinin hayatı bitirilir, sonrasında da tablodaki ilgili indis sıfırlanır). İş bu sayacın ismi Linux kaynak kodlarında "f_count" ismindedir. >> Dosyanın kapatılması için "close" isimli POSIX fonksiyonu kullanılmakta olup, doğrudan bir sistem fonksiyonu olan "sys_close" isimli fonksiyonu çağırmaktadır. "close" fonksiyonunun tek parametre alır ki bu parametre "open" fonksiyonunun geri döndürdüğü parametredir, fakat başarısızlık durumunda "-1" ile geri dönmektedir. Genel olarak bu fonksiyonun geri dönüş değeri kontrol edilmez çünkü umulur ki dosyayı açarken herhangi bir aksaklık olmadıysa kapatırken de olmayacaktır. Bu fonksiyon, "open" fonksiyonuna nazaran "unistd.h" isimli başlık dosyasında bildirilmiştir. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "fcntl.h" #include "sys/stat.h" void exit_sys(const char* msg); int main(void) { /* # INPUT # //.. */ /* # OUTPUT # //.. */ int fd; if ((fd = open("test.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) { exit_sys("open"); } puts("Success!.."); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Dosya Göstericisi Kavramı: >> Dosya göstericisi, "read" ve "write" işlemleri için bir orjin noktasıdır. Dosyadaki her bir bayt için bir adet off-set numarası getirilmiştir. Konum bilgisi için bu off-set bilgisi kullanılmaktadır. >> Dosyanın ortasından bir yere ekleme işlemi mümkün değildir, sadece ilgili konumdan itibaren yazma işlemi yapılacaktır. Bu durumda halihazırdaki veri silinebilir. Eğer iş bu göstericiyi EOF konumuna aldıktan sonra yazma işlemi yaparsak, dosyayı büyütüyor olacağız (EOF konumu, dosyanın en sonundaki karakterden bir sonraki konumdur). >>> EOF konumundan itibaren bir bayt okuma yaparsak, bir şey okumamış oluruz fakat bu olağan bir durumdur. >> Dosya ilk açıldığında dosya göstericisi sıfırıncı off-set'i göstermektedir, daha doğrusu birinci karakteri göstermektedir eğer bir yazı varsa. Yani en baştadır. Üç karakter okuma ya da yazma yaptığımız zaman artık dördüncü off-set'i gösteriyor olacaktır, bir diğer değişle beşinci karakteri gösterecektir. >> Dosya göstericisinin konumu EOF konumundan da öteye konumlandırılabilir(okuma ya da yazma yapmaksızın). İlgili konumdan itibaren yazma yapıldığında, EOF konumundan itibarenki konumlar artık Dosya Delikleri olarak geçer(yazmanın başladığı konuma kadarki olanlar) ve bu konumdaki karakterler sıfır karakteri ile doldurulur. Özel bir konu olduğundan şu an için detaylarından bahsedilmeyecektir. >> C dilindeki "fopen" fonksiyonu için dosya açılış modu kavramı vardır fakat işletim sistemi için böyle bir kavram söz konusu değildir. İşletim sistemine göre dosyalar baytlardan oluşmaktadır. C dilindeki "binary_mode" a bu yönüyle benzerlik gösterebilir. >> Dosya göstericisinin konum bilgisi, dosya nesnesi içerisindedir. Yani bir dosyayı iki defa açsak bile iki farklı dosya nesnesi hayata geleceğinden, iki farklı dosya konum göstericisi elde edeceğiz. "f_pos" isimli değişken bu konum bilgisini tutmaktadır. > "read" fonksiyonu, >> Günün sonunda dosyadan okuma yapmak için varolan tek POSIX fonksiyonudur. Bu fonksiyon da bir sistem fonksiyon olan "sys_read" isimli fonksiyonu çağırmaktadır. >> Bu fonksiyon "unistd.h" isimli başlık dosyasında bildirilmiştir. Fonksiyonun birinci parametresi hangi dosyadan okuma yapacağımızı anlatan bir "file handler" ki bunu bizler "open" fonksiyonunun geri dönüş değeri olarak elde ediyoruz. Fonksiyonun ikinci parametresiyse okunacak bilgilerin bellekte yerleştirileceği adres. Okunulan yazılar bu adrese yazılacaktır. Üçüncü parametre ise okunacak byte sayısını belirtmektedir. Fonksiyonun geri dönüş değeri ise başarı durumunda okunan bayt miktarıdır, başarısızlık durumunda ise "-1" şeklindedir. Burada dikkat edilmesi gereken husus, 0 bayt okumanın da legal bir okuma olduğudur. Yanlış parametre geçilmesi, diskin bozulması gibi durumlarda iş bu "read" fonksiyonu "-1" ile geri dönmektedir. Örneğin, bizler 10 bayt okumak için bir dosyayı açıyor olalım. Fakat dosyanın içerisinde ise 7 karakter olmuş olsun. Bu durumda 7 karakter okunacak, dosya göstericisi EOF konumuna gelecek ve "read" fonksiyonu "7" ile geri dönecektir. "read" fonksiyonunun "0" ile geri dönmesi, EOF konumundan itibaren okuma yapıldığı anlamına gelmekte olup GAYET NORMAL BİR DURUMDUR. "-1" İLE GERİ DÖNMESİ ABNORMAL BİR DURUMDUR. Geri dönüş değerinin türü "ssize_t" türündendir. Bu tür POSIX'e özgü olup, bir kaç başlık dosyasında "typedef" edilmiştir ve İŞARETLİ BİR TAM SAYIYA TEKABÜL EDECEĞİ GARANTİ ALTINDADIR. 32-bit sistemlerde "long", 64-bit sistemlerde ise "long long" türünün eş anlamlıdır. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "fcntl.h" #include "sys/stat.h" #include "unistd.h" void exit_sys(const char* msg); int main(void) { /* # INPUT # //.. */ /* # OUTPUT # //.. */ int fd; char buffer[10 + 1]; // 11 karakterlik yerimiz var. ssize_t result; if ((fd = open("read.c", O_RDONLY)) == -1) exit_sys("open"); if((result = read(fd, buffer, 10)) == -1) // 10 karakter okuyacağız. exit_sys("read"); close(fd); buffer[result] = '\0'; // 10. indise, yani 11. karaktere, de ilgili sayıyı işliyoruz. puts(buffer); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include "stdio.h" #include "stdlib.h" #include "fcntl.h" #include "sys/stat.h" #include "unistd.h" void exit_sys(const char* msg); #define BUFFER_SIZE 4096 int main(void) { /* # INPUT # //.. */ /* # OUTPUT # //.. */ int fd; char buffer[BUFFER_SIZE + 1]; // 11 karakterlik yerimiz var. ssize_t result; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); while((result = read(fd, buffer, BUFFER_SIZE)) > 0) { if(-1 == result) exit_sys("read"); buffer[result] = '\0'; printf("%s", buffer); } close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (11_27_11_2022) > "write" POSIX fonksiyonu, >> Dosyaya yazmak için çağrılan bir fonksiyondur. Çoğu sistemde doğrudan sistem fonksiyonu olan "sys_write" isimli fonksiyonu çağırmaktadır. >> İmzası ise şu şekildedir; İlk parametre, "open" fonksiyonundan elde edilen "File handler". İkinci parametre ise okumanın yapılacağı veri alanının başlangıç adresi. Bu alandan okuma yapacağı için burasını bir nevi "source" olarak düşünebiliriz. Bundan sebeptir ki ikinci parametre "const" bir göstericidir. Üçüncü parametre ise kaç bayt yazılacağının bilgisidir. Yine 0 bayt yazmak istememiz, normal bir durumdur ve fonksiyon sıfır ile geri döner. Buradan hareketle çok büyük olasılıkla diyebiliriz ki yazmak istediğimiz bayt miktarını geri döndürmektedir. Diskin tam dolu olması ya da kesme sinyallerinin gelmesi gibi durumlarda da fonksiyonumuz "-1" ile geri dönmektedir. Yazma işlemi sırasında diskin dolması durumunda yazabildiği bayt kadarını geri döndürür. >> İş bu fonksiyon da "unistd.h" isimli başlık dosyasında bildirilmiştir. >> Yazma işlemi de yine bayt bayt şeklindedir. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "string.h" #include "fcntl.h" #include "sys/stat.h" #include "unistd.h" void exit_sys(const char* msg); int main(void) { /* # INPUT # //.. */ /* # OUTPUT # //.. */ int fd; char buffer[] = "This is a test."; ssize_t result; if ((fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("open"); if( write(fd, buffer, strlen(buffer)) == -1) exit_sys("write"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > "pread" & "pwrite" POSIX fonksiyonları: Yukarıda da belirtildiği üzere "read" ve "write" fonksiyonları işlevlerini dosya göstericisinin bulunduğu konumdan itibaren yapmakta, bu işlemler sırasında da iş bu göstericinin yerini ilerletmiş olmaktadırlar. İşte "pread" ve "pwrite" fonksiyonları ise okuma ve yazma işlemleri sırasında dosya göstericisinin gösterdiği yerden değil de argüman olarak aldığı "offset" konumundan itibaren yapmaktadırlar, dolayısıyla dosya göstericisinin konumunu DEĞİŞTİRMEZLER. Fakat bu fonksiyonların kullanımı seyrektir. Fonksiyonların prototipi aşağıdaki gibidir. #include ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset); ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset); Bu fonksiyonların klasik "read" ve "write" fonksiyonundan tek farkı, son parametreleri olan "offset" parametreleridir. > Bir dosyanın kopyalanması, >> UNIX/Linux sistemlerde geleneksel olarak dosya kopyalama işlemi, kaynak dosyadan hedef dosyaya bir döngü içerisinde, baytların kopyalanması yoluyla gerçekleştirilir. Fakat bazı UNIX türevi işletim sistemleri bünyelerinde bu işi gerçekleştiren alt seviyeli sistem fonksiyonları da bulundurmaktadır. Linux işletim sisteminde bu iş "copy_file_range" isimli sistem fonksiyonu tarafından yürütülmektedir. Fakat UNIX ailesi içinde taşınabilirlik olması açısından geleneksel yöntem tercih edilmelidir. >> Bu işlem sırasında ne kadar büyüklükte bir tampon bölge kullanılmalıdır? Tipik olarak dosya sistemindeki bir blokun büyüklüğü bu iş için tercih edilir. "stat", "fstat" ve "lstat" gibi fonksiyonlar bu büyülüğün katlarını bizlere vermektedir. Blok uzunlukları ekseriyetle 512 rakamının katları şeklindedir. 512 olmasının sebebi ise diskin en küçük 512 baytlık küçük parçalara ayrılmış olmasıdır (dosya sisteminden dosya sistemine bu rakam değişebilir ama). * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "fcntl.h" #include "sys/stat.h" #include "unistd.h" #define BUFFER_SIZE 4096 void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # INPUT # ./main source.txt destiny.txt # INPUT (source.txt) # This is a text file */ /* # OUTPUT (destiny.txt) # This is a text file */ if(argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } int fds, fdd; // File Handler for Source, File Handler for Destiny. if((fds = open(argv[1], O_RDONLY)) == -1) exit_sys(argv[1]); if((fdd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys(argv[2]); char buffer[BUFFER_SIZE]; ssize_t result; /* * Döngünün her bir turunda kaynak dosyadan 4096 baytlık okuma talep edilecek. * (n-1). turda 4096 bayttan küçük bir bayt okunacak. * n. turde ise 0 bayt okunacak, böylelikle "read" fonksiyonu sıfır ile geri dönecek. * Herhangi bir IO hatasında da -1 ile geri dönecek. */ while((result = read(fds, buffer, BUFFER_SIZE)) > 0) { /* * 4096 bayt okunursa, "result" değişkeninin değeri 4096 olacak. * 4096 bayt yazılırsa, "write" fonksiyonu 4096 ile geri dönecek. * Eğer başka bir değerle dönerse "okuduğumuz bayt miktarını yazamamışız" * olacağız ve programı sonlandıracağız. */ if(write(fdd, buffer, result) != result) { fprintf(stderr, "cannot write to file!...\n"); exit(EXIT_FAILURE); } } if(-1 == result) exit_sys("read"); close(fds); close(fdd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Dosya Göstericisinin Konumlandırılması ("lseek" fonksiyonu), >> "lseek" fonksiyonu da doğrudan "sys_lseek" isimli sistem fonksiyonunu çağırmaktadır. >> Kullanımı, standart bir C fonksiyonu olan, "fseek" fonksiyonuna çok benzemektedir. >> İş bu fonksiyon da "unistd.h" isimli başlık dosyasında bildirilmiş olup imzası şöyledir; birinci parametresi, "open" fonksiyonunun geri dönüş değeri olan "File Handler". İkinci parametresi ise "off-set" bilgisi, yani kaçıncı "off-set" e konumlandıracağımızı belirtmektedir. İkinci parametre, POSIX sistemlerinde işaretli bir tam sayının tür eş ismidir. Üçüncü parametre ise dosyanın hangi orjin noktasından itibaren konumlandırılmanın yapılacağını bildirmektedir ki bu orjinler dosyanın başı, dosyanın sonu(EOF) noktası ve en son kullanılan orjin şeklindedir. Üçüncü parametre olarak 0, 1 ya da 2 rakamları girilebilir. Pek tabii bu rakamların yerine sırasıyla "SEEK_SET", "SEEK_CUR" ve "SEEK_END" isimli sembolik sabitleri de kullanabiliriz. Fonksiyonun geri dönüş değeri ise göstericinin yeni konum bilgisidir. Başarısızlık durumunda da, örneğin dosyanın başından itibaren -5. konum gibi, -1 ile geri dönmektedir. EOF konumundan itibaren, +n. konuma, konumlandırmak için de o sistemde kullanılan dosya sisteminin buna izin vermesi gerekmektedir. Böylesi bir durumda yazma işlemi yapılırsa, aradaki konumlar artık birer dosya delikleri("file holes") olarak anılır. Bu konu ileride ele alınacaktır. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "string.h" #include "fcntl.h" #include "sys/stat.h" #include "unistd.h" void exit_sys(const char* msg); #define MyStrLength(buffer) (sizeof(buffer) / sizeof(*buffer)) int main(void) { /* # OUTPUT (destiny.txt) # This is a text file This is a test line. */ const char buffer[] = "\nThis is a test line."; int fd; if((fd = open("destiny.txt", O_WRONLY)) == -1) exit_sys("open"); lseek(fd, 0, SEEK_END); // const unsigned long buffer_size = sizeof(buffer) / sizeof(*buffer); // const unsigned long buffer_size = sizeof buffer; // const unsigned long buffer_size = MyStrLength(buffer); if(write(fd, buffer, strlen(buffer)) == -1) exit_sys("write"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> Dosya açarkenki "O_APPEND" modu, atomik bir biçimde "write" çağrısından evvel, dosya göstericisinin konumunu EOF konumuna çekmektedir. > POSIX standartlarına göre dosyaya yapılan okuma ve yazma işlemleri sistem genelinde atomik yapıdadır; >> İki farklı proses bir dosyayı açsa ve her ikisi de yazma işlemi yapsa, önce birininki yazılır ve sonrasında diğerininki. Yani baytların yarısı ilk prosesten, diğer yarısı ikinci prosesten gelecek şekilde bir yazma yapılmaz. Ama hangi prosesinkinin önce yazacağını bilemeyiz. Burada iki prosesin de ilgili işlemleri aynı lokasyona yaptığı varsayılmıştır. >> Benzer şekilde iki farklı prosesten biri okuma biri yazma yapıyor olsun. Ya yazma işleminden evvelki yazı okunur ya da yazma işlemi bittikten sonraki yazı. Burada da iki prosesin de ilgili işlemleri aynı lokasyona yaptığı varsayılmıştır. >> Yukarıdaki iki senaryoda da senkronizasyon biz kullanıcalara bırakılmıştır. Burada da devreye dosya kilitleme mekanizması girmektedir; ama dosyanın bütününü kilitleriz ama sadece dosyanın belirli bir kısmını. Fakat dosyanın bütününün kilitlenmesi iyi bir teknik değildir. Bunun yerine sadece işlemin yapılacağı bölümler, ilgili "off-set" alanları kilitlenmelidir. Dosyanın geri kalanı erişime açık bırakılmalıdır. Burada önemli olan günün sonunda bir prosesin yazdığı baytların kalmasıdır. İki prosesin baytlarının iç içe geçmesi aslında dosyanın bozulması anlamına gelmektedir. > Proseslerin "Umask" değerleri, >> Bu değerler proseslerin kontrol blokları içerisinde saklanmaktadır ve "parent" prosesten aktarılmaktadır. Türü "mode_t" ile ifade edilir. >> "umask" isimli POSIX fonksiyonu ile ayrıca "get" ve "set" işlemleri yapılabilmektedir. >> Bir dosya açarken, o dosyaya vermiş olduğumuz erişim haklarının YANSITILMAMASI için kullanılır. Yani "umask" değerleri, maskelenecek değerleri belirtmektedir. Dolayısıyla "umask" değerleri neyse, bu değerler dosyaya yansıtılmayacaktır. Eğer "umask" değeri sıfır ise maskelecek değer olmadığı anlamına gelip, bütün değerler dosyaya yansıtılacaktır. * Örnek 1, "umask" değerleri => S_IXUSR | S_IWGRP Dosyaya verilen değerler => rwxrwxrwx Dosyaya yansıtılan değerler => rw-r-xrwx * Örnek 2, "umask" değerleri => 0 Dosyaya verilen değerler => rwxrwxrwx Dosyaya yansıtılan değerler => rwxrwxrwx > Hatırlatıcı Notlar: >> Linux Kernel dökümantasyonu için "kernel.org" internet sitesini kullanabiliriz. >> Tek hamlede yazılabilecek maksimum karakter alanı için pratik bir limit söz konusu değildir. Zaman zaman da döngünün her anında büyük büyük okumalar yerine biraz da ideal büyüklükteki okumalar yaparsak daha hızlı sonuç alabiliriz. >> Bir C/C++ programcısı olarak UNIX/Linux türevi sistemlerde dosya işlemleri yapmak için üç adet seçenek söz konusu olabilir; >>> C/C++ dillerinin dosya fonksiyonlarını kullanmak. Fakat bu fonksiyonlar spesifik bir sistem gereksinimini karşılayacak şekilde tasarlanmamışlardır. Tavsiye edilen, işimizi bu fonksiyonlar görüyor ise bu fonksiyonlar ile görmektir. >>> POSIX fonksiyonlarını çağırmak. >>> Sistem fonksiyonlarını çağırmak. Fakat çoğu sistemde POSIX fonksiyonları doğrudan sistem fonksiyonlarını çağırdığından, özel özel sistem fonksiyonlarını çağırmamıza gerek yoktur. >> İşlemlerin atomik olması, ilgili işlemlerin "task-switch" e maruz kalmadan işlenmesi anlamına gelmektedir. Bir diğer anlamı ise sadece o anda bir prosesin işleminin yapılıyor oluşudur. >> "IO-Scheduler" : Proseslerden gelen okuma ve yazma işlem taleplerinin, diskin o anki okuma başlığının konumuna göre yeniden sıralanması işlemine denkmektedir. >> "/dev" dizinin altında "zero" isimli bir aygıt sürücüsü bulunmaktadır. Aygıt sürücüleri de bir nevi dosya olarak işlem görmektedir. Bu dosyadan kaç bayt okursak, o bayt ededince sıfır elde ederiz. Bu dizinde EOF konumuna gelinmez, dolayısıyla sonsuz büyüklükte düşünebiliriz. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "fcntl.h" #include "sys/stat.h" #include "unistd.h" void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 */ int fd; unsigned char buffer[100]; if((fd = open("/dev/zero", O_RDONLY)) == -1) exit_sys("open"); read(fd, buffer, 100); for(int i = 0; i < 100; ++i) printf("%02X%c", buffer[i], i % 16 == 15 ? '\n' : ' '); printf("\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "/dev" dizinin altındaki bir başka aygıt sürücüsü de "full" isimli aygıt sürücüsüdür. Bu dosyaya bir şey yazmak istediğimiz zaman diskin dolu olduğu etkisini göstermektedir. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "string.h" #include "fcntl.h" #include "sys/stat.h" #include "unistd.h" void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # write: No space left on device */ int fd; unsigned char buffer[100] = "I am Ahmopasa"; if((fd = open("/dev/full", O_WRONLY)) == -1) exit_sys("open"); if(write(fd, buffer, strlen(buffer)) == -1) exit_sys("write"); //for(int i = 0; i < 100; ++i) printf("%02X%c", buffer[i], i % 16 == 15 ? '\n' : ' '); //printf("\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "/dev" dizini altındaki bir diğer aygıt sürücüsü de NULL isimli aygıt sürücüsü. Buraya yazılan her şey kayboluyor. Altı boş bir sepet olarak düşünebiliriz. >> "/dev" dizini altındaki bir diğer aygıt sürücüsü de "random" isimli olandır. Buradan okuma yaptığımız zaman rastgele baytlar okuyor olacağız. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include "fcntl.h" #include "sys/stat.h" #include "unistd.h" void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # 09 0B F5 80 F2 9A AC 58 B6 D8 DD 20 26 32 8A 47 B5 2F E8 75 64 63 CF 90 65 5F 2C E9 03 C8 59 5E 48 D9 8C E8 0F 91 0C 52 70 21 0C E3 89 FE 29 63 74 4B 20 1A 66 86 3A 52 D1 51 8E C5 9A D2 8F C4 46 FC A3 76 6E 87 56 CB 08 DC 13 63 97 E0 EA 3D 0D 90 89 99 DF 1E 16 C4 75 F8 AD A5 96 79 A7 B4 A1 CE 34 9E */ int fd; unsigned char buffer[100]; if((fd = open("/dev/random", O_RDONLY)) == -1) exit_sys("open"); read(fd, buffer, 100); for(int i = 0; i < 100; ++i) printf("%02X%c", buffer[i], i % 16 == 15 ? '\n' : ' '); printf("\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (12_03_12_2022) > Proseslerin "Umask" değerleri (devam), >> "shell" programının "umask" değerini, "umaks" isimli komut ile elde edebiliriz. Bu değer genel olarak "0022" ya da "0002" oktal değerlerinden birisi olmaktadır. >>> Bu oktal değerlerden her bir basamak aslında üç adet "bit" değerinin toplamını göstermektedir. Örneğin, "0002" oktal sayısını "000 000 000 010" bitleri ile gösterebiliriz. Bu bit grubunun, >>>> En sol taraftaki üçlü olanlar sırasıyla "set_userid", "set_grpid" ve "sticky_bit" bitlerini temsil etmektedir. >>>> Soldan ikinci üçlü olanlar "owner" grubu için sırasıyla "r", "w" ve "x" haklarını temsil etmektedir. >>>> Soldak üçüncü olanlar ise "group" grubu için sırasıyla "r", "w" ve "x" haklarını temsil etmektedir. >>>> En sağdakiler ise "other" grubu için sırasıyla "r", "w" ve "x" haklarını temsil etmektedir. Buradan hareketle "0002" oktal rakamı aslında "other" grubu için "w" hakkının maskeleneceğini belirtmektedir. Tekrar hatırlatmakta fayda olacaktır; "umask" komutu bir "shell" komutudur ve "shell" programı üzerinden çalıştırılacak bütün programların "umask" değerini bize gösterir. Her ne kadar prosesler kendi "umask" değerini değiştirebilse de, "umask" değeri babadan oğula geçen bir değerdir. Bir proses, kendi "umask" değerini değiştirmek için de yine aynı isimdeki POSIX fonksiyonunu kullanır. >> "shell" programının kendi "umask" değerini "umask" komutu ile değiştirirken, yeni maskelerin değerini oktal dijitler halinde vermeliyiz. Örneğin, "shell" programı üzerinden "umask" komutu ile "shell" prosesinin maske değerinin "0002" olduğunu öğrenelim. O halde yine aynı program üzerinden "umask 022" komutunu çalıştırdığımız zaman, yeni maske değerimiz "0022" olacaktır. En yüksek anlamlı oktal dijiti bizler geçmediğimiz için "0" dijiti geçildi. Böylelikle yeni maske değerimiz "0022" oldu. Bu da "group" ve "other" kimseler için "w" hakkının maskeleneceği anlamına gelmektedir. Artık bu "shell" prosesinin çalıştırdığı her bir prosesin varsayılan maske değeri "0022" olacaktır. Bizler "022" yerine "22" girseydik de yine sonuç aynı olacaktır çünkü "shell" prosesi en yüksek anlamlı bitlerden girilmeyenleri "0" olarak girildiğini varsayıyor. >> "shell" programının maske değerlerini tamamiyle ortadan kaldırmak için "umask 0" komutunu çalıştırmamız gerekmektedir. Böylece artık bu "shell" programından çalıştırılan bütün proseslerin "umask" değerleri de sıfır olacaktır. Fakat yep yeni bir "shell" programı çalıştırdığımız zaman artık varsayılan "umask" değerlerini kullanacaktır. Kalıcı olarak "shell" programının varsayılan maske değerini değiştirme yöntemlerini ileride göreceğiz("shell" programının "start-up" dosyaları ile oynayarak). >> Prosesler, kendi "umask" değerlerini değiştirmek için, "umask" isimli POSIX fonksiyonunu çağırmaları gerekmektedir. Geri dönüş değeri eski "umask" değeridir. Başarısızlık senaryosu mevcut değildir. "sys/stat.h" isimli başlık dosyasında tanımlanmıştır. Buradan da görüleceği üzere, prosesimizin "umask" değerini elde edebilmek için öncelikle onu değiştirmemiz gerekmektedir. "umask" isimli POSIX fonksiyonuna oktal değerler geçebildiğimiz gibi S_IXXX şeklindeki sembolik sabitleri de argüman olarak geçebiliriz. Fakat çok eski sistemler için hala S_IXXX şeklindeki sembolik sabitleri kullanmalıyız eğer o sistemler standartları takip etmedilerse. Okunabilirliği arttırdığı gerekçesiyle, S_IXXX şeklindeki sembolik sabitlerin argüman olarak geçilmesi tavsiye edilmektedir. Ek bilgi olarak hatırlatmakta fayda vardır; "umask" POSIX fonksiyonuna oktal değer geçilmesi, "rwx" şeklinde üçerli grupların daha rahat ifade edilebilmesinden kaynaklıdır. Oktal değer yerine 16'lık tabanda bir değer de geçilebilir, 10'luk tabanda bir değer de, 2'lik tabanda bir değer de. ÇÜNKÜ ARTIK S_IXXX ŞEKLİNDEKİ SEMBOLİK SABİTLERİN SAYISAL DEĞERLERİ SİSTEMDEN SİSTEME DEĞİŞMEMEKTEDİR. Başka proseslerin "umask" değerlerini herhangi bir fonksiyon üzerinden değiştiremeyiz, spesifik olarak bir prosesinkini yapamayız yani. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); mode_t get_umask_value(void); int main() { /* # OUTPUT # Initial umask : [18] After zero-ing umask : [0] After tweaking umask : [146] Success..! */ printf("Initial umask : [%d]\n", get_umask_value()); umask(0); printf("After zero-ing umask : [%d]\n", get_umask_value()); umask(S_IWUSR | S_IWGRP | S_IWOTH); printf("After tweaking umask : [%d]\n", get_umask_value()); int fd; if( (fd = open("test.txt", O_WRONLY | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO)) == -1) exit_sys("open"); printf("Success..!\n"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } mode_t get_umask_value(void) { mode_t mode = umask(0); umask(mode); return mode; } * Örnek 2, Our Bash Program(version 3.5); Daha önce geliştirdiğimiz "shell" programımıza "umask" komutunun eklenmesi (YÜKSEK ÖNCELİKLİ ÜÇ BİT, YANİ DAHA EVVEL GÖRMEDİKLERİMİZ, ELE ALINMAMIŞTIR. Argüman olarak OKTAL değer geçilmemiştir.) #include "stdio.h" #include "stdlib.h" #include "string.h" #include "unistd.h" #include "limits.h" #include "errno.h" #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 char parse_cmd_line(void); mode_t get_umask_value(void); void dir_proc(void); void clear_proc(void); void pwd_proc(void); void cd_proc(void); void umask_proc(void); int check_umask_arg(const char* str); void exit_sys(const char* msg); typedef struct tagCMD{ char* name; void (*proc)(void); } CMD; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {"cd", cd_proc}, {"umask", umask_proc}, {NULL, NULL} }; char g_cmdline[MAX_CMD_LINE]; char* g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; int main(void) { char* str; int i; if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); for (;;) { fprintf(stdout, "CSD: %s> ", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(); if(g_nparams == 0) continue; if(!strcmp(g_params[0], "exit")) break; for(i = 0; g_cmds[i].name != NULL; ++i) if(!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if(g_cmds[i].name == NULL) fprintf(stderr, "bad command: %s\n", g_params[0]); } return 0; } char parse_cmd_line(void) { char* str; g_nparams = 0; for(str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) { g_params[g_nparams++] = str; } } mode_t get_umask_value(void) { mode_t mode = umask(0); umask(mode); return mode; } void dir_proc(void) { fprintf(stdout, "dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { if (g_nparams > 1) { fprintf(stdout, "pwd command must be used w/o an argument!\n"); return; } fprintf(stdout, "%s\n", g_cwd); } void cd_proc(void) { // DEFAULT if (g_nparams > 2) { printf("Too mang arguments!..\n"); return; } // APPROACH - I if (g_nparams == 1) { printf("Too few arguments!..\n"); return; } // APPROACH - I if (chdir(g_params[1]) == -1) { printf("%s!\n", strerror(errno)); return; } /* // APPROACH - II char* dir; if (g_nparams == 1) { if((dir = getenv("HOME")) == NULL) exit_sys("getenv"); } else dir = g_params[1]; // APPROACH - II if (chdir(dir) == -1) { printf("%s!\n", strerror(errno)); return; } */ // DEFAULT if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); } void umask_proc(void) { if(g_nparams > 2) { printf("Too many arguments for umask command!...\n"); return; } if(g_nparams == 1) { mode_t mode = get_umask_value(); mode_t mode_bits[] = { S_IXOTH, S_IWOTH, S_IROTH, S_IXGRP, S_IWGRP, S_IRGRP, S_IXUSR, S_IWUSR, S_IRUSR, S_ISVTX, S_ISGID, S_ISUID }; unsigned int retval = 0; for(int i = 0; i < 12; ++i) if(mode & mode_bits[i]) retval |= 1 << i; printf("%04o\n", retval); return; } if(g_nparams == 2) { if(!check_umask_arg(g_params[1])) { printf("[%s] octal number out of range!...\n", g_params[1]); return; } unsigned int argval; sscanf(g_params[1], "%o", &argval); mode_t mode = 0; mode_t mode_bits[] = { S_IXOTH, S_IWOTH, S_IROTH, S_IXGRP, S_IWGRP, S_IRGRP, S_IXUSR, S_IWUSR, S_IRUSR, S_ISVTX, S_ISGID, S_ISUID }; for(int i = 0; i < 12; ++i) if(argval >> i & 1) mode |= mode_bits[i]; umask(mode); return; } } int check_umask_arg(const char* str) { if(strlen(str) > 4) return 0; for(int i = 0; str[i] != '\0'; ++i) if(str[i] < '0' || str[i] > '7') return 0; return 1; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, S_IXXX şeklindeki sembolik sabitlerin oktal karşılıkları; ┌────────┬───────────────┬──────────────────────────────────────────────┬─────────────────┐ ├ Name │ Numeric Value │ Description │ Bit-Wise │ ├────────┼───────────────┼──────────────────────────────────────────────┼─────────────────┤ │S_IRWXU │ 0700 │ Read, write, execute/search by owner. │ 000 111 000 000 │ │S_IRUSR │ 0400 │ Read permission, owner. │ 000 100 000 000 │ │S_IWUSR │ 0200 │ Write permission, owner. │ 000 010 000 000 │ │S_IXUSR │ 0100 │ Execute/search permission, owner. │ 000 001 000 000 │ ├────────┼───────────────┼──────────────────────────────────────────────┼─────────────────┤ │S_IRWXG │ 070 │ Read, write, execute/search by group. | 000 000 111 000 │ │S_IRGRP │ 040 │ Read permission, group. │ 000 000 100 000 │ │S_IWGRP │ 020 │ Write permission, group. │ 000 000 010 000 │ │S_IXGRP │ 010 │ Execute/search permission, group. │ 000 000 001 000 │ ├────────┼───────────────┼──────────────────────────────────────────────┼─────────────────┤ │S_IRWXO │ 07 │ Read, write, execute/search by others. │ 000 000 000 111 │ │S_IROTH │ 04 │ Read permission, others. │ 000 000 000 100 │ │S_IWOTH │ 02 │ Write permission, others. │ 000 000 000 010 │ │S_IXOTH │ 01 │ Execute/search permission, others. │ 000 000 000 001 │ ├────────┼───────────────┼──────────────────────────────────────────────┼─────────────────┤ │S_ISUID │ 04000 │ Set-user-ID on execution. │ │ │S_ISGID │ 02000 │ Set-group-ID on execution. │ N / A │ │S_ISVTX │ 01000 │ On directories, restricted deletion flag. │ │ └────────┴───────────────┴──────────────────────────────────────────────┴─────────────────┘ * Örnek 4, Our Bash Program(version 3.7); Sembolik sabitler yerine oktal rakamların kullanıldığı: #include "stdio.h" #include "stdlib.h" #include "string.h" #include "unistd.h" #include "limits.h" #include "errno.h" #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 char parse_cmd_line(void); mode_t get_umask_value(void); void dir_proc(void); void clear_proc(void); void pwd_proc(void); void cd_proc(void); void umask_proc(void); int check_umask_arg(const char* str); void exit_sys(const char* msg); typedef struct tagCMD{ char* name; void (*proc)(void); } CMD; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {"cd", cd_proc}, {"umask", umask_proc}, {NULL, NULL} }; char g_cmdline[MAX_CMD_LINE]; char* g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; int main(void) { /* # INPUT # CSD: /home> umask 22 CSD: /home> umask */ /* # OUTPUT # 0022 */ char* str; int i; if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); for (;;) { fprintf(stdout, "CSD: %s> ", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(); if(g_nparams == 0) continue; if(!strcmp(g_params[0], "exit")) break; for(i = 0; g_cmds[i].name != NULL; ++i) if(!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if(g_cmds[i].name == NULL) fprintf(stderr, "bad command: %s\n", g_params[0]); } return 0; } char parse_cmd_line(void) { char* str; g_nparams = 0; for(str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) { g_params[g_nparams++] = str; } } mode_t get_umask_value(void) { mode_t mode = umask(0); umask(mode); return mode; } void dir_proc(void) { fprintf(stdout, "dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { if (g_nparams > 1) { fprintf(stdout, "pwd command must be used w/o an argument!\n"); return; } fprintf(stdout, "%s\n", g_cwd); } void cd_proc(void) { // DEFAULT if (g_nparams > 2) { printf("Too mang arguments!..\n"); return; } // APPROACH - I if (g_nparams == 1) { printf("Too few arguments!..\n"); return; } // APPROACH - I if (chdir(g_params[1]) == -1) { printf("%s!\n", strerror(errno)); return; } // APPROACH - II char* dir; if (g_nparams == 1) { if((dir = getenv("HOME")) == NULL) exit_sys("getenv"); } else dir = g_params[1]; // APPROACH - II if (chdir(dir) == -1) { printf("%s!\n", strerror(errno)); return; } // DEFAULT if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); } void umask_proc(void) { if(g_nparams > 2) { printf("Too many arguments for umask command!...\n"); return; } if(g_nparams == 1) { printf("%04o\n", (int)get_umask_value()); return; } if(g_nparams == 2) { if(!check_umask_arg(g_params[1])) { printf("[%s] octal number out of range!...\n", g_params[1]); return; } int argval; sscanf(g_params[1], "%o", &argval); umask(argval); return; } } int check_umask_arg(const char* str) { if(strlen(str) > 4) return 0; for(int i = 0; str[i] != '\0'; ++i) if(str[i] < '0' || str[i] > '7') return 0; return 1; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Dosya sistemine ilişkin yardımcı fonksiyonlar: >> "creat" fonksiyonu, "open" fonksiyonunu sarmayalan bir fonksiyondur. İlk UNIX sistemlerinden beridir varolan bir fonksiyondur. Fonksiyonun birinci parametresi, hedef dosyanın yol ifadesini belirtmektedir. İkinci parametresi erişim bilgilerini belirtir. Bu iki argüman ile "open" fonksiyonunu "O_WRONLY | O_CREAT | O_TRUNC" açış modlarıyla açmaktadır. >> Yukarıda işlenen "open", "close", "read", "write" ve "lseeek" fonksiyonlarına ek olarak pek çok yardımcı dosya fonksiyonları da bulunmaktadır. Bu bölümde bu fonksiyonlardan önemli olanlarını tanıtacağız. İş bu yardımcı fonksiyonlar sırasıyla "stat", "fstat" ve "lstat" fonksiyonlarıdır. Aslında bu üçü aynı şeyi farklı parametrik yapılar ile gerçekleştirmektedir. Bu üç fonksiyon bir dosyaya ilişkin bilgileri elde etmek için kullanılırlar. İş bu fonksiyonlar, "ls -l" komutu çalıştırıldığında ekrana çıkan yazıların elde edilmesini sağlayan bir fonksiyonlardır.Fonksiyonların imzaları da şöyledir; #include int stat(const char* pathname, struct stat* statbuf); int fstat(int fd, struct stat *statbuf); int lstat(const char* pathname, struct stat* statbuf); >>> "stat" fonksiyonunun birinci parametresi, bilgisi elde edilecek dosyanın yol ifadesidir. İkinci parametresi ise alınan bilgilerin yerleştirileceği "stat" isimli bir yapı nesnesinin adresidir. Başarı durumunda "0", başarısızlık durumunda "-1" değerine geri dönecektir. * Örnek 1, #include "stdio.h" #include "stdlib.h" #include void exit_sys(const char* msg); int main() { struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "stat" isimli yapının elemanları ise şu şekildedir; struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* i-node number */ mode_t st_mode; /* File type and mode */ nlink_t st_nlink; /* Number of hard links */ uid_t st_uid; /* User ID of owner */ gid_t st_gid; /* Group ID of owner */ dev_t st_rdev; /* Device ID (if special file) */ off_t st_size; /* Total size, in bytes */ blksize_t st_blksize; /* Block size for filesystem I/O */ blkcnt_t st_blocks; /* Number of 512B blocks allocated */ /* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields. For the details before Linux 2.6, see NOTES. */ struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */ #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec }; Bu veri elemanlarından, >>>> "st_dev" elemanı, dosyanın içinde bulunduğu aygıtın aygıt numarasını belirtir. Türü "dev_t" biçiminde olup, herhangi bir tam sayı türü biçiminde olabilir. Genellikle programcılar pek gereksinim duyulmaz. >>>> "st_ino" elemanı, ilgili dosyanın bilgilerini barındıran "i-node" elemanının, "i-node" tablosundaki indis numarasını belirtir. Her dosya için bir "i-node" elemanı vardır ki bu "i-node" elemanları da "i-node" tablosu içerisindedir. Fakat ömrü biten dosyalara ilişkin indis numaraları, başka dosyalar için kullanılabilir. Bütün bu organizasyon ise dosya sistemlerinin disk organizasyonları kapsamındadır. Artık bellekte değil, disk üzerindeki organizasyondan bahsediyoruz. "ls -i" komutu ile bizler bu indis bilgilerine erişebiliriz. Bu elemanın türü "ino_t" biçiminde olup, işaretsiz bir tam sayı biçiminde olmalıdır. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # i-node number: 305691 */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); printf("i-node number: %llu", (unsigned long long)file_info.st_ino); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (13_04_12_2022) > "i-node tablosu" ve "dosya betimleyici tablosu" aslında birbirine çok benzemektedir. Bir tanesi disk tarafı ile ilgilenirken, diğeri bellek tarafıyla ilgilenmektedir. "open" fonksiyonu bir dosyayı açarken "i-node" tablosundan ilgili dosyaya ait bilgileri çekiyor ve bunları dosya betimleyici tablosuna yeni eklenecek olan "file" türden yapının içine yazmaktadır. Benzer şekilde "stat" fonksiyonu da yine "i-node" tablosundan ilgili dosyaya ait bilgileri çekmektedir. > 2008 yılından sonraki sistemlerde, "umask" fonksiyonuna parametre olarak oktal değerler geçebiliriz fakat önceki sistemlerde S_IXXX şeklindeki sembolik sabitleri kullanmalıyız. > Dosya sistemine ilişkin yardımcı fonksiyonlar(devam): >> Bu fonksiyonları sırasıyla "stat", "fstat" ve "lstat" isimli fonksiyonlardır. >>> "stat" fonksiyonunun birinci parametre olan "stat" yapısının elemanları(devam): struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* i-node number */ mode_t st_mode; /* File type and mode */ nlink_t st_nlink; /* Number of hard links */ uid_t st_uid; /* User ID of owner */ gid_t st_gid; /* Group ID of owner */ dev_t st_rdev; /* Device ID (if special file) */ off_t st_size; /* Total size, in bytes */ blksize_t st_blksize; /* Block size for filesystem I/O */ blkcnt_t st_blocks; /* Number of 512B blocks allocated */ /* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields. For the details before Linux 2.6, see NOTES. */ struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */ #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec }; Bu veri elemanlarından, >>>> "st_mode" isimli eleman, dosyanın erişim haklarını ve dosyanın türünü(regular file, directory etc.) içermektedir. Hangi hakların olup olmadığı öğrenmek için, "st_mode & S_IXXX" şeklinde bir "bitwise-AND" işlemi uygulamamız gerekmektedir. Çünkü "st_mode" elemanı, çeşitli bitleri "1" olan bir elemandır. Dosyanın tür bilgisi de yine bu elemanın içerisinde bitsel olarak kodlanmıştır. Fakat bitlerin konumları sistemden sisteme değişmektedir. Dolayısıyla bu bitlerin rakamsal karşılıklarını değil, bu bitlere karşılık gelen fonksiyonel makroları ya da sembolik sabitleri kullanmalıyız. >>>>> "sys/stat.h" içerisindeki "S_ISXXX" biçimindeki makroları kullanmaktır. Bu makrolar, eğer dosya ilgili türden ise "non-zero" bir değer, aksi halde "zero" değer döndürmektedir. Bu makrolar şu şekildedir; S_ISBLK(m) - Test for a block special file. - "ls -l" de 'b' olarak gözükür. S_ISCHR(m) - Test for a character special file. - "ls -l" de 'c' olarak gözükür. S_ISDIR(m) - Test for a directory. - "ls -l" de 'd' olarak gözükür. S_ISFIFO(m) - Test for a pipe or FIFO special file. - "ls -l" de 'p' olarak gözükür. S_ISREG(m) - Test for a regular file. - "ls -l" de '-' olarak gözükür. S_ISLNK(m) - Test for a symbolic link. - "ls -l" de 'l' olarak gözükür. S_ISSOCK(m) - Test for a socket. - "ls -l" de 's' olarak gözükür. >>>>> "sys/stat.h" içerisindeki sembolik sabitleri kullanmaktır. Bu sabitleri KULLANMADAN EVVEL ilk önce "mode_t" türünden olan "mode" değişkenimizi "S_IFMT" ile "bitwise-AND" işlemine sokuyoruz. Çıkan sonuçları da aşağıdaki sembolik sabitlerle "==" işlemine tabii tutuyoruz. S_IFBLK - Block special. S_IFCHR - Character special. S_IFIFO - FIFO special. S_IFREG - Regular. S_IFDIR - Directory. S_IFLNK - Symbolic link. S_IFSOCK - Socket. [Option End] * Örnek 1, #include #include #include void exit_sys(const char* msg); void display_modes(mode_t mode); int main() { /* # OUTPUT # xwrxwrxwr- */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); display_modes(file_info.st_mode); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void display_modes(mode_t mode) { // ACCESS RIGHTS ON THE FILE static mode_t modes[] = { S_IXOTH, S_IWOTH, S_IROTH, S_IXGRP, S_IWGRP, S_IRGRP, S_IXUSR, S_IWUSR, S_IRUSR }; for(int i = 0; i < 9; ++i) printf("%c", modes[i] & mode ? "xwr"[i % 3] : '-'); // TYPE OF THE FILE - v1 static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; for(int i = 0; i < 7; ++i) if( (mode & S_IFMT) == f_types[i] ) { printf("%c", "bcp-dls"[i]); break; } /* // TYPE OF THE FILE - v2 if( S_ISBLK(mode) ) printf("%c", 'b'); else if( S_ISCHR(mode) ) printf("%c", 'c'); else if( S_ISDIR(mode) ) printf("%c", 'd'); else if( S_ISFIFO(mode) ) printf("%c", 'p'); else if( S_ISREG(mode) ) printf("%c", '-'); else if( S_ISLNK(mode) ) printf("%c", 'l'); else if( S_ISSOCK(mode) ) printf("%c", 's'); else fprintf(stderr, "SOMETHING WENT WRONG!...\n"); */ } >>>> "st_nlink" elemanı, dosyanın "hard-link" sayısını belirtmektedir. "hard-link" kavramı ileride ele alınacaktır. Bu tür ("nlink_t") herhangi bir tam sayı türü olabilir. "ls -l" komutundaki, dosya izinlerinden sonra gelen ama "user id" bilgisinden önce gelen kısımdaki, bilgidir bu bilgi. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # hard-link number: 1 */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); printf("hard-link number: %llu", (unsigned long long)file_info.st_nlink); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "st_uid" ve "st_gid" elemanları ise dosyanın sırasıyla Kullanıcı ID ve Grup ID bilgilerini tutmaktadır. "ls -l" komutu ile karşımıza yazı olarak çıkan bilgiler, bu elemanlarda rakam olarak tutulmaktadır. Bu rakamların isim karşılığı ise daha önceki derslerde de anlatıldığı üzere "etc/passwd" ve "etc/group" dosyalarından elde etmektedir. "uid_t" türü herhangi bir tam sayı türü olabilir. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # User ID : 0 Group ID : 0 */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); printf("User ID : %llu\n", (unsigned long long)file_info.st_uid); printf("Group ID : %llu", (unsigned long long)file_info.st_gid); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "st_rdev" elemanı, dosya bir aygıt dosyası ise, temsil ettiği aygıtın numarasını bize vermektedir. "dev_t" türündendir. >>>> "st_size" elemanı ise dosyanın uzunluğunu tutmaktadır. "off_t" türü, daha önceden de belirttiğimiz gibi işaretli bir tam sayı türü olabilir. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* # test.txt # Ulya Yürük */ /* # OUTPUT # File Size : 12 */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); printf("File Size : %lld\n", (long long)file_info.st_size); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "st_blksize" elemanı, dosyamızın kapladığı blok sayısının bayt cinsinden değerini tutmaktadır. Dosyalar diskte bloklar halinde tutulurlar. Diyelim ki dosyamızın 1000 byte olsun. Sistemimizdeki blokların en küçük bayt değeri de 512 şeklindedir. Bu durumda dosyamız diskte iki blokluk yer kaplayacaktır. Geriye kalan 24 baytlık alan ölü alan olacaktır. Dolayısıyla bu eleman 1024 baytlık bir değer tutacaktır. (Ek olarak, 512 bayt büyüklüğü, dosyaları kopyalarkenki efektif "buffer" uzunluğu olarak da kullanılmaktadır.) Yine 1 baytlık bir dosya oluştursak bile, bu yine bir blok içerisinde saklanacaktır ve bloklar daha küçük parçalara bölünemezler. Windows sistemlerde bunu şöyle de görebiliriz; "Dosyanın büyüklüğü" ve "Dosyanın diskte kapladığı alan". Buradan hareketle diyebiliriz ki 10 adet 10 baytlık dosyalar, 1 adet 100 baytlık dosyadan daha fazla disk üzerinde yer kaplarlar. Bu elemanın türü işaretli bir tam sayı türü olabilir. Bu eleman 512'nin katları şeklinde değer tutmaktadır. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # Block Size : 4096 */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); printf("Block Size : %lld\n", (long long)file_info.st_blksize); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "st_blocks" elemanı, dosyanın kapladığı blok sayısını belirtmektedir. Örneğin, 1000 baytlık bir dosya disk üzerinde 3 blok yer kaplayacaktır eğer her bir blok 512 baytlık ise. İşte 2 rakamını bu eleman tutmaktadır. Bu elemanın türü de işaretli bir tam sayı türü olmalıdır. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # Block Amount : 8 */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); printf("Block Amount : %lld\n", (long long)file_info.st_blocks); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> UNIX/Linux sistemleri bir dosya için üç tane tarih/zaman bilgisi tutmaktadır. Bunlardan ilki son yazma zamanı, ikincisi son okuma zamanı ve üçüncüsü ise dosyanın "i-node" bilgilerinin en son ne zaman değiştirildiği bilgisidir. Microsoft sistemlerinde dosyanın ilk hayata geldiği tarih/zaman bilgisi tutulurken, UNIX/Linux sistemlerinde böyle bir bilgi tutulmamaktadır. "write" fonksiyonu işini başarılı bir şekilde yaptığında en son yazma zamanı bilgisini, "read" fonksiyonu en son okuma zamanı bilgisini değiştirirken, "chmod" komutu ise dosyanın "i-node" bilgilerinin en son ne zaman değiştirildiği bilgisini değiştirir. Ek olarak "write" fonksiyonu ile dosyanın uzunluğunu değiştirirsek, ilgili dosyanın "i-node" bilgilerini de değiştirmiş olacağız. "stat" yapısının "st_atim", "st_mtim" ve "st_ctim" isimli elemanları sırasıyla "time of last access", "time of modification" ve "time of last status change" bilgilerini tutmaktadır. 2008 öncesi POSIX standartlarda bu elemanların isimleri "st_atime", "st_mtime" ve "st_ctime" şeklindeydi. Eski elemanların türler "time_t" şeklindeyken, yeni standartlarda tür "timespec" türünden. Eskiye dönük uyumluluğu korumak adına da, şu anki isimlerin içindeki bir takım elemanlar, eski isimler ile tekrardan "typedef" edilmiştir. struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */ #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec Eski standartlarda bu üç eleman "01/01/1970" tarihinden itibaren geçen saniye sayısını tutmaktaydılar ki C dilinde tarihin bu şekilde olması bir zorunluluk değildir, yeni standartlardan itibaren, yeni isimleriyle birlikte, çözünürlülüğünü nanosaniye çektiler. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # Last Modification : 09/12/2022 18:22:52 Last Access : 09/12/2022 18:22:52 Last I-node Changed : 09/12/2022 18:22:52 */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); struct tm* time_info; time_info = localtime(&file_info.st_mtim.tv_sec); printf( "Last Modification : %02d/%02d/%04d %02d:%02d:%02d\n", time_info->tm_mday, time_info->tm_mon + 1, time_info->tm_year + 1900, time_info->tm_hour, time_info->tm_min, time_info->tm_sec ); time_info = localtime(&file_info.st_atim.tv_sec); printf( "Last Access : %02d/%02d/%04d %02d:%02d:%02d\n", time_info->tm_mday, time_info->tm_mon + 1, time_info->tm_year + 1900, time_info->tm_hour, time_info->tm_min, time_info->tm_sec ); time_info = localtime(&file_info.st_ctim.tv_sec); printf( "Last I-node Changed : %02d/%02d/%04d %02d:%02d:%02d\n", time_info->tm_mday, time_info->tm_mon + 1, time_info->tm_year + 1900, time_info->tm_hour, time_info->tm_min, time_info->tm_sec ); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "ls -l" komutunun temsili bir implementasyonu, * Örnek 1, #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char* msg); void my_ls_command(struct stat* file_info, const char* file_name); int main() { /* # test.txt # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # [-xwrxwrxwr 1 0 0 25 Dec 9 19:57 test.txt] */ struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); my_ls_command(&file_info, "test.txt"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void my_ls_command(struct stat* file_info, const char* file_name) { static char buffer[BUFFER_SIZE]; static mode_t modes[] = { S_IXOTH, S_IWOTH, S_IROTH, S_IXGRP, S_IWGRP, S_IRGRP, S_IXUSR, S_IWUSR, S_IRUSR }; static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; int index = 0; for(int i = 0; i < 7; ++i) if( (file_info->st_mode & S_IFMT) == f_types[i] ) { buffer[index++] = "bcp-dls"[i]; break; } for(int i = 0; i < 9; ++i) buffer[index++] = modes[i] & file_info->st_mode ? "xwr"[i % 3] : '-'; index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_nlink); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_uid); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_gid); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_size); struct tm* ptime; ptime = localtime(&file_info->st_mtim.tv_sec); index += strftime(buffer + index, BUFFER_SIZE, " %b %2e %H:%M", ptime); sprintf(buffer + index, " %s", file_name); printf("[%s]", buffer); } >>> "fstat" fonksiyonu, "stat" fonksiyonunun aksine dosyanın yol ifadesini değil, dosya betimleyicisini argüman olarak almaktadır. "open" fonksiyonu halihazırda ilgili dosyanın yol ifadesini kullanarak, dosyaya ait bilgileri diskten çekmektedir. Dolayısıyla dosyaya ait bilgileri tekrardan yol ifadesi kullanarak almak, dosya betimleyicisi kullanarak almaktan daha yavaş bir yöntemdir. Burada vurgulanmak istenen nokta şudur; başka bir amaç için halihazırda açık olan bir dosyanın bilgilerine "fstat" ile çok daha hızlı bir şekilde erişebiliriz. Ama önce bir dosyayı açıp, sonra ona "fstat" uygulamamız anlamsız olacaktır. * Örnek 1, #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char* msg); void my_ls_command(struct stat* file_info, const char* file_name); int main() { /* # test.txt # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # [-xwrxwrxwr 1 0 0 25 Dec 9 19:58 test.txt] */ int fd; if((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); struct stat file_info; if(fstat(fd, &file_info) == -1) exit_sys("stat"); my_ls_command(&file_info, "test.txt"); close(fd); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void my_ls_command(struct stat* file_info, const char* file_name) { static char buffer[BUFFER_SIZE]; static mode_t modes[] = { S_IXOTH, S_IWOTH, S_IROTH, S_IXGRP, S_IWGRP, S_IRGRP, S_IXUSR, S_IWUSR, S_IRUSR }; static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; int index = 0; for(int i = 0; i < 7; ++i) if( (file_info->st_mode & S_IFMT) == f_types[i] ) { buffer[index++] = "bcp-dls"[i]; break; } for(int i = 0; i < 9; ++i) buffer[index++] = modes[i] & file_info->st_mode ? "xwr"[i % 3] : '-'; index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_nlink); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_uid); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_gid); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_size); struct tm* ptime; ptime = localtime(&file_info->st_mtim.tv_sec); index += strftime(buffer + index, BUFFER_SIZE, " %b %2e %H:%M", ptime); sprintf(buffer + index, " %s", file_name); printf("[%s]", buffer); } >>> Sembolik Bağlantı Dosyaları aslında bir gösterici dosyasıdır. Bu dosyanın "i-node" elemanı sadece hangi dosyayı gösterdiğini belirtmektedir. Windows sistemlerindeki kısayol uygulamalarına benzemektedir. Diskte bu dosya için bir yer ayrılmamakta, sadece bir adet "i-node" elemanı oluşturuluyor. Eğer "stat" ya da "fstat" fonksiyonlarına bu dosyayı geçersek, bu dosyanın gösterdiği dosyanın bilgilerini çekeriz. "lstat" fonksiyonuna bu dosyayı geçersek, işte bu dosyanın kendisine dair bilgileri çekeriz. Unutulmamalıdır ki "stat" ve "fstat", arada kaç tane sembolik bağlantı dosyayı olursa olsun, her daim en son gösterilen esas dosyaya erişmektedir. "open" fonksiyonu da benzer şekilde bağlantı linkini izleyecektir. > Hatırlatıcı Notlar, >> "open" fonksiyonu sadece "regular" dosyalar oluşturmaktadır. >> "internal fragmantation", dosyaların disk üzerinde kapladıkları alanlardaki ölü alanlardır. Örneğin, 512 bayt büyüklüğünde bloklarımız olsun ve 12 baytlık bir dosya oluşturalım. Geriye kalan 500 baytlık alan ölü alan olacaktır ve bu isimle anılacaktır. Örneğin, 10 adet 10 bayt büyüklüğünde dosyamız olsun. Blok büyüklüğü de 512 bayt olsun. Normal şartlarda bu on dosya, on adet bloğu kaplayacaktır. Her bir blok içerisindeki 502 baytlık kısım ölü alan olacaktır. İşte, bu on dosyayı uç uca getirerek tek bir dosya haline getirilmesine "tar" işlemi denmektedir. Böylece bu yeni dosya sadece bir blok kaplayacağı için, disk üzerinde yeni yerler kazanmış olacağız. Fakat bu on dosya uç uca getirildiğinde, bunların bilgilerini tutmak için de yeni alanlar tahsis edilebilinir. Bu neden ile bu yeni dosyanın büyüklüğü 100 bayttan geçkin olacaktır. "zip" işlemi ise var olan dosyaların sıkıştırılmasına denir. Genel olarak önce "tar", sonra "zip" işlemi uygulanır. "zip" işlemi aslında dosyayı daha az bayt ile temsil etmektir. /*================================================================================================================================*/ (14_10_12_2022) > Dosya sistemine ilişkin yardımcı fonksiyonlar(devam): >> Bu fonksiyonlar sırasıyla "stat", "fstat" ve "lstat" isimli fonksiyonlardır. >>> "lstat" fonksiyonu(devam): * Örnek 1, Aşağıdaki örnek çevrimiçi gcc derleyicisi kullanılarak çalıştırılmıştır. İlgili dosya bir sembolik bağlantı dosyası DEĞİLDİR. #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char* msg); void my_ls_command(struct stat* file_info, const char* file_name); int main(void) { /* # test.txt # */ /* # OUTPUT # [-xwrxwrxwr 1 0 0 0 Dec 15 13:08 test.txt] */ struct stat file_info; if(lstat("test.txt", &file_info) == -1) exit_sys("stat"); my_ls_command(&file_info, "test.txt"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void my_ls_command(struct stat* file_info, const char* file_name) { static char buffer[BUFFER_SIZE]; static mode_t modes[] = { S_IXOTH, S_IWOTH, S_IROTH, S_IXGRP, S_IWGRP, S_IRGRP, S_IXUSR, S_IWUSR, S_IRUSR }; static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; int index = 0; for(int i = 0; i < 7; ++i) if( (file_info->st_mode & S_IFMT) == f_types[i] ) { buffer[index++] = "bcp-dls"[i]; break; } for(int i = 0; i < 9; ++i) buffer[index++] = modes[i] & file_info->st_mode ? "xwr"[i % 3] : '-'; index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_nlink); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_uid); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_gid); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_size); struct tm* ptime; ptime = localtime(&file_info->st_mtim.tv_sec); index += strftime(buffer + index, BUFFER_SIZE, " %b %2e %H:%M", ptime); sprintf(buffer + index, " %s", file_name); printf("[%s]", buffer); } >>>> Sembolik Bağlantı Dosyaları üzerine; >>>>> Geçen derste de anlatıldığı üzere, bir dosyayı işaret eden dosyalara sembolik bağlantı dosyaları denmektedir. Bu dosyalara aynı zamanda "soft-link" dosyalar da denmektedir. >>>>> Sembolik bağlantı dosyaları adeta bir "pointer" dosyalardır. >>>>> İşletim sistemleri, bu tip dosyalar için, diskte yalnızda bir "i-node" elemanı tutmaktadır. Komut satırından "ln -s" ile sembolik bağlantı dosyaları oluşturulabilir. >>>>> Bu tip dosyalar "ls -l" komutunda "->" işareti ile gösterilmektedir. Yine bu tip dosyaların tipleri, "ls -l" komutunda "l" harfi ile gösterilmektedir. >>>>> Ek olarak, bir sembolik bağlantı dosyası, başka bir sembolik bağlantı dosyasını da gösterebilir. >>>>> BİR İŞLETİM SİSTEMİNDE, BİR DOSYANIN YOL İFADESİNİ ARGÜMAN OLARAK ALAN FONKSİYONLARIN HEMEN HEPSİ SEMBOLİK BAĞLANTILARI TAKİP EDER. YANİ SEMBOLİK BAĞLANTI DOSYASININ KENDİSİNİ DEĞİL, GÖSTERDİĞİ DOSYAYI ELE ALIR. Örneğin, "lstat" fonksiyonu izlemez fakat "open" fonksiyonu izlemektedir. >>>>> Sembolik bağlantı dosyasının gösterdiği dosyayı silmemiz durumunda, bizim sembolik bağlantı dosyamız artık "dangling symbolic-link file" olarak geçer. Böyle bir dosyayı açmaya çalıştığımız zaman da sanki hiç var olmamış bir dosyayı açarken karşılaşacağımız "errno" hatası ile karşılaşırız. Çünkü bağlantının işaret ettiği bir dosya mevcut değildir. >>>>> Sembolik bağlantı dosyaları, Windows sistemlerindeki kısayol dosyalarına çok benzemektedir. >>>>> "a" isimli sembolik bağlantı dosyası, "b" dosyasını gösteriyor olsun. "b" de "c" isimli sembolik bağlantı dosyasını gösteriyor olsun. Bu durumuna "loop" durumu denmektedir. Bir dosya fonksiyonuna(ama temel ama yardımcı), bu dosyalardan birisinin yol ifadesi ya da "File Description" değeri argüman olarak geçildiğinde, iş bu fonksiyonlar "errno" değerini uygun değer ile değiştiriyorlar. Çünkü çoğu dosya fonksiyonu, "lseek" fonksiyonu istisnai bir fonksiyondur çünkü bu fonksiyon sembolik bağlantıları İZLEMEMEKTEDİR, sembolik bağlantıları İZLEMEKTEDİR. ORTADA BİR SONSUZ DÖNGÜ SÖZ KONUSU OLDUĞUNDAN, SİSTEM ÇÖKEBİLİR. "errno" değişkeninin değeri de "ELOOP" biçimindedir. >>> Komut satırından "stat" ve "fstat" komutlarını da çağırarak ilgili dosyalara ait bilgileri çekebiliriz. * Örnek 1, "stat sample.c" Size: 2832, Dosyanın büyüklüğü Blocks: 8, Dosyanın kaç blok kapladığı IO Blocks, 4096, Dosyanın kapladığı blokların byte cinsinden karşılığı Inode: 1207667, Dosyanın "i-node" numarası //... >> "unlink" ve "remove" fonksiyonları: Bu iki fonksiyon da dosya silmek için kullanılan fonksiyonlardır. Her iki fonksiyon da aynı şeyi yapmaktadır fakat "unlink" fonksiyonu bir POSIX fonksiyonuyken, "remove" fonksiyonu ise standart bir C fonksiyonudur. Her iki fonksiyon da silinecek dosyaya ait bir yol ifadesini argüman olarak almaktadır ve başarı durumunda "0" ile başarısızlık durumunda ise "-1" ile geri dönmektedir. "remove" fonksiyonu "stdio.h" başlık dosyasında bildirilmiş, "unlink" ise "unistd.h" başlık dosyasında.Daha önce de belirtildiği gibi bir dosyayı silebilmek için prosesimizin o dosya üzerinde "w" hakkı değil, ilgili dosyanın içinde bulunduğu dizinde "w" hakkına sahip olması gerekmektedir. Buradan hareketle silinecek dosyanın sahibi konumunda olmayan fakat dosyanın bulunduğu dizinde "w" hakkına sahip olan prosesler de silme işlemini gerçekleştirebilir(Process ID değeri "0", yani "root", olan prosesler herhangi bir kontrole tabii tutulmadan direkt olarak dosyayı silebilirler). "ls -l" komutunu çalıştırdığımız zaman karşımıza çıkan bilgilerden "hard-link" bilgisi, dosyanın ne zaman fiziksel olarak silineceğini göstermektedir. Bu bir sayaç gibidir. "hard-link" adedi sıfıra olduğunda dosya fiziksel olarak silinir. Ek olarak bizler "open" ile açtığımız fakat kapatmadığımız dosyaları da silebiliriz. Bu durumda ilgili dosyamız ilk önce "dizin girişi" denilen bölgeden silinmekte, ilgili dosya tamamiyle kapatıldıktan sonra da fiziksel olarak silinmektedir(Birden fazla prosesin silinecek olan dosyayı açması durumunda, bütün proseslerin bu dosyayı kapatması gerekmektedir. İşte bunları tutan sayaca da "hard-link" sayacı denmektedir). Kapalı olan bir dosyanın silinmesi de aynı şekildedir; ilk önce dizin girişinden silinirler ve "hard-link" sayacı bir eksiltilir. Eğer bu sayaç "0" olursa dosya fiziksel olarak da silinir. "hard-link" sayacının detaylarına ileride değineceğiz. * Örnek 1,Yol İfadelerinde ismi verilen dosyaların silinmesi: //.. #include #include #include int main(int argc, char* argv[]) { /* # INPUT # test.txt x.dat */ /* # OUTPUT # */ if(argc == 1) { fprintf(stderr, "file name(s) must be specified!...\n"); exit(EXIT_FAILURE); } for(int i = 1; i < argc; ++i) { if(unlink(argv[i])== -1) { perror("unlink"); } } return 0; } >>> Dizin Girişleri(Directory Entry): >>>> Bu girişler, ilgili dizinin içerisinde bulunmaktadır ve formatları dosya sisteminden dosya sistemine değişebilmektedir. >>>> Dizin Girişleri, ilgili dizinde bulunan dosyalara ait "Dosya İsmi - I-Node Numaraları" çiftlerini tutmaktadır. >>>>> Bir dosya silindiği zaman ilk olarak, Dizin Girişindeki ilgili dosyaya "Dosya İsmi - I-Node Numara" çifti silinmektedir. Fakat dosyanın fiziksel olarak silinip silinmeyeceği, ilgili dosyaya ait "hard-link" numarasına bağlıdır. İlgili "i-node" elemanını kaç adet dizin girişi gösterirse, o dosyanın "hard-link" sayacı da o kadar oluyor. * Örnek 1, Directory A I-Node Table (File Name) - (I-Node No) ... a.txt 187181 ... 187181 Directory B ... (File Name) - (I-Node No) ... b.txt 187181 ... İki adet "a.txt" ve "b.txt" adında dosyamız olsun. Görüldüğü üzere bu iki dosyanın da "i-node" numarası aynı. Dosyaların "i-node" numaraları birbiri ile aynı olduğu için, "open" fonksiyonu ile "a.txt" ye ait yol ifadesi geçmekle "b.txt" ye ait yol ifadesini geçmenin arasında bir fark yoktur. İkisi de günün sonunda, "187181" numarasına karşılık gelen dosyayı açacaktır. Bu durumda iki farklı dizin girişi, tek bir "i-node" elemanını gösteriyordur. İşte bu dosyanın "hard-link" rakamı da iki olacaktır. Ama "remove" ile ama "unlink" fonksiyonu ile bir dosya silindiğinde, ilk olarak dizin girişinden silinme yapılmakta ve ilgili dosyaya ait "hard-link" numarası da bir azaltılmakta. Yukarıdaki örneğin baz alırsak, "a.txt" dosyasını silersek, "hard-link" numarası artık bir oldu. "b.txt" dosyası hala aynı "i-node" elemanına sahip olduğundan, ilgili dosya fiziksel olarak silinmedi. Nihayet "b.txt" dosyasını da silersek, "hard-link" numarası sıfır olacağından, dosya tamamiyle siliniyor. Aslında tamamen silinmekten kastedilen şey şudur; Daha önce de belirttiğimiz gibi dosyalar bloklardan oluşmaktadır. Bir dosyanın "i-node" elemanı o dosyanın disteki hangi blokları kullandığı bilgisini tutmaktadır. Ek olarak iki blok daha vardır. Bunlardan bir tanesi hangi "i-node" numarasının kullanımda, hangisinin boş olduğunu bilgisini tutmaktadır ki buna "i-node Bitmap" denir. Diğeri ise diskteki hangi blokların kullanımda, hangilerinin boş olduğu bilgisini tutmaktadır ki buna "Block Bitmap" denir. Bir dosya tamamiyle silindi dediğimizde, kendisine ait "i-node" elemanının içerisi SİLİNMİYOR. SADECE "i-node Bitmap" ve "Block Bitmap" içerisindeki ilgili indeksler boşaltılıyor. Böylelikle işletim sistemi gerçek manada silme işlemi yerine, ilgili alanın üzerine yazma için müsait olduğu bilgisini tutuyor. GERÇEK MANADA SİLMEK İÇİN, İLGİLİ DOSYAYI AÇIP BİZZAT BİZ SİLMELİYİZ. >>>> Yeni bir dizin oluşturduğumuz zaman genellikle "hard-link" sayacını iki olarak buluruz. Çünkü yeni bir dizin hayata geldiğinde "." ve ".." isminde iki dizin daha oluşturuluyor. Daha önce de anlatıldığı üzere "." dizinin kendisini, ".." ise bir üst dizini göstermektedir (Yani "." dizininin "i-node" numarası ile dizinimizin "i-node" numarası aynı olurken, ".." dizininin "i-node" numarası ile kendi dizinimizin bir üst dizininin "i-node" numarası aynı olmaktadır. Fakat "." ve ".." dizinleri "ls -l" komutu ile görüntülenmeyecektir. "-a" seçeneğini de "ls" komutuna eklememiz gerekiyor ki bu iki dizini görüntüleyebilelim. "-i" seçeneğini de kullanırsak, "i-node" numaralarını da göreceğiz.). Bir dizin içerisinde yeni bir dizin hayata getirdiğimiz zaman, ".." dizininden dolayı "hard-link" sayacını arttıracaktır çünkü ".." dizini bir üst dizinden "i-node" numarasını almaktadır. >>> "hard-link" ve "soft-link": >>>> Sembolik link çıkartmak için "ln -s" komutu kullanılmaktadır. Örneğin, "ln -s x.dat y.dat" şeklindeki bir komut çalıştırıldığında, "y.dat" isimli dosya, "x.dat" isimli dosyaya bağlanmış olmaktadır. Burada esas dosya "x.dat" şeklinde, sembolik link dosyası da "y.dat" şeklindedir. Yani "y.dat" dosyası, "x.dat" dosyasını göstermektedir. >>>> "hard-link" çıkartmak için de "ln" komutunu herhangi bir "switch" olmadan kullanmamız gerekmektedir. Örneğin, "ln sample.c x.c" komutunu çalıştırırsak, "i-node" numaraları aynı iki dosya meydana getiririz. Bu iki dosya birbirinin eşleniği olacaktır çünkü ikisi de aynı "i-node" numarasına sahiptir. Burada yeni oluşturulan dosya bir "pointer" dosya DEĞİLDİR. Canlı kanlı bir dosyadır. >>>> "hard-link" sayacı, kaç farklı dosyanın aynı "i-node" numarasına sahip olduğunu tutan bir sayaçtır. Fakat "soft-link" ise bir dosyanın diğer dosyası "point" etmesi durumudur. >> "chmod" fonksiyonu; dosyanın erişim hakları, diskteki ilgili "i-node" elemanı içerisindedir. "stat" fonksiyonu ile bu bilgileri belleğe aktarıp "ls" komutu ile de ekrana yazdırıyoruz. Bir dosya ilk hayata getirilirken de "open" fonksiyonuna bu erişim hakları argüman olarak geçilmektedir. "chmod" POSIX fonksiyonu ile bu erişim haklarını daha sonra değiştirebiliriz. Bu POSIX fonksiyonu ile dosyanın içerisine dokunmadan sadece erişim hakları üzerinde bir değişiklik yapılmaktadır. İş bu fonksiyonumuz "sys/stat.h" içerisinde tanımlanmış olup birinci parametresi ilgili dosyaya ait yol ifadesiyken, ikinci parametresi ise "mode_t" türünden erişim haklarıdır. Başarı durumunda fonksiyon "0", başarısızlık durumunda "-1" ile geri dönmektedir. Fonksiyonun ikinci parametresine, 2008 sonrası sistemlerde, oktal değer de verebiliriz fakat öncesindeki sistemlerde S_IXXX sembolik sabitlerini "bit-wise OR" işlemiyle kombine ederek vermemiz gerekmektedir(tıpkı "open" fonksiyonuna erişim haklarının bilgisini geçer gibi ama tavsiye edilen yöntem yine S_IXXX şeklindeki sembolik sabitlerin kullanılması). "chmod" fonksiyonu ile bir dosyanın erişim haklarını değiştirebilmek için ya o dosya ile aynı "owner" grupta olmalıyız ya "root" kullanıcısı olmalıyız ya da bir takım özel haklara sahip olmalıyız. Aksi halde işlem yapmamız mümkün değildir. "owner" grupta olmak için prosesin ve dosyanın Etkin Kullanıcı ID bilgilerinin birbiri ile aynı olması gerekmektedir. Dosyanın dördüncü üç bitlik erişim hakları olan "S_ISUID", "S_ISGID" ve "S_ISVTX" hakları da yine bu fonksiyon ile değiştirilmeye çalışılabilir. Fakat bazı sistemler "S_ISUID" ve "S_ISGID" erişim hakları değiştirmeye izin vermeyebilir. Yine "chmod" isimli bir "shell" komutu da bulunmaktadır ki bu da aslında "chmod" isimli fonksiyon kullanılarak yazılmıştır. Yine bu "shell" komutunda da erişim hakları oktal dijitler ile belirtilmektedir. Örneğin, "chmod 664 a.txt" şeklinde bir "shell" komutu çalıştıralım. Buradaki "664" oktal sayısının karşılığı, yani üçerli bit halindeki açılmışı, "110 110 100" biçimindedir. Bu da şu anlama gelmektedir;"rw-rw-r--". Fakat buradaki kritik nokta, prosesimizin Etkin Kullanıcı ID değeri ile "a.txt" dosyasının id değeri birbirinin aynısı olmalıdır ya da bizler "root" kullanıcısı olmalıyız (sudo chmod 664 a.txt). Bu komutu örnekte olduğu gibi oktal sayılar ile birlikte kullanırsak, prosesin "umask" değeri DEVREYE GİRMEYECEKTİR. Bu "shell" komutunun diğer kullanım biçiminde de "owner", "group" ya da "other" gruplarından birisine ya da bir kaçına spesifik bir yetki vermede ya da çıkarmada kullanılır. Örneğin, "chmod u+w x.dat" dediğimiz sadece "user" grubuna "w" hakkı ver demektir fakat bu şekilde kullanırsak prosesin "umask" değeri de DEVREYE GİRECEKTİR. "chmod u-w x.dat" ise sadece "user" grubundan "w" hakkını kaldır demektir. "group" ve "other" için ekleme ve çıkarma için sırasıyla "g+w" / "g-w" ve "o+w" / "o-w" şeklinde kullanmalıyız. "a+w" dersek "owner", "group" ve "other" kısımlarının hepsine "w" hakkı ver demektir. "a-w" ise bütün herkesten "w" hakkının silinmesi anlamına gelmektedir. "chmod +w x.dat" şeklindeki bir komut ise bütün gruplara "w" hakkı eklemektedir. Yine bu komutu kullanırken diğer hakları da kombine edebiliriz. Örneğin, "chmod ug+wr x.dat" dediğimiz zaman "user" ve "group" kişilerine "w" ve "r" hakkı verilecektir. Son olarak "chmod" komutunu "=" ile birlikte kullanabiliriz. Örneğin, "chmod g=rw x.dat" dediğimiz zaman "x.dat" dosyasının sadece grup bilgileri için "r" ve "w" hakkı verilecektir. BU ŞEKİLDEKİ BİR KULLANIMDA DA PROSESİN "umask" DEĞERİ DEVREYE GİRMEZ. "chmod" fonksiyonu, prosesin "umask" değerlerinden etkilenmemektedir. Bu nedenle, bu fonksiyona geçilen haklar doğrudan ilgili dosyaya yansıtılmaktadır. Fakat "open" fonksiyonunda durum böyle değildir; arada "umask" değerleri ile filtreleme yapılmaktadır. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int check_umask_arg(const char* str); int main(int argc, char* argv[]) { if(argc < 3) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } /* * "argv[1]", yeni "umask" değerleri. */ if(!check_umask_arg(argv[1])) { fprintf(stderr, "invalid mode: %s\n", argv[1]); exit(EXIT_FAILURE); } int mode; sscanf(argv[1], "%o", &mode); for(int i = 2; i < argc; ++i) if(chmod(argv[i], mode) == -1) fprintf(stderr, "cannot change mode: %s\n", argv[i]); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } int check_umask_arg(const char* str) { if(strlen(str) > 4) return 0; for(int i = 0; str[i] != '\0'; ++i) if(str[i] < '0' || str[i] > '7') return 0; return 1; } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int check_umask_arg(const char* str); void display_modes(mode_t mode); int main(int argc, char* argv[]) { // error handling section if(argc < 3) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } if(!check_umask_arg(argv[1])) { fprintf(stderr, "invalid mode: %s\n", argv[1]); exit(EXIT_FAILURE); } // print the initial "st_mode" struct stat file_info; if(stat("test.txt", &file_info) == -1) exit_sys("stat"); display_modes(file_info.st_mode); // change the "st_mode" int modeval; mode_t mode; mode_t modes[] = { S_ISUID, S_ISGID, S_ISVTX, S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH }; sscanf(argv[1], "%o", &modeval); for(int i = 11; i >= 0; --i) if(modeval >> i & 1) mode |= modes[11 - i]; for(int i = 2; i < argc; ++i) if(chmod(argv[i], mode) == -1) fprintf(stderr, "cannot change mode: %s\n", argv[i]); // print the last "st_mode" if(stat("test.txt", &file_info) == -1) exit_sys("stat"); display_modes(file_info.st_mode); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } int check_umask_arg(const char* str) { if(strlen(str) > 4) return 0; for(int i = 0; str[i] != '\0'; ++i) if(str[i] < '0' || str[i] > '7') return 0; return 1; } void display_modes(mode_t mode) { // TYPE OF THE FILE - v2 if( S_ISBLK(mode) ) printf("%c", 'b'); else if( S_ISCHR(mode) ) printf("%c", 'c'); else if( S_ISDIR(mode) ) printf("%c", 'd'); else if( S_ISFIFO(mode) ) printf("%c", 'p'); else if( S_ISREG(mode) ) printf("%c", '-'); else if( S_ISLNK(mode) ) printf("%c", 'l'); else if( S_ISSOCK(mode) ) printf("%c", 's'); else fprintf(stderr, "SOMETHING WENT WRONG!...\n"); } >> "chown" fonksiyonu, bir dosyanın Kullanıcı ID ve Grup ID değerlerini değiştirmek için kullanılır. Örneğin, "x.dat" isminde bir dosyamız olsun. Bu dosyanın Kullanıcı ID değeri "kaan", Grup ID değeri ise "study" olsun. Bu dosyanın Kullanıcı ID değerini "ali" olarak değiştirmemiz, aslında dosyayı oluşturan kişinin "ali" olduğu algısı oluşturmaktadır. Bu da "ali" kullanıcısını zan altında bırakabilir. Böyle bir kötü niyetli kullanım sezdiklerinden, bazı sistemler bu fonksiyonun kullanılmasına müsaade etmemektedir. "_POSIX_CHOWN_RESTRICTED" makrosunun ilgili sistemde tanımlı olup olmadığını sorgulayarak, hangi sistemlerin bu fonksiyonun kullanımına izin verdiğini öğrenebiliriz fakat günümüzdeki çoğu sistemde bu makro TANIMLIDIR(iş bu makro da "unistd.h" başlık dosyasında tanımlanmıştır). Eğer prosesimizin Etkin Kullanıcı ID değeri, ilgili dosyanın Kullanıcı ID değeri ile aynıysa, bu fonksiyon çağrısı sonucunda dosyanın Grup ID değeri prosesin kendi Grup ID değeri ya da prosesin kendi ek Grup ID değerlerinden birisi olarak DEĞİŞTİRİLİR. BAMBAŞKA BİR Grup ID DEĞERİ İLE YİNE DEĞİŞTİREMEYİZ. BAHSİ GEÇEN KISITLAMA SADECE PROSESİN ETKİN KULLANICI ID DEĞERİ İLE DOSYANIN KULLANICI ID DEĞERİNİN FARKLI OLMASI DURUMUNDA GEÇERLİDİR. Bu fonksiyon da yine "unistd.h" isimli başlık dosyasında bildirilmiştir. Birinci parametresi ilgili dosyaya ait olan yol ifadesi, ikinci parametresi ve üçüncü parametresi ise sırasıyla yeni Kullanıcı ID ve yeni Grup ID değerleridir. Eğer prosesimizin Etkin Kullanıcı ID değeri "0" ise, herhangi bir kısıtlamaya maruz kalmadan dosyanın kullanıcı ve Grup ID değerlerini rahatlıkla değiştirebilir. Ek olarak bu fonksiyon ile yalnızda kullanıcı ya da Grup ID değeri de değiştirilebilir. Bu durumda değiştirmek İSTEMEDİKLERİMİZ için "-1" değerini geçmeliyiz. Fonksiyonumuz başarı durumunda "0", başarısızlık durumunda ise "-1" ile geri dönmektedir. POSIX fonksiyonuna ek olarak "chown" isimli bir "shell" komutu daha vardır. Bu komut ile yine dosyaların Kullanıcı ID ve Grup ID değerlerini değiştirebiliriz. "sudo chown kaan:studt test.txt" komutu ile "test.txt" dosyasının Kullanıcı ID ve Grup ID değerlerini sırasıyla "kaan" ve "studt" haline getirmiş oluruz. "sudo chown kaan test.txt" komutu ile sadece Kullanıcı ID, "sudo chown :studt test.txt" ile de sadece Grup ID değerlerini DEĞİŞTİRİRİZ. * Örnek 1, #include #include int main(int argc, char* argv[]) { /* # OUTPUT # chown IS RESTRICTED */ #ifdef _POSIX_CHOWN_RESTRICTED printf("chown IS RESTRICTED\n"); #else printf("chown IS [NOT] RESTRICTED\n"); #endif } * Örnek 2, (AŞAĞIDAKİ PROGRAMI "sudo" İLE ÇALIŞTIRIRSAK, İŞLEM BAŞARILI OLACAKTIR) #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # chown: Operation not permitted */ if(chown("test.txt", 1001, -1) == -1) exit_sys("chown"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar, >> Dizin Girişinden silinen bir dosyanın "hard-link" sayacını görmek için üçüncü parti "utility" programlar kullanmamız gerekiyor. Eğer silinen bu dosya o "i-node" numarasına sahip son dosya ise, dosya fiziksel olarak da silineceği için, iş bu sayacı görmemiz imkansızdır. Aynı "i-node" numarasına sahip dosyaları görüntülemek için pratik bir yöntem mevcut değildir. Öyle ya da böyle, diskte tek tek dosyalar aranarak bulunmaktadır. >> Editör programları üzerinden "Save As" dediğimiz zaman "hard-link" oluşturulmamaktadır. Çünkü "Save As" demekle aslında birbirinden bağımsız iki dosya oluşturmak istemekteyiz. >> "du" komutu ile gösterilen rakam 1024 baytlık bloklar şeklinde hesaplanması sonucu elde edilen rakamdır. /*================================================================================================================================*/ (15_11_12_2022) > Dosya sistemine ilişkin yardımcı fonksiyonlar(devam): >> "fchown" fonksiyonu, "chown" fonksiyonunun "File Description" alan versiyonudur. Dolayısıyla halihazırda açılmış bir dosya var ise onun "fd" değerini bu fonksiyona geçebiliriz. "chown" fonksiyonundaki kısıtlamalar da yine bu fonksiyon için de geçerlidir. Her iki fonksiyon arasındaki tek fark, birisinin "id" değeri almasıyken diğerinin "fd" değeri alması. Buradan "fd" ile kastedilen şey "open" fonksiyonunun geri dönüş değeridir. >> "fchmod" fonksiyonu, "chmod" fonksiyonunun "File Description" alan versiyonudur. Dolayısıyla halihazırda açılmış bir dosya var ise onun "fd" değerini bu fonksiyona geçebiliriz. Yine "chmod" ile aynı başlık dosyasında bildirilmişlerdir. >> "mkdir" fonksiyonu, bir dizin oluşturmak için kullanılan bir fonksiyondur. Çünkü "open" fonksiyonu ile bir dizin değil, sadece "regular file" oluşturabiliriz. "sys/stat.h" başlık dosyasında bildirilmiş olup, ilk parametresi oluşturulacak dizinin yol ifadesidir. İkinci parametresi de ilgili dizinin erişim haklarıdır. Tıpkı "creat" fonksiyonunda olduğu gibi, bu fonksiyona geçilen erişim hakları da ilgili prosesin "umask" değerinden etkilenecektir. Eğer prosesin "umask" değerinden etkilenmemesini istiyorsak, ilgili prosesin "umask" değerini evvelce sıfıra çekmeliyiz. Başarı durumunda iş bu fonksiyon "0" ile, hata durumunda ise "-1" ile dönmektedir. Anımsayacağımız üzere dizinlerdeki "x" hakkı, yol ifadelerinin açılması sırasında ilgili dizinin içinden geçilebilirlik anlamına gelmektedir. Dolayısıyla bir dizin oluştururken bu dizine "x" hakkının verilmesi önem arz etmektedir. Bir dizin hayata getirdiğimiz zaman, yeni oluşturulan bu dizinin içinde iki tane daha dizin otomatik olarak oluşturulur. Bu dizinle "." ve ".." dizinleri. "." dizini yeni oluşturduğumuz dizinin bulunduğu dizinin "i-node" elemanını işaret ederken, ".." dizini ise yeni oluşturulan dizinin bulunduğu dizinin bir üst dizininin "i-node" elemanını işaret eder. Böylelikle "." ve ".." dizinleri ilgili "i-node" elemanlarındaki "hard-link" sayaçlarını (yani dizinimizin bulunduğu dizinin ve onun bir üst dizininin "hard-link" sayaçları) da bir arttıracaktır. "mkdir" POSIX fonksiyonun ek olarak "mkdir" isimli bir "shell" komutu daha vardır ki bu komut da yine aynı isimli fonksiyon kullanılarak yazılmıştır. İş bu komut ile yeni dizinler oluşturabiliriz. Prosesin "umask" değeri, bu komutun varsayılan kullanım biçiminde, dizinin erişim haklarını etkilemektedir. Bunu önlemek için ilgili komutu "-m" ya da "--mode" seçenekleri ile birlikte kullanabiliriz. İş bu seçenekler ile birlikte kullandığımız zaman, erişim haklarını oktal sayılar ile belirtebiliriz. Örneğin, "mkdir xxx" ve "mkdir -m 777 yyy". * Örnek 1, //.. #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # Command line arguments: test Success!... */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if(mkdir(argv[1], S_IRWXU | S_IRWXG | S_IRWXO) == -1) exit_sys("mkdir"); printf("Success!...\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte prosesin "umask" değeri sıfıra ÇEKİLMEMİŞTİR. #include #include #include #include void exit_sys(const char* msg); void display_modes(mode_t mode); mode_t get_umask_value(void); int main(int argc, char* argv[]) { /* # OUTPUT # Command line arguments: Ahmopasa umask : [22] permissions : [777] Success!... drwxr-xr-x */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } /* @ umask: 000 010 010 */ printf("umask : [%o]\n", get_umask_value()); /* @ S_IRWXU: 111 000 000 @ S_IRWXG: 000 111 000 @ S_IRWXO: 000 000 111 @ directory_permissions: 111 111 111 */ mode_t directory_permissions = S_IRWXU | S_IRWXG | S_IRWXO; printf("permissions : [%o]\n", directory_permissions); /* @ umask : 000 010 010 @ umask' : 111 101 101 @ directory_permissions : 111 111 111 @ (umask') & (directory_permissions) : 111 101 101 @ : rwx r-x r-x */ if(mkdir(argv[1], directory_permissions) == -1) exit_sys("mkdir"); printf("Success!...\n"); struct stat file_info; if(stat(argv[1], &file_info) == -1) exit_sys("stat"); display_modes(file_info.st_mode); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void display_modes(mode_t mode) { // TYPE OF THE FILE - v1 static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; for(int i = 0; i < 7; ++i) if( (mode & S_IFMT) == f_types[i] ) { printf("%c", "bcp-dls"[i]); break; } /* // TYPE OF THE FILE - v2 if( S_ISBLK(mode) ) printf("%c", 'b'); else if( S_ISCHR(mode) ) printf("%c", 'c'); else if( S_ISDIR(mode) ) printf("%c", 'd'); else if( S_ISFIFO(mode) ) printf("%c", 'p'); else if( S_ISREG(mode) ) printf("%c", '-'); else if( S_ISLNK(mode) ) printf("%c", 'l'); else if( S_ISSOCK(mode) ) printf("%c", 's'); else fprintf(stderr, "SOMETHING WENT WRONG!...\n"); */ // RIGHTS ON THE FILE static mode_t modes[] = { S_IXOTH, S_IWOTH, S_IROTH, S_IXGRP, S_IWGRP, S_IRGRP, S_IXUSR, S_IWUSR, S_IRUSR }; for(int i = 8; i >= 0; --i) printf("%c", modes[i] & mode ? "xwr"[i % 3] : '-'); } mode_t get_umask_value(void) { mode_t mode = umask(0); umask(mode); return mode; } * Örnek 3, Aşağıdaki örnekte prosesin "umask" değeri SIFIRLANMIŞTIR. #include #include #include #include void exit_sys(const char* msg); void display_modes(mode_t mode); mode_t get_umask_value(void); int main(int argc, char* argv[]) { /* # OUTPUT # Command line arguments: Ahmopasa umask : [0] permissions : [777] Success!... drwxrwxrwx */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } umask(0); /* @ umask: 000 000 000 */ printf("umask : [%o]\n", get_umask_value()); /* @ S_IRWXU: 111 000 000 @ S_IRWXG: 000 111 000 @ S_IRWXO: 000 000 111 @ directory_permissions: 111 111 111 */ mode_t directory_permissions = S_IRWXU | S_IRWXG | S_IRWXO; printf("permissions : [%o]\n", directory_permissions); /* @ umask : 000 000 000 @ umask' : 111 111 111 @ directory_permissions : 111 111 111 @ (umask') & (directory_permissions) : 111 111 111 @ : rwx rwx rwx */ if(mkdir(argv[1], directory_permissions) == -1) exit_sys("mkdir"); printf("Success!...\n"); struct stat file_info; if(stat(argv[1], &file_info) == -1) exit_sys("stat"); display_modes(file_info.st_mode); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void display_modes(mode_t mode) { // TYPE OF THE FILE - v1 static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; for(int i = 0; i < 7; ++i) if( (mode & S_IFMT) == f_types[i] ) { printf("%c", "bcp-dls"[i]); break; } /* // TYPE OF THE FILE - v2 if( S_ISBLK(mode) ) printf("%c", 'b'); else if( S_ISCHR(mode) ) printf("%c", 'c'); else if( S_ISDIR(mode) ) printf("%c", 'd'); else if( S_ISFIFO(mode) ) printf("%c", 'p'); else if( S_ISREG(mode) ) printf("%c", '-'); else if( S_ISLNK(mode) ) printf("%c", 'l'); else if( S_ISSOCK(mode) ) printf("%c", 's'); else fprintf(stderr, "SOMETHING WENT WRONG!...\n"); */ // RIGHTS ON THE FILE static mode_t modes[] = { S_IXOTH, S_IWOTH, S_IROTH, S_IXGRP, S_IWGRP, S_IRGRP, S_IXUSR, S_IWUSR, S_IRUSR }; for(int i = 8; i >= 0; --i) printf("%c", modes[i] & mode ? "xwr"[i % 3] : '-'); } mode_t get_umask_value(void) { mode_t mode = umask(0); umask(mode); return mode; } >> "rmdir" fonksiyonu, bir dizin silmek için çağrılması gereken bir fonksiyondur. Bir dizin silmek için "unlink" ya da "remove" isimli FONKSİYONLARI KULLANAMAYIZ. Bu fonksiyon da standart bir POSIX fonksiyonudur. İş bu fonksiyon "unistd.h" içerisinde bildirilmiştir. Tek parametre olarak silinecek dizinin yol ifadesini alır. Başarı durumunda "0" ile, başarısızlık durumunda "-1" ile geri döner. Fakat içi dolu bir dizini BU FONKSİYON İLE SİLEMEYİZ. BU FONKSİYON İLE DİZİN SİLME İŞLEMİ YAPABİLMEK İÇİN İLGİLİ DİZİNİN İÇİNİN BOŞ OLMASI GEREKMEKTEDİR("." ve ".." DİZİNLERİ GENELLİKLE SİLİNEMEDİĞİ İÇİN BUNLAR KAPSAM DIŞIDIR). Bir dizinin tamamen silinebilmesi için, tıpkı "regular file" da olduğu gibi, "hard-link" sayacının sıfıra düşmesi gerekmektedir. Fakat bir dizinin "hard-link" sayacının ÇIKARTILMASI TAVSİYE EDİLMEZ. ÇÜNKÜ BU DURUM, DİZİN AĞACI DOLAŞAN KODLARIN SONSUZ DÖNGÜYE GİRMESİNE NEDEN OLABİLİR. Bunun yerine "soft-link" çıkartmalıyız. İş bu fonksiyona argüman olarak bir sembolik bağlantı dosyasının yol ifadesi geçilirse, iş bu fonksiyon bağlantıyı TAKİP ETMEYECEKTİR ve "errno" değerini de "ENOTDIR" biçiminde değiştirecektir. İş bu fonksiyonun başarılı olabilmesi için, prosesin silinecek dizin üzerinde "w" hakkına sahip olması gerekli değildir. Sadece silinecek dizinin bulunduğu dizin üzerinde bu hakka sahip olması gerekmektedir. Daha önce de bu konuya değinmiştik. "shell" programı üzerinden bir dizin silmek için de "rmdir" isimli bir komut da vardır. Bu komut da aynı isimli POSIX fonksiyon kullanılarak yazılmıştır. Yine bu komut ile silme yapabilmek için de dizinimizin boş olması gerekmektedir. Fakat içi dolu bir dizini tek hamlede silmek için "rm" komutunu "-r" seçeneği ile birlikte kullanılmalıdır. Örneğin, "rm -r xxx". * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # Command line arguments: Ahmopasa Success! The directory is being created... Success! The directory is being deleted... */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if(mkdir(argv[1], S_IRWXU | S_IRWXG | S_IRWXO) == -1) exit_sys("mkdir"); else printf("Success! The directory is being created...\n"); if(rmdir(argv[1]) == -1) exit_sys("rmdir"); else printf("Success! The directory is being deleted...\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "etc/passwd" dosyası içerisindeki bilgilerin çekilmesi: >>> Tipik bir "etc/passwd" dosyası aşağıdaki biçimdedir (tipik bir "etc/passwd" dosyasının her bir satırı bir kullanıcıya tekabül etmektedir ve her bir satır ":" ile ayrılmış kelimelerde meydana gelmektedir): ... ahmopasa:x:1000:1000:Ahmet Kandemir PEHLİVANLI,,,:/home/ahmopasa:/bin/bash ahmo:$6$DcUtZx5u4mUVFQfA$wSGIC2szvgP5zeos4YoaVEbVPbnAZxsSuydIUl9K0xy8DhfPo4hWROBFGm7.xWu5hq/uPVKDIvADYsX6q905Q0:18736:0:99999:7::: ali:x:1001:1001::/home/ali:/bin/my"shell" vali:x:1002:1002::/home/vali:/bin/my"shell" ... Buradan hareketle diyebiliriz ki; Satır başı ile ilk ":" ayracı arasındaki kısım kullanıcının ismi, Birinci ":" ile ikinci ":" arasındaki kısım kullanıcının şifresi ki 'x' olması durumunda ilgili şifre "etc/shadow" dosyasında kaydedilmiştir, İkinci ":" ve üçüncü ":" arasındaki kısım kullanıcının id bilgisi ki bu kullanıcının çalıştıracağı prosesler bu id değerine sahip olacaktır, Üçüncü ":" ile dördüncü ":" arası kullanıcının Grup ID bilgisi ki bu kullanıcının çalıştıracağı prosesler bu Grup ID değerine sahip olacaktır fakat grup isminin ne olduğu "etc/group" dosyası içerisindedir, Dördüncü ":" ile beşinci ":" arasındaki kısım kullanıcının kişisel bilgilerinin olduğu alan, Beşinci ":" ile altıncı ":" arasındaki alan ise çalıştırılacak "bash" programının "Current Working Directory" bilgisi, Altıncı ":" ile satır sonu arasındaki alan ise çalıştırılacak "bash" programını belirtmektedir. >>> UNIX/Linux sistemlerinde kullanıcı bilgilerinin "etc/passwd" ve "etc/group" içerisinde saklanması standart değildir fakat yaygın biçimi bu şekildedir. Bunun üstesinden gelebilmek için de standart POSIX fonksiyonları geliştirilmiştir. İş bu fonksiyonlar "pwd.h" başlık dosyası içerisinde bildirilmiştir. İş bu fonksiyonlar, >>>> "getpwnam" fonksiyonu, bir kullanıcının ismini argüman olarak alır ve o kullanıcı hakkındaki bilgileri "etc/passwd" dosyasından çeker. Başarı durumunda fonksiyonun geri dönüş değeri, statik ömürlü bir yapının başlangıç adresidir. Statik ömürlü olması hasebiyle bellekte yer tahsisi ile uğraşmak durumunda değiliz (YAPININ KENDİSİNİ DÖNDÜRMEMEKTEDİR). İş bu yapının türü "passwd" türüdür. İlgili kullanıcı bulunamadığında veya herhangi başka bir hata durumunda fonksiyon NULL ile dönmektedir. Fakat ilgili kullanıcı kaydı bulunamazsa, "errno" değişkenini DEĞİŞTİRİLMEZ. Buradan hareketle öncelikle "errno" değişkeni sıfıra çekilir ve iş bu fonksiyonunun geri dönüş değerinin NULL olup olmadığına bakılır. Sonrasında da "errno" değişkeninin değerinin sıfır olup olmadığı kontrol edilir. Eğer geri dönüş değeri NULL ise ama "errno" sıfır değil ise patolojik bir sorun vardır, aksi halde ilgili kullanıcı bulunamamıştır. >>>>> "struct passwd" türü aşağıdaki elemanlardan oluşmaktadır. Her bir eleman, sırasıyla "etc/passwd" içerisindeki ilgili satırda ":" ile ayrılan bölümlere hitap etmektedir. struct passwd{ char *pw_name, // User's login name. char *pw_passwd, // User's password. uid_t pw_uid, // Numerical user ID. gid_t pw_gid, // Numerical group ID. char *pw_gecos, // User Personal Information char *pw_dir, // Initial working directory of the program that will be used as "shell". char *pw_"shell", // Program to use as "shell". }; * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # Command line arguments: Ahmopasa ------------------------------------------- root x 0 0 root /root /bin/bash ------------------------------------------- */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } struct passwd* pass; errno = 0; if((pass = getpwnam(argv[1])) == NULL) { if(errno == 0) { fprintf(stderr, "such user name cannot be found!...\n"); exit(EXIT_FAILURE); } else { exit_sys("getpwnam"); } } puts("-------------------------------------------"); printf("%s\n", pass->pw_name); printf("%s\n", pass->pw_passwd); printf("%llu\n", (unsigned long long)pass->pw_uid); printf("%llu\n", (unsigned long long)pass->pw_gid); printf("%s\n", pass->pw_gecos); printf("%s\n", pass->pw_dir); printf("%s\n", pass->pw_"shell"); puts("-------------------------------------------"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "getpwuid" fonksiyonu ise kullanıcının id bilgisini bizden almaktadır. "getpwnam" fonksiyonundan başka bir farklılığı yoktur. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # Command line arguments: 0 ------------------------------------------- root x 0 0 root /root /bin/bash ------------------------------------------- */ /* # OUTPUT # Command line arguments: 1 ------------------------------------------- daemon x 1 1 daemon /usr/sbin /usr/sbin/nologin ------------------------------------------- */ /* # OUTPUT # Command line arguments: 100 ------------------------------------------- _apt x 100 65534 /nonexistent /usr/sbin/nologin ------------------------------------------- */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } struct passwd* pass; errno = 0; if((pass = getpwuid(atoi(argv[1]))) == NULL) { if(errno == 0) { fprintf(stderr, "such user name cannot be found!...\n"); exit(EXIT_FAILURE); } else { exit_sys("getpwnam"); } } puts("-------------------------------------------"); printf("%s\n", pass->pw_name); printf("%s\n", pass->pw_passwd); printf("%llu\n", (unsigned long long)pass->pw_uid); printf("%llu\n", (unsigned long long)pass->pw_gid); printf("%s\n", pass->pw_gecos); printf("%s\n", pass->pw_dir); printf("%s\n", pass->pw_"shell"); puts("-------------------------------------------"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "getpwent" fonksiyonu ise her çağrımda sıradaki kullanıcının bilgilerini geri döndürmektedir. Bu fonksiyonu kullanarak bir döngü içerisinde bütün kullanıcılara dair bilgiyi çekebiliriz. Fakat işimiz bittiğinde, "endpwent" fonksiyonunu çağırmalıyız ki "etc/passwd" dosyasını kapatabilelim. Eğer tekrardan dosyanın başından okumak istiyorsak, "setpwent" fonksiyonunu çağırmalıyız. "getpwent" fonksiyonu da "getpwnam" fonksiyonunun geri dönüş özelliklerine sahiptir. İşin başında "setpwent" fonksiyonunun çağrılması GEREKMEMEKTEDİR. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # root x 0 0 root /root /bin/bash ------------------------------------------- ... ------------------------------------------- */ struct passwd* pass; /* ',' operatörü önce sol tarafı işletir fakat sonucu ıskarta eder. Daha sonra sağ tarafı işletir ve elde ettiği sonuc operatörün geri döndürdüğü değerdir. Dolayısıyla iş bu operatörün geri dönüş değeri, ilgili sağ operanttır. Yani 'NULL' karşılaştırmasına "pass" değişkeni girecektir fakat aynı zamanda her turda "errno" değişkeni sıfıra çekilmektedir. */ while((errno = 0, pass = getpwent()) != NULL) { printf("%s\n", pass->pw_name); printf("%s\n", pass->pw_passwd); printf("%llu\n", (unsigned long long)pass->pw_uid); printf("%llu\n", (unsigned long long)pass->pw_gid); printf("%s\n", pass->pw_gecos); printf("%s\n", pass->pw_dir); printf("%s\n", pass->pw_"shell"); puts("-------------------------------------------"); } if(errno != 0) exit_sys("getpwent"); endpwent(); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "etc/group" dosyası içerisindeki bilgilerin çekilmesi: >>> Aynı mekanizma ve aynı mantık grup bilgileri için de oluşturulmuştur. Tipik bir "etc/group" dosyası aşağıdaki biçimdedir (tipik bir "etc/group" dosyasının her bir satırı bir gruba tekabül etmektedir ve her bir satır ":" ile ayrılmış kelimelerde meydana gelmektedir): ... adm:x:4:syslog,ahmopasa ahmopasa:x:1000: sambashare:x:132:ahmopasa ... Buradan hareketle diyebiliriz ki; Satır başı ile ilk ":" ayracı arasındaki kısım grubun ismi, Birinci ":" ile ikinci ":" arasındaki kısım grubun şifresi, İkinci ":" ve üçüncü ":" arasındaki grubun id numarası, Üçüncü ":" ile dördüncü ":" arası iş bu gruba dahil olan kullanıcıların isimleir (yani "supplementary group" bölümü). >>> Grup bilgilerini çeken fonksiyonlar "getgrnam", "getgrgid", "getgrent", "setgrent" ve "endgrent" fonksiyonlarıdır ve "grp.h" başlık dosyasında bildirilmişlerdir. Bu fonksiyonlardan, >>>> "getgrnam" ve "getgrgid" fonksiyonları argüman olarak sırasıyla grup ismi ve Grup ID değerini alırlar ve geriye "group" türünden statik ömürlü bir yapının başlangıç adresini döndürürler. >>>>> "struct group" yapısı aşağıdaki elemanlarda oluşmaktadır ve her bir eleman "etc/group" içerisindeki ilgili satırdaki ilgili bölüme dair elemanları tutar. struct group{ char *gr_name /* The name of the group. */ char *gr_passwd /* The password of the group. */ gid_t gr_gid /* Numerical group ID. */ char **gr_mem /* Pointer to a NULL-terminated array of character pointers to member names. (Bu dizinin son indeksi NULL)*/ }; >>>> "getgrent" fonksiyonu ise her çağırmada sıradaki grubun bilgilerini geri döndürmektedir. >>>> "endgrent" fonksiyonu ise "etc/group" dosyasını kapatmak için kullanılır. İşimiz bittikten sonra bu fonksiyonu da çağırmalıyız. >>>> "setgrent" ise tekrardan baştan okuma yapmak için çağırmamız gereken bir fonksiyondur. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # root x 0 ------------------------------------------- ... ------------------------------------------- */ struct group* pass; while((errno = 0, pass = getgrent()) != NULL) { printf("%s\n", pass->gr_name); printf("%s\n", pass->gr_passwd); printf("%llu\n", (unsigned long long)pass->gr_gid); for(int i = 0; pass->gr_mem[i] != NULL; ++i) { if(i != 0) printf(", "); printf("%s", pass->gr_mem[i]); } puts("-------------------------------------------"); } if(errno != 0) exit_sys("getpwent"); endgrent(); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar, >> İsminin başında "f" dosya işlem fonksiyonları, "f" olmayanların, parametre olarak "File Description" alan versiyonlarıdır. Örneğin, "chown" ve "fchown", "stat" ve "fstat". Bu tip "File Description" alan fonksiyonlar, almayan versiyonlarına nazaran daha hızlı bir çalışma sunmaktadır eğer halihazırda açılmış bir dosya varsa. Fakat "f" li versiyonlar daha az kullanılırlar. >> Bu derse kadar gördüğümüz dosya fonksiyonlarını iki gruba ayırabiliriz; >>> Temel Dosya Fonksiyonları "open", "close", "read" "write" ve "lseek" fonksiyonlarıdır. >>> Yardımcı Dosya Fonksiyonları "unlink" / "remove", "chmod" / "fchmod", "stat" / "fstat" / "lstat", "chown" / "fchown", "mkdir" / "rmdir" fonksiyonlarıdır. >> UNIX/Linux, macOS ve Windows sistemlerinde "." ve ".." dizinleri SİLİNEMEMEKTEDİR. >> Unutmamalıyız ki bir çok "shell" komutu, aynı isimli POSIX fonksiyonu kullanılarak yazılmıştır. >> Bir dizinin boş olması, içinde sadece "." ve ".." dizinlerinin olması demektir. >> Hiç bir POSIX fonksiyonu "errno" değerini "0" değerine ÇEKMEZ. >> Bazı POSIX fonksiyonları, başarılı olmalarına rağmen, "errno" değişkeninin değerini DEĞİŞTİRİRLER. >> "shell" komutu ile yeni bir kullanıcı eklerken, ilgili ismin daha önce alınıp alınmadığı kontrol edilmekte fakat elle yapılan kullanıcı eklemelerinde böyle bir kontrol yoktur. Dolayısıyla ilk gördüğü ismin bilgileri çekilecektir. Ama "Undefined Behaviour" meydana gelecektir. >> Hiç bir sistemde kullanıcının şifresi direkt olarak saklanmaz. O şifrenin şifrelenmiş hali saklanır. Karşılaştırma yapılırken bu şifrelenmiş versiyonlar karşılaştırılır. >> "etc/passwd" dosyasına yazma yapan bir POSIX fonksiyonu YAZILMAMIŞTIR. >> Sistemin, bir kullanıcının hangi gruplara dahil olduğunu tespit edebilmesi için, "etc/group" isimli dosyayı baştan sonra taraması ve ilgili kullanıcı isminin geçtiği grup isimlerini belirlemesi gerekiyor. >> "Advanced Programming In The UNIX Environment" ve "The Linux Programming Interface" isimli kitapları da takviye olarak kullanabiliriz. >> "getpwent" fonksiyonunu implemente ederken bir adet statik ömürlü 4096 karakter uzunluğunda dizi oluşturalım. "fgets" ile "etc/passwd" içerisindeki her bir satırı sırayla okuyup ilgili bu diziye yazalım. "strtok" ile bu diziden de "parse" işlemi yapalım. Elde ettiğimiz her bir bilgisi de statik ömürlü "struct passwd" nin elemanlarına atayalım. İlgili bu yapının başlangıç adresi, statik ömürlü dizinin başlangıç adresini göstermeli. /*================================================================================================================================*/ (16_17_12_2022) > Dosya sistemine ilişkin yardımcı fonksiyonlar(devam): >> "etc/passwd" ve "etc/group" dosyalarından kullanıcıların id bilgilerinin çekilerek, daha önce yazdığımız "my_ls_command" isimli fonksiyonda kullanılması: Normal şartlarda bir kullanıcının bilgileri "etc/passwd" ve "etc/group" dosyalarından silindiğinde, bu kullanıcının oluşturduğu dosyaların da silinmesi GEREKMEZ. Komut satırı üzerinden silme işlemi yaparken ekstra seçenek kullanmalı, el ile silme işlemi yaparken de bizler el ile ilgili dosyaları silmeliyiz. Tabii silinmesi gerekiyorsa. Bu durumda eğer kullanıcıya dair bir bilgi "etc/passwd" içerisinde bulunamaz ise ekrana ilgili Kullanıcı ID'nin rakamsal değerini, bulunması durumunda ise bu id'ye karşılık gelen kullanıcı ismini ekrana yazdırmalıyız. * Örnek 1, "my_ls_command" isimli fonksiyonun güncellenmiş hali: #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char* msg); void my_ls_command(struct stat* file_info, const char* file_name); int main(void) { /* # test.txt # */ /* # OUTPUT # [-rwxrwxrwx 1 0 0 0 Jan 6 23:16 test.txt] */ struct stat file_info; if(lstat("test.txt", &file_info) == -1) exit_sys("stat"); my_ls_command(&file_info, "test.txt"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void my_ls_command(struct stat* file_info, const char* file_name) { static char buffer[BUFFER_SIZE]; static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH }; static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; int index = 0; for(int i = 0; i < 7; ++i) if( (file_info->st_mode & S_IFMT) == f_types[i] ) { buffer[index++] = "bcp-dls"[i]; break; } for(int i = 0; i < 9; ++i) buffer[index++] = modes[i] & file_info->st_mode ? "rwx"[i % 3] : '-'; struct passwd* pw; char uname[BUFFER_SIZE]; if((pw = getpwuid(file_info->st_uid)) == NULL) sprintf(uname, "%llu", (unsigned long long)file_info->st_uid); else strcpy(uname, pw->pw_name); struct group* gr; char gname[BUFFER_SIZE]; if((gr = getgrgid(file_info->st_gid)) == NULL) sprintf(gname, "%llu", (unsigned long long)file_info->st_gid); else strcpy(gname, gr->gr_name); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_nlink); index += sprintf(buffer + index, " %s", uname); index += sprintf(buffer + index, " %s", gname); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_size); struct tm* ptime; ptime = localtime(&file_info->st_mtim.tv_sec); index += strftime(buffer + index, BUFFER_SIZE, " %b %2e %H:%M", ptime); sprintf(buffer + index, " %s", file_name); printf("[%s]", buffer); } * Örnek 2, Our Bash Program(version 4): #include #include #include #include #include #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 #define BUFFER_SIZE 4096 char parse_cmd_line(void); void dir_proc(void); void clear_proc(void); void pwd_proc(void); void cd_proc(void); void ls_proc(void); void exit_sys(const char* msg); typedef struct tagCMD{ char* name; void (*proc)(void); } CMD; CMD g_cmds[] = { {"dir", dir_proc}, {"clear", clear_proc}, {"pwd", pwd_proc}, {"cd", cd_proc}, {"ls", ls_proc}, {NULL, NULL} }; char g_cmdline[MAX_CMD_LINE]; char* g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; int main(void) { char* str; int i; if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); for (;;) { fprintf(stdout, "CSD: %s> ", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(); if(g_nparams == 0) continue; if(!strcmp(g_params[0], "exit")) exit(EXIT_SUCCESS); for(i = 0; g_cmds[i].name != NULL; ++i) if(!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if(g_cmds[i].name == NULL) fprintf(stderr, "bad command: %s\n", g_params[0]); } return 0; } char parse_cmd_line(void) { char* str; g_nparams = 0; for(str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) { g_params[g_nparams++] = str; } g_params[g_nparams] = NULL; } void dir_proc(void) { fprintf(stdout, "dir command executing...\n"); } void clear_proc(void) { system("clear"); } void pwd_proc(void) { if (g_nparams > 1) { fprintf(stdout, "pwd command must be used w/o an argument!\n"); return; } fprintf(stdout, "%s\n", g_cwd); } void cd_proc(void) { // DEFAULT if (g_nparams > 2) { printf("Too mang arguments!..\n"); return; } // APPROACH - I if (g_nparams == 1) { printf("Too few arguments!..\n"); return; } if (chdir(g_params[1]) == -1) { printf("%s!\n", strerror(errno)); return; } // APPROACH - II /* char* dir; if (g_nparams == 1) { if((dir = getenv("HOME")) == NULL) exit_sys("getenv"); } else dir = g_params[1]; if (chdir(dir) == -1) { printf("%s!\n", strerror(errno)); return; } */ // DEFAULT if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); } void ls_proc(void) { if(g_nparams != 1) { fprintf(stderr, "wrong number of arguments!...\n"); return; } DIR* dir; if( (dir = opendir(g_cwd)) == NULL) exit_sys("opendir"); int fd; if( (fd = dirfd(dir)) == -1 ) exit_sys("dirfd"); struct dirent* de; struct stat file_info; while(errno = 0, (de = readdir(dir)) != NULL) { /* * Dördüncü parametreye "0" geçilmesi durumunda, "stat" semantiği uygulanacaktır. "AT_SYMLINK_NOFOLLOW" sembolik sabiti "fcntl.h" içerisinde bildirilmiştir. */ if(fstatat(fd, de->d_name, &file_info, AT_SYMLINK_NOFOLLOW) == -1) exit_sys("fstatat"); // my_ls_command(&info, de->d_name); static char buffer[BUFFER_SIZE + 1]; static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH }; static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; int index = 0; for(int i = 0; i < 7; ++i) if( (file_info.st_mode & S_IFMT) == f_types[i] ) { buffer[index++] = "bcp-dls"[i]; break; } for(int i = 0; i < 9; ++i) buffer[index++] = modes[i] & file_info.st_mode ? "rwx"[i % 3] : '-'; struct passwd* pw; char uname[BUFFER_SIZE]; if((pw = getpwuid(file_info.st_uid)) == NULL) sprintf(uname, "%llu", (unsigned long long)file_info.st_uid); else strcpy(uname, pw->pw_name); struct group* gr; char gname[BUFFER_SIZE]; if((gr = getgrgid(file_info.st_gid)) == NULL) sprintf(gname, "%llu", (unsigned long long)file_info.st_gid); else strcpy(gname, gr->gr_name); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info.st_nlink); index += sprintf(buffer + index, " %s", uname); index += sprintf(buffer + index, " %s", gname); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info.st_size); struct tm* ptime; ptime = localtime(&file_info.st_mtim.tv_sec); index += strftime(buffer + index, BUFFER_SIZE, " %b %2e %H:%M", ptime); sprintf(buffer + index, " %s", de->d_name); printf("[%s]\n", buffer); } if(errno != 0) exit_sys("readdir"); closedir(dir); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> Dizin dosyalarının "open" fonksiyonu ile açılması: Anımsayacağımız üzere dizinler de birer dosya olarak ele alınmaktadır. Dosya sistemleri de dizinlerin "open" fonksiyonu ile açılmasını mümkün kılmışlar fakat açılan bu dizinlerde okuma ve yazma işlemlerini işletim sistemlerinin insifiyatına bırakmışlardır. Günümüzde çoğu Unix türevi işletim sistemi, dizinlerin "open" ile açılmasına izin vermekte fakat bu dizinlerden "read" ve "write" fonksiyonlarını kullanarak okuma ve yazma işlemi yapmamıza izin vermemektedir. Fakat "lseek" işleminin, açılan bu dosya üzerinde yapılmasına MÜSAADE VARDIR. (Dizin dosyalarının gerçek formatları dosya sisteminden dosya sistemine değişkenlik göstermektedir. Kursun sonlarına doğru Ext-2 dosya sistemi de incelenecektir.) Bu anlatımda, proseslerin dizin üzerinde gerekli izinlere sahip olduğu varsayılmıştır. Dizin dosyalarını "open" ile açarken "read" ve "write" yapamayacağımızdan bahsetmiştik. Peki "open" ile açarken hangi açış modunu kullanacağız? İşte POSIX standartlarında bunun için "O_SEARCH" isimli bir mod geliştirilmiştir. Aslında bu mod, ileride ele alınacak olan, at'li POSIX (openat vs.) fonksiyonları için düşünülmüştür. Bu açış modu kullanılarak açılan dizinler üzerinde "read" ve "write" işlemleri yapılamaz sadece iş bu at'li fonksiyonlarda kullanılabilir ("open" fonksiyonunun geri dönüş değeri olan "fd" yi bu tip fonksiyonlara geçerek). BU MOD LINUX & macOS İŞLETİM SİSTEMLERİNDE DESTEKLENMEDİĞİNDEN, BU İŞLETİM SİSTEMİNDE "O_RDONLY" MODUNU KULLANMALIYIZ. NOT: İŞLETİM SİSTEMLERİ "open" İLE AÇILAN DİZİNLERDE OKUMA VE YAZMA İŞLEMLERİNE "read" VE "write" FONKSİYONLARI ÜZERİNDEN İZİN VERMEMEKTEDİR. BAŞKA FONKSİYONLARI BU İŞLEMLER İÇİN KULLANABİLİRİZ. * Örnek 1, "open" fonksiyonunun "0_RDONLY" modu ile açılması: #include #include #include #include void exit_sys(const char* msg); int main(void) { // OUTPUT => Success!... int fd; if( (fd = open(".", O_RDONLY)) == -1) { exit_sys("open"); } printf("Success!...\n"); close(fd); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, "open" fonksiyonunun "O_WRONLY" modu ile açılması: #include #include #include #include void exit_sys(const char* msg); int main(void) { // OUTPUT => open: Is a directory int fd; if( (fd = open(".", O_WRONLY)) == -1) { exit_sys("open"); } printf("Success!...\n"); close(fd); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, "open" fonksiyonunun "O_SEARCH" modu ile açılması: #include #include #include #include void exit_sys(const char* msg); int main(void) { // OUTPUT => error: ‘O_SEARCH’ undeclared (first use in this function) int fd; if( (fd = open(".", O_SEARCH)) == -1) // Bu açış modu Linux'ta desteklenmiyor. { exit_sys("open"); } printf("Success!...\n"); close(fd); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> UNIX sistemlerdeki bazı at'li fonksiyonlar: Anımsayacağımız üzere yol ifadesini argüman olarak alan POSIX dosya fonksiyonlarının, argüman olarak "file descriptor" alan versiyonları vardı ki bu versionların isimlerli "f" harfi ile başlamaktaydı. Örneğin, "stat" ve "lstat" fonksiyonları yol ifadesi alırken, "fstat" fonksiyonu bir "file descriptor" alır. Benzer şekilde "chmod" ve "fchmod" fonksiyonlarını, "chown" ve "fchown" fonksiyonlarını örnek gösterebiliriz. İşte bu "f" li fonksiyonların birde "at" li versiyonları bulunmaktadır. Örneğin, "fstatat", "fchmodat", "fchownat" gibi. Aslında bu "at" li versiyonlar seyrek kullanılan fonksiyonlardır. Bu "at" li versiyonlar, argüman olarak bir "file descriptor" alırlar ve bu "fd", "open" ile açılmış bir DİZİNE AİT OLMALIDIR. AKSİ HALDE İLGİLİ FONKSİYON BAŞARISIZ OLACAKTIR. Eğer bu "at" li fonksiyon bir yol ifadesi de alıyorsa, bunun göreli bir yol ifadesi olması gerekiyor. Mutlak bir yol ifadesi geçtiğimiz zaman, birinci parametre işlevsiz hale geliyor ve "at" siz versiyondan bir farkı kalmıyor. Peki yol ifadesini göreli geçtiğimiz zaman ne olur? Prosesin "current working directory" konumu yerine, birinci parametrede geçilen "file descriptor" tarafından belirtilen dizin konum olarak ele alınır. POSIX standartlarına göre biz dizin "O_SEARCH" modunda açılmışsa, prosesimizin ilgili dizin üzerinde "x" hakkına sahip olmadığı kontrol edilmez. Başka modlar ile açılmışsa bu kontrol işlemi gerçekleştirilir. * Örnek 1, aşağıda "open" ve "openat" fonksiyonlarının karşılaştırılması yapılmıştır: int open(const char *path, int oflag, ...); int openat(int fd, const char *path, int oflag, ...); "openat" fonksiyonunun; -> Birinci parametresi, "open" ile açılmış bir DİZİNE AİT "fd" olmalıdır. Aksi halde "openat" fonksiyonu kafadan başarısız olacaktır. -> İkinci parametresi, GÖRELİ BİR YOL İFADESİ OLMALIDIR. MUTLAK BİR YOL İFADESİ KULLANDIĞIMIZ ZAMAN BİRİNCİ PARAMETRE İŞLEVSİZ HALE GELECEK OLUP, BU FONKSİYONUN "open" FONKSİYONUNDAN BİR FARKI KALMAYACAKTIR. * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Success!... openat: No such file or directory */ int fddir; if( (fddir = open("/usr/include", O_RDONLY)) == -1 ) exit_sys("open"); printf("Success!...\n"); int fd; if( (fd = openat(fddir, "test.txt", O_RDONLY)) == -1 ) exit_sys("openat"); printf("Success!...\n"); close(fd); close(fddir); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Success!... Success!... */ int fddir; if( (fddir = open("/usr/include", O_RDONLY)) == -1 ) exit_sys("open"); printf("Success!...\n"); int fd; if( (fd = openat(fddir, "/home/test.txt", O_RDONLY)) == -1 ) exit_sys("openat"); printf("Success!...\n"); close(fd); close(fddir); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, aşağıda "chmod" fonksiyonu, diğer versiyonları ile birlikte karşılaştırılmıştır. int chmod(const char *path, mode_t mode); int fchmod(int fildes, mode_t mode); int fchmodat(int fd, const char *path, mode_t mode, int flag); * Örnek 5, aşağıda "chown" fonksiyonu, diğer versiyonları ile birlikte karşılaştırılmıştır. int chown(const char *path, uid_t owner, gid_t group); int fchown(int fildes, uid_t owner, gid_t group); int fchownat(int fd, const char *path, uid_t owner, gid_t group, int flag); > Dizin girişlerinin elde edilmesi: >> Bir dizin girişindeki (directory entries) bilgileri elde etmek için bir takım POSIX fonksiyonları bulundurulmuştur. Burada dikkat etmemiz gereken nokta, dizinlerin "open" ve "openat" fonksiyonları ile açılabildiği fakat pek çok sistemde "read" fonksiyonu ile okumanın yapılamadığıdır. Ek olarak dizin dosyalarının iç formatı, dosya sisteminden dosya sistemine değişebilmektedir. İş bu POSIX fonksiyonların bulunma amacı da budur. >> Linux sistemlerinde bu amaç için "getdents" isimli bir SİSTEM FONKSİYONU BULUNMAKTADIR. >> Dizin girişlerini elde edebilmek için evvela dizinimizi "opendir" isimli POSIX fonksiyonu ile açmamız gerekiyor. Bunu gerçekleştirebilmek için de prosesimizin ilgili dizin üzerinde "r" hakkının olması gerekiyor. >>> "opendir" fonksiyonu, "dirent.h" isimli başlık dosyasında bildirilmiştir. Argüman olarak sadece açılacak dizin'in ismini almaktadır. Geri dönüş değeri ise "DIR" türünden bir yapı nesnesinin adresidir. Fonksiyon başarısızlık durumunda NULL değerini döndürür ve "errno" uygun değerini alır. Yine bu fonksiyonun da "f" li versiyonu vardır ve ismi "fopendir" şeklindedir. >>>> "DIR": Bir tür eş ismidir. İlgili fonksiyonları direkt olarak bu "DIR" türünden argüman almaktadırlar. Bu yapı türü halihazırda tahsis edilmiştir, ilgili bellek alanını geri vermeyeceğiz. >>>> "fopendir" : "open" veya "openat" ile açılan bir dizine dair "DIR" bilgisini elde etmek için bu fonksiyonu çağırabiliriz çünkü bu fonksiyon bir "file descriptor" almaktadır. * Örnek 1, "opendir" fonksiyonunun kullanılması: #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Success!... */ DIR* dir; if ( (dir = opendir("/usr/include")) == NULL ) exit_sys("opendir"); printf("Success!...\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, //.. #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # opendir: Not a directory */ DIR* dir; if ( (dir = opendir("test.txt")) == NULL ) exit_sys("opendir"); printf("Success!...\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> Bir dizin girişindekileri okuyabilmek içi öncesinde "opendir" ya da "fopendir" ile açmamız gerekiyor, "readdir" fonksiyonunu kullanmamız gerekmektedir. İş bu fonksiyon da bir POSIX fonksiyonudur. >>> "readdir" fonksiyonu, yine "opendir" gibi, "dirent.h" isimli başlık dosyasında bildirilmiştir. Argüman olarak bir "DIR" yapısının adresini almaktadır. Geri dönüş değeri olarak "dirent" isimli yapının adresini döndürmektedir. İş bu yapı da statik ömürlüdür. İş bu fonksiyon her çağrıldığında bir sonraki dizin girişi geri döndürülür. Dolayısıyla bütün dizin girişlerini okuyabilmek için bir döngü içinde çağrı yapmamız gerekiyor. İş bu fonksiyon, dizin girişinin sonuna geldiğinde bizlere NULL değerini döndürmektedir. İş bu fonksiyon başarısızlık durumunda da NULL değerini döndürmektedir ve ek olarak "errno" değişkenini uygun değere çekmektedir. BİZLER ARTIK HEM NULL HEM DE "errno" KONTROLÜ YAPMAMIZ GEREKİYOR. >>>> "dirent" yapısı, POSIX standartlarına göre en az iki elemana sahip olmalıdır. Bu elemanlar "d_ino" ve "d_name" isimlerine sahiptirler. Bu elemanlar sırasıyla "ino_t" ve "char[]" türlerindendir. İş bu yapı ikiden fazla elemana da sahip olabilir. Linux işletim sisteminde, bu iki elemana ek olarak 3 eleman daha bu yapının içerisindedir. Bu elemanlar "d_off", "d_reclen" ve "d_type" isimli elemanlardır. Bu üç elemandan en önemlisi şüphesiz "d_type" isimli olanıdır çünkü dosyanın tür bilgisini de tutmaktadır. Böylelikle tekrardan "stat" fonksiyonuna çağrı yapmayacağız. FAKAT UNUTULMAMALIDIR Kİ POSIX STANDARTLARINDA SADECE YUKARIDA ZİKREDİLEN İKİ ELEMAN VARDIR. Linux işletim sisteminde ilgili yapı aşağıdaki biçimdedir; struct dirent { ino_t d_ino; /* i-node number */ off_t d_off; /* offset to the next dirent */ unsigned short d_reclen; /* length of this record */ unsigned char d_type; /* type of file; not supported by all file system types */ char d_name[256]; /* filename */ }; Bu elemanlardan; -> "d_ino", dosyaya ait "i-node" numarasını tutmaktadır. -> "d_name", dizin girişinin ismini vermektedir. Bu dizin girişi "regular file" olabileceği gibi bir "directory" de olabilir. -> "d_type", Linux sistemlerinde bitsel düzeyde kodlanmadığından ki bu durumda karşılaştırma için "==" operatörünü kullanmalıyız, aşağıdaki değerlerden birisini tutmaktadır: DT_UNKNOWN, The type is unknown. Only some filesystems have full support to return the type of the file, others might always return this value. DT_REG, A regular file. DT_DIR, A directory. DT_FIFO, A named pipe, or FIFO DT_SOCK, A local-domain socket. DT_CHR, A character device. DT_BLK, A block device. DT_LNK, A symbolic link. >> Dizin girişlerinin kapatılması "closedir" isimli bir POSIX fonksiyonuyla mümkündür. >>> "closedir" fonksiyonu argüman olarak yine bir "DIR" nesnesinin adresini alırlar. Başarısızlık durumunda "-1" ile başarı durumunda 0 ile geri dönerler. * Örnek 1, dizin girişlerinin ekrana yazdırılması: #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # COMMAND LINE # /usr/include */ /* # OUTPUT # 952460 -> .. 952462 -> . 948136 -> caml 898005 -> caca_types.h 898004 -> caca_conio.h ... */ if( argc != 2 ) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } DIR* dir; if ( (dir = opendir(argv[1])) == NULL ) exit_sys("opendir"); struct dirent* de; while( errno = 0, (de = readdir(dir)) != NULL ) { printf("%llu -> %s\n", (unsigned long long)de->d_ino, de->d_name); } if(errno != 0) exit_sys("readdir"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, argüman olarak geçilen bir dizinin girişindekilerinin yazdırılması: #include #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void my_ls_command(struct stat* file_info, const char* file_name); void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* Command Line Arguments => /usr/include */ /* # OUTPUT # [drwxr-xr-x 1 root root 4096 Jun 8 04:56 ..] [drwxr-xr-x 1 root root 4096 Jun 8 05:42 .] [lrwxrwxrwx 1 root root 19 Jun 8 05:42 caml] [-rw-r--r-- 1 root root 3398 Oct 20 13:39 caca_types.h] [-rw-r--r-- 1 root root 4759 Oct 20 13:39 caca_conio.h] ... */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } DIR* dir; if( (dir = opendir(argv[1])) == NULL) exit_sys("opendir"); char path[BUFFER_SIZE]; struct dirent* de; struct stat info; while(errno = 0, (de = readdir(dir)) != NULL) { /* * "lstat" ile dosya bilgilerini çekmek için, yol ifadesinin ilgili dosya isminin başında bulunması gerekmektedir. * Bu tür durumlarda fonksiyonların "at" li versiyonlarını kullanmak daha uygun olabilir. "dirfd" isimli fonksiyonu burada kullabiliriz. Üçüncü örnekte kullanılmıştır. */ sprintf(path, "%s/%s", argv[1], de->d_name); if(lstat(path, &info) == -1) exit_sys("lstat"); my_ls_command(&info, de->d_name); } if(errno != 0) exit_sys("readdir"); closedir(dir); } void my_ls_command(struct stat* file_info, const char* file_name) { static char buffer[BUFFER_SIZE]; static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH }; static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; int index = 0; for(int i = 0; i < 7; ++i) if( (file_info->st_mode & S_IFMT) == f_types[i] ) { buffer[index++] = "bcp-dls"[i]; break; } for(int i = 0; i < 9; ++i) buffer[index++] = modes[i] & file_info->st_mode ? "rwx"[i % 3] : '-'; struct passwd* pw; char uname[BUFFER_SIZE]; if((pw = getpwuid(file_info->st_uid)) == NULL) sprintf(uname, "%llu", (unsigned long long)file_info->st_uid); else strcpy(uname, pw->pw_name); struct group* gr; char gname[BUFFER_SIZE]; if((gr = getgrgid(file_info->st_gid)) == NULL) sprintf(gname, "%llu", (unsigned long long)file_info->st_gid); else strcpy(gname, gr->gr_name); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_nlink); index += sprintf(buffer + index, " %s", uname); index += sprintf(buffer + index, " %s", gname); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_size); struct tm* ptime; ptime = localtime(&file_info->st_mtim.tv_sec); index += strftime(buffer + index, BUFFER_SIZE, " %b %2e %H:%M", ptime); sprintf(buffer + index, " %s", file_name); printf("[%s]\n", buffer); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void my_ls_command(struct stat* file_info, const char* file_name); void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* Command Line Arguments => /usr/include */ /* # OUTPUT # [drwxr-xr-x 1 root root 4096 Jun 8 04:56 ..] [drwxr-xr-x 1 root root 4096 Jun 8 05:42 .] [lrwxrwxrwx 1 root root 19 Jun 8 05:42 caml] [-rw-r--r-- 1 root root 3398 Oct 20 13:39 caca_types.h] [-rw-r--r-- 1 root root 4759 Oct 20 13:39 caca_conio.h] [drwxr-xr-x 3 root root 4096 Jun 8 05:10 alsa] [-rw-r--r-- 1 root root 91325 Mar 5 06:43 slang.h] [drwxr-xr-x 2 root root 4096 Jun 8 05:10 ogg] [-rw-r--r-- 1 root root 39438 Oct 20 13:39 caca.h] ... */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } DIR* dir; if( (dir = opendir(argv[1])) == NULL) exit_sys("opendir"); int fd; if( (fd = dirfd(dir)) == -1 ) exit_sys("dirfd"); struct dirent* de; struct stat info; while(errno = 0, (de = readdir(dir)) != NULL) { /* * Dördüncü parametreye "0" geçilmesi durumunda, "stat" semantiği uygulanacaktır. "AT_SYMLINK_NOFOLLOW" sembolik sabiti "fcntl.h" içerisinde bildirilmiştir. */ if(fstatat(fd, de->d_name, &info, AT_SYMLINK_NOFOLLOW) == -1) exit_sys("lstat"); my_ls_command(&info, de->d_name); } if(errno != 0) exit_sys("readdir"); closedir(dir); } void my_ls_command(struct stat* file_info, const char* file_name) { static char buffer[BUFFER_SIZE]; static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH }; static mode_t f_types[] = { S_IFBLK, S_IFCHR, S_IFIFO, S_IFREG, S_IFDIR, S_IFLNK, S_IFSOCK }; int index = 0; for(int i = 0; i < 7; ++i) if( (file_info->st_mode & S_IFMT) == f_types[i] ) { buffer[index++] = "bcp-dls"[i]; break; } for(int i = 0; i < 9; ++i) buffer[index++] = modes[i] & file_info->st_mode ? "rwx"[i % 3] : '-'; struct passwd* pw; char uname[BUFFER_SIZE]; if((pw = getpwuid(file_info->st_uid)) == NULL) sprintf(uname, "%llu", (unsigned long long)file_info->st_uid); else strcpy(uname, pw->pw_name); struct group* gr; char gname[BUFFER_SIZE]; if((gr = getgrgid(file_info->st_gid)) == NULL) sprintf(gname, "%llu", (unsigned long long)file_info->st_gid); else strcpy(gname, gr->gr_name); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_nlink); index += sprintf(buffer + index, " %s", uname); index += sprintf(buffer + index, " %s", gname); index += sprintf(buffer + index, " %llu", (unsigned long long)file_info->st_size); struct tm* ptime; ptime = localtime(&file_info->st_mtim.tv_sec); index += strftime(buffer + index, BUFFER_SIZE, " %b %2e %H:%M", ptime); sprintf(buffer + index, " %s", file_name); printf("[%s]\n", buffer); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> "printf" fonksiyonu, varsayılan biçimde, sola dayalı olarak ekrana bastırmaktadır. Format belirleyicisi "%d" nin içerisine rakam yazmamız durumunda, o rakam kadarlık bir yer ayıracak fakat sağa dayalı olarak yazacaktır. Bu rakamın negatifini kullanmamız durumunda ise sola dayalı olarak ekrana basacaktır. * Örnek 1 #include int main() { /* # OUTPUT # 1-Bir 11-Onbir 111-Yuzonbir 1-Bir 11-Onbir 111-Yuzonbir 1 -Bir 11 -Onbir 111 -Yuzonbir */ int a = 1; const char* a_name = "Bir"; int b = 11; const char* b_name = "Onbir"; int c = 111; const char* c_name = "Yuzonbir"; printf("%d-%s\n", a, a_name); printf("%d-%s\n", b, b_name); printf("%d-%s\n", c, c_name); puts(""); printf("%5d-%s\n", a, a_name); printf("%5d-%s\n", b, b_name); printf("%5d-%s\n", c, c_name); puts(""); printf("%-5d-%s\n", a, a_name); printf("%-5d-%s\n", b, b_name); printf("%-5d-%s\n", c, c_name); } >> Bir rakamın kaç basamaklı olduğunu pratik hesaplamak için ilgili sayının on tabanına göre logaritmasını alır ve bir ekleriz. Fakat matematik fonksiyonlarını kullanırken, "-lm" seçeneğini kullanmamız gerekiyor. * Örnek 1, gcc derleyicisinde "-lm" seçeneğiyle birlikte kullanılmıştır. #include #include int main() { /* # OUTPUT # 6 */ int max = 0; int a[] = { 1, 11, 111, 1111, 11111, 111111 }; int n_digit; for(int i = 0; i < sizeof(a) / sizeof(a[0]); ++i) { n_digit = (int)log10(a[i]) + 1; if (max < n_digit) max = n_digit; } printf("%d", max); } >> "printf" fonksiyonunda hizalama yaparken kaç karakterlik hizalama yapılacağı çalışma zamanında belli olacaksa, "%d" format belirleyicisinin içine "*" karakteri yazıyoruz. Sonrasında "," karakterinden sonra bu "*" karakterine karşılık gelecek değişkeni belirtiyoruz. * Örnek 1, #include #include int main() { /* # OUTPUT # 1-Bir 11-Onbir 111-Yuzonbir 1-Bir 11-Onbir 111-Yuzonbir 1 -Bir 11 -Onbir 111 -Yuzonbir 1-Bir 11-Onbir 111-Yuzonbir 1 -Bir 11 -Onbir 111 -Yuzonbir */ int a = 1; const char* a_name = "Bir"; int b = 11; const char* b_name = "Onbir"; int c = 111; const char* c_name = "Yuzonbir"; printf("%d-%s\n", a, a_name); printf("%d-%s\n", b, b_name); printf("%d-%s\n", c, c_name); puts(""); printf("%5d-%s\n", a, a_name); printf("%5d-%s\n", b, b_name); printf("%5d-%s\n", c, c_name); puts(""); printf("%-5d-%s\n", a, a_name); printf("%-5d-%s\n", b, b_name); printf("%-5d-%s\n", c, c_name); puts(""); int n_digit = 5; printf("%*d-%s\n", n_digit, a, a_name); printf("%*d-%s\n", n_digit, b, b_name); printf("%*d-%s\n", n_digit, c, c_name); puts(""); printf("%-*d-%s\n", n_digit,a, a_name); printf("%-*d-%s\n", n_digit,b, b_name); printf("%-*d-%s\n", n_digit,c, c_name); } >> "ls" komutu ilgili dizindekileri sıraya dizdikten sonra ekrana basmaktadır. Doğal sırayı görmek için "-f" seçeneğini kullanmalıyız. "-l" komutu daha sonra belirtirsek de detaylarını da ekleyecektir. Örneğin, "ls -fl /usr/include". Fakat UNIX sistemlerinde "-f" seçeneğini kullanırsak "-r", "-S" ve "-t" seçenekleri "ignore" edilirken "-A", "-g", "-l", "-n", "-o", "-s" seçenekleri "ignore" edilebilir. FAKAT DOĞAL SIRADAN BİR ANLAM ÇIKARTMAMALIYIZ. Çünkü silinen bir dizinin yerine başka bir dizin gelebilir. >> Linux işletim sisteminde dosya işlemleri önce "sys_open" sistem fonksiyonu ile dizin'in açılması, "sys_getdents" sistem fonksiyonu ile dizin girişlerinin okunması ve nihayet "sys_close" sistem fonksiyonu ile dizin'in kapatılması yoluyla yapılmaktadır. Fakat POSIX standartlarında taşınabilirlik adına bu işlemler sırasıyla "opendir", "readdir" ve "closedir" fonksiyonlarına devredilmiştir. Şüphesiz bu POSIX fonksiyonları da aslında dizini açıp, ona ait betimleyiciyi "DIR" yapısının içerisinde saklamaktadır. Eğer elimizde bir "DIR" yapısı varsa, bizler de açık bir dizine ait dizin betimleyicisini elde etmek istiyorsak, "dirfd" isimli fonksiyonu kullanabiliriz. >>> "dirfd" isimli fonksiyon parametre olarak sadece "DIR" yapısının adresini alır, geri dönüş olarak ilgili betimleyiciyi döndürür. Başarısızlık durumunda "-1" ile geri dönmektedir. /*================================================================================================================================*/ (17_18_12_2022) > Dizin girişlerinin elde edilmesi (devam): Dizin girişlerini elde ettikten sonra her birisini ilgili "stat" fonksiyonlarına sokarak daha fazla detay elde etmemiz de mümkündür. Bir önceki derste de yapılan aslında budur. >> "rewinddir" fonksiyonu, "readdir" ile okumanın sil baştan tekrardan yapılmasını sağlamaktadır. Anımsayacağımız üzere her bir "readdir" çağrısı sonrasında bir sonraki dizin girişine dair bilgiler bize döndürülmektedir. En sonunda da bize NULL değeri döndürülüyor. Bu noktadan sonra yapılacak her "readdir" çağrısı bize NULL değerini döndürecektir. İşte böyle bir durumda bir defalık "rewinddir" çağrısı ile arka plandaki dosya göstericisinin konumunu en başa çekebiliriz. Buradaki dosya göstericisi, ilgili dizine ait bir dosya göstericisidir. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* Command Line Arguments => . */ /* # OUTPUT # mest.txt main.c test.txt .. . a.out -------- mest.txt main.c test.txt .. . a.out */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } DIR* dir; if( (dir = opendir(argv[1])) == NULL ) exit_sys("opendir"); struct dirent* de; while( errno = 0, (de = readdir(dir)) != NULL ) printf("%s\n", de->d_name); if( errno != 0 ) exit_sys("readdir"); puts("--------"); rewinddir(dir); while( errno = 0, (de = readdir(dir)) != NULL ) printf("%s\n", de->d_name); if( errno != 0 ) exit_sys("readdir"); closedir(dir); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "telldir" fonksiyonu ile o anki konum göstericisinin konum bilgisini öğrenebilir, "seekdir" fonksiyonu ile de elde edilen konum bilgisine yeniden konumlandırma yapabiliriz. Bu iki fonksiyonun kullandığı konum bilgisi "long" TÜRÜNDENDİR. Burada unutmamalıyız ki okuma yaptıktan sonra konum göstericisinin konumunu çektiğimizden ötürü, ilgili dosya göstericisinin konumu hali hazırda bir ileri ötelenmiş olacaktır. İş bu sebepten dolayı, ikinci defa okumaya başladığımızda, yeniden "test.txt" isimli dizin girişini okumamış olduk. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* Command Line Arguments => . */ /* # OUTPUT # mest.txt main.c test.txt .. . a.out -------- .. . a.out */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } DIR* dir; if( (dir = opendir(argv[1])) == NULL ) exit_sys("opendir"); long loc; struct dirent* de; while( errno = 0, (de = readdir(dir)) != NULL ) { printf("%s\n", de->d_name); if(!strcmp(de->d_name, "test.txt")) loc = telldir(dir); } if( errno != 0 ) exit_sys("readdir"); puts("--------"); seekdir(dir, loc); while( errno = 0, (de = readdir(dir)) != NULL ) printf("%s\n", de->d_name); if( errno != 0 ) exit_sys("readdir"); closedir(dir); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Dizin ağacının öz yinelemeli biçimde dolaşımı: Dizin girişlerinin bir fonksiyonun kendisini çağırmasıyla dolaşma işlemidir. İki farklı yaklaşım ele alınabilir. İlk yaklaşımda ilk dizin komple gezilir. Sonrasında varsa bu dizin içerisindeki diğer dizinler sırasıyla gezilir. Sonrasında varsa bu dizinler içerisindeki diğer dizinler komple gezilir. Buradaki amaç bir dizinin gezilmesini bitirmeden başka bir dizini gezmeye GEÇİLMEMESİDİR. İkinci yaklaşım türünde ise bir dizin taranırken bir dizin ile karşılaşılması durumunda programın akışının iş bu yeni dizini taramaya geçmesidir. Bu yaklaşım türündeki amaç ile görülen ilk dizini taramayı hedeflemektedir. UNUTULMAMALIDIR Kİ DİZİN AĞACI DOLAŞIRKEN "lstat" FONKSİYONUNU KULLANMALIYIZ. ÇÜNKÜ DİZİNLERİ GÖSTEREN SEMBOLİK LİNKLERİ "stat" İLE ÇAĞIRDIĞIMIZ ZAMAN BAĞLANTI LİNKİNİ TAKİP EDECEĞİNDEN, SONSUZ DÖNGÜYE GİREBİLİRİZ. Özyinelemeli çalışım bittikten sonra prosesin çalışma dizini orjinal hale getirilmelidir. * Örnek 1, "Depth-First" dolaşım örneği: #include #include #include #include #include #include #include void walkdir(const char* path); void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* Command Line Arguments => . */ /* # OUTPUT # [mest.txt] [main.c] [test.txt] [..] [.] [a.out] */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } walkdir(argv[1]); return 0; } void walkdir(const char* path) { /* * i. Dizini açtık. Fakat prosesimiz, ilgili dosya üzerinde 'r' hakkına sahip değilse fonksiyon başarısız olacaktır. Bu durumda programı komple sonlandırmak doğru olmaz. */ DIR* dir; if( (dir = opendir(path)) == NULL ) { fprintf(stderr, "cannot read directory: %s\n", path); return; } /* * ii. Prosesimizin 'Current Working Directory' konumunu, o anki dizin olacak şekilde değiştirdik. */ if( (chdir(path)) == -1) { fprintf(stderr, "current working directory cannot change to: [%s]\n", path); goto EXIT; } struct stat finfo; struct dirent* de; while( errno = 0, (de = readdir(dir)) != NULL ) { /* * iii. Bilgisini aldığımız dizinin bilgilerini işlemeye başladık. */ printf("[%s]\n", de->d_name); /* * iv. Her bir dizin içerisinde bulunan "." ve ".." dizinlerini pas geçmemiz gerekli. Aksi halde sonsuz döngüye gireceğiz. */ if( !strcmp(de->d_name, ".") || !strcmp(de->d_name, "..") ) continue; /* * v. Artık prosesimizin göreli yol ifadesi, yukarıda açtığımız dizin şeklindedir. Dolayısıyla tekrardan 'path' bilgisini değiştirmemize gerek kalmadı. Artık bu noktada bulunan dosyaların bilgisini çekiyoruz. */ if ( lstat(de->d_name, &finfo) == -1 ) { fprintf(stderr, "cannot read directory: %s\n", de->d_name); continue; } /* * vi. Eğer bu gerçekten bir dizin ise, bu dizinin içini aramaya koyuluyoruz. */ if(S_ISDIR(finfo.st_mode)) { walkdir(de->d_name); /* * vii. İçerideki dizinlerin hepsini gezdikten sonra prosesimizin 'current working directory' bilgisini tekrardan bir üst dizine göre ayarlıyoruz. */ if( (chdir("..")) == -1) { fprintf(stderr, "current working directory cannot change to: [%s]\n", path); goto EXIT; } } } if( errno != 0 ) fprintf(stderr, "cannot read directory info: [%s]\n", path); EXIT: closedir(dir); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, İlgili dizin ağacını kademeli olarak yazdırılması: #include #include #include #include #include #include #include void walkdir(const char* path, int level); void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* Command Line Arguments => /usr/include */ /* # OUTPUT # ... [ncurses_dll.h] [search.h] [event2] [buffer_compat.h] [event-config.h] [event_struct.h] ... [xcb] [xcb.h] ... [mtd] [mtd-abi.h] ... [librsvg-2.0] [librsvg] ... [..] [.] [krb5.h] [paths.h] [linux] ... [ImageMagick-6] [..] [magick] ... [wand] ... [nl_types.h] [evdns.h] */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } walkdir(argv[1], 0); return 0; } void walkdir(const char* path, int level) { /* * i. Dizini açtık. Fakat prosesimiz, ilgili dosya üzerinde 'r' hakkına sahip değilse fonksiyon başarısız olacaktır. Bu durumda programı komple sonlandırmak doğru olmaz. */ DIR* dir; if( (dir = opendir(path)) == NULL ) { fprintf(stderr, "cannot read directory: %s\n", path); return; } /* * ii. Prosesimizin 'Current Working Directory' konumunu, o anki dizin olacak şekilde değiştirdik. */ if( (chdir(path)) == -1) { fprintf(stderr, "current working directory cannot change to: [%s]\n", path); goto EXIT; } struct stat finfo; struct dirent* de; while( errno = 0, (de = readdir(dir)) != NULL ) { /* * iii. Bilgisini aldığımız dizinin bilgilerini işlemeye başladık. */ printf("%*s[%s]\n", level * 4, "", de->d_name); /* * iv. Her bir dizin içerisinde bulunan "." ve ".." dizinlerini pas geçmemiz gerekli. Aksi halde sonsuz döngüye gireceğiz. */ if( !strcmp(de->d_name, ".") || !strcmp(de->d_name, "..") ) continue; /* * v. Artık prosesimizin göreli yol ifadesi, yukarıda açtığımız dizin şeklindedir. Dolayısıyla tekrardan 'path' bilgisini değiştirmemize gerek kalmadı. Artık bu noktada ilgili dizin içerisindekilerinin bilgisini çekiyoruz. */ if ( lstat(de->d_name, &finfo) == -1 ) { fprintf(stderr, "cannot read directory: %s\n", de->d_name); continue; } /* * vi. Eğer bu gerçekten bir dizin ise, bu dizinin içini aramaya koyuluyoruz. */ if(S_ISDIR(finfo.st_mode)) { walkdir(de->d_name, level + 1); /* * vii. İçerideki dizinlerin hepsini gezdikten sonra prosesimizin 'current working directory' bilgisini tekrardan bir üst dizine göre ayarlıyoruz. */ if( (chdir("..")) == -1) { fprintf(stderr, "current working directory cannot change to: [%s]\n", path); goto EXIT; } } } if( errno != 0 ) fprintf(stderr, "cannot read directory info: [%s]\n", path); EXIT: closedir(dir); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, İlgili dizin ağacını kademeli olarak yazdırılması ve prosesin çalışma dizininin korunması: #include #include #include #include #include #include #include void walkdir(const char* path); void walkdir_recur(const char* path, int level); void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* Command Line Arguments => /usr/include */ /* # OUTPUT # ... [ncurses_dll.h] [search.h] [event2] [buffer_compat.h] [event-config.h] [event_struct.h] ... [xcb] [xcb.h] ... [mtd] [mtd-abi.h] ... [librsvg-2.0] [librsvg] ... [..] [.] [krb5.h] [paths.h] [linux] ... [ImageMagick-6] [..] [magick] ... [wand] ... [nl_types.h] [evdns.h] */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } walkdir(argv[1]); return 0; } void walkdir(const char* path) { char cwd[PATH_MAX]; if( getcwd(cwd, PATH_MAX) == NULL ) { perror("getcwd"); return; } walkdir_recur(path, 0); if( chdir(cwd) == -1 ) { perror("chdir"); return; } } void walkdir_recur(const char* path, int level) { /* * i. Dizini açtık. Fakat prosesimiz, ilgili dosya üzerinde 'r' hakkına sahip değilse fonksiyon başarısız olacaktır. Bu durumda programı komple sonlandırmak doğru olmaz. */ DIR* dir; if( (dir = opendir(path)) == NULL ) { fprintf(stderr, "cannot read directory: %s\n", path); return; } /* * ii. Prosesimizin 'Current Working Directory' konumunu, o anki dizin olacak şekilde değiştirdik. */ if( (chdir(path)) == -1) { fprintf(stderr, "current working directory cannot change to: [%s]\n", path); goto EXIT; } struct stat finfo; struct dirent* de; while( errno = 0, (de = readdir(dir)) != NULL ) { /* * iii. Bilgisini aldığımız dizinin bilgilerini işlemeye başladık. */ printf("%*s[%s]\n", level * 4, "", de->d_name); /* * iv. Her bir dizin içerisinde bulunan "." ve ".." dizinlerini pas geçmemiz gerekli. Aksi halde sonsuz döngüye gireceğiz. */ if( !strcmp(de->d_name, ".") || !strcmp(de->d_name, "..") ) continue; /* * v. Artık prosesimizin göreli yol ifadesi, yukarıda açtığımız dizin şeklindedir. Dolayısıyla tekrardan 'path' bilgisini değiştirmemize gerek kalmadı. Artık bu noktada ilgili dizin içerisindekilerinin bilgisini çekiyoruz. */ if ( lstat(de->d_name, &finfo) == -1 ) { fprintf(stderr, "cannot read directory: %s\n", de->d_name); continue; } /* * vi. Eğer bu gerçekten bir dizin ise, bu dizinin içini aramaya koyuluyoruz. */ if(S_ISDIR(finfo.st_mode)) { walkdir_recur(de->d_name, level + 1); /* * vii. İçerideki dizinlerin hepsini gezdikten sonra prosesimizin 'current working directory' bilgisini tekrardan bir üst dizine göre ayarlıyoruz. */ if( (chdir("..")) == -1) { fprintf(stderr, "current working directory cannot change to: [%s]\n", path); goto EXIT; } } } if( errno != 0 ) fprintf(stderr, "cannot read directory info: [%s]\n", path); EXIT: closedir(dir); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, #include #include #include #include #include #include #include int disp(const char* fname, const struct stat* finfo, int level); int walkdir(const char* path, int (*proc)(const char* fname, const struct stat* finfo, int level)); int walkdir_recur(const char* path, int level, int (*proc)(const char* fname, const struct stat* finfo, int level)); int main(int argc, char* argv[]) { /* Command Line Arguments => /usr/include */ /* # OUTPUT # ... [re_comp.h] [glib-2.0] [gobject] [gparam.h] [gtypeplugin.h] [genums.h] [gsignal.h] [gobject-autocleanups.h] [gsourceclosure.h] [gtype.h] [gparamspecs.h] [gvaluearray.h] [gmarshal.h] [glib-enumtypes.h] [gvalue.h] [gobjectnotifyqueue.c] [gbinding.h] function terminated prematurely with 1 code!... */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } int result; if( (result = walkdir(argv[1], disp)) == -1 ) { fprintf(stderr, "function terminates unexpectedly!...\n"); exit(EXIT_FAILURE); } if( result != 0 ) printf("function terminated prematurely with %d code!...\n", result); else printf("function terminated normally!...\n"); return 0; } int disp(const char* fname, const struct stat* finfo, int level) { printf("%*s[%s]\n", level * 4, "", fname); if( !strcmp(fname, "gbinding.h") ) return 1; return 0; } int walkdir(const char* path, int (*proc)(const char* fname, const struct stat* finfo, int level)) { char cwd[PATH_MAX]; if( getcwd(cwd, PATH_MAX) == NULL ) { perror("getcwd"); return -1; } int result; result = walkdir_recur(path, 0, proc); if( chdir(cwd) == -1 ) { perror("chdir"); return -1; } return result; } int walkdir_recur(const char* path, int level, int (*proc)(const char* fname, const struct stat* finfo, int level)) { int result = 0; DIR* dir; if( (dir = opendir(path)) == NULL ) { fprintf(stderr, "cannot read directory: %s\n", path); return -1; } if( (chdir(path)) == -1) { fprintf(stderr, "current working directory cannot change to: [%s]\n", path); result = -1; goto EXIT; } struct stat finfo; struct dirent* de; while( errno = 0, (de = readdir(dir)) != NULL ) { if( !strcmp(de->d_name, ".") || !strcmp(de->d_name, "..") ) continue; if ( lstat(de->d_name, &finfo) == -1 ) { fprintf(stderr, "cannot read directory: %s\n", de->d_name); continue; } if ( (result = proc(de->d_name, &finfo, level)) != 0) result = -1; goto EXIT; if(S_ISDIR(finfo.st_mode)) { result = walkdir_recur(de->d_name, level + 1, proc); if( (chdir("..")) == -1) { fprintf(stderr, "current working directory cannot change to: [%s]\n", path); result = -1; goto EXIT; } if( result != 0 ) goto EXIT; } } if( errno != 0 ) fprintf(stderr, "cannot read directory info: [%s]\n", path); EXIT: closedir(dir); return result; } > Hatırlatıcı Notlar: >> "scandir" fonksiyonu, kendi içerisinde "opendir" fonksiyonunu barındırmaktadır. Dolayısıyla daha üst seviye bir fonksiyondur. Prototipi aşağıdaki şekildedir. #include int scandir( const char *dir, struct dirent ***namelist, int (*sel)(const struct dirent *), int (*compar)(const struct dirent **, const struct dirent **) ); Birinci parametresi bir dizine ait yol ifadesidir. İkinci parametre, bir göstericiyi gösteren göstericinin adresidir. Buradaki gösterici dinamik ömürlü bir bellek alanını göstermektedir ve işimiz bittiğinde bu alanın geri verilmesinden BİZ SORUMLUYUZ. Üçüncü ve dördüncü parametreler ise birer fonksiyon adresleri olup; üçüncü parametre filtreleme yapacak olan fonksiyonun adresini, dördüncü parametre ise sıralama yapacak olan fonksiyonun adresidir. Buradaki amaç, bizim istediğimiz özelliklere haiz bir dizin girişlerini sıralı bir şekilde elde etmektir. Burada, dördüncü parametre için standart bir fonksiyon yazıldığı için bizlerin tekrardan bir fonksiyon yazmasına gerek kalmamıştır. Yazılan bu fonksiyonun adı ise "alphasort". Üçüncü parametreye NULL geçilmesi durumunda ise dizin girişindeki bütün girişler elde edilecektir. * Örnek 1, "/home" dizini içerisindeki dosyalardan başı "t" ya da "T" ile başlayanların sıraya dizilmiş bir şekilde elde edilmesi: #include #include #include #include int my_filter(const struct dirent* de); void exit_sys(const char* msg); int main(void) { /* In the directory: /home */ /* # INPUT # test.txt mest.txt main.c */ /* # OUTPUT # [1] test.txt */ int result; struct dirent** dents; if( (result = scandir("/home", &dents, my_filter, alphasort)) == -1 ) exit_sys("scandir"); for(int i = 0; i < result; ++i) { printf("%s\n", dents[i]->d_name); } for(int i = 0; i < result; ++i) free(dents[i]); free(dents); return 0; } int my_filter(const struct dirent* de) { return de->d_name[0] == 't' || de->d_name[0] == 'T'; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Dördüncü parametre olarak yeni bir fonksiyon yazılması: #include #include #include #include #include #include int my_filter(const struct dirent* de); int cmp_size(const struct dirent** de1, const struct dirent** de2); void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument: /usr/include */ /* # OUTPUT # tar.h term.h term_entry.h termcap.h termio.h termios.h tgmath.h thread_db.h threads.h tic.h time.h ttyent.h */ int result; struct dirent** dents; if( chdir(argv[1]) == -1 ) exit_sys("chdir"); if( (result = scandir(argv[1], &dents, my_filter, cmp_size)) == -1 ) exit_sys("scandir"); for(int i = 0; i < result; ++i) { printf("%s\n", dents[i]->d_name); } for(int i = 0; i < result; ++i) { free(dents[i]); } free(dents); return 0; } int my_filter(const struct dirent* de) { return de->d_name[0] == 't' || de->d_name[0] == 'T'; } int cmp_size(const struct dirent** de1, const struct dirent** de2) { struct stat finfo1, finfo2; if( stat((**de1).d_name, &finfo1) == -1 || stat((**de2).d_name, &finfo2) == -1 ) exit_sys("stat"); if( finfo1.st_size > finfo2.st_size ) return -1; if( finfo1.st_size < finfo2.st_size ) return 1; return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (18_24_12_2022) > Dizin ağacının öz yinelemeli biçimde dolaşımı (devam): Dizin ağacını öz yinelemeli olarak dolaşan "scandir" fonksiyonun ek olarak iki fonksiyon daha mevcuttur. Bu fonksiyonlar "ftw" ve "nftw" isimli fonksiyonlardır. Bu fonksiyonlar POSIX fonksiyonlarıdır. Günümüzde "ftw" isimli fonksiyon "deprecated" hale getirildi. Artık günümüzde "nftw" isimli fonksiyon kullanılmaktadır. Bu fonksiyonu Linux altında, GNU kütüphanesiyle birlikte kullanmak için, "_XOPEN_SOURCE" isimli sembolik sabitini en az "500" rakamı olacak şekilde tanımlamamız gerekiyor. Tanımlandığı nokta, başlık dosyalarının yukarısında olmalıdır. "nftw" isimli fonksiyonun imzası şu şekildedir: #include int nftw( const char *path, int (*fn)(const char * path, const struct stat *finfo, int flag, struct FTW *ftw), int fd_limit, int flags ); -> Fonksiyonun birinci parametresi, öz yinelemeli olarak dolaşılacak dizinin yol ifadesidir. -> İkinci parametre bir "callback" parametresidir. Her dizin girişi bulunduğunda bu "callback" fonksiyona çağrı yapılacaktır. "callback" fonksiyonunun parametreleri de şu şekildedir: -> Birinci parametre, bulunan dizin girişinin yol ifadesi yerleştirilir. Bu yol ifadesinin baş kısmı tamamen bizim "nftw" fonksiyonuna verdiğimiz dizin ifadesinden oluşmaktadır. Eğer "nftw" fonksiyonuna mutlak yol ifadesi geçilmişse, bu "callback" fonksiyonunun birinci parametresi de mutlak bir yol ifadesi olacaktır. Göreli olsaydı, göreli olacaktı. -> İkinci parametre, bulunan dizin girişine ilişkin "stat" yapısının adresini tutmaktadır. -> Üçüncü parametre, bulunan dizin girişinin türünü belirtmektedir. Bu tür şunlardan birine tam eşit ("==" operatörünü kullanmalıyız) olmalıdır. Bu türler; FTW_D - Bulunan giriş bir dizine aittir. FTW_DNR - Bulunan giriş bir dizine aittir. Ancak bu dizinin içi okunamamıştır. Artık bu dizin öz yineleme sırasında dolaşılamayacaktır. FTW_DP - "post-order" biçimindeki dolaşımda bir dizin ile karşılaşıldığında FTW_D bayrağı yerine bu bayrak kullanılacaktır. FTW_F - Bulunan dizin girişinin "regular file" a ait belirtmektedir. FTW_NS - Bulunan dizin girişi için "stat" çağrısının başarısız olduğunu belirtmektedir. Bu durumda fonksiyona geçilen "stat" yapısının elemanları ANLAMLI DEĞİLDİR. FTW_SL - Bulunan giriş bir SEMBOLİK LİNK DOSYASINA İLİŞKİNDİR. FTW_SLN - Bulunan giriş bir SEMBOLİK LİNK DOSYASINA İLİŞKİNDİR. Fakat bu sembolik bağlantı dosyası "dangling" konumundadır. Yani hedefteki gerçek dosya MEVCUT DEĞİLDİR. -> Dördüncü parametre, "FTW" türünden bir yapının adresini tutmaktadır. Bu yapı bünyesinde iki elemana sahip olup isimleri "level" ve "base" biçimindedir. Bu elemanlar da "int" türdendir. "level" elemanı derine dalarken kaçıncı derinlikte olduğumuzun bilgisini tutarken, "base" elemanı ise dizin girişinin, birinci parametrede belirtilen yol ifadesinin kaçıncı indeksten başladığını belirtmektedir. Örneğin, "/home/kaan/Study" dizinini dolaşmak istemiş olalım. Fonksiyon da dizin girişi olarak "sample.c" yi bulmuş olsun. Fonksiyon, bize, bu girişi "/home/kaan/Study/sample.c" biçiminde verecektir. İş bu "base" elemanı "17" değerini tutacaktır. Çünkü "sample.c" ismindeki "s" harfi, ilgili yol ifadesindeki 17. karaktere denk gelmektedir. -> Fonksiyonun üçüncü parametresi, kullanılacak maksimum dosya betimleyicisinin adedini belirtmektedir. Dün yazdığmız "walkdir_recur" isimli fonksiyon her çağrıldığında, fonksiyonun gövdesinde çağrılan "opendir" fonksiyonundan dolayı, "File Description Table" bir elemana daha sahip oluyor. İşte bu tabloyu doldurmamak adına, bu üçüncü parametreyi kullanarak, bu fonksiyon ile oluşturulacak maksimum "File Description" adedini belirtmiş oluyoruz. Her ne kadar bir dizini dolaşmayı bitirip tekrar bir üst dizine döndüğümüz zaman ilgili "File Description" yok edilse de, her gördüğümüz ilk dizinin içine girilmektedir. Bu üçüncü parametre ile kaç defa ilk gördüğümüz dizine gireceğimiz sınırlanmış olmaktadır. -> Fonksiyonun dördüncü parametresi ise öz yinelemeli dolaşım sırasında bazı belirlemeler için kullanılmaktadır. Bu parametre çeşitli sembolik sabitlerin "bit-wise OR" işlemine sokulması ile oluşturulmaktadır. Bu dördüncü parametreye hiç bir bayrak geçmek istemez ise "0" değerini geçebilir. İş bu sabitler; FTW_CHDIR - Eğer bu bayrak belirtilirse, fonksiyon her dizine geçtiğinde, prosesin çalışma dizinini de o dizin olacak şekilde değiştirmektedir. FTW_DEPTH - Normal şartlarda öz yinelemeli dolaşım "pre-order" denilem biçimde yapılmaktadır. Yani önce bütün dizin girişlerinin ele alınması, sonrasında da öz yineleme yapılması demektir. Bu bayrak belirtilirse dolaşım "post-order" biçiminde yapılacaktır. Yani önce öz yineleme yapılması, sonrasında dizin girişlerinin ele alınmasıdır. FTW_MOUNT - Unix/Linux sistemlerinde tek bir ağaç vardır, haliyle en altta sadece bir tane "root" dizin mevcuttur. Sisteme "Removable Drive" bağlantısı yapıldığında bir nevi "mount" işlemi yapmış oluyoruz. Fakat bu tip harici bağlantıların dosya sistemleri, bizim işletim sisteminde kullanılan dosya sisteminden farklı olmaktadır. Eğer bu bayrak belirtilirse, öz yineleme sırasında bu tip "mount" edilmiş dizinler MUAF TUTULACAKTIR. FTW_PHYS - Bu bayrak belirtilirse, bir sembolik link ile karşılaşıldığında sembolik linki İZLEMEMEKTEDİR. Fakat varsayılan durumlarda sembolik linkler izlenmektedir. Daha önce de belirttiğimiz gibi, bir dizine açılan sembolik linklerin izlenmesi sonsuz döngü meydana getirebilir. -> "nftw" fonksiyonunun geri dönüş değeri ise başarısızlık durumunda "-1", başarı durumunda "0" şeklindedir fakat başarılı olması durumunda "callback" olarak kullanılan fonksiyonun geri dönüş değeri ile geri dönmektedir. Dolayısıyla bu "callback" fonksiyonunu "0" ile geri döndürürsek, öz yinelemeye devam etmek istediğimizi belirtmiş oluruz. Başka bir hata olmaması durumunda "nftw" de "0" ile geri dönecektir. Velev ki bizler "callback" fonksiyonundan sıfırdan başka bir değer ile geri dönersek, "nftw" fonksiyonu öz yinelemeyi kontrollü bir şekilde durdurur ve iş bu sıfırdan başka değer ile geri döner. * Örnek 1, #define _XOPEN_SOURCE 500 #include #include #include int call_back(const char * path, const struct stat *finfo, int flag, struct FTW *ftw); void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => "." */ /* # OUTPUT # [.] [./main.c] [./testTESTtest.txt] [./test.txt] [./a.out] result = 0 */ if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } int result; if((result = nftw(argv[1], call_back, 100, FTW_PHYS) ) == -1) exit_sys("nftw"); printf("result = %d\n", result); return 0; } int call_back(const char * path, const struct stat *finfo, int flag, struct FTW *ftw) { printf("[%s]\n", path); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #define _XOPEN_SOURCE 500 #include #include #include int call_back(const char * path, const struct stat *finfo, int flag, struct FTW *ftw); void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => "." */ /* # OUTPUT # [.] [main.c] [testTESTtest.txt] [test.txt] [a.out] result = 0 */ if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } int result; if((result = nftw(argv[1], call_back, 100, FTW_PHYS) ) == -1) exit_sys("nftw"); printf("result = %d\n", result); return 0; } int call_back(const char * path, const struct stat *finfo, int flag, struct FTW *ftw) { printf("[%s]\n", path + ftw->base); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #define _XOPEN_SOURCE 500 #include #include #include int call_back(const char * path, const struct stat *finfo, int flag, struct FTW *ftw); void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => "." */ /* # OUTPUT # [.] [main.c] [testTESTtest.txt] [test.txt] [a.out] result = 0 */ if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } int result; if((result = nftw(argv[1], call_back, 100, FTW_PHYS) ) == -1) exit_sys("nftw"); printf("result = %d\n", result); return 0; } int call_back(const char * path, const struct stat *finfo, int flag, struct FTW *ftw) { printf("%*s[%s]\n", ftw->level * 4, "", path + ftw->base); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, #define _XOPEN_SOURCE 500 #include #include #include int call_back(const char * path, const struct stat *finfo, int flag, struct FTW *ftw); void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => "/" */ /* # OUTPUT # [driver] - cannot read directory. [fd] - cannot read directory. [fdinfo] - cannot read directory. [ns] - cannot read directory. [fd] - cannot read directory. [map_files] - cannot read directory. [fdinfo] - cannot read directory. [ns] - cannot read directory. [fd] - cannot read directory. [fdinfo] - cannot read directory. [ns] - cannot read directory. [fd] - cannot read directory. [map_files] - cannot read directory. [fdinfo] - cannot read directory. [ns] - cannot read directory. [fd] - cannot read directory. [fdinfo] - cannot read directory. [ns] - cannot read directory. [fd] - cannot read directory. [map_files] - cannot read directory. [fdinfo] - cannot read directory. [ns] - cannot read directory. [private] - cannot read directory. [localauthority] - cannot read directory. [root] - cannot read directory. [unattended-upgrades] - cannot read directory. [private] - cannot read directory. [ldconfig] - cannot read directory. [partial] - cannot read directory. [private] - cannot read directory. [partial] - cannot read directory. [sessions] - cannot read directory. [polkit-1] - cannot read directory. [private] - cannot read directory. result = 0 */ if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } int result; if((result = nftw(argv[1], call_back, 100, FTW_PHYS) ) == -1) exit_sys("nftw"); printf("result = %d\n", result); return 0; } int call_back(const char * path, const struct stat *finfo, int flag, struct FTW *ftw) { switch(flag) { // default: printf("%*s[%s]\n", ftw->level * 4, "", path + ftw->base); break; case FTW_DNR: printf("%*s[%s] - cannot read directory.\n", ftw->level * 4, "", path + ftw->base); break; case FTW_NS: printf("%*s[%s] - cannot get stat info.\n", ftw->level * 4, "", path + ftw->base); break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Programlama yoluyla "hard-link" ve "soft-link" oluşturulması: Komut satırından "hard-link" ve "soft-link" çıkarmak için "ln" komutunu kullanmaktayız. "-s" seçeneğini de eklersek, "soft-link" meydana getiririz. Aksi halde "hard-link" oluşacaktır. Disk üzerinde, dosyalara ait bilgiler "i-node table" denilen bir tabloda tutulmaktadır. Memory üzerindeyse dizin girişleri "dosya_ismi - i_node_number" ikilisi biçiminde oluşmaktadır. Eğer iki farklı dizin girişi, "i-node table" üzerindeki aynı indisi gösteriyorsa, ilgili dizin girişinin "hard-link" sayacı iki olmaktadır. "soft-link" dosyası ise "i-node table" üzerinde farklı bir indise sahip fakat içerisindeki "i-node" elemanı sadece kendisi ile bağlı olan esas dosyaya ait "i-node" numarasını tutmaktadır. Windows sistemlerindeki "kısayol dosyaları" gibi düşünebiliriz. >> "hard-link" çıkartılması "link" fonksiyonu ile mümkündür ve fonksiyonumuz aşağıdaki parametrik yapıya sahiptir; #include int link(const char *path1, const char *path2); -> Birinci parametre, "hard-link" i çıkartılacak dosyaya ait yol ifadesidir. Eğer bu ifade bir sembolik link dosyasına aitse, sembolik link dosyasının kendisinin mi yoksa gösterdiği dosyanın mı "hard-link" inin çıkartılacağı işletim sistemine göre değişmektedir. -> İkinci parametresi, yeni dizin girişinin ismini belirtmektedir. -> Yeni bir dizin girişi oluşturacağımız için, ilgili dizin üzerinde prosesimizin "w" hakkı yok ise fonksiyon başarısız olacaktır. Eğer bir dizine dair "hard-link" çıkartacaksak prosesimiz yeteri kadar yetkiye sahip olmalı ve işletim sistemi de buna izin vermelidir. Aksi halde DİZİNLERE "hard-link" ÇIKARTILAMAZ. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => test.txt mest.txt */ /* # OUTPUT # *A new file called 'mest.txt' has been created.* */ if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if(link(argv[1], argv[2]) == -1) exit_sys("link"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>> "link" fonksiyonunun "linkat" isimli "at" li versiyonu da vardır. Diğer "at" li versiyonlardan bir farkı yoktur. Yani ilk parametre bir "file descriptor". İkinci parametre ise, ilk parametreden itibaren göreli olan bir yol ifadesi. Üçüncü ve dördüncü parametreler de bu mantıkta. İş bu yol ifadeleri mutlak bir yol ifadesiyse, birinci ve üçüncü parametreler işlev görmeyecektir. Beşinci parametre ise sembolik bağlantının izlenip izlenmeyeceğini belirtmektedir. Varsayılan durumda sembolik bağlantı izlenmemektedir fakat izlemek için bu parametreye "AT_SYMLINK_FOLLOW" sembolik sabitini geçmeliyiz. Bu beşinci parametreye "0" girilmesi, varsayılan durumu gerçekleştirecektir. >> "soft-link" çıkartılması, "symlink" isimli POSIX fonksiyonu ile mümkündür. Aşağıdaki parametrik yapıya sahiptir; #include int symlink(const char *path1, const char *path2); -> Fonksiyonun birinci parametresi, sembolik bağlantısı çıkartılacak dosyanın yol ifadesidir. -> Fonksiyonun ikinci parametresi, sembolik bağlantı dosyasının yol ifadesidir. -> Başarı durumunda "0" değerine, başarısızlık durumunda "-1" değerine geri dönecektir. Sistemlerde, sembolik bağlantı dosyasının kendisine ait erişim haklarının bir önemi YOKTUR. Erişim hakları kontrol edilirken sembolik bağlantı dosyasının kendisi yerine gösterdiği esas dosyaya bakılır. Hem komut satırından sembolik bağlantı oluştururken hem de "symlink" fonksiyonunu kullanırken "dangling" durumda olan sembolik bağlantı dosyası oluşturabiliriz. Yani kaynak bir dosyanın olması mecburi değildir. Her ne kadar dizinlerin "hard-link" leri çıkartılması sorunlu bir durum olduğundan yukaruda bahsetmiştik. Halbuki aynı durum "soft-link" için geçerli değildir. Yani bir dizinin sembolik bağlantısını oluşturabiliriz. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => q.txt mest.txt */ /* # OUTPUT # *A new file called 'mest.txt' has been created.* */ if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if(symlink(argv[1], argv[2]) == -1) exit_sys("link"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>> "symlink" versiyonu da "at" li bir versiyona sahiptir. İsmi "symlinkat". Fonksiyonun ikinci parametresi bir "fire descriptor". Üçüncü parametre ise, ikinci parametreden itibaren göreli bir yol ifadesi. Birinci parametre ise kaynak olan dosyanın yol ifadesidir. > "readlink" POSIX fonksiyonu: "lstat" fonksiyonu ile bir dosyanın sembolik bağlantı dosyası olduğunu öğrenebiliyoruz fakat bu fonksiyon iş bu sembolik bağlantı dosyasının neyi gösterdiğinin bilgisini tutmamaktadır. İşte "readlink" fonksiyonu ile bizler sembolik bağlantı dosyalarının neyi gösterdiğini öğrenebiliyoruz. "ls" komutu da "lstat" fonksiyonunun yanında bu fonksiyonu da kullanmaktadır. >> "readlink" fonksiyonu aşağıdaki parametrik yapıya sahiptir; #include ssize_t readlink( const char * path, char * buf, size_t bufsize ); -> Birinci parametre, içi okunacak sembolik bağlantı dosyasının yol ifadesidir. -> İkinci parametre, gösterilen esas dosyanın yol ifadesinin yazılacağı "buffer" ın başlangıç adresi. -> Üçüncü parametre, iş bu "buffer" bölgesinin büyüklüğü. -> Eğer birinci parametredeki ifade, ikinci ve üçüncü parametrelerde bilgileri verilen "buffer" alanına sığmaz ise, fonksiyon BAŞARISIZ OLMUYOR. SADECE BİLGİYİ KIRPMAKTADIR. Yol ifadesinin baş kısımları ilgili "buffer" alanına yazılırken, son kısımlar kırpılmaktadır. Eğer birinci parametredeki ifade "buffer" alanından küçükse, geriye kalan alanların ne ile doldurulacağı kesin değil. "buffer" alanını doldururken, en sona "\0" karakteri EKLENMEMEKTEDİR. Dolayısıyla bu alandaki bilgileri işlerdek dikkatli olmalıyız. Fonksiyon geri dönüş değeri olarak "buffer" alanına yazdığı karakterlerin adedini döndürmektedir. Fakat herhangi bir aksili olması durumunda "-1" ile geri döner ve "buffer" alanına ellenmez. * Örnek 1, görüldüğü üzere yeni alanın bittiği yere "\0" karakteri eklenmedi. #include #include #include void exit_sys(const char* msg); #define PATH_MAX_II 4096 int main(int argc, char** argv) { /* Command Line Argument => q.txt mest.txt */ /* # INPUT # q.txt */ /* # OUTPUT # {xxxxxXXXXXxxxxxXXXXX} [5] => {q.txtXXXXXxxxxxXXXXX} */ if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (symlink(argv[1], argv[2]) == -1) exit_sys("link"); char buffer[PATH_MAX_II] = "xxxxxXXXXXxxxxxXXXXX"; printf("{%s}\n", buffer); ssize_t result; if ( (result = readlink(argv[2], buffer, PATH_MAX_II)) == -1 ) exit_sys("readlink"); printf("[%lld] => ", (long long)result); printf("{%s}\n", buffer); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include void exit_sys(const char* msg); #define PATH_MAX_II 4096 int main(int argc, char** argv) { /* Command Line Argument => q.txt mest.txt */ /* # INPUT # q.txt */ /* # OUTPUT # {xxxxxXXXXXxxxxxXXXXX} [5] => {q.txt} [5] => q.txt [5] => {q.txt} */ if (argc != 3) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (symlink(argv[1], argv[2]) == -1) exit_sys("link"); char buffer[PATH_MAX_II + 1] = "xxxxxXXXXXxxxxxXXXXX"; printf("{%s}\n", buffer); ssize_t result; if ( (result = readlink(argv[2], buffer, PATH_MAX_II)) == -1 ) exit_sys("readlink"); // Approach - I if ( result < PATH_MAX_II ) { buffer[result] = '\0'; printf("[%lld] => ", (long long)result); printf("{%s}\n", buffer); } else { /* * Bizler "buffer" alanımızı 4096 + 1 karakter uzunluğunda belirledik. Yol ifadesinin * bu uzunluktan da büyük olması çok çok uç bir durumdur. Eğer "buffer" alanı daha küçük * belirlenmiş olsaydı, bu durumda bu "buffer" alanını adım adım büyütme yapmamız * gerekmektedir ki bu sebeple tam yol ifadesini kayıt altına alabilelim. */ fprintf(stderr, "the path maybe truncated!...\n"); } // Approach - II printf("[%lld] => ", (long long)result); for(ssize_t i = 0; i < result; ++i) { putchar(buffer[i]); } puts(""); // Approach - III printf("[%lld] => ", (long long)result); printf("{%.*s}\n", (int)result, buffer); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> "Appropriate Priviledges" kavramı, uygun haklara haiz olmak anlamındadır. Yani prosesin Etkin Kullanıcı ID değerinin "root" / "0" olması veya ilgili prosesin Etkin Kullanıcı ID değerinin "root" / "0" olmaması ama bir takım özel haklara haiz olması manasındadır. Ya hep ya hiç kuralı POSIX sistemlerde uygulanmak zorunda değildir. Linux sistemlerinde bu özel durumlara ilgili kullanıcının "capability" si denir. Yani bir özellik bir kullanıcıya bahşedilme durumudur. >> "unlink" ve "remove" isimli fonksiyonlar sembolik bağlantı dosyalarını İZLEMEMEKTEDİR. Dolayısıyla bu fonksiyonlar ile bir silme işlemi yaptığımız zaman sembolik bağlantı dosyasının kendisini silmiş olacağız. Öte yandan "open" fonksiyonu sembolik bağlantıları izlemektedir. Genel olarak POSIX fonksiyonlarının çoğu sembolik bağlantı dosyalarını izlemektedir. >> Sembolik bağlantı dosyalarını kullanarak bir "loop" meydana getirebiliriz. "open" ile bu durumdaki bir sembolik bağlantı dosyasını açmaya çalıştığımız vakit, "errno" değeri "ELOOP" değerini alacaktır (burada sonsuz döngünün belli miktar kadar dönmesi gerekmektedir). >> Linux sistemlerinde maksimum yol ifadesi uzunluğu 4096 karakterdir. Fakat POSIX sistemlerde yol ifadesinin en fazla kaç karakter uzunlukta olacağı, "limits.h" içerisinde belirtilen, "PATH_MAX" isimli sembolik sabit üzerinden öğrenilebilir. Fakat bu sembolik sabitin tanımlı olması da bir ZORUNLULUK DEĞİLDİR. Bu konu teferruatlı olduğu için ileride ele alınacaktır. >> "printf" fonksiyonunda "%s" içerisinde "." atomu ve yanına bir rakam yazmamız, sadece o rakam kadarlık yaz manasındadır. * Örnek 1, #include int main(void) { /* # OUTPUT # >>> xXxxXXxxxXXXxxxxXXXXxxxxxXXXXX <<< >>> xXxxXXxxxX <<< */ char buffer[30] = "xXxxXXxxxXXXxxxxXXXXxxxxxXXXXX"; printf(">>> %s <<<\n", buffer); int n_digit = 10; printf(">>> %.*s <<<\n", n_digit, buffer); } /*================================================================================================================================*/ (19_25_12_2022) > "access" fonksiyonu: Bir dosyayı açmadan, o dosya üzerinde bir takım işlemlerin yapılıp yapılamayacağını sorgulamak için kullanılan bir POSIX fonksiyonudur. Bu fonksiyon, prosesin gerçek Kullanıcı ID ve gerçek Grup ID bilgilerini kullanarak sorgulama yapmaktadır fakat daha önceki fonksiyon örneklerinde Etkin Kullanıcı ID ve Etkin Grup ID bilgileri kullanılmıştır. Anımsayacağımız üzere Etkin Kullanıcı ID ile gerçek Kullanıcı ID değerleri ekseriyetle birbirine eşitti. Benzer şekilde etkin Grup ID ile gerçek Grup ID değerleri de. Öyle bir senaryo düşünelim ki prosesimizin Etkin Kullanıcı ID değeri "root", yani "0". Fakat gerçek Kullanıcı ID değeri "kaan". Bir dosya üzerinde de "kaan" ın bir yetkisi yok. İşte "access" fonksiyonu bu durumda başarısız olacaktır çünkü kendisi işleme gerçek Kullanıcı ID değerini sokmaktadır. Ek olarak bu fonksiyon atomik bir yapıda değildir. Yani bizler bu fonksiyon ile bir dosyaya "yazma" yapıp yapamayacağımıza dair bir sorgulama yapalım. Fakat yazma işlemine başlamadan evvel başka bir proses, bu dosyanın haklarını değiştirmiş olsun. Sorgulama ile yazma işlemi arasına başka bir prosesin müdahalesi olduğu için bizim "yazma" işlemimiz başarısız olacaktır. Dolayısıyla bu fonksiyon ile yaptığımız sorgulamaların %100 doğru olmayabilir. Fonksiyonun imzası aşağıdaki gibidir; #include int access(const char *path, int amode); -> Birinci parametre, sorgulama yapılacak dosyanın yol ifadesidir. -> İkinci parametre ise sorgulama yapılacak erişim haklarını belirtmektedir ve aşağıda belirtilen sembolik sabitlerin "bit-wise OR" işlemine sokulması ile elde edilebilir. R_OK - Okuma yapılabilir mi? W_OK - Yazma yapılabilir mi? X_OK - Çalıştırılabilir mi? F_OK - İlgili dosya var mı? -> Test olumlu ise "0" değerine, olumsuz ise "-1" değerine geri dönmektedir. Başarısızlık durumunda "errno" değişkeni uygun değer almaktadır. İş bu fonksiyonun GNU libc kütüphanesinde tanımlı iki verisyonu daha vardır ve bunlar sırasıyla "euidaccess" ve "eaccess" ismindedirler. Bu iki fonksiyon da yine "access" ile aynı işi yapmaktadır fakat prosesin Etkin Kullanıcı ID ve Etkin Grup ID bilgilerini işleme sokmaktadır. Fakat bu iki fonksiyon POSIX standartlarında YOKTUR. Bu iki fonksiyonu da kullanabilmek için "_GNU_SOURCE" makrosunu da en tepede tanımlamamız gerekiyor. >> "access" fonksiyonu birde "at" li bir versiyona sahiptir ve ismi "faccessat" biçimindedir. Aşağıdaki parametrik yapıya sahiptir; #include int faccessat(int fd, const char *path, int amode, int flag); -> Birinci parametre bir "file descriptor". -> İkinci parametre, birinci parametredeki "fd" ye bağlı göreli bir yol adresi. Göreli bir yol ifadesi geçildiğinde, birinci parametre başlangıç noktası sayılacaktır. Mutlak bir yol ifadesi geçildiğinde birinci parametre dikkate alınmayacaktır. -> Üçüncü parametre, sorgulama yapılacak erişim haklarını belirtmektedir. "access" fonksiyonuna geçilen sembolik sabitler bu parametreye geçilir. -> Dördüncü parametre ise etkin id değerlerinin mi yoksa gerçek id değerlerinin mi kullanılacağını belirtmektedir. Buraya "AT_EACCESS" geçilmesi durumunda etkin id değerleri kullanılacaktır. "0" geçilmesi durumunda da gerçek id değerleri kullanılacaktır. * Örnek 1, Aşağıdaki örnekte "/" dizini açılmış ve bu dizine ait "fd" ilgili "faccessat" fonksiyonuna argüman olarak geçilmiştir: #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => home/q.txt */ /* # OUTPUT # file exists!... */ if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } int fd; if( (fd = open("/", O_RDONLY)) == -1) exit_sys("open"); if(faccessat(fd, argv[1], F_OK, AT_EACCESS) == 0) fprintf(stdout, "file exists!...\n"); else fprintf(stdout, "file does not exist!...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte prosesin o anki "current working directory" noktası kullanılmış. Arama bu noktadan itibaren yapılmıştır: #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => home/q.txt */ /* # OUTPUT # file does not exist!... */ if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if(faccessat(AT_FDCWD, argv[1], F_OK, AT_EACCESS) == 0) fprintf(stdout, "file exists!...\n"); else fprintf(stdout, "file does not exist!...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1, İş bu örnekte "errno" değerinin kontrolü yapılmamıştır: #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* Command Line Argument => q.txt */ /* # OUTPUT # q.txt is readable, writable and executable!... */ if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if(!access(argv[1], F_OK)) { if( access(argv[1], R_OK | W_OK | X_OK) == 0) fprintf(stdout, "%s is readable, writable and executable!...\n", argv[1]); } else { fprintf(stderr, "%s does not exists!...\n", argv[1]); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Dosya betimleyicilerinin çiftlenmesi: Anımsayacağımız üzere proseslerin kontrol bloğunda, Dosya Betimleyici Tablosunun adresi yer almaktadır. Aşağıda bu ilişkiye dair bir gösterim mevcuttur; task_struct(files) -> files_struct(fdt) -> 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. "files_struct" içerisindeki "fdt" isimli eleman ise "fdtable" türünden bir yapı nesnesinin adresini tutmaktadır. "task_struct" içerisindeki "files" isimli bir eleman ise "files_struct" türden bir yapı nesnesinin adresini tutmaktadır. Bu tablonun ilk üç indisi halihazırda doluyken, dördüncü indisten itibaren bizlerin kullanımına sunulmaktadır. Her bir başarılı "open" fonksiyonu çağrısı sonrasında bu tablodaki ilk boş indise yeni bir "fd" de eklenmektedir. Açılmış dosyanın bilgileri de diskten çekilerek, yeni oluşturulan "fd" yapı nesnesinin içerisine konur. Erişim hakları, "i-node" numarası vb. bilgiler diskten çekilmektedir. Nasıl ki "hard-link" çıkartılırken farklı dizin girişleri aynı "i-node" numarasına sahip olursa, burada da bir "file" türünden elemanın adresi Dosya Betimleyici Tablosundaki farklı indislerde bulunmaktadır. Dosya Betimleyici Tablosunun sıfır numaralı indisine halk arasında "stdout" betimleyicisi, bir numaralı indisine "stdin" ve iki numaralı indisine de "stderr" denmektedir. "unistd.h" içerisinde bunlar için de sembolik sabitler tanımlanmıştır; #define STDIN_FILENO 0 #define STDOUT_FILENO 1 #define STDERR_FILENO 2 Fakat bir ve iki numaralı indisler aynı "file" yapısının adresini tutmaktadırlar. "stdin" isimli indisteki "file" nesnesi, "Terminal Device Driver" ı aittir. Benzer şekilde "stdout" ve "stderr" de aynı betimleyici göstermekte olup, yine "Terminal Device Driver" a aittir. Bu üçü "Terminal Device Driver" içerisindeki ilgili fonksiyonlara çağrı yapmaktadır. "Aygıt sürücüler(device drivers)" de dosya gibi açılarak kullanılmaktadır, yani "open" fonksiyonu ile aygıt sürücüleri açarak kullanabiliriz. Devamında "read" ile okuma ve "write" ile yazma yapabiliriz. Dolayısıyla bir "file" nesnesi bir dosyaya ilişkin olabileceği gibi bir aygıt sürücüsüne ilişkin olabilir. Eğer "file" nesnesi bir dosyaya ilişkin ise bu dosya üzerinde bahsi geçen işlemleri yapabiliriz. Eğer bir aygıt sürücüsüne ilişkin ise bu aygıt sürücüsünün ilgili fonksiyonlarına çağrı yapılacaktır. Sonuçta bizim dosyamız üzerinde "read" işlemi yaparken aslında sistem fonksiyonlarına çağrı yapılmakta. Aygıt sürücüsüne "read" yapıldığında da bu sürücünün sunduğu "read" fonksiyonuna çağrı yapılacaktır. Dosyaları kapattığımız zaman("close" fonksiyonu ile), "file" yapısının içerisinde bulunan sayaç("f_count") bir azaltılmaktadır. Bu sayaç sıfıra geldiğinde "file" nesnesinin kendisi de YOK EDİLECEKTİR. "file" yapısı aşağıdaki gibidir; https://elixir.bootlin.com/linux/v6.1.4/source/include/linux/fs.h#L940 struct file { union { struct llist_node f_llist; struct rcu_head f_rcuhead; unsigned int f_iocb_flags; }; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; /* * Protects f_ep, f_flags. * Must not be taken from IRQ context. */ spinlock_t f_lock; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock; loff_t f_pos; struct fown_struct f_owner; const struct cred *f_cred; struct file_ra_state f_ra; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif void *private_data; /* needed for tty driver, and maybe others */ #ifdef CONFIG_EPOLL struct hlist_head *f_ep; /* Used by fs/eventpoll.c to link all the hooks to this file */ #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; errseq_t f_sb_err; /* for syncfs */ } __randomize_layout __attribute__((aligned(4))) /* lest something weird decides that 2 is OK */ ; Bir betimleyicinin gösterdiği "file" nesnesini gösteren başka bir betimleyici oluşturabiliriz. Bunun için iki adet POSIX fonksiyonu oluşturulmuştur. Bu fonksiyonları "dup" ve "dup2" isimli fonksiyonlardır. >> "dup" fonksiyonu aşağıdaki biçimde bir parametrik yapıya sahiptir; #include int dup(int fildes); -> Bu fonksiyonun aldığı parametre, açık bir dosyaya ilişkin "fd". Geri dönüş değeri olarak da iş bu "fd" tarafından gösterilen "file" nesnesini gösteren yeni bir "fd". Bu fonksiyonun en düşük "fd" değerini verdiği garanti altındadır. Yani tablodaki en düşük indisi vermektedir. -> Argüman olarak geçtiğimiz "fd" nin geçersiz olması, Dosya Betimleyici Tablosunun tamamen dolması ki maksimum indis sayısı varsayılan durumda 1024 adettir, vb. nedenlerden dolayı fonksiyonumuz başarısız olacaktır ve "-1" ile geri dönecektir. Buradaki "fd" aslında Dosya Betimleyici Tablosundaki bir indistir. Başarısızlık durumunda yine "errno" uygun değerini alacaktır. Açık dosyanın bütün bilgileri "file" nesnesinin içinde olduğuna göre ve iki "fd" de aynı "file" nesnesini gösteriyorsa, ortaya aynı "file pointer" a sahip olma durumu çıkacaktır. Aşağıdaki örnekte bu gösterilmiştir. * Örnek 1, aşağıdaki kodlar "main.c" dosyası içerisindedir. #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # INPUT # *main.c* */ /* # OUTPUT # [#include <] | [stdio.h> ] */ int fd; if( (fd = open("main.c", O_RDONLY)) == -1 ) exit_sys("open"); int fd2; if( (fd2 = dup(fd)) == -1 ) exit_sys("dup"); char buffer[10 + 1]; ssize_t result; if( (result = read(fd, buffer, 10)) == -1 ) exit_sys("read"); buffer[result] = '\0'; printf("[%s]", buffer); printf(" | "); if( (result = read(fd, buffer, 10)) == -1 ) exit_sys("read"); buffer[result] = '\0'; printf("[%s]", buffer); /* * "file" nesnesinin kendisini de yok etmek için bütün * "fd" lerin kapatılması gerekmektedir. */ close(fd); close(fd2); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "dup2" isimli fonksiyon, işlevsel olarak "dup" fonksiyonunu da kapsamaktadır fakat biraz daha ayrıntılı biçimidir. Bu fonksiyon aşağıdaki parametrik yapıya sahiptir; #include int dup2(int fildes, int fildes2); -> Birinci parametresi, çiftlemek istediğimiz "fd" yi belirtmektedir. -> İkinci parametresi ise yeni oluşturulacak olanın "fd" bilgisidir. Anımsanacağı üzere "fd" bilgileri birer indis bilgileridir. Dolayısıyla yeni oluşturulacak "fd", iş bu ikinci parametreki indis numarasına sahip olacaktır. Eğer bu parametre ile gösterilen açık bir dosyaya ilişkin ise ilgili dosya önce kapatılacaktır. Fakat bu durum, bahsi geçen dosyanın yok edileği anlamına da gelmesin çünkü halihazırda çiftlenmiş bir "fd" olabilir. -> Başarısızlık durumunda "-1" ile, başarı durumunda ise ikinci parametredeki ile geri dönmektedir ve "errno" uygun değerini alacaktır. Artık bu fonksiyon ilk boş betimleyiciyi değil, ikinci parametresine geçilen betimleyici ile geri dönmektedir.Bir diğer deyişle; bu fonksiyona geçilen "fd" değerleri aynı "file" nesnesini gösterecektir(fonksiyonun başarılı olduğu varsayılmıştır). Fakat unutmamalıyız ki iş bu fonksiyon, argüman olarak geçilen "fd" değerlerini işleme sokmadan önce birbiri ile eşit olup olmadığını kontrol etmektedir. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # INPUT # *main.c* */ /* # OUTPUT # <<< 3 | 31 >>> [#include <] | [stdio.h> ] */ int fd; if( (fd = open("main.c", O_RDONLY)) == -1 ) exit_sys("open"); int fd2; if( (fd2 = dup2(fd, 31)) == -1 ) exit_sys("dup2"); printf("<<< %d | %d >>>\n", fd, fd2); char buffer[10 + 1]; ssize_t result; if( (result = read(fd, buffer, 10)) == -1 ) exit_sys("read"); buffer[result] = '\0'; printf("[%s]", buffer); printf(" | "); if( (result = read(fd, buffer, 10)) == -1 ) exit_sys("read"); buffer[result] = '\0'; printf("[%s]", buffer); close(fd); close(fd2); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> "ls" komutu ile bir dizinin kendi özelliklerini görebilmek için "-d" seçeneğini kullanmamız gerekiyor. Aksi halde ilgili dizin içerisindekileri ekrana yazdıracaktır. >> "at" li fonksiyonların birinci parametresine bir "file descriptor" geçmek yerine "AT_FDCWD" sembolik sabitini kullanmamız durumunda prosesin "current working directory" konumu başlangıç noktası olarak ele alınacaktır. Dolayısıyla bir "fd" elde etmek için "open" ile bir dizin açmamız gerek kalmamaktadır. Eğer ikinci parametre mutlak yol ifadesi olursa, birinci parametre ele alınmayacağı için günün sonunda "at" siz versiyonlarını kullanmış gibi olacağız. Velevki ikinci parametre göreli bir yol ifadesi olsaydı, birinci parametredeki "fd" başlangıç noktası olarak ele alınacaktır. Eğer birinci parametreye de "AT_FDCWD" geçerksek, prosesin o anki "current working directory" konumu başlangıç olarak ele alınacaktır. İşte bu durum sonucunda "at" siz versiyonu kullanmaktan yine bir farkı kalmamaktadır. Çünkü "at" siz versiyonlara geçilen yol ifadesi göreli ise prosesin o anki "current working directory" konumu başlangıç olarak ele alınmaktadır. Pekiyi neden "at" li fonksiyonları kullanmalıyız? "at" li versiyonların almış olduğu diğer parametrelerden de faydalanmak için. AYRICA UNUTULMAMALIDIR Kİ "at" Lİ FONKSİYONLARIN BİRİNCİ PARAMETRESİNE GEÇİLEN "fd", İKİNCİ PARAMETRESİNİN MUTLAK OLMASI DURUMUNDA HERHANGİ BİR KONTROLE TABİİ DEĞİLDİR. GEÇERSİZ BİR "fd" GEÇİLEBİLİR. >> Bu kursta çalıştırılan kodların çoğu "https://www.onlinegdb.com/" isimli internet sitesinde çalıştırılmaktadır. Bu sitedeki "cwd" konumu "/home" biçimindedir. "/" ise "root" dizindir. /*================================================================================================================================*/ (20_07_01_2023) > IO Yönlendirmesi (IO Redirection): Bir dosya üzerinde işlem yaptığımızı zannederken, aslında bambaşka bir dosya üzerinde işlem yapıyor oluşumuzdur. Arka planda ise dosya betimleyicisi "fd" nin başka bir dosya nesnesini göstermesi durumudur. Bu işlem en çok "stdin", "stdout" ve "stderr" dosyaları üzerinde uygulanmaktadır. Anımsanacağı üzere, bir proses hayata geldiğinde dosya betimleyici tablosunun ilk üç indisi dolu durumdadır. Sıfır numaralı indis "stdin", bir ve iki numaralı indisler ise sırasıyla "stdout" ve "stderr" isimli dosya nesnelerine ilişkindir. Burada bir ve iki numaralı indiste bulunan dosya betimleyicileri "duplicate" edilmiştir. Bunun detaylarına ilerleyen konularda değineceğiz. Bir dosya betimleyicisi disk üzerindeki gerçek bir dosyaya ilişkin de olabilir, bir aygıt sürücüsüne ilişkin de olabilir. Dosya betimleyici tablosunun ilk üç indisinde bulunan betimleyiciler "Terminal Device Driver" isimli aygıt sürücüsüne ilişkindirler. Aygıt sürücüsüne ilişkin dosya betimleyicileri kullanılarak "read", "write" gibi POSIX fonksiyonları çağrıldığında aygıt sürücüsünün kendi içerisindeki okuma ve yazmaya ilişkin fonksiyonlar çağrılmaktadır ve asıl işi aygıt sürücülerinin ilgili fonksiyonları yapmaktadır. Yine burada da, POSIX fonksiyonları sarmalayıcı bir görev üstlenmiştir, diyebiliriz. Aygıt sürücülere ilişkin detaylar kursun ilerleyen dönemlerinde işlenecektir. Yine unutmamalıyız ki aygıt sürücüler de "kernel mode" biçiminde çalışmaktadır. Sıfır indisli dosya betimleyicisi "read-onlu" modda açılmıştır ve bu betimleyici kullanılarak bir okuma yapılmak istendiğinde aygıt sürücüsünün okuma işlevi görev fonksiyonu çağrılacaktır ve iş bu fonksiyon da klavyeden okuma işlemini gerçekleştirecektir. Benzer şekilde bir ve iki numaralı indisli dosya betimleyiciler de "write-only" modda açılmıştır ve aygıt sürücüsünün yazma işlevini yürüten fonksiyonları çağrılacaktır eğer bu dosya betimleyicisini kullanırsak. Bu durumda da iş bu fonksiyonlar, ekrana yazı basma işlevini yürüteceklerdir. Buradan da görüleceği gibi aygıt sürücüler sanki birer dosyaymış gibi ele alınmaktadır. Yine POSIX sistemlerde "stdint", "stdout" ve "stderr" isimli dosya nesnelerine hitap eden sıfır, bir ve iki numaraları dosya betimleyicilerine sırasıyla "STDIN_FILENO", "STDOUT_FILENO" ve "STDERR_FILENO" sembolik sabitlerini tanımlamışlardır. İndis numaraları yerine iş bu sembolik sabitleri de kullanabiliriz. Amaç okunabilirliği arttırmaktır. Bu üç sembolik sabit de "unistd.h" isimli başlık dosyasında tanımlanmıştır. C dilindeki "stdio.h" içerisinde tanımlanmış ve "stdin" ve "stdout" dosyaları üzerinde işlem yapan "printf", "scanf" gibi fonksiyonlar günün sonunda sıfır ve bir numaralı indislerde bulunan dosya betimleyicilerini "read" ve "write" fonksiyonları ile çağırmaktadır. Burada "print" isimli fonsksiyon "write" isimli POSIX fonksiyonunu çağırırken, "scanf" ise "read" isimli fonksiyonu çağırmaktadır. Sadece ilgili POSIX fonksiyonlarını çağırmadan evvel yazılacak ya da okunacak yazıları bir "buffer" içerisinde depolarlar. Dosya betimleyici tablosunun ilk üç betimleyicisinde otomatik olarak yer alan "stdin", "stdout" ve "stderr" isimli dosyaları bizler KAPATMAMALIYIZ. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* * Görüldüğü üzere "open" fonksiyonuna çağrı yapmadan direkt * olarak ekrana yazı bastık. */ write(1, "this is a test\n", 16); // OUTPUT => this is a test } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include void exit_sys(const char* msg); int main() { char buffer[4096]; ssize_t result; /* 'Enter' tuşuna basılana kadar klavyeden okuma yapacak ve * bunları da 'buffer' alanına yazacaktır. */ if((result = read(0, buffer, 4096)) == -1) exit_sys("read"); // INPUT => Ulya Yuruk /* * 'buffer' alanındakiler ekrana basacaktır. */ if((write(1, buffer, result)) == -1) exit_sys("write"); // OUTPUT => Ulya Yuruk return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include void exit_sys(const char* msg); int main() { /* # q.txt # Number: 0 Number: 1 Number: 2 Number: 3 Number: 4 Number: 5 Number: 6 Number: 7 Number: 8 Number: 9 */ /* Bir numaralı dosya betimleyicisi kapatıldı. */ close(1); int fd; /* * Bir numaralı betimleyici kapatıldı. "open" fonksiyonu da müsait olan en düşük betimleyiciyi * döndüreceği garanti olduğu için bize bir numaralı betimleyiciyi döndürmektedir. Fakat bizler * "open" ile "q.txt" dosyasını açtığımız için, bir numaralı betimleyici artık "q.txt" dosyasına * ilişkin. */ if((fd = open("q.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("open"); /* * 'printf' fonksiyonu, günün sonunda 'write' fonksiyonunu bir numaralı betimleyici kullanarak * çağırdığı için, ekrana yazmak yerine açmış olduğumuz dosyaya yazacaktır. */ for(int i = 0; i < 10; ++i) printf("Number: %d\n", i); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } 3 numaralı örnekteki gibi bir "IO" yönlendirmesi yapmamız genel hatlarıyla bir soruna yol açmaz çünkü "open" fonksiyonu ilk boş betimleyiciyi bize döndürmektedir. Eğer bizler bu üç betimleyici haricindeki bir betimleyiciye yönlendirme yaparsak bir sorun ile karşılaşabiliriz çünkü "open" fonksiyonu bizim kapattığımız betimleyiciyi geri döndürmeyebilir. Karşılaşacağımız bir diğer problem ise "multi-thread" programlama sırasındadır. "close" ile kapattıktan sonra "open" ile açmadan evvel başka bir "thread" "open" fonksiyonuna çağrı yaparsa, ilk boş betimleyiciyi o alacağı için, bizler başka bir betimleyici alabiliriz. Bu problemlere çözüm olarak "dup2" fonksiyonunu kullanmalıyız çünkü iş bu fonksiyon hem atomik olarak yukarıda yapılanları yapmaktadır hem de bizim istediğimiz dosya betimleyicisini geri döndürmektedir(başarılı olduğu varsayılmıştır). Fakat geri dönmek için, "dup2" öncesinde ilgili dosya betimleyicisini de yedeklemeliyiz. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main() { /* # q.txt # Number: 0 Number: 1 Number: 2 Number: 3 Number: 4 Number: 5 Number: 6 Number: 7 Number: 8 Number: 9 */ int fd; /* * "open" fonksiyonu en düşük "fd" değerini bize döndürecektir. */ if((fd = open("q.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("open"); printf("fd : [%d]\n", fd); // OUTPUT => fd : [3] /* * Dosya Betimleyici Tablosunun bir numaralı indisindeki "file" nesnesi kapatılıyor fakat * ilgili "file" nesnesi henüz yok edilmiyor çünkü iki numaralı indis de hala bu "file" * nesnesini göstermektedir. Ek olarak, bir numaralı indis ile "fd" aynı "file" nesnesini * göstermektedir. */ if(dup2(fd, 1) == -1) exit_sys("dup2"); /* İlgili "fd" betimleyicisini biz açtığımız için biz kapatmalıyız. */ close(fd); /* * 'printf' fonksiyonu, günün sonunda 'write' fonksiyonunu bir numaralı betimleyici kullanarak * çağırdığı için, ekrana yazmak yerine açmış olduğumuz dosyaya yazacaktır. Çünkü sadece bir * numaralı indis, yukarıda "open" ile oluşturulan "file" nesnesini göstermektedir. */ for(int i = 0; i < 10; ++i) printf("Number: %d\n", i); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # Number: 0 Number: 1 Number: 2 Number: 3 Number: 4 Number: 5 Number: 6 Number: 7 Number: 8 Number: 9 */ int fd; /* * "open" fonksiyonu en düşük "fd" değerini bize döndürecektir. */ if((fd = open("q.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("open"); printf("fd : [%d]\n", fd); // OUTPUT => fd : [3] /* * Dosya Betimleyici Tablosunun bir numaralı indisindeki "file" nesnesi kapatılıyor fakat * ilgili "file" nesnesi henüz yok edilmiyor çünkü iki numaralı indis de hala bu "file" * nesnesini göstermektedir. Ek olarak, bir numaralı indis ile "fd" aynı "file" nesnesini * göstermektedir. */ if(dup2(fd, 1) == -1) exit_sys("dup2"); /* İlgili betimleyiciyi biz açtığımız için biz kapatmalıyız. */ close(fd); /* * 'printf' fonksiyonu, günün sonunda 'write' fonksiyonunu bir numaralı betimleyici kullanarak * çağırdığı için, ekrana yazmak yerine açmış olduğumuz dosyaya yazacaktır. Çünkü sadece bir * numaralı indis, yukarıda "open" ile oluşturulan "file" nesnesini göstermektedir. */ for(int i = 0; i < 10; ++i) printf("Number: %d\n", i); /* 'print' tamponlu çalıştığı için iş bu tamponu sıfırlamamız gerekiyor. */ fflush(stdout); /* Bir numaralı betimleyici tekrardan kapatılıyor ve iki numaralı betimleyici göstermeye * başlıyor. Artık yazılar dosyaya değil, ekrana çıkacaktır eğer 'print' kullanırsak. */ if(dup2(2, 1) == -1) exit_sys("dup2"); for(int i = 0; i < 10; ++i) printf("Number: %d\n", i); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # Number: 0 Number: 1 Number: 2 Number: 3 Number: 4 Number: 5 Number: 6 Number: 7 Number: 8 Number: 9 */ /* İki numaralı dosya betimleyicisi kapatıldı. */ close(2); int fd; /* * İki numaralı betimleyici kapatıldı. "open" fonksiyonu da müsait olan en düşük betimleyiciyi * döndüreceği garanti olduğu için bize iki numaralı betimleyiciyi döndürmektedir. Fakat bizler * "open" ile "q.txt" dosyasını açtığımız için, iki numaralı betimleyici artık "q.txt" dosyasına * ilişkin. */ if((fd = open("q.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("open"); int fd_stdout; /* * Bir numaralı betimleyicinin gösterdiği "file" nesnesini gösteren yeni bir betimleyici elde edildi. * Böylelikle bir numaralı betimleyici yedeklenmiş oldu. */ if((fd_stdout = dup(1)) == -1) exit_sys("dup"); /* * Bir numaralı betimleyici kapatıldı ve "fd" betimleyicisi ile aynı dosya nesnesini gösterir oldular. */ if(dup2(fd, 1) == -1) exit_sys("dup2"); /* İlgili betimleyiciyi biz açtığımız için biz kapatmalıyız. */ close(fd); /* Artık bu aşamada bir numaralı betimleyici "q.txt" dosyasını gösterir durumdadır. */ /* * 'printf' fonksiyonu, günün sonunda 'write' fonksiyonunu bir numaralı betimleyici kullanarak * çağırdığı için, ekrana yazmak yerine açmış olduğumuz dosyaya yazacaktır. */ for(int i = 0; i < 10; ++i) printf("Number: %d\n", i); /* 'print' tamponlu çalıştığı için iş bu tamponu sıfırlamamız gerekiyor. */ fflush(stdout); /* Bir numaralı betimleyici tekrardan kapatılıyor ve 'fd_stdout' isimli betimleyici göstermeye * başlıyor. */ if(dup2(fd_stdout, 1) == -1) exit_sys("dup2"); for(int i = 0; i < 10; ++i) printf("Number: %d\n", i); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, #include #include #include #include void exit_sys(const char* msg); int main() { /* # q.txt # 10 20 30 30 20 10 10 20 30 30 20 10 20 30 10 20 10 */ /* # OUTPUT # 10 20 30 30 20 10 */ int fd; if((fd = open("q.txt", O_RDONLY)) == -1) exit_sys("open"); /* * Sıfır numaralı betimleyici kapatılıyor ve ilgili betimleyici "fd" betimleyicisi * ile aynı dosyayı göstermeye başlıyor. */ if(dup2(fd, 0) == -1) exit_sys("dup2"); /* İlgili betimleyici biz açtığımız için bizler kapatmalıyız. */ close(fd); int val; /* * "scanf" fonksiyonu da sıfır nolu betimleyiciyi kullanarak "read" fonksiyonunu * çağırdığı için, dosyadan okuma yapıyor. Klavyeden değil. */ while( scanf("%d", &val) == 1 ) printf("%d\n", val); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan şöyle bir senaryo da gerçekleşebilir; -> "open" fonksiyonu ile bir dosyayı açmak isteyelim. Fakat "open" işleminden evvel 1 numaralı "fd" nin gösterdiği "stdout" u da kapatılmış olsun. Anımsanacağınız üzere "open" fonksiyonu bize en düşük "fd" değerini verecektir. -> Dolayısıyla "open" ile açacağımız dosya artık 1 numaralı "fd" değeri ile gösterilmektedir. -> Daha sonra bizler "dup2(fd, 1)" şeklinde bir çağrı yapalım. Buradaki "fd", az evvel "open" ile açtığımız dosyaya aittir. -> "dup2" fonksiyonuna geçilen argümanların ikisi de aynı ise fonksiyon bir işlem gerçekleştirmiyordu. -> Fakat hemen ardından "close(fd)" çağrısı yapmamız aslında "1" numaralı betimleyici kapatacaktır. Bizler aslında gereksiz olarak gördüğümüz "fd" yi kapatmak istiyorken, zaten bir tane olan "fd" yi kapatmış olacağız. Bunu gidermek için aşağıdaki gibi bir koruma mekanizması gerçekleştirebiliriz: if(fd != 1) { if(dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); } Böylece "fd" ile "1" farklı betimleyiciler ise gereksiz olan "fd" kapatılacaktır. Tabii bu durum çok nadir karşımıza çıkabilecek bir durumdur. Dolayısıyla özel bir durum belirtilmediyse, böyle bir kontrole gerek yoktur. "./sample > q.txt" biçimindeki bir kabuk komutu kullanıldığında, kabuk programı "sample" programını ve "q.txt" dosyasını açıyor. Daha sonra "sample" programının bir numaralı betimleyicisini "dup2" ile "q.txt" dosyasının betimleyicisine eşliyor. Böylelikle "stdout" dosyası yerine "q.txt" dosyasına yazma yapıyor eğer "sample" programı içerisinde "printf" vb. fonksiyonlar varsa. "q.txt" dosyası açılırken "O_WRONLY | O_TRUNC" modlarını kullanıyor. Fakat kabuk programının bunu nasıl yaptığı ileriki konularda ele alınacaktır. Bu yöntem ile "ls", "cat" gibi kabuk komutlarını da istediğimiz dosyaya yazma yapmalarını sağlayabiliriz. Bu durumda kabuk üzerinde şu komutu uygulayabiliriz; "ls -l > q.txt". Eğer ">" yerine ">>" kullanılırsa, "q.txt" dosyasının açış modu "O_CREAT | O_WRONLY | O_APPEND" biçiminde olacaktır. Böylelikle yazma işlemi dosyanın sonuna yapılacaktır. "ls -l >> q.txt" komutunu örnek olarak verebiliriz. Burada "IO" yönlendirmesini yapan "./sample" veya "ls" programları değil, kabuk programına ">" seçeneğinin geçilmesidir. "<" sembolünün kullanılması ise sıfır indisli betimleyicinin ilgili dosyaya yönlendirileceği anlamındadır. Örneğin, "./sample < q.txt" şeklinde bir komut çalıştıralım. İlgili "q.txt" dosyası "O_RDONLY" modda açılır ve "./sample" prosesinin sıfır numaralı betimleyicisini "q.txt" dosyasına yönlendirir. Böylelikle "./sample" programı klavyeden okuma yapmak yerine dosyadan okuma yapacaktır; "scanf" programı artık dosyadan okuma yapacaktır. Velevki "n>" kullanırsak, "n" numaralı betimleyiciyi sağ taraftaki dosyaya yönlendirmektedir(yazma amacıyla). "10<" ise 10 numaralı betimleyiciyi ilgili dosyaya okuma amacıyla yönlendirecektir. "ls -l 2> test.txt" şeklindeki bir komut, iki numaralı betimleyiciyi ilgili text dosyasına yönlendirecektir. Kabuk, bütün bu yönlendirme işlemlerini şu şekilde yapmaktadır; önce bir kez "fork" işlemi yapar, daha sonra yönlendirme işlemini. En son da "exec" işlemini. Dosya betimleyici tablosu prosese özgür bir tablodur, dolayısıyla dosya betimleyiciler de prosese özgürüdür. Son olarak kabuk üzerinden hem "stdout" hem de "stdin" dosyalarını birlikte yönlendirebiliriz. Örneğin, "./sample > out.txt < in.txt" komutunu çalıştırırsak; bir numaralı betimleyici "out.txt" ye yazarken, sıfır numaralı betimleyici ise "in.txt" dosyasından okuma yapacaktır. > "stderr" dosyası nedir? Anımsayacağımız üzere dosya betimleyici tablosunun bir ve iki numaralı indisli betimleyicileri aynı "file" nesnesini gösterir durumdadırlar. Yani "fprintf" fonksiyonu üzerinden "stdout" ve "stderr" isimli betimleyicileri (ya da bir ve iki indis numaralı betimleyicileri) kullandığımız vakit, çıktının ekrana yapıldığı görmekteyiz. Burada "fprintf" kullanma sebebimiz, istediğimiz dosya betimleyicisini belirleme imkanımızın olmasıdır. Çünkü "printf" fonksiyonu direkt olarak "stdout" isimli betimleyiciyi kullanmaktadır. Bu konudaki farklılık "scanf" ile "fscanf" fonksiyonları arasında da vardır. Fakat unutmamalıyız ki C dilindeki "stdout", "stdin" ve "stderr" isimli değişkenler DOSYA BETİMLEYİCİSİ NİTELEMEZLER. BUNLAR "FILE*" türden değişkenlerdir. * Örnek 1, #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # This is a output => stdout This is a output => stderr */ fprintf(stdout, "This is a output => stdout\n"); // printf("This is a output => stdout\n"); fprintf(stderr, "This is a output => stderr\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, "stdout", "stdin" ve "stderr" isimleri bir dosya betimleyicisini nitelemezler. Çünkü bunlar "FILE*" türden değişkenlerdir: #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # expected ‘FILE * restrict’ {aka ‘struct _IO_FILE * restrict’} but argument is of type ‘int’ extern int fprintf (FILE *__restrict __stream, */ fprintf(0, "This is a output => stdout\n"); // warning: NULL argument where non-NULL required (argument 1) [-WnonNULL] fprintf(1, "This is a output => stderr\n"); // warning: passing argument 1 of ‘fprintf’ makes pointer from integer without a cast [-Wint-conversion] return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } O halde "stderr" dosya betimleyicisinin var oluş amacı nedir? Bir programdaki hata mesajlarını, programcı, "stderr" dosyasına yazdırsın ama "stdout" dosyasına yazdırmasın. Böylelikle ilgili dosya betimleyicilerini tekrardan yönlendirerek, programın çıktısını incelemek ve okumak daha kolay olsun. Örneğin, kabuk üzerinden "./sample > test.txt" komutunu çalıştırdığımız zaman ilgli programın üreteceği mesajları ekrana değil "test.txt" dosyasına yazılacaktır. Buradan hareketle diyebiliriz ki hata mesajlarının "stderr" dosyasına yazdırılması iyi bir tekniktir. Eğer ileride "IO" yönlendirmesi yaparsak hata mesajları yönlendirilen dosyaya yazılacaktır. Eğer yönlendirme yapmaz isek yine ekrana çıkmaya devam edecektir. Bir nevi ileriye dönük yatırım olarak da görebiliriz. * Örnek 1, herhangi bir "IO" yönlendirmesi yapmazsak: #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # This is a output => stdout This is a output => stderr */ fprintf(stdout, "This is a output => stdout\n"); fprintf(stderr, "This is a output => stderr\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, "./sample > test.txt" şeklinde bir yönlendirme yaparsak: #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # This is a output => stderr */ fprintf(stdout, "This is a output => stdout\n"); fprintf(stderr, "This is a output => stderr\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, "./sample 2> test.txt" şeklinde bir yönlendirme yaparsak: #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # This is a output => stdout */ fprintf(stdout, "This is a output => stdout\n"); fprintf(stderr, "This is a output => stderr\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, "find / -name "sample.c"" biçiminde bir komut çalıştırdığımız zaman karşımıza aşağıdaki çıktılar gelecektir: $ find / -name "sample.c" find: '/lost+found': Permission denied find: '/home/redxx': Permission denied find: '/home/objc': Permission denied find: '/root': Permission denied find: '/run/cryptsetup': Permission denied find: '/run/cups/certs': Permission denied find: '/run/httpd': Permission denied //... * Örnek 5, "find / -name "sample.c" 2> err.txt" biçiminde bir komut çalıştırdığımız zaman karşımıza aşağıdaki çıktılar gelecektir(https://www.tutorialspoint.com/linux_terminal_online.php): $ find / -name "sample.c" 2> err.txt $ * Örnek 6, "find / -name "sample.c" 2> /dev/NULL" biçiminde bir komut çalıştırdığımız zaman, hata mesajları "/dev/NULL" dosyasına yazılacaktır ki bu dosya kendisine yazılanların hepsini silmektedir. Dipsiz bir kuyu gibi de düşünebiliriz. Dolayısıyla hata mesajları için yeni bir dosya oluşturmak istemiyorsak, iş bu aygıt sürücüsünü kullanabiliriz. > Hatırlatıcı Notlar: >> Windows Sistem Programlama kursu da açılabilir, CSD tarafından. >> Aygıt sürücüler "open" fonksiyonu ile açılmaktadır ve parametre olarak aygıt sürücüyü temsil eden bir dizin girişi belirtilir. Örneğin, fd = open("/dev/NULL", O_WRONLY); Ancak bu dizin girişi gerçek bir dosyaya ait değildir. Bu giriş için sadece bir adet "i-node" elemanı bulundurulmaktadır. İşletim sistemi böyle bir dosya açılmaya çalışıldığında, aslında bir aygıt sürücü ile işlem yapılmak istediğini anlamaktadır. Yani aygıt sürücü bir dosya gibi açılıyor olsada, bir dosya ile ilgisi yoktur. Aygıt dosyaları, "dummy" bir dosyadır ve "kernel" içerisindeki aygıt sürücüsünü temsil etmektedir. Buradaki dizin girişleri aygıt sürücüye ulaşmak için kullanılan ara bir dosyadır. /*================================================================================================================================*/ (21_08_01_2023) > "/dev/zero" aygıt sürücüsünden okuma yapılması: Bu aygıt sürücüsünden okunan her bayt sıfır olarak okunur. Ek olarak, bu aygıt sürücüsüne yazılanlar da atılacaktır. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 0 0 0 0 0 0 0 0 0 */ FILE* f; if((f = fopen("/dev/zero", "rb")) == NULL) { fprintf(stderr, "cannot open the file!...\n"); exit(EXIT_FAILURE); } int ch; for(int i = 0; i < 10; ++i) { if((ch = fgetc(f)) == EOF) { fprintf(stderr, "cannot read from the file!...\n"); exit(EXIT_FAILURE); } fprintf(stdout, "%d ", ch); fflush(stdout); } fclose(f); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > "/dev/random" aygıt sürücüsünden okuma yapılması: Döngünün her turunda ilgili aygıt sürücüsünden rastgele bayt okunmaktadır. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # 39 - 57 B8 - 184 AE - 174 39 - 57 D8 - 216 BD - 189 AD - 173 F8 - 248 72 - 114 58 - 88 */ FILE* f; if((f = fopen("/dev/random", "rb")) == NULL) { fprintf(stderr, "cannot open the file!...\n"); exit(EXIT_FAILURE); } int ch; for(int i = 0; i < 10; ++i) { if((ch = fgetc(f)) == EOF) { fprintf(stderr, "cannot read from the file!...\n"); exit(EXIT_FAILURE); } fprintf(stdout, "%02X - %d\n", ch, ch); fflush(stdout); } fclose(f); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # 1F 3F F5 10 4E A5 2C 83 B3 85 45 B2 0D BC BC 15 91 BA FC 54 60 9E 78 63 B7 BF 37 80 DC 85 19 86 07 53 3C 5C FC C9 6C C1 EA 71 A0 92 C9 42 14 55 11 30 79 F7 E3 BB E2 D9 64 56 C0 3E 4C 82 33 5F D1 09 EA A1 BD 86 59 84 6C C0 74 75 0B 7B EA 4A E8 A3 9E 3E 7E 45 35 AE 85 6C 87 27 10 75 75 4B 6E DC D8 02 07 B7 7A 6B 4E 7B CA 7D 28 6B 11 C0 14 AC AF 4A 9C 87 02 91 E1 53 91 AD FE 2B F4 8C 5B 18 E2 9C DF CD F0 0D 7B 02 6B F8 F1 A0 57 80 7F 29 E3 F3 06 BB F1 A1 0C F3 0B FA A8 DE 23 F9 A6 CC B8 D1 19 36 6F A7 28 2B C7 C2 F6 F9 BE 4A D5 1E 8F 0F D3 9E B4 10 2D 57 8F 8B 4B 12 9A 98 51 8F 15 98 84 71 C7 50 FC EC 71 36 67 C0 F1 DE F2 CE 28 FF 20 9D 70 0A A7 82 B7 54 4D 82 DA 73 2D C3 E3 34 32 82 E7 97 F9 1D CD 97 EC BE 8E F6 64 A4 3B 83 F1 2E 89 BE 6F B9 81 21 05 5A D3 22 */ FILE* f; if((f = fopen("/dev/random", "rb")) == NULL) { fprintf(stderr, "cannot open the file!...\n"); exit(EXIT_FAILURE); } int ch; for(int i = 0; i < 256; ++i) { if((ch = fgetc(f)) == EOF) { fprintf(stderr, "cannot read from the file!...\n"); exit(EXIT_FAILURE); } fprintf(stdout, "%02X%c",ch, i % 16 == 15 ? '\n' : ' '); fflush(stdout); } fclose(f); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Kabuk üzerinde "pipe" işlemleri: Bu konunun detaylarına ileride değineceğiz fakat şimdilik sadece bir giriş yapmak gerekirse; kabuk üzerinden "a | b" şeklinde bir komut çalıştıralım. Burada "a" ve "b" iki ayrı program olsun. Kabuk, "a" programının dosya betimleyici tablosundaki bir indisli betimleyiciyi kullanarak yazdığı şeyleri, "b" programı sanki sıfır indisli betimleyiciyi kullanarak okumuş havası estirmektedir. Yani "a" nın ekrana yazdığı şeyleri, sanki "b" klavyeden okuyormuş gibi bir etki oluşturuyor. Burada ilgili "a" ve "b" programlarına argüman da geçebiliriz. O zaman "a aa aaa | b bb bbb" biçiminde bir komut girmemiz gerekiyor ki burada "aa" ve "aaa" argümanları "a" programına aitken "bb" ve "bbb" argümanları ise "b" programına ait olacaktır. * Örnek 1, /* a.c */ #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # */ for(int i = 0; i < 10; ++i) printf("%d\n", i); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* b.c */ #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # */ int val; while(scanf("%d", &val) == 1) printf("%d\n", val); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Komuk Komutu >>> "./a | ./b" <<< Çıktı >>> 1 2 3 4 5 6 7 8 9 0 Öte yandan, "stdout" indisini kullanan iki programı, bu şekilde çalıştırdığımız zaman, her iki program da "pipe" mekanizmasından faydalanır fakat sadece bir tanesininki ekrana basılır. Örneğin, "ls -l | ps -e" komutunu çalıştırdığımız zaman sadece "ps" isimli programın ekrana bastığı görülür. Bu demek değildir ki "ls" programı çalıştırılmadı. O da çalıştırıldı fakat "ps" programı okuma yapmadı. Buradan da diyebiliriz ki gönderilen bilgileri kullanmak zorunda değiliz. Bir diğer örnek; "ps -e | wc sample.c". Bu komutun çıktısı "sample.c" dosyasının kelime adedi olacaktır. Bu durumda "ps -e" komutunun çıktısı yine "pipe" a yönlendirilir fakat "wc" komutu "stdin" dosyasından okuma yapmayacağı için bu bilgileri işleme sokmayacaktır. UNIX/Linux sistemlerindeki dosya yol ifadesi alan POSIX kabuk komutlarına bir yol ifadesi verilmezse, bu komutlar "stdin" dosyasından okuma yapacak biçimde tasarlanmışlardır. Örneğin, "cat" komutu bir dosyanın içeriğini "stdout" dosyasına yazdırır ancak bu komuta argümansız kullanılırsa, okumayı "stdin" dosyasından yapacaktır. Dolayısıyla klavyeden okunan her şey direkt olarak ekrana yazılacaktır. Benzer şekilde "wc" isimli program da aynı etkiyi gösterecektir. Argüman geçmediğimiz zaman, klavyeden okuduklarına "word count" işlemi uygulayacaktır. Eğer "ps -e | wc" biçiminde bir kabuk komutu çalıştırırsak; "ps" nin ekrana yazdıklarını "wc" sanki klavyeden okuyormuş gibi yapacak. Dolayısıyla bizler bu yöntem ile o an çalışan proseslerin adedini bu şekilde öğrenebiliriz. Benzer şekilde "ps -e | more" komutunu çalıştırdığımız zaman, "ps -e" komutunun ekrana yazdırdıkları sayfa sayfa görüntülenecektir. Boru işlemleri de yinelemeli olarak kullanabiliriz. Örneğin, "a | b | c". Burada "a" nın ekrana yazdıklarını "b" klavyeden okuyormuş gibi yapıyor, "b" nin ekrana yazdıklarını da "c" programı klavyeden okuyacaktır. Eğer "pipe" mekanizması işletilmeseydi, üçüncü bir dosyaya/dosyalara ihtiyacımız vardır. Örneğin, ps -e > temp.txt wc temp.txt rm temp.txt > İşlemcilerin Koruma Mekanizması: Özünde iki temel şeyden oluşmaktadır. Bilindiği üzere prosesler RAM üzerinde çalışmaktadırlar. Bir proses, başka bir prosesin alanına girdiği zaman, o alandakileri casusluk amacıyla izleyebilir ya da oradaki bilgileri değiştirebilir ki bu iki durum da felakete yol açacaktır. İşte işlemcileri tasarlayanlar bunlar yapılamasın diye bir koruma mekanizması geliştirmişlerdir. Bu mekanizma hem proseslerin birbirlerinin alanlarına geçmesini engellemekte hem de tehlikeli "assembly" komutların çalıştırılmasının önüne geçmektedir. Fakat her türlü işlemcide böyle bir koruma mekanizmasının varlığından söz edilemez. Ek olarak "multi-processing" işlemcilerde bu mekanizmaya ihtiyaç duyulmaktadır. Çünkü diğer türlü işlemci zaten o anda bir adet prosesi çalıştırdığı için, o prosesin etki alanını bilmesine gerek yoktur. Örneğin, gömülü sistemlerde eğer "multi-processing" yapmıyorsak işlemcilerin koruma mekanizmasına ihtiyacımız yoktur çünkü zaten bir adet proses çalışmaktadır. Windows, Linux ve macOS işletim sistemlerini yükleyebilmek için işlemcinin koruma mekanizması olması gerekmektedir. İş bu koruma mekanizması, işletim sisteminin kendisini de olaya dahil etmemelidir. Yani işletim sistemleri, bu koruma mekanizmasına takılı kalmaması gerekmektedir. Peki bu mekanizma nasıl çalışmaktadır? CPU bu tip ihlalleri fark ettiği anda olayı direkt olarak işletim sistemine bildiriyor ve cezayı işletim sistemi prosesi derhal sonlandırarak kesiyor. * Örnek 1, koruma mekanizmasının ihlal eden bir program: #include int main() { /* # OUTPUT # */ char* str = (char*)0x1fc345; // putchar(*str); // Segmentation Fault return 0; } İşlemcilerin koruma mekanizmasına takılmayan bir diğer etmenler de özel proseslerdir. Bu özel prosesler işletim sisteminin kendi kodları ve aygıt sürücülerinin kodlarıdır. İş bu özel proseslere de "kernel mode" prosesler denmektedir. Bu grup haricindeki diğer üçüncü parti prosesler ki bunlara "user mode" prosesler denir, işlemcilerin koruma mekanizmasına takılmaktadır. Proseslerin "mode" bilgisi, "sudo" komutundan bağımsızdır. "sudo" da hakeza "user mode" bir program olup, dosyalara erişim konusunda ilgili prosese avantaj sağlamaktadır. Yani bir prosesi "sudo" ile çalıştırdığımız zaman o proses "kernel mode" a dönüşmemektedir. Proseslerin sahip olduğu "kernel mode", "user mode" bilgileri işlemcileri tasarlayan kişiler tarafından tasarlanmıştır. Dolayısıyla bazı işlemcilerde bu "mode" sayısı dörde kadar çıkabilmektedir. Bizler kendi programlarımızı "kernel mode" olarak genel olarak çalıştıramayız. Bunu yapmanın tek yolu, yukarıda da belirtildiği üzere, aygıt sürücüsü biçiminde kod yazmaktır. Aygıt sürücülerin yüklenmesi için "root" şifresi gerektiğinden, yine koruma sağlanmış oluyor. Bu anlatılanlara ek olarak birde şöyle bir durum vardır: sistem fonksiyonlarını çağıran prosesler, sistem fonksiyonunun işi bitene kadar, "kernel mode" seviyesine çıkartılırlar. İlgili sistem fonksiyonlarının işi bittiğinde tekrardan "user mode" seviyesine çekilirler fakat bu işlem zaman alan bir işlemdir. O halde diyebiliriz ki prosesler ömürlerinin bazı kısımlarını "user mode", bazı kısımlarını "kernel mode" olarak geçirmektedir. Bunu hesaplamak için "time" komutunu kullanabiliriz. Bu komuta çalıştırılan prosesin adını geçmemiz yeterli olacaktır. Örneğin, "time ./sample" komutu çalıştırdığımız zaman aşağıdaki çıktıyı alacağız: real 0m0,134s user 0m0,001s sys 0m0,001s Buradaki "sys", "kernel mode"; "user", "user mode"; "real" ise total zamanı temsil etmektedir. > Standart C kütüphanesindeki dosya fonksiyonları ile POSIX dosya fonksiyonları arasındaki ilişki: Standart C kütüphanelerindeki iş bu dosya fonksiyonları aslında birer sarma fonksiyonlarıdır. Yani UNIX/Linux dünyası için bu fonksiyonlar "open" POSIX fonksiyonunu çağırırken, Windows dünyası için Windows API fonksiyonlarını çağırmaktadır. C dilindeki "fopen" fonksiyonunun imzası aşağıdaki gibidir: #include FILE *fopen(const char *restrict pathname, const char *restrict mode); İş bu fonksiyonun geri dönüş değerinin türü "FILE" yapısının adresi. Peki bu "FILE" içerisinde neler var? Dosya betimleyicisi "fd" bilgisi olmak üzere, işe yarar diğer bilgiler bu yapı içerisinde tutulmaktadır. Standart C dosya fonksiyonlarının en belirgin bir özelliği "buffered" olmalarıdır. Yani kendi içlerinde "buffer" alan oluştururlar. Yani bilgiler geçici olarak bu "buffer" içerisinde saklanır. Fakat burada esasında olan bir "cache" sistemidir ama "buffer" terimi daha çok kullanılmaktadır. İş bu sebepten dolayıdır ki "FILE" yapısı içerisinde, bu "buffer" alanına ilişkin bilgiler de tutulmaktadır. Standart C dosya fonksiyonları "buffered" oldukları için sistem fonksiyonları daha az çağrılmaktadır. Dolayısıyla daha hızlıdırlar. Aşağıda buna bir örnek verilmiştir: * Örnek 1, /* Version - I */ #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # 10 20 30 30 20 10 */ int fd; if( (fd = open("q.txt", O_RDONLY)) == -1) exit_sys("open"); char ch; ssize_t result; while( (result = read(fd, &ch, 1)) > 0 ) putchar(ch); if( result == -1 ) exit_sys("read"); close(fd); putchar('\n'); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* Version - II */ #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # 10 20 30 30 20 10 */ int fd; if( (fd = open("q.txt", O_RDONLY)) == -1) exit_sys("open"); char ch; ssize_t result; char buffer[512]; while( (result = read(fd, buffer, 512)) > 0 ) for(int i = 0; i < result; ++i) putchar(buffer[i]); if( result == -1 ) exit_sys("read"); close(fd); putchar('\n'); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Burada iki numaralı versiyon daha hızlı çalışacaktır çünkü sistem fonksiyonu daha az çağrılacağı için prosesin "mode" durumu daha az bir şekilde "kernel mode" a çekilecektir. Standart C dosya fonksiyonları da bu şekilde "buffer" kullanmaktadır. Örneğin, "fgetc()" fonksiyonu ile bir bayt bile okumak istesek arka planda ilgili "buffer" kadarlık alan okunur fakat bize sadece bir baytlık kısmı döndürür. Aynı fonksiyonu tekrar çağırmamız durumunda yeniden okuma yapmaz, "buffer" içerisinden bir sonraki baytlık alanı döndürür. Bu işlem ilgili "buffer" tamamiyle okunana kadar devam eder. Bu aşamada tekrardan "fgetc" fonksiyonunu çağırmamız durumunda, yeniden bir "buffer" lık kadar okuma yapılır. Bu "buffer" alanının büyüklüğü "stdio.h" başlık dosyası içerisinde tanımlanan "BUFSIZ" sembolik sabiti kadardır. Standart C dosya fonksiyonlarındaki iş bu "buffer" sadece okumanın yapılacağı bir "buffer" değil hem okumanın hem de yazmanın yapılabileceği bir "buffer" alanıdır. Dolayısıyla yazma işleminden evvel önce bu "buffer" alanına yazılmaktadır. Bu noktada da "write" sistem fonksiyonu devreye girmektedir. Bir "buffer" alanının "write" fonksiyonu ile diske aktarılması işlemine de "flush" işlemi denmektedir. Eğer bizler "fflush" isimli standart C fonksiyonunu çağırırsak, "buffer" içerisindeki bilgiler diske yazılacaktır. Aksi halde "buffer" alanının dolması beklenecektir. "fgetc" fonksiyonunun karşılığı da bu durumda "fputc" fonksiyonudur. Son olarak "fclose" fonksiyonu çağrıldığında ilgili "buffer" alanından diske yazım yapılmaktadır. Yine unutmamalıyız ki "fflush" fonksiyonunu çağırabilmek için ilgili dosyanın "w" ya da "r+" modda açılması gerekmektedir. > Hatırlatıcı Notlar: >> "ps" isimli kabuk komutu, o an çalışan proseslerin listesini vermektedir. >> "wc" isimli kabuk komutu, argüman olarak geçilen dosyadaki kelimeleri saymaktadır. >> "more" isimli kabuk komutu, çıktıyı sayfa sayfa görüntülemek için kullanılır. >> CSD'nin Sembolik Makina Dili kursunu da takip et. >> Bir aygıt sürücüsünü diğer programlardan ayıran en önemli özelliği, aygıt sürücüsünün "kernel mode" çalışan programlar olmasıdır. >> Sistem fonksiyonları "kernel" içerisindeki fonksiyonlardır. Dolayısıyla böyle fonksiyonları çağıran prosesler, geçici süreyle "kernel mode" seviyesine çekilirler. /*================================================================================================================================*/ (22_14_01_2023) > Standart C kütüphanesindeki dosya fonksiyonları ile POSIX dosya fonksiyonları arasındaki ilişki (devam): Anımsanacağı üzere C dilindeki dosya açmakta kullanılan standart fonksiyonlar bize "FILE" türünden bir yapının adresini döndürmektedir. İş bu "FILE" yapısına aslında "stream" denilmekte olup, kurs boyunca bizler "Dosya Bilgi Göstericisi" ismi ile anacağız. Genel olarak "stream" denildiği zaman, özellikle haberleşme dünyasında, istenildiği kadar "bayt" ın okunabildiği sistemler akla gelmektedir. Tipik bir "FILE" yapısı aşağıdaki elemanlara sahiptir: (https://elixir.bootlin.com/uclibc-ng/latest/source/libc/sysdeps/linux/common/bits/uClibc_stdio.h#L212) struct __STDIO_FILE_STRUCT { unsigned short __modeflags; #ifdef __UCLIBC_HAS_WCHAR__ unsigned char __ungot_width[2]; /* 0: current (building) char; 1: scanf */ /* Move the following futher down to avoid problems with getc/putc * macros breaking shared apps when wchar config support is changed. */ /* wchar_t ungot[2]; */ #else /* __UCLIBC_HAS_WCHAR__ */ unsigned char __ungot[2]; #endif /* __UCLIBC_HAS_WCHAR__ */ int __filedes; #ifdef __STDIO_BUFFERS unsigned char *__bufstart; /* pointer to buffer */ unsigned char *__bufend; /* pointer to 1 past end of buffer */ unsigned char *__bufpos; unsigned char *__bufread; /* pointer to 1 past last buffered read char */ #ifdef __STDIO_GETC_MACRO unsigned char *__bufgetc_u; /* 1 past last readable by getc_unlocked */ #endif /* __STDIO_GETC_MACRO */ #ifdef __STDIO_PUTC_MACRO unsigned char *__bufputc_u; /* 1 past last writeable by putc_unlocked */ #endif /* __STDIO_PUTC_MACRO */ #endif /* __STDIO_BUFFERS */ #ifdef __STDIO_HAS_OPENLIST struct __STDIO_FILE_STRUCT *__nextopen; #endif #ifdef __UCLIBC_HAS_WCHAR__ wchar_t __ungot[2]; #endif #ifdef __STDIO_MBSTATE __mbstate_t __state; #endif #ifdef __UCLIBC_HAS_XLOCALE__ void *__unused; /* Placeholder for codeset binding. */ #endif #ifdef __UCLIBC_HAS_THREADS__ int __user_locking; __UCLIBC_IO_MUTEX(__lock); #endif /* Everything after this is unimplemented... and may be trashed. */ #if __STDIO_BUILTIN_BUF_SIZE > 0 unsigned char __builtinbuf[__STDIO_BUILTIN_BUF_SIZE]; #endif /* __STDIO_BUILTIN_BUF_SIZE > 0 */ }; Fakat unutmamalıyız ki "FILE" yapısının içerisindeki bu elemanlar, C standartlarında açıklanmış değil. Çünkü bu, implementasyonu yapan kişilere bırakılmış. İş bu elemanları kısaca özetlemek gerekirse; -> İşletim sistemi düzeyinde okuma/yazma işlemleri için gereken dosya betimleyicisi("fd") -> Sistem fonksiyonlarının çağırımını azaltmak için kullanılan tamponun başlangıç adresini tutan bir gösterici -> İlgili tamponda en son kalınan noktayı gösteren bir gösterici -> Tamponun uzunluğunu tutan bir eleman ya da tamponun bittiği yeri gösteren bir gösterici -> ... Pekiyi "FILE" yapısının yer tahsilatı nasıl yapılmaktadır? Bu konuda bir kaç yaklaşım söz konusudur. Bunlardan ilki iş bu yapı nesnesini dinamik ömürlü bir şekilde hayata getirmek ki ilgili alanın geri verilmesi "fclose()" fonksiyon çağrısı sırasnıda yapılacaktır. İkinci ise "FILE" türden elemanları olan statik bir diziden bizlere eleman vermesi biçimindedir. Üçüncü yöntem ise bu yöntemleri karma bir şekilde kullanılmasıdır. > C standart dosya fonksiyonlarındaki tamponlama mekanizmaları: Üç farklı tamponlama mekanizması kullanılmaktadır. Bunlar "Full Buffering(Tam Tamponlama)", "Line Buffering(Satır Tamponlama)" ve "No Buffering(Sıfır Tamponlama)" şeklindedir. Bu üç tamponlama yöntemi de açılan her dosya için ayarlanabilmektedir. Pekiyi nedir bu tamponlama mekanizmaları? >> "Full Buffering(Tam Tamponlama)" : Okuma ve yazma işlemleri sırasında ilk önce bahsi geçen tampon tamamiyle doldurulur. Daha sonra tek kalemde tampon boşaltılarak okuma ve yazma işlemi fiili olarak gerçekleştirilir. Tabiri caiz ise doldur, boşalt yöntemidir. Buradaki kritik nokta fiili olarak okuma/yazma yapılabilmesi için ilgili tamponun tamamiyle dolu olması gerekmekte, aksi halde iş bu fiiller gerçekleştirilmemektedir. Tampon burada tam kapasite ile kullanılmaktadır. "fflush" ve "fclose" fonksiyonları ilgili tamponu boşaltmaktadır. >> "Line Buffering(Satır Tamponlama)" : Okuma ve yazma işlemleri sırasında '\n' karakteri görülene kadar tamponlama yapılır. İş bu karakter görüldükten sonra tampon boşaltılmak suretiyle fiili olarak okuma/yazma işlemi gerçekleştirilir. Tampon boşaltılırken '\n' karakteri de boşaltılır. Bu tip tamponlama kullanmak için açık olan dosyamızın bir "text" dosyası olması gerekmektedir. "binary" modda açılan bir dosyayı bu tip tamponlama mekanizmasına çekebiliriz fakat saçma bir yaklaşım olacaktır. Yine unutmamalıyız ki '\n' görene kadarki karakterlerin adedi, tamponun büyüklüğünden fazla ise tampon dolacaktır. Bu nokta da yine tampon boşaltıldıktan sonra okuma/yazma işlemi yapılacaktır. "fflush" ve "fclose" fonksiyonları ilgili tamponu boşaltmaktadır. >> "No Buffering(Sıfır Tamponlama)" : Okuma ve yazma işlemleri sırasında tampon hiç kullanılmamaktadır. Direkt olarak okuma/yazma işlemi yapılmaktadır. Pekiyi akla şu iki soru gelmektedir; bir dosyanın varsayılan tamponlama modu nedir ve bir dosyanın tamponlama modu nasıl değiştirilir? >> C standartları bir dosyanın varsayılan tamponlama modu hakkında bir şey söylememiştir. Ancak şu üç dosya hakkında bir şeyler söylemiştir ki bu dosyalar "stdin", "stdout" ve "stderr" dosyalarıdır. Geriye kalan dosyaların varsayılan tamponlama mekanizması ilgili kütüphaneyi yazanların insifiyatına bırakılmıştır. Fakat mevcuttaki standart C kütüphaneleri genel olarak varsayılan durumda "Full Buffering(Tam Tamponlama)" modu esas almaktadır. "stdio.h" içerisinde tanımlanan "stdin", "stdout" ve "stderr" isimli dosyalar "FILE" yapı türünden birer adres belirtmektedirler. Tıpkı "fopen()" fonksiyonu ile elde ettiklerimiz gibi. Bu dosyaların "fd" değeleri de sırasıyla "0", "1" ve "2" numaralarıdır. Fakat bunlardan "1" ve "2" numaralı değerler aynı dosya nesnesini göstermektedir. İş bu "FILE" türünden nesneler, standart C fonksiyonlarında da kullanılabilirler. Örneğin, "fprintf(stdout, ...);". Dolayısıyla bu çağrı aslında "printf(...);" biçimindeki çağrıdan farksızdır. Bu üç "FILE" türden nesneyi("Dosya Bilgi Göstericileri) bizler kapatmamalıyız. "stdin" ve "stdout" dosyalarının varsayılan tamponlama mekanizması için standartlar şöyle demiştir; -> Eğer interaktif olmayan bir aygıta yönlendirilmişlerse ki dosyalar interaktif aygıtlar değildir fakat terminal interaktiftir, hiç bir zaman satır tamponlamalı ya da sıfır tamponlamalı OLAMAZ. TAM TAMPONLAMALI OLMAK ZORUNDADIR. Ancak yönlendirilmemiş iseler HİÇ BİR ZAMAN TAM TAMPONLU OLAMAZLAR. Bu durumda ya satır tamponlu ya da sıfır tamponlu olabilirler. Klavye ve ekran, yani terminal, interaktif kabul edilirker, disk dosyaları interaktif kabul EDİLMEZLER. * Örnek 1, Yazının tamponda bekletilmesi: #include int main() { /* # OUTPUT # */ printf("Hello World"); /* * İşin başında "stdout" dosyası interaktif terminale yönlendirilmiş vaziyette. Dolayısıyla * satır tamponlama ya da sıfır tamponlama yapmakta. Sıfır tamponlama yapsaydı "Hello World" yazıdı direkt * ekrana çıkacaktı ve programın akışı sonsuz döngüye girecekti. Fakat görüyoruz ki satır tamponlama yapılmış. * Bu da demektir ki ya '\n' karakteri görülecek ya da "fflush()" ya da "fclose()" gibi fonk. çağrılacak ya da * programın akışı "return" deyimine gelmeli ki tampon boşaltılsın. Biz ise sonsuz döngü oluşturduğumuz için * yukarudaki şartlardan hiç biri sağlanmadığı için "Hello World" yazısı tamponda bekletilmektedir... * Microsoft'un C kütüphanesinde sıfır tamponlama yapmaktadır. Dolayısıyla o derleyicide derlediğimiz zaman * yazı direkt olarak ekrana basılacaktır. */ for(;;) ; return 0; } * Örnek 2, #include int main() { /* # OUTPUT # Hello World */ printf("Hello World"); // printf("Hello World\n"); // APPROACH - I : İmleç bir aşağı satıra geçecektir. // fflush(stdout); // APPROACH - II : İmleç bir aşağı satıra geçmeyecektir. /* * Yukarıdaki iki yöntemden birisini "glibc" kütüphanelerinde kullanmamız durumunda ilgili yazı * ekrana direkt olarak basılacaktır. */ for(;;) ; return 0; } * Örnek 3, #include int main() { /* # OUTPUT # Hello World */ /* * Fakat şöyle de bir şey vardır; "stdin" dosyasından okuma yapıldığında "stdout" dosyası * "flush" edilmektedir fakat bu durum C standartlarında bir zorunluluk değildir. */ printf("Hello World"); getchar(); for(;;) ; return 0; } * Örnek 4, İmleci bir aşağı satıra geçirmeden ilgili yazıya direkt ekrana bastırmanın diğer yolu: #include int main() { /* # OUTPUT # Hello World */ setbuf(stdout, NULL); printf("Hello World"); for(;;) ; return 0; } "stderr" dosyasının varsayılan tamponlama mekanizması için standartlar şöyle demiştir; -> İster interaktif aygıta ister interaktif olmayan bir aygıt yönlendirilsin, HİÇ BİR ZAMAN TAM TAMPONLU OLAMAZ. Ancak satır ya da sıfır tamponlamalı olur. >> Dosyanın tamponlama modları iki fonksiyon ile değiştirilmektedir. Bu fonksiyonların isimleri sırasıyla "setbuf" ve "setvbuf" isimli fonksiyonlarıdır. Burada "setvbuf" fonksiyonu, işlevsellik açısından "setbuf" fonksiyonunu da kapsamaktadır. Fakat bu dosyaları kullanabilmek için, dosyayı açtıktan sonra hiç bir işlev yapmadan, direkt bu fonksiyonlara çağrı yapmalıyız. Aksi halde Tanımsız Davranışa yol açacağız. >>> "setbuf" fonksiyonu: Standart bir C fonksiyonudur ve parametrik yapısı aşağıdaki gibidir; #include void setbuf(FILE * stream, char * buf); Birinci parametresi bir "FILE" türünden adres, ikinci parametresi ise "buffer" olarak kullanılacak alanın başlangıç adresi. Böylelikle bizler kendi tamponumuzun kullanılmasını sağlayabiliriz. Bu fonksiyon tamponlama yöntemini değiştirmemektedir. Eğer ikinci parametreye NULL geçmeniz durumunda, "No Buffering(Sıfır Tamponlama)" moduna geçecektir. İkinci parametreye geçilen yeni tampon bölgesinin alanıda yine "BUFSIZ" büyüklüğünde olmalıdır. * Örnek 1, #include #include int main() { /* # q.txt # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # A [A] [h] [m] [e] [t] [ ] [K] [a] [n] [d] [e] [m] [i] [r] [ ] [P] [e] [h] [l] [i] [v] [a] [n] [l] [i] [] [] [] [] [] [] [] */ FILE* f; if((f = fopen("q.txt", "r")) == NULL) exit_sys("fopen"); char buffer[BUFSIZ]; setbuf(f, buffer); int ch; ch = fgetc(f); putchar(ch); putchar('\n'); for(int i = 0; i < 32; ++i) printf("[%c]%c", buffer[i], i % 16 == 15 ? '\n' : ' '); putchar('\n'); fclose(f); return 0; } >>> "setvbuf" fonksiyonu: Standart bir C fonksiyonudur. Hem halihazırdaki tamponu değiştirmek hem tamponlama yöntemini hem de tamponun boyutunu değiştirmek için kullanılır. Parametrik yapısı aşağıdaki gibidir; #include int setvbuf(FILE * stream, char * buf, int type, size_t size); Birinci parametre "FILE" türünden adres, ikinci parametresi yeni tampon olarak kullanılacak alanın başlangıç adresi ki bu parametreye NULL geçilmesi durumunda tampon değiştirilmeyecektir, üçüncü parametre tamponlama modu ki şu sembolik sabitlerden birisi olmalıdır; -> _IOFBF : Full Buffering(Tam Tamponlama) -> _IOLBF : Line Buffering(Satır Tamponlama) -> _IONBF : No Buffering(Sıfır Tamponlama) Son parametre ise tamponun uzunluk bilgisidir. Programcı ikinci parametreye NULL adres geçip son parametre üzerinden halihazırdaki tamponun uzunluğunu da değiştirebilir. Eğer üçüncü parametreye "_IONBF" geçmemiz durumunda, ikinci ve son parametreler işlevsiz hale gelecektir. Fonksiyonumuz başarı durumunda "0" değerine, başarısızlık durumunda ise "non-zero" bir değere dönmektedir ve POSIX sistemlerinde "errno" değeri uygun biçimde değiştirilmektedir. Fakat halihazırdaki tamponlama modunu bize döndüren standart bir C fonksiyonu mevcut değildir. * Örnek 1, #include #include int main() { /* # q.txt # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # A Ahmet Kandemir Pehlivanli�y< */ FILE* f; if((f = fopen("q.txt", "r")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } /* * Yeni tampon bölgemizin çöp değerler ile hayata geldiğini * unutmayalım. */ char buffer[512]; if(setvbuf(f, buffer, _IOLBF, 512) != 0) { fprintf(stderr, "cannot set the buffer!...\n"); exit(EXIT_FAILURE); } int ch; ch = fgetc(f); putchar(ch); putchar('\n'); for(int i = 0; i < 32; ++i) putchar(buffer[i]); putchar('\n'); fclose(f); return 0; } Bu iki fonksiyona ek olarak "setbuffer" ve "setlinebuf" isimli iki fonksiyon daha vardır fakat bunlar ne C standartlarında ne de POSIX standartlarında yer alan fonksiyonlardır. Sadece "glibc" kütüphanesinde bulunurlar. Dolayısıyla bunların kullanılması pek tavsiye edilmezler. > Bazı POSIX dosya fonksiyonları: >> "fileno" isimli bu fonksiyon, argüman olarak bir "FILE" türden yapı nesnesinin adresini almakta ve iş bu nesnenin içerisindeki "int" türden "fd" değişkenini geri döndürmektedir. Böylelikle C standart fonksiyonunu kullanarak açtığımız bir dosyayı, POSIX fonksiyonları kullanarak işleyebiliriz. İş bu fonksiyonun prototipi şöyledir: #include int fileno(FILE *stream) POSIX standartlarına göre bu fonksiyon başarısız olabilir ve bu durumda "-1" ile geri dönecektir. Fakat bu fonksiyon, tam randımanlı bir kontrol yapamadığı için geçersiz "FILE" türden nesnelerde de başarılı olabilir. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # fileno: Bad file descriptor */ FILE* f; f = malloc(sizeof(FILE)); if(fileno(f) == -1) exit_sys("fileno"); printf("OK\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, "FILE" yapısı içerisindeki "fd" çekilmiştir: #include #include #include void exit_sys(const char* msg); int main() { /* # q.txt # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # Ahmet Kand */ FILE* f; if((f = fopen("q.txt", "r")) == NULL) { fprintf(stderr, "cannot open the file!...\n"); exit(EXIT_FAILURE); } /* * Unutmamalıyız ki "fileno" fonksiyonu ile "FILE" içerisindeki "fd" yi çekerek işlem yaptığımız * zaman, iş bu "fd" tarafından gösterilen "file" yapısına ait "file pointer" da değiştirilmiş * olur. Yani okuma işlemine yukarıdaki "f" değişkeni üzerinden devam edersek, başka konumdan * itibaren okumaya başlayabiliriz. Bu konuda temkinli olmalıyız. Fakat genellikle bir soruna * yol açmamaktadır. */ int fd; if((fd = fileno(f)) == -1) exit_sys("fileno"); char buffer[10 + 1]; ssize_t result; if((result = read(fd, buffer, 10)) == -1) exit_sys("read"); buffer[result] = '\0'; puts(buffer); fclose(f); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "fdopen" fonksiyonu, "fileno" fonksiyonunun yaptığının tam tersini yapmaktadır. POSIX fonksiyonu ile başladığımız yola, C fonksiyonları ile devam edebilmemize olanak sağlamaktadır. Yani parametre olarak bir "fd" almakta, geri dönüş değeri de "FILE" türden yapının adresini oluşturmaktadır. İlgili fonksiyonunun imzası aşağıdaki gibidir; #include FILE *fdopen(int fildes, const char *mode); Fonksiyonun birinci parametresi ilgili "fd", ikinci parametresi ise dosyanın açış modlarına ilişkindir. Başarısızlık durumunda "NULL" adresine geri dönerken, başarı durumunda "FILE" nesnesinin adresini döndürür. Unutulmamalıdır ki ikinci parametre ile "open" fonksiyonu açarken geçilen açış modlarının birbiri ile uyumlu olması gerekmektedir. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main() { /* # q.txt # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # Ahmet Kandemir Pehlivanli */ int fd; if((fd = open("q.txt", O_RDONLY)) == -1) exit_sys("open"); FILE* f; if((f = fdopen(fd, "r")) == NULL) exit_sys("fdopen"); int ch; while((ch = fgetc(f)) != EOF) putchar(ch); if(ferror(f)) { fprintf(stderr, "cannot read file!...\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> "musl libc", "diet libc", "Standart C Library by P.J. Plauger" : Çeşitli C kütüphanelerini, özellikle "stdio", incelemek için. >> Advanced Programming in the UNIX Environment, 3rd Edition by W. Stevens & Stephen Rago : Bu kursa takviye olacak bir kitap. /*================================================================================================================================*/ (23_15_01_2023) > C standart dosya fonksiyonlarındaki tamponlama mekanizmaları (devam): Windows sistemlerindeki C derleyicilerinde varsayılan, yani herhangi başka bir yere yönlendirilme yapılmamışsa, "stdout" sıfır tamponlamalıyken "stdin" satır tamponlamalıdır. Buna karşın UNIX/Linux sistemlerinde her iki dosya da satır tamponlamalıdır. Fakat unutmamalıyız ki "stdin", "stdout" ve "stderr" dosyaları ilgili derleyiciler tarafından kapatılacağı için otomatik olarak "flush" da edilmektedirler. Son olarak "stdin" ve "stdout" arasında bir senkronizasyon sağlanması açısından, "stdin" dosyasından okuma yapıldığında "stdout" dosyası da "flush" edilmektedir fakat bu durum C standartlarında garanti edilmemiştir ama çoğu C derleyicisi bu özelliktedir. * Örnek 1, #include int main() { /* * İlgili yazı Windows sistemlerinde ekrana basılacak fakat UNIX/Linux sistemlerinde basılmayacak */ printf("Hello World"); for(;;) ; return 0; } * Örnek 2, #include int main() { /* * İlgili yazı programın sonlanmasından doğan "flush" nedeniyle her zaman ekrana basılacaktır. */ printf("Hello World"); return 0; } * Örnek 3, #include int main() { /* * Fakat bizler programın sonlanmasını beklemeden, ilgili yazının sonuna '\n' karakteri koyarak, yazının * her iki sistemde de ekrana basılmasını garanti edebiliriz. Fakat bu durumda imleç bir alt satıra * geçecektir. */ printf("Hello World\n"); for(;;) ; return 0; } * Örnek 4, #include int main() { /* * Eğer bizler hem imleç bir alt satıra geçmesin hem de ilgili yazı ekrana her şekilde basılsın * istiyorsak, önümüzde iki seçenek vardır: * Ya "fflush(stdout)" çağrısı ile ilgili tamponu elle boşaltmak, * Ya "stdout" tamponunu işin başında sıfır tamponlamalı moda çekmek. */ setvbuf(stdout, NULL, _IONBF, 0); // APPROACH - II setbuf(stdout, NULL); // APPROACH - III printf("Hello World"); fflush(stdout); // APPROACH - I for(;;) ; return 0; } * Örnek 5, #include int main() { /* * "stdin" dosyasından okuma yapılmadan evvel "stdout" dosyası "flush" edilmektedir fakat * bu durum C standartlarında garanti altına alınmamıştır. Dolayısıyla ekrana ilgili * yazı her halükarda çıkacaktır eğer iş bu derleyici bu özelliği sunuyorsa. */ printf("Hello World"); getchar(); return 0; } UNIX/Linux sistemlerinde "stdin" dosyası, dosyaya yönlendirilmemiş ise satır tamponlamalı moddadır. Dolayısıyla bizler bir klavyeden bir karakter dahi okusak, '\n' karakteri görülene kadarki bilgiler tampona çekilecek ve bu tampondan bizlere bir karakter verilecektir. Tampon dolu olduğu müddetçe, her bir karakter okumak istediğimizde bu tampondan veri alacağız. Burada unutmamamız gereken nokta '\n' karakterinin kendisi de tampona çekilmesidir. Tabii "scanf", "getchar", "gets" gibi fonksiyonlar ortak tampondan çalışmaktadırlar. Yani bu fonksiyonlar da "stdin" dosyasından okuma yaparlar. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main() { /* # INPUT # Ali */ /* # OUTPUT # [A] [l] [i] [ ] */ int ch; /* * Aşağıdaki "getchar()" çağrısında "Ali" yazısını girmemiz durumunda, * "Ali\n" komple "stdin" tamponuna çekilir ve bu tampondaki ilk * karakter olan 'A' karakteri geri döndürülür. */ ch = getchar(); printf("[%c]\n", ch); /* * "stdin" tamponu hala dolu olduğu için bizlere sıradaki karakter olan * 'l' karakteri tampondan döndürülür. */ ch = getchar(); printf("[%c]\n", ch); /* * "stdin" tamponu hala dolu olduğu için bizlere sıradaki karakter olan * 'i' karakteri tampondan döndürülür. */ ch = getchar(); printf("[%c]\n", ch); /* * "stdin" tamponu hala dolu olduğu için bizlere sıradaki karakter olan * '\n' karakteri tampondan döndürülür. */ ch = getchar(); printf("[%c]\n", ch); /* Bu noktada tampon boş olduğu için artık yeni bir satırlık bilgi isteyecektir. */ return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi bizler ikinci "getchar()" çağrısında da klavyeden giriş almak istesek, nasıl bir yol izlemeliyiz? "fflush(stdin)" çağrısı geçersiz bir çağrıdır çünkü standartlara göre "read-only" modda açılmış bir dosya "flush" edilemezler. Fakat bazı derleyiciler bu özelliği desteklemektedir ama bundan kaçınmalıyız. Bu durumda geriye kalan şey tamponu elle boşaltmaktır. Çünkü "stdin" dosyasını sıfır tamponlu moda da ÇEKEMEYİZ. İlgili tamponu boşaltan özel bir fonksiyon da standartlarda mevcut olmadığından, bizler yeni bir fonksiyon yazmalıyız. Fakat tamponu boşaltırken "EOF" konumunu da kontrol etmemiz daha iyi olacaktır. * Örnek 1, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # Ali Kaan */ /* # OUTPUT # [A] [K] */ int ch; /* * Aşağıdaki "getchar()" çağrısında "Ali" yazısını girmemiz durumunda, * "Ali\n" komple "stdin" tamponuna çekilir ve bu tampondaki ilk * karakter olan 'A' karakteri geri döndürülür. */ ch = getchar(); printf("[%c]\n", ch); clear_stdin(); /* * "stdin" tamponu elle boşaltıldığı için artık klavyeden yeni bir giriş * isteyecektir. */ ch = getchar(); printf("[%c]\n", ch); return 0; } void clear_stdin(void) { int ch; /* * Aşağıda "EOF" durumunun neden sorgulandığı, ileriki derslerde * anlatılacaktır. */ while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # 120 */ /* # OUTPUT # [1] [20] [ ] */ int ch = 31; /* * Aşağıdaki "getchar()" çağrısında "120" sayısını girmemiz durumunda, * "120\n" biçiminde "stdin" tamponuna çekilir ve bu tampondaki ilk * karakter olan '1' karakteri geri döndürülür. */ ch = getchar(); printf("[%c]\n", ch); /* * Tamponda "20\n" karakterleri kaldı. Bu durumda "20" karakterleri * tampondan çekilecektir. Bu noktada artık tamponda '\n' karakteri * kaldı. */ scanf("%d", &ch); printf("[%d]\n", ch); /* * Bu noktada da tamponda son kalan '\n' karakteri çekildi ve tampon * tamamiyle boşaltıldı. */ ch = getchar(); printf("[%c]\n", ch); return 0; } void clear_stdin(void) { int ch; /* * Aşağıda "EOF" durumunun neden sorgulandığı, ileriki derslerde * anlatılacaktır. */ while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "stdin" dosyası gerek "shell" üzerinden "<" sembolü gerek "dup" fonksiyonları kullanılarak bir dosyaya yönlendirildiği vakit, dosyanın sonuna geldiğinde "EOF" durumu ile karşılaşmaktadır. Fakat herhangi bir yönlendirme yapılmadığında da terminal aygıt sürücüsünden yönlendirilmiştir ki bu durumda da klavyeden girilenleri okuyacaktır. Fakat klavyede "EOF" durumu söz konusu DEĞİLDİR. İşte klavyeden giriş yaparken "EOF" etkisi oluşturmak için bir takım tuş kombinasyonları kullanmamız gerekiyor. Windows sistemlerinde "CTRL+Z", UNIX/Linux sistemlerinde "CTRL+D" tuş kombinasyonları bu etkiyi meydana getirmektedir. Bu tuş kombinasyonları girildiğinde açık olan dosyalar kapatılmaz sadece "EOF" etkisi oluşturulur. Klavyeden okuma yaparken, kullanıcının gerçektende EOF etkisi mi oluşturmak istediğini yoksa niyetinin sadece CTRL ve Z/D tuşlarına basmak olduğunu sorgulamamız gerekmektedir. Aksi halde kullanıcı bir işlem yapmak istersek yanlışlıkla "EOF" etkisi oluşturacaktır. Burada meydana getirilen "EOF" etkisi YALANCI BİR ETKİDİR. Unutmamalıyız ki normal bir dosyayı okurken "EOF" konumuna geldiğimiz zaman tekrar okuma yaparsak yine "EOF" konumunu okuyacağız fakat tuş kombinasyonlarında bu geçerli değildir. Çünkü yalanı bir "EOF" etkisidir. * Örnek 1, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # ali CTRL + D */ /* # OUTPUT # Not EOF!!! >>> EOF <<< */ int ch; ch = getchar(); if(ch == EOF) printf(">>> EOF <<<\n"); else printf("Not EOF!!!\n"); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > C dilinde okuma yapan bazı fonksiyonların incelenmesi: İş bu fonksiyonlar "getchar", "getc", "scanf", "gets" gibi fonksiyonlardır. Bu fonksiyonlar varsayılan durumda "stdin" dosyasından okuma yapmaktadırlar. Fakat unutmamalıyız ki "gets" fonksiyonu C99 ile "deprecated" hale getirildi, C11 ile de dilden kaldırıldı fakat bir takım derleyiciler hala bünyelerinde bulundurmaktadır. C11 ile dile "gets_s" fonksiyonu da eklendi fakat bu fonksiyonun desteklenmesi derleyicilere bırakıldı. Örneğin, MSVC ve GCC derleyicileri bu fonksiyonu desteklememektedir. Bu dört fonksiyon da aynı tampon üzerinde çalışmaktadır. * Örnek 1, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # 120 */ /* # OUTPUT # No: 120 Name: [120 - ] */ int no; char name[15]; /* * Aşağıdaki "scanf" çağrısında klavyeden "120" sayısını girdiğimizi varsayalım. * Dolayısıyla tampona "120\n" karakterleri aktarılacaktır ve "scanf" fonksiyonu * "120" sayısını "no" değişkenine atayacaktır. Artık tamponda '\n' karakteri kaldı. */ printf("No: "); scanf("%d", &no); /* * Tamponda hala bir karakter olduğu için ki '\n' karakteridir, ikinci bir klavyeden * okuma yapılmadı ve iş bu karakter tampondan çekildi. Artık tampon boşaltılmış * durumdadır. */ printf("Name: "); gets(name); printf("[%d - %s]\n", no, name); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # 120 Ahmet */ /* # OUTPUT # No: 120 Name: Ahmet [120 - Ahmet] */ int no; char name[15]; /* * Aşağıdaki "scanf" çağrısında klavyeden "120" sayısını girdiğimizi varsayalım. * Dolayısıyla tampona "120\n" karakterleri aktarılacaktır ve "scanf" fonksiyonu * "120" sayısını "no" değişkenine atayacaktır. Artık tamponda '\n' karakteri kaldı. */ printf("No: "); scanf("%d", &no); clear_stdin(); /* * Tampon artık boşaltılmış olduğu için, klavyeden yeniden okuma yapıldı. */ printf("Name: "); gets(name); printf("[%d - %s]\n", no, name); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "getc" fonksiyonuna daha sonra değinilecektir. >> "getchar" fonksiyonu, "stdin" dosyasından bir karakter okuyan bir fonksiyondur. Eğer tampon tamamiyle boş ise yeni bir satırı tampona çekiyor. "gets" ve "scanf" gibi fonksiyonlar, arka planda "getchar" kullanılarak yazılmış fonksiyonlardır. Yani en temel fonksiyondur diyebiliriz. İş bu fonksiyon, dosyt sonuna geldiğinde ya da "IO" hatası olduğunda "EOF" değeri ile geri dönmektedir. Fonksiyonun prototipi şöyledir; #include int getchar(void); Görüldüğü üzere fonksiyonun geri dönüş değeri "int" türünden. Bunun sebebi tampondaki karakter "0xFF" mi yoksa dosya sonuna mı gelindiğinin bilgisi birbirinden ayırt edilemezdi. Yani kullanıcı "-1" mi girdi yoksa "EOF" konumuna mı gelindi. >> "gets" fonksiyonu, C11 ile dilden kaldırılmış olmasına rağmen derleyiciler bünyelerinde bu fonksiyonu muhafaza etmektedir. Bu fonksiyon, '\n' de dahil olmak üzere okuma yapıyor. Sonrasında parametre olarak geçilen "buffer" alanına '\n' de dahil olmak üzere tampondakileri "buffer" a aktarıyor. Son olarak "buffer" dakilerin son karakterini '\0' ile değiştiriyor. Dolayısıyla tampondaki son karakter olan '\n' karakteri '\0' karakteri ile değiştirilmiş olmaktadır. Tampondaki bütün karakterler okunduğu için, tampon tamamiyle boşaltılmıştır. Eğer tamponda halihazırda karakterler varsa, iş bu fonksiyon klavyeden bir girşi beklemeyecektir. İş bu fonksiyonun prototipi de şu şekildedir; #include char *gets(char *s); Fonksiyon, argüman olarak aldığını geri döndürüyor. Eğer hiç okuma yapmadan direkt olarak "EOF" ile karşılaşırsa, NULL ile geri dönmektedir. "gets" fonksiyonunun dilden kaldırılma sebebi, argüman olarak geçilen "buffer" ın büyüklüğünün yeteri kadar büyük olmaması durumunda, ilgili "buffer" da TAŞMA MEYDANA GELECEĞİDİR. Bu fonksiyon yerine sunulan fakat derleyicilere opsiyonel olarak bıraklın "gets_s" fonksiyonu taşmaya karşı korumalıdır. Bu durumda bizler bi' "gets_s" fonksiyonu da yazabiliriz. "gets_s" fonksiyonunun prototipi de aşağıdaki gibidir; #include char *gets_s( char *str, rsize_t n ); Fonksiyonun birinci parametresi, yazının kaydedileceği "buffer" alanı. İkinci parametresi ise yazının büyüklüğü. "rsize_t" de yine bir tür eş ismi olur, hangi türe ait olduğu derleyicilere opsionel olarak sunulmuştur. Yine "gets_s" gibi çalışan fakat standartlarda olan bir diğer fonksiyon da "fgets" fonksiyonudur. Bu fonksiyonun prototipi de aşağıdaki gibidir; #include char *fgets( char *str, int count, FILE *stream ); İş bu fonksiyonun birinci parametresi, yazının yazılacağı dizinin başlangıç adresi. İkinci parametresi dizinin büyüklüğü. Son parametresi ise kaynak olarak kullanılacak dosyaya ait bilgiler. Bu fonksiyon, tampondaki '\n' karakterini de diziye yerleştirmektedir. Dolayısıyla bizler elle iş bu '\n' karakterini '\0' karakteri ile değiştirmekte fayda vardır. * Örnek 1, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # ankara */ /* # OUTPUT # [97 - nkara] */ /* * Klavyeden "ankara" girdiğimiz zaman, "getchar" sadece 'a' karakterini * bize döndürecektir. Artık tamponda "nkara\n" karakterleri kaldı. */ int ch; ch = getchar(); /* * Tamponda halihazırda karakterler olduğu için, yeniden bir klavyeden * giriş istenmedi. Tampondakiler "name" isimli diziye yerleştirildi. */ char name[15]; gets(name); printf("[%d - %s]\n", ch, name); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # CTRL + D */ /* # OUTPUT # EOF ile karsilasildi. */ char name[15]; if(gets(name) == NULL) printf("EOF ile karsilasildi.\n"); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # *** stack smashing detected ***: terminated */ char name[5]; if(gets(name) == NULL) printf("EOF ile karsilasildi.\n"); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Temsili "gets" fonksiyonunun yazımı: #include #include #include char* my_gets(char* s); void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # Ali Kandemir */ /* # OUTPUT # [Ali] [Kandemir] *** stack smashing detected ***: terminated */ char name[5]; my_gets(name); printf("[%s]\n", name); return 0; } char* my_gets(char* s) { int index = 0; int ch; while((ch = getchar()) != '\n' && ch != EOF) { s[index] = ch; ++index; } if(index == 0 && ch == EOF) return NULL; s[index] = '\0'; return s; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5, Temsili "gets_s" fonksiyonunun yazımı: #include #include #include char* my_gets_s(char* s, size_t n); void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # Ahmet Kandemir CTRL + D */ /* # OUTPUT # [Ahme] [Kand] [] */ char name[5]; my_gets_s(name, 5); printf("[%s]\n", name); return 0; } char* my_gets_s(char* s, size_t n) { int ch; size_t i; //for(i = 0; (i < n - 1) && (ch = getchar()) != '\n' && (ch != EOF); ++i) for(i = 0; i < n - 1; ++i) if((ch = getchar()) != '\n' && ch != EOF) s[i] = ch; s[i] = '\0'; if(i == 0 && ch == EOF) return NULL; return s; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 6, #include #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # Pehlivanli Ahmet */ /* # OUTPUT # [Pehlivanl] [Ahmet ] */ char name[64]; fgets(name, 10, stdin); printf("[%s]\n", name); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 7, #include #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # Pehlivanli Ahmet */ /* # OUTPUT # [Pehlivanl] [Ahmet] */ char name[64]; fgets(name, 10, stdin); char* str; if((str = strchr(name, '\n')) != NULL) *str = '\0'; printf("[%s]\n", name); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "scanf" fonksiyonu, yine tampondan karakter karakter okuma yapan fakat başarılı yerleştirmelerin adedini geri döndüren bir fonksiyondur. Başarısız ilk yerleştirmede fonksiyon işlevini sonlandırıyor. Eğer hemen "EOF" ile karşılaşırsa, "EOF" ile geri dönüyor. Buradaki önemli olan nokta format karakterine uygun olup olmaması, ilgili karakterin. Prototipi şöyledir; #include int scanf ( const char * format, ... ); İş bu fonksiyon kelimelerin arasındaki boşluk karakterlerini ve kelimelerin arasındaki boşluk karakterini atmakta, kelimenin sonundaki boşluk karakterlerini ki buna '\n' de dahildir, ATMAMAKTADIR. İş bu fonksiyonu kullanırken, format karakterlerinin arasına yazacağımız karakterleri, giriş yaparken de kullanmalıyız. İş bu fonksiyon, başarısız girişimde girişleri tekrardan tampona bıraktığı için, tamponu elle temizlememek gerekebilir. * Örnek 1, #include #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # 10 ali */ /* # OUTPUT # result: 1, a: 10, b: 482268176 */ int a, b; int result; /* * Çıktıtan da görüldüğü üzere girişlerin başındaki ve aralardaki * boşluk karakterleri atılmıştır. Başarılı giriş sadece bir * adet olduğu için, "1" ile geri dönmüştür. "ali" yazısı da * değerlendirilmiş fakat tampona geri verilmiştir. */ result = scanf("%d%d", &a, &b); printf("result: %d, a: %d, b: %d\n", result, a, b); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # 10/15 */ /* # OUTPUT # result: [2], a: [10], b: [15] */ int a, b; int result; result = scanf("%d/%d", &a, &b); printf("result: [%d], a: [%d], b: [%d]\n", result, a, b); return 0; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include char* my_gets_s(char* s, size_t n); void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # 10AnkarA */ /* # OUTPUT # result: [1], a: [10], b: [978329392] AnkarA */ int a, b; int result; char buffer[1024]; result = scanf("%d/%d", &a, &b); printf("result: [%d], a: [%d], b: [%d]\n", result, a, b); puts(gets(buffer)); return 0; } char* my_gets_s(char* s, size_t n) { int ch; size_t i; //for(i = 0; (i < n - 1) && (ch = getchar()) != '\n' && (ch != EOF); ++i) for(i = 0; i < n - 1; ++i) if((ch = getchar()) != '\n' && ch != EOF) s[i] = ch; s[i] = '\0'; if(i == 0 && ch == EOF) return NULL; return s; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, #include #include #include #include int disp_menu(void); void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # 4 1 2 3 Q */ /* # OUTPUT # Quitting... Adding record... Deleting record... Listing record... -Infinite Loop- */ int option; for(;;) { option = disp_menu(); switch(option) { case 1: printf("Adding record...\n"); break; case 2: printf("Deleting record...\n"); break; case 3: printf("Listing record...\n"); break; case 4: printf("Quitting...\n"); goto EXIT; } } EXIT: return 0; } int disp_menu(void) { printf("1- Add A Record.\n"); printf("2- Delete A Record.\n"); printf("3- List A Record.\n"); printf("4- Quit.\n"); int option; printf("\nChoose an item: "); fflush(stdout); /* * Bizler format karakterine uygun olmayan bir giriş yaptığımız zaman, örneğin 'a' karakteri, * "scanf" fonksiyonu ilgili karakteri tampona geri yerleştiriyor ve fonksiyon sonlanıyor. * "main" içerisindeki "for loop" dolayısıyla tekrardan bu fonksiyon çağrılıyor. Fakat tampon * hala boşaltılmadığı için, klavyeden giriş beklemeden, direkt olarak tampondakini yerleştirmeye * çalışıyor. Yine başarısız oluyor. Az evvelki senaryo tekrar ediyor, ediyor, ediyor... * Bunun önüne geçmek için "scanf" çağrısı sonrasında tamponu boşaltmalıyız. */ scanf("%d", &option); return option; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5, #include #include #include #include int disp_menu(void); void clear_stdin(void); void exit_sys(const char* msg); int main() { /* # INPUT # 4 1 2 3 Q -1 */ /* # OUTPUT # Quitting... Adding record... Deleting record... Listing record... Invalid option!!! Invalid option!!! */ int option; for(;;) { option = disp_menu(); switch(option) { case 1: printf("Adding record...\n"); break; case 2: printf("Deleting record...\n"); break; case 3: printf("Listing record...\n"); break; case 4: printf("Quitting...\n"); goto EXIT; } } EXIT: return 0; } int disp_menu(void) { printf("1- Add A Record.\n"); printf("2- Delete A Record.\n"); printf("3- List A Record.\n"); printf("4- Quit.\n"); int option; printf("\nChoose an item: "); fflush(stdout); if(scanf("%d", &option) != 1 || (option < 0 || option > 4)) { printf("Invalid option!!!\n"); clear_stdin(); return -1; } return option; } void clear_stdin(void) { int ch; while((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } İş bu fonksiyon, başarısız bir giriş okuduğunda ilgili girişi tekrardan tampona bıraktığından bahsetmiştik. İşte bunu yaparken kullandığı fonksiyon ise "ungetc" isimli fonksiyondur. Fonksiyonun prototipi şöyledir; #include int ungetc( int ch, FILE *stream ); Bu fonksiyon da standart bir C fonksiyonudur. Başarı durumunda tampona bırakılan karaktere, başarısızlık durumunda ise "EOF" a geri dönmektedir. >> "getc" fonksiyonu, bir dosyayı "byte byte" okurken işe yaramaktadır. Bilindiği üzere standart C fonksiyonları tamponlu fonksiyonlardır ki gereksiz yere sistem fonksiyonları çağırarak sistemi yavaşlatmasın. Fakat iş bu standart C fonksiyonlarının da çağırmanın bir maliyeti vardır. İşte bu maliyeti azaltmak adına, "fgetc" fonksiyonu yerine, "getc" isimli alternatif bir fonksiyon da C standartlarında bulundurulmuştur. Ek olarak, iş bu fonksiyon bir fonksiyonel makro olarak da yazılabilmektedir. Dolayısıyla diyebiliriz ki "fgetc" ile "getc" arasındaki tek fark, "getc" nin makro olarak da yazılabilir olmasıdır. "getc" fonksiyonunun prototipi de şu şekildedir; #include int getc(FILE *stream); İş bu fonksiyonun özellikleri de "fgetc" ile aynıdır. > UNIX/Linux sistemlerinde Prosesler Hakkında Temel Bilgiler: Daha önce de anlatıldığı üzere çalışmakta olan bütün programlara "proses" denmektedir. Dolayısıyla her bir prosesin, sistem genelinde o an için tek olan, tam sayısal bir "ID" değeri vardır. Sistem ilk "boot" edildiğinde ki "boot" kodu "0" numaralı "ID" ye ilişkin oluyor. Daha sonra "1" numaralı "ID" ye sahip başka bir proses meydana getiriliyor. Bu noktadan sonra da diğer prosesler hayata gelmeye başlıyor. İş bu "1" numaralı "ID" ye sahip prosese ise "init" prosesi denirken, artık bu noktadan sonra "0" "ID" numaralı proses kullanım dışı kalıyor, kullanılmıyor. Dolayısıyla "0" numarası da kullanılmamaktadır. Öte yandan "1" numaralı proses artık hayatını yaşamaya devam etmektedir. Hayatı biten prosesin "ID" değeri, yeni oluşturulacak proseslerce kullanılabilir fakat bilinmeyen bir "t" anında her proses kendisine has "ID" değerine sahiptir. Proseslerin "ID" değerleri, o prosese ait olak kontrol bloklarına erişmek için bir anahtar görevindedir. Buradaki mekanizma "hash-table" biçimindedir. Proseslerin "ID" değerleri sistemden sisteme değişebileceği için UNIX/Linux türevi sistemlerde "pid_t" tür eş ismi ile temsil edilmektedirler. Bu isim ise "unistd.h" ve "sys/types.h" içerisindedir. Standartlarca bu tür "işaretli" bir tür olmak zorundadır. O an çalışan prosesin "ID" değerini elde etmek için "getpid" isimli fonksiyonu kullanabiliriz. İş bu fonksiyonun prototipi aşağıdaki gibidir; #include pid_t getpid(void); Bu fonksiyon başarısız olamaz. Bir POSIX fonksiyonudur. * Örnek 1, #include #include #include int main() { /* # OUTPUT # 5145 5145 */ pid_t pid; pid = getpid(); /* * Arka plandaki türün ne olacağı garanti altına alınmadığından, * sadece işaretli bir tam sayı olabileceğinden dolayı, * en kötü senaryo düşünülerek "long long" türüne dönüştürülmüştür. */ printf("%lld\n", (long long)pid); /* * Bazı derleyiciler, "long long" türünden de büyük işaretli tam sayı * tanımlamış olabilirler. İş bu türlerin eş ismi ise C99 ile dile * eklenen "intmax_t" türüdür. Yukarıdaki dönüşüme alternatif olarak * bu türe de dönüşüm yapabiliriz. */ printf("%jd\n", (intmax_t)pid); return 0; } Bir proses, başka bir proses tarafından hayata getirilir. Sistem "boot" edilirken hayata gelen ilk proses "0" numaralı olmasına karşın, "1" numaralı proses hayata gelmeden evvel hayatı bitmektedir. Dolayısıyla bütün proseslerin atası "1" numaralı ve "init" isimli prosestir. Bu "change" olayına da "swapper" ya da "pager" denmektedir. İşletim sistemi proseslere "ID" değeri verirken, en son hayata gelen prosesin "ID" değerinin bir fazlasını vermektedir. Maksimum rakama ulaştığında, bütün "ID" listesini en baştan gezip hayatı bitenlerinkini vermeye başlıyor. Maksimum proses "ID" değeri ise sistemden sisteme değişiklik göstermektedir. /*================================================================================================================================*/ (24_21_01_2023) > UNIX/Linux sistemlerinde Prosesler Hakkında Temel Bilgiler (devam): Anımsanacağı üzere, prosesler başka prosesler tarafından sistem fonksiyonları kullanılarak hayata getirilirler. Bu durumda hayata gelen prosese "child process (alt proses)" denirken, onu hayata getirene ise "parent process(alt proses)" denmektedir. Üst prosesleri aynı olan proseslere ise "sibling process (kardeş proses)" denilmektedir. Fakat kardeşlik bazı sistemlerde, bazı konularda önem arz etmektedir. Bir prosesin üst prosesinin "ID" değerini ise "getppid" isimli POSIX fonksiyonunu kullanarak öğreneiliriz. Bu fonksiyon ise "unistd.h" başlık dosyasında bildirilmiştir. Fonksiyonun prototipi de aşağıdaki gibidir; #include pid_t getppid(void); Bu fonksiyonun da başarısız olması beklenemez çünkü her prosesin bir üst prosesi vardır. Bir proses hayata geldiğinde, onu hayata getiren prosesten bağımsız çalışmaktadır dolayısıyla ömrü, onu hayata getirenden evvel de bitebilir sonrasında da. Evvelce bitmesi durumunda alt prosesimiz artık "orphan process (yetim/öksüz proses)" olacaktır ve işletim sistemi tarafından kendisine yeni bir üst proses atanacaktır. Yeni atanılan bu proses ise "1" numaralı "ID" değerine sahip, "init" isimli, prosestir. * Örnek 1, #include #include int main() { /* # OUTPUT # pid : 439 ppid: 438 */ pid_t pid, ppid; pid = getpid(); printf("pid : %lld\n", (long long)pid); ppid = getppid(); printf("ppid: %lld\n", (long long)ppid); return 0; } Bir sistemde proseslerin sahip olabileceği maksimum "ID" değeri, bir prosesin hayata getirebileceği maksimum proses adedi ve o an çalışabilecek maksimum proseslerin adetleri limitler ile sınırlandırılmıştır. Bu limitler kalıcı olarak değiştirilebildiği gibi anlık olarak da değiştirilmektedir. Bu limitler sistemin kapasitesine, sistemdeki bileşenlerin (örn. RAM, HDD) büyüklüğüne göre de değişmektedir. Yani her sistemde bu limitler aynı değildir. Örneğin, Linux sistemlerinde "/proc/sys/kernel" dizini içerisindeki: >> "threads-max" isimli dosyada, aynı anda var olabilecek toplam proseslerin sayısı belirtilmiştir. Bu sayı o an hayatta olan proses ve "thread" lerin toplamıdır (Linux sisteminde "thread" ler de prosesler olarak ele alınırlar). Unutmamalıyız ki bu dosyada belirtilen rakam, o sistemin özelliklerine göre değişiklik göstermektedir. >> "pid_max" isimli dosyada, bir prosesin alabileceği maksimum "ID" değeri yazmaktadır. İşletim sistemi, bu değerden sonra tekrar baştan başlamak suretiyle, hayatı biten proseslerin "ID" değerini yeni prosesler için kullanacaktır. Fakat bu değer ARTMAMAKTADIR. 32-bit ve 64-bit sistemlerde değişiklik göstermektedir. Öte yandan, belli bir kullanıcının hayata getirebileceği toplam proses ve "thread" değeri, "getrlimit" ya da "ulimit -u" kabuk çağrısı ile elde edilebilir. Fakat unutmamalıyız ki "root" kullanıcılar ya da yeterli yetkiye sahip diğer kullanıcılar, bu tip kısıtlamalara maruz kalmamaktadır. Bu limit değerlerinin bir sonraki "boot" anına kadar değiştirmek için ilgili dosyaları açıp yeni limit değerlerini girerek yapılabilir ya da "sysctl" kabuk komutu kullanılabilir. Fakat kalıcı değişiklik istiyorsak, sistem "boot" edilirken başvurulan bazı konfigürasyon dosyalarını değiştirmemiz lazım. Örneğin, "/etc/sysctl.conf" dosyasına yeni limitler girmek. Artık sistem her açıldığında, bu limitler ile açılacaktır. Ek olarak "kernel parameters" yoluyla da değiştirilebilir. >> "kernel parameters": Nasıl ki bizlerin yazdığı programlar komut satırı argümanları alıyorsa, "kernel" de komut satırından argüman almaktadır. "kernel" de kendini "initialize" ederken bu geçilen parametreleri kullanmaktadır. > UNIX/Linux sistemlerinde proseslerin hayata getirilmesi: Bu tip sistemlerde bir prosesi hayata getirmenin yegane yolu "fork" isimli POSIX fonksiyonunu çağırmaktır. Bu fonksiyon ise ilgili sistemlerdeki sistem fonksiyonlarını çağırmaktadır. Örneğin, Linux sistemlerinde "sys_fork" / "sys_clone" isimli sistem fonksiyonları çağrılmaktadır. "fork" fonksiyonunun prototipi aşağıdaki gibidir; #include pid_t fork(void); "fork" fonksiyonu aslında şöyle bir temayı yapmaktadır; bir klonlama makinesine bir kişi girdiğinde, çıkışta birbiri ile aynı olan iki kişi olacaktır. Klonlama sonrasında bu iki kişinin GEÇMİŞİ AYNI OLACAKTIR. Fakat gelecekte başlarına neler geleceği kesin değildir ve bu iki kişi hayatlarını birbirinden bağımsız bir şekilde sürdüreceklerdir. İşte bizim programımızın akışı bu tema gibidir; Yani, akış "fork" fonksiyonuna giriyor ve çıkıyor. Fakat "fork" içerisinde halihazırda mevcut olan prosesin kontrol bloğunun birebir kopyası çıkartılıyor. Sadece bir iki nokta birbiri ile kopyalanmamaktadır. "fork" fonksiyonundan çıktıktan sonra birbirinden ayrı iki proses olarak hayatlarını devam ettiriyorlar. BU DA DEMEKTİR Kİ BU PROSESLERE AİT OLAN BELLEK ALANLARI DA BİRBİRİNDEN BAĞIMSIZDIR. ÇÜNKÜ PROSESİN KONTROL BLOĞU KOPYALANIRKEN, PROSESLERE AİT BELLEK ALANLARI DA KOPYALANMAKTADIR. "fonk" fonksiyonundan hem yeni hayata gelen hem de kopyalanan proses çıkmaktadır. Alt prosesin ömrü, "fork" fonksiyonundan çıkmadan evvelki son andan itibaren başlamıştır. Bellek alanları birbirinden ayrı olduğu için, birinin yaptığı değişikliği diğeri görmeyecektir. Burada unutmamamız gereken nokta, "fork" fonksiyonundan sonraki kod parçacıkları her iki proses tarafından da çalıştırılacaktır(varsayılan durumda). Fakat bizler "if" blokları ile proseslerin yapacağı işleri ayırabiliriz. Pekiyi bizler bunu nasıl başaracağız? "fonk" fonksiyonunu çağıran prosese üst proses, klonlama işlemi sonrasında hayata gelen prosese de alt proses denmektedir. Fakat üst proses, "fork" fonksiyonundan çıkarken, alt prosesin "ID" değeri ile geri dönmektedir. Alt proses ise "0" "ID" ile geri dönmektedir ama bu demek değildir ki alt prosesin "ID" değeri "0" dır. Sadece bu fonksiyonun geri dönüş değeri sıfır ise alt proses, değil ise üst proses anlamındadır. Alt prosesin "ID" değeri ayrıdır. "fork" fonksiyonu BAŞARISIZ OLABİLİR. Bu durumda "-1" ile geri dönmektedir. * Örnek 1, "fork" fonksiyonunun kullanımı: #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # Hello World! Hello World! */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); printf("Hello World!\n"); /* * Bu fonksiyonu çağırma sebebimiz, iki proses arasında bir gecikme * sağlayarak, çıktının daha okunabilir olmasını sağlamaktır. */ sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Tipik bir "fork" fonksiyonunun kullanımı. Aşağıdaki kalıbı kullanmamız tavsiye edilmektedir: #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # [1456] : Process ID of master process. [1460] : Fork retun value. ------------------------------------------------------- [1456] : Process ID of master process. [1455] : Parent process ID of master process. ------------------------------------------------------- Hello World! [0] : Fork retun value. ++++++++++++++++++++++++++++++++++++++++++++++++++++++ [1460] : Process ID of slave process. [1456] : Parent process ID of slave process. ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Hello World! */ pid_t pid; printf("[%lld] : Process ID of master process.\n", (long long)getpid()); if((pid = fork()) == -1) exit_sys("fork"); printf("[%lld] : Fork retun value.\n", (long long)pid); if(pid != 0) { /* * Üst proses, alt prosesin "ID" değeri ile klonlamadan çıktığı için, * bu bloktaki kodlar üst proses tarafından koşulacaktır. */ puts("-------------------------------------------------------"); printf("[%lld] : Process ID of master process.\n", (long long)getpid()); printf("[%lld] : Parent process ID of master process.\n", (long long)getppid()); puts("-------------------------------------------------------"); } else { /* * Alt proses, sıfır ile klonlamadan çıktığı için, * bu bloktaki kodlar da alt proses tarafından koşulacaktır. */ puts("++++++++++++++++++++++++++++++++++++++++++++++++++++++"); printf("[%lld] : Process ID of slave process.\n", (long long)getpid()); printf("[%lld] : Parent process ID of slave process.\n", (long long)getppid()); puts("++++++++++++++++++++++++++++++++++++++++++++++++++++++"); } printf("Hello World!\n"); /* * Bu fonksiyonu çağırma sebebimiz, iki proses arasında bir gecikme * sağlayarak, çıktının daha okunabilir olmasını sağlamaktır. */ sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include void exit_sys(const char* msg); int g_x = 100; int main() { /* # OUTPUT # g_x = 100 g_x = 100 */ pid_t pid; printf("g_x = %d\n", g_x); if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) { g_x = 1000; } else { /* * Klonlama işlemi sonrasında proseslerin bellek alanları da * kopyalandığı için, "g_x" değişkenininden iki tane vardır. * Dolayısıyla aslında değiştirilen üst prosesin içerisindeki * "g_x" değişkenidir. */ printf("g_x = %d\n", g_x); } sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Burada unutmamamız gereken nokta, "fork" fonksiyonundan sonra aynı kodlardan iki tane olmasıdır. Dolayısıyla iki tane "pid" değerinin olması ve her prosesin kendi "pid" değişkenine yeni değeri yazması söz konusudur. Alt proses hayatına "fork" fonksiyonunun sonunda hayata geldiği için de tekrardan "fork" çağrısı yapılmamakta, sonsuz döngüye girmemektedir. Son olarak, "fork" fonksiyonundan çıkan her iki fonksiyonun hangisinin ilk çıktığının bir garantisi yoktur. Bu, işletim sisteminin çizelgeleyici algoritmalarına bağlıdır. * Örnek 1, #include #include int main() { /* # OUTPUT # Common code... Common code... Common code... Common code... Common code... Common code... Common code... Common code... */ /* * Aşağıdaki "fork" çağrısı sonrasında iki prosesimiz vardır. * process * / \ * process process */ fork(); /* * Yukarıdaki çağrıdan sonra meydana gelen iki prosesin her birisi * aşağıdaki çağrıdan dolayı klonlanacaktır. Dolayısıyla artık dört * tane prosesimiz vardır. * process * / \ * process process * / \ / \ * process process process process */ fork(); /* * Yukarıdaki çağrıdan sonra meydana gelen dört prosesin her birisi * aşağıdaki çağrıdan dolayı klonlanacaktır. Dolayısıyla artık sekiz * tane prosesimiz vardır. * process * / \ * process process * / \ / \ * process process process process * / \ / \ / \ / \ * process process process process process process process process */ fork(); printf("Common code...\n"); sleep(1); return 0; } * Örnek 2, #include #include int main() { /* # OUTPUT # a : 3 a : 3 a : 3 a : 3 a : 3 a : 3 a : 3 a : 3 */ int a = 0; /* * Aşağıdaki klonlama işlemi öncesinde 'a' değişkeninin değeri '1' oldu. * Klonlama sonrasında iki tane 'a' değişkeni oldu ve değerleri '1'. */ ++a; fork(); /* * Aşağıdaki klonlama işlemi öncesinde 'a' değişkeninin değeri '2' oldu. * Klonlama sonrasında dört tane 'a' değişkeni oldu ve değerleri '2'. */ ++a; fork(); /* * Aşağıdaki klonlama işlemi öncesinde 'a' değişkeninin değeri '3' oldu. * Klonlama sonrasında sekiz tane 'a' değişkeni oldu ve değerleri '3'. */ ++a; fork(); printf("a : %d\n", a); sleep(1); return 0; } Bu örneklerde de görüldüğü üzere klonlama işlemi sonrasında her bir proses aynı koda sahip fakat artık birbirleri arasında bir bağ kalmamıştır. Klonlama işlemi sırasında proseslerin kontrol bloklarının da birbirine kopyalandığından bahsetmiştik. Peki iş bu kontrol blokları içerisinde bir gösterici tarafından gösterilen dosya betimleyici tablosunun durumu nasıl olacak? Bildiğiniz üzere iki tür kopyalama türü vardır. Bunlardan birisi "Deep Copy(Derin Kopyalama)", diğeri ise "Shallow Copy(Sığ Kopyalama)". İkisi arasındaki fark; derin kopyalama yaparken gösterici tarafından gösterilen bütün nesnelerin de birer kopyalarının çıkartılmasıyken, sığ kopyalamada sadece göstericinin birer kopyasının çıkartılması ve böylece iki göstericinin aynı nesneyi göstermesidir. "fork" işlemi sırasında da prosesin "Process Control Block" ğu ve bu blok içerisindeki gösterici tarafından gösterilen "File Description Table" ın birebir kopyası çıkartılıyor. Yani bu noktaya kadar derin kopyalama yapılıyor. Artık iki tane "Process Control Block" ve "File Description Table" var. Fakat bu "File Description Table" lar tarafından gösterilen "file" türünden nesnelerin KOPYASI ÇIKARTILMIYOR. Dolayısıyla her iki "File Description Table" içerisinde bulunan "fd" ler, aslında tek bir "file" türünden nesneyi gösteriyor. Yani bu nokta için sığ kopyalama yapılıyor. Günün sonunda alt ve üst prosesler aynı "file" türden nesneyi gösteriyorlar. Dolayısıyla bu "file" içerisindeki referans sayaçları da bir artıyor. Bu da şu manaya geliyor; alt ya da üst prosesten birisi, dosya konumlandırıcısını değiştirirse, diğeri artık yeni konumu görecektir(dosyayı açtıktan sonra "fork" işleminin yapıldığı varsayılmıştır). * Örnek 1, Klonlama sonrasında her iki proses de aynı "file" nesnesini göstermektedir. #include #include #include #include void exit_sys(const char* msg); int main() { /* # q.txt # Ahmet Kandemir Pehlivanli */ /* # OUTPUT # Kandemir */ int fd; if((fd = open("q.txt", O_RDONLY)) == -1) exit_sys("open"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) { lseek(fd, 5, SEEK_SET); } else { char buffer[10 + 1]; ssize_t result; if((result = read(fd, buffer, 10)) == -1) exit_sys("read"); buffer[result] = '\0'; puts(buffer); } close(fd); sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Buradan da diyebiliriz ki o an çalışmakta olan bütün prosesler aslında aynı "file" türünden nesneleri göstermektedir. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # OK OK */ pid_t pid; /* * Standart C dosya fonksiyonları tamponlu çalışmaktadır ve "stdout" ise * UNIX/Linux standartlarına göre satır tamponludur. Yazının sonunda * '\n' karakteri olmadığı için, yazı hala tamponda bekletilmektedir. */ printf("OK"); if((pid = fork()) == -1) exit_sys("fork"); /* * Klonlama işlemi sonrasında tamponlar da kopyalanmaktadır. Dolayısıyla * artık iki tamponumuz var. Aşağıdaki çağrıdan dolayı tampona '\n' * karakteri geleceğinden, tampon boşaltılacaktır. İş bu sebepten * dolayı ekrana iki defa basılmıştır. */ printf("\n"); sleep(1); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> Anımsayacağımız üzere "multi-processing" sistemlerde prosesler zaman paylaşımlı olarak ve birbirinden bağımsız bir şekilde çalışmaktadır. Her proses de kendisine özgü bir adet "Process Control Block" a sahip olduğuna ve bu bloğa da ilgili prosesin "ID" değeri ile erişilmektedir. Ek olarak bu proseslerin kontrol blokları birbirine bağlı listeler ile bağlıdır. O an çalışan prosesin proses kontrol bloğu ise global bir gösterici olan "current" isimli gösterici tarafından gösterilmektedir. Fakat proseslerin "ID" değerinden o prosesin kontrol bloğuna erişmek için de bir "hash-table" kullanılmaktadır. >> O an çalışmakta olan proseslerin listesini çıkartmak için ya "/proc/" dizinine geçip "ls" komutunu çalıştırmalıyız ki bu durumda ekrana çıkanlar rakamlar o an çalışan prosesleri belirtmektedir, ya da "ps -e" kabuk komutunu çalıştırmaktır. /*================================================================================================================================*/ (25_22_01_2023) > UNIX/Linux sistemlerinde proseslerin hayata getirilmesi (devam): Eskiden "thread" kavramı olmadığı için işin bir kısmını "fork" ile hayata getirdiğimiz proseslere yaptırtıyorduk. Fakat "thread" konusu artık hayatımızın bir parçası olduğu için böyle senaryolarda "thread" leri kullanmaktayız. > UNIX/Linux sistemlerinde proseslerin sonlandırılması: Proseslerin sonlandırılması da yine sistem fonksiyonları tarafından yapılmaktadır. Linux dünyasında bu sistem fonksiyonunun adı "sys_exit". Bir POSIX fonksiyonu olan "_exit" ise prosesleri sonlandırmaktadır. Standart C fonksiyonu olan "exit" de prosesleri sonlandırmaktadır. Fakat "exit" ile "_exit" fonksiyonlarını birbiri ile karıştırmamalıyız. Standart C fonksiyonu olan "exit", Unix/Linux sistemlerinde yine "_exit" fonksiyonunu çağırmaktadır ki "_exit" ise "sys_exit" isimli sistem fonksiyonunu çağırmaktadır. Fakat Standart C fonksiyonu, "_exit" çağrısından önce bir takım işlemleri de yerine getirmektedir. Örneğin, iş bu fonksiyon "stdio" tamponlarını boşaltıyor, bütün "stdio" dosyalarını "fclose" ile kapatıyor ki bu fonksiyon da aslında arka planda yine "close" isimli POSIX fonksiyonunu çağırmaktadır. Tabii diğer yandan "_exit" fonksiyonu da prosesi sonlandırmadan evvel işletim sistemi düzeyinde açılmış olan ve "File Description" lar tarafından gösterilen "file" nesnelerini kapatıyor. Nihayetinde "exit" fonksiyonu, "_exit" fonksiyonunu çağırmaktadır. Fakat sizin de fark edeceğiniz üzere Standart C fonksiyonu, C standartlarında yapılan işlemleri de ele almaktadır. POSIX fonksiyonu olan ise yine POSIX standartlarında yapılan işlemleri ele almaktadır. "_exit" fonksiyonunun prototipi aşağıdaki gibidir; #include void _exit(int status); Fonksiyon, parametre olarak prosesin "exit code" almaktadır. POSIX standartlarında da belirtildiği üzere, "_exit" fonksiyonu "stdio" tamponlarını boşaltmamaktadır. Çünkü hatırlarsanız, bu tamponlu mekanizma tamamiyle Standart C kütüphanesinin oluşturduğu bir mekanizma. Fakat "open" ile açtığımız dosyaları bu fonksiyon kapatmaktadır. İş bu fonksiyon, başarılı sonlanmalar için "0" ile ama başarısız sonlanmalar için "non-zero" bir değer ile dönmektedir. Standart C fonksiyonu olan "exit" ise aşağıdaki parametrik yapıya sahiptir; #include void exit(int status); Yukarıda da belirtiğimiz üzere, "exit" fonksiyonu prosesi sonlandırmadan evvel bir takım işlemler yapmaktadır. Bu işlemlere, C kütüphanelerinin hayata gelirken yaptığı işlemlerin tekrar geri verilmesi de eklenebilir. Yani "exit" fonksiyonu, Standart C kütüphanelerinin içi gereklidir. İş bu "exit" fonksiyonu, ayrıca, "atexit" ile kayıt altına alınmış fonksiyonları, kayıt sırasına ters olarak çağırmaktadır. "atexit" fonksiyonu ise aşağıdaki parametrik yapıya sahiptir; #include int atexit(void (*func)(void)); İş bu fonksiyon, başarılı olma durumunda "0" ile aksi halde "non-zero" ile geri dönmektedir. Bu fonksiyonun işlevi, bu fonksiyon ile kayıt altına alınan fonksiyonları tekrardan çağırmak içindir. * Örnek 1, #include #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # bar foo */ atexit(foo); atexit(bar); /* * "exit" fonksiyonu, "atexit" ile kayıt altına alınanları * ters sıraya göre çağıracaktır. Yani ilk olarak "bar", * sonrasında ise "foo" çağrılacaktır. */ exit(EXIT_SUCCESS); return 0; } C standartlarına göre, eğer bir program içerisinde hiç "exit" çağırmaz isek, programın akışı "main" fonksiyonundan çıktıktan sonra "main" fonksiyonunun geri dönüş değeri ile "exit" fonksiyonu çağrılmaktadır. Yani "exit" her halükarda çağrılmaktadır. * Örnek 1, #include #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # bar foo */ atexit(foo); atexit(bar); /* * "main" içerisinde elle "exit" çağrısı yapmadık fakat C standartlarınca * programın akışı "main" fonksiyonundan çıktıktan sonra, "main" in geri * dönüş değeri ile "exit" çağrılıyor. "exit" çağrıldığı için "atexit" * ile kayıt altına alınanlar da yine ters sıra ile çağrılıyor. */ return 31; } Fakat C dilinde proses sonlandırmalar "exit" fonksiyonu ile birlikte "abort" fonksiyonuyla da yapılmaktadır. İş bu fonksiyon "abnormal" bir sonlanmaya yol açmaktadır. Yani "exit" fonksiyonunun aksine "stdio" tamponlarını BOŞALTMAMAKTADIR. "abort" fonksiyonu da aşağıdaki parametrik yapıya sahiptir; #include void abort(void); Bu fonksiyonu, programda ciddi sorunlar meydana geldiğinde çağırabiliriz. Bu fonksiyon, UNIX/Linux sistemlerinde, "SIGABRT" biçimindeki bir sinyal oluşmasına yol açıyor. İş bu sinyal ise prosesin sonlanmasına yol açıyor. * Örnek 1, #include #include #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # */ atexit(foo); atexit(bar); printf("Hello World!"); /* * İş bu C fonksiyonu "abnormal" biçimde sonlandığı * için yine ekrana bir şey basılmamıştır. Ek olarak * kayıt altına aldığımız diğer fonksiyonlar da * çağrılmamıştır. */ abort(); return 0; } Linux sistemleri için prosesleri sonlandırmayı özetlemek gerekirse; exit() ----> _exit() ----> sys_exit Pekiyi iş bu "exit code" kavramı nedir? Açıkçası işletim sistemi, "_exit" içerisine yazdığımız rakam ile hiç ilgilenmemektedir. İşletim sistemi sadece bu rakamı üst prosese iletmek ile ilgileniyor. Yani bizim prosesimizi hayata getiren üst proses, bizim prosimizin sonlanırken kullandığı "exit code" u öğrenmek isterse, bunu kendisine iletiyor. Bu bahsi geçen rakam, üst proses ile alt proses arasındaki ilişkide anlam kazanıyor. O zaman buradan şunu çıkartabiliriz; "exit" fonksiyonu arka planda "_exit" fonksiyonunu çağırmakta ve çağırırken de bizim "exit" e geçtiğimiz değeri kullanmakta. Öte yandan işletim sistemi bu rakamın ne olduğu ile ilgilenmemekte. Dolayısıyla "exit" e geçtiğimiz rakamdan bağımsız bir şekilde, "exit" fonksiyonu yine tamponları boşaltma vs. işlemleri YERİNE GETİRMEKTEDİR. "exit" fonksiyonu çağrıldığında ilk olarak "atexit" ile kayıt altına aldığımız fonksiyonlara ters sıra ile çağrı yapmakta, daha sonra "tmpfile" fonksiyonu ile oluşturulan geçici dosyaları siler, ilgili tamponları boşaltır ve bunları kapatır. * Örnek 1, #include #include #include void foo(void) { printf("foo"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # Hello World!bar foo */ atexit(foo); atexit(bar); printf("Hello World!"); /* * Burada gerçekleşen şey şudur; * i. "Hello World!" yazısı tampona alınır. * ii. Programın akışı "main" den çıktıktan sonra "exit(0)" çağrısı yapılır. * iii. "atexit" ile kayıt altına alınanlar ters sıra ile çağrılır. * iv. Tampona "bar\n" yazısı da alınır. "stdout" dosyası satır tamponlamalı * olduğu için "Hello World!bar\n" yazısı tampon boşaltıldığı için ekrana çıkar. * v. Tampona "foo" yazısı alınır. * vi. "stdout" tamponu boşaltılacağı için "foo" yazısı da ekrana basılır. */ return 0; } * Örnek 2, #include #include #include void foo(void) { fprintf(stderr, "foo"); } void bar(void) { fprintf(stderr, "bar"); } int main() { /* # OUTPUT # barfooHello World! */ atexit(foo); atexit(bar); printf("Hello World!"); /* * Burada gerçekleşen şey şudur; * i. "Hello World!" yazısı "stdout" tamponuna alınır. * ii. Programın akışı "main" den çıktıktan sonra "exit(0)" çağrısı yapılır. * iii. "atexit" ile kayıt altına alınanlar ters sıra ile çağrılır. * iv. "bar" yazısı "stderr" tamponuna alınır. * v. "foo" yazısı da "stderr" tamponuna alınır. * vi. "stderr" dosyası bu sistemlerde sıfır tamponlamalı olduğu için * ekrana "barfoo" yazısı çıkar. * vii. Diğer tamponlar da boşaltılacağı için ekrana "Hello World!" yazısı * çıkar. */ return 0; } > Alt proseslerin sonlanmalarının beklenmesi ve bunların "exit code" larının elde edilmesi: Bu işlem POSIX sistemlerinde iki fonksiyon ile mümkündmür. Bunlar sırasıyla "wait" ve "waitpid" isimli fonksiyonlardır. "waitpid" fonksiyonu, işlevsellik açısından "wait" fonksiyonunu da kapsamaktadır. >> "wait" fonksiyonu: İş bu fonksiyon üst prosesi bloke etmektedir ta ki alt proses sonlanana kadar ve aşağıdaki parametrik yapıya sahiptir; #include pid_t wait(int* status); Parametrelerinden de anlaşılacağı üzere argüman olarak "int" türden bir değişkenin adresini almaktadır. İş bu fonksiyon, çağrıldıktan sonra, ilk sonlanacak alt prosesi beklemektedir. Örneğin, peş peşe "fork" işlemi gerçekleştirelim. İlk sonlanan alt prosesi bu fonksiyon kaale alacaktır. Ek olarak, iş bu fonksiyon, kendisini çağıran "thread" i BLOKE ETMEKTEDİR. Bu bloke işlemi ilk alt proses sonlanana kadar sürmektedir. Fakat bu fonksiyonu çağırdığımız an, halihazırda sonlanmış bir fonksiyon varsa, bu fonksiyon bloke işlemini GERÇEKLEŞTİRMEZ. İş bu fonksiyonun geri dönüş değeri, ömrü biten proses aittir. Yani başarı durumunda "exit code" u alınan prosesin "ID" değeri ile geri döner. Bu fonksiyona argüman olarak "int" türden değişkenin adresinin geçildiğinden bahsetmiştik. İşte alınan "exit code" bu değişkenin içerisine yazılmaktadır fakat POSIX standartlarınca kaçıncı bitlerde "exit code" olduğu, hangi bitlerde hangi bilgilerin tutulduğu garanti altına alınmamıştır. Bunun için makrolar yazılmıştır. Unutmamalıyız ki prosesler "abnormal" biçimde, sinyal mekanizması kullanılarak, sonlanabilir. Bu durumda bu prosesler için "exit code" oluşmayacaktır. Bir prosesin "exit code" u temin edebilmek için, prosesin normal biçimde sonlanmış olması gerekmektedir. İş bu makrolar; >>> "WIFEXITED" : Bir prosesin normal mi "abnormal" mi sonlandığı bilgisini döndürmektedir. Normal sonlanmalarda "non-zero" ile geri dönmektedir. Bu makroya, "wait" fonksiyonuna geçilen "int" türden nesne geçilir ve onun bitlerine bakar. "abnormal" sonlanmalarda ise "0" ile geri dönmektedir. >>> "WIFSIGNALED" : Bir prosesin "abnormal" biçimde, bir sinyal mekanizmasından ötürü, sonlanıp sonlanmadığını sorgulamak için kullanılır. "abnormal" ise "0", aksi halde "non-zero" ile geri döner. >>> "WIFSTOPPED": Bir proses "SIGSTOP" sinyali ile geçici süreyle durdurulmuş olabilir. Bu durumu sorgulamak için kullanılır. >>> "WEXITSTATUS": Bir prosesin "exit code" unu çekmek için kullanılır fakat bunun için prosesin normal biçimde sonlanması gerekmektedir. "wait" fonksiyonuna argüman olarak NULL geçmemiz durumunda, ilk alt proses sonlanana kadar bekleyecektir. Yani "exit code" istemiyorum demek de diyebiliriz. "wait" fonksiyonu çağrıldığında, ilgili üst proses tarafından hayata getirilen alt proses yoksa, ya da fonksiyona geçersiz bir adres fonksiyon BAŞARISIZ OLABİLMEKTEDİR. C standartlarına göre "exit code" için "int" türü denmiştir. Bu fonksiyondaki önemli nokta, prosesin sonlanmasını beklemektir. * Örnek 1, Alt proses beklenmektedir: #include #include #include #include void exit_sys(const char* msg); void child_process(void); int main() { /* # OUTPUT # parent started running... parent resumes running... child is running... 0 child is running... 1 child is running... 2 child is running... 3 child is running... 4 child exited with exit code: 100 parent is about to end... */ printf("parent started running...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* Alt proses sonlanacağı için, "else" bloğuna gerek duymadık. */ if(pid == 0) child_process(); /* Alt proses sonlanacağı için bu aşamada sadece üst proses vardır. */ printf("parent resumes running...\n"); int stat; if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); printf("parent is about to end...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(void) { for(int i = 0; i < 5; ++i) { printf("child is running... %d\n", i); sleep(1); } exit(100); } * Örnek 2, Alt proses beklenmemektedir: #include #include #include #include void exit_sys(const char* msg); void child_process(void); int main() { /* # OUTPUT # parent started running... parent resumes running... child exited with exit code: 0 parent is about to end... child is running... 0 */ printf("parent started running...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* Alt proses sonlanacağı için, "else" bloğuna gerek duymadık. */ if(pid == 0) child_process(); /* Alt proses sonlanacağı için bu aşamada sadece üst proses vardır. */ printf("parent resumes running...\n"); int stat; /* if(wait(&stat) == -1) exit_sys("wait"); */ if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); printf("parent is about to end...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(void) { for(int i = 0; i < 5; ++i) { printf("child is running... %d\n", i); sleep(1); } exit(100); } * Örnek 3, "abnormal" sonlanma durumunda: #include #include #include #include void exit_sys(const char* msg); void child_process(void); int main() { /* # OUTPUT # parent started running... parent resumes running... child is running... 0 child is running... 1 child is running... 2 child is running... 3 child is running... 4 parent is about to end... */ printf("parent started running...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* Alt proses sonlanacağı için, "else" bloğuna gerek duymadık. */ if(pid == 0) child_process(); /* Alt proses sonlanacağı için bu aşamada sadece üst proses vardır. */ printf("parent resumes running...\n"); int stat; if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); printf("parent is about to end...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(void) { for(int i = 0; i < 5; ++i) { printf("child is running... %d\n", i); sleep(1); } abort(); } * Örnek 4, Alt proses hemen sonlanmış ise: #include #include #include #include void exit_sys(const char* msg); void child_process(void); int main() { /* # OUTPUT # parent started running... parent resumes running... parent is running... 0 child is about to end... parent is running... 1 parent is running... 2 parent is running... 3 parent is running... 4 child exited with exit code: 31 parent is about to end... */ printf("parent started running...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* Alt proses sonlanacağı için, "else" bloğuna gerek duymadık. */ if(pid == 0) child_process(); /* Alt proses sonlanacağı için bu aşamada sadece üst proses vardır. */ printf("parent resumes running...\n"); for(int i = 0; i < 5; ++i) { printf("parent is running... %d\n", i); sleep(1); } int stat; if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); printf("parent is about to end...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(void) { printf("child is about to end...\n"); exit(31); } "wait" fonksiyonu, birden fazla sonlanmamış varsa ilk sonlananı ele alacaktır. Eğer birden fazla sonlanmış var ise hangisini ele alacağınının bir garantisi YOKTUR. "exit code" ile hangi proses olduğunu öğrenebiliriz. Ek olarak, ne kadar "fork" yaptı isek o kadar "wait" YAPMALIYIZ. * Örnek 1, #include #include #include #include #define NCHILDS 5 void exit_sys(const char* msg); void child_process(int val); int main() { /* # OUTPUT # parent is waiting for childs to exit... child exited with exit code: 101 child exited with exit code: 100 child exited with exit code: 102 child exited with exit code: 103 child exited with exit code: 104 */ pid_t pids[NCHILDS]; printf("parent is waiting for childs to exit...\n"); for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) child_process(100 + i); } int stat; for(int i = 0; i < NCHILDS; ++i) { if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(int val) { /* "rand" fonksiyonu bütün alt proseslerde aynı değeri üretecektir. */ sleep(rand() % 10 + 1); exit(val); } * Örnek 2, #include #include #include #include #define NCHILDS 5 void exit_sys(const char* msg); void child_process(int val); int main() { /* # OUTPUT # parent is waiting for childs to exit... child exited with exit code: 100 child exited with exit code: 104 child exited with exit code: 101 child exited with exit code: 102 child exited with exit code: 103 */ pid_t pids[NCHILDS]; printf("parent is waiting for childs to exit...\n"); for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) child_process(100 + i); } int stat; for(int i = 0; i < NCHILDS; ++i) { if(wait(&stat) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(int val) { srand(val); sleep(rand() % 10 + 1); exit(val); } >> "waitpid" fonksiyonu, "wait" fonksiyonunun daha gelişmiş versiyonudur. Fonksiyonun parametrik yapısı aşağıdaki gibidir; #include pid_t waitpid(pid_t pid, int *stat_loc, int options); İş bu fonksiyonun birinci parametresi, hangi alt prosesi beklemek istiyorsak, onun proses "ID" değeridir. Bu parametre bir kaç biçimde geçilebilir; >>> Eğer bu parametreye "-1" den de küçük değerlerin geçilmesi durumunda, o değerin mutlak değerini "Group ID" olarak sahip olan prosesi ele alacaktır. Yani, "-13560" gibi bir sayı girmemiz durumunda proses "Group ID" değeri "13560" olan proses ele alınacaktır. >>> Eğer bu parametreye "-1" geçilirse, tamamen "wait" gibi davranır. >>> Eğer bu parametreye "0" geçilirse, "Group ID" değeri, bu fonksiyonu çağıran üst prosesin "Group ID" değerine sahip prosesi ele alır. >>> Eğer pozitif bir sayı geçilirse ki en normal durum budur, proses "ID" değeri bu sayı olan prosesi ele alacaktır. Eğer bu parametreye geçilen "ID" değerleri, o prosesin alt prosesine ait değilse başarısız olacaktır. İkinci parametre, "exit code" un yerleştirileceği "int" türden nesnenin adresidir. Yine bu parametreye de NULL adres geçilebilir. Bu durumda alt proses beklenir fakat "exit code" bilgisi elde edilmez. Üçüncü parametre çok önemli değil, genellikle "0" geçilir ama bazı seçenekleri de "bit-wise OR" ile almaktadır. Bu üçüncü parametre şunlardan birisi olabilir; >>> "WEXITED" : İllaki bitmiş prosesleri ele alacaktır. >>> "WSTOPPED" : Bir sinyal ile durdurulan ("stop" edilen) prosesleri ele alıyoruz. >>> "WNOWAIT" : "Ben bekleme yapmak istemiyorum. Sadece sonlanmış varsa onu almak, aksi halde fonksiyon başarısız olsun" anlamındadır. >>> "WNOHANG" : Bu durumda, eğer alt proses henüz sonlanmamışsa, bekleme yapmaz ve başarısızla sonuçlanır. >>> ... Aşağıda pekiştirici örneklere yer verilmiştir; * Örnek 1, #include #include #include #include #define NCHILDS 5 void exit_sys(const char* msg); void child_process(int val); int main() { /* # OUTPUT # parent is waiting for childs to exit... child exited with exit code: 100 child exited with exit code: 101 child exited with exit code: 102 child exited with exit code: 103 child exited with exit code: 104 */ pid_t pids[NCHILDS]; printf("parent is waiting for childs to exit...\n"); for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) child_process(100 + i); } /* * Aşağıda, alt prosesler farklı zamanlarda sonlanacaktır fakat onları * ele alırken bizim istediğimiz sıraya göre ele alacağız. */ int stat; for(int i = 0; i < NCHILDS; ++i) { if(waitpid(pids[i], &stat, 0) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(int val) { srand(val); sleep(rand() % 10 + 1); exit(val); } * Örnek 2, #include #include #include #include #define NCHILDS 5 void exit_sys(const char* msg); void child_process(int val); int main() { /* # OUTPUT # parent is waiting for childs to exit... child exited with exit code: 100 child exited with exit code: 104 child exited with exit code: 101 child exited with exit code: 102 child exited with exit code: 103 */ pid_t pids[NCHILDS]; printf("parent is waiting for childs to exit...\n"); for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) child_process(100 + i); } /* * Aşağıda, alt prosesler farklı zamanlarda sonlanacaktır. "wait" etkisi * oluşturduğumuz için ilk sonlananı ele alıyoruz. */ int stat; for(int i = 0; i < NCHILDS; ++i) { if(waitpid(-1, &stat, 0) == -1) exit_sys("wait"); if(WIFEXITED(stat)) printf("child exited with exit code: %d\n", WEXITSTATUS(stat)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void child_process(int val) { srand(val); sleep(rand() % 10 + 1); exit(val); } BİR PROGRAMCI, "fork" İŞLEMİ İLE HAYATA GETİRDİĞİ HER ALT PROSESİ "wait" / "waitpid" FONKSİYONLARI İLE BEKLEMESİ İYİ BİR TEKNİKTİR. AKSİ HALDE HORTLAK PROSESLER OLUŞACAKTIR. > Hortlak Prosesler ("zombie processes"): "exit" fonksiyonu çağrısından sonra işletim sistemi ilgili prosese ait olan bellek alanını bellekten tamamyile kaldırmaktadır. Dolayısıyla bu prosesin oluşturduğu bütün kaynakları da yok etmektedir. O prosese ait "exit code" ise prosesin kontrol bloğu içerisine yazmaktadır. Fakat işletim sistemi proses kontrol bloğunu ortadan kaldıramıyor. Çünkü işletim sistemi şöyle düşünüyor; Üst proses, "wait" ya da "waitpid" fonksiyonu ile bu "exit code" u henüz almamış ama her an isteyebilir düşüncesiyle beklemektedir. Dolayısıyla proses kontrol bloğu yok edilmeyen fakat ilgili prosese ait bellek alanları vb. şeyleri yok edilen prosesler "zombie" proses denmektedir. Yani özetle alt prosesin bitmesi ve üst prosesin "wait" yapmaması sonucu alt prosesin "zombie process" olarak anılma durumudur. Proseslerin kontrol blokları "kernel" içerisin büyük yer kaplıyorlar. Dolayısıyla "zombie process" durumu uzun vadede tehlikeli sonuçlara neden olacaktır. Sistemde "leak" oluşacaktır. Pekiyi üst proses alt prosesten daha evvel sonlanırsa ne olacak? Bu durumda alt proses öksüz durumda kalacak ve işletim sistemi tarafından üst proses olarak "1" numaralı "ID" değerine sahip "init" isimli proses atanıyor. Bu durumda "zombie" proses OLUŞMAYACAKTIR. Pekiyi şu senaryoda neler olacak? Alt proses sonlandı. Üst proses de "wait" yapmadı fakat o da sonlandı. Bu durumda işletim sistemi artık prosesin kontrol bloğunu yok edecektir. Bundan sebeple diyebiliriz ki "zombie" proses oluşması için gereken şartlar şunlardır; >> Üst proses yeni bir alt proses hayata getirecek. >> Alt proses bitmesine rağmen, üst proses "wait" fonksiyonları ile iş bu alt prosesi beklemeyecek. Bu tip proseslerin önüne geçmenin bir kaç yolu vardır: >> En doğal yolu, üst prosesin "wait" fonksiyonlarını çağırmasıdır. >> Bir diğer yöntem ise sinyal mekanizmalarından faydalanmaktır. Çünkü "wait" fonksiyonları üst prosesin donmasına sebebiyet vermektedir. Alt proses tamamlandığında üst prosese bir sinyal gönderiyor. Eğer üst proses "wait" sırasında bloke olmak istemiyorsa, alt proses tarafından gönderilen bu sinyali İŞLEMELİDİR. >> Son olarak alt proses hayata geldiğinde işletim sistemine bir söz vermeliyiz: "ben bu alt prosesin "exit code" bilgisini almayacağım. İşletim sistemi de alt proses sonlanır sonlanmaz ona ait alanları yok etmektedir. Öte yandan, PROSESİMİZİN ÖMRÜ ÇOK KISA İSE, "zombie" KAVRAMINA TAKILMASAK DA OLUR. > Hatırlatıcı Notlar: >> İşimizi standart C fonksiyonları ile halledebiliyorsak, o fonksiyonlar ile halletmeliyiz. Eğer halledemiyorsak ve POSIX fonksiyonlarına ihtiyacımız varsa, her iki standartlardaki fonksiyonları birlikte kullanırken dikkatli davranmalıyız. >> Başarılı sonlanmalar "0" ile başarısızlar ise "non-zero" bir değer ile sağlanmalıdır (POSIX/Linux sistemlerinde). >> C'de programlarımızı "exit" ile sonlandırmalıyız çünkü yukarıda açıklanan bazı ön işlemleri yerine getirmektedir. Fakat bazı durumlarda direkt olarak "_exit" ile sonlandırmak da gerekebilir. * Örnek 1, #include #include #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } int main() { /* # OUTPUT # */ atexit(foo); atexit(bar); printf("Hello World!"); /* * İş bu C fonksiyonu "abnormal" biçimde sonlandığı * için yine ekrana bir şey basılmamıştır. Ek olarak * kayıt altına aldığımız diğer fonksiyonlar da * çağrılmamıştır. */ _exit(0); return 0; } >> C standartlarına göre, "main" fonksiyonu içerisinde "return" deyimini yazmazsak, "return 0;" yazmış kabul edilmekteyiz. Bu kural sadece "main" fonksiyonuna özgüdür. >> Komut satırından "echo $?" kabuk komutunu çalıştırdığımız zaman, "shell" programının en son çalıştırdığı prosesin "exit code" bilgisini elde etmiş oluruz. >>> Eğer sadece "$?" yazıp "Enter" tuşuna basarsak, son çalıştırılan prosesin "exit code" bilgisini elle yazıp "Enter" tuşuna basmış gibi oluyor. Fakat o "exit code" değerinde bir program ismi olmadığından, "shell" programı hata verecektir. >>> "echo euhuheueueu" komutu ise ise ekrana "euhuheueueu" basacaktır. >> Paralel programlamada işi gereksiz yere "thread" lere bölersek verim açısından zarar edebiliriz. /*================================================================================================================================*/ (26_28_01_2023) > Hortlak Prosesler ("zombie processes") (devam): "zombie process" oluşmasının şu dezavantajları vardır; >> Üst prosesin ömrü fazla değilse, genellikle üst prosesin "zombie process" oluşturması ciddi bir soruna yol açmaz. Ancak üst proses uzun süreli çalışıyorsa, "zombie process" ler önemli bir sistem kaynağının boşa harcanmasına yol açabilir. >>> "zombie process" lere ait proses "ID" değerleri, o proses "zombie process" olmaktan kurtulana kadar, sistem tarafından kullanılamamaktadır. Dolayısıyla zamanla proses "ID" lerin tükenmesine yol açabilir. Pekiyi bizler "zombie process" oluşmasını engellemek için sadece "wait" fonksiyonlarını mı kullanabiliriz? Anımsanacağınız üzere "wait" fonksiyonları çağrıldığında, üst proses "block" edilecektir. Halbuki bazı uygulamalarda üst proses için bu istenmeyen bir durumdur. Böyle senaryolarda elimizde iki adet çözüm vardır fakat ikisi de sinyal yöntemleri ile ilgilidir; >> Alt proses bittiğinde, "SIGCHLD" sinyalinde üst proses "wait" fonksiyonlarını çağırırmak. >> Alt prosesin "exit code" bilgisini almak istemediğimizi işletim sistemine söylersek, alt proses işi bittiğinde ona dair kaynaklar işletim sistemi tarafından boşaltılacaktır. * Örnek 1, Aşağıdaki örnekte üst proses toplamda 5 saniye çalışmıştır. Bu sürece "wait" fonksiyonlarını da çağırmadığı için kısa süreliğine de olsa alt prosesler "zombie process" durumundadır. Bunu görüntüleyebilmek için başka bir "shell" kabuk programını açmalı, o anki çalışma dizinini iş bu programınki haline getirmeli ve son olarak "ps -u" komutunu çalıştırmalıdır. Ekrana çıkan bilgilerden, "COMMAND" satırında "defunct" ile nitelenen proses "zombie process" tir. O satırın, "STAT" sütununda da "Z+" yazacaktır. Eğer üst proses sonlandıktan sonra aynı kabuk komutunu tekrar çalıştırdığımız zaman ilgili "zombie process" lerin yok olduğunu da göreceğiz. Eğer üst proses senelerce çalışmaya devam etseydi, "zombie process" durumunda olan iş bu alt proses de hayatta kalmaya devam edecektir. Görüldüğü gibi ömrü kısa olan üst prosesler için "zombie process" durumu göz ardı edilebilir. #include #include #include void exit_sys(const char* msg); int main() { /* # OUTPUT # parent process continues to run: 0 child process is about to terminate... parent process continues to run: 1 parent process continues to run: 2 parent process continues to run: 3 parent process continues to run: 4 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) { /* Üst Proses */ for(int i = 0; i < 5; ++i) { printf("parent process continues to run: %d\n", i); sleep(1); } } else { /* Alt Proses */ printf("child process is about to terminate...\n"); exit(EXIT_SUCCESS); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > "exec" işlemleri: Bir grup fonksiyona verilen ortak isimdir. Farklı parametrik yapılar ile özünde aynı işi yapmaktadırlar. İş bu fonksiyonlar "unistd.h" başlık dosyasında bildirilmiştir. Fakat bu fonksiyonların hepsi arka planda "execve" isimli sistem fonksiyonuna uygun parametreler ile çağrı yapmaktadır. "exec" fonksiyonları "Environment Variables of Processes(Proseslerin Çevre Değişkenleri)" konusu ile yakın ilişki içerisindedir. Bu konu, çevre değişkenleri konusundan sonra işlenecektir. > Proeseslerin Çevre Değişkenleri: İşletim sistemlerinin çoğunda var olan bir konudur. Prosese özgü "anahtar-değer" çiftlerini tutan sözlüksel bir veri yapısıdır. "anahtar" verildiğinde "değer" döndürmektedir. "hash-table", "dictionary" vb. veri yapılarının işletim sistemi tarafından organiza edilmiş halidir. Burada "anahtar" kısmına "Çevre Değişkeni", "anahtar" a karşı gelen "değer" için ise "Çevre Değişkeninin Değeri" denmektedir. Fakat unutmamalıyız ki "anahtar-değer" çifti bir yazı biçiminde OLMAK ZORUNDADIR. Örneğin, "anahtar" a karşılık gelen şey "ankara" yazısı olabilir, buna karşılık gelen "değer" ise "06" yazısı olabilir. Proseslerin çevre değişkenleri ve bunlara karşılık gelen değerleri pek çok işletim sisteminde, ilgili prosesin bellek alanı içerisindeki özel bit bölgede tutulmaktadır. Pekiyi bizler ne tür işlemlere haiziz? >> Bir çevre değişkeni, yani "anahtar", verildiğinde ona karşılık gelen "değer" i elde etmek için standart bir C fonksiyonu vardır. O fonksiyona "anahtar" bilgisini geçtiğimiz vakit, bizlere ona karşılık gelen "değer" i döndürmektedir. Prototipi aşağıdaki gibidir: #include char *getenv(const char *name); Fonksiyona geçilen adres '\0' karakteri ile biten bir adres olmalıdır. Ek olarak, bize geri döndürdüğü adrestekileri DEĞİŞTİRMEK TANIMSIZ DAVRANIŞTIR. Sadece "get" etmek için kullanmalıyız. "set" için başka fonksiyonlara ihtiyacımız vardır. Ek olarak bu adresi "free()" fonksiyonuna göndermemeliyiz. Statik ömürlüdür. Eğer "anahtar" a karşılık gelen bir "değer" yok ise iş bu fonksiyon NULL ile geri dönmektedir. Başarısızlık durumunda "errno" herhangi bir değere çekilmez. Proseslerin çevre değişkenleri UNIX/Linux sistemlerinde "Case-Sensitive", fakat Windows sistemlerinde böyle bir duyarlılık yoktur. Ek olarak çevre değişkenleri, yani "anahtar" lar, boşluk karakteri içermemektedir. Proseslerin çevre değişkenleri, ilgili prosesin bellek alanında olduğunu söylemiştik. "fork" işlemi sırasında da proseslerin bellek alanları da çoğaltıldığı için, çevre değişkenleri de çoğaltılmaktadır. Buradan hareketle diyebiliriz ki alt prosesin çevre değişkenleri de üst prosesten gelmektedir. Örneğin, "shell" programı üzerinden başka bir program çalıştırdığımız zaman, "shell" in çevre değişkenleri o programa aktarılmaktadır. "shell" programının çevre değişkenlerini "env" isimli komut üzerinden öğrenebiliriz. Bu komutu çalıştırdığımız zaman görüyoruz ki "anahtar-değer" çiftleri her satırda bir tanesi olacak şekilde ekrana basılmaktadır. Pekiyi "shell" programı çevre değişkenlerini nereden almaktadır? "shell" programı "login" programı tarafından çalıştırıldığı için, "login" programını da "terminal" çalıştırmaktadır. Zincirin halkaları biçiminde bir çağrım var. Bu da demekdir ki her bir program, çevre değişken listesine eklemeler yapmaktadır. Kümülatif bir yaklaşım söz konusudur. "shell" programının çevre değişkenler listesine eklediklerini şu linkten görebiliriz; https://man7.org/linux/man-pages/man1/sh.1p.html "shell" programı üzerinden "cd" komutunu çalıştırdığımız zaman, bu komut ile birlikte geçilen dizin, artık "shell" programının "PWD" isimli "anahtar" ına karşılık gelecektir. Fakat "chdir" isimli POSIX fonksiyonu böyle bir etkiye sahip değildir. "shell" programı üzerinden "anahtar" karşılık gelen "değer" i bulmak için "echo $HOME" şeklinde bir yaklaşım sergilemeliyizi. Bu durumda ekrana "HOME" a karşılık gelen basılacaktır. Windows sistemler için "echo %HOME%" şeklinde yapmalıyız. * Örnek 1, #include #include #include int main(int argc, char* argv[]) { /* # Command Line Arguments: # PATH */ /* # OUTPUT # /opt/swift/swift-5.7.3-RELEASE-ubuntu22.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } char* value; if((value = getenv(argv[1])) == NULL) { fprintf(stderr, "environment variable not found!...\n"); exit(EXIT_FAILURE); } puts(value); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include int main(int argc, char* argv[]) { /* # Command Line Arguments: # PatH */ /* # OUTPUT # environment variable not found!... */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } char* value; if((value = getenv(argv[1])) == NULL) { fprintf(stderr, "environment variable not found!...\n"); exit(EXIT_FAILURE); } puts(value); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include int main(int argc, char* argv[]) { /* # Command Line Arguments: # PWD */ /* # OUTPUT # /home */ chdir("/home"); if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } char* value; if((value = getenv(argv[1])) == NULL) { fprintf(stderr, "environment variable not found!...\n"); exit(EXIT_FAILURE); } puts(value); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> Prosesin "anahtar-değer" çift listesine yeni bir "anahtar-değer" çifti eklemek. >> Prosese ait olan bütün çevre değişken listesini komple elde etmek. > 7. Çalışma Sorusunun Çözümü: Aşağıdaki kod, test aşamasında hata vermektedir. Hocanın çözümü ile karşılaştırınız. #include #include #include #include #include #define SIZE 50 #define NCHILDS 5 #define PART_SIZE (SIZE / NCHILDS) /* * İş bu yapının elemanları, parçalı dosyalardaki en küçük * değere sahip indisi bulmak içindir. */ typedef struct tagFMERGE{ FILE* f; int val; char path[32]; }FMERGE; /* * İş bu dizinin elemanları sıralı bir şekilde dosyalara, * daha sonrada sıralı biçimde tek bir dosyaya yazılacak. */ int g_a[SIZE]; void generate_random_numbers(void); void my_sort(int partno); int my_sort_comp(const void* pv1, const void* pv2); void merge_and_write(void); bool test(void); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0. child process is running... 1. child process is running... 3. child process is running... 2. child process is running... 4. child process is running... OK, parent process resumes... test failed test: fail */ generate_random_numbers(); pid_t pids[NCHILDS]; for(int i = 0; i < NCHILDS; ++i) { if((pids[i] = fork()) == -1) exit_sys("fork"); if(pids[i] == 0) { /* Alt Proses */ my_sort(i); /* * Aşağıdaki "exit" çağrısı "my_sort" * içerisinde de yapılabilir. */ // exit(EXIT_SUCCESS); } /* * Bu noktaya sadece üst prosesin akışı gelmektedir. * Alt proses "exit" yaptığı için, alt prosesin akışı gelmeyecektir. * Alt prosesler kendi işlerini yaparken, üst proses ise yeni alt * prosesler hayata getirmektedir. */ } int status; int fail_flag = 0; for(int i = 0; i < NCHILDS; ++i) { /*Approach - I * Alt prosesler belli bir sıraya göre * beklenmeyecektir ve "exit code" * bilgisi "discard" edilmiştir. */ // if(wait(NULL) == -1) exit_sys("wait"); /*Approach - II * Alt prosesler belli bir sıraya göre * beklenmeyecektir ve "exit code" * bilgisi aşağıda ele alınacaktır. */ if(waitpid(pids[i], &status, 0) == -1) exit_sys("waitpid"); /* * Normal sonlanıp sonlanmadığı teyit ediliyor. */ if(WIFEXITED(status)) { /* * Normal sonlanma olduğu görülüyor. */ if(WEXITSTATUS(status) != 0) { fprintf(stderr, "%d. child terminated with the exit code: %d\n", i, WEXITSTATUS(status)); fail_flag = 1; } } else { /* * Abnormal sonlanma olduğu görülüyor. */ fprintf(stderr, "%d. child terminated \"abnormally\" with the exit code: %d\n", i, WEXITSTATUS(status)); fail_flag = 1; } /* * İlgili alt proseslerden en az bir tanesinin başarısız olması * durumunda süreç sonlanacaktır. */ if(fail_flag) exit(EXIT_FAILURE); } printf("OK, parent process resumes...\n"); /* * Parçalara ayrılan ".dat" dosyaları tek bir dosya haline * getiriliyor. */ merge_and_write(); printf("test: %s\n", test() ? "success" : "fail"); return 0; } void generate_random_numbers(void) { for(int i = 0; i < SIZE; ++i) { g_a[i] = rand() % 1000000; } } void my_sort(int partno) { printf("%d. child process is running...\n", partno); /* * İlgili dizideki elemanları sıralıyoruz. Her bir alt * proses 100'000 lik elemanı sıralayacaktır. */ qsort( g_a + PART_SIZE * partno, PART_SIZE, sizeof(int), my_sort_comp ); FILE* f; char path[32]; /* * Aşağıda her bir dosya için ayrı bir isim oluşturulmuştur. */ sprintf(path, "part-%d.dat", partno); if((f = fopen(path, "wb")) == NULL) { fprintf(stderr, "cannot open file: %s\n", path); exit(EXIT_FAILURE); } /* * İlgili dizimideki elemanları da sırasıyla ilgili dosyalara * yazıyoruz. Toplamda 10 adet ".dat" uzantılı dosya oluşacaktır. */ if(fwrite(g_a + PART_SIZE * partno, sizeof(int), PART_SIZE, f) != PART_SIZE) { fprintf(stderr, "cannot write file: %s\n", path); exit(EXIT_FAILURE); } fclose(f); /* * Eğer "stdio" tamponlarının boşaltılmasını istemiyorsak, * "_exit(EXIT_SUCCESS)" yaparak da çıkabiliriz. */ exit(EXIT_SUCCESS); } int my_sort_comp(const void* pv1, const void* pv2) { /* * pv1 > pv2 : Pozitif * pv1 < pv2 : Negatif * pv1 == pv2 : 0 */ /* * C dilinde "void" türünden göstericileri * başka türden göstericilere atarken * "C-style" dönüşüm yapmak zorunda değiliz. * Fakat bu durum sadece "void" türüne özel * C++ dilinde yine dönüşüm yapmak zorundayız. */ const int* ci1 = (const int*)pv1; const int* ci2 = (const int*)pv2; /* * Eğer bizim değişkenlerimiz "long" türden olsalardı, * aşağıdaki gibi bir işlem yanlış sonuçlar doğurtabilirdi. * Sonuçta fonksiyonun geri dönüş türü "int" ve "long" türden * işlem yaparken kırpılma olabilir. Dolayısıyla aşağıdaki * gibi bir yaklaşımı sadece türler "int" ise YAPINIZ. */ return *ci1 - *ci2; } void merge_and_write(void) { FMERGE fmerges[NCHILDS]; for(int i = 0; i < NCHILDS; ++i) { sprintf(fmerges[i].path, "part-%d.dat", i); if((fmerges[i].f = fopen(fmerges[i].path, "rb")) == NULL) { fprintf(stderr, "cannot open file: %s\n", fmerges[i].path); exit(EXIT_FAILURE); } if(fread(&fmerges[i].val, sizeof(int), 1, fmerges[i].f) != 1) { fprintf(stderr, "cannot write file: %s\n", fmerges[i].path); exit(EXIT_FAILURE); } } FILE* dest; if((dest = fopen("sort.dat", "wb")) == NULL) { fprintf(stderr, "cannot open file: %s\n", "sort.dat"); exit(EXIT_FAILURE); } int n = NCHILDS; int min, min_index; int i; while(n > 0) { min = fmerges[0].val; min_index = 0; /* * Her bir dosyanın içindeki rakamların * "min" değişkeniyle olan ilişkisi sınanıyor. */ for(i = 0; i < n; ++i) { if(fmerges[i].val < min) { min = fmerges[i].val; min_index = i; } } if(fwrite(&min, sizeof(int), 1, dest) != 1) { fprintf(stderr, "cannot write file: %s\n", "sort.dat"); exit(EXIT_FAILURE); } /* * En küçük değere sahip dosyaya ait olan dosya göstericisi * bir ilerletilmelidir. */ if(fread(&fmerges[min_index].val, sizeof(int), 1, fmerges[min_index].f) != 1) { /* * İlgili dosya göstericisi "EOF" konumuna gelip gelmediği * sınanmalıdır. Artık ileriki döngülerde iş bu dosyaya * bakılmayacaktır. */ if(feof(fmerges[min_index].f)) { fclose(fmerges[min_index].f); if(unlink(fmerges[min_index].path) == -1) exit_sys("unlink"); fmerges[min_index] = fmerges[n - 1]; --n; } else { fprintf(stderr, "cannot read file: %s\n", fmerges[min_index].path); exit(EXIT_FAILURE); } } } } bool test(void) { FILE* dest; if((dest = fopen("sort.dat", "rb")) == NULL) { fprintf(stderr, "test failed \n", "sort.dat"); exit(EXIT_FAILURE); } int prev_val; if(fread(&prev_val, sizeof(int), 1, dest) != 1) { fprintf(stderr, "test failed \n", "sort.dat"); return false; } int next_val; while(fread(&next_val, sizeof(int), 1, dest) == 1) { if(next_val < prev_val) return false; prev_val = next_val; } /* Son operasyonun bir "IO" hatası olup olmadığı sınanmıştır. */ if(ferror(dest)) { fprintf(stderr, "test failed(fatal) \n", "sort.dat"); return false; } fclose(dest); return true; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (27_29_01_2023) > Proeseslerin Çevre Değişkenleri (devam): >> Prosesin "anahtar-değer" çift listesine yeni bir "anahtar-değer" çifti eklemek için POSIX fonksiyonu olan "setenv" ve "putenv" isimli fonksiyonları kullanacağız. C standartlarında bir prosesin çevre değişkenlerine ekleme yapan bir fonksiyon bulunmamaktadır. >>> "setenv" fonksiyonu, aşağıdaki prototipe sahiptir: #include int setenv(const char *key, const char *value, int overwrite); Fonksiyonun birinci parametresi "anahtar" a karşılık gelirken, ikinci parametresi ise "değer" e karşılık gelmektedir. Üçüncü parametre ise "anahtar" ın halihazırda bulunması durumunda, ona karşılık gelen "değer" in değişsin mi değişmesin mi bilgisidir. Üçüncü parametre "non-zero" bir değer geçilirse, "değer" bilgisi değiştirilecektir. İş bu fonksiyon başarısızlık durumunda "-1", başarı durumunda "0" ile geri döner. Başarısızlık durumunda yine "errno" değiştirilir. * Örnek 1, Aşağıdaki fonksiyonda "anahtar=değer" biçiminde girilen girişler, '=' karakterinden itibaren ayırmıştır. #include #include #include #include void set_env_var(int argc, char* argv[]); void get_env_var(int argc, char* argv[]); int main(int argc, char* argv[]) { /* # Command Line Arguments: # ali=100 veli=200 selami=300 */ /* # OUTPUT # environment variable not found: ali=100 environment variable not found: veli=200 environment variable not found: selami=300 ali ---> 100 veli ---> 200 selami ---> 300 */ if(argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } get_env_var(argc, argv); set_env_var(argc, argv); get_env_var(argc, argv); return 0; } void set_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { /* * "argv[i]" yazısı içerisinde '=' karakteri arıyoruz. * Döngünün ilk turunda "ali=100" ele alındığı varsayılırsa, * "str" değişkeni '=' karakterini gösterecektir. */ if((str = strchr(argv[i], '=')) == NULL) { fprintf(stderr, "invalid argument: %s\n", argv[i]); continue; } /* * Eğer bu karakter bulunmuşsa, programın akışı buraya * gelecektir. '=' karakterini '\0' karakteri ile * değiştiriyoruz. Artık "ali=100" şeklindeki girişimiz * "ali\0100" halini aldı. Girişler bir yazı biçiminde * olduğu için aslında giriş "ali\0100\0" biçimindedir. */ *str = '\0'; /* * "str" değişkeni artık ilk '\0' karakterini, "str + 1" ise * '\0' karakterinin bir yanındaki karakteri gösteriyor. Yani * "1" karakterini. Dolayısıyla o nokta, başlangıç noktası * olarak ele alındı. Bitiş noktası olarak zaten yazının * sonundaki '\0' ele alınmaktadır. */ if(setenv(argv[i], str + 1, 1) == -1) { perror("setenv"); } } } void get_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { if((str = getenv(argv[i])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[i]); continue; } printf("%s ---> %s\n", argv[i], str); } } * Örnek 2, #include #include #include #include void set_env_var(int argc, char* argv[]); void get_env_var(int argc, char* argv[]); int main(int argc, char* argv[]) { /* # Command Line Arguments: # aliveli selami=300 */ /* # OUTPUT # environment variable not found: aliveli environment variable not found: selami=300 invalid argument: aliveli environment variable not found: aliveli selami ---> 300 */ if(argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } get_env_var(argc, argv); set_env_var(argc, argv); get_env_var(argc, argv); return 0; } void set_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { /* * "argv[i]" yazısı içerisinde '=' karakteri arıyoruz. */ if((str = strchr(argv[i], '=')) == NULL) { fprintf(stderr, "invalid argument: %s\n", argv[i]); continue; } /* * Eğer bu karakter bulunmuşsa, programın akışı buraya * gelecektir. '=' karakterini '\0' karakteri ile * değiştiriyoruz. */ *str = '\0'; /* * "str" değişkeni artık '\0' karakterini, "str + 1" ise * '\0' karakterinin bir yanındaki karakteri gösteriyor. * Dolayısıyla o nokta, başlangıç noktası olarak ele alındı. */ if(setenv(argv[i], str + 1, 1) == -1) { perror("setenv"); } } } void get_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { if((str = getenv(argv[i])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[i]); continue; } printf("%s ---> %s\n", argv[i], str); } } * Örnek 3, #include #include #include #include void set_env_var(int argc, char* argv[]); void get_env_var(int argc, char* argv[]); int main(int argc, char* argv[]) { /* # Command Line Arguments: # ali=100 ali=200 */ /* # OUTPUT # environment variable not found: ali=100 environment variable not found: ali=200 ali ---> 200 ali ---> 200 */ if(argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } get_env_var(argc, argv); set_env_var(argc, argv); get_env_var(argc, argv); return 0; } void set_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { /* * "argv[i]" yazısı içerisinde '=' karakteri arıyoruz. */ if((str = strchr(argv[i], '=')) == NULL) { fprintf(stderr, "invalid argument: %s\n", argv[i]); continue; } /* * Eğer bu karakter bulunmuşsa, programın akışı buraya * gelecektir. '=' karakterini '\0' karakteri ile * değiştiriyoruz. */ *str = '\0'; /* * "str" değişkeni artık '\0' karakterini, "str + 1" ise * '\0' karakterinin bir yanındaki karakteri gösteriyor. * Dolayısıyla o nokta, başlangıç noktası olarak ele alındı. */ if(setenv(argv[i], str + 1, 1) == -1) { perror("setenv"); } } } void get_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { if((str = getenv(argv[i])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[i]); continue; } printf("%s ---> %s\n", argv[i], str); } } * Örnek 4, #include #include #include #include void set_env_var(int argc, char* argv[]); void get_env_var(int argc, char* argv[]); void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # Command Line Arguments: # ali=100 ali=200 */ /* # OUTPUT # environment variable not found: ali=100 environment variable not found: ali=200 ali ---> 100 ali ---> 100 */ if(argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } get_env_var(argc, argv); set_env_var(argc, argv); get_env_var(argc, argv); return 0; } void set_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { /* * "argv[i]" yazısı içerisinde '=' karakteri arıyoruz. */ if((str = strchr(argv[i], '=')) == NULL) { fprintf(stderr, "invalid argument: %s\n", argv[i]); continue; } /* * Eğer bu karakter bulunmuşsa, programın akışı buraya * gelecektir. '=' karakterini '\0' karakteri ile * değiştiriyoruz. */ *str = '\0'; /* * "str" değişkeni artık '\0' karakterini, "str + 1" ise * '\0' karakterinin bir yanındaki karakteri gösteriyor. * Dolayısıyla o nokta, başlangıç noktası olarak ele alındı. */ if(setenv(argv[i], str + 1, 0) == -1) { perror("setenv"); } } } void get_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { if((str = getenv(argv[i])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[i]); continue; } printf("%s ---> %s\n", argv[i], str); } } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>> "putenv" fonksiyonu, yine "setenv" fonksiyonu gibi çevre değişkenlerine ekleme yapmaktadır. Fonksiyonun prototipi aşağıdaki gibidir; #include int putenv(char *string); Fonksiyonumuz görüldüğü üzere tek bir parametre almaktadır. Eşleştirme kısmını kendi bünyesinde halletmektedir. Aslında "bizim yukarıda elle yaptığımız şeyi yapmaktadır", diyebiliriz. Argüman olarak aldığı yazının içerisinde "=" olmalı ve "anahtar" var ise mutlaka buna karşılık gelen "değer" değiştirilmektedir. İş bu fonksiyona geçilen adres bilgisi, doğrudan prosesin çevre bilgisi olarak kullanılmakta. Dolayısıyla bu adres bilgisi programcı tarafından değiştirilmemesi gerekmektedir. Yani bu fonksiyona geçilen yazı direkt olarak çevre değişkeni olarak atanmaktadır. Programın ilerleyen safhalarında bu yazının bir biçimde DEĞİŞTİRİLMEMESİ GEREKİYOR. * Örnek 1, #include #include #include #include void set_env_var(int argc, char* argv[]); void get_env_var(int argc, char* argv[]); void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # Command Line Arguments: # ali=100 ali=200 */ /* # OUTPUT # environment variable not found: ali=100 environment variable not found: ali=200 environment variable not found: ali=100 environment variable not found: ali=200 */ if(argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } get_env_var(argc, argv); set_env_var(argc, argv); get_env_var(argc, argv); return 0; } void set_env_var(int argc, char* argv[]) { for(int i = 1; i < argc; ++i) if(putenv(argv[i]) == -1) perror("putenv"); /* * "putenv" fonksiyonunun ve çevre değişkenleri hakkında * detaylı bilgi ilerleyen konularda işleneceği için * şu an için bu kısım boş bırakılmıştır. */ } void get_env_var(int argc, char* argv[]) { char* str; for(int i = 1; i < argc; ++i) { if((str = getenv(argv[i])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[i]); continue; } printf("%s ---> %s\n", argv[i], str); } } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde prosesin çevre değişkenleri, prosesin bellekteki adres alanı içerisindeki bir dizide tutulmaktadır. Bu dizi öyle bir dizi ki her bir indisindeki gösterici "anahtar=değer\0" biçimindeki yazıları göstermektedir. Bu dizinin en sonki elemanı da NULL değere sahip bir göstericidir. İş bu dizi ise "environ" isimli göstericiyi gösteren gösterici şeklinde olup, türü "char**" şeklindedir. Fakat bu değişkenin "extern" bildirimi yapılmadığı için, kullanmadan önce, bizler bu bildirimi yapmalıyız. Aşağıda temsili bir şeması çizilmiştir; environ(char**) ---> _pointer1_(char*) ---> ali=100\0 _pointer2_(char*) ---> vali=200\0 ... ---> ... NULL Aslında "putenv" fonksiyonu, argüman olarak aldığı yazıyı direkt olarak bu diziye enjekte etmektedir. Windows sistemlerindeki organizasyon tam olarak böyle değildir. * Örnek 1, "putenv" fonksiyonunun tipik kullanımı: #include #include #include #include void release_memory(char** env_vars, int argc, char* argv[]); char** create_memory(int argc, char* argv[]); void set_env_var(int argc, char* argv[]); void get_env_var(int argc, char* argv[]); int main(int argc, char* argv[]) { /* # Command Line Arguments: # ali=100 vali=200 */ /* # OUTPUT # environment variable not found: ali environment variable not found: vali Success! Success! ali ---> 100 vali ---> 200 */ if(argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } /* * Girilen komut satırları başka bir diziye kopyalandı. */ char** env_vars = create_memory(argc, argv); /* * Komut satırı argümanlarıyla ilk sorgulama yapıldı. */ get_env_var(argc, argv); /* * Daha sonra prosesin çevre değişkenleri listesine yeni * ekleme yapıldı. */ set_env_var(argc, env_vars); /* * Komut satırı argümanlarıyla son sorgulama yapıldı. */ get_env_var(argc, argv); /* * Kopyalanan dizi tekrar geri verildi. */ release_memory(env_vars, argc, argv); return 0; } void release_memory(char** env_vars, int argc, char* argv[]) { for(int i = 0; i < argc; ++i) { free(env_vars[i]); } free(env_vars); } char** create_memory(int argc, char* argv[]) { char** env_vars; env_vars = (char**)malloc(argc * sizeof(char*)); if(env_vars) { for(int i = 0; i < argc; ++i) { env_vars[i] = (char*)malloc(32 * sizeof(char)); if(env_vars[i]) { strcpy(env_vars[i], argv[i]); } } } return env_vars; } void set_env_var(int argc, char* argv[]) { /* * "argv" dizisinin her bir elemanı direkt olarak yeni * bir çevre değişkeni olarak eklenmektedir. */ for(int i = 1; i < argc; ++i) if(putenv(argv[i]) == 0) printf("Success!\n"); } void get_env_var(int argc, char* argv[]) { char* str; /* * Fakat sorgulama yapmak için dizi içerisindeki '=' * karakteri, '\0' karakteriyle değiştirilmelidir. */ for(int i = 1; i < argc; ++i) { if((str = strchr(argv[i], '=')) == NULL) { continue; } *str = '\0'; } /* * Artık dizinin ilk harfi ile birinci '\0' arasındakiler "anahtar" * olarak aranılacaktır. */ for(int i = 1; i < argc; ++i) { if((str = getenv(argv[i])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[i]); continue; } printf("%s ---> %s\n", argv[i], str); } } * Örnek 2, "environ" isimli değişkenin kullanımı: #include #include /* Aşağıdaki bildirimi bizlerin yapması zorunludur. */ extern char** environ; int main() { /* # OUTPUT # HOSTNAME=Check LANGUAGE=en_US:en PWD=/home HOME=/home/runner5 LANG=en_US.UTF-8 GOROOT=/usr/local/go TERM=xterm DISPLAY=:1 SHLVL=1 PS1=#ogdb"shell"# LC_ALL=en_US.UTF-8 PATH=/opt/swift/swift-5.0-RELEASE-ubuntu14.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DEBIAN_FRONTEND=noninteractive _=/script/tinit */ for(int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } * Örnek 3, Temsili bir "getenv" implementasyonu: #include #include #include #include extern char** environ; void exit_sys(const char* msg); char* my_getenv(const char* name); int main() { /* # OUTPUT # /opt/swift/swift-5.0-RELEASE-ubuntu14.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin */ char* val; if((val = my_getenv("PATH")) == NULL) { fprintf(stderr, "environment variable not found!...\n"); exit(EXIT_FAILURE); } puts(val); return 0; } char* my_getenv(const char* name) { char* str; /* * İlgili dizinin her bir indisindeki yazı incelenir: */ for(int i = 0; environ[i] != NULL; ++i) { /* * İlgili yazının içerisinde '=' karakteri mevcutsa, * "str" değişkeni bu karakteri gösteriyordur. */ if((str = strchr(environ[i], '=')) != NULL) /* * "environ[i]" adresi dizinin başlangıcı, "str" * ise '=' karakterini gösteriyorsa, bu iki göstericinin * farkı bize "anahtar" ın eleman sayısını verecektir. * "name" değişkeni ile dizinin başından itibaren * bu eleman sayısı kadarki kısmı karşılaştırırsak ve * eşit ise ilgili "anahtar" ı bulmuş olacağız. "str" den * bir sonraki eleman ise bize "değer" bilgisini verecektir. */ if(!strncmp(name, environ[i], str - environ[i])) return str + 1; } return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Proseslere ilişkin çevre değişkenleri, prosese özgü bellek alanı içerisinde tahsis edilmiştir. Bir "fork" işlemi sırasında üst prosesin bellek alanı da kopyalandığı için, alt proses ile üst proses aynı çevre değişkenlerine sahip olacaklardır. Fakat "fork" işleminden sonra her iki prosesin çevre değişkenleri birbirinden bağımsız hareket edeceklerdir. Birisinde yapılan değişiklik diğer prosesinkini etkilemeyecektir. Pekiyi bizler belli bir çevre değişken ya da değişkenlerinin bizim programımıza geçmesini istiyorsak ne yapmalıyız? Bizim programımızı çalıştıran program her neyse, onun çevre değişkenlerini değiştirerek. Örneğin, "shell" programı üzerinden "export FRUIT=Banana" komutunu çalıştırdığımız zaman, "FRUIT=Banana" ikilisi artık "shell" programının bir çevre değişkeni olacaktır. Bunun bir diğer alternatifi de "FRUIT=Banana" ve "export FRUIT" komutlarını sırasıyla çağırmak olacaktır. Fakat unutmamalıyız ki bu durum geçici sürelidir. Başka bir "shell" programı çalıştırdığımız vakit, iş bu çevre değişkeni orada GÖZÜKMEYECEKTİR. Pekiyi bizler kalıcı bir şekilde bu soruna nasıl çözüm bulabiliriz? "shell" programı çalışmaya başlamadan evvel bir takım dosyalardan bilgi okuması yapmaktadır. Bu dosyalara genel hatlarıyla "shell" programının "start-up" dosyaları denmektedir. Diğer taraftan kabuk programının çevre değişkenlerini hiç değiştirmeden, doğrudan çalıştırılan programınkilerini değiştirebiliriz de. Bunun için "FRUIT=Banana ./our_program" şeklinde programımızı çalıştırmalıyız. Diğer taraftan bir "shell" programı üç farklı biçimde çalıştırılabilir, dolayısıyla iş bu "start-up" dosyaları da birbirinden farklıdır. Bir "shell" programı aşağıdaki biçimlerde çalıştırılabilir: >>> "Interactive Login 'shell'" biçiminde. Buradaki interaktiflik, komut satırına düşmeyi kastetmektedir. Yani bir "shell" programı üzerinden bir program çalıştırdıktan sonra, programın bitmesiyle beraber, imlecin tekrardan "shell" programında belirmesidir. Linux işletim sisteminde "CTRL + ALT F1" tuş kombinasyonlarına bastığımız vakit karşımıza çıkan siyah ekran, iş bu "shell" programıdır. Bu tür çalıştırmada dosya okuma sırası aşağıdaki gibidir; -> İlk olarak "/etc/profile" dosyası okunacaktır, eğer varsa. Olmaması durumunda bir sorun oluşmayacaktır. -> Daha sonra sırasıyla "~/.bash_profile", "~/.bash_login" ve "~/.profile" dosyaları ele alınır. Fakat sadece ilk bulduğu dosyadan okuma yapacaktır. Dolayısıyla bizler çevre değişkenlerimizi ilk olarak "~/.bash_profile" içerisine yazmalıyız. Buradaki "~" işareti "/home" dizinidir. Yine unutmamalıyız ki bu üç dosyaya sadece bu tip bir "shell" programı bakmaktadır. >>> "Interactive Non-Login "shell"" biçiminde. Buradaki "Login" durumu, "shell" programını çalıştırdığımız zaman bizden kullanıcı adı ve şifre sorması demektir. Bilgileri doğru girdikten sonra "shell" komutunu kullanmaya başlayabiliriz. Başlat çubuğuna "terminal" yazınca karşımıza gelen "shell" budur. Daha doğrusu en sık kullandığımız "shell" komutudur. Bu tür çalıştırmada dosya okuma sırası aşağıdaki gibidir; -> İlk olarak "~/.bashrc" dosyası okunacaktır, eğer varsa. >>> "Non-Interactive Login "shell"" biçiminde. "Non-Interactive" olması, tek bir komutu çalıştırıp çıkacak biçimde olmasıdır. Örneğin, "/bin/bash -c ls -l" komutunu normal bir "shell" programı üzerinden çalıştırığımız zaman, "/bin/bash" içerisindeki "shell" programı tek bir seferlik çalışacak, "ls -l" programını çalıştıracak ve daha sonra sonlanacaktır. Bu etkiyi oluşturan şey ise "-c" seçeneğinin geçilmesidir. Bu tür çalıştırmada dosya okuma sırası aşağıdaki gibidir; -> "BASH_ENV" isimli bir çevre değişkenini araştırılır ve buna karşılık gelen "script" dosyasını çalıştırır. Bütün bunlara ek olarak "Interactive Login "shell"" ile "Interactive Non-Login "shell"" tip "shell" programlarının aynı dosyaya bakmasını olanak vermek için "~/.bash_profile" dosyasına aşağıdaki komutu yazmalıyız. if [ -f ~/.bashrc ]; then . ~/.bashrc; fi Böylelikle "~/.bashrc" dosyasına yazacağımız komutlar ki "export FRUIT=Banana" bir örnek olarak gösterilebilir, her iki tip "shell" programı tarafından da kalıcı olarak bilinir hale gelecektir. Bir üç versiyon hakkındaki detaylara, aşağıdaki dökümandan ulaşabiliriz; https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Bash-Startup-Files > Çevre Değişkenlerinin Silinmesi: Bir çevre değişkeni "unsetenv" isimli POSIX fonksiyonu ile listeden silinebilmektedir. Fonksiyonun prototipi şöyledir; #include int unsetenv(const char *name); Parametre olarak çevre değişkeninin ismini almakta. Başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri döner ve "errno" uygun şekilde değiştirilir. * Örnek 1, #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments: # PWD */ /* # OUTPUT # /home environment variable not found: PWD */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } char* value; if((value = getenv(argv[1])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[1]); exit(EXIT_FAILURE); } puts(value); if(unsetenv(argv[1]) == -1) exit_sys("unsetenv"); if((value = getenv(argv[1])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[1]); exit(EXIT_FAILURE); } puts(value); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Çevre Değişkenlerine Duyulan Gereksinim: Bir takım aşağı seviyeli işlemlerin parametrik hale getirilmesi için kullanılmaktadır. Yani bu aşağı seviyeli bazı işlemlerin basit biçimde dışarıdan değiştirilmesine olanak sağlamaktır. Bazı çevre değişkenleri, bazı POSIX fonksiyonları tarafından da kullanılmaktadır. Örneğin "exec" fonksiyonlarının "p" li biçimleri, iş bu çevre değişkenlerine başvurmaktadır. Ek olarak, dinamik bir kütüphane yüklerken "LD_LIBRARY_PATH" isimli çevre değişkenine de başvurulmaktadır. Zaman zaman da uygulama programcıları bu çevre değişkenlerini kullanmaktadır. Örneğin, "gcc" derleyicisi "<>" ile eklenen başlık dosyalarının isimlerini "C_INCLUDE_PATH" isimli çevre değişkeninde de aramaktadır. Aşağıda örnek bir komut verilmiştir; "export C_INCLUDE_PATH=/home/kaan:/home/kaan/Study" Artık "gcc" derleyicisi hem "/home/kaan" dizinine hem de "/home/kaan/Study" dizinine ilgili başlık dosyaları için bakacaktır. > "exec" işlemleri (devam): Bir program dosyasını çalıştırmaktadırlar. Benzer isimde bir grup fonksiyondan oluşmaktadır. Parametrik olarak aralarında farklılıklar vardır. "exec" fonksiyonlarının prototipleri şunlardır; #include extern char **environ; int execl (const char *path, const char *arg0, ... /*, (char *)0 */); int execle (const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/); int execlp (const char *file, const char *arg0, ... /*, (char *)0 */); int execv (const char *path, char *const argv[]); int execve (const char *path, char *const argv[], char *const envp[]); // Sistem Fonksiyonu int execvp (const char *file, char *const argv[]); int fexecve(int fd, char *const argv[], char *const envp[]); Bu fonksiyonlara ek olarak, GNU-C kütüphanesinde ve Linux sistemlerine özgü, aşağıdaki "exec" fonksiyonu daha vardır ki prototipleri aşağıdaki gibidir; #include extern char **environ; int execvpe(const char *file, char *const argv[], char *const envp[]); int execveat(int dirfd, const char *pathname, const char *const argv[], const char *const envp[], int flags); Aslında UNIX/Linux sistemleri iş bu "exec" fonksiyonlarının hepsini sistem fonksiyonu olarak bulundurmamaktadır. Örneğin, Linux sistemlerinde "execve" isimli fonksiyon bir sistem fonksiyonu olacak şekilde yazılmıştır. Diğerleri ise iş bu sistem fonksiyonu çağıracak şekilde tasarlanmışlardır. Yine Linux'a özgü olarak "execveat" isimli fonksiyon da bir sistem fonksiyonu olacak şekilde yazılmıştır. "exec" fonksiyonları, proseslerin yaşamlarına başka bir kodla devam etmelerini sağlamaktadır. Çünkü bir "exec" çağrısı sonucunda prosesin kontrol bloğunda ciddi bir değişiklik yapılıyor fakat tamamiyle değiştirilmişyor. Ek olarak o prosesin bellekteki alanı boşaltılıyor ve bu fonksiyona argüman olarak geçilen programın kodları iş bu bellek alanına yükleniyor. > Hatırlatıcı Notlar: >> "getenv" fonksiyonu ile çevre değişkenlerini DEĞİŞTİRMEYE ÇALIŞMAMALIYIZ. Çevre değişkenlerini değiştirmek için önce "getenv" ile bilgileri temin ediyoruz. Sonrasında bir tampon üzerinde bu değişkenleri değiştiriyoruz. Son olarak "setenv" fonksiyonu ile bu tampondakileri yeni çevre değişkenleri olarak değiştiriyoruz. Eğer parametre olarak geçilen yazının içerisinde "=" olmaması durumunda, "değer" bilgisi boş bir yazı olacaktır. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri dönmektedir ve "errno" değiştirilmektedir. /*================================================================================================================================*/ (28_04_02_2023) > "exec" işlemleri (devam): POSIX standartlarında bulunan "exec" fonksiyonlarından, >> Bünyesinde 'l' karakteri içerenler komut satırı argümanlarını bir liste biçiminde almaktadır. >> Bünyesinde 'v' karakteri içerenler komut satırı argümanlarını bir "vector" biçiminde almaktadır. >> Bünyesinde 'p' karakteri bulunanlar, argüman olan isimleri, prosesin "PATH" çevre değişkeninde de aranacağını belirtmektedir. >> Bünyesinde 'e' karakteri barındıranlar ise argüman olan isimleri prosesin çevre değişkenlerinde aranacağını belirtmektedir. Linux sistemlerinde bulunan aşağıdaki "exec" fonksiyonları, arka planda "execve" isimli sistem fonksiyonunu çağırmaktadır: -> "execl" -> "execv" -> "execlp" -> "execvp" -> "execle" Benzer şekilde aşağıdaki fonksiyon ise "execveat" isimli sistem fonksiyonunu çağırmaktadır: -> "fexecve" Tüm "exec" fonksiyonları başarı durumunda geri dönmezler. Kendilerinin işlevi prosesi başka bir prosese çevirmek. Dolayısıyla günün sonunda yine tek bir proses var. Ancak ve ancak başarısız durumunda "-1" değerine geri dönerler ve "errno" değişkeni uygun bir şekilde değiştirilir. Dolayısıyla "exec" fonksiyonu genelde alt prosesler tarafından çağrılmaktadır ki bizim üst prosesimiz varlığını devam ettirsin. İş bu sebepten ötürüdür ki "fork-exec" ekseriyetle birlikte kullanılır. "exec" fonksiyonlarının incelenmesi: >> "execl" fonksiyonu, en çok kullanılan versiyondur. Prototipi aşağıdaki gibidir; #include int execl(const char *path, const char *arg0, ... /*, (char *)0 */); Fonksiyonun birinci parametresi çalıştırılacak olan "executable" dosyanın yol ifadesidir. Göreli olabileceği gibi mutlak bir yol ifadesi de olabilir. Tabii göreli olması durumunda, bu fonksiyonu çağıran prosesin "Current Working Directory" konumundan itibaren arama yapılacaktır. Fonksiyonun ikinci parametresi ise çalıştırılacak olan ilgili dosyanın alacağı komut satırı argümanlarıdır. İş bu dosyayı "shell" programından çağırırken yazdıklarımızı bir liste haline getirip bu parametreye geçiyoruz. Fonksiyon, başarı durumunda geri dönmeyecektir fakat başarısız durumda "-1" ile geri dönecektir. Fonksiyonun üçüncü parametresine istediğimiz kadar argüman geçebiliriz. Fakat unutmamalıyız ki en sonki parametre mutlaka "(char*)0" biçiminde olmalıdır, eğer üçüncü parametreye argüman geçmişsek. Çünkü geçilen argümanlar "int" türünden küçük ise "integer promotion", "float" türünden ise "double" türüne dönüştürülür. İş bu sebepten dolayı ve NULL sembolik sabitinin arka planda neye karşılık geldiği bilinmediğinden ki bazı sistemlerde "0" a ama bazı sistemlerde "void *" a denk gelmektedir, NULL sembolik sabitini "char*" türüne bizzat dönüştürmeliyiz. Son olarak bu fonksiyonun birinci parametresi ile ikinci parametresinin aynı olması bir zorunluluk değil fakat iyi bir alışkanlıktır. * Örnek 1, Aşağıdaki çıktıyı elde edebilmek için "mample" isimli çalıştırılabilir bir dosyanın olması gerekmektedir: /* sample.c */ #include #include #include void exit_sys(const char* msg); int main(void) { /* * Prosesin "Current Working Directory" konumunda olan "mample" isimli program * çalıştırılacaktır. Birinci, ikinci, üçüncü ve dördüncü parametreler sanki * aynı programı "shell" programıyla çalıştırmışız gibi "mample" programına * geçilecektir. */ if(execl("mample", "mample", "ali", "veli", "selami", (char*)NULL) == -1) exit_sys("execl"); /* * Prosesin akışı bu noktaya hiç bir zaman gelmeyecektir. Çünkü başarılı * olması durumunda artık "mample" programı çalışacaktır ve onun kodları * koşacaktır. Başarısızlık durumunda "exit_sys" fonksiyonu çağrılacaktır. */ return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # ./a.out ali veli selamli */ /* * "sample.c" programındaki "execl" fonksiyonuna geçilen, * Birinci parametre çalıştırılabilir dosya olan "mample" dosyasının ismi. * İkinci parametre, "shell" programından direkt olarak "mample" programını * çalıştırdığımız zaman geçtiğimiz argümanlardan ilki, yani "argv[0]". * Üçüncü parametre, "shell" programından direkt olarak "mample" programını * çalıştırdığımız zaman geçtiğimiz argümanlardan ikincisi, yani "argv[1]". * Dördüncü parametre, "shell" programından direkt olarak "mample" programını * çalıştırdığımız zaman geçti ğimiz argümanlardan ikincisi, yani "argv[2]". * Beşinci parametre, "shell" programından direkt olarak "mample" programını * çalıştırdığımız zaman geçtiğimiz argümanlardan ikincisi, yani "argv[3]". */ for(int i = 0; i < argc; ++i) puts(argv[i]); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 runner23 runner23 16080 Feb 9 19:37 a.out -rwxrwxrwx 1 root root 655 Feb 9 19:37 main.c */ if(execl("/bin/ls", "/bin/ls", "-l", (char*)NULL) == -1) exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14085 14085 16152 Feb 9 20:09 a.out -rwxrwxrwx 1 root root 399 Feb 9 20:09 main.c */ /* * Nasılsa programın akışı sadece ve sadece "execl" başarısız * olduğunda aşağı satıra geçecekse, aşağı satıra ilgili fonksiyonun * yazılması makül bir davranıştır. */ execl("/bin/ls", "/bin/ls", "-l", (char*)NULL); exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # execl: No such file or directory */ execl("/bin/lsS", "/bin/lsS", "-l", (char*)NULL); exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Çıktılardan da görüldüğü üzere bizim esas programımız hayatına artık başka kodlar ile devam etmektedir. Pekiyi bizler "shell" programının yaptığı gibi yapmak istiyorsak, yani hem esas programımız hayatına kendi kodları ile devam etsin hem de başka program da çalışsın, o zaman önce "fork", sonrasında da alt proses için "exec" fonksiyonlarından birisini çağırmamız gerekmektedir. Bu yüzdendir ki "exec" fonksiyonları "fork" fonksiyonları ile birlikte kullanılmaktadır. Fakat "login" programı aslında "exec" programını tek başına kullanmaktadır. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14077 14077 16240 Feb 9 21:01 a.out -rwxrwxrwx 1 root root 1381 Feb 9 21:01 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid == 0) { /* * Alt proses bu bloktaki kodları koşturacaktır. "exec" * fonksiyonunu çağırdığımız için artık hayatına başka * kodlar ile devam edecektir. */ execl("/bin/ls", "/bin/ls", "-l", (char*)0); /* * Fonksiyon sadece başarısız olduğunda geri dönecektir. * Bu durumda da bu fonksiyon çağrılacağı için, alt prosesin * akışı bu bloktan dışarı çıkmayacaktır. * Fakat bu çağrı sonucunda standart C fonksiyonu olan * "exit" çağrılacağı için giriş-çıkış tamponları boşaltılacaktır. * Bunun önüne geçmek için "_exit(EXIT_FAILURE)" çağrısı yapmalıyız. */ exit_sys("execl"); } /* * Bu noktaya sadece üst prosesin akışı gelecektir. */ printf("parent process is running."); /* * Üst prosesimiz, alt prosesi beklemesi gerekmektedir. Çünkü "execl" ile * çalıştırılan prosesin üst prosesi, aslında "fork" fonksiyonunu çağıran * prosesin kendisidir. * Sadece ona dair çıkış kodunu almak istemedik. */ if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid); // wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14056 14056 16240 Feb 9 21:07 a.out -rwxrwxrwx 1 root root 569 Feb 9 21:07 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* myvec[] = { "/bin/ls", "-l", NULL }; /* * "execv" fonksiyonunun ikinci parametresinin adresi bir dizinin başlangıç adresi. * Bu dizinin elemanları birer "const-pointer" ve gösterdikleri yazılar ise "char[]" türden. * Öte yandan argüman olarak geçilen dizinin elemanları ise "pointer", gösterdikleri yazılar * ise yine "char[]" türden. Bu da demektir ki ilgili fonksiyonun gövdesinde, bizim dizimizin * elemanları BAŞKA YAZILARI GÖSTEREMEYECEKTİR. */ if(!pid && execv("/bin/ls", myvec) == -1) exit_sys("execl"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bu örneklerde "fork" işleminden hemen sonra "exec" işlemi yapılmaktadır fakat bazı zamanlarda "fork" işlemlerinden sonra alt proses için bir takım işlemler yapmamız, bu işlemlerden sonra "exec" çağırmamız gerekebilir. Artık "fork" ve "exec" fonksiyonlarının birlikte kullanım biçimi o anki duruma göre değişiklik gösterecektir. Pekiyi bu iki fonksiyonu birlikte çağırmamız ne kadar verimli? Yukarıda da açıklandığı üzere "fork" işleminden sonra prosesin bellek alanı da kopyalanmaktadır. Bu bellek alanı boşaltılıp, üzerinde "exec" ile yeni programın bellek alanı kopyalanmaktadır. Eski sistemlerde bu durum verimsizdi ve bunun için "vfork" isimli POSIX fonksiyonu kullanılmaktadyı. Bu fonksiyon "fork" işlemi sırasında prosesin bellek alanını düşük seviyede kopyalamaktadır. Fakat bu fonksiyondan hemen sonra "exec" yapmazsak, "Tanımsız Davranış" meydana gelecektir. Lakin işlemcilerin gelişmesi ve yeni teknolojilerin kullanılmasından dolayı artık "vfork" fonksiyonuna gerek kalmamıştır. Çünkü bu kopyalama maliyeti bir takım işlemler ile örtbas edilmetedir. Örneğin, işlemcilerin "sayfalama mekanizmaları (paging mechanizm)" ve ya "Copy On Write" yaklaşımı. Bu iki tekniğe de ilerleyen konularda değinilecektir. "vfork" fonksiyonu 2008 POSIX standartları ile kaldırılmıştır. >> "execv" fonksiyonu, "execl" ye nazaran çalıştırılacak programın komut satırı argümanları için bir gösterici dizisinin adresini istemektedir. Bunun haricinde işlevsel olarak "execl" ile aynıdır. Fonksiyonun imzası şu şekildedir; #include int execv(const char *path, char *const argv[]); Fonksiyonun birinci parametresi çalıştırılacak programın yol ifadesini almaktadır. İkinci argüman ise yine gösterici dizisinin başlangıç adresi. Bu dizinin her bir elemanı "const pointer-to-char" biçimindedir. Yani bu dizinin elemanları başka bir "char[]" türden yazıyı gösteremezler. Fakat bu dizinin gösterdiği yazıda değişiklik yapabiliriz. Bu fonksiyon bir yazılardan oluşan dizinin adresini argüman olarak aldığı için, son argümandan sonra NULL sabitini "cast" etmemize lüzum yoktur. Sadece diziyi oluştururken son elemanı NULL yapacağız. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/ls -l */ /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14067 14067 16240 Feb 10 00:00 a.out -rwxrwxrwx 1 root root 1083 Feb 10 00:00 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * "execv" isimli fonksiyonun birinci parametresi çalıştırılacak olan * dosyanın ismi. Öte yandan "argv" dizisinin sıfır indisinde ise "main" * programı yer alırken, birinci indisten itibaren bizlerin komut satırına * yazacakları yer alacak. Birinci indis ise çalıştırılacak programın ismi. */ if(!pid && execv(argv[1], argv + 1) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14056 14056 16240 Feb 9 21:07 a.out -rwxrwxrwx 1 root root 569 Feb 9 21:07 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* myvec[] = { "/bin/ls", "-l", NULL }; if(!pid && execv(myvec[0], &myvec[0]) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/cp main.c my_copy_main.c */ /* # OUTPUT # */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execv(argv[1], argv + 1) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* my_copy_main.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/cp main.c my_copy_main.c */ /* # OUTPUT # */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execv(argv[1], argv + 1) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "execlp" fonksiyonundaki "p" harfi "PATH" lafını işaret etmektedir. Bu fonksiyonun imzası, "p" siz versiyonu ile aynıdır fakat birinci parametre bazında iki fonksiyon arasında bir semantik fark vardır. Fonksiyonun imzası şu şekildedir; #include int execlp(const char *file, const char *arg0, ... /*, (char *)0 */); Fonksiyonun birinci parametresinde belirtilen argüman çalıştırılacak programın ismidir. Fakat bu isim "PATH" isimli çevre değişkeni içerisinde aranmakta, ilk bulduğuna da "exec" işlemi uygulamaktadır. "PATH" çevre değişkeni, ':' karakteri ile ayrılmış yol ifadelerinden oluşan bir yazıdır. Burada prosesin "Current Working Directory" konumuna BAKILMAMAKTADIR. Windows sistemlerinde ise "PATH" çevre değişkeni '%' karakteri ile birbirinden ayrılmıştır. Öte yandan dikkat etmemiz gereken bir diğer nokta ise şudur; birinci parametresine geçilen argümanın içerisinde '/' karakteri var ise bu fonksiyon "execl" GİBİ DAVRANACAKTIR. Yani "PATH" değişkenine bakılmaz. Buradan hareketle diyebiliriz ki "p" li versiyon "p" siz veriyonu kapsamaktadır. Fonksiyon, ilgili programın ismini "PATH" değişkeninde bulamaz ise başarısız olacaktır. "PATH" çevre değişkeni aşağıdaki formatta bulunması gerekmektedir; /opt/swift/swift-5.7.3-RELEASE-ubuntu22.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin Tekrar hatırlatmak gerekirse iş bu "p" li versiyon, prosesin "Current Working Directory" konumuna hiç bir şekilde bakmamaktadır.Tabii geçilen argümanda hiç bir '/' karakterinin olmadığı varsayılmıştır. Ek olarak, "PATH" çevre değişkeninde prosesin o anki çalışma dizini '.' karakteri ile de belirtilebilir. Böylesi bir senaryoda artık prosesin o anki çalışma dizinine de bakacaktır. Örneğin, aşağıdaki gibi bir "PATH" çevre değişkeni; /opt/swift/swift-5.7.3-RELEASE-ubuntu22.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/. Artık böylesi bir "PATH" çevre değişkeni olduğu için, birinci parametreye geçilen isim fonksiyonun çalışma dizininde de aranacaktır. Tabii son olarak söylemekte fayda vardır ki "PATH" çevre değişkeni dizin girişlerinden oluşmuştur. Çalıştırılmak istenen dosyanın ismi iş bu dizin girişlerinde sırasıyla aranmakta ve ilk bulduğu dosyayı çalıştırmaya çalışacaktır. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14068 14068 16248 Feb 10 00:56 a.out -rwxrwxrwx 1 root root 516 Feb 10 00:56 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execlp("ls", "ls", "-l", (char*)0) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14068 14068 16248 Feb 10 00:56 a.out -rwxrwxrwx 1 root root 516 Feb 10 00:56 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Artık fonksiyonumuz "execl" gibi davranacaktır. */ if(!pid && execlp("/bin/ls", "/bin/ls", "-l", (char*)0) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, /* sample.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # main is running. execv: No such file or directory parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Fonksiyonun birinci parametresinde '/' karakteri kullanıldığı için "PATH" değişkenine * bakılmayacak. Fakat '.' karakteri prosesin o anki çalışma dizinini belirttiği için * "mample" isimli program bulunmaktadır. Buradan hareketle diyebiliriz ki "shell" * programının kendisi de "execlp" çağırmaktadır. */ if(!pid && execlp("./mample", "./mample", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # ali veli selami */ /* # OUTPUT # ./a.out ali veli selamli */ for(int i = 0; i < argc; ++i) puts(argv[i]); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "execvp" fonksiyonu, "execlp" fonksiyonu ile aynı karakteristik özelliklere sahiptir. Sadece argüman olarak bir "vector" almaktadır. "execl" ve "execv" arasındaki fark neyse, "execlp" ile "execvp" arasındaki fark da odur diyebiliriz. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # ls -l */ /* # OUTPUT # main is running. total 20 -rwxr-xr-x 1 14097 14097 16280 Feb 10 01:19 a.out -rwxrwxrwx 1 root root 605 Feb 10 01:19 main.c parent process is running. */ printf("main is running.\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execvp(argv[1], &argv[1]) == -1) exit_sys("execv"); printf("parent process is running."); wait(NULL); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Our Bash Program (version 5) #include #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 #define BUFFER_SIZE 4096 typedef struct tagCMD{ char* name; void (*proc)(void); } CMD; void parse_cmd_line(void); void cd_proc(void); void exit_sys(const char* msg); CMD g_cmds[] = { {"cd", cd_proc}, {NULL, NULL} }; char g_cmdline[MAX_CMD_LINE]; char* g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[PATH_MAX]; int main(void) { char* str; int i; pid_t pid; if(!getcwd(g_cwd, PATH_MAX)) exit_sys("getcwd"); for (;;) { fprintf(stdout, "CSD: %s> ", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmd_line(); if(g_nparams == 0) continue; if(!strcmp(g_params[0], "exit")) exit(EXIT_SUCCESS); for(i = 0; g_cmds[i].name != NULL; ++i) if(!strcmp(g_params[0], g_cmds[i].name)) { g_cmds[i].proc(); break; } if(g_cmds[i].name == NULL) { if((pid = fork()) == -1) { perror("fork"); continue; } if(!pid && execvp(g_params[0], &g_params[0]) == -1) { fprintf(stderr, "Command not found, or cannot execute!\n"); continue; } if(wait(NULL) == -1) exit_sys("wait"); } } return 0; } void parse_cmd_line(void) { char* str; g_nparams = 0; for(str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) { g_params[g_nparams++] = str; } g_params[g_nparams] = NULL; } void cd_proc(void) { char *dir; if (g_nparams > 2) { printf("too many arguments!\n"); return; } if (g_nparams == 1) { if ((dir = getenv("HOME")) == NULL) exit_sys("fatal error (getenv)"); } else dir = g_params[1]; if (chdir(dir) == -1) { printf("%s\n", strerror(errno)); return; } if (getcwd(g_cwd, PATH_MAX) == NULL) exit_sys("fatal error (getcwd)"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> Değişken sayıda argüman alan fonksiyonlar: Bu fonksiyonlar bir şekilde kaç adet argüman almaları gerektiğini bilmeleri gerekiyor. * Örnek 1, #include int main(int argc, char** argv) { /* # OUTPUT # warning: format ‘%d’ expects a matching ‘int’ argument [-Wformat=] printf("%d - %d - %d\n", 17, 9); warning: too many arguments for format [-Wformat-extra-args] printf("%d - %d\n", 17, 9, 1993); 17 - 9 - 1336753584 17 - 9 - 1993 17 - 9 */ /* * "printf" fonksiyonu kaç adet geçildiğini hesaplamak için * birinci parametresinde yazılan "%d" formatlayıcılarının * adedine bakıyor. Eğer bunların adedi geçilen argümanın adedinden * fazla ise, fazla olanlar için rastgele değerler çekiyor. */ printf("%d - %d - %d\n", 17, 9); /* * Eğer bunlar aynı ise herhangi bir sorun yok. */ printf("%d - %d - %d\n", 17, 9, 1993); /* * Eğer bunların adedi eksikse, fazla geçilen argüman * ıskartaya çıkartılacaktır. */ printf("%d - %d\n", 17, 9, 1993); return 0; } >> NULL sembolik sabitinin neye karşılık geldiği standartlarca kesin değildir ve aşağıdakilerden birisi olabilir; #define NULL 0 #define NULL (void *) >> C23 standartlarında "NULLptr" dile eklenecektir. Artık C dilinde NULL yerine "NULLptr" kullanmamız gerekebilir. >> C dilinde fonksiyonların imzalarında parantezlerinin içinin boş bırakılması ama tanımlarken parantezlerinin içine "void" yazılması, ilgili fonksiyonu istediğimiz kadar bir argüman ile çağırabiliriz demektir. Öte yandan imzalarında parantezlerin içine "void" yazmamız fakat tanımlarkenki parantezlerinin içinin boş bırakılması, ilgili fonksiyonun argüman almayacağını belirtir. C++ dilinde "void" yazmak ile boş bırakmak aynı şeydir. * Örnek 1, #include void HelloWorld(void); int main(void) { /* # OUTPUT # error: too many arguments to function ‘HelloWorld’ HelloWorld(123); */ /* * İmzasında "void" yazdığı için artık argüman geçemeyiz. */ HelloWorld(123); return 0; } void HelloWorld() { printf("HelloWorld"); } * Örnek 2, #include void HelloWorld(); int main(void) { /* # OUTPUT # HelloWorld */ /* * İmzasında bir şey belirtilmediği için istenildiği kadar * argüman geçilebilir. */ HelloWorld(123, 321, "Ahmet", 'a'); return 0; } void HelloWorld(void) { printf("HelloWorld"); } >> "errno" değişkeninin aldığı değerlerin açıklaması: https://man7.org/linux/man-pages/man3/errno.3.html >> C dilindeki göstericiler için "const" niteleyicisi şu biçimlerde kullanılabilir: ' ' // is a * // pointer to char* x; // x is a pointer to char. const char* y; // y is a pointer to const char. const char*const z; // z is a const pointer to const char. char** n; // n is a pointer to pointer to char. const char** p; // p is a pointer to pointer to const char. const char* const* q; // q is a pointer to const-pointer to const char. const char* const* const i; // i is a const-pointer to const-pointer to const char. * Örnek 1, int main(void) { /* * Aşağıdaki atama dilin kurallarına göre legaldir. */ char* names[] = { "ali", "veli", "selami" }; char** ppnames; ppnames = names; return 0; } * Örnek 2, int main(void) { /* # OUTPUT # warning: assignment to ‘const char **’ from incompatible pointer type ‘char **’ [-Wincompatible-pointer-types] ppnames = names; */ /* * Aşağıdaki atama dilin kurallarına göre LEGAL DEĞİLDİR. Çünkü "names" * dizisinin her bir elemanı "char *" türden. Dizinin birinci elemanının adresi * ise "char**" türden. Öte yandan "ppnames" ise "const char**" türden. Burada "const" * özelliğinin düşmesi mevzu bahis değildir. */ char* names[] = { "ali", "veli", "selami" }; const char** ppnames; ppnames = names; return 0; } * Örnek 3, int main(void) { /* # OUTPUT # warning: assignment to ‘const char **’ from incompatible pointer type ‘char **’ [-Wincompatible-pointer-types] ppnames = names; */ /* * Aşağıdaki atama dilin kurallarına göre legaldir. */ const char* names[] = { "ali", "veli", "selami" }; const char** ppnames; ppnames = names; return 0; } * Örnek 4, int main(void) { /* # OUTPUT # warning: assignment to ‘const char **’ from incompatible pointer type ‘char **’ [-Wincompatible-pointer-types] ppnames = names; */ /* * Aşağıdaki atama dilin kurallarına göre legaldir. * "names" dizisinin her bir elamanı "const char*" türden. Birinci elemanın adresi ise "const char**" türden. * "ppanesm" ise öyle bir "non-const" göstericidir ki */ const char* names[] = { "ali", "veli", "selami" }; const char* const* ppnames; ppnames = names; return 0; } >> Komut satırından girilen argümanların en sonuncusu daima NULL türden olmalıdır. Yani "main" fonksiyonunun ikinci parametresi olan "argv" dizisinin son elemanı her daim NULL olmalıdır. >> UNIX/Linux sistemlerinde çevre değişkenleri büyük harf/küçük harf duyarlılığına sahipken, Windows sistemlerinde böyle bir duyarlılık yoktur. >> "PATH" çevre değişkeninin elde edilmesi: #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # /opt/swift/swift-5.7.3-RELEASE-ubuntu22.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin */ char* path; path = getenv("PATH"); puts(path); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "PATH" çevre değişkenini "shell" programı vasıtasıyla değiştirmek için şu işlemler uygulanmalıdır: -> Önce "echo $PATH" ile ilgili çevre değişkeni ekrana basılır. -> Daha sonra "PATH=$PATH:." çağrısı yapılmalıdır. Artık "PATH" değişeninin sonuna ":." karakterleri eklenmiştir. "shell" programını yeniden çalıştırana kadar veya yeni bir "shell" programı çalıştırana kadar "exec" fonksiyonlarının "p" li versiyonları, prosesin o anki çalışma dizinine de ilgili isim için bakacaklardır. Kalıcı bir değişiklik için "shell" programının "start-up" dosyalarında oynama yapmamız gerekmektedir. Fakat proseslerin çalışma dizinlerinin "PATH" çevre değişkenine eklenmesi, güvenlik zaafiyeti oluşturabileceği için, iyi bir teknik değildir. /*================================================================================================================================*/ (29_05_02_2023) > "exec" işlemleri (devam): >> "exec" fonksiyonlarının "p" li versiyonları, çalıştırılmak istenen programı, "PATH" çevre değişkeninde belirtilen dizinlerden birisinde bulmuş olsun. Eğer bu programı çalıştıramaz ve "errno" değişkeninin değerleri de "EINVAL" YA DA "ENOEXEC" değerlerinden birisini alırsa, iş bu "exec" fonksiyonları bahsi geçen programı "shell" programına ait bir dosya olduğu kanısına varır ve bu programı "/bin" konumundaki "sh" isimli varsayılan "shell" programının kendisi ile çalıştırmaya çalışır. Ancak "exec" fonksiyonlarının diğer versiyonlar, için böyle bir aksiyon almamaktadır. Bahsi geçen bu senaryo ilerleyen paragraflarda tekrar açıklanacaktır. Son olarak belirtmekte fayda vardır ki bu tip versiyonlar eğer "PATH" değişkenini bulamadıkları zaman takınacakları tavır sistemden sisteme değişiklik gösterecektir. Öte yandan pek çok sistem, böylesi bir durumda, "PATH" çevre değişkeni "/bin:/usr/bin" biçimindeymiş gibi davranmaktadır. >> "execle" fonksiyonundaki "e" harfi o prosesin çevre değişkenlerini kastetmektedir. Anımsanacağı üzere bir prosesin çevre değişkenleri, o prosesin bellek alanında tutulmaktadır ve "fork" işlemi sırasında üst prosesten kopyalanmaktadır. Pekiyi "exec" fonksiyonları prosesin bellek alanını tamamiyle ortadan kaldırdığına göre, bir prosesin çevre değişkeninin akıbeti ne olacaktır? İşte "exec" fonksiyonlarının "e" siz versiyonları, "exec" sırasında üst prosesin çevre değişkenlerini bir yerde saklayıp, işlem bittikten sonra ilgili değerleri yeni proses için tekrardan kullanmaktadır. Bu durumda üst prosesten aktarılmış oluyor. Pekiyi "e" li versiyonlar nasıl bir davranış sergiliyorlar? Bu tip versiyonlar da çevre değişkenlerini argüman olarak almaktadırlar. Fonksiyonun prototipi aşağıdaki gibidir; #include int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/); Fonksiyonun prototipi "execl" ile benzerdir. Sadece iş bu fonksiyon, son parametre olarak çevre değişkenlerini argüman olarak almaktadır. Son parametreye geçeceğimiz dizinin elemanları "anahtar=değer\0" biçiminde olmalıdır. Yine en sondan bir önceki argüman için "(char*)NULL" geçmeyi unutmamalıyız, eğer değişken sayıda argüman geçersek. Yine çevre değişkenlerini tuttuğumuz dizinin son elemanı NULL olmak zorundadır. >> "execve" fonksiyonu aslında bir sistem fonksiyonudur. Dışarıdan argüman olarak çevre değişkeni almaktadır fakat iş bu çevre değişkenlerini tuttuğumuz dizinin son elemanı NULL olmak zorundadır. Fonksiyonun prototipi şu şekildedir; #include int execve(const char *path, char *const argv[], char *const envp[]); Yine bu fonksiyona geçilecek en sonki argüman, çevre değişkenlerini içeren dizinin başlangıç adresidir. İş bu fonksiyon aslında taban fonksiyon olarak işlev görmekte olup "execl", "execlp", "execv", "execvp" ve "execle" fonksiyonları tarafından arka planda çağrılmaktadır. * Örnek 1, Aşağıdaki örnekte "e" siz versiyon kullanılmıştır: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # second ali veli selamli */ /* # OUTPUT # Main program started running!... Second program started running!... Command Line Arguments: ./a.out ali veli selamli Environment Variables: HOSTNAME=Check LANGUAGE=en_US:en PWD=/home HOME=/home/runner11 LANG=en_US.UTF-8 GOROOT=/usr/local/go TERM=xterm DISPLAY=:1 SHLVL=1 PS1=#ogdb"shell"# LC_ALL=en_US.UTF-8 PATH=/opt/swift/swift-5.7.3-RELEASE-ubuntu22.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DEBIAN_FRONTEND=noninteractive _=/script/tinit */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && execv(argv[1], &argv[1]) == -1) exit_sys("execve"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* second.c */ #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { printf("Second program started running!...\n"); printf("\nCommand Line Arguments:\n"); for(int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvironment Variables:\n"); for(int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "e" li versiyon kullanılmıştır. Fakat aşağıdaki "main" isimli programı ilgili komut satırı argümanları ile çalıştırmanız gerekiyor: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # second ali veli selamli */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* env[] = { "name=ahmet", "surname=pehlivanli", "address=istanbul", "PATH=/bin:/usr/bin", NULL }; if(!pid && execve(argv[1], &argv[1], env) == -1) exit_sys("execve"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* second.c */ #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { printf("Second program started running!...\n"); printf("\nCommand Line Arguments:\n"); for(int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvironment Variables:\n"); for(int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki program, https://www.onlinegdb.com/ sitesindeki "/bin" dizininde bulunan "sh" isimli kabuk programını çalıştırmaktadır. #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* env[] = { "name=ahmet", "surname=pehlivanli", "address=istanbul", "PATH=/bin:/usr/bin", NULL }; if(!pid && execve(argv[1], &argv[1], env) == -1) exit_sys("execve"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Aşağıdaki program, https://www.onlinegdb.com/ sitesindeki "/bin" dizininde bulunan "sh" isimli kabuk programını çalıştırmaktadır. #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); char* env[] = { "name=ahmet", "surname=pehlivanli", "address=istanbul", "PATH=/bin:/usr/bin", NULL }; if(!pid && execle(argv[1], "/bin/sh", (char*)0, env) == -1) exit_sys("execve"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5, Aşağıda "execv" fonksiyonunun temsili bir implementasyonu gösterilmiştir. #include #include #include #include extern char** environ; int my_execv(const char* path, char* const* argv); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && my_execv(argv[1], &argv[1]) == -1) exit_sys("my_execv"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } int my_execv(const char* path, char* const* argv) { return execve(path, argv, environ); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 6, Aşağıda "execl" fonksiyonunun temsili bir implementasyonu gösterilmiştir. #include #include #include #include #include extern char** environ; #define MAX_ARG 4096 int my_execl(const char* path, const char* arg0, ...); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && my_execl("/bin/sh", "/bin/sh", (char*)0) == -1) exit_sys("my_execl"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } int my_execl(const char* path, const char* arg0, ...) { va_list vl; va_start(vl, arg0); char* args[MAX_ARG + 1]; char* arg; args[0] = (char*)arg0; int i; for(i = 0; (arg = va_arg(vl, char*)) != NULL && i < MAX_ARG; ++i) args[i] = arg; args[i] = NULL; va_end(vl); return execve(path, args, environ); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "fexecve" fonksiyonu "execve" fonksiyonunun "fd" li versiyonu gibidir. Yani açılmış bir dosyaya ait "fd" betimleyicisini argüman olarak alır. Eğer biz çalıştırmak istediğimiz dosyayı "open" fonksiyonu ile zaten açmışsak, bu fonksiyonu kullanabiliriz. Fonksiyonun prototipi şu şekildedir; #include int fexecve(int fd, char *const argv[], char *const envp[]); Fonksiyonun birinci parametresi, çalıştırılacak dosyaya ait olan "fd" betimleyicisidir. Diğer parametreler ise "execve" ile aynıdır. Eğer bu fonksiyonu çağıracak proses, çalıştırılacak olan program üzerinde "r" ve "w" haklarına sahip değilse, çalıştırılacak olan programın "O_EXEC" bayrağı ile açılmış olması gerekmektedir. Fakat unutmamalıyız ki bu bayrak Linux sistemlerinde mevcut değildir. Öte yandan hem POSIX hem de Linux sistemlerinde çalıştırılacak dosyayı "O_RDONLY" modda açabiliriz. Son olarak Linux sistemlerinde var olan fakat standart olmayan ama "O_EXEC" manasına gelen "O_PATH" bayrağını da kullanabiliriz. Buradan da şu sonucu çıkartabiliriz ki taşınabilirlik açısından "O_RDONLY" modda açmamız gerekiyor. * Örnek 1, Aşağıdaki programı gerçek bir komut satırından çalıştırmamız gerekiyor: #include #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/sh */ printf("Main program started running!...\n"); int fd; if((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && fexecve(fd, &argv[1], environ) == -1) exit_sys("my_execl"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/ls */ printf("Main program started running!...\n"); printf("\nCommand Line Arguments:\n"); for(int i = 0; i < argc; ++i) puts(argv[i]); printf("\nEnvironment Variables:\n"); for(int i = 0; environ[i] != NULL; ++i) puts(environ[i]); printf("Main program is still running!...\n"); int fd; if((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); if(fexecve(fd, &argv[1], environ) == -1) exit_sys("my_execl"); printf("Main program is about to stop!...\n"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/ls */ printf("Main program started running!...\n"); int fd; if((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid && fexecve(fd, &argv[1], environ) == -1) exit_sys("my_execl"); if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, #include #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(int argc, char** argv) { /* # Command Line Arguments # /bin/ls */ printf("Main program started running!...\n"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(!pid) { int fd; /* * Dosya açma işlemi "fork" işleminden sonra ama "exec" işleminden * önce yapıldığı için, dosyanın kapatılması "exec" fonksiyonu * sırasında otomatik olarak gerçekleşecektir. */ if((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); if(fexecve(fd, &argv[1], environ) == -1) exit_sys("my_execl"); // close(fd); } if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Dosyalarda "Close-on-exec" bayrakları: Öyle bir bayraktır ki "exec" yapıldığında ilgili açık olan dosyanın "close" edileceğini belirtir ve her bir "fd" için bir bayrak vardır. Bu bayraklar prosesin kontrol bloğu içerisinde yer alır. Bu bayrağın "set" edilmesi demek, "exec" işlemi sırasında ilgili dosyaların da otomatik olarak kapatılacağı demektir. "file" nesneleri yok edilmiyor, sadece dosyalar kapatılıyor. Yani "file" içerisindeki sayaçlar bir azaltılıyor. Bu sayaçlar sıfıra düştüğünde yok ediliyor. Varsayılan durumda bu bayrak "set" EDİLMEMİŞTİR. Pekiyi bizler bu bayrağı nasıl "set" ederiz? Birinci yöntem dosyayı ilk açışımız sırasında özel bir bayrak kullanmamız ki bunun adı "FD_CLOEXEC" biçimindedir. İkinci yöntemde ise "fcntl" isimli fonksiyonu çağırmamız. -> Dosyayı ilk açış sırasında ilgili bayrağı "set" etmek; int fd; fd = open("test.txt", O_RDONLY | FD_CLOEXEC); -> "fcntl" isimli fonksiyonu ile "set" etmek; int fd; fd = open("test.txt", O_RDONLY); if(fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC) == -1) exit_sys("fcntl"); -> "fcntl" isimli fonksiyonu ile "reset" etmek; int fd; fd = open("test.txt", O_RDONLY); if(fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) & ~FD_CLOEXEC) == -1) exit_sys("fcntl"); Pekiyi bizler bu bayrağı "set" etmesek, "exec" ile çalıştırılan proses evvelki proses dair "fd" niteleyicilerini nasıl bilebilir? Yöntemlerden biri komut satırı argümanlarını kullanmak, diğeriyse prosesler arası haberleşme tekniklerini kullanmak. * Örnek 1, "main" programında açılan dosyaya ait "fd" betimleyicisi, "mample" programına komut satırı olarak gönderilmiştir. /* main.c */ #include #include #include #include #include void exit_sys(const char* msg); int main(void) { printf("Main program started running!...\n"); int fd; if((fd = open("main.c", O_RDONLY)) == -1) exit_sys("open"); char buffer[10 + 1]; sprintf(buffer, "%d", fd); if(execl("mample", "mample", buffer, (char*)0) == -1) exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include int main(int argc, char** argv) { int fd; fd = strtol(argv[1], NULL, 10); char buffer[100 + 1]; ssize_t result; if((result = read(fd, buffer, 100)) == -1) { perror("read"); exit(EXIT_FAILURE); } buffer[result] = '\0'; close(fd); } * Örnek 2, Aşağıdaki örnekte ilgili bayrak "set" edildiği için artık yeni proses okuma yapamayacaktır. /* main.c */ #include #include #include #include #include void exit_sys(const char* msg); int main(void) { printf("Main program started running!...\n"); int fd; if((fd = open("main.c", O_RDONLY | FD_CLOEXEC)) == -1) exit_sys("open"); char buffer[10 + 1]; sprintf(buffer, "%d", fd); if(execl("mample", "mample", buffer, (char*)0) == -1) exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include int main(int argc, char** argv) { int fd; fd = strtol(argv[1], NULL, 10); char buffer[100 + 1]; ssize_t result; if((result = read(fd, buffer, 100)) == -1) { perror("read"); exit(EXIT_FAILURE); } buffer[result] = '\0'; close(fd); } * Örnek 3, Aşağıdaki örnekte "FD_CLOEXEC" bayrağı "set" edilmiştir: * main.c */ #include #include #include #include #include void exit_sys(const char* msg); int main(void) { printf("Main program started running!...\n"); int fd; if((fd = open("main.c", O_RDONLY)) == -1) exit_sys("open"); if(fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC) == -1) exit_sys("fcntl); char buffer[10 + 1]; sprintf(buffer, "%d", fd); if(execl("mample", "mample", buffer, (char*)0) == -1) exit_sys("execl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include int main(int argc, char** argv) { int fd; fd = strtol(argv[1], NULL, 10); char buffer[100 + 1]; ssize_t result; if((result = read(fd, buffer, 100)) == -1) { perror("read"); exit(EXIT_FAILURE); } buffer[result] = '\0'; close(fd); } > Hatırlatıcı Notlar: >> Bu dersteki kodlarda "argc" nin değerinin belli bir sayıya eşit olup olmadığı sınanmamıştır fakat sınanması iyi bir tekniktir. >> Bir sistem fonksiyonunun çağrılması, "syscall" isimli sistem fonksiyonu üzerinden yapılır. * Örnek 1, #include #include #include #include extern char** environ; void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14053 14053 16200 Feb 11 02:50 a.out -rwxrwxrwx 1 root root 409 Feb 11 02:50 main.c */ char* args[] = { "/bin/ls", "-l", NULL }; if(syscall(SYS_execve, "/bin/ls", args, environ) == -1) exit_sys("execve"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # #include #include #include #include #include Yorumlayıcı Dosyalarının Çalıştırılması: "exec" fonksiyonları bir dosyayı çalıştırmadan evvel ilk olarak bu dosyanın çalıştırılabilir bir dosya olup olmadığını sorguluyor. Eğer çalıştırılabilir bir dosya değilse, örneğin ilgili dosyanın bir "text" dosyası olduğunu varsayalım (Linux dünyasında çalıştırılabilir dosyalar "ELF" ya da "a.out" formatındadır), bu durumda bu dosyayı açıp ilk satırı, "\n" karakteri görene kadar okuyor. Okumuş olduğu karakterler ise aşağıdaki kalıba uygun olmalıdır: #! [args] Buradaki "#!" karakterleri birleşik olmalıdır. karakterinden önceki boşluk karakteri opsiyonel bir karakterdir. Yani "#!" ile arasında bir boşluk oladabilir, olmayadabilir. Fakat ile [args] arasında en az bir boşluk olmalıdır. [args] ise birden fazla argümanı temsilen sadece bir defa yazılmıştır, yani [args] yazan yere boşluklarla ayrılmış argümanlar gelebilir. Son olarak "#!" karakterinden evvel boşluk olmamalı ve bu iki karakter bitişik yazılmalıdır. Buradaki "#!" karakterlerine ise literatürde "shebang", "hashbang", "sharp-exclamation" gibi isimler verilmiştir. Eğer birinci satırda yazılanlar bu kalıba uyuyorsa, "exec" fonksiyonları yol ifadesi ile belirtilen dosyayı çalıştırmaya çalışacaklar ve [args] bölümünde belirtilen argüman(lar)ı bu fonksiyona argüman olarak geçecektir. Bütün bu işlemler "kernel" içerisinde gerçekleşmektedir. Tabii buradaki ile belirtilen dosyanın, bu dosyayı çalıştırmak isteyen prosese "x" hakkını da vermiş olması gerekmektedir. * Örnek 1, Aşağıdakiler "shebang" kalıbına uygundur: #! /bin/bash #!/bin/bash #! /usr/bin/python #! make -f * Örnek 2, Aşağıdaki örnekte "shebang" içermeyen bir metin dosyası çalıştırılmak istenmiştir: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # execl: Exec format error */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Prosesimizin "test.txt" dosyası üzerinde "x" hakkına * sahip olması gerekmektedir. */ if(pid == 0 && execl("test.txt", "test.txt", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* test.txt */ Merhaba Dunya * Örnek 3, Aşağıdaki program ile "test.txt" dosyası çalıştırılmak istenmiş fakat "shebang" karakterlerinden dolayı "sample" programı çalıştırılmıştır. /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Sample is running... Optional_Argument /home/sample test.txt ali veli selami */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Görüldüğü üzere "execl" fonksiyonuna geçilen ikinci argüman, "sample" programına * geçilmemiştir. Bunun sebebi, "shebang" sırasında "execl" fonksiyonuna geçilen birinci * argümandan sonraki argümanın es geçilmesidir. */ if(pid == 0 && execl("test.txt", "test.txt", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* test.txt */ #! /home/kaan/sample Optional_Argument /* sample.c */ #include int main(int argc, char** argv) { puts("Sample is running..."); for(int i = 0; i < argc; ++i) { puts(argv[i]); } } Bu örneğe bakarak diyebiliriz ki esas çalıştırılmak istenen "test.txt" dosyasıdır. Bu dosyanın ilk satırında "shebang" karakterleri olduğu için, "sample" programı çalıştırılacaktır. "sample" programına geçilen argümanların sırası ise şu şekildedir; "sample" programındaki <----> İfade ettiği şey argv[0] <----> "shebang" karakterleriyle birlikte yazılan ifadesinin kendisi. argv[1] <----> "shebang" karakterleriyle birlikte yazılan isteğe bağlı [argv] argümanları (eğer varsa). argv[2] <----> "main" programı içerisinde çağrılan "exec" fonksiyonlarına geçilen birinci argüman. argv[3] ... argv[n] <----> "main" programı içerisinde çağrılan "exec" fonksiyonlarına geçilen üçüncü, dördüncü vb. diğer komut satırı argümanları. Eğer böyle bir argüman geçmemişsek, bu indisten sonraki yazılar da boş kalacaktır. Dikkat edilmesi gereken nokta, bu fonksiyona geçilen ikinci argüman es geçilecektir. * Örnek 4, /* test.txt */ #! /home/kaan/sample ankara /* main.c */ //... int main(void) { /* # OUTPUT # Sample is running... /home/kaan/sample ankara test.txt ali veli selami */ //... if(pid == 0 && execl("test.txt", "test.txt", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); //... return 0; } //... /* sample.c */ //... int main(int argc, char** argv) { puts("Sample is running..."); for(int i = 0; i < argc; ++i) { puts(argv[i]); } } * Örnek 5, /* test.txt */ #! /home/kaan/sample ankara /* main.c */ //... int main(void) { /* # OUTPUT # Sample is running... /home/kaan/sample ankara test.txt veli selami */ //... if(pid == 0 && execl("test.txt", "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); //... return 0; } //... /* sample.c */ //... int main(int argc, char** argv) { puts("Sample is running..."); for(int i = 0; i < argc; ++i) { puts(argv[i]); } } Burada dikkat etmemiz gereken bir diğer nokta da "shebang" satırının toplam uzunluğudur. Bu uzunluk 255 karakter ile sınırlanmış olup, 256. karakter olarak "\n" karakteri gelmektedir. Diğer yandan "shebang" karakterleriyle belirtilen dosyanın yol ifadesi mutlak ya da göreli yol ifadesi olabilir. Göreli bir yol ifadesi kullanılması durumunda, "exec" fonksiyonlarını çağıran prosesin "Current Working Directory" konumu baz alınacaktır. Örneğin, #! sample biçiminde yazılan "shebang" satırına göre, "sample" ismi o prosesin "Current Working Directory" konumunda aranacaktır. Fakat tavsiye edilen yöntem ise mutlak yol ifadesi kullanılmasıdır. Öte yandan "shebang" satırında belirtilen [args] kısmına yazacağımız argümanların sayısı arasında standart bir yöntem yoktur. UNIX türevi işletim sistemlerinde bu biçim, işletim sistemini yazan kimselere bırakılmıştır. Bazı sistemler boşluklarla ayrılmış birden fazla argümanı kabul ederken, bazıları sadece bir adet argüman kabul etmektedir. Örneğin, aşağıdaki "shebang" satırındaki [args] kısmında bulunanlar Linux sistemlerinde tek bir argüman olarak ele alınmaktadır; #! /home/kaan/sample ankara istanbul izmir Yani "sample" programını çalıştırdığımız vakit "ankara istanbul izmir" argümanları, "argv[1]" olarak aktarılacaktır. * Örnek 1, /* test.txt */ #! /home/kaan/sample ankara istanbul izmir /* main.c */ //... int main(void) { /* # OUTPUT # Sample is running... /home/kaan/sample ankara istanbul izmir test.txt ali veli selami */ //... if(pid == 0 && execl("test.txt", "test.txt, "ali", "veli", "selami", (char*)0) == -1) exit_sys("execl"); //... return 0; } //... /* sample.c */ //... int main(int argc, char** argv) { puts("Sample is running..."); for(int i = 0; i < argc; ++i) { puts(argv[i]); } } Fakat aynı "shebang" satırını başka UNIX türevi işletim sistemlerinde kullandığımız vakit sadece "ankara" argümanı ilgili prosese aktarmakta, diğerlerini elimine etmektedir. Bazı başka işletim sistemleri ise bu argümanları ayrı ayrı ilgili prosese aktarılmaktadır. İş bu farklılıklardan ötürü bizlerin sadece bir adet argümanı "shebang" satırında belirtmemiz uygun olacaktır. "shebang" satırının bir diğer kullanım yeri de "shell" programı üzerinden "script" dosyalarını çalıştırmaktır. * Örnek 1, Bir "script" dosyasının direkt çalıştırılması: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 1 2 3 4 5 6 7 8 9 10 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * "sample.sh" dosyası çalıştırılmak istenmiştir. Fakat bu dosyadaki "shebang" * satırında herhangi bir argüman kullanılmamıştır. "execl" fonksiyonuna da * herhangi bir argüman geçilmemiştir. Bu durumda "shell.sh" içerisindeki * "script" kodları çalıştırılmıştır. "shell" script dosyalarında "#" karakteri * yorum satırı olarak işlev gördüğünden, herhangi bir sonsuz döngü meydana * gelmemiştir. Prosesimizin, bu şekilde çalıştırdığımız "sample.sh" dosyası * üzerinde "x" hakkında sahip olmasına gerek yoktur. Fakat bizler "shell" * programının kendisi üzerinden, yani "./sample.sh" komutu ile, ilgili "script" * dosyasını çalıştırırsak artık "x" hakkında sahip olmamız gerekmektedir. */ if(pid == 0 && execl("sample.sh", "sample.sh", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* sample.sh */ #! /bin/bash for i in {1..10} do echo $i done * Örnek 2, Bir "script" dosyasının dolaylı yoldan çalıştırılması: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 1 2 3 4 5 6 7 8 9 10 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid == 0 && execl("test.txt", "test.txt", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* test.txt */ #! /bin/bash sample.sh /* sample.sh */ for i in {1..10} do echo $i done * Örnek 3, Bir "python" kodunun çalıştırılması: /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 0 1 2 3 4 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Yine burada "test.txt" dosyasını kullanmadan, tıpkı birinci örnekte olduğu gibi, * direkt olarak "sample.py" dosyasını da çalıştırabiliriz. */ if(pid == 0 && execl("test.txt", "test.txt", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* test.txt */ #! /bin/python3 sample.py /* sample.py */ for i in range(5): print(i) Pekiyi çalıştırmak istediğimiz dosyanın ilk satırında "shebang" yoksa ya da hatalı bir şekilde yazılmışsa ne olacak? Bu durumda "exec" fonksiyonları "-1" ile geri dönüp, "errno" değişkenine uygun değer atayacaktır. Diğer yandan "exec" fonksiyonlarının "p" li versiyonları, örneğin "execlp" ve "execvp", çalıştırılmak istenen dosyanın başında "shebang" görmezlerse sanki dosyanın başında aşağıdaki gibi bir "shebang" varmış gibi davranırlar; #! /bin/sh Öte yandan çalıştırılmak istenen dosyanın da prosese "x" hakkını vermiş olması gerekmektedir. Buradan hareketle diyebiliriz ki "shell" programı üzerinden "script" dosyalarını çalıştırırken başında "shebang" olmasına gerek yoktur eğer "p" li "exec" fonksiyonları kullanırsak. Diğer versiyonlarda böyle bir davranış söz konusu değildir. "p" li versiyonların sahip olduğu bu davranış 2017 POSIX standartlarından itibaren zorunlu kılınmıştır. Son olarak buradaki "sh" programı standartlarca kesin olarak çalışacak program değildir. Başka sistemlerde başka yorumlayıcı programlar çalışabilir. * Örnek 1, /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 1 2 3 4 5 */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); /* * "p" li versiyonlar, dosya ismi içerisinde "/" karakteri olmasına * rağmen işlev görmektedirler. */ if(pid == 0 && execlp("./sample.sh", "./sample.sh", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* sample.sh */ for i in 1 2 3 4 5 do echo $i done POSIX standartlarınca "shebang" satırındaki dosyasının da bir yorumlayıcı dosyasının olması durumunda neler olacağı garanti altına alınmamıştır fakat Linux işletim sisteminde dört kademeye kadar gidilmesine izin verilmiştir. Yani "recursive" biçimde "shebang" uygulamak için sınırımız dört adettir. Fakat tavsiye edilen yöntem, bundan kaçınmamızdır. > "system" fonksiyonu, standart bir C fonksiyonudur. "non-interactive shell" programını çalıştırır. Yani normal "shell" programından bir komut çalıştırırken "-c" komutunu kullanma gibidir. Bir diğer değişle ilgili komutu bir defa çalıştırır ve kendisini sonlandırır. Örneğin, aşağıdaki komutu "shell" programı üzerinde çalıştıralım; bash -c "ls -l; wc sample.c" "bash" programı çalıştırılacak, prosesin "Current Working Directory" konumunda bulunan dosyalar detaylarıyla birlikte ekrana basılacak, devamında ise "sample.c" dosyasındaki karakterlerin adedi ekrana basılacaktır. Daha sonra da "bash" programı sonlanacaktır. İşte "system" fonksiyonu da argüman olarak aldıklarını "bash" programı ile bir defa çalıştırmaktadır. Yine bu fonksiyon da bünyesinde "fork" ve "exec" işlemlerini gerçekleştirmektedir. Fonksiyonun imzası ise aşağıdaki gibidir; #include int system(const char *command); Bu fonksiyon argüman olarak çalıştırılmak istenen "shell" komutlarını yazı biçiminde almakta ve uygun bir geri dönüş değeri ile geri dönmektedir. Geri dönüş değerinin detaylarına ilerleyen derslerde değineceğiz. * Örnek 1, Aşağıdaki program ile "shell" üzerinden "ls -l; wc main.c" komutunu çalıştırmamız aynı şeydir: #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14100 14100 16080 Feb 24 23:18 a.out -rwxrwxrwx 1 root root 274 Feb 24 23:18 main.c 21 33 274 main.c */ system("ls -l; wc main.c"); // system("gcc main.c -o my_main"); // system("gcc main.c -o mymain2"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Temsili bir "system" implementasyonu: #include #include #include #include void my_system(const char* cmd); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14034 14034 16192 Feb 24 23:21 a.out -rwxrwxrwx 1 root root 630 Feb 24 23:21 main.c */ my_system("ls -l"); return 0; } void my_system(const char* cmd) { pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid == 0 && execlp("bash", "bash", "-c", cmd,(char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (31_19_02_2023) > "system" fonksiyonu (devam): Bu fonksiyon ile "shell" programında çalıştırabileceğimiz bütün komutları bir C programı ile de gerçekleştirebiliriz. Bu fonksiyon standart bir C fonksiyon olduğu için sadece UNIX/Linux türevi işletim sistemlerinde değil, bütün sistemlerde işlev görmektedir. Dolayısıyla ilgili sistemde bulunan "shell" programını çalıştırmaktadır. Örneğin, Windows sistemlerinde "cmd.exe" programını çalıştırmaktadır. Pekiyi işletim sistemi olmayan sistemlerde bu fonksiyon nasıl işlev görecektir? Bu durumda bir sistemde "shell" programına benzer bir program olup olmadığını anlamak için, bu programa "NULL" argümanını geçerek sorgulama yaptırmalıyız. Eğer geri dönüş değeri "0" ise ilgili programlardan olmadığını, "non-zero" ise böyle bir "shell" programının varlığından emin olmuş oluruz. Tabii Windows, Linux, macOS gibi işletim sistemi için yazılan programlarda böylesine bir kontrole lüzum yoktur. Pekiyi bu fonksiyon başka hangi değerler ile geri dönmektedir? Daha önce de bahsedildiği üzere bu fonksiyon bünyesinde sırasıyla "fork" ve "exec" yapmaktadır. Eğer başarılı bir şekilde "fork" yapsa fakat "exec" sırasında başarısız olursa, "_exit(127)" çağrısı ile oluşturulan ve "waitpid" fonksiyonu ile elde edilen değere geri dönmektedir. Eğer "fork" sırasında ya da "fork" sonrasındaki "wait" çağrısı sırasında başarısız olursa, "-1" ile geri dönmektedir. Eğer başarılı bir şekilde "fork" ve "exec" yapmışsa, bu durumda çalıştırdığı kabuk programının "waitpid" fonksiyonuyla elde edilen "status" değerine geri dönmektedir. Örneğin, system("ls -l); şeklinde bir fonksiyon çağrısı yapalım. Bu durumda bizim prosesimiz "fork/exec" yaparak "/bin/sh" dosyasını "-c" seçeneği ile çalıştıracaktır. Hayata gelen "/bin/sh" da yine "fork/exec" yaparak, "ls" programını "-l" seçeneğiyle birlikte çalıştıracaktır. Fakat bazı "shell" programları, ikinci defa "fork" yapmadan direkt olarak "exec" yapmaktadırlar. Böylesi bir durumda ise bir defa "fork", iki defa "exec" uygulanmış olur. Eğer bu "ls" komutu da başarısız olursa, "waitpid" ile elde edilen "status" değerine, "system" fonksiyonu geri döner. UNIX/Linux sistemlerinde ise başarılı olan "shell" komutları ekseriyetle "exit code" olarak "0" ile geri dönmektedir. Fakat "errno" değişkeni sadece "fork" işlemi ve "wait" işlemi başarısız olduğunda uygun değere çekilmektedir. Dolayısıyla "errno" değerini ekrana yazdırmak için "system" fonksiyonunun geri dönüş değerinin "-1" ya da "127" olması gerekmektedir. Kabuk komutunun başarısız olması "system" fonksiyonunu bağlamaz. Aşağıda bu fonksiyonun kullanımına dair bir takım örnekler verilmiştir: * Örnek 1, #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14056 14056 16080 Feb 25 19:15 a.out -rwxrwxrwx 1 root root 372 Feb 25 19:15 main.c */ int result; result = system("ls -l"); /* * Burada "system" fonksiyonu ya "fork" ya da "fork" sonrası çağrılan * "wait" sırasında hata olması durumunda "-1" ile geri dönecektir. * Ek olarak, çalıştırdığı komutun hata vermesi durumunda, bu komutun * "waitpid" ile elde edilen "status" bilgisi sorgulanmıştır. Dolayısıyla * hata sorgulamalarını detaylı bir biçimde yapmak istiyorsak, aşağıdaki * gibi bir sorgulama yapmalıyız. */ if( result == -1 || (WIFEXITED(result) && WEXITSTATUS(result) == 127)) { fprintf(stderr, "Command failed!..\n"); exit(EXIT_FAILURE); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14033 14033 16080 Feb 25 19:16 a.out -rwxrwxrwx 1 root root 514 Feb 25 19:16 main.c 30 68 514 main.c */ int result; result = system("ls -l; wc main.c"); /* * Eğer çalıştırdığımız komutun hata durumunu sorgulamak istemiyorsak, * aşağıdaki gibi bir sorgulama da yapabiliriz. */ if( result == -1 ) { exit_sys("system"); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Temsili bir "system" fonksiyon implementasyonu(bazı detaylar göz ardı edilmiştir): #include #include #include #include int my_system(const char* command); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # /bin/sh: 1: la: not found Command failed!.. */ int result; result = my_system("la -l"); /* * Burada "fork" ya da "wait" sırasında bir hata varsa ya da başarılı * bir şekilde sonlanmış ve "exit code" bilgisi de 127 ise programın * akışı bloğun içerisine girecektir. Fakat "errno" değişkeni böylesi * bir senaryoda yeni değerine çekilmeyebilir. */ if(result == -1 || WIFEXITED(result) && WEXITSTATUS(result) == 127) { fprintf(stderr, "Command failed!..\n"); exit(EXIT_FAILURE); } return 0; } int my_system(const char* command) { /* * Eğer ilgili sistemde bir "shell" programı * yoksa "0" ile geri dönmektedir. */ if(command == NULL) return 0; pid_t pid; /* * Eğer "fork" ya da "wait" sırasında hata olursa, * "-1" ile geri dönmektedir. */ if((pid = fork()) == -1) return -1; /* * Eğer "exec" sırasında hata olursa, "127" ile * geri dönmektedir. */ if(pid == 0 && execl("/bin/sh", "/bin/sh", "-c", command, (char*)0) == -1) _exit(127); int status; /* * Eğer "fork" ya da "wait" sırasında hata olursa, * "-1" ile geri dönmektedir. */ if(waitpid(pid, &status, 0) == -1) return -1; /* * Aksi halde "wait" fonksiyonu ile elde edilen * "status" bilgisi ile geri dönmektedir. */ else return status; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi bizler ne zaman "system" fonksiyonu kullanmalı, ne zaman bizzat "fork/exec" yapmalıyız? Eğer işimizi "system" fonksiyonu görüyorsa kullanmalı, bir takım detaylar ile uğraşmak istiyorsak bizler "fork/exec" yapmalıyız. Fakat unutulmamalıdır ki "system" fonksiyonu da bünyesinde "shell" programını çalıştırdığı için bir miktar yavaş ve tükettiği kaynak açısından verimsiz olabilir. > "set-user-id", "set-group-id" ve "sticky" bayrakları: Daha önce detaylarıyla gördüğümüz dosyalara erişim hakları şunlardan meydana gelmekteydi; rwxrwxrwx Bu haklardan soldaki üçlü "owner", ortadaki üçlü "group" ve geri kalan üçlü ise "other" olarak anılan haklardı. Bu haklara ek olarak üç bayrak daha görmüştük ki bunlar hakkında detaylı bilgi o gün verilmemişti. Bahsi geçen o üç bayrak, işte konu başlığı olan bayraklardır. Dolayısıyla dosyalara erişim hakları 12 bayraktan meydana gelmektedir. Pekiyi bu bayrakları fonksiyonlarda nasıl kullanacağız? Bahse konu olan bu üç bayrak "S_ISUID", "S_ISGID" ve "S_ISVTX" sembolik sabitleridir. Bunları gerek "open", gerek "chmod" fonksiyonlarında kullanarak bir dosyanın bu haklarını değiştirebiliriz. Pekiyi "shell" programı üzerinden bu bayrakları nasıl "set" edebiliriz? "set-user-id" bayrağını bir dosya için "set" etmek için "u+s" seçeneğini "chmod" komutuyla birlikte kullanmamız gerekmektedir. Örneğin, rwxrwxrwx haklarına sahip bir "sample" dosyası düşünelim. Bu dosyanın "set-user-id" bayrağını chmod u+s sample komutu ile "set" edersek, artık yeni haklar aşağıdaki gibi olacaktır; rwsrwxrwx Buradan hareketle diyebiliriz ki eğer bir dosya daha önce "x" hakkına sahipse ve bunun üzerine "set-user-id" hakkı verilmişse "x" yerine "s" gelecektir. Unutulmamalıdır ki "u+s" diyerek bizler "owner" gruptakiler için "set-user-id" hakkını vermiş oluyoruz. Pekiyi aşağıdaki haklara sahip bir "test.txt" dosyasına "set-user-id" hakkını verirsek ne olacaktı? rw-rw-rw- Bu durumda "owner" kısmındaki "-" yerine "S" gelecektir. Yani, chmod u+s test.txt komutu sonrasında yeni haklar aşağıdaki gibi olacaktı; rwSrw-rw- Buradan hareketle diyebiliriz ki "x" hakkının daha önce verilmesine bağlı olarak son durum "s" ya da "S" olacaktır. Böylelikle bu haklara bakan kişi hem "x" hakkını hem de "set-user-id" bayrağını anlamış olacaktır. Eğer bir dosyanın "set-group-id" bayrağını "set" etmek istiyorsak, tıpkı "set-user-id" bayrağını "set" ederkenki gibi, "u+s" yerine "g+s" seçeneğini kullanmalıyız. Örneğin, chmod g+s test.txt komutunu çalıştırdığımız zaman "test.txt" dosyasının "group" kısmı için "set-group-id" bayrağı "set" edilmiş olacaktır. Tabii "group" kısmındaki "x" hakkının olmasına ya da olmamasına bağlı olarak, bu harfin olduğu yerde "s" ya da "S" harfi gelecektir. Eğer "chmod" komutuyla birlikte sadece "+s" seçeneğini kullanırsak hem "set-user-id" hem de "set-group-id" bayrakları o dosya için "set" edilmiş olacaktır. Tabii bu iki kısımdaki "x" hakkının durumuna göre "s" ya da "S" harfleri gelecektir. Son olarak "sticky" bayrağını "set" etmek için "chmod" komutuyla birlikte "+t" seçeneceğini kullanmalıyız. Çünkü bu bayrak sadece "other" kısımdakiler için mevcut. "owner" ve "group" için bu bayrağı "set" etmemiz mümkün değildir. Yine bu bayrak için "t" ya da "T" olma durumu, "x" hakkının var olup olmamasına bağlıdır. Bu üç bayrağı tekrar "unset" etmek için de "+" yerine "-" yazmalıyız. Eğer bu üç bayrağı da programlama yoluyla değiştirmek istiyorsak, "chmod" fonksiyonunu şu aşağıdaki şekilde çağırabiliriz; if(chmod("sample", S_IRWXU |S_IRWXG | S_IRWXO | S_ISUID) == -1) exit_sys("chmod"); Artık "sample" programı şu aşağıdaki haklara sahip olacaktır; rwsrwxrwx Pekiyi halihazırda var olan bir dosyaya bu üç bayrak hakkını nasıl ekleyebiliriz? Bunun için ilk önce "stat" fonksiyonlarından birisiyle ilgili dosyanın erişim haklarını temin etmeli, daha sonra bu erişim haklarıyla "S_ISUID", "S_ISGID", "S_ISVTX" bayrak- larından bir ya da birkaçı ile "Bit-wise OR" işlemi yapmalıyız. Ancak "stat" fonksiyonları sadece erişim haklarını değil, ilgili dosyanın tür bilgisini de içermelidir. Dolayısıyla bu tür bilgisi bayraklarını maskeleyerek "Bit-wise OR" işlemi yapmamız tavsiye edilen bir davranıştır. Burada kullanacağımız maske ise "S_IFMT" maskesidir. Örneğin, aşağıdaki kod parçacığı sadece "set-user-id" bayrağını "sample" dosyası nezdinde "set" edecektir. struct stat finfo; if(stat("sample", &finfo) == 0) exit_sys("stat"); if(chmod("sample", (finfo.st_mode & ~S_IFMT) | S_ISUID ) == -1)) exit_sys("chmod"); Şimdi gelelim bu bayrakların asıl işlevine; "set-user-id" ve "set-group-id" bayrakları çalıştırılabilir dosyalar üzerinde etkisi olan bayraklardır. Diyelim ki bizim prosesimizin Kullanıcı ID değerleri "kaan" ve Group ID değerleri de "study" olsun. Öte yandan çalıştırmak istediğimiz "/bin/passwd" dosyasının özellikleri aşağıdaki gibi olsun; -rwsr-xr-x 1 root root ... Bütün herkesin bu dosyayı çalıştırabileceğini buradan anlayabiliyoruz. Ek olarak, "set-user-id" bayrağı da "set" edilmiştir. Bu programı çalıştırmak isteyen bizim prosesimizin Kullanıcı ID değerleri "kaan", Group ID değerleri de "study" olsun. Şimdi bizler "/bin/passwd" dosyasını çalıştırmak istediğimizde, prosesimizin Etkin Kullanıcı ID değerimiz "root" oluyor. Yani çalıştırmak istediğimiz dosyanın User ID değerine çekiliyor. Bu örnek nezdinde sanki "root" kullanıcısı çalıştırmış gibi oluyor. Bunun gerçekleşme sebebi ise ilgili "/bin/passwd" dosyasının "set-user-id" bayrağının "set" edilmesinden dolayıdır. Buradaki kilit nokta çalıştırılabilen bir dosyanın "set-user-id" bayrağı "set" edilmişse, bu dosyayı çalıştırmak isteyen prosesin Etkin Kullanıcı ID değeri iş bu dosyanın User ID değerine çekiliyor. "set-group-id" de tamamen aynı mantıkla işlev görmektedir. Yani çalıştırılabilir bir dosyanın "set-group-id" bayrağı "set" edilmişse, bu dosyayı çalıştırmak isteyen prosesin Etkin Group ID değeri, bu dosyanın Group ID değerine çekilecektir. Tabii bu bayrak, "set-user-id" bayrağı kadar önemli değildir. * Örnek 1, Aşağıdaki örnekte "main" programı, "mample" programını çalıştırmaktadır. Fakat çalıştırmadan evvel "mample" programının Kullanıcı ID ve Group ID değerleri "root" olarak değiştirilmiştir. "set-user-id" komutunu "set" etmeden ve "set" ettikten sonra "mample" programını çalıştırarak aradaki farkı görebiliriz. /* main.c */ #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # */ pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid == 0 && execl("mample", "mample", (char*)0) == -1) exit_sys("execl"); if(wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Real User ID : 14008(runner8) Effective User ID: 14008(runner8) Real Group ID : 14008(runner8) Effective Group ID: 14008(runner8) */ struct passwd* pwd; if ((pwd = getpwuid(getuid())) == NULL) exit_sys("getpwuid"); printf("Real User ID : %lld(%s)\n", (long long)getuid(), pwd->pw_name); if ((pwd = getpwuid(geteuid())) == NULL) exit_sys("getpwuid"); printf("Effective User ID: %lld(%s)\n", (long long)geteuid(), pwd->pw_name); struct group* gr; if ((gr = getgrgid(getgid())) == NULL) exit_sys("getgrgid"); printf("Real Group ID : %lld(%s)\n", (long long)getgid(), gr->gr_name); if ((gr = getgrgid(getegid())) == NULL) exit_sys("getgrgid"); printf("Effective Group ID: %lld(%s)\n", (long long)getegid(), gr->gr_name); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Aşağıda ise bu kodları çalıştıran "shell" programının komutları sırayla yazılmıştır; gcc main.c -o main gcc mample.c -o mample ls -l mample -rwxr-xr-x 1 kaan study ... mample sudo chown root:root mample ls -l mample -rwxr-xr-x 1 root root ... mample ./main Real User ID : 14008(runner8) Effective User ID : 14008(runner8) Real Group ID : 14008(runner8) Effective Group ID: 14008(runner8) sudo chmod u+s mample ls -l mample -rwsr-xr-x 1 root root ... mample ./main Real User ID : 14008(runner8) Effective User ID : 0(root) Real Group ID : 14008(runner8) Effective Group ID: 14008(runner8) sudo chmod g+s mample ls -l mample -rwsr-sr-x 1 root root ... mample ./main Real User ID : 14008(runner8) Effective User ID : 0(root) Real Group ID : 14008(runner8) Effective Group ID: 0(root) Pekiyi "sticky" bayrağı ne işe yaramaktadır? Çok da etkisi olan bir "bit" değildir. Evvelce başka anlamlar için oluşturulmuş, fakat daha sonra başka anlamlar yüklenmiştir. Bu bayrak ise çalıştırılabilir dosyalardan ziyade dizinler üzerinde etkili olan bir bayraktır. Bir dizinin "sticky" bayrağı "set" edilirse, o dizine yazma hakkı olan proses(ler) o dizindeki başkalarına ait (yani Kullanıcı ID değeri başka olan) dosyaları SİLEMEMEKTE ve İSMİ DEĞİŞTİRİLEMEMEKTEDİR. Normalde bir dosyayı silebimek için bu dosyanın içinde bulunduğu dizinde yazma hakkımızın olması yeterlidir fakat bu bayrak "set" edilmişse artık silemiyoruz. Dolayısıyla sadece "root" prosesler ve ilgili dosyanın sahibi olan prosesler silme, isim değiştirme işlemi yapabilecektir. Örneğin, Linux sistemlerindeki "/tmp" dizininin "stick" bayrağı "set" edilmiştir. Dolayısıyla bu dizin içerisinde bulunan dosyalardan sadece bize ait olanları silebilir/ismini değiştirebiliriz. Aşağıda buna bir örnek de verilmiştir. CMD:> ls -ld /tmp drwxrwxrwt 4 root root 120 Mar 5 16:57 /tmp CMD:> id uid=14076 gid=14076 groups=14076 > Hatırlatıcı Notlar: >> "/bin/sh" dosyası aslında "/bin/dash" dosyasına sembolik linktir. "/bin/dash" ise evvelki sistemlerde kullanılan bir "shell" programıyken, "/bin/bash" ise daha gelişmiş bir "shell" programıdır. /*================================================================================================================================*/ (32_25_02_2023) > "set-user-id", "set-group-id" ve "sticky" bayrakları (devam): Daha önce de belirttiğimiz üzere "set-user-id" ve "set-group-id" bayrakları çalıştırılabilir dosyalar için söz konusudur. Fakat dizinler için "set-group-id" bayrağının bir anlamı daha vardır: Bir proses, bir dizin içerisinde yeni bir dosya/dizin hayata getirirse, bu dosyanın/dizinin Kullanıcı ID değeri prosesin Etkin Kullanıcı ID değerini alır. Dosyanın/dizinin Group ID değeri için POSIX iki seçenek sunmaktadır; ya bu dosyayı/dizini hayata getiren prosesin Etkin Group ID ya da dosyanın/dizinin hayata geldiği dizinin Group ID değerini alacak. Linux sistemleri, dosyanın/dizinin hayata geldiği dizinin "set-group-id" bayrağı "set" EDİLMEMİŞSE birinci yöntemi, "set" EDİLMİŞSE ikinci yöntemi izlemektedir. BSD sistemlerinde ise varsayılan durumda ikinci yöntemi izlemektedir. Öte yandan dizinlerin "set-user-id" bayraklarının "set" edilmesi benzer bir etkiye YOL AÇMAMAKTADIR. Fakat bu iki bayrak özünde "executable" dosyalar için söz konusudur. Öte yandan "shebang" içeren dosyalarda durum nasıldır? Çünkü bunlar da neticede çalıştırılabilen birer dosyalardır. Açıkça söylemek gerekirse "exec" fonksiyonları, bir takım güvenlik açıklarından dolayı, bu bayrakları dikkate almamaktadır. Şimdi, elimizde aşağıdaki özelliklere haiz "test.script" isminde bir dosyamız olsun; rwsrwxrwx İş bu dosyamız ise bünyesinde şöyle bir "shebang" satırı barındırsın; #! /bin/abbccc Daha sonra da "other.script" isimli ve "test.script" dosyasına sembolik bağlantı içeren başka bir dosyamız olsun ve bizler bu sembolik bağlantı dosyasını "exec" yapmaya çalışalım. Bu durumda da aslında "test.script" dosyasına "exec" uygulanmış olacaktır. Dolayısıyla aslında "/bin/abbccc" dosyası çalıştırılacaktır ve komut satırı argümanı olarak da "other.script" geçilecektir. İşte tam da bu komut satırının geçildiği anda, kullanıcı "other.script" içerisindeki sembolik linki başka bir dosyaya yönlendirirse, artık "test.script" dosyası değil yeni yönlendirilen dosya çalıştırılacaktır. Bu yöntemle bizler istediğimiz programları çalıştırabiliriz. Bu durum da güvenlik açığına neden olabilir. Artık bu tip "shebang" içeren dosyaların "set-user-id" ve "set-group-id" bayrakları dikkate alınmamaktadır. > Proseslerin "saved-set-user-id" ve "saved-set-group-id" değerleri: Bu güne kadar görmüş olduğumuz proseslere ilişkin ID değerleri şu şekildeydi; Real User ID & Real Group ID Effective User ID & Effective Group ID Normal şartlarda, bu ID değerlerinden "Real User ID" ve "Effective User ID" değerleri birbirinin aynısıdır. Abnormal durumda bu iki ID değeri birbirinden ayrılır ki bu abnormal durum da geçen gördüğümüz konudur; "set-user-id"/"set-group-id" bayrak(ları) "set" edilmiş çalıştırılabilir bir program çalışmışsa prosesin "Real User/Group ID" değerimiz sabit kalırken "Effective User/Group ID" değeri çalıştırılan bu dosyanın "Kullanıcı/Group ID" değeri olarak değiştiriliyor. Şimdi ise torbaya başlıkta belirtilen iki ID değeri daha ekleniyor. Böylelikle proseslere ilişkin ID değerleri aşağıdaki gibidir; Real User ID & Real Group ID & Saved Set User ID Effective User ID & Effective Group ID & Saved Set Group ID Bir proses, "set-user-id" ve/veya "set-group-id" bayrakları "set" edilmiş ya da edilmemiş bir dosyayı çalıştırdığında, bu prosesin "saved-set-user-id" ve/veya "saved-set-group-id" bayrakları, prosesin yeni "Effective User ID" ve/veya "Effective Group ID" değerlerini alırlar. Örneğin, aşağıdaki ID değerlerine sahip bir prosesimiz olsun; Real User ID: kaan Effective User ID: kaan Real Group ID: study Effective Group ID: study ... Şimdi de "set-user-id" bayrağı "set" edilmiş aşağıdaki dosyayı bu proses ile çalıştıralım. -rwsr-xr-x 1 root root 59976 Nov 24 12:05 /bin/passwd Artık prosesimizin ID değerleri aşağıdaki gibi olacaktır; Real User ID: kaan Effective User ID: root Real Group ID: study Effective Group ID: study ... Bu işlem sonucunda prosesimizin "saved-set-user-id" değeri de "root" olacaktır. Dolayısıyla proseslerin ID değerleri aşağıdaki gibidir: Real User ID: kaan Effective User ID: root Saved User ID: root Real Group ID: study Effective Group ID: study Saved Group ID: root Burada prosesin "Effective Group ID" değerinin aynı kalmasının yegane sebebi, "/bin/passwd" dosyasının sadece "set-user-id" bayrağının "set" edilmiş olmasından kaynaklanmaktadır. Özetle şunu diyebiliriz ki bir proses "exec" işlemi uyguladığında, "saved-set-user-id" ve "saved-set-group-id" bayrakları, ilgili dosyanın Kullanıcı ID ve Group ID değerlerini almaktadır. Bu işlemler sırasında ilgili dosyanın "set-user-id" ve "set-group-id" bayraklarının bir önemi yoktur. Tabii bu iki yeni ID değeri de yine prosesin kontrol bloğu içerisinde saklanmaktadır. Pekiyi bu iki yeni ID değerinin işlevi nedir? Bu ID değerleri, proseslerin ID değerlerinde oynama yapan bir takım POSIX fonksiyonlarında kullanılmaktadır. > Proseslerin ID değerleriyle ilişkili POSIX fonksiyonları: O an çalışmakta olan bir prosesin "Real User ID" ve "Effective User ID" değerlerini bize veren iki fonksiyon vardır. Bunlar sırasıyla "getuid" ve "geteuid" isimli fonksiyonlardır. Fakat POSIX dünyasında "saved-set-user-id" ve "saved-set-group-id" değerlerini bize veren bir fonksiyon yoktur. Fakat Linux dünyasında bu bayrakları bize veren fonksiyonlar mevcuttur. >> "getuid" ve "geteuid" fonksiyonları, aşağıdaki imzaya sahiptir: #include uid_t getuid(void); uid_t geteuid(void); Bu fonksiyonların başarısız olması mümkün değildir. Geri dönüş değeri bir "typedef" biçimindedir ve karşılık geldiği tür sistemden sisteme değişmektedir. Fakat bu türün bir tam sayı olması şarttır. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Real User ID: 14012(runner12) Effective User ID: 14012(runner12) */ struct passwd* pass; uid_t r_uid; r_uid = getuid(); if((pass = getpwuid(r_uid)) == NULL) exit_sys("getpwuid"); printf("Real User ID: %ju(%s)\n", (uintmax_t)r_uid, pass->pw_name); uid_t e_uid; e_uid = geteuid(); if((pass = getpwuid(e_uid)) == NULL) exit_sys("getpwuid"); printf("Effective User ID: %ju(%s)\n", (uintmax_t)e_uid, pass->pw_name); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi o anda çalışmakta olan bir prosesin "Real Group ID" ve "Effective Group ID" değerlerini nasıl temin edebiliriz? Bunun için de sırasıyla "getgid" ve "getegid" fonksiyonlarını kullanacağız. >> "getgid" ve "getegid" fonksiyonları, aşağıdaki imzaya sahiptir: #include gid_t getgid(void); gid_t getegid(void); Bu fonksiyonlar da yine başarısız olamamaktadır. Yine geri dönüş değerinin türü de "typedef" edilmiştir ve hangi türe karşılık geldiği sistemden sisteme değişmektedir. Fakat tam sayı bir tür olması zorunluluktur. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Real Group ID: 14024(runner24) Effective Group ID: 14024(runner24) */ struct group* grp; gid_t r_gid; r_gid = getgid(); if((grp = getgrgid(r_gid)) == NULL) exit_sys("getgrgid"); printf("Real Group ID: %ju(%s)\n", (uintmax_t)r_gid, grp->gr_name); gid_t e_gid; e_gid = getegid(); if((grp = getgrgid(e_gid)) == NULL) exit_sys("getgrgid"); printf("Effective Group ID: %ju(%s)\n", (uintmax_t)e_gid, grp->gr_name); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Aşağıda ise yukarıda işlenen dört fonksiyonun tek bir programdaki kullanımı gösterilmiştir: * Örnek 1, #include #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Real User ID: 14011(runner11) Real Group ID: 14011(runner11) Effective User ID: 14011(runner11) Effective Group ID: 14011(runner11) */ struct passwd* pass; struct group* grp; uid_t r_uid; r_uid = getuid(); if((pass = getpwuid(r_uid)) == NULL) exit_sys("getpwuid"); printf("Real User ID: %ju(%s)\n", (uintmax_t)r_uid, pass->pw_name); gid_t r_gid; r_gid = getgid(); if((grp = getgrgid(r_gid)) == NULL) exit_sys("getgrgid"); printf("Real Group ID: %ju(%s)\n", (uintmax_t)r_gid, grp->gr_name); uid_t e_uid; e_uid = geteuid(); if((pass = getpwuid(e_uid)) == NULL) exit_sys("getpwuid"); printf("Effective User ID: %ju(%s)\n", (uintmax_t)e_uid, pass->pw_name); gid_t e_gid; e_gid = getegid(); if((grp = getgrgid(e_gid)) == NULL) exit_sys("getgrgid"); printf("Effective Group ID: %ju(%s)\n", (uintmax_t)e_gid, grp->gr_name); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi bizler proseslerin ID değerlerini nasıl değiştirebiliriz? İşte bu durumda devreye şu dört fonksiyon girmektedir; "setuid", "seteuid", "setgid" ve "setegid". Bu dört fonksiyon da başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri döner ve "errno" değişkenini uygun bir değerle değiştirir. >> "setuid" fonksiyonu, aşağıdaki gibi bir parametrik yapıya sahiptir: #include int setuid(uid_t uid); Bu fonksiyon, eğer kendisini çağıran proses "priviledged-user" ise, kendisinin "Real User ID", "Effective User ID" ve "Saved User ID" değerlerini argüman olarak aldığı ID değeri ile değiştirmektedir. Ayrı ayrı ID değerlerini değiştirmemekte, üçünü birden değiştirmektedir. Bu noktada "getuid" fonksiyonundan ayrılmaktadır çünkü o fonksiyon sadece "Real User ID" değerini bize geri döndürmektedir. Pekiyi bu fonksiyonu çağıran proses "priviledged-user" değilse, nasıl işlev görecek? Eğer argüman olarak aldığı ID değeri bu fonksiyonu çağıran prosesin "Real User ID" ya da "Saved User ID" değerine eşitse, artık "Effective User ID" değerini iş bu argüman olarak alınan ID değerine eşit olacak. Özetle; -> Eğer prosesimiz "priviledged-user" ise bu fonksiyon ile prosesimizin "Real User ID", "Effective User ID" ve "Saved User ID" değerlerinin üçü birden değiştirilir. -> Eğer prosesimiz "priviledged-user" DEĞİL İSE ve bu fonksiyona geçilen ID değeri prosesimizin "Real User ID" ya da "Saved User ID" değerine eşitse, sadece "Effective User ID" değeri değiştirilir. Şu örneği inceleyelim; ID değerleri aşağıdaki gibi olan bir prosesimiz olsun. Real User ID: kaan Effective User ID: kaan Saved User ID: kaan ... Bu proses "set-user-id" bayrağı "set" edilmiş, Kullanıcı ID ve Group ID değeri "root" olan bir dosyayı çalıştırsın. Artık prosesimiz aşağıdaki ID değerlerine sahip olacaktır. Real User ID: kaan Effective User ID: root Saved User ID: root ... Şimdi de bir sonraki paragrafta detaylarını göreceğimiz "seteuid" fonksiyonu ile prosesimizin sadece "Effective User ID" değerini "kaan" olarak değiştirelim. Artık prosesimiz aşağıdaki ID değerlerine sahip olacaktır; Real User ID: kaan Effective User ID: kaan Saved User ID: root ... Şimdi ise "setuid" fonksiyonuna çağrı yaparak yeniden "root" olabiliriz. Çünkü bizler "priviledged-user" değiliz. Bu durumda argüman olarak geçilen ID değeri ya "Real User ID" değerine ya da "Saved User ID" değerine eşit olmalıdır. Artık prosesimiz aşağıdaki ID değerlerine sahip olacaktır; Real User ID: root Effective User ID: root Saved User ID: root ... İşte "Saved User ID" değerinin esas var oluş amacı da "root" a yükseltilen ID değerini "kaan" a düşürmek ve tekrardan "root" haline getirmektir. >> "seteuid" fonksiyonu, aşağıdaki gibi bir parametrik yapıya sahiptir: #include int seteuid(uid_t uid); Bu fonksiyon, eğer kendisini çağıran proses "priviledged-user" ise sadece "Effective User ID" değerini argüman olarak aldığı ID değeri ile değiştirmektedir. Bu fonksiyon, "setuid" fonksiyonundan daha sık kullanılmaktadır. Pekiyi bu fonksiyonu çağıran proses "priviledged-user" değilse, nasıl işlev görecek? Tıpkı "setuid" gibi işlev görecektir. Yani, kendisini çağıran prosesin "Real User ID" değeri ya da "Saved User ID" değeri argüman olarak alınan ID değerine eşitse, prosesimizin sadece "Effective User ID" değerini değiştirecektir. Şimdi de yukarıda anlatılan senaryonun üzerinden tekrar geçelim: Prosimizin işin başında aşağıdaki ID değerlerine sahip olsun; Real User ID: kaan Effective User ID: kaan Saved User ID: kaan ... Şimdi de "set-user-id" bayrağı "set" edilmiş, Kullanıcı ID ve Group ID değeri "root" olan bir programı prosesimiz ile "exec" yapalım. Artık yeni ID değerleri aşağıdaki gibi olacaktır; Real User ID: kaan Effective User ID: root Saved User ID: root ... Şimdi iş bu programımız da bünyesinde aşağıdaki gibi bir fonksiyon çağrısı barındırsın; ... seteuid(getuid()); ... Artık bizim esas prosesimiz aşağıdaki ID değerlerine sahip olacaktır; Real User ID: kaan Effective User ID: kaan Saved User ID: root ... Bu noktada prosesimiz "root" durumundan "kaan" durumuna düşmüş oldu. Artık "root" gibi işlem yapamayız. "kaan" olarak bir takım işlemler yaptıktan sonra tekrardan "root" olmak isteyelim. Bu durumda aşağıdaki kod parçacıkları işimizi görecektir; ... seteuid(0); Yukarıdaki fonksiyon çağrısı yerine aşağıdaki fonksiyon çağrısı da aynı işlevi görecektir çünkü bizim prosesimiz artık "priviledged-user" değil ve argüman olarak geçilen ID değeri de bizim "Saved User ID" değerine eşit. Artık prosesimiz aşağıdaki ID değerlerine sahip olacaktır. Real User ID: kaan Effective User ID: root Saved User ID: root ... Bu noktada prosesimiz "kaan" durumundan "root" durumuna yükselmiş oldu. Artık "root" olarak hayatımıza devam edebiliriz. Buradan da görüleceğiz üzere saklı ID değerleri kavramı hiç olmasaydı, bu tip geri dönüşler mümkün olmayacaktı. İşte bu nedenden dolayı saklı ID değerleri vardır. Eğer yapacaklarımız arasında bu tip kıdem düşüşü yoksa/yükselişi yoksa, saklı ID ile uğraşmamıza gerek yoktur. >> "setgid" ve "setegid" fonksiyonları da sırasıyla "setuid" ve "seteuid" fonksiyonlarıyla aynı semantiğe sahiptir. Kullanıcı yerine Group değişmektedir. İş bu fonksiyonlar aşağıdaki parametrik yapıya sahiptir; #include int setgid(gid_t gid); int setegid(gid_t gid); Bu fonksiyonlar, Group ID değerleri çok önemli olmadığından, pek fazla kullanılmamaktadırlar. Yine "setuid" ve "seteuid" fonksiyonlarındaki aynı senaryo, bu fonksiyonlarda da geçerlidir. Şöyleki; İşin başında prosesimiz aşağıdaki ID değerlerine sahip olsun: Real User ID: kaan Effective User ID: kaan Saved User ID: kaan Real Group ID: study Effective Group ID: study Saved Group ID: study Şimdi biz "set-group-id" bayrağı "set" edilmiş, Kullanıcı ID değeri "ali" ve Group ID değeri "test" olan bir programa "exec" uygulayalım. Artık bizim prosesimizin yeni ID değerleri aşağıdaki gibi olacaktır. Real User ID: kaan Effective User ID: kaan Saved User ID: kaan Real Group ID: study Effective Group ID: test Saved Group ID: test Şimdi hayata gelen yeni program ise bünyesinde şöyle kodlar barındırsın; ... setegid(getgid()); ... Artık bizim esas prosesimizin ID değerleri şöyle olacaktır: Real User ID: kaan Effective User ID: kaan Saved User ID: kaan Real Group ID: study Effective Group ID: study Saved Group ID: test Artık bu noktada prosesimizin Group ID değeri "test" den "study" olmuş oldu. Bir takım işlemler yaptıktan sonra tekrardan geri dönmek isteyelim; ... setegid(test_gid); ... Artık prosesimiz aşağıdaki ID değerlerine sahip olacaktır; Real User ID: kaan Effective User ID: kaan Saved User ID: kaan Real Group ID: study Effective Group ID: test Saved Group ID: test Bu dört fonksiyonu da kısaca özetlemek gerekirse; -> Eğer "priviledged-user" isek, --> "setuid" ve "setgid" fonksiyonları ile prosesimizin bütün ID değerlerini değiştiririz. --> "seteuid" ve "setegid" fonksiyonları ise sadece "Effective User ID" ve "Effective Group ID" değerlerini değiştirir. -> Eğer "priviledged-user" değilsek ve prosesimizin "Real User ID"/"Real Group ID" ya da "Saved User ID"/"Saved Group ID" değerleri argüman olarak alınan ID değerine eşitse, "setuid"/"setgid" ve "seteuid"/"setegid" fonksiyonları sadece prosesimizin "Effective User ID"/"Effective Group ID" değerini değiştirir. Ayrıca, eğer bizler bu tip geri dönmeler ile işimiz yoksa, saklı ID değerleriyle bir işimiz yoktur. Son olarak, "Advanced Programming In The Linux Env." ya da "The Linux Programming Interface" isimli kitaplardan saklı ID değerlerine ilişkin kısımlara göz atılması tavsiye edilir. Şimdi yukarıda anlatılan dört fonksiyona ek olarak iki fonksiyon daha mevcuttur. Bunlar sırasıyla "setreuid" ve "setregid" isimli fonksiyonlardır. Aslında bu fonksiyonlara mutlak anlamda gerek yoktur ancak bazı işlemleri hızlandırmaktadır. Aslında "setreuid"/"setregid" fonksiyonları bir nevi "switch" yapmak için yazılmış bir fonksiyondur. Yani "Real User ID"/"Real Group ID" ile "Effective User ID"/"Effective Group ID" değerlerini birbiriyle değiştirmek için kullanılır. Fonksiyonlar aşağıdaki parametrik yapıya sahiptir: #include int setreuid(uid_t ruid, uid_t euid); int setregid(gid_t rgid, gid_t egid); Eğer değiştirilmek istenmeyen bir ID varsa, o parametre için "-1" değeri argüman olarak geçilir. Artık argüman geçilen diğer ID değeri değiştirilecektir. Fonksiyon aşağıdaki senaryoya göre çalışmaktadır: -> Eğer "priviledged-user" isek, her iki ID de değiştirilir. -> Eğer "priviledged-user" değil isek ve argüman olarak geçilen ID değeri prosesimizin "Real", "Effective" ya da "Saved" ID değerlerinden birine eşitse, "setreuid" fonksiyonu sadece "Effective" olan ID değeri değiştirilir. "setregid" fonksiyonunun aynı eşitlemeyi yapabilmesi için argüman olan ID ile ya "Real" ya da "Saved" ID değerinin eşit olması gerekmektedir. Son olarak, "setreuid" fonksiyonu bazında, "Real" ID değerinin "Effective" ya da "Saved" ID değerine eşitlenmesi POSIX standartlarınca tanımlı değildir. Fakat Linux sistemlerinde bu davranış tanımlıdır fakat o da sadece "Effective" ID üzerinde işlem yapmaktadır. Ama "setregid" fonksiyonu için bahsi geçen bu son davranış TANIMLIDIR. Ek olarak eğer prosesin "Real" ID değeri değiştirilmişse ya da "Effective" ID değeri "Real" ID değerinden farklı bir değere eşitlenmişse, bu durumda "Saved" ID değeri yeni "Effective" ID değerine eşitlenecektir. Öte yandan bu fonksiyona geçilen argümanların bir tanesi bile uygun değilse, fonksiyon tümüyle başarısız olmaktadır. Tabii başarı durumunda "0", başarısızluk durumunda "-1" ile geri döner ve "errno" uygun bir şekilde "set" edilir. Örneğin, aşağıdaki gibi bir fonksiyon çağrısı yapmış olalım; ... setreuid(geteuid(), getuid()); Burada yapılan şey "Effective User ID" değerini "Real User ID" değerine atamak, "Real User ID" değerini de "Effective User ID" değerine atamak. Bu fonksiyonu çağıran prosesin İLK HALİ DE AŞAĞIDAKİ GİBİ OLSUN; Real User ID: kaan Effective User ID: root Saved User ID: root ... Fakat yukarıdaki fonksiyon çağrısı sonrasında ID değerleri aşağıdaki gibi olacaktır; Real User ID: root Effective User ID: kaan Saved User ID: root ... Burada "Saved User ID" değiştirilmedi çünkü "Effective User ID" değeri "Real User ID" değerine atandı. Başka bir ID değerine atansaydı, o da değiştirilecekti. Eğer tekrar eski haline dönmek istiyorsak, yine yukarıdaki fonksiyon çağrısını yapmalıyız. Eğer bu fonksiyonu çağıran proses "priviledged-user" OLMASAYDI, "Real User ID" değerinin değiştirilmesi POSIX standartlarınca ucu açık bırakılmıştır. Buradaki "switch" ile kastedilen işte bu şekildeki fonksiyon çağrısıdır. Aksi halde "Real User ID" ve "Effective User ID" değerlerini başka değerlere de "set" edebiliriz eğer yeni ID değerleri uygun ise. Tabii bütün bu altı fonksiyon için en doğru kaynak yine POSIX dökümantasyonudur. Fonksiyonu kullanmadan evvel ilgili dökümantasyondan son kontrol yapılması tavsiye edilir. Linux türevi sistemlerde ise bu üç ID değerini alabileceğimiz bir fonksiyon vardır. Bu fonksiyonlar "getresuid" ve "getresgid" isimli fonksiyonlardır. "uid_t"/"gid_t" türünden adres alırlar ve ID değerlerini bu adresin içine yazarlar. Bu fonksiyonlar için de "#define _GNU_SOURCE" tanımlamasını yapmalıyız. Aksi halde bu fonksiyonları kullanamayız. * Örnek 1, #define _GNU_SOURCE #include #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Real User ID: 14027(runner27) Effective User ID: 14027(runner27) Saved User ID: 14027(runner27) Real Group ID: 0(root) Effective Group ID: 0(root) Saved Group ID: 0(root) */ struct passwd* pass; struct group* grp; uid_t r_uid, e_uid, s_uid; gid_t r_gid, e_gid, s_gid; if(getresuid(&r_uid, &e_uid, &s_uid) == -1) exit_sys("getresuid"); if((pass = getpwuid(r_uid)) == NULL) exit_sys("getpwuid"); printf("Real User ID: %ju(%s)\n", (uintmax_t)r_uid, pass->pw_name); if((pass = getpwuid(e_uid)) == NULL) exit_sys("getpwuid"); printf("Effective User ID: %ju(%s)\n", (uintmax_t)e_uid, pass->pw_name); if((pass = getpwuid(s_uid)) == NULL) exit_sys("getpwuid"); printf("Saved User ID: %ju(%s)\n", (uintmax_t)s_uid, pass->pw_name); if((grp = getgrgid(r_gid)) == NULL) exit_sys("getgrgid"); printf("Real Group ID: %ju(%s)\n", (uintmax_t)r_gid, grp->gr_name); if((grp = getgrgid(e_gid)) == NULL) exit_sys("getgrgid"); printf("Effective Group ID: %ju(%s)\n", (uintmax_t)e_gid, grp->gr_name); if((grp = getgrgid(s_gid)) == NULL) exit_sys("getgrgid"); printf("Saved Group ID: %ju(%s)\n", (uintmax_t)s_gid, grp->gr_name); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } ÖZETLE "set-user-id"/"set-group-id" bayrakları "set" edilmiş bir programı çalıştırırken prosesimizin ID değerini kısa süreliğine değiştirip tekrar eski haline getirmek için iş bu saklı ID ve burada bahsedilen 6-7 fonksiyonu kullanabiliriz. > Hatırlatıcı Notlar: >> Dosya işlemlerindeki testlerde proseslerin Etkin ID değerleri kullanılmaktadır. >> "https://pubs.opengroup.org/onlinepubs/9699919799/" adresindeki dökümanlar bütün POSIX dünyasına ilişkin dökümanlardır fakat "man" sayfaları sadece o sisteme ilişkindir. POSIX standartları ile "man" arasında farklılık oluşabilir. >> "types.h" isimli başlık dosyasında, türlere ilişkin "typedef" bilgileri mevcuttur. >> POSIX dünyasındaki yetki konusu ya hep ya hiç şeklindeyken Linux dünyasında "appropriate privileges" mevzusu vardır. Buradan kastedilen "capability" durumudur. "appropriate-user/priviledged-user/appropriate privileges" derken de kastedilen ya "root" olmamız ya da bir şeyleri yapmak için "capability" e sahip olmamızdır. /*================================================================================================================================*/ (33_26_02_2023) > Proseslerin ID değerleriyle ilişkili POSIX fonksiyonları (devam): Linux sistemlerinde mevcut olan "getresuid" ve "getresgid" fonksiyonlarının "set" eden versiyonları da vardır. İsimleri "setresuid" ve "setresgid" biçimindedir. Bu fonksiyonların imzaları aşağıdaki gibidir; #define _GNU_SOURCE /* See feature_test_macros(7) */ #include int setresuid(uid_t ruid, uid_t euid, uid_t suid); int setresgid(gid_t rgid, gid_t egid, gid_t sgid); Uygun önceliğe sahip olmayan prosesler "Real", "Effective" ve "Saved" ID değerlerini ancak ve ancak o anki "Real", "Effective" ve "Saved" ID değerlerinden birisiyle değiştirebilirler. Başka bir ID değeriyle değiştiremezler. Fakat prosesimiz uygun önceliklere sahipse, yani "priviledged-user" ise, iş bu ID değerlerini herhangi bir ID değeriyle değiştirebilirler. Yine bu fonksiyona geçilen uygunsuz bir argüman durumunda fonksiyon tümüyle başarısız olur. Yine bu fonksiyon için de "#define _GNU_SOURCE" tanımlamasını yapmalıyız. Aksi halde bu fonksiyonları kullanamayız. > Proseslerin Ek Grupları("Supplementary Groups") : Anımsanacağı üzere bir prosesin bir dosyaya erişim hakları sorgulanırken ilk önce prosesin "root" olup olmadığına bakılıyor. "root" olduğu anlaşıldığında bu sorgulama sonlanıyor ve bütün haklar ilgili prosese veriliyordu. Eğer "root" olmadığı anlaşılırsa, ilgili prosesin sırasıyla o dosyanın sahibi mi yoksa aynı grupta mı olup olmadığı sorgulanmaktaydı. En son da ilgili prosesin diğer kullanıcı olduğu varsayılıyordu. İşte sorgulama grup aşamasına geldiğinde sadece prosesin grup ID değerlerine değil, aynı zamanda prosesin sahip olduğu ek grup ID değerlerine de bakılıyor. Eğer bunlardan birisi uyuşuyorsa, ilgili dosya ile prosesin aynı grupta olduğu anlaşılmaktadır. Buradaki önemli nokta bir prosesin grup ID değerleri ile ek gruplarının ID değerleri aynı kıdem seviyesinde olmasıdır. Burada amaçlanan şey bir prosesin biden fazla grup ID değerine sahip olmasıdır. İşte bu ek gruplara da "supplementary group" denmektedir. Pekiyi bir prosesin ek grup ID değerleri nasıl tespit ediliyor? Hatırlayacağınız üzere "/etc/passwd" dosyasdından bakarak bir prosesin "Real User ID" ve "Real Group ID" değerleri temin edilmektedir. Bu işlem tabii "login" programı tarafından yürütülmektedir. >> "login" programı şematik olarak şu şekilde işlev görmektedir; -> "/etc/passwd" dosyasına başvurarak ilk önce o kullanıcıya için parola doğrulaması yapmaktadır. Tabii bunun için "/etc/shadow" dosyasına da başvurması gerekebilir eğer şifre bu dosyada ise. -> Eğer doğrulama işlemi başarılı ise yine "/etc/passwd" dosyasına başvurarak burada belirtilen Kullanıcı ID ve Group ID değerlerini ilgili prosesin "Real", "Effective" ve "Saved" ID değerleri olarak atıyor. Bu işlemi ise "setuid" ve "setgid" fonksiyonları ile yapmaktadır. -> "chdir" fonksiyonuyla prosesin "Current Working Directory" konumunu, "/etc/passwd" dosyasında belirtilen konum olarak güncelle. -> Daha sonra "exec" fonksiyonu ile "/etc/passwd" dosyasında belirtilen "shell" programı çalıştırılır. Burada "login" programının "fork" yapmadan direkt olarak "exec" yaptığına dikkat ediniz. Ek gruplar ise önce "/etc/passwd", sonra "/etc/groups" dosyalarından temin edilmektedir. "/etc/groups" dosyasındaki her bir satır bir gruba aittir ve her bir satırın sonunda ise o gruba dahil olanlar belirtilmiştir. İşte "/etc/passwd" dosyasından elde edilen grup ID değeri, "/etc/groups" dosyasında tek tek aranıyor ve bir yerde toplanıyor. Tipik bir "/etc/groups" dosyası aşağıdaki gibidir: ... r9:x:14009: runner10:x:14010: kaan, ali runner11:x:14011: kaan runner12:x:14012: serdar ... Prosesin ek grupları da yine prosesin kontrol bloğunda saklanmaktadır. Bu ek grupları temin etmek için POSIX sistemlerinde "getgroups" isimli bir fonksiyon mevcuttur. Bu fonksiyon, ek gruplara ilişkin ID değerlerini prosesin kontrol bloğundan temin etmektedir. İlgili fonksiyonun prototipi aşağıdaki şekildedir; #include int getgroups(int gidsetsize, gid_t grouplist[]); Fonksiyonun ikinci parametresi, elemanları "gid_t" türünden olan bir dizinin başlangıç adresi. Dizinin birinci parametresi ise iş bu dizinin toplam eleman sayısıdır. Fonksiyon, başarılı olması durumunda olması gereken ek grup ID adedine geri dönmektedir. Başarısızlık durumunda ise "-1" ile. Eğer bu fonksiyona geçilen dizinin eleman sayısı, olması gereken ek grup ID adedinden az ise fonksiyon başarısız olacaktır. Ek olarak birinci parametreye "0" değerini geçersek, olması gereken ID adedini döndürür ve ikinci parametre ile geçilen diziye de bu ID değerlerini İŞLEMEZ. Pekiyi bir ek grup adedi ne kadardır? Bunun cevabı sistemden sisteme değişkenlik gösterdiği için, "limits.h" dosyasında tanımlı, "NGROUPS_MAX" isimli sembolik sabitini kullanabiliriz. Fakat bu sembolik sabit çalışma zamanında artabilir. Bu durumda da "sysconf" fonksiyonu ile son halini "get" etmeliyiz. Öte yandan, bütün bunlar ile uğraşmak yerine, iş bu fonksiyonun birinci parametresine "0" geçmeli ve geri dönüş değeri kadarlık bir alanı bellekte tahsis etmeliyiz. Ayrıca POSIX standartlarınca proseslerin "Effective Group ID" değerinin bu diziye yazılıp yazılmayacağı, işletim sistemini yazanlara bırakılmıştır ama Linux sistemleri "Effective Group ID" değerini de bu diziye yazmaktadır. Dolayısıyla bellekte yer tahsis ederken "NGROUPS_MAX + 1" büyüklüğünde yer tahsis etmeliyiz. Ancak bu fonksiyonun birinci parametresine "0" geçildiğinde geri döndürülen değer "Effective Group ID" değerini de kapsamaktadır. Özetle; -> "NGROUPS_MAX + 1" büyüklüğünde bellekte bir yer tahsis edilir. Fonksiyon başarısız olursa, "sysconf" fonksiyonu ile esas büyüklük alınır ve bellekteki bu alan tekrar genişletilir. -> Direkt olarak "sysconf" fonksiyonu ile esas büyüklük elde edilir ve bu büyüklük kullanılarak bellekte yer açılır. -> Dizi uzunluğu yeteri kadar büyük bir değer olduğu varsayılır. Fakat bu fonksiyonun başarısı da kontrol edilmelidir. -> Bu fonksiyonun birinci parametresine "0" geçilir ve olması gereken ID değerlerinin adedi alınır. Bu adet sayısına göre bellekte yer tahsisi yapılır. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { int n_groups; if((n_groups = getgroups(0, NULL)) == -1) exit_sys("getgroups"); printf("[%d] => ", n_groups); gid_t* sgids; if((sgids = (gid_t*)malloc(n_groups * sizeof(gid_t))) == NULL) { fprintf(stderr, "Not enough of memory!...\n"); exit(EXIT_FAILURE); } if(getgroups(n_groups, sgids) == -1) exit_sys("getgroups"); struct group* grp; for(int i = 0; i < n_groups; ++i) { if((grp = getgrgid(sgids[i])) == NULL) exit_sys("getgrgid"); if(i != 0) printf(", "); printf("%ju(%s) ", (uintmax_t)sgids[i], grp->gr_name); } /* Tamponu sıfırlamak için çağrılmıştır. */ printf("\n"); free(sgids); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bu ek grupları "set" etmek için bir fonksiyon POSIX fonksiyonlarında mevcut değildir. Fakat Linux sistemlerinde "setgroups" isminde bir fonksiyon tanımlamışlardır. Fonksiyonun prototipi aşağıdaki gibidir; #include int setgroups(size_t size, const gid_t *list); Fonksiyonun ikinci parametresi, "set" edilecek ek grupların bulunduğu dizinin başlangıç adresidir. Bu dizideki ID değerleri, prosesin ek grup ID değeri olarak atanacaktır. Fakat bu listeye, prosesin "Effective Group ID" değeri eklenmemelidir. Başarı durumunda proses "0", başarısız durumda ise "-1" ile geri dönmektedir. Yine unutmamalıyız ki bu fonksiyonu çağırabilmek için de prosesin uygun önceliğe sahip olması gerekmektedir. Fonksiyonun birinci parametresi ise yine ikinci parametre ile geçilen dizinin toplam eleman sayısını belirtmektedir. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # [0] => setgroups: Operation not permitted */ int n_groups; if((n_groups = getgroups(0, NULL)) == -1) exit_sys("getgroups"); printf("[%d] => ", n_groups); gid_t* sgids; if((sgids = (gid_t*)malloc(n_groups * sizeof(gid_t))) == NULL) { fprintf(stderr, "Not enough of memory!...\n"); exit(EXIT_FAILURE); } if(getgroups(n_groups, sgids) == -1) exit_sys("getgroups"); struct group* grp; for(int i = 0; i < n_groups; ++i) { if((grp = getgrgid(sgids[i])) == NULL) exit_sys("getgrgid"); if(i != 0) printf(", "); printf("%ju(%s) ", (uintmax_t)sgids[i], grp->gr_name); } printf("\n"); if(setgroups(n_groups, sgids) == -1) exit_sys("setgroups"); free(sgids); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Bu programın "set-user-id" bayrağının "set" edilmiş olması gerekmektedir. Bunun için de ilk önce "sample" programının "User ID" değerini "sudo chown root sample" komutuyla "root" yapmalıyız. Daha sonra "sudo chmod u+s sample" komutunu "shell" programında çalıştırmalıyız ki "sample" programının "set-user-id" bayrağı "set" edilsin. Sıralama bu şekilde olmalıdır. Programı da normal kullanıcı olarak çalıştırmalıyız. #include #include #include #include #include void exit_sys(const char* msg); int main(void) { uid_t e_uid; e_uid = geteuid(); int n_groups; if((n_groups = getgroups(0, NULL)) == -1) exit_sys("getgroups"); printf("[%d] => ", n_groups); gid_t* sgids; if((sgids = (gid_t*)malloc(n_groups + 1 * sizeof(gid_t))) == NULL) { fprintf(stderr, "Not enough of memory!...\n"); exit(EXIT_FAILURE); } if(getgroups(n_groups, sgids) == -1) exit_sys("getgroups"); struct group* grp; for(int i = 0; i < n_groups; ++i) { if((grp = getgrgid(sgids[i])) == NULL) exit_sys("getgrgid"); if(i != 0) printf(", "); printf("%ju(%s) ", (uintmax_t)sgids[i], grp->gr_name); } printf("\n"); sgids[n_groups] = 0; if(setgroups(n_groups + 1, sgids) == -1) exit_sys("setgroups"); free(sgids); /* * Prosesin bütün ID değerleri, * "Real User ID" değerine çekildi. */ if(setuid(getuid()) == -1) exit_sys("setuid"); printf("Success!...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "fork" işlemi sırasında üst prosesin ek grupları, hayata getirilen alt prosese aktarılmaktadır. Örneğin, "shell" programı üzerinden başka bir program çalıştırdığımız zaman, "shell" programının uyguladığı "fork" neticesinde, "shell" programının ek grupları iş bu hayata yeni getirilen prosese aktarılmaktadır. "shell" programı da kendisine ait olan bu ek grupları, "login" programından almaktadır. Öte yandan POSIX dünyasında olmayan ama Linux dünyasında kullanılan bir fonksiyon daha vardır ki bu fonksiyon tipik olarak "login" programı tarafından kullanılmaktadır. İş bu fonksiyonumuzun ismi "initgroups". Aşağıdaki şekilde bir parametrik yapıya sahiptir; #include int initgroups(const char *user, gid_t group); Birinci parametresiyle de kullanıcının İSMİNİ almaktadır. Bu ismi "/etc/group" dosyası içerisinde tek tek aramaktadır. Daha sonra "setgroups" fonksiyonuyla prosesin bu ek gruplarını "set" eder. Tabii fonksiyonun bu işlemi yapabilmesi için "priviledged-user" bir proses olması gerekmektedir. Başarısızlık durumunda "-1", başarı durumunda "0" ile geri döner. Fonksiyon ikinci parametresiyle EKLENECEK yeni ID değerini alıyor. "login" programı tipik olarak bu argümana prosesin "Effective Group ID" değerini geçmektedir çünkü "/etc/group" dosyasında "Effective Group ID" değeri BULUNMAMAKTADIR. > Zaman paylaşımlı çalışmak: Sistemimizde birden fazla proses çalışıyor olabilir. Bir proses de birden fazla "thread" i çalıştırıyor da olabilir. Fakat bir proses ilk çalışmaya başladığında tek bir "thread" ile çalışmaya başlıyor ki bunun adı da "main-thread". Daha sonra biz kullanıcılar o proses için başka başka "thread" ler hayata getirebiliriz. Pekiyi zaman paylaşımlı çalışmak ne demek? Varsayalım ki elimizde tek çekirdekli bir adet CPU olsun. O anda da bir kaç tane prosesin çalıştığını, her bir proses de bünyesinde iki ya da üç tane "thread" çalıştırıyor olsun. İşte işletim sisteminin "Schelurer" isimli alt mekanizmaları her bir "thread" i peyderpey CPU'ya atıyor. CPU, bu "thread" i bir miktar çalıştırıyor. Daha sonra diğer "thread" atanıyor. İşte bu şekilde her bir "thread" in küçük zaman dilimlerinde çalıştırılmasına zaman paylaşımlı çalışma denmektedir. Aslında o anda sadece bir "thread" CPU tarafından çalıştırılıyor fakat dışarıdan bakıldığında sanki bütün prosesler aynı anda çalışıyormuş gibi gözükmektedir. Bir CPU'nun bir "thread" i çalıştırdığı o süreye de "time quanta" süresi denmektedir. Bu süre tamamen işletim sisteminin tasarımına bağlıdır. Linux sistemlerinde tipik süre 60 milisaniyedir. Windows sistemlerinde ise bu süre 20 milisaniye civarındadır. Burada iki "thread" arası geçiş zorla yaptırılmaktadır. Yani 60 milisaniye çalıştırılan bir "thread" zorla CPU'dan ayrılmakta, en son kaldığı yer kaydedilmekte ve aynı "thread" tekrardan CPU'ya atandığında kaldığı yerden devam etmektedir. Bir "thread" in zorla CPU'dan koparılmasına ve diğer "thread" in CPU'ya atanması işlemine de "task-switch" ya da "context-switch" denmektedir. Tabii bu işlem de belli bir zaman almaktadır. Burada çizelgelenen şey "thread" lerdir. Burada yeni gelen "thread" başka bir prosese ait de olabilir. Pekiyi bu "time quanta" süresinin uzun ya da kısa olması nelere sebebiyet vermektedir? Eğer bu süre uzun tutulursa, diğer "thread" ler sanki hiç çalışmıyormuş gibi görünecektir. Bu süre kısa tutulduğunda ise çok sık "task-switch" gerçekleşeceğinden dolayı birim zamanda yapılan iş miktarı azalacaktır. İşletim sistemi dünyasında birim zamanda yapılan iş miktarına ise "throughput" denmektedir. Pekiyi bu "time quanta" süresi nasıl hesaplanıyor? Tabii ki dışsal donanım kesmeleriyle. CPU'ya 1 milisaniyede bir "timer" devresinden kesme gelmekte ve "kernel" da bunları saymaktadır. 60 milisaniye olduğunda ise "task-switch" meydana geliyor. Linux sistemlerde bu "timer" devresi için "jiffy" kavramı da kullanılmaktadır. Bahsi geçilen bu "timer" devreler ise artık günümüzde CPU'nun içerisine yerleştirilmektedir. "thread" akışının "time quanta" sonrasında zorla CPU'dan kopartılmasına işletim sistemleri dünyasında "preemptive OS" denmektedir. Windows, Linux, macOS işletim sistemleri bu tür işletim sistemleridir. Bazı tip işletim sistemleri ise "cooperative multi-task" işletim sistemleri olarak geçmektedirler. Bu tip işletim sistemlerinde "thread" ler zorla CPU'dan koparılmazlar. "thread" ler kendi istekleriyle CPU'dan ayrılırlar. Dolayısıyla bir "thread" kendini bırakmazsa, diğer "thread" ler çalışma fırsatı bulamazlar. Bu senaryoya da "starvation" denmektedir. Fakat bu tip sistemler çok kısıtlı alanlarda kullanılmaktadır. Örneğin, palmOS ve Windows 3.X sistemleri böyledir. > Hatırlatıcı Notlar: >> "NGROUPS_MAX" gibi bazı sembolik sabitleri için "Runtime Increasable Values" denmektedir. /*================================================================================================================================*/ (34_04_03_2023) > Zaman paylaşımlı çalışmak (devam): Önceki derslerde anlatılan Zaman Paylaşımlı Çalışma methodu, aslında tek "core" a sahip işlemciler için geçerlidir. Pekiyi "multi-core" işlemciler için durum nasıldır? Böylesi işlemcilerde her bir "core" için ayrı bir "queueu" vardır ve her bir "core" yine zaman paylaşımlı çalışmaktadır. Örneğin, 4 "core" işlemciye sahip olduğumuzu düşünelim. Bu durumda her bir "core" için bir adet "queueu" olacağından yine dört adet "run-queueu" olacaktır fakat her bir "core" önceki derste anlatıldığı üzere kendi sırasındaki "thread" leri çalıştıracaktır. Dolayısıyla bilinmeyen bir t anında gerçekten de aynı anda çalışan dört adet "thread" den söz edebiliriz. Fakat "core" özelinde bakarsak, bilinmeyen bir t anında yine sadece bir adet "thread" söz konusudur. Linux işletim sistemi günümüzde bu sistemi kullanmaktadır fakat öncesinde bütün işlemciler için sadece bir adet "run-queueu" mevcuttu. Boşalan işlemcilere "thread" ler bu tekil "run-queueu" dan atanmaktaydı. Bu sistemin etkin olmadığı görüldüğü için kullanımından vazgeçilmiştir. İşletim sistemi bir "thread" i bir "run-queueu" ya atadıktan sonra onu bambaşka bir "run-queueu" ya atayadabilir. Çünkü işin başında müsait olan bir "core", daha sonra namüsait olabilir. Bu durumda müsait olan diğer "core" lar devreye girecektir. Windows ve macOS sistemleri de günümüzde Linux'un yöntemini kullanmaktadır. Çizelgeleme Algoritmasının detaylarına ise "thread" ler konusunda değinilecektir. Şu an için kavramsal olarak bir giriş yapmış olduk. Öte yandan klavyeden bir giriş bekleyen, disk üzerinde okuma/yazma işlemi yapacak olan, "socket" ten okuma yapacak olan "thread" ler için işletim sistemi nasıl bir yol izlemektedir? Bu tip "thread" ler bahsi geçen dışsal olayları yapacağı zaman, "run-queue" dan çıkartılır ve "wait-queueu" dediğimiz bekleme sırasına alınırlar. Böylelikle bu "thread" ler CPU'da çalışma zamanının boşa harcanmasına yol açmazlar. Ancak ilgili dışsal olay gerçekleştikten sonra tekrardan "run-queue" ya alınırlar. İşte bir "thread" in "run-queue" dan çıkartılıp "wait-queue" ya alınmasına ise ilgili "thread" in bloke olması denmektedir. "wait/waitpid" fonksiyonları da çağıran "thread" in bloke olmasını sağlarlar. Bütün bu işlemler o "thread" in "quanta" süresini ne kadar harcadığına bakmazlar. Uzun sürecek dışsal bir olay gerçekleşeceği vakit çalışma kuyruğundan alınır, bekleme kuyruğuna atanırlar. Burada işletim sistemi sürekli olarak çalışmamakta, donanımsal kesme geldikçe kontrolleri sağlamaktadır. Eee pekiyi bir "thread" in bekleme kuyruğundan alınıp, tekrardan çalışma kuyruğuna atanmasını sağlayan sistem nedir? Bu durumda "_exit" sistem fonksiyonu da kullanılmakta, donanımsal kesme de. Örneğin, "socket" ten bir okuma yaparken "network" kartından bir kesme geldiğinde ilgili "thread" uyandırılıyor veya bir proses "_exit" ile sonlandığında "wait/waitpid" çağrısı yapan "thread" tekrardan çalışma kuyruğuna alınıyor. Yine "sleep" fonksiyonunda da "timer" bir kesme uygulanmakta, sayaç ilgili değere ulaştığında "thread" tekrardan uyandırılmaktadır. Özetle; -> Çalışma kuyruğundan bekleme kuyruğuna geçiş için dışsal bir olayın vuku bulması gerekmektedir. -> Bekleme kuyruğundan tekrardan çalışma kuyruğuna geçiş için donanımsal kesme, "timer" kesmesi, "_exit" sistem fonksiyonunun çağrılması gibi olaylar vuku bulması gerekmektedir. Bekleme kuyrukları da her olay için ayrıdır. Örneğin, aygır sürücüler kendi bekleme kuyruklarını oluşturup bloke işlemlerini kendileri yapmaktadır. Paralel programlamada hangi "thread" in hangi "core" da çalıştırılacağını ayarlamamız gerekebilir. Bu duruma da "Processor Affinity" denmektedir. "thread" konusunda detaylarına değinilecektir. > "thread" hakkında genel bilgileri: "thread" ler "IO Yoğun" ve "CPU Yoğun" olmak üzere iki kategoriye ayrılmaktadır. "IO Yoğun" olanlar kendisine verilen "quanta" süresini çok az kullanıp hemen bloke olan "thread" lerdir. "CPU Yoğun" olanlar ise kendisine verilen "quanta" süresini büyük ölçüde kullanan "thread" lerdir. Dolayısıyla "CPU Yoğun" olanlar sistemi yavaşlatma potansiyeli taşırken, "IO Yoğun" olanlar böyle bir potansiyele sahip değildir. Çünkü birisi sürekli olarak işlemciyi çalıştırırken, diğeri hayatının çoğunu "wait-queue" da geçirmektedir. Fakat bu kavramlar insanlar tarafından uydurulmuş kavramlardır. İşletim sistemi açısından böyle kavramlar mevcut değildir. > Bir programın akışı sırasında iki nokta arası geçen zamanın ölçülmesi: * Örnek 1, Aşağıdaki programı "shell" vasıtasıyla "&" atomunu kullanarak "./sample &" biçiminde art arda çalıştırdığımız zaman göreceğiz ki ne kadar çok çalıştırırsak hesaplamanın süresi bi' o kadar da artacaktır. #include "stdio.h" #include "stdlib.h" #include int main(int argc, char** argv) { /* # INPUT # */ /* # OUTPUT # Total Work Hours: 19.100542 Total Duration: 20 */ clock_t clock_start, clock_stop; clock_start = clock(); for(long long i = 0; i < 10000000000; ++i) ; clock_stop = clock(); /* * Linux sistemlerde "thread" lerin bekleme zamanı dikkate alınmamaktadır eğer "clock" * ile ölçüm yapıyorsak. */ printf("Total Work Hours: %f\n", (double)(clock_stop - clock_start) / CLOCKS_PER_SEC); time_t time_start, time_stop; time_start = time(NULL); for(long long i = 0; i < 10000000000; ++i) ; time_stop = time(NULL); printf("Total Duration: %lld\n", (long long)(time_stop - time_start)); } * Örnek 2, Aşağıdaki programı "shell" üzerinden "time sample" biçiminde çalıştırırsak yine geçen zamanı hesaplamış olacağız. Eğer "time sample &" biçiminde peşpeşe çalıştırırsak yine hesap süresinin arttığını da göreceğiz. #include "stdio.h" #include "stdlib.h" #include int main(int argc, char** argv) { /* # INPUT # */ /* # OUTPUT # real 0m17,356s user 0m17,315s sys 0m0,012s */ for(long long i = 0; i < 10000000000; ++i) ; /* * Ekrandaki çıktılardan, * "real" olan gerçek duvar saatine göre geçen zamanı. 17 saniye. * "sys" olan "kernel" zamanı. 17 saniye * "user" olan ise "user" zamanı. 0 saniye. */ printf("Total Duration: %lld\n", (long long)(time_stop - time_start)); } > İşlemcilerin sayfalama mekanizması: Tıpkı işlemcilerdeki koruma mekanizması gibi, her işlemci de sayfalama mekanizmasına sahip değildir. Yani modern ve kapasiteli işlemcilerde bu mekanizma varken, mikrokontroller düzeyindekilerde mevcut değildir. Bu özelliği, işlemcilerin ilgili ayarları ile oynayarak, aktif veya pasif hale de getirebiliriz. Pekiyi nedir bu sayfalama mekanizması? Anımsayacağımız üzere RAM sıralı baytlardan oluşmaktadır. Bu sıralı baytları bir grup haline getirdiğimiz zaman ise bir sayfa oluşmaktadır. Genel olarak bir sayfa 4096 bayttan oluşmaktadır. Dolayısıyla RAM'in ilk 1-4095 baytlık kısmına sıfırıncı sayfa, ikinci 4096 - 8191'lık kısma birinci sayfa vs. denmektedir. Özetle fiziksel RAM'in, her biri 4K bayt büyüklüğündeki sayfalara ayrılması olayıdır. Pekiyi nedir bu sistem? Derleyiciler bir C kodunu derlerken sanki RAM'de sadece bu program çalışacakmış gibi bir kod üretirler. Yani o program, 4 GB'lik bir RAM'in 4. MB'den itibaren yüklenecekmiş gibi derlenir. Yani ilk 4 MB'lık kısım boş gibi düşünebiliriz. Fakat İşletim sistemi de bunu RAM'e aktarırken parçalara bölerek yükler. Örneğin, bir kısmını RAM'in bir bölgesine yüklerken diğer kısımlarını uygun diğer alanlara yükler fakat ardışık bir yükleme söz konusu değildir. Pekiyi programımız çalışmaya başladığında düzen nasıl sağlanmaktadır? İşte burada devreye Sayfa Tablosu kavramı girmektedir. Sayfa Tablosu iki sütundan oluşan bir tablodur. Sol taraftaki sütunda sayfa numaraları yazarken, sağ taraftaki sütunda ise bu numaralara karşılık gelen RAM'deki gerçek alanlardır. Dolayısıyla Sayfa Tablosunun sol sütunu ardışılken, sağ sütunu karışık vaziyettedir. İş bu nedenden dolayıdır ki programlarımızdaki adresler aslında Sanal Adreslerdir. CPU ise bu adrese erişmek için önce Sayfa Tablosuna bakmakta, oradan hareketle RAM'deki gerçek yerine erişmektedir. PROGRAMIMIZIN SANAL ADRESLERİ ARDIŞILDIR. Bilgisayarın donanımı da Sayfa Tablosuna uyumlu olarak tasarlandığı için sistem geneli bir yavaşlama söz konusu değildir. ÖZETLE; İşletim sistemi prosesin bölümlerini RAM'de farklı yerlere yüklüyor ve bunları ardışılmış gibi göstermek için Sayfa Tablosunu oluşturuyor. CPU'da bu tabloya bakarak çalıştığı için kesiksiz bir çalışma gerçekleşiyor. Sayfa Tablosunun temsili gösterimi aşağıdaki gibidir; Sayfa Tablosu Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 4562 11546 4563 95412 4564 12457 ... ... Şimdi işlemcimiz aşağıdaki kodu çalıştırmak istesin; MOV eax, [5C34782] Buradaki "5C34782" değeri hexadecimal bir değerdir ve sanal bir adres bilgisidir. İşlemci ilk olarak bu adresin kaçıncı sanal sayfada olduğunu hesaplar. Bunun için ilgili sayının 12 kez ötelenmesi gerekmektedir. Yani ilgili sayının sağdan ilk üç rakamını atarsak, geriye kalan değer bize kaçıncı sanal sayfada olduğunu söylemektedir. 5C34782 >> 12 = 5C34 Fakat elde edilen bu sayı yine hexadecimal bir sayıdır. Bunun decimal karşılığı ise 23604 nolu sanal sayfadır. Sayfa Tablomuz da aşağıdaki gibi olsun; Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 23602 47324 23603 52689 23604 29671 ... ... Bu tabloya göre 23604 sanal sayfasına karşılık gelen fiziksel RAM alanı 29671'dir. Pekiyi bu alandaki kaçıncı indise erişecektir? İşte yine devreye ilk başktaki adres bilgimiz girmektedir; "5C34782" Bu adres bilgisinin sağdan üç indisini attığımız zaman geriye kalan adresin sanal sayfa numarası olduğundan bahsetmiştik. İşte atılan bu üç indis ise "offset" numarasıdır. Yani "782". Bu da yine "hexadecimal" dir. Karşılığı ise "1922" değeridir. Zaten "5C34" adresi fiziksel RAM'de "52689" e yönlendirilmişti. "1922" de "offset" değeridir. Her bir sayfa da 4096 bayttan meydana gelmektedir. O vakit gerçek adres değeri "52689 * 4096 + 1922" biçimindedir. Örneğin, programımız şöyle bir fonksiyon çağırmış olsun; foo(); Derleyici de buna karşılık şöyle bir kod üretmiş olsun; CALL 6F14678 Burada 6F14678, "foo" fonksiyonunun sanal adresidir. Program çalışırken işlemci bu adresi ikiye böler. -> 6F14: Sanal Sayfa No(hexadecimal) -> 678: Offset(hexadecimal) Daha sonra sayfa tablosuna bakar ve 6F14'e karşılık gelen fiziksel RAM alanına bakar. Bu alanın da 7C45(hexadecimal) olduğunu varsayalım. O zaman işlemcinin erişeceği fiziksel adresi aşağıdaki gibidir; 7C45 * 4096 + 678 Buraya kadar öğrendiklerimiz şu şekilde; programımız RAM'e yüklenirken parçalara ayrılarak, RAM'in farklı alanlarına yükleniyor. Bu alanların tipik büyüklüğü 4096 bayt biçimindedir. İşletim sistemi de bu farklı alanları koordine edebilmek adına bir sayfa tablosu oluşturuyor. Sol sütunda sanal sayfa numaralarını, sağ tarafta ise bu numaralara karşılık gelen RAM'deki alanları yazmaktadır. Pekiyi sayfa tablosu kim tarafından oluşturuluyor? Tabii ki ilgili program belleğe yüklenirken "exec" fonksiyonları vasıtasıyla oluşturulmaktadır. Buradan hareketle diyebiliriz ki her proses kendine has bir sayfa tablosuna sahiptir. İşlemcinin içindeki bir "register" ise o anda "quanta" süresi işleyen "thread" in sayfa tablosunu göstermektedir. Eğer "task-switch" sonrasında başka bir "thread" atanırsa, artık yeni atanan "thread" in sayfa tablosu da atanacaktır. Dolayısıyla ilgili "register" artık yeni geleninkini gösterecektir. Böylece şöyle bir şey oluyor; farklı proseslerdeki aynı sanal sayfa numaraları, aslında aynı fiziksel adresleri GÖSTERMEMEKTEDİR. İşlemcilerin sayfalama mekanizması sayesinde prosesler birbirlerinin bellek alanlarına erişmesi engellenmiştir. > Hatırlatıcı Notlar: >> O(1) Scheduler: Eski Linux sistemlerinde tek bir "run-queueu" olduğundan bahsetmiştik. Boşa çıkan "core" a bir "thread" atanması için bu "run-queueu" nun kontrol edilmesi gerekmektedir çünkü sıradaki ilk "thread" i atarsak verimsiz bir çalışma yapabiliriz. Bunun da yegane sebebi bazı "thread" lerin evvelki "core" da çalışmaya devam etmesinin ya da önceliği yüksek olan "thread" lerin önce alınmasının getireceği faydadır. İşte hangi "thread" in boş olan ilk "core" a atanacağını bulan algoritmanın karmaşıklığı da O(1) karmaşıklığıdır. Herhangi bir döngü olmaksızın en uygun "thread" seçilmektedir. Detaylı bilgi için: https://en.wikipedia.org/wiki/O(1)_scheduler >> O(n) Scheduler: "n" kadar "thread" içeren "run-queue" dan en uygun "thread" in bulunması için bir döngü yardımıyla sıranın baştan sonra dolaşılmasıdır. Buradaki "n" ise "run-queue" içerisindeki eleman sayısını işaret etmektedir. Bu tip zamanlayıcıylarda eleman sayısı arttıkça geçecek zaman da "linear" olarak değişecektir. Detaylı bilgi için: https://en.wikipedia.org/wiki/O(n)_scheduler >> Bir "thread" in "quanta" süresinden sonra tekrardan aynı "core" de çalışmasının avantajları vardır çünkü o "thread" e ilişkin bir takım bilgiler o "core" a kopyalanmaktadır. >> Bir "thread" hayatı boyunca tipik 3 farklı durumdadır. Bunlardan birisi "Run", diğeri "Ready" ve sonuncusu "Wait". "Run" durumunda olan "thread" ler "run-queue" ya alınmış ve "quanta" süresi işlemekte olan "thread" lerdir. "quanta" süresinden sonra durumu "Ready" olacaktır ve bir sonraki "quanta" süresini bekleyecektir. Eğer "Run" durumundayken dışsal bir olay gerçekleşirse durumu artık "Wait" olacaktır. Dışsal olayın tamamlanmasından sonra yeniden "Ready" konumuna geçip bir sonraki "quanta" süresini bekleyecektir. En sonunda ise "thread" sonlanmaktadır. >> "shell" programından "sample" programını aşağıdaki haliyle çalıştıralım; ./sample & Artık "shell" programı "sample" programını "wait/waitpid" fonksiyonları ile beklemeyecektir. Bu biçimdeki bir çalıştırma sonucunda bizlere bir numara verilmektedir. Bu numarayı "fd" komutuna geçersek, artık o proses için "wait/waitpid" yapılacaktır. >> Bir program içerisindeki adresler gerçek adres değillerdir. Sanal adreslerdir. >> Donanımsal olarak sayfa tablosuna başvuru işini hızlandırmak için işlemciler bünyelerinde "Translation Lookaside Buffer" denilen bir "cache" sistemi barındırmaktadır. >> "cache": Bir yere erişimi azaltmak için onun bir bölümünün daha hızlı bir yere aktarılması demektir. Örneğin, bizler disk üzerinde bir yere sık sık erişmek isteyelim. Bu bölümü bizler belleğe aktarıyoruz. Lüzum görüldüğünde önce bellekteki bu alana bakıyoruz. Bulamazsak bizzat diske bakıyoruz. İşte "cache" demek budur. /*================================================================================================================================*/ (35_05_03_2023) > İşlemcilerin sayfalama mekanizması (devam): Pekiyi 32-bit sistemlerde Sayfa Tablosunun büyüklüğü ne kadar olur? Anımsayacağımız üzere bellek 4096 baytlık alanlara bölünmüştü ve 32-bit bir sistemde de maksimum RAM 4GB büyüklüğünde olacaktır. Dolayısıyla bizler en fazla 1MB adedince sayfa sayısına sahip olabiliriz. Intel mimarilerde sayfa tablosunun her bir satırı 4 bayt'lık yer kaplamaktadır. Dolayısıyla her proses bu mekanizma için 4MB yer kaplamaktadır. İşlemcileri tasarlayanlar bu büyüklüğü azaltmak için bir takım yöntemlere başvurmaktadır. Örneğin, sanal adresleri bizler ikiye ayırmıştık. Bir kısım sayfa tablosundaki indisi, bir kısım da bu indis içerisindeki konum bilgisini taşımaktadır. İşte Intel sanal adresleri üç parçaya ayırmıştır. Öte yandan dörde ayıran mimariler de bulunmaktadır. Fakat bu konunun detayları kurs kapsamında değildir. Derneğin "80X86 ve ARM Sembolik Makina Dilleri" kursunda ele alınmaktadır. Bizler kurs boyunca sanal adresleri ikiye ayıracağız. Pekiyi durum 64-bit sistemlerde nasıldır? Böyle sistemlerde maksimum RAM kapasitesi 2^64 bayt büyüklüğündedir. Dolayısıyla proseslerin sayfa tabloları da aşırı büyük yer kaplayacaktır. İşlemcileri tasarlayanlar sanal adresi dört parçaya ayırmışlardır. Fakat bu konu da kurs kapsamında anlatılmayacaktır. Bütün bu anlatılanlardan sonra bizler neden böyle bir mekanizmaya ihtiyaç duyalım? Anımsanacağınız üzere programlar derlenirken bellekte sadece kendileri çalışacakmış gibi derlenirler. Bu da demektir ki bellekte ardışıl bir biçimde konumlanmaktalar. Fakat bu programı çalıştırdığımız zaman bellekte ardışıl bir biçimde yer KAPLAMIYORLAR. Bunun iki sebebi vardır. Bunlardan birisi "Fragmantation" dediğimiz olgu, diğeri ise "Virtual Memory" dediğimiz sanal bellek kavramıdır. >> "Fragmantation" dediğimiz olgu şudur; proseslerin belleğe ardışıl bir biçimde yüklendiğini ve işleri bittiğinde de bellekten silindiğini varsayalım. Bir zaman sonra bellek üzerinde küçük alanlar oluşacaktır. Çünkü her proses bellekte aynı büyüklükte alan kaplamamaktadır. Bu küçük alanlara da herhangi bir proses sığmamakta ve totalde büyük yer kaplayacaklardır. İşte bu problemi minimize eden şey proseslerin parçalar halinde belleğe aktarılmasıdır. Bellek de zaten bloklara ayrılmış vaziyettedir. Prosesin hangi parçasının belleğin hangi parçacığına yüklendiğinin de bilgisini bir şekilde kaydedilir. Bu yöntem belleklerde olduğu gibi disk yönetiminde de kullanılmaktadır. Fakat bu yöntem de "Fragmantation" olgusunu tamamiyle engellememektedir. Örneğin, prosesimiz eşit 5 parçaya ayrılmış olsun. Bellek de yine eşit parçalı vaziyette. Program belleğe yüklendiğinde, programın ilk dört parçası bellekteki parçaların tamamını kapladığını fakat son beşinci parçanın bellekteki parçanın tamamını kaplamadığını varsayalım. Bu durumda bellekteki bu son parçanın geldiği alanda "Internal Fragmantation" oluşacaktır. Fakat bu tip bir parçalanmanın önüne geçmek mümkün değildir. >>> "Internal Fragmantation": Elimizde 22 kiloluk elma olsun. Depomuzda ise 6 kasalık yer kalmış olsun ve her bir kasa 4 kilo alsın. Bu durumda depodaki 5 kasalık alan ağzına kadar dolu olacaktır. Altıncı kasanın ise sadece yarısı dolmuş olacaktır. İşte bu boş kalan iki kiloluk alan "Internal Fragmantation" denir. Yani her nesnenin son sayfasında kalan boşluk kastedilen şeydir. >> Sanal Bellek Kullanımı: Öyle bir tekniktir ki bir programın tamamını ilk başta belleğe yüklemek yerine sadece belli kısımlarını yüklemek ve gerektiğinde de diğer kısımlarını diskten yüklemek için geliştirilmiştir. Böyle bir mekanizmanın işlevsel olması için de mutlaka sayfalama mekanizmasının olması gerekmektedir. Pekiyi bu tekniği kullanarak "n" tane prosesi belleğe yüklediğimizi ve belleğin de dolduğu düşünelim. Unutmayalım ki bellek de sayfalardan oluşmakta ve prosesin her bir parçası bir sayfaya yüklenmekte. Bu durumda işletim sistemi bir karar veriyor; bellekteki seçtiği bir sayfada bulunan prosesin o parçasını bellekten çıkarıyor ve yerine yeni eklenecek prosesin parçasını koyuyor. Prosesin bellekte olmayan bir parçasının diskten belleğe çekilmesine "swap-in" denir. Bunun tam tersi olaya ise "swap-out" denir. Pekiyi işletim sistemi bellekteki hangi sayfanın boşaltılacağını nasıl belirler? "page swapping algorithms" konusu bu soruya yanıt aramaktadır. Fakat, gelecekte kullanılma olasılığı en düşük olan sayfanın bellekten atılması en iyi örnektir. Fakat bu "swap" işlemi yavaş bir işlem olmasından dolayı sistem genelinde en önemli zayıflatıcı etkilerden birini oluşturmaktadır. Pekiyi bu yavaşlığı nasıl egale edebiliriz? Akla ilk gelen şey bellek büyüklüğünü arttırmak, yani RAM'i büyütmek. Fakat bu da maliyeti beraberinde getirmektedir. İkinci yöntem hard disk yerine SSD kullanımı olacaktır. O programın kodunun küçültülmesi de bir yöntemdir. Bir diğer yöntem ise "page swapping algorithms" tekniklerinden en iyisini kullanmaktır. Bütün bunların yanı sıra şöyle bir senaryo düşünelim; bellekte hiç boş sayfa yok fakat diskten yeni bir sayfa belleğe aktarılacak. Bu durumda bir sayfa bellekten atılmak için seçilecek. İşte bu seçim sırasında o sayfada bir takım değişiklikler oluştuysa durum ne olacaktır? Bu durumda işletim sistemi bu güncellenmiş fakat çıkartılacak sayfayı "swap file" ya da "page file" ismindeki dosyalara yazacaktır. Linux sistemlerinde istediğimiz bir dosyayı bu amaç için kullandırtabiliriz. Eğer kullanılacak bu "swap file" dosyaları da dolarsa artık yapacak bir şey kalmamaktadır. Sistemin sanal bellek limitine ulaştığımız kabul edilir. Artık sadece yeni "swap file" dosyaları sisteme eklenmelidir. Son olarak belirtmekte fayda var ki bu "swap file" dosyalar çalışma zamanı ile ilgilidir. Bilgisayarı kapattığımız zaman buradaki veriler de silinecektir. Pekiyi işlemci sanal sayfa tablosuna baktığında, sayfa numarasına ilişkin bellekte bir yer bulamazsa ne olur? Aşağıdaki biçimde bir sayfa tablomuz olsun; Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 23602 47324 23603 - 23604 29671 ... ... İşte işlemcimiz 23603 numaralı indise baktığında herhangi bir RAM alanının atanmadığı görecektir. Bu durumda "page fault" isimli bir kesme oluşur. Bu noktada işletim sisteminin "page fault handler" isimli aracı devreye giriyor. Bu araç ise yukarıda bahsedilen "swap" işlemlerini gerçekleştiriyor ve tablonun o indisine karşılık bellekte bir alan atıyor. Artık işlemci sayfa tablosu aşağıdaki gibi olacaktır; Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 23602 47324 23603 32145 23604 29671 ... ... Artık işlemci 23603 numaralı indise eriştiğinde bellekteki yerine de erişebilecek. Toparlayacak olursak; işletim sistemi programı belleğe yüklerken sadece belirli kısımlarını belleğe aktarıyor, lüzum gördüğünde diğer kısımlarını diskten çekiyor. Bu noktada devreye "Minimum Working Set" değişkeni girmektedir. Belleğe aktarılan kısımlar için sayfa tablosu dolduruluyor, aktarılmayan kısımlar için "-" işareti koyuluyor. Eğer işlemci prosesin belleğe aktarılmamış sayfalarına erişmek isterse ki bu durumda sayfa tablosundaki bellek sütununun ilgili satırında "-" işareti olacak, "page fault handler" isimli araç devreye giriyor ve diskten ilgili alanı belleğe aktarıyor, yani "swap" gerçekleşiyor ve sayfa tablosunun bellek sütunun ilgili satırı güncelleniyor. "page fault" mekanizması sırasında da ilk olarak işlemcinin erişmek istediği alanın legal bir alan olup olmadığını kontrol etmektedir. Yani o alanın "swap" dosyasında mı yoksa programın bellek alanında mı vs. İşte illegal bir alana erişmek istiyorsa işlemci, "page fault" başarısız olmakta ve proses tümüyle sonlanmaktadır. Eğer rastgele bir adrese erişmek istesek, bu durum "page fault" mekanizması tarafından sonlandırılmaktadır. Öte yandan uygulama programıcısı olarak bu mekanizmanın daha az ya da daha çok devreye girmesini SAĞLATAMAYIZ. Bütün süreç işletim sistemi tarafından yürütülmektedir. Bunun haricinde RAM'i büyüterek, iyi bir "Page Swapping Algo." seçerek, daha fazla "Minimum Working Set" ayarlayarak "page fault" mekanizmasının daha az çalışmasını sağlatabiliriz. Pekiyi işletim sisteminin bellek yönetimi kısmını yazanlar hangi bilgileri tutmak zorundadırlar? İşte işletim sistemleri, tipik olarak bu mekanizmalar için aşağıdaki bilgileri tutmak zorundadırlar: -> Tüm fiziksel RAM'deki bütün sayfaların boş olup olmadığına ilişkin tablo. -> Bir fiziksel sayfa boş değilse hangi proses tarafından kullanıldığı bilgisi. -> "swap" dosyalarının yerleri ve organizasyonu. -> Hangi proseslerin hangi sayfalarının o anda fiziksel RAM'deki hangi fiziksel sayfalarını kullanmakta. -> ... Görüleceği üzere bellek yönetimi bir işletim sisteminin en önemli ve en zor yazılan alt sistemlerinden biridir. Pekiyi bütün bu mekanizma içerisinde işletim sistemi nerededir? Aslında sanal belleğin bir kısmı biz kullanıcılar için, bir kısmı ise "kernel" kodları için ayrılmıştır. Dolayısıyla sayfa tablosunun da bir kısmı biz kullanıcılar, bir kısmı "kernel" kodları için ayrılmıştır. Fakat sayfa tablosunun "kernel" kodları için ayrıldığı bölüm bütün tablolarda aynıdır. "task-switch" esnasında bile o kodlar korunurlar. Peki bizler bir program içerisinde yüksek miktarda dinamik tahsisat yaptığımızda ne olur? Linux sistemlerinde "malloc" fonksiyonu "brk" ya da "sbrk" denilen sistem fonksiyonunu çağırabilmektedir. Ancak arka planda sanal bellek mekanizması açısından şunlar gerçekleşmektedir; -> İşletim sistemi "malloc" ile tahsis edilen alanı sayfa tablosunda oluşturur. Bu aşamada da tahsis edilen alanın toplam "swap" dosyasının büyüklüğünü geçmediğini garanti etmeye çalışır. Çünkü günün sonunda tahsis edilen alan "swap" dosyası içerisinde de bulunacaktır. -> İşletim sistemi, "swap" dosyasının boyutu yeterliyse, tahsisatı kabul etmektedir. Fakat sistemden sisteme değişmekle birlikte, "swap" dosyasında tahsisat yapılabilir ya da yapılmayabilir. Yapılması durumunda "swap" dosyası ciddi manada dolacağı için başka proses aynı işlemi de yapamayabilir. Fakat Linux sistemleri dinamik alan kullanılmaya başlandığı vakit "swap" dosyasında tahsilat yapmaktadır. Aşağıdaki iki kodu çalıştırarak farkını gözlemleyiniz: * Örnek 1, Dinamik Alan henüz kullanılmamaktadır. #include "stdio.h" #include "stdlib.h" int main(int argc, char** argv) { /* # OUTPUT # not enough memory!... */ /* * 5GB büyüklüğünde bir yer ayrılmaya çalışıyor. */ char* pc; pc = (char*) malloc(5000000000); if(pc == NULL) goto NOT_ENOUGH_MEMORY; NOT_ENOUGH_MEMORY: fprintf(stderr, "not enough memory!...\n"); exit(EXIT_FAILURE); } * Örnek 2, Dinamik Alan KULLANILMIŞTIR. #include "stdio.h" #include "stdlib.h" int main(int argc, char** argv) { /* # OUTPUT # not enough memory!... */ /* * 5GB büyüklüğünde bir yer ayrılmaya çalışıyor. */ char* pc; pc = (char*) malloc(5000000000); if(pc == NULL) goto NOT_ENOUGH_MEMORY; for(long long i = 0; i < 5000000000; ++i) pc[i] = 0; goto ENOUGH_MEMORY; NOT_ENOUGH_MEMORY: fprintf(stderr, "not enough memory!...\n"); exit(EXIT_FAILURE); ENOUGH_MEMORY: fprintf(stdout, "Ok!...\n"); exit(EXIT_SUCCESS); } Bu noktada "swap" dosyasının yetersiz gelmesi durumunda "swap" dosyasının büyüklüğünü arttırmamız gerekmektedir. Dolayısıyla yeterli büyüklükteki "swap" dosyası ile daha büyük programları da çalıştırabiliriz. > Prosesler Arası Haberleşme Yöntemlerinden Paylaşılan Bellek Alanları ("Shared Memory"): Aşağıdaki şekilde iki tane sayfa tablomuz olsun: Proses - I Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 95134 45321 ... ... Proses - II Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 32148 45321 ... ... Bu iki sayfa tablosundaki "95134" ve "32148" sanal sayfa numaraları fiziksel RAM'de aynı alanı göstermektedir. Dolayısıyla bu iki proses bellekteki o alanı paylaşımlı olarak kullanmaktadır ve bu iki proses bu bellek alanını kullanarak birbiriyle haberleşmektedir. Bu yöntem, prosesler arası haberleşme yöntemlerinden bir tanesidir. Fakat bu yaklaşım normalde sergilenen bir yaklaşım değildir. Normal şartlarda prosesler arasında tam bir izolasyon söz konusudur. > Hatırlatıcı Notlar: >> Kaan Arslan'ın "İntel İşlemcileri Korumalı Mod Yazılım Mimarisi" isimli kitabı, tavsiye edilir. >> Intel işlemciler "core" mantığını şöyle implemente etmektedir: "dual-core" sisteme geçmeden evvel fiziksel sadece bir tane çekirdek vardı. Fakat bu çekirdeğin içeriğindeki bazı parçalar çiftlendi, iki tane oldu. Dolayısıyla aslında fiziken bir tane çekirdek var fakat işlevsellik açısından iki tane gibi. Bu sisteme de "Hyper Threading" denmekte. İşte "dual-core" a geçince artık fiziken iki tane çekirdek var fakat işlevsellik açısından dört tane. Bu yüzdendir ki işlemcilerin dökümanında yazan çekirdek sayısı gerçekte var olan işlemci sayısını, iş parçacığı sayısı ise işlevsellik açısından işlemci sayısını temsil etmektedir. Örneğin, elimizde i7-7700 model bir işlemcimiz olsun. Bu işlemci çekirdek sayısı olarak 4, iş parçacığı sayısı olarak 8 adet çekirdek içermektedir. Yani aslında 4 adet fiziki çekirdek var fakat çakma çekirdek sayısı 8. İşletim sistemi burada 8 çekirdek varmış gibi davranmaktadır. >> "Minimum Working Set" : İşletim sisteminin bir prosesi belleğe yüklerken baştan yükleyeceği en az sayfa sayısıdır. Bu değer önceden belirlenmiştir. Hiçbir programın bu değerden daha az sayıdaki sayfası bellekte bulunamaz. >> "Page Swapping Algo." : https://en.wikipedia.org/wiki/Page_replacement_algorithm >> Linux sistemlerinde bir dosyanın "swap file" olarak kullanmak için: https://acloudguru.com/hands-on-labs/creating-swap-space-on-a-linux-system?utm_source=google&utm_medium=paid-search&utm_campaign=cloud-transformation&utm_term=ssi-global-acg-core-dsa&utm_content=free-trial&gclid=Cj0KCQjwwtWgBhDhARIsAEMcxeBefkHI234CiO9EAcml2sjJ5Dayyo0A7sjVH_xbfw5WFSkfBtFwgRYaApBIEALw_wcB >>> Bu tip sistemlerde o anki toplam "swap" alanlarına "/proc/swaps" dosyasından bakabilir ya da "swapon -s" komutunu "shell" programından çalıştırarak bakabiliriz. >> Windows sistemlerde "swap" dosyasının karşılığı Performans Seçenekleri, Sanal Bellek sekmesindedir. /*================================================================================================================================*/ (36_11_03_2023) > İşlemcilerin sayfalama mekanizması (devam): Her bir sayfa tablosu aynı zamanda işletim sisteminin de kodlarını barındırmaktadır ve bu kısım her bir sayfa tablosunda birbirinin aynısıdır. Dolayısıyla sayfa tablosunun bu kısımları fiziksel RAM'deki aynı bölgeyi göstermektedir. Pekiyi prosesler sayfa tablosunun bu kısmına normal yollar ile erişebilirler mi yoksa bir koruma mekanizması da var mıdır? Tabii normal prosesler sayfa tablosunun bu kısmına erişemezler. Çünkü sayfa tablosunun o kısmında bulunan sanal sayfa numaralarına tekabül eden fiziksel RAM sayfaları, "kernel mode" olarak nitelendirilmiştir. Dolayısıyla prosesimizin "user mode" proses değil, "kernel mode" proses olması gerekmektedir. Esasında fiziksel sayfalar "kernel mode" ve "user mode" olarak nitelendirilmiştir. "user mode" prosesler sadece "user mode" olan fiziksel sayfalara erişebilirken, "kernel mode" olan prosesler her iki "mode" daki sayfalara da erişebilmektedir. Eğer "user mode" bir proses "kernel mode" bir fiziksel sayfaya erişmeye çalışırsa işlemci "fault" oluşturur ve işletim sistemi ilgili prosesi sonlandırır. Öte yandan "user mode" sayfalar ekstra bir etikete daha sahiptir. Bu etiketler kabaca şu şekildedir; "read only", "read/write" ve "execute". Bu etiketler yine "user mode" prosesleri ilgilendirmektedir. Bu etiketler bir nevi erişim biçimi olarak da görülebilir. Örneğin, "normal mode" bir proses "normal mode" ve "read only" olarak nitelendirilmiş fiziksel sayfaya yazma yapmak istesin. İşlemci yine içsel kesme("page fault") oluşturacak ve işletim sistemi prosesi sonlandıracaktır. Öte yandan pek çok işlemci ailesinde, bir kodun bir fiziksel sayfada çalışabilmesi için o kodun "executable" niteliğine sahip fiziksel bir sayfada bulunması gerekmektedir. Aksi halde yine "page fault" olacaktır. * Örnek 1, Aşağıdaki programda "read only" olan fiziksel sayfada "write" işlemi yapılmaya çalışılmıştır. #include "stdio.h" int main(int argc, char** argv) { /* # OUTPUT # */ char* str = "ahmo"; /* * C standartlarına göre "string" leri "update" etmek * Tanımsız Davranıştır. Öte yandan "strin" ler RAM'e * aktarılırken "exec" fonksiyonları tarafından * "read only" bir sayfaya yükleniyorlar. Dolayısıyla * bizler aslında "read only" niteliğindeki bir sayfaya * "write" yapmaya çalışıyoruz ve prosesimiz işletim * sistemi tarafından sonlandırılmaktadır. */ *str = 'a'; puts(str); return 0; } * Örnek 2, Bazı derleyiciler, özellikle C++ derleyicileri, "const" olan ve global isim alanındaki değişkenleri de yine fiziksel RAM'in "read only" olarak nitelendirilmiş kısımlarına aktarabilirler. Tabii bu bir zorunluluk değildir... #include "stdio.h" const int x = 100; int main(int argc, char** argv) { /* # OUTPUT # */ /* * Bu dönüşüm ile "const" özelliği düşmüştür. Artık derleme * zamanında bir hata almayacağız. Fakat nesnemiz özü itibariyle * hala "const" bir nesnedir. */ int* ptr = (int*)&x; /* * Fakat "const" ve global isim alanındaki nesneler belleğe * aktarılırken "read only" olan fiziksel sayfalara aktarılabilirler. * Dolayısıyla bizler o alanlarda "write" işlemi yapacağımız * için yine "page fault" oluşacaktır. */ *ptr = 200; printf("%d\n", *ptr); return 0; } * Örnek 3, #include "stdio.h" int main(int argc, char** argv) { /* # OUTPUT # 200 */ const int x = 100; /* * Bu dönüşüm ile "const" özelliği düşmüştür. Artık derleme * zamanında bir hata almayacağız. */ int* ptr = (int*)&x; /* * "const" ve yerel isim alanındaki nesneler "stack" içerisinde * hayata geldiklerinden, bu alanların "read only" bir fiziksel * sayfaya aktarılmaları mümkün değildir. Dolayısıyla aşağıda * gerçektende bir güncelleme yapılmıştır. */ *ptr = 200; printf("%d\n", *ptr); return 0; } Pekiyi bizler bir programı iki defa çalıştırdığımız zaman sayfa tabloları açısından durum nasıl olacaktır? Bu yöndeki beklentimiz iki ayrı sayfa tablosunun hayata gelmesi ve her bir tablonun sağ tarafındaki fiziksel RAM'deki alanları gösteren kısımların da birbirinden farklı olması yönündedir. Fakat işletim sistemi bu noktada bir optimizasyon yapmakta ve her iki prosesin sayfa tablolarının sağ tarafını birbiriyle aynı yapmaktadır. Dolayısıyla iki prosesin sanal sayfaları aslında RAM'de aynı yeri göstermektedir. Ek olarak sayfa tablolarındaki fiziksel sayfaları da "read only" olarak işaretlemektedir. Eğer proseslerden birisi bu alanlardan birisine yazma yapmak isterse "page fault" oluşur ve yeni bir sayfa oluşturulup ilgili tablo da güncellenir. Fakat diğer sayfalar aynı kalır. Böylelikle yazma yapılmayana kadar aslında iki tablo da bellekte aynı alanları gösterir. Bu mekanizmaya da "Copy on Right" denir. > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication): İki ya da daha fazla prosesin birbirine bayt göndermesi ya da alması konusu prosesler arası haberleşmeyi kapsamaktadır. Böyle bir haberleşmenin sağlanabilmesi için özel mekanizmalar kullanılmaktadır. Genellikle iki ana grupta ele alınırlar: -> Aynı makinedeki prosesler arasında haberleşme. Burada işletim sisteminin sağladığı özel yöntemler kullanılmaktadır. -> Farklı makinelerdeki prosesler arası haberleşme. Buradaki makineler bir şekilde birbirlerine bağlanmıştır. Farklı makineler farklı dünyalara da ait olabilirler. Dolayısıyla bir takım kuralları belirlemiş olmaları gerekiyor. İşte bu kurallara da protokol denmektedir. IP protokol ailesini kursun ilerleyen derslerinde göreceğiz. Öte yandan farklı makinelerde uygulanan haberleşme yöntemini aynı makinede çalışan prosesler için de uygulayabiliriz fakat performans düşecektir. "client-server" sistemlerde, Dağıtık Sistemlerde(Distributed Systems) vb. sistemlerde prosesler arası haberleşme yöntemleri bariz bir şekilde kullanılmaktadır. Dağıtık Sistemler, bir işin bir grup bilgisayara yaptırıldığı temalara denir. Programın bir kısmının bir makinede, bir kısmının bir makinede çalıştırılma biçimidir. Dağıtık Sistemler genelde "cloud" sistemlerde kullanılır. >> Aynı makinede koşan prosesler arası haberleşme yöntemleri: Pek çok işletim sistemi şu haberleşme yöntemlerini bir şekilde ortak kullanmaktadır: Boru Haberleşmesi, Paylaşılan Bellek Alanları, Mesaj Kuyrukları. Bu yöntemler benzer biçimde implemente edilmişlerdir. Bunların dışında Windows'a özgü, Linux'a özgü yöntemler de kullanılmıştır. Tabii kültürler arası farklılık yine görülmektedir. >>> Boru Haberleşmeleri: İki ana gruba ayrılırlar. Bunlar İsimsiz("Unnamed/Anonymus") ve İsimli("Named/FIFO") Boru Haberleşmeleridir. İsimsiz Boru Haberleşmesi "parent" proses ve "child" proses arasındayken, İsimli Boru Haberleşmesi herhangi iki proses arasında kullanılır. Pekiyi nedir bu boru? Boru aslında bir ortam olup, iki proses arasındaki bir kuyruk sistemidir. Hangi sırada baytlar yazılmışsa, o sırada okunurlar. Yani FIFO biçimindedir. Arka planda tabii fiziksel bir sayfa vardır. Boru Haberleşmesi iki ana ayak üzerine kuruludur. Bir ayak boru sisteminin oluşturulması, diğer ayak ise yazma ve okuma işlemlerinde senkronizasyonun sağlanması üzerinedir. Çünkü okuma ve yazma yaparken "ezme" diye tabir edilen "overwrite" oluşmaması gerekmektedir. Açık ara en çok kullanılan yöntem, boru haberleşmesi yöntemidir. Boruya yazmak ve borudan okumak için oluşturulan özel fonksiyonlar yoktur. Dosya işlemlerinde kullanılan "read" ve "write" fonksiyonları kullanılmaktadır. Özetle borular birer dosya gibi ele alınmaktadır. Boruların belli bir uzunlukları vardır. Varsayılan uzunluk 4096 bayt iken, günümüzde Linux sistemlerinde 65536 bayta yükseltmiştir. Bir sayfa uzunluğunu 4096 bayt ise 65536 bayt ise 16 sayfaya denk gelmektedir. UNIX/Linux sistemlerde borular tek yönlü, Windows sistemlerde ise iki yönlü olarak kullanılmaktadır. Pekiyi boruların tek yönlü olması ne demektir? Baştan bizler proseslere roller atıyoruz; hangisinin yazacağını, hangisinin okuyacağına dair. Eğer yazan taraf okuma yaparsa, kendi yazdığını okuyacaktır. Fakat iki yönlü borularda yazan taraf okuma da yapabilir. Dolayısıyla karşılıklı okuma yapmak için iki boru kullanmamız gerekiyor. >>>> Boruya yazma işlemi: Daha önce de belirtildiği gibi "write" fonksiyonu kullanılır ve argüman olarak geçilen "fd" değişkeni borulara ilişkindir. "write" fonksiyonu yazabildiği kadar karakter ile değil, yazması gereken bütün karakterleri yazdıktan sonra geri döner. Pekiyi yazma işlemi yaparken boru dolarsa ne olur? Bu durumda ilgili boru, karşı taraftaki "read" yapana kadar, "write" fonksiyonunca bloke edilir. "read" yapılıp da ilgili boruda yer açılırsa bloke kaldırılır. Boruların kapasitesi de Linux sistemlerde 65K iken bazı sistemlerde 4K bayt büyüklüğündedir. Fakat FARKLI PROSESLER İLE AYNI boruya yazma yaparken, proseslerden birisinin tek kalemde yazacağı uzunluk "PIPE_BUF" sembolik sabitinden daha fazlaysa, İÇ İÇE GEÇME MEYDANA GELEBİLİR. Dolayısıyla bir boruya yazarken "PIPE_BUF" sembolik sabitinden daha küçük boyutlarda yazma yapmalıyız. Öte yandan "write" fonksiyonu ile boruya yazma yaparken bir sinyal alırsak, "write" fonksiyonu "-1" ile geri döner ve boruya hiç bir şey yazmaz, başarısız olur. Dolayısıyla "errno" değişkeni de "EINTR" değerini alır. Eskiden "Partial Write" yapmaktaydı yani yazabildiği kadarını yazıyordu. Fakat artık hiç yazmadan, direkt olarak "-1" ile geri dönmektedir. Benzer biçimde boruda yeteri kadar yer yoksa, bu durumda ilgili "thread" blokede bekleyecektir, ve o anda bir sinyal gelirse yine başarısız olup "-1" ile geri dönecektir. Yine boruya BİR ŞEY YAZMAYACAKTIR. Özetle; blokeli boru yazma işlemlerinde, POSIX standartlarına göre, Kısmi Yazım yapılmamaktadır. Öte yandan, eğer borunun toplam kapasitesinden daha büyük bir değeri yazmaya kalkarsak ya yazabildiği kadarı ile geri dönülür ya da "errno" değişkeni uygun bir değere çekilip yazma işlemi direkt engellenebilir. Örneğin, Linux sistemlerde borunun toplam uzunluğu 64K fakat "PIPE_BUF" 4K uzunlukta. Son olarak borulara yazma işlemi yine atomik düzeydedir. >>>> Borudan okuma işlemi: Boruya yazma işleminden farklıdır. Örneğin, borudan 50 byte okumak isteyelim fakat boruda 10 bayt olsun. Bu 10 bayt okunur ve bu değer ile geri dönülür. Eğer bu aşamada tekrardan okuma yapmak istersek, ilgili "thread" bloke edilecektir. En az 1 bayt'lık bilgi boruda olduğunda bloke kalkar. Pekiyi boru haberleşmesi nasıl sonlanır? İlkin yazma yapan boruyu sonlandırır. Daha sonra okuma yapan taraf okuma yapmaya devam eder. Ta ki boruda okunacak bayt kalmayana denk. Normal şartlarda okunacak bayt kalmadığında ilgili "thread" bloke edilmekteydi fakat burada yazan taraf da boruyu kapattığı için "read" fonksiyonu "0" ile geri döner. Daha sonra okuyan taraf da boruyu kapatır. Böylelikle boru haberleşmesi sonlanmış olur. >>>> İsimsiz Boru Haberleşmeleri: Sıradan iki proses arasında yapamadığımız ama alt ve üst proses arasında yapabildiğimiz haberleşme yöntemidir. Böylesi bir haberleşmenin sağlanabilmesi için aşağıdaki adımların atılması gerekmektedir: -> Üst proses, henüz "fork" fonksiyonunu çağırmadan evvel, "pipe" isimli POSIX fonksiyonunu çağırmalı ve isimsiz bir boru hayata getirmelidir. "pipe" fonksiyonu aşağıdaki imza yapısına sahiptir; #include int pipe(int fildes[2]); Fonksiyon argüman olarak elemanları "int" türden olan bir dizinin başlangıç adresini istemektedir. Aldığı bu dizinin ilk iki elemanına "fd" değişkenleri yerleştirmektedir. Yani aslında Dosya Betimleyici Tablosundaki "file" türden nesnelerin indislerini argüman olarak aldığı diziye yerleştiriyor fakat buradaki "file" türden nesneler borular ile ilişkilidir. Bu dizinin 0. indisi borudan okuma yapmak, 1. indisi ise boruya yazma yapmak için kullanılır. Fonksiyon, başarısızlık durumunda "-1" ile geri döner ve "errno" değişkeni uygun bir değere çekilir. Başarı durumunda "0" ile geri döner. -> Boru hayata geldikten sonra "fork" fonksiyon çağrısı yapılır. Artık bu aşamada üst ve alt prosesler aynı "fd" değişkenleri ile aynı "file" türden nesneleri gösterir durumdadır. -> To be continue... * Örnek 1, #include "stdio.h" #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* * İlk aşamada boru hayata getirilir. */ int fdpipe[2]; if(pipe(fdpipe) == -1) exit_sys("pipe"); /* * İkinci aşamada "fork" yapılır. */ int pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) { /* Parent process. */ } else { /* Child process. */ } puts("Ok!.."); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> C dilinde "const" bir nesnenin "update" edilmesi Tanımsız Davranış oluşturur. >> "Copy on Right" : https://en.wikipedia.org/wiki/Copy-on-write >> "ELF" formatı bir "executable" dosya formatıdır. UNIX/Linux dünyasında en çok kullanılan dosya formatıdır. Windows ise "PE" isimli bir dosya formatı kullanmaktadır. Sırasıyla "readelf" ve "dumbbin" isimli programlar bu dosya formatlarını okumamıza olanak sağlamaktadır. /*================================================================================================================================*/ (37_12_03_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> Boru Haberleşmeleri (devam): >>>> İsimsiz Boru Haberleşmeleri (devam): "pipe" fonksiyonuyla isimsiz bir boru oluşturup "fork" işlemi yaptıktan sonra sırada iş alt ve üst proseslere görev tayin etmektir. -> Bu aşamada alt ve üst proses ikişer adet "fd" dosya betimleyicilerine sahiptir. Proseslere görev atamalarını yaptıktan sonra kullanılmayan dosya betimleyicilerini kapatmamız gerekmektedir. Bunun sebebine ileride değineceğiz. Bizler üst prosese yazma görevini, alt prosese ise okuma görevini verelim. Görev atamalarından sonra kullanılmayan "fd" değişkenlerini kapatmalıyız. * Örnek 1, #include "stdio.h" #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* * İlk aşamada boru hayata getirilir. */ int fdpipe[2]; if(pipe(fdpipe) == -1) exit_sys("pipe"); /* * İkinci aşamada "fork" yapılır. */ int pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Üçüncü aşamada alt ve üst prosese * görev dağılımı yapılır. */ if(pid != 0) { /* * Üst prosese yazma görevini atayalım. * Dolayısıyla okumak için kullanılan "fd" * değişkenini geri vermemiz gerekiyor. */ close(fdpipe[0]); //... } else { /* * Alt prosese okuma görevini atayalım. * Dolayısıyla yazma için kullanılan "fd" * değişkenini geri vermemiz gerekiyor. */ close(fdpipe[1]); //... } puts("Ok!.."); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Artık okuma ve yazma işlemleri "read" ve "write" POSIX fonksiyonları ile gerçekleştirilebilir. Bu işlemlerden sonra borunun ilk olarak yazan taraf tarafınca kapatılması gerekiyor. Daha sonra boruda kalanlar da okunur. En son okuyan taraf boruyu kapatır. Eğer boru haberleşmesinde borunun kapatılması ilk olarak okuyan tarafından kapatılırsa ve yazan taraf boruya yazmaya devam ederse "SIGPIPE" isimli bir sinyal oluşur. Bu sinyal de prosesin sonlanmasına yol açar. Yani yazma tarafı kapatılan bir borudan okuma yapmakta bir sorun yoktur fakat okuma tarafı kapatılan bir boruya yazma işlemi yapmakta SORUN VARDIR. * Örnek 1, #include "stdio.h" #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 Ok!.. Ok!.. */ /* * İlk aşamada boru hayata getirilir. */ int fdpipe[2]; if(pipe(fdpipe) == -1) exit_sys("pipe"); /* * İkinci aşamada "fork" yapılır. */ int pid; if((pid = fork()) == -1) exit_sys("fork"); /* * Üçüncü aşamada alt ve üst prosese * görev dağılımı yapılır. */ if(pid != 0) { /* * Üst prosese yazma görevini atayalım. * Dolayısıyla okumak için kullanılan "fd" * değişkenini geri vermemiz gerekiyor. */ close(fdpipe[0]); /* * Dördüncü aşamada artık okuma ve yazma işlemlerini * gerçekleştireceğiz. Burada bizler boruya "i" * değişkeninin adresinden itibaren, "int" büyüklüğünde, * 10 adet veriyi boruya yazıyoruz. Çünkü burada çalışan * mekanizma aşağı yukarı şu şekildedir; * "i" değişkeninin değerini bizler boruya yazıyoruz. * Alt proses de borudan okuma yapıp, okuduklarını başka * bir değişkenin adresine yazıyor. */ for(int i = 0; i < 10; ++i) if((write(fdpipe[1], &i, sizeof(int))) == -1) exit_sys("write"); /* * Boruyu ilk kapatan yazan taraf olmalıdır. */ close(fdpipe[1]); /* * Tabii yazma tarafını kapattıktan sonra * alt prosesi YİNE BEKLEMELİYİZ. */ if(wait(NULL) == -1) exit_sys("wait"); } else { /* * Alt prosese okuma görevini atayalım. * Dolayısıyla yazma için kullanılan "fd" * değişkenini geri vermemiz gerekiyor. */ close(fdpipe[1]); /* * Dördüncü aşamada artık okuma ve yazma işlemlerini * gerçekleştireceğiz. Burada bizler borudan okuma * yapacağız. Her okumada da "int" büyüklüğünde bir * bayt okunup, "read_value" değişkeninin adresine * yazılacaktır. Aksi bir durum olmadıkça, okunan * değer kadar "read" fonksiyonu geri dönecektir. * Normal şartlarda borunun ilk olarak yazan taraf * tarafından kapatılması gerekiyor. Eğer böyleyse * ve boruda da okunacak bir şey kalmadıysa, "read" * fonksiyonu "0" ile geri dönecektir. */ int read_value; int read_result; while((read_result = read(fdpipe[0], &read_value, sizeof(int))) > 0) { printf("%d ", read_value); /* * Yine ekrana basarken "\n" kullanmadığımız için * "fflush" çağrısı yapmalıyız. */ fflush(stdout); } /* * Eğer "-1" ile geri dönmüş ise programı direkt * sonlandırmalıyız. */ if(read_result == -1) exit_sys("read"); printf("\n"); /* * Yazma tarafı kapatıldıktan sonra okuma tarafı * kapatılmalıdır. */ close(fdpipe[0]); } puts("Ok!.."); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekala bizler alt prosesi "sleep" fonksiyonu ile bekletsek ne olur? Alt proses ilgili süre boyunca bloke olacaktır. Aynı zamanda da üst proses boruya yazacaktır. Boru dolduğunda üst proses de bloke olacaktır. Alt prosesin blokesi kalktığında borudan okumaya başlayacaktır. Boruda yer açıldığı için üst prosesin de blokesi kalkacaktır. Eğer bizler üst prosesi "sleep" fonksiyonu ile bekletseydik ne olacaktı? Boruya ilgili süre boyunca bir şey yazılmayacağı için alt proses beklemede kalacak. İlgili süre sonunda üst proses yazmaya başlayacak ve alt proses de okumaya. Buradan da diyebiliriz ki senkronizasyon kendi içerisinde sağlanmaktadır. Eğer üst proses abnormal bir biçimde sonlanırsa, bütün "fd" değişkenleri de kapatılacağı için, alt proses boruda kalanları okuyacaktır. Eğer yazma sırasında bir sinyal gelirse, "write" fonksiyonu ilgili baytların tamamını yazamayabilir. Bu duruma da "partial write" denmektedir. Bir sinyal gelme olasılığının olduğu durumlarda, "write" fonksiyonunun geri dönüş değerini "-1" ile birlikte "sizeof(int)" ile de karşılaştırmalıyız. Aşağıdaki gibi bir kodu kullanabiliriz: //... int result; if((result = write(fdpipe[1], &i, sizeof(int))) == -1) exit_sys("write"); if(result != sizeof(int)) { fprintf(stderr, "partial write error!..\n"); exit(EXIT_FAILURE); } Fakat bir prosese ya "root" proses sinyal gönderebilir ya bir kullanıcı terminal üzerinden bir sinyal gönderebilir ya da ilgili proses ile aynı Kullanıcı ID değerine sahip başka bir proses sinyal gönderebilir. Öte yandan alt ve üst prosesin kullanmadıkları dosya betimleyicisi "fd" değişkenlerini kapatmaları gerektiğini söylemiştik. Yani okuma yapacak olan proses, yazmaya ait olanı; yazma yapacak olan da okumaya ait olanı. Bunun iki sebebi vardır. İlki, "read" fonksiyonu "0" ile geri dönebilmesi için boruya yazma potansiyeli olan hiç bir "fd" değişkeni OLMAMASI GEREKMEKTE ve borunun da boş olması gerekmektedir. Eğer en az bir tane yazma potansiyeline sahip "fd" değişkeni varsa, "read" fonksiyonu "0" ile geri dönmeyecektir. Böylesi bir durumda haberleşmenin sonlandırılması sorunlu hale gelecektir. Tabii okuma potansiyeli olan "fd" değişkenlerinin birden fazla olması, yukarıdaki gibi bir sonuç doğurmaz sadece dosya betimleyici tablosunda fazladan betimleyici tutmuş oluruz. İsrafa ne gerek var. Özetle; -> OKUMA YAPAN TARAFIN YAZMA BETİMLEYİCİSİNİ KAPATMAMASI KRİTİK ÖNEMLİ BİR ŞEY. -> YAZMA YAPAN TARAFIN OKUMA BETİMLEYİCİSİNİ KAPATMAMASI O KADAR DA KRİTİK ÖNEMLİ BİR ŞEY DEĞİL, TUTUMLULUK AÇISINDAN KAPATMASI GEREKİYOR. Pekiyi böylesine bir haberleşme aracına neden ihtiyaç duyalım? Sonuç olarak alt ve üst prosesin kodlarını bizler yazmaktayız. "thread" kavramı işletim sistemlerine girmeden evvel bir işi hızlandırmak için alt prosesler oluşturulur ve bu alt prosesleri birlikte çalıştırarak bir işi daha erken bitirmeye çalışırdık. Prosesler arasında da bir izolasyon olduğu için, iki prosesin haberleşmesi için böylesi bir teknik kullanılmaktaydı. Fakat "thread" ler hayatımıza girdikten sonra artık böylesi bir kullanıma artık gerek kalmadı. Artık "thread" ler daha sık kullanılmaktadır. Bizler buraya kadarki kısımda sadece "fork" işlemi uyguladık. Peki "fork" ve "exec" işlemlerinden sonra da yine prosesler arası haberleşme sağlanabilir mi? Bunun sağlanabilmesi için "exec" yapan alt prosesin borulara ilişkin "fd" değişkenlerini bilmesi gerekmektedir. Anımsanacağız üzere dosya betimleyici tablosunun ilk üç indisinde bulunan "stdin", "stdout" ve "stderr" isimli dosyalar sabitti. Bu üç indisi kullanarak bizler yine prosesler arası haberleşmeyi sağlayabiliriz. Örneğin, aşağıdaki "shell" kabuk komutunu ele alalım: a | b Burada "a" prosesinin "stdout" a yazdıklarını, "b" prosesi "stdin" den okuyordu. Yani arka planda "a" prosesinin "stdout" dosyası bir boruya yönlendirildiği için, o boruya yazmaktadır. Benzer şekilde "b" prosesinin "stdin" i de boruya yönlendirildiği için, o da borudan okuma yapmaktadır. Arka planda "pipe", "fork", "exec" ve "dup2" gibi fonksiyonlar koşmaktadır. Aşağıda bu işlevi gören bir program kodları mevcuttur. * Örnek 1, #include "stdio.h" #include #include #include #include #define MAX_ARGV 1024 void exit_sys(const char*); int main(int argc, char** argv) { /* # Command Line Arguments # "ls -l | wc" */ /* # OUTPUT # 3 20 110 */ /* * Programın devam edebilmesi için sadece 1 adet * komut satırı argümanın sahip olması gerekmektedir. */ if(2 != argc) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } /* * Girilen komut satırı argümanı içerisinde "|" karakteri * aranır. Bulunması halinde yerine "\0" yazılır ki ayrıştırma * rahat yapılsın. */ char* ppos; if((ppos = strchr(argv[1], '|')) == NULL) { fprintf(stderr, "invalid argument: %s\n", argv[1]); exit(EXIT_FAILURE); } *ppos = '\0'; int n; char* cmdl[MAX_ARGV + 1], *cmdr[MAX_ARGV + 1]; char* str; /* * Girilen komut satırı argümanları ayrıştırılır. Yazının ortasında * "\0" karakteri olduğu için, yazının başından bu karakter görülene * kadar döngü devam eder ve her bir komut da "cmdl" dizisine yazılır. */ n = 0; for(str = strtok(argv[1], " \t"); str != NULL; str = strtok(NULL, " \t")) cmdl[n++] = str; cmdl[n] = NULL; /* * Girilen komut satırı argümanları ayrıştırılır. Yazının ortasında * "\0" karakteri olduğu için, bu noktadan yazının sonuna kadar * kadar döngü devam eder ve her bir komut da "cmdr" dizisine yazılır. */ n = 0; for(str = strtok(ppos + 1, " \t"); str != NULL; str = strtok(NULL, " \t")) cmdr[n++] = str; cmdr[n] = NULL; int pipefds[2]; /* Boruyu hayata getirmiş olduk. */ if(pipe(pipefds) == -1) exit_sys("pipe"); pid_t pidl; /* * İlk alt prosesimizi hayata getiriyoruz. */ if((pidl = fork()) == -1) exit_sys("fork"); /* * İlk alt process "exec" çağrısı yapacaktır. Dolayısıyla * onun akışı bu "if" bloğunun aşağısına akmayacaktır. */ if(pidl == 0) { /* * Bu prosese yazma görevi verdik. Gereksiz dosya * betimleyicilerini kapatmaları gerekmektedir. */ close(pipefds[0]); /* * Bu alt prosesin "stdout" dosyasını boruya * yönlendirelim. Artık ekrana değil, boruya * yazacaktır. Başarısızlık durumunda "_exit" * kullandık çünkü "stdout" vb. tamponlar üst * prosesten kopyalandığı için iki defa "flush" * işlemi olsun istemedik. */ if(dup2(pipefds[1], 1) == -1) _exit(EXIT_FAILURE); /* * Burada bizler çiftleme sonucunda orjinal olanı kapatıyoruz. * Her ne kadar "exec" sonrasında bu kapatılsa bile, o ana * kadar boş yere tutmanın bir anlamı yoktur. */ close(pipefds[1]); /* * Artık alt prosesimiz "exec" uygulayacaktır. */ if(execvp(cmdl[0], cmdl) == -1) _exit(EXIT_FAILURE); // Unreachable code... } pid_t pidr; /* * Şimdi de ikinci alt prosesimizi hayata * getiriyoruz. */ if((pidr = fork()) == -1) exit_sys("fork"); /* * İkinci alt process de "exec" çağrısı yapacaktır. Dolayısıyla * onun akışı bu "if" bloğunun aşağısına akmayacaktır. */ if(pidr == 0) { /* * Bu prosese okuma görevi verdik. Gereksiz dosya * betimleyicilerini kapatmaları gerekmektedir. */ close(pipefds[1]); /* * Bu alt prosesin "stdout" dosyasını boruya * yönlendirelim. Artık ekrana değil, boruya * yazacaktır. Başarısızlık durumunda "_exit" * kullandık çünkü "stdout" vb. tamponlar üst * prosesten kopyalandığı için iki defa "flush" * işlemi olsun istemedik. */ if(dup2(pipefds[0], 0) == -1) _exit(EXIT_FAILURE); /* * Burada bizler çiftleme sonucunda orjinal olanı kapatıyoruz. * Her ne kadar "exec" sonrasında bu kapatılsa bile, o ana * kadar boş yere tutmanın bir anlamı yoktur. */ close(pipefds[0]); /* * Artık alt prosesimiz "exec" uygulayacaktır. */ if(execvp(cmdr[0], cmdr) == -1) _exit(EXIT_FAILURE); // Unreachable code... } /* * Bu programda iki alt proses de kendi arasında * haberleşme yapacağı için, üst prosesin boru * betimleyicilerini kapatmamız gerekmektedir. */ close(pipefds[0]); close(pipefds[1]); /* * Alt prosesleri beklememiz gerekmektedir. */ if(waitpid(pidl, NULL, 0) == -1) exit_sys("waitpid"); if(waitpid(pidr, NULL, 0) == -1) exit_sys("waitpid"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Ayrıca yukarıda yaptıklarımızı gerçekleştiren bazı POSIX fonksiyonları da vardır. Bu fonksiyonlar "popen", "pclose" fonksiyonlarıdır. Bu fonksiyonlar da yine "stdin" ve "stdout" tamponlarını yönlendirmektedir. "popen" ile açtığımız boru mekanizması "pclose" ile kapatılmalıdır. "fdopen" fonksiyonu, arka planda çağrılan bir fonksiyondur. >>>>> "popen" fonksiyonu: aşağıdaki parametrik yapıya sahiptir: #include FILE *popen(const char *command, const char *mode); Bu fonksiyonun birinci parametresi, "shell" programını interaktif olmayan bir biçimde çalıştırırken, kullanılacak "shell" komutudur. Yani buradaki komut, "shell" programı tarafından interaktif olmayan bir biçimde çalıştırılır. Bu fonksiyonun ikinci parametresi ise haberleşme için kullanılacak boruda yapılacak işlemi belirtmektedir. Bu parametre ya "r" ya da "w" olabilir. "r" olması durumunda, birinci parametreye geçilen programın "stdout" a yazdıkları boruya yönlendirilmiş olacak. Bizler de, bu fonksiyonun geri dönüş değerini kullanarak, bu borudan okuma yapabileceğiz. Öte yandan ikinci parametre "w" olursa, birinci parametreyle geçilen programın "stdin" i boruya yönlendirilmiş olacak. Bizler, yine bu fonksiyonun geri dönüş değerini kullanarak, boruya yazdığımız zaman birinci parametredeki program borudan okudukları üzerinde işlem yapacaktır. Açıkça görüldüğü üzere, programın geri dönüş türü "FILE*" olması hasebiyle bu değeri standart C dosya fonksiyonlarıyla birlikte kullanabiliriz. Tabii yine unutmamak gerekir ki "buffered" mekanizma burada da geçerlidir. Bir takım işlemler sonrasdında "flush" yapmaya ihtiyaç duyabiliriz. Bu fonksiyon başarısız olduğunda "NULL" değerine dönmektedir. >>>>> "pclose" fonksiyonu: Aşağıdaki parametrik yapıya sahiptir: #include int pclose(FILE *stream); Parametre olarak, "popen" ile elde ettiğimiz "FILE" türünden nesnenin adresini istemektedir. Başarı durumunda çalıştırılan "shell" komutunun "wait" fonksiyonları ile elde edilen durum bilgisini geri döndürür. Başarısızlık durumunda "-1" ile geri döner. * Örnek 1, #include "stdio.h" #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* # OUTPUT # total 20 -rwxr-xr-x 1 14061 14061 16208 Mar 26 10:52 a.out -rwxrwxrwx 1 root root 631 Mar 26 10:52 main.c */ FILE* f; /* * "ls" komutu normalde ekrana yazmaktadır. Fakat boruya * yönlendirildiği için boruya yazacaktır. Bizde "r" ile * borudan okuma yapacağız. */ if((f = popen("ls -l", "r")) == NULL) exit_sys("popen"); int ch; while((ch = fgetc(f)) != EOF) putchar(ch); pclose(f); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include "stdio.h" #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* # OUTPUT # cc1: fatal error: mainn.c: No such file or directory compilation terminated. 1 */ FILE* f; if((f = popen("gcc mainn.c -o main", "r")) == NULL) exit_sys("popen"); int ch; while((ch = fgetc(f)) != EOF) putchar(ch); int status; if((status = pclose(f)) == -1) exit_sys("pclose"); if(WIFEXITED(status)) // Normal sonlanmışsa: printf("%d\n", WEXITSTATUS(status)); else // Abnormal sonlanmışsa: printf("shell terminated abnormally!...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include "stdio.h" #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* # OUTPUT # 0 1 190 */ FILE* f; if((f = popen("wc", "w")) == NULL) exit_sys("popen"); for(int i = 0; i < 100; ++i) fprintf(f, "%d", i); pclose(f); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>> İsimli Boru Haberleşmeleri: İsimsiz boru haberleşmesine nazaran, bu haberleşme biçimi herhangi iki proses arasında haberleşme için kullanılabilir. İsimli borular ile haberleşme tipik olarak şu aşağıdaki aşamalardan geçmektedir: -> Bir adet "pipe" dosyası meydana getiriyoruz. Nasıl dizin girişleri "d", normal dosyalar "-" ile gösteriliyorsa, "pipe" dosyaları da "p" ile gösterilmektedir eğer "ls" komutunu çağırırsak. Böylesi bir dosyayı oluşturabilmek için "mkfifo" isimli POSIX fonksiyonunu çağırmamız gerekmektedir. Tabii bu fonksiyon ile oluşturduğumuz dosyaya erişim hakları yine prosesin "umask" değerinden etkilenmektedir. Oluşturulan bu dosya sadece dizin girişi olarak yer kaplamakta, fiziksel bir dosya olarak diskte yer kaplamamaktadır. Ayrıca aynı işlevi gören ve aynı isimde bir "shell" komutu da mevcuttur. Bu komut ile "pipe" dosyası oluştururken, prosesin "umask" değeri DEVREYE GİRMEZ. "mkfifo" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int mkfifo(const char *path, mode_t mode); Birinci parametre, oluşturulacak "pipe" dosyasının yol ifadesini belirtir. İkinci parametre ise iş bu dosyanın sahip olacağı erişim haklarını erişir. Fonksiyon başarılı olduğunda "0" ile başarısızlık durumunda "-1" ile geri döner. Boru dosyasını haberleşecek iki prosesten birisinin oluşturması bir zorunluluk değildir. Dışarıdan üçüncü bir proses de bu dosyayı oluşturabilir. * Örnek 1, #include "stdio.h" #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* # OUTPUT # Ok... */ if(mkfifo("testFifo", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) == -1) exit_sys("mkfifo"); printf("Ok!...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Haberleşecek olan iki proses de oluşturulan bu boru dosyasını "open" fonksiyonu ile açar. Açım sırasında tipik olarak "O_RDONLY" ve "O_WRONLY" modları kullanılmaktadır. Her ne kadar "O_RDWR" modu da kullanılsa, bu modu kullanmak uygun değildir. Ek olarak POSIX standartlarınca boru dosyalarının "O_RDWR" olarak açılması "unspecified" olarak belirtilmiştir. Eğer proseslerden birisi ilgili boruyu "O_RDONLY" modunda açmışsa, başka bir proses aynı boruyu "O_WRONLY" ya da "O_RDWR" modunda açana kadar, "open" fonksiyonu tarafından bloke edilir. Benzer biçimde prosesimiz boruyu "O_WRONLY" modunda açmışsa, başka bir proses "O_RDONLY" ya da "O_RDWR" modunda açılana kadar, prosesimiz "open" fonksiyonu tarafından bloke edilir. Eğer proseslerden birisi "O_RDWR" modunda açılmışsa, bloke GERÇEKLEŞMEZ FAKAT BU MODDA BORUNUN AÇILMASI TAVSİYE EDİLMEMEKTEDİR. Özetle "open" fonksiyonu iki proses de boruyu uygun biçimde açana kadar, proseslerin bloke edilmesine neden olmaktadır. * Örnek 1, // Birinci proses: Yazma yapacak olan. #include "stdio.h" #include #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* # OUTPUT # */ int fd; if((fd = open("testFifo", O_WRONLY)) == -1) exit_sys("open"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // İkinci proses: Okuma yapacak olan. #include "stdio.h" #include #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* # OUTPUT # */ int fd; if((fd = open("testFifo", O_RDONLY)) == -1) exit_sys("open"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> To be continued... > Hatırlatıcı Notlar: >> İnteraktif olmayan bir biçimde "shell" programını çalıştırmak için "-c" seçeneği kullanılır. >> İnteraktif olmayan bir biçimde çalıştırılan "shell" programlarının "exit" kodları, aslında çalıştırılan "shell" komutlarının "exit" kodlarıdır. >> Boru dosyalarının "O_RDWR" modunda açılması Linux sistemlerince tanımlı olan, fakat POSIX standartlarınca "unspecified" olarak nitelenen bir durumdur. /*================================================================================================================================*/ (38_18_03_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> Boru Haberleşmeleri (devam): >>>> İsimli Boru Haberleşmeleri (devam): İsimli boru haberleşmelerinde aşağıdaki akış şeması izlenmektedir: -> Bir biçimde isimli bir boru oluşturmamız gerekmektedir. Bunu sağlamak için "mkfifo" isimli kabuk komutunu kullanabiliriz. Öte yandan haberleşecek iki prosesten birisi de yine "mkfifo" isimli POSIX fonksiyonunu kullanarak da bu dosyayı oluşturabilir. Son olarak üçüncü bir harici program bu boruyu oluşturabilir. Aşağıdaki kabuk komutu ile "mypipe" isminde bir boru oluşturalım. $mkfifo mypipe Kabuk programı üzerinden "ls -l mypipe" komutunu çalıştırdığımız vakit aşağıdaki çıktıyı alacağız: prw-r--r-- 1 14088 14088 0 Mar 29 16:18 mypipe -> Artık haberleşecek olan iki prosesin ilgili boru dosyasını açması gerekmektedir. Burada okuma yapacak olan proses boruyu açtıktan sonra bloke edilir. Ta ki boruya yazma yapacak olan proses ilgili boruyu açana dek. Benzer şekilde yazma yapacak olan proses boruyu ilk açtığında yine bloke edilir, ta ki boruyu okuma amacı taşıyan prosesin boruyu açmasına kadar. Dolayısıyla okuma ve yazma yapacak olan proseslerin birlikte boruyu açması gerekmektedir. Daha önce de belirttiğimiz gibi eğer proseslerden birisi boruyu "O_RDWR" modunda açarsa blokeye düşmeyecektir fakat bu modda açım POSIX standartlarınca "unspecified" olarak belirtilmiştir. -> Artık bu noktada isimsiz boru haberleşmesinden bir farkımız kalmamıştır. "read" ve "write" fonksiyonlarıyla boruya yazma ve okuma işlemleri yapılabilir. -> Haberleşme sonlanacağı zaman, yazan proses tarafında sonlanması uygundur. Böylelikle okuyan taraf boruda kalanları da okuyacak ve borunun yazma tarafının kapatıldığını görüp "read" fonksiyonu "0" ile geri dönecektir. -> Borunun yok edilmesi, boruyu oluşturan kişi tarafından gerçekleştirilmesi uygundur. Komut satırından bir boru dosyasını yok etmek için "remove" ya da "unlink" fonksiyonlarını kullanabiliriz. Tabii gerçekten de boru dosyasının silinebilmesi için bu dosyayı gösteren "fd" sayısının "0" olması gerekmektedir. Aksi halde sadece dizin girişinden kaldırılacaktır. Eğer boru üçüncü parti bir proses tarafından "mkfifo" ile oluşturulmuşsa, "unlink" ve ya "remove" isimli POSIX fonksiyonlarını kullanması gerekmektedir. Öte yandan başka bir haberleşme için boru dosyası silinmeyedebilir. Aşağıda böyle bir kullanıma örnek verilmiştir: if(unlink("mypipe") == -1) exit_sys("unlink"); * Örnek 1, Aşağıdaki örnekteki "mypipe" isimli boru dosyasının bir şekilde oluşturulduğu varsayılmıştır. Dolayısıyla ilk önce hangi programın çalıştırıldığının bir önemi yoktur. "write" isimli program klavyeden girilenleri bu boruya yazmakta, "read" ise boruya yazılanları ekrana basmaktadır. Unutmamalıyız ki "read" isimli program borudan kaç bayt okuması gerektiğini bilmemektedir. Dolayısıyla 4096 bayt kadarlık bilgiyi okumak isteyecektir. Fakat okuyabildiği kadar bilgiyi okuyacak. Önceki örnekte "sizeof(int)" kadar bilginin yazıldığı bilindiği için "sizeof(int)" kadarlık bilgi okundu. Bu örnekte boruya kaç bayt yazıldığı bilinmediği için 4096 bayt okunmak istenmiştir. Yüksek bir değer girilerek, tek seferde okunmak amaçlanmıştır. /* write */ #include "stdio.h" #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { /* * Aşağıdaki program, değişik uzunluktaki yazıları * boruya yazmaktadır. */ int fdpipe; if((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); char buf[BUFFER_SIZE]; char* str; for(;;) { /* * Diğer prosese göndermek istediğimiz yazı beklenmektedir. */ printf("Text: "); /* * Genel olarak "stdin" den okuma yapan fonksiyonlar * "stdout" tamponunu da "flush" etmektedir. Fakat bu * garanti edilmemiştir. */ fflush(stdout); /* * "stdin" tamponundakileri "buf" adresinden itibaren * 4096 karakterlik alana alıyoruz. Fakat burada yazının * sonundaki '\n' karakteri de alınmaktadır. */ if(fgets(buf, BUFFER_SIZE, stdin) != NULL) /* * Dolayısıyla bu '\n' karakterini '\0' ile değiştirmeliyiz. * Çünkü C'nin standart fonksiyonları '\0' karakterine göre * hareket etmektedir. */ if((str = strchr(buf, '\n')) != NULL) *str = '\0'; /* * POSIX standartlarınca bir yere "0" bayt yazmak "unspecified" * olarak betimlenmiştir. */ if(*buf == '\0') continue; /* * Eğer "stdin" tamponuna "quit" girilmişse program * direkt sonlanacaktır. Karşı taraf ise boruda kalanları okuduktan * sonra "read" fonksiyonu "0" ile geri döneceğinden döngüden çıkar. * Böylece karşı taraf da sonlanır. */ if(!strcmp(buf, "quit")) break; /* * Blokeli modda, "write" fonksiyonu parçalı yazım * yapmayacaktır. "buf" adresinden itibaren, "strlen(buf)" * kadarlık karakter artık boruya yazılacaktır. */ if(write(fdpipe, buf, strlen(buf)) == -1) exit_sys("write"); } /* * Açtığımız boru dosyası kapatıldı, silinmedi. */ close(fdpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include "stdio.h" #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { /* * Aşağıdaki program ise okuyabildiği maksimum karakteri * okumaya çalışmaktadır. Eğer karşı taraf iki defa "write" * yaparsa, her ikisi de okunacaktır. */ int fdpipe; if((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); ssize_t result; char buf[BUFFER_SIZE + 1]; /* * Borudan 4096 karakteri "buf" adresinden itibaren * yazacağız. Eğer karşı taraf boruyu kapatmışsa ve boruda * okunacak başka bir şey kalmadıysa, "0" ile geri dönecektir. * Eğer bir "IO" hatası olmuşsa da "-1" ile. */ while((result = read(fdpipe, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; puts(buf); } /* * Bir "IO" hatası olduğu için programı sonlandırıyoruz. */ if(result == -1) exit_sys("read"); /* * Her şey yolunda gittiği için boruyu biz de kapatıyoruz. */ close(fdpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Peki bizler bu örnekte klavyeden sadece "Enter" tuşuna bassaydık ne olurdu? "write" programındaki "fgets" fonksiyonu ilgili adrese sadece '\n' karakterini yazardı. Bizler de bu karakteri '\0' karakterine dönüştürürdük. "strlen(buf)" değeri "0" olacağından boruya bir şey yazmayacaktır. Boruya bir şey yazılmadığından, "read" prosesi blokede bekleyecektir. Fakat bu senaryo "0" bayt yazma ve "0" bayt okuma kapsamında özel olarak ele alınmalıdır. >>>>> Hedefe "0" bayt yazma: Linux'ta "write" POSIX fonksiyonu ile bir "regular" dosyaya "0" bayt yazmak istediğimizde önce bir takım ön kontroller gerçekleştirilir, örneğin ilgili dosyanın yazma modunda açılıp açılmadığınının sorgulanması gibi. Eğer bu kontoller sırasında bir başarısızlık oluşursa, "write" fonksiyonu "-1" ile geri döner. Aksi halde "0" ile geri döner. Her iki durumda da YAZMA İŞLEMİ GERÇEKLEŞMEZ. Fakat POSIX standartlarında bu durum "unspecified" olarak belirtilmiştir. Öte yandan POSIX standartları, "regular" dosyalar dışında "0" bayt yazma işlemini de "unspecified" olarak belirtmiştir. Dolayısıyla boru gibi dosyalara "0" yazıldığında ne olacağı o sisteme bağlı bir durumdur. Özetle; POSIX standartlarınca boruya "0" bayt yazma işlemi "unspecified" olarak betimlenmiştir. >>>>> Hedeften "0" bayt okuma: POSIX standartlarınca "read" fonksiyonu yine bir takım ön kontrolleri yerine getirir. Örneğin, dosyanın okuma modunda açılıp açılmadığına bakar. Bu kontroller sırasında bir hata olması durumunda "-1" ile geri döner. Aksi halde "0" ile geri döner. Bu noktada "write" fonksiyonundan ayrılmaktadır. HERHANGİ BİR OKUMA İŞLEMİ YAPMAYACAKTIR. * Örnek 2, Aşağıdaki örnekte boru dosyası "write" programı tarafından oluşturulacaktır. /* write */ #include "stdio.h" #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { /* * Aşağıdaki program, değişik uzunluktaki yazıları * boruya yazmaktadır. */ /* * Haberleşme için kullanılacak boruyu iş bu program oluşturacağı için * ilk bu programın çalıştırılması gerekmektedir. */ if(mkfifo("mypipe", S_IRUSR | S_IWUSR | S_IRGRP | S_IRGRP) == -1) exit_sys("mkfifo"); int fdpipe; if((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); char buf[BUFFER_SIZE]; char* str; for(;;) { /* * Diğer prosese göndermek istediğimiz yazı beklenmektedir. */ printf("Text: "); /* * Genel olarak "stdin" den okuma yapan fonksiyonlar * "stdout" tamponunu da "flush" etmektedir. Fakat bu * garanti edilmemiştir. */ fflush(stdout); /* * "stdin" tamponundakileri "buf" adresinden itibaren * 4096 karakterlik alana alıyoruz. Fakat burada yazının * sonundaki '\n' karakteri de alınmaktadır. */ if(fgets(buf, BUFFER_SIZE, stdin) != NULL) /* * Dolayısıyla bu '\n' karakterini '\0' ile değiştirmeliyiz. * Çünkü C'nin standart fonksiyonları '\0' karakterine göre * hareket etmektedir. */ if((str = strchr(buf, '\n')) != NULL) *str = '\0'; /* * POSIX standartlarınca bir yere "0" bayt yazmak "unspecified" * olarak betimlenmiştir. */ if(*buf == '\0') continue; /* * Eğer "stdin" tamponuna "quit" girilmişse program * direkt sonlanacaktır. Karşı taraf ise boruda kalanları okuduktan * sonra "read" fonksiyonu "0" ile geri döneceğinden döngüden çıkar. * Böylece karşı taraf da sonlanır. */ if(!strcmp(buf, "quit")) break; /* * Blokeli modda, "write" fonksiyonu parçalı yazım * yapmayacaktır. "buf" adresinden itibaren, "strlen(buf)" * kadarlık karakter artık boruya yazılacaktır. */ if(write(fdpipe, buf, strlen(buf)) == -1) exit_sys("write"); } /* * Açtığımız boru dosyası kapatıldı, silinmedi. */ close(fdpipe); /* * Öte yandan bu program boruyu oluşturduğu için, * yine bu programın boruyu yok etmesi gerekmektedir. */ if(unlink("mypipe") == -1) exit_sys("mypipe"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include "stdio.h" #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { /* * Aşağıdaki program ise okuyabildiği maksimum karakteri * okumaya çalışmaktadır. Eğer karşı taraf iki defa "write" * yaparsa, her ikisi de okunacaktır. */ int fdpipe; if((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); ssize_t result; char buf[BUFFER_SIZE + 1]; /* * Borudan 4096 karakteri "buf" adresinden itibaren * yazacağız. Eğer karşı taraf boruyu kapatmışsa ve boruda * okunacak başka bir şey kalmadıysa, "0" ile geri dönecektir. * Eğer bir "IO" hatası olmuşsa da "-1" ile. */ while((result = read(fdpipe, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; puts(buf); } /* * Bir "IO" hatası olduğu için programı sonlandırıyoruz. */ if(result == -1) exit_sys("read"); /* * Her şey yolunda gittiği için boruyu biz de kapatıyoruz. */ close(fdpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "stream" tabanlı haberleşme yaparken okuyan program kaç bayt yazıldığını bilmediği için, yazan tarafa bir takım sorumluluklar düşmektedir. Aksi halde boruya yazma işlemi yapılırken bütün yazılar ardıardına yazılacağından, okuma işleminden sonra bütün yazılar ardı ardına gelir. Bu problemi de iki şekilde çözebiliriz. Bunlardan ilki, boruya ilk başta kaç bayt yazılacağının bilgisini yazmak ve ardından da içeriğin kendisini. Diğer yöntem ise yazılacak sonuna özel bir karakter ekleyerek boruya yazmak ve okuma yaparken de bu özel karaktere göre ayrıştırmak. İkinci yöntemde şöyle bir sıkıntı vardır: O özel karakteri nasıl tespit edebiliriz? Birer bayt şeklinde okuma yapmak sağlıklı değil çünkü çok fazla sistem fonksiyonu çağrılmaktadır. Bunun yerine bir blok bilginin borudan alınması ve bu blok bilgi içerisinde ilgili özel karakterin aranması gerekmektedir. Öte yandan bu yönteme benzer şu yöntemi de kullanabiliriz; yazma yapmadan evvel ilgili yazının sonuna '\n' karakteri yerleştirmek. Daha sonra ilgili borunun betimleyicisini "fdopen" fonksiyonuna göndererek bir "FILE*" elde etmek. Son olarak elde ettiğimiz bu değeri "fgets" fonksiyonuna argüman olarak geçmek ve böylelikle ilgili borudan '\n' karakteri görene kadar okuma yapmak. Fakat ilk yöntem tavsiye edilmektedir. * Örnek 1, Aşağıdaki program "/usr/include" içerisindeki dosyaların isimlerini boru üzerinden diğer programa göndermiştir. Boruya yazma yaparken şu şekilde bir kodlama izlemektedir: [dizi_ismi_uzunluğu][dizi_ismi] Yani yukarıdaki yöntemlerden birincisine dair bir örnektir. /* write */ #include "stdio.h" #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { /* # Command Line Argument # /usr/include */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } DIR* fddir; if((fddir = opendir(argv[1])) == NULL) exit_sys("opendir"); int fdpipe; /* * İlgili boru dosyasının halihazırda oluşturulduğu * varsayılmıştır. */ if((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); size_t len; struct dirent* de; /* * "readdir" fonksiyonu "IO" hatası ve dizinin * sonuna gelindiğinde de "NULL" ile geri dönmektedir. * Bunu ayırt edebilmek için "errno" değişkenini baştan * sıfıra çekiyoruz. */ while(errno = 0, (de = readdir(fddir)) != NULL) { /* * İlk önce yazılacak olan karakterin uzunluğunu alıp, * boruya bunun bilgisini yazıyoruz. "len" adresinden * "sizeof(size_t)" bayt kadarlık kısım boruya yazılacak. */ len = strlen(de->d_name); if(write(fdpipe, &len, sizeof(size_t)) == -1) exit_sys("write"); /* * İlgili boruya, "de->d_name" adresinden itibaren * "len" adedince karakter yazılacaktır. */ if(write(fdpipe, de->d_name, len) == -1) exit_sys("write"); } /* * Dizin girişinin sonuna gelinmişse, "errno" hala sıfırdır. */ if(errno != 0) exit_sys("readdir"); /* * En son boruyu açtığımız için ilkin boruyu kapatmalıyız. */ close(fdpipe); closedir(fddir); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include "stdio.h" #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int fdpipe; if((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); size_t len; char buf[BUFFER_SIZE + 1]; ssize_t result; for(;;) { /* * Borudan okuduğumuz "sizeof(size_t)" kadarlık bilgi * "len" adresine işlendi. Artık bir sonraki okumada * bu bayt kadarlık okuma yapmamız gerekiyor. */ if((result = read(fdpipe, &len, sizeof(size_t))) == -1) exit_sys("read"); if(result == 0) break; /* * Şimdi ise uzunluk kadarlık bilgi okundu. */ if(read(fdpipe, buf, len) == -1) exit_sys("read"); buf[len] = '\0'; puts(buf); } close(fdpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte yukarıda belirtilen yöntemlerden ikincisi kullanılmıştır. Yani her kayıttan sonra özel bir karakter boruya yazılmaktadır. Dolayısıyla boruya yazma yaparken şu şekilde bir kodlama izlemektedir: [dizi_ismi][\n] Burada '\n' kullanılmasının yegane sebebi, standart fonksiyonların bazıları '\n' karakterini göre kadar okuma yapmalarıdır. Bizler burada okuma esnasında borudaki karakterleri tek tek okumak yerine, "fgets" fonksiyonundan faydalanılmıştır. Öte yandan okuma yapmak için "open" yerine "fopen" fonksiyonunu da kullanabilirdik. /* write */ #include "stdio.h" #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { /* # Command Line Argument # /usr/include */ if(argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } DIR* fddir; if((fddir = opendir(argv[1])) == NULL) exit_sys("opendir"); int fdpipe; /* * İlgili boru dosyasının halihazırda oluşturulduğu * varsayılmıştır. */ if((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); char delim = '\n'; size_t len; struct dirent* de; /* * "readdir" fonksiyonu "IO" hatası ve dizinin * sonuna gelindiğinde de "NULL" ile geri dönmektedir. * Bunu ayırt edebilmek için "errno" değişkenini baştan * sıfıra çekiyoruz. */ while(errno = 0, (de = readdir(fddir)) != NULL) { /* Boruya ilk önce yazıyı yazıyoruz. */ if(write(fdpipe, de->d_name, strlen(de->d_name)) == -1) exit_sys("write"); /* * Daha sonra da özel karakterimizi direkt boruya yazıyoruz. * Burada '\n' yerin '\0' da kullanabilirdik fakat '\0' görene * kadar giden standart bir C fonksiyonu olmadığından, '\n' daha * mantıklıdır. */ if(write(fdpipe, &delim, 1) == -1) exit_sys("write"); } /* * Dizin girişinin sonuna gelinmişse, "errno" hala sıfırdır. */ if(errno != 0) exit_sys("readdir"); /* * En son boruyu açtığımız için ilkin boruyu kapatmalıyız. */ close(fdpipe); closedir(fddir); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include "stdio.h" #include #include // No need for Alternative Way - II #include // No need for Alternative Way - II #define BUFFER_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { // Alternative Way - I /* * İlk önce boruyu açarak ona ait olan "fd" yi * elde ettik. */ int fdpipe; if((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); FILE* fpipe; /* * Daha sonra boruya ait olan "fd" üzerinden * "FILE" yapısını elde ettik. */ if((fpipe = fdopen(fdpipe, "r")) == NULL) exit_sys("fdopen"); // Alternative Way - II /* * Boru dosyasını direkt olarak standart C fonksiyonu * ile açarak "FILE" yapısı elde ettik. Fakat C'nin standart yazma * fonksiyonları tamponlu çalıştığı için yazmadan hemen sonra "fflush" * yapmamız gerekebilir. Aksi halde yazılar boruya yazılmadan tampon * dolana kadar bekletilir. */ if((fpipe = fopen("mypipe", "r")) == NULL) exit_sys("fdopen"); // Common Section size_t len; char buf[BUFFER_SIZE]; ssize_t result; for(;;) { /* * Borudaki bilgiler, "buf" adres alanından itibaren yazılacaktır. * Yine '\n' karakteri de borudan çekilip, "buf" dizisine yazılacaktır. */ if(fgets(buf, BUFFER_SIZE, fpipe) == NULL) break; printf("%s", buf); } /* * "fclose" fonksiyonu, argüman olarak aldığı betimleyiciye iliştirilen * diğer betimleyicileri de kapatmaktadır. */ fclose(fpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi isimli borular ile "Client-Server" haberleşme mimarisi nasıl uygulanabilirdir? "Client-Server" haberleşme mimarisinde ortamın pek bir önemi yoktur. Ortam yeri gelir boru olur, yeri gelir "socket" olur. Bu mimariyi bir tasarım kalıbı olarak da değerlendirebiliriz. Bu mimaride "Clint" olanlar "Server" dan bir istekte bulunurlar. "Server" ise gelen bu istekleri yerine getirip "Client" lara geri dönüş yapar. Burada "Client" lar isteklerini boru üzerinden iletirken tek bir boru kullanabilirler. Sonuç olarak her "Client", kendi isteğini bir "struct" içerisinde gönderiyor. Fakat "Server", cevapları tek bir boru üzerinden göndermesi manasızdır. Birden fazla "Client" tek bir borudan okuma yapacağı için, hangisi okursa boru boşaltılacaktır. Dolayısıyla "Server" dan geri bildirimler, her "Client" a özgü borular üzerinden yapılmalıdır. Özetle; -> From Client to Server, 1 Pipe Only -> From Server to Client, 1 Pipe per Client İşte "Client-Server" arasındaki haberleşme akışı da aşağıdaki şekilde gerçekleşmektedir: -> İsteklerini iletmek isteyen "Client" lar hangi boruyu kullanmaları gerektiğini işin başında bilmeleri gerekiyor ve bu boruyu açmaları gerekmektedir. Benzer şekilde "Server" ın da bu boruyu bilmesi gerekmektedir. Yani bu boruyu bizler komut satırından oluşturabiliriz. -> "Client" lar isteklerini bir "struct" haline getirmelidir. Örneğin, struct tagMSG{ pid_t pid; // Bu ID değeri o an için eşsizdir ve "Client" ın tespitinde kullanılır. int type; // Burada mesajın tür bilgisi tutulabilir. char message[]; // "Server" a iletilmek istenen mesajı tutacak tampon. Yani mesajın içeriği. }MSG; -> "Server" ın "Client" lara geri bildirim yapması için bir boru kullanması gerektiğini söyledik. Pekiyi bu boruyu kim oluşturacak? İşin başında kaç adet "Client" ın bağlanacağı bilinemez. Dolayısıyla çalışma zamanına yönelik bir çözüm üretmeliyiz. Akla ilk gelen yöntem, "Client" in göndereceği "struct" içerisindeki "type" bilgisini alan "Server", bu bilgi doğrultusunda bir boru oluşturur. Fakat "Server" kendi içerisinde "dictionary" tipindeki bir veri yapısında bu "type" bilgisi ile oluşturulan borunun "fd" bilgisini saklamalıdır. C++ dilinde bu tür veri yapılarına "std::set", "std::map" vb. örnek verilebilir. C dilinde böylesi bir veri yapısı yok. Dolayısıyla ya üçüncü parti kütüphanelerdekileri kullanmalıyız ya da daha ilkel bir veri yapısını kendimiz oluşturmalıyız. Bu veri yapısını oluşturduktan sonra, "Server" tarafı "Client" lara cevap vermek için kullanılacak olan boruları oluşturabilir. -> Bu haberleşme mimarisinde "Client" ın gönderdiği her mesaja "Server" ın bir cevap vermesi beklenir. Fakat "Client" ilk mesajını yolladıktan sonra "Server" tarafından boru hayata getirilirken bir hata oluşabilir ve boru oluşamaz. Bu durumda bu hata bilgisi "Client" a geri nasıl gönderilmelidir? Bu problemi çözmek için ilgili borunun "Client" tarafından oluşturulması ve bu borunun "fd" bilgisinin de gönderilen mesajın içine eklenmesi gerekir. Şimdi elimizde iki farklı çözüm var. Bu noktada izler ikinci çözümden devam edelim. YANİ İLGİLİ BORU "Client" TARAFINDAN OLUŞTURULMAKTADIR. Yine unutmamalıyız ki ilgili boru "Client" tarafından oluşturulduğu için yine ilgili "Client" tarafından yok edilmelidir. -> Bu aşamada "Client" ın hangi borudan "Server" a mesaj göndereceği ve "Server" ın gelen bu mesajı hangi boru üzerinden o "Client" a iletmesi gerektiği bilinmektedir. > Hatırlatıcı Notlar: >> "mkfifo" kabuk komutunu kullanırken, "-m" seçeneğini kullanırsak, oluşturulacak boru dosyasının erişim haklarını önceden belirleyebiliriz. >> Sıralı arama yapmak, eleman sayısının 20'yi geçmediği durumlarda, gayet de verimli bir arama yöntemidir. /*================================================================================================================================*/ (39_19_03_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> Boru Haberleşmeleri (devam): >>>> İsimli Boru Haberleşmeleri (devam): "Client-Server" haberleşme akışı aşağıdaki şekilde devam etmektedir (devam): -> Haberleşme sonlanırken de "el sıkışma" felsefesi izlenmelidir. Yani "Client" tarafı haberleşmeyi sonlandırmak istediğini "Server" a iletir. Bu talebi alan "Server" bir takım kontroler gerçekleştirir ve haberleşmenin sonlanması için onay verir. Bu onayı alan "Client" ise gerekli kapatma işlemlerini gerçekleştirir. * Örnek 1, Aşağıda "Client-Server" mimarisinde boru kullanımına dair bir örnek verilmiştir. Haberleşmenin sağlanabilmesi için işin başında "serverpipe" isimli boru dosyasının var olması gerekmektedir. Daha sonra "Client" proses, "Server" tarafından kendisine mesaj gönderilirken kullanılacak olan borunun isminin girilmesini beklemektedir. Örneğin, bu isim olarak "clientpipe" girildiğini varsayalım. Artık haberleşme şu aşağıdaki şekilde olacaktır; -> From "Client" to "Server", "serverpipe". -> From "Server" to "Client", "clientpipe". Burada "clientpipe" isimli boru "Server" tarafından oluşturulacaktır ve oluşturduğu borunun "fd" değerini de "clienpipe" isimli boru üzerinden "Client" a göndermektedir. Bu "fd" değeri artık o "Client" ın ID değerini oluşturacaktır. Eğer bir sorun olmadığını varsayarsak, iki proses de artık haberleşmektedir. Eğer "Client" program "CMD ls -l" mesajını gönderirse, "Server" tarafı "ls -l" kabuk komutu "shell" programında çalıştırtacaktır. Fakat sonuçları "Client" a gönderecektir. Bir nevi "Server" programdan kabuk komutlarını çalıştırmasını istiyoruz. Haberleşmeyi sonlandırmak için de "Client" program "quit" mesajını "Server" a iletmesi yeterlidir. Fakat arka planda şöyle bir akış izlenir; -> "Client" program "Server" programa "DISCONNECT_REQUEST" mesajını gönderir. -> Bu mesajı alan "Server", bir takım kontroller sonrasında haberleşmenin sonlanma teklifini kabul eder ise "Client" programa "DISCONNECT_ACCEPTED" mesajını iletir. -> Bu mesajı alan "Client", son olarak "Server" a "DISCONNECT" mesajını gönderir ve haberleşme sonlanır. Aşağıda ilgili programlara dair kodlar mevcuttur. /* server.c */ #include #include #include #include #include #include #include #define SERVER_PIPE "serverpipe" #define MAX_MSG_LEN 32768 #define MAX_PIPE_PATH 1024 #define MAX_CLIENT 1024 /* "Client" tarafından "Server" a gönderilen mesaj yapısı. */ typedef struct tagCLIENT_MSG { int msglen; int client_id; char msg[MAX_MSG_LEN]; } CLIENT_MSG; /* "Server" tarafından "Client" a gönderilen mesaj yapısı. */ typedef struct tagSERVER_MSG { int msglen; char msg[MAX_MSG_LEN]; } SERVER_MSG; /* Bir mesajın içeriği. */ typedef struct tagMSG_CONTENTS { char *msg_cmd; char *msg_param; } MSG_CONTENTS; /* Bir mesaj geldiğinde çağrılacak fonksiyon. */ typedef struct tagMSG_PROC { const char *msg_cmd; int (*proc)(int, const char *msg_param); } MSG_PROC; /* * "Server", kendisine bağlanan "Client" lara * ilişkin bilgileri bu yapı nesnesi üzerinden * saklayacaktır. */ typedef struct tagCLIENT_INFO { int fdp; char path[MAX_PIPE_PATH]; } CLIENT_INFO; int get_client_msg(int fdp, CLIENT_MSG *cmsg); int putmsg(int client_id, const char *cmd); void parse_msg(char *msg, MSG_CONTENTS *msgc); void print_msg(const CLIENT_MSG *cmsg); int invalid_command(int client_id, const char *cmd); int connect_proc(int client_id, const char *msg_param); int disconnect_request_proc(int client_id, const char *msg_param); int disconnect_proc(int client_id, const char *msg_param); int cmd_proc(int client_id, const char *msg_param); void exit_sys(const char *msg); /* * "CONNECT" mesajı geldiğinde "connect_proc" fonksiyonu * çağrılacak. Yani "Client", bu aşağıdaki komutlardan birisini * göndermelidir. Örneğin, "CMD ls -l" şeklinde bir mesaj "Client" * tarafından gönderildiğinde, sonuçlar "Client" ın terminalinde * çıkacaktır. */ MSG_PROC g_msg_proc[] = { {"CONNECT", connect_proc}, {"DISCONNECT_REQUEST", disconnect_request_proc}, {"DISCONNECT", disconnect_proc}, {"CMD", cmd_proc}, {NULL, NULL} }; /* * "Server", kendisine bağlanan "Client" ları * veri yapısı olarak C'deki dizileri kullanmıştır. */ CLIENT_INFO g_clients[MAX_CLIENT]; int main(void) { int fdp; CLIENT_MSG cmsg; MSG_CONTENTS msgc; int i; /* * Haberleşmeyi başlatmadan evvel bir şekilde "serverpipe" * isimli boru dosyasının var olması gerekmektedir. */ if ((fdp = open(SERVER_PIPE, O_RDWR)) == -1) exit_sys("open"); for (;;) { /* * İş bu fonksiyon ile "Client" lardan gelen mesajlar * ilgili borudan okunur. */ if (get_client_msg(fdp, &cmsg) == -1) exit_sys("get_client_msg"); /* Hangi mesajın okunduğu ekrana yazılmıştır. */ print_msg(&cmsg); /* Şimdi bu mesajı "parse" etmişiz. */ parse_msg(cmsg.msg, &msgc); for (i = 0; g_msg_proc[i].msg_cmd != NULL; ++i) /* * Bu okunan mesaj, yukarıdaki C dizisinde saklanan * mesajlar arasında mı değil mi sorgusu yapmışız. */ if (!strcmp(msgc.msg_cmd, g_msg_proc[i].msg_cmd)) { /* * Eğer öyleyse, karşılık gelen fonksiyonuna çağrı yapmışım. */ if (g_msg_proc[i].proc(cmsg.client_id, msgc.msg_param)) { } break; } if (g_msg_proc[i].msg_cmd == NULL) if (invalid_command(cmsg.client_id, msgc.msg_cmd) == -1) continue; } close(fdp); return 0; } int get_client_msg(int fdp, CLIENT_MSG *cmsg) { /* İlk önce mesajın uzunluk bilgisi borudan okunacaktır. */ if (read(fdp, &cmsg->msglen, sizeof(int)) == -1) return -1; /* Daha sonra hangi "Client" ın bu mesajı yazdığı bilgisi okunacaktır. */ if (read(fdp, &cmsg->client_id, sizeof(int)) == -1) return -1; /* Daha sonra da boruya yazılmış olan mesajın bizzat kendisi okunacaktır. */ if (read(fdp, cmsg->msg, cmsg->msglen) == -1) return -1; /* En sonunda ise ilgili mesajın sonuna '\0' karakteri yerleştirilmiştir. */ cmsg->msg[cmsg->msglen] = '\0'; return 0; } int putmsg(int client_id, const char *cmd) { SERVER_MSG smsg; int fdp; strcpy(smsg.msg, cmd); smsg.msglen = strlen(smsg.msg); fdp = g_clients[client_id].fdp; return write(fdp, &smsg, sizeof(int) + smsg.msglen) == -1 ? -1 : 0; } void parse_msg(char *msg, MSG_CONTENTS *msgc) { int i; msgc->msg_cmd = msg; for (i = 0; msg[i] != ' ' && msg[i] != '\0'; ++i) ; msg[i++] = '\0'; msgc->msg_param = &msg[i]; } void print_msg(const CLIENT_MSG *cmsg) { printf("Message from \"%s\": %s\n", cmsg->client_id ? g_clients[cmsg->client_id].path : "", cmsg->msg); } int invalid_command(int client_id, const char *cmd) { char buf[MAX_MSG_LEN]; sprintf(buf, "INVALID_COMMAND %s", cmd); if (putmsg(client_id, buf) == -1) return -1; return 0; } int connect_proc(int client_id, const char *msg_param) { int fdp; char buf[MAX_MSG_LEN]; /* * "Server", "Client" a geri dönmek için kullanacağı * boruyu kendisi oluşturmuştur. */ if (mkfifo(msg_param, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) == -1) { printf("CONNECT message failed! Params = \"%s\"\n", msg_param); return -1; } /* Daha sonra bu boruyu açmış. */ if ((fdp = open(msg_param, O_WRONLY)) == -1) exit_sys("open"); g_clients[fdp].fdp = fdp; strcpy(g_clients[fdp].path, msg_param); /* En sonunda ise bu boruya aşağıdaki yazıyı yazmıştır. */ sprintf(buf, "CONNECTED %d", fdp); if (putmsg(fdp, buf) == -1) exit_sys("putmsg"); return 0; } int disconnect_request_proc(int client_id, const char *msg_param) { if (putmsg(client_id, "DISCONNECT_ACCEPTED") == -1) return -1; return 0; } int disconnect_proc(int client_id, const char *msg_param) { close(g_clients[client_id].fdp); if (remove(g_clients[client_id].path) == -1) return -1; return 0; } int cmd_proc(int client_id, const char *msg_param) { FILE *f; char cmd[MAX_MSG_LEN] = "CMD_RESPONSE "; int i; int ch; if ((f = popen(msg_param, "r")) == NULL) { printf("cannot execute shell command!..\n"); return -1; } for (i = 13; (ch = fgetc(f)) != EOF; ++i) cmd[i] = ch; cmd[i] = '\0'; if (putmsg(client_id, cmd) == -1) return -1; return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #define SERVER_PIPE "serverpipe" #define MAX_CMD_LEN 1024 #define MAX_MSG_LEN 32768 #define MAX_PIPE_PATH 1024 typedef struct tagCLIENT_MSG { int msglen; int client_id; char msg[MAX_MSG_LEN]; } CLIENT_MSG; typedef struct tagSERVER_MSG { int msglen; char msg[MAX_MSG_LEN]; } SERVER_MSG; typedef struct tagMSG_CONTENTS { char *msg_cmd; char *msg_param; } MSG_CONTENTS; typedef struct tagMSG_PROC { const char *msg_cmd; int (*proc)(const char *msg_param); } MSG_PROC; void sigpipe_handler(int sno); int putmsg(const char *cmd); int get_server_msg(int fdp, SERVER_MSG *smsg); void parse_msg(char *msg, MSG_CONTENTS *msgc); void check_quit(char *cmd); int connect_to_server(void); int cmd_response_proc(const char *msg_param); int disconnect_accepted_proc(const char *msg_param); int invalid_command_proc(const char *msg_param); void clear_stdin(void); void exit_sys(const char *msg); /* * "Server" tarafından gönderilen mesajların * bilgisidir. */ MSG_PROC g_msg_proc[] = { {"CMD_RESPONSE", cmd_response_proc}, {"DISCONNECT_ACCEPTED", disconnect_accepted_proc}, {"INVALID_COMMAND", invalid_command_proc}, {NULL, NULL} }; int g_client_id; int g_fdps, g_fdpc; /* Function Definitions */ int main(void) { char cmd[MAX_CMD_LEN]; char *str; SERVER_MSG smsg; MSG_CONTENTS msgc; int i; if (signal(SIGPIPE, sigpipe_handler) == SIG_ERR) exit_sys("signal"); if ((g_fdps = open(SERVER_PIPE, O_WRONLY)) == -1) exit_sys("open"); if (connect_to_server() == -1) { fprintf(stderr, "cannot connect to server! Try again...\n"); exit(EXIT_FAILURE); } for (;;) { printf("Client>"); fflush(stdout); fgets(cmd, MAX_CMD_LEN, stdin); if ((str = strchr(cmd, '\n')) != NULL) *str = '\0'; check_quit(cmd); if (putmsg(cmd) == -1) exit_sys("putmsg"); if (get_server_msg(g_fdpc, &smsg) == -1) exit_sys("get_client_msg"); parse_msg(smsg.msg, &msgc); for (i = 0; g_msg_proc[i].msg_cmd != NULL; ++i) if (!strcmp(msgc.msg_cmd, g_msg_proc[i].msg_cmd)) { if (g_msg_proc[i].proc(msgc.msg_param) == -1) { fprintf(stderr, "command failed!\n"); exit(EXIT_FAILURE); } break; } if (g_msg_proc[i].msg_cmd == NULL) { /* command not found */ fprintf(stderr, "Fatal Error: Unknown server message!\n"); exit(EXIT_FAILURE); } } return 0; } void sigpipe_handler(int sno) { printf("server down, exiting...\n"); exit(EXIT_FAILURE); } int putmsg(const char *cmd) { CLIENT_MSG cmsg; int i, k; for (i = 0; isspace(cmd[i]); ++i) ; for (k = 0; !isspace(cmd[i]); ++i) cmsg.msg[k++] = cmd[i]; cmsg.msg[k++] = ' '; for (; isspace(cmd[i]); ++i) ; for (; (cmsg.msg[k++] = cmd[i]) != '\0'; ++i) ; cmsg.msglen = (int)strlen(cmsg.msg); cmsg.client_id = g_client_id; if (write(g_fdps, &cmsg, 2 * sizeof(int) + cmsg.msglen) == -1) return -1; return 0; } int get_server_msg(int fdp, SERVER_MSG *smsg) { if (read(fdp, &smsg->msglen, sizeof(int)) == -1) return -1; if (read(fdp, smsg->msg, smsg->msglen) == -1) return -1; smsg->msg[smsg->msglen] = '\0'; return 0; } void parse_msg(char *msg, MSG_CONTENTS *msgc) { int i; msgc->msg_cmd = msg; for (i = 0; msg[i] != ' ' && msg[i] != '\0'; ++i) ; msg[i++] = '\0'; msgc->msg_param = &msg[i]; } void check_quit(char *cmd) { int i, pos; for (i = 0; isspace(cmd[i]); ++i) ; pos = i; for (; !isspace(cmd[i]) && cmd[i] != '\0'; ++i) ; if (!strncmp(&cmd[pos], "quit", pos - i)) strcpy(cmd, "DISCONNECT_REQUEST"); } int connect_to_server(void) { char name[MAX_PIPE_PATH]; char cmd[MAX_CMD_LEN]; char *str; SERVER_MSG smsg; MSG_CONTENTS msgc; int response; printf("Pipe name:"); fgets(name, MAX_PIPE_PATH, stdin); if ((str = strchr(name, '\n')) != NULL) *str = '\0'; if (access(name, F_OK) == 0) { do { printf("Pipe already exists! Overwrite? (Y/N)"); fflush(stdout); response = tolower(getchar()); clear_stdin(); if (response == 'y' && remove(name) == -1) return -1; } while (response != 'y' && response != 'n'); if (response == 'n') return -1; } sprintf(cmd, "CONNECT %s", name); if (putmsg(cmd) == -1) return -1; /* * "Server" ın "Client" a haber göndereceği boru * "Server" tarafından oluşturulacak. Boru hayata gelene * kadar bizler beklemede kalıyoruz. */ while (access(name, F_OK) != 0) usleep(300); if ((g_fdpc = open(name, O_RDONLY)) == -1) return -1; if (get_server_msg(g_fdpc, &smsg) == -1) exit_sys("get_client_msg"); parse_msg(smsg.msg, &msgc); if (strcmp(msgc.msg_cmd, "CONNECTED")) return -1; g_client_id = (int)strtol(msgc.msg_param, NULL, 10); printf("Connected server with '%d' id...\n", g_client_id); return 0; } int cmd_response_proc(const char *msg_param) { printf("%s\n", msg_param); return 0; } int disconnect_accepted_proc(const char *msg_param) { if (putmsg("DISCONNECT") == -1) exit_sys("putmsg"); exit(EXIT_SUCCESS); return 0; } int invalid_command_proc(const char *msg_param) { printf("invalid command: %s\n", msg_param); return 0; } void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > "fcntl" fonksiyonu: İş bu fonksiyon, halihazırda açılmış olan bir dosyanın ki bu dosya "regular" dosya olabileceği gibi boru dosyası da olabilir, çeşitli açım özelliklerini değiştirmek için kullanılır. Yani bir dosyayı açarken parametre olarak girdiğimiz bayrakları daha sonra değiştirmek istediğimizde bu fonksiyondan faydalanırız. Bu fonksiyonu kullanabilmek için ilgili dosyanın "open" ile açılmasına gerek yoktur. Örneğin, bir boru hayata getirirken bizler "open" fonksiyonunu kullanmayız. Çağırdığımız "mkfifo" fonksiyonu ise bizlere iki elemanlı ve elemanları bir "fd" olan dizinin başlangıç adresini döndürür. İşte bu "fd" leri kullanarak ilgili boru dosyalarının bir takım özelliklerini değiştirmek istiyorsak, "fcntl" fonksiyonunu çağırmalıyız. Anımsanacağınız üzere bu fonksiyonu, bir prosesin "close on exec" bayrağını değiştirmek için de kullanmıştık. Ayrıca iş bu fonksiyon bayrakların "get" edilmesi için de kullanılmaktadır. Fonksiyonumuz aşağıdaki parametrik yapıya sahiptir; #include int fcntl(int fildes, int cmd, ...); Fonksiyonun birinci parametresi, hangi "fd" üzerinde işlem yapılacağını belirtir. İkinci parametre ise bizim ne yapmak istediğimizi belirten parametredir. Bu parametrelere sadece bir takım sembolik sabitleri geçebiliriz. Üçüncü parametre ise sadece "set" işlemi sırasında yeni değeri geçmek için kullanılır. Fonksiyon başarısızlık durumunda "-1" ile geri döner. Başarı durumunda ise "get" ettiğimiz değer ile geri dönmektedir. Fonksiyonun ikinci parametresine: >> "F_GETFL" ya da "F_SETFL" sembolik sabitlerinden birisini geçmemiz durumunda, fonksiyon "Dosya Durum Bayraklarını (file status flags)" ve "Dosya Erişim Modunu (file access mode)" sırasıyla "get" ve "set" etmektedir. Dosya Durum Bayrakları şunlardır: O_APPEND O_DSYNC O_NONBLOCK O_RSYNC O_SYNC Biz şimdiye kadar bu bayraklardan sadece "O_APPEND" bayrağını "open" fonksiyonu sırasında gördük. Dosya Erişim Modları da şunlardır: O_EXEC O_RDONLY O_RDWR O_SEARCH O_WRONLY Eğer programcı Dosya Durum Bayraklarını "F_GETFL" ile elde etmek istemişse, ilgili fonksiyonun geri dönüş değerini yukarıdaki durum bayrakları ile "bitwise-AND" işlemine sokması gerekmektedir. Takribi olarak aşağıdaki biçimde bir çözüm üretmeliyiz: //... result = fcntl(fd, F_GETFL); if(result & O_APPEND) { /* "O_APPEND" bayrağı "set" edilmiş demektir. */ } Öte yandan Dosya Erişim Modlarını "get" etmek istiyorsa yukarıdaki çözümü UYGULAMAMALIDIR. Bunun için önce "fcntl" fonksiyonunun geri dönüş değeri "O_ACCMODE" ile "bitwise-AND" işlemine sokulmalı. Çıkan sonuç ile yukarıdaki erişim modları "==" sorgulaması yapılmalıdır. Takribi olarak aşağıdaki biçimde bir çözüm üretmeliyiz: //.. result = fcntl(fd, F_GETFL); if((result & O_ACCMODE) == O_RWWR) { /* "O_RWWD" bayrağı "set" edilmiş demektir. */ } Şimdi de "O_NONBLOCK" bayrağını "set" etmek isteyelim. Bu sefer de aşağıdaki gibi bir çözüm üretmeliyiz; //... fcntl(fd, F_SETFL, O_NONBLOCK); Fakat bu şekilde kullanırsak, bütün bayraklar (Dosya Durum Bayrakları ve Dosya Erişim Modu) "set" edilecektir. Dolayısıyla bizler ilk önce "get", sonra "set" yapmalıyız ki sadece bizim istediğimiz "set" edilsin. Bunun için de: //.. result = fcntl(fd, F_GETFL); if(fcntl(fd, F_SETFL, result | O_NONBLOCK) == -1) exit_sys("fcntl); Şimdi de "O_NONBLOCK" bayrağını "clear" edelim: //.. result = fcntl(fd, F_GETFL); if(fcntl(fd, F_SETFL, result & ~O_NONBLOCK) == -1) exit_sys("fcntl); >> "F_GETFD" ya da "F_SETFD" sembolik sabitlerinden birisini geçmemiz durumunda fonksiyonumuz "Dosya Betimleyici Bayraklarını" sırasıyla "get" ve "set" edecektir. POSIX standartları, Dosya Betimleyici Bayrakları olarak, sadece bir adet bayrak tanımlamıştır. Bu bayrak ise şudur: FD_CLOEXEC Dolayısıyla biz bu sembolik sabitler ile sadece "FD_CLOEXEC" bayrağını değiştirebiliriz. Anımsanacağınız üzere bizler daha evvelinde dosyanın "close on exec" bayrağı üzerinde oynamalar yapmıştık. Hatırlamak gerekirse; "get" etmek için aşağıdaki yöntemi kullanabiliriz. //... result = fcntl(fd, F_GETFD); if(result & FD_CLOEXEC) { /* "close on exec" bayrağı "set" edilmiş. */ } İlgili bayrağı "set" etmek için de yine aşağıdaki yöntemi kullanabiliriz: //... result = fcntl(fd, F_GETFD); if(fcntl(fd, F_SETFD, result | FD_CLOEXEC) == -1) exit_sys("fcntl); İlgili bayrağı "clear" etmek için de yine aşağıdaki yöntemi kullanabiliriz: //... result = fcntl(fd, F_GETFD); if(fcntl(fd, F_SETFD, result & ~FD_CLOEXEC) == -1) exit_sys("fcntl); >> "F_DUPFD" sembolik sabitini geçmemiz durumunda fonksiyonumuz argüman olarak aldığı "fd" değerini çiftleyecektir. Daha önce bizler bu çiftleme işlemi için "dup" ve "dup2" fonksiyonunu kullanmıştık. "fcntl" fonksiyonu ile çiftlemek için üçüncü bir parametre daha girmemiz gerekmektedir. Böylelikle üçüncü parametredeki "fd" değişkeninden büyük eşit olan ilk boş "fd" değişkeni ile çiftleme yapılacaktır. Karşılaştırırsak, //... fd2 = dup(fd); // İlk boş betimleyici bize verilecek. //... fd3 = dup2(fd, 10); // 10 numaralı betimleyici bize verilecek. //... fd4 = fcntl(fd, F_DUPFD, 15); // 15 numaralıdan büyük eşit ilk betimleyici. İlgili fonksiyonun kullanım biçimi de aşağıdaki gibidir; //... if((fd2 = fcntl(fd, F_DUPFD, 50)) == -1) exit_sys("fcntl); Burada "fd" nin çiftlenmiş betimleyicisi 50 ya da ilk büyük betimleyicidir. Aşağıdaki gibi bir kullanım ise "dup" fonksiyonunun kullanımı aynıdır: //... if((fd2 = fcntl(fd, F_DUPFD, 0)) == -1) exit_sys("fcntl); >> "F_DUPFD_CLOEXEC" sembolik sabiti kullanırsak, "F_DUPFD" ve "close on exec" bayrağının "set" edilmesini birlikte yapmaktadır. Yani özetle bu komut kodu hem dosya betimleyicisini çiftler hem de çiftlenmiş olan yeni betimleyicinin "close on exec" bayrağını "set" eder. //... if((fd2 = fcntl(fd, F_DUPFD_CLOEXEC, 0)) == -1) exit_sys("fcntl); Artık "fd2" betimleyicisinin aynı zamanda "close on exec" bayrağı da "set" edilmiştir. İş bu fonksiyonun ikinci parametreye geçilen diğer bayrakları ise o konular işlendiği zamanda anlatılacaktır. > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> Boru Haberleşmeleri (devam): >>>> Blokesiz Mod Kavramı & "Non-blocking" Boru İşlemleri: Bizler "open" fonksiyonunu işlerken "O_NONBLOCK" bayrağından bahsetmemiştik. Aygıt dosyaları, boru dosyaları gibi özel dosyaları "open" ile açarken "O_NONBLOCK" açış modunu da ekleyerek açmamız durumunda ilgili proses bloke EDİLMEYECEKTİR. "regular" dosyalar üzerinde bu bayrağın bir işlevi yoktur. Pekiyi nedir bloke edilmemek? Anımsanacağınız üzere bir borudan okuma yapmak istesek ama boru boş olsa ve biz "read" fonksiyonunu çağırdığımız zaman prosesimiz bloke edilecektir. İşte isimli boruları bu bayrak ile açarsak, prosesimiz blokeye düşmeden "read" fonksiyonumuz başarısızlıkla geri dönecektir. Benzer durum "write" fonksiyonu ile yazma için de geçerlidir. Fakat "write" fonksiyonu ile blokesiz modda "Partial Write" gerçekleşebilir. Öte yandan isimsiz boru haberleşmesi yaparken boruyu bizler açmadığımız için, bu bayrağı kullanmak istiyorsak "fcntl" fonksiyonunu kullanmalıyız. Çünkü isimsiz boru haberleşmesinde alt ve üst proses haberleşmesi için "open" fonksiyonu KULLANILMAMAKTADIR. Kullanılan "pipe" fonksiyonu da varsayılan durumda "blocking" moddadır. Özetle bloke oluşacak ise ilgili fonksiyon bloke oluşturmamakta, başarısız olmaktadır. Böylesi bir başarısızlık durumunda "errno" değişkeni "EAGAIN" değerini almaktadır. UNUTULMAMALIDIR Kİ VARSAYILAN DURUM "blocking" DURUMUDUR. > Hatırlatıcı Notlar: >> POSIX'te borular, aynı makinedeki prosesler arasında haberleşme için kullanılmaktadır. >> Daha önceki derste "Client-Server" için bir tasarım kalıbı olduğundan bahsetmiştik. Bizler burada boruları kullandığımız için aynı makinedeki iki prosesin haberleşmesini gerçekleştireceğiz. Farklı makinedeki proseslerin haberleşmesi için başka ortamlar kullanmamız gerekmektedir eğer "Client-Server" mimarisini kullanacaksak. Her ne kadar "Client-Server" dendiğinde akla "TCP-IP Socket" programlaması gelsede, özünde bir tasarım kalıbıdır. Bu haberleşme mimarisinde esas işi yapan taraf "Server" tarafıdır. Fakat istekte bulunan taraf "Client" tarafıdır. >> GNU kütüphanesinde C standartlarında olmayan bir takım veri yapıları mevcuttur. >> Dosyalarda "Close-on-exec" bayrakları: Öyle bir bayraktır ki "exec" yapıldığında ilgili açık olan dosyanın "close" edileceğini belirtir ve her bir "fd" için bir bayrak vardır. Bu bayraklar prosesin kontrol bloğu içerisinde yer alır. Bu bayrağın "set" edilmesi demek, "exec" işlemi sırasında ilgili dosyaların da otomatik olarak kapatılacağı demektir. "file" nesneleri yok edilmiyor, sadece dosyalar kapatılıyor. Yani "file" içerisindeki sayaçlar bir azaltılıyor. Bu sayaçlar sıfıra düştüğünde yok ediliyor. Varsayılan durumda bu bayrak "set" EDİLMEMİŞTİR. Pekiyi bizler bu bayrağı nasıl "set" ederiz? Birinci yöntem dosyayı ilk açışımız sırasında özel bir bayrak kullanmamız ki bunun adı "FD_CLOEXEC" biçimindedir. İkinci yöntemde ise "fcntl" isimli fonksiyonu çağırmamız. >> "write" fonksiyonu ile blokeli modda yazarken "Partial Write" oluşmazken, blokesiz modda OLUŞUR. /*================================================================================================================================*/ (40_25_03_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> Boru Haberleşmeleri (devam): >>>> Blokesiz Mod Kavramı & "Non-blocking" Boru İşlemleri (devam): Aşağıda isimsiz borular üzerinde "non-blocking" bir uygulamalar geliştirilmiştir. * Örnek 1, Aşağıda başarısızlık durumunda ilgili "thread" ler sonlanmaktadır. #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char** argv) { /* # OUTPUT # read: Resource temporarily unavailable */ /* * 0. Index: Read * 1. Index: Write */ int pipefds[2]; if(pipe(pipefds) == -1) exit_sys("pipe"); /* * Okuma yapılacak taraf artık "non-blocking" modda. Artık boruda okunacak * en az bir bayt varsa okunacak, aksi halde "read" fonksiyonu başarısız * olacaktır. */ if(fcntl(pipefds[0], F_SETFL, fcntl(pipefds[0], F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); /* * Yazma yapılacak taraf artık "non-blocking" modda. Artık boruda yer kalmadığında * "write" fonksiyonu başarısız olacaktır. */ if(fcntl(pipefds[1], F_SETFL, fcntl(pipefds[1], F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) /* Parent will write to the pipe. */ { close(pipefds[0]); /* * Üst proses mahsus bir saniye bloke edilmiştir çünkü "sleep" * fonksiyonu ilgili "thread" in blokesine yol açar. Geçen bu zamanda * alt prosesteki "read", boru boş olduğu için başarısız olacaktır. */ sleep(1); for(int i = 0; i < 10; ++i) if(write(pipefds[1], &i, sizeof(int)) == -1) exit_sys("write"); close(pipefds[1]); if(wait(NULL) == -1) exit_sys("wait"); } else /* Child will read from the pipe. */ { close(pipefds[1]); ssize_t result; int read_val; while((result = read(pipefds[0], &read_val, sizeof(int))) > 0) { printf("%d ", read_val); fflush(stdout); } if(result == -1) { /* * "fork" öncesi bizler tamponlu işler yapmışsak, "fork" sonrasında * bu tamponlar da alt prosese aktarılır. Dolayısıyla o tamponlar da * "flush" edilir. Fakat bu programda böyle bir işlem yapılmadığı için * "_exit()" ile değil "exit()" ile çıkılmıştır. */ exit_sys("read"); } close(pipefds[0]); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda başarısızlık durumunda ilgili boruların son durumuna tekrar tekrar bakılmaktadır. #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char** argv) { /* # OUTPUT # 1 2 ... >>> Child process: background processing... <<< >>> Parent process: background processing... <<< >>> Child process: background processing... <<< >>> Child process: background processing... <<< ... 100000 */ /* * 0. Index: Read * 1. Index: Write */ int pipefds[2]; if(pipe(pipefds) == -1) exit_sys("pipe"); /* * Okuma yapılacak taraf artık "non-blocking" modda. Artık boruda okunacak * en az bir bayt varsa okunacak, aksi halde "read" fonksiyonu başarısız * olacaktır. */ if(fcntl(pipefds[0], F_SETFL, fcntl(pipefds[0], F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); /* * Yazma yapılacak taraf artık "non-blocking" modda. Artık boruda yer kalmadığında * "write" fonksiyonu başarısız olacaktır. */ if(fcntl(pipefds[1], F_SETFL, fcntl(pipefds[1], F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0) /* Parent will write to the pipe. */ { close(pipefds[0]); for(int i = 0; i < 100000;) { if(write(pipefds[1], &i, sizeof(int)) == -1) { if(errno == EAGAIN) { /* * Borunun dolduğu anlaşılmaktadır. Arka planda bir takım * işler yapıldığı varsayılmıştır. Belli aralıklar ile boru * kontrol edilecektir. */ printf(">>> Parent process: background processing... <<<\n"); usleep(500); continue; } else exit_sys("write"); } ++i; } close(pipefds[1]); if(wait(NULL) == -1) exit_sys("wait"); } else /* Child will read from the pipe. */ { close(pipefds[1]); ssize_t result; int read_val; for(;;) { if((result = read(pipefds[0], &read_val, sizeof(int))) == 0) break; if(result == -1) if(errno == EAGAIN) { /* * Borunun boş anlaşılmaktadır. Arka planda bir takım * işler yapıldığı varsayılmıştır. Belli aralıklar ile boru * kontrol edilecektir. */ printf(">>> Child process: background processing... <<<\n"); usleep(500); continue; } else exit_sys("read"); // printf("%d\n", read_val); } printf("\n"); close(pipefds[0]); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Anımsanacağı üzere, isimli borularda, "open" fonksiyonu blokeli modda çalıştığında, ilgili borunun ters amaçla açılması gerçekleşene kadar ilgili "thread" i bloke etmekteydi. Eğer "open" fonksiyonuna "O_NONBLOCK" bayrağını da geçersek artık blokeye yol açmayacaktır. Şimdi bu bayrağın "open" sırasında kullanılması şöyle bir takım problemlere yol açacaktır: -> Eğer "O_NONBLOCK" bayrağı kullanılmasaydı, "read" yapacak olan proses, "open" fonksiyon çağrısı sırasında bloke olacaktı. Bu bayrak kullanıldığı için programın akışı "open" fonksiyonundan çıkacaktır eğer karşı taraf da "write" modda açmamış ise. Eğer bu aşamada "read" yapılırsa ve o boruya yazma potansiyeli olan bir "fd" de yok ise "read" fonksiyonu "0" ile geri dönecektir. -> Eğer "O_NONBLOCK" bayrağı kullanılmasaydı, "write" yapacak olan proses, "open" fonksiyon çağrısı sırasında bloke olacaktı. Bu bayrak kullanıldığı için "open" fonksiyonu başarısız olacaktır eğer o an karşı taraf boruyu "read" modda açmamış ise. Ek olarak "errno" değişkeni de "ENXIO" değerini alacaktır. Eğer "open" fonksiyonunun bu başarısızlığını kontrol etmez ve "write" yapmaya çalışırsak sinyal oluşacaktır. Dolayısıyla her iki prosesin de kullanacakları boruyu eş zamanlı açmaları gerekmektedir. Bunu sağlamak için de bizler bir senkronizasyon mekanizması geliştirmeliyiz. Örneğin, -> Proseslerden birisi "blocking" diğeri "non-blocking" modda olursa problem çözülecektir. -> Proseslerden birisini "sleep" benzeri fonksiyonlar ile bloke edebiliriz. O proses blokeli bir şekilde beklerken, karşı taraf işini halledeceği için, problem ortadan kalkacaktır. -> Proseslerin her ikisi de boruyu blokeli modda açtıktan sonra "fcntl" fonksiyonu ile modunu blokesiz hale getirmek. * Örnek 1, Aşağıdaki her iki proses de boruyu blokesiz modda açmıştır. Fakat "read" yapacak olan "getchar()" ile bloke edilmiştir. Böylelikle "write" yapan boruyu açtıktan sonra bizler "read" yapacak olanın blokesini kaldırmalıyız. /* write */ #include "stdio.h" #include #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { int fdpipe; if((fdpipe = open("mypipe", O_RDONLY | O_NONBLOCK)) == -1) exit_sys("open"); for(int i = 0; i < 100000;) { if(write(fdpipe, &i, sizeof(int)) == -1) { if(errno == EAGAIN) { printf(">>> Parent process: background processing... <<<\n"); usleep(500); continue; } else exit_sys("write"); } ++i; } close(fdpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include "stdio.h" #include #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { int fdpipe; if((fdpipe = open("mypipe", O_WRONLY | O_NONBLOCK)) == -1) exit_sys("open"); getchar(); ssize_t result; int read_val; for(;;) { if((result = read(fdpipe, &read_val, sizeof(int))) == 0) break; if(result == -1) if(errno == EAGAIN) { /* * Borunun boş anlaşılmaktadır. Arka planda bir takım * işler yapıldığı varsayılmıştır. Belli aralıklar ile boru * kontrol edilecektir. */ printf(">>> Child process: background processing... <<<\n"); usleep(500); continue; } else exit_sys("read"); // printf("%d\n", read_val); } printf("\n"); close(fdpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki her iki proses de boruyu blokeli modda açtıktan sonra modları blokesiz hale getirilmiştir. Artık "open" sırasında bloke olmakta, "read"/"write" sırasında bloke OLMAMAKTADIRLAR. /* write */ #include "stdio.h" #include #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { int fdpipe; if((fdpipe = open("mypipe", O_RDONLY)) == -1) exit_sys("open"); if(fcntl(fdpipe, F_SETFL, fcntl(fdpipe, F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); for(int i = 0; i < 100000;) { if(write(fdpipe, &i, sizeof(int)) == -1) { if(errno == EAGAIN) { printf(">>> Parent process: background processing... <<<\n"); usleep(500); continue; } else exit_sys("write"); } ++i; } close(fdpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include "stdio.h" #include #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { int fdpipe; if((fdpipe = open("mypipe", O_WRONLY)) == -1) exit_sys("open"); if(fcntl(fdpipe, F_SETFL, fcntl(fdpipe, F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); ssize_t result; int read_val; for(;;) { if((result = read(fdpipe, &read_val, sizeof(int))) == 0) break; if(result == -1) if(errno == EAGAIN) { /* * Borunun boş anlaşılmaktadır. Arka planda bir takım * işler yapıldığı varsayılmıştır. Belli aralıklar ile boru * kontrol edilecektir. */ printf(">>> Child process: background processing... <<<\n"); usleep(500); continue; } else exit_sys("read"); // printf("%d\n", read_val); } printf("\n"); close(fdpipe); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bir takım kabuk programlarını da boru haberleşmesi yöntemiyle haberleştirebiliriz. Örneğin, bir "shell" programına aşağıdaki komutu girelim: ls > mypipe Burada "mypipe" isimli boru dosyasının halihazırda var olduğu varsayılmıştır. Artık "ls" programı "stdout" a değil "mypipe" yazacaktır. Varsayılan ayarda "open" fonksiyonu blokeli olduğu için, bu "shell" programı aynı boru "read" amacıyla açılana kadar bloke edilecektir. Şimdi de şu aşağıdaki komutu başka bir "shell" programına girelim: cat < mypipe Normalde "cat" programı "stdin" den okuduklarını "stdout" a yazmaktadır. Artık "mypipe" den okuduklarını "stdout" a yazacaktır. > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları: UNIX/Linux sistemlerinde "IPC" fonksiyonları denildiğinde varsayılan durumda akla üç adet fonksiyon gelir. Bunlar Mesaj Kuyrukları, Paylaşılan Bellek Alanları ve Semafor fonksiyonlarıdır. Her ne kadar borular da birer "IPC" fonksiyonu olsalar da hal arasında öyle bilinmezler. Semaforlar senkronizasyonla alakalı olduklarından, bu başlık altında o konu işlenmeyecektir. "System 5 IPC" fonksiyonları 70'li yıllardan beridir POSIX dünyasında var olan fonksiyonlardır. Fakat bir takım eksik yönlerinden dolayı 90'lı yıllarda yenilenmiş versiyonları da POSIX dünyasına eklenmiştir. İşte 70'li yıllardan bari dilde var olan versiyonlarına "System 5 IPC", 90'lı yıllarda yenilenmiş hallerine ise "POSIX IPC" fonksiyonları denir. İşin özünde her ikisi de POSIX çatısı altındadır. Pekiyi yeni versiyonlarına neden "POSIX IPC" fonksiyonları denir? Aslında bakarsanız POSIX standartları kendi içerisinde uyum kategorilerine ayrılır. Örneğin, "XSI", "SHM" isimli uyum kategorisi. "XSI" uyum kategorisi evvelden beri var olan ve "UNIX Single Specification" u oluşturan kategoridir. Fonksiyonların imzalarının yanında destekledikleri uyum kategorisi de belirtilmiştir. "UNIX" ve "AT&T" nin "System 5" versiyonunda ilk defa bu mekanizma kullanıldığı için fonksiyonlara "System 5 IPC" fonksiyonları denmektedir. Halk arasında "XSI IPC" fonksiyonları denmektedir. 90'lı yıllarda dile eklenenlere ise POSIX dünyasında daha sonra katıldıkları için o ismi almışlardır. Özetlemek gerekirse 3 adet mekanizma bulunmaktadır: Mesaj Kuyrukları, Paylaşılan Bellek Alanları ve Semaforlar. Her ne kadar borular da bir "IPC" mekanizması olsalar da borular ayrı ele alınmaktadır. Bizler kurs sırasında "System 5" ve "POSIX" "IPC" fonksiyonlarını ilgili konular altında karşılıklı göreceğiz. İlgili fonksiyonların isimlendirmesi kabaca şu şekildedir: System 5 IPC - POSIX IPC Paylaşılan Bellek Alanları => shmget - shm_open Mesaj Kuyrukları => msgget - mq_open Semaforlar => semget - sem_open Bir sistem programcısı olarak her iki gruptakileri bilmek iyi olsa da "POSIX IPC" fonksiyonlarını kullanmalıyız. Öte yandan proseslerin "System 5 IPC" mekanizmasını kullanabilmesi için "key-value" kavramı kullanılmaktadır. Haberleşme yapacak olan prosesler "xxxget" fonksiyonuna sayısal bir değer olan "key" değerini argüman olarak geçmeleri halinde, ismine "ID" diyeceğimiz "value" değerini elde edeceklerdir. Aynı "key" değerini fonksiyona geçen iki farklı proses, aynı "value" değerine sahip olacaktır. Dolayısıyla haberleşme yapacak olan iki proses aynı "key" değerini "xxxget" fonksiyonuna geçmelidir. Bu fonksiyondan elde ettiğimiz "ID" değerini okuma/yazma yapmak için mekanizmadaki diğer fonksiyonlara geçeceğiz. İşte "System 5 IPC" mekanizmasının bir anomalisi burada ortaya çıkmaktadır. Ya bambaşka bir proses bizim kullanacağımız "key" değerini kullanmışsa? Bu da bazı kontrollerin yapılmasını gerektirmektedir. Öte yandan "xxxget" fonksiyonlarının bize döndürdüğü "ID" değeri sistem geneli eşsiz bir değerdir. Eğer bu "ID" değeri bir şekilde karşı prosese ulaştırılırsa, o proses "xxxget" yapmasına gerek kalmaz. Almış olduğu bu "ID" değerini direkt kullanabilir. Yine değinilmesi gereken bir diğer nokta da "xxxget" fonksiyonuna geçilen "key" değeri Mesaj Kuyruğu söz konusu olduğunda başka bir isim alanı, Paylaşılan Bellek Alanı olduğunda başka bir isim alanı ve Semaforlar olduğunda başka bir isim alanındadır. Ayrıca "System 5 IPC" nesneleri, boruların aksina, sistemin ilk "reboot" anına kadar sistemde kalmaktadır eğer biz "xxxctl" ile yok etmezsek. Yani proses sonlansa bile ilgili nesneler hayatta kalmaya devam etmektedir. Buna "Kernel Persistance" denmektedir. Fakat borular, prosesin sonlanmasıyla birlikte yol olmaktadır. >>>> Mesaj Kuyrukları: Bildiğiniz üzere borular "stream" tarzı haberleşme sunmaktadır. Yani bir taraf boruya bir miktar yazıyor, karşı taraf yazılanların bir miktarını okuyor vs. Bu haberleşmenin bir alternatifi ise paket tarzı haberleşmedir. Protokol ailelerinde buna "Datagram" haberleşmesi de denir. Bir taraf bir "paket" bilgiyi karşı tarafa gönderiyor, karşı taraf ise bu paketin TAMAMINI ALIYOR. Burada gönderilen paketin bir kısmını almak gibi bir durum söz konusu değildir. İşte mesaj kuyrukları böylesi bir haberleşme yapmaktadır. Mesaj adı altında bir grup paket, kuyruk halindedir. Şekil olarak bağlı liste örnek gösterilebilir. Öte yandan borular ise şekil olarak dizilere benzetilebilir. Mesaj kuyrukları iki proses arasında haberleşme yapmaktadır. >>>>> "System 5 IPC" Fonksiyonları: "msgget", "msgsend", "msgcrv" ve "msgctl" fonksiyonlarıdır. Bu fonksiyonların isimlerinin altı karakterden oluştuğuna dikkat ediniz. Bu fonksiyonların işlevleri sırasıyla şu şekildedir: Bir mesaj kuyruğu hayata getirmek ya da var olanı açmak, bir mesaj kuyruğuna bayt yazmak, bir mesaj kuyruğundan bayt okumak/almak, bir mesaj kuyruğu üzerinde bir takım işlemler gerçekleştirmek(silme işlemi de dahil). >>>>>> "msgget": Mesaj kuyruğu hayata getirir ya da halihazırdaki bir mesaj kuyruğunu açar. Bu yönüyle "open" fonksiyonuna benzetebiliriz. Fonksiyonun prototipi şöyledir: #include int msgget(key_t key, int msgflg); Fonksiyonun birinci parametresi tam sayı biçiminde bir "key" belirtmektedir. İkinci parametre ise şu bayrakları alabilir: -> IPC_CREAT: İlgili anahtara ilişkin bir mesaj kuyruğu varsa, olan mesaj kuyruğu açılır. Ancak yoksa, yeni bir mesaj kuyruğu oluşturulur. Buradaki semantik "open" fonksiyonundaki "O_CREAT" semantiğine benzemektedir. Öte yandan bu bayrak kullanılmışsa, yeni oluşturulacak mesaj kuyruğunun da erişim haklarını belirtmesi gerekmektedir. Bu fonksiyon üçüncü bir parametre almadığı için, erişim hakları da bu bayral ile "Bitwise-OR" işlemine sokulmalıdır. Burada kullanılacak olan erişim bayrakları, dosyalarda gördüğümüz "S_Ixxxx" olan bayraklardır. Tabii halihazırda bir mesaj kuyruğu varsa bu bayrakların bir önemi KALMAYACAKTIR. -> IPC_EXCL: Bu bayrak tek başına değil, "IPC_CREAT" ile birlikte, "IPC_CREAT | IPC_EXCL | ..." biçiminde kullanılır. İlgili "key" değerine karşılık gelen bir mesaj kuyruğu yoksa, yani sıfırdan bir mesaj kuyruğunu açmak için kullanılır. Eğer bu "key" değerine karşılık gelen mesaj kuyruğu varsa fonksiyon başarısız olacaktır. Fonksiyon başarısız olduğunda da "errno" değişkeni "EEXIST" değerini alır. Yine buradaki semantik de "open" fonksiyonundaki "O_EXCL" bayrağı gibidir. Haberleşecek her iki proses de bu bayrağı kullanabilir. -> Bu parametreye "0" değerini geçebiliriz. Dolayısıyla var olan açılacaktır. Fonksiyonun örnek kullanımı aşağıdaki gibi olabilir: ... msgid = msgget(0x12345, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); ... Tabii buradaki "S_Ixxx" bayraklarına karşılık sayısal değerler de getirilmiştir. Dolayısıyla yukarıdaki işlemi şu şekilde olabilir: ... msgid = msgget(0x12345, IPC_CREAT | 0644); ... /*================================================================================================================================*/ (41_26_03_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları (devam): >>>> Mesaj Kuyrukları (devam): >>>>> "System 5 IPC" Fonksiyonları (devam): Bir sistemdeki "System 5 IPC" nesnelerini "shell" programı üzerinden "ipcs" komutunu çalıştırarak görebiliriz. Unutulmamalıdır ki bu nesneler prosesler sonlansa bile ömürlerine devam eder. Ta ki sistem "reboot" gerçekleştiğinde ya da "msgctl" fonksiyonu ile silinirler. "ipcs" komutu ekrana bastığı "key" değerleri için 16'lık tabanı kullanmaktadır. >>>>>> "msgget" (devam) : Bu fonksiyonun birinci parametresine "IPC_PRIVATE" geçmemiz durumunda, daha önce kullanılmamış bir "key" değerinin geçildiği varsayılacaktır. Artık bu durumda "key" çakışmasının önüne geçilmiş olacak. Fakat fonksiyonun geri döndürdüğü "id" değerini de karşı prosese bir şekilde ulaştırmamız gerekmektedir çünkü karşı fonksiyon da bu bayrağı kullanırsa, o farklı bir "id" değerini elde edecektir. Dolayısıyla bu bayrak pek bir kullanışlı kalmamaktadır. Karşı tarafa göndermek için başka bir haberleşme yöntemi, komut satırı argümanı olarak gönderim vb. yöntemler izlenebilir. "msgget" fonksiyonu başarısız olduğunda "-1" ile başarılı olduğunda da pozitif bir "id" değerine geri döner. * Örnek 1, Aşağıda bu fonksiyonun kullanımına dair bir örnek verilmiştir. #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>>>> "msgsend": Mesaj kuyruğuna bir mesaj göndermek için kullanılır. Prototipi şöyledir: #include int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); Fonksiyonun birinci parametresi, "msgget" fonksiyonuna "key" vererek elde ettiğimiz, "id" değeridir. Bu "id" değeri sistem geneli tekil olduğu için ve proses bu "id" değerini zaten biliyorsa, tekrardan "msgget" çağrısına gerek yoktur. İkinci parametresi ise gönderilecek bayt yığınının başlangıç adresidir. Üçüncü parametre ise mesajın uzunluğudur, ilgili bayt yığınının büyüklüğü DEĞİL. Kuyruğa yazılacak olan bayt yığınları şu şekilde oluşturulması bir ZORUNLULUKTUR: -> Mesajın ilk kısımları "long" türden olacak ve mesajın türü anlamına gelecek. Yani o mesajın "id" değerini bizler bayt yığınının baş kısmında bulundurmak zorundayız. Bu "id" değerinin "0" dan büyük olması gerekmektedir. -> Yukarıdaki "long" alanın bittiği yerde göndermek istediğimiz mesajın baytları gelmelidir. Böylelikle "long" alan ile bizim göndereceğimiz mesajın baytları ardışıl olmalıdır. Bu şartları sağlamanın en kolay yolu bir "struct" kullanmaktır. Çünkü "struct" ın ilk elemanı, aynı zamanda o "struct" ın başlangıç adresidir ve "struct" içerisindeki elemanların baytları da ardışıl vaziyettedir. Bir diğer alternatif yöntem ise bir "char" türden dizi oluşturup "memcpy" fonksiyonu ile bu diziye baytları yazmaktır. Aşağıda bu "struct" için bir örnek verilmiştir: struct MYMSG{ long mtype; // Mesajın türü. Bir nevi bu mesajın "id" değeridir. char msg[1024]; // Mesajın kendisi. "msgsnd" fonksiyonunun üçüncü parametresine bu büyüklük geçilecek. }; Yukarıdaki yapının son elemanı olan dizinin boyutunu, C99 sonrası dönemde "flexible array member" olarak, yazmayabiliriz. Bu eleman için bir alan tahsisi yapılmıyor. Aksi halde ilgili dizinin boyutunu ya "1024" olarak takribi belirtmeli ya da "1" belirtip daha sonra dinamik bellek yönetimi ile daha sonra büyütmeliyiz. Aşağıda ise ilgili dizinin uzunluğunun "1" seçilmesi durumuna dair bir örnek verilmiştir: //... struct MSG{ long mtype; char msg[1]; }; //... int main() { //... struct MSG* msg; msg = (struct MSG*)malloc(sizeof(long) + n); msg->mtype = 1; memcpy(msg->msg, /*message content*/, ...); //... free(msg); } Görüldüğü üzere dinamik bellek yönetimi biraz zahmetli. Bu zahmete girmemek adına, mesajın uzunluğu takribi 8192 olarak seçilebilir. Öte yandan bu yapının ilk elemanını mesajın "id" değeri olarak kullanılabileceğinden bahsetmiştir. Pekiyi nedir bu mesajın "id" değeri mevzusu? Burada mesajı alan taraf işin içine girmektedir. Şöyleki; -> Mesajı alan taraf spesifik bir "id" değerine ilişkin mesajları almak isteyebilir. Bu yönüyle "client-server" haberleşmesinde kullanılabilir. -> Mesajı alan taraf en yüksek "id" değerine sahip olanı ilk olarak almak isteyebilir. Bu yönüyle "priority queue" gibi de kullanılır. Son parametre ise gönderim sırasında kullanılabilen özel bir bayraktır. Bu bayrak "Bitwise-OR" işlemine tabi tutulabilir. Fakat POSIX standartlarınca sadece tek bir bayrak vardır o da "IPC_NOWAIT" bayrağıdır. Pekiyi bu bayrak ne işe yarar? Normalde mesaj kuyruğu doluysa, bu fonksiyon blokeye yol açmaktadır. Bu bayrağı kullanırsak blokede beklemeyiz ve fonksiyon başarısız olup "-1" ile geri döner ve "errno" değişkeni de "EAGAIN" değerine çekilir. Bu bayrağı kullanmak istemiyorsak, "0" geçebiliriz. Ek olarak belirtmek gerekir ki mesaj kuyruklarının da bir limiti vardır. Bu limit değerine ulaşıldığında, iş bu fonksiyon eğer "IPC_NOWAIT" bayrağı olmadan çağrılmışsa, prosesin bloke olmasına neden olur. Aksi halde fonksiyon başarısız olacaktır. Velevki mesaj kuyruğunda yer açılırsa bloke kalkar. Görüyoruz ki tıpkı borulardaki senkronizasyon burada da sağlanmış durumdadır. Son olarak "msgsnd" fonksiyonu, başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri döner ve "errno" uygun değeri alır. * Örnek 1, Aşağıdaki örnek "msgget" sırasında kullanılan örneğin üzerine eklenmiştir: #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; int value; }; int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; for(int i = 0; i < 10; ++i) { /* * Eğer mesajın "id" değeri önemsiz ise "0" hariç böylesi sabit bir değer geçebiliriz. */ msg.mtype = 1; msg.value = i; /* * Fonksiyonun; * Üçüncü parametresine ilgili mesajın uzunluğunun geçildiğine dikkat ediniz. * Dördüncü parametresine "0" geçildiği için "blocking" modda çalışacaktır. */ if(msgsnd(msgid, &msg, sizeof(int), 0) == -1) exit_sys("msgsnd"); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>>>> "msgrcv" : Mesaj kuyruğundan bir mesaj okumak için kullanılır. Fonksiyonun prototipi şöyledir: #include ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); Fonksiyonun birinci parametresi, "msgget" fonksiyonuna "key" vererek elde ettiğimiz, "id" değeridir. Bu "id" değeri sistem geneli tekil olduğu için ve proses bu "id" değerini zaten biliyorsa, tekrardan "msgget" çağrısına gerek yoktur. Fonksiyonun ikinci ve üçüncü parametreleri, kuyruktan alınan mesajın yerleştirileceği alanın başlangıç adresi ve mesajın uzunluk bilgisidir. Tabii ikinci parametreye geçilen adresin ilk kısmı, yine "long" türden olmalıdır. Bu "long" türden sonra bizim mesajın başladığı adres gelmektedir. Özetle; ikinci parametre ilgili "struct" yapısının adresiyken, üçüncü parametre ise bu "struct" içerisindeki mesajın uzunluk bilgisidir. İkinci ve üçüncü parametrede kullanılacak "struct" aşağıdaki gibi olabilir: struct MSG{ long mtype; char msg[8192]; }; Kuyruktaki mesajın içeriği yukarıdaki yapınıın içerisine yerleştirilecektir. Fonksiyonun dördüncü parametresi ise alınacak mesajların "id" bilgisini belirtmektedir. Bu parametre aşağıdaki durumlardan birisini karşılamalıdır: -> "0" geçilmesi durumunda, kuyrukta "FIFO" sistemi uygulanacaktır. Yani sıradaki ilk mesaj okunacaktır. "0" değerinin geçerli bir "id" değeri olmadığını UNUTMAYINIZ. -> "0" dan büyük pozitif bir değer geçilirse, o değere sahip ilk mesaj kuyruktan okunur. Örneğin, bu parametreye "100" geçilmiş olsun. Kuyruğun muhtelif yerlerinde de bu "id" değerine sahip birden fazla mesaj paketi olsun. Bizler en öndeki paketi almış olacağız. -> "0" dan küçük negatif bir değer geçilirse, "Priority Queue" sistemi uygulanacaktır. Şöyleki; geçilen negatif değerin mutlak değeri alınır. İş bu değere eşit ya da bu değerden küçük olan en küçük "id" değerine sahip paket ilk okunur. Örneğin, "-10" değeri geçilmiş olsun. Kuyruktaki mesajların "id" değerleri de aşağıdaki gibi olsun: 20 5 30 2 8 40 Mutlak değeri alındığında, "10" değeri elde edilmiş olur. İlk okumada "10" dan küçük en düşük "id" değeri olan "2" okunur. Daha sonra "5" ve en sonunda "8". Bu aşamada tekrar okuma yapılırsa fonksiyon blokeye yol açacaktır. Çünkü uygun "id" değerine sahip mesaj paketi kalmamıştır. Burada en düşük "id" değerine sahip olan paket en öncelikli paket haline gelmiştir. Fonksiyonun son parametresi ise mesajın alımına ilişkin bayrak değeridir. Buraya şu aşağıdaki bayraklar geçilebilir: -> "IPC_NOWAIT" : Artık ilgili fonksiyon "non-blocking" modda çalışacaktır. Kuyrukta okunacak uygun "id" değerine sahip mesaj kalmadıysa, fonksiyon başarısız olacaktır. "errno" değeri de "EAGAIN" değerine çekilir. -> "MSG_NOERROR" : Kuyruktaki paketin içerisindeki mesajın uzunluğu, bu fonksiyonun üçüncü parametresiyle geçtiğimiz uzunluktan büyükse, ilgili mesaj kırpılarak alınır. Böylelike parçalı okuma yapılmış olur. Bu bayrak bunu mümkün kılmaktadır. Bu bayrak kullanılmazsa ve yukarıdaki durum gerçekleştiğinde, fonksiyon başarısız olacaktır. "errno" değeri de "E2BIG" değerine çekilir. -> "0" geçilmesi durumunda, yukarıdaki semantikler uygulanmayacaktır. Fonksiyonun geri dönüş değeri ise başarı durumunda alınan paketin içerisindeki mesajın uzunluğu, başarısızlık durumunda ise "-1" değerindedir. Bu noktada bir hatırlatma yapmak gerekir ki mesaj kuyruklarında karşı tarafın mesaj kuyruğunu kapatması gibi bir durum SÖZ KONUSU DEĞİLDİR. Kuyrukta okunacak uygun paket kalmadıysa, okuma yapan taraf ya bloke olacaktır ya da başarısız olup sonlanacaktır. * Örnek 1, Aşağıdaki örnek "msgget" sırasında kullanılan örneğin üzerine eklenmiştir: #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; int value; }; int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; for(;;) { /* * Aşağıda son parametre "0" geçildiği için, alınan mesajın kırpılırsa, * fonksiyon başarısız olacaktır. Dolayısıyla kaç bayt alındığının bir * önemi kalmamıştır. */ if(msgrcv(msgid, &msg, sizeof(int), 0, 0) == -1) exit_sys("msgrcv"); printf("%ld. [%d]\n", msg.mtype, msg.value); /* Bu aşamada okunacak uygun mesaj paketi yok ise proses bloke olacaktır.*/ } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıda "IPC_NOWAIT" bayrağının kullanımına bir örnek verilmiştir: #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; int value; }; int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; for(;;) { /* * Aşağıda son parametre "IPC_NOWAIT" geçildiği için bloke olmayacaktır. */ if(msgrcv(msgid, &msg, sizeof(int), 0, IPC_NOWAIT) == -1 && errno == EAGAIN) break; else exit_sys("msgcrv"); printf("%ld. [%d]\n", msg.mtype, msg.value); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte ise özel bir mesaj alınması taktirde kuyruktan okuma işlemi sonlanacaktır: #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; int value; }; int main(int argc, char** argv) { int msgid; /* * Aşağıda bu "key" değerine ilişkin bir mesaj kuyruğu yok ise yeni bir tanesi * oluşturulacaktır. */ if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; for(;;) { if(msgrcv(msgid, &msg, sizeof(int), 0, 0) == -1) exit_sys("msgcrv"); /* Paket içerisindeki mesaj "31" ise okuma sonlanacaktır. */ if(msg.value == 31) break; printf("%ld. [%d]\n", msg.mtype, msg.value); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de mesaj kuyruklarını kullanarak iki prosesi haberleştirelim. Bu iki prosesten birisi klavyeden okuduklarını kuyruğa yazacak, diğeri de kuyruktakileri okuyarak ekrana basacaktır. * Örnek 1, Aşağıda "FIFO" semantiği uygulanmıştır. /* write */ #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; msg.mtype = 1; // Bu değer ile işimiz olmadığından, baştan "1" atıyoruz. char* str; for(;;) { printf("Message Text: "); fflush(stdout); /* * Klavyeden okuduklarımızı ilgili diziye yazıyoruz. * Burada '\n' karakteri de diziye alınmaktadır. */ if(fgets(msg.buffer, 8192, stdin) == NULL) continue; /* * Daha sonra bu '\n' karakterini '\0' ile değiştiriyoruz. */ if((str = strchr(msg.buffer, '\n')) != NULL) *str = '\0'; /* * "strlen" fonksiyonu yazının sonundaki '\0' karakterini * dahil etmediği için, o karakter kuyruğa aktarılmayacaktır. * Eğer bu karakteri de dahil etmek isteseydik, şöyle bir uzunluk * girmeliydik => " strlen(msg.buffer) + 1" */ if(msgsnd(msgid, &msg, strlen(msg.buffer), 0) == -1) exit_sys("msgsnd"); /* "quit" yazısı paketlenirse, bu proses sonlanacaktır. */ if(!strcmp(msg.buffer, "quit")) break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; ssize_t result; for(;;) { if((result = msgrcv(msgid, &msg, 8192, 0, 0)) == -1) exit_sys("msgcrv"); /* "quit" yazısı alınırsa, bu proses sonlanacaktır. */ if(!strcmp(msg.buffer, "quit")) break; /* * Karşı taraftan '\0' karakteri gönderilmediği için, * bizler eklemek istiyoruz. Çünkü birazdan ekrana basacağız. * Eğer karşı taraf bu karakteri de gönderseydi, bu satıra ve * "result" değerine gerek kalmayacaktı. */ msg.buffer[result] = '\0'; printf("%ld. [%s]\n", msg.mtype, msg.buffer); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıda ise "Priority Queue" semantiği uygulanmıştır. /* write */ #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); void clear_stdin(void); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; char* str; for(;;) { printf("Message Type: "); scanf("%ld", &msg.mtype); /* * "scanf" fonksiyonu klavyeden girilen '\n' * karakterini almayacaktır. Dolayısıyla bizler * "stdin" tamponunu boşaltmalıyız. */ clear_stdin(); printf("Message Text: "); fflush(stdout); if(fgets(msg.buffer, 8192, stdin) == NULL) continue; if((str = strchr(msg.buffer, '\n')) != NULL) *str = '\0'; /* Yazının sonundaki '\0' karakteri de gönderilecektir. */ if(msgsnd(msgid, &msg, strlen(msg.buffer) + 1, 0) == -1) exit_sys("msgsnd"); if(!strcmp(msg.buffer, "quit")) break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void clear_stdin(void) { while(getchar() != '\n'); } /* read */ #include #include #include #include #include #include #define MSG_KEY 0x1234567 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; ssize_t result; for(;;) { /* * Dördüncü parametre negatif bir sayı olduğundan, * "id" değeri en küçük olan paket ilk alınacaktır. * Böylelikle "FIFO" yerine "Priority Queue" semantiği * uygulanmış oldu. */ if((result = msgrcv(msgid, &msg, 8192, -100, 0)) == -1) exit_sys("msgcrv"); if(!strcmp(msg.buffer, "quit")) break; printf("%ld. [%s]\n", msg.mtype, msg.buffer); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de mesaj kuyrukları kullanılarak "Client-Server" haberleşmesi yapalım. Burada toplamda iki adet mesaj kuyruğu yeterli gelecektir. Bir tanesi "Client" lardan "Server" a, diğer ise "Server" dan "Client" a. Bu uygulama aşağıdaki şu şartları sağlayacaktır: -> "Client" programlar kendi proses "id" değerlerini, gönderilecek mesaj paketlerinin "id" değeri olarak kullanacaklardır. Aslında mesajın tür değeri, o prosesin "id" değeri oluyor. -> Paketlenen mesajlar, "ServerQueue" diyeceğimiz bir mesaj kuyruğuna gönderilsin. -> "Server" programın "FIFO" semantiğini uygulamasını isteyelim. Dolayısıyla "msgrcv" fonksiyonunun dördüncü parametresi "0" olmalıdır. Sırayla aldığı paketlerdeki istekleri yerine getirir. -> İsteklerin cevaplarını tekrar paket haline getirir. Bu paketlerin "id" değerleri, yine "client" programların proses "id" değerleri olur. -> Paketlenen mesajlar bu sefer başka bir mesaj kuyruğuna eklenir. İsmini de "ClientsQueue" diyelim. -> "ClientsQueue" den okuma yapacak olan "Client" ler, "msgrcv" fonksiyonunun dördüncü parametresine kendi proses "id" değerlerini geçerler. Böylelikle kendilerini ilgilendiren paketleri çekerler. > Hatırlatıcı Notlar: >> "proc" dosya sistemi bellekte oluşturulur. "kernel" tarafından yapılan işlemleri dış dünyaya bildirmek için kullanılır. >> "flexible array member": Bir yapının son elemanı bir diziyse, bu dizinin boyutunu belirtmek zorunda değiliz. Buradaki tek ön koşul, yapının son elemanının bir dizi olması. Tabii ilgili derleyicinin C99 standartlarını desteklemesi gerekmektedir. >> "FIFO", "First In First Out" mottosunu güderken "Priority Queue" ise önceliği yüksek olanın ilk sırada olduğu sistemlerdir. >> Borular "stream" tabanlıyken, mesaj kuyrukları paket tabanlıdır. >> "stdin" den okuma yapıldığında "stdout" tamponu boşaltılıyor fakat garanti altına alınmamıştır. /*================================================================================================================================*/ (42_01_04_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları (devam): >>>> Mesaj Kuyrukları (devam): >>>>> "System 5 IPC" Fonksiyonları (devam): Daha önceki derste "Client-Server" haberleşmesi için mesaj kuyrukları kullanılacaksa iki adet mesaj kuyruğunun kullanılması gerektiğinden bahsetmiştik. Pekiyi bizler sadece ve sadece bir adet mesaj kuyruğu kullansak nasıl olur? Teknik olarak böyle bir şey mümkündür. "Client" programlar yine kendi proses "id" değerlerini gönderilecek mesaj paketlerniin "id" değeri olarak kullanırlar. "Server" ise yanıtları yine aynı "id" değerini kullanarak aynı kuyruğa yazar. Fakat bu yöntem beraberinde "deadlock" problemini de getirebilir. >>>>>> "deadlock": Her ne kadar bu konu ileride ele alınacak olsa, kısaca şöyle bir açıklama yapılabilir; Birisi birisini beklerken, başka birisi de başka birisini beklemek durumudur. Yani prosesler birbirlerini bekledikleri için sistem kitleniyor. Bu sorun bir senkronizasyon problemidir ve dikkatsizlik sonucu ortaya çıkar. Örneğin, "Server" program almış olduğu isteği işlerken diğer "Client" programlar kuyruğu istekler ile doldururlar. Sonuçta mesaj kuyruklarının da bir limiti vardır ve kuyruğa yazma fonksiyonu blokeli modda kullanıldığında ilgili prosesin bloke olmasına yol açarlar. Artık kuyruk dolduğu için "Server" program isteğin cevabını da yazamaz. Öte yandan, dikkat edilmezse, "Client" program kuyruğa yazdığı kendi paketi okuyabilir. Bu da başlı başına bir problemdir. Ek olarak bahsetmek gerekirse; iki kuyruklu model de kullansak, tek kuyruklu model de kullansak veya her bir "Client" için ayrı bir mesaj kuyruğu da oluşturak, bir "Client" kuyruktan mesaj okumaz ise o kuyruk dolağı için kilitlenme oluşacaktır. Tabii "Client" programları da bizler yazacağımız için böylesi bir genellikle durum gerçekleşmez. Ancak "Client" programlar, yanlış programlama tekniklerinden dolayı bir şekilde bloke olsalar, yine kilitlenme gerçekleşebilir ve diğer "Client" programlar bu durumdan olumsuz etkinelebilir. Dolayısıyla hangi yöntemin kullanılacağı işin başında tartışılmalıdır. Şimdi de elimizdeki en son "System 5 IPC" fonksiyonu olan "msgctl" fonksiyonunu inceleyelim: >>>>>> "msgctl": Bu fonksiyon, mesaj kuyrukları üzerinde bir takım işlemleri yapabilmemize olanak sağlar. Fonksiyonun prototipi şu şekildedir: #include int msgctl(int msqid, int cmd, struct msqid_ds *buf); Fonksiyonun birinci parametresi üzreinde işlem yapılacak mesaj kuyruğununun "id" değeridir. Bildiğiniz üzere bu "id" değeri "msgget" fonksiyonuna bir "key" değeri geçilerek elde ediliyordu. Fonksiyonun ikinci parametresi ise mesaj kuyruğuna uygulanacak işleme dair parametredir. Bu parametre şu değerlerden birisi olmalıdır: "IPC_STAT", "IPC_SET" ve "IPC_RMID". Fonksiyonun üçüncü parametresi ise "msqid_ds" türünden bir yapının adresidir. Bu yapı "sys/msg.h" isimli başlık dosyasında tanımlanmış olup aşağıdaki şekildeki gibidir: structure msqid_ds { struct ipc_perm msg_perm // Operation permission structure. msgqnum_t msg_qnum // Number of messages currently on queue. msglen_t msg_qbytes // Maximum number of bytes allowed on queue. pid_t msg_lspid // Process ID of last msgsnd(). pid_t msg_lrpid // Process ID of last msgrcv(). time_t msg_stime // Time of last msgsnd(). time_t msg_rtime // Time of last msgrcv(). time_t msg_ctime // Time of last change. }; Şimdi de "msqid_ds" yapı türünün elemanlarını sırasıyla inceleyelim: >>>>>>>> "msg_perm" : Bu elemanın türü "ipc_perm" yapı türündendir. Bu tür, "sys/ipc.h" isimli başlık dosyasında şu şekilde tanımlanmıştir: struct ipc_perm { uid_t uid // Owner's user ID. gid_t gid // Owner's group ID. uid_t cuid // Creator's user ID. gid_t cgid // Creator's group ID. mode_t mode // Read/write permission. }; Şimdi de "ipc_perm" yapı türünün elemanlarını sırasıyla inceleyelim: >>>>>>>>>> "uid" : Mesaj kuyruğunda en son "msgctl" ile değişiklik yapan prosesin "Etkin Kullanıcı ID" değerini göstermektedir. >>>>>>>>>> "gid" : Mesaj kuyruğunda en son "msgctl" ile değişiklik yapan prosesin "Etkin Grup ID" değerini göstermektedir. >>>>>>>>>> "cuid": Mesaj kuyruğunu ilk oluşturan prosesin "Etkin Kullanıcı ID" değerini göstermektedir. >>>>>>>>>> "cgid": Mesaj kuyruğunu ilk oluşturan prosesin "Etkin Grup ID" değerini göstermektedir. >>>>>>>>>> "mode": Mesaj kuyruğunun sahip olduğu erişim haklarını belirtmektedir. >>>>>>>> "msg_stime" : Mesaj kuyruğuna yapılan en sonki "msgsnd" işleminin tarihini belirtmektedir. "epoch" dan geçen zamandır. >>>>>>>> "msg_rtime" : Mesaj kuyruğuna yapılan en sonki "msgrcv" işleminin tarihini belirtmektedir. "epoch" dan geçen zamandır. >>>>>>>> "msg_ctime" : Mesaj kuyruğuna yapılan en sonki "msgctl" işleminin tarihini belirtmektedir. "epoch" dan geçen zamandır. >>>>>>>> "msg_qnum" : Mesaj kuyruğunda o anda bulunan mesaj paket sayısını verir. Bu elemanın türü "unsigned integer" olmak zorundadır. En azından "unsigned short" türünden olmalıdır. >>>>>>>> "msg_qbytes": Mesaj kuyruğundaki paketlerin içerisindeki mesajların toplam uzunluğunu verir. "MSGMNB" değeri bu değerin limit değeridir. Bu elemanın türü "unsigned integer" olmak zorundadır. En azından "unsigned short" türünden olmalıdır. >>>>>>>> "msg_lspid" : Mesaj kuyruğuna en son "msgsnd" yapan prosesin "id" değerini verir. >>>>>>>> "msg_lrpid" : Mesaj kuyruğundan en son "msgrcv" yapan prosesin "id" değerini verir. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri dönmektedir. Pekiyi bütün bunlar ne anlama gelmektedir? Fonksiyonun ikinci parametresi "IPC_STAT", "IPC_SET" ve "IPC_RMID" değerlerinden birisini geçmemiz gerekmektedir. Şimdi de bu değerlerin ne anlama geldiğine bakalım: >>>>>>> "IPC_STAT" : İkinci parametreye bu değeri geçtiğimiz zaman, ilgili fonksiyonun üçüncü parametresine geçilen "msqid_ds" yapısının elemanları, üzerinde çalışılan mesaj kuyruğunun bilgileri ile doldurulur. * Örnek 1, Aşağıdaki örnekte "msgctl" fonksiyonunun ikinci parametresine "IPC_STAT" değeri geçilerek üzerinde çalışılan mesaj kuyruğununun bilgileri elde edilmiştir. #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; msg.mtype = 1; strcpy(msg.buffer, "test"); if(msgsnd(msgid, &msg, 4, 0) == -1) exit_sys("msgsnd"); struct msqid_ds msginfo; if(msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); /* * "msginfo.msg_qbytes" ve "msginfo.msg_qnum" isimli elemanlar en az "unsigned short" * olmak zorundalar fakat şu anki sistemde neye karşılık gelmediğinden, C11 ile dile * eklenen "uintmax_t" türünü kullandık. Bu tür, o sistemdeki en geniş "unsigned integer" * türüne denk gelmektedir. */ printf("Maximum # of bytes: %ju\n", (uintmax_t)(msginfo.msg_qbytes)); printf("Maximum # of message packages: %ju\n", (uintmax_t)(msginfo.msg_qnum)); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>>>>> "IPC_SET" : İkinci parametreye bu değeri geçtiğimiz zaman üzerinde çalışılan mesaj kuyruğunun bazı bilgilerini değiştirebiliriz. Sadece şu bilgiler değiştirilebilir: "msg_perm.uid", "msg_perm.gid", "msg_perm.mode" ve "msg_qbytes". Bu elemanlar aslında "msqid_ds" türündeki yapının "msg_perm" isimli elemanlarıdır. Bahsi geçen elemanların isimlerinden de anlaşılacağı üzere, bu elemanlar erişim kontrolü için kullanılan elemanlardır. Ek olarak "IPC_SET" bayrağını kullanabilmemiz için prosesimizin ya "appropriate priviledged" olması ya da "Etkin Kullanıcı ID" değerinin "msqid_ds" yapısının "msg_perm" isimli elemanının "cuid" veya "uid" değerine eşit olması gerekmektedir. Öte yandan prosesimiz ilgili mesaj kuyruğuna "write" hakkı varsa kuyruğa mesaj paketi gönderebilir, "read" hakkı varsa kuyruktan mesaj paketi okuyabilir. Fakat "owner", "group" ve "other" kontrolü şu şekilde sağlanmaktadır: -> "msqid_ds" yapısının "msg_perm" isimli elemanının "uid" isimli elemanı ile bizim prosesimizin "Etkin Kullanıcı ID" değeri aynıysa, bizler mesaj kuyruğu için "owner" statüsündeyiz. -> "msqid_ds" yapısının "msg_perm" isimli elemanının "gid" isimli elemanı ile bizim prosesimizin "Etkin Grup ID" değeri aynıysa, bizler mesaj kuyruğu için "group" statüsündeyiz. -> Yukarıdaki şartlar karşılanmamışsa, bizler "other" statüsündeyiz. Son olarak "IPC_SET" işlemi yapılırken "msqid_ds" yapısının diğer elemanları dikkate alınmamaktadır. Tabii sadece belli bir değeri değiştireceksek, önce "IPC_STAT" yapmalı daha sonra "IPC_SET" uygulamalıdır. Çünkü "IPC_SET" işlemi elemanların hepsine etki etmektedir. * Örnek 1, #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct msqid_ds msginfo; if(msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); printf("Maximum # of bytes: %ju\n", (uintmax_t)(msginfo.msg_qbytes)); msginfo.msg_qbytes = 3000; /* * Bu işlemi yapabilmemiz için prosesimizin bir takım yetkinliklere veya mesaj kuyruğunu * oluşturan proses olmalı ya da mesaj kuyruğunda en son "stat" işlemi uygulamış bir proses * olmalıdır. Aksi halde değiştirme işlemi başarısız olacaktır. */ if(msgctl(msgid, IPC_SET, &msginfo) == -1) exit_sys("msgctl"); if(msgctl(msgid, IPC_STAT, &msginfo) == -1) exit_sys("msgctl"); printf("Maximum # of bytes: %ju\n", (uintmax_t)(msginfo.msg_qbytes)); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>>>>>> "IPC_RMID" : Bir mesaj kuyruğunu silmek için kullanılır. İkinci parametreye bu değer geçildiğinde "msgctl" fonksiyonunun üçüncü parametresine neyin geçildiği önemli kalmaktadır. Fakat bazı sistemler üçüncü parametreye geçilen değeri "ignore" ederken bazıları etmemektedir. POSIX standartları "ignore" etmektedir. "IPC_SET" yapabilmek için prosesimizin sahip olması gereken "id" değerleri, bu işlemi yapmak için de geçerlidir. Yani prosesimiz prosesimizin ya "appropriate priviledged" olması ya da "Etkin Kullanıcı ID" değerinin "msqid_ds" yapısının "msg_perm" isimli elemanının "cuid" veya "uid" değerine eşit olması gerekmektedir. Eğer bir mesaj kuyruğu bu işlem ile silinmez ise sistem "reboot" olduğunda yine silinecektir. Son olarak belirtmekte fayda varki bir proses mesaj kuyruğu üzerinde işlem yaparken bir başka proses mesaj kuyruğunu silerse, ilgili mesaj kuyruğu anında silinecektir. Dolayısıyla mesaj kuyruğu üzerinde işlem yapan diğer prosesler de hata durumunda düşecektir. Burada, dosyalardaki silme işleminin aksine, en son proses de sonlandıktan sonra gerçek silme GERÇEKLEŞMİYOR. DİREKT OLARAK SİLİNİYOR. * Örnek 1, #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 void exit_sys(const char*); int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); if(msgctl(msgid, IPC_RMID, NULL) == -1) exit_sys("msgctl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan mesaj kuyruklarının da bir limiti vardır. Pekiyi bu limit değerleri nelerdir? POSIX standartları bu limitlerin neler olduğundan BAHSETMEMİŞTİR. Sadece bir limitin olduğundan bahsetmiştir. Öte yandan Linux sistemleri üç adet limit kullanmaktadır. Bu limitler sırasıyla şunlardır; MSGMAX, MSGMNI, MSGMNB. Şimdi bu limitlerin detaylarına bakalım: >>>>> MSGMAX: Bir mesaj paketi içerisindeki mesajın toplam uzunluğu. Mesajın tür bilgisi hariç, bizzat mesajın kendisinin tutulduğu alanın büyüklüğüdür burada kastedilen. Tipik olarak bu uzunluk 8192'dir. Bu değer "proc" dosya sistemindeki "/proc/sys/kernel" dizini içerisindeki "maxmsg" isimli dosya üzerinden değiştirilebilir. Tabii bunu yapabilmek için prosesin "appropriate priviledged" olması gerekmektedir. >>>>> MSGMNI: Bu limit sistem geneli oluşturulacak mesaj kuyruklarının maksimum adedini belirtmektedir. Yine bu değeri "/proc/sys/kernel" dizini içerisindeki "msgmni" isimli dosyadan elde edebiliriz. Tipik olarak bu uzunluk 32000'dir. >>>>> MSGMNB: Bu limit, bir mesaj kuyruğunda bulunan paketlerin içerisindeki mesajların toplam uzunluk değeridir. "msgsnd" fonksiyonu blokeli modda çalışırken bu değer açılırsa, bloke oluşur. Yine bu değeri "/proc/sys/kernel" dizinindeki "msgmnb" isimli dosyadan elde edilebilir. Tipik olarak değeri 16384 bayttır. Bizler bu değerleri "kernel" derlemesi sırasında değiştirilebiliriz. Bütün bunlar bir yana, borulardaki sıfır bayt yazma durumu mesaj kuyrukları için de geçerli midir? Burada boruların aksine mesaj kuyruğuna sıfır bayt yazılması özel bir şeydir. Yine bir paket oluşturulup kuyruğa yazım gerçekleşiyor. Dolayısıyla kuyruğu doldurabiliriz. Bu tür paketlerin içerisindeki mesajların uzunluğu işlemci tarafından bir baytmış gibi ele alınır. Pekiyi böylesi mesajlar neden gönderilir? En sık karşılaşılan yöntem, haberleşmenin sonlanma bilgisini karşı tarafa iletmek içindir. Yukarıdaki örnekte "quit" yazısı gönderildiğinde haberleşme sonlanmaktaydı. Bunun yerine kullanabiliriz. * Örnek 1, Aşağıda sıfır bayt yazma iletişimi sonlandırmak amacıyla gönderilmektedir. /* write */ #include #include #include #include #include #define MSG_KEY 0x1234567 #define NORMAL_MSG 1 #define QUIT_MSG 2 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); size_t len; struct MSG msg; char* str; for(;;) { printf("Message Text: "); fflush(stdout); if(fgets(msg.buffer, 8192, stdin) == NULL) continue; if((str = strchr(msg.buffer, '\n')) != NULL) *str = '\0'; if(!strcmp(msg.buffer, "quit")) { msg.mtype = QUIT_MSG; len = 0; } else { msg.mtype = NORMAL_MSG; len = strlen(msg.buffer) + 1; } if(msgsnd(msgid, &msg, len, 0) == -1) exit_sys("msgsnd"); if(msg.mtype == QUIT_MSG) break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include #include #define MSG_KEY 0x1234567 #define NORMAL_MSG 1 #define QUIT_MSG 2 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; if((msgid = msgget(MSG_KEY, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; ssize_t result; for(;;) { if((result = msgrcv(msgid, &msg, 8192, 0, 0)) == -1) exit_sys("msgcrv"); if(msg.mtype == QUIT_MSG) break; printf("%ld. [%s]\n", msg.mtype, msg.buffer); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "System 5 IPC" fonksiyonlarının önemli bir handikapı ilgili mesaj kuyruklarını açarken kullanılan "key" değerinin bir çakışmaya mahal vermesidir. Yani kullandığımız "key" değeri halihazırda başka programlar tarafından kullanılma durumudur. Her ne kadar bu problemi gidermek etmek için "IPC_PRIVATE" bayrağı kullanılsa da, elde ettiğimiz "id" değerinin diğer prosese bir şekilde aktarılması gerekmektedir. İşte bu çakışma ihtimalini daha da azaltan bir fonksiyon POSIX standartlarına eklenmiştir. İsmi "ftok" olan bu fonksiyon aşağıdaki parametrik yapıya sahiptir: #include key_t ftok(const char *path, int id); Burada fonksiyonun birinci parametresine varolan bir dosyanın yol ifadesi, ikinci parametresine de bir sayı gireceğiz. Başarılı olursa da bizlere bir "key" değeri dönecek. Eğer bu iki parametreyi aynı tutarsak, elde edeceğimiz "key" değeri aynı olacaktır. Fakat en az birisi farklı olursa, elde edeceğimiz "key" değeri de farklılaşacaktır. Öte yandan yol ifadesindeki dosyayı silip aynı isimle tekrar oluştursak, aynı "key" değerini elde edemeyebiliriz. Bu yönde bir garanti sunulmamaktadır. Ek olarak ikinci parametreye geçilen sayının düşük anlamlı 8 bit'i kaale alınacaktır. Eğer iş bu bit değerleri "0" olursa, sonucun ne olacağı "unspecified" olarak belirlenmiştir. Son olarak fonksiyon başarısız olursa "-1" ile geri dönüp "errno" değişkenini uygun değere çekecektir. Bu fonksiyonun yegane amacı, "System 5 IPC" nesneleri için tekil bir "key" değeri üretmek. Libc kütüphanesinde, yol ifadesiyle belirtilen dosyanın "I-node" numarası ile ikinci parametresindeki "id" değeri kombine edilmektedir. Burada tam manasıyla tekil bir "key" değerinin üretileceği garanti edilmemiştir. Her ne kadar dosyaların "I-node" numaraları tekil olsa da ikinci parametreyle geçilen "id" değeriyle yapılan kombinasyon işlemi sonucu aynı "key" değeri elde edilebilir. * Örnek 1, /* write */ #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 #define NORMAL_MSG 1 #define QUIT_MSG 2 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); size_t len; struct MSG msg; char* str; for(;;) { printf("Message Text: "); fflush(stdout); if(fgets(msg.buffer, 8192, stdin) == NULL) continue; if((str = strchr(msg.buffer, '\n')) != NULL) *str = '\0'; if(!strcmp(msg.buffer, "quit")) { msg.mtype = QUIT_MSG; len = 0; } else { msg.mtype = NORMAL_MSG; len = strlen(msg.buffer) + 1; } if(msgsnd(msgid, &msg, len, 0) == -1) exit_sys("msgsnd"); if(msg.mtype == QUIT_MSG) break; } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include #include #include #define KEY_NAME "/home/kaan" #define KEY_ID 123 #define NORMAL_MSG 1 #define QUIT_MSG 2 void exit_sys(const char*); struct MSG{ long mtype; char buffer[8192]; }; int main(int argc, char** argv) { int msgid; key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); if((msgid = msgget(key, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("msgget"); printf("Ok\n"); struct MSG msg; ssize_t result; for(;;) { if((result = msgrcv(msgid, &msg, 8192, 0, 0)) == -1) exit_sys("msgcrv"); if(msg.mtype == QUIT_MSG) break; printf("%ld. [%s]\n", msg.mtype, msg.buffer); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "System 5 IPC" nesneleri için iki önemli "shell" komutu vardır. Bunlar "ipcs" ve "ipcrm". Bizler daha önce zaten "ipcs" komutunu görmüştük. Anımsanacağı üzere bu komut "/proc/sysvipc" dizinindeki "msg", "sem" ve "shm" isimli dosyalara başvuararak işlem yapmaktadır. "ipcrm" komutu ise belli bir "ipc" nesnesini silmek için kullanılmaktadır. Silme işlemi "key" değerine göre veya "id" değerine göre yapılabilir. Bu değerleri görmek için yine "ipcs" komutunu kullanabiliriz. "ipcrm" komutunun seçeneklerinden küçük harfli olanlar "id", büyük harfli olanlar ise "key" değerine göre silme işlemi yaptırtmaktadır. "-q/-Q" seçeneği ise mesaj kuyruğu silmek için kullanılır. Paylaşılan bellek alanları için de "-m/-M" seçeneklerini kullanmalıyız. > Hatırlatıcı Notlar: >> C dilinde bir fonksiyon esasında "char" türden parametreyi kullansa bile fonksiyonun bildirim ve imzasında "int" türden belirtmeliyiz. Bu bir zorunluluk değil ama iyi bir pratiktir. Böylesi durumlarda gelenek "int" kullanılması yönünde. Benzer şekilde "short" türü için de geçerlidir. /*================================================================================================================================*/ (43_02_04_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları (devam): Anımsanacağınız üzere "IPC" fonksiyonları iki gruba ayrılmıştır. Bir grup "System 5 IPC", diğer gruba ise "POSIX IPC" fonksiyonları denmektedir. Bir tanesi 70'li yıllardan beri varolurken, "POSIX IPC" nesneleri 90'lı yıllardan itibaren hayatımıza girmeye başlamıştır. Bu gruplardaki fonksiyonların arayüzleri, kendi grupları içerisinde birbirine benzemektedir. Fakat birbirleriyle karşılaştırıldıklarında işin yapılış şekli değişmektedir. Örneğin "POSIX IPC" fonksiyonları "key" değeri yerine direkt bir dosya ismi kullanmaktadır. Tıpkı isimli borularda olduğu gibi. Fakat bu dosyanın, bir dizin girişi biçiminde bulundurulması zorunluluk değildir. Öte yandan POSIX standartlarına göre bu dosyanın kök dizinde olması gerekmektedir. Yani kullanacağımız isim, kök dizindeki bir dosyaya ilişkin olmalıdır. Örneğin, "/my_message_queue" şeklinde. Ama "ls" kabuk komutu çalıştırdığımız zaman "my_message_queue" ismini görmeyeceğiz. Fakat işletim sistemi izin veriyorsa kök dizin içerisindeki başka dizinleri de kullanabiliriz. Fakat unutmamalıyız ki isimlerin de çakışabilmesi mümkünse de bu olasılık "key" değerlerinin çakışma ihtimalinden daha düşüktür. Buraya kadar yazılanlardan da anlaşılacağı üzere "POSIX IPC" fonksiyonları tıpkı dosya fonksiyonlarına benzemektedir. Mesela "System 5 IPC" fonksiyonlarında kuyruğu sildiğimiz zaman direkt olarak silme işlemi gerçekleşmektedir. Kuyruk üzerinde işlem yapanlar beklenmemektedir. Fakat "POSIX IPC" fonksiyonlarında kuyruk silme işlemi yaptığımız zaman direkt olarak silme GERÇEKLEŞMİYOR. Kuyruk üzerinde işlem yapan son proses de işini bitirdikten sonra gerçek manada silme gerçekleşmektedir. Bütün bunlara ek olarak her iki "IPC" grubundaki nesneler "kernel persistance" biçimindedir. Yani ya direkt olarak elle silinmelidir ya da ilk "reboot" gerçekleştikten sonra silinecektir. Pekiyi bizler hangi fonksiyonları kullanmalıyız? Her iki fonksiyon grubunun da birbirinden üstün olduğu, zayıf olduğu noktaları vardır. "POSIX IPC" fonksiyonları daha güncel olması hasebiyle bu gruptakileri kullanmaya çalışmalıyız. Son olarak "POSIX IPC" nesneleri "libc" kütüphanesinde değil, "librt" kütüphanesinde tanımlanmıştır. Dolayısıyla dosyaları derlerken "-lrt" seçeneğini de kullanmamız gerekiyor. Şimdi "POSIX IPC" mesaj kuyruklarını inceleyelim. >>>> Mesaj Kuyrukları: Mesaj kuyruklarında "POSIX IPC" fonksiyonlarını kullanabilmek için şu sırayı gözetmemiz gerekmektedir: -> Üzerinde çalışılacak mesaj kuyruğu, "mq_open" fonksiyonu ile açılır. Burada bütün prosesler bu fonksiyonu geçerken aynı ismi kullanmaldır. "mq_open" fonksiyonunun protipi şu şekildedir; #include mqd_t mq_open(const char *name, int oflag, ...); Fonksiyon ya iki argüman ile ya da dört argüman ile çağrılabilir. Eğer mesaj kuyruğu var ise iki argümanla, mesaj kuyruğu yeniden oluşturulacak ise dört argüman ile çağrılmalıdır. Buradaki dört argümandan son iki argüman mesaj kuyruğunun bir takım özelliklerini belirtmektedir. Öte yandan fonksiyonun birinci ve ikinci parametresi ise sırasıyla açılacak/oluşturulacak mesaj kuyruğunun ismini ve yapılacak isme dair argümanlardır. İkinci parametre şu üç değerden sadece birisini alabilir; "O_RDONLY", "O_WRONLY" ve "O_RDWR". Fakat bunlardan bir tanesi şu değerler ile "bitwise-OR" işlemine sokulabilir; "O_CREAT", "O_EXCL" ve "O_NONBLOCK". Unutmamalıyız ki "O_EXCL" bayrağı "O_CREAT" ile birlikte kullanılır ve o isme dair bir kuyruk varsa fonksiyon başarısız olacaktır. Öte yandan "O_NONBLOCK" bayrağı ise okuma ve yazma işlemlerinin blokesiz modda yapılacağını belirtir. Anımsanacağı üzere bu bayrak, "SYSTEM 5 IPC" fonksiyonlarında, "msgsnd" ve/veya "msgrcv" fonksiyonlarında kullanılmaktaydı. Bütün bunlardan hareketle "O_CREAT" bayrağı kullanılmışsa, "mq_open" fonksiyonuna üçüncü ve dördüncü parametrelerini geçmeliyiz. Çünkü yeni bir mesaj kuyruğu oluşurken bu parametrelerdeki bilgileri kullanmaktadır. Pekiyi nedir bu üçüncü ve dördüncü parametreler? Üçüncü parametre, mesaj kuyruğunun sahip olacağı erişim haklarına ilişkindir ve dosyalarda kullandığımız "S_IXXX" sembolik sabitleri kullanılır. Bu sembolik sabitleri "bitwise-OR" işlemine sokabiliriz. Dördüncü parametre ise bir "struct" türünden nesnenin adresi olmalıdır. Bu yapının türü ise "mq_attr" türündendir. Bu yapının tanımı aşağıdaki gibidir; struct mq_attr { long mq_flags; /* Flags (ignored for mq_open()) */ long mq_maxmsg; /* Max. # of messages on queue */ long mq_msgsize; /* Max. message size (bytes) */ long mq_curmsgs; /* # of messages currently in queue (ignored for mq_open()) */ }; Yapının "mq_flags" isimli elemanı sadece "O_NONBLOCK" bayrağını içerebilir. "mq_maxmsg" elemanı ise kuyrukta tutulabilecek maksimum mesaj paketinin sayısını belirtmektedir. "mq_msgsize" ise bir mesajın kaplayabileceği maksimum büyüklüğü belirtmektedir. Bu büyüklük bir mesaj paketinin büyüklüğü değil, o paket içerisindeki mesajın büyüklük bilgisidir. "mq_curmsgs" ise kuyrukta o anda bulunan mesaj paket adedini belirtmektedir. Şimdi bu yapının "mq_flags" ve "mq_curmsgs" isimli elemanları "mq_open" fonksiyonunca göz ardı edilir. Dolayısıyla bizler "mq_attr" türünden bir nesnenin adresini "mq_open" fonksiyonuna geçmeden evvel sadece "mq_maxmsg" ve "mq_msgsize" isimli elemanlarını değiştirmeliyiz. "mq_open" fonksiyonu, bu iki elemanı dikkate alacaktır. Linux standart dökümanlarında "mq_open" fonksiyonunun dördündü paremetresi "non-const" olarak belirtilmiştir fakat görüleceği üzere bu parametre "set" konusunda bir işlevi yoktur. Dolayısıyla bizler "const mq_attr" türden nesnenin adresini geçmeliyiz. Öte yandan "mq_open" fonksiyonunun bu dördüncü parametresine "NULL" değeri de geçilebilir. Bu durumda oluşturulacak mesaj kuyruğu varsayılan değerler ile hayata gelecektir. Bu varsayılan değerler sistemden sisteme değişmektedir. Linux sistemlerinde "mq_maxmsg" değeri "10" iken, "mq_msgsize" değeri "8192" dir. Ek olarak "mq_open" fonksiyonunda kuyruk özelliklerini girerken "mq_maxmsg" ve "mq_msgsize" değerleri için o işletim sistemi alt ve üst limit de belirlemiş olabilir. Eğer bizim gireceğimiz değerler bu limitlerin dışında kalırsa, fonksiyon başarısız olur ve "errno" değişkeni "EINVAL" değerine çekilir. İlaveten "priviledged user" olan prosesler "mq_maxmsg" değişkenini arttırabilirken, "non-priviledged users" olanlar arttıramamaktadır. POSIX standartları bu limitler hakkında bir şey söylememiştir. İleride göreceğimiz "getrlimit" ve "setrlimit" fonksiyonları ile bu limit değerlerini değiştirebiliriz. Fonksiyonun geri dönüş değeri de bir "handle" değeridir. Bu değer Linux standartlarınca "fd" olarak ele alınmış fakat POSIX bu konuda "file descriptor" olmalıdır değil, herhangi bir "descriptor" olmalıdır demiştir. Özetle Linux sistemlerinde geri dönüş değeri, prosesin "Dosya Betimleyici Tablosunda" bir indeks numarasıdır. Fakat POSIX standartlarınca böyle bir zorunluluktan bahsedilmemiştir. Öte yandan Linux sistemlerinde geri dönüş değerinin "Dosya Betimleyici Tablosundaki" en düşük betimleyici olacağına dair bir şey söylenmemiştir. Fakat fonksiyon başarısız olursa "-1" ile geri dönecektir. Bizler mesaj kuyruğu üzerinde işlem yaparken bu "descriptor" değerini kullanacağız. Fonksiyonun geri dönüş değer türü "mqd_t" hangi türe karşılık geleceğine dair POSIX standartları bir şey söylememiştir. Yapı türleri de dahil olmak üzere herhangi bir tür olabilir. -> Mesaj kuyruğunu oluşturduktan sonra mesaj paketi göndermek için "mq_send" fonksiyonunu kullancağız. Fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio); Fonksiyonun birinci parametresi, "mq_open" ile elde ettiğimiz "descriptor" değeridir. Fonksiyonun ikinci parametresi ise gönderilecek mesajı belirtmektedir. Bu parametrenin "const char*" olmasından dolayı bizler göndereceğimiz şeyi "const char*" türüne "cast" ettikten sonra bu fonksiyona geçmeliyiz. Fonksiyonun üçüncü parametresi ise gönderilecek mesajın uzunluğunu, son parametre ise iş bu mesajın öncelik derecesini belirtmektedir. Bu öncelik derecesi "0" a eşit ya da "0" dan büyük girilmelidir. Bu öncelik derecesi ise şöyle işlemektedir; büyük değere sahip mesajlar kuyruğun önünde yer alırken, küçük değere sahipler kuyruğun arkalarında yer almaktadır. Eğer aynı değere sahip mesajlar varsa, yeni gelen mesaj eski mesajların arkasına yerleştirilmektedir. Anımsanacağı üzere "System 5 IPC" mekanizmasında öncelik değeri küçük olanlar mesajın baş kısmına, büyük olanlar ise mesajın arka kısmına yerleştirilmekteydi. Öte yandan "POSIX IPC" mesaj kuyruklarında, belli bir değeri kuyruktan almak gibi bir durum SÖZ KONUSU DEĞİLDİR. Son olarak bu parametreye geçeceğimiz değer "MQ_PRIO_MAX" sembolik sabit değerinden de KÜÇÜK OLMALIDIR. İş bu sembolik sabit ise "limits.h" içerisinde tanımlanmıştır. Fonksiyon başarı durumund "0" değerine, başarısızlık durumunda "-1" ile geri dönüp kuyruğa hiç mesaj yazmayacaktır. Son olarak "POSIX" mesaj kuyruklarının da bir limiti vardır. Kuyruğu açış sırasında kullanılan "O_NONBLOCK" bayrağına göre bu fonksiyon ya başarısızla geri dönecek ya da ilgili prosesi bloke edecektir. -> Mesaj kuyruğundan paket okuması yapabilmek için "mq_receive" fonksiyonunu kullanmalıyız. Fonksiyonun parametresi şu şekildedir; #include ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio); Fonksiyonun ilk parametresi yine "mq_open" ile elde ettiğimiz "descriptor" değeri. İkinci parametre ise mesajın yerleştirileceği alanın başlangıç adresi. Bu adres "char*" türden olduğu için bizler "char*" türüne "cast" yapmak durumunda kalabiliriz. Üçüncü parametre ise başlangıç adresi ikinci parametre olan alanın genişliğidir. Fakat bu parametreye dikkat etmeliyiz çünkü bu parametrenin, mesaj kuyruğu oluşturulurken dördüncü parametresine geçilen "mq_attr" yapı türünün "mq_msgsize" isimli elemanının değerinden küçük olmaması gerekir. Aksi halde "mq_receive" fonksiyonu direkt başarısız olmakta ve "errno" değişkenini uygun değere çekecektir. Pekiyi iş bu dördüncü parametre kullanılmamışsa, yani mesaj kuyruğu varsayılan değerler ile oluşturulmuşsa ya da bizler bu dördüncü parametredeki yapı türünün elemanlarınının değerini bilmiyorsak, ne yapmalıyız? İşte bu durumda devreye "mq_getattr" fonksiyonu girmektedir. Bu fonksiyonun ilgili mesaj kuyruğunun özelliklerini "get" eden bir fonksiyondur. Fakat bu "get" işlemi çalışma zamanında faal olacağı için, bizler "malloc" fonksiyonunu da kullanmamız gerekmektedir. "mq_receive" fonksiyonunun son parametresi ise kuyruktan alınan mesajın öncelik derecesidir. Kuyruktan alınan mesajın önceliği, bu adrese yazılacaktır. Tabii "NULL" adresini de geçebiliriz. Fonksiyonun geri dönüş değeri de başarı durumunda alınan mesajın uzunluğunu, başarısız durumunda ise "-1" ile geri döner. Başarısızlık durumunda kuyruktaki mesajı ALMAYACAKTIR. -> Pekiyi haberleşme nasıl sonlandırılmalıdır? Burada da tıpkı "System 5 IPC" mesaj kuyruklarındaki gibi özel bir mesajın kuyruğa yazılması gerekmektedir. Buradaki özel mesaj "0" uzunlukta bir mesaj olabileceği gibi belli bir değere sahip mesaj da olabilir. -> Son olarak "mq_close" fonksiyonu ile ilgili "descriptor" kapatılmalıdır. Fonksiyonun parametrik yapısı şöyledir: #include int mq_close(mqd_t mqdes); Bu fonksiyon çağrısını haberleşen her bir prosesin yapması tavsiye edilir. Fakat proses sonlandığında otomatik olarak ilgili "descriptor" kapatılacaktır. -> "POSIX IPC" mesaj kuyruklarının da "kernel persistance" olduğundan bahsetmiştik. Pekiyi bizler mesaj kuyruklarını nasıl sileceğiz? (To be continued...) Şimdi de bu vakte kadar gördüklerimize dair bir örnek yapalım: * Örnek 1, /* write */ #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void exit_sys(const char*); int main(int argc, char** argv) { struct mq_attr attr; attr.mq_maxmsg = 10; // Bir şekilde sistemimizdeki en büyük değerin "10" olduğunu öğrendik diyelim. attr.mq_msgsize = 32;// Bu değeri biz belirledik. Dolayısıyla kuyruktan okuma yaparken bu "32" değerini kullanabiliriz. mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, &attr)) == -1) exit_sys("mq_open"); for(int i = 0; i < 100; ++i) { if(mq_send(mqd, (const char*)&i, sizeof(int), 0) == -1) exit_sys("mq_send"); } mq_close(mqd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); /* * Kuyruğu biz oluşturduğumuz için mesajın genişlik bilgisini biliyorduk. */ char buffer[32]; int value; for(;;) { if(mq_receive(mqd, buffer, 32, NULL) == -1) exit_sys("mq_receive"); value = *(const int*)buffer; printf("%d\n", value); fflush(stdout); if(99 == value) break; } printf("\n"); mq_close(mqd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> Linux standartlarınca "mq_open" fonksiyonun dördüncü parametresi için "struct mq_attr*" belirtilmiş. Fakat POSIX standartları bu parametrenin "const" mu "non-const" mu olduğu konusunda bir şey söylememişler. Fakat bu yapı "global namepsace" içerisinde tanımlanarak kullanıldığında herhangi bir problemle karşılaşılmamıştır. Dolayısıyla bizler "const" olarak kullanacağız. >> "open" gibi fonksiyonlar ile açtığımız dosyalar için bir "file" türden nesne oluşturulur ve prosesin "Dosya Betimleyici Tablosu" ndaki ilgili indislere bu "file" dosyalarına ilişkindir. >> "mq_open" fonksiyonunda "O_EXCL" bayrağını yalnız başına kullanırsak yani "O_CREAT" ile birlikte kullanmazsak tanımsız davranış oluşacaktır. /*================================================================================================================================*/ (44_08_04_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları (devam): >>>> Mesaj Kuyrukları (devam): Daha önceki derste yaptığımız örneklerde mesaj kuyruğunu biz oluşturduğumuz için mesajın kaplayacağı alanı da biliyorduk. Dolayısıyla mesajı alırken ayırmamız gereken alan bilgisi bizde zaten vardı. Pekiyi bizler bu bilgiyi bilmiyorsak nasıl davranmalıyız? Çünkü karşı taraf mesaj kuyruğunu varsayılan değerlerle hayata getirebileceği gibi kendisi de bu değerleri belirleyebilir. Anımsanacağı üzere, böylesi bir durumda "mq_getattr" isimli fonksiyon devreye girecektir. İş bu fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int mq_getattr(mqd_t mqdes, struct mq_attr *mqstat); Fonksiyonun birinci parametresi, mesaj kuyruğuna ilişkin olan "descriptor" değeridir. İkinci parametre ise "mq_attr" yapı türünden bir nesnenin adresi. Mesaj kuyruğunun bilgileri, bu adresteki nesneye yerleştirilecektir. Geri dönüş değeri ise başarı durumunda "0", aksi halde "-1" biçimindedir. "mq_getattr" fonksiyonunun "set" işlemi yapan versiyonu da mevcuttur. Bu fonksiyon, mesaj kuyruğu oluşturulduktan sonra bazı özelliklerini değiştirmek için kullanılır. Fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int mq_setattr(mqd_t mqdes, const struct mq_attr * mqstat, struct mq_attr * omqstat); Fonksiyonun birinci parametresi, mesaj kuyruğuna ilişkin olan "descriptor" değeridir. İkinci parametre, yeni özelliklerin bulunduğu "mq_attr" yapı türünden bir nesnenin adresidir. Son parametre ise değiştirilmeden önceki bilgileri "get" etmek için kullanılan ve "mq_attr" yapı türünden bir nesnenin adresidir. Eğer eski değerleri istemiyorsak, üçüncü parametreye "NULL" değeri geçebiliriz. Geri dönüş değeri ise başarı durumunda "0", aksi halde "-1" biçimindedir. Pekiyi bizler bu fonksiyon ile "mq_attr" yapısındaki hangi elemanları değiştirebiliriz? Sadece ve sadece "mq_flags" isimli elemanı. Yapının diğer elemanları göz ardı edilecektir. Şimdi ise bu anlatılanların işlendiği bir örnek yapalım: * Örnek 1, Aşağıdaki örnekte öncelik değeri yüksek olan mesaj kuyruğun başında yer alırken, düşük olan kuyruğun sonunda yer almaktadır. Bunu görmek için ilk önce "write" programını çalıştırıp kuyruğu doldurmalı, sonrasında "read" fonksiyonu ile kuyruktakileri okumalıyız. /* write */ #include #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void clear_stdin(void); void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); int prio; char str*; char buffer[8192]; for(;;) { /* Kullanıcıdan gönderilecek mesaj alınır. */ printf("Message Text: "); fflush(stdout); if(fgets(buffer, stdin, 8192) == NULL) continue; if((str = strchr(buffer, '\n')) != NULL) *str = '\0'; /* Kullanıcıdan gönderilecek mesajın öncelik bilgisi de alınır. */ printf("Priority: "); fflush(stdout); scanf("%d", &prio); clear_stdin(); /* * İlgili mesajın sonundaki '\0' karakteri gönderilmeyecektir. * Eğer isteseydik, "strlen(buffer) + 1" dememiz gerekiyordu. */ if(mq_send(mqd, buffer, strlen(buffer), prio) == -1) exit_sys("mq_send"); if(!strcmp(buffer, "quit")) break; } mq_close(mqd); /* Mesaj kuyruğu bu noktada silinmiştir. */ return 0; } void clear_stdin(void) { int ch; while((ch = getchar() != '\n') && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); /* Kuyruğun özelliklerini bilmediğimiz için ilk önce öğreniyoruz. */ struct mq_attr attr; if(mq_getattr(mqd, &attr) == -1) exit_sys("mq_getattr"); /* * Mesajın ne kadarlık yer kaplayacağını öğrendikten sonra, o kadarlık bir alanı ayırıyoruz. * "attr.mq_msgsize + 1" dememizin sebebi, ayrılan bu ekstralık alana daha sonra '\0' karakteri * ekleneceği içindir. Karşı tarafın mesajın sonunda '\0' ekleyip eklemediği bilinmediği için * biz en kötüyü düşünerek hareket ettik. Eğer ekleyerek göndermişse, yazının sonunda iki adet * '\0' karakterinden olacaktır. */ char* buffer; if((buffer = malloc(attr.mq_msgsize + 1)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } int prio; ssize_t result; for(;;) { if(mq_receive(mqd, buffer, attr.mq_msgsize, &prio) == -1) exit_sys("mq_receive"); buffer[result] = '\0'; printf("%d. %s\n", prio, buffer); if(!strcmp(buffer, "quit")) break; } printf("\n"); free(buffer); mq_close(mqd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi sıra "POSIX IPC" mesaj kuyruklarının sistemden nasıl silineceğine geldi. Burada devreye "mq_unlink" fonksiyonu devreye girmektedir. Anımsanacağı üzere mesaj kuyrukları silme işlemi uygulanmadığı veya sistem "reboot" edilmediği sürece sistemde varlığını sürdürecektir. Öte yandan, burada direkt olarak mesaj kuyruğu silinmemektedir, tıpkı dosyalarda olduğu gibi. Ancak ve ancak bu kuyruk üzerinde işlem yapan prosesler işlemlerini bitirdikten sonra gerçek anlamda silinme gerçekleşecektir. Şunu da belirtmekte fayda vardır ki mesaj kuyruğunu oluşturan prosesin silmesi uygun görülmektedir. Eğer kuyruğu oluşturan kişi bilinmiyorsa, taraflardan birisi kuyruğu silebilir. Pekiyi nedir bu "mq_unlink" fonksiyonu? İlgili mesaj kuyruğu aşağıdaki parametrik yapıya sahiptir; #include int mq_unlink(const char *name); Fonksiyon, silinecek olan mesaj kuyruğunun ismini almaktadır. Başarı durumunda "0", başarısızlık durumunda "-1" ile geri dönmektedir. Aşağıdaki biçimde de bu fonksiyonu kullanabiliriz; //... #define MSG_QUEUE_NAME "/my_message_queue" //... if(mq_unlink(MSG_QUEUE_NAME) == -1) exit_sys("mq_unlink"); Malumunuz olduğu üzere "System 5 IPC" mesaj kuyruklarının bir limiti vardır. Bu limit kavramı yine "POSIX IPC" mesaj kuyrukları için de geçerlidir. Fakat POSIX standartları bu konuda bir şey söylememektedir. Linux sistemlerinde ise bir mesaj paketi içerisindeki mesajın uzunluğunun 8192 bayt, bir kuyruğun alabileği toplam mesaj paketi adedi de 10'dur. Pekiyi bu değerler nereden gelmektedir? "/proc/sys/fs/mqueue" dizini içerisindeki dosyalardan bu değerler belli olmaktadır. İş bu dosyalar şu şekildedir; "msg_default", "msg_max", "msgsize_default", "msgsize_max" ve "queues_max". Şimdi bu dosyaları inceleyelim: >>>>> "msg_default" : Bir kuyruğu varsayılan değerler ile oluştururken, o kuyrukta bulunabilecek maksimum mesaj paketi adedini belirtil. Bu değer, birazdan göreceğimiz "msg_max" değerinden büyük olamaz. >>>>> "msg_max" : "mq_open" ile bir mesaj kuyruğu oluştururken, varsayılan değerler yerine bizim atayacağımız değerler ile oluşturulmasına olanak veren "mq_attr" yapısının "mq_maxmsg" isimli elemanının alabileceği maksimum değeri belirler. Yani bizler mesaj kuyruğu yaratırken, "msg_max" değerinden daha büyük bir değeri "mq_attr.mq_maxmsg" elemanına veremeyiz. >>>>> "msgsize_default": Bir kuyruğu varsayılan değerler ile oluştururken, o kuyruktaki mesaj paketlerinin içindeki mesajların alabileceği maksimum uzunluk değeridir. Bu değer, birazdan göreceğimiz "msgsize_max" değerinden büyük olamaz. >>>>> "msgsize_max" : "mq_open" ile bir mesaj kuyruğu oluştururken, varsayılan değerler yerine bizim atayacağımız değerler ile oluşturulmasına olanak veren "mq_attr" yapısının "mq_msgsize" isimli elemanının alabileceği maksimum değeri belirler. Yani bizler mesaj kuyruğu yaratırken, "msgsize_max" değerinden daha büyük bir değeri "mq_attr.mq_msgsize" elemanına veremeyiz. >>>>> "queues_max" : Sistem geneli oluşturulabilecek maksimum mesaj kuyruğu adedidir. Bu adetten daha büyük bir mesaj kuyruğunu hayata getiremeyiz. Öte yandan "root" olan prosesler, yukarıda detayları açıklanan "limit" değerlerinden etkilenmemektedir. Fakat "root" prosesin de takıldığı bir "limit" değeri vardır. Ek olarak, yukarıdaki beş "limit" dosyası, "root" tarafından değiştirilebilir. İlgili "limit" değerleri için bkz. (https://man7.org/linux/man-pages/man7/mq_overview.7.html). Fakat unutmamalıyız ki buradaki "limit" değerleri Linux sistemleri için geçerlidir. Diğer sistemlerde, o sistemin kaynak dökümanlarına bakılması gerekmektedir. Bütün bunlar bir yana olsun bazı POSIX türevi sistemlerde mesaj kuyrukları, özel bir dosya biçiminde, "mount" edilbilmektedir. Örneğin, Linux sistemlerinde mesaj kuyruklarına ilişkin bu özel dosya, "/dev/mqueue" dizini üzerine "mount" edilmiştir. Yani "/dev/mqueue" dizini içerisine girdiğimizde, "ls" kabuk komutuyla sistemde bulunan bütün "POSIX" mesaj kuyruklarını görebilir ve "rm" kabuk komutu ile bunları silebiliriz. Linux sistemlerinde, yukarıdaki "mount" işlemi otomatik olarak gerçekleştirilir. Tabii bizler de istediğimiz başka dizinler üzerinde bu özel dosyayı "mount" işlemi gerçekleştirebiliriz. Bunun için de kabuk programından şu aşağıdaki komutu çalıştırmalıyız: sudo unmount /dev/mqueue Artık ilgili özel dosya, "unmount" edilmiştir. Şimdi bizler bu özel dosyayı başka bir yere de "mount" etmemiz gerekiyor. Bunun için ilk olarak bir dizin oluşturmamız gerekiyor. Dolayısıyla önce aşağıdaki kabuk komutunu çalıştırmalıyız: mkdir my_message_queue_point Daha sonra aşağıdaki komutu çalıştırmalıyız. Fakat bizler "appropriate priviledged" olmamız gerekmektedir: sudo mount -t mqueue somerandomname my_message_queue_point Şimdi bizler "mount" işlemini gerçekleştirdik. "ls -l" çektiğimiz zaman "my_message_queue_point" isimli dizin girişi karşımıza çıkacaktır. "my_message_queue_point" içerisine girip "ls -l" çekersek, sistemdeki POSIX mesaj kuyruklarını görebiliriz. Şimdi burada bir noktaya dikkat çekmek gerekmektedir. "mount" işlemi sırasında "my_message_queue_point" isimli dizinin "sticky_bit" değeri de "set" edilmektedir. Dolayısıyla bu dizinin sahibi bile olsak, bu dizin içerisinde istediğimiz bir dosyayı silemiyorduk("appropriate priviledged" olmadğımız varsayılmıştır). Yalnızda kendimize ait dosyaları silebiliyor, sahibi başka olan dosyaları silemiyor idik. Halbuki iş bu "sticky_bit" değeri "set" edilmeseydi, ilgili dizin içerisinde istediğimiz dosyaları silebiliyorduk. Öte yandan bizler tekrardan "unmount" işlemi yaparsak, "my_message_queue_point" ismi dizin girişinde hala görülecektir fakat içi boş bir dizin gibi işlevi olacaktır. Yani bu aşamada "ls -l" çeksek, bir şey göremeyeceğiz. Pekiyi "mount" işlemi sırasında kullandığımız "somerandomname" değeri ne manaya gelmektedir? Sistem geneli "mount" edilen bütün özel dosyaların tutulduğu bir dosya düşünün. Bu dosya içerisinde "mount" edilen noktayı işaret eden isim olarak kullanılmaktadır. Yani bütün "mount point" ler bir isim ile ilişkilendirilmektedir. İş bu dosyayı görebilmek için "mount" isimli kabuk komutunu çalıştırmamız yeterli olacaktır. Mesaj kuyrukları bahsini kapatmadan evvel bahsedilmesi gereken iki fonksiyon daha vardır; "mq_timedsend" ve "mq_timedreceive" isimli fonksiyonlardır. Bu fonksiyonlar sırasıyla "mq_send" ve "mq_receive" fonksiyonlarının zaman aşımlı versiyonlarıdır. Pekiyi nedir bu zaman aşımı durumu? Üzerinde çalışılan kuyruk boşken ya da doluyken belirlenen zaman çerçevesinde blokeye sebebiyet vermekte, zaman aşımı olması durumunda da ilgili fonksiyonların başarısız olmaları demektir. "errno" değişkeni de uygun değere çekilecektir. Fonksiyonların prototipleri şu şekildedir; #include #include int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio, const struct timespec *abstime); ssize_t mq_timedreceive(mqd_t mqdes, char *restrict msg_ptr, size_t msg_len, unsigned *msg_prio, const struct timespec *abstime); Bu fonksiyonların "mq_send" ve "mq_receive" fonksiyonlarından tek farklı, son parametre olan zaman aşımı parametresidir. Bu parametre "timespec" yapı türünden bir nesnenin adresidir. "const" olduğuna dikkat ediniz. "timespec" yapısı aşağıdaki gibi tanımlanmıştır; #include struct timespec { time_t tv_sec; /* Seconds. */ long tv_nsec; /* Nanoseconds. */ }; 01.01.1970'den itibaren geçen saniyeyi tutmaktadır. "tv_nsec" ise bu saniyenin nanosaniye cinsinden değeridir. Pekiyi bizler zaman aşım süresini nasıl belirleyeceğiz? Şöyleki; -> "clock_gettime()" fonksiyon çağrısı ile şimdiki zamana ilişkin "timespec" değeri elde edilir. "clock_gettime" fonksiyonu aynı zamanda bir standart C fonksiyonudur. Fonksiyon aşağıdaki parametrik yapıya sahiptir; #include int clock_gettime(clockid_t clock_id, struct timespec *tp); Fonksiyonun birinci parametresi saat türüne ilişkin bir parametredir. Bu parametrenin "CLOCK_REALTIME" sembolik sabiti alınması uygun olur. Fonksiyonun ikinci parametresi ise içi doldurulacak "timespec" yapısının adresidir. -> Elde edilen bu "timespec" değerine istenilen zaman aşım süresi eklenir. -> Yeni "timespec" değeri ise "mq_timedsend"/"mq_timedreceive" fonksiyonlarında kullanılır. Aşağıda bu fonksiyonun kullanımına ilişkin bir örnek yapılmıştır. * Örnek 1, /* write */ #include #include #include #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void clear_stdin(void); void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); struct timespec ts; if(clock_gettime(CLOCK_REALTIME, &ts) == -1) exit_sys("clock_gettime"); // printf("Total Sec. since Epoch: %lld\n", (long long)ts.tv_sec); /* Şu ankinin üzerine 10 saniye ekledik. */ ts.tv_sec += 10; char buffer[100]; if(mq_timedsend(mqd, buffer, 100, 0, &ts) == -1) { if(errno == ETIMEDOUT) { /* Timedout oluştu. Program çalıştıktan sonra 10 saniye sonra akış buraya gelecektir. */ } else { exit_sys("mq_timedsend"); } } mq_close(mqd); if(mq_unlink(MSG_QUEUE_NAME) == -1) exit_sys("mq_unlink"); return 0; } void clear_stdin(void) { int ch; while((ch = getchar() != '\n') && ch != EOF) ; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* "mq_open" için gerekli. */ #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #define MSG_QUEUE_NAME "/my_message_queue" void exit_sys(const char*); int main(int argc, char** argv) { mqd_t mqd; if((mqd == mq_open(MSG_QUEUE_NAME, O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, NULL)) == -1) exit_sys("mq_open"); struct mq_attr attr; if(mq_getattr(mqd, &attr) == -1) exit_sys("mq_getattr"); char* buffer; if((buffer = malloc(attr.mq_msgsize)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } int prio; ssize_t result; for(;;) { if(mq_receive(mqd, buffer, attr.mq_msgsize, &prio) == -1) exit_sys("mq_receive"); buffer[result] = '\0'; printf("%d. %s\n", prio, buffer); if(!strcmp(buffer, "quit")) break; } printf("\n"); free(buffer); mq_close(mqd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Son olarak belirtmemiz gerekiyor ki zaman aşımlı işlemler blokesiz işlemler değillerdir. Blokesiz işlemlerde hiç bloke oluşmazken, bu tip zaman aşımlı işlemlerde belirlenen süre boyunca bloke oluşur. Artık mesaj kuyrukları bahsini bitirmiş oluyoruz. Malumunuz olduğu üzere "System 5 IPC" fonksiyonları ile "POSIX IPC" fonksiyonlarını karşılıklı olarak görmekteyiz. Mesaj kuyrukları bahsini kapattığımıza göre, şimdi sırada Paylaşılan Bellek Alanları vardır. İlk olarak "System 5" versiyonu, daha sonra "POSIX" versiyonunu göreceğiz. >>>> Paylaşılan Bellek Alanları: İki prosesin "Sayfa Tablolarının" sol sütunlarındaki farklı Sanal Sayfa Numaraları, aynı tablonun sağ sütunundaki aynı Fiziksel Sayfaya(RAM'deki karşılığına) ilişkin olması durumudur. Daha öncesinde de kabaca bir açıklama yapılmıştır. İki prosesin de aynı fiziksel sayfaya eriştiği; birisi yazma yaparken diğeri okuma yapabilmektedir. Fakat bu haberleşme yöntemi, mesaj kuyruklarındakinin ve/veya borulardakinin aksine, kendi içerisinde bir senkronizasyona sahip değildir. Senkronizasyonun sağlanabilmesi için de "semaphore" gibi senkronizasyon nesnelerine ihtiyaç duymaktadır. İşte bu yüzdendir ki "IPC" nesnelerine "semaphore" lar eklenmiştir. Bizler bu "semaphore" konusunu "thread" konusunda göreceğiz. Eğer senkronizasyon sağlanamaz ise üzerine yazma, iki defa okuma gibi sorunlar meydana gelebilir. "System 5" paylaşılan bellek alanları aşağıdaki aşamalardan geçerek kullanılmaktadır; -> "shmget" fonksiyonuna "key" değeri verilerek bir "id" elde edilir. Duruma göre ya yeni bir paylaşılan bellek alanı oluşturularak ya da var olan açılarak bir "id" elde edilir. Diğer prosesler bu "id" değerini biliyorsa, "shmget" fonksiyonunu kullanmalarına gerek kalmayacaktır. "shmget" fonksiyonu aşağıdaki parametrik yapıya sahiptir; #include int shmget(key_t key, size_t size, int shmflg); Fonksiyonun birinci parametresi, "id" elde etmek için kullanılacak "key" değeridir. Aynı "id" değerini elde etmek için aynı "key" değerleri kullanmalıyız. Tabii yine "ftok" fonksiyonunu "key" elde etmek için de kullanabilir ya da "IPC_PRIVATE" sembolik bayrağını kullanabiliriz. Tabii, böylesi bir bayrak kullanımı sonrasında elde edilen "id" değerini karşı proseslere bir şekilde ulaştırmamız gerekmektedir. Fonksiyonun ikinci parametresi, paylaşılacak bellek alanının büyüklüğünü belirtmektedir. Bu değerin o sistemdeki sayfa sayısının kapladığı alan kadar olması tavsiye edilir. Fakat POSIX standartlarınca böyle bir zorunluluk yoktur. Örneğin, sistemimizde bir sayfa 4096 bayt yer kaplasın. Bizler de fonksiyonun ikinci parametresine 5000 değerini girelim. İşletim sistemi iki adet sayfayı bize ayıracaktır. Dolayısıyla ikinci sayfadaki 3192 bayt boşa gidecektir. Çünkü bellekteki en küçük birim sayfalardan oluşmaktadır (bkz. "internal fragmantation"). Öte yandan Linux sistemlerinde, fonksiyonun ikinci parametresine geçilen değer sayfa katlarına yuvarlanmaktadır. Bizim örneğimiz için 5000 değeri 8192 değerine yuvarlanacaktır. > Hatırlatıcı Notlar: >> Bizler burada yazarken "POSIX IPC" mesaj kuyrukları tabiri ya da "System 5 IPC" mesaj kuyrukları tabiri kullandık. Aslında "IPC" kelimesi başlı başına prosesler arası haberleşmeyi kastetmektedir. Dolayısıyla aslında kastettiğimiz şey "POSIX" mesaj kuyrukları ve "System 5" mesaj kuyruklarıdır. >> Sayfa Tablosunun Fiziksel Sayfa kısmı aslında RAM'deki kısmı göstermektedir. /*================================================================================================================================*/ (45_09_04_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları (devam): >>>> Paylaşılan Bellek Alanları (devam): "shmget" fonksiyonunun üçüncü parametresi, tıpkı "msgget" fonksiyonunda olduğu gibi, erişim hakları ve yeni bir paylaşılan bellek alanı oluştururken kullanılacak bayraklara ilişkin seçenekleri belirtmektedir. Yine burada erişim hakları için "S_IXXX" biçimindeki sembolik sabitleri, yeni oluşturulacak paylaşılan bellek alanları için "IPC_CREAT" ya da "IPC_CREAT | IPC_EXCL" bayrakları kullanılmaktadır. Fonksiyonun geri dönüş değeri ise başarı durumunda "id" değeri, başarısızlık durumunda ise "-1" değeridir. * Örnek 1, Aşağıdaki örnekte, bir adet Paylaşılan Bellek Alanı nesnesi oluşturulmuş ve ona dair "id" değeri elde edilmiştir. "write" yapacak olan proses de "read" yapacak olan proses de şimdilik aynı kodları kullanacağı için kodlar ayrı ayrı belirtilmemiştir. #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shmget"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Anımsanacağınız üzere sistem geneli bütün "IPC" nesnelerini görmek için "ipcs" kabuk komutunu kullanıyorduk. Mesaj kuyruklarını görmek için "-q" seçeneğini kullanırken, paylaşılan bellek alanları için "-m" seçeneğini kullanacağız. Tabii iş bu kabuk komutu da "/proc/sysvipc" dizinindeki dosyalara bakarak iş yapmaktadır. -> Şimdi sıradaki işlem halihazırda oluşturulmuş olan paylaşılan bellek alanı nesnesini, prosesin "Sayfa Tablosuna", yeni bir giriş oluşturarak, ekliyoruz. Artık o prosesin sayfa tablosunun o indeksi, ilgili paylaşılan bellek alanını göstermektedir. Tabii bu işlemi haberleşecek diğer proseslerin de yapması gerekmektedir. Böylelikle iki farklı prosese ait sayfa tablolarının sol taraflarındaki indisler, bellek üzerinde aynı yeri gösterir olacaklardır. "System 5" terminolojisinde bu bağlama işlemine "attach" denmektedir. Dolayısıyla bu "attach" işlemi sonrasında ilgili sayfa tabloları aşağıdaki biçimde olacaktır: Proses - I Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 95134 45321 ... ... Proses - II Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 32148 45321 ... ... Pekiyi bizler bu "attach" işlemini nasıl gerçekleştireceğiz? Tabii ki "shmat" isimli POSIX fonksiyonuyla. Bu fonksiyon aşağıdaki parametrik yapıya sahiptir: #include void *shmat(int shmid, const void *shmaddr, int shmflg); Fonksiyonun birinci parametresi, paylaşılan bellek alanı nesnesinin "id" değeridir. İkinci parametre ise talep edilen sanal adresi belirtmektedir. Bir nevi işletim sistemine "O sanal adres yolu ile paylaşılan bellek alanı nesnesine erişmek istiyorum" demektir. Fakat bu sanal adres halihazırda kullanılıyorsa ya da işletim sistemi tarafından kullanılamaz bir adres olması durumunda, fonksiyon başarısız olacaktır. Bu parametreye "NULL" değeri de geçilebilir. Bu durumda işletim sistemi sanal adresi kendisi seçecektir. Tavsiye edilen yol ise iş bu ikinci parametreye "NULL" geçilmesidir. Son olarak bu parametreye değer geçeceksek, ilgili değerin sayfa katları biçiminde olması beklenir. Örneğin, sayfa sayısı 4096 ise bizim geçeceğimiz değer 4096'nın katları biçiminde olmasıdır. Fonksiyonun son parametresi ise bir takım bayraklar almaktadır. Bunlar "SHM_RND" ve "SHM_RDONLY" bayraklarıdır. Eğer bu bayrakları kullanmak istemiyorsak, "0" değerini kullanabiliriz. Şimdi bu bayraklar için bir takım nüanslar vardır. "SHM_RND" bayrağını ele alalım; şimdi fonksiyonun üçüncü parametresinde bu bayrak kullanılmazsa ve fonksiyonun ikinci parametresine geçilen değer de sayfa sayısı katlarında değilse, "shmat" fonksiyonu başarısız olacaktır. Öte yandan üçüncü parametrede "SHM_RND" bayrağı kullanılırsa ve ikinci parametreye geçilen değer de sayfa sayısı katlarında DEĞİLSE, bu sefer bu değer sayfa sayısının katları olacak biçimde aşağı doğru YUVARLANIR. POSIX standartlarında bu aşağıdaki biçimde tarif edilmektedir: "shmaddr - ((uintptr_t)shmaddr & SHMLBA)" Burada "SHMLBA" sembolik sabiti, o sistemdeki sayfa katına karşılık gelmektedir. "%" işlemi ile de bölme işleminden kalan değer elde edilmektedir. Şimdi de "SHM_RDONLY" bayrağını ele alalım; işletim sistemi ilgili paylaşılan bellek alanı nesnesine "read-only" olarak işaretlemektedir. Bu alana yazma amacıyla erişirsek, "fault" oluşacak ve işletim sistemi prosesi sonlandıracaktır. Eğer "SHM_RDONLY" bayrağı kullanılmazsa, erişim "read-write" biçiminde olacaktır. Fonksiyon, başarı durumunda paylaşılan bellek alanı nesnesine erişmede kullanılan sanal adrese, başarısızlık durumunda ise "(void*)-1" değerine geri dönmektedir. * Örnek 1, Aşağıdaki örnek, bir önceki örneğin devamı niteliğindedir. /* write */ #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { /* * Bu proses "read" yapacağı için "shmget" fonksiyonunun * son parametresine "0" geçilmiştir. */ int shmid; if((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Şimdi haberleşecek prosesler iş bu paylaşılan bellek alanı nesnesini kullanabilirler. Fakat unutmamalıyız ki "Paylaşılan Bellek Alanları" kendi içerisinde senkronizasyon sağlamamaktadır. Bizler bu senkronizasyonu dışarıdan sağlamamız gerekmektedir. Halbuki borular ve mesaj kuyruklarında senkronizasyon otomatik olarak sağlanmaktadır. * Örnek 1, Aşağıdaki örnekte senkronizasyon yöntemi çok ilkel bir yolla sağlanmaya çalışılmıştır. Haberleşmeyi sağlamak için ilk olarak "write" programı çalıştırılmalı ve bellek alanına yazılmalı, daha sonra "read" programı çalıştırılmalı ve bellek alanından okuma yapılmalıdır. Aşağıdaki örnek, bir önceki örneğin devamı niteliğindedir. /* write */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); printf("Virtual address of shared memory: [%p]\n", shmaddr); strcpy(shmaddr, "This is a test message!...\n"); printf("The message has been written to the [%p]\n", shmaddr); printf("Press ENTER to continue...\n"); getchar(); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); printf("Virtual address of shared memory: [%p]\n", shmaddr); printf("A message has been read : [%s]\n", shmaddr); printf("Press ENTER to continue...\n"); getchar(); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Pekiyi haberleşme nasıl sonlanmalıdır? Görüleceği üzere bizler ilk önce bir nesne oluşturup, bunu prosesin bellek alanına "attach" ettik. Sonlandırma yaparken ilk önce ilgili nesne ilgili bellek alanından "detach" edilmeli ve daha sonra bu nesne yok edilmelidir. İşte "detach" işlemi için "shmdt" fonksiyonu, yok etme işlemi için de "shmctl" fonksiyonu kullanılır. "shmdt" fonksiyonunun prototipi şöyledir; #include int shmdt(const void *shmaddr); Fonksiyon, "shmat" fonksiyonu ile elde ettiğimiz sanal adresi alır. Başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri dönecektir. Öte yandan bu "detach" işlemi yapılmazsa, proses sonlandığında otomatik olarak yapılmaktadır. "detach" işleminden sonra paylaşılan bellek alanı nesnesini de yok etmemiz gerektiğini söylemiştik. Çünkü ya bizler "shmctl" ile silmeliyiz. Aksi halde sistem ilk "reboot" edilene kadar hayatta kalacaktır. "shmctl" fonksiyonu aşağıdaki parametrik yapıya sahiptir; #include int shmctl(int shmid, int cmd, struct shmid_ds *buf); Fonksiyonun üçüncü parametresi "shmid_ds" yapı türünden bir adres değeridir. Bu yapı türü, ilgili paylaşılan bellek alanı nesnesi için bir takım bilgiler tutmaktadır. Bu yapı türü "sys/shm.h" başlık dosyasında aşağıdaki gibi tanımlanmıştır; struct shmid_ds{ struct ipc_perm shm_perm; /* Operation permission structure. */ size_t shm_segsz; /* Size of segment in bytes. */ pid_t shm_lpid; /* Process ID of last shared memory operation. */ pid_t shm_cpid; /* Process ID of creator. */ shmatt_t shm_nattch; /* Number of current attaches. */ time_t shm_atime; /* Time of last shmat */ time_t shm_dtime; /* Time of last shmdt */ time_t shm_ctime; /* Time of last change by shmctl */ }; "shmid_ds" yapısının "shm_perm" isimli elemanı "ipc_perm" yapı türündendir. "System 5 IPC" Mesaj Kuyrukları bahsinde zaten bu yapı türünü görmüştük. Hatırlayacak olursa; struct ipc_perm { uid_t uid // Owner's user ID. gid_t gid // Owner's group ID. uid_t cuid // Creator's user ID. gid_t cgid // Creator's group ID. mode_t mode // Read/write permission. }; Bu yapı türü de "sys/ipc.h" başlık dosyasında belirtilmiştir. Bu yapının elemanlarının sırasıyla; o anki "Kullanıcı ID" ve "Group ID" değerleri, ilgili "IPC" nesnesini oluşturan prosesin "Kullanıcı ID" ve "Group ID" değerleri, ilgili "IPC" nesnesinin sahip olduğu erişim haklarıdır. "shmid_ds" yapısının "shm_atime", "shm_dtime" ve "shm_ctime" isimli elemanları sırasıyla ilgili "IPC" nesnesinin en son ki "attach" zamanı, "detach" zamanı ve "shmid_ds" yapısındaki değiştirilme zaman bilgisini tutmaktadır. "shmid_ds" yapısının "shm_segsz" isimli elemanı ise ilgili "IPC" nesnesinin büyüklük bilgisini tutmaktadır. "shmid_ds" yapısının "shm_lpid" ve "shm_cpid" isimli elemanları sırasıyla "attach"/"detach" yapan prosesin "id" değeri ve ilgili "IPC" nesnesini oluşturan prosesin "id" değerini tutmaktadır. "shmid_ds" yapısının "shm_nattch" isimli veri elemanı ise kaç defa "attach" yapıldığı bilgisini tutmaktadır. Fonksiyonun ikinci parametresi, iş bu paylaşılan bellek alanı nesnesini yok etmek ya da "shmid_ds" yapı nesnesini "get" ve/veya "set" etmek için kullanılır. Dolayısıyla bu parametre şu bayraklardan birisini alabilir; "IPC_STAT", "IPC_SET" ve "IPC_RMID". "IPC_STAT" bayrağı kullanıldığında, ilgili "IPC" nesnesinin özellikleri "get" edilir ve fonksiyonun üçüncü parametresine geçilen yapı türü bu bilgiler ile doldurulur. "IPC_SET" bayrağı kullanıldığında, ilgili "shmid_ds" yapısının bazı özellikleri "set" edilir. Bu özellikler şunlardır; "shmid_ds.uid", "shmid_ds.gid" ve "shmid_ds.mode". Tabii bu özellikleri değiştirebilmek için, tıpkı mesaj kuyruklarında olduğu gibi, prosesimizin "Appropriate Priviledged" olması ya da prosesin "Etkin Kullanıcı ID" değerinin "shmid_ds" yapısının "shm_perm" isimli elemanının ki bu eleman da aslında "ipc_perm" yapı türündendir, "uid" ya da "cuid" değerine eşit olması gerekmektedir. "IPC_RMID" bayrağı kullanıldığında, ilgili "IPC" nesnesi yok edilecektir. Tabii bu bayrağı kullanabilmek için, "IPC_SET" bayrağını kullanmak için sahip olmamız gereken koşullara sahip olmalıyız. Ek olarak, bu bayrak kullandıldığı taktirse ilgili fonksiyonun son parametresine hangi adresin geçildiğinin bir önemi kalmaz. Bu durumda "NULL" adresini geçebiliriz. Fonksiyonun birinci parametresi ise ilgili paylaşılan bellek alanının "id" değeridir. Bu fonksiyonun işlevini "msgctl" fonksiyonuna benzetilebilir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", hata durumunda "-1" ile geri dönmektedir. Pekiyi ilgili "IPC" nesnesini kim yok etmelidir? Burada tavsiye edilen, hayata getiren proses tarafından silinmesidir. Ayrıca şunu da belirtmek gerekir ki "System 5" mesaj kuyruklarında, mesaj kuyruğu silindiğinde, direkt olarak silme işlemi gerçekleşiyor ve mesaj kuyruğu üzerinde işlem yapan prosesler hata durumuna düşmekteydi. Fakat "System 5" paylaşılan bellek alanları için böyle bir şey söz konusu değildir. İlgili "IPC" nesnesinin gerçek manada silinmesi için, "attach" yapan bütün proseslerin "detach" yapması gerekmektedir. * Örnek 1, Aşağıdaki örnek, bir önceki örneğin devamı niteliğindedir. İlgili "IPC" nesnesini oluşturan proses, silme işlemini gerçekleştirmiştir. /* write */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, IPC_CREAT | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); printf("Virtual address of shared memory: [%p]\n", shmaddr); strcpy(shmaddr, "This is a test message!...\n"); printf("The message has been written to the [%p]\n", shmaddr); printf("Press ENTER to continue...\n"); getchar(); if(shmdt(shmaddr) == -1) exit_sys("shmdt"); /* * Tam manasıyla silme işlemi, bütün prosesler "detach" yaptığında * gerçekleşeceği için, yukarıdaki gibi herhangi bir bekleme * yapılmamıştır. */ if(shmctl(shmid, IPC_RMID, NULL) == -1) exit_sys("shmctl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #include #define SHM_KEY 0x123456 void exit_sys(const char*); int main(int argc, char** argv) { int shmid; if((shmid = shmget(SHM_KEY, 4096, 0)) == -1) exit_sys("shmget"); char* shmaddr; if((shmaddr = (char*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); printf("Virtual address of shared memory: [%p]\n", shmaddr); printf("A message has been read : [%s]\n", shmaddr); printf("Press ENTER to continue...\n"); getchar(); if(shmdt(shmaddr) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Tıpkı mesaj kuyruklarında olduğu gibi "System 5" paylaşılan bellek alanları da bir takım limitlere sahiptir. Her ne kadar POSIX standartları bu limitlerin detayları hakkında bir şey söylemese de Linux sistemi şu limit değerlerine sahiptir; "SHMALL", "SHMMIN", "SHMMAX", "SHMMNI". -> "SHMALL" : Sistem geneli Paylaşılan Bellek Alanları nesnelerinin kaplayabileceği toplam alanın, sayfa sayısı cinsinden, değeridir. Bu değer "/proc/sys/kernel" dizinindeki "shmall" isimli dosyadan elde edilir ve değiştirilebilirdir. Buradaki varsayılan değer çok büyüktür. Adeta bir sınır olmadığını belirtir. -> "SHMMAX" : Bir Paylaşılan Bellek Alan nesnesinin maksimum büyüklüğüdür. "/proc/sys/kernel" dizini içerisindeki "shmmax" dosyasından elde edilir. Buradaki varsayılan değer çok büyüktür. Adeta bir sınır olmadığını belirtir. -> "SHMMIN" : Bir Paylaşılan Bellek Alan nesnesinin minimum büyüklüğüdür. Şimdiki sistemlerde "1" bayt değerindedir. Fakat "shmget" fonksiyonu ile paylaşılan bellek alanı oluşturmak istersek, sistem bize bir sayfa kadar yer ayıracaktır. Ek olarak bu değer için "proc" dosya sisteminde bir dosya mevcut değildir. -> "SHMMNI" : Sistem geneli oluşturulabilecek maksimum Paylaşılan Bellek Alanı nesnesinin maksimum adet bilgisidir. Bu değer "/proc/sys/kernel" dizinindeki "shmmni" isimli dosyadan elde edilir. Şimdi "System 5" paylaşılan bellek alanlarını bitirdiğimize göre, "POSIX" paylaşılan bellek alanlarını görmeye geçebiliriz. >>>> Paylaşılan Bellek Alanları: "POSIX" paylaşılan bellek alanları, daha önce gördüğümüz "POSIX" mesaj kuyruklarına benzer bir kullanıma sahiptirler. "System 5" versiyona nazaran burada "key" değerleri değil, dosya ismi kullanılmaktadır. Dolayısıyla sanki iş bu "IPC" nesnesi bir dosya gibi ele alınmaktadır. Ayrıca "POSIX" paylaşılan bellek alanları, "Memory Mapped File" denilen olguyla da birleştirilmiştir. "POSIX" versiyonlar daha genç olduğu için yine taşınabilirlik problemi çıkartabilir ama pek çok sistem bu arayüzü destekler niteliktedir. Yine hatırlatmakta fayda vardır ki bu versiyona ait fonksiyonlar "librt" kütüphanesinde olduğu için derleme aşamasında "-lrt" seçeneği kullanılmalıdır. "POSIX" paylaşılan bellek alanları aşağıdaki aşamalardan geçerek kullanılmaktadır; -> "POSIX" paylaşılan bellek alanı nesnesini açmak ya da oluşturmak için "shm_open" kullanılır. Fonksiyonun prototipi aşağıdaki gibidir; #include int shm_open(const char *name, int oflag, mode_t mode); Fonksiyonun birinci parametresi, paylaşılan bellek alanı nesnesini belirtmektedir. Tıpkı "POSIX" mesaj kuyruklarında olduğu gibi bu ismin de kök dizindeki bir dosya ismi olarak verilmesi gerekmektedir. Fakat bazı sistemler, ilgili dosya ismi olarak, başka dizinlerdeki dosyaların ismini de kabul etmektedir. > Hatırlatıcı Notlar: >> Bir şeyi aşağı doğru nasıl yuvarlarız? Bölümünden kalan rakam kendisinden çıkartılarak. Örneğin, 13 rakamını 10'a yuvarlamak için 13'ün 10'a bölümünden kalan rakamı 13'ten çıkartırız. >> Bir bellek alanının başlangıcı "0x0000000..." biçiminde, sonu ise "0xFFFFFFF..." biçimindedir. >> Adres değerinde, sağdan üç basamağı sıfıra çekmek, aşağı yuvarlamaktır eğer sayfa sayısı 4096 ise. Çünkü 10'luk tabandaki 4096, 16'lık tabanda 1000 değeridir. >> Ek olarak "POSIX" Paylaşılan Bellek Alanları nesnelerini Linux sistemlerinde "/dev/shm" dizini içerisinde görüntüleyebiliriz. /*================================================================================================================================*/ (46_15_04_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları (devam): >>>> Paylaşılan Bellek Alanları (devam): Fonksiyonun ikinci parametresi, açış bayraklarını belitmektedir. Bu bayraklar şunlardan birisi olabilir; "O_RDONLY", "O_RDWR", "O_CREAT", "O_EXCL" ve "O_TRUNC". Bu bayrakların açıklamaları da şu şekildedir; "O_RDONLY" bayrağı, ilgili bellek alanının "read-only" modunda açılır. Sadece okuma yapabiliriz. "O_RDWR" bayrağı, ilgili bellek alanının "read-write" modunda açılır. Hem okuma hem yazma yapılabilir. "O_CREAT" bayrağı, ilgili bellek alanı yoksa yeni bir tanesinin oluşturulacağını belirtir. Halihazırda varsa eğer, var olan bellek alanı açılacaktır. "O_EXCL" bayrağı ise aslında "O_CREAT" ile birlikte kullanılır ve halihazırda bir tane paylaşılan bellek alanı var ise ilgili fonksiyonun başarısız olmasını sağlar. Aksi halde yeni bir bellek alanı oluşturulur. "O_TRUNC" bayrağı ise ilgili bellek alanının sıfırlanarak açılmasını sağlar. Yani içerisindeki bilgiler sıfırlandıktan sonra açılır. Bu bayraklardan "O_RDONLY" ve "O_RDWR" bayraklarını aynı anda kullanamayız. Fakat diğer üç bayrağı, bu iki bayraktan birisi/bir kaçı ile "bitwise-OR" işlemine sokabiliriz. Fonksiyonun üçüncü parametresi ise ilgili bellek alanına erişim bayraklarını belirtmektedir. Bu bayraklar "S_IXXX" biçimindeki bayraklardır. Eğer fonksiyonun ikinci parametresinde "O_CREAT" bayrağı kullanılmış ise iş bu üçüncü parametredeki bayraklar önem arz etmektedir. Aksi halde buraya geçilen argümanların bir önemi kalmamaktadır. Fonksiyonun geri dönüş değeri ise başarı durumunda bir "file descriptor", yani "fd" değeri döndürür. Halbuki "POSIX" mesaj kuyruklarında geri dönüşün "file descriptor" olması bir zorunluluk değildir. Başarısızlık durumunda ise "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. * Örnek 1, Aşağıdaki örnekte bir adet paylaşılan bellek alanı nesnesi oluşturulmuştur. /* write */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Örnekte de görüleceği üzere paylaşılan bellek alanının boyutu hakkında bir bilgi kullanmadık. Çünkü "POSIX" paylaşılan bellek alanlarını oluştururken değil, oluşturduktan sonra boyutlamamız gerekmektedir. -> "ftruncate" fonksiyonu bir "POSIX" fonksiyonu olup, iş bu boyutlandırmayı yapacak olan fonksiyondur. Daha evvel işlediğimiz yardımcı dosya fonksiyonlarında bu fonksiyona değinmediğimiz için şimdi değineceğiz. Fonksiyonun parametrik yapısı aşağıdaki gibidir; #include int ftruncate(int fildes, off_t length); Fonksiyonun birinci parametresi, üzerinde işlem yapılacak dosyaya ait "fd" değerini almaktadır. İkinci parametre ise o dosyanın yeni uzunluk değeridir. Bu fonksiyon genellikle küçültme işlemi için kullanılır ve küçültme sırasında dosyanın sonundaki ilgili kısım budanır/yok edilir. Küçültme işleminden sonra dosyanın yeni boyutu, bu fonksiyonun ikinci parametresi olacaktır. Öte yandan dosyanın boyutunu da büyütebiliriz. Büyütme işlemi sırasında, yeni gelen ekstra kısım sıfırlar ile doldurulur. Bu günkü sistemde, büyütme sonrası yeni gelen kısım, Dosya Delikleri ile sağlanmaktadır. Tabii ilgili dosya sisteminin de Dosya Deliklerini desteklemesi gerekmektedir. Fonksiyon başarı durumunda "0", başarısızlık durumunda "-1" ile geri döner ve "errno" değişkeni de uygun değere çekilir. Son olarak bu fonksiyonun işlev görebilmesi için prosesin ilgili dosya nezdinde "write" hakkında sahip olması gerekir. * Örnek 1, Aşağıdaki örnekte ilgili fonksiyonun kullanımına bir örnek verilmiştir. #include #include #include #include #define SHM_NAME "/sample_POSIX_shared_memory" void exit_sys(const char*); int main(int argc, char** argv) { int fd; if((fd = open("SHM_NAME", O_RDWR)) == -1) exit_sys("open"); /* Artık ilgili dosyanın büyüklüğü "1000" olmuştur. */ if(ftruncate(fd, 1000) == -1) exit_sys("ftruncate"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Tabii "ftruncate" fonksiyonunun "fd" yerine argüman olarak dosya ismi alan versiyonu da bulunmaktadır. Bu versiyonunun ismi ise "truncate" biçimindedir. İşlev bakımından iki fonksiyonun bir farkı bulunmamaktadır. Fonksiyonun parametrik yapısı aşağıdaki gibidir; #include int truncate(const char *path, off_t length); Fonksiyonun birinci parametresi üzerinde işlem yapılacak dosyanın yol ifadesi, ikinci parametresi ise o dosyanın yeni uzunluk bilgisidir. Fonksiyon başarı durumunda "0", başarısızlık durumunda "-1" ile geri döner ve "errno" değişkeni de uygun değere çekilir. Son olarak aynı boyut değerini bu iki fonksiyona geçmemize bir değişikliğe yol açmayacaktır. * Örnek 1, Aşağıdaki kodlar, bir önceki paragraftaki örnekteki kodların güncellenmiş halidir. /* write */ #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Şimdi sırada ilgili paylaşılan bellek alanı nesnesini bir yere "map" etmeliyiz. "System 5" paylaşılan bellek alanlarında, "map" etme yerine "attach" etme terimi kullanılmaktaydı. Bunun için "mmap" isimli POSIX fonksiyonu kullanılmaktadır. Fakat "mmap" isimli bu fonksiyon, sadece bu noktada "map" işlemi için değil daha genel bir kullanımı olan detaylı bir fonksiyonudur. Bu fonksiyon bir sistem fonksiyonudur ve detayları ileride ele alınacaktır. Kabaca şu şekilde özetleyebiliriz; #include void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off); Fonksiyonun birinci parametresi, "map" işlemi için önerilen adresi belirtmektedir. Tıpkı "System 5" paylaşılan bellek alanlarında olduğu gibi belli bir adresin kullanılması mümkündür fakat bu tavsiye edilmez. Bunun yerine bu parametre için "NULL" değerinin geçilmesi tavsiye edilir. Fonksiyonun ikinci parametresi ne kadarlık bir alanın "map" işlemine tabii tutulacağını belirtmektedir. Böylelikle paylaşılan bellek alanı nesnesini kısmet "map" edebiliriz. Öte yandan bu parametreye geçilen değerin sayfa katlarında olması bir zorunluluk değildir fakat bir çok sistem buraya geçilen değeri sayfa katı olacak şekilde yukarı doğru yuvarlamaktadır. Örneğin, bizler bu parametre için 100 bayt geçtiğini düşünelim. Sistem, 100 bayt yerine sayfa katı olacak şekilde yukarı doğru yuvarlayacağı için, 4096 bayt olarak işleyecektir. Ek olarak, erişim için burada belirtilen değeri kullanmalıyız. Bu değerden öte bir değeri erişim için kullanmak "Tanımsız Davranış" olacaktır. Yine buraya geçilen değerin "0" olmaması gerekmektedir. Fonksiyonun üçüncü parametresi ise "map" edilen sayfaların "protection" özelliklerini belirtmektedir. İşlemci düzeyindeki koruma özellikleridir bunlar. Anımsanacağın üzere sayfa tablosundaki sayfalar bir takım "attribute" lere sahiptir. İşte bizler bu parametreler ile iş bu "attribute" leri atıyoruz. Bu parametreye şu bayraklardan bir ya da bir kaç tanesini geçebiliriz; "PROT_READ", "PROT_WRITE", "PROT_EXEC" ve "PROT_NONE". Birden fazlasını bir arada kullancaksak, yine "bit-wise OR" işlemine sokmamız gerekmektedir. Bu bayrakların açıklamaları şu şekildedir; "PROT_READ" bayrağı, ilgili sayfanın "read-only" olacağını belirtmektedir. Böylesi sayfalara yazma yapılacağı zaman, işlemci "exception" oluşturur ve proses "SIGSEGV" sinyali ile sonlandırılır. "PROT_WRITE" bayrağı, ilgili sayfanın "write-only" olacağını belirtir. Eğer "PROT_READ | PROT_WRITE" biçiminde bir kombin yaparsak, ilgili sayfa hem okumaya hem yazmaya müsait olacaktır. "PROT_EXEC" bayrağı ise ilgili sayfada çalıştırılabilir bir kodun olması durumunda, iş bu kod parçacığının çalıştırılabileceğin belirtmektedir. "PROT_NONE" bayrağı ise ilgili sayfaya herhangi bir erişimin mümkün olamayacağını belirtmektedir. Yani bu bayrağa sahip olan bir sayfa, ne okunabilir ne de üzerinde yazma yapılabilir. Aslında bütün işlemciler buradaki özelliklerin hepsini desteklemeyebilir. Örneğin, Intel işlemcileri "PROT_WRITE" bayrağı okuma özelliğini de kapsamaktadır. Son olarak bu özellikler daha sonra da değiştirilebilir. Fonksiyonun dördüncü parametresi ise bir takım değerlerden oluşmaktadır. Bu değerler; "MAP_SHARED", "MAP_PRIVATE" ve "MAP_FIXED". Bu bayrakların açıklamaları şu şekildedir; "MAP_SHARED" bayrağı ise yazma işleminin normal bir şekilde yapılacağını ve karşı tarafın da yazılanları okuyacağını belirtmektedir. Normalde hemen her zaman bu bayrak kullanılmaktadır. "MAP_PRIVATE" bayrağı, "copy-on-write" semantiği için düşünülmüştür. Yazma yapmaya kalktığımız zaman, bu sayfanın bir kopyası oluşturulur ve bizler bu kopyaya yazma yaparız. Dolayısıyla karşı taraf, bizim yazdıklarımızı göremeyecektir. Tabii çıkartılan bu kopya, yine bizim prosesimize özgü bir kopyadır. Bu bayrağı kullanan proses, öteki proses bu alana yazma yaparsa, yazılanları okuyup okumayacağı POSIX standartlarınca "unspecified" olarak belirtilmiştir. Linux sistemlerinde de bu "unspecified" durum korunmuştur. Bu bayrağın kullanılması bazı özel durumlarda tercih edilmektedir. Örneğin, işletim sistemleri çalıştırılabilir dosyaların ".data" bölümlerini belleğe yüklerken "MAP_PRIVATE" bayrağını kullanıyor. Eğer aynı çalıştırılabilir dosya ikinci defa çalıştırılmışsa, ilk çalıştırılan ile ikincinin ".data" bölümü aynı yer oluyor. Ta ki yazma yapılana kadar. Yazma işleminden sonra ".data" bölümleri ayrılmaktadır. "MAP_FIXED" bayrağı, fonksiyonun birinci parametresindeki adres olarak "NULL" geçilmemesi durumunda, geçilen adres değerinin aynen kullanılmasını sağlar. Ek olarak bu bayrağı kullanmışsak, birinci parametredeki adresin sayfa katlarında olması, POSIX standartlarınca bir zorunluluk değildir. Eğer bu bayrağı kullanmazsak, birinci parametreye geçeceğimiz adres değeri işletim sistemi tarafından bir ipucu olarak ele alınacaktır. Bu bayraklardan "MAP_SHARED" ve "MAP_PRIVATE" olanlardan sadece birisi kullanılabilirken, "MAP_FIXED" bayrağı bu iki bayrak ile "bitwise-OR" işlemine sokulabilir. Linux sistemlerinde ise bu bayraklara ek olarak bayraklar da vardır. Bu bayraklardan en önemlisi "MAP_ANONYMUS" bayrağıdır. Bu bayrak kullanılarak yapılan "map" işlemine ise "anonymus mapping" denir. Fakat böylesi bir "map" işlemi dosya üzerinde yapılamamaktadır. Dolayısıyla bu işlem sırasında fonksiyona geçilen "fd" ve "offset" parametreleri dikkate alınmamaktadır. Artık "map" edilen yerin karşılığı "swap" dosyası olacaktır. Bu da bir nevi "malloc" ile bellek tahsisatı anlamına gelmektedir. Zaten "glibc" kütüphanesindeki "malloc" fonksiyonları arka planda "anonymus mapping" işlemi yapmaktadır. Pekiyi bizler "anonymus mapping" işlemini direkt olarak "malloc" yerine kullanabilir miyiz? Aslında evet. Fakat unutmamalıyız ki "malloc" bayt temelli tahsisat yaparken, "anonymus mapping" sayfa katları temelli tahsisat yapmaktadır. İş bu sebepten dolayıdır ki büyük miktarda tahsisat yaparken "anonymus mapping" yapmak daha hızlı ve etkili olabilir. Son olarak belirtmekte fayda vardır ki "MAP_ANONYMUS" bayrağı "MAP_PRIVATE" ya da "MAP_SHARED" ile birlikte kullanılır. Fakat ikisi arasında da şöyle bir fark vardır; "fork" işlemi sırasında "map" edilen alan da "child" prosese aktarılır. Eğer "MAP_ANONYMUS | MAP_PRIVATE" yaparsak, alt proses ile üst prosesin kullanacağı "map" alanı ayrışmaktadır (copy on write). Eğer "MAP_ANONYMUS | MAP_SHARED" yaparsak, alt proses ile üst prosesin kullanacağı "map" alanı ayrışmamaktadır. Yani iki proses de aynı "swap" dosyasını kullancaktır. Dolayısıyla alt-üst proses birbiriyle de haberleşebilir. Öte yandan "fork" işlemi hiç yapılmazsa, iki kullanım arasında bir fark kalmayacaktır. Linux sistemlerindeki bir diğer önemli bayrak da MAP_UNINITIALIZED" bayrağıdır. Bu bayrak kullanıldığında, tahsis edilen alan SIFIRLANMAYACAKTIR. Çünkü normal şartlarda, "anonymus mapping" sırasında, "mmap" ile tahsis edilen alanlar sıfırlanmaktadır. Fonksiyonun beşinci ve altıncı parametreleri bir adet "fd" ve "offset" değeri içindir. Buradaki "offset" değeri, paylaşılan bellek alanının o "offset" değerinden itibaren "map" edileceğini belirtmek için kullanılır. Örneğin, paylaşılan bellek alanımız 4MB olsun. Biz bu nesnenin 1MB'sinden itibarek, 64K'lık kısmını "map" edebiliriz. Fonksiyon başarı durumunda, "map" işlemi sonrasındaki sanal adrese geri dönmektedir. Başarısızlık durumunda ise "MAP_FAILED" değerine geri dönmektedir. Pek çok sistemde bu sembolik sabit aşağıdaki biçimde tanımlanmıştır; #define MAP_FAILED ((void*)-1) Öte yandan bu altıncı parametre, dördüncü parametrede "MAP_FIXED" kullanılması durumunda, birinci parametrenin gereksinim duyduğu hizalamada olmak zorundadır. Yani burada kastedilen şey şudur; Fonksiyonun birinci parametresine geçilen değer ile altıncı parametreye geçilen değerin, sayfa katlarına bölümünden elde edilen kalanı aynı olmalıdır. Örneğin, fonksiyonun birinci parametresine geçilen değer "5000" olsun. Sayfa katları da "4096" nın katları biçiminde olsun. İşte bu durumda "5000 % 4096" işleminden bizler "4" değerini kalan olarak elde edeceğiz. İşte altıncı parametreye geçilen değerin yine "4096" a bölümünden kalanı "4" olması gerekmektedir. Eğer "MAP_FIXED" kullanılmamış ise böyle bir zorunluluk yoktur. * Örnek 1, Aşağıdaki örnekte senkronizasyonu karadüzen sağlanmış iki proses paylaşılan bellek alanı kullanarak haberleşmektedir. /* write */ #include #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); void *shmaddr; if((shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); char buffer[4096]; char* str; for(;;) { printf("Text: "); if(fgets(buffer, 4096, stdin) == NULL) continue; if((str = strchr(buffer, '\n')) != NULL) *str = '\0'; strcpy((char*)shmaddr, buffer); if(!strcmp((char*)shmaddr, "quit")) break; } printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); void *shmaddr; if((shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); for(;;) { printf("Press ENTER to read...\n"); getchar(); puts((char*)shmaddr); if(!strcmp((char*)shmaddr, "quit")) break; } printf("Ok\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "anonymus mapping" yapılarak alan tahsisatı yapılmıştır: #include #include #include #include #include void exit_sys(const char*); int main(void) { char* faddr; /* * "ANONYMOUS MAPPING" sırasında "fd" yerine "-1" geçilmesi Linux dökümanlarında belirtilmiştir. "offset" * değeri de dikkate alınmayacağı için "0" geçilmiştir. Tıpkı "malloc" ile yer tahsisatı yapmışız gibi * olacaktır. */ if((faddr = (char*)mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0)) == MAP_FAILED) exit_sys("mmap"); strcpy(faddr, "ulya_yuruk"); printf("Press ENTER to continue!...\n"); getchar(); puts(faddr); if(munmap(faddr, 4096) == -1) exit_sys("unmap"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Paylaşılan Bellek Alanlarında bizim dikkat etmemiz gereken şey, ister "System 5" ister "POSIX" olsun, ilk önce iş bu nesneyi oluşturuyor, daha sonra bu nesnenin büyüklüğünü belirtiyor ve son olarak bu nesneyi "attach/map" yaparak proseslerin sayfa tablolarına eklemekteyiz. Son olarak bir paylaşılan bellek alanı nesnesi, bir prosesin sanal tablosuna birden fazla kez "attach/map" edilebilir. > Hatırlatıcı Notlar: >> Dosya Delikleri, bazı dosya sistemlerinin desteklediği bir özelliktir. Bir dosyayı "ftruncate" ya da "truncate" ile büyütmek istediğimiz zaman dosya gerçekte büyümüyor. Yani, disk üzerinde yeni alan için henüz bir alan tahsisi gerçekleştirmiyor. Fakat dosya sisteminde bu yeni alanın bilgilerini bir şekilde saklıyor ve dosyanın boyutunu yeni boyut olarak gösteriyor. Eğer okuma yaptığımız nokta, bu yeni alana denk gelirse bizlere "0" gösteriyor. Fakat ne zamanki bu yeni alana bir yazma işlemi yapalım, işte o vakit gerçekten de disk üzerinde alan tahsisi yapıyor. /*================================================================================================================================*/ (47_16_04_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları (devam): >>>> Paylaşılan Bellek Alanları (devam): -> Paylaşılan Bellek Alanı nesnesini kullandıktan sonra, "map" edilen alanın geri verilmesi yani boşaltılması gerekmektedir. Nasılki "malloc" çağrısı için "free" çağrısını yapıyorsak, "map" işlemi için de "unmap" işlemini yapmamız gerekmektedir. Bu işi görecek POSIX fonksiyonu ise "munmap" fonksiyonudur. Fonksiyonun parametrik yapısı aşağıdaki gibidir; #include int munmap(void *addr, size_t len); Fonksiyonun birinci parametresi, daha önce "map" edilen alanın başlangıç adresidir. Yani "mmap" fonksiyonunun geri dönüş değeri. Fonksiyonun ikinci parametresi ise "unmap" edilecek alanın uzunluk bilgisidir. Bu fonksiyon, birinci parametresindeki adresten başlayan ve ikinci parametresindeki uzunluk kadar olan alanı "unmap" etmektedir. Eğer başlangıç noktası bir sayfanın ortalarında ve bitiş noktası ise başka sayfanın ortalarına denk geliyorse, bu iki sayfa ve aradaki sayfaların hepsi komple "unmap" edilmektedir. Öte yandan POSIX standartları, birinci parametredeki adresin sayfa katlarında olması için bir zorlama yapabilir fakat Linux standartları bunu zorunlu kılmaktadır. Ek olarak, birinci parametreye geçilen adres değeri daha önce "mmap" ile elde edilmemişse, bu durum "unspecified" olarak POSIX standartlarında tanımlanmıştır. Eğer daha önce "unmap" edilmiş bir alanı tekrar "unmap" edersek, fonksiyon herhangi bir değişikliğe sebebiyet vermeyecektir. Böylesi bir durumda da fonksiyon BAŞARISIZ olmamaktadır. Fonksiyonun geri dönüş değeri ise başarı durumuna göre "0", başarısızlık durumuna göre "-1" biçimindedir ve "errno" uygun değere çekilir. Son olarak şu noktaya dikkat etmeliyiz; uzun bir "map" edilmiş alanımız olsun. Bizler "unmap" ederken bu alanın başlangıç ve bitiş noktalarını değil, orta kısımlardan iki nokta seçmiş olalım. "unmap" işlemi sonrasında artık iki adet "map" edilmiş alanımız oluşacaktır. Çünkü bizler orta kısımları "unmap" ettiğimiz için, kenarda kalanlar bundan etkilenmeyecektir. Buna da "Partial Mapping" diyebiliriz. Unutmamalıyız ki prosesin ömrü bittiğinde sistem tarafından otomatik olarak "unmap" işlemi gerçekleştirilecektir. -> Artık paylaşılan bellek alanı nesnesi yok edilebilir. Bunun için "shm_unlink" fonksiyonunu kullanmalıyız. Eğer ilgili nesne yok edilmezse, sistem ilk "reboot" olana dek, hayatını sürdürecektir. İlgili fonksiyonun prototipi şu şekildedir; #include int shm_unlink(const char *name); Burada fonksiyonumuz paylaşılan bellek alanı nesnesinin ismini alır ve onu yok eder. Başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri döner. Tabii yok etme işinin de yine hayata getiren prosese bırakılması tavsiye edilendir. Gerçek manada silmenin gerçekleşmesi için yine ilgili paylaşılan bellek alanı nesnesini kullanan bütün proseslerin, bu nesneyi "unmap" etmesi gerekmektedir. * Örnek 1, /* write */ #include #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); void *shmaddr; if((shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); char buffer[SHM_SIZE]; char* str; for(;;) { printf("Text: "); if(fgets(buffer, SHM_SIZE, stdin) == NULL) continue; if((str = strchr(buffer, '\n')) != NULL) *str = '\0'; strcpy((char*)shmaddr, buffer); if(!strcmp((char*)shmaddr, "quit")) break; } printf("Ok\n"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include /* İlgili mesaj kuyruklarının erişim hakları için gerekli. */ #include #define SHM_NAME "/sample_POSIX_shared_memory" #define SHM_SIZE 4096 void exit_sys(const char*); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); void *shmaddr; if((shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); close(shmfd); for(;;) { printf("Press ENTER to read...\n"); getchar(); puts((char*)shmaddr); if(!strcmp((char*)shmaddr, "quit")) break; } printf("Ok\n"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi yapılan örneklerde de görüldüğü üzere, "shm_open" ile elde ettiğimiz "fd" değişkeni "main" fonksiyonlarının değişik yerlerinde "close" çağrıları ile geri verilmiştir. Çünkü bu kapatma işleminin "mmap" işlemine bir etkisi yoktur. Tabii eğer "stat/fstat" gibi fonksiyon çağrıları yapacaksak, kapatma işlemini bu çağrılardan sonra yapmalıyız. Yani burada kastedilen şey kapatma işlemi ile "mapping" işleminin birbirinden bağımsız olmasıdır. "close" ile "unmap" YAPILMAMAKTADIR. Paylaşılan Bellek Alanları nesnesini kullanırken dikkat etmemiz gereken nokta, eğer bir kuyruk sistemi oluşturulmamışsa, yeni bilgiler eskilerinin üzerine yazılmaktadır. Şu vakte kadarki örneklerde, hep üzerine yazma yapılmıştır ve senkronizasyon kara düzen sağlanmıştır. Bütün bunlara ek olarak belirtmekte fayda vardır ki paylaşılan bellek alanları nesneleri "Bellek Tabanlı Dosya" mekanizması ile de ilişki içerisindedir. >>>>> Bellek Tabanlı Dosyalar ("Memory Mapped Files"): Bu mekanizma, 90'lı yıllarda "POSIX IPC" nesneleriyle birlikte, UNIX türevi sistemlere eklenen bir mekanizmadır. Benzer yıllarda da Windows ve macOS türevi işletim sistemlerine de eklenmiştir. Pekiyi nedir bu mekanizma? Diskteki bir dosyanın belleğe çekilmesi ve bellekte yapılan işlemlerin diskteki dosya üzerinde de etkisinin görülmesidir. Anımsanacağınız üzere diskteki bilgileri değiştirmek için ilk önce ilgili dosyayı açıyor, daha sonra dosya sonuna kadar "read" ile okuyup bilgileri belleğe çekiyorduk. Devamında değişiklik yapıp, yeni halini "write" ile tekrardan dosyaya yazıyorduk. Fakat bu mekanizmaya sahip dosyaları bizler yine açıyoruz. Açma işleminden sonra ilgili dosya, prosesin sanal bellek alanına enjekte edilmektedir. Dolayısıyla bu aşamada yapacağımız değişiklikler dosyaya direkt yansımaktadır. Prosesin sanal bellek alanına enjekte edildiği için, aynı dosyayı başka prosesler de açarsa, prosesler arası haberleşme olarak da kullanılabilir. Pekiyi bizler bu mekanizmayı nasıl kullanabiliriz? Şu adımları takip ederek, bu mekanizmadan faydanalabiliriz: -> Açılacak olan dosya belirlendikten sonra, "open" fonksiyonu ile bu dosya açılır. -> Daha sonra direkt olarak "mapping" işlemi açılmış olan bu dosyaya uygulanır. -> "mapping" işleminden sonra ya da "main" fonksiyonunun sonunda "close" çağrısı ile "open" ile elde ettiğimiz "fd" değeri geri verilebilir. -> Her ne kadar prosesin sonlanması ile "map" edilen alanlar otomatik olarak "unmap" edilecekse de bizler elle "unmap" işlemi yapmalıyız. -> Eğer bizler kendimizin hayata getirdiği dosyayı açmıyorsak, "unlink" ya da "remove" fonksiyon çağrıları yapmamalıyız. * Örnek 1, Aşağıda, yukarıda anlatılanlar için, bir örnek bulundurulmuştur. #include #include #include #include #include #include #include void exit_sys(const char*); int main(int argc, char** argv) { /* i. Dosyayı açtık. */ int fd; if((fd = open("main.c", O_RDWR)) == -1) exit_sys("open"); /* ii. Dosyanın uzunluk bilgisini temin ettik. */ struct stat finfo; if(fstat(fd, &finfo) == -1) exit_sys("fstat"); /* * iii. Dosyayı, prosesin sanal bellek alanına "map" ettik. "finfo.st_size" değerinin * "0" olmadığından emin olunuz. Aksi halde "mmap" fonksiyonu başarısız olacaktır. */ char* faddr; if((faddr = (char*)mmap(NULL, finfo.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); /* iv. Daha sonra dosyadakileri işledik. */ for(int i = 0; i < finfo.st_size; ++i) putchar(faddr[i]); memcpy(faddr, "ankara", 6); /* * v. Programın akışı tam bu noktadayken ilgili dosyayı kontrol ettiğimiz zaman * göreceğiz ki "ankara" yazısı dosyanın başına yazılmıştır. Dosyada halihazırda * karakter bulundurduğu için, oradaki karakterlerin üzerine yazılacaktır. Yapılan * değişiklik direkt olarak dosyaya yansıyacaktır. */ getchar(); /* vi. Artık "unmap" yapabiliriz. */ if(unmap(faddr, finfo.st_size) == -1) exit_sys("unmap"); /* vii. Artık "fd" değerini geri verebiliriz. */ close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bu mekanizmayı kullanırken dikkat etmemiz gereken noktalardan birisi de "open" fonksiyonuna geçtiğimiz açış mod bilgileri ile "mmap" fonksiyonuna geçilen bilgilerin birbiriyle uyumlu olmasıdır. Hakeza "mmap" fonksiyonua geçilen bilgilerin de birbiriyle uyumlu olması gerekmektedir. Aksi halde fonksiyonlar başarısız olacak ve "errno" değişkeni uygun değere çekilecektir. Örneğin, bizler dosyayı açarken "O_RDWR" modunda açalım. "mmap" fonksiyonunda da yalnızca "PROT_READ" modunu kullanalım. Bu durumda, prosesimizin dosyaya yazma hakkı olsa bile, "PROT_READ" bayrağını kullandığımız için "mmap" fonksiyonu başarısız olacaktır. Benzer biçimde "open" fonksiyonunda "O_WRONLY" bayrağı kullansak ve "mmap" için de "PROT_WRITE" bayrağını kullansak, yine sorunla karşılaşabiliriz. Çünkü bazı sistemlerdeki "PROT_WRITE" bayrağı aynı zamanda "PROT_READ" bayrağını da içermektedir. Dolayısıyla dosyayı açarken "O_RDWR" modunun kullanılmasını istemektedir. Buradan hareketle yalnızca "O_WRONLY" bayrağını kullanmamalıyız. Bu kural POSIX standartlarında bellek tabanlı dosyaları açarken ve "shm_open" fonksiyonu için geçerlidir. Kural gereği açım sırasında "read" özelliğinin de bulunması gerekmektedir. Son olarak Bellek Tabanlı Dosyalar büyütülemez. Fakat bizler normal dosyaları "truncate"/"ftruncate" fonksiyonlarıyla ya da dosya konum göstericisini "EOF" konumuna çekip, bu konumdan itibaren yazma yaparak büyütebiliriz. Diğer yandan bizler, dosyanın büyüklüğünden daha büyük alanları "map" edebiliriz. Örneğin, dosyamızın büyüklüğü "10278" bayt olsun. Bizler de "mapping" sırasında büyüklük olarak "20000" değerini, "offset" olarak da "0" değerini geçelim. Şimdi "10278" bayt büyüklüğündeki bir dosya 3 sayfa yer kaplayacaktır "(4096 * 2 + 2086)". Fakat üçüncü sayfadaki "2010" (4096*3 - 10278) bayt boş kalacaktır. Eğer bizler üçüncü sayfadaki boş kalan "2010" baytlık alana erişmek istersek bir sorun olmayacaktır. Fakat dördüncü sayfanın başından itibaren, "20000" inci bayta kadarlık alana erişmeye çalışırsak, yani "12288" bayt ile "20000" bayt arası, programımız çökecektir. O vakit bizler hangi durumlarda, 3. sayfada boş kalan "2010" bayta erişmeli hangi durumlarda "12288" bayt ile "20000" bayt arasına erişmeye çalışmalıyız? (to be continued) /*================================================================================================================================*/ (48_29_04_2023) > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "System 5 IPC" fonksiyonları ve "POSIX IPC" fonksiyonları (devam): >>>> Paylaşılan Bellek Alanları (devam): >>>>> Bellek Tabanlı Dosyalar ("Memory Mapped Files"): Anımsanacağınız üzere bir dosyayı büyütmek için, dosya konum göstericisini dosyanın sonuna çekmek ve o konumdan itibaren "write" işlemi yapmamız gerekiyordu. Yöntemlerden birisi de buydu. Fakat Bellek Tabanlı Dosyaları, dosyanın büyüklüğünden daha fazla büyüklükte "map" etmek ve fazla kısma "write" yaparak, büyütemeyiz. Öte yandan Bellek Tabanlı Dosyalarda yapılan değişikliklerin direkt olarak disk üzerinde değişikliğe yol açtığından daha önce bahsetmiştik. Aslında bu durum, POSIX standartlarınca belli şartlar altında garanti edilmiştir. Pekiyi nedir bu şartlar? "msync" isimli fonksiyonun çağrılması. İş bu fonksiyon çağrıldığı müddetçe Bellek Tabanlı Dosya'da yapılan değişiklik, diske yansıtılacak ya da diskte yapılan değişiklik Bellek Tabanlı Dosya'ya yansıtılacaktır. >>>>>> "msync" : Fonksiyon aşağıdaki prototipe sahiptir; #include int msync(void *addr, size_t len, int flags); Fonksiyonun birinci parametresi, "mmap" ile elde ettiğimiz adres bilgisidir. Bu adres bilgisinin sayfa katlarında olması POSIX tarafından bir zorunluluk değildir. Fakat Linux türevi sistemlerde böylesi bir zorunluluk vardır. Fonksiyonun ikinci parametresi ise "flush" edilecek alanın büyüklük bilgisidir. Tipik olarak bu değer, "mmap" fonksiyonunda kullandığımız büyüklük bilgisi olabilir. Fakat burada kullandığımız değer, sayfa katları olacak şekilde yukarı doğru yuvarlanmaktadır. Örneğin, sayfa katları 4096 biçiminde olsun. Bizler de 3000 değerini girelim. Yukarı doğru yuvarlandığı için sanki 4096 değerini geçmişiz gibi olacaktır. Fonksiyonun son parametresi ise şu bayraklardan birisini almaktadır; "MS_SYNC", "MS_ASYNC" ve "MS_INVALIDATE". >>>>>>> "MS_SYNC" : Buradaki yön bilgisi bellekten diske doğrudur. Yani Bellek Tabanlı Dosya üzerinde bir değişiklik yapıldığında, ilgili değişikliğin diskteki dosya üzerinde yansıtılmasıdır. Tabii, "msync" fonksiyonu geri döndükten sonra, "flush" işleminin bitmesi garantidir. >>>>>>> "MS_ASYNC" : "MS_SYNC" bayrağı gibidir. Ancak bu bayraktan farkı, "flush" işlemi başlatıldıktan sonra "msync" fonksiyonunun hemen geri dönmesidir. Dolayısıyla "flush" işleminin bitmiş olması garnati değildir. >>>>>>> "MS_INVALIDATE" : Buradaki yön bilgisi diskten belleğe doğrudur. Yani başka bir proses diskteki dosyayı güncellediğinde, ilgili değişikliğin Bellek Tabanlı Dosya üzerinde yansıtılmasıdır. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda "-1" biçimindedir. Öte yandan "munmap" işlemi sırasında da "msync" işlemi gerçekleştirilmektedir. Dolayısıyla bir proses "munmap" yapmadan sonlansa bile "munmap" çağrılacağı için yine "msync" işlemi uygulanacaktır. Öte yandan Linux sistemleri, başka bir yaklaşık sergilemektedir. Şimdi bu yaklaşımı incelemeden evvel "write" ve "read" fonksiyonlarının nasıl çalıştığını detaylarıyla öğrenmeliyiz; >>>>>> "write" fonksiyonu ile normal bir dosyaya yazma yaptığımız zaman aslında direkt olarak diske yazma işlemi gerçekleşmez. İlk önce "kernel" içerisinde bulunan "buffer cache" veya "page cache" isimli bir tampona yazılır. Bu tampon, ismine "kernel daemon" diyeceğimiz bir başka akış tarafından belirli aralıklarla "flush" edilerek içerisindekiler diske aktarılır. Böylelikle disk üzerindeki yazma işlem adedinin azaltılarak ömrünün uzun olması, işi yapan sistem fonksiyonunu daha az çağırarak sistem performansını arttırmak vb. şeyler hedeflenmektedir. Bu konu esasında "IO Scheduling" konusu ile ilgilidir. Tabii iş bu tamponun "flush" edilme sıklığı da önemlidir. Çünkü çok fazla beklenirse, elektrik kesilmesi gibi durumlarda, bilgi kayıpları oluşabilir. >>>>>>> "IO Scheduling" : Diske yazılacak ya da diskten okunacak bilgilerin bazılarının bir araya getirilerek belli bir sırada işleme sokulmasıdır. >>>>>> "read" fonksiyonu ise "write" fonksiyonunun kullandığı tamponları kullanmaktadır. Çünkü POSIX standartlarınca "write" fonksiyonu geri döndüğünde, artık aynı dosyaya yapılan bir sonraki "read" işlemiyle o yazdıklarımızın okunması garanti altındadır. Linux sistemlerinde Bellek Tabanlı Dosyalar ise "msync" fonksiyon çağrısı yerine, haberleşmek için kullanılacak olan ve prosesin sanal sayfa tablosuna enjekte edilen bellek alanı, aslında yukarıda zikredilen "buffer cache" / "page cache" isimli tamponlara yönlendirilmiştir. Dolayısıyla haberleşecek iki proses, aslında bu "buffer cache" / "page cache" isimli tamponları kullanarak haberleşmektedir. Bu duruma ise "Unified File System" tasarımı denmektedir. Tabii taşınabilirlik açısından bizlerin "msync" kullanması tavsiye edilmektedir. * Örnek 1, Aşağıdaki örnekte "msync" kullanılmamıştır. Haberleşmenin sağlanması için içi dolu "test.txt" dosyasının mevcut olması gerekmektedir. Daha sonra ilk "write", sonrasında da "read" programını çalıştırmalıyız. Görüleceği üzere prosesler hayatlarını devam ettiriyor olmalarına rağmen yapmış oldukları değişiklik yansıtılmaktadır. Çünkü Linux sistemlerinde kullanılan "Unified File System" tasarımı kullanılmıştır. /* read */ #include #include #include #include #include #include #include void exit_sys(const char*); int main(void) { int fd; if((fd = open("test.txt", O_RDWR)) == -1) exit_sys("open"); struct stat finfo; if(fstat(fd, &finfo) == -1) exit_sys("fstat"); printf("Press ENTER to read!...\n"); getchar(); char* faddr; if((faddr = (char*)mmap(NULL, finfo.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); printf("[%lld] : ", (long long)finfo.st_size); for(int i = 0; i < finfo.st_size; ++i) putchar(faddr[i]); printf("\nPress ENTER to exit!...\n"); getchar(); if(munmap(faddr, finfo.st_size) == -1) exit_sys("unmap"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* write */ #include #include #include #include void exit_sys(const char*); int main(void) { int fd; if((fd = open("test.txt", O_WRONLY)) == -1) exit_sys("open"); printf("Press ENTER to write!...\n"); getchar(); if(write(fd, "abbccc", 6) == -1) exit_sys("write"); printf("Press ENTER to exit!...\n"); getchar(); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "msync" isimli fonksiyon kullanılmıştır. #include #include #include #include #include #include #include void exit_sys(const char*); int main(void) { int fd; if((fd = open("test.txt", O_RDWR)) == -1) exit_sys("open"); struct stat finfo; if(fstat(fd, &finfo) == -1) exit_sys("fstat"); char* faddr; if((faddr = (char*)mmap(NULL, finfo.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); memcpy(faddr, "ulya_yuruk", 10); /* * Bellekte yapılan değişiklikler, diske yansıtılmıştır. */ if(msync(faddr, finfo.st_size, MS_SYNC) == -1) exit_sys("msync"); printf("Press ENTER to exit!...\n"); if(munmap(faddr, finfo.st_size) == -1) exit_sys("unmap"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Anımsanacağınız üzere "private mapping" yapmanın asıl amacı "Copy On Write" mekanizmasından faydalanmaktır. Böylelikle haberleşecek iki proses aynı "page cache" isimli tamponu kullanacak, ancak "write" yaptığında bu tamponun bir kopyası çıkartılıp ona yazılacaktır. Bu mekanizma özellikle şu noktalarda kullanılmaktadır; "executeable" dosyalar ve "dynamic libraries" dosyalarının belleğe yüklenmesi. Örneğin, "sample" isimli çalıştırılabilir bir dosyamız olsun. Bu dosyamız ise "ELF" dosya türünden olsun. Bu dosya kabaca şu şu içeriklere sahiptir; ".text", ".data", ".bsdd", vb. "exec" fonksiyonları "ELF" formatında bir dosyayı belleğe aktarırken her bir bölümü "private mapping" yaparak yüklemektedir. Dolayısıyla aynı programı ikinci kez çalıştırdığımızda, mümkün olduğunda aynı fiziksel bellek kullanılmaktadır. Eğer bunlardan birisi "write" işlemi yaparsa, "copy on write" mekanizmasıyla, ikisinin fiziksel belleği farklılaştırılmaktadır. Diğer yandan "exec" fonksiyonları, yukarıda belirtilen bölümleri belleğe aktarırken, ilk değer verilmemiş global değişkenlerin bulunduğu ".bss" alanını sıfırlayarak belleğe aktarmaktadır. Buradaki sıfırlama işlemi "mapping" sırasında yapılmaktadır. /*================================================================================================================================*/ (49_30_04_2023) > Prosesin sayfalarının "lock" edilmesi: Anımsanacağınız üzere prosesin sanal bellek tablosu dolduğunda, işletim sistemi bazı programların daha az kullanılan sayfalarını ilgili tablodan çıkartıyordu ("swap out"). İşte bunu engellemek için de bir mekanizma geliştirilmiştir. Bu mekanizma sayesinde "swap out" engellenmektedir. Böylelikle şu iki avantaja sahip olabiliriz; -> "swap in/out" yapılmayacağı için görece bir hız kazancı elde edebiliriz. Fakat "Real Time OS" sistemlerde bu tip kız kazançları önemli olabilir. -> "swap" dosyalarından bilgi çalmak isteyen kişiler de engellenmiş olur. Bu mekanizmayı bir takım alternatif yönmtemlerle kullanabiliriz. Örneğin, "mmap" fonksiyonunun dördüncü parametresine "MAP_LOCKED" bayrağını geçmemiz, "mapping" sırasında ilgili sayfayı da kilitleyecektir. Fakat bu bayrak POSIX standartlarında mevcut değildir. Linux ve türevi sistemlerde, belli bir "kernel" serisinden itibaren kullanılabilir. Buna ek olarak "mlock" fonksiyonlarını kullanabiliriz. Son olarak bir prosesin, normal kullanıcılar tarafından, en fazla kaç sayfasının kilitlenebileceği "RLIMIT_MEMLOCK" isimli değer ile sınırlandırılmıştır. >> "RLIMIT_MEMLOCK" : Bünyesinde iki adet alt limit barındırır. Bunlar "soft-limit" ve "hard-limit" değerleridir. Her iki limit değerinin kaç olduğu "ulimit -a" kabuk komutu ile öğrenebiliriz. Örneğin, bazı sistemlerde "64 kB", bazılarında "497904 kB" değerindedir. Tabii uygun önceliğe sahip prosesler ("root" veya "appropriate priviledged users") bu limit değerine takılmamaktadır. Şimdi de "lock" işlemini gerçekleştirme yöntemlerini inceleyelim. >> "mlock" isimli fonksiyon aşağıdaki prototipe sahiptir: #include int mlock(const void *addr, size_t len); Fonksiyon birinci parametresiyle başlangıç adresini, ikinci parametresiyle de uzunluk bilgisini almaktadır. Buradaki "addr + len" içerisinde kalan bütün sayfalar "lock" edilmektedir. Örneğin, "addr" adresi bir sayfanın ortasına denk gelmektedir. "addr + len" konumu da bir sonraki sayfaya tekabül eder olsun. Bu fonksiyon her iki sayfayı da "lock" etmektedir. POSIX standartlarınca "addr" değerinin sayfa katlarında olması bir zorunluluk değildir. Bu zorunluluğu işletim sistemini yazanlara bırakmıştır. Benzer şekilde Linux sistemlerinde de bu değerin sayfa katlarında olması bir zorunluluk değildir. İkinci parametre herhangi bir değerde olabilir. Fonksiyonun geri dönüş değeri ise başarı durumuna göre "0", başarısızlık durumunda "-1" ile geri döner ve herhangi bir değişiklik yapmaz. Yani ("No changes is made to any locks"). Tabii burada kısmi kilitleme de yapılmamaktadır. "errno" değişkeni de uygun biçimde "set" edilir. Bu fonksiyon ile genellikle "mapping" yaptığımız adresleri kilitleriz. * Örnek 1, Aşağıdaki örnekte ilgili adres değeri sayfa katları olacak şekilde yeniden ayarlanmıştır. #include #include #include #include void exit_sys(const char* msg); char buffer[4096]; int main(void) { /* # OUTPUT # Address : [0x556bbafaa000] Ok */ void* addr = (void*)((uintptr_t)buffer & ~0xFFF); printf("Address : [%p]\n", addr); if(mlock(addr, 4096) == -1) exit_sys("mlock"); puts("Ok"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ilgili adres değeri direkt olarak kullanılmıştır. #include #include #include void exit_sys(const char* msg); char buffer[4096]; int main(void) { /* # OUTPUT # Address : [0x563cdb255040] Ok */ printf("Address : [%p]\n", buffer); if(mlock(buffer, 4096) == -1) exit_sys("mlock"); puts("Ok"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bu fonksiyon ile kilitlenmeye çalışılan sayfalar o anda bellekte değilse, bu fonksiyon ilgili sayfaları önce belleğe almakta ve devamında kilitleme işlemini gerçekleştirmektedir. Dolayısıyla bu fonksiyon başarılı bir şekilde geri döndüyse, ilgili sayfaların kilitlenmiş olması garanti edilmiştir. >> "mmap" fonksiyonu kullanılarak sayfaların kilitlenmesi: "mmap" fonksiyonunun "flags" isimli dördüncü parametresine "MAP_LOCKED" bayrağını geçerek "map" yaptığımız alanları aynı zamanda "lock" edebiliriz. Tabii bu işlem sırasında da "RLIMIT_MEMLOCK" isimli limit değerine takılabiliriz. Fakat Linux sistemlerindeki bu bayrak, talep edilen bütün sayfaları belleğe çekemeyebilir. Bu durumda ilgili "mmap" fonksiyonu BAŞARISIZ OLMAMAKTADIR. >> "mlockall" fonksiyonu: Bir POSIX fonksiyonu olup, bir prosesin bütün sayfalarını "lock" eder. Aşağıdaki parametrik yapıya sahiptir: #include int mlockall(int flags); Fonksiyonun parametresi şu bayrak değerlerinden bir yada bir kaçını alabilir: "MCL_CURRENT" ve "MCL_FUTURE". Bu bayraklardan, -> "MCL_CURRENT" : An itibariyle bellekte bulunan bütün sayfaların kilitleneceği anlamına gelir. -> "MCL_FUTURE" : Şu andan itibaren belleğe aktarılacak olan bütün sayfaların kilitleneceği anlamına gelir. Bu bayrakları "bit-wise OR" işlemine sokarak beraber kullanabiliriz. Fonksiyonun geri dönüş değeri de başarı ya da başarısızlık durumuna göre sırasıyla "0" ya da "-1" biçimindedir. Fakat bu fonksiyonu kullanırken "RLIMIT_MEMLOCK" isimli limit değerine takılabiliriz. > Prosesin sayfalarının "unlock" edilmesi: Bu işlemi gerçekleştirmek için "munlock" isimli POSIX fonksiyonunu kullanabiliriz. >> "munlock" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int munlock(const void *addr, size_t len); Fonksiyonun birinci parametresi "unlock" edilecek sayfaya ilişkin adres bilgisi, ikinci parametre ise uzunluk bilgisidir. Tıpkı "mlock" fonksiyonunda olduğu gibi ilgili adres bilgilerinin bulunduğu sayfalar "unlock" edilecektir. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri dönecektir. Aşağıdaki biçimde kullanımı vardır: if(munlock(buffer, 4096) == -1) exit_sys("munlock"); Tabii programın sonlanmasıyla otomatik olarak "unlock" işlemi gerçekleştirilmektedir. Öte yandan birden fazla "lock" işlemini de tek bir "munlock" fonksiyon çağrısı ile "unlock" yapabiliriz. Bütün bunlara ek olarak, "munlockall" isimli POSIX fonksiyonu ile bir prosesin bütün sayfalarını "unlock" edebiliriz. >> "munlockall" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int munlockall(void); Tabii bu fonksiyon da yukarıda belirtilen "RLIMIT_MEMLOCK" değerine takılabilir. > "Threads in POSIX" : >> "thread" kavramı, 90'lı yıllarda işletim sistemlerinde kullanılmaya başlanmıştır. Fakat ilk ortaya çıkış tarihi evvelki yıllara dayanmaktadır. >> Bizler bu konu altında yüksek seviyeli dillerin "thread" kütüphanelerini değil, işletim sisteminin "thread" kavramı için sunduğu hizmetleri göreceğiz. Sonuçta C++ dili, C# ve Java gibi dillerdeki "thread" kütüphaneleri, işletim sisteminin "thread" kavramı için sunduğu hizmetleri bir nevi "wrap" etmektedir ve bu dillerdeki ilgili kütüphaneler "thread" kavramını "cross-platform" yani platform-bağımsız hale getirmektedir. C diline de C99 ile eklenmiş fakat "optional" olarak bırakılmıştır. "MSVC" derleyicileri tarafından henüz desteklenmemekte, "glibc" kütüphanesinde desteklenmektedir. Özetle; bu konu altında anlatılacak başlıklar, yukarıdaki dillerin kütüphanelerince kullanılmaktadır. >> Pekiyi "thread" nedir? Bir prosesin bağımsız olarak çizelgelenen akışlarına denmektedir. Anımsanacağınız üzere işletim sisteminin çizelgeleyicisi, prosesleri değil "thread" leri "quanta" süresi boyunca işletmektedir. Tabii "quanta" süresi dolduktan sonra yeni gelecek "thread" aynı proses de ait olabilir başka bir prosese de ait olabilir. Buradan hareketle diyebiliriz ki bir proses yalnızca bir "thread" den değil, birden fazla "thread" den oluşabilir. >> Bir proses çalışmaya tek bir "thread" ile başlar. Örneğin, "fork" işlemi ile alt proses oluşturduğumuzda alt proseste sadece bir adet "thread" oluşmaktadır. Halihazırda üst proseste bulunan diğer "thread" ler, programcı tarafından oluşturulmalıdır. Benzer biçimde "exec" yaptıktan sonra yeni proses tek bir "thread" ile çalışmaya başlar. Önceki proseste bulunan "thread" ler yok edilir. >> "thread" mekanizması şu faydaları sağlamaktadır: >>> Arka planda yapılması istenen işler için güzel bir araçtır. Çünkü bizim prosesimiz "block" olduğunda aslında "thread" bloke edilmektedir. Örneğin, klavyeden bir karakter okumak isteyelim. Aynı zamanda da ekrana bir saat basılmasını isteyelim. "getchar()" fonksiyonu blokeye yol açacağı için ekrana saat basılma işi de duracaktır. İşte bu işi "thread" ler ile yaparsak, sadece "getchar()" çağrısını yapan "thread" bloke olacaktır. Fakat ekrana saat basılmaya devam edecektir. >>> Diyelim ki o an sistemimizde dört adet proses koşuyor olsun. "thread" kavramı olmasaydı, 1/4 oranı ile bir proses çalışacaktı. Şimdi "thread" mekanizması ile proseslerden bir tanesine iki tane daha "thread" ekleyelim. Şimdi toplamda 6 adet, fakat ilgili proses bazında 3 adet "thread" var olacaktır. Dolayısıyla ilgili proses artık 3/6 oranı ile çalışacaktır. Bu da demektir ki o proses daha hızlı çalışacaktır. Örneğin, "Unilever" isimli marka bünyesinde "Cornetto", "Algida", "Carte Dor", "Magnum" gibi dondurma markalarını barındırmaktadır. Böylelikle müşteriler açısından bir çeşit oluşturulmakta fakat kazanç tek bir kişinin cebine yansımaktadır. İşte "thread" mekanizmasının sunduğu performans artışı da bunun gibidir. >>> Paralel programlama yapabilmek için de "thread" leri mutlat suretle kullanması gerekmektedir. >> "thread" kavramı ile şu aşağıdaki kavramlar birbiriyle ilişki içerisindedir: >>> "Concurrent Computing" : Birden fazla akışın söz konusu olduğu bütün durumlar için kullanılabilmektedir. Genel bir terimdir. >>> "Multithreading Programming" : Bir işin birden fazla "thread" ile gerçekleştirildiği uygulamalara denir. >>> "Reentrancy" : Bu terim genellikle fonksiyonlar için kullanılır. Fonksiyon akışının iç içe geçebilmesidir. Örneğin, iki farklı "thread" in aynı fonksiyon akışına girmesidir. Özyineleme, "reentrancy" demek değildir. Özyinelemede tek bir akış vardır. >>> "Parallel Programming" : Aynı makinada bir programın çeşitli "thread" lerinin farklı CPU'da eş zamanlı çalıştırma gayretine denmektedir. >>> "Distributed Computing" : Bir işi farklı bilgisayarlara dağıtarak, eş zamanlı bir biçimde ele almaktır. > Hatırlatıcı Notlar: >> Bir adres değerinin sayfa katları olacak şekilde aşağı doğru yuvarlanması: #include #include #include #include char buffer[4096]; int main(void) { /* # OUTPUT # Old : [0x563030815040] New : [0x563030815000] */ printf("Old : [%p]\n", buffer); /* Aşağıda, bir adres değeri, sayfa katları olacak şekilde aşağı doğru, tekrar hizalanmıştır. * @ "uintptr_t" : türü C99 ile dile eklenen ve bir "pointer" uzunluğunda * hizalanmış olan "İşaretsiz Tam Sayı" türüdür. * @ "(uintptr_t)buffer" : Adres değerleri "bit-wise" işlemlere sokulamazlar. Bu * adres değerinin kapladığı alan, sanki işaretsiz tam sayı kullanıldığında kaplayacak * alan biçimine getirilmiştir. * @ "0xFFF" : Sistemimizdeki sayfa uzunluğu "4096" bayt olarak varsayılmıştır. * "4096" baytın 16'lık tabandaki gösterimidir. 2'lik tabandaki "...0000 1111 1111 1111" * ifadesidir. * @ "~0xFFF" : 2'lik tabandaki "...1111 0000 0000 0000" ifadesidir. * @ "(uintptr_t)buffer & ~0xFFF" : İlgili işaretsiz tam sayı değerinin en düşük anlamlı * bitleri sıfırlanmıştır. Böylelikle 10'luk tabandaki karşılığı "4096" nın katları olmuştur. * @ "(void*)((uintptr_t)buffer & ~0xFFF)" : Artık ilgili işaretsiz tam sayı değeri adres olarak * yorumlanacaktır. */ void* addr = (void*)((uintptr_t)buffer & ~0xFFF); printf("New : [%p]\n", addr); return 0; } >> "Resource Limit" değerlerini "get" etmek için: #include #include void exit_sys(const char* msg); char buffer[4096]; int main(void) { /* # OUTPUT # Soft Limit: [65536] Hard Limit: [65536] */ struct rlimit limits; getrlimit(RLIMIT_MEMLOCK, &limits); printf("Soft Limit: [%lld]\n", (long long)limits.rlim_cur); printf("Hard Limit: [%lld]\n", (long long)limits.rlim_max); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (50_06_05_2023) > "Threads in POSIX" (devam): >> "thread" ile ilgili fonksiyonlar, "pthread" isimli POSIX başlık dosyasında yer alırlar ve bu fonksiyonların isimleri "pthread_" ön eki ile başlarlar. Örneğin, "pthread_join". >> "pthread" kütüphanesindeki fonksiyonları kullanabilmek için, derleme sırasında "-lpthread" seçeneğini kullanmamız gerekmektedir. Böylelikle ilgili kütüphane "linker" tarafından işleme sokulacaktır. >> "thread" ler aynı prosesin farklı akışlarıdır. Bu açıdan bir işi iki "thread" e yaptırmak ile iki ayrı prosese yaptırmak açısından şöyle farklılıklar vardır. Örneğin, >>> "thread" lerin oluşturulması ve yok edilmesi proseslere nazaran daha hızlıdır. >>> "thread" ler, içinde bulunduğu prosesin bellek alanını kullanır. Fakat prosesler, kendilerine has alanları kullanır. Bu nedenledir ki iki prosesin haberleşmesi, aynı proses içerisindeki "thread" lerin haberleşmesine nazaran daha maliyetlidir. >>> "thread" ler daha az sistem kaynağı harcama eğilimindedirler. >>> Proses kavramı daha kapsayıcı bir terimdir. Çünkü prosesler, o an çalışmakta olan bir programın bütün bilgilerini içerir. Fakat "thread" ler sadece birer akış belirtmektedir. Bu sebepledir ki; >>>> "thread" lerin Gerçek/Etkin Kullanıcı ID ve Gerçek/Etkin Grup ID değerleri yoktur. Fakat prosesler bu ID değerlerine sahiptirler. >>>> Prosesler "Current Working Directory", "Environment Variables", "Dosya Betimleyici Tablosu" vb. kavramalara sahiptir fakat "thread" ler için böyle kavramlar söz konusu değildir. Yani bir prosese özgü "Dosya Betimleyici Tablosu" varken "thread" e özgü "Dosya Betimleyici Tablosu" mevcut değildir. Dolayısıyla bir "thread" in açtığı dosyayı diğer "thread" de görecektir. >>> "thread" ler arasında "parent-child" ilişkisi yoktur. Bu sebepledir ki herhangi bir "thread", herhangi bir "thread" tarafından oluşturulabilir. Benzer şekilde bir "thread" in "exit code" bilgisi diğer "thread" ler tarafından da temin edilebilir. >> Bir "thread" oluşturulurken, "thread" akışının başlatılacağı bir fonksiyon da belirtilmelidir. Nasılki bir program ilk çalışmaya başladığında sadece "main-thread" varsa ve bu da "main" fonksiyonundan çalışmaya başlıyorsa, bizlerin oluşturacağı diğer "thread" de bizim göstereceğimiz fonksiyondan çalışmaya başlayacaktır. >> "thread" ler o prosesin "static" veri elemanlarını ortak kullanırlar. Yani bir "thread" bu tip bir veri elemanını değiştirdiğinde, diğer "thread" bu değişikliği görecektir. Anımsanacağınız üzere "static" veri elemanları "global namespace" alanındakiler veya "static" anahtar kelimesi ile nitelenen değişkenlerdi. Hakeza otomatik ömürlü değişkenler, "thread" lere özgüdür. Yani her "thread" için bir kopya oluşturulur. Dolayısıyla bir "thread", o değişken üzerinde bir değişiklik yaptığında, diğer "thread" bunu göremeyecektir. Bunun da sebebi "thread" in "stack" alanları birbirinden ayrılmış olmasıdır. >>> Bir "thread" in "stack" büyüklüğü "64-bit" Linux sistemlerinde 8MB büyüklüğünde, "32-bit" Linux sistemlerde ise 2MB büyüklüğündedir. Windows sistemlerde ise 1MB büyüklüğündedir. >> Proseslerin ID değerleri "pid_t" türü ile temsil edilirken, "thread" lerin ID değerleri "pthread_t" türü ile temsil edilmektedir. Öte yandan proseslerin ID değerleri sistem genelinde "unique" haldeyken, "thread" lerin ID değerleri o proses genelinde "unique" haldedir.Yani farklı proseslerin oluşturduğu "thread" lerin ID değerleri birbiriyle aynı olabilir. >> POSIX "thread" kütüphanesindeki ilgili fonksiyonların ortak özellikleri şu şekildedir; >>> Bütün fonksiyonların isimleri "pthread_" ön eki ile başlamaktadır. >>> Çoğu fonksiyonun geri dönüş değeri "int" türdendir. Bu fonksiyonlar başarı durumunda "0" değerine, başarısızlık durumunda "errno" değişkeninin alacağı değeri geri döndürmektedir ve "errno" değişkenini "set" ETMEMEKTEDİR. Çünkü her "thread" kendisinin "errno" değişkenini "set" etmektedir. Diğer "thread" lerin "errno" değişkenleri bundan etkilenmeyecektir. "perror" fonksiyonu da kendi "thread" inin "errno" değişkenini kullanmaktadır. Dolayısıyla hata mesajını ekrana yazarken aşağıdaki gibi bir fonksiyonu kullanabiliriz: void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Aşağıda ise bir hata kontrolünün tipik uygulanışı yer almaktadır: int result; //... if((result = pthread_xxx(...)) != 0) exit_sys("pthread_xxx", result); //... >>> Bu fonksiyonların bulunduğu kütüphanenin ismi ise "pthread.h" biçimindedir. >> Pekiyi bizler bir "thread" i nasıl oluştururuz? Anımsanacağınız üzere bir program hayata tek bir "thread" ile gelmektedir ve buna da "main-thread" denmekteydi. İkinci ve diğer "thread" leri bizler oluşturmalıyız. İşte bir "thread" oluşturmak için kullanacağımız fonksiyonun ismi "pthread_create" biçimindedir. >>> "pthread_create" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg); Fonksiyonun birinci parametresi, oluşturulacak "thread" in ID değerini elde etmek için kullanılan adres değeridir. "pthread_t" türünden bir nesnenin adresini bu parametreye geçerek, oluşturulan "thread" in ID değerini "get" edebiliriz. İş bu "pthread_t" türünün hangi gerçek türe karşılık geldiği sistemden sisteme değişiklik göstermektedir. Aritmetik türlere karşılık geldiği gibi bir yapı türüne de denk gelebilmektedir. İşte bu sebepten ötürü iki "pthread_t" türden değişkenleri kendi aralarında karşılaştırmaya çalışmamalıyız. Bunun için özel olarak yazılmış fonksiyonları kullanmalıyız. Fonksiyonun ikinci parametresi, oluşturulacak olan "thread" in bir takım özelliklerini önceden "set" etmek için kullanılır. Bu parametreye "NULL" değerini geçmemiz, varsayılan özelliklerin kullanılacağı anlamındadır. Bu parametre daha sonra tekrar ele alınacaktır. Fonksiyonun üçüncü parametresi, oluşturulacak "thread" in akışının başlayacağı fonksiyonun adresini belirtmektedir. Bu fonksiyon öyle bir fonksiyon olmalıdır ki geri dönüş değeri ve parametresi "void*" olmalıdır. Fonksiyonun dördüncü ve son parametresi, üçüncü parametresine geçtiğimiz fonksiyona geçilecek değeri belirtmektedir. * Örnek 1, Aşağıdaki kodu derlerken "-lpthread" seçeneğini de kullanmalıyız. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 sub-thread: 2 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 3; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 3; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda "thread" in akışının başladığı fonksiyona parametre de geçilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); #define TOTAL_THREADS 3 int main(int argc, char** argv) { int result; pthread_t tid[TOTAL_THREADS]; char* buffer; for(int i = 0; i < TOTAL_THREADS; ++i) { if((buffer = (char*)malloc(1024)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } snprintf(buffer, 1024, "%d. Thread\n", i); if((result = pthread_create(&tid[i], NULL, thread_proc, buffer)) != 0) exit_sys("pthread_create", result); } puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { char* buffer = (char*)param; for(int i = 0; i < 5; ++i) { printf("%d. Thread: %s\n", i, buffer); sleep(1); } free(param); return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>> Bizler bir "thread" oluşturduğumuzda, o "thread" i oluşturan "main-thread" mi ilk çalışmaya başlar yoksa oluşturulan "thread" mi? POSIX standartlarınca hangisinin önce başlayacağına dair bir garanti yoktur. Bu iş tamamiyle işletim sisteminin çizelgeleyicisine bağlıdır. Bu noktada garanti olan şey, "thread" lerin oluşturulduktan sonra otomatik olarak çalışmaya başlamalarıdır. Çünkü bazı sistemlerde "thread" ler oluşturulduktan sonra onlara ayrıca "start" vermek gerekmektedir. >> Pekiyi bir "thread" nasıl sonlanır? Birden fazla yöntem vardır. Bunlar; >>> Yöntemlerden bir tanesi doğal ölümdür. Yani "thread" in akışı sona geldiğinde "thread" otomatik olarak yok edilir. >>> Bir diğer yöntem ise "pthread_exit" fonksiyonunun kullanımıdır. Nasılki "exit" / "_exit" fonksiyonları kendi proseslerini sonlandırıyorsa, "pthread_exit" fonksiyonu da kendi "thread" ini sonlandırmaktadır. Yani hangi "thread" akışı "pthread_exit" fonksiyonunu görürse sonlandırılır. >>>> "pthread_exit" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include void pthread_exit(void *value_ptr); Fonksiyonun tek parametresi, ilgili "thread" i sonlandırırken alacağı "exit code" bilgisidir. Tıpkı "exit" veya "_exit" fonksiyonlarının kullanımı gibi. Öte yandan şu noktaya da dikkat etmeliyiz: "main" fonksiyonunun geri dönüş değer "int" türden ve "exit" fonksiyonunun parametresi de "int" türden. İşte "pthread_xxx" fonksiyonlarının geri dönüş değeri "void*" türden ve "pthread_exit" fonksiyonunun parametresi de "void*" türden. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 main-thread: 0 sub-thread: 1 main-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { if(i == 2) pthread_exit(NULL); printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>> Üçüncü yöntem ise "exit" ya da "_exit" fonksiyonlarının çağrılmasıdır. Bir programda bu iki fonksiyondan birisinin çağrılması durumunda proses sonlanacağı için o prosesin bütün "thread" leri de otomatik olarak sonlanacaktır. Tabii bu iki fonksiyonu "main-thread" çağırabildiği gibi daha sonra oluşturulan "thread" ler de çağırabilir. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 main-thread: 0 sub-thread: 1 main-thread: 1 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { if(i == 2) exit(EXIT_SUCCESS); printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi bu noktada dikkat etmemiz gereken bir nokta vardır: Hiç bir "thread", yukarıdaki "exit", "_exit" veya "pthread_exit" fonksiyonlarından birini henüz çağırmamışken "main-thrad" in akışı "main" fonksiyonunun sonuna gelirse, otomatik olarak "exit" fonksiyonu çağrılacağı için, yine bütün "thread" ler sonlanacaktır. Fakat bu demek değildir ki "main-thread" bittiğinde proses komple sonlanacaktır. Buradaki kilit nokta, "exit" çağrıldığı için prosesin sonlanmasıdır. * Örnek 1, Aşağıdaki programda sonradan oluşturulan "thread" in elliye kadar sayması beklenirken "main-thread" in akışı "main" fonksiyonunun sonuna gelmesinden dolayı "exit" fonksiyonu çağrılacak ve ilgili bütün "thread" ler yok edilecektir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 sub-thread: 2 main-thread: 3 sub-thread: 3 main-thread: 4 sub-thread: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 50; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "main-thread" in akışı "main" fonksiyonunu bitirmiş ve dolayısıyla "exit" çağrısı yapılmıştır. Bu sebeple bütün "thread" ler yok edilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 sub-thread: 0 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 50; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekteki "main-thread", "pthread_exit" ile sonlandırılmıştır. Daha sonra oluşturulan yeni "thread" in akışı bitmiş ve proses komple sonlanmıştır. Buradan da görüleceği üzere "main-thread" dediğimiz "threat" ile sonradan oluşturulan "thread" arasında bir fark yoktur. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 sub-thread: 2 sub-thread: 3 sub-thread: 4 sub-thread: 5 sub-thread: 6 sub-thread: 7 sub-thread: 8 sub-thread: 9 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 3) pthread_exit(NULL); printf("main-thread: %d\n", i); sleep(1); } return 0; } void* thread_proc(void* param) { for(int i = 0; i < 10; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return param; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Pekiyi akla şu soru gelmektedir: "main-thread" sonlanacağı için, onun akışı "main" fonksiyonunun sonuna gelmeyecektir. Pekiyi bu durumda proses nasıl sonlanacaktır? İşte bir prosesin hayattaki son "thread" i sonlandığında, otomatik olarak proses de sonlandırılmaktadır. >>> Dördüncü sonlandırma yöntemi ise sinyallerin kullanımıdır. Bir prosese sinyal gönderilmişse ve ilgili proses de bu sinyali ele almamışsa prosesimiz sonlandırılabilir. Özellikle bazı sinyaller prosesi sonlandırma potansiyeline sahiptir. >>> Beşinci sonlandırma yöntemi ise "pthread_cancel" fonksiyonu ile başka "thread" ler tarafından sonlandırılma yöntemidir. >>>> "pthread_cancel" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_cancel(pthread_t thread); Fonksiyonun tek parametresi, "pthread_create" fonksiyonunun birinci parametresine geçilen, "thread" in ID değeridir. Bu fonksiyon geri döndüğünde, sonlandırılmak istenen "thread" in sonlanmış olması GARANTİ DEĞİLDİR. Sadece sonlanma süreci başlamıştır. > Hatırlatıcı Notlar: >> C dilinde "Integer Types" ve "Floating-Point Types" türlerinin birleşim kümesi "Aritmetik Tür" olarak geçmektedir. >> "sleep()" fonksiyonu, kendisini çağıran "thread" i belirtilen süre boyunca bloke etmektedir. >> "Senkronize" demek, ilgili fonksiyon geri döndüğünde bahsi geçen işi yapmış olması garantidir. Öte yandan "Asenkron" demek, ilgili fonksiyon geri döndüğünde bahsi geçen işi yapmış olması GARANTİ DEĞİLDİR. Yani iş başlamıştır ama bitmiş olmayabilir. /*================================================================================================================================*/ (51_07_05_2023) > "Threads in POSIX" (devam): >> "thread" lerin hayata getirilmesi (devam): Anımsayacağımız üzere bir "thread" oluştururken "pthread_create" fonksiyonunu kullanılmaktaydık. >>> "pthread_create" fonksiyonunun son parametresine, üçüncü parametresine geçilen fonksiyona gönderilecek olan değeri geçiyorduk. Normal şartlarda dördüncü parametreye bizler birer adres değerleri geçmekteyiz. Eğer adres yerine bir değer geçmek istiyorsak önce bu değeri "void*" a "cast" etmeli, sonrasında "pthread_create" fonksiyonuna geçmeliyiz. Fonksiyonun geri dönüş değeri ise başarı durumunda "0" değerine, başarısızlık durumunda ise yukarıda da açıklandığı üzere hata koduna geri dönmektedir. >> "thread" lerin sonlandırılması (devam): >>> Beşinci sonlandırma yöntemi için "pthread_cancel" fonksiyonundan bahsetmiştik. İş bu fonksiyon bazı detaylara sahiptir. Fakat bu detaylara ilerleyen vakitlerde değinilecektir. >> Biten "thread" lerin "exit code" bilgilerini nasıl alacağız? Anımsanacağınız üzere tamamlanan proseslerin "exit code" bilgileri "wait" fonksiyonları ile temin edilmekteydi. Öte yandan "thread" ler arasında da "parent-child" ilişkisi olmadığından bahsetmiştik. İşte "thread" ler de "pthread_join" isimli fonksiyonu kullanarak, bir "thread" in "exit code" bilgisini elde edebiliriz. >>> "pthread_join" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_join(pthread_t thread, void **value_ptr); Fonksiyonun birinci parametresi "exit code" bilgisi temin edilecek "thread" in ID değeridir. Fonksiyonun ikinci parametresi ise "exit code" bilgisinin yazılacağı "void*" türden göstericinin adresini belirtmektedir. Daha doğrusu bu fonksiyon başarılı bir şekilde geri dönerse, "pthread_create" fonksiyonunun üçüncü parametresine geçilen fonksiyonun geri döndürdüğü adres değeri bu fonksiyonun ikinci parametresi ile geçilen adrese yazılacaktır. O fonksiyon da "void*" türden bir değer döndürdüğü için, o değerin adresi de "void**" türden olmaktadır. Eğer bu "exit code" değerini istemiyorsak, ikinci parametreye "NULL" değerini geçebiliriz. Fonksiyonun geri dönüş değeri başarı durumunda "0", başarısızlık durumunda ise hata kodu biçimindedir. Öte yandan iş bu fonksiyon, kendisini çağıran "thread" i bloke etmektedir. Eğer halihazırda sonlanmış bir "thread" in "exit code" değerini almak istersek, bloke oluşmayacaktır. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 sub-thread: 1 sub-thread: 2 sub-thread: 3 sub-thread: 4 Success!... Exit Code: [31] */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); printf("Exit Code: [%d]\n", (int)exit_code); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Tıpkı proseslerde olduğu gibi, "thread" lerde de "zombie" olma durumu oluşabilir. Örneğin, hayata getirdiğimiz bir "thread" i "join" etmezsek, bu "thread" in akışı bittiğinde, "zombie thread" oluşacaktır. Fakat "thread" lerin "zombie" olması, proseslerin "zombie" olmasından daha az zararlı da olsa yine de sistem için sorunlara yol açabilir. Dolayısıyla taviye edilen şey bir "thread" i hayata getirdikten sonra onu "join" etmektir. Pekiyi bizler hayata getirdiğimiz "thread" i bekleyemiyorsak ne yapmalıyız? O vakit "thread" lerin "detach" modda oluşturulmaları gerekmektedir. "thread" lerin "detach" moda sokulmasına ilerleyen vakitlerde değinilecektir. * Örnek 1, Aşağıdaki örnekte oluşturulan "thread" beklenmiş fakat "exit code" bilgisi alınmamıştır. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... sub-thread: 0 sub-thread: 1 sub-thread: 2 sub-thread: 3 sub-thread: 4 Success!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> "thread" lerin dışarıdan sonlandırılması, "cancel" edilmesi: Anımsayacağımız üzere "thread" ler başka "thread" ler tarafından sonlandırılabilmektedir. İşte bu işi yapan POSIX fonksiyonunun ismi "pthread_cancel" biçimindedir. Fakat unutmamalıyız "thread" lerin ID değerleri, o proseste anlamlıdır. Dolayısıyla başka bir prosesin "thread" sonlandıramayız. >>> "pthread_cancel" fonksiyonu, aşağıdaki parametrik yapıya sahiptir: #include int pthread_cancel(pthread_t thread); Fonksiyon parametre olarak sonlandırılacak "thread" in ID değerini alır. Geri dönüş değeri de başarı durumunda "0", başarısızlık durumunda hata kodudur. * Örnek 1, Aşağıdaki örnekte bizim tarafımızdan oluşturulan "thread" sonlandırılmış, "main-thread" ise akışına devam etmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 Success!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi bir "thread" in başka bir "thread" tarafından sonlandırılma işleminin bazı detayları vardır. Örneğin, sonlandırma işlemi aslında bir nevi talep olarak ele alınmaktadır. Dolayısıyla bir "thread" i sonlandırmak istediğimizde, direkt olarak sonlandırma işlemi yapılmamaktadır. İlgili "thread" in akışı "cancellation point" diye isimlendirilen noktalara geldiğinde gerçek sonlandırma yapılmaktadır. >>> "Cancellation Point" içeren fonksiyonların listesine aşağıdaki link üzerinden ulaşabiliriz. https://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_09.html#tag_02_09_05_02 Fakat kabaca gerekirse "read", "write", "wait" vb. temel fonksiyonlar "cancellation point" barındırmaktadır. Eğer bir "thread" iş bu "cancellation point" leri görmez ise o "thread" i sonlandıramayız. Dolayısıyla beklemeye devam edeceğiz. * Örnek 1, Aşağıdaki örnekte diğer "thread" için sonlandırma talebi gönderilmiş fakat onun akışı "for" döngüsünden çıkamadığı için sonlandırma işlemi gerçekleştirilememiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 main-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { for(;;); for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Pekiyi bizler yukarıdaki "cancellation point" içeren fonksiyonları çağırmak istemiyorsak ve ilgili "thread" imizin de sonlanabilmesini istiyorsak ne yapmalıyız? İşte bu durumda devreye içinde boş bir "cancellation point" olan POSIX fonksiyonu girmektedir. Yani yalnızda "cancellation point" için yazılmış olan "pthread_testcancel" isimli bu fonksiyonu kullanmalıyız. >>> "pthread_testcancel" aşağıdaki parametrik yapıya sahiptir: #include void pthread_testcancel(void); Fonksiyon herhangi bir parametre almamakta ve herhangi bir değer geriye döndürmemektedir. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 main-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 Success!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { for(;;) pthread_testcancel(); for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Öte yandan bir "thread" in "cancellation point" gördüğündeki davranışını da değiştirebiliriz. Bunun için iki adet POSIX fonksiyonu bulunmaktadır. Bu fonksiyonlar "pthread_setcancelstate" ve "pthread_setcanceltype" ismindedirler. >>> "pthread_setcanceltype" fonksiyonu, bu fonksiyonu çağıran "thread" in sonlandırma biçimini belirtir. Aşağıdaki gibi bir parametrik yapıya sahiptir: #include int pthread_setcanceltype(int type, int *oldtype); Fonksiyonun birinci parametresi şu değerlerden birisini almaktadır; "PTHREAD_CANCEL_DEFERRED" ve "PTHREAD_CANCEL_ASYNCHRONOUS". -> "PTHREAD_CANCEL_DEFERRED" : Bu değer kullanıldığında, "ilgili 'thread' in akışı bir 'cancellation point' e girdiğinde sonlandır" anlamına gelir. Bir "thread" oluşturulduğunda sahip olduğu varsayılan özellik budur. -> "PTHREAD_CANCEL_ASYNCHRONOUS" : Bu değer kullanıldığında, ilgili "thread" anında sonlandırılacaktır. Fonksiyonun ikinci parametresi ise önceki sonlandırma biçiminin yerleştirileceği "int" türden nesnenin adresini almaktadır. POSIX standartlarınca bu parametreye "NULL" geçilebilmesi için bir şey belirtilmemiştir. Dolayısıyla bu parametreye "NULL" değerini geçmemeliyiz. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda ilgili hata kodudur. * Örnek 1, Aşağıdaki programda tarafımızca oluşturulan "thread", direkt olarak sonlandırılmıştır. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 main-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 Success!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("\nSuccess!...\n"); return 0; } void* thread_proc(void* param) { int old_status; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_status); for(;;); return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>> "pthread_setcancelstate" fonksiyonu, kendisini çağıran "thread" in sonlandırma durumunun değiştirilmesi için kullanılmaktadır. Fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int pthread_setcancelstate(int state, int *oldstate); Fonksiyonun birinci parametresi şu iki değerden birisi olabilir; "PTHREAD_CANCEL_ENABLE" ve "PTHREAD_CANCEL_DISABLE". -> "PTHREAD_CANCEL_ENABLE" değerinin kullanılması demek ilgili "thread" in "pthread_cancel" ile sonlandırılabileceği anlamındadır. Bir "thread" oluşturulduğunda sahip olduğu varsayılan özellik budur. -> "PTHREAD_CANCEL_DISABLE" değerinin kullanılması demek ilgili "thread" in "pthread_cancel" ile sonlandırılamayacağı anlamındadır. Eğer bir "thread" bu duruma sokulursa, "pthread_cancel" çağrısı sonrasında "thread" sonlandırılmaz ama sonlandırma isteği askıda bekletilir. Eğer "thread" in durumu "PTHREAD_CANCEL_ENABLE" durumuna çekilirse, sonlandırma isteği işleme alınır. "PTHREAD_CANCEL_DEFERRED" / "PTHREAD_CANCEL_ASYNCHRONOUS" durumuna göre ya o an sonlandırılır ya da bir "cancellation point" e girdikten sonra sonlandırılır. Fonksiyonun ikinci parametresi ise önceki sonlandırma biçiminin yerleştirileceği "int" türden nesnenin adresini almaktadır. POSIX standartlarınca bu parametreye "NULL" geçilebilmesi için bir şey belirtilmemiştir. Dolayısıyla bu parametreye "NULL" değerini geçmemeliyiz. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda ilgili hata kodudur. * Örnek 1, Aşağıdaki programda sonlandırılma özelliği pasif hale getirilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread : 0 main-thread: 1 sub-thread : 1 sub-thread : 2 main-thread: 2 sub-thread : 3 main-thread: 3 sub-thread : 4 main-thread: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); if(PTHREAD_CANCELED == exit_code) printf("\nThe sub-thread has been terminated!...\n"); return 0; } void* thread_proc(void* param) { int old_status; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_status); for(int i = 0; i < 5; ++i) { printf("sub-thread : %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda sonlandırılma özelliği aktif hale getirilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread : 0 main-thread: 1 sub-thread : 1 sub-thread : 2 main-thread: 2 main-thread: 3 main-thread: 4 The sub-thread has been terminated!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); if(PTHREAD_CANCELED == exit_code) printf("\nThe sub-thread has been terminated!...\n"); return 0; } void* thread_proc(void* param) { int old_status; pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_status); for(int i = 0; i < 5; ++i) { printf("sub-thread : %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Son olarak "pthread_cancel" ile sonlandırılan "thread" lerin "exit code" değeri, "PTHREAD_CANCELED" olarak, "pthread_join" fonksiyonundan elde edilmektedir. Bu değer pek çok sistemde aşağıdaki gibi tanımlanmıştır: #define PTHREAD_CANCELED ((void*)-1) Aşağıda bunun için bir örnek verilmiştir: * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... main-thread: 0 sub-thread: 0 main-thread: 1 sub-thread: 1 main-thread: 2 main-thread: 3 main-thread: 4 The sub-thread has been terminated!... */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { if(i == 2) if((result = pthread_cancel(tid)) != 0) exit_sys("pthread_cancel", result); printf("main-thread: %d\n", i); sleep(1); } void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); if(PTHREAD_CANCELED == exit_code) printf("\nThe sub-thread has been terminated!...\n"); return 0; } void* thread_proc(void* param) { int old_status; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_status); for(int i = 0; i < 5; ++i) { printf("sub-thread: %d\n", i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> Pekiyi bizler bir "thread" in ID değerini nasıl "get" edebiliriz? Böylesi bir durumda "pthread_self" isimli POSIX fonksiyonu devreye girmektedir. Böylelikle bu fonksiyonu çağıran "thread" in ID değerini elde edebileceğiz. Fonksiyonun imzası aşağıdaki gibidir: #include pthread_t pthread_self(void); Fonksiyon başarısız olamamaktadır. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Ok!... -464677056: 0 -464681408: 0 -464677056: 1 -464681408: 1 -464677056: 2 -464681408: 2 -464677056: 3 -464681408: 3 -464677056: 4 -464681408: 4 */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); puts("Ok!...\n"); for(int i = 0; i < 5; ++i) { printf("%d: %d\n", (int)pthread_self(),i); sleep(1); } void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("%d: %d\n", (int)pthread_self(),i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> "Unspecified" demek programın çökmeyeceğini fakat sonucun öyle de olabileceği veya böyle de olabileceğini belirtir. >> "Undefined" demek ise programın çökebileceğini de belirtmektedir. /*================================================================================================================================*/ (52_13_05_2023) > "Threads in POSIX" (devam): >> Bir "thread" oluştururken varsayılan özelliklerde oluşturmamak için, "pthread_create" fonksiyonunun ikinci parametresine "pthread_attr_t" yapı türünden bir nesnenin adresini geçmemiz gerekmektedir. Hatırlayacağınız üzere daha önceki örneklerde bu parametre için "NULL" değeri geçilmişti. Pekiyi iş bu yapı türü neyin nesidir? Aslında bu tür "pthread.h" ve "sys/types.h" başlıklarında tür eş ismi olarak tanımlanmıştır fakat hangi türe karşılık geldiğine dair kesin bir şey söylenmemiştir. Tipik olarak bu tür bir yapı türünü temsil etmektedir. Ancak bu yapının hangi elemanlardan oluştuğu POSIX standartlarınca açıklanmamıştır. Dolayısıyla bu yapının elemanlarının ne olacağı sistemden sisteme değişiklik göstermektedir. Dolayısıyla bu yapı türünü kullanmak için bir takım fonksiyonlara çağrı yapmamız gerekmektedir. Örneğin, nesne yönelimli programlamada sınıfın veri elemanlarının "private" yapılması ve bu veri elemanlarına "ctor", "setter" ve "getter" fonksiyonları ile ulaşılması yukarıdaki duruma örnek olabilir. Pekiyi bu fonksiyonlar nelerdir? Tipik olarak "get" amacı taşıyan fonksiyonlar "pthread_attr_getXXX" biçiminde, "set" amacı taşıyanlar ise "pthread_attr_setXXX" biçiminde bir isimlendirmeye sahiptir. Öte yandan bu yapının içini doldurmak, bir nevi "initialize" etmek için, "pthread_attr_init" fonksiyonunu kullanırken bu yapıyı yok etmek için de "pthread_attr_destroy" fonksiyonunu kullanacağız. Özetle aşağıdaki sıralamayı takip edeceğiz; >>> İlk olarak "pthread_attr_t" türünden bir nesne hayata getireceğiz. * Örnek 1, //... #include //... int main(int argc, char** argv) { pthread_attr_t tattr; //... return 0; } //... >>> Daha sonra bu yapı nesnesine "pthread_attr_init" fonksiyonu ile ilk değer verilir. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_attr_init(pthread_attr_t *attr); Fonksiyon argüman olarak "pthread_attr_t" türünden nesnenin adresini alarak ona ilk değer verir. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda ise hata kodunun kendisi ile geri dönmektedir. * Örnek 1, //... #include //... int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); //... return 0; } //... >>> Artık "pthread_attr_setXXX" ve/veya "pthread_attr_getXXX" biçimindeki fonksiyonları kullanarak "thread" in özellikleri "set" ve/veya "get" edilebilir. Tabii bu bahsi geçen fonksiyonlar farklı alt konular ile ilgili olduğu için zamanla bu fonksiyonlar incelenecektir. * Örnek 1, //... #include //... int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setXXX(&tattr, ...)) != 0) exit_sys("pthread_attr_setXXX", result); //... return 0; } //... >>> Sonrasında "pthread_create" fonksiyonu ile belirlediğimiz özelliklerde bir "thread" hayata getirebiliriz. * Örnek 1, //... #include //... int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setXXX(&tattr, ...)) != 0) exit_sys("pthread_attr_setXXX", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); //... return 0; } //... >>> En sonunda da "pthread_attr_destroy" fonksiyonu ile "pthread_attr_init" ile yaptığımız işlemler geri alınabilir. Fonksiyonun parametrik yapısı aşağıdaki gibidir: #include int pthread_attr_destroy(pthread_attr_t *attr); Fonksiyon argüman olarak "pthread_attr_t" türünden yapının adresini almaktadır. POSIX standartlarınca bu fonksiyon için başarısızlık için bir şey söylenmemiştir. Dolayısıyla bu fonksiyon başarısız olamaz. Başarı durumunda ise "0" ile geri dönecektir. Öte yandan bu fonksiyonu, "pthread_create" fonksiyonundan hemen sonra çağrılabilir. Çünkü "pthread_create" fonksiyonu argüman olarak aldığı "pthread_attr_t" türünden nesnenin adresini saklamak yerine, bu adres içerisindekileri kopyalamaktadır. Bir nevi "pthread_attr_t" içerisindekileri başka yere almaktadır. * Örnek 1, //... #include //... int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setXXX(&tattr, ...)) != 0) exit_sys("pthread_attr_setXXX", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys("pthread_attr_destroy", result); //... return 0; } //... Şimdi de bütün bu süreci içine alan tek bir örnek verelim: * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setXXX(&tattr, ...)) != 0) exit_sys("pthread_attr_setXXX", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys("pthread_attr_destroy", result); void* exit_code; if((result = pthread_join(tid, &exit_code)) != 0) exit_sys("pthread_join", result); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 5; ++i) { printf("%d: %d\n", (int)pthread_self(),i); sleep(1); } return (void*)31; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi "thread" özelliklerinden bir kaçına değinelim. >>>> "Stack Size": Bir "thread" in varsayılan "stack" büyüklüğü POSIX sistemlerinde 8MB, Windows sistemlerinde ise 1MB büyüklüğündedir. "pthread_attr_getstacksize" fonksiyonu ile bu büyüklüğü temin edebilir, "pthread_attr_setstacksize" ile yeni bir büyüklük değeri girebiliriz. >>>>> "pthread_attr_setstacksize" fonksiyonunun prototipi aşağıdaki biçimdedir: #include int pthread_attr_setstacksize(const pthread_attr_t *attr, size_t stacksize); Fonksiyonun birinci parametresi, "thread" özelliklerine ilişkin "pthread_attr_t" türünden nesnenin adresini almaktadır. İkinci parametre ise "thread" in yeni "stack" uzunluk bilgisidir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda hata kodunun kendisidir. >>>>> "pthread_attr_getstacksize" fonksiyonunun prototipi aşağıdaki biçimdedir: #include int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t* stacksize); Fonksiyonun birinci parametresi, "thread" özelliklerine ilişkin "pthread_attr_t" türünden nesnenin adresini almaktadır. İkinci parametre ise "thread" in yeni "stack" bilgisinin yerleştirileceği adrestir. POSIX standartlarında, fonksiyonun başarısızlığından söz edilmemektedir. Başarı durumunda ise "0" ile geri dönmektedir. * Örnek 1, #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Default Stack Size: 8388608 Default Stack Size: 2097152 Ok Done */ int result; pthread_attr_t tattr; /* * 1. İlk önce "tattr" yapısına ilk değer veriyoruz. */ if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); /* * 2. Daha sonra "main-thread" in "stack" bilgisini * elde ediyoruz. */ ssize_t size; if((result = pthread_attr_getstacksize(&tattr, &size)) != 0) exit_sys("pthread_attr_setXXX", result); printf("Default Stack Size: %zd\n", size); /* * 3. Daha sonra "main-thread" in "stack" bilgisini * güncelliyoruz. */ if((result = pthread_attr_setstacksize(&tattr, 2097152)) != 0) exit_sys("pthread_attr_setXXX", result); /* * 4. Daha sonra "main-thread" in "stack" bilgisini * yeniden elde ediyoruz. */ if((result = pthread_attr_getstacksize(&tattr, &size)) != 0) exit_sys("pthread_attr_setXXX", result); printf("Default Stack Size: %zd\n", size); /* * 5. Daha sonra özellikleri güncellenmiş bir "thread" oluşturuyoruz. */ pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys("pthread_attr_destroy", result); if((result = pthread_join(tid, NULL)) != 0) exit_sys("pthread_join", result); puts("Done"); return 0; } void* thread_proc(void* param) { puts("Ok"); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> Anımsanacağınız üzere bir "thread" oluşturduktan sonra "zombie thread" oluşmaması için ya bu "thread" i beklemeli ya da ilgili "thread" in "detach" moda sokulması gerektiğinden bahsetmiştik. Pekiyi nedir bu "detach" mevzusu? Hayata getirdiğimiz "thread" in akışı bittiğinde kaynakları otomatik olarak boşaltılmaktadır. Dolayısıyla "pthread_join" ile "thread" i beklememize gerek kalmıyor. Unutmamalıyız ki bir "thread" hayata geldiğinde ya onu "pthread_join" ile beklemeli veya onu "detach" moda sokarak kendi halinde çalışmasını sağlamalıyız. Pekiyi bizler bir "thread" i nasıl "detach" moda sokabiliriz? Burada bir kaç farklı yöntem devreye girmektedir. Bunlar "pthread_detach" fonksiyonunu kullanmak ve "pthread_attr_setdetachstate" ile uygun özellikleri belirtip "pthread_create" fonksiyonuna çağrı yaparak. >>> "pthread_attr_setdetachstate" ve "pthread_create" fonksiyonlarını kullanmak: Bu yöntemde bizler bir "thread" oluşturmadan evvel onun bir takım özelliklerini "pthread_attr_setdetachstate" ile değiştiriyoruz. İş bu fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); Fonksiyonun birinci parametresi "pthread_attr_t" türünden yapı nesnesinin adresidir. İkinci parametre ise şu iki değerden birisini alabilir; PTHREAD_CREATE_DETACHED ve PTHREAD_CREATE_JOINABLE. -> PTHREAD_CREATE_DETACHED: Bu değeri alması durumunda oluşturulacak "thread" artık "detach" modda olacaktır. Yani akışı bittiğinde kaynakları otomatik olarak geri verilecektir. -> PTHREAD_CREATE_JOINABLE: Bu değeri alması durumunda oluşturulacak "thread" artık "joinable" modda olacaktır. Yani akışı bittiğinde kaynakları otomatik olarak geri VERİLMEYECEKTİR. Dolayısıyla bizler bu "thread" i "pthread_join" fonksiyonu ile beklemeliyiz. Aksi halde "zombie thread" meydana gelecektir. Varsayılan özellik olarak bu özelliğe sahiptir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda hata kodunun kendisine dönmektedir. Son olarak belirtmek gerekir ki "detach" moda sokulan bir "thread" artık "pthread_join" fonksiyonu ile BEKLENEMEZ. Bu durum "Tanımsız Davranış" a neden olacaktır. * Örnek 1, Aşağıdaki programda oluşturulacak olan "thread", "detach" moda sokularak oluşturulmuştur. Dolayısıyla bizim "main-thread", onu beklememiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Done */ int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys("pthread_attr_init", result); if((result = pthread_attr_setdetachstate(&tattr, PTHREAD_CREATE_DETACHED)) != 0) exit_sys("pthread_attr_setdetachstate", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys("pthread_attr_destroy", result); puts("Done"); return 0; } void* thread_proc(void* param) { puts("Ok"); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>> "pthread_detach" fonksiyonunu çağırmak da bir diğer yöntemdir. Fonksiyon, aşağıdaki prototipe sahiptir: #include int pthread_detach(pthread_t thread); Fonksiyon "detach" duruma sokulacak "thread" in ID değerini parametre olarak alır. Başarı durumunda "0" ile başarısızlık durumunda "non-zero" bir değere dönmektedir. Eğer "detach" edilecek "thread" zaten "detach" modda ise bu fonksiyon çağrısı "Tanımsız Davranış" meydana getirecektir, POSIX standartlarınca. * Örnek 1, Aşağıdaki programda bir "thread" oluşturulduktan sonra "detach" edilmiştir. #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Done */ int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_detach(tid)) != 0) exit_sys("pthread_detach", result); puts("Done"); return 0; } void* thread_proc(void* param) { puts("Ok"); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >> "thread" lerin senkronize edilmesi: "thread" lerin birlikte ortak bir amacı gerçekleştirmesi, yani uyum içerisinde çalışması gerekebilmektedir. Özellikle ortak kaynaklara erişen "thread" ler için senkronizasyon önemli bir meseledir. Örneğin, aşağıdaki programda sonuç bazı durumlarda 2'000'000 çıkmamaktadır. * Örnek 1, #include #include #include #include #include int g_count = 0; void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Done => 1217532 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("Done => %d\n", g_count); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 1000000; ++i) ++g_count; return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 1000000; ++i) ++g_count; return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Bunun yegane sebebi "quanta" süresi bittiği anda thread" in o anki akışı ters bir noktada olmasından kaynaklanmaktadır. Örneğin, aşağıdaki "assembly" kodu "g_count" değişkeninin değerini bir arttıran kod olduğunu varsayalım: MOV reg, g_count INC reg MOV g_count, reg Burada yapılan şey ise şudur: -> "g_count" değişkeninin değeri "register" bölgesine çekiliyor. -> Daha sonra o değer bir arttırılıyor. -> Sonrasında bu yeni değer "register" bölgesinden tekrardan "g_count" değişkeninin adresine çekiliyor. İşte "quanta" süresi birinci aşamanın sonunda ve ikinci aşamanın başında, yani "g_count" değişkeninin değeri "register" bölgesine çekildikten hemen sonra ama oradaki değerin bir arttırılmasından evvel, bitmesi durumunda "g_count" değeri henüz bir arttırılmamış olacaktır. Yani, MOV reg, g_count ----> "quanta" süresi bitti. Bu "thread" artık bir süre CPU'da işletilmeyecektir. INC reg MOV g_count, reg Bu durumda diğer "thread" ise "g_count" değişkeninin bu "thread" tarafından güncellenmemiş değerini güncelleyecektir. Fakat bu "thread" tekrar çalışmaya başladığında, diğer "thread" tarafından güncellenmiş değer yerine, "register" bölgesine çekmiş olduğu değeri güncelleyecektir. Görüleceği üzere tek bir C deyimi kullanmamıza rağmen arka planda bir kaç adet "assembly" kodu çalıştırılmaktadır. Pekiyi bizler yukarıdaki bu problemi nasıl çözebiliriz? Bunun için iki yöntem vardır. Bunlar, >>> İşlemlerin atomik yapılması, yani tek hamlede yapılması. Yukarıdaki örneği ele alırsak arttırma işlemi için üç adet makina kodu çalıştırılmaktadır. Bu işi tek bir makina kodu ile yapmamız halinde sorunu çözmüş olacağız. Fakat bu örnek için böyle bir şey mümkün değildir. >>> Söz konusu işlemler sırasında "thread" ler arası geçiş olduğunda diğer "thread" in bekletilmesi, o "thread" in işleme konmaması. Böylelikle ilk "thread" yukarıdaki üç makina komutunu tamamladıktan sonra diğer "thread" işleme devam edecektir. Genellikle kullanılan yöntem de bu yöntemdir. Bu yöntemde devreye "Kritik Kod" kavramı girmektedir. >>>> "Kritik Kod" : Bir "thread" in akışı "Kritik Kod" bölgesine girdiğinde, "quanta" süresi dolmasına rağmen, başka bir "thread" in o bölgeye girmemesi demektir. Ta ki ilk "thread" in akışı "Kritik Kod" bölgesinden çıkana kadar. Burada "thread" in akışının kesilmesi engellenemez fakat başka "thread" lerin o bölgeye girmesi engellenebilir. Tabii ikinci "thread", ilk "thread" in akışının komple bitmesini değil sadece "Kritik Kod" alanından çıkmasını beklemektedir. Pekiyi "Kritik Kod" alanları nasıl oluşturulur? Bu alanlar elle oluşturulamaz. Örneğin, aşağıdaki programda uygulanmaya çalışılan şey sonuçsuz kalacaktır: * Örnek 1, Aşağıdaki örnekte "g_flag" bayrağı "0" olduğunda "Kritik Kod" çalıştırılması ve "1" olduğunda çalıştırılmaması hedeflenmiştir. #include #include #include #include #include int g_count = 0; int g_flag = 0; void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # Done => 1797364 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("Done => %d\n", g_count); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 1000000; ++i) { while(g_flag == 1) ; ----> "quanta" süresi bitti. Bu "thread" artık bir süre CPU'da işletilmeyecektir. g_flag = 1; ++g_count; // Kritik Kod g_flag = 0; } return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 1000000; ++i) { while(g_flag == 1) ; g_flag = 1; ++g_count; // Kritik Kod g_flag = 0; } return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Bu örnekte iki adet sıkıntılı nokta vardır. Yukarıda belirtilen noktada kesinti olduğunda, yani "while" bloğundan sonra fakat "g_flag" değişkenine "1" atanmadan evvel, yine yukarda verilen diğer örnekteki durum gerçekleşecektir. Öte yandan ilgili "thread" ler "while" içerisinde "busy loop" biçimde CPU zamanı harcamaktadır. Arzu edilen ise CPU zamanı harcamamasıdır. Öyleyse bizler "Kritik Kod" alanlarını nasıl oluşturabiliriz? Bu noktada artık işletim sistemi de mevzuya dahil olmaktadır. Artık genel ismi "Senkronizasyon Nesneleri" olan bir takım nesneler kullanılacaktır. Bu nesneler genel itibariyle "kernel moda" a geçiş yapabilmektedir. Çünkü "kernel mode" olarak atomik işlemler yapmak, işlemcilerin sağladığı bazı özel makina komutları ile mümkün olabilmektedir. Bazı senkronizasyon nesneleri ise daha az "kernel mode" a geçiş yaparak bu makina komutlarının da yardımıyla daha etkin işlev görmektedir. >>>>> "Senkronizasyon Nesneleri" : "Kritik Kod" alanı oluşturmak için yaygın kullanılan senkronizasyon nesnelerinden birisi "mutex" nesneleridir. "Mutual Exlusion" isminden türetilmiştir. Pek çok farklı işletim sistemlerinde benzer biçimde bulunmaktadır. Linux sistemlerinde ise senkronizasyon nesneleri, ismi "futex" olan, bir sistem fonksiyonu yoluyla gerçekleştirilmektedir. Dolayısıyla Linux sistemlerde kullanılan senkronizasyon nesneleri arka planda "futex" isimli sistem fonksiyonunu çağırmaktadır. > Hatırlatıcı Notlar: >> C standartlarınca "errno" değişkeni çok kısıtlı alanda kullanılmaktadır. Fakat POSIX standartlarınca, standart C fonksiyonları da birer POSIX fonksiyonu sayıldığı için, iş bu "errno" değerini bir hayli kullanmaktadır. Örneğin, "malloc" fonksiyonu POSIX standartlarınca hata durumunda "errno" değişkenini "set" eder fakat C standartlarınca "errno" değişkenini "set" etme zorunluluğu yoktur. Öte yandan "fopen" fonksiyonu da başarısız olduğunda POSIX standartları "errno" değişkenini uygun değere çekmektedir. * Örnek 1, Standart bir C fonksiyonunun başarısını, POSIX sistemlerinde, aşağıdaki gibi sorgulayabiliriz. //... int main() { //... if((p = malloc(SIZE)) == NULL) exit_sys("malloc); } //... Tabii bizler, Standart C uyumunu korumak için, ilgili fonksiyonların geri dönüş değerini "errno" üzerinden sorgulamamalıyız. * Örnek 1, Standart bir C fonksiyonunun başarısını aşağıdaki şekilde sorgulamak daha uygundur. //... int main() { //... if((p = malloc(SIZE)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n); exit(EXIT_FAILURE); } } >> "thread" lerde "Race Condition" durumu senkronize edilmemiş "thread" ler için kullanılan genel bir kavramdır. /*================================================================================================================================*/ (53_20_05_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. Ancak ilk "thread" bu bölgeden çıktıktan sonra diğer "thread" in girmesine izin verilmektedir. >>>> "Kritik Kod" (devam): Öyle kodlardır ki bilinmeyen bir "t" anında sadece bir adet "thread" in işlettiği kodlardır. Böylesi kodlar oluşturmanın en yaygın yöntemi ise "Senkronizasyon Nesneleri" diyeceğimiz nesneleri kullanmaktır. Bu nesnelerden en çok kullanılanı ise "mutex" nesneleridir. >>>>> "mutex" nesneleri: "Mutual Exclusion" kelimesinden türetilmiştir. "pthread_mutex_t" türü ile temsil edilmektedir. Diğer tür eş isimleri gibi "thread.h" ve/veya "sys/types.h" dosyaları içerisinde "typedef" edilmişlerdir. Herhangi bir türe karşılık gelebilir. Örneğin, Linux sistemlerinde bir "struct" türünün eş ismidir. Pekiyi bizler bu "mutex" nesnelerini nasıl kullanacağız? Şöyleki; >>>>>> Bu türden "global" bir nesne tanımlar. * Örnek 1, //... pthread_mutex_t g_mutex; int main(int argc, char** argv) { //... return 0; } //... >>>>>> Daha sonra iş bu "mutex" nesnesine tabiri caizse ilk değer vereceğiz. Burada iki farklı yöntemi izleyebiliriz; "PTHREAD_MUTEX_INITIALIZER" isimli makroyu kullanmak veya "pthread_mutex_init" fonksiyonunu kullanmak. >>>>>>> "PTHREAD_MUTEX_INITIALIZER" makrosu bir C makroosudur. Bu makro varsayılan özellikler ile ilk değer verilmesine olanak sağlar. * Örnek 1, //... pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; int main(int argc, char** argv) { //... return 0; } //... >>>>>>> "pthread_mutex_init" fonksiyonu aşağıdaki prototipe sahiptir: #include int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); Fonksiyonun birinci parametresi ilk değer verilecek "mutex" nesnesinin adresini, ikinci parametresi ise ilgili "mutex" nesnesinin özelliklerini içeren "özellik bilgisinin" adresini almaktadır. Bu ikinci parametreye daha sonra değinilecektir. Fakat bu ikinci adrese "NULL" değerinin geçilmesi durumunda varsayılan özellikler ile "mutex" nesnesi oluşturulur. Yani aslında "PTHREAD_MUTEX_INITIALIZER" makrosunun kullanılması gibi olacaktır. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda ise hata kodunun kendisine dönecektir. * Örnek 1, //... pthread_mutex_t g_mutex; int main(int argc, char** argv) { int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); //... return 0; } //... Pekiyi hangi durumda makroyu hangi durumda da ilgili fonksiyonu çağırmalıyız? Eğer ilgili "mutex" nesnesi bir yapının içerisindeyse ve bu yapıyı da bizler dinamik bir biçimde tahsis edeceksek, "mutex" nesnesi için "pthread_mutex_init" fonksiyonunu kullanmalıyız. >>>>>> Öte yandan POSIX standartlarınca yukarıdaki iki yöntemden biri kullanılarak ilk değer verilen "mutex" nesnesi "pthread_mutex_destroy" fonksiyonu ile yok edilmelidir. Çünkü bazı sistemler ilgili "mutex" nesnesi içerisinde dinamik ömürlü nesneler oluşturabilir. İlgili fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_mutex_destroy(pthread_mutex_t *mutex); Fonksiyon, parametre olarak yok edilecek "mutex" nesnesinin adresini alır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine dönüş yapacaktır. * Örnek 1, //... pthread_mutex_t g_mutex; int main(int argc, char** argv) { int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); //... if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } //... Pekiyi bizler yukarıdaki örneklerde "mutex" nesnesini oluşturduktan sonra "Kritik Kod" bölgesini nasıl oluşturacağız? Bu durumda bizler "pthread_mutex_lock" ve "pthread_mutex_unlock" fonksiyonları girmektedir. "Kritik Kod" bölgesinin başında "pthread_mutex_lock" fonksiyonu, sonunda ise "pthread_mutex_unlock" fonksiyonu çağrılmalıdır. Eğer bir "thread" in akışı ilk defa "pthread_mutex_lock" çağrısına denk gelirse, artık kullanılan "mutex" nesnesinin sahibi o "thread" e ait olacaktır. Bu noktada eğer başka "thread" lerin akışı da "pthread_mutex_lock" fonksiyonuna gelirse, o "thread" ler bloke edilecektir. Eğer ilgili "mutex" nesnesinin sahibi olan "thread" in akışı "pthread_mutex_unlock" fonksiyonuna gelirse, ilgili "mutex" nesnesinin sahipliği ortadan kaldırılacaktır. Eğer bu noktada birden fazla "thread" var ise uygun olan "thread", ilgili "mutex" nesnesinin sahipliğini alacaktır. Fakat bekleyen "thread" ler arasında seçim yapılırken "FIFO" uygulanmaz. İşletim sistemi adil bir şekilde davranmaya çalışır. Buradaki önemli nokta sahipliği alınan "mutex" nesnesi artık kilitlenmiş olmaktadır. Bu noktada diğer "thread" lerin akışı bloke edilir. Eğer sahiplik geri verilmişse diğer "thread" lerden uygun olan sahipliği alır. İşte sahipliği kazandıran fonksiyon "pthread_mutex_lock" fonksiyonuyken, sahipliği geri veren fonksiyon ise "pthread_mutex_unlock" fonksiyonudur. >>>>> "pthread_mutex_lock" ve "pthread_mutex_unlock" fonksiyonları: #include int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); Bu iki fonksiyonun da argüman olarak "lock" edilecek ve "unlock" edilecek, yani sahiplini alınacak ve geri verilecek olan, "mutex" nesnesinin adresini almaktadır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine geri dönüş yaparlar. * Örnek 1, //... pthread_mutex_t g_mutex; int main(int argc, char** argv) { int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); //... if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; for(int i = 0; i < 1000000; ++i) { /* * I. Bir "thread" in akışı buraya geldikten sonra artık * "g_mutex" nesnesinin sahibi artık o "thread" olur. Dolayısıyla * diğer "thread" lerin akışı bu noktaya geldiğinde eğer sahiplik * hala başka "thread" de ise o "thread" lerin akışları bloke * edilir. */ if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; /* * Ancak ilgili "mutex" nesnesine sahip olan "thread" in akışı bu * fonksiyon çağrısından sonra, sahip olduğu "mutex" nesnesinin * kilidini kaldırır. */ if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } return NULL; } void* thread_proc2(void* param) { int result; for(int i = 0; i < 1000000; ++i) { /* * İlgili "g_mutex" nesnesi kilitlendiği için akışı buraya * gelen "thread" artık bloke edilir. Ta ki kilit kaldırılıncaya * kadar. */ if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } return NULL; } //... Buradaki kilit nokta ortak kullanılan kaynak adedince "mutex" nesnesinin olması gerektiğidir. 3 farklı kaynak ortak olarak kullanılacaksa, üç adet "mutex" nesnesine ihtiyacımız vardır. * Örnek 1, Aşağıdaki programda ilgili "mutex" nesnesi sıra ile kilitlenmiştir. İlk önce hayata "tid1" ID numarasına sahip "thread" geldiği için ilgili "mutex" nesnesinin sahibi o olacaktır. Aşağıda da görüleceği üzere "Kritik Kod" bölgesinin tek bir noktada olması GEREKMEMEKTEDİR. Önemli olan aynı "mutex" nesnesi ile ortak bir alanda çalışma yapılıyor oluşudur. #include #include #include #include #include int g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # Done => 2000000 */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("Done => %d\n", g_count); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; for(int i = 0; i < 1000000; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } return NULL; } void* thread_proc2(void* param) { int result; for(int i = 0; i < 1000000; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda ise "Kritik Kod" bölgesi tek bir noktada oluşturulmuştur. Buradaki önemli olan nokta ortak kullanılan kaynak bir adettir, yani "g_count" nesnesi. Dolayısıyla bizim bir adet "mutex" nesnesine ihtiyacımız vardır. O da "g_mutex" nesnesi. #include #include #include #include #include int g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void increment_g_count(void); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("Done => %d\n", g_count); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { increment_g_count(); return NULL; } void* thread_proc2(void* param) { increment_g_count(); return NULL; } void increment_g_count(void) { int result; for(int i = 0; i < 9000000; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Buradaki en önemli bir diğer husus ise "mutex" sahipliğinin "thread" temelli olmasıdır. Yani bir "thread" bir "mutex" nesnesinin sahipliğini aldıktan sonra sadece kendisi geri verebilir. Başka "thread" ler bu sahipliği geri veremezler. Yani bir "mutex" nesnesi, sadece kendisini "lock" eden "thread" tarafından "unlock" edilebilir. Böylesi bir durumda "pthread_mutex_lock" fonksiyonu ya başarısız olur ya da "Tanımsız Davranış" a neden olur. Bu durum ise ilgili "mutex" nesnesinin özelliğine göre değişiklik göstermektedir. Yukarıdaki "mutex" fonksiyonlarına ek olarak bir takım yardımcı "mutex" fonksiyonları da mevcuttur. Bunlar "pthread_mutex_trylock", "pthread_mutex_timedlock" vb. isimli fonksiyonlardır. >>>>> "pthread_mutex_trylock" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_mutex_trylock(pthread_mutex_t *mutex); Bu fonksiyon yine argüman olarak kilitlenecek "mutex" nesnesinin adresini alır. Fonksiyon başarı durumunda "0" ile başarısızlık durumunda ise hata kodu ile geri dönmektedir. Eğer ilgili "mutex" nesnesi başka bir "thread" tarafından kilitlenmiş ise bu fonksiyonu çağıran "thread" bloke olmaz ve "EBUSY" hata kodu ile geri döner. Dolayısıyla bizler aşağıdaki gibi bir kontrol mekanizması kullanmalıyız: if((result = pthread_mutex_trylock(&g_mutex)) != 0 && result == EBUSY) /* Başka şeyler yap... */ else exit_sys("pthread_mutex_trylock", result); Fakat bu fonksiyonu kullanırken ilgili "mutex" nesnesinin hala kilitli olup olmadığının sorgulanması biz programcılara bırakılmıştır. Yani otomatik olarak sorgulama YAPILMAMAKTADIR. Burada senaryoyu bizler oluşturmalıyız. >>>>> "pthread_mutex_timedlock" fonksiyonu, "pthread_mutex_lock" fonksiyonunun zaman aşımlı versiyonudur. Bu fonksiyon ile bir "mutex" nesnesi daha önce kilitlenmiş ise ilgili "mutex" nesnesi açılana kadar değil, bir müddet geçtikten sonra hala açılmamış ise geri dönülmesi sağlanmaktadır. Yani kilit açılana kadar değil, bir süre boyunca bu fonksiyonu çağıran "thread" bloke edilecektir. Fonksiyonun prototipi aşağıdaki gibidir; #include #include int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abstime); Fonksiyonun ilk parametresi yine kilitlenecek "mutex" nesnesinin adresini, ikinci parametresi ise 1.1.1970 tarihinden geçen mutlak zaman bilgisidir. Yani ilk önce şimdiki zamanı alıp, onun üzerine istenilen bekleme süresini ekledikten sonra ikinci parametresine geçeceğiz. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisini döndürmektedir. İlgili fonksiyonun kullanımı şöyledir; struct timespec ts; //... if(clock_gettime(CLOCK_REALTIME, &ts) == -1) exit_sys("clock_gettime"); ts.tv_sec += 5; if((result = pthread_timedlock(&g_mutex, &ts)) != 0 && result == ETIMEDOUT) /* İlgili "mutex" nesnesi beş saniye geçmesine rağmen hala kilitli olduğu için fonksiyon başarısız oldu. */ else exit_sys("pthread_timedlock", result); Şimdi de pekiştirici bir örnek yapalım: * Örnek 1, Aşağıdaki örnekte ilgili "mutex" elemanı KİLİTLENMEMİŞTİR. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void do_something(const char* name); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # thread_I: I. Step thread_II: I. Step thread_II: II. Step thread_I: II. Step thread_I: III. Step thread_I: IV. Step thread_II: III. Step thread_II: IV. Step thread_I: V. Step thread_II: V. Step --------------------- thread_I: I. Step thread_I: II. Step thread_I: III. Step --------------------- thread_II: I. Step thread_II: II. Step thread_II: III. Step thread_I: IV. Step thread_I: V. Step thread_II: IV. Step --------------------- thread_I: I. Step thread_II: V. Step thread_I: II. Step --------------------- thread_II: I. Step thread_I: III . Step thread_II: II. Step thread_I: IV. Step thread_II: III. Step thread_II: IV. Step thread_I: V. Step --------------------- thread_II: V. Step --------------------- */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, "thread_I")) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, "thread_II")) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 3; ++i) do_something((const char*)(param)); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 3; ++i) do_something((const char*)(param)); return NULL; } void do_something(const char* name) { printf("%s: I. Step\n", name); usleep(rand() % 100); printf("%s: II. Step\n", name); usleep(rand() % 200); printf("%s: III. Step\n", name); usleep(rand() % 300); printf("%s: IV. Step\n", name); usleep(rand() % 400); printf("%s: V. Step\n", name); usleep(rand() % 500); puts("---------------------"); } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise ilgili "mutex" nesnesi kilitlenmiştir. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void do_something(const char* name); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # thread_II: I. Step thread_II: II. Step thread_II: IIII. Step thread_II: IV. Step thread_II: V. Step --------------------- thread_II: I. Step thread_II: II. Step thread_II: IIII. Step thread_II: IV. Step thread_II: V. Step --------------------- thread_II: I. Step thread_II: II. Step thread_II: IIII. Step thread_II: IV. Step thread_II: V. Step --------------------- thread_I: I. Step thread_I: II. Step thread_I: IIII. Step thread_I: IV. Step thread_I: V. Step --------------------- thread_I: I. Step thread_I: II. Step thread_I: IIII. Step thread_I: IV. Step thread_I: V. Step --------------------- thread_I: I. Step thread_I: II. Step thread_I: IIII. Step thread_I: IV. Step thread_I: V. Step --------------------- */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, "thread_I")) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, "thread_II")) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 3; ++i) do_something((const char*)(param)); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 3; ++i) do_something((const char*)(param)); return NULL; } void do_something(const char* name) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); printf("%s: I. Step\n", name); usleep(rand() % 100); printf("%s: II. Step\n", name); usleep(rand() % 200); printf("%s: III . Step\n", name); usleep(rand() % 300); printf("%s: IV. Step\n", name); usleep(rand() % 400); printf("%s: V. Step\n", name); usleep(rand() % 500); puts("---------------------"); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte kullanılan "mutex" nesnesi kilitlenmemiştir. #include #include #include #include #include typedef struct tagNODE{ int val; struct tagNODE* next; }NODE; typedef struct tagLLIST{ size_t count; NODE* head; NODE* tail; pthread_mutex_t mutex; }LLIST; LLIST* create_list(void); NODE* add_item(LLIST* llist, int value); void walk_list(LLIST* llist); void destroy_list(LLIST* llist); void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # [12825] => 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 0 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 292 293 185 186 187 188 189 190 191 192 193 194 304 195 196 197 198 309 310 311 12 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 292 293 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 420 421 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 */ LLIST* llist; if((llist = create_list()) == NULL) { fprintf(stderr, "cannot create a linked list!...\n"); exit(EXIT_FAILURE); } int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); printf("[%zd] => ", llist->count); walk_list(llist); destroy_list(llist); return 0; } LLIST* create_list(void) { LLIST* llist; if((llist = (LLIST*)malloc(sizeof(LLIST))) == NULL) return NULL; llist->count = 0; llist->head = NULL; llist->tail = NULL; return llist; } NODE* add_item(LLIST* llist, int value) { NODE* new_node; if((new_node = (NODE*)malloc(sizeof(NODE))) == NULL) /* Yeni bir düğüm oluşturduk. */ return NULL; if(llist->head == NULL) /* Eğer bağlı listede hiç düğüm yok ise ilk düğüm "new_node" olacaktır. */ llist->head = new_node; else /* Aksi halde son düğümün içindeki gösterici, "new_node" düğümünü gösterecektir. */ llist->tail->next = new_node; /* Son düğümün hangi düğüm olduğu bilgisi güncellendi. */ llist->tail = new_node; /* Yeni oluşturulan düğümün içindeki bilgi güncellendi. */ new_node->val = value; /* Bağlı listedeki düğüm adedi güncellendi. */ ++llist->count; return new_node; } void walk_list(LLIST* llist) { NODE* node; node = llist->head; /* Bağlı listenin ilk düğümünü temin ettik. */ /* * Son düğüme geldiğimiz zaman, son düğmün içindeki gösterici * "NULL" olacağından döngü sonra erecektir. */ while(node != NULL) { printf("%d ", node->val); fflush(stdout); node = node->next; } } void destroy_list(LLIST* llist) { NODE* node, *temp_node; node = llist->head; while(node != NULL) { temp_node = node->next; /* Silinecek düğümden bir sonraki düğümü saklıyoruz. */ free(node); /* Daha sonra silmek istediğimiz düğümü siliyoruz. */ node = temp_node; /* Daha sonra yukarıda sakladığımızı silmek istediğimizi belirtiyoruz. */ } free(llist); } void* thread_proc1(void* param) { LLIST* llist = (LLIST*)(param); for(int i = 0; i < 10000; ++i) if(add_item(llist, i) == NULL) { fprintf(stderr, "cannot allocate a linked list...\n"); exit(EXIT_FAILURE); } return NULL; } void* thread_proc2(void* param) { LLIST* llist = (LLIST*)(param); for(int i = 0; i < 10000; ++i) if(add_item(llist, i) == NULL) { fprintf(stderr, "cannot allocate a linked list...\n"); exit(EXIT_FAILURE); } return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> "restrict" göstericiler, C99 ile dile eklenmiştir. Göstericilere özel bir nitelemedir. Yani sadece göstericinin kendisi "restrict" olabilir ve şu manaya gelmektedir: İlgili göstericinin gösterdiği belleğe sadece o gösterici ile erişebiliriz. Başka göstericiler ile aynı belleğe erişmeyeceğiz. Eğer ilgili bellek alanlarına başka göstericiler erişirse, "Tanımsız Davranış" meydana gelecektir. "Pointer Alliasing" optimizasyonuna olanak sağlar. Daha çok birden fazla gösterici-tip argüman alan fonksiyon imzalarına karşımıza çıkar. Böylesi bir senaryoda ise bize anlatımak istenen şey şudur; bu iki adres çakışık olmayacak. Yani iş bu fonksiyonun iki parametresine aynı adresi geçmeyeceğiz veya bir dizinin başlangıç adreslerini geçtiğimiz zaman bu iki adres değeri başka dizilere ait olmalı. Aynı diziye ait farklı adres bilgilerini bile geçsek yine sorun olacaktır. Tabii bu kural ilgili fonksiyonun çağrısı sürdüğü müddetçe geçerlidir. İlgili fonksiyondan geri döndükten sonra bu kural ortadan kalkmaktadır. Yani "restrict" olarak nitelenen göstericinin ömrü bittikten sonra ilgili alana diğer göstericiler üzerinden erişebiliriz. >> Aşağıdaki kullanım biçimi C99 ile legal hale gelmiştir: * Örnek 1, #include struct INFO{ int a; float f; }info; int main() { info = (struct INFO){ 10, 10.01f }; printf("%d - %f\n", info.a, info.f); // OUTPUT => 10 - 10.010000 int *ptr; ptr = (int[]){ 10, 20, 30}; // ptr = (int[3]){ 10, 20, 30}; for(int i = 0; i < 3; ++i) printf("%d ", *ptr++); // OUTPUT => 10 20 30 puts(""); ptr = (int[5]){100, 150, [2]=31, 200, 300}; for(int i = 0; i < 5; ++i) printf("%d ", *ptr++); // OUTPUT => 100 150 31 200 300 return 0; } /*================================================================================================================================*/ (54_21_05_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. Ancak ilk "thread" bu bölgeden çıktıktan sonra diğer "thread" in girmesine izin verilmektedir. >>>> "Kritik Kod" (devam): >>>>> "mutex" nesneleri (devam): Aşağıda "mutex" nesnesinin önemini belirten bir örnek de verilmiştir. * Örnek 1, Aşağıdaki örnekte üç adet "thread" oluşturulmuştur. #include #include #include #include #include #include typedef struct tagNODE{ int val; struct tagNODE* next; }NODE; typedef struct tagLLIST{ size_t count; NODE* head; NODE* tail; pthread_mutex_t mutex; }LLIST; LLIST* create_list(void); NODE* add_item(LLIST* llist, int value); NODE* add_item_head(LLIST* llist, int value); int walk_list(LLIST* llist); int destroy_list_thread_safe(LLIST* llist); void destroy_list(LLIST* llist); void* thread_proc1(void* param); void* thread_proc2(void* param); void* thread_proc3(void* param); void exit_sys(const char* msg, int return_value); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 ... = [20000] */ LLIST* llist; if((llist = create_list()) == NULL) exit_sys("create_list", errno); int result; pthread_t tid1, tid2, tid3; if((result = pthread_create(&tid1, NULL, thread_proc1, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid3, NULL, thread_proc3, llist)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid3, NULL)) != 0) exit_sys("pthread_join", result); printf(" = [%zd]\n", llist->count); destroy_list(llist); puts("Ok"); return 0; } LLIST* create_list(void) { LLIST* llist; if((llist = (LLIST*)malloc(sizeof(LLIST))) == NULL) return NULL; llist->count = 0; llist->head = NULL; llist->tail = NULL; int result; if((result = pthread_mutex_init(&llist->mutex, NULL)) != 0) { free(llist); errno = result; /* "errno" değeri "thread" e özgüdür. */ return NULL; } return llist; } NODE* add_item(LLIST* llist, int value) { NODE* new_node; if((new_node = (NODE*)malloc(sizeof(NODE))) == NULL) /* Yeni bir düğüm oluşturduk. */ return NULL; int result; if((result = pthread_mutex_lock(&llist->mutex)) != 0) { free(new_node); goto FAIL; } if(llist->head == NULL) llist->head = new_node; else llist->tail->next = new_node; llist->tail = new_node; new_node->val = value; ++llist->count; if((result = pthread_mutex_unlock(&llist->mutex)) != 0) { /* * Bu noktada bizler "new_node" düğümünü geri vermedik * çünkü ilgili düğüm halihazırda bağlı listeye dahil * edilmiştir. */ goto FAIL; } return new_node; FAIL: errno = result; /* "errno" değeri "thread" e özgüdür. */ return NULL; } NODE* add_item_head(LLIST* llist, int value) { NODE* new_node; if((new_node = (NODE*)malloc(sizeof(NODE))) == NULL) /* Yeni bir düğüm oluşturduk. */ return NULL; int result; if((result = pthread_mutex_lock(&llist->mutex)) != 0) { free(new_node); goto FAIL; } if(llist->head == NULL) llist->tail = new_node; new_node->next = llist->head; llist->head = new_node; new_node->val = value; ++llist->count; if((result = pthread_mutex_unlock(&llist->mutex)) != 0) goto FAIL; return new_node; FAIL: errno = result; return NULL; } int walk_list(LLIST* llist) { NODE* node; int result; if((result = pthread_mutex_lock(&llist->mutex)) != 0) { return errno = result; } node = llist->head; while(node != NULL) { printf("%d ", node->val); fflush(stdout); node = node->next; } if((result = pthread_mutex_unlock(&llist->mutex)) != 0) { return errno = result; } return 0; } int destroy_list_thread_safe(LLIST* llist) { NODE* node, *temp_node; int result; if((result = pthread_mutex_lock(&llist->mutex)) != 0) goto FAIL; node = llist->head; while(node != NULL) { temp_node = node->next; free(node); node = temp_node; } if((result = pthread_mutex_unlock(&llist->mutex)) != 0) goto FAIL; if((result = pthread_mutex_destroy(&llist->mutex)) != 0) { free(llist); goto FAIL; } free(llist); return 0; FAIL: return errno = result; } void destroy_list(LLIST* llist) { NODE* node, *temp_node; node = llist->head; while(node != NULL) { temp_node = node->next; free(node); node = temp_node; } int result; if((result = pthread_mutex_destroy(&llist->mutex)) != 0) free(llist); free(llist); } void* thread_proc1(void* param) { LLIST* llist = (LLIST*)(param); for(int i = 0; i < 10000; ++i) if(add_item(llist, i) == NULL) exit_sys("add_item", errno); return NULL; } void* thread_proc2(void* param) { LLIST* llist = (LLIST*)(param); for(int i = 0; i < 10000; ++i) if(add_item_head(llist, i) == NULL) exit_sys("add_item", errno); return NULL; } void* thread_proc3(void* param) { LLIST* llist = (LLIST*)(param); walk_list(llist); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } >>>>>> "mutex" özellikleri: Anımsayacağınız üzere "mutex" nesnesine ilk değer verirken "mutex" özelliklerini es geçmiştik. Tıpkı "thread" nesnesinde yaptıklarımız gibi burada da ilk önce "mutex" özellik nesnesini oluşturup, onu "init" yapmalıyız. Daha sonra değiştirmek istediğimiz özellik var ise ilgili "set" fonksiyonlarını çağırıyoruz. En sonunda da bu özellik nesnesini kullanarak "mutex" nesnesini hayata getiriyoruz. >>>>>>> "mutex" özellik nesnesinin tanımlanması ve "init" edilmesi: "mutex" özellik nesnesi "pthread_mutexattr_t" türünden bir nesnedir. Bu nesneye ilk değer vermek için de "pthread_mutexattr_init" fonksiyonunu çağırmamız gerekmektedir. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_mutexattr_init(pthread_mutexattr_t *attr); Fonksiyon argüman olarak üzerinde işlem yapılacak olan "mutex" özellik nesnesinin adresini alır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine dönmektedir. * Örnek 1, //... int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); //... return 0; } //... >>>>>>> Bu aşamada "pthread_mutexattr_setXXX" biçiminde olan "set" fonksiyonları ile ilgili "mutex" özelliklerini değiştirebileceğiz. * Örnek 1, //... int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); /* "mutex" özellik nesnesi bu aşamada "set" edildi. */ //... return 0; } //... >>>>>>> Artık "mutex" nesnesi, ilgili "mutex" özellik nesnesi ile oluşturulabilir. * Örnek 1, //... int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); /* "mutex" özellik nesnesi bu aşamada "set" edildi. */ if((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys("pthread_mutex_init", result); //... return 0; } //... >>>>>>> "mutex" nesnesi hayata geldikten sonra ilgili "mutex" özellik nesnesini yok edebiliriz. Bunun için "pthread_mutexattr_destroy" fonksiyonunu çağırmamız gerekmektedir. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); Fonksiyon argüman olarak üzerinde işlem yapılacak olan "mutex" özellik nesnesinin adresini alır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine dönmektedir. * Örnek 1, //... int main(int argc, char** argv) { /* # OUTPUT # Done => 18000000 */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); /* "mutex" özellik nesnesi bu aşamada "set" edildi. */ if((result = pthread_mutex_init(&g_mutex, mattr)) != 0) exit_sys("pthread_mutex_init", result); if((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys("pthread_mutexattr_destroy", result); //... return 0; } //... Pekiyi bizler "mutex" nesnesinin hangi özelliklerini değiştirebiliriz? Şöyle bir senaryo düşünelim; bir "thread", bir "mutex" nesnesinin sahipliğini almış olsun. Eğer aynı "thread", aynı "mutex" nesnesinin sahipliğini almak isterse şu üç farklı sonuçtan birisi meydana gelecektir: "thread" kendisini kilitler yani "deadlock" oluşur, "thread" ilgili "mutex" nesnesinin sahipliğini sorunsuz bir şekilde ikinci kez alır, halihazırda sahipliği alınan bir "mutex" nesnesinin sahipliği ikinci defa alınmak istendiğinde ilgili fonksiyon başarısız olur. İşte bu sonuç, "mutex" nesnesinin "type" özelliğine bağlıdır. Bu özellik ise "pthread_mutexattr_settype" isimli fonksiyon ile "set" edilir. >>>>>>>> "pthread_mutexattr_settype" fonksiyonu aşağıdaki prototipe sahiptir: #include int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); Fonksiyonun birinci parametresi ilgili "mutex" özellik nesnesinin adresini almaktadır. İkinci parametre ise şu değerlerden birisini alır: "PTHREAD_MUTEX_NORMAL", "PTHREAD_MUTEX_ERRORCHECK", "PTHREAD_MUTEX_RECURSIVE" ve "PTHREAD_MUTEX_DEFAULT". -> "PTHREAD_MUTEX_NORMAL" : Eğer bir "mutex" nesnesinin sahipliği ikinci kez alınmak istenirse, "deadlock" oluşmasına neden olur. -> "PTHREAD_MUTEX_ERRORCHECK" : Sahipliği alınmamış "mutex" nesnesinin sahipliğinin geri verilmesi ve ikinci kez bir "mutex" nesnesinin sahipliğinin alınmak istenmesi durumunda "pthread_mutex_lock" fonksiyonun başarısız olmasına neden olur. -> "PTHREAD_MUTEX_RECURSIVE" : Sahipliğini aldığı "mutex" nesnesinin tekrardan sahipliğini almasını sağlar fakat sahipliğini aldığı adet kadar sahipliği geri vermelidir. -> "PTHREAD_MUTEX_DEFAULT" : Bu durum varsayılan durumdur ve yukarıdaki üç durumdan birisidir. Linux ve türevi sistemlerde "PTHREAD_MUTEX_NORMAL" durumundadır. Fonksiyonun geri dönüş değeri başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisidir. * Örnek 1, Aşağıda varsayılan durum "mutex" özellik nesnesi kullanılmıştır ve "deadlock" meydana gelmiştir. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # */ int result; if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); foo(); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void* thread_proc2(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); foo(); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void foo(void) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); puts("\n--------------"); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıda ise "PTHREAD_MUTEX_RECURSIVE" tür bilgisi olarak belirtilmiştir. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # -------------- -------------- */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); if((result = pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE)) != 0) exit_sys("pthread_mutexattr_settype", result); if((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys("pthread_mutex_init", result); if((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys("pthread_mutexattr_destroy", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); foo(); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void* thread_proc2(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); foo(); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void foo(void) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); puts("\n--------------"); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi de şöyle bir senaryo düşünelim; bir "thread", bir "mutex" nesnesinin sahipliğini aldıktan hemen sonra sonlanmaktadır. Bu durumda diğer "thread" ler sahiplik almak isterlerse ise ya "deadlock" oluşacak ya da bu sahiplik alma isteği başarısızlık olacaktır. İşte bu durumda ilgili "mutex" nesnesinin "robust" olma durumuna bakılır. Bu noktada da devreye "pthread_mutexattr_setrobust" fonksiyonu girmektedir. >>>>>>>> "pthread_mutexattr_setrobust" fonksiyonunun parametrik yapısı aşağıdaki gibidir: #include int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust); Fonksiyonun birinci parametresi ilgili "mutex" özellik nesnesinin adresini almaktadır. İkinci parametre ise şu değerlerden birisini almaktadır; "PTHREAD_MUTEX_STALLED" ve "PTHREAD_MUTEX_ROBUST" -> "PTHREAD_MUTEX_STALLED" : Bu durum varsayılan durumdur. Eğer başka bir "thread" ilgili "mutex" nesnesini tekrardan kilitlemeye çalışırsa "deadlock" oluşacaktır. -> "PTHREAD_MUTEX_ROBUST" : Bu durumda ise ilgili "mutex" nesnesini kilitlemeye çalışan fonksiyon başarısız olur. Şimdi böylesi bir "mutex" nesnesini kilitlemeye çalışan ilk "thread" için "pthread_mutex_lock" fonksiyonu "EOWNERDEAD" hata kodu ile geri dönmektedir. Diğer "thread" lerin yeniden bu "mutex" nesnesini kilitlemeye çalışması durumunda ise ilgili "pthread_mutex_lock" fonksiyonunun "EOWNERDEAD" hata kodu ile geri dönüp dönmemesi işletim sistemini yazanlara bırakılmıştır. POSIX standartları sadece ilk "thread" için garanti vermektedir. Fonksiyon başarı durumunda "0", başarısızlık durumunda "-1" ile geri dönecektir. * Örnek 1, Aşağıdaki programda "deadlock" gözlemlenmektedir. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # */ int result; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void* thread_proc2(void* param) { sleep(1); int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ilgili "mutex" nesnesi "robust" olarak oluşturulmuştur. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char* msg, int return_value); pthread_mutex_t g_mutex; int main(int argc, char** argv) { /* # OUTPUT # pthread_mutex_lock : Owner died */ int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys("pthread_mutexattr_init", result); if((result = pthread_mutexattr_setrobust(&mattr, PTHREAD_MUTEX_ROBUST)) != 0) exit_sys("pthread_mutexattr_settype", result); if((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys("pthread_mutex_destroy", result); return 0; } void* thread_proc1(void* param) { int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); } void* thread_proc2(void* param) { sleep(1); int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys("pthread_mutex_lock", result); return NULL; } void exit_sys(const char* msg, int return_value) { if(return_value) fprintf(stderr, "%s : %s", msg, strerror(return_value)); else perror(msg); exit(EXIT_FAILURE); } Şimdi yukarıdaki senaryodaki "mutex" nesnesini tekrar kilitlenebilir kılmak için de "pthread_mutex_consistent" isimli fonksiyonu çağırmalıyız. >>>>>>>> "pthread_mutex_consistent" fonksiyonu aşağıdaki parametrik yapıya sahiptir: #include int pthread_mutex_consistent(pthread_mutex_t *mutex); Fonksiyon parametre olarak tekrar kullanılabilir hale getirilecek "mutex" nesnesinin adresini alır. Başarı durumunda "0", başarısızlık durumunda hata kodunun kendisine dönecektir. Bir "mutex" nesnesi "consistent" duruma sokulursa, aynı zamanda kilitlenmiş de olur. Dolayısıyla aşağıdaki gibi örnek bir kullanım gözetilebilir: result = pthread_mutex_lock(&g_mutex); if(result == EOWNERDEAD) pthread_mutex_consistent(&g_mutex); else exit_sys("pthread_mutex_lock", result); /* "g_mutex" is locked now. */ > Hatırlatıcı Notlar: >> Bir fonksiyon yazarken, >>> Eğer fonksiyon başarısız olmuş ise başarısızlığın nedenini dış dünyaya bildirmeye çalışın. >>> Eğer fonksiyon başarısız olmuş ise programın o anki durumunun ilgili fonksiyon çağrısından evvelki haline geri getirmeye çalışın. Yani aslında o fonksiyon hiç çağrılmamış gibi olsun. Bu durum ise "strong guarantee" demektir. Eğer program stabil ise fakat fonksiyon çağrılmadan evvelki duruma da geri dönmemiş ise "basic guarantee" denmektedir. /*================================================================================================================================*/ (55_27_05_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. Ancak ilk "thread" bu bölgeden çıktıktan sonra diğer "thread" in girmesine izin verilmektedir. >>>> "Kritik Kod" (devam): >>>>> "mutex" nesneleri (devam): Pekiyi "mutex" nesnelerinin prosesler arasındaki kullanımı nasıl olmaktadır? Anımsayacağımız üzere bu zamana kadar bizler aynı prosese ait "thread" leri senkronize etmiştik. Farklı proseslerdeki "thread" leri senkronize etmemiz için bizlerin "Paylaşılan Bellek Alanları" yöntemini kullanmamız gerekmektedir çünkü bu "mutex" nesnelerine isim verilemediği için "İsimli Boru Haberleşmesi" yöntemini kullanamayız. Yani "Paylaşılan Bellek Alanları" yöntemi bir nevi zorunluluk olmuş oluyor. Fakat bu da yeterli gelmemektedir; ilgili "mutex" nesnesini de "Paylaşılan Bellek Alanlarında" kullanılabilir hale getirmemiz gerekmektedir. Bunu da POSIX standartlarınca "mutex" özellik nesneleri ile gerçekleştiriyoruz. Bu işlemi de "pthread_mutexattr_setpshared" isimli fonksiyon ile gerçekleştirebiliriz. >>>>>> "pthread_mutexattr_setpshared" isimli fonksiyon aşağıdaki parametrik yapıya sahiptir: #include int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); Fonksiyonun birinci parametresi "mutex" özellik nesnesinin adresini, ikinci parametresi ise ilgili "mutex" nesnesinin paylaşılabilirlik durumunu belirtmektedir. Bu ikinci parametre şu iki değerden birisini almaktadır; "PTHREAD_PROCESS_SHARED" ve "PTHREAD_PROCESS_PRIVATE". -> "PTHREAD_PROCESS_SHARED" : İlgili "mutex" nesnesi paylaşılabilir durumdadır. -> "PTHREAD_PROCESS_PRIVATE" : İlgili "mutex" nesnesi paylaşılamaz durumdadır. Varsayılan durum bu durumdur. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", başarısızlık durumunda hata kodunun kendisine geri dönmektedir. Buradan da anlaşılacağı üzere "mutex" nesnesinin prosesler arasında haberleşme için kullanılması biraz zahmetlidir. * Örnek 1, Aşağıdaki örnekte iki proses "mutex" ve "Paylaşılan Bellek Alanları" yöntemi kullanılarak senkronize bir biçimde haberleştirilmiştir. Burada ilk olarak "write" fonksiyonu, daha sonra "read" fonksiyonu çalıştırılmalıdır. Çünkü "mutex" nesnesinin ve "Paylaşılan Bellek Alanı" nesnesinin oluşturulması ve yok edilmesi "write" programı tarafından gerçekleştirilmektedir. Fakat unutmamalıyız ki "Paylaşılan Bellek Alanı" nesnesi "shm_unlink" fonksiyonu ile yok edilse bile gerçek manada yok edilmenin gerçekleşmesi için ilgili nesnenin başka prosesler tarafından kullanılmaması gerekmektedir. Aksi halde gerçek manada silme işlemi gerçekleşmeyecektir. Fakat "mutex" nesnesi için böyle bir şey söz konusu olmadığından, "mutex" nesnesini yok etmeden evvel, başka proseslerin iş bu "mutex" nesnesini kullanmadığından EMİN OLMALIYIZ. Örneğin, "Paylaşılan Bellek Alanı" nesnesi içinde ayrı bir sayaç daha tutulabilir ve bu sayacın değeri "0" olduğunda "mutex" nesnesi yok edilir. Tabii bu sayacın kontrolü de yine "Kritik Kod" alanında yapılmalıdır. Ek olarak "Condition Variables" denilen bir başka senkronizasyon nesnesini de kullanabiliriz. Fakat aşağıdaki programda karadüzen bir sistem kurulmuştur. Çünkü "Paylaşılan Bellek Alanı" nesnesi yok edilirken içerisindekiler de yok edilir. Son olarak aşağıdaki programları derlerken "-lrt -lpthread" seçeneklerini kullanmalıyız. /* sharing.h */ #ifndef SHARING_H_ #define SHARING_H_ #include typedef struct tagSHARED_INFO { int count; //... pthread_mutex_t mutex; }SHARED_INFO; #endif /* write */ #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_NAME "/shared_memory_for_mutex" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if((result = pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_mutexattr_setpshared", result); if((result = pthread_mutex_init(&shmaddr->mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); shmaddr->count = 0; if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); for(int i = 0; i < 1000000000; ++i) ++shmaddr->count; if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%d\n", shmaddr->count); printf("Press ENTER to exit!...\n"); getchar(); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* read */ #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_NAME "/shared_memory_for_mutex" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int result; if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); for(int i = 0; i < 1000000000; ++i) ++shmaddr->count; if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("%d\n", shmaddr->count); printf("Press ENTER to exit!...\n"); getchar(); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde "mutex" nesneleri nispeten hızlı senkronizasyon araçlarıdır. Linux sistemlerinde ise senkronizasyon nesnelerinin bir çoğu "futex" denilen sistem fonksiyonunu kullanmaktadır. Öte yandan "mutex" fonksiyonları duruma göre "kernel mode" a geçmeden "user mode" olarak işlemlerini yapmaktadır. Şöyleki; -> "pthread_mutex_lock" fonksiyonu ile bir "mutex" nesnesini kilitlemek isteyelim. -> "mutex" nesnesi, kendi içerisinde bayrak değişkenleri tutarak, kilitleme işlemini atomik bir biçimde yapmak isteyecektir. Fakat atomikliği sağlamanın bir yolu ise "kernel mode" a geçerek, "CLI" gibi makine kodlarıyla kesme mekanizmasını kapatarak (yani "task-switch" mekanizmasını kapatarak), ilgili bayrak değişkenlerini "set" etmek de olabilir. Fakat "kernel mode" a geçmenin de maliyeti yüksektir. -> İşte yeni modern işlemciler "compare and set" gibi atomik makine kodları eklenmiştir. Bu makine komutları sayesinde bu tür bayrak değişkenleri "kernel mode" a geçmeden "user mode" da atomik bir biçimde "set" edilebilmektedir. Tabii burada bahsedilen durum kilitli olmayan bir "mutex" nesnesinin kilitlenmesi için geçerlidir. Eğer ilgili "mutex" nesnesi halihazırda kilitli ise "pthread_mutex_lock" fonksiyonu ilgili "thread" i bloke edecektir. Fakat burada da şöyle bir nüans vardır; ilgili "mutex" nesnesinin kilitli olduğunun görülmesi üzerine hemen bloke işlemi gerçekleştirilmez ve kısa bir müddet meşgul bir döngü içerisinde beklenir. Umulur ki beklenen bu zaman içerisinde kilit kaldırılır. Eğer bu süre sonunda kilit kalkmamış olursa ilgili "thread" bloke edilir. Buradaki meşgul bekleme durumuna da "spin lock" denilmektedir. İşte bu bloke işlemi için "kernel mode" a geçiş yapılması gerekmektedir... Dolayısıyla hiç "kernel mode" a geçmeden işlem yapmanın bir olanağı yoktur. >>>>> "Conditional Variables" : Tıpkı "mutex" nesneleri gibi bu koşul nesneleri de birer senkronizasyon nesneleridir. Fakat "mutex" nesneleri gibi tek başlarına değil, bir "mutex" nesnesi ile birlikte kullanılmaktadırlar. Bu nesnelerin temel kullanım amacı, belli bir koşul sağlanana kadar, ilgili "thread" i bloke etmektir. Buradaki koşul programcı tarafından oluşturulmaktadır. Örneğin, global isim alanında belirtilen "g_count" değişkeninin değerinin sıfırdan büyük olması. Bu koşul değişkenleri nesnelerini kullanabilmek için belli yöntemlerin izlenmesi gerekmektedir. Şöyleki: >>>>>> "pthread_cond_t" türünden bir nesne hayata getiriyoruz. Bu tür "sys/types.h" içerisinde bir türe karşılık gelmektedir ve tipik olarak bir yapının tür eş ismidir. Tipik olarak bu nesne "global" isim alanı içerisinde tanımlanır ki diğer "thread" ler tarafından görülür olsun. >>>>>> Koşul değişkenleri nesnelerini de tıpkı "mutex" nesnelerinde olduğu gibi iki biçimde ilk değer verebiliriz; "PTHREAD_COND_INITIALIZER" makrosunun kullanılması veya "pthread_cond_init" fonksiyonunun kullanımı. >>>>>>> "PTHREAD_COND_INITIALIZER" makrosunun kullanımı aşağıdaki biçimdedir. * Örnek 1, //... pthread_cond_t g_cound = PTHREAD_COND_INITIALIZER; //... int main(int argc, char** argv) { //... return 0; } //... >>>>>>> "pthread_cond_init" fonksiyonu aşağıdaki prototipe sahiptir: #include int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t * attr); Fonksiyonun ilk parametresi ilk değer verilecek koşul değişkeni nesnesinin adresi, ikinci parametresi ise koşul değişkeni özellik nesnesidir. Bu özellik nesnesine daha sonra değinilecektir. Dolayısıyla bu nesneyi kullanmayacağımız için "NULL" değerini kullanacağız. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönecektir. Yine "mutex" nesnelerinde olduğu gibi bu fonksiyonun ikinci parametresine "NULL" değerini geçmekle yukarıdaki "PTHREAD_COND_INITIALIZER" makrosunun kullanımı arasında bir fark yoktur. >>>>>> Koşul değişkenleri nesnelerinin tek başlarına kullanılamayacağından bahsetmiştik. Şimdi de bu nesne ile birlikte kullanacağımız bir "mutex" nesnesini hayata getirmemiz gerekmektedir. >>>>>> Programcının artık bir koşul oluşturması gerekmektedir. Örneğin, "g_flag" değişkeninin "1" değerinde olması. Buradaki koşuldan kastedilen şey ilgili "thread" in bloke olmaması için gerekli olan durum manasındadır. Yukarıdaki örneği baz alırsak da "g_flag" değişkeninin değeri "1" değil ise ilgili "thread" bloke olacak, "1" olması durumunda bloke kalkacaktır. Bloke olmayı kötü bir şey olarak varsayarsak, koşulun sağlanmaması bir ceza olarak görebiliriz. Tipik olarak "global" isim alanındaki değişkenler koşul olarak kullanılırlar. Aşağıdaki kalıp bu duruma bir örnek gösterilebilir: * Örnek 1, //... pthread_mutex_lock(&g_mutex); /* Bu aşamada "mutex" nesnesinin sahipliği alındı, yani kilitlendi. */ while( g_flag != 1 ) /* Buradaki koşul sağlanmadığı sürece aşağıdaki fonksiyon çağrılacaktır. */ pthread_cond_wait(&g_cond, &g_mutex); /* Koşul sağlanmadığı için bu fonksiyon ilgili "thread" i bloke edecektir. */ /* * Bu aşamada kritik kod * bölgesi bulunmaktadır. */ pthread_mutex_unlock(&g_mutex); /* Bu aşamada "mutex" nesnesinin sahipliği geri verildi, yani kilit kaldırıldı. */ Pekiyi bu "pthread_cond_wait" fonksiyonu tam olarak ne iş görür? >>>>>>> "pthread_cond_wait" fonksiyonu, koşul değişkeni nesnesini bekleyen fonksiyondur. Prototipi aşağıdaki gibidir: #include int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex); Fonksiyonun birinci parametresi ilgili koşul değişkeni nesnesinin adresi, ikinci parametresi ise ilgili "mutex" nesnesinin adresini almaktadır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. Bu fonksiyon ilgili "thread" nesnesini bloke etmektedir eğer koşul sağlanmamışsa. "thread" in akışı bu fonksiyona girdiğinde artık ilgili "mutex" nesnesinin kilidi kaldırılmakta, sahipliği geri verilmektedir. Buradaki bloke olma ve sahipliğinin geri verilmesi atomik bir olaydı. Şimdi burada iki farklı önemli nokta vardır: İlgili koşulun sağlanması ve bloke edilen ilgili "thread" in uyandırılması ya da blokesinin kaldırılması. Bu iki önemli nokta da diğer "thread" veya "thread" ler tarafından GERÇEKLEŞTİRİLİR. Diğer "thread" (ler) tarafından koşul uygun hale getirildikten sonra blokenin kalkması için iki farklı fonksiyon daha çağrılmalıdır. Bunlar "pthread_cond_broadcast" ve "pthread_cond_signal" isimli fonksiyonlardır. >>>>>>> Bu iki fonksiyonun da prototipi aşağıdaki gibidir: #include int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond); Fonksiyon parametre olarak ilgili koşul değişken nesnesinin adresini almaktadır. Başarı durumunda "0", başarısızlık durumunda ise hata kodunun kendisine dönmektedir. Bu fonksiyonlardan "pthread_cond_broadcast" isimli fonksiyon ilgili koşul değişken nesnesini blokede bekleyen bütün "thread" leri uyandırırken, "pthread_cond_signal" fonksiyonu en az bir tanesini uyandırmaktadır. Genel olarak "pthread_cond_signal" fonksiyonu kullanılır. Şimdi yukarıdaki kalıbı tekrar inceleyelim: * Örnek 1, //... pthread_mutex_lock(&g_mutex); /* Bu aşamada "mutex" nesnesinin sahipliği alındı, yani kilitlendi. */ while( g_flag != 1 ) /* Buradaki koşul sağlanmadığı sürece aşağıdaki fonksiyon çağrılacaktır. */ pthread_cond_wait(&g_cond, &g_mutex); /* * Koşul sağlanmadığı için bu fonksiyon ilgili "thread" i bloke edecek ve * ilgili "mutex" nesnesinin sahipliği de geri verilmiştir. Fakat bloke kaldırılırsa, * fonksiyondan çıkmadan hemen önce ilgili "mutex" nesnesini tekrar kilitlemeye * çalışmaktadır. Burada ilgili "mutex" nesnesi boş ise kilitleyecektir. Eğer * başka "thread" ler tarafından kilitlenmiş ise, varsayılan durumda, kilit * açılana kadar bizim "thread" yine bloke olacaktır. Bu son bloke işleminin * kalkması için artık ilgili "mutex" nesnesinin kilitlenebilir durumda olması * gerekmektedir. Bu noktada "pthread_cond_signal" veya "pthread_cond_broadcast" * fonksiyonlarının yeniden çağrılması lüzumsuzdur. */ /* * Bu aşamada kritik kod * bölgesi bulunmaktadır. */ pthread_mutex_unlock(&g_mutex); /* Bu aşamada "mutex" nesnesinin sahipliği geri verildi, yani kilit kaldırıldı. */ Yukarıdaki kalıba göre bütün bunları özetleyecek olursak; -> İlgili koşul sağlanmamış ise ilgili "thread" in akışı "pthread_cond_wait" fonksiyonuna girecektir. -> İlgili "thread" in akışı "pthread_cond_wait" fonksiyonuna girince bloke olacak ve sahip olduğu ilgili "mutex" nesnesi geri verilecektir. -> Bu aşamada diğer "thread" ler tarafından koşul sağlanmalı ve "pthread_cond_broadcast" veya "pthread_cond_signal" fonksiyonlarına çağrı yapılmalıdır. Böylelikle ilgili "thread" in blokesi kalkacaktır. -> Uyanan/blokesi kalkan "thread", ilgili "mutex" nesnesinin sahipliğini geri almaya çalışacaktır. Bu noktada devreye "pthread_mutex_lock" fonksiyonu girecektir. Eğer ilgili "mutex" nesnesi başka "thread" tarafından kilitlenmişse, varsayılan durumda, kilidin kalkmasını bekleyecektir. Kilit kalktığında da bizim "thread" ilgili "mutex" nesnesini kilitleyecek ve "thread" in akışı "pthread_cond_wait" fonksiyonundan çıkacaktır. Eğer yukarıdaki koşul sağlanmadan "pthread_cond_broadcast" veya "pthread_cond_signal" fonksiyonlarına çağrı yapılırsa bloke yine kalkacak ve akış yine "pthread_mutex_lock" fonksiyonuna girecektir. Burada yine ilgili "mutex" nesnesi başka "thread" tarafından kilitlenmişse, varsayılan durumda, kilidin kalkmasını bekleyecektir. Kilit kalktığında da bizim "thread" ilgili "mutex" nesnesini kilitleyecek ve "thread" in akışı "pthread_cond_wait" fonksiyonundan çıkacaktır. Fakat KOŞUL SAĞLANMADIĞI İÇİN "thread" İN AKIŞI TEKRARDAN "pthread_cond_wait" FONKSİYONUNA GİRECEKTİR. -> Artık "thread" in akışı "Kritik Kod" bölgesine geçecektir. Buradaki en önemli nokta ilgili koşulun sağlanmasından sonra "pthread_cond_broadcast" veya "pthread_cond_signal" fonksiyonlarına çağrının yapılmasıdır. Eğer ilgili "mutex" nesnesi kilitlenebilir durumda ise yeniden bizim "thread" tarafından kilitlenecektir. Bu kilit ise en sonunda da "pthread_mutex_unlock" tarafından açılacaktır. Burada "pthread_mutex_lock" ve "pthread_mutex_unlock" fonksiyonlarına yapılan çağrının kesin sonuçları için ilgili fonksiyonların dökümanına bakmalıyız. Diğer "thread" ler bahsi geçen koşulu sağlamak ve uykudan uyandırmak için aşağıdaki kalıbı izleyebilir: * Örnek 1, //... pthread_mutex_lock(&g_mutex); g_flag = 1; /* Diğer "thread" in koşulu uygun hale gerekmektedir. */ pthread_mutex_unlock(&g_mutex); //... pthread_cond_signal(&g_cond); /* Daha sonra bloke edilen ilgili "thread" i uyandırması. */ //... > Hatırlatıcı Notlar: >> Türkçe'nin Etimoloji Sözlüğü: https://www.nisanyansozluk.com/ >> "quanta" süresi biten "thread" tekrardan "Run Queue" sırasına alınır. Eğer koşul sağlanmadığı için bloke edilmesi veya herhangi bir sebepten dolayı bloke edilmesi durumunda "Wait Queue" sırasına alınır. Şartların sağlanmasından sonra tekrardan "Run Queue" sırasına alınır. /*================================================================================================================================*/ (56_03_06_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. Ancak ilk "thread" bu bölgeden çıktıktan sonra diğer "thread" in girmesine izin verilmektedir. >>>> "Kritik Kod" (devam): >>>>> "Conditional Variables" : >>>>>> Koşul nesnesinin kullanımı bittikten sonra "pthread_cond_destroy" fonksiyonu ile boşaltılmalıdır. Fonksiyonun prototipi şu şekildedir: #include int pthread_cond_destroy(pthread_cond_t *cond); Fonksiyon parametre olarak koşul değişken nesnesinin adresini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönecektir. * Örnek 1, Aşağıdaki örnekte ilk olarak "Thread-1" çalışmaya başlayacaktır. #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER; int g_count = 0; int main(int argc, char** argv) { /* # OUTPUT # Thread-1 will handle the condition. Press ENTER to fix it!... Thread-2 may wait at the condition variable... Thread-2 will be locked... Thread-2 is waiting now... The condition is being established!... Thread-1 is unlocked... Thread-2 is unlocked... */ pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if((result = pthread_cond_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_cond_destroy", result); return 0; } void* thread_proc1(void* param) { puts("Thread-1 will handle the condition. Press ENTER to fix it!..."); getchar(); int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); /* * Bu örnek için bu kodun kritik bölgeye alınmasına lüzum yoktur. * Çünkü sadece bir "thread" bu değişkeni değiştirmektedir. */ g_count = 1; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); /* * Aşağıdaki "signal" çağrısından evvel koşulun sağlanmış olması * gerekmektedir. Aksi halde ilgili "thread" tekrar uykuya dalacaktır. */ if((result = pthread_cond_signal(&g_cond)) != 0) exit_sys_errno("pthread_cond_signal", result); if(g_count == 1) puts("The condition is being established!..."); puts("Thread-1 is unlocked..."); } void* thread_proc2(void* param) { puts("Thread-2 may wait at the condition variable..."); int result; if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); puts("Thread-2 will be locked..."); while(g_count != 1) { puts("Thread-2 is waiting now..."); if((result = pthread_cond_wait(&g_cond, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); } if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); puts("Thread-2 is unlocked..."); return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Koşul değişkenlerinin kullanımına ilişkin bazı ayrıntılar da yok değildir. Şöyleki: >>>>>> "pthread_cond_signal" veya "pthread_cond_broadcast" fonksiyonlarına hiç çağrı yapılmamasına rağmen, bazı içsel nedenlerden dolayı, uykuya alınan "thread" ler kendiliğinden uyanmaktadır. Bu duruma "Spurious Wakeup" denmektedir. Döngü kullandığımız için ve koşul da sağlanmadığı için uyanan bu "thread" ler tekrar uyutulur. >>>>>> Anımsanacağınız üzere "pthread_cond_signal" fonksiyonu en az bir adet "thread" i uyandırmaktadır. Fakat bazen birden fazla "thread" de uyandırılmaktadır. Varsayalım iki adet "thread" uyandı. Böylesi bir durumda uyanan "thread" lerden birisi "pthread_cond_wait" fonksiyonundan çıkmadan evvel ilgili "mutex" nesnesini kilitleyecektir. Diğer ikinci "thread" ise bu aşamada ilgili "mutex" nesnesini beklemeye başlayacaktır. Sonrasında da ilgili "mutex" nesnesini kilitleyen "thread", "pthread_cond_wait" fonksiyonundan çıkıp ilgili Kritik Kod bölgesine geçecektir. En sonunda da ilgili "mutex" nesnesinin kilini tekrar kaldıracaktır. Bu noktada da "pthread_cond_wait" içerisinde bekleyen ikinci "thread" ilgili "mutex" nesnesini kilitleyip "pthread_cond_wait" fonksiyonundan çıkacak ve ilgili Kritik Kod bölgesine geçecektir. En sonunda da o da ilgili "mutex" nesnesinin kilidi kaldıracaktır. Bu durumunu engellemek ve ilgili "thread" leri teker teker uyandırıp "pthread_cond_wait" fonksiyonundan çıkmasını istiyorsak, ilgili Kritik Kod bölgesine koşulu eski haline getirecek bir kod parçacığı eklemeliyiz. Böylelikle koşul tekrar bozulacağı için, ekstradan uyanan diğer "thread" ler tekrar uykuya dalacaktır. >>>>>> Öte yandan birden fazla "thread", "pthread_cond_signal" ve "pthread_cond_broadcast" fonksiyonlarına çağrı yapar olsun. Koşul değişkenini bekleyen birden fazla "thread" uyanacaktır. Fakat bunlardan bir tanesi ilgili "mutex" nesnesinin sahipliğini alacak ve "pthread_cond_wait" fonksiyonundan çıkacaktır. Uyanan diğer "thread" ler ise "pthread_cond_wait" içerisinde ilgili "mutex" nesnesinin kilidinin açılmasını bekleyecektir. Eğer döngü kullanmazsak, ilgili "mutex" nesnesi boşa çıktıktan sonra, bekleyen bu "thread" lerden birisi de "pthread_cond_wait" fonksiyonundan çıkıp Kritip Kod bölgesine girecektir. Eğer Kritik Kod bölgesinde tüketilecek bir kaynak var ise ve bu kaynaklar da "pthread_cond_wait" fonksiyonundan çıkan ilk "thread" tarafından tüketilmişse, bir sorun meydana gelebilir. Bu duruma ise "Üretici-Tüketici Problemleri" denmektedir. >>>>>>> "Üretici-Tüketici Problemi" : En çok karşılaşılan senkronizasyon problemidir. Aynı prosesin "thread" lerin arasında karşımıza çıkabileceği gibi farklı proseslerin "thread" leri arasında da karşımıza çıkabilmektedir. Bu problemde en az bir üretici "thread" ve en az bir tüketici "thread" vardır. Üretici "thread", bir takım işlemlerden sonra bir değer üretir ve bu değerin işlenmesi için de bu değeri tüketici "thread" e havale eder. Buradaki havale edilme işlemi paylaşılan bir alan vesilesi ile gerçekleştirilir. Örneğin, prosesler arasında kullanılacak ise Paylaşılan Bellek Alanları, aynı proses içerisinde kullanılacak ise "global" isim alanı kullanılır. Yani üretici "thread" bir değer üretip bunu paylaşılan alana yazmak, tüketici "thread" ise bu alana yazılan değeri alarak işlemektir. Buradaki önemli husus üretici "thread" in ilgili paylaşılan alandaki değeri ezmemesi, yani üzerine yazmaması, gerekirken tüketici "thread" in aynı değeri iki defa alıp işlememesi gerekmektedir. Eğer bu iki "thread" arasında bir hız farkı olursa ya aynı değer iki defa işlenecek ya da önceki değerin üzerine yeni değer yazılacağı için değer kaçırılacaktır. Bu problemin amacı ise hız kazanmaktır. Koordineli bir biçimde, bir yandan üretim yapılırken bir yandan da tüketim yapılmasıdır. Eğer eş zamanlı olarak bu işlemler yapılmasaydı, yani seri bir biçimde yapılsaydı, ortada bir gecikme oluşacaktır. Eğer tek bir CPU da olsa birden fazla "thread" kullanılması yine hız kazandıracaktır. Öte yandan bu problemde iki "thread" arasında kullanılan paylaşılan alan bir "FIFO" kuyruk sistemi olursa, üretici ve tüketici "thread" ler birbirlerini daha az bekleyecektir. Kuyruk tam dolduğunda tüketici beklerken, kuyruk tam boşaldığında tüketici bekleyecektir. Bir diğer yandan bu problemde kullanılan "thread" ler birden fazla da olabilmektedir. Yani çok üretici ve çok tüketici aynı paylaşılan alanı kullanmaktadır. Bu problem ile gerçek hayatta çok yerde karşılaşılmaktadır. Örneğin, "Client-Server" uygulamalar. Birden fazla "Client" üretim yaparken, birden fazla "Server" tüketim yapmaktadır. * Örnek 1, Aşağıdaki örnekte iki proses arasında koordinasyon sağlanmadığı için bazı değerler kaçırılmış, bazı değerler iki defa işlenmiştşir. #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int g_shared; int main(int argc, char** argv) { /* # OUTPUT # 0 0 1 2 3 4 7 8 8 9 10 10 12 13 14 15 16 19 20 21 22 24 24 25 */ pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int val = 0; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ g_shared = val; if(val == 25) break; ++val; } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int val; for(;;) { val = g_shared; usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == 25) break; } puts(""); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } "Üretici-Tüketici Problemleri" tipik olarak semaforlar ve koşul değişkenleri kullanılarak çözülmektedir. Şimdilik koşul değişkenleri ile kullanımı üzerinde durup, semaforlar ile birlikte kullanımına ileride değineceğiz. Koşul değişkenleri ile bu problemin çözümüne ilişkin kodlar kabaca aşağıdaki gibidir: //... int g_flag = 0; pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER; pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER; //... /* "Producer thread" */ for(;;) { /* Bir değer üretilir. */ pthread_mutex_lock(&g_mutex); while(g_flag == 1) pthread_cond_wait(&cond_producer, &g_mutex); /* Üretilen değer paylaşılan alana yerleştirilir. */ g_flag = 1; pthread_mutex_unlock(&g_mutex); pthread_cond_signal(cond_consumer); } /* "Consumer thread" */ for(;;) { pthread_mutex_lock(&g_mutex); while(g_flag == 0) pthread_cond_wait(&cond_consumer, &g_mutex); /* Paylaşılan alandaki değer alınır. */ g_flag = 0; pthread_mutex_unlock(&g_mutex); pthread_cond_signal(cond_producer); } >>>>>> "pthread_cond_signal" ve "pthread_cond_broadcast" isimli fonksiyonlar, o an uykuda olan "thread" leri uyandırırlar. Yani bu fonksiyonlar çağrıldıdığında uyuyan "thread" yok ise boş yere çağrılmış olacaktır. >>>>>> Öte yandan "pthread_cond_signal" ve "pthread_cond_broadcast" isimli fonksiyonlar ile bir "thread" uyansa fakat kilitlenecek "mutex" nesnesi halihazırda kilitliyse, uyanan bu "thread" ilgili "mutex" nesnesinin kilidi açılana kadar bloke edilir. Kilit kaldırılınca uyanan bu "thread" ilgili "mutex" nesnesinin sahipliğini alır ve "pthread_cond_wait" fonksiyonundan çıkar. Dolayısıyla "pthread_cond_signal" ve "pthread_cond_broadcast" fonksiyonlarını ilgili "mutex" nesnesi kilitlenebilir durumdayken çağrılması gerekmektedir. /*================================================================================================================================*/ (57_04_06_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. Ancak ilk "thread" bu bölgeden çıktıktan sonra diğer "thread" in girmesine izin verilmektedir. >>>> "Kritik Kod" (devam): >>>>> "Conditional Variables" : >>>>>> Koşul değişkenlerinin kullanımına ilişkin bazı hususlar (devam): >>>>>>> "Üretici-Tüketici Problemi" (devam): Aşağıdaki örnekte iki proses arasında koordinasyon sağlandığı için artık hiç değer kaçırılmamıştır. Buradaki koordinasyon koşul değişkenleri ile sağlanmıştır. Kritik Kod bölgesinin kısa tutulması, sadece gerekli kısımların içeri alınması iyi bir tekniktir. * Örnek 1, Aşağıdaki örnekte iki "thread" için oluşturulan ortak alan bir nesnedir. #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); pthread_cond_t g_cond_producer; pthread_cond_t g_cond_consumer; pthread_mutex_t g_mutex; int g_flag = 0; int g_shared; int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 */ int result; if((result = pthread_cond_init(&g_cond_producer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_cond_init(&g_cond_consumer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int val = 0; int result; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(g_flag == 1) if((result = pthread_cond_wait(&g_cond_producer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); g_shared = val; g_flag = 1; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if(val == 25) break; ++val; } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int val; int result; for(;;) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(g_flag == 0) if((result = pthread_cond_wait(&g_cond_consumer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = g_shared; g_flag = 0; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == 25) break; } puts(""); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte iki "thread" için oluşturulan ortak alan bir kuyruk sisetmidir. Böylelikle üretici "thread" kuyruk tam doluyken, tüketici "thread" ise kuyruk tam boşken bekleyecektir. Bu da bekleme olasılıklarını düşürecektir. Burada kullanılan kuyruk sistemi yine çeşitli biçimlerde gerçekleştirilebilir. En çok kullanılan yöntem, "circual queue" de denilen, Döngüsel Kuyruk Sistemidir. Böylesi bir sistemde bir dizi oluşturulur. Daha sonra "head" ve "tail" diyeceğimiz iki adet indeks ya da gösterici ilgili dizinin sırasıyla başını ve sonunu tutar. Kuyruğa eleman eklenirken "tail", kuyruktan eleman alınırken "head" isimli indeks/gösterici ötelenir. Eğer "head" ve "tail" ilgili dizinin sonuna gelirse, dizinin tekrar başına alınır. Böylelikle sanki bir "ring" sistemi varmış gibi gözükür. Son olarak dizideki eleman sayısını da yine bir sayaçta tutarak dizideki toplam eleman sayısının bilgisi saklanır. #include #include #include #include #include #include #define QUEUE_SIZE 25 void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); pthread_cond_t g_cond_producer; pthread_cond_t g_cond_consumer; pthread_mutex_t g_mutex; int g_queue[QUEUE_SIZE]; int g_head = 0; int g_tail = 0; int g_count = 0; int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 */ int result; if((result = pthread_cond_init(&g_cond_producer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_cond_init(&g_cond_consumer, NULL)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int val = 0; int result; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(g_count == QUEUE_SIZE) /* Kuyruk tam dolduğunda üretici bekleyecektir. */ if((result = pthread_cond_wait(&g_cond_producer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); g_queue[g_tail++] = val; /* * Eğer "g_tail" in değeri 'g_tail % QUEUE_SIZE' işleminin sonucudur. * Böylelikle dizinin sonuna geldikten sonra tekrardan dizinin başına geçiyoruz. */ g_tail %= QUEUE_SIZE; ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&g_cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if(val == 25) break; ++val; } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int val; int result; for(;;) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(g_count == 0) /* Kuyruk tam boşken tüketici bekleyecektir. */ if((result = pthread_cond_wait(&g_cond_consumer, &g_mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = g_queue[g_head++]; /* * Eğer "g_head" in değeri 'g_head % QUEUE_SIZE' işleminin sonucudur. * Böylelikle dizinin sonuna geldikten sonra tekrardan dizinin başına geçiyoruz. */ g_head %= QUEUE_SIZE; --g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&g_cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == 25) break; } puts(""); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Koşul değişkenlerinin zaman aşımlı beklemeye yol açan bir başka biçimi daha vardır. Bu, "pthread_cond_timedwait" isimli fonksiyon ile gerçekleştirilmektedir. Belli bir zaman aşımı dolunca ilgili koşul değişkeni otomatik olarak açılır. İş bu fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_cond_timedwait(pthread_cond_t * cond, pthread_mutex_t * mutex, const struct timespec * abstime); Yine burada üçüncü parametre ile belirtilen zaman aşımı değeri MUTLAKTIR, GÖRELİ DEĞİLDİR. Yine bu fonksiyon da atomik bir biçimde ilgili "mutex" nesnesinin sahipliğini bırakır ve çıkışta yine "mutex" in sahipliğini almaya çalışır. Yani buradaki bekleme süresi, belirtilen zaman kadar olacaktır. Dolayısıyla bu fonksiyon zaman aşımı gerçekleştiğinde "ETIMEDOUT" değerine geri dönmektedir. Aksi halde diğer hata kodlarına geri dönmektedir. Bu da demektir ki başarısızlık durumunda hata kodunun "ETIMEDOUT" olup olmadığı yine kontrol edilmelidir. Öte yandan koşul değişkenleri de özellik parametresine sahiptir, tıpkı "mutex" nesneleri gibi. Fakat koşul değişkenlerinin değiştirilebilir iki adet özelliği vardır. Yine bu özellikleri "set" etmek, "mutex" nesnelerinin özelliklerini "set" etmek gibidir. Yani aynı yöntem kullanılır. Şöyleki; -> İlk olarak "pthread_condattr_t" türünden bir özellik nesnesi bildirir. -> Daha sonra "pthread_condattr_init" ile bu nesneye ilk değerini verir. İlgili fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_condattr_init(pthread_condattr_t *attr); -> Daha sonra "pthread_condattr_setXXX" fonksiyonları ile ilgili özellik nesnesinin bazı özelliklerini "set" eder. -> Sonrasında da "pthread_cond_init" fonksiyonuna bu özellik nesnesinin adresi geçilir ki ilgili özelliklere bahsi geçen koşul değişkeni haiz olsun. -> Son olarak ilgili özellik nesnesinin "pthread_cond_init" fonksiyonundan sonra korunmasına gerek yoktur. Bu nedenle "pthread_condattr_destroy" fonksiyonu ile boşaltılabilir. Bu fonksiyonun da prototipi aşağıdaki gibidir: #include int pthread_condattr_destroy(pthread_condattr_t *attr); Pekiyi bizler hangi özellikleri "set" edebiliriz? İki adet özelliği "set" edebiliriz. >>>>>> Bunlardan bir tanesi ilgili koşul nesnesinin prosesler arasında kullanımını mümkün kılan özelliktir. Tabii ilgili koşul nesnesinin paylaşılan bellek alanında oluşturulması gerekmtekdir. Tabii bu durumda "mutex" nesnesinin de yine paylaşılan bellek alanında oluşturulması gerekmtekdir. Bu işlemler için "pthread_condattr_setpshared" fonksiyonu ile mümkündür. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared); Fonksiyonun ilk parametresi ilgili koşul nesnesine ait olan özellik nesnesinin adresini, ikinci parametre ise şu değerlerden birisini almaktadır; "PTHREAD_PROCESS_SHARED" ve "PTHREAD_PROCESS_PRIVATE". -> "PTHREAD_PROCESS_SHARED" : -> "PTHREAD_PROCESS_PRIVATE" : Varsayılan değer budur. * Örnek 1, Aşağıdaki örnekte "üretici-tüketici" problemi prosesler arasında işlenmiştir. Paylaşılan Bellek Alanı kullanılmıştır. Programlardan ilk önce "Producer", daha sonra "Consumer" isimli program çalıştırılmalıdır. /* sharing.h */ #ifndef PRODCONS_H_ #define PRODCONS_H_ #include #define QUEUE_SIZE 25 #define SHM_NAME "/producer-consumer" /* * Paylaşılan bellek alanına yazılacak nesne aşağıdaki nesnedir. */ typedef struct tagSHARED_INFO{ pthread_cond_t cond_producer; pthread_cond_t cond_consumer; pthread_mutex_t mutex; int head; int tail; int queue[QUEUE_SIZE]; int count; }SHARED_INFO; #endif /* Producer */ #include #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int result; pthread_mutexattr_t mattr; if((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if((result = pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_mutexattr_setpshared", result); if((result = pthread_mutex_init(&shmaddr->mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); pthread_condattr_t cattr; if((result = pthread_condattr_init(&cattr)) != 0) exit_sys_errno("pthread_condattr_init", result); if((result = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_condattr_setpshared", result); if((result = pthread_cond_init(&shmaddr->cond_producer, &cattr)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_cond_init(&shmaddr->cond_consumer, &cattr)) != 0) exit_sys_errno("pthread_cond_init", result); if((result = pthread_condattr_destroy(&cattr)) != 0) exit_sys_errno("pthread_condattr_destroy", result); shmaddr->count = 0; shmaddr->head = 0; shmaddr->tail = 0; int val = 0; for(;;) { usleep(rand() % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(shmaddr->count == QUEUE_SIZE) /* Kuyruk tam dolduğunda üretici bekleyecektir. */ if((result = pthread_cond_wait(&shmaddr->cond_producer, &shmaddr->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); shmaddr->queue[shmaddr->tail++] = val; /* * Eğer "g_tail" in değeri 'g_tail % QUEUE_SIZE' işleminin sonucudur. * Böylelikle dizinin sonuna geldikten sonra tekrardan dizinin başına geçiyoruz. */ shmaddr->tail %= QUEUE_SIZE; ++shmaddr->count; if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&shmaddr->cond_consumer)) != 0) exit_sys_errno("pthread_cond_signal", result); if(val == QUEUE_SIZE) break; ++val; } /* Alternatif - I */ // puts("Press ENTER to finish the communication!..."); getchar(); /* Alternatif - I */ /* Alternatif - II */ if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(shmaddr->count != 0) if((result = pthread_cond_wait(&shmaddr->cond_producer, &shmaddr->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); /* Alternatif - II */ if((result = pthread_cond_destroy(&shmaddr->cond_consumer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_cond_destroy(&shmaddr->cond_producer)) != 0) exit_sys_errno("pthread_cond_destroy", result); if((result = pthread_mutex_destroy(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int val; int result; for(;;) { usleep(rand() % 300000); if((result = pthread_mutex_lock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); while(shmaddr->count == 0) if((result = pthread_cond_wait(&shmaddr->cond_consumer, &shmaddr->mutex)) != 0) exit_sys_errno("pthread_cond_wait", result); val = shmaddr->queue[shmaddr->head++]; shmaddr->head %= QUEUE_SIZE; --shmaddr->count; printf("%d ", val); fflush(stdout); if((result = pthread_mutex_unlock(&shmaddr->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); if((result = pthread_cond_signal(&shmaddr->cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); if(val == QUEUE_SIZE) break; } puts(""); /* Alternatif - II */ if((result = pthread_cond_signal(&shmaddr->cond_producer)) != 0) exit_sys_errno("pthread_cond_signal", result); /* Alternatif - II */ if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>> Bir diğer "set" edilebilir özellik ise "pthread_cond_timdwait" fonksiyonunda zaman aşımında kullanılan saatin cinsidir. Bunun için "pthread_condattr_setclock" isimli fonksiyon kullanılır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_condattr_setclock(pthread_condattr_t *attr, clockid_t clock_id); Fonksiyonun ilk parametresi ilgili koşul nesnesine ait olan özellik nesnesinin adresini, ikinci parametre ise ilgili saat nesnesinin ID'sidir. Varsayılan değer "system clock" değeridir. >>>>> "Semafor" Nesneleri: Sayaçlı senkronizasyon nesneleridir. Bu nesnelerin amacı bir Kritik Kod bölgesine en fazla "n" tane "thread" in aynı anda girmesini sağlamaktır. İşte bünyelerindeki sayaç, bu "thread" lerin adedini tutmaktadır. Bu nesnelerin en önemli kullanım sebebi "n" tane kaynağı "k" adet kullanıcıya dağıtarak, kaynak paylaştırmak için kullanılır. "n" taneden daha fazla "thread" Kritik Kod bölgesine girmek isterse, bu "thread" ler blokede bekletilecektir. Burada "n" değerinin "1" olması durumunda ilgili semafor nesneleri "Binary Semaphore" olarak adlandırılır ve "mutex" nesneleri gibi bir etkiye sahiptir. Fakat "mutex" nesnelerinden farkı, sahiplik ile ilgilidir. Anımsanacağınız üzere "mutex" nesnesinin sahipliğini alan "thread" bu sahipliğini bırakabilir. Fakat semafor nesnelerinde böyle bir durum yoktur. Diğer "thread" ler içerideki iş bu sayaç değerini değiştirebilir. Bundan dolayıdır ki hataya açık nesneler olarak da görülebilir. Semafor nesneleri de bünyesinde iki farklı arayüz bulundurur. Bunlar "POSIX" ve "System 5" semafor nesneleridir. Tıpkı Paylaşılan Bellek Alanları ve Mesaj Kuyrukları gibi. Bundan dolayıdır ki semafor nesneleri de "IPC" konusu ile ilişkilidir. Buradaki arayüzlerden "System 5" arayüz oldukça kötü tasarlanmıştır. Fakat "POSIX" arayüzü ile bu durum düzeltilmiştir. Dolayısıyla ilk olarak "POSIX", daha sonra "System 5" arayüzü işlenecektir. Tavsiye edilen arayüz ise "POSIX" arayüzüdür. >>>>>> "POSIX" semafor nesneleri: İsimli ve isimsiz olarak iki farklı biçimdedir. İsimli olanlar prosesler arasında kullanıma daha uygunken, isimsiz olanlar aynı prosesin "thread" ler arasında kullanıma uygundur. Fakat yine farklı prosesler arasında da kullanılabilmektedir. >>>>>>> İsimsiz "POSIX" nesneleri: "semt_t" türü ile temsil edilmektedir. Herhangi bir türe karşılık gelebilir. Bu türden nesneleri kullanabilmek için aşağıdaki prosedürü takip etmeliyiz: -> "sem_t" türünden bir nesne bildiririz. -> "sem_init" isimli fonksiyon ile bu nesneye ilk değer veririz. Fonksiyonun prototipi aşağıdaki gibidir: #include int sem_init(sem_t *sem, int pshared, unsigned value); Fonksiyonun birinci parametresi "sem_t" türünden nesnenin adresini, ikinci parametre ise ilgili Semafor nesnesinin prosesler arasında paylaşılıp paylaşılamayacağı bilgisini alır. "0" değeri geçilmesi durumunda prosesler arasında PAYLAŞIMI OLMAYACAĞI, "non-zero" bir değer ise prosesler arasında PAYLAŞILACAĞINI belirtir. Üçüncü parametre ise semafor nesnesi içindeki sayacın değerini belirtir. Bu değer Kritik Kod bölgesine aynı anda en fazla kaç "thread" in gireceğini belirtir. Fonksiyon, başarı durumunda "0" ile başarısızlık durumunda "-1" ile geri döner ve "errno" değişkeni uygun değere çekilir. Son olarak her ne kadar isimsiz semafor nesneleri de prosesler arasında kullanılabilir olsalarda, isimli semafor nesneleri bu iş için genellikle daha uygundur. > Hatırlatıcı Notlar: >> "xlinux.nist.gov/dads/" isimli internet sitesi "Dictionary of Algorithms & Data Structures" için kullanılır. >> "foldoc.org" isimli internet sitesi de bilgisayar bilimlerinde sözlük olarak kullanılır. /*================================================================================================================================*/ (58_10_06_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. >>>> "Kritik Kod" (devam): >>>>> "Semafor" Nesneleri (devam): >>>>>> "POSIX" semafor nesneleri (devam): >>>>>>> İsimsiz "POSIX" nesneleri (devam): -> Daha sonra Kritik Kod bölgesi oluşturulur. Bu sefer "sem_wait" ve "sem_post" isimli fonksiyonlar kullanılır ve ilgili kod parçacıkları bu iki fonksiyon çağrısı arasında yazılır. Bu iki fonksiyon şu şekilde çalışmaktadır; diyelim ki semafor nesnesinin içerisindeki sayaç "3" olsun. Bir "thread" Kritik Kod alanına girerken akışı "sem_wait" fonksiyonundan da geçecektir. Bu durumda semafor nesnesinin sayacı bir azaltılacaktır. İkinci bir "thread" de aynı kritik kod bölgesine girmek istediğinde, onun da akışı yine "sem_wait" fonksiyonundan geçecektir ve semafor nesnesinin içindeki sayaç bir azaltılacaktır. Üçüncü bir "thread" de yine aynı kritik kod bölgesinden geçerken benzer durum yaşanacaktır. Fakat semafor nesnesinin sayacı "0" olduğundan dolayı dördüncü ve sonrasındaki "thread" ler blokede bekletilecektir. Benzer şekilde kritik kod bölgesinde işi biten "thread" lerin akışı da "sem_post" fonksiyonundan geçecektir ve semafor nesnesinin sayacı "1" olacaktır. İşte tam da bu anda dışarıdaki blokede bekleyen "thread" lerden birisi kritik kod bölgesine girecektir. Onun da akışı "sem_wait" içerisinden geçtiği için semafor nesnesi içindeki sayacın değeri tekrar "0" olacaktır. Bir nevi berber koltuğu bekleyen müşterileri bu duruma benzetebiliriz. Dolayısıyla belli bir kaynağın belli "thread" lere paylaştırılması mottosu güdülmüştür. İlgili fonksiyonların prototipi aşağıdaki gibidir: #include int sem_wait(sem_t *sem); int sem_post(sem_t *sem); Fonksiyonlar argüman olarak ilgili semafor nesnesinin adresini alırlar. Başarı durumunda "0", hata durumunda ise "-1" değerine geri dönerler ve "errno" değişkeni uygun değere çekilir. Son olarak tekrar hatırlatmakta fayda vardır ki "sem_wait" fonksiyonuna girmek için blokede bekleyen "thread" lerden hangisinin ilk önce gireceğine dair bir garanti POSIX standartlarınca verilmemiştir. Burada "sem_post" yapmak için "sem_wait" yapmaya GEREK YOKTUR. "sem_wait" fonksiyonunun birde "sem_timedwait" isimli zaman aşımlı bir versiyonu daha vardır. Bu versionda, semafor nesnesinde bloke olunmuşsa, zaman aşımı dolduğu vakit bloke kaldırılmaktadır. Fonksiyonun prototipi şöyledir; #include #include int sem_timedwait(sem_t * sem, const struct timespec * abstime); Ancak buradaki zaman aşımı göreli değil, mutlak zamanı belirtmektedir. Fonksiyon eğer zaman aşımından dolayı başarısız olmuşsa "-1" ile geri döner ve "errno" değişkeni "ETIMEDOUT" değerini alır. -> Semafor nesnesinin kullanımı bittikten sonra "sem_destroy" ile boşa çıkartılmalıdır. Fonksiyonun prototipi şu şekildedir: #include int sem_destroy(sem_t *sem); Fonksiyonlar argüman olarak ilgili semafor nesnesinin adresini alırlar. Başarı durumunda "0", hata durumunda ise "-1" değerine geri dönerler ve "errno" değişkeni uygun değere çekilir. * Örnek 1, Aşağıdaki semafor nesnesinin kullanımına bir örnek verilmiştir. #include #include #include #include #include #include #include void* thread_1(void* param); void* thread_2(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); #define SIZE 10 sem_t g_sem; int g_count; int main(int argc, char** argv) { /* # OUTPUT # 20 */ int result; pthread_t tid1, tid2; if((sem_init(&g_sem, 0, 1)) == -1) exit_sys("sem_init"); if((result = pthread_create(&tid1, NULL, thread_1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); printf("%d\n", g_count); if(sem_destroy(&g_sem) == -1) exit_sys("sem_destroy"); return 0; } void* thread_1(void* param) { for(int i = 0; i < SIZE; ++i) { if(sem_wait(&g_sem) == -1) exit_sys("sem_wait"); ++g_count; if(sem_post(&g_sem) == -1) exit_sys("sem_post"); } return NULL; } void* thread_2(void* param) { for(int i = 0; i < SIZE; ++i) { if(sem_wait(&g_sem) == -1) exit_sys("sem_wait"); ++g_count; if(sem_post(&g_sem) == -1) exit_sys("sem_post"); } return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte birden fazla "thread", "3" adet kaynağa atanmıştır. Böylelikle aynı anda en fazla üç adet "thread" in çalıştığı, diğer "thread" lerin ise uykuda beklediği gözlemlenmektedir. #include #include #include #include #include #include #include #define EPOCH 10 #define NTHREADS 10 #define NRESOURCES 3 typedef struct tagTHREAD_INFO{ unsigned int seed; pthread_t tid; char name[32]; }THREAD_INFO; typedef struct tagRESOURCES{ int useflags[NRESOURCES]; pthread_mutex_t mutex; sem_t sem; }RESOURCES; void assign_resource(THREAD_INFO* ti); void do_with_resources(THREAD_INFO* ti, int nresource); void* thread_1(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); RESOURCES g_resources; int main(int argc, char** argv) { /* # OUTPUT # */ THREAD_INFO* threads_info[NTHREADS]; srand(time(NULL)); for(int i = 0; i < NRESOURCES; ++i) g_resources.useflags[i] = 0; if((sem_init(&g_resources.sem, 0, NRESOURCES)) == -1) exit_sys("sem_init"); int result; if((result = pthread_mutex_init(&g_resources.mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); for(int i = 0; i < NTHREADS; ++i) { if((threads_info[i] = (THREAD_INFO*)malloc(sizeof(THREAD_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } snprintf(threads_info[i]->name, 32, "Thread-%2d", i + 1); threads_info[i]->seed = rand(); if((result = pthread_create(&threads_info[i]->tid, NULL, thread_1, threads_info[i])) != 0) exit_sys_errno("pthread_create", result); } for(int i = 0; i < NTHREADS; ++i){ if((result = pthread_join(threads_info[i]->tid, NULL)) != 0) exit_sys_errno("pthread_join", result); free(threads_info[i]); } if((result = pthread_mutex_destroy(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if(sem_destroy(&g_resources.sem) == -1) exit_sys("sem_destroy"); return 0; } void assign_resource(THREAD_INFO* ti) { /* En fazla 'n' tane "thread" in Kritik Kod bölgesine girmesine olanak sağlıyor. */ if(sem_wait(&g_resources.sem) == -1) exit_sys("sem_wait"); /* * Buradaki "mutex" nesnesi ile "semafor" nesnesinin bir arada kullanılmasının * yegane amacı kaynak bittiğinde diğer "thread" lerin uykuda bekletilmesidir. */ int result; if((result = pthread_mutex_lock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); int i; for(i = 0; i < NRESOURCES; ++i) { if(!g_resources.useflags[i]) { g_resources.useflags[i] = 1; break; } } if((result = pthread_mutex_unlock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("[%s] is acquired resources: [%d]...\n", ti->name, i + 1); do_with_resources(ti, i + 1); usleep(rand_r(&ti->seed) % 50000); if((result = pthread_mutex_lock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); g_resources.useflags[i] = 0; if((result = pthread_mutex_unlock(&g_resources.mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); printf("[%s] is released resources: [%d]...\n", ti->name, i + 1); if(sem_post(&g_resources.sem) == -1) exit_sys("sem_post"); usleep(rand_r(&ti->seed) % 1000); } void do_with_resources(THREAD_INFO* ti, int nresource) { printf("[%s] is doing something with resource: [%d]...\n", ti->name, nresource); } void* thread_1(void* param) { THREAD_INFO* ti = (THREAD_INFO*) param; for(int i = 0; i < EPOCH; ++i) assign_resource(ti); return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>>> "Üretici-Tületici" Problemlerinin semafor nesneleri ile çözümü: Anımsanacağınız üzere bu problemi Koşul Değişkenleri ile çözmüştük. Fakat semafor nesneleri ile daha kolay bir şekilde çözebiliriz. Yine aynı mantık ile hareket edilmektedir; yani iki "thread" birbirini uyandırmaktadır. /*================================================================================================================================*/ (59_11_06_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. >>>> "Kritik Kod" (devam): >>>>> "Semafor" Nesneleri (devam): >>>>>> "POSIX" semafor nesneleri (devam): >>>>>>> "Üretici-Tüketici" Problemlerinin semafor nesneleri ile çözümü (devam): Aşağıda bu problemin semafor nesneleri kullanılarak çözümü mevcuttur. Bu çözümde aynı prosesin "thread" leri kullanılmıştır. * Örnek 1, Aşağıdaki yaklaşımda kuyruk sistemi yerine bir nesne kullanılmıştır. #include #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); sem_t g_sem_producer; sem_t g_sem_consumer; int g_shared; int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 */ if((sem_init(&g_sem_producer, 0, 1)) == -1) exit_sys("sem_init"); if((sem_init(&g_sem_consumer, 0, 0)) == -1) exit_sys("sem_init"); int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if(sem_destroy(&g_sem_consumer) == -1) exit_sys("sem_destroy"); if(sem_destroy(&g_sem_producer) == -1) exit_sys("sem_destroy"); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int val = 0; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if(sem_wait(&g_sem_producer) == -1) exit_sys("sem_wait"); g_shared = val; if(sem_post(&g_sem_consumer) == -1) exit_sys("sem_post"); if(val == 25) break; ++val; } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int val; for(;;) { if(sem_wait(&g_sem_consumer) == -1) exit_sys("sem_wait"); val = g_shared; if(sem_post(&g_sem_producer) == -1) exit_sys("sem_post"); usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == 25) break; } puts(""); return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Şimdi de aynı problemin kuyruk sistemi kullanılarak çözümü gerçekleştirilmiştir. Burada dikkat edilmesi gereken üreteci için kullanılacak semafor nesnesinin sayacı, kuyruk uzunluğunda olması gerekmekte. Yine tüketici çalışmadığı zaman üretici kuyruk uzunluğu kadar çalışacaktır. Benzer biçimde üretici çalışmadığı zaman ise tüketici kuyruktakilerin hepsini aldıktan sonra beklemeye geçecektir. İlgili programı derlerken "gcc sample.c syncqueue.c -o sample -lpthread" şeklinde komut çalıştırmalıyız. /* syncqueue.h */ #ifndef SYNCQUEUE_H_ #define SYNCQUEUE_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagSYNC_QUEUE{ DATATYPE* queue; size_t size; size_t head; size_t tail; sem_t sem_producer; sem_t sem_consumer; }SYNC_QUEUE; /* Function Prototypes */ SYNC_QUEUE* init_syncqueue(size_t size); DATATYPE populate_syncqueue(SYNC_QUEUE* sq, DATATYPE value); DATATYPE depopulate_syncqueue(SYNC_QUEUE* sq, DATATYPE* value); DATATYPE destroy_syncqueue(SYNC_QUEUE* sq); #endif /* syncqueue.c */ #include #include "syncqueue.h" /* Function Definitions */ SYNC_QUEUE* init_syncqueue(size_t size) { SYNC_QUEUE* sq; if((sq = (SYNC_QUEUE*)malloc(sizeof(SYNC_QUEUE))) == NULL) goto FAILED_I; if((sq->queue = (DATATYPE*)malloc(sizeof(DATATYPE) * size)) == NULL) goto FAILED_II; sq->size = size; sq->head = sq->tail = 0; if(sem_init(&sq->sem_producer, 0, size) == -1) goto FAILED_III; if(sem_init(&sq->sem_consumer, 0, 0) == -1) goto FAILED_III; return sq; FAILED_III: free(sq->queue); FAILED_II: free(sq); FAILED_I: return NULL; } DATATYPE populate_syncqueue(SYNC_QUEUE* sq, DATATYPE value) { if(sem_wait(&sq->sem_producer) == -1) return -1; sq->queue[sq->tail++] = value; sq->tail %= sq->size; if(sem_post(&sq->sem_consumer) == -1) return -1; return 0; } DATATYPE depopulate_syncqueue(SYNC_QUEUE* sq, DATATYPE* value) { if(sem_wait(&sq->sem_consumer) == -1) return -1; *value = sq->queue[sq->head++]; sq->head %= sq->size; if(sem_post(&sq->sem_producer) == -1) return -1; return 0; } DATATYPE destroy_syncqueue(SYNC_QUEUE* sq) { if(sem_destroy(&sq->sem_producer) == -1) return -1; if(sem_destroy(&sq->sem_consumer) == -1) return -1; free(sq->queue); free(sq); return 0; } /* sample.c */ #include #include #include #include #include #include #include "syncqueue.h" #define EPOCH 100 void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 */ SYNC_QUEUE* sq; if((sq = init_syncqueue(EPOCH)) == NULL) exit_sys("init_syncqueue"); pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_producer, sq)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, sq)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if(destroy_syncqueue(sq) == -1) exit_sys("destroy_syncqueue"); return 0; } void* thread_producer(void* param) { SYNC_QUEUE* sq = (SYNC_QUEUE*)param; unsigned int seed = time(NULL) + 123; int val = 0; for(;;) { usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if((populate_syncqueue(sq, val)) == -1) exit_sys("populate_syncqueue"); if(val == EPOCH) break; ++val; } return NULL; } void* thread_consumer(void* param) { SYNC_QUEUE* sq = (SYNC_QUEUE*)param; unsigned int seed = time(NULL) + 456; int val; for(;;) { if((depopulate_syncqueue(sq, &val)) == -1) exit_sys("depopulate_syncqueue"); usleep(rand_r(&seed) % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ printf("%d ", val); fflush(stdout); if(val == EPOCH) break; } puts(""); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 3, Şimdi de "Üretici-Tüketici" problemini semaforlar kullanarak farklı prosesler arasındaki çözümünü gerçekleştirelim. Fakat çalıştırırken ilk önce "Producer", sonra "Consumer" programını çalıştırmalıyız. /* sharing.h */ #ifndef SHARING_H #define SHARING_H #include #include #define QUEUE_SIZE 100 #define SHM_NAME "/producer-consumer" /* * Paylaşılan bellek alanına yazılacak nesne aşağıdaki nesnedir. */ typedef struct tagSHARED_INFO{ sem_t sem_producer; sem_t sem_consumer; int head; int tail; int queue[QUEUE_SIZE]; }SHARED_INFO; #endif /* Producer */ #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); if(sem_init(&shmaddr->sem_producer, 1, QUEUE_SIZE) == -1) exit_sys("sem_init"); if(sem_init(&shmaddr->sem_consumer, 0, QUEUE_SIZE) == -1) exit_sys("sem_init"); shmaddr->head = 0; shmaddr->tail = 0; int val = 0; for(;;) { usleep(rand() % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if(sem_wait(&shmaddr->sem_producer) == -1) exit_sys("sem_wait"); shmaddr->queue[shmaddr->tail++] = val; shmaddr->tail %= QUEUE_SIZE; if(sem_post(&shmaddr->sem_consumer) == -1) exit_sys("sem_wait"); if(val == QUEUE_SIZE) break; ++val; } puts("Press ENTER to continue..."); getchar(); if(sem_destroy(&shmaddr->sem_consumer) == -1) exit_sys("sem_destroy"); if(sem_destroy(&shmaddr->sem_producer) == -1) exit_sys("sem_destroy"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 */ srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); int val = 0; for(;;) { if(sem_wait(&shmaddr->sem_consumer) == -1) exit_sys("sem_wait"); val = shmaddr->queue[shmaddr->head++]; shmaddr->head %= QUEUE_SIZE; if(sem_post(&shmaddr->sem_producer) == -1) exit_sys("sem_post"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if(val == QUEUE_SIZE) break; } puts(""); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>>> İsimli Semafor Nesneleri: Daha önce işlenilen "POSIX" Paylaşılan Bellek Alanları ve "POSIX" Mesaj Kuyruklarına benzer biçimde kullanılmaktadır. Kullanım adımları ise şu şekildedir; -> İlk önce "sem_open" fonksiyonu ile isimli bir semafor nesnesi oluşturulur. Eğer aynı isimde bir semafor nesnesi var ise o açılır. Fonksiyonun prototipi şu şekildedir: #include sem_t *sem_open(const char *name, int oflag, ...); Fonksiyonun birinci parametresi prosesler arasında kullanım için gereken bir isimdir. Bu isim yine diğer "POSIX IPC" nesnelerinde olduğu gibi kök dizinde bir isim olmalıdır. İkinci parametre ise açık modlarını belirtmektedir. Bu parametre şu bayraklardan birisini almaktadır: "O_CREAT" ve "O_EXCL". Burada "O_CREAT" bayrağının kullanılması durumunda ilgili isimde bir nesne yok ise yeni bir tane oluşturulur. Aksi halde var olan açılır. "O_EXCL" bayrağı ise "O_CREAT" ile birlikte kullanılır ve ilgili isimde bir nesne varsa fonksiyon başarısız olacaktır. Aksi halde sıfırdan bir nesne oluşturulur. Öte yandan semafor nesnelerine yazma veya okuma yapmak gibi bir kavram POSIX standartlarında mevcut değildir. Dolayısıyla açış modu olarak "O_RDONLY", "O_WRONLY" veya "O_RDWR" gibi bayraklar kullanılmaz. Bu durumda halihazırdaki bir semafor nesnesi açılacaksa, bu ikinci parametreye "0" geçilebilir. Eğer yeni bir tane oluşturulacaksa, iki parametre daha bu fonksiyona geçilmelidir. Üçüncü parametre erişim hakları içinken, dördüncü parametre ise semafor nesnesi içerisindeki sayacı belirtmektedir. Burada dikkat etmemiz gereken şey, bu üçüncü parametreye geçeceğimiz "access" bilgilerinin prosesin "umask" değerinden etkilenir olmasıdır. Son olarak üçüncü parametreye geçilen erişim haklarında hem "read" hem de "write" hakkının bulunuyor olması gerekmektedir. Her ne kadar POSIX bu konuda bir şey söylememiş olsa bile Linux standartlarınca bu şekildedir. Buradan hareketre diyebiliriz ki, eğer yeni bir semafor nesnesi oluşturulacaksa, fonksiyonun prototipi aşağıdaki gibidir: #include sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); Fonksiyonun geri dönüş değer ise başarı durumunda yeni oluşturulan semafor nesnesinin adresine, başarısızlık durumunda ise hata kodunun("SEM_FAILED") kendisine dönmektedir. Yine "errno" değişkeni de uygun değere çekilir. -> Artık Kritik Kod bölgesi tıpkı isimsiz semafor nesnelerinde olduğu gibi "sem_wait" ve "sem_post" fonksiyonlarının arasına yazılması gerekmektedir. -> Semafor nesnesinin kullanımı bittikten sonra "sem_close" fonksiyonu ile boşaltılmalıdır. Fonksiyonun prototipi de şu şekildedir: #include int sem_close(sem_t *sem); Fonksiyon ilgili semafor nesnesinin adresini alır. Başarı durumunda "0", hata durumudna "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. -> Son olarak isimli bu semafor nesnesi "sem_unlink" fonksiyonu ile yok edilmelidir. Eğer nesne yok edilmezse "kernel Persistance" bir biçimde ilk "reboot" edilene kadar sistemde kalmaya devam edecetir. Fonksiyonun prototipi aşağıdaki gibidir: #include int sem_unlink(const char *name); Fonksiyon semafor nesnesinin ismini parametre olarak alır. Başarı durumunda "0", hata durumunda "-1" ile geri döner ve "errno" değişkeni uygun değere çekilir. İsimli POSIX semafor nesneleri de tıpkı diğer "POSIX" IPC nesnelerinde olduğu gibi "/dev/shm" dizini içerisinden görüntülenmektedir. Fakat Linux sistemlerinde semafor nesnelerinin isimlerinin başlarına "sem." yazısı eklenerek saklanmaktadır. * Örnek 1, Aşağıda tüketici-üretici probleminin isimli semafor nesneleri kullanılarak çözümü belirtilmiştir. Yine ilk önce "Producer", daha sonra "Consumer" programı çalıştırılmalıdır. /* sharing.h */ #ifndef SHARING_H #define SHARING_H #include #include #define QUEUE_SIZE 100 #define SHM_NAME "/producer-consumer" #define SEM_PRODUCER_NAME "/producer-semaphore" #define SEM_CONSUMER_NAME "/consumer-semaphore" typedef struct tagSHARED_INFO{ int head; int tail; int queue[QUEUE_SIZE]; }SHARED_INFO; #endif /* Producer */ #include #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); /* Artık ilgili dosyanın büyüklüğü "4096" olmuştur. */ if(ftruncate(shmfd, SHM_SIZE) == -1) exit_sys("ftruncate"); sem_t* sem_producer; if((sem_producer = sem_open(SEM_PRODUCER_NAME, O_CREAT, S_IRUSR | S_IWUSR, QUEUE_SIZE)) == SEM_FAILED) exit_sys("sem_open"); sem_t* sem_consumer; if((sem_consumer = sem_open(SEM_CONSUMER_NAME, O_CREAT, S_IRUSR | S_IWUSR, 0)) == SEM_FAILED) exit_sys("sem_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); shmaddr->head = 0; shmaddr->tail = 0; int val = 0; for(;;) { usleep(rand() % 300000); /* "rand" fonksiyonu "thread-safe" DEĞİLDİR. */ if(sem_wait(sem_producer) == -1) exit_sys("sem_wait"); shmaddr->queue[shmaddr->tail++] = val; shmaddr->tail %= QUEUE_SIZE; if(sem_post(sem_consumer) == -1) exit_sys("sem_wait"); if(val == QUEUE_SIZE) break; ++val; } puts("Press ENTER to continue..."); getchar(); if(sem_close(sem_producer) == -1) exit_sys("sem_close"); if(sem_close(sem_consumer) == -1) exit_sys("sem_close"); if(sem_unlink(SEM_PRODUCER_NAME) == -1) exit_sys("shm_unlink"); if(sem_unlink(SEM_CONSUMER_NAME) == -1) exit_sys("shm_unlink"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); if(shm_unlink(SHM_NAME) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #include #include "sharing.h" #define SHM_SIZE sizeof(SHARED_INFO) void exit_sys(const char*); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 */ srand(time(NULL)); int shmfd; if((shmfd = shm_open(SHM_NAME, O_RDWR, 0)) == -1) exit_sys("shm_open"); SHARED_INFO* shmaddr; if((shmaddr = (SHARED_INFO*)mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shmfd, 0)) == MAP_FAILED) exit_sys("mmap"); sem_t* sem_producer; if((sem_producer = sem_open(SEM_PRODUCER_NAME, 0)) == SEM_FAILED) exit_sys("sem_open"); sem_t* sem_consumer; if((sem_consumer = sem_open(SEM_CONSUMER_NAME, 0)) == SEM_FAILED) exit_sys("sem_open"); int val = 0; for(;;) { if(sem_wait(sem_consumer) == -1) exit_sys("sem_wait"); val = shmaddr->queue[shmaddr->head++]; shmaddr->head %= QUEUE_SIZE; if(sem_post(sem_producer) == -1) exit_sys("sem_post"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if(val == QUEUE_SIZE) break; } puts(""); if(sem_close(sem_consumer) == -1) exit_sys("sem_close"); if(sem_close(sem_producer) == -1) exit_sys("sem_close"); if(munmap(shmaddr, SHM_SIZE) == -1) exit_sys("munmap"); close(shmfd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >>>>>> "System 5" semafor nesneleri: Yine burada izlenecek yol da diğer "System 5 IPC" nesnelerinde izlenen yol gibidir. Benzer arayüze sahiptirler. Fakat "System 5" semafor nesneleri "POSIX" semafor nesnelerine göre oldukça karışık bir arayüze sahiptir. Bu nedenle "POSIX" semafor nesnelerinin kullanılması tavsiye edilmektedir eğer özel nedenler yoksa. Öte yandan "System 5" semafor nesneleri aynı prosesin "thread" leri arasında değil, farklı prosesler arasında kullanım için düşünülmüştür. Zaten bunların tasarlandığı yıllarda henüz "thread" ler uygulamaya girmemiştir. Pekiyi "System 5" semafor nesnelerinin arayüzünü karışık yapan unsurlar nelerdir? Bu unsurlardan bir tanesi tek hamlede birden fazla semafor üzerinde işlem yapılmasıdır. Ayrıca sayaç mekanizması da biraz karışık ve çok işlevli olarak tasarlanmıştır. Bu tip semafor nesneleri karmaşık tasarımından dolayı gün geçtikte, "POSIX" versiyonlarına nazaran, daha az kullanılmaktadır. Şimdi bu tipin tipik kullanımına bakalım; -> Semafor nesnesi "semget" fonksiyonu ile (anımsarsanız diğer "System 5 IPC" nesneleri "shmget" ve "msgget" fonksiyonu kullanılır) oluşturulur yada var olan açılır. Hatırlayacağınız üzere "System 5 IPC" nesnelerinde "key-value" çifti kullanılmaktadır. "semget" fonksiyonunun prototipi aşağıdaki gibidir: #include int semget(key_t key, int nsems, int semflg); Fonksiyonun birinci parametresi, prosesler arasında kullanım için gereken, "key" değeridir. Bu "key" değeri kullanılarak bir "ID" değeri elde edilecektir. Bu "ID" değeri ise sistem genelinde "unique" haldedir. Yine bu "key" değerini bir isimden elde etmek için "ftok" fonksiyonunu kullanabiliriz. Tabii fonksiyonun bu birinci parametresi, tıpkı diğer "System 5 IPC" nesnelerinde olduğu gibi "IPC_PRIVATE" olarak da girilebilir. Böylesi bir durumda sistem, olmayan bir "key" kullanarak bizlere "ID" değeri vermektedir. /*================================================================================================================================*/ (60_17_06_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. >>>> "Kritik Kod" (devam): >>>>> "Semafor" Nesneleri (devam): >>>>>> "System 5" semafor nesneleri (devam): "semget" fonksiyonunun ikinci parametresi ise semafor sayısını belirtmektedir. Yani bu parametre toplam kaç tane semaforun oluşturulacağını belirtmektedir. Örneğin, "Üretici-Tüketici" problemi için iki adet semafor nesnesi oluşturmalıyız. Fonksiyonun üçüncü parametresi ise oluşturulacak "IPC" nesnesinin erişim haklarını belirtmektedir. Bu parametre yine "IPC_CREAT" ve "IPC_EXCL" bayrakları da "bitwise OR" işlemi ile eklenebilir. Anımsanacağız üzere sadece "IPC_CREAT" kullanılması durumunda ilgili "key" değerine sahip semafor nesnesi yok ise yeniden oluşturulacak, bu bayrakla "IPC_EXCL" ile birlikte kullanılması durumunda ise yoksa oluşturulacak fakat varsa hata ile geri dönülecektir. Tabii "IPC_EXCL" bayrağının da yalnız başına kullanılamayacağını unutmamalıyız. Fonksiyon başarı durumunda "IPC" nesnesinin "ID" değerine, hata durumunda ise "-1" ile geri döner ve "errno" değişkeni uygun değere çekilir. * Örnek 1, Aşağıdaki örnekte semafor nesneleri oluşturulmuştur. #include #include #include #include #include #define KEY_NAME "." #define KEY_ID 123 void exit_sys(const char* msg); int main(void) { key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); int semid; if((semid = semget(key, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } -> Şimdi bizler "semget" fonksiyonu ile bir nevi semafor kümesi oluşturduk. Bu küme içerisindeki semaforlar, birer indis numarasına sahiptir. Tıpkı normal dizilerde olduğu gibi. Yukarıdaki örneğin baz alırsak, küme içerisindeki semaforların indis numarası sırasıyla "0" ve "1" biçimindedir. İşte "semctl" isimli fonksiyon ile bu küme içerisindeki semafor nesneleri ile işlem yapacağız. Fonksiyonun prototipi aşağıdaki gibidir: #include int semctl(int semid, int semnum, int cmd, ...); Fonksiyonun birinci parametresi, "semget" ile elde edilen "ID" değerini belirtmektedir. İkinci parametre ise semafor kümesi içerisinde üzerinde işlem yapılacak olan semaforun indis numarasıdır. Fonksiyonun üçüncü parametresi ise bu indis numarasına sahip olan semafor nesnesine uygulanacak işlemleri belirtmektedir. Bu işlemlere göre fonksiyona dördüncü parametre geçilmesi gerekebilir. Öylesi bir durumda, bu parametreye geçilmesi gereken argüman "union semun" türünden olmalıdır. İlgili tür POSIX standartlarınca aşağıdaki biçimde tanımlanmıştır: union semun { int val; struct semid_ds *buf; unsigned short *array; }arg; /* Burada "arg" isminin kullanılması gerekmemektedir. */ Anımsayacağımız üzere birlik elemanları çakışık yerleştirilmektedir. Dolayısıyla bu dördüncü parametre, üçüncü parametredeki işleme göre, ya "int" ya "struct semid_ds*" ya da "unsigned short*" türünden bir nesne olmalıdır. Fakat bu birlik bildirimi herhangi bir başlık dosyasında BULUNMAMAKTADIR. Dolayısıyla programcının kendisi bu bildirimi YAPMALIDIR. Pekiyi bizler bu fonksiyonun üçüncü parametresine hangi argümanları geçebiliriz? Bu argümanlar şunlardır: "GETVAL", "SETVAL", "GETALL", "SETALL", "GETPID", "GETNCNT", "GETZCNT" vs. -> "GETVAL", eğer o semafor üzerinde okuma hakkımız varsa iş bu semafor nesnes içerisindeki sayacın değerini alabiliriz. Bu durumda "semctl" fonksiyonu, o semaforun sayaç değeri ile geri dönecektir. -> "SETVAL", eğer o semafor üzerinde yazma hakkımız varsa iş bu semafor nesnes içerisindeki sayaca değer vermek için kullanılır. Bu durumda "semctl" fonksiyonunun dördüncü parametresine "semun" birliğini geçmeliyiz. Fakat öncesinde bu birliğin "val" isimli elemanına bir değer atamalıyız. -> "GETALL", semafor kümesindeki bütün semaforların içindeki sayaç değerleri elde edilir. Bu durumda "semctl" fonksiyonunun dördüncü parametresine "semun" birliğini geçmeliyiz. Fakat öncesinde bu birliğin "array" isimli elemanına bir değer atamalıyız. Bunu da semafor kümesindeki semafor kadar içi boş bir dizi oluşturup, bu diziyi birliğin "array" isimli elemanına atayarak gerçekleştirebiliriz. Unutmamalıyız ki bu dizinin elemanları "unsigned short" türünden olmalıdır. Artık fonksiyonun ikinci parametresine geçilen değerin bir önemi kalmamıştır. Tabii bu işlemi yapabilmek için prosesin ilgili semafor nesnesi üzerinde okuma hakkının olması gerekmektedir. -> "SETALL", semafor kümesindeki bütün semaforların sayaçlarını "set" etmek için kullanılır. Bu durumda "semctl" fonksiyonunun dördüncü parametresine "semun" birliğini geçmeliyiz. Fakat birliğin "array" isimli elemanına, biz programcıların oluşturup içini doldurduğu "unsigned short" türünden diziyi atamalıyız. Yine bizim oluşturacağımız dizi ile semafor kümesindeki semaforların adedi aynı olmalıdır. Artık fonksiyonun ikinci parametresine geçilen değerin bir önemi kalmamıştır. Tabii bu işlemi yapabilmek için prosesin ilgili semafor nesnesi üzerinde yazma hakkının olması gerekmektedir. -> "GETPID", o semafor nesnesi üzerinde en son "semop" işlemi uygulayan prosesin "PID" değerini verir. Çok seyrek kullanılmaktadır. Yine prosesin "read" hakkına sahip olması gerekmektedir. -> "GETNCNT" ve "GETZCNT", sırasıyla semafor sayacının arttırılmasını ve sıfır olmasını bekleyen diğer proseslerin adedini elde etmek için kullanılır. Yine prosesin "read" hakkına sahip olması gerekmektedir. -> ... Fonksiyon başarısızlık durumunda "-1" ile geri dönmekte ve "errno" değerini uygun değere çekmektedir. -> Kritik Kod bölgesi "semop" fonksiyonu ile oluşturulur. İşte işlerin karıştığı nokta da burasıdır. Fonksiyonun prototipi şöyledir: #include int semop(int semid, struct sembuf *sops, size_t nsops); Fonksiyonun birinci parametresi "semget" fonksiyonu ile elde ettiğimiz "ID" değeridir. İkinci parametre ise "sembuf" isimli bir yapının adresini almaktadır. Bu yapı "sys/sem.h" içerisinde bildirilmiştir. Fakat bu nesnenin içi, bu fonksiyonu çağıran kişi tarafından doldurulur. "sembuf" yapısı aşağıdaki biçimdedir: struct sembuf { unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ }; -> "sem_num" isimli eleman, semafor kümesinde işlem yapılacak semaforun indis bilgisidir. -> "sem_op" isimli eleman, semafor sayacı üzerinde yapılacak işlemi belirtmektedir. Bu veri elemanı şu değerlerden birisini almaktadır: -> ">0" bir değer geçilirse, geçilen bu değer ile semafor nesnesi içerisindeki sayaç değeri toplanır. -> "<0" bir değer geçilirse, o andaki semafor sayacına bakılır. Eğer semafor sayacından buradaki değerin mutlak değeri çıkartıldığında sonuç negatif ise ilgili "thread" bloke edilir. Ancak burada çıkartma işlemi YAPILMAZ. Eğer bu çıkartma işleminden elde edilen sonuç sıfır ya da sıfırdan büyük olacaksa ÇIKARTMA İŞLEMİ YAPILIR VE BLOKEYE YOL AÇMAZ. * Örnek 1, semafor sayacının değerinin "5" olduğunu varsayalım. Biz de "sem_op" değerine "-4" geçmiş olalım. Bu durumda "(5) - (|-4|)" işleminin sonucu "1" olacağı için çıkartma işlemi gerçekleştirilir. Artık semafor sayacının değeri "1" olmuştur ve herhangi bir bloke oluşmamıştır. * Örnek 2, semafor sayacının değerinin "5" olduğunu varsayalım. Eğer "sem_op" değerine "5" girmiş olsaydık, "5 - 5" işleminin sonucu sıfır olacağı için çıkartma işlemi yapılır ve yine bloke oluşmaz. Artık semafor sayacının değeri "0" olmuştur. * Örnek 3, semafor sayacının değerinin "5" olduğunu varsayalım. Eğer "sem_op" değerine "-10" girmiş olsaydık, "(5) - (|-10|)" işleminin sonucu negatif olacağı için ÇIKARTMA İŞLEMİ GERÇEKLEŞTİRİLMEZ VE İLGİLİ THREAD BLOKE EDİLİR. Artık bu blokeden kurtulmadan tek yolu, semafor sayacının değerini "10" ya da daha büyük değere çekmekle mümkündür. Yani başka bir proses "sem_op" değerine "+10" değerini girmeli ki "10 - 10" işleminin sonucu sıfır olsun, bloke kalksın ve sayacın yeni değeri artık "0" olsun. -> "==0" olursa, özel bir durumdur. Başka bir anlama gelmektedir. Bu durumda ilgili semafor nesnesi içerisindeki sayaç "0" olana kadar ilgili "process" blokede bekletilir. Blokeden çıkmanın tek yolu ise bu sayacın değerini "0" dan yukarı çıkartmaktır. Yani "sem_op" değeri sıfır ise bu durum semafor sayacı "0" olmadığı sürece blokede bekle, anlamına gelmektedir. Bir nevi tam tersi bir durumdur. -> "sem_flg" isimli eleman "IPC_NOWAIT" veya "SEM_UNDO" bayraklarından birisini veya her ikisini içerebilir. Eğer bu bayrakları kullanmak istemiyorsak da "0" değerini geçebiliriz. Bu iki bayrak ise şu anlamlara gelmektedir: -> "IPC_NOWAIT" : Blokesiz işlem yapmak için kullanılmaktadır. Artık bloke oluşturabilecek durumlarda bloke oluşmaz ve "semop" fonksiyonu "-1" değeri ile geri döner ve "errno" değişkeni "EAGAIN" değerini alır. -> "SEM_UNDO" : İşlem sonrasında "Semaphore Adjusment" değerini işleme sokarak ters işlem yapmaktadır. Bu bayrağı şimdilik es geçiyoruz. > Hatırlatıcı Notlar: >> Semafor nesneleri de yine "Kernel Persistance" biçimindedir. Ya sistem "boot" edilene kadar ya da o semafor nesnesi silinene kadar sistemde kalmaktadır. >> "ipcs -s" komutu ile sistemdeki semafor nesnelerini "shell" programı ile görüntüleyebiliriz. >> "ipcrm -s " diyerek de o sistemdeki "ID" değerine sahip semafor nesnesi silinmektedir. /*================================================================================================================================*/ (61_18_06_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. >>>> "Kritik Kod" (devam): >>>>> "Semafor" Nesneleri (devam): >>>>>> "System 5" semafor nesneleri (devam): "semop" fonksiyonunun üçüncü parametresi ise ikinci parametredeki "sembuf" dizisinin eleman sayısını belirtmektedir. Yani aslında ikinci parametreye tek bir "sembuf" nesnesinin adresini değil, elemanları "struct sembuf" olan bir dizinin başlangıç adresini de geçebiliriz. Bu durumda "semop" fonksiyonu, birden fazla semafor nesnesi üzerine işlem yapar. Fakat böyle bir kullanım çok seyrektir. Dolayısıyla bu parametre "1" geçilir. Eğer "semop" fonksiyonunda birden fazla semafor nesnesi üzerinde işlem yapılacaksa, bu işlemler atomik yapılmaktadır. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", hata durumunda ise "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. -> "System 5" semafor nesneleri de diğer "System 5 IPC" nesneleri gibi "Kernel Persistance" biçimindedir. Ya silinene kadar ya da "reboot" işlemine kadar sistemde kalıcıdır. Bu tip semafor nesnelerini silmek için de "semctl" fonksiyonunda "IPC_RMID" bayrağını kullanmak gerekmektedir. Bu durumda "semctl" fonksiyonunun ikinci parametresi dikkate alınmayacaktır. Tıpkı diğer "System 5 IPC" nesnelerinde olduğu gibi, "System 5" semafor nesnelerinin bazı değerleri "semctl" fonksiyonunda "IPC_GET" ve "IPC_SET" bayrakları ile "get" ve "set" edilebilmektedir. "semid_ds" türünden bir yapı işletim sistemi tarafından oluşturulur ki bu yapı aşağıdaki gibi bildirilmiştir: struct semid_ds { struct ipc_perm sem_perm; /* Ownership and permissions */ time_t sem_otime; /* Last semop time */ time_t sem_ctime; /* Creation time/time of last modification via semctl() */ unsigned long sem_nsems; /* No. of semaphores in set */ }; -> "sem_perm" : İlgili "IPC" nesnesinin erişim hakkını belirtir. -> "sem_otime" ve "sem_ctime" : İlgili semafor nesnesi üzerinde yapılan son işlemlerin zamanları hakkında bilgi verir. -> "sem_nsems" : Semafor kümesindeki toplam semafor adedini belirtmektedir. "semctl" ile ilk değer verilmemiş semafor nesneleri için yapının "sem_ctime" ve "sem_nsems" isimli elemanları çöp değerdedir. Standartlar, henüz "semop" fonksiyonu çağrılmadan evvel yapının "sem_otime" isimli elemanının "0" değerinde olacağını garanti etmektedir. Bu garanti sayesinde iki proses aynı semafor nesnesine ilişkin "init." işlemi yapmak istediğinde oluşabilecek sorun çözülebilmektedir. Şimdi de "System 5" semafor nesneleri ile kabaca Kritik Kod oluşturulmasına bakalım: * Örnek 1, #include #include #include #include #include #define KEY_NAME "." #define KEY_ID 123 union semun { int val; struct semid_ds *buf; unsigned short *array; }; void exit_sys(const char* msg); int main(void) { //... key_t key; if((key = ftok(KEY_NAME, KEY_ID)) == -1) exit_sys("ftok"); int semid; if((semid = semget(key, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); union semun arg; unsigned short semvals[2] = {1, 0}; arg.array = semvals; if(semctl(semid, 1, SETALL, arg) == -1) exit_sys("semctl"); //... { struct sembuf sbuf; sbuf.sem_num = 0; /* "0" indisli semafor nesnesini ele alacağız. */ sbuf.sem_op = -1; /* Bu semafor nesnesinin sayacını "1" azaltacağız. */ sbuf.sem_flags = 0; semop(semid, &sbuf, 1); /* Kritik Kod */ sbuf.sem_num = 0; /* "0" indisli semafor nesnesini ele alacağız. */ sbuf.sem_op = 1; /* Bu semafor nesnesinin sayacını "1" arttıracağız. */ sbuf.sem_flags = 0; semop(semid, &sbuf, 1); } if(semctl(semid, 0, IPC_RMID) == -1) exit_sys("semctl"); //... return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Daha önce de belirtildiği gibi "System 5" semafor nesneleri prosesler arasında kullanım için düşünülmüştür. Özellikle "Üretici-Tüketici" problemleri gibi tipik senkronizasyon problemleri, bu nesneler ile çözülmekteydi. Mademki bu "System 5" semafor nesneleri prosesler arasında kullanılmaktadır, o halde bu nesneler ağırlıklı olarak Paylaşılan Bellek Alanları ile birlikte kullanılmaktadır. * Örnek 1, Aşağıdaki örnekte semafor nesnesinin silinmesini geciktirmek için kara düzen bir sistem uygulanmıştır. /* Producer */ #include #include #include #include #include #include #include #include #define SHM_KEY 0x12345678 #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO{ int head; int tail; int queue[QUEUE_SIZE]; int semid; /*0: producer; 1: consumer*/ }SHARED_INFO; union semun{ int val; struct semid_ds* buf; unsigned short* array; struct seminfo* __buf; }; void exit_sys(const char* msg); int main(void) { srand(time(NULL)); int shmid; if((shmid = shmget(SHM_KEY, sizeof(SHARED_INFO), IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shmget"); SHARED_INFO* si; if((si = (SHARED_INFO*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); si->head = 0; si->tail = 0; /* * Semafor nesnesi "IPC_PRIVATE" ile hayata getirilmiştir. */ if((si->semid = semget(IPC_PRIVATE, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); union semun arg; unsigned short semvals[2] = { QUEUE_SIZE, 0 }; arg.array = semvals; if(semctl(si->semid, 0, SETALL, arg) == -1) exit_sys("semctl"); struct sembuf sbuf; int val = 0; for(;;) { usleep(rand() % 300000); sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); si->queue[si->tail++] = val; si->tail %= QUEUE_SIZE; sbuf.sem_num = 1; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); if(val == 100) break; ++val; } printf("Press ENTER to finish...\n"); getchar(); if(semctl(si->semid, 0, IPC_RMID) == -1) exit_sys("semctl"); if(shmdt(si) == -1) exit_sys("shmdt"); if(shmctl(shmid, IPC_RMID, 0) == -1) exit_sys("shmctl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #define SHM_KEY 0x12345678 #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO{ int head; int tail; int queue[QUEUE_SIZE]; int semid; /*0: producer; 1: consumer*/ }SHARED_INFO; void exit_sys(const char* msg); int main(void) { srand(time(NULL)); int shmid; if((shmid = shmget(SHM_KEY, 0, 0)) == -1) exit_sys("shmget"); SHARED_INFO* si; if((si = (SHARED_INFO*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); struct sembuf sbuf; int val = 0; for(;;) { sbuf.sem_num = 1; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); val = si->queue[si->head++]; si->head %= QUEUE_SIZE; sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if(val == 100) break; } printf("\n"); if(shmdt(si) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise, yukarıdaki kara düzeni kullanmak yerine, yeni bir semafor nesnesi kullanılarak iki prosesin haberleşmesinin sonlandırılması sağlanmıştır. Fakat Paylaşılan Bellek Alanı'nı "Producer" oluşturduğu için, ilk olarak onun çalıştırılması gerekmektedir. /* sharing.h */ #ifndef SHARING_H #define SHARING_H #define SHM_KEY 0x12345678 #define SEM_KEY 0X12345678 #define QUEUE_SIZE 10 typedef struct tagSHARED_INFO{ int head; int tail; int queue[QUEUE_SIZE]; int semid; /*0: producer; 1: consumer*/ }SHARED_INFO; union semun{ int val; struct semid_ds* buf; unsigned short* array; struct seminfo* __buf; }; #endif /* Producer */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char* msg); int main(void) { srand(time(NULL)); int shmid; if((shmid = shmget(SHM_KEY, sizeof(SHARED_INFO), IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shmget"); SHARED_INFO* si; if((si = (SHARED_INFO*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); si->head = 0; si->tail = 0; if((si->semid = semget(IPC_PRIVATE, 2, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); int semid; if((semid = semget(SEM_KEY, 1, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); union semun arg; unsigned short semvals[2] = { QUEUE_SIZE, 0 }; arg.array = semvals; if(semctl(si->semid, 0, SETALL, arg) == -1) exit_sys("semctl"); arg.val = 0; if(semctl(semid, 0, SETVAL, arg) == -1) exit_sys("semctl"); struct sembuf sbuf; sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(semid, &sbuf, 1) == -1) exit_sys("semop"); int val = 0; for(;;) { usleep(rand() % 300000); sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); si->queue[si->tail++] = val; si->tail %= QUEUE_SIZE; sbuf.sem_num = 1; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); if(val == 100) break; ++val; } sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(semid, &sbuf, 1) == -1) exit_sys("semop"); if(semctl(si->semid, 0, IPC_RMID) == -1) exit_sys("semctl"); if(semctl(semid, 0, IPC_RMID) == -1) exit_sys("semctl"); if(shmdt(si) == -1) exit_sys("shmdt"); if(shmctl(shmid, IPC_RMID, 0) == -1) exit_sys("shmctl"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* Consumer */ #include #include #include #include #include #include #include #include #include "sharing.h" void exit_sys(const char* msg); int main(void) { srand(time(NULL)); int shmid; if((shmid = shmget(SHM_KEY, 0, 0)) == -1) exit_sys("shmget"); SHARED_INFO* si; if((si = (SHARED_INFO*)shmat(shmid, NULL, 0)) == (void*)-1) exit_sys("shmat"); int semid; if((semid = semget(SEM_KEY, 1, IPC_CREAT|S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("semget"); struct semid_ds semds; if(semctl(semid, 0, IPC_STAT, &semds) == -1) exit_sys("semctl"); union semun arg; if(semds.sem_otime == 0) { arg.val = 0; if(semctl(semid, 0, SETVAL, arg) == -1) exit_sys("semctl"); } struct sembuf sbuf; sbuf.sem_num = 0; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(semid, &sbuf, 1) == -1) exit_sys("semop"); int val = 0; for(;;) { sbuf.sem_num = 1; sbuf.sem_op = -1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); val = si->queue[si->head++]; si->head %= QUEUE_SIZE; sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(si->semid, &sbuf, 1) == -1) exit_sys("semop"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if(val == 100) break; } printf("\n"); sbuf.sem_num = 0; sbuf.sem_op = 1; sbuf.sem_flg = 0; if(semop(semid, &sbuf, 1) == -1) exit_sys("semop"); if(shmdt(si) == -1) exit_sys("shmdt"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Görüldüğü üzere "System 5" semafor nesneleri, "POSIX" Semafor nesnelerine nazaran kullanımı daha zahmetlidir. Fakat bir takım "wrapper" fonksiyonlar yazarak "POSIX" semafor nesnelerini taklit edebilir, yukarıdaki karmaşıklığı minimize edebiliriz. Şöyleki: /* Anahtar ve erişim haklarını alarak bir semafor nesnesi oluşturur. */ int sem_create(int key, int mode) { return semget(key, 1, IPC_CREAT | mode); } /* Oluşturulan bu semafor nesnesi açılır. */ int sem_open(int key) { return semget(key, 1, 0); } /* Açılan bu semafor nesnesinin sayacına ilk değeri atanır. */ int sem_init(int semid, int val) { union semun{ int val; struct semid_ds* buf; unsigned short* array; struct seminfo *__buf; }su; su.val = val; return semctl(semid, 0, su); } /* Kritik Kod oluşturmak için kullanılır. */ int sem_wait(int semid) { struct sembuf sb; sb.sem_num = 0; sb.sem_op = -1; sb.sem_flg = 0; return semop(semid, &sb, 1); } /* Kritik Kod oluşturmak için kullanılır. */ int sem_post(int semid) { struct sembuf sb; sb.sem_num = 0; sb.sem_op = 1; sb.sem_flg = 0; return semop(semid, &sb, 1); } /* İlgili semafor nesnesi yok edilir. */ int sem_destroy(int semid) { return semctl(semid, 0, IPC_RMID); } Son olarak tekrar hatırlatmakta fayda vardır ki "System 5" semafor nesneleri prosesler arasında haberleşme için tasarlanmışlardır. Bu nesneler tasarlandığı yıllarda henüz "thread" ler kullanımda değildi. Her ne kadar günümüzde bu semafor nesneleri aynı prosesin "thread" leri arasında da kullanım olanağı olsada, böyle bir kullanım verimsiz ve gereksizdir. "thread" ler arasında haberleşmede semafor nesnesi kullanacaksak, tavsiye edilen isimsiz POSIX semaforlarını kullanmalıyız. Çünkü POSIX senkronizasyon nesneleri UNIX ve türevi sistemlere "thread" ler ile birlikte eklenmiştir. Şimdi de bir diğer senkronizasyon nesnesi olan bariyer senkronizasyon nesnesini inceleyelim: >>>>> "Bariyer Senkronizasyon Nesnesi" : Bir duvar düşünün; bir kişi bu duvarı yıkamıyor ve ikinci bir kişiyi çağırıyor. İki kişi birlikte yıkamadığı için üçüncü bir kişi çağırıyorlar. En sonunda bu üç kişi duvarı yıkabiliyor. İşte bariyer senkronizasyon nesneleri de bu mottodadır. /*================================================================================================================================*/ (62_01_07_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. >>>> "Kritik Kod" (devam): >>>>> "Bariyer Senkronizasyon Nesnesi" (devam): Nispeten az kullanılan senkronizasyon nesneleridir. Windows sistemlerinde bu tip nesnelerin tam bir karşılığı yoktur. Bu tip senkronizasyon nesneleri, bir işin parçalarının bir takım "thread" lere yaptırılması fakat nihai işin bu "thread" lerin işlerini bitirtikten sonra yaptırılması için kullanılır. Örneğin, yeteri kadar büyük bir diziyi sıralamak isteyelim. Bu işi de 10 farklı "thread" ile gerçekleştirmek isteyelim. Her "thread" kendisine düşen kısmı sıraladıktan sonra, en son da birleştirme işlemi gerçekleştirelim. İşte bu birleştirme işlemi için kullanılmaktadır. Bu senkronizasyon nesnesini aşağıdaki adımları takip ederek kullanabiliriz: -> İlk önce "pthread_barrier_t" türünden "global" bir nesne tanımlanır. POSIX standartlarınca bu tür herhangi bir türe karşılık gelebilir. "sys/types.h" ve "pthread.h" içerisinde "define" edilmişlerdir. Genel olarak "struct" türüne karşılık gelmektedir. -> Tanımlanan bu nesneye ilk değer verilmesi gerekmektedir. Bunun için "pthread_barrier_init" isimli POSIX fonksiyonu kullanılmaktadır. "barrier" nesnelerini makrolar ile "init" EDEMEYİZ. Fonksiyonun parametrik yapısı aşağıdaki gibidir: #include int pthread_barrier_init(pthread_barrier_t * barrier, const pthread_barrierattr_t * attr, unsigned count); Fonksiyonun birinci parametresi, ilgili "barrier" nesnesinin adresidir. İkinci parametre ise bir "attribute" nesnesidir. Bu parametreye "NULL" değeri geçilebilir. Bu durumda varsayılan özellikler kullanılacaktır. Eğer varsayılan özellikler kullanılmayacaksa ki bir adet özellik nesnesi vardır ve o da ilgili nesnenin prosesler arasında kullanılıp kullanılmayacağını belirtmektedir. Varsayılan durumda nesne, prosesler arasında kullanılamamaktadır. Eğer bu özelliği değiştirmek istiyorsak; -> İlk önce "pthread_barrierattr_t" türünden bir nesne oluşturulur. -> Daha sonra bu nesneye "pthread_barrierattr_init" fonksiyonu ile ilk değer verilir. -> Daha sonra ilgili özelliğin "set" ve "get" edilmesi için sırasıyla "pthread_barrierattr_setpshared" ve "pthread_barrierattr_getpshared" fonksiyonları çağrılabilir. Burada tek bir "attribute" olduğu için sadece "PTHREAD_PROCESS_SHARED" sembolik sabiti kullanımaktadır. Bu özellik ise ilgili nesnenin prosesler arasında kullanılıp kullanılamayacağını belirtmektedir. -> En sonunda ise bu "attribute" nesnesini "destroy" etmeliyiz. Bunu da "pthread_barrierattr_destroy" ile gerçekleştirebiliriz. Tabii yine prosesler arasında kullanabilmek için, ilgili bariyer nesnesini de Paylaşılan Bellek Alanında oluşturmamız gerekmektedir. Son parametre ise kaç tane nesne geldiğinde bariyerin açılacağı bilgisidir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Burada dikkat etmemiz gereken şey bariyer kırıldıktan sonra tekrar otomatik olarak YENİLENMESİDİR. -> "n" tane "thread" in belli bir noktada bekletilmesi için "pthread_barrier_wait" fonksiyonu kullanılmaktadır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_barrier_wait(pthread_barrier_t *barrier); Fonksiyon argüman olarak ilgili "barrier" nesnesinin adresini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Bu fonksiyon ilgili "thread" lerin "barrier" nesnesi tarafından bekletilmesine yol açar. "n" tane "thread" bekletildiğinde artık bloke kaldırılır. Dolayısıyla bu fonksiyon çağrıldığında ilgili "thread" in kendi işini bitirmiş olması gerekmektedir. Yukarıdaki diziyi sıralama örneğini baz alırsak, kendi içerisinde sıralanmış dizileri hangi "thread" birleştirecektir? POSIX standartları bu iş için yine "pthread_barrier_wait" fonksiyonunu görevli kılmıştır. Şöyleki; Bu fonksiyon aynı zamanda "PTHREAD_BARRIER_SERIAL_THREAD" sembolik sabiti ile de geri dönmektedir. İşte hangi "thread" in çağırdığı "pthread_barrier_wait" fonksiyonu bu sembolik sabit ile geri dönerse, nihai birleştirme işi de o "thread" tarafından yapılacaktır. Fakat hangi şartlar altında fonksiyonun bu sembolik sabit ile döneceği konusunda POSIX standartları bir şey söylememiştir. -> Artık ilgili "barrier" nesnesini "destroy" etmeliyiz. Bunun için "pthread_barrier_destroy" fonksiyonunu çağırmalıyız. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_barrier_destroy(pthread_barrier_t *barrier); Yine argüman olarak ilgili bariyer nesnesinin adresini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. * Örnek 1, Aşağıda 3 adet "thread" kullanılmıştır. #include #include #include #include #include void* pthread_proc1(void* param); void* pthread_proc2(void* param); void* pthread_proc3(void* param); void exit_sys_errno(const char* msg, int eno); pthread_barrier_t g_barrier; int main(void) { pthread_t tid1, tid2, tid3; int result; if((result = pthread_barrier_init(&g_barrier, NULL, 3)) != 0) exit_sys_errno("pthread_barrier_init", result); if((result = pthread_create(&tid1, NULL, pthread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid3, NULL, pthread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_barrier_destroy(&g_barrier)) != 0) exit_sys_errno("pthread_barrier_destroy", result); return 0; } void* pthread_proc1(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc1 started...\n"); seed = time(NULL) + 123; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc1 will merge >>>\n"); } printf("pthread_proc1 returned after %d seconds...\n", sleep_time); return NULL; } void* pthread_proc2(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc2 started...\n"); seed = time(NULL) + 456; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc2 will merge >>>\n"); } printf("pthread_proc2 returned after %d seconds...\n", sleep_time); return NULL; } void* pthread_proc3(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc3 started...\n"); seed = time(NULL) + 789; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc3 will merge >>>\n"); } printf("pthread_proc3 returned after %d seconds...\n", sleep_time); return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } * Örnek 2, Aşağıda 3 adet "thread" kullanılmıştır. Ek olarak ilgili bariyer nesnesi prosesler arasında kullanıma açılmıştır. #include #include #include #include #include void* pthread_proc1(void* param); void* pthread_proc2(void* param); void* pthread_proc3(void* param); void exit_sys_errno(const char* msg, int eno); pthread_barrier_t g_barrier; int main(void) { int result; pthread_barrierattr_t battr; if((result = pthread_barrierattr_init(&battr)) != 0) exit_sys_errno("pthread_barrierattr_init", result); if((result = pthread_barrierattr_setpshared(&battr, PTHREAD_PROCESS_SHARED)) != 0) exit_sys_errno("pthread_barrierattr_setpshared", result); if((result = pthread_barrierattr_destroy(&battr)) != 0) exit_sys_errno("pthread_barrierattr_destroy", result); pthread_t tid1, tid2, tid3; if((result = pthread_barrier_init(&g_barrier, &battr, 3)) != 0) exit_sys_errno("pthread_barrier_init", result); if((result = pthread_create(&tid1, NULL, pthread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid3, NULL, pthread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_barrier_destroy(&g_barrier)) != 0) exit_sys_errno("pthread_barrier_destroy", result); return 0; } void* pthread_proc1(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc1 started...\n"); seed = time(NULL) + 123; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc1 will merge >>>\n"); } printf("pthread_proc1 returned after %d seconds...\n", sleep_time); return NULL; } void* pthread_proc2(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc2 started...\n"); seed = time(NULL) + 456; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc2 will merge >>>\n"); } printf("pthread_proc2 returned after %d seconds...\n", sleep_time); return NULL; } void* pthread_proc3(void* param) { unsigned int seed; int sleep_time; int result; printf("pthread_proc3 started...\n"); seed = time(NULL) + 789; sleep_time = rand_r(&seed) % 10; sleep(sleep_time); if((result = pthread_barrier_wait(&g_barrier)) != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_errno("pthread_barrier_wait", result); if(result == PTHREAD_BARRIER_SERIAL_THREAD) { printf("<<< pthread_proc3 will merge >>>\n"); } printf("pthread_proc3 returned after %d seconds...\n", sleep_time); return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } * Örnek 3.0, Aşağıda ise bir dizi parçalara bölünerek sıralanmıştır. #include #include #include #include #include #include /* Thread'siz 50000000 için 11.30 saniye (100000000 için 43.1)*/ #define SIZE 50000000 #define NTHREADS 10 void sort_with_single_thread(void); void sort_with_multiple_thread(void); void exit_sys_thread(const char *msg, int err); void *thread_proc(void *param); int comp(const void *pv1, const void *pv2); void merge(void); int check(void); pthread_barrier_t g_barrier; int g_nums[SIZE]; int g_snums[SIZE]; clock_t g_start, g_stop; double g_telaped; int main(void) { puts("\n--------------------------------"); sort_with_single_thread(); // Total Duration: 5.080036 puts("\n--------------------------------"); sort_with_multiple_thread(); // Total Duration: 8.873042 puts("\n--------------------------------"); return 0; } void sort_with_single_thread(void) { g_start = clock(); qsort(g_nums, SIZE, sizeof(int), comp); merge(); g_stop = clock(); printf(check() ? "Sorted\n" : "Not Sorted\n"); g_telaped = (double)(g_stop - g_start) / CLOCKS_PER_SEC; printf("Total Duration: %f\n", g_telaped); } void sort_with_multiple_thread(void) { int result; int i; pthread_t tids[NTHREADS]; srand(time(NULL)); for (i = 0; i < SIZE; ++i) g_nums[i] = rand(); g_start = clock(); if ((result = pthread_barrier_init(&g_barrier, NULL, NTHREADS)) != 0) exit_sys_thread("pthread_barrier_init", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_create(&tids[i], NULL, thread_proc, (void *)i)) != 0) exit_sys_thread("pthread_create", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_join(tids[i], NULL)) != 0) exit_sys_thread("pthread_join", result); pthread_barrier_destroy(&g_barrier); g_stop = clock(); printf(check() ? "Sorted\n" : "Not Sorted\n"); g_telaped = (double)(g_stop - g_start) / CLOCKS_PER_SEC; printf("Total Duration: %f\n", g_telaped); } void exit_sys_thread(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } void *thread_proc(void *param) { int part = (int)param; int result; qsort(g_snums + part * (SIZE / NTHREADS) , SIZE / NTHREADS, sizeof(int), comp); result = pthread_barrier_wait(&g_barrier); if (result != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_thread("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) merge(); /* other jobs... */ return NULL; } int comp(const void *pv1, const void *pv2) { const int *pi1 = (const int *)pv1; const int *pi2 = (const int *)pv2; return *pi1 - *pi2; } void merge(void) { int indexes[NTHREADS]; int min, min_index; int i, k; int partsize; int size = NTHREADS; partsize = SIZE / NTHREADS; for (i = 0; i < NTHREADS; ++i) indexes[i] = i * partsize; while(size > 0) { min = g_nums[indexes[0]]; min_index = 0; for (k = 1; k < size; ++k) if (g_nums[indexes[k]] < min) { min = g_nums[indexes[k]]; min_index = k; } g_snums[i] = min; ++indexes[min_index]; if(indexes[min_index] >= (i+1) * partsize){ indexes[min_index] = indexes[size - 1]; --size; } } } int check(void) { int i; for (i = 0; i< SIZE - 1; ++i) if (g_snums[i] > g_snums[i + 1]) return 0; return 1; } * Örnek 3.1, Aşağıda ise bir dizi parçalara bölünerek sıralanmıştır. #include #include #include #include #include #include /* Thread'siz 50000000 için 11.30 saniye (100000000 için 43.1)*/ #define SIZE 50000000 #define NTHREADS 10 struct INDEX{ int index; int part; }; void sort_with_single_thread(void); void sort_with_multiple_thread(void); void exit_sys_thread(const char *msg, int err); void *thread_proc(void *param); int comp(const void *pv1, const void *pv2); void merge(void); int check(void); pthread_barrier_t g_barrier; int g_nums[SIZE]; int g_snums[SIZE]; clock_t g_start, g_stop; double g_telaped; int main(void) { puts("\n--------------------------------"); sort_with_single_thread(); // Total Duration: 5.080036 puts("\n--------------------------------"); sort_with_multiple_thread(); // Total Duration: 8.873042 puts("\n--------------------------------"); return 0; } void sort_with_single_thread(void) { g_start = clock(); qsort(g_snums, SIZE, sizeof(int), comp); g_stop = clock(); printf(check() ? "Sorted\n" : "Not Sorted\n"); g_telaped = (double)(g_stop - g_start) / CLOCKS_PER_SEC; printf("Total Duration: %f\n", g_telaped); } void sort_with_multiple_thread(void) { int result; int i; pthread_t tids[NTHREADS]; srand(time(NULL)); for (i = 0; i < SIZE; ++i) g_nums[i] = rand(); g_start = clock(); if ((result = pthread_barrier_init(&g_barrier, NULL, NTHREADS)) != 0) exit_sys_thread("pthread_barrier_init", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_create(&tids[i], NULL, thread_proc, (void *)i)) != 0) exit_sys_thread("pthread_create", result); for (i = 0; i < NTHREADS; ++i) if ((result = pthread_join(tids[i], NULL)) != 0) exit_sys_thread("pthread_join", result); pthread_barrier_destroy(&g_barrier); g_stop = clock(); printf(check() ? "Sorted\n" : "Not Sorted\n"); g_telaped = (double)(g_stop - g_start) / CLOCKS_PER_SEC; printf("Total Duration: %f\n", g_telaped); } void exit_sys_thread(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } void *thread_proc(void *param) { int part = (int)param; int result; qsort(g_snums + part * (SIZE / NTHREADS) , SIZE / NTHREADS, sizeof(int), comp); result = pthread_barrier_wait(&g_barrier); if (result != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) exit_sys_thread("pthread_barrier_wait", result); if (result == PTHREAD_BARRIER_SERIAL_THREAD) merge(); /* other jobs... */ return NULL; } int comp(const void *pv1, const void *pv2) { const int *pi1 = (const int *)pv1; const int *pi2 = (const int *)pv2; return *pi1 - *pi2; } void merge(void) { struct INDEX indexes[NTHREADS]; int min, min_index; int i, k; int partsize; int size = NTHREADS; partsize = SIZE / NTHREADS; for (i = 0; i < NTHREADS; ++i){ indexes[i].index = i * partsize; indexes[i].part = i; } while(size > 0) { min = g_nums[indexes[0].index]; min_index = 0; for (k = 1; k < size; ++k) if (g_nums[indexes[k].index] < min) { min = g_nums[indexes[k].index]; min_index = k; } g_snums[i] = min; ++indexes[min_index].index; if(indexes[min_index].index >= (i+1) * indexes[min_index].part){ indexes[min_index] = indexes[size - 1]; --size; } } } int check(void) { int i; for (i = 0; i< SIZE - 1; ++i) if (g_snums[i] > g_snums[i + 1]) return 0; return 1; } >>>>> "Spinlock" senkronizasyon nesnesi: Tek bir "thread" akışının Kritik Kod bölgesine girmesini sağlamaktadır. Diğer senkronizasyon nesnelerine nazaran meşgul bir döngü içerisinde bekleme yapmakta, blokeye yol açmamaktadır. Anımsayacağınız üzere "thread" ler bloke edilirken, yani "wait" kuyruğuna alınırken", ve blokeleri kaldırılırken, yani "run" kuyruklarına alınırken, bir zaman harcarlar. Öte yandan meşgul bir döngü içerisinde beklemek de "quanta" süresini boşa harcamaya neden olmaktadır. İşte "quanta" süresinin boşa harcanması, bir "thread" in önce "wait" daha sonra "run" kuyruklarına alınmasından daha önemsizse, bu durumlarda "Spinlock" senkronizasyon nesnesi kullanılabilir. Bir diğer deyişle Kritik Kod bölgesi çok kısa olan ve o bölgenin çağrılma olasılığı düşük olan senaryolarda "Spinlock" kullanabiliriz. Yani meşgul bir döngü içerisinde beklemeye değecekse, "Spinlock"; aksi halde diğer senkronizasyon nesneleri. Kaldı ki diğer senkronizasyon nesneleri az da olsa "spin" yapmaktadır. Diğer yandan tek "core" lu işlemcilerde bu senkronizasyon nesnesinin kullanımı hiç tavsiye edilmez. Birden fazla "core" içeren işlemcilerde kullanırken de dikkatli olmalıyız. Burada tavsiye edilen şey aynı prosesin "thread" leri arasında senkronizasyon için akla ilk gelmesi gereken "mutex" nesneleridir. "Spinlock" nesneleri bazı uç senaryolarda performansı arttırmak için dikkatli kullanılmalıdır. Pekiyi "Spinlock" nesneleri nasıl kullanılır? -> "pthread_spinlock_t" türünden global bir nesne oluşturulur. Bu tür yine "pthread.h" ve "sys/types.h" içerisinde tanımlanmıştır. Herhangi bir türe karşılık gelebilir. Tipik olarak "struct" türüne karşılık gelmektedir. -> Daha sonra bu nesneye "pthread_spin_init" fonksiyonu ile ilk değer verilir. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_spin_init(pthread_spinlock_t *lock, int pshared); Fonksiyonun birinci parametresi, yukarıdaki nesnenin adresini almaktadır. İkinci parametre ise prosesler arasında kullanılıp kullanılamayacağını belirtmektedir. Eğer kullanılacaksa "PTHREAD_PROCESS_SHARED" değerini, aksi halde "0" değerini almaktadır. Eğer prosesler arasında kullanılacaksa, Paylaşılan Bellek Alanları kullanılmalıdır. Diğer taraftan "Spinlock" nesnesinin, "barrier" nesnelerine nazaran, ayrıca bir "attribute" nesnesine sahip olmadığına dikkat ediniz. Yine "Spinlock" nesnesine ilk değer vermek için bir makro bulunmamaktadır. /*================================================================================================================================*/ (63_02_07_2023) > "Threads in POSIX" (devam): >> "thread" lerin senkronize edilmesi (devam): >>> Anımsanacağınız üzere "thread" ler arası senkronizasyon sağlanabilmesi için "Kritik Kod" bölgelerinin oluşturulması gerekmektedir. Böylelikle bir "thread" ilgili "Kritik Kod" bölgesine girdiğinde, diğer "thread" bekletilmektedir. >>>> "Kritik Kod" (devam): >>>>> "Spinlock" senkronizasyon nesnesi (devam): -> Kritik Kod bölgesi "pthread_spin_lock" ve "pthread_spin_unlock" fonksiyonları ile oluşturulur. Fonksiyonların prototipleri şu şekildedir: #include int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock); Fonksiyonlar argüman olarak ilgili "Spinlock" nesnesinin adresini alırlar. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönerler. "thread" in akışı "pthread_spin_lock" fonksiyonuna girdiğinde meşgul bir döngü içerisinde beklemektedir. "pthread_spin_lock" fonksiyonunun "pthread_spin_trylock" biçimi de vardır. Bu fonksiyon ise "spin" yapmak yerine başarısızlıkla geri döner. Bu tür durumlarda ise hata kodu genellikle "EBUSY" biçimindedir. -> Son aşamada ise ilgili "Spinlock" nesnesinin "destroy" edilmesi gerekmektedir. Bunun için de "pthread_spin_destroy" fonksiyonu çağrılmalıdır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_spin_destroy(pthread_spinlock_t *lock); Fonksiyon ilgili "Spinlock" nesnesinin adresini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Aşağıda ise "Spinlock" nesnesinin kullanımına dair örnekler verilmiştir. "Spinlock" nesnesinin kullanımı CPU yoğun bir kullanımdır. Dolayısıyla birden fazla çekirdekli işlemcilerde daha iyi sonuç verebilmektedir. Fakat işletim sistemi aşağıdaki "thread" leri aynı çekirdeklere atarsa, tam tersine daha kötü sonuçlar da elde edebiliriz. * Örnek 1, #include #include #include #include #include #define MAX_COUNT 10000000 void* pthread_proc1(void* param); void* pthread_proc2(void* param); void exit_sys_errno(const char* msg, int eno); pthread_spinlock_t g_spinlock; int g_count; int main(void) { int result; if((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); printf("Ok... %d\n", g_count); return 0; } void* pthread_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } * Örnek 2, Aşağıdaki örnekte ise "Spinlock" ile "mutex" nesnesi hız bakımından karşılaştırılmıştır. #include #include #include #include #include #define MAX_COUNT 10000000 void* pthread_spin_proc1(void* param); void* pthread_spin_proc2(void* param); void* pthread_mutex_proc1(void* param); void* pthread_mutex_proc2(void* param); void spin_lock(void); void mutex_lock(void); void exit_sys_errno(const char* msg, int eno); pthread_spinlock_t g_spinlock; pthread_mutex_t g_mutex; int g_count; int main(void) { /* # OUTPUT # Ok... 20000000, in 1.886349 seconds Ok... 40000000, in 1.951004 seconds */ spin_lock(); mutex_lock(); return 0; } void* pthread_spin_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_spin_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_mutex_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_mutex_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void spin_lock(void) { double start, end; int result; start = clock(); if((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_spin_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_spin_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void mutex_lock(void) { double start, end; int result; start = clock(); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_mutex_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_mutex_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } >>>>> "Read-Write Lock" Nesneleri: Bir diğer sık karşılaşılan senkronizasyon nesnesi de "Reader-Writer Lock" nesneleridir. Öyle bir senkronizasyon nesnesidir ki birden fazla "thread" in okuma yapmasına izin vermekte, fakat bir "thread" yazma işlemi yapıyorsa diğer "thread" lerin okuma veya yazma yapmasına izin vermemektedir. Yani şöyle de diyebiliriz: elimizde bir adet bağlı liste ve bu bağlı listeye "insert", "delete" ve "search" işlemleri yapan bir grup "thread" ler söz konusu olsun. Burada "thread" lerden birisi bağlı listeye "insert" ya da "delete" işlemi yaparken diğerlerinin beklemesi gerekmektedir. Benzer biçimde "thread" lerden birisi "search" işlemi yaparken diğer "thread" lerin "insert" ya da "delete" işlemi yapamaması gerekmektedir. Sadece birden fazla "thread" in aynı anda "search" işlemi yapmasına müsaade edilmelidir. Buradaki "insert" ve "delete" işlemleri "write", "search" işlemi de "read" işlemi olarak ele alınabilir. Burada açıklanan durumu daha önce işlenen senkronizasyon nesnelerini direkt olarak kullanarak basitçe bir yolu yoktur. Örneğin, "mutex" nesnelerini ele alalım: "write" işlemi yaparken diğerlerinin beklemesini sağlayabiliriz fakat "read" yaparken diğerlerinin "read" yapmasını da engelleriz. Dolayısıyla iki tane "read" işlemi yapılamaz. Pekiyi "Reader-Write Lock" nesneleri nasıl kullanılır? -> İlk önce "pthread_rwlock_t" türünden nesne oluşturulur. Bu tür yine "pthread.h" ve "sys/types.h" dosyalarında herhangi bir tür olabilecek şekilde "typedef" edilmiştir. Genellikle "struct" türüne karşılık gelmektedir. -> Daha sonra bu nesneye ilk değerini vereceğiz. Bunun için ya "PTHREAD_RWLOCK_INITIALIZER" makrosu ile statik bir biçimde ya da "pthread_rwlock_init" fonksiyonu ile ilk değer verebiliriz. İlgili fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_rwlock_init(pthread_rwlock_t * rwlock, const pthread_rwlockattr_t * attr); Fonksiyonun ilk parametresi ilgili "rwlock" nesnesinin adresini almaktadır. İkinci parametre ise iş bu "rwlock" nesnesine ait bir "attribute" nesnesinin adresidir. Bu özellik nesnesini kullanabilmek için; -> İlk önce "pthread_rwlockattr_t" türünden bir nesne oluşturulur. -> Daha sonra bu nesneye "pthread_rwlockattr_init" fonksiyonu ile ilk değer verilir. -> Daha sonra "pthread_rwlockattr_setXXX" fonksiyonları ile yeni özellikler iliştirilir. Fakat şimdilik sadece bir adet özellik bulunmaktadır. O da prosesler arasında kullanılabilirliğine ilişkindir. Varsayılan durumda (yani "PTHREAD_RWLOCK_INITIALIZER" makrosu kullanıldığında ya da "pthread_rwlock_init" fonksiyonunda "attribute" argümanı için "NULL" değeri geçildiğinde), ilgili "rwlock" nesnesi prosesler arasında kullanımı YOKTUR. -> Daha sonra bu "attribute" nesnesi "pthread_rwlock_init" fonksiyonunda kullanılır ve böylelikle ilgili özelliklerde bir "rwlock" nesnesi ilk değerini alır. -> En sonunda da "pthread_rwlockattr_destroy" ile iş bu "attribute" nesnesini yok etmeliyiz. Bu yok etme işlemini, ilgili "rwlock" nesnesine ilk değer verdikten hemen sonra da gerçekleştirebiliriz. Bu parametreye "NULL" adres geçilmesi durumunda varsayılan değerler kullanılacaktır. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. -> Kritik Kod bölgesi oluşturmak için elimizde iki adet fonksiyon bulunmaktadır. Bunlar "pthread_rwlock_rdlock" ve "pthread_rwlock_wrlock" isimlerinde olup, sırasıyla "read" ve "write" işlemleri için kullanılacaktır. Burada niyetimize göre iki farklı "lock" fonksiyonu mevcuttur. Fakat kilitlenen "rwlock" nesnesinin kilidini açmak için sadece "pthread_rwlock_unlock" fonksiyonu kullanılmaktadır. Fonksiyonların prototipi ise aşağıdaki gibidir: #include int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); Fonksiyonlar argüman olarak ilgili "rwlock" nesnesinin adresini almaktadır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Tabii buradaki "rwlock" nesnesini kilitleyen fonksiyonların "try" lı versiyonları da vardır ki o fonksiyonlar kilitlenmiş nesne karşısında başarısızla geri dönmektedir. Bunlar "pthread_rwlock_tryrdlock" ve "pthread_rwlock_trywrlock" isimli fonksiyonlardır. -> İlgili "rwlock" nesnesi ile işimiz bittikten sonra "destroy" işlemi gerçekleştirilmelidir. Bunun için "pthread_rwlock_destroy" fonksiyonu çağrılmalıdır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); Fonksiyon argüman olarak ilgili "rwlock" nesnesinin adresini almaktadır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. * Örnek 1, Aşağıda "rwlock" nesnesinin kullanımına ilişkin bir örnek verilmiştir: #include #include #include #include #include #include void exit_sys_errno(const char *msg, int err); void *thread_proc1(void *param); void *thread_proc2(void *param); void *thread_proc3(void *param); void *thread_proc4(void *param); pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER; int main(void) { int result; pthread_t tid1, tid2, tid3, tid4; if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid3, NULL, thread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid4, NULL, thread_proc4, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if ((result = pthread_join(tid4, NULL)) != 0) exit_sys_errno("pthread_join", result); pthread_rwlock_destroy(&g_rwlock); return 0; } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } void *thread_proc1(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 12345; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread1 ENTERS to critical section for READING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread1 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); } return NULL; } void *thread_proc2(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 23456; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread2 ENTERS to critical section for READING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread2 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); } return NULL; } void *thread_proc3(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 35678; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_wrlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread3 ENTERS to critical section for WRITING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread3 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_rdlock", result); } return NULL; } void *thread_proc4(void *param) { int result; int seedval; seedval = (unsigned int)time(NULL) + 356123; for (int i = 0; i < 10; ++i) { usleep(rand_r(&seedval) % 300000); if ((result = pthread_rwlock_wrlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_rdlock", result); printf("thread4 ENTERS to critical section for WRITING...\n"); usleep(rand_r(&seedval) % 300000); printf("thread4 EXITS from critical section...\n"); if ((result = pthread_rwlock_unlock(&g_rwlock)) == -1) exit_sys_errno("pthread_rwlock_rdlock", result); } return NULL; } >> Atomik İşlemler: "thread" ler dünyasında atomiklik demek bir işlemin kesilmeden tek parça halinde yapılmasına denkmektedir. Genel olarak makina komutları atomiktir. Yani bir makina kodu çalıştırılırken kesilme olmaz, dolayısıyla "thread" ler arası geçiş oluşamaz. Çünkü bu geçiş donanım kesmeleri ile sağlanmaktadır ve donanım kesmeleri de ancak makine komutları arasında etkili olabilmektedir. Pekiyi iki işlemci ya da çekirdek aynı "global" değişkeni aynı anda değiştirmek isterse ne olur? Normal şartlarda hangi işlemci ya da çekirdek geç kalmışsa, onun ürettiği değer geçerli olacaktır. Fakat özel bazı durumlarda ilgili değişkende bozulmalar meydana gelebilmektedir. Aşağıdaki makina kodunu düşünelim; INC g_count, 1 Bu komut çalışırken "thread" ler arasında geçiş oluşmayacak olsa da Intel marka işlemci ya da çekirdek, bu işlem sırasında bellek-işlemci arasındaki "bus" hattını tutup işlemi atomik yapmamaktadır. Bu tarz işlemlerde Intel işlemciler, "bus" ı tutup bırakabilmektedir. Yani kesikli bir biçimde çalışmaktadır. İşte tam bu anda başka bir işlemci ya da çekirdek "bus" ın kontrolünü alarak oraya bir şeyler yazarsa, evvelki işlemci bu yeni değeri alacaktır. Bu da bozuk değerin üretilmesine yol açacaktır. Fakat bunun olma olasılığı çok düşüktür. Ancak aşağıdaki gibi bir durumda meydana gelebilir; /* Birinci çekirdek */ MOV g_val, 100 /* İkinci çekirdek */ MOV g_val, 200 Buradaki "g_val" içerisinde "100" ya da "200" olması normal karşılanırken, bozuk bir değerin olması istenmeyen bir durumdur. İşte Intel işlemcilerde, buradaki "g_val" değişkeni belleğe uygun şekilde hizalanmamışsa, bozuk bir değer üretilebilmektedir. Diğer yandan işlemciler bu biçimdeki erişimlerde ilgili makina komutunun sonuna kadar "bus" ın tutulması için de olanak sağlamaktadır. Örneğin, Intel işlemcilerinde komutun başına eklenen "LOCK" deyimi sayesinde ilgili "bus" sonuna kadar tutulacaktır: LOCK INC g_val, 1 Öte yandan bu "LOCK" deyimi komutu yavaşlatmaktadır. Dolayısıyla varsayılan durumda bu deyim kullanılmaz. Eğer tek makina komutu yerine aynı işi yapan üç makina komutu kullansak nasıl olur? Şöyleki; MOV reg, g_val INC reg, 1 MOV g_val, reg Tabii bu durumda da bu makina kodları arasında "thread" ler arası geçiş meydana gelebilir. Bu da ilgili değişkende bozulmaya yol açacaktır. Biz bu bozulmayı önlemek için de Kritik Kod bölgesi oluşturmalıyız. Şimdi buradan da görüleceği üzere bir işi yapmak için tek bir makina kodunu çalıştırmakla birden fazla makina kodunu çalıştırmak arasında avantaj ve dezavantaj bulunmaktadır. Pekiyi bizler hangi durumda hangi yaklaşımı seçmeliyiz? Burada C ile yazarken arada "inline assembly" kodu da yazabiliriz. Bu kullanım derleyiciye özgüdür. Ancak "inline assembly" yazmak hem zahmetli hem de makine dili bilmeyi gerektirir. Bir diğer alternatif ise şudur: Bazı C derleyicilerinde "built-in" ya da "intrinsic" fonksiyon denilen bir kavram vardır. Bu tip fonksiyonlar öyle fonksiyonlardır ki derleyiciler bu fonksiyonların ne yaptığını kafadan bilmektedir. C standartları ile bir alakası yoktur. Derleyici bu fonksiyonların çağrıldığı yerlerde fonksiyon çağrısı yerine direkt olarak kodu yerleştirmektedir. Böylelikle tek bir makina kodu ile bir iş yapacağız ve bu makine kodunun başına da "LOCK" getirilecek. Böylelikle hem "thread" ler arasında hem de "bus" alma/bırakma sırasında gerçekleşebilecek sorunların önüne geçmiş olacağız. Dolayısıyla bizler bir değişkenin değerini bir arttırma işini "multi-processor" işlemcilerde "mutex" kullanarak yapmak yerine ya "Inline Assembly" kullanmalı ya da derleyicilerin sunduğu "built-in"/"intrinsic" fonksiyonları kullanmalıyız. C11 ile birlikte C diline de "atomic" niteleyicisi eklenmiştir ki bu niteleyici sayesinde bizler ne "Inline Assembly" ne de ilgili "built-in"/"intrinsic" fonksiyonları kullanmaya gerek kalmamıştır. C++ dilinde ise "atomic" şablon sınıfı kullanılmaktadır. Şimdi buradaki durumu özetlersek; -> Tek bir makine komutu çalıştırılken "bus" hattının alınıp bırakılması sırasında, ilgili "bus" hattı başka işlemci tarafından alınırsa, değişkenimizin değeri bozulabilir. Bunu engellemek için ilgili makine kodunun başına "LOCK" deyimini eklemeliyiz. -> Birden fazla makine komutu çalıştırılken, komutlar arasındaki geçiş sırasında, "thread" ler arası geçiş meydana gelebilir. Bunu engellemek için de "mutex" nesneleri ile Kritik Kod bölgesi de oluşturabiliriz. -> Bu iki sıkıntıyı gidermek için ya "Inline Assembly" ya derleyicilerin sunduğu "built-in" fonksiyonlar ya da C11 ile dile eklenen "atomic" niteleyicisini kullanmalıyız. Böylelikle yukarıdaki iki problemin de önüne geçmiş olacağız. Fakat UNUTMAMALIYIZ Kİ HER İŞLEM ATOMİK YAPILAMAZ. > İşlemci Tasarımları: İşlemciler iki ana sınıfa ayırabiliriz. Bunlar "CISC" ve "RISC" isimli sınıflardır. Aslında bu sınıflar birer spekturumdur. Dolayısıyla işlemciler, iki ucu bu sınıf olan bir spekturumun herhangi bir yerinde olabilir. Pekiyi bu iki sınıfın arasındaki farkılıklar nelerdir? Şöyleki; >> "CISC" işlemciler çok sayıda makina kodu barındırır. Bu komutların bazıları bir takım karmaşık işlemleri gerçekleştirmektedir. Halbuki "RISC" işlemcileri az sayıda makina kodu barındırır, böylelikle daha etkin çalışabilsin. >> "CISC" işlemcilerin makina kodları değişik uzunluklarda olabilmektedir. Öte yandan "RISC" işlemcilerinki aynı uzunluktadır. Dolayısıyla "CISC" işlemcilerinin makina kodlarının çalışma süresi değişkenlik gösterirken, "RISC" işlemcilerinin komutları genellikle aynı çalışma sürelerine sahiptir. >> "CISC" işlemcilerde genel amaçlı az sayıda "register" içerir. Fakat "RISC" işlemciler genel amaçlı çok sayıda "register" içerir. >> "CISC" işlemcilerindeki "pipeline" işlemcileri, "RISC" işlemcilerindeki kadar etkin YAPILAMAMAKTADIR. >> "CISC" işlemcileri doğrudan bellek üzerinde işlem yapan makina kodlarına sahiptir. Fakat "RISC" işlemcileri ise "load/store" şeklinde çalışmaktadır. Yani bellek üzerinde doğrudan işlem yapılmaz, önce işlemcinin "register" larına çekilir. >> "CISC" işlemcilerinin makina komutlarında iki operand bulunur. Dolayısıyla işlem sonucunda, işleme giren operandlardan birisinin değeri bozulmaktadır. Halbuki "RISC" işlemcilerinin makine komutları 3 operand bulunur ki bu üçüncü operand işlemin sonucunun yazılacağı operanddır. >> "CISC" işlemcileri daha çok güç harcama eğilimindeyken, "RISC" işlemcileri daha az güç harcama eğilimindedir. Günümüzde "RISC" işlemcilerinin tasarımının daha iyi bir tasarım olduğu kabul edilmektedir. Fakat Intel gibi firmalar, çok yaygın işlemci kullandığı için hala "CISC" tasarımını devam ettirmektedir. > İşlemci Mimarileri: Bugün çok işlemcili ve çok çekirdekli sistemlerde, işlemci-bellek bağlantısı olarak, iki farklı mimari kullanılmaktadır. Bunlar "SMP" ve "NUMA" isimli mimarilerdir. >> "SMP" : "Symmetric Multiprocessor" olarak da geçer. Bu mimaride işlemciler ya da çekirdekler aynı belleğe erişmektedir. Dolayısıyla iletişim çakışmasını engellemek için, bir işlemci ya da çekirdek belleğe eriştiğinde diğerleri beklemede kalmaktadır. Bu yüzdendir ki bu mimaride işlemci ya da çekirdek sayısı arttıkça performans da düşmeye başlayacaktır. Tabii bu mimaride her işlemci ya da çekirdek kendine ait özel bir "cache" sistemine sahiptir. İlk önce bu "cache" sistemine, sonrasında belleğe başvurulmaktadır. Bu yüzdendir ki "Cache Consistancy" donanımsal olarak da sağlanmaktadır. Günümüzde yaygın olarak bu mimari kullanılmaktadır. >>> "Cache Consistancy" : Bir işlemci ya da çekirdek kendisine ait "cache" ye bir şey yazdığında, belleğin o bölgesi de "cache" içerisinde mevcutsa ve o bölge başka bir işlemci ya da çekirdeğin "cache" sinde de varsa, diğer işlemci ya da çekirdeğe ait olan "cache" de tazelenmektedir. >> "NUMA" : "Non-Unified Memory Access" olarak da geçer. Bu mimaride her çekirdek ya da işlemcinin, bellekte bağımsız olarak erişebileceği ayrı bir "bank" i vardır. Her işlemci ya da çekirdek, kendi "bank" bölümüne hızlı erişirken diğer işlemci ya da çekirdeklerin "bank" bölgelerine yavaş erişir. Yani herkesin tuttuğu kendinedir. Bu nedenle işlemci ve çekirdeklerin belleğe erişim süreleri eriştikleri lokasyona bağlı olarak değişiklik göstermektedir. Günümüzde daha az kesim tarafından bu mimari kullanılmaktadır. Örneğin, Intel firmasının sunuculara yönelik ürettiği "Xeon" marka işlemciler. > Hatırlatıcı Notlar: >> "Processor Effinity" : Bir programın işlemcinin hangi çekirdeğine atanacağına karar verilmesidir. /*================================================================================================================================*/ (64_08_07_2023) > "Threads in POSIX" (devam): >> Atomik İşlemler (devam): Anımsanacağı üzere bazı C derleyicileri "built-in" ya da "intrinsic" fonksiyonlara sahiptir. Bu fonksiyonlar özel fonksiyonlar olup, derleyiciler tarafından ne yaptığı bilinmektedir. Bu tip fonksiyonlardan bazıları "macro" gibi açılırken bazıları açılmamaktadır. Yine bu fonksiyonların herhangi bir "prototipi" de bulunmamaktadır. Örneğin, aşağıdaki kodu inceleyelim: //... for(int i = 0; i < strlen(s); ++i){ //... } //... Normal şartlarda "strlen" fonksiyonu standart bir C fonksiyonudur ve derleyici bu fonksiyonun ne yaptığını normal şartlarda bilemediği için ilgili "for-loop" için herhangi bir optimizasyon uygulayamaz. Dolayısıyla döngünün her turunda "strlen" fonksiyonuna bir çağrı yapar. Fakat o derleyicide "strlen" fonksiyonu aynı zamanda "built-in" / "intrinsic" fonksiyonsa, burada bir optimizasyon yapılabilir. Pekiyi bu fonksiyonların neler olduğuna nasıl ulaşabiliriz? "gcc" derleyicileri için şu bağlantıyı kullanabiliriz; "https://gcc.gnu.org/onlinedocs/gcc/x86-Built-in-Functions.html". Bu listedeki fonksiyonlardan bazıları "atomic" işlemler için bulundurulmaktadır. Bu fonksiyonların isimleri ilk başlarda "__sync_" ön eki alırken, C++ diline "atomic" kütüphanesinin eklenmesi ile birlikte "__atomic_" ön eki almaktadır. Dolayısıyla artık bu ön eki alanların kullanılması tavsiye edilmektedir. Öte yandan "__atomic_" ön ekine sahip fakat "Memory Model" parametreli bazı fonksiyonlar vardır. Böylesi fonksiyonlara parametre olarak "__ATOMIC_SEQ_CST" değerini geçmeliyiz fakat bu değerin ne olduğunu, "Memory Model" ile neyin kastedildiğine bu kursta değinilmemektedir. Bu fonksiyonlardan bazıları şunlardır; "__atomic_fetch_add", "__atomic_store", "__atomic_load" vb. Şimdi de bu fonksiyonları kabaca inceleyelim: >>> "__atomic_fetch_add" : Bir değişkenin değerini değiştirmek için kullanılır. * Örnek 1, Aşağıdaki örnekte bir değişkenin değeri bir arttırılmıştır. #include int g_count = 0; int main(int argc, char *argv[]) { /* # OUTPUT # g_count : 0 g_count : 1 */ printf("g_count : %d\n", g_count); __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); printf("g_count : %d", g_count); return 0; } * Örnek 2, Aşağıdaki örnekte ise "mutex", "spinlock" nesneleri ve "built-in" fonksiyonun kullanımı karşılaştırılmıştır: #include #include #include #include #include #define MAX_COUNT 10000000 void* pthread_spin_proc1(void* param); void* pthread_spin_proc2(void* param); void* pthread_mutex_proc1(void* param); void* pthread_mutex_proc2(void* param); void* pthread_builtIn_proc1(void* param); void* pthread_builtIn_proc2(void* param); void spin_lock(void); void mutex_lock(void); void built_in_ones(void); void exit_sys_errno(const char* msg, int eno); pthread_spinlock_t g_spinlock; pthread_mutex_t g_mutex; int g_count; int main(void) { /* # OUTPUT # Ok... 20000000, in 1.886349 seconds Ok... 40000000, in 1.951004 seconds Ok... 60000000, in 0.656282 seconds */ spin_lock(); mutex_lock(); built_in_ones(); return 0; } void* pthread_spin_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_spin_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_spin_lock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_spin_unlock(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_mutex_proc1(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_mutex_proc2(void* param) { int result; for(int i = 0; i < MAX_COUNT; ++i) { if((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_lock", result); ++g_count; if((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_spin_unlock", result); } return NULL; } void* pthread_builtIn_proc1(void* param) { for(int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void* pthread_builtIn_proc2(void* param) { for(int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void spin_lock(void) { double start, end; int result; start = clock(); if((result = pthread_spin_init(&g_spinlock, 0)) != 0) exit_sys_errno("pthread_spin_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_spin_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_spin_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_spin_destroy(&g_spinlock)) != 0) exit_sys_errno("pthread_spin_destroy", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void mutex_lock(void) { double start, end; int result; start = clock(); if((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_mutex_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_mutex_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void built_in_ones(void) { double start, end; int result; start = clock(); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_builtIn_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_builtIn_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } >>> "__atomic_store" : Bir değişkene değer atamak için kullanılır. Bu fonksiyonun birde sonuna "_n" eki alan versiyonu da vardır ki bu versiyon değeri doğrudan almaktadır. >>> "__atomic_load" : Bir değişkenin değerini temin etmek için kullanılır. Yine bu fonksiyonunda "_n" li versiyonu bulunmaktadır. Unutmamalıyız ki bu tip fonksiyonlar derleyiciye bağlı fonksiyonlardır. Böylesi "atomic" işlemler için taşınabilirliği sağlamak adına ilgili programlama diline "atomic" işlemleri yapabilme özelliği eklenmiştir. C dili söz konusu olduğunda, C11 standardı ile birlikte dile "thread" kavramı da eklendiği için, "__Atomic" anahtar sözcüğü de eklenmiştir. Bu anahtar sözcük bir "type qualifier" olup, tıpkı "const" ve "volatile" anahtar sözcükleri gibidir. Öte yandan "_Atomic" anahtar sözcüğü "type specifier" olarak parantezle birlikte de kullanılabilmektedir. Aşağıda her iki kullanıma da bir örnek verilmiştir; _Atomic int g_count = 0; _Atomic(int) g_count = 0; C11 ile birlikte ayrıca "stdatomic.h" başlık dosyası da C standartlarıan eklenmiştir. Bu dosya içerisinde çeşitli "atomic" fonksiyonların prototipleri, "_Atomic" ile oluşturulmuş bazı "typedef" isimleri de bulunmaktadır. Artık derleyicilere özgü "built-in" / "intrinsic" fonksiyonlar yerine, "_Atomic" anahtar kelimesini kullanabiliriz. * Örnek 1, Aşağıdaki örnekte "built-in" kullanımı ve "_Atomic" kullanımı karşılaştırılmıştır. #include #include #include #include #include #define MAX_COUNT 10000000 void* pthread_builtIn_proc1(void* param); void* pthread_builtIn_proc2(void* param); void* pthread_atomic_proc1(void* param); void* pthread_atomic_proc2(void* param); void built_in_ones(void); void atomic_ones(void); void exit_sys_errno(const char* msg, int eno); int g_Count; _Atomic int g_count; int main(void) { /* # OUTPUT # Ok... 20000000, in 0.522390 seconds Ok... 20000000, in 0.071704 seconds */ built_in_ones(); atomic_ones(); return 0; } void* pthread_builtIn_proc1(void* param) { for(int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void* pthread_builtIn_proc2(void* param) { for(int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void* pthread_atomic_proc1(void* param) { for(int i = 0; i < MAX_COUNT; ++i) ++g_Count; return NULL; } void* pthread_atomic_proc2(void* param) { for(int i = 0; i < MAX_COUNT; ++i) ++g_Count; return NULL; } void built_in_ones(void) { double start, end; int result; start = clock(); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_builtIn_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_builtIn_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void atomic_ones(void) { double start, end; int result; start = clock(); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, pthread_atomic_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, pthread_atomic_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); end = clock(); printf("Ok... %d, in %f seconds\n", g_count, (double)(end - start) / CLOCKS_PER_SEC); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); } Son olarak C11 ile dile eklenen bu "atomic" konusu "optional" olarak bırakılmıştır. Bu da demektir ki her C derleyicisi bu konuyu desteklemeyebilir. Örneğin, gömülü sistemler söz konusu olduğunda ilgili derleyici destek vermeyebilir. C++ dilinde ise "atomic" isminde bir şablon sınıf mevcuttur. C++11 ile dile eklenmiştir. Kullanımı aşağıdaki gibidir: std::atomic g_count = 0; Sadece "g_count" değişkeninin değerini ekrana yazdırırken "int" türüne dönüştürmeliyiz. Böylelikle "atomic" sınıfının "operator int()" fonksiyonuna çağrı yapılsın. >> "threads" ve "fork" / "exec" işlemi: Anımsayacağımız üzere birden fazla "thread" e sahip bir proseste "fork" işlemi yapıldığında hayata gelen alt proses her zaman tek bir "thread" e sahip olur. Bu "thread" ise "fork" çağrısını yapan "thread" dir. Örneğin, 10 adet "thread" oluşturalım ve bunlardan sadece bir tanesi "fork" çağrısı yapmış olsun. Artık bu çağrıyı yapan üst proses olurken, çağrı sonucunda hayata gelen ise alt proses olacaktır. Her ne kadar bu süreç sırasında üst prosesin bellek alanı alt prosese kopyalansada, alt proseste sadece bir adet "thread" akışı olacaktır. Bu da üst prosesin akışıdır. Fakat şunu da belirtmek gerekirki "fork" işlemi ile "thread" lerin bir arada kullanmaktan kaçınmalıyız. * Örnek 1, Aşağıdaki örnekte de görüleceği üzere "main thread" içerisinde iki adet "thread" meydana gelmesine rağmen, tek bir akış yeniden oluşmuştur. Toplamda üç adet akış vardır. #include #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # thread - 1: 0 Child Process terminated. thread - 2: 0 Parent Process thread - 1: 1 thread - 2: 1 thread - 1: 2 thread - 2: 2 */ int result; pthread_t tid1, tid2; pid_t pid; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((pid = fork()) == 1) exit_sys("fork"); if(pid != 0){ printf("Parent Process\n"); } else{ printf("Child Process terminated.\n"); pthread_exit(NULL); } if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int i; for(i = 0; i < 3; ++i){ printf("thread - 1: %d\n", i); sleep(1); } return NULL; } void *thread_proc2(void *param) { int i; for(i = 0; i < 3; ++i){ printf("thread - 2: %d\n", i); sleep(1); } return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise "thread" lerden sadece bir tanesi "fork" yapmaktadır. Yine toplamda üç adet "thread" akışı vardır. #include #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # thread - 1: 0 thread - 2: 0 thread - 1: 1 thread - 2: 1 thread - 1: 2 thread - 2: 2 thread - 1: 3 thread - 2: 3 thread - 2: 4 thread - 1: 4 thread - 1: 4 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int i; pid_t pid; for(i = 0; i < 5; ++i){ printf("thread - 1: %d\n", i); if(i == 3) if((pid = fork()) == -1) exit_sys("fork"); sleep(1); } if(pid != 0 && waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return NULL; } void *thread_proc2(void *param) { int i; for(i = 0; i < 5; ++i){ printf("thread - 2: %d\n", i); sleep(1); } return NULL; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } Bir diğer yandan şunu da söylemek gerekir ki bir prosesin son "thread" i sonlandığı vakit, işletim sistemi tarafından sonlandırılır. Öte yandan birden çok "thread" in bulunduğu ortamlarda ve bir "thread" in "fork" işlemi yapması durumunda, üst ve alt proseslerde bazı işlerin yapılması gerekmektedir. Bunu gerçekleştirebilmek için "pthread_atfork" fonksiyonunu kullanmamız gerekmektedir. >>> "pthread_atfork" : Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)); Fonksiyonun üç parametresi de bir fonksiyon göstericisidir. "prepare" isimli parametreye geçilen fonksiyon "fork" işlemi yapılmadan evvel üst proses tarafından, "parent" ise "fork" işleminden sonra üst proses tarafından ve "child" ise "fork" işleminden sonra alt proses tarafından çağrılmaktadır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Pek tabii bu fonksiyon da birden fazla kez çağrılabilir. Bu durumda en sonki çağrıya bağlı olan fonksiyonlar ilk olarak çağrılacaktır. * Örnek 1, Aşağıdaki örnekte ID değerlerinin aynı olmasının sebebi, "fork" işlemi sırasında "ID" bilgisinin de üst proses olan "thread" den kopyalanmasıdır. #include #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void prepare(void); void parent(void); void child(void); void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # thread - 1: 0 thread - 2: 0 thread - 1: 1 thread - 2: 1 thread - 1: 2 thread - 2: 2 thread - 1: 3 void prepare(void) : 140110887732800 thread - 2: 3 void parent(void) : 140110887732800 void child(void) : 140110887732800 thread - 2: 4 thread - 1: 4 thread - 1: 4 */ int result; pthread_t tid1, tid2; if((result = pthread_atfork(prepare, parent, child)) != 0) exit_sys_errno("pthread_atfork", result); if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void *thread_proc1(void *param) { int i; pid_t pid; for(i = 0; i < 5; ++i){ printf("thread - 1: %d\n", i); if(i == 3) if((pid = fork()) == -1) exit_sys("fork"); sleep(1); } if(pid != 0 && waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); return NULL; } void *thread_proc2(void *param) { int i; for(i = 0; i < 5; ++i){ printf("thread - 2: %d\n", i); sleep(1); } return NULL; } void prepare(void) { printf("void prepare(void) : %llu\n", (unsigned long long)pthread_self()); } void parent(void) { printf("void parent(void) : %llu\n", (unsigned long long)pthread_self()); } void child(void) { printf("void child(void) : %llu\n", (unsigned long long)pthread_self()); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } Bütün bunlara ek olarak birden çok "thread" in kullanıldığı uygulamalarda "fork" yaparken senkronizasyon nesnelerine de dikkat etmeliyiz. Çünkü "fork" sırasında başka bir "thread" ilgili senkronizasyon nesnesini kilitleyebilir. Bu durumda hayata gelen alt prosesin senkronizasyon nesnesi kilitli olacaktır. Eğer alt proseste bu senkronizasyon nesnesi kullanılmak istenirse, zaten kilitli olduğu için, "deadlock" oluşacaktır. Çünkü kiliti ancak kilitleyen açacaktır. Örneğin, bir "thread" aşağıdaki gibi bir Kritik Kod bölgesi oluştursun: void thread_proc1(){ //... pthread_mutex_lock(&g_mutex); // => Bir başka "thread", bu "thread" in akışı tam da buradayken, "fork" çağrısı yapsın. pthread_mutex_unlock(&g_mutex); //... } "fork" çağrısı yapan "thread" ilgili "mutex" nesnesini kilitlememiştir fakat "fork" çağrısından dolayı bellek alanı kopyalancağı için ilgili "mutex" nesnesi de kopyalanacaktır. Fakat kilitli bir biçimde kopyalanacaktır. Artık alt prosesteki bu "mutex" nesnesinin açılmasının bir olanağı yoktur. İşte böylesi bir senaryoda "pthread_atfork" fonksiyonu yararlı olabilir. Şöyleki; -> "fork" işleminden evvel, "prepare" argümanına geçilen fonksiyon içerisinde, kullanılan "mutex" nesnesinin kilidi açılabilir. Böylelikle bu "mutex" nesnesi alt prosese kilidi açık bir biçimde geçecektir. -> Üst proses olarak nitelenen diğer "thread" ler ilgili "mutex" nesnesini tekrardan kilitleyebilir. Yani, yukarıda belirtilen noktaya dikkat etmeliyiz. Eğer "fork" işleminden hemen sonra "exec" işlemi yapmak, sadece "fork" yapmaya nazaran, daha az problemlidir. Benzer şekilde "fork" yapmadan sadece "exec" yapmamız halinde, üst prosesteki halihazırdaki diğer "thread" ler sonlanır ve yeni proses tek bir "thread" ile hayata gelir. Yine bu durumda da prosesin bellek alanının gittiğini unutmayınız. Özetle birden fazla "thread" in olduğu ortamlarda şu noktalara dikkat etmeliyiz: -> Eğer amacımız "exec" ise "fork" ve "exec" yapmakta bir problem yoktur. Fakat "exec" yapmayacakak, sadece "fork" yapmak kötü bir tekniktir. -> "fork" işleminden sonra alt proseste sadece bir "thread" olur. -> "fork" yapmadan direkt olarak "exec" yaparsak, o ana kadarki bütün "thread" ler sonlandırılı. Sadece "exec" çağrısı yapan hayatta kalır ki bu da çağrıyı yapabilsin. > Hatırlatıcı Notlar: >> "volatile" : "const" ve "volatile" niteleyicileri şu bağlamda birbiriyle eştir; "non-const" bir değişkenin adresini "const" bir değişkene atayamadığımız gibi, "non-volatile" bir değişkenin adresini de "volatile" bir değişkene atayamayız. Benzer şekilde nesnemiz gösterici ise hem kendisi hem de gösterdiği yer hem "const" hem de "volatile" olabilir. Buna "cv-qualifier" denir. "volatile" ise şu anlama gelir; bir nesne eğer "volatile" ise bir başka akış tarafından nesnenin değeri değiştirilebilir. Eğer bir göstericisi "volatile" ise, yani kendisi "volatile" değil sadece gösterdiği yer "volatile", her defasında gösterdiği yere gidip taze kopyasını almaktadır. >> "atomic_t" : O nesne üzerinde yapılacak işlemlerin tek bir makina komutu ile yapılmasını sağlatır. /*================================================================================================================================*/ (65_09_07_2023) ve (66_16_07_2023) > "Threads in POSIX" (devam): >> "thread" lerin çizelgelenmesi: İşletim sistemlerinde proseslerin ve "thread" lerin CPU'ya atanıp zaman paylaşımlı olarak çalıştırılmasına proseslerin çizelgelenmesi denmektedir. "thread" ler olmadan evvel çizelgelenen şeyler proseslerin kendisiydi fakat "thread" lerin gelmesi ile birlikte artık "thread" ler çizelgelenmektedir. İşletim sisteminin "thread" leri çizelgelemekte kullandığı algoritmalara ise adı üzerinde Çizelgeleme Algoritmaları denmektedir. Tabii tarih boyunca çeşitli algoritmalar çeşitli işletim sistemlerinde geliştirilmeye çalışılmıştır. Böylelikle "interactivity" ve "throughput" arasında bir denge sağlanmaya çalışılmıştır. Günümüzde en yaygın kullanılan çizelgeleme algoritması ise "Round Robin Scheduling" denilen Döngüsel Çizelgeleme algoritmasıdır. >>> "Round Robing Scheduling" : Bu çizelgeleme algoritmasında "thread" ler bir Çalışma Kuyruğunda tutulur ve sırası gelen CPU'ya atanır. "quanta" süresi bittikten sonra zorla CPU'dan kopartılır ve yerine çalışma kuyruğunda bulunan sıradaki "thread" atanır. Bir döngü içerisinde bu mekanizma işletilir. CPU'da çalıştırılan "thread" bloke olursa ilgili "thread" çalışma kuyruğundan çıkartılır ve Bekleme Kuyruğuna alınır. Blokenin kalkması durumunda ilgili "thread" tekrardan çalışma kuyruğuna alınır ki bu kuyruklar arasındaki işlem genellikle Kesme Mekanizması ile uygulanır. * Örnek 1, "sleep" fonksiyonunu ele alalım. Bu çağrıyı yapan "thread" ilk başta CPU'ya atanır. Programın akışı "sleep" çağrısına geldiğinde CPU'dan kopartılır ve "sleep" için oluşturulan bekleme kuyruğuna alınır. Pekiyi işletim sistemi sürenin tamamlandığını nasıl anlamaktadır? Bu noktada devreye yine Zaman Kesmeleri("Timer Interrupts") girmektedir. Linux sistemlerinde bir bir milisaniyede bir zaman kesmesi oluşur ve işletim sisteminin kodu devreye girer. Buna "jiffy" de denir. Devreye giren bu kod "sleep" için oluşturulan bekleme kuyruklarını da kontrol etmekte, süresi bitenleri tekrardan çalışma kuyruklarına aktarmaktır. * Örnek 2, bir "thread" bir soketten okuma yapmak istesin ancak ilgili sokete henüz bilgi gelmemiş olsun. İşte işletim sistemi bu "thread" i bloke edip, çalışma kuyruğundan çıkarmakta ve ilgili bekleme kuyruğuna eklemektedir. Eğer sokete bilgi gelirse, yine bir kesme oluşur. Bu da gelen bilginin işletim sistemi tarafından alınmasına neden olur. Böylece bilginin gelmesini bekleyen "thread" uyandırılıp çalışma kuyruğuna yerleştirilir. Örneklerden de görüleceği üzere bir "t" anında çalışma kuyruklarında belli bir adet "thread" bulunurken, bekleme kuyruklarında da belli bir adet "thread" blokeli haldedir. Tabii buradan da diyebiliriz ki her bir olay için özel bir bekleme kuyruğu oluşturulmaktadır. Şunu da belirtmekte fayda vardır ki bu çizelgeleme algoritması da birden çok varyasyona sahiptir. Örneğin, Windows sistemleri Öncelik Sınıfı Temelinde Döngüsel Çizelgeleme ismindeki halini kullanırken Linux sistemleri ilk başlarda özel bir isim verilmeyen fakat yine döngüsel çizelgeleme algoritması kullanmaktadır. 2.6 nolu sürümden sonra O(1) Çizelgelemesi, 2.6.23 nolu sürüm ile birlikte "Completely Fair Scheduling" ismindeki çizelgelemeyi kullanmaktadır. UNIX türevi sistemlerde ise proseslerin çizelgeleme politikaları ("Process Scheduling Policy") bulunmaktadır. Bu politikalardan bazıları "SCHED_OTHER" ve "SCHED_NORMAL" ismindeki politikalardır. Her proses bir çizelgeleme politikasına sahiptir. POSIX standartlarınca proseslerin çizelgeleme politikaları şunlardan birisi olabilir: "SCHED_FIFO", "SCHED_RR", "SCHED_OTHER" ve "SCHED_SPORADIC. Bu çizelgelerden "SCHED_SPORADIC" olan opsiyonel olarak bırakılmıştır ve Linux sistemlerinde desteklenmemektedir. Yine POSIX standartlarınca "SCHED_FIFO" ve "SCHED_RR" olanlarına Gerçek Zamanlı Çizelgeleme Politikaları da denmektedir ve açıkça POSIX standarlarında tanımlanmışlardır. Fakat bunlar tam manası ile gerçek zamanlı değillerdir. Yani bu politikaları takip eden prosesler, gerçek zamanlı işletim sistemlerindeki gibi davranmayacaktır. Öte yandan "SCHED_OTHER" ise "implementation defined" olarak bırakılmıştır ve POSIX standartlarında varsayılan politika budur. Ayrıca o işletim sisteminde "SCHED_OTHER" politikası "SCHED_FIFO" veya "SCHED_RR" olarak da implemente edilebilir, POSIX standartları buna izin vermektedir. Diğer yandan prosesin çizelgeleme politikası "fork" işlemi sırasında üst prosesten alınmaktadır. Linux sistemlerinde varsayılan politika "SCHED_OTHER" biçimindedir (Linux dünyasında bu politikaya "SCHED_NORMAL" da denmektedir fakat "SCHED_NORMAL" politikası POSIX standartlarında geçmemektedir). Yine POSIX standartlarınca bir prosesin çizelgeleme politikası, onun bütün "thread" lerinin de politikasıdır. Dolayısıyla bir prosesin çizelgeleme politikasını değiştirisek, o prosesin bütün "thread" lerinin çizelgeleme politikası da değişecektir. FAKAT Linux SİSTEMLERİ BUNA UYMAMAKTADIR. Linux SİSTEMLERİNDE SADECE "main thread" ÇİZELGELEME POLİTİKASI DEĞİŞMEKTEDİR. Bir prosesin çizelgeleme politikasının ne olduğu bilgisi, Proses Kontrol Bloğu içerisinde saklanmaktadır. Şimdi de bu politikaları inceleyelim: >>> "SCHED_OTHER" : POSIX standartlarınca o işletim sistemini yazanlarına bırakılmıştır. Fakat POSIX standartları, bu politikayı kullananlar proseslerin CPU kullanma miktarları konusunda etkili olabilecek bir kavram da tanımlamıştır ki bu kavram "nice" değeridir. Bu "nice" değerini açıklamadan evvel "SCHED_OTHER" politikasının ne anlam ifade ettiğine değinelim; bu politika kabaca şu yönergeleri içermektedir: -> İşletim sistemi, çalışma kuyruğundaki her bir "thread" için bir "quanta" sayaç değeri tutar. Linux sistemlerinde bu değer "task_struct" yapısı içerisinde saklanır (Anımsayacağımız üzere Linux sistemlerinde "thread" ler de tıpkı prosesler gibi "task_struct" yapısı ile temsil edilirler). Örneğin, bu sayaç değerinin 200 olduğunu varsayalım. -> Bir "thread" CPU'ya atandıktan sonra her bir Zaman Kesmesi olduğunda, ona ait "quanta" sayaç değeri bir eksiltilir. Günümüzdeki bu zaman kesmesi bir milisaniye olduğundan, her bir milisaniyede bu değer bir eksilecektir. Tıpkı 199, 198... -> Bu "quanta" sayaç değeri sıfıra düştüğünde, "thread" ler arası geçiş meydana gelir ve ilgili "thread" CPU'dan kopartılır. Daha sonra çalışma kuyruğundaki diğer "thread" CPU'ya atanır. Genel olarak "quanta" sayaç değeri daha büyük olan "thread" CPU'ya önce atanır. Örneğin, çalışma kuyruğunda toplam beş adet "thread" olsun. Bunların "quanta" sayaç değerleri de sırasıyla 18, 89, 0, 120, 5 biçiminde olsun. Bu durumda, "thread" ler arası geçiş meydana geldiğinde, "quanta" sayaç değeri 120 olan "thread" CPU'ya atanacaktır. -> Çalışma kuyruğundaki bütün "thread" lerin "quanta" sayaç değerleri sıfıra düştükten sonra sayaç değerleri tekrardan doldurulmaktadır. Dolayısıyla "quanta" sayacı sıfır olan "thread" ler, diğer "thread" lerin de sayaçları sıfır olana dek CPU'ya atanmamaktadır. -> Bütün bu işleyiş sırasında, "quanta" sayaç değerinin ne olduğuna bakılmaksızın, bir "thread" bloke olursa çalışma kuyruğundan çıkartılır ve ilgili bekleme kuyruğuna alınır. Artık işletim sistemi bu "thread" i tekrardan CPU'ya atamak için çizelgelemez. Ancak işletim sistemi, çalışma kuyruklarındaki bütün "thread" lerin "quanta" sayaçları bittikten sonra ilgili sayaçlara doldurma yaparken, bekleme kuyruklarındakileri de göz önüne alır. Fakat bekleme kuyruklarındakilere doldurma yaparken daha fazla doldurma yapmaktadır. Böylelikle ilgili "thread" in uyanması durumunda, çalışma kuyruğundaki diğer "thread" lerin "quanta" sayaçlarından daha fazla değere sahip olacaktır. Böylesi bir yaklaşımın daha adil olacağı düşünülmektedir. Pekiyi bütün bunlar göz önüne alındığında, bir "thread" in "quanta" sayacının doldurma işlemi sırasında alacağı değer ne olacaktır? İşte burada "thread" in "nice" değeri devreye girmektedir. Bu değer ile orantılı olacak bir biçimde "quanta" sayaç değerleri doldurulmaktadır. Fakat POSIX standartları bu "nice" değerinin etkisinin tam olarak nasıl olacağını belirtmemiştir. Bu değerin detayları sistemden sisteme değişiklik gösterebilir. Son olarak şunu da belirtmekte fayda vardır ki yukarıda anlatılanlar ilgili politikanın kaba halidir, ayrıntılarına değinilmemiştir. Şimdi de bu "nice" değerini inceleyelim: >>>> "nice" değeri : POSIX standartlarınca bu değer sıfır ile "2*NZERO-1" değeri arasında bir değer almaktadır. Linux sistemlerinde "NZERO" değeri "20" olduğu için "nice" değeri [0, 39] değerleri arasında bir değer alacaktır. Bu değerin rakamsal olarak büyük olması daha düşük önceliğe sahip olunacağını belirtmektedir. Bu "NZERO" değeri ise UNIX türevi sistemler için orta noktayı temsil etmektedir. Pekiyi bizler bu "nice" değeri ile nasıl oynama yapabilir miyiz? Burada "setpriority" / "getpriority" ve "nice" / "renice" isimli POSIX fonksiyonları devreye girmektedir. >>>>> "setpriority" / "getpriority" : Fonksiyonların prototipi aşağıdaki gibidir: #include int getpriority(int which, id_t who); int setpriority(int which, id_t who, int value); Fonksiyonların birinci parametresi olan "which" parametresi, şu değerlerden birisi olabilir: "PRIO_PROCESS", "PRIO_PGRP" ve "PRIO_USER". -> "PRIO_PROCESS" : Yalnızca tek bir prosesin "nice" değerinin temin etmek / değiştirmek için. -> "PRIO_PGRP" : Bir proses grubundaki bütün proseslerin "nice" değerini temin etmek / değiştirmek için. -> "PRIO_USER" : Belli bir Etkin Kullanıcı ID değerine ilişkin bütün proseslerin "nice" değerini temin etmek / değiştirmek için. Fonksiyonların ikinci parametresi olan "who" parametresi, "which" isimli parametreye göre, sırasıyla ilgili prosesin Proses ID, Process Grup ID ve Ekin Kullanıcı ID değeri olmaktadır. Bu nedenden dolayı "id_t" isimli bir tür "typedef" edilmiştir ki bu ise arka planda "pid_t" ve "uid_t" türlerinin her ikisini de ifade edebilecek bir türdür. Bu parametreye "0" değerini geçmemiz halindeyse, "which" parametresi yerine çağrıyı yapan prosesinkiler baz alınacaktır. Fonksiyonun üçüncü parametresi olan "value" ise aslında "nice" değeri olup, "NZERO" değeri ile toplama anlamındadır. Dolayısıyla bu parametreye "10" geçmemiz, "NZERO" değeri de "20" ise, "nice" değeri "30" olacaktır. Bu durumda ilgili "thread" in önceliği düşecektir. Bu parametreye "-10" geçmemiz halinde ise yeni "nice" değeri "10" olacaktır, eğer "NZERO" da "20" ise. Bu durumda ilgili "thread" in önceliği yükselmiş olacaktır. Ancak POSIX standartlarına göre, bu üçüncü parametreye geçeceğimiz değerler sonucunda "nice" değeri "[0,39]" aralığından çıkıyorsa fonksiyon başarısız olmaz. Bu aralıktaki minimum ya da maksimum değerini alır. "setpriority" fonksiyonunun geri dönüş değeri ise başarı durumunda "0", hata durumunda ise "-1" değerine geri dönmektedir. "getpriority" fonksiyonu ise başarı durumunda "NZERO" değerine, hata durumunda "-1" değerine geri dönmektedir. Fakat bu fonksiyonunun "-1" değeri döndürmesi iki anlamlıdır; ya gerçekten de başarısızlık olundu ya da "NZERO" değeri olarak "-1" geri döndürüldü. Bunu ayırt etmek için de aşağıdaki gibi bir yöntem uygulanabilir: errno = 0; if((result = getpriority(...)) == -1 && errno != 0) exit_sys("getpriority"); Öte yandan bizler herhangi bir prosesin "nice" değerini elde edebiliriz, bu konuda bir erişim kontrolü uygulanmamaktadır. Dİğer yandan biz "getpriority" fonksiyonu ile bir proses grubunun ya da belli bir etkin kullanıcı ID değerine ilişkin proseslerin "nice" değerlerini elde ederken hangi prosesinkini elde etmiş olacağız? POSIX standartlarınca en düşük "nice" değerini bize vermektedir, yani önceliği en yüksek olanınki. Ancak "setpriority" fonksiyonu ile bir prosesin "nice" değerini düşürmek istiyorsak, prosesimiz "priviledged" olması gerekmektedir. Eğer yükseltmek istiyorsak, prosesimizin Kullanıcı ID değerleri ile hedef prosesin Kullanıcı ID değerleri aynı olmalıdır. Özetle; -> Bizler herhangi bir prosesin "nice" değerini yükselterek onun daha az CPU zamanı harcamasını sağlayabilmemiz için hedef prosesin Etkin Kullanıcı ID değeri ile bizim Etkin ya da Gerçek Kullanıcı ID değeri ile aynı olması gerekmektedir. Başka bir deyişle sadece kendi proseslerimizin "nice" değerini yükseltebiliriz. -> Herhangi bir prosesin "nice" değerini düşürmek için prosesimizin "priviledged" olması gerekmektedir. Burada prosesimizin "root" ya da "CAP_SYS_NICE" yeterliliğine sahip olması gerekmektedir. Yine "setpriority" fonksiyonu ile bir prosesin "nice" değerini değiştirirsek, POSIX standartlarınca, o prosesin bütün "thread" lerinin "nice" değeri değişecektir. Fakat Linux sistemlerinde ise sadece "main-thread" inki değişecektir. Bu fonksiyonu çağıran "thread" başka bir "thread" oluşturursa, yine bu "nice" değeri de aktarılacaktır. Aşağıda bu fonksiyonların kullanımına ilişkin örnekler verilmiştir: * Örnek 1, #include #include #include #include #include void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # Priority : 0 Priority : 10 */ int result; errno = 0; if((result = getpriority(PRIO_PROCESS, 0)) == -1 && errno != 0) exit_sys("getpriority"); printf("Priority : %d\n", result); if(setpriority(PRIO_PROCESS, 0, 10)) exit_sys("setpriority"); result = getpriority(PRIO_PROCESS, 0); printf("Priority : %d\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # Priority : 0 setpriority: Permission denied */ int result; errno = 0; if((result = getpriority(PRIO_PROCESS, 0)) == -1 && errno != 0) exit_sys("getpriority"); printf("Priority : %d\n", result); if(setpriority(PRIO_PROCESS, 0, -1)) exit_sys("setpriority"); errno = 0; if((result = getpriority(PRIO_PROCESS, 0)) == -1 && errno != 0) exit_sys("getpriority"); printf("Priority : %d\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte kendi prosesimizin "nice" değerini önce yükseltmek, sonra da düşürmek istedik. #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char** argv) { /* # Command Line Arguments # ./sample 0 1 ./sample 0 -1 */ /* # OUTPUT # Success.. setpriority: Permission denied */ if(argc != 3){ fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } pid_t pid = (pid_t)atol(argv[1]); int prio = atoi(argv[2]); if(setpriority(PRIO_PROCESS, pid, prio) == -1) exit_sys("setpriority"); printf("Success...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>>> "nice" fonksiyonu: Sadece kendi prosesinin "nice" değerini değiştirmektedir. Prototipi aşağıdaki gibidir. #include int nice(int incr); Bu fonksiyon parametre olarak aldığı değeri, o anki "nice" değeri üzerine eklemektedir. POSIX standartlarına göre, prosesin bütün "thread" leri üzerinde etkili olmaktadır. Ancak Linux sistemlerinde ise sadece bu fonksiyonu çağıran "thread" inkini değiştirmektedir. "setpriority" fonksiyonunda olduğu gibi, bir "thread" başka bir "thread" oluştururken, "nice" değeri de aktarılmaktadır. Yine başarı durumunda yeni "nice" değerini döndürmekte, hata durumunda ise "-1" ile geri dönmektedir. Fakat yeni "nice" değerinin "-1" olması durumunda yine "-1" ile döneceği için yukarıdaki gibi "errno" değişkenini kullanarak hata kontrolü yapmalıyız. * Örnek 1, Bu program, notların devamında işlenecek olan "nice" kabuk komutunda kullanılmıştır. #include #include #include #include #include #include void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # OUTPUT # Priority : 0 Priority : -2 Priority : -4 */ int result; errno = 0; if((result = nice(0)) == -1 && errno != 0) exit_sys("nice"); printf("Priority : %d\n", result); errno = 0; if((result = nice(-2)) == -1 && errno != 0) exit_sys("nice"); printf("Priority : %d\n", result); errno = 0; if((result = nice(-2)) == -1 && errno != 0) exit_sys("nice"); printf("Priority : %d\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki program ise "nice" komutu ile birlikte ve komut olmadan çalıştırılmıştır. #include #include #include #include #include #include int set_nice_value(int new_value); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int err); int main(void) { /* # "shell" # sudo ./sample sudo nice --5 ./sample */ /* # OUTPUT # New Nice Value: 5 Old Nice Value: 0 New Nice Value: 0 Old Nice Value: -5 */ int old_value = set_nice_value(5); printf("Old Nice Value: %d\n", old_value); return 0; } int set_nice_value(int new_value) { int old_value; errno = 0; if((old_value = nice(0)) == -1 && errno != 0) exit_sys("nice"); errno = 0; int result; if((result = nice(new_value)) == -1 && errno != 0) exit_sys("nice"); printf("New Nice Value: %d\n", result); return old_value; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } Diğer yandan akla şu gelmektedir; nadem "nice" ve "setpriority" fonksiyonları bir prosesin "main-thread" i üzerinde etkili olmaktadır, belli bir "thread" in "nice" değerini POSIX standartlarına uygun biçimde nasıl değiştirebiliriz? Anımsayacağınız üzere Linux sistemlerinde her bir "thread" bir "task_struct" yapısına sahiptir. Dolayısıyla prosesin ID değeri "task_struct" yapısı içerisinde saklandığından, "thread" e özgüdür. Diğer yandan yine "task_struct" yapısı içerisindeki "thread_group" isimli bağlı liste, "main-thread" e ilişkin bütün "thread" lerin "task_struct" yapılarını tutmaktadır. Pekiyi bir "thread" e ilişkin olan proses ID değerine nasıl ulaşacağız? İşte bunun için "Linux-Specific" olan "gettid()" isimli fonksiyonu çağırmalıyız. Hangi "thread" içerisinde çağrılmışsa, onun ID değerini döndürecektir. "getpid" gibi POSIX fonksiyonu ise "main-thread" e ilişkin ID değerini döndürecektir. Dolayısıyla bir "thread" in "nice" değerini değiştirmek için ilk önce "gettid()" ile onun ID değerini almalı, bu değeri "setpriority()" fonksiyonuna geçmeliyiz. Özetle; -> Linux sistemlerinde her bir "thread" için ayrı bir "task_struct" yapısı bulunmaktadır. Dolayısıyla her bir "thread" in ayrı bir ID değeri vardır. -> "main-thread" e ilişkin "task_struct" içerisindeki ID değeri, prosese ilişkin ID değeri olarak kullanılmaktadır. POSIX fonksiyonu olan "getpid" fonksiyonunu çağırırsak, prosese ilişkin ID değerini elde ederiz. -> Prosesin "main-thread" i içerisinde, o prosese ilişkin bütün "thread" lerin "task_struct" yapıları da tutulmaktadır. Dolayısıyla bu yapı kullanılarak bütün "task_struct" yapılarına erişilebilir. -> Her "task_struct" içerisinde bir ID varsa, belli bir "thread" in ID değerini ise bir Linux fonksiyonu olan "gettid" fonksiyonunu çağırmalıyız. Hangi "thread" içerisinde çağrılmışsa, ona ait olan ID değeri elde edilecektir. Şimdi de "shell" komutlarını kullanarak bir prosesin "nice" değerini değiştirmeyi, okumayı öğrenelim. >>>>> "ps" komutu: Proseslerin "nice" değerini görmek için kullanılır. Bu komut "proc" dosya sisteminden bilgileri almaktadır. Bu komutun kullanımı ise şöyledir; -> Sadece "ps" olarak çalıştırılırsa, içinde bulunulan terminalde ve kullanıcıya ilişkin prosesler kısa biçimde listelenir. Şöyleki: $ ps PID TTY TIME CMD 16 pts/0 00:00:00 bash 249 pts/0 00:00:00 ps -> "-l" seçeneği ile birlikte "ps -l" biçiminde kullanırsak, içinde bulunulan ve kullanıcıya ilişkin prosesler hakkında daha fazla bilgiler elde edeceğiz. Şöyleki: $ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 1000 16 15 0 80 0 - 1551 do_wai pts/0 00:00:00 bash 0 R 1000 250 16 0 80 0 - 1869 - pts/0 00:00:00 ps -> "-u" seçeneği ile birlikte "ps -u ahmopasa" biçiminde kullanırsak, "ahmopasa" ya ait bütün terminallerde çalışan prosesler hakkında bilgi verir. Şöyleki: $ ps -u ahmopasa PID TTY TIME CMD 16 pts/0 00:00:00 bash 108 pts/1 00:00:00 sh 109 pts/1 00:00:00 sh 114 pts/1 00:00:00 sh 118 pts/1 00:00:12 node 129 pts/1 00:00:00 node 142 pts/2 00:00:03 node 151 pts/3 00:00:06 node 175 pts/1 00:00:34 node 190 pts/1 00:00:10 node 219 pts/1 00:00:00 node 251 pts/0 00:00:00 ps Eğer bu seçeneği "-l" ile birlikte kullanırsak, detaylı bir şekilde sonuç elde etmiş olacağız. Şöyleki: $ ps -lu ahmopasa F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 1000 16 15 0 80 0 - 1551 do_wai pts/0 00:00:00 bash 4 S 1000 108 107 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 109 108 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 114 109 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 118 114 0 80 0 - 233954 - pts/1 00:00:12 node 0 S 1000 129 118 0 80 0 - 157259 - pts/1 00:00:00 node 4 S 1000 142 141 0 80 0 - 147383 do_epo pts/2 00:00:03 node 4 S 1000 151 150 0 80 0 - 146765 do_epo pts/3 00:00:06 node 0 S 1000 175 118 0 80 0 - 239904 - pts/1 00:00:34 node 0 S 1000 190 118 0 80 0 - 209607 - pts/1 00:00:10 node 0 S 1000 219 175 0 80 0 - 148137 - pts/1 00:00:00 node 0 R 1000 252 16 0 80 0 - 1869 - pts/0 00:00:00 ps -> "-t" veya "-tty" seçeneğini "ps -t pts/1" biçiminde kullanırsak, sadece o terminalde çalışan prosesler hakkında bilgi ediniriz. Şöyleki: $ ps -t pts/1 PID TTY TIME CMD 108 pts/1 00:00:00 sh 109 pts/1 00:00:00 sh 114 pts/1 00:00:00 sh 118 pts/1 00:00:12 node 129 pts/1 00:00:00 node 175 pts/1 00:00:34 node 190 pts/1 00:00:10 node 219 pts/1 00:00:00 node Yine bu seçeneği de "-l" ile birleştirip kullanabiliriz. Şöyleki: $ ps -l -t pts/1 F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 4 S 1000 108 107 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 109 108 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 114 109 0 80 0 - 721 do_wai pts/1 00:00:00 sh 0 S 1000 118 114 0 80 0 - 233954 - pts/1 00:00:12 node 0 S 1000 129 118 0 80 0 - 157259 - pts/1 00:00:00 node 0 S 1000 175 118 0 80 0 - 239904 - pts/1 00:00:35 node 0 S 1000 190 118 0 80 0 - 209607 - pts/1 00:00:10 node 0 S 1000 219 175 0 80 0 - 148137 - pts/1 00:00:00 node -> "-o" seçeneğini kullanarak, ekrana yazdırılan sütunların sıralamasını değiştirebiliriz. -> "-T" seçeneğini kullanırsak, o prosese ait bütün "thread" leri ekrana yazdırmış olacağız. >>>>> "top" komutu: Bu komutunu çalıştırarak, proseslerin kullandığı kaynakları anlık bir şekilde görüntüleyebiliriz. Şöyleki: $ top top - 01:31:05 up 4:01, 0 users, load average: 0.00, 0.00, 0.00 Tasks: 21 total, 1 running, 20 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.0 us, 0.1 sy, 0.0 ni, 99.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st MiB Mem : 12664.5 total, 12095.1 free, 278.7 used, 290.7 buff/cache MiB Swap: 4096.0 total, 4096.0 free, 0.0 used. 12142.8 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 175 ahmopasa 20 0 962052 95960 35924 S 0.7 0.7 0:58.73 node 190 ahmopasa 20 0 838428 50348 32844 S 0.3 0.4 0:17.67 node 1 root 20 0 928 532 476 S 0.0 0.0 0:00.06 init 14 root 20 0 1276 376 20 S 0.0 0.0 0:00.00 init 15 root 20 0 1276 376 20 S 0.0 0.0 0:00.42 init 16 ahmopasa 20 0 6204 5036 3288 S 0.0 0.0 0:00.30 bash 106 root 20 0 1016 116 20 S 0.0 0.0 0:00.00 init 107 root 20 0 1016 116 20 S 0.0 0.0 0:00.00 init 108 ahmopasa 20 0 2884 944 856 S 0.0 0.0 0:00.00 sh 109 ahmopasa 20 0 2884 948 856 S 0.0 0.0 0:00.00 sh 114 ahmopasa 20 0 2884 928 836 S 0.0 0.0 0:00.00 sh 118 ahmopasa 20 0 936072 72460 36160 S 0.0 0.6 0:19.91 node 129 ahmopasa 20 0 629036 50052 33072 S 0.0 0.4 0:01.16 node 140 root 20 0 1016 116 20 S 0.0 0.0 0:00.00 init 141 root 20 0 1016 116 20 S 0.0 0.0 0:03.44 init 142 ahmopasa 20 0 589532 44760 30684 S 0.0 0.3 0:06.83 node 149 root 20 0 1016 116 20 S 0.0 0.0 0:00.00 init 150 root 20 0 1016 116 20 S 0.0 0.0 0:06.51 init 151 ahmopasa 20 0 588512 43360 30604 S 0.0 0.3 0:12.92 node 219 ahmopasa 20 0 592548 47572 32448 S 0.0 0.4 0:00.79 node 448 ahmopasa 20 0 7788 3664 3068 R 0.0 0.0 0:00.00 top "q" tuşuna basarak da bu komutu sonlandırabiliriz. >>>>> "nice" komutu : Eğer bir programı belli bir "nice" değeri ile çalıştırmak istiyorsak, bu komutunu kullanabiliriz. Örneğin, $ sudo nice -5 ./sample şeklinde bir komut çalıştırdığımız zaman, Priority : 5 Priority : 3 Priority : 1 şeklinde bir sonuç elde etmekteyiz. Eğer, $ sudo ./sample şeklinde bir komut çalıştırırsak da , Priority : 0 Priority : -2 Priority : -4 şeklinde bir sonuç elde edeceğiz. Görüleceği üzere ilgili program "5" "nice" değeri ile çalıştırılmıştır. Eğer negatif bir değer ile çalıştırılmasını istiyorsak aşağıdaki biçimde kullanmalıyız: $ sudo nice --5 ./sample Bu durumda aşağıdaki biçimde bir çıktı elde edeceğiz: Priority : -5 Priority : -7 Priority : -9 Burada kullanılan "sample" programı, "nice" POSIX fonksiyonunun açıklandığı kısımdaki örnek programdır. >>>>> "renice" komutu : Arka planda "setpriority" fonksiyonunu kullanarak, belli bir prosesin "nice" değerini değiştirmektedir. Bunun için ilgili prosesin halihazırda çalışıyor olması gereklidir. Örneğin, $ renice -1 -p 6827 komutunu çalıştırdığımız zaman "PID" değeri 6827 olan prosesin "nice" değeri "1" azaltılacaktır. Tabii biz burada tek çekirdekli işletim sistemlerini baz alarak değerlendirme yaptık. Birden çok çekirdeğin bulunduğu sistemlerde çoğu işletim sistemi, her bir çekirdeğin çalışma kuyruğunu birbirinden ayırmaktadır. Tabii bir çekirdeğin çalışma kuyruğundaki "thread" ler azalırsa, diğer çalışma kuyruğundaki "thread" lerin bu kuyruğa aktarılabilir. Örneğin, Linux'un "O(1)" çizelgeleme algoritmasında tek bir çalışma kuyruğu bulunmakta, işi biten CPU'lara atamalar bu tek kuyruktan yapılmaktaydı. Günümüzde bu yaklaşımı LCW mağazalarındaki kasa kuyrukları örnek gösterilebilir. Fakat Linux'taki "CFS" algoritması ile birlikte her çekirdek için ayrı bir çalışma kuyruğu oluşturulmakta, gerektiği durumlarda kuyruk arasında "thread" aktarımı gerçekleşmektedir. > Hatırlatıcı Notlar: >> "libgen.rs" internet istesinden PDF dosyalarına ulaşabiliriz. >> CPU'ya atanan "thread" in zorla kopartılmasına İngilizce'de "preemptive" denir. >> "task_struct" içerisinde saklanan "quanta" sayacı, aslında "quanta" süresidir. Her ne kadar daha önceki derslerde bu değer için tipik olarak 60 milisaniye olduğunu belirtsek de "nice" değeri gibi faktörlere bağlı olarak değişmektedir. Buradaki "nice" değerinin yüksek olması, diğerlerine karşı nazik olunacağı anlamına gelmektedir. Bundan dolayıdır ki öncelik olarak diğerlerinin daha yüksek önceliği olacaktır, CPU kullanımı açısından. >> "sudo" şifresi "1". >> Gerek "nice" gerek "setpriority" fonksiyonları sadece "main-thread" in "nice" değerini değiştirmektedir. Dolayısıyla bir "thread" oluşturduktan sonra "nice" değerini değiştirirsek, sadece fonksiyonu çağıranınki değişecektir. Velev ki oluşturmadan önce "nice" değerini değiştirseydik, bu değer oluşturulan "thread" e aktarılacaktı. Tabii bu durum Linux için geçerlidir çünkü POSIX standartlarınca bütün "thread" ler etkilenecektir. >> Linux sistemlerinde "thread" ler için de "task_struct" yapısı kullanılmaktadır. Dolayısıyla "thread" lerin de bir ID değeri bulunmaktadır. Dolayısıyla bir prosesin ID değerini almak istediğimiz zaman, o prosesin "main-thread" inin ID değerini elde etmiş olmaktayız. POSIX standartları bu biçimdeki "task_struct" kullanımını desteklememektedir. /*================================================================================================================================*/ (67_22_07_2023) > "Threads in POSIX" (devam): >> "thread" lerin çizelgelenmesi (devam): >>> "SCHED_OTHER" (devam): >>>> "nice" değeri (devam): Şimdi de "gettid()" fonksiyonunu inceleyelim: >>>>> "gettid" fonksiyonu: Bir Linux fonksiyonudur. Prototipi aşağıdaki gibidir. #define _GNU_SOURCE #include pid_t gettid(void); Fonksiyon başarısız olamamaktadır. Başarı durumunda ise kendisini çağıran "thread" in ID değerine geri dönmektedir. Bu fonksiyonu kullanırken kaynak kodun en üst kısmına "_GNU_SOURCE" makrosunu bildirmeliyiz. Buna alternatif olarak "-D _GNU_SOURCE" biçimindeki komut satırını da kullanabiliriz: * Örnek 1, #define _GNU_SOURCE #include #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # Process ID : 14715 Main-Thread ID : 14715 Thread-I ID : 14719 */ printf("Process ID : %jd\n", (intmax_t)getpid()); printf("Main-Thread ID : %jd\n", (intmax_t)gettid()); pthread_t tid; int result; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param){ pid_t tid; tid = gettid(); printf("Thread-I ID : %jd\n", (intmax_t)tid); return NULL; } void exit_sys(const char* msg){ perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki programda ise spesifik bir "thread" in ID değeri "setpriority" fonksiyonu ile değiştirilmiştir. Fakat bu davranışın Linux sistemlere özgü olduğunu unutmayınız. #include #include #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # Main-Thread Priority(old): 0 Thread Priority(old): 0 Thread Priority(new): 10 Main-Thread Priority(old): 0 */ int prio; errno = 0; if((prio = getpriority(PRIO_PROCESS, getpid())) == -1 && errno != 0) exit_sys("getpriority"); printf("Main-Thread Priority(old): %d\n", prio); pthread_t tid; int result; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); errno = 0; if((prio = getpriority(PRIO_PROCESS, getpid())) == -1 && errno != 0) exit_sys("getpriority"); printf("Main-Thread Priority(old): %d\n", prio); return 0; } void* thread_proc(void* param){ pid_t tid; tid = gettid(); int prio; errno = 0; if((prio = getpriority(PRIO_PROCESS, tid)) == -1 && errno != 0) exit_sys("getpriority"); printf("Thread Priority(old): %d\n", prio); if(setpriority(PRIO_PROCESS, tid, 10) == -1) exit_sys("setpriotity"); errno = 0; if((prio = getpriority(PRIO_PROCESS, tid)) == -1 && errno != 0) exit_sys("getpriority"); printf("Thread Priority(new): %d\n", prio); return NULL; } void exit_sys(const char* msg){ perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Yine burada unutmamalıyız ki POSIX standardına göre sadece bir adet "thread" ID değeri bulunmaktadır ki bu da aslında "main-thrad" e ilişkin olup, prosesin ID değeri olarak da geçmektedir. Dolayısıyla bu ID değerini kullanarak "nice" değerini değiştirdiğimizde POSIX standartlarına göre ilgili prosesteki bütün "thread" ler, Linux sistemlerinde ise o ID değerine sahip spesifik "thread" in "nice" değeri değişmiş olacaktır. >>> "SCHED_FIFO" ve "SCHED_RR" : Varsayılan çizelgeleme algoritmaları değildir fakat ikisi de prosese özgüdür. Tabii POSIX standartlarına göre bir prosesin çizelgeleme algoritması bunlardan birisi seçildiğinde, yine bütün "thread" lerinin de çizelgeleme algoritması etkilenmektedir. Ancak POSIX, belli bir "thread" in çizelgeleme algoritmasının bunlardan birisi olarak seçilmesini mümkün kılmaktadır. Diğer yandan bu iki çizelgeleme politikası, "SCHED_OTHER" politikasından daha baskındır. Yani o an sistemde bu politika ile çalışabilecek bir "thread" varsa, bloke olmadıkça veya sonlanmadıkça, "SCHED_OTHER" olanlar CPU'ya atanmazlar. Öte yandan "SCHED_FIFO" veya "SCHED_RR" çizelgeleme algoritmasına sahip "thread" ler, "Statik Öncelik (Static Priority)" kavramına sahiptir. Bu kavram, "SCHED_OTHER" da bulunan "nice" değerinden farklıdır. POSIX standartları bu "static priority" kavramının alt ve üst limitlerinin ne olması gerektiği konusunda bir belirlemede bulunmamış fakat bu değerlerin programın çalışma zamanında elde edilebilmesi için şu iki fonksiyonu bulundurmaktadır: "sched_get_priority_max" ve "sched_get_priority_min". >>>> "sched_get_priority_min" ve "sched_get_priority_max" : Fonksiyonların prototipi aşağıdaki gibidir: #include int sched_get_priority_max(int policy); int sched_get_priority_min(int policy); Fonksiyon parametre olarak çizelgeleme politikasının ismini, geri dönüş değeri olarak da "static priority" değerinin azami ve asgari değerlerini döndürmektedir. Başarısızlık durumunda ise "-1" değeri ile geri dönmektedirler. Eğer fonksiyona parametre olarak "SCHED_OTHER" değil, "SCHED_FIFO" veya "SCHED_RR" olarak girilmelidir. Aksi halde sonuç "implementation defined" olmaktadır. Diğer yandan "static priority" değeri sıfırdan küçük OLAMAMAKTADIR. * Örnek 1, Aşağıdaki örnekten de görüleceği üzere "SCHED_FIFO" ve "SCHED_RR" algoritmaları aynı "Static Priority" seviyesindedir. #include #include #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # SCHED_FIFO : [1,99] SCHED_RR : [1,99] */ int prio_min, prio_max; if((prio_min = sched_get_priority_min(SCHED_FIFO)) == -1) exit_sys("sched_get_priority_min"); if((prio_max = sched_get_priority_max(SCHED_FIFO)) == -1) exit_sys("sched_get_priority_max"); printf("SCHED_FIFO : [%d,%d]\n", prio_min, prio_max); if((prio_min = sched_get_priority_min(SCHED_RR)) == -1) exit_sys("sched_get_priority_min"); if((prio_max = sched_get_priority_max(SCHED_RR)) == -1) exit_sys("sched_get_priority_max"); printf("SCHED_RR : [%d,%d]\n", prio_min, prio_max); return 0; } void exit_sys(const char* msg){ perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Burada bahsi geçen "Static Priority" değerinden küçük değere sahip olan düşük öncelikli, büyük değere sahip olan ise yüksek önceliklidir. Bütün bunlara ek olarak, bu iki çizelgeleme algoritmasında "Öncelik Sınıfı(Priority Class)" dayalı bir çizelgeleme kuyruğu oluşturulmaktadır. Her bir "Static Priority" değerine sahip "thread" ler kendi içerisinde sıralanmaktadır. Örneğin, bu değeri 10, 10, 15, 20, 30 olan beş farklı "thread" için toplamda dört adet kuyruk bulundurulmaktadır. Dolayısıyla bir sistemde bu "Static Priority" değerler [1,99] arasında ise 99 farklı kuyruk bulundurulmaktadır. İşte değeri yüksek olan kuyruktakiler ilk olarak CPU'ya atanmaktadır. Dolayısıyla yüksek öncelikli varken düşük öncelikli CPU'ya atanmayacaktır. >>>> "SCHED_FIFO" : Bu çizelgelemeye sahip, aynı "Static Priority" değerine sahip bir grup "thread" olsun. Buradaki çizelgeleme şunlara göre yapılmaktadır: -> Kuyruğun en başındaki "thread" CPU'ya atanır ve "quanta" süresine bakılmaksızın sürekli olarak çalıştırılır. Ancak biterse veya bloke olursa CPU'dan kopartılır. -> Bloke olan iş bu "thread" in blokesi kalktığında, kendi kuyruğunun en sonuna yerleştirilir. Örneğin, elimizde aşağıdaki "Static Priority" değerlerine sahip 6 adet "thread" imiz olsun: T1(10), T2(10), T3(10), T4(10), T5(12), T6(12) Daha sonra bu "thread" ler aşağıdaki gibi kuyruk haline getirilir: Q1 ---> T5(12), T6(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Burada dikkat etmemiz gereken husus "Static Priority" değeri yüksek olanlar daha öncelikli bir kuyrukta toplanırken, düşük olan ise daha düşük öncelikli kuyrukta toplanmıştır. İşte bu notkada CPU'ya ilk olarak "T5" isimli "thread" atanacaktır çünkü hem "Static Priority" değeri yüksek kuyrukta hem de kuyruğun en başında. Artık "quanta" süresine bakılmaksızın bu "thread" sürekli çalışacaktır. Ancak bu "thread" bloke olursa veya tamamlanırsa, "T6" isimli "thread" atanacaktır ve aynı şeyler bunun için de geçerlidir. "T6" da tamamlandıktan sonra veya bloke olursa, sıra "T1" e gelecektir çünkü kuyruğun başında o vardır. İşte bu mekanizma içerisinde bloke olan ve blokesi çözülen olursa, kendi kuyruğunun en sonuna eklenecektir. Dolayısıyla düşük öncelik kuyruğundaki "thread" ler yine bu kuyruğun sonuna atanan "thread" i bekleyecektir. >>>> "SCHED_RR" : Bu çizelgeleme politikası "SCHED_FIFO" ile çok benzerdir. Aralarındaki tek fark, "SCHED_RR" politikasına sahip olan "thread" ler CPU'da bir "quanta" süresince çalıştırılmasıdır. Halbuki "SCHED_FIFO" olanlar "quanta" süresinden bağımsız bir şekilde çalıştırılmaktaydı. Örneğin, elimizde aşağıdaki "Static Priority" değerlerine sahip 6 adet "thread" imiz olsun: T1(10), T2(10), T3(10), T4(10), T5(12), T6(12) Daha sonra bu "thread" ler aşağıdaki gibi kuyruk haline getirilir: Q1 ---> T5(12), T6(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Yalnız buradaki "quanta" süresi, "SCHED_OTHER" politikasındaki "quanta" süresinden bağımsızdır. O politikadaki süreler, onların "nice" değerine göre değişkenlik gösterirken, buradakilerin süreleri sistem genelinde sabittir. Linux sistemlerinde bu politikaya sahip "thread" lerin "quanta" süreleri 100ms kadardır. Pekiyi bu politikayı benimseyen "thread" ler ilgili "quanta" süresini nasıl temin edebilirler? Bunun için POSIX standartları "sched_rr_get_interval" isminde bir fonksiyon bulundurmaktadır. >>>> "sched_rr_get_interval" : Fonksiyonun prototipi aşağıdaki gibidir. #include int sched_rr_get_interval(pid_t pid, struct timespec *interval); Bizler "SCHED_FIFO" ile "SCHED_RR" politikasına sahip "thread" leri ayrı ayrı inceledik fakat bu "thread" ler bir arada bulunabilirler. Örneğin, aşağıdaki gibi "thread" kümemiz olsun: T1(10 / SCHED_FIFO), T2(10 / SCHED_RR), T3(10 / SCHED_FIFO), T4(10 / SCHED_RR), T5(12 / SCHED_FIFO), T6(12 / SCHED_RR) Bunlar aslında aşağıdaki gibi bir kuyruk oluşturacaklardır: Q1 ---> T5(12), T6(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Burada ilk "T5" isimli "thread" CPU'ya atanacaktır. Kendisi "SCHED_FIFO" politikasına sahip olduğu için ya bloke olana dek ya tamamlanana dek ya da daha yüksek öncelikli "SCHED_FIFO" / "SCHED_RR" bir başka "thread" uyanana kadar CPU'dan kopartılmayacaktır. Örneğimizde bu "thread" in bloke olduğunu ve tekrar uyandırıldığını varsayalım. Dolayısıyla kuyruğumuz aşağıdaki biçimde olacaktır: Q1 ---> T6(12), T5(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Şimdi ise "T6" isimli "thread" CPU'ya atanır ve "quanta" süresince çalıştırılır. Bu süre dolduğunda ise kendi kuyruğunun sonuna alınır. Şöyleki: Q1 ---> T5(12), T6(12) Q2 ---> T1(10), T2(10), T3(10), T4(10) Ve çalışma bu şekilde devam eder. Tabii "Q1" dekilerin hepsinin bloke olduğunda artık "T1" isimli "thread" atanacaktır. Eğer "T1" çalıştırılıken "Q1" dekilerden birisi uyanırsa, "T1" direkt CPU'dan kopartılacaktır da. Bunun sebebi ise daha yüksek öncelikli bir "thread" in uyanmış olmasıdır. Şimdi burada dikkat etmemiz gereken, "SCHED_FIFO" veya "SCHED_RR" politikasına sahip olan "thread" varsa "SCHED_OTHER" politikasını benimseyenler hiç bir zaman çizelgelenmezler. Linux çekirdeğinde ise gerçekleştirimi kolaylaştırmak için "SCHED_OTHER" politikasını benimseyen "thread" ler için, sanki "Static Priority" değerleri varmış ve sıfırmış gibi, bir kuyruk oluşturulur. Çünkü "SCHED_OTHER" politikasını benimseyenlerde "Static Priority" kavramı yoktur. Böylelikle "SCHED_FIFO" / "SCHED_RR" politikasına sahip olanlar "[1,99]" arasında bir "Static Priority" değerine sahip olurken, "SCHED_OTHER" ise "0" değerinde bir "Static Priority" değerine sahip olmuş olur. Pekiyi çok işlemcili ya da çekirdekli sistemlerde yukarıda açıklanan mekanizma nasıl işletilmektedir? POSIX standartları bu konuda bir şey söylememektedir ancak her bir CPU ya da çekirdek diğerlerinden bağımsız bir birim olarak ele alınmaktadır. Örneğin aşağıdaki gibi bir "thread" kümemiz olsun: T1(SCHED_OTHER), T2(10 / SCHED_RR), T3(SCHED_OTHER), T4(12 / SCHED_FIFO) Burada ilk olarak "T2" isimli "thread" boş bir CPU'ya atanacaktır. Eğer diğer CPU'lar da boş ise önce "T1" atanacaktır. Eğer hala boş CPU kalmışsa, geri kalanları da ona atar. Fakat işletim sistemi isterse "T2" ve "T4" isimli "thread" leri bir CPU'ya, "T1" ve "T3" isimli "thread" leri başka bir CPU'ya atayabilir. Özetle birden fazla CPU ya da çekirdek varsa ilk olarak "thread" ler çekirdeklere atanır. Daha sonra her bir CPU ya da çekirdek diğerlerinden bağımsızmış gibi çizelgeleme yapılır. Windows sistemlerindeki çizelgeleme politikası ise buradaki "SCHED_RR" politikasına benzemektedir. Pekiyi bizler hangi durumlarda hangi çizelgeleme politikasını kullanmalıyız? Buradaki "SCHED_FIFO" ve "SCHED_RR" politikaları nispeten "soft real-time" uygulamalarda tercih edilmelidir. Dolayısıyla bir olay gerçekleştiğinde o olayın hemen ele alınmasını gerekiyorsa, bu politikaları tercih edebiliriz. Örneğin, bir ısı sensöründen okuma yapalım ve değer eşik değerini geçtiğinde de bir müdahale gereksin. İşte böylesi durumlar için "SCHED_FIFO" politikasını benimseyen "thread" ler kullanabiliriz. Tabii "SCHED_FIFO" olanların yoğun bir şekilde kullanılması, diğer "thread" lerin çalışamamasına da neden olabilmektedir. Bu nedenle "SCHED_FIFO" olanların bloke olabilmesi arzu edilmektedir. Tabii bir "thread" in yoğun bir işlemi izlemesi de gerekebilir. Bu durumda bu "thread" için "SCHED_FIFO" politikası da benimsenebilir. Böylelike o "thread" sürekli bir biçimde çalışması sağlanabilir. "SCHED_RR" ise bir grup "thread" in kendi aralarında zaman paylaşımlı bir biçimde çalıştırıldığı durumlarda kullanılabilir. Bütün bunlardan sonra şimdi de proses ve "thread" lerin çizelgeleme politikaları ile "SCHED_FIFO" ve "SCHED_RR" politikasını benimseyenlerin "Static Priority" değerleri nasıl değiştirildiğini inceleyelim: "sched_getparam" ve "sched_setparam", "sched_setscheduler" ve "sched_getscheduler", "sched_yield. >>> "sched_getparam" ve "sched_setparam" : Bu fonksiyonlar "SCHED_FIFO" ve "SCHED_RR" proseslerin "Static Priority" değerlerini sırasıyla "get" ve "set" etmek için kullanılır. Bu fonksiyonlar birer POSIX fonksiyonudur. Bu fonksiyonları "SCHED_OTHER" politikasına ilişkin "thread" ler üzerinde kullanmaya ÇALIŞMAMALIYIZ. Yine bir prosesin "Static Priority" değeri değiştirildiğinde, o proses ait bütün "thread" lerin bu değeri değiştirilecektir, tabii "SCHED_FIFO" ve "SCHED_RR" politikasını benimsemişse. Fakat Linux sistemlerde ise sadece "main-thread" bundan etkilenmektedir. Tabii Linux sistemlerinde o "thread" in ID değerini elde edip bu fonksiyonlarda kullanılırsa, o ID değerine sahip olanınki de değiştirilecektir. Fonksiyonların prototipleri aşağıdaki gibidir: #include int sched_getparam(pid_t pid, struct sched_param *param); int sched_setparam(pid_t pid, const struct sched_param *param); Fonksiyonun birinci parametresi, işlemin yapılacağı proses ilişkin ID değeridir. Bu parametreye "0" değerinin geçilmesi durumunda bu çağrıyı yapan "thread" ele alınacaktır. İkinci parametre ise "sched_param" isimli bir yapı türündendir ki bu yapı türü şimdilik tek bir elemana sahiptir. Yapının tanımı ise aşağıdaki gibidir: struct sched_param { ... int sched_priority; ... }; Fonksiyonlar başarı durumunda "0", hata durumunda ise "-1" değerine geri dönmekte ve "errno" uygun değere çekilmektedir. Linux'ta proseslerin "Static Priority" değerlerini "get" etmek için herhangi bir ön koşul gerekmemektedir. Fakat diğer işletim sistemlerinde bu durum farklılık gösterebilir. "set" etmek için yine o prosesin uygun önceliklere sahip olması gerekmektedir. * Örnek 1, Bu fonksiyonu "SCHED_OTHER" prosesler için kullanmamalıyız. Linux, "SCHED_OTHER" prosesler için, "sched_getparam" her zaman "0" değerini döndürmektedir. #include #include #include void exit_sys(const char *msg); int main(void) { /* # OUTPUT # Static Priority: 0 */ struct sched_param sparam; if(sched_getparam(0, &sparam) == -1) exit_sys("sched_getparam"); printf("Static Priority: %d\n", sparam.sched_priority); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, "SCHED_OTHER" politikasını benimseyen prosesler için "sched_setparam" fonksiyonu başarılı olamayacaktır. #include #include #include void exit_sys(const char *msg); int main(void) { /* # OUTPUT # ched_setparam: Invalid argument */ struct sched_param sparam; sparam.sched_priority = 1; if(sched_setparam(0, &sparam) == -1) exit_sys("sched_setparam"); printf("Static Priority: %d\n", sparam.sched_priority); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "sched_setscheduler" ve "sched_getscheduler" : Bir prosesin çizelgeleme politikasını sırasıyla değiştirmek ve almak için kullanılan POSIX fonksiyonlarıdır. Buradaki "sched_getscheduler" yalnızda prosesin çizelgeleme politikasını alırken, "sched_setscheduler" ise hem değiştirmekte hem de "SCHED_FIFO" ve "SCHED_RR" politikasını benimseyenlerin "Static Priority" değerlerini de değiştirmektedir. Fonksiyonların prototipleri şöyledir: #include int sched_getscheduler(pid_t pid); int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param); Fonksiyonların birinci parametresi ilgili prosesin ID değeridir. Bu parametreye sıfır geçilmesi durumunda, fonksiyonu çağıran proses ele alınacaktır. Fonksiyonun üçüncü parametresi ise "SCHED_FIFO" ve "SCHED_RR" olanların "Static Priority" değerini belirtmektedir. Fonksiyonun ikinci parametresi ise çizelgeleme politikasına ait değerdir. Fonksiyonlar başarı durumunda "0", hata durumunda ise "-1" ile geri dönmektedir. Tabii prosesin çizelgeleme politikasını değiştirmek için o prosesin uygun önceliğe sahip olması gerekmektedir. Yine POSIX standartlarına göre bu iki fonksiyon prosesin bütün "thread" leri üzerinde, Linux sistemlerinde ise yalnızca ilgili "thread" üzerinde etkili olmaktadır. * Örnek 1, Aşağıdaki programı çalıştırabilmek için prosesimizin uygun önceliğe sahip olması gerekmektedir. #include #include #include void exit_sys(const char *msg); int main(void) { /* # OUTPUT # sched_setparam: Invalid argument */ struct sched_param sparam; sparam.sched_priority = 10; if(sched_setscheduler(0,SCHED_FIFO, &sparam) == -1) exit_sys("sched_setscheduler"); printf("Static Priority: %d\n", sparam.sched_priority); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (68_23_07_2023) > "Threads in POSIX" (devam): >> "thread" lerin çizelgelenmesi (devam): >>> "sched_yield" : Özellikle "SCHED_FIFO" veya "SCHED_RR" politikasını takip eden "thread" lerin kendi istekleri ile CPU'yu bırakması istenmektedir. Burada ilgili "thread" bloke olmamakta, kendi çalışma kuyruğunun sonuna gönderilmektedir. İşte bu işi yapan fonksiyon, bu fonksiyondur. Fonksiyonun prototipi şu şekildedir: #include int sched_yield(void); Fonksiyon başarı durumunda "0", hata durumunda ise "-1" değerine geri dönmektedir. Fakat POSIX standartlarınca bu fonksiyon için herhangi bir hata durumu tanımlanmamıştır. Linux sistemlerinde bu fonksiyon her zaman başarılı olmaktadır. Şimdiye kadar proses temelinde çalışan çizelgeleme fonksiyonlarını gördük. Anımsanacağınız üzere Linux sistemlerinde bu çizelgeleme algoritmaları yalnızda tek bir "thread" üzerinde etkili olurken, POSIX standartlarınca o prosese ait bütün "thread" ler üzerinde etkili olmaktadır. Pekiyi POSIX standartlarına göre belli bir "thread" in çizelgeleme algoritmasını nasıl değiştirebiliriz? İşte burada "attribute" nesneleri devreye girmektedir. Anımsayacağımız üzere bir "thread" oluşturulmadan evvel oluşturacağımız "attribute" nesnelerini kullanarak, o "thread" e ilişkin bir takım özellikleri değiştirebilmekteydik. Bu amaçla oluşturulan "pthread_attr_getXXX" ve "pthread_attr_setXXX" isimli fonksiyonları kullanmaktaydık. İşte bu amaçla kullanacağımız fonksiyonlar ise "pthread_attr_setinheritsched", "pthread_attr_getschedparam" ve "pthread_attr_setschedparam", "pthread_attr_getschedpolicy" ve "pthread_attr_setschedpolicy" vb. isimli fonksiyonlardır. Bu fonksiyonlar "SCHED_FIFO" ve "SCHED_RR" politikasına sahip olanlar için düşünülmüştür. Öte yandan "SCHED_OTHER" politikasını izleyen spesifik bir "thread" in "nice" değerini değiştirecek POSIX fonksiyonu bulunmamaktadır. >>> "pthread_attr_setinheritsched" : Bir "thread" in çizelgeleme bilgisini değiştirmek için ilk önce bu fonksiyonu çağırarak, ilgili çizelgeleme bilgisini kendisini hayata getiren "thread" den almamasını sağlamalıyız. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched); Fonksiyonun ilk parametresi ilgili "attribute" nesnesinin adresini geçerken, ikinci parametreye şu değerlerden birisini geçmeliyiz: "PTHREAD_INHERIT_SCHED" ve "PTHREAD_EXPLICIT_SCHED". -> "PTHREAD_INHERIT_SCHED" : Çizelgeleme algoritmasını bir üst "thread" den almasını sağlamaktadır. -> "PTHREAD_EXPLICIT_SCHED" : Çizelgeleme algoritmasını bir üst "thread" den almamasını sağlamaktadır. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Pek tabii bu fonksiyonun da "get" eden versiyonu da bulunmaktadır. >>> "pthread_attr_getschedpolicy" ve "pthread_attr_setschedpolicy" : Fonksiyonların prototipleri aşağıdaki gibidir. #include int pthread_attr_getschedpolicy(const pthread_attr_t * attr, int * policy); int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); Bu fonksiyonlar belli bir "thread" için çizelgeleme algoritmasının sırasıyla temin edilmesi ve değiştirilmesi için kullanılmaktadır. Başarı durumunda "0", hata durumunda ise hata koduna kendisine dönmektedir. >>> "pthread_attr_getschedparam" ve "pthread_attr_setschedparam" : Fonksiyonların prototipleri aşağıdaki gibidir. #include int pthread_attr_getschedparam(const pthread_attr_t * attr, struct sched_param * param); int pthread_attr_setschedparam(pthread_attr_t * attr, const struct sched_param * param); Bu fonksiyonlar ise ilgili politikayı izleyen "thread" lerin "Static Priority" değerlerini sırasıyla temin etmek ve değiştirmek için kullanılmaktadır. Fonksiyon, başarı durumunda "0", hata durumunda ise hata koduna kendisine dönmektedir. Tabii burada ilgili "attribute" nesnesi üzerinde işlem yaparken prosesimizin uygun önceliğe sahip olmasına gerek yoktur. Ancak "thread" oluşturma aşamında prosesimizin uygun önceliğe sahip olması gerekmektedir. * Örnek 1, Aşağıdaki örnekte "SCHED_FIFO" çizelgeleme algoritmasına sahip ve "Static Priorit" değeri "10" olan bir "thread" oluşturulmak istenmiştir. #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys_errno(const char* msg, int eno); int main(void) { int result; pthread_attr_t tattr; if((result = pthread_attr_init(&tattr)) != 0) exit_sys_errno("pthread_attr_init", result); if((result = pthread_attr_setinheritsched(&tattr, PTHREAD_EXPLICIT_SCHED)) != 0) exit_sys_errno("pthread_attr_setinheritsched", result); if((result = pthread_attr_setschedpolicy(&tattr, SCHED_FIFO)) != 0) exit_sys_errno("pthread_attr_setschedpolicy", result); struct sched_param sparam; sparam.sched_priority = 10; if((result = pthread_attr_setschedparam(&tattr, &sparam)) != 0) exit_sys_errno("pthread_attr_setschedparam", result); pthread_t tid; if((result = pthread_create(&tid, &tattr, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_attr_destroy(&tattr)) != 0) exit_sys_errno("pthread_attr_destroy", result); printf("Press CTRL+C to exit!..\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param){ pause(); return NULL; } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Bu program çalışırken ikinci bir terminal programı üzerinden, "ps -T --tty pts/2 -o pid, pri, policy, cmd, tid" komutunu çalıştırırsak ki yukarıdaki örnek programımızı "sudo" ile çalıştırmayı unutmamalıyız, aşağıdaki biçimde bir çıktı elde etmiş olacağız: PID PRI POL CMD TID 9251 19 TS sudo ./sample 9251 9252 19 TS ./sample 9252 9252 50 FF ./sample 9253 Burada "PID" ile "TID" değeri "9252" olan aslında "main-thread". Ancak bizim oluşturduğumuz "thread" in ID değeri ise "9253". İlgili ID değerlerine karşılık gelen "POL" sütunundan da görüleceği üzere yeni oluşturulan "thread" in çizelgeleme algoritması değiştirilmiştir. Diğer yandan bir "thread" i hayata varsayılan "attribute" ile getirdikten sonra da çizelgeleme politikası ve "Static Priority" değerini değiştirebiliriz. Bunun için "pthread_getschedparam" ve "pthread_setschedparam" isimli fonksiyonları kullanacağız. >>> "pthread_getschedparam" ve "pthread_setschedparam" : Fonksiyonların prototipleri aşağıdaki gibidir. #include int pthread_getschedparam(pthread_t thread, int * policy, struct sched_param * param); int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param); Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine dönmektedir. Tabii "set" işlemi için de prosesimizin uygun önceliğe sahip olması gerekmektedir. Yine bu fonksiyonlar da "SCHED_FIFO" ve "SCHED_RR" politikasını izleyen "thread" ler için kullanılmalıdır. * Örnek 1, #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys_errno(const char* msg, int eno); int main(void) { int result; struct sched_param sparam; sparam.sched_priority = 10; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_setschedparam(tid, SCHED_FIFO, &sparam)) != 0) exit_sys_errno("pthread_setschedparam", result); printf("Press CTRL+C to exit!..\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param){ pause(); return NULL; } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Bu örnekteki değişimi görebilmek için, program çalışırken başka bir terminale geçip, "ps -T --tty pts/2 -o pid, pri, policy, cmd, tid" komutunu çalıştırmalıyız. Böylelikle aşağıdaki çıktıyı ekranda göreceğiz: PID PRI POL CMD TID 9909 19 TS sudo ./sample 9909 9910 19 TS ./sample 9910 9910 50 FF ./sample 9911 Görüldüğü üzere "9910" ID numaralı olan "main-thread" iken, "9911" olan bizim oluşturduğumuz "thread" dir. Eğer bir "thread" in yalnızca "Static Priority" değerini değiştirmek istiyorsak, "pthread_setschedprio" fonksiyonunu kullanmalıyız. >>> "pthread_setschedprio" : Fonksiyonların prototipleri aşağıdaki gibidir. #include int pthread_setschedprio(pthread_t thread, int prio); Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisinde dönmektedir. Yine bu fonksiyon da "SCHED_FIFO" ve "SCHED_RR" politikasını izleyen "thread" ler için kullanılmalıdır. Bu fonksiyonun "get" versiyonu bulunmamaktadır. * Örnek 1, Aşağıdaki örnekte sonradan oluşturulan "thread" in "Static Priority" değeri ilk olarak 10, daha sonra "20" olarak değiştirilmiştir. #include #include #include #include #include #include void* thread_proc(void* param); void exit_sys_errno(const char* msg, int eno); int main(void) { int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); struct sched_param sparam; sparam.sched_priority = 10; if((result = pthread_setschedparam(tid, SCHED_FIFO, &sparam)) != 0) exit_sys_errno("pthread_setschedparam", result); int policy; if((result = pthread_getschedparam(tid, &policy, &sparam)) != 0) exit_sys_errno("pthread_getschedparam", result); printf("Policy : %d\n", sparam.sched_priority); if((result = pthread_setschedprio(tid, 20)) != 0) exit_sys_errno("pthread_setschedprio", result); if((result = pthread_getschedparam(tid, &policy, &sparam)) != 0) exit_sys_errno("pthread_getschedparam", result); printf("Policy : %d\n", sparam.sched_priority); printf("Press CTRL+C to exit!..\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param){ pause(); return NULL; } void exit_sys_errno(const char* msg, int eno){ fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Tabii bütün bunları özetleyecek olursak; -> "SCHED_OTHER" politikasını izleyen spesifik bir "thread" in "nice" değerini değiştirecek bir POSIX fonksiyonu yoktur. Çünkü "setpriority" ve "getpriority" fonksiyonları POSIX standartlarınca proses temelli çalışmaktadır. Fakat Linux sistemlerinde ise sadece "main-thread" üzerinde etkili olmaktadırlar. Dolayısıyla Linux sistemlerinde, bu iki fonksiyonu kullanarak, bir "thread" in "nice" değerini DEĞİŞTİREBİLİRİZ. FAKAT BUNU POSIX SİSTEMLERİNDE YAPAMAYIZ. -> "SCHED_FIFO" ve "SCHED_RR" politikasını izleyen "thread" lerin çizelgeleme politikasını ve "Static Priority" değerini spesifik bir "thread" için değiştirebiliriz. Yukarıdaki POSIX fonksiyonlara, o "thread" in ID değerini geçerek bunu gerçekleştirebiliriz. >> "Processor Affinity" : Birden çok CPU veya çekirdeğin bulunduğu sistemlerde, bir "thread" in işletim sisteminin isteğine bırakılmaksızın sadece o CPU'ya atanması, çizelgelenmesi durumudur. Buradaki amaç, paralel programlama esnasında "thread" lerin aynı CPU'ya atanmasını engelleyerek eş zamanlı çalışma süresini arttırmaktır. Çünkü işletim sistemleri toplam faydayı göz önüne alarak atama gerçekleştirmektedir. Örneğin, sistemimiz dört çekirdekli bir sistem olsun. Biz de dört "thread" barındıran bir program yazmış olalım. İşletim sistemi bu dört "thread" i de ayrı ayrı çekirdeklere atamayabilir çünkü bu çekirdeklerin "cache" kısımları birbirinden ayrıdır ve aynı "thread" in tekrar aynı çekirdeğe atanması toplam fayda açısından da tercih edilebilir. Ancak programcı bir takım nedenlerden dolayı da belli "thread" lerin belli çekirdeklere atanması isteyebilir ki paralel programlama sekteye uğramasın. Böylelikle "thread" ler farklı çekirdeklere atanarak, onların eş zamanlı çalışmasına olanak sağlamış oluruz. "Processor Affinity" konusu taşınabilir bir konu olmadığından, POSIX standartlarına yansıtılmamıştır. Dolayısıyla bu işler işletim sistemine özgü sistem fonksiyonlarıyla ya da onları sarmalayan kütüphane fonksiyonları ile gerçekleştirilmektedir. "GNU libc" kütüphanesinde, yani Linux sistemlerinde, bu iş için iki adet fonksiyon bulundurulmaktadır. Bunlar "sched_setaffinity" ve "sched_getaffinity" isimli fonksiyonlardır. >>> "sched_setaffinity" ve "sched_getaffinity" : Fonksiyonun prototipi aşağıdaki gibidir: #define _GNU_SOURCE /* See feature_test_macros(7) */ #include int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask); int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask); Fonksiyonların birinci parametresi ilgili "thread" in ID değerini belirtmektedir. Yani bu fonksiyonlara biz, "gettid()" ile elde edilen "thread" e özgü değerleri geçebiliriz. Eğer "0" geçilirse, bu fonksiyonu çağıran "thread" in ID değeri geçilecektir. Fonksiyonların üçüncü parametreleri "cpu_set_t" türündendir ki bu tür arka planda varsayılan durumda 1024 bit'lik bir veri yapısıdır. Bu veri yapısı muhtemelen aşağıdaki biçimde "typedef" edilmiştir. typedef struct { unsigned long bitarray[16]; }cpu_set_t; Fonksiyonların ikinci parametresi ise üçüncü parametdeki ilgili "bit" dizisinin "byte" uzunluğunu belirtmektedir. Tipik olarak bu parametreye, üçüncü parametredeki yapı nesnesinin "sizeof" değeri geçilir. Fonksiyonun geri dönüş değeri başarı durumunda "0", hata durumunda ise "-1" biçimindedir. Diğer yandan "cpu_set_t" türden bir nesnenin bit'lerini "set" veya "reset" işlemi için de çeşitli makrolar bulundurulmuştur. Bu makrolardan önemli olanları ise şunlardır: #define _GNU_SOURCE /* See feature_test_macros(7) */ #include void CPU_ZERO(cpu_set_t *set); void CPU_SET(int cpu, cpu_set_t *set); void CPU_CLR(int cpu, cpu_set_t *set); int CPU_ISSET(int cpu, cpu_set_t *set); int CPU_COUNT(cpu_set_t *set); -> "CPU_ZERO" : Tüm bit dizisini sıfırlamaktadır. -> "CPU_SET" : Bit dizisi içerisindeki belli bir bit'i "1" yapmaktadır. -> "CPU_CLR" : Bit dizisi içerisindeki belli bir bit'i "0" yapmaktadır. -> "CPU_ISSET" : Bit dizisi içerisindeki belli bir bit'in durumunu almak için kullanılır. -> "CPU_COUNT" : Bit dizisi içerisindeki kaç bit olduğunu döndürmektedir. İlgili başlık dosyasından diğer makrolara da ulaşabiliriz. Öte yandan, bu iki fonksiyonu kullanabilmek için, ilgili kaynak dosyasının en üstüne "_GNU_SOURCE" sembolik sabitini "#define" etmeliyiz. Tabii şu noktaya da dikkat çekmekte fayda vardır: "sched_setaffinity" fonksiyonuna geçtiğimiz "cpu_set_t" yapısı içerisindeki ilgili dizinin her bir bitinin kullanılabilirliği, o sistemdeki toplam işlemci sayısına göre değişkenlik de göstermektedir. Bütün bunlara ek olarak, makinelerdeki işlemci veya çekirdeklerin numaraları ise sıfırdan başlamaktadır. Son olarak "sched_setaffinity" fonksiyonu ile kendi "thread" lerimizin CPU ayarını değiştirebiliriz ancak başkalarına ait "thread" lerin ayarlarını değiştiremeyiz. Bu fonksiyonun başarılı olabilmesi için, ilgili fonksiyonu çağıran prosesin Etkin Kullanıcı ID değerinin hedef "thread" in sahibi olan prosesin Gerçek veya Etkin Kullanıcı ID değeri ile aynı olması gerekmektedir. Tabii prosesimiz uygun önceliklere sahipse, herhangi bir "thread" in CPU ayarını da değiştirebilir. > Hatırlatıcı Notlar: >> Komut satırı programından "tty" komutunu çalıştırdığımız zaman, o terminal programının "pts" bilgisini öğreniriz. Fakat o terminali kullanarak "sudo" ile bir program çalıştırırsak, bu sefer "pts" bilgisi de değişecektir. Yani normalde iki adet açık terminal olsun. Bunlar üzerinden "tty" komutunu çalıştırdığımızda sırasıyla "pts/0" ve "pts/1" çıktılarını göreceğiz. Eğer bu terminallerden birisi ile "sudo" komutunu kullanarak program çalıştırırsak, artık "pts/2" olacaktır. >> Spesifik bir "thread" in "nice" değerini değiştirmeye yarayan POSIX fonksiyonu mevcut değildir. "setpriority" gibi POSIX fonksiyonlar prosesinkini değiştirdiği için, o prosesteki bütün "thread" lerinki de değişecektir. "getpriority" de yine benzer şekilde prosesinkini aldığı için ve o prosesteki bütün "thread" lerin değeri aynı olduğu için yine bir sorun yoktur. Ancak Linux sistemlerinde ise sadece "main-thread" e ilişkin değerler etkilenmektedir. Dolayısıyla Linux sistemlerde spesifik bir "thread" e ilişkin "nice" değerini değiştirebiliriz, ona ait ID değerini kullanarak. /*================================================================================================================================*/ (69_29_07_2023) > "Threads in POSIX" (devam): >> "Processor Affinity" : "sched_setaffinity" ve "sched_getaffinity" fonksiyonlarının ikinci parametresine neden "sizeof" değerini geçtiğimiz konusunda tereddütler oluşabilir çünkü "cpu_set_t" türünü bildirenler, bu türün kaç "byte" olduğunu bilmektedir. Ancak her ne kadar bu tür "1024" bit uzunluğunda olsa bile, işlemci ya da çekirdek sayısının çok fazla olduğu sistemlerde bu "bit" uzunluğu yeterli gelmeyebilir. İşte bu durumda dinamik bellek yöntemleri devreye girmektedir. Yani "CPU_ALLOC" ve "CPU_FREE" fonksiyonlarını kullanmalı ve "CPU_ALLOC_SIZE" fonksiyonu ile de oluşturulan "byte" uzunluğu elde edilmelidir. Dolayısıyla bu fonksiyonlar ile elde edeceğimiz uzunluk bilgisini de "affinity" fonksiyonlarına geçeceğiz. İşte bu nedenden dolayıdır. Şimdi de pekiştirici örneklere bakalım: * Örnek 1, "main-thread" i bir CPU'ya atamak: #define _GNU_SOURCE #include #include #include #include void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); // Bütün bit'leri sıfırlandı. CPU_SET(1, &cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "1" numaralı CPU'da "main-thread" çalışacaktır. if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) == -1) exit_sys("sched_setaffinity"); puts("Ok\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ile yeni oluşturulan "thread" ile "main-thread" farklı CPU'lara atanmıştır. #define _GNU_SOURCE #include #include #include #include #include #include void* thread_proc(void*); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); cpu_set_t cpu_set; CPU_ZERO(&cpu_set); // Bütün bit'leri sıfırlandı. CPU_SET(1, &cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "1" numaralı CPU'da "main-thread" çalışacaktır. if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) == -1) exit_sys("sched_setaffinity"); puts("Ok\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void*) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); // Bütün bit'leri sıfırlandı. CPU_SET(2, &cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "2" numaralı CPU'da "main-thread" çalışacaktır. if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) == -1) exit_sys("sched_setaffinity"); puts("Ok\n"); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte ise birden fazla "thread" oluşturulup, her biri farklı CPU'lara atanmıştır. #define _GNU_SOURCE #include #include #include #include #include #include #define TOTAL_CPUs 7 void* thread_proc(void*); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { /* # OUTPUT # */ int result; pthread_t tid[TOTAL_CPUs]; for(size_t i = 0; i < TOTAL_CPUs; ++i){ if((result = pthread_create(&tid[i], NULL, thread_proc, (void*)i)) != 0) exit_sys_errno("pthread_create", result); } for(size_t i = 0; i < TOTAL_CPUs; ++i){ if((result = pthread_join(tid[i], NULL)) != 0) exit_sys_errno("pthread_join", result); } return 0; } void* thread_proc(void* i) { size_t index = (size_t)i; cpu_set_t cpu_set; CPU_ZERO(&cpu_set); // Bütün bit'leri sıfırlandı. CPU_SET(index, &cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "2" numaralı CPU'da "main-thread" çalışacaktır. if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) == -1) exit_sys("sched_setaffinity"); puts("Ok\n"); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Linux sistemlerinde yukarıdaki "affinity" fonksiyonlarına ek olarak iki fonksiyon daha bulunmaktadır. Bunlar "pthread_setaffinity_np" ve "pthread_getaffinity_np" isimli fonksiyonlardır. İsimlerin sonundaki "_np" son eki ise "non-portable" anlamındadır. Bu fonksiyonların yukarıdaki fonksiyonlardan farkı, ilk parametreye "pthread_t" türünden bir ID değeri almasıdır. Böylelikle başka bir "thread" akışı içerisinde olmadan onların CPU ayarlarını değiştirebiliriz. Tabii bu iki fonksiyon da yine Linux sistemi için geliştirilmiştir. Son olarak fonksiyonlar başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. * Örnek 1, Aşağıda "main-thread" ile bizim oluşturduğumuz yeni "thread" farklı CPU'lara atanmıştır. #define _GNU_SOURCE #include #include #include #include #include #include void* thread_proc(void*); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); int main(void) { int result; pthread_t tid; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); cpu_set_t cpu_set; CPU_ZERO(&cpu_set); // Bütün bit'leri sıfırlandı. CPU_SET(1, &cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "1" numaralı CPU'da "main-thread" çalışacaktır. if((result = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set)) != 0) exit_sys_errno("pthread_setaffinity_np", result); CPU_ZERO(&cpu_set); // Bütün bit'leri sıfırlandı. CPU_SET(2, &cpu_set); // Sadece tek bir bit'i bir, diğerleri sıfır. Artık "2" numaralı CPU'da yeni oluşturulan "thread" çalışacaktır. if((result = pthread_setaffinity_np(tid, sizeof(cpu_set), &cpu_set)) != 0) exit_sys_errno("pthread_setaffinity_np", result); puts("Ok\n"); if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void*) { puts("Ok\n"); return NULL; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Pekiyi bizler işin başında o sistemde kaç adet CPU olduğunu nasıl anlayabiliriz? Bunun için "shell" programı üzerinden "nproc" veya "lscpu" isimli komutları kullanabiliriz. * Örnek 1, "nproc" komut çıktısı: $ nproc 8 * Örnek 2, "lscpu" komut çıktısı: $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz CPU family: 6 Model: 158 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 9 BogoMIPS: 5615.99 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm consta nt_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf _lm abm 3dnowprefetch invpcid_single pti ssbd ibrs ibpb stibp fsgsbase bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt xsaveopt xsavec xgetbv1 xsaves flush_l1d arch_capabilities Virtualization features: Hypervisor vendor: Microsoft Virtualization type: full Caches (sum of all): L1d: 128 KiB (4 instances) L1i: 128 KiB (4 instances) L2: 1 MiB (4 instances) L3: 6 MiB (1 instance) Vulnerabilities: Itlb multihit: KVM: Mitigation: VMX unsupported L1tf: Mitigation; PTE Inversion Mds: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown Meltdown: Mitigation; PTI Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization Spectre v2: Mitigation; Full generic retpoline, IBPB conditional, IBRS_FW, STIBP conditional, RSB filling Srbds: Unknown: Dependent on hypervisor status Tsx async abort: Not affected Aslında bu iki komut bu bilgileri "proc" dosya sistemindeki "/proc/cpuinfo" dizininden alınmaktadır. Doğrudan "cat" komutunu kullanıp bu dizindekileri ekrana da yazdırabiliriz. Pekala sistemimizde o anda kaç CPU ya da çekirdek bulunduğu bilgisini "get_nprocs", "get_nprocs_conf" ve "sched_getcpu()" isimli fonksiyonlarla elde edebiliriz. >>> "get_nprocs" ve "get_nprocs_conf" : Bu fonksiyonlar birer Linux fonksiyonudur ve prototipleri aşağıdaki gibidir: #include int get_nprocs(void); int get_nprocs_conf(void); Buradaki "get_nprocs_conf" fonksiyonu makinadeki toplam işlemci ya da çekirdek sayısını, "get_nprocs" ise işletim sisteminin o makinede dikkate aldığı işlemci ya da çekirdek sayısını döndürmektedir. Bu fonksiyonlar başarısız olamamaktadır. * Örnek 1, #define _GNU_SOURCE #include #include int main(void) { /* # OUTPUT # 8 of 8 is used right now!.. */ printf("%d of ", get_nprocs()); printf("%d is used right now!..\n", get_nprocs_conf()); return 0; } >>> "sched_getcpu" : Bu fonksiyon ise o anda hangi CPU'da çalıştırıldığı bilgisini vermektedir. Yine bu fonksiyon da bir Linux fonksiyonudur. Tabii çalışmakta olan bir "thread" işletim sistemi tarafından farklı CPU'lara atanabilir. Dolayısıyla bu fonksiyon ile elde edeceğimiz değer de farklılık gösterecektir. Fonksiyonun prototipi aşağıdaki gibidir: #include int sched_getcpu(void); * Örnek 1, #define _GNU_SOURCE #include #include int main(void) { /* # OUTPUT # 1 */ printf("%d\n", sched_getcpu()); return 0; } >> "Thread Safety" : "Thread Safety" konusu genel olarak fonksiyonlar için kullanılan bir kavramdır. Bir fonksiyonun "Thread Safety" olması demek, o fonksiyonun birden fazla "thread" tarafından çağrılması durumunda, bir sorun oluşmaması demektir. Pekiyi bir fonksiyonu "Thread Safety" olmaktan çıkartan şeyler nelerdir? Şüphesiz yalnızca yerel değişkenleri ve parametre değişkenlerini kullanan bir fonksiyon "Thread Safety" dir. Dolayısıyla "static" ömürlü kaynak kullanan fonksiyonlar "Thread Safety" olmaktan çıkacaktır. Çünkü böylesi bir fonksiyonu birden fazla "thread" çağırdığında, ilgili kaynak bozulacaktır. Örneğin, Standart C fonksiyonu olan "localtime" fonksiyonu aslında "Thread Safety" değildir çünkü arka planda "static" ömürlü yerel değişken kullanmaktadır. Buradaki kilit nokta ilgili fonksiyonun illa da "static" ömürlü nesne kullanması değildir. "static" ömürlü nesne kullanması gibi ortak başka kaynakları da kullanması o fonksiyonu "Thread Safety" olmaktan çıkartacaktır. Pekiyi bizler bir fonksiyonun "Thread Safety" olmadığı biliyorsak, onu nasıl kullanmalıyız? Bu durumda devreye "mutex" gibi nesneler girmektedir. Tabii burada devreye yine o fonksiyonun dökümantasyonu devreye girmektedir. Dolayısıyla üçüncü parti fonksiyon kullanırken, ilgili fonksiyonun bulunduğu kütüphaneyi inceleyerek durumunu öğrenmeliyiz. Standart C fonksiyonları söz konusu olduğunda da şöyle yapabiliriz: -> Microsoft dünyasında, 2004 yılına kadar, Standart C fonksiyonlarının "Thread Safety" versiyonları ve olmayan versionlarını birlikte bulundurmaktaydı. Ancak 2004 yılı itibariyle artık sadece "Thread Safety" versiyonlarını bulundurmaktadır. Dolayısıyla Microfot C derleyicisi ile çalışıyorsak, bütün Standart C fonksiyonları "Thread Safety" dir. -> UNIX/Linux dünyasında ise iki farklı version bulundurulmaktadır. Varsayılan isimde olanlar "Thread Safety" değilken, sonu "_r" ile biten fonksiyonlar "Thread Safety" dir. "localtime" fonksiyonunu ele alırsak; "localtime" isimli fonksiyon "Thread Safety" değilken, "localtime_r" isimli fonksiyon "Thread Safety" dir. Fakat şu noktayı da unutmamak gerekir ki sonu "_r" ile bitenler, orjinal versiyonlarından farklı prototiplere sahiptirler. > Hatırlatıcı Notlar: >> "pthread_t" türü ile "tid_t" türleri arasındaki fark şudur: Anımsanacağı üzere POSIX standartlarınca yalnızca prosesler ID değerine sahiptir. Fakat Linux sistemlerinde durum böyle değildir. Bu sistemlerde her bir "thread" kendi ID değerine sahiptir. Dolayısıyla Linux sistemlerinde hangi akış "gettid()" fonksiyonunu çağırırsa, kendisine ait olan ID değerini elde edecektir. Eğer bu sistemlerde "getpid" fonksiyonu çağırırsak, "main-thread" in ID değerini elde etmiş oluruz. Diğer yandan POSIX sistemlerinde "gettid" fonksiyonu bulunmamaktadır. Dolayısıyla bu sistemlerde "getpid" fonksiyonunu çağırırsak, prosesin ID değerini elde etmiş oluruz. Özetle; POSIX Linux getpid() Proses main-thread gettid() N/A the-thread >> "shell" programı üzerinden "PS1='CSD>'" komutunu çalıştırırsak, artık satır başı "CSD>" olarak gözükecektir. Eğer "PS1='#'" yapsaydık, satır başları "#" olarak gözükecekti. /*================================================================================================================================*/ (70_30_07_2023) & (71_05_08_2023) > "Threads in POSIX" (devam): >> "Thread Safety" (devam): Şimdi de UNIX/Linux dünyasındaki bazı fonksiyonları inceleyelim: >>> "rand_r" fonksiyonu: "rand" fonksiyonunun "Thread Safety" versiyonudur. Anımsanacağı üzere "rand" fonksiyonu, "srand" ile birlikte çalışır ve arka planda "global" isim alanındaki bir değişkeni kullanırlar. Dolayısıyla farklı "thread" ler "rand" fonksiyonunu aynı anda çağırdığında "Data Racing" meydana gelecektir eğer herhangi bir önlem almamışsak. Fonksiyonun prototipi aşağıdaki gibidir: #include int rand_r(unsigned int *seedp); Fonksiyona tohum değerine ilişkin nesnenin adresini almaktadır. Buraya "global" isim alanındaki bir nesnenin adresini de geçebiliriz. Fakat kilit nokta farklı "thread" lerin farklı tohum nesnelerini kullanıyor olmasıdır. * Örnek 1, Aşağıdaki örnekte iki "thread" ile "rand_r" fonksiyonunu kullanmıştır. Ancak bu "thread" ler farklı tohum değerleri kullandığı için bir sorun olmayacaktır. #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # Thread-Consumer: 0 Thread-Consumer: 1 Thread-Producer: 0 Thread-Producer: 1 Thread-Consumer: 2 Thread-Producer: 2 Thread-Producer: 3 Thread-Consumer: 3 Thread-Producer: 4 Thread-Consumer: 4 Thread-Consumer: 5 Thread-Consumer: 6 Thread-Consumer: 7 Thread-Producer: 5 Thread-Producer: 6 Thread-Producer: 7 Thread-Consumer: 8 Thread-Producer: 8 Thread-Consumer: 9 Thread-Producer: 9 */ pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_producer(void* param) { unsigned int seed = time(NULL) + 123; int rand_val; for(int i = 0; i < 10; ++i){ rand_val = rand_r(&seed); usleep(rand_val % 500000); printf("Thread-Producer: %d\n", i); } return NULL; } void* thread_consumer(void* param) { unsigned int seed = time(NULL) + 456; int rand_val; for(int i = 0; i < 10; ++i){ rand_val = rand_r(&seed); usleep(rand_val % 500000); printf("Thread-Consumer: %d\n", i); } return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise farklı "thread" ler "global" isim alanındaki nesneyi kullanmışlardır fakat yine farklı tohum değerleri ile. #include #include #include #include #include #include void* thread_producer(void* param); void* thread_consumer(void* param); void exit_sys_errno(const char* msg, int eno); unsigned int seed; int main(int argc, char** argv) { /* # OUTPUT # Thread-Consumer: 0 Thread-Consumer: 1 Thread-Producer: 0 Thread-Producer: 1 Thread-Consumer: 2 Thread-Producer: 2 Thread-Producer: 3 Thread-Consumer: 3 Thread-Producer: 4 Thread-Consumer: 4 Thread-Consumer: 5 Thread-Consumer: 6 Thread-Consumer: 7 Thread-Producer: 5 Thread-Producer: 6 Thread-Producer: 7 Thread-Consumer: 8 Thread-Producer: 8 Thread-Consumer: 9 Thread-Producer: 9 */ pthread_t tid1, tid2; int result; if((result = pthread_create(&tid1, NULL, thread_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_producer(void* param) { seed = time(NULL) + 123; int rand_val; for(int i = 0; i < 10; ++i){ rand_val = rand_r(&seed); usleep(rand_val % 500000); printf("Thread-Producer: %d\n", i); } return NULL; } void* thread_consumer(void* param) { seed = time(NULL) + 456; int rand_val; for(int i = 0; i < 10; ++i){ rand_val = rand_r(&seed); usleep(rand_val % 500000); printf("Thread-Consumer: %d\n", i); } return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Aşağıda ise "rand" fonksiyonunun ve "srand" fonksiyonunun temsili implementasyonları verilmiştir: * Örnek 1, Bu örnek "The C Programming Language" kitabından alınmıştır. unsigned long int next = 1; void srand(unsigned int seed){ next = seed; } int rand(void){ next = next * 1103515245 + 12345; return (unsigned int)(next / 65536) % 32768; } >>> "localtime_r" fonksiyonu: Bu fonksiyon da "localtime" fonksiyonunun "Thread Safety" halidir. "localtime" fonksiyonu "static" ömürlü nesne kullanması hasebiyle "Thread Safety" değildir. "localtime_r" ise bizden aldığı ilgili nesneyi değiştirerek bize geri döndürmektedir. Dolayısıyla arka planda "static" değişken kullanılmamaktadır. Fonksiyonun prototipi aşağıdaki gibidir: #include struct tm *localtime_r(const time_t *timep, struct tm *result); Burada fonksiyon ikinci parametresi ile aldığı yapı nesnesinin adresini kullandıktan sonra tekrar geri döndürecektir. * Örnek 1, #include #include #include int main(int argc, char** argv) { /* # OUTPUT # 04:37:12 */ time_t t; struct tm tm_val, *p_tm; t = time(NULL); p_tm = localtime_r(&t, &tm_val); printf("%02d:%02d:%02d\n", p_tm->tm_hour, p_tm->tm_min, p_tm->tm_sec); return 0; } Öte yandan UNIX/Linux sistemlerinde yalnızca bazı Standart C fonksiyonlarının "_r" son eki ile biten versiyonları yoktur. Benzer biçimde yine "_r" son eki ile biten POSIX fonksiyonları da vardır. Örneğin, "getpwnam", "getpwuid" gibi fonksiyonlar arka planda yine "static" ömürlü nesnelerin adreslerini geri döndürmektedir. İşte bu fonksiyonların "_r" son eki almış versiyonları ise şu prototiplere sahiptir: #include #include int getpwnam_r(const char *name, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result); int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result); Bu fonksiyonlar, "etc/passwd" dosyasındaki satırların yerleştirileceği tampon alanın başlangıç adresini ve bu alanın uzunluk bilgisini istemektedir. Bu uzunluk bilgisini ise "sysconf(_SC_GETPW_R_SIZE_MAX)" çağrısı ile elde edebiliriz. Yine ek olarak "struct passwd" nesnesinin adres bilgisini de geçeriz. Son parametrelere ise "struct passwd" nesnesinin adresi yerleştirilir, başarı durumunda. Başarısız durumunda bu adrese "NULL" değeri geçilir. Ayrıca fonksiyonların başarı durumundaki değeri "0", hata durumunda ise hata kodunun kendisidir. * Örnek 1, Aşağıdaki örnekte ilgili tampon bölgesinin büyüklüğü olarak yeteri kadar büyük bir alan belirlenmiştir. #include #include #include #include int main(void) { /* # OUTPUT # root, 0 */ char buffer[4096]; struct passwd pwd, *p_pwd; int result; getpwnam_r("root", &pwd, buffer, 4096, &p_pwd); if(result != 0){ fprintf(stderr, "getpwnam_r: %s!..\n", strerror(result)); exit(EXIT_FAILURE); } if(p_pwd == NULL){ fprintf(stderr, "no user found!..\n"); exit(EXIT_FAILURE); } printf("%s, %lld\n", pwd.pw_name, (long long)pwd.pw_uid); return 0; } >> "Thread Specific Global Variables" : Anımsanacağı üzere "global" isim alanındaki nesneler hem "static" ömürlüler hem de "thread" ler tarafından ortak bir şekilde görülür vaziyettedirler. Diğer yandan "automatic" ömürlü nesneler ise "thread" lere özgü biçimdedir. İşte "global" isim alanındaki bu "static" ömürlü nesnelerin "thread" lere özgü olmasına, "thread local storage" denmektedir. Buradaki ilgili nesne yine "global" isim alanında ve "static" ömürlü fakat o "thread" e özgüdür. Dolayısıyla bir işletim sistemi bir "thread" oluştururken sadece ilgili "stack" alanı değil, "thread local storage" alanı da oluşturmaktadır. Buradaki "thread local storage" bölümünde, o "thread" e özgü fakat "static" ömürlü nesneler bulundurulmaktadır. Tabii "dynamic" ömürlü nesneler için de bu ayrım söz konusudur. Yani onlar da "thread" e özgü olabilmektedir. Fakat burada kullandığımız "thread local storage" kavramı aslında Windows dünyasında kullanılan bir kavramdır. UNIX/Linux dünyasında bu kavramın karşılığı "thread specific data" biçimindedir. Pekiyi işletim sisteminin ekstra oluşturduğu bu "thread specific data" alanı nasıldır? Burada aşağıdaki gibi bir veri yapısı kullanılmaktadır: Slot No In-use Flags Pointer 0 1 ---> 1 0 NULL 2 1 ---> 3 1 ---> 4 1 ---> ... ... ... Görüleceği üzere ilgili veri yapısı "slot" lardan oluşmaktadır ve her birisinin bir numarası vardır. Bu "slot" ların türleri ise "pthread_key_t" türündendir. Ayrıca o "slot" un boş ya da dolu olduğunu da "flag" ile belirtilmektedir. Eğer o "slot" dolu ise ilgili "Pointer" kısmındaki gösterici ilgili nesneyi, eğer o "slot" boş ise "NULL" değerini göstermektedir. Tabii buradaki "flag" kısmına da gerek yoktur, diyebiliriz. Fakat bu "flag" sütununa, "Pointer" kısmına "NULL" adres yerleştirilebileceği için gereksinim duyulmaktadır. Pekiyi bizler bu tabloyu nasıl kullanacağız? Burada dört adet POSIX fonksiyonu elimizin altındadır: "pthread_key_create", "pthread_getspecific", "pthread_setspecific" ve "pthread_key_delete" isimli fonksiyonlarıdır. Bu fonksiyonları ise şu şekilde kullanabiliriz: -> "pthread_key_create" ile aslında biz bir "slot" oluştururuz. Bu "slot", daha önce hayata getirilen ve hayata getirilecek olan her bir "thread" de var olacaktır. Tabii her bir "thread" için bu "slot" a farklı bir adres tayin edebilmekteyiz. Fonksiyonun prototipi şu şekildedir: #include int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); Fonksiyonun birinci parametresi, "slot" numarasının yerleştirileceği "pthread_key_t" türünden nesnenin adresini almaktadır. Bu "pthread_key_t" türünden nesne tipik olarak programın "global" isim alanında tanımlanmaktadır. Fonksiyonun ikinci parametresi ise o "slot" yok edilirken çağrılacak fonksiyonu belirtmektedir. Bu parametre "NULL" değerini alabilir. Fonksiyonun geri dönüş değeri ise hata durumunda hata kodunun kendisine, başarı durumunda ise "0" değerine geri dönmektedir. Artık her bir "thread" de geçerli bir "slot" tahsis etmiş olduk. -> Programcı "thread" e özgü "static" verileri bir yapı ile sarmalar ve bu yapı türünden dinamik ömürlü nesne oluşturur. Daha sonra bu nesnenin adresini de "pthread_setspecific" fonksiyonu ile o "thread" in ilgili "slot" una yerleştirir. "pthread_setspecific" fonksiyonunu hangi "thread" çağırmışsa, adres bilgisi o "thread" in ilgili slotuna yerleştirilecektir. İş bu fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_setspecific(pthread_key_t key, const void *value); Fonksiyon birinci parametresine, "pthread_key_create" fonksiyonunun birinci parametresine geçtiğimiz, "slot" bilgisisini içeren "pthread_key_t" türünden değişkeni geçeriz. İkinci parametresine de o "slot" a yazılacak olan adres bilgisini geçeriz. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. -> Pekala o "thread" in belli bir "slot" una yerleştirilen adresin temin edilmesi ise "pthread_getspecific" fonksiyonu ile mümkündür. Fonksiyonun prototipi aşağıdaki gibidir: #include void *pthread_getspecific(pthread_key_t key); Fonksiyon birinci parametresine, "pthread_key_create" fonksiyonunun birinci parametresine geçtiğimiz, "slot" bilgisisini içeren "pthread_key_t" türünden değişkenini alır. Geri dönüş değeri olarak da o "slot" da bulunan adres bilgisini geri döndürür. Ancak öyle bir "slot" yoksa ya da o "slot" da herhangi bir adres belirtilmemişse "NULL" değerine geri dönmektedir. Başarısızlık için herhangi bir hata kodu belirtilmemiştir. -> Son olarak oluşturulan "slot" un tekrar geri verilmesi uygundur. Bunun için "pthread_key_delete" isimli fonksiyonu kullanacağız. Zaten bir "thread" de sona ermek üzereyse, ona ait tüm "slot" lar da boşaltılacaktır. Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_key_delete(pthread_key_t key); Fonksiyon parametre olarak, "pthread_key_create" fonksiyonunun birinci parametresine geçtiğimiz, "slot" bilgisisini içeren "pthread_key_t" türünden değişkenini alır. Başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. * Örnek 1, Aşağıdaki örnekte "thread" ler zaten sona ereceğinden "pthread_key_delete" fonksiyon çağrısına lüzum görülmemiştir. #include #include #include #include #include pthread_key_t g_key; typedef struct tagTHREAD_SPECIFIC_DATA{ int count; /* ... */ } THREAD_SPECIFIC_DATA; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 31 69 */ int result; if((result = pthread_key_create(&g_key, NULL)) != 0) exit_sys_errno("pthread_key_create", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } tsd->count = 31; pthread_setspecific(g_key, tsd); foo(); return NULL; } void* thread_proc2(void* param) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } tsd->count = 69; pthread_setspecific(g_key, tsd); foo(); return NULL; } void foo(void) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)pthread_getspecific(g_key)) == NULL){ fprintf(stderr, "cannot get the thread specific data!..\n"); exit(EXIT_FAILURE); } printf("%d\n", tsd->count); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } -> Pekiyi bu örnekteki dinamik ömürlü bellek alanlar ne zaman geri verilmelidir? Burada iki farklı yöntem karşımıza çıkmaktadır. İlki, o bellek alanını tahsis eden ilgili "thread" tarafından, "manuel" olarak sonlandırılmasıdır. Diğeri ise "pthread_key_create" fonksiyonunun ikinci parametresine bu işi üstlenecek fonksiyonun adresini geçmektir. İş bu ikinci parametreye geçilen o fonksiyon, o "key" değeri ile oluşturulan her bir "slot" için, tabii o "slot" a da bir adres değeri geçilmişse, sistem tarafından bir kez çağrılacaktır. * Örnek 1, Aşağıdaki örnekte her bir "thread" in tahsis ettiği dinamik ömürlü alan "key_deleter" isimli fonksiyon tarafından serbest bırakılmaktadır. #include #include #include #include #include pthread_key_t g_key; typedef struct tagTHREAD_SPECIFIC_DATA{ int count; /* ... */ } THREAD_SPECIFIC_DATA; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void key_deleter(void* ptr); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # foo called!.. 31 key_deleter called!.. foo called!.. 69 key_deleter called!.. */ int result; if((result = pthread_key_create(&g_key, key_deleter)) != 0) exit_sys_errno("pthread_key_create", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_key_delete(g_key)) != 0) exit_sys_errno("pthread_key_delete", result); return 0; } void* thread_proc1(void* param) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } tsd->count = 31; pthread_setspecific(g_key, tsd); foo(); return NULL; } void* thread_proc2(void* param) { THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)malloc(sizeof(THREAD_SPECIFIC_DATA))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } tsd->count = 69; pthread_setspecific(g_key, tsd); foo(); return NULL; } void foo(void) { puts("foo called!.."); THREAD_SPECIFIC_DATA* tsd; if((tsd = (THREAD_SPECIFIC_DATA*)pthread_getspecific(g_key)) == NULL){ fprintf(stderr, "cannot get the thread specific data!..\n"); exit(EXIT_FAILURE); } printf("%d\n", tsd->count); } void key_deleter(void* ptr) { puts("key_deleter called!.."); free(ptr); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Şimdi buraya kadarki örneklerde "thread specific data" kullanan fonksiyonları bizler yazarak onları "thread safe" hale getirdik. Pekiyi başkaları tarafından yazılan fonksiyonlar "thread safe" değilse onları nasıl "thread safe" hale getirebiliriz? Bunun bir yolu, eğer bahsi geçen fonksiyon Standart C fonksiyon ise veya kullanacağımız kütüphane derleyici ile aşağı seviyeli bir bağlantıya sahipse, "thread specific data" için oluşturmamız gereken "slot" ları programın "start-up" kodları içerisinde oluşturmalıyız. Bu "start-up" kodları, aslında programın "main" fonksiyonunun çağrılmasından evvel çağrılan kodlardır. Örneğin, Microsoft derleyicileri Standart C fonksiyonlarını bu şekilde "thread safe" hale getirmiştir. >>> Burada da şu noktayı açıklamak gereklidir: Bir C programında ilk çağrılan fonksiyon "main" fonksiyonu değildir. Aslında ilk çağrılan kodlar, derleyicinin "start-up" kodları dediği kodlardır ki bu kodlar "main" fonksiyonunu çağırmaktadır. Yani programın akışı şu şekilde diyebiliriz: ... ... ... call to main call to exit Buradan da görüleceği üzere "main" fonksiyonu aslında "start-up" kodları tarafından çağrılmakta, programın akışı "main" fonksiyonundan geri geldikten sonra "exit" fonksiyonu çağrılmaktadır. İşte derleyici ile aşağı seviyeli bir bağlantıya sahip bir kütüphanede "thread safe data" kullanılacaksa, iş bu "start-up" kodlarının bulunduğu alanda ilgili "slot" lar oluşturulabilir (C++ gibi dillerde de "global" değişkenler kullanılarak bir takım kodlar "main" fonksiyonundan evvel çağrılmasını sağlayabiliriz fakat bu yaklaşımı C dilinde uygulayamayız çünkü C dilinde böylesi bir kullanım için sabit ifadesi kullanmalıyız.). Pekiyi bizler böylesi fonksiyonları kendi kütüphanemiz için nasıl yazabiliriz? Burada iki farklı yöntem karşımıza çıkmaktadır: Kütüphanemize "init" ismine benzer isimde bir fonksiyon eklemek ve işin başında bu fonksiyonun programcı tarafından çağrılması gerektiğini belirtmek, "pthread_once" fonksiyonunu kullanmak. Şimdi de bu yöntemleri inceleyelim: >>> "pthread_once": Bazı durumlarda bir "thread" akışının aynı yerden tekrar tekrar geçtiği halde sadece bir defa bir şeyin yapması istenebilmektedir. Bunun için "pthread_once" isimli bir POSIX fonksiyonu bulundurulmaktadır. Fonksiyonun prototipi şu şekildedir: #include int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)); Fonksiyonun birinci parametresi "pthread_once_t" türünden bir nesnenin adresidir. Fakat bu nesneye aşağıdaki gibi ilk değer verilmiş olması gerekmektedir: pthread_once_t once_control = PTHREAD_ONCE_INIT; Fonksiyonun ikinci parametresi ise bir defaya mahsus çağrılacak olan fonksiyonun adresidir. Fonksiyon başarı durumunda "0", hata durumunda ise hata kodunun kendisine geri dönmektedir. Eğer fonksiyonun birinci parametresine geçtiğimiz nesne tekil bir nesne ise, fonksiyonun ikinci parametresindeki fonksiyon iki defa çağrılacaktır. Eğer farklı nesneler geçersek, o nesnelerin adedince ilgili fonksiyon çağrılacaktır. * Örnek 1, #include #include #include #include #include pthread_once_t g_ones = PTHREAD_ONCE_INIT; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void init(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # init proc!.. */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 10; ++i) foo(); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 10; ++i) foo(); return NULL; } void foo(void) { int result; if((result = pthread_once(&g_ones, init)) != 0 ) exit_sys_errno("pthread_once", result); } void init(void) { printf("init proc!..\n"); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include pthread_once_t g_ones[5]; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void init(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # init proc!.. init proc!.. init proc!.. init proc!.. init proc!.. */ for(int i = 0; i < 5; ++i) g_ones[i] = PTHREAD_ONCE_INIT; int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 10; ++i) foo(); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 10; ++i) foo(); return NULL; } void foo(void) { int result; for(int i = 0; i < 5; ++i) if((result = pthread_once(&g_ones[i], init)) != 0 ) exit_sys_errno("pthread_once", result); } void init(void) { printf("init proc!..\n"); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include #include void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(pthread_once_t* t_ones); void init(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # init proc!.. init proc!.. */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { pthread_once_t t_ones = PTHREAD_ONCE_INIT; for(int i = 0; i < 10; ++i) foo(&t_ones); return NULL; } void* thread_proc2(void* param) { pthread_once_t t_ones = PTHREAD_ONCE_INIT; for(int i = 0; i < 10; ++i) foo(&t_ones); return NULL; } void foo(pthread_once_t* t_ones) { int result; if((result = pthread_once(t_ones, init)) != 0 ) exit_sys_errno("pthread_once", result); } void init(void) { printf("init proc!..\n"); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Aslında bu fonksiyonun yaptığı şeyi "mutex" kullanarak da gerçekleştirebiliriz. Şöyleki: * Örnek 1, #include #include #include #include #include #define MY_PTHREAD_ONCE_INIT 0 pthread_once_t g_ones = MY_PTHREAD_ONCE_INIT; void* thread_proc1(void* param); void* thread_proc2(void* param); void init(void); int my_pthread_once(pthread_once_t* once_control, void(*init_routing)(void)); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # init proc!.. */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { int result; if((result = my_pthread_once(&g_ones, init)) != 0 ) exit_sys_errno("pthread_once", result); return NULL; } void* thread_proc2(void* param) { int result; if((result = my_pthread_once(&g_ones, init)) != 0 ) exit_sys_errno("pthread_once", result); return NULL; } void init(void) { printf("init proc!..\n"); } int my_pthread_once(pthread_once_t* once_control, void(*init_routing)(void)) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int result; if((result = pthread_mutex_lock(&mutex)) != 0) return result; if(*once_control == 0){ init_routing(); *once_control = 1; } if((result = pthread_mutex_unlock(&mutex)) != 0) return result; return 0; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Artık bu fonksiyonu kullanarak, yalnızca ilk "thread" in bir kez "thread specific data" için "slot" oluşturmasını sağlayabiliriz. >>> Buradaki "init" ismi temsili bir isimdir. Buradaki amaç, böylesi bir fonksiyonun işin başında programcı tarafından çağrılmasını sağlatmaktır. İş bu fonksiyonun bünyesinde "thread specific data" için "slot" oluşturabiliriz. Bu yöntem diğer yönteme nazaran daha etkilidir fakat programcının burada bu fonksiyonu çağırmayı unutmaması gerekmektedir. Şimdi de Standart C fonksiyonu olan "srand" ve "rand" fonksiyonlarının "thread safe" biçimlerini fonksiyonların prototiplerini değiştirmeden kendimiz yazalım: * Örnek 1, Aşağıdaki örnekte "srand" ve "rand" fonksiyonlarının parametrik yapıları aynı kalacak biçimde "thread safe" hale getirilmiştir. Bir "thread" ilk kez "srand" ya da "rand" fonksiyonunu çağırdığında bir "slot" oluşturacaktır. Sonraki "thread" ler ise bu "slot" u kullanacaktır. Buradaki kilit nokta her iki fonksiyon için sadece bir adet "slot" oluşturulmasıdır. Benzer yaklaşımı büyük kütüphanelerde de uygulayabiliriz. /* my_rand.h */ #ifndef MY_RAND_H #define MY_RAND_H /* VARIABLES */ #define SEED_INIT_VALUE 12345 /* FUNCTIONS */ void my_srand(unsigned int seed); int my_rand(void); #endif /* my_rand.c */ #include #include #include #include #include "my_rand.h" typedef struct tagTSD_RAND{ unsigned int seed; //... }TSD_RAND; pthread_once_t g_rand_once; pthread_key_t g_rand_key; static void exit_sys_errno(const char* msg, int eno); static void destructor(void* ptr); static void rand_init_once(void); static TSD_RAND* rand_init(void); void my_srand(unsigned int seed) { TSD_RAND* tsd_rand = rand_init(); tsd_rand->seed = seed; } int my_rand(void) { TSD_RAND* tsd_rand = rand_init(); tsd_rand->seed = tsd_rand->seed * 1103515245 + 12345; return (unsigned int)(tsd_rand->seed / 65536) % 32768; } static void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } static void destructor(void* ptr) { free(ptr); } static void rand_init_once(void) { int result; if((result = pthread_key_create(&g_rand_key, destructor)) != 0) exit_sys_errno("pthread_key_create", result); } static TSD_RAND* rand_init(void) { int result; if((result = pthread_once(&g_rand_once, rand_init_once)) != 0) exit_sys_errno("pthread_once", result); TSD_RAND* tsd_rand; if((tsd_rand = pthread_getspecific(g_rand_key)) == NULL){ if((tsd_rand = (TSD_RAND*)malloc(sizeof(TSD_RAND))) == NULL){ fprintf(stderr, "fatal error: cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if((result = pthread_setspecific(g_rand_key, tsd_rand)) != 0) exit_sys_errno("fatal error: pthread_setspecific", result); tsd_rand->seed = SEED_INIT_VALUE; } return tsd_rand; } /* main.c */ #include #include #include #include #include #include "my_rand.h" #define MY_PTHREAD_ONCE_INIT 0 #define EPOCH 5 void* thread_proc1(void* param); void* thread_proc2(void* param); void* thread_proc3(void* param); void* thread_proc4(void* param); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 0. Thread-3 : 83 0. Thread-4 : 86 0. Thread-2 : 68 0. Thread-1 : 68 1. Thread-3 : 77 1. Thread-4 : 15 1. Thread-2 : 88 1. Thread-1 : 88 2. Thread-3 : 93 2. Thread-4 : 35 2. Thread-2 : 17 2. Thread-1 : 17 3. Thread-3 : 86 3. Thread-2 : 98 3. Thread-4 : 92 3. Thread-1 : 98 4. Thread-3 : 49 4. Thread-2 : 27 4. Thread-1 : 27 4. Thread-4 : 21 */ int result; pthread_t tid1, tid2, tid3, tid4; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid3, NULL, thread_proc3, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid4, NULL, thread_proc4, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid3, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid4, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { int result; for(int i = 0; i < EPOCH; ++i){ sleep(1); result = my_rand() % 100; printf("%d. Thread-1 : %d\n", i, result); } return NULL; } void* thread_proc2(void* param) { int result; for(int i = 0; i < EPOCH; ++i){ sleep(1); result = my_rand() % 100; printf("%d. Thread-2 : %d\n", i, result); } return NULL; } void* thread_proc3(void* param) { int result; for(int i = 0; i < EPOCH; ++i){ sleep(1); result = rand() % 100; printf("%d. Thread-3 : %d\n", i, result); } return NULL; } void* thread_proc4(void* param) { int result; for(int i = 0; i < EPOCH; ++i){ sleep(1); result = rand() % 100; printf("%d. Thread-4 : %d\n", i, result); } return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Şimdi de bir diğer Standart C fonksiyonu olan "strerror" fonksiyonunu inceleyelim. Anımsanacağı üzere bu fonksiyonu bizler "exit_sys_errno" fonksiyonu ile sarmalayarak kullandık. Aslında bu fonksiyon "thread safe" DEĞİLDİR. Dolayısıyla "strerror" yerine "strerror_r" fonksiyonunu kullanmalıyız. Fakat "strerror_r" fonksiyonunun kullanması da biraz zahmetlidir. Bu nedenle GNU/glibc kütüphanesinin belli bir versiyonundan sonra "strerror" fonksiyonu "thread safe" olarak tanımlanmıştır. Ama yine de taşınabilirlik açısından "strerror_r" fonksiyonunu kullanmamız uygun olacaktır. Öte yandan bazı C derleyicileri "extension" olarak, "thread local storage" denilen bir özellik de sunmaktadır. Örneğin, "gcc" ve "clang" derleyicileri bu özelliği sunmaktadır. Ancak bu özellik UNIX/Linux genelindeki bütün derleyici ve platformlarda bulunmamaktadır. Pekiyi nedir bu özellik? Bu özelliğe göre "static" ömürlü bir değişken "__thread" niteleyicisi kullanılarak bildirilmesi halinde artık ilgili değişken "thread" e özgü olmaktadır. * Örnek 1, Aşağıdaki "g_x" değişkeni "thread" e özgüdür. Burada programcı bu değişkeni normal bir değişken gibi kullanabilir. int __thread g_x; * Örnek 2.0, Aşağıdaki örnekte "static" ömürlü ilgili değişken herhangi bir "slot" kullanılmadan iki "thread" tarafından da kullanılmıştır. Dolayısıyla bir "race conditions" meydana gelmiştir. #include #include #include #include #include int g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 69 69 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { g_count = 31; sleep(1); foo(); return NULL; } void* thread_proc2(void* param) { g_count = 69; sleep(1); foo(); return NULL; } void foo(void) { printf("%d\n", g_count); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2.1, Aşağıdaki örnekte ise "static" ömürlü nesne için bir "slot" oluşturulmuş ve iki "thread" tarafından da kullanılması sağlanmıştır. Dolayısıyla bir "race condition" meydana GELMEMİŞTİR. #include #include #include #include #include pthread_key_t g_key; int g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void key_deleter(void* ptr); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # foo called!.. 31 key_deleter called!.. foo called!.. 69 key_deleter called!.. */ int result; if((result = pthread_key_create(&g_key, key_deleter)) != 0) exit_sys_errno("pthread_key_create", result); pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_key_delete(g_key)) != 0) exit_sys_errno("pthread_key_delete", result); return 0; } void* thread_proc1(void* param) { int* tsd; if((tsd = (int*)malloc(sizeof(int))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } sleep(1); *tsd = 31; pthread_setspecific(g_key, tsd); foo(); return NULL; } void* thread_proc2(void* param) { int* tsd; if((tsd = (int*)malloc(sizeof(int))) == NULL){ fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } sleep(1); *tsd = 69; pthread_setspecific(g_key, tsd); foo(); return NULL; } void foo(void) { puts("foo called!.."); int* tsd; if((tsd = (int*)pthread_getspecific(g_key)) == NULL){ fprintf(stderr, "cannot get the thread specific data!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *tsd); } void key_deleter(void* ptr) { puts("key_deleter called!.."); free(ptr); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2.2.0, Aşağıdaki örnekte ise "static" ömürlü nesne için "__thread" niteleyicisi kullanılmıştır. Dolayısıyla artık bu değişken "thread" e özgüdür. #include #include #include #include #include int __thread g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(void); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # 31 69 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { g_count = 31; sleep(1); foo(); return NULL; } void* thread_proc2(void* param) { g_count = 69; sleep(1); foo(); return NULL; } void foo(void) { printf("%d\n", g_count); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2.2.1, Aşağıdaki örnekte ise "static" ömürlü nesne için "__thread" niteleyicisi kullanılmıştır. Dolayısıyla artık bu değişken "thread" e özgüdür. #include #include #include #include #include int __thread g_count; void* thread_proc1(void* param); void* thread_proc2(void* param); void foo(const char* str); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # Thread - 1 : 11 Thread - 2 : 110 */ int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc1(void* param) { g_count = 1; foo("Thread - 1"); return NULL; } void* thread_proc2(void* param) { g_count = 100; foo("Thread - 2"); return NULL; } void foo(const char* str) { for(int i = 0; i < 5; ++i){ g_count += i; } printf("%s : %d\n", str, g_count); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Pekiyi giriş çıkış akımları da birer "thread safe" öğeler midir? Anımsanacağı üzere "fopen" fonksiyonu "FILE" türünden bir yapı nesnesinin adresini döndürmekte, bu yapının içerisinde de kullanılacak tamponun adresi bulunmaktaydır. Pekala bizler "global" isim alanında "FILE" türden bir gösterici tanımlasak ve bunu da iki farklı "thread" ile kullansak herhangi bir sorun meydana gelmeyecektir. Çünkü POSIX standartlarınca "stream" işlemleri "thread safe" dir. Dolayısıyla özel olarak bir şey yapmamıza gerek yoktur. Benzer biçimde de Windows sistemlerinde de "stream" işlemleri "thread safe" dir. Ancak Standart C standartlarında böyle bir garanti VERİLMEMEKTEDİR. * Örnek 1, #include #include #include #include #include FILE* g_f; void* thread_proc1(void* param); void* thread_proc2(void* param); void exit_sys_errno(const char* msg, int eno); int main(int argc, char** argv) { /* # OUTPUT # */ if((g_f = fopen("test.txt", "w")) == NULL){ fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } int result; pthread_t tid1, tid2; if((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_sys_errno("pthread_create", result); if((result = pthread_join(tid1, NULL)) != 0) exit_sys_errno("pthread_join", result); if((result = pthread_join(tid2, NULL)) != 0) exit_sys_errno("pthread_join", result); fclose(g_f); return 0; } void* thread_proc1(void* param) { for(int i = 0; i < 10000; ++i) fprintf(g_f, "Thread - 1: %d\n", i); return NULL; } void* thread_proc2(void* param) { for(int i = 0; i < 10000; ++i) fprintf(g_f, "Thread - 2: %d\n", i); return NULL; } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >> "Thread Pool" Kavramı: "thread" ler konusunda karşımıza çıkabilecek bir kavramdır. Belli bir "thread" i işin başında oluşturup, onları "suspend" edip, tam ihtiyaç anında kullanmaktır. Böylelikle tam ihtiyaç anında "thread" oluşturmak için zaman kaybetmemiş olacağız. Daha sonra da ihtiyaç anına göre "thread" lerin belli bir kısmını yok ediyorlar. C ve C++ dillerinde bu kavram için standart bir kütüphane yoktur. Benzer şekilde "glibc" kütüphanesinde de bu kavram için fonksiyonlar bulunmamaktadır. Fakat Windows sistemleri bu kavramı desteklemektedir. Dolayısıyla taşınabilirlik açısından üçüncü parti kütüphaneleri kullanmalıyız. Ancak Qt kütüphanesi veya Java ve C# gibi nesne yönelimli bazı diller bu kavramı hazır olarak bünyelerinde bulunmaktadır. Buradaki asıl amaç taşınabilirlik sağlamaktır. Yoksa ilgili dil ve kütüphanelerdeki fonksiyonlar, günün sonunda POSIX "pthread" fonksiyonlarını veya "Windows API" fonksiyonlarını çağırmaktadır. /*================================================================================================================================*/ (72_06_08_2023) > "Threads in POSIX" (devam): >> "Thread Pool" Kavramı (devam): "server" uygulamalarında "server" program çok sayıda "client" dan gelen mesajları işlemesi gerekebilir. Bu tip uygulamalarda tipik döngü ise aşağıdaki gibidir: for(;;){ msg = get_msg(); process_msg(msg); } Burada "get_msg" fonksiyonu "client" dan mesajı alan, "process_msg" ise bu mesajı işleyen fonksiyon olmak üzere; "server" programın amacı "client" ları bekletmeden, onların taleplerini hızlıca yapmaktır. Öte yandan bu tip uygulamalar "TCP/IP socket" programlamada karşımıza çıkmaktadır ki bu proglamlama izleyen paragraflarda açıklanacaktır. Pekiyi bizler buradaki döngüyü nasıl hızlandırabiliriz? Akla ilk gelen yöntem "process_msg" fonksiyonunun başka bir "thread" e yaptırılması ve böylelikle "server" ın hemen bir sonraki mesajı ele almaya çalışmasıdır. Fakat buradaki sıkıntı ise şudur: Her mesaj için bir "thread" oluşturmak, o mesaj ile işimiz bittikten sonra da o "thread" in yok edilmesi gerekmektedir. Fakat bu oluşturma ve yok etme işlemleri de zaman maliyeti oluşturmaktadır. Burada iki yöntem uygulayabiliriz: -> Yukarıdaki döngüyü çalıştıran "thread" in gelen mesajları bir kuyruk sistemine yazması ve "üretici-tüketici" problemi biçiminde "n" tane "client thread" in bu kuyruktaki mesajları işlemesi yöntemidir. Burada işin başında "n" tane "thread" oluşturulacağı için, her mesaj için yeniden "thread" oluşturmaya lüzum kalmayacaktır. Tabii işin sonunda yine ilgili "thread" ler yok edilmektedir. -> "thread pool" yöntemini burada kullanabiliriz. Burada "n" tane "thread" iş başlamadan oluşturulur fakat "suspend" edilir. Her mesaj geldiğinde "thread" ler sırayla uyandırılır ve mesajı işlemesi sağlanır. Fakat mesajın işlenmesi bittiğinde ilgili "thread" ler yok edilmemekte, "suspend" edilerek uyutulmaktadır. Görüleceği üzere bu iki yöntem de birbirine benzer yöntemlerdir. Fakat birinci yöntemdeki "üretici-tüketici" problemi için kuyruk sisteminin oluşturulması ve senkronize edilmesi gerekmektedir. Tabii gelen mesajların bu kuyruğa yazılması ve bu kuyruktan alınması da yine zaman kaybına yol açmaktadır. İkinci yöntemde ise daha doğrudan bir işlem söz konusudur. Çünkü her gelen mesaj için bir "thread" direkt olarak uyandırılmakta, araya herhangi bir kuyruk sistemi dahil edilmemekte, işin sonunda da tekrardan uyutulmaktadır. Pekiyi işin başında bizler kaç adet "thread" oluşturmalıyız, hangi yöntemi kullanmamızdan bağımsız olarak? Açıkçası sistemimiz tek çekirdekli veya işlemcili ise bu "n" sayısının büyük tutulması önemli bir hız kazancı sağlamayacaktır. Tabii bu sistemlerde bizim prosesimiz ile rekabet eden diğer üçüncü parti prosesler varsa, bizimkinin fazla "thread" oluşturması bizim prosesimizin daha çok çalıştırılmasına neden olabilecektir. Eğer ilgili makinede rekabet edecek başka programlar yoksa, bu "n" sayısının büyük tutulması sanıldığı kadar fazla olmayabilecektir. Öte yandan mesajların işlenmesi için başka "thread" ler oluşturulmazsa, bu mesajın işlenmesi sırasında bir bloke oluşması durumunda programın geneli etkilenecektir. Diğer durumda ise sadece o mesajı işleyen tekil "thread" bloke olacağından programın geneli etkilenmeyecektir. Dolayısıyla bu "n" sayısının belirlenmesinde şu kritere dikkat etmeliyiz: -> Mesajın işlenmesi önemli blokelere neden olmuyorsa, bu "n" sayısının işlemci ya da çekirdek kadar olması uygundur. Hatta "Processor Affinity" ile mesajları farklı çekirdeklere bağlayabilir. Eğer mesajın işlenmesi sırasında blokeler oluşacaksa, bu "n" değeri de daha da arttırılabilir. (Tabii bu tür durumlarda Linux sistemlerinde, "Processor Affinity" uygulanmasa bile, CFS algoritmasından dolayı toplam fayda olacak biçimde dengelenme sağlanmaya çalışılmaktadır.) Bütün bunları özetleyecek olursak; -> Tek çekirdekli ya da tek işlemcili sistemlerde mesajın işlenmesi işi "n" tane "thread" e yaptırılması, eğer mesajın işlenmesi sırasında önemli blokeler oluşturacaksa, fayda sağlayacaktır. Ancak ilgili mesajın işlenmesi CPU yoğun ise veya sistemde programımız için rekabet edecek başka prosesler yoksa, sanıldığı kadar fayda sağlamayacaktır. -> Çok çekirdekli ya da çok işlemcili sistemlerde mesajın işlenmesi CPU yoğun ise bu "n" değeri çekirdek ya da işlemci sayısı kadar olabilir. Eğer mesajın işlenmesi sırasında önemli blokeler oluşacaksa, bu "n" değeri büyütülebilir. Öte yandan "thread pool" yönteminde şunu da belirtmekte fayda vardır: Mesajların işlenmesi sırasında havuzda "suspend" edilmiş hiç "thread" kalmadıysa ve hala işlenmeyi bekleyen mesaj varsa, bu durumda yeni "thread" ler oluşturularak havuza eklenecektir. İş yoğunluğu azaldığında da yine havuzdakilerin bir bölümü yok edilecektir. >> "threads.h" kütüphanesi: 2011 yılı itibariyle Standart C kütüphanesine isteğe bağlı bir "thread" kütüphanesi eklenmiştir. Bu kütüphane hala Microsoft tarafından desteklenmemekte, fakat güncel gcc ve clang derleyicileri tarafından desteklenmektedir. Bu kütüphanenin kullanımı kabaca şu şekildedir: -> Bir "thread" oluşturmak için "thrd_create" isimli Standart C Fonksiyonunu çağırmalıyız. Fonksiyonun prototipi aşağıdaki gibidir: int thrd_create( thrd_t *thr, thrd_start_t func, void *arg ); Fonksiyonun birinci parametresi oluşturulacak "thread" i temsil edecek ID değerinin yazılacağı, "thrd_t" türünden, nesnenin adresidir. İkinci parametre ise "thread" akışının başlatılacağı fonksiyonun adresini, üçüncü parametre ise iş bu fonksiyona geçilecek argümanı belirtmektedir. Buradaki "thrd_start_t" türü aşağıdaki biçimde "typedef" edilmiştir: typedef int (*thrd_start_t)(void*); -> Bir "thread" oluşturduktan sonra "thrd_join" ile "join" edilebilir veya "thrd_detach" ile "detached" duruma sokulabilir. Fonksiyonların prototipleri ise aşağıdaki gibidir: int thrd_join( thrd_t thr, int *res ); int thrd_detach( thrd_t thr ); -> Bir "thread" i belli bir süre blokede bekletmek için "thrd_sleep" fonksiyonu kullanılır ki prototipi aşağıdaki gibidir: int thrd_sleep( const struct timespec* duration, struct timespec* remaining ); "threads." kütüphanesi, "time.h" kütüphanesini de bünyesinde barındırması garanti altındadır. Dolayısıyla ekstradan "time.h" kütüphanesini eklememize gerek yoktur. -> Bu kütüphanedeki fonksiyonların geri dönüş değerleri genellikle "int" türden olup başarı durumunda "thrd_success", hata durumunda "thrd_error" ya da "thrd_nomem" değerlerine geri dönerler. Başarı kontrolü ise aşağıdaki biçimde yapılabilir: if(thrd_xxx(...) != thrd_success){ //... } c * Örnek 1, #include #include #include int thread_proc(void* param); int main() { /* # OUTPUT # main-thread : 0 other-thread: 0 main-thread : 1 other-thread: 1 main-thread : 2 other-thread: 2 main-thread : 3 other-thread: 3 main-thread : 4 other-thread: 4 main-thread : 5 other-thread: 5 main-thread : 6 other-thread: 6 main-thread : 7 other-thread: 7 main-thread : 8 other-thread: 8 main-thread : 9 other-thread: 9 */ thrd_t tid; if(thrd_create(&tid, thread_proc, "other-thread") != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } struct timespec ts; ts.tv_sec = 1; ts.tv_nsec = 0; for(int i = 0; i < 10; ++i){ printf("%s : %d\n", "main-thread", i); thrd_sleep(&ts, NULL); } if(thrd_join(tid, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } return 0; } int thread_proc(void* param) { char* name = (char*)param; struct timespec ts; ts.tv_sec = 1; ts.tv_nsec = 0; for(int i = 0; i < 10; ++i){ printf("%s: %d\n", name, i); thrd_sleep(&ts, NULL); } return 0; } C11 ile birlikte senkronizasyon için sadece "mutex" ve "conditional variables" nesneleri bulundurulmuştur. "mutex" nesnesi "mtx_t" türü ile temsil edilmekte olup, kullanım biçimi aşağıdaki gibidir: -> "mtx_init" -> "mtx_lock" veya "mtx_timedlock" veya "mtx_trylock" -> "mtx_unlock" -> "mtx_destroy" Aşağıda ise bu fonksiyonların kullanımına bir örnek verilmiştir: * Örnek 1, Aşağıdaki örnekte "mutex" koruması YAPILMAMIŞTIR. #include #include #include int g_count; int thread_proc1(void* param); int thread_proc2(void* param); int main() { /* # OUTPUT # Result: 0 Result: 1082511 */ printf("Result: %d\n", g_count); thrd_t tid1, tid2; if(thrd_create(&tid1, thread_proc1, NULL) != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } if(thrd_create(&tid2, thread_proc2, NULL) != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } if(thrd_join(tid1, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } if(thrd_join(tid2, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } printf("Result: %d\n", g_count); return 0; } int thread_proc1(void* param) { for(int i = 0; i < 1000000; ++i) ++g_count; return 0; } int thread_proc2(void* param) { for(int i = 0; i < 1000000; ++i) ++g_count; return 0; } * Örnek 2, Aşağıdaki örnekte "mutex" koruması YAPILMIŞTIR. #include #include #include int g_count; mtx_t g_mutex; int thread_proc1(void* param); int thread_proc2(void* param); int main() { /* # OUTPUT # Result: 0 Result: 2000000 */ printf("Result: %d\n", g_count); if(mtx_init(&g_mutex, mtx_plain) != thrd_success){ fprintf(stderr, "cannot init mutex!..\n"); exit(EXIT_FAILURE); } thrd_t tid1, tid2; if(thrd_create(&tid1, thread_proc1, NULL) != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } if(thrd_create(&tid2, thread_proc2, NULL) != thrd_success){ fprintf(stderr, "cannot create thread!..\n"); exit(EXIT_FAILURE); } if(thrd_join(tid1, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } if(thrd_join(tid2, NULL) != thrd_success){ fprintf(stderr, "cannot join thread!..\n"); exit(EXIT_FAILURE); } printf("Result: %d\n", g_count); return 0; } int thread_proc1(void* param) { for(int i = 0; i < 1000000; ++i){ mtx_lock(&g_mutex); ++g_count; mtx_unlock(&g_mutex); } return 0; } int thread_proc2(void* param) { for(int i = 0; i < 1000000; ++i){ mtx_lock(&g_mutex); ++g_count; mtx_unlock(&g_mutex); } return 0; } Burada bizler ilgili kütüphanenin çok az bir kısmına değindik. Kütüphanedeki diğer Standart C Fonksiyonları için ilgili dökümanlara bakabiliriz. >> C++ dilindeki "thread" kütüphanesi: Şablon temelli bir kütüphane olup, sınıfsal bir tasarıma sahiptir. Yine 2011 yılında dile eklenmiştir. C dilinin kütüphanesine nazaran daha ayrıntılıdır. Bu sınıf türünden nesnenin "ctor." fonksiyonuna akışın başlayacağı fonksiyonu geçebilmekteyiz. Fakat mutlak suretle ".join()" ya da ".detach()" isimli fonksiyonları çağırmamız gerekmektedir aksi halde bir "exception throw" söz konusu olacaktır. * Örnek 1, #include #include #include void thread_proc(); int main() { /* # OUTPUT # main-thread: 0 other-thread: 0 main-thread: other-thread: 11 other-thread: main-thread: 2 2 main-thread: other-thread: 3 3 other-thread: main-thread: 44 other-thread: 5 main-thread: 5 other-thread: main-thread: 66 main-thread: other-thread: 7 7 other-thread: main-thread: 8 8 other-thread: main-thread: 99 */ std::thread t{thread_proc}; for(int i = 0; i < 10; ++i){ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "main-thread: " << i << "\n"; } t.join(); return 0; } void thread_proc() { for(int i = 0; i < 10; ++i){ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "other-thread: " << i << "\n"; } } * Örnek 2, #include #include #include void thread_proc(int count); int main() { /* # OUTPUT # main-thread: 10 other-thread: 0 other-thread: 1 main-thread: 11 other-thread: 2 main-thread: 12 other-thread: 3 main-thread: 13 other-thread: 4 main-thread: 14 other-thread: 5 main-thread: 15 other-thread: 6 main-thread: 16 other-thread: 7 main-thread: 17 other-thread: 8 main-thread: 18 other-thread: 9 main-thread: 19 */ std::thread t{thread_proc, 10}; for(int i = 10; i < 20; ++i){ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "main-thread: " << i << "\n"; } t.join(); return 0; } void thread_proc(int count) { for(int i = 0; i < count; ++i){ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "other-thread: " << i << "\n"; } } Yine C++ dilinde de çeşitli senkronizasyon nesneleri bulunmaktadır. Bu nesneler ise "mutex" isimli başlık dosyasındadır. Artık "lock" ve "unlock" işlemleri ilgili sınıfın üye fonksiyonları üzerinden gerçekleştirilmektedir. * Örnek 1, #include #include #include #include int g_count; std::mutex g_mutex; void thread_proc1(int count); void thread_proc2(int count); int main() { /* # OUTPUT # Count: 0 Count: 2000000 */ std::cout << "Count: " << g_count << "\n"; std::thread t1{thread_proc1, 10}; std::thread t2{thread_proc2, 100}; t1.join(); t2.join(); std::cout << "Count: " << g_count << "\n"; return 0; } void thread_proc1(int count) { for(int i = 0; i < 1000000; ++i){ g_mutex.lock(); ++g_count; g_mutex.unlock(); } } void thread_proc2(int count) { for(int i = 0; i < 1000000; ++i){ g_mutex.lock(); ++g_count; g_mutex.unlock(); } } Buradaki C++ kütüphanesi C dilinin kütüphanesinden daha kapsamlıdır. Kütüphane şablon temelli olması hasebiyle, kullanırken daha da dikkatli olmamız gerekmektedir. Tabii C++ dilindeki bu kütüphane, UNIX/Linux ve macOS sistemlerinde "pthread", Windows sistemlerinde ise "Windows API" fonksiyonları kullanılarak gerçekleştirilmiştir. > UNIX/Linux Sistemlerinde Sinyal İşlemleri: Sinyal işlemleri UNIX/Linux sistemlerinde sistem programlama faaliyetlerinde yoğun bir biçimde kullanılmaktadır. Dolayısıyla iyi bilinmesi gerekmektedir. "thread" ler konusu kadar kapsamlı olmasa bile yine de kapsamlı bir konudur. Pekiyi nedir bu sinyal kavramı? Sinyaller, kesme mekanizmalarına da benzetilebilir. UNIX/Linux sistemlerde asenkron işlem yapılmasına olanak sağlarlar. Sinyaller normalde proseslere gönderilmektedir fakat "thread" kavramının da işletim sistemine eklenmesiyle "thread" lere de sinyal gönderilebilir. Fakat burada dikkat etmemiz gereken husus ise sinyali sadece kendi "thread" imize gönderebiliyor oluşumuzdur. Çünkü "thread" lerin ID değerleri yalnızda ait oldukları proseslerde anlamlıdırlar. Bir prosese sinyal geldiğinde prosesin akışı o anda kesilir ve ismine "Sinyal Fonksiyonu (Signal Function)" denilen bir fonksiyon çalıştırılır. Daha sonra bu fonksiyon bittiğinde akış tekrardan kaldığı yere geri döner ve o noktadan çalışmaya devam eder. Böylelikle bir işi yaparken araya başka işlemlerin girebilmesine olanak sağlamış oluyoruz. Pekiyi bir sinyal nasıl oluşur? Burada çeşitli durumlar söz konusudur. Örneğin, programcının yaptığı çeşitli ihlallerde, işletim sistemi tarafından prosese sinyal gönderilmektedir. Öte yandan bazı sinyaller ise bazı aygıt sürücüleri tarafından proseslere gönderilir. Örneğin, terminal aygıt sürücüsü prosesin ilişkin olduğu terminale, programcının "CTRL+C" ya da "CTRL+Backspace" tuşlarına basmasından dolayı, bazı sinyaller gönderebilmektedir. Benzer şekilde bir program da başka bir programa açıkça sinyal gönderebilmektedir. Örneğin, bazı POSIX fonksiyonları belirli şartlar oluştuğunda sinyal gönderebilmektedir. Diğer yandan bir sinyal bir prosese gönderildiğinde o prosesteki hangi "thread" in bu sinyali alacağı, POSIX standartlarınca, ilgili işletim sistemini yazanların isteğine bırakılmıştır. Bütün bunlara ek olarak, her bir sinyal bir numaraya sahiptir. POSIX sistemlerinde sinyallere ilişkin numaraların ne olacağı yine işletim sisteminin yazanların isteğine bırakılmıştır ancak taşınabilirlik sağlamak için bu sinyal numaraları "signal.h" başlık dosyası içerisinde "SIGXXX" içerisinde sembolik sabitler ile "define" edilmişlerdir. Dolayısıyla bu sembolik sabitleri kullanmalıyız. Bir diğer yandan, yukarıda da açıklandığı üzere, bir prosese sinyal geldiğinde prosesin akışı o anda kesilip ismine sinyal fonksiyonu denilen fonksiyona geçmektedir. Aslında bu durum tam olarak böyle değildir. Şöyleki; prosese bir sinyal gelmesi halinde, eğer programcı bir sinyal fonksiyonu "set" etmişse, yukarıda anlatılan senaryo gerçekleşmektedir. Eğer böyle bir sinyal fonksiyonu "set" EDİLMEMİŞSE, bu durumda "Varsayılan Eylem (Default Action)" uygulanmaktadır. Bu ise sinyalin ne olduğuna bağlıdır. Bazı sinyaller için bu aksiyon ilgili sinyalin görmezden gelinmesiyken, bazı sinyallerde prosesin sonlandırılması biçimindedir. Bazı sinyallerde ise bu aksiyon hem prosesin sonlandırılması hem de "core file" oluşturulması biçimindedir. Buradaki "core file" aslında teşhis amacıyla oluşturulan ve "debugger" altında incelenebilen özel dosyalardır. Dolayısıyla programcı bu varsayılan aksiyonları sinyal temelinde öğrenmesi gerekmektedir. Bütün bunlara ek olarak, bir sinyal oluştuğunda ilk önce bu sinyal hedef prosese "deliver" edilir. Daha sonra iş bu proses gelen bu sinyali mümkün olan en kısa zamanda işlemek ister. Eğer bir sinyal fonksiyonu da "set" edilmişse, bu fonksiyonu en kısa zamanda çağırmak isteyecektir. İşte bir prosese sinyalin geldiği an ile ilgili sinyal fonksiyonunun çağrılması arasındaki bu zamana ise sinyalin askıda olması, yani "pending" olması denmektedir. Eğer programın akışı sinyalin geldiği anda bir sistem fonksiyonu içerisindeyse ve bu sistem fonksiyonu da uzun sürecek bir eylem başlatmışsa ya da açıkça bloke olmuşsa, ilgili sistem fonksiyon işletim sistemi tarafından başarısızlıkla sonlandırır ve tez zamanda ilgili sinyal fonksiyonunu çağırır. Eğer bir POSIX fonksiyonu da gelen sinyalden dolayı başarısız olmuşsa, "errno" değişkeninin değeri "EINTR" değerine çekilecektir. Buradan da görüleceği üzere bir sinyal geldiği anda programın akışınının aniden kesilmesi gerçekleşmez, bir takım ön kontrollerden geçmektedir. Örneğin, bizler bir "pipe" üzerinden "read" yapmak isteyelim ancak okunacak hiç "byte" bulunmasın. Bu durumda "read" fonksiyonu blokeye yol açacaktır. İşte bu sırada bir sinyal gelirse, "read" fonksiyonu başarısızlıkla geri döner ve "errno" değişkeni "EINTR" değerini alır. Bu tür durumlarda bizler ilgili fonksiyonun başarısız olma nedeninin gelen sinyalden olduğunu anlayıp, onu tekrardan çağırmak da isteyebiliriz. Şöyleki: while((result = read(...)) == -1 && errno == EINTR) ; if(result == -1) exit_sys("read"); Pek tabii programcı bu tür POSIX fonksiyonlarının otomatik olarak yeniden çağrılmasını da sağlayabilir. Eğer bu sağlanmışsa, ilgili fonksiyon hiç geri dönmez ancak ilgili sinyal fonksiyonu da çalıştırılır. Bu otomatik yeniden çağırma işlemi ise kütüphane tarafından değil, bizzat çekirdek tarafından gerçekleştirilir. Tabii bu durum her POSIX fonksiyonu için de geçerli değildir. Buradaki husus ilgili sistem fonksiyonunun ya da onu çağıran POSIX fonksiyonunun "yavaş bir fonksiyon" olması gerekmektedir. Yani blokeye yol açabilecek sistem fonksiyonlarıdır. Bir diğer deyişle programcı, çağırdığı sistem fonksiyonunun ya da POSIX fonksiyonunun bir sinyal karşısındaki davranışını da BİLMEK ZORUNDADIR. Tabii bir sinyal için sinyal fonksiyonu atanmamışsa ve varsayılan aksiyon da görmezden gelinmesiyse, bu durumda ilgili sistem fonksiyonunun ya da POSIX fonksiyonunun başarısız olmasının, o fonksiyonu yeniden çağırmanın da bir önemi kalmamaktadır. /*================================================================================================================================*/ (73_12_08_2023) > UNIX/Linux Sistemlerinde Sinyal İşlemleri (devam): Şimdi bu "yavaş fonksiyonlar" ile kastedilen şeyi detaylandıralım; burada kastedilen esas şey programın akışının bir sistem fonksiyonu içerisinde göreli bir biçimde uzun zaman bekleyebilmesidir. Örneğin, "read" fonksiyonu ile bir disk dosyasından okuma yapmak isteyelim ve bu işlem sırasında da bir sinyal meydana gelsin. Şimdi bu okuma işlemi yavaş bir işlem değildir. Dolayısıyla "read" fonksiyonu başarı ile geri döndükten sonra, eğer "set" edilmiş ise, ilgili sinyal fonksiyonu çağrılacaktır. Ancak "read" fonksiyonu ile bir "pipe" ya da "socket" üzerinden yaparsak, okunacak bilgi kalmadığında bloke oluşacaktır. İşte bu durumda "read" fonksiyonunu "yavaş fonksiyon" olarak değerlendireceğiz. Böylesi bir durumda da sinyal gelmesi halinde ilk önce "read" fonksiyonu başarısızla sonuçlanacak ve programın akışı daha sonra ilgili sinyal fonksiyonuna geçecek eğer herhangi bir fonksiyon belirlenmişse. Pekiyi bizler bir sinyal oluştuğunda çağrılacak olan "sinyal fonksiyonu" nu nasıl "set" edebiliriz? Burada karşımıza iki farklı yöntem çıkmaktadır. İlki "signal" fonksiyonu kullanılarak, ikincisi ise "sigaction" fonksiyonu ile. Maalesef POSIX standartlarının oluşturulduğu dönemlerde, UNIX ve türevi sistemlerdeki ilgili sinyal fonksiyonunun davranışları arasında farklılık görülmekteydi. Dolayısıyla POSIX standartlarınca "signal" fonksiyonunun davranışı "öyle de davranabilir, böyle de davranabilir" biçimindedir. Bu da beraberinde taşınabilirlik sorunlarını getirdi. İşte "signal" fonksiyonunun oluşturduğu taşınabilirlik sorunu, "sigaction" isimli POSIX fonksiyonu ile çözülmüştür. Kursun işleyişi sırasında ilk olarak "signal", daha sonra "sigaction" fonksiyonları anlatılacak olup programcının "sigaction" fonksiyonunu kullanması tavsiye edilmektedir. Şimdi de bu fonksiyonları inceleyelim: >> "signal" : Fonksiyonun prototipi aşağıdaki gibidir. #include void (*signal(int sig, void (*func)(int)))(int); Görüleceği üzere fonksiyonun prototipi karışık. Bunu aşağıdaki biçimde de yazabiliriz: #include void (*signal(int sig, void (*func)(int))) (int); Buradan da anlaşılacağı üzere "signal" fonksiyonunun geri dönüş değeri bir fonksiyon göstericisi. Öyle bir gösterici ki gösterdiği fonksiyonun geri dönüş değeri "void", aldığı parametre ise "int" türden. Pekiyi bu "signal" fonksiyonu ne türden argümanlar almaktadır? Şöyleki: #include void (*signal ( int sig, void (*func)(int) ) ) (int); Buradan da görüleceği üzere "signal" fonksiyonunun birinci parametresi "int" türden, ikinci parametresi ise yine bir fonksiyon göstericisi. Bu fonksiyon göstericisi ise öyle bir gösterici ki gösterdiği fonksiyon "void" türden geri dönüş değerine sahip ve "int" türden parametre almaktadır. Yani "signal" fonksiyonunun birinci parametresine hangi sinyal için "set" işlemi uygulanacağı, ikinci parametre ise çağrılacak fonksiyonu belirmektedir. Fonksiyonun geri dönüş değeri ise başarı durumunda bir önceki sinyal fonksiyonunun adresiyle, hata durumunda ise "SIG_ERR" özel değeri ile geri dönmektedir. Bu özel değer ise "signal.h" içerisinde tanımlanmış bir fonksiyon adresidir ve başarısızlığı anlatmaktadır. * Örnek 1, Aşağıdaki program normal şartlarda "for" döngüsünü çalıştırmaktadır. Fakat klavyeden "CTRL+C" yaptığımız zaman işletim sistemi prosesimize bir "SIGINT" sinyalini gönderecektir. Normal şartlarda bu sinyal için herhangi bir sinyal fonksiyonu atanmadığı zaman proses sonlandırılırken, aşağıdaki örnekte bir fonksiyon atadığımız için artık prosesimiz sonlanmayacak ve atanmış olan fonksiyon çağrılacaktır. O fonksiyondan sonra akış tekrardan "for" döngüsüne gireceği için, programımızı sonlandırmak için başka yöntemler denemek durumunda kalacağız. #include #include #include void sigint_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # ^CA signal occured!.. ^CA signal occured!.. ... */ if(signal(SIGINT, sigint_handler) == SIG_ERR) exit_sys("signal"); for(;;) ; return 0; } void sigint_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise "read" fonksiyonu yavaş fonksiyon olarak ele alınmıştır. Yukarıda açıklanan otomatik yeniden başlatma özelliği aktif edildiğinden, ilgili sinyal fonksiyonundan hemen sonra tekrardan "read" fonksiyonu çağrılmaktadır. #include #include #include #include void sigint_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # ^CA signal occured!.. ^CA signal occured!.. ^CA signal occured!.. ... */ if(signal(SIGINT, sigint_handler) == SIG_ERR) exit_sys("signal"); char buffer[4096 + 1]; ssize_t result; if((result = read(0, buffer, 4096)) == -1) exit_sys("read"); buffer[result] = '\0'; printf("Entered Value: %s\n", buffer); return 0; } void sigint_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi "signal" fonksiyonunun ikinci parametresindeki fonksiyon göstericisinin gösterdiği fonksiyonun parametresi neden "int" türdendir? Bunun yegane amacı, işletim sisteminin oluşan sinyalin numarasını da sinyal fonksiyonuna aktardığından dolayı, farklı sinyallere aynı fonksiyonu "set" etmektir. Böylelikle ilgili sinyal fonksiyonunun gövdesinde hangi sinyal nedeniyle fonksiyonun çağrılmış olduğunu belirleyebilir. Diğer yandan "signal" fonksiyonunun ikinci parametresine şu değerlerden birisini de geçebiliriz: "SIG_DFL", "SIG_IGN". -> "SIG_DFL" : Sinyali "default action" a çekmek için kullanılır. Yani bu değer kullanıldığında, sanki hiç sinyal fonksiyonu "set" edilmemiş gibi bir etki oluşturmaktadır. Varsayılan davranışın ne olacağı ise ilgili sinyalin dökümanlarında belirtilmiştir. -> "SIG_IGN" : Sinyali görmezden gelme, yani "ignore" etme, aksiyonu alması için kullanılır. Bir sinyal "SIG_IGN" ile görmezden gelinirse, bu sinyal oluştuğunda, sanki hiç oluşmamış gibi bir davranış gösterecektir. Fakat her sinyal "ignore" EDİLEMEMEKTEDİR. Pekala "signal" fonksiyonunun geri dönüş değeri de "SIG_DFL" veya "SIG_IGN" değerlerinden birisi olabilir. Örneğin, biz bir sinyali ilk kez "set" ediyorsak bir önceki sinyal fonksiyonu muhtemelen "SIG_DFL" veya "SIG_IGN" biçiminde olacaktır. * Örnek 1, #include #include #include void sigint_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # SIG_DFL */ void (*old_handler)(int); if((old_handler = signal(SIGINT, SIG_IGN)) == SIG_ERR) exit_sys("signal"); if(old_handler == SIG_DFL) printf("SIG_DFL"); return 0; } void sigint_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de "signal" fonksiyonunun olumsuz yönlerine değinelim. Yukarıda da açıklandığı üzere POSIX standartlarınca bu fonksiyon "implementation defined" olarak tanımlanmıştır. Bu da taşınabilirliği bozmaktadır. Bunun da yegane sebebi şunlardır: -> Eski "AT&T" UNIX sistemlerinde ve bu kökten gelen sistemlerde "signal" fonksiyonu ile bir "set" işlemi yapıldıktan sonra , bir sinyal oluşması durumunda, ilgili sinyal aksiyonu varsayılan olarak değiştiriliyordu. Bunu gidermek için de programcılar ilgili sinyal fonksiyonunun bünyesinde yeniden bir "set" işlemi yapmaktaydılar. Örneğin, void signal_handler(int sno) { if(signal(SIGXXX, signal_handler) == SIG_ERR) exit_sys("signal"); //... } Böylece sinyal varsayılana çekilir çekilmez tekrardan olması gereken değere "set" edilmektedi. Fakat aşağıdaki durumda için bir önlem alınamıyor, bu da programın sonlanmasına neden olmaktaydı. void signal_handler(int sno) { /* BU NOKTADA AYNI İLGİLİ SİNYAL VARSAYILANA ÇEKİLİ DURUMDADIR. EĞER AYNI SİNYALDEN EĞER AYNI SİNYAL YENİDEN OLUŞURSA, PROGRAM SONLANABİLMEKTEDİR. */ if(signal(SIGXXX, signal_handler) == SIG_ERR) exit_sys("signal"); //... } Ancak "BSD" UNIX sistemlerinde bu problem, ilgili sinyalin varsayılana çekilmemesi sağlanarak aşılmaya çalışılmıştır. Dolayısıyla BSD ve türevi sistemlerde bu varsayılana çekme durumu gerçekleşmemektedir. Linux sistemlerinde ise "AT&T" semantiği uygulandığı için yine ilgili sinyal varsayılana çekilmektedir. Ancak "signal" isimli POSIX fonksiyonu, "glibc" kütüphanesinin belirli bir versiyonundan sonra, "signal" sistem fonksiyonu yerine "sigaction" sistem fonksiyonu kullanılarak yazıldığı için, sinyali yine varsayılana ÇEKMEMEKTEDİR. Dolayısıyla artık Linux sistemlerinde "AT&T" değil, "BSD" semantiği uygulanmaktadır. POSIX standartları ise "AT&T" & "BSD" sistemlerindeki farklılıkların geçerli olabilmesi için mekanizmayı işletim sistemini yazanların isteğine bırakmıştır. -> "AT&T" ve bu kökten gelen sistemlerde bir sinyal oluştuğunda o sinyal, ilgili sinyal fonksiyonu çalıştığı süre boyunca bloke EDİLMİYORDU. Yani aynı sinyalden üst üste gelmesi durumunda ilgili sinyal fonksiyonu da "recursive" biçimde çalıştırılmış oluyor, bu da "stack overflow" gibi sorunlara neden olabiliyordu. "BSD" sistemlerinde ise bir sinyal oluştuğunda o sinyal, ilgili sinyal fonksiyonu çalıştığı süre boyunca BLOKE EDİLİYORDU. Böylece "recursive" biçimde ilgili sinyal fonksiyonunun çalışması engellenmiş olurdu. Linux sistemlerinde ise "signal" isimli sistem fonksiyonu "AT&T" semantiğini, "signal" isimli POSIX fonksiyonu ise, "glibc" kütüphanesinin belirli bir versiyonundan sonra, "sigaction" isimli sistem fonksiyonunu çağırmakta ve "BSD" semantiğini uygulamaktadır. POSIX standartlarınca, ilgili sinyal fonksiyonu çalıştığı sürece aynı sinyalin bloke edilip edilmeyeceğini işletim sistemini yazanlara bırakmıştır. -> "AT&T" ve bu kökten gelen sistemlerde yavaş sistem fonksiyonları başarısız olmakta ve "errno" değişkeni "EINTR" değerini almaktadır. Halbuki "BSD" sistemlerinde ise otomatik "restart" işlemi yapılmaktadır. Linux sistemleri ise "AT&T" semantiği kullandığı için otomatik "restart" işlemi yapmamaktadır. Ancak "signal" fonksiyonu, "glibc" kütüphanesinin belirli bir versiyonundan sonra, "sigaction" sistem fonksiyonu çağrılacak biçimde yazılmıştır ve otomatik "restart" işlemi YAPILMAKTADIR. POSIX standartlarınca ise bu özellik ilgili işletim sistemini yazanların isteğine bırakılmıştır. Bu durumda Linux sistemlerindeki "glibc" kütüphanesinde bulunan "signal" fonksiyonu: -> Sinyali tekrardan varsayılana çekmez. -> Sinyali, sinyal fonksiyonu çalıştığı sürece bloke etmekte. -> Otomatik "restart" işlemini uygulamaktadır. >> "alarm" fonksiyonu : Öte yandan "alarm" isimli bir POSIX fonksiyonu daha bulundurulmuştur. Bu fonksiyona geçilen süre bilgisi dolduğunda, fonksiyonu çağıran prosese "SIGALRM" isimli sinyali göndermektedir. Bu sinyalin varsayılan davranışı ise prosesin sonlandırılması biçimindedir. "alam" fonksiyonunu birden çok çağırdığımızda süreler biriktirilmez ve her yeni çağrı bir öncekini devre dışı bırakarak sayacı yeniden "set" etmektedir. Fonksiyonun prototipi şöyledir: #include unsigned alarm(unsigned seconds); Fonksiyon saniye sayısını parametre olarak alır. Bu parametreye "0" değerinin geçilmesi durumunda, daha önceki "alarm" işlevinin devre dışı bırakılacağı anlamına gelmektedir. Eğer daha önce bu fonksiyon çağrılıp da bir "set" işlemi yapılmışsa, o "set" işlemi için kalan saniye sayısını geri döndürmektedir. Eğer daha önce bu fonksiyon çağrılmamışsa ya da çağrılmış fakat süresi dolmuşsa, "0" değeri ile geri dönmektedir. FONKSİYON BAŞARISIZ OLAMAMAKTADIR. * Örnek 1, #include #include #include #include void sigalrm_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 1 2 3 A signal occured!.. 4 5 6 7 8 9 */ if(signal(SIGALRM, sigalrm_handler) == SIG_ERR) exit_sys("signal"); alarm(5); for(int i = 0; i < 10; ++i){ sleep(1); printf("%d\n", i); } return 0; } void sigalrm_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (74_13_08_2023) > UNIX/Linux Sistemlerinde Sinyal İşlemleri (devam): >> "sigaction" : "signal" fonksiyonunun oldukça iyileştirilmiş halidir ve o fonksiyondaki semantik belirsizlikler kaldırılmıştır. Dolayısıyla programcının "sigaction" fonksiyonunu kullanması iyi bir tekniktir. Ancak bu fonksiyonun kullanımı, "signal" fonksiyonu kullanımından daha karışıktır. Fonksiyonun prototipi ise aşağıdaki gibidir: #include int sigaction(int sig, const struct sigaction * act, struct sigaction * oact); Fonksiyonun birinci parametresi yine "set" edilecek sinyalin numarasını belirtmektedir. Fonksiyonun ikinci parametresi "sigaction" isimli bir yapı nesnesinin adresini almaktadır. Programcı sinyal "set" işlemi ile ilgili bazı bilgileri, "sigaction" türünden yapı nesnesinin içerisine yerleştirir ve sonra bu nesnenin adresini de fonksiyona geçer. Fonksiyonun üçüncü parametresi yine bu türdendir, "NULL" değeri geçilebilir. Bu durumda bir önceki sinyal "set" işlemine ait özellikleri temin edememiş oluruz. Ancak "NULL" değeri geçilmezse, bu parametreye adresi geçilen yapı nesnesine daha önceki sinyal "set" özellikleri yerleştirilecektir. Pekala ikinci parametreye de "NULL" değeri geçilebilir, bu durumda sadece bir önceki sinyal "set" işlemine ait özellikleri temin etmiş olacağız. Hem ikinci hem de üçüncü parametreye "NULL" değeri de geçilebilir. Fakat böyle yapmanın bir mantığı yoktur. Fonksiyon başarı durumunda "0" değerine, hata durumunda ise "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Fonksiyonun kullanımındaki en önemli nokta şüphesiz "sigaction" yapı nesnesinin içinin doldurulmasıdır. Bu yapı türü "signal.h" içerisinde aşağıdaki biçimde bildirilmiştir: struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; }; -> "sa_handler" : Sinyal fonksiyonunun adresini temsil etmektedir. "SIG_DFL" ve "SIG_IGB" özel değerleri de girilebilir. -> "sa_sigaction" : Daha gelişmiş olan sinyal fonksiyonunun adresini temsil etmektedir. UNIX/Linux sistemlerine "güvenilir sinyaller" adı altında bazı semantik eklemeler sonucunda böylesi bir sinyal fonksiyonu da eklenmiştir. Yapının ya bu elemanına ya da "sa_handler" isimli elemanına adres geçmesi gerekmektedir. Her iki elemana da adres geçmemesi GEREKMEKTEDİR. "SIG_DFL" ve "SIG_IGB" özel değerleri de girilebilir. -> sa_mask : Sinyal bloke kümesini belirtmektedir. Varsayılan durumda bir sinyal oluştuğu zaman, ilgili sinyal fonksiyonu çalıştığı süre boyunca, aynı numaralı sinyal bloke olmaktadır. Bir sinyal bloke olduğunda, o sinyal ilgili porsese gelmesi durumunda "pending" durumda bekletilmesi demektir. Fakat sinyaller istiflenemezler. Yani bir sinyal fonksiyonu işletilirken aynı sinyalden birden fazla gelmesi durumunda, akış sinyal fonksiyonundan çıktıktan sonra, sadece bir defa için ilgili sinyal fonksiyonu yeniden çağrılır. Bu şekilde ilgili sinyal fonksiyonunun "recursive" biçimde çağrılması engellenmiş olmaktadır. Ancak programcı isterse, ilgili sinyal fonksiyonu çalıştığı sürece, başka sinyallerinde bloke edilmesini sağlayabilir. İşte yapının bu elemanı bu amaçla kullanılmaktadır. Bir çeşit bir "bit" dizisi olarak düşünülmüştür. Yani bu dizinin çeşitli elemanları, çeşitli sinyalleri bloke edilip edilmeyeceğini belirtmektedir. Dolayısıyla bu dizinin elemanlarını "set" ve "reset" etmek için çeşitli fonksiyonlar bulundurulmuştur. Tabii bu fonksiyonlar makro biçimde de yazılmış olabilirler. Bunların listesi şu şekildedir: "sigemptyset", "sigfillset", "sigaddset", "sigdelset", "sigismember". -> "sigemptyset" : "bit" dizisi içerisindeki bütün "bit" leri "reset" etmektedir. -> "sigfillset" : "bit" dizisi içerisindeki bütün "bit" leri "set" etmektedir. -> "sigdelset" : "bit" dizisinin belli bir "bit" ini "reset" etmektedir. -> "sigaddset" : "bit" dizisinin belli bir "bit" ini "set" etmektedir. -> "sigismember" : Belli bir sinyalin "bit" dizisi içerisindeki değerini, yani "set" ya da "reset" olduğunu, bize vermektedir. Fonksiyonlar başarı durumunda "0", hata durumunda "-1" değerine geri dönmektedir. Ancak "sigismember" başarı durumunda "0" ya da "1", hata durumunda ise "-1" değerine geri dönmektedir. Tabii bu fonksiyonların başarısının kontrolüne gerek yoktur. Bu fonksiyonların tipik kullanımı aşağıdaki biçimdedir: sigset_t sset; sigemptyset(&sset); sigaddset(&ssset, SIGINT); sigaddset(&sset, SIGALARM); Böylelikle "SIGINT" ve "SIGALARM" sinyalleri bir küme haline getirilmiş oldu. Henüz bu sinyaller için bloke işlemi UYGULANMADI. Bu bloke işlemini de tipik olarak aşağıdaki biçimde yapabiliriz: struct sigaction sa; //... sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGINT); sigaddset(&sa.sa_mask, SIGALRM); //... if(sigaction(SIGTERM, &sa, NULL) == -1) exit_sys("sigaction"); Artık "SIGTERM" sinyali için oluştuğunda ilgili sinyal fonksiyonu çağrılacaktır. Programın akışı da bu fonksiyon içerisinde bulunduğu sürece "SIGINT" ve "SIGALRM" sinyalleri bloke edilecektir. Zaten varsayılan durumda ilgili sinyalin kendisinin de bloke olduğundan bahsetmiştik. Bir diğer deyişle ilgili sinyalin kendisi zaten bloke olmakta. Bizler de bu "bit" dizisi ile ilave bloke olmasını istediğimiz sinyalleri belirtmiş oluyoruz. Programcı yapının "sa_mask" elemanına bir değer mutlaka atamalıdır. Anımsanacağı üzere yerel nesnelerin içerisinde rastgele değerler vardır. Kaldıki "global" isim alanındaki nesne olsalar bile içi boş bir "bit" dizisi belirtmesi garanti değildir. Dolayısıyla yapının bu elemanına şöyle değer atayabiliriz: sigemptyset(&sa.sa_mask); Artık ilgili sinyal fonksiyonu çalışırken, ilgili sinyal dışındaki hiç bir sinyal bloke edilmeyecektir. -> "sa_flags" : Bazı sembolik sabitlerin "bitwise-OR" işlemine sokulmasıyla oluşturulur. Bu sembolik sabitler şunlardır: "SA_RESETHAND", "SA_RESTART", "SA_SIGINFO", "SA_NODEFER", "SA_NOCLDWAIT", "SA_NOCLDSTOP", "SA_ONSTACK". Tabii bu bayrakları kullanmak istemiyorsak, "0" değerini de atayabiliriz. -> "SA_RESETHAND" : İlgili sinyan fonksiyonu çalıştırıldığında, sinyal otomatik olarak varsayılana çekilir. Varsayılan durumda bu bayrak "set" EDİLMEMİŞTİR. Bu bayrak "AT&T" semantiğini uygulayabilmek için bulundurulmuştur. -> "SA_RESTART" : Bu bayrak "set" edilirse yavaş POSIX fonksiyonları, yani onların çağırdığı sistem fonksiyonları, sırasında bir "sinyal" oluşması durumunda ilgili sinyal fonksiyonu çağrılır. Ancak fonksiyon başarısız olmaz çünkü arka plandaki sistem fonksiyonunun "restart" edilmesi çekirdek tarafından otomatik olarak yapılmaktadır. Bir diğer deyişle programın akışı ilgili sistem fonksiyonu ilgili sinyalin gelmesi durumunda başarısız olmayacaktır. -> "SA_SIGINFO" : Sinyal fonksiyonu için "sigaction" yapısının "sa_handler" elemanı değil "sa_sigaction" elemanı dikkate alınmaktadır. Varsayılan durumda yapının "sa_handler" elemanı kullanılmaktadır. Bu konunun detaylarına "Gerçek Zamanlı Sinyaller (Realtime Signals)" konusu sırasında değinilecektir. -> "SA_NODEFER" : Anımsanacağı üzere varsayılan durumda bir sinyal fonksiyonu çalışırken, o sinyal bloke edilmektedir. Bu bayrak kullanılırsa, o sinyal de artık bloke olmayacaktır. Bu durumda ilgili sinyal fonksiyonu "recursive" biçimde çağrılabilmektedir. -> "SA_NOCLDWAIT" : Bu bayrak "SIGCHLD" sinyali için anlamlıdır. Otomatik olarak "zombie process" oluşmasını engellemek için kullanılır. Bu bayrak "set" edildiğinde ilgili prosesin sonlanması ile kaynakları da "wait" fonksiyonlarını beklemeden serbest bırakılır. Dolayısıyla programcı artık "wait" işlemi yapmayacaktır. -> "SA_NOCLDSTOP" : Bu bayrak belirtildiğinde, alt proses durdurulduğunda ya da yeniden çalışmaya devam ettirildiğinde, üst prosese "SIGCHLD" sinyalini göndermeyecektir. -> "SA_ONSTACK" : İç içe sinyal oluştuğunda taşma problemleri gerçekleşebilmektedir. Bunun için alternatif "stack" kullanımı da mümkündür. İşte bu bayrak ilgili sinyal fonksiyonu çalışırken alternatif "stack" alanının kullanılacağını belirtmektedir. Tabii bu alternatif "stack" alanının da "sigaltstack" POSIX fonksiyonu ile "set" edilmesi gerekmektedir. Şimdi de bu fonksiyonun kullanımına ilişkin örnekler verelim: * Örnek 1, Aşağıdaki örnekte ilgili sinyal oluştuğunda ve ilgili sinyal fonksiyonu çalıştığı sürece, başka bir sinyal bloke edilmeyecektir. İşlem sonunda "restart" olmayacaktır. #include #include #include #include void sigalarm_handler(int sig_no); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 1 2 3 4 ALARM 5 6 7 8 9 */ struct sigaction sa; sa.sa_handler = sigalarm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if(sigaction(SIGALRM, &sa, NULL) == -1) exit_sys("sigaction"); alarm(5); for(int i = 0; i < 10; ++i){ printf("%d\n", i); sleep(1); } } void sigalarm_handler(int sig_no) { printf("ALARM\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Anımsanacağı üzere "glibc" kütüphanesindeki "signal" POSIX fonksiyonu belli bir versiyondan sonra "sigaction" isimli sistem fonksiyonu çağrılarak yazılmıştır. Dolayısıyla yeni sürümlerde sinyali işin başında varsayılan aksiyona çekmiyor, sinyal fonksiyonu çalıştığı sürece aynı sinyali bloke ediyor ve otomatik "restart" işlemi de yapıyor. İşte aşağıda bu işlemleri gerçekleştiren bir "signal" fonksiyon implementasyonu kullanılmıştır. #include #include #include #include void (*my_signal(int sig, void (*handler)(int)))(int); void sigalarm_handler(int sig_no); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 1 2 3 4 ALARM 5 6 7 8 9 */ if(my_signal(SIGALRM, sigalarm_handler) == SIG_ERR) exit_sys("my_signal"); alarm(5); for(int i = 0; i < 10; ++i){ printf("%d\n", i); sleep(1); } } void (*my_signal(int sig, void (*handler)(int)))(int) { struct sigaction sa, sa_old; sa.sa_handler = sigalarm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if(sigaction(SIGALRM, &sa, &sa_old) == -1) return SIG_ERR; return sa_old.sa_handler; } void sigalarm_handler(int sig_no) { printf("ALARM\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "kill" Fonksiyonu: Şimdi de bir prosese sinyal gönderilme mekanizmasını inceleyelim. Bunun için POSIX sistemlerinde "kill" isminde bir fonksiyon bulundurulmaktadır. Fonksiyonun prototipi şu şekildedir: #include int kill(pid_t pid, int sig); Fonksiyonun birinci parametresi hedef prosesin ID değerini, ikinci parametresi ise gönderilecek sinyalin numarasını almaktadır. Başarı durumunda "0", hata durumunda "-1" değerine geri dönmeketedir. Fonksiyonun birinci parametresinin şöyle detayları vardır: -> Eğer bu parametre sıfırdan büyük ise sinyal yalnızca belirtilen ID değerine sahip prosese gönderilmektedir. -> Eğer bu parametreye sıfır değerini geçersek, sinyal, sinyali gönderen prosesin grup ID değeri ile aynı proses grup ID değerine sahip olan bütün proseslere gönderilecektir. Tabii burada o proses grubuna sinyal gönderme yetkisinin de olması gerek. Yani biz kendi proses grubumuzdaki bütün proseslere bu biçimde sinyal gönderebiliriz. -> Eğer bu parametre "-1" geçilirse, sinyal, sinyal gönderme hakkı olan bütün proseslere gönderilecektir. -> Eğer bu parametreye sıfırdan küçük bir değer girilirse, girilen değerin mutlak değeri bir proses grup ID olarak kabul edilir ve bu ID değerine sahip olanların hepsine sinyal gönderilir. Tabii burada o proses grubuna sinyal gönderme yetkisinin de olması gerekmektedir. Burada belirtilen proses grup kavramı ilerleyen paragraflarda ele alınacaktır. Genellikle bu fonksiyon ile bir prosesin sonlandırılması için sinyaller gönderilir ancak başka amaçlarla da sinyallerin gönderilmesi söz konusu olabilmektedir. Burada başka bir prosese sinyal gönderebilmek için prosesimizin ya "appropriate priviledged" olması ya da gerçek kullancı veya etkin kullanıcı ID değerimizin sinyali alacak olan prosesin gerçek ya da "Saklanmış Kullanıcı ID (saved-set user id)" değerine eşit olması gerekmektedir. Buradaki Saklanmış Kullanıcı ID değerine ilerleyen paragraflarda da ele alınacaktır. Özetle; -> Bizler ancak kendi proseslerimize sinyal gönderebiliriz. Herhangi bir prosese sinyal gönderebilmek için etkin kullanıcı id değerimizin "0" olması Şimdi de bu konuyla ilgili örneklere bakalım: * Örnek 1, Aşağıdaki örnekte ilk olarak "slave" programı çalıştırıldı. Daha sonra başka bir terminalden "ps -u" komutu çalıştırılarak bu "slave" prosesinin ID değeri öğrenildi. Devamında yine aynı terminal üzerinden "master" programı çalıştırıldı ve "slave" programının ID'si ile gönderilmek istenen sinyal komut satırı olarak "master" programına gönderilmiştir. /* master.c */ #include #include #include #include void exit_sys(const char* msg); typedef struct tagSIGNAL_INFO{ const char* name; int sig; } SIGNAL_INFO; SIGNAL_INFO g_signal_info[] = { {"SIGINT", SIGINT}, {"SIGTERM", SIGTERM}, {"SIGKILL", SIGKILL}, {NULL, 0} }; int main(int argc, char** argv) { /* # OUTPUT # $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ ./master SIGINT 13269 $ kill SIGTEMP 13269 */ if(argc != 3){ fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } pid_t pid = (pid_t)atol(argv[2]); for(int i = 0; g_signal_info[i].name != NULL; ++i) if(!strcmp(argv[1], g_signal_info[i].name)) if(kill(pid, g_signal_info[i].sig) == -1) exit_sys("kill"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* slave.c */ #include #include #include #include void sigint_handler(int sno); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # 0 1 2 3 4 //... 36 37 SIGINT handler is running!.. 38 39 40 41 SIGINT handler is running!.. 42 SIGINT handler is running!.. 43 SIGINT handler is running!.. 44 SIGINT handler is running!.. 45 SIGINT handler is running!.. 46 SIGINT handler is running!.. 47 48 //... 55 Terminated */ struct sigaction sa, sa_old; sa.sa_handler = sigint_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGINT, &sa, &sa_old) == -1) exit_sys("sigaction"); for(int i = 0; i < 60; ++i){ printf("%d\n", i); sleep(1); } return 0; } void sigint_handler(int sno) { printf("SIGINT handler is running!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan sinyal numaralarından o sinyale ilişkin yazı elde edebileceğimiz bir fonksiyon da POSIX standartlarında bulundurulmuştur. Bu fonksiyon "strsignal" isimli fonksiyondur. Fonksiyonun prototipi aşağıdaki gibidir: #include char *strsignal(int signum); Ayrıca "glibc" kütüphanesinde de POSIX standartlarında bulunmayan iki fonksiyon daha bulunmaktadır. Bunlar "sigdescr_np" ve "sigabbrev_np" isimli fonksiyonlardır. Fonksiyonların prototipleri de aşağıdaki gibidir: #include const char *sigdescr_np(int sig); const char *sigabbrev_np(int sig); Fakat bu iki fonksiyonu kullanmadan evvel kaynak dosyasının yukarısında "_GNU_SOURCE" sembolik sabitini tanımlamalıyız. Bu üç fonksiyonun kullanımına ilişkin örnek aşağıdaki gibidir: * Örnek 1, #define _GNU_SOURCE #include #include #include int main() { /* # OUTPUT # Terminated TERM Terminated */ printf("%s\n", strsignal(SIGTERM)); printf("%s\n", sigabbrev_np(SIGTERM)); printf("%s\n", sigdescr_np(SIGTERM)); } Son olarak "kill" fonksiyonu ile bir prosese "0" numaralı sinyali göndererek o prosesin hala bulunup bulunmadığını, yani sonlanmamış olduğunu test edebiliriz. Aslında "0" numaralı bir sinyal mevcut değildir fakat bu değer bu amaçla kullanılmaktadır. Aslında "0" numaralı sinyal prosese hiç gönderilmemektedir. "kill" fonksiyonu ile "0" numaralı sinyali prosese gönderdiğimizde fonksiyon başarılı oluyorsa, o prosesin sistemde bulunduğu anlarız. Fakat fonksiyon bir şekilde başarısız oluyorsa ve "errno" değişkeninin değeri de "ESRCH" değerini alıyorsa, ilgili prosesin sistemde bulunmadığını anlarız. Eğer "errno" değişkeni değer olarak "EPERM" değerini almışsa, ilgili prosesin sistemde bulunduğunu fakat uygun önceliğe sahip olmadığını anlarız. Özetle; if(kill(pid, 0) == -1 && errno == ESRCH){ /* Proses Sonlanmış */ } else { /* Proses Sonlanmamış */ } >> "kill" kabuk komutu: Pekala bir prosese komut satırından da sinyal gönderebiliriz. Bunun için "kill" isminde kabuk komutu bulundurulmaktadır ki zaten bu fonksiyon da "kill" fonksiyonu kullanılarak, yukarıda bizim yaptığımıza benzer biçimde, yazılmıştır. Bu komut ile bir prosese sinyal gönderilirken o sinyalin isminin başındaki "SIG" ismi belirtilmemelidir. Örneğin, kill -USR1 12767 komutunu çalıştırdığımız zaman ID numarası 12767 olan prosese "SIGUSR1" sinyali gönderilecektir. Pek tabii buradaki sinyal ismi yerine o sinyalin numarasını da belirtebiliriz fakat sinyal numaralarının taşınabilir olmadığını, sistemden sisteme hangi sinyale ilişkin olduğunu da UNUTMAYINIZ. /*================================================================================================================================*/ (75_26_08_2023) > UNIX/Linux Sistemlerinde Sinyal İşlemleri (devam): >> "kill" komutu (devam): Bu "kill" komutu sinyal ismi ya da numarası kullanmadan belirtilmeden kullanılırsa, varsayılan durumda, "SIGTERM" sinyalini göndermektedir. Örneğin, kill 12801 komutunu çalıştırdığımız vakit "12801" ID numaralı prosese "SIGTERM" sinyali gönderilecektir. Bu sinyal bir prosesi sonlandırmak için kullanılmaktadır. Bu sinyal türü "ignore" edilebilir, proses tarafından bloke edilebilir ya da bu sinyal için bir "handler" fonksiyonu "set" edilebilir. Bir prosesi sonlandırmak için kullanılan ikinci sinyal ise "SIGKILL" sinyalidir. Bu sinyalin "SIGTERM" sinyalinden farkı ise onun gibi "ignore" ve bloke EDİLEMEZ ve bu sinyal için "handler" fonksiyon "set" EDİLEMEZ oluşudur. Eğer "sigaction" ile "SIGKILL" sinyali için bir "handler" tanımlanmaya çalışılırsa fonksiyon başarısız olur ve "errno" değeri "EINVAL" değerini alır. Bu durumda bir prosesi garantili sonlandırmak için "SIGKILL" sinyali gönderilmelidir. Örneğin, kill -KILL 12801 komutu "12801" ID numaralı prosesi sonlandıracaktır. >> "raise" fonksiyonu: Bir prosesin kendisine sinyal göndermesi için kullanılan bir fonksiyondur. "kill" fonksiyonuna nazaran senkron bir fonksiyondur. Fonksiyonun prototipi şu şekildedir: #include int raise(int sig); Fonksiyon argüman olarak gönderilecek sinyalin numarasını parametre olarak alır. Başarı durumunda "0", hata durumunda ise sıfır dışı bir değere geri döner. Bu fonksiyon aynı zamanda Standart C fonksiyonudur. Fakat C Standartlarında sinyal olgusundan bahsedilmiş ve hiç bir ayrıntısına girilmemiştir. * Örnek 1, #include #include #include #include void signal_handler(int signo); void exit_sys(const char* msg); int main() { /* # OUTPUT # 0 1 2 3 4 5 A signal occured!.. 6 ... */ struct sigaction sa, sa_old; sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, &sa_old) == -1) exit_sys("sigaction"); for(int i = 0; i < 60; ++i){ printf("%d\n", i); if(i == 5) raise(SIGUSR1); sleep(1); } return 0; } void signal_handler(int signo) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan POSIX standartlarına göre, çok "thread" li bir ortamda bu fonksiyon hangi "thread" tarafından çağrılmışsa, senkronluğu sağlamak adına, ilgili sinyal "handler" fonksiyonu o "thread" tarafından çalıştırılır. >> Sinyallerin prosesler karşısında bloke edilmeleri: Prosesler bazı sinyalleri kendilerine karşı bloke etmektedir. Böylesi bir durumda o sinyal oluşursa, işletim sistemi bu sinyali o prosese teslim etmez ve askıda bekletir. Eğer proses ilgili sinyale karşı olan blokesini kaldırırsa, işletim sistemi de ilgili sinyali prosese teslim edilir. Ancak sinyaller askıya alındıklarında biriktirilmezler. Yani bir proses bir sinyale kendini bloke etmişse ve bu sırada o sinyal türünden birden fazla sinyal gelmesi halinde, blokenin kaldırılmasının ardından sadece bir defa teslim işlemi gerçekleştirilir. Peki bizler bir "t" anında prosesimizin kendini hangi sinyallere karşı bloke ettiğini nasıl anlarız? UNIX türevi işletim sistemleri her bir proses için bir "signal mask" kümesi tutmaktadır. Bu küme prosesin o anda hangi proseslere karşı kendini bloke ettiğini belirtmektedir. "thread" konusu UNIX türevi işletim sistemlerine girmesiyle her bir "thread" için bir "signal mask" kümesi tutulur olmuştur. Bir sinyalin bu kümeye girmesi durumunda, ilgili proses nezdinde sinyalimiz bloke edilecektir. İşte bu "signal mask" kümesi üzerinde işlem yapabilmek için "sigprocmask" isimli POSIX fonksiyonu kullanılmaktadır. >>> "sigprocmask" fonksiyonu: #include int sigprocmask(int how, const sigset_t * set, sigset_t * oset); Fonksiyonun birinci parametresi "signal mask" kümesi üzerinde ne yapılacağını belirtmektedir. Bu parametre şunlardan birisi olabilir: -> "SIG_BLOCK" : Fonksiyonun ikinci parametresi ile belirtilen sinyalleri, halihazırdaki "signal mask" kümesi içerisine eklemek. -> "SIG_UNBLOCK" : Fonksiyonun ikinci parametresi ile belirtilen sinyalleri, halihazırdaki "signal mask" kümesi içerisinden çıkartmak. -> "SIG_SETMASK" : Fonksiyonun ikinci parametresi ile belirtilen sinyalleri, halihazırdaki "signal mask" kümesi haline getirmek. Fonksiyonun ikinci parametresi ise "sigaction" fonksiyonunda gördüğümüz "sigset_t" türündendir. Anımsanacağı üzere bu tür sinyalleri "bit" düzeyinde ifade etmek için kullanılmaktadır. Fonksiyonun üçüncü parametresi ise prosesin daha önceki "signal mask" kümesinin yerleştirileceği nesneyi belirtmektedir. Aslında ikinci ve üçüncü parametrelere "NULL" değeri de geçilebilir ki bu durumda bu parametreler fonksiyon tarafından kullanılmayacaktır. Fonksiyon başarı durumunda "0", hata durumunda ise "-1" değerine geri dönmektedir. Son olarak prosesin "signal mask" kümesinde "SIGKILL" ya da "SIGSTOP" sinyalleri bulunsa bile bu sinyalleri bloke edemiyoruz ve "sigprocmask" fonksiyonu başarısız OLMAMAKTADIR. * Örnek 1, Aşağıdaki örnekte "SIGTERM" sinyali beş saniyeliğine proses nezdinde bloke edilmiştir. Bu beş saniye içerisinde dışarıdan "SIGTERM" sinyali gelmesi halinde sinyal teslim edilmeyecek, blokenin kalması ile birlikte teslim edilecektir. #include #include #include #include void exit_sys(const char* msg); int main() { sigset_t sset, old_sset; sigemptyset(&sset); sigaddset(&sset, SIGTERM); // Blokesi istenen sinyaller bir küme haline getirildi. if(sigprocmask(SIG_BLOCK, &sset, &old_sset) == -1) // Daha sonra prosesin "signal mask" kümesine eklendi. exit_sys("sigprocmask"); // "SIGTERM" bu noktada blokelidir. printf("sleep for 15 seconds while SIGERM is being blocked\n"); sleep(5); // APPROACH - I if(sigprocmask(SIG_UNBLOCK, &sset, NULL) == -1) // Daha sonra prosesin "signal mask" kümesinden çıkartıldı. exit_sys("sigprocmask"); // APPROACH - II // if(sigprocmask(SIG_SETMASK, &old_sset, NULL) == -1) // Daha sonra prosesin "signal mask" kümesinden çıkartıldı. // exit_sys("sigprocmask"); // "SIGTERM" bu noktada blokesizdir. printf("program continues running!..\n"); sleep(5); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan o an askıda olan sinyallerin kümesini de elde edebiliriz. Bunun için "sigpending" isimli POSIX fonksiyonu kullanılmaktadır. >>> "sigpending" fonksiyonu: #include int sigpending(sigset_t *set); Fonksiyon askıdaki sinyalleri, argüman olarak geçtiğimiz "sigset_t" nesnesi içerisine yerleştirmektedir. Bu aşamada programcı belli bir sinyalin bu kümede olup olmadığını, yukarıda detayları işlenen "sigismember" fonksiyonu ile, öğrenebilir. Fakat "sigpending" fonksiyonu pek kullanılan bir fonksiyon da değildir. >> "pause" fonksiyonu: Belli bir sinyal oluşana kadar ilgili prosesi blokede bekletmek içindir. Fonksiyonun prototipi şu şekildedir: #include int pause(void); Fonksiyon sadece "-1" değerine geri dönmektedir ve "errno" değişkeni yalnızca "EINTR" değerini almaktadır. Dolayısıyla fonksiyonun başarısını kontrol etmeye lüzum yoktur. Burada şu noktalara dikkat etmeliyiz: -> Sinyal oluştuğunda herhangi bir "handler" fonksiyon "set" edilmemişse, "pause" fonksiyonu zaten geri dönmeyecek ve proses sonlandırılacaktır. -> Eğer böylesi bir fonksiyon "set" edilmişse, önce o fonksiyon çalıştırılmakta daha sonra "pause" geri dönmektedir. Bu fonksiyonu aşağıdaki biçimde şöyle kullanabiliriz: for(;;) pause(); Burada arzu edilen sinyal geldikçe iş yapan, aksi durumda uykuda bekleyen bir akış söz konusudur. Tabii böylesi bir programı sonlandırmanın bir yolu ise sinyal fonksiyonu "set" edilmemiş o sinyali bu prosese göndermek olabilir. * Örnek 1, Aşağıdaki programa "SIGUSR1" sinyali geldikçe programın akışı "for" döngüsünden çıkıp "sig_handler" fonksiyonuna girecek, daha sonra tekrardan "for" döngüsüne geri dönecektir. #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); int main() { struct sigaction sa, sa_old; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, &sa_old) == -1) exit_sys("sigaction"); for(;;) pause(); return 0; } void sig_handler(int sno) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "threads" ve "signals" : Sinyaller ilk UNIX sistemlerinden beri var olan kavramlarken, "thread" kavramı 90'lı yıllardan itibaren var olmaya başlamıştır. Dolayısıyla "thread" kavramının olmadığı dönemlerde proseslerin tek bir akışı olduğundan, sinyaller o akışa gönderiliyordu. 90'lı yıllarda "thread" kavramının yaygınlaşması ile sinyal konusunda da bazı revizeler yapılmıştır. Örneğin, hangi "thread" in o sinyal fonksiyonunu çalıştıracağı POSIX standartlarınca işletim sistemini yazanların isteğine bırakılmıştır. Bununla birlikte bir "thread", kendi prosesi içerisindeki başka "thread" e sinyal gönderebilmektedir. Anımsanacağınız üzere "thread" lerin ID değerleri kendi proseslerinde anlamlıdır. Yani başka bir prosesin "thread" ine direkt olarak sinyal gönderememekteyiz. Tabii bir "thread" e sinyal göndermek demek, ilgili sinyal fonksiyonunun o "thread" tarafından çalıştırılmasını sağlamak demektir. İşte bir "thread" ile kendi prosesimizdeki başka bir "thread" e sinyal gönderebilmek için de "pthread_kill" POSIX fonksiyonu bulundurulmuştur. >>> "pthread_kill" fonksiyonu: Fonksiyonun prototipi aşağıdaki gibidir. #include int pthread_kill(pthread_t thread, int sig); Fonksiyonun birinci parametresi hedef "thread" in ID değerini, ikinci parametresi ise gönderilecek sinyalin numarasını almaktadır. Başarı durumunda "0", hata durumunda hata kodunun kendisi ile geri dönmektedir. Tabii ilgili sinyal için bir sinyal fonksiyonu "set" edilmemişse, yalnızca ilgili "thread" değil, bütün proses sonlandırılacaktır. * Örnek 1, Aşağıdaki örnekte "main-thread" diğer "thread" e "SIGUSR1" sinyalini göndermiştir. #include #include #include #include #include #include void* thread_proc(void* param); void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # Main is running: 0 Thread is running: 0 Main is running: 1 Thread is running: 1 ... Main is running: 5 A signal occured!.. Thread is running: 5 Main is running: 6 Thread is running: 6 ... Main is running: 9 Thread is running: 9 */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); pthread_t tid; int result; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); for(int i = 0; i < 10; ++i){ printf("Main is running: %d\n", i); if(i == 5 && (result = pthread_kill(tid, SIGUSR1)) != 0) exit_sys_errno("pthread_kill", result); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 10; ++i){ printf("Thread is running: %d\n", i); sleep(1); } return NULL; } void sig_handler(int sno) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "main-thread" diğer "thread" e "SIGUSR2" sinyalini göndermiştir. Fakat o sinyal için "set" edilmiş bir fonksiyon olmadığı için proses sonlandırılmıştır. #include #include #include #include #include #include void* thread_proc(void* param); void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # Main is running: 0 Thread is running: 0 Main is running: 1 Thread is running: 1 Main is running: 2 Thread is running: 2 Main is running: 3 Thread is running: 3 Main is running: 4 Thread is running: 4 Main is running: 5 */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); pthread_t tid; int result; if((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); for(int i = 0; i < 10; ++i){ printf("Main is running: %d\n", i); if(i == 5 && (result = pthread_kill(tid, SIGUSR2)) != 0) exit_sys_errno("pthread_kill", result); sleep(1); } if((result = pthread_join(tid, NULL)) != 0) exit_sys_errno("pthread_join", result); return 0; } void* thread_proc(void* param) { for(int i = 0; i < 10; ++i){ printf("Thread is running: %d\n", i); sleep(1); } return NULL; } void sig_handler(int sno) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Pekala bizler belli "thread" leri belli sinyallere karşı blokeli hale getirebiliriz. Çünkü UNIX/Linux sistemlerinde her bir "thread" için de bir "signal mask" kümesi bulunmaktadır. Bunun için de bizler "pthread_sigmask" fonksiyonunu kullanmalıyız. >>> "pthread_sigmask" fonksiyonu: Fonksiyonun prototipi aşağıdaki gibidir: #include int pthread_sigmask(int how, const sigset_t * set, sigset_t * oset); Fonksiyonun parametrik yapısı "sigprocmask" ile birebir aynıdır. Ancak geri dönüş değeri başarı durumunda "0", hata durumunda ise hata kodunun kendisi biçimindedir. Hangi "thread" bu fonksiyonu çağırmışsa, onun "signal mask" kümesi etkilenecektir. Bu fonksiyonun tipik kullanım biçimi bir sinyalin spesifik bir "thread" tarafından ele alınması şeklindedir. Bunun için o sinyalin ilgili "thread" hariç aynı prosesteki bütün sinyallere karşı bloke edilmesi gerekmektedir. Böylelikle bloke edilmeyen o "thread" ilgili sinyali işleyecektir. Pekala bir prosesin bütün "thread" leri bir sinyale karşı bloke edilmesi demek, o prosesin ilgili sinyale karşı bloke edilmesi demektir. Pek tabii Linux sistemlerinde "kill" fonksiyonunu kullanarak, "gettid" ile "ID" değerini elde ettiğimiz "thread" leri sonlandırabiliriz. Çünkü sinyal adeta o "thread" e gönderilmiş gibi bir etki oluşacaktır. Çünkü Linux sistemlerinde "thread" ler de birer proses olarak ele alınmaktadır. Yani Linux sistemlerinde başka proseslerin "thread" lerine bu yöntemle sinyal gönderebiliriz. Fakat POSIX standartlarınca bunu gerçekleştirmek mümkün değildir. Bu durum Linux-kernel tasarımından dolayıdır. > Hatırlatıcı Notlar: >> İşletim sistemleri dünyasında bir fonksiyonun senkron işlem yapması demek; o fonksiyon geri döndüğünde o işin bitmiş olmasının garanti edilmesi demektir. Örneğin "read", "write" gibi fonksiyonlar senkron işlem yapmaktadır. Çünkü "read" fonksiyonu geri döndüğünde okuma işi bitmiş durumdadır. Öte yandan bir fonksiyonun asenkron işlem yapması demek; o fonksiyon geri döndüğünde o işin bitmek zorunda olmadığı anlamına gelmektedir. Örneğin, "kill" fonksiyonu geri döndüğünde sinyalin ilgili proses tarafından işlenmiş olduğunun bir garantisi yoktur. /*================================================================================================================================*/ (76_27_08_2023) > UNIX/Linux Sistemlerinde Sinyal İşlemleri (devam): >> "threads" ve "signals" (devam): Seyrek olarak programcılar, kritik bir takım işlemler yaparken, sinyalleri "pthread_sigmask" ile bloke edip, daha sonra blokeyi kaldırıp, "pause" ile de ilgili sinyalin gelmesini beklemek isteyebilir. Böylelikle ilgili sinyal gelmediği sürece akış ilerlemeyecektir. Örneğin, pthread_sigmask(); // Kritik Kod // ... pthread_sigmask(); <------- TEHLİKELİ ALAN pause(); // Bu kısma sinyal geldiğinde geçilmek istenmektedir. Burada gerçekleştirilmek istenen şey, şekilden de görüleceği üzere, sinyallerin blokesini kaldırdıktan hemen sonra "pause" ile ilgili sinyalin gelmesini beklemek ve sinyal geldikten sonra programın akışının ilgili kısma geçmesini sağlamaktır. Fakat programın akışı tam da TEHLİKELİ ALAN noktasındayken bir sinyal gelmesi durumunda bu sinyal teslim edilebilir. Dolayısıyla prosesimiz ya sonlanacak ya da ilgili sinyal fonksiyonu işletilecektir. Daha sonrasında da akış "pause" fonksiyonuna girecektir. Beklenen sinyal halihazırda geldiği için tekrar gelmeyeceğinden, programın akışı bu "pause" fonksiyonundan çıkamayacaktır. Dolayısıyla bizler sinyallerin blokesini kaldırma işlemi ile "pause" işlemini atomik olarak gerçekleştirmemiz gerekmektedir. İşte bunun için "sissuspend" fonksiyonunu bulundurulmuştur. >>> "sigsuspend" fonksiyonu: Fonksiyonun prototipi aşağıdaki gibidir: #include int sigsuspend(const sigset_t *sigmask); Fonksiyon parametre olarak yeni "signal mask" kümesini alır, bu kümeyi bu fonksiyonu çağıran "thread" in "signal mask" kümesi yapar ve atomik bir biçimde de "pause" işlemini gerçekleştirir. Böylece yukarıda bahsedilen blokenin açılıp "pause" fonksiyonunda bekleme atomik hale getirilmiş olur. Fonksiyonun geri dönüş değeriyse şunlardan birisidir: -> Eğer proses ilgili sinyal için herhangi bir fonksiyon "set" etmemişse, proses sonlanacağı için, fonksiyon hiç geri dönmeyebilir. -> Eğer bir sinyal fonksiyonu "set" edilmişse, fonksiyon "-1" ile geri döner ve "errno" değişkeni "EINTR" değerini alır. Tabii burada ilgili sinyalin gelmesi ile fonksiyondan çıkılmasıyla birlikte o "thread" in evvelki "signal mask" kümesi yeniden "set" edilir. POSIX standartları bu fonksiyonun sadece ilgili "thread" e ait olan "signal mask" kümesini etkilemektedir. "thread" kavramı olmadan önce ise prosesin "signal mask" kümesini etkilemektedir. Dolayısıyla "thread" lerin kullanılmadığı bir ortamda bu fonksiyonu kullanmamız halinde prosesin "signal mask" kümesi etkilenmekte, "thread" lerle birlikte bu fonksiyonu çağıran "thread" in "signal mask" kümesi etkilenmektedir. O halde "sigsuspend" fonksiyonunun işleyişini şu şekilde de temsili olarak gösterebiliriz: pthread_sigmask(SIG_SETMASK, &sset, &oldset); pause(); pthread_sigmask(SIG_SETMASK, &oldset, &NULL); Son olarak "sigsuspend" fonksiyonunun geri dönüş değerinin kontrol edilmesine gerek yoktur. * Örnek 1, Aşağıdaki program çalışırken bir başka "terminal" üzerinden "kill" komutu ile bu prosese "SIGUSR1" sinyali göndermelisiniz. #include #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { struct sigaction sa; sigset_t sset, old_sset; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); sigfillset(&sset); // Burada bütün sinyaller bloke edilmiştir. if(sigprocmask(SIG_BLOCK, &sset, &old_sset) == -1) exit_sys("sigprocmask"); for(int i = 0; i < 10; ++i){ /* (I) * Programın akışı buradayken bir sinyal geldiğinde ilgili sinyal * askıda bekletilecektir. Böylelikle önem kritik işlemler sırasında * rahatsız edilmemiş olacağız. */ printf("%d\n", i); sleep(1); } /* (II) * Programın akışı bu fonksiyona girmesiyle birlikte ilgili sinyal gelmemişse * programın akışı "suspend" edilecektir. Ta ki beklenen sinyal gelene dek. Eğer * bu bekleme sırasında ilgili sinyal gelirse veya halihazırda askıda bir sinyal * varsa ona ilişkin "handler" fonksiyon çalıştırılacak ve programın akışı da * "sigsuspend" fonksiyonunu tamamlayacaktır. */ sigsuspend(&old_sset); /* (III) * Dolayısıyla programın akışının buraya gelebilmesi için ya askıda bir sinyal olması * ki böylelikle ilgili "handler" fonksiyonunun çağrılacak ya da "suspend" sırasında * bir sinyal gelmesi ve ona ait "handler" fonksiyonunun çağrılması gerekmektedir. */ printf("Ok\n"); return 0; } void sig_handler(int sno) { printf("A signal occured!..\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Aslında "sigsuspend" fonksiyonunun kullanımının nerelerde gerektiği biraz zor anlaşılmaktadır. Çünkü bu fonksiyona ihtiyaç aslında oldukça seyrektir. Genellikle şu tipik senaryoda bu fonksiyona ihtiyaç duymaktayız: -> Programcı başka bir prosesten bir sinyal beklemekte ve ancak o sinyal geldiğinde koduna devam etmek istemektedir. Fakat bu sinyali beklemeden evvel sinyalleri bloke ederek bazı önem-kritik işlemler de yapmak isteyebilir. (Dolayısıyla bu süre zarfında beklenen sinyal gelirse, sinyal askıya alınacaktır.) Programcı daha sonra sinyalleri açarak "pause" işlemi ile diğer prosesten gelecek sinyalini bekleyecektir. Eğer "sigsuspend" fonksiyonu olmasaydı, sinyalleri açtığımız nokta ile "pause" yapmak istediğimiz nokta arasında beklenen sinyalin gelmesi durumunda artık o sinyale ilişkin "handler" fonksiyonu işletilecektir. Benzer şey sinyalleri açmadan evvel beklenen sinyalin gelmesi durumunda da geçerlidir. Programın akışı ilgili "handler" fonksiyonundan çıkıp "pause" fonksiyonuna gireceğinden, sonsuz bekleme oluşacaktır. Çünkü beklenen sinyal zaten gelmiştir. Buradan da görüleceği üzere bu fonksiyonu: -> Ya önem-kritik işlemler sırasında rahatsız edilmemek için kullanıyor. -> Ya bir işi yapmak için bir sinyalin gelmesini bekliyorsak kullanıyoruz. Pek tabii bir prosesin diğer prosesin işini yapmasını beklemesi, prosesler arası haberleşme yöntemleri ile de mümkündür. Ancak bu tür durumlar için sinyallerin kullanılması daha pratiktir. Bu yüzdendir ki prosesler arasında haberleşme yöntemi olarak sinyalleri de kullanabiliriz. Son olarak şunu da belirtmekte fayda vardır; sinyal fonksiyonu içerisinde "errno" değişkeninin değerini değiştirme potansiyeline sahip bir fonksiyon çağrısı varsa, bu durum sorunlara yol açabilir. Anımsanacağı üzere bazı POSIX fonksiyonları başarı durumunda da "errno" değişkenini değiştirebilmektedir. Buradaki sorun ise şu şekilde açıklanabilir: void signal_handler(int sig_no) { if(some_posix_func() == -1){ <--- (I) : Tam bu noktada bir sinyal geldiğini varsayalım. perror("some_posix_func"); exit(EXIT_FAILURE); } } Buradaki "(I)" noktasında bir sinyal gelmesi durumunda "errno" değişkeninin yeni değeri "some_posix_func" fonksiyonuna göre belirlenecektir. Fakat bizim beklentimiz, "signal_handler" fonksiyonu tarafından bunun belirlenmesidir. Dolayısıyla bizler yanlış değeri ekrana yazdırmış olacağız. Bunun için "errno" değerini işin başında bir değerde saklayıp, işin sonunda bu değer ile yeniden "set" işlemi yapmamız gerekmektedir. Şöyleki: void signal_handler(int sig_no) { int temp_errno = errno; // Bir takım işlemler... errno = temp_errno; } Aşağıda bu konuyla ilgili bir örnek verilmiştir: * Örnek 1, Aşağıdaki örnekte ise hata kodu yanlış bastırılmıştır. Çünkü "raise" fonksiyonunun çağrılma sebebi, ilgili dosyanın bulunamamasıdır. Dolayısıyla ekrana bu hata kodu yazdırılmalıdır. Fakat "errno" değişkeni ilgili sinyal fonksiyonunda yeniden "set" edildiği için hata kod mesajı da değişecektir. #include #include #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # A signal occured!.. open: Operation not permitted */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); int fd; if((fd = open("file_not_found", O_RDONLY)) == -1){ raise(SIGUSR1); exit_sys("open"); } printf("Ok\n"); return 0; } void sig_handler(int sno) { printf("A signal occured!..\n"); kill(1, SIGKILL); /* kill will fail */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte olması gereken hata kodu yazdırılmıştır. Çünkü ilgili dosya bulunamadığı için "raise" fonksiyonu çağrılacaktır. Bu durumda da hata kodunun ilgili dosyanın bulunamadığına ilişkin olmalıdır. #include #include #include #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); void exit_sys_errno(const char* msg, int eno); int main() { /* # OUTPUT # A signal occured!.. open: No such file or directory */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); int fd; if((fd = open("file_not_found", O_RDONLY)) == -1){ raise(SIGUSR1); exit_sys("open"); } printf("Ok\n"); return 0; } void sig_handler(int sno) { int temp_errno = errno; printf("A signal occured!..\n"); kill(1, SIGKILL); /* kill will fail */ errno = temp_errno; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_errno(const char* msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >> Sinyal Güvenli Fonksiyon Kavramı ("Asynchronous Signal Safe Functions"): Bizler bir fonksiyonun içerisinde olduğumuzu, bu noktada bir sinyal geldiğini ve bu durumda da ilgili sinyale ilişkin "handler" fonksiyonun çalıştığını düşünelim. İş bu "handler" fonksiyon, sinyal gelmeden içerisinde bulunduğumuz fonksiyonu çağırsa ne olacaktır? Bu duruma iç içe çağırma durumu denmekte ve "thread-safe" durumuna benzemektedir. POSIX standartları böylesi iç içe çağrılabilecek fonksiyonlara ise "async-signal-safe" fonksiyonlar ismini vermektedir. Bütün asenkron sinyal güvenli POSIX fonksiyonların listesi "System Interfaces/General Information/Sinal Concepts" başlığı altında listelenmektedir. Bu listedeki fonksiyonları hem dışarıda hem de "handle" fonksiyonlarda rahatlıkla kullanılabilir. Burada belirtilmeyen fonksiyonları ise ya dışarıda ya da "handle" fonksiyonu içerisinde kullanmalıyız. Öte yandan "async-signal-safe" fonksiyonlar, aynı zamanda "thread-safe" fonksiyonlardır. Fakat her "thread-safe" fonksiyon aynı zamanda "async-signal-safe" fonksiyon DEĞİLDİR. Çünkü "async-signal-safe" fonksiyonlar, "thread-safe" fonksiyonların daha katı biçimidir. ÖZETLE; bir fonksiyonun kesilerek aynı "thread" tarafından yeniden çağrılmasına "async-signal-safe", farklı "thread" ler tarafından olması durumuna ise "thread-safe" denir. (see https://en.wikipedia.org/wiki/Reentrancy_(computing) for more info.) * Örnek 1.0, Ne "thread-safe" ne de "async-signal-safe": int tmp; void swap(int* x, int* y) { tmp = *x; *x = *y; /* Hardware interrupt might invoke isr() here. */ *y = tmp; } void isr() { int x = 1, y = 2; swap(&x, &y); } int main() { //... } * Örnek 1.1, "thread-safe" ama "async-signal-safe" DEĞİL: _Thread_local int tmp; void swap(int* x, int* y) { tmp = *x; *x = *y; /* Hardware interrupt might invoke isr() here. */ *y = tmp; } void isr() { int x = 1, y = 2; swap(&x, &y); } * Örnek 1.2, hem "thread-safe" hem "async-signal-safe": void swap(int* x, int* y) { int tmp; tmp = *x; *x = *y; *y = tmp; /* Hardware interrupt might invoke isr() here. */ } void isr() { int x = 1, y = 2; swap(&x, &y); } Diğer yandan "sig_atomic_t" isimli bir tür daha vardır, C99 ile de C diline eklenmiştir. Bu türden global bir nesne tanımlandığında, bu nesne üzerindeki atama işlemleri atomik bir biçimde gerçekleştirilecektir. Ancak "++", "--" gibi işlemlerin atomik olacağının bir garantisi YOKTUR. Bu tür aynı zamanda "volatile" özelliğini de bazı derleyiciler nezdinde kapsamaktadır. >> Proseslerin Durdurulup Yeniden Çalıştırılması: Burada devreye "SIGSTOP" ve "SIGCONT" sinyalleri devreye girmektedir ki bu sinyaller ilgili prosesi sırasıyla "suspend" eder ve "suspend" halini kaldırır. "SIGSTOP" ile "suspend" edilen bir proses, "SIGCONT" sinyali gelen kadar "suspend" durumunu korur. Bu sinyallerden "SIGSTOP" sinyali, tıpkı "SIGKILL" sinyalinde olduğu gibi, "ignore" ve bloke EDİLEMEZ. Ayrıca "SIGSTOP" sinyali için bir fonksiyon "set" EDİLEMEMEKTEDİR. * Örnek 1, Aşağıdaki programı bir terminalden çalıştırıp, ikinci bir terminal üzerinden de "SIGSTOP" ve "SIGCONT" sinyalleri göndererek durumu gözlemleyebiliriz. #include #include #include void exit_sys(const char* msg); int main() { for(int i = 0; i < 20; ++i){ printf("%d\n", i); sleep(1); } } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } "suspend" edilen prosesler, "ps -u" komutu çalıştırıldığında karşımıza çıkan tablonun "STAT" sütununda, "T" harfi ile görülürler. Diğer yandan klavyeden "CTRL+Z" tuşuna basıldığında, terminal aygıt sürücüsü devreye girer ve oturumun ön plan proses grubuna "SIGSTOP" sinyali gönderir. Yani klavyeden bu tuş kombinasyonu yaparak, o an çalışmakta olan programa, "SIGSTOP" sinyali göndertebilmekteyiz. Bu biçimde, kabuk üzerinden durdurulan prosesler, "fg" kabuk komutu ile kaldığı yerden çalışmaya devam ettirilebilmektedir. Bu komut ise aslında ilgili prosese "SIGCONT" sinyalini göndermektedir. Yukarıdaki örnek üzerinde bu kabukları şu şekilde çalıştırabiliriz: $ ./mample 1 2 3 ... <--- Tam bu noktada "CTRL+Z" tuşuna basıldı. [2]+ Durdu ./mample $fg 3 15 16 17 ... Pekiyi bizim prosesimiz bir "child" proses oluşturmuşsa durum nasıl olacaktır? Bu durumda ilgili proses grubundakilerin hepsine bu sinyal gönderilmektedir. (Çünkü "fork" yaptığımız zaman alt proses ve üst proses aynı proses grup içerisinde bulunur. Proses grupları, oturum kavramı gibi konular ileride ele alınacaktır.) * Örnek 1, Aşağıdaki programı çalıştırdıktan sonra "CTRL+Z" tuşuna bastığınız zaman hem alt hem de üst prosesin "suspend" edildiğini göreceksiniz. #include #include #include #include #define COUNT_DOWN 5 void exit_sys(const char* msg); int main() { pid_t pid; if((pid = fork()) == -1) exit_sys("fork"); if(pid != 0){ for(int i = COUNT_DOWN; i > 0; --i){ printf("Parent: %d\n", i); sleep(1); } if(waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else{ for(int i = 0; i < COUNT_DOWN; ++i){ printf("Child: %d\n", i); sleep(1); } _exit(0); } } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan "wait" fonksiyonları ile alt proses beklenirken, bu beklemeden ancak alt proses sonlandığında çıkabilmektedir. Alt prosesin sonlanması ise iki biçimde olabilmektedir. Bunlar "exit" ya da "_exit" fonksiyonlarının çağrılması ve bir sinyal dolayısıyla olmaktadır. Anımsanacağı üzere bizler "wait" fonksiyonlarının "status" parametrelerini "WIFEXITED" ve "WIFSIGNALED" makrolarına sokarak bu durumu anlayabiliyorduk. Zaten prosesin "exit code" bilgisinin oluşabilmesi için onun da normal bir biçimde sonlanmış olması gerekiyordu. Dieğr yandan "waitpid" fonksiyonunun üçüncü parametresine ise "WUNTRACED" ve/veya "WCONTINUED" bayrakları geçilirse bu durumda "waitpid" fonksiyonu alt proses "suspend" edildiğinde ve yeniden çalıştırıldığında da sonlandırılacaktır. Bu durumda da "status" parametresi "WIFSTOPPED" ve "WIFCONTINUED" makrolarına sokularak bu durum anlaşılabilmektedir. > Hatırlatıcı Notlar: >> Bir prosesi terminal yoluyla sonlandırmanın "CTRL+C" tuşu dışındaki diğer yolu ise "CTRL+\" (bazı sistemlerde "CTRL+Backspace") tuşunu kullanmaktır. Bu durumda terminal aygıt sürücüsü oturumun ön plan proses grubuna "SIGQUIT" isimli bir sinyal göndermektedir. Bu sinyal prosese gönderildiğinde, eğer proses bu sinyali ele almamışsa, proses sonlandırılı ve "code" dosyası oluşturulur. /*================================================================================================================================*/ (77_02_09_2023) > UNIX/Linux Sistemlerinde Sinyal İşlemleri (devam): Anımsanacağı üzere proses sonlandırma yöntemlerinden bir diğeri de "abort" fonksiyon çağrısı iledir. Bu çağrıyı, "exit" ile sonlandırma yapamayacak durumlarda yapmalıyız. "abort" çağrısı sonrasında "SIGABRT" sinyalinin oluşmasına neden olur. >> "SIGABRT" Sinyali: Bu sinyalin varsayılan davranışı ise, UNIX türevi sistemlerde, "core" dosyasının oluşmasıdır. Böylelikle "debugger" altında iş bu dosyayı inceleyebiliriz. "SIGABRT" sinyali için başka sinyal fonksiyonlar "set" edilmiş olsa bile, o fonksiyonun çalışması bittikten sonra, yine de proses sonlandırılır. Diğer yandan "SIGABRT" sinyali "block" ve "ignore" edilmişse bile, proses yine de sonlandırılır. Bu sinyal ile prosesin sonlandırılmasının engellenmesinin tek yolu, sinyal fonksiyonu içerisinde "long jump" işleminin gerçekleştirilmesidir. * Örnek 1, #include #include #include #include void sigabrt_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # 0 1 2 3 4 5 SIGABRT handler */ struct sigaction sa; sa.sa_handler = sigabrt_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGABRT, &sa, NULL) == -1) exit_sys("sigaction"); for (int i = 0; i < 60; ++i) { printf("%d\n", i); sleep(1); if (i == 5) abort(); } return 0; } void sigabrt_handler(int sig) { printf("SIGABRT handler\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Diğer yandan UNIX ve türevi sistemlerde işlemci tarafından oluşturulan içsel kesmelerden kaynaklı da proseslere sinyaller gönderilmektedir. Bunlardan en çok karşılaşılanı "SIGSEGV" isimli sinyaldir. >> "SIGSEGV" Sinyali: Bu sinyal, işletim sistemi tarafından programımıza tahsis edilmemiş bir bölgeye erişmeye çalıştığımız zaman, işletim sistemi tarafından prosese gönderilir ve varsayılan davranışı yine prosesi sonlandırmasıdır. Bu sinyal de "block" ve "ignore" EDİLEMEZLER. Bu sinyal oluştuğunda ve sinyal fonksiyonu da "set" edilmişse, sinyal fonksiyonu çalıştırılır. Ancak çalışması bittiğinde, proses yine sonlandırılır. Bu sinyal ile prosesin sonlandırılmasının engellenmesinin tek yolu, sinyal fonksiyonu içerisinde "long jump" işleminin gerçekleştirilmesidir. * Örnek 1, Aşağıdaki program Linux sistemlerinde çalıştırılmıştır. Bu sistemlerde eğer sinyal fonksiyonu içerisinde "exit" çağrısı yapılmamışsa, aynı sinyal yeniden oluşturulur, aynı sinyal fonksiyonu yeniden çağrılır... Fakat bazı UNIX türevi sistemlerde sinyal fonksiyonunun sonlanmasını takiben proses de sonlandırılmaktadır. #include #include #include #include void sigsegv_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # SIGSEGV handler SIGSEGV handler SIGSEGV handler SIGSEGV handler ... */ struct sigaction sa; sa.sa_handler = sigsegv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGSEGV, &sa, NULL) == -1) exit_sys("sigaction"); char* ptr = (char*)0x123456789; // Programımız için tahsis edilmemiş bir alana erişimden kaynaklı // sinyal gönderilmiştir. putchar(*ptr); return 0; } void sigsegv_handler(int sig) { printf("SIGSEGV handler\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bir diğer önemli sinyal de "SIGCHLD" isimli sinyaldir. Bu sinyal proses oluşturulan bölümde bahsetmiştik; >> "SIGCHLD" : UNIX ve türevi sistemlerde alt proses sonlanırken, üst prosese bu sinyali göndermektedir. Eskiden bu sinyalin ismi "SIGCLD" ismindeydi ve artık POSIX standartları "SIGCLD" sinyalini desteklememektedir. İşte "zombie process" oluşumunu engellemenin bir yolu da bu sinyale ilişkin bir fonksiyon tanımlayıp, bu fonksiyon çağrısı sırasında "wait" çağrısı yapmaktır. Böylelikle alt proses sonlandığında ilgili sinyal fonksiyonu da çağrılacağından, "zombie process" oluşumu engellenmiş olacaktır. Tabii şu noktaya da dikkat etmeliyiz; -> Bu sinyal geldiğinde, sinyal fonksiyonu çalıştırılırken, birden fazla alt proses de o sırada sonlanmış olabilir. İşte bunlardan gönderilen sinyaller biriktirilemeyeceği için, programın akışı sinyal fonksiyonundan çıktığında, askıda olan o sinyallerden sadece bir tanesi işlenir. Diğerleri ıskarta edilir. İşte bunun çözümü ise sinyal fonksiyonu içerisindeki "wait" çağrısını bir döngü içerisinde yapmaktır. "wait" çağrısının blokeye yol açmaması için de "WNOHANG" değerini kullanmamız gerekmektedir, aksi halde bloke oluşacaktır. Diğer yandan bu sinyalin varsayılan aksiyonu, sinyalin "ignore" edilmesidir. Yani bu sinyal için herhangi bir şey yapmamışsak, sinyal yine oluşacak ancak işletim sistemi tarafından sinyal "ignore" edilecektir. * Örnek 1, #include #include #include #include #include #include void sig_handler_func(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Parent Process Child Process SIGCHLD handler */ struct sigaction sa; sa.sa_handler = sig_handler_func; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { printf("Parent Process\n"); //... pause(); } else { printf("Child Process\n"); //... _Exit(100); } return 0; } void sig_handler_func(int sig) { printf("SIGCHLD handler\n"); int errno_temp = errno; // Eğer beklenecek hiç alt proses yoksa "-1" ile, // "WNOHANG" uygulanmış ancak henüz bir alt proses // sonlanmamışsa "0" değerine geri döner. Aksi halde // proses ID değeri ile geri döner. Bu döngüyle birlikte // sonlanan bütün alt prosesler beklenmiş olacaktır. while (waitpid(-1, NULL, WNOHANG) > 0) ; errno = errno_temp; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include #include void sig_handler_func(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Parent Process Child Process SIGCHLD handler :> Parent Process Child Process SIGCHLD handler :> Parent Process Child Process SIGCHLD handler :> */ struct sigaction sa; sa.sa_handler = sig_handler_func; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid; for (int i = 0; i < 3; ++i) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { printf("Parent Process\n"); //... getchar(); // :> } else { printf("Child Process\n"); //... _Exit(100); } } return 0; } void sig_handler_func(int sig) { printf("SIGCHLD handler\n"); int errno_temp = errno; while (waitpid(-1, NULL, WNOHANG) > 0) ; errno = errno_temp; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, #include #include #include #include #include #include void sig_handler_func(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Child Process Press ENTER to continue... Child Process Child Process SIGCHLD handler SIGCHLD handler */ struct sigaction sa; sa.sa_handler = sig_handler_func; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if(sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid; for (int i = 0; i < 3; ++i) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { printf("Child Process\n"); usleep(rand() % 100000); _exit(EXIT_SUCCESS); } } printf("Press ENTER to continue...\n"); getchar(); return 0; } void sig_handler_func(int sig) { printf("SIGCHLD handler\n"); int errno_temp = errno; while (waitpid(-1, NULL, WNOHANG) > 0) ; errno = errno_temp; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan bu "SIGCHLD" sinyalinde şöyle de bir semantik vardır; anımsanacağı üzere "thread" leri "detach" ettiğimiz vakit, onların "exit code" değerini ıskarta ediyor ve bellekte kapladığı alan otomatik olarak boşaltılıyordu. Aynı işlevi bu sinyalde de yapabiliriz. Şöyleki; -> Bu sinyalin varsayılan işlevini, "fork" çağrısından evvel, açıkça "ignore" etmemiz durumunda, alt proses sonlandığında kaynakları otomatik olarak boşaltılır. Her ne kadar zaten varsayılan davranış "ignore" olsa da biz yine açıkça "ignore" etmeliyiz. Eğer açıkça "ignore" etme işlemi "fork" işleminden sonra yapılırsa, sonucun ne olacağı işletim sisteminden işletim sistemine göre değişmektedir. Ancak ilgili sinyal fonksiyonunda artık "wait" uygulayamayız. İşte bu yöntem de "zombie process" oluşumunu engellemenin bir diğer yoludur. Diğer yandan POSIX standartlarınca "sigaction" fonksiyonunda "SA_NOCLDWAIT" bayrağı da bulundurulmaktadır. Bu bayrak yalnız "SIGCHLD" sinyali için kullanılabilir. Programcı isterse bu bayrağı kullanarak da "zombie process" oluşumunun önüne geçebilir. Ancak bu bayrağın kullanılması durumunda, halihazırda "SIGCHLD" sinyali için "set" edilen fonksiyonun çağrılıp çağrılmayacağı işletim sistemine bağlıdır. * Örnek 1, #include #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Press ENTER to continue... Child Process Child Process Child Process */ struct sigaction sa; sa.sa_handler = SIG_IGN; sa.sa_flags = SA_RESTART; if(sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid; for (int i = 0; i < 3; ++i) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { printf("Child Process\n"); usleep(rand() % 100000); _exit(EXIT_SUCCESS); } } printf("Press ENTER to continue...\n"); getchar(); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Son olarak yukarıda detaylarına değinilen sinyallere ek olarak "SIGUSR1" ve "SIGUSR2" sinyalleri, programcılar kendi uygulamalarında kullansın diye, bulundurulmaktadır. Bu sinyalleri prosesler arası haberleşme amacıyla kullanabiliriz. Bu sinyallerin varsayılan eylemi ise prosesin sonlandırılmasıdır. >> Gerçek Zamanlı Sinyaller: Normal sinyaller bir takım dezavantajlara sahiptirler. Örneğin, sinyallerin biriktilememesi, sinyaller arasında önceliğin olmaması vs. Dolayısıyla 90'lı yıllarda "realtime extension" adı altında eklenmişlerdir. Fakat bu tip sinyallere isim verilmemiş, numarası "[SIGRTMIN,SIGRTMAX]" arasında olan numaralar verilmiştir. Gerçek Zamanlı Sinyallerin Normal Sinyallerden farkı şunlardır; -> Gerçek Zamanlı olanlar kuyruklanmaktadır ve o sayı kadarkiler ele alınır. Halbuki Normal sinyallerde bir sayaç mekanizması yoktur. Dolayısıyla o sinyalden kaç tane oluşmuş olursa olsun, yalnızca bir tanesi ele alınacaktır. Fakat Gerçek Zamanlı sinyallerde ise on tanesi de ele alınacaktır. -> Gerçek Zamanlı sinyallerde bir bilgi de sinyalle birlikte iliştirilebilmektedir. Bu bilgi bir "int" değer de olabilir, bir adres bilgisi de. Adres bilgisi kullanılması durumunda kullanılan adresin o proses nezdinde ANLAMLI OLMASI GEREKİR. Bir diğer ifadeyle "shared memory" kullanmamız gerekmektedir. -> Gerçek Zamanlı sinyallerde yine bir öncelik kavramı da vardır. Küçük numaralı sinyal yüksek öncelik belirtmektedir. -> Gerçek Zamanlı sinyal göndermek için "kill" değil, "sigqueue" kullanmalıyız. "kill" kullanılması durumunda kuyruklama işleminin yapılıp yapılmayacağı işletim sistemine bağlıdır. -> "set" işlemi için "signal" DEĞİL, "sigaction" kullanmalıyız. Bu fonksiyonun bazı parametreleri Gerçek Zamanlı sinyaller içindir. Dolayısıyla ilgili yapının "sa_handler" elemanı yerine "sa_sigaction" isimli elemanını kullanmalıyız. Yine bununla birlikte ilgili yapının "sa_flags" elemanına "SA_SIGINFO" bayrağını da "bit-wise OR" işlemiyle geçmeliyiz. Şöyleki; struct sigaction sa; //... sa.sa_sigaction = signal_handler_func; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; Dolayısıyla kullanılacak sinyal "handle" fonksiyonunun da prototipi aşağıdaki gibi olmak zorundadır; void signal_handler_func(int, siginfo_t*, void*); Prototipin birinci parametresi beklenen sinyal numarasıdır. İkinci parametresi ise "siginfo_t" türünden bir adres bilgisi olup, beklenen sinyale ilişkin ayrıntılı bilgileri içerir. POSIX standartlarınca "siginfo_t" türü aşağıdaki gibidir; siginfo_t { int si_signo; // Oluşan sinyalin numarası. int si_code; // Signal code. int si_errno; // O andaki "errno" değeri. pid_t si_pid; // Sinyali gönderen prosesin "Process ID" değeridir. uid_t si_uid; // Sinyali gönderen prosesin "Real User ID" değeridir. void *si_addr; // Address of faulting instruction. int si_status; // Exit value or signal. long si_band; // Band event for SIGPOLL. union sigval si_value; // Signal value. }; Prototipin üçüncü parametresi çok da önemli değildir. /*================================================================================================================================*/ (78_03_09_2023) & (79_09_09_2023) & (80_10_09_2023) > UNIX/Linux Sistemlerinde Sinyal İşlemleri (devam): >> Gerçek Zamanlı Sinyaller (devam): Gerçek Zamanlı sinyallerin numaralarının "[SIGRTMIN,SIGRTMAX]" arasında olduğunu belirtmiştik. POSIX standartlarında ise bir sistemin desteklemesi gereken asgari Gerçek Zamanlı sinyal adedi ise "_POSIX_RTSIG_MAX" sembolik sabitiyle belirtilmiştir ki değeri ise "8" dir. Dolayısıyla bir UNIX türevi sistem en az sekiz adet Gerçek Zamanlı sinyal oluşturabilmelidir. Pekala sistemimizdeki desteklenen Gerçek Zamanlı sinyal adedini ise "limits.h" başlık dosyasındaki "RTSIG_MAX" sembolik sabiti üzerinden de öğrenebiliriz. Ancak yine "RTSIG_MAX" sembolik sabitinin tanımlanması bir zorunluluk değildir, "sysconf" fonksiyonuna "_SC_RTSIG_MAX" değerini geçerek öğrenebiliriz. Bu konunun detaylarına ileride işlenecek olan Sistem Limitleri konusunda ele alınacaktır. Öte yandan Gerçek Zamanlı sinyallerin kuyruklanabilir olduğunu söylemiştik. Yani aynı sinyal birden fazla kez oluştuğunda, bu sinyaller işletim sistemi tarafından saklanmaktadır. Pekiyi bu kuyruğun uzunluk bilgisi nedir? Bu bilgi de yine sistemden sisteme değişiklik göstermektedir. Fakat POSIX standartları, bir sistemin desteklemesi gereken azami kuyruk uzunluğunu "_POSIX_SIGQUEUE_MAX" sembolik sabitiyle, "limits.h" içerisinde, belirtmiştir. Ancak bu sembolik sabit toplam kuyruk uzunluğu olup, sinyal özelinde oluşturulan kuyruk uzunluğu DEĞİLDİR. Tabii o sistemdeki gerçek kuyruk uzunluk bilgisini de "SIGQUEUE_MAX" sembolik sabitiyle de öğrenebiliriz. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # SIGRTMIN: 34 SIGRTMAX: 64 SIGRTMAX - SIGRTMIN: 30 RTSIG_MAX: 32 _POSIX_SIGQUEUE_MAX: 32 */ printf("SIGRTMIN: %d\n", SIGRTMIN); printf("SIGRTMAX: %d\n", SIGRTMAX); printf("SIGRTMAX - SIGRTMIN: %d\n", SIGRTMAX - SIGRTMIN); printf("RTSIG_MAX: %d\n", RTSIG_MAX); printf("_POSIX_SIGQUEUE_MAX: %d\n", _POSIX_SIGQUEUE_MAX); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi yukarıda belirtilen fakat tanımlanması bir zorunluluk olmayan sembolik sabitler konusu nedir? -> Belirli bir sistem limiti düşünelim. Bu limit, "boot" edildikten sonra hiç değiştirilmeyecek olsun. Bu değerin "define" edilmesinde herhangi bir sakınca bulunmamaktadır. Diğer yandan "config" edilebilir bir limit düşünelim. Dolayısıyla "boot" sürecinden sonra bunun değeri değişebilir. İşte böylesi değerleri de "define" etmeye lüzum görülmemiştir. POSIX standartları da, bu tip değerleri ortak paydada buluşturabilmek için, sistem çalışırken değeri değişebilecek olanlara "define" zorunluluğu GETİRMEMİŞTİR. O anki değerini alabilmek için de "sysconf" isimli fonksiyon geliştirilmiştir. Konunun detaylarına ileride işlenecek olan "System Limits" konusunda tekrar değinilecektir. * Örnek 1, #include #include #include #include #include int set_sigqueue_max(void); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # SIGRTMIN: 34 SIGRTMAX: 64 SIGRTMAX - SIGRTMIN: 30 RTSIG_MAX: 32 _POSIX_SIGQUEUE_MAX: 32 SIGQUEUE_MAX: 28421 */ printf("SIGRTMIN: %d\n", SIGRTMIN); printf("SIGRTMAX: %d\n", SIGRTMAX); printf("SIGRTMAX - SIGRTMIN: %d\n", SIGRTMAX - SIGRTMIN); printf("RTSIG_MAX: %d\n", RTSIG_MAX); printf("_POSIX_SIGQUEUE_MAX: %d\n", _POSIX_SIGQUEUE_MAX); printf("SIGQUEUE_MAX: %d\n", set_sigqueue_max()); return 0; } int set_sigqueue_max(void) { int queue_max; #ifdef SIGQUEUE_MAX queue_max = SIGQUEUE_MAX; #else queue_max = sysconf(_SC_SIGQUEUE_MAX); #endif return queue_max; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Gerçek Zamanlı sinyalleri göndermek için "sigqueue" fonksiyonu çağrılır. Prototipi aşağıdaki gibidir; #include int sigqueue(pid_t pid, int signo, union sigval value); Fonksiyonun birinci parametresi, sinyalin gönderileceği prosesin ID değeridir. İkinci parametre, gönderilecek sinyalin numarasıdır. Bu numara için "SIGRTMIN + n" kalıbını kullanmalıyız. Buradaki "n" ibaresi ise kaç numaralı sinyali gönderdiğimizi belirtmektedir. Üçüncü parametre ise sinyale iliştirilecek ek bilgiyi belirtmektedir. Bu parametre ise aşağıdaki gibi tanımlanmıştır: union sigval { int sival_int; void* sival_ptr; }; "union" olmasından dolayı yapının ilgili elemanlarından sadece bir tanesini kullanabiliriz. Tabii adres bilgisi kullanılacaksa, adres bilgisinin hedef proses nezdinde anlamlı olması gerekmektedir. Fonksiyon başarı durumunda "0", hata durumunda "-1" değerine geri döner. Tabii bu fonksiyonla sinyal gönderebilmek için, "kill" fonksiyonunda olduğu gibi, sinyali gönderen prosesin; -> "Gerçek/Etkin Kullanıcı ID" değerinin sinyali alan prosesin "Gerçek/Saklı Kullanıcı ID" değerine eşit olması -> Uygun önceliğe sahip olması gerekmektedir. Bu fonksiyonla; -> Normal sinyaller gönderebiliriz ancak bu normal sinyaller KUYRUKLANAMAYACAKTIR. -> Gerçek Zamanlı gönderdiğimiz sinyalleri, yukarıda prototipi paylaşılan "signal_handler_func" isimli fonksiyon kullanarak yakalamak zorunda değiliz. Normal sinyalleri yakalamk için kullanılan "handler" fonksiyonları da kullanabiliriz. Fakat sinyale iliştirilen ek bilgiyi ELDE EDEMEYİZ. -> "kill" fonksiyonunda olduğu gibi, "process group" a sinyal gönderebilme yetimiz YOKTUR. Diğer yandan; -> "kill" fonksiyonuyla gönderilen normal sinyali, yukarıdaki 3 parametreli "handler" fonksiyon ile yakalayabiliriz fakat iliştirilen değeri elde edemeyiz. -> "kill" fonksiyonuyla Gerçek Zamanlı sinyal de gönderebiliriz fakat bu işlemin semantiği açıkça belirtilmediğinden, yakalanan sinyaller kuyruklanmayabilir. Ek olarak, "kill" isimli kabuk komutunu "-q" / "--queue" seçeneği ile birlikte kullanırak da Gerçek Zamanlı sinyal gönderebiliriz. Gerçek Zamanlı sinyalleri tam manasıyla alabilmek için artık yukarıda "signal_handler_func" ismiyle paylaşılan fonksiyon prototipini kendi "signal handler" fonksiyonumuzda kullanmalı ve "SA_SIGINFO" bayrağını da "sa_flags" elemanına "bit-wise OR" ile geçmeliyiz. * Örnek 1.0, Gerçek Zamanlı sinyallerin tam manasıyla alınmasına ilişkin bir örnektir. Aşağıdaki iki programı da iki farklı terminal üzerinden çalıştırılması gerekmektedir. // Gerçek Zamanlı sinyalin beklenmesi #include #include #include #include void sig_handler(int, siginfo_t*, void*); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # sig_handler: 100 :> */ struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(SIGUSR1, &sa, NULL) == -1) exit_sys("sigaction"); printf("waiting for signals...\n"); for(;;) pause(); return 0; } void sig_handler(int, siginfo_t* info, void*) { printf("sig_handler: %d\n", info->si_int); /* "si_int" is Linux specific. */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Gerçek Zamanlı sinyalin gönderilmesi #include #include #include #include void exit_sys(const char* msg); // ./sq // ./sq 45545 10 100 int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } union sigval sv; sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, Aşağıdaki örnekte ise sinyal bekleyen proses 34 numaralı sinyali beklemektedir. Dolayısıyla beklenmeyen bir sinyal gönderilmesi halinde, o sinyalin varsayılan davranışı uygulanacaktır. // Gerçek Zamanlı sinyalin beklenmesi #include #include #include #include void sig_handler(int, siginfo_t*, void*); void exit_sys(const char* msg); // ./sample 34 int main(int argc, char** argv) { /* # OUTPUT # 34 sig_handler: 123 :> */ if (argc != 2) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(atoi(argv[1]), &sa, NULL) == -1) exit_sys("sigaction"); printf("waiting for signals...\n"); for(;;) pause(); return 0; } void sig_handler(int signo, siginfo_t* info, void*) { printf("%d sig_handler: %d\n", signo, info->si_int); /* "si_int" is Linux specific. */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Gerçek Zamanlı sinyalin gönderilmesi #include #include #include #include void exit_sys(const char* msg); // .sq // ./sq 45545 34 123 int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } union sigval sv; sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0, Aşağıdaki örnekte ise gönderilen sinyallerin bir kuyrukta bekletildiği görülmüştür. "FIFO" sırası gözetilmiştir. // Gerçek Zamanlı sinyalin beklenmesi #include #include #include #include void sig_handler(int, siginfo_t*, void*); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # 34 sig_handler 123 34 sig_handler 456 34 sig_handler 789 */ if (argc != 2) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(atoi(argv[1]), &sa, NULL) == -1) exit_sys("sigaction"); sigset_s ss; sigfillset(&ss); if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Sleeps for 10 seconds...\n"); sleep(10); sigfillset(&ss); if (sigprocmask(SIG_UNBLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Waiting for a signal...\n"); for (;;) pause(); return 0; } void sig_handler(int signo, siginfo_t* info, void*) { printf("%d sig_handler: %d\n", signo, info->si_int); /* "si_int" is Linux specific. */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Gerçek Zamanlı sinyalin gönderilmesi #include #include #include #include void exit_sys(const char* msg); // ./sq // ./sq 46398 34 123 // ./sq 46398 34 456 // ./sq 46398 34 789 int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } union sigval sv; sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.1, Aşağıdaki örnekte ise gerçek zamanlı olmayan sinyallerin kuyruklanmadığı görülmüştür. // Gerçek Zamanlı sinyalin beklenmesi #include #include #include #include void sig_handler(int, siginfo_t*, void*); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # 10 sig_handler 12 */ if (argc != 2) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(atoi(argv[1]), &sa, NULL) == -1) exit_sys("sigaction"); sigset_s ss; sigfillset(&ss); sigdelset(&ss, SIGINT); // "Ctrl+C" yaptığımız zaman programı sonlandırabilmek için. if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Sleeps for 10 seconds...\n"); sleep(10); sigfillset(&ss); if (sigprocmask(SIG_UNBLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Waiting for a signal...\n"); for (;;) pause(); return 0; } void sig_handler(int signo, siginfo_t* info, void*) { printf("%d sig_handler: %d\n", signo, info->si_int); /* "si_int" is Linux specific. */ } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Gerçek Zamanlı sinyalin gönderilmesi #include #include #include #include void exit_sys(const char* msg); // ./sq // ./sq 46398 10 12 // ./sq 46398 10 13 // ./sq 46398 10 14 // ./sq 46398 10 15 // ./sq 46398 10 16 // ./sq 46398 10 17 int main(int argc, char** argv) { if (argc != 4) { fprintf(stderr, "wrong # of arguments!..\n");; exit(EXIT_FAILURE); } union sigval sv; sv.sival_int = atoi(argv[3]); if (sigqueue(atoi(argv[1]), atoi(argv[2]), sv) == -1) exit_sys("sigqueue"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bütün bunların yanı sıra POSIX standartlarında sinyalleri senkron bir biçimde beklemek için "sigwait" ve "sigwaitinfo" isimli iki fonksiyon bulundurulur. Yani sinyalin işlenmesi tamamlanmalı ki esas akışımız yoluna devam edebilsin. >> "sigwait" & "sigwaitinfo": Fonksiyonların prototipleri aşağıdaki gibidir; #include int sigwait(const sigset_t * set, int * sig); int sigwaitinfo(const sigset_t * set, siginfo_t * info); Fonksiyonların birinci parametresi beklemesini istediğimiz sinyalleri belirtmek için kullanılır. Bu küme normal sinyaller içerebildiği gibi Gerçek Zamanlı sinyalleri de içerebilir. Burada arka planda bir "bit" dizisi bulundurulur ve ilgili sinyallere karşı gelen ilgili indeksteki "bit" değeri "1" yapılır. Böylelikle hangi "bit" değerleri "1" ise onlar beklenir. Beklenen sinyalin gelmesi durumunda da o sinyali ilgili kümeden çıkartır. Tabii bu bekleme ile kastedilen, bu fonksiyonları çağıran "thread" in blokede bekletilmesidir. Hangi sinyalin işlenmesi bittiyse, onun bilgilerini da ikinci parametredeki adrese yazar. Bu iki fonksiyon arasındaki tek fark "sigwait" fonksiyonunun oluşan sinyalin sadece numarasını vermesi, "sigwaitinfo" nun ise daha çok bilgiyi vermesidir. Başarı durumunda fonksiyonlar "0", hata durumunda ise "sigwait" fonksiyonu hata kodunun kendisine dönerken "sigwait" ise "-1" e geri dönüp "errno" değişkenini uygun değere çeker. POSIX standartlarınca sadece "EINVAL" değeri tanımlanmıştır. Diğer yandan fonksiyonun birinci parametresine geçtiğimiz sinyal kümesinde bulunan Gerçek Zamanlı sinyaller kuyruklanabilir olduğundan, kuyruktaki yalnızca ilk sinyalin bilgilerini elde edebiliriz. Velevki Gerçek Zamanlı ve normal sinyallerden birden fazla oluşması durumunda, yani "pending" durumda hem normal hem de Gerçek Zamanlı sinyaller varsa, bunlardan hangisinin bilgilerinin elde edileceği POSIX standartlarınca tanımlanmamıştır, yani "unspecified". Öte yandan fonksiyonun birinci parametresinde belirtilen kümedeki sinyallerin, daha önceden bloke edilmiş olmaları gerekmektedir. POSIX standartlarınca bloke edilmeden iş bu "sigwait" & "sigwaitinfo" çağrılarının "Tanımsız Davranış" oluşturacağı belirtilmiştir. Son olarak bloke edilen sinyaller yine iş bu fonksiyon çağrıları tarafından otomatik bir biçimde "unblock" EDİLMEMEKTEDİR. Dolayısıyla bizler bloke ettiğimiz gibi blokeyi de kaldırmalıyız. * Örnek 1.0, İkinci bir terminal programı üzerinden "SIGUSR1" sinyali "receiver" prosesine gönderilmiştir. // Signal Sender: "14248", "receiver" prosesinin ID'sidir. "kill -USR1 14248" // Sinal Receiver: #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Waiting for signals... Signal-10 occured!.. */ sigset_t ss; sigaddset(&ss, SIGINT); sigaddset(&ss, SIGUSR1); // Block the signals: if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Waiting for signals...\n"); int result, signal_result; if ((result = sigwait(&ss, &signal_result)) != 0) { fprintf(stderr, "sigwait: %s\n", strerror(result)); exit(EXIT_FAILURE); } /* Start: Sinyalin İşlendiği Kısım */ printf("Signal-%d occured!..\n", signal_result); /* End: Sinyalin İşlendiği Kısım */ // Unblock the signals: if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, Kendi terminali üzerinden "SIGINT" sinyali, "Ctrl+C" yapılarak, "receiver" prosesine gönderilmiştir. // Sinal Receiver: #include #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Waiting for signals... Signal-2 occured!.. */ sigset_t ss; sigaddset(&ss, SIGINT); sigaddset(&ss, SIGUSR1); // Block the signals: if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); printf("Waiting for signals...\n"); int result, signal_result; if ((result = sigwait(&ss, &signal_result)) != 0) { fprintf(stderr, "sigwait: %s\n", strerror(result)); exit(EXIT_FAILURE); } /* Start: Sinyalin İşlendiği Kısım */ printf("Signal-%d occured!..\n", signal_result); /* End: Sinyalin İşlendiği Kısım */ // Unblock the signals: if (sigprocmask(SIG_BLOCK, &ss, NULL) == -1) exit_sys("sigprocmask"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Görüldüğü gibi bizler sinyalleri bloke ettikten sonra, "sigwait" & "sigwaitinfo" çağrıları ile aslında o sinyalleri de işlemiş olduk. Beklediğimiz sinyaller için bir "handler" fonksiyon kullanmadık da, ana akışı kullandık. Böylelikle senkron bir biçimde yapmış olduk. Daha önceki örneklerde yapılış biçimleri asenkron biçimdeydi. > "long jump" : Anımsanacağı üzere "goto" ifadeleri ile programın akışının bir noktadan bir noktaya geçmesini mümkün kılabilmekteyiz. Ancak C standartlarına göre "goto" deyiminin aynı fonksiyon içerisinde bulunması gerekmektedir. * Örnek 1, #include int foo() { goto MY_EXIT; // error: label ‘MY_EXIT’ used but not defined } int main(void) { /* # OUTPUT # Waiting for signals... Signal-10 occured!.. */ foo(); MY_EXIT: ; return 0; } Dolayısıyla "goto" ifadeleriyle fonksiyonun dışındaki bir alana programın akışı geçiremiyoruz. Çünkü, -> "goto" çağrısının yapıldığı fonksiyondaki yerel değişkenlerin, "goto" çağrısından sonraki durumları. -> "goto" çağrısı ile gidilen fonksiyondaki "return" çağrısı ile nereye geri dönülecektir? gibi problemleri de beraberinde getirecektir. İşte akışı fonksiyon dışına geçirmemize olan tanıyan mekanizmaya ise "long jump" denmektedir. Fakat bu mekanizmanın kullanımı da sınırlıdır. Programın akışının daha önce geçmiş olduğu bir noktaya "long jump" yapabiliyoruz, her ne kadar ismi tam olarak bu anlamı veremiyor olsada. Yani teknik olarak geçmişe gidiyoruz. C dilinde "long jump" işlemi ise iki adet Standart C fonksiyonu ile yapılmaktadır. Bunlar "setjmp" ve "longjmp" isimli fonksiyonlardır. >> "setjmp" ve "longjmp": Fonksiyonların prototipleri aşağıdaki gibidir; #include int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val); "setjmp" ile geri dönmek istediğimiz noktayı belirliyor, "longjmp" ile de o noktaya tekrar dönüyoruz. Eğer "setjmp" çağrısı ilk kez çağrılmışsa "0", "longjmp" vasıtasıyla çağrılmışsa da "longjmp" ın ikinci parametresine geçtiğimiz değerle geri dönmektedir. Fonksiyonların birinci parametresi ise "jmp_buf" yapı türündendir. Bizler bu yapı türünden bir nesneyi argüman olarak fonksiyonlara geçmeliyiz. Eğer "setjmp" çağrısının geri dönüş değerini kontrol etmezsek, sonsuz döngüye gireceğiz. * Örnek 1, Sonsuz Döngü: #include #include jmp_buf g_checkPoint; void baz() { printf("baz()\n"); longjmp(g_checkPoint, 31); } void bar() { printf("bar()\n"); baz(); } void foo() { printf("foo()\n"); setjmp(g_checkPoint); bar(); } int main(int argc, char** argv) { /* # OUTPUT # foo() bar() baz() bar() baz() bar() baz() bar() baz() bar() baz() ... */ foo(); return 0; } * Örnek 2, Normal kullanım: #include #include jmp_buf g_checkPoint; void baz() { printf("baz()\n"); longjmp(g_checkPoint, 31); } void bar() { printf("bar()\n"); baz(); } void foo() { printf("foo()\n"); int jump_result; if ((jump_result = setjmp(g_checkPoint)) == 0) bar(); else printf("Came from the past: %d\n", jump_result); } int main(int argc, char** argv) { /* # OUTPUT # foo() bar() baz() Came from the past: 31 */ foo(); return 0; } Öte yandan "setjmp" ve "longjmp" isimli fonksiyonlar, ilgili prosesin "signal mask" kümesinin korunacağını garanti ETMEMEKTEDİR. Yani "setjmp" çağrısı sırasında prosesin sinyal bloke kümesinin de kayıt altına alınacağı, "longjmp" çağrısı ile tekrar geri yükleneceği GARANTİ EDİLMEMİŞTİR. İşte bu garantiyi sağlayanlar "sigsetjmp" ve "siglongjmp" isimli fonksiyonlardır. Bir diğer deyişle bu iki fonksiyon, evvelki fonksiyonların sinyalli versiyonlarıdır. >> "sigsetjmp" ve "siglongjmp" : Fonksiyonların prototipleri aşağıdaki gibidir; #include int sigsetjmp(sigjmp_buf env, int savesigs); void siglongjmp(sigjmp_buf env, int val); Fonksiyonların kullanım biçimileri "setjmp" ve "longjmp" fonksiyonları ile benzerdir. Tek fark "sigsetjmp" iki parametre almasıdır ki iş bu ikinci parametre prosesin sinyal kümesinin kayıt altına alınıp alınmayacağını belirtir; bu parametreye "0" geçilirse ilgili sinyal bloke kümesi dikkate alınmaz, "0" dışı ise dikkate alınır. Pekiyi bizler "long jump" işlevine tam olarak hangi senaryolarda gereksinim duyarız? Örneğin, oluşan sinyalden dolayı sonsuz döngüye girmemiz durumunda, yine bu mekanizma ile daha güvenli bir yere atlayabiliriz. Fakat "long jump" ile gittiğimiz yerde hala "asenkron sinyal güvenli" fonksiyonları kullanmak zorundayız. Bu mekanizma, bu konuda bize bir güvence SAĞLAMAMAKTADIR. * Örnek 1.0, "SIGSEGV" sinyalinin oluşması, "sigsegv_handler" tekrar tekrar çağrılmasına sebebiyet verecektir. #include #include #include #include void sigsegv_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # SIGSEGV handler SIGSEGV handler SIGSEGV handler SIGSEGV handler ... */ struct sigaction sa; sa.sa_handler = sigsegv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; // "SA_RESTART" bayrağından dolayı yeniden başlatılacaktır. if(sigaction(SIGSEGV, &sa, NULL) == -1) exit_sys("sigaction"); char* ptr = (char*)0x123456789; // Programımız için tahsis edilmemiş bir alana erişimden kaynaklı // sinyal gönderilmiştir. putchar(*ptr); return 0; } void sigsegv_handler(int sig) { printf("SIGSEGV handler\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, Dolayısıyla bundan kurtulmak için "exit" fonksiyon çağrısını, "sigsegv_handler" içerisinde yapabiliriz. #include #include #include #include void sigsegv_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # SIGSEGV handler */ struct sigaction sa; sa.sa_handler = sigsegv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; // "SA_RESTART" bayrağından dolayı yeniden başlatılacaktır. if(sigaction(SIGSEGV, &sa, NULL) == -1) exit_sys("sigaction"); char* ptr = (char*)0x123456789; // Programımız için tahsis edilmemiş bir alana erişimden kaynaklı // sinyal gönderilmiştir. putchar(*ptr); return 0; } void sigsegv_handler(int sig) { printf("SIGSEGV handler\n"); exit(EXIT_FAILURE); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.2, Bir diğer alternatif ise daha güvenli bir noktaya "long jump" yapmaktır. #include #include #include #include #include void sigsegv_handler(int sig); void exit_sys(const char* msg); jmp_buf g_jbuf; int main(void) { /* # OUTPUT # SIGSEGV handler */ struct sigaction sa; sa.sa_handler = sigsegv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; // "SA_RESTART" bayrağından dolayı yeniden başlatılacaktır. if(sigaction(SIGSEGV, &sa, NULL) == -1) exit_sys("sigaction"); char* ptr = (char*)0x123456789; if (sigsetjmp(g_jbuf, 1) == 31) { printf("SIGSEGV handler\n"); //... } else { // Programımız için tahsis edilmemiş bir alana erişimden kaynaklı // sinyal gönderilmiştir. putchar(*ptr); } return 0; } void sigsegv_handler(int sig) { //... siglongjmp(g_jbuf, 31); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Diğer taraftan bazı sinyallerin, örneğin "SIGSEGV" sinyali, varsayılan davranışı prosesi sonlandırmak ve bir "core" dosyası oluşturmaktadır. Böyle sinyaller terminal ekranında "core dumped" yazısının çıkmasına da neden olur. >> "core" dosyası: Bu dosyanın üretilme amacı, bu dosyanın "debugger" tarafından incelenmesini sağlatmaktır. Böylelikle programın nerede ve nasıl çöktüğüyle alakalı bilgi alabiliriz. Tabii bu dosyanın üretilmesi hususunda da bir takım sınırlamalar mevcuttur. Linux sistemleri için bahsedecek olursak; "ulimit -a" komutu ile sorgulama yaptığımız zaman "core file size" değerinin "0" OLMAMASI gerekmektedir. Pekala aynı sorgulamayı, "ulimit -c" komutu ile de gerçekleştirebiliriz. "core file size" değerinin "unlimited" hale getirilmesi için, "ulimit -c unlimited" komutunun çalıştırılması gerekmektedir. Yine iş bu "core" dosyalarının isimlendirme kurallarını görmek için, "/proc/sys/kernel" dizini içerisindeki "core_pattern" isimli dosyaya bakmalıyız. Bu dosya içerisinde bir "core" dosyasının hangi isimle ve nerede oluşturulacağı bilgisi belirtilmiştir. Tipik olarak aşağıdaki gibi bilgiler yer almaktadır, iş bu dosyada; "/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %e" Buradan hareketle; -> "systemd-coredump" : "core" dosyasını oluşturan program. "/usr/lib/systemd" dizini içerisinde yer alır. -> "%P" : PID of dumped process, as seen in the initial PID namespace (since Linux 3.12). -> %u : Numeric real UID of dumped process. -> %g : Numeric real GID of dumped process. -> %s : Number of signal causing dump. -> %t : Time of dump, expressed as seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). -> %c : Core file size soft resource limit of crashing process (since Linux 2.6.24). -> ... : (see "https://man7.org/linux/man-pages/man5/core.5.html" for more information.) Pekiyi bizler "core" dosyalarını nasıl görüntüleyebiliriz? İş bu dosyalar genellikle ".lz4" formatında, yani sıkıştırılmış biçimde, "/var/lib/systemd/coredump/" dizininde oluşturulurlar. "debugger" programlar sıkıştırılmış bu dosyaları yükleyemediklerinden, ilk olarak bu dosyaları "coredumpctl" isimli program vasıtasıyla açıyoruz. Eğer bu program sistemimizde yüklü değilse, "sudo apt install systemd-coredump" komutuyula sistemimize yükleyebiliriz. Daha sonra, "coredumpctl list" komutuyla da sistemimizdeki bütün "core" dosyalarını görüntüleyebiliriz. Daha sonra "coredumpctl gdb" komutuyla da en son oluşturulan "core" dosyasının detaylarını görüntüleyebiliriz. > Proses Grupları: Prosesler konusundaki bir diğer kavram da Proses Grup kavramıdır. Bu kavram bir takım prosese tek hamlede sinyal gönderebilmek için oluşturulmuştur. Yine Proses Grup kavramı da ID değerine sahiptir ve buna "Process Group ID" denmektedir. Bu ID değeri ise grup içerisinde bulunan bir prosesin ID değerine eşittir. İş bu prosese ise "Process Group Leader" denir. Yani proses grubunun lideri konumunda olan prosesin ID değeri, o proses grubunun ID değeridir. Genellikle proses grupları, proses grup lideri tarafından oluşturulur fakat liderin o gruptan daha sonra ayrılması da mümkündür. Bu durumda o proses grubunun ID değeri DEĞİŞMEYECEKTİR. Benzer şekilde lider konumunda olan prosesin sonlanması durumunda onun ID değerini işletim sistemi başka yerde kullanmaz çünkü halihazırda bir proses grubu tarafından kullanıldığı için. Diğer yandan proses grubunun sonlandırılması, yani lağvedilmesi, için o proses grubunda hiç bir prosesin kalmaması gerekiyor. Öte yandan "fork" işlemi sırasında proses grup ID değeri de alt prosese, üst proses tarafından, aktarılır. Yani üst proses hangi gruptaysa, alt proses de o grupta olacaktır. Pekiyi bizler bir prosesin içinde bulunduğu proses grubuna ilişkin bilgileri nasıl öğrenebiliriz? Bu noktada devreye "getpgrp" ve "getpgid" isimli POSIX fonksiyonları girecektir. >> "getpgrp" : Fonksiyonu çağıran prosesin "Process Group ID" değerini bize geri döndürür. Fonksiyonun prototipi aşağıdaki gibidir; #include pid_t getpgrp(void); Fonksiyon için herhangi bir hata senaryosu tanımlanmamıştır, yani her zaman için başarılı olur. >> "getpgid" : Herhangi bir prosesin içinde bulunduğu proses grubunun ID değerini bize döndürür. Fonksiyonun prototipi aşağıdaki gibidir; #include pid_t getpgid(pid_t pid); Fonksiyon argüman olarak hangi grupta olduğunu merak ettiğimiz prosesin ID değerini alır. "0" değerini geçersek, fonksiyonu çağıran prosesinkini bize döndürecektir. Fonksiyonumuz hata durumunda "-1" ile geri döner ve "errno" değişkenini uygun değerine çeker. Pekiyi bizler bir proses grubu nasıl oluşturur veya bir prosesin grubunu nasıl değiştirebiliriz? Bu noktada da devreye "setpgid" fonksiyonu girmektedir. >> "setpgid" : Fonksiyonun prototipi aşağıdaki gibidir; #include int setpgid(pid_t pid, pid_t pgid); Fonksiyonun birinci parametresine Proses Grup ID değerini değiştirmek istediğimiz prosesin ID değerini, ikinci parametresine de hedef Proses Grup ID değerini geçiyoruz. Böylelikle prosesimiz başka bir gruba dahil olacaktır. Eğer bu iki parametreye aynı değeri geçersek, o ID değerinde yeni bir proses grubu oluşturulur ve prosesimiz de grubun lideri olur. Tabii bu işlemleri aynı oturum("session") içerisinde, prosesimizin uygun önceliğe sahip olup olmadığına bakılmaksızın, kendimiz ve/veya alt proseslerimiz için gerçekleştirebiliriz. Fakat buradaki kritik nokta ise şudur; alt prosesimiz "exec" işlemi uyguladıktan sonra artık onun proses grup ID değerini DEĞİŞTİREMEYİZ. Oturum("session") kavramına ilerleyen zamanlarda değinilecektir. Son olarak fonksiyonun birinci parametresine "0" geçilmesi, bu fonksiyonu çağıran proses; ikinci parametresine "0" geçilmesi, birinci parametresine geçilen proses kastedilmektedir. Dolayısıyla aşağıdaki çağrılar aynı anlama gelmektedir; setpgid(getpid(), getpid()); setpgid(0, getpid()); setpgid(getpid(), 0); setpgid(0, 0); İşte kabuk program(lar)ı da aslında bu şekilde çalışmaktadır. Yani "shell" programı önce "fork" yapar. Üst ya da alt proses içerisinde "setpgid" fonksiyonunu çağırır ve yeni bir proses grubu oluşturup, alt prosesin bu grubun lideri olmasını sağlar. * Örnek 1, "shell" programı üzerinden "|" kullanarak birden fazla komut çalıştırdığımızı varsayalım: "ls -l | grep "sample" Bu çağrı sonucunda "shell" programı "ls" ve "grep" programları için "fork" işlemi yapacaktır. İlk "fork" çağrısı "ls" için yapılacağından, oluşturulacak proses grubunun lideri de "ls" olacaktır. Eğer "ls" ve "grep" programları da kendi içerisinde "fork" yapıyorsa, oluşan bu yeni alt prosesler de yine aynı proses grubuna eklenecektir. Buradaki kilit nokta tek bir komut satırında birden fazla program çalıştırılmasıdır. * Örnek 2, // Terminal - I: "cat | grep "sample" " komutunu çalıştıralım. // Termianl - II: "ps -t pts/0 -o pid, ppid, pgid,cmd" kabuk komutunu çalıştırdığımız zaman, "Terminal - I" tarafından çalıştırılan prosesleri göreceğiz: PID PPID PGID CMD 34667 34658 34667 bash 49458 34667 49458 cat 49459 34667 49458 grep Görüleceği üzere "cat" ve "grep" prosesleri için tek bir proses grubu oluşturulmuş. * Örnek 3.0, #include #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # Parent Process ID: 53 Grandparent Process ID: 52 Parent Process Group ID: 53 Child Process ID: 57 Parent Process ID: 53 Child Process Group ID: 57 */ pid_t pid, pgid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* Parent Process */ printf("Parent Process ID: %jd\n", (intmax_t)getpid()); printf("Grandparent Process ID: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Parent Process Group ID: %jd\n", (intmax_t)pgid); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else { /* Child Process */ sleep(1); // Yeni bir proses grubu oluşturulacak. if (setpgid(getpid(), getpid()) == -1) exit_sys("setpgid"); printf("Child Process ID: %jd\n", (intmax_t)getpid()); printf("Parent Process ID: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Child Process Group ID: %jd\n", (intmax_t)pgid); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3.1, #include #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # Parent Process ID: 614 Grandparent Process ID: 613 Parent Process Group ID: 614 Child Process ID: 618 Parent Process ID: 614 Child Process Group ID: 614 */ pid_t pid, pgid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* Parent Process */ printf("Parent Process ID: %jd\n", (intmax_t)getpid()); printf("Grandparent Process ID: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Parent Process Group ID: %jd\n", (intmax_t)pgid); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else { /* Child Process */ sleep(1); printf("Child Process ID: %jd\n", (intmax_t)getpid()); printf("Parent Process ID: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Child Process Group ID: %jd\n", (intmax_t)pgid); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Aşağıdaki programda ise "Ctrl+C" yaparak proses grubundaki proseslere "SIGINT" sinyali gönderilmiştir. #include #include #include #include #include #include void sig_handler(int sno); void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # Parent Process ID: 230 Grandparent Process ID: 229 Parent Process Group ID: 230 Child Process ID: 234 Parent Process ID: 230 Child Process Group ID: 230 :> ^C waitpid: Interrupted system call SIGINT in the Process: 234 SIGINT in the Process: 230 */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) == -1) exit_sys("sigaction"); pid_t pid, pgid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* Parent Process */ printf("Parent Process ID: %jd\n", (intmax_t)getpid()); printf("Grandparent Process ID: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Parent Process Group ID: %jd\n", (intmax_t)pgid); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else { /* Child Process */ sleep(1); printf("Child Process ID: %jd\n", (intmax_t)getpid()); printf("Parent Process ID: %jd\n", (intmax_t)getppid()); pgid = getpgrp(); printf("Child Process Group ID: %jd\n", (intmax_t)pgid); pause(); } return 0; } void sig_handler(int sno) { printf("SIGINT in the Process: %jd\n", (intmax_t)getpid()); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Şu da belirtilmelidir ki "thread" ler için herhangi bir proses grup kavramı YOKTUR. Proses Grup kavramı yalnızca prosesler temelinde mevcuttur. Yani bütün "thread" ler içerisinde yapılan "getgrp" çağrıları aynı sonucu verecektir. > Hatırlatıcı Notlar: >> Bu dünyada senkron ve asenkron kelimeleri şu anlamlarda kullanılırlar: -> Asenkron demek; esas akış devam ederken, o olayın arka planda yapılması demektir. Örneğin, biz bir yerden okuma yapacağız. Asenkron çalışırsak, okuma işlemini başlatırız fakat onun bitmesini beklemeyiz. Arka planda okuma işi yapılırken, esas akış da kendi işine bakar. -> Senkron demek; esas akışın devam edebilmesi için, o olayın tamamlanması gerekmektedir. >> C99 ile dile eklenen "_Exit" fonksiyonu, "_exit" POSIX fonksiyonu ile birebir aynı işlevselliktedir. /*================================================================================================================================*/ (81_16_09_2023) & (82_17_09_2023) & (83_23_09_2023) & (84_24_09_2023) & (85_30_09_2023) & (86_01_10_2023) & (87_07_10_2023) & (88_08_10_2023) & (89_14_10_2023) & (90_15_10_2023) & (91_22_10_2023) & (92_28_10_2023) & (93_29_10_2023) & (94_04_11_2023) & (95_05_11_2023) & (96_11_11_2023) > Oturum("Session") Kavramı: Proses Gruplarının oluşturduğu topluluğa Oturum denir. Bir Oturum içerisinde bir adet ön plan("foreground") proses grubu olurken, birden fazla arka plan("background") proses grubu olabilir. İşte "shell" programı üzerinden program çalıştırırken komut satırı ifadesi olarak "&" kullanırsak, artık o program arka planda çalışacak ve "shell" programı onu "wait" ile beklemeyecektir. İşte böylesi programlar, o Oturum içerisinde arka plan("background") olarak kabul edilirler. Eğer komut satırı ifadesi olarak "&" kullanmadan program çalıştırırsak, artık o program ön plan("foreground") olacak ve "shell" programı onu "wait" ile bekleyecektir. "shell" programından "Ctrl+c" ve/veya "Ctrl+delete" yaparsak, ön plandaki proses grubuna ilgili sinyaller gönderilir. Diğer yandan Proses Gruplarından arka planda olanları ön plana, ön planda olanı da arka plana çekebiliriz. Zaten Oturum kavramı da "shell" gibi terminal programları için düşünülen kavramlardır. Öte yandan Oturum'ların da bir ID değeri vardır ki bu değer aslında Oturum'u hayata getiren, yani lider konumunda olan, Proses Grubunun ID değeridir. Bir diğer deyişle Oturum'u hayata getiren proses grubu lider konumunda oluyor ve grubun ID değeri, o oturumun da ID değeri oluyor. Özetle; -> "shell", bir adet proses grubu oluşturur ve kendisini bu grubun lideri yapar. -> Yine "shell", bir oturum oluşturur ve kendisinin içinde bulunduğu grubu, yani yukarıda oluşturduğu grubu, o oturumun lideri yapar. -> Yine "shell", "&" ile biten komutlara ilişkin proses grup(lar)ı oluşturur ve bu grupları da yukarıda oluşturduğu oturumun arka plan proses grubu olarak belirler. Tabii prosesin kendisinin bulunduğu proses grubu ön plan proses grubu olur. -> Yine "shell", "&" ile bitmeyen komuta ilişkin bir proses grubu oluşturur, onu içinde bulunduğu oturumun ön plan proses grubu olarak belirler. Tabii prosesin kendisinin bulunduğu proses grubu arka plan proses gruplarından birisi olur. Böylece bir "t" anında bir oturum içerisinde birden fazla arka plan proses grubu mevcut olabilirken, bir tane ön plan proses grubu olur. * Örnek 1, // Terminal - I : "bash" üzerinden aşağıdaki kodları çalıştıralım ve programların sonsuza kadar çalıştığını varsayalım. ./sample & ./mample // Terminal - II: İkinci bir terminal üzerinden aşağıdaki komutu çalıştıralım. "-t pts/0" ifadesi "Terminal - I" e ilişkindir. "ps -o pid, pgid, sid, cmd, -t pts/0" Bu komut çağrısı sonucunda "Terminal - II" nin çıktısı şöyle olacaktır; PID PGID SID CMD 31684 31684 31684 bash 52630 52630 31684 ./mample 52632 52632 31684 ./sample Görüleceği üzere "Terminal - I" olan "bash" programı "mample" ve "sample" için birer proses grubu oluşturmuş ve bu programları o proses gruplarının lideri yapmıştır. Daha sonra bu proses gruplarını içeren bir Oturum oluşturmuştur. Her ne kadar yukarıdaki çıktıdan bakıldığında hangi proses gruplarının ön plan, hangilerinin arka plan olduğu anlaşılamasa da "./mample" arka plan, "./sample" ise ön plandır. Diğer yandan bir Oturum mutlak suretle bir terminal sürücüsüyle bağlantılı olmalıdır. İşte "Ctrl+C" ve/veya "Ctrl+Delete" yaptığımız zaman gönderilen sinyal, iş bu terminal sürücüsünden gönderilir. Bu terminal sürücüsüne ise "controlling terminal" denir. Pekiyi bizler bir oturumun ID değerini nasıl elde edebiliriz? Bu noktada devreye "getsid" fonksiyonu devreye girmektedir. >> "getsid" : Fonksiyonun prototipi aşağıdaki gibidir. #include pid_t getsid(pid_t pid); Fonksiyon parametre olarak bir "process ID" değeri alır. Eğer bu değer "0" olarak girilirse, fonksiyonu çağıran proses ele alınır. Hata durumunda "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(int argc, char** argv) { /* # OUTPUT # Parent > Process ID: 252 Parent > Grandparent Process ID: 251 Parent > Process Group ID: 252 Parent > Process Season ID: 252 Child > Process ID: 256 Child > Grandparent Process ID: 252 Child > Process Group ID: 252 Child > Process Season ID: 252 */ pid_t pid, pgid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* Parent Process */ printf("Parent > Process ID: %jd\n", (intmax_t)getpid()); printf("Parent > Grandparent Process ID: %jd\n", (intmax_t)getppid()); printf("Parent > Process Group ID: %jd\n", (intmax_t)getpgrp()); printf("Parent > Process Season ID: %jd\n", (intmax_t)getsid(0)); if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); } else { /* Child Process */ sleep(1); printf("Child > Process ID: %jd\n", (intmax_t)getpid()); printf("Child > Grandparent Process ID: %jd\n", (intmax_t)getppid()); printf("Child > Process Group ID: %jd\n", (intmax_t)getpgrp()); printf("Child > Process Season ID: %jd\n", (intmax_t)getsid(0)); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi bizler nasıl oturum oluştururuz? Tabii ki "setsid" fonksiyonunu kullanarak. >> "setsid" : Fonksiyonun prototipi aşağıdaki gibidir. #include pid_t setsid(void); Burada fonksiyon şu işlemleri yerine getirmektedir; -> Yeni bir oturum, bu oturum içerisinde de yeni bir proses grubu oluşturur. -> Oluşturulan iş bu oturum ve proses grubunun lideri ise iş bu fonksiyonu çağıran prosestir. Herhangi bir hata durumunda fonksiyon "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. Örneğin, bu fonksiyonu çağıran proses içinde bulunduğu proses grubunun lideriyse fonksiyon başarısız olacaktır. Yani "shell" üzerinden çağrıdığımız prosesler direkt olarak "setsid" çağrılıyorsa, fonksiyon çağrısı başarısız olacaktır. Bunun "work-around" yöntemi ise önce "fork" yapmak ve alt proseste bu fonksiyon çağrısı yapmaktır. Tipik olarak bu fonksiyon, "shell" tarafından işin başında çağrılır. Böylelikle hem bir proses grubu hem de bir oturum oluşturulmuş olur ki bunların lideri de "shell" programıdır, eğer bir komut "shell" üzerinden henüz çalıştırılmamışsa. Velevki bir komut "&" ile bitmeyen komutlar "shell" üzerinden çalıştırılırsa, bu komuta ilişkin proses(ler) için yeni bir proses grubu oluşturacak ve bu grudu da oturumun ön plan proses grubu olarak atayacaktır. * Örnek 1.0, #include #include #include void exit_sys(const char *msg); int main(void) { /* # OUTPUT # setsid: Operation not permitted */ if (setsid() == -1) exit_sys("setsid"); printf("%s\n", "Hello World"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, #include #include #include void exit_sys(const char *msg); int main(void) { /* # OUTPUT # Hello World */ pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) exit(EXIT_SUCCESS); if (setsid() == -1) exit_sys("setsid"); printf("%s\n", "Hello World"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Buraya kadar bizler önce proses grubu, sonrasında da bir oturum oluşturmayı gördük. Pekiyi bizler iş bu oturumu bir terminal sürücüsüyle nasıl ilişkilendirebiliriz? Şöyleki; "open" fonksiyonu ile, "0_NOCTTY" bayrağını kullanmadan, bir terminal aygıt sürücüsü açmak. Eğer o anda hedef terminal aygıt sürücüsü başka bir oturuma bağlıysa, o bağlantı kopartılacaktır. * Örnek 1, #include #include #include #include int main(void) { /* # OUTPUT # Hello World */ pid_t pid; if ((pid = fork()) == -1) _exit(EXIT_FAILURE); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); // Dosya betimleyicilerini kapatıyoruz. // Toplamda 1024 olduğu varsayılmıştır. for (int i = 0; i < 1024; ++i) close(i); if (open("/dev/tty3", O_RDONLY) == -1) _exit(EXIT_FAILURE); if (open("/dev/tty3", O_WRONLY) == -1) _exit(EXIT_FAILURE); if (dup(1) == -1) _exit(EXIT_FAILURE); // Artık aşağıdaki yazı "tty3" terminal ekranında // gözükecektir. printf("%s\n", "Hello World"); return 0; } Eğer bizler ön plan proses grubunun hangisi olduğunu merak ediyorsak veya bir proses grubunu ön plan proses grubu yapmak istiyorsak, sırasıyla "tcgetpgrp" ve "tcsetpgrp" isimli fonksiyonları çağırmalıyız. >> "tcgetpgrp" ve "tcsetpgrp" : Fonksiyonların prototipi aşağıdaki gibidir. #include pid_t tcgetpgrp(int fildes); int tcsetpgrp(int fildes, pid_t pgid_id); Fonksiyonlar terminal aygıt sürücüsüne ilişkin dosya betimleyiciler ile çalışmaktadır. Bu konudaki bir diğer husus da şudur; arka planda çalışmakta olan proses grupları, içinde bulundukları oturuma ilişkin terminalden okuma/yazma yapmak istediklerinde: -> İlk önce ilgili proses grubuna "SIGTTIN" sinyali gönderilir. Bu sinyalin varsayılan eylemi proseslerin durdurulması yönündedir. Yani o gruptaki bütün prosesler durdurulur. -> İlgili terminal programından "fg" komutu ile iş bu proses grubunu ön plan proses grubu olarak atanır. -> Daha sonra ilgili proses grubuna "SIGCONT" sinyali gönderilerek prosesler yeniden çalışır duruma getirilir. * Örnek 1, Aşağıdaki programı bir terminal üzerinden çalıştırınız. 10 saniye sonra, ikinci bir terminal vasıtasıyla "ps -l" komutunu çalıştırın. Çıktı ekranında aşağıdaki prosesin "status" bilgisi için "terminated" yazdığı görülecektir. #include #include int main(void) { printf("Sleep in 10 seconds...\n"); sleep(10); // 10 saniye boyunca bloke edilecek. printf("Sleep ended.\n"); int ch; ch = getchar(); putchar(ch); return 0; } * Örnek 2, Arka planda çalışmak olan bir proses okuma yapmak istediğinde "SIGTTIN" sinyali gönderilir ve proses durdurulur. Bu durumda bir "signal_handler" tanımlayarak, hata kodunun bir dosyaya yapılmasını mümkün kılabiliriz. Eğer yeniden başlatılamayan bir sistem fonksiyonu içerisinde bulunuyorsak, ilgili sistem fonksiyonu "EINTR" hata koduyla geri dönecektir. Aşağıdaki programı "&" ifadesiyle birliktte çalıştırmayı unutmayınız. #include #include #include #include #include void sig_handler(int sig); void exit_sys(const char* msg); FILE* g_f; int main(void) { /* # log.txt # SIGTTIN occured! getchar terminated by signal!... */ if ((g_f = fopen("log.txt", "w")) == NULL) exit_sys("fopen"); struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if(sigaction(SIGTTIN, &sa, NULL) == -1) exit_sys("sigaction"); sleep(5); // 5 saniye bekledikten sonra ---> (1) // ---> (1) : "getchar" çağrısı sırasında bir sistem fonksiyonu // içerisinde olacağız. Bu nedenden dolayı ilgili sistem fonksiyonu // "EINTR" hata koduyla geri dönecektir. if (getchar() == EOF && errno == EINTR) fprintf(g_f, "getchar terminated by signal!...\n"); sleep(1); fclose(g_f); return 0; } void sig_handler(int sig) { fprintf(g_f, "SIGTTIN occured!\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Terminal, Oturum ve Proses Grupları hakkındaki diğer hususlarda şöyledir; -> Bir terminal ekranı kapatıldığında, yani ekranın sağ üst köşesindeki "x" tuşuna basarak, terminal aygıt sürücüsü, o terminalin ilişkin olduğu oturumda bulunan bütün proseslere "SIGHUP" sinyali gönderir. Bu sinyalin varsayılan davranışı ise prosesin sonlandırılması yönündedir. Eğer bu sinyali "ignore" olarak ele alırsak terminal üzerinden yapacağımız "read" işlemleri("read" fonksiyon çağrısı) "0" ile geri dönerken, "write" işlemleri ise ("write" fonksiyon çağrısı) "EIO" hata kodu ile başarısız olmaktadır. Burada ilgili prosesin ön ya da arka planda çalışıyor olması önemsizdir. Dolayısıyla biz bir "shell" ekranını kapatırsak, o ekran üzerinden çalıştırılmış bütün prosesler de sonlanacaktır. Ayrıca burada şöyle de bir nüans vardır; anımsanacağı üzere arka planda çalışan bir proses terminal üzerinden okuma yapmak istediğinde kendisine "SIGTTIN" sinyali gönderilir ki bu sinyalin varsayılan davranışı ilgili prosesi "stop" duruma getirmesidir. İşte terminal ekranı kapatıldığında bu tip "stop" durumdaki proseslere de yine "SIGHUP" sinyali gönderilir ancak "stop" durumdakiler sadece "SIGKILL" sinyalini işlediklerinden, gönderilen "SIGHUP" sinyali "pending" durumda bekletilir ve prosese iletilmez. İşte bu problemi gidermek için bazı kabuk programları "SIGHUP" sinyaline ek olarak "SIGCONT" sinyali de gönderilir. Böylece ilgili proses önce tekrar çalışır hale getirilir, sonra "SIGHUP" sinyalini alması sağlanır ve en sonunda başlar başlamaz sonlanması sağlanır. -> Bir terminalinden "exit" komutu ile çıkarsak ön plan proses gruplarına yine "SIGHUP" sinyali gönderilir. Arka plandakilerin de o terminal ile bağlantısı kopar. Örneğin, "bash" programında "exit" ile çıkmak istersek ilk önce arka plan proses gruplarına ilişkin uyarı mesajı verir. İkinci kez "exit" komutunun çağrılmasıyla o proses gruplarına da "SIGHUP" sinyali gönderir. Tabii "stop" durumdakiler için ayrıca "SIGCONT" sinyali de gönderilir. -> Bir terminal kapatıldığında ya da "exit" ile çıkıldığında, o terminalde çalışan programların çalışmasına devam etmesi isteniyorsa, bunun için "nohup" ve "disown" isimli komutlardan faydalanmak gerekir. "nohup" komutu çalıştırdığı programın "SIGHUP" sinyalini "ignore" eder, "disown" ise prosesi oturumdan koparır. Örneğin: $ nohup ./sample & Artık terminal kapatılsa bile bu prosesler çalışmaya devam edecektir. "nohup" komutu "stdout" dosyasını "nohup.out" isimli bir dosyaya yönlendirmektedir. -> Bir proses grubundaki her bir prosesin üst prosesi, o alt proses ile aynı proses grubundaysa ya da o alt proses ile aynı oturumda değilse, böyle proses gruplarına "Öksüz Proses Grupları (Orphan Process Groups)" denir. Bir diğer deyişle eğer bir proses grubundaki en az bir prosesin üst prosesi kendisiyle aynı oturumda ancak farklı bir proses grubundaysa, o proses grubu "Öksüz Proses Grupları(Orphan Process Groups)" DEĞİLDİR. Bu tanımdan yola çıkarak "shell" programının içinde bulunduğu proses grubu da aslında bir "Orphan Process Group" biçimindedir. Çünkü "shell" programını çalıştıran üst proses aslında farklı bir oturumdadır. Öte yandan bir proses grubu öksüz hale geldiğinde, eğer o proses grubu içerisinde "stop" durumda olan bir proses varsa, öksüz hale gelmiş olan bu proses grubundaki proseslere "SIGHUP" ve "SIGCONT" sinyalleri de gönderilir. Diğer yandan böylesi öksüz proses grupları terminalden okuma işlemi yapmak istediklerinde "read" fonksiyonu "EIO" değeriyle başarısız olurken, "write" fonksiyonları ise terminal aygıt sürücüsünün "-tostop" özelliği kapalıysa normal olarak terminale yazmakta ancak açıksa "EIO" değeriyle başarısız olur. Örneğin, üst prosesi sonlanmış bir alt proses ile terminalden "read" yapmaya çalışırsak başarısız olacağız. Ancak terminale "write" yapmaya çalışırsak, "-tostop" özelliğine göre, ya yazdıklarımız ekrana çıkacak ya da "EIO" değeriyle başarısız olacaktır. Bir diğer yandan öksüz proses gruplarına, arka planda çalışıp çalışmadığına bakılmaksızın, terminalden "read" yaptığında "SIGTTIN" sinyalinin, "write" yaptığında ise "SIGTTOU" sinyalinin gönderilmediğine, terminalin o anki "-tostop" ayarına bakılmaksızın, DİKKAT EDİNİZ. Son olarak kabuk programları, terminal bağlantısı koptuğunda ya da terminal penceresi kapatıldığında, öksüz proses gruplarına "SIGHUP" sinyali gönderilmeyecek, dolayısıyla bu öksüz proses grubundaki prosesler hayatlarına DEVAM EDECEKTİR. Bu konular hakkında ayrıntılı açıklamalar için "Advanced Programming in the UNIX Environment by W. Richard Stevens". > POSIX sistemlerinde "sleep" işlemleri: Bir "thread" in akışını belli bir süre bekletmek için POSIX ve türevi sistemlerde "sleep" fonksiyonları bulundurulmaktadır. Bu fonksiyonlar "thread" i bloke ederek çalışma kuyruğundan çıkartır, özel "sleep" kuyruklarına yerleştirir. Vakit dolduğunda da yeniden çalışma kuyruğuna aktarılır. Böylece CPU zamanı harcamadan istenen bekleme sağlanmış olur. Çok eski bir fonksiyondur. Bu fonksiyonlar sırasıyla, "sleep", "usleep", "nanosleep" ve "clock_nanosleep" isimli fonksiyonlardır. >> "sleep" : Saniye cinsinden duyarlılığa sahiptir. Fonksiyonun prototipi aşağıdaki gibidir; #include unsigned sleep(unsigned seconds); Fonksiyon parametre olarak beklenecek sürenin saniye cinsinden değerini alır. Geri dönüş değeri ise, sinyal dolayısıyla gerçekleşen, erken sonlanmalarda kalan saniyeyi belirtir. "0" değeri ile sonlanması ise normal sonlanma olduğunu belirtmektedir. Sinyal gelmesi durumunda ya proses sonlandırılır ya da ilgili "signal_handler" fonksiyonu çağrılır. Fakat bir sinyal geldiğinde "sleep" fonksiyonu hiçbir zaman yeniden başlatılmaz, yani "SA_RESTART" bayrağını kullansak bile. >> "usleep" : Bir POSIX fonksiyonu değildir fakat Linux ve bazı Linux türevi sistemlerde bulunmaktadır. Bu fonksiyon mikrosaniye çözünürlüğe sahiptir. Prototipi aşağıdaki gibidir; #include int usleep(useconds_t usec); Fonksiyon başarı durumunda "0", hata durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Bu fonksiyon da tıpkı "sleep" gibi yeniden başlatılamaz. >> "nanosleep" : Nanosaniye çözünürlüğe sahiptir. Bir POSIX fonksiyonudur. Fonksiyonun prototipi aşağıdaki gibidir; #include int nanosleep(const struct timespec *rqtp, struct timespec *rmtp); Fonksiyonun "timespec" yapısını daha önceki örneklerde de kullanmıştık. Aşağıdaki gibi bir tanımı vardır: struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds [0, 999'999'999] */ }; Fakat POSIX standartları bu çözünürlüğü garanti etmemektedir çünkü işlemcimizin de nanosaniye mertebesinde çalışabilmesi gerekmektedir. Dolayısıyla işlemci izin verdiği müddetçe nanosaniyeye doğru bir yaklaşma söz konusudur. Fonksiyonun birinci parametresine beklemek istediğimiz süre miktarını, ikinci parametresine bir başarısızlık durumunda geri kalan sürenin yazılacağı adres bilgisini geçeriz. Bu ikinci parametreye "NULL" değerini geçebiliriz. Her iki parametreye aynı nesnenin adresini geçmenin bir sakıncası yoktur. Fonksiyon başarı durumunda "0", hata durumunda "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Waiting for 3.5 seconds... :> OK */ printf("Waiting for 3.5 seconds...\n"); struct timespec ts; ts.tv_sec = 3; ts.tv_nsec = 500000000; // 500M nanoseconds = 0.5 seconds if (nanosleep(&ts, NULL) == -1) exit_sys("nanosleep"); printf("OK\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include #include void sig_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Waiting for 3.5 seconds... :> [2] : signal handler OK Time Left: 2 seconds Time Left: 392434000 nanoseconds */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) == -1) exit_sys("sigaction"); printf("Waiting for 3.5 seconds...\n"); struct timespec ts, ts_remaining; ts.tv_sec = 3; ts.tv_nsec = 500000000; // 500M nanoseconds = 0.5 seconds if (nanosleep(&ts, &ts_remaining) == -1 && errno != EINTR) exit_sys("nanosleep"); printf("OK\n"); printf("Time Left: %ju seconds\n", (intmax_t)ts_remaining.tv_sec); printf("Time Left: %ld nanoseconds\n", (intmax_t)ts_remaining.tv_nsec); return 0; } void sig_handler(int sig) { printf("[%d] : signal handler\n", sig); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "clock_nanosleep" : "nanosleep" fonksiyonu şöyle bir handikaba sahiptir; gerçek zamana göre bekleme yapmasıdır. Yani bekleme esnasında sistem saatinin değişmesi durumunda, bekleme işlemi daha kısa sürebilir. Yani "nanosleep" fonksiyonu sistem saatinin değişmesinden etkilenmektedir. İşte sistem saatindeki değişiklikten etkilenmeyen "clock_nanosleep" fonksiyonu POSIX standardında eklenmiştir. Fonksiyonun prototipi aşağıdaki gibidir; #include int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *rqtp, struct timespec *rmtp); Fonksiyonun birinci parametresi arka planda kullanılacak saat cinsini belirtmektedir. POSIX standartlarınca bu saat cinsleri şunlardır: -> "CLOCK_REALTIME" : Bekleme işleminin sistem saatindeki değişiklikten etkilenmesini sağlar. Örneğin, 30 saniye beklemek isterken, sistem saatinin ileri alınmasından dolayı, daha az bekleme söz konusu olabilmektedir. -> "CLOCK_MONOTONIC" : Bekleme işleminin sistem saatining değiştirilmesi ve diğer faktörlerden etkilenmemesini sağlar. Bu tip kararlı beklemeler için tercih edilmesi gereken tiptir. -> "CLOCK_PROCESS_CPUTIME_ID" : İşlemci zamanının ölçülmesinde kullanılır. Genel olarak bu tip, "timer tick" ler ile birlikte kullanılır. Bu belli bir prosesin bütün "thread" lerinin harcadığı CPU zamanının ölçülmesinde kullanılır. Yani prosesin "sleep" gibi uykuda beklediği veya blokede beklediği zamanlar hesapa dahil edilmeyecektir. -> "CLOCK_THREAD_CPUTIME_ID" : "thread" zamanının ölçülmesinde kullanılır. Genel olarak bu tip, "timer tick" ler ile birlikte kullanılır. Bu belli bir "thread" in harcadığı CPU zamanının ölçülmesinde kullanılır. Yani prosesin "sleep" gibi uykuda beklediği veya blokede beklediği zamanlar hesapa dahil edilmeyecektir. -> "clock_getcpuclockid()" : BURAYA TEKRAR GELİNECEKTİR. -> "pthread_getcpuclockid(): BURAYA TEKRAR GELİNECEKTİR. Fonksiyonun ikinci parametresi şu değerlerden birisini alır: -> "TIMER_ABSTIME" : Bu durumda bekleme zamanı göreli değil, mutlak olacaktır. Dolayısıyla bekleme miktarını belirtilen "timespec" yapısında beklemenin sonlandırılacağı mutlak zaman bilgisi bulunmalıdır. Mutlak zamanlı beklemeleri "thread" konusundaki zaman aşımlı senkronizasyon nesnelerini incelerken görmüştük. -> "0" : Beklemenin göreli olduğunu belirtir. Fonksiyonun üçüncü parametresi bekleme zamanını, dördüncü parametre ise fonksiyonun hata durumunda sonlanması durumunda geriye kalan zamanı belirtmektedir. Bu son parametreye "NULL" değerini geçebiliriz. Fonksiyonumuz hata durumunda hata kodunun kendisiyle, başarı durumunda "0" ile geri döner. * Örnek 1, #include #include #include #include #include #include void sig_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Waiting for 3.5 seconds... :> [2] : signal handler OK Time Left: 1 seconds Time Left: 184699300 nanoseconds */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) == -1) exit_sys("sigaction"); printf("Waiting for 3.5 seconds...\n"); struct timespec ts, ts_remaining; ts.tv_sec = 3; ts.tv_nsec = 500000000; // 500M nanoseconds = 0.5 seconds if (clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, &ts_remaining) == -1 && errno != EINTR) exit_sys("nanosleep"); printf("OK\n"); printf("Time Left: %ju seconds\n", (intmax_t)ts_remaining.tv_sec); printf("Time Left: %ld nanoseconds\n", (intmax_t)ts_remaining.tv_nsec); return 0; } void sig_handler(int sig) { printf("[%d] : signal handler\n", sig); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Şimdiki zamanın elde edilmesi: Şu andaki zamanı almak, zaman ölçümü yapmak vb. işler için halihazırda Standart C fonksiyonları bulundurulmuştur. Ancak bu fonksiyonlar genel olarak düşük bir çözünürlülüğe sahiptir. Bu fonksiyonlar, "time", "localtime", "gmtime", "ctime" ve "asctime", "mktime", "difftime", "strftime", "clock". isimli fonksiyonlardır. Bunlardan, >> "time" : "epoch" dan geçen zamanı saniye sayısını "time_t" türünden veren bir fonksiyondur. Prototipi şöyledir: #include time_t time(time_t *tloc); Fonksiyona "NULL" değerini geçersek, zaman bilgisini geri dönüş değeri olarak döndürür. Aksi halde adresini geçtiğimiz nesneye yazar. Buradaki "time_t" türü C standartlarına göre herhangi bir nümerik tür(`float`, "double", "int" vs.) olabilir. Fakat POSIX standartlarına göre tam sayı türü olması gerekmektedir. Diğer taraftan C standartlarında "epoch" belirtilmemişken, POSIX'e göre "epoch" noktası "01.01.1970" olarak belirlenmiştir. Zaten çoğu C derleyicisi de "epoch" olarak bu tarihi almaktadır. >> "localtime" : Fonksiyonun prototipi şöyledir: #include struct tm *localtime(const time_t *timer); Fonksiyon argüman olarak aldığı "time_t" türünden yapıyı bileşenlerine ayrıştırır ve "struct tm" biçiminde geri döndürür. "struct tm" yapısının bileşenleri şöyledir: struct tm { int tm_sec; // Seconds [0,60]. int tm_min; // Minutes [0,59]. int tm_hour; // Hour [0,23]. int tm_mday; // Day of month [1,31]. int tm_mon; // Month of year [0,11]. int tm_year; // Years since 1900. int tm_wday; // Day of week [0,6] (Sunday =0). int tm_yday; // Day of year [0,365]. int tm_isdst; // Daylight Savings flag. }; >> "gmtime" : Bu fonksiyon ise tarih ve zamanı UTC(eski adıyla GMT) olarak geri döndüren fonksiyondur. Yani "localtime" fonksiyonunnu UTC biçiminde döndüren halidir. Özünde her iki fonksiyon da aynı şeyi yapmaktadır. Fonksiyonun prototipi aşağıdaki gibidir: #include struct tm *gmtime(const time_t *timer); >> "ctime" ve "asctime" : Tarih ve zaman bilgisini bize doğrudan bir yazı biçiminde vermektedir. İkisi arasındaki tek fark aldığı argümanların türlerinin farklı olmasıdır. Fonksiyonların prototipleri aşağıdaki gibidir: #include char *ctime(const time_t *clock); char *asctime(const struct tm *timeptr); >> "mktime" : Argüman olarak verdiğimiz "timepoint" ile "epoch" arasında geçen saniyeyi bize döndürür. Fonksiyonun prototipi şöyledir: #include time_t mktime(struct tm *timeptr); >> "difftime" : C Standartlarına göre "epoch" ve "time_t" türü açıkça belirtilmediğinden, iki "time_t" arasındaki farkı almak için, böyle bir fonksiyon geliştirilmiştir. POSIX standartlarında bu fonksiyonun kullanılmasına gerek yoktur çünkü "epoch" ve "time_t" zaten tanımlıdır. Fonksiyonun prototipi aşağıdaki gibidir: #include double difftime(time_t time1, time_t time0); >> "strftime" : Adeta "snprintf" fonksiyonu gibi, tarih-zaman üzerinde formatlama yapmaktadır. Böylece tarih-zaman bilgisini istediğimiz gibi yazdırabiliriz. Fonksiyonun prototipi şöyledir: #include size_t strftime(char * s, size_t maxsize, const char * format, const struct tm * timeptr); Kullanılan formatlama karakterleri için fonksiyonun dökümantasyonuna bakınız. >> "clock" : Anımsanacağı üzere "time" fonksiyonu bize saniye bilgisini vermektedir fakat C Standartlarına göre "epoch" ve "time_t" bilgisi tanımlı değildir, yani derleyiciye bağlıdır. Dolayısıyla "time" fonksiyonu kullanarak, taşınabilir bir şekilde, iki "timepoint" arasında geçen süreyi hesaplamamız mümkün değildir. İşte bu problemi gidermek için "clock" fonksiyonu geliştirilmiştir. Fonksiyonun prototipi aşağıdaki gibidir: #include clock_t clock(void); Fonksiyonun geri dönüş türü "clock_t" türü aslında bir "time-tick" sayısını belirtmektedir ve "clock_t" türü ise C standartlarınca ve POSIX standartlatınca nümerik(tam sayı ya da gerçek sayı) bir tür olabilir. Ancak bir saniye için kaç adet "time-tick" gerektiği ise "CLOCKS_PER_SEC" sembolik sabiti ile tanımlanmıştır. "CLOCKS_PER_SEC" sembolik sabiti, -> Güncel Linux sistemlerinde bir milyon olarak tanımlanmıştır. Dolayısıyla Linux sistemlerindeki duyarlılık mikrosaniye mertebesindedir. -> Güncel Windows sistemlerinde ise bin olarak tanımlanmıştır. Dolayısıyla Windows sistemlerinde milisaniye mertebesinde duyarlılık vardır. O halde programın farklı iki noktasında bu fonksiyonu çağırıp, daha sonra geri dönüş değerlerinin farkını alıp ve en sonunda da sonucu iş bu sembolik sabite bölersek bahsi geçen iki nokta arasında geçen saniyeyi hesaplamış oluruz. Takribi olarak şöyledir; clock_t start, stop; //... start = clock(); //... stop = clock(); // Burada sonucu "double" türüne dönüştürmemiz önemli. // Aksi halde küsüratlar atılacaktır. double result = (double)(stop - start) / CLOCKS_PER_SEC; Bunlara ek olarak zaman ölçülmesinde kullanılan POSIX fonksiyonları da vardır. Bunlar, "clock_gettime", "clock_getres", "clock_settime", "times" isimli fonksiyonlardır. Bunlardan, >> "clock_gettime" : Fonksiyonun prototipi aşağıdaki gibidir: #include int clock_gettime(clockid_t clock_id, struct timespec *tp); Birinci parametremiz zaman ölçümünde kullanılacak saatin türünü almaktadır. Bu tür "CLOCK_REALTIME", "CLOCK_MONOTONIC", "CLOCK_PROCESS_CPUTIME_ID", "CLOCK_THREAD_CPUTIME_ID", "clock_getcpuclockid()" ve "pthread_getcpuclockid() değerlerinden birisi olabilir. Fonksiyonun ikinci parametresi "timespec" yapı nesnesinin adresini alır. Zaman bilgisi bu adrese yerleştirilir. Fonksiyon başarı durumunda "0", hata durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. * Örnek 1, #include #include #include void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Elapsed Time in nanoseconds: 1609367200 Elapsed Time in seconds : 1.609367 */ struct timespec ts_start, ts_end; if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == -1) exit_sys("clock_gettime"); for (int i = 0; i < 1000000000; ++i) ; if (clock_gettime(CLOCK_MONOTONIC, &ts_end) == -1) exit_sys("clock_gettime"); long long elapsed_time = (ts_end.tv_sec * 1000000000LL + ts_end.tv_nsec) - (ts_start.tv_sec * 1000000000LL + ts_start.tv_nsec); printf("Elapsed Time in nanoseconds: %lld\n", elapsed_time); printf("Elapsed Time in seconds : %f\n", elapsed_time / 1000000000.); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include void do_work(); void do_work_in_detail(); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Elapsed Time in nanoseconds: 13187545600 Elapsed Time in seconds : 13.187546 Elapsed Time in nanoseconds: 8058837800 Elapsed Time in seconds : 8.058838 */ do_work(); puts("\n"); do_work_in_detail(); return 0; } void do_work() { struct timespec ts_start, ts_end; if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == -1) exit_sys("clock_gettime"); for (int j = 0; j < 5; ++j) { for (int i = 0; i < 1000000000; ++i) ; sleep(1); } if (clock_gettime(CLOCK_MONOTONIC, &ts_end) == -1) exit_sys("clock_gettime"); long long elapsed_time = (ts_end.tv_sec * 1000000000LL + ts_end.tv_nsec) - (ts_start.tv_sec * 1000000000LL + ts_start.tv_nsec); printf("Elapsed Time in nanoseconds: %lld\n", elapsed_time); printf("Elapsed Time in seconds : %f\n", elapsed_time / 1000000000.); } void do_work_in_detail() { struct timespec ts_start, ts_end; if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_start) == -1) exit_sys("clock_gettime"); for (int j = 0; j < 5; ++j) { for (int i = 0; i < 1000000000; ++i) ; sleep(1); } if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_end) == -1) exit_sys("clock_gettime"); long long elapsed_time = (ts_end.tv_sec * 1000000000LL + ts_end.tv_nsec) - (ts_start.tv_sec * 1000000000LL + ts_start.tv_nsec); printf("Elapsed Time in nanoseconds: %lld\n", elapsed_time); printf("Elapsed Time in seconds : %f\n", elapsed_time / 1000000000.); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "clock_getres" : Her ne kadar "clock_gettime" fonksiyonu nanosaniye mertebesinde bir duyarlılığa sahip olsa bile gerçekte duyarlılık öyle olmayabilir. Çünkü duyarlılık işlemcinin o anki durumuna, işletim sisteminin çekirdek yapısı gibi faktörlere bağlı olarak değişkenlik gösterebilir. İşte duyarlılığın ne derece olduğu bilgisini "clock_getres" fonksiyonu ile öğrenebiliriz. Fonksiyonun prototipi aşağıdaki gibidir: #include int clock_getres(clockid_t clock_id, struct timespec *res); Fonksiyonun birinci parametresi duyarlılığı ölçülecek olan saat türünü belirtmektedir. İkinci parametre ise duyarlılık bilgisinin yerleştirileceği "timespec" yapısının adresini alır. * Örnek 1, Bir nanosaniye mertebesinde duyarlılığa sahip olduğu görülmüştür. #include #include #include void exit_sys(const char* msg); int main(void) { struct timespec ts; if (clock_getres(CLOCK_MONOTONIC, &ts) == -1) exit_sys("clock_gettime"); long long result = (ts.tv_sec * 1000000000LL + ts.tv_nsec); printf("Result: %lld\n", result); // Result: 1 return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "clock_settime" : Söz konusu saat türünü "set" etmek için kullanılır ancak çağıran prosesin uygun önceliğe sahip olması gerekmektedir. Öte yandan her saat türü "set" EDİLEMEYEBİLİR. Fonksiyonun prototipi aşağıdaki gibidir: #include int clock_settime(clockid_t clock_id, const struct timespec *tp); >> "times" : Bir prosesin "user-mode" ve "kernel-mode" olarak çalışırken harcadığı zamanlar, işletim sistemi tarafından, o prosesin kontrol bloğunda saklanır. İşte saklanan bu bilgileri bu fonksiyonla elde edebiliriz. Yine bir POSIX fonksiyonudur. Fonksiyonun prototipi aşağıdaki gibidir: #include clock_t times(struct tms *buffer); Fonksiyonumuz "tms" türünden bir yapı nesnesinin adresini alarak, çağrıldığı ana kadar geçen zamanı bu adrese yerleştirir. "tms" yapı nesnesi aşağıdaki gibidir: struct tms { clock_t tms_utime; // Prosesin "user-mode" da harcadığı zaman. clock_t tms_stime; // Prosesin "kernel-mode" da harcadığı zaman. clock_t tms_cutime; // "wait" fonksiyonlarıyla beklenen bütün alt proseslerin "user-mode" da harcadığı zaman. clock_t tms_cstime; // "wait" fonksiyonlarıyla beklenen bütün alt proseslern "kernel-mode" da harcadığı zaman. }; Fonksiyonun geri dönüş değeri; -> Başarı durumunda, belli bir orjinden geçen gerçek zamanı belirten bir değerdir ancak bu değer tek başına bir anlam ifade etmeyecektir. -> Hata durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. İşte kabuk komutu olan "time" komutu bu fonksiyonu çağırmaktadır. * Örnek 1, Aşağıdaki programı "./main "ls -l" komutuyla çalıştırınız. #include #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # Sleep in 2 seconds... :> Start working... :> total 24 -rw-r--r-- 1 ahmopasa ahmopasa 50 Feb 25 21:28 log.txt -rwxr-xr-x 1 ahmopasa ahmopasa 16384 Feb 27 10:06 main -rw-r--r-- 1 ahmopasa ahmopasa 979 Feb 27 10:06 main.c Itself: User Mode: 160 Kernel Mode: 0 Childs: User Mode: 0 Kernel Mode: 0 */ if (argc != 2) { fprintf(stderr, "wrong number of arguments\n"); exit(EXIT_FAILURE); } printf("Sleep in 2 seconds...\n"); sleep(2); printf("Start working...\n"); for (int i = 0; i < 1000000000; ++i) ; if (system(argv[1]) == -1) exit_sys("system"); struct tms ts; if (times(&ts) == -1) exit_sys("times"); puts("Itself:"); printf("User Mode: %jd\n", (intmax_t)ts.tms_utime); printf("Kernel Mode: %jd\n", (intmax_t)ts.tms_stime); puts("Childs:"); printf("User Mode: %jd\n", (intmax_t)ts.tms_cutime); printf("Kernel Mode: %jd\n", (intmax_t)ts.tms_cstime); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0, Komut satırından "./main ./sample" çağırarak "./sample" programının süresini ölçebiliriz. // ./sample int main() { for (int i = 0; i < 1000000000; ++i) ; } // ./mample #include #include #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # Itself: User Mode: 0.000000 Kernel Mode: 0.000000 Childs: User Mode: 0.000163 Kernel Mode: 0.000000 */ if (argc != 2) { fprintf(stderr, "wrong number of arguments\n"); exit(EXIT_FAILURE); } if (system(argv[1]) == -1) exit_sys("system"); struct tms ts; if (times(&ts) == -1) exit_sys("times"); puts("Itself:"); printf("User Mode: %f\n", (double)ts.tms_utime / CLOCKS_PER_SEC); printf("Kernel Mode: %f\n", (double)ts.tms_stime / CLOCKS_PER_SEC); puts("Childs:"); printf("User Mode: %f\n", (double)ts.tms_cutime / CLOCKS_PER_SEC); printf("Kernel Mode: %f\n", (double)ts.tms_cstime / CLOCKS_PER_SEC); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.1, // ./sample int main() { for (int i = 0; i < 1000000000; ++i) ; } // ./mample #include #include #include #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # OUTPUT # Itself: User Mode: 0.000000 Kernel Mode: 0.000000 Childs: User Mode: 0.000166 Kernel Mode: 0.000000 */ if (argc < 2) { fprintf(stderr, "wrong number of arguments\n"); exit(EXIT_FAILURE); } pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execvp(argv[1], &argv[1]) == -1) _exit(EXIT_FAILURE); if (wait(NULL) == -1) exit_sys("wait"); struct tms ts; if (times(&ts) == -1) exit_sys("times"); puts("Itself:"); printf("User Mode: %f\n", (double)ts.tms_utime / CLOCKS_PER_SEC); printf("Kernel Mode: %f\n", (double)ts.tms_stime / CLOCKS_PER_SEC); puts("Childs:"); printf("User Mode: %f\n", (double)ts.tms_cutime / CLOCKS_PER_SEC); printf("Kernel Mode: %f\n", (double)ts.tms_cstime / CLOCKS_PER_SEC); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > "Interval Timer Processes" : Bazen belli periyotta sürekli işlemlerin yapılması gerekebilmektedir. Örneğin, saniyede bir olacak şekilde bir fonksiyon çağrılması. Bu mekanizmayı oluşturmanın bir yolları şunlardır; -> Bir "thread" oluşturmak, bu "thread" içerisinde bir döngü yazmak ve "clock_nanosleep" gibi bir fonksiyonla bekleme yapmak. Ancak bunun için "thread" e ihtiyaç duymaktayız. -> "setitimer" isimli POSIX fonksiyonlarını kullanmak. >> "setitimer" : Bu fonksiyon ile iş bu mekanizma basit bir biçimde oluşturulabilmektedir. Ancak bu fonksiyon "deprecated" edilmiştir. Fonksiyonun prototipi aşağıdaki gibidir: #include int setitimer(int which, const struct itimerval * value, struct itimerval * ovalue); Fonksiyonun birinci parametresi "Interval Timer" mekanizmasına ilişkin bir tür bilgisidir. Bu tür bilgisine göre de değişik sinyaller gönderilir. Bu tür bilgisi aşağıdakilerden birisini alır: -> ITIMER_REAL: Gerçek zamana dayalı ölçüm için kullanılır. Zaman dolduğunda "SIGALRM" sinyali gönderilir. -> ITIMER_VIRTUAL: Prosesin çalışmasına göre ölçüm yapılır. Yani proses çalışmadığı sürece saat işlememektedir. Dolayısıyla uykuda geçen zaman dikkate alınmamaktadır. Zaman dolduğunda "SIGVTALRM" sinyali gönderilir. -> ITIMER_PROF: Prosesin çalışmasına göre ölçüm yapılır. Ancak prosesin uykuda geçirdiği zamanlar bu sefer dikkate alınır. Zaman dolduğunda "SIGPROF" sinyali gönderilir. Fonksiyonun ikinci parametresi "itimerval" yapı türünden türündendir. Bu yapı aşağıdaki gibidir: struct itimerval { struct timeval it_interval; // Periyodik işleme başlamak için gereken zaman bilgisi. struct timeval it_value; // Periyot zamanının bilgisi }; Ancak yapının elemanlarının "timeval" türünden olduğuna dikkat ediniz. Bu yapı ise aşağıdaki gibidir: struct timeval { time_t tv_sec; // Seconds. suseconds_t tv_usec; // Microseconds. }; Fonksiyonun üçüncü parametresi ise bir önceki "setitimer" çağrısıyla "set" edilen bilginin elde edilmesi için kullanılır, "NULL" değeri geçilebilir. Fonksiyon başarı durumunda "0", hata durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. * Örnek 1, #include #include #include #include #include void sig_handler(int sig); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # :> sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... ... */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGALRM, &sa, NULL) == -1) exit_sys("sigaction"); struct itimerval itval; itval.it_value.tv_sec = 5; // İlk periyoda kadar beş saniye geçecek. itval.it_value.tv_usec = 0; itval.it_interval.tv_sec = 1; // Her bir saniyede bir tetiklenecek. itval.it_interval.tv_usec = 0; if (setitimer(ITIMER_REAL, &itval, NULL) == -1) exit_sys("setitimer"); // Asenkron çalışma olduğundan, "main thread" i bekletmeliyiz. for(;;) pause(); return 0; } void sig_handler(int sig) { printf("sig_handler...\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Pekala mekanizmayı durdurmak da mümkündür. #include #include #include #include #include void sig_handler(int sig); void exit_sys(const char* msg); int g_count; int main(void) { /* # OUTPUT # :> sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... The signal has been called 10 time until now. */ struct sigaction sa; sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGALRM, &sa, NULL) == -1) exit_sys("sigaction"); struct itimerval itval; itval.it_value.tv_sec = 5; // İlk periyoda kadar beş saniye geçecek. itval.it_value.tv_usec = 0; itval.it_interval.tv_sec = 1; // Her bir saniyede bir tetiklenecek. itval.it_interval.tv_usec = 0; if (setitimer(ITIMER_REAL, &itval, NULL) == -1) exit_sys("setitimer"); // Asenkron çalışma olduğundan, "main thread" i bekletmeliyiz. for(;;) { if (g_count == 10) { // Mekanizma "disable" edilecektir. itval.it_value.tv_sec = 0; itval.it_value.tv_usec = 0; itval.it_interval.tv_sec = 0; itval.it_interval.tv_usec = 0; if (setitimer(ITIMER_REAL, &itval, NULL) == -1) exit_sys("setitimer"); break; } else { pause(); } } printf("The signal has been called %d time until now.\n", g_count); return 0; } void sig_handler(int sig) { printf("sig_handler...\n"); ++g_count; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Fakat bu fonksiyonun "deprecated" edildiğini söylemiştik. Çünkü şu dezavantajlara sahiptir; -> Haberdar etme mekanizması sadece sinyaller ile olması. Dolayısıyla "thread" kullanarak haberdar edilebilir olmamaları. -> Kullanılan sinyallerin Gerçek Zamanlı sinyaller olmamasından dolayı biriktirilebilir olmamaları, ekstra bilginin iletilememeleri vb. -> Zaman duyarlılığının mikrosaniye düzeyinde olmasıdır. İşte bu dezavantajları egale etmek için POSIX standartlarına bir takım fonksiyonlar eklenmek suretiyle bir mekanizma kurulmuştur. Fakat bu mekanizmanın kullanılması, "setitimer" fonksiyonunun kullanılmasına nazaran daha zahmetlidir. Kullanım biçimi şöyledir: -> İlk önce "timer_create" isimli fonksiyonu çağırarak bir adet "Internal Timer" nesnesi oluşturulur. -> Daha sonra "timer_settime" fonksiyonu ile periyot belirlenmelidir ve çalışmaya başlarız. -> En sonunda "timer_delete" fonksiyonu ile yukarıda oluşturduğumuz "Internal Timer" nesnesini yok ederiz. Bu fonksiyonlardan, >>> "timer_create" : Fonksiyonun prototipi aşağıdaki gibidir. #include #include int timer_create(clockid_t clockid, struct sigevent * evp, timer_t * timerid); Fonksiyonun birinci parametresi kullanılacak saat türüdür. Burada kullanılacak saat türü daha evvel gördüğümüz "CLOCK_REALTIME", "CLOCK_MONOTONIC", "CLOCK_PROCESS_CPUTIME_ID", "CLOCK_THREAD_CPUTIME_ID", "clock_getcpuclockid()" ve "pthread_getcpuclockid()" değerlerinden birisi olabilir. Fonksiyonun ikinci parametresi "sigevent" yapı türünden bir nesne adresi almaktadır. Bu yapı aşağıdaki gibi tanımlanmıştır: struct sigevent { int sigev_notify; // Notification type. int sigev_signo; // Signal number. union sigval sigev_value; // Signal value. void (*sigev_notify_function)(union sigval); // Notification function. pthread_attr_t *sigev_notify_attributes; // Notification attributes. }; Bu yapı türünden bir nesneyi bizler doldurduktan sonra fonksiyona geçmeliyiz. Dolayısıyla bu yapının, -> "sigev_notify" : Periyot tamamlandığında haberdar edilmenin nasıl yapıldığını belirtmek içindir ve şu değerlerden birisini yerleştirebiliriz; -> "SIGEV_NONE" : Haberdar edilme yapılmayacaktır. -> "SIGEV_SIGNAL" : Haberdar edilme sinyal yoluyla yapılacaktır. -> "SIGEV_THREAD" : Haberdar edilme, bu mekanizma tarafından oluşturulan, bir "thread" yoluyla yapılacaktır. Fakat toplamda bu iş için kaç adet "thread" oluşturulacağı POSIX standartlarınca işletim sistemini yazanların isteğine bırakılmıştır. -> "sigev_signo" : Haberdar edilme eğer sinyal yoluyla yapılacaksa, oluşturulacak sinyalin numarasıdır. Bu numara normal bir sinyale ait olabileceği gibi Gerçek Zamanlı bir sinyale de ait olabilir. Eğer normal sinyal kullanılırsa bir biriktirme yapılamayacağı için, ilgili proses bloke edildiğinde kaç periyot geçtiğini anlayamayız. İşte bunun için POSIX standartlarında "timer_getoverrun" isimli bir fonksiyon bulundurulmuştur. Bu fonksiyon sayesinde sinyalden dolayı bloke oluştuğunda kaç periyot geçtiği bilgisini öğrenebiliriz. -> "sigev_value" : Gerçek Zamanlı sinyallerde sinyale iliştirilecek bilgiyi belirtmektedir. Bu eleman aynı zamanda "thread" ile haberdar etme yönteminde de kullanılır. Bu tür de bir yapı türü olup, aşağıdaki gibi tanımlanmıştır: union sigval { int sival_int; // Integer signal value. void *sival_ptr; // Pointer signal value. }; -> "sigev_notify_function" : Eğer haberdar edilme "thread" yoluyla yapılacaksa, "thread" in çağıracağı fonksiyonu belirtmektedir. -> "pthread_attr_t" : "thread" oluştururken "thread" e ilişkin bir takım özellikleri "set" etmek için kullanılır. "NULL" geçilmesi halinde varsayılan özelliklerle "thread" oluşturulur. Fonksiyonun ikinci parametresine "NULL" değerini geçebiliriz. Bu durumda haberdar edilme sinyal yoluyla yapılacaktır ve varsayılan sinyal gönderilecektir. Fakat POSIX standartlarına göre varsayılan sinyalin ne olduğu belirtilmemiştir. Linux sistemlerinde bu "SIGALRM" sinyalidir. Fonksiyonun üçüncü parametresi ise oluşturulan "Interval Timer" nesnesinin ID değerinin yerleştirileceği adres bilgisidir. Fonksiyonun geri dönüş değeri başarı durumunda "0", hata durumunda "-1" olur ve "errno" değişkeni uygun değere çekilir. >>> "timer_settime" : Fonksiyonun prototipi aşağıdaki gibidir. #include int timer_settime(timer_t timerid, int flags, const struct itimerspec * value, struct itimerspec * ovalue); Fonksiyonun birinci parametresi, "timer_create" fonksiyonunun son parametresine geçtiğimiz nesne olmalıdır. Fonksiyonun ikinci parametresine ya "0" ya da "TIMER_ABSTIME" değerlerinden birini geçmeliyiz. "0" değeri geçilirse göreli zaman, "TIMER_ABSTIME" geçilirse de mutlak zaman dikkate alınır. Genellikle göreli zaman kullanılır, yani "0" değeri geçilir. Fonksiyonun üçüncü parametresi ilk haberdar edilmenin ve periyodik haberdar edilmenin ayarlandığı parametredir. Bu parametre "itimerspec" yapı türünden olup, programcı tarafından doldurulduktan sonra fonksiyona gönderilmelidir. Aşağıdaki gibi tanımlanmıştır: struct itimerspec { struct timespec it_interval; Timer period. struct timespec it_value; Timer expiration. }; -> "it_value" : İlk haberdar edilmeye kadar geçecek zamanı belirtir. -> "it_interval" : Haberdar edilme periyodunu belirtir. Bu yapının elemanları ise "timespec" türünden olup, aşağıdaki gibi tanımlanmıştır: struct timespec { time_t tv_sec; /* Seconds. */ long tv_nsec; /* Nanoseconds. */ }; Fonksiyonun son parametresi ise eski değerleri "get" etmek için kullanılır. Bu parametre "NULL" geçilebilir. Fonksiyonun geri dönüş değeri ise başarı durumunda "0", hata durumunda ise "-1" olur ve "errno" uygun değere çekilir. >> "timer_delete" : Fonksiyonun prototipi aşağıdaki gibidir. #include int timer_delete(timer_t timerid); Fonksiyonun parametresi "timer_create" ile oluşturduğumuz, "timer_settime" ile kullandığımız o nesnedir. Başarı durumunda "0", hata durumunda "-1" ile geri döner. Fakat normal şartlarda bu fonksiyonun geri dönüş değerinin kontrolüne gerek yoktur. Aşağıda bu mekanizmanın kullanılmasına ilişkin örnekler verilmiştir: * Örnek 1, Aşağıdaki örnekte oluşturulan "Interval Timer" nesnesi, zaten program sonlanacağı için, yok edilmemiştir. #include #include #include #include #include void sig_handler(int signo, siginfo_t* info, void* context); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Ok :> sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... sig_handler... ... */ struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(SIGRTMIN, &sa, NULL) == -1) exit_sys("sigaction"); struct sigevent se; se.sigev_notify = SIGEV_SIGNAL; se.sigev_signo = SIGRTMIN; se.sigev_value.sival_int = 31; timer_t it; if (timer_create(CLOCK_MONOTONIC, &se, &it) == -1) exit_sys("timer_create"); struct itimerspec ts; ts.it_value.tv_sec = 5; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; if (timer_settime(it, 0, &ts, NULL) == -1) exit_sys("timer_settime"); puts("Ok"); for (;;) pause(); return 0; } void sig_handler(int signo, siginfo_t* info, void* context) { printf("sig_handler...\n"); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte oluşturulan "Interval Timer" nesnesi, zaten program sonlanacağı için, yok edilmemiştir. #include #include #include #include #include #include void sig_handler(int signo, siginfo_t* info, void* context); void exit_sys(const char* msg); jmp_buf g_jumper; int g_counter; int main(void) { /* # OUTPUT # Starting Continues :> sig_handler... Continues sig_handler... Continues sig_handler... Continues sig_handler... Continues sig_handler... Continues sig_handler... Continues sig_handler... Continues sig_handler... Continues sig_handler... Continues sig_handler... Continues sig_handler... Finished */ struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(SIGRTMIN, &sa, NULL) == -1) exit_sys("sigaction"); struct sigevent se; se.sigev_notify = SIGEV_SIGNAL; se.sigev_signo = SIGRTMIN; se.sigev_value.sival_int = 31; timer_t it; if (timer_create(CLOCK_MONOTONIC, &se, &it) == -1) exit_sys("timer_create"); struct itimerspec ts; ts.it_value.tv_sec = 5; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; if (timer_settime(it, 0, &ts, NULL) == -1) exit_sys("timer_settime"); puts("Starting"); for (;;) { if (setjmp(g_jumper) == 1) { break; } else { puts("Continues"); pause(); } } puts("Finished"); return 0; } void sig_handler(int signo, siginfo_t* info, void* context) { printf("sig_handler...\n"); if (g_counter++ == 10) { longjmp(g_jumper, 1); } } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıdaki örnekte ise hem sinyalle birlikte bir bilgi iliştirilmiş hem de ilgili nesne de yok edilmiştir. #include #include #include #include #include #include void sig_handler(int signo, siginfo_t* info, void* context); void exit_sys(const char* msg); jmp_buf g_jumper; int g_counter; int main(void) { /* # OUTPUT # Starting Continues :> [31] : sig_handler Continues [31] : sig_handler Continues [31] : sig_handler Continues [31] : sig_handler Continues [31] : sig_handler Continues [31] : sig_handler Continues [31] : sig_handler Continues [31] : sig_handler Continues [31] : sig_handler Continues [31] : sig_handler Continues Finished */ struct sigaction sa; sa.sa_sigaction = sig_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(SIGRTMIN, &sa, NULL) == -1) exit_sys("sigaction"); struct sigevent se; se.sigev_notify = SIGEV_SIGNAL; se.sigev_signo = SIGRTMIN; se.sigev_value.sival_int = 31; timer_t it; if (timer_create(CLOCK_MONOTONIC, &se, &it) == -1) exit_sys("timer_create"); struct itimerspec ts; ts.it_value.tv_sec = 5; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; if (timer_settime(it, 0, &ts, NULL) == -1) exit_sys("timer_settime"); puts("Starting"); for (;;) { if (setjmp(g_jumper) == 1) { break; } else { puts("Continues"); pause(); } } timer_delete(it); puts("Finished"); return 0; } void sig_handler(int signo, siginfo_t* info, void* context) { if (g_counter++ == 10) { longjmp(g_jumper, 1); } printf("[%d] : sig_handler\n", info->si_int); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Aşağıda da "thread" yoluyla haberdar edilme yapılmıştır. #include #include #include #include #include #include void thread_handler(union sigval sval); void exit_sys(const char* msg); int main(void) { /* # OUTPUT # Press ENTER to exit!... :> [69] : thread_handler... [69] : thread_handler... [69] : thread_handler... [69] : thread_handler... [69] : thread_handler... [69] : thread_handler... ... Finished */ struct sigevent se; se.sigev_notify = SIGEV_THREAD; se.sigev_notify_function = thread_handler; se.sigev_notify_attributes = NULL; se.sigev_value.sival_int = 69; timer_t it; if (timer_create(CLOCK_MONOTONIC, &se, &it) == -1) exit_sys("timer_create"); struct itimerspec ts; ts.it_value.tv_sec = 5; ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; ts.it_interval.tv_nsec = 0; if (timer_settime(it, 0, &ts, NULL) == -1) exit_sys("timer_settime"); printf("Press ENTER to exit!...\n"); getchar(); timer_delete(it); puts("Finished"); return 0; } void thread_handler(union sigval sval) { printf("[%d] : thread_handler...\n", sval.sival_int); } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > Sistemle İlgili Parametrik Değerlerin Elde Edilmesi, Kullanılması: UNIX türevi sistemlerde, sistemden sisteme değişebilecek, birden çok parametrik değer bulunmaktadır. Örneğin, yol ifadelerinin uzunluğu, oluşturulabilecek alt proseslerin toplam adedi, "exec" fonksiyonlarına geçilebilecek komut satırı argümanlarının sayısı, ek grupların ("supplementary groups") adedi ki bu konuya ileride değineceğiz, bir prosesin açık tutabileceği maksimum dosya sayısı gibi pek çok parametre sistemden sisteme değişmektedir. İşte taşınabilir bir program yapabilmek için iş bu parametrik değerlerin o sistemdeki gerçek değerlerinin biliniyor olması gerekmektedir. Genel olarak sistemdeki parametrik değerlerin tanımlandığı temel dosya "limits.h" isimli dosyadır. Bu dosyayı incelediğimizde değerlerin şu gruplara ayrıldığı görülür: "Minimum Values", "Runtime Invariant Values (Possibly Indeterminate)", "Pathname Variable Values", "Runtime Increasable Values", "Numerical Limits", ... Bu gruplardan, >> "Minimum Values" : Bir parametrik değerin, o sistemde olabilecek en küçük değerinin ne olacağı, bu gruptaki sembolik sabitlerle belirlenmiştir. Bu sembolik sabitlerin isimleri genellikle "_MAX" son ekine, "_POSIX_" ön ekine sahiptir. Her ne kadar isim içerisinde "MAX" kullanılmışsa da esasında o sistemde karşılık geldiği değerin, standartta belirtilen değerden fazla olması gerekmektedir. Örneğin, -> Bir prosesin açık tutabileceği MAKSİMUM dosya sayısı "_POSIX_OPEN_MAX" sembolik sabitiyle belirtilmiştir ve değeri "20" dir. İşte o sistemde "_POSIX_OPEN_MAX" sembolik sabitinin karşılığı olan sayı, MİNİMUM "20" olmalıdır. -> Bir kullanıcının oluşturabileceği MAKSİMUM alt proses sayısı ise "_POSIX_CHILD_MAX" sembolik sabitiyle belirtilmiştir ve değeri "25" tir. İşte o sistemde "_POSIX_CHILD_MAX" sembolik sabitinin karşılığı olan sayı, MİNİMUM "25" olmalıdır. -> "_POSIX_ARG_MAX" sembolik sabiti ise "exec" fonksiyonlarında kullanılabilecek komut satırı argümanı ve çevre değişkenlerinin MAKSİMUM "byte" uzunluğuna ilişkindir ve karşılığı olan sayı MİNİMUM "4096" olmalıdır. -> ... Örneklerde de görüldüğü üzere isminde "MAX" olması, sadece okunabilirliği arttırmaya yöneliktir. "Minimum Values" kategorisinde olması, hedef sistemde karşılık geldiği değerin POSIX standardında belirtilen taban değerden fazla olması gerektiği içindir. Yani bir işletim sistemi yazmaya kalkarsak "_POSIX_OPEN_MAX" sembolik sabitinin değerini EN AZ "20", "_POSIX_CHILD_MAX" için EN AZ "25" ve "_POSIX_ARG_MAX" için EN AZ "4096" olacak şekilde tanımlamalıyız. Diğer yandan bu gruptaki sembolik sabitlerin karşılığı olan sayılar esasında çok küçük sayılardır. Ancak modern sistemler dikkate alındığında, karşılık geldiği değerler daha büyüktür. Dolayısıyla bu sembolik sabitlerin önemi de DÜŞÜKTÜR (Yani pek bir işe yaramamaktadır). O halde programcının bu sembolik sabitlere değil, o sistemdeki değerine ihtiyacı vardır. >> "Runtime Invariant Values (Possibly Indeterminate)" : İşte bu kategori, "Minimum Values" kategorisindeki "_POSIX_" ön eki ve "_MAX" son ekine sahip değerlerin o sistemdeki gerçek değerlerine ilişkindir. Bu kategorideki sembolik sabitler "_POSIX_" ön ekine sahip DEĞİLDİR. Örneğin, -> "CHILD_MAX" sembolik sabiti, "_POSIX_CHILD_MAX" sembolik sabitinin o sistemdeki gerçek değeridir. -> "OPEN_MAX" sembolik sabiti, "_POSIX_OPEN_MAX" sembolik sabitinin o sistemdeki gerçek değeridir. -> ... Ancak bu gruptaki bazı sembolik sabitler, yukarıdakiler gibi değildir. Yani bazı parametrik değişkenlerin değerleri sistemin çalışması esnasında değiştirilebilir, bazı parametrelerin sınır değeri de olmayabilir veya sistemin o anki kaynaklarına bağlı olabilir. Dolayısıyla böylesi parametrelerin değerlerini baştan belirlemek mümkün olmayabilir. İşte böylesi parametreler için POSIX standartları, böylesi sembolik sabitler için, o sistemde bir sembolik sabitin "limits.h" içerisinde TANIMLANMIŞ OLMASINI GEREKTİRMEMEKTEDİR. Dolayısıyla bu gruptaki sembolik sabitler ancak tanımlı ise kullanabiliriz. Pekiyi bu durumda ne yapmalıyız? Burada programcı "#ifdef" ile ilgili sembolik sabitin o sistemde tanımlı olup olmadığını sorgulamalıdır. Eğer tanımlıysa kullanmalı, tanımlı değilse "sysconf", "pathconf" ve "fpathconf" fonksiyonlarına başvurmalıdır. Tabii "#ifdef" yapmadan da iş bu fonksiyonlar çağrılabilir. Burada "#ifdef" yapma amacımız gereksiz yere fonksiyon çağrılmamasını sağlamaktır. >> "Pathname Variable Values" : Bu kategoride belirtilen sembolik sabitlerin değerleri o anda kullanılan dosya sistemine bağlı olarak değişebilmektedir. Çünkü UNIX ve türevi sistemlerde farklı dosya sistemleri farklı noktalara "mount" edilebilmekte, dosya sistemlerinin özellikleri de birbirinden farklılık olabilmektedir. Dolayısıyla bu tür özellikler bu kategori altında toplanmıştır. Örneğin, -> "NAME_MAX" : Bir dizin girişinin isminin maksimum karakter sayıdır. "null" karakter dahil değildir. -> "PATH_MAX" : Mutlak bir yol ifadesinin alabileceği maksimum karakter sayısıdır. "null" karakter dahildir. -> ... Diğer yandan bu kategorideki sembolik sabitler, POSIX standartları gereği tüm dizin ağacı içerisinde sabitse, tanımlı olması ancak sabit değilse tanımlı olmaması gerekmektedir. Yani tanımlı olanları doğrudan kullanabiliriz. Aksi halde bu bilgileri "pathconf", "fpathconf" fonksiyonlarına başvurmalıyız. Dolayısıyla burada yine ilk önce "#ifdef" yapmalı, tanımlı değilse iş bu fonksiyonları çağırmalıyız. >> "Runtime Increasable Values" : Bu gruptaki sembolik sabitler sistemden sisteme değişebilir ve değerleri çalışma zamanında arttırılabilir. Ancak bu sembolik sabitlerin ilgili sistemdeki MİNİMUM değerleri tanımlı OLMALIDIR. Zaten bu MİNİMUM değerler "Minimal Values" kategorisi altında "_POSIX_" ile başlayan ve "_MAX" ile biten sembolik sabitlerdir. Gerçek değerleri programın çalışma zamanında "sysconf" fonksiyonu ile elde edilebilir. Örneğin, -> "NGROUPS_MAX" : Maksimum "supplementary group" adedi. -> ... >> "Numerical Limits" : Bu gruptaki sembolik sabitler "primitive" türlerin o sistemdeki MAKSİMUM ve MİNİMUM değerlerini belirtmektedir. -> "INT_MIN" : "int" türünün o sistemdeki en düşük değerini belirtir. -> "INT_MAX" : "int" türünün o sistemdeki en büyük değerini belirtir. Şimdi de yukarıda bahsedilen fonksiyonlara değinelim: >> "sysconf" : Yukarıda da belirtildiği üzere bu fonksiyon, ilgili sembolik sabitlerin çalışma zamanındaki değerlerini elde etmek için kullanılır. Fonksiyonun prototipi aşağıdaki gibidir. #include long sysconf(int name); Fonksiyon parametre olarak gerçek değerini öğrenmek istediğimiz sembolik sabiti alır. Ancak sembolik sabitleri kullanırken "_POSIX_" ön ekine sahip sabitler için, bu ön ek yerine, "_SC_" ön eki getirilir. Diğer sembolik sabitler için direkt "_SC_" ön eki getirilir. Örneğin, -> _POSIX_MESSAGE_PASSING => _SC_MESSAGE_PASSING -> LOGIN_NAME_MAX => _SC_LOGIN_NAME_MAX -> ... Fonksiyon başarı durumunda ilgili sembolik sabite, hata durumunda ise "-1" değerine geri döner ve "errno" değişkeni uygun değere çekilir. Ancak bu fonksiyon, eğer ilgili sembolik sabit için bir sınır belirlenmemişse(örneğin, sınırsız olması), yine başarısız olur ve "-1" ile geri döner fakat "errno" değişkenine dokunulmaz. Dolayısıyla "errno" değişkenini baştan sıfıra çekip, bu fonksiyon çağrısı sonrasında kontrol etmeliyiz. Eğer hala "0" ise ilgili sembolik sabit için bir sınır tayin edilmemiş demektir. * Örnek 1, Aşağıdaki örnekte "OPEN_MAX" sembolik sabitinin gerçek değeri elde edilmiştir. #include #include #include #include #include int main(void) { /* # OUTPUT # 175 */ long result; #ifdef OPEN_MAX result = OPEN_MAX; printf("It is defined: "); #else errno = 0; if ((result = sysconf(_SC_OPEN_MAX)) == -1) { if (errno == 0) fprintf(stderr, "Infinite Value...\n"); else perror("sysconf"); exit(EXIT_FAILURE); } #endif printf("%ld\n", result); return 0; } >> "pathconf" ve "fpathconf" : Fonksiyonların prototipleri aşağıdaki gibidir. #include long pathconf(const char *path, int name); long fpathconf(int fildes, int name); Fonksiyonların ikinci parametreleri gerçek değerini öğrenmek istediğimiz sembolik sabiti alır. "sysconf" fonksiyonu için "_SC_" ön ekini yazıyorken, bu fonksiyonlar için "_PC_" ön ekini yazıyoruz. Örneğin, -> _POSIX_NO_TRUNC => _PC_NO_TRUNC -> PIPE_BUF => _PC_PIPE_BUF -> ... Fonksiyonların birinci parametreleri ise sırasıyla yol ifadesi("pathconf") ve açık dosya betimleyicisi("fpathconf") biçimindedir. Fonksiyon başarı durumunda ilgili sembolik sabite, hata durumunda ise "-1" değerine geri döner ve "errno" değişkeni uygun değere çekilir. Ancak bu fonksiyon, eğer ilgili sembolik sabit için bir sınır belirlenmemişse(örneğin, sınırsız olması), yine başarısız olur ve "-1" ile geri döner fakat "errno" değişkenine dokunulmaz. Dolayısıyla "errno" değişkenini baştan sıfıra çekip, bu fonksiyon çağrısı sonrasında kontrol etmeliyiz. Eğer hala "0" ise ilgili sembolik sabit için bir sınır tayin edilmemiş demektir. * Örnek 1, Aşağıda kök dizinden itibaren, yani kök dizine göreli biçimde, yol ifadesinin alabileceği maksimum uzunluğu temin ettik. Dolayısıyla mutlak yol ifadesi için gereken karakter sayısı, burada elde edilenden bir fazla olmalıdır. Çünkü bu fonksiyonların verdikleri değer, bizim onlara geçtiğimiz dizine görelidir. Tabii Linux sistemlerinde bu sembolik sabit tanımlı olduğundan, bu fonksiyonların bu amaçla çağrılmasına gerek yoktur. #include #include #include #include #include int main(void) { /* # OUTPUT # 4096 */ long result; errno = 0; if ((result = pathconf("/", _PC_PATH_MAX)) == -1) { if (errno == 0) fprintf(stderr, "Infinite Value...\n"); else perror("sysconf"); exit(EXIT_FAILURE); } printf("%ld\n", result); return 0; } * Örnek 2, Aşağıda ise "#ifdef" çağrısı ile ilgili sembolik sabitin tanımlı olup olmadığı test edilmiştir. #include #include #include #include #include int main(void) { /* # OUTPUT # It is defined: 4096 */ long result; #ifdef PATH_MAX result = PATH_MAX; printf("It is defined: "); #else errno = 0; if ((result = pathconf("/", _PC_PATH_MAX)) == -1) { if (errno == 0) fprintf(stderr, "Infinite Value...\n"); else perror("sysconf"); exit(EXIT_FAILURE); } #endif printf("%ld\n", result); return 0; } İşte bu fonksiyonları da aşağıdaki gibi "#ifdef" kullanarak çağırabiliriz; * Örnek 1, #include #include #include #include #include #define MAX_PATH_TEMP 4096 #ifdef MAX_PATH static long g_max_path = MAX_PATH; #else static long g_max_path = 0; #endif int path_max(void) { if (!g_max_path) { long result; errno = 0; if ((result = pathconf("/", _PC_PATH_MAX)) == -1) { if (errno == 0) g_max_path = MAX_PATH_TEMP; else { perror("sysconf"); exit(EXIT_FAILURE); } } else g_max_path = result + 1; } return g_max_path; } int main(void) { /* # OUTPUT # Started Continues Ended */ puts("Started"); char* path; if ((path = (char*)malloc(path_max())) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } puts("Continues"); free(path); puts("Ended"); return 0; } > Proses Limitlerinin Elde Edilmesi: İşletim sistemini bir kaynak yöneticisi olarak düşünebiliriz. Prosesler ise işletim sistemi tarafından sunulan çeşitli kaynakları kullanmaktadır. Yani işletim sistemi bir prosesin kullanabileceği kaynağı sınırlandırmaktadır. Aksi halde bir prosesin kullanacağı kaynak diğer proses(ler)in kaynaklara erişimini kısıtlayabilirdi. Bir prosesin kaynak limit bilgileri yine o prosesin kontrol bloğu içerisinde yer alır. UNIX türevi işletim sistemleri, her kaynak için bir "soft-limit" ve bir "hard-limit" değeri tutmaktadır. Kontroller sırasında "soft-limit" dikkate alınır. "soft-limit" değerine ulaşan proses başarısız olur. Diğer yandan herhangi bir proses "soft-limit" değerini yükseltebilir, ancak "hard-limit" değerine kadar. Hakeza "hard-limit" değerini de düşürebilirler ancak düşürdükten sonra tekrardan yükseltemezler, eski değerine bile. Yalnızca uygun önceliğe sahip prosesler iş bu "hard-limit" değerini yükseltebilir ve/veya düşürebilirler. Peki bir prosesin kaynakları nelerdir, bu kaynaklar nasıl "get" edilir ve "set" edilir? Kaynaklar "" başlık dosyası içerisinde, "RLIMIT" ön ekiyle birlite tanımlanmışlardır. Şöyleki; "RLIMIT_STACK", "RLIMIT_NOFILE", "RLIMIT_AS", "RLIMIT_CORE", "RLIMIT_CPU", "RLIMIT_DATA", "RLIMIT_FSIZE", ... Bu kaynaklardan, -> "RLIMIT_STACK" : Bir "thread" in "stack" uzunluğunu(büyüklüğünü) sınırlandırmak için kullanılır. -> "RLIMIT_NOFILE" : Prosesin dosya betimleyici tablosunun uzunluğunu belirtmektedir. Bu limitin "soft-limit" değerini değiştirdiğimizde, kendi prosesimizinkini büyütmüş olacağız. Artık "fork" sonucu hayata gelen proseslerinki de bu değişiklikten ETKİLENECEKTİR. -> "RLIMIT_AS" : Bir prosesin kullanabileceği maksimum sanal bellek miktarını belirler. -> "RLIMIT_CORE" : Bu limiti daha önce "core" dosyalarında da görmüştük, "core" dosyalarının maksimum uzunluğunu belirtir. -> "RLIMIT_CPU" : Prosesin harcayacağı CPU zamanını sınırlandırmak için bulundurulmuştur. Eğer burada belirtilen zaman aşılırsa, "SIGXCPU" sinyali gönderilir. Bu sinyalin varsayılan davranışı prosesin sonlandırılması yönündedir. -> "RLIMIT_DATA" : Prosesin "data segment" kısmını belirler. "malloc" gibi tahsilat fonksiyonları bu "data segment" kısmını büyüttüğü için, bu limit değerinden de etkilenmektedir. -> "RLIMIT_FSIZE" : Prosesin toplamda oluşturabileceği dosya uzunluğudur(büyüklüğüdür). Eğer limit 5 MB ise o proses toplamda ancak 5 MB büyüklüğünde dosya oluşturabilir. Bu kaynakları "get" etmek için "getrlimit", "set" etmek için "setrlimit" isimli POSIX fonksiyonları kullanılır. >> "getrlimit" ve "setrlimit" : Fonksiyonlardan "getrlimit" fonksiyonu "get", "setrlimit" ise "set" amacıyla kullanılır. Fonksiyonların prototipleri aşağıdaki gibidir. #include int getrlimit(int resource, struct rlimit *rlp); int setrlimit(int resource, const struct rlimit *rlp); Fonksiyonların birinci parametreleri kaynağın türünü, ikinci parametreleri ise kaynak bilgilerine ilişkin "rlimit" yapı türünden bir adrestir. Bu yapı aşağıdaki gibi tanımlanmıştır; struct rlimit { rlim_t rlim_cur; // The current (soft) limit. rlim_t rlim_max; // The hard limit. }; Bu yapının elemanlarının türü olan "rlim_t" işaretsiz bir tam sayı türüdür. Fonksiyonlar başarı durumunda "0", hata durumunda "-1" ile geri döner ve "errno" değişkenini uygun değere çekerler. Fonksiyonlardan "getrlimit" isimli olanın ikinci parametresinin "const" olmadığına, yani bu parametreye geçtiğimiz adrese bilgilerin yazılacağını; "setrlimit" isimli olanın ise "const" olduğuna, yani bu parametreye geçtiğimiz adresteki bilgilerle "set" işlemi yapacağına dikkat ediniz. "getrlimit" fonksiyonu ile elde edilmek istenen limitler; -> Sınırsız olması halinde "rlimit" yapısının "rlim_cur" ve "rlim_max" elemanlarına "RLIM_INFINITY" özel değeri yerleştirilir. -> Sistemimizde tanımsız olabilir veya elde edilme imkanı olmayabilir. Bu durumda "rlimit" yapısının "rlim_cur" elemaına "RLIM_SAVED_CUR", "rlim_max" elemanına ise "RLIM_SAVED_MAX" değeri yerleştirilir. Aşağıda fonksiyonların kullanımına ilişkin örnekler verilmiştir. * Örnek 1, #include #include #include #include int main(void) { /* # OUTPUT # soft limit: [8388608] hard limit: infinite */ struct rlimit rl; if (getrlimit(RLIMIT_STACK, &rl) == -1) { perror("getrlimit"); exit(EXIT_FAILURE); } if (rl.rlim_cur == RLIM_INFINITY) printf("sotf limit: infinite\n"); else if (rl.rlim_cur == RLIM_SAVED_CUR) printf("sotf limit: unspecified\n"); else printf("soft limit: [%ju]\n", (uintmax_t)rl.rlim_cur); if (rl.rlim_max == RLIM_INFINITY) printf("hard limit: infinite\n"); else if (rl.rlim_max == RLIM_SAVED_MAX) printf("hard limit: unspecified\n"); else printf("hard limit: [%ju]\n", (uintmax_t)rl.rlim_max); return 0; } * Örnek 2, #include #include #include #include int main(void) { /* # OUTPUT # soft limit: [10000000] hard limit: infinite */ struct rlimit rl; rl.rlim_cur = 10000000; rl.rlim_max = RLIM_SAVED_MAX; if (setrlimit(RLIMIT_STACK, &rl) == -1) { perror("getrlimit"); exit(EXIT_FAILURE); } if (getrlimit(RLIMIT_STACK, &rl) == -1) { perror("getrlimit"); exit(EXIT_FAILURE); } if (rl.rlim_cur == RLIM_INFINITY) printf("sotf limit: infinite\n"); else if (rl.rlim_cur == RLIM_SAVED_CUR) printf("sotf limit: unspecified\n"); else printf("soft limit: [%ju]\n", (uintmax_t)rl.rlim_cur); if (rl.rlim_max == RLIM_INFINITY) printf("hard limit: infinite\n"); else if (rl.rlim_max == RLIM_SAVED_MAX) printf("hard limit: unspecified\n"); else printf("hard limit: [%ju]\n", (uintmax_t)rl.rlim_max); return 0; } * Örnek 3, #include #include #include #include int main(void) { /* # OUTPUT # soft limit: [175] hard limit: [175] */ struct rlimit rl; if (getrlimit(RLIMIT_NOFILE, &rl) == -1) { perror("getrlimit"); exit(EXIT_FAILURE); } if (rl.rlim_cur == RLIM_INFINITY) printf("sotf limit: infinite\n"); else if (rl.rlim_cur == RLIM_SAVED_CUR) printf("sotf limit: unspecified\n"); else printf("soft limit: [%ju]\n", (uintmax_t)rl.rlim_cur); if (rl.rlim_max == RLIM_INFINITY) printf("hard limit: infinite\n"); else if (rl.rlim_max == RLIM_SAVED_MAX) printf("hard limit: unspecified\n"); else printf("hard limit: [%ju]\n", (uintmax_t)rl.rlim_max); return 0; } Son olarak bir prosesin kaynak kullanımını elde etmek için "getrusage" isimli POSIX fonksiyonu da bulundurulmuştur. >> "getrusage" : Fonksiyonun prototipi aşağıdaki gibidir. #include int getrusage(int who, struct rusage *r_usage); Fonksiyonun birinci parametresi kaynak bilgilerinin elde edileceği prosesi, ikinci parametresi ise bu kaynak bilgilerinin yerleştirileceği yapı nesnesinin adresini almaktadır. "rusage" yapısı aşağıdaki gibi tanımlanmıştır: struct rusage { struct timeval ru_utime; // Prosesin "user-mode" da çalışma zamanı. struct timeval ru_stime; // Prosesin "kernel-mode" da çalışma zamanı. }; Fonksiyon başarı durumunda "0", hata durumunda "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. Diğer yandan bir özelliğin "hard-limit" değeri onun "soft-limit" değerinden de aşağısına çekilememektedir. Öte yandan burada şu noktaya dikkat etmeliyiz; prosesin limit bilgileri de üst prosesten alt prosese aktarılmaktadır. Örneğin, bir kullanıcının hayata getirebileceği maksimum proses adedi "RLIMIT_NPROC" sembolik sabitiyle ifade edilir. Bu değeri değiştirdiğimiz zaman artık prosesimiz ve bu prosesten türetilen diğer proseslerimiz bu değişiklikten etkilenecektir. Fakat bizim başka proseslerimiz bundan etkilenmeyecektir çünkü değişiklik onların kontrol bloğunda gerçekleşmemiştir. Yani sistem geneli değil, o proses ve alt proseslerinde değişiklik geçerlidir. Yani "shell" programının kaynak limitleri arttırılırsa, o "shell" programı ile çalıştırılan bütün programlara bu sirayet edecektir. Eğer ikinci bir "shell" programını çalıştırırsak ve bu ikinci "shell" ile başka program çalıştırırsak, varsayılan limit değerleri kullanılacaktır. Eğer "shell" programını "root" olarak çalıştırırsak, artık "hard-limit" değerini de arttırabiliriz. > "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. }; -> "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. > Alternatif(İleri) "IO" Modelleri: Şimdiye kadar "read" ve "write" fonksiyonları ile klasik "IO" işlemleri yaptık. Halbuki UNIX türevi sistemlerde, bloke oluştuğunda kullanılabilecek, alternatif "IO" modelleri de mevcuttur. Bunlara "İleri IO(Advanced IO)" modelleri denir. Bu modellere şu tip senaryoda ihtiyaç duyulmaktadır; -> "client-server" bir uygulama yazmak isteyelim. "server" programı, "n" tane "client" programlarından gelen istekleri okusun ve onlara yanıt göndersin. Haberleşme için isimli borular("named pipes") ya da soketler("sockets") kullanılsın. Ancak bu yöntemi kullandığımız zaman, okuma esnasında boruda ya da sokette hiç veri yoksa, bloke oluşacaktır. Örneğin, for (;;) { for(int i = 0; i < N; ++i) { read(...); write(...); } } şeklindeki kontrol sırasında, eğer hiç veri yoksa, bloke oluşacaktır. Dolayısıyla diğer "client" lardan gelen bilgileri göremeyeceğiz. Bu neden dolayı bu döngüyü aşağıdaki gibi güncelleyebiliriz. for (;;) { for(int i = 0; i < N; ++i) { result = read(...); if (result == -1 && errno == EAGAIN) continue; write(...); } } Fakat bu şekildeki yaklaşımda da işlemci sürekli olarak bir döngü içerisinde dönmektedir. Yani "busy-loop" oluşacaktır. Bu problemi gidermek için de "thread" ya da "process" oluşturabiliriz. Böylece her bir "client" için ayrı bir "thread" ya da "process" oluşturulur. O "client" ın istekleri o "thread" ya da "process" tarafından sağlanır. Böylece veri gelmediği zaman sadece o "thread" ya da "process" bloke olacaktır. Tabii "process" oluşturmak daha maliyetli olduğundan, "thread" oluşturmak daha mantıklı gelebilir. Ancak yaklaşım ise ölçeklenebilir("scalable") DEĞİLDİR. Yani "client" sayısı arttıkça çok daha fazla sistem kaynağı kullanılmaya başlanacağından, bazı olumsuzluklar ortaya çıkabilir. İşte "Advanced IO" işlemler problemleri çözmesi için geliştirilmiştir. POSIX sistemlerinde "Advanced IO" işlemler dört ana bölümde incelenir. Bunlar "Multiplexed IO", "Signal Driven IO", "Asynchronous IO" ve "Scatter-Gather IO" modülleridir. Bu modellerden, >> "Multiplexed IO" : Bu modelde bir grup betimleyici izlemeye alınır. Bu betimleyicilerde ilgilenilen olay, "read", "write" veya "error", gerçekleşmemişse blokeye yol açar. Ta ki bu betimleyicilerden en az birinde beklenen olay gerçekleşene kadar. Bu durumda bloke çözülür. "Multiplexed IO" için "select" ve "poll" POSIX fonksiyonları kullanılmaktadır. >>> "select" : Bu fonksiyonu bir grup betimleyiciyi takip eder. Eğer beklenen aksiyon takip ettiği betimleyicilerin hiç birisinde yoksa, blokeye yol açar. Ancak en az bir tanesinde beklenen olay gerçekleşirse bloke çözülür. En kaba haliyle bu şekilde çalışır. Fonksiyon tasarımında bazı kusurlar bulunmasına rağmen, en çok kullanılan "Advanced IO" fonksiyonlarındandır. Fonksiyonun prototipi aşağıdaki gibidir. #include int select(int nfds, fd_set * readfds, fd_set * writefds, fd_set * errorfds, struct timeval * timeout); Fonksiyonun parametrelerindeki "fd_set" türü aslında bir "bit" dizisi belirtir ve her bir öğesi bir betimleyiciye karşılık gelir. Bu dizinin, -> Belli bir indeksindeki öğenin değerini "1" yapmak için "FD_SET" -> Belli bir indeksindeki öğenin değerini "0" yapmak için "FD_CLR" -> Bütün öğelerini "0" yapmak için "FD_ZERO" -> Belli bir öğesinin değerini sınamak için "FD_ISSET" makroları kullanılır. Fonksiyonun ikinci parametresine, üçüncü ve dördüncü parametrelerine sırasıyla "read", "write" ve "error/exception" amacıyla izlenecek betimleyicilerin kümesini, ilgili "bit" dizisini doldurduktan sonra, geçeriz. Böylelikle hangi betimleyicileri ne amaçla izleyeceğimizi belirlemiş oluruz. Bu parametrelere "NULL" değeri de geçilebilir, bu durumda ilgili izlemenein yapılmayacağını belirtir. Fonksiyonun birinci parametresi ise yukarıdaki üç kümedeki en yüksek betimleyicinin bir fazla değerini almaktadır. Aslında bu parametre işlemleri hızlandırmak için düşünülmüştür. Olabilecek en yüksek betimleyici değeri ise "FD_SETSIZE" sembolik sabitiyle tanımlanmıştır. Örneğin, "18", "23" ve "47" numaralı betimleyicileri çeşitli sebeplerle izlemek istiyoruz. İşte fonksiyonun birinci parametresine "48" değerini geçeriz. Ancak buraya "FD_SETSIZE" sembolik sabiti geçilse bile bir sorun oluşmayacaktır. Fonksiyonun son parametresi ise bir zaman aşımı belirtir. Zaman aşımı ise en kötü durumda blokenin ne kadar süreceğini belirtir. Örneğin, bir grup betimleyiciyi izlemek isteyelim fakat blokenin maksimum 10 saniye sürmesini, sonrasında çözülmesini isteyelim. İşte bunun için bu parametreyi kullanacağız. Bu parametrede kullanılan "timeval" yapısı ise aşağıdaki biçimde tanımlıdır. struct timeval { time_t tv_sec; // Seconds. suseconds_t tv_usec; // Microseconds. }; Bu yapı mikrosaniye çözünürlüğünde, bir zaman aralığı belirtmek için, kullanılır. Yapının her iki elemanına da "0" değeri geçilebilir, bu durumda "select" fonksiyonu işlevini hemen gerçekleştirip geri döner. Fonksiyonun ikinci, üçüncü, dördüncü ve beşinci parametrelerine "NULL" değeri geçilebilir. Bu durumda o işlev çalıştırılmaz. Fonksiyonun geri dönüş değeri ise şöyledir; -> Başarısızlık durumunda "-1" değerine döner. -> Hiç bir betimleyicide beklenen olay gerçekleşmemiş fakat zaman aşımı dolmuşsa "0" değerine geri döner. -> En az bir betimleyicide beklenen olay gerçekleşmişse, toplam gerçekleşen olay sayısını geri döndürür. Buradaki kritik nokta betimleyici adedi değil, gerçekleşen olayların adedidir. Ancak bu fonksiyonun geri dönüş değeri pek kontrol edilmez. Bu fonksiyonun detaylı kullanımı ise aşağıdaki gibidir: -> Bir grup betimleyiciyi "read" amacıyla izlemek isteyelim. Bunun için ilk olarak ilgili betimleyici kümesini bir "fd_set" nesnesi içerisinde, yukarıdaki makrolar ile, belirtmeli ve bu nesnenin adresini de fonksiyonun ikinci adresine geçmeliyiz. Fonksiyonun birinci parametresine de betimleyici kümesindeki en yüksek betimleyici değerinin bir fazlasını geçmeliyiz. Şöyleki; //... fd_set rset; FD_ZERO(&rset); FD_SET(fdr1, &rset); /* 'fd1 = 18' varsayalım. */ FD_SET(fdr2, &rset); /* 'fd2 = 23' varsayalım. */ FD_SET(fdr3, &rset); /* 'fd3 = 47' varsayalım. */ //... // max_fds = getmax(fd1, fd2, fd3); // 'psudue-code' //... select(max_fds + 1, &rset, NULL, NULL, NULL); Burada "select" fonksiyonu, bizim ona verdiğimiz betimleyicileri "read" amacıyla izler. Eğer ilgili betimleyicilerin hiç birisine bilgi gelmemişse, "select" fonksiyonu akışı bloke eder. Ancak en az birisine bilgi gelmesi durumunda bloke çözülür. Burada "select" fonksiyonunun "read" yapmadığına, sadece "read" yapılabilecek bir durum oluştuğunu tespit ettiğine, DİKKAT EDİNİZ. Tabii programcının, blokenin çözülmesiyle birlikte, hangi betimleyiciye bilgi geldiğini anlaması gerekmektedir. İşte "select" bize bunun bilgisini vermektedir. Dolayısıyla bu fonksiyonu bir kez değil, bir döngü içerisinde(genellikle) kullanılmaktadır. -> Yukarıdaki betimleyicilerin birisinde beklenen olayın gerçekleştiğini varsayalım. Örneğin, "23" numaralı betimleyici. Beklenen olayın gelmesinden dolayı bloke çözümlenecektir ve "select" fonksiyonuna geçtiğimiz "fd_set" nesnesindeki ilgili betimleyiciye karşılık gelen "bit" değeri "set", diğer "bit" değerler ise "reset" edilecektir. Yani "select" fonksiyonuna geçtiğimiz "fd_set" nesnesinin içeriğini bozmakta; beklenen olayın gerçekleştiği betimleyiciye ilişkin "bit" değerini "set", diğer "bit" değerlerini "reset" etmektedir. Dolayısıyla "select" fonksiyonundan çıktıktan sonra hangi "bit" değerlerinde değişiklik yapıldığını tek tek sorgulamalıyız. Böylelikle hangi olayın gerçekleştiğini tespit edebiliriz. Birden fazla "bit" değerinin de "set" edilmesi mümkündür. Bu nedenle kontrolü "if-else" ile değil, ayrık "if" ile yapmalıyız. Şöyleki; //... fd_set rset; FD_ZERO(&rset); FD_SET(fdr1, &rset); /* 'fd1 = 18' varsayalım. */ FD_SET(fdr2, &rset); /* 'fd2 = 23' varsayalım. */ FD_SET(fdr3, &rset); /* 'fd3 = 47' varsayalım. */ //... // max_fds = getmax(fd1, fd2, fd3); // 'psudue-code' //... if (select(max_fds + 1, &rset, NULL, NULL, NULL) == -1) exit_sys("select"); if (FD_ISSET(fdr1, &rset)) { read(frd1, ...); // 'psudue-code' } if (FD_ISSET(fdr2, &rset)) { read(frd2, ...); // 'psudue-code' } if (FD_ISSET(fdr3, &rset)) { read(frd3, ...); // 'psudue-code' } Artık "read" veya "recv" / "recvfrom" fonksiyonları ile okuma yapmamız gerekmektedir. Bu fonksiyonlar sırasyıla "pipe" ve "socket" üzerinden "read" yapan fonksiyonlardır. Bu aşamada "read" fonksiyonları blokeye yol açmayacaktır. -> Eğer çok fazla betimleyici izlenecekse, "select" fonksiyonu çıkışı, onlar için ayrık "if" deyimlerinin yazılması da yorucu gelebilir. Bu durumda iki farklı yol izlenebilir. Bunlardan ilki bütün betimleyicileri bir döngü içerisinde kontrol etmektir. Şöyleki; //... // max_fds = getmax(fd1, fd2, fd3); // 'psudue-code' //... if (select(max_fds + 1, &rset, NULL, NULL, NULL) == -1) exit_sys("select"); for (int i = 0; i <= max_fds; ++i) { if (FD_ISSET(i, &rset)) { read(i, ...); // 'psudue-code' } } Bu yaklaşım ile "rset" nesnesinin "[0, max_fds]" arasındaki "bit" değerlerine bakılmış, "set" edilenler için "read" çağrısı yapılmıştır. İkinci yöntem ise izlenecek betimleyicileri baştan bir diziye yerleştirmektir. Şöyleki; //... int fds[3]; fds[0] = fdr1; /* 'fd1 = 18' varsayalım. */ fds[1] = fdr2; /* 'fd2 = 23' varsayalım. */ fds[2] = fdr3; /* 'fd3 = 47' varsayalım. */ //... fd_set rset; FD_ZERO(&rset); for (int i = 0; i < 3; ++i) { FD_SET(fds[i], &rset); } //... // max_fds = getmax(fds, 3); // 'psudue-code' //... if (select(max_fds + 1, &rset, NULL, NULL, NULL) == -1) exit_sys("select"); for (int i = 0; i < 3; ++i) { if (FD_ISSET(fds[i], &rset)) { read(fds[i], ...); // 'psudue-code' } } Böylece izlenmek istenen betimleyicilere "fds" dizisinden ulaşabiliriz. Ancak "select" fonksiyonunu döngü içerisinde kullanırken ona verdiğimiz betimleyici kümesinin bozulacağını, dolayısıyla döngünün başında onun yeniden yüklenmesi gerektiğine dikkat ediniz. Şöyleki; fd_set rset, orset; //... FD_ZERO(&rset); FD_SET(fd1, &rset); // fd1 = 18 varsayalım FD_SET(fd2, &rset); // fd2 = 23 varsayalım FD_SET(fd3, &rset); // fd3 = 48 varsayalım maxfds = getmax(fd1, fd2, fd3); for (;;) { orset = rset; // Bu iki yapı nesnesi birbirine atanabilir, elemanları karşılıklı olarak birbirine atanacaktır. if (select(maxfds + 1, &orset, NULL, NULL, NULL) == -1) exit_sys("select"); if (FD_ISSET(fd1, &orset)) { read(fd1, ...); } if (FD_ISSET(fd2, &orset)) { read(fd2, ...); } if (FD_ISSET(fd3, &orset)) { read(fd3, ...); } } Böylelikle "select" çıkışında bozulan nesnemiz "orset" olacak, döngünün her başında ise tekrardan orjinal değerlerini kazanacaktır. -> Eğer betimleyici karşı taraf tarafından kapatılırsa, bu durumda "select" fonksiyonu yine okuma yapılmış gibi davranacaktır. Fakat "read" fonksiyonu bu durumda "0" bayt okuyacaktır. Dolayısıyla bizim de kendi tarafımızdan ilgili betimleyiciyi önce kapatmalı, devamında da bunu izleme kümesinden çıkartmalıyız. Şöyleki; if ((result = read(fds[i], buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (result == 0) { close(fds[i]); FD_CLR(fds[i], &rset); } Diğer yandan bu fonksiyonu kullanırken şu noktalara da dikkat etmeliyiz: -> Bu fonksiyonun normal disk dosyaları için çağrılması anlamsızdır çünkü "select" fonksiyonu beklenen olay her zaman gerçekleşmiş gibi davranacaktır. Dolayısıyla uzun süre beklemeye yol açabilecek "shell", "pipe", "socket" gibi aygıtlar için kullanılmalı ve bu aygıtlar da blokeli modda açılmalıdır. -> "select" eşliğinde okuma yapılırken, betimleyicinin karşı tarafça kapatılması durumunda, "select" fonksiyonu sanki "read" işlemi yapılmış gibi davranacaktır. Bu durumda "read" fonksiyonu "0" bayt okuyacaktır. Dolayısıyla ilgili betimleyicinin de takip kümesinden çıkartılması gerekmektedir. -> "select" fonksiyonu ile "write" işlemi için izleme yaparken; normal şartlarda o boruda yer kalmadığında bloke oluşur ancak "select" fonksiyonu ile izleme yaptığımız zaman o boruda yer açıldıkça bloke çözülecektir. Daha sonra o boruda yer varsa, o boruya yazma yapacağız. Tıpkı "read" fonksiyonundaki gibi. Fakat burada şu noktaya dikkat etmeliyiz; "select" fonksiyonu en az bir "byte" kadarlık alan yazmaya müsaitse blokeyi çözmektedir. Fakat "write" ise tüm bilgi boruya yazılana kadar bloke oluşturmaktadır. Tabii okuyan taraf yazılan kadar okuyorsa, sorun oluşmayacaktır. Benzer durum "socket" kavramında da geçerlidir. Ancak "select" fonksiyonunun "write" işlemi için kullanılması seyrek kullanılır. -> "select" fonksiyonunun dördüncü parametresi çok kısıtlı bir kullanım alanına sahiptir, "pipe" mekanizmasında bir etkisi yoktur. "socket" mekanizmasında ise "out of band data" durumunda işlevsellik kazanır. Dolayısıyla önemli bir kullanıma sahip değildir ve genellikle "NULL" değeri geçilir. "out of band data" mevzusuna ileri konularda değinilecektir. -> Anımsanacağı üzere borularda önce yazan tarafın boruyu kapatması gerekmektedir. Eğer okuyan taraf önce kapatırsa ve yazan taraf boruya yazma yaparsa "SIGPIPE" sinyalinin oluşur. "select" fonksiyonunda, "write" için izleme yapılırken, okuyan tarafın boruyu önce kapatması durumunda sanki yazma olayı varmış gibi bir durum oluşur ve bloke çözülür. Eğer boruya yazma işlemi yapılırsa yine "SIGPIPE" sinyali oluşur. Benzer durum "socket" lerde de söz konusudur. -> Eskiden UNIX ve türevi sistemlerde, yüksek çözünürlüklü bekleme yapan "nanosleep" ve "clock_nanosleep" fonksiyonları mevcut değilken, "select" fonksiyonu "sleep" amacıyla kullanılmaktaydı. Bunun için ikinci, üçüncü ve dördüncü parametrelerine "NULL" değeri, beşinci parametresine ise belli bir süre geçilmesi gerekmektedir. Böylece mikrosaniye duyarlılığında "sleep" işlevini gerçekleştirmiş oluruz. -> "FD_SETSIZE" sembolik sabiti Linux sistemlerinde 1024 olarak tanımlanmıştır. Dolayısıyla, prosesimizin dosya betimleyici tablosunun büyüklüğü 1024'ü geçse bile, bizler sadece 1024 adedini takip edebiliriz. Bu problem için çözümler geliştirilmiş olsa da bu çözümler taşınabilir değildir. Şimdi de aşağıdaki açıklayıcı örnekleri inceleyelim: * Örnek 1, #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char* msg); int main(void) { /* Ulya Yuruk Read Value: Ulya Yuruk */ fd_set rset; FD_ZERO(&rset); FD_SET(0, &rset); if ( select(0 + 1, &rset, NULL, NULL, NULL) == -1 ) exit_sys("select"); char buf[BUFFER_SIZE + 1]; ssize_t result; if (FD_ISSET(0, &rset) && (result = read(0, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("Read Value: %s", buf); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char* msg); int main(void) { /* Ulya Yuruk Read Value: Ulya Yuruk :> */ fd_set rset, orset; FD_ZERO(&rset); FD_SET(0, &rset); char buf[BUFFER_SIZE + 1]; ssize_t result; for (;;) { orset = rset; if ( select(0 + 1, &orset, NULL, NULL, NULL) == -1 ) exit_sys("select"); if (FD_ISSET(0, &orset)) { if ((result = read(0, buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (result == 0) break; } buf[result] = '\0'; printf("Read Value: %s", buf); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aşağıda yer alan adımlardan ilk önce "Step - I" adımını, daha sonra "Step - II" adımını takip ediniz. İkinci adımdaki program ilk önce blokede bekleyecektir. Daha sonra "Step - III" adımını takip ediniz. İşte çıktıyı, ikinci adımdaki programın terminalinde, görebiliriz. Daha sonra sırasıyla "Step - IV" ve "Step - V" adımlarını takip ettiğimizde, ikinci adımdaki programın terminalinden çıktılarını görebiliriz. Diğer taraftan bizler ilgili boruyu "O_RDONLY" modunda açıyor, karşı tarafta "O_WRONLY" ya da "O_RDWR" modunda açıyor olsun. Açma işlemi tamam olana dek "open" fonksiyonu blokeye yol açacaktır. İşte bu blokenin oluşmaması için ilkin "mkfifo" komutunu kullandık. Tabii aşağıdaki programdaki "Step - III", "Step - IV" ve "Step - V" in kendi iç sırasını da değiştirerek sonucu gözlemlemeliyiz. Diğer yandan programların sonlandırılması her ne kadar "Ctrl+D" ile yapılmış olsa da "Ctrl+C" ile de yapılabilir. Çünkü bir proses herhangi bir şekilde sonlanırsa, ona ait açık betimleyiciler yine kapatılacaktır. // Step - I: Creation of 'named pipes' using 'mkfifo' via 'shell' program: $ mkfifo x y z $ ls -l prw-r--r-- 1 runner27 runner27 0 Mar 4 03:46 x prw-r--r-- 1 runner27 runner27 0 Mar 4 03:46 y prw-r--r-- 1 runner27 runner27 0 Mar 4 03:46 z // Step - II: Listening the 'named pipes': #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 4096 void exit_sys(const char* msg); int main(int argc, char* argv[]) { /* # Command Line Arguments # x y z */ /* # OUTPUT # Opening named pipes... It may block. // ----->(1) x opened... waiting in select... selami peer closed the descriptor... // ----->(2) y opened... waiting in select... ali peer closed the descriptor... // ----->(3) z opened... waiting in select... ulya peer closed the descriptor... There is no open descriptor, closing the program... */ if (argc == 1) { fprintf(stderr, "too few arguments...\n"); exit(EXIT_FAILURE); } puts("Opening named pipes... It may block."); int fds[MAX_SIZE]; int max_fd = -1; fd_set rset, orset; int count = 0; FD_ZERO(&rset); for (int i = 1; i < argc; ++i) { if ((fds[i] = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); printf("%s is open\n", argv[i]); if (fds[i] > max_fd) max_fd = fds[i]; FD_SET(fds[i], &rset); ++count; } ssize_t result; char buf[BUFFER_SIZE + 1]; for (;;) { printf("waiting in select...\n"); orset = rset; if ( select(max_fd + 1, &orset, NULL, NULL, NULL) == -1 ) exit_sys("select"); for (int i = 0; i <= max_fd; ++i) { if (FD_ISSET(fds[i], &orset)) { if ((result = read(fds[i], buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (result == 0) { printf("peer closed the descriptor...\n"); close(fds[i]); FD_CLR(fds[i], &rset); --count; if (count == 0) goto EXIT; } buf[result] = '\0'; printf("%s\n", buf); } } } EXIT: printf("There is no open descriptor, closing the program...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } // Step - III: Call "cat > x" command on the second 'shell' program: // ----->(1) $ cat > x selami "Ctrl+D" // Step - IV: Call "cat > x" command on the third 'shell' program: // ----->(2) $ cat > y ali "Ctrl+D" // Step - V: Call "cat > x" command on the fourth 'shell' program: // ----->(3) $ cat > z ulya "Ctrl+D" >>> "poll" : "select" fonksiyonunun alternatifi bir fonksiyondur. Her ne kadar aynı amaç için kullanılsalar da "poll" fonksiyonunun imzası ve kullanılış biçimi farklıdır. Bu fonksiyon "select" fonksiyonuna nazaran daha iyi gözükse de kullanımı daha zordur. Fonksiyonun prototipi aşağıdaki gibidir. #include int poll(struct pollfd fds[], nfds_t nfds, int timeout); Fonksiyonun birinci parametresi "pollfd" türünden bir yapı dizisidir. Çünkü "poll" fonksiyonu izlenecek betimleyicileri bir yapı dizisi olarak almaktadır. "struct pollfd" türü aşağıdaki gibi tanımlıdır; struct pollfd { int fd; // İzlenecek betimleyici belirtir. short events; // İzleme amacını belirtir. short revents; // The output event flags (see below). }; -> "fd" isimli elemanı izlenecek betimleyiciyi belirtir. Eğer bu betimleyici değeri negatif herhangi bir değer olarak girilirse o betimleyici için izleme yapılmamaktadır. Dolayısıyla "pollfd" dizisinden bir elemanı mantıksal olarak çıkartmak için bu betimleyici negatif bir değere çekilebilir. -> "events" isimli elemanı ise izleme amacını belirtir. -> "revents" isimli eleman ise, fonksiyon sonlandığında, oluşan olay hakkında bilgi verir. Programcı yapının "fd" ve "events" elemanlarına değer atadıktan sonra "poll" fonksiyonunu çağırmalıdır. Yapının "revents" isimli elemanı ise fonksiyon tarafından "set" edilir. Fonksiyonun ikinci parametresi ise bu yapı dizisinin uzunluk bilgisini alır. Son parametre ise zaman aşımı belirtir ve milisaniye çözünürlülüğe sahiptir. Eğer bu parametreye, -> "-1" değeri geçilirse, zaman aşımı uygulanmaz. -> "0" değeri geçilirse fonksiyon, betimleyicilerin durumuna bakar ve hemen geri döner. "pollfd" yapısının "events" ve "revents" elemanları bir takım sembolik sabitler alırlar. Bunlar, "POLLIN", "POLLRDNORM", "POLLRDBAND", "POLLPRI", "POLLOUT", "POLLWRNORM", "POLLWRBAND", "POLLERR", "POLLHUP" ve "POLLNVAL" sembolik sabitlerinden birisini ya da birkaçını alır. Birkaçını alması durumunda "bit-wise OR" kullanılır. Bunlardan, -> "POLLIN" : Okuma amaçlı izlemeyi belirtir. Boruda ya da sokette okunacak bilgi oluştuğunda fonksiyon tarafından bu bayrak "set" edilmektedir. Soketlerde "accept" yapan tarafta bir bağlantı istedği oluştuğunda da "POLLIN" bayrağı "set" edilmektedir. Aynı zamanda soketlerde karşı taraf soketi kapattığında da "POLLIN" bayrağı "set" edilmektedir. -> "POLLOUT" : Yazma amaçlı izlemeyi belirtir. Boruya ya da sokete yazma durumu oluştuğunda (yani boruda ya da "network" tamponunda yazma için yer açıldığında) fonksiyon tarafından bu bayrak "set" edilmektedir. Aynı zamanda soketlerde karşı taraf soketi kapattığında da "POLLOUT" bayrağı "set" edilmektedir. -> "POLLERR" : Hata amaçlı izlemeyi belirtir. Bu bayrak yapının "events" elemanında "set" edilmez, fonksiyon tarafından yapının "revents" elemanında "set" edilmektedir. Bu bayrak borularda okuma yapan tarafın boruyu kapatmasıyla yazma yapan tarafta "set" edilmektedir. (Normal olarak okuyan tarafın boruyu kapattığı durumda boruya yazma yapıldığında "SIGPIPE" sinyalinin oluştuğunu anımsayınız.) Eğer okuyan taraf boruyu kapattığında boruya yazma için yer varsa yazma yapan tarafta aynı zamanda "POLLOUT" bayrağı da "set" edilmektedir. "POLLERR" bayrağı soketlerde kullanılmamaktadır. -> "POLLHUP" : Boruya yazan tarafın boru betimleyicisini kapattığında okuma yapan tarafta bu bayrak "set" edilmektedir. Bu bayrak yapının "events" elemanında "set" edilmez, fonksiyon tarafından yapının "revents" elemanında "set" edilmektedir. ("HUP", "hang up" anlamına gelmektedir.) Eğer boruya yazma yapan taraf boruyu kapattığında hala boruda okunacak bilgi varsa okuma yapan tarafta aynı zamanda "POLLIN" bayrağı da "set" edilmektedir. "POLLHUP" bayrağı soketlerde kullanılmamaktadır. -> "POLLRDHUP" : Soketlerde karşı taraf soketi kapattığında ya da "shutdown" fonksiyonu "SHUT_WR" argümanıyla çağrıldığında oluşur. -> "POLLNVAL" : Bu bayrak yapının "events" elemanında "set" edilmez. Fonksiyon tarafından eğer izlenen bir betimleyici kapalıysa yapının "revents" elemanında fonksiyon tarafından "set" edilmektedir. Bu sembolik sabitlerin anlamları için "The Linux Programming Interface" kitabından şu tabloları da vermek istiyoruz: -> Boruda bilgi yok ve yazan tarafın betimleyicisi kapalı ===> Okuyan tarafta "POLLHUP" -> Boruda bilgi var ve yazan tarafın betimleyicisi kapalı ===> Okuyan tarafta "POLLIN|POLLHUP" -> Boruda bilgi var ve yazan tarafın betimleyicisi açık ===> Okuyan tarafta "POLLIN" -> Boruda yazma için yer yok ve okuyan tarafın betimleyicisi kapalı ===> Yazan tarafta "POLLERR" -> Boruda yazma için yer var ve okuyan tarafın betimleyicisi kapalı ===> Yazan tarafta "POLLOUT|POLLERR" -> Boruda yazma için yer var ve okuyan tarafın betimleyicisi açık ===> Yazan tarafta "POLLOUT" -> Sokette bilgi var ===> Okuyan tarafta "POLLIN" -> Network tamponunda yazacak yer var ===> Yazan tarafta "POLLOUT" -> accept yapan tarafta bağlantı isteği oluştuğunda ===> accept yapan tarafta "POLLIN" -> Karşı taraf soketi kapattığında ===> karşı tarafta "POLLIN|POLLOUT|POLLRDHUP" "poll" fonksiyonunun geri dönüş değeri başarı durumunda "non-negative" değer döndürür. Eğer bu değer, -> "0" ise zaman aşımından dolayı sonlanmış demektir. Eğer zaman aşımı parametresine "0" geçilmiş ancak yine de hiç bir olay gerçekleşmemişse, yine "0" değeri ile döner. -> Pozitif ise fonksiyonun ilk parametresinde belirtilen dizideki, olayı gerçekleşen, eleman sayısına döner. Toplam olay sayısına dönmez. Ancak fonksiyon hata durumunda "-1" ile geri döner ve "errno" uygun değere çekilir. Diğer yandan bu fonksiyonu kullanırken şu noktalara da dikkat etmeliyiz: -> Fonksiyonu yine bir döngü içerisinde çağırmalı, birinci parametresindeki dizinin her bir elemanını beklenen olaya karşın kontrol etmeliyiz. Bunun için yukarıdaki yapının "revents" elemanını kullanmalıyız. Eğer bu eleman "0" değerinde ise beklenen olay gerçekleşmemiş demektir. Tabii bu kontrollerleri yine ayrık "if" deyimleriyle gerçekleştirmeliyiz. //... struct pollfd pfds[1]; pfds[0].fd = 0; pfds[0].events = POLLIN; // Okuma yapacağımızı belirtiyoruz. //... for (;;) { if (poll(pfds, 1, -1) == -1) exit_sys("poll"); if (pfds[0].revents & POLLIN) { read(pfds[0].fd, ...); // 'psudue-code' //... } } //... -> Borular ve soketler kapatıldığında hem "POLLIN" hem de "POLLHUP" olayları gerçekleşebilir. Dolayısıyla bu olayların takibini ayrık "if" deyimleri ile değil "if-else if" deyimleri ile kontrol edilmeleri daha uygun olur. Çünkü karşı taraf boruya yada sokete bilgi yazıp hemen ardından boruyu yada soketi kapattığında "POLLIN" ve "POLLHUP" birlikte oluşabilir. Bu durumda, ayrık "if" deyimleri kullanılırsa ve "POLLIN" kontrolünde boru yada sokettekilerin hepsi okunmazsa, eksik okuma yapılmış olacağız. Eğer "if-else if" yaparsak, eksik okusak bile döngünün bir sonraki turunda yine "POLLIN" ve "POLLHUP" oluşacağından, okumaya devam edebileceğiz. Ta ki borudakilerin hepsi okunana kadar. Boruda bir şey kalmadığında sadece "POLLHUP" oluşacaktır. Çünkü "POLLHUP" yalnızda bir defa oluşmamakta, kapatılan boru ya da soket üzerinde "poll" çağrısı yapılırsa, tekrar tekrar oluşmaktadır. -> Eğer ilgili betimleyicinin değeri eksi ise "poll" fonksiyonu o betimleyici ile ilgilenmeyecektir. Örneğin, "-1" değerini atayabiliriz. Şimdi de aşağıdaki açıklayıcı örnekleri inceleyelim: * Örnek 1, Aşağıdaki örnekte "shell" aygıtı kullanılmıştır. #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char* msg); int main(int argc, char* argv[]) { ssize_t result; char buf[BUFFER_SIZE + 1]; struct pollfd pfds[1]; pfds[0].fd = 0; pfds[0].events = POLLIN; // Okuma yapacağız. for (;;) { if (poll(pfds, 1, -1) == -1) exit_sys("poll"); if (pfds[0].revents & POLLIN) { if ((result = read(pfds[0].fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s", buf); } } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0.0, Aşağıdaki örnekte ise borular kullanılmıştır. Toplam açılan boru adedince döngü ilerletilmiş, bir diğer yandan da kapatılan boruların adedi sayılmıştır. Böylelikle diziden mantıksal çıkartma yapılmış oldu, kapatılan betimleyiciler açısından. #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 void exit_sys(const char *msg); int main(int argc, char *argv[]) { /* # Command Line Arguments # x y z */ if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); struct pollfd pfds[MAX_SIZE]; // "count" ile açılan toplam boruların adedini // "tcount" ile kapatılacak boruların sayısını tutmuş olacağız. int tcount, count = 0; for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((pfds[i - 1].fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); pfds[i - 1].events = POLLIN; // Okuma yapacağız. printf("%s opened...\n", argv[i]); ++count; } // "count" ile aslında esas dizimizi baştan sona dolaşmış olacağız. // "tcount" ile kapatılan boruların sayısını tutmuş olacağız. tcount = count; ssize_t result; char buf[BUFFER_SIZE + 1]; // Buradaki "+1", yazının sonuna '\0' koyabilmek içindir. for (;;) { printf("waiting at poll...\n"); if (poll(pfds, count, -1) == -1) exit_sys("poll"); for (int i = 0; i < count; ++i) { if (pfds[i].revents & POLLIN) { printf("POLLIN occured...\n"); if ((result = read(pfds[i].fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); } else if (pfds[i].revents & POLLHUP) { // İlgili boru kapatıldığında, "POLLHUP" oluşacaktır. printf("POLLHUP occured...\n"); // Dolayısıyla o boruya ilişkin betimleyiciyi kapatıyoruz. close(pfds[i].fd); // Dizinin ilgili indisindeki yapının "fd" değerini "-1" e çekiyoruz. // "-1" olduğu için artık "poll" fonksiyonu bu betimleyici ile ilgilenmeyecektir. // Dolayısıyla dizimide yer değişikliği yapmak zorunda değiliz. pfds[i].fd = -1; // En sonunda da sayacı bir azaltıyoruz. Çünkü kapatılan bir adet betimleyici mevcut. --tcount; } } // Eğer en bütün betimleyiciler kapatılmışsa da döngüden çıkıyoruz. if (tcount == 0) break; } printf("there is no descriptor open, finishes...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0.1, Pekala yukarıdaki örnekte "-1" değerini vererek diziden mantıksal çıkartma yapmak yerine, kapatılanı diziden gerçekten çıkartabiliriz. #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 void exit_sys(const char *msg); int main(int argc, char *argv[]) { /* # Command Line Arguments # x y z */ char buf[BUFFER_SIZE + 1]; struct pollfd pfds[MAX_SIZE]; ssize_t result; int tcount, count; if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); count = 0; for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((pfds[i - 1].fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); pfds[i - 1].events = POLLIN; printf("%s opened...\n", argv[i]); ++count; } tcount = count; for (;;) { printf("waiting at poll...\n"); if (poll(pfds, count, -1) == -1) exit_sys("poll"); for (int i = 0; i < count; ++i) { if (pfds[i].revents & POLLIN) { printf("POLLIN occured...\n"); if ((result = read(pfds[i].fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); } else if (pfds[i].revents & POLLHUP) { printf("POLLHUP occured...\n"); close(pfds[i].fd); pfds[i] = pfds[tcount - 1]; --tcount; } } count = tcount; if (count == 0) break; } printf("there is no descriptor open, finishes...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.1, Aşağıdaki örnekte de borular kullanılmıştır. Ancak yukarıdakine nazaran tek bir sayaç kullanılmıştır. Dolayısıyla döngü daha kısa sürede tamamlanabilir. #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 void exit_sys(const char *msg); int main(int argc, char *argv[]) { /* # Command Line Arguments # x y z */ if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); struct pollfd pfds[MAX_SIZE]; // "count" açılan ve kapatılan boruların takibini yapacağız. int count = 0; for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((pfds[i - 1].fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); pfds[i - 1].events = POLLIN; printf("%s opened...\n", argv[i]); ++count; } // Bu noktada açılan boruların adedini bilmekteyiz. ssize_t result; char buf[BUFFER_SIZE + 1]; // Buradaki "+1", yazının sonuna '\0' koyabilmek içindir. for (;;) { printf("waiting at poll...\n"); if (poll(pfds, count, -1) == -1) exit_sys("poll"); for (int i = 0; i < count; ++i) { if (pfds[i].revents & POLLIN) { printf("POLLIN occured...\n"); if ((result = read(pfds[i].fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); } else if (pfds[i].revents & POLLHUP) { // İlgili boru kapatıldığında, "POLLHUP" oluşacaktır. printf("POLLHUP occured...\n"); // Dolayısıyla o boruya ilişkin betimleyiciyi kapatıyoruz. close(pfds[i].fd); // Kapatılan boruya ilişkin betimleyiciye sahip nesnemiz, dizinin // son indisindeki nesne ile yer değiştirilmşitir. Aslında o indis // kapatıldığı için, dizinin sonundaki onun yerini almıştır. pfds[i] = pfds[count - 1]; // En sonunda da sayacı bir azaltıyoruz. Döngünün her turunda --count; } } if (count == 0) break; } printf("there is no descriptor open, finishes...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi "select" ve "poll" fonksiyonlarını karşılaştırırsak; -> "select" fonksiyonunun kullanımı, "poll" fonksiyonuna nazaran, görece daha kolaydır. -> "select" fonksiyonu azami "FD_SETSIZE" sembolik sabitinin değeri kadar betimleyici desteklerken, "poll" fonksiyonu için böyle bir azami sınır yoktur. Dolayısıyla "poll" fonksiyonu ile istediğimiz kadar betimleyiciyi takip edebiliriz. İşte "poll" fonksiyonunun en büyük avantajı da bu özelliğidir. -> Her iki fonksiyonda da betimleyici sayısı arttıkça, performans düşebilir. -> "poll" fonksiyonunu kullanabilmek için "pollfd" türünden bir yapı dizisinin oluşturulması gerekir. Ancak "select" fonksiyonundaki "fd_set" veri yapısı "bit" düzeyinde olduğu için daha az yer kaplama eyilimdedir. -> "select" fonksiyonunu kullanırken, fonksiyona verdiğimiz kümeler fonksiyon tarafından güncellendiği için, fonksiyonun her çağrılmasında kümelerin orjinal değerini saklayarak kullanmalıyız. -> "select" fonksiyonundaki zaman aşımı duyarlılığı mikrosaniye, "poll" fonksiyonundaki ise "milisaniye" düzeyindedir. Bu iki fonksiyonun ortak dezavantajı da betimleyici sayısının artması ile performanslarının düşme eyiliminde olmasıdır. İşte Linux sistemleri, bunu telafi etmek için, "epoll" isimli bir fonksiyon geliştirmişlerdir. Ancak bu fonksiyon, diğer iki fonksiyonun aksine bir POSIX fonksiyonu değil, bir Linux fonksiyonudur. >>> "epoll" Fonksiyonları : Bu fonksiyonlar Linux sistemlerinde, "poll" ve "select" fonksiyonuna nazaran, en iyi performansı veren mekanizmadır. Ancak POSIX olmadığı için taşınabilir değildir. Bu fonksiyonları, temelde üç fonksiyondan oluşur. Bu fonksiyonlar, "epoll_create", "epoll_create1", "epoll_ctl" ve "epoll_wait" isimli fonksiyonlardır. Bu fonksiyonlardan, >>>> "epoll_create" : Fonksiypnun prototipi aşağıdaki gibidir. #include int epoll_create(int size); Fonksiyonun parametresi kaç adet betimleyicinin izleneceğine dair bir ip ucu belirtmektedir. Fakat programcı bu parametre için kullandığı argümandan daha fazlasını da izleyebilir. Daha sonra bu parametre tasarımcıları rahatsız etmiş ve "epoll_create1" isimli fonksiyon geliştirilmiştir. Fonksiyon başarı durumunda bir "handler" işlevi gören nesneye, hata durumunda "-1" değerine geri döner. >>>> "epoll_create1" : Fonksiypnun prototipi aşağıdaki gibidir. #include int epoll_create1(int flags); Fonksiyon parametre olarak ya "0" ya da "EPOLL_CLOEXEC" bayrağını alır. Fonksiyon başarı durumunda bir "handler" işlevi gören nesneye, hata durumunda "-1" değerine geri döner. >>>> "epoll_ctl" : Fonksiyonun prototipi aşağıdaki gibidir. #include int epoll_ctl(int epfd, int op, int fd, struct epoll_event *_Nullable event); Fonksiyonun birinci parametresi, "epoll_create" veya "epoll_create1" ile elde ettiğimiz "handle" değeridir. İkinci parametre ise "EPOLL_CTL_ADD", "EPOLL_CTL_MOD" veya "EPOLL_CTL_DEL" değerlerinden birisini alır. Bu değerlerden, -> "EPOLL_CTL_ADD" : İzlemek istediğimiz betimleyiciyi mekanizmaya dahil etmek için kullanılırız. -> "EPOLL_CTL_MOD" : İzlemek istediğimiz betimleyiciyle ilgili izleme değişikliği için kullanırız. -> "EPOLL_CTL_DEL" : İzlemek istediğimiz betimleyiciyi mekanizmadan çıkartmak için kullanırız. Fonksiyonun üçüncü parametresi ise izlenecek betimleyiciyi, son parametre ise izlenecek olayı belirtir. Son parametrenin türü olan "struct epoll_event" türü şöyle tanımlıdır: struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; Bu yapının "events" isimli elemanı izlenecek olayıları anlatır. Bu elemanın alabileceği tipik değerler, "EPOLLIN", "EPOLLOUT", "EPOLLERR", "EPOLLHUP", "EPOLLET", ... değerleri veya bu değerlerin "bit-wise OR" ile birleştirilmiş hali olabilir. Bu değerlerden, -> "EPOLLIN" : Okuma amaçlı izlemeyi belirtir. Boruda ya da sokette okunacak bilgi oluştuğunda "epoll_wait" tarafından bu bayrak "set" edilir. Soketlerde "accept" yapan tarafta bir bağlantı istedği oluştuğunda da "EPOLLIN" bayrağı "epoll_wait" tarafından "set" edilmektedir. "EPOLLIN" bayrağı aynı zamanda karşı taraf soketi kapattığında da oluşmaktadır. -> "EPOLLOUT" : Yazma amaçlı izlemeyi belirtir. Boruya ya da sokete yazma durumu oluştuğunda (yani boruda ya da "network" tamponunda yazma için yer açıldığında) fonksiyon tarafından bu bayrak "set" edilmektedir. "EPOLLOUT" bayrağı aynı zamanda karşı taraf soketi kapattığında da oluşmaktadır. -> "EPOLLERR" : Hata amaçlı izlemeyi belirtir. Bu bayrak "epoll_ctl" fonksiyonunda "set" edilmez, "epoll_wait" fonksiyonu tarafından "set" edilmektedir. Bu bayrak borularda okuma yapan tarafın boruyu kapatmasıyla yazma yapan tarafta "set" edilmektedir. (Normal olarak okuyan tarafın boruyu kapattığı durumda boruya yazma yapıldığında "SIGPIPE" sinyalinin oluştuğunu anımsayınız.) Eğer okuyan taraf boruyu kapattığında boruya yazma için yer varsa yazma yapan tarafta aynı zamanda "EPOLLOUT" bayrağı da "set" edilmektedir. "EPOLLERR" bayrağı soketlerde kullanılmamaktadır. -> "EPOLLHUP" : Boruya yazan tarafın boru betimeleyicisini kapattığında okuma yapan tarafta bu bayrak "set" edilmektedir. Bu bayrak "epoll_ctl" fonksiyonunda set edilmez, "epoll_wait" tarafından yapının "set" edilmektedir. ("HUP", "hang up" anlamına gelmektedir.) Eğer boruya yazma yapan taraf boruyu kapattığında hala boruda okunacak bilgi varsa okuma yapan tarafta aynı zamanda "EPOLLIN" bayrağı da set edilmektedir. "EPOLLHUP" bayrağı soketlerde kullanılmamaktadır. -> "EPOLLET" : "epoll" fonksiyonunun "Edge Triggered" mı "Level Triggered" mı çalışacağına karar verir. -> ... Daha önce "poll" bayrakları için verdiğimiz tabloyu "epoll" bayrakları için de benzer biçimde vermek istiyoruz: -> Boruda bilgi yok ve yazan tarafın betimleyicisi kapalı ===> Okuyan tarafta "EPOLLHUP" -> Boruda bilgi var ve yazan tarafın betimleyicisi kapalı ===> Okuyan tarafta "EPOLLIN|EPOLLHUP" -> Boruda bilgi var ve yazan tarafın betimleyicisi açık ===> Okuyan tarafta "EPOLLIN" -> Boruda yazma için yer yok ve okuyan tarafın betimleyicisi kapalı ===> Yazan tarafta "EPOLLERR" -> Boruda yazma için yer var ve okuyan tarafın betimleyicisi kapalı ===> Yazan tarafta "EPOLLOUT|EPOLLERR" -> Boruda yazma için yer var ve okuyan tarafın betimleyicisi açık ===> Yazan tarafta "EPOLLOUT" -> Sokette bilgi var ===> Okuyan tarafta "EPOLLIN" -> Network tamponunda yazacak yer var ===> Yazan tarafta "EPOLLOUT" -> accept yapan tarafta bağlantı isteği oluştuğunda ===> accept yapan tarafta "EPOLLIN" -> Karşı taraf soketi kapattığında ===> karşı tarafta "EPOLLIN|EPOLLOUT|EPOLLRDHUP" Yapının "data" elemanı ise bir "union" olarak, typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; }epoll_data_t; biçimde tanımlanlanmıştır. Bu yapı türü bir nevi bilgi almak için, kullanılır. Eğer programcı, daha fazla bilgi almak istiyorsa, o bilgileri bir yapı içerisinde tanımlar ve yapının adresini de "epoll_data_t" türünün "ptr" elemanına atar. "epoll_ctl" fonksiyonu başarı durumunda "0", hata durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Pekiyi "epoll_event" yapısının "events" elemanında kullanılan "Edge Triggered" ve "Level Triggered" kavramları da nelerdir? Aslında bu kavramlar "select", "poll" ve "epoll" fonksiyonlarının arka planında gerçekleşen elektronik olayları betimlerler. Bu kavramlardan, >>>>> "Level Triggered" : Düzey Tetikleme olarak Türkçeleştirilebilir. Elektronik devrelerdeki Kare Dalga'ya benzer. Bir olayın gerçekleşmesi için yine bir dalganın gelmesine ihtiyaç duyulur ancak dalga kesildiğinde o olay devam eder. "select" ve "poll" fonksiyonları ile "epoll" fonksiyonunun varsayılan hali "Level Triggered" olarak çalışmaktadır. Yani bir boruya bilgi geldiği zaman ve bu fonksiyonlar çağrıldığında, bu fonksiyonlar durumu bize bildirirler. Gelen o bilgi borudan okunmadığı sürece, bu fonksiyonları tekrar çağırmamız durumunda, bu fonksiyonlar durumun yine bize bildirecektir. Örneğin, "stdin" dosyasını izliyorsak ve klavyeden bir giriş yapıldıysa bu fonksiyonlar blokeyi çözer. Eğer biz "read" ile okuma yapmadan bu fonksiyonları çağırırsak, artık bloke oluşmaz. Çünkü bir kez tetiklenme gerçekleşmiş, ona ilişkin aksiyon alınmıştır. Yani işlemcinin o ayağında 5V bulunduğu sürece, bir işlem yapılacaktır. >>>>> "Edge Triggered" : Kenar Tetikleme olarak da Türkçeleştirilebilir. Elektronik devrelerdeki Kare Dalga tipik örnektir. Bir olayın gerçekleşmesi için bir dalganın gelmesine ihtiyaç duyulmasıdır. Dalga kesildiğinde de olay son bulacaktır. Dalga geldikçe o olay gerçekleşecektir. "epoll" fonksiyonunun son parametresi olan "epoll_event" yapısının "events" elemanına "EPOLLET" eklenirse, "Edge Triggered" olarak çalışır. Örneğin, "stdin" dosyasını izliyorsak ve klavyeden bir giriş yapıldıysa bu fonksiyonlar blokeyi çözer. Ancak okuma yapmazsak, tekrar bloke oluşur. Çünkü bir kez tetikleme gerçekleşmiş ancak ona ilişkin aksiyon alınmamıştır. Yani işlemcinin o ayağındaki volt yalnızca sıfırdan beşe yükselmesi durumunda bir işlem yapılacaktır. >>>> "epoll_wait ": Tıpkı "select" ve "poll" fonksiyonlarında olduğu gibi, eğer izlenen betimleyicilerde beklenen olay gerçekleşmezse, blokeye yol açar. Ancak en az bir betimleyicide beklenen olay gerçekleşmişse, blokeyi çözer. Fonksiyonun prototipi aşağıdaki gibidir. #include int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); Fonksiyonun birinci parametresi, "epoll_ctl" fonksiyonunun birinci parametresine geçtiğimiz, "handle" değeridir. İkinci parametre ise oluşan olayların saklanacağı dizi adresidir. Üçüncü parametre ise ikinci parametresine başlangıç adresini geçtiğimiz dizinin uzunluğudur. Son parametre ise zaman aşımı parametresidir. "-1" değeri zaman aşımının kullanılmayacağını, "0" ise betimleyicilere hemen bakılıp çıkılacağı anlamındadır. Fonksiyon, -> Başarı durumunda, ikinci parametresine başlangıç adresini geçtiğimiz, diziye eklediği eleman sayısına. -> Başarısızlık durumunda "-1" ile geri döner. -> "0" ile geri dönmesi ise zaman aşımına takıldığı anlamındadır. Bu fonksiyonları ise şu şekilde kullanmalıyız; -> "epoll_create" veya "epoll_create1" fonksiyonu ile bizler bir "handle" elde ederiz. -> "struct epoll_event" türünden bir nesne tanımlayıp, içini amacımıza uygun biçimde doldurmalıyız. Sonrasında takip edeceğimiz betimleyicileri ve az evvel oluşturduğumuz "struct epoll_event" türünden nesneyi, "epoll_ctl" fonksiyonu ile, "epoll" mekanizmasına dahil ederiz. Burada her bir betimleyici için ayrı ayrı "epoll_ctl" fonksiyon çağrısını yapmak durumundayız. -> "epoll_wait" fonksiyonunu yine bir döngü içerisinde çağırıp, gerçekleşen olayları işleyeceğiz. Yine "EPOLLHUP" ve "EPOLLIN" olaylarını ayrık "if" deyimleri ile DEĞİL, "if-else if" deyimi ile kontrol etmeliyiz. -> En sonunda "epoll_create" ları ile elde ettiğimiz "handler" nesnesini "close" fonksiyonu ile kapatmalıyız. Böylelikle bütün mekanizma da sonlanmış olacaktır. Her ne kadar programın sonlanması ile bu "handler" değeri de yok edilse bile, erkenden mekanizmayı sonlandırmak isteyedebiliriz. Diğer yandan "epoll" fonksiyonlarını kullanırken şu noktalara da dikkat etmeliyiz; -> Belli bir betimleyiciyi izleme kümesinden çıkartmak için, normal olarak, "epoll_ctl" fonksiyonuna "EPOLL_CTL_DEL" sembolik sabitini geçeriz. Ancak "epoll" fonksiyonlarında buna çoğu kez gerek kalınmaz. Bir dosyaya ilişkin son betimleyici de kapatılırsa, o betimleyici izleme kümesinden otomatik olarak çıkartılır. -> "epoll_wait" fonksiyonunun "maxevents" isimli elemanına geçtiğimiz argüman aslında bizim takip etmek istediğimiz olayların adedini belirtir. Çünkü bu olayları bizler ikinci parametresi olan "events" dizisi üzerinden takip edeceğiz. Ancak bu demek değildir ki mekanizmanın çalışması sırasında "maxevents" e geçtiğimiz argüman kadar olay gerçekleşecek. Örneğin, "maxevents" değişkenine "5" değerini geçelim. Dizimizin boyutu da aslında "10" elemanlı olsun. İşte fonksiyonumuz maksimum "5" adet olayı takip edip, "events" dizisine işleyecektir. Pekala o an "15" adet olay da gerçekleşmiş olabilir. "15" olaydan "5" tanesini takip etmiş olacağız. Eğer "maxevents" için "10" değerini kullansaydık ve sistemimizde yine "15" tane olay olsaydı, "10" tanesinin takibini yapabilmiş olacaktık. Özetle "maxevents" ile dizimizin boyutunu belirtir, fonksiyonun geri dönüş değeriyle de dizinin ilk kaç elemanının doldurulduğunu tespit ederiz. Şimdi de aşağıdaki örnekleri inceleyelim: * Örnek 1, Aşağıdaki örnekte "shell" programı kullanıldığı için "EPOLLHUP" için kontrol yapılmamıştır. "0" nolu betimleyiciden okuma işlemi yapılmıştır. #include #include #include #include #define MAX_EVENTS 1 #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int epfd; if ((epfd = epoll_create(1)) == -1) exit_sys("epoll_create"); struct epoll_event ee; ee.events = EPOLLIN; ee.data.fd = 0; if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee) == -1) exit_sys("epoll_ctl"); struct epoll_event ee_result[MAX_EVENTS]; // Örneğimizde "0" numaralı betimleyiciyi kullanacağımız için, dizinin boyutunu "1" yaptık. int n_events; // Gerçekleşen olayın adedini tutacaktır, o olayın gerçekleştiği betimleyici adetlerini değil. ssize_t result; // Ne kadarlık okuma yapıldığı bilgisini tutmaktadır. char buf[BUFFER_SIZE + 1]; // Okunanların yazılacağı dizi. for (;;) { if ((n_events = epoll_wait(epfd, ee_result, MAX_EVENTS, -1)) == -1) exit_sys("epoll_wait"); for (int i = 0; i < n_events; ++i) { if (ee_result[i].events & EPOLLIN) { if ((result = read(ee_result[i].data.fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (result == 0) goto EXIT; buf[result] = '\0'; printf("%s", buf); } } } EXIT: close(epfd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise "x", "y" ve "z" isimli boruları önce oluşturmalıyız ve ayrı "shell" programları ile bu boruları açmalıyız. Daha sonra bu boruları kullanarak programımızı çalıştırabilir, diğer terminallerden veri gönderebilir, programımız üzerinden de bunları okuyabiliriz. Borular kullandığımız için de "EPOLLHUP" oluşmaktadır. #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 #define MAX_EVENTS 5 void exit_sys(const char *msg); int main(int argc, char *argv[]) { /* # Command Line Arguments # x y z */ if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } int epfd; // "handler" değerini tutan değişken. if ((epfd = epoll_create(1)) == -1) exit_sys("epoll_create"); printf("opens named pipes... it may block...\n"); int fd; // Açılan boruya ilişkin betimleyici. struct epoll_event ee; int count = 0; // Toplam betimleyici sayısı. for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } // Komut satırı argümanlarında belirtilen boruları açıyoruz. if ((fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); // Yapının içini dolduruyoruz. ee.events = EPOLLIN; ee.data.fd = fd; // Her bir betimleyici için bu çağrıyı yapmak durumundayız. if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ee) == -1) exit_sys("epoll_ctl"); printf("%s opened...\n", argv[i]); ++count; } int nevents; // Beklenen kaç adet olayın gerçekleştiğini sayıyoruz. ssize_t result; // Borudan yapılan okuma miktarı. char buf[BUFFER_SIZE + 1]; // Borudan okunanların yazılacağı dizi. struct epoll_event ree[MAX_EVENTS]; // Beklenen eylemlerin sonuçlarının yazılacağı dizi. for (;;) { printf("waiting at epoll_wait...\n"); // (Varsa)Gerçekleşen olayıları "get" ediyoruz. if ((nevents = epoll_wait(epfd, ree, MAX_EVENTS, -1)) == -1) exit_sys("epoll_wait"); // Sonuçları irdeliyoruz. for (int i = 0; i < nevents; ++i) { if (ree[i].events & EPOLLIN) { // Karşı taraf boruya yazmışsa, "EPOLLIN"; printf("EPOLLIN occured...\n"); if ((result = read(ree[i].data.fd, buf, BUFFER_SIZE)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); } else if (ree[i].events & EPOLLHUP) { // Karşı taraf boruyu kapatmışsa, "EPOLLHUP"; printf("EPOLLHUP occured...\n"); close(ree[i].data.fd); --count; // Kapatılan borular için sayacı azaltıyoruz. Böylelikle dizimiz daha erken sonlanacaktır. } } if (count == 0) break; } printf("there is no descriptor open, finishes...\n"); close(epfd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Linux sistemlerindeki "epoll" fonksiyonları ile "POSIX" fonksiyonu olan "select" ve "poll" fonksiyonlarını performans açısından, saniye cinsinden, karşılaştırmak istersek; Number of descriptors poll() CPU time select() CPU time epoll CPU time 10 0.61 0.73 0.41 100 2.9 3.0 0.42 1000 35 35 0.53 10000 990 930 0.66 Bu sonuçlar "The Linux Programming Environment by Michale Kerrisk" kitabından alınmıştır. Görüldüğü üzere Linux sistemleri söz konusu olduğunda "epoll" fonksiyonu daha performanslı çalışmaktadır ancak bu fonksiyonun "POSIX" uyumlu OLMADIĞINA DİKKAT EDİNİZ. >> "Signal Driven IO" : Bu modelde bir grup betimleyici izlemeye alınır. Bu betimleyicilerde ilgilenilen olay, "read", "write" veya "error", gerçekleşmemişse blokede beklenmez. Beklenen olay gerçekleştiğinde "SIGIO" sinyali prosese gönderilir. Programcı da bu blokeye maruz kalmadan "read" ve/veya "write" yapabilir. Güncel POSIX standartlarında bulunmamaktadır. Linux ve bazı UNIX türevi sistemler desteklemektedir. Bu modeli şu aşamaları takip ederek kullanabiliriz: -> Betimleyici(ler) "open" fonksiyonuyla açılırlar. Dolayısıyla maksadımızın ne olduğuna bağlı olarak, "open" fonksiyonuna ilgili bayrakları da geçmeliyiz. Örneğin, "O_RDONLY" bayrağını kullanmamız sadece "read", "O_WRONLY" kullanmamız yalnızca "write", "O_RDWR" kullanmamız ise hem "read" hem "write" amacını taşıdığını belirtir. Eğer "socket" söz konusu ise ilgili fonksiyonlar kullanılarak açılır. -> "SIGIO" sinyaline ilişkin bir "signal handler" fonksiyon "set" edilir. -> İlgili betimleyicide beklenen olay oluştuğunda, hangi prosese sinyal gönderileceğini, "fcntl" fonksiyonu üzerinden "set" ederiz. Fakat genel olarak sinyalin kendi prosesimize gönderilmesini isteriz. Dolayısıyla "fcntl" fonksiyonunun ikinci parametresine "F_SETOWN", üçüncü parametresine ise "getpid()" çağrısını ekleriz. Böylece ilgili olay gerçekleştiğinde, kendimize ilgili sinyali göndermiş oluruz. Eğer üçüncü parametreye negatif bir değer geçersek, o değerin mutlak değerindeki ID değerine sahip proses grubu, hedef alınır. -> "fcntl" fonksiyonu ile ilgili betimleyici blokesiz moda sokulmalıdır. Ancak bunu yaparkan, ilk önce kullanılan bayrakları, "F_GETFL" ile "get" etmeliyiz. Daha sonra elde ettiğimiz bayrak bilgilerini, "F_SETFL" komutunu da kullanarak, "O_NONBLOCK | O_ASYNC" bayraklarıyla "bit-wise OR" ile birleştirmeliyiz. Ancak "O_ASYNC" bayrağının da POSIX standartlarında bulunmadığına dikkat ediniz. -> Artık akış normal biçimde devam eder. İlgilenilen olay gerçekleştiğinde, yukarıda bahsedilen sinyal oluşacaktır. Bu modülü kullanırken şu noktalara dikkat etmeliyiz; -> "Signal Driven IO" modülü "Edge Triggered" biçimde çalışmaktadır. Yani takip ettiğimiz olaylara ilişkin sinyal oluşumu o boruda bilgi olduğu sürece değil, o boruya her bilgi geldiğinde yapılacaktır. Dolayısıyla borudan azar azar okumak yerine, yazılanların hepsini okumalıyız. Bir diğer deyişle sinyal oluştuğunda, bir döngü içerisinde okuma/yazma işlemi başarısız olana kadar, okuma yapmaya çalışmalıyız. Örneğin, boruya 100 bayt gelmiş olsun. Bu durumda sinyal oluşur. Eğer biz yüz bayttan daha az bayt okursak, boruya ya da sokete yeni bilgi gelene kadar yeni bir sinyal oluşmayacağından, boruda kalanları okuma şansımız kalmayacaktır. Bu nedenle o zaman kadar gelmiş bütün bilgileri bir döngü içerisinde okumalıyız. -> Sinyal oluştuğunda boruda okuma/yazma işlemlerinin nasıl yapılacağı da bir problemdir. Akla ilk gelen yöntem, ilgili "signal_handler" fonksiyonu içerisinde bu işlemin yapılmasıdır. Ancak genellikle iyi bir teknik değildir. Çünkü ilgili "signal_handler" içerisinde uzun sürecek işlemler yapmak tavsiye edilmez. "signal_handler" içerisinde aynı sinyalin tekrar oluşması durumunda, o sinyaller bloke oluşacaktır. Blokenin çözülmesiyle birlite, bloke edilenlerden sadece bir tanesini "handle" edebileceğiz. "SIGIO" sinyali de gerçek zamanlı bir sinyal olmadığından, kuyruklanmayacaktır. Diğer yandan "signal_handler" içerisinde yalnızca "signal safe" fonksiyonları çağırabiliriz. Fakat okuma/yazma işlemi ve sonrasında pek çok "signal safe" OLMAYAN FONKSİYONLARIN ÇAĞRILMASI gerekebilir. İşte "signal_handler" içerisinde bir "flag" değişkeni "set" edilir ve ilgili işlemler sinyal fonksiyonu dışında gerçekleştirilir. -> Birden fazla betimleyici üzerinde izleme yaparken, "SIGIO" yerine, artık Gerçek Zamanlı bir sinyal kullanmalıyız. Bu sinyaller hem kuyruklanmakta hem de ek bilgi yerleştirmek mümkündür. Anımsanacağı üzere Gerçek Zamanlı sinyalleri kullanırken, ilgili "signal_handler" fonksiyonunun "siginfo_t" türünden parametresine, "siginfo_t" türünden bir yapı nesnesinin adresini geçiyor ve "signal_handler" fonksiyon ise bu yapı nesnesinin içini dolduruyordu. Bu yapı Linux sistemlerince aşağıdaki gibi tanımlanmıştır: siginfo_t { int si_signo; // Oluşan sinyalin numarası. int si_errno; /* An errno value */ int si_code; // Sinyalin neden oluştuğu bilgisi. "bit-mask" DEĞİLDİR. Hangi değerleri alacağı hususunda; "https://man7.org/linux/man-pages/man2/sigaction.2.html". int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */ pid_t si_pid; // Sinyali oluşturan prosesin ID değeri. uid_t si_uid; /* Real user ID of sending process */ int si_status; /* Exit value or signal */ clock_t si_utime; /* User time consumed */ clock_t si_stime; /* System time consumed */ union sigval si_value; /* Signal value */ int si_int; /* POSIX.1b signal */ void *si_ptr; /* POSIX.1b signal */ int si_overrun; /* Timer overrun count; POSIX.1b timers */ int si_timerid; /* Timer ID; POSIX.1b timers */ void *si_addr; /* Memory location which caused fault */ long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */ int si_fd; // Sinyale yol açan betimleyici. short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */ void *si_lower; /* Lower bound when address violation occurred (since Linux 3.19) */ void *si_upper; /* Upper bound when address violation occurred (since Linux 3.19) */ int si_pkey; /* Protection key on PTE that caused fault (since Linux 4.6) */ void *si_call_addr; /* Address of system call instruction (since Linux 3.5) */ int si_syscall; /* Number of attempted system call (since Linux 3.5) */ unsigned int si_arch; /* Architecture of attempted system call (since Linux 3.5) */ } -> Bu modülü senkron biçimde de kullanabiliriz. Bunun için ilk olarak beklemek istediğimiz sinyalleri "sigprocmask" üzerinden prosesimize karşı bloke etmeli, o sinyalin oluşmasından sonra "sigwaitinfo" fonksiyonu ile o sinyali işlemeli, en sonunda da yine "sigprocmask" üzerinden blokeyi kaldırmalıyız. Sinyalin işlenmesi için "sigwaitinfo" kullanıldığından, herhangi bir "signal_handler" fonksiyona ihtiyaç duyulmamıştır. -> Linux sistemlerinde performans sıralaması, "epoll" > "'Signal Based IO' module w/ synchronized signal" > "select" & "poll" biçiminde olacaktır. Şimdi de aşağıdaki örnekleri inceleyelim: * Örnek 1.0, Aşağıdaki örnekte ilgili "signal_handler" içerisindeki "printf" çağrısında "\n" kullanmadığımız için tampon boşaltılmayacağından, ekrana bir şey yazılmayacaktır. #include #include #include #include #include void signal_handler_function(int signo); void exit_sys(const char *msg); int main(void) { struct sigaction sa; sa.sa_handler = signal_handler_function; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGIO, &sa, NULL) == -1) exit_sys("sigaction"); if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK | O_ASYNC) == -1) exit_sys("fcntl"); for (;;) pause(); return 0; } void signal_handler_function(int signo) { printf("signal_handler_function"); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, #include #include #include #include #include #include #define BUFFER_SIZE 1024 void signal_handler_function(int signo); void exit_sys(const char *msg); int main(void) { /* # INPUT # Ulya */ /* # OUTPUT # [29] - signal_handler_function */ struct sigaction sa; sa.sa_handler = signal_handler_function; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGIO, &sa, NULL) == -1) exit_sys("sigaction"); if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK | O_ASYNC) == -1) exit_sys("fcntl"); for (;;) pause(); return 0; } void signal_handler_function(int signo) { printf("[%d] - signal_handler_function\n", signo); /* UNSAFE */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0, Aşağıdaki yöntem de kötü bir tekniktir. #include #include #include #include #include #include #define BUFFER_SIZE 1024 void signal_handler_function(int signo); void exit_sys(const char *msg); int main(void) { /* # INPUT # Ulya */ /* # OUTPUT # [29] - signal_handler_function: Ulya */ struct sigaction sa; sa.sa_handler = signal_handler_function; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGIO, &sa, NULL) == -1) exit_sys("sigaction"); if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK | O_ASYNC) == -1) exit_sys("fcntl"); for (;;) pause(); return 0; } void signal_handler_function(int signo) { printf("[%d] - signal_handler_function: ", signo); /* UNSAFE */ char buf[BUFFER_SIZE + 1]; ssize_t result; for (;;) { result = read(STDIN_FILENO, buf, BUFFER_SIZE); if (result == -1) { if (errno == EAGAIN) break; exit_sys("read"); } buf[result] = '\0'; printf("%s\n", buf) ; /* UNSAFE */ } } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.1, İşte daha iyi bir çözüm aşağıdaki gibidir. "g_sigio_flag" bayrağı ilgili sinyali takip etmek için kullanılmıştır. #include #include #include #include #include #include #define BUFFER_SIZE 1024 void signal_handler_function(int signo); void exit_sys(const char *msg); volatile sig_atomic_t g_sigio_flag; int main(void) { /* # INPUT # Ulya */ /* # OUTPUT # [29] - signal_handler_function: Ulya */ struct sigaction sa; sa.sa_handler = signal_handler_function; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGIO, &sa, NULL) == -1) exit_sys("sigaction"); if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK | O_ASYNC) == -1) exit_sys("fcntl"); char buf[BUFFER_SIZE + 1]; ssize_t result; for (;;) { pause(); if (g_sigio_flag) { for (;;) { result = read(STDIN_FILENO, buf, BUFFER_SIZE); if (result == -1) { if (errno == EAGAIN) break; exit_sys("read"); } buf[result] = '\0'; printf("%s\n", buf) ; } g_sigio_flag = 0; } } return 0; } void signal_handler_function(int signo) { printf("[%d] - signal_handler_function: ", signo); /* UNSAFE */ g_sigio_flag = 1; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Gerçek Zamanlı sinyal kullanımına ilişkin bir örnektir. #define _GNU_SOURCE // For 'F_SETSIG' flag. #include #include #include #include #include #include #define BUFFER_SIZE 1024 void signal_handler_function(int signo, siginfo_t* info, void* context); void exit_sys(const char *msg); sig_atomic_t g_sigio_flag; int main(void) { /* # INPUT # Ulya Yuruk */ /* # OUTPUT # [34] - signal_handler_function: Ulya Yuruk */ struct sigaction sa; sa.sa_sigaction = signal_handler_function; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_SIGINFO; if (sigaction(SIGRTMIN, &sa, NULL) == -1) exit_sys("sigaction"); if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK | O_ASYNC) == -1) exit_sys("fcntl"); if (fcntl(STDIN_FILENO, F_SETSIG, SIGRTMIN) == -1) exit_sys("fcntl"); char buf[BUFFER_SIZE + 1]; ssize_t result; for (;;) { pause(); if (g_sigio_flag) { for (;;) { result = read(STDIN_FILENO, buf, BUFFER_SIZE); if (result == -1) { if (errno == EAGAIN) break; exit_sys("read"); } buf[result] = '\0'; printf("%s\n", buf) ; } g_sigio_flag = 0; } } return 0; } void signal_handler_function(int signo, siginfo_t* info, void* context) { printf("[%d] - signal_handler_function: ", signo); /* UNSAFE */ g_sigio_flag = 1; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Yukarıdaki örneklere nazaran sinyalin beklenmesi senkron olarak gerçekleştirilebilir. Aşağıdaki örnekte programın çalıştırıldığı "shell" programına ilaveten üç tane daha çalıştırılmalı, sonrasında "mkfifo x y z" komutu ile ilgili isimlerde üç adet borunun oluşturulması ve son olarak ilave oluşturulan "shell" programlarında sırasıyla "cat > x", "cat > y" ve "cat > z" komutlarını çalıştırmalıyız. Artık bu komutları çalıştırdığımız "shell" programlarından gönderdiklerimizi, ilk baştaki "shell" programından okuyabiliriz. Herhangi bir "signal_handler" fonksiyon kullanılmamıştır. #define _GNU_SOURCE #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 void exit_sys(const char *msg); int main(int argc, char *argv[]) { /* # Command Line Argumnts # ./main x y z */ /* # OUTPUT # opens named pipes... it may block... x opened... y opened... z opened... Ulya Yuruk Uskudar Istanbul Saglik Bilimleri Universitesi there is no descriptor open, finishes... */ if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); int fd; int count = 0; for (int i = 1; i < argc; ++i) { if (count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); if (fcntl(fd, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL)|O_NONBLOCK|O_ASYNC) == -1) exit_sys("fcntl"); if (fcntl(fd, F_SETSIG, SIGRTMIN) == -1) exit_sys("fcntl"); printf("%s opened...\n", argv[i]); ++count; } sigset_t sset; sigaddset(&sset, SIGRTMIN); // İlgili sinyali bloke etmeliyiz. if (sigprocmask(SIG_BLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); // Akış senkron olduğundan ve yukarıda bloke ettiğimizden, // ilgili sinyal oluştuğunda aşağıdaki kodlar çalıştırılacaktır. char buf[BUFFER_SIZE + 1]; ssize_t result; siginfo_t sinfo; for (;;) { if ((sigwaitinfo(&sset, &sinfo)) == -1) exit_sys("sigwaitinfo"); if (sinfo.si_code & POLL_IN) { // Borularda karşı tarafın kapatması durumunda da "POLL_IN" oluşur. for (;;) { result = read(sinfo.si_fd, buf, BUFFER_SIZE); if (result == -1) { if (errno == EAGAIN) break; exit_sys("read"); } if (result == 0) { --count; close(sinfo.si_fd); break; } buf[result] = '\0'; printf("%s", buf); } if (count == 0) break; } } // Akış buraya geldiyse, işimiz bitmiş demektir. // Böylelikle blokeyi kaldırmalıyız. if (sigprocmask(SIG_UNBLOCK, &sset, NULL) == -1) exit_sys("sigprocmask"); printf("there is no descriptor open, finishes...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >> "Asynchronous IO" : Bu modelde "read"/"write" işlemleri başlatılır, ancak arka planda çekirdek tarafından asenkron bir biçimde sürdürülür.İşlem tamamlandığında ise programcı haberdar edilir. Yine bloke gerçekleşmez. Bu modül POSIX standartları tarafından destekenir. Bu modülde kullanılan fonksiyonlar "aio" ön eki alırlar. Kullanılacak başlık dosyasını ismi "aio.h" şeklindedir. Tipik olarak bu modülü kullanmak için şu aşama aşamaları sırayla takip etmemiz gerekmektedir: -> "aiocb" yapı türünden bir nesne tanımlanır ve içi biz programcılar tarafından doldurulur. POSIX standartlarınca bu yapı aşağıdaki gibi tanımlıdır. struct aiocb { int aio_fildes; // File descriptor. off_t aio_offset; // File offset. volatile void *aio_buf; // Location of buffer. size_t aio_nbytes; // Length of transfer. int aio_reqprio; // Request priority offset. struct sigevent aio_sigevent; // Signal number and value. int aio_lio_opcode; // Operation to be performed. }; Yapının "aio_fildes" isimli eleman okuma/yazma yapmak istediğimiz betimleyiciyi belirtir. Yapının "aio_offset" isimli elemanı ise okuma/yazma işleminin dosyanın neresinden itibaren yapılacağını belirtir. Asenkron okuma/yazma işlemleri, dosya göstericisinin gösterdiği konum itibariyle yapılmamaktadır (Eğer dosyamız "O_APPEND" modunda açılmışsa ve yazma işlemi yapacaksak, bu elemana yazacaklarımız dikkate alınmaz. Ayrıca "seekable" olmayan aygıtlar söz konusu olduğunda, bu elemana "0" değeri geçebiliriz). Yapının "aio_buf" isimli elemanı ise okuma/yazma işlemi sırasında kullanılacak tampon bölgeyi belirtir ve işlemler sırasında bu bölgenin hayatta olması gerekmektedir. Yapının "aio_nbytes" isimli elemanı ise aslında yapının "aio_buf" elemanı ile belirttiğimiz tampon bölgenin uzunluğunu belirtir. Yapının "aio_reqprio" isimli elemanı ise yapılacak okuma/yazma işlemi için bir ipucu belirtir. Tabii işletim sisteminin bunu kullanacağı kesin DEĞİLDİR. "0" değeri geçilebilir. Yapının "aio_sigevent" isimli elemanı ise işlemler bittiğinde yapılacak bildirime ilişkin bilgileri barındırır. Bu eleman "sigevent" yapı türünden olup, struct sigevent { int sigev_notify; // Notification type. int sigev_signo; // Signal number. union sigval sigev_value; // Signal value. void (*sigev_notify_function)(union sigval); // Notification function. pthread_attr_t *sigev_notify_attributes; // Notification attributes. }; biçimde tanımlanmıştır. Yine bu "sigevent" yapısının elemanlarını da doldurmamız gerekmektedir. "sigevent" yapısının "sigevent" isimli elemanı işlemler bittiğinde yapılacak bildirimi belirtir. Bu eleman "SIGEV_NONE", "SIGEV_SIGNAL" ve "SIGEV_THREAD" değerlerinden birisini alır. Bu değerlerse sırasıyla bir bildirimin yapılmayacağını, bildirimin sinyal ile yapılacağını ve bildirimin "thread" ile ki bu "thread" "kernel" tarafından oluşturulur, yapılacağını belirtir. "sigevent" yapısının "sigev_signo" isimli elemanı, eğer bildirimde sinyaller kullanılacaksa, kullanılacak sinyalin numarasını belirtir. "sigevent" yapısının "sigev_value" isimli elemanı ise ilgili "signal_handler" ya da "thread" e verdiğimiz fonksiyona gönderilecek bilgiyi temsil eder. Bu eleman "union sigval" türünden olup, union sigval { int sival_int; void* sival_ptr; }; biçiminde tanımlanmıştır. "sigevent" yapısının "sigev_notify_function" isimli elemanı, eğer "thread" üzerinden bildirim yapılacaksa, "thread" tarafından kullanılacak fonksiyonun ne olduğunu belirtir. "sigevent" yapısının "sigev_notify_attributes" isimli elemanı ise kullanılacak "thread" in özelliklerini belirtir, "NULL" değerini geçebiliriz. "aiocb" yapısının "aio_lio_opcode" isimli elemanına daha sonra değineceğiz. -> Şimdi okuma/yazma işlemlerini "aio_read"/"aio_write" fonksiyonları ile başlatmamız gerekmektedir. Artık bu çağrılar sonrasında kendi akışımız da akmaya devam edecektir. İşlemler bittiğinde bize bildirim yapılacaktır. Fonksiyonların prototipi aşağıdaki gibidir. #include int aio_read(struct aiocb *aiocbp); int aio_write(struct aiocb *aiocbp); Fonksiyonlara argüman olarak yukarıda tanımladığımız "aiocb" türünden nesnenin adresini geçeriz. Unutmamalıyız ki adresini geçtiğimiz nesnemiz, çalışma boyunca hayatta kalmalıdır. Fonksiyonlar başarı durumunda "0", hata durumunda "-1" ile geri dönerler ve "errno" değişkenini uygun değere çekerler. Bu fonksiyonlar sadece bir defalık mekanizmayı çalıştırmaktadır. Bu fonksiyonlar "signal-safe" DEĞİLDİR. -> Anımsanacağı üzere "aiocb" yapısının "aio_nbytes" isimli elemanında kullanılan tampon bölgenin büyüklüğünü belirtmiştik. Ancak bu büyüklüğün tamamının kullanılmadığı senaryolar da olabilir. Pekiyi ne kadarlık bir büyüklüğün kullanıldığını nasıl tespit ederiz? İşte bunun için "aio_return" fonksiyonunu kullanırız. Fonksiyonun prototipi aşağıdaki gibidir. #include ssize_t aio_return(struct aiocb *aiocbp); Fonksiyon argüman olarak yine "aio_read"/"aio_write" fonksiyonlarına geçtiğimiz adres değerini alır. Başarı durumunda kullanılan büyüklük bilgisine, hata durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Eğer işlemler tamamlanmadan bu fonksiyonu çağırırsak, "Tanımsız Davranış" oluşur. Tabii işlemler sonucunda ne kadarlık büyüklük bilgisinin kullanıldığını öğrenmek için, fonksiyona argüman olarak geçtiğimiz "aiocb" türünden nesnenin "sigev_value" isimli elemanının "sival_ptr" isimli elemaını, ilk başta "aiocb" türünden nesnenin içini doldururken kullanmalıyız. Bu fonksiyon "signal-safe" dir. -> En sonunda da mekanizmayı tekrar çalıştırmak için "aio_read"/"aio_write" çağrılarını tekrarlamalıyız. -> Mekanizma işlerken anlık bilgi almak için "aio_error" isimli fonksiyonu kullanırız. Fonksiyonun prototipi aşağıdaki gibidir. #include int aio_error(const struct aiocb *aiocbp); Fonksiyon argüman olarak durumunu merak ettiğimiz "aiocb" yapısının adresini alır. Fonksiyonun kendisi başarısız olursa "-1" ile geri döner ve "errno" değişkenini uygun değere çeker. Eğer "EINPROGRESS" koduna geri dönerse mekanizmanın henüz tamamlanmadığını, "ECANCELED" ile geri dönerse mekanizmanın iptal edildiği("Linux specific" ), "0" ile geri dönerse mekanizmanın başarılı biçimde tamamlandığını, diğer hata kodları ile geri dönerse de o koda ilişkin hata olduğunu belirtir. -> Başlatılan bir mekanizmayı iptal etmek istersek, "aio_cancel" fonksiyonunu kullanmalıyız. Fonksiyonun prototipi aşağıdaki gibidir. #include int aio_cancel(int fildes, struct aiocb *aiocbp); Fonksiyonun ikinci parametresi, iptal etmek istediğimiz mekanizma için kullanılan "aiocb" yapı nesnesinin adres değeridir. Fonksiyonun birinci parametresi ise dosya betimleyicisini belirtir. "NULL" değerinin geçilmesi durumunda, o betimleyiciyle ilişkili bütün "Asynchronous IO" mekanizması iptal edilir. Fonksiyonun kendisi başarısız olursa "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Eğer "AIO_CANCELED" değerine geri dönerse iptal isteği başarılı, "AIO_NOTCANCELED" değerine geri dönerse iptal isteğinin başarısız olduğu, "AIO_ALLDONE" değerine geri dönerse mekanizmanın zaten tamamlandığı anlamına gelir. Şimdi de aşağıdaki örnekleri inceleyelim: * Örnek 1, Aşağıda "thread" kullanılmıştır. "0" nolu betimleyiciye yazılanlar işlendikten sonra ekrana basılmıştır. #include #include #include #include #define BUFFER_SIZE 4096 void io_proc(union sigval sval); void exit_sys(const char *msg); int main(void) { /* # INPUT # Ulya Yuruk Uskudar Istanbul Ctrl+d */ /* # OUTPUT # waiting at pause, press Ctrl+C to exit... IO occured: Ulya Yuruk IO occured: Uskudar Istanbul Ctrl+d pressed... */ struct aiocb cb; cb.aio_fildes = STDIN_FILENO; // "0" nolu betimleyici izlenecektir. cb.aio_offset = 0; char buf[BUFFER_SIZE + 1]; // Tampon bölge. cb.aio_buf = buf; cb.aio_nbytes = BUFFER_SIZE; cb.aio_reqprio = 0; cb.aio_sigevent.sigev_notify = SIGEV_THREAD; // "thread" kullanılacaktır. cb.aio_sigevent.sigev_value.sival_ptr = &cb; // Bilgi olarak "cb" nesnesinin kendisini kullanacağız. cb.aio_sigevent.sigev_notify_function = io_proc; // "thread" tarafından kullanılacak fonksiyon. cb.aio_sigevent.sigev_notify_attributes = NULL; // Artık arka planda "0" nolu betimleyici "read" amacıyla izlenecektir. // Yani asenkron bir biçimde. Eğer herhangi bir şey yazılırsa, bize // bildirilecektir. if (aio_read(&cb) == -1) exit_sys("aio_read"); // Fakat "main" akış buraya geçecektir. printf("waiting at pause, press Ctrl+C to exit...\n"); // Bu örnek nezdinde "main" akışı bekletiyoruz fakat // başka şeyler de yapabilirdik. pause(); return 0; } void io_proc(union sigval sval) { // Bize iliştirilen nesneye ulaşmış olduk. Eğer // nesnemiz "global" olsaydı, bu dönüşüme gerek // kalmayacaktır. struct aiocb *cb = (struct aiocb *)sval.sival_ptr; ssize_t result; if ((result = aio_return(cb)) == -1) exit_sys("aio_return"); // Arka plandaki mekanizma sonlandırılmıştır. if (result == 0) { printf("Ctrl+d pressed...\n"); // Bu örnek nezdinde betimleyiciyi kapatmak istemeyebiliriz. // close(cb->aio_fildes); return; } // Bize iliştirilen nesnenin tampon bölgesinde // ulaşmış olduk. Artık onu işleyebiliriz. // Eğer tampon bölgesi "global" olsaydı, bu şekilde // bir dönüşüme gerek kalmayacaktır. char *buf = (char *)cb->aio_buf; buf[result] = '\0'; printf("IO occured: %s", buf); // Mekanizmayı tekrardan kuruyoruz. Aksi halde // bir defalık çalıştırılmış olacaktı. if (aio_read(cb) == -1) exit_sys("aio_read"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0, Aşağıda ise "malloc" kullanılarak tahsilat yapılmıştır. Artık boru mekanizması kullanılmıştır. Dolayısıyla ilk başta üç adet "x", "y" ve "z" isimli boruyu "mkfifo x y z" kabuk komutu ile oluşturmalıyız. Daha sonra üç adet daha "shell" programı çalıştırıp, her birinde sırasıyla, "cat > x", "cat > y" ve "cat > z" komutlarını çalıştırmalıyız. Bu aşamadan sonra yeni oluşturduğumuz üç terminal programından gönderdiklerimizi kendi programımızın ekranından görebiliriz. #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 volatile atomic_int g_count; void io_proc(union sigval sval); void exit_sys(const char *msg); int main(int argc, char** argv) { /* # Command Line Arguments # x y z */ /* # OUTPUT # x opened... y opened... z opened... Waiting @pause: Press Ctrl+C to exit. :> */ if (argc == 1) { fprintf(stderr, "too few arguments!..\n"); exit(EXIT_FAILURE); } printf("Opening named pipes...It may cause block...\n"); int fd; struct aiocb* cb; for (int i = 1; i < argc; ++i) { if (g_count > MAX_SIZE) { fprintf(stderr, "too many arguments! last ones ignored!..\n"); break; } if ((fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); printf("%s opened\n", argv[i]); if ((cb = (struct aiocb*)malloc(sizeof(struct aiocb))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } cb->aio_fildes = fd; cb->aio_offset = 0; if ((cb->aio_buf = malloc(BUFFER_SIZE + 1)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } cb->aio_nbytes = BUFFER_SIZE; cb->aio_reqprio = 0; cb->aio_sigevent.sigev_notify = SIGEV_THREAD; cb->aio_sigevent.sigev_value.sival_ptr = cb; cb->aio_sigevent.sigev_notify_function = io_proc; cb->aio_sigevent.sigev_notify_attributes = NULL; if (aio_read(cb) == -1) exit_sys("aio_read); ++g_count; } printf("Waiting @pause: Press Ctrl+C to exit.\n"); pause(); return 0; } void io_proc(union sigval sval) { struct aiocb *cb = (struct aiocb *)sval.sival_ptr; ssize_t result; char *buf = (char *)cb->aio_buf; if ((result = aio_return(cb)) == -1) exit_sys("aio_return"); if (result == 0) { printf("pipe closed...\n"); close(cb->aio_fildes); free(buf); free(cb); --g_count; if (g_count == 0) { // Kendimize "SIGINT" sinyali gönderiyoruz. // Ana akış "pause" dan çıkacaktır. if (raise(SIGINT) != 0) exit_sys("raise"); } return; } buf[result] = '\0'; printf("IO occured: %s", buf); if (aio_read(cb) == -1) exit_sys("aio_read"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.1, Yukarıdaki örnekte iki defa "malloc" ile dinamik bellek tahsisatı gerçekleştiriyorduk. İşte böyle yapmak yerine, "aiocb" yapısı ile bu yapının elemanı tarafından gösterilen alanı birleştirip, tek hamlede tek tahsilat yapabiliriz. #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 #define MAX_SIZE 128 typedef struct { struct aiocb cb; char buf[BUFFER_SIZE + 1]; } IOCB_INFO; void io_proc(union sigval sval); void exit_sys(const char *msg); volatile atomic_int g_count; int main(int argc, char *argv[]) { /* # Command Line Arguments # x y z */ if (argc == 1) { fprintf(stderr, "too few arguments!...\n"); exit(EXIT_FAILURE); } printf("opens named pipes... it may block...\n"); g_count = 0; IOCB_INFO *ioinfo; int fd; for (int i = 1; i < argc; ++i) { if (g_count >= MAX_SIZE) { fprintf(stderr, "too many arguments, last arguments ignored!...\n"); break; } if ((fd = open(argv[i], O_RDONLY)) == -1) exit_sys("open"); printf("%s opened...\n", argv[i]); if ((ioinfo = (IOCB_INFO *)malloc(sizeof(IOCB_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ioinfo->cb.aio_fildes = fd; ioinfo->cb.aio_offset = 0; ioinfo->cb.aio_buf = ioinfo->buf; ioinfo->cb.aio_nbytes = BUFFER_SIZE; ioinfo->cb.aio_reqprio = 0; ioinfo->cb.aio_sigevent.sigev_notify = SIGEV_THREAD; ioinfo->cb.aio_sigevent.sigev_value.sival_ptr = ioinfo; ioinfo->cb.aio_sigevent.sigev_notify_function = io_proc; ioinfo->cb.aio_sigevent.sigev_notify_attributes = NULL; if (aio_read(&ioinfo->cb) == -1) exit_sys("aio_read"); ++g_count; } printf("waiting at pause, press Ctrl+C to exit...\n"); pause(); free(ioinfo); return 0; } void io_proc(union sigval sval) { ssize_t result; IOCB_INFO *ioinfo = (IOCB_INFO *)sval.sival_ptr; if ((result = aio_return(&ioinfo->cb)) == -1) exit_sys("aio_return"); if (result == 0) { printf("pipe closed...\n"); close(ioinfo->cb.aio_fildes); free(ioinfo); --g_count; if (g_count == 0 && raise(SIGINT) != 0) return; } ioinfo->buf[result] = '\0'; printf("%s", ioinfo->buf); if (aio_read(&ioinfo->cb) == -1) exit_sys("aio_read"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Şimdiye kadar görmüş olduğumuz IO modellerinin kullanımları ve performansları hakkında şunları söyleyebiliriz: -> "'Multiplexed IO' using 'select' and 'poll'" ve "Asynchronous IO" modelleri POSIX standartlarında bulunan taşınabilir modellerdir. -> "'Multiplexed IO' using 'epoll'" ve "Signal Driven IO" modeli ise Linux sistemlerine özgüdür. Dolayısıyla taşınabilir değildir. -> Linux sistemlerinde performansı en yüksek model "'Multiplexed IO' using 'epoll'" modeli olup, perfomans sıralaması iyiden kötüye doğru "'Multiplexed IO' using 'epoll'" > "Signal Driven IO" == "Asynchronous IO" > "'Multiplexed IO' using 'select' and 'poll'" şeklindedir. >> "Scatter-Gather IO" : Bu modül, diğer modüllere nazaran daha az karmaşıktır. Peşisıra birden fazla "read" işlemi yapmak veya birden fazla kaynaktan peşpeşe "write" işlemi yapmak isteyelim. Dolayısıyla ilgili işler için peşpeşe "read"/"write" fonksiyonlarını çağırmamız gerekir. İşte böyle yapmak yerine bilgilerimizi önce bir "buffer" alanında biriktirip, bu "buffer" alanını kullanarak "read"/"write" işlemi yaparsak, daha az "kernel mode" a "switch" işlemi yapılacağından, zamandan tasarruf etmiş oluruz. İşte bu işlevi gören "readv" ve "writev" POSIX fonksiyonları geliştirilmiştir. Fonksiyonların prototipleri aşağıdaki gibidir. #include ssize_t readv(int fildes, const struct iovec *iov, int iovcnt); ssize_t writev(int fildes, const struct iovec *iov, int iovcnt); Fonksiyonların birinci parametresi "read"/"write" işleminin yapılacağı dosya betimleyicisini, ikinci parametresi elemanları "iovec" yapı türünden olan dizinin başlangıç adresini ve üçüncü parametre ise iş bu dizinin uzunluğunu belirtir. Fonksiyonlar başarı durumunda "read"/"write" yapılan toplam "byte" miktarına, hata durumunda ise "-1" değerine geri döner ve "errno" değişkeni uygun değere çekilir. Bu fonksiyonlar ile yapılan okuma/yazma işlemleri de atomik bir biçimde gerçekleştirilmektedir. "iovec" yapısı aşağıdaki gibi tanımlanmıştır. struct iovec { void *iov_base; // Base address of a memory region for input or output. size_t iov_len; // The size of the memory pointed to by iov_base. }; Yapının "iov_base" isimli elemanı tampon bölge olarak kullanılacak alanın başlangıç adresini, "iov_len" ise tampon bölgenin uzunluğunu belirtir. Programcı bu fonksiyonları kullanırken, fonksiyonların ikinci elemanına geçmek için bir yapı dizisi oluşturur ve içini doldururuz. Şimdi de aşağıdaki örnekleri inceleyelim: * Örnek 1.0, Aşağıda "writev" kullanımına ilişkin örnek verilmiştir. #include #include #include #include #include #include #include #define BUFFER_SIZE 10 void exit_sys(const char *msg); int main(void) { /* # test.txt # aaaaaaaaaabbbbbbbbbbcccccccccc */ int fd; if ((fd = open("test.txt", O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); char *buf1[BUFFER_SIZE]; memset(buf1, 'a', BUFFER_SIZE * sizeof('a')); char *buf2[BUFFER_SIZE]; memset(buf2, 'b', BUFFER_SIZE * sizeof('a')); char *buf3[BUFFER_SIZE]; memset(buf3, 'c', BUFFER_SIZE * sizeof('a')); struct iovec vec[3]; vec[0].iov_base = buf1; vec[0].iov_len = BUFFER_SIZE; vec[1].iov_base = buf2; vec[1].iov_len = BUFFER_SIZE; vec[2].iov_base = buf3; vec[2].iov_len = BUFFER_SIZE; if (writev(fd, vec, 3) == -1) exit_sys("writev"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, Aşağıda "readv" kullanımına ilişkin örnek verilmiştir. #include #include #include #include #include #include #include #define BUFFER_SIZE 10 void exit_sys(const char *msg); int main(void) { /* # test.txt # aaaaaaaaaabbbbbbbbbbcccccccccc */ /* # OUTPUT # aaaaaaaaaabbbbbbbbbbcccccccccc */ int fd; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); struct iovec vec[3]; char *buf1[BUFFER_SIZE]; vec[0].iov_base = buf1; vec[0].iov_len = BUFFER_SIZE; char *buf2[BUFFER_SIZE]; vec[1].iov_base = buf2; vec[1].iov_len = BUFFER_SIZE; char *buf3[BUFFER_SIZE]; vec[2].iov_base = buf3; vec[2].iov_len = BUFFER_SIZE; if (readv(fd, vec, 3) == -1) exit_sys("writev"); write(1, buf1, BUFFER_SIZE); write(1, buf2, BUFFER_SIZE); write(1, buf3, BUFFER_SIZE); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > UNIX ve türevi sistemlerde "Daemon(Servis)" Programlarının Yazımı: Genellikle "User-mode" da çalışmak üzere geliştirilen programlardır. Arka planda sessiz sedasız çalışırlar. İsimleri son ek olarak "-d" harfini alır. Eğer başında da "-k" harfi varsa "kernel" a aittirler. "ps -e" komutu ile bunları görebiliriz. Bu programlar kullanıcılar ile terminal programı üzerinden etkileşime girmezler, bir "User Interface" kavramına sahip değildir. Bu programlar da pekala istenildiği zaman başlatılabilir, istenildiği zaman sonlandırılabilir. Bu tip programların çalıştırılması "sudo" ile yapılması da uygundur. Aslında bu tip programlar UNIX ve türevi sistemlerde "init package" içerisindeki bir takım özel "utility" tarafından başlatılmakta, sürdürülmekte ve sonlandırılmaktadır. Yani ilgili dağıtımlar, bu tür programları idare edebilmek için, özel komutlar bulundurabilmektedir. >> "init packages" : Yaygın olarak kullanılan "init package" türleri şunlardır; "sysvinit", "Upstart" ve "systemd". Bu paketler çeşitli proje grupları tarafından oluşturulmuşlardır. Bu paketlerden, >>> "sysvinit": Klasik System5'teki işlevleri yapan init paketidir. Linux uzun bir süre bu paketi kullanmıştır. Bu paketler ismine "run level" denilen bir "boot" seçeneği barındırırlar. Böylece sistem "boot" edilirken sadece ihtiyaç duyulan "Daemon" programları çalıştırmaktadır. >>> "Upstart" : 2006 yılında oluşturulmuştur ve 2010'ların ortalarına kadar (bazı dağıtımlarda hala) kullanılmaya yaygın biçimde kullanılmıştır. Artık geliştirilmesi durdurulmuştur. Fakat hala kullanılıyor olabilmektedir. Yine bu paket de "run level" özelliğini kullanır. >>> "systemd" : 2010 yılında oluşturulmuştur ve son yıllarda pek çok Linux dağıtımında kullanılmaya başlanmıştır. Bugün en yaygın kullanılan init paketi durumundadır. İçeriğinde "boot" sonrasında devreye girecek programlardan ve bazı komutlar barındırmaktadır. Komutlar genellikle son ek olarak "-ctl" ekini alır. Paketin en önemli komutu "systemctl" isimli komuttur. Çünkü bu komut adeta "systemd" paketinin ön yüzü konumundadır ve servis yönetim işleri temel olarak bu komut yoluyla yapılmaktadır. Bu paketteki diğer komutlar ise "/lib/systemd" dizini içerisinde yer alır. Öte yandan "systemd" paketi çalışmaya başladığında çeşitli konfigürasyon dosyalarına başvurmaktadır. Bu konfigürasyon dosyaları "/etc/systemd" dizininde bulunmaktadır. Bu dizinde yer alan ".conf" uzantılı dosyaların içeriği genel olarak "değişken=değer" çifti şeklindedir fakat yorum satırı halindedir. Sistem yöneticisi ilgili satırı yorum satırı olmaktan çıkartarak, o özelliği kullanabilir. Diğer yandan kendi "Daemon" programımızın "systemd" kontrolüne girmesini istiyorsak, ismine "unit file" denilen, bir dosya daha oluşturmamız gerekmektedir. Aslında sistem genelinde çeşitli amaçlarla oluşturulan başka "unit file" lar da mevcuttur. Bu dosyalar ve uzantıları ise aşağıdaki gibidir. Dosya İsmi Uzantısı "Service Unit File" ".service" "Socket Unit File" ".socket " "Slice Unit File" ".slice" "Mount Unit File" ".mount" "Automount Unit File" ".automount" "Target Unit File" ".target " "Timer Unit File" ".timer " "Path Unit File" ".path" "Swap Unit File" ".swap" Tipik olarak biz programcılar, kendi "Daemon" programlarımız için, ".service" uzantılı "Service Unit File" oluştururuz. Bu "unit file" lar ise tipik olarak "/lib/systemd/system" dizini içerisinde oluşturulur. Fakat varsayılan senaryoda sırasıyla kontrol edilecek dizinler ise aşağıdaki gibidir. "/lib/systemd/system" "/etc/systemd/system" "/run/systemd/system" "/usr/lib/systemd/system" "/usr/local/lib/systemd/system" Bu pakette de artık "run level" seçeneği yerine "Target Unit File" kullanılır. Dolayısıyla "run level" ismi yerine "target unit" ismi kullanılır. Konunun detaylarına "Linux Service Management Made Easy with systemd by Donald A. Tevault" kitabından bakabiliriz. Pekiyi bizler kendi "Daemon" programlarımız için "Service Unit File" nasıl oluştururuz? Minimalist bir biçimde ilgili dosya aşağıdaki içeriklere sahip olur. # mydaemon.service [Unit] Description=Mydaemon Unit [Service] Type=forking ExecStart=/usr/bin/mydaemond [Install] WantedBy=multi-user.target Buradaki, -> "#" işareti, o satırı yorum satırı haline getirir. -> "Description", bizim "unit file" ı temsil eden bir yazıdır. -> "Type", bizim "Daemon" programımızın nasıl çalıştırılacağını belirtir. Buradaki "forking", normal fork mekanizmasıyla çalıştırma, anlamına gelmektedir. -> "ExecStart", bizim "Daemon" programımızın nerede olduğunu belirtmektedir. "Daemon" programlarımızı tipik olarak "/usr/bin" dizini ya da "usr/local/bin" içerisinde bulundurmalıyız. -> "WantedBy", hangi "Target Unit File" ile sistem "boot" edildiğinde bizim "Daemon" programının yükleneceğini belirtir. Buradaki "multi-user.target" isimli "Target Unit File", klasik ve tipik olan, boot işlemini belirtmektedir. Biz böylece minimalist bir ".service" dosyası oluşturduk. Dökümantasyon için "man systemd.service" sayfasına başvurmalıyız. Pekiyi servis yönetimini nasıl gerçekleştireceğiz? Bunun için "systemctl" komutu bulundurulmuştur. Bu komutu ise, "systemctl enable my_daemon" biçiminde kullanabiliriz. Buradaki, -> "my_daemon" ise bizim "Daemon" programımızın ismini belirtmektedir. Spesifik olarak uzantıyı da yazmamıza gerek yoktur. -> "enable" ise "Daemon" programımızın bir sonraki "boot" işleminden sonra başlatılacağını belirtmektedir. Eğer hemen şimdi başlatılmasını istiyorsak, "--now" seçeneğini de kullanmalıyız. Örneğin, "systemctl enable my_daemon --now". Bu komuta ilişkin diğer seçenekler ise şunlardır; -> "disable" : "Daemon" programımızın "boot "sırasında devreye sokulmasını istemiyorsak bu komutu kullanmalıyız. Bu seçeneğin kullanım biçimi, "systemctl disable my_daemon" biçimindedir. -> "start" : "Daemon" programı çalıştırmak için kullanılır. Örneğin, "stop" durumdakini tekrar çalıştırmak için, "systemctl start my_daemon" komutunu kullanabiliriz. -> "stop" : "Daemon" programı durdurmak için kullanılır. Bu seçeneğin kullanım biçimi, "systemctl stop my_daemon" şeklindedir. Durdurma işlemi sırasında varsayılan ayarlarda "SIGTERM" sinyali gönderilir, "systemd" paketi tarafından. Eğer "Daemon" program hala sonlanmamışsa, bu sefer ona "SIGKILL" sinyali gönderilir. -> "status" : "Daemon" programımızın durumunu öğrenmek için kullanılır. Bu seçeneğin kullanım biçimi, "systemctl status [my_daemon]" biçimindedir. Eğer herhangi bir "Daemon" program ismi vermezsek, bütün "Daemon" programlar gösterilecektir. Bu komut için "sudo" dememize gerek yoktur. -> "is-enabled" : "Daemon" programımızın "boot" zamanında devreye girip girmeyeceğini, "status" komutunun yanı sıra, bu komut ile de anlayabiliriz. Komutun kullanım biçimi, "systemctl is-enabled my_daemon" biçimindedir. -> "restart" : "Daemon" programımızı önce durdurur, sonra tekrar çalıştırır. Bu seçeneğin kullanım biçimi, "systemctl restart my_daemon" şeklindedir. -> "reload" : Bizler bir "unit" dosyası üzerinde değişiklik yaptığımızda, "systemd" paketi bütün "unit file" ları inceleyerek bir ağaç yapısı oluşturduğundan ve bu yapının da güncellemeden dolayı yeniden oluşturulması gerektiğinden, bu komutu çağırmalıyız. Bu seçeneğin kullanım biçimi, "systemctl daemon-reload" şeklindedir. Bu komutun da parametresiz olduğuna dikkat ediniz. Diğer yandan "systemd" paketlerinde; bir alt prosesin üst prosesi sonlandığında, onun üst prosesi artık "init" prosesi OLMAZ. Bu pakete göre üst proses artık "systemd" OLUR. Sistemimizde hangi "init" paketinin kullanıldığını öğrenmek için, sudo ls -l /proc/1/exe komutunu çalıştırabiliriz. Dolayısıyla ekrana çıkan, lrwxrwxrwx 1 root root 0 Kas 11 11:45 /proc/1/exe -> /usr/lib/systemd/systemd çıktı incelendiğinde görüleceği üzere sistemimizde "systemd" paketi kullanılmaktadır. Yine benzer şekilde, cat /proc/1/status komutunu ya da ps -p 1 (ya da "ps -p 1 -o comm=") komutunu çalıştırarak da öğrenebiliriz. Bir "Daemon" program yazabilmek için öncelikle onun terminal programı ile kesilmesi gerekmektedir. Komut satırından "&" ile bir programı çalıştırmak, onun terminal ile BAĞLANTISINI KESMEYECEKTİR. Bir "Daemon" programının yazılması, şu aşamalardan geçilerek mümkündür: -> İlk adım olarak; bu tür servis programları, bir dosya açmak istediklerinde, tam olarak belirlenen haklarla bunu gerçekleştirebilmelidir. Dolayısıyla ilk olarak bu tip programların "umask" değerini sıfıra çekiyoruz. Şöyleki; umask(0); -> İkinci adım olarak; terminalle olan ilişkimizin sonlandırılması gerekmektedir. Bunu "setsid" fonksiyon çağrısı ile gerçekleştirebiliriz. Anımsanacağı üzere bu fonksiyon çağrıldığında yeni bir Oturum ve yeni bir proses grubu oluşturmakta, prosesimizi de bu proses grubunun ve Oturumun lideri yapmaktaydı. Ancak biz kendi prosesimizi "shell" programı üzerinden çağırdığımız için, aslında prosesimiz için halihazırda bir proses grubu oluşturulacak ve bizler de grubun lideri yapılacağız. Dolayısıyla bu noktada "setsid" çağrısı başarısız olacaktır. Çünkü çağrının başarılı olabilmesi için o prosesin herhangi bir proses grup lideri olmaması gerekir. Bu problemi çözmek için, "setsid" çağrısından evvel, bir kez "fork" yapıp üst prosesimizi sonlandırmamız gerekecektir. Şöyleki; pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); Bu aşamada kalırsak, prosesimizin bulunduğu proses grubu Öksüz Proses Grubu olacaktır. Artık "setsid" çağrısını yapabiliriz. Şöyleki; if (setsid() == -1) _exit(EXIT_FAILURE); -> Üçüncü adım olarak; "Daemon" programların çalışma dizinlerinin ("Current Working Directory") sağlam bir dizin olması tavsiye edilir. Aksi takdirde o dizin silinirse programımızın çalışması bozulabilir. Bu nedenle "Daemon" programların çoğu Kök Dizin'i (silinemeyeceği için) çalışma dizini yapmaktadır. Tabii bu zorunlu değildir. Kök Dizin yerine, varlığı garanti edilmiş, herhangi bir dizin de çalışma dizini yapılabilir. Şöyleki: if (chdir("/") == -1) _exit(EXIT_FAILURE); -> Dördüncü adım olarak; Bir sonraki aşamada o ana kadar açılmış olan bütün betimleyicileri kapatması gerekmektedir. Buna "0", "1" ve "2" numaralı betimleyiciler de dahildir. Her ne kadar terminal ile olan ilişkimizi sonlandırsak bile prosesimiz "0", "1" ve "2" numaralı betimleyicileri hala kullanabilir. Dolayısıyla terminale hala bir şeyler yazabiliriz. O ana kadar açık olan betimleyicileri kapatmanın kolay bir yolu olmadığından, ilk olarak dosya betimleyici tablosunun uzunluğunu elde etmeli ve bu tablodakileri sırasıyla kapatmalıyız. Halihazırda kapalı olanlara tekrar kapatma işleminin uygulanması programımızın çökmesine neden OLMAYACAKTIR. İlgili tablonun toplam uzunluğunu da "sysconf" çağrısında "_SC_OPEN_MAX" sembolik sabitini kullanarak ya da "getrlimit" çağrısında "RLIMIT_NOFILE" sembolik sabiti kullanarak elde edebiliriz. Şöyleki; long maxfd; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); -> Beşinci adım olarak; Her ne kadar açık olan betimleyicilerin hepsinin kapatılması önerilse de, "0", "1" ve "2" numaralı betimleyicilerin "/dev/null" aygıtına yönlendirilmesi daha uygun olur. Çünkü her ne kadar bizim "Daemon" programımız betimleyicileri kullanmasa bile çağıracağı fonksiyonlar bu betimleyicileri kullanabilir. Dolayısıyla o fonksiyonlar probleme yol açabilir. Bunu da şöyle gerçekleştirebiliriz; int fd; if ((fd = open("/dev/null", O_RDONLY)) == -1) // fd is guaranteed to be "0" _exit(EXIT_FAILURE); if (dup(fd) == -1 || dup(fd) == -1) // now descriptor "1" and "2" redirected to "/dev/null" _exit(EXIT_FAILURE); -> Altıncı adım olarak(opsiyonel); "Daemon" programlar çoğu zaman tek kopya halinde çalıştırılır. Yani aynı "Daemon" programının birden fazla kez çalıştırılması sorunlara yol açabilmektedir. Bunun gerçekleştirimini dosya kilitleme bölümünde Bütünsel Kilitleme başlığında işlemiştik. (kilit vurulacak dosya genel olarak "/run" dizini içerisinde, ".pid" uzantısı ile oluşturulur. Bu dizinde işlem yapabilmek için prosesimizin uygun önceliğe sahip olması gerekir.) -> Yedinci adım olarak; "Daemon" haline getirilmiş programı sonlandırmak için tipik olarak "SIGTERM" sinyali kullanılır. Çünkü bu sinyal, "SIGKILL" sinyaline nazaran, "handler" edilebilir bir sinyaldir. İşte "Daemon" programımız bir takım sonlandırma işlemleri de yapacaksa, bunu "SIGTERM" sinyalinin işlendiği sırada gerçekleştirebilir. Zaten işletim sistemleri kapanırken yine "SIGTERM" sinyali göndereceğinden, en kötü olasılıkla, sonlandırma işlemleri yine o noktada yapılacaktır eğer ilgili sinyal "set" edilmişse. Burada sinyalin "set" işleminin "Daemon" oluşturma işleminden önce de yapabiliriz, sonra da. Diğer yandan "Daemon" program oluştururken şu husulara da dikkat etmeliyiz: -> Bazı işletim sistemleri "SIGTERM" sinyalinin gönderiminden beş saniye sonra "SIGKILL" sinyali gönderdiğinden, sonlandırma işlemlerinin yapılması yavaş olmayacak biçimde, "SIGTERM" sinyalinin işlenmesi sırasında beklenir. -> "Daemon" programlar başlatılırken bir takım "configuration" dosyalarındaki yönergelere bakılarak başlatılmaktadır. Bu konfigürasyon dosyaları ise genelde "/etc" dizini içerisinde, ".conf" uzantısı ile bulunurlar. İş bu konfigürasyon dosyalarının okunma sırası "Daemon" oluşturmadan evvel olabileceği gibi "Daemon" oluşumundan sonra da olabilir. -> Terminalle ilişkimiz koptuğu için hata mesajlarının yazımına dikkat etmeliyiz. Bunun için genellikle "log" dosyaları kullanılır ki bu konuya ileride değineceğiz. -> "Daemon" programların "reinit." edilmesi de sık karşılaşılan bir durumdur. Yani konfigürasyon dosyasını okuyup çalışan "Daemon" programının, ilgili konfigürasyon dosyasını güncelledikten sonra, konfigürasyon dosyasını yeniden okuyup çalışmasını sağlatabiliriz. Genel olarak bu işlem için "SIGHUP" sinyalini kullanırız. Her ne kadar bu sinyal genel olarak terminal aygıt sürücüsü, "shell" programları tarafından kullanılıyor olsa bile, bizim programımızın terminal ile ilişkisi kesildiğinden "SIGHUP" sinyali "Daemon" programlar için ıskarta edilmiş bir sinyal durumundadır. Şimdi de bu yazılanları anlatan "Daemon" program(lar) oluşturalım: * Örnek 1.0, Bu örneği bir şablon program olarak da kullanabiliriz. #include #include #include #include #include #include #include #include #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" void sigterm_handler(int signo); void exit_sys(const char *msg); int main(void) { // Yedinci Adım: struct sigaction sa; sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) exit_sys("sigaction"); // Altıncı Adım(opsiyonel): int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) { /* log mekanizması ile mesaj oluşturulabilir */ _exit(EXIT_FAILURE); } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* log mekanizması ile mesaj oluşturulabilir */ } else { /* log mekanizması ile mesaj oluşturulabilir */ } _exit(EXIT_FAILURE); } // Birinci Adım: Prosesin "umask" değerini sıfıra çekiyoruz. umask(0); // İkinci Adım: Yeni bir Oturum ve Proses Grubu oluşturmadan evvel uygun şartları sağlıyoruz. pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); // İkinci Adım: Artık yeni bir Oturum ve Proses Grubu oluşturabiliriz. if (setsid() == -1) _exit(EXIT_FAILURE); // Üçüncü Adım: "Current Working Directory" konumunu silinmeyeceğinden emin olduğumuz bir dizin olarak belirliyoruz. if (chdir("/") == -1) _exit(EXIT_FAILURE); // Dördüncü Adım: Açık olan dosya betimleyicilerinin hepsini kapatıyoruz. errno = 0; long maxfd; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) // Eğer tablonun uzunluk bilgisi tanımlı değilse, fonksiyon "-1" ile geri döner... if (errno == 0) //...fakat "errno" değerini değiştirmez... maxfd = DEF_FDT_SIZE; //...Bu senaryo için biz bir uzunluk tayin ediyoruz. else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); // Beşinci Adım: İlk üç betimleyiciyi "/dev/null" aygıtına yönlendiriyoruz. int fd0, fd1, fd2; if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); // Artık "Daemon" programımız kendi işini görecektir. pause(); return 0; } void sigterm_handler(int signo) { /* cleanup processing */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 1.1, Pekala yukarıdaki şablon programı fonksiyonlar oluşturarak da kullanabiliriz. #include #include #include #include #include #include #include #include #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" void sigterm_handler(int signo); void end_daemon(void); void check_instance(void); void make_daemon(void); void exit_sys(const char *msg); int main(void) { check_instance(); // Altıncı Adım(opsiyonel) make_daemon(); // Birinci,..., Beşinci Adım end_daemon(); // Yedinci Adım pause(); return 0; } void sigterm_handler(int signo) { /* cleanup processing */ } void end_daemon(void) { struct sigaction sa; sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) exit_sys("sigaction"); } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) { /* log mekanizması ile mesaj oluşturulabilir */ _exit(EXIT_FAILURE); } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* log mekanizması ile mesaj oluşturulabilir */ } else { /* log mekanizması ile mesaj oluşturulabilir */ } _exit(EXIT_FAILURE); } } void make_daemon(void) { umask(0); pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; long maxfd; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); int fd0, fd1, fd2; if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, #include #include #include #include #include #include #include #include void read_config(void); void check_instance(void); void sigterm_handler(int signo); void sighup_handler(int signo); void exit_sys(const char *msg); #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" #define CONFIG_FILE "/etc/mydaemond.conf" void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) _exit(EXIT_FAILURE); if (chdir("/") == -1) _exit(EXIT_FAILURE); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else _exit(EXIT_FAILURE); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) _exit(EXIT_FAILURE); if ((fd1 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if ((fd2 = dup(fd0)) == -1) _exit(EXIT_FAILURE); if (fd0 != 0 || fd1 != 1 || fd2 != 2) _exit(EXIT_FAILURE); } int main(void) { struct sigaction sa; int fd; make_daemon(); check_instance(); read_config(); sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) { /* log mekanizması yoluyla hata mesajı oluşturulabilir */ _exit(EXIT_FAILURE); } sa.sa_handler = sighup_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGHUP, &sa, NULL) == -1) { /* log mekanizması yoluyla hata mesajı oluşturulabilir */ _exit(EXIT_FAILURE); } /* ... */ pause(); return 0; } void read_config(void) { /* ... */ } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) { /* log mekanizması ile mesaj oluşturulabilir */ _exit(EXIT_FAILURE); } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { /* log mekanizması ile mesaj oluşturulabilir */ } else { /* log mekanizması ile mesaj oluşturulabilir */ } _exit(EXIT_FAILURE); } } void sigterm_handler(int signo) { /* cleanup processing */ } void sighup_handler(int signo) { read_config(); /* read_config asenkron sinyal güvenli bir fonksiyon olmalı */ } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi yukarıda anlatılan "log" dosyası, bir diğer deyişle "log" mekanizması nedir? Terminal bağlantısı bulunmayan "Daemon" programlarının ya da "kernel" modüllerinde, hata veya diğer mesajların terminal ekranına yazdırılması, pek mümkün olmayabilir(ya da uygun olmayabilir). İşte böylesi programlar için bir takım mesajların kullanıcıya ulaştırılabilmesi için "log" mekanizması geliştirilmiştir. UNIX ve türevi sistemlerde "kernel" tarafından desteklenen kapsamlı bir "log" mekanizması mevcuttur. Böylece farklı kaynaklardan gelen mesajların biriktirilip saklanması mümkündür. Bu mekanizmaya genel olarak "syslog" ismi verilir ve gösterimine ise "The Linux Programming Environment by Michale Kerrisk" kitabından bakılabilir. Linux sistemlerindeki "syslog" mekanizmasını açıklayacak olursak; Bu mekanizmanın ana aktörü, yani üst seviye önemli bir bileşeni, "syslogd" isimli "Daemon" bir programdır. Bu program "user mode" da çalışır, birden fazla kaynaktan gelen mesajları alır, organize eder ve hedef dizine/dosyaya bunları yazar. Hedef dizin/dosya pekala değiştirilebilir. Bu programın dinlediği kaynaklar ise şunlardır: Yerel kullanımlar için "/dev" dizini içerisindeki "log" dosyası, uzak kullanımlar için "UDP 514" portu. Aslında buradaki "/dev/log" dosyasına iki farklı aktör yazma yapmaktadır. Bunlardan biri normal kullanıcılar, diğeri ise aygıt sürücüler ve "kernel" modüllerdir. Normal kullanıcılar "syslog" isimli POSIX fonksiyonunu kullanırken, aygıt sürücü ve "kernel" moduülleri ise "klogd" isimli "kernel mode" da çalışan bir "Daemon" programı kullanırlar. "user mod" da "log" işlemleri için üç farklı fonksiyon kullanılır. Bunlar "openlog", "syslog" ve "closelog" isimli POSIX fonksiyonlarıdır. Bu fonksiyonlardan, >> "openlog" : "log" mekanizmasını başlatır. Fonksiyonun prototipi aşağıdaki gibidir. #include void openlog(const char *ident, int logopt, int facility); Fonksiyonun birinci parametresi "log" mesajlarında görüntülenecek program ismini belirtir. Genellikle programın ismi bu parametreye argüman olarak geçilir. Linux sistemlerinde bu parametreye "NULL" değerinin geçilmesi, program isminin geçilmesi anlamına gelir. Ancak POSIX standartlarına göre "NULL" geçme senaryosu tanımlanmamıştır. Fonksiyonun ikinci parametresi ise, "LOG_PID", "LOG_CONS", "LOG_NDELAY", "LOG_ODELAY" ve "LOG_NOWAIT" sembolik sabitlerinin "bit-wise OR" işlemine sokulması ile elde edilen değeri alır. Bu sembolik sabitlerden, -> "LOG_PID" : "log" mesajında prosesin ID değerinin de ekleneceği anlamına gelir. -> "LOG_CONS" : "log" mesajlarının aynı zamanda "Default Console" ekranına da yazdırılacağı anlamına gelir("/dev/console"). -> "LOG_NDELAY" : "log" sisteminin hemen açılacağı anlamına gelir. -> "LOG_ODELAY" : "log" sisteminin ilk "log" işlemi yapıldığında açılacağı anlamına gelir. Varsayılan davranış budur. -> "LOG_NOWAIT" : "child process" söz konusu olduğunda, "log" işlemi için bu proseslerin hayata getirilmelerinin beklenmeyeceği anlamındadır. Pekala bu parametreye "0" da geçebiliriz. Ancak tipik olarak "LOG_PID" sabiti geçilir. Fonksiyonun üçüncü parametresi ise "log" mesajını yollayan prosesin kim olduğuna dair bilgi vermek için kullanılır. Bu parametreye, "LOG_USER", "LOG_KERN", "LOG_DAEMON", "LOG_LOCAL0...7" sembolik sabitlerinden birisini alır. Bu sembolik sabitlerden, -> "LOG_USER" : "log" mesajının normal prosesler tarafından gönderildiği anlamındadır. -> "LOG_KERN" : "log" mesajının "kernel" tarafından gönderildiği anlamındadır. Bu sembolik sabit POSIX'te mevcut DEĞİLDİR. -> "LOG_DAEMON" : "log" mesajının bir sistem "Daemon" programı tarafından gönderildiği anlamındadır. Bu sembolik sabit POSIX'te mevcut DEĞİLDİR. -> "LOG_LOCAL0...7" : "log" mesajının özel "log" kaynakları tarafından gönderildiği anlamındadır. Pekala bu parametreye "0" da geçebiliriz. Bu durumda "LOG_USER" geçilmiş gibi olur. Bu fonksiyonun başarısı kontrol edilememektedir. Başarısızlık durumu diğer "log" fonksiyonları ile anlaşılmaktadır. Bu fonksiyonun çağrılması mekanizmanın kullanılabilmesi için zorunlu DEĞİLDİR. Bu durumda barsayılan değerler kullanılacaktır. >> "syslog" : Bu fonksiyon "log" işlemlerini gerçekleştiren fonksiyondur. Asıl fonksiyon bu fonksiyondur. Fonksiyonun prototipi aşağıdaki gibidir. #include void syslog(int priority, const char *format, ...); Fonksiyonun birinci parametresi mesajın önem derecesini belirtir ve şu sembolik sabitlerden birisini alır: "LOG_EMERG", "LOG_ALERT", "LOG_CRIT", "LOG_ERR", "LOG_WARNING", "LOG_NOTICE", "LOG_INFO" ve "LOG_DEBUG" Bu sembolik sabitler de yine hata mesajlarında görünür olup, en çok kullanılanları şunlardır; -> "LOG_ERR" : Hata mesajları için kullanılır. -> "LOG_WARNING" : Uyarı mesajları için kullanılır. -> "LOG_INFO" : Genel bilgilendirme mahiyetinde kullanılır. Fonksiyonun diğer parametreleri "printf" fonksiyonunun parametreleri gibidir. Bu fonksiyonun da başarısı kontrol edilememektedir. Öte yandan "openlog" fonksiyonunun çağrılması bir zorunluluk olmadığından, "openlog" fonksiyonunun üçüncü parametresi ile bu fonksiyonun birinci parametresini kombine edebiliriz. >> "closelog" : "log" mekanizmasını sonlandırır, kapatır. Bu fonksiyon, eğer "log" mekanizması henüz başlatılmadıysa, bir şey yapmamaktadır. Fonksiyonun prototipi aşağıdaki gibidir. #include void closelog(void); Öte yandan "log" mekanizmasında kullanılan varsayılan hedef dizin, Linux sistemlerinde, "/var/log" dizininde bulunan "syslog" dosyasıdır. Bu dosya disk tabanlı olmadığından, içeriğindeki bilgiler "reboot" işlemi sonrası silinmektedir. Bu dosyanın büyüklüğü artabileceği için, arka planda kuyruk sistemine benzer bir mekanizma kullanılır. Dolayısıyla bir müddet sonra evvelce yazılmış mesajlar kaybolabilmektedir. Pekala bizler yazma işleminin yapılacağı hedef dizini de değiştirebiliriz. Yukarıda da belirtildiği üzere Linux sistemlerinde bu yazma işlemi için "syslogd" isimli "Daemon" program kullanılır. Fakat yeni Linux sürümlerinde "rsyslogd" isimli "Daemon" kullanılmaktadır. Bu iki "Daemon" program ise hedef dizinin ne olduğuna karar verirken sırasıyla "/etc/syslog.conf" ya da "/etc/rsyslog.conf" dosyalarına bakar. İşte bu dosyalarda bilgi bulamadığında, varsayılan dizin olan "/var/log/syslog" dosyasını seçer. Dolayısıyla hedef dizinin hangisi olmasını istiyorsak, "/etc/syslog.conf" ya da "/etc/rsyslog.conf" dosyalarında değişiklik yapmalıyız. Pekiyi bizler "log" dosyalarını nasıl inceleyeceğiz? Esasında, dosyanın sonunu görebilmek için "tail" kabuk komutunu kullanabiliriz. Anımsanacağı üzere bu komut, dosyanın sonundaki son on satırı gösterir. "-n" seçeneği ile daha fazla satırı da görüntüleyebiliriz. Bu komuta ek olarak bir takım "utility" araçlar da bulundurulmuştur. Örneğin lnav, glogg, ksystemlog gibi. Yine "systemd" isimli "init" paketindeki "systemctl" ve "journalctl" komutlarıyla da görüntüleme yapabiliriz. Şimdi de "log" mekanizmasına ilişkin örnekleri inceleyelim. * Örnek 1, "log" mekanizmasına ilişkin temel bir örnek. #include #include int main(void) { openlog("sample", LOG_PID, LOG_USER); syslog(LOG_INFO, "This is a test..."); closelog(); return 0; } * Örnek 2, Bir "Daemon" program içerisinde "log" mekanizmasının kullanımına ilişkin örnek. #include #include #include #include #include #include #include #include #include #include void read_config(void); void check_instance(void); void sigterm_handler(int signo); void sighup_handler(int signo); void exit_daemon(const char *msg); #define DEF_FDT_SIZE 4096 #define LOCK_FILE_PATH "/run/mydaemond.pid" #define CONFIG_FILE "/etc/mydaemond.conf" void make_daemon(void) { pid_t pid; long maxfd; int fd0, fd1, fd2; umask(0); if ((pid = fork()) == -1) exit_daemon("fork"); if (pid != 0) _exit(EXIT_SUCCESS); if (setsid() == -1) exit_daemon("setsid"); if (chdir("/") == -1) exit_daemon("chdir"); errno = 0; if ((maxfd = sysconf(_SC_OPEN_MAX)) == -1) if (errno == 0) maxfd = DEF_FDT_SIZE; else exit_daemon("sysconf"); for (long i = 0; i < maxfd; ++i) close(i); if ((fd0 = open("/dev/null", O_RDONLY)) == -1) exit_daemon("open"); if ((fd1 = dup(fd0)) == -1) exit_daemon("dup"); if ((fd2 = dup(fd0)) == -1) exit_daemon("dup"); if (fd0 != 0 || fd1 != 1 || fd2 != 2) { syslog(LOG_ERR, "invalid file descriptors"); _exit(EXIT_FAILURE); } } int main(void) { struct sigaction sa; int fd; openlog("mydaemond", LOG_PID, LOG_USER); make_daemon(); check_instance(); read_config(); sa.sa_handler = sigterm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGTERM, &sa, NULL) == -1) exit_daemon("sigaction"); sa.sa_handler = sighup_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGHUP, &sa, NULL) == -1) exit_daemon("sigaction"); syslog(LOG_INFO, "ok, daemon is running"); for (;;) pause(); closelog(); return 0; } void read_config(void) { syslog(LOG_INFO, "mydaemon is reading configuration file..."); } void check_instance(void) { int fd; if ((fd = open(LOCK_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) exit_daemon("open"); if (flock(fd, LOCK_EX|LOCK_NB) == -1) { if (errno == EWOULDBLOCK) { syslog(LOG_ERR, "Only one instance of this daemon can be run..."); _exit(EXIT_FAILURE); } exit_daemon("flock"); } } void sigterm_handler(int signo) { syslog(LOG_INFO, "mydaemond is terminating..."); _exit(EXIT_SUCCESS); } void sighup_handler(int signo) { syslog(LOG_INFO, "mydaemond got SIGHUP and read config file..."); read_config(); /* read_config asenkron sinyal güvenli bir fonksiyon olmalı */ } void exit_daemon(const char *msg) { syslog(LOG_ERR, "%s: %s", msg, strerror(errno)); _exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> GUI üzerinden başlatılan terminal programına sahte ("pseudo") terminal, oturum açma ekranında "switch" yaptığımız terminal ise gerçek terminal olarak geçer. Fakat işlevsel olarak aralarında bir farklılık yoktur. >> "forground/background" çalışmayı organize eden terminallere ise "job controlling terminal" adı verilir. >> UTC saat biçimi dünyanın her yerinde aynıdır. Türkiye'nin yerel saati UTC biçimine göre, "day-light saving" durumuna bağlı olarak ya "+2" ya da "+3" formundadır. >> "shell" programının kaynak limitleri "ulimit" komutuyla görüntülenmektedir. Bu komut da "cd" gibi içsel bir komuttur. Bu komutu "-a" ile kullanırsak bütün "soft-limit" değerlerini görüntülemiş oluruz. Bütün "hard-limit" değerleri için "-H -a" seçeneğini kullanmamız gerekmektedir. Limitlerde değişiklik yapmak için limite ilişkin seçenekleri kullanmalıyız. Örneğin, "ulimit -n 5000" komutuyla hem "soft-limit" hem de "hard-limit" değiştirilir. Eğer yalnızca "soft-limit" değerini istiyorsak "-S", yalnızca "hard-limit" değerini istiyorsak "-H" seçeneklerini yazmalıyız. Tabii "hard-limit" değerini arttırma işlemi için prosesimizin uygun önceliğe sahip olması gerekmektedir. Fakat "ulimit" komutu içsel bir komut olduğundan "sudo" ile çalıştıramayız. Bu yüzden ilk başta "shell" programını "root" olarak çalıştırmalıyız ki kendisinin limit değerlerini arttırabilelim. >> UNIX ve türevi sistemlerde dosyanın yol ifadesinden hareketle o dosyanın "inode" numarasını elde edebiliriz; "stat", "fstat", "lstat" fonksiyonlarıyla. Ancak bu işlemin tersini gerçekleştirebileceğiz bir mekanizma mevcut DEĞİLDİR. Dolayısıyla "find" komutunu kullanarak arama yöntemiye yapılabilir. >> "select" fonksiyonu "BSD" sistemleri için tasarlanmışken, "poll" fonksiyonu "AT&T" sistemleri için tasarlanmış fakat daha sonra ikisi de POSIX çatısı altına alınmıştır. >> Boru oluşturduktan sonra "cat > x" komutunu çalıştırmamız, "cat" komutunun çıktılarının "x" borusuna yazılacağını belirtir. >> "epoll" fonksiyonu ile sistem genelinde izlenecek maksimum betimleyici sayısı "/proc/sys/fs/epoll/max_user_watches" dosyasında bulunmaktadır. Bu değer pekala değiştirilebilmektedir. >> "Ctrl + F + N" tuşlarına basarsak artık pencereli arayüzden siyah renkli terminale veya tam tersi yönde geçiş sağlarız. Örneğin, "Ctrl + F + 7" ile pencereli terminale geçiş yaparken "Ctrl + F + 5", "Ctrl + F + 6" ile siyah renkli terminale geçiş sağlarız. Buradaki pencereli terminallere genel olarak "pseudo terminals", siyah renkli olanlara ise "virtual terminals" adı verilir. /*================================================================================================================================*/ (97_12_11_2023) & (98_18_11_2023) & (99_19_11_2023) & (100_25_11_2023) & (101_26_11_2023) & (102_02_12_2023) & (103_03_12_2023) & (104_09_12_2023) & (105_10_12_2023) & (106_16_12_2023) & (107_17_12_2023) & (108_05_01_2024) & (109_07_01_2024) & (110_12_01_2024) & (111_14_01_2024) & (112_19_01_2024) & (113_21_01_2024) & (114_26_01_2024) & (115_28_01_2024) & (116_02_02_2024) & (117_04_02_2024) & (118_09_02_2024) & (119_11_02_2024) & > Prosesler Arası Haberleşme Yöntemleri (Interprocess Communication) (devam): >> Farklı makinede koşan prosesler arası haberleşme yöntemleri (devam): Bu haberleşme yönteminde ise prosesler farklı makinalarda koşmaktadır. Farklı makinelerin prosesleri arasında haberleşme (yani bir ağ içerisinde haberleşme), aynı makinenin prosesleri arasındaki haberleşmeye göre daha karmaşık unsurlar içermektedir. Çünkü burada ilgili işletim sisteminin dışında pek çok belirlemelerin de önceden yapılmış olması gerekir. İşte ağ haberleşmesinde önceden yapılan belirlemelere, daha doğrusu belirlenmiş kurallar topluluğuna, "protocol" denilmektedir. Ağ içerisindeki haberleşmedeki tarihsel süreç içerisinde pek çok protokol geliştirilmiş, bu protokolleri birleştirerek de Protokol Aileleri oluşturulmuştur. Bunların bazıları büyük şirketlerin kontrolü altında olup, hala kullanılmaktadır. Ancak açık bir protokol ailesi olan IP Protokol Ailesi, günümüzde farklı makinelerin prosesleri arasındaki haberleşmede, hemen her zaman tercih edilen bir protokoldür. Protokol Aileleri oluşturulurken protokoller, birbirinin üzerine eklenerek oluşturulduğu gibi, yalın halde de kullanılmış olabilir. Böylece Protokol Aileleri bünyelerinde hem katmanlı bir yapıya hem de tekil protokolleri barındırabilir. Buradaki katmanlı yapı oluşturulurken de üstteki gelen protokol, alttaki protokolün zaten var olduğu fikriyle ve yine alttaki protokol kullanılarak, katmanlı yapıya eklenmiştir. Aslında bu katmanlı yapıya prosedürel programlama tekniğinde "zaten var olan bir fonksiyonu kullanarak daha yüksek seviyeli bir fonksiyon yazmaya" benzetebiliriz. Yani Standart C Fonksiyonlarının POSIX/Windows Fonksiyonları kullanılarak oluşturulması gibi. Diğer yandan bu katmanlı yapının nasıl oluşturulması gerektiğine yönelik, "OSI Model (Open System Interconnection Model)" isimli, referans bir döküman da "ISO" tarafından 80'li yılların başında geliştirilmiştir. Bu referans kaynağa göre katmanlı bir yapı toplamda 7 katmandan oluşmalıdır. Bu katmanlar, yüksek seviyeden aşağı seviyeye doğru, aşağıdaki gibidir. "Uygulama Katmanı (Application Layer)" | "Sunum Katmanı (Presentation Layer)" | "Oturum Katmanı (Session Layer)" | "Aktarım Katmanı (Transort Layer)" | "Network Katmanı (Network Layer)" | "Veri Bağlantı Katmanı (Data Link Layer)" | "Fiziksel Katman (Physical Layer) " Bu katmanlardan, -> "Fiziksel Katman (Physical Layer)" : En aşağı seviyeli elektriksel tanımlamaların yapıldığı katmandır. Örneğin kabloların, konnektörlerin özellikleri vs. Yani bu katman, iletişim için gereken ortamı betimlemektedir. -> "Veri Bağlantı Katmanı (Data Link Layer)" : Artık bilgisayarlar arasında fiziksel bir adreslemenin yapıldığı ve bilgilerin paketlere ayrılarak gönderilip alındığı bir ortam tanımlarlar. Yani bu katmanda bilgilerin gönderildiği ortam değil, gönderilme biçimi ve fiziksel adresleme tanımlanmaktadır. Ağ üzerinde her birimin donanımsal olarak tanınabilen fiziksel bir adresinin olması gerekir. Örneğin bugün kullandığımız Ethernet kartları "Ethernet Protocol" denilen bir protokole uygun tasarlanmıştır. Bu ethernet protokolü "OSI Model" in fiziksel ve veri bağlantı katmanına karşılık gelmektedir. Ethernet protokolünde ağa bağlı olan her birimin ismine "MAC" denilen 6 baytlık fiziksel bir adresi vardır. Ethernet protokolünde "MAC" adresini bildiğimiz ağa bağlı bir birime bilgi gönderebiliriz. Bilgiler "Paket Anahtarlaması (Packet Switching)" denilen teknikle gönderilip alınmaktadır. Bu teknikte bilgiler bir paket adı altında bir araya getirilir, sonra ilgili fiziksel katmana seri bir biçimde gönderilir. -> "Network Katmanı (Network Layer)": Artık "internetworking" yapmak için gerekli kuralları tanımlamaktadır. "Internetworking" terimi "'network' lerden oluşan 'network' ler" anlamına gelir. Aynı fiziksel ortamda bulunan ağlara ise "Yerel Ağlar (Local Area Networks)" denilmektedir. Bu yerel ağlar "router" denilen aygıtlarla birbirlerine bağlanmaktadır. Böylece "internetworking" ortamı oluşturulmaktadır. Tabii böyle bir ortamda artık ağa birimler için fiziksel adresler kullanılamaz. Bu ortamlarda ağa birimlere mantıksal bir adres ataması yapılmalıdır. İşte bu katman, "internetworking" ortamı içerisinde, bir birimden diğerine bir paket bilginin gönderilmesi için gereken tanımlamaları içermektedir. Ağ katmanı bu nedenle en önemli katmandır. Ağ katmanında artık fiziksel bir adresleme değil, mantıksal adresleme sistemi kullanılmaktadır. Ayrıca bilgilerin paketlere ayrılarak, "router" lardan dolaşıp, hedefe varması için gereken rotalama mekanizması da bu katmanda tanımlanmaktadır. Yani elimizde yalnızca network katmanı varsa biz yalnızca "internetworking" ortamında belli bir kaynaktan hedefe bir paket yollayabiliriz. -> "Aktarım Katmanı (Transort Layer)": Network katmanının üzerindedir. Aktarım katmanında artık kaynak ile hedef arasında bir bağlantı oluşturulabilmekte ve veri aktarımı daha güvenli olarak yapılabilmektedir. Aynı zamanda aktarım kavramı "multiplex" bir kaynak hedef yapısı da oluşturmaktadır. Bu sayede bilgiler hedefteki spesifik bir programa gönderilebilmektedir. Bu işleme "port numaralandırması" da denilmektedir. -> "Oturum Katmanı (Session Layer)": Pek çok protokol ailesinde yoktur. Görevi oturum açma kapama gibi yüksek seviyeli bazı belirlemeleri yapmaktır. -> "Sunum Katmanı (Presentation Layer)": Verilerin sıkıştırılması, şifrelenmesi gibi tanımlamalar içermektedir. -> "Uygulama Katmanı (Application Layer)": Bu protokolü kullanan bütün programlar aslında uygulama katmanını oluşturmaktadır. Yani ağ ortamında haberleşen her program zaten kendi içerisinde açık ya da gizli bir protokol oluşturmuş durumdadır. Örneğin IP protokol ailesindeki somut işleri yapmakta kullanılan Telnet, SSH, HTTP, POP3 gibi protokoller uygulama katmanı protokolleridir. Şimdi de protokol ailelerini inceleyelim: >>> "IP" Ailesi: Açılımı "Internet Protocol" biçimindedir. Protokol ailelerinden en meşhur olan "IP" ailesidir. Bu protokol ailesinin kısa bir tarihi ise şu şekildedir; -> IP ailesi 70'li yıllarda Vint Cerf ve Bob Kahn tarafından geliştirilmiştir. Cerf ve Kahn 1974 yılında önce TCP üzerinde sonra da bu protokol üzerinde çalışmışlar ve o tarihlerde ilk versiyonlarını oluşturmuşlardır. 1980'li yıllar itibariyle hepimizin katıldığı Internet'in (I'nin büyük yazıldığına dikkat ediniz) bu aileyi kullanmaya başlamasıyla populer olmuştur. Böylece IP ailesini kullanarak yazdığımız programlar hem aynı bilgisayarda hem yerel ağımızdaki bilgisayarlarda hem de Internet'te çalışabilmektedir. Aynı zamanda açık bir (yani bir şirketin malı değil) protokol ailesi olması da cazibeyi çok artırmıştır. Pekiyi "Internet" denilen kavram da nedir? Bugün hepimizin bağlandığı büyük ağa "Internet" denilmektedir. Bu ağ ilk kez 1969 yılında Amerika'da Amerikan Savunma Bakanlığı'nın bir soğuk savaş projesi biçiminde başlatıldı. O zamana kadar yalnızca kısıtlı ölçüde yerel ağlar vardı. 1969 yılında ilk kez bir "WAN (Wide Area Network)" oluşturuldu. Bu proje Amerikan Savunma Bakanlığı'nın DARPA isimli araştırma kurumu tarafından başlatılmıştır ve projeye "ARPA.NET" ismi verilmiştir. Daha sonra bu ağa Amerika'daki çeşitli devlet kurumları ve üniversiteler katıldı. Sonra ağ Avrupa'ya sıçradı. 1983 yılında bu ağ "NCP" protokolünden "IP" ailesine geçiş yaptı. Bundan sonra artık "APRA.NET" ismi yerine "Internet" ismi kullanılmaya başlandı. (Internet sözcüğü I herfi küçük harfle yazılırsa "internetworking" anlamında büyük harfle yazılırsa bugün katıldığımız dev ağ anlamında kullanılmaktadır.) Biz de "IP" ailesini kullanarak kendi "internetworking" ortamımızı oluşturabiliriz. Örneğin bir şirket hiç Internet'e bağlanmadan kendi internet'ini oluşturabilir. Buna eskiden "intranet" denirdi. "IP" ailesi herkesin kendi internet'ini oluşturabilmesi için bütün gerekli protokolleri barındırmaktadır. Tabii sinerji bakımından herkes zaten var olan ve Internet denilen bu dev ağa bağlanmayı tercih etmektedir. Diğer yandan bu "IP" ailesi ise dört katmandan oluşmaktadır. Bu katmanlar, yüksek seviyeden aşağı seviyeye doğru, +---------------------+-------------------------------+ | Application Layer | HTTP, SSH, POP3, IMAP, ... | +---------------------+---------------+---------------+ | Transport Layer | TCP | UDP | +---------------------+---------------+---------------+ | Network Layer | IP | +---------------------+-------------------------------+ | Physical/Data Link | Ethernet | | Layer | Wireless | +---------------------+-------------------------------+ biçiminde gösterilebilir. Görüleceği üzere bu ailede Fiziksel Katman ve Veri Bağlantı Katmanı birarada bulunmaktadır. Yani üst üste değil. Bu katmanlardan, -> Fiziksel Katman ve Veri Bağlantı Katmanı için "Ethernet" ve "Wireless" protokolleri kullanılmaktadır. -> Ağ Katmanı için, aileye ismini veren, "IP" isimli protokol kullanılmaktadır. -> Aktarım Katmanı için "TCP" ve "UDP" protokolleri kullanılmaktadır. -> Uygulama Katmanı için, "TCP" protokolünün üzerine gelen, HTTP, TELNET, SSH, POP3, IMAP gibi pek çok protokol kullanılmaktadır. Tabii IP protokol ailesinde bu hiyerarşik yapıyla ilgili olmayan irili ufaklı pek çok protokol de bulunmaktadır. Şimdi de katmanlardaki protokolleri inceleyelim. >>>> "IP" : Ailenin en önemli ve taban protokolülüdür. Temel protokol olduğundan, tek başına kullanıldığında, sadece bir paket gönderme ve alma işini gerçekleştirir, ki bu paketler aslında "IP Package" olarak da adlandırılır. Bundan dolayıdır ki bu protokolün tek başına kullanılması seyrek rastlanılan durumdur. Uygulamalarda "Transport Layer" da bulunan "TCP" & "UDP" protokolleri kullanılır ki bunlar da esasında "IP" nin üzerine oturtulmuştur. "TCP" protokolü daha sık kullanıldığından, genellikle "TCP/IP" ismiyle geçer. "IP" protokolüne göre ağa katılan her bir cihaza "host" denilir. Her "host" cihazının da bir mantıksal adresi bulunur ki buna "IP Address" denilir. "IP" protokolü ise temelde ikiye ayrılır. Bunlar "IPv4" ve "IPv6" şeklindedir. Bu ikisi arasındaki temel fark "IPv4" olanlarda "IP Address" uzunluğu 4 bayt iken, "IPv6" olanlarda ise 16 bayt uzunluğundadır. >>>> "TCP" : Bu protokol bağlantılı ("connection-oriented") bir protokoldür. Yani buradaki bağlantılı olma ile kastedilen, "IP Package" ile yapılan mantıksal bir bağlantı olmasıdır. Bir diğer deyişle gönderen taraf ile alan taraf birbiriyle tanışır ve haberleşmenin güvenliği açısından işlemler sırasında birbirleriyle konuşabilirler. Dolayısıyla akla "Client-Server" tarzını getirtir. Bu yönüyle "TCP" protokolü güvenilir("reliable") bir protokoldür. Çünkü alıcı ile gönderen arasında mantıksal bir bağlantı olduğundan, yolda kaybolan paketlerin telafisi mümkündür. Böylece alıcı taraf, gönderenin gönderdiklerini, eksiksiz ve bozulmadan aldığını bilir. Aynı zamanda "TCP" de bir Akış Kontrol("Flow Control") mekanizması da vardır. Bu sayede alıcı taraf, kendi tamponunun taşması durumunda, göndericiyi durdurabilmektedir. Diğer yandan bu protokol "stream" tabanlıdır. Yani, tıpkı boru haberleşmesinde olduğu gibi, gönderen tarafın bilgilerinin bir kuyruk sistemi eşliğinde oluşturulması ve alıcının istediği kadar byte'ı parça parça okuyabilmesi demektir. "stream" tabanlı haberleşmenin oluşturulması için gönderilecek bilginin "IP Package" haline getirilmesi ve numaralandırılması, daha sonra hedefte tekrar birleştirilmesi gerekmektedir. Örneğin, biz bir "host" cihazından başka bir "host" cihazına 10000 bayt büyüklüğünde bir bilgi göndermek isteyelim. İlk önce bu 10000 bayt paketlere ayrılır ve her birisine bir numara verilir. Daha sonra hedefe gönderilir ve orada tekrar birleştirilir. Öte yandan gönderen tarafın gönderdiği sıranın alıcı tarafça korunması da bir zorunluluk değildir. Yani ilk gönderdiğimiz, karşı tarafa en son ulaşan paket olabilir. >>>> "UDP" : Bu protokol bağlantısız ("connectionless") bir protokoldür. Yani "TCP" deki gibi alıcı ile gönderici arasında bir haberleşme, el sıkışma durumu yoktur. Bu yönüyle akla televizyon yayınlarını getirtir. Verici görüntüyü yollar ancak alıcının alıp almadığıyla ilgilenmez. Vericinin görüntüyü yollaması için alıcıyla bağlantı kurması gerekmemektedir. Bu yönüyle "UDP" protokolü güvenilir olmayan("unreliable") bir protokoldür. Alıcı ile gönderici arasında bir haberleşme olmadığından, bilginin iletilip iletilmediği meçhuldür. Benzer şekilde, bu protokolde, bir Akış Kontrol("Flow Control") mekanizması da bulunmamaktadır. Diğer yandan bu protokol "datagram" tabanlıdır. Yani, tıpkı mesaj kuyruklarında olduğu gibi, bilginin paket paket iletilmesi demektir. Bu protokolde alıcı taraf, gönderen tarafın tüm paketini, tek hamlede almak zorundadır. Dolayısıyla, "stream" tabanlıda olduğu gibi, gönderilecek verinin paketlere ayrılması ve hedefte tekrar birleştirilmesi gibi bir şey söz konusu değildir. Yine bu protokolde de gönderim sırasının korunacağı garanti değildir. Bütün bunları göz önüne aldığımız neden en çok kullanılan protokolün "TCP" protokolü olduğunu anlayabiliriz. Pekiyi "IP" protokol ailesine bağlı "host" cihazları arasındaki haberleşme için gönderilen paketin hedef "host" cihazında hangi programa ilişkin olduğu nasıl belirlenmektedir? Çünkü bir "host" cihazında farklı programlar farklı "host" cihazları ile haberleşiyor olabilir. İşte gönderilen paketlerin o "host" cihazında ayrıştırılması için "Protocol Port Number" isminde bir içsel numara daha uydurulmuştur. Şimdi de bu içsel numarayı inceleyelim. >>>> "Protocol Port Number" : Başlıkta da belirtildiği gibi gelen paketlerin o "host" içerisindeki hangi programa ilişkin olduğunu belirten numaradır. Bu numarayı şirketlerin içsel dahili numarası olarak da görebiliriz. Bu numaralar "IPv4" ve "IPv6" da 2 baytle ifade edilmektedir. İlk 1024 numara "IP" protokol ailesinin uygulama katmanındaki protokoller için ayrılmış olup, bunlara "Well Known Ports" denilmektedir. Bu nedenle programcılar numara belirlerken 1024'ten büyük olacak biçimde belirleme yapmaları gerekir. Bu numara "TCP" ve "UDP" protokollerinde olup, "IP" protokolünde bulunmamaktadır. Dolayısıyla bir "host", "TCP" ya da "UDP" kullanarak bir bilgi gönderecekse, bilginin gönderileceği "host" un "IP" numarasını ve bilginin orada kime gönderileceğini anlatan port numarasını belirtmek zorundadır. Aynı şekilde bilgiyi almak isteyen program da kendisinin hangi port numarasıyla ilgilendiğini de belirtmek durumundadır. Kullanım kolaylığı sağlaması açısından, "IP" numarası ve port numarası çiftine, "IP End Point" de denilmektedir. Öte yandan yukarıda "IP Package" kavramından bahsetmiştik. Şimdi de onun detaylarına değinelim. >>>> "IP Package" : Anımsanacağı üzere "TCP" ve "UDP" protokollerinin "IP" protokolü üzerine oturdulduğunu belirtmiştik. Dolayısıyla biz "TCP" kullanarak bir veri göndermek istediğimizde, aslında önce "TCP Package" oluşturulur. Ancak alt seviyedeki protokol "IP" olduğundan, gönderimin "IP Package" olarak yapılması gerekmektedir. Bir "IP Package" ise iki kısımdan oluşur. Bunlar "IP Header" ve "IP Data" şeklinde olup, gösterimi aşağıdaki gibidir. +-------------------------+ | IP Header | +-------------------------+ | IP Data | +-------------------------+ Bu kısımlardan, -> "IP Header" : Söz konusu paketin hedef "host" cihazına ulaştırılabilmesi için gerekli bilgiler bulunur. -> "IP Data" : Gönderilecek asıl bilginin bulunduğu kısımdır. İşte "TCP Package" / "UDP Package" ise bu kısımda bulunur. Bu paketin(IPv4 için olanı) detaylı gösterimi ise aşağıdaki gibidir. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +-----------+-----------+----------------------+-----------------------------------------------+ ^ | Version | IHL | Type of Service | Total Length | (4 bytes) | | (4 bits) | (4 bits) | (8 bits) | (16 bits) | | +-----------+-----------+----------------------+-----------+-----------------------------------+ | | Identification | Flags | Fragment Offset | (4 bytes) | | (16 bits) | (3 bits) | (13 bits) | | +-----------------------+----------------------+-----------+-----------------------------------+ | | Time to Live (TTL) | Protocol | Header Checksum | (4 bytes) | 20 bytes | (8 bits) | (8 bits) | (16 bits) | | +-----------------------+----------------------+-----------------------------------------------+ | | Source IP Address (32 bits) | (4 bytes) | +----------------------------------------------------------------------------------------------+ | | Destination IP Address (32 bits) | (4 bytes) | +----------------------------------------------------------------------------------------------+ v | Segment (L4 protocol (TCP/UDP) + Data) | +----------------------------------------------------------------------------------------------+ Pekiyi "TCP Package" / "UDP Package" kavramları da nedir? >>>>> "TCP Package" / "UDP Package": Bu paketlerin görünümleri ise aşağıdaki gibidir. -> "TCP Package" : Aşağıdaki gibi gösterilebilir. +-------------------------+ | IP Header | +-------------------------+ <---+ | TCP Header | | +-------------------------+ IP Data | TCP Data | | +-------------------------+ <---+ -> "UDP Package" : Aşağıdaki gibi gösterilebilir. +-------------------------+ | IP Header | +-------------------------+ <---+ | UDP Header | | +-------------------------+ IP Data | UDP Data | | +-------------------------+ <---+ Şekillerden de görüleceği üzere "TCP Package" / "UDP Package" ın "header" ve "data" kısımları aslında "IP Package" ın "data" kısmı gibi oluşturulmaktadır. Böylece yolculuk eden paket aslında bir "TCP Package" / "UDP Package" değil "IP Package" tır. Bu paketlerin detaylı gösterimleri ise aşağıdaki gibidir. >>>>>> "TCP Package" : <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +----------------------------------------------+-----------------------------------------------+ ^ | Source Port | Destination Port | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ | | Sequence Number | (4 bytes) | | (32 bits) | | +----------------------------------------------------------------------------------------------+ | | Acknowledgement Number | (4 bytes) | | (32 bits) | | 20 bytes +-----------+----------------+-----------------+-----------------------------------------------+ | |Header Len.| Reserved | Control Bits | Window Size | (4 bytes) | | (4 bits) | (6 bits) | (6 bits) | (16 bits) | | +-----------+----------------+-----------------+-----------------------------------------------+ | | Checksum | Urgent | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Options | | (0 or 32 bits) | +----------------------------------------------------------------------------------------------+ | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ >>>>>> "UDP Package" : <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +----------------------------------------------+-----------------------------------------------+ ^ | Source Port | Destination Port | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ | 8 bytes | Header Length | Checksum | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ Öte yandan protokollerde işlemcinin "endian" özelliği de önemlidir. "IP" ailesi "Big Endian" formata göre tasarlanmıştır. Bu formata, protokol aileleri arasında, "Network Byte Ordering" de denmektedir. Dolayısıyla bizlerin kullanacağımız değerleri ilk önce "Big Endian" formatına dönüştürmesi gerekmektedir. Pekiyi bu protokoller arka planda hangi kütüphaneyi kullanmaktadır? Esasında bu tip haberleşme işletim sisteminin çekirdekleri tarafından yapılmaktadır. Tabii "user-mode" programlar için sistem çağrılarını yapan, bir takım gereksinimleri de karşılayan fonksiyonlara ihtiyaç vardır. Bu ihtiyacı karşılayan ve en çok başvurulan kütüphane ise "socket" kütüphanesidir. >>> "socket" Kütüphanesi: 1983 yılında BSD işletim sisteminin 4.2 sürümünde geliştirilmiştir ve pek çok UNIX türevi sistem bu kütüphaneyi aynı biçimde benimsemiştir. Microsoft'un Windows sistemleri de yine bu API kütüphanesini, "Winsock" ya da kısaca "WSA (Windows Socket API)" adıyla, desteklemektedir. Windows tarafındaki bu kütüphane hem klasik BSD soket API fonksiyonlarını hem de başı "WSAXXX" ile başlayan Windows'a özgü API fonksiyonlarını barındırmaktadır. Yani UNIX/Linux sistemlerinde yazdığımız soket programlarını küçük değişikliklerle Windows sistemlerine de "port" edebiliriz. Diğer yandan "socket" kütüphanesi yalnızca "IP" protokol ailesi için tasarlanmış bir kütüphane değildir. Bütün protokollerin ortak kütüphanesidir. Bu nedenle kütüphanedeki fonksiyonlar daha genel biçimde tasarlanmıştır. Dolayısıyla "socket" kütüphanesindeki fonksiyonları kullanırken, aslında arka planda işlemler "TCP/IP" ve "UDP/IP" protokollerine uygun bir biçimde gerçekleştirilmektedir. Örneğin biz "send" isimli "socket" fonksiyonu ile bir bilgiyi göndermek istediğimizde, aslında bu fonksiyon arka planda bir TCP paketi dolayısıyla da bir IP paketi oluşturarak protokole uygun bir biçimde, bu bilgiyi göndermektedir. "socket" kütüphanesinin yalnızca bir API arayüzü olduğuna dikkat ediniz. Son olarak "socket" kütüphanesi POSIX tarafından da desteklenmektedir. Bu kütüphanedeki fonksiyonların önemli bir bölümü ise "" içerisinde bulunur. Fakat bu başlık dosyasına ek olarak, kullanacağımız diğer fonksiyonlar için, başka başlık dosyalarının da eklenmesi gerekebilir. Kursun devamındaki konuların işleniş sırası şu şekilde olacaktır: -> "TCP/IP" "Client-Server" programların oluşturulması konusunu ele alacağız. -> Sonra "TCP/IP" haberleşmesinin bazı protokol detaylarından bahsedeceğiz. -> Sonra da "UDP/IP" haberleşme üzerinde duracağız. -> Son olarak da "Client-Server" arasındaki haberleşmenin detayları üzerinde duracağız. Bu konulardan, >>>> "TCP/IP" : Bir "TCP/IP" uygulamasında "server" ve "client" olmak üzere iki ayrı program yazılır; "TCP Server Program" ve "TCP Client Program". Biz önce "TCP Server Program" ın daha sonra da "TCP Client Program" ın yazımı üzerinde duracağız. >>>>> "TCP Server Program" : Bu program tipik olarak aşağıdaki fonksiyonları sırayla çağırarak gerçekleştirilir. "socket" > "bind" > "listen" > "accept" > "recv/read" Bu fonksiyonlar birer POSIX fonksiyonu olup, fonksiyonlardan, -> "socket" : Haberleşme için gerekli "socket" nesnesini oluşturur. Bu fonksiyon ilgili "socket" nesnesini oluşturur ve ona ilişkin bir "File Description" verir. Biz de diğer fonksiyonlarda bu dosya betimleyicisini kullanırız. Fonksiyonun prototipi aşağıdaki gibidir. #include int socket(int domain, int type, int protocol); Fonksiyonun birinci parametresi, kullanılacak protokol ailesini belirtir. Bu parametre "AF_XXX(Address Family)" biçimindeki sembolik sabitlerden biri olarak girilir. "IPv4" için bu parametreye "AF_INET", "IPv6" için "AF_INET6" girilmelidir. Pekala başka sembolik sabitler de mevcuttur. Fonksiyonun ikinci parametresi kullanılacak protokolün "stream" tabanlı mı yoksa "datagram" tabanlı mı olacağını belirtmektedir. "stream" tabanlı olanlar için "SOCK_STREAM", "datagram" olanlar için "SOCK_DGRAM" sembolik sabitleri kullanılmalıdır. Ancak başka sembolik sabitler de mevcuttur. Bu parametreye "TCP" protokolü için "SOCK_STREAM", "UDP" protokolü için "SOCK_DGRAM" sembolik sabitleri geçilmelidir. Fonksiyonun üçüncü parametresi ise "Transport Layer" daki protokolü belirtmektedir. Ancak zaten ikinci parametreden "Transport Layer" daki protokol anlaşılıyorsa, üçüncü parametre "0" olarak geçilebilmektedir. Yani ikinci parametreye "TCP" için "SOCK_STREAM" ve "UDP" için de "SOCK_DGRAM" geçmişken, üçüncü parametreye direkt "0" geçebiliriz. Pekala bu parametreye "TCP" için "IPPROTO_TCP", "UDP" için "IPPROTO_UDP" sembolik sabitlerini geçebiliriz. Fakat bu sembolik sabitler "" içerisindedir. Fonksiyonun geri dönüş değeri başarı durumunda "fd" değerine, hata durumunda "-1" değerine geri döner ve "errno" değişkeni uygun değişkene çekilir. Bu fonksiyonun, tıpkı "open" gibi, Dosya Betimleyici Tablosundaki en düşük indeks değerini, yani "fd" yi, döndürdüğüne dikkat ediniz. -> "bind" : "socket" nesnesini oluşturduktan sonra bu nesneyi bir "port" ile bağlamalıyız. Bu işlem "server" ın hangi "port" a bakacağı ve hangi "network" arayüzünden (kartından) gelen bağlantı isteklerini kabul edeceği belirlenir. Bu fonksiyon dinleme işlevini gerçekleştirmez, sadece "socket" nesnesine bu bilgileri yerleştirir. Fonksiyonun prototipi aşağıdaki gibidir. #include int bind(int socket, const struct sockaddr *addr, socklen_t addrlen); Fonksiyonun birinci parametresi, oluşturduğumuz "socket" nesnesine ilişkin "fd" değeridir. İkinci parametre ise, her ne kadar "sockaddr" yapı türünden bir adres değeri de olsa, kullandığımız protokol ailesine ait olan bir yapı türündendir. Bu yüzdendir ki protokolümüze ait olan yapı türünden nesneyi bu fonksiyona geçerken türünü "sockaddr" yapı türüne dönüştürmemiz gerekmektedir. Burada "IPv4" için "sockaddr_in" yapısı, "IPv6" için "sockaddr_in6" yapısı kullanılır. Programcı ilgili yapıyı doldurduktan sonra fonksiyona geçmelidir. Üçüncü parametre ise ikinci parametredeki yapının büyüklük bilgisidir. "sockaddr_in" yapısı aşağıdaki gibi tanımlıdır. #include struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; // 'uint16_t' struct in_addr sin_addr; }; Yapının "sin_family" isimli elemanı, kullandığımız protokol ailesini belirtir. Yani "socket" fonksiyonunun birinci parametresine geçtiğimiz sembolik sabiti kullanmalıyız. Yapının "sin_port" isimli eleman ise dinleyeceğimiz "port" numarasını belirtir. Yapının "sin_addr" isimli elemanı ise dinleyeceğimiz "IP" numarasını belirtir. Bu eleman ise "in_addr" yapı türünden olup, aşağıdaki gibi tanımlıdır. #include struct in_addr { in_addr_t s_addr; // 'uint32_t' }; "in_addr" yapısının "s_addr" isimli elemanı İşaretsiz Tam Sayı türündendir. Bu elemana "INADDR_ANY" sembolik sabitini atarsak, bütün "network" kartlarından gelen istekleri kabul etmiş oluruz. Gerek "sockaddr_in" yapısının gerek "in_addr" yapısının "netinet/in.h" içerisinde tanımlandığına dikkat ediniz. Fonksiyon başarı durumunda "0", hata durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. İşte, yukarıda açıkladığımız "endian" kavramı burada devreye girmektedir. Artık "sockaddr_in" yapısının "sin_port" ve "in_addr" yapısının "sin_addr" isimli elemanları için kullandığımız değerlerin "Big Endian" olduğuna dikkat etmeliyiz. Fakat bunun için fonksiyonlar da bulundurulmuştur. Elimizdeki makinanın "endian" durumundan bağımsız, kullanacağımız parametreyi "Big Endian" formatına dönüştüren bu fonksiyonlar ise şunlardır; "htons" ve "htonl". Bu fonksiyonlar elimizdeki değeri "Big Endian" formatına dönüştürürler. Fonksiyonların prototipi aşağıdaki gibidir. #include uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); Fonksiyonlar sırasıyla "long" türünden ve "short" türünden değerleri argüman olarak alırlar ve "Big Endian" formatta geri döndürürler. Pekala bu fonksiyonların tersi işlevini yapan, "ntohl" ve "ntohs" isimli fonksiyonlar da vardır. Bunların prototipi aşağıdaki gibidir. #include uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); -> "listen" : Aktif bir şekilde o "port" üzerinden dinleme yapmak için bu fonksiyon çağrılmalıdır. Fonksiyonun prototipi aşağıdaki gibidir. #include int listen(int socket, int backlog); Fonksiyonun birinci parametresi, o "socket" nesnesine ilişkin "fd" değeridir. İkinci parametre ise bir kuyruk uzunluğu belirtmektedir. Bu kuyruk, işletim sistemi tarafından "listen" çağrısından sonra ilgili "port" a gelen bağlantı isteklerinin yerleştirildiği kuyruktur. Bu kuyruğun uzun tutulması, bağlantı isteklerinin kaçırılmamasına olanak tanır. Linux sistemlerinde varsayılan değer "128" biçiminde olup, "/proc/sys/net/core/somaxconn" dosyasındaki değeri değiştirerek, bu varsayılan değeri de değiştirebiliriz. Fonksiyon başarı durumunda "0" değerine, hata durumunda ise "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Bu fonksiyon blokeye yol açmamaktadır. Son olarak bu fonksiyon işletim sisteminin "firewall" koruma mekanizmasına takılabilir, işletim sistemi konuyla ilişkili olarak uyarı verebilir. Dolayısıyla dinediğimiz "port" u açık hale getirmemiz gerekebilir. -> "accept" : Bu fonksiyon ise ilgili kuyruktaki bağlantı isteklerini "get" eden fonksiyondur. Çünkü bu fonksiyon bağlantı kuyruğuna bakar, orada bir bağlantı isteği varsa onu alır ve hemen geri döner. Eğer orada bir bağlantı isteği yoksa, default durumda, blokeye yol açar. Fonksiyonun prototipi şöyledir. #include int accept(int socket, struct sockaddr *address, socklen_t *address_len); Fonksiyonun birinci parametresi, o "socket" nesnesine ilişkin "fd" değeridir. İkinci parametre ise bağlantı kuyruğundaki istek bilgilerinin yerleştirileceği "sockaddr" yapı türünden bir adres bilgisidir. Yine, "bind" fonksiyonunda olduğu gibi, "sockaddr" yapısı genel bir tür belirtir. Dolayısıyla Burada "IPv4" için "sockaddr_in" yapısı, "IPv6" için "sockaddr_in6" yapısı türünden değişkenin adresini geçeceğiz. Tabii yine değişkenimizin türünü "sockaddr" türüne dönüştürdükten sonra fonksiyona geçmeliyiz. Bu parametreye geçtiğimiz yapı türünden nesnenin içi artık fonksiyon tarafından DOLDURULACAKTIR. Çünkü "client" program "server" programa bağlanırken de bir "IP" ve "port" numarası belirtir. İşte bağlantı kuyruğundaki istekler, "client" programın bu değerlerini içermektedir. "accept" ile bunları temin etmiş olacağız. Örneğin, "client" programın "IP" adresinin "178.231.152.127", kullandığı "port" numarasının da "52310" olduğunu varsayalım. "server" programınkiler ise sırasıyla "176.234.135.196" ve "55555" biçiminde olsun. Dolayısıyla "Server-Client" arasındaki bağlantı şeması takribi olarak aşağıdaki gibi olacaktır. Client Server (178.231.152.127:52310) ---> (176.234.135.196:55555) "accept" fonksiyonunun üçüncü parametresi, tıpkı "bind" fonksiyonundaki gibi, ikinci parametresindeki yapının büyüklük bilgisidir. Ancak bu parametrenin adres olduğuna dikkat ediniz. "accept" fonksiyonu yine bu adrese yazma yapacaktır. Böylelikle "client" programın kullandığı protokolün ne olduğunu anlayabileceğiz. Çünkü her protokolde farklı olan "sockaddr" türünün de büyüklük bilgisi farklıdır. Fonksiyon başarısızlık durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Ancak başarı durumunda, o "client" a özel yeni bir "socket" betimleyicisine geri döner. Artık o "client" ile haberleşmek için fonksiyonun geri döndürdüğü betimleyiciyi kullanmalıyız. Buraya kadar, "server" program içerisinde, iki farklı "socket" nesnesi oluşturuldu. Bunlardan birisi ilk başta "socket" fonksiyonu ile elde ettiğimiz, diğeri de "accept" ile elde ettiğimiz. İşte "socket" fonksiyonu ile elde ettiğimiz ve "bind", "listen", "accept" fonksiyonlarına argüman olarak geçtiğimiz "socket" nesnesi için "TCP/IP" terminolojisinde "Passive Socket", "accept" ile ettiğmize ise "Active Socket / Listening Socket" denir. Bu soket nesnelerinden "Passive Socket" yalnızca bir kez oluşturulurken, "Active Socket" ise bağlantı isteği yollayan "client" adedince oluşturulur. Dolayısıyla "socket", "bind" ve "listen" fonksiyonlarını bir kez çağırmamız gerekirken, "accept" fonksiyonunu bir döngü içerisinde çağırmalıyız. Böylece, döngünün her turunda, her "client" için farklı "socket" betimleyicisi elde edeceğiz. Pekiyi "accept" ile elde ettiğimiz bağlantı isteklerindeki "client" programların "IP" adreslerini ve kullandıkları "port" numaralarınıı nasıl yazdıracağız? Aslında bu konu için şu noktalara dikkat etmemiz gerekmektedir: -> "accept" ile elde ettiğimiz bu bilgiler "Big Endian" formatında, yani Network Byte Ordering" formatında olduğundan, bunları kullanmak için "ntohl" ve "ntohs" fonksiyonlarını kullanmamız gerekmektedir. -> "IP" adresleri genellikle "Noktalı Desimal Format(Dotted Decimal Format)" denilen bir formatta yazıya dökülmektedir. Bütün bunlar ışığında kullanacağımız iki fonksiyon vardır. Bunlar, "inet_ntoa" ve "inet_addr" isimli POSIX fonksiyonlarıdır. Bu fonksiyonları sırasıyla ilgili "IP" adresini yazı formatına dönüştürür ve aldığı yazıyı "IP" adresi formatına dönüştürür. Fonksiyonların prototipleri şöyledir. #include char *inet_ntoa(struct in_addr in); in_addr_t inet_addr(const char *cp); Buradaki "inet_ntoa" fonksiyonu argüman olarak bir "IP" adresi alır ve geriye statik ömürlü bir yazı döndürür. Bu yazı "Dotted Decimal Format" biçimindedir. Fonksiyon başarısız olamamaktadır. FONKSİYONUN ARGÜMAN OLARAK ADRES BİLGİSİ ALMADIĞINA DİKKAT EDİNİZ. Fonksiyonun geri döndürdüğü yazı statik ömürlü olduğundan "thread-safe" OLMADIĞINA ve "free" işlemine gerek kalmadığına da DİKKAT EDİNİZ. Buradaki "inet_addr" ise "inet_ntoa" fonksiyonunun tersi işlemi yapmaktadır. "Dotted Decimal Format" biçimindeki bir yazıyı argüman olarak alır ve "IP" adresine dönüştürür eğer başarılı olursa. Başarısızlık durumunda "(in_addr_t)(-1)" değerine geri döner. Ancak karşılaştırma yaparken direkt "-1" ile de karşılaştırma yapsak olur çünkü "in_addr_t" türü işaretsiz olduğundan, "-1" ile karşılaştırırken de "-1" değeri de en büyük pozitif değere dönüştüreceğinden, sorun olmayacaktır. Fakat başarısızlığa ilişkin herhangi bir "errno" değeri tanımlı olmadığından, "errno" değişkeni uygun değere çekilmeyecektir. Son olarak "inet_ntoa" fonksiyonunun aldığı ve "inet_addr" fonksiyonunun geri döndürdüğü "IP" adresinin formatının "Big Endian" olduğuna dikkat ediniz. -> "recv" : Artık "client" programlar tarafından gönderilen bilgileri "get" edebiliriz. Fonksiyonun prototipi aşağıdaki gibidir. #include ssize_t recv(int socket, void *buffer, size_t length, int flags); Fonksiyonun ilk üç parametresi "read" fonksiyonunki gibidir. Yani sırasıyla ilgili betimleyici, okunan bilgilerin yerleştirileceği o tampon alanın başlangıç adresi ve okunmak istenen toplam bayt sayısıdır. Fonksiyonun dördüncü parametresi ise "MSG_PEEK", "MSG_OOB" ve "MSG_WAIALL" sembolik sabitlerinin "bitwise-OR" işlemine sokularak elde edilebilecek bir bayrak değeridir. Bu sembolik sabitlerin anlamlarına daha sonra değineceğiz. Sadece "MSG_PEEK" için şunu söyleyebiliriz; bilginin alındıktan sonra hala orada tutulacağını belirtir, yani silinmez. Pekala bu son parametreye "0" da geçilebilir. Bu durumda "recv" fonksiyonu "read" fonksiyonu gibi işlev görecektir. Diğer taraftan "recv" fonksiyonu varsayılan durumda blokeli çalışmaktadır, tıpkı borularda olduğu gibi. Yine, tıpkı borulardaki gibi, eğer en az bir bayt varsa okuyabildiği kadarını okur ve hemen okuduğu kadarına geri döner. Eğer hiç bayt yoksa, en az bir bayt gelene kadar blokeye yol açar. Fonksiyon başarı durumunda okuduğu bayt sayısına, hata durumunda ise "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. Eğer karşı taraf soketi kapatmışsa, yine borularda olduğu gibi, fonksiyon "0" ile geri döner. Dolayısıyla karşı tarafın soketi kapattığını bu fonksiyon ile anlayabiliriz. Bu yüzdendir ki "recv" fonksiyonunun başarısını hem "0" hem de "-1" e karşı kontrol etmeliyiz. "-1" olması ortada beklenmeyen bir hata olduğuna, "0" olması ise soketin karşı tarafça kapatıldığı anlamındadır. Eğer fonksiyon blokesiz çalıştırılırsa, bloke bekleme yerine başarısızlıkla geri döner. >>>>> "TCP Client Program" : Bu program tipik olarak aşağıdaki fonksiyonları sırayla çağırarak gerçekleştirilir. "socket" > "bind" (isteğe bağlı) > "gethostbyname" (isteğe bağlı) > "connect" > "send/write" Bu fonksiyonlardan, -> "socket" : Bu fonksiyonun kullanım biçimi yine "server" programda olduğu gibidir. -> "bind" : Artık bir "socket" nesnesi oluşturduktan sonra onu bir "port" ile ilişkilendirmek zorunda değiliz. Genellikle "client" programlar "socket" nesnesini bir "port" ile ilişkilendirmezler. Ancak belli bir "port" kullanmak gerekiyorsa ilişkilendirir. Çünkü bir "bind" işlemi yapmazsa, işletim sistemi "connect" fonksiyonu sırasında boş bir "port" numarası atayacaktır. İşte işletim sistemi tarafından atanmış böyle "port" lara ise "Kısa Ömürü Port (Ephemeral Port)" denir. Ancak "client" program bağlantı sağlayabilmesi için "server" programın "IP" adresini ve "port" numarasını bilmek zorundadır. "IP" adreslerinin akılda tutulması zor olduğundan, o adreslerle eşleşen "host" isimleri oluşturulmuştur. Ancak "IP" ailesi bu "host" isimleriyle değil, yine "IP" numaralarıyla çalışmaktadır. İşte bu "host" isimlerle "IP" numaraları eşleştiren özel "server" programlara ise "Domain Name Server(DNS)" ismi verilmiştir. Bu "DNS" ler ise yine "IP" protokol ailesinin mensubudurlar, çünkü o ailedeki "DNS" isimli protokolü kullanırlar. Dolayısıyla "client" programın elinde "IP" adresi yerine host ismi varsa, "DNS" işlemi yaparak, o "host" ismine karşı gelen "IP" numarasını elde etmesi gerekir. Öte yandan iş bu "DNS" lerde "host" isimleri ile "IP" numaraları birebir karşılık gelmemektedir. Yani bir isim birden fazla numarayla ilişkili olabilirken, bir numara ise birden fazla isimle ilişkili olabilir. Dolayısıyla bizler bir takım fonksiyonlar kullanmak durumundayız. Bu fonksiyonlar, "gethostbyname" ve "gethostbyaddr" isimli geleneksel POSIX fonksiyonlarıdır. Ancak "deprecated" EDİLMİŞLERDİR. Dolayısıyla artık POSIX standartlarında mevcut DEĞİLDİRLER. Fakat bu fonksiyonlar yerine, "getnameinfo" ve "getaddrinfo" isimli POSIX fonksiyonu standarda eklenmiştir. Bu iki yeni fonksiyonun detaylarına aşağıda değinilecektir. -> "connect" : Artık "client" program bağlantıyı sağlayabilir. Fonksiyonunun prototipi aşağıdaki gibidir. #include int connect(int socket, const struct sockaddr *address, socklen_t address_len); Fonksiyonun birinci parametresi, "socket" fonksiyonu ile elde ettiğimiz soket betimleyicisini belirtir. İkinci parametre ise bağlanılacak "server" a ilişkin, yine "sockaddr" yapı türünden, adres belirtir fakat "IPv4" için "sockaddr_in" yapısını kullanmalıyız. Tabii "sockaddr_in" yapısının içini biz doldurduktan sonra, "const struct sockaddr" türüne dönüştürerek "connect" fonksiyonuna geçmeliyiz. Fonksiyonun üçüncü parametresi ise yine ikinci parametredeki yapı adresinin uzunluk bilgisidir. Fonksiyon başarı durumunda "0", hata durumunda "-1" değerine geri döner ve "errno" değişkenini uygun değere çeker. "sockaddr_in" içini doldururken "IP" adresi kısmına "127.0.0.1" adresini yazarsak, o anda çalışılan makinenin "IP" adresini kullanmış oluruz. Bu standart bir "IP" adresidir. Yani o anda çalışılan makinenin "IPv4" adresi "127.0.0.1" ile temsil edilmektedir. Bu adrese "loopback address" de denilmektedir. Bazı işletim sistemlerinde (Windows, Linux ve macOS) "localhost" ismi de o anda çalışılan makinenin "host" ismi olarak kullanılabilmektedir. Fakat bu isim standart DEĞİLDİR. Eğer "client" program bağlanmaya çalışırken aktif bir "server" yoksa ya da "server" programın bağlantı kuyruğu doluysa, "connect" fonksiyonu belli bir süre bekler ve başarısız olur. Bu durumda "errno" değişkeninin değeri ise "ECONNREFUSED(Connection Refused)" değerine çekilir. -> "send" : Artık "server" programa veri gönderebiliriz. Fonksiyonun prototipi aşağıdaki gibidir. #include ssize_t send(int socket, const void *buffer, size_t length, int flags); Fonksiyonun ilk üç parametresi "write" fonksiyonunki gibidir. Yani sırasıyla ilgili betimleyici, gönderilecek bilgilerin yerleştirileceği o tampon alanın başlangıç adresi ve gönderilmek istenen toplam bayt sayısıdır. Fonksiyonun dördündü parametresi ise "MSG_EOR", "MSG_OOB" ve "MSG_NOSIGNAL" sembolik sabitlerinin "bitwise-OR" işlemine sokularak elde edilebilecek bir bayrak değeridir. Bu sembolik sabitlerin anlamlarına daha sonra değineceğiz. Eğer bu son parametreye "0" geçilirse, fonksiyonun davranışı "write" gibi olacaktır. Fonksiyon tampona yazdığı bayt miktarına geri döner, başarı durumunda. Hata durumunda ise "-1" e geri döner ve "errno" değişkeni uygun değere çekilir. Aslında programın akışının "send" fonksiyonundan geri dönmesi, bilginin karşı tarafa iletildiği anlamına GELMEMEKTEDİR. Çünkü bu fonksiyon sadece bir tampona yazmaktadır. İşletim sistemiyse o tampondan, "TCP/IP" kullandığımız, için "IP" paketleri oluşturarak göndermektedir. Bir diğer deyişle biz tampona yazarız, sonrasında işlemci bir döngü içerisinde paket paket gönderme işlemine başlar. Pekiyi o anda iş bu tampon doluysa ne olacaktır? İşte "send" fonksiyonu gönderilecek bilginin tamamı tampona aktarılan kadar blokeye yol açar, POSIX standartlarınca. Eğer fonksiyon blokesiz çalıştırılırsa, bloke bekleme yerine başarısızlıkla geri döner. Öte yandan karşı taraf "socket" i kapatmışsa, bu fonksiyon varsayılan senaryoda "SIGPIPE" sinyalinin oluşmasına yol açar. Anımsanacağı üzere borularda okuma yapan program boruyu kapatmışsa ve yazma yapan taraf yazma yaptığında da yine bu sinyal oluşmaktaydı. Eğer bu sinyalin oluşması istenmiyorsa, "send" fonksiyonunun son parametresine "MSG_NOSIGNAL" bayrağı geçilmelidir. Bu durumda "send" fonksiyonu başarısız olur ve "errno" değişkeni "EPIPE" değerini alır. Buraya kadarki aşamalar doğrultusunda birbiriyle haberleşen iki program yazabiliriz. Pekiyi haberleşmenin sonlandırılma süreci nasıl olacaktır? Anımsanacağı üzere UNIX ve türevi sistemlerde "socket" nesnelerine ilişkin betimleyiciler birer "fd" olarak ele alınır. Dolayısıyla "close" fonksiyonları ile bu betimleyicileri kapatabiliriz. Pekala bu "close" çağrısını yapmasak bile, prosesin herhangi bir şekilde sonlanması üzerine işletim sistemi açık dosya betimleyicilerini kapatacağı için, bizim "fd" de yine kapatılacaktır. Ancak "Active Socket" nesnelerinin bu şekilde "close" ile kapatılması tavsiye edilmez. Çünkü, -> Direkt "close" ile betimleyiciyi kapatırsak, o "socket" nesnesine ilişkin veri yapıları ve bağlantı bilgileri de silineceği için, "send" / "recv" fonksiyonları ile bilgi gönderip/alamayabiliriz. Örneğin, "send" / "recv" işleminden hemen sonra "close" işlemi yaparsak, daha pakek karşıya gönderilmeden/tampondakilerin alması bitmeden silme işlemi gerçekleşebilir. Yani paketin karşıya gönderilmesi/alınması GARANTİ DEĞİLDİR. Çünkü "send" / "recv" sadece tampona yazmakta/tampondan okuma yapmaktadır. "close" ile bu tampon da silineceğinden, bilgiyi göndermiş olmamız / bilgiyi tampondan çekmiş olmamız kesin değildir. Bir diğer deyişle "close" çağrısını bilgisayarın kapatırken "power" tuşuna basmak gibi de düşünebiliriz. Halbuki "shutdown" fonksiyon çağrısı, bilgisayarı "shutdown" işlevi ile kapatmak, gibidir. Yani, işletim sistemini "shutdown" ettiğimizde tüm prosesler uygun biçimde sonlandırılıp sistem stabil olarak kapatılmaktadır. Dolayısıyla önce kontrollü bir şekilde haberleşme sonlandırılmalı, daha sonra "fd" ler kapatılmalıdır. Dolayısıyla bizler sırasıyla "shutdown" ve "close" fonksiyonlarını çağırmalıyız. İşte kapatma işleminin bu fonksiyonlar ile yapılması durumuna ise "Graceful Close (Zarif Kapatma)" denir. Bu fonksiyonlardan, -> "shutdown" : Bu fonksiyonun temelde üç işlevi vardır. Bunlar haberleşmeyi TCP çerçevesinde el sıkışarak sonlandırmak ki bu konu daha sonra işlenecektir, "send" ile tampona yazılanların gönderildiğinden emin olmak ve "read" / "write" işlemini sonlandırıp diğer işlemlere devam edebilmek ki bu duruma da aslında "Half Close" işlemi denir. "shutdown" fonksiyonunun prototipi aşağıdaki gibidir. #include int shutdown(int socket, int how); Fonksiyonunun birinci parametresi o "socket" nesnesin ilişkin betimleyici, ikinci parametresi ise sonlandırma biçimini belirtir. Bu ikinci parametre ise "SHUT_RD", "SHUR_WR" ve "SHUT_RDWR" sembolik sabitlerden birisini alır. Bu sabitler ise sırasıyla soketten artık okuma yapılmayacağına ki sokete yazma yapabiliriz, sokete artık yazma yapılmayacağına ki soketten okuma yapılabilir ve soketten ne okuma ne yazma yapılmayacağını belirtir. Bu sembolik sabitlerden "SHUR_WR" ve "SHUT_RDWR" kullanılması halinde "shutdown" fonksiyonu, "send" ile tampona yazdıklarımızın karşıya gönderilmesinden emin olur. Yani bu iki sembolik sabit kullanılırsa, bilgiler karşıya gönderilene kadar blokeye yol açar. "shutdown" fonksiyonu başarı durumunda "0", hata durumunda ise "-1" değerine geri döner. Bu fonksiyon sadece "Active Socket / Listening Socket" için çağrılmalıdır. -> "close" : Borularıdaki "close" fonksiyonudur. "TCP/IP" konusundaki şu noktalar da önemlidir: -> "socket" nesnelerine ilişkin "fd" ler "dup" işlemi ile çoğaltılabilmektedir. Bu durumda "close" sonrasında "socket" nesnesi henüz yok edilmez eğer ona ilişkin başka "fd" değişkenler de varsa. Benzer biçimde "fork" işlemi ile yine o "socket" nesnesine ait "fd" nin çoğaltılabileceğini unutmayınız. -> "send" ve "recv" fonksiyonlarında kullanılan tampona "Network Buffer" ismi verilmiştir. İşletim sistemi "send" için ayrı bir tampon, "recv" için ayrı bir tampon tutmaktadır. Anımsanacağı üzere "send" fonksiyonu kendi tamponuna yazıyor, "recv" ise kendi tamponundan okuma yapmaktadır. Bu iki tampon arasındaki bilgi aktarımı ise "IP" paketi olarak, işletim sisteminin görevidir. Dolayısıyla akışın "send" fonksiyonundan geri dönmesi demek tampona yazılanların karşı tarafa iletildiği ANLAMINA GELMEMEKTEDİR. -> "socket" programlamada bir tarafın tek bir "send" / "write" çağrısı ile tampona yazdıklarını diğer taraf birden fazla "read" / "recv" çağrısı ile alabilir. Benzer şekilde birden fazla kez çağrılan "send" / "write" ile tampona yazma yapıldığında, bu tek bir "read" / "recv" çağrısı ile alınmayabilir. Dolayısıyla bizler bir döngü içerisinde okuma ya da yazma yapmak durumunda kalabiliriz. Aşağıda garantili okumaya ilişkin bir örnek fonksiyon verilmiştir: ssize_t read_socket(int sock, char *buf, size_t len) { size_t left, index; left = len; index = 0; while (left > 0) { if ((result = recv(sock, buf + index, left, 0)) == -1) return -1; if (result == 0) break; index += result; left -= result; } return (ssize_t)index; } Bu fonksiyonun çalışması oldukça basittir. "recv" ile her defasında "left" kadar bayt okunmak istenmiştir. Ancak "left" kadar değil, "result" kadar bayt okunmuş da olabilir. Bu durumda "left", okunan miktar kadar azaltılmış ve "index" ise o miktar kadar artırılmıştır. Programın akışı bu fonksiyondan şu durumlarda çıkacaktır; Bağlantı kopması ve "recv" in başarısız olması, karşı tarafın soketi kapatması ve "recv" in "0" ile geri dönmesi, istenen miktar kadar okunmuş olması. Özetle; bir soketten "n-byte" okuma işlemi tek bir "recv" ile olmak zorunda değildir. Soket programlamaya yeni başlayanlar, sanki bir disk dosyasından ya da borudan bilgi okunuyor gibi, tek bir okuma çağrısı ile bunu yapma eğilimindedirler. Halbuki bu işlem yukarıdaki gibi bir döngüyle ya da "recv" fonksiyonuna "MSG_WAITALL" bayrağı girilerek yapılmak zorundadır. Fakat "MSG_WAITALL" bayrağı, alma tamponundan daha yüksek miktarda verilerin okunması için uygun olmayabilmektedir. Bu konu ileride ele alınacaktır. -> Bilindiği gibi "inet_ntoa" fonksiyonu dört baytlık "IPv4" adresini noktalı desimal formata, "inet_addr" fonksiyonu da bu işin tersini yapmaktaydı. İşte bu iki fonksiyonun "IPv6" adresini de kapsayan halleri de geliştirilmiştir. Bu fonksiyonlar ise sırasıyla "inet_ntop" ve "inet_pton" isimli fonksiyonlardır. Fonksiyonların prototipleri aşağıdaki gibidir. #include const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); int inet_pton(int af, const char *src, void *dst); Bu iki fonksiyonun birinci parametresi "IPv4" için "AF_INET", "IPv6" için "AF_INET6" olarak girilmelidir. Fonksiyonların ikinci parametresine; "inet_ntop" için dönüştürülecek nümerik "IPv4" ya da "IPv6" türündeki "IP" adresini temsil eden nesnenin adresini, "inet_pton" için noktalı desimal formatın bulunduğu yazının adresini alır. Fonksiyonların üçüncü parametresine; "inet_ntop" için dönüştürme sonucu elde edilen noktalı desimal formattaki yazının yazılacağı alanın başlangıç adresini, "inet_pton" için nümerik adresin yerleştirileceği adresi almakta ve bu adrese "IPv4" için 4 baytlık "IPv6" için 16 baytlık yerleştirme yapılır. Fonksiyonlar bu parametredeki adres alanlarına yazma YAPARLAR. "inet_ntop" fonksiyonunun son parametresi ise üçüncü parametresindeki dizinin uzunluğunu belirtir. Bu parametreye "IPv4" için "INET_ADDRSTRLEN", "IPv6" için "INET6_ADDRSTRLEN" sembolik sabitler girilebilir. Fonksiyonların geri dönüş değeri; "inet_ntop" için başarı durumunda üçüncü parametresine geçtiğimiz adres bilgisine ve başarısızlık durumunda "NULL" değerine, "inet_pton" için başarı durumunda "1" değerine ve başarısızlık durumunda "0" ya da "-1" değerine geri döner ki başarıszlığın kaynağı birinci parametre ise "-1", ikinci parametre ise "0" değeridir. Fonksiyonların kullanımı; "inet_ntop" için, char ntopbuf[INET_ADDRSTRLEN]; ... printf( "connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port) ); "inet_pton" için, if (inet_pton(AF_INET, server_name, &sin_server.sin_addr.s_addr) == 0) { ... } biçimindedir. -> Anımsanacağı üzere "gethostbyname" fonksiyonu "deprecated" edildiği için yerine "getaddrinfo" isimli fonksiyon eklenmiştir. Bu fonksiyon da yine "IPv6" yı destekler niteliktedir. Fonksiyonunun prototipi aşağıdaki gibidir. #include int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); Fonksiyonun birinci parametresi noktalı desimal formatındaki "IP" adresi veya "host" ismini alır. İkinci parametre ise port numarasını yazı olarak alır. Fakat "NULL" değeri geçilebilir. Bu durumda port numarası "0" olarak belirlenecektir. Fakat biz programcılar eğer port numarasının atıyorsak, bu parametreye "NULL" geçeriz. Diğer yandan bu ikinci parametreye "IP" ailesinin "Applicatino Layer" a ilişkin spesifik bir protokolün ismi de girilebilmektedir. Örneğin "http", "ftp" gibi. Bu durumda bu protokollere ilişikin port numaraları zaten bilindiğinden, o port numaraları girilmiş gibi işlem görülür. Fonksiyonun üçüncü parametresi ise nasıl bir "IP" adresi istediğimizi belirten filtreleme parametresidir. Bu parametre "addrinfo" yapı türünden "const" bir nesnenin adresini aldığından, ilgili nesnenin içini doldurduktan sonra fonksiyona geçmeliyiz. "addrinfo" yapı türü ise aşağıdaki gibi tanımlanmıştır. struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *ai_next; }; Bu yapının "ai_flags" elemanı pek çok bayrak değeri alabilmektedir. Bu değer 0 olarak da geçilebilir. Yapının "ai_family" elemanı ise "IPv4" için "AF_INET", "IPv6" için "AF_INET6", her ikisi için "AF_UNSPEC" sembolik sabitlerinden birisini alır. Yapının "ai_socktype" elemanı "0", "SOCK_STREAM" ya da "SOCK_DGRAM" sembolik sabitlerinden birisini alır. Yapının diğer elemanlarına ilişkin açıklamayı ilgili dökümandan temin edebiliriz. Ancak POSIX standartları yapının sadece ilk dört elemanını programcının doldurmasını, geri kalanlarını da sıfırlamasını tavsiye etmektedir. Pekala fonksiyonun bu üçüncü parametresine "NULL" değerini de geçebiliriz. Bu durumda ilgili "it referred to a structure containing the value zero for the ai_flags, ai_socktype, and ai_protocol fields, and AF_UNSPEC for the ai_family field" biçiminde olacaktır. Fonksiyonun son parametresi ise bir bağlı listenin ilk düğümünün adresini alır. Buradaki bağlı listedeki düğümleri birbirine bağlayan gösterici ise "addrinfo" yapısının "ai_next" isimli elemanıdır. Bu bağlı listede ise "host" a ilişkin birden fazla "IP" adresi olması durumunda doldurulur. Genellikle sadece bağlı listenin başındaki düğüm kontrol edilir fakat zaman zaman bağlı listenin baştan sonra gezinilmesi de gerekebilir. "getaddrinfo" fonksiyonu başarı durumunda "0", hata durumunda ise hata kodunun kendisine döner. Bu hata kodları "strerror" ile değil, "gai_strerror" ile yazıya dökülmelidir. "gai_strerror" fonksiyonunun prototipi aşağıdaki gibidir. #include const char *gai_strerror(int ecode); Öte yandan yukarıda bahsedilen bağlı listenin de günün sonunda boşaltılması gerekmektedir. Bunun için "freeaddrinfo" fonksiyonu çağrılır. Fonksiyonun prototipi şöyledir. #include void freeaddrinfo(struct addrinfo *ai); Fonksiyon yukarıda açıklanan bağlı listenin en başındaki düğümün adresini alır ve tüm bağlı listeyi boşaltır. "getaddrinfo" fonksiyonunun tipik kullanım biçimi ise aşağıdaki gibidir. struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; ... if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); ... Bu fonksiyon sayesinde artık "inet_addr" ya da "inet_pton" ile "server address" bilgisinin noktalı desimal formatta olup olmadığını sorgulamaya ve duruma göre "gethostbyname" ile "DNS" işlemi yapmaya ARTIK GEREK KALMAMIŞTIR. Çünkü bu iki işlem artık "getaddrinfo" fonksiyonu ile yapılmaktadır. -> Anımsanacağı üzere "gethostbyaddr" fonksiyonu "deprecated" edildiği için yerine "getnameinfo" isimli fonksiyon eklenmiştir. Bu fonksiyon da yine "IPv6" yı destekler niteliktedir. Fonksiyonunun prototipi aşağıdaki gibidir. #include int getnameinfo(const struct sockaddr *addr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags); Fonksiyonun birinci parametresi "sockaddr_in" ya da "sockaddr_in6" yapısını almaktadır. İkinci parametre birinci parametredeki yapının uzunluğudur. Fonksiyonun sonraki dört parametresi sırasıyla noktalı hostun yazısal temsilin yerleştirileği dizinin adresi ve uzunluğu, port numarasına ilişkin yazının (servis ismi) yerleştirileceği dizinin adresi ve uzunluğudur. Son parametre "0" geçilebilir. Maksimum "host" ismi "NI_MAXHOST" ile maksimum servis ismi ise "NI_MAXSERV" ile belirtilmiştir. -> Bir taraf "IPv4" kullanırken diğer taraf "IPv6" kullanabilir. -> "socket" haberleşmesinde kullanılan iki program daha vardır; "getpeername" ve "getsockname" fonksiyonları. Bu fonksiyonlardan "getpeername" fonksiyonu, bağlı bir soketi parametre olarak alır ve karşı tarafın "IP" adresini ve "port" numarasını bize "sockaddr_in" ya da "sockaddr_in6" biçiminde verir. Tabii aslında "server", bağlantıyı yaptığında karşı tarafın bilgisini zaten "accept" fonksiyonunda almaktadır. Dolayısıyla bu bilgiyi saklayarak daha sonra kullanabiliriz. Eğer saklamamışsak, "getpeername" ile istediğimiz zaman "get" edebiliriz. Fonksiyonun prototipi aşağıdaki gibidir. #include int getpeername(int sock, struct sockaddr *addr, socklen_t *addrlen); Fonksiyonun birinci parametresi soket betimleyicisidir. İkinci parametre duruma göre karşı tarafın bilgilerinin yerleştirileceği "sockaddr_in" ya da "sockaddr_in6" yapı nesnesinin adresini alır. Son parametre ikinci parametredeki yapının uzunluğunu belirtmektedir. Eğer buraya az bir uzunluk girilirse kırpma yapılır ve gerçek uzunluk verdiğimiz adresteki nesneye yerleştirilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Diğer fonksiyonumuz olan "getsockname" ise "getpeername" fonksiyonunun tersi işlemi yapmaktadır. Bu fonksiyon kendi bağlı soketimizin "IP" adresini ve "port" numarasını elde etmek için kullanılır. Genellikle bu fonksiyona gereksinim duyulmamaktadır. Burada bağlantı sağlandıktan sonra "client" programın bilgilerinin alınması ve gerektiğinde o bilgilerin kullanılması tavsiye edilir. Sık sık "gerpeername" çağrısı pek tavsiye edilmez. Fonksiyonun prototipi aşağıdaki gibidir. #include int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen); Fonksiyonun parametrik yapısı ve geri dönüş değeri getpeername fonksiyonundaki gibidir. -> "socket" haberleşmesinde "peer" kelimesi karşı tarafı betimlemektedir. Şimdi de "Client-Server" programlara ilişkin örnekleri inceleyelim: * Örnek 1, "Server-Client" program. /* client.c */ #include #include #include #include #include #include #include #define SERVER_NAME "127.0.0.1" #define SERVER_PORT 55555 #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(void) { /* Creation of a 'socket' */ int client_sock; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); /* Binding the 'socket' to a port */ /* // Optional { struct sockaddr_in sin_client; sin_client.sin_family = AF_INET; sin_client.sin_port = htons(50000); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } */ struct sockaddr_in sin_server; sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); /* Evaluating an IP address through 'DNS'. */ struct hostent *hent; if ((sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME)) == -1) { // 'gethostbyname' is absolute now. if ((hent = gethostbyname(SERVER_NAME)) == NULL) { fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno)); exit(EXIT_FAILURE); } memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } /* Sending connection requests to the 'server'. */ if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); printf("connected...\n"); /* Sending data to the 'server'. */ char buffer[BUFFER_SIZE]; char* str; for (;;) { printf("CSD:>"); fflush(stdout); if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buffer, '\n')) != NULL) *str = '\0'; if (send(client_sock, buffer, strlen(buffer), 0) == -1) exit_sys("send"); if (!strcmp(buffer, "quit")) break; } /* Closing the communication properly. */ close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* server.c */ #include #include #include #include #include #include #include #define SERVER_PORT 55555 #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(void) { /* Creation of a 'socket' */ int server_sock; struct sockaddr_in sin_server; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); /* Binding the 'socket' to a port */ sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (const struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); /* Listening the port */ if (listen(server_sock, 8) == -1) exit_sys("listen"); /* Evaluating connection requests in the connection queue. */ printf("waiting for connection...\n"); int client_sock; struct sockaddr_in sin_client; socklen_t sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf( "Connected Client => [%s:%d]\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port) ); /* Reading data sent from the 'client'. */ char buffer[BUFFER_SIZE + 1]; ssize_t result; for (;;) { if ((result = recv(client_sock, buffer, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); printf("[%jd] => ", (intmax_t)result); if (0 == result) break; buffer[result] = '\0'; if (!strcmp(buffer, "quit")) break; printf("[%s]", buffer); } /* Closing the communication properly. */ shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, "Server-Client" programlar genellikle komut satırından argüman alacak biçimde tasarlanırlar; "IP" adresini ve "port" numarasını. /* client.c */ #include #include #include #include #include #include #include #include #define SERVER_NAME "127.0.0.1" #define SERVER_PORT 55555 #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char* argv[]) { /* Checking the server IP address via 'Command Line Arguments' */ int server_port; if (argc < 2) server_port = SERVER_PORT; else if (argc == 3) server_port = atoi(argv[1]); else { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } /* Creation of a 'socket' */ int client_sock; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); /* Binding the 'socket' to a port */ /* // Optional { struct sockaddr_in sin_client; sin_client.sin_family = AF_INET; sin_client.sin_port = htons(50000); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } */ printf("Connecting to Port: %d\n", server_port); struct sockaddr_in sin_server; sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); /* Evaluating an IP address through 'DNS'. */ struct hostent *hent; if ((sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME)) == -1) { // 'gethostbyname' is absolute now. if ((hent = gethostbyname(SERVER_NAME)) == NULL) { fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno)); exit(EXIT_FAILURE); } memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } /* Sending connection requests to the 'server'. */ if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); printf("connected...\n"); /* Sending data to the 'server'. */ char buffer[BUFFER_SIZE]; char* str; for (;;) { printf("CSD:>"); fflush(stdout); if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buffer, '\n')) != NULL) *str = '\0'; if (send(client_sock, buffer, strlen(buffer), 0) == -1) exit_sys("send"); if (!strcmp(buffer, "quit")) break; } /* Closing the communication properly. */ close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* server.c */ #include #include #include #include #include #include #include #include #define SERVER_PORT 55555 #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char* argv[]) { /* Checking the port number via 'Command Line Arguments' */ int server_port; if (argc == 1) server_port = SERVER_PORT; else if (argc == 2) server_port = atoi(argv[1]); else { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } /* Creation of a 'socket' */ int server_sock; struct sockaddr_in sin_server; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); /* Binding the 'socket' to a port */ sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (const struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); /* Listening the port */ printf("Listening Port: %d\n", server_port); if (listen(server_sock, 8) == -1) exit_sys("listen"); /* Evaluating connection requests in the connection queue. */ printf("waiting for connection...\n"); int client_sock; struct sockaddr_in sin_client; socklen_t sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf( "Connected Client => [%s:%d]\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port) ); /* Reading data sent from the 'client'. */ char buffer[BUFFER_SIZE + 1]; ssize_t result; for (;;) { if ((result = recv(client_sock, buffer, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); printf("[%jd] bytes received: ", (intmax_t)result); if (0 == result) break; buffer[result] = '\0'; if (!strcmp(buffer, "quit")) break; printf("[%s]", buffer); } /* Closing the communication properly. */ shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3.0, Yine "Server-Client" programımız için belli başlı seçenekler de tayin edebiliriz. /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_server, sin_client; struct hostent *hent; char buf[BUFFER_SIZE]; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int server_port, bind_port; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = atoi(optarg); break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); if ((sin_server.sin_addr.s_addr = inet_addr(server_name)) == -1) { if ((hent = gethostbyname(server_name)) == NULL) { fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno)); exit(EXIT_FAILURE); } memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* server.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char buf[BUFFER_SIZE + 1]; ssize_t result; int option; int server_port; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); for (;;) { if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3.1, Yukarıdaki örneklerde yazdığımız "Server-Client" programın, güncel fonksiyonlar kullanılmış hali: /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE]; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* server.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char buf[BUFFER_SIZE + 1]; char ntopbuf[INET_ADDRSTRLEN]; ssize_t result; int option; int server_port; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); for (;;) { if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi "TCP/IP" kullanımı sırasında "server" programa bağlanan birden fazla "client" program varsa, "read" / "recv" ile o "client" için okuma yaptığında henüz bilgi gelmemişse, "server" program bloke olacaktır. Dolayısıyla diğer "client" programlardan gelenleri okuyamayacaktır. Dolayısıyla bu durumda "Advanced IO" yöntemlerinin kullanılması gerekmektedir. Anımsanacağı üzere bu teknikler şunlardır; "Multiplexed IO", "Signal Driven IO", "Asynchronous IO". Aslında bu teknikleri biz boruları üzerinde işlemiştik fakat bu tekniklerin soketler üzerindeki kullanımları da aynıdır. Diğer yandan birden fazla "client" program bir "server" a bağlandığında, "server" programın bir döngü içerisinde "accept" çağrısı yapması gerekmektedir. Aynı zamanda da bağlanan "client" programlar ile de haberleşebilmesi gerekmektedir ki "accept" fonksiyonu da varsayılan senaryoda blokeye yol açmaktadır. İşte bu iki problemi gidermek için şu yaklaşımlar geliştirilmiştir: -> En basit fakat bir o kadar da verimsiz olan "fork" yöntemidir. Çünkü "fork" işlemi çok maliyetli bir işlemdir. Eğer az sayıda "client" bağlanacaksa tercih edilebilir. Bu modelde her "accept" işleminden sonra "fork" çağrısı yapılır. Böylece alt proses, bağlanan "client" ile haberleşmeye başlar. Üst proses de yine diğer "client" ların bağlanması için bir döngü içerisinde "accept" yapmaya devam eder. Fakat burada üst proses alt prosesin SONLANMASINI BEKLEMEMELİDİR. AKSİ HALDE ÜST PROSES DE BLOKE OLACAĞINDAN, "fork" İŞLEMİNİN BİR ANLAMI KALMAYACAKTIR. Diğer yandan alt prosesi beklemezsek, "zombie process" oluşacaktır. Dolayısıyla "SIGCHLD" sinyalini bizler "handler" etmeliyiz. Böylece üst proses alt prosesi beklemeyecektir. Bu yöntemdeki kritik nokta, "fork" işlemiyle üst prosesin bellek alanının alt prosese kopyalanmasıdır. Böylelikle "accept" ile elde ettiğimiz "Listening Socket" aslında alt prosese geçmiştir. -> Bir diğer yöntem ise "thread" yöntemidir. Bu yöntem, "fork" yönteminden daha az maliyetlidir. Bu yöntemde her "accept" işleminden sonra bir "thread" oluşturulur. "thread" ler aynı bellek alanını kullandığından, kullanacağımız parametrelere dikkat etmeliyiz. Fakat oluşturulan "thread" ler "detach" edilmelidir ki "zombie thread" oluşmasın. -> Bir diğer yöntem ise "select" yöntemidir. Biz bu yöntemi daha önce boruları üzerinde görmüştük. "socket" üzerindeki kullanımı da çok benzerdir. Şöyleki; "socket" nesnesine bilgi geldiğinde, karşı taraf "socket" nesnesini kapattığında ve yeni bir bağlantı isteği oluştuğunda bu durum, "select" tarafından, "read" olayı olarak yorumlanır. Diğer yandan işin başında "Passive Socket", "select" fonksiyonunun okuma kümesine, eklenir. Eğer "client" program tarafından yeni bir bağlantı isteği gelirse, takip edilen "Passive Socket" nezdinde beklenen okuma olayı gerçekleşmiş demektir. Dolayısıyla bizler "accept" çağrısını o "client" için yapabiliriz. Fakat "accept" ile elde ettiğimiz "Active Socket" nesnesini, "select" fonksiyonunun okuma kümesine eklememiz gerekmektedir. Artık "client" ile bilgi alışverişinde bulunabiliriz. Son olarak, karşı tarafın "socket" nesnesini kapattığını "recv" fonksiyonu ile anladığımız zaman, bizler de o "client" programa ilişkin "Active Socket" nesnesini kontrollü bir şekilde kapatmamız gerekmektedir. -> Bir diğer yöntem ise "poll" yöntemidir. "select" modeline benzerdir. Bu modeli kullanırken dikkat edilmesi gereken noktalar şunlardır; İlk olarak bir adet "Passive Socket" oluştururuz ve bunu "pollfd" dizisinin içerisine yerleştiririz. Anımsanacağı üzere bu dizinin elemanlarını takip edeceğiz, dinleyeceğiz. Daha sonra bu dizininin elemanları olan ilgili "pollfd" yapısının "events" isimli elemanına "POLLIN" değerini atarız. Devamında ise, yani "poll" fonksiyonundan geri döndüğümüzde, "pollfd" dizisini gezeriz ve elemanlarının sahip olduğu "revents" elemanlarını "POLLIN" değeriyle karşılaştırırız. Yeni bir "client" bağlanmışsa, "POLLIN" oluşacaktır. Dolayısıyla "accept" yapabiliriz. Fakat "accept" ile elde ettiğimiz "Active Socket" i de "pollfd" dizisine eklemeliyiz. Yani o "client" ın sahip olduğu "pollfd" bilgileri, "pollfd" dizisine eklenmelidir. Öte yandan karşı taraf soket nesnesini kapattığında yine "POLLIN" oluşacaktır. Bu durumda "recv" fonksiyonunun geri döndürdüğü değere bakarız. Eğer "0" ise "pollfd" dizisinden o "client" in sahip olduğu "pollfd" bilgileri çıkartılmalıdır. -> Bir diğer yöntemse "epoll" yöntemidir. Anımsanacağı üzere "epoll" fonksiyonları Linux dünyasında en verimli fonksiyonlardı. Bu fonksiyonlardan "epoll_create" ya da "epoll_create1" ile bir betimleyici oluşturuyor, daha sonra bu betimleyiciyi "epoll_ctl" ile izleme listesine ekliyor ve "epoll_wait" ile de izleme işlemini gerçekleştiriyorduk. Fakat "server" yazarkenki kritik noktalar ise şunlardır; ilk önce "Passive Socket" izleme listesine eklenmelidir. Soketten yapılan okuma işlemleri "EPOLLIN" olayına neden olmaktadır. Dolayısıyla bu olay oluştuğunda, olaya konu soketin ilgili "Passive Socket" olup olmadığı da sınanmalıdır. Eğer öyleyse, "accept" yapmalı ve "Active Socket" elde etmeliyiz. Karşı taraf soketi kapattığında ise hem "EPOLLIN" hem de "EPOLLERR" olayları oluşmaktadır. Dolayısıyla "EPOLLIN" oluştuğunda yine "recv" çağrısının geri dönüş değerini de kontrol etmeliyiz. Eğer bu değer "0" ise soketin karşı tarafça kapatıldığını düşünerek, biz de "Active Socket" nesnemizi kapatmalıyız. "Active Socket" in de kapatılmasıyla izleme işlemi de otomatik sona ereceğinden, yeniden "epoll_ctl" çağrısına gerek yoktur. Son olarak, karşı tarafın soketi kapatmasından dolayı oluşan "EPOLLIN" ve "EPOLLERR" olaylarında, "getpeername" fonksiyonu KULLANILMAMALIDIR. (Halbuki "select" ve "poll" fonksiyonlarında, bu durumda, "getpeername" fonksiyonu kullanılabilmektedir.) Öte yandan bu modeli kullanırken şu noktaya da dikkat etmemiz gerekmektedir; Anımsanacağı gibi "epoll" modelinde varsayılan izleme biçimi "Düzey Tetiklemeli (Level Triggered)" biçimdedir. Örneğin, Düzey Tetiklemeli izlemede sokete bilgi gelmiş olsun. Bu durumda "epoll_wait" yapıldığında "EPOLLIN" olayı gerçekleşecektir. Ancak eğer biz sokete gelen tüm bilgileri okumazsak, "epoll_wait" fonksiyonunu bir daha çağırdığımızda, yine "EPOLLIN" olayı gerçekleşecektir. Çünkü Düzey Tetiklemede sokette okunacak bilgi olduğu sürece, "epoll_wait" çağrısı, hep bu olayı oluşturacaktır. Ancak Kenar Tetiklemede durum böyle değildir. Kenar Tetiklemeli modda sokete bilgi gelmiş olsun. Bu durumda "epoll_wait" yapıldığında "EPOLLIN" olayı gerçekleşecektir. Biz bu olayda soketteki tüm bilgileri okumasak bile, artık "epoll_wait" fonksiyonunu çağırdığımızda, "EPOLLIN" olayı oluşmayacaktır. "EPOLLIN" olayı, bu modda, yalnızca sokete yeni bir bilgi geldiğinde oluşmaktadır. Dolayısıyla bu modda çalışırken şu noktalara dikkat etmeliyiz; İlk olarak "accept" ile elde ettiğimiz "Active Socket" nesnesine ait betimleyiciyi, Kenar Tetiklemeli modda izlemek için, "epoll_event" yapısının "events" elemanına "EPOLLET" bayrağının eklenmesi gerekmektedir. Daha sonra Kenar Tetiklemeli modda "Active Socket" e bilgi geldiğinde gelen bilgilerin hepsinin okunmasına gayret edilmelidir. Çünkü eğer biz sokete bilgi geldiğinde onların hepsini okumazsak, bir daha "EPOLLIN" olayı ancak yeni bir bilgi geldiğinde oluşacağından, gelmiş olan bilgilerin işleme sokulması gecikebilecektir. (Halbuki Düzey Tetiklemeli modda gelen bilgilerin hepsi okunmasa bile bir sonraki "epoll_wait" çağrısında yine "EPOLLIN" olayı gerçekleşeceği için böyle bir durum söz konusu olmayacaktır.) Diğer yandan Kenar Tetiklemeli modda sokete gelen tüm bilgilerin okunması için "Active Socket" nesnesine ait betimleyicinin blokesiz modda olması gerekir. Aksi takdirde "recv" ya da "read" yaparken sokette bilgi kalmamışsa, bloke oluşacaktır. Soket varsayılan olarak blokeli modda olup, blokesiz moda sokmak için aşağıdaki yöntemi kullanabiliriz. if (fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); Öte yandan blokesiz modda "recv" ya da "read" çağrıları ile, bu çağrılar başarısız olana kadar, okuma işlemi şu şekilde yapılabilir. for (;;) { if ((result = recv(...)) == -1) { if (errno == EAGAIN) break; exit_sys("recv"); } // ... } Son olarak, aslında, "epoll" modelinde bazı soket betimleyicileri Düzey Tetiklemeli bazıları ise Kenar Tetiklemeli modda olabilir. Yani, "Passive Socket" nesnesi Düzey Tetiklemeli modda tutup diğerlerini Kenar Tetiklemeli modda tutabilirsiniz. -> Bir diğer modelimiz ise "Asynchronous IO" modelinde kullanılan ve isimleri "aio_" ön ekine sahip fonksiyonları kullanmaktır. Anımsanacağı üzere bu modelde mekanizma başlatılıyor fakat bizim akışımız akmaya devam ediyordu. Sadece beklenen olay sona erdiğinde bize bilgisi geçiliyordu. Daha öncesinde bu modeli borular üzerinde işlemiştik. Bu modeli kullanırken şu noktalara dikkat etmeliyiz; "accept" işleminin bu mekanizmaya dahil edilmesi gerekmemektedir. Yani akış "accept" işleminde bloke olabilir. Tabii istenirse "accept" işlemi de bu mekanizmaya dahil edilebilir. Çünkü "accept" işlemi de bir okuma durumu oluşturmaktadır. Diğer yandan, bir okuma (ya da yazma) olayından sonra, yeniden aynı mekanizmanın "aio_read" fonksiyonu çağrılarak kurulması gerekmektedir. Yani "aio_read" bir kez değil, her defasında yeniden çağrılmalıdır. Şimdi de bu anlatılanlar doğrultusunda, "Client-Server" program örnekleri inceleyelim: * Örnek 1, "fork" yapılarak oluşturulan "Client-Server". /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void client_proc(int sock, struct sockaddr_in *sin); char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; int p_flag, err_flag; pid_t pid; struct sigaction sa; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; sa.sa_handler = SIG_IGN; // 'SIGCHLD' is ignored. if (sigaction(SIGCHLD, &sa, NULL) == -1) exit_sys("sigaction"); if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { client_proc(client_sock, &sin_client); exit(EXIT_SUCCESS); } } close(server_sock); return 0; } void client_proc(int sock, struct sockaddr_in *sin) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char ntopbuf[INET_ADDRSTRLEN]; unsigned port; ssize_t result; inet_ntop(AF_INET, &sin->sin_addr, ntopbuf, INET_ADDRSTRLEN); port = (unsigned)ntohs(sin->sin_port); for (;;) { if ((result = recv(sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, port, buf); revstr(buf); if (send(sock, buf, result, 0) == -1) exit_sys("send"); } printf("client disconnected %s:%u\n", ntopbuf, port); shutdown(sock, SHUT_RDWR); close(sock); } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, "thread" oluşturmak suretiyle oluşturulan "Client-Server". Burada ilgili "thread" e "client" programın özellikleri "CLIENT_INFO" yapısı ile verilmiştir. Bu yapı dinamik ömürlü olduğundan, işlemler sonrasında tekrardan geri verilmiştir. /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); typedef struct tagCLIENT_INFO { int sock; struct sockaddr_in sin; } CLIENT_INFO; /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; int p_flag, err_flag; pthread_t tid; CLIENT_INFO *ci; int result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sin = sin_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char ntopbuf[INET_ADDRSTRLEN]; unsigned port; ssize_t result; CLIENT_INFO *ci = (CLIENT_INFO *)param; inet_ntop(AF_INET, &ci->sin.sin_addr, ntopbuf, INET_ADDRSTRLEN); port = (unsigned)ntohs(ci->sin.sin_port); for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, port, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("client disconnected %s:%u\n", ntopbuf, port); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, "select" modeline ilişkin bir örnektir. Bu örnekte "select" fonksiyonunun blokesi çözüldüğünde önce betimleyicinin dinleme soketine ilişkin betimleyici olup olmadığına bakılmıştır. Eğer betimleyici dinleme soketine ilişkinse, "accept" işlemi uygulanmıştır. Değilse "recv" işlemi uygulanmıştır. Karşı taraf soketi kapattığında "recv" fonksiyonu "0" ile geri dönecektir. Bu durumda ilgili soket okuma kümesinden çıkartılmıştır. Pekala bu örnek nezdinde de, bağlanan her "client" için, yine bir "CLIENT_INFO" yapısı tahsis edilebilirdi. İş bu "client" bilgileri, bu yapının içinde saklanabilirdi. Sonra da dosya betimleyicisinden hareketle, "CLIENT_INFO" nesnesine hızlı bir biçimde erişmek için, "hash tablosu" oluşturulabilirdi. /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sinaddr_len; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; int p_flag, err_flag; fd_set rset, tset; int maxfds; ssize_t result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); FD_ZERO(&rset); FD_SET(server_sock, &rset); maxfds = server_sock; printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); tset = rset; if (select(maxfds + 1, &tset, NULL, NULL, NULL) == -1) exit_sys("select"); for (int fd = 0; fd <= maxfds; ++fd) if (FD_ISSET(fd, &tset)) { if (fd == server_sock) { sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); FD_SET(client_sock, &rset); if (client_sock > maxfds) maxfds = client_sock; inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("connected client ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } else { sinaddr_len = sizeof(sin_client); if (getpeername(fd, (struct sockaddr *)&sin_client, &sinaddr_len) == -1) exit_sys("getpeername"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); if ((result = recv(fd, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result > 0) { buf[result] = '\0'; if (!strcmp(buf, "quit")) goto DISCONNECT; printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (send(fd, buf, result, 0) == -1) exit_sys("send"); } else { /* result == 0 */ DISCONNECT: shutdown(fd, SHUT_RDWR); close(fd); FD_CLR(fd, &rset); printf("client disconnected ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } } } } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, "poll" modeline ilişkin bir örnektir. Burada pfds isimli bir pollfd dizisini oluşturulmuştur. Bu dizinin maksimum uzunluğu MAX_CLIENT kadardır. Her bağlantı sağlandığında yeni client için bu pollfd dizisine bir eleman eklenmiştir. Bir client disconnect olduğunda bu diziden ilgili eleman silinmiştir. /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 #define MAX_CLIENT 1000 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sinaddr_len; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; int p_flag, err_flag; struct pollfd pfds[MAX_CLIENT]; int npfds, count; ssize_t result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); pfds[0].fd = server_sock; pfds[0].events = POLLIN; npfds = 1; printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); if (poll(pfds, npfds, -1) == -1) exit_sys("poll"); count = npfds; for (int i = 0; i < count; ++i) { if (pfds[i].revents & POLLIN) { if (pfds[i].fd == server_sock) { if (npfds >= MAX_CLIENT) { fprintf(stderr, "number of clints exceeds %d limit!...\n", MAX_CLIENT); continue; } sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); pfds[npfds].fd = client_sock; pfds[npfds].events = POLLIN; ++npfds; inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("connected client ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } else { sinaddr_len = sizeof(sin_client); if (getpeername(pfds[i].fd, (struct sockaddr *)&sin_client, &sinaddr_len) == -1) exit_sys("getpeername"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); if ((result = recv(pfds[i].fd, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result > 0) { buf[result] = '\0'; if (!strcmp(buf, "quit")) goto DISCONNECT; printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (send(pfds[i].fd, buf, result, 0) == -1) exit_sys("send"); } else { DISCONNECT: shutdown(pfds[i].fd, SHUT_RDWR); close(pfds[i].fd); pfds[i] = pfds[npfds - 1]; --npfds; printf("client disconnected ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } } } } } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5.0, "epoll" modeline ilişkin bir örnektir. Burada Düzey Tetiklemeli hali kullanılmıştır. Daha önce borular kullanılarak bir gerçekleştirim sağlanmıştı. Soketlerle de işlemler benzer biçimde yürütülmektedir. /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 #define MAX_EVENTS 1024 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sinaddr_len; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; struct epoll_event ee; struct epoll_event ree[MAX_EVENTS]; int p_flag, err_flag; int epfd; int nevents; ssize_t result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); if ((epfd = epoll_create(1024)) == -1) exit_sys("epoll_create"); ee.events = EPOLLIN; ee.data.fd = server_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_sock, &ee) == -1) exit_sys("epoll_ctl"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); if ((nevents = epoll_wait(epfd, ree, MAX_EVENTS, -1)) == -1) exit_sys("epoll_wait"); for (int i = 0; i < nevents; ++i) { if (ree[i].events & EPOLLIN) { if (ree[i].data.fd == server_sock) { sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); ee.events = EPOLLIN; ee.data.fd = client_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &ee) == -1) exit_sys("epoll_ctl"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("connected client ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } else { if ((result = recv(ree[i].data.fd, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); buf[result] = '\0'; if (result > 0) { sinaddr_len = sizeof(sin_client); if (getpeername(ree[i].data.fd, (struct sockaddr *)&sin_client, &sinaddr_len) == -1) exit_sys("getpeername"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (send(ree[i].data.fd, buf, result, 0) == -1) exit_sys("send"); } else { shutdown(ree[i].data.fd, SHUT_RDWR); close(ree[i].data.fd); printf("client disconnected\n"); } } } } } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5.1, Yine "epoll" modeline ilişkin bir örnektir. Fakat burada artık Kenar Tetiklemeli hali kullanılmıştır. Fakat bu örnek Kenar Tetikleme için güzel bir örnek değildir. Bir nevi temsilidir. /* server.c */ #include #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 #define MAX_EVENTS 1024 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sinaddr_len; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ socklen_t sin_len; char ntopbuf[INET_ADDRSTRLEN]; int option; int server_port; struct epoll_event ee; struct epoll_event ree[MAX_EVENTS]; int p_flag, err_flag; int epfd; int nevents; ssize_t result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); if ((epfd = epoll_create(1024)) == -1) exit_sys("epoll_create"); ee.events = EPOLLIN; ee.data.fd = server_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_sock, &ee) == -1) exit_sys("epoll_ctl"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); if ((nevents = epoll_wait(epfd, ree, MAX_EVENTS, -1)) == -1) exit_sys("epoll_wait"); for (int i = 0; i < nevents; ++i) { if (ree[i].events & EPOLLIN) { if (ree[i].data.fd == server_sock) { sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); if (fcntl(client_sock, F_SETFL, fcntl(client_sock, F_GETFL) | O_NONBLOCK) == -1) exit_sys("fcntl"); ee.events = EPOLLIN|EPOLLET; ee.data.fd = client_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &ee) == -1) exit_sys("epoll_ctl"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("connected client ===> %s:%u\n", ntopbuf, (unsigned)ntohs(sin_client.sin_port)); } else { for (;;) { if ((result = recv(ree[i].data.fd, buf, BUFFER_SIZE, 0)) == -1) { if (errno == EAGAIN) break; exit_sys("recv"); } buf[result] = '\0'; if (result > 0) { sinaddr_len = sizeof(sin_client); if (getpeername(ree[i].data.fd, (struct sockaddr *)&sin_client, &sinaddr_len) == -1) exit_sys("getpeername"); inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (send(ree[i].data.fd, buf, result, 0) == -1) exit_sys("send"); } else { shutdown(ree[i].data.fd, SHUT_RDWR); close(ree[i].data.fd); printf("client disconnected\n"); break; } } } } } } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 6, "Asynchronous IO" modeline ilişkin bir örnektir. /* server.c */ #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 #define MAX_EVENTS 1024 void io_proc(union sigval sval); char *revstr(char *str); void exit_sys(const char *msg); typedef struct tagCLIENT_INFO { struct aiocb cb; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ char ntopbuf[INET_ADDRSTRLEN]; unsigned port; } CLIENT_INFO; /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; int option; int server_port; CLIENT_INFO *ci; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); for (;;) { printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); if ((ci = (CLIENT_INFO *)calloc(1, sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } inet_ntop(AF_INET, &sin_client.sin_addr, ci->ntopbuf, INET_ADDRSTRLEN); ci->port = ntohs(sin_client.sin_port); printf("connected client ===> %s:%u\n", ci->ntopbuf, ci->port); ci->cb.aio_fildes = client_sock; ci->cb.aio_offset = 0; ci->cb.aio_buf = ci->buf; ci->cb.aio_nbytes = BUFFER_SIZE; ci->cb.aio_reqprio = 0; ci->cb.aio_sigevent.sigev_notify = SIGEV_THREAD; ci->cb.aio_sigevent.sigev_value.sival_ptr = ci; ci->cb.aio_sigevent.sigev_notify_function = io_proc; ci->cb.aio_sigevent.sigev_notify_attributes = NULL; if (aio_read(&ci->cb) == -1) exit_sys("aio_read"); } close(server_sock); return 0; } void io_proc(union sigval sval) { CLIENT_INFO *ci = (CLIENT_INFO *)sval.sival_ptr; ssize_t result; if ((result = aio_return(&ci->cb)) == -1) exit_sys("aio_return"); ci->buf[result] = '\0'; if (result > 0) { printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ci->ntopbuf, (ci->port), ci->buf); revstr(ci->buf); if (send(ci->cb.aio_fildes, ci->buf, result, 0) == -1) exit_sys("send"); if (aio_read(&ci->cb) == -1) exit_sys("aio_read"); } else { shutdown(ci->cb.aio_fildes, SHUT_RDWR); close(ci->cb.aio_fildes); printf("client disconnected ===> %s:%u\n", ci->ntopbuf, (ci->port)); free(ci); } } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; ssize_t result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%s\n", buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "UDP/IP" : Anımsanacağı üzere bu protokol, bağlantılı olmayan bir protokoldür. Bir taraf diğer tarafa hiç bağlanmadan, onun "IP" adresini ve "port" numarasını bilerek, "UDP" paketlerini gönderebilir. Fakat gönderen taraf, alan tarafın paketi alıp almadığını bilmez. Yani "UDP" protokolünde bir akış kontrolü yoktur. Dolayısıyla alan taraf pekala bilgi kaçırabilir. Bu protokol, kaçırılan bu bilgilerin telafisini kendisi yapmamaktadır. Halbuki "TCP" protokülünde, bir bağlantı oluşturulduğu için bir akış kontrolü uygulanarak, karşı tarafa ulaşmamış "TCP" paketlerinin yeniden gönderilmesi sağlanmaktadır. Diğer yandan "UDP", "TCP" ye göre daha hızlıdır. Zaten "TCP" bir bakıma "UDP" nin organize edilmiş bağlantılı biçimidir. Pekiyi "UDP" protokolü ile ağ katmanı protokolü olan "IP" protokolü arasındaki fark nedir? Açıkçası her iki protokol aslında paketlerin iletimini yapmaktadır. Aslında "UDP" protokolünün gerçekten de "IP" protokolünden çok farkı yoktur. Ancak "UDP", bir "Transport Layer" protokolü olduğu için, "port" numarası içermektedir. Halbuki "IP" protokolünde port numarası kavramı yoktur. Yani "IP" protokolünde biz bir "host" cihaza paket gönderebiliriz. Fakat onun belli bir "port" una paket gönderemeyiz. Bunun dışında "UDP" ile "IP" protokollerinin kullanımları konusunda yine de bazı farklılıklar vardır. Aslında biz programcı olarak doğrudan "IP" paketleri de gönderebiliriz. Buna "raw socket" kullanımı denilmektedir. >>>>> "Raw Socket" : Bazen "TCP" ve "UDP" yerine doğrudan "IP" protokolünü kullanmak isteyebiliriz. Buna soket programlamada "raw socket" denilmektedir. Diğer protokol ailelerinde de "raw socket", "Network Layer" protokolünü belirtmektedir. Biz kursumuzda "raw socket" işlemleri üzerinde durmayacağız. Ancak daha aşağı seviyeli çalışmalar için ya da örneğin "Transport Layer" gerçekleştirmek için "raw socket" kullanımını bilmek gerekir. Genel bir "raw soket" oluşturmak için, soket nesnesi yaratılırken, protokol ailesi için "AF_PACKET" girilir. Soket türü için de "SOCK_RAW" girilmelidir. "IP" protokolü için protokol ailesi yine "AF_INET" ya da "AF_INET6" girilip soket türü "SOCK_RAW" olarak girilebilir. Öte yandan, "TCP" protokolündeki "server" ve "client" kavramları, "UDP" protokolünde çok keskin değildir. Ancak yine de genellikle hizmet alan tarafa "client", hizmet veren tarafa "server" denilmektedir. UDP'de "client" daha çok gönderim yapan, "server" ise okuma yapan taraftır. Bütün bunlara ek olarak, "UDP" özellikle periyodik kısa bilgilerin gönderildiği ve alındığı durumlarda hız nedeniyle tercih edilmektedir. "UDP" haberleşmesinde bilgiyi alan tarafın ("server") bilgi kaçırabilmesi söz konusu olabileceğinden dolayı, böyle kaçırmalarda sistemde önemli bir aksamanın olmaması gerekir. Eğer bilgi kaçırma durumlarında sistemde önemli aksamalar oluşabiliyorsa "UDP" yerine "TCP" tercih edilmelidir. Örneğin, bir televizyon yayınında görüntüye ilişkin bir "frame" karşı tarafça alınmadığında önemli bir aksama söz konusu değildir. Belki görüntüde bir kasis olabilir ancak bu durum önemli kabul edilmemektedir. Benzer şekilde, birtakım makineler belli periyotlarda "server" a "ben çalışıyorum" demek için periyodik "UDP" paketleri yollayabilir. "Server" da hangi makinenin çalışmakta olduğunu, yani bozulmamış olduğunu, bu sayede anlayabilir. Diğer yandan, bir araba simülatörü arabanın durumunu "UDP" paketleriyle dış dünyaya verebilir. Son olarak, "TCP" ve "UDP" protokollerinde bir uzunluk bilgisi yoktur. Uzunluk bilgisi "IP" protokolünde bulunmaktadır. "IPv4" ve "IPv6" protokollerinde bir "IP" paketi en fazla "64K" uzunlukta olabilmektedir. Tabii "TCP" stream tabanlı olduğu için bu "64K" uzunluğun "TCP" için bir önemi yoktur. Ancak "UDP" paket tabanlı olduğu için bir, "UDP" paketi IP paketinin uzunluğunu aşamaz. Dolayısıyla bir "UDP" paketi en fazla "64K" uzunlukta olabilmektedir. Bu yüzden büyük paketlerin "UDP" ile gönderilmesi için programcının paketlere kendisinin manuel numaralar vermesi gerekebilir (zaten "TCP" protokolü bu şekilde bir numaralandırmayı kendi içerisinde yapmaktadır). "UDP" haberleşmesinin önemli bir farkı da "broadcasting" işlemidir. "broadcasting", yerel ağda belli bir "host" cihazın diğer tüm "host" cihazlara "UDP" paketleri gönderebilmesine denilmektedir. "TCP" de böyle bir "broadcasting" mekanizması yoktur. Aşağıda "UDP Header" yapısının gösterimi verilmiştir. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +----------------------------------------------+-----------------------------------------------+ ^ | Source Port | Destination Port | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ | 8 bytes | Header Length | Checksum | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ Görüleceği üzere "UDP Header" yapısı sekiz bayt uzunluğundadır. Şimdi de "UDP Server Program" ve "UDP Client Program" yazımlarını inceleyelim. >>>>> "UDP Server Program": Bu program tipik olarak aşağıdaki fonksiyonları sırayla çağırarak gerçekleştirilir. "socket (SOCK_DGRAM)" ---> "bind" ---> "recvfrom/sendto" ---> "close" >>>>> "UDP Client Program": Bu program tipik olarak aşağıdaki fonksiyonları sırayla çağırarak gerçekleştirilir. "socket (SOCK_DGRAM)" ---> "bind (isteğe bağlı)" ---> "gethostbyname/getaddrinfo (isteğe bağlı)" ---> "sendto/recvfrom" ---> "close" Bu fonksiyonlardan "socket", "bind" ve "close" fonksiyonlarını görmüştük. Dikkat etmemiz gereken nokta "socket" fonksiyonuna artık "SOCK_DGRAM" sembolik sabitini geçmemiz ve haberleşmenin sonlanması sırasında "shutdown" fonksiyonuna gerek kalmamasıdır. "recvfrom" ile "sendto" ise aşağıdaki gibidir. -> "recvfrom" : "UDP" paketlerini okumak için kullanılan recvfrom prototipi şöyledir: #include ssize_t recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len); Fonksiyonun birinci parametresi okuma işleminin yapılacağı soketi belirtir. İkinci parametre alınacak bilginin yerleştirileceği adresi belirtmektedir. Üçüncü parametre, ikinci parametredeki alanın uzunluğunu belirtir. Eğer buradaki değer "UDP" paketindeki gönderilmiş olan bayt sayısından daha az ise kırpılarak diziye yerleştirme yapılmaktadır. Fonksiyonun üçüncü parametresi birkaç bayrak değerine sahiptir ki bunlar "MSG_PEEK", "MSG_OOB" ve "MSG_WAITALL" isimli bayraklardır. Fakat bu parametre için "0" girilebilir. Fonksiyonun dördüncü parametresi "UDP" paketini gönderen tarafın "IP" adresinin ve "port" numarasının yerleştirileceği "sockaddr_in" yapısının adresini alır. Son parametre ise bu yapının uzunluğunu tutan "int" nesnenin adresini almaktadır. Fonksiyon başarı durumunda "UDP" paketindeki byte sayısına, başarısızlık durumunda "-1" değerine geri dönmektedir. Eğer okunacak paket yoksa ve karşı taraf da haberleşmeyi sonlandırmışsa, fonksiyon "0" ile geri dönmektedir. Diğer yandan "recvfrom" fonksiyonunun herhangi bir "client" tan gelen paketi alabildiğine dikkat ediniz. Dolayısıyla her "recvfrom" ile alınan paket farklı bir "client" a ilişkin olabilmektedir. Son olarak, "recvfrom" fonksiyonu, eğer soketi blokeli moddaysa ki varsayılan durum budur, "UDP" paketi gelene kadar blokeye yol açar. Blokesiz modda ise fonksiyon bekleme yapmaz ve "-1" değeriyle geri döner ve "errno" değişkeni "EAGAIN" değeriyle "set" edilir. -> "sendto" : Fonksiyonunun prototipi de şöyledir: #include ssize_t sendto(int socket, const void *message, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len); Fonksiyonun parametreleri "recvfrom" da olduğu gibidir. Yani birinci parametre gönderim yapılacak soketi belirtir. İkinci ve üçüncü parametreler gönderilecek bilgilerin bulunduğu tamponu ve onun uzunluğunu belirtmektedir. Yine bu fonksiyonda da bir "flags" parametresi vardır. Dördüncü parametre bilginin gönderileceği "IP" adresini ve "port" numarasını belirtir. Son parametre ise dördüncü parametredeki yapının ("sockaddr_in" ya da "sockaddr_in6") uzunluğunu alır. Fonksiyon blokeli modda, bilgi pakedi "network" tamponuna yazılana kadar, blokeye yol açmaktadır. "sendto" fonksiyonu da başarı durumunda "network" tamponuna yazılan bayt sayısına, başarısızlık durumunda "-1" e geri dönmektedir. Şimdi de bu anlatılanların gösterildiği bir adet "server-client" uygulama örneği verelim. * Örnek 1, Aşağıda tipik bir "UDP client-server" örneği verilmiştir. Bu örnekte "client", yine bir "prompt" a düşerek kullanıcıdan bir yazı istemektedir. Bu yazıyı "UDP" paketi biçiminde "server" a yollamaktadır. "Server" da bu yazıyı alıp görüntüledikten sonra yazıyı ters çevirip "client" a geri yollamaktadır. Programların komut satırı argümanları diğer örneklerde olduğu gibidir. /* server.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 char *revstr(char *str); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; int server_port; char buf[BUFFER_SIZE + 1]; char ntopbuf[INET_ADDRSTRLEN]; ssize_t result; int option; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); printf("waiting UDP packet...\n"); for (;;) { sin_len = sizeof(sin_client); if ((result = recvfrom(server_sock, buf, BUFFER_SIZE, 0, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("recvfrom"); buf[result] = '\0'; inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("%jd byte(s) received from %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_client.sin_port), buf); revstr(buf); if (sendto(server_sock, buf, result, 0, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("sendto"); } close(server_sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "localhost" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client, sin_server; socklen_t sin_len; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res; int gai_result; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; char buf[BUFFER_SIZE + 1]; /* BUFFER_SIZE is enough */ char ntopbuf[INET_ADDRSTRLEN]; ssize_t result; char *str; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } freeaddrinfo(res); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (sendto(client_sock, buf, strlen(buf), 0, res->ai_addr, sizeof(struct sockaddr_in)) == -1) exit_sys("send"); sin_len = sizeof(sin_server); if ((result = recvfrom(client_sock, buf, BUFFER_SIZE, 0, (struct sockaddr *)&sin_server, &sin_len)) == -1) exit_sys("recvfrom"); buf[result] = '\0'; inet_ntop(AF_INET, &sin_server.sin_addr, ntopbuf, INET_ADDRSTRLEN); printf("%jd byte(s) received from server %s:%u: \"%s\"\n", (intmax_t)result, ntopbuf, (unsigned)ntohs(sin_server.sin_port), buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } "UDP/IP" konusundaki şu noktalar da önemlidir: -> Anımsanacağı üzere "connect" ve "accept" çağrılarının "TCP" de kullanıldığından bahsetmiştik. Açıkçası bu fonksiyonları pekala "UDP" de de kullanabiliriz. Eğer "UDP" bir soket, "connect" ile "UDP" "server" a bağlanırsa ki "server" da bunu "accept" ile kabul etmelidir, bu durumda artık iki taraf da "recvfrom" ve "sendto" fonksiyonlarının yerine "recv" ve "send" fonksiyonlarını kullanabilir. Tabii burada yine datagram haberleşmesi yapılmaktadır. Yalnızca her defasında gönderme ve alma işlemlerinde karşı tarafın soketine ilişkin bilgilerin belirtilmesine gerek kalmamaktadır. Bu biçimdeki "connect" ve "accept" bağlantısında yine bir akış kontrolü uygulanmamaktadır. -> Aslında "UDP" de kullandığımız "recvfrom" ve "sendto" fonksiyonları yerine "recv" ve "send" fonksiyonlarını da kullanabilirdik. Bunun için "recvfrom" ve "sendto" fonksiyonlarının son iki parametresine "NULL" değerini geçmeliyiz. Şöyleki, result = recv(sock, buf, len, flags); result = recvfrom(sock, buf, len, flags, NULL, NULL); çağrısı ile result = send(sock, buf, len, flags); result = sendto(sock, buf, len, flags, any_value, any_value); çağrıları eş değerdir. >>>> "Server-Client" arasındaki haberleşmenin detayları: >>>>> "TCP/IP" ya da "UDP/IP" kullanılarak oluşturulan "server" uygulamalarında, "server" programın gelen istekleri yerine getirmesi, zaman kaybı oluşturabilmektedir. Çünkü bir "client" programdan gelen istekleri işlerken, diğer "client" programları bekletmek durumundadır. Bu bekletmeyi en aza indirmek için de çeşitli çözümler geliştirilmiştir. Örneğin, "client" programlardan gelen istekleri bir "thread" e yaptırtmak gibi. Bu durumda da "thread" oluşturma ve yok etmenin maliyeti karşımıza çıkmaktadır. Çünkü çözümün "scalable" olması da gerekmektedir. Bu problem için de işin başında belli sayıda "thread" in hayata getirilmesi fakat "suspend" edilmesi, yani "thread pool" mekanizmasının kullanılması, gerekmektedir. Fakat gerek POSIX gerek Standart C fonksiyonlarında böyle bir mekanizma standart olarak mevcut olmadığından, ya bizler geliştirmeliyiz ya da üçüncü kişiler tarafından geliştirilenleri kullanmalıyız. (Windows sistemlerinde böyle bir mekanizma mevcuttur.) İşte bu noktada sistemimizdeki çekirdek sayısı devreye girmektedir. Çünkü tavsiye edilen şudur ki sistemimizdeki çekirdek adedince "thread" oluşturup, bir takım "Processor Affinity" teknikleriyle, işleri bu "thread" lere eş zamanlı olarak yaptırılması taktirde gerçekten de verim elde etmiş olacağız. Aksi halde, sistemimizdeki çekirdek sayısından daha fazla "thread" kullanmamız durumunda, gözle görülür bir verim elde edemeyebiliriz. Buradaki kilit nokta sistemimizdeki her bir işlemcinin kendine has "Run Queue" ya sahip olması, bizlerin de oluşturduğumuz "thread" leri ayrı ayrı bunlara ataması, dolayısıyla bir "t" anında bütün çekirdeklerin bizim için çalışmasıdır. Tabii bunun gerçekleştirim yöntemleri birden fazla olabilir. Şöyleki; -> Sistemimizdeki çekirdek sayısınca "thread" oluşturulup, her bir "thread" ayrı ayrı aşağıdaki kodu çağırır: for (;;) { recvfrom(...) } -> "server" program aldıklarını bir kuyruğa yazar. Oluşturduğumuz "thread" ler ise bu kuyruktan alım yaparlar. Yani, for (;;) { recvfrom(...) } Bu ikisi arasındaki en temel fark birisinde gelenler önce kuyrukta biriktirilir, diğerinde ise direkt olarak ilgili "thread" tarafından işleme alınır. (Linux sistemlerindeki "epoll" modelinde, bu şekilde bir "thread" li kullanımda, Linux genel olarak her işlemci ya da çekirdek için gerektiğinde kendisi "thread" oluşturmaktadır. Dolayısıyla "epoll" modelinde yukarıdaki gibi bir organizasyon yapılmasa da daha iyi bir performans göstermektedir.) Pekiyi bu çözüm de yeterli gelmezse ne yapmamız gerekmektedir? İşte bu noktada devreye "Load Balancing" mekanizması ki işletmecisine de "Load Balancer" denir, girer. Bu mekanizmaya göre "server" makinenin bir klonu çıkartılır ve sistem içerisine dahil edilir. Anlık olarak "server" makinaların iş yükü kontrol edilir ve "client" programlardan gelen istekler, "server" programların yoğunluğuna göre, sistem içerisinde dağıtılır. İşte bu dağıtımı yapana "Load Balancer", sistemin kendisine ise "Load Balancing" denir. Fakat burada iki farklı yaklaşım söz konusudur. Bu sistemin kabaca çalışma mantığı ise şöyledir; -> "client" program aslında "Load Balancer" ile bağlantı sağlar. "Load Balancer" ise "client" programı, en az meşgul olan "server" a iletir. "Load Balancing" sistemi "hardware" temelli kurulabileceği gibi "software" temelli de kurulabilir. Yazılımsal gerçekleştirim, donanımsala göre, daha esnek olabilmektedir. Ancak donanımsal gerçekleştirimler yine bazı durumlarda daha etkin olabilmektedir. Donanmsal "Load Balancer" larda "client", "server" ile bağlantı kurmak istediğinde, "Load Balancer" devreye girip sanki yalnızca en az meşgul olan "server" sistemde varmış gibi bağlantıyı onun kabul etmesini sağlamaktadır. Yazılımsal "Load Balancer" larda "Load Balancer" ise bir "proxy" gibi çalışmaktadır. "client", "Load Balancer" ile bağlantı sağlar. "Load Balancer" ise bunu en az meşgul "server" a yönlendirir. Bu kez "client", bu server ile bağlantı kurar. Buradaki "Load Balancer" görevini yapan "proxy" programınının "server" yüklerini sürekli izlemesi gerekmektedir. Bunun için genellikle "UDP" protokolü kullanılmaktadır. Yani "UDP" ile "server" makineler sürekli bir biçimde kendi durumlarını "proxy" ye iletirler. "proxy" de bu bilgilerden hareketle en az meşgul "server" ı tespit eder. Tabii "server" makineler eğer devre dışı kalırsa "proxy" inin bunu fark etmesi ve artık ona yönlendirme yapmaması gerekir. Benzer biçimde yeni bir "server" makinesi sisteme eklendiğinde "proxy" hemen onu da sisteme otomatik olarak dahil etmelidir. >>>>> Şimdiye kadarki "Server-Client" program örneklerinde bizler metin mesajı gönderdik ve aldık. Halbuki gerçek hayattaki uygulamalarda veri alışverişi bu kadar basit değildir, daha karmaşıktır. "client" program, "server" programdan, çok çeşitli şeyleri yapmasını isteyebilir. Pekiyi bu istekler nasıl iletilir? Bu problem için iki farklı teknik kullanılır. Bunlar, "binary-based" ve "text-based" yaklaşımlardır. Bu yaklaşımlardan, >>>>>> "binary-based" : Bu yönteme göre mesajı gönderecek kişi ilk olarak mesajın tipini ve uzunluk bilgisini bir yapı nesnesi içerisine almak suretiyle gönderir. Daha sonra da gönderilecek mesajın kendisini karşı tarafa gönderir. Yani gönderen taraf kabaca şu şekilde bir süreç işletir; //... typedef struct tagMSG_HEADER { int len; int type; } MSG_HEADER; // Mesajın tipini ve uzunluk bilgisini göndereceğimiz o yapı nesnesi. typedef struct tagMSG_XXX { // message info } MSG_XXX; // Gönderilecek mesajın kendisi. typedef struct tagMSG_YYY { // message info } MSG_YYY; // Gönderilecek bir başka mesajın kendisi. //... MSG_HEADER header; header.len = sizeof(MSG_XXX); header.type = MSG_TYPE_XXX; send(sock, &header, sizeof(MSG_HEADER), 0); MSG_XXX msg_xxx; // Mesajın içeriği doldurulur. send(sock, &msg_xxx, sizeof(MSG_XXX), 0); MSG_YYY MSG_yyy; // Mesajın içeriği doldurulur. send(sock, &MSG_yyy, sizeof(MSG_YYY), 0); //... Buna karşın mesajı alan da kabaca aşağıdaki gibi bir süreç işletir; //... typedef struct tagMSG_HEADER { int len; int type; } MSG_HEADER; // Mesajın tipini ve uzunluk bilgisini göndereceğimiz o yapı nesnesi. //... MSG_HEADER header; recv(sock, &header, sizeof(MSG_HEADER), MSG_WAITALL); switch (header.type) { case MSG_TYPE_XXX: recv(sock, &msg_xxx, sizeof(header.len), MSG_WAITALL); process_msg_xxx(&msg_xxx); break; case MSG_TYPE_YYY: recv(sock, &msg_yyy, sizeof(header.len), MSG_WAITALL); process_msg_yyy(&msg_yyy); break; // ... } //... Burada aklınıza şöyle bir soru gelebilir: Okuyan taraf zaten mesajın kodunu (numarasını) elde edince o mesajın kaç bayt uzunlukta olduğunu bilmeyecek mi? Bu durumda mesajın uzunluğunun karşı tarafa iletilmesine ne gerek var? İşte mesajlar sabit uzunlukta olmayabilir. Örneğin mesajın içerisinde bir metin bulunabilir. Bu durumda mesajın gerçek uzunluğu bu metnin uzunluğuna bağlı olarak değişebilir. Genel bir çözüm için mesajın uzunluğunun da karşı tarafa iletilmesi gerekmektedir. >>>>>> "text-based" : Her ne kadar "binary-based" yöntemi daha hızlı ve etkin olma eğiliminde ise de pratikte daha çok "text-based" yöntemi kullanılmaktadır. Çünkü bu yöntemde mesajlaşmalar insanlar tarafından yazısal biçimde de oluşturulabilmektedir. Örneğin, "IP" protokol ailesinin uygulama katmanındaki "POP3", "Telnet", "FTP" gibi protokoller "text-based" mesajlaşmayı kullanmaktadır. Bu yöntem ise işleyiş kabaca şu şekildedir; -> "client" programdan "server" programa ve "server" programdan "client" programa gönderilen mesajlar bir yazı olarak gönderilir. Karşı taraf bu yazıyı alır, "parse" eder ve gereğini yapar. Programlama dillerinde yazılarla işlem yapabilen pek çok standart araç bulunduğu için bu biçimde mesajların işlenmesi genel olarak daha kolaydır. Ancak mesaj tabanlı haberleşme genel olarak daha yavaştır. Çünkü birtakım bilgilerin yazısal olarak ifade edilmesi "binary" ifade edilmesinden genel olarak daha fazla yer kaplama eğilimindedir. Diğer taraftan "text-based" yöntemdeki bir diğer handikap ise mesajın nerede bittiğinin tespit edilmesinin güç olmasıdır. Bu problem için de bir kaç çözüm geliştirilmiştir. Bu çözümlerden en çok kullanılanı, gönderilen mesajların sonuna bir takım özel karakterler eklenmesidir. Örneğin "IP" protokol ailesinin uygulama katmanındaki protokoller genel olarak mesajları "CR/LF ('\r' ve '\n' çiftiyle)" bitirmektedir. Tabii bu biçimdeki mesajlaşmalarda soketten "CR/LF" çifti görülene kadar okuma yapılması gerekir. Bu işlem soketten "byte-byte" okuma ile yapılmamalıdır. Çünkü her bayt okuması için prosesin "kernel" moda geçmesi zaman kaybı oluşturmaktadır. Belli bir karakter ya da karakter kümesi görülene kadar soketten okuma işleminin yapılması gerekmektedir. Bunun için şöyle bir yöntem izlenebilir; -> Karakterler soketten tek tek okunmaz. Blok blok okunurak bir tampona yerleştirilir. -> Sonra bu tampondan karakterler elde edilir. Tabii blok okuması yapıldığında birden fazla satır tamponda bulunabilecektir. -> Bu durumda okuma sırasında tamponda nerede kalındığının da tutulması gerekir. İşte bu gerçekleştirimi sağlayan klasik bir algoritma da "Effective TCP/IP Programming" kitabında verilmiştir. Takribi olarak aşağıdaki gibi bir algoritmadır: ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t)(str - bstr); // The result will be cast to 'ssize_t' from 'ptrdiff_t'. } Bu fonksiyonun birinci parametresi okuma yapılacak soketi, ikinci ve üçüncü parametreleri okunacak satırın yerleştirileceği dizinin adresini ve uzunluğunu almaktadır. Buradaki dizinin sonunda her zaman "CR/LF" ve "NULL" karakter bulunacaktır. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısı ile ("CR/LF" dahil) geri dönmektedir. Karşı taraf soketi kapatmışsa ve hiçbir okuma yapılamamışsa bu durumda fonksiyon "0" ile geri dönmektedir. Bu durumda programcının verdiği dizinin içeriği kullanılmamalıdır. Mesajın sonunda "CR/LF" çifti olmadıktan sonra fonksiyon başarılı okuma yapmamaktadır. Fonksiyon başarısızlık durumunda "-1" değerine geri döner ve "errno" uygun biçimde "set" edilir. Buradaki "sock_readline" fonksiyonu bir satır okunana kadar blokeye yol açmaktadır. Dolayısıyla çok "client" lı "server" uygulamalarında "select", "poll" ve "epoll" gibi modellerde bu fonksiyon bu haliyle kullanılamaz. Örneğin, biz "select" fonksiyonunda bir grup soketi bekliyor olalım. Bir sokete bir satırın yarısı gelmiş olabilir. Bu durumda biz "read_line" fonksiyonunu çağırırsak bloke oluşacaktır. Tabii gerçi satırın geri kalan kısmı zaten kısa bir süre sonra gelecek olsa da bu durum yine bir kusur oluşturacaktır. Aşağıda bu fonksiyonun kullanımına ilişkin bir örnek verilmiştir. * Örnek 1, Aşağıdaki örnekte "client" program, "server" programa "CR/LF" ile sonlandırılmış bir yazı göndermektedir. "server" program da bu yazıyı yukarıdaki "sock_readline" fonksiyonunu kullanarak okuyup ekrana "(stdout dosyasına)" yazdırmaktadır. /* server.c */ #include #include #include #include #include #include #include #include #include #define DEF_SERVER_PORT 55555 #define BUFFER_SIZE 4096 ssize_t sock_readline(int sock, char *str, size_t size); void exit_sys(const char *msg); /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; char buf[BUFFER_SIZE + 1]; char ntopbuf[INET_ADDRSTRLEN]; ssize_t result; int option; int server_port; int p_flag, err_flag; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("connected client ===> %s:%u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); for (;;) { if ((result = sock_readline(client_sock, buf, BUFFER_SIZE)) == -1) exit_sys("sock_readline"); if (result == 0) break; if (!strcmp(buf, "quit\r\n")) break; buf[strlen(buf) - 2] = '\0'; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SHUT_RDWR); close(client_sock); close(server_sock); return 0; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); /* ./client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[BUFFER_SIZE]; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; strcat(str, "\r\n"); if (send(client_sock, buf, strlen(buf), 0) == -1) exit_sys("send"); if (!strcmp(buf, "quit\r\n")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bir diğer yöntem ise, baştan yazının uzunluğunun iletilmesidir. Yani önce mesajın uzunluğu gönderilir. Daha sonra mesajın kendisi. Karşı taraf da önce yazının uzunluğunu alır, daha sonra o uzunluk kadar okuma yapar. Şimdi de "recv" / "recvfrom" ve "send" / "sendto" fonksiyonlarında kullandığımız sembolik sabitler olan "MSG_PEEK", "MSG_OOB", "MSG_WAITALL", "MSG_EOR", "MSG_NOSIGNAL" vb. bayrakların işlevleri nelerdir? Bu bayrakların işlevleri kabaca şunlardır: -> "MSG_PEEK" : Bu bayrak gelen bilginin tampondan okunacağını ancak tampondan atılmayacağını belirtmektedir. Yani biz "MSG_PEEK" bayrak değeri ile okuma yaparsak hem bilgiyi elde ederiz hem de sanki hiç okuma yapmamışız gibi bilgi "network" tamponunda kalır. Dolayısıyla bizim "thread" imiz ya da başka bir "thread" "recv" yaparsa tampondakini okuyacaktır. Bazen (çok seyrek olarak) mesajın ne olduğuna "MSG_PEEK" ile bakıp duruma göre onu kuyruktan almak isteyebiliriz. Eğer mesaj bizim beğenmediğimiz bir mesajsa onu almak istemeyebiliriz. Onun başka bir "thread" tarafından işlenmesini sağlayabiliriz. İşte bu amaç için bu bayrağı kullanabiliriz. -> "MSG_OOB" : "Out-of-band data (urgent data)" denilen okumalar için kullanılmaktadır. "Out-of-band data" konusu ayrı bir paragrafta açıklanacaktır. -> "MSG_WAITALL" : Bu bayrak "n-byte" okunmak istendiğinde bu "n-byte" ın hepsi okunana kadar bekleme sağlamaktadır. Fakat bu durumda bir sinyal geldiğinde yine "recv", "-1" ile geri döner ve "errno" değişkeni "EINTR" ile "set" edilir. Yine soket kapatıldığında ya da soket üzerinde bir hata oluştuğunda fonksiyon talep edilen kadar bilgiyi okuyamamış olabilir. Biz daha önce "n-byte" okuma yapmak için aşağıdaki gibi bir fonksiyon önermiştik: ssize_t read_socket(int sock, char *buf, size_t len) { size_t left, index; left = len; index = 0; while (left > 0) { if ((result = recv(sock, buf + index, left, 0)) == -1) return -1; if (result == 0) break; index += result; left -= result; } return (ssize_t) index; } İşte aslında "recv" fonksiyonundaki "MSG_WAITALL" bayrağı adeta bunu sağlamaktadır. Ancak yine de bu bayrağın bazı sistemlerde bazı problemleri vardır. Örneğin, "Windows" sistemlerinde ve "Linux" sistemlerinde bu bayrakla okunmak istenen miktar "network" alım tamponunun büyüklüğünden fazlaysa istenen miktarda bayt okunamayabilmektedir. Ancak bu bayrak yüksek olmayan miktarlarda okumalar için, yukarıdaki fonksiyonun yerine, kullanılabilmektedir. -> "MSG_EOR" : Soket türü "SOCK_SEQPACKET" ise kaydı sonlandırmakta kullanılır. -> "MSG_NOSIGNAL" : Normal olarak "send" ya da "write" işlemi yapılırken karşı taraf soketi kapatmışsa bu fonksiyonların çağrıldığı tarafta "SIGPIPE" sinyali oluşmaktadır. Ancak bu bayrak kullanılırsa böylesi durumlarda "SIGPIPE" sinyali oluşmaz, "send" ya da "write" fonksiyonu "-1" ile geri döner ve "errno" değişkeni "EPIPE" değeri ile "set" edilir. Yine Linux sistemlerine ögzü bazı bayraklar da mevcuttur. Örneğin "recv" ve "send" işleminde "MSG_DONTWAIT" bayrağı bir çağrımlık "non-blocking" etki yaratmaktadır. Yani "recv" sırasında "network" tamponunda hiç bilgi yoksa "recv" bloke olmaz, "-1" ile geri döner ve "errno" değişkeni "EAGAIN" değeri ile "set" edilir. "send" işlemi sırasında da "network" tamponu dolu ise "send" bloke olmaz "-1" ile geri döner ve "errno" yine "EAGAIN" değeri ile "set" edilir. Şimdi de bir takım "Client-Server" programların nasıl yazıldıklarına değinelim: >>>> Matematiksel işlem yapabilen bir "Client-Server" program yazmak isteyelim. Birden fazla "client" program, "server" programımıza bağlanabilsin ve her bir "client" için de "thread" kullanalım. Genel olarak bu tip programlarda "client" program bir mesaj gönderdiğinde "server" program kendisine geri dönüş yapar. Bu geri dönüş olumlu ise istediği şeyin sonucu, olumsuz ise hatanın nedenine ilişkindir. Yine bu tür programlarda "client" programın kullanacağı komutlar ile "server" programın karşılık vereceği komutlar da önceden belirlenmiş durumdadır. Diğer taraftan, "Application Layer" daki protokollerde gerçekleşen fiziksel bağlantı, aslında hizmet almak için yeterli değildir. "client" programların ayrıca "server" programla mantıksal bağlantı da kurması gerekmektedir. Yani "server" ile "client" programlar birbirlerine gönderdikleri komutların manalarını da bilmeleri gerekmektedir. Örneğin, "client" program "LOGIN" komutuyla birlikte kullanıcı adı ve şifre bilgisini "server" a iletir. "server" program da doğrulamanın sağlanması durumunda "LOGIN_ACCEPTED", sağlanamaması durumunda "ERROR LOGIN_FAILED" mesajını tekrar "client" programa iletir. Bu şekilde iki program arasında mantıksal bağlantı da kurulmuş olur. Aşağıda bu konuya ilişkin bir örnek de verilmiştir. * Örnek 1, Bu örnekte her "client" bağlantısında bir "thread" açılıp o "thread" yoluyla ilgili "client" ile konuşulmaktadır. Yani buradaki "server", "IO" modeli olarak, "thread" modelini kullanmaktadır. Bir "client", "server" a kullanıcı adı ve parola ile bağlanmaktadır. "server" program bir "CSV" dosyasına bakarak kullanıcı adı ve parola bilgisini doğrulamaktadır. Bir "client" bağlandığında "server" program "CLIENT_INFO" isimli bir yapı türünden bir nesne yaratıp "client" ın bilgilerini orada saklamaktadır. Aslında bu tür programlarda tüm "client" ların bilgileri bir dizi ya da bağlı liste içerisinde tutulmalıdır. Çünkü "server" tüm "client" lara belli bir mesajı göndermek isteyebilir. "server" programı aşağıdaki gibi derleyebilirsiniz: gcc -Wall -o calc-server calc-server.c -lm Prototipleri "math.h" içerisinde olan Standart C fonksiyonları "libc" kütüphanesinde değildir. "libm" isimli ayrı bir kütüphanededir. Maalesef "gcc" otomatik olarak bu kütüphaneyi link aşamasına dahil etmemektedir. Bu nedenle matematiksel fonksiyonları kullanırken "linker" için "-lm" seçeneğinin bulundurulması gerekmektedir. "client" program, yani "calc-client.c", "server" a fiziksel olarak "TCP" ile bağlandıktan sonra ona kullanıcı adı ve parolayı mesaj olarak gönderir. Sonra bir komut satırına düşer. Komutlar komut satırından verilmektedir. "client" programın "server" programa gönderebileceği komutlar, "LOGIN \r\n" "ADD op1 op2\r\n" "SUB op1 op2\r\n" "MUL op1 op2\r\n" "DIV op1 op2\r\n" "SQRT op1\r\n" "POW op1\r\n" "LOGOUT\r\n" şeklindedir. Diğer taraftan "server" programın "client" programa göndereceği mesajlar ise, "LOGIN_ACCEPTED\r\n" "LOGOUT_ACCEPTED\r\n" "RESULT result\r\n" "ERROR message\r\n" şeklinde olacaktır. /* calc-server.c */ #include #include #include #include #include #include #include #include #include #include #include #include #include #define CREDENTIALS_PATH "credentials.csv" #define DEF_SERVER_PORT 55555 #define MAX_MSG_SIZE 4096 #define MAX_MSG_PARAMS 32 #define MAX_USER_NAME 64 #define MAX_PASSWORD 64 #define MAX_CREDENTIALS 1024 typedef struct tagCREDENTIAL { char user_name[MAX_USER_NAME]; char password[MAX_PASSWORD]; } CREDENTIAL; typedef struct tagCLIENT_INFO { int sock; struct sockaddr_in sin; char buf[MAX_MSG_SIZE + 1]; // MAX_MSG_SIZE is enough CREDENTIAL credential; } CLIENT_INFO; typedef struct tagMSG { char *params[MAX_MSG_PARAMS]; int count; } MSG; typedef struct tagCLIENT_MSG_PROC { char *msg; bool (*proc)(CLIENT_INFO *, const MSG *); } CLIENT_MSG_PROC; int read_credentials(void); void *client_thread_proc(void *param); ssize_t sock_readline(int sock, char *str, size_t size); void receive_msg(CLIENT_INFO *ci); void send_msg(CLIENT_INFO *ci, const char *msg); int is_empty_line(const char *line); void exit_client_thread(CLIENT_INFO *ci); void parse_msg(char *msg, MSG *msgs); bool login_proc(CLIENT_INFO *ci); bool add_proc(CLIENT_INFO *ci, const MSG *msg); bool sub_proc(CLIENT_INFO *ci, const MSG *msg); bool mul_proc(CLIENT_INFO *ci, const MSG *msg); bool div_proc(CLIENT_INFO *ci, const MSG *msg); bool sqrt_proc(CLIENT_INFO *ci, const MSG *msg); bool pow_proc(CLIENT_INFO *ci, const MSG *msg); bool logout_proc(CLIENT_INFO *ci, const MSG *msg); char *revstr(char *str); void exit_sys(const char *msg); CLIENT_MSG_PROC g_client_msgs[] = { {"ADD", add_proc}, {"SUB", sub_proc}, {"MUL", mul_proc}, {"DIV", div_proc}, {"SQRT", sqrt_proc}, {"POW", pow_proc}, {"LOGOUT", logout_proc}, {NULL, NULL} }; CREDENTIAL g_credentials[MAX_CREDENTIALS]; int g_ncredentials; /* ./server [-p port] */ int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; int option; int server_port; int p_flag, err_flag; pthread_t tid; CLIENT_INFO *ci; int result; p_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "p:")) != -1) { switch (option) { case 'p': p_flag = 1; server_port = atoi(optarg); break; case '?': if (optopt == 'p') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (optind - argc != 0) { fprintf(stderr, "too many arguments!...\n"); exit(EXIT_FAILURE); } if (read_credentials() == -1) { fprintf(stderr, "cannot read credentials...\n"); exit(EXIT_FAILURE); } if (!p_flag) server_port = DEF_SERVER_PORT; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(server_port); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("listening port %d\n", server_port); for (;;) { sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); // printf("connected client ===> %s : %u\n", inet_ntop(AF_INET, &sin_client.sin_addr, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sin_client.sin_port)); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sin = sin_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } int read_credentials(void) { char buf[MAX_USER_NAME + MAX_PASSWORD + 32]; FILE *f; char *str; if ((f = fopen(CREDENTIALS_PATH, "r")) == NULL) return -1; g_ncredentials = 0; while (fgets(buf, MAX_USER_NAME + MAX_PASSWORD + 32, f) != NULL) { if (is_empty_line(buf) == 0) continue; if ((str = strtok(buf, ",")) == NULL) return -1; strcpy(g_credentials[g_ncredentials].user_name, str); if ((str = strtok(NULL, "\n")) == NULL) return -1; strcpy(g_credentials[g_ncredentials].password, str); if ((str = strtok(NULL, "\n")) != NULL) return -1; ++g_ncredentials; } fclose(f); return 0; } int is_empty_line(const char *line) { while (*line != '\0') { if (!isspace(*line)) return -1; ++line; } return 0; } void *client_thread_proc(void *param) { char ntopbuf[INET_ADDRSTRLEN]; unsigned port; CLIENT_INFO *ci = (CLIENT_INFO *)param; MSG msg; int i; inet_ntop(AF_INET, &ci->sin.sin_addr, ntopbuf, INET_ADDRSTRLEN); port = (unsigned)ntohs(ci->sin.sin_port); if (!login_proc(ci)) { send_msg(ci, "ERROR incorrect user name or password\r\n"); exit_client_thread(ci); } send_msg(ci, "LOGIN_ACCEPTED\r\n"); printf("client connected with user name \"%s\"\n", ci->credential.user_name); for (;;) { receive_msg(ci); *strchr(ci->buf, '\r') = '\0'; printf("Message from \"%s\": \"%s\"\n", ci->credential.user_name, ci->buf); parse_msg(ci->buf, &msg); if (msg.count == 0) { send_msg(ci, "ERROR empty command\r\n"); continue; } for (i = 0; g_client_msgs[i].msg != NULL; ++i) if (!strcmp(g_client_msgs[i].msg, msg.params[0])) { if (!g_client_msgs[i].proc(ci, &msg)) goto EXIT; break; } if (g_client_msgs[i].msg == NULL) { send_msg(ci, "ERROR invalid command\r\n"); } } printf("client disconnected %s : %u\n", ntopbuf, port); EXIT: shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void receive_msg(CLIENT_INFO *ci) { ssize_t result; if ((result = sock_readline(ci->sock, ci->buf, MAX_MSG_SIZE)) == -1) { fprintf(stderr, "sock_readline: %s\n", strerror(errno)); exit_client_thread(ci); } if (result == 0) { fprintf(stderr, "sock_readline: client unexpectedly down...\n"); exit_client_thread(ci); } } void send_msg(CLIENT_INFO *ci, const char *msg) { if (send(ci->sock, msg, strlen(msg), 0) == -1) exit_client_thread(ci); } void exit_client_thread(CLIENT_INFO *ci) { shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); pthread_exit(NULL); } void parse_msg(char *buf, MSG *msg) { char *str; msg->count = 0; for (str = strtok(buf, " \r\n\t"); str != NULL; str = strtok(NULL, " \r\n\t")) msg->params[msg->count++] = str; msg->params[msg->count] = NULL; } bool login_proc(CLIENT_INFO *ci) { MSG msg; char *user_name, *password; receive_msg(ci); parse_msg(ci->buf, &msg); user_name = msg.params[1]; password = msg.params[2]; if (msg.count != 3) return false; if (strcmp(msg.params[0], "LOGIN") != 0) return false; for (int i = 0; i < g_ncredentials; ++i) if (strcmp(user_name, g_credentials[i].user_name) == 0 && strcmp(password, g_credentials[i].password) == 0) { ci->credential = g_credentials[i]; return true; } return false; } bool add_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = op1 + op2; sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool sub_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = op1 - op2; sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool mul_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = op1 * op2; sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool div_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = op1 / op2; sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool sqrt_proc(CLIENT_INFO *ci, const MSG *msg) { double op, result; char buf[MAX_MSG_SIZE]; if (msg->count != 2) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op = atof(msg->params[1]); result = sqrt(op); sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool pow_proc(CLIENT_INFO *ci, const MSG *msg) { double op1, op2, result; char buf[MAX_MSG_SIZE]; if (msg->count != 3) { send_msg(ci, "ERROR invalid operand in command\r\n"); return true; } op1 = atof(msg->params[1]); op2 = atof(msg->params[2]); result = pow(op1, op2); sprintf(buf, "RESULT %f\r\n", result); send_msg(ci, buf); return true; } bool logout_proc(CLIENT_INFO *ci, const MSG *msg) { send_msg(ci, "LOGOUT_ACCEPTED\r\n"); printf("%s logging out...\n", ci->credential.user_name); return false; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* calc-client.c */ #include #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "127.0.0.1" #define DEF_SERVER_PORT "55555" #define MAX_MSG_SIZE 4096 #define MAX_MSG_PARAMS 32 typedef struct tagMSG { char *params[MAX_MSG_PARAMS]; int count; } MSG; typedef struct tagSERVER_MSG_PROC { char *msg; bool (*proc)(const MSG *); } SERVER_MSG_PROC; ssize_t sock_readline(int sock, char *str, size_t size); void receive_msg(int sock, char *msg); void send_msg(int sock, const char *msg); void parse_msg(char *buf, MSG *msg); int parse_error(char *buf, MSG *msg); int login_attempt(int sock, const char *user_name, const char *password); bool result_proc(const MSG *msg); bool error_proc(const MSG *msg); bool logout_accepted_proc(const MSG *msg); void exit_sys(const char *msg); SERVER_MSG_PROC g_server_msgs[] = { {"ERROR", error_proc}, {"RESULT", result_proc}, {"LOGOUT_ACCEPTED", logout_accepted_proc}, {NULL, NULL} }; /* ./calc-client [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char buf[MAX_MSG_SIZE]; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; const char *user_name, *password; MSG msg; int i; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } user_name = argv[optind + 0]; password = argv[optind + 1]; if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); if (login_attempt(client_sock, user_name, password) == -1) goto EXIT; printf("connection successful...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, MAX_MSG_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (buf[strspn(buf, " \t")] == '\0') /* check if buf contains white spaces */ continue; strcat(str, "\r\n"); send_msg(client_sock, buf); receive_msg(client_sock, buf); parse_msg(buf, &msg); for (i = 0; g_server_msgs[i].msg != NULL; ++i) if (!strcmp(g_server_msgs[i].msg, msg.params[0])) { if (!g_server_msgs[i].proc(&msg)) goto EXIT; break; } } EXIT: shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void receive_msg(int sock, char *msg) { ssize_t result; if ((result = sock_readline(sock, msg, MAX_MSG_SIZE)) == -1) exit_sys("receive_msg"); if (result == 0) { fprintf(stderr, "receive_msg: unexpectedly down...\n"); exit(EXIT_FAILURE); } } void send_msg(int sock, const char *msg) { if (send(sock, msg, strlen(msg), 0) == -1) exit_sys("send_msg"); } void parse_msg(char *buf, MSG *msg) { char *str; if (parse_error(buf, msg) == 0) return; msg->count = 0; for (str = strtok(buf, " \r\n\t"); str != NULL; str = strtok(NULL, " \r\n\t")) msg->params[msg->count++] = str; msg->params[msg->count] = NULL; } int parse_error(char *buf, MSG *msg) { while (isspace(*buf)) ++buf; if (!strncmp(buf, "ERROR", 5)) { buf += 5; while (isspace(*buf)) ++buf; *strchr(buf, '\r') = '\0'; msg->count = 2; msg->params[0] = "ERROR"; msg->params[1] = buf; return 0; } return -1; } int login_attempt(int sock, const char *user_name, const char *password) { char buf[MAX_MSG_SIZE]; MSG msg; sprintf(buf, "LOGIN %s %s\r\n", user_name, password); send_msg(sock, buf); receive_msg(sock, buf); parse_msg(buf, &msg); if (!strcmp(msg.params[0], "ERROR")) { fprintf(stderr, "login error: %s\n", msg.params[1]); return -1; } if (strcmp(msg.params[0], "LOGIN_ACCEPTED") != 0) { fprintf(stderr, "unexpected server message!...\n"); return -1; } return 0; } bool result_proc(const MSG *msg) { printf("%s\n", msg->params[1]); return true; } bool error_proc(const MSG *msg) { printf("Error: %s\n", msg->params[1]); return true; } bool logout_accepted_proc(const MSG *msg) { return false; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>> "chat" programı yazmak istediğimizde "client" programların "server" programa göndereceği komutlar, "LOGIN \r\n" "SEND_MESSAGE \r\n "LOGOUT\r\n" şeklinde olacaktır. Buna karşılık "server" programın da "client" programa göndereceği komutlar, "LOGIN_ACCEPTED\r\n" "ACTIVE_USERS\r\n" "NEW_USER_LOGGEDIN \r\n" "USER_LOGGEDOUT \r\n" "LOGOUT_ACCEPTED\r\n" "DISTRIBUTE_MESSAGE \r\n" "LOGOUT_ACCEPTED\r\n" "ERROR " şeklinde olacaktır. Bütün bunları göz önüne aldığımız zaman öyle bir programın akışı ise şu şekilde olacaktır; -> "client" önce "LOGIN" mesajı ile "server" a mantıksal bakımdan bağlanır. "server" da bağlantıyı kabul ederse "client" a "LOGIN_ACCEPTED" mesajını gönderir. Tabii oturuma yeni kullanıcı katıldığı için aynı zamanda "server" diğer tüm "client" lara "NEW_USER_LOGGEDIN" yollar. Bağlanan "client" a ise oturumdakilerin hepsinin listesini "ACTIVE_USERS" mesajı ile iletmektedir. -> "client" bir mesajın oturumdaki herkes tarafından görülmesini sağlamak amacıyla "server" a "SEND_MESSAGE" mesajını gönderir. "server" da bu mesajı oturumdaki tüm "client" lara "DISTRIBUTE_MESSAGE" mesajıyla iletir. -> Bir kullanıcı "logout" olmak istediğinde "server" a "LOGOUT" mesajını gönderir. "server" da bunu kabul ederse "client" a "LOGOUT_ACCEPTED" mesajını gönderir. Ancak "client" ın "logout" olduğu bilgisinin oturumdaki diğer "client" lara da iletilmesi gerekmektedir. Bunun için "server" tüm "client" lara "USER_LOGGED" mesajını göndermelidir. -> Yine bir hata durumunda server "client" lara "ERROR" mesajı gönderebilir. Aslında "chat" programları için "IP" protokol ailesinde "IRC (Internet Relay Chat)" protokolü bulunmaktadır. "IRC-server" ve "IRC-client" programları Linux sistemlerinde zaten bulunmaktadır. Siz de bu protokolün dokümanlarını inceleyerek bu protokol için "client" ve/veya "server" programları yazabilirsiniz. "IRC" protokolü "RFC 1459" olarak dokümante edilmiştir. Başka kurumların da "chat" protokollerinin bazıları artık dokümante edilmiştir. Microsoft'un "MSN Chat" protokülünün dokümanlarına aşağıdaki adresten erişebilirsiniz: http://www.hypothetic.org/docs/msn/ >>> Komut satırı tabanlı bir "telnet", "ssh" benzeri, "client-server" bir program yazmak isteyelim. Amacımız bir "client" ın server makinede komut satırından işlem yapmasını sağlamak olsun. Yani "client", "server" a bağlanacak ve ona "shell" komutları yollayacak. Komutların çıktısını da görecek. Böyle bir programın "server" tarafının çatısı şöyle oluşturulabilir: -> "client" program bağlandığında server iki boru yaratır ve "fork" işlemi yapar. -> "fork" işleminden sonra, henüz "exec" işlemi yapmadan, alt prosesin "stdin" betimleyicisini borunun birine, "stdout" betimleyicisini de diğerine yönlendirir. Böylece üst proses boruya yazma yaptığında aslında alt proses bunu "stdin" betimleyicisinden okuyacaktır. Benzer biçimde alt proses diğer boruya yazma yaptığında üst proses de bunu diğer borudan okuyabilecektir. -> Bu yönlendirmelerden sonra "server", "exec" yaparak "shell" programını çalıştırır. -> "client", "server" a "shell" komutunu gönderdiğinde "server", komutu "shell" programına işletir ve çıktısını elde eder ve "client" a yollar. Aslında bu işlemi yapan iki standart protokol vardır; "telnet" ve "ssh" protokolleri. "telnet" protokolü güvenlik bakımından zayıf olduğu için günümüzde daha çok "ssh" kullanılmaktadır. Aşağıda konuya ilişkin bir örnek verilmiştir. * Örnek 1, Our Bash Program (version 6) : Aşağıdaki programda üst prosesin "/bin/bash" programını çalıştırıp ona komutlar yollaması ve ondan komutlar alması beklenmektedir. Bunun için üst proses iki boru yaratmıştır. Sonra alt prosesi yaratarak "exec" uygulamıştır. Ancak üst proses henüz "exec" yapmadan alt prosesin "stdin" betimleyicisini ve "stdout" betimleyicisini boruya yönlendirmiştir. Böylece şöyle bir mekanizma oluşturulmuştur: Üst proses borulardan birine yazdığında sanki alt prosesin "stdin" dosyasına yazmış gibi olmaktadır. Yine üst proses diğer borudan okuma yaptığında alt prosesin "stdout" dosyasına yazılanları okumuş gibi olmaktadır. Fakat bu programın da şöyle kusurları vardır; Bu kusurlardan ilki üst proses kabuğa komutu ilettikten sonra onun "stdout" ya da "stdin" dosyasına yazdıklarını okumaya çalışmaktadır. Ancak ne kadar bilginin okunacağı belli değildir. İkinci kusur ise üst proses alt prosesin yazdıklarını okuyabilmek için biraz gecikme uygulamıştır. Ancak alt proses bu gecikmeden uzun süre çalışıyorsa program hatalı çalışır. #include #include #include #include #include #include #include #define BUFFER_SIZE 65536 void exit_sys(const char *msg); int main(void) { pid_t pid; int fdsout[2]; int fdsin[2]; char buf[BUFFER_SIZE + 1]; ssize_t result; int status; if (pipe(fdsin) == -1) exit_sys("pipe"); if (pipe(fdsout) == -1) exit_sys("pipe"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { /* child */ close(fdsin[0]); close(fdsout[1]); if (dup2(fdsin[1], 1) == -1) _exit(EXIT_FAILURE); if (dup2(fdsin[1], 2) == -1) _exit(EXIT_FAILURE); if (dup2(fdsout[0], 0) == -1) _exit(EXIT_FAILURE); close(fdsin[1]); close(fdsout[0]); if (execl("/bin/bash", "/bin/bash", (char *)NULL) == -1) _exit(EXIT_FAILURE); /* unreachable code */ } /* parent process */ close(fdsin[1]); close(fdsout[0]); /* parent writes fdsout[1] and read fdsin[0] */ if (fcntl(fdsin[0], F_SETFL, fcntl(fdsin[0], F_GETFL)|O_NONBLOCK) == -1) exit_sys("fcntl"); for (;;) { printf("Command: "); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if (write(fdsout[1], buf, strlen(buf)) == -1) exit_sys("write"); if (!strcmp(buf, "exit\n")) break; usleep(300000); if ((result = read(fdsin[0], buf, BUFFER_SIZE)) == -1) { if (errno == EAGAIN) continue; exit_sys("read"); } buf[result] = '\0'; printf("%s", buf); } if (wait(&status) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("Shell exits normally with exit code: %d\n", WEXITSTATUS(status)); else printf("shell exits abnormally!..\n"); close(fdsin[0]); close(fdsout[1]); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> Şimdi de elektronik posta alma ve göndermenin nasıl olduğuna değinelim; tipik olarak e-posta gönderme ve alma işlemleri aşağıdaki gibidir: -> Bunun için bir e-posta sunucu programının bulunuyor olması gerekir. Eğer tüm sistemi siz kuruyorsanız bu sunucuyu ("server") da sizin kurmanız gerekmektedir. Zaten Windows sistemlerinde ve UNIX/Linux sistemlerinde bu sunucular hazır biçimde bulunmaktadır. Tabii eğer "domain" hizmetini aldığınız bir kurum varsa, onlar da zaten e-posta hizmeti vermek için hazır e-posta sunucuları bulundurmaktadır. E-posta gönderebilmek için ya da e-posta alabilmek için bizim e-posta sunucusunun adresini biliyor olmamız gerekir. Gönderme işleminde kullanılacak sunucu ile alma işleminde kullanılacak sunucu farklı olabilmektedir. Örneğin CSD'nin e-posta sunucusuna "mail.csystem.org" adresiyle erişilebilmektedir. Bu sunucu hem gönderme hem de alma işlemini yapmaktadır. E-posta gönderebilmek için "client" program ile "server" program, "SMTP (Simple Mail Transfer Protocol)" denilen bir protokolle haberleşmektedir. O halde gönderim için bizim e-posta sunucusuna bağlanarak "SMTP" protokolü ile göndereceğimiz e-postayı ona iletmemiz gerekir. -> Biz göndereceğimiz e-postayı "SMTP" protokolü ile e-posta sunucumuza ilettikten sonra, bu sunucu hedef e-posta sunucusuna, bu e-postayı yine "SMTP" protokolü ile iletmektedir. E-postayı alan sunucu bunu bir posta kutusu "(mail box)" içerisinde saklar. -> Karşı taraftaki "client" program, "POP3" ya da "IMAP" protokolü ile, kendi e-posta sunucuna bağlanarak posta kutusundaki e-postayı yerel makineye indirir. Bir diğer deyişle akış aşağıdaki gibidir: client ---> kendi e-posta sunucumuz ---> hedef e-posta sunucusu --------> client SMTP SMTP POP3/IMAP Görüldüğü gibi "POP3" ve "IMAP" protokolleri e-posta sunucusunun posta kutusundaki zaten gelmiş ve saklanmış olan e-postaları yerel makineye indirmek için kullanılmaktadır. E-posta almak için yaygın kullanılan iki adet protokol vardır. Bunlar, "POP3 (Post Office Protocol Version 3)" ve "IMAP (Internet Message Access Protocol)" isimli protokollerdir. "IMAP" protokolü, "POP3" protokolünden daha güvenli ve ayrıntılı tasarlanmıştır. Bu iki protokol e-posta almak için kullanıldığından, aslında birer "client" protokolleridir. "client" programlar e-posta almak için "POP3" ve "IMAP" isimli "server" programlara bağlanırlar ve buraya gelmiş olanları kendi sistemlerine "get" ederler. Bu protokollerden, >>>>> "POP3 (Post Office Protocol Version 3)" : Detayları "RFC 1939" dokümanlarında açıklanmıştır. Fakat kabaca şöyledir: -> "client" program 110 numaralı (ya da 995 numaralı) porttan "server" a "TCP" ile fiziksel olarak bağlanır. -> Protokolde mesajlaşma tamamen "text-based" ve satırsal biçimde yapılmaktadır. Satırlar "CR/LF" karakterleriyle sonlandırılmaktadır. Protokolde "client" ın gönderdiği her komuta karşı "server" bir yanıt göndermektedir. (Fiziksel bağlantı sağlandığında da "server" bir onay mesajı gönderir.) Eğer yanıt olumluysa mesaj "+OK" ile, eğer yanıt olumsuzsa mesaj "-ERR" ile başlatılmaktadır. Yani "server" ın "client" a gönderdiği mesajın genel biçimi şöyledir: "+OK [diğer bilgiler] CR/LF" "-ERR [diğer bilgiler] CR/LF" -> Fiziksel bağlantıdan sonra "client" program mantıksal olarak "server" a "login" olmalıdır. "login" olmak için önce "user name" sonra da "password" gönderilmektedir. "User name" ve "password" gönderme işlemi aşağıdaki iki komutla yapılmaktadır. "USER CR/LF" "PASS CR/LF" Kullanıcı adı e-posta adresiyle aynıdır. Örneğin biz "test@csystem.org" için e-posta sunucusuna bağlanıyorsak buradaki kullanıcı ismi "test@csystem.org" olacaktır. Parola e-postalarınızı okumak için kullandığınız paroladır. Sisteme başarılı bir biçimde "login" olduğumuzu varsayıyoruz. Tipik olarak "server" bize şu mesajı iletecektir: +OK Logged in. Eğer "password" yanlış girilmişse yeniden önce "user name" ve sonra "password" gönderilmelidir. -> Client program "LIST" komutunu göndererek e-posta kutusundaki mesaj bilgilerini elde eder. "LIST" komutuna karşılık "server" önce aşağıdaki gibi bir satır gönderir: +OK 6 messages: Burada "server" e-posta kutusunda kaç e-posta olduğunu belirtmektedir. Sonra her e-postaya bir numara vererek onların "byte" uzunluklarını satır satır iletir. Komut yalnızca "." içeren bir satırla son bulmaktadır. Örneğin: +OK 6 messages: 1 1565 2 5912 3 11890 4 4920 5 9714 6 4932 . -> Belli bir e-posta "RETR" komutuyla elde edilmektedir. Bu komuta elde edilecek e-postanın index numarası girilir. Örneğin: "RETR 2 CR/LF" "RETR" komutuna karşı server önce aşağıdaki gibi bir satır gönderir: +OK 5912 octets Burada programcı bu satırı "parse" ederek burada belirtilen miktarda "byte" kadar soketten okuma yapmalıdır. Anımsanacağı gibi porttan tam olarak "n-byte" okumak "TCP" de tek bir "recv" ile yapılamamaktadır. -> Mesajı silmek için "DELE" komutu kullanılır. Komuta parametre olarak silinecek mesajın indeks numarası girilmektedir. Örneğin: "DELE 3 CR/LF" Bu komut uygulandığında "server" henüz e-postayı posta kutusundan silmez. Yalnızca onu "silinecek" biçiminde işaretler. Silme işlemi "QUIT" komutuyla oturum sonlandırıldığında yapılmaktadır. Eğer "client" silme eyleminden pişmanlık duyarsa "RSET" komutuyla ilk duruma gelir. "RSET" komutu "logout" yapmaz. Yalnızca silinmiş olarak işaretlenenlerin işaretlerini kaldırır. -> "STAT" komutu o anda e-posta kutusundaki e-posta sayısını bize vermektedir. Bu komut gönderildiğinde aşağıdaki gibi bir yanıt alınacaktır: +OK 5 27043 Burada "server" e-posta kutusunda toplam 5 adet e-postanın bulunduğunu ve bunların "byte" uzunluklarının da 27043 olduğunu söylemektedir. -> Protokol, "client" programın "QUIT" komutunu göndermesiyle sonlandırılmaktadır. Örneğin: "QUIT CR/LF" -> "POP3" protokolününde "client" belli bir süre server'a hiç mesaj göndermezse, "server" "client" ın soketini kapatıp bağlantıyı koparmaktadır. Her ne kadar "RFC 1939" da "server" ın en azından 10 dakika beklemesi gerektiği söylenmişse de "server" ların çoğu çok daha az bir süre beklemektedir. Diğer yandan "POP3" protokolünde "client" programın gönderdiği yazısal komutlar için "server" programın gönderdiği yanıtlar "parse" edilerek tam gerektiği kadar okuma yapılabilir. * Örnek 1, Aşağıdaki programda biz, basitlik sağlamak amacıyla, "server" dan gelen mesajları başka bir "thread" ile ele aldık. /* pop3.c */ #include #include #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "mail.csystem.org" #define DEF_SERVER_PORT "110" #define BUFFER_SIZE 4096 ssize_t sock_readline(int sock, char *str, size_t size); void *thread_proc(void *param); void send_msg(int sock, const char *msg); void exit_sys_thread(const char *msg, int err); void exit_sys(const char *msg); /* ./pop3 [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; char buf[BUFFER_SIZE + 1]; pthread_t tid; int result; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 0) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(res); if ((result = pthread_create(&tid, NULL, thread_proc, (void *)client_sock)) != 0) exit_sys_thread("pthread_create", result); usleep(500000); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (buf[strspn(buf, " \t")] == '\0') /* check if buf contains white spaces */ continue; strcat(str, "\r\n"); send_msg(client_sock, buf); sleep(1); if (!strcmp(buf, "QUIT\r\n")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); if ((result = pthread_join(tid, NULL)) != 0) exit_sys_thread("pthread_join", result); return 0; } void *thread_proc(void *param) { int sock = (int)param; char buf[BUFFER_SIZE]; int result; for (;;) { if ((result = sock_readline(sock, buf, BUFFER_SIZE)) == -1) exit_sys("receive_msg"); if (result == 0) { printf("closing connection...\n"); break; } printf("%s", buf); } exit(EXIT_SUCCESS); return NULL; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void send_msg(int sock, const char *msg) { if (send(sock, msg, strlen(msg), 0) == -1) exit_sys("send_msg"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } void exit_sys_thread(const char *msg, int err) { fprintf(stderr, "%s: %s\n", msg, strerror(err)); exit(EXIT_FAILURE); } * Örnek 2, Aslında "POP3" protokolünde "client" programın her mesajına karşılık "server" ın nasıl bir yazı gönderdiği bilindiğine göre, buradan hareketle hiç "thread" oluşturmadan gönderilen komut için, yanıt elde edilebilir. Aşağıda bu fikre bir örnek verilmiştir. Örneğimizde "LIST" ve "RETR" komutları özel olarak ele alınmıştır. "LIST" komutunda "server" ın listeyi ilettikten sonra son satırda "." gönderdiğini anımsayınız. Biz de aşağıda programda satırda "." görene kadar okuma yaptık. "RETR" komutunda "+OK" yazısından sonra mesajdaki "byte" sayısının da gönderildiğini anımsayınız. Biz de bundan faydalanarak soketten o kadar "byte" okuduk. /* pop3.c */ #include #include #include #include #include #include #include #include #include #include #include #define DEF_SERVER_NAME "mail.csystem.org" #define DEF_SERVER_PORT "110" #define BUFFER_SIZE 4096 ssize_t sock_readline(int sock, char *str, size_t size); void send_msg(int sock, const char *msg); void receive_msg(int sock, char *msg); void getcmd(const char *buf, char *cmd); void proc_list(int sock); void proc_retr(int sock); void exit_sys(const char *msg); /* ./pop3 [-s server] [-p server_port] [-b client_port] */ int main(int argc, char *argv[]) { int client_sock; struct sockaddr_in sin_client; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; struct addrinfo *res, *ri; int gai_result; char *str; int option; int s_flag, p_flag, b_flag, err_flag; const char *server_name; int bind_port; const char *server_port; char buf[BUFFER_SIZE + 1]; char cmd[BUFFER_SIZE]; s_flag = p_flag = b_flag = err_flag = 0; opterr = 0; while ((option = getopt(argc, argv, "s:p:b:")) != -1) { switch (option) { case 's': s_flag = 1; server_name = optarg; break; case 'p': p_flag = 1; server_port = optarg; break; case 'b': b_flag = 1; bind_port = atoi(optarg); break; case '?': if (optopt == 's' || optopt == 'p' || optopt == 'b') fprintf(stderr, "-%c option must have an argument!\n", optopt); else fprintf(stderr, "-%c invalid option!\n", optopt); err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 0) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (!s_flag) server_name = DEF_SERVER_NAME; if (!p_flag) server_port = DEF_SERVER_PORT; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (b_flag) { sin_client.sin_family = AF_INET; sin_client.sin_port = htons(bind_port); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } if ((gai_result = getaddrinfo(server_name, server_port, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_result)); exit(EXIT_FAILURE); } for (ri = res; ri != NULL; ri = ri->ai_next) if (connect(client_sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); receive_msg(client_sock, buf); printf("%s", buf); freeaddrinfo(res); usleep(500000); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (buf[strspn(buf, " \t")] == '\0') /* check if buf contains white spaces */ continue; strcat(str, "\r\n"); send_msg(client_sock, buf); getcmd(buf, cmd); if (!strcmp(cmd, "LIST")) proc_list(client_sock); else if (!strcmp(cmd, "RETR")) proc_retr(client_sock); else { receive_msg(client_sock, buf); printf("%s", buf); } if (!strcmp(cmd, "QUIT")) break; } shutdown(client_sock, SHUT_RDWR); close(client_sock); return 0; } ssize_t sock_readline(int sock, char *str, size_t size) { char *bstr = str; static char *bp; static ssize_t count = 0; static char buf[2048]; if (size <= 2) { errno = EINVAL; return -1; } while (--size > 0) { if (--count <= 0) { if ((count = recv(sock, buf, sizeof(buf), 0)) == -1) return -1; if (count == 0) return 0; bp = buf; } *str++ = *bp++; if (str[-1] == '\n') if (str - bstr > 1 && str[-2] == '\r') { *str = '\0'; break; } } return (ssize_t) (str - bstr); } void send_msg(int sock, const char *msg) { if (send(sock, msg, strlen(msg), 0) == -1) exit_sys("send_msg"); } void receive_msg(int sock, char *msg) { ssize_t result; if ((result = sock_readline(sock, msg, BUFFER_SIZE)) == -1) exit_sys("receive_msg"); if (result == 0) { fprintf(stderr, "receive_msg: unexpectedly down...\n"); exit(EXIT_FAILURE); } } void getcmd(const char *buf, char *cmd) { int i; for (i = 0; buf[i] != '\0' && !isspace(buf[i]); ++i) cmd[i] = buf[i]; cmd[i] = '\0'; } void proc_retr(int sock) { ssize_t result; char bufrecv[BUFFER_SIZE + 1]; ssize_t n; int i, ch; for (i = 0;; ++i) { if ((result = recv(sock, &ch, 1, 0)) == -1) exit_sys("sock_readline"); if (result == 0) return; if ((bufrecv[i] = ch) == '\n') break; } bufrecv[i] = '\0'; printf("%s\n", bufrecv); n = (ssize_t)strtol(bufrecv + 3, NULL, 10); while (n > 0) { if ((result = recv(sock, bufrecv, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; bufrecv[result] = '\0'; printf("%s", bufrecv); fflush(stdout); n -= result; } } void proc_list(int sock) { ssize_t result; char bufrecv[BUFFER_SIZE]; do { if ((result = sock_readline(sock, bufrecv, BUFFER_SIZE)) == -1) exit_sys("sock_readline"); if (result == 0) break; printf("%s", bufrecv); } while (*bufrecv != '.'); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi biz bir e-postaya bir resim ya da dosya iliştirirsek ne olacaktır? "POP3" protokolü çok eski bir protokoldür. Internet'in uygulama katmanındaki ilk protokollerden biridir. Bu protokolde her şey yazı gibi gönderilip alınmaktadır. Dolayısıyla bir kullanıcı e-postasına bir resim ya da dosya iliştirdiğinde, onun içeriği yazıya dönüştürülerek sanki bir yazıymış gibi, gönderilmektedir. Pekiyi e-postanın bu gibi farklı içerikleri "client" tarafından nasıl ayrıştırılacaktır? İşte bir yazı içerisinde değişik içerikler "MIME" denilen sistemle başlıklandırılmaktadır. E-postaları içeriklerine ayrıştırabilmek için ilgili içeriklerin nasıl yazıya dönüştürüldüğünü ve nasıl geri dönüşüm yapıldığını bilmeniz gerekmektedir. Bunun için "Base-64" denilen yöntem kullanılmaktadır. Şimdi de oluşturduğumuz soket nesnelerinin özelliklerini nasıl "set" ve "get" edebileceğimize bakalım. Bunun için sırasıyla iki fonksiyon bulundurulmuştur. Bunlar "setsockopt" ve "getsockopt" isimli fonksiyonlardır. Fonksiyonlardan, >>>> "setsockopt" : Soket özelliğini "set" etmek için kullanılır. Fonksiyonun prototipi aşağıdaki gibidir. #include int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len); Fonksiyonun birinci parametresi özelliği değiştirilecek soketi belirtir. İkinci parametresi değişimin hangi düzeyde yapılacağını belirten bir sembolik sabit biçiminde girilir. Soket düzeyi için tipik olarak "SOL_SOCKET" girilmelidir. Üçüncü parametre hangi özelliğin değiştirileceğini belirtmektedir. Dördüncü parametre değiştirilecek özelliğin değerinin bulunduğu nesnenin adresini almaktadır. Son parametre, dördüncü parametredeki nesnenin uzunluğunu belirtmektedir. Fonksiyon başarı durumunda "0", başarısızlık durumunda "-1" değerine geri döner. >>>> "getsockopt" : Soket özelliğini "get" etmek için kullanılır. Fonksiyonun prototipi aşağıdaki gibidir. #include int getsockopt(int socket, int level, int option_name, void * option_value, socklen_t * option_len); Parametreler "setsockopt" ta olduğu gibidir. Yalnızca dördüncü parametrenin yönü değişiktir ve beşinci parametre gösterici almaktadır. Fonksiyonların üçüncü parametresine geçtiğimiz sembolik sabitler ise şunlardır; "SO_BROADCAST", "SO_OOBLINE", "SO_SNDBUF", "SO_RECVBUF", "SO_REUSEADDR", ... Bu sabitlerden, -> "SO_REUSEADDR" : Bu seçeneği belli bir "port" için "bind" işlemi yapmış bir "server" ın, "client" bağlantısı sağladıktan sonra, sonlanması sonucunda bu "server" ın yeniden çalıştırılıp aynı "port" u "bind" edebilmesi için kullanılmaktadır. Bir "port" u "bind" eden server, bir "client" ile bağlandıktan sonra çökerse ya da herhangi bir biçimde sonlanırsa işletim sistemleri o "port" un yeniden belli bir süre "bind" edilmesini engellemektedir. Bunun nedeni eski çalışan "server" ile yeni çalışacak olan "server" ın göndereceği ve alacağı paketlerin karışabilme olasılığıdır. Eski bağlantıda yollanmış olan paketlerin ağda maksimum bir geçerlilik süresi vardır ki işletim sistemi de bunun iki katı kadar bir süre (2 dakika civarı; neden iki katı olduğu protokolün aşağı seviyeli çalışması ile ilgilidir) bu portun yeniden bind edilmesini engellemektedir. İşte eğer "SO_REUSEADDR" soket seçeneği kullanılırsa, artık sonlanan ya da çöken bir "server" hemen yeniden çalıştırıldığında, "bind" işlemi sırasında "Address already in use" biçiminde bir hata ile karşılaşılmayacaktır. Bu soket seçeneğini, int sockopt = 1; ... if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt)) == -1) exit_sys("setsockopt"); şeklinde "setsockopt" fonksiyonu ile "set" edebiliriz. Buradan da görüleceği üzere bu bayrağı kullanırken "int" bir nesne alıp onun içerisine sıfır dışı bir değer yerleştirip, onun adresini "setsockopt" fonksiyonunun dördüncü parametresine girmek gerekir. Bu nesneye "0" girip fonksiyonu çağırırsak bu özelliği kapatmış oluruz. Diğer yandan "SO_REUSEADDR" bayrağı daha önce bir program tarafından "bind" edilmiş soketin, ikinci kez başka bir program tarafından "bind" edilmesi için kullanılmamaktadır. Eğer böyle bir ihtiyaç varsa (nadiren olabilir) Linux'ta (fakat POSIX'te değil) "SO_REUSEPORT" soket seçeneği kullanılmalıdır. Aşağıda bu bayrağın kullanımına ilişkin bir örnek verilmiştir. * Örnek 1, Aşağıdaki "server" programını "client" ile bağlandıktan sonra "Ctrl+C" ile sonlandırınız. Sonra yeniden çalıştırmaya çalışınız. "SO_REUSEADDR" seçeneği kullanıldığından dolayı bir sorun ile karşılaşılmayacaktır. Daha sonra "server" programdan o kısmı silerek yeniden denemeyi yapınız. Örneğimizdeki "client" program "server" adresini ve "port" numarasını, "server" program ise yalnızca "port" numarasını komut satırı argümanı olarak almaktadır. Programları farklı terminallerden, ./server 55555 ./client localhost 555555 şeklinde çalıştırabilirsiniz. /* server.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock, sock_client; struct sockaddr_in sinaddr, sinaddr_client; socklen_t sinaddr_len; char ntopbuf[INET_ADDRSTRLEN]; in_port_t port; ssize_t result; char buf[BUFFER_SIZE + 1]; int sockopt = 1; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } port = (in_port_t)strtoul(argv[1], NULL, 10); if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt)) == -1) exit_sys("setsockopt"); sinaddr.sin_family = AF_INET; sinaddr.sin_port = htons(port); sinaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock, (struct sockaddr *)&sinaddr, sizeof(sinaddr)) == -1) exit_sys("bind"); if (listen(sock, 8) == -1) exit_sys("listen"); printf("Waiting for connection...\n"); sinaddr_len = sizeof(sinaddr_client); if ((sock_client = accept(sock, (struct sockaddr *)&sinaddr_client, &sinaddr_len)) == -1) exit_sys("accept"); printf("Connected: %s : %u\n", inet_ntop(AF_INET, &sinaddr_client, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sinaddr_client.sin_port)); for (;;) { if ((result = recv(sock_client, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%ld bytes received from %s (%u): %s\n", (long)result, ntopbuf, (unsigned)ntohs(sinaddr_client.sin_port), buf); } shutdown(sock_client, SHUT_RDWR); close(sock_client); close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct addrinfo *ai, *ri; struct addrinfo hints = {0}; char buf[BUFFER_SIZE]; char *str; int result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; if ((result = getaddrinfo(argv[1], argv[2], &hints, &ai)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result)); exit(EXIT_FAILURE); } for (ri = ai; ri != NULL; ri = ri->ai_next) if (connect(sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(ai); printf("Connected...\n"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((send(sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); } shutdown(sock, SHUT_RDWR); close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } -> "SO_REUSEPORT" : Bu soket seçeneği benzer biçimde Windows sistemlerinde "SO_EXCLUSIVEADDRUSE" biçimindedir. Yani bu soket seçenekleri kullanıldığında aynı "port" birden fazla "server" tarafından "bind" edilip aynı anda kullanılabilir. Bu bayraklarla birden fazla proses aynı "port" u "bind" ettiğinde bir "client" tan bu "port" a "connect" işlemi yapıldığı zaman işletim sistemi belli bir "load balancing" yaparak bağlantının "server" lardan biri tarafından kabul edilmesini sağlayacaktır. Şimdi de "TCP/IP" ve "UDP/IP" protokollerinin alt seviyelerini incelemeye başlayalım. Ancak bu protokollerin aşağı seviyeli çalışma biçimleri biraz karmaşıktır. Biz burada çok derine inmeden bu çalışma biçimini açıklayacağız. >>> "TCP/IP" Protokolünün Alt Seviye İşlemleri: "TCP" protokolü 1981 yılında "RFC 793" dokümanı ile tanımlanmıştır (https://tools.ietf.org/html/rfc793). Sonradan protokole bazı revizyonlar ve eklemeler de yapılmıştır. Protokolün son güncel versiyonu 2022'de "RFC 9293" dokümanında tanımlanmıştır. Paket tabanlı protokollerin hepsinde gönderilip alınan veriler paket biçimindedir (yani bir grup bayt biçimindedir). Bu paketlerin "başlık (header)" ve "veri (data)" kısımları vardır. Örneğin "Ethernet paketinin başlık" ve "veri kısmı", "IP paketinin başlık" ve "veri kısmı", "TCP paketinin başlık" ve "veri kısmı" vb. Öte yandan "TCP" protokolü aslında "IP" protokolünün üzerine oturtulmuştur. Yani aslında "TCP" paketleri "IP" paketleri gibi gönderilip alınmaktadır. Nihayet aslında bu paketler bilgisayarımıza "Ethernet" ya da "Wireless" paketi olarak gelmektedir. Paketlerin başlık kısımlarında önemli "meta data" bilgileri bulunmaktadır. O halde, örneğin aslında bizim "network" kartımıza bilgiler, "Ethernet" paketi gibi gelmektedir. Aslında "IP" paketi "Ethernet" paketinin veri kısmında, "TCP" paketi de aslında "IP" paketinin veri kısmında konuşlandırılmaktadır. Yani aslında bize gelen "Ethernet" paketinin veri kısmında "IP" paketi, "IP" paketinin veri kısmında da "TCP" paketi bulunmaktadır. "TCP" de gönderdiğimiz veriler aslında "IP" paketinin veri kısmını oluşturmaktadır. * Örnek 1, bir "host" tan diğerine bir "TCP" paketinin gönderildiğini düşünelim. "TCP" paketi "TCP Header" ve "TCP Data" kısmından oluşmaktadır. Şöyleki: +-------------------------+ | TCP Header | +-------------------------+ | TCP Data | +-------------------------+ Ancak "TCP" paketi aslında "IP" paketi gibi gönderilmektedir. "IP" paketi de "IP Header" ve "IP Data" kısımlarından oluşmaktadır. İşte aslında "TCP" paketi "IP" paketinin "Data" kısmında bulundurulur. Yani yolculuk eden "TCP" paketinin görünümü şöyledir: +-------------------------+ | IP Header | +-------------------------+ <---+ | TCP Header | | +-------------------------+ IP Data | TCP Data | | +-------------------------+ <---+ "TCP" paketi de bilgisayarımızın "Ethernet" kartına sanki "Ethernat" paketi gibi gelmektedir. "Ethernet" paketi de "Ethernet Header" ve "Ethernet Data" kısımların oluşmaktadır. İşte bütün "TCP" paketi aslında "IP" paketi gibi, "IP" paketi de Ethernet paketi gibi, gönderilip alınmaktadır. Şöyleki: +-------------------------+ | Ethernet Header | +-------------------------+ <----------------+ | IP Header | | +-------------------------+ <---+ | | TCP Header | | Ethernet Data +-------------------------+ IP Data | | TCP Data | | | +-------------------------+ <---+------------+ Bu durumu aşağıdaki gibi de gösterebiliriz: +-------------------------+ | Ethernet Header | +----+--------------------+----+ | IP Header | +----+--------------------+----+ | TCP Header | +----+--------------------+----+ | TCP Data | +-------------------------+ * Örnek 2, Biz "TCP" de "send" fonksiyonuyla "ankara" yazısını gönderiyor olalım. Bu "ankara" yazısını oluşturan baytlar aslında "TCP" paketinin veri kısmındadır. Şöyleki: +-------------------------+ | Ethernet Header | +----+--------------------+----+ | IP Header | +----+--------------------+----+ | TCP Header | +----+--------------------+----+ | "ankara" | +-------------------------+ >>>> "Ethernet" protokolu ise, "(IEEE 802.3)", "OSI" katmanına göre fiziksel ve veri bağlantı katmanının işlevlerini yerine getirmektedir. >>>> "Wireless" haberleşme için kullanılan "Wi-Fi" protokolü, "(IEEE 802.11)", "Ethernet" protokolünün telsiz (wireless) biçimi gibi düşünülebilir. Tabii "IP" paketleri aslında yalnızca bilgisayarımıza gelirken "Ethernet" paketi ya da "Wi-Fi" paketi olarak gelir. Dışarıda rotalanırken "Ethernet" paketi söz konusu değildir. Diğer yandan "IP" protokolünün "IPv4" ve "IPv6" biçiminde iki versiyonunun olduğunu da anımsayınız. Ancak "TCP" ve "UDP" protokollerinin böyle bir versiyon numarası yoktur. "TCP" paketi "IPv4" paketinin veri kısmında da "IPv6" paketinin veri kısmında da konuşlandırılmış olabilir. Şimdi de "IPv4" ve "IPv6" protokollerinin başlık kısımlarını irdeleyelim. >>>> "IPv4" protokolünün başlık kısmı şöyledir: Her satırda 4 bayt bulunmaktadır. Toplam başlık uzunluğu 20 bayttır. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +-----------+-----------+----------------------+-----------------------------------------------+ ^ | Version | IHL | Type of Service | Total Length | (4 bytes) | | (4 bits) | (4 bits) | (8 bits) | (16 bits) | | +-----------+-----------+----------------------+-----------+-----------------------------------+ | | Identification | Flags | Fragment Offset | (4 bytes) | | (16 bits) | (3 bits) | (13 bits) | | +-----------------------+----------------------+-----------+-----------------------------------+ | | Time to Live (TTL) | Protocol | Header Checksum | (4 bytes) | 20 bytes | (8 bits) | (8 bits) | (16 bits) | | +-----------------------+----------------------+-----------------------------------------------+ | | Source IP Address (32 bits) | (4 bytes) | +----------------------------------------------------------------------------------------------+ | | Destination IP Address (32 bits) | (4 bytes) | +----------------------------------------------------------------------------------------------+ v | Segment (L4 protocol (TCP/UDP) + Data) | +----------------------------------------------------------------------------------------------+ Bu başlıklardan, -> "Version" : "IP" versiyonunu içerir. "IPv4" ya da "IPv6" değerlerinden birini içermektedir. "IPv4" için "4" değeri kullanılmaktadır. -> "IHL" : "Internet Header Length" bilgisini içermektedir. Genelde 20 bayt değerini içerir. Fakat farklı değerler aldığı durumlar da söz konusu olabilmektedir. -> "Type of Service" : "DS" / "DSCP" / "ECN" alanlarını içermektedir. Paket önceliği konusunda kullanılmaktadır. -> "Total Length" : "Header" ve "data" nın toplam uzunluk bilgisini içermektedir. -> "Identification", "Flags", "Frament Offset" : Paketin ikinci 4 baytlık kısmı "fragmentation" için kullanılmaktadır. -> "Time to Live (TTL)" : Paketin yaşam ömrünün bilgisini içermektedir. Yaşam ömrü her "router" geçildiğinde bir azalmaktadır. Eğer "TTL" değeri "0" olursa paket "router" tarafından çöpe atılmaktadır. "TTL" genel olarak yolunu şaşırmış paketlerin "network" lerde sonsuza kadar dolaşmasını önlemek için kullanılmaktadır. Bazen de paket "router" lar arasında bir "loop (routing loop)" içerisinde takılıp kalmaktadır. "TTL", bu gibi durumları engellemek için kullanılmaktadır. Dünya'da en uzak noktaya bile data gönderirken maksimum 15-20 router geçilmektedir. -> "Protocol" : "L4" protokol bilgisini içermektedir. Örneğin, "TCP" için "6", "UDP" için "17", "ICMP" için "1" değerlerini içermektedir. -> "Header Checksum" : "Router" lar "IP" paket "header" ının yolda bozulup bozulmadığını, bu değeri kontrol ederek sağlayabilmektedir. -> "Source IP Address" : Kaynak "IP" adresinin "unicast IP" adresi olması gerekmektedir. -> "Destination IP Address" : Hedef "IP" adresi "unicast", "broadcast" ve "multicast" "IP" adresi olabilir. Buradan da gördüğünüz gibi "IP" başlığında kaynak ve hedef "IP" adresleri ve "IP" paketinin toplam uzunluğu bulunmaktadır. Port kavramının "IP" protokolünde olmadığını anımsayınız. >>>> "IPv6" protokolünün başlık kısmı şöyledir: Toplam başlık uzunluğu 40 byte'a sabitlenmiştir. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +-----------+-----------------+----------------------------------------------------------------+ ^ | Version | Traffic Class | Flow Label | (4 bytes) | | (4 bits) | (4 bits) | | | +-----------+-----------------+----------------+-----------------------+-----------------------+ | | Payload Length | Next Header | Hop Limit | (4 bytes) | | (16 bits) | | | | +----------------------------------------------+-----------------------+-----------------------+ | | | | | | | | | | | | | | Source IP Address (128 bits) | (16 bytes) | | | | 40 bytes | | | | | | +----------------------------------------------------------------------------------------------+ | | | | | | | | | | | | | | Destination IP Address (128 bits) | (16 bytes) | | | | | | | | | | +----------------------------------------------------------------------------------------------+ v Bu başlıklardan, -> "Version" : "IP" versiyonunu içerir. "IPv6" için "6" değeri kullanılmaktadır. -> "Traffic Class" : Paket önceliği konusunda kullanılmaktadır. -> "Flow Label" : Bir sunucuyla yapılan haberleşme için bir numara belirlenmektedir ve haberleşme boyunca bütün paketlerde bu numara kullanılarak iletişim sağlanmaktadır. Farklı amaçlar için de kullanıldığı durumlar vardır. -> "Payload Length" : Datanın boyutunu içermektedir. -> "Next Header" : "L4" protokol bilgisini içermektedir. -> "Hop Limit" : "IPv4"'teki "TTL" alanıyla aynıdır. TCP paketi yukarıda da belirttiğimiz gibi "IP" paketinin veri kısmındadır. "TCP" paketi de "TCP Header" ve "TCP Data" kısımlarından oluşmaktadır. "TCP" başlık kısmı şöyledir ki her satırda 4 byte bulunmaktadır: <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +----------------------------------------------+-----------------------------------------------+ ^ | Source Port | Destination Port | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ | | Sequence Number | (4 bytes) | | (32 bits) | | +----------------------------------------------------------------------------------------------+ | | Acknowledgement Number | (4 bytes) | | (32 bits) | | 20 bytes +-----------+----------------+-----------------+-----------------------------------------------+ | |Header Len.| Reserved | Control Bits | Window Size | (4 bytes) | | (4 bits) | (6 bits) | (6 bits) | (16 bits) | | +-----------+----------------+-----------------+-----------------------------------------------+ | | Checksum | Urgent | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Options | | (0 or 32 bits) | +----------------------------------------------------------------------------------------------+ | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ Bu başlıklardan, -> Burada her satır "32-bit" yani "4" bayt yer kaplamaktadır. -> "TCP" başlığı "20" bayttan "60" bayta kadar değişen uzunlukta olabilir. -> "Header Length", "TCP" verisinin hangi "offset" ten başladığını dolayısıyla "TCP" başlığının "DWORD" (4 bayt olarak) uzunluğunu belirtir. Yani başlığın bayt uzunluğu için buradaki değer "4" ile çarpılmalıdır. Böylece "Header Length" kısmında en az "5" (toplam "20" bayt), en fazla "15" (toplamda "60" bayt) değeri bulunabilir. -> Bu başlıkta kaynak ve hedef "IP" adreslerinin ve "TCP" nin veri kısmının uzunluğunun bulunmadığına dikkat ediniz. Çünkü bu bilgiler zaten "IP" başlığında doğrudan ya da dolaylı biçimde bulunmaktadır. "TCP" paketi her zaman "IP" paketinin veri kısmında konuşlandırılmaktadır. -> Başlıktaki "Control Bits" alanı "6" bitten oluşmaktadır. Her bit, bir özelliği temsil eder. Buradaki belli bitler "set" edildiğinde başlıktaki belli alanlar da anlamlı hale gelebilmektedir. Buradaki bitler, yani bayraklar şunlardır: "URG", "ACK", "PSH", "RST", "SYN" ve "FIN". -> Flags alanındaki birden fazla bit 1 olabilir. Yani birden fazla flag set edilmiş olabilir. Pekala Bir "TCP" paketi "(TCP segment)" yalnızca başlık içerebilir. Yani hiç veri içermeyebilir. Başka bir deyişle, "TCP" paketinin veri kısmı "0" bayt uzunluğunda olabilir. Yine "TCP" protokolünde o anda iki tarafın da bulunduğu bir "durum (state)" vardır. Taraflar belli eylemler sonucunda durumdan duruma geçiş yaparlar. Bu nedenle "TCP" nin çalışması bir "sonlu durum makinesi (finite state machine)" biçiminde ele alınıp açıklanabilir. Henüz bağlantı yoksa iki taraf da "CLOSED" denilen durumdadır. "TCP" de tarafların hangi olaylar sonucunda hangi durumda olduklarına ilişkin diyagrama "durum diyagramı (state diagram)" denilmektedir. ("TCP" durum diyagramı için Google'da "TCP state diagram" araması ile görsellerden çizilmiş diyagramları görebilirsiniz.). "TCP" bağlantısının kurulması için "client" ile "server", veri kısmı boş olan (yani yalnızca başlık kısmı bulunan) paketleri gönderip almaktadır. Buna "el sıkışma (hand shaking)" denilmektedir. "TCP" de bağlantı kurulması için yapılan el sıkışma dörtlü "(four-way)" ya da üçlü "(three-way)" olabilir. Burada üçlü demekle bağlantı için toplam "3" paketin yolculuk etmesi, dörtlü demekle toplam "4" paketin yolculuk etmesi kastedilmektedir. Uygulamada daha çok üçlü el sıkışma kullanılmaktadır. "TCP" de bağlantının kurulabilmesi için iki tarafın da birbirlerine; -> "SYN" biti "set" edilmiş ve veri kısmı olmayan "TCP" paketi (20 bayt) göndermesi -> Karşı taraftan "ACK" biti "set" edilmiş ve verisi olmayan "TCP" paketi alması gerekir. Yukarıda da belirttiğimiz gibi bunun iki yolu olabilir; Dörtlü ve Üçlü El Sıkışması >>>> Dörtlü El Sıkışması: Temsili gösterimi aşağıdaki gibidir. Burada "4" adet paket kullanıldığı için buna dörtlü el sıkışma denilmektedir. Client Server +-----------------+ +-----------------+ | CLOSED | | LISTEN | +-----------------+ +-----------------+ ------- SYN -----> +-----------------+ | SYN-SENT | +-----------------+ <------ ACK ------ <------ SYN ------ +-----------------+ +-----------------+ | ESTABLISHED | | SYN-RECEIVED | +-----------------+ +-----------------+ ------- ACK -----> +-----------------+ | ESTABLISHED | +-----------------+ >>>> Üçlü El Sıkışması: Temsili gösterimi aşağıdaki gibidir. "Server" bağlantı sırasında "ACK" ile "SYN" bitini tek bir paket olarak da gönderilebilir. (Yani paketin "Flags" kısmında hem "SYN" hem de "ACK" biti "set" edilmiş olabilir.) Buna da üçlü el sıkışma denilmektedir. Client Server +-----------------+ +-----------------+ | CLOSED | | LISTEN | +-----------------+ +-----------------+ ------- SYN -----> +-----------------+ | SYN-SENT | +-----------------+ <--- SYN + ACK --- +-----------------+ | SYN-RECEIVED | +-----------------+ ------- ACK -----> +-----------------+ +-----------------+ | ESTABLISHED | | ESTABLISHED | +-----------------+ +-----------------+ Bağlantının kopartılması için iki tarafın da birbirlerine "FIN" biti "set" edilmiş paketler gönderip "ACK" biti "set" edilmiş paketleri alması gerekir. Bağlantının kopartılması da tipik olarak üçlü ya da dörtlü el sıkışma yoluyla yapılmaktadır. Bağlantının kopartılması talebini herhangi bir taraf başlatabilir. >>>> Dörtlü El Sıkışması: "FIN" ve "ACK" paketleri ayrı ayrı gönderilirse dörtlü el sıkışma gerçekleşmiş olur. Dörtlü el sıkışma ile bağlantının kopartılması şöyle yapılmaktadır: Peer - 1 Peer - 2 +-----------------+ +-----------------+ | ESTABLISHED | | ESTABLISHED | +-----------------+ +-----------------+ ------- FIN -----> +-----------------+ +-----------------+ | FIN-WAIT-1 | | CLOSE_WAIT | +-----------------+ +-----------------+ <------ ACK ------ +-----------------+ | FIN-WAIT-2 | +-----------------+ <------ FIN ------ +-----------------+ | LAST-ACK | +-----------------+ ------- ACK -----> +-----------------+ +-----------------+ | TIME-WAIT | | CLOSED | +-----------------+ +-----------------+ +-----------------+ | CLOSED | +-----------------+ Burada iki taraf da birbirlerine "FIN" biti "set" edilmiş ve veri kısmı olmayan "TCP" paketleri gönderip, "ACK" biti "set" edilmiş ve veri kısmı olmayan "TCP" paketleri almıştır. >>>> Üçlü El Sıkışması: "FIN" ve "ACK" paketleri tek bir paket olarak gönderilirse üçlü el sıkışma gerçekleşmiş olur. Üçlü el sıkışma ile bağlantının kopartılmasında bir taraf tek bir pakette hem "FIN" biti "set" edilmiş hem de "ACK" biti "set" edilmiş paket göndermektedir. Şöyleki: Peer - 1 Peer - 2 +-----------------+ +-----------------+ | ESTABLISHED | | ESTABLISHED | +-----------------+ +-----------------+ ------- FIN -----> +-----------------+ +-----------------+ | FIN-WAIT-1 | | CLOSE_WAIT | +-----------------+ +-----------------+ <--- FIN + ACK --- +-----------------+ +-----------------+ | FIN-WAIT-2 | | LAST-ACK | +-----------------+ +-----------------+ ------- ACK -----> +-----------------+ +-----------------+ | TIME-WAIT | | CLOSED | +-----------------+ +-----------------+ +-----------------+ | CLOSED | +-----------------+ Burada özetle bir taraf önce karşı tarafa "FIN" paketi yollamıştır. Karşı taraf buna "ACK+FIN" ile karşılık vermiştir. Diğer taraf da son olarak karşı tarafa "ACK" yollamıştır. Ancak bağlantıyı kopartmak isteyen taraf bu "ACK" yollama işinden sonra "MSL (Maximum Segment Life)" denilen bir zaman aralığının iki katı kadar beklemektedir (Tipik olarak 2 dakika). >>>>> "MSL" : Bir paketin kaybolduğuna karar verilmesi için gereken zamanı belirtmektedir. Eğer alıcı taraf beklemeden hemen "CLOSED" duruma geçseydi bu durumda gönderici taraf yeniden bağlantı kurduğunda henüz alıcı taraf paketi almamışsa sanki eski bağlantı devam ettiriliyormuş gibi bir durum olabilirdi. Öte taraftan soket programlamada bağlantı "shutdown" ile "SHUT_RD" kullanılarak kopartıldığında yine yukarıdaki 3'lü el sıkışma gerçekleşmektedir. Bağlantının koparılması "yarım biçimde de (half close") yapılabilir. Bu durumda bir taraf diğer tarafa "FIN" paketi gönderir. Karşı taraf da buna "ACK" paketi ile karşılık verir. Bundan sonra artık "FIN" gönderen taraf veri gönderemez ama alabilir, "ACK" gönderen taraf ise veri alamaz ama gönderebilir. Şöyleki: Peer - 1 Peer - 2 +-----------------+ +-----------------+ | ESTABLISHED | | ESTABLISHED | +-----------------+ +-----------------+ ------- FIN -----> +-----------------+ +-----------------+ | FIN-WAIT-1 | | CLOSE_WAIT | +-----------------+ +-----------------+ <------ ACK ------ +-----------------+ | CLOSING | +-----------------+ +-----------------+ | TIME_WAIT | +-----------------+ Bunun tersi de şöyle söz konusu olabilir: Peer - 1 Peer - 2 <------ FIN ------ +-----------------+ +-----------------+ | CLOSE_WAIT | | FIN-WAIT-1 | +-----------------+ +-----------------+ ------- ACK -----> +-----------------+ | CLOSING | +-----------------+ +-----------------+ | TIME_WAIT | +-----------------+ Burada da artık Peer-2 veri gönderemez ama alabilir, Peer-1 ise veri veri alamaz fakat gönderebilir. Pekiyi shutdown fonksiyonunun "half close" işlemindeki etkisi nasıldır? Aslında "shutdown" fonksiyonunun ikinci parametresinde belirtilen "SHUT_WR", "SHUT_RD" ve "SHUT_RDWR" değerlerinin protokoldeki bağlantının kopartılması süreciyle bir ilgisi yoktur. "shutdown" fonksiyonu her durumda "half close" uygulamaktadır. Yani "shutdown" fonksiyonunun ikinci parametresi ne olursa olsun, bu fonksiyonu çağıran taraf karşı tarafa "FIN" paketi yollar, karşı taraf da bu tarafa "ACK" paketi yollar. Zaten protokolün kendisinde "half close" işlemi "SHUT_WR", "SHUT_RD" ya da "SHUT_RDWR" biçiminde bir bilgi taşımamaktadır. "TCP" protokolü tasarlandığında "half close" işleminin "bir tarafı göndermeye kapatıp diğer tarafı almaya kapatmak" gibi bir işlev göreceği düşünülmüştür. Ancak bu "half close" işleminin işletim sistemleri tarafından tam olarak nasıl ele alınacağı "TCP/IP" soket gerçekleştirimini yapanlar tarafından belirlenmektedir. Şimdi Linux sistemlerinde "shutdown" fonksiyonunun muhtemel gerçekleştirimi ve arka planda gerçekleşen muhtemel işlemler konusunda bilgi verelim. * Örnek 1, Bir taraf "shutdown" fonksiyonunu "SHUT_WR" parametresiyle aşağıdaki gibi çağırmış olsun. Ancak karşı taraf "shutdown" fonksiyonunu çağırmamış olsun. Şöyleki: shutdown(sock, SHUT_WR); Burada "SHUT_WR" uygulayan taraf diğer tarafa "FIN" paketi gönderir, diğer taraf da buna "ACK" ile yanıt verir ve "half close" işlemi gerçekleşir. Artık "SHUT_WR" uygulayan taraf bundan sonra diğer tarafa veri göndermemeli diğer taraf da karşı taraftan veri almamalıdır. Bir taraf "SHUT_WR" ile "half close" uyguladığında karşı taraf "recv" işlemi yaparsa, sanki soket kapatılmış gibi, recv fonksiyonu "0" ile geri dönecektir. "SHUT_WR" yapan taraf "send" fonksiyonunu kullandığında ise "SIGPIPE" sinyali oluşacaktır. * Örnek 2, Şimdi de bir taraf "shutdown" fonksiyonunu "SHUT_RD" ile çağırmış olsun. Şöyleki: shutdown(sock, SHUT_RD); Bu durumda yine "SHUT_RD" uygulayan taraf karşı tarafa "FIN" paketi gönderir ve karşı taraftan "ACK" paketi alır. Böylece "half close" işlemi gerçekleşir. Artık "SHUT_RD" uygulayan taraf veri almayacak fakat veri gönderebilecektir. Karşı taraf ise veri alabilecek ancak veri gönderemeyecektir. Tabii aslında karşı taraf "shutdown" fonksiyonunun hangi parametreyle çağrıldığını bilmemektedir. Dolayısıyla aslında soket fonksiyonlarıyla veri göndermeye devam edebilecektir. Karşı taraf eğer "send" işlemi yaparsa burada işletim sistemi değişik davranışlar gösterebilmektedir. Karşı tarafın gönderdiği paketler karşı tarafa ulaştığında, "SHUT_RD" yapan taraftaki işletim sistemi bu paketleri hiç dikkate almayabilir. Böylece "SHUT_RD" yapan taraf "recv" fonksiyonunu çağırsa bile fonksiyon "0" ile geri döner. Ya da işletim sistemi böylesi bir durumda karşı taraf veri gönderdiğinde ona "RST" bayrağı "set" edilmiş paket gönderip ki buna "connection reset" denilmektedir, karşı tarafın artık "send" işlemlerinde "SIGPIPE" sinyali üretmesini sağlayabilir. * Örnek 3, Şimdi de "shutdown" fonksiyonunun "SHUT_RDWR" parametresi ile çağrıldığını düşünelim. Bu en çok kullanılan parametredir. Bu durumda yine fonksiyonu çağıran taraf karşı tarafa "FIN" paketi gönderir, karşı taraftan "ACK" paketi alır. Yine "half close" işlemi gerçekleşir. Ancak artık "SHUT_RDWR" uygulayan taraf "recv" ve "send" işlemlerini yapamayacaktır. "SHUT_RDWR" uygulayan taraf "recv" fonksiyonunu çağırırsa fonksiyon "0" ile geri dönecek, "send" fonksiyonunu çağırırsa doğrudan "SIGPIPE" sinyali oluşacaktır. Bu durumda "SHUT_RDWR" uygulayan tarafın karşı tarafı, artık "send" işlemi yaparsa yine davranış yukarıda "SHUT_RD" fonksiyonunda belirtildiği gibi gerçekleşecektir. Tabii normal olarak iki tarafın da aslında ayrı ayrı "shutdown" fonksiyonunu çağırması gerekir. Bu durumda dörtlü el sıkışma gerçekleşecektir. "TCP/IP" soket programlamada önce bir taraf "shutdown" uygulayıp "half close" oluşturabilir. Diğer taraf da bunu anlayıp o da "shutdown" uygulayarak dörtlü el sıkışma oluşturabilir. Diğer yandan TCP'de akış kontrolü için "acknowledgement" yani "alındı" bildirimi kullanılmaktadır. Bir taraf bir tarafa bir paket veri gönderirken verinin yanı sıra aynı zamanda paketin "Flags" kısmındaki "PSH" bitini "1" yapar. Karşı taraf da paketi aldığını diğer tarafa haber vermek için diğer tarafa "ACK" biti "set" edilmiş bir paket gönderir. "TCP" de her gönderilen paket için bir "alındı" bilgisinin alınması gerekir. Eğer paketi gönderen taraf bu paketi içeren bir "ACK" paketi alamazsa bu durumda "paketin karşı tarafa ulaşmadığından" şüphelenmektedir. Bu durumda paketi gönderen taraf, belli bir algoritma ile belirli zaman aralıklarıyla, aynı paketi yeniden göndermektedir. Böylesi bir durumda paketi alan taraf aynı paketi birden fazla kez de alabilir. Bu durumda bu paketlerden yalnızca tek bir kopyasının işleme sokulması, alan tarafın sorumluluğundadır. Tabii gönderen tarafın paketi yolda kaybolabileceği gibi alan tarafın "ACK" paketi de yolda kaybolabilir. Bu durumda yine gönderen taraf "ACK" alamadığına göre göndermeye devam edecektir. Bu durumda alıcı taraf bunun için yine "ACK" gönderecektir. Öte yandan, yukarıda da belirttiğimiz gibi, paketin "data" kısmı dolu olmak zorunda değildir. Bağlantı sağlanırken ve sonlandırırken gönderilen "SYN" ve "FIN" paketleri "data" içermemektedir. "ACK" paketi ayrı bir paket olarak gönderilmek zorunda da değildir. Bilgiyi alan taraf hem bilgi gönderirken hem de "ACK" işlemi yapabilir. Şöyleki: +---------+ | PSH | +---------+ --- data ---> +---------------+ | PSH + ACK | +---------------+ <--- data --- +---------+ | ACK | +---------+ ------------> Aslında izleyen paragraflarda da ele alınacağı gibi "ACK" biti yalnızca "alındığını bildirme için değil", pencere genişliklerinin ayarlanması için de kullanılmaktadır. Yani bir taraf karşı taraftan bilgi almadığı halde yine "ACK" gönderebilir. Diğer yandan "TCP" de kümülatif bir "acknowledgement" sistemi kullanılmaktadır. Yani paketi gönderen taraf bu paket için "ACK" almadan başka paketleri gönderebilir. Paketleri alan taraf, birden fazla paket için, tek bir "ACK" yollayabilir. Kümülatif "ACK" işlemi için "sıra numarası (sequence number)" denilen bir değerden faydalanılmaktadır. "Sıra numarası (sequence number)", gönderilen paketin içerisindeki kaçıncı bayttan başladığını belirten bir değerdir. Bunu dosyalardaki dosya göstericisine benzetebiliriz. Sıra numarası, "TCP" başlığında "32-bit" lik bir alanda tutulmaktadır. Sıra numarası bu alanın sonuna geldiğinde yeniden başa dönmektedir ki buna "wrapping" denir. Yine sıra numarası bağlantı kurulduğunda sıfırdan başlatılmaz, rastgele bir değerden başlatılmaktadır. * Örnek 1, Belli bir anda bir tarafın sıra numarası "1552" olsun. Şimdi bu taraf karşı tarafa "300" bayt göndersin. Artık bu gönderimden sonra sıra numarası "1852" olacaktır. Yani bir sonraki gönderimde bu taraf, sıra numarası olarak, "1852" yi kullanacaktır. Sıra numarası her bilgi gönderiminde bulundurulmak zorundadır. Bilgiyi alan taraf "ACK" paketini gönderirken paketteki sıra numarasını "talep ettiği sonraki sıra numarası" olarak paketin sıra numarasını belirten kısmına yerleştirir. Şöyleki: Peer-1 Peer-2 300 byte (sequence Number: 3560) -----> 100 byte (sequence Number: 3860) -----> <---- ACK (Acknowledgement Number: 3960) 50 byte (sequence Number: 3960) ------> <---- ACK (Acknowledgement Number: 4010) 10 byte (sequence Number: 4010) ------> Buradaki gönderimde gönderen taraf önce "300" baytlık bir paketi, sonra "100" baytlık bir paketi karşı tarafa göndermiştir. Karşı taraf ise bu iki paket için tek bir "ACK" göndermiştir. Karşı tarafın gönderdiği "ACK" aslında diğer taraftan yeni talep edeceği sıra numarasındaki bilgiyi belirtmektedir. İki paketi gönderen taraf karşı taraftan gelen "ACK" içerisindeki bu sıra numarasına baktığında bu iki paketinde alındığını anlamaktadır. Görüldüğü gibi her paket için ayrı bir "ACK" yollanmak zorunda değildir. Buna "kümülatif alındı (cumulative acknowledgment)" bildirimi denilmektedir. * Örnek 2, Bir tarafın karşı tarafa peş peşe beş paket gönderdiğini düşünelim. Karşı taraftan bir adet "ACK" gelmiş olsun. Gönderen taraf bu "ACK" paketine bakarak gönderdiği bilginin ne kadarının karşı taraf tarafından alındığını anlayabilmektedir. Pekiyi bir "TCP" paketi, yani ("TCP segment"), gönderici ("sender") tarafından gönderildikten sonra alıcı ("receiver") bunu alamamışsa ne olacaktır? Çünkü "TCP" nin güvenli bir protokol olması demek, bir biçimde böyle bir durumda bir telafinin yapılması demektir. İşte yukarıda da belirttiğimiz gibi "TCP" protokolü şöyle yöntem izlemektedir: -> Gönderen taraf her gönderdiği paket ("TCP segment") için bir zamanlayıcı kurar ki zamanlayıcıya "retransmission timer" denilmektedir. -> Eğer belli süre içerisinde gönderilen "TCP" paketini kapsayan bir "ACK" gelmediyse, gönderici taraf aynı paketi yeniden göndermektedir. Böylece aslında gönderilen paket henüz onun için "ACK" gelmedikçe gönderme tamponundan atılmaz. "Retransmission timer" bazı değerlere göre dinamik bir biçimde oluşturulmaktadır. Bunun detayları için önerilen kaynaklara bakılabilir. Tabii böyle bir sistemde alıcı taraf aynı paketi birden fazla kez alabilmektedir. Yukarıda da belirttiğimiz gibi bu durumda bu paketlerin yalnızca tek bir kopyasını alıp diğerlerini atmak alıcı tarafın sorumluluğundadır. Diğer taraftan "TCP" protokolünün bir "akış kontrolü (flow control)" oluşturduğunu belirtmiştik. Akış kontrolünün amacı tampon taşmasının engellenmesidir. Bağlantı sağlandıktan sonra bir tarafın diğer tarafa sürekli bilgi gönderdiğini düşünelim. Bu bilgileri işletim sistemi alacak ve bekletecektir. Pekiyi ya ilgili proses soketten okuma yapmazsa? Bu durumda hala karşı taraf bilgi gönderirse işletim sisteminin ayırdığı tampon taşabilir. Tipik olarak işletim sistemleri bağlantı yapılmış her soket için iki tampon bulundurmaktadır: "Gönderme tamponu (send buffer)" ve "alma tamponu (receive buffer)". Biz "send" fonksiyonunu kullandığımızda, göndermek istediğimiz bilgiler gönderme tamponuna yazılır ve hemen "send" fonksiyonu geri döner. Gönderme tamponundaki bilgilerin paketlenerek gönderilmesi belli bir zaman sonra işletim sistemi tarafından yapılmaktadır. Eğer "send" işlemi sırasında zaten gönderme tamponu doluysa "send" fonksiyonu gönderilecek olanları tamamen tampona yazana kadar blokeye yol açmaktadır. Alma tamponu "(receive buffer)" karşı tarafın gönderdiği bilgilerin alınması için kullanılan tampondur. Karşı tarafın gönderdiği bilgiler alındığında işletim sistemi bu bilgileri alma tamponuna yerleştirir. Aslında "recv" fonksiyonu bu tampondan bilgileri almaktadır. send ---> [gönderme tamponu] ---> işletim sistemi gönderiyor ---> ||||| <--- işletim sistemi alıyor ---> [alma tamponu] <--- recv Akış kontrolünün en önemli unsurlarından biri, alma tamponunun taşmasını engellemektir. * Örnek 1, Gönderici taraf sürekli bilgi gönderirse fakat alıcı taraftaki proses "recv" işlemiyle hiç okuma yapmazsa, alıcı taraftaki işletim sisteminin alıcı tamponu dolabilir ve sistem çökebilir. İşte akış kontrolü sayesinde alıcı taraf gönderici tarafa "artık gönderme, benim tamponum doldu" diyebilmektedir. * Örnek 2, Şimdi bir taraftaki prosesin diğer tarafa bir döngü içerisinde "send" fonksiyonuyla bilgi gönderdiğini ancak diğer taraftaki prosesin bu bilgiyi almadığını varsayalım. Akış kontrolünün uygulandığı durumda ne olacaktır? İşte önce "send" ile gönderilenler karşı tarafa iletilecektir. Karşı tamponu dolduğunda karşı taraf, gönderen tarafa "artık gönderme" diyecektir. Bu durumda göndermeyi kesen taraftaki proses hala send işlemi yapacağına göre o tarafın da gönderme tamponu dolacak ve "send" fonksiyonu blokeye yol açacaktır (Linux sistemlerinde tek bir "send" ile gönderme tamponundan daha büyük bir bilgiyi göndermek istediğimizde tüm bilgi yine tampona yerleştirilene kadar bloke oluşmaktadır.) * Örnek 3, Aşağıdaki "client" program bağlantı kurduktan sonra bir döngü içerisinde "server" programa "send" fonksiyonu ile bilgi göndermektedir. Ancak "server" program bu bilgiyi "recv" ile okumamaktadır. Yukarıda da belirttiğimiz gibi bu durumda "server" programın tamponu dolacak ve "server" program karşı tarafa "artık gönderme" diyecek. Bu kez de karşı tarafın gönderme tamponu dolacak dolayısıyla "send" fonksiyonu da bir süre sonra blokede bekleyecektir. Bu programları farklı terminallerden, "./server 55555" "./client localhost 55555" gibi çalıştırabilirsiniz: /* server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock_server, sock_client; struct sockaddr_in sinaddr, sinaddr_client; socklen_t sinaddr_len; char ntopbuf[INET_ADDRSTRLEN]; in_port_t port; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } port = (in_port_t)strtoul(argv[1], NULL, 10); if ((sock_server = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sinaddr.sin_family = AF_INET; sinaddr.sin_port = htons(port); sinaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock_server, (struct sockaddr *)&sinaddr, sizeof(sinaddr)) == -1) exit_sys("bind"); if (listen(sock_server, 8) == -1) exit_sys("listen"); printf("Waiting for connection...\n"); sinaddr_len = sizeof(sinaddr_client); if ((sock_client = accept(sock_server, (struct sockaddr *)&sinaddr_client, &sinaddr_len)) == -1) exit_sys("accept"); printf("Connected: %s : %u\n", inet_ntop(AF_INET, &sinaddr_client, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sinaddr_client.sin_port)); printf("Press any key to EXIT...\n"); getchar(); shutdown(sock_client, SHUT_WR); close(sock_client); close(sock_server); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* client.c */ #include #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct addrinfo *ai, *ri; struct addrinfo hints = {0}; char buf[BUFFER_SIZE] = {0}; int result; ssize_t sresult; ssize_t stotal; if (argc != 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; if ((result = getaddrinfo(argv[1], argv[2], &hints, &ai)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result)); exit(EXIT_FAILURE); } for (ri = ai; ri != NULL; ri = ri->ai_next) if (connect(sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(ai); printf("Connected...\n"); stotal = 0; for (;;) { printf("send calls...\n"); if ((sresult = send(sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("send"); stotal += sresult; printf("bytes sent: %jd, total bytes sent: %jd\n", sresult, stotal); } close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de bu akış kontrolünün detaylarını inceleyelim. "TCP" de bunun için "pencere (window)" kavramı kullanılmaktadır. Pencerenin bir büyüklüğü ("window size") vardır. Pencere büyüklüğü "TCP" başlığında belirtilmektedir. Pencere büyüklüğü demek, "hiç 'ACK' gelmediği durumda göndericinin en fazla gönderebileceği byte sayısı" demektir. * Örnek 1, pencere genişliğinin "8K" olması demek, "alıcı 'ACK' göndermedikten sonra göndericinin en fazla 8K gönderebilmesi" demektir. Pencere genişliği, alıcı taraf tarafından gönderici tarafa bildirilir. * Örnek 1, pencere genişliği alıcı taraf için "8K" olsun. Bu durumda gönderici taraf sırasıyla, "1K + 1K + 1K + 1K + 1K" uzunluğunda toplam "5K" lık bilgiyi karşı tarafa göndermiş olsun. Eğer henüz "ACK" gelmemişse gönderici taraf en fazla "3K" kadar daha bilgi gönderebilir. Diğer yandan "TCP" de her "ACK" sırasında yeni pencere genişliği de karşı tarafa gönderilmek zorundadır. Yani "ACK" paketi gönderilirken aynı zamanda yeni pencere genişliği de gönderilmektedir. "ACK" paketi yalnızca alındı bilgisini göndermek için değil, pencere genişliğini ayarlamak için de gönderilebilmektedir. Başka bir deyişle bir taraf, "yalnızca bilgi aldığı için" "ACK" göndermek zorunda değildir. Hiç bilgi almadığı halde yeni pencere genişliğini karşı tarafa bildirmek için de "ACK" gönderebilir. Pencere genişliği en fazla "64K" olabilir. Çünkü bunun için "TCP" başlığında "16-bit" yer ayrılmıştır. * Örnek 1, Şimdi bir tarafın diğer tarafa "send" fonksiyonu ile sürekli bilgi gönderdiğini ancak diğer tarafın bilgiyi "recv" ile okumadığını düşünelim. İşletim sisteminin alıcı taraf için oluşturduğu alma tamponunun "1MB" olduğunu düşünelim. Alıcı taraf muhtemelen bilgi geldikçe "ACK" yaparken "64K" lık pencere genişliğini karşı tarafa bildirecektir. Ancak zamanla alma tamponu dolduğu için bu pencere genişliğini düşürecek en sonunda "ACK" ile pencere genişliğini "0" yapacak ve karşı tarafa "artık gönderme" diyecektir. Pencere genişliği ile alma tamponunun genişliği birbirine karıştırılmamalıdır; Alma tamponu gelen bilgilerin yerleştirildiği tampondur. Pencere genişliği karşı tarafın "ACK" almadıktan sonra gönderebileceği maksimum bayt sayısıdır. Pekiyi pencere genişlikleri ve sıra numaraları bağlantı sırasında nasıl karşı tarafa bildirilmektedir? İşte bağlantı kurulurken "client" taraf "SYN" paketi içerisinde kendi başlangıç sıra numarasını karşı tarafa iletmektedir. "Server" da bağlantıyı kabul ederken yine "SYN" (ya da "SYN + ACK") paketinde kendi sıra numarasını karşı tarafa bildirmektedir. Pencere genişliği de aslında ilk kez bağlantı yapılırken "ACK" paketlerinde belirtilmektedir. "TCP/IP" de "stack" gerçekleştirimleri "ACK" stratejisi için bazı yöntemler uygulamaktadır. * Örnek 1, Eğer gönderilecek paket varsa bununla birlikte "ACK" paketinin gönderilmesi, ACK'ların iki paket biriktirildikten sonra gönderilmesi vb. * Örnek 2, Pencere genişliklerinin ayarlanması için de bazı stratejiler izlenebilmektedir. Bunun için "TCP/IP Protocol Suite" kitabının 466'ıncı sayfasına başvurabilirsiniz. "TCP" paketindeki önemli "Flag"'lerden birisi de "RST" bitidir. Buna "reset isteği" denilmektedir. Bir taraf "RST" bayrağı "set" edilmiş paket alırsa artık karşı tarafın "abnormal" bir biçimde bağlantıyı kopartıp yeniden bağlanma talep ettiği anlaşılır. Normal sonlanma el sıkışarak başarılı bir biçimde yapılırken, "RST" işlemi anormal sonlanmaları temsil eder. * Örnek 1, soket kütüphanelerinde hiç "shutdown" yapmadan soket "close" edilirse "close" eden taraf karşı tarafa "RST" paketi göndermektedir. Halbuki önce "shutdown" yapılırsa el sıkışmalı sonlanma gerçekleştirilir. O halde her zaman aktif soketler "shutdown" yapıldıktan sonra "close" edilmelidir. >>> "UDP/IP" Protokolünün Alt Seviye İşlemleri: "UDP" protokolü aslında saf "IP" protokolüne çok benzerdir. "UDP" yi "IP" den ayıran iki önemli farklılık şudur: -> "UDP", port numarası kavramına sahiptir. -> "UDP" nin hata için bir "checksum" mekanizması vardır. Yani bir taraf diğer tarafa "UDP" paketi gönderirken, gönderdiği veri için, "checksum" bilgisini de "UDP" başlık kısmına iliştirmektedir. Bir "UDP" paketi yine aslında "IP" paketinin veri kısmında bulunmaktadır. "UDP header" ı "8" bayttan oluşmaktadır ve yapısı aşağıdaki gibidir. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +----------------------------------------------+-----------------------------------------------+ ^ | Source Port | Destination Port | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ | 8 bytes | Length | Checksum | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ Burada "UDP" paketinin toplam uzunluğunun bulunması aslında gereksizdir. Çünkü uzunluk "TCP" de olduğu gibi aslında "IP" paketinin başlığına bakılarak tespit edilebilmektedir. Ancak hesaplama kolaylığı oluşturmak için bu uzunluk "UDP" başlığında ayrıca bulundurulmuştur. Ayrıca "checksum", "UDP" paketlerinde bulunmak zorunda değildir. Eğer gönderici "checksum" kontrolü istemiyorsa burayı "0" bitleriyle doldurur. (Eğer zaten "checksum" "0" ise burayı "1" bitleriyle doldurmaktadır.) Alan taraf "checksum" hatasıyla karşılaşırsa "TCP" de olduğu gibi paketi yeniden talep etmez. Yalnızca onu atar. >> Aynı makinede koşan prosesler arası haberleşme yöntemleri (devam): >>> "UNIX Domain Socket" : Aynı makinadaki prosesler arasında "socket" fonksiyonları kullanılarak haberleşme yapılmasıdır. Aynı makinadaki "Client-Server" uygulamalarında, birden çok "client" bağlnacaksa, "socket" fonksiyonları kullanılarak gerçekleştirim sağlanması daha kolaydır ve haberleşme daha hızlıdır. Çünkü "IP" protokol ailesinin kullanılması oldukça yavaş bir haberleşme sağlamaktadır. İşte bu amaçla oluşturulan soket nesnelerine "UNIX Domain Socket" adı verilir. Buradaki soket nesneleri, prosesler arasında kullanılan soket nesnelerinden farklıdır. Yine bu "UNIX Domain Socket" pekala Windows sistemleri ve macOS sistemleri tarafından da desteklenmektedir. Bir "UNIX Domain Socket" oluşturabilmek için "socket" fonksiyonunun birinci parametresi, yani "(protocol family)", için "AF_UNIX" geçilmelidir. "UNIX Domain Socket" kullanırken şu noktalara da dikkat etmeliyiz; -> "UNIX Domain Socket", "TCP/IP" ya da "UDP/IP" soketlerle bir ilgisi yoktur. Bu soketler UNIX/Linux sistemlerinde oldukça etkin bir biçimde gerçekleştirilmektedir. Dolayısıyla aynı makinenin prosesleri arasında haberleşmede borulara, mesaj kuyruklarına, paylaşılan bellek alanlarına bir seçenek olarak kullanılabilmektedir. Hatta bazı UNIX türevi sistemlerde (ama Linux'ta böyle değil) aslında çekirdek tarafından önce bu protokol gerçekleştirilip, daha sonra da boru mekanizması bu protokol kullanılarak, gerçekleştirilmektedir. Böylece örneğin aynı makinedeki iki prosesin haberleşmesi için "UNIX Domain Socket", "TCP/IP" ve "UDP/IP" soketlerine göre çok daha hızlı çalışmaktadır. -> Aynı makine üzerinde çok "client" lı uygulamalar için "UNIX Domain Socket", boru haberleşmesine ve mesaj kuyruklarına göre organizasyonel avantaj bakımından tercih edilebilmektedir. Çünkü çok "client" lı boru uygulamalarını ve mesaj kuyruğu uygulamalarını yazmak daha zahmetlidir. Programcılar "TCP/IP" ve "UDP/IP" soket haberleşmesi yaparken kullandıkları fonksiyonların aynısını "UNIX Domain Socket" de de kullanabilmektedir. Böylece örneğin elimizde bir "TCP/IP" ya da "UDP/IP" bir "Client-Server" program varsa, bu programı kolaylıkla "UNIX Domain Socket" kullanılacak biçimde değiştirebiliriz. -> "UNIX Domain Socket" kullanımı ise en çok boru kullanımına benzemektedir. Ancak "UNIX Domain Socket" in, borulara olan bir üstünlüğü "full duplex" haberleşme sunmasıdır. Bilindiği gibi borular "half duplex" bir haberleşme sunmaktadır. Ancak genel olarak boru haberleşmeleri, "UNIX Domain Socket" haberleşmelere göre, daha hızlı olma eğilimindedir. -> "UNIX Domain Socket", kullanım olarak daha önce görmüş olduğumuz "TCP/IP" ve "UDP/IP" soketlerine çok benzemektedir. Yani işlemler sanki "TCP/IP" ya da "UDP/IP" tip "Client-Server" program yazılıyormuş gibi yapılır. Başka bir deyişle "UNIX Domain Socket" de, "client" ve "server" programların genel yazım adımları "TCP/IP" ve "UDP/IP" ile aynıdır. -> "UNIX Domain Socket", "client" ın "server" a bağlanması için gereken adres, bir dosya ismi yani yol ifadesi biçimindedir. Kullanılacak yapı "sockaddr_in" değil, "sockaddr_un" yapısıdır. Bu yapı "" dosyası içerisinde bildirilmiştir ve en azından şu elemanlara sahip olmak zorundadır: #include struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; }; Bu yapının "sun_family" elemanı "AF_UNIX" biçiminde, "sun_path" elemanı da soketi temsil eden dosyanın yol ifadesi biçiminde girilmelidir. Burada yol ifadesiyle belirtilen dosya "bind" işlemi tarafından yaratılmaktadır. Yaratılan bu dosyanın türü "ls -l" komutunda "(s)ocket" biçiminde görüntülenmektedir. Eğer bu dosya zaten varsa "bind" fonksiyonu başarısız olur. Dolayısıyla bu dosyanın varsa silinmesi gerekmektedir. O halde "client" ve "server" programlar işin başında bir isim altında anlaşmalıdır. Önemli bir nokta da şudur: "sockaddr_un" yapısının kullanılmadan önce sıfırlanması gerekmektedir. Öte yandan "bind" tarafından yaratılan bu soket dosyaları, normal bir dosya değildir. Yani "open" fonksiyonuyla açılamamaktadır. -> "UNIX Domain Socket", port numarası biçiminde bir kavramın olmadığına dikkat ediniz. Port numarası kavramı "IP" ailesinin aktarım katmanına ilişkin bir kavramdır. "UNIX Domain Socket", "AF_UNIX" protokol ailesi ismiyle oluşturulan başka bir ailenin soketleridir. -> "UNIX Domain Socket", "server" programı "accept" uyguladığında "client" a ilişkin "sockaddr_un" yapısından, aslında bu protokolde bir "port" kavramı olmadığına göre "server" program bağlantıdan, bir bilgi elde etmeyecektir. Fakat yine de "client" program da "bind" uygulayıp ondan sonra sokete bağlanabilir. Bu durumda "server", "client" bağlantısından sonra "sockaddr_un" yapısından "client" ın "bind" ettiği soket dosyasının yol ifadesini elde eder. -> "UNIX Domain Socket", aynı makinenin prosesleri arasında haberleşme sağladığına göre, bunlarda "send" işlemi ile gönderilen bilginin tek bir "recv" işlemi ile alınması beklenir. Gerçekten de Linux sistemlerinde tasarım bu biçimde yapılmıştır. Ancak POSIX standartları bu konuda bir garanti vermemektedir. Ancak Linux sistemlerinde tıpkı borularda olduğu gibi bu işlem için ayrılan tampon büyüklüğünden fazla miktarda bayt, "send" (ya da "write") ile gönderildiğinde, parçalı okuma gerçekleşebilmektedir. Aşağıda "UNIX Domain Socket" için aşağıdaki örnekleri inceleyelim. * Örnek 1, Aşağıdaki "server" programda "thread" modeli kullanılmıştır. Yani "server" her "client" bağlantısında bir "thread" yaratmaktadır. Bu programların her ikisinde de komut satırı argümanı olarak soket dosyasının yol ifadesi alınmaktadır. Bir soket dosyası zaten var ise "bind" işleminin başarısız olacağını anımsayınız. /* uds-server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 typedef struct tagCLIENT_INFO { int sock; struct sockaddr_un sun; } CLIENT_INFO; void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; CLIENT_INFO *ci; ssize_t result; pthread_t tid; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (bind(server_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); for (;;) { printf("waiting for connection...\n"); sun_len = sizeof(sun_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("accept"); printf("Connected new client\n"); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sun = sun_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough CLIENT_INFO *ci = (CLIENT_INFO *)param; ssize_t result; for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: %s\n", (intmax_t)result, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("client disconnected...\n"); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int client_sock; struct sockaddr_un sun_server; ssize_t result; char buf[BUFFER_SIZE + 1]; char *str; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (connect(client_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("connect"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(client_sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise "client" program da "bind" işlemi uygulamaktadır. Böylece "server", "client" ın soket ismini "accept" fonksiyonundan elde edebilmektedir. Ancak uygulamada client'ın bu biçimde "bind" yapması genellikle tercih edilmemektedir. Eğer "client" a bir isim verilecekse, sonraki paragrafta açıklanacağı gibi, "soyut bir isim" verilmelidir. Buradaki örneğimizde yine "server" program soket dosyasının yol ifadesi ile çalıştırılmalıdır. "client" program da hem "server" soketin hem de "client" soketin yol ifadesi ile ./uds-server serversock ./uds-client serversock clientsock biçiminde çalıştırılmalıdır. /* uds-server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 typedef struct tagCLIENT_INFO { int sock; struct sockaddr_un sun; } CLIENT_INFO; void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; CLIENT_INFO *ci; ssize_t result; pthread_t tid; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (bind(server_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); for (;;) { printf("waiting for connection...\n"); sun_len = sizeof(sun_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("accept"); printf("Connected new client: %s\n", sun_client.sun_path); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sun = sun_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough CLIENT_INFO *ci = (CLIENT_INFO *)param; ssize_t result; for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from \"%s\": %s\n", (intmax_t)result, ci->sun.sun_path, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("\"%s\" client disconnected...\n", ci->sun.sun_path); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int client_sock; struct sockaddr_un sun_server, sun_client; ssize_t result; char buf[BUFFER_SIZE + 1]; char *str; if (argc != 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_client, 0, sizeof(sun_client)); sun_client.sun_family = AF_UNIX; strcpy(sun_client.sun_path, argv[2]); if (bind(client_sock, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("bind"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (connect(client_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("connect"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(client_sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Aslında Linux sistemlerinde "client" ın "server" a kendini tanıtması için "soyut (abstract)" bir adres de oluşturulabilmektedir. "client" program "sockaddr_un" yapısındaki "sun_path" elemanının ilk baytını "NULL" karakter olarak geçip, diğer baytlarına bir bilgi girebilir. "client", bu biçimde "bind" işlemi yaptığında, artık soket dosyası yaratılmaz. Ancak bu isim "accept" ile karşı tarafa iletilir. Dolayısıyla "client" ın girmiş olduğu yol ifadesi aslında soyut bir yol ifadesi olarak "client" ı tespit etmek amacıyla kullanılabilir. (Bu özelliğin POSIX standartlarında bulunmadığını, yalnızca Linux sistemlerine özgü olduğunu bir kez daha anımsatmak istiyoruz.) /* uds-server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 typedef struct tagCLIENT_INFO { int sock; struct sockaddr_un sun; } CLIENT_INFO; void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; CLIENT_INFO *ci; ssize_t result; pthread_t tid; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (bind(server_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); for (;;) { printf("waiting for connection...\n"); sun_len = sizeof(sun_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("accept"); printf("Connected new client: %s\n", sun_client.sun_path + 1); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sun = sun_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough CLIENT_INFO *ci = (CLIENT_INFO *)param; ssize_t result; for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from \"%s\": %s\n", (intmax_t)result, ci->sun.sun_path + 1, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("\"%s\" client disconnected...\n", ci->sun.sun_path + 1); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int client_sock; struct sockaddr_un sun_server, sun_client; ssize_t result; char buf[BUFFER_SIZE + 1]; char *str; if (argc != 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_client, 0, sizeof(sun_client)); sun_client.sun_family = AF_UNIX; strcpy(sun_client.sun_path + 1, argv[2]); if (bind(client_sock, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("bind"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (connect(client_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("connect"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(client_sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4, Aslında Linux sistemlerinde "server" program da "bind" işlemini yaparken soyut isim kullanabilir. Yani "server" program da aslında "sockaddr_un" yapısındaki "sun_path" elemanının ilk karakterini "NULL" karakter yapıp diğer karakterlerine soketin ismini yerleştirebilir. Bu durumda haberleşme sırasında gerçekte hiçbir soket dosyası yaratılmayacaktır. Tabii soket dosyalarının önemli bir işlevi erişim haklarına sahip olmasıdır. Soyut isimler kullanıldığında böyle bir erişim hakkı kontrolü yapılmamaktadır. (Aşağıdaki örnekte server program da soyut bir isim kullanmaktadır. Buradaki haberleşmede hiç soket dosyasının yaratılmayacağına dikkat ediniz.) /* uds-server.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 typedef struct tagCLIENT_INFO { int sock; struct sockaddr_un sun; } CLIENT_INFO; void *client_thread_proc(void *param); char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int server_sock, client_sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; CLIENT_INFO *ci; ssize_t result; pthread_t tid; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((server_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path + 1, argv[1]); if (bind(server_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); for (;;) { printf("waiting for connection...\n"); sun_len = sizeof(sun_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("accept"); printf("Connected new client: %s\n", sun_client.sun_path + 1); if ((ci = (CLIENT_INFO *)malloc(sizeof(CLIENT_INFO))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ci->sock = client_sock; ci->sun = sun_client; if ((result = pthread_create(&tid, NULL, client_thread_proc, ci)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } if ((result = pthread_detach(tid)) != 0) { fprintf(stderr, "pthread_detach: %s\n", strerror(result)); exit(EXIT_FAILURE); } } close(server_sock); return 0; } void *client_thread_proc(void *param) { char buf[BUFFER_SIZE + 1]; // BUFFER_SIZE is enough CLIENT_INFO *ci = (CLIENT_INFO *)param; ssize_t result; for (;;) { if ((result = recv(ci->sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received from \"%s\": %s\n", (intmax_t)result, ci->sun.sun_path + 1, buf); revstr(buf); if (send(ci->sock, buf, result, 0) == -1) exit_sys("send"); } printf("\"%s\" client disconnected...\n", ci->sun.sun_path + 1); shutdown(ci->sock, SHUT_RDWR); close(ci->sock); free(ci); return NULL; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int client_sock; struct sockaddr_un sun_server, sun_client; ssize_t result; char buf[BUFFER_SIZE + 1]; char *str; if (argc != 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((client_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) exit_sys("socket"); memset(&sun_client, 0, sizeof(sun_client)); sun_client.sun_family = AF_UNIX; strcpy(sun_client.sun_path + 1, argv[2]); if (bind(client_sock, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("bind"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path + 1, argv[1]); if (connect(client_sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("connect"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(client_sock, buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(client_sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5, "UNIX Domain Socket", "datagram" haberleşme de yapılabilir. Bu haberleşme mesaj kuyruklarına bir seçenek oluşturmaktadır. "UNIX Domain Socket", datagram haberleşmede gönderilen "datagram" ların aynı sırada alınması garanti edilmiştir. Yani gönderim "UDP/IP" de olduğu gibi güvensiz değil, güvenlidir. Anımsanacağı gibi "UDP/IP" de gönderilen "datagram" lar hedefe farklı sıralarda ulaşabiliyordu. Aynı zamanda bir "datagram" ağda kaybolursa bunun bir telafisi söz konusu değildi. "UNIX Domain Socket" her şey aynı makinede ve işletim sisteminin kontrolü altında gerçekleştirildiği için böylesi bir durum söz konusu olmayacaktır. Aşağıda "UNIX Domain Socket" kullanılarak bir "datagram" haberleşme örneği verilmiştir. Burada "server" hiç bağlantı sağlamadan herhangi bir "client" tan paketi alır, oradaki yazıyı ters çevirip ona geri gönderir. Hem "client" hem de "server" ayrı ayrı iki dosya ismi ile "bind" işlemi yapmaktadır. "server" program komut satırı argümanı olarak kendi "bind" edeceği soket dosyasının yol ifadesini, "client" program ise hem kendi "bind" edeceği soket dosyasının yol ifadesini hem de "server" soketin yol ifadesini almaktadır. (Bu örnekte "server" ve "client" programları, önce "remove" fonksiyonu ile daha önce yaratılan soket dosyasını aynı zamanda silmektedir.) Bu örnekte "client", "server" a bir "datagram" mesaj göndermekte ve "server" da onu ters çevirip "client" a geri yollamaktadır. "server" program, "server" soketin yol ifadesini komut satırı argümanı olarak almaktadır. "client" program da hem "server" soketin yol ifadesini hem de "client" soketin yol ifadesini komut satırı argümanı olarak almaktadır. Programları, ./uds-dg-server serversock ./uds-dg-client serversock clientsock komutlarıyla çalıştırabiliriz. /* uds-dg-server.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 char *revstr(char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct sockaddr_un sun_server, sun_client; socklen_t sun_len; ssize_t result; char buf[BUFFER_SIZE + 1]; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) exit_sys("socket"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); if (remove(argv[1]) == -1 && errno != ENOENT) exit_sys("remove"); if (bind(sock, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("bind"); printf("Waiting for client data...\n"); for (;;) { sun_len = sizeof(sun_client); if ((result = recvfrom(sock, buf, BUFFER_SIZE, 0, (struct sockaddr *)&sun_client, &sun_len)) == -1) exit_sys("recvfrom"); buf[result] = '\0'; printf("%ld bytes received from \"%s\": %s\n", (long)result, sun_client.sun_path, buf); revstr(buf); if (sendto(sock, buf, strlen(buf), 0, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("sendto"); } close(sock); return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* uds-dg-client.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct sockaddr_un sun_client, sun_server, sun_response; socklen_t sun_len; char buf[BUFFER_SIZE]; char *str; ssize_t result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) exit_sys("socket"); memset(&sun_client, 0, sizeof(sun_client)); sun_client.sun_family = AF_UNIX; strcpy(sun_client.sun_path, argv[2]); if (remove(argv[2]) == -1 && errno != ENOENT) exit_sys("remove"); if (bind(sock, (struct sockaddr *)&sun_client, sizeof(sun_client)) == -1) exit_sys("bind"); memset(&sun_server, 0, sizeof(sun_server)); sun_server.sun_family = AF_UNIX; strcpy(sun_server.sun_path, argv[1]); for (;;) { printf("Yazı giriniz:"); fgets(buf, BUFFER_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (sendto(sock, buf, strlen(buf), 0, (struct sockaddr *)&sun_server, sizeof(sun_server)) == -1) exit_sys("sendto"); sun_len = sizeof(sun_server); if ((result = recvfrom(sock, buf, BUFFER_SIZE, 0, (struct sockaddr *)&sun_response, &sun_len)) == -1) exit_sys("recvfrom"); buf[result] = '\0'; printf("%ld bytes received from \"%s\": %s\n", (long)result, sun_response.sun_path, buf); } close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 6, "UNIX Domain Socket", "isimsiz boru haberleşmesine" benzer biçimde de kullanılabilmektedir. Anımsanacağı gibi isimsiz borularla yalnızca üst ve alt proseslerer arasında haberleşme yapılabiliyordu (yine anımsayacağınız gibi "pipe" fonksiyonu bize iki betimleyici veriyordu. Biz de "fork" işlemi ile bu betimleyicileri alt prosese geçiriyorduk). İşte isimsiz borularla yapılan şeylerin benzeri soketlerle de yapılabilmektedir (isimsiz soketlere İngilizce "unbound sockets" de denilmektedir). "İsimsiz (unbound)" soket yaratımı "socketpair" isimli fonksiyonla yapılmaktadır. Fonksiyonun prototipi şöyledir: #include int socketpair(int domain, int type, int protocol, int sv[2]); Fonksiyonun birinci parametresi protokol ailesinin ismini alır. Her ne kadar fonksiyon genel olsa da pek çok işletim sistemi bu fonksiyonu yalnızca "UNIX Domain Socket" için gerçekleştirmektedir (gerçekten de üst ve alt prosesler arasında "UNIX Domain Socket" varken, örneğin, TCP/IP soketleriyle haberleşmenin zarardan başka bir faydası olmayacaktır.) Linux sistemleri isimsiz soket olarak yalnızca "UNIX Domain Socket" desteklemektedir. Dolayısıyla bu birinci parametre Linux sistemlerinde "AF_UNIX" biçiminde geçilmelidir. Fonksiyonun ikinci parametresi kullanılacak soketin türünü belirtir. Bu parametre yine "SOCK_STREAM" ya da "SOCK_DGRAM" biçiminde girilmelidir. Üçüncü parametre kullanılacak "Transport Layer" belirtmektedir. Bu parametre "0" olarak geçilebilir. Son parametre bir çift soket betimleyicisinin yerleştirileceği iki elemanlı "int" türden dizinin başlangıç adresini almaktadır. Fonksiyon başarı durumunda "0" değerine, başarısızlık durumunda "-1" değerine geri dönmektedir. Fonksiyonun kullanımı, //... int socks[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks) == -1) exit_sys("socketpair"); //... biçimindedir, tipik olarak. "socketpair" fonksiyonu "SOCK_STREAM" soketler için zaten bağlantı sağlanmış iki soketi bize vermektedir. Yani bu fonksiyon çağrıldıktan sonra "listen", "accept" ve "connect" gibi fonksiyonların çağrılması gereksizdir. Dolayısıyla tipik haberleşme şöyle gerçekleştirilmektedir: -> "socketpair" fonksiyonu ile soket çifti yaratılır. -> Soket çifçi yaratıldıktan sonra "fork" ile alt proses yaratılır. -> İki taraf da kullanmayacakları soketleri kapatırlar. Hangi prosesin, "socketpair" fonksiyonunun son parametresine yerleştirilen hangi soket betimleyicisini kullanacağının bir önemi yoktur. -> Haberleşme soket fonksiyonlarıyla gerçekleştirilir. Pekiyi isimsiz borularla, "socketpair" fonksiyonuyla oluşturulan isimsiz "UNIX Domain Socket" arasında ne fark vardır? Aslında bu iki kullanım benzer etkilere sahiptir. Ancak en önemli farklılık "UNIX Domain Socket" "çift yönlü (full duplex)" bir haberleşme sağlamasıdır. Normalde isimsiz mesaj kuyrukları olmadığına dikkat ediniz. Halbuki isimsiz "UNIX Domain Socket"sanki isimsiz mesaj kuyrukları gibi de kullanılabilmektedir. Aşağıdaki programda tıpkı isimsiz boru haberleşmesinde olduğu gibi üst ve alt prosesler birbirleri arasında isimsiz "UNIX Domain Socket" yoluyla haberleşmektedir. Buradaki soketlerin çift yönlü haberleşmeye olanak verdiğini anımsayınız. /* uds-socketpair.c */ #include #include #include #include #include #include #define BUFFER_SIZE 1024 char *revstr(char *str); void exit_sys(const char *msg); int main(void) { int socks[2]; char buf[BUFFER_SIZE + 1]; char *str; ssize_t result; pid_t pid; if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks) == -1) exit_sys("socketpair"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent */ close(socks[1]); for (;;) { if ((result = recv(socks[0], buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; revstr(buf); if (send(socks[0], buf, strlen(buf), 0) == -1) exit_sys("send"); } if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(socks[0]); exit(EXIT_SUCCESS); } else { /* child */ close(socks[0]); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if ((send(socks[1], buf, strlen(buf), 0)) == -1) exit_sys("send"); if (!strcmp(buf, "quit")) break; if ((result = recv(socks[1], buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; printf("%ld bytes received: %s\n", (long)result, buf); } close(socks[1]); exit(EXIT_SUCCESS); } return 0; } char *revstr(char *str) { size_t i, k; char temp; for (i = 0; str[i] != '\0'; ++i) ; for (--i, k = 0; k < i; ++k, --i) { temp = str[k]; str[k] = str[i]; str[i] = temp; } return str; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > "out of band data" : "OOB Verisi (Out-Of-Band Data)" bazı "stream" protokollerinde olan bir özelliktir. Örneğin "TCP" protokolü "OOB" verisini 1 "byte" olarak desteklemektedir. "OOB" verisine "TCP" de "Acil (Urgent)" veri de denilmektedir. Bunun amacı "OOB" verisinin normal "stream" sırasında değil, daha önce gönderilenlerin (eğer hedef "host" ta henüz onlar okunmamışsa) önünde ele alınabilmesidir. Yani biz "TCP" de birtakım verileri gönderdikten sonra "OOB" verisini gönderirsek, bu veri önce göndermiş olduklarımızdan daha önde işleme sokulabilir. Böylece "OOB" verisi uygulamalarda önce gönderilen birtakım bilgilerin iptal edilmesi gibi gerekçelerle kullanılabilmektedir. Diğer yandan "OOB" verisini gönderebilmek için, -> "send" fonksiyonunun "flags" parametresine "MSG_OOB" bayrağını girmek gerekir. Tabii "TCP" yalnızca 1 "byte" uzunluğunda "OOB" verisinin gönderilmesine izin vermektedir. Bu durumda eğer "send" ile birden fazla bayt "MSG_OOB" bayrağı ile gönderilmek istenirse gönderilenlerin yalnızca son baytı "OOB" olarak gönderilir. Son bayttan önceki tüm baytlar normal veri olarak gönderilmektedir. adımlarını takip etmeliyiz. "OOB" verisini almak için de, -> Normal olarak "OOB" verisi, "recv" fonksiyonunda "MSG_OOB" bayrağı ile alınmaktadır. Ancak bu bayrak kullanılarak "recv" çağrıldığında, eğer bir "OOB" verisi sırada yoksa, "recv" başarısız olmaktadır. "recv" fonksiyonunun "MSG_OOB" bayraklı çağrısında başarılı olabilmesi için o anda bir "OOB" verisinin gelmiş olması gerekir. Pekiyi OOB verisinin geldiğini nasıl anlarız? İşte tipik yöntem "SIGURG" sinyalinin kullanılmasıdır. Çünkü sokete bir "OOB" verisi geldiğinde işletim sistemi, "SIGURG" sinyali oluşturabilmektedir. Çünkü varsayılan durumlarda, "OOB" verisi geldiğinde, sinyal gönderilmemektedir. Bunu kesinleştirmek için şu adımları takip etmeliyiz; -> soket üzerinde "fcntl" fonksiyonu ile "F_SETOWN" komut kodunu kullanarak "set" işlemi yapmak gerekir. "fcntl" fonksiyonunun son parametresi bu durumda sinyalin gönderileceği prosesin "ID" değeri olarak girilmelidir. Eğer bu parametre negatif bir "Process Group ID" olarak girilirse, bu durumda işletim sistemi, o proses grubunun bütün üyelerine bu sinyali gönderecektir. Tabii tipik olarak sinyalin soket betimleyicisine sahip olan prosese gönderilmesi istenir. Bu işlem ise if (fcntl(sock_client, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); çağrısı ile gerçekleştirilebilir. Diğer yandan "SIGURG" sinyalinin varsayılan davranışı da "IGNORE" biçimindedir. Yani ilgili proses eğer bu sinyali "set" etmemişse sanki sinyal oluşmamış gibi bir davranış gözükür. Dolayısıyla "SIGURG" sinyalini de "set" etmeliyiz. Aşağıdaki bu konuya ilişkin bir örnek verilmiştir. * Örnek 1, Aşağıdaki "server" programda bir "OOB" verisi geldiğinde "SIGURG" sinyali oluşturulmaktadır. Bu sinyalin içerisinde "recv" fonksiyonu "MSG_OOB" bayrağı ile çağrılmıştır. "OOB" verisinin okunması için "MSG_OOB" bayrağı gerekir. Ancak "OOB" verisinin olmadığı bir durumda bu bayrak kullanılırsa "recv" başarısız olmaktadır. O halde "SIGURG" sinyali geldiğinde "recv" fonksiyonu "MSG_OOB" bayrağı ile çağrılmalıdır. Bu durumda "TCP" de her zaman yalnızca 1 bayt okunabilmektedir. Ayrıca "server" programda "SIGURG" sinyali "set" edilirken "sigaction" yapısının "flags" parametresinin "SA_RESTART" biçiminde geçildiğine dikkat ediniz. Bu "recv" üzerinde beklerken oluşabilecek "SIGURG" sinyalinden sonra "recv" in otomatik yeniden başlatılması için kullanılmıştır. Yine buradaki "server" program port numarasını, "client" program ise "server" adresini ve port numarasını komut satırı argümanı olarak almaktadır. /* oobserver.c */ #include #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void sigurg_handler(int sno); void exit_sys(const char *msg); int sock_client; int main(int argc, char *argv[]) { int sock; struct sockaddr_in sinaddr, sinaddr_client; socklen_t sinaddr_len; char ntopbuf[INET_ADDRSTRLEN]; in_port_t port; ssize_t result; char buf[BUFFER_SIZE + 1]; struct sigaction sa; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } sa.sa_handler = sigurg_handler; sa.sa_flags = SA_RESTART; sigemptyset(&sa.sa_mask); if (sigaction(SIGURG, &sa, NULL) == -1) exit_sys("sigaction"); port = (in_port_t)strtoul(argv[1], NULL, 10); if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sinaddr.sin_family = AF_INET; sinaddr.sin_port = htons(port); sinaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock, (struct sockaddr *)&sinaddr, sizeof(sinaddr)) == -1) exit_sys("bind"); if (listen(sock, 8) == -1) exit_sys("listen"); printf("Waiting for connection...\n"); sinaddr_len = sizeof(sinaddr_client); if ((sock_client = accept(sock, (struct sockaddr *)&sinaddr_client, &sinaddr_len)) == -1) exit_sys("accept"); if (fcntl(sock_client, F_SETOWN, getpid()) == -1) exit_sys("fcntl"); printf("Connected: %s : %u\n", inet_ntop(AF_INET, &sinaddr_client, ntopbuf, INET_ADDRSTRLEN), (unsigned)ntohs(sinaddr_client.sin_port)); for (;;) { if ((result = recv(sock_client, buf, BUFFER_SIZE, 0)) == -1) exit_sys("recv"); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%ld bytes received from %s (%u): %s\n", (long)result, ntopbuf, (unsigned)ntohs(sinaddr_client.sin_port), buf); } shutdown(sock_client, SHUT_RDWR); close(sock_client); close(sock); return 0; } void sigurg_handler(int sno) { char oob; if (recv(sock_client, &oob, 1, MSG_OOB) == -1) exit_sys("recv"); printf("OOB Data received: %c\n", oob); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* oobclient.c */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int sock; struct addrinfo *ai, *ri; struct addrinfo hints = {0}; char buf[BUFFER_SIZE]; char *str; int result; if (argc != 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; if ((result = getaddrinfo(argv[1], argv[2], &hints, &ai)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result)); exit(EXIT_FAILURE); } for (ri = ai; ri != NULL; ri = ri->ai_next) if (connect(sock, ri->ai_addr, ri->ai_addrlen) != -1) break; if (ri == NULL) exit_sys("connect"); freeaddrinfo(ai); printf("Connected...\n"); for (;;) { printf("Yazı giriniz:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((send(sock, buf, strlen(buf), buf[0] == 'u' ? MSG_OOB : 0)) == -1) exit_sys("send"); } shutdown(sock, SHUT_RDWR); close(sock); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> "TCP/IP" için yazmış olduğumuz "Server-Client" program aslında UNIX ve türevi sistemler içindir. Windows sistemlerindeki soket kütüphanesine "Winsock" denilmektedir. Şu anda bu kütüphanenin 2'inci versiyonu kullanılmaktadır. Winsock API fonksiyonları "UNIX/Linux uyumlu" fonksiyonlar ve Windows'a özgü fonksiyonlar olmak üzere iki biçimde kullanılabilmektedir. Ancak Winsock'un UNIX/Linux uyumlu fonksiyonlarında da birtakım değişiklikler söz konusudur. Bir UNIX/Linux ortamında yazılmış soket uygulamasının Windows sistemlerine aktarılması için şu düzeltmelerin yapılması gerekir: -> POSIX'in soket sistemine ilişkin tüm başlık dosyaları kaldırılır. Onun yerine "winsock2" başlık dosyası "include" edilir. -> "xxx_t" biçimindeki tür eş isimleri silinir ve onların yerine ki bu konuda ilgili dokümanlara da bakabilirsiniz, "int", "short", "unsigned int", "unsigned short" türleri kullanılır. Örneğin "ssize_t" türü ve "socklen_t" türleri yerine "int" türleri kullanılmalıdır. -> Windows'ta soket sisteminin başlatılması için "WSAStartup" fonksiyonu işin başında çağrılır ve işin sonunda da bu işlem "WSACleanup" fonksiyonuyla geri alınır. Bu fonksiyonları şöyle kullanbilirsiniz: WSADATA wsadata; ... if ((result = WSAStartup(MAKEWORD(2, 2), &wsadata)) != 0) exit_sys("WSAStartup", EXIT_FAILURE, result); ... WSACleanup(); -> Windows'ta dosya betimleyicisi kavramı yoktur. Onun yerine "handle" kavramı vardır. Dolayısıyla soket türü de "int" değil, "SOCKET" isimli bir tür eş ismidir. -> "shutdown" fonksiyonunun ikinci parametresi "SD_RECEIVE", "SD_SEND" ve "SD_BOTH" biçimindedir. -> "close" fonksiyonu yerine "closesocket" fonksiyonu ile soket kapatılır. -> Windows'ta soket fonksiyonları başarısızlık durumunda -1 değerine geri dönmezler. "socket" fonksiyonu başarısızlık durumunda "INVALID_SOCKET" değerine, diğerleri ise "SOCKET_ERROR" değerine geri dönmektedir. -> "Visual Studio IDE" sinde varsayılan durumda "deprecated" durumlar "error" e yükseltilmiştir. Bunlar için bir makro "define" edilebilmektedir. Ancak proje ayarlarından "sdl check", "disable" da edilebilir. Benzer biçimde proje ayarlarından "Unicode" değeri "not set" yapılmalıdır. -> Projenin "linker" ayarlarından "Input/Additional Dependencies > Edit" alanına "Winsock" kütüphanesi olan "Ws2_32.lib" kütüphanesi eklenir. -> Windows'ta son soket API fonksiyonlarının başarısızlık nedenleri "WSAGetLastError" fonksiyonuyla elde edilmektedir. Yani Windows sistemlerinde "errno" değişkeni set edilmemektedir. Belli bir hata kodunun yazıya dönüştürülmesi de biraz ayrıntılıdır. Bunun için aşağıdaki fonksiyonu kullanabilirsiniz: void ExitSys(LPCSTR lpszMsg, DWORD dwLastError) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Aşağıda bu yapılanları gösteren bir örnek verilmiştir. * Örnek 1, /* client.c */ #include #include #include #include #define SERVER_NAME "192.168.153.131" #define SERVER_PORT 55555 #define BUFFER_SIZE 4096 void ExitSys(LPCSTR lpszMsg, DWORD dwLastError); int main(void) { WSADATA wsadata; SOCKET client_sock; struct sockaddr_in sin_server; struct hostent *hent; char buf[BUFFER_SIZE]; char *str; int result; if ((result = WSAStartup(MAKEWORD(2, 2), &wsadata)) != 0) ExitSys("WSAStartup", result); if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) ExitSys("socket", WSAGetLastError()); /* { struct sockaddr_in sin_client; sin_client.sin_family = AF_INET; sin_client.sin_port = htons(50000); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); } */ sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); if ((sin_server.sin_addr.s_addr = inet_addr(SERVER_NAME)) == SOCKET_ERROR) { if ((hent = gethostbyname(SERVER_NAME)) == NULL) ExitSys("gethostbyname", WSAGetLastError()); memcpy(&sin_server.sin_addr.s_addr, hent->h_addr_list[0], hent->h_length); } if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == SOCKET_ERROR) ExitSys("connect", WSAGetLastError()); printf("connected server...\n"); for (;;) { printf("csd>"); fflush(stdout); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(client_sock, buf, (int)strlen(buf), 0) == SOCKET_ERROR) ExitSys("send", WSAGetLastError()); if (!strcmp(buf, "quit")) break; } shutdown(client_sock, SD_BOTH); closesocket(client_sock); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastError) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* server.c */ #include #include #include #include #include #define SERVER_PORT 55555 #define BUFFER_SIZE 4096 void ExitSys(LPCSTR lpszMsg, DWORD dwLastError); int main(void) { WSADATA wsadata; SOCKET server_sock, client_sock; struct sockaddr_in sin_server, sin_client; int sin_len; char buf[BUFFER_SIZE + 1]; int result; if ((result = WSAStartup(MAKEWORD(2, 2), &wsadata)) != 0) ExitSys("WSAStartup", result); if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) ExitSys("socket", WSAGetLastError()); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(SERVER_PORT); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); if (listen(server_sock, 8) == SOCKET_ERROR) ExitSys("listen", WSAGetLastError()); printf("waiting for connection...\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == SOCKET_ERROR) ExitSys("accept", WSAGetLastError()); printf("connected client ===> %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); for (;;) { if ((result = recv(client_sock, buf, BUFFER_SIZE, 0)) == SOCKET_ERROR) ExitSys("recv", WSAGetLastError()); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%jd byte(s) received: \"%s\"\n", (intmax_t)result, buf); } shutdown(client_sock, SD_BOTH); closesocket(client_sock); closesocket(server_sock); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastError) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >> Sanal Makineler Hk. : Bu kurstaki denemeler ekseriyetle sanal makinalar kullanılarak gerçekleştirildi. Bu makinalardan önemli olanları "VMware Player" ve "VirtualBox" isimli programlardır. Bu makinalardan, -> "VMware Player" : Bu programı çalıştırdığımızda, sanal makinanın "MAC Address" bilgisini öğrenmek için, "Virtual Machine Settings/Network Adapter/Advanced" ayarından sanal "ethernet" kartının "MAC" adresini görebiliriz. Buradan "MAC" adresini, "00-0c-29-76-3b-e8" şeklinde görebiliriz. "IP" adresini görmek için de ilgili komut satırı programından, "arp -a" komutunu çalıştırmalıyız. Bu komutun çıktısı aşağıdaki gibidir: Interface: 192.168.153.1 --- 0x10 Internet Address Physical Address Type 192.168.153.128 00-0c-29-76-3b-e8 dynamic 192.168.153.131 00-0c-29-76-3b-fc dynamic 192.168.153.255 ff-ff-ff-ff-ff-ff static 224.0.0.22 01-00-5e-00-00-16 static 224.0.0.251 01-00-5e-00-00-fb static 224.0.0.252 01-00-5e-00-00-fc static 239.255.255.250 01-00-5e-7f-ff-fa static 255.255.255.255 ff-ff-ff-ff-ff-ff static Buradan "00-0c-29-76-3b-e8" numaralı "MAC" adresine karşılık gelen "IP" adresinin "192.168.153.128" olduğunu görüyoruz. >> UNIX/Linux, macOS ve Windows sistemlerinde yalnızca "CR ('\r')" karakteri imleci bulunulan satırın başına geçirmektedir. Dolayısıyla bir yazının sonunda "CR/LF" çifti varsa yazının ekrana bastırılmasında bir sorun oluşmayacaktır. Çünkü önce "CR" karakteri imleci bulunulan satırın başına geçirecek sonra "LF" karakteri aşağı satırın başına geçirecektir. Böylece yazının sonunda "LF" karakterinin bulunmasıyla "CR/LF" karakterlerinin bulunması arasında bir fark oluşmayacaktır. >> "Wireshark" Programı: Yerel ağımızda aslında "router" tarafından gönderilip alınan paketlerin hepsi yerel ağdaki tüm bilgisayarlara ulaşmaktadır. "Ethernet" ve "Wireless" kartları yalnızca kendilerini ilgilendiren paketleri alıp işletim sistemini haberdar edebilmektedir. Ancak bu kartlar için yazılmış özel programlar sayesinde bilgisayarımıza ulaşan tüm paketler incelenebilmektedir. Bu tür yardımcı programlara ise "network sniffer" da denilmektedir. En yaygın kullanılan "network sniffer" program "wireshark" isimli "open-source" programdır. Bu programın eskiden ismi "Ethereal" biçimindeydi. Aslında "wireshark" programı "libpcap" isimli "open-source" kütüphane kullanılarak yazılmıştır. Yani asıl işlevsellik bu kütüphanededir. "Wireshark" adeta "libpcap" kütüphanesinin bir "önyüzü (frontend)" gibidir. Bu kütüphanenin Windows versiyonuna "npcap" denilmektedir. Linux Debian türevi sistemlerde kütüphane aşağıdaki gibi indirilebilir: sudo apt-get install libpcap-dev Benzer biçimde Linux'ta "wireshark" programını da "GUI" arayüzü yazılım yöneticisinden yüklenebileceği gibi komut satırından Debian türevi sistemlerde aşağıdaki gibi yüklenebilir: sudo apt-get install wireshark Wireshark programının kullanımına ilişkin pek çok "tutorial" bulunmaktadır. Kursumuzun "E-Books" klasöründe de birkaç kitap bulunmaktadır. /*================================================================================================================================*/ (120_16_02_2024) & (121_18_02_2024) & (122_23_02_2024) & (123_25_02_2024) & (124_01_03_2024) > UNIX/Linux Sistemlerinde Kütüphane İşlemleri: Kütüphane terimi "hazır kodların bulunduğu topluluklar" için kullanılan bir terimdir. Ancak aşağı seviyeli dünyada kütüphane kavramı daha farklı bir biçimde kullanılmaktadır. Aşağı seviyeli dünyada, "içerisinde derlenmiş bir biçimde fonksiyonların bulunduğu dosyalara kütüphane (library)" denilmektedir. Aslında kütüphaneler yalnızca fonksiyon değil, global nesneler de içerebilmektedir. Kütüphaneler "statik" ve "dinamik" olmak üzere ikiye ayrılmaktadır. Statik kütüphane dosyalarının uzantıları UNIX/Linux sistemlerinde ".a (archive)" biçiminde, Windows sistemlerinde ".lib (library)" biçimindedir. Dinamik kütüphane dosyalarının uzantıları ise UNIX/Linux sistemlerinde ".so (shared object), Windows sistemlerinde ".dll (dynamic link library)" biçimindedir. UNIX/Linux dünyasında kütüphane dosyaları geleneksel olarak başında "lib" öneki olacak biçimde isimlendirilmektedir. * Örnek 1, "x" isimli bir statik kütüphane dosyası UNIX/Linux sistemlerinde genellikle "libx.a" biçiminde, "x" isimli bir dinamik kütüphane dosyası ise UNIX/Linux sistemlerinde genellikle "libx.so" biçiminde isimlendirilmektedir. Şimdi de statik ve dinamik kütüphaneleri kendi içerisinde irdeleyelim: >> "Static Kütüphaneler" : Statik kütüphaneler aslında "object modules (yani .o dosyalarını)" tutan birer kap gibidir. Yani statik kütüphaneler, uzantısı ".o" olan dosyalardan, oluşmaktadır. Statik kütüphanelere "link" aşamasında "linker" tarafından bakılır. Bir program, statik kütüphane dosyasından bir çağırma yaptıysa (ya da o kütüphaneden bir global değişkeni kullandıysa), "linker" program o statik kütüphane içerisinde ilgili fonksiyonun bulunduğu "object" modülü link aşamasında statik kütüphane dosyasından çekerek çalıştırılabilir dosyaya yazar. (Yani statik kütüphaneden bir tek fonksiyon çağırsak bile aslında o fonksiyonun bulunduğu object modülün tamamı çalıştırılabilen dosyaya yazılmaktadır.) Statik kütüphaneleri kullanan programlar artık o statik kütüphaneler olmadan çalıştırılabilirler. Statik kütüphane kullanımının şu dezavantajları vardır: -> Kütüphaneyi kullanan farklı programlar aynı fonksiyonun (onun bulunduğu "object" modülün) bir kopyasını çalıştırılabilir dosya içerisinde bulundururlar. Yani örneğin "printf" fonksiyonu statik kütüphanede ise her "printf" kullanan C programı aslında "printf" fonksiyonunun bir kopyasını da barındırıyor durumda olur. Bu da programların fazla yer kaplamasına yol açacaktır. -> Aynı statik kütüphaneyi kullanan programlar belleğe yüklenirken, işletim sistemi aynı kütüphane kodlarınını yeniden fiziksel belleğe yükleyecektir. İşletim sistemi bu kodların ortak olarak kullanıldığını anlayamamaktadır. -> Statik kütüphanede bir değişiklik yapıldığında onu kullanan programların yeniden link edilmesi gerekir. Statik kütüphane kullanımının şu avantajları vardır: -> Kolay konuşlandırılabilirler. Statik kütüphane kullanan bir programın yüklenmesi için başka dosyalara gereksinim duyulmamaktadır. -> Statik kütüphanelerin kullanımları kolaydır, statik kütüphane kullanan programlar için daha kolay "build" ya da "make" işlemi yapılabilmektedir. -> Statik kütüphane kullanan programların yüklenmesi dinamik kütüphane kullanan programların yüklenmesinden çoğu kez daha hızlı yapılmaktadır. Ancak bu durum çeşitli koşullara göre tam ters bir hale de gelebilmektedir. UNIX/Linux sistemlerinde statik kütüphane dosyaları üzerinde işlemler "ar" isimli utility program yoluyla yapılmaktadır. "ar" programına önce bir seçenek, sonra statik kütüphane dosyasının ismi, sonra da bir ya da birden fazla "object" modül ismi komut satırı argümanı olarak verilir. Örneğin: "ar r libmyutil.a x.o y.o" Buradaki "r" seçeneğini belirtmektedir. "ar" eski bir komut olduğu için burada seçenekler '-' ile başlatılarak verilmemektedir. Komuttaki "libmyutil.a" işlemden etkilenecek statik kütüphane dosyasını "x.o" ve "y.o" argümanları ise "object" modülleri belirtmektedir. Tipik "ar" seçenekleri ve yaptıkları işler şunlardır: -> "r (replace)" seçeneği (yanında "-" olmadığına dikkat ediniz) ilgili "object" modüllerin kütüphaneye yerleştirilmesini sağlar. Eğer kütüphane dosyası yoksa komut aynı zamanda onu yaratmaktadır. Örneğin: "ar r libmyutil.a x.o y.o" Burada "libmyutil.a" statik kütüphane dosyasına "x.o" ve "y.o" isimli "object" modülleri yerleştirilmiştir. Eğer "libmyutil.a" dosyası yoksa aynı zamanda bu dosya yaratılacaktır. -> "t" seçeneği kütüphane içerisindeki "object" modüllerin listesini almakta kullanılır. Örneğin: "ar t libsample.a" "d (delete)" seçeneği kütüphaneden bir "object" modülü silmekte kullanılır. Örneğin: "ar d libmyutil.a x.o" "x (extract)" seçeneği kütüphane içerisindeki "object" modülü bir dosya biçiminde diske save etmekte kullanılır. Ancak bu "object" modül kütüphane dosyasından silinmeyecektir. Örneğin: "ar x libmyutil.a x.o" -> "m (modify)" seçeneği de bir "object" modülün yeni versiyonunu eski versiyonla değiştirmekte kullanılır. O halde "x.c" ve "y.c" dosyalarının içerisindeki fonksiyonları statik kütüphane dosyasına eklemek için sırasıyla şunlar yapılmalıdır: "gcc -c x.c" "gcc -c y.c" "ar r libmyutil.a x.o y.o" Statik kütüphane kullanan programları derlerken statik kütüphane dosyaları komut satırında belirtilebilir. Bu durumda "gcc" ve "clang" derleyicileri o dosyayı link işleminde kullanmaktadır. Örneğin: "gcc -o app app.c libmyutil.a" Burada "libmyutil.a" dosyasına C derleyicisi bakmamaktadır. "gcc" aslında bu dosyayı "linker" a iletmektedir. Biz bu işlemi iki adımda da yapabilirdik: "gcc -c app.c" "gcc -o app app.o libmyutil.a" Her ne kadar GNU'nun "linker" programı aslında "ld" isimli programsa da genellikle programcılar bu "ld" "linker" ını doğrudan değil yukarıdaki gibi "gcc" yoluyla kullanırlar. Çünkü "ld" "linker" ı kullanılırken "libc" kütüphanesi gibi "start-up object" modüller gibi modüllerin de programcı tarafından "ld linker" ına verilmesi gerekmektedir. Bu da oldukça sıkıcı bir işlemdir. Halbuki biz "ld linker" ını "gcc" yoluyla çalıştırdığımızda "ld linker" ına bu dosyalar zaten "default" kütüphaneler ve "object" modüller "gcc" tarafından verilmektedir. Komut satırında kütüphane dosyalarının komut satırı argümanlarının sonunda belirtilmesi uygundur. Çünkü "gcc" programı kütüphane dosyalarının solundaki dosyalar "link" edilirken ilgili kütüphane dosyasını bu işleme dahil ederler. Örneğin: "gcc -o app app1.o libmyutil.a app2.o" Böylesi bir kullanımda "libmyutil.a" komut satırı argümanının solunda yalnızca "app1.o" dosyası vardır. Dolayısıyla yalnızca bu modül için bu kütüphaneye bakılacaktır, "app2.o" bu kütüphaneye bakılmayacaktır. Şüphesiz statik kütüphane kullanmak yerine aslında "object" modülleri de doğrudan "link" işlemine sokabiliriz. Örneğin: "gcc -o sample sample.c x.o y.o" Ancak çok sayıda "object" modül söz konusu olduğunda bu işlemin zorlaşacağına dikkat ediniz. Yani, "object modüller dosyalara benzetilirse statik kütüphane dosyaları dizinler gibi" düşünülebilir. Derleme işlemi sırasında kütüphane dosyası "-l" biçiminde de belirtilebilir. Bu durumda arama sırasında "lib" öneki ve ".a" uzantısı aramaya dahil edilmektedir. Yani örneğin: "gcc -o sample sample.c -lmyutil" işleminde aslında "libmyutil.a" (ya da "libmyutil.so") dosyaları aranmaktadır. Arama işlemi sırasıyla bazı dizinlerde yapılmaktadır. Örneğin, "/lib" dizini, "/usr/lib" dizini, "/usr/local/lib" vb. dizinlere bakılmaktadır. Ancak "bulunulan dizine" bakılmamaktadır. İşte "-l" seçeneği ile "belli bir dizine de bakılması isteniyorsa" "-L" seçeneği ile ilgili dizin belirtilebilir. Örneğin: "gcc -o sample sample.c -lmyutil -L." Buradaki "." çalışma dizinini temsil etmektedir. Artık "libmyutil.a" kütüphanesi için bulunulan dizine de ("current working directory") bakılacaktır. Birden fazla dizin için "-L" seçeneğinin yinelenmesi gerekmektedir. Örneğin: "gcc -o sample sample.c -lmyutil -L. -L/home/csd" Geleneksel olarak "-l" ve "-L" seçeneklerinden sonra boşluk bırakılmamaktadır. Ancak boşluk bırakılmasında bir sakınca yoktur. Bir statik kütüphane başka bir statik kütüphaneye bağımlı olabilir. Örneğin biz, "liby.a" kütüphanesindeki kodda "libx.a" kütüphanesindeki fonksiyonları kullanmış olabiliriz. Bu durumda "liby.a" kütüphanesini kullanan program "libx.a" kütüphanesini de komut satırında belirtmek zorundadır. Örneğin: "gcc -o sample sample.c libx.a liby.a" >> "Dynamic Libraries" : Dinamik kütüphane dosyalarının UNIX/Linux sistemlerinde uzantıları ".so" ("shared object" ten kısaltma), Windows sistemlerinde ise ".dll (Dynamic Link Library)" biçimindedir. Bir dinamik kütüphaneden bir fonksiyon çağrıldığında "linker", statik kütüphanede olduğu gibi gidip fonksiyonun kodunu (fonksiyonun bulunduğu "object" modülü) çalıştırılabilen dosyaya yazmaz. Bunun yerine çalıştırılabilen dosyaya çağrılan fonksiyonun hangi dinamik kütüphanede olduğu bilgisini yazar. Çalıştırılabilen dosyayı yükleyen işletim sistemi o dosyanın çalışması için gerekli olan dinamik kütüphaneleri çalıştırılabilen dosyayla birlikte bütünsel olarak sanal bellek alanına yüklemektedir. Böylece birtakım ayarlamalar yapıldıktan sonra artık çağrılan fonksiyon için gerçekten o anda sanal belleğe yüklü olan dinamik kütüphane kodlarına gidilmektedir. * Örnek 1, Biz "app" programımızda "libmyutil.so" dinamik kütüphanesinden "foo" isimli fonksiyonu çağırmış olalım. Bu "foo" fonksiyonunun kodları dinamik kütüphaneden alınıp "app" dosyasına yazılmayacaktır. Bu "app" dosyası çalıştırıldığında işletim sistemi bu "app" dosyası ile birlikte "libmyutil.so" dosyasını da sanal belleğe yükleyecektir. Programın akışı "foo" çağrısına geldiğinde akış "libmyutil.so" dosyası içerisindeki "foo" fonksiyonunun kodlarına aktarılacaktır. Dinamik kütüphane dosyalarının bir kısmının değil, hepsinin prosesin adres alanına yüklendiğine dikkat ediniz. Tabii işletim sisteminin sanal bellek mekanizması aslında yalnızca bazı sayfaları fiziksel belleğe yükleyebilecektir. Dinamik kütüphane kullanımının avantajları şunlardır: -> Çalıştırılabilen dosyalar fonksiyon kodlarını içermezler. Dolayısıyla önemli bir disk hacmi kazanılmış olur. Oysa statik kütüphanelerde statik kütüphanelerden çağrılan fonksiyonlar çalıştırılabilen dosyalara yazılmaktadır. -> Dinamik kütüphaneler birden fazla program proses tarafından fiziksel belleğe tekrar tekrar yüklenmeden kullanılabilmektedir. Yani işletim sistemi arka planda aslında aynı dinamik kütüphaneyi kullanan programlarda bu kütüphaneyi tekrar tekrar fiziksel belleğe yüklememektedir. Bu da statik kütüphanelere göre önemli bir bellek kullanım avantaj oluşturmaktadır. Bu durumda eğer dinamik kütüphanenin ilgili kısmı daha önce fiziksel yüklenmişse bu durum dinamik kütüphane kullanan programın daha hızlı yüklemesine de yol açabilmektedir. * Örnek 1, "Process1" ve "Process2" biçiminde iki programın çalıştığını düşünelim. Bunlar aynı dinamik kütüphaneyi kullanıyor olsun. İşletim sistemi bu dinamik kütüphaneyi bu proseslerin sanal bellek alanlarının farklı yerlerine yükleyebilir. Ancak aslında işletim sistemi sayfa tablolarını kullanarak mümkün olduğunca aslında bu iki dinamik kütüphaneyi aynı fiziksel sayfaya eşlemeye çalışacaktır. Tabii bu durumda proseslerden biri dinamik kütüphane içerisindeki bir statik nesneyi değiştirdiğinde artık "copy on write" mekanizması devreye girecek ve dinamik kütüphanenin o sayfasının yeni bir kopyası oluşturulacaktır. Aslında bu durum "fork" fonksiyonu ile yeni bir prosesin yaratılması durumuna çok benzemektedir. -> Dinamik kütüphaneleri kullanan programlar bu dinamik kütüphanelerdeki değişikliklerden etkilenmezler. Yani biz dinamik kütüphanenin yeni bir versiyonunu oluşturduğumuzda bunu kullanan programları derlemek ya da "link" etmek zorunda kalmayız. * Örnek 1, Bir dinamik kütüphaneden "foo" fonksiyonunu çağırmış olalım. Bu "foo" fonksiyonunun kodları bizim çalıştırılabilir dosyamızın içerisinde değil de dinamik kütüphanede olduğuna göre dinamik kütüphanedeki "foo" fonksiyonu değiştirildiğinde bizim programımız artık değişmiş olan "foo" fonksiyonunu çağıracaktır. Dinamik kütüphanelerin gerçekleştiriminde ve kullanımında önemli bir sorun vardır. Dinamik kütüphanelerin tam olarak sanal belleğin neresine yükleneceği baştan belli değildir. Halbuki çalıştırılabilen dosyanın sanal belleğin neresine yükleneceği baştan bilinebilmektedir. Yani çalıştırılabilen dosyanın tüm kodları aslında derleyici ve bağlayıcı tarafından zaten "onun sanal bellekte yükleneceği yere göre" oluşturulmaktadır. Fakat dinamik kütüphanelerin birden fazlası prosesin sanal adres alanına yüklenebildiğinden bunlar için yükleme adresinin önceden tespit edilmesi mümkün değildir. İşte bu sorunu giderebilmek için işletim sistemlerinde değişik teknikler kullanılmaktadır. >>> Windows sistemlerinde, "import-export tablosu ve relocation tablosu" denilen yöntem tercih edilmiştir. Bu sistemlerde dinamik kütüphane belli bir adrese yüklendiğinde işletim sistemi o dinamik kütüphanenin "relocation" tablosuna bakarak gerekli makine komutlarını düzeltmektedir. Dinamik kütüphane fonksiyonlarının çağrımı için de "import" tablosu ve "export" tablosu denilen tablolar kullanılmaktadır. >>> UNIX/Linux dünyasında dinamik kütüphanelerin herhangi bir yere yüklenebilmesi için ise "Konumdan Bağımsız Kod (Position Independent Code)" denilen teknik kullanılmaktadır. Konumdan bağımsız kod "nereye yüklenirse yüklenilsin çalışabilen kod" anlamına gelmektedir. Konumdan bağımsız kod oluşturabilmek derleyicinin yapabileceği bir işlemdir. Konumdan bağımsız kod oluşturabilmek için "gcc" ve "clang" derleyicilerinde derleme sırasında "-fPIC" seçeneğinin bulundurulması gerekmektedir. Biz kursumuzda konumdan bağımsız kodun nasıl oluşturulabildiği üzerinde durmayacağız. Bu konuyu anlayabilmek için sembolik makine dilleri ve "ELF" formatı hakkında bilgi sahibi olmak gerekir. Pekiyi Windows sistemlerinin kullandığı "relocation" tekniği ile UNIX/Linux sistemlerinde kullanılan "konumdan bağımsız kod tekniği" arasında performans bakımından ne farklılıklar vardır? İşte bu tekniklerin kendi aralarında bazı avantaj ve dezavantajları bulunmaktadır. -> Windows'taki teknikte "relocation" işlemi bir zaman kaybı oluşturabilmektedir. Ancak bir "relocation" işlemi yapıldığında kodlar daha hızlı çalışma eğilimindedir. -> Konumdan bağımsız kod tekniğinde ise "relocation" işlemine gerek kalmaz. Ancak dinamik kütüphanelerdeki fonksiyonlar çağrılırken göreli biçimde daha fazla zaman kaybedilmektedir. Aynı zamanda bu teknikte kodlar biraz daha fazla yer kaplamaktadır. Linux sistemlerinde aslında dinamik kütüphaneler ismine "dinamik linker (dynamic linker)" denilen bir dinamik kütüphane tarafından yüklenmektedir. Bu dinamik kütüphane "ld.so" ya da "ld-linux.so" ismiyle bulunmaktadır. Programın yüklenmesinin "execve" sistem fonksiyonu tarafından yapıldığını anımsayınız. Bu sistem fonksiyonu ayrıntılı birtakım işlemler yaparak tüm yüklemeyi gerçekleştirmektedir. Bu sürecin ayrıntıları olmakla birlikte kabaca "execve" süreci bu bağlamda şöyle yürütülmektedir: -> "execve" fonksiyonu önce işletim sistemi için gereken çeşitli veri yapılarını oluşturur sonra çalıştırılabilen dosyayı belleğe yükler. -> Sonra da dinamik "linker" kütüphanesini belleğe yükler. -> Bundan sonra akış dinamik "linker" daki koda aktarılır. Dinamik "linker" da çalıştırılabilir dosyada belirtilen dinamik kütüphaneleri yükler. -> Sonra da akışı çalıştırılabilen dosyada belirtilen gerçek başlangıç adresine "(entry point)" aktarır. Dinamik "linker" kodları aslında "user-mod" da "mmap" sistem fonksiyonunu kullanarak diğer dinamik kütüphaneleri yüklemektedir. UNIX/Linux sistemlerinde bir dinamik kütüphane oluşturma işlemi şöyle yapılır: -> Önce dinamik kütüphaneye yerleştirilecek object modüllerin "-fPIC" seçeneği ile "Konumdan Bağımsız Kod (Position Independent Code)" tekniği kullanılarak derlenmesi gerekir. "-fPIC" seçeneğinde "-f" ten sonra boşluk bırakılmamalıdır. -> Link işleminde "çalıştırılabilir (executable)" değil de "dinamik kütüphane" dosyasının oluşturulması için "-shared" seçeneğinin kullanılması gerekir. "-shared" seçeneği kullanılmazsa linker dinamik kütüphane değil, normal çalıştırılabilir dosya oluşturmaya çalışmaktadır. Zaten bu durumda "main" fonksiyonu olmadığı için linker error mesajı verecektir. "gcc -fPIC a.c b.c c.c" "gcc -o libmyutil.so -shared a.o b.o c.o" -> Dinamik kütüphanelere daha sonra dosya eklenip çıkartılamaz. Onların her defasında yeniden bütünsel biçimde oluşturulmaları gerekmektedir. Yukarıdaki işlem aslında tek hamlede de aşağıdaki gibi yapılabilmektedir: "gcc -o libmyutil.so -shared -fPIC a.c b.c c.c" Biz yukarıda dinamik kütüphanelerin nasıl oluşturulduğunu gördük. Pekiyi dinamik kütüphaneler nasıl kullanılmaktadır? Dinamik kütüphane kullanan bir program link edilirken kullanılan dinamik kütüphanenin komut satırında belirtilmesi gerekir. Örneğin: "gcc -o app app.c libmyutil.so" Tabii bu işlem yine -l seçeneği ile de yapılabilirdi: "gcc -o app app.c -lmyutil -L." Standart C fonksiyonlarının ve POSIX fonksiyonlarının bulunduğu "glibc" kütüphanesi link aşamasında otomatik olarak devreye sokulmaktadır. Yani biz standart fonksiyonları ve POSIX fonksiyonları için link aşamasında kütüphane belirtmek zorunda değiliz. Default durumda "gcc" (ve tabii "clang") derleyicileri standart C fonksiyonlarını ve POSIX fonksiyonlarını ("glibc" kütüphanesi) dinamik kütüphaneden alarak kullanır. Ancak programcı isterse "-static" seçeneği ile statik "link" işlemi de yapabilir. Bu durumda bu fonksiyonlar statik kütüphanelerden alınarak çalıştırılabilen dosyalara yazılacaktır. Örneğin: "gcc -o app -static app.c" Tabii bu biçimde statik "link" işlemi yapıldığında çalıştırılabilen dosyanın boyutu çok büyüyecektir. Eğer "glibc" kütüphanesinin varsayılan olarak devreye sokulması istenmiyorsa "nodefaultlibs" seçeneğinin kullanılması gerekmektedir. Örneğin: "gcc -nodefaultlibs -o app app.c" Burada "glibc" kütüphanesi devreye sokulmadığı için "link" aşamasında hata oluşacaktır. Bu durumda kütüphanenin açıkça belirtilmesi gerekir. Örneğin: "gcc -nodefaultlibs -o app app.c -lc" Bir kütüphanenin statik ve dinamik biçimi aynı anda bulunuyorsa ve biz bu kütüphaneyi "-l" seçeneği ile belirtiyorsak bu durumda varsayılan olarak kütüphanenin dinamik versiyonu devreye sokulmaktadır. Eğer kütüphanelerin statik versiyonlarının devreye sokulması isteniyorsa "-static" komut satırı argümanının kullanılması gerekmektedir. Örneğin: "gcc -o app app.c -lmyutil -L." Burada eğer "libmyutil.so" ve "libmyutil.a" dosyaları varsa "libmyutil.so" dosyası kullanılacaktır. Yani dinamik link yapılacaktır. Tabii biz açıkça statik kütüphanenin ya da dinamik kütüphanenin kullanılmasını isteyebiliriz: "gcc -o app app.c libmyutil.a" ya da "gcc -static -o app app.c -lmyutil -L." Burada "glibc" kütüphanesinin dinamik biçimi devreye sokulacaktır. Ancak "libmyutil" kütüphanesi statik biçimde "link" edilmiştir. Eğer "-static" komut satırı argümanı kullanılırsa bu durumda tüm kütüphanelerin statik versiyonları devreye sokulmaktadır. Tabii bu durumda biz açıkça dinamik kütüphanelerin "link" işlemine sokulmasını isteyemeyiz. Örneğin: "gcc -static -o app app.c libmyutil.so" Bu işlem başarısız olacaktır. Çünkü "-static" argümanı zaten "tüm kütüphanelerin statik olarak link edileceği" anlamına gelmektedir. Bir programın kullandığı dinamik kütüphaneler "ldd" isimli "utility" program ile basit bir biçimde görüntülenebilir. Örneğin: $ ldd sample linux-vdso.so.1 (0x00007fff38162000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7ec0b5c000) /lib64/ld-linux-x86-64.so.2 (0x00007f7ec114f000) "ldd" programı dinamik kütüphanelerin kullandığı dinamik kütüphaneleri de görüntülemektedir. Programın doğrudan kullandığı dinamik kütüphanelerin listesi "readelf" komutuyla aşağıdaki gibi de elde edilebilir: $ readelf -d sample | grep "NEEDED" 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] Pekiyi bizim programımız örneğin "libmyutil.so" isimli bir dinamik kütüphaneden çağrı yapıyor olsun. Bu "libmyutil.so" dosyasının program çalıştırılırken nerede bulundurulması gerekir? İşte program çalıştırılırken ilgili dinamik kütüphane dosyasının özel bazı dizinlerde bulunuyor olması gerekmektedir. Yükleme sırasında hangi dizinlere bakıldığı "man ld.so" sayfasından elde edilebilir. Bu dokümanda dinamik kütüphanenin bulunabilmesi için sırasıyla tek tek hangi dizinlere bakıldığı belirtilmiştir. Dinamik "linker" ın dinamik kütüphaneleri nerede ve nasıl aradığının bazı ayrıntıları vardır. Ancak konuyu basitleştirirsek dinamik "linker", "LD_LIBRARY_PATH" isimli çevre değişkeni ile belirtilen dizinlerde, dinamik kütüphane dosyalarını aramaktadır. "LD_LIBRARY_PATH" çevre değişkeni normal olarak çevre değişken listesinde bulunmaz. Programcının bunu eklemesi gerekebilmektedir. Bu çevre değişkeninin değeri ":" karakterleriyle ayrılmış olan dizinlerden oluşmalıdır. İşte dinamik "linker" tek tek bu dizinlere bakmaktadır. Örneğin: "export LD_LIBRARY_PATH=/home/kaan:/home/kaan/Study/Unix-Linux-SysProg" Burada artık dinamik kütüphaneler "/home/kaan" dizininde ve "/home/kaan/Unix-Linux-SysProg" dizininde aranacaktır. Eğer biz "LD_LIBRARY_PATH" çevre değişkenine "." biçiminde dizin eklersek buradaki "." o anda bulunulan dizin anlamına gelmektedir. Örneğin: "export LD_LIBRARY_PATH=/home/kaan:/home/kaan/Study/Unix-Linux-SysProg:." Dinamik "linker" eğer dinamik kütüphaneyi "LD_LIBRARY_PATH" ile belirtilen dizinde bulamazsa bu durumda "/lib", "/usr/lib", "/lib64" ve "/usr/lib64" dizinlerine de bakmaktadır. (Bu 64'lü dizinlere "32-bit" sistemlerde ve bazı "64-bit" sistemlerde bakılmamaktadır.) O halde dinamik kütüphanemizin dinamik "linker" tarafından bulunmasını istiyorsak onu doğrudan "/lib" ya da "/usr/lib" dizinlerinin içerisine de yerleştirebiliriz. "/lib" dizini sistemin yüklenmesi için de gereken aşağı seviyeli kütüphanelerin bulunduğu dizindir. Programcının bu tür durumlarda "/usr/lib" dizinini tercih etmesi uygundur. Dinamik kütüphanelerin aranacağı yer aslında doğrudan çalıştırılabilen dosyanın içerisine "(.dynamic isimli bölüme ('section'))" de yazılabilir. Bunun için "-rpath " linker seçeneği kullanılmalıdır. Buradaki yol ifadesinin mutlak olması gerekir. Ancak eskiden bunun ELF formatında için "DT_RPATH" isimli tag kullanılırken daha sonra bu tag "deprecated" yapılmış ve "DT_RUNPATH" tag'ı kullanılmaya başlanmıştır. "-rpath" seçeneğini "linker" a geçirebilmek için "gcc" de "-Wl" seçeneğini kullanmak gerekir. "-Wl" seçeneği bitişik yazılan virgüllü alanlardan oluşmaktadır. "gcc" bunu "ld linker" ına virgüller yerine boşluklar koyarak geçirmektedir. Örneğin: "gcc -o app app.c -Wl,-rpath,/home/csd/Study/Unix-Linux-SysProg libmyutil.so" Burada ELF formatının "DT_RUNPATH" tag'ına yerleştirme yapılmaktadır. Çalıştırılabilir dosyaya iliştirilen "rpath" bilgisi "readelf" programı ile aşağıdaki gibi görüntülenebilir: $ readelf -d app | grep "RUNPATH" 0x000000000000001d (RUNPATH) Library runpath: [/home/csd/Study/Unix-Linux-SysPro Biz bu tag'a birden fazla dizin de yerleştirebiliriz. Bu durumda yine dizinleri ':' ile ayırmamız gerekir. Örneğin: "gcc -o app app.c -Wl,-rpath,/home/csd/Study/Unix-Linux-SysProg:/home/kaan libmyutil.so" Birden fazla kez "-rpath" seçeneği kullanıldığında bu seçenekler tek bir "DT_RUNPATH" tag'ına aralarına ':' karakteri getirilerek yerleştirilmektedir. Yani aşağıdaki işlem yukarıdaki ile eşdeğerdir: "gcc -o app app.c -Wl,-rpath,/home/csd/Study/Unix-Linux-SysProg,-rpath,/home/kaan libmyutil.so" Eğer DT_RPATH tag'ına yerleştirme yapılmak isteniyorsa linker seçeneklerine ayrıca --disable-new-dtags seçeneğinin de girilmesi gerekmektedir. Örneğin: "gcc -o app app.c -Wl,-rpath,/home/csd/Study/Unix-Linux-SysProg,--disable-new-dtags libmyutil.so" DT_RPATH tag'ını da aşağıdaki gibi görüntüleyebiliriz: $ readelf -d app | grep "RPATH" 0x000000000000000f (RPATH) Library rpath: [/home/csd/Study/Unix-Linux-SysProg] Yukarıda da belirttiğimiz gibi DT_RPATH tag'ı bir süredir "deprecated" yapılmıştır. Çalıştırılabilir dosyaya "DT_RUNPATH" tag'ının mutlak yol ifadesi biçiminde girilmesi kullanım sorunlarına yol açabilmektedir. Çünkü bu durumda dinamik kütüphaneler uygulamanın kurulduğu dizine göreli biçimde konuşlandırılacağı zaman kurulum yeri değiştirildiğinde sorunlar oluşabilmektedir. Örneğin biz çalıştırılabilir dosyanın "DT_RUNPATH" tag'ına "home/kaan/test" isimli yol ifadesini yazmış olalım. Programımızı ve dinamik kütüphanemizi bu dizine yerleştirirsek bir sorun oluşmayacaktır. Ancak başka bir dizine yerleştirirsek dinamik kütüphanemiz bulunamayacaktır. İşte bunu engellemek için "-rpath" seçeneğinde '$ORIGIN' argümanı kullanılmaktadır. Buradaki '$ORIGIN' argümanı o anda uygulamanın bulunduğu dizini temsil etmektedir. Örneğin: gcc -o app app.c -Wl,-rpath,'$ORIGIN'/. libmyutil.so Burada artık çalıştırılabilen dosya nereye yerleştirilirse yerleştirilsin dinamik kütüphaneler onun yerleştirildiği dizinde aranacaktır. Aslında arama sırası bakımından "DT_RPATH" tag'ının en yukarıda olması (daha doğrusu "LD_LIBRARY_PATH" in yukarısında olması) yanlış bir tasarımdır. Geriye doğru uyumu koruyarak bu yanlış tasarım "DT_RUNPATH" tag'ı ile telafi edilmiştir. "DT_RUNPATH" tag'ına "LD_LIBRARY_PATH" çevre değişkeninden sonra başvurulmaktadır. (Bunun için "man ld.so" sayfasındaki sıralamayı gözden geçiriniz.) Dinamik kütüphanelerin aranması sırasında "/lib" ve "/usr/lib" dizinlerine bakılmadan önce özel bir dosyaya da bakılmaktadır. Bu dosya "/etc/ld.so.cache" isimli dosyadır. "/etc/ld.so.cache" dosyası aslında "binary" bir dosyadır. Bu dosya hızlı aramanın yapılabilmesi için "sözlük (dictionary)" tarzı yani algoritmik aramaya izin verecek biçimde bir içeriğe sahiptir. Bu dosya ilgili dinamik kütüphane dosyalarının hangi dizinler içerisinde olduğunu gösteren bir yapıdadır. (Yani bu dosya ".so" dosyalarının hangi dizinlerde olduğunu belirten binary bir dosyadır.) Başka bir deyişle bu dosyanın içerisinde "falanca .so dosyası filanca dizinde" biçiminde bilgiler vardır. İlgili ".so" dosyasının yerinin bu dosyada aranması dizinlerde aranmasından çok daha hızlı yapılabilmektedir. Pekiyi bu "/etc/ld.so.cache" dosyasının içerisinde hangi ".so" dosyaları vardır? Aslında bu dosyanın içerisinde "/lib" ve "/usr/lib" dizinindeki ".so" dosyalarının hepsi bulunmaktadır. Ama programcı isterse kendi dosyalarını da bu cache dosyasının içerisine yerleştirebilir. Pekiyi neden böyle bir "cache" dosyasına gereksinim duyulmuştur? İşte dinamik kütüphaneler yüklenirken "/lib" ve "/usr/lib" dizinlerinin taranması göreli olarak uzun zaman almaktadır. Bu da programın yüklenme süresini uzatabilmektedir. Halbuki bu dizinlere bakılmadan önce bu "cache" dosyasına bakılırsa ilgili dosyanın olup olmadığı varsa nerede olduğu çok daha hızlı bir biçimde elde edilebilmektedir. Burada dikkat edilmesi gereken nokta bu "cache" dosyasına "/lib" ve "/usr/lib" dizinlerinden daha önce bakıldığı ve bu dizinlerin içeriğinin de zaten bu cache dosyasının içerisinde olduğudur. O halde aslında "/lib" ve "/usr/lib" dizinlerinde arama çok nadir olarak yapılmaktadır. Bu "cache" dosyasına "LD_LIBRARY_PATH" dizininden daha sonra bakılmaktadır. O halde programcının kendi ".so" dosyalarını da eğer uzun süreliğine konuşlandıracaksa bu "cache" dosyasının içerisine yazması tavsiye edilmektedir. Pekiyi "/etc/ld.so.cache" dosyasına biz nasıl bir dosya ekleriz? Aslında programcı bunu dolaylı olarak yapmaktadır. Şöyle ki: -> "/sbin/ldconfig" isimli bir program vardır. Bu program "/etc/ld.so.conf" isimli bir text dosyasına bakar. Bu dosya dizinlerden oluşmaktadır. Bu "ldconfig" programı bu dizinlerin içerisindeki "so" dosyalarını "/etc/ld.so.cache" dosyasına eklemektedir. Şimdilerde "/etc/ld.so.conf" dosyasının içeriği şöyledir: "include /etc/ld.so.conf.d/*.conf" Bu satır "/etc/ld.so.conf.d" dizinindeki tüm ".conf" uzantılı dosyaların bu işleme dahil edileceğini belirtmektedir. Biz "ldconfig" programını çalıştırdığımızda bu program "/lib", "/usr/lib" ve "/etc/ld.so.conf" (dolayısıyla "/etc/ld.so.conf.d" dizinindeki ".conf" dosyalarına) bakarak "/etc/ld.so.cache" dosyasını yeniden oluşturmaktadır. O halde bizim bu cache'e ekleme yapmak için tek yapacağımız şey "/etc/ld.so.conf.d" dizinindeki bir ".conf" dosyasına yeni bir satır olarak bir dizinin yol ifadesini girmektir. (".conf" dosyaları her satırda bir dizinin yol ifadesinden oluşmaktadır.) Tabii programcı isterse bu dizine yeni bir ".conf" dosyası da ekleyebilir. İşte programcı bu işlemi yaptıktan sonra "/sbin/ldconfig" programını çalıştırınca artık onun eklediği dizinin içerisindeki ".so" dosyaları da "/etc/ld.so.cache" dosyasının içerisine eklenmiş olacaktır. Daha açık bir anlatımla programcı bu "cache" dosyasına ekleme işini adım adım şöyle yapar: -> Önce ".so" dosyasını bir dizine yerleştirir. -> Bu dizinin ismini "/etc/ld.so.conf.d" dizinindeki bir dosyanın sonuna ekler. Ya da bu dizinde yeni "conf" dosyası oluşturarak dizini bu dosyanın içerisine yazar. -> "/sbin/ldconfig" programını çalıştırır. "ldconfig" programının "sudo" ile çalıştırılması gerektiğine dikkat ediniz. Zaten "/sbin" dizinindeki tüm programlar "super user" için bulundurulmuştur. Programcı "/etc/ld.so.conf.d" dizinindeki herhangi bir dosyaya değil de "-f" seçeneği ile kendi belirlediği bir dosyaya da ilgili dizini yazabilmektedir. Başka bir deyişle "-f" seçeneği "şu config dosyasına da bak" anlamına gelmektedir. "ldconfig" her çalıştırıldığında sıfırdan yeniden "cache" dosyasını oluşturmaktadır. Programcı "/lib" ya da "/usr/lib" dizinine bir "so" dosyası eklediğinde "ldconfig" programını çalıştırması zorunlu olmasa da iyi bir tekniktir. Çünkü o dosya da "cache" dosyasına yazılacak ve daha hızlı bulunacaktır. "ldconfig" programında "-p" seçeneği ile "cache" dosyası içerisindeki tüm dosyalar görüntülenebilmektedir. Kütüphane dosyalarının "so" isimleri denilen bir isimleri de bulunabilmektedir. Kütüphane dosyalarının "so" isimleri "linker" tarafından kullanılan isimleridir. Kütüphane dosyası oluşturulurken "so" isimleri verilmeyebilir. Yani bir kütüphane dosyasının "so" ismi olmak zorunda değildir. Kütüphane dosyalarına "so" isimlerini vermek için "-soname " linker seçeneği kullanılmaktadır. Kütüphanelere verilen "so" isimleri ELF formatının dinamik bölümündeki "(dynamic section)" "SONAME" isimli bir tag'ına yerleştirilmektedir. "-soname" komut satırı argümanı linker'a ilişkin olduğu için "-Wl" seçeneği ile kullanılmalıdır. Örneğin biz "libx.so" isimli bir dinamik kütüphaneyi "so" ismi vererek oluşturmak isteyelim. Bu işlemi şöyle yapabiliriz: "gcc -o libx.so -fPIC -shared -Wl,-soname,liby.so libx.c" Burada "libx.so" kütüphane dosyasına "liby.so" "so" ismi verilmiştir. Kütüphane dosyalarına iliştirilen "so" isimleri "readelf" ile aşağıdaki gibi görüntülenebilir: $ readelf -d libx.so | grep "SONAME" 0x000000000000000e (SONAME) Kitaplık so_adı: [liby.so] Aynı işlem "objdump" programıyla da şöyle yapılabilir: objdump -x libx.so | grep "SONAME" SONAME liby.so Tabii yukarıda da belirttiğimiz gibi biz dinamik kütüphanelere "so" ismi vermek zorunda değiliz. "so" ismi içeren bir kütüphaneyi kullanan bir program link edilirken "linker" çalıştırılabilen dosyaya "so" ismini içeren kütüphanenin ismini değil "so" ismini yazmaktadır. Yukarıdaki örneğimizde "libx.so" kütüphanesi "so" ismi olarak "liby.so" ismini içermektedir. Şimdi "libx.so" dosyasını kullanan "app.c" dosyasını derleyip link edelim: "gcc -o app app.c libx.so" Burada link işleminde "libx.so" dosya ismi kullanılmıştır. Ancak oluşturulan "app" dosyasının içerisine linker bu ismi değil, "so" ismi olan "liby.so" ismini yazacaktır. Örneğin: $ readelf -d app | grep "NEEDED" 0x0000000000000001 (NEEDED) Paylaşımlı kitaplık: [liby.so] 0x0000000000000001 (NEEDED) Paylaşımlı kitaplık: [libc.so.6] O halde biz buradaki "app" dosyasını çalıştırmak istediğimizde yükleyici (yani dinamik "linker") artık "libx.so" dosyasını değil, "liby.so" dosyasını yüklemeye çalışacaktır. Örneğin. $ export LD_LIBRARY_PATH=. $ ./app ./app: error while loading shared libraries: liby.so: cannot open shared object file: No such file or directory Tabii yukarıda belirttiğimiz gibi eğer kütüphaneyi oluştururken ona "so" ismi vermeseydik bu durumda linker "app" dosyasına "libx.so" dosyasını yazacaktı ve yükleyici de (dynamic "linker") bu dosyası yükleyecekti. Pekiyi yukarıdaki örnekte "app" programı artık "liby.so" dosyasını kullanıyor gibi olduğuna göre ve böyle de bir dosya olmadığına göre bu işlemlerin ne anlamı vardır? İşte biz bıu örnekte "so" ismine ilişkin dosyayı bir sembolik "link" dosyası haline getirirsek ve bu sembolik link dosyası da "libx.so" dosyasını gösterir hale gelirse sorunu ortadan kaldırabiliriz. Örneğin: $ ln -s libx.so liby.so $ ls -l liby.so lrwxrwxrwx 1 kaan study 7 Şub 25 16:44 liby.so -> libx.so Şimdi artık "app" dosyasını çalıştırmak istediğimizde yükleyici "liby.so" dosyasını yüklemek isteyecektir. Ancak "liby.so" dosyası da zaten "libx.so" dosyasını belrttiği için yine "libx.so" dosyası yüklenecektir. Yani artık "app" dosyasını çalıştırabiliriz. Tabii burada tüm bunları neden yapmış olduğumuza bir anlam verememiş olabilirsiniz. İşte bunun anlamını izleyen paragraflarda dinamik kütüphanelerin versiyonlanması konusunda açıklayacağız. UNIX/Linux sistemlerinde dinamik kütüphane dosyalarına isteğe bağlı olarak birer versiyon numarası verilebilmektedir. Bu versiyon numarası dosya isminin bir parçası durumundadır. Linux sistemlerinde izlenen tipik numaralandırma "(convention)" şöyledir: .so... Örneğin: libmyutil.so.2.4.6 Majör numaralar büyük değişiklikleri, minör numaralar ise küçük değişiklikleri anlatmaktadır. Majör numara değişirse yeni dinamik kütüphane eskisiyle uyumlu olmaz. Burada "uyumlu değildir" lafı eski dinamik kütüphaneyi kullanan programların yenisini kullanamayacağı anlamına gelmektedir. Çünkü muhtemelen bu yeni versiyonda fonksiyonların isimlerinde, parametrik yapılarında değişiklikler söz konusu olmuş olabilir ya da bazı fonksiyonlar silinmiş olabilir. Fakat majör numarası aynı ancak minör numaraları farklı olan kütüphaneler birbirleriyle uyumludur. Yani alçak minör numarayı kullanan program yüksek minör numarayı kullanırsa sorun olmayacaktır. Bu durumda tabii yüksek minör numaralı kütüphanede hiçbir fonksiyonun ismi, parametrik yapısı değişmemiş ve hiçbir fonksiyon silinmemiş olmalıdır. Örneğin yüksek minör numaralarda fonksiyonlarda daha hızlı çalışacak biçimde optimizasyonlar yapılmış olabilir. Ya da örneğin yüksek minör numaralarda yeni birtakım fonksiyonlar da eklenmiş olabilir. Çünkü yeni birtakım fonksiyonlar eklendiğinde eski fonksiyonlar varlığını devam ettirmektedir. Tabii yine de bu durum dinamik kütüphanenin eski versiyonunu kullanan programların düzgün çalışacağı anlamına gelmemektedir. Çünkü programcılar kodlarına yeni birtakım şeyler eklerken istemeden eski kodların çalışmasını da bozabilmektedir. (Bu tür problemler Windows sistemlerinde eskiden ciddi sıkıntılara yol açmaktaydı. Bu probleme Windows sistemlerinde "DLL cehennemi (DLL Hell)" deniyordu.) Linux sistemlerinde versiyonlama bakımından bir dinamik kütüphanenin üç ismi bulunmaktadır: -> Gerçek ismi (real name) -> so ismi (so name) -> Linker ismi (linker name) Kütüphanenin majör ve çift minör versiyonlu ismine gerçek ismi denilmektedir. Örneğin: "libmyutil.so.2.4.6" "so" ismi ise yalnızca majör numara içeren ismidir. Örneğin yukarıdaki gerçek ismin "so" ismi şöyledir: "libmyutil.so.2" Linker ismi ise hiç versiyon numarası içermeyen ismidir. Örneğin yukarıdaki kütüphanelerin linker ismi ise şöyledir: "libmyutil.so" İşte tipik olarak "so" ismi gerçek isme sembolik link, linker ismi de en yüksek numaralı "so" ismine sembolik link yapılır. linker ismi ---> so ismi ---> gerçek ismi Örneğin: gcc -o libmyutil.so.1.0.0 -shared -fPIC libmyutil.c (gerçek isimli kütüphane dosyası oluşturuldu) ln -s libmyutil.so.1.0.0 libmyutil.so.1 (so ismi oluşturuldu) ln -s libmyutil.so.1 libmyutil.so (linker ismi oluşturuldu) Burada oluşturulan üç dosyayı "ls -l" komutu ile görüntüleyelim: lrwxrwxrwx 1 kaan study 14 Şub 25 15:45 libmyutil.so -> libmyutil.so.1 lrwxrwxrwx 1 kaan study 18 Şub 25 15:45 libmyutil.so.1 -> libmyutil.so.1.0.0 -rwxr-xr-x 1 kaan study 15736 Şub 25 15:45 libmyutil.so.1.0.0 Dinamik kütüphanelerin "linker" isimleri o kütüphaneyi kullanan programlar "link" edilirlen "link" aşamasında ("link" ederken) kullanılan isimlerdir. Bu sayede "link" işlemini yapan programcıların daha az tuşa basarak genel bir isim kullanması sağlanmıştır. Bu durumda örneğin biz "libmyutil" isimli kütüphaneyi kullanan programı link etmek istersek şöyle yapabiliriz: "gcc -o app app.c libmyutil.so" Ya da şöyle yapabiliriz: "gcc -o app app.c -lmyutil -L." Burada aslında "libmyutil.so" dosyası "so ismine" "so" ismi de "gerçek isme link yapılmış" durumdadır. Yani bu komutun aslında eşdeğeri şöyledir: "gcc -o app app.c libmyutil.so.1.0.0" Yukarıda anlattıklarımızı özetlersek geldiğimiz noktayı daha iyi kavrayabiliriz: -> Bir dinamik kütüphane oluştururken ona bir versiyon numarası da atanabilmektedir. Örneğin biz oluşturduğumuz "myutil" dinamik kütüphanesine "1.0.0" versiyon numarası atamış olalım. Bu durumda kütüphanemizin gerçek ismi "libmyutil.so.1.0.0" olacaktır. Kütüphanemizi aşağıdaki gibi derlemiş olalım: $ gcc -fPIC -shared -o libmyutil.so.1.0.0 -Wl,-soname,libmyutil.so.1 libmyutil.c -> Dinamik kütüphanelerin "so ismi" kütüphanelerin içerisine yazılan ismidir. Yukarıdaki gibi bir derlemede biz "libmyutil.so.1.0.0" kütüphanesinin içerisine "so ismi" olarak "libmyutil.so" ismini yerleştirdik. "so isimleri" genel olarak yalnızca majör numara içeren isimlerdir. Bizim bu aşamada tipik olarak bir sembolik link oluşturarak "so" ismine ilişkin dosyanın gerçek kütüphane dosyasını göstermesini sağlamamız gerekir. Bunu şöyle yapabiliriz: $ ln -s libmyutil.so.1.0.0 libmyutil.so.1 Şimdi her iki dosyayı da görüntüleyelim: lrwxrwxrwx 1 kaan study 18 Mar 1 20:02 libmyutil.so.1 -> libmyutil.so.1.0.0 -rwxr-xr-x 1 kaan study 15736 Mar 1 19:57 libmyutil.so.1.0.0 -> Dinamik kütüphanenin link aşamasında kullanılmasını kolaylaştırmak için sonunda versiyon uzuntısı olmayan bir "linker ismi" oluşturabiliriz. Tabii bu "linker" ismi aslında gerçek kütüphaneye referans edecektir. Ancak bu referansın doğrudan değil de "so ismi" üzerinden yapılması daha esnek bir kullanıma yol açacaktır. Örneğin: $ ln -s libmyutil.so.1 libmyutil.so Artık kütüphanenin "linker ismi" "so ismine", "so ismi" de gerçek ismine sembolik link yapılmış durumdadır. Bu üç dosyayı aşağıda yeniden görüntüleyelim: lrwxrwxrwx 1 kaan study 14 Mar 1 20:07 libmyutil.so -> libmyutil.so.1 lrwxrwxrwx 1 kaan study 18 Mar 1 20:02 libmyutil.so.1 -> libmyutil.so.1.0.0 -rwxr-xr-x 1 kaan study 15736 Mar 1 19:57 libmyutil.so.1.0.0 Aşağıdaki gibi bir durum elde ettiğimize dikkat ediniz: Linker ismi ---> so ismi ---> gerçek isim -> Şimdi kütüphaneyi kullanan bir "app" programını derleyip link edelim: $ gcc -o app app.c libmyutil.so Şimdi "LD_LIBRARY_PATH" çevre değişkenini belirleyip programı çalıştıralım: $ LD_LIBRARY_PATH=. ./app 30.000000 -10.000000 200.000000 0.500000 Burada app programının kullandığı kütüphane ismi app dosyasının içerisinde kütüphanenin "so ismi" olarak set edilecektir. Yani burada "app" dosyası sanki "libmyutil.so.1" dosyasını kullanıyor gibi olacaktır. Örneğin: $ readelf -d app | grep "NEEDED" 0x0000000000000001 (NEEDED) Paylaşımlı kitaplık: [libmyutil.so.1] 0x0000000000000001 (NEEDED) Paylaşımlı kitaplık: [libc.so.6] İşte "app" programını yükleyecek olan dinamik linker aslında "libmyutil.so.1" dosyasını yüklemeye çalışacaktır. Bu dosyann kütüphanenin gerçek ismine sembolik "link" yapıldığını anımsayınız. Bu durumda gerçekte yüklenecek olan dosya "libmyutil.so.1" dosyası değil, "libmyutil.so.1.0.0" dosyası olacaktır. Yani çalışmada bir sorun ortaya çıkmayacaktır. Pekiyi tüm bunların amacı nedir? Bunu şöyle açıklayabiliriz: -> Örneğin kütüphanemizin libmyutil.so.1.1.0 biçiminde majör numarası aynı, minör numarası farklı öncekiyle uyumlu yeni bir versiyonunun daha oluşturulduğunu düşünelim. Şimdi biz uygulamamızı çektiğimiz dizin içerisindeki "libmyutil.so" dosyasını bu yeni versiyonu referans edecek biçimde değiştirebiliriz. Bu durumda dinamik "linker" "app" programını yüklemeye çalışırken aslında artık "libmyutil.so.1.1.0" kütüphanesini yükleyecektir. Burada biz hiç "app" dosyasının içini değiştirmeden artık "app" dosyasının kütüphanenin yeni minör versiyonunu kullamasını sağlamış olduk. -> Şimdi de kütüphanemizin "libmyutil.so.2.0.0" biçiminde yeni bir majör versiyonunun oluşturulduğunu varsayalım. "1" numaralı majör versiyonla "2" numaralı majör versiyon birbirleriyle uyumlu değildir. Biz bu "libmyutil.so.2.0.0" yeni versiyonu derlerken ona "so ismi" olarak artık "libmyutil.so.2" ismini vermeliyiz. Tabii bu durumda biz yine "libmyutil.so.2" sembolik bağlantı dosyasının "libmyutil.so.2.0.0" dosyasını göstermesini sağlamalıyız. Artık kütüphanenin 2'inci versiyonunu kullanan programlarda yüklenecek kütüphane "libmyutil.so.2" kütüphanesi olacaktır. Bu kütüphanede ikinci versiyonunun gerçek kütüphane ismine sembolik link yapılmış durumdadır. "so ismine" ilişkin sembolik link çıkartma ve "/etc/ld.so.cache" dosyasının güncellenmesi işlemi ldconfig tarafından otomatik, yapılabilmektedir. Yani aslında örneğin biz kütüphanenin gerçek isimli dosyasını "/lib" ya da "/usr/lib" içerisine yerleştirip "ldconfig" programını çalıştırdığımızda bu program zaten "so ismine" ilişkin sembolik "link" i de oluşturmaktadır. Örneğin biz "libmyutil.so.1.0.0" dosyasını "/usr/lib" dizinine kopyalayalım ve "ldconfig" programını çalıştıralım. "ldconfig" programı "libmyutil.so.1" sembolik "link" dosyasını oluşturup bu sembolik "link" dosyasının "libmyutil.so.1.0.0" dosyasına referans etmesini sağlayacaktır. Tabii cache'e de "libmyutil.so.1" dosyasını yerleştirecektir. Örneğin: $ ldconfig -p | grep "libmyutil" libmyutil.so.1 (libc6,x86-64) => /lib/libmyutil.so.1 $ ls -l /usr/lib | grep "libmyutil" lrwxrwxrwx 1 root root 18 Mar 1 21:02 libmyutil.so.1 -> libmyutil.so.1.0.0 -rwxr-xr-x 1 root root 15736 Mar 1 21:01 libmyutil.so.1.0. Özetle Dinamik kütüphane kullanırken şu konvansiyona uymak iyi bir tekniktir: -> Kütüphane ismini "lib" ile başlatarak vermek -> Kütüphane ismine majör ve minör numara vermek -> Gerçek isimli kütüphane dosyasını oluştururken "so ismi" olarak "-Wl,-soname" seçeneği ile kütüphanenin "so ismini" yazmak -> Kütüphane için "linker ismi" ve "so ismini" sembolik link biçiminde oluşturmak -> Kütüphane paylaşılacaksa onu "/lib" ya da tercihen "/usr/lib" dizinine yerleştirmek ve "ldconfig" programı çalıştırarak "/etc/ld.so.cache" dosyasının güncellenmesini sağlamak Dinamik kütüphane dosyaları program çalıştırıldıktan sonra çalışma zamanı sırasında çalışmanın belli bir aşamasında da yüklenebilir. Buna "dinamik kütüphane dosyalarının dinamik yüklenmesi" de denilmektedir. Dinamik kütüphane dosyalarının baştan "dinamik linker" tarafından değil de programın çalışma zamanı sırasında yüklenmesinin bazı avantajları şunlar olabilmektedir: -> Dinamik kütüphaneler baştan yüklenmediği için program başlangıçta daha hızlı yüklenebilir. -> Programın sanal bellek alanı gereksiz bir biçimde doldurulmayabilir. Örneğin nadiren çalışacak bir fonksiyon dinamik kütüphanede olabilir. Bu durumda o dinamik kütüphanenin işin başında yüklenmesi gereksiz bir yükleme zamanı ve bellek israfına yol açabilmektedir. Dinamik kütüphanelerin dinamik yüklenmesi dlopen, dlsym, dlerror ve dlclose fonksiyonlarıyla yapılmaktadır. Bu fonksiyonlar "libdl" kütüphanesi içerisindedir. Dolayısıyla link işlemi için "-ldl" seçeneğinin bulundurulması gerekir. -> Dinamik kütüphanelerin dinamik yüklenmesi için önce "dlopen" fonksiyonu ile dinamik kütüphanenin yüklenmesinin sağlanması gerekir. "dlopen" fonksiyonunun prototipi şöyledir: #include void *dlopen(const char *filename, int flag); Fonksiyonun birinci parametresi yüklenecek dinamik kütüphanenin yol ifadesini, ikinci parametresi seçenek belirten bayrakları almaktadır. Fonksiyon başarı durumunda kütüphaneyi temsil eden bir handle değerine, başarısızlık durumunda "NULL" adrese geri dönmektedir. "dlopen" fonksiyonunun birinci parametresindeki dinamik kütüphane isminde eğer hiç "/" karakteri yoksa bu durumda kütüphanenin aranması daha önce ele aldığımız prosedüre göre yapılmaktadır. Eğer dosya isminde en az bir "/" karakteri varsa dosya yalnızca bu mutlak ya da göreli yol ifadesinde aranmaktadır. Dinamik yükleme sırasında yüklenecek kütüphanenin SONAME alanında yazılan isme hiç bakılmamaktadır. (Bu "SONAME" alanındaki isim yalnızca "link" aşamasında "linker" tarafından kullanılmaktadır.) Örneğin: void *dlh; if ((dlh = dlopen("libmyutil.so.1.0.0", RTLD_NOW)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } Burada "dlopen" fonksiyonunun ikinci parametresine "RTLD_NOW" bayrağı geçilmiştir. Bu bayrağın etkisi izleyen paragraflarda ele alınacaktır. -> Başarısızlık durumunda fonksiyon "errno" değişkenini "set" etmez. Başarısızlığa ilişkin yazı doğrudan "dlerror" fonksiyonuyla elde edilmektedir: char *dlerror(void); -> Kütüphanenin adres alanından boşaltılması ise "dlclose" fonksiyonuyla yapılmaktadır: #include int dlclose(void *handle); Aynı kütüphane dlopen fonksiyonu ile ikinci kez yüklenebilir. Bu durumda gerçek bir yükleme yapılmaz. Ancak yüklenen sayıda "close" işleminin yapılması gerekmektedir. Kütüphanenin içerisindeki fonksiyonlar ya da global nesneler adresleri elde edilerek kullanılırlar. Bunların adreslerini elde edebilmek için dlsym isimli fonksiyon kullanılmaktadır: #include void *dlsym(void *handle, const char *symbol); Fonksiyon başarı durumunda ilgili sembolün adresine, başarısızlık durumunda NULL adrese geri döner. Örneğin: double (*padd)(double, double); ... if ((padd = (double (*)(double, double))(dlsym(dlh, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = padd(10, 20); printf("%f\n", result); Ancak burada C standartları bağlamında bir pürüz vardır. C'de (ve tabii C++'ta) fonksiyon adresleri ile data adresleri tür dönüştürme operatörü ile bile dönüştürülememektedir. Yani yukarıdaki tür dönüştürmesi ile atama geçersizdir. Ayrıca "void *" türü "data" adresi için anlamlıdır. Yani biz C'de de C++'ta da "void" bir adresi fonksiyon göstericisine, fonksiyon adresini de "void" bir göstericiye atayamayız. Ancak pek çok derleyici "default" durumda bu biçimdeki dönüştürmeleri kabul etmektedir. Yani yukarıdaki kod aslında C'de geçersiz olmasına karşın "gcc" ve "clang" derleyicilerinde sorunsuz derlenecektir. (Derleme sırasında "-pedantic-errors" seçeneği kullanılırsa derleyiciler standartlara uyumu daha katı bir biçimde ele almaktadır. Dolayısıyla yukarıdaki kod bu seçenek kullanılarak derlenirse "error" oluşacaktır.) Pekiyi bu durumda ne yapabiliriz? İşte bunun için bir hile vardır; -> Fonksiyon göstericisinin adresini alırsak artık o bir "data" göstericisi haline gelir. Bir daha "*" kullanırsak "data" göstericisi gibi aslında fonksiyon göstericisinin içerisine değer atayabiliriz. Örneğin: if ((*(void **)&padd = dlsym(dlh, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } Sembol isimleri konusunda dikkat etmek gerekir. Çünkü bazı derleyiciler bazı koşullar altında isimleri farklı isim gibi "object" dosyaya yazabilmektedir. Buna "name decoration" ya da "name mangling" denilmektedir. Örneğin C++ derleyicileri fonksiyon isimlerini parametrik yapıyla kombine ederek başka bir isimle "object" dosyaya yazar. Halbuki "dlsym" fonksiyonunda sembolün dinamik kütüphanedeki dekore edilmiş isminin kullanılması gerekmektedir. Sembollerin dekore edilmiş isimlerini elde edebilmek için "nm" utility'sini kullanabilirsiniz. Örneğin: "nm libmyutil.so.1.0.0" "nm utility" si ELF formatının "string" tablosunu görüntülemektedir. Aynı işlem readelf programında -s ile de yapılabilir: "readelf -s libmyutil.so.1.0.0" Aşağıda bir dinamik kütüphane dinamik olarak yüklenmiş ve oradan bir fonksiyon ve "data" adresi alınarak kullanılmıştır. Buradaki dinamik kütüphaneyi daha önce yaptığımız gibi derleyebilirsiniz: "gcc -fPIC -shared -o libmyutil.so.1.0.0 -Wl,-soname,libmyutil.so.1 libmyutil.c" Aşağıda bu konuya ilişkin bir örnek verilmiştir. * Örnek 1, /* libmyutil.c */ #include double add(double a, double b) { return a + b; } double sub(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } /* app.c */ #include #include #include typedef double (*PROC)(double, double); int main(void) { void *dlh; PROC padd, psub, pmul, pdiv; double result; if ((dlh = dlopen("libmyutil.so.1.0.0", RTLD_NOW)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } if ((*(void **)&padd = dlsym(dlh, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = padd(10, 20); printf("%f\n", result); if ((*(void **)&psub = dlsym(dlh, "sub")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = psub(10, 20); printf("%f\n", result); if ((*(void **)&pmul = dlsym(dlh, "multiply")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = pmul(10, 20); printf("%f\n", result); if ((*(void **)&pdiv = dlsym(dlh, "divide")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = pdiv(10, 20); printf("%f\n", result); dlclose(dlh); return 0; } Dinamik kütüphane "dlopen" fonksiyonuyla yüklenirken global değişkenlerin ve fonksiyonların nihai yükleme adresleri bu "dlopen" işlemi sırasında hesaplanabilir ya da onlar kullanıldıklarında hesaplanabilir. İkisi arasında kullanıcı açısından bir fark olmamakla birlikte tüm sembollerin adreslerinin yükleme sırasında hesaplanması bazen yükleme işlemini (eğer çok sembol varsa) uzatabilmektedir. Bu durumu ayarlamak için dlopen fonksiyonunun ikinci parametresi olan flags parametresi kullanılır. Bu "flags" parametresi, -> "RTLD_NOW" olarak girilirse (yukarıdaki örnekte böyle yaptık) tüm sembollerin adresleri "dlopen" sırasında; -> "RTLD_LAZY" girilirse kullanıldıkları noktada; hesaplanmaktadır. İki biçim arasında çoğu kez programcı için bir farklılık oluşmamaktadır. Ancak aşağıdaki örnekte bu iki biçimin ne anlama geldiği gösterilmektedir. * Örnek 1, Aşağıdaki örnekte "libmyutil.so.1.0.0" kütüphanesindeki "foo" fonksiyonu gerçekte olmayan bir bar fonksiyonunu çağırmıştır. Bu fonksiyonun gerçekte olmadığı "foo" fonksiyonunun sembol çözümlemesi yapıldığında anlaşılacaktır. İşte eğer bu kütüphaneyi kullanan "app.c" programı kütüphaneyi "RTLD_NOW" ile yüklerse tüm semboller o anda çözülmeye çalışılacağından dolayı "bar" fonksiyonunun bulunmuyor olması hatası da "dlopen" sırasında oluşacaktır. Eğer kütüphane "RTLD_LAZY" ile yüklenirse bu durumda sembol çözümlemesi "foo" nun kullanıldığı noktada (yani dlsym fonksiyonunda) gerçekleşecektir. Dolayısıyla hata da o noktada oluşacaktır. Bu programı "RTLD_NOW" ve "RTLD_LAZY" bayraklarıyla ayrı ayrı derleyip çalıştırınız. /* libmyutil.c */ #include void bar(void); void foo(void) { bar(); } /* app.c */ #include #include #include int main(void) { void *dlh; void (*pfoo)(void); double result; if ((dlh = dlopen("libmyutil.so.1.0.0", RTLD_LAZY)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } printf("dlopen called\n"); if ((*(void **)&pfoo = dlsym(dlh, "foo")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } pfoo(); dlclose(dlh); return 0; } Bazen bir dinamik kütüphane içerisindeki sembollerin o dinamik kütüphaneyi kullanan kodlar tarafından kullanılması istenmeyebilir. Örneğin dinamik kütüphanede "bar" isimli bir fonksiyon vardır. Bu fonksiyon bu dinamik kütüphanenin kendi içerisinden başka fonksiyonlar tarafından kullanılıyor olabilir. Ancak bu fonksiyonun dinamik kütüphanenin dışından kullanılması istenmeyebilir. (Bunun çeşitli nedenleri olabilir. Örneğin kapsülleme sağlamak için, dışarıdaki sembol çakışmalarını ortadan kaldırmak için vs.) İşte bunu sağlamak amacıyla "gcc" ve "clang" derleyicilerine özgü "__attribute__((...))" eklentisindan faydalanılmaktadır. "__attribute__((...))" eklentisi pek çok seçeneğe sahip platform spesifik bazı işlemlere yol açmaktadır. Bu eklentinin seçeneklerini gcc dokümanlarından elde edebilirsiniz. Bizim bu amaçla kullanacağımız "__attribute__((...))" seçeneği "visibility" isimli seçenektir. Aşağıdaki örnekte "bar" fonksiyonu "foo" fonksiyonu tarafından kullanılmaktadır. Ancak kütüphanenin dışından bu fonksiyonun kullanılması istenmemiştir. Eğer fonksiyon isminin soluna "__attribute__((visibility("hidden")))" yazılırsa bu durumda bu fonksiyon dinamik kütüphanenin dışından herhangi bir biçimde kullanılamaz. Örneğin: void __attribute__((visibility("hidden"))) bar(void) { // ... } Burada fonksiyon özelliğinin (yani "__attribute__" sentaksının) fonksiyon isminin hemen soluna getirildiğine ve çift parantez kullanıldığına dikkat ediniz. Burada kullanılan özellik "visibility" isimli özelliktir ve bu özelliğin değeri "hidden" biçiminde verilmiştir. * Örnek 1, Aşağıdaki örnekte "libmyutil.so.1.0.0" kütüphanesindeki "foo" fonksiyonu dışarıdan çağrılabildiği halde "bar" fonksiyonu dışarıdan çağrılamayacaktır. Tabii kütüphane içerisindeki "foo" fonksiyonu bar fonksiyonunu çağırabilmektedir. Dosyaları, gcc -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.c gcc -o app app.c libmyutil.so.1.0.0 -ldl gibi derleyebilirsiniz. /* libmyutil.c */ #include void __attribute__((visibility("hidden"))) bar(void) { printf("bar\n"); } void foo(void) { printf("foo\n"); bar(); } /* app.c */ #include #include #include int main(void) { void *dlh; void (*pfoo)(void); void (*pbar)(void); double result; if ((dlh = dlopen("./libmyutil.so.1.0.0", RTLD_NOW)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } if ((*(void **)&pfoo = dlsym(dlh, "foo")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } pfoo(); if ((*(void **)&pbar = dlsym(dlh, "bar")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } pbar(); dlclose(dlh); return 0; } * Örnek 2, Pekiyi dinamik kütüphaneyi dinamik yüklemeyip normal yöntemle kullansaydık ne olacaktı? İşte bu durumda hata, programı link ederken oluşmaktadır. Örneğin: $ gcc -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.c $ gcc -o app app.c libmyutil.so.1.0.0 -ldl /usr/bin/ld: /tmp/ccK2cCXC.o: in function `main': app.c:(.text+0xe): undefined reference to `bar' collect2: error: ld returned 1 exit status Bu testi yapabilmek için app.c programı şöyle olabilir: #include void __attribute__((visibility("hidden"))) bar(void) { printf("bar\n"); } void foo(void) { printf("foo\n"); bar(); } Bir dinamik kütüphane normal olarak ya da dinamik olarak yüklendiğinde birtakım ilk işlerin yapılması gerekebilir. (Örneğin kütüphane "thread-safe" olma iddiasındadır ve birtakım senkronizasyon nesnelerinin ve "thread" e özgü alanların yaratılması gerekebilir.) Bunun için "gcc" ve "clang" derleyicilerine özgü olan "__attribute__((constructor))" fonksiyon özelliği "(function attribute)" kullanılmaktadır. Benzer biçimde dinamik kütüphane programın adres alanından boşaltılırken de birtakım son işlemler için "__attribute__((destructor))" ile belirtilen fonksiyon çağrılmaktadır. (Aslında bu "constructor" ve "destructor" fonksiyonları normal programlarda da kullanılabilir. Bu durumda ilgili fonksiyonlar "main" fonksiyonundan önce ve "main" fonksiyonundan sonra çağrılmaktadır.) Dinamik kütüphane birden fazla kez yüklendiğinde yalnızca ilk yüklemede toplamda bir kez "constructor" fonksiyonu çağrılmaktadır. Benzer biçimde "destructor" fonksiyonu da yalnızca bir kez çağrılır. * Örnek 1.0, Aşağıda normal bir programda "__attribute__((constructor))" ve "__attribute__((destructor))" fonksiyon özelliklerinin kullanımına bir örnek verilmiştir. Ekranda şunları göreceksiniz: constructor foo begins... constructor foo ends... main begins... main ends... destructor bar begins... destructor bar ends... İlgili programın kodları da şöyle olabilir: #include void __attribute__((constructor)) foo(void) { printf("constructor foo begins...\n"); printf("constructor foo ends...\n"); } void __attribute__((destructor)) bar(void) { printf("destructor bar begins...\n"); printf("destructor bar ends...\n"); } int main(void) { printf("main begins...\n"); printf("main ends...\n"); return 0; } * Örnek 1.1, Aşağıda da dinamik kütüphane içerisinde "__attribute__((constructor))" ve "__attribute__((destructor))" fonksiyon özelliklerinin kullanımına bir örnek verilmiştir. Derlemeyi aşağıdaki gibi yapabilirsiniz: $ gcc -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.c $ gcc -o app app.c libmyutil.so.1.0.0 -ldl Kütüphaneye "so ismi" verdiğimiz için sembolik link oluşturmayı unutmayınız: $ ln -s libmyutil.so.1.0.0 libmyutil.so.1 Programı çalıştırmadan önce "LD_LIBRARY_PATH" çevre değişkenini de ayarlayınız: $ export LD_LIBRARY_PATH=. İlgili programın kodları da şöyle olabilir: /* libmyutil.c */ #include void __attribute__((constructor)) constructor(void) { printf("constructor begins...\n"); printf("constructor ends...\n"); } void __attribute__((destructor)) destructor(void) { printf("destructor begins...\n"); printf("destructor ends...\n"); } void foo(void) { printf("foo\n"); } /* app.c */ #include void foo(void); int main(void) { foo(); return 0; } Bir kütüphane oluşturmak isteyen kişi kütüphanesi için en azından bir başlık dosyasını kendisi oluşturmalıdır. Çünkü kütüphane içerisindeki fonksiyonları kullanacak kişiler en azından onların prototiplerini bulundurmak zorunda kalacaklardır. Kütüphaneler için oluşturulacak başlık dosyalarında kütüphane için anlamlı sembolik sabitler, fonksiyon prototipleri, inline fonksiyon tanımlamaları, "typedef" bildirimleri gibi "nesne yaratmayan" bildirimler bulunmalıdır. Başlık dosyalarında include korumasının yapılması unutulmamalıdır. Aşağıda kütüphane için bir başlık dosyası oluşturma örneği verilmiştir. * Örnek 1, Programımızı şağıdaki gibi derleyebilirsiniz: $ gcc -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.c $ gcc -o app app.c libmyutil.so.1.0.0 Sembolik bağlantı yoksa aşağıdaki gibi yaratabilirsiniz: ln -s libmyutil.so.1.0.0 libmyutil.so.1 İlgili programın kodları da şöyle olabilir: /* util.h */ #ifndef UTIL_H_ #define UTIL_H_ /* Function prototypes */ void foo(void); void bar(void); #endif /* libmyutil.c */ #include #include "util.h" void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } /* app.c */ #include #include "util.h" int main(void) { foo(); bar(); return 0; } * Örnek 2, C++'ta yazılmış kodların da kütüphane biçimine getirilmesinde farklı bir durum yoktur. Sınıfların bildirimleri başlık dosyalarında bulundurulur. Bunlar yine C++ derleyicisi ile ("g++" ya da "clang++") derlenir. Aynı biçimde kullanılır. Aşağıda C++'ta yazılmış olan bir sınıfın dinamik kütüphaneye yerleştirilmesi ve oradan kullanılmasına bir örnek verilmiştir. Derleme işlemlerini şöyle yapabilirsiniz: $ g++ -shared -fPIC -Wl,-soname,libmyutil.so.1 -o libmyutil.so.1.0.0 libmyutil.cpp $ g++ -o app app.cpp libmyutil.so.1.0.0 İlgili programın kodları da şöyle olabilir: // util.hpp #ifndef UTIL_HPP_ #define UTIL_HPP_ /* Function prototypes */ namespace CSD { class Date { public: Date() = default; Date(int day, int month, int year); void disp() const; private: int m_day; int m_month; int m_year; }; } #endif // libmyutil.cpp #include #include "util.hpp" namespace CSD { Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::disp() const { std::cout << m_day << '/' << m_month << '/' << m_year << std::endl; } } // app.cpp #include #include "util.hpp" using namespace CSD; int main() { Date d{10, 12, 2009}; d.disp(); return 0; } /*================================================================================================================================*/ (125_03_03_2024) & (126_10_03_2024) > "build" işlemleri ve "build automation tools" : Bir projeyi tek bir kaynak dosya biçiminde organize etmek iyi bir teknik değildir. Böylesi bir durumda dosyada küçük bir değişiklik yapıldığında bile tüm kaynak dosyanın yeniden derlenmesi gerekmektedir. Aynı zamanda bu biçim kodun güncellenmesini de zorlaştırmaktadır. Proje tek bir kaynak dosyada olduğu için bu durum grup çalışmasını da olumsuz yönde etkilemektedir. Bu nedenle projeler birden fazla "C ya da C++" kaynak dosyası biçiminde organize edilir. * Örnek 1.0, 10000 satırlık bir proje "app1.c, app2.c, app3.c, ..., app10.c" biçiminde 10 farklı kaynak dosya biçiminde oluşturulmuş olsun. Pekiyi "build" işlemi bu durumda nasıl yapılacaktır? "Build" işlemi için önce her dosya bağımısz olarak "-c" seçeneği ile derlenir ve ".o" uzantılı "amaç dosya (object module)" haline getirilir. Şöyleki: $ gcc -c app1.c $ gcc -c app2.c # gcc -c app3.c ... # gcc -c app10.c Sonra bu dosyalar link aşamasında birleştirilir. Şöyleki: # gcc -o app app1.o app2.o app3.o ... app10.o * Örnek 1.1, Bu çalışma biçiminde bir kaynak dosyada değişiklik yapıldığında yalnızca değişikliğin yapılmış olduğu kaynak dosya yeniden derlenir ancak "link" işlemine yine tüm amaç dosyalar dahil edilir. Yani "app3.c" üzerinde bir değişilik yapmış olalım: $ gcc -c app3.c $ gcc -o app app1.o app2.o app3.o ... app10.o İşte bu sıkıcı işlemi ortadan kaldırmak ve build işlemini otomatize etmek için "build otomasyon araçları (build automation tools)" denilen araçlar geliştirilmiştir. Bunların en eskisi ve yaygın olanı "make" isimli araçtır. "make" arasının yanı sıra "cmake" gibi "qmake" gibi daha yüksek seviyeli build araçları da zamanla geliştirilmiştir. "make" aracı pek çok sistemde benzer biçimde bulunmaktadır. Bugün UNIX/Linux sistemlerinde "GNU make" aracı kullanılmaktadır. Microsoft klasik make aracının "nmake" ismiyle başka versiyonunu geliştirmiştir. Ancak Microsoft uzun bir süredir "msbuild" denilen başka bir "build" sistemini kulanmaktadır. * Örnek 1, Microsoft'un Visual Studio IDE'si arka planda bu "msbuild" aracını kullanmaktadır. * Örnek 2, Qt Framework'ünde "qmake" isimli üst düzey make aracı kullanılmaktadır. * Örnek 3, Bazı IDE'ler "cmake" kullanmaktadır. En fazla kullanılan build otomasyon aracı "make" isimli araçtır. "make" aracını kullanmak için ismine "make dosyası" denilen bir dosya oluşturulur. Sonra bu dosya "make" isimli program ile işletilir. Dolayısıyla "make" aracının kullanılması için "make" dosyalarının nasıl oluşturulduğunun bilinmesi gerekir. "Make" dosyaları aslında kendine özgü bir dil ile oluşturulmaktadır. Bu "make" dilinin kendi sentaksı ve semantiği vardır. "make" aracı için çeşitli kitaplar ve öğretici dokümanlar "(tutorials)" oluşturulmuştur. Orijinal dokümanlarına aşağıdaki bağlantıdan erişilebilir: "https://www.gnu.org/software/make/manual/" Yukarıda da belirttiğimiz gibi "make" aracı değişik sistemlerde birbirine benzer biçimde bulunmaktadır. Microsoft'un make aracına "nmake" denilmektedir. GNU Projesi kapsamında bu "make" aracı yeniden yazılmıştır. Bugün ağırlıklı olarak GNU projesindeki "make" aracı kullanılmaktadır. Bu araca "GNU Make" de denilmektedir. Bir make dosyası "kurallardan (rules)" oluşmaktadır. Bir kuralın "(rule)" genel biçimi şöyledir: hedef (target) : ön_koşullar (prerequisites) işlemler (recipes) Örneğin: app: a.o b.o c.o gcc -o app a.o b.o c.o Burada "app" hedefi, "a.o b.o c.o" ön koşulları ve "gcc -o app a.o b.o c.o" satırı da "işlemleri (recipes)" belirtmektedir. Hedef genellikle bir tane olur. Ancak ön koşullar birden fazla olabilir. İşlemler tek bir satırdan oluşmak zorunda değildir. Eğer birden fazla satırdan oluşacaksa satırlar alt alta yazılır. İşlemler belirtilirken yukarıdaki satırdan bir "TAB" içeriye girinti verilmek zorundadır. Örneğin: app: a.o b.o c.o gcc -o app a.o b.o c.o Kuraldaki hedef ve ön koşullar tipik olarak birer dosyadır. Kuralın anlamı şöyledir: -> Ön koşullarda belirtilen dosyaların herhangi birinin tarih ve zamanı hedefte belirtilen dosyanın tarih ve zamanından ileri ise (yani bunlar güncellenmişse) bu durumda belirtilen işlemler yapılır. Yukarıdaki kuralı yeniden inceleyiniz: app: a.o b.o c.o gcc -o app a.o b.o c.o Burada eğer "a.o" ya da "b.o" ya da "c.o" dosyalarının tarih ve zamanı "app" dosyasının tarih ve zamanından ilerideyse aşağıdaki kabuk komutu çalıştırılacaktır: gcc -o app a.o b.o c.o Bu "link" işlemi anlamına gelir. "Link" işleminden sonra artık "app" dosyasının tarih ve zamanı ön koşul dosyalarından daha ileride olacağı için kural "güncel (up to date)" hale gelir. Artık bu kural işletildiğinde bu "link" işlemi yapılmayacaktır. Bu "link" işleminin yeniden yapılabilmesi için "a.o" ya da "b.o" ya da "c.o" dosyalarında güncelleme yapılmış olması gerekir. Bu dosyalar derleme işlem sonucunda oluşacağına göre bu dosyaların güncellenmesi aslında bunlara ilişkin ".c" dosyalarının derlenmesiyle olabilir. Şimdi aşağıdaki kuralları yazalım: a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c gcc -c c.c Bu kurallar "ilgili .c dosyalarında bir değişiklik olduğunda onları yeniden derle" anlamına gelmektedir. Şimdi önceki kuralla bu kuralları bir araya getirelim: app: a.o b.o c.o gcc -o app a.o b.o c.o a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c gcc -c c.c "make" programı çalıştırıldığında önce program "make" dosyasından hareketle bir "bağımlılık grafı (dependency graph)" oluşturmaktadır. Bağımlılık grafı "hangi dosya hangi dosyanın durumuna bağlı" biçimin de oluşturulan bir graftır. Yukarıdaki örnekte "a.o", "b.o" ve "c.o" dosyaları aşağıdaki kurallara bağımlıdır. Daha sonra "make" programı sırasıyla bu grafa uygun olarak aşağıdan yukarıya kuralları işletmektedir. Yukarıdaki örnekte birinci kural ikinci, üçüncü ve dördüncü kurallara bağımlıdır. Dolayısıyla önce bu kurallar işletilip daha sonra birinci kural işletilir. Böylece bu make dosyasından şöyle sonuç çıkmaktadır: -> "Herhangi bir .c dosya değiştirildiğinde onu derle ve hep birlikte link işlemi yap". Kuralın hedefindeki dosya yoksa koşulun sağlandığı kabul edilmektedir. Yani bu durumda ilgili işlemler yapılacaktır. Yukarıdaki örnekte "object dosyalarını silersek" bu durumda derleme işlemlerinin hepsi yapılacaktır. Normal olarak her ön koşul dosyasının bir bir hedefle ilişkili olması beklenir. Yani ön koşulda belirtilen dosyaların var olması gerekmektedir. make dosyası hazırlandıktan sonra make programı ile dosya işletilir. make programı işletilecek dosyayı "-f" ya da "--file" seçeneği ile komut satırı argümanından almaktadır. Örneğin: $ make -f project.mak Ancak "-f" seçeneği kullanılmazsa make programı sırasıyla "GNUmakefile", "makefile" ve "Makefile" dosyalarını aramaktadır. GNU dünyasındaki genel eğilim projenin make dosyasının "Makefile" biçiminde isimlendirilmesidir. Açık kaynak kodlu bir yazılımda projenin "make" dosyasının da verilmiş olması beklenir. Böylece kaynak kodları elde eden kişiler yeniden derlemeyi komut satırında "make" yazarak yapabilirler. Aslında "make" programı çalıştırılırken program belli bir hedefi gerçekleştirmek için işlem yapar. Gerçekleştirilecek hedef "make" programında komut satırı argümanı olarak verilmektedir. Eğer hedef belirtilmezse ilk hedef gerçekleştirilmeye çalıştırılır. Örneğin: # Makefile app: a.o b.o c.o gcc -o app a.o b.o c.o a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c gcc -c c.c project: project.c gcc -o project project.c Burada birbirinden bağımsız iki hedef vardır: "app" ve "project". Biz "make" programını hedef belirtmeden çalıştırırsak ilk hedef gerçekleştirilmeye çalışılır. Ancak belli bir hedefin de gerçekleştirilmesini sağlayabiliriz. Örneğin: $ make project Bir kuralda ön koşul yoksa kuralın sağlandığı varsayılmaktadır. Yani bu durumda doğrudan belirtilen işlemler "(recipes)" yapılır. Örneğin: clean: rm -f *.o Burada "make" programını aşağıdaki çalıştırmış olalım: $ make clean Bu durumda tüm ".o" dosyaları silinecektir. Örneğin: # Makefile app: a.o b.o c.o gcc -o app a.o b.o c.o a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c gcc -c c.c clean: rm -f *.o install: sudo cp app /usr/local/bin Burada "clean" hedefi "rebuild" işlemi için "object" dosyaları silmektedir. "install" hedefi ise elde edilen programı belli bir yere kopyalamaktadır. Bir kaynak dosya bir başlık dosyasını kullanıyorsa bağımlılıkta bu başlık dosyasının da belirtilmesi uygun olur. Çünkü bu başlık dosyasında bir güncelleme yapıldığında bu kaynak dosyanın da yeniden derlenmesi beklenir. Örneğin: a.o: a.c app.h gcc -c app.c Burada artık "app.h" dosyası üzerinde bir değişiklik yapıldığında derleme işlemi yeniden yapılacaktır. "make" dosyasına dışarıdan parametre aktarabiliriz. Bunun için komut satırında "değişken=değer" sentaksı kullanılmaktadır. Burada geçirilen "değer-${değişken}" ifadesi ile "make" dosyasının içerisinden kullanılabilir. Örneğin: ${executable}: a.o b.o c.o gcc -o app a.o b.o c.o a.o: a.c gcc -c a.c b.o: b.c gcc -c b.c c.o: c.c app.h gcc -c c.c clean: rm -f *.o install: sudo cp app /usr/local/bin Burada "executable" dosyanın hedefi komut satırından elde edilmektedir. Örneğin biz "make" programını şöyle çalıştırabiliriz: make executable=app Bu durumda app dosyası hede olarak ele alıacaktır. Make dosyası içerisinde değişkenler kullanılabilmektedir. Bir değişken "değişken = değer" sentaksıyla oluşturulur ve "make" dosyasının herhangi bir yerinde "${değişken}" biçiminde kullanılır. Örneğin: CC = gcc OBJECTS = a.o b.o c.o INSTALL_DIR = /usr/local/bin APP_NAME = app ${APP_NAME}: ${OBJECTS} ${CC} -o app a.o b.o c.o a.o: a.c ${CC} -c a.c b.o: b.c ${CC} -c b.c c.o: c.c app.h gcc -c c.c clean: rm -f ${OBJECTS} install: sudo cp ${APP_NAME} ${INSTALL_DIR} > Hatırlatıcı Notlar: >> O anda makinemizdeki işletim sistemi hakındaki bilgi uname komutuyla elde edilebilir. Bu komut -r ile kullanılırsa o makinede yüklü olan kernel versiyonu elde edilmektedir. Örneğin: $ uname -r 4.15.0-20-generic >> Bir C programını derlediğimizde "link" işlemi için bir "main" fonksiyonunun bulunması gerekmektedir. Aslında GNU'nun "linker" programının ismi "ld" isimli programdır. "gcc" zaten bu "ld linker" ını çalıştırmaktadır. Bir programın "link" edilebilmesi için aslında "main" fonksiyonunun bulunması gerekmez. "main" fonksiyonu "assembly" düzeyinde anlamlı geçerli anlamlı bir fonksiyon değildir. C için anlamlı bir fonksiyondur. Yani örneğin biz bir "assembly" programı yazarsak onu istediğimiz yerden çalışmaya başlatabiliriz. Bir dosya "executable" olarak "link" edilirken tek gerekli olan şey "entry point" denilen akışın başlatılacağı noktadır. "Entry point", "ld linker" ında "--entry" seçeneği ile belirtilmektedir. Biz bir C programını "gcc" ile derlediğimizde "gcc" aslında "ld linker" ını çağırırken ismine "start-up modüller" denilen bir grup modülü de link işlemine gizlice dahil etmektedir. Programın gerçek "entry point" i bu "start-up" modül içerisinde bir yerdedir. Aslında "main" fonksiyonunu bu "start-up" modül çağırmaktadır. Bu "start-up" modülün görevi birtakım hazırlık işlemlerini yapıp komut satırı argümanlarıyla "main" fonksiyonunu çağırmaktır. Zaten akış "main" fonksiyonunu bitirdiğinde yeniden "start-up" modüldeki koda döner ki orada "exit" yapılmıştır. "Start-up" modülün kodlarını şöyle süşünebilirsiniz: ... ... ... call main call exit O halde "link" aşamsında bu "start-up" modül katıldığı için aslında "main" isimli bir fonksiyon aranmaktadır. Yani "start-up" modül, "main" fonksiyonunu çağırmasaydı linker onu aramayacaktı. Biz aslında hiçbir kütüphaneyi "link" aşamasına dahil etmeden programın "entry-point" ini kendismiz belirleyerek akışı istediğimiz fonksiyondan başlatabiliriz. Tabii bu durumda sistem fonksiyonlarını bile sembolik makine dilinde ya da gcc'nin "inline" sembolik makine dilinde kendimizin yazması gerekecektir. Aşağıda böyle bir örnek verilmiştir. Buradaki programın isminin "x.c" olduğunu varsayalım. Bu programı aşağıdaki gibi derleyip link edebilirsiniz: $ gcc -c x.c $ ld -o x x.o --entry=foo Programın kodu da aşağıdaki gibi olabilir: #include #include #include ssize_t my_write(int fd, const void *buf, size_t size) { register int64_t rax __asm__ ("rax") = 1; register int rdi __asm__ ("rdi") = fd; register const void *rsi __asm__ ("rsi") = buf; register size_t rdx __asm__ ("rdx") = size; __asm__ __volatile__ ( "syscall" : "+r" (rax) : "r" (rdi), "r" (rsi), "r" (rdx) : "rcx", "r11", "memory" ); return rax; } void my_exit(int status) { __asm__ __volatile__ ( "movl $60, %%eax\n\t" "movl %0, %%edi\n\t" "syscall" : : "g" (status) : "eax", "edi", "cc" ); } void foo() { my_write(1, "this is a test\n", 15); my_exit(0); } /*================================================================================================================================*/ (127_15_03_2024) & (128_17_03_2024) & (129_22_03_2024) & (130_24_03_2024) & (131_29_03_2024) & (132_05_04_2024) & (133_07_04_2024) (134_19_04_2024) & (135_21_04_2024) & (136_26_04_2024) & (137_28_04_2024) & (138_03_05_2024) & (139_05_05_2024) & (140_10_05_2024) (141_12_05_2024) & (142_24_05_2024) & (143_26_05_2024) & (144_31_05_2024) & (145_02_06_2024) & (146_07_06_2024) & (147_09_06_2024) (148_16_06_2024) & (149_23_06_2024) > "kernel modules" : Kernel'ın bir parçası gibi işlev gören, herhangi bir koruma engeline takılmayan, kernel modda çalışan özel olarak hazırlanmış modüllere (yani kod parçalarına) Linux dünyasında "kernel modülleri (kernel modules)" denilmektedir. Diğer yandan kernel modülleri eğer kesme gibi bazı mekanizmaları kullanıyorsa ve bir donanım aygıtını yönetme iddiasındaysa bunlara özel olarak "aygıt sürücüleri (device drivers)" da denilmektedir. Nasıl bir masaüstü bilgisayara kart taktığımızda artık o kart donanımın bir parçası haline geliyorsa, "kernel" modülleri ve aygıt sürücüleri de "install" edildiklerinde adeta "kernel" ın bir parçası haqline gelmektedir. Bu yüzdendir ki, -> Her aygıt sürücü bir "kernel" modülüdür ancak her kernel modülü bir aygıt sürücü değildir. Bu nedenle biz yalnızca "kernel modülü" dediğimizde genel olarak aygıt sürücüleri de dahil etmiş olacağız. Biz bu bölümde Linux sistemleri için "kernel" modüllerinin ve aygıt sürücülerinin nasıl yazılacağını ele alacağız. "kernel" modülleri ve aygıt sürücüler genel bir konu değildir. Her işletim sisteminin o sisteme özgü bir aygıt sürücü mimarisi vardır. Hatta bu mimari işletim sisteminin versiyonundan versiyonuna da değişebilmektedir. Bu nedenle aygıt sürücü yazmak genel bir konu değil, o işletim sistemine hatta işletim sisteminin belirli versiyonlarına özgü bir konudur. "kernel" modüllerinde ve aygıt sürücülerde her türlü fonksiyon kullanılamaz. Bunları yazabilmek için özel başlık dosyalarına ve kütüphanelere gereksinim duyulmaktadır. Bu nedenle ilk yapılacak şey bu başlık dosyalarının ve kütüphanelerin ilgili sisteme yüklenmesidir. Genellikle bir Linux sistemini yüklediğimizde zaten "kernel" modüllerini ve aygıt sürücüleri oluşturabilmek için gereken kütüphaneler ve başlık dosyaları zaten yüklü biçimde bulunmaktadır. Tabii programcı "kernel" kodlarını da kendi makinesine indirmek isteyebilir. Bunun için aşağıdaki komut kullanılabilir: "sudo apt-get install linux-source" Eğer sisteminizde Linux'un kaynak kodları yüklü ise bu kaynak kodlar "/usr/src" dizininde bulunmaktadır. Bu dizindeki "linux-headers-$(uname -r)" dizini, kaynak kodlar yüklü olmasa bile bulunan bir dizindir ve bu dizin çekirdek modülleri ve aygıt sürücülerin "build edilmeleri" için gereken başlık dosyalarını barındırmaktadır. Benzer biçimde "/lib/modules" isimli dizinde "$(uname -r)" isimli bir dizin vardır. Bu dizin "kernel" modüllerinin "build" edilmesi için gereken bazı kodları ve kütüphaneleri bulundurmaktadır. Özetle "kernel" modülleri ve aygıt sürü yazmak için gerekli olan dizinler şunlardır: -> "linux-headers-$(uname -r)" dizini. Bu dizinde başlık dosyaları bulunmaktadır. -> "/lib/modules/$(uname -r)" dizini. Burada da "kernel" modülün "build" edilmesi için gerekli olan dosyalar ve kütüphaneler bulunmaktadır. Bir "kernel" modülünde biz "user-mod" için yazılmış kodları kullanamayız. Çünkü orası ayrı bir dünyadır. Ayrıca biz "kernel" modüllerinde "kernel" içerisindeki her fonksiyonu kullanamayız. Yalnızca bazı fonksiyonları kullanabiliriz. Bunlara "kernel tarafından export edilmiş fonksiyonlar" denilmektedir. "kernel" tarafından "export" edilmiş fonksiyon kavramının, "sistem fonksiyonu" kavramından, bir ilgisi yoktur. Sistem fonksiyonları "user-mod" dan çağrılmak üzere tasarlanmış ayrı bir grup fonksiyondur. Oysa "kernel" tarafından "export" edilmiş fonksiyonlar "user-mod" dan çağrılamazlar. Yalnızca "kernel" modüllerinden çağrılabilirler. Buradan çıkan sonuç şudur: -> Bir "kernel" modül yazılırken ancak "kernel" ın "export" ettiği fonksiyon ve veriler kullanılabilmektedir. Tabii "kernel" ın kaynak kodları çok büyüktür ancak buradaki kısıtlı sayıda fonksiyon "export" edilmiştir. Benzer biçimde programcının oluşturduğu bir "kernel" modül içerisindeki belli fonksiyonları da programcı "export" edebilir. Bu durumda bu fonksiyonlar da başka "kernel" modüllerinden kullanılabilirler. O halde özetle: -> Kernel modülleri yalnızca kernel içerisindeki export edilmiş fonksiyonları kullanabilirler. -> Kendi "kernel" modülümüzde istediğimiz bir fonksiyonu "export" edebiliriz. Bu durumda bizim "kernel" modülümüz "kernel" ın bir parçası haline geldiğine göre başka "kernel" modüller de bizim "export" ettiğimiz fonksiyonları kullanabilir. Mademki "kernel" modüller işletim sisteminin "kernel" kodlarındaki fonksiyon ve verileri kullanabiliyorlar o zaman "kernel" modüller o anda çalışılan "kernel" ın yapısına da bağlı durumdadırlar. Yukarıda da ifade ettiğimiz gibi işletim sistemlerinde "kernel modül yazmak" ya da "aygıt sürücü yazmak" biçiminde genel bir konu yoktur. Her işletim sisteminin "kernel" modül ve aygıt sürücü mimarisi diğerlerinden farklıdır. Dolayısıyla bu konu spesifik bir işletim sistemi için geçerli olabilecek oldukça platform bağımlı bir konudur. Hatta işletim sistemlerinde bazı versiyonlarda genel aygıt sürücü mimarisi bile değiştirilebilmektedir. Dolayısıyla eski aygıt sürücüler yeni versiyonlarda çalışamamakta yenileri de eski versiyonlarda çalışamamaktadır. "Kernel" modüllerinin ve aygıt sürücülerin yazımı için programcının genel olarak "kernel" yapısını bilmesi gerekmektedir. Çünkü bunları yazarken "kernel" ın içerisindeki "export" edilmiş fonksiyonlar kullanılmaktadır. Linux "kernel" modüller ve aygıt sürücüler hakkında yazılmış birkaç kitap vardır. Bunların en klasik olanı "Linux Device Drivers (3. Edition)" kitabıdır. Bu konudaki resmi dokümanlar "kernel.org" sitesindeki "documentation" kısmında bulunmaktadır. Bir "kernel" modülünü derlemek ve "link" etmek maalesef sanıldığından daha zordur. Her ne kadar "kernel" modüller ELF "object" dosyaları biçimindeyse de bunlarda özel bazı "bölümler (sections)" bulunmaktadır. Dolayısıyla bu modüllerin derlenmesinde özel "gcc" seçenekleri devreye sokulur. "Kernel" modüllerin "link" edilmeleri de bazı kütüphane dosyalarının devreye sokulmasıyla yapılmaktadır. Dolayısıyla bir "kernel" modülün "manuel" biçimde "build edilmesi" için bazı ayrıntıı bilgilere gereksinim duyulmaktadır. İşte "kernel" tasarımcıları bu sıkıcı işlemleri kolaylaştırmak için özel "make dosyaları" düzenlemişlerdir. Programcı bu "make" dosyalarından faydalanarak "build" işlemini çok daha kolay yapabilmektedir. * Örnek 1, "Kernel" modüller için "build" işlemini yapan örnek bir "Makefile" aşağıdaki gibi olabilir: obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Burada önce "/lib/modules/$(uname -r)/build" dizinindeki "Makefile" çalıştırılmış ondan sonra çalışma bu yazdığımız "make" dosyasından devam ettirilmiştir. Özetle bu make dosyası "generic.c" isimli dosyanın derlenerek "kernel" modül biçiminde "link" edilmesini sağlamaktadır. "Kernel" modül birden fazla kaynak dosyadan oluşturulabilir. Bu durumda ilk satır şöyle oluşturulabilir: "obj-m += a.o b.o c.o... " Ya da bu belirtme işlemi ayrı satır halinde de yapılabilir: "obj-m += a.o" "obj-m += b.o" "obj-m += c.o" ... Bizim oluştuduğumuz Makefile dosyasındaki "all" hedefine dikkat ediniz: "make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules" "make" programının "-C" seçeneği "Makefile" dosyasını aramadan önce bu seçeneğin argümanında belirtilen dizine geçiş yapmaktadır. Dolayısıyla aslında yukarıdaki satırla "/lib/modules/$(shell uname -r)/build" dizinindeki "Makefile" dosyası çalıştırılacaktır. "Make" dosyasının başındaki kısım aslında standart bir "make" yönergesi değildir. Ana "make" dosyası bu dosyayı ele almaktadır. Tipik olarak bir "Makefile" dosyasının içeriği de aşağıdaki gibidir: # Makefile obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean * Örnek 2, Tabii aslında "make" dosyası parametrik biçimde de oluşturabilmektedir. Bu durumda "make" programı çalıştırılıken bu parametrenin değeri de belirtilmelidir. Örneğin: make file=hellomodule Bu durumda "Makefile" dosyamızın içeriği de aşağıdaki gibi olacaktır: # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Şimdi en basit bir kernel modülü oluştutup bunu bir başlangıç noktası olarak kullanalım. Bu modülümüze "helloworld" ismini verelim. Bu kernel modül aşağıdaki gibi build edilebilir: "$ make file=helloworld" "Build" işlemi bittiğinde "kernel" modül "helloworld.ko" dosyası biçiminde oluşturulacaktır. Burada "ko" uzantısı "kernel object" sözcüklerinden kısaltılmıştır. Bir "kernel" modül, "kernel" ın içerisine "insmod" isimli programla yerleştirilmektedir. Tabii bu programın "sudo" ile "root" önceliğinde çalıştırılması gerekmektedir. Örneğin, "$ sudo insmod helloworld.ko" Artık "kernel" modülümüz "kernel" ın içerisine yerleştirilmiştir. Yani modülümüz adeta "kernel" ın bir parçası gibi işlev görecektir. "Kernel" modüller istenildiği zaman "rmmod" isimli programla "kernel" dan çıkartılabilirler. Bu programın da yine "sudo" ile "root" önceliğinde çalıştırılması gerekir. Örneğin: "$ sudo rmmod helloworld.ko" Aşağıda bu konuya ilişkin bir örnek verilmiştir. * Örnek 1, "make" işlemi şöyle yapılabilir: "$ make file=helloworld" Dosyaların içerikleri ise şöyle olabilir: # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include MODULE_LICENSE("GPL"); int init_module(void) { printk(KERN_INFO "Hello World...\n"); return 0; } void cleanup_module(void) { printk(KERN_INFO "Goodbye World...\n"); } En basit bir kernel modülde aşağıdaki iki temel dosya include edilmelidir: "#include " "#include " Bu iki dosya "/lib/modules/$(uname -r)/build/include" dizini içerisindedir. (Yani "libc" ve POSIX kütüphanelerinin başlık dosyalarının bulunduğu "/usr/include" içerisinde değildir.) Yukarıda kullandığımız "make" dosyası "include" dosyalarının bu dizinde aranmasını sağlamaktadır. Eskiden "kernel" modüllerine modül lisansının eklenmesi zorunlu değildir. Ancak belli bir süreden sonra bu zorunlu hale getirilmiştir. Modül lisansı "MODULE_LICENSE" isimli makro ile belirtilmektedir. Bu makro "" dosyası içerisinde bildirilmiştir. Tipik modül lisansı aşağıdaki gibi "GPL" biçiminde oluşturulabilir: MODULE_LICENSE("GPL"); Bir "kernel" modül yüklendiğinde "kernel" modül içerisinde belirlenmiş olan bir fonksiyon çağrılır (bu fonksiyon C++'taki "constructor" gibi düşünülebilir.) "Default" çağrılacak fonksiyonun ismi `init_module` biiçimindedir. Bu fonksiyonun geri dönüş değeri "int" türdendir ve parametresi yoktur. Fonksiyon başarı durumunda "0" değerine başarısızlık durumunda negatif hata koduna geri dönmelidir. Bu fonksiyon başarısızlıkla geri dönerse modülün yüklenmesinden vazgeçilmektedir. Benzer biçimde bir modül "kernel" alanından boşaltılırken de yine bir fonksiyon çağrılmaktadır. (Bu fonksiyon da C++'taki "destructor" gibi üşünülebilir.) "Default" çağrılacak fonksiyonun ismi "cleanup_module" biçimindedir. Bu fonksiyonun geri dönüş değeri ve parametresi "void" biçimdedir. "Kernel" modüller tıpkı daha önce görmüş olduğumuz "deamon" lar gibi ekrana değil "log" dosyalarına yazarlar. Bunun için "kernel" içindeki "printk" isimli fonksiyon kullanılmaktadır. Yukarıdaki "helloworld" modülünde kullanmış olduğumuz "printk" fonksiyonu "'kernel' ın 'printf' fonksiyonu" gibi düşünülebilir. "printk" fonksiyonun genel kullanımı "printf" fonksiyonu gibidir. "Default" durumda bu fonksiyon mesajların "/var/log/syslog" dosyasına yazdırılması sağlamaktadır. "printk" fonksiyonun prototipi "" dosyası içerisindedir. "printk" fonksiyonun örnek kullanımı şöyledir: printk(KERN_INFO "This is test\n"); Mesajın solundaki "KERN_XXX" biçimindeki makrolar aslında bir "string" açımı yapmaktadır. Dolayısıyla yan yana iki "string" birleştirildiği için mesaj yazısının başında küçük bir önek bulunur. Bu örnek (yani bu makro) mesajın türünü ve aciliyetini belirtmektedir. Tipik "KERN_XXX" makroları şunlardır: "KERN_EMERG" "KERN_ALERT" "KERN_CRIT" "KERN_ERR" "KERN_WARN" "KERN_NOTICE" "KERN_INFO" "KERN_DEBUG" Bu makroların tipik yazım biçimi şöyledir: #define KERN_EMERG "<0>" #define KERN_ALERT "<1>" #define KERN_CRIT "<2>" #define KERN_ERR "<3>" #define KERN_WARNING "<4>" #define KERN_NOTICE "<5>" #define KERN_INFO "<6>" #define KERN_DEBUG "<7>" Ancak bu makrolar da çeşitli "kernel" versiyonlarında değişiklikler yapılabilmektedir. Aslında "KERN_XXX" makroları ile "printk" fonksiyonunu kullanmak yerine "pr_xxx" makroları da kullanılabilir. Şöyle ki: "printk(KERN_INFO "Hello World...\n");" ile "pr_info("Hello World...\n");" tamamen eşdeğerdir. Diğer "pr_xxx" makroları şunlardır: "pr_emerg" "pr_alert" "pr_crit" "pr_err" "pr_warning" "pr_notice" "pr_info" "pr_debug" "printk" fonksiyonun yazdıklarını "/var/log/syslog" dosyasına bakarak görebiliriz. Örneğin: "tail /var/log/syslog" Ya da "dmesg" programı ile de aynı bilgi elde edilebilir. "Kernel" modüller "kernel" ın içerisine yerleştirildiği için "kernel" modüllerde biz "user-mode" daki kütüphaneleri kullanamayız. Örneğin, "kernel" mode içerisinde Standart C fonksiyonlarını ve POSIX fonksiyonlarını kullanamayız. Çünkü Standart C fonksiyonları ve POSIX foksiyonları "user-mode" programlar için oluşturulmuş kütüphanelerin içerisindedir. Biz "kernel" modüllerin içerisinde yalnızca "export edilmiş kernel fonksiyonlarını" kullanabiliriz. "Kernel" modüller içerisinde kullanılabilecek "export" edilmiş "kernel" fonksiyonları "Linux Kernel API" ismi altında "kernel.org" tarafından dokümante edilmiştir. Bu fonksiyonların dokğmantasyonuna aşağıdaki bağlantıdan erişebilirsiniz: https://docs.kernel.org/core-api/kernel-api.html Aslında "init_module" ve "cleanup_module" fonksiyonlarının ismi değiştirilebilir. Fakat bunun için bildirimde bulunmak gerekir. Bildirimde bulunmak için ise "module_init(...)" ve "modeul_exit(...)" makroları kullanılmaktadır. Bu makrolar kaynak kodun herhangi bir yerinde bulundurulabilir. Ancak makro içerisinde belirtilen fonksiyonların daha yukarıda bildirilmiş olması gerekmektedir. Bu makrolar tipik olarak kaynak kodun sonuna yerleştirilmektedir. * Örnek 1, "make" işlemi şöyle yapılabilir: "$ make file=helloworld" # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include int helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } void helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); * Örnek 2, Genellikle "kernel" modül içerisindeki global değişkenlerin ve fonksiyonların "internal linkage" yapılması tercih edilmektedir. Bu durum birtakım isim çakışmalarını da engelleyecektir. "make" işlemi şöyle yapılabilir: "$ make file=helloworld" # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include static int helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } static void helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); * Örnek 3, "Kernel" modüllerde "init" ve "cleanup" fonksiyonlarında fonksiyon isimlerinin soluna "__init" ve "__exit" makroları getirilebilmektedir. Bu makrolar "" dosyası içerisindedir. Bu dosya da "" dosyası içerisinden "include" edilmiştir. "__init" makrosu ilgili fonksiyonu ELF dosyasının özel bir bölümüne ("section") yerleştirir. Modül yüklendikten sonra bu bölüm "kernel" alanından atılmaktadır. "__exit" makrosu ise "kernel" ın içine gömülmüş modüllerde fonksiyonun dikkate alınmayacağını (dolayısıyla hiç yüklenmeyeceğini) belirtir. Ancak sonradan yüklemelerde bu makronun bir etkisi yoktur. "make" işlemi şöyle yapılabilir: "$ make file=helloworld" # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include static int __init helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } static void __exit helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); Nasıl "user-mode" programlarda "main" fonksiyonuna komut satırı argümanları geçirilebiliyorsa benzer biçimde kernel modüllere de argüman (ya da parametre diyebiliriz) geçirilebilmektedir. Bu konuya genel olarak "kernel modül parametreleri" denilmektedir. "Kernel" modüllere parametre geçirme işlemi "insmod" ile modül yüklenirken komut satırında modül isminden sonra "değişken=değer" çiftleriyle yapılmaktadır. Örneğin: "sudo insmod helloworld.ko number=10 msg="\"This is a test\"" values=10,20,30,40,50" Bu örnekte "number" parametresi "int" bir değerden, "msg" parametresi ise bir yazıdan oluşmaktadır. "values" parametresi birden fazla "int" değerden oluşmaktadır. Bu tür parametrelere modülün dizi parametreleri denilmektedir. "Kernel" modüllere geçirilen parametreleri modül içerisinde almak için "module_param" ve "module_param_array" isimli makrolar kullanılır. Bu makrolardan, >> "module_param" makrosunun üç parametresi vardır: "module_param(name, type, perm)" "name" parametresi ilgili değişkenin ismini belirtmektedir. Biz makroyu çağırmadan önce bu isimde bir global değişkeni tanımlamalıyız. Ancak buradaki değişken isminin komut satırında verilen parametre (argüman da diyebiliriz) ismi ile aynı olması gerekmektedir. "type" ilgili parametrenin türünü belirtir. Bu tür şunlardan biri olabilir: "int" "long" "short" "uint" "ulong" "ushort" "charp" "bool" "invbool" Buradaki "charp" "char" türden adresi, "invbool" ise geçirilen argümanın "bool" bakımdan tersini temsil etmektedir. "module_param" makrosunun "perm" parametresi "/sys/modules/" dizininde yaratılacak olan "parameters" dizininin erişim haklarını belirtir. Bu makrolar global alanda herhangi bir yere yerleştirilebilir. * Örnek 1, "kernel" modülümüzde "count" ve "msg" isimli iki parametre olsun. Bunlara ilişkin "module_param" makroları şöyşe oluşturumalıdır: int count = 0; char *msg = "Ok"; module_param(count, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param(msg, charp, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); "char *" türünden modül parametresi için makrodaki türün "charp" biçiminde olduğuna dikkat ediniz. Buradaki gösterici "const" olamamaktadır. Bizim bir parametre için "module_param" makrosunu kullanmış olmamız modül yüklenirken bu parametrenin belirtilmesini zorunlu hale getirmemektedir. Bu durumda bu parametreler "default" değerlerde kalacaktır. Yukarıdaki parametreleri "helloworld" modülüne aşağıdaki gibi geçirebiliriz: "$ sudo insmod helloworld.ko count=100 msg="\"this is a test\""" Burada neden iç içe tırnakların kullanıldığını merak edebilirsiniz. Şöyleki; -> "shell" programı üzerinde tırnaklar "boşluklarla ayrılmış olan yazıların tek bir komut satırı argümanı olarak ele alınacağını belirtmektedir. Ancak bizim ayrıca yazısal argümanları modüllere parametre yoluyla aktarırken onları tırnaklalamız gerekir. Bu nedenle iç içe iki tırnak kullanılmıştır. >> "Kernel" modüle birden fazla değer de bir dizi gibi aktarılabilir. Bunun için "module_param_array" makrosu kullanılmaktadır. "module_param_array" makrosu da şöyledir: "module_param_array(name, type, nump, perm)" Makronun birinci ve ikinci parametreleri yine değişken ismi ve türünü belirtir. Tabii buradaki değişken isminin bir dizi ismi olarak girilmesi gerekmektedir. Üçüncü parametre toplam kaç değerin modüle dizi biçiminde aktarıldığını belirten "int" bir nesnenin adresini (ismini değil) alır. Son parametre yine oluşturulacak dosyanın erişim haklarını belirtmektedir. * Örnek 1, static int values[5]; static int size; module_param_array(values, int, &size, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param_array makrosuyla bir diziye değer aktarırken değerlerin virgüllerle ayrılmış bir biçimde girilmesi gerekmektedir. Şöyleki; "sudo insmod helloworld.ko values=1,2,3,4,5" Burada eğer verilen değerler dizinin uzunluğundan fazla olursa zaten modül yüklenmemektedir. Bu örnekte biz girilen değerlerin sayısını "size" nesnesinden alabiliriz. Aşağıdaki örnekte üç parametre komut satırından "kernel" modüle geçirilmiştir. Komut satırındaki isimlerle programın içerisindeki değişken isimlerinin aynı olması gerektiğine dikkat ediniz. Yazıların geçirilmesinde iki tırnaklar kullanılır. Dizi geçirirken yanlışlıkla virgüllerin arasına boşluk karakterleri yerleştirmeyiniz. * Örnek 1, Programu şöyle make yapabilirsiniz: "make file=helloworld" Yüklemeyi şöyle yapabilirsiniz: sudo insmod helloworld.ko count=100 msg="\"this is a test\"" values=1,2,3,4,5 # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* helloworld.c */ #include #include MODULE_LICENSE("GPL"); static int count = 0; static char *msg = "Ok"; static int values[5]; static int size; module_param(count, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param(msg, charp, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); module_param_array(values, int, &size, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init helloworld_init(void) { int i; printk(KERN_INFO "Hello World...\n"); printk(KERN_INFO "count = %d\n", count); printk(KERN_INFO "msg = %s\n", msg); for (i = 0; i < size; ++i) { printk(KERN_INFO "%d\n", values[i]); } return 0; } static void __exit helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); Modül parametreleri kernel tarafından "/sys/module" içerisindeki modül ismine ilişkin dizinin altındaki "parameters" dizininde dosyalar biçiminde dış dünyaya sunulmaktadır. İşte makrodaki erişim hakları buradaki parametre dosyalarının erişim haklarını belirtmektedir. "Kernel" modül "root" kullanıcısı tarafından yüklendiğine göre bu dosyaların da Kullanıcı ID ve Grup ID değerleri "root" olacaktır. * Örnek 1, Yukarıdaki "helloworld" modülü için bu dosyalar "/sys/module/helloworld/parameters" dizini içerisinde aşağıdaki gibidir: "$ ls -l /sys/module/helloworld/parameters" toplam 0 -rw-r--r-- 1 root root 4096 Mar 22 22:24 count -rw-r--r-- 1 root root 4096 Mar 22 22:24 msg Bu dosyalar doğrudan kernel modüldeki parametre değişkenlerini temsil etmektedir. Yani örneğin biz buradaki "count" dosyasına başka bir değer yazdığımızda "kernel" modülümüzdeki "count" değeri de değişmiş olacaktır. Tabii yukarıdaki erişim haklarıyla biz dosyaya yazma yapamayız. Bu erişim haklarıyla yazma yapabilmemiz için yazmayı yapan programın "root" olması gerekir. Terminalden bu işlem aşağıdaki gibi yapılabilir: $ sudo bash -c "echo 200 > /sys/module/helloworld/parameters/count" Burada işlemi aşağıdaki gibi yapamayacağımıza dikkat ediniz: $ sudo echo 200 > /sys/module/helloworld/parameters/count Çünkü burada her ne kadar "echo" programı "root" önceliğinde çalıştırılıyorsa da dosyayı açan kullanıcı "root" değildir. Linux'ta bir "kernel" modül artık "user-mode" tan kullanılabilir hale getirildiyse buna "aygıt sürücü (device driver)" denilmektedir. Aygıt sürücüler "open" fonksiyonuyla bir dosya gibi açılırlar. Bu açma işleminden bir dosya betimleyicisi elde edilir. Bu dosya betimleyicisi "read", "write", "lseek", "close" gibi fonksiyonlarda kullanılabilir. Aygıt sürücülere ilişkin dosya betimleyicileri bu fonksiyonlarla kullanıldığında aygıt sürücü içerisindeki belirlenen bazı fonksiyonlar çağrılmaktadır. Yani tersten gidersek biz örneğin aygıt sürücümüze ilişkin dosya betimleyicisi ile "read" ve "write" fonksiyonu çağrıldığığımızda aslında aygıt sürücümüzdeki belli fonksiyonlar çalışırılmaktadır. Böylece aygıt sürücü ile "user-mod" arasında veri transferleri yine dosyalarda olduğu gibi dosya fonksiyonlarıyla yapılabilmektedir. Pekiyi "user-mode" tan aygıt sürücümüzdeki herhangi bir fonksiyonu da çağırabilir miyiz? Yanıt evet. Bunun için "ioctl" isimli bir POSIX fonksiyonu (tabii bu POSIX fonksiyonu "sys_ioctl" isimli sistem fonksiyonunu çağırmaktadır) kullanılmaktadır. Aygıt sürücü içerisinde fonksiyonlara birer kod numarası atanır. Sonra ioctl fonksiyonunda bu kod numarası belirtilir. Böylece akış "user-mod" tan "kernel-moda" geçerek belirlenen fonksiyonu kernel modda çalıştıracaktır. Özetle; -> Bir aygıt sürücüsü "kernel-modda" çalışmaktadır. -> Biz aygıt sürücüsünü "open" fonksiyonu ile açıp elde ettiğimiz betimleyici ile "read", "write", "lseek" ve "close" fonksiyonlarını çağırdığımızda aygıt sürücüsü içerisindeki ilgili fonksiyon çalıştırılmaktadır. -> Aygıt sürücüsü içerisindeki herhangi bir fonksiyon ise "user-mode" tan "ioctl" isimli fonksiyonla çalıştırılmaktadır. Tabii "user mode" programlar aygıt sürücü içerisindeki kodları "read", "write", "lseek", "close", "ioctl" gibi fonksiyonlar yoluyla çalıştırdıklarında proses, "user-mode" tan geçici süre "kernel-mode"'a geçer, ilgili kodlar "kernel-mode" da koruma engeline takılmadan çalıştırılır. Fonksiyonların çalışması bittiğinde proses yine "user-mode" da döner. Pekiyi aygıt sürücüleri açmak için "open" fonksiyonunda yol ifadesi olarak (yani dosya ismi olarak) ne verilecektir? İşte aygıt sürücüler dosya sisteminde bir dizin girişiyle temsil edilmektedir. O dizin girişi "open" ile açıldığında aslında o dizin girişinin temsil ettiği aygıt sürücü açılmış olur. Bu biçimdeki aygıt srücüleri temsil eden dizin girişlerine "aygıt dosyaları (device files)" denilmektedir. Aygıt dosyaları diskte bir dosya belirtmemektedir. "Kernel" içerisindeki aygıt sürücüyü temsil eden bir dizin girişi belirtmektedir. Aygıt dosyalarının "i-node" tablosunda bir "i-node" elemanı vardır ancak bu "i-node" elemanı diskte bir yer değil "kernel" da bir aygıt sürücü belirtmektedir. Pekiyi bir aygıt dosyası nasıl yaratılmaktadır ve nasıl bir aygıt sürücüyü temsil eder hale getirilmektedir? İşte her aygıt sürücünün majör ve minör numaraları vardır. Aynı zamanda aygıt dosyalarının da majör ve minör numaraları vardır. Bir aygıt sürücünün majör ve minör numarası bir aygıt dosyasının majör ve minör numarasıyla aynıysa bu durumda o aygıt dosyası o aygıt sürücüyü temsil eder. Aygıt dosyaları özel dosyalardır. Bir dosyanın aygıt dosyası olup olmadığı "ls -l" komutunda dosya türü olarak 'c' (karakter aygıt sürücüsü) ya da 'b' (blok aygıt sürücüsü) ile temsil edilmektedir. Anımsanacağı gibi dosya bilgileri "stat", "fstat", "lstat" fonksiyonlarıyla elde ediliyordu. İşte "struct stat" yapısının "dev_t" türünden "st_rdev" elemanı eğer dosya bir aygıt dosyasıysa dosyanın majör ve minör numaralarını belirtir. Biz de "" dosyasındaki "S_ISCHR" ve "S_ISBLK" makrolarıyla ilgili dosyanın bir aygıt dosyası olup olmadığını öğrenebiliriz. Yukarıda da belirttiğimiz gibi aygıt sürücüler "karakter aygıt sürücüleri (character device driver)" ve "blok aygıt sürücüleri (block device driver)" olmak üzere ikiye ayrılmaktadır. Karakter aygıt sürücüleri daha yaygın kullanılmaktadır. Biz kursumuzda önce karakter aygıt sürücülerini sonra blok aygıt sürücülerini ele alacağız. O halde şimdi bizim bir aygıt dosyasını nasıl oluşturacağımızı ve aygıt sürücüye nasıl majör ve minör numara atayacağımızı bilmemiz gerekir. Aygıt dosyaları "mknod" isimli POSIX fonksiyonuyla (bu fonksiyon Linux'ta doğrudan "sys_node" isimli sistem fonksiyonunu çağırmaktadır) ya da komut satırından "mknod" komutuyla (bu komut da "mknod" fonksiyonu ile işlemini yapmaktadır) yaratılabilir. "mknod" fonksiyonun prototipi şöyledir: #include int mknod(const char *pathname, mode_t mode, dev_t dev); Fonksiyonun birinci parametresi yaratılacak aygıt dosyasının yol ifadesini, ikinci parametresi erişim haklarını ve üçüncü parametresi de aygıt dosyasının majör ve minör numaralarını belirtmektedir. Aygıt dosyasının majör ve minör numaraları "dev_t" türünden tek bir değer ile belirtilmektedir. "dev_t" türü POSIX standartlarına göre herhangi bir tamsayı türü olabilmektedir. Biz majör ve minör numaraları user mod programlarda "makedev" isimli makroyla oluştururuz. Bir "dev_t" türünden değerin içerisinden major numarayı almak için major makrosu, minor numarayı almak için ise minor makrosu bulunmaktadır: #include dev_t makedev(unsigned int maj, unsigned int min); unsigned int major(dev_t dev); unsigned int minor(dev_t dev); Yani aslında majör ve minör numaralar "dev_t" türünden bir bir değerin belli bitlerinde bulunmaktadır. Ancak bu numaraların "dev_t" türünden değerin hangi bitlerinde bulunduğu sistemden sisteme değişebileceği için bu makrolar kullanılmaktadır. Ancak "kernel" modda bu makrolar yerine aşağıdakiler kullanılmaktadır: #include MKDEV(major, minor) MAJOR(dev) MINOR(dev) Linux'ta son versiyonlar da dikkate alındığında "dev_t" türü "32-bit" lik işaretsiz bir tamsayı türündendir. Bu "32-bit" in yüksek anlamlı 12 biti majör numarayı, düşük anlamlı 20 biti ise minör numarayı temsil etmektedir. Ancak programcı bu varsayımlarla kodunu düzenlememeli, yukarıda belirtilen makroları kullanmalıdır. "mknod" fonksiyonun ikinci parametresindeki erişim haklarına aygıt dosyasının türünü belirten aşağıdaki sembolik sabitlerden biri de "bit-wise OR" operatörü ile eklenmelidir: S_IFCHR (Karakter aygıt sürücüsü) S_IFBLK (Blok aygıt sürücüsü) Aslında "mknod" fonksiyonu ile Linux sistemlerinde isimli boru dosyaları, UNIX domain soket dosyaları ve hatta normal dosyalar da yaratılabilmektedir. Bu durumda fonksiyonun aygıt numarası belirten üçüncü parametresi fonksiyon tarafından dikkate alınmamaktadır. Bu özel dosyalar için erişim haklarına eklenecek makrolar da şunlardır: S_IFREG (Disk dosyası yaratmak için) S_IFIFO (İsimli boru dosyası yaratmak için) S_IFSOCK (UNIX domain soket dosyası yaratmak için) Aslında "mknod" fonksiyonu aygıt dosyaları yaratmak için kullanılıyor olsa da yukarıda belirttiğimiz özel dosyaları da yaratabilmektedir. Tabii zaten isimli boru dosyasını yaratmak için mkfifo fonksiyonu, normal dosyaları yaratmak için "open" fonksiyonu kullanılabilmektedir. "mknod" fonksiyonu başarı durumunda "0" değerine, başarısızlık durumunda "-1" değerine geri dönmektedir. Ayrıca "mknod" POSIX fonksiyonunun "mknodat" isimli "at" li bir versiyonu da bulunmaktadır: #include int mknodat(int fd, const char *path, mode_t mode, dev_t dev); Bu "at" li versiyon daha önce görmüş dolduğumuz "at" li fonksiyonlar gibi çalışmaktadır. Yani fonksiyon ilgili dizine ilişkin dosya betimleyicisini ve göreli yol ifadesini parametre olarak alır. O dizinden göreli biçimde yol ifadesini oluşturur. Yine fonksiyonun birinci parametresine "AT_FDCWD" özel değeri geçilebilir. Bu durumda fonksiyon "at" siz versiyondaki gibi çalışır. Diğer "at" li fonksiyonlarda olduğu gibi bu fonksiyonun da ikinci parametresindeki yol ifadesi mutlak ise birinci parametresindeki dizin hiç kullanılmamaktadır. "mknod" ve "mknodat" fonksiyonları prosesin "umask" değerini dikkate almaktadır. Bu fonksiyonlarla aygıt dosyası yaratabilmek için (diğer özel dosyalar için gerekmemektedir) prosesin uygun önceliğe sahip olması gerekmektedir. Aşağıdaki aygıt dosyası yaratan "mymknode" isimli bir fonksiyon yazılmıştır. * Örnek 1, Fonksiyonun genel kullanımı şöyledir: ./mymknod [-m ya da --mode ] [c ya da b] Tipik bir çalıştırma şöyle olabilir: $ sudo ./mymknode -m 666 mydriver c 25 0 Programın kodu ise aşağıdaki gibidir: /* mymknod.c */ #include #include #include #include #include #include #include bool ismode_correct(const char *mode); void exit_sys(const char *msg); int main(int argc, char *argv[]) /* ./mymknod [-m ] */ { int m_flag; int err_flag; char *m_arg; int result; int mode; dev_t dev; struct option options[] = { {"mode", required_argument, NULL, 'm'}, {0, 0, 0, 0} }; m_flag = err_flag = 0; opterr = 0; while ((result = getopt_long(argc, argv, "m:", options, NULL)) != -1) { switch (result) { case 'm': m_flag = 1; m_arg = optarg; break; case '?': if (optopt == 'm') fprintf(stderr, "option -m or --mode without argument!...\n"); else if (optopt != 0) fprintf(stderr, "invalid option: -%c\n", optopt); else fprintf(stderr, "invalid long option!...\n"); err_flag = 1; break; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind != 4) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (m_flag) { if (!ismode_correct(m_arg)) { fprintf(stderr, "incorrect mode argument!..\n"); exit(EXIT_FAILURE); } sscanf(m_arg, "%o", &mode); } else mode = 0644; if (argv[optind + 1][1] != '\0') { fprintf(stderr, "invalid type argument: %s\n", argv[optind + 1]); exit(EXIT_FAILURE); } if (argv[optind + 1][0] == 'c') mode |= S_IFCHR; else if (argv[optind + 1][0] == 'b') mode |= S_IFBLK; else { fprintf(stderr, "invalid type argument: %s\n", argv[optind + 1]); exit(EXIT_FAILURE); } dev = makedev(atoi(argv[optind + 2]), atoi(argv[optind + 3])); umask(0); if (mknod(argv[optind + 0], mode, dev) == -1) exit_sys("mknod"); return 0; } bool ismode_correct(const char *mode) { if (strlen(mode) > 3) return false; while (*mode != '\0') { if (*mode < '0' || *mode > '7') return false; ++mode; } return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aslında yukarıda yazdığımız "mymknod" programının aynısı zaten "mknod" isimli kabuk komutu biçimide bulunmaktadır. Bu komutun genel biçimi şöyledir: sudo mknod [-m ya da --mode Tipik bir çalıştırma şöyle olabilir:: $ sudo mknod devfile c 25 0 Yukarıdaki komut uygulandığında oluşturulan dosya şöyle olacaktır: crw-rw-rw- 1 root root 25, 0 Mar 29 22:05 mydriver Bir "kernel" modülün karakter aygıt sürücüsü haline getirilebilmesi için öncelikle bir aygıt numarasıyla (majör ve minör numara ile) temsil edilip çekirdeğe kaydettirilmesi ("register" ettirlmesi) gerekmektedir. Bu işlem tipik olarak "register_chrdev_region" isimli fonksiyonla yapılır. Fonksiyonun prototpi şöyledir: #include int register_chrdev_region(dev_t from, unsigned count, const char *name) Fonksiyonun birinci parametresi aygıt sürücünün majör ve minör numaralarına ilişkin "dev_t" türünden değeri almaktadır. Bu parametre için argüman genellikle "MKDEV" makrosuyla oluşturulmaktadır. MKDEV makrosu majör ve minör numarayı argüman olarak alıp bundan "dev_t" türünden aygıt numarası oluşturmaktadır. Fonksiyonun ikinci parametresi ilk parametrede belirtilen minör numaradan itibaren kaç minör numaranın kaydettirileceğini belirtmektedir. Örneğin biz "majör=20", "minör=0" dan itibaren "5" minör numarayı kaydettirebiliriz. Fonksiyonun son parametresi "proc" ve "sys" dosya sistemlerindeki görüntülenecek olan aygıt sürücünün ismini belirtmektedir. "Kernel" modüllerin isimleri "kernel" modül dosyasından gelmektedir. Ancak karakter aygıt sürücülerinin isimlerini biz istediğimiz gibi veririz. Tabii her aygıt sürücü bir "kernel" modül biçiminde yazılmak zorundadır. "register_chrdev_region" fonksiyonu başarı durumunda "0" değerine, başarısızlık durumunda negatif "errno" değerine geri döner. "register_chrdev_region" fonksiyonu ile "register" ettirilen majör ve minör numaralar "unregister_chrdev_region" fonksiyonuyla geri bırakılmalıdır. Aksi halde modül "kernel" alanından "rmmod" komutuyla atılsa bile bu aygıt numaraları tahsis edilmiş bir biçimde kalmaya devam etmektedir. "unregister_chrdev_region" fonksiyonun prototipi şöyledir: #include void unregister_chrdev_region (dev_t from, unsigned count); Fonksiyonun birinci parametresi aygıt sürücünün "register" ettirilmiş olan majör ve minör numarasını, ikinci parametresi ise yine o noktadan başlayan kaç minör numaranın "unregister" ettirileceğidir. Bir aygıt sürücü "register_chrdev_region" fonksiyonuyla majör ve minör numarayı "register" ettirdiğinde artık "/proc/devices" dosyasında bu aygıt sürücü için bir satır yaratılmaktadır. Aygıt sürücü "unregister_chrdev_region" fonksiyonuyla yok edildiğinde "/proc/devices" dosyasındaki satır silinmektedir. * Örnek 1, Aşağıdaki örnekte "kernel" modülün "init" fonksiyonunda "register_chrdev_region" fonksiyonu ile "Majör:25", "Minor:1" olacak biçimde bir aygıt numarası "kernel" a kaydettirilmiştir. Bu kayıt modülün "exit" fonksiyonunda "unregister_chrdev_region" fonksiyonu ile silinmiştir. Kernel modülü aşağıdaki gibi derleyebilirsiniz: "$ make file=generic-char-driver" Modülü "install" ettikten sonra "/proc/modules" ve "/proc/devices" dosyalarına bakınız. "proc/devices" dosyasında aygıt sürücünün belirlediğiiz isimle kaydettirildiğini göreceksiniz. Programın kodu ise aşağıdaki gibidir: # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module intitialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!..\n"); return result; } return 0; } static void __exit generic_exit(void) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } module_init(generic_init); module_exit(generic_exit); Bir çekirdek modülü bir aygıt numarasıyla ilişkilendirdikten sonra artık ona gerçek anlamda bir karakter aygıt sürücü kimliği kazandırmak gerekmektedir. Bu işlem struct cdev isimli bir yapının için doldurularak sisteme eklenmesi (yerleştirilmesi) ile yapılır. Linux çekirdeği tüm çekirdek modülleri ve aygıt sürücüleri çeşitli veri yapılarıyla tutmaktadır. Aygıt sürücü yazan programcılar çekirdeğin bu organizasyonunu bilmek zorunda değillerdir. Ancak bazı işlemleri tam gerektiği gibi yapmak zorundadırlar. (Linux çekirdeğinin aygıt sürücü mimarisi oldukça karmaşıktır. Bu konu "Linux Kernel" kursunda ele alınmaktadır.) cdev yapısı aşağıdaki gibi bir yapıdır: struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; }; Bu türden bir yapı nesnesi programcı tarafından global olarak (statik ömürlü olarak) tanımlanabilir ya da alloc_cdev isimli çekirdek fonksiyonuyla çekirdeğin heap sistemi (slab allocator) kullanılarak dinamik bir biçimde tahsis edilebilir. (İşletim sistemlerinin çekirdeğinin ayrı bir heap sistemi vardır. Linux çekirdeğinde spesifik türden nesnelerin hızlı tahsis edilmesi için "slab allocator" denilen bir heap sistemi kullanılmaktadır.) Eğer bu yapı nesnesi programcı tarafından, -> global bir biçimde tanımlanacaksa yapının elemanlarına ilkdeğer vermek için cdev_init fonksiyonu çağrılmalıdır. cdev_init fonksiyonunun parametrik yapısı şöyledir: #include void cdev_init(struct cdev *cdev, const struct file_operations *fops); Fonksiyonun birinci parametresi ilkdeğer verilecek global cdev nesnesinin adresini alır. İkinci parametre ise file_operations türünden bir yapı nesnesinin adresi almaktadır. file_operations isimli yapı birtakım fonksiyon adreslerinden oluşmaktadır. Yani yapının tüm elemanları birer fonksiyon göstericisidir. Bu yapı user moddaki program tarafından ilgili aygıt dosyası açılıp çeşitli işlemler yapıldığında çağrılacak fonksiyonların adreslerini tutmaktadır. Örneğin user moddaki program open, close, read, write yaptığında çağrılacak fonksiyonlarımızı burada belirtiriz. file_operations yapısı büyük bir yapıdır: struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); }; Bu yapının bazı elemanlarına atama yapabiliriz. Şöyleki: struct file_operations g_file_ops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; Yapının owner elemanına THIS_MODULE makrosunun atanması iyi bir tekniktir. Biz burada "aygıt sürücümüz open fonksiyonuyla açıldığında generic_open isimli fonksiyon çağrılsın", aygıt sürücümüz close fonksiyonu ile kapatıldığında "generic_release isimli fonksiyonumuz çağrılsın" demiş olmaktayız. -> cdev_alloc fonksiyonuyla dinamik bir biçimde tahsis edilecekse bu işlem cdev_init ile yapılmaz, çünkü zaten cdev_alloc bu işlemi de yapmaktadır. Fakat yine de programcının bu kez manuel olarak bu yapının bazı elemanlarına değer ataması gerekir. Burada kullanılan cdev_alloc fonksiyonunun parametrik yapısı aşağıdaki gibidir: #include struct cdev *cdev_alloc(void); Fonksiyon başarı durumunda cdev yapısının adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Yukarıda da belirttiğimiz gibi cdev yapısı cdev_alloc ile tahsis edilmişse cdev_init yapılmasına gerek yoktur. Ancak bu durumda programcının manuel olarak yapının owner ve ops elemanlarına değer ataması gerekir. Örneğin: struct cdev *g_cdev; ... if ((gcdev = cdev_alloc()) == NULL) { printk(KERN_ERROR "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; Bu iki yoldan biriyle oluşturulmuş olan cdev yapısının en sonunda cdev_add isimli fonksiyonla çekirdek veri yapılarına yerleştirilmeleri gerekir. cdev_add fonksiyonunun prototipi aşağıdaki gibidir: #include int cdev_add(struct cdev *devp, dev_t dev, unsigned count); Fonksiyonun birinci parametresi cdev türünden yapı nesnesinin adresini almaktadır. İkinci parametre aygıt sürücünün majör ve minör numarasını belirtmektedir. Üçüncü parametresi ise ilgili minör numaradan itibaren kaç minör numaranın kullanılacağı belirtir. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda negatif errno değerine geri döner. Örneğin: if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { ... return result; } Tabii aygıt sürücü boşaltılırken bu yerleştirme işlemi cdev_del fonksiyonuyla geri alınmalıdır. cdev_del fonksiyonu, struct cdev yapısı cdev_alloc ile tahsis edilmişse aynı zamanda onu free hale de getirmektedir. cdev_del fonksiyonunun prototipi aşağıdaki gibidir: #include void cdev_del(struct cdev *devp); Fonksiyon parametre olarak cdev yapısının adresini almaktadır. Özetle çekirdek modülümüzün tam bir karakter aygıt sürücüsü haline getirilmesi için şunlar yapılmalıdır: -> struct cdev isimli bir yapı türünden nesne global olarak (statik ömürlü olarak) tanımlanmalı ya da cdev_alloc fonksiyonu ile çekirdeğin heap sistemi içerisinde tahsis edilmelidir. Eğer bu nesne global olarak tanımlanacaksa nesneye cdev_init fonksiyonu ile ilkdeğerleri verilmelidir. Eğer nesne cdev_alloc fonksiyonu ile çekirdeğin heap alanında tahsis edilecekse bu durumda ilkdeğer verme işlemi bu fonksiyon tarafından yapılmaktadır. Ancak programcının yine yapının bazı elemanlarını manuel olarak doldurması gerekmektedir. -> Oluşturulan bu struct cdev nesnesi cdev_add çekirdek fonksiyonu ile çekirdeğe eklenmelidir. -> Çekirdek modülü çekirdek alanından atılırken modülün exit fonksiyonunda cdev_add işleminin geri alınması için cdev_del fonksiyonunun çağrılması gerekmektedir. Buradaki önemli bir nokta şudur: -> cdev_add fonksiyonu cdev nesnesinin içini çekirdekteki uygun veri yapısına kopyalamamaktadır. Bizzat bu nesnenin adresini kullanmaktadır. Yani çekirdek modülü var olduğu sürece bu cdev nesnesinin de yaşıyor olması gerekmektedir. Bu da cdev nesnesinin ve file_operations nesnesinin global biçimde (ya da statik ömürlü biçimde) tanımlanmasını gerektirmektedir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, Aşağıda bu işlemlerin yapıldığı örnek bir karakter aygıt sürücüsü verilmiştir. Bu aygıt sürücü majör=25, minör=0 aygıtını kullanmaktadır. Dolayısıyla aşağıdaki programın testi için şöyle bir aygıt dosyasının yaratılmış olması gerekir. Yaratımı sudo mknod mydriver -m=666 c 25 0 gibi yapabilirsiniz. Bu aygıt sürücü insmod ile yüklendiğinde artık biz user mode'da "mydriver" dosyasını açıp kapattığımızda file_operations yapısına yerleştirdiğimiz generic_open ve generic_release fonksiyonları çağrılacaktır. # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Yukarıdaki programda biz cdev nesnesini global olarak tanımladık. Aşağıda nesnenin cdev_alloc fonksiyonu ile dinamik biçimde tahsis edilmesine bir örnek verilmiştir. # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot alloc cdev!...\n"); return result; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Çekirdek kodları ya da aygıt sürücü kodları çoğu zaman çekirdek alanı ile user alanı arasında veri transfer yapmak isterler. Örneğin sys_read sistem fonksiyonu çekirdek alanında elde ettiği bilgileri user alanındaki programcının verdiği adrese kopyalar. Benzer biçimde sys_write fonksiyonu da bunun tersini yapmaktadır. Çekirdek alanı ile user alanı arasında memcpy fonksiyonu ile transfer yapmaya çalışmak uygun değildir. Bunun birkaç nedeni vardır. Bu tür transferlerde kernel mod programcılarının user alanındaki adresin geçerliliğini kontrol etmesi gerekir. Aksi takdirde kernel modda geçersiz bir alana kopyalama yapmak sistemin çökmesine yol açabilmektedir. Ayrıca user alanına ilişkin prosesin sayfa tablosunun bazı bölümleri o anda bellekte olmayabilir (yani swap out yapılmış olabilir). Böyle bir durumda işleme devam etmek çekirdek tasarımı açısından sorun olmaktadır. Eğer böyle bir durum varsa çekirdek kodlarının önce sayfa tablosunu RAM'e geri yükleyip işlemine devam etmesi gerekmektedir. İşte yukarıda açıklanan bazı nedenlerden dolayı çekirdek alanı ile user alanı arasında kopyalama işlemi için özel çekirdek fonksiyonları kullanılmaktadır. Yani biz user mod programları ile kernel modülümüz arasında transferleri özel bazı kernel fonksiyonlarıyla yapmalıyız. Bu amaçla kullanılan çeşitli kernel fonksiyonları ve makroları bulunmaktadır. En temel iki fonksiyon copy_to_user ve copy_from_user fonksiyonlarıdır. Bu fonksiyonların prototipleri şöyledir: #include unsigned long copy_to_user(void *to, const void *from, unsigned len); unsigned long copy_from_user(void *to, const void *from, unsigned len); Fonskiyonların birinci parametreleri kopyalamanın yapılacağı hedef adresi belirtmektedir. Yani copy_to_user için birinci parametre user alanındaki adres, copy_from_user için birinci parametre kernel alanındaki adrestir. İkinci parametre kaynak adresi belirtmektedir. Bu kaynak adres copy_to_user için kernel alanındaki adres, copy_from_user için user alanındaki adrestir. Son parametre transfer edilecek byte sayısını belirtmektedir. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda transfer edilemeyen byte sayısına geri dönerler. Kernel mod programcılarının bu fonksiyonlar başarısızken bunu çağıran foksiyonlarını -EFAULT (Bad address) ile geri döndürmesi uygun olur. (Örneğin sys_read ve sys_write fonksiyonlarına biz geçersiz bir user mode adresi verirsek bu sistem fonksiyonları da -EFAULT değeri ile geri dönmektedir. Bu hata kodunun yazısal karşılığı "Bad address" biçimindedir.) Bazen user alanındaki adresin zaten geçerliliği sınanmıştır. Bu durumda yeniden geçerlilik sınaması yapmadan yukarıdaki işlemleri yapan __copy_to_user ve __copy_from_user fonksiyonları kullanılabilir. Bu fonksiyonların parametrik yapıları aynıdır. Bu fonksiyonların yukarıdakilerden tek farkı adres geçerliliğine ilişkin sınama yapmamalarıdır: #include unsigned long __copy_to_user(void *to, const void *from, unsigned len); unsigned long __copy_from_user(void *to, const void *from, unsigned len); Bazı durumlarda programcı 1 byte, 2 byte, 4 byte, 8 byte'lık verileri transfer etmek isteyebilir. Bu küçük miktardaki verilerin transfer edilmesi için daha hızlı çalışan özel iki makro vardır: put_user ve get_user. Bu makroların parametrik yapısı şöyledir: #include put_user(x, ptr); get_user(x, ptr); Burada x aktarılacak nesneyi belirtir. (Bu nesnenin adresini programcı almaz, makro içinde bu işlem yapılmaktadır.) ptr ise transfer adresini belirtmektedir. Aktarım ikinci parametrede belirtilen adresin türünün uzunluğu kadar yapılmaktadır. Başka bir deyişle biz makroya hangi türden nesne verirsek zaten makro o uzunlukta tranfer yapmaktadır. Makrolar başarı durumunda 0, başarısızlık durumunda negatif hata koduna geri dönmektedir. Bu makroların da geçerlilik kontrolü yapmayan __put_user ve __get_user isimli versiyonları vardır: #include __put_user(x, ptr); __get_user(x, ptr); Örneğin biz çekirdek modülümüzdeki 4 byte'lık int bir x nesnesinin içerisindeki bilgiyi puser ile temsil edilen user adresine kopyalamak isteyelim. Bu işlemi şöyle yaparız: int x; int *puser; ... put_user(x, puser); Nihayet user alanındaki adresin geçerliliği de access_ok isimli makroyla sorgulanabilmektedir. Makro şöyledir: #include access_ok(type, addr, size); Buradaki type sınanacak geçerliliğin türünü anlatmaktadır. Okuma geçerliliği için bu parametre VERIFY_READ, yazma geçerliliği için VERIFY_WRITE ve hem okuma hem de yazma geçerliliği için VERIFY_READ|VERIFY_WRITE biçiminde girilmelidir. İkinci parametre geçerliliği sınanacak adresi ve üçüncü parametre de o adresten başlayan alanın uzunluğunu belirtmektedir. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. Örneğin biz user alanında puser adresiyle başlayan 100 byte'lık alanın yazma bakımından geçerli bir alan olup olmadığını sınamak isteyelim. Bu sınamayı çekirdek modülümüzde şöyle yapabiliriz: if (access_ok(VERIFY_WRITE, puser, 100)) { // adres geçerli ... } Biz şimdiye kadar aygıt dosyası open ile açıldığında ve close ile kapatıldığında aygıt sürücümüz içerisindeki fonksiyonlarımızın çağrılmasını sağladık. Şimdi de aygıt dosyası üzerinde read ve write fonksiyonları uygulandığında aygıt sürücümüzdeki ilgili fonksiyonların çağrılması üzerinde duracağız. Aygıt sürücümüz için read ve write fonksiyonları aşağıdaki parametrik yapıya uygun olacak biçiminde file_operations yapısına yerleştirilmelidir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct file_operations g_file_ops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release, .read = generic_read, .write = generic_write, }; Artık aygıt dosyası üzerinde read POSIX fonksiyonu çağrıldığında aygıt sürücümüzdeki generic_read fonksiyonu write POSIX fonksiyonu çağrıldığında aygıt sürücümüzdeki generic_write POSIX fonksiyonu çağrılacaktır. read ve write fonksiyonlarının birinci parametresi açılmış dosyaya ilişkin struct file nesnesinin adresini belirtir. Anımsanacağı gibi bir dosya açıldığında kernel sys_open fonksiyonunda bir dosya nesnesi (struct file) tahsis edip bu dosya nesnesinin adresini dosya betimleyici tablosunda bir slota yerleştirip onun indeksini dosya betimleyicisi olarak geri döndürüyordu. İşte bu read ve write fonksiyonlarının birinci parametreleri bu dosya nesnesinin adresini belirtmektedir. Daha önceden de belirttiğimiz gibi file yapısı içerisinde dosya göstericisinin konumu, dosyanın erişim hakları, referans sayacının değeri, dosyanın açış modu ve açış bayrakları ve başka birtakım bilgiler bulunmaktadır. Linux kernel 2.4.30'daki file yapısı şöyledir: struct file { struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct fown_struct f_owner; unsigned int f_uid, f_gid; int f_error; size_t f_maxcount; unsigned long f_version; // needed for tty driver, and maybe others void *private_data; // preallocated helper kiobuf to speedup O_DIRECT struct kiobuf *f_iobuf; long f_iobuf_lock; }; Biz burada bilerek sadelik yüzünden eski bir çekirdeğin file yapısını verdik. Yeni çekirdeklerde buna birkaç eleman daha eklenmiştir. Ancak temel elemanlar yine aynıdır. read ve write fonksiyonlarının ikinci parametresi user alanındaki transfer adresini belirtir. Üçüncü parametreler okunacak ya da yazılacak byte miktarını belirtmektedir. Son parametre dosya göstericisinin konumunu belirtir. Ancak bu parametre file yapısı içerisindeki f_pos elemanının adresi değildir. Çekirdek tarafından read ve write fonksiyonları çağrılmadan önce file yapısı içerisindeki f_pos elemanının değeri başka bir nesneye atanıp o nesnenin adresi read ve write fonksiyonlarına geçirilmektedir. read ve write fonksiyonları sonlandığında çekirdek adresini geçirdiği nesnenin değerini file yapısının f_pos elemanına kendisi yerleştirmektedir. Fonksiyon başarı durumunda transfer edilen byte sayısına, başarısızlık durumunda negatif errno değerine geri dönmelidir. Biz aygıt sürücümüz için read ve write fonksiyonlarını yazarken transfer edilen byte miktarı kadar dosya göstericisini kendimizin ilerletmesi gerekir. Bu işlem fonksiyonların son parametresi olan off göstericisinin gösterdiği yerin güncellenmesi ile yapılır. Örneğin n byte transfer edilmiş olsun. Bu durumda dosya göstericisinin konumu aşağıdaki gibi güncellenebilir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { ... *off += n; return n; } Aygıt sürücüsünüzün read ve write fonksiyonlarında dosya göstericisini konumlandırmak için file yapısının f_pos elemanını güncellemeyiniz. Dosya göstericisinin konumlandırılması her zaman read ve write fonksiyonlarının son parametresi yoluyla yapılmaktadır. Çekirdeğin dosya göstericisini nasıl güncellediğine ilişkin aşağıdaki gibi bir temsili kod örneği verebiliriz: loff_t off; ... off = filp->f_pos; read(filp, buf, size, &off); filp_f_pos = off; Şimdi de bu konuya ilişkin örnekleri inceleyelim: * Örnek 1, Aşağıdaki örnekte aygıt sürücü için read fonksiyonu yazılmıştır. Bu fonksiyon aslında g_buf isimli dizinin içini dosya gibi vermektedir. Ayrıca aşağıdaki aygıt sürücüye read ve write fonksiyonları içi boş bir biçimde yerleştirilmiştir. User mode'dan read ve write yapıldığında aygıt sürücümüzün içerisindeki bu fonksiyonların çalıştığını gözlemleyiniz. # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_read called...\n"); return size; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_write called...\n"); return size; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[100]; if ((fd = open("mydriver", O_RDWR)) == -1) exit_sys("open"); read(fd, buf, 100); write(fd, buf, 100); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi de aygıt sürücümüzün read fonksiyonunun gerçekten bir dosyadan okuma yapıyormuş gibi davranmasını sağlayalım. Bunun için dosyamızı temsil eden aşağıdaki gibi global bir dizi kullanacağız: static char g_buf[] = "01234567890ABCDEFGH"; Buradaki diziyi sanki bir dosya gibi ele alacağız. Aygıt sürücümüzün read fonksiyonu aşağıdaki gibi olacaktır: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t slen; slen = strlen(g_buf); esize = *off + size > slen ? slen - *off : size; if (copy_to_user(buf, g_buf + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } Burada önce dosya göstericisinin gösterdiği yerden itibaren "size" kadar byte'ın gerçekten dizi içerisinde olup olmadığına bakılmıştır. Eğer "*off + size" değeri bu dizinin uzunluğundan fazlaysa "size" kadar değer değil, "slen - *off" kadar değer okunmuştur. Aygıt sürücülerin read ve write fonksiyonlarında dosya göstericisinin ilerletilmesi programcının sorumluluğundadır. Bu nedenle okuma işlemi yapıldığında dosya göstericisinin konumu aşağıdaki gibi artırılmıştır: *off += size; read fonksiyonunun okunabilen byte sayısına geri döndürüldüğüne dikkat ediniz. copy_to_user fonksiyonu ile tüm byte'lar user alanına kopyalanamamışsa fonksiyon -EFAULT değeri ile geri döndürülmüştür. * Örnek 1, # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static char g_buf[] = "01234567890ABCDEFGH"; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t slen; slen = strlen(g_buf); esize = *off + size > slen ? slen - *off : size; if (copy_to_user(buf, g_buf + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "generic_write called...\n"); return size; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[1024]; ssize_t result; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 3)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 5)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 30)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); if ((result = read(fd, buf, 30)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: \"%s\"\n", (intmax_t)result, buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aygıt sürücü için write fonksiyonu da tamamen read fonksiyonuna benzer biçimde yazılmaktadır. write fonksiyonu içerisinde biz user moddaki bilgiyi copy_from_user ya da get_user fonksiyonlarıyla alırız. Yine write fonksiyonu da bir sorun çıktığında -EFAULT değeri ile, başarılı sonlanmada ise yazılan (kernel alanına yazılan) byte miktarı ile geri dönmelidir. Aşağıdaki örnekte aygıt sürücü bellekte oluşturulmuş bir dosya gibi davranmaktadır. Aygıt sürücünün taklit ettiği dosya en fazla 4096 byte olabilmektedir: #define FILE_MEMORY_MAX_SIZE 4096 ... static char g_fmem[FILE_MEMORY_MAX_SIZE]; Ancak buradaki FILE_MEMORY_MAX_SIZE bellek dosyasının maksimum uzunluğunu belirtmektedir. Bellek dosyasının gerçek uzunluğu g_fmem_size nesnesinde tutulmaktadır. Aygıt sürücünün write fonksiyonu aşağıdaki gibi yazılmıştır: static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } Burada yine dosya göstericisinin gösterdiği yerden itibaren yazılmak istenen byte sayısı FILE_MEMORY_MAX_SIZE değerini aşıyorsa geri kalan miktar kadar yazma yapılmıştır. Burada yine dosya göstericisinin ilerletildiğine dikkat ediniz. Dosya göstericisinin ilerletilmesi her zaman programcının sorumluluğundadır. Aygıt sürücümüzün read fonksiyonu da şöyledir: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } Burada da dosya göstericisinin gösterdiği yerden itibaren okunmak istenen byte sayısının g_fmem_size değerinden büyük olup olmadığına bakılmıştır. Yine dosya göstericisi fonksiyon tarafından güncellenmiştir. Buradaki aygıt sürücüyü test etmek için "app-write.c" ve "app-read.c" isimli iki ayrı programdan faydalanılmıştır. "app-write.c" bellek dosyasına yazma yapmakta, "app-read.c" ise bellek dosyasından okuma yapmaktadır. Bu örnekte bellek dosyasına yazılanların aygıt sürücü çekirdekte bulunduğu sürece kalıcı olduğuna dikkat ediniz. * Örnek 1, # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } module_init(generic_init); module_exit(generic_exit); /* app-write.c */ #include #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[5000]; if ((fd = open("mydriver", O_WRONLY)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); memset(buf, 'x', 5000); if ((result = write(fd, buf, 5000)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* app-read.c */ #include #include #include #include #include #define BUFFER_SIZE 5 void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[BUFFER_SIZE + 1]; if ((fd = open("mydriver", O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fd, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; printf("%s", buf); fflush(stdout); } putchar('\n'); if (result == -1) exit_sys("read"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } User moddan aygıt dosyası betimleyicisi ile lseek işlemi yapıldığında aygıt sürücünün file_operations yapısı içerisine yerleştirilen llseek fonksiyonu çağrılmaktadır. Fonksiyonun parametrik yapısı şöyledir: static loff_t generic_llseek(struct file *filp, loff_t off, int whence); Fonksiyonun birinci parametresi dosya nesnesini, ikinci parametresi konumlandırılmak istenen offset'i, üçüncü parametresi ise konumlandırmanın nereye göre yapılacağını belirtmektedir. Bu fonksiyonu gerçekleştirirken programcı file yapısı içerisindeki f_pos elemanını güncellemelidir. Tipik olarak programcı whence parametresini switch içerisine alır. Hedeflenen offset'i hesaplar ve en sonunda file yapısının f_pos elemanına bu hedeflenen offset'i yerleştirir. Hedeflenen offset uygun değilse fonksiyon tipik olarak -EINVAL değeriyle geri döndürülür. Eğer konumlandırma offset'i başarılı ise fonksiyon dosya göstericisinin yeni değerine geri dönmelidir. Aşağıda daha önce yapmış olduğumuz bellek dosyası örneğine llseek fonksiyonu da eklenmiştir. Fonksiyon aşağıdaki gibi yazılmıştır: static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } Burada önce whence parametresine bakılarak dosya göstericisinin konumlandırılacağı offset belirlenmiştir. Sonra dosya nesnesinin f_pos elemanı güncellenmiştir. * Örnek 1, # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #define DEV_MAJOR 25 #define DEV_MINOR 0 #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static loff_t generic_llseek(struct file *filp, loff_t off, int whence); static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .llseek = generic_llseek, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = register_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, MKDEV(DEV_MAJOR, DEV_MINOR), 1)) < 0) { unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(&g_cdev); unregister_chrdev_region(MKDEV(DEV_MAJOR, DEV_MINOR), 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } module_init(generic_init); module_exit(generic_exit); /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[4096]; if ((fd = open("mydriver", O_RDWR)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if (lseek(fd, 0, 0) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); if (lseek(fd, -2, 1) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; if (lseek(fd, -2, 2) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Biz şimdiye kadarki örneklerimizde aygıt sürücümüzün majör ve minör numarasını baştan belirledik. Bunun en önemli sakıncası zaten o numaralı bir aygıt sürücünün yüklü olarak bulunuyor olmasıdır. Bu durumda aygıt sürücümüz yüklenemeyecektir. Aslında daha doğru bir strateji tersten gitmektir. Yani önce aygıt sürücümüz içerisinde biz boş bir aygıt numarasını bulup onu kullanabiliriz. Tabii sonra user moddan bu aygıt numarasına ilişkin bir aygıt dosyasını da yaratmamız gerekir. Boş bir aygıt numarasını bize veren alloc_chrdev_region isimli bir kernel fonksiyonu vardır. Fonksiyonun parametrik yapısı şöyledir: int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); Fonksiyonun birinci parametresi boş aygıt numarasının yerleştirileceği dev_t nesnesinin adresini alır. İkinci ve üçüncü parametreler başlangıç minör numarası ve onun sayısını belirtir. Son parametre ise aygıt sürücüsünün "/proc/devices" dosyasında ve "/sys/dev" dizininde görüntülenecek olan ismini belirtmektedir. alloc_chrdev_region fonksiyonu zaten register_chrdev_region fonksiyonunun yaptığını da yapmaktadır. Dolayısıyla bu iki fonksiyondan yalnızca biri kullanılmalıdır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda negatif errno değerine geri döner. Örneğin: dev_t g_dev; ... if ((result = alloc_chrdev_region(&g_dev, 0, 1, "generic-char-driver")) < 0) { printk(KERN_ERR "cannot register device!...\n"); return result; } Aygıt sürücümüzde alloc_chrdev_region fonksiyonu ile boş bir majör numara numaranın bulunup aygıt sürücümüzün register ettirildiğini düşünelim. Pekiyi biz bu numarayı nasıl bilip bu numaraya uygun aygıt dosyası yaratacağız? İşte bunun için genellikle izlenen yöntem "/proc/devices" dosyasına bakıp oradan majör numarayı alıp aygıt dosyasını yaratmaktır. Tabii bu manuel olarak yapılabilir ancak bir shell script ile otomatize de edilebilir. Aşağıdaki "load" isimli script bu işlemi yapmaktadır: #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module Artık biz bu load script'i ile aygıt sürücümüzü yükleyip aygıt dosyamızı yaratacağız. Bu script'i load ismiyle yazıp aşağıdaki gibi x hakkı vermelisiniz: chmod +x load Çalıştırmayı komut satırı argümanı vererek aşağıdaki gibi yapmalısınız: $ sudo ./load generic-char-driver Burada load script'i çalıştırıldığında hem aygıt sürücü çekirdek alanına yüklenmekte hem de yüklenen aygıt sürücünün majör numarasıyla minör numara 1 olacak biçimde "generic-char-driver" isimli aygıt dosyası yaratılmaktadır. Aygıt sürücünün çekirdek alanından atılması manuel bir biçimde "rmmod" komutuyla yapılabilir. Tabii aynı zamanda bu aygıt sürücü için yaratılan aygıt dosyasının da silinmesi gerekir. Yukarıdaki script'te aygıt dosyası zaten varsa aynı zamanda o silinmektedir. Tabii aygıt dosyasını çekirdek alanından atarak silen ayrı bir "unload" isimli script'i de aşağıdaki gibi yazabiliriz: #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Tabii yine bu script dosyasının da "x" hakkına sahip olması gerekmektedir: $ chmod +x unload unload Script'ini aşağıdaki gibi çalıştırabilirsiniz: $ sudo ./unload generic-char-driver Aşağıdaki örnekte alloc_chrdev_region fonksiyonuyla hem boş aygıt numarası elde edilip hem de bu aygıt numarası register ettirilmiştir. Yükleme işlemi yukarıdaki "load" scrip'i ile yapılmalıdır. Kernel modülün boşaltılması işlemi manuel olarak ya da "unload" script'i ile yapılabilir. Örneğin: $ sudo ./load generic-char-driver ... $ sudo ./unload generic-char-driver Şimdi de bu konuya ilişkin örneklere bakalım: * Örnek 1, # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* generic-char-driver.c */ #include #include #include #include #include #define FILE_MEMORY_MAX_SIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static loff_t generic_llseek(struct file *filp, loff_t off, int whence); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .llseek = generic_llseek, .release = generic_release }; static char g_fmem[FILE_MEMORY_MAX_SIZE]; static size_t g_fmem_size; static int __init generic_init(void) { int result; printk(KERN_INFO "generic-char-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "generic-char-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "generic-char-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "generic-char-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > g_fmem_size ? g_fmem_size - *off : size; if (copy_to_user(buf, g_fmem + *off, esize) != 0) return -EFAULT; *off += esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; esize = *off + size > FILE_MEMORY_MAX_SIZE ? FILE_MEMORY_MAX_SIZE - *off : size; if (copy_from_user(g_fmem + *off, buf, esize) != 0) return -EFAULT; *off += esize; if (*off > g_fmem_size) g_fmem_size = *off; return esize; } static loff_t generic_llseek(struct file *filp, loff_t off, int whence) { loff_t newpos; switch (whence) { case 0: newpos = off; break; case 1: newpos = filp->f_pos + off; break; case 2: newpos = g_fmem_size + off; break; default: return -EINVAL; } if (newpos < 0 || newpos > g_fmem_size) return -EINVAL; filp->f_pos = newpos; return newpos; } module_init(generic_init); module_exit(generic_exit); /* load */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; ssize_t result; char buf[4096]; if ((fd = open("generic-char-driver", O_RDWR)) == -1) exit_sys("open"); if ((result = write(fd, "ankara", 6)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if ((result = write(fd, "izmir", 5)) == -1) exit_sys("write"); printf("%jd bytes written\n", (intmax_t)result); if (lseek(fd, 0, 0) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); if (lseek(fd, -2, 1) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; if (lseek(fd, -2, 2) == -1) exit_sys("lseek"); if ((result = read(fd, buf, 8)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%s\n", buf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aşağıda gelinen noktaya kadar görülmüş olan konular kullanılarak yazılmış basit bir boru örneği verilmiştir. Bu boru örneğinde bir proses boruyu yazma modunda açar ve prosesin write fonksiyonuyla yazdıkları aygıt sürücü içerisindeki bir FIFO kuyruk sistemine yazılır. Diğer proses de read fonksiyonuyla bu FIFO kuyruk sisteminden kuyruktan okuma yapar. Bu gerçekleştirim orijinal "isimli boru (named pipe)" gerçekleştirimine benziyorsa da ondan farklıdır. Burada yapılan gerçekleştirimin önemli noktaları şunlardır: -> write fonksiyonu borudaki boş alan miktarından daha fazla bilgi yazılmaya çalışılırsa blokeye yol açmaz, yazabildiği kadar byte'ı yazar ve boruya yazabildiği byte sayısına geri döner. Halbuki anımsanacağı gibi orijinal borularda talep edilen miktarın tamamı yazılana kadar write bloke oluşturmaktadır. -> read fonksiyonu boruda hiçbir bilgi yoksa blokeye yol açmaz 0 ile geri döner. Ancak read eğer boruda en az bir byte varsa okuyabildiği kadar byte'ı okuyup okuyabildiği byte sayısına geri döner. -> Aygıt sürücünün read/write fonksiyonlarında hiçbir senkronizasyon uygulanmamıştır. Dolayısıyla eşzamanlı işlemlerde tanımsız davranışlar ortaya çıkabilir. Örneğin iki farklı proses bu boruya yazma yaparsa senkronizasyondan kaynaklanan sorunlar oluşabilir. -> İki proses de boruyu kapatsa bile boru silinmemektedir. Halbuki orijinal borularda prosesler isimli boruyu kapatınca boru içerisindeki tüm bilgiler silinmektedir. -> Bu uygulamada sistem genelinde tek bir boru yaratılmaktadır. Yani bizim boru aygıt sürücümüz tek bir boru üzerinde işlemler yapmaktadır. Halbuki orijinal isimli borularda programcılar birbirinden bağımsız istedikleri kadar çok isimli boru yaratabilmektedir. Aygıt sürücünüzü önce build edip sonra aşağıdaki gibi yüklemelisiniz: $ make file=pipe-dirver $ sudo ./load pipe-driver Buradaki boruyu aygıt sürücüsünü test etmek için "prog1" ve "prog2" isimli iki program yazılmıştır. "prog1" klavyeden alınan yazıları boruya yazmakta, "prog2" ise klavyeden alınan uzunlukta byte'ı borudan okumaktadır. Boruyu test temek için boru uzunluğunu azaltabilirsiniz. Biz örneğimizde boru uzunluğunu 4096 aldık. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 20 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; esize = MIN(g_count, size); if (g_tail < g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail > g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aygıt sürücülerdeki kodlar user mode'dan farklı prosesler tarafından kullanılıyor olabilir. Ayrıca ileride göreceğimiz gibi aygıt sürücüler donanım kesmelerini de kullanıyor olabilir. Dolayısıyla aygıt sürücü kodları eşzamanlı (concurrent) erişime uygun biçimde yazılmalıdır. User mode'daki bir proses aygıt sürücü içerisindeki bir kaynağı kullanıyorken user mode'daki diğer prosesin o kaynağın bozulmaması için diğerini beklemesi gerekebilmektedir. Kernel mode'da aygıt sürücü kodları daha önce user mode'da gördüğümüz senkronizasyon nesnelerini kullanamaz. Çünkü daha önce gördüğümüz senkronizasyon nesneleri user mode'dan kullanılsın diye oluşturulmuştur. Çekirdeğin içerisinde kernel mode'dan kullanılabilecek ayrı senkronizasyon nesneleri bulunmaktadır. Bu bölümde aygıt sürücülerin kernel mode'da kullanabileceği senkronizasyon nesnelerini göreceğiz. Kernel mode için user mode'dakine benzer senkronizasyon nesneleri kullanılmaktadır. Bunların genel çalışma biçimi user mode'dakilere benzemektedir. Kernel mutex mekanizması 2.6 çekirdeğinde çekirdeğe eklenmiştir. Bundan önce mutex işlemleri binary semaphore'larla yapılıyordu. Bu mutex mekanizması user mode'daki mutex mekanizmasına çok benzemektedir. Yine kernel mode'daki mutex'in thread temelinde sahipliği vardır. Bu mutex mekanizması yine thread'i bloke edip sleep kuyruklarında bekletebilmektedir. Kernel mode mutex mekanizmasının tipik gerçekleştirimi şöyledir: -> lock işlemi sırasında işlemcinin maliyetsiz compare/set (compare/exchange) komutlarıyla mutex'in kilitli olup olmadığına bakılır. -> diğer bir işlemcideki proses mutex'i kilitlemişse boşuna bloke olmamak için yine compare/set komutlarıyla biraz spin işlemi yapılır. -> spin işleminden sonuç elde edilemezse bloke oluşturulur. Kernel mode'daki mutex'ler tipik olarak şöyle kullanılmaktadır: -> Mutex nesnesi "mutex" isimli bir yapıyla temsil edilmektedir. Sistem programcısı bu yapı türünden bir nesne yaratır ve ona ilk değerini verir. DEFINE_MUTEX(name) makrosu hem mutex türünden nesneyi tanımlamakta hem de ona ilk değerini vermektedir. Örneğin: #include DEFINE_MUTEX(g_mutex); Burada biz hem g_mutex isminde bir global nesne tanımlamış olduk hem de ona ilk değer vermiş olduk. Aynı işlem önce nesneyi tanımlayıp sonra mutex_init fonksiyonuyla da yapılabilir. Örneğin: struct mutex g_mutex; ... mutex_init(&g_mutex); DEFINE_MUTEX makrosuna nesnenin adresinin verilmediğine dikkat ediniz. Bu makro ve mutex_init fonksiyonunun prototipleri başlık dosyasında bulunmaktadır. Her ne kadar mutex_init bir fonksiyon görünümündeyse de aslında çekirdek kodlarında hem bir makro olarak hem de bir fonksiyon olarak bulunmaktadır. Mevcut Linux çekirdeklerinde fonksiyonların makro gerçekleştirimleri aşağıdaki gibidir: #define DEFINE_MUTEX(mutexname) \ struct mutex mutexname = __MUTEX_INITIALIZER(mutexname) #define mutex_init(mutex) \ do { \ static struct lock_class_key __key; \ \ __mutex_init((mutex), #mutex, &__key); \ } while (0) -> Mutex'i kilitlemek için mutex_lock fonksiyonu kullanılır: void mutex_lock(struct mutex *lock); Mutex'in kilitli olup olmadığı ise mutex_trylock fonksiyonuyla kontrol edilebilir: #include int mutex_trylock(struct mutex *lock); Eğer mutex kilitliyse fonksiyon bloke olmadan 0 değeriyle geri döner. Eğer mutex kilitli değilse mutex kilitlenir ve fonksiyon 1 değeri ile geri döner. Mutex nesnesi mutex_lock ile kilitlenmek istendiğinde bloke oluşursa bu blokeden sinyal yoluyla çıkılamamaktadır. Örneğin mutex_lock ile kernel mode'da biz mutex kilidini alamadığımızdan dolayı bloke oluştuğunu düşünelim. Bu durumda ilgili prosese bir sinyal gelirse ve eğer o sinyal için sinyal fonksiyonu set edilmişse thread uyandırılıp sinyal fonksiyonu çalıştırılmamaktadır. İşte eğer mutex'in kilitli olması nedeniyle bloke oluştuğunda sinyal yoluyla thread'in uyandırılıp sinyal fonksiyonunun çalıştırması isteniyorsa mutex nesnesi mutex_lock ile değil, mutex_lock_interrupible fonksiyonu ile kilitlenmeye çalışılmalıdır. mutex_lock_interruptible fonksiyonunun prototipi şöyledir: #include int mutex_lock_interruptible(struct mutex *lock); Fonksiyon eğer mutex kilidini alarak sonlanırsa 0 değerine, bloke olup sinyal dolayısıyla sonlanırsa -EINTR değerine geri dönmektedir. Programcı bu fonksiyonun -EINTR ile sonlandığını tespit ettiğinde ilgili sistem fonksiyonunun yeniden çalıştırılabilirliğini sağlamak için -ERESTARTSYS ile geri dönebilir. -> Mutex nesnesinin kilidini bırakmak için (unlock etmek için) mutex_unlock fonksiyonu kullanılmaktadır: void mutex_unlock(struct mutex *lock); Bu durumda örneğin tipik olarak aygıt sürücü içerisinde belli bir bölgeyi mutex yoluyla koruma şöyle yapılmaktadır: DEFINE_MUTEX(g_mutex); ... if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; ... ... ... mutex_unlock(&g_mutex); Aşağıdaki örnekte yukarıdaki boru sürücüsü daha güvenli olacak biçimde mutex nesneleriyle senkronize edilmiştir. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static DEFINE_MUTEX(g_mutex); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; esize = MIN(g_count, size); if (g_tail < g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; mutex_unlock(&g_mutex); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (mutex_lock_interruptible(&g_mutex) < 0) return -ERESTARTSYS; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail > g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; mutex_unlock(&g_mutex); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Kernel'da da user moddakine benzer semaphore nesneleri vardır. Kernel semaphore nesneleri de sayaçlıdır. Yine bunların sayaçları 0'dan büyükse semaphore açık durumdadır, sayaçlar 0 değerinde ise semaphore kapalı durumdadır. Kritik koda girildiğinde yine sayaç 1 eksiltilir. Sayaç 0 olduğunda thread bloke edilir. Yine bloke işleminde biraz spin işlemi yapılıp sonra bloke uygulanmaktadır. Kernel semaphore nesneleri şöyle kullanılmaktadır: -> Semaphore nesnesi struct semaphore isimli bir yapıyla temsil edilmiştir. Bir semaphore nesnesi DEFINE_SEMAPHORE(name) makrosuyla aşağıdaki gibi oluşturulabilir. #define DEFINE_SEMAPHORE(g_sem); Bu biçimde yaratılan semaphore nesnesinin başlangıçta sayaç değeri 1'dir. Yeni çekirdeklerde (v6.4-rc1 ve sonrası) bu makro iki parametreli olarak da kullanılabilmektedir: DEFINE_SEMAPHORE(g_sem, n); Buradaki ikinci parametre semaphore sayacının başlangıçtaki değerini belirtmektedir. Semaphore nesneleri sema_init fonksiyonuyla da yaratılabilmektedir: struct semaphore g_sem; ... sema_init(&g_sem, 1); Fonksiyonun ikinci parametresi başlangıç sayaç numarasıdır. -> Kritik kod "down" ve "up" fonksiyonları arasına alınır. "down" fonksiyonları sayacı bir eksilterek kritik koda giriş yapar. "up" fonksiyonu ise sayacı bir artırmaktadır. Fonksiyonların prototipleri şöyledir: #define void down(struct semaphore *sem); int down_interruptible(struct semaphore *sem); int down_killable(struct semaphore *sem); int down_trylock(struct semaphore *sem); int down_timeout(struct semaphore *sem, long jiffies); void up(struct semaphore *sem); Kritik kod "down" fonksiyonu ile oluşturulduğunda thread bloke olursa sinyal yoluyla uyandırılamamaktadır. Ancak kritik kod "down_interruptible" fonksiyonu ile oluşturulduğunda thread bloke olursa sinyal yoluyla uyandırılabilmektedir. "down_killable" bloke olmuş thread'i yalnızca SIGKILL sinyali geldiğinde blokeden kurtarıp sonlandırabilmektedir. "down_killable" fonksiyonunda eğer thread bloke olursa diğer sinyaller yine blokeyi sonlandıramamaktadır. "down_trylock" yine nesnenin açık olup olmadığına bakmak için kullanılır. Eğer nesne açıksa yine sayaç 1 eksiltilir ve kritik koda girilir. Bu durumda fonksiyon 0 dışı bir değerle geri döner. Nesne kapalıysa fonksiyon bloke olmadan 0 değerine geri döner. "down_timeout" ise en kötü olasılıkla belli miktar "jiffy" zamanı kadar blokeye yol açmaktadır. ("jiffy" kavramı ileride ele alınacaktır.) Fonksiyon zaman aşımı dolduğundan dolayı sonlanmışsa negatif hata koduna, normal bir biçimde sonlanmışsa 0 değerine geri dönmektedir. "down_interruptible" fonksiyonu normal sonlanmada 0 değerine, sinyal yoluyla sonlanmada -ERESTARTSYS değeri ile geri döner. Normal uygulama eğer bu fonksiyonlar -ERESTARTSYS ile geri dönerse aygıt sürücüdeki fonksiyonun da aynı değerle geri döndürülmesidir. Zaten çekirdek bu -ERESTARTSYS geri dönüş değerini aldığında asıl sistem fonksiyonunu eğer sinyal için otomatik restart mekanizması aktif değilse -EINTR değeri ile geri döndürmektedir. Bu da tabii POSIX fonksiyonlarının başarısız olup errno değerini EINTR biçiminde set edilmesine yol açmaktadır. "up" fonksiyonu yukarıda da belirttiğimiz gibi semaphore sayacını 1 artırmaktadır. Kernel semaphore nesneleriyle kritik kod aşağıdaki gibi oluşturulmaktadır: DEFINE_SEMAPHORE(g_sem); ... down_interruptible(&g_sem); ... ... ... up(&g_sem); Yukarıdaki boru örneğinde biz mutex nesnesi yerine binary semaphore nesnesi de kullanabilirdik. Aşağıda aynı örneğin binary semaphore ile gerçekleştirimi görülmektedir. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem) < 0) return -ERESTARTSYS; esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem) < 0) return -ERESTARTSYS; esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } User modda gördüğümüz diğer senkronizasyon nesnelerinin benzerleri de çekirdek içerisinde bulunmaktadır. Örneğin spinlock kullanımına çekirdek kodlarında ve aygıt sürücülerde sıkça rastlanmaktadır. Anımsanacağı gibi spinlock uykuya dalarak değil, (yani bloke olarak değil) meşgul bir döngü içerisinde kilidin açılmasını bekleyen senkronizasyon nesnelerini belirtiyordu. Spinlock nesnelerinin çok işlemcili ya da çekirdekli sistemlerde kullanılabileceğini belirtmiştik. Tek işlemcili ya da çekirdekli sistemlerde spinlock ile kritik kod oluşturmak kötü bir tekniktir. Çünkü kilidi başka bir thread alırsa diğer thread CPU'yu meşgul bir döngüde bekleyecektir. Spinlock nesneleri küçük kod blokları için ve özellikle çok işlemcili ya da çok çekirdekli sistemlerde kullanılması gereken senkronizasyon nesneleridir. Spinlock nesnesinin kilidini alan thread'in bloke olmaması gerekir. Aksi takdirde istenmeyen sonuçlar oluşabilir. Özetle spinlock nesnesinin kilidini alan thread şu durumlara dikkat etmelidir: -> Thread, kilidi uzun süre kapalı tutmamalıdır. -> Thread, kilidi aldıktan sonra bloke olmamalıdır. -> Thread, kilidi aldıktan sonra IRQ (donanım kesmeleri) dolayısıyla kontrolü bırakma konusunda dikkatli olmalıdır. Linux'ta bir thread spinlock kilidini almışsa artık quanta süresi dolsa bile thread'ler arası geçiş kapatılmaktadır. Kernel spinlock nesneleri tipik olarak şöyle kullanılmaktadır: -> Spinlock nesnesi spinlock_t türü ile temsil edilmektedir. Spinlock nesnesinin yaratılması statik düzeyde aşağıdaki gibi yapılabilir: #include spinlock_t g_spinlock = SPIN_LOCK_UNLOCKED; Burada spinlock nesnesi kilit açık bir biçimde oluşturulmuştur. Ancak bu makro yeni çekirdeklerde kaldırılmıştır. Yeni çekirdeklerde DEFINE_SPINLOCK makrosu spinlock nesnesini kapalı olarak oluşturmakta kullanılabilmektedir. Örneğin: DEFINE_SPINLOCK(g_spinlock); Aynı işlem spin_lock_init fonksiyonuyla da yapılabilmektedir: #include void spin_lock_init(spinlock_t *lock); -> Kritik koda giriş için aşağıdaki fonksiyonlar kullanılmaktadır: #include void spin_lock(spinlock_t *lock); void spin_lock_irq(spinlock_t *lock); void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); void spin_lock_bh(spinlock_t *lock); "spin_lock" fonksiyonu klasik spin yapan fonksiyondur. "spin_lock_irq" fonksiyonu o anda çalışılan işlemci ya da çekirdekteki IRQ'ları (yani donanım kesmelerini) kapatarak kilidi almaktadır. Yani biz bu fonksiyonla kilidi almışsak kilidi bırakana kadar donanım kesmeleri oluşmayacaktır. "spin_lock_irqsave" fonksiyonu kritik koda girerken donanım kesmelerini kapatmakla birlikte önceki bir durumu geri yükleme yeteneğine sahiptir. Aslında bu fonksiyonların bazıları makro olarak yazılmıştır. Örneğin "spin_lock_irqsave" aslında bir makrodur. Biz bu fonksiyonun ikinci parametresine nesne adresini geçmemiş olsak da bu bir makro olduğu için aslında ikinci parametrede verdiğimiz nesnenin içerisine IRQ durumlarını yazmaktadır. "spin_lock_bh" fonksiyonu yalnızca yazılım kesmelerini kapatmaktadır. -> Kilidin geri bırakılması için ise aşağıdaki fonksiyonlar kullanılmaktadır: #include void spin_unlock(spinlock_t *lock); void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); void spin_unlock_irq(spinlock_t *lock); void spin_unlock_bh(spinlock_t *lock); Yukarıdaki lock fonksiyonlarının hepsinin bir unlock karşılığının olduğunu görüyorsunuz. Biz kilidi hangi lock fonksiyonu ile almışsa o unlock fonksiyonu ile bırakmalıyız. Örneğin: spinlock_t g_spinlock = SPIN_LOCK_UNLOCKED; ... spin_lock(&g_spinlock); ... ... ... spin_unlock(&g_spinlock); Ya da örneğin: spinlock_t g_spinlock = SPIN_LOCK_UNLOCKED; ... unsigned long irqstate; ... spin_lock_irqsave(&g_spinlock, irqstate); ... ... ... spin_unlock_irqrestore(&g_spinlock, irqstate); -> Yine kernel spinlock nesnelerinde de try'lı lock fonksiyonları bulunmaktadır: #include int spin_trylock(spinlock_t *lock); int spin_trylock_bh(spinlock_t *lock); Bu fonksiyonlar eğer spinlock kilitliyse spin yapmazlar ve 0 ile geri dönerler. Eğer kilidi alırlarsa sıfır dışı bir değerle geri dönerler. Her ne kadar yukarıdaki boru sürücüsündeki read ve write fonksiyonlarında kuyruğu korumak için spinlock kullanımı uygun değilse de biz yine kullanım biçimini göstermek için aşağıdaki örneği veriyoruz. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; spinlock_t g_spinlock; static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } spin_lock_init(&g_spinlock); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; spin_lock(&g_spinlock); esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; spin_unlock(&g_spinlock); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; spin_lock(&g_spinlock); esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; spin_unlock(&g_spinlock); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Biz daha önce user modda reader/writer lock nesnelerini görmüştük. Bu nesneler birden fazla thread'in kritik koda okuma amaçlı girmesine izin veriyordu. Ancak bir thread kritik koda yazma amaçlı girmişse diğer bir thread'in okuma ya da yazma amaçlı kritik koda girmesine izin vermiyordu. İşte user moddaki reader/write lock nesnelerinin bir benzeri kernel modda reader/writer spinlock nesneleri biçiminde bulunmaktadır. Yine kernel modda da kritik koda okuma amaçlı ya da yazma amaçlı giren fonksiyonlar vardır. reader/writer spinlock nesneleri rwlock_t türüyle temsil edilmektedir. Bunların yaratılması rwlock_init fonksiyonuyla yapılmaktadır: #include void rwlock_init(rwlock_t *lock); reader/writer spinlock nesneleri ile ilgili diğer çekirdek fonksiyonları şunlardır: #include void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock); void read_unlock(rwlock_t *lock); void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void read_unlock_irq(rwlock_t *lock); void read_unlock_bh(rwlock_t *lock); void write_lock(rwlock_t *lock); void write_lock_irqsave(rwlock_t *lock, unsigned long flags); void write_lock_irq(rwlock_t *lock); void write_lock_bh(rwlock_t *lock); int write_trylock(rwlock_t *lock); void write_unlock(rwlock_t *lock); void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void write_unlock_irq(rwlock_t *lock); void write_unlock_bh(rwlock_t *lock); Nesne read amaçlı lock edilmişse read amaçlı unlock işlemi, write amaçlı lock edilmişse write amaçlı unlock işlemi uygulanmalıdır. Fonksiyonların diğer işlevleri normal spinlock nesnelerinde olduğu gibidir. User moddaki senkronizasyon nesnelerinin benzerlerinin çekirdek içerisinde de bulunduğunu görüyorsunuz. Ancak user moddaki her senkronizasyon nesnesinin bir kernel mod karşılığı yoktur. Örneğin user moddaki "koşul değişkenlerinin (condition variables)" bir kernel mod karşılığı bulunmamaktadır. Ayrıca burada ele almadığımız (belki ileride ele alacağımız) yalnızca çekirdek içerisinde kullanılan birkaç senkronizasyon nesnesi daha bulunmaktadır. Biz user modda çeşitli fonksiyonların çeşitli koşullar altında blokeye yol açtığını belirtmiştik. Bir thread bloke olduğunda thread geçici süre (belli bir koşul sağlanana kadar) ilgili CPU'nun "çalışma kuyruğundan (run queue)" çıkartılır, ismine "bekleme kuyruğu (wait queue)" denilen bir kuyruğa yerleştirilir. Blokeye yol açan koşul ortadan kalktığında ise thread yeniden bekleme kuyruğundan alınarak CPU'nun çalışma kuyruğuna yerleştirilir. Biz şimdiye kadar user modda hep sistem fonksiyonları yoluyla blokelerin oluştuğunu gördük. Ancak kernel moddaki aygıt sürücülerde blokeyi aygıt sürücünün kendisi oluşturmaktadır. * Örnek 1, Biz boru aygıt sürücümüzde read işlemi yapıldığında eğer boruda okunacak hiç bilgi yoksa read işlemini yapan user moddaki thread'i bloke edebiliriz. Boruya bilgi geldiğinde de thread'i yeniden çalışma kuyruğuna yerleştirip blokeyi çözebiliriz. İşte bu bölümde aygıt sürücüde thread'lerin nasıl bloke edileceği ve blokenin nasıl çözüleceği üzerinde duracağız. Mevcut Linux sistemlerinde her CPU ya da çekirdeğin ayrı bir çalışma kuyruğu (run queue) bulunmaktadır. Ancak bir ara O(1) çizelgelemesi ismiyle Linux'ta bu konuda bir değişikliğe gidilmişti. O(1) çizelgelemesi tekniğinde toplam tek bir çalışma kuyruğu bulunuyordu. Hangi CPU ya da çekirdeğe atama yapılacaksa bu tek olan çalışma kuyruğundan thread alınıyordu. O(1) çizelgelemesi Linux'ta kısa bir süre kullanılmıştır. Bunun yerine "CFS (Completely Fair Scheduling)" çizelgeleme sistemine geçilmiştir. Çalışmakta olan bir thread'in bloke olması sırasında thread'in yerleştirileceği tek bir "bekleme kuyruğu (wait queue)" yoktur. Her CPU ya da çekirdek için de ayrı bir bekleme kuyruğu bulundurulmamaktadır. Bekleme kuyrukları ilgili olay temelinde oluşturulmaktadır. Örneğin sleep fonksiyonu dolayısıyla bloke olan thread'ler ayrı bir bekleme kuyruğuna, boru dolayısıyla bloke olan thread'ler ayrı bir bekleme kuyruğuna yerleştirilmektedir. Aygıt sürücüleri yazanlar da kendi olayları için kendi bekleme kuyruklarını yaratmaktadır. Tabii kernel'daki mutex ve semaphore fonksiyonları da aslında kendi içerisinde bir bekleme kuyruğu kullanmaktadır. Çünkü bu fonksiyonlar da blokeye yol açmaktadır. Yukarıda da belirttiğimiz gibi her aygıt sürücü kendi bloke olayları için kendinin kullanacağı bekleme kuyrukları yaratabilmektedir. Çekirdek içerisinde bekleme kuyruklarını yaratan ve yok eden çekirdek fonksiyonları bulunmaktadır. Yine çekirdek içerisinde bir thread'i çalışma kuyruğundan çıkartıp bekleme kuyruğuna yerleştiren, bekleme kuyruğundan çıkartıp çalışma kuyruğuna yerleştiren fonksiyonlar bulunmaktadır. Linux'ta bekleme kuyrukları wait_queue_head_t isimli bir yapıyla temsil edilmektedir. Bir bekleme kuyruğu DECLARE_WAIT_QUEUE_HEAD(name) makrosuyla oluşturulabilir. Örneğin: #include DECLARE_WAIT_QUEUE_HEAD(g_wq); Ya da nesne tanımlanıp init_waitqueue_head fonksiyonuyla da ilk değerlenebilir: #include wait_queue_head_t g_wq; ... init_waitqueue_head(&g_wq); Bir thread'i (yani task_struct nesnesini) çalışma kuyruğundan çıkartıp istenilen bekleme kuyruğuna yerleştirme işlemi wait_event makrolarıyla gerçekleştirilmektedir. Temel wait_event makroları şunlardır: wait_event(wq_head, condition); wait_event_interruptible(wq_head, condition); wait_event_killable(wq_head, condition); wait_event_timeout(wq_head, condition, timeout); wait_event_interruptible_timeout(wq_head, condition, timeout); wait_event_interruptible_exclusive(wq_head, condition); Bunlardan, -> wait_event makrosu thread'i "uninterruptible" biçimde bekleme kuyruğuna yerleştirir. Bu biçimde bloke olmuş thread'lerin blokeleri sinyal dolayısıyla çözülememektedir. -> wait_event_interruptible makrosu ise aynı işlemi "interruptible" olarak yapmaktadır. Yani sinyal geldiğinde thread bekleme kuyruğundan uyandırılır. wait_event_interruptible makrosunun wait_event makrosundan farkı eğer thread uyutulmuşsa, uykudan bir sinyalle uyandırılabilmesidir. Halbuki wait_event ile uykuya dalmış olan thread sinyal oluşsa bile uykudan uyandırılmamaktadır. wait_event_interruptible makrosu eğer sinyal dolayısıyla uyanmışsa -ERESTARTSYS değeri ile, koşul sağlandığından dolayı uyanmışsa 0 değeri ile geri dönmektedir. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); int g_flag = 0; ... if (wait_event_interruptible(g_wq, g_flag != 0) != 0) return -ERESTARTSYS; Bu tür durumlarda böylesi flag değişkenlerini atomic almak iyi bir tekniktir. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); atomic_t g_flag = ATOMIC_INIT(0); ... if (wait_event_interruptible(g_wq, atomic_read(&g_flag) != 0) != 0) return -ERESTARTSYS; -> wait_event_killable makrosu yalnızca SIGKILL sinyali için thread'i uyandırmaktadır. Yani bu biçimde bekleme kuyruğuna yerleştirilmiş bir thread'in blokesi sinyal geldiğinde çözülmez, ancak SIGKILL sinyali ile thread yok edilebilir. wait_event_killable ile thread uykuya dalındığında ise yalnızca SIGKILL sinyali ile thread uykudan uyandırılabilmektedir. Tabii programcı wait_event_interruptible makrosunun geri dönüş değerine bakmalı, eğer thread sinyal dolayısıyla uykudan uyandırılmışsa -ERESTARTSYS değeriyle kendi fonksiyonundan geri dönmelidir. -> wait_event_timeout ve wait_event_interruptible_timeout makrolarının wait_event makrolarından farkı thread'i en kötü olasılıkla belli bir jiffy zaman aşımı ile uyandırabilmesidir. Jiffy kavramı izleyen bölümlerde ele alınacaktır. -> wait_event_interruptible_exclusive (bunun interrutible olmayan biçimi yoktur) makrosu Linux çekirdeklerine 2.6'ının belli sürümünden sonra sokulmuştur. Yine bu makroyla birlikte aşağıda ele alınan wake_up_xxx_nr makroları da eklenmiştir. Bir prosesin exclusive olarak wait kuyruğuna yerleştirilmesi onlardan belli sayıda olanların uyandırılabilmesini sağlamaktadır. Makrolardaki "condition (koşul)" parametresi bool bir ifade biçiminde oluşturulmalıdır. Bu ifade ya sıfır olur ya da sıfır dışı bir değer olur. Bu koşul ifadesi "uyanık kalmak için bir koşul" belirtmektedir. Yani bu koşul uyandırma koşulu değildir, uyanık kalma koşuludur. Çünkü bu makrolarda koşula bakılması uyumadan önce ve uyandırılma işleminden sonra yapılmaktadır. Yani önce koşula bakılır. Koşul sağlanmıyorsa thread uyutulur. Thread uyandırıldığında yeniden koşula bakılır. Koşul sağlanmıyorsa yeniden uyutulur. Dolayısıyla uyanma işlemi çekirdek kodlarında tıpkı koşul değişkenlerinde (condition variable) olduğu gibi döngü içerisinde yapılmaktadır. Örneğin: DECLARE_WAIT_QUEUE_HEAD(g_wq); int g_flag = 0; ... wait_event(g_wq, g_flag != 0); Burada koşul g_flag != 0 biçimindedir. wait_event makroları fonksiyon değil makro biçiminde yazıldığı için bu koşul bu haliyle makronun içinde kullanılmaktadır. (Yani ifadenin sonucu değil, kendisi makroda kullanılmaktadır.) Makronun içerisinde önce koşula bakılmakta, bu koşul sağlanıyorsa thread zaten uyutulmamaktadır. Eğer koşul sağlanmıyorsa thread uyutulmaktadır. Thread uykudan uyandırıldığında tıpkı koşul değişkenlerinde olduğu gibi yeniden koşula bakılmakta eğer koşul sağlanmıyorsa thread yeniden uyutulmaktadır. Tabii wait_event makroları o andaki thread'i çizelgeden (yani run kuyruğundan) çıkartıp wait kuyruğuna yerleştirdikten sonra "context switch" işlemini de yapmaktadır. Context switch işlemi sonrasında artık run kuyruğundaki yeni bir thread çalışır. wait_event makrolarının temsili kodunu şöyle düşünebilirsiniz: while (koşul_sağlanmadığı_sürece) { } Bekleme kuyruğunda blokede bekletilen thread wake_up makrolarıyla uyandırılmaktadır. Uyandırılmaktan kastedilen şey thread'in bekleme kuyruğundan çıkartılıp yeniden çalışma kuyruğuna (run queue) yerleştirilmesidir. wait_event makrolarındaki koşula wake_up bakmamaktadır. wake_up makroları yalnızca thread'i bekleme kuyruklarından çalışma kuyruğuna taşımaktadır. Koşula uyandırılmış thread'in kendisi bakmaktadır. Eğer koşul sağlanmıyorsa yeniden uyutulmaktadır. Yani biz koşulu sağlanır duruma getirmeden wake_up işlemi yaparsak thread yeniden uykuya dalacaktır. (Zaten "koşulu sağlayan thread'i uyandırma" işlemi mümkün değildir.) En çok kullanılan wake_up makroları şunlardır: wake_up(wq_head); wake_up_nr(wq_head, nr); wake_up_all(wq_head); wake_up_interruptible(wq_head); wake_up_interruptible_nr(wq_head, nr); wake_up_interruptible_all(wq_head); Bu makroların çalışmasının anlaşılması için bekleme kuyrukları hakkında biraz ayrıntıya girmek gerekir. Bekleme kuyruğunu temsil eden wait_queue_head_t yapısı şöyle bildirilmiştir: struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; Görüldüğü gibi bu bir bağlı listedir. Bağlı liste spinlock ile korunmaktadır. Bu bağlı liste aşağıdaki yapılardan oluşmaktadır: struct __wait_queue { unsigned int flags; void *private; wait_queue_func_t func; struct list_head task_list; }; Bu yapının ayrıntısına girmeyeceğiz. Ancak yapıdaki flags elemanına dikkat ediniz. Bekleme kuyruğuna yerleştirilen bir thread'in exclusive bekleme yapıp yapmadığı (yani wait_event_intrerruptible_exclusive ile bekleme yapıp yapmadığı) bu flags elemanında saklanmaktadır. Bu wait kuyruğunun bekleyen thread'leri (onların task_struct adreslerini) tutan bir bağlı liste olduğunu varsayabilirsiniz. Yani bekleme kuyrukları aşağıdaki gibi düşünülebilir: T1 ---> T2 ---> T3 ---> T4 ---> T5 ---> T6 ---> T7 ---> T8 ---> NULL Bu thread'lerden bazıları exclusive bekleme yapmış olabilir. Bunları (E) ile belirtelim: T1 ---> T2 ---> T3 ---> T4(E) ---> T5 ---> T6(E) ---> T7 ---> T8(E) ---> NULL Yukarıdaki wake_up makrolarından, -> wake_up makrosu kuyruğun başından itibaren ilk exclusive bekleme yapan thread'e kadar bu thread de dahil olmak üzere tüm thread'leri uyandırmaktadır. Tabii bu thread'lerin hepsi uyandırıldıktan sonra ayrıca koşula da bakmaktadır. Örneğimizde wake_up makrosu çağrıldığında T1, T2, T3 ve T4 thread'leri uyandırılacaktır. Görüldüğü gibi wake_up makrosu aslında 1 tane exclusive thread uyandırmaya çalışmaktadır. Ancak onu uyandırırken kuyruğun önündeki exclusive olmayanları da uyandırmaktadır. -> wake_up_nr makrosu, wake_up makrosu gibi davranır ancak 1 tane değil en fazla nr parametresiyle belirtilen sayıda exclusive thread'i uyandırmaya çalışır. Başka bir deyişle wake_up(g_wq) çağrısı ile wake_up_nr(g_qw, 1) çağrısı aynı anlamdadır. Eğer yukarıdaki örnekte wake_up_nr(g_wq, 2) çağrısını yapmış olsaydık T1, T2, T2, T4, T5, T6 thread'leri uyandırılırdı. Tabii bu thread'lerin uyandırılmış olması wait_event makrolarından çıkılacağı anlamına gelmemektedir. Uyandırma işleminden sonra koşula yeniden bakılmaktadır. -> wake_up_all makrosu bekleme kuyruğundaki tüm exclusive thread'leri ve exclusive olmayan thread'leri yani kısaca tüm thread'leri uyandırmaktadır. Tabii yine uyanan thread'ler koşula bakmaktadır. -> wake_up_interruptible, wake_up_interruptible_nr ve wake_up_interruptible_all makroları interruptible olmayan makrolar gibi çalışmaktadır. Ancak bu makrolar bekleme kuyruğunda yalnızca "interruptible" wait_event fonksiyonlarıyla bekletilmiş thread'lerle ilgilenmektedir. Diğer thread'ler kuyrukta yokmuş gibi davranmaktadır. Şimdi de bu makroları kullandığımız bir örnek verelim: * Örnek 1, Aygıt sürücümüzün read ve write fonksiyonları aşağıdaki gibi olsun: wait_queue_head_t g_wq; atomic_t g_flag; ... static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver write...\n"); atomic_set(&g_flag, 1); wake_up_interruptible(&g_wq); return 0; } Burada eğer birden fazla thread read yaparsa exclusive olmayan bir biçimde bekleme kuyruğunda bekleyecektir. write işleminde wake_up_interruptible makrosu ile uyandırma yapıldığına dikkat ediniz. Bekleme kuyruğunda exclusive bekleyen thread olmadığına göre burada tüm read yapan thread'ler uyandırılacaktır. Onların koşulları sağlandığı için hepsi read fonksiyonundan çıkacaktır. Şimdi bu read fonksiyonunda exclusive bekleme yapmış olalım: static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible_exclusive(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } Artık write fonksiyonunda wake_up makrosu çağrıldığında yalnızca bir tane exclusive bekleme yapan thread uyandırılacağı için read fonksiyonundan yalnızca bir thread çıkacaktır. Test için aşağıdaki kodları kullanabilirsiniz. /* wait-driver.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Wait-Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wq; static atomic_t g_flag; static int __init generic_init(void) { int result; printk(KERN_INFO "wait-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "wait-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } init_waitqueue_head(&g_wq); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "wait-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "wait-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "wait-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver read...\n"); atomic_set(&g_flag, 0); if (wait_event_interruptible_exclusive(g_wq, atomic_read(&g_flag) != 0) != 0) { printk(KERN_INFO "Signal occured..."); return -ERESTARTSYS; } return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "wait-driver write...\n"); atomic_set(&g_flag, 1); wake_up_interruptible(&g_wq); return 0; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* wait-test-read.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[32]; ssize_t result; if ((fd = open("wait-driver", O_RDONLY)) == -1) exit_sys("open"); printf("reading begins...\n"); if ((result = read(fd, buf, 32)) == -1) exit_sys("result"); printf("Ok\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* wait-test-write.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[32] = {0}; if ((fd = open("wait-driver", O_WRONLY)) == -1) exit_sys("open"); if (write(fd, buf, 32) == -1) exit_sys("write"); printf("Ok\n"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Burada bir noktaya dikkatinizi çekmek istiyoruz. Daha önce görmüş olduğumuz mutex, semaphore, read/write kilitleri gibi senkronizasyon nesnelerinin kendilerinin oluşturduğu bekleme kuyrukları vardır. Bu senkronizasyon nesneleri bloke oluşturmak için kendi bekleme kuyruklarını kullanmaktadır. Şimdi de daha önce yapmış olduğumuz boru örneğimizi gerçek bir boru haline getirelim. Yani eğer boruda en az 1 byte boş alan kalmadıysa read fonksiyonu blokede en az 1 byte okuyana kadar beklesin. Eğer boruda tüm bilgileri yazacak kadar boş yer kalmadıysa bu kez de yazan taraf blokede beklesin. Burada izlenecek temel yöntem aslında kursumuzda "koşul değişkenleri (condition variable)" denilen senkronizasyon nesnelerindeki yöntemin aynısı olmalıdır. Okuyan thread kuyruktaki byte sayısını belirten g_count == 0 olduğu sürece bekleme kuyruğunda beklemelidir. Tabii bizim kuyruk üzerinde işlem yaptığımız kısımları senkronize etmemiz gerekir. Bunu da bir binary semaphore nesnesi ya da mutex nesnesi yapabiliriz. Semaphore nesnesini ve bekleme kuyruğunu aşağdaki gibi yaratabiliriz: static wait_queue_head_t g_wq; DEFINE_SEMAPHORE(g_sem); Okuyan taraf önce semaphore kilidini eline almalı ancak eğer uykuya dalacaksa onu serbest bırakıp uykuya dalmalıdır. Kuyruk üzerinde aynı anda işlemler yapılabiceği için tüm işlemlerin kritik kod içerisinde yapılması uygun olur. O halde read işleminin tipik çatısı şöyle olmalıdır: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } // kuyruktan okuma işlemleri up(&g_sem); Burada önce down_interruptible fonksiyonu ile semaphore kilitlenmeye çalışılmıştır. Eğer semaphore zaten kilitliyse semaphore'un kendi bekleme kuyruğunda thread uykuya dalacaktır. Daha sonra g_count değerine bakılmıştır. Eğer g_count değeri 0 ise önce semaphore serbest bırakılıp sonra thread bekleme kuyruğunda uyutulmuştur. Thread bekleme kuyruğundan uyandırıldığında yeniden semaphore kontrolünü ele almaktadır. Tabii eğer birden fazla thread bekleme kuyruğundan uyandırılırsa yalnızca bunlardan biri semaphore kontrolünü ele alacaktır. Tabii bundan sonra kuyruktan bilgiler okunacak ve semaphore kilidi serbest bırakılacaktır. Eğer birden fazla thread bekleme kuyruğundan uyanmışsa bu kez diğer bir thread semaphore kontrolünü ele alacak ve g_count değerine bakacaktır. Yukarıda da belirttiğimiz gibi aslında bu bir "koşul değişkeni" kodu gibidir. Çekirdek içerisinde böyle bir nesne olmadığı için manuel uygulanmıştır. Benzer biçimde write işleminin de çatısı aşağıdaki gibidir: if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } // kuyruğa yazma işlemleri up(&g_sem); Burada benzer işlemler uygulanmıştır. Eğer kuyrukta yazma yapılmak istenen kadar boş alan varsa akış while döngüsünün içerisine girmeyecektir. (Buradaki while koşulunun "PIPE_BUFSIZE - g_count < size" biçiminde olduğuna dikkat ediniz.) Dolayısıyla yazma işlemi kritik kod içerisinde yapılabilecektir. Ancak kuyrukta yeteri kadar yer yoksa semaphore kilidi serbest bırakılıp thread bekleme kuyruğunda bekletilecektir. Çıkışta benzer işlemler yapılmaktadır. Aslında burada spinlock nesneleri de kullanılabilir. Ancak zaten mutex, semaphore ve read/write lock nesneleri kendi içerisinde bir miktar spin yapmaktadır. Bu örnekte semaphore yerine spinlock kullanabilir miyiz? Spinlock için şu durumları gözden geçirmelisiniz: -> Spinlock nesnesinde bekleme CPU zamanı harcanarak meşgul bir döngü içerisinde yapılmaktadır. Dolayısıyla spinlock nesneleri kilidin kısa süreli bırakılacağından emin olunabiliyorsa kullanılmalıdır. -> Spinlock içerisinde sinyal işlemleri bekletilmektedir. Yani spinlock beklemelerinin "interruptible" bir biçimi yoktur. Aslında bu uygulamada spinlock nesneleri de kullanılabilir. Ancak yine de kilitli kalınan kod miktarı dikkate alındığında semaphore nesnesi daha uygun bir seçenektir. Burada yazma işlemleri için "yazma bekleme kuyruğu" ve okuma işlemleri için "okuma bekleme kuyruğu" biçiminde iki bekleme kuyruğu olduğuna dikkat ediniz. Çünkü yazan taraf okuma bekleme kuyruğundaki thread'leri okuyan taraf ise yazma bekleme kuyruğundaki thread'leri uyandırmak isteyecektir. Şimdi de bu anlatılanları işlediğimiz bir örnek verelim: * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aslında wait_event fonksiyonları export edilmiş birkaç fonksiyon çağrılarak yazılmıştır. Dolayısıyla wait_event fonksiyonlarını çağırmak yerine programcı daha aşağı seviyeli (zaten wait_event fonksiyonlarının çağırmış olduğu) fonksiyonları çağırabilir. Yani bu işlemi daha aşağı seviyede manuel de yapabilir. Prosesin manuel olarak wait kuyruğuna alınması prepare_to_wait ve prepare_to_wait_exclusive isimli fonksiyonlar tarafından yapılmaktadır: #include void prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state); void prepare_to_wait_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state); Bu fonksiyonların birinci parametreleri bekleme kuyruğu nesnesinin adresini almaktadır. İkinci parametreleri bu kuyruğa yerleştirilecek wait_queue_entry nesnesinin adresini almaktadır. Fonksiyonların üçüncü parametreleri TASK_UNINTERRUPTIBLE ya da TASK_INTERRUPTIBLE biçiminde geçilebilir. Bir wait_queue_entry nesnesi şöyle oluşturulabilir: DEFINE_WAIT(wqentry); Ya da açıkça tanımlanıp init_wait makrosuyle ilk değerlenebilir. Örneğin: struct wait_queue_entry wqentry; ... init_wait(&wqentry); DEFINE_WAIT makrosu global tanımlamalarda kullanılamamktadır. Çünkü bu makro küme parantezleri içerisinde sabit ifadesi olmayan ifadeler barındırmaktadır. Ancak makro yerel tanımlamalarda kullanılabilir. Dolayısıyla prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları da aslında bekleme kuruğuna bir wait_queue_enty nesnesi eklemektedir. Yani programcının bunun için yeni bir wait_queue_entry nesnesi oluşturması gerekmektedir. prepare_to_wait_exclusive exclusive uyuma için kullanılmaktadır. prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları şunları yapmaktadır: -> Thread'i çalışma kuyruğundan çıkartıp bekleme kuyruğuna yerleştirir. (Çalışma kuruğunun organizasyonu ve bu işlemin gerçek ayrıntıları biraz karmaşıktır.) -> Thread'in durum bilgisini (task state) state parametresiyle belirtilen duruma çeker. -> prepare_to_wait fonksiyonu kuyruk elemanını eclusive olmaktan çıkartırken, prepare_to_wait_exclusive onu exclusive yapar. Thread'in çalışma kuyruğundan bekleme kuyruğuna aktarılması onun uykuya dalması anlamına gelmemektedir. Programcı artık thread çalışma kuyruğunda olmadığına göre schedule fonksiyonu ile thread'ler arası geçiş (context switch) uygulamalı ve akış kontrolünü başka bir thread'e bırakmalıdır. Zaten thread'in çalışma kuyruğundan çıkartılması artık yeniden çalışma kuyruğuna alınmadıktan sonra uykuda bekletilmesi anlamına gelmektedir. Tabii biz prepare_to_wait ya da prepare_to_wait_exclusive fonksiyonlarını çağırdıktan sonra bir biçimde koşul durumuna bakmalıyız. Eğer koşul sağlanmışsa hiç prosesi uykuya daldırmadan hemen bekleme kuyruğundan çıkarmalıyız. Eğer koşul sağlanmamışsa gerçekten artık schedule fonksiyonuyla "thread'lerarası geçiş" yapmalıyız. Thread'imiz schedule fonksiyonunu çağırdıktan sonra artık uyandırılana kadar bir daha çizelgelenmyecektir. Bu da bizim uykuya dalmamız anlamına gelmektedir. Pekiyi thread'imiz uyandırıldığında nereden çalışmaya devam edecektir? İşte schedule fonksiyonu thread'ler arası geçiş yaparken kalınan yeri thread'e ilişki task_struct yapısının içerisine kaydetmektedir. Kalının yer schudule fonksiyonunun içerisinde bir yerdir. O halde thread'imiz uyandırıldığında schedule fonksiyonunun içerisinden çalışma devam eder. schedule fonksiyonu geri dönecek ve thread akışı devam edecektir. wake_up fonksiyonları thread'i bekleme kuyruklarından çıkartıp çalışma kuyruğuna eklemektedir. Ancak prepare_to_wait ve prepare_to_wait_exclusive fonksiyonları çağrıldıktan sonra eğer koşulun zaten sağlandığı görülürse bu durumda uyandırma wake_up fonksiyonlarıyla yapılmadığı için bekleme kuyruğundan thread'in geri çıkartılması da programcının sorumluluğundadır. Bu işlem finish_wait fonksiyonu ile yapılmaktadır. #include void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry); Bu fonksiyon zaten thread wake_up fonksiyonları tarafından bekleme kuyruğundan çıkartılmışsa herhangi bir işlem yapmamaktadır. Bu durumda manuel uyuma şöyle yapılabilir. DEFINE_WAIT(wqentry); prepare_to_wait(&g_wq, &wqentry, TASK_UNINTERRUPTIBLE); if (!condition) schedule(); finish_wait(&wqentry); Tabii eğer thread INTERRUPTIBLE olarak uyuyorsa schedule fonksiyonundan çıkıldığında sinyal dolayısıyla da çıkılmış olabilir. Bunu anlamak için signal_pending isimli fonksiyon çağrılır. Bu fonksiyon sıfır dışı bir değerle geri dönmüşse uyandırma işleminin sinyal yoluyla yapıldığı anlaşılır. Bu durumda tabii aygıt sürücüdeki fonksiyon bu durumda -ERESTARTSYS ile geri döndürülmelidir. signal_pending fonksiyonunun prototipi şöyledir: #include int signal_pending(struct task_struct *p); Fonksiyon parametre olarak thread'e ilişkin task_struct yapısının adresini parametre olarak almaktadır. Bu durumda INTERRUPTIBLE uyuma aşağıdaki gibi yapılabilir: DEFINE_WAIT(wqentry); prepare_to_wait(&g_wq, &wqentry, TASK_INTERRUPTIBLE); if (!condition) schedule(); if (signal_pending(current)) return -ERESTARTSYS; finish_wait(&wqentry); wake_up makrolarının şunları yaptıpını anımsayınız: -> Wait kuyruğundaki prosesleri çıkartarak run kuyruğuna yerleştirir. -> Prosesin durumunu TASK_RUNNING haline getirir. Aşağıda boru örneğinde manuel uykuya dalma işlemi uygulamıştır: * Örnek 1, /* pipe-driver-manual-wait.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver-manual-wait")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; DEFINE_WAIT(wqentry); if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); prepare_to_wait(&g_wqread, &wqentry, TASK_INTERRUPTIBLE); schedule(); if (signal_pending(current)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; DEFINE_WAIT(wqentry); if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); prepare_to_wait(&g_wqwrite, &wqentry, TASK_INTERRUPTIBLE); schedule(); if (signal_pending(current)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver-manual-wait", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) exit_sys("write"); printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver-manual-wait", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aslında wait_event fonksiyonları yukarıda açıkladığımız daha aşağı seviyeli fonksiyonlar kullanılarak gerçekleştirilmiştir. Mevcut son Linux çekirdeğinde wait_event_interruptible fonksiyonu şöyle yazılmıştır: #define wait_event_interruptible(wq_head, condition) \ ({ \ int __ret = 0; \ might_sleep(); \ if (!(condition)) \ __ret = __wait_event_interruptible(wq_head, condition); \ __ret; \ }) Burada gcc'nin bileşik ifade de denilen bir eklentisi (extension) kullanılmıştır. Bu makro ayrıntılar göz ardı edilirse __wait_event_interruptible makrosunu çağırmaktadır. Bu makro şöyle tanımlanmıştır: #define __wait_event_interruptible(wq_head, condition) \ ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \ schedule()) Burada ___wait_event makrosunun interruptible olan ve olmayan kodların ortak makrosu olduğu görülmektedir. Bu makro da şöyle tanımlanmıştır: #define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \ ({ \ __label__ __out; \ struct wait_queue_entry __wq_entry; \ long __ret = ret; /* explicit shadow */ \ \ init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \ for (;;) { \ long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state); \ \ if (condition) \ break; \ \ if (___wait_is_interruptible(state) && __int) { \ __ret = __int; \ goto __out; \ } \ \ cmd; \ } \ finish_wait(&wq_head, &__wq_entry); \ __out: __ret; \ }) Aygıt sürücülerimize arzu edersek "blokesiz (nonbloking)" okuma yazma desteği verebiliriz. Tabii bu detseğin verilebilmesi için aygıt sürücünün okuma yazma sırasında bloke oluşturması gerekmektedir. Anımsanacağı gibi blokesiz işlem yapabilmek için open POSIX fonksiyonunda fonksiyonun ikinci parametresine O_NONBLOCK bayrağının eklenmelidir. Normal disk dosyalarında O_NONBLOCK bayrağının bir anlamı yoktur. Ancak boru gibi özel dosyalarda ve aygıt sürücülerde bu bayrak şu anlama gelmektedir: -> Okuma sırasında eğer okunacak bir bilgi yoksa read fonksiyonu bloke oluşturmaz başarısızlıkla geri döner ve errno değeri EAGAIN olarak set edilir. -> Yazma sırasında yazma eylemi meşguliyet yüzünden yapılamıyorsa write fonksiyonu bloke oluşturmaz başarısızlıkla geri döner ve errno değeri yine EAGAIN olarak set edilir. Aygıt sürücü açıldığında open fonksiyonun ikinci parametresi file yapısının (dosya nesnesinin) f_flags elemanına yerleştirilmektedir. Dosya nesnesinin adresinin aygıt sürücüdeki fonksiyonları filp parametresiyle aktarıldığını anımsayınız. Bu durumda biz aygıt dosyasının blokesiz modda açılıp açılmadığını şöyle test edebiliriz: if (filp->f_flags & O_NONBLOCK) { /* blokesiz modda mı açılmış */ /* open fonksiyonunda aygıt O_NONBLOCK bayrağı ile açılmış */ } Aygıt sürücümüz blokesiz modda işlemlere izin vermiyorsa biz bu durumu kontrol etmeyebiliriz. Yani böyle bir aygıt sürücüde programcının aygıt sürücüyğ O_NONBLOCK bayrağını kullanarak açmışsa bu durumu hiç dikkate almayabiliriz. (Örneğin disk dosyalarında blokesiz işlemlerin bir anlamı olmadığı halde Linux çekirdeği disk dosyaları O_NONBLOCK bayrağıyla açıldığında hata ile geri dönmeden bayrağı dikkate almamaktadır.) Eğer bu kontrol yapılmak isteniyorsa aygıt sürücünün açılması sırasında kontrol aygıt sürücünün open fonksiyonund yapılabilir. Bu durumda open fonksiyonunu -EINVAL değeriyşe geri döndürebilirsiniz. Örneğin: static int generic_open(struct inode *inodep, struct file *filp) { if (filp->f_flas & O_NONBLOK) return -EINVAL; return 0; } Pekiyi boru aygıt sürücümüze nasıl blokesiz mod desteği verebiliriz? Aslında bunun için iki şeyi yapmamız gerekir: -> Yazma yapıldığı zaman boruda yazılanları alacak kadar yoksa aygıt sürücümüzün write fonksiyonunu -EAGAIN değeriyşe geri döndürmeliyiz. Örneğin: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } ... -> Okuma yapıldığı zaman eğer boruda hiç bilgi yoksa aygıt sürücümüzün read fonksiyonunu -EAGAIN geri döndürmeliyiz. Örneğin: ... if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } ... read ve write fonksiyonlarının -EAGAIN değeriyle geri döndürülmeden önce aygıt dosyasının blokesiz modda açlıp açılmadığının kontrol edilmesi gerektiğine dikkat ediniz. Aşaşğıdaki örnekte boru aygıt sürücüsüne blokesiz okuma ve yazma desteği verilmiştir. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); static unsigned char g_pipebuf[PIPE_BUFSIZE]; static size_t g_head; static size_t g_tail; static size_t g_count; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_count, size); if (g_tail <= g_head) size1 = MIN(PIPE_BUFSIZE - g_head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_pipebuf + g_head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_pipebuf, size2) != 0) return -EFAULT; g_head = (g_head + esize) % PIPE_BUFSIZE; g_count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_count, size); if (g_tail >= g_head) size1 = MIN(PIPE_BUFSIZE - g_tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_pipebuf + g_tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_pipebuf, buf + size1, size2) != 0) return -EFAULT; g_tail = (g_tail + esize) % PIPE_BUFSIZE; g_count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE]; char *str; ssize_t result; if ((pdriver = open("pipe-driver", O_WRONLY|O_NONBLOCK)) == -1) exit_sys("open"); for (;;) { printf("Text:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if ((result = write(pdriver, buf, strlen(buf))) == -1) if (errno == EAGAIN) { printf("write returns -1 with errno = EAGAIN...\n"); continue; } printf("%jd bytes written...\n", (intmax_t)result); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY|O_NONBLOCK)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) if (errno == EAGAIN) { printf("read returns -1 with errno = EAGAIN...\n"); continue; } buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Çekirdek modülleri ve aygıt sürücüler dinamik bellek tahsis etmeye gereksinim duyabilirler. Ancak kernel mod programlar dinamik tahsisatları malloc, calloc ve realloc gibi fonksiyonlarla yapamazlar. Çünkü bu fonksiyonlar user mod programlar tarafından kullanılacak biçimde prosesin bellek alanında tahsisat yapmak için tasarlanmışlardır. Oysa çekirdeğin ayrı bir heap sistemi vardır. Bu nedenle çekirdek modülleri ve aygıt sürücüler çekirdeğin sunduğu fonksiyonlarla çekirdeğin heap alanında tahsisat yapabilirler. Biz de bu bölümde bu fonksiyonlar üzerinde duracağız. Anımsanacağı gibi Linux sistemlerinde proseslerin bellek alanları sayfa tabloları yoluyla izole edilmişti. Ancak çekirdek tüm proseslerin sayfa tablosunda aynı yerde bulunmaktadır. Başka bir deyişle her prosesin sayfa tablosunda çekirdek hep aynı sanal adreslerde bulunmaktadır. Örneğin sys_open sistem fonksiyonuna girildiğinde bu fonksiyonun sanal adresi her proseste aynıdır. 32 Bit linux sistemlerinde proseslerin sanal bellek alanları 3GB User, 1GB Kernel olmak üzere 2 bölüme ayrılmıştır. 64 bit Linux sistemlerinde ise yalnızca sanal bellek alanının 256 TB'si kullanılmaktadır. Bu sistemlerde user alanı için 128 TB, Kernel alanı için de 128 TB yer ayrılmıştır. 32 Bit Linux sistemlerindeki prosesin sanal bellek alanı şöyle gösterilebilir: 00000000 USER ALANI (3 GB) C0000000 KERNEL ALANI (1GB) 64 Bit Linux sistemlerindeki sanal bellek alanı ise kabaca şöyledir: 0000000000000000 USER ALANI (128 TB) 0000800000000000 BOŞ BÖLGE (yaklaşık 16M TB) FFFF800000000000 KERNEL ALANI (128 TB) FFFFFFFFFFFFFFFF Bir sistem fonksiyonunun çağrıldığını düşünelim. İşlemci kernel moda'a otomatik olarak geçirilecektir. Bu durumda sayfa tablosu değişmeyecektir. Pekiyi kernel nasıl tüm fiziksel belleğe erişebilmektedir? İşte 32 bitlik sistemlerde proseslerin sayfa tablolarının son 1GB'yi sayfalandırdığı girişleri tamamen fiziksel belleği eşlemektedir. Başka bir deyişle bu sistemlerde çekirdek alanının başlangıcı olan C0000000 adresi aslında sayfa tablosunda 00000000 fiziksel adresini belirtmektedir. Böylece kernel'ın herhangi bir fiziksel adrese erişmek istediği zaman tek yapacağı şey bu adrese C00000000 değerini toplamaktır. Bu sistemlerde C0000000 adresinden itibaren proseslerin sayfa tabloları zaten fiziksel belleği 0'dan itibaren haritalandırmaktadır. Ancak 32 bit sistemlerde şöyle bir sorun vardır: Sayfa tablosunda C0000000'dan itibaren sayfalar fiziksel belleği haritalandırdığına göre 32 bit sistemlerin maksimum sahip olacağı 4GB fiziksel RAM'in hepsi haritalandırılamamaktadır. İşte Linux tasarımcıları sayfa tablolarında C0000000'dan itibaren fiziksel RAM'in 1GB'sini değil 896MB'sini haritalandırmıştır. Geri kalan 128 MB'lik sayfa tablosu alanı fiziksel RAM'de 896MB'nin ötesine erişmek için değiştirilerek kullanılmaktadır. Yani 32 bit sistemlerde kernel fiziksel RAM'in ilk 896MB'sine doğrudan ancak bunun ötesine sayfa tablosunun son 128 MB'lik bölgesini değiştirerek erişmektedir. 32 bit sistemlerde 896MB'nin ötesine dolaylı biçimde erişildiği için bu bölgeye "high memory zone" denilmektedir. Tabii 64 bit sistemlerde böyle bir problem yoktur. Çünkü bu sistemlerde yine sayfa tablolarının kernel alanı fiziksel RAM'i başından itibaren haritlandırmaktadır. Ancak 128TB'lik alan zaten şimdiki bilgisayarlara takılabilecek fiziksel RAM'in çok ötesindedir. Bu nedenle 64 bit sistemlerde "high memory zone" kavramı yoktur. Çekirdek kodların kernel alanın başlangıcı PAGE_OFFSET makrosuyla belirlenmiştir. Linux çekirdeği için fiziksel RAM temel olarak 3 bölgeye (zone) ayrılmıştır: ZONE_DMA ZONE_NORMAL ZONE_HIGHMEM ZONE_DMA ilgili sistemde disk ile RAM arasında transfer yapan DMA'nın erişebildiği RAM alanıdır. Bazı sistemlerde DMA tüm fiziksel RAM'in her yerine transfer yapamamaktadır. ZONE_NORMAL doğrudan çekirdeğin sayfa tablosu yoluyla haritalandırdığı fiziksel bellek bölgesidir. Intel 32 bit Linux sistemlerinde bu bölge ilk 896 MB'dir. Ancak 64 bit Linux sistemlerinde bu bölge tüm fiziksel RAM'i içermektedir. ZONE_HIGHMEM ise 32 bit sistemlerde çekirdeğin doğrudan haritalandıramadığı sayfa tablosunda değişiklik yapılarak erişilebilen fiziksel RAM alanıdır. 32 Linux sistemlerinde 896 MB'nin yukarısındaki fiziksel RAM ZONE_HIHMEM alanıdır. Yukarıda da belirttiğimiz gibi 64 bit Intel işlemcilerinde ZONE_HIGHMEM biçiminde bir alan yoktur. User mode programlarda kullandığımız malloc fonksiyonunun uyguladığı klasik tahsisat yöntemi "boş alan bağlı liste" denilen yöntemdir. Bu yöntemde yalnızca boş alanlar bir bağlı listede tutulmaktadır. Dolayısıyla malloc gibi bir fonksiyon bu bağlı listede uygun bir elemanı bağlı listeyi dolaşarak bulmaktadır. free fonksiyonu da tahsis edilmiş olan alanı bu boş bağlı listeye eklemektedir. Tabii free fonksiyonu aynı zamanda bağlı listedeki komşu alanları da daha büyük bir boş alan oluşturacak biçimde birleştirmektedir. Ancak bu klasik yöntem çekirdek heap sistemi için çok yavaş kalmaktadır. Bu nedenle çekirdeğin heap sistemi için hızlı çalışan tahsisat algoritmaları kullanılmaktadır. Eğer tahsis edilecek bloklar eşit uzunlukta olursa bu durumda tahsisat işlemi ve geri bırakmak işlemi O(1) karmaşıklıkta yapılabilir. Örneğin heap içerisindeki tüm blokların 16 byte uzunlukta olduğunu düşünelim. Bu durumda 16 byte'lık tahsisat sırasında uygun bir boş alan aramaya gerek kalmaz. Bir dizisi içerisinde boş alanlar tutulabilir. Bu boş alanlardan herhangi biri verilebilir. Tabii uygulamalarda tahsis edilecek alanların büyükleri farklı olmaktadır. İşte BSD ve Linux sistemlerindekullanılan "dilimli tahsisat sistemi (slab allocator)" denilen tahsisat sisteminin anahtar noktası eşit uzunlukta ismine "dilim (slab)" denilen blokların tahsis edilmesidir. Kernel içerisinde çeşitli nesneler için o nesnelerin uzunluğuna ilişkin farklı dilimli tahsisat sistemleri oluşturulmuştur. Örneğin bir proses yaratıldığında task_struct yapısı çekirdeğin heap alanında tahsis edilmektedir. İşte dilimli tahsisat sistemlerinden biri sizeof(struct task_struct) kadar dilimlerdne oluşan sistemdir. Böylece pek çok kernel nesnesi için ayrı dilimli tahisat sistemleri luşturulmuştur. Bunların yanı sıra ayrıca bir de genel kullanım için blok uzunlukları 32, 64, 96, 128, 192, 256 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, ... biçiminde olan farklı dilimli tahsisat sistemleri de bulundurulmuştur. Böylece kernek mod programcısı belli uzunlukta bir alan tahsis etmek istediğinde bu uzunlupa en yakın bu uzunluktan büyük bir dilimli tahsisat sistemini kullanır. Tabii kernel mode programcılar isterse kendi nesneleri için o nesnelerin uzunluğu kadar yeni dilimli tahsisat sistemleri de oluşturabilmektedir. Aslında dilimli tahsisat sisteminin hazırda bulundurduğu dilimler işletim sisteminin sayfa tahsisatı yapan başka bir tahsisat algoritmasından elde edilmektedir. Linux sistemlerinde sayfa temelinde tahsisat yapmak için kullanılan tahsisat sistemine "buddy allocator" denilmektedir. (CSD işletim sisteminde buna "ikiz blok sistemi" denilmektedir.) Çekirdek kodlamasında çekirdek alanında dinamik tahsisat yapmak için kullanılan en genel fonksiyon kmalloc isimli fonksiyondur. Bu fonksiyon aslında parametresiyle belirtilen uzunluğa en yakon önceden yaratılmış olan dilimli tahsisat sisteminden dilim vermektedir (yani blok tahsis etmektedir). Örneğin biz kmalloc fonksiyonu ile 100 byte tahsis etmek istesek 100 byte'lık blokların bulunduğu önceden yaratılmış bir dilimli tahsisat sistemi olmadığ için kmalloc 128 byte'lık bloklara sahip dilimli tahsisat sisteminden bir dilim tahsis ederek bize vermektedir. Tabii bu örnekte 28 byte boşuna tahsis edilmiş olacaktır. Ancak çekirdek tahsisat sisteminin amacı en uygun miktarda belleği tahsis etmek değil talep edilen miktarda belleği hızlı tahsis etmektir. kmalloc fonksiyonu ile tahsis edilen dilimler kfree fonksiyonu ile serbest bırakılmaktadır. Fonksiyonların prototipleri şöyledir: void *kmalloc (size_t size, int flags); void kfree (const void *objp); kmalloc fonksiyonunun birinci parametresi tahsis edilecek byte sayısını belirtir. İkincisi parametresi ise tahsis edilecek alan ve biçim hakkında çeşitli bayrakları içermektedir. Bu ikinci parametre çeşitli sembolik sabitlerden oluşturulmaktadır. Burada önemli birkaç bayrak şunlardır: -> GFP_KERNEL: Kernel alanı içerisinde normal tahsisat yapmak için kullanılır. Bu bayrak en sık bu kullanılan bayraktır. Burada eğer RAM doluysa işletim sistemi prosesi bloke ederek swap işlemi ile yer açabilmektedir. Bu işlem sırasında akış kernel mode'da bekleme kuruklarında bekletilebilir. Tahsisat işlemi ZONE_NORMAL alanından yapılmaktadır. -> GFP_NOWAIT: GFP_KERNEL gibidir. Ancak hazırda bellek yoksa proses uykuya yatılmaz. Fonksiyon başarısız olur. -> GFP_HIGHUSER: 32 bit sistemlerde ZONE_HIHMEM alanından tahssat yapar. -> GFP_DMA: İlgili sistemde DMA'nın erişebildiği fiziksel RAM alanından tahsisat yapar. kmalloc fonksiyonu başarı durumunda tahsis edilen alanın sanal bellek adresiyle başarısızlık durumunda NULL adresle geri dönmektedir. Çekirdek modülleri ve aygıt sürücüler dinamik tahsisat başarısız olursa tipik olarak -ENOMEM değerine geri dönmelidir. kfree fonksiyonu ise daha önce kmalloc ile tahsis edilmiş olan alanın başlangıç adresini parametre olarak almaktadır. Aşağıda daha önce yapmış olduğumuz boru aygıt sürücüsündeki kuyruk sistemini kmalloc fonksiyonu ile tahsis edilip kfree fonksiyonu ile serbest bırakılmasına örnek verilmiştir. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); struct QUEUE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; }; static struct QUEUE *g_queue; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } if ((g_queue = (struct QUEUE *)kmalloc(sizeof(struct QUEUE), GFP_KERNEL)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { kfree(g_queue); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_queue->count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_queue->count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_queue->count, size); if (g_queue->tail <= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_queue->pipebuf + g_queue->head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_queue->pipebuf, size2) != 0) return -EFAULT; g_queue->head = (g_queue->head + esize) % PIPE_BUFSIZE; g_queue->count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_queue->count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_queue->count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_queue->count, size); if (g_queue->tail >= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_queue->pipebuf + g_queue->tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_queue->pipebuf, buf + size1, size2) != 0) return -EFAULT; g_queue->tail = (g_queue->tail + esize) % PIPE_BUFSIZE; g_queue->count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Yukarıda da belirttiğimiz gibi istersek genel amaçlı kmalloc fonksiyonunu kullanmak yerine kendimiz tam istediğimiz büyüklükte dilimlere sahip olan yeni bir dilimli tahsisat sistemi yaratıp onu kullanabiliriz. Yeni bir dilimli tahsisat sisteminin yaratılması kmem_cache_create fonksiyonu ile yapılmaktadır. Fonksiyonun prototipi şöyledir: #include struct kmem_cache *kmem_cache_create( const char *name, unsigned int size, int align, slab_flags_t flags, void (*ctor)(void *) ); Fonksiyonun birinci parametresi yeni yaratılacak dilim sisteminin ismini belirtmektedir. Herhangi bir isim verilebilir. İkinci parametre dilimlerin büyüklüğünü belirtmektedir. Üçücnü parametre hizalama değerini belirtir. Bu parametre 0 geçilirse default hizalama kullanılır. Fonksiyonun dördüncü parametresi yaratılacak dilim sistemine ilişkin bazı özelliklerin belirlenmesi için kullanılmaktadır. Buradaki bayrakların önemli birkaç tanesi şöyledir: -> SLAB_NO_REAP: Fiziksel RAM'in dolması nedeniyle kullanılmayan dilimlerin otomatik olarak sisteme iade edileceği anlamına gelir. Uç durumlarda bu bayrak kulanılabilir. -> SLAB_HWCACHE_ALIGN: Bu bayrak özellikle SMP sistemlerinde işlemci ya da çekirdeklerin cache alanları için hizalama yapılmasının sağlamaktadır. Yaratım sırasında bu parametreyi kullanabilirsiniz. -> SLAB_CACHE_DMA: Bu parametre DMA alanında (DMA zone) tahsisat için kullanılmaktadır. Fonksiyonun son parametresi dilim sistemi yaratıldığında çağrılacak callback fonksiyonu belirtmektedir. Bu parametre NULL geçilebilir. Fonksiyon başarı durumunda kmem_cache_create fonksiyonu kmem_cache türünden bir yapı nesnesinin adresiyle başarısızlık durumunda NULL adrese geri dönmektedir. Başarısızlık durumunda aygıt sürücü fonksiyonunun -ENOMEM değeri ile geri döndürülmesi uygundur. Örneğin: if ((g_queue_cachep = kmem_cache_create("pipe-driver-cachep", sizeof(struct QUEUE), 0, SLAB_HWCACHE_ALIGN, NULL)) == NULL) { ... return -ENOMEM; } Yaratılmış olan bir dilim sisteminden tahsisatlar kmem_cache_alloc fonksiyonu ile yapılmaktadır. Fonksiyonun parametresi şöyledir: #include void *kmem_cache_alloc(kmem_cache_t *cache, int flags); Fonksiyonun birinci parametresi yaratılmış olan dilim sisteminin handle değerini, ikinci parametresi yaratım bayraklarını almaktadır. Bu bayraklar kmalloc fonksiyonundaki bayraklarla aynıdır. Yani örneğin bu parametreye GFP_KERNEL geçilebilir. Fonksiyon başarı durumunda tahsis edilen sanal adrese başarısızlık durumunda NULL adrese geri dönmektedir. Bu durumda aygıt sürücüdeki fonksiyonun -ENOMEM değeri ile geri döndürülmesi uygundur. Örneğin: if ((g_queue = (struct QUEUE *)kmem_cache_alloc(g_queue_cachep, GFP_KERNEL)) == NULL) { ... return -ENOMEM; } kmem_cache_alloc fonksiyonu ile tahsis edilen dinamik alan kmem_cache_free fonksiyonu ile serbest bırakılabilir. Fonksiyonun prototipi şöyledir: #include void kmem_cache_free(kmem_cache_t *cache, const void *obj); Fonksiyonun birinci parametresi dilim sisteminin handle değerini, ikincisi parametresi ise serbest bırakılacak dilimin adresini belirtmektedir. Örneğin: kmem_cache_free(g_queue_cachep, g_queue); kmem_cache_create fonksiyonu ile yaratılmış olan dilim sistemi kmem_cache_destroy fonksiyonu ile serbest bırakılabilir. Fonksiyonun prototipi şöyledir. #include int kmem_cache_destroy(kmem_cache_t *cache); Fonksiyon dilim sisteminin handle değerini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda negatif errno değerine geri döner. Örneğin: kmem_cache_destroy(g_queue_cachep); Pekiyi kmalloc yerine yeni bir dilimli tahsisat sisteminin yaratılması tercih edilmeli midir? Yukarıda da belirttiğimiz gibi kmalloc fonksiyonu da aslında önceden yaratılmış belli uzunluktaki dilim sistemlerinden tahsisat yapmaktadır. Ancak "çok sayıda aynı büyüklükte alanların" tahsis edildiği durumlarda programcının talep ettiği uzunlukta kendi dilim sistemini yaratması tavsiye edilebilir. Bunun dışında genel amaçlı kmalloc fonksiyonu tercih edilebilir. Örneğin boru aygıt sürücümüzde yeni bir dilim sisteminin yaratılmasına hiç gerek yoktur. Ancak bir aşağıda örnek vermek amacıyla boru aygıt sürücünde yeni bir dilim sistemi yarattık. Örneği inceleyiniz. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static wait_queue_head_t g_wqread; static wait_queue_head_t g_wqwrite; static DEFINE_SEMAPHORE(g_sem); struct QUEUE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; }; static struct QUEUE *g_queue; struct kmem_cache *g_queue_cachep; static int __init generic_init(void) { int result; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } if ((g_queue_cachep = kmem_cache_create("pipe-driver-cachep", sizeof(struct QUEUE), 0, SLAB_HWCACHE_ALIGN, NULL)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } if ((g_queue = (struct QUEUE *)kmem_cache_alloc(g_queue_cachep, GFP_KERNEL)) == NULL) { kmem_cache_destroy(g_queue_cachep); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } init_waitqueue_head(&g_wqread); init_waitqueue_head(&g_wqwrite); return 0; } static void __exit generic_exit(void) { kmem_cache_free(g_queue_cachep, g_queue); kmem_cache_destroy(g_queue_cachep); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (size == 0) return 0; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (g_queue->count == 0) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqread, g_queue->count > 0)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(g_queue->count, size); if (g_queue->tail <= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, g_queue->pipebuf + g_queue->head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, g_queue->pipebuf, size2) != 0) return -EFAULT; g_queue->head = (g_queue->head + esize) % PIPE_BUFSIZE; g_queue->count -= esize; up(&g_sem); wake_up_interruptible_all(&g_wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize, size1, size2; if (down_interruptible(&g_sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - g_queue->count < size) { up(&g_sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(g_wqwrite, PIPE_BUFSIZE - g_queue->count >= size)) return -ERESTARTSYS; if (down_interruptible(&g_sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - g_queue->count, size); if (g_queue->tail >= g_queue->head) size1 = MIN(PIPE_BUFSIZE - g_queue->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(g_queue->pipebuf + g_queue->tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(g_queue->pipebuf, buf + size1, size2) != 0) return -EFAULT; g_queue->tail = (g_queue->tail + esize) % PIPE_BUFSIZE; g_queue->count += esize; up(&g_sem); wake_up_interruptible_all(&g_wqread); return esize; } module_init(generic_init); module_exit(generic_exit); Aygıt sürücünün majör ve minör numaraları ne anlam ifade etmektedir? Majör numara aygıt sürücünün türünü belirtir. Minör numara ise aynı türden aygıt sürücülerin farklı örneklerini (instance'larını) belirtmektedir. Örneğin biz yukarıdaki "pipe-driver" aygıt sürücümüzün tek bir boruyu değil on farklı boruyu idare etmesini isteyebiliriz. Bu durumda aygıt sürücümüzün bir tane majör numarası ancak 10 tane minör numarası olacaktır. Aygıt sürücülerin majör numaraları aynı ise bunların kodları da aynıdır. O aynı kod birden fazla aygıt için işlev görmektedir. Örneğin seri portu kontrol eden bir aygıt sürücü söz konusu olsun. Ancak bilgissayarımızda dört seri port olsun. İşte bu durumda bu seri porta ilişkin aygıt dosyalarının hepsinin majör numaraları aynıdır. Ancak minör numaraları farklıdır. Ya da örneğin terminal aygıt sürücüsü bir tanedir. Ancak bu aygıt sürücü birden fazla terminali yönetebilmektedir. O halde her terminale ilişkin aygıt dosyasının majör numaraları aynı minör numaraları farklı olacaktır. Örneğin: /dev$ ls -l tty1 tty2 tty3 tty4 tty5 crw--w---- 1 root tty 4, 1 Haz 2 15:05 tty1 crw--w---- 1 root tty 4, 2 Haz 2 15:05 tty2 crw--w---- 1 root tty 4, 3 Haz 2 15:05 tty3 crw--w---- 1 root tty 4, 4 Haz 2 15:05 tty4 crw--w---- 1 root tty 4, 5 Haz 2 15:05 tty5 Pekiyi birden fazla aygıtı yönetecek (yani birden fazla minör numaraya sahip olan) bir aygıt sürücü nasıl yazılabilir? Her şeyden önce birden fazla minör numara kullanan aygıt sürücüleri yazarken dikkatli olmak gerekir. Çünkü tek bir kod birden fazla aynı türden bağımsız aygıtı idare edecektir. Dolayısıyla bu tür durumlarda bazı nesnelerin senkronize edilmesi gerekebilir. Birden fazla minör numara üzerinde çalışacak aygıt sürücüleri tipik olarak şöyle yazılmaktadır: -> Minör numara sayısının aşağıdaki gibi ndevices isimli parametre yoluyla komut satırından aşağıdaki gibi aygıt sürücüye aktarıldığını varsayacağız: #define NDEVICES 10 ... static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); Programcının majör ve minör numaraları tahsis etmesi gerekir. Yukarıda da yaptığımız gibi majör numara alloc_chrdev_region fonksiyonuyla dinamik olarak belirlenebilmektedir. Bu fonksiyon aynı zamanda belli bir minör numaradan başlayarak n tane minör numarayı da tahsis edebilmektedir. Örneğin: if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } Burada 0'ıncı minör numaradan ndevices tane minör numara için aygıt tahsisatı yapılmıştır. -> Her aygıt bir yapıyla temsil edilmelidir. Bunun için N elemanlı bir yapı dizisi yaratabilirsiniz. Bu dizi global düzeyde tanımlanabileceği gibi kmalloc fonksiyonuyla dinamik biçimde de tahsis edilebilir. Oluşturulan bu yapının içerisine struct cdev nesnesi de eklenmelidir. Örneğin: struct PIPE_DEVICE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static struct PIPE_DEVICE *g_pdevices; ... if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } Burada görüldüğü gibi her farklı borunun farklı bekleme kuyrukları ve semaphore nesnesi vardır. cdev yapı nesnesinin yapının içerisine yerleştirilmesinin amacı şleride görüleceği gibi bu adresten hareketle yapı nesnesinin adresinin elde edilmesini sağlamaktır. Bunun nasıl yapıldığı izleyen pragraflarda görülecektir. -> N tane minör numaralı aygıt için cdev_add fonksiyonuyla aygıtlar çekirdeğe eklenmelidir. Örneğin: for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); if ((result = cdev_add(&g_pdevices[i].cdev, dev, 1)) < 0) { for (k = 0; k < i; ++k) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "Cannot add device!...\n"); return result; } } Burada yapı dizisininin her elemanındaki elemanlara ilkdeğerleri verilmiştir. Sonra her boru için ayrı bir cdev nesnesi cdev_add fonksiyonu ile eklenmiştir. Eklemelerden biri başarısız olursa daha önce eklenenlerin de cdev_del fonksiyonu ile silindiğine dikkat ediniz. -> Bizim read, write gibi fonksiyonlarında file yapısı türünden adres belirten filp parametre değişkeni yoluyla DEVICE yapısına erişmemiz gerekir. Bu işlem dolaylı bir biçimde şöyle yapılmaktadır: -> Önce aygıt sürücünün open fonksiyonunda programcı inode yapısının i_cdev elemanından hareketle cdev nesnesinin içinde bulunduğu yapı nesnesinin başlangıç adresini container_of makrosuyla elde eder. Çünkü inode yapısının i_cdev elemanı cdev_add fonkisyonuyla eklenen cdev yapı nesnesinin adresini tutmaktadır. -> Programcı device nesnesinin adresini elde ettikten sonra onu file yapısının private_data elemanına yerleştirir. file yapısının private_data elemanı programcının kendisinin isteğe bağlı olarak yerleştirebileceği bilgiler için bulundurulmuştur. Bu işlemler aşağıdaki gibi yapılabilir: static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } -> Aygıt sürücünün read ve write fonksiyonları yazılır. -> release (close) işleminde yapılacak birtakım son işlemler varsa yapılır. -> Aygıt sürücünün exit fonksiyonunda yine tüm minör numaralar için cdev_del fonksiyonu çağrılır ve unregister_chrdev_region işlemi yapılır. 8) Birden fazla minör numara için çalışacak aygıt sürücülerin birden fazla aygıt dosdyası yaratması gerekir. Yani aygıt sürücüsü kaç minör numarayı destekliyorsa o sayıda aygıt dıosyalarının yaraılması gerekmektedir. Bu da onları yüklemek için kullandığımız load scriptinde değişiklik yapmayı gerektirmektedir. N tane minör numaraya ilişkin aygıt dosyası yaratacak biçimde yeni bir "loadmulti" isimli script aşağıdaki gibi yazılabilir: #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod ${module}$i c $major $i chmod $mode ${module}$i done Buradaki "loadmulti" script'i iki komut satırı argümanıyla aşağıdaki örnekteki gibi çalıştırılmalıdır: $ sudo ./loadmulti 10 pipe-driver ndevices=10 Burada "loadmulti" script'i hem aygıt sürücüyü yükleyecek hem de pipe-driver0, pipe-driver1, ..., pipedriver9 biçiminde aygıt dosyalarını yaratacaktır. Aşağıda yaratılmış olan örnek aygıt dosyalarına dikkat ediniz: crw-rw-rw- 1 root root 236, 0 Haz 7 22:09 pipe-driver0 crw-rw-rw- 1 root root 236, 1 Haz 7 22:09 pipe-driver1 crw-rw-rw- 1 root root 236, 2 Haz 7 22:09 pipe-driver2 crw-rw-rw- 1 root root 236, 3 Haz 7 22:09 pipe-driver3 crw-rw-rw- 1 root root 236, 4 Haz 7 22:09 pipe-driver4 crw-rw-rw- 1 root root 236, 5 Haz 7 22:09 pipe-driver5 crw-rw-rw- 1 root root 236, 6 Haz 7 22:09 pipe-driver6 crw-rw-rw- 1 root root 236, 7 Haz 7 22:09 pipe-driver7 crw-rw-rw- 1 root root 236, 8 Haz 7 22:09 pipe-driver8 crw-rw-rw- 1 root root 236, 9 Haz 7 22:09 pipe-driver9 Aygıt dosyalarının majör numaralarının hepsi aynıdır ancak minör numaraları farklıdır. Burada adeta birbirinden bağımsız 10 ayrı boru aygıtı var gibidir. Ancak aslında tek bir aygıt sürücü kodu bulunmaktadır. Tabii bizim benzer biçimde "unload" script'ini de tüm aygıt dosyalarını silecek biçimde düzeltmemiz gerekir. Bunun için "unloadmulti" scritp'ini aşağıdaki yazabiliriz: #!/bin/bash module=$2 mode=666 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done Bu scrip'te biz modülü önce çekirdekten sonra da "loadmulti" ile yarattığımız aygıt dosyalarını dosya sisteminden sildik. Script aşağıdaki örnekteki gibi kullanılmalıdır: sudo ./unloadmulti 10 pipe-driver Daha önce yapımış olduğumuz boru aygıt sürücücüsünün 10 farklı minör numarayı destekleyen biçimini aşağıda veriyoruz. * Örnek 1, /* pipe-driver.c */ #include #include #include #include #include #include #include #include #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define NDEVICES 10 #define PIPE_BUFSIZE 10 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; struct PIPE_DEVICE { unsigned char pipebuf[PIPE_BUFSIZE]; size_t head; size_t tail; size_t count; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static dev_t g_dev; static struct PIPE_DEVICE *g_pdevices; static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init generic_init(void) { int result; dev_t dev; int i, k; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); if ((result = cdev_add(&g_pdevices[i].cdev, dev, 1)) < 0) { for (k = 0; k < i; ++k) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "Cannot add device!...\n"); return result; } } return 0; } static void __exit generic_exit(void) { int i; for (i = 0; i < ndevices; ++i) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(g_dev, ndevices); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver-closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (pdevice->count == 0) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqread, pdevice->count > 0)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->count, size); if (pdevice->tail <= pdevice->head) size1 = MIN(PIPE_BUFSIZE - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, pdevice->pipebuf + pdevice->head, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_to_user(buf + size1, pdevice->pipebuf, size2) != 0) return -EFAULT; pdevice->head = (pdevice->head + esize) % PIPE_BUFSIZE; pdevice->count -= esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (PIPE_BUFSIZE - pdevice->count < size) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqwrite, PIPE_BUFSIZE - pdevice->count >= size)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(PIPE_BUFSIZE - pdevice->count, size); if (pdevice->tail >= pdevice->head) size1 = MIN(PIPE_BUFSIZE - pdevice->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(pdevice->pipebuf + pdevice->tail, buf, size1) != 0) return -EFAULT; if (size2 != 0) if (copy_from_user(pdevice->pipebuf, buf + size1, size2) != 0) return -EFAULT; pdevice->tail = (pdevice->tail + esize) % PIPE_BUFSIZE; pdevice->count += esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqread); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod ${module}$i c $major $i chmod $mode ${module}$i done /* unloadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done /* prog1.c */ #include #include #include #include #include #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len; if ((fd = open("pipe-driver5", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int size; ssize_t result; if ((pdriver = open("pipe-driver5", O_RDONLY)) == -1) exit_sys("open"); for (;;) { printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!..\n"); continue; } if (size == 0) break; if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aygıt sürücüden bilgi okumak için "read" fonksiyonun, aygıt sürücüye bilgi göndermek için ise "write" fonksiyonun kullanıldığını gördük. Ancak bazen aygıt sürücüye "write" fonksiyonunu kullanmadan bazı bilgilerin gönderilmesi, aygıt sürücüden "read" fonksiyonunu kullanmadan bazı bilgilerin alınması gerekebilmektedir. Bazen hiç bilgi okumadan ve bilgi göndermeden aygıt sürüceden bazı şeyleri yapmasını da isteyebiliriz. Bu tür bazı işlemlerin "read" ve "write" fonksiyonlarıyla yaptırılması mümkün olsa bile kullanışsızdır. Örneğin yukarıdaki boru aygıt sürücümüzde "(pipe-driver)" biz aygıt sürücüden kullandığı "FIFO" alanın uzunluğunu isteyebiliriz ya da bu alanın boyutunu değiştirmek isteyebiliriz. Bu işlemleri "read" ve "write" fonksiyonlarıyla yapmaya çalışsak aygıt sürücümüz sanki boruyu temsil eden kuyruktan okuma yazma yapmak istediğimizi sanacaktır. Tabii yukarıda da belirttiğimiz gibi zorlanırsa bu tür işlemler "read" ve "write" fonksiyonlarıyla yine de yapılabilir. Ancak böyle bir kullanımım mümkün hale getirilmesi ve "user mode" tan kullanılması oldukça zor olacaktır. İşte aygıt sürücüye komut gönderip ondan bilgi almak için genel amaçlı "ioctl" isminde özel bir POSIX fonksiyonu bulundurulmuştur. Linux sistemlerinde "ioctl" fonksiyonu "sys_ioctl" isimli sistem fonksiyonunu çağırmaktadır. "ioctl" fonksiyonunun parametrik yapısı şöyledir: #include int ioctl(int fd, unsigned long request, ...); Fonksiyonun birinci parametresi aygıt sürücüye ilişkin dosya betimleyicisini belirtir. İkinci parametre ileride açıklanacak olan komut kodudur. Programcı aygıt sürücüsünde farklı komutlar için farklı komut kodları (yani numaralar) oluşturur. Sonra bu komut kodlarını "switch" içerisine sokarak hangi numaralı istekte bulunulmuşsa ona yönelik işlemleri yapar. "ioctl" fonksiyonu iki parametreyle ya da üç parametreyle kullanılmaktadır. Yani fonksyonun üçüncü parametresi isteğe bağlıdır. Eğer bir veri transferi söz konusu değilse "ioctl" genellikle iki argümanla çağrılır. Ancak bir veri transferi söz konusu ise "ioctl" üç argümanla çağrılmalıdır. Bu durumda üçüncü argüman "user mode" taki transfer adresini belirtir. Tabii aslında bu üçüncü parametrenin veri transferi ile ilgili olması dolayısıyla da bir adres belirtmesi zorunlu değildir. "ioctl" fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. "errno" uygun biçimde set edilmektedir. "User mode" dan bir program aygıt sürücü için "ioctl" fonksiyonunu çağırdığında akış "user mode" dan kernel moda geçer ve aygıt sürücüdeki "file_operations" yapısının "unlocked_ioctl" elemanında belirtilen fonksiyon çağrılır. Bu fonksiyonun parametrik yapısı şöyle olmalıdır: long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); Fonksiyonun birinci parametresi yine dosya nesnesinin adresini, ikinci parametresei "ioctl" fonksiyonunda kullanılan komut kodunu (yani "ioctl" fonksiyonuna geçirilen ikinci argümanı) ve üçüncü parametresi de ek argümanı (yani "ioctl" fonksiyonuna geçirilen üçüncü argümanı) belirtmektedir. Tabii programcının eğer "ioctl" fonksiyonu iki argümanlı çağrılmışsa bu üçüncü parametreye erişmemesi gerekir. Bu fonksiyon başarı durumunda 0 değerine başarısızlık durumunda negatif hata koduna geri dönmelidir. Fakat bazen programcı doğrudan iletilecek değeri geri dönüş değeri biçiminde oluşturabilir. Bu durumda geri dönüş değeri pozitif değer olabilir. Örneğin: static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); ... static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release, .unlocked_ioctl = generic_ioctl }; "ioctl" işleminde "ioctl" fonksiyonunun ikinci parametresi olan kontrol kodu dört parçanın bit düzeyinde birleştirilmesiyle oluşturulmaktadır. Bu parçaların belli bir uzunlukları vardır. Ancak bu parçalara ilişkin bitlerin 32 bit içerisinde belli pozisyonlara yerleştirilmesini kolaylaştırmak için "_IOC" isimli bir makro bulundurulmuştur. Bu makronun parametreleri şöyledir: _IOC(dir, type, nr, size) Bu makro buradaki parçaları bit düzeyinde birleştirerek bir 4 byte'lık bir tamsayı biçiminde vermektedir. Makronun parametrelerini oluşturan dört parçanın anlamları ve bit uzunlukları şöyledir: -> dir (direction): Bu 2 bitlik bir alandır ([30, 31] bitler). Burada kullanılacak sembolik sabitler "_IOC_NONE", "_IOC_READ", "_IOC_WRITE" ve "_IOC_READ|_IOC_WRITE" biçimindedir. Buradaki "_IOC_READ" aygıt sürücüden bilgi alınacağını "_IOC_WRITE" ise aygıt sürücüye bilgi gönderileceğini belirtmektedir. Buradaki yön "ioctl" sistem fonksiyonu tarafından dosyanın açış moduyla kontrol edilmemektedir. Örneğin biz buradaki yönü "_IOC_READ|_IOC_WRITE" biçiminde vermiş olsak bile dosyası "O_RDONLY" modunda açıp bu ioctl işlemini yapabiliriz. Eğer programcı böyle bir kontrol yapmak istiyorsa aygıt sürücünün "ioctl" fonksiyonu içerisinde bu kontrolü yapabilir. -> type: Bu 8 bitlik bir alandır ([8, 15] bitleri). Bu alana aygıt sürücüyü yazan istediği herhangi bir byte'ı verebilir. Genellikle bu byte bir akarkter sabiti olarak verilmektedir. Buna "magic number" da denilmektedir. -> nr: Bu 8 bitlik bir alandır ([0, 7] bitleri). Programcı tarafından kontrol koduna verilen sıra numarasını temsil etmektedir. Genellikle aygıt sürücü programcıları 0'dan başlayarak her koda bir numara vermektedir. -> size: Bu 14 bitlik bir alandır ([16:29] bitleri). Bu alan kaç byte'lık bir transferin yapılacağını belirtmektedir. Buradaki "size" değeri aslında çekirdek tarafından kullanılmamaktadır. Dolayısıyla biz 14 bitten daha büyük transferleri de yapabiliriz. Kullanım kolaylığı sağlamak için genellikle "_IOC" makrosu bir sembolik sabit biçiminde define edilir. Örneğin: #define PIPE_MAGIC 'x' #define IOC_PIPE_GETBUFSIZE _IOC(_IOC_READ, PIPE_MAGIC, 0, 4) Aslında "_IOC" makrosundan daha kolay kullanılabilen aşağıdaki makrolar da oluşturulmuştur: #ifndef __KERNEL__ #define _IOC_TYPECHECK(t) (sizeof(t)) #endif #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) Bu makrolarda "_IOC" makrosunun birinci parametresinin artık belirtilmediğine dikkat ediniz. Çünkü makrolar zaten isimlerine göre "_IOC" makrosunun birinci parametresini kendisi oluşturmaktadır. Ayrıca artık uzunluk (size parametresi) byte olarak değil tür olarak belirtilmelidir. Makrolar bu türleri "sizeof" operatörüne kendisi sokmaktadır. Görüldüğü gibi, -> "_IO" makrosu veri transferinin söz konusu olmadığı durumda, -> "_IOR" aygıt sürücüden okuma yapıldığı durumda, -> "_IOW" aygıt sürücüye yazma yapıldığı durumda, -> "_IOWR" ise aygıt sürücüden hem okuma hem de yazma yapıldığı durumlarda kullanılmaktadır. Örneğin: #define PIPE_MAGIC 'x' #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_MAGIC, 0, int) "ioctl" için kontrol kodları hem aygıt sürücünün içerisinden hem de "user mode" dan kullanılacağına göre ortak bir başlık dosyasının oluşturulması uygun olabilir. Burada "ioctl" kontrol kodları bulundurulabilir. Örneğin boru aygıt sürücümüz için "pipe-driver.h" dosyası açağıdaki gibi düzenlenebilir: // pipe-driver.h #ifndef PIPEDRIVER_H_ #define PIPEDRIVER_H_ #include #include #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #endif Aygıt sürücüdeki ioctl fonksiyonunu yazarken iki noktaya dikkat etmek gerekir: -> "ioctl" fonksiyonun üçüncü parametresi "unsigned long" türden olmasına karşın aslında genellikle "user mod" programcısı buraya bir nesnesin adresini geçirmektedir. Dolayısıyla bu transfer adresine aktarım gerekmektedir. Bunun için "copy_to_user", "copy_from_use", "put_user", "get_user" gibi "adresin geçerliliğini sorguladıktan sonra tranfers yapan fonksiyonlar" kullanılabilir. -> "User mod" programcısının olmayan bir komut kodu girmesi durumunda "ioctl" fonksiyonu "-ENOTTY" değeri ile geri döndürülmelidir. Bu tuhaf hata kodu ("TTY", "tele type terminal" sözcüklerinden kısaltmadır) tarihsel bir durumdan kaynaklanmaktadır. Bu hata kodu için "user mode" da "Inappropriate ioctl for device" biçiminde bir hata yazısı elde edilmektedir. User moddaki "ioctl" fonksiyonu başarı durumunda 0 değerine geri döndüğü için aygıt sürücüsündeki "ioctl" fonksiyonu da genel olarak başarı durumunda 0 ile geri döndürülmelidir. Yukarıda da belirttiğmiz gibi olmayan bir "ioctl" kodu için agıt sürücüdeki fonksiyonun "-ENOTTY" ile geri döndrülmesi uygundur. Bazı aygıt sürücülerinde başarı durumunda aygıt sürücüden bilgi "ioctl" fonksiyonunun üçüncü parametresi yoluyla değil geri dönüş değeri yoluyla elde edilmektedir. Bu durumda aygıt sürücüdeki "ioctl" fonksiyonu pozitif değerle de geri döndürülebilir. Ancak bu durum seyrektir. Biz transferin "ioctl" fonksiyonunun üçüncü parametresi yoluyla yapılmasını tavsiye ediyoruz. Aygıt sürücüdeki "ioctl" fonksiyonu tipik olarak bir "switch" deyimi ile gerçekleştirilmektedir. Örneğin, static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_PIPE_GETBUFSIZE: // ... break; default: return -ENOTTY; } return 0; } Burada "switch" deyiminin "default" bölümünde fonksiyonun "-NOTTY" değeri ile geri döndürüldüğüne dikkat ediniz. Tabii fonksiyon üçüncü parametresi ile belirtilen transfer adresi geçersiz bir adrezse yine "-EFAULT" değeri ile döndürülmelidir. Aşağıdaki örnekte boru yagıt sürücüsüsünün kullandığı boru uzunluğu "IOC_PIPE_GETBUFSIZE" "ioctl" koduyla elde edilip "IOC_PIPE_SETBUFSIZE" fonksiyomıyla değiştirilebilmektedir. Buradaki "IOCTL" kodları şöyle oluşturulmuştur: #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETCOUNT _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 1, size_t) #define IOC_PIPE_SETBUFSIZE _IOW(PIPE_DRIVER_MAGIC, 2, size_t) #define IOC_PIPE_PEEK _IOWR(PIPE_DRIVER_MAGIC, 3, struct PIPE_PEEK) Ancak bu örnek için yukarıda vermiş olduğumuz boru aygıt sürücüsünde bazı değişiklikler yaptık. Bu değişiklikler şunlardır: -> Artık borunun uzunluğu "PIPE_DEVICE" yapısının içerisinde tutulmaya başlanmıştır: struct PIPE_DEVICE { unsigned char *pipebuf; size_t head; size_t tail; size_t count; size_t bufsize; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; Buradaki "bufsize" elemanı ilgili borunun uzunluğunu belirtmektedir. Default uzunluk test için kolaylık sağlamak amacıyla yine 10 olarak tutulmuştur. Bu değişiklik sayesinde artık her minör numaraya ilişkin boru uzunluğu farklılaşabilecektir. -> Aygıt sürücümüz içerisindeki "ioctl" fonksiyonu aşağıdaki gibi yazılmıştır: static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct PIPE_DEVICE *pdevice; printk(KERN_INFO "ioctl"); pdevice = (struct PIPE_DEVICE *)filp->private_data; switch (cmd) { case IOC_PIPE_GETCOUNT: return put_user(pdevice->count, (size_t *)arg); case IOC_PIPE_GETBUFSIZE: return put_user(pdevice->bufsize, (size_t *)arg); case IOC_PIPE_SETBUFSIZE: return set_bufsize(pdevice, arg); case IOC_PIPE_PEEK: return read_peek(pdevice, arg); default: return -ENOTTY; } return 0; } Burada gördüğünüz gibi ilgili minör numaradaki borudaki byte sayısı "IOC_PIPE_GETCOUNT", uzunluğu ise "IOC_PIPE_GETBUFSIZE" ioctl kodu ile alınmakta ve bu boru uzunluğu uzunluk "IOC_PIPE_SETBUFSIZE" ioctl kodu ile değiştirilebilmektedir. Boru için kullanılan tampon değiştirilirken eşzamanlı erişimlere dikkat edilmelidir. Çünkü daha önceden de belirttiğimiz gibi aygıt sürücünün içerisindeki fonksiyonlar farklı prosesler tarafından aynı anda çağrılabilmektedir. Bu tür durumlarda daha önce görmüş olduğumuz çekirdek senkronizasyon nesneleri ile işlemlerin senktronize edilmesi gerekmektedir. Örneğimizdeki senkronizasyon "PIPE_DEVICE" yapısının içeriisndeki semaphore nesnesi yoluyla yapılmıştır. "IOC_PIPE_PEEK" "ioctl" kodu borudan atmadan okuma yapmakta kullanılmaktadır. Normal olarak borudan "read" fonksiyonu ile okuma yapıldığında okunanlar borudan atılmaktadır. Ancak bu "ioctl" kodu ile borudan okuma yapıldığında okunanlar borudan atılmamaktadır. Bu "ioctl" kodu için aşağıdaki gibi bir yapı oluşturulmuştur: struct PIPE_PEEK { size_t size; void *buf; }; Yapının "size" elemanı kaç byte "peek" işleminin yapılacağını, "buf" elemanı ise "peek" edilen byte'ların yerleştirileceği adresi belirtmektedir. Tabii boruda mevcut olan byte sayısından daha fazla byte "peek" edilmek istenirse boruda olan kadar byte "peek" edilmektedir. "Peek" edilen byte sayısı aygıt sürücü tarafından yapının size elemanına aktarılmaktadır. Aygıt sürücümüzü yine "loadmulti" script'i ile aşağıdaki gibi yükleyebilirsiniz: $ sudo ./loadmulti 10 pipe-driver ndevices=10 Aygıt sürücünün çekirdekten atılması da yine "unloadmulti" script'i ile yapılabilir: $ sudo ./unloadmulti 10 pipe-driver Aşağıda örneğin tüm modlarını veriyoruz. * Örnek 1, /* pipe-driver.h */ #ifndef PIPEDRIVER_H_ #define PIPEDRIVER_H_ #include #include struct PIPE_PEEK { size_t size; void *buf; }; #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETCOUNT _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 1, size_t) #define IOC_PIPE_SETBUFSIZE _IOW(PIPE_DRIVER_MAGIC, 2, size_t) #define IOC_PIPE_PEEK _IOWR(PIPE_DRIVER_MAGIC, 3, struct PIPE_PEEK) #endif /* pipe-driver.c */ #include #include #include #include #include #include #include #include #include "pipe-driver.h" #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define NDEVICES 10 #define DEF_PIPE_BUFSIZE 10 #define MAX_PIPE_BUFSIZE 131072 /* 128K */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release, .unlocked_ioctl = generic_ioctl }; struct PIPE_DEVICE { unsigned char *pipebuf; size_t head; size_t tail; size_t count; size_t bufsize; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg); static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg); static dev_t g_dev; static struct PIPE_DEVICE *g_pdevices; static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init generic_init(void) { int result; dev_t dev; int i, k; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } for (i = 0; i < ndevices; ++i) { g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; g_pdevices[i].bufsize = DEF_PIPE_BUFSIZE; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); g_pdevices[i].pipebuf = (char *)kmalloc(DEF_PIPE_BUFSIZE, GFP_KERNEL); result = cdev_add(&g_pdevices[i].cdev, dev, 1); if (g_pdevices[i].pipebuf == NULL || result < 0) { if (g_pdevices[i].pipebuf != NULL) kfree(g_pdevices[i].pipebuf); for (k = 0; k < i; ++k) { cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices[k].pipebuf); } kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "Cannot add device!...\n"); return result; } } return 0; } static void __exit generic_exit(void) { int i; for (i = 0; i < ndevices; ++i) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); unregister_chrdev_region(g_dev, ndevices); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (pdevice->count == 0) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqread, pdevice->count > 0)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->count, size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, pdevice->pipebuf + pdevice->head, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, pdevice->pipebuf, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->head = (pdevice->head + esize) % pdevice->bufsize; pdevice->count -= esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if (size > pdevice->bufsize) size = pdevice->bufsize; while (pdevice->bufsize - pdevice->count < size) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqwrite, pdevice->bufsize - pdevice->count >= size)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->bufsize - pdevice->count, size); if (pdevice->tail >= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(pdevice->pipebuf + pdevice->tail, buf, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(pdevice->pipebuf, buf + size1, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->tail = (pdevice->tail + esize) % pdevice->bufsize; pdevice->count += esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqread); return esize; } static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct PIPE_DEVICE *pdevice; printk(KERN_INFO "ioctl"); pdevice = (struct PIPE_DEVICE *)filp->private_data; switch (cmd) { case IOC_PIPE_GETCOUNT: return put_user(pdevice->count, (size_t *)arg); case IOC_PIPE_GETBUFSIZE: return put_user(pdevice->bufsize, (size_t *)arg); case IOC_PIPE_SETBUFSIZE: return set_bufsize(pdevice, arg); case IOC_PIPE_PEEK: return read_peek(pdevice, arg); default: return -ENOTTY; } return 0; } static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg) { char *new_pipebuf; size_t size; if (arg > MAX_PIPE_BUFSIZE) return -EINVAL; if (arg <= pdevice->count) return -EINVAL; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if ((new_pipebuf = (char *)kmalloc(arg, GFP_KERNEL)) == NULL) { up(&pdevice->sem); return -ENOMEM; } if (pdevice->count != 0) { if (pdevice->tail <= pdevice->head) { size = pdevice->bufsize - pdevice->head; memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, size); memcpy(new_pipebuf + size, pdevice->pipebuf, pdevice->count - size); } else memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, pdevice->count); } pdevice->head = 0; pdevice->tail = pdevice->count; kfree(pdevice->pipebuf); pdevice->pipebuf = new_pipebuf; pdevice->bufsize = arg; up(&pdevice->sem); return 0; } static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg) { size_t esize, size1, size2; struct PIPE_PEEK *userpp = (struct PIPE_PEEK *)arg; struct PIPE_PEEK pp; int status = 0; if (copy_from_user(&pp, userpp, sizeof(struct PIPE_PEEK)) != 0) return -EFAULT; if (pp.size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; esize = MIN(pdevice->count, pp.size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(pp.buf, pdevice->pipebuf + pdevice->head, size1) != 0) { status = -EFAULT; goto EXIT; } if (size2 != 0) if (copy_to_user(pp.buf + size1, pdevice->pipebuf, size2) != 0) { status = -EFAULT; goto EXIT; } if (put_user(esize, &userpp->size) != 0) status = -EFAULT; EXIT: up(&pdevice->sem); return status; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod ${module}$i c $major $i chmod $mode ${module}$i done /* unloadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done /* prog1.c */ #include #include #include #include #include #include #include "pipe-driver.h" #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len, bufsize, new_bufsize; if ((fd = open("pipe-driver5", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (buf[0] == '!') { new_bufsize = atoi(&buf[1]); printf("%zd\n", new_bufsize); if (ioctl(fd, IOC_PIPE_SETBUFSIZE, new_bufsize) == -1) exit_sys("ioctl"); if (ioctl(fd, IOC_PIPE_GETBUFSIZE, &bufsize) == -1) exit_sys("ioctl"); printf("new pipe buffer size is %zu\n", bufsize); } else { len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #include "pipe-driver.h" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int count, size; ssize_t result; struct PIPE_PEEK pp; char *peekbuf; if ((pdriver = open("pipe-driver5", O_RDONLY)) == -1) exit_sys("open"); for (;;) { if (ioctl(pdriver, IOC_PIPE_GETCOUNT, &count) == -1) exit_sys("ioctl"); printf("There are (is) %d byte(s) in the pipe\n", count); printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if (size < 0) { pp.size = -size; if ((pp.buf = malloc(-size)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (ioctl(pdriver, IOC_PIPE_PEEK, &pp) == -1) exit_sys("ioctl"); peekbuf = (char *)pp.buf; for (size_t i = 0; i < pp.size; ++i) putchar(peekbuf[i]); putchar('\n'); free(pp.buf); } else { if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> Belli bir anda yüklenmiş olan modüller "/proc/modules" dosyasından elde edilebilir. Bu dosya bir "text" dosyadır. Dosyanın her satırında bir "kernel" modülün bilgisi vardır. Örneğin: "$ cat /proc/modules" helloworld 16384 0 - Live 0x0000000000000000 (OE) vmw_vsock_vmci_transport 32768 2 - Live 0x0000000000000000 vsock 40960 3 vmw_vsock_vmci_transport, Live 0x0000000000000000 snd_ens1371 28672 2 - Live 0x0000000000000000 snd_ac97_codec 131072 1 snd_ens1371, Live 0x0000000000000000 gameport 20480 1 snd_ens1371, Live 0x0000000000000000 ac97_bus 16384 1 snd_ac97_codec, Live 0x0000000000000000 binfmt_misc 24576 1 - Live 0x0000000000000000 intel_rapl_msr 20480 0 - Live 0x0000000000000000 .... Aslında yüklü modüllerin bilgileri "lsmod" isimli bir yardımcı programla da görüntülenebilmektedir. Tabii "lsmod" aslında "/proc/modules" dosyasını okuyup onu daha anlaşılır biçimde görüntülemektedir. >> "insmod" ile yüklediğimiz her modül için "/sys/module" dizinin içerisinde ismi modül ismiyle aynı olan bir dizin yaratılmaktadır. "/proc/modules" dosyası ile bu dizini karıştırmayınız. "/proc/modules" dosyasının satırları yüklü olan modüllerin isimlerini ve bazı temel bilgilerini tutmaktadır. Modüllere ilikin asıl önemli bilgiler "kernel" tarafından "/sys/modules" dizininde tutulmaktadır. "sys" dosya sistemi de "proc" dosya sistemi gibi "kernel" tarafından bellek üzerinde oluşturulan ve içeriği "kernel" tarafından güncellenen bir dosya sistemidir. Örneğin "helloworld.ko" modülünü yükledikten sonra bu dizinin içeriği şöyle görüntülenmektedir: "$ ls /sys/module/helloworld -l" toplam 0 -r--r--r-- 1 root root 4096 Mar 22 21:25 coresize drwxr-xr-x 2 root root 0 Mar 22 21:25 holders -r--r--r-- 1 root root 4096 Mar 22 21:25 initsize -r--r--r-- 1 root root 4096 Mar 22 21:25 initstate drwxr-xr-x 2 root root 0 Mar 22 21:25 notes -r--r--r-- 1 root root 4096 Mar 22 21:25 refcnt drwxr-xr-x 2 root root 0 Mar 22 21:25 sections -r--r--r-- 1 root root 4096 Mar 22 21:25 srcversion -r--r--r-- 1 root root 4096 Mar 22 21:25 taint --w------- 1 root root 4096 Mar 22 21:22 uevent >> "errno" değişkeni aslında "libc" kütüphanesinin (standart C ve POSIX kütüphanesi) içerisinde tanımlanmış bir değişkendir. "Kernel" modda yani "kernel" ın içerisinde "errno" isimli bir değişken yoktur. >>> Bu nedenle kerneldaki fonksiyonlar POSIX fonksiyonları gibi başarısızlık durumunda "-1" ile geri dönüp "errno" değişkeninin "set" etmezler. "Kernel" içerisindeki fonksiyonlar başarısızlık durumunda negatif "errno" değeri ile geri dönerler. Örneğin, "open" POSIX fonksiyonu "sys_open" isimli "kernel" içerisinde bulunan sistem fonksiyonunu çağırdığında onun negatif bir değerle geri dönüp dönmediğine bakar. Eğer "sys_open" fonksiyonu negatif değerle geri dönerse bu durumda bu değerin pozitiflisini "errno" değişkenine yerleştirip "-1" ile geri dönmektedir. Başka bir deyişle aslında bizim çağırdığımız "int" geri dönüş değerine sahip POSIX fonksiyonları, sistem fonksiyonlarını çağırıp, o fonksiyonlar negatif bir değerle geri dönmüş ise bir hata oluştuğunu düşünerek o negatif değerin pozitiflisini "errno" değişkenine yerleştirip "-1" ile geri dönmektedir. "Kernel" modül yazan programcıların da bu geleneğe uyması iyi bir tekniktir. Şöyleki: if (some_control_failed) /* burada kontrol yapılıyor */ return -EXXXX; /* fonksiyon başarısız ise negatif errno değeriyle geri döndürülüyor */ Özetle biz "kernel" içerisindeki geri dönüş değeri "int" olan bir fonksiyonu çağırdığımızda onun başarılı olup olmadığını geri dönüş değerinin negatif olup olmadığı ile kontrol ederiz. Eğer çağırdığımız fonksiyonun geri dönüş değeri negatif ise onun pozitif hali başarısızlığa ilişkin "errno" numarasını vermektedir. >>> POSIX arayüzünde adrese geri dönen fonksiyonlar genel olarak başarısızlık durumunda "NULL" adrese geri dönmektedir. Oysa "kernel" kodlarında adrese geri dönen fonksiyonlar başarısız olduklarında yine sanki bir adresmiş gibi negatif "errno" değerine geri dönerler. Örneğin şöyle bir "kernel" fonksiyonu olsun: void *foo(void); Biz bu fonksiyonu kernel modülümüz içerisinde çağırdığımızda eğer fonksiyon başarısızsa negatif "errno" değerini bir adres gibi geri döndürmektedir. Negatif küçük değerlerin ikiye tümleyen aritmetiğinde başı birlerle dolu olan bir sayı olacağına dikkat ediniz. Örneğin, bu "foo" fonksiyonu "EPERM" değeri ile geri dönüyor olsun. "EPERM" değeri "1" dir. "64-bit" sistemdeki "-1" değeri ise şöyledir: FF FF FF FF FF FF FF FF Bu değer ise çok yüksek bir adres gibidir. O zaman eğer fonksiyon çok yüksek bir adres geri döndürdüyse başarısız olduğu sonucunu çıkartabiliriz. Tabi bu işlemler için makrolar bulundurulmuştur. Kernel kodlarındaki "ERR_PTR" isimli makro ya da "inline" fonksiyon, bir tamsayı değeri alıp onu adres türüne dönüştürmektedir. Bu nedenle adrese geri dönen fonksiyonlarda aşağıdaki gibi kodlar görebilirsiniz: void *foo(void) { ... if (expression) return ERR_PTR(-EXXXX); ... } "ERR_PTR" aşağıdaki gibi tanımlanmıştır: static inline void *ERR_PTR(long error) { return (void *) error; } Bu işlemin tersi de "PTR_ERR" makrosu ya da iinline fonksiyonu ile yapılmaktadır. Yani "PTR_ERR" bir adresi alıp onu tamsayıya dönüştürmektedir. Bu fonksiyon da şöyle tanımlanmıştır: static inline long PTR_ERR(const void *ptr) { return (long) ptr; } Pekiyi bir adres değerinin içerisinde "errno" hata kodunun olduğunu nasıl anlarız? İşte negatif "errno" değerleri bir adres gibi ele alındığında adeta adres alanın sonındaki adresler gibi bir görünümde olacaktır. "errno" değerleri için toplamda ayrılan sayılar da sınırlı olduğu için kontrol kolaylıkla yapılabilir. Ancak bu kontrol için "IS_ERR" isimli bir makro ya da inline fonksiyon da bulundurulmuştur: static inline long IS_ERR(const void *ptr) { return (unsigned long)ptr > (unsigned long)-4095; } Burada fonksiyon adresin adres alanının son "4095" adresinden biri içerisinde mi kontrolünü yapmaktadır. Negatif "errno" değerlerinin hepsi bu aralıktadır. Tabii "4095" errno değeri yoktur. Burada geleceğe uyumu korumak için "4095" lik bir alan ayrılmıştır. Bu durumda kernel kodlarında adrese geri dönen fonksiyonların başarısızlığı aşağıdaki gibi kontrol edilebilmektedir. Şöyleki: void *ptr; ptr = foo(); if (IS_ERR(ptr)) return PTR_ERR(ptr) Kernel modül programcılarının da buradaki konvansiyona uygun kod yazması iyi bir tekniktir. Linux çekirdeğindeki "EXXX" sembolik sabitleri POSIX arayüzündeki "EXXX" sabitleriyle aynı değerdedir. >> Bir kernel modülü yazarken o modül le ilgili önemli bazı belirlemeler "modül makroları" denilen "MODULE_XXXX" biçimindeki makrolarla yapılmaktadır. Her ne kadar bu modül makrolarının bulundurulması zorunlu değilse de şiddetle tavsiye edilmektedir. En önemli üç makronun tipik kullanımı şöyledir: MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("General Character Device Driver"); Modül lisansı herhangi bir "open-source" lisans olabilir. Tipik olarak "GPL" tercih edilmektedir. "MODULE_AUTHOR" makrosu ile modülün yazarı belirtilir. "MODULE_DESCRPTION" modülün ne iş yaprığına yönelik kısa bir başlık yazısı içermektedir. Bu makrolar global alanda herhangi bir yere yerleştirilebilmektedir. >> Tıpkı user modda olduğu gibi aygıt sürücülerde de basit atama, artırma, eksiltme gibi işlemlerin atomic yapılmasını sağlayan özel fonksiyonlar vardır. Aslında bu işlemler thread'ler konusunda görmüş olduğumuz gcc'nin built-in atomic fonksiyonlarıyla yapılabilir. Ancak çekirdek içerisindeki fonksiyonların kullanılması uyum bakımından daha uygundur. Bu fonksiyonların hepsi nesneyi atomic_t türü biçiminde istemektedir. Bu aslında içerisinde yalnızca int bir nesne olan bir yapı türüdür. Bu yapı nesnesinin içerisindeki değeri alan atomic_read isimli bir fonksiyon da vardır. Atomic fonksiyonların bazıları şunlardır: #include atomic_set atomic_add atomic_sub atomic_inc atomic_dec ... Bu fonksiyonların hepsinin atomic_t türünden nesnenin adresini alan bir parametresi vardır. atomic_set fonksiyonunun ikinci parametresi set edilecek değeri almaktadır. Yukarıda da belirttiğimiz gibi atomic_t türü aslında int bir elemana sahip bir yapı biçimindedir. atomic_t türünden bir değişkene ilkdeğer vermek için ATOMIC_INIT makrosu da kullanılabilir. Örneğin: atomic_t g_count = ATOMIC_INIT(0); Yukarıda da belirttiğimiz gibi atomic_t nesnesi içerisindeki değeri atomic_read makrosuyla elde edebiliriz. Örneğin: val = atomic_read(&g_count); Bit işlemlerine yönelik atomik işlemler de yapılabilmektedir: void set_bit(nr, void *addr); void clear_bit(nr, void *addr); void change_bit(nr, void *addr); test_bit(nr, void *addr); int test_and_set_bit(nr, void *addr); int test_and_clear_bit(nr, void *addr); int test_and_change_bit(nr, void *addr); >> Aslında bekleme kuyrukları wait_queue_entry isimli yapı nesnelerinden oluşan bir çift bağlı listedir. wait_queue_head_t yapısı da bağlı listenin ilk ve son elemanlarının adresini tutmaktadır: wait_queue_head ---> wait_queue_entry <-----> wait_queue_entry <-----> wait_queue_entry <-----> wait_queue_entry ... Çekirdek kodlarında bu yapılar "include/linux/wait.h" dosyası içersinde aşağıdaki gibi bildirilmiştir: struct wait_queue_head { spinlock_t lock; struct list_head head; }; struct wait_queue_entry { unsigned int flags; void *private; wait_queue_func_t func; struct list_head entry; }; >> Biz aygıt sürücü kodumuzda o anda quanta süresini bırakıp çizelgeleyicinin kendi algortimasına göre sıradaki thread'i çizelgelemesini sağlayabiliriz. Bunun schedule isimli fonksiyon kullanılmaktadır. Bu fonksiyon bloke oluşturmamaktadır. Yalnızca thread'ler arası geçiş (context switch) oluşturmaktadır. Fonksiyon herhangi bir parametre almamaktadır: #include void schedule(void); >> Biz kernel mode'da kod yazarken belli bir fiziksel adrese erişmek istersek onun sanal adresini bulmamız gerekir. Bu işin manuel yapılması yerine bunun için __va isimli makro kullanılmaktadır. Biz bu makroya bir fiziksel adres veririz o da bize o fiziksel adrese erişmek için gereken sanal adresi verir. Benzer biçimde bir sanal adresin fiziksel RAM karşılığını bulmak için de __pa makrosu kullanılmaktadır. Biz bu makroya sanal adresi veririz o da bize o sanal adresin aslında RAM'deki hangi fiziksel adres olduğunu verir. __va makrosu parametre olarak unsigned long biçiminde fiziksel adresi alır, o fiziksel adrese erişmek için gerekli olan sanal adresi void * türünden bize verir. __pa makrosu bunun tam tersini yapmaktadır. Bu makro bizden unsigned long biçiminde sanal adresi alır. O sanal adrese sayfa tablo tablosunda karşı gelen fiziksel adresi bize verir. Kernel mode'da RAM'in her yerine erişebildiğimize ve bu konuda bizi engelleyen hiçbir mekanizmanın olmadığına dikkat ediniz. >> Linux'un dosya sistemi önemli üç yapı vardır. Bunlar file, inode ve dentry yapılarıdır. file isimli yapıya biz "dosya nesnesi" demiştik. Anımsanacağı gibi ne zaman bir dosya açılsa dosya betimleyici tablosunda dosya betimleyicisi denilen bir indeks bu dosya nesnesini göstermektedir. Biz user mode'taki dosya işlemlerinde bu konuyu zaten açıklamıştık. Ancak kısaca bir anımsatma yapalım: Dosya Betimeleyici Tablosu -------------------------- 0 ----> dosya nesnesi (struct file) 1 ----> dosya nesnesi (struct file) 2 ----> dosya nesnesi (struct file) 3 ----> dosya nesnesi (struct file) .... Dosya nesnesi "açık dosyaların bilgilerini" tutmaktadır. Aşağıda file yapısının mevcut çekirdeklerdeki içeriğini görüyorsunuz: struct file { union { /* fput() uses task work when closing and freeing file (default). */ struct callback_head f_task_work; /* fput() must use workqueue (most kernel threads). */ struct llist_node f_llist; unsigned int f_iocb_flags; }; /* * Protects f_ep, f_flags. * Must not be taken from IRQ context. */ spinlock_t f_lock; fmode_t f_mode; atomic_long_t f_count; struct mutex f_pos_lock; loff_t f_pos; unsigned int f_flags; struct fown_struct f_owner; const struct cred *f_cred; struct file_ra_state f_ra; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif /* needed for tty driver, and maybe others */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct hlist_head *f_ep; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; errseq_t f_sb_err; /* for syncfs */ } __randomize_layout __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */ inode yapısı dosyanın diskteki bilgilerini tutmaktadır. Yani aynı dosya üç kez açılsa çekirdek üç farklı file nesnesi oluşturmaktadır. Ancak bu dosya diskte bir tane olduğuna göre çekirdek bunun için toplamda bir tane inode yapısı oluşturacaktır. file yapısının içerisinde dosyanın diskteki bilgilerine ilişkin bu inode yapısınıa f_inode elemanı yoluyla erişilebilmektedir. Daha önceden de gördüğümüz gibi bir dosya isimleri diskte dizin girişlerinde tutulmaktadır. Örneğin "/home/kaan/Study/test.c" isimli dosyanın i-node elemanına erişmek için işletim sistemi sırasıyla "/home", "/home/kaan", "ome/kaan/Stduy" ve "/home/kaan/Study/test.txt" dizin girişlerini taramak zorundadır. Bu işleme işletim sistemlerinde "yol ifadelerinin çözümlenmesi (pathname resolution)" denilmektedir. Aynı dosyaların tekrar tekrar açılması durumunda bu işlemlerin yeniden yapılması oldukça zahmetlidir. Dolayısıyla bulunan dizin girişlerinin bir yapı ile temsil edilerek bir cache sisteminde saklanması uygundur. İşte Linux çekirdeğinde dizin girişleri "dentry" isimli bir yapıyla temsil edilmektedir. Linux sistemleri yukarıda açıkladığımız "inode" ve "dentry" nesnelerini bir cache sisteminde tutmaktadır. Böylece bir dosya yeniden açıldığında onun bilgilerine diske hiç başvurmadan hızlı bir biçimde erişilmektedir. Linux dünyasında bu cache sistemlerine "inode cache" ve "dentry cache" denilmektedir. file, inode ve denrty nesneleri için bu yapıların büyüklüğünde ayrı dilimli tahsisat sistemleri oluşturulmuştur. Pekiyi yukarıdaki nesneler arasındaki ilişki nasıldır? Dosya sistemine dosya betimleyicisi yoluyla erişildiğini anımsayınız. Dosya betimleyicisinden dosya nesnesi (file nesnesi) elde edilmektedir. Dosya nesnesinin içerisinde o dosyanın dizin girişi bilgilerini tutan dentry nesnesinin adresi tutulur. Bu nesnenin içerisinde de inode nesnesinin adresi tutulmaktadır: file ---> dentry ---> inode Ancak 2.6 çekirdekleriyle birlikte file yapısından inode bilgilerine kolay erişebilmek için ayrıca file yapısı içerisinde doğrudan inode nesnesinin adresi de tutulmaya başlanmıştır. Bir aygıt sürücü üzerinde dosya işlemi yapıldığında çekirdek aygıt sürücü fonksiyonlarına dosya nesnesinin adresini (filp göstericisi) geçirmektedir. Yalnızca aygıt sürücü open fonksiyonuyla açılırken ve close fonksiyonu ile kapatılırken inode nesnesinin adresi de bu fonksiyonlara geçirilmektedir. Aygıt sürücünün fonksiyonlarının parametrik yapılarını aşağıda yeniden veriyoruz: int open(struct inode *inodep, struct file *filp); int release(struct inode *inodep, struct file *filp); ssize_t read(struct file *filp, char *buf, size_t size, loff_t *off); ssize_t write(struct file *filp, const char *buf, size_t size, loff_t *off); loff_t llseek(struct file *filp, loff_t off, int whence); /*================================================================================================================================*/ (150_28_06_2024) & (151_30_06_2024) & (152_05_07_2024) & (153_07_07_2024) > "proc" dosya sistemi: Anımsanacağı gibi proc dosya sistemi disk tabanlı bir dosya sistemi değildir. Çekirdek çalışması sırasında dış dünyaya bilgi vermek için bazen de davranışını dış dünyadan gelen verilerle değiştirebilmek için proc dosya sistemini kullanmaktadır. Daha sonra proc gibi sys isimli bir dosya sistemi de Linux'a eklenmiştir. proc dosya sistemi aslında yalnızca çekirdek tarafından değil aygıt sürücüler tarafından da kullanılabilmektedir. Ancak bu dosya sisteminin içerisinde user moddan dosyalar ya da dizinler yaratılamamaktadır. proc dosya sistemindeki tüm girişlerin dosya uzunluları 0 biçiminde rapor edilmektedir. proc dosya sisteminin kullanımına yönelik çekirdek fonksiyonları çekirdeğin versyionları ile zamanla birkaç değiştirilmiştir. Dolayısıyla eski çekirdeklere çalışan kodlar yeni çekirdeklerde derlenmeyecektir. Biz burada en yeni fonksiyonları ele alacağız. User moddan prog dosya sistemindeki bir dosya üzerinde open, read, write, lseek, close işlmeler yapıldığında aslında aygıt sürücülerin belirlediği fonksiyonlar çağrılmaktadır. Yani örneğin biz user moddan proc dosya sistemi içerisindeki bir dosyadan okuma yapmak istediğimizde aslında onu oluşturan aygıt sürücünün içerisindeki bir fonksiyon çalışıtırılır. Bu fonksiyon bize okuma sonucunda elde edilecek bilgileri verir. Benzer biçimde proc dosya sistemindeki bir dosyaya user moddan yazma yapılmak istendiğinde aslında o dosyaya ilişkin aygıt sürücünün bir fonksiyonu çağrılmaktadır. Yani proc dosya sistemi aslında aygıt sürücüden fonksiyon çağıran bir mekanizmaya sahiptir. proc dosya sisteminde bir dosya yaratabilmek için proc_create isimli fonksiyon kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct proc_ops *proc_ops); Fonksiyonun, -> Birinci parametresi yaratılacak dosyanın ismini belirtir. -> İkinci parametresi erişim haklarını belirtmektedir. Bu parametre 0 geçilirse default erişim hakları kullanılır. -> Üçüncü parametre dosyanın hangi dizinde yaratılacağını belirtmektedir. Bu parametre NULL geçilirse dosya ana "/proc" dizini içerisinde yaratılır. proc dosya sistemi içerisinde dizinlerin nasıl yaratıldığını izleyen paragraflarda açıklayacağız. -> Son parametre proc dosya sistemindeki ilgi dosyaya yazma ve okuma yapldığında çalıştırılacak fonksiyonları belirtir. Aslında birkaç sene önceki çekirdeklerde (3.10 çekirdeklerine kadarki çekirdeklerde) bu fonksiyonun son parametresi proc_ops yapısını değil, file_operations yapısını kullanıyordu. Dolayısıyla çekirdeğinizdeki fonksiyonun son parametresinin ne olduğuna dikkat ediniz. Örneğin önceki kursun yapıldığı makinede bu son parametre file_operations yapısına ilişkinken bu kursun yapıldığı makinede proc_ops yapısına ilişkindir. proc_ops yapısı şöyle bildirilmiştir: #include struct proc_ops { unsigned int proc_flags; int (*proc_open)(struct inode *, struct file *); ssize_t (*proc_read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*proc_read_iter)(struct kiocb *, struct iov_iter *); ssize_t (*proc_write)(struct file *, const char __user *, size_t, loff_t *); /* mandatory unless nonseekable_open() or equivalent is used */ loff_t (*proc_lseek)(struct file *, loff_t, int); int (*proc_release)(struct inode *, struct file *); __poll_t (*proc_poll)(struct file *, struct poll_table_struct *); long (*proc_ioctl)(struct file *, unsigned int, unsigned long); #ifdef CONFIG_COMPAT long (*proc_compat_ioctl)(struct file *, unsigned int, unsigned long); #endif int (*proc_mmap)(struct file *, struct vm_area_struct *); unsigned long (*proc_get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); }; proc_ops yapısının elemanlarına ilişkin fonksiyon göstericilerinin türlerinin file_operations yapısındaki elemanlara ilişkin fonksiyon gösterişcilerinin türleri ile aynı olduğuna dikkat ediniz.ç Bu fonksiyonların kullanımı tamamen aygıt sürücü için oluşturduğumuz file_operations yapısı ile aynı biçimdedir. -> Fonksiyon başarı durumunda yaratılan dosyanın bilgilerini içeren proc_dir_entry türünden bir yapı nesnesinin adresiyle, başarısızlık durumunda NULL adresle geri dönmektedir. Bu durumda çağıran fonksiyonun -NOMEM gibi bir hata değeriyle geri döndürülmesi yaygındır. proc dosya sisteminde yaratılan dosya remove_proc_entry fonksiyonuyla silinebilmektedir. Fonksiyonun prototipi şöyledir: #include void remove_proc_entry(const char *name, struct proc_dir_entry *parent); Fonksiyonun, -> Birinci parametresi silinecek dosyanın ismini, -> İkinci parametresi dosyanın içinde bulunduğu dizine ilişkin proc_dir_entry nesnesinin adresini almaktadır. Yine bu parametre NULL adres girilirse dosyanın ana "/proc" dizininde olduğu kabul edilmektedir. proc dosya sistemi genel olarak text tabanlı bir dosya sistemi biçiminde düşünülmüştür. Yani buradaki dosyalar genel olarak text içeriğe sahiptir. Siz de aygıt sürücünüz için proc dosya sisteminde dosya oluşturacaksınız onların içeriğini text olarak oluşturmalısınız. Şimdi de örneklerle ile pekiştirelim: * Örnek 1, Aşağıdaki örnekte proc sisteminde dosya yaratan iskelet bir aygıt sürücü programı verilmiştir. Bu aygıt sürücüde"/proc" dizininde "procfs-driver" isminde bir dosya yaratılmaktadır. Aygıt sürücüyü install ettikten sonra "/proc" dizininde bu dosyanın yaratılıp yaratılmadığını kontrol ediniz. Bu dosyayı "cat" ile komut satırından okumak istediğinizde "cat" programı bu dosyayı açıp, read işlemi uygulayıp kapatacaktır. "cat" işleminden sonra "dmesg" komutu ile aygıt sürücümüzde belirlediğimiz fonksiyonların çağrıldığını doğrulayınız. /* procfs-driver.c */ #include #include #include #include #include #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static int proc_open(struct inode *inodep, struct file *filp); static int proc_release(struct inode *inodep, struct file *filp); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static struct proc_ops g_proc_ops = { .proc_open = proc_open, .proc_release = proc_release, .proc_read = proc_read, .proc_write = proc_write }; static int __init generic_init(void) { int result; struct proc_dir_entry *pde; printk(KERN_INFO "procfs-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "procfs-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } if ((pde = proc_create("procfs-driver", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, NULL, &g_proc_ops)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("procfs-driver", NULL); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "procfs driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver read\n"); return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver write...\n"); return 0; } static int proc_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file opened...\n"); return 0; } static int proc_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file closed...\n"); return 0; } static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver proc file read...\n"); return 0; } static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver proc file write...\n"); return 0; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module * Örnek 2, Aşağıdaki örnekte aygıt sürücüsü içerisindeki count isimli global değişken proc dosya sistemindeki "profs-driver" isimli bir dosya ile temsil edilmiştir. Bu dosyadan okuma yapıldığında bu count değişkeninin değeri elde edilmektedir. Dosyaya yazma yapıldığında bu count değişkeninin değeri güncellenmektedir. Yazma işleminde dosya göstericisi dikkate alınmamış ve yazma işlemi her zaman sanki ilgili dosyanın başından itibaren yapılıyormuş gibi bir etki oluşturulmuştur. /* procfs-driver.c */ #include #include #include #include #include #define PIPE_BUFSIZE 4096 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static int proc_open(struct inode *inodep, struct file *filp); static int proc_release(struct inode *inodep, struct file *filp); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static struct proc_ops g_proc_ops = { .proc_open = proc_open, .proc_release = proc_release, .proc_read = proc_read, .proc_write = proc_write }; static int g_count = 123; static char g_count_str[32]; static int __init generic_init(void) { int result; struct proc_dir_entry *pde; printk(KERN_INFO "procfs-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "procfs-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } if ((pde = proc_create("procfs-driver", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, NULL, &g_proc_ops)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("procfs-driver", NULL); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "procfs driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver read\n"); return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver write...\n"); return 0; } static int proc_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file opened...\n"); return 0; } static int proc_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file closed...\n"); return 0; } static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_count_str, "%d\n", g_count); left = strlen(g_count_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_count_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "procfs-driver proc file read...\n"); return esize; } static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; char count_str[31]; int count; esize = size > 31 ? 31 : size; if (esize != 0) { if (copy_from_user(count_str, buf, esize) != 0) return -EFAULT; } count_str[esize] = '\0'; if (kstrtoint(count_str, 10, &count) != 0) return -EINVAL; if (count < 0 || count > 1000) return -EINVAL; g_count = count; strcpy(g_count_str, count_str); printk(KERN_INFO "procfs-driver proc file write...\n"); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Biz yukarıdaki örneklerde dosyayı proc dosya sisteminin kök diininde yarattık. İstersek proc dizininde bir dizin yaratıp dosyalarımızı o dizinin içerisinde de oluşturabilirdik. proc dosya sisteminde bir dizin yaratmak için proc_mkdir fonksiyonu kullanılmaktadır: #include struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent); Fonksiyonun, -> Birinci parametresi yaratılacak dizin'in ismini, -> İkinci parametresi dizinin hanfi dizin içerisinde yaratılacağını belirtir. Bu parametre NULL geçilirse dizin proc dosya sisteminin kök dizininde yaratılır. -> proc_mkdir fonksiyonu başarısızlık durumunda NULL adrese geri dönmektedir. Çağıran fonksiyonun yine -ENOMEM değeriyle geri döndürülmesi uygundur. Geri değerini proc_create fonksiyonun parent parametresinde kullanırsak ilgili dosyamızı bu dizinde yaratmış oluruz. Tabii benzer biçimde dizin içerisinde dizin de yaratabiliriz. Örneğin: struct proc_dir_entry *pdir; pdir = proc_mkdir("procfs-driver", NULL); proc_create("info", 0, pdir, &g_proc_ops); Dizinlerin silinmesi yine remove_proc_entry fonksiyonuyla yapılmaktadır. Tabii dizin içerisindeki dosyaları silerken remove_proc_entry fonksiyonda dosyanın hangi dizin içerisinde olduğu belirtilmelidir. Bu fonksiyon ile dizin silinirken dizin'in içi boş değilse bile o dizin ve onun içindeki girişlerin hepsi silinmektedir. Ayrıca kök dizindeki girişleri silmek için proc_remove fonksiyonu da bulundurulmuştur. Fonksiyonun prototipi şöyledir: #include void proc_remove(struct proc_dir_entry *de); Bu fonksiyon parametre olarak proc_create ya da proc_mkdir fonksiyonun verdiği geri dönüş değerini alır. proc dosya sisteminin kök dizininde silme yapılmak isteniyorsa aşağıdaki her iki çağrım eşdeğerdir: remove_proc_entry("file_name", NULL); proc_remove("file_name"); Şimdi de örneklerle ile pekiştirelim: * Örnek 1, Aşağıdaki örnekte proc dosya sisteminin kök dizininde "procfs-driver" isimli bir dizin yaratılmış, onun içerisinde de "count" bir dosya yaratılmıştır. Bu örneğin yukarıdaki örnekten tek farkı dosyanın proc dosya sisteminin kökünde değil bir dizinin içerisinde yaratılmış olmasıdır. /* procfs-driver.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static int proc_open(struct inode *inodep, struct file *filp); static int proc_release(struct inode *inodep, struct file *filp); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release }; static struct proc_ops g_proc_ops = { .proc_open = proc_open, .proc_release = proc_release, .proc_read = proc_read, .proc_write = proc_write }; static int g_count = 123; static char g_count_str[32]; static int __init generic_init(void) { int result; struct proc_dir_entry *pde_dir; struct proc_dir_entry *pde; printk(KERN_INFO "procfs-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "procfs-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_fops; if ((result = cdev_add(g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "Cannot add device!...\n"); return result; } if ((pde_dir = proc_mkdir("procfs-driver", NULL)) == NULL) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } if ((pde = proc_create("count", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_proc_ops)) == NULL) { remove_proc_entry("procfs-driver", NULL); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("procfs-driver", NULL); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "procfs driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver read\n"); return 0; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { printk(KERN_INFO "procfs-driver write...\n"); return 0; } static int proc_open(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file opened...\n"); return 0; } static int proc_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "procfs-driver proc file closed...\n"); return 0; } static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_count_str, "%d\n", g_count); left = strlen(g_count_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_count_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "procfs-driver proc file read...\n"); return esize; } static ssize_t proc_write(struct file *filp, const char *buf, size_t size, loff_t *off) { size_t esize; char count_str[31]; int count; esize = size > 31 ? 31 : size; if (esize != 0) { if (copy_from_user(count_str, buf, esize) != 0) return -EFAULT; } count_str[esize] = '\0'; if (kstrtoint(count_str, 10, &count) != 0) return -EINVAL; if (count < 0 || count > 1000) return -EINVAL; g_count = count; strcpy(g_count_str, count_str); printk(KERN_INFO "procfs-driver proc file write...\n"); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod $module c $major 0 chmod $mode $module /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Şimdi de boru aygıt sürücüsüne proc dosya sistemi desteği verelim. Aygıt sürücümüz proc kök dizininde minör numara kadar ayrı dizin oluşturmaktadır. Sonra da bu dizinlerin içerisinde ilgili aygıtların bufsize ve count değerlerini iki dosya ile dış dünyaya vermektedir. Örneğimizde dikkat edilmesi gereken birkaç nokta üzerinde durmak istiyoruz: -> proc dosya sistemi içerisindeki bir dosya user moddan açıldığında aygıt sürücümüz hangi dosyanın açıldığını nereden bilecektir? İşte dosya nesnesi görevinde olan file yapısının f_path elemanı path isimli bir yapı türündendir. Bu yapı şöyle bildirilmiştir: struct path { struct vfsmount *mnt; struct dentry *dentry; } __randomize_layout; Yapının deentry elemanı dosya hakkında bilgiler içermektedir: struct dentry { /* RCU lookup touched fields */ unsigned int d_flags; /* protected by d_lock */ seqcount_spinlock_t d_seq; /* per dentry seqlock */ struct hlist_bl_node d_hash; /* lookup hash list */ struct dentry *d_parent; /* parent directory */ struct qstr d_name; struct inode *d_inode; /* Where the name belongs to - NULL is negative */ unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ /* Ref lookup also touches following */ struct lockref d_lockref; /* per-dentry lock and refcount */ const struct dentry_operations *d_op; struct super_block *d_sb; /* The root of the dentry tree */ unsigned long d_time; /* used by d_revalidate */ void *d_fsdata; /* fs-specific data */ union { struct list_head d_lru; /* LRU list */ wait_queue_head_t *d_wait; /* in-lookup ones only */ }; struct hlist_node d_sib; /* child of parent list */ struct hlist_head d_children; /* our children */ /* * d_alias and d_rcu can share memory */ union { struct hlist_node d_alias; /* inode alias list */ struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */ struct rcu_head d_rcu; } d_u; }; Burada yapının d_name elemanı qstr isimli bir yapı türündedir: struct qstr { union { struct { HASH_LEN_DECLARE; }; u64 hash_len; }; const unsigned char *name; }; İşte buradaki name elemanı ilgili dosyanın ismini belirtmektedir. Özetle biz file yapısından hareketle dosyanın ismini elde edebilmekteyiz. Böylece biz proc dosya sistemi içerisinde bir dosya açıldığında o dosyanın isminden hareketle hangi dosyanın açılmış olduğunu anlayabiliriz. Ayrıca dentry yapısının d_parent elemanı dosya ya da dizin'in içinde bulunduğu dizine ilişkin dentry nesnesini vermektedir. Yani biz istersek dosyanın içinde bulunduğu dizinin ismini de alabiliriz. Aşağıda örneği bir bütün olarak veriyoruz. * Örnek 1, Aygıt sürücüyü yine aşağıdaki gibi derleyebilirsiniz: $ make file=pipe-driver Yüklemeyi aşağıdaki gibi yapabilirsiniz: $ sudo ./loadmulti 5 pipe-driver ndevices=5 Aygıt sürüyü yükledikten sonra artık proc dosya sisteminde "pipe-driver" isimli bid dizin oluşturulacak ve bu dizin de aşağıdaki gibi 3 dizin yaratılmış olacaktır: $ ls /proc/pipe-driver pipe0 pipe1 pipe2 pipe3 pipe4 Bu dosyaların her birinin içerisinde de "bufsize" ve "count" isimli iki dosya bulunacaktır. Burada teti "prog1.c" ve "prog2.c" programları yardımıyla yapabilirisiniz. Örneğin "prog1" programı ile "pipe-driver3" borusunu açıp içerisine bir şeyler yazarsanız "/proc/pipe-driver/pipe3/count" dosyasının içerisinde yazılan byte sayısını görebilirsiniz. Aygıt sürücümüzü yine "unloadmulti" script'i ile boşaltabilirisiniz: $ sudo ./unloadmulti 5 pipe-drive Aşağıda ise programa ilişkin kodları mevcuttur. /* pipe-driver.h */ #ifndef PIPEDRIVER_H_ #define PIPEDRIVER_H_ #include #include struct PIPE_PEEK { size_t size; void *buf; }; #define PIPE_DRIVER_MAGIC 'p' #define IOC_PIPE_GETCOUNT _IOR(PIPE_DRIVER_MAGIC, 0, size_t) #define IOC_PIPE_GETBUFSIZE _IOR(PIPE_DRIVER_MAGIC, 1, size_t) #define IOC_PIPE_SETBUFSIZE _IOW(PIPE_DRIVER_MAGIC, 2, size_t) #define IOC_PIPE_PEEK _IOWR(PIPE_DRIVER_MAGIC, 3, struct PIPE_PEEK) #endif /* pipe-driver.c */ #include #include #include #include #include #include #include #include #include #include "pipe-driver.h" #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define NDEVICES 10 #define DEF_PIPE_BUFSIZE 10 #define MAX_PIPE_BUFSIZE 131072 /* 128K */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Pipe Driver"); static int generic_open(struct inode *inodep, struct file *filp); static int generic_release(struct inode *inodep, struct file *filp); static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off); static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static int proc_open(struct inode *inodep, struct file *filp); static int proc_release(struct inode *inodep, struct file *filp); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off); static struct file_operations g_fops = { .owner = THIS_MODULE, .open = generic_open, .read = generic_read, .write = generic_write, .release = generic_release, .unlocked_ioctl = generic_ioctl }; static struct proc_ops g_proc_ops = { .proc_open = proc_open, .proc_release = proc_release, .proc_read = proc_read, }; /* static struct file_operations g_proc_ops = { .open = proc_open, .release = proc_release, .read = proc_read, }; */ struct PIPE_DEVICE { unsigned char *pipebuf; size_t head; size_t tail; size_t count; size_t bufsize; struct semaphore sem; wait_queue_head_t wqread; wait_queue_head_t wqwrite; struct cdev cdev; }; struct PROC_INFO { struct PIPE_DEVICE *pdevice; int filetype; /* 0 = count, 1 = bufsize */ char strbuf[32]; }; static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg); static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg); static dev_t g_dev; static struct PIPE_DEVICE *g_pdevices; static int ndevices = NDEVICES; module_param(ndevices, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); static int __init generic_init(void) { int result; dev_t dev; int i, k; struct proc_dir_entry *pde_root, *pde_pipe; char namebuf[16]; printk(KERN_INFO "pipe-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, ndevices, "pipe-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_pdevices = (struct PIPE_DEVICE *)kmalloc(sizeof(struct PIPE_DEVICE) * ndevices, GFP_KERNEL)) == NULL) { unregister_chrdev_region(g_dev, ndevices); return -ENOMEM; } if ((pde_root = proc_mkdir("pipe-driver", NULL)) == NULL) { kfree(g_pdevices); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } for (i = 0; i < ndevices; ++i) { sprintf(namebuf, "pipe%d", i); if ((pde_pipe = proc_mkdir(namebuf, pde_root)) == NULL) { kfree(g_pdevices); unregister_chrdev_region(g_dev, 1); remove_proc_entry("pipe-driver", NULL); return -ENOMEM; } g_pdevices[i].head = g_pdevices[i].tail = g_pdevices[i].count = 0; g_pdevices[i].bufsize = DEF_PIPE_BUFSIZE; sema_init(&g_pdevices[i].sem, 1); init_waitqueue_head(&g_pdevices[i].wqread); init_waitqueue_head(&g_pdevices[i].wqwrite); cdev_init(&g_pdevices[i].cdev, &g_fops); dev = MKDEV(MAJOR(g_dev), i); g_pdevices[i].pipebuf = (char *)kmalloc(DEF_PIPE_BUFSIZE, GFP_KERNEL); result = cdev_add(&g_pdevices[i].cdev, dev, 1); if (g_pdevices[i].pipebuf == NULL || result < 0) { if (g_pdevices[i].pipebuf != NULL) kfree(g_pdevices[i].pipebuf); for (k = 0; k < i; ++k) { cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices[k].pipebuf); } kfree(g_pdevices); unregister_chrdev_region(dev, ndevices); printk(KERN_ERR "Cannot add device!...\n"); return result; } if ((proc_create("count", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_pipe, &g_proc_ops)) == NULL || proc_create("bufsize", S_IRUSR|S_IRGRP|S_IROTH, pde_pipe, &g_proc_ops) == NULL) { remove_proc_entry("pipe-driver", NULL); for (k = 0; k < i; ++k) { cdev_del(&g_pdevices[k].cdev); kfree(g_pdevices[k].pipebuf); } unregister_chrdev_region(g_dev, 1); return -ENOMEM; } } return 0; } static void __exit generic_exit(void) { int i; for (i = 0; i < ndevices; ++i) cdev_del(&g_pdevices[i].cdev); kfree(g_pdevices); remove_proc_entry("pipe-driver", NULL); unregister_chrdev_region(g_dev, ndevices); printk(KERN_INFO "pipe-driver module exit...\n"); } static int generic_open(struct inode *inodep, struct file *filp) { struct PIPE_DEVICE *pdevice; pdevice = container_of(inodep->i_cdev, struct PIPE_DEVICE, cdev); filp->private_data = pdevice; printk(KERN_INFO "pipe-driver opened...\n"); return 0; } static int generic_release(struct inode *inodep, struct file *filp) { printk(KERN_INFO "pipe-driver closed...\n"); return 0; } static ssize_t generic_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; while (pdevice->count == 0) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqread, pdevice->count > 0)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->count, size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(buf, pdevice->pipebuf + pdevice->head, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_to_user(buf + size1, pdevice->pipebuf, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->head = (pdevice->head + esize) % pdevice->bufsize; pdevice->count -= esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqwrite); return esize; } static ssize_t generic_write(struct file *filp, const char *buf, size_t size, loff_t *off) { struct PIPE_DEVICE *pdevice; size_t esize, size1, size2; pdevice = (struct PIPE_DEVICE *)filp->private_data; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if (size > pdevice->bufsize) size = pdevice->bufsize; while (pdevice->bufsize - pdevice->count < size) { up(&pdevice->sem); if (filp->f_flags & O_NONBLOCK) return -EAGAIN; if (wait_event_interruptible(pdevice->wqwrite, pdevice->bufsize - pdevice->count >= size)) return -ERESTARTSYS; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; } esize = MIN(pdevice->bufsize - pdevice->count, size); if (pdevice->tail >= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->tail, esize); else size1 = esize; size2 = esize - size1; if (copy_from_user(pdevice->pipebuf + pdevice->tail, buf, size1) != 0) { up(&pdevice->sem); return -EFAULT; } if (size2 != 0) if (copy_from_user(pdevice->pipebuf, buf + size1, size2) != 0) { up(&pdevice->sem); return -EFAULT; } pdevice->tail = (pdevice->tail + esize) % pdevice->bufsize; pdevice->count += esize; up(&pdevice->sem); wake_up_interruptible_all(&pdevice->wqread); return esize; } static long generic_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct PIPE_DEVICE *pdevice; printk(KERN_INFO "ioctl"); pdevice = (struct PIPE_DEVICE *)filp->private_data; switch (cmd) { case IOC_PIPE_GETCOUNT: return put_user(pdevice->count, (size_t *)arg); case IOC_PIPE_GETBUFSIZE: return put_user(pdevice->bufsize, (size_t *)arg); case IOC_PIPE_SETBUFSIZE: return set_bufsize(pdevice, arg); case IOC_PIPE_PEEK: return read_peek(pdevice, arg); default: return -ENOTTY; } return 0; } static int set_bufsize(struct PIPE_DEVICE *pdevice, unsigned long arg) { char *new_pipebuf; size_t size; if (arg > MAX_PIPE_BUFSIZE) return -EINVAL; if (arg <= pdevice->count) return -EINVAL; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; if ((new_pipebuf = (char *)kmalloc(arg, GFP_KERNEL)) == NULL) { up(&pdevice->sem); return -ENOMEM; } if (pdevice->count != 0) { if (pdevice->tail <= pdevice->head) { size = pdevice->bufsize - pdevice->head; memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, size); memcpy(new_pipebuf + size, pdevice->pipebuf, pdevice->count - size); } else memcpy(new_pipebuf, pdevice->pipebuf + pdevice->head, pdevice->count); } pdevice->head = 0; pdevice->tail = pdevice->count; kfree(pdevice->pipebuf); pdevice->pipebuf = new_pipebuf; pdevice->bufsize = arg; up(&pdevice->sem); return 0; } static int read_peek(struct PIPE_DEVICE *pdevice, unsigned long arg) { size_t esize, size1, size2; struct PIPE_PEEK *userpp = (struct PIPE_PEEK *)arg; struct PIPE_PEEK pp; int status = 0; if (copy_from_user(&pp, userpp, sizeof(struct PIPE_PEEK)) != 0) return -EFAULT; if (pp.size == 0) return 0; if (down_interruptible(&pdevice->sem)) return -ERESTARTSYS; esize = MIN(pdevice->count, pp.size); if (pdevice->tail <= pdevice->head) size1 = MIN(pdevice->bufsize - pdevice->head, esize); else size1 = esize; size2 = esize - size1; if (copy_to_user(pp.buf, pdevice->pipebuf + pdevice->head, size1) != 0) { status = -EFAULT; goto EXIT; } if (size2 != 0) if (copy_to_user(pp.buf + size1, pdevice->pipebuf, size2) != 0) { status = -EFAULT; goto EXIT; } if (put_user(esize, &userpp->size) != 0) status = -EFAULT; EXIT: up(&pdevice->sem); return status; } static int proc_open(struct inode *inodep, struct file *filp) { const char *file_name = filp->f_path.dentry->d_name.name; const char *parent_file_name = filp->f_path.dentry->d_parent->d_name.name; struct PROC_INFO *pi; printk(KERN_INFO "pipe-driver proc file opened...\n"); if ((pi = (struct PROC_INFO *)kmalloc(sizeof(struct PROC_INFO), GFP_KERNEL)) == NULL) return -ENOMEM; pi->pdevice = &g_pdevices[parent_file_name[4] - '0']; if (!strcmp(file_name, "bufsize")) pi->filetype = 1; else if (!strcmp(file_name, "count")) pi->filetype = 0; filp->private_data = pi; return 0; } static int proc_release(struct inode *inodep, struct file *filp) { struct PROC_INFO *pi; pi = (struct PROC_INFO *)filp->private_data; kfree(pi); return 0; } static ssize_t proc_read(struct file *filp, char *buf, size_t size, loff_t *off) { struct PROC_INFO *pi; size_t esize, left; pi = (struct PROC_INFO *)filp->private_data; switch (pi->filetype) { case 0: sprintf(pi->strbuf, "%lu\n", (unsigned long)pi->pdevice->count); left = strlen(pi->strbuf) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, pi->strbuf + *off, esize) != 0) return -EFAULT; *off += esize; } break; case 1: sprintf(pi->strbuf, "%lu\n", (unsigned long)pi->pdevice->bufsize); left = strlen(pi->strbuf) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, pi->strbuf + *off, esize) != 0) return -EFAULT; *off += esize; } break; } return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 mode=666 /sbin/insmod ./${module}.ko ${@:3} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) for ((i = 0; i < $1; ++i)) do rm -f ${module}$i mknod ${module}$i c $major $i chmod $mode ${module}$i done /* unloadmulti (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$2 /sbin/rmmod ./$module.ko || exit 1 for ((i = 0; i < $1; ++i)) do rm -f ${module}$i done /* prog1.c */ #include #include #include #include #include #include #include "pipe-driver.h" #define PIPE_SIZE 4096 void exit_sys(const char *msg); int main(void) { int fd; char buf[PIPE_SIZE]; char *str; size_t len, bufsize, new_bufsize; if ((fd = open("pipe-driver3", O_WRONLY)) == -1) exit_sys("open"); for (;;) { printf("Enter text:"); fflush(stdout); fgets(buf, PIPE_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (!strcmp(buf, "quit")) break; if (buf[0] == '!') { new_bufsize = atoi(&buf[1]); printf("%zd\n", new_bufsize); if (ioctl(fd, IOC_PIPE_SETBUFSIZE, new_bufsize) == -1) exit_sys("ioctl"); if (ioctl(fd, IOC_PIPE_GETBUFSIZE, &bufsize) == -1) exit_sys("ioctl"); printf("new pipe buffer size is %zu\n", bufsize); } else { len = strlen(buf); if (write(fd, buf, len) == -1) exit_sys("write"); printf("%lu bytes written...\n", (unsigned long)len); } } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #include #include "pipe-driver.h" #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { int pdriver; char buf[BUFFER_SIZE + 1]; int count, size; ssize_t result; struct PIPE_PEEK pp; char *peekbuf; if ((pdriver = open("pipe-driver3", O_RDONLY)) == -1) exit_sys("open"); for (;;) { if (ioctl(pdriver, IOC_PIPE_GETCOUNT, &count) == -1) exit_sys("ioctl"); printf("There are (is) %d byte(s) in the pipe\n", count); printf("Size:"); scanf("%d", &size); if (size > BUFFER_SIZE) { printf("size is very long!...\n"); continue; } if (size == 0) break; if (size < 0) { pp.size = -size; if ((pp.buf = malloc(-size)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (ioctl(pdriver, IOC_PIPE_PEEK, &pp) == -1) exit_sys("ioctl"); peekbuf = (char *)pp.buf; for (size_t i = 0; i < pp.size; ++i) putchar(peekbuf[i]); putchar('\n'); free(pp.buf); } else { if ((result = read(pdriver, buf, size)) == -1) exit_sys("read"); buf[result] = '\0'; printf("%jd bytes read: %s\n", (intmax_t)result, buf); } } close(pdriver); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (154_12_07_2024) & (155_14_07_2024) & (156_19_07_2024) & (157_21_07_2024) & (158_28_07_2024) > "kernel modules" : >> "Timer Interrupts" : Şimdi de aygıt sürücülerde zamanlama işlemlerinin nasıl yapılacağı üzerinde duracağız. Çekirdeğin zamanlama mekanizması periyodik oluşturulan donanım kesmeleriyle sağlanmaktadır. Bu kesmelere genel olarak "timer kesmeleri" ya da Linux terminolojisinde "jiffy" denilmektedir. Eskiden tek CPU'lu makineler kullanıyordu ve eski işlemcilerde işlemcinin içerisinde periyodik kesme oluşturacak bir mekanizma yoktu. Ancak daha sonraları işlemcilere kendi içerisinde periyodik kesme oluşturabilecek timer devreleri eklendi. Bugün ağırlıklı olarak birden fazla çekirdeğe sahip işlemcileri kullanıyoruz. Bu işlemcilerin içerisindeki çekirdeklerin her birinde o çekirdekte periyodik kesme oluşturan (local interrupts) timer devreleri bulunmaktadır. Böylece her çekirdek kendi timer devresiyle "context switch" yapmakta ve proses istatistiklerini güncellemektedir. Bugün PC mimarisinde yerel çekirdeklerin kesme mekanizmaları dışında ayrıca bir de eski sistemlerde zaten var olan IRQ0 hattına bağlı global bir timer devresi de bulunmaktadır. Bu global timer devresi "context switch" yapmak için değil sistem zamanının ilerletilmesi amacıyla kullanılmaktadır. Bugünkü Linux sistemlerinde söz konusu olan bu timer devrelerinin hepsi 1 milisaniye, 4 milisaniye ya da 10 milisaniyeye kurulmaktadır. Eskiden ilk Linux çekirdeklerinde 10 milisaniyelik periyotlar kullanılıyordu. Sonra bilgisayarlar hızlanınca 1 milisaniye periyot yaygın olarak kullanılmaya başlandı. Ancak bugünlerde 4 milisaniye periyotları kullanan çekirdekler de yaygın biçimde bulunmaktadır. Aslında timer frekansı çekirdek konfigüre edilirken kullanıcılar tarafından da değiştirilebilmektedir. (Çekirdek derlenmeden önce çekirdeğin davranışları üzerinde etkili olan parametrelerin belirlenmesi sürecine "çekirdeğin konfigüre" edilmesi denilmektedir.) Şimdi de bu "jiffy" kavramı üzerinde duralım: >>> "jiffy" : Global timer kesmelerine (PC mimarisinde IRQ0) ilişkin kesme kodları çekirdek içerisindeki jiffies isimli bir global değişkeni artırmaktadır. Böylece eğer timer kesme periyodu biliniyorsa iki jiffies değeri arasındaki farka bakılarak bir zaman ölçümü mümkün olabilmektedir. Timer frekansı Linux kernel içerisindeki HZ isimli sembolik sabitle belirtilmiştir. Timer periyodu çekirdek konfigüre edilirken değiştirilebilmektedir. Genellikle bu süre 1 ms, 4 ms ya da 10 ms olmaktadır. (Ancak değişik mimarilerde farklı değerlerde olabilir.) Örneğin kursun yapıldığı sanal makinede timer periyodu 4 milisaniye'dir. Bu da saniyede 250 kez timer kesmesinin oluşacağı anlamına gelmektedir. Başka bir deyişle bu makinede HZ sembolik sabiti 250 olarak define edilmiştir. İşte timer kesmesi her oluşturduğunda işletim sisteminin kesme kodu (interrupt handler) devereye girip "jiffies" isimli global değişkeni 1 artırmaktadır. Bu jiffies değişkeni unsigned long türdendir. Bildindiği gibi unsigned long türü 32 bit Linux sistemlerinde 32 bit 64 bit Linux sistemlerinde 64 bittir. 32 bit Linux sistemlerinde ayrıca jiffies_64 isimli bir değişken daha vardır. Bu değişken hem 32 bit sistemde hem de 64 bit sistemde 64 bitliktir. 32 bit sistemde jiffies değişkeni 32 bit olduğu için bilgisayar uzun süre açık kalırsa taşma (overflow) oluşabilmektedir. Ancak 64 bit sistemlerde taşma mümkün değildir. 32 bit sistemlerde jiffies_64 değeri çekirdek tarafından iki ayrı makine komutuyla güncellenmektedir. Çünkü 32 bit sistemlerde 64 bit değeri belleğe tek hamlede yazmak mümkün değildir. Bu nedenle jiffies_64 değerinin taşma durumunda yanlış okunabilme olasılığı vardır. Hem 32 bit hem 64 bit sistemlerde 64 bitlik jiffies değerini düzgün bir biçimde okuyabilmek için get_jiffies_64 isimli fonksiyon bulundurulmuştur. Fonksiyonun prototipi aşağıdaki gibidir: #include u64 get_jiffies_64(void); Biz 32 bit sistemde de olsak bu fonksiyonla 64 bitlik jiffies değerini düzgün bir biçimde okuyabiliriz. * Örnek 1, Aşağıdaki örnekte çekirdek modülü içerisinde proc dosya sisteminde "jify-module" isimli bir dizin, dizin'in içerisinde de "jiffy" ve "hertz" isimli iki dosya yaratılmıştır. jiffy dosyası okunduğunda o anki jiffies değeri elde edilmektedir. hertz dosyası okunduğunda ise timer frekansı elde edilmektedir. Aygıt sürücüyü aşağıdaki gibi derleyip yükleyebilirsiniz: $ make file=jiffy-module $ sudo insmod jiffy-module.ko Boşaltımı da şöyle yapabilirsiniz: $ sudo rmmod jiffy-driver.ko Programa ilişkin kodlar aşağıdaki gibidir: /* jiffy-module.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Yukarıda da belirttiğimiz gibi eğer 64 bit sistemde çalışılıyorsa jiffies değerinin taşması (overflow olması) mümkün değildir. Ancak 32 bit sistemlerde timer frekansı 1000 ise 49 günde taşma meydana gelebilmektedir. Aygıt sürücü programcısı bazen geçen zamanı hesaplamak için iki noktada jiffies değerini alıp aradaki farka bakmak isteyebilmektedir. Ancak bu durumda 32 bit sistemlerde "overflow" olasılığının ele alınması gerekir. İşaretli sayıların ikili sistemdeki temsiline dayanarak iki jiffies arasındaki fark aşağıdaki gibi tek bir ifadeyle de hesaplanabilmektedir: unsigned long int prev_jiffies, next_jiffies; ... net_jiffies = (long) next_jiffies - (long) prev_jiffies; Çekirdek içerisinde iki jiffy değerini alarak bunları öncelik sonralık ilişkisi altında karşılaştıran aşağıdaki fonksiyonlar bulunmaktadır: time_after(jiffy1, jiffy2) time_before(jiffy1, jiffy2) time_after_eq(jiffy1, jiffy2) time_before_eq(jiffy1, jiffy2) Bu fonksiyonların hepsi bool bir değere geri dönmektedir. Bu fonksiyonlar 32 bit sistemlerde taşma durumunu da dikkate almaktadır. time_after fonksiyonu birinci parametresiyle belirtilen jiffy değerinin ikinci parametresiyle belirtilen jiffy değerinden sonraki bir jiffy değeri olup olmadığını belirlemekte kullanılmaktadır. Diğer fonksiyonlar da bu biçimde birinci parametredeki jiffy değeri ile ikinci parametredeki jiffy değerini karşılaştırmaktadır. Çekirdek içerisinde jiffies değerini çeşitli biçimlere dönüştüren aşağıdaki fonksiyonlar da bulunmaktadır: #include unsigned long msecs_to_jiffies(const unsigned int m); unsigned long usecs_to_jiffies(const unsigned int m); unsigned long usecs_to_jiffies(const unsigned int m); Bu işlemin tersini yapan da üç fonksiyon vardır: #include unsigned int jiffies_to_msecs(const unsigned long j); unsigned int jiffies_to_usecs(const unsigned long j); unsigned int jiffies_to_nsecs(const unsigned long j); Bu fonksiyonlar o andaki aktif HZ değerini dikkate almaktadır. Ayrıca jiffies değerini saniye ve nano saniye biçiminde ayırıp bize struct timespec64 biçiminde bir yapı nesnesi olarak veren jiffies_to_timespec64 isimli bir fonksiyon da vardır. Bunun tersi timespec64_to_jiffies fonksiyonuyla yapılmaktadır. timespec64 yapısı da şöyledir: struct timespec64 { time64_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; Eski çekirdeklerde bu fonksiyonların yerine aşağıdaki fonksiyonlar bulunyordu: #include unsigned long timespec_to_jiffies(struct timespec *value); void jiffies_to_timespec(unsigned long jiffies, struct timespec *value); unsigned long timeval_to_jiffies(struct timeval *value); void jiffies_to_timeval(unsigned long jiffies, struct timeval *value); Aşağıdaki örnekte proc dosya sisteminde "jiffy-module" dizini içerisinde ayrıca "difference" isimli bir dosya da yaratılmıştır. Bu dosya her okunduğunda önceki okumayla aradaki jiffy farkı yazdırılmaktadır. * Örnek 1, /* jiffy-module.c */ #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static struct proc_ops g_procops_difference = { .proc_read = proc_read_difference, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static char g_difference_str[512]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("difference", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_difference) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off) { static unsigned long prev_jiffies; loff_t left, esize; long int net_jiffies; struct timespec64 ts; net_jiffies = (long)jiffies - (long)prev_jiffies; jiffies_to_timespec64(net_jiffies, &ts); sprintf(g_difference_str, "Jiffy difference: %10ld (%ld seconds + %ld nonoseconds)\n", net_jiffies, (long)ts.tv_sec, (long)ts.tv_nsec); left = (loff_t)strlen(g_difference_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_difference_str + *off, esize) != 0) return -EFAULT; *off += esize; } prev_jiffies = jiffies; return (ssize_t)esize; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Aygıt sürücü içerisinde bazen belli bir süre bekleme yapmak gerekebilmektedir. Biz kursumuzda daha önce user modda bekeleme yapan fonksiyonları görmüştük. Ancak o fonksiyonlar kernel modda kullanılamamtadır. Kermel modda çekirdek içerisindeki olanaklarla bekleme yapılabilmektedir. Eğer bekleme süresi kısa ise bekleme işlemi meşgul bir döngü ile yapılabilir. Örneğin: while (time_before(jiffies, jiffies_target)) schedule(); Burada o anki jiffies değeri hedef jiffies değerinden küçükse schedule fonksiyonu çağrılmıştır. schedule fonksiyonu thread'i uykuya yatırmamaktadır. Yalnızca thread'ler arası geçiş oluşmasına yol açmaktadır. Yani bu fonksiyon uykuya dalmadan CPU'yu bırakmak için kullanılmaktadır. schedule fonksiyonunu çağıran thread çalışma kuyruğunda (run queue) kalmaya devam eder. Yine çalışma sırası ona geldiğinde kaldığı yerden çalışmaya devam eder. Ancak meşgul bir döngü içerisinde schedule işlemi yine önemli bir CPU zamanın harcanmasına yol açmaktadır. Bu nedenle uzun beklelemelerin yukarıdaki gibi yapılması tavsiye edilmemektedir. Uzun beklemelerin uykuya dalarak yapılması gerekir. Uzun beklemeler için bir wait kuyruğu oluşturulup wait_event_timeout ya da wait_event_interrptible_timeout fonksiyonlarıyla koşul 0 yapılarak gerçekleştirilebilir. Ancak bunun için bir wait kuyruğunun oluşturulması gerekir. Bu işlemi zaten kendi içerisinde yapan özel fonksiyonlar vardır. schedule_timeout fonksiyonu belli bir jiffy zamanı geçene kadar thread'i çekirdek tarafından bu amaçla oluşturulmuş olan bir wait kuyruğunda bekletir. signed long schedule_timeout(signed long timeout); Fonksiyon parametre olarak beklenecek jiffy değerini alır. Eğer sinyal dolayısıyla fonksiyon sonlanırsa kalan jiffy sayısına, eğer zaman aşımının dolması nedeniyle fonksiyon sonlanısa 0 değerine geri döner. Fonksiyon başarısız olmamaktadır. Fonksiyonu kullanmadan önce prosesin durum bilgisini set_current_state isimli fonksiyonla değiştirmek gerekir. Değiştirilecek durum TASK_UNINTERRUPTIBLE ya da TASK_INTERRUPTIBLE olabilir. Bu işlem yapılmazsa bekleme gerçekleşmemektedir. Örneğin: set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(jiffies + 5 * HZ); Uzun beklemeyi kendi içerisinde schedule_timeout kullanarak yapan üç yardımcı fonksiyon da vardır: #include void msleep(unsigned int msecs); unsigned long msleep_interruptible(unsigned int msecs); void ssleep(unsigned int secs); Aşağıdaki örnekte "jiffy-module" dizinindeki "sleep" dosyasından okuma yapıldığında (denemeyi cat yapabilirsiniz) 10 saniye bekleme oluşacaktır. * Örnek 1, /* jiffy-module.c */ #include #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("procfs driver"); static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t proc_read_sleep(struct file *filp, char *buf, size_t size, loff_t *off); static struct proc_ops g_procops_jiffy = { .proc_read = proc_read_jiffy, }; static struct proc_ops g_procops_hertz = { .proc_read = proc_read_hertz, }; static struct proc_ops g_procops_difference = { .proc_read = proc_read_difference, }; static struct proc_ops g_procops_sleep = { .proc_read = proc_read_sleep, }; static char g_jiffies_str[32]; static char g_hertz_str[32]; static char g_difference_str[512]; static int __init generic_init(void) { struct proc_dir_entry *pde_dir; if ((pde_dir = proc_mkdir("jiffy-module", NULL)) == NULL) return -ENOMEM; if (proc_create("jiffy", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_jiffy) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("hertz", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_hertz) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("difference", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_difference) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } if (proc_create("sleep", S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH, pde_dir, &g_procops_sleep) == NULL) { remove_proc_entry("jiffy-module", NULL); return -ENOMEM; } return 0; } static void __exit generic_exit(void) { remove_proc_entry("jiffy-module", NULL); printk(KERN_INFO "jiffy-module module exit...\n"); } static ssize_t proc_read_jiffy(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_jiffies_str, "%lu\n", jiffies); left = strlen(g_jiffies_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_jiffies_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "jiffy file read...\n"); return esize; } static ssize_t proc_read_hertz(struct file *filp, char *buf, size_t size, loff_t *off) { size_t esize; size_t left; sprintf(g_hertz_str, "%d\n", HZ); left = strlen(g_hertz_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_hertz_str + *off, esize) != 0) return -EFAULT; *off += esize; } printk(KERN_INFO "hertz file read...\n"); return esize; } static ssize_t proc_read_difference(struct file *filp, char *buf, size_t size, loff_t *off) { static unsigned long prev_jiffies; loff_t left, esize; long int net_jiffies; struct timespec64 ts; net_jiffies = (long)jiffies - (long)prev_jiffies; jiffies_to_timespec64(net_jiffies, &ts); sprintf(g_difference_str, "Jiffy difference: %10ld (%ld seconds + %ld nonoseconds)\n", net_jiffies, (long)ts.tv_sec, (long)ts.tv_nsec); left = (loff_t)strlen(g_difference_str) - *off; esize = left < size ? left : size; if (esize != 0) { if (copy_to_user(buf, g_difference_str + *off, esize) != 0) return -EFAULT; *off += esize; } prev_jiffies = jiffies; return (ssize_t)esize; } static ssize_t proc_read_sleep(struct file *filp, char *buf, size_t size, loff_t *off) { /* set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(HZ * 10); */ ssleep(10); return 0; } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean Aygıt sürücü içerisinde kısa beklemeler gerebilmektedir. Çünkü bazı donanım aygıtlarının programlanabilmesi için bazı beklemelere gereksinim duyulabilmektedir. Kısa beklemeler meşgul döngü yoluyla yani hiç sleep yapılmadan sağlanmaktadır. Ayrıca kısa bekleme yapan fonksiyonlar atomiktir. Atomiklikten kastedilen şey threadler arası geçiş işleminin kapatılmasıdır. Yani kısa bekleme yapan fonksiyonlar threadler arası geçiş işlemini o işlemci için kapatırlar. Bu sırada thread'ler arası geçiş söz konusu olmamaktadır. Ancak donanım kesmeleri bu süre içerisinde oluşabilmektedir. Kısa süreli döngü içerisinde bekleme yapan fonksiyonlar şunlardır: void ndelay(unsigned int nsecs); void udelay(unsigned int usecs); void mdelay(unsigned int msecs); Burada delay nano saniye cinsinden bekleme yapmak için, udelay mikro saniye cinsinden bekeleme yapmak için mdelay ise mili saniye cinsinden bekleme yapmak için kullanılmaktadır. Öte yandan Linux çekirdeklerine belli versiyonyondan sonra bir timer mekanizması da eklenmiştir. Bu sayede aygıt sürücü programcısı belli bir zaman sonra belirlediği bir fonksiyonun çağrılmasını saplayabilmektedir. Bu mekanizmaya "kernel timer" mekanizması denilmektedir. >>> "kernel timer" : Maalesef kernel timer mekanizması da birkaç kere arayüz olarak değiştirilmiştir. Bu mekanizma kullanılıken dikkat edilmesi gereken bir nokta callback fonksiyonun bir proses bağlamında çağrılmadığıdır. Yani callback fonksiyon çağrıldığında biz current makrosu ile o andaki prosese erişemeyiz. O anda çalışan prosesin user alanına kopyalamalar yapamayız. Çünkü callback fonksiyon timer kesmeleri tarafından çağrılmaktadır. Dolayısıyla callback fonksiyon çağrıldığında o anda hangi prosesin çalışmakta olduğu belli değildir. Son Linux çekirdeklerindeki kernel timer kullanımı şöyledir: -> struct timer_list türünden bir yapı nesnesi statik düzeyde tanımlanır ve bu yapı nesnesine ilk değeri verilir. DEFINE_TIMER makrosu ile hem tanımlama hem de ilkdeğer verme işlemi birlikte yapılabilir. Makro şöyledir: #include #define DEFINE_TIMER(_name, _function) Örneğin: DEFINE_TIMER(g_mytimer, timer_proc); Ya da alternatif olarak struct timer_list nesnesi yaratılıp timer_setup makrosuyle de ilkdeğer verilebilir. Makronun parametrik yapısı şöyledir: #include #define timer_setup(timer, callback, flags) Makronun birinci parametresi timer nesnesinin adresini almaktadır. İkinci parametresi çağrılacak fonksiyonun belirtir. flags parametresi 0 geçilebilir. Örneğin: static struct timer_list g_mytimer; timer_setup(&g_mytimer, timer_proc, 0); Buradaki timer fonksiyonun parametrik yapısı şöyle olmalıdır: void timer_proc(struct timer_list *tlisr); -> Tanımlanan struct timer_list nesnesi add_timer fonksiyonu ile bir bağlı listeye yerleştirilir. Bu bağlı liste çekirdeğin içerisinde çekirdek tarafından oluşturulmuş bir listedir. add_timer fonksiyonunun prototipi şöyledir: #include void add_timer(struct timer_list *timer); -> Daha sonra ne zaman fonksiyonun çağrılacağını anlatmak için mod_timer fonksiyonu kullanılır. #include int mod_timer(struct timer_list *timer, unsigned long expires); Buradaki expiry parametresi jiffy türündendir. Bu parametre hedef jiffy değerini içermelidir. (Yani jiffies + gecikme jiffy değeri) -> Timer nesnesinin silinmesi için del_timer ya da del_timer_sync fonksiyonu kullanılmaktadır: #include int del_timer(struct timer_list * timer); int del_timer_sync(struct timer_list * timer); del_timer fonksiyonu eğer timer fonksiyonu o anda başka bir işlemcide çalışıyorsa asenkron biçimde silme yapar. Yani fonksiyon sonlandığında henüz silme gerçekleşmemiş göreli bir süre sonra gerçekleşecek olabilir. Halbuki del_timer_sync fonksiyonu geri dönünce timer silinmesi gerçekleşmiş olur. Eğer timer silinmezse modül çekirdekten atıldığında tüm sistem çökebilir. Normal olarak belirlenen fonksiyon yalnızca 1 kez çağrılmaktadır. Ancak bu fonksiyonun içerisinde yeniden mod_timer çağrılarak çağırma periyodik hale getirilebilir. Aşağıda kernel timer kullanımına basit bir örnek verilmiştir: * Örnek 1, /* timer-module.c */ #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Timmer Module"); static void timer_proc(struct timer_list *tlist); DEFINE_TIMER(g_mytimer, timer_proc); static int __init generic_init(void) { add_timer(&g_mytimer); mod_timer(&g_mytimer, jiffies + msecs_to_jiffies(5000)); printk(KERN_INFO "timer-module module init...\n"); return 0; } static void timer_proc(struct timer_list *tlist) { static int count = 0; if (count == 5) { del_timer(&g_mytimer); count = 0; return; } ++count; printk(KERN_INFO "timer callback (%d)\n", count); mod_timer(&g_mytimer, jiffies + msecs_to_jiffies(5000)); } static void __exit generic_exit(void) { printk(KERN_INFO "timer-module module exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean >> "threads in kernel modules" : Önceki konularda da UNIX/Linux sistemlerinde kernel mode'da çalışan işletim sistemine ait thread'ler olduğundan bahsetmiştik. Bu thread'ler çalışma kuyruğunda (run queue) bulunan ve uykuya dalabilen işletim sisteminin bir parçası durumundaki thread'lerdir. İşletim sistemine ait bu thread'ler çeşitli işlemlerden sorumludurlar. Linux işletim sisteminde kernel thread'ler genellikle "user mode daemon"lar gibi sonu 'd' ile bitecek biçimde isimlendirilmiştir. Ancak çekirdeğe ait olan bu thread'lerin ismi 'k' (kernel'dan geliyor) ile başlatılmıştır. Örneğin "kupdated", "kswapd", "keventd" gibi. İşte aygıt sürücüler de isterlerse arka planda kernel mode'da bir proses gibi çalışan thread'ler yaratabilirler. Ancak bu thread'ler bir proses ile ilişkisiz biçimde çalıştırılmaktadır. Bu nedenle bunlar içerisinde current makrosu, ve copy_to_user ya da copy_from_user gibi fonksiyonlar kullanılamaz. Aygıt sürücü kodlarımız genellikle bir olay olduğunda (örneğin kesme gibi) ya da user mode'dan çağrıldığında (read, write, ioctl gibi) çalıştırılmaktadır. Ancak kernel thread'ler aygıt sürücüye sanki bir programmış gibi kernel mode'da sürekli çalışma imkanı vermektedir. Kernel thread'ler sırasıyla şu adımlarlardan geçilerek kullanılmaktadır: -> Önce kernel thread aygıt sürücü içerisinde yaratılır. Yaratılma modülün init fonksiyonunda yapılabileceği gibi aygıt sürücü ilk kez açıldığında open fonksiyonunda ya da belli bir süre sonra belli bir fonksiyonda da yapılabilmektedir. Kernel thread'ler kthread_create fonksiyonuyla yaratılmaktadır: #include struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char *namefmt); Fonksiyonun birinci parametresi thread akışının başlatılacağı fonksiyonun adresini almaktadır. Bu fonksiyon void * türünden parametreye ve int geri dönüş değerine sahip olma zorundadır. Fonksiyonun ikinci parametresi thread fonksiyonuna geçirilecek parametreyi belirtmektedir. Eğer bir kernel thread'e bir parametre geçirilmek istenmiyorsa bu parametre için NULL adres girilebilir. Fonksiyon üçüncü parametresi proc dosya sisteminde (dolayısıyla "ps" komutunda) görüntülenecek ismi belirtir. Fonksiyon başarı durumunda yaratılan thread'in task_struct adresine, başarısızlık durumunda negatif errno değerine geri dönmektedir. Adrese geri dönen diğer kernel fonksiyonlarında olduğu gibi fonksiyonun başarı durumu IS_ERR makrosuyla test edilmelidir. Eğer fonksiyon başarısız olmuşsa negatif errno değeri PTR_ERR makrosuyle elde elde edilebilir. Örneğin: struct task_struct *ts; ts = kthread_create(...); if (IS_ERR(ts)) { printk(KERN_ERROR "cannot create kernel thread!..") return PTR_ERR(ts); } Anımsanacağı gibi Linux sistemlerinde prosesler ve thread'ler task_struct yapısıyla temsil edilmektedir. İşte bu fonksiyon da başarı durumunda çekirdek tarafından yaratılan task_struct nesnesinin adresini bize vermektedir. Kernel thread bu fonksiyonla yaratıldıktan sonra hemen çalışmaz. Onu çalıştırmak için wake_up_process fonksiyonun çağrılması gerekir: #include int wake_up_process(struct task_struct *tsk); Fonksiyon ilgili kernel thread'in task_struct adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda negatif errno değerine geri döner. Aslında yukarıdaki işlemi tek hamlede yapan kthread_run isimli bir fonksiyon da vardır: struct task_struct *kthread_run(int (*threadfn)(void *data), void *data, const char *namefmt); -> Kernel thread kthread_stop fonksiyonuyla herhangi bir zaman ya da aygıt sürücü bellekten atılırken yok edilebilir: int kthread_stop(struct task_struct *ts); Fonksiyon thread sonlanana kadar blokeye yol açar. Fonksiyon thread fonksiyonunun exit koduyla (yani thread fonksiyonunun geri dönüş değeri ile) geri dönmektedir. Genellikle programcılar thread fonksiyonlarını başarı durumunda sıfır, başarısızlık durumunda sıfır dışı bir değerle geri döndürmektedir. Burada önemli nokta kthread_stop fonksiyonunun kernel thread'i zorla sonlandırılmadığıdır. Kernel thread'in sonlanması zorla yapılmaz. kthread_stop fonksiyonu bir bayrağı set eder. Kernel thread de tipik olarak bir döngü içerisinde "bu bayrak set edilmiş mi" diye bakar. Eğer bayrak set edilmişse kendini sonlandırır. Kernel thread'in bu bayrağa bakması kthread_should_stop fonksiyonuyla yapılmaktadır. #include bool kthread_should_stop(void); Fonksiyon eğer bu flag set edilmişse sıfır dışı bir değere, set edilmediyse 0 değerine geri dönmektedir. Tipik olarak kernel thread fonksiyonu aşağıdaki gibi bir döngüde yaşamını geçirir: while (!kthread_should_stop()) { ... } Tabii aslında biz kthread_create fonksiyonu ile bir kernel thread yaratmak istediğimizde asıl thread'in başlatıldığı fonksiyon çekirdek içerisindeki bir fonksiyondur. Bizim kthread_create fonksiyonuna verdiğimiz fonksiyon bu fonksiyon tarafından çağrılmaktadır Dolayısıyla bizim fonksiyonumuz bittiğinde akış yine çekirdek içerisindeki asıl fonksiyona döner. O fonksiyonda da yaratılmış thread kaynakları otomatik boşaltılır. Yani biz bir thread yarattığımız zaman onun yok edilmesi thread fonksiyonu bittiğinde otomatik yapılmaktadır. Kernel thread'in kendisini sonşandırması do_exit fonksiyonuyla sağlanabilmektedir. Aslında do_exit fonksiyonu prosesleri sonlandıran sys_exit fonksiyonun doğrudan çağırdığı taban fonksiyondur. #include void do_exit(long code); Fonksiyon thread'in exit kodunu parametre olarak almaktadır. Kernel thread normal biçimde ya da kthread_stop fonksiyonuyal sonlanmışsa artık thread'in tüm kaynakları (task_struct yapısı da) serbest bırakulmaktadır. Dolayısıyla artık ona kthread_stop uygulamamak gerekir. Aşağıdaki örnekte modül initialize edilirken kernel thread yaratılmış, modül yok edilirken kthread_stop ile thread'in sonlanması beklenmiştir. kernel thread içerisinde msleep fonksiyonu ile 1 saniyelik beklemeler yapılmıştır. * Örnek 1, /* kernel-thread-module.c */ #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("Kernel Thread Module"); static int kernel_thread_proc(void *param); struct task_struct *g_ts; static int __init generic_init(void) { printk(KERN_INFO "kernel-thread-module init...\n"); g_ts = kthread_run(kernel_thread_proc, NULL, "kmythreadd"); if (IS_ERR(g_ts)) { printk(KERN_ERR "cannot create kernel thread!..\n"); return PTR_ERR(g_ts); } return 0; } static int kernel_thread_proc(void *param) { static int count; printk(KERN_INFO "kernel-thread starts...\n"); while (!kthread_should_stop()) { printk(KERN_INFO "kernel-thread is running: %d\n", count); msleep(1000); ++count; } return 0; } static void __exit generic_exit(void) { int ecode; ecode = kthread_stop(g_ts); printk(KERN_INFO "kernel thread exits with code \"%d\"\n", ecode); printk(KERN_INFO "kernel-thread-module exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean >> "Interrupts" : İşlemcinin çalıştırmakta olduğu koda ara vererek başka bir kodu çalıştırması ve çalıştırma bittikten sonra kaldığı yerden devam etmesi sürecine "kesme (interrupt)" denilmektedir. Kesmeler oluşma biçiminde göre üçe ayrılmaktadır: -> Donanım Kesmeleri (Hardware Interrupts) -> İçsel Kesmeler (Internal Interrupts) -> Yazılım Kesmeleri (Software Interrupts) Kesme denildiğinde akla default olarak donanım kesmeleri gelmektedir. Donanım kesmeleri CPU'nın bir ucunun (genellekle bu uca INT ucu denilmektedir) elektriksel olarak dışsal bir birim tarafından uyarılmasıyla oluşmaktadır. Yani donanım kesmeleri o anda çalışmakta olan koddan bağımsız bir biçimde dış dünyadaki birimler tarafından oluşturulmaktadır. PC terminolojisinde donanım kesmesi oluşturan kaynaklara IRQ da denilmektedir. İçsel kesmeler CPU'nun kendi çalışması sırasında kendisinin oluşturduğu kesmelerdir. Intel bu tür kesmelerin önemli bir bölümünü "fault" olarak isimlendirmektedir. Örneğin fiziksel RAM'de olmayan bir sayfaya erişildiğinde CPU "page fault" denilen içsel kesme oluşturmaktadır. Yazılım kesmeleri ise programcının program kodyla oluşturduğu kesmelerdir. Her türlü CPU'da yazılım kesmesi oluşturulamamaktadır. Bir kesme oluştuğunda çalışıtırılan koda "kesme kodu (interrupt handler)" denilmektedir. Donanım kesmesi oluşturan elektronik birimlerin hepsi doğrudan CPU'nın INT ucuna bağlanmamaktadır. Çünkü bunun pek çok sakıncası vardır. Genellikle bu amaçla bu işe aracılık eden daha akıllı bir işlemciler kullanılmaktadır. Bu işlemcilere genel olarak "kesme denetelecileri (interrupt controllers)" denilmektedir. Bazı mimarilerde kesme denetleyicisi işlemci işlemcinin içerisinde bulunmakıtadır. Bazı mimarilerde ise dışarıda ayrı bir entegre devre olarak bulunmaktadır. Tabii artık pek çok entegre devre SoC (System on Chip) adı altında tek bir entegre devrenin içersine yerleşirilmiş durumdadır. Kesme denetleyicilerinin temel işlevi şöyledir: -> Birden fazla donanım biriminin aynı anda kesme oluşturması durumunda kesme denetleyicisi bunları sıraya dizebilmektedir. -> Birden fazla donanım biriminin aynı anda kesme oluşturması durumunda kesme denetleyicisi bunlara öncelik verebilmektedir. -> Belli birimlerden gelen kesme isteklerini kesme denetleyicisi görmezden gelebilmektedir. Buna ilgili IRQ'nun disable edilmesi denilmektedir. -> Kesme denetleyicileri çok çekirdekli donanımlarda kesmenin belli bir çekirdekte çalışttırılabilmesini sağlayabilmektedir. Bugün kullandığımız PC'lerde (laptop ve notebook'lar da dahil olmak üzere) eskiden kesme denetleyicisi olarak bir tane Intel'in 8259 (PIC) denilen entegre devresi kullanılıyordu. Bunun 8 girişi bulunuyordu. Yani bu kesme denetleyicisinin uçları 8 ayrı donanım birimine bağlanabiliyordu. | (INT ucu CPU'ya bağlanır) <8259 (PIC)> | | | | | | | | 0 1 2 3 4 5 6 7 Bu uçlara IRQ uçları deniliyordu ve bu uçlar değişik donanım birimlerine bağlıydı. Böylece bir donanım birimi kesme oluşturmak isterse kesme denetleyicisinin igili ucunu uyarıyordu. Kesme deetleyicisi de CPU'nın INT ucunu uuarıyordu. İlk PC'lerde toplam 8 IRQ vardı. Ancak 80'li yılların ortalarında PC mimarisinde değişikler yapılarak kesme denetleyicisinin sayısı iki'ye yükseltildi. Böylece IRQ uçlarının sayısı da 15'e yükseltilmiş oldu. Intel'in iki 8259 işlemcisini katkat bağlayabilmek için birinci kesme kesme denetleyicisinin (Master PIC) bir ucunun ikinci kesme denetleyicsinin INGT ucuna bağlanması gerekmektedir. İşte PC mimarsinde birinci kesme denetleyicisinin 2 numaralı ucu ikinci kesme denetleyicisine bağlanmıştır. Böylece toplam IRQ'ların sayısı 16 değil 15 olmaktadır. | (INT ucu CPU'ya baplanır) | <8259 (PIC)> <8259 (PIC)> | | X | | | | | | | | | | | | | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Ancak zamanla 15 IRQ ucu da yetersiz kalmaya başlamıştır. Çok çekirdekli sistemlerde her çekirdeğin (yani CPU'nun) ayrı bir INT ucu vardır. Yani bu çekirdekler diğerlerinden bağımsız kesme alabilmektedir. İşte zamanla Intel'in klasik 8259 kesme denetleyicisi daha gelişmiş olam ve ismine IOAPIC denilen kesme denetleyicisi ile değiştirilmiştir. Bugün kullandığımız Intel tabanlı bilgisayar mimarisinde artık IOAPIC kesme denetleyicileri bulunmaktadır. Bu yeni kesme denetleyicisinin 24 IRQ ucu vardır. IOAPIC birden fazla çekirdeğin bulunduğu durumda tek bir çekirdeğe değil tüm çekirdeklere bağlanmaktadır. Dolayısıyla istenilen bir çekirdekte kesme oluşturabilmektedir. IOAPIC devresinin bazı uçları bazı donanım birimelrine bağlı biçimdedir. Ancak bazı uçları boştadır. Bugün kullanılan ve ismine PCI ya da PCI-X denilen genişleme yuvaalarının bazı uçları bu IOAPIC ile bağlantılıdır. Dolayısıyla genişleme yuvalarına takılan kartlar da IRQ oluşturabilmektedir. Bugün Pentium ve eşdeğer AMD ieşlemcilerinin içerisinde (her çekirdeğin içeisidne) aynı zamanda ismine "Local APIC" denilen bir kesme denetleyicisi de vardır. Bu local APIC iki uca sahiptir. Local APIC içerisinde aynı zamanda bir timer devresi de bulunmaktadır. Bu timer devresi periyodik donanım kesmesi oluşturmak için kullanılmaktadır. Intel ve AMD çekirdeklerinin içerisinde bulunan APIC devresinin en önemli özelliği kesmeleri artık uçlarla değil veri yoluyla (data bus) oluşturabilmesidir. Buu özellik sayesinde hiç işlemcinin INT uyarılmadan çok fazla sayıda kesme sanki belleğe bir değer yazıyormuş gibi oluşturulabilmektedir. Bu tekniğe "Message Signaled Intterpt (MSI)" denilmektedir. Gerçekten de bugün PCI slotlara takılan bazı kartlar kesmeleri doğrudan belli bir çekirdekte MSI kullanarak oluşturmaktadır. O halde kullanığımız Intel taabanlı PC mimarisideki bugünkü durum şöyledir: -> Bazı donanım birimleri built-in biçimde IOAPIC'in uçlarına bağlı durumdadır. Bu uçlar eskiye uyumu korumak için 8259'un uçlarıyla aynı biçimde bağlı gibi IRQ oluşturmaktadır. -> Bazı PCI kartlar slot üzerindeki 4 IRQ hattından (INTA, INTB, INTC, INTD) birini kullanarak kesme oluşturmaktadır. Bu hatlar IOAPIC'in bazı uçlarına bağlıdır. -> Bazı PCI kartlar ise doğurdan modern MSI sistemini kullanarak IOAPIC'i pass geçerek bellek işlemleriyle doğrudan ilgili çekirdekte kesme oluşturabilmektedir. Bir aygıt sürücü programcısı mademki birtakım kartlar için onu işler hale getiren temel yazılımları da yazma iddiasındadır. O halde o kartın kullanacağı kesme için kesme kodlarını (interrupt handlers) yazabilmelidir. Tabii işletim sisteminin aygıt sürücü mimarisinde bu işlemler de özel zernel fonksiyonlarıyla yapılır. Yani kesme kodu yazmanın belli bir kuralı vardır. Pekiyi çok çekirdekli bilgisayar sistemlerinde oluşan bir kesme Intel taabanlı PC mimariisnde hangi çekirdek tarafından işlenmektedir? İşte bugün kullanılan IOAPIC devreleri bu bakımdan şu özelliklere sahiptir: -> Kesme IOAPIC tarafından donanım biriminin istediği bir çekirdekte oluşturulabilir. -> Kesme IOAPIC tarafından en az yüklü çekirdeğe karar verilerek orada oluşturulabilmektedir. -> Kesme IOAPIC tarafından döngüsel bir biçimde (yani sırasıyla her bir çekirdekte) oluşturulabilmektedir. IOAPIC'in en az yüklü işlemciyi bilmesi mümkün değildir. Onu ancak işletim sistemi bilebilir. İşte işlemcilerin Local APIC'leri içerisinde özel bazı yazmaçlar vardır. Aslında IOAPIC bu yazmaçtaki değerlere bakıp en düşüğünü seçmektedir. Bu değerleri de işletim sistemi set eder. İşletim sisteminin yaptığı bu faaliyete "kesme dengeleme (IRQ balancing)" denilmektedir. Linux sistemlerinde bir süredir kesme dengelemesi işletim sisteminin kernel thread'i (irqbalance) tarafından yapılmaktadır. Böylece Linux sistemlerinde aslında donanım kesmeleri her defasında farklı çekirdeklerde çalıştırılıyor olabilir. Pek çok CPU ailesinde donanım kesmelerinin teorik maksimum bir limiti vardır. Örneğin Intel mimarisinde toplam kesme sayısı 256'yı geçememektedir. Yani bu mimaride en fazla 256 farklı kesme oluşturulabilmektedir. Bu mimaride her kesmenin bir numarası vardır. IRQ numarası ile kesme numarasının bir ilgisi yoktur. Biz örneğin PIC ya da IOAPIC'i programlayarak belli bir kesmenin belli bir IRQ için belli numaralı bir kesmenin oluşmasını sağlayabiliriz. Örneğin timer (IRQ-0) için 8 numaralı kesmenin çalışmasını sağlayabiliriz. Pekiyi bir IRQ oluşturulduğunda çekirdek kaç numaralı kesme kodunun çalıştırılacağını nereden anlamaktadır? İşte PIC ya da IOAPIC CPU'nun INT ucunu uyararak kesme oluşturuken veri yolunun ilk 8 ucundan kesme numarasını da CPU'ya bildirmektedir. >> "Interrupts in Kernel Modules" : Şimdi de aygıt sürücüler içerisinde kesmelerin nasıl ele alınacağı üzerinde duralım. Bir donanım kesmesi oluştuğunda aslında işletim sisteminin kesme kodu (interrupt handler) devreye girmektedir. Ancak işletim sisteminin kesme kodu istek doğrultusunda aygıt sürücülerin içerisindeki fonksiyonları çağırabilmektedir. Farklı aygıt sürücüleri aynı IRQ için istekte bulunabilir. Bu durumda işletim sistemi IRQ oluştuğunda farklı aygıt sürücülerdeki fonksiyonları belli bir düzen içerisinde çağırmaktadır. Aygıt sürücü programcısı bir kesme oluştuğunda aygıt sürücüsünün içerisindeki bir fonksiyonunun çağrılmasını istiyorsa önce onu request_irq isimli kernel fonksiyonuyla register ettirmelidir. #include int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id); Fonksiyonun birinci parametresi IRQ numarasını, ikinci parametresi IRQ oluştuğunda çağrılacak fonksiyonu belirtmektedir. Bu fonksiyonun geri dönüş değeri irqreturn_t türünden parametreleri de sırasıyla int ve void * türündendir. Örneğin: irqreturn_t my_irq_handler(int irq, void *dev_id) { ... } Buradaki irqreturn_t türü bir enum türü olarak typedef edilmiştir. Bu enum türünün elemanları şunlardır: enum irqreturn { IRQ_NONE = (0 << 0), IRQ_HANDLED = (1 << 0), IRQ_WAKE_THREAD = (1 << 1), }; typedef enum irqreturn irqreturn_t; request_irq fonksiyonun üçüncü parametresi bazı bayraklardan oluşur. Bu bayrak 0 geçilebilir ya da örneğin IRQF_SHARED geçilebilir. Diğer seçenkler için dokümanlara başvurabilirsiniz. IRQF_SHARED aynı kesmenin birden fazla aygıt sürücü tarafından kullanılabileceği anlamına gelmektedir. (Tabii biz ilk register ettiren değilsek daha önce resgister ettirenlerin bu bayrağı kullanmış olması gerekir. Aksi halde biz de bu bayrağı kullanamayız.) Fonksiyonun dördüncü parametresi "/proc/interrupts" dosyasında görüntülenecek ismi belirtir. Son parametre ise sistem genelinde tek olan bir nesnenin adresi olarak girilmelidir. Aygıt sürücü programcıları bu parametreye tipik olarak aygıt yapısını ya da çağrılacak foksiyonu girerler. Bu parametre IRQ handler fonksiyonuna ikinci parametre olarak geçilmektedir. Fonksiyon başarı durumunda o değerine başarısızlık durumunda negatif hata değerine geri dönmelidir. Örneğin: if ((result = request_irq(1, my_irq_handler, IRQF_SHARED, "my_irq1", NULL)) != 0) { ... return result; } Bir kesme kodu request_irq fonksiyonuyla register ettirilmişse bunun geri alınması free_irq fonksiyonuyla yapılmaktadır: #include const void *free_irq(unsigned int irq, void *dev_id); Fonksiyonun birinci parametresi silinecek irq numarasını, ikinci parametresi irq_reuest fonksiyonuna girilen son parametreyi belirtir. Fonksiyon başarı durumunda aygıt irq_request fonksiyonunda verilen isme, başarısızlık durumunda NULL adrese geri dönmektedir. Geri dönüş değeri bir hata kodu ieçrmemektedir. Normal olarak fonksiyonun ikinci parametresine request_irq fonksiyonunun son parametresiyle aynı değer geçilir. Bu parametrenin neden bu fonksiyona geirildiğinin bazı ayrıntıları vardır. Örneğin: if (free_irq(1, NULL) == NULL) printk(KERN_INFO "cannot free IRQ\n"); Pekiyi IRQ fonksiyonundan (IRQ habdler) hangi değerle geri dönülmelidir. Aslında programcı bu fonksiyondan ya IRQ_NONE değeri ile ya da IRQ_HANDLED değeri ile geri döner. Eğer programcı kesme kodu içerisinde yapmak istediği şeyi yapmışsa fonksiyondan IRQ_HANDLED, yapamamışsa ya da yapmak istememişse fonksiyondan IRQ_NONE değeri ile geri döner. Örneğin: static irqreturn_t my_irq_handler(int irq, void *dev_id) { ... return IRQ_HANDLED; } Donanımsal kesme mekanizmasının tipik örneklerinden biri klavye kullanımıdır. PC klavyesinde bir tuşa basıldığında klavye içerisindeki işlemci (keyboard encoder - Eskiden Intel 8048 ya da Holtek HT82K629B) basılan ya da çekilen tuşun klavyedeki sıra numarasını (buna "scan code" denilmektedir) dış dünyaya seri bir biçimde kodlamaktadır. Bu bilgi bilgisayardaki klavye denetleyicisine (Eskiden Intel 8042) gelir. Klavye denetleyicisi (keyboard controller) bu scan kodu kendi içerisinde bir yazmaçta saklar. PIC ya da IOAPIC'in 1 numaralı ucu klavye denetleyicisine bağlıdır ve bu uçtan IRQ1 kesmesini oluşturulmaktadır. Dolayısıyla biz bir tuşa bastığımızda otomatik olarak basılan tuşa ilişkin klavye scan kodu bilgisayar tarafına iletilir ve IRQ1 kesmesi oluşturulur. IRQ1 kesme kodu birincil olarak işletim sistemi tarafından ele alınmaktadır. İşletim sistemi de bu IRQ oluştuğunda aygıt sürücülerin belirlediği fonksiyonları çağırmaktadır. Klavyede yalnızca bir tuşa basılınca değil parmak tuştan çekildiğinde de yine klavye işlemcisi çekilen tuşun scan kodunu klavye denetleyicisine gönderip IRQ1 kesmesinin oluşmasına yol açmaktadır. Yani hem tuşa basında hem de parmak tuştan çekildiğinde IRQ1 oluşmaktadır. Klavye terminolojisinde parmağın tuşa basılmasıyla gönderilen scan koda "make code", parmağın tuştan çekilmesiyle gönderilen koda ise "break code" denilmektedir. Bugün PC'lerde kullandığımız klavyelerde parmak tuştan çekildiğinde önce PC tarafında bir F0 byte'ı ve sonra da tuşun scan kodu gönderilmektedir. Örneğin parmağımızı "A" tuşuna basıp çekelim şu scan kodlar bilgisayar tarafında gönderilecektir: Ctrl, Shift, Alt, Caps-Lock gibi tuşların diğer tuşlardan bir farkı yoktur. Ctrl+C gibi bir tuşa basıdığı işletim sisteminin tuttuğu flag değişkenlerle tespit edilmektedir. Örneğin biz Ctrl+C tuşlarına basıp çekmiş olalım. Klavye işlemcisi bilgisayar tarafında şu kodları gönderecektir: İşte Ctrl tuşuna basıldığını fark eden işletim sistemi bir flag'i set eder, parmak bu tuştan bırakıldığında flag'i reset eder. Böylece diğer tuşlara basıldığında bu flag'e bakılarak Ctrl tuşu ile bu tuşa basılıp basılmadığı anlaşılmaktadır. Pekiyi biz Linux'ta stdin dosyasından (0 numaralı betimleyici) okuma yaptığımızda neler olmaktadır? İşte aslında işletim sistemi bir tula basıldığında basılan tuşları klavye denetleyicisinde alır ve onları biz kuyruk sisteminde saklar. Terminal aygıt sürücüsü de bu kuyruğa başvurur. Kurukta hiç tuş yoksa thread'i bu amaçla oluşturulmuş bir wait kuruğunda bekletir. Klavyeden tuşa basılınca wait kuruğunda bekleyen thread'leri uyandırır. Yani okuma yapıldığında o anda klavyeden okuma yapılmamaktadır. Kuyruklanmış tuşlar okunmaktadır. Klavyedeki tuşların üzerinde yazan harflerin hiçbir önemi yoktur. Yani İngilizce klavye ile Türkçe klavye aynı tuşlar için aynı scan kodu göndermektedir. Basılan tuşun hangi tuş olduğu aslında dil ayarlarına bakılarak işletim sistemi tarafından anlamlandırılmaktadır. Klavye ile bilgisayar arasındaki iletişim tek yönlü değil çift yönlüdür. Yani klavye denetleyicisi de (PC tarafındaki denetleyici) isterse klavye içerisindeki işlemciye komutlar gönderebilmektedir. Aslında klavye üzerindeki ışıkların yakılması da klavyenin içerisinde tuşa basılınca yapılmamaktadır. Işıklı tuşlara basıldığında gönderilen scan kod klavye denetleyicisi tarafından alınır, eğer bu tuş ışıklı tuşlardan biri ise klavye denetleyicisi klavye işlemcisine "falanca ışığı yak" komutunu göndermektedir. Özetle yeniden ifade edersek klavyedki ışıklar klaye devresi tarafından ilgili tuşlara basılınca yakılmamaktadır. Karşı taraftan emir geldiğinde yakılmaktadır. Tasarımın bu biçimde yapılmış olması çok daha esnek bir kullanım oluşturmaktadır. Aşağıdaki örnekte klavyeden tuşa basıldığında ve çekildiğinde oluşan 1 numaralı IRQ ele alınıp işlenmiştir. * Örnek 1, /* irq-driver.c */ #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, }; static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "Cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { static int count = 0; ++count; if (count % 1000 == 0) printk(KERN_INFO "Keyboard IRQ occurred: %d\n", count); return IRQ_HANDLED; } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); Bazen sistem programcısı belli bir IRQ'yu belli süre için disable etmek isteyebilir. Bunun için disable_irq ve enable_irq isimli iki kernel fonksiyonu kullanılmaktadır. Bu fonksiyonlar belli numaralı bir IRQ'yu disable ve enable etmeketdir. Ancak bu fonksiyonlar bu işlemi doğrudan kesme denetleyicisini (PIC ya da IOAPIC) programlayarak yapmamaktadır. void disable_irq(unsigned int irq); void enable_irq(unsigned int irq); Fonksiyonlar IRQ numarasını parametre olarak alır. /*================================================================================================================================*/ (159_02_08_2024) & (160_04_08_2024) > "kernel modules" : > IO Portlarının Kullanımı: CPU ile RAM arasında veri transferi aslında tamamen elektriksel düzeyde 1'lerle 0'larla gerçekleşmektedir. CPU'nun adres uçları (address bus) RAM'in adres uçlarına bağlanır. Bu adres uçları RAM'den ransfer edilecek bilginin fiziksel adresini belirtmek için kullanılmaktadır CPU'nun veri uçları (data bus) uçları ise bilginin alınıp gönderilmesinde kullanılmaktadır. İşlemin okuma mı yazma mı olduğu genellikle R/W biçiminde isimlendirilen ayrı bir control ucuyla yapılmaktadır. Örneğin, 32 bit Intel işlemcilerinde MOV EAX, [XXXXXXXX] komutu RAM'deki XXXXXXXX adresinden başlayan 4 byte bilginin CPU içerisindeki EAX yazmacına çekileceği anlamına gelmektedir. Bu makine komutu işletilirken CPU önce erişilecek adres olan XXXXXXXX adresini adres uçlarına elektriksel işaret olarak kodlar. RAM bu adresi alır, bu adresten başlayan 4 byte'lık bilgiyi veri uçlarına elektirksel olarak kodlar. CPU'da bu uçlardan bilgiyi yine elektirksel olarak alır ve EAX yazmacına yerleştirir. CPU'nun adres uçları RAM'in adres uçlarına, CPU'nun veri uçları ise RAM'in veri uçlarına bağlıdır. Tranfer yönü R/W ucuyla belirlenmektedir. Tabii CPU'lar bugün DRAM belleklerden daha hızlıdır. Dolayısıyla CPU RAM'den yanıt gelene kadar beklemektedir (wait state). Bir bilisayar sisteminde yalnızca Merkezi İşlemci (CPU) değil aynı zamanda yerel birtakım olaylardan sorumlu yardımcı işlemciler de vardır. Bu yardımcı işlemcilere genellikle "controller (denetleyici)" denilmektedir. Örneğin klasik PC mimarisinde "Kesme Denetleyicisi (Intel 8250-PIC)", "Klavye Denetleyicisi (Intel 8042-KC)", "UART denetleyicisi (Intel 8250/NS 16550-UART)" gibi pek çok işlemci vardır. Bu işlemcilere komutlar tıpkı CPU/RAM haberleşmesinde olduğu gibi elektriksel düzeyde CPU'nun adres ve veri uçları yoluyla gönderilmekte ve bu işlemcilerden bilgiler yine tıpkı RAM'de olduğu gibi adres ve veri uçları yoluyla alınmaktadır. Yani CPU'nun adres ve veri uçları yalnızca RAM'e değil yardımcı işlemcilere de bağlıdır. Pekiyi bu durumda CPU RAM'e erişirken aynı zamanda yardımcı işlemcilere de erişmez mi? İşte CPU'ların genellikle IO/Mem biçiminde isimlendirilen bir uçları daha vardır. Bu ucun 5V ya da 0V olması erişimin RAM'e mi yoksa yardımcı işlemciye mi yapılacağını belirtir. Yardımcı işlemcileri tasarlayanlar bu uca bakarak bilginin RAM'e değil kendilerine geldiğini anlayabilirler. Nomral RAM erişimlerine ilişkin MOV ya da LOAD/STORE makine komutlarında bu IO/Mem ucu "mem" biçiminde aktive edilir. Ancak bazı IN, OUT gibi komutlarda bu uç "IO" biçiminde aktive edilmektedir. Bu durumda yardımcı işlemcilere erişmek için MOV, LOAD/STORE komutları değil genellikle IN, OUT biçiminde isimlendirilen komutları kullanılmaktadır. Ancak bazı yardımcı işlemciler bu "IO/Mem" ucu tam tersine "Mem" olarak aktive edildiğinde de işlevini yapacak biçimde konfigüre edilmiş olabilir. Bu durumda bu işlemcilere biz IN, OUT komutlarıyla değil RAM'e erişiyormuş gibi MOV, LOAD/STORE komutlarıyla erişiriz. İşte bu tekniğe "Memory Mapped IO" denilmektedir. Memory Mapped IO yardımcı işlemcilere sanki RAM'miş gibi erişme anlamına gelir. Bunun da sistem programcısı için önemli avantajları vardır. Sistem programcısı bu sayede göstericileri kullanarak bu işlemcilere erişebilmektedir. Normal "IO/Mem" ucu "IO" biçiminde aktive edilerek erişimlere "Port-Mapped IO" da denilmektedir. Bazı mimarilerde her iki teknik de yoğun kullanılmaktadır. Ancak bazı mimarilerde "memory mapped io" tekniği daha yoğun kullanılabilmektedir. Memory mapped IO tekniği kullanılırken artık RAM'in ilgili adresteki kısmına erişilemez ya da bu erişimin bir anlamı kalmaz. Yani adeta bu teknikte sanki RAM'in bir bölümü çıkartılmış onun yerine ilgili işlemci oraya takılmış gibi bir etki oluşmaktadır. Örneğin memory mapped IO bilgisayar sistemlerinde grafik kartları tarafından yoğun olarak kullanılmaktadır. Grafik kartlarını tasarlayanlar kartın üzerindeki RAM'in (bu ana RAM değil) içeriğini belli periyotlarla ekrana göndermektedir. Programcı da C'de göstericileri kullanarak belli adrese yazma yapma yoluyla ekrana belirli şeylerin çıkmasını sağlayabilmektedir. Pekiyi yardımcı işlemcileri biribirnden ayıran şey nedir? İşte CPU'nun adres uçları bu yardımcı işlemciler tarafından özel bazı değerlerde ise dikkate alınmaktadır. Yani nasıl RAM'deki byte'ların adresleri varsa yardımcı işlemcilerin de birer donanımsal adresleri vardır. Bu adreslere genellikle "port numaraları" da denilmektedir. Yardımcı işlemcilerin port numaraları donanım mimarisini tasarlayanlar tarafından donanımsal olarak önceden belirlenmiştir. Ancak modern sistemlerde programlama yoluyla değiştirilebilen port adresleri de söz konusu olmaktadır. PC mimarisinde programlanabilen port numarasına sahip olan işlemcilere "plug and play (PnP)" işlemciler de denilmektedir. O halde bizim bir yardımcı işlemciyi programlayabilmemiz için şu bilgileri edinmiş olmamız gerekmektedir: -> Yardımcı işlemci "port mapped IO" mu yoksa "memory mapped IO" mu kullanmaktadır? -> Yardımcı işlemcinin port numaraları (ya da memory mapped io söz konusu ise belek adresleri) nedir? -> Bu yardımcı işlemcinin hangi portuna (memory mapped io söz konusu ise hangi adrese) hangi değerler gönderildiğinde bu işlemci ne yapacaktır? -> İşlemci bize bilgi verecekse hangi bunu portu okuyarak (memory mapped IO söz konusu ise hangi adresi okuyarak) vermektedir. Verilen bilginin biçimi nedir? Yardımcı işlemciler yalnızca bilgisayar donanımın içerisinde donanımsal olarak çivilenmiş bir biçimde bulunmayabilirler. Bazı bilgisayar sistemlerinde (örneğin masasüstü PC'lerde) genişleme yuvaları vardır. Bu genişleme yuvaları CPU'nun adres ve veri yoluna erişebilmektedir. Bu genişleme yuvaları için kart tasarlayan tasarımcılar kartlarının üzerinde yardımcı işlemcileri bulundurabilirler. Böylece ilgili kart takıldığında sanki sisteme yeni bir yardımcı işlemci takılmış gibi etki oluşmaktadır. CPU'ya neden "merkezi (central)" işlemci denildiğini artık anlayabilirsiniz. Bilgisayar sistemlerinde kendi yerel işlemlerinden sorumlu pek çok yardımcı işlemci olabilir. Ancak bunların hepsini elektriksel olarak CPU programlamaktadır. Bu nedenle CPU'ya merkezi işlemci denilmiştir. Tabii CPU aslında bizim yazdığımız programları çalıştırır. Yani yardımcı işlemcileri de sonuç olarak biz programlamış oluruz. Pekiyi yardımcı işlemcileri programlarken onlara tek hamlede kaç byte bilgi gönderip onlardan kaç byte bilgi okuyabiliriz? İşte bazı işlemciler (özellikle eskiden tasarlanmış olanlar) byte düzeyinde programlanmaktadır. Bazıları ise WORD düzeyinde bazıları ise DWORD düzeyinde programlanabilmektedir. O halde bizim bir haberleşme portuna 1 byte, 2 byte, 4 byte gönderip alabilmemiz gerekir. Pekiyi bir yardımcı işlemci kernel modda programlanabiliyorsa user mode programlar bu yardımcı işlemciyi nasıl kullanmaktadır? İşte tipik olarak user mode programlar ioctl işlemleriyle aygıt sürücünün kodlarını çalıştırırlar. Aygıt sürücüler de bu kodlarda ilgili yarmcı işlemciye komutlar yollayabilir. Bazen read/write işlemleri de bu amaçla kullanılabilmektedir. Tabii karmaşık yardımcı işlemciler için aygıt sürücüleri yazanlar faydalaı işlemlerin daha kolay yapılabilmesi için daha yüksek seviyeli fonksiyonları bir API kütüphanesi yoluyla sağlayabilmektedir. İşlemcilerin IN, OUT gibi makine komutları "özel (privileged)" komutlardır. Bunlar user mode'dan kullanılırsa işlemci koruma mekanizması gereği bir içsel kesme oluşturur, işletim sistemi de bu kesme kodunda prosesi sonlandırır. Dolayısıyla bu komutları kullanarak donanım aygıtlarıyla konuşabilmek için kernel mod aygıt sürücü yazmak gerekir. Aygıtlara erişmekte kullanılan komutlar CPU mimarisine göre değişebildiğnden Linux çekirdeğinde bunlar için ortak arayüze sahip inline fonksiyonlar bulundurulmuştur. Bu fonksiyonlar şunlardır: #include unsigned char inb(int addr); unsigned short inw(int addr); unsigned int inl(int addr); void outb(unsigned char b, int addr); void outw(unsigned short b, int addr); void outl(unsigned int b, int addr); inb portlarından 1 byte, inw 2 byte, inl 4 byte okumak için kullanılmaktadır. Benzer biçimde haaberleşme portlarına outb 1 byte, outw 2 byte ve outl 4 byte göndermek için kullanılmaktadır. Bazı mimarilerde bir bellek adresinden başlayarak belli bir sayıda byte'ı belli bir porta gönderen ve belli bir prottan yapılan okumaları belli bir adresten itibaren belleğe yerleştiren özel makine makumtları vardır. Bu komutlara string komutları denilmektedir. (Intel'de string komutları yalnızca IO işlemleri ile ilgili değildir.) İşte bu komutlara sahip mimarilerde bu string komutlarıyla IN, OUT yapan çekirdek fonksiyonları da bulundurulmıştur: #include void insb(unsigned long addr, void *buffer, unsigned int count); void insw(unsigned long addr, void *buffer, unsigned int count); void insl(unsigned long addr, void *buffer, unsigned int count); void outsb(unsigned long addr, const void *buffer, unsigned int count); void outsw(unsigned long addr, const void *buffer, unsigned int count); void outsl(unsigned long addr, const void *buffer, unsigned int count); insb, insw ve insl sırasıyla 1 byte 2 byte ve 4 byte'lık sitring fonksiyonlarıdır. Bu fonksiyonlar birinci parametresiyle belirtilen port numarasından 1, 2 ya da 4 byte'lık bilgileri ikinci parametresinde belirtilen adresten itibaren belleğe yerleştirirler. Bu işlemi de count kere tekrar ederler. Yani bu fonksiyonlar porttan count defa okuma yapıp okunanları buffer ile belirtilen adresten itibaren belleğe yerleştirmektedir. outsb, outsw ve outsl fonksiyonları ise bu işlemin tam tersini yapmaktadır. Yani bellekte bir adresten başlayarak count tane byte'ı birinci parametresiyle belirtilen port'a yerleştirmektedir. Bazı sistemlerde aygıtlar yavaş kalabilmektedir. Yani bus çok hızlı aygıt yavaş ise o aygıt port'larına peşi sıra bilgiler gönderilip alınırken sorunlar oluşabilmektedir. Bunun için bilgiyi porta gönderdikten ya da bilgiyi port'tan aldıktan sonra kısa bir süre bekleme yapmak gerekebilir. İşte bu nedenle yukarıdaki fonksiyonların bekleme yapan p'li (pause) versiyonları da bulundurulmuştur. #include unsigned char inb_p(int addr); unsigned short inw_p(int addr); unsigned int inl_p(int addr); void outb_p(unsigned char b, int addr); void outw_p(unsigned short b, int addr); void outl_p(unsigned int b, int addr); Linux'ta user moddan haberleşme portlarına IN ve OUT yapmak için basit bir aygıt sürücüsü de bulundurulmuştur. Bu aygıt sürücüsüne "/dev/port" aygıt dosyasıyla erişilebilir. Tabii user mod programların bu biçimde her defasında kernel moda geçerek In/OUT yapması verimsiz bir yöntemdir. Ancak yine de basit uygulamalar için faydalı kullanımlar söz konusu olabilmektedir. Bu aygıt sürücü dosya gibi açıkdıktan sonra sanki her dosya offset'i bir haberleşme portuymuş gibi işlem görmektedir. Yine okuma yazma sırasında dosya göstericisi ilerletilmekte dolayısıyla başka porta konumlandırılmaktadır. Aşağıdaki örnekte 0x60 numaralı porttan "/dev/port" aygıt sürücüsü yoluyla 1 byte okunmaktadır. * Örnek 1, /* app.c*/ #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int result; unsigned char ch; if ((fd = open("/dev/port", O_RDWR)) == -1) exit_sys("open"); if (lseek(fd, 0x60, SEEK_SET) == -1) exit_sys("lseek"); if ((result = read(fd, &ch, 1)) == -1) exit_sys("read"); printf("%02X\n", ch); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bir haberleşme portu ile çalışmadan önce o portun boşta olup olmadığını belirlemek gerekebilir. Çünkü başka aygıtların kullandığı port'lara erişmek sorunlara yol açabilmektedir. Tabii eğer biz ilgili port'un kullanılmasının bir soruna yol açmayacağından emin isek başkalarının kullandığı kullandığı port'ları doğrudan kullanabiliriz. Çekirdek bu bakımdan bir kontrol yapmamaktadır. Kullanmadan önce bir portun başkaları tarafından kullanılıp kullanılmadığının sorgulanması için request_region isimli çekirdek fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include struct resource *request_region(unsigned long first, unsigned long n, const char *name); Fonksiyonun birinci parametresi kullanılmak istenen port numarasının başlangıç numarasını, ikinci parametresi ilgili port numarasından itibaren ardışıl kaç port numarasının kullanılacağını, üçüncü parametresi ise "/proc/ioports" dosyasında görüntülenecek ismi belirtmektedir. Fonksiyon başarı durumunda portları betimleyen resource isimli yapının başlangıç adresine başarısızlık durumunda NULL adrese geri dönmektedir. request_region fonksiyonu ile tahsis edilen port umaraları release_region foksiyonu ile serbest bırakılmalıdır: #include void release_region(unsigned long start, unsigned long n); Yukarıda da belirttiğimiz gibi portların kullanılması için bu biçimde tahsisat yapma zorunluluğu yoktur. Ancak programcı programlanabilir IO portları söz konusu olduğunda ilgili port numaralarını başkalarının kullanmadığından emin olmak için bu yöntemi izlemelidir. PC'lerdeki klavye denetleyicisinin (klavye içerisindeki değil PC tarafındaki denetleyicinin (orijinali Intel 8042)) 60H ve 64H numaralı iki port'u vardır. 60H portu hem okunabilir hem de yazılabilir durumdadır. 60H portu 1 byte olarak okunduğunda son basılan ya da çekilen tuşun klavye scan kodu elde edilmektedir. Yukarıda dabelirttiğimiz gibi klavye terminolojisinde uşa basılırken oluşturulan scan koduna "make code", parmak tuştan çekildiğinde oluşturulan scan koduna ise "break code" denilmektedir. Klavye içerisindeki işlemcinin (keyboard encoder) break code olarak önce bir F0 byte sonra da make code byte'ını gönderdiğini belirtmiştik. İşte PC içerisindeki klavye denetleyicisi bu break kodu aldığında bunu iki byte olarak değil yüksek anlamlı biti 1 olan byte olarak saklamaktadır. Böylece biz 60H port'unu okuduğumuzda onun yüksek anlamlı bitine bakarak okuduğumuz scan kodunun make code mu yoksa break code mu olduğunu anlayabiliriz. Klavye denetleyicisinin 60H portuna gönderilen 1 byte değere "keyboard encoder command" denilmektedir. Bu 1 byte'lık komut klavye denetleyicisi tarafından klavye içerisindeki işlemciye gönderilir. Ancak bu 1 byte'tan sonra bazı komutlar parametre almaktadır. Parametreler de komutton sonra 1 byte olarak aynı port yoluyla iletilmektedir. Aşağıdaki örnekte klavyeden tuşlara basıldığında basılan ve çekilen tuşların make ve break code'ları yazdırılmaktadır. * Örnek 1, /* irq-driver.c */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, }; static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static unsigned char g_keymap[128] = { [30] = 'A', [31] = 'S', [32] = 'D', [33] = 'F', }; static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "Cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { unsigned char code; char *code_type; code = inb(0x60); code_type = code & 0x80 ? "Break code: " : "Make code: "; if (g_keymap[code & 0x7F]) printk(KERN_INFO "%s %c (%02X)\n", code_type, g_keymap[code & 0x7F], code); else printk(KERN_INFO "%s %02X\n", code_type, code); return IRQ_HANDLED; } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); Kesme kodları bazen bilgiyi bir kaynaktan alıp (örneğin network kartından, seri porttan, klavye denetleyicisinden) onu bir yere (genellikle bir kuyruk sistemi) yerleştirip, uyuyan thread'leri uyandırmaktır. Örneğin bir thread'in klavyeden bir tuşa basılana kadar bekleyeceğini düşünelim. Bu durumda thread işletim sistemi tarafından bir bekleme kuyruğuna alınır. Klavyeden bir tuşa basıldığında oluşan IRQ içerisinde bu bekleme kuyruğunda bekleyen thread'ler uyandırılır. Aşağıda örnekte aygıt sürücüye aygıt sürücünün bir IOCTL komutunda thread bloke edilmiştir. Sonra klavyeden bir tuşa basıldığında thread uykudan uyandırılıp basılmış olan tuşuna scan kodu IOCTL kodu tarafından thread'e verilmiştir. * Örnek 1, /* irq-driver.h */ #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif /* irq-driver.c */ #include #include #include #include #include #include #include #include "irq-driver.h" MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, .unlocked_ioctl = keyboard_ioctl, }; static DECLARE_WAIT_QUEUE_HEAD(g_wq); static int g_key; static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "Cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { int key; if (g_key != 0) return IRQ_NONE; key = inb(0x60); if (key & 0x80) return IRQ_NONE; g_key = key; wake_up_all(&g_wq); return IRQ_HANDLED; } static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_GETKEY: g_key = 0; if (wait_event_interruptible(g_wq, g_key != 0)) return -ERESTARTSYS; if (copy_to_user((void *)arg, &g_key, sizeof(int)) != 0) return -EFAULT; return g_key; default: return -ENOTTY; } } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include #include "irq-driver.h" void exit_sys(const char *msg); int main(void) { int fd; int key; if ((fd = open("irq-driver", O_RDONLY)) == -1) exit_sys("open"); if (ioctl(fd, IOC_GETKEY, &key) == -1) exit_sys("ioctl"); printf("Scan code: %d (%02x)\n", key, key); close(fd); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aşağıdaki örnekte IOC_SETLIGHTS ioctl komutu ile 8042 klavye denetleyicine komut gönderme yoluyla klavye ışıkları yakılıp söndürülmektedir. Klavyede üç ışıklı tuş vardır: Caps-Lock, Num-Lock ve Scroll_Lock. Bu ışıkları yakıp söndürebilmek için önce 60h portuna 0xED komutu gönderilir. Sonra yine 60h portuna ışıkların durumunu belirten 1 byte gönderilir. Bu byte'ın düşük anlamlı 3 biti sırasıyla Scroll-Lock, Num-Lock ve Caps-Lock tuşlarının ışıklarını belirtmektedir: 7 6 5 4 3 CL NL SL x x x x x x x x 60h portuna komut göndermeden önce 64h portundan elde edilen değerin 2 numaralı bitinin 0 olması gerekmektedir. Ayrıntılı bilgi için http://www.brokenthorn.com/Resources/OSDev19.html sayfasını inceleyebilirsiniz. Aşağıdaki aygıt sürücüde IOC_SETLIGHTS IOCTK komutunda klavye ışıklarının yakılıp söndürülmesi sağlanmıştır. Burada "app.c" isimli user mode program bir komut satırı argümanı almış ve o komut satırı argümanınındaki sayıyı yukarıda alattığımız gibi klavye denetleyicisine göndermiştir. Artık pek çok klavyede Scroll Lock ve Num Lock tuşlarının ışıkları bulunmamaktadır. Programın Caps Lock ışığını yakmasını istiyorsanız 4 argümanıyla (CL bitinin 2 numaralı bit olduğuna dikkat ediniz) söndürmek istiyorsanız 0 argümanıyla çalıştırabilirsiniz. Örneğin: $ ./app 4 $ ./app 0 * Örnek 1, /* irq-driver.h */ #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif /* irq-driver.c */ #include #include #include #include #include #include #include #include "irq-driver.h" MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Character Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static irqreturn_t keyboard_irq_handler(int irq, void *dev_id); static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg); static dev_t g_dev; static struct cdev *g_cdev; static struct file_operations g_file_ops = { .owner = THIS_MODULE, .unlocked_ioctl = keyboard_ioctl, }; #ifndef IRQDRIVER_H_ #define IRQDRIVER_H_ #include #define KEYBOARD_MAGIC 'k' #define IOC_GETKEY _IOR(KEYBOARD_MAGIC, 0, int) #endif static DECLARE_WAIT_QUEUE_HEAD(g_wq); static int g_key; static int __init generic_init(void) { int result; if ((result = alloc_chrdev_region(&g_dev, 0, 1, "irq-driver")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } if ((g_cdev = cdev_alloc()) == NULL) { printk(KERN_INFO "Cannot allocate cdev!...\n"); return -ENOMEM; } g_cdev->owner = THIS_MODULE; g_cdev->ops = &g_file_ops; if ((result = cdev_add(g_cdev, g_dev, 1)) != 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "Cannot add character device driver!...\n"); return result; } if ((result = request_irq(1, keyboard_irq_handler, IRQF_SHARED, "irq-driver", &g_cdev)) != 0) { cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "interrupt couldn't registered!...\n"); return result; } printk(KERN_INFO "irq-driver init...\n"); return 0; } static irqreturn_t keyboard_irq_handler(int irq, void *dev_id) { int key; if (g_key != 0) return IRQ_NONE; key = inb(0x60); if (key & 0x80) return IRQ_NONE; g_key = key; wake_up_all(&g_wq); return IRQ_HANDLED; } static long keyboard_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOC_GETKEY: g_key = 0; if (wait_event_interruptible(g_wq, g_key != 0)) return -ERESTARTSYS; if (copy_to_user((void *)arg, &g_key, sizeof(int)) != 0) return -EFAULT; return g_key; case IOC_SETLIGHTS: while ((inb(0x64) & 2) != 0) ; outb(0xED, 0x60); while ((inb(0x64) & 2) != 0) ; outb(arg, 0x60); break; default: return -ENOTTY; } return 0; } static void __exit generic_exit(void) { free_irq(1, &g_cdev); cdev_del(g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "irq-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* load (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unload (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* app.c */ #include #include #include #include #include #include "irq-driver.h" #define CAPS_LOCK 0x04 #define NUM_LOCK 0x02 #define SCROLL_LOCK 0x04 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; int key; int keycode; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } keycode = atoi(argv[1]); if ((fd = open("irq-driver", O_RDONLY)) == -1) exit_sys("open"); if (ioctl(fd, IOC_SETLIGHTS, keycode) == -1) exit_sys("ioctl"); close(fd); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Derleyiciler ve işlemciler tarafından yapılan önemli bir optimizasyon temasına "komutların yer değiştirilmesi (instruction reordering)" denilmektedir. Bu optimizasyon derleyici tarafından da bizzat işlemcinin kendisi tarafından da yapılabilmektedir. Burada biribirlerini normal bir durumda etkilemeyecek iki ya da daha fazla ayrı makine komutunun yerleri daha hızlı çalışma sağlamak için değiştirilmektedir. Bu tür yer değiştirmeler normal user mod programlarda hiçbir davranış değişiklğine yol açmazlar. Ancak işletim sistemi ve aygıt sürücü kodlarında ve özellikle IO portlarına erişim söz konusu olduğunda bu optimizasyon olumsuz yan etkilere yol açabilmektedir. Örneğin birbirleriyle alakasız iki adrese yazma yapılması durumunda yazma komutlarının yer değiştirmesi işlemcinin bu işleri daha hızlı yapabilmesine yol açabilmektedir. Fakat IO portları ve memory mapped IO söz konusu olduğunda bu sıralama değişikliği istenmeyen olumsuz sonuçlar doğurabilmektedir. İşte bu yer değiştirmeyi ortadan kaldırmak için "bariyer (barrier)" koyma yöntemi uygulanmaktadır. Derleyici ve işlemci bariyerin yukarısıyla aşağısını yer değiştirmemektedir. Bariyer fonksiyonları şunlardır: #include void rmb(void); void wmb(void); void mb(oid); rmb fonksiyonun aşağısındaki kodlar yukarısındaki okuma işlemleri yapıldıktan sonra yapılırlar. wmb fonksiyonun ise yukarısındaki yazma işlemleri yapıldıktan sonra aşağıdaki işlemler yapılırlar. mb fonksiyonu ise hem okuma hem yazma için yukarıdaki ve aşağıaki kodları birbirlerinden ayırmaktadır. Örneğin PORT1 portuna yazma yapıldıktan sonra PORT2 portuna yazma yapılacak olsun. Şöyle bir bariyer kullanmalıyız: outb(cmd1, port1); wmb(); outb(cmd2, port2); Memory Mapped IO işlemi pek çok mimaride normal göstericilerle yapılabilmektedir. Yani aslında bu mimarilerde memory mapped IO için özel kernel fonksiyonlarının kullanılmasına gerek yoktur. Ancak bazı mimarilerde memory mapped io işlemi için özel bazı işlemlerin de yapılması gerekebilmektedir. Bu nedenle bu işlemlerin taşınabilir yapılabilmesi için özel kernel fonksiyonlarının kullanılması tavsiye edilir. Tıpkı normal IO işlemlerinde olduğu gibi memory mapped IO için de iki farklı aygıt aynı adres bölgesini kullanmasın diye bir registration işlemi söz konusudur. Bu işlemler request_mem_region ve release_mem_region fonksiyonlarıyla yapılmaktadır: #include struct resource *request_mem_region(unsigned long start, unsigned long len, const char *name); Fonksiyonun birinci parametresi başlangıç bellek adresini, ikinci parametresi alanın uzunluğunu belitmektedir. Üçüncü parametre ise "/proc/iomem" dosyasında görüntülenecek isimdir. Fonksiyon başarı durumunda resource isimli bir yapı nesnesinin adresine, başarısızlık durumunda NULL adrese geri dönmektedir. Daha önce rgister ettirilmiş olan bellek bölgesini serbest bırakmak için (yani register ettirilmemiş hale getirmek için) release_mem_region fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include void release_mem_region(unsigned long start, unsigned long len); Fonksiyonun birinci parametresi başlangıç bellek adresini, ikinci parametresi ise uzunluğu belirtmektedir. Aşağıdaki fonksiyonlar addr ile belirtilen bellek adresinden 1 byte, 2 byte ve 4 byte okurlar. #include unsigned int ioread8(void *addr); unsigned int ioread16(void *addr); unsigned int ioread32(void *addr); Aşağıdaki fonksiyonlar ise addr ile belirtilen bellek adresine 1 byte, 2 byte ve 4 byte bilgi yazmaktadır: #include void iowrite8(u8 value, void *addr); void iowrite16(u16 value, void *addr); void iowrite32(u32 value, void *addr); Yukarıdaki fonksiyonların rep'li versiyonları da vardır: #include void ioread8_rep(void *addr, void *buf, unsigned long count); void ioread16_rep(void *addr, void *buf, unsigned long count); void ioread32_rep(void *addr, void *buf, unsigned long count); void iowrite8_rep(void *addr, const void *buf, unsigned long count); void iowrite16_rep(void *addr, const void *buf, unsigned long count); void iowrite32_rep(void *addr, const void *buf, unsigned long count); Bu fonksiyonlar memmory mapped IO adresinden belli bir adrese belli miktarda (count parametresi) byte, word ya da dword transfer etmektedir. Tıpkı memcpy fonksiyonunda olduğu gibi memory mapped IO adresi ile bellek arasında blok kopyalaması yapan iki fonksiyon bulunmaktadır: #include void memcpy_fromio(void *dest, const void *source, unsigned int count); void memcpy_toio(void *dest, const void *source, unsigned int count); Belli bir memory mapped IP adresine belli bir byte'ı n defa dolduran fonksiyon da şöyledir: #include void memset_io(void *dest, u8 value, unsigned int count); Bu fonksiyonu memset fonksiyonuna benzetebilirsiniz. Aygıtın kullandığı bellek adresi genellikle fiziksel adrestir. Örneğin aygıt 0xFFFF8000 gibi bir adresi kullanıyorsa bu genellikle fiziksel anlamına gelir. Halbuki aygıt sürücünün bu fiziksel adrese erişebilmesi için bu dönüşümü yapacak sayfa tablosu girişlerin olması gerekir. İşte bu girişleri elde edebilmek için şu fonksiyonlar bulundurulmuştur: #include void *ioremap(unsigned long physical_address, unsigned long size); Bu fonksiyon fiziksel adrese erişebilmek için gereken sayfa tablosu adresini bize verir. Gerçi genellikle fiziksel RAM daha önceden de belirtildiği gibi Linux'ta sanal belleğin PAGE_OFFSET ile belirtilen kısmından başlanarak map edilmiştir. Bu işlemi geri almak için iounmap fonksiyonu kullanılmaktadır: #include void iounmap(void *addr); /*================================================================================================================================*/ (161_09_08_2024) & (162_11_08_2024) & (163_16_08_2024) & (164_18_08_2024) > "kernel modules" : >> Blok Aygıt Sürücüler: Anımsanacağı üzere aygıt sürücülerinin "karakter aygıt sürücüleri (character device driver)" ve "blok aygıt sürücüleri (block device driver)" olmak üzere ikiye ayrıldığından ve Karakter Aygıt Sürücülerinin ne olduğundan bahsetmiştik. Şimdi de Blok Aygıt Sürücülerine değineceğiz. Blok aygıt sürücüleri (block device drivers) disk benzeri birimlerden bloklu okuma ve yazma yapabilmek için kullanılan özel aygıt sürücülerdir. Daha önceden de belirttiğimiz gibi disk benzeri birimlerden bir hamlede okunabilecek ya da yazılabilecek bilgi miktarına "sektör" denilmektedir. İşte blok aygıt sürücüleri transferleri byte byte değil blok blok (sektör sektör) yapmaktadır. Örneğin bir diskten 1 byte okuma diye bir şey yoktur. Ya da bir diske 1 yazma diye bir şey yoktur. Diskteki 1 byte değiştirilecekse önce onun bulunduğu sektör RAM' okunur, değişiklik RAM üzerinde yapılır. Sonra o sektör yeniden diske yazılır. Tipik transfer bu adımlardan geçilerek gerçekleştirilmektedir. Bir sektör değişebilse de hemen her zaman 512 byte'tır. Bir Linux sistemini kurduğumuzda "/dev" dizininin altında disklerle işlem yapan aygıt sürücülere yönelik aygıt dosyaşarı da oluşturulmuş durumdadır. Blok aygıt sürücülerine ilişkin aygıt dosyaları "ls -l" komutunda dosya türü olarak 'b' biçiminde görüntülenmektedir. Örneğin: $ ls -l /dev ... brw-rw---- 1 root disk 8, 0 Ağu 7 13:57 sda brw-rw---- 1 root disk 8, 1 Ağu 7 13:57 sda1 brw-rw---- 1 root disk 8, 2 Ağu 7 13:57 sda2 brw-rw---- 1 root disk 8, 3 Ağu 7 13:57 sda3 crw-rw----+ 1 root cdrom 21, 0 Ağu 7 13:57 sg0 crw-rw---- 1 root disk 21, 1 Ağu 7 13:57 sg1 ... Burada sda sygıt dosyası diske bir bütün olarak erişmek için sda1, sda2, sda3 aygıt dosyaları ise diskteki disk bölümlerine (partition) erişmek için kullanılmaktadır. Bu aygıt dosyalarının majör numaralarının aynı olduğuna ancak minör numaralarının farklı olduğuna dikkat ediniz. Biz bir flash belleği USB soketine taktığımızda burada flash belleğe erişmek için gerekli olan aygıt dosyaları otomatik biçimde oluşturulacaktır. İşletim sistemleri bloklu çalışan aygıtlarda erişimi hızlandırmak için ismine "IO çizelgelemesi (IO Scheduling)" denilen bir yöntem uygulamaktadır. Çeşitli prosesler diskten çeşitli sektörleri okumak istediğinde ya da yazmak istediğinde bunlar işletim sistemi tarafından birleşitirlerek disk erişimleri azaltılmaktadır. Yani bu tür transferlerde transfer talep edildiği anda değil biraz bekletilerek (çık kısa bir zaman) gerçekleştirilebilmektedir. Bu tür durumlarda işletim sistemleri ilgi thread'i bloke ederek transfer sonlanana kadar wait kuyruklarında bekletmektedir. Disk sistemi bilgisayar sistemlerinin en yavaş kısmını oluşturmaktadır. SSD diskler bile yazma bakımından RAM'e göre binlerce kat yavaştır. İşte işletim sistemleri aslında ayrık olan birtakım okuma yazma işlemlerini diskte mümkün olduğunca ardışıl hale getirerek disk erişiminden kaynaklanan zaman kaybını minimize etmeye çalışır. Disk sistemlerinde ayrık işlemler yerine peşi sıra blokların tek hamlede okunup yazılması ciddi hız kazancı sağlayabilmektedir. Farklı proseslerin sektör okuma istekleri aslında bazen birbirine yakın bölgelerde gerçekleşit. İşte onların yniden sıralanması gibi faaliyetler IO çizelgeleycisinin önemli görevlerindendir. Blok aygıt sürücüleri bazı bakımlardan karakter aygıt aygıt sürücülerine benzese de bu IO çizelgelemesi yüzünden tasarımsal farklılıklara sahiptir. Karakter aygıt sürücülerinde her read ve write işlemi için bir IO çizelgelemesi yapılmadan aygıt sürücünün fonksiyonları çağrılmaktadır. Çünkü karakter aygıt sürücülerinde neticede disk gibi zaman alıcı birimler ile uğraşılmamaktadır. Ancak blok aygıt sürücülerinde transfer isteği çizelgelenerek bazen gecikmelerle yerine getirilmektedir. Bir blok aygıt sürücüsü oluşturmak için ilk yapılacak işlem tıpkı karakter aygıt sürücülerinde olduğu gibi blok aygıt sürücüsünün bir isim altında aygıt numarası belirtilerek register ettirilmesidir. Bu işlem register_blkdev fonkiyonuyla yapılmaktadır: #include int register_blkdev(unsigned int major, const char *name); Fonksiyonun birinci parametresi aygıtın majör numarasını belirtir. Eğer majör numara olarak 0 geçilirse fonksiyon boş bir majör numarayı kendisi tahsis etmektedir. Fonksiyonun ikinci parametresi ise "/proc/devices" dosyasında görüntülenecek olan ismi belirtmektedir. Fonksiyon başarı durumunda majör numaraya, başarısızlık durumunda negatif error koduna geri dönmektedir. İkinci parametre aygıt sürücünün /proc/devices dosyasında görüntülenecek ismini belirtmektedir. Örneğin: if ((g_major = register_blkdev(0, "generic-blkdev")) < 0) { printk(KERN_INFO "Cannot alloc char driver!...\n"); return result; } Modül boşaltılırken bu işlemin geri alınması için unregister_blkdev fonksiyonu kullanılmaktadır: #include void unregister_blkdev(unsigned int major, const char *name); Fonksiyonun parametrik yapısı register_blkdev fonksiyonuyla tamamen aynıdır. Örneğin: unregister_blkdev(g_major, "generic-blkdev"); Bizim daha önce kullandığımız "load" script'i karakter aygıt aygıt dosyası yaratıyordu. Halbuki bizim artık blok aygıt dosyaları yaratmamız gerekir. Bunun için "load" ve "unload" script'lerini "loadblk" ve "unloadblk" ismiyle yeniden yazacağız. Tabii aslında "unload" script'inde değiştirilecek bir şey yoktur. Ancak isimsel uyumluluk bakımından biz her iki dosyayı da yeniden yeni isimlerle oluşturacağız: /* loadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* generic-blkdev-driver.c */ #include #include #include #include MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Block Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static int g_major; static int __init generic_init(void) { int result; if ((g_major = register_blkdev(0, "generic-blkdev")) < 0) { printk(KERN_INFO "cannot alloc block driver!..\n"); return result; } printk(KERN_INFO "generic-block-driver init...\n"); return 0; } static void __exit generic_exit(void) { unregister_blkdev(g_major, "generic-blkdev"); printk(KERN_INFO "generic-block-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += $(file).o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Blok aygıt sürücüleri için en önemli nesne gendisk isimli nesnesidir. Blok aygıt sürücü çekirdekte bu nesne ile temsil edilmektedir. gendisk nesnesi alloc_disk isimli fonksiyonla (aslında bir makro olarak yazılmıştır) tahsis edilmektedir. Ancak 5'li çekirdeklerle birlikte fonksiyonun ismi blk_alloc_disk biçiminde değiştirilmiştir. Ayrıca aşağıdaki fonksiyonların bir bölümünün bulunduğu dosyası da çekirdeğin 5.18 versiyonunda kaldırılmış buradaki fonksiyonların prototipleri dosyasına taşınmıştır. #include struct gendisk *alloc_disk(int minors); /* eski çekirdek versiyonları bu fonksiyonu kullanıyor */ struct gendisk *blk_alloc_disk(int minors); /* yeni çekirdek versiyonlar bu fonksiyonu kullanıyor */ Fonksiyonlar parametre olarak aygıt sürücünün destekleyeceği minör numara sayısını almaktadır. Geri dönüş değeri de diski temsil eden gendisk isimli yapı nesnesinin adresidir. (Birden fazla minör numaranın söz konusu olduğu durumda geri döndürülen adres aslında bir dizi adresidir.) Fonksiyonlar başarsızlık durumunda NULL adrese geri dönmektedir. Başarısızlık durumunda bu fonksiyonları çağıran aygıt sürücü fonksiyonlarını -ENOMEM değeri ile geri döndürebilirsiniz. Örneğin: static struct gendisk *g_gdisk; if ((g_gdisk = blk_alloc_disk(1)) == NULL) { ... return -NOMEM; } alloc_disk ile elde edilen gendisk nesnesinin içinin doldurulması gerekmektedir. Bu yapının doldurulması gereken elemanları şunlardır: -> Yapının major isimli elemanına aygıt sürücünün majör numarası yerleştirilmelidir. Örneğin: g_gdisk->major = g_major; -> Yapının first_minor elemanına aygıt sürücünün ilk minör numarası yerleştirilmelidir (Tipik olarak 0). Örneğin: g_gdisk->first_minor = 0; -> Yapının flags elemanına duruma göre bazı bayraklar girilebilmektedir. Öreğin ramdisk için bu bayrak GENHD_FL_NO_PART_SCAN biçiminde girilebilir. Örneğin: g_gdisk->flags = GENHD_FL_NO_PART_SCAN; -> Yapının fops elemanına aygıt sürücü açıldığında, kapatıldığında, ioctl işlemi sırasında vs. çağrılacak fonksiyonların bulunduğuğu block_device_operations isimli yapının adresi atanmalıdır. Bu yapı karakter aygıt sürücülerindeki file_operations yapısına benzetilebilir. Yapıının iki önemli elemanı open ve release elemanlarıdır. Burada belirtilen fonksiyonlar aygıt sürücü açıldığında ve her kapatıldığında çağrılmaktadır. Örneğin: static struct block_device_operations g_bops = { /* ... */ }; g_gdisk->fops = &g_bops; -> Yapının queue elemanına programcı tarafından yaratılacak olan request_queue nesnesinin adresi atanmalıdır. request_queue aygıt sürücüden read/write işlemi yapıldığında çekirdeğin IO çizelgeleyici alt sistemi tarafından optimize edilen işlemlerin yerleştirileceği kuyruk sistemidir. Programcı ileride görüleceği üzere bu kuyruk sisteminden istekleri alarak yerine getirir. Maalesef bu kuyruğun yaratılması ve işleme sokulması için gerekli kernel fonksiyonları kernel'ın çeşitli versiyonlarında değiştirilmiştir. Eskiden 4'lü çekirdeklerde request_queue nesnesi oluşturmak için blk_init_queue isimli bir fonksiyon kullnılıyordu. Sonra 5'li çekirdeklerle birlikte request_queue işlemleri üzerinde değişiklikler yapıldı. Biz kursumuzda 5'li çekirdekler kullanıyoruz. Ancak önceki çekirdeklerdeklerde kullanılan fonksiyonlar üzerinde de durmak istiyoruz. Burada önce eski çekirdeklerdeki request queue işlemlerini ele alıp sonra ayrı paragrafta yeni çekirdeklere ilişkin değişiklikler üzerinde duracağız. Eski çekirdeklerde request_queue nesnesi oluşturmak için blk_init_queue isimli bir fonksiyon kullanılıyordu. Fonksiyonun prototipi şöyledi: #include typedef void (request_fn_proc) (struct request_queue *q); struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); Fonksiyonun birinci parametresi kuyruktan gelen istekleri almakta kullanılan request_fn_proc türünden bir nesnenin adresini almaktadır. İkinci parametre kuyruğu korumak için kullanılan spinlock nesnesini belirtmektedir. Fonksiyonun birinci parametresine geri dönüş değeri void olan parametresi struct gendisk * türünden olan bir fonksiyonun adresi girilmelidir. Bu fonksiyon kuyruk işlemlerini yapmak için bulundurulur. blk_init_queue fonksiyonu başarı durumunda request_queue nesnesinin adresi ile, başarısızlık durumunda NULL adresle geri dönmektedir. Bu durumda çağıran fonksiyonu -ENOMEM değeri geri döndürebilirsiniz. Örneğin: static struct request_queue *g_rq; static spinlock_t g_sl; static void request_proc(struct request_queue *rq) { /* ... */ } ... if ((g_rq = blk_init_queue(request_proc, &g_sl)) == NULL) { ... retur -ENOMEM; } Biz burada elde ettiğimiz request_queue nesnesinin adresini gendisk yapısının queue elemanına atamalıyız. Örneğin: g_gdisk->queue = g_rq; blk_init_queue fonksiyonuyla yaratılan kuyruk nesnesinin kullanım bittikten sonra cleanup_queue fonksiyonuyla boşaltılması gerekmektedir: #include void blk_cleanup_queue(struct request_queue *rq); Fonksiyon parametre olarak request_queue nesneesinin adresini almaktadır. Yeni kuyruk fonksiyonları izleyen paragraflarda ele alınacaktır. -> gendik yapısının içerisine diskin (blok aygıt sürücüsünün temsil ettiği medyanın) kapasitesi set edilmelidir. Bu işlem set_capacity fonksiyonuyla yapılmaktadır. Fonksiyon prototipi şöyledir: #include void set_capacity(struct gendisk *disk, sector_t size); Fonksiyonun birinci parametresi gendisk yapısının adresini, ikinci parametresi aygıtın sektör uzunluğunu almaktadır. (Aslında bu fonksiyon gendisk yapısının ilgili elemanını set etmektedir.) Fonksşyonun ikinci parametresi aygıt sürücünün temsil ettiği aygıtın sektör uzunluğunu almaktadır. Bir sektör 512 byte'tır. Örneğin: set_capacity(g_gdisk, 1000); -> gendisk yapısının disk_name isimli elemanı char türden bir dizi belirtmektedir. Bu diziye disk isminin bulunduğu yazı kopyalanmalıdır. Disk ismi "/sys/block" dizininde görüntülenecek dosya ismini belirtmektedir. Örneğin: strcpy(g_gdisk->disk_name, "myblockdev"); -> Nihayet gendisk yapısının private_data elemanına programcı kendi yapı nesnesinin adresini yerleştirebilir. Örneğin daha önce karakter aygıt sürücülerinde yaptığımız gibi bu private_data elemanına gendisk nesnesinni içinde bulunduğu yapı nesnesinin adresini atayabiliriz. Aşağıdaki örnekte yukarıda anlatılan kısma kadar olan işlemleri içeren bir blok aygıt sürücü örneği verilmiştir. Örneğin: struct BLOCKDEV { spinlock_t sl; struct gendisk *gdisk; struct request_queue *rq; size_t capacity; }; static struct BLOCKDEV g_bdev; ... g_gdisk->private_data = &g_bdev; Tabii buradaki örnekte g_bdev zaten bir nesne oludğu için ona private_data yoluyla erişmeye gerek kalmamaktadır. Ancak aygıt sürücümüz birden fazla minör numarayı destekliyorsa her aygıtın ayrı bir BLOCKDEV yapısı olacağı için ilgili aygıta bu private_data elemanı yoluyla erişebiliriz. blk_alloc_disk fonksiyonu ile elde edilen gendisk nesnesi add_disk fonksiyonu ile sisteme eklenmelidir: #include void add_disk(struct gendisk *disk); /* eski çekirdek versiyonlarındaki prototip */ Bu fonksiyonun geri dönüş değeri 5'li çekirdeklerle birlikte int yapılmıştır: int add_disk(struct gendisk *disk); /* yeni çekirdek versiyonlarındaki prototip */ Fonksiyon başarı durumunda 0 değrine başarısızlık durumunda negatif errno değerine geri dönmektedir. Örneğin: if ((result = add_disk(g_gdisk)) != 0) { ... return result; } alloc_disk ve add_disk fonksiyonuyla tahsis edilen ve sisteme yerleştirilen gendisk nesnesi del_gendisk fonksiyonuyla serbest bırakılmaktadır: #include void del_gendisk(struct gendisk *gdisk); Örneğin: if ((g_gdisk = alloc_disk(1)) == NULL) { ... return -ENOMEM; } if ((result = add_disk(g_bdev->gdisk)) != 0) { ... return result; } ... del_gendisk(g_gdisk); Aşağıda şimdiye kadar gördüğümüz işlemleri yapan basit bir blok aygıt sürücüsü iskeleti verilmiştir. * Örnek 1, Burada iki ayrı program veriyoruz. Birinci program global değişkenler kullanılarak yazılmıştır. İkincisi ise bu global değişkenlerin BLOCKDEV isimli bir yapıya yerleştirilmiş biçimidir. Tabii yukarıda da belirttiğimiz gibi aşağıdaki aygıt sürücü kodlarında eski request_queue fonksiyonları kullanılmıştır. Dolayısıyla bu kodlar 5'li ve sonraki çekirdeklerde derlenmeyecektir. /* generic-blkdev.c */ #include #include #include #include #include #define KERNEL_SECTOR_SIZE 512 #define CAPACITY (KERNEL_SECTOR_SIZE * 1000) MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Block Device Driver"); MODULE_AUTHOR("Kaan Aslan"); static int g_major; static struct gendisk *g_gdisk; static struct block_device_operations g_bops = { //... }; static struct request_queue *g_rq; static struct request_queue *g_rq; static spinlock_t g_sl; typedef void (request_fn_proc) (struct request_queue *q); struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); static void request_proc(struct request_queue *rq); static int __init generic_init(void) { int result; if ((g_major = register_blkdev(0, "generic-blkdev")) < 0) { printk(KERN_INFO "cannot block driver!..\n"); return result; } if ((g_gdisk = blk_alloc_disk(1)) == NULL) { printk(KERN_ERR "Cannot alloc disk!..\n"); return -ENOMEM; } if ((result = add_disk(g_gdisk)) != 0) { del_gendisk(g_gdisk); printk(KERN_ERR "Cannot add disk!..\n"); return result; } g_gdisk->major = g_major; g_gdisk->first_minor = 0; g_gdisk->flags = GENHD_FL_NO_PART_SCAN; g_gdisk->fops = &g_bops; if ((g_rq = blk_init_queue(request_proc, &g_sl)) == NULL) { del_gendisk(g_gdisk); unregister_blkdev(g_major, "generic-blkdev"); return -ENOMEM; } g_gdisk->queue = g_rq; set_capacity(g_gdisk, CAPACITY); strcpy(g_gdisk->disk_name, "myblockdev"); printk(KERN_INFO "generic-block-driver init...\n"); return 0; } static void request_proc(struct request_queue *rq) { /* ... */ } static void __exit generic_exit(void) { cleanup_queue(g_rq); del_gendisk(g_gdisk); unregister_blkdev(g_major, "generic-blkdev"); printk(KERN_INFO "generic-block-driver exit...\n"); } module_init(generic_init); module_exit(generic_exit); /* generic-blkdev.c */ #include #include #include #include #include #define KERNEL_SECTOR_SIZE 512 #define CAPACITY (KERNEL_SECTOR_SIZE * 1000) MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("General Block Device Driver"); MODULE_AUTHOR("Kaan Aslan"); int g_major = 0; struct BLOCKDEV { spinlock_t sl; struct gendisk *gdisk; struct request_queue *rq; size_t capacity; }; static int generic_open(struct block_device *bdev, fmode_t mode); static void generic_release(struct gendisk *gdisk, fmode_t mode); static void request_proc(struct request_queue *rq); static struct block_device_operations g_devops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static struct BLOCKDEV *g_bdev; static int __init generic_init(void) { int result = 0; if ((g_major = register_blkdev(g_major, "generic-bdriver")) < 0) { printk(KERN_ERR "Cannot register block driver!...\n"); return g_major; } if ((g_bdev = kmalloc(sizeof(struct BLOCKDEV), GFP_KERNEL)) == NULL) { printk(KERN_ERR "Cannot allocate memory!...\n"); result = -ENOMEM; goto EXIT1; } memset(g_bdev, 0, sizeof(struct BLOCKDEV)); g_bdev->capacity = CAPACITY; spin_lock_init(&g_bdev->sl); if ((g_bdev->rq = blk_init_queue(request_proc, &g_bdev->sl)) == NULL) { printk(KERN_ERR "Canno allocate queue!...\n"); result = -ENOMEM; goto EXIT2; } g_bdev->rq->queuedata = g_bdev; if ((g_bdev->gdisk = alloc_disk(1)) == NULL) { result = -ENOMEM; goto EXIT3; } g_bdev->gdisk->major = g_major; g_bdev->gdisk->first_minor = 0; g_bdev->gdisk->flags = GENHD_FL_NO_PART_SCAN; g_bdev->gdisk->fops = &g_devops; g_bdev->gdisk->queue = g_bdev->rq; set_capacity(g_bdev->gdisk, g_bdev->capacity >> 9); g_bdev->gdisk->private_data = g_bdev; strcpy(g_bdev->gdisk->disk_name, "blockdev"); add_disk(g_bdev->gdisk); printk(KERN_INFO "Module initialized with major number %d...\n", g_major); return result; EXIT3: blk_cleanup_queue(g_bdev->rq); EXIT2: kfree(g_bdev); EXIT1: unregister_blkdev(g_major, "generic-bdriver"); return result; } static int generic_open(struct block_device *bdev, fmode_t mode) { printk(KERN_INFO "device opened...\n"); return 0; } static void generic_release(struct gendisk *gdisk, fmode_t mode) { printk(KERN_INFO "device closed...\n"); } static void request_proc(struct request_queue *rq) { /* ... */ } static void __exit generic_exit(void) { del_gendisk(g_bdev->gdisk); blk_cleanup_queue(g_bdev->rq); kfree(g_bdev); unregister_blkdev(g_major, "generic-bdriver"); printk(KERN_INFO "Goodbye...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module b $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız ) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module /* sample.c */ #include #include #include #include #include #include "keyboard-ioctl.h" void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("generic-bdriver", O_RDONLY)) == -1) exit_sys("open"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Blok aygıt sürücülerinde yukarıda belirtilen işlemlerden sonra artık transfer işleminin yapılması için bulundurulan fonksiyonun (örneğimizde request_proc) yazılması aşamasına geldik. User mode kodlar tarafından blok transferine yönelik bir istek oluştuğunda (örneğin blok aygıt sürücüsünden bir sektör okunmak istediğinde) çekirdeğin IO çizelgeleyicisi bunları çizelgeleyerek uygun bir zamanda transfer edilebilmesi için bizim belirttiğimiz ve yukarıdaki örnekte ismini request_proc olarak verdiğimiz fonksiyonu çağrımaktadır. Yani örneğimizdeki request_proc bizim tarafımızdan değil çekirdek tarafından callback fonksiyon olarak çağrılmaktadır. Bizim de bu fonksiyon içerisinde kuyruğa bırakılmış blok transfer isteklerini kuyruktan alarak gerçekleştirmemiz gerekir. Örneğin bir blok aygıt sürücüsünü user mode'da open fonksiyonuyla açıp içerisinden 10 byte'ı read fonksiyonuyla okumak isteyelim. Eğer bu aygıt sürücü karakter aygıt sürücüsü olsaydı çekirdek doğrudan aygıt sürücünün read fonksiyonunu çağıracaktı. Aygıt sürücü de istenen 10 byte'ı user mode'daki adrese transfer edecekti. Halbuki blok aygıt sürücüsü durumunda çekirdek aygıt sürücüden 10 byte transfer istemeyecektir. İlgili 10 byte'ın blunduğu bloğun (block ardışıl n sektördür) transferini isteyecektir. User mod programa o bloğun içerisindeki 10 byte'ı vermek kernel'ın görevidir. Blok aygıt sürücülerinden transferler byte düzeyinde değil blok düzeyinde yapılmaktadır. Yani block aygıt sürücülerinden transfer edilecek en küçük birim 1 sektör yani 512 byte'tır. Şüphesiz kernel 10 byte okuma isteğine konu olan yerin aygıttaki sektör numarasını hesap eder ve o sektörden itibaren blok transferi ister. Kernel aygıt sürücünün transfer edeceği blokları bir kuyruk sistemine yerleştirir. Bu kuyruk sistemi request_queue denilen bir yapı ile temsil edilmiştir. Bu kuyruğun içerisindeki kuyruk elemanları request isimli yapı nesnelerinden oluşmaktadır. (Yani request_queue aslında request yapılarının oluşturduğu bir kuyruk sistenidir.) Her request nesnesi kendi içerisinde bio isimli yapı nesnelerinden oluşmaktadır. request nesnesinin içerisindeki bio nesnleri bir bağlı liste biçiminde tutulmaktadır. Her bio yapısının sonunda ise değişken sayıda (yani n tane) bio_vec yapıları bulunmaktadır. İşte transfer işini yapacak fonksiyon transfere ilişkin bilgileri bu bio_vec yapısından elde etmektedir. request_queue: request ---> request ---> request ---> request ... request: bio ---> bio ---> bio ---> bio ---> bio ---> ... bio: bio_vec[N] Buradan da görüldüğü gibi request_queue nesneleri request nesnelerinden, request nesneleri bio nesnelerinden, bio nesneleri de bio_vec nesnelerinden oluşmaktadır. İşte transfer fonksiyonu kernel tarafından çağrıldığında "request_queue içerisindeki request nesnelerine elde edip, bu request nesnelerinin içerisindeki bio nesnelerini elde edip, bio nesneleri içerisindeki bio_vec dizisinde belirtilen transfer bilgilerine ilişkin transfeleri" yapması gerekir. Şüphesiz bu işlem ancak açık ya da örtük iç içe 3 döngü ile yapılabilir. Yani bir döngü request nesnelerini elde etmeli, onun içerisindeki bir döngü bio nesnelerini elde etmeli, onun içerisindeki bir döngü de bio_vec nesleerini elde etmelidir. request_queue içerisindeki request nesnelerinin elde edilmesi birkaç biçimde yapılabilmektedir. Yöntemlerden tipik olanı şöyledir: static void request_proc(struct request_queue *rq) { struct request *rqs; for (;;) { if ((rqs = blk_fetch_request(rq)) == NULL) break; if (blk_rq_is_passthrough(rqs)) { __blk_end_request_all(rqs, -EIO); continue; } ... __blk_end_request_all(rqs, 0); } } Burada blk_fetch_request fonksiyonu kuyruğun hemen başındaki request nesnesini alarak onu kuyruktan siler. Böylece döngü içerisinde tek tek request nesneleri elde dilmiştir. blk_rq_is_passthrough fonksiyonu dosya sistemi ile alakalı olmayan request nesnelerini geçmek için kullanılır. Bazı request nesneleri transferle ilgili değildir. Bunların geçilmesi gerekmektedir. Bir request nesnesi kuyruktan alındıktan sonra akibeti konusunda çekirdeğe bilgi verilmesi gerekmektedir. İşte bu işlem __blk_end_request_all fonksiyonuyla yapılmaktadır. Bu fonksiyonun ikinci parametresi -EIO girilirse bu durum bu işlemin yapılmadığını 0 girilirse bu da bu işlemin başarılı bir biçimde yapıldığını belirtmektedir. Pekiyi çekirdek ne zaman kuyruğa request nesnesi yerleştirmektedir? Şüphesiz çekirdeğin böyle bir nesneyi kuyruğua yerleştirmesi için aygıt üzerinde bir okuma ya da yazma olayının gerçekleşmiş olması gerekir. Bunun tipik yolu aygıt dosyasının open fonksiyonuyla açılıp read ya da write yapılmasıdır. Tabii blok aygıt sürücüsü bir dosya sistemi yani bir disk bölümü haline getirilmiş olabilir. Bu durumda formatlama gibi, mount etme gibi eylemlerde de gizli bir okuma yazma işlemleri söz konusu olmaktadır. Pekiyi çekirdek aygıt sürücü açılıp aşağıdaki gibi iki ayrı read işleminde iki ayrı request nesnesi mi oluşturmaktadır? if ((fd = open("generic-bdriver", O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); Aslında çekirdek burada iki okumanın aynı blok içerisinde kaldığını anladığı için aygıttan yalnızca tek blokluk okuma talep edecektir. Çünkü okunan iki kısım da aynı blok içerisindedir. (Çekirdeğin bu biçimde düzenleme yapan kısmına "IO çizelgeleyicisi (IO scheduler)" denilmektedir.) Eğer bu iki okuma aşağıdaki gibi yapılmış olsaydı bu durumda çekirdek iki farklı blok için iki farklı request nesnesi oluşturacaktı: if ((fd = open("generic-bdriver", O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); lseek(fd, 5000, 0); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); Pekiyi çekirdek aygıt sürücüden kaç byte'lık bir bloğun transferini istemektedir? Bunun bir sektör olması gerektiğini düşünebilirsiniz. Ancak çekirdek sektör küçük olduğu için aygıt sürücüden bir "blok" transfer istemektedir. Bir blok ardışıl n tane sektörden oluşmaktadır (örneğin bir blok 8 sektörden yani 4K'dan oluşabilir) ve bu durum çekirdek konfigürasyonuna bağlıdır. Ancak çekirdek transfer isteklerinde istenen bloğu her zaman sektör olarak ifade etmektedir. Başka bir deyişle çekirdek aygıt sürücüden "şu sektörden itibaren 4096 byte transfer et" gibi bir istekte bulunmaktadır. Şimdi biz request nesnesinin içerisinden bio nesnelerini, bio nesnelerinin içerisinden de bio_vec nesnelerini elde edelim. Pekiyi çekirdek neden transfer bilgilerini doğrudan request nesnelerinin içerisine kodlamak yerine böylesi iç nesneler oluşturmuştur? İşte aslında bir request nesnesi ardışıl sektör transferi ile ilgilidir. Ancak bu ardışıl sektörler read ya da write biçimde olabilir. İşte bu read ve write yönleri o request nesnesinin ayrı bio nesnelerinde kodlanmıştır. Read ve write işlemleri ise aslında birden fazla tampon ile (yani aktarılacak hedef adres ile) ilgili olabilir. Bu bilgiler de bio_vec içerisine kodlanmıştır. Dolayısıyla aslında programcı tüm bio'lar içerisindeki bio_vec'leri dolaşmalıdır. Bir süre önceye kadar request içerisindeki bio nesnelerinin dolaşılması normal kabul ediliyordu. Ancak belli bir çekirdekten sonra bu tavsiye edilmemeye başlanmıştır. Fakat yine de request içerisindeki bio nesneleri şöyle dolaşılabilir: struct bio *bio; ... bio = rqs->bio; for_each_bio(bio) { ... } for_each_bio makrosunun artık doğrudan yukarıdaki biçimde kullanılması önerilmemektedir. Artık bugünlerde bio nesnelerini dolaşmak yerine zaten bio nesnelerini ve onların içerisindeki bio-vec'leri dolaşan (yani içteki iki döngünün işlevini tek başına yapan) rq_for_each_segment isimli makronun kullanılması tavsiye edilmektedir. Bu makro şöyle yzılmıştır: #define rq_for_each_segment(bvl, _rq, _iter) \ __rq_for_each_bio(_iter.bio, _rq) \ bio_for_each_segment(bvl, _iter.bio, _iter.iter) Görüldüğü gibi aslında bu makro request nesnesi içerisindeki bio nesnelerini, bio nesneleri içerisindeki bio_vec nesnelerini dolaşmaktadır. rq_for_each_segment makrosunun birinci parametresi bio_vec tründen nesneyi almaktadır. Makronun ikinci parametresi request nesnesinin adresini almaktadır. Üçüncü parametre ise dolaşım sırasında kullanılacak bir iteratör nesnesidir. Bu nesne req_iterator isimli bir yapı türünden olmalıdır. Bu durumda transfer fonksiyonun yeni durumu aşağıdaki gibi olacaktır: static void request_proc(struct request_queue *rq) { struct request *rqs; struct bio_vec biov; struct req_iterator iterator; struct BLOCKDEV *bdev = (struct BLOCKDEV *) rq->queuedata; for (;;) { if ((rqs = blk_fetch_request(rq)) == NULL) break; if (blk_rq_is_passthrough(rqs)) { __blk_end_request_all(rqs, -EIO); continue; } rq_for_each_segment(biov, rqs, iterator) { /* biov kullanılarak transfer yapılır */ } __blk_end_request_all(rqs, 0); printk(KERN_INFO "new request object\n"); } } Gerçek transfer bilgileri bio_vec yapılarının içerisinde olduğuna göre acaba bu yapı nasıldır? İşte bu yapı şöyle bildirilmiştir: struct bio_vec { struct page *bv_page; unsigned int bv_len; unsigned int bv_offset; }; Yapının bv_page elemanı transferin yapılacağı adresi belirtmektedir. Çekirdek ismine eskiden "buffer cache" denilen daha sonra "page cache" denilen bir cache veri yapısı kullanmaktadır. İşletim sistemi read/write işlemlerinde önce bu "page cache" e başvurmakta eğer ilgili blok cache'te varsa buradan almaktadır. Eğer ilgili blok cache'te yoksa blok aygıt sürücüsünden transfer istemektedir. Blok aygıt sürücüleri karakter aygıt sürücülerinin yaptığı gibi transferleri user alanına kopyalamamaktadır. Blok aygıt sürücüleri transferi çekirdek tarafından tahsis edilen cache'teki sayfalara yapar. Çekirdek o sayfalardaki bilgiyi user alanına transfer eder. Ancak bu sayfa adresinin özellikle 32 bit Linux sistemlerinde sayfa tablosunda girişi olmayabilir. Programcının bu girişi oluşturması gerekmektedir. Bu girişi oluşturmak için kmap_atomic isimli fonksiyon ve girişi boşaltmak için ise kunmap_atomic isimli fonksiyon kullanılmaktadır. Yapının ikinci elemanı olan bv_len transfer edilecek byte sayısını barındırmaktadır. (Bu elemanda transfer edilecek sektör sayısı değil doğrudan byte sayısı bulunmaktadır.) Nihayet yapının üçüncü elemanı olan bv_offset birinci elemanında belirtilen adresten uzaklığı tutmaktadır. Yani aslında gerçek transfer adresi bv_page değildir. bv_page + bv_offset'tir. Bu yapıda en önemli bilgi olan transferin söz konusu olduğu sektör bilgisi yoktur. İşte aslında transfere konu olan sektör numarası bio yapısının içerisindedir. Her ne kadar biz yukarıdaki makroda bio yapısına erişemiyor olsak da buna dolaylı olarak iterator nesnesi yoluyla iterator.iter.bi_sect ifadesi ile erişilebilmektedir. Bu durumda rq_for_each_segment fonksiyonu içerisinde transfer tipik olarak şöyle yapılmaktadır: rq_for_each_segment(biov, rqs, iterator) { sector_t sector = iterator.iter.bi_sector; char *buf = (char *)kmap_atomic(biov.bv_page); size_t len = biov.bv_len; int direction = bio_data_dir(iterator.bio); transfer_block(bdev, sector, buf + biov.bv_offset, len, direction); kunmap_atomic(buf); } Burada transferin yapılacağı sektörün numarası bio_vec içerisinde bulunmadığından bu bilgi iterator yolu ile iterator.iter.bi_sector ifadesiyle alınmıştır. Transfer adresi olan buf adresi biov.bv_page adresinden biov.bv_offset kadar ileridedir. Yine transferin yönü doğrudan bio_vec yapısının içerisinde değildir. Bu yön bilgisinin bio_data_dir makrosuyla iterator yoluyla alındığına dikkat ediniz. (Aslında yön bilgisi bio yapısının içerisindedir. Bu makro onu buradan almaktadır.) Örneğimizde transferin transfer_block isimli fonksiyonla yapldığını görüyorsunuz. Bu fonksiyon bizim tarafımızdan yazılan gerçek transferin yapıldığı fonksiyondur. Aygıt sürücünün en önemli bilgilerinin bizim tarafımızdan oluşturulan BLOCKDEV isimli yapıda tutulduğunu anımsayınız. Her ne kadar bu yapı nesnesinin adresi global g_bdev göstericisinde tutuluyor olsa da örneğimizde requeat_queue nesnesinin queuedata elemanından çekilerek alınıp transfer_block fonksiyonuna verilmiştir. Bu durum yukarıdaki örnek için gereksiz olsa da birden fazla minör numarayla çalışıldığı durumda aygıt bilgilerinin yerinin belirlebilmesi için genel bir çözüm oluşturmak amacıyla transfer_block fonksiyonuna aktarılmıştır. Aygıt bilgilerinin request_queue yapısının quedata elemanına nesne tahsis edildikten sonra yerleştirildiğini anımsayınız. Buradaki bizim tarafımızdan yazılması gereken transfer_block fonksiyonun parametrik yapısı şöyle olabilir: static void transfer_block(struct BLOCKDEV *bdev, sector_t sector, char *buf, size_t len, int direction); direction değeri READ (0) ve WRITE (1) sembolik sabitleriyle define edilmiştir. Aşağıda RAMDISK 5'li çekirdekler öncesinde kullabileceğiniz block aygıt sürücüsünün tüm kodlarını görüyorsunuz. * Örnek 1, /* ramdisk-driver.c */ #include #include #include #include #include #define KERNEL_SECTOR_SIZE 512 #define CAPACITY (KERNEL_SECTOR_SIZE * 1000) MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Ramdisk Device Driver"); MODULE_AUTHOR("Kaan Aslan"); int g_major = 0; struct BLOCKDEV { spinlock_t sl; struct gendisk *gdisk; struct request_queue *rq; size_t capacity; void *data; }; static int generic_open(struct block_device *bdev, fmode_t mode); static void generic_release(struct gendisk *gdisk, fmode_t mode); static void request_proc(struct request_queue *rq); static void transfer_block(struct BLOCKDEV *bdev, sector_t sector, char *buf, size_t len, int direction); static struct block_device_operations g_devops = { .owner = THIS_MODULE, .open = generic_open, .release = generic_release }; static struct BLOCKDEV *g_bdev; static int __init generic_init(void) { int result = 0; if ((g_major = register_blkdev(g_major, "generic-bdriver")) < 0) { printk(KERN_ERR "Cannot register block driver!...\n"); return g_major; } if ((g_bdev = kmalloc(sizeof(struct BLOCKDEV), GFP_KERNEL)) == NULL) { printk(KERN_ERR "Cannot allocate memory!...\n"); result = -ENOMEM; goto EXIT1; } memset(g_bdev, 0, sizeof(struct BLOCKDEV)); g_bdev->capacity = CAPACITY; if ((g_bdev->data = vmalloc(CAPACITY)) == NULL) { printk(KERN_ERR "Cannot allocate memory!...\n"); result = -ENOMEM; goto EXIT2; } spin_lock_init(&g_bdev->sl); if ((g_bdev->rq = blk_init_queue(request_proc, &g_bdev->sl)) == NULL) { printk(KERN_ERR "Canno allocate queue!...\n"); result = -ENOMEM; goto EXIT3; } g_bdev->rq->queuedata = g_bdev; if ((g_bdev->gdisk = alloc_disk(1)) == NULL) { result = -ENOMEM; goto EXIT4; } g_bdev->gdisk->major = g_major; g_bdev->gdisk->first_minor = 0; g_bdev->gdisk->flags = GENHD_FL_NO_PART_SCAN; g_bdev->gdisk->fops = &g_devops; g_bdev->gdisk->queue = g_bdev->rq; set_capacity(g_bdev->gdisk, g_bdev->capacity >> 9); g_bdev->gdisk->private_data = g_bdev; strcpy(g_bdev->gdisk->disk_name, "blockdev"); add_disk(g_bdev->gdisk); printk(KERN_INFO "Module initialized with major number %d...\n", g_major); return result; EXIT4: blk_cleanup_queue(g_bdev->rq); EXIT3: vfree(g_bdev->data); EXIT2: kfree(g_bdev); EXIT1: unregister_blkdev(g_major, "generic-bdriver"); return result; } static int generic_open(struct block_device *bdev, fmode_t mode) { printk(KERN_INFO "device opened...\n"); return 0; } static void generic_release(struct gendisk *gdisk, fmode_t mode) { printk(KERN_INFO "device closed...\n"); } static void request_proc(struct request_queue *rq) { struct request *rqs; struct bio_vec biov; struct req_iterator iterator; struct BLOCKDEV *bdev = (struct BLOCKDEV *)rq->queuedata; for (;;) { if ((rqs = blk_fetch_request(rq)) == NULL) break; if (blk_rq_is_passthrough(rqs)) { __blk_end_request_all(rqs, -EIO); continue; } rq_for_each_segment(biov, rqs, iterator) { sector_t sector = iterator.iter.bi_sector; char *buf = (char *)kmap_atomic(biov.bv_page); size_t len = biov.bv_len; int direction = bio_data_dir(iterator.bio); transfer_block(bdev, sector, buf + biov.bv_offset, len, direction); kunmap_atomic(buf); } __blk_end_request_all(rqs, 0); } } static void transfer_block(struct BLOCKDEV *bdev, sector_t sector, char *buf, size_t len, int direction) { if (direction == READ) memcpy(buf, (char *)bdev->data + sector * KERNEL_SECTOR_SIZE, len); else memcpy((char *)bdev->data + sector * KERNEL_SECTOR_SIZE, buf, len); } static void __exit generic_exit(void) { del_gendisk(g_bdev->gdisk); blk_cleanup_queue(g_bdev->rq); vfree(g_bdev->data); kfree(g_bdev); unregister_blkdev(g_major, "generic-bdriver"); printk(KERN_INFO "Goodbye...\n"); } module_init(generic_init); module_exit(generic_exit); # Makefile obj-m += generic.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean /* loadblk (bu satırı dosyaya kopyalamayınız) */ #!/bin/bash module=$1 mode=666 /sbin/insmod ./$module.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module b $major 0 /* unloadblk (bu satırı dosyaya kopyalamayınız ) */ #!/bin/bash module=$1 /sbin/rmmod ./$module.ko || exit 1 rm -f $module Yukarıdaki örneklerdeki request kuyruk yapısı Linux'un 5'ten önceki çekirdeklerine özgüdür. Maalesef blok aygıt sürücülerinin içsel yapısı birkaç kere değiştirilmiştir. Burada yeni çekirdeklerdeki request kuyruk yapısı üzerinde duracağız. Yeni çekirdeklerde bu kuyruk yapısı şöyle kullanılmaktadır: -> Programcı blk_mq_tag_set isimli yapı türünden bir yapı nesnesi oluşturur. Bu yapı nesnesi blok yagıt sürücüsünün bilgilerinin tutulacağı yapının bir elemanı olarak alınabilir. Ya da global bir değişken olarak alınabilir. #include struct BLOCKDEV { spinlock_t sl; struct gendisk *gdisk; struct blk_mq_tag_set ts; struct request_queue *rq; size_t capacity; void *data; }; static struct BLOCKDEV *g_bdev; -> Bu blk_mq_tag_set yapısının içi aşağıdaki gibi doldurulur: g_bdev->ts.ops = &g_mqops; g_bdev->ts.nr_hw_queues = 1; g_bdev->ts.queue_depth = 128; g_bdev->ts.numa_node = NUMA_NO_NODE; g_bdev->ts.cmd_size = 0; g_bdev->ts.flags = BLK_MQ_F_SHOULD_MERGE; g_bdev->ts.driver_data = &g_bdev; Buradaki elemanlar çekirdeğin blok aygıt sürücü mimarisiyle ilgilidir. Biz burada tipik değerler kullandık. blk_mq_tag_set yapısının ops elemanı transfer isteklerini yerine getiren ana fonksiyonun adresini almaktadır. Bu fonksiyonun parametrik yapısı şöyle olmalıdır: static blk_status_t request_proc(struct blk_mq_hw_ctx *ctx, const struct blk_mq_queue_data *data); Yapının driver_data elemanına bu fonksiyon içerisinde eişilebilecek nesnenin adresi girilmelidir. Örneğimizde bu elemana g_bdev nesnesinin adresini girdik. Tabii aslında bu nesne global olduğu için zaten bu nesneye her yerden erişilebilmektedir. Ancak birden fazla minör numaranın desteklendiği durumda buraya ilgili minör numaraya ilişkin aygıt bilgisi girilebilir. -> İçi doldurulan blk_mq_tag_set nesnesi blk_mq_alloc_tag_set fonksiyonu ile tahsis edilip set edilmelidir. Fonksiyonun prototipi şöyledir: #include int blk_mq_alloc_tag_set(struct blk_mq_tag_set *set); Fonksiyon blk_mq_tag_set yapı nesnesinin adresini alır. Başarı durumunda 0 değerine başarısızlık durumunda negatif errno değerine geri döner. Yukarıdaki blok aygıt sürücüsü bir dosya sistemi ile formatlanarak sanki bir disk bölümüymüş gibi de kullanılabilir. Bunun için önce aygıt sürücünün idare ettiği alanın formatlanması gerekir. Formatlama işlemi mkfs isili utility programla yapılmaktadır: $ sudo mkfs -t ext2 generic-bdriver Burada -t seçeneği volümün hangi dosya sistemi ile formatlanacağını belirtmektedir. Formatlama aslında volümdeki bazı sektörlerde özel meta alanlarının yaratılması anlamına gelmektedir. Dolayısıyla mkfs komutu aslında ilgili aygıt sürücüyü açıp onun bazı sektörlerine bazı bilgileri yazmaktadır. Formatlama işleminden sonra artık blok aygıt sürücüsünün mount edilmesi gerekmektedir. mount işlemi bir dosya sisteminin dizin ağacının belli bir dizinine monte edilmesi anlamına gelmektedir. Dolayısıyla mount komutunda kullanıcı block aygıt sürücüsünü ve mount edilecek dizini girmektedir. Örneğin: $ sudo mount generic-bdriver /mnt/myblock Burada mount noktası (mount point) /ment dizinin altında myblock isimli dizindir. Bu dizinin kullanıcı tarafından önceden mkdir komutu ile yaratılması gerekir. Tabii mount noktalarının /mnt dizinin altında bulndurulması gibi zorunluluk yoktur. Mount noktasına ilişkin dizinin içinin boş olması da gerekmez. Fakat mount işleminden sonra artık o dizinin altı görünmez. Dosya sisteminin kök dizini o dizin olacak biçimde dosya sistemi ağaca monte edilmiş olur. mount komutu aslında mount isimli bir sistem fonksiyonu çağrılarak gerçekleştirilmektedir. Yani aslında bu işlem programlama yoluyla da yapılabilmektedir. Aygıt sürücümüzü mount ettikten sonra artık onu unmount etmeden rmmod komutuyla boşaltamayız. mount edilen dosya sistemi umount komutuyla eski haline getirilmektedir. Örneğin: $ sudo umount /mnt/myblock umount komutunun komutu argümanının mount noktasını belirten dizin olduğuna dikkat ediniz. Tabii aslında umount komutu da işlemini umount isimli sistem fonksiyonuyla yapmaktadır. > Hatırlatıcı Notlar: >> Linux sistemlerinde /dev dizinini altında "loopN" (N burada bir sayıdır) isimli "loopback aygıtı" denilen ilginç bir blok aygıt sürücüsü vardır. Bu loopback aygıtı aslında bir dosyayı disk gibi kullanmak için düşünülmüştür. Bu aygıt sürücüün tipik kullanımı şöyledir: -> Önce aygıtı temsil eden bir dosya yaratılır. Mademki bu dosya bir disk yerine geçecektir. O zaman bu dosya belli bir sektör uzunluğunda dd komutuyla oluşturulabilir: dd if=/dev/zero of=mydisk.dat bs=512 count=2880 Burada toplam 2880 * 512 byte uzunlukta içi 0'larla dolu bir dosya oluşturulmuştur. -> Şimdi losetup isimli programla bu dosyanın bir loopback aygıt sürücüsü ile ilişkilendirilmesi gerekmektedir. Örneğin: $ sudo losetup /dev/loop0 mydisk.dat Artık biz /dev/loop0 blok aygıt sürücüsü ile işlem yaptığımızda bu aygıt sürücü hedef olarak mydisk.dat dosyasını kullanacaktır. -> Artık dosya sistemini mkfs ile formatlayabiliriz: $ sudo mkfs -t ext2 /dev/loop0 -> Artık mount işlemi yapabiliriz: $ mkdir mydisk $ sudo mount /dev/loop0 mydisk Artık mydisk dizini bizim için adeta bir disk volümü gibidir. Ancak orada yapacağımız işlemler aslında yalnızca mydisk.dat dosyasını etkileyecektir. Bu işlemler şöyle geri alınmaktadır -> Önce volüm umont yapılır: $ sudo umount mydisk -> Şimdi /dev/loop0 ile dosya (yani mydisk.dat) arasında bağlantı losetup -d komutuyla koparılır /*================================================================================================================================*/ (165_20_09_2024) & (165_20_09_2024) & (167_27_09_2024) & (168_29_09_2024) & (169_06_10_2024) & (170_11_10_2024) & (171_13_10_2024) (172_18_10_2024) & (173_20_10_2024) & (174_25_10_2024) & (175_27_10_2024) & (176_01_11_2024) & (177_03_11_2024) & (178_08_11_2024) (179_10_11_2024) & (180_17_11_2024) & (181_22_11_2024) & (182_24_11_2024) & (183_01_12_2024) & (184_06_12_2024) & (185_08_12_2024) (186_15_12_2024) > Dosya Sistemleri ve Disk İşlemleri: Kursumuzun bu bölümünde dosya sistemlerini belli bir derinlikte inceleyeceğiz. Dosya sistemlerini ele almadan önce bilgisayar sistemlerindeki disk sistemleri hakkında bazı temel bilgilerin edinilmesi gerekmektedir. Eskiden diskler yerine teyp bantları kullanılıyordu. Teyp bantları sıralı erişim sağlıyordu. Sonra manyetik diskler kullanılmaya başlandı. Kişisel bilgisayarlardaki manyetik disklere "hard disk" de deniyordu. Bugünlerde artık hard diskler de teknoloji dışı kalmaya başlamıştır. Bugün disk sistemleri için artık flash bellekler (EEPROM bellekler) kullanılmaktadır. Yani SSD (Solid State Disk) diye isimlendirilen bu disk sistemlerinin artık mekanik bir parçası yoktur. Bunlar tamamen yarı iletken teknolojisiyle üretilmiş entegre devrelerdir. Disk sisteminin türü olursa olsun bu disk sistemini yöneten ondan sorumlu bir denetleyici birim bulunmaktadır. Buna "disk denetleyicisi (disk controller)" denilmektedir. Kernel mod ayghıt sürücüler bu disk denetleyicisini programlayarak transferleri gerçekleştirmektedir. Bugünkü disk sistemlerini şekilsel olarak aşağıdaki gibi düşünebiliriz: DISK BIRIMI <======> DİSK DENETLEYİCİSİ <=======> Aygıt Sürücü <=======> User Mod | | | Disk Cache Örneğin biz uder modda bir diskten bir sektör okumak istediğimizde bu işlemi yapan blok aygıt sürücüsünden istekte bulunuruz. >> Sektör: Bir diskten transfer edilecek en küçük birime sektör denilmektedir. Bir sektör genel olarak 512 byte uzunluğundadır. Örneğin biz bir diskteki 1 byte'ı değiştirmek istesek önce onun içinde bulunduğu sektörü belleğe okuyup değişikliği bellekte yapıp o sektörü yeniden diske yazarız. Disklere byte düzeyinde değil sektör düzeyinde erişilmektedir. Diskteki her sektöre ilk sektör 0 olmak üzere bir numara verilmiştir. Disk denetleyicileri bu numarayla çalışmaktadır. Eskiden disklerdeki koordinat sistemi daha farklıydı. Daha sonra teknoloji geliştikçe sektörünü yerini belirlemek için tek bir sayı kullanılmaya başlandı. Bu geçiş sırasında kullanılan bu sisteme LBA (Logical Block Addressing) deniliyordu. Artık ister hard diskler olsun isterse SSD'ler olsun tıpkı bellekte her byte'ın bir numarası olduğu gibi her sektörün de bir sektör numarası vardır. Transflerler bu sektör numarasayla yapılmaktadır. Aslında "dosya (file)" kavramı mantıksal bir kavramdır. Diskteki fiziksel birimler sektör denilen birimlerdir. Yani diskler dosyalardan oluşmaz sektörlerden oluşur. Dosya bir isim altında bir grup sektörü organize etmek için uydurulmuş bir kavramdır. Aslında dosyanın içindekiler diskte ardışıl sektörlerde olmak zorunda -değildir. Kullanıcı için dosya sanki ardışıl byte'lardan oluşan bir topluluk gibidir. Ancak bu bir aldatmacadır. Dosyadaki byte'lar herhangi bir biçimde ardışıl olmak zorunda değildir. Örneğin elimizde 100K'lık bir dosya olsun. Aslında bu 100K'lık dosya diskte 200 sektör içerisindedir. Peki bu dosyanın parçaları hangi 200 sektör içerisindedir? İşte bir biçimde bu bilgiler de disk üzerinde tutulmaktadır. Blok aygıt sürücüleri disk denetleyicilerini programlar, disk denetleyicileri disk birimine erişir ve transfer gerçekleşir. Disk tarnsferleri CPU aracalığıyla değil "DMA (Direct Memory Access)" denilen özel denetleyicilerle sağlanmaktadır. Yani aygıt sürücü disk denetleyicisini ve DMA'yı programlar transfer yapılana kadar bekler. Bu surada işletim sistemi zaman alacak bu işlemi meşgul bir döngüde beklemez. O anda istekte bulunan thread'i bekleme kuyruğuna yerleştirerek sıradaki thread'i çizelgeler. İşletim sistemlerinde diskten transfer işlemi yapan blok aygıt sürücüleri ismine "disk cache" ya da "buffer cache" ya da "page cache" denilen bir cache sistemi kullanmaktadır. Tabii cache sistemi aslında çekirdek tarafından organize edilmiştir. Blok aygıt sürücüsünden bir sektörlük bilgi okunmak istediğinde aygıt sürücü önce bu cache sistemine bakar. Eğer istenen sektör bu cache sisteminde varsa hiç bekleme yapmadan oradan alıp talep eden thread'e verir. Eğer sektör cache'te yoksa blok aygıt sürücüsü disk denetleyicisini ve DMA denetleyicisini programlayarak sektörü önce cache'e tarnsfer eder. Oradan talep eden thread'e verir. Bu amaçla kullanılan cache'lerde cache algoritması (cache replacement algorithm) genel olarak LRU algoritmasıdır. Yani son zamanalrda erişilen yerler mümkün olduğunca cache'te tutulmaktadır. İşlerim sistemlerinin dosya sistemleri arka planda bu blok aygıt sürücülerini kullanmaktadır. Dolayısıyla tüm dosya işlemleri aslında bu cache sistemi ile gerçekleşmektedir. Yani örneğin bugünkü modern işletim sistemlerinde ne zaman bir dosya işlemi yapılsa o dosyanın okunan ya da yazılan kısmı disk cache içerisine çekilemektedir. Aynı dosya üzerinde bir işlem yapıldığında zaten o dosyanın diskteki blokları cache'te olduğu için gerçek anlamda bir disk işlemi yapılmayacaktır. Pekiyi aygıt sürücü bir sektörü yazmak isterse ne olmaktadır? İşte yazma işlemleri de doğrudan değil cache yoluyla yapılmaktadır. Yani sektör önce disk cache'e yazılır. Sonra çizelgelenir ve işletim sisteminin bir kernel thread'i yoluyla belli periyotlarda diske transfer edilir. USer moddan çeşitli thread'lerin diskten okumalar yaptığını düşünelim. Önce bu talepler işletim sistemi tarafından kuyruklanır, çizelgelenir sonra etkin bir biçimde transfer gerçekleştirilir. İşletim sistemlerinin bu kısmına "IO çizelgeleyeicisi (IO scheduler)" denilmektedir. Disk sistemleriyle ilgili programcıların ilk bilmesi gereken işlemler bir sektörün okunmasının ve yazılmasının nasıl yapılacağıdır. Yukarıda bu işlemleri yapan yazılımsal birimin blok aygıt sürücüleri olduğunu belirtmiştik. Aygıt sürücülerin de birer dosya gibi açılıp kullanıldığını biliyoruz. O halde sektör transferi için bizim hangi aygıt sürücüyü kullanacağımızı bilmemiz gerekir. UNIX/Linux sistemlerinde bilindiği gibi tüm temel aygıt sürücülere ilişkin aygıt dosyaları "/dev" dizini içerisindedir. Bir Linux sisteminde lsblk" komutu ile disklere ilişkin blok aygıt sürücülerinin listesini elde edebilirsiniz. Örneğin: $ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sda 8:0 0 60G 0 disk ├─sda1 8:1 0 1M 0 part ├─sda2 8:2 0 513M 0 part /boot/efi └─sda3 8:3 0 59,5G 0 part / sr0 11:0 1 1024M 0 rom Linux sistemlerinde disklere ilişkin blok aygıt sürücüleri diskin türüne göre farklı biçimlerde isimlendirilmektedir. Örneğin hard diskler ve SSD diskler tipik olarak "sda", "sdb", "sdc" biçiminde isimlendirilmektedir. Micro SD kartlar ise genellikle "mmcblk0", mmcblk1", "mmcblk2" gibi isimlendirilmektedir. Örneğin burada "sda" ismi har diski bir bütün olarak ele alan aygıt dosyasının ismidir. Disk disk bölümlerinden oluşmaktadır. Bu disk bölümlerini sanki ayrı disklermiş gibi ele alan aygıt dosyaları da "sda1", "sda2", "sda3" biçimindedir. Burada "disk bölümü (disk partition)" terimini biraz açmak istiyoruz. >> "Disk Partition" : Bir diskin bağımsız birden fazla diskmiş gibi kullanılabilmesi için disk mantıksal bölümlere ayrılmaktadır. Bu bölümlere "disk bölümleri (disp partitions)" denilmektedir. Bir disk bölümü diskin belli bir sektöründen başlar belli bir sektör uzunluğu kadar devam eder. Disk bölümlerinin hangi sektörden başladığı ve hangi uzunlukta olduğu diskin başındaki bir tabloda tutulmaktadır. Bu tabloya "disk bölümleme tablosu (disk partition table)" denilmektedir. Disk bölümleme tablosu eskiden diskin ilk sektöründe tutuluyordu. Sonra UEFI BIOS'larla birlikte eski sistemle uyumlu olacak biçimde yeni disk bölümleme tablo formatı geliştirildi. Bunlara "GUID Disk Bölümleme Tablosu (GUID Partition Table)" denilmektedir. Örneğin 3 disk bölümüne sahip bir diskin mantıksal organizasyonu şöykedir: Disk Bölümleme Tablosu Birnci Disk Bölümü İkinci Disk Bölümü Üçüncü Disk Bölümü İşte "lsblk" yaptığımız Linux sisteminde biz "/dev/sda" aygıt dosyaısnıürüsünü açarsak tüm diski tek parça olarak ele alırız. Eğer "/dev/sda" aygıt dosyasını açarsak sanki Birinci Disk Bölümü ayrı diskmiş gibi yalnızca o bölümü ele alabiliriz. Örneğin "/dev/sda" aygıt dosyasından okuyacağımız 0 numaralı sektör aslında İkinci Disk Bölümünün ilk sektörürüdr. Tabii bu sektör "/dev/sda" aygıt dosyasındaki 0 numaralı sektör değildir. Linux sistemlerinde bir diskten bir sektör okumak için yapılacak tek şey ilgili aygıt sürücüyü open fonksiyonuyla açmak dosya göstericisini konumlandırıp read fonksiyonu ile okuma yapmaktır. Biz yukarıda bir diskten okunup yazılabilen en küçük birimin bir sektör olduğunu (512 byte) söylemiştik. Her ne kadar donanımsal olarak bir diskten okunabilecek ya da diske yazılabilecek en küçük birim bir sektör olsa da aslında işletim sistemleri transferleri sektör olarak değil blok blok yapmaktadır. Bir blok ardışıl n tane sektöre denilmektedir. Örneğin Linux işletim sisteminin disk cache sistemi aslında 4K büyüklüğünde bloklara sahiptir. 4K'nın aynı zamanda sayfa büyüklüğü olduğunu anımsayınız. Dolayısıyla biz Linux'ta aslında disk ile bellek arasında en az 4K'lık transferler yapmaktayız. O halde işletim sisteminin dosya sistemi ve diske doğrudan erişen sistem programcıları Linux sistemlerinde diskten birer sektör okuyup yazmak yerine 4K'lık blokları okuyup yazarsa sistemle daha uyumlu çalışmış olur. Pekiyi biz ilgili disk aygıt sürücüsünü açıp read fonksiyonu ile yalnızca 10 byte okumak istersek ne olur? İşte bu durumda blok aygıt sürücüsü gerçek anlamda o 10 byte'ın içinde bulunduğu 4K'lık bir kısmı diskten okur onu cache'e yerleştirir ve bize onun yalnızca 10 byte'ını verir. Aynı byte'ları ikinci kez okumak istersek gerçek anlamda bir disk okuması yapılmayacak RAM'de saklanmış olan cache'in içerisindeki bilgiler bize verilecektir. Aşağıda diski bir bütün olarak gören "/dev/sda" aygıt sürücüsü açılıp onun ilk sektörü okunmuş ve içeriği HEX olarak ekrana (stdout dosyasına) yazıdırılmıştır. Burada bir noktaya dikkatinizi çekmek istiyoruz. Bu aygıt sürücüyü temsil eden aygıt dosyasına ancak root kullanıcısı erişebilmektedir. Bu dosyaların erişim haklarına dikkat ediniz: $ ls /dev/sda* -l brw-rw---- 1 root disk 8, 0 Ağu 29 14:56 /dev/sda brw-rw---- 1 root disk 8, 1 Ağu 29 14:56 /dev/sda1 brw-rw---- 1 root disk 8, 2 Ağu 29 14:56 /dev/sda2 brw-rw---- 1 root disk 8, 3 Ağu 29 14:56 /dev/sda3 Bu durumda programınızı sudo ile çalıştırmalısınız. * Örnek 1, #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; unsigned char buf[512]; if ((fd = open("/dev/sda", O_RDONLY)) == -1) exit_sys("open"); if (read(fd, buf, 512) == -1) exit_sys("read"); for (int i = 0; i < 512; ++i) printf("%02x%c", buf[i], i % 16 == 15 ? '\n' : ' '); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Dosya sistemi (file system) denildiğinde ne anlaşılmaktadır? Bir dosya sisteminin iki yönü vardır: Bellek ve disk. Dosya sisteminin bellek tarafı işletim sisteminin açık dosyalar için kernel alanında yaptığı organizasyonla (dosya betimelyici tablosu, dosya nesnesi vs) ilgilidir. Disk tarafı ise diskteki organizasyonla ilgilidir. Biz kursumuzda bellek tarafındaki organizasyonun temellerini gördük. Şimdi bu bu bölümde disk üzerindeki organizasyonu ele alacağız. Dosya kavramını diskte oluşturmak için farklı dosya sistemleri tarafından farklı disk organizasyonları kullanılmaktadır. Bugün kullanılan çok sayıda dosya sistemi vardır. Bu sistemlerin her birinin disk organizasyonu diğerinden az çok farklıdır. Ancak bazı dosya sistemlerinin birbirlerine organizasyonları birbirine çok benzemektedir. Bunlar adreta bir aile oluşturmaktadır. Örneğin Microsoft'un FAT dosya sistemleri, Linux'un ext dosya sistemleri kendi aralarında birbirine oldukça benzemektedir. Microsft'un dünyanın kişisel bilgisayarlarında kullandığı dosya sistemlerine aile olarak FAT (File Allocation Table) denilmektedir. Bu FAT dosya sistemlerinin kendi içerisinde FAT12, FAT16 ve FAT32 biçiminde varyasyonları vardır. Microsoft daha sonra yine FAT tabanlı ancak çok daha gelişmiş NTFS denilen bir dosya sistemi gerçekleştirmiştir. Bugün Windows sistemlerinde genel olarak NTFS (New Technology File SYstems) dosya sistemleri kullanılmaktadır. Ancak Microsoft hala FAT tabanlı dosya sistemlerini de desteklemektedir. Linux sistemlerinde "EXT (Extended File System)" ismi verilen "i-node tabanlı" dosya sistemleri kullanılmaktadır. Bu EXT dosya sistemlerinin EXT2, EXT3, EXT4 biçiminde varyasyonları vardır. Bugünkü Linux sistemlerinde en çok EXT4 dosya sistemi kullanılmaktadır. Apple firması yine i-node tabanlı HFS (Hierarchical File System), HFS+ (Hierarchical File System Plus) ve APFS (Apple File System) isimli dosya sistemlerini kullanmaktadır. Bunlar da aile olarak birbirlerine çok benzemektedir. Bugünkü macOS sistemlerinde genellikle HFS+ ya da APFS dosya sistemleri kullanılmaktadır. Bilindiği gibi UNIX/Linux sistemlerinde Windows sistemlerinin aksine "sürücü (drive)" kavramı yoktur. Dosya sisteminde tek bir "kök dizin (root directory)" vardır. Eğer biz bir dosya sistemine erişmek istiyorsak önce onu belli bir dizinin üzerine "mount" ederiz. Artık o dosya sisteminin kökü mount ettiğimiz dizin üzerinde bulunur. Örneğin bir flash belleği USB yuvasına taktığımızda Windows'ta o flash bellek bir sürücü olarak gözükmektedir. Ancak Linux sistemlerinde o flash bellek belli bir dizinin altında gözükür. Yani o dizine mount işlemi yapılmaktadır. Bir dosya sistemi bir dizine mount edildiğinde artık o dizin ve onun altındaki dizin ağacı görünmez olur. Onun yerine mount ettiğimiz blok aygıtındaki dosya sisteminin kökü görünür. Mount işlemi Linux sistemlerinde aslında bir sistem fonksiyonuyla yapılmaktadır. Bu sistem fonksiyonu "libc" kütüphanesinde "mount" ismiyle bulunmaktadır. >> "mount" : Fonksiyonun prototipi şöyledir: #include int mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data); Fonksiyonun, -> Birinci parametresi blok aygıt dosyasının yol ifadesini, -> İkinci parametre mount dizinini (mount point) belirtmektedir. -> Üçüncü parametre dosya sisteminin türünü almaktadır. -> Dördüncü parametre mount bayraklarını belirtmektedir. Bu parametre 0 geçilebilir. -> Son parametre ise dosya sistemi için gerekebilecek ekstra verileri belirtmektedir. Bu parametre de NULL adres geçilebilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Dosya sisteminin türünün otomatik tespit eden bazı özel fonksiyonlar bulunmaktadır. Örneğin, "libmount" kütüphanesi içerisindeki statfs fonksiyonuyla ya da "libblkid" kütüphanesi içerisindeki fonksiyonlarla bunu sağlayabilirsiniz. Tabii bu fonksiyonu çağırabilmek için prosesimizin etkin kullanıcı id'sinin 0 olması ya da prosesimizin uygun önceliğe (appropriate privilege) sahip olması gerekir. mount fonksiyonu POSIX standartlarında yoktur. Çünkü işletim sisteminin gerçekleştirimine oldukça bağlı bir fonksiyondur. Tabii kullanıcılar mount işlemini bu sistem fonksiyonu yoluyla değil, "mount" isimli kabuk komutuyla yapmaktadır. mount işlemi için elimizde bir blok aygıt sürücüsüne ilişkin aygıt dosyasının bulunuyor olması gerekir. Ancak blok aygıt sürücüleri mount edilebilmektedir. Tabii ilgili blok aygıt sürücüsünün sektörleri içerisinde bir dosya sisteminin bulunuyor olması gerekir. mount isimli kabuk komutunun tipik kullanımı şöyledir: sudo mount Mount edilecek dizine genel olarak İngilizce "mount point" de denilmektedir. Örneğin bilgisayarımıza bir SD kart okuyucu bağlamış olalım. lsblk yaptığımızda şöyle bir görüntüyle karşılaştığımızı varsayalım: $ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sda 8:0 0 60G 0 disk ├─sda1 8:1 0 1M 0 part ├─sda2 8:2 0 513M 0 part /boot/efi └─sda3 8:3 0 59,5G 0 part / sdb 8:16 1 0B 0 disk sdc 8:32 1 14,8G 0 disk ├─sdc1 8:33 1 60M 0 part /media/kaan/72FA-ACF3 └─sdc2 8:34 1 14,8G 0 part /media/kaan/fa57bb30-99ca-4966-8249-6b0c6c4f4d8d sdd 8:48 1 0B 0 disk sr0 11:0 1 1024M 0 rom Burada taktığımız SD kart "sdc" ismiyle gözükmektedir. "/dev/sdc" aygıt dosyası SD kartı bir bütün olarak görmektedir. Bu SD kartın içerisinde iki farklı disk bölümünün oluşturulduğu görülmektedir. Bu disk bölümlerine ilişkin aygıt dosyaları da "/dev/sdc1" ve "/dev/sdc2" dosyalarıdır. Biz "/dev/sdc" aygıtını mount edemeyiz. Çünkü bu aygıt, diski bir bütün olarak görmektedir. Oysa "/dev/sdc1" ve "/dev/sdc2" aygıtlarının içerisinde daha önceden oluşturulmuş olan dosya sistemleri vardır. Biz bu aygıtları mount edebiliriz. Mount işlemi için sistem yöneticisinin bir dizin oluşturması gerekir. Mount işlemleri için Linux sistemlerinde kök dizinin altında bir "mnt" dizini oluşturulmuş durumdadır. Yani mount edilecek dizini bu dizinin altında yaratabilirsiniz. Tabii böyle bir zorunluluk yoktur. Biz bulunduğumuz dizinde boş bir dizin yaratıp bu dizini mount point olarak kullanabiliriz. Örneğin: $ sudo mount /dev/sdc1 mydisk mount komutu ilgili blok aygıtındaki dosya sistemini otomatik olarak tespit etmeye çalışır. Genellikle bu tespit otomatik yapılabilmektedir. Ancak bazı özel aygıtlar ve dosya sistemleri için bu belirlemenin açıkça yapılması gerekebilir. Bunun için mount komutunda "-t " seçeneği kullanılır. Örneğin: $ sudo mount -t vfat /dev/sdc1 mydisk Burada -t seçeneğine argümanı olarak aşağıdaki gibi dosya sistemleri kullanılabilir: ext2 ext3 ext4 ntfs vfat tmpfs xfs ... Dosya sisteminin otomatik belirlenmesi mount sistem fonksiyonu tarafından yapılmaktadır. mount komutu birtakım işlemlerle bunu sağlamaktadır. Mount edilmiş olan bir blok aygıtının mount işlemi umount isimli sistem fonksiyonuyla kaldırılabilir. >> "umount" : Fonksiyonun prototipi şöyledir: #include int umount(const char *target); Fonksiyon mount dizinini parametre olarak almaktadır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. Artık umount yapıldıktan sonra mount point dizinin içeriğine yeniden erişilebilmektedir. Unmount işlemi de yine komut satırından "umount" komutuyla yapılabilmektedir. Komutun genel biçimi şöyledir: $ sudo umount Örneğin: $ sudo umount mydisk Pek çok UNIX türevi sistemde olduğu gibi Linux sistemlerinde de "otomatik mount" mekanizması bulunmaktadır. Sistem boot edildiğinde konfigürasyon dosyalarından hareketle otomatik mount işlemleri yapılabilmektedir. USB aygıtları genel olarak zaten otomatik mount işlemi oluşturmaktadır. "systemd" init sisteminde "mount unit" dosyaları ile otomatik mount işlemleri yönetilebilmektedir. Klasik "system5" init sistemlerinde çekirdek yüklendikten sonra "/etc/fstab" dosyasında otomatik mount edilecek blok aygıtları belirtilebilmektedir. "/etc/fstab" dosyasına "systemd" tarafından da açılış sırasında bakılmaktadır. Aşağıda mount sistem fonksiyonu çağrılarak mount işlemi yapan bir örnek verilmiştir. Programı, $ sudo ./mymount /dev/sdc1 mydisk vfat benzer biçimde çalıştırabilirsiniz: * Örnek 1, /* mymount.c */ #include #include #include void exit_sys(const char *msg); /* mymount */ int main(int argc, char *argv[]) { if (argc != 4) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if (mount(argv[1], argv[2], argv[3], 0, NULL) == -1) exit_sys("mount"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Linux sistemlerinde bir dosyayı sanki blok aygıtı gibi gösteren hazır aygıt sürücüler bulunmaktadır. Bunlara "loop" aygıt sürücüleri denilmektedir. Bu aygıt sürücülere ilişkin aygıt dosyaları "/dev" dizini içerisinde "loopN" ismiyle (burada N bir sayı belirtiyor) bulunmaktadır. Örneğin: $ ls -l /dev/loop* brw-rw---- 1 root disk 7, 0 Haz 4 22:31 /dev/loop0 brw-rw---- 1 root disk 7, 1 Haz 4 22:31 /dev/loop1 brw-rw---- 1 root disk 7, 2 Haz 4 22:31 /dev/loop2 brw-rw---- 1 root disk 7, 3 Haz 4 22:31 /dev/loop3 brw-rw---- 1 root disk 7, 4 Haz 4 22:31 /dev/loop4 brw-rw---- 1 root disk 7, 5 Haz 4 22:31 /dev/loop5 brw-rw---- 1 root disk 7, 6 Haz 4 22:31 /dev/loop6 brw-rw---- 1 root disk 7, 7 Haz 4 22:31 /dev/loop7 crw-rw---- 1 root disk 10, 237 Haz 4 22:31 /dev/loop-control Bir dosyayı blok aygıt sürücüsü biçiminde kullanabilmek için önce "losetup" programı ile bir hazırlık işleminin yapılması gerekir. Hazırlık işleminde "loop" aygıt sürücüsüne ilişkin aygıt dosyası ve blok aygıt sürücüsü olarak gösterilecek dosya belirtilir. Bu işlemin sudo ile yapılması gerekmektedir. Örneğin: $ sudo losetup /dev/loop0 mydisk.dat Tabii bizim burada "mydisk.dat" isimli bir dosyaya sahip olmamız gerekir. İçi 0'larla dolu 100 MB'lik böyle bir dosyayı dd komutuyla aşağıdaki gibi oluşturabiliriz: $ dd if=/dev/zero of=mydisk.dat bs=512 count=100000 Burada artık "/dev/loop0" aygıt dosyası adeta bir disk gibi kullanılabilir hale gelmiştir. Biz bu "/dev/loop0" dosyasını kullandığımızda bu işlemlerden aslında "mydisk.dat" dosyası etkilenecektir. Sıfırdan bir diske ya da bir disk bölümüne bir dosya sistemi yerleştirebilmek için onun formatlanması gerekir. UNIX/Linux sistemlerinde formatlama için "mkfs.xxx" isimli programlar bulundurulmuştur. Örneğin aygıtta FAT dosya sistemi oluşturmak için "mkfs.fat" programı, ext4 dosya sistemi oluşturmak için "mkfs.ext4" programı kullanılmaktadır. Örneğin biz yukarıda oluşturmuş olduğumuz "/dev/loop" aygıtını ext2 dosya sistemi ile aşağıdaki gibi formatlayabiliriz: $ sudo mkfs.ext2 /dev/loop0 Burada işlemden aslında "mydisk.dat" dosyası etkilenmektedir. Artık formatladığımız aygıta ilişkin dosya sistemini aşağıdaki gibi mount edebiliriz: $ mkdir mydisk $ sudo mount /dev/loop0 mydisk Loop aygıtının dosya ile bağlantısını kesmek için "losetup" programı "-d" seçeneği ile çalıştırılır. Tabii önce aygıtın kullanımdan düşürülmesi gerekir: $ sudo umount mydisk $ sudo losetup -d /dev/loop0 Eğer loop aygıt sürücüsünün bir dosyayı onun belli bir offset'inden itibaren kullanmasını istiyorsak losetup programında "-o (ya da "--offset") seçeneğini kullanmalıyız. Örneğin bir disk imajının içerisindeki Linux dosya sisteminin disk imajının 8192'inci sektöründen başladığını varsayalım. "dev/loop0" aygıt sürücüsünün bu imaj dosyasını bu offset'ten itibaren kullanmasını şöyle sağlayabiliriz: $ sudo losetup -o 4194304 /dev/loop0 am335x-debian-11.7-iot-armhf-2023-09-02-4gb.img Buradaki "512 * 8192 = 4194304" olduğuna dikkat ediniz. Şimdi de yukarıda değindiğimiz FAT, ext dosya sistemlerini sırasıyla inceleyelim. Biz kursumuzda önce FAT dosya sisteminden bahsedeceğiz sonra UNIX/Linux sistemlerindeki i-node tabanlı EXT dosya sistemleri üzerinde duracağız. >> FAT Dosya Sistemi: FAT dosya sistemi Microsof tarafından DOS işletim sistemi için geliştirilmiştir. Ancak bu dosya sistemi hala kullanılmaktadır. FAT dosya sistemi o zamanlar teknolojisiyle tasarlanmıştır. Dolayısıyla modern dosya sistemlerinde bulunan bazı özellikler bu dosya sisteminde bulunmamaktadır. FAT dosya sistemi kendi aralarında FAT12, FAT16 ve FAT32 olmak üzere üç gruba ayrılmaktadır. Bu sistemlerin arasındaki en önemli fark dosya sistemi içerisindeki FAT (File Allocation Table) denilen tablodaki elemanların uzunluklarıdır. FAT12'de FAT elemanları 12 bit, FAT16'da 16 bit ve ve FAT32'de 32 bittir. Microsoft, Windows sistemlerine geçtiğinde bu FAT sistemini biraz revize etmiştir. Buna da VFAT denilmektedir. Bir disk ya da disk bölümü (Disk Partition) FAT dosya sistemiyle formatlandığında disk bölümünde dört mantıksal bölüm oluşturulmaktadır: -> Boot Sektör -> FAT Bölümü -> Root Dir Bölümü -> Data Bölümü Bir dosya sisteminin içi boş bir biçimde kullanıma hazır hale getirilmesi sürecine formatlama denilmektedir. Formatlama sırasında ilgili disk ya da disk bölümünde ilgili dosya sistemi için meta data alanlar oluşturulmaktadır. Windows'ta ilgili disk ya da disk bölümünü FAT dosya sistemiyle formatlamak için "Bilgisayar Yönetimi / Disk Yönetimi" kısmından ilgili disk bölümü seçilir ve farenin sağ tuşuna basılarak formatlama yapılır. Benzer biçimde formatlama "Bilgisayarım (My Computer)" açılarak orada ilgili disk bölümünün üzerine sağa tıklanarak da yapılabilmektedir. Linux sistemlerinde bir blok aygıt sürücüsü ya da doğrudan bir dosya "mkfs.fat" programıyla formatlanabilir. Biz yukarıda da belirttiğimiz gibi bir dosyayı sanki disk gibi kullanacağız. Örneğin "dd" programıyla 50MB'lik içi sıfırlarla dolu bir dosya oluşturalım: $ dd if=/dev/zero of=mydisk.dat bs=512 count=100000 Burada 512 * 100000 byte'lık (yaklaşık 50 MB) içi sıfırlarla dolu bir dosya oluşturulmuştur. Bu dosyayı "/dev/loop0" blok aygıt sürücüsü biçiminde kullanılabilmesi şöyle sağlanabilir: $ sudo losetup /dev/loop0 mydisk.dat Şimdi artık "mkfs.fat" programı ile formatlamayı yapabiliriz. Yukarıda FAT'in FAT12, FAT16 ve FAT32 olmak üzere üç türünün olduğunu belirtmiştik. FAT türü "mkfs.fat" programında -F12, -F16 ya da -F32 seçenekleriyle belirtilmektedir. Örneğin biz blok aygıtımızı FAT16 biçiminde şöyle formatlayabiliriz: $ sudo mkfs.fat -F16 /dev/loop0 Aslında "mkfs.xxx" programları blok aygıt dosyası yerine normal bir dosya üzerinde de formatlama yapabilmektedir. Tabii biz kursumuzda bir blok aygıtı oluşturup onu mount edeceğiz. Şimdi biz FAT16 olarak formatladığımız "/dev/loop0" blok aygıtını mount edebiliriz. Tabii bunun için önce bir "mount dizininin (mount point)" oluşturulması gerekmektedir: $ mkdir fat16 $ sudo mount /dev/loop0 fat16 Artık fat16 dizini oluşturduğumuz FAT dosya sisteminin kök dizinidir. Ancak bu dosya sisteminin tüm bilgileri "mydisk.dat" dosyasında bulundurulacaktır. FAT dosya sistemiyle formatlanmış olan bir diskin ya da disk bölümünün ilk sektörüne "Boot Sector" denilmektedir. Dolayısıyla boot sektör ilgili diskin ya da disk bölümünün mantıksal 0 numaralı sektöründedir. Boot sektör isminden de anlaşılacağı gibi 512 byte uzunluğundadır. Bu sektörün iç organizasyonu şöyledir: Jmp Kodu | BPB (BIOS Parameter Block) | DOS Yükleyici Programı | 55 AA Boot sektörün hemen başında Intel Mimarisinde BPB bölümünü atlayarak DOS işletim sistemini yükleyen yükleyici program için bir jmp komutu bulunmaktadır. Bugün artık DOS işletim sistemi kullanılmadığı için buradaki jmp kodun ve yükleyici programın bir işlevi kalmamıştır. Ancak BPB alanı eskiden olduğu yerdedir ve dosya sistemi hakkında kritik bilgiler bu bölümde tutulmaktadır. Sektörün başındaki Jmp Code tipik olarak "EB 3C 90" makine komutundan oluşmaktadır. Bazı kaynaklar bu jmp kodu da BPB alanına dahil etmektedir. Eğer dosya sisteminde yüklenecek bir DOS işletim sistemi yoksa buradaki yükleyici program yerine format programı buraya ekrana mesaj çıkartan küçük program kodu yerleştirmektedir. Aşağıda "mkfs.fat" programı ile FAT16 biçiminde formatlanan FAT dosya sisteminin boot sektör içeriği görülmektedir: $ hexdump -C mydisk.dat -n 512 -v 00000000 eb 3c 90 6d 6b 66 73 2e 66 61 74 00 02 04 04 00 |.<.mkfs.fat.....| 00000010 02 00 02 00 00 f8 64 00 20 00 08 00 00 00 00 00 |......d. .......| 00000020 a0 86 01 00 80 01 29 fa 0b 93 c5 4e 4f 20 4e 41 |......)....NO NA| 00000030 4d 45 20 20 20 20 46 41 54 31 36 20 20 20 0e 1f |ME FAT16 ..| 00000040 be 5b 7c ac 22 c0 74 0b 56 b4 0e bb 07 00 cd 10 |.[|.".t.V.......| 00000050 5e eb f0 32 e4 cd 16 cd 19 eb fe 54 68 69 73 20 |^..2.......This | 00000060 69 73 20 6e 6f 74 20 61 20 62 6f 6f 74 61 62 6c |is not a bootabl| 00000070 65 20 64 69 73 6b 2e 20 20 50 6c 65 61 73 65 20 |e disk. Please | 00000080 69 6e 73 65 72 74 20 61 20 62 6f 6f 74 61 62 6c |insert a bootabl| 00000090 65 20 66 6c 6f 70 70 79 20 61 6e 64 0d 0a 70 72 |e floppy and..pr| 000000a0 65 73 73 20 61 6e 79 20 6b 65 79 20 74 6f 20 74 |ess any key to t| 000000b0 72 79 20 61 67 61 69 6e 20 2e 2e 2e 20 0d 0a 00 |ry again ... ...| 000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| Burada yükleyici programın DOS olmaması durumunda ekrana yazdırdığı mesaj görülmektedir. Tabii bu mesajın çıkması için bu diskin ya da disk bölümünün aktif disk ya da aktif disk bölümü olması gerekir. Yani bu diskten ya da disk bölümünde boot etme girişimi olmadıktan sonra bu mesaj görülmeyecektir. FAT dosya sisteminin en önemli meta data bilgileri boot sektörün hemen başındaki BPB (Bios Parameter Block) alanında tutulmaktadır. Bu bölümün bozulması durumunda dosya sistemine erişim mümkün olamamaktadır. Başka bir deyişle bu dosya sisteminin bozulmasını sağlamak için tek yapılacak şey bu BPB alanındaki byte'ları sıfırlamaktır. Tabii zamanla FAT dosya sistemindeki diğer bölümleri inceleyerek bozulmuş olan BPB alanını onaran yardımcı araçlar da çeşitli kişiler ve kurumlar tarafından geliştirilmiştir. Boot sektörün sonunda "55 AA" değeri bulunmaktadır. Bu bir sihirli sayı (magic number) olarak bulundurulmaktadır. Bazı programlar ve bazı boot loader'lar kontrolü boot sektöre bırakmadan önce bu sihirli sayıyı kontrol edebilmektedir. Böylece rastgele bozulmalarda bu sihirli sayı da bozulacağı için yetersiz olsa da basit bir kontrol mekanizması oluşturulabilmektedir. FAT dosya sisteminde en önemli kısım şüphesiz "BPB (BIOS Parameter Block)" denilen kısımdır. BPB hemen boot sektörün başındadır ve FAT dosya sisteminin diğer bölümleri hakkında kritik bilgiler içermektedir. Tabii BPB bölümü 1980'lerin anlayışıyla tasarlanmıştır. Bu tasarımda hatalar DOS'un çeşitli versiyonlarında geçmişe doğru uyumu koruyarak giderilmeye çalışılmıştır. Biz burada önce FAT12 ve FAT16 sistemlerinde kullanılan BPB bloğunun içeriğini tek tek ele alacağız. FAT32 ile birlikte BPB bloğuna eklemeler de yapılmıştır. FAT32 BPB formatını daha sonra ele alacağız. >>> FAT12 & FAT16 : Aşağıda FAT12 ve FAT16 sistemlerindeki BPB bloğunun formatı açıklanmaktadır. Tablodaki Offset sütunu Hex olarak ilgili alanın Boot sektörün başından itibaren kaçıncı byte'tan başladığını belirtmektedir. Offset (Hex) Uzunluk Anlamı 00 3 Byte Jmp Kodu 03 8 Byte OEM Yorum Alanı 0B WORD Sektördeki Byte Sayısı 0C BYTE Cluster'daki Sektör Sayısı 0E WORD Ayrılmış Sektörlerin Sayısı 10 BYTE FAT Kopyalarının Sayısı 11 WORD Kök Dizinlerindeki Girişlerin Sayısı 13 WORD Toplam Sektör Sayısı (Eski) 15 BYTE Ortam Belirleyicisi (Media Descriptor) 16 WORD FAT'in Bir Kopyasındaki Sektör Sayısı 18 WORD Bir Yüzdeki Sektör Dilimlerinin Sayısı (Artık Kullanılmıyor) 1A WORD Disk Yüzeylerinin (Kafalarının) Sayısı (Artık Kullanılmıyor) 1C DWORD Saklı Sektörlerin Sayısı 20 DWORD Yeni Toplam Sektör Sayısı 24 3 Byte Reserved 27 DWORD Volüm Seri Numarası 2B 11 Byte Volüm İsmi Bu tabloya göre, -> Jump Kodu: Yukarıda da belirttiğimiz gibi BPB bloğunu geçerek yükleyici programa atlayan makine komutlarından oluşmaktadır. Boot loader programlar akışı buradan boot sektöre devretmektedir. Dolayısıyla BPB alanının atlanması gerekmektedir. Burada bazen Intel short jump bazen de near jump komutları bulunur. Tipik içerik "EB 3C 90" biçimindedir. -> OEM Yorum Alanı: Formatlama programının kendine özgü yazdığı 8 byte'lık küçük yazıdır. Buraya eskiden DOS işletim sisteminin versiyon numarası yazılıyordu. Örneğin Windows bu BPB alanın yeni biçiminin tanındığı en eski sistem olan "MSDOS5.0" yazısını buraya yerleştirmektedir. Ancak buraya yerleştirilen yazı herhangi bir biçimde kullanılmamaktadır. -> Sektördeki Byte Sayısı: Bir sektörde kaç byte olduğu bilgisi burada tutulmaktadır. Tabii bu değer hemen her zaman 512'dir. Yani Little Endian formatta hex olarak burada "00 02" değerlerini görmemiz gerekir. -> Cluster'daki Sektör Sayısı: Dosyaların parçaları disk üzerinde ardışıl bir biçimde konumlandırılmak zorunda değildir. FAT dosya sisteminde bir dosyanın hangi parçasının diskte nerede konumlandırıldığı FAT (File Allocation Table) denilen bir bölümde saklanmaktadır. Eğer bir dosya çok fazla parçaya ayrılırsa hem disk üzerinde daha çok yayılmış olur hem de FAT bölümünde bu dosyanın parçalarının yerini tutmak için gereken alan büyür. Bu nedenle dosyaların parçaları sektörlere değil, cluster denilen birimlere bölünmüştür. Bir cluster ardışıl n tane sektörün oluşturduğu topluluktur. Örneğin bir cluster'ın 4 sektör olması demek 4 sektörden oluşması (yani 2K) demektir. Şimdi elimizde 10,000 byte uzunluğunda bir dosya olsun. Bir cluster'ın 1 sektör olduğunu düşünelim. Bu durumda bu 10,000 byte'lık dosya toplamda 10000 / 512 = 19.53125 yani 20 cluster yer kaplayacaktır. FAT bölümünde bu 20 cluster 20 elemanlık yer kaplayacaktır. Şimdi bir cluster'ın 4 sektörden oluştuğunu düşünelim. Bu durumda 10,000 byte'lık dosya 10000 / 2048 = 4.8828125 yani 5 cluster yer kaplayacaktır. Bu dosyanın yerini tutmak için FAT bölümünde 5 eleman yeterli olacaktır. Görüldüğü gibi cluster bir dosyanın bir parçasını tutabilen en düşük tahsisat birimidir. Halbuki sektör diskten transfer edilecek en küçük birimdir. Sektör yerine dosya sisteminin cluster kavramını kullanmasının iki nedeni vardır. Birincisi cluster ardışıl sektörlerden oluştuğu için dosyanın parçaları diskte daha az yayılmış olur. İkincisi de dosyanın parçalarının yerlerini tutmak için daha az alan gerekmektedir. Pekiyi bir cluster kaç sektörden oluşmalıdır? Eğer bir cluster çok fazla sayıda sektörden oluşursa dosyanın son parçasında kullanılmayan alan (buna "içsel bölünme (internal fragmentation)" da denilmektedir) fazlalaşır diskin kullanım kapasitesi azalmaya başlar. Örneğin bir cluster'ın 32 sektörden (16K) oluştuğunu varsayalım. Bu durumda 1 byte'lık bir dosya bile 16K yer kaplayacaktır. Çünkü dosya sisteminin minimum tahsisat birimi 16K'dır. Örneğin bir sistemde 100 tane 1 byte'lık dosyanın diskte kapladığı alanla 1 tane 100 byte'lık dosyanın diskte kapladığı alan kıyaslandığında 100 tane 1 byte'lık dosyanın diskte çok daha fazla yer kapladığı görülecektir. İşte UNIX/Linux sistemlerinde dosyaları tek bir dosyada peşisıra birleştiren ve bunların yerlerini dosyanın başındaki bir başlık kısmında tutan "tar" isimli bir yardımcı program bulunmaktadır. "tar" programının bir sıkıştırma yapmadığına diskteki kaplanan alanı azaltmak için yalnızca dosyaları uç uca eklediğine dikkat ediniz. Tabii genellikle dosyalar tar'landıktan sonra ayrıca sıkıştırılabilir. Bu sistemlerdeki "tar.gz" gibi dosya uzantıları tar'landıktan sonra zip'lenmiş olan dosyaları belirtmektedir. Pekiyi o halde bir cluster'ın kaç sektör olacağına nasıl karar verilmektedir? İşte sezgisel olarak disk hacmi büyüdükçe kaybedilen alanların önemi azalacağı için cluster'ın çok sektörden oluşturulması, disk hacmi azaldıkça az sektörden oluşturulması yoluna gidilmektedir. Format programları bu değerin kullanıcı tarafından belirlenmesine olanak sağlamakla birlikte default değer de önermektedir. Linux'taki "mkfs.fat" programında ise cluster boyutu "-s" seçeneği ile belirlenmektedir. Örneğin: $ sudo mkfs.fat -F16 -s 2 /dev/loop0 Burada bir cluster 2 sektörden oluşturulmuştur. İşte BPB bloğunun "0C" offset'inde bir cluster'ın kaç sektörden oluştuğu bilgisi yer almaktadır. İşletim sistemi dosyaların parçalarına erişirken hep bu bilgiyi kullanmaktadır. (Burada değeri disk editörü ile değiştirsek dosya sistemi tamamen saçmalayacaktır.) Yukarıdaki örnek boot sektörde bir cluster 4 sektörden (yani 4K = 2048 byte'tan) oluşmaktadır. -> Ayrılmış Sektörlerin Sayısı: Burada boot sektörü izleyen FAT bölümünün kaçıncı sektörden başladığı bilgisi yer almaktadır. Tabii buradaki orijin FAT disk bölümünün başıdır. Yani boot sektör 0'ıncı sektörde olmak üzere FAT bölümünün kaçıncı sektörden başladığını belirtmektedir. Pekiyi neden boot sektör ile FAT arasında boşluk bırakmak gerekebilir? İşte hard disklerde işletim sistemi FAT bölümünü ilk silindire hizalamak isteyebilir. Eğer özel uygulamalarda boot sektör yükleyici programı uzunsa yükleyicinin diğer parçaları da burada bulunabilmektedir. Yukarıdaki örnek FAT bölümünün boot sektöründe bu byte'lar "04 00" biçimindedir. Little Endian formatta bu değer 4'tür. O halde bu dosya sisteminde FAT bölümü 4'üncü sektörden başlamaktadır. -> FAT Kopyalarının Sayısı: FAT bölümü izleyen paragraflarda da görüleceği gibi FAT dosya sisteminin önemli bir meta-data alanıdır. Bu nedenle bu bölümün backup amaçlı birden fazla kopyasının bulundurulması uygun görülmüştür. Tipik olarak bu alanda 2 değeri bulunur. Yani FAT bölümünün toplamda iki kopyası vardır. FAT bölümünün kopyaları hemen birbirinin peşi sıra dizilmiştir. Yani bir kopyanın bittiği yerde diğeri başlamaktadır. -> Kök Dizinlerindeki Girişlerin Sayısı: FAT dosya sistemindeki bölümlerin dizilimin şöyle olduğunu belirtmiştik: Boot Sektör FAT ve Kopyaları Root Dir Bölümü Data Bölümü İşletim sisteminin tüm bölümlerin hangi sektörden başladığını ve kaç sektör uzunlukta olduğunu bilmesi gerekir. İşte "Root Dir" bölümü dizin girişlerinden oluşmaktadır. Bir dizin girişi 32 byte uzunluğundadır. Burada toplam kaç giriş olduğu belirtilmektedir. Dolayısıyla "Root Dir" bölümünün sektör uzunluğu buradaki sayının 32'ye bölümü ile hesaplanır. Bizim oluşturduğumuz örnek FAT16 disk bölümünde burada "0x0200" (512) değeri bulunmaktadır. Bu durumda Root Dir bölümünün sektör uzunluğu 512 / 32 = 16'dır. -> Toplam Sektör Sayısı (Eski): Bu alanda disk bölümündeki toplam sektör sayısı bulundurulmaktadır. Ancak BPB formatının tasarlandığı 1980'lerin başında henüz hard diskler çok yeniydi ve teknolojinin bu kadar hızlı gelişeceği düşünülmemişti. Dolayısıyla toplam sektör sayısı için 2 byte'lık yer o zamanlar için yeterli gibiydi. Toplam sektör sayısı için ayrılan 2 byte'lık yerde yazılabilecek maksimum değer 65535'tir. Bu değeri 512 ile çarparsak 33MB'lık bir alan söz konusu olur. Gerçekten de o devirlerde diskler 33MB'den daha yukarıda formatlanamıyordu. DOS 4.01'e kadar 33MB bir üst sınırdı. Ancak DOS 4.01 ile birlikte bu toplam sektör sayısı geçmişe doğru uyum korunarak 4 byte yükseltildi. Dolayısıyla DOS 4.01 ve sonrasında artık disk bölümünün toplam kapasitesi 2^32 * 2^9 = 2TB'ye yükselmiş oldu. 4 byte'tan oluşan yeni toplam sektör sayısı alanı boot sektörün "0x20" offset'inde bulunmaktadır. Dosya sistemleri toplam sektör sayısı için önce "0x13" offset'inde bulunan bu alana başvurmaktadır. Eğer bu alanda 0 yoksa bu alandaki bilgiyi, eğer bu alanda 0 varsa "0x20" offset'inden çekilen DWORD bilgiyi dikkate almaktadır. -> Ortam Belirleyicisi (Media Descriptor): Bu alanda dosya sisteminin konuşlandığı medyanın türünün ne olduğu bilgisi bulunmaktadır. Aslında artık böyle bir bilgi işletim sistemleri tarafından kullanılmamaktadır. Buradaki 1 byte'ın yaygın değerleri şunlardır: 0xF0: 1.44 Floppy Disk 0xF8: Hard disk Bu alanda artık hep F8 byte'ı bulunmaktadır. -> FAT'in Bir Kopyasındaki Sektör Sayısı: Bu alanda FAT'in bir kopyasının kaç sektör uzunluğunda olduğu bilgisi bulunmaktadır. FAT'in default olarak 2 kopyasının olduğunu anımsayınız. -> Bir Yüzdeki Sektör Dilimlerinin Sayısı (Artık Kullanılmıyor): Bu alanda diskin bir yüzeyinde kaç sektör dilimi olduğu bilgisi yer almaktadır. Eskiden sektörlerin koordinatları "yüzey numarası, track numarası ve sektör dilimi numarası" ile belirtiliyordu. Uzunca bir süredir artık bu sistem terk edilmiştir. Dolayısıyla bu alana başvurulmamaktadır. -> Disk Yüzeylerinin (Kafalarının) Sayısı (Artık Kullanılmıyor): Burada diskte toplam kaç yüzey (kafa) olduğu bilgisi yer alıyordu. Ancak yine koordinat sistemi uzunca bir süre önce değiştirildiği için bu alan artık kullanılmamaktadır. -> Saklı Sektörlerin Sayısı: Bu alanda FAT dosya sisteminin diskin toplamda kaçıncı sektöründen başladığı bilgisi yer almaktadır. Bu bilgi aynı zamanda Disk Bölümleme Tablosu (Disk Partition Table) içerisinde de yer almaktadır. İşletim sistemleri bu iki değeri karşılaştırıp BPB bloğunun bozuk olup olmadığı konusunda bir karar da verebilmektedir. -> Yeni Toplam Sektör Sayısı: "0x13" offset'indeki WORD olarak bulundurulan eski "toplam sektör sayısı" bilgisinin DWORD olarak yenilenmiş biçimi bu alanda tutulmaktadır. -> Volüm Seri Numarası: Bir disk bölümü FAT dosya sistemi ile formatlandığında oraya rastgele üretilmiş olan bir "volüm seri numarası" atanmaktadır. Bu volüm seri numarası eskiden floppy disket zamanlarında disketin değişip değişmediğini anlamak için kullanılıyordu. Bugünlerde artık bu alan herhangi bir amaçla kullanılmamaktadır. Ancak sistem programcısı bu seri numarasından başka amaçlar için faydalanabilir. -> Volüm İsmi: Her volüm formatlanırken ona bir isim verilmektedir. Bu isim o zamanki dosya isimlendirme kuralı gereği 8 + 3 = 11 karakterden oluşmaktadır. FAT dosya sistemine ilişkin bir uygulama yazabilmek için yapılacak ilk şey boot sektörü okuyup buradaki BPB bilgilerini bir yapı nesnesinin içerisine yerleştirmektir. Bu bilgilerden hareketle bizim FAT dosya sistemine ilişkin meta data alanlarının ilgili disk bölümünün kaçıncı sektöründen başlayıp kaç sektör uzunluğunda olduğunu elde etmemiz gerekir. Çünkü dosya sistemi ile ilgili işlemlerin hepsinde bu bilgilere gereksinim duyulacaktır. Bu bilgilerin yerleştirileceği yapı şöyle olabilir: typedef struct tagBPB { uint16_t fatlen; /* Number of sectors in FAT (A) */ uint16_t rootlen; /* Number of sectors in ROOT (NA) */ uint16_t nfats; /* Number of copies of FAT (A) */ uint32_t tsects; /* Total sector (A) */ uint16_t bps; /* Byte per sector(A) */ uint16_t spc; /* Sector per cluster(A) */ uint16_t rsects; /* Reserved sectors(A) */ uint8_t mdes; /* Media descriptor byte(A) */ uint16_t spt; /* Sector per track(A) */ uint16_t rootents; /* Root entry (A) */ uint16_t nheads; /* Number of heads (A) */ uint16_t hsects; /* Number of hidden sector( A) */ uint16_t tph; /* Track per head (NA) */ uint16_t fatloc; /* FAT directory location (NA) */ uint16_t rootloc; /* Root directory location (NA) */ uint16_t dataloc; /* First data sector location (NA) */ uint32_t datalen; /* Number of sectors in Data (NA) */ uint32_t serial; /* Volume Serial Number (A) */ char vname[12]; /* Volume Name (A) */ } BPB; Burada (A) ile belirtilen elemanlar zaten BPB içerisinde olan (available) elemanlardır. NA (not available) ile belirtilen elemanlar BPB içerisinde yoktur. Dört işlemle hesaplanarak değeri oluşturulacaktır. Linux'ta boot sektör'ü okuyarak oradaki BPB bilgilerini yukarıdaki gibi bir yapıya yerleştiren örnek bir program aşağıda verilmiştir. Derlemeyi şöyle yapabilirsiniz: $ gcc -o app fatsys.c app.c Programı FAT dosya sistemine ilişkin blok aygıt dosyasının yol ifadesini vererek sudo ile çalıştırabilirsiniz. Örneğin: $ sudo ./app /dev/loop0 Aşağıdakine benzer bir çıktı elde edilecektir: Byte per sector: 512 Sector per cluster: 4 Number of reserved sectors: 4 Number of FAT copies: 100 Number of sectors in Root Dir: 32 Number of FAT copies: 2 Number of sectors in volume: 100000 Media Descriptor: F8 Number of Root Dir entries: 200 Number of hidden sectors: 0 FAT location: 4 Root Dir location: 204 Data location: 236 Volume Serial Number: BC7B-4578 Volume Name: FAT16 Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, Biz aşağıdaki örnekte FAT dosya sistemine ilişkin tüm önemli alanların yerlerine ve uzunluklarına ilişkin bilgileri elde ederek bir yapıya yerleştirdik. Artık şu bilgilere sahibiz: -> FAT bölümünün yeri ve uzunluğu (yapının fatloc ve fatlen elemanları) -> Root DIR bölümünün yeri ve uzunluğu (yapının rootloc ve rootlen elemanları) -> Data bölümünün yeri ve uzunluğu (yapının dataloc ve datalen elemanları) Programın kodları: /* fatsys.h */ #ifndef FATSYS_H_ #define FATSYS_H_ #include #define FILE_INFO_LENGTH 32 /* Type Declarations */ typedef struct tagBPB { uint16_t fatlen; /* Number of sectors in FAT (A) */ uint16_t rootlen; /* Number of sectors in ROOT (NA) */ uint16_t nfats; /* Number of copies of FAT (A) */ uint32_t tsects; /* Total sector (A) */ uint16_t bps; /* Byte per sector(A) */ uint16_t spc; /* Sector per cluster(A) */ uint16_t rsects; /* Reserved sectors(A) */ uint8_t mdes; /* Media descriptor byte(A) */ uint16_t spt; /* Sector per track(A) */ uint16_t rootents; /* Root entry (A) */ uint16_t nheads; /* Number of heads (A) */ uint16_t hsects; /* Number of hidden sector( A) */ uint16_t tph; /* Track per head (NA) */ uint16_t fatloc; /* FAT directory location (NA) */ uint16_t rootloc; /* Root directory location (NA) */ uint16_t dataloc; /* First data sector location (NA) */ uint32_t datalen; /* Number of sectors in Data (NA) */ uint32_t serial; /* Volume Serial Number (A) */ char vname[12]; /* Volume Name (A) */ } BPB; /* Function prototypes */ int read_bpb(int fd, BPB *bpb); #endif /* fatsys.c */ #include #include #include #include #include #include "fatsys.h" int read_bpb(int fd, BPB *bpb) { uint8_t bsec[512]; if (read(fd, bsec, 512) == -1) return -1; bpb->bps = *(uint16_t *)(bsec + 0x0B); bpb->spc = *(uint8_t *)(bsec + 0x0D); bpb->rsects = *(uint16_t *)(bsec + 0x0E); bpb->fatlen = *(uint16_t *)(bsec + 0x16); bpb->rootlen = *(uint16_t *)(bsec + 0x11) * FILE_INFO_LENGTH / bpb->bps; bpb->nfats = *(uint8_t *)(bsec + 0x10); if (*(uint16_t *)(bsec + 0x13)) bpb->tsects = *(uint16_t *)(bsec + 0x13); else bpb->tsects = *(uint32_t *)(bsec + 0x20); bpb->mdes = *(bsec + 0x15); bpb->spt = *(uint16_t *)(bsec + 0x18); bpb->rootents = *(uint16_t *)(bsec + 0x11); bpb->nheads = *(uint16_t *)(bsec + 0x1A); bpb->hsects = *(uint16_t *)(bsec + 0x1C); bpb->tph = (uint16_t)(bpb->tsects / bpb->spt / bpb->nheads); bpb->fatloc = bpb->rsects; bpb->rootloc = bpb->rsects + bpb->fatlen *bpb->nfats; bpb->dataloc = bpb->rootloc + bpb->rootlen; bpb->datalen = bpb->tsects - bpb->dataloc; bpb->serial = *(uint32_t *)(bsec + 0x27); memcpy(bpb->vname, bsec + 0x2B, 11); bpb->vname[11] = '\0'; return 0; } /* app.c */ #include #include #include #include #include "fatsys.h" void exit_sys(const char *msg); int main(int argc, char *argv[]) { BPB bpb; int fd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDWR)) == -1) return -1; if (read_bpb(fd, &bpb) == -1) exit_sys("read_bpb"); printf("Byte per sector: %d\n", bpb.bps); printf("Sector per cluster: %d\n", bpb.spc); printf("Number of reserved sectors: %d\n", bpb.rsects); printf("Number of FAT copies: %d\n", bpb.fatlen); printf("Number of sectors in Root Dir: %d\n", bpb.rootlen); printf("Number of FAT copies: %d\n", bpb.nfats); printf("Number of sectors in volume: %u\n", bpb.tsects); printf("Media Descriptor: %02X\n", bpb.mdes); printf("Number of Root Dir entries: %02X\n", bpb.rootents); printf("Number of hidden sectors: %d\n", bpb.hsects); printf("FAT location: %d\n", bpb.fatloc); printf("Root Dir location: %d\n", bpb.rootloc); printf("Data location: %d\n", bpb.dataloc); printf("Number of sectors in Data: %d\n", bpb.datalen); printf("Volume Serial Number: %04X-%04X\n", bpb.serial >> 16, 0xFFFF & bpb.serial); printf("Volume Name: %s\n", bpb.vname); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Şimdi yukarıdaki yapıp biraz daha geliştirelim. Bunun için dosya sistemini temsil eden aşağıdaki gibi bir yapı oluşturabiliriz: typedef struct tagFATSYS { int fd; /* Volume file descriptor */ BPB bpb; /* BPB info */ uint32_t fatoff; /* Offset of FAT */ uint32_t rootoff; /* Offset of root directory */ uint32_t dataoff; /* Offset of DATA */ uint32_t clulen; /* Cluster length as bytes */ /* ... */ } FATSYS; Dosya işlemi yaparken dosya sisteminin belirli bölümlerine konumlandırma yapacağımız için onların offset'lerini de FATSYS yapısının içerisine yerleştireceğiz. Dosya sistemini açan ve kapatan aşağıdaki fonksiyonlar oluşturabiliriz: FATSYS *open_fatsys(const char *path) { FATSYS *fatsys; int fd; if ((fd = open(path, O_RDWR)) == -1) return NULL; if ((fatsys = (FATSYS *)malloc(sizeof(FATSYS))) == NULL) return NULL; if (read_bpb(fd, &fatsys->bpb) == -1) { free(fatsys); return NULL; } fatsys->fd = fd; fatsys->fatoff = fatsys->bpb.fatloc * fatsys->bpb.bps; fatsys->rootoff = fatsys->bpb.rootloc * fatsys->bpb.bps; fatsys->dataoff = fatsys->bpb.dataloc * fatsys->bpb.bps; fatsys->clulen = fatsys->bpb.bps * fatsys->bpb.spc; return fatsys; } int close_fatsys(FATSYS *fatsys) { if (close(fatsys->fd) == -1) return -1; free(fatsys); return 0; } Kullanım şöyle olabilir: FATSYS *fatsys; if ((fatsys = open_fatsys("/dev/loop0")) == NULL) exit_sys("open_fatsys"); close_fatsys(fatsys); Aşağıda bu değişliklerin yapıldığı kodlar verilmiştir. * Örnek 1, /* fatsys.h */ #ifndef FATSYS_H_ #define FATSYS_H_ #include #define FILE_INFO_LENGTH 32 /* Type Declarations */ typedef struct tagBPB { uint16_t fatlen; /* Number of sectors in FAT (A) */ uint16_t rootlen; /* Number of sectors in ROOT (NA) */ uint16_t nfats; /* Number of copies of FAT (A) */ uint32_t tsects; /* Total sector (A) */ uint16_t bps; /* Byte per sector(A) */ uint16_t spc; /* Sector per cluster(A) */ uint16_t rsects; /* Reserved sectors(A) */ uint8_t mdes; /* Media descriptor byte(A) */ uint16_t spt; /* Sector per track(A) */ uint16_t rootents; /* Root entry (A) */ uint16_t nheads; /* Number of heads (A) */ uint16_t hsects; /* Number of hidden sector( A) */ uint16_t tph; /* Track per head (NA) */ uint16_t fatloc; /* FAT directory location (NA) */ uint16_t rootloc; /* Root directory location (NA) */ uint16_t dataloc; /* First data sector location (NA) */ uint32_t datalen; /* Number of sectors in Data (NA) */ uint32_t serial; /* Volume Serial Number (A) */ char vname[12]; /* Volume Name (A) */ } BPB; typedef struct tagFATSYS { int fd; /* Volume file descriptor */ BPB bpb; /* BPB info */ uint32_t fatoff; /* Offset of FAT */ uint32_t rootoff; /* Offset of root directory */ uint32_t dataoff; /* Offset of DATA */ uint32_t clulen; /* Cluster length as bytes */ /* ... */ } FATSYS; /* Function prototypes */ int read_bpb(int fd, BPB *bpb); FATSYS *open_fatsys(const char *path); int close_fatsys(FATSYS *fatsys); int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf); int wite_cluster(FATSYS *fatsys, uint32_t clu, const void *buf); #endif /* fatsys.c */ FATSYS *open_fatsys(const char *path) { FATSYS *fatsys; int fd; if ((fd = open(path, O_RDWR)) == -1) return NULL; if ((fatsys = (FATSYS *)malloc(sizeof(FATSYS))) == NULL) return NULL; if (read_bpb(fd, &fatsys->bpb) == -1) { free(fatsys); return NULL; } fatsys->fd = fd; fatsys->fatoff = fatsys->bpb.fatloc * fatsys->bpb.bps; fatsys->rootoff = fatsys->bpb.rootloc * fatsys->bpb.bps; fatsys->dataoff = fatsys->bpb.dataloc * fatsys->bpb.bps; fatsys->clulen = fatsys->bpb.bps * fatsys->bpb.spc; return fatsys; } int close_fatsys(FATSYS *fatsys) { if (close(fatsys->fd) == -1) return -1; free(fatsys); return 0; } /* app.c */ #include #include #include #include #include "fatsys.h" void exit_sys(const char *msg); int main(int argc, char *argv[]) { FATSYS *fatsys; if ((fatsys = open_fatsys("/dev/loop0")) == NULL) exit_sys("open_fatsys"); close_fatsys(fatsys); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } FAT dosya sisteminde dosya sistemindeki "Data Bölümü" dosya içeriklerinin tutulduğu bölümdür. İşletim sistemi bu bölümün sektörlerden değil cluster'lardan oluştuğunu varsaymaktadır. Anımsanacağı gibi "cluster" bir dosyanın parçası olabilecek en küçük tahsisat birimidir ve ardışıl n sektörden olulmaktadır. Buradaki n değeri 2'nin bir kuvvetidir (yani 1, 2, 4, 8, ... biçiminde). İşte volümün Data bölümündeki her cluster'a 2'den başlanarak (0 ve 1 reserved bırakılmıştır) bir cluster numarası karşı getirilmiştir. Örneğin bir cluster'ın 4 sektörden oluştuğunu düşünelim. Bu durumda Data bölümünün ilk 4 sektörü 2 numaralı cluster, sonraki 4 sektörü 3 numaralı cluster, sonraki 4 sektörü 4 numaralı cluster biçiminde numaralanmaktadır. Bizim FAT dosya sistemi üzerinde ilk yapmaya çalışacağımız alt seviye işlemlerden biri belli bir numaralı cluster'ı okuyup yazan fonksiyonları gerçekleştirmektir. Bu fonksiyonların prototipleri şöyle olabilir: int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf); int wite_cluster(FATSYS *fatsys, uint32_t clu, const void *buf); Data bölümünün ilk cluster'ının 2 numaralı cluster olduğunu 0, 1 cluster'larının kullanılmadığını anımsayınız. Bu fonksiyonlar basit biçimde şöyle yazılabilir: int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf) { if (lseek(fatsys->fd, fatsys->dataoff + (clu - 2) * fatsys->clulen, SEEK_SET) == -1) return -1; return read(fatsys->fd, buf, fatsys->clulen); } int write_cluster(FATSYS *fatsys, uint32_t clu, const void *buf) { if (lseek(fatsys->fd, fatsys->dataoff + (clu - 2) * fatsys->clulen, SEEK_SET) == -1) return -1; return write(fatsys->fd, buf, fatsys->clulen); } Aşağıda fonksiyonun kullanımına ilişkin bir örnek verilmiştir. * Örnek 1, /* fstsys.h */ #ifndef FATSYS_H_ #define FATSYS_H_ #include #define FILE_INFO_LENGTH 32 /* Type Declarations */ typedef struct tagBPB { uint16_t fatlen; /* Number of sectors in FAT (A) */ uint16_t rootlen; /* Number of sectors in ROOT (NA) */ uint16_t nfats; /* Number of copies of FAT (A) */ uint32_t tsects; /* Total sector (A) */ uint16_t bps; /* Byte per sector(A) */ uint16_t spc; /* Sector per cluster(A) */ uint16_t rsects; /* Reserved sectors(A) */ uint8_t mdes; /* Media descriptor byte(A) */ uint16_t spt; /* Sector per track(A) */ uint16_t rootents; /* Root entry (A) */ uint16_t nheads; /* Number of heads (A) */ uint16_t hsects; /* Number of hidden sector( A) */ uint16_t tph; /* Track per head (NA) */ uint16_t fatloc; /* FAT directory location (NA) */ uint16_t rootloc; /* Root directory location (NA) */ uint16_t dataloc; /* First data sector location (NA) */ uint32_t datalen; /* Number of sectors in Data (NA) */ uint32_t serial; /* Volume Serial Number (A) */ char vname[12]; /* Volume Name (A) */ } BPB; typedef struct tagFATSYS { int fd; /* Volume file descriptor */ BPB bpb; /* BPB info */ uint32_t fatoff; /* Offset of FAT */ uint32_t rootoff; /* Offset of root directory */ uint32_t dataoff; /* Offset of DATA */ uint32_t clulen; /* Cluster length as bytes */ /* ... */ } FATSYS; /* Function prototypes */ int read_bpb(int fd, BPB *bpb); FATSYS *open_fatsys(const char *path); int close_fatsys(FATSYS *fatsys); int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf); int wite_cluster(FATSYS *fatsys, uint32_t clu, const void *buf); #endif /* fatsys.c */ #include #include #include #include #include #include "fatsys.h" int read_bpb(int fd, BPB *bpb) { uint8_t bsec[512]; if (read(fd, bsec, 512) == -1) return -1; bpb->bps = *(uint16_t *)(bsec + 0x0B); bpb->spc = *(uint8_t *)(bsec + 0x0D); bpb->rsects = *(uint16_t *)(bsec + 0x0E); bpb->fatlen = *(uint16_t *)(bsec + 0x16); bpb->rootlen = *(uint16_t *)(bsec + 0x11) * FILE_INFO_LENGTH / bpb->bps; bpb->nfats = *(uint8_t *)(bsec + 0x10); if (*(uint16_t *)(bsec + 0x13)) bpb->tsects = *(uint16_t *)(bsec + 0x13); else bpb->tsects = *(uint32_t *)(bsec + 0x20); bpb->mdes = *(bsec + 0x15); bpb->spt = *(uint16_t *)(bsec + 0x18); bpb->rootents = *(uint16_t *)(bsec + 0x11); bpb->nheads = *(uint16_t *)(bsec + 0x1A); bpb->hsects = *(uint16_t *)(bsec + 0x1C); bpb->tph = (uint16_t)(bpb->tsects / bpb->spt / bpb->nheads); bpb->fatloc = bpb->rsects; bpb->rootloc = bpb->rsects + bpb->fatlen *bpb->nfats; bpb->dataloc = bpb->rootloc + bpb->rootlen; bpb->datalen = bpb->tsects - bpb->dataloc; bpb->serial = *(uint32_t *)(bsec + 0x27); memcpy(bpb->vname, bsec + 0x2B, 11); bpb->vname[11] = '\0'; return 0; } FATSYS *open_fatsys(const char *path) { FATSYS *fatsys; int fd; if ((fatsys = (FATSYS *)malloc(sizeof(FATSYS))) == NULL) return NULL; if ((fd = open(path, O_RDWR)) == -1) return NULL; if (read_bpb(fd, &fatsys->bpb) == -1) { close(fd); free(fatsys); return NULL; } fatsys->fd = fd; fatsys->fatoff = fatsys->bpb.fatloc * fatsys->bpb.bps; fatsys->rootoff = fatsys->bpb.rootloc * fatsys->bpb.bps; fatsys->dataoff = fatsys->bpb.dataloc * fatsys->bpb.bps; fatsys->clulen = fatsys->bpb.bps * fatsys->bpb.spc; return fatsys; } int close_fatsys(FATSYS *fatsys) { if (close(fatsys->fd) == -1) return -1; free(fatsys); return 0; } int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf) { if (lseek(fatsys->fd, fatsys->dataoff + (clu - 2) * fatsys->clulen, SEEK_SET) == -1) return -1; return read(fatsys->fd, buf, fatsys->clulen); } int write_cluster(FATSYS *fatsys, uint32_t clu, const void *buf) { if (lseek(fatsys->fd, fatsys->dataoff + (clu - 2) * fatsys->clulen, SEEK_SET) == -1) return -1; return write(fatsys->fd, buf, fatsys->clulen); } /* app.c */ #include #include #include #include #include "fatsys.h" void exit_sys(const char *msg); int main(int argc, char *argv[]) { FATSYS *fatsys; unsigned char buf[8192]; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fatsys = open_fatsys(argv[1])) == NULL) exit_sys("open_fatsys"); if (read_cluster(fatsys, 2, buf) == -1) exit_sys("read_cluster"); for (int i = 0; i < fatsys->clulen; ++i) printf("%02X%c", buf[i], i % 16 == 15 ? '\n' : ' '); close_fatsys(fatsys); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } FAT dosya sisteminde her dosya cluster'lara bölünerek Data bölümündeki cluster'larda tutulmaktadır. Dosyanın parçaları ardışıl cluster'larda olmak zorunda değildir. Örneğin bir cluster'ın 4 sektör olduğu bir volümde 10000 byte uzunluğunda bir dosya söz konusu olsun. Bir cluster'ın bıyutu 4 * 512 = 2048 byte'tır. O halde bu dosya 5 cluster yer kaplayacaktır. Ancak son cluster'da kullanılmayan bir miktar boş alan da kalacaktır. İşte örneğin bu dosyanın cluster numaraları aşağıdaki gibi olabilir: 2 8 14 15 21 Görüldüğü gibi dosyanın parçaları ardışıl cluster'larda olmak zorunda değildir. Tabi işletim sistemi genellikle dosyanın parçalarını mümkün olduğu kadar ardışıl cluster'larda saklama çalışır. Ancak bu durum mümkün olmayabilir. Belli bir süre sonra artık dosyaların parçalarını birbirinden uzaklaşmaya başlayabilir. İşte FAT dosya sisteminde hangi dosyanın hangi parçalarının Data bölümünün hangi cluster'larında olduğunun saklandığı meta data alana FAT (File Allocation Table) denilmektedir. FAT bölümü FAT elemanlarından oluşur. FAT'lar 12 bit 16 bit ve 32 bit olmak üzere üçe ayrılmaktadır. 12 bit FAT'lerde FAT elemanları 12 bit, 16 bit FAT'lerde FAT elemanları 16 bit ve 32 bit FAT'lerde FAT elemanları 32 bit uzunluğundadır. İlk iki cluster kullanılmadığı için FAT'in ilk elemanı da kullanılmaktadır. FAT bağlı listelerden oluşan bir meta data alanıdır. Her dosyanın ilk cluster'ının nerede olduğu dizin girişinde tutulmaktadır. Sonra her FAT elemanı dosyanın ğparçasının hangi cluster'da olduğu bilgisini tıtar. Volümde toplan N tane cluster varsa FAT bölümünde de toplam N tane FAT elemanı vardır. FAT bölümünde her bir dosya için ayrı bir bağlı liste bulunmaktadır. Bir dosyanın ilk cluster'ı biliniyorsa sonraki tüm cluster'ları bu bağlı liste izlenerek elde edilebilmektedir. Bağlı listenin organizasyonu şu biçimdedir: Dosyanın ilk cluster'ının yerinin 8 olduğunu varsayalım. Şimdi FAT'in 8'inci elemanına gidildiğinde orada 14 yazıyor olsun. 14 numaralı elemanına gittiğimizde orada 18 yazdığını düşünelim. 18 elemana gittiğimizde orada 22 yadığını düşünelim. Nihayet 22 numaralı elemana gittiğimizde orada FFFF biçiminde özel bir değerin yazdığını varsayalım. Bu durumu şekilsel olarak şöyle gösterebiliriz: 8 ----> 14 ----> 18 ----> 22 (FFFF) Bu durumda bu dosyanın cluster'ları sırasıyla 8 14 18 22 numaralı cluster'lardır. Burada FFFF değeri EOF anlamına özel bir cluster numarasıdır. Yani FAT'teki her FAT elemanı dosyanın sonraki parçasının hangi cluster'da olduğunu belirtmektedir. Böylece işletim sistemi dosyanın ilk cluster numarasını biliyorsa bu zinciri takip ederek onun bütün cluster'larını elde edebilir. Örneğin 1 cluster'ın 4 sektör olduğu bir FAT16 sisteminde 19459 byte'lık bir dosya toplam 10 cluster yer kaplamaktadır. Biz bu dosyanın ilk cluster numarasının 4 olduğunu biliyoruz. Aşağıdaki örnek FAT bölümünde bu dosyanın tüm cluster'larının numaraları bağlı liste izlenerek elde edilebilecektir: 00000800 f8 ff ff ff 00 00 ff ff 05 00 06 00 07 00 08 00 |................| 00000810 09 00 0a 00 0b 00 0c 00 0d 00 ff ff 00 00 00 00 |................| 00000820 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000830 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000840 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000850 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| ... Bu byte'lar bir FAT16 sisteminin FAT bölümüne ilişkin olduğuna göre her bir FAT elemanı 2 byte yer kaplayacaktır. Burada FAT elemanlarının hex karşılıkları şöyledir (Little Endian notasyon kullanıldığına dikkat ediniz): 0 1 2 3 4 5 6 7 8 9 10 11 12 13 <0000> <0005> <0006> <0007> <0008> <0009> <000A> <000B> <000C> <000D> Burada FAT elemanlarının umaralarını desimal sistemde elemanların yukarısına yazdık. Söz konusu dosyanın ilk cluster numarasının 4 olduğunu bildiğimizi varsayıyoruz. 4 numaralı FAT elemanında 5 (0005) yazmaktadır. O halde dosyanın sonraki cluster numarası 5'tir. 5 numaralı FAT elemanında 6 (0006) yazmaktadır. 6 numaralı FAT elemanında 7 (0007), 7 numaralı FAT elemanında 8 (0008), 8 numaralı FAT elemanında 9 (0009), 9 numaralı FAT elemanında 10 (000A), 10 numaralı FAT elemanında 11 (000B), 11 numaralı FAT elemanında 12 (0000D), 12 numaralı FAT elemanında 13 (000D), 13 FAT elemanında da özel değer olan 65535 (FFFF) bulunmaktadır. Bu özel değer zinicirin sonuna gelindiğini belirtmektedir. Bu durumda bu dosyanın tüm parçaları sırasıyla şu cluster'lardadır: 4 5 6 7 8 9 10 11 12 13 Burada işletim sisteminin dosyanın parçalarını diskte ardışışıl cluster'lara yerleştirdiğini görüyorsunuz. Ancak bu durum her zaman böyle olma zorunda değildir. 16 bir FAT'te bir FAT elemanında bulunacak değerler şunlar olabilmektedir (değerler little Endian olarak WORD'e dönüştürülmüştür): 0000 Boş cluster 0001 Kullanılmıyor 0002 - FFEF Geçerli, sonraki cluster FFF0H - FFF6 Reserved cluster FFF7 Bozuk cluster, işletim sistemi bu cluster'a dosya parçası yerleştirmez FFF8 - FFFF Son cluster Pekiyi işletim sistemleri FAT bölümünü nasıl ele alıp işlemektedir? Aslında FAT bölümündeki sektörler zaten çok kullanıldığı için işletim sisteminin aşağı seviyeli disk cache sisteminde bulunuyor durumda olurlar. Ancak işletim sistemleri genellikle FAT elemanları temelinde de bir cache sistemi de oluşturmaktadır. Böylece bir cluster değeri verildiğinde eğer daha önce o cluster ile işlemler yapılmışsa o cluster'ın sonraki cluster'ı hızlı bir biçimde elde edilebilmektedir. Biz burada volümü açtığımızda tüm FAT bölümünü okuyup FATSYS yapısının içerisine yerleştireceğiz. Sonra da ilk cluster numarası bilinen dosyaların cluster zincirini elde eden bir fonksiyon yazacağız. openfat_sys fonksiyonunun yeni versiyonu aşağıdaki gibi olabilir: FATSYS *open_fatsys(const char *path) { FATSYS *fatsys; int fd; if ((fatsys = (FATSYS *)malloc(sizeof(FATSYS))) == NULL) return NULL; if ((fd = open(path, O_RDWR)) == -1) goto EXIT1; if (read_bpb(fd, &fatsys->bpb) == -1) goto EXIT2; fatsys->fd = fd; fatsys->fatoff = fatsys->bpb.fatloc * fatsys->bpb.bps; fatsys->rootoff = fatsys->bpb.rootloc * fatsys->bpb.bps; fatsys->dataoff = fatsys->bpb.dataloc * fatsys->bpb.bps; fatsys->clulen = fatsys->bpb.bps * fatsys->bpb.spc; if ((fatsys->fat = (uint8_t *)malloc(fatsys->bpb.fatlen * fatsys->bpb.bps)) == NULL) goto EXIT2; if (lseek(fatsys->fd, fatsys->fatoff, SEEK_SET) == -1) goto EXIT3; if (read(fd, fatsys->fat, fatsys->bpb.fatlen * fatsys->bpb.bps) == -1) goto EXIT3; return fatsys; EXIT3: free(fatsys->fat); EXIT2: close(fd); EXIT1: free(fatsys); return NULL; } İlk cluster numarası bilinen dosyanın cluster zincirini elde eden fonksiyon da aşağıdaki gibi yazılabilir: uint16_t *getclu_chain16(FATSYS *fatsys, uint32_t firstclu, uint16_t *count) { uint16_t clu, n; uint16_t *chain, *temp; uint32_t capacity; clu = firstclu; capacity = CHAIN_DEF_CAPACITY; n = 0; if ((chain = (uint16_t *)malloc(sizeof(uint16_t) * CHAIN_DEF_CAPACITY)) == NULL) return NULL; do { chain[n++] = clu; if (n == capacity) { capacity *= 2; if ((temp = realloc(chain, sizeof(uint16_t) * capacity )) == NULL) { free(chain); return NULL; } chain = temp; } clu = *(uint16_t *)(fatsys->fat + clu * 2); } while (clu < 0xFFF8); *count = n; return chain; } Bu fonksiyonda dosyanın cluster zinciri için uint16_t türünden dinamik büyütülen bir dizi oluşturulmuştur. Dizi eski uzunluğunun iki katı olacak biçimde büyütülmektedir. Fonksiyon bize cluster zincirini vermekte hem de bu zincirin uzunluğunu vermektedir. Aşağıda tüm kodlar bütün olarak verilmiştir. * Örnek 1, /* fatsys.h */ #ifndef FATSYS_H_ #define FATSYS_H_ #include #define FILE_INFO_LENGTH 32 #define CHAIN_DEF_CAPACITY 8 /* Type Declarations */ typedef struct tagBPB { uint16_t fatlen; /* Number of sectors in FAT (A) */ uint16_t rootlen; /* Number of sectors in ROOT (NA) */ uint16_t nfats; /* Number of copies of FAT (A) */ uint32_t tsects; /* Total sector (A) */ uint16_t bps; /* Byte per sector(A) */ uint16_t spc; /* Sector per cluster(A) */ uint16_t rsects; /* Reserved sectors(A) */ uint8_t mdes; /* Media descriptor byte(A) */ uint16_t spt; /* Sector per track(A) */ uint16_t rootents; /* Root entry (A) */ uint16_t nheads; /* Number of heads (A) */ uint16_t hsects; /* Number of hidden sector( A) */ uint16_t tph; /* Track per head (NA) */ uint16_t fatloc; /* FAT directory location (NA) */ uint16_t rootloc; /* Root directory location (NA) */ uint16_t dataloc; /* First data sector location (NA) */ uint32_t datalen; /* Number of sectors in Data (NA) */ uint32_t serial; /* Volume Serial Number (A) */ char vname[12]; /* Volume Name (A) */ } BPB; typedef struct tagFATSYS { int fd; /* Volume file descriptor */ BPB bpb; /* BPB info */ uint32_t fatoff; /* Offset of FAT */ uint32_t rootoff; /* Offset of root directory */ uint32_t dataoff; /* Offset of DATA */ uint32_t clulen; /* Cluster length as bytes */ uint8_t *fat; /* FAT sectors */ /* ... */ } FATSYS; /* Function prototypes */ int read_bpb(int fd, BPB *bpb); FATSYS *open_fatsys(const char *path); int close_fatsys(FATSYS *fatsys); int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf); int wite_cluster(FATSYS *fatsys, uint32_t clu, const void *buf); uint16_t *getclu_chain16(FATSYS *fatsys, uint32_t firstclu, uint16_t *count); void freeclu_chain(uint16_t *chain); #endif /* fatsys.c */ #include #include #include #include #include #include "fatsys.h" int read_bpb(int fd, BPB *bpb) { uint8_t bsec[512]; if (read(fd, bsec, 512) == -1) return -1; bpb->bps = *(uint16_t *)(bsec + 0x0B); bpb->spc = *(uint8_t *)(bsec + 0x0D); bpb->rsects = *(uint16_t *)(bsec + 0x0E); bpb->fatlen = *(uint16_t *)(bsec + 0x16); bpb->rootlen = *(uint16_t *)(bsec + 0x11) * FILE_INFO_LENGTH / bpb->bps; bpb->nfats = *(uint8_t *)(bsec + 0x10); if (*(uint16_t *)(bsec + 0x13)) bpb->tsects = *(uint16_t *)(bsec + 0x13); else bpb->tsects = *(uint32_t *)(bsec + 0x20); bpb->mdes = *(bsec + 0x15); bpb->spt = *(uint16_t *)(bsec + 0x18); bpb->rootents = *(uint16_t *)(bsec + 0x11); bpb->nheads = *(uint16_t *)(bsec + 0x1A); bpb->hsects = *(uint16_t *)(bsec + 0x1C); bpb->tph = (uint16_t)(bpb->tsects / bpb->spt / bpb->nheads); bpb->fatloc = bpb->rsects; bpb->rootloc = bpb->rsects + bpb->fatlen *bpb->nfats; bpb->dataloc = bpb->rootloc + bpb->rootlen; bpb->datalen = bpb->tsects - bpb->dataloc; bpb->serial = *(uint32_t *)(bsec + 0x27); memcpy(bpb->vname, bsec + 0x2B, 11); bpb->vname[11] = '\0'; return 0; } FATSYS *open_fatsys(const char *path) { FATSYS *fatsys; int fd; if ((fatsys = (FATSYS *)malloc(sizeof(FATSYS))) == NULL) return NULL; if ((fd = open(path, O_RDWR)) == -1) goto EXIT1; if (read_bpb(fd, &fatsys->bpb) == -1) goto EXIT2; fatsys->fd = fd; fatsys->fatoff = fatsys->bpb.fatloc * fatsys->bpb.bps; fatsys->rootoff = fatsys->bpb.rootloc * fatsys->bpb.bps; fatsys->dataoff = fatsys->bpb.dataloc * fatsys->bpb.bps; fatsys->clulen = fatsys->bpb.bps * fatsys->bpb.spc; if ((fatsys->fat = (uint8_t *)malloc(fatsys->bpb.fatlen * fatsys->bpb.bps)) == NULL) goto EXIT2; if (lseek(fatsys->fd, fatsys->fatoff, SEEK_SET) == -1) goto EXIT3; if (read(fd, fatsys->fat, fatsys->bpb.fatlen * fatsys->bpb.bps) == -1) goto EXIT3; return fatsys; EXIT3: free(fatsys->fat); EXIT2: close(fd); EXIT1: free(fatsys); return NULL; } int close_fatsys(FATSYS *fatsys) { free(fatsys->fat); if (close(fatsys->fd) == -1) return -1; free(fatsys); return 0; } int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf) { if (lseek(fatsys->fd, fatsys->dataoff + (clu - 2) * fatsys->clulen, SEEK_SET) == -1) return -1; return read(fatsys->fd, buf, fatsys->clulen); } int write_cluster(FATSYS *fatsys, uint32_t clu, const void *buf) { if (lseek(fatsys->fd, fatsys->dataoff + (clu - 2) * fatsys->clulen, SEEK_SET) == -1) return -1; return write(fatsys->fd, buf, fatsys->clulen); } uint16_t *getclu_chain16(FATSYS *fatsys, uint32_t firstclu, uint16_t *count) { uint16_t clu, n; uint16_t *chain, *temp; uint32_t capacity; clu = firstclu; capacity = CHAIN_DEF_CAPACITY; n = 0; if ((chain = (uint16_t *)malloc(sizeof(uint16_t) * CHAIN_DEF_CAPACITY)) == NULL) return NULL; do { chain[n++] = clu; if (n == capacity) { capacity *= 2; if ((temp = realloc(chain, sizeof(uint16_t) * capacity )) == NULL) { free(chain); return NULL; } chain = temp; } clu = *(uint16_t *)(fatsys->fat + clu * 2); } while (clu < 0xFFF8); *count = n; return chain; } void freeclu_chain(uint16_t *chain) { free(chain); } /* app.c */ #include #include #include #include #include "fatsys.h" void exit_sys(const char *msg); int main(int argc, char *argv[]) { FATSYS *fatsys; uint16_t count; uint16_t *chain; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fatsys = open_fatsys(argv[1])) == NULL) exit_sys("open_fatsys"); if ((chain = getclu_chain16(fatsys, 4, &count)) == NULL) { fprintf(stderr, "cannot get cluster chain!...\n"); exit(EXIT_FAILURE); } printf("Number of clusters in file: %u\n", count); for (uint16_t i = 0; i < count; ++i) printf("%u ", chain[i]); printf("\n"); freeclu_chain(chain); close_fatsys(fatsys); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Biz şimdi ilk cluster'ını bildiğimiz bir text dosyanın içeriğini yazdırmak isteyelim. Bunun için önce getclu_chain16 fonksiyonunu çağırırız. Sonra read_cluster fonksiyonu ile cluster'ları okuyup içini yazdırabiliriz. Ancak burada şöyle bir sorun vardır: Dosyanın son cluster'ı tıka basa dolu değildir. Orada dosyaya dahil olmayan bye'lar da vardır. İşletim sistemi dosyanın uzunluğunu elde edip son cluster'daki dosyaya dahil olmayan kısmı belirleyebilmektedir. Aşağıda ilk cluster'ı bilinen bir text dosyanın yazdırılmasına yönelik bir örnek verilmiştir. Burada dosyanın son cluster'ındaki dosyaya ait olmayan kısım da yazdırılmaktadır. * Örnek 1, /* app.c */ #include #include #include #include #include "fatsys.h" void exit_sys(const char *msg); int main(int argc, char *argv[]) { FATSYS *fatsys; unsigned char buf[8192]; uint16_t count; uint16_t *chain; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fatsys = open_fatsys(argv[1])) == NULL) exit_sys("open_fatsys"); if ((chain = getclu_chain16(fatsys, 4, &count)) == NULL) { fprintf(stderr, "cannot get cluster chain!...\n"); exit(EXIT_FAILURE); } for (uint16_t i = 0; i < count; ++i) { if (read_cluster(fatsys, chain[i], buf) == -1) exit_sys("read_cluster"); for (int i = 0; i < fatsys->clulen; ++i) putchar(buf[i]); } freeclu_chain(chain); close_fatsys(fatsys); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } İşletim sisteminin dosya sistemi bize aslında cluster'larda olan dosya parçalarını "dosya" adı altında ardışıl byte topluluğu gibi göstermektedir. Biz işletim sisteminin sistem fonksiyonu ile dosyayı açarız ve read fonksiyonu ile okumayı yaparız. Bütün diğer işlemler işletim sisteminin çekirdek kodları tarafından yapılmaktadır. Biz yukarıda 16 bit FAT için işlemler yaptık. Pekiyi 12 bit ve 32 bit FAT bölüm nasıldır? 32 bit FAT önemli bir farklılığa sahip değildir. Her FAT elemanı 32 bit yani 4 byte uzunluktadır. Dolayısıyla daha büyük bir volüm için kullanılabilir. 16 bit FAT'te toplam 65536 FAT elemanı elemanı olabilir (Bazılarının kullanılmadığını da anımsayınız.) Bir cluster en fazla 64 sektör uzunluğunda olabilmektedir. Bu durumda FAT16 sistremlerinde volümün maksimum uzunluğu 2^16 * 2^6 * 2^9 = 2GB. 12 Bit FAT'ler biraz daha karmaşık görünümdedir. 12 bit 8'in katı değildir ve 3 hex digitle temsil edilmektedir. Bu nedenle 12 Bit FAT'te FAT zinciri izlenirken dikkat edilmelidir. Eğer volüm küçükse (eskiden floppy diskler vardı ve onlar çok küçüktü) FAt12 sistemi FAT tablosunun daha az yer kaplamasını sağlamaktadır. FAT12 sisteminde bir FAT elemanı 12 bit olduğu için FAT bölümünde en fazla 2^12 = 4096 FAT elemanı olabilir. Microsoft kendi format programında FAT12 volümlerinde bir cluster'ı maksimum 8 sektör olarak almaktadır. Bu durumda FAT12 volümü maksimum 2^12 * 2^3 * 2^9 = 2^24 = 16MB olabilmektedir. Başka bir deyişle Microsoft 16MB'nin yukarısındaki volümleri FAT12 olarak formatlamamaktadır. Aşağıda 12 bit FAT tablosunun baş kısmı görülmektedir: 00000200 f8 ff ff 00 40 00 05 60 00 07 80 00 09 a0 00 0b |....@..`........| 00000210 c0 00 ff 0f 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000230 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000240 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000250 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000260 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000270 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 12 bit'in 3 hex digit yani 1.5 olduğuna dikkat ediniz. Buradaki 12 bit şöyle yapılmaktadır. Cluster numarası önce 1.5 ile çarpılır ve noktalı kısım atılır. (Bu işlem 3 ile çarpılıp 2'ye bölünme biçiminde yapılabilir.) Elde edilen offset'ten WORD bilgi çekilir. Eğer cluster numarası çifte yüksek anlamlı 4 bit atılır, eğer cluster numarası tek ise düşük anlamlı 4 bit atılır. Yüksek anlamlı 4 bit'in atılması 0x0FFF ile "bit and" işlemi uygulanarak, düşük anlamlı 4 bit'in elde edilmesi sayının 4 kez sağa ötelenerek yapılabilir. Örneğin yukarıdaki FAT bölümünde biz 4 numaralı cluster'ın değerini elde edecek olalım. 4 * 1.5 = 6'dır. 6'ıncı offset'ten WORD çekilirse 0x6005 değeri elde edilir. Yüksek anlamı 4 bit atıldığında ise 0x005 değeri elde edilecektir. Şimdi 5 numaralı cluster'ın değerini elde etmek isteyelim. Bu durumda 5 * 1.5 = 7.5 olur. Noktadan sonraki kısım atılırsa 7 elde edilir. 7'inci offset'ten WORD öçekildiğinde 0x0060 değeri elde edilecektir. Bu değerin de düşük anlamlı 4 biti atıldığında 0x006 değeri elde edilir. 12 Bit FAT sisteminde bir FAT elemanın alabileceği değerler de şöyledir: 000 Boş cluster 001 Kullanılmıyor 002 - FEF Geçerli, sonraki cluster FF0H - FF6 Reserved cluster FF7 Bozuk cluster, işletim sistemi bu cluster'a dosya parçası yerleştirmez FF8 - FFF Son cluster 12 bit FAT tablosunda ilk cluster değeri bilinen dosyanın cluster zincirlerini elde etmek için aşağıdaki gibi bir fonksiyon yazılabilir. uint16_t *getclu_chain12(FATSYS *fatsys, uint32_t firstclu, uint16_t *count) { uint16_t clu, word, n; uint16_t *chain, *temp; uint32_t capacity; clu = firstclu; capacity = CHAIN_DEF_CAPACITY; n = 0; if ((chain = (uint16_t *)malloc(sizeof(uint16_t) * CHAIN_DEF_CAPACITY)) == NULL) return NULL; do { chain[n++] = clu; if (n == capacity) { capacity *= 2; if ((temp = realloc(chain, sizeof(uint16_t) * capacity )) == NULL) { free(chain); return NULL; } chain = temp; } word = *(uint16_t *)(fatsys->fat + clu * 3 / 2); clu = clu % 2 == 0 ? word & 0x0FFF : word >> 4; } while (clu < 0xFF8); *count = n; return chain; } Fonksiyonda 12 bit FAT değerinin elde edilmesi şöyle yapılmıştır: clu = clu % 2 == 0 ? word & 0x0FFF : word >> 4; >>> FAT32: FAT32 sisteminde her FAT elemanı 32 bittir. Ancak bu sistemde boot sektördeki BPB alanında da faklılıklar vardır. Bu nedenle 32 bit FAT sistemi FAT12 ve FAT16 ile tam uyumlu değildir. FAT32 için bazı fonksiyonların yeniden yazılması gerekir. Biz FAT dosya sisteminin boot sektörünü, FAT ve Data bölümlerini ele aldık. Ele almadığımız tek bölüm "Root Dir" bölümüdür. Şimdi "Root Dir" bölümü ve dosya bilgilerinin nasıl saklandığı konusu üzerinde duracağız. Microsoft'un FAT dosya sisteminde ve UNIX/Linux sistemlerinde kullanılan i-node tabanlı dosya sistemlerinde dizinler de tamamen bir dosya gibi organize edilmektedir. Yani dizinler de aslında birer dosyadır. Bir dosyanın içerisinde o dosyanın bilgileri bulunurken bir dizin dosyasının içerisinde o dizindeki dosyalara ilişkin bilgiler bulunmaktadır. Yani dizinler aslında "o dizindeki dosyaların bilgilerini içeren dosyalar" gibidir. Bir dizin dosyası "dizin girişlerinden (directory entry) oluşmaktadır. FAt12 ve FAT16 dosya sistemlerinde bir dizin dosyasındaki dizin girişleri 32 byte uzunlupundaydı. Yani dizin ddosyaları 32 byte'lık kayıtların peşi sıra gelmesiyle oluşuyordu. O zamanalarda DOS sistemlerinde bir dosyanın ismi için en fazla 8 karakter, uzantısı için de en fazla 3 karakter kullanılabiliyordu. Dolayısıyla 32 byte'lık dizin girişlerinin 1 byte'ı dosyanın ismi için ayrılmıştı. Sonra Microsoft dosya isimlerini 8+3 formatından çıkartarak onların 255'e kadar uzatılmasını sağladı. Ancak bu yaparken de geçmişe doğur uyumu korumak için birden fazla 32 byte'lık dizin girişleri kullandı. Biz önce burada klasik 8+3'lük dizin girişlerinin formatını göreceğiz. 32'lik klasik dizin girişi formatı şöyledir: Offset (Hex) Uzunluk Anlamı 00 8 Byte Dosya ismi (File Name) 08 3 Byte Dosya Uzantısı (Extension) 0B 1 Byte Dosya Özelliği (Attribute) 0C 1 Byte Kullanılmıyor (Reserved) 0D BYTE Yaratılma Zamanının Milisaniyesi 0E WORD Dosyanın Yaratılma Zamanı (Creation Time) 10 WORD Dosyanın Yaratılma Tarihi (Creation Date) 12 WORD Son Okunma Zamanı (Last Access Time) 14 WORD Kullanılmıyor (Reserved) 16 WORD Son Yazma Zamanı (Last Write Time) 18 WORD Son Yazma Tarihi (Last Write Date) 1A WORD İlk Cluster Numarası (First Cluster) 1C DWORD Dosyanın Uzunluğu (File Length) Aşağıda "x.txt" dosyanın ve "mydir" dizinin 32 byte'lık dizin girişleri görülmektedir. 58 20 20 20 20 20 20 20 54 58 54 20 00 0b 5d 92 |X TXT ..].| 59 59 59 59 00 00 5d 92 59 59 0e 00 0f 00 00 00 |YYYY..].YY......| 4d 59 44 49 52 20 20 20 20 20 20 10 00 7a f0 96 |MYDIR ..z..| 59 59 59 59 00 00 f0 96 59 59 10 00 00 00 00 00 |YYYY....YY......| Bu formattaki, -> Dosya İsmi: 32'lik dizin girişlerinin ilk 8'byte'ı dosya isminden oluşmaktadır. Eğer dosya ismi 8 karakterden kısa ise SPACE karakterleriyle (0x20) padding yapılmaktadır. Klasik FAT16 ve FAT12 sistemlerinde dosya isimlerinin ve uzantılarının büyük harf-küçük harf duyarlılığı yoktur. Tüm dosyalar bu sistemlerde "büyük harfe dönüştürülerek" dizin girişlerinde tutulmaktadır. -> Dosya Uzantısı: Dosya uzantısı en fazla 3 karakterden oluşmaktadır. Eğer 3 karakterden kısa ise SPACE (0x20) karakterleriyle padding yapılmaktadır. -> Dosya Özelliği: Bu alanda dosyanın özleliklerine ilişkin bit bit alanı bulundurulmaktadır. Buradaki her bit'in bir anlamı vardır. Özellik byte'ı aşağıdaki bitlerden oluşmaktadır: 7 6 5 4 3 2 1 0 Reserved Reserved Archive Dir VLabel System Hidden ReadOnly Eğer ReadOnly biti 1 ise dosya "read-only" biçimdedir. Böyle dosyalara işletim sistemi yazma yapmaz. Hidden biti 1 ise dosya "dir" komutu uygulandığında görüntülenmez. DOS işletim sisteminin kendi dosyalarını System özelliği ile vurgulamaktadır. Yani eğer bir dosya işletim sistemine ilişkin bir dosya ise System biti 1 olur. Volüm isimleri boot sektörün yanı sıra kök dizinde bir dosya ismi gibi de tutulmaktadır. Böyle girişlerin VLabel biti 1 olur. Eğer bir dizin söz konusu ise Dir biti 1 olmaktadır. FAT dosya sisteminde normal dosyalara "Archive" dosyaları denilmektedir. Bu nedenle bu bit hemen her zaman 1 olarak görülür. Aşağıda "x.txt" ve "mydir" dizin girişlerine ilişkin özellik byte'ınn bitleri görülmektedir: x.txt (0x20) 0 0 1 0 0 0 0 0 mydir (0x10) 0 0 0 1 0 0 0 0 "x.txt" dosyasının özellik bitlerinden yalnızca Archive biti set edilmiştir. "mydir" dizinin de yalnızca "Dir" biti set edilmiştir. İşletim sistemi bir dizin girişinin normal bir dosyaya mı yoksa bir dizin dosyasına mı ilikin olduğunu özellik byte'ının 4 numaralı bitine bakarak tespit etmektedir. -> Tarih ve Zaman Bilgileri FAT12 ve FAT16 dosya sistemlerinde 2 byte ile kodlanmaktadır. Eskiden DOS sistemlerinde dosyanın yaratılma tarihi ve zamanı ve son okunma tarihi tutulmazdı. Bu alanlar "reserved" durumdaydı. Sonra Microsoft bu alanları bu amaçla kullanmaya başladı. Tarih bilgisi byte içerisinde bitsel düzeyde tutulmaktadır. Tarih bilgisinin tutuluş formatı şöyledir: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 y y y y y y y m m m m d d d d d Burada WORD değerin düşük anlamlı 5 biti gün için, sonraki 4 biti ay için ve geri kalan 7 biti yıl için bulundurulmuştur. Tarih bilgisinin yıl alanı için 7 bit ayrıldığına göre buraya nasıl 2024 gibi bir tarih yerleştirilebilmektedir. İşte DOS işletim sisteminin ilk versiyonu 1980 yılında oluşturulduğu için buradaki tarih bilgisi her zmana 1980 yılından itibaren bir offset belirtmektedir. Yani örneğin 2024 yılı için buradaki yıl bitlerine 44 kodlanmaktadır. Örneğin bir dosyanın 32'lik dizin girişi şöyledir: 58 20 20 20 20 20 20 20 54 58 54 20 18 AB 03 B3 59 59 59 59 00 00 09 B3 59 59 06 00 1B 00 00 00 Buaradk 0x18'inci offset'ten little endian formatta WORD çekersek 0x5959 değerini elde ederiz. Şimdi bu WORD değeri 2'lik sistemde ifade edelim: 5 9 5 9 0101 1001 0101 1001 Şimdi de yukarıda belirttiğimiz gibi sayıyı bit'lerine ayrıştıralım: 0101100 => 44 1010 => 10 11001 => 25 yıl ay gün O halde buradaki tarih 25/10/2024'tür. 16 bitle (WORD ile) zaman bilgisi de şöyle kodlanmıştır: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 h h h h h m m m m m m s s s s s Burada bir noktayaya dikkat ediniz: 0 ile 24 arasındaki saatler 5 bit ile tutulabilir. 0 ile 60 arasındaki dakikalar ise ancak 6 bit ile tutulabilir. Burada geriye 5 bit almıştır. Dolayısıyla saniyeleri tutmak için bu 5 bit yeterli değildir. İşte FAT dosya sistemini tasarlayanlar saniyedeki duyarlılığı azaltarak saniye değerinin yarısını buraya yazılması yoluna gitmişlerdir. Yani zaman bilgisinin saniye kısmı eski FAT12 ve FAT16 sistemlerinde tam duyarlılıkla tutulamamaktadır. Örneğin yukarıdaki dizin girişinde dosyanın son değiştirilme zamanı için 0x16'ıncı offset'ten WORD çektiğimizde 0xB309 değerini elde ederiz. Şimdi bu değeri 2'lik sisteme dönüştürelim: B 3 0 9 1011 0011 0000 1001 Şimdi de bit alanlarını ayrıştıralım: 10110 => 22 011000 => 24 01001 => 9 saat dakika saniye Burada işletim sistemi saniye alanına mevcut saniyenin yarısını yazdığına göre bu dosyanın değiştirilme zamanı 22:24:18 olacaktır. Burada küçük bir noktaya dikkatinizi çekmek istiyoruz: Eskiden dizin girişinin 0x0D numaralı BYTE'ı da "reserved" durumdaydı sonra bu byte'a dosyanın yaratılma zamanına ilişkin milisaniye değeri yerleştirildi. Dolayısıyla artık dosyanın yaratılma zamanı saniye duyarlılığında ifade edilebilmektedir. Bu durumda yaratılma zamanındaki saniye 2 ile çarpılıp buradaki milisaniye ile toplanmaktadır. Tabii genel olarak Microsoft'un arayüzü dosyaların zaman gilfgilerinin saniyelerini default durumda zaten gmstermemektedir. Örneğin: D:\>dir Volume in drive D is YENI BIRIM Volume Serial Number is 2C68-EBFD Directory of D:\ 25.10.2024 22:24 27 x.txt 25.10.2024 22:26 8 con 25.10.2024 22:26 59 y.txt 25.10.2024 22:28 mydir 3 File(s) 94 bytes 1 Dir(s) 52.194.304 bytes free -> İlk Cluster Numarası: Biz daha önce FAT bölümünü incelerken bir dosyanın cluster zincirini elde edebilmek için onun ilk cluster numarasının bilinmesi gerektiğini belirtmiştik. (Bir bağlı listeyi dolaşabilmek için onun ilk düğümünün yerinin bilinmesi gerektiğini anımsayınız.) İşte bir dosyanın ilk cluster numarası dizin girişinde saklanmaktadır. Yani işletim sistemi önce dosyanın dizin girişini bulmakta sonra FAT'ten onun cluster zincirini elde etmektedir. Yukarıdaki dosyasının dizin girişini yeniden veriyoruz: 58 20 20 20 20 20 20 20 54 58 54 20 18 AB 03 B3 59 59 59 59 00 00 09 B3 59 59 06 00 1B 00 00 00 Burada söz konusu dosyanın ilk cluster numarası dizin girişinin 0x1A ofsfetinden başlayan WORD bilgidir. Bu bilgiyi örnek dizin girişinden çektiğimizde 0x006 değerini elde ederiz. Bu durumda bu dosyanın ilk cluster numarası 6'dır. -> Dosyanın Uzunluğu: Dosyanın uzunluğu dizin girişindeki son 4 byte'lık (DWORD alan) alanda tutulmaktadır. İşletim sistemi dosyanın son cluster'ındaki geçerli byte sayısını bu uzunluktan yararlanarak elde etmektedir. Örneğin yukarıdaki dizin girişine ilişkin dosya uzunluğu 0x0000001B = 27'dir. Dizin dosyalarına ilişkin uzunluklar için işletim sistemi hep 0 değerini yazmaktadır. Dizinler de bir dosya gibi ele alınmaktadır. Dolayısıyla dizinlerin de bir cluster zinciri vardır. Ancak FAT12 ve FAT16 sistemlerinde kök dizinin yeri ve uzunluğu baştan bellidir. Kök dizin için bir cluster zinciri yoktur. FAT32 dosya sisteminde kök dizin de normal bir dizin gibi büyüyebilmektedir. Yani kök dizinin de bir cluster zinciri vardır. 32'lik bir dizin girişini aşağıdaki gibi bir yapıyla temsil edebiliriz: #pragma pack(1) typedef struct tagDIR_ENTRY { unsigned char name[8]; unsigned char ext[3]; uint8_t attr; char reserved1[1]; uint8_t crtime_ms; uint16_t crdate; uint16_t crtime; uint16_t rdtime; char reserved2[2]; uint16_t wrtime; uint16_t wrdate; uint16_t fclu; uint32_t size; } DIR_ENTRY; #pragma pack() Pekiyi bir dosya silindiğinde ne olur? İşletim sistemi FAT dosya sisteminde bir dosya silindiğinde iki işlem yapar: -> Dosyanın silindiğinin anlaşılması için dizin girişindeki dosya isminin ilk karakterini 0xE5 olarak değiştirir. Böylece dizin girişlerini tararken 32'lik girişin ilk karajkter 0xE5 ise o dizin girişini silindiği gerekçesiyle atlamaktadır. Ancak işletim sistemi bu 32'lik dizin girişinin diğer byte'larına dokunmamaktadır. -> İşletim sistemi dosyanın cluster zincirini de sıfırlamaktadır. Böylece bu dosyanın FAT'te kapladığı alan artık "boş" gözükecektir. Ancak işletim sistemi dosyanın Data bölümündeki cluster'ları üzerinde herhangi bir işlem yapmaz. Pekiyi FAT dosya sisteminde "undelete" yapan programlar nasıl çalışmaktadır? İşte bu programlar dosyanın dizin girişine bakıp onun ilk cluster'ının numarasını elde edip FAT bölümünde yersine bir algoritmayla onun cluster zincirini yeniden oluşturmaya çalışmaktadır. Ancak böyle bir kurtarmanın garantisi yoktur. Çünkü işletim sistemi boşaltılmış cluster'ları başka bir dosya için tahsis etmiş olabilir. Ya da FAT'teki cluster zincirini tahmin eden programlar bu konuda yanılabilmektedir. Ancak ne olursa olsun dosyanın ilk karakteri silindiği için bu karakter kurtarma sırasında kullanıcıya sorulmaktadır. Öte yandan FAT12 ve FAT16 sistemlerinin orijinali yalnızca 8+3'lük dosya isimlerini destekliyordu. Yani bir dosyanın ismi en fazla 8 karakterden uzantısı da en fazla 3 karakterden oluşabiliyordu. 90'lı yılların ortalarına doğru Microsoft FAT dosya sisteminde uzun dosya isimlerinin de kullanılmasına olanak sağlamıştır. Microsoft bunu yaparken geçmişe doğru uyumu mümkün olduğunca korumaya da çalışmıştır. Microsoft'un bu yeni düzenlemesinde 8+3'ten daha uzun dosya izimleri birden fazla 32'lik girişle temsil edilmektedir. Ancak Microsoft geçmişe doğru uyumumu korumak için her uzun dosya isminin bir de 8+3'lük kısa ismini oluşturmak istemiştir. Bu durumda uzun dosya isimlerinin kullanıldığı FAT sistemlerinde 8+3'lük alan sığmayan dosya isimleri aşağıdaki formata göre dizin girişlerinde bulundurulmaktadır: <32'lik giriş> <32'lik giiriş> ... <32'lik giriş> Tabii eğer istenirse (örneğin Linux böyle yapmaktadır) 8+3'lük sınıfı aşmayana dosyalar da sanki uzun isimli dosyalarmış gibi saklanabilmektedir. Burada dosyanın 8+3'lük kısa isminin dışındaki uzun ismi de 32'lik girişlerde ASCII olarak değil UNICODE olarak tutulmaktadır. Uzun dosya isimlerine ilişkin girişlerin sonunda 8+3'lük kısa bir girişin de bulundurulduğunu belirtmiştik. Peki bu uzun dosya isminden kısa giriş nasıl elde edilmektedir? Uzun dosya isimlerinin tutulduğu 32'lik girişlerin ilk byte'ında önemli bilgiler vardır. Bu byte'a "sıra numarası (sequence number)" denilmektedir. Bu byte bit bit anlamlandırılmaktadır. Byte'ın bitlerinin anlamları şöyledir: D L X X X X X X Burada en yükske anlamlı bit olan D biti 32'lik girişin silinip silinmeidğini anlatmaktadır. Eğer bu giriş silinmişse bu bit 1, silinmemişse 0 olacaktır. 7 numaralı bit (L biti) 32'lik girişlerin aşağıdan yukarıya doğru son giriş olup olmadığını belirtmektedir. Ger kalan 6 bit 32'lik girişlerin sıra numarasını belirtir. Yani her 32'lik girişin bir sıra numarası vardır. Her 32'lik giriş uzun dosya isminin 13 UNICODE karakterini tutmaktadır. Pekiyi biz 32'lik bir girişin eski kısa ilişkin 32'lik bir giriş mi yoksa uzun ismin 32'lik girişlerinden biri mi olduğunu nasıl anlayabiliriz? İşte bunun için 32'lik girişin 0x0B offset'inde bulunan özellik byte'ının düşük anlamlı 4 bitine bakmak gerekir. Eğer bu 4 bitin hepsi 1 ise bu 32'lik giriş uzun dosya isminin 32'lik girişlerinden biridir. Aşağıda uzun dosya isimlerine ilişkin 32'lik girişlerin genel formatı verilmiştir. Ayrıntılı format için Microsoft'un "FAT File System Specification" dokümanına başvurabilirsiniz. Field name Offset Size Description LDIR_Ord 0 1 Sequence number (1-20) to identify where this entry is in the sequence of LFN entries to compose an LFN. One indicates the top part of the LFN and any value with LAST_LONG_ENTRY flag (0x40) indicates the last part of the LFN. LDIR_Name1 1 10 Part of LFN from 1st character to 5th character. LDIR_Attr 11 1 LFN attribute. Always ATTR_LONG_NAME and it indicates this is an LFN entry. LDIR_Type 12 1 Must be zero. LDIR_Chksum 13 1 Checksum of the SFN entry associated with this entry. LDIR_Name2 14 12 Part of LFN from 6th character to 11th character. LDIR_FstClusLO 26 2 Must be zero to avoid any wrong repair by old disk utility. LDIR_Name3 28 4 Part of LFN from 12th character to 13th character. Biz sonraki örneklerde uzun dosya isimlerini dikkate almayacağız. Onları geçeceğiz. Aşağıda kök uzun dosya isimlerinin ve silinmiş dosya isimlerinin geçilerek kök dosya sistemindeki dosyaların listesini elde eden bir fonksiyon verilmiştir. * Örnek 1, /* fatsys.h */ #ifndef FATSYS_H_ #define FATSYS_H_ #include #define FILE_INFO_LENGTH 32 #define CHAIN_DEF_CAPACITY 8 #define ROOT_DEF_CAPACITY 8 #define DIR_ENTRY_SIZE 32 /* Type Declarations */ typedef struct tagBPB { uint16_t fatlen; /* Number of sectors in FAT (A) */ uint16_t rootlen; /* Number of sectors in ROOT (NA) */ uint16_t nfats; /* Number of copies of FAT (A) */ uint32_t tsects; /* Total sector (A) */ uint16_t bps; /* Byte per sector(A) */ uint16_t spc; /* Sector per cluster(A) */ uint16_t rsects; /* Reserved sectors(A) */ uint8_t mdes; /* Media descriptor byte(A) */ uint16_t spt; /* Sector per track(A) */ uint16_t rootents; /* Root entry (A) */ uint16_t nheads; /* Number of heads (A) */ uint16_t hsects; /* Number of hidden sector( A) */ uint16_t tph; /* Track per head (NA) */ uint16_t fatloc; /* FAT directory location (NA) */ uint16_t rootloc; /* Root directory location (NA) */ uint16_t dataloc; /* First data sector location (NA) */ uint32_t datalen; /* Number of sectors in Data (NA) */ uint32_t serial; /* Volume Serial Number (A) */ char vname[12]; /* Volume Name (A) */ } BPB; typedef struct tagFATSYS { int fd; /* Volume file descriptor */ BPB bpb; /* BPB info */ uint32_t fatoff; /* Offset of FAT */ uint32_t rootoff; /* Offset of root directory */ uint32_t dataoff; /* Offset of DATA */ uint32_t clulen; /* Cluster length as bytes */ uint8_t *fat; /* FAT sectors */ uint8_t *rootdir; /* Root sectors */ /* ... */ } FATSYS; #pragma pack(1) typedef struct tagDIR_ENTRY { unsigned char name[8]; unsigned char ext[3]; uint8_t attr; char reserved1[1]; uint8_t crtime_ms; uint16_t crdate; uint16_t crtime; uint16_t rdtime; char reserved2[2]; uint16_t wrtime; uint16_t wrdate; uint16_t fclu; uint32_t size; } DIR_ENTRY; #pragma pack() /* Function prototypes */ int read_bpb(int fd, BPB *bpb); FATSYS *open_fatsys(const char *path); int close_fatsys(FATSYS *fatsys); int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf); int wite_cluster(FATSYS *fatsys, uint32_t clu, const void *buf); uint16_t *getclu_chain16(FATSYS *fatsys, uint32_t firstclu, uint16_t *count); uint16_t *getclu_chain12(FATSYS *fatsys, uint32_t firstclu, uint16_t *count); void freeclu_chain(uint16_t *chain); DIR_ENTRY *get_rootents(FATSYS *fatsys, uint16_t *count); #endif /* fatsys.c */ #include #include #include #include #include #include "fatsys.h" int read_bpb(int fd, BPB *bpb) { uint8_t bsec[512]; if (read(fd, bsec, 512) == -1) return -1; bpb->bps = *(uint16_t *)(bsec + 0x0B); bpb->spc = *(uint8_t *)(bsec + 0x0D); bpb->rsects = *(uint16_t *)(bsec + 0x0E); bpb->fatlen = *(uint16_t *)(bsec + 0x16); bpb->rootlen = *(uint16_t *)(bsec + 0x11) * FILE_INFO_LENGTH / bpb->bps; bpb->nfats = *(uint8_t *)(bsec + 0x10); if (*(uint16_t *)(bsec + 0x13)) bpb->tsects = *(uint16_t *)(bsec + 0x13); else bpb->tsects = *(uint32_t *)(bsec + 0x20); bpb->mdes = *(bsec + 0x15); bpb->spt = *(uint16_t *)(bsec + 0x18); bpb->rootents = *(uint16_t *)(bsec + 0x11); bpb->nheads = *(uint16_t *)(bsec + 0x1A); bpb->hsects = *(uint16_t *)(bsec + 0x1C); bpb->tph = (uint16_t)(bpb->tsects / bpb->spt / bpb->nheads); bpb->fatloc = bpb->rsects; bpb->rootloc = bpb->rsects + bpb->fatlen *bpb->nfats; bpb->dataloc = bpb->rootloc + bpb->rootlen; bpb->datalen = bpb->tsects - bpb->dataloc; bpb->serial = *(uint32_t *)(bsec + 0x27); memcpy(bpb->vname, bsec + 0x2B, 11); bpb->vname[11] = '\0'; return 0; } FATSYS *open_fatsys(const char *path) { FATSYS *fatsys; int fd; if ((fatsys = (FATSYS *)malloc(sizeof(FATSYS))) == NULL) return NULL; if ((fd = open(path, O_RDWR)) == -1) goto EXIT1; if (read_bpb(fd, &fatsys->bpb) == -1) goto EXIT2; fatsys->fd = fd; fatsys->fatoff = fatsys->bpb.fatloc * fatsys->bpb.bps; fatsys->rootoff = fatsys->bpb.rootloc * fatsys->bpb.bps; fatsys->dataoff = fatsys->bpb.dataloc * fatsys->bpb.bps; fatsys->clulen = fatsys->bpb.bps * fatsys->bpb.spc; if ((fatsys->fat = (uint8_t *)malloc(fatsys->bpb.fatlen * fatsys->bpb.bps)) == NULL) goto EXIT2; if ((fatsys->rootdir = (uint8_t *)malloc(fatsys->bpb.rootlen * fatsys->bpb.bps)) == NULL) goto EXIT3; if (lseek(fatsys->fd, fatsys->fatoff, SEEK_SET) == -1) goto EXIT4; if (read(fd, fatsys->fat, fatsys->bpb.fatlen * fatsys->bpb.bps) == -1) goto EXIT4; if (lseek(fatsys->fd, fatsys->rootoff, SEEK_SET) == -1) goto EXIT4; if (read(fd, fatsys->rootdir, fatsys->bpb.rootlen * fatsys->bpb.bps) == -1) goto EXIT4; return fatsys; EXIT4: free(fatsys->rootdir); EXIT3: free(fatsys->fat); EXIT2: close(fd); EXIT1: free(fatsys); return NULL; } int close_fatsys(FATSYS *fatsys) { free(fatsys->fat); if (close(fatsys->fd) == -1) return -1; free(fatsys); return 0; } int read_cluster(FATSYS *fatsys, uint32_t clu, void *buf) { if (lseek(fatsys->fd, fatsys->dataoff + (clu - 2) * fatsys->clulen, SEEK_SET) == -1) return -1; return read(fatsys->fd, buf, fatsys->clulen); } int write_cluster(FATSYS *fatsys, uint32_t clu, const void *buf) { if (lseek(fatsys->fd, fatsys->dataoff + (clu - 2) * fatsys->clulen, SEEK_SET) == -1) return -1; return write(fatsys->fd, buf, fatsys->clulen); } uint16_t *getclu_chain16(FATSYS *fatsys, uint32_t firstclu, uint16_t *count) { uint16_t clu, n; uint16_t *chain, *temp; uint32_t capacity; clu = firstclu; capacity = CHAIN_DEF_CAPACITY; n = 0; if ((chain = (uint16_t *)malloc(sizeof(uint16_t) * CHAIN_DEF_CAPACITY)) == NULL) return NULL; do { chain[n++] = clu; if (n == capacity) { capacity *= 2; if ((temp = realloc(chain, sizeof(uint16_t) * capacity )) == NULL) { free(chain); return NULL; } chain = temp; } clu = *(uint16_t *)(fatsys->fat + clu * 2); } while (clu < 0xFFF8); *count = n; return chain; } uint16_t *getclu_chain12(FATSYS *fatsys, uint32_t firstclu, uint16_t *count) { uint16_t clu, word, n; uint16_t *chain, *temp; uint32_t capacity; clu = firstclu; capacity = CHAIN_DEF_CAPACITY; n = 0; if ((chain = (uint16_t *)malloc(sizeof(uint16_t) * CHAIN_DEF_CAPACITY)) == NULL) return NULL; do { chain[n++] = clu; if (n == capacity) { capacity *= 2; if ((temp = realloc(chain, sizeof(uint16_t) * capacity )) == NULL) { free(chain); return NULL; } chain = temp; } word = *(uint16_t *)(fatsys->fat + clu * 3 / 2); clu = clu % 2 == 0 ? word & 0x0FFF : word >> 4; } while (clu < 0xFF8); *count = n; return chain; } void freeclu_chain(uint16_t *chain) { free(chain); } DIR_ENTRY *get_rootents(FATSYS *fatsys, uint16_t *count) { DIR_ENTRY *dent, *temp; DIR_ENTRY *dents; uint32_t capacity; uint16_t n; if ((dents = (DIR_ENTRY *)malloc(DIR_ENTRY_SIZE * ROOT_DEF_CAPACITY)) == NULL) return NULL; n = 0; capacity = ROOT_DEF_CAPACITY; dent = (DIR_ENTRY *)fatsys->rootdir; for (uint16_t i = 0; i < fatsys->bpb.rootents; ++i) { if (dent[i].name[0] == 0) break; if (dent[i].name[0] == 0xE5 || (dent[i].attr & 0XF) == 0x0F) continue; if (n == capacity) { capacity *= 2; if ((temp = realloc(dents, DIR_ENTRY_SIZE * capacity )) == NULL) { free(dents); return NULL; } dents = temp; } dents[n++] = dent[i]; } *count = n; return dents; } /* app.c */ #include #include #include #include #include #include "fatsys.h" void exit_sys(const char *msg); int main(int argc, char *argv[]) { FATSYS *fatsys; unsigned char buf[8192]; uint16_t count; uint16_t *chain; DIR_ENTRY *dents; if (argc != 2) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } if ((fatsys = open_fatsys(argv[1])) == NULL) exit_sys("open_fatsys"); if ((dents = get_rootents(fatsys, &count)) == NULL) { fprintf(stderr, "cannot get root entries!...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < count; ++i) { for (int k = 0; k < 8; ++k) if (dents[i].name[k] != ' ') putchar(dents[i].name[k]); if (dents[i].ext[0] != ' ') putchar('.'); for (int k = 0; k < 3; ++k) if (dents[i].ext[k] != ' ') putchar(dents[i].ext[k]); putchar('\n'); } close_fatsys(fatsys); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } İşletim sistemi bir dizin dosyası içerisindeki 32'lik girişlerini gözden geçirirken dosya isminin ilk karakterini '\0' karakter olarak gördüğünde (yani sayısal 0 değeri) işlemini sonlandırmaktadır. Yani dizin dosyası içerisindeki bütün girişlerin gözden geçirilmesine gerek yoktur. Yukarıda da belirttiğimiz gibi dosya isminin ilk karakteri 0xE5 ise işletim sistemi bu 32'lik girişi de silinmiş dosya olduğu gerekçesiyle geçmektedir. İşletim sistemlerinde bir yol ifadesi verildiğinde o yol ifadesinin hedefindeki dosya ya da dizine ilişkin dizin girişinin elde edilmesine "yol ifadelerinin çözümlenmesi (pathname resolution)" denilmektedir. Yol ifadelerinin çözümlenmesi eğer yol ifadesi mutlaksa kök dizinden itibaren, göreli ise prosesin çalışma dizininden itibaren yapılmaktadır. Örneğin FAT dosya sistemine ilişkin "\a\b\c\d.dat" biçiminde bir yol ifadesi verilmiş olsun. Burada hedeflenen "d.dat" dosyasına ilişkin dizin girişi bilgileridir. Ancak bunun için önce kök dizinde "a" girişi, sonra "a" dizininde "b" girişi, sonra "b" dizininde "c" girişi sonra da "c" girişinde "d.dat" girişi bulunmalıdır. Tabii biz burada Windows'taki bir yol ifadesini temel aldık. UNIX/Linux sistemlerinde dosya sistemleri mount edildiği için bu yol ifadesi aslında mount noktasına görelidir. İşletim sistemleri bir yol ifadesini çözümlerken yol ifadesindeki tüm yol bileşenlerine ilişkin dizin giriş bilgilerini de bir cache sisteminde saklamaktadır. İşletim sistemlerinin oluşturduğu bu cache sistemine "directory entry cache" ya da kısaca "dentry cache" denilmektedir. Örneğin prgramcı aşağıdaki gibi bir yol ifadesi kullanmış olsun: "\a\b\c\d.dat" İşletim sistemi buradaki "a", "b", "c" ve "d.dat" dosyalarına ilişkin dizin giriş bilgilerini bir cache sisteminde saklamaktadır. Böylece benzer yol ifadeleri için hiç disk okuması yapılmadan bu cache sisteminden bu bilgiler elde edilebilmektedir. Pekiyi FAT dosya sistemi için yol ifadelerini çözen basit yalın bir kodu nasıl yazabiliriz? Bizim bir yol ifadesi verildiğinde o yol ifadesini parse edip oradaki yol bileşenlerini elde edebilmemiz gerekir. Sonra dizinler bir dosya olduğuna göre dizinlere ilişkin cluster zincirinde diğer bileşenin aranması gerekir. İşlemler böyle devam ettirilir. FAT dosya sistemi için yol ifadesini çözümleyen bir fonksiyonun parametrik yapısı şöyle olabilir: int resolve_path(FATSYS *fatsys, const char *path, DIRECTORY_ENTRY *de); Biz mutlak yol ifadelerini çözümleyecek olalım. Her dizinin bir cluster zinciri vardır. Ancak FAT12 ve FAT16 sistemlerinde kök dizinin bir cluster zinciri yoktur. Kök dizinin yeri ve uzunluğu baştan bellidir. >> "ext" Dosya Sistemi : UNIX/Linux sistemlerinde i-node tabanlı dosya sistemleri kullanılmaktadır. i-node tabanlı dosya sistemlerinin temel organizasyonu FAT dosya sistemlerinden oldukça farklıdır. i-node tabanlı dosya sistemlerinin çeşitli varyasyonu vardır. Linux sistemleri ve BSD sistemleri ağırlıklı olarak ext (extended file system) denilen dosya sistemini kullanmaktadır. ext dosya sistemi ilk kez 1992 yılında tasarlanmıtır. Sonra zaman içerisinde bu dosya sisteminin ext2, ext3 ve ext4 biçiminde çeşitli varyasyonları oluşturulmuştur. Bugün artık genellikle bu ailenin son üyesi olan ext4 dosya sistemi kullanılmaktadır. Ancak yukarıda da belirttiğimiz gibi i-node tabanlı dosya sistemleri bir aile belirtmektedir. Bu ailenin FAT sistemlerinde olduğu gibi genel tasarımı birbirine benzerdir. Biz burada ext dosya sisteminin en uzun süre kullanılan versiyonu olan ext konusunda temel bilgiler vereceğiz. ext2 dosya sisteminin resmi dokümantasyonuna aşağıdaki bağlantıdan erişebilirsiniz: https://cscie28.dce.harvard.edu/lectures/lect04/6_Extras/ext2-struct.html ext2 dosya sistemi üzerinde incelemeler ve denemeler yapmak için yine loop aygıtlarından faydalanabilrisiniz. Bunun için yine önce içi sıfırlarla dolu bir dosya oluşturulur: $ dd if=/dev/zero of=ext2.dat bs=512 count=400000 Sonra loop aygıt sürücüsü bu dosya için hazırlanır: $ sudo losetup /dev/loop0 ext2.dat Artık aygıt formatlanabilir: $ mkfs.ext2 /dev/loop0 Mount işlemi aşağıdaki gibi yapılabilir: $ mkdir ext2 $ sudo /dev/loop0 ext2 i-node tabanlı dosya sistemlerinde volüm kabaca aşağıdaki bölümlere ayrılmaktadır: -> -> -> -> Boot blok (boot block) işletim sistemini boot eden kodların bulunduğu bloktur. Süper blok (super block) FAT dosya sistemlerindeki boot sektör BPB alanına benzemektedir. Yani burada dosya sistemine ilişkin meta data bilgiler bulunmaktadır. i-node blok i-node elemanlarından oluşmaktadır. Data block FAT dosya sistemindeki Data bölümü gibidir. FAT dosya sistemindeki "cluster" yerine i-node tabanlı dosya sistemlerinde "blok (block)" terimi kullanılmaktadır. Bir blok bir dosyanın parçası olabilecek en küçük birimdir. Süper blok hemen volümün 1024 byte offset'inde bulunmaktadır. (yani volümün başında boot sektör programları için 1024 byte yer ayrılmıştır. Süper blok süper bloktta belirtilen blok uzunluğu kadar uzunluğa sahiptir. Ayrıca izleyen paragraflarda da görüleceği gibi ext2 dosya sisteminde her blok grupta süper bloğun bir kopyası da bulunmaktadır.) Yukarıda da belirttiğimiz gibi burada volüm hakkında meta data bilgileri bulunmaktadır. Buradaki alanlar ext2 dokümantasyonunda ayrıntılarıyla açıklanmıştır. Süper blok içerisindeki alanlar aşağıdaki gibidir: | Alan Boyut (Byte) Açıklama |-----------------------|----|----------------------------------------------------------------------------------------- | s_inodes_count | 4 | Dosya sistemindeki toplam inode sayısı. | s_blocks_count | 4 | Dosya sistemindeki toplam blok sayısı. | s_r_blocks_count | 4 | Rezerve edilmiş blok sayısı. | s_free_blocks_count | 4 | Boş blok sayısı. | s_free_inodes_count | 4 | Boş inode sayısı. | s_first_data_block | 4 | İlk veri bloğunun numarası (bu, kök dizinin bulunduğu blok). | s_log_block_size | 4 | Blok boyutunun logaritmasının değeri (örneğin, 1 KB için 10, 4 KB için 12, vs.). | s_log_frag_size | 4 | Parçacık boyutunun logaritması. | s_blocks_per_group | 4 | Her blok grubundaki blok sayısı. | s_frags_per_group | 4 | Her blok grubundaki fragman sayısı. | s_inodes_per_group | 4 | Her blok grubundaki inode sayısı. | s_mtime | 4 | Dosya sisteminin son değiştirilme zamanı (Unix zaman damgası). | s_wtime | 4 | Dosya sisteminin son yazılma zamanı (Unix zaman damgası). | s_mnt_count | 2 | Dosya sisteminin kaç kez bağlandığı (mount) sayısı. | s_max_mnt_count | 2 | Dosya sisteminin kaç kez daha bağlanabileceği (yani, montaj sayısı aşımı). | s_magic | 2 | Süper blok sihirli sayısı (bu, EXT2 dosya sistemini tanımlar ve genellikle `0xEF53`'tür). | s_state | 2 | Dosya sisteminin durumu (örneğin, temiz mi, hata mı). | s_errors | 2 | Hata durumunda yapılacak işlem (örneğin, “ignore”, “panic”, vb.). | s_minor_rev_level | 2 | Küçük revizyon seviyesi (EXT2’yi güncelleyen küçük değişiklikler için). | s_lastcheck | 4 | Dosya sisteminin son kontrol tarihi (Unix zaman damgası). | s_checkinterval | 4 | Dosya sisteminin kontrol edilmesi gereken süre (saniye cinsinden). | s_creator_os | 4 | Dosya sistemini oluşturan işletim sistemi türü (örneğin, Linux, Solaris, vb.). | s_rev_level | 4 | EXT2 dosya sistemi revizyon seviyesi. | s_def_resuid | 2 | Varsayılan rezerv kullanıcı ID'si (uid). | s_def_resgid | 2 | Varsayılan rezerv grup ID'si (gid). | s_first_ino | 4 | İlk inode numarası (genellikle kök dizini için). | s_inode_size | 2 | Inode boyutu (genellikle 128 veya 256 byte). | s_block_group_nr | 2 | Bu süper blok ile ilişkili blok grubu numarası. | s_feature_compat | 4 | Uyumluluk özelliklerinin bit maskesi. | s_feature_incompat | 4 | Uyumsuz özelliklerin bit maskesi. | s_feature_ro_compat | 4 | Okuma-yazma uyumsuz özelliklerinin bit maskesi. | s_uuid | 16 | Dosya sisteminin benzersiz tanımlayıcısı (UUID). | s_volume_name | 16 | Dosya sistemi adının (etiketinin) olduğu alan. | s_last_mounted | 64 | Dosya sisteminin son bağlandığı dizin yolu. | s_algorithm_usage_bmp | 4 | Bloklar ve inode'lar için kullanılan algoritmaların bit maskesi. | s_prealloc_blocks | 1 | Önceden tahsis edilecek blok sayısı. | s_prealloc_dir_blocks | 1 | Önceden tahsis edilecek dizin blokları sayısı. | s_padding | 118| Alanın sonundaki boşluk (süper bloğun uzunluğunu tamamlar). Buradaki önemli alanlar hakkında kısa bazı açıklamalar yapmak istiyoruz: -> s_inodes_count: Bu alanda dosya sistemindeki toplam i-node elemanlarının sayısı bulunmaktadır. Bir ext2 disk bölümünde en fazla buradaki i-node elemanlarının sayısı kadar farklı dosya bulunabilir. -> s_blocks_count: Burada Data bölümündeki toplam blokların sayısı bulunmaktadır. -> s_r_blocks_count: Burada ayrılmış (reserve edielmiş) blokların sayısı bulunmaktadır. -> s_free_blocks_count: Burada Data bölümünde kullanımayan boş blokların sayısı tutulmaktadır. -> s_log_block_size: Burada 1024 değerinin 2 üzeri kaçla çarpılacağını belirten değer tutulmaktadır. Yani blok uzunluğu 1024 << s_log_block_siz biçiminde hesaplanmaktadır. Örneğin burada 2 değeri yazılıyorsa blok uzunluğu 2^2 * 1024 = 4096 byte'tır. -> s_inode_size: Burada bir i-ndeo elemanının kaç byte olduğu bilgisi yer almaktadır. Örnek dosya sistemimizde i-node elemanları 256 byte uzunluğundadır. ext2 dosya sisteminin super block bilgisi ve bazı önemli alanlarına ilişkin bilgiler "dumpe2fs" isimli utility programla elde edilebilir. Örneğin: $ dumpe2fs /dev/loop0 Aşağıda bir ext2 süper bloğunun örnek bir içeriği verilmektedir: 00004000 60 c3 00 00 50 c3 00 00 c4 09 00 00 f4 b6 00 00 |`...P...........| 00000410 55 c3 00 00 00 00 00 00 02 00 00 00 02 00 00 00 |U...............| 00000420 00 80 00 00 00 80 00 00 b0 61 00 00 51 5c 2e 67 |.........a..Q\.g| 00000430 51 5c 2e 67 01 00 ff ff 53 ef 00 00 01 00 00 00 |Q\.g....S.......| 00000440 42 5c 2e 67 00 00 00 00 00 00 00 00 01 00 00 00 |B\.g............| 00000450 00 00 00 00 0b 00 00 00 00 01 00 00 38 00 00 00 |............8...| 00000460 02 00 00 00 03 00 00 00 ec 89 02 3e a8 11 4c 01 |...........>..L.| 00000470 b0 b5 f5 48 1e 30 79 d6 00 00 00 00 00 00 00 00 |...H.0y.........| 00000480 00 00 00 00 00 00 00 00 2f 68 6f 6d 65 2f 6b 61 |......../home/ka| 00000490 61 6e 2f 53 74 75 64 79 2f 55 6e 69 78 4c 69 6e |an/Study/UnixLin| 000004a0 75 78 2d 53 79 73 50 72 6f 67 2f 44 69 73 6b 49 |ux-SysProg/DiskI| 000004b0 4f 2d 46 69 6c 65 53 79 73 74 65 6d 73 2f 65 78 |O-FileSystems/ex| 000004c0 74 32 00 00 00 00 00 00 00 00 00 00 00 00 0c 00 |t2..............| 000004d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000004e0 00 00 00 00 00 00 00 00 00 00 00 00 6e 47 e8 bc |............nG..| 000004f0 81 50 45 f3 bb 96 6c 7c 51 bc e3 8a 01 00 00 00 |.PE...l|Q.......| 00000500 0c 00 00 00 00 00 00 00 42 5c 2e 67 00 00 00 00 |........B\.g....| Burada toplam i-node elemanlarının sayısı 0xC360 (50016) tanedir. Disk bölümünün i-node bloğu i-node elemanlarından oluşmaktadır. Her i-node elemanının ilki 0 olmak üzere bir indeks numarası vardır. Örneğin: 0 i-node elemanı 1 i-node elemanı 2 i-node elemanı 3 i-node elemanı 4 i-node elemanı ... 300 i-node elemanı 301 i-node elemanı 302 i-node elemanı 303 i-node elemanı 304 i-node elemanı ... Bir dosyanın ismi haricindeki bütün bilgileri dosyaya ilişkin i-node elemanında tutulmaktadır. Zaten stat fonksiyonları da aslında bilgileri bu i-node elemanından almaktadır. Her dosyanın diğerlerinden farklı bir i-node numarası olduğuna dikkat ediniz. Dolayısıyla dosyanın i-node numarası o dosyayı karakterize etmektedir. ("ls" komutunda dosyanın i-node numaralarının -i seçeneği ile elde edildiğini anımsayınız.) Burada hatırlatma yapmak amacıyla stat yapısını yeniden vermek istiyoruz: struct stat { dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* inode number */ mode_t st_mode; /* protection */ nlink_t st_nlink; /* number of hard links */ uid_t st_uid; /* user ID of owner */ gid_t st_gid; /* group ID of owner */ dev_t st_rdev; /* device ID (if special file) */ off_t st_size; /* total size, in bytes */ blksize_t st_blksize; /* blocksize for file system I/O */ blkcnt_t st_blocks; /* number of 512B blocks allocated */ time_t st_atime; /* time of last access */ time_t st_mtime; /* time of last modification */ time_t st_ctime; /* time of last status change */ }; ext2 dosya sisteminde bir i-node elemanın alanları aşağıdaki gibidir: | Alan Boyut (Byte) Açıklama |-------------------------------------------------------------------------------------------------------------------- |i_mode | 2 | Dosya türü ve izinler (örneğin, `S_IFREG` (normal dosya), `S_IFDIR` (dizin), vs.). |i_uid | 2 | Dosya sahibinin kullanıcı kimliği (UID). |i_size_lo | 4 | Dosyanın boyutunun alt 32 biti (byte cinsinden). |i_atime | 4 | Son erişim zamanı (Unix zaman damgası). |i_ctime | 4 | Son inode değişiklik zamanı (Unix zaman damgası). |i_mtime | 4 | Son değişiklik (modifikasyon) zamanı (Unix zaman damgası). |i_dtime | 4 | Dosyanın silinme zamanı (Unix zaman damgası), eğer geçerliyse. |i_gid | 2 | Dosya sahibinin grup kimliği (GID). |i_links_count| 2 | Dosyaya bağlı olan hard link (bağlantı) sayısı. |i_blocks | 4 | Dosyanın disk üzerinde kullandığı blok sayısı (block, 512 byte'lık bloklar). |i_flags | 4 | Dosya bayrakları (örneğin, `i_dirty`, `i_reserved` gibi). |i_osd1 | 4 | Linux spesifik alan (genellikle genişletilmiş özellikler için kullanılır). |i_block[15] | 4 × 15 = 60| Dosyanın bloklarına işaretçi |i_generation | 4 | Dosyanın versiyon numarası (özellikle NFS gibi ağ dosya sistemlerinde kullanılır). |i_file_acl | 4 | Dosya için ACL (Access Control List) blok numarası. |i_dir_acl | 4 | Dizin için ACL blok numarası. |i_faddr | 4 | Dosyanın "fragman adresi" (bu, çoğu zaman sıfırdır ve eski EXT2 uygulamalarında kullanılır). Biz bu alanların büyük çoğunluğunu aslında stat fonksiyonunda görmüştük. Ancak stat yapısında olmayan bazı elemanlar da burada bulunmaktadır. Biz stat yapısında olmayan bazı önemli elemanlar üzerinde durmak istiyoruz: -> i_dtime: Bu alanda eğer dosya silinmişse dosyanın ne zaman silindiğine yönelik tarih zaman bilgisi tutulmaktadır. Buradaki değer 01/01/1970'ten geçen saniye sayısı cinsindedir. -> i_block ve i_blocks: Bu elemanlar izleyen paragraflarda adaha ayrıntılı bir biçimde ele alınacaktır. -> i_flags: Bu alanda ilgili dosyaya ilişkin bazı bayraklar tutulmaktadır. -> i_file_acl: Dosyaya ilişkin "erişim kontrol listesi (access control list)" ile ilgili bilgiler tutulmaktadır. i-node elemanında dosyanın isminin tutulmadığına dikkat ediniz. ext2 dosya sisteminde bir i-node elemanının uzunluğu süper bloğun s_inode_size elemaında yazmaktadır. Örnek sistemimizde i-node elemanları 256 byte uzunluktadır. Pekiyi dosyanın ismi nerededir ve dosyanın i-node numarası nereden elde edilmektedir? Bunu izleyen paragraflarda göreceğiz. Biz yukarıda i-node tabanlı bir disk bölümünün kaba organizasyonunun aşağıdaki gibi olduğunu belirtmiştik: (1024 byte) Burada sanki süper bloktan hemen sonra i-node blok geliyormuş gibi biz organizasyon resmedilmiştir. Halbuki süper bloktan hemen sonra i-node blok gelmemektedir. ext2 dosya sisteminin gerçek yerleşimi aşağıdaki gibidir: (1024 byte) Bir disk bölümü aslında blok gruplarından (block groups) oluşmaktadır. Her block grubu disk bölümüne ilişkin bir bölümü belirtir. Bir block grubu aşağıdaki gibi bir yapıya sahiptir: İşte "blok grup betimleyici tablosu (block group descriptor table)" block group'ları hakkında bilgi veren bir bölümdür. Bir kaç blok bilginin yer aldığı super block'un "s_blocks_per_group" elemanında saklanmaktadır. Her blok grupta belli sayıda i-node elemanı vardır. Bir blok gruptaki i-node elemanlarının sayısı super block'taki s_inodes_per_group elemanıyla belirtilmektedir. Yani aslında ext2 dosya sisteminde aşağıdaki gibi bir oragnizasyon söz konusudur: (1024 byte) (1024 byte) (1024 byte) (1024 byte) (1024 byte) (1024 byte) (1024 byte) (1024 byte) (1024 byte) ... Görüldüğü gibi ext2 dosya sisteminde super bloğun tek bir kopyası yoktur. Her block grupta super blok yeniden yer almaktadır. Bir blok grupta ayrı bir i-node tablosunun ve data bölümünün olduğuna dikkat ediniz. Pekiyi neden ext2 dosya sisteminde disk bölümü birden fazla blok gruplara ayrılmıştır? İşte bunun nedenlerinden biri güvenliktir. Yani i-node bloklardan biri bozulduğunda diğeri bozulmamış biçimde kalabilir. Blok gruplarındaki "blok grup betimelyici tablosu (block group descriptor table)" blok grupları hakkında bazı meta data bilgileri tutmaktadır. Ancak blok grup betimleyicilerinde yalnızca o blok grubuna ilişkin bilgiler değil tüm blok gruplarına ilişkin bilgiler tutulmaktadır. Yani her blok grubunda yeniden tüm blok gruplarına ilişkin bilgiler tutulmaktadır. Block grup betimleyici tablosu blok grup betimleyicilerindne oluşan bir dizi gibidir: Block Grup Betimleyici Tablosu ... Bir blok grup betimleyicisinin alanları şöyledir: +----------------------------+---------------------------+--------------------------------------+ | Yapı Elemanı | Boyut (Byte) | Açıklama | +----------------------------+---------------------------+--------------------------------------+ | bg_block_bitmap | 4 | Blok haritasının başlangıç adresi | | bg_inode_bitmap | 4 | İnode haritasının başlangıç adresi | | bg_inode_table | 4 | İnode tablosunun başlangıç adresi | | bg_free_blocks_count | 2 | Blok grubunda serbest blok sayısı | | bg_free_inodes_count | 2 | Blok grubunda serbest inode sayısı | | bg_used_dirs_count | 2 | Blok grubundaki kullanılan dizin sayısı | | bg_flags | 2 | Blok grubunun bayrakları (flags) | | bg_reserved | 12 | Rezerv alan (genellikle sıfırdır) | +----------------------------+---------------------------+--------------------------------------+ Blok grup betimleyicisi toplamda 32 byte yer kaplamaktadır. Her blok grubunda blok bitmap'in, i-node bitmap'in ve i-node tablosunun yerinin blok numarası tutulmaktadır. Buradaki blok uzunlukları disk bölümünün başından itibaren yer belirtir. Disk bölümü içerisindeki blokların numaralandırılması ile ilgili ince bir nokta vardır. Eğer disk bölümündeki blok uzunluğu 1K yani 1024 byte ise boot block 0'ıncı bloktadır. Dolayısıyla ilk blok grubundaki süper blok 1'inci bloktadır. Ancak blok büyüklüğü 1024'ten (yani 1K'dan) fazla ise bu durumda boot blok ile süper blok tek blok kabul edilmektedir. Boot blok ile süper bloğun bulunduğu ilk bloğun numarası 0'dır. İlk blok grup betimleyici tablosunun yeri de blok grubundaki super block'tan hemen sonradır. Örneğin dosya sistemindeki blok uzunluğu 4K (4096 byte) ise ilk blok betimleyici tablosunun yeri 4096'ıncı = 0x1000 offset'indedir. (Bu durumda boot blok ile süper bloğun 0 numaralı blok biçiminde tek blok olarak ele alındığını anımsayınız.) Bir blok grubunun toplam kapladığı blok sayısı süper block içerisindeki s_blocks_per_group elemanında tutulmaktadır. Örneğin biz k numaralı blok grubun blok numarasını k * s_blocks_per_group işlemiyle elde edebiliriz. Aşağıda örnek bir blok grup betimleyici tablosu verilmiştir: 00001000 0e 00 00 00 0f 00 00 00 10 00 00 00 c7 79 a4 61 |.............y.a| 00001010 02 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00001020 0e 80 00 00 0f 80 00 00 10 80 00 00 25 3d b0 61 |............%=.a| 00001030 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00001040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| .... Bir blok grup betimleyicisi 32 byte uzunluktadır. Burada toplam iki blok grup betimleyicisi yani disk bölümünde toplam iki blok grubu bulunmaktadır.Bu iki blok grup betimleyicisini ayrı ayrı aşağıda veriyoruz: 00001000 0e 00 00 00 0f 00 00 00 10 00 00 00 c7 79 a4 61 |.............y.a| 00001010 02 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00001020 0e 80 00 00 0f 80 00 00 10 80 00 00 25 3d b0 61 |............%=.a| 00001030 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Disk bölümünde toplam kaç blok grubu olduğu süper bloktaki s_blocks_count elemanında yazmaktadır. Bir blok grubundaki blok grup betimleyici tablosunun uzunluğu 1 blok kadardır. Blok bitmap'in ve I-node bitmap'in uzunlukları doğrudan süper blokta yazmamaktadır. Bu uzunluklar dolaylı bir biçimde hesaplanmaktadır. Dolayısıyla bir blok gruptaki data alanın başlangıç bloğu da dolaylı bir biçimde hesaplanmaktadır. Pekiyi bir dosyanın i-node numarası biliniyorsa onun disk bölümündeki yerini nasıl hesaplarız? Burada bizim bu i-node elemanının hangi blok grubunda ve o blok grubunun i-node tablosunda nerede olduğunu belirlememiz gerekir. i-node tablosundaki i-node elemanları 1'den başlatılmıştır. Yani bizim elimizde i-node numarası n olan bir dosya varsa aslında bu dosya i-node tablosunun n - 1'inci i-node elemanındadır. Çünkü i-node tablosunun ilk i-node elemanının numrası 0 değil 1'dir. Her blok grupta eşit sayıda i-node elemanı bulunmaktadır. Bir blok gruptaki i-node elemanlarının sayısı doğrudan süper bloktaki s_inodes_per_group elemanında belirtilmektedir. Bu durumda ilgili i-node numarasına ilişkin i-node elemanı i-node numarası n olmak üzere (n - 1) / s_inodes_per_group işlemiyle elde edilebilir. Tabii bu durumda (n - 1) % s_inodes_per_group ifadesi de i-node elemanının o blok gruptaki i-node tablosunun kaçıncı elemanında olduğunu verecektir. Anımsanacağı gibi her blok grubunun i-node tablosunun yeri blok grup betimleyicisinin bg_inode_table elemanında belirtiliyordu. Bir blok grubunun toplam kaç tane bloktan oluştuğu süper bloktaki s_blocks_per_group elemanında tutulmaktadır. Dolayısıyla k'ıncı blok grubunun yeri k * s_blocks_per_group değeri ile tespit edilir. Blok numaralarına boot block dahil değildir. Yani ilk blok grubunun süper bloğunun blok numarası 0'dır. Bu durumda manuel olarak n numaralı i-node numarasına sahip bir dosyanın i-node elemanına şöyle erişilebilir: -> Önce n / s_inodes_per_group ile ilgili i-node elemanının hangi blok grubununda olduğu tespit edilir. Bu değer k olsun. -> Bu blok grubunun yeri k * s_blocks_per_group değeri ile elde edilir ve bu bloğa gidilir. Her bloğun başında 1 blokluk süper blok vardır. Süper bloğu blok grup betimleyici tablosu izler. Blok grup betimleyici tablosu blok grup betimleyicilerinden oluşmaktadır. Her blok grup betimleyicisi 32 byte yer kaplamaktadır. Dolayısıyla biz k numaralı blok grubuna ilişkin blok betimleyicisinin yerini k * 32 ile tespit edebiliriz. (Aslında tüm blok gruplarındaki blok grup betimleyici tablolarının birbirinin aynısı olduğunu anımsayınız.) -> İlgili blok grubunun i-node tablosunun yeri blok grup betimleyicisinin bg_inode_table elemanında belirtilmektedir. Artık biz i-node elemanını burada belirtilen bloktan itibaren n % s_inodes_per_group kadar ilerideki i-node elemanı olarak elde edebiliriz. Bir i-node elemanının uzunluğunun 256 byte olduğunu belirtmiştik. Şimdi 12'inci i-node elemanın yerini bu adımlardan geçerek bulmaya çalışalım. Elimizdeki disk bölümünde bir blok grupta toplam 25008 tane i-node elemanı vardır. O halde 12 numaralı i-node elemanı 0'ıncı blok grubunun 11'inci i-node elemanındadır. 0'ınci blok grubu eğer blok uzunluğu 1K'dan fazla ise diskin 0'ıncı bloğundan başlamaktadır. (Tabii 0'sıncı bloğın hemen başında boot blok, ondan 1024 byte sonra da 0'ın blok grubunun süper bloğu bulunmaktadır.) O halde elimizdeki disk bölümünün 0'ıncı blok grubunun blok betimleyici tablosu 1'inci bloktadır. Bunun yeri de bir blok 4096 byte olduğuna göre 0x1000 offset'indedir. Buradan elde edilen blok grup betimleyici tablosu şöyledir: 00001000 0e 00 00 00 0f 00 00 00 10 00 00 00 c7 79 a4 61 |.............y.a| 00001010 02 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00001020 0e 80 00 00 0f 80 00 00 10 80 00 00 25 3d b0 61 |............%=.a| 00001030 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00001040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00001050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| ... Her blok grup betimleyicisinin 32 byte olduğunu anımsayınız. Bu duurmda 0'ıncı blok grup betimleyicisi şöyledir: 00001000 0e 00 00 00 0f 00 00 00 10 00 00 00 c7 79 a4 61 |.............y.a| 00001010 02 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Bu blok grubundaki i-node tablosunun blok numarası blok grup betimleyicisinin 8'inci offset'inde bulunan bg_inode_table elemanındadır. Bu elemandaki değer 0x00000010 (16)'dır. O halde bizim 16 numaralı bloğa gitmemiz gerekir. 16 numaralı blok disk bölümünün 16 * 4096 = 65536 (0x10000) offset'indedir. Artık bu offset'te ilgili blok grubundaki i-node elemanları bulunmaktadır. Bir i-node elemanı 128 byte olduğuna göre 11'inci elemanının yeri 11 * 256 = 2816 (0xB00) byte ileridedir. O halde bu tablonun disk bölümünün başından itibarenki yeri 65536 + 2816 = 68352 (0x10B00) offset'indedir. Aşağıda ilgili i-node elemanının 256 byte'lık içeriği görülmektedir: 00010b00 a4 81 00 00 c8 79 00 00 87 5c 2e 67 87 5c 2e 67 |.....y...\.g.\.g| 00010b10 87 5c 2e 67 00 00 00 00 00 00 01 00 40 00 00 00 |.\.g........@...| 00010b20 00 00 00 00 01 00 00 00 00 08 00 00 01 08 00 00 |................| 00010b30 02 08 00 00 03 08 00 00 04 08 00 00 05 08 00 00 |................| 00010b40 06 08 00 00 07 08 00 00 00 00 00 00 00 00 00 00 |................| 00010b50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010b60 00 00 00 00 2e db 09 7c 00 00 00 00 00 00 00 00 |.......|........| 00010b70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010b80 20 00 00 00 c8 76 3d ba c8 76 3d ba c8 76 3d ba | ....v=..v=..v=.| 00010b90 87 5c 2e 67 c8 76 3d ba 00 00 00 00 00 00 00 00 |.\.g.v=.........| 00010ba0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010bb0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010bc0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010bd0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010be0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010bf0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Buradaki ilk WORD bilgi (0x81A4) dosyanın erişim haklarını sonraki WORD bilgi (0x0000) kullanıcı id'sini belirtmektedir. Sonraki DWORD bilgi (0x000079C8) de dosyanın uzunluğunu belirtmektedir. Diğer elemanların anlamlarına i-node yapısından erişebilirsiniz. i-node tablosundaki ilk n tane i-node elemanı reserved biçimde tutulmaktadır. Bunaların sayısı süper bloktaki s_first_ino elemanında belirtilmektedir. Üzerinde çalıştığımız dosya sisteminde s_first_ino değeri 11'dir. Yani ilk 10 i-node elemanı reserve edilmiştir. İlk i-node elemanının numarası 11'dir. Örnek dosya sistemimizdeki durum şöyledir: 0. Blok Grubunun i-node Tablosu <1 numaralı i-node elemanı> <2 numaralı i-node elemanı> <3 numaralı i-node elemanı> ... <10 numaralı i-node elemanı> <11 numaralı i-node elemanı (ilk reserved olmaayan eleman)> ... Reserve edilmiş ilk i-node elemanlarının anlamları şöyledir: XT2_BAD_INO 1 bad blocks inode EXT2_ROOT_INO 2 root directory inode EXT2_ACL_IDX_INO 3 ACL index inode (deprecated?) EXT2_ACL_DATA_INO 4 ACL data inode (deprecated?) EXT2_BOOT_LOADER_INO 5 boot loader inode EXT2_UNDEL_DIR_INO 6 undelete directory inode Peekiyi ext2 dosya sisteminde bir dosyanın parçalarının (yani bloklarının) nerelerde olduğu bilgisi nerede tutulmaktadır? Anımsanacağı gibi FAT dosya sisteminde dosyanın parçalarının (orada block yerine cluster terminin kullanıldığını anımsayınız) diskin hangi cluster'larında olduğu FAT bölümünde saklanıyordu. İşte yalnızca ext2 dosya sisteminde değil i-node tabanlı dosya sistemlerinde bir dosyanın diskte hangi bloklarda bulunduğu i-node elemanın içerisinde tutulmaktadır.i-node elemanlarının genel olarak 1228 byte ya da 256 byte uzunlukta olduğunu anımsayınız. Büyük bir dosyanın blok numaralarının bu kadar alana sığmayacağı açıktır. PPekiyi o zaman dosyanın blok numaraları i-node elemanında nasıl tutulmaktadır? İşte i-node elemanında dosyanın hangi bloklerda olduğu "doğrudan (direct)", "dolaylı (indriect)", "çift dolaylı (double indirect)" ve "üç dolaylı (triple indirect)"" bloklarda tutulmaktadır. i-node elemanının demimal 40'ncı offset'inde (i-node elemanın 0x28 offset'indek, "i_blocks" isimli elemanında) 15 elemanlık her biri DWORD değerlerden oluşan bir dizi vardır. Bu diziyi şöyle gösterebiliriz: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 çift dolaylı blok numarası> 14 <üç dolaylı blok numarası> Bu durumda eğer dosya 12 blok ya da ondan daha küçükse zaten dosyanın parçalarının blok numaraları bu diznini ilk 12 elemanından doğrudan elde edilmektedir. Eğer dosya 12 bloktan büyükse bu durumda bu dizinin 12'indeksindeki elemanda yazan blok numarası dosyanın diğer bloklarının blok numaralarını tutan bloğun numarasıdır. Yani dizinin 12'inci elemanında blierilen bloğa gidildiğinde bu bloğun içerisinde blok numaraları vardır. Bu blok numaraları da dosyanın 12'inci bloğundan itibaren bloklarının numaralarını belirtmektedir. Örneğin bir blok 4096 byte olsun. Bu durumda bir blokta 1024 tane blok numarası olabilir. 12 blok numarası doğrudan olduğuna göre dolaylı blokla toplam dosyanın 1024 + 12 = 1036 tane bloğunun yeri tutulmuş olacaktır. Pekiyi ya bu sistemde dosya 1026 bloktan daha büyükse? İşte bu durumda çift dolaylı blok numarasına başvurulmaktadır. Çift dolaylı blok numarasına ilişkin bloğa gidildiğinde oradaki blok numaraları dosyanın blok numaraları değil dosyanın blok numaralarının turulduğu blok numaralarıdır. Eğer dosya çift dolaylı bloklara sığmıyorsa üç dolaylı bloğa başvurulmaktadır. Üç dolaylı blokta belirtilen blok numarasında çift dolaylı blokların numaraları vardır. Çift dolaylı blokların içerisinde dolaylı blokların numaraları vardır. Nihayet dolaylı blokların içerisinde de asıl blokların numaraları vardır. Pekiyi her bloğun 4K uzunluğunda olduğu bir sistemde bir dosyanın i-node elemanında belirtilen maksimum uzunluğu ne olabilir? İşte bu uzunluk aşağıdaki değerlerin toplamıyla elde edilebilir: 12 tane doğrudan blok = 12 * 4096 1 tane dolaylı blok = 1024 * 4096 1 tane çift dolaylı blok = 1024 * 1024 * 4096 1 tane üç dolaylı blok = 1024 * 1024 * 1024 * 4096 Toplam = 12 * 4096 + 1024 * 4096 + 1024 * 12024 * 4096 + 1024 * 1024 * 1024 * 4096 = 4448483065856 = 4 TB civarı. Şimdi aşağıdaki i-node elemanına bakıp dosya bloklarının yerlerini tespit edelim: 00010b00 a4 81 00 00 c8 79 00 00 87 5c 2e 67 87 5c 2e 67 |.....y...\.g.\.g| 00010b10 87 5c 2e 67 00 00 00 00 00 00 01 00 40 00 00 00 |.\.g........@...| 00010b20 00 00 00 00 01 00 00 00 00 08 00 00 01 08 00 00 |................| 00010b30 02 08 00 00 03 08 00 00 04 08 00 00 05 08 00 00 |................| 00010b40 06 08 00 00 07 08 00 00 00 00 00 00 00 00 00 00 |................| 00010b50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010b60 00 00 00 00 2e db 09 7c 00 00 00 00 00 00 00 00 |.......|........| 00010b70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010b80 20 00 00 00 c8 76 3d ba c8 76 3d ba c8 76 3d ba | ....v=..v=..v=.| 00010b90 87 5c 2e 67 c8 76 3d ba 00 00 00 00 00 00 00 00 |.\.g.v=.........| 00010ba0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010bb0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010bc0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010bd0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010be0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010bf0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Burada dosyanın tüm blokları 0x28'inci offset'teki doğrudan bloklarda belirtilmektedir: 00 08 00 00 => 0x800 01 08 00 00 => 0x801 02 08 00 00 => 0x802 03 08 00 00 => 0x803 04 08 00 00 => 0x804 05 08 00 00 => 0x805 06 08 00 00 => 0x806 07 08 00 00 => 0x807 Dosya 0x4 offset'inde belirtilen 0x79C8 = 31176 byte uzunluğundadır. Bu sistemde bir blok 4K olduğuna göre toplam dosyanın parçalarının 8 blok olması gerekmektedir. İşte burada söz konusu dosyanın blokları disk bölümünün başından itibaren 0x800, 0x801, 0x802, 0x803, 0x804, 0x805, 0x806 ve 0x807'inci bloklardadır. Söz konusu sistemde bir blok 4096 byte olduğuna göre dosyanın ilk bloğunun offset numarası 0x800 * 0x1000 = 0x800000 biçimindedir. Şimdi de ext2 dosya sisteminde dizin organizasyonu üzerinde duralım. Tıpkı FAT dosya sistemlerinde olduğu gibi ext2 dosya sisteminde de dizinler birer dosya gibi organize edilmiştir. (Anımsanacağı gibi dizin dosyalarından biz opendir, readdir, closedir fonksiyonlarıyla okuma yapabiliyorduk.) Yani dizinler aslında birer dosya gibidir. Dizin dosyaları "dizin girişleri (directory entries)" denilen girişlerden oluşmaktadır. ... Bir dizin girişin format şöyledir: +----------------+--------------+-------------------------+ | Offset (bytes) | Size (bytes) | Açıklama | +----------------+--------------+-------------------------+ | 0 | DWORD | i-node numarası | | 4 | WORD | Girişin toplam uzunluğu | | 6 | BYTE | Dosya isminin uzunluğu | | 7 | BYTE | Dosyanın türü | | 8 | 0-255 Bytes | Dosya ismi | +----------------+--------------+-------------------------+ Aslında buradaki bilgiler Linux'taki readdir POSIX fonksiyonu ile de alınabilmektedir. readdir fonksiyonu POSIX standartlarına göre en az iki elemana sahip olmak zorundadır. Bunlar d_ino ve d_name elemanlarıdır. Ancak Linux'taki read bize daha fazla bilgi vermektedir. Linux'taki dirent yapısı şöyledir: struct dirent { ino_t d_ino; /* Inode number */ off_t d_off; /* Not an offset; see below */ unsigned short d_reclen; /* Length of this record */ unsigned char d_type; /* Type of file; not supported by all filesystem types */ char d_name[256]; /* Null-terminated filename */ }; Dizin girişleri FAT dosya sistemindeki gibi eşit uzunlukta girişlerden oluşmamaktadır. Bunun nedeni dosya isimlerinin 0 ile 255 karakter arasında değişebilmesidir. Dizin girişlerinin hemen başında DWORD bir alanda dosyanın i-node numarası belirtilmektedir. Dizinler değişken uzunlukta olduğu için ilgili girişin toplam kaç byte uzunlukta olduğu sonraki WORD elemanda tutulmaktadır. Girişteki dosya isminin uzunluğu ise sonraki BYTE elemanında tutulmaktadır. Dosyanın türü hiç i-node elemanına erişmeden elde edilebilsin diye dizin girişlerinde de tutulmaktadır. Dosya türlerini belirten değerler şöyledir: +------------------+-------+-------------------+ | İsim | Değer | Anlamı | +------------------+-------+-------------------+ | EXT2_FT_UNKNOWN | 0 | Unknown File Type | | EXT2_FT_REG_FILE | 1 | Regular File | | EXT2_FT_DIR | 2 | Directory File | | EXT2_FT_CHRDEV | 3 | Character Device | | EXT2_FT_BLKDEV | 4 | Block Device | | EXT2_FT_FIFO | 5 | Buffer File | | EXT2_FT_SOCK | 6 | Socket File | | EXT2_FT_SYMLINK | 7 | Symbolic Link | +------------------+-------+-------------------+ Pekiyi ext2 dosya sisteminde işletim sistemi bir yol ifadesini nasıl çözümlemektedir? Örneğin "/a/b/c.txt" gibi bir yol ifadesinde "c.txt" dosyasının i-node elemanına nasıl erişmektedir? İşte kök dizin dosyasının bilgileri 2 numaralı i-node elemanındadır. İşletim sistemi önce kök dizinin i-node elemanını elde eder. Oradan kök dizinin bloklarına erişir. O bloklar içerisinde ilgili girişi arar. İşlemlerini bu biçimde devam ettirir. Örneğin "/a/b/c.txt" dosyasının i-node elemanına erişmek için önce kök dizinde "a" girişini arar. Sonra "a" girişinin dizin olduğunu doğrular. Sonra "a" dizininde "b" girişini arar. "b" girişinin de dosya olduğunu doğrular. Sonra "b" girişinin içerisinde "c.txt" arar ve hedef dosyanın i-node bilgilerine erişir. Şimdi adım adım elimizdeki disk bölümünde "/a/b/c.txt" dosyasının yerini bulmaya çalışalım. Tabii buradaki kök dizin aslında mount edilmiş dosya sisteminin köküdür. Biz bu dosya sistemini kursumuzda aşağıdaki noktaya mount ettik: "/home/kaan/Study/UnixLinux-SysProg/DiskIO-FileSystems" Kök dizinin i-node elemanı (2 numaralı i-node elemanı) aşağıda verilmiştir: 00010100 ed 41 00 00 00 10 00 00 a8 69 4c 67 a7 69 4c 67 |.A.......iLg.iLg| 00010110 a7 69 4c 67 00 00 00 00 00 00 04 00 08 00 00 00 |.iLg............| 00010120 00 00 00 00 04 00 00 00 2b 06 00 00 00 00 00 00 |........+.......| 00010130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00010180 20 00 00 00 a0 15 ed 2d a0 15 ed 2d 1c 7e f4 e6 | ......-...-.~..| 00010190 42 5c 2e 67 00 00 00 00 00 00 00 00 00 00 00 00 |B\.g............| 000101a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000101b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000101c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000101d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000101e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000101f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Burada 0x28'inci offset'teki i_blocks elemanının yalnızca ilkinin dolu olduğunu görüyoruz. Demek ki kök dizin tek bir bloktan oluşmaktadır. Kök dizinin blok numarası 0x62B'dir. Şimdi 0x62b bloğunun offset'ini hesaplayalım. Bunun için bu değeri 0x1000 (4096) ile çarpmamız gerekir: 0x62B * 0x1000 = 0x62B000 (6467584) Diskin bu offset'indeki değerler şöyledir: 0062b000 02 00 00 00 0c 00 01 02 2e 00 00 00 02 00 00 00 |................| 0062b010 0c 00 02 02 2e 2e 00 00 0b 00 00 00 14 00 0a 02 |................| 0062b020 6c 6f 73 74 2b 66 6f 75 6e 64 00 00 0c 00 00 00 |lost+found......| 0062b030 10 00 07 01 73 74 64 69 6f 2e 68 00 b2 61 00 00 |....stdio.h..a..| 0062b040 c4 0f 01 02 61 00 00 00 00 00 00 00 00 00 00 00 |....a...........| 0062b050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0062b060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0062b070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0062b080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0062b090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0062b0a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0062b0b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Buradaki dizin girişlerini çözelim. Dizin giriş formatını aşağıda yeniden veriyoruz: +----------------+--------------+-------------------------+ | Offset (bytes) | Size (bytes) | Açıklama | +----------------+--------------+-------------------------+ | 0 | DWORD | i-node numarası | | 4 | WORD | Girişin toplam uzunluğu | | 6 | BYTE | Dosya isminin uzunluğu | | 7 | BYTE | Dosyanın türü | | 8 | 0-255 Bytes | Dosya ismi | +----------------+--------------+-------------------------+ İlk dizin girişinin i-node numarası 2'dir. Bu girişin uzunluğu 0x0C = 12'dir. O halde bu dizin girişi şöyledir: 0062b000 02 00 00 00 0c 00 01 02 2e 00 00 00 02 Burada dosya ismi 1 karakter uzunluktadır. Dosya ismi yalnızca 0x2E karakterinde oluşmaktadır. Bu karakter de "." karakteridir. Sonraki dizin girişinin i-node numarası yine 2'dir. Girişin uzunluğu yine 0xC = 12'dir. O halde giriş şöyledir: 0062b000 02 00 00 00 |................| 0062b010 0c 00 02 02 2e 2e 00 00 |................| Buradaki dosya uzunluğunun 2 olduğu görülmektedir. Dosya ismi de 0x2E 0x2E karakterinden oluşmaktadır. Bu da ".." ismidir. Her dizinin ilk iki elemanının bu biçimde olduğunu anımsayınız. Sonraki giriş ise şöyledir: 0062b010 0b 00 00 00 14 00 0a 02 |................| 0062b020 6c 6f 73 74 2b 66 6f 75 6e 64 00 00 |lost+found......| Burada dosya i-node numarası 0x0b = 11'dir. Dizin girişinin uzunluğu 0x14 = 20'dir. Dosya isminin uzunluğu 0xA = 10'dur. Dosya ismi "lost+found" biçimindedir. Sonraki giriş ise şöyledir: 0062b020 0c 00 00 00 |lost+found......| 0062b030 10 00 07 01 73 74 64 69 6f 2e 68 00 |....stdio.h..a..| Buaraki girişin i-node numarası 0xC = 12'dir. Girişin toplam uzunluğu 0x10 = 16'dır. Dosyanın isminin uzunluğu 7'dir. Dosya ismi "stdio.h" biçimindedir. Sonraki giriş ise şöyledir: 0062b030 b2 61 00 00 |....stdio.h..a..| 0062b040 c4 0f 01 02 61 00 00 00 00 00 00 00 00 00 00 00 |....a...........| Burada dosyanın i-node numarası 0x61B2 = 25010'dur. Girişin uzunluğu 0xC4 = 196'dır. (Bu değerin çok uzun olması önemli değildir. Çünkü bu dizindeki son dosyadır.) Dosya isminin uzunluğu 1'dir. Dosya türü 0x02'dir. Yani bu giriş bir dizin belirtmektedir. Dosya ismi "a" biçimindedir. İşte işletim sistemi 0x61B2 = 25010'ıncı i-node elemanında bu dizinin bilgilerinin olduğunu tespit eder ve o i-node elemanını okur. Bu i-node elemanı aşağıdaki gibidir: 08010100 ed 41 00 00 00 10 00 00 f8 2b 53 67 a7 69 4c 67 |.A.......+Sg.iLg| 08010110 a7 69 4c 67 00 00 00 00 00 00 03 00 08 00 00 00 |.iLg............| 08010120 00 00 00 00 02 00 00 00 2b 86 00 00 00 00 00 00 |........+.......| 08010130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 08010140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 08010150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 08010160 00 00 00 00 f9 65 fb 3c 00 00 00 00 00 00 00 00 |.....e.<........| 08010170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 08010180 20 00 00 00 a0 15 ed 2d a0 15 ed 2d 04 7d e1 7b | ......-...-.}.{| 08010190 a7 69 4c 67 a0 15 ed 2d 00 00 00 00 00 00 00 00 |.iLg...-........| 080101a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 080101b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 080101c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 080101d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 080101e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 080101f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| "a" dizinine ilişkin i-node elemanının 0x28'inci offset'teki blokları bir tanedir ve blok numarası 0x862B = 34547'dir. Bu bloğun offset'i de 34547 * 4096 = 140685312'dir. Dizine ilişkin dizin bloğunun içeriği şöyledir: 0862b000 b2 61 00 00 0c 00 01 02 2e 00 00 00 02 00 00 00 |.a..............| 0862b010 0c 00 02 02 2e 2e 00 00 b3 61 00 00 e8 0f 01 02 |.........a......| 0862b020 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |b...............| 0862b030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0862b040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0862b050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0862b060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0862b070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0862b080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0862b090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0862b0a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Bu dizin girişlerine bakıldığında "b" isimli girişin bulunduğu görülmektedir. İşte yol ifadesi bu aşamalardan geçilerek çözümlenmektedir. Pekiyi bir dosya oluşturulurken boş bloklar nasıl tespit edilmektedir? İşte her blok grup betimleyicisi kendi blok grubundaki boş blokları "block bitmap" denilen tabloda bit düzeyinde tutmaktadır. Blok bitmap tablosu her biti bir bloğun boş mu dolu mu olduğunu tutmaktadır. Blok grup betimleyicisinde yalnızca blok grubunun yeri tutulur. Bunun blok uzunluğu ilgili blok gruplarındaki blok sayısına bakılarak tespit edilmelidir. Her blok grubunda eşit sayıda blok bulunur. Bu sayı süper blok içerisindeki s_blocks_per_group elemanında saklanmaktadır. Aşağıda bir grup betimleyicisinin blok bitmap tablosunun bir bölümünü görüyorsunuz: 0000e000 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e010 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e020 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e030 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e040 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e050 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e060 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e070 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e080 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e090 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e0a0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e0b0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| 0000e0c0 ff ff ff ff ff ff 01 00 00 00 00 00 00 00 00 00 |................| 0000e0d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000e0e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000e0f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000e100 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Buradaki FF byte'larına dikkat ediniz. FF byte'ı aslında ikilik sistemde 1111 1111 bitlerine karşılık gelmektedir. Yani bu bloklar tamamen tahsis edilmiştir. 00 olan byte'lara ilişkin bloklar tahsis edilmemiş durumdadır. i-node elemanlarının tahsis edilip edilmediğine yönelik de benzer bir tablo tutulmaktadır. Buna "i-node bitmap" tablosu denilmektedir. Her blok grubunda bir i-node bitmap tablosu bulunur. Bu tablo da bitlerden oluşmaktadır. Her bit ilgili i-node elemanının boş mu dolu mu olduğunu belirtir. i-node bitmap tablosunun yeri de yine blok grup betimleyicisinde tutulmaktadır. Bu tablonun uzunluğu da yine süper bloktaki "bir grup bloğundaki i-node elemanlarının sayısı" dikkate alınarak tespit edilmektedir. Aşağıda örnek bir i-node bitmap tablosunun bir kısmını görüyorsunuz: 0000f000 ff 0f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000f010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000f020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000f030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000f040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000f050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000f060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000f070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 0000f080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| Bu blok grubunda toplam 12 i-node elemanı tahsis edilmiş durumdadır. Biz yukarıdaki örneklerde dosya sistemini tanıyabilmek için manuel işlemler yaptık. Pekiyi bu işlemleri programlama yoluyla nasıl yapabiliriz? Yukarıda açıkladığımız dosya sistemi alanlarına ilişkin yapılar çeşitli kütüphanelerin içerisinde hazır bir biçimde bulunmaktadır. Örneğin "libext2fs" kütüphanesi kurulduğunda dosyasında tüm yapı bildirimleri bulunacaktır. >>> "libext2fs" kütüphanesi: Kütüphanenin kurulumunu şöyle yapabilirsiniz: $ sudo apt-get install libext2fs-dev Aslında bu kütüphane ve başlık dosyası yalnızca ext2 dosya sistemine ilişkin değil, ext2 ve ext4 dosya sistemine ilişkin de yapıları ve fonksiyonları bulundurmaktadır. * Örnek 1, Örneğin başlık dosyası içerisindeki süper blok yapısı aşağıdaki gibi bildirilmiştir: struct ext2_super_block { /*000*/ __u32 s_inodes_count; /* Inodes count */ __u32 s_blocks_count; /* Blocks count */ __u32 s_r_blocks_count; /* Reserved blocks count */ __u32 s_free_blocks_count; /* Free blocks count */ /*010*/ __u32 s_free_inodes_count; /* Free inodes count */ __u32 s_first_data_block; /* First Data Block */ __u32 s_log_block_size; /* Block size */ __u32 s_log_cluster_size; /* Allocation cluster size */ /*020*/ __u32 s_blocks_per_group; /* # Blocks per group */ __u32 s_clusters_per_group; /* # Fragments per group */ __u32 s_inodes_per_group; /* # Inodes per group */ __u32 s_mtime; /* Mount time */ /*030*/ __u32 s_wtime; /* Write time */ __u16 s_mnt_count; /* Mount count */ __s16 s_max_mnt_count; /* Maximal mount count */ __u16 s_magic; /* Magic signature */ __u16 s_state; /* File system state */ __u16 s_errors; /* Behaviour when detecting errors */ __u16 s_minor_rev_level; /* minor revision level */ /*040*/ __u32 s_lastcheck; /* time of last check */ __u32 s_checkinterval; /* max. time between checks */ __u32 s_creator_os; /* OS */ __u32 s_rev_level; /* Revision level */ /*050*/ __u16 s_def_resuid; /* Default uid for reserved blocks */ __u16 s_def_resgid; /* Default gid for reserved blocks */ /* * These fields are for EXT2_DYNAMIC_REV superblocks only. * * Note: the difference between the compatible feature set and * the incompatible feature set is that if there is a bit set * in the incompatible feature set that the kernel doesn't * know about, it should refuse to mount the filesystem. * * e2fsck's requirements are more strict; if it doesn't know * about a feature in either the compatible or incompatible * feature set, it must abort and not try to meddle with * things it doesn't understand... */ __u32 s_first_ino; /* First non-reserved inode */ __u16 s_inode_size; /* size of inode structure */ __u16 s_block_group_nr; /* block group # of this superblock */ __u32 s_feature_compat; /* compatible feature set */ /*060*/ __u32 s_feature_incompat; /* incompatible feature set */ __u32 s_feature_ro_compat; /* readonly-compatible feature set */ /*068*/ __u8 s_uuid[16] __nonstring; /* 128-bit uuid for volume */ /*078*/ __u8 s_volume_name[EXT2_LABEL_LEN] __nonstring; /* volume name, no NUL? */ /*088*/ __u8 s_last_mounted[64] __nonstring; /* directory last mounted on, no NUL? */ /*0c8*/ __u32 s_algorithm_usage_bitmap; /* For compression */ /* * Performance hints. Directory preallocation should only * happen if the EXT2_FEATURE_COMPAT_DIR_PREALLOC flag is on. */ __u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/ __u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */ __u16 s_reserved_gdt_blocks; /* Per group table for online growth */ /* * Journaling support valid if EXT2_FEATURE_COMPAT_HAS_JOURNAL set. */ /*0d0*/ __u8 s_journal_uuid[16] __nonstring; /* uuid of journal superblock */ /*0e0*/ __u32 s_journal_inum; /* inode number of journal file */ __u32 s_journal_dev; /* device number of journal file */ __u32 s_last_orphan; /* start of list of inodes to delete */ /*0ec*/ __u32 s_hash_seed[4]; /* HTREE hash seed */ /*0fc*/ __u8 s_def_hash_version; /* Default hash version to use */ __u8 s_jnl_backup_type; /* Default type of journal backup */ __u16 s_desc_size; /* Group desc. size: INCOMPAT_64BIT */ /*100*/ __u32 s_default_mount_opts; /* default EXT2_MOUNT_* flags used */ __u32 s_first_meta_bg; /* First metablock group */ __u32 s_mkfs_time; /* When the filesystem was created */ /*10c*/ __u32 s_jnl_blocks[17]; /* Backup of the journal inode */ /*150*/ __u32 s_blocks_count_hi; /* Blocks count high 32bits */ __u32 s_r_blocks_count_hi; /* Reserved blocks count high 32 bits*/ __u32 s_free_blocks_hi; /* Free blocks count */ __u16 s_min_extra_isize; /* All inodes have at least # bytes */ __u16 s_want_extra_isize; /* New inodes should reserve # bytes */ /*160*/ __u32 s_flags; /* Miscellaneous flags */ __u16 s_raid_stride; /* RAID stride in blocks */ __u16 s_mmp_update_interval; /* # seconds to wait in MMP checking */ __u64 s_mmp_block; /* Block for multi-mount protection */ /*170*/ __u32 s_raid_stripe_width; /* blocks on all data disks (N*stride)*/ __u8 s_log_groups_per_flex; /* FLEX_BG group size */ __u8 s_checksum_type; /* metadata checksum algorithm */ __u8 s_encryption_level; /* versioning level for encryption */ __u8 s_reserved_pad; /* Padding to next 32bits */ __u64 s_kbytes_written; /* nr of lifetime kilobytes written */ /*180*/ __u32 s_snapshot_inum; /* Inode number of active snapshot */ __u32 s_snapshot_id; /* sequential ID of active snapshot */ __u64 s_snapshot_r_blocks_count; /* active snapshot reserved blocks */ /*190*/ __u32 s_snapshot_list; /* inode number of disk snapshot list */ #define EXT4_S_ERR_START ext4_offsetof(struct ext2_super_block, s_error_count) __u32 s_error_count; /* number of fs errors */ __u32 s_first_error_time; /* first time an error happened */ __u32 s_first_error_ino; /* inode involved in first error */ /*1a0*/ __u64 s_first_error_block; /* block involved in first error */ __u8 s_first_error_func[32] __nonstring; /* function where error hit, no NUL? */ /*1c8*/ __u32 s_first_error_line; /* line number where error happened */ __u32 s_last_error_time; /* most recent time of an error */ /*1d0*/ __u32 s_last_error_ino; /* inode involved in last error */ __u32 s_last_error_line; /* line number where error happened */ __u64 s_last_error_block; /* block involved of last error */ /*1e0*/ __u8 s_last_error_func[32] __nonstring; /* function where error hit, no NUL? */ #define EXT4_S_ERR_END ext4_offsetof(struct ext2_super_block, s_mount_opts) /*200*/ __u8 s_mount_opts[64] __nonstring; /* default mount options, no NUL? */ /*240*/ __u32 s_usr_quota_inum; /* inode number of user quota file */ __u32 s_grp_quota_inum; /* inode number of group quota file */ __u32 s_overhead_clusters; /* overhead blocks/clusters in fs */ /*24c*/ __u32 s_backup_bgs[2]; /* If sparse_super2 enabled */ /*254*/ __u8 s_encrypt_algos[4]; /* Encryption algorithms in use */ /*258*/ __u8 s_encrypt_pw_salt[16]; /* Salt used for string2key algorithm */ /*268*/ __le32 s_lpf_ino; /* Location of the lost+found inode */ __le32 s_prj_quota_inum; /* inode for tracking project quota */ /*270*/ __le32 s_checksum_seed; /* crc32c(orig_uuid) if csum_seed set */ /*274*/ __u8 s_wtime_hi; __u8 s_mtime_hi; __u8 s_mkfs_time_hi; __u8 s_lastcheck_hi; __u8 s_first_error_time_hi; __u8 s_last_error_time_hi; __u8 s_first_error_errcode; __u8 s_last_error_errcode; /*27c*/ __le16 s_encoding; /* Filename charset encoding */ __le16 s_encoding_flags; /* Filename charset encoding flags */ __le32 s_reserved[95]; /* Padding to the end of the block */ /*3fc*/ __u32 s_checksum; /* crc32c(superblock) */ }; Kütüphane aşağıdaki bağlantıdan indirebileceğiniz pdf dosyasında dokümante edilmiştir: https://www.dubeyko.com/development/FileSystems/ext2fs/libext2fs.pdf Şimdi "libext2fs" kütüphanesinin basit bir kullanımı üzerinde duracağız. Kütüphanenin temel başlık dosyası isimli dosyadır. Bu dosyanın include edilmesi gerekir. #include Kütüphaneyi kullanan programlar derlenirken link aşamasında "-lext2fs" seçeneğinin bulundurulması gerekir. Örneğin: $ gcc -o sample sample.c -lext2fs Kütüphane içerisindeki fonksiyonların çoğunun geri dönüş değerleri errcode_t türündendir. errcode_t türü long biçimde typedef edilmiştir. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda hata ile ilgili bir değere geri dönmektedir. Hatayı yazdırmak için error_message isimli fonksiyon bulunmaktadır. Bu fonksiyona errcode_t değeri parametre olarak verilir, fonksiyon da hata yazısına ilişkin static bir dizinin başlangıç adresine geri döner. * Örnek 1, ext2_filsys fs; errcode_t err; if ((err = ext2fs_open("/dev/loop0", 0 /* EXT2_FLAG_RW */, 0, 0, unix_io_manager, &fs)) != 0) { fprintf(stderr, "%s\n", error_message(err)); exit(EXIT_FAILURE); } Bu kütüphane kullanılarak yazılmış programlar genel olarak aygıtlara eriştiği için "sudo" ile çalıştırılması gerekir. Aksi takdirde "Permission denied (EACCESS)" hatası ortaya çıkacaktır. Kütüphanenin kullanılması için, -> ilk yapılacak işlem dosya sisteminin ext2fs_open fonksiyonu ile açılmasıdır. Fonksiyonun prototipi şöyledir: #include errcode_t ext2fs open (const char *name, int args, int superblock, int block size, io_manager manager, ext2_filsys *ret fs); Fonksiyonun birinci parametresi dosya sisteminin bulunduğu dosyanın ya da aygıt dosyasının yol ifadesini almaktadır. Parametrelerin çoğu default 0 geçilebilir. Ancak io_manager parametresi için unix_io_manager argümanının girilmesi gerekmektedir. Fonksiyon ext2fil_sys türünden handle belirten bir nesne vermektedir. Bu tür şöyle typedef edilmiştir: typedef struct struct_ext2_filsys *ext2_filsys; Dosya sistemine ilişkin tüm bilgiler bu struct_ext2_filsys yapısının içerisindedir. Bu yapı şöyle bildirilmiştir: struct struct_ext2_filsys { errcode_t magic; io_channel io; int flags; char * device_name; struct ext2_super_block * super; unsigned int blocksize; int fragsize; dgrp_t group_desc_count; unsigned long desc_blocks; struct opaque_ext2_group_desc * group_desc; unsigned int inode_blocks_per_group; ext2fs_inode_bitmap inode_map; ext2fs_block_bitmap block_map; ... }; Örneğin bu yapının super elemanı süper blok bilgilerinin bulunduğu ext2_super_block isimli yapı nesnesinin adresini vermektedir. * Örnek 1, ext2_filsys fs; errcode_t err; if ((err = ext2fs_open("/dev/loop0", 0 /* EXT2_FLAG_RW */, 0, 0, unix_io_manager, &fs)) != 0) { fprintf(stderr, "cannot open file system!..\n"); exit(EXIT_FAILURE); } -> Açılan dosya sisteminin işlem bitince ext2fs_close fonksiyonu ile kapatılması gerekir. Fonksiyonun prototipi, #include errcode_t ext2fs close (ext2_filsys fs); biçimindedir. * Örnek 1, ext2_filsys fs; errcode_t err; if ((err = ext2fs_open("/dev/loop0", 0 /* EXT2_FLAG_RW */, 0, 0, unix_io_manager, &fs)) != 0) { fprintf(stderr, "cannot open file system!..\n"); exit(EXIT_FAILURE); } /* ... */ ext2fs_close(fs); -> Belli bir numaraya sahip i-node elemanını elde edebilmek için ext2fs_read_inode fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include errcode_t ext2fs_read_inode(ext2_filsys fs, ext2_ino_t ino, struct ext2_inode *inode); Fonksiyonun birinci parametresi dosya sistemini temsil eden handle değeridir. İkinci parametre bilgileri elde edilecek i-node elemanın numarasını belirtir. Üçüncü parametre de i-node bilgilerinin yerleştirileceği yapının adresini almaktadır. * Örnek 1, if ((err = ext2fs_read_inode(fs, 13, &inode)) != 0) { fprintf(stderr, "cannot read inode!..\n"); exit(EXIT_FAILURE); } Aşağıda genel kullanıma ilişkin örnekler verilmiştir: * Örnek 1, #include #include int main(void) { ext2_filsys fs; errcode_t err; if ((err = ext2fs_open("/dev/loop0", 0 /* EXT2_FLAG_RW */, 0, 0, unix_io_manager, &fs)) != 0) { fprintf(stderr, "cannot open file system!..\n"); exit(EXIT_FAILURE); } printf("Number of i-node: %lu\n", (unsigned long)fs->super->s_inodes_count); printf("Total Block: %lu\n", (unsigned long)fs->super->s_blocks_count); /* ... */ ext2fs_close(fs); return 0; } Öte yandan "e2fsprogs" isimli pakette de ext2, ext3 ve ext4 dosya sistemlerine ilişkin pek çok yardımcı program bulunmaktadır. Örneğin bizim daha önce kullandığımız "dumpe2fs" programı bu paketin bir parçasıdır. Buradaki programlar "libext2fs" kütüphanesi kulanılarak yazılmıştır. Paket pek çok Linux dağıtımında default biçimde kurulu durumdadır. Eğer dağıtımınızda kurulu değilse bu paketi aşağıdaki gibi kurabilirsiniz: $ sudo apt-get install e2fsprogs Bir diske birden fazla bağımsız dosya sisteminin (ve belki de işletim sisteminin) yüklenebilmesi için diskin mantıksal bakımdan parçalara ayrılması gerekmektedir. Diskin mantıksal bakımdan parçalara ayrılmasına ise "disk bölümlemesi" denilmektedir. Disk bölümlemesi aslında disk bölümlerinin hangi sektörden başlayıp kaç sektör uzunluğunda olduğunun belirlenmesi anlamına gelmektedir. Böylece her dosya sistemi başkasının alanına müdahale etmeden yalnızca o disk bölümünü kullanmaktadır. Diskteki disk bölümleri hakkında bilgileri barındıran tabloya "disk bölümleme tablosu (disk partition table)" denilmektedir. Bugün için iki disk bölümleme tablo formatı kullanılmaktadır: -> Klasik (legacy) MBR Disk Bölümleme Tablo Formatı -> Modern UEFI BIOS Sistemlerinin Kullandığı GPT (Guid Partition Table) Formatı UEFI BIOS'lar GPT disk bölümleme tablosu kullanırken eski sistemler ve gömülü sistemler genel olarak klasik MBR disk bölümleme tablosunu kullanmaktadır. Gömülü sistemler için oluşturduğumuz SD kartlar'daki disk bölümleme tablosu klasik (legacy) disk bölümleme tablosudur. Ancak bugünkü büyük çaplı UEFI BIOS'lar önce GPT disk bölümleme tablosuna bakmakta eğer onu bulamazsa klasik disk bölümleme tablosunu aramaktadır. Yani geçmişe doğru uyum korunmaya çalışılmıştır. Daha önceden de belirttiğimiz gibi disk sistemlerine okuma ve yazma işlemleri blok aygıt sürücüleri tarafından yapılmaktadır. Dolayısıyla bir UNIX türevi sistemde her disk için "/dev" dizininin altında bir blok aygıt dosyası bulunmaktadır. Aynı zamanda her disk bölümü için de bir blok aygıt dosyası bulunur. Böylece bir diskin bütünü üzerinde de yalnızca onun belli bir bölümü üzerinde de çalışılabilir. Daha önceden de kullanmış olduğumuz "lsblk" komutu bize sistemimizdeki diskler ve onların bölümleri hakkında bilgiler vermektedir. Örneğin çalıştığımız sistemde "lsblk" komutunu uyguladığımızda şöyle bir çıktı ile karşılaşmaktayız: $ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS loop0 7:0 0 195,3M 0 loop /home/kaan/Study/UnixLinux-SysProg/DiskIO-FileSystems/ext2 sda 8:0 0 60G 0 disk ├─sda1 │ 8:1 0 1M 0 part ├─sda2 │ 8:2 0 513M 0 part /boot/efi └─sda3 8:3 0 59,5G 0 part / sr0 11:0 1 1024M 0 rom Buradaki "sda" aygıtı bir diski bütün olarak ele almak için kullanılmaktadır. Bu diskin 3 tane disk bölüme vardır. Bu disk bölümleri de "sda1", "sda2" ve "sda3" aygıtlarıdır. Bu aygıtlara ilişkin "/dev" dizini altında aygıt dosyaları bulunmaktadır. Örneğin: $ ls -l /dev/sda* brw-rw---- 1 root disk 8, 0 Ara 6 15:08 /dev/sda brw-rw---- 1 root disk 8, 1 Ara 6 15:08 /dev/sda1 brw-rw---- 1 root disk 8, 2 Ara 6 15:08 /dev/sda2 brw-rw---- 1 root disk 8, 3 Ara 6 15:08 /dev/sda3 Diskleri temsil eden isimler o diskin türüne göre değişebilmektedir. Normal hard diskler için isimlendirme "sda", "sdb", "sdc" biçiminde yapılmaktadır. Bunların bölümleri de "sda1", "sda2", "sdb1, "sdb2" biçiminde yapılır. MicroSD kartlar "mmcblk0", "mmcblk1", "mmcblk2" biçiminde isimlendirilmektedir. Bunların bölümleri de "mmcblk0p1", "mmcblk0p2", "mmcblk1p1", "mmcblk1p2" biçiminde yapılmaktadır. Eğer disk bir loop aygıtı biçiminde oluşturulmuşsa disk bölümleri "loop0p1", "loop0p2", "loop1p1", "loop1p2" biçiminde isimlendirilmektedir. Şimdi de disk bölümleme formatlarını inceleyelim: >> "Klasik (legacy) MBR Disk Bölümleme Tablo Formatı" : Klasik MBR (legacy) disk bölümlendirmesinde diskin ilk sektörüne (0 numaralı sektörüne) MBR (Master Boot Record) sektörü denilmektedir. MBR sektörünün sonundaki 2 byte MBR'nin bilinçli olarak oluşturulduğunu belirten sihirli bir sayıdan (magic number) oluşmaktadır. Bu sihirli sayı hex olarak 55 AA biçimindedir. Aşağıda "loop0" aygıtı üzerinde oluşturulmuş bir MBR sektörü görülmektedir. * Örnek 1, $ sudo hexdump /dev/loop0 -C -v -n 512 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001b0 00 00 00 00 00 00 00 00 03 74 de d6 00 00 80 01 |.........t......| 000001c0 01 00 83 20 0d 13 3f 00 00 00 01 b0 04 00 00 20 |... ..?........ | 000001d0 0e 13 83 3e 18 26 40 b0 04 00 c0 af 04 00 00 00 |...>.&@.........| 000001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| Sektörün sonunun 55 AA ile bittiğine dikkat ediniz. Klasik MBR Disk Bölümlemesi'nde MBR sektörünün sonundaki 64 byte'a "Disk Bölümleme Tablosu (Disk Partition Table)" denilmektedir. Tabii sektörün sonunda hex olarak 55 AA bulunduğu için disk bölümleme tablosu da bu 55 AA byte'larının hemen gerisindeki 64 byte'tadır. O halde MBR sektörünün sonu aşağıdaki gibidir: ... <64 byte (Disk Bölümleme Tablosu)> 55 AA Yukarıdaki MBR sektörünün son 64 byte'ı ve 55 AA değerleri aşağıda verilmiştir: 80 01 |.........t......| 000001c0 01 00 83 20 0d 13 3f 00 00 00 01 b0 04 00 00 20 |... ..?........ | 000001d0 0e 13 83 3e 18 26 40 b0 04 00 c0 af 04 00 00 00 |...>.&@.........| 000001e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| Başka bir deyişle Disk Bölümleme Tablosu MBR sektörünün 0x1BE (446) offset'inden başlayıp 64 byte sürmektedir. Disk Bölümleme Tablosu'ndaki her disk bölümü 16 byte ile betimlenmektedir. Dolayısıyla klasik Disk Bölümleme Tablosu 4 disk bölümünü barındırmaktadır. Pekiyi bu durumda 4'ten fazla disk bölümü oluşturulamaz mı? İşte "Genişletilmiş Disk Bölümü (Extended Disk Partition)" kavramı ile bu durum mümkün hale getirilmiştir. Yukarıdaki Disk Bölümleme Tablosu'nun 16 byte'lık disk bölümleri aşağıda verilmiştir: 80 01 01 00 83 20 0d 13 3f 00 00 00 01 b0 04 00 00 20 0e 13 83 3e 18 26 40 b0 04 00 c0 af 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Disk Bölümleme Tablosu'ndaki 16 byte'lık disk bölümünün içeriği şöyledir: +--------------+----------------+---------------------------------------------------------------------+ | Offset (Hex) | Uzunluk | Anlamı | +--------------+----------------+---------------------------------------------------------------------+ | 0 | 1 BYTE | Disk Bölümünün Aktif Olup Olmadığı Bilgisi | | 1 | 3 BYTE | Disk Bölümünün Eski Sistemdeki (CHS Sistemindeki) Başlangıç Sektörü | | 4 | 1 BYTE | Sistem ID Değeri | | 5 | 3 BYTE | Disk Bölümünün Eski Sistemdeki (CHS Sistemindeki) Bitiş Sektörü | | 8 | 4 BYTE (DWORD) | Disk Bölümünün LBA Sistemindeki Başlangıç Sektörü | | C | 4 BYTE (DWORD) | Disk Bölümündeki Sektör Sayısı (Disk Bölümünün Uzunluğu) | +--------------+----------------+---------------------------------------------------------------------+ -> 4 disk bölümünden yalnızca bir tanesi aktif olabilmektedir. Sistem aktif disk bölümünden boot edilmektedir. Aktif disk bölümü için 0x80 değeri, aktif olmayan disk bölümü için 0x00 değeri kullanılmaktadır. -> Eskiden diskteki bir sektörün yeri "hangi yüzde (her yüzü bir kafa okuduğu için, hangi kafada), hangi track'te (track'e silindir (cylinder) de denilmektedir) ve hangi sektör diliminde olduğu bilgisiyle ifade ediliyordu. Bu koordinat sistemine CHS (Cylinder-Head-Sector) koordinat sistemi deniyordu. Sonra bu koordinat sisteminden vazgeçildi. Sektörün yeri ilk sektör 0 olmak üzere tek bir sayıyla temsil edilmeye başlandı. -> Her disk bölümünde farklı bir işletim sisteminin kullandığı dosya sistemi bulunuyor olabilir. "Sistem ID Değeri" o disk bölümünde hangi işletim sistemine ilişkin bir dosya sisteminin bulunduğunu belirtmektedir. Böylece Disk Bölümleme Tablosu'nu inceleyen kişiler disk bölümlerinin hangi işletim sistemi için oluşturulduğunu anlayabilmektedir. Tüm Sistem ID Değerleri için bunların listelendiği dokümanlara başvurabilirsiniz. Biz burada birkaç System ID değerini verelim: 0C: Windows FAT32 Sistemi 0E: Windows FAT Sistemi 0F: Genişletilmiş Disk Bölümü 83: Linux Dosya Sistemlerinden Birisi 82: Linux İçin Swap Alanı Olarak Kullanılacak Disk Bölümü -> Bir disk bölümü için en önemli iki bilgi onun diskin hangi sektöründen başlayıp kaç sektör uzunlukta olduğudur. Yani disk bölümünün başlangıç sektör numarası ve toplam sektör sayısıdır. İşletim sistemleri böylece kendileri için belirlenmiş olan disk bölümlerinin dışına erişmezler. Yani disk bölümleri adeta disk içerisindeki disklerin yerlerini belirtmektedir. -> 90'lı yıllarla birlikte diskteki sektörlerin adreslenmesi için CHS sistemi yavaş yavaş bırakılmaya başlanmış LBA (Logical Block Address) denilen sisteme geçilmiştir. Bu sistemde diskin ilk sektörü 0 olmak üzere her sektöre artan sırada bir tamsayı karşılık düşürülmüştür. İşte bu koordinat sistemine LBA denilmektedir. Artık MBR Disk Bölümleri'nde disk bölümünün başlangıç sektörü LBA sistemine göre belirtilmektedir. -> LBA sisteminde bir disk bölümünde en fazla 2^32 tane sektör bulunabilir. Bir sektör 2^9 (512) byte olduğuna göre MBR Disk Bölümleme Tablosu en fazla 2^41 = 2TB diskleri destekleyebilmektedir. Gömülü sistemlerde henüz bu büyüklükte diskler kullanılmadığı için klasik MBR Disk Bölümleme Tablosu iş görmektedir. Ancak masaüstü sistemlerde artık bu sınır aşılmaktadır. İşte UEFI BIOS'lar tarafından kullanılan "GUID Disk Bölümlemesi (GPT)" bu sınırı çok daha ötelere taşımaktadır. Disk Bölümleme Tablosu manuel bir biçimde oluşturulabilir. Ancak Disk Bölümleme Tablosu üzerinde işlem yapan çeşitli araçlar da vardır. Linux sistemlerinde en yaygın kullanılan iki araç "fdisk" ve "gparted" isimli araçlardır. fdisk komut satırından kullanılabilen basit bir programdır. "gparted" ise GUI arayüzü ile görsel bir biçimde aynı işlemleri yapmaktadır. "fdisk" temel bir araçtır. Dolayısıyla Linux sistemleri kurulduğunda büyük olasılıkla zaten kurulmuş olarak sisteminizde bulunuyor olacaktır. Ancak "gparted" programını aşağıdaki gibi siz kurmalısınız: $ sudo apt-get install gparted Bu noktada Linux sistemlerindeki fdisk programının kullanımı üzerinde duracağız. Bu tür denemeleri loop aygıtları üzerinde yapmalısınız. fdisk kullanımını maddeler halinde açıklayalım. -> Hangi disk üzerinde işlem yapılacaksa o diske ilişkin aygıt dosyası fdisk programına komut satırı argümanı olarak verilmelidir. Tabii disk aygıt dosyaları "root" kullanıcısına ilişkin olduğu için fdisk programı da genellikle "sudo" ile çalıştırılır. Örneğin önce blok aygıt sürücülerimize bakalım: ****************************************************** $ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT sda 8:0 0 80G 0 disk ├─sda1 8:1 0 512M 0 part /boot/efi ├─sda2 8:2 0 1K 0 part └─sda5 8:5 0 79,5G 0 part / sr0 11:0 1 1024M 0 rom ****************************************************** Burada "sda" diski bir bütün olarak gösteren aygıt sürücüsüdür. Bu aygıt sürücüye ilişkin aygıt dosyası "/dev/sda" biçimindedir. "sda1", "sda2" ve "sda5" disk üzerindeki disk bölümleridir. Bizim bölümlendirme için diski bir bütün olarak ele almamız gerekir. Bu nedenle "sda" diski için fdisk programı şöyle çalıştırılmalıdır: $ sudo fdisk /dev/sda Tabi biz örneğimizde loop aygıtı kullanacağız. Bu durumda loop aygıtını şöyle kullanıma hazır hale getirebiliriz: ****************************************************** $ dd if=/dev/zero of=disk.img bs=300M count=1 1+0 kayıt girdi 1+0 kayıt çıktı 314572800 bytes (315 MB, 300 MiB) copied, 1,23241 s, 255 MB/s ****************************************************** Buradan diski temsil eden içi sıfırlarla dolu 300MB'lik bir dosya oluşturduk. Şimdi bu dosyayı "/dev/loop0" aygıt dosyası ile bir blok aygıtı gibi gösterelim: $ sudo losetup /dev/loop0 disk.img Artık "/dev/loop0" aygıt dosyası sanki bir disk gibi kullanılabilecektir. Bu aygıt üzerinde işlem yaptığımızda işlemden "disk.img" dosyası etkilenecektir. Artık blok aygıt sürücülerine baktığımızda "loop0" aygıtını göreceğiz: ****************************************************** $ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT loop0 7:0 0 300M 0 loop sda 8:0 0 80G 0 disk ├─sda1 8:1 0 512M 0 part /boot/efi ├─sda2 8:2 0 1K 0 part └─sda5 8:5 0 79,5G 0 part / sr0 11:0 1 1024M 0 rom ***************************************************** Artık fdisk programını bu aygıt üzerinde kullanabiliriz: $ sudo fdisk /dev/loop0 -> Artık interaktif bir biçimde disk bölümlendirme işlemleri yapılabilir. Burada tek harfli çeşitli komutlar girildiğinde interaktif bir biçimde işlemler yapılmaktadır. Bu komutlardan önemli olanlarını açıklamak istiyoruz: -> "n" (new) komutu yeni bir disk bölümü oluşturmak için kullanılmaktadır. Bu komut verildiğinde yaratılacak disk bölümünün "primary" bölüm mü "extended" bölüm mü olduğu sorulmaktadır. Primary disk bölümü ana 4'lük girişteki bölümlerdir. Dolayısıyla burada genellikle "p" komutu ile "primary" disk bölümü oluşturulur. Sonra bize 4 girişten hangisinin disk bölümü olarak oluşturulacağı sorulmaktadır. Bu durumda sıradaki numarayı vermek (disk tamamen ilk kez bölümlendiriliyorsa 1) uygun olur. Sonra da bize ilgili disk bölümünün hangi sektörden başlatılacağı ve ne uzunlukta olacağı sorumaktadır. Aşağıda bir örnek görüyorsunuz: ************************************************************************************** Komut (yardım için m): n Disk bölümü tipi p birincil (0 birincil, 0 genişletilmiş, 4 boş) e genişletilmiş (mantıksal disk bölümleri için konteyner) Seç (varsayılan p): p Disk bölümü numarası (1-4, varsayılan 1): 1 İlk sektör (2048-614399, varsayılan 2048): Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-614399, varsayılan 614399): +50M Yeni bir disk bölümü 1, 'Linux' tipinde ve 50 MiB boyutunda oluşturuldu. *************************************************************************************** -> "p" (print) komutu oluşturulmuş olan disk bölümlerini görüntülemektedir. Örneğin: ************************************************************************************** Komut (yardım için m): p Disk /dev/loop0: 300 MiB, 314572800 bayt, 614400 sektör Birimler: sektör'i 1 * 512 = 512 baytın Sektör boyutu (montıksal/fiziksel): 512 bayt / 512 bayt G/Ç boyutu (en düşük/en uygun): 512 bayt / 512 bayt Disketikeri tipi: dos Disk belirleyicisi: 0x267a62e0 Aygıt Açılış Başlangıç Son Sektör Boyut ld Türü /dev/loop0p1 2048 104447 102400 50M 83 Linux ************************************************************************************** -> fdisk yaratılan disk bölümlerinin ID'sini default olarak 0x83 (Linux) yapmaktadır. Eğer disk bölümüne FAT dosya sistemi yerleştirilecekse "t" (type) komutu ile bölüm ID'si değitirilmelidir. Örneğin: ************************************************************************************** Komut (yardım için m): t Seçilen disk bölümü 1 Hex kod (bütün kodlar için L tuşlayın): c 'Linux' disk bölümünün tipini 'W95 FAT32 (LBA)' olarak değiştirin. Komut (yardım için m): p Disk /dev/loop0: 300 MiB, 314572800 bayt, 614400 sektör Birimler: sektör'i 1 * 512 = 512 baytın Sektör boyutu (montıksal/fiziksel): 512 bayt / 512 bayt G/Ç boyutu (en düşük/en uygun): 512 bayt / 512 bayt Disketikeri tipi: dos Disk belirleyicisi: 0x267a62e0 Aygıt Açılış Başlangıç Son Sektör Boyut ld Türü /dev/loop0p1 2048 104447 102400 50M c W95 FAT32 (LBA) ************************************************************************************** Şu anda biz bir FAT disk bölümü yaratmış olduk. Şimdi ikinci Linux dosya sistemleri için ikinci bölümünü de yaratalım: ************************************************************************************** Komut (yardım için m): n Disk bölümü tipi p birincil (1 birincil, 0 genişletilmiş, 3 boş) e genişletilmiş (mantıksal disk bölümleri için konteyner) Seç (varsayılan p): p Disk bölümü numarası (2-4, varsayılan 2): İlk sektör (104448-614399, varsayılan 104448): Last sector, +/-sectors or +/-size{K,M,G,T,P} (104448-614399, varsayılan 614399): Yeni bir disk bölümü 2, 'Linux' tipinde ve 249 MiB boyutunda oluşturuldu. Komut (yardım için m): p Disk /dev/loop0: 300 MiB, 314572800 bayt, 614400 sektör Birimler: sektör'i 1 * 512 = 512 baytın Sektör boyutu (montıksal/fiziksel): 512 bayt / 512 bayt G/Ç boyutu (en düşük/en uygun): 512 bayt / 512 bayt Disketikeri tipi: dos Disk belirleyicisi: 0x267a62e0 Aygıt Açılış Başlangıç Son Sektör Boyut ld Türü /dev/loop0p1 2048 104447 102400 50M c W95 FAT32 (LBA) /dev/loop0p2 104448 614399 509952 249M 83 Linux ************************************************************************************** Artık diskimizde iki disk bölümü vardır. -> Bir disk bölümünü aktive etmek için "a" komutu (activate) kullanılmaktadır. Örneğin biz FAT32 disk bölümünü aktif disk bölümü haline getirelim: ************************************************************************************** Komut (yardım için m): a Disk bölümü numarası (1,2, varsayılan 2): 1 Disk bölümü 1'de önyüklenebilir bayrağı artık etkin. Komut (yardım için m): p Disk /dev/loop0: 300 MiB, 314572800 bayt, 614400 sektör Birimler: sektör'i 1 * 512 = 512 baytın Sektör boyutu (montıksal/fiziksel): 512 bayt / 512 bayt G/Ç boyutu (en düşük/en uygun): 512 bayt / 512 bayt Disketikeri tipi: dos Disk belirleyicisi: 0x267a62e0 Aygıt Açılış Başlangıç Son Sektör Boyut ld Türü /dev/loop0p1 * 2048 104447 102400 50M c W95 FAT32 (LBA) /dev/loop0p2 104448 614399 509952 249M 83 Linux ************************************************************************************** -> fdisk önce yazılacakları kendi içerisinde biriktirmekte sonra bunları diske yazmaktadır. Biriktirilenlerin diske yazılması için "w" (write) komutu kullanılmaktadır. Örneğin: ************************************************************************************** Komut (yardım için m): w Disk bölümleme tablosu değiştirildi. Disk bölüm tablosunu yeniden okumak için ioctl() çağrılıyor. Disk bölümü tablosu yeniden okunamadı.: Geçersiz bağımsız değişken Çekirdek hala eski tabloyu kullanıyor. Yeni tablo bir sonraki yeniden başlatma işleminden sonra ya da partprobe(8) veya kpartx(8)'i çalıştırdığınızda kullanılacak. ************************************************************************************** -> Bir disk bölümünü silmek için "d" komutu kullanılmaktadır. Disk bölümlerini silerken dikkat ediniz. -> Disk bölümlerini oluşturduktan sonra çekirdeğin onları o anda görmesi için "partprobe" komutu kullanılmalıdır. Örneğin: ************************************************************************************** $ sudo partprobe /dev/loop0 [sudo] kaan için parola: $ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT loop0 7:0 0 300M 0 loop ├─loop0p1 259:0 0 50M 0 part └─loop0p2 259:1 0 249M 0 part sda 8:0 0 80G 0 disk ├─sda1 8:1 0 512M 0 part /boot/efi ├─sda2 8:2 0 1K 0 part └─sda5 8:5 0 79,5G 0 part / sr0 11:0 1 1024M 0 rom ************************************************************************************** Aslında yukarıda yapılan işlemlerin sonucu olarak Disk Bölümleme Tablosu'ndaki iki giriş (32 byte) güncellenmiştir. -> fdisk programının başka komutları da vardır. Örneğin disk bölümlendirmesi yapıldıktan sonra bu bölümlendirme bilgileri "O" komutu ile bir dosyaya aktarılabilir. Sonra "I" komutu ile bu dosyadan yükleme yapılabilir. Böylece farklı diskler için aynı işlemlerin daha kolay yapılması sağlanabilmektedir. /*================================================================================================================================*/ (187_22_12_2024) & (188_27_12_2024) & (189_05_01_2025) & (190_10_01_2025) & (191_12_01_2025) & (192_19_01_2025) > Proseslerin Yetki Kavramları: Biz modern Linux sistemlerinde yetki gerektiren programları "sudo" komutuyla ("sudo" bir programdır) çalıştırmaktayız. Bir programı "sudo" ile çalıştırdığımızda yaratılan prosesin etkin kullanıcı id'si 0 ve etkin grup id'si, gerçek kullanıcı id'si ve gerçek grup id'si 0 olur. Kullanıcı id'si sıfır olan kullanıcı genellikle "root" ismiyle bulundurulmaktadır. Pek çok UNIX türevi sistemde aynı zamanda 0 numaralı bir grup da bulunmaktadır. Bu da genellikle "root" biçiminde isimlendirilmektedir. Yani "root" isminde hem bir kullanıcı hem de bir grup bulunmaktadır. Anımsanacağı gibi prosesin etkin grup id'sinin 0 olması hiçbir erişim ayrıcalığı oluşturmamaktadır. Asıl olan "etkin kullanıcı id'sinin" 0 olmasıdır. Anımsanacağı gibi Linux sistemleri install edilirken kurulum sırasında kullanıcının girdiği isimle aynı olan bir kullanıcı ismi ve grup ismi oluşturulmaktadır. Kullanıcı ve grup id'lerinin farklı isim alanlarında olduğunu anımsayınız. Örneğin 1000 numaralı bir kullanıcı id'si de olabilir, bir grup id'si de olabilir. Ancak aynı id'ye sahip birden fazla kullanıcı ya da grup olmamalıdır. (Tabii bu durum aslında yasak değildir. Yani biz örneğin etkin kullanıcı id'si 0 olan başka bir kullanıcı da yaratabiliriz. Çekirdek, isimleri değil numaraları işleme sokmaktadır.) Şimdi de "sudo" olmaya ilişkin alt maddeleri inceleyelim: >> Bir programı "sudo" ile çalıştırdığımızda çalıştırılan programa ilişkin prosesin etkin ve gerçek kullanıcı id'sinin ve grup id'sinin 0 olduğunu belirtmiştik. Yani "sudo" ile bir programı çalıştırdığımızda adeta program "root" kullanıcısı tarafından çalıştırılıyormuş gibi bir etki oluşmaktadır. Linux dağıtımlarının bir bölümünde "root" kullanıcısı ile "login" olma güvenlik gerekçesiyle engellenmiştir. Ancak bu dağıtımlarda "root" olarak "bash" komut satırına düşmek istiyorsanız "sudo" ile "bash" programını çalıştırabilirsiniz. Örneğin: $ sudo bash Bir programı "sudo" ile çalıştırmak istediğimizde bizden önce kendi kullanıcımıza ilişkin parola istenmektedir. Ancak bir süre içerisinde artık "sudo" yapıldığında parola istenmeyecektir. >> Her kullanıcı "sudo" yapamamaktadır. "sudo" yapabilen kullanıcılara "sudoer" denilmektedir. Sistem kurulurken kurulum programı tarafından yaratılan kullanıcı "sudoer" durumundadır. Bir kullanıcıyı "sudoer" yapabilmek için en basit yol o kullanıcıyı "/etc/group" dosyasında "sudo" grubuna eklemektir. Örneğin kursun yapıldığı makinedeki "/etc/group" dosyasının "sudo" grup satırı şöyledir: sudo:x:27:kaan Buradan "sudo" grubunun grup id'sinin 27 olduğu ve "kaan" kullanıcısının bu gruba ek grup (supplemantary group) biçiminde dahil olduğu görülmektedir. Şimdi biz "ali" kullanıcısının da "sudo" yapabilmesini istiyorsak "ali" kullanıcı ismini de aşağıdaki gibi satıra eklemeliyiz: sudo:x:27:kaan,ali Aslında bu işlem komut satırında "usermod" komutu ile "-a" ve "-G" seçenekleri kullanılarak da yapılabilmektedir. Örneğin: $ sudo usermod -a -G sudo student Tabii bu komutu şöyle de uygulayabilirdik: $ sudo usermod -aG sudo student Kullanıcıya "sudo" yeteneği verebilmenin diğer bir yolu da "/etc/sudoers" dosyasına yeni bir giriş eklemektir. Biz burada bu yöntem üzerinde durmayacağız. >> Bir programı "sudo" ile çalıştırdığımızda üst prosesin çevre değişkenleri default durumda çalıştırılan programa aktarılmamaktadır. Örneğin biz kabukta bazı çevre değişkenleri tanımlamış olalım. "sudo" ile bir program çalıştırdığımızda bu çevre değişkenleri default durumda yaratılan prosese aktarılmayacaktır. Bunu basit bir biçimde deneyimleyebiliriz. Bunun için önce komut satırında aşağıdaki gibi çevre değişkenleri oluşturabiliriz: $ export XXX=100 $ export YYY=100 Şimdi "env" komutunu uygularsak bu çevre değişkenlerinin yaratılmış olduğunu görürüz. Ancak "sudo env" komutunu uyguladığımızda bu çevre değişkenleri alt prosese aktarılmayacağı için onları göremeyeceğiz. Ancak kabuğun çevre değişkenleri çalıştırılan programa ilişkin prosese "-E" seçeneği kullanılarak aktarılabilmektedir. Örneğin: $ sudo -E env Ancak "-E" seçeneği de güvenlik gerekçesiyle PATH çevre değişkenini yaratılan prosese geçirmemektedir. >> sudo işlemi konusunda "/etc/sudoers" dosya da etkilidir. Bu dosyaya girişler eklenerek belli bir proses sudoer yapılabilmektedir. Bu dosyanın başındaki aşağıdaki satırlar önemlidir: Defaults env_reset Defaults mail_badpass Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" Defaults use_pty Buradaki "env_reset" satırında "env_reset" yerine "env_keep" getirilirse defaut olarak burada belirtilen çevre değişkenleri sudo ile çalıştırılan proseslere aktarılmaktadır. Örneğin: Defaults env_keep += "XX YY" Burada sudo ile bir program çalıştırıldığında alt prosese XX ve YY çevre değişkenleri aktarılacaktır. env_keep += "*" tüm çevre değişkenlerinin aktarılacağı anlamına gelir. Ancak yine PATH çevre değişkeni bunun dışındadır. Örneğin: Defaults env_keep += "*" Eğer PATH çevre değişkeninin de yaratılan prosese aktarılması isteniyorsa secure_path satırı kaldırılmalı ya da # ile yorum satırı içerisine alınmalıdır. Örneğin: Defaults env_reset Defaults mail_badpass # Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" Defaults use_pty Öte yandan Klasik UNIX tasarımında erişim bakımından "ya hep ya hiç" sistemi uygulanmaktadır. Yani sıradan kullanıcılar sistemle ilgili önemli dosyalara erişemezler ve sistemde davranış değişikliklerine yol açabilecek sistem fonksiyonlarını çağıramazlar. Ancak prosesin kullanıcı id'si 0 ise (yani proses "root" hakkına sahipse) her şeyi yapabilirler. Bazı UNIX türevi sistemlerde bu "ya hep ya hiç" sistemi "yeteneklilik (capability)" denilen kavramla yumuşatılmıştır. Bu sistemlerde çeşitli yetenekler vardır. Bir proses root hakkına sahip olmasa bile bu yeteneklerden bazılarına sahip olabilir. Bu durumda bu yetenekleri gerektiren bazı işlemleri yapabilir. Yeteneklilik (capability) konusu POSIX standartlarına sokulmamıştır. POSIX standartları genel olarak "uygun öncelik (appropriate priviledge)" terimini kullanmaktadır. Burada uygun öncelik prosesin root önceliğinde olması ya da ilgili işlemi yapacak yeteneğe sahip olması anlamına gelmektedir. Yeteneklilik konusu POSIX tarafından standardize edilmiş bir konu olmadığı için farklı UNIX türevi sistemlerde farklı biçimlerde gerçekleştirilmiştir. Ancak bunlar arasında en geniş tasarıma sahip olan Linux sistemleridir. * Örnek 1, Bir thread'in sahip olabileceği yetenekler proses kontrol bloğunda bitsel bir biçimde tutulmaktadır. Linux'taki yetenekler şunlardır: CAP_CHOWN: Dosya sahibi ve grup değiştirme yeteneği. CAP_DAC_OVERRIDE: Dosya erişim kontrollerini geçme yeteneği (okuma/yazma/çalıştırma). CAP_DAC_READ_SEARCH: Dosya okuma ve dizin arama izinlerini geçme yeteneği. CAP_FOWNER: Dosya üzerinde, sahibi tarafından yapılan işlemleri yapma yeteneği (örneğin dosyanın erişim özelliklerinin değiştirilmesi gibi) CAP_FSETID: Dosyanın UID veya GID’sini değiştirme yeteneği. CAP_KILL: Diğer proseslere sinyal gönderme yeteneği. CAP_SETGID: Kullanıcı grubunu değiştirme yeteneği. CAP_SETUID: Kullanıcı kimliğini değiştirme yeteneği. CAP_SETPCAP: Process yetenekliliğini değiştirme yeteneği. CAP_SETFCAP: Bir dosyanın yeteneklerini değiştirme yeteneği CAP_LINUX_IMMUTABLE: Bir dosyanın değiştirilemez (immutable) hale getirilmesini sağlama yeteneği. CAP_NET_BIND_SERVICE: Düşük numaralı portlara (1024'ten küçük) bağlanma yeteneği. CAP_NET_BROADCAST: Ağda broadcast (yayın) yapma yeteneği. CAP_NET_ADMIN: Ağ ayarlarını yönetme, ağ arayüzlerini yapılandırma yeteneği. CAP_NET_RAW: Düşük seviyeli ağ işlemleri yapabilme yeteneği. CAP_IPC_LOCK: Bellek üzerinde kilit (lock) işlemleri yapma yeteneği. CAP_IPC_OWNER: IPC (Inter-process communication) kaynaklarının sahipliğini değiştirme yeteneği. CAP_SYS_MODULE: Linux modüllerini yükleme ve kaldırma yeteneği. CAP_SYS_RAWIO: Donanımsal I/O portlarına erişme yeteneği CAP_SYS_CHROOT: chroot (root dosya sistemi değiştirme) işlemi yapma yeteneği. CAP_SYS_PTRACE: Diğer süreçleri izleme ve kontrol etme yeteneği (örneğin, hata ayıklama). CAP_SYS_PACCT: Process accounting verilerini okuma veya yazma yeteneği. CAP_SYS_ADMIN: Sistem yöneticisi yetkileri (modül yükleme, dosya sistemi değiştirme vb.). CAP_SYS_BOOT: Sistemi yeniden başlatma veya açma yeteneği. CAP_SYS_NICE: Diğer proseslerin nice değerini değiştirme yeteneği. CAP_SYS_RESOURCE: Sistem kaynaklarını yönetme ve limitlerini ayarlama yeteneği. CAP_SYS_TIME: Sistemin saatini değiştirme yeteneği. CAP_SYS_TTY_CONFIG: TTY (terminal) ayarlarını değiştirme yeteneği. CAP_MKNOD: Aygıt dosyaları oluşturma yeteneği. CAP_LEASE: Dosya kiralama (lease) mekanizmasını kullanma yeteneği. CAP_AUDIT_WRITE: Audit (denetim) sistemine yazma yeteneği. CAP_AUDIT_CONTROL: Audit sistemi yapılandırmalarını değiştirme yeteneği. CAP_SETFCAP: Dosya yetenekiliğini (capabilities) ayarlama yeteneği. CAP_MAC_OVERRIDE: MAC (Mandatory Access Control) politikalarını geçme yeteneği. CAP_MAC_ADMIN: MAC politikalarını yönetme yeteneği. CAP_SYSLOG: Sistem günlüklerini (log) okuma veya yazma yeteneği. CAP_WAKE_ALARM: Sistemi uyanma alarmıyla uyandırma yeteneği. CAP_BLOCK_SUSPEND: Sistemin uykuya geçmesini engelleme yeteneği. CAP_AUDIT_READ: Audit verilerini okuma yeteneği. CAP_PERFMON: Performans izleme (profiling) yeteneği. CAP_BPF: BPF (Berkeley Packet Filter) programlarını yükleme ve çalıştırma yeteneği. CAP_CHECKPOINT_RESTORE: Bir süreç için checkpoint alma ve geri yükleme yeteneği. Örneğin biz kill fonksiyonu kabaca yalnızca kendimizin oluşturduğu proseslere sinyal gönderebiliriz. Ancak eğer prosesimizin CAP_KILL yetenekliliği varsa bu durumda tıpkı root prosesi gibi başka proseslere de sinyal gönderebiliriz. Linux sistemlerindeki yeteneklilik (capability) konusu maalesef biraz karmaşık bir konudur. İzleyen paragraflarda konunun ayrıntılarına gireceğiz. Aslında Linux sistemlerinde yeteneklilik proses temelinde değil thread temelinde uygulanmaktadır. Yani prosesin farklı thread'leri farklı yeteneklere sahip olabilmektedir. Ancak pratikte prosesin tüm thread'leri genellikle aynı yeteneklere sahip olur. Yetenekler proses kontrol bloğunda bitsel düzeyde tutulmaktadır. Yani yukarıdaki listedeki CAP_XXXX sembolik sabitleri aslında tüm bitleri 0 olan yalnızca tek biti 1 olan sembolik sabitlerdir. Eskiden proses kontrol bloğunda yetenekleri tutan nesneler 32 bitlik nesnelerdi. Sonra çeşitli yetenekler de sisteme eklenince bunlar 64 bite yükseltildiler. Bugünkü Linux çekirdeklerinde therad'e ilişkin yetenekler task_struct yapısının cred isimli elemanının gösterdiği struct cred yapısında tutulmaktadır. Bir thread'in dört grup yetenekleri vardır: -> Etkin yetenekler (effective capabilities), -> İzin verilen yetenekler (permitted capabilities), -> Aktarılan yetenekler (inheritable capabilities), -> Sarmalayan yetenekler (bounding capabilities). Test işlemlerine etkin yetenekler sokulmaktadır. Örneğin thread'imizin başka bir prosese sinyal gönderebilmesi için thread'imizin etkin yetenekleri arasında CAP_KILL bulunuyor olması gerekir. İzin verilen yetenekler (permitted capabilities) etkin yetenekler için bir üst küme oluşturmaktadır. Yani bir thread'in kendisinin etkin yetenekleri ancak izin verilen yetenekleri kadar olabilir. Thread'in bir etkin yetenek kümesinde bir yetenek yoksa ancak izin verilen kümesinde varsa proses izin verilen kümesi içerisindeki o yeteneği etkin kümesine taşıyabilir. Ancak izin verilen kümesini genişletemez. Başka bir deyişle thread'in etkin yetenek kümesi izin verilen yetenek kümesinin bir alt kümesi biçimindedir. Aktarılabilen yetenek kümesi thread bir programı çalıştırdığında onun izin verilen kümesine dahil edilebilecek yetenekleri belirtmektedir. Benzer biçimde thread'in sarmalayan yetenekleri de izin verilen yeteneklerin belirlenmesi sırasında etki göstermektedir. Bu konu izleyen paragraflarda ele alınacaktır. Biz yukarıda Linux çekirdeğinin yetenekleri thread temelinde işleme soktuğunu belirttik. Yetenekler fork işlemi sırasında ya da pthread_create işlemi sırasında (bu da bir çeşit fork işlemi gibidir) üst thread'ten alt thread'e aktarılmaktadır. Yani aslında default durumda bir prosesin tüm thread'leri aynı yeteneklere sahiptir. Fakat biz istersek prosesin belli bir thread'inin yeteneklerini diğer thread'lere dokunmadan değiştirebiliriz. Üst proses fork uyguladığında onun "etkin", "izin verilen" ve "aktarılan" yeteneklerinin hepsi alt prosese aktarılmaktadır. Herhangi bir önceliğe sahip olmayan sıradan proseslerin etkin, izin verilen ve aktarılan yetenek kümesinde hiçbir yetenek yoktur. Örneğin biz bir terminal açıp orada çalışan kabuk programının (bash) yetenek kümelerine baksak bu üç yetenek kümesinin de boş küme olduğunu görürüz. Linux sistemlerinde thread'in yeteneklerini elde etmek için ve onu set etmek için sys_getcap ve sys_setcap isimli iki sistem fonksiyonu bulundurulmuştur. Ancak libc kütüphanesinde bu sistem fonksiyonlarını çağıran fonksiyonlar bulunmamaktadır. Eğer bu sistem fonksiyonlarını çağıracaksanız bunların numaralarını belirterek syscall isimli fonksiyonla çağırabilirsiniz. Ancak bu sistem fonksiyonlarının kullanımı biraz zahmetlidir. Bu fonksiyonlar yerine bu sistem fonksiyonları çağrılarak yazılmış olan "libcap" isimli bir kütüphane de bulunmaktadır. Sistem programcıları genellikle sys_getcap ve sys_setcap fonksiyonlarını kullanmak yerine zaten bunları kullanan "libcap" kütüphanesini kullanmayı tercih etmektedir. Ancak bu "libcap" kütüphanesi default durumda genellikle yüklü olmaz. Bu kütüphaneyi Debian türevi sistemlerde aşağıdaki gibi yükleyebilirsiniz. $ sudo apt install libcap2 libcap-dev Bir prosesin yetenek kümeleri "/proc//status" dosyasından elde edilebilir. Buradaki yeteneklere ilişkin bilgiler aşağıdaki gibi gözükecektir: CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 000001ffffffffff Bu örnekte prosesin bütün yetenek kümesi boştur. Yani proses hiçbir yeteneğe sahip değildir. Anımsanacağı gibi aslında "proc" dosya sisteminde her için bir dizin yaratılmaktadır. Yani "/proc//status" yol ifadesindeki pid aslında ilgili thread'in gettid fonksiyonu ile elde edilen pid numarasıdır. (POSIX standartlarında thread'lerin pid değerinin olmadığını ancak Linux sistemlerinde Linux çekirdeğinin thread'leri prosesler gibi oluşturduğu için thread'lerin de pid değerlerinin olduğunu anımsayınız. Yine anımsanacağı gibi proc dosya sisteminde ana thread'e ilişkin "task" dizininde prosesin thread'lerinin pid numaraları bulunmaktadır.) Bir thread'in yetenekleri programlama yoluyla "libcap" kütüphanesindeki cap_get_proc fonksiyonu ile elde edilebilir. >> "cap_get_proc" : Fonksiyon kendisini çağıran thread'in yetenek bilgilerini temsil eden cap_t türünden bir handle değeri ile geri döner. #include cap_t cap_get_proc(void); cap_t türü aşağıdaki gibi bildirilmiştir: typedef struct __user_cap_header_struct { unsigned int version; // Yapı versiyonu pid_t pid; // İlgili işlem ID'si } __user_cap_header_t; typedef struct __user_cap_data_struct { unsigned int effective; // Etkin yetenekler unsigned int permitted; // İzin verilen yetenekler unsigned int inheritable; // Miras alınan yetenekler } __user_cap_data_t; typedef struct __user_cap_struct { __user_cap_header_t header; // Başlık bilgisi __user_cap_data_t data[2]; // Yetenek verisi (genellikle iki set) } *cap_t; * Örnek 1, cap_t caps; if ((caps = cap_get_proc()) == NULL) exit_sys("get_cap_proc"); >> "cap_get_pid" : Bu fonksiyon ise pid değeri verilen prosesin (ya da thread'in) yeteneklerini elde etmektedir. Fonksiyonun prototipi şöyledir: #include cap_t cap_get_pid(pid_t pid); cap_t türünün bir yapı adresini belirttiğine dikkat ediniz. Bu fonksiyonlar kendi içerisinde cap_t türünden bir yapı nesnesini dinamik olarak tahsis edip onun adresiyle geri dönemktedir. Bu dinamik alaın boşaltımı cap_free fonksiyonu ile yapılmalıdır. >> "cap_free" : #include int cap_free(void *obj_d); Aslında cap_free fonksiyonu yalnızca free fonksiyonunu çağırmaktadır. * Örnek 1, if ((caps = cap_get_proc()) == NULL) exit_sys("get_cap_proc"); /* ... */ cap_free(caps); Her ne kadar cap_get_pid fonksiyonu sanki proses id alıyor gibiyse de yukarıda da belirttiğimiz gibi yetenekler aslında prosese değil thread'e özgüdür. Biz bu fonksiyona bir prosesin pid değerini geçirdiğimizde o prosesin ana thread'ine ilişkin yetenekleri elde etmiş oluruz. Belli bir thread'in bilgileri elde edilecekse doğrudan gettid fonksiyonu ile thread'in pid değeri elde edilebilir. cap_get_proc fonksiyonu ise o anda çalışmakta olan thread'in yeteneklerini elde etmekte kullanılmaktadır. Thread'in yetenekleri elde edildiğinde onların yazdırılması da bir sorundur. Bu nedenle "libcap" kütüphanesinde cap_to_text isimli bir fonksiyon bulundurulmuştur. >> "cap_to_text" : Fonksiyonun prototipi şöyledir: #include char *cap_to_text(cap_t caps, ssize_t *length_p); Fonksiyonun birinci parametresi cap_t türünden handle değerini, ikinci parametresi verilen yazının uzunluğunun yerleştirileceği nesnenin adresini belirtir. İkinci parametre NULL adres geçilebilir. Bu durumda fonksiyon böyle bir yerleştirme yapmaz. Fonksiyon oluşturulan yazının adresine geri dönmektedir. Yine bu adresin de cap_free kullanımdan sonra boşaltılması gerekir. Eğer cap_to_text fonksiyonunda yalnızca "=" biçiminde bir yazı elde ediliyorsa bu durum thread'in herhangi bir yeteneğe sahip olmadığı anlamına gelmektedir. Aşağıda o anda çalışmakta olan thread'in yetenekleri ekrana yazdırılmıştır. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { cap_t caps; char *captext; if ((caps = cap_get_proc()) == NULL) exit_sys("cap_get_proc"); if ((captext = cap_to_text(caps, NULL)) == NULL) { cap_free(caps); exit_sys("cap_to_text"); } printf("%s\n", captext); cap_free(captext); cap_free(caps); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bir thread'in yetenekleri onu yaratan thread'ten aktarılmaktadır. Yani genel olarak bir proses bir thread yarattığında ya da fork işlemi yaptığında yaratılan thread ya da proses bu işlemi uygulayan thread ile aynı yeteneklere sahip olur. Sıradan bir prosesin ya da onun thread'lerinin yeteneklerine baktığımızda tipik olarak şöyle olduğunu görürüz: CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 000001ffffffffff Buradan çıkan sonuç şudur: Sıradan bir prosesin ya da thread'in aktarılan, izin verilen ve etkin yetenek kümesi boş kümedir. Yani bu küme hiçbir yeteneği kapsamamaktadır. Ancak sarmalayan yeteneklerin (bounding capabilities) bütün bitlerinin 1 olduğunu görüyorsunuz. (Her ne kadar burada yüksek anlamlı bitlerin bazılarını 0 görüyor olsanız da zaten Linux o bitleri kullanmamaktadır.) Etkin kullanıcı id'si 0 olan proseslerin hiçbir engele takılmadığını anımsayınız. Linux çekirdeği önce prosesin etkin kullanıcı id'sini kontrol etmektedir. Eğer prosesin etkin kullanıcı id'si 0 ise zaten herhangi bir yetenek kontrolü yapmasına gerek kalmamaktadır. Yani yetenekler ancak etkin kullanıcı id'si 0 olmayan prosesler için anlamlı bir özelliktir. Pekiyi sıradan bir prosesin tüm thread'lerinin yetenekleri boş küme olduğuna göre bu prosesler ve thread'ler nasıl yetenek kazanmaktadır? İşte bunun iki yolu olabilir: -> Yetenek kazandırabilecek yeteneğe sahip ya da etkin kullanıcı id'si 0 olan bir proses bir alt proses yaratıp onun yeteneklerini oluşturup sonra kullanıcı id'sini 0 olmaktan çıkartabilir. -> Aslında Linux sistemlerinde çalıştırılabilen dosyalara da yetenekler atanabilmektedir. Bir çalıştırılabilen dosya exec fonksiyonlarıyla çalıştırıldığında izleyen paragraflarda açıklanacağı gibi otomatik olarak proses bazı yeteneklere sahip olabilmektedir. Bu durum aslında daha önce gördüğümüz çalıştırılabilen dosyaların "set-user-id" ve "set-grup-id" bayraklarına benzemektedir. Örneğin thread'imizin yetenek kümeleri boş küme olsun. Ama biz "xxx" isimli programı exec fonksiyonlarıyla çalıştırmak isteyelim. Bir kez fork yapıp alt proseste exec yaptığımızda artık alt prosesimizin yetenekleri bu "xxx" dosyasında belirtilen yeteneklere sahip olabilmektedir. Yani prosesleirn ve thread'lerin yetenekleri aslında genellikle exec işlemi sırasında boş küme olmaktan çıkmaktadır. Pekiyi thread'imize belli bir yeteneği nasıl kazandırabiliriz? Bir thread etkin yeteneklerini ancak izin verilen yetenekler kadar artırabilir. O halde önce thread'imizin izin verilen yeteneklerini artırması gerekir. Bunun için sırasıyla şu işlemler yapılmalıdır: -> Önce içi boş bir yetenek nesnesi cap_init fonksiyonuyla oluşturulur. cap_init fonksiyonunun prototipi şöyledir: #include cap_t cap_init(void); cap_init fonksiyonu başarısızlık durumunda NULL adrese geri dönmektedir. * Örnek 1, ... cap_t newcaps; if ((newcaps = cap_init()) == NULL) exit_sys("cap_init"); ... -> Bu yetenek nesnesine cap_set_flag fonksiyonu ile çeşitli bayraklar eklenir. Fonksiyonun prototipi şöyledir: #include int cap_set_flag(cap_t cap_p, cap_flag_t flag, int ncap, const cap_value_t *caps, cap_flag_value_t value); Fonksiyonun birinci parametresi yeteneklerin set edileceği nesneyi belirtmektedir. İkinci parametre hangi yetenekler üzerinde işlem yapılacağını belirtir. Bu parametre aşağıdaki sembolik sabitlerden yalnızca birini içerebilir: CAP_EFFECTIVE CAP_INHERITABLE CAP_PERMITTED Fonksiyon tarafından set edilecek yetenekler dördüncü parametresi ile belirtilen dizinden elde edilmektedir. Yani programcı cap_value_t türünden bir dizi oluşturup bu dizinin içerisine yenetekleri yerleştirir. Fonksiyonun üçün paraöetresine de bu dizinin uzunluğunu geçirir. Fonksiyoun son parametresi aşağıdaki iki değerdne biri olarak girilir: CAP_CLEAR CAP_SET Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönemtedir. * Örnek 1, ... cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL}; ... if (cap_set_flag(newcaps, CAP_PERMITTED, 2, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_flag(newcaps, CAP_EFEFCTIVE, 2, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } ... -> Oluşturulan yetenekler cap_set_proc fonksiyonu ile o anda çalışan thread'e aktarılır. cap_Set_proc fonksiyonun prototipi şöyledir: #include int cap_set_proc(cap_t cap_p); Fonksiyon hazırlanan capt_t nesnesini parametre olarak alır. Thread'in yeteneklerini set eder. Başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri döner. Örneğin: if (cap_set_proc(newcaps) == -1) { cap_free(newcaps); exit_sys("cap_set_proc"); } Şimdi de bu konuya ilişkin örnekleri inceleyelim: * Örnek 1, Aşağıda bir thread'in yeteneklerini değiştiren örnek bir program verilmiştir. Tabii cap_set_proc fonksiyonun da kullanılması için prosesin uygun önceliğe sahip olması gerekir. (Yani prosesin etkin kullanıcı id'si 0 olmalı ya da ilgili thread yetenek değiştirme yeteneği olan CAP_SYS_ADMIN yeteneğine sahip olmalıdır.) #include #include #include void exit_sys(const char *msg); int main(void) { cap_t newcaps; cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL}; if ((newcaps = cap_init()) == NULL) exit_sys("cap_init"); if (cap_set_flag(newcaps, CAP_PERMITTED, 2, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_flag(newcaps, CAP_EFFECTIVE, 2, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_proc(newcaps) == -1) { cap_free(newcaps); exit_sys("cap_set_proc"); } cap_free(newcaps); printf("Ok\n"); getchar(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıda önce thread'in izin verilen ve etkin yetenekleri değiştirilmiş sonra da bu yenekler elde edilip yazdırılmıştır. Programın çalıştırılması sonucunda aşağdıaki gibi bir çıktı elde edilecektir: cap_chown,cap_kill=ep Programın kodları ise şu şekildedir: #include #include #include void exit_sys(const char *msg); int main(void) { cap_t newcaps, caps; cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL}; char *captext; if ((newcaps = cap_init()) == NULL) exit_sys("cap_init"); if (cap_set_flag(newcaps, CAP_PERMITTED, 2, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_flag(newcaps, CAP_EFFECTIVE, 2, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_proc(newcaps) == -1) { cap_free(newcaps); exit_sys("cap_set_proc"); } cap_free(newcaps); if ((caps = cap_get_proc()) == NULL) exit_sys("cap_get_proc"); if ((captext = cap_to_text(caps, NULL)) == NULL) { cap_free(caps); exit_sys("cap_to_text"); } printf("%s\n", captext); cap_free(captext); cap_free(caps); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 3, Proses root olarak (etkin kullanıcı id'si 0 olarak) çalışıyor olsun. Prosesin etkin kullanıcı id'si seteuid fonksiyonuyla değiştirildiğinde prosesin tüm thread'lerinin etkin yenetenleri sıfırlanmaktadır. Ancak izin verilen yeteneklerine dokunulmamaktadır. Ancak prsesin gerçek kullanıcı id'si ile etkin kullanıcı id'si setuid fonksiyonu ile değiştirildiğinde prosesin tüm thread'lerinin hem izin verilen hem de etkin yetenekleri sıfırlanmaktadır. Bu konudaki ayrıntılar için "capabilities(7)" man sayfasına başvurabilirsiniz. Aşağıdaki örnekte seteuid fonksiyonundna sonra thread'in etkin yeteneklerinin düşürüldüğüne ilişkin bir örnek verilmiştir. Program çalıştırıldığında ekranda şunlar görünecektir: cap_chown,cap_kill,cap_setuid=ep cap_chown,cap_kill,cap_setuid=p Programın kodları ise şu şekildedir: #include #include #include #include void exit_sys(const char *msg); void disp_capability(void) { cap_t caps; char *captext; if ((caps = cap_get_proc()) == NULL) exit_sys("cap_get_proc"); if ((captext = cap_to_text(caps, NULL)) == NULL) { cap_free(caps); exit_sys("cap_to_text"); } printf("%s\n", captext); cap_free(captext); cap_free(caps); } int main(void) { cap_t newcaps; cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL, CAP_SETUID}; if ((newcaps = cap_init()) == NULL) exit_sys("cap_init"); if (cap_set_flag(newcaps, CAP_PERMITTED, 3, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_flag(newcaps, CAP_EFFECTIVE, 3, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_proc(newcaps) == -1) { cap_free(newcaps); exit_sys("cap_set_proc"); } cap_free(newcaps); disp_capability(); if (seteuid(1000) == -1) exit_sys("setuid"); disp_capability(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Linux çekirdek kendi içerisinde aslında hep yetenek kontrolü yapmaktadır. Yani bir prosesin root önceliğinde olması (yani etkin kullanıcı id'sinin 0 olması) aslında izimn verilen ve etkin yeteneklerin hepsinin set edildiği anlamına gelmektedir. Dolayısıyla biz bir programı sudo ile çalıştırdıktan sonra onun etkin yeteneklerini değiştirirsek artık proses'in etkin kullanıcı id'si 0 olsa bile proses yetenek kaybedecektir. open fonksiyonu tarafından uygulanan erişim kontrollerinin thread yetenekleriyle bir ilgisi yoktur. Yani etkin user id'si 0 olan bir root proses yenek kümesi düşürülse bile yine open fonksiyonunda başarısız olmaz. Linux sistemlerinde yalnızca thread'lerin değil çalıştırılabilen dosyaların da yetenekleri vardır. Biz daha önce çalıştırılabilen bir dosyanın "set-user-id" ve "set-group-id" biçiminde isimlendirilen bayraklarının işlevlerini incelemiştik. Çalıştırılabilen bir dosyanın "set-user-id" bayrağı set edilmişse o programı exec yapan prosesin etkin kullanıcı id'si dosyanın kullanıcı id'si haline getiriliyordu. Aynı durum "set-group-id" bayrağı için de benzer biçimde işletiliyordu. Çalıştırılabilen bir dosyanın "set-group-id" bayrağı set edilmişse bu dosya exe yapıldığında prosesin etkin grup id'si dosyanın grup id'si oluyordu. İşte bu mekanizmanın benzeri yetenek temelinde de oluşturulmuştur. Çalıştırılabilen dosyaların da yetenekleri set edilmiş olabilir. Bu durumda bu dosyalar exec yapıldığında proses otomatik olarak o yeteneğe sahip hale gelmektedir. Örneğin bir program dosyasının CAP_KILL yeteneğinin set edilmiş olduğunu varsayalım. Ancak bu dosyanın etkin kullanıcı id'si root olmasın. Bu dosya çalıştırıldığında ilgili thread CAP_KILL yeteneğine sahip olacaktır. (Program birden fazla thread'e sahipse exec işlemiyle yalnızca exec yapan thread'in yaşamına devam ettiğini anımsayınız.) Ancak dosya yetenekleri konusunun da bazı ayrıntıları vardır. Dosya yetenekleri i-node elemanlarının içerisinde tutulmaktadır. Bu nedenle dosya sisteminin de dosya yeteneklerini tutma özelliğine sahip olması gerekir. Örneğin FAT dosya sistemlerinde böyle bir alan bulunmamaktadır. Yalnızca çalıştırılabilen dosyalar için yetenek kavramı söz konusudur. Tabii bir text dosyanın içerisinde bir script kodu da bulunabilir. Bu durumda bu dosyalar da çalıştırılabilen dosya olarak ele alınabilmektedir. Dosyaların yeteneklerini görüntülemek için "libcap" paketinde "getcap" isimli bir yardımcı program da bulunmaktadır. * Örnek 1, "ping" programının CAP_NET_RAW yeteneği vardır. "getcap" ile bu dosyanın yeteneklerini aşağıdaki gibi görüntüleyebiliriz: $ /usr/bin/ping cap_net_raw=ep /usr/bin/ping cap_net_raw=ep Tıpkı thread'ler gibi dosyaların da "izin verilen (permitted)", etkin (effective) ve aktarılan (inheritable) yetenekleri vardır. Dosyaların yetenekleri "setcap" isimli programla değiştirilebilmektedir. Programın örnek kullanımı şöyledir: $ sudo setcap "cap_net_bind_service=pei cap_net_raw=ep" sample Burada sample programına çeşitli yetenekler set edilmiştir. Bu yeneklerin tek bir komut satırı argümanı biçiminde verildiğine dolayısıyla da tırnak içeeisine alındığına dikkat edniz. Eğer dosyadan tüm yetenekler silinecekse komut aşağıdaki gibi kullanılabilir: $ sudo setcap = sample setcap programının kullanımına ilişkin ayrıntılar için man sayfalarına başvurabilirsiniz. * Örnek 1, "sample" isimli programımıza izin verilen ve etkin olarak CAP_KILL yeteneğini eklemek isteyelim. Bu işlemi şöyle yapabiliriz: $ sudo setcap "cap_kill=ep" sample Şimdi dosyanın yeteneğini "getcap" komutu ile görüntüleyelim: $ getcap sample sample cap_kill=ep Tabii bir dosyanın yeteneğini değiştirebilmek için ilgili thread'in de CAP_SETPCAP yeneğine sahip olması gerekir. root prosesleri tüm yeneklere sahipmiş gibi düşünmeliyiz. Tabii aslında dosyaların yeteneklerini elde etmek için ve onlara yetenek iliştirmek için Linux çekirdeğinde sistem fonksiyonları bulunmaktadır. "getcap" ve "setcap" programları libcap kütüphanesindeki cap_set_file ve cap_get_file fonksiyonları kullanılarak yazılmıştır. Bu fonksiyonlar da sys_setxattr, sys_fsetxattr, sys_lsetxattr ve sys_getxattr, sys_fgetxattr ve sys_lgetxattr sistem fonksiyonları çağrılarak yazılmıştır. Bu sistem fonksiyonları için "libc" kütüphanesinde sarma fonksiyonlar bulunmaktadır. Ancak biz burada bu işlemleri "libcap" kütüphanesindeki cap_set_file ve cap_get_file fonksiyonlarını kullanarak yapacağız. Bu fonksiyonlardan, >> "cap_get_file" fonksiyonun prototipi şöyledir: #include cap_t cap_get_file(const char *path_p); Fonksiyon dosyanın yol ifadesini alır ve başarı durumunda dosyanın yetenek bilgilerine ilişkin cap_t nesnesine geri döner. Bu nesnenin kullanım bittikten sonra cap_free fonksiyonu ile boşaltılması gerekmektedir. Fonksiyon başarısızlık durumunda NULL adrese gerei dönmektedir. Örneğin: cap_t caps; if ((caps = cap_get_file("/usr/bin/ping")) == NULL) exit_sys("cap_get_file"); Aşağıda fonksiyonun kullanımına ilişkin örnek bir program verilmiştir. * Örnek 1, #include #include #include void exit_sys(const char *msg); void disp_cap(cap_t caps) { char *captext; if ((captext = cap_to_text(caps, NULL)) == NULL) { cap_free(caps); exit_sys("cap_to_text"); } printf("%s\n", captext); cap_free(captext); } int main(int argc, char *argv[]) { cap_t caps; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((caps = cap_get_file(argv[1])) == NULL) exit_sys("cap_get_file"); disp_cap(caps); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >> "cap_set_file" fonksiyonun prototipi de şöyledir: #include int cap_set_file(const char *path_p, cap_t cap_p); Fonksiyonun birinci parametresi dosyanın yol ifadesini, ikinci parametresi dosyaya iliştirilecek yetenekleri belirtmektedir. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri dönmektedir. Tabii bir dosyaya yetenek set edebilmek için ya prosesin root olması (etkin kullanıcı id'sinin 0 olması) ya da prosesin CAP_SETFCAP yeteneğine sahip olması gerekmektedir. * Örnek 1, ... cap_t caps, newcaps; cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL, CAP_SETUID}; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((newcaps = cap_init()) == NULL) exit_sys("cap_init"); if (cap_set_flag(newcaps, CAP_PERMITTED, 3, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_flag(newcaps, CAP_EFFECTIVE, 3, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_file(argv[1], newcaps) == -1) { cap_free(newcaps); exit_sys("cap_set_proc"); } cap_free(newcaps); Burada CAP_CHOWN, CAP_KILL, CAP_SETUID yenekleri "izin verilen" ve "etkin" yetenekler olarak dosyaya iliştirilmiştir. Aşağıda bu iki fonksiyonun ortak kullanılmasına ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include void exit_sys(const char *msg); void disp_cap(cap_t caps) { char *captext; if ((captext = cap_to_text(caps, NULL)) == NULL) { cap_free(caps); exit_sys("cap_to_text"); } printf("%s\n", captext); cap_free(captext); } int main(int argc, char *argv[]) { cap_t caps, newcaps; cap_value_t caparray[] = {CAP_CHOWN, CAP_KILL, CAP_SETUID}; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((newcaps = cap_init()) == NULL) exit_sys("cap_init"); if (cap_set_flag(newcaps, CAP_PERMITTED, 3, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_flag(newcaps, CAP_EFFECTIVE, 3, caparray, CAP_SET) == -1) { cap_free(newcaps); exit_sys("cap_set_flag"); } if (cap_set_file(argv[1], newcaps) == -1) { cap_free(newcaps); exit_sys("cap_set_proc"); } cap_free(newcaps); if ((caps = cap_get_file(argv[1])) == NULL) { cap_free(newcaps); exit_sys("cap_get_file"); } disp_cap(caps); cap_free(caps); cap_free(newcaps); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Ayrıca açık bir dosya için yenekleri alan ve set eden aşağıdaki fonksiyonlar da bulunmaktadır: >> "cap_set_fd" & "cap_get_fd" : Fonksiyonun protototipi şu şekildedir: #include cap_t cap_get_fd(int fd); int cap_set_fd(int fd, cap_t caps); Pekiyi çeşitli yetenekere sahip bir program dosyası exec yapıldığında ne olur? Bu süreç biraz karşıktır. Normal olarak set-user-id ve set-group-id bayraklarında olduğu gibi prosesin dosyada belirtillen yeteneklere sahip olması gerekir. Ancak sürecin açıklayacağımız bazı ayrıntıları vardır. Öncelikle aşağıdaki gibi bir deneme yapalım. Sistem zamanını öğrenmek ve ayarlamak için kullanılan "date" programını kendi dizinimize kopyalayalım: $ whereis date date: /usr/bin/date /usr/share/man/man1/date.1.gz /usr/share/man/man1/date.1posix.gz $ cp /usr/bin/date date Sistem tarihini değiştirmeye çalışalım: $ ./date -s "2025-01-12" ./date: tarih ayarlanamadı: İşleme izin verilmedi Paz 12 Oca 2025 00:00:00 +03 Görüldüğü gibi bu işlem için "sudo" gerekmektedir. Şimdi de dosyaya sistem tarihini değiştirmek için gereken cap_sys_time yeteneğini iliştirelim: $ sudo setcap cap_sys_time=pe date İşlemimizi doğrulayalım: $ sudo getcap date date cap_sys_time=ep Şimdi sistem tarihini değiştirmeye çalışalım: $ ./date -s "2025-01-12" Paz 12 Oca 2025 00:00:00 +03 Görüldüğü gibi biz programı çalıştırdığımızda artık prosesimiz dosyada belirtilen CAP_SYS_TIME yeteneğini kazanmıştır. Ancak konunun bazı ayrıntıları vardır. İzleyen paragraflarda bu ayrıntılar üzerinde duracağız. Diğer yandan bir thread (proses de diyebiliriz) çalıştırılabilen bir dosyayı exec fonksiyonlarıyla çalıştırdığında thread'in yeetenekleri aaşağıdaki biçimde değişime uğramaktadır (The Linux Programming Interface kitabından alınma): P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset) P'(effective) = F(effective) & P'(permitted) P'(inheritable) = P(inheritable) Burada P prosesin exec öncesindeki yeteneklerini P' exec sonrasındaki yeteneklerini, F dosyanın yeteneklerini, cap_bset ise prosesin çevreleyen yeteneklerini (process bounding capabilitiees) belirtmektedir. Dosyanın etkin yetenekleri eskiden her yetenek için farklı değildi. Bütün yenekler için tek bir bayrak kullanılıyordu. Ancak bir süredir bu durum değiştirilmiştir. Artık her yetenek için ayrı bir etkinlik bayrağı turulmaktadır. Prosesin çevreleyen yetenekleri (bounding capabilites) her yetenek için 0/1 biçiminde bitlerden oluşmaktadır. Genellikle prosesin çeevreleyen yeteneklerinin bütün bitleri 1 durumdadır. Örneğin sıradan bir programın yeeneklerine "proc" dosya sisteminden baktığımızda şunları görmekteyiz: CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 000001ffffffffff Görüldüğü gibi çevreleyen yeteneklerin tüm bitleri 1 durumundadır. (Yüksek anlamlı 0 olan hex digitlere ilişkin bitlerin zaten kullanılmadığına dikkat ediniz.) Buradaki durumu madde madde şöyle açıklayabiliriz: -> exec yapıldıktan sonra prosesin yeni izin verilen yetenekleri şu biçimde oluşturulmaktadır: P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset) Burada bit OR operatörünün solundaki ifade şu anlama gelmektedir: "Prosesin exec öncesindeki aktarılan yeteenekleri eğer aynı zamanda dosyanın aktarılan yetenekleri içerisinde varsa bu yetenekler alınmaktadır". Bit Or operatörünün sağ tarafındaki ifade ise şu anlama gelmektedir: "Dosyanın izin verilen yetenekleri eğer prosesin çavreleyen yeteneklerinde varsa bu yetenekler alınmaktadır. Biz yukarıda prosesin çevreleyen yeteneklerinin genellikle tüm bitlerinin 1 olduğunu beelirtmiştik. Bu durumda dosyanın izin verilen yeteneklerinin hepsi alınacaktır. Örneğin bizim dizine kopyaladığımız "date" programının yetenekleri şöyledir: $ getcap date date cap_sys_time=ep Yani dosyanın etkin ve izin verilen yenekleri "cap_sys_time" yeteeneğie sahiptir. O halde yukarıda satır dikkat alındığında exec sonrasında prosesin izin verilen yeetenekleri "cap_sys_time" içerecektir. -> exec sonrasında prosesin etkin yetenekleri exec sonrasındaki izin verilen yeteneklerinin dosyanın etkin yetenekleriyle maskelenmesi sonucunda elde edilmektedir: P'(effective) = F(effective) & P'(permitted) Dosyanın etkin yeteneklerinin bir şalter görevi gördüğüne dikkat ediniz. Eğer dosyanın etkin yeteneklerindeki bir yetenek 0 ise bu durumda bu yetenek exec sonrasındaki prosesin etkin yeteneklerine yansıtılmayacaktır. Proseesin exec öncesindeki aktarılan yetenekleri ile exec sonrasındaki aktarılan yeetenekleri arasında bir farklılık yoktur: P'(inheritable) = P(inheritable) Pekiyi Linux'taki yetenek konusu neden bu kadar karışık hale getirilmiştir? Tasarımın bu biçimi aslında fazlaca esnek duruma izin vermektedir. Ancak bu esneklikten faydalanma duurmu pratikte çokça karşımıza çıkmamaktadır. Bura aslında temel kullanım için şunlar söylenebilir: -> Bir programı sudo ile çalıştırdığımızda ya da programımızın etkin kullanıcı id'si 0 ise bu durum adeta "tüm etkin yetenekleri set edilmiş" bir proses anlamına gelmektedir. -> Çekirdek içerisinde özel bazı durumlarda genel olarak etkin kullanıcı id'sinin 0 olup olmadığı biçiminde değil thread'in ilgili yeteneğe sahip olup olmadığı biçiminde kontroller yapılmaktadır. -> Biz çalıştırılabilen bir dosyanın beelli bir etkin yeteneğini ve izin verilen yeeteneğini set edersek o programı exec ile çalıştırrdığımızda o yeneke exec sonrasında prosesimizin izin verilen yeteneklerine ve etkin yetenelerine otomatik bir biçimde geçer. Tersten gidersek biz bir programın çalıştırılması ile ilgili prosesin bir yeetenek kazanmasını istiyorsak program dosyası için ilgili yeteneği etkin ve izin verilen biçimde set etmeeliyiz. /*================================================================================================================================*/ (193_26_01_2025) & (194_31_01_2025) & (195_03_02_2025) & (196_07_02_2025) > Linux Çekirdeğinin Derlenmesi : Bu bölümde Linux kaynak kodlarının derlenmesi ve sistemin yeni çeekirdekle açılması üzerinde duracağız. Çekirdek kodlarının derlenmesi tüm Linux sistemleri için aynı biçimde yapılmaktadır. Ancak sistemin yeni çekirdekle açılması kullanılan "boot loader" programın ayarlarıyla ilgilidir. Bugün masaüstü bilgisayarlarında ağırlıklı olarak GRUB isimli boot lader kullanılmaktadır. Biz burada bu nedenle sürecin GRUB'ta nasıl yürütüleceğini ele alacağız. Gömülü sistemlerde ise ağırlıklı olarak U-Boot denilen boot loader kullanılmaktadır. Biz kursumuzda U-Boot hakkında bir açıklama yapmayacağız. İşletim sistemlerinin çekirdeklerini belleğe yükleyip kontrolün çekirdek kodlarına bırakılmasını sağlayan araçlara "önyükleyici (boot loader)" denilmektedir. Microsoft Windows sistemlerinde kendi önyükleyici programını kullanmaktadır. Buna "Windows Boot Manager" ya da kısaca "bootmgr" de denilmektedir. UNIX/Linux dünyasında çeşitli önyükleyici programlar kullanılmıştır. Halen en yaygın kullanılan önyükleyici program "grub" isimli programdır. Tabii "grub" aynı zamanda Windows işletim sistemini de yükleyebilmektedir. Grub önyükleyicisinden önce Linux sistemlerinde uzun bir süre "lilo" isimli önyükleyici kullanılmıştır. Gömülü sistemlerde de çeşitli önyükleyiciler kullanılabilmektedir. Bazı gömülü sistemlerde o gömülü sistemi üreten kurum tarafından oluşturulmuş olan önyükleyiciler kullanılmaktadır. Ancak gömülü sistemlerde en çok kullanılan önyükleyici "U-Boot" isimli önyükleyicidir. Nasıl C'deki main fonksiyonuna komut satırı argümanları geçiriliyorsa işletim sistem sistemi çekirdeklerine de çeşitli biçimlerde parametreler geçirilebilmektedir. Böylece çekirdek belli bir konfigürasyonla işlev görecek biçimde başlatılabilmektedir. Linux çekirdeğini önyükleyici yüklediğine göre çekirdek parametreleri de önyükleyici tarafından çekirdeğe aktarılmaktadır. Linux'ta bu parametreler "grub" önyükleyicisinin başvurduğu dosyalarda belirtilmektedir. Grub önyükleyicisinin kullanımı biraz ayrıntılıdır. Ancak biz burada grub işlemlerini daha basit ve görsel biçimde yapabilmek için "grub-customizer" isimli bir programdan faydalanacağız. Bu programı Debian türevi sistemlerde aşağıdaki gibi yükleyebilirsiniz: $ sudo add-apt-repository ppa:danielrichter2007/grub-customizer $ sudo apt-get update $ sudo apt-get install grub-customizer Pekiyi nedne işletim sistemini yüklemek için ayrı bir programa gereksinim duyulmuştur? Eskiden işletim sistemleri doğrudan BIOS kodları tarafındna yüklenebiliyordu. Ancak zamanla işletim sistemleri parametreler alacak biçimde geliştirildi. Önyükleyiciler birden fazla çekirdeğin bulunduğu durumlarda basit ayarlarla sistem yöneticisinin istediği çekirdekle boot işlemini yapabilmektedir. Diskte birden fazla işletim sisteminin bulunduğu durumlarda sistemin istenilen bir işletim sistemi tarafından boot edilmesini sağlayabilmektedir. Örneğin makinemizde hem Windows hem de Linux aynı anda bulunuyor olabilir. Önyükleyicimiz bize bir menü çıkartıp hangi işletim sistemi ile boot işlemini yapmak istediğimizi sorabilir. Eskiden basit boot prosedürleri zamanla daha karmaşık hale gelmiştir. Önyükleyici programlara gereksinim duyulmaya başlanmıştır. Pekiyi nedenleri biz Linux çekirdeğini kaynak kodlardan yeniden derlemek isteyebiliriz? İşte bunun tipik nedenleri şunlar olabilir: -> Bazı çekirdek modüllerinin ve aygıt sürücülerin çekirdek imajından çıkartılması ve dolayısıyla çekirdeğin küçültülmesi için. -> Yeni birtakım modüllerin ve aygıt sürücülerin çekirdek imajına eklenmesi için. -> Çekirdeğe tamamen başka özelliklerin eklenmesi için. -> Çekirdek üzerinde çekirdek parametreleriyle sağlanamayacak bazı konfigürasyon değişikliklerinin yapılabilmesi için. -> Çekirdek kodlarında yapılan değişikliklerin etkin hale getirilmesi için. -> Çekirdeğe yama yapılması için. -> Yeni çıkan çekirdek kodlarının kullanılabilir hale getirilmesi için. Çekirdeğin derlenmesi için öncelikle çekirdek kaynak kodlarının derleme yapılacak bilgisayara indirilmesi gerekir. Pek çok dağıtım default durumda çekirdeğin kaynak kodlarını kurulum sırasında makineye çekmemektedir. Çekirdek kodları "kernel.org" sitesinde bulundurulmaktadır. Tarayıcdan "kernel.org" sitesine girilip "pub/linux/kernel" dizinine geçildiğinde tüm yayınlanmış çekirdek kodlarını göreceksiniz. İndirmeyi tarayıcıdan doğrudan yapabilirsiniz. Eğer indirmeyi komut satırından "wget" programıyla yapmak istiyorsanız aşağıdaki URL'yi kullanabilirsiniz: https://cdn.kernel.org/pub/linux/kernel/v[MAJOR_VERSION].x/linux-[VERSION].tar.xz Buradaki MAJOR_VERSION "3", "4", "5" gibi tek bir sayıyı belirtmektedir. VERSION ise çekirdek büyük ve küçük numaralarını belirtmektedir. Örneğin biz çekirdeğin 5.15.12 versiyonunu şöyle indirebiliriz: $ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.12.tar.xz Örneğin çekirdeğin 6.8.1 versiyonunu da şöyle indirebiliriz: $ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.8.1.tar.xz Çekirdek kodları indirildikten sonra onun açılması gerekir. Açma işlemi tar komutuyla aşağıdaki gibi yapılabilir: $ tar -xvJf linux-5.15.12.tar.xz Debian tabanlı sistemlerde o anda makşnede yüklü olan mevcut çekirdeğin kaynak kodlarını indirmek için aşağıdaki komutu kullanabilirisiniz: $ sudo apt-get install linux-source Burada yükleme "/usr/src" dizinine yapılacaktır. Linux kaynak kodlarının verisyonlanması eskiden daha farklıydı. Cekirdeğin 2.6 versionundan sonra versiyon numaralandırma sistemi değiştirilmiştir. Eskiden (2.6 ve öncesinde) versiyon numaraları çok yavaş ilerletiliyordu. 2.6 sonrasındaki yeni versiyonlamada versiyon numaraları daha hızlı ilerletilmeye başlanmıştır. Bugün kullanılan Linux versiyonları nokta ile ayrılmış üç sayıdan oluşmaktadır: Majör.Minör.Patch-Extra (-rcX, -stable, -custom, -generic) Buradaki "majör numara" büyük ilerlemeleri "minör numara" ise küçük ilerlemeleri belirtmektedir. Eskiden (2.6 ve öncesinde) tek sayı olan minör numaralar "geliştirme versiyonlarını (ya da beta versiyonlarını)", çift olanlar ise stabil hale ggetirilmiş dağıtılan versiyonları belirtiyordu. Ancak 2.6 sonrasında artık tek ve çift minör numaralar arasında böyle bir farklılık kalmamıştır. Patch numarası birtakım böceklerin ya da çok küçük yeniliklerin çekirdeğe dahil edildiği versiyonları belirtmektedir. Bu bağlamda minör numaralardan daha küçük bir ilerlemenin söz konusu olduğunu anlatmaktadır. Burada Extra ile temsil edilen alanda "rcX (X burada bir sayı belirtir) "stable", "custom", "generic", "realtime" gibi sözcükler de bulunmaktadır. "rc" harfleri "release candidate" sözcüklerin kısaltmadır. Satabil sürümün öncesindeki son geliştirme sürümlerini belirtmektedir. "stable" sözcüğü dağıtılan kararlı sürümü belirtir. Eğer sistem programcısı çekirdekte kendisi birtakım değişiklikler yapmışsa genellikle bunun sonuna "custom" sözcüğünü getirir. Tabii bu "custom" sözcüğünü ayrıca "-" biçiminde numaralar da izleyebilir. Buradaki numaralar sistem programcısının kendi özelleştirmesine ilişkin numaralardır. "generic" sözcüğü ise genel kullanım için yapılandırılmış bir çekirdek olduğunu belirtmektedir. "realtime" yapılandırmanın gerçek zamanlı sistem özelliği kazandırmak için yapıldığını belirtmektedir. "generic" ve "realtime" sözcüklerinin öncesinde "-N-" biçiminde bir sayı da bulunabilmektedir. Bu sayı "dağıtıma özgü yama ya da derleme numarasını belirtmektedir. Çalışmakta olan Linux sistemi hakkında bilgiler "uname -a" komutu ile elde edilebilir. Örneğin: $ uname -a inux kaan-virtual-machine 5.15.0-91-generic #101-Ubuntu SMP Tue Nov 14 13:30:08 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux Bu bilgi içerisindeki çekirdek versiyonu "uname -r" ile elde edilebilir: $ uname -r 5.15.0-91-generic Buradan biz çekirdeğin "5.15.0" sürümünün kullanıldığını anlıyoruz. Burada genel yapılandırılmış bir çekirdek söz konusudur. 91 sayısı dağıtıma özgü yama ya da derleme numarasını belirtir. Aslında "uname" komutu bu bilgileri "/proc" dosya sisteminin içerisinde almaktadır. Örneğin: $ cat /proc/version Linux version 5.15.0-91-generic (buildd@lcy02-amd64-045) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #101-Ubuntu SMP Tue Nov 14 13:30:08 UTC 2023 Çekirdeğin derlenmesi için zaten çekirdek kodlarında bir "build sistemi" oluşturulmuştur. Buna "KConfig sistemi" ya da "KBuild sistemi" denilmektedir. Biz önce çekirdek derleme işleminin hangi adımlardan geçilerek yapılacağını göreceğiz. Sonra çekirdeğin önemli konfigürasyon parametreleri üzerinde biraz duracağız. Sonra da çekirdekte bazı değişiklikler yapıp değiştirilmiş çekirdeği kullanacağız. Linux'ta çekirdeğin davranışını değiştirmek için farklı olanaklara sahip 5 yöntem kullanılabilmektedir: -> Çekirdeğin boot parametreleri yoluyla davranışının değiştirilmesi. Bunun için çekirdeğin yeniden derlenmesi gerekmez. -> Kernel mode aygıt sürücüsü yazmak yoluyla çekirdeğin davranışının değiştirilmesi. Bunun çekirdek kodlarının yeniden derlenmesi gerekmez. -> Çekirdeğin konfigürasyon parametrelerinin değiştirilmesiyle davranışının değiştirilmesi. Bunun için çekirdeğin yeniden derlenmesi gerekir. -> Çekirdeğin kodlarının değiştirilmesiyle davranışının değiştirilmesi. Bunun için de çekirdeğin yeniden derlenmesi gerekir. -> Çekirdeğin bazı özellikleri "proc" dosya sistemindeki bazı dosyalara birtakım değerler yazarak da değiştirilebilmektedir. Aslında bu tür değişiklikler "systemd" init sisteminde "systemctl" komutuyla da yapılabilmektedir. Örneğin sistem çalışırken bir prosesin açabileceği dosya sayısını "proc" dosya sistemi yoluyla şöyle değiştirebiliriz: $ echo 2048 | sudo tee /proc/sys/fs/file-max Linux'ta çekirdek derlemesi tipik olarak aşağıdaki aşamalardan geçilerek gerçekleştirilmektedir: >> Derleme öncesinde derlemenin yapılacağı makinede bazı programların yüklenmiş olması gerekmektedir. Gerekebilecek tipik programlar aşağıda verilmiştir: $ sudo apt update $ sudo apt install build-essential libncurses-dev bison flex libssl-dev wget gcc-arm-linux-gnueabihf \ binutils-arm-linux-gnueabihf libelf-dev dwarves >> Çekirdek kodları indirilerek açılır. Biz bu konuyu yukarıda ele almıştık. İndirmeyi şöyle yapanbiliriz: $ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.9.2.tar.xz Bu işlemden sonra "linux-6.9.2.tar.xz" isimli dosya indirilmiş durumdadır. Onu aşağıdaki gibi açabiliriz: $ tar -xvJf linux-6.9.2.tar.xz Bu işlemden sonra "linux-6.9.2" isminde bir dizin oluşturulacaktır. Ayıca ek bir bilgi olarak eğer Ubuntu türevi bir dağıtımda çalışıyorsanız istediğniz bir çekirdeği aşağıdaki gibi indirip kurabilirsiniz: sudo apt install linux-image-<çekirdek_sürümü> Örneğin: $ sudo apt install linux-image-5.15.0-91-generic >> Çekirdek derlenmeden önce konfigüre edilmelidir. Çekirdeğin konfigüre edilmesi birtakım çekirdek özelliklerin belirlenmesi anlamına gelmektedir. Konfigürasyon bilgileri çekirdek kaynak kod ağacının kök dizininde (örneğimizde "linux-6.9.2" dizini) ".config" ismiyle bulunmalıdır. Bu ".config" dosyası default durumda kaynak dosyaların kök dizininde bulunmamaktadır. Bunun çekirdeği derleyen kişi tarafından oluşturulması gerekmektedir. Çekirdek konfigürasyon parametreleri oldukça fazladır. Biz izleyen paragraflarda önemli çekirdek konfigürasyon parametrelerini göreceğiz. Çekirdek konfigürasyon parametreleri çok fazla olduğu için bunlar bazı genel amaçları karşılayacak biçimde default değerlerle önceden oluşturulmuş durumdadır. Bu önceden oluşturulmuş default konfigürasyon dosyaları "arch//configs" dizininin içerisinde bulunmaktadır. Örneğin Intel X86 mimarisi için bu default konfigürasyon dosyaları şöyledir: $ ls arch/x86/configs hardening.config i386_defconfig tiny.config x86_64_defconfig xen.config Burada biz 64 bit Linux sistemleri için "x86_64_defconfig" dosyasını kullanabiliriz. O halde bu dosyayı kaynak dosyaların bulunduğu dizininin kök dizinine ".config" ismiyle kopyalayabiliriz: $ cp arch/x86/configs/x86_64_defconfig .config Biz bütün işlemlerde çekirdek kaynak kodlarının kök dizininde bulunduğumuzu (current working directory) varsayacağız. Ancak burada bir noktaya dikkatinizi çekmek istiyoruz. Linux kaynak kodlarındaki default konfigürasyon dosyaları minimal biçimde konfigüre edilmiştir. Bu nedenle pek çok modül bu default konfigürasyon dosyalarında işaretlenmiş değildir. Bu default konfigürasyon dosyalarını kullanarak derleme yaptığınızda bazı çekirdek modüllerinin seçilmemiş olması nedeniyle sisteminiz açılmayabilir. Bu tür denemeleri zaten var olan konfigürasyon dosyalarını kullanarak yaparsanız daha fazla modül dosyası oluşturulabilir ancak daha az zahmet çekebilirsiniz. Linux sistemlerinde genel olarak "/boot" dizini içerisinde "configs-<çekirdek_sürümü>" ismi altında mevcut çekirdeğe ilişkin konfigürasyon dosyası bulundurulmaktadır. Burada bir noktaya dikkatinizi çekmek istiyoruz. Çekirdek kaynak kodlarındaki "arch//configs/x86_64_defconfig" dizinindeki konfigürasyon dosyası ".config" ismiyle kopyalandıktan sonra ayrıca "make menuconfig" gibi bir işlemle onun satırlarına bazı default değerlerin de eklenmesi gerekir. Bu default değerler "arch/" dizinindeki "Kconfig" dosyasından gelmektedir. Bu nedenle bu default konfigürasyon dosyalarını kaynak kök dizine ".config" ismiyle kopyaladıktan sonra aşağıda belirtildiği gibi "make menuconfig" yapmalısınız. Aslında ".config" dosyasını oluşturmanın başka alternatif yolları da vardır: -> make defconfig: Bu komut çalıştığımız sisteme uygun olan konfigürasyon dosyasını temel alarak mevcut donanım bileşenlerini de gözden geçirerek sistemin açılması için gerekli minimal bir konfigürasyon dosyasını ".config" ismiyle oluşturmaktadır. Örneğin biz 64 bit Intel sistemine ilişkin bir bilgisayarda çalışıyorsak "make defconfig" dediğimizde "arch/x86/configs/x86_64_defconfig" dosyası temel alınarak o anda çalışılmakta olan çekirdek donanımları da dikkate alınarak nispeten minimal olan bir konfigürasyon dosyası oluşturmaktadır. -> make oldconfig: Bu seçeneği kullanmak için kaynak kök dizinde bir ".config" dosyasının bulunyor olması gerekir. Ancak bu seçenek KConfig dosyasındaki ve kaynak dosya ağacındaki diğer değişiklikleri de göz önüne alarak bu eski ".config" dosyasını eğer söz konusu mimaride birtakım değişiklikler söz konusu ise o değişikliklere uyumlandırmaktadır. Yani örneğin biz eski bir ".config" dosyasını kullanıyor olabiliriz. Ancak çekirdeğin yeni versiyonlarında ek birtakım başka konfigürasyon parametreleri de eklenmiş olabilir. Bu durumda "make oldconfig" bize bu eklenenler hakkında da bazı sorular sorup bunların dikkate alınmasını sağlayacaktır. -> make _defconfig: Bu seçenek belli bir platformun default konfig dosyasını ".config" dosyası olarak save etmektedir. Örneğin biz Intel makinelerinde çalışıyor olabiliriz ancak BBB için default konfigürasyon dosyası oluşturmak isteyebiliriz. Eğer biz "make defconfig" yaparsak Intel tabanlı bulunduğumuz platform dikkate alınarak ".config" dosyası oluşturulur. Ancak biz burada örneğin "make bb.org_defconfig" komutunu uygularsak bu durumda Intel mimarisinde çalışıyor olsak da "bb.org_defconfig" konfigürasyon dosyası ".config" olarak save edilir. Tabii bu durumda biz aslında yine ilgili platformun konfigürasyon dosyasını manuel olarak ".config" biçiminde de kopyalayabiliriz. -> make modules: Bu seçenek ile yalnızca modüller derlenir. Yani bu seçenek ".config" dosyasında belirtilen aygıt sürücü dosyalarını derler ancak çekirdek derlemesi yapmaz. Yalnızca "make" işlemi zaten aynı zamanda bu işlemi de yapmaktadır. Aşağıdaki ilave konfig seçenekleri ise seyrek kullanılmaktadır: -> make allnoconfig: Tüm seçenekleri hayır (no) olarak ayarlar (minimal yapılandırma). -> make allyesconfig: Tüm seçenekleri evet (yes) olarak ayarlar (maksimum özellikler). -> make allmodconfig: Tüm aygıt sürücülerin çekirdeğin dışında modül (module) biçiminde derleneceğini belirtir. -> make localmodconfig: Sistemde o anda yüklü modüllere dayalı bir yapılandırma dosyası (".config" dosyası) oluşturur. -> make silentoldconfig: Yeni seçenekler için onları görmezden gelir ve o yeni özellikler ".config" dosyasına yansıtılmaz. -> make dtbs: Kaynak kod ağacında "/arch/platform/boot/dts" dizininideki aygıt ağacı kaynak dosyalarını derler ve "dtb" dosyalarını elde eder. Gömülü sistemlerde bu işlemin yapılması ve her çekirdek versiyonuyla o versiyonun "dtb" dosyasının kullanılması tavsiye edilir. Yukarıda da belirttiğimiz gibi aslında pek çok dağıtım o anda yüklü olan çekirdeğe ilişkin konfigürasyon dosyasını "/boot" dizini içerisinde "config-$(uname -r)" ismiyle bulundurmaktadır. Örneğin kursun yapılmakta olduğu Mint datıtımında "/boot" dizinin içeriği şöyledir: $ ls /boot config-5.15.0-91-generic grub initrd.img-5.15.0-91-generic vmlinuz efi initrd.img System.map-5.15.0-91-generic vmlinuz-5.15.0-91-generic Buradaki "config-5.15.0-91-generic" dosyası çalışmakta olduğumuz çekirdekte kullanılan konfigürasyon dosyasıdır. Benzer biçimde BBB'deki built-in eMMC içerisinde bulunan çekirdekteki "/boot" dizininin içeriği de şöyledir: SOC.sh dtbs System.map-5.10.168-ti-r71 initrd.img-5.10.168-ti-r71 uboot config-5.10.168-ti-r71 vmlinuz-5.10.168-ti-r71 Buradaki konfigürasyon dosyası da "config-5.10.168-ti-r71" biçimindedir. Eğer çalışılan sistemdeki konfigürasyon dosyasını temel alacaksanız bu dosyayı Linux kaynak kodlarının bulunduğu kök dizine ".config" ismiyle kopyalayabilirsiniz. Örneğin: $ cp /boot/config-$(uname -r) .config Fakat eski bir konfigürasyon dosyasını yemni bir öçekirdekle kullanmak için ayrıca "make oldconfig" işleminin de yapılması gerekmektedir. >> Şimdi elimizde pek çok değerin set edilmiş olduğu ".config" isimli bir konfigürasyon dosyası vardır. Artık bu konfigürasyon dosyasından hareketle yalnızca istediğimiz bazı özellikleri değiştirebiliriz. Bunun için "make menuconfig" komutunu kullanabiliriz: $ make menuconfig Bu komut ile birlikte grafik ekranda konfigürasyon seçenekleri listelenecektir. Tabii buradaki seçenekler default değerler almış durumdadır. Bunların üzerinde değişiklikler yaparak ".config" dosyasını save edebiliriz. Aslında "make menuconfig" işlemi hiç ".config" dosyası oluşturulmadan doğrudan da yapılabilmektedir. Bu durumda hangi sistemde çalışılıyorsa o sisteme özgü default config dosyası temel alınmaktadır. Biz en azından "General stup/Local version - append to kernel release" seçeneğine "-custom" gibi bir sonek girmenizi böylece yeni çekirdeğe "-custom" soneki iliştirmenizi tavsiye ederiz. ".config" dosyası elde edildiğinde çekirdek imzalamasını ortadan kaldırmak için dosyayı açıp aşağıdaki özellikleri belirtildiği gibi değiştirebilirsiniz (bunların bazıları zaten default durumda aşağıdaki gibi dde olabilir): CONFIG_SYSTEM_TRUSTED_KEYS="" CONFIG_SYSTEM_REVOCATION_KEYS="" CONFIG_SYSTEM_TRUSTED_KEYRING=n CONFIG_SECONDARY_TRUSTED_KEYRING=n CONFIG_MODULE_SIG=n CONFIG_MODULE_SIG_ALL=n CONFIG_MODULE_SIG_KEY="" Çekirdek imzalaması konusu daha ileride ele alınacaktır. Yukarıda a belirttiğimiz gibi derlenecek çekirdeklere yerel bir versiyon numarası da atanabilmektedir. Bu işlem Bu işlem "make menuconfig" menüsünde "General Setup/Local version - append custom release" seçeneği kullanılarak ya da ".config" dosyasında "CONFIG_LOCALVERSION" kullanılarak yapılabilir. Örneğin: CONFIG_LOCALVERSION="-custom" Artık çekirdek sürümüne "-custom" sonekini eklemiş olduk. >> Derleme işlemi için "make" komutu kullanılmaktadır. Örneğin: $ make Eğer derleme işleminin birden fazla CPU ile yapılmasını istiyorsanız "-j" seçeneğini komuta dahil edebilirsiniz. Çalışılan sistemdeki CPU sayısının "nproc" komutuyla elde edildiğini anımsayınız: $ make -j$(nproc) Derleme işlemi bittiğinde ürün olarak biz "çekirdek imajını", "çekirdek tarafından yüklenecek olan modül dosyalarını (aygıt sürücü dosyalarını)" ve diğer bazı dosyaları elde etmiş oluruz. Derleme işleminden sonra elde oluşturulan dosyalar ve onların yerleri şöyledir (buradaki <çekirdek_sürümü> "uname -r" ile elde edilecek değeri belirtiyor): -> Sıkıştırılmış Çekirdek Imajı: "arch//boot" dizininde "bzImage" ismiyle oluşturulmaktadır. Denemeyi yaptığımız Intel makinede dosyanın yol ifadesi "arch/x86_64/boot/bzImage" biçimindedir. -> Çekirdeğin Sıkıştırılmamış ELF İmajı: Kaynak kök dizininde "vmlinux" isminde dosya biçiminde ooluşturulur. -> Çekirdek Modülleri (Aygıt Sürücü Dosyaları): "drivers" dizininin altındaki dizinlerde ve "fs" dizininin altındaki dizinlerde ve "net" dizininin altındaki dizinlerde. Ancak "make modules_install" ile bunların hepsi belirli bir dizine çekilebilir. -> Çekirdek Sembol Tablosu: Kaynak kök dizininde "System.map" ismiyle bulunuyor. Çekirdeğin derlemesi ne kadar zaman almaktadır? Şüphesiz bu derlemenin yapıldığı makineye göre değişebilir. Ancak derleme sürecinin uzamasına yol açan en önemli etken çekirdek konfigüre edilirken çok fazla modülün seçilmesidir. Pek çok dağıtım "belki lazım olur" gerekçesiyle konfigürasyon dosyalarında pek çok modülü dahil etmektedir. Bir dağıtımın konfigürasyon dosyasını kullandığınız zaman çekirdek derlemesi uzayacaktır. Ayrıca çekirdek konfigüre edilirken çok fazla modülün dahil edilmesi modüllerin çok fazla yer kaplamasına da yol açabilmektedir. Çekirdek kodlarındaki platforma özgü default konf,gürasyon dosyaları daha minimalist bir biçimde oluşturulmuş durumdadır. >> Derleme sonrasında farklı dizinlerde oluşturulmuş olan aygıt sürücü dosyalarını (modülleri) belli bir dizine kopyalamak için "make modules_install" komutu kullanılmaktadır. Bu komut seçeneksiz kullanılırsa default olarak "/lib/modules/<çekirdek_sürümü>" dizinine kopyalama yapar. Her ne kadar bu komut pek çok ".ko" uzantılı aygıt sürücü dosyasını hedef dizine kopyalıyorsa da bunların hepsi çekirdek tarafından belleğe yüklenmemektedir. Çekirdek gerektiği zaman gereken aygıt sürücüleri bu dizinden alarak yüklemektedir. Örneğin: $ sudo make modules_install Aslında "make modules_install" komutunun modül dosyalarını (aygıt sürücü dosyalarını) istediğimiz bir dizine kopyalamasını da sağlayabiliriz. Bunun için INSTALL_MOD_PATH komut satırı argümanı kullanılmaktadır. Örneğin: $ sudo INSTALL_MOD_PATH=modules make modules_install Burada aygıt sürücü dosyaları "/lib/modules/<çekirdek_sürümü>" dizinine değil bulunulan yerdeki "modules" dizinine kopyalanacaktır. Pekiyi "make modules_install" komutu yalnızca modül dosyalarını mı hedef dizine kopyalıyor? Hayır aslında bu komut modül dosyalarının kopyalanması dışında bazı dosyaları da oluşturup onları da hedef dizine kopyalamaktadır. Bu komut sırasıyla şunları yapmaktadır: -> Modül dosyalarını "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. -> "modules.dep" isimli dosyayı oluşturur ve bunu "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. -> "modules.alias" isimli dosyayı oluşturur ve bunu "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. -> "modules.order" isimli dosyayı oluşturur ve "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. -> "modules.builtin" isimli dosyayı "/lib/modules/<çekirdek_sürümü>" dizinine kopyalar. Aslında burada oluşturulan dosyaların bazıları mutlak anlamda bulunmak zorunda değildir. Ancak sistemin öngörüldüğü gibi işlev göstermesi için bu dosyaların ilgili dizinde bulunması uygundur. Bir aygıt sürücü başka bir aygıt sürücüleri de kullanıyor olabilir. Bu durumda bu aygıt sürücü yüklenirken onun kullandığı tüm sürücülerin özyinelemeli olarak yüklenmesi gerekir. İşte "modules.dep" dosyası bir aygıt sürücünün yüklenmesi için başka hangi sürücülerin yüklenmesi gerektiği bilgisini tutmaktadır. Aslında "modules.dep" bir text dosyadır. Bu text dosya" satırlardan oluşmaktadır. Satırların içeriği şöyledir: : ... Dosyanın içeriğine şöyle örnek verebiliriz: ... kernel/arch/x86/crypto/nhpoly1305-sse2.ko.zst: kernel/crypto/nhpoly1305.ko.zst kernel/lib/crypto/libpoly1305.ko.zst kernel/arch/x86/crypto/nhpoly1305-avx2.ko.zst: kernel/crypto/nhpoly1305.ko.zst kernel/lib/crypto/libpoly1305.ko.zst kernel/arch/x86/crypto/curve25519-x86_64.ko.zst: kernel/lib/crypto/libcurve25519-generic.ko.zst ... Eğer bu "modules.dep" dosyası olmazsa bu durumda "modeprob" komutu çalışmaz ve çekirdek modülleri yüklenirken eksik yükleme yapılabilir. Dolayısıyla sistem düzgün bir biçimde açılmayabilir. Eğer bu dosya elimizde yoksa ya da bir biçimde silinmişse bu dosyayı yeniden oluşturabiliriz. Bunun için "dempmod -a" komutu kullanılmaktadır. Komut doğrudan kullanıldığında o anda çekirdek sürümü için "modules.dep" dosyasını oluşturmaktadır. Örneğin: $ sudo depmod -a Ancak siz yüklü olan başka bir çekirdek sürümü için "modules.dep" dosyasını oluşturmak istiyorsanız bu durumda çekirdek sürümünü de komut satırı argümanı olarak aşağıdaki gibi komuta vermelisiniz: $ sudo depmod -a <çekirdek sürümü> Tabii depmod komutunun çalışabilmesi için "/lib/modules/<çekirdek_sürümü> dizininde modül dosyalarının bulunuyor olması gerekir. Çünkü bu komut bu dizindeki modül dosyalarını tek tek bulup ELF formatının ilgili bölümlerine bakarak modülün hangi modülleri kullandığını tespit ederek "modules.dep" dosyasını oluşturur. "modules.alias" dosyası belli bir isim ya da id ile aygıt sürücü dosyasını eşleştiren bir text dosyadır. Bu dosyanın bulunmaması bazı durumlarda sorunlara yol açmayabilir. Ancak örneğin USB port'a bir aygıt takıldığında bu aygıta ilişkin aygıt sürücünün hangisi olduğu bilgisi bu dosyada tutulmaktadır. Bu durumda bu dosyanın olmayışı aygıt sürücünün yüklenememesine neden olabilir. Dosyanın içeriği aşağıdaki formata uygun satırlardan oluşmaktadır: alias Örnek bir içerik şöyle olabilir: ... alias usb:v05ACp*d*dc*dsc*dp*ic*isc*ip*in* apple_mfi_fastcharge alias usb:v8086p0B63d*dc*dsc*dp*ic*isc*ip*in* usb_ljca alias usb:v0681p0010d*dc*dsc*dp*ic*isc*ip*in* idmouse alias usb:v0681p0005d*dc*dsc*dp*ic*isc*ip*in* idmouse alias usb:v07C0p1506d*dc*dsc*dp*ic*isc*ip*in* iowarrior alias usb:v07C0p1505d*dc*dsc*dp*ic*isc*ip*in* iowarrior ... Bu dosya bir biçimde silinirse yine "depmod" komutu ile oluşturulabilir. (Yani depmod komutu yalnızca "modules.dep" dosyasını değil bu dosyayı da oluşturmaktadır.) "modules.order" dosyası aygıt sürücü dosyalarının yüklenme sırasını barındıran bir text dosyadır. Bu dosyanın her satırında bir çekirdek aygıt ssürücüsünün dosya yol ifadesi bulunur. Daha önce yazılmış aygıt sürücüler daha sonra yazılanlardan daha önce yüklenir. Bu dosyanın olmaması genellikle bir soruna yol açmaz. Ancak modüllerin belli sırada yüklenmemesi bozukluklara da neden olabilmektedir. Bu dosyanın da silinmesi durumunda yine bu dosya da "depmod" komutuyla oluşturulabilmektedir. >> Eğer gömülü sistemler için derleme yapıyorsanız kaynak kod ağacındaki "arch//boot/dts" dizini içerisindeki aygıt ağacı kaynak dosyalarını da derlemelisiniz. Tabii elinizde zaten o versiyona özgü aygıt dosyası bulunuyor olabilir. Bu durumda bu işlemi hiç yapmayabilirsiniz. Aygıt ağacı kaynak dpsyalarını derlemek için "make dtbs" komutunu kullanabilirsiniz: $ make dtbs Derlenmiş aygıt ağacı dosyaları "arch//boot/dts" dizininde oluşturulacaktır. >> Bizim çekirdek imajını, geçici kök dosya sistemine ilişkin dosyayı ve aygıt ağacı dosyasını uygun yere yerleştirmemiz gerekir. Bu dosyalar "/boot" dizini içerisinde bulunmalıdır. Ancak aslında bu işlem de "make install" komutuyla otomatik olarak yapılabilmektedir. "make install" komutu aynı zamanda "grub" isimli bootloder programın konfigürasyon dosyalarında da güncelleme yapıp yeni çekirdeğin "grub" menüsü içerisinde görünmesini de sağlamaktadır. Komut şöyle kullanılabilir: $ sudo make install Bu komut ile sırasıyla yapılanlar şunlardır: -> Çekirdek imaj dosyası "arch//boot/bzImage" hedef "/boot" dizinine "vmlinuz-<çekirdek_sürümü>" ismiyle kopyalanır. -> "System.map" dosyası hedef "/boot" dizinine "System.map-<çekirdek_sürümü>" ismiyle kopyalanır. -> ".config" dosyası "/boot" dizinine "config-<çekirdek_sürümü>" ismiyle kopyalanır. -> "Geçici kök dosya sistemi dosyası oluşturulur ve hedef "/boot" dizinine "initrd.img-<çekirdek_sürümü>" ismiyle kopyalanır. -> Eğer "grub" boot loader kullanılıyorsa "grub" konfigürasyonu güncellenir ve "grub"" menüsüne yeni girişler eklenir. Böylece sistemin otomatik olarak yeni çekirdekle açılması sağlanır. Yukarıda da belirttiğimiz gibi derleme işlemi sonucunda elde edilmiş olan dosyaların hedef sistemde bazı dizinlerde bulunuyor olması gerekir. Bu yerleri bir kez daha belirtmek istiyoruz: -> Çekirdek Imajı ---> "/boot" dizinine -> Çekirdek Sembol Tablosu ---> "/boot" dizinine -> Modül Dosyaları ---> "/lib/modules/<çekirdek_sürümü>/kernel" dizinin altında Ancak yukarıdaki dosyalar dışında isteğe bağlı olarak aşağıdaki dosyalar da hedef sisteme konuşlandırılabilir: -> Konfigürasyon Dosyası ---> "/boot" dizini -> Geçici Kök Dosya Sistemi Dosyası ---> "/boot" dizinine -> Modüllere İlişkin Bazı Dosyalar ---> "/lib/modules/<çekirdek_sürümü>" dizinine Pekiyi yukarıda belirttiğimiz dosyalar hedef sistemdeki ilgili dizinlere hangi isimlerle kopyalanmalıdır? İşte tipik isimlendirme şöyle olmalıdır (buradaki <çekirdek_sürümü> "uname -r" komutuyla elde edilecek olan yazıdır): -> Çekirdek İmajı: "/boot/vmlinuz-<çekirdek_sürümü>". Örneğin "vmlinuz-6.9.2-custom" gibi. -> Çekirdek Sembol Tablosu: "/boot/System.map-<çekirdek_sürümü>". Örneğin "System.map-6.9.2-custom" gibi. -> Modüllere İlişkin Dosyalar: Bunlar yukarıda da belirttiğimiz gibi "/lib/modules/<çekirdek_sürümü>" dizininin içerisine kopyalanmalıdır. -> Konfigürasyon Dosyası: "/boot/config-<çekirdek_sürümü>". Örneğin "config-6.9.2-custom" gibi. -> Geçici Kök Dosya Sistemine İlişkin Dosya: "/boot/initrd.img-<çekirdek_sürümü>". Örneğin "initrd.img-6.9.2-custom" gibi. Ayrıca bazı dağıtımlarda "/boot" dizini içerisindeki "vmlinuz" dosyası default olan "vmlinuz-<çekirdek_sürümü>" dosyasına, "inird.img" dosyası da "/boot/initrd.img-<çekirdek_sürümü>" dosyasına sembolik link yapılmış durumda olabilir. Ancak bu sembolik bağlantıları "grub" kullanmamaktadır. Aşağıda Intel sistemindeki "/boot" dizinin default içeriğini görüyorsunuz: $ ls -l total 141168 -rw-r--r-- 1 root root 261963 Kas 14 2023 config-5.15.0-91-generic drwx------ 3 root root 4096 Oca 1 1970 efi drwxr-xr-x 7 root root 4096 Ara 5 19:02 grub lrwxrwxrwx 1 root root 28 Ara 5 20:28 initrd.img -> initrd.img-5.15.0-91-generic -rw-r--r-- 1 root root 126391088 Tem 11 20:19 initrd.img-5.15.0-91-generic -rw------- 1 root root 6273869 Kas 14 2023 System.map-5.15.0-91-generic lrwxrwxrwx 1 root root 25 Ara 5 20:28 vmlinuz -> vmlinuz-5.15.0-91-generic -rw-r--r-- 1 root root 11615272 Kas 14 2023 vmlinuz-5.15.0-91-generic Pekiyi derleme sonucunda elde ettiğimiz dosyaları manuel isimlendirirken çekirdek sürüm yazısını nasıl bileceğiz? Bunun için "uname -r" komutunu kullanamayız. Çünkü bu komut bize o anda çalışmakta olan çekirdeğin sürüm yazısını verir. Biz yukarıdaki denemede Linux'un "6.9.2" sürümünü derledik. Bunun sonuna da "-custom" getirirsek sürüm yazısının "6.9.2-custom" olmasını bekleriz. Ancak bu sürüm yazısı aslında manuel olarak isim değiştirmekle oluşturulamamaktadır. Bu sürüm yazısı çekirdek imajının içerisine yazılmaktadır ve bizim bazı dosyayalara verdiğimiz isimlerin çekirdek içerisindeki bu yazıyla uyumlu olması gerekir. Default olarak "kernel.org" sitesinden indirilen kaynak kodlar derlendiğinde çekirdek sürümü "6.9.2" gibi üç haneli bir sayı olmaktadır. Yani yazının sonunda "-generic" gibi "-custom" gibi bir sonek yoktur. İşte çekirdeği derlemeden önce daha önceden de belirttiğimiz gibi ".config" dosyasında "CONFIG_LOCALVERSION" özelliğine bu sürüm numarasından sonra eklenecek bilgiyi girebilirsiniz. Örneğin: CONFIG_LOCALVERSION="-custom" Anımsayacağınız gibi bu işlem "make menuconfig" menüsünde "General Setup/Local version - append custom release" seçeneği kullanılarak da yapılabilmektedir. Biz buradaki örneğimizde bu işlemi yaparak çekirdeği derledik. Dolayısıyla bizim derlediğimiz çekirdekte çekirdek imajı içerisinde yazan sürüm ismi "6.9.2-custom" biçimindedir. Pekiyi biz bu ismi unutsaydık nasıl öğrenebilirdik. Bunun basit bir yolu sıkıştırılmamış çekirdek dosyası içerisindeki (kaynak kök dizindeki "vmlinux" dosyası) string tablosunda "Linux version" yazısını aramaktır. Örneğin: $ strings vmlinux | grep "Linux version" Linux version 6.9.2-custom (kaan@kaan-virtual-machine) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) # SMP PREEMPT_DYNAMIC Linux version 6.9.2-custom (kaan@kaan-virtual-machine) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #2 SMP PREEMPT_DYNAMIC Thu Dec 5 17:55:14 +03 2024 Buradan sürüm yazısının "6.9.2-custom" olduğu görülmektedir. O halde bizim derleme sonucunda elde ettiğimiz dosyaları manuel biçimde kopyalarken sürüm bilgisi olarak "6.9.2-custom" yazısını kullanmalıyız. Çekirdek imajının "/boot" dizinine manuel kopyalanması işlemi şöyle yapılabilir (kaynak kök dizinde bulunduğumuzu varsayıyoruz): $ sudo cp arch/x86_64/boot/bzImage /boot/vmlinuz-6.9.2-custom Konfigürasyon dosyasını da şöyle kopyalayabiliriz: $ sudo cp .config /boot/config-6.9.2-custom Tabii bizim çekirdek modüllerini de "/lib/modules/6.9.2-custom/kernel" dizinine kopyalamamız gerekir. Ayrıca bir de geçici kök dosya sistemine ilişkin dosyayı da kopyalamamız gerekir. Çekirdek modüllerinin kopyalanması biraz zahmetli bir işlemdir. Çünkü bunlar derlediğimiz çekirdekte farklı dizinlerde bulunmaktadır. Bu kopyalamanın en etkin yolu "make modules_install" komutunu kullanmaktır. Benzer biçimde çekirdek dosyalarının ve gerekli diğer dosyaların uygun yerlere kopyalanması için en etkin yöntem "make install" komutudur. Normal olarak biz "make install" yaptığımızda eğer sistemimizde "grub" önyükleyicisi varsa komut "grub" konfigürasyon dosyalarında güncellemeler yaparak sistemin yeni çekirdekle açılmasını sağlamaktadır. Ancak kullanıcı bir menü yoluyla sistemin kendi istediği çekirdekle açılmasını sağlayabilir. Grub menüsü otomatik olarak görüntülenmemektedir. Boot işlemi sırasında ESC tuşuna basılırsa menü görüntülenir. Eğer "grub" menüsünün her zaman görüntülenmesi isteniyorsa "/etc/default/grub" dosyasındaki iki satır aşağıdaki gibi değiştirilmelidir: GRUB_TIMEOUT_STYLE=menu GRUB_TIMEOUT=5 Buradaki GRUB_TIMEOUT satırı eğer menünün müdahale yapılmamışsa en fazla 5 saniye görüntüleneceğini belirtmektedir. Bu işlemden sonra "update-grub" programı da çalıştırılmalıdır: $ sudo update-grub Bu tür denemeler yapılırken "grub" menüleri bozulabilmektedir. Düzeltme işlemleri bazı konfigürasyon dosyalarının edit edilmesiyle manuel biçimde yapılabilir. Konfigürasyon dosyaları güncelleendikten sonra "update-grub" programı mutlaka çalıştırılmalıdır. Ancak eğer "grub" konfigürasyon dosyaları konusunda yeterli bilgiye sahip değilseniz "grub" işlemlerini görsel bir biçimde "grub-customizer" isimli programla da yapabilirsiniz. Bu program "debian depolarında" olmadığı için önce aşağıdaki gibi programın bulunduğu yerin "apt" kayıtlarına eklenmesi gerekmektedir: $ sudo add-apt-repository ppa:danielrichter2007/grub-customizer $ sudo apt-get update Bu işlemden sonra kurulum yapılabilir: $ sudo apt-get install grub-customizer Biz yukarıda çekirdek derleme ve yeni çekirdeği kurma sürecini maddeler halinde açıkladık. Şimdi yukarıdaki adımları özet hale getirelim: -> Çekirdek derlemesi için gerekli olan araçlar indirilir. -> Çekirdek kodları indirilir ve açılır. -> Zaten hazır olan konfigürasyon dosyası ".config" biçiminde kaynak kök dizine save edilir. -> Konfigrasyon dosyası üzerinde "make menuconfig" komutu ile değişiklikler yapılır. -> Çekirdek derlemesi "make -j$(nproc)" komutu ile gereçekleştirilir. -> Modüller ve ilgili dosyalar hedefe "sudo make modules_install" komututu ile konuşlandırılır. -> Çekirdek imajı ve ilgili dosyalar "sudo make install" komutu ile hedefe konuşlandırılır. Pekiyi yeni çekirdeği derleyip sisteme dahil ettikten sonra nasıl onu sistemden tamamen çıkartabiliriz? Tabii yapılan işlemlerin tersini yapmak gerekir. Bu işlem manuel biçimde şöyle yapılabilir: -> "/lib/modules/<çekirdek_sürümü>" dizini tamamen silinebilir. -> "/boot" dizinindeki çekirdek sürümüne ilişkin dosyalar silinmelidir. -> "/boot" dizininden çekirdek sürümüne ilişkin dosyalar silindikten sonra "update-grub" programı sudo ile çalıştırılmalıdır. Bu program "/boot" dininini inceleyip otomatik olarak ilgili girişleri "grub" menüsünden siler. Yani aslında "grub" konfigürasyon dosyaları üzerinde manuel değişiklik yapmaya gerek yoktur. "grub" işlemleri için diğer bir alternatif ise "grub-customizer" programı ile görsel silme yapmaktır. Ancak bu program "/boot" dizini içerisindeki dosyaları ve modül dosyalarını silmez. Yalnızca ilgili girişleri "grub" menüsündne çıkartmaktadır. Pekiyi biz Intel sisteminde çalışırken ARM için çekirdek derlemesini nasıl yapabiliriz? Bir platformda çalışırken başka bir platform için derleme yapılabilir. Ancak hedef platforma ilişkin ismine "araç zinciri (toolchain)" denilen bir paketin yüklenmiş olması gerekir. Araç zincirleri yalnızca derleyicilerden değil sistem programlama için gerekli olan çeşitli programları barındıran paketlerdir. Örneğin ARM platformu için çeşitli araç zincirleri bulunmaktadır. ARM platformu için en yaygın kullanılan araç zincirleri aşağıdaki bağlantıdan indirilebilir: https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads Örneğin Beaglebone Black (BBB) için Windows'ta çalışan araç zinciri bu sitede aşağıdaki bağlantıya tıklanarak indirilebilir: arm-gnu-toolchain-14.2.rel1-mingw-w64-i686-arm-none-linux-gnueabihf.zip Genel olark araç zincirleri kullanılmadan önce birkaç çevre değişkeninin set edilmesi gerekmektedir: -> CROSS_COMPILE isimli çevre değişkeni araç zincirinin öneki ile set edilmelidir. Örneğin: $ export CROSS_COMPILE=arm-none-linux-gnueabihf- -> PATH çevre değişkenine araç zincirine ilişkin "bin" dizininin eklenmesi gerekir: $ PATH=$PATH:/home/kaan/Study/UnixLinux-SysProg/arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf/bin -> ARCH çevre değişkeninin hedef platformu belirten bir yazı ile set edilmesi gerekir. ARM platformu için bu yazı "arm" biçimindedir: $ export ARCH=arm Bundan sonra çekirdeğin kaynak kodları yukarıda belirtildiği gibi derlenebilir. Burada bu işlemin ayrıntısı üzerinde durmayacağız. Şimdi de çekirdek kodlarının değiştirilip derlenmesine bir örnek verelim. Çekirdek kodlarında değişiklik yapmanın birkaç yolu olabilir: -> Çekirdek kodlarındaki bir dosya içerisinde bulunan fonksiyon kodlarında değişiklik yapılması. -> Çekirdek kodlarındaki bir dosya içerisine yeni bir fonksiyon eklenmesi. -> Çekirdek kodlarındaki bir dizin içerisine yeni bir C kaynak dosyası eklenmesi. -> Çekirdek kodlarındaki bir dizin içerisine yeni bir dizin ve bu dizinin içerisinde çok sayıda C kaynak dosyalarının eklenmesi. Bu yollardan, >> Eğer biz birinci maddedeki ve ikinci maddedeki gibi çekirdek kodlarına yeni bir dosya eklemiyorsak çekirdeğin derlenmesini sağlayan make dosyalarında bir değişiklik yapmamıza gerek yoktur. >> Ancak üçüncü ve dördüncü maddedeki gibi çekirdeğe yeni bir kaynak dosya ya da dizin ekleyeceksek bu eklemeyi yaptığımız dizindeki make dosyasında bu ekleme izleyen paragraflarda açıklayacağımız biçimde belirtilmelidir. Böylece çekirdek yeniden derlendiğinde bu dosyalar da çekirdek imajının içerisine eklenmiş olacaktır. >> Eğer kaynak kod ağacında bir dizinin altına yeni bir dizin eklemek istersek bu durumda o dizini yine ana dizine ilişkin make dosyasında belirtmemiz ve o dizinde ayrı bir Makefile oluşturmamız gerekmektedir. Pekiyi çekirdek kodlarındaki bir dosya içerisindeki bir fonksiyonda değişiklik yaptığımızda çekirdek modüllerini yeniden hedef makineye aktarmamız gerekir mi? İşte genel olarak bu tür basit değişikliklerde çekirdek modüllerinin güncellenmesi gerekmemektedir. Ancak ne olursa olsun bu durum yapılan değişikliklere de bağlıdır. Bu nedenle çekirdek modüllerinin de yeniden "make modules_install" komutu hedef makineye çekilmesi önerilir. Örneğin biz çekirdek kaynak kod ağacında "fs/open.c" içerisinde "chdir" sistem fonksiyonun aşağıdaki gibi bir satır ekleyelim: SYSCALL_DEFINE1(chdir, const char __user *, filename) { struct path path; int error; unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY; printk(KERN_INFO "directory is changing...\n"); // YENİ EKLENEN KOD PARÇASI retry: error = user_path_at(AT_FDCWD, filename, lookup_flags, &path); if (error) goto out; error = path_permission(&path, MAY_EXEC | MAY_CHDIR); if (error) goto dput_and_out; set_fs_pwd(current->fs, &path); dput_and_out: path_put(&path); if (retry_estale(error, lookup_flags)) { lookup_flags |= LOOKUP_REVAL; goto retry; } out: return error; } Bu işlemden sonra sırasıyla aşağıdakiler yapıp sisytemi yeni ekirdekle açabiliriz: make -j$(nproc) make modules_install make install Bu yeni çekirdekte ne zaman bir dizin değiştirilse bir log yazısı oluşturulmaktadır. Bu yazıları "dmesg" komutuyla görebilirsiniz. Pekiyi biz çekirdeğin kaynak kod ağacına yeni bir ".c" dosyası eklemek istersek ne yapacağız? İşte bu durumda çekirdeğin make dosyalarında bu eklemenin belirtilmesi gerekmektedir. Çekirdek kodlarında her kaynak kod dizininde ayrı bir Makefile dosyası bulunmaktadır. Programcı yeni kaynak dosyayı hangi dizine ekliyorsa o dizine ilişkin Makefile içerisine aşağıdaki gibi bir satır eklemesi gerekir: obj-y += dosya_ismi.o Böylece artık make işlem yapıldığında bu dosya da derlenip çekirdek imajına dahil edilecektir. Buradaki += operatörü obj-y isimli hedefe ekleme yapma anlamına gelmektedir. "obj" sözcüğünün yanındaki "-y" harfi ilgili dosyanın çekirdeğin bir parçası biçiminde çekirdek imajının içerisine gömüleceğini belirtmektedir. Make dosyalarının bazı satırlarında "obj-y" yerine "obj-m" de görebilirsiniz. Bu da ilgili dosyanın ayrı bir modül biçiminde derleneceği anlamına gelmektedir. Eklemeler genellikle çekirdek imajının içine yapıldığı için biz de "obj-y" kullanırız. Eğer bir dosyayı biz çekirdek imajının içine gömmek yerine ayrı bir çekirdek modülü olarak derlemek istiyorsak bu durumda dosyayı yerleştirdiğimiz dizinin "Makefile" dosyasına aşağıdaki gibi bir ekleme yaparız: obj-m += dosya_ismi.o Eğer çekirdek kaynak kodlarına tümden bir dizin eklemek istiyorsak bu durumda o dizini oluşturduğumuz dizindeki "Makefile" dosyasına aşağıdaki gibi bir ekleme yaparız: obj-y += dizin_ismi/ Burada dizin isminden sonra '/' karakterini unutmayınız. Tabii bu ekleme bir modül biçiminde de olabilirdi: obj-m += dizin_ismi/ Fakat bu ekleme yapıldıktan sonra bizim ayrıca yarattığımız dizinde "Makefile" isimli bir dosya oluşturmamız ve o dosyanın içerisinde o dizinde çekirdek kodlarına ekleyeceğimiz dosyaları belirtmemiz gerekir. Örneğin biz "drivers" dizininin altına "mydriver" isimli bir dizin oluşturup onun da içerisine "a.c" "b.c" ve "c.c" dosyalarını eklemiş olalım. Bu durumda önce "drivers" dizini içerisindeki Makefile dosyasına aşağıdaki gibi bir satır ekleriz: obj-y += mydriver/ Sonra da "mydriver" dizini içerisinde "Makefile" isimli bir dosya oluşturup bu dosyanın içerisinde de bu dizin içerisindeki dosyaları belirtiriz. Örneğin: obj-y += a.o obj-y += b.o obj-y += c.o Örneğin biz kaynak kod ağacında "drivers" dizinin altında "mydriver" isimli dizin yaratıp onun içerisine "mydrive.c" dosyasını yerleştirmek isteyelim. Sırasıyla şunları yapmamız gerekir: >> "drivers" dizini altında "mydriver" dizini yaratırız. >> "drivers" dizini içerisindeki Makefile dosyasına aşağıdaki satır ekleriz: obj-y += mydriver/ >> "drivers/mydriver" dizini içerisinde "mydriver.c" dosyasını oluştururz. Dosyanın içeriği şöyle olabilir: #include #include static int __init helloworld_init(void) { printk(KERN_INFO "Hello World...\n"); return 0; } static void __exit helloworld_exit(void) { printk(KERN_INFO "Goodbye World...\n"); } module_init(helloworld_init); module_exit(helloworld_exit); >> "drivers/mydriver" dizini içerisinde "Makefile" isimli dosya oluştururz ve içine aşağıdaki satır ekleriz: obj-y += mydriver.o >> Çekirdek kod dizinin kök dizinine gelip ve sırasıyla aşağıdaki komutları uygularız: make -j$(nproc) make modules_install make install Böylece sistem yeni çekirdekle açılabilir. Aygıt sürücünün çekirdeğe dahil edildiğini geçmiş dmesg mesajlarına bakarak aşağıdaki gibi anlayabilirssiniz: $ dmesg | grep -i "Hello World..." [ 0.949515] Hello World... Şimdi de yeni bir sistem fonksiyonunu çekirdeğe eklemek isteyelim. Linux çekirdeğinde sistem fonksiyonlarının adresleri bir fonksiyon gösterici dizisinde tutulmaktadır. Bu gösterici dizisinin her elemanı bir sistem fonksiyonun adresini içerir. O halde çekirdeğe bir sistem fonksiyonu ekleyebilmek için sistem fonksiyonunu bir dosya içerisine yazmak ve bu tabloya o fonksiyonu gösteren bir giriş eklemek gerekir. Bunun yapılış biçimi Linux'un çeşitli versiyonlarında değiştirilmiştir. Aşağıda güncel bir versiyonda bu işlemin nasıl yapıldığına ilişkin bir örnek vereceğiz: >> Sistem fonksiyonumuz "mysyscall" biçiminde isimlendirmiş olalım. Önce yine çekirdek kaynak kod ağacında uygun bir dizine yine bir dosya eklemek gerekir. Bunun için en uygun dizin "kernel" dizinidir. Bu durumda sistem fonksiyonumuzu "kernel" dizini içerisinde "mysyscall.c" ismiyle yazabiliriz: /* mysyscall.c */ #include #include #include SYSCALL_DEFINE0(mysyscall) { printk(KERN_INFO "My system call\n"); return 0; } Bundan sonra kernel dizini içerisindeki "Makefile" dosyasına aşağıdaki satırı ekleriz: obj-y += mysyscall.o >> Sistem fonksiyon tablosuna ilgili sistem fonksiyonu bir eleman olarak girilir. Sistem fonksiyon tablosu "arch//syscall/xxx.tbl" dosyasında belirtilmektedir. 64 bit Linux sistemleri için bu dosya "arch/x86/entry/syscalls/syscall_64.tbl" biçimindedir. Ekleme bu dosyanın sonuna aşağıdaki gibi yapılabilir: ...... 544 x32 io_submit compat_sys_io_submit 545 x32 execveat compat_sys_execveat 546 x32 preadv2 compat_sys_preadv64v2 547 x32 pwritev2 compat_sys_pwritev64v2 # This is the end of the legacy x32 range. Numbers 548 and above are # not special and are not to be used for x32-specific syscalls. 548 common mysyscall sys_mysyscall >> Artık çekirdek aaşağıdaki gibi derlenebilir: $ sudo make -j$(nproc) >> Çekirdek modüllerini aşağıdaki gibi install edebiliriz: $ sudo make modules_install >> Çekirdeğin kendisini de şöyle install edebiliriz: $ sudo make install Sistem fonksiyonunu çekişrdeğe yerleştirip yeni çekirdekle makinemizi açtıktan sonra fonksiyonun tesitini aşağıdaki gibi yapabiliriz: #include #include #include #define SYS_mysyscall 548 int main(void) { printf("running...\n"); syscall(SYS_mysyscall); return 0; } Bu programı derleyip çalıştırdıktan sonra "dmesg" yaptığımızda aşağıdaki gibi bir çıktı elde etmeliyiz: .... file uses a different sequence number ID, rotating. [ 144.816248] warning: `ThreadPoolForeg' uses wireless extensions which will stop working for Wi-Fi 7 hardware; use nl80211 [ 487.365691] My system call /*================================================================================================================================*/ (197_09_02_2025) & (198_16_02_2025) & (199_21_02_2025) & (200_23_02_2025) & (201_07_03_2025) & (202_09_03_2025) (203_16_03_2025) & (204_21_03_2025) > Veritabanı Yönetim Sistemleri (VTYS) : Her ne kadar sistem programlamanın doğrudan veritabanlarıyla bir ilgisi yoksa da C programcılarının yine de sistem programcıların bazı durumlarda veritabanları oluşturup onları kullanması gerekebilmektedir. Bu bölümde C'den SQL kullanarak veritabanlarıyla nasıl işlem yapılacağı üzerinde duracağız. Eskiden veritabanı işlemleri kütüphanelerle yapılıyordu. Daha sonra veritabanı işlemleri için özel programlar geliştirildi. Veritabanı işlemlerini ayrıntılı ve etkin bir biçimde gerçekleştiren yazılımlara "Veritabanı Yönetim Sistemleri (VTYS/DBMS)" denilmektedir. Günümüzde çeşitli firmalar ve kurumlar tarafından geliştirilmiş pek çok VTYS vardır. Bunların bazıları kapalı ve ücretli yazılımlardır. Bazıları ise açık kaynak kodlu ve ücretsiz yazılımlardır. En çok kullanılan VTYS yazılımları şunlardır: -> IBM DB2 (Dünyanın ilk VTYS'sidir.) -> Oracle (Oracle firmasının en önemli ürünü.) -> SQL Server (Microsoft firmasının VTYS'si.) -> MySQL (Açık kaynak kodlu, ancak Oracle firması satın aldı ve gelecekteki durumu tartışmalı.) -> MariaDB (Açık kaynak kodlu, MySQL Oracle tarafından satın alınınca kapatılma tehlikesine karşı MySQL varyantı olarak devam ettirilmektedir.) -> PostgreSQL (Açık kaynak kodlu, son yıllarda geniş kesim tarafından kullanılan VTYS.) -> SQLite (Gerçek anlamda bir VTYS değil, VTYS'yi taklit eden mini bir kütüphane gibi. Bu tür yazılımlara "gömülü VTYS" de denilmektedir.) -> Microsoft Access Jet Motoru (Bu da Microsoft'un gömülü bir VTYS sistemidir. Microsoft Access tarafından da kullanılmaktadır.) Bir yazılımın VTYS olabilmesi için onun bazı gelişmiş özelliklere sahip olması gerekir: -> VTYS'ler kullanıcılarına yüksek seviyeli bir çalışma modeli sunmaktadır. -> VTYS'ler genellikle dış dünyadan istekleri "SQL (Structured Query Language)" denilen dekleratif bir dille almaktadır. Yani programcı VTYS'ye iş yaptırmak için SQL denilen bir yüksek seviyeli dekleratif bir dil kullanmaktadır. VTYS SQL komutlarını alıp onları parse eder ve C ve C++ gibi dillerde yazılmış olan motor kısmı (engine) tarafından işlemler yapılır. SQL, veritabanı işlemlerini yapan bir dil değildir. Programcı ile VTYS arasında yüksek seviyeli iletişim için kullanılan bir dildir. VTYS'lerin motor kısımları genellikle C ve C++ gibi sistem programlama dilleriyle yazılmaktadır. -> VTYS'ler pek çok yüksek seviyeli araçlara da sahiptir. Örneğin backup ve restore işlemlerini yapan araçlar her VTYS'de bulunmaktadır. -> VTYS'ler genellikle birden fazla kullanıcıya aynı anda hizmet verecek biçimde client-server mimarisine uygun biçimde yazılmaktadır. Bunlara uzaktan erişilebilmekte ve aynı anda pek çok kullanıcı bunlara iş yaptırabilmektedir. -> VTYS'ler ileri derece güvenlik sunmaktadır. Bir kullanıcı başka bir kullanıcının bilgilerine erişememektedir. -> VTYS'ler yüksek miktarda kayıtlardan oluşan veritabanları üzerinde etkin bir biçimde işlemler yapabilmektedir. Sistem programlama uygulamalarında bazen küçük veritabanlarının oluşturulması gerekebilmektedir. Bu tür durumlarda kapasiteli VTYS'ler yerine tek bir dosyadan oluşan adeta bir kütüphane biçiminde yazılmış olan gömülü VTYS'lerden (embedded DBMS) faydalanılmaktadır. Bunların en çok kullanılanı SQLite denilen gömülü VTYS'dir. Örneğin bir tarayıcı yazdığımızı düşünelim. Son ziyaret edilen Web sayfalarının bir biçimde tarayıcıdan çıkıldıktan sonra saklanması gerekir. İşte bu tür durumlarda SQLite gibi basit yapıda VTYS'ler tercih edilebilmektedir. Internet bağlantısı olmayan mobil cihazlarda da SQLite gibi gömülü VTYS'ler çokça kullanılmaktadır. Örneğin biz bir soket uygulaması yazmış olalım. Bu uygulama bir log tutacak olsun. Burada SQLServer, MySQL gibi büyük çaplı VTYS'ler yerine SQLite gibi bir gömülü VTYS'yi tercih edebiliriz. Gömülü VTYS'ler büyük çaplı veritabanlarında iyi bir performans gösterememektedir. Bunlar daha çok küçük ve orta çaplı veritabanlarında kullanılmaktadır. Veritabanı işlemleri için C'den ziyade yüksek seviyeli diller tercih edilmektedir. Örneğin Java, C#, Python gibi diller veritabanı işlemlerinde oldukça yaygın kullanılmaktadır. Benzer biçimde JavaScript de Web uygulamalarında veritabanları üzerinde işlem yapmak için kullanılan dillerdendir. Günümüzde veritabanlarının en çok kullanıldığı uygulamalar Web uygulamalarıdır. Bir Web mağazasına girdiğinizde oradaki bütün ürünler veritabanlarında tutulmaktadır. VTYS'lerin client-server mimarisine uygun bir biçimde yazıldığını belirtmiştik. VTYS'lerle tipik çalışma şu biçimdedir: -> Programcı "kullanıcı ismi" ve "parola" ile VTYS'ye bağlanır. Bu durumda programcı client durumunda, VTYS ise server durumundadır. -> Programcı VTYS'ye yaptırmak istediği şeyleri SQL dilinde oluşturur ve VTYS'ye SQL komutlarını gönderir. -> VTYS bu SQL komutlarını parse eder ve istenilen işlemleri yapar, programcıya işlemin sonuçlarını iletir. -> Programcı işi bittiğinde bağlantıyı kapatır. Her ne kadar SQLite ve Microsoft Access Jet Motoru gibi VTYS'ler aslında client-server çalışmıyor olsa da bunlar çalışma biçimi olarak geniş kapasiteli VTYS'leri taklit etmektedir. Veritabanları tasarım bakımından birkaç gruba ayrılmaktadır. Günümüzde en çok kullanılan veritabanı mimarisine "İlişkisel Veritabanı (Relational Database) Mimarisi" denilmektedir. İlişkisel veritabanları "tablolardan (tables)", tablolar da sütun ve satırlardan oluşmaktadır. Örneğin biz öğrencilerin bilgilerini tutmak için bir veritabanı tablosu oluşturalım. Bu tablo aşağıdaki görünümde olsun: Adı Soyadı Numarası Sınıfı --------------------------------------- Ali Serçe 1234 3B Güray Sönmez 6745 2C Ayşe Er 6234 2B ... Tablolardaki sütunlara "alan (field)", satırlara ise "satır (row)" ya da "kayıt (record)" denilmektedir. MySQL, SQLServer, Oracle, SQLite gibi VTYS'ler ilişkisel veritabanı mimarisini kullanmaktadır. Hiyerarşik bilgileri (örneğin bir ağaç yapısını) tutmak için hiyerarşik veritabanı mimarileri kullanılabilmektedir. Son 15 senedir ismine "nosql" denilen ilişkisel olmayan ve özellikle metin tabanlı bilgiler üzerinde işlem yapan veritabanı mimarileri sıkça kullanılır hale gelmiştir. Ancak en yaygın kullanılan mimari ilişkisel veritabanı mimarisidir. Şimdi de yukarıda kabaca açıkladığımız MySQL, SQLite gibi VTYS'lerini açıklayalım: >> "MySQL" : MySQL'i kurmak için tek yapılacak şey server programı http://dev.mysql.com/downloads/ sitesinden indirip yüklemektir. Kurulum oldukça basittir. Birtakım sorular default değerlerle geçilebilir. Ancak kurulum sırasında MySQL kurulum programı bizden “root” isimli yetkili kullanıcının parolasını istenecektir. Bu parola yetkili olarak VTYS'ye bağlanmak için gerekir. Server programın yanı sıra bir yönetim ekranı elde etmek için ayrıca "MySql Workbench" programı da kurulabilir. MySQL Linux sistemlerinde Debian paket yöneticisi ile aşağıdaki gibi basit bir biçimde kurulabilir: $ sudo apt-get install mysql-server Kütüphane dosyaları da şöyle indirilebilir: $ sudo apt-get install libmysqlclient21 MySQL Workbench ise komut satırı yerine Web Sayfasından indirilerek kurululabilir. Yukarıda da belirttiğimiz gibi MySQL kısmen paralı hale getirilince bunun MariaDB isimli bir klonu oluşturuldu. MariaDB'nin uzun vadede açık kaynak kod güvencesi olduğu için tercih edebilirsiniz. >> "SQL" : SQL paralı bir üründür. Fakat bunun da "Express Edition" isminde bedava bir sürümü vardır. Bu sürüm Microsoft'un sayfasından indirilip kurulabilir. Tıpkı MySQL'de olduğu gibi SQL Server'da da yönetim konsol programı vardır. Buna "SQL Server Management Studio" denilmektedir. Bunun da indirilip kurulması tavsiye edilir. >> "SQLite" : SQLite zaten tek bir DLL'den oluşmaktadır. Dolayısıyla aslında kurulumu diye bir durum söz konusu değildir. Fakat biz burada C için örnekler yaparken SQLite başlık dosyalarına ve SQLite DLL’inin import kütüphanesine sahip olmak zorundayız. Bunların nasıl elde edileceği sonraki konularda ele alınacaktır. SQLite yönetim konsolu olarak pek çok alternatif vardır. Bunlardan biri "FireFox Add On" olarak çalışmaktadır. Diğer seçenekler ise “SQLite Studio” ve "SQLite Browser" programlarıdır. Cross Platform olan bu araç ilgili web sayfasından indirilerek kurulabilir. Ya da daha genel "DBeaver" da tercih edilebilir. SQLite'ı Windows için aşağıdaki bağlantıdan indirebilirsiniz: https://www.sqlite.org/download.html Buradan indirilen zip dosyasının içerisinde bir tane ".DLL" dosyası bir ".DEF" dosyası bulunacaktır. Bu DLL'i PATH dizinlerinin içerisine ya da uygulama dizininin içerisine çekebilirsiniz. Linux'ta SQLite şöyle indirilebilir: $ sudo apt-get install sqlite İlişkisel veritabanları tablolardan, tablolar da sütunlardan (fields) oluşmaktadır. Tabii sütunların da veri türleri vardır. SQL Standartları'nda standart bazı veri türleri belirtilmiştir. Ancak SQL VTYS'den VTYS'ye değişiklik gösterebilmektedir. Dolayısıyla her VTYS'nin SQL komutlarında bazı farklılıklar bulunabilmektedir. Biz burada bazı standart sütun türleri üzerinde duracağız. Çalıştığınız VTYS'nin dokümanlarından onlara özgü ayrıntıları elde edebilirsiniz. -> INTEGER: Tamsayısal bilgileri tutan bir türdür. İstenirse kaç digitlik sayıların tutulacağı da belirtilebilir. -> INT: Tipik olarak 4 byte uzunluğunda işaretli tamsayı türüdür. (Örneğin bu tür C’deki int türü ile temsil edilebilir.) -> SMALLINT: Tipik olarak 2 byte'lık işaretli tamsayı türüdür. (Örneğin bu tür C’deki short türü ile temsil edilebilir.) -> BIGINT: Tipik olarak 8 byte uzunluğunda işaretli tamsayı türüdür. (Örneğin bu tür C’deki long long türü ile temsil edilebilir.) -> FLOAT: Tipik olarak 4 byte'lık gerçek sayı türüdür. (Örneğin bu tür C’deki float türü ile temsil edilebilir.) -> DOUBLE: Tipik olarak 8 byte'lık gerçek sayı türüdür. (Örneğin bu tür C’deki double türü ile temsil edilebilir.) -> TIME: Zaman bilgisini saklamak için kullanılan türdür. -> DATE: Tarih bilgisini saklamak için kullanılan türdür. -> CHAR(n): n karakterli yazıyı tutmak için kullanılan türdür. -> VARCHAR(n): En fazla n karakterli bir yazıyı tutmak için kullanılan türdür. -> TINYTEXT: Yazısal bilgileri tutmak için kullanılan türdür. (Tipik olarak 256 byte'a kadar) -> TEXT: Yazısal bilgileri tutmak için kullanılan türdür. (Tipik olarak 64K'ya kadar) -> LONGTEXT: (Tipik olarak 4GB'ye byte'a kadar) -> TINYBLOB: Binary bilgileri tutmak için kullanılan türdür. (Tipik olarak 256 byte'a kadar) -> BLOB: Binary bilgileri tutmak için kullanılan türdür. (Tipik olarak 64K'ya kadar) -> LONGBLOB: Binary bilgileri tutmak için kullanılan türdür. (Tipik olarak 4GB'ye byte'a kadar) Tablo sütunlarının türleri tablo yaratılırken belirlenmektedir. Şimdi de temel SQL komutlarını görelim. SQL bazı ayrıntıları olan dekleratif bir programlama dilidir. Komutlardan oluşmaktadır. Biz burada temel SQL komutlarını ayrıntılarına girmeden ele alacağız. SQL büyük harf küçük harf duyarlılığı olmayan (case insensitive) bir dildir. Ancak geleneksel olarak anahtar sözcüklerin büyük harflerle yazılması tercih edilmektedir. SQL komutlarının sonunda sonlandırıcı olarak ';' karakteri bulundurulmaktadır. Komutlar, -> CREATE DATABASE Komutu: İlişkisel veritabanlarında “veritabanı” tablolardan oluşmaktadır. Bu nedenle önce bir veritabanının yaratılması, sonra da onun içerisinde tabloların yaratılması gerekir. Veritabanlarını yaratmak için CREATE DATABASE komutu kullanılır. Komutun genel biçimi şöyledir: CREATE DATABASE ; Örneğin: CREATE DATABASE student; -> USE Komutu: Belli bir veritabanı üzerinde işlemler yapmak için öncelikle onun seçilmesi gerekir. Bu işlem USE komutuyla yapılır. Komutun genel biçimi şöyledir: USE ; -> SHOW DATABASES Komutu: Bu komut VTYS'de yaratılmış olarak bulunan veritabanlarını gösterir. Komutun genel biçimi şöyledir: SHOW DATABASES; -> CREATE TABLE Komutu: Bu komut veritabanı için bir tablo yaratmak amacıyla kullanılır. Komutun genel biçimi şöyledir: CREATE TABLE ( , , ... ); Aslında bu komutun bazı ayrıntıları vardır. Bu ayrıntılar ilgili dokümanlardan öğrenilebilir. Örneğin: CREATE TABLE student_info(student_id PRIMARY KEY AUTO_INCREMENT, student_name VARCHAR(45), student_no INTEGER); Bir tabloda tekrarlanması yasaklanmış olan sütunlara “birincil anahtar (primary key)” denilmektedir. Tablodaki kayıtların hepsinin birincil anahtar sütunları farklı olmak zorundadır. Başka bir deyişle biz bir tabloya orada zaten var olan birincil anahtar değerine ilişkin bir kayıt ekleyemeyiz. Her tabloda bir tane birincil anahtarın olması tavsiye edilmektedir. Birincil anahtarın tablo yaratılırken CREATE TABLE komutunda belirtilme biçimi çeşitli VTYS’lerde farklı olabilmektedir. -> DROP TABLE Komutu: Bu komut tabloyu silmek için kullanılır. Komutun genel biçimi şöyledir: DROP TABLE ; Örneğin: DROP TABLE person; -> INSERT INTO Komutu: Bu komut bir tabloya bir satır eklemek için kullanılır. Komutun genel biçimi şöyledir: INSERT INTO (sütun1, sütun2, sütun3,...) VALUES (değer1, değer2, değer3,...); Tabloya satır eklerken aslında her sütun bilgisinin belirtilmesi gerekmez. Bu durumda o sütun için tablo yaratılırken (CREATE TABLE komutunda) belirlenmiş olan default değerler kullanılır. Komutun ayrıntılı genel biçimi için ilgili dokümanlara başvurabilirsiniz. Örneğin: INSERT INTO student_info(student_name, student_no) VALUES('Güray Sönmez', 754); Değerler girilirken yazılar ve tarihler tek tırnak içerisinde belirtilmelidir. -> WHERE Cümleciği: Pek çok komut bir WHERE kısmı içermektedir. Where cümleciği koşul belirtmek için kullanılır. Koşullar karşılaştırma operatörleriyle oluşturulur. Mantıksal operatörlerle birleştirilebilir. Örneğin: WHERE age > 20 AND birth_place = 'Eskişehir' LIKE operatörü joker karakterleri kullanılarak belli bir kalıba uyan yazı koşulu oluşturur. Örneğin: WHERE student_name LIKE 'A%' Burada student_name için 'a' ile başlayanlar koşulu verilmiştir. % karakteri "geri kalanı herhangi biçimde olabilir" anlamına gelir. Örneğin: WHERE student_name LIKE '%an' Burada sonu 'an' ile bitenler koşulu verilmiştir. WHERE cümleciğinin bazı detayları vardır. Bu detaylar ilgili dokümanlardan öğrenilebilir. -> DELETE FROM Komutu: Bu komut bir tablodan satır silmek için kullanılır. Komutun genel biçimi şöyledir: DELETE FROM ; -> UPDATE Komutu: Update komutu belli kayıtların alan bilgilerini değiştirmek amacıyla kullanılır. Örneğin ismi "Kağan" olan bir kaydı "Kaan" olarak değiştirmek isteyebiliriz. Ya da bir müşterinin bakiyesini değiştirmek isteyebiliriz. Komutun genel biçimi şöyledir: UPDATE SET alan1 = değer1, alan2 = değer2, ... WHERE ; Örneğin: UPDATE student_info SET student_name = 'Kaan Kaplan' WHERE student_name = 'Kaan Aslan' DELETE ve UPDATE komutlarını kullanrıken dikkat ediniz. Çünkü eğer koşul belirtmezseniz ya da koşulu yanlış belirtirseniz yaptıpınız işlemden birden fazla kayıt etkilenir. Örneğin: UPDATE student_info SET student_name = 'Ali Ballı' WHERE student_name = 'Veli Ballı'; Burada koşul zayıf oluşturulmuştur. Bu durumda bütün "Veli Ballı" isimleri "Ali Ballı" olarak değiştirilir. Örneğin: UPDATE student_info SET student_name = 'Ali Ballı' WHERE student_name = 'Veli Ballı' AND student_no = 754; Artık burada ismi "Veli Ballı" olan ve numarası 754 olan satırın ismi "Ali Ballı" olarak değiştirilecektir. -> SELECT Komutu: Koşulu sağlayan kayıtların elde edilmesi SELECT komutuyla yapılmaktadır. SELECT komutunun genel biçimi oldukça ayrıntılıdır. Çünkü komuta çeşitli cümlecikler monte edilebilmektedir. Komutun genel biçimi şöyledir: SELECT FROM [WHERE 600; Burada öğrenci numarası 600'den büyük olan öğrencilerin tüm sütun bilgileri elde edilmiştir. Eğer SELECT edilen kayıtlar belli bir sütuna göre sıralı biçimde elde edilmek istenirse ORDER BY cümleciği komuta eklenir. Örneğin: SELECT * FROM student WHERE student_id > 600 ORDER BY stdent_name; Burada öğrenci numarası 600'den büyük olan öğrencilerin tüm sütun bilgileri elde edilmiştir. Eğer SELECT edilen kayıtlar belli bir sütuna göre sıralı biçimde elde edilmek istenirse ORDER BY cümleciği komuta eklenir. Örneğin: SELECT * FROM student WHERE student_id > 600 ORDER BY stdent_name; ORDER BY default olarak kayıtları küçükten büyüğe (ASC) vermektedir. Ancak DESC ile büyükten küçüğe de sıralama yapılabilir. Örneğin: SELECT * FROM student WHERE student_no > 600 ORDER BY student_name DESC; ORDER BY cümleciğinde birden fazla sütun belirtilebilir. Bu durumda ilk sütun değerleri aynıysa diğer sütunlar dikkate alınır. Örneğin: SELECT * FROM student WHERE student_no > 600 ORDER BY student_name DESC, student_no ASC; Burada ismi aynı olanlar numaralarına göre küçükten büyüğe elde edilecektir. LIMIT cümleceği de SELECT cümlesiyle kullanılabilir. LIMIT anahtar sözcüğnün yanında bir sayı bulunur. Koşulu sağlayan kayıtların belli sayıda miktarını elde etmek için kullanılır. Örneğin: SELECT * FROM student WHERE student_no > 600 ORDER BY student_name DESC, student_no ASC LIMIT 10; WHERE cümleciğinde built-in fonksiyonlar kullanılabilir. Örneğin: SELECT * FROM city WHERE char_length(city) = 6; şeklindedir. İlişkisel veritabanlarında tablolarda veri tekrarı istenmez. Örneğin bir öğrenci veritabanı oluşturacak olalım. Bir öğrencinin çeşitli bilgilerinin yanı sıra onun okulu hakkında da bilgileri tutmak isteyelim. Aşağıdaki gibi bir tablo tasarımı uygun değildir: Adı Soyadı No Okul Adı Okulun Bulunduğu Şehir Okulun Türü -------------------------------------------------------------------------------------- Ali Serçe 123 Tarsus Amerikan Lisesi Mersin Devlet Lisesi Kaan Aslan 745 Eskişehir Atatürk Lisesi Eskişehir Devlet Lisesi Hasan Bulur 734 Tarsus Amerikan Lisesi Mersin Devlet Lisesi ... ... ... ... Burada Okul Adı bilgileri gereksiz bir biçimde tekrarlanmaktadır. Bu tekrarı engellemek için iki tablo oluşturabiliriz. Öğrenci Tablosu Adı Soyadı No Okul ID'si ---------------------------------- Ali Serçe 123 100 Kaan Aslan 745 235 Hasan Bulur 734 100 ... ... ... Okul Tablosu Okul Id'si Okul Adı Okulun Bulunduğu Şehir Okulun Türü ------------------------------------------------------------------------------------ ... ... ... ... 100 Tarsus Amerikan Lisesi Mersin Devlet Lisesi 150 Eskişehir Atatürk Lisesi Eskişehir Devlet Lisesi ... ... ... ... Burada veri tekrarı ortadan kaldırılmıştır. Tabii bu tablolarda da Okul ID'si ortak bir sütundur. Bu ortak sütun tablolar arasında ilişki kurmak için gerekmektedir. Bu tür sütunlara "foreign key" de denilmektedir. Ancak yukarıdaki gibi tekrarlar engellendiğinde gerekli bilgiler artık tek bir tablodan değil, çeşitli tablolardan çekilip alınacaktır. İşe çeşitli tablolardan bilgilerin çekilip alınması işlemine "JOIN" işlemi denilmektedir. JOIN işleminin birkaç biçimi vardır (INNER JOIN, OUTER JOIN, LEFT JOIN, RIGHT JOIN gibi). Ancak en fazla kullanılan JOIN işlemi "INNER JOIN" denilen işlemdir. JOIN denildiğinde zaten default olarak INNER JOIN anlaşılır. INNER JOIN işleminde eğer iki tablo söz konusu ise önce iki tablonun kartezyen çarpımları elde edilir. Her kaztezyen çarpım iki tablonun birleştirilmesi biçiminde ("join" ismi oradan geliyor) elde edilmektedir. Sonra kartezyen çarpımlarda yalnızca belli koşulu sağlayan satırlar elde edilir. Böylece tablolar "ilişkisel (relational)" biçimde birleştirilmiş olur. INNER JOIN sentaksı iki biçimde oluşturulabilmektedir. Birinci sentaks klasik eski tip sentakstır. İkinci sentaks daha modern biçimdir. Klasik eski tip sentaks şöyledir: SELECT FROM INNER JOIN ON ; Örneğin: SELECT student.student_name, student.student_no, school.school_name FROM student INNER JOIN school ON student.school_id = school.school_id WHERE stduent.student_no > 600; Sütun isimleri belirtilirken eğer çakışma yoksa yalnızca isimler yazılabilir. Ancak çakışma varsa tablo ismi ve nokta operatörü ile sütunun hangi tabloya ilişkin olduğu belirtilmelidir. Bazı uygulamacılar çakışma olsa da olmasa da niteliklendirme yaparlar. Bazı uygulamacılar yalnızca çakışan sütunlarda niteliklendirme yaparlar. Yukarıdaki örnekte tüm sütunlar niteliklendirilerek belirtilmiştir. Bu örnek şöyle de yapılabilirdi: SELECT student_name, student_no, school_name FROM student INNER JOIN school ON student.school_id = school.school_id WHERE student_no > 600; Modern INNER JOIN sentaksında SELECT komutunun FROM kısmında birden fazla tablo ismi belirtilir. Koşul da yine WHERE cümleciğine taşınır. Örneğin: SELECT student_name, student_no, school_name FROM student, school WHERE student.school_id = school.school_id AND student_no > 600; Daha çok bu modern biçim tercih edilmektedir. Şimdi de SQLite, MYSQL gibi VTYS'leri nasıl kullanacağımıza değinelim: >> "SQLite" : İşlemlere başlamadan önce SQLite’ın programalama kurulumunu yapmamız gerekir. SQLite yukarıda da belirtitğimiz gibi çok küçük (tek bir dinamik kütüphaneden oluşan) bir VTYS’dir. Dolayısıyla onun kurulması Windows’ta bildiğimiz anlamda bir setup işlemi ile yapılmaz. Tabii bizim C’den SQLite kütüphanesini kullanabilmemiz için ona ilişkin başlık ve kütüphane dosyalarını elde etmemiz gerekir. Windows'ta SQLite'ın resmi indirme sitesi şöyledir: https://sqlite.org/download.html Buradan aşağıdaki iki indirme yapılır: -> Precompiled Binaries for Windows (32 bit ya da 64 bit) -> SQLite Amalgamation Birinci indirmede tek bir DLL elde edilecektir. İkinci indirmede de "sqlite3.h" başlık dosyası ve kaynak dosyası elde edilecektir. Ayrıca SQlite için "sqlite3" isminde komut satırından kullanılan bir program da bulundurulmuştur. Bu programın kullanımına ilişkin bilgileri aşağıdaki bağlantıdan edinebilirsiniz: https://www.sqlite.org/cli.html Birinci indirmede Windows için gereken sqlite3.dll ve sqlite3.def dosyaları elde edilir. Buradaki “.def” dosyasına “module definition file” denilmektedir. Bu dosya “DLL’in import kütüphanesi” gibi link aşamasına dahil edilebilir. Ya da istenirse aşağıdaki komutla bu “.def” dosyasından “.lib” uzantılı “import kütüphanesi de oluşturulabilmektedir: LIB /DEF:sqlite3.def /machine:x86 Buradaki machine argümanı hedef sistemi belirtmektedir. Burada 32 bit Windows sistemleri için x86, 64 bit Windows sistemleri için "x64" kullanılmalıdır. Örneğin: LIB /DEF:sqlite3.def /machine:x64 İkinci indirmeden biz SQLite’ın kaynak dosyalarını elde ederiz. Buradaki “sqlite3.h” dosyası SQLite fonksiyonları için başlık dosyası niteliğindedir. Mac OS X için kurulum Windows’takine benzemektedir. Yine ilgili “.zip” dosyaları indirilip kurulum yapılabilir. Bu sistemlerde derleme yaparken link aşamasında "-lsqlite3" ile kütüphane dosyasını belirtmeyi unutmayınız. Kurulum sonrası her şeyin hazır oladuğunu anlamak için SQLite kütüphanesinin versiyon numarasını yazdıran aşağıdaki gibi bir programla test işlemi yapabilirsiniz: #include #include "sqlite3.h" int main(void) { printf("%s\n", sqlite3_libversion()); return 0; } C’de SQLite veritabanı ile işlem yapmak için önce o veritabanının sqlite3_open fonksiyonuyla açılması gerekir. Bu işlemden sqlite3 türünden bir handle elde edilir. sqlite3_open fonksiyonunun prototipi şöyledir: int sqlite3_open( const char *filename, /* Database filename (UTF-8) */ sqlite3 **ppDb /* OUT: SQLite db handle */ ); Fonksiyonun birinci parametresi bizden sqlite dosyasının yol ifadesini alır. İkinci parametresi sqlite3 isimli yapı türünden bir göstricinin adresini almaktadır. Fonksiyon handle alanını (yani sqlite yapı nesnesini) oluşturur. Onun adresini bu göstericinin içerisine yerleştirir. Fonksiyonun geri dönüş değeri işlemin başarısını belirtmektedir. Fonksiyon başarılıysa SQLITE_OK değerine geri döner. Fonksiyon başarısız olduğunda yine dosyanın sqlite3_close fonksiyonuyla kapatılması gerekir. Hata nedeni de sqlite3_errmsg fonksiyonuyla yazdırılabilir. Bu durumda sqlite dosyasının açılması tipik olarak şöyle yapılabilir: if (sqlite3_open("student.db", &db) != SQLITE_OK) { fprintf(stderr, "sqlite3_open failed: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); exit(EXIT_FAILURE); } sqlite3_open fonksiyonu dosya varsa olanı açar, yoksa yeni bir SQLite DB dosyası yaratır. sqlite3_errmsg fonksiyonunun parametrik yapısı şöyledir: const char *sqlite3_errmsg(sqlite3 *db); sqlite3_close fonksiyonunun prototipi ise şöyledir: int sqlite3_close(sqlite3*); Dosya kapatılırken başarı kontrolü yapmaya gerek yoktur. Başarısızlık durumlarında hata mesajını stderr dosyasına yazdırıp programı sonlandıran bir sarma fonksiyon şöyle yazılabilir: void sqlite3_exit(const char *msg, sqlite3 *db) { fprintf(stderr, "%s failed => %s\n", msg, sqlite3_errmsg(db)); sqlite3_close(db); exit(EXIT_FAILURE); } Böylece biz hata durumlarını aşağıdaki gibi ele alabiliriz: if (sqlite3_open("studentxxx.db", &db) != SQLITE_OK) sqlite3_exit("sqlite3_open", db); Aşağıda bir örnek program verilmiştir: * Örnek 1, #include #include #include "sqlite3.h" void sqlite3_exit(const char *msg, sqlite3 *db); int main(void) { sqlite3 *db; if (sqlite3_open("student.db", &db) != SQLITE_OK) sqlite3_exit("sqlite3_open", db); printf("success...\n"); sqlite3_close(db); return 0; } void sqlite3_exit(const char *msg, sqlite3 *db) { fprintf(stderr, "%s failed => %s\n", msg, sqlite3_errmsg(db)); sqlite3_close(db); exit(EXIT_FAILURE); } SQLite'a bir SQL cümlesi göndermek için sqlite3_exec fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: int sqlite3_exec( sqlite3 *db, /* An open database */ const char *sql, /* SQL to be evaluated */ int (*callback)(void*,int,char**,char**), /* Callback function */ void *param, /* 1st argument to callback */ char **errmsg /* Error msg written here */ ); Fonskiyonun birinci parametresi sqlite3_open fonksiyonundan elde edilen handle değeridir. İkinci parametre sql cümlesinin yazısını alır. Üçüncü parametre işlemden sonra çağrılacak “callback” fonksiyonun adresini almaktadır. Bu parametre NULL geçilebilir. Dördüncü parametre bu “callback” fonksiyona geçirilecek argümanı belirtir. Bu parametre de NULL geçilebilir. Son parametre ise char * türünden bir göstericinin adresini almaktadır. Hata drumunda hata mesajının adresi bu göstericiye yerleştirilir. Bu parametre NULL olarak da geçilebilir. Fonksiyonun geri dönüş değeri işlemin başarısını belirtir. Fonksiyon başarılıysa SQLITE_OK değerine geri dönmektedir. Bu durumda biz hata mesajını yazdırdıktan sonra sqlite3_free fonksiyonu ile tahsis edilen alanı serbest bırakabiliriz. Örneğin: if (sqlite3_exec(db, "INSERT INTO student_info(student_name, student_no) VALUES('Rasim Öztekin', 367)", NULL, NULL, NULL) != SQLITE_OK) sqlite3_exit("sqlite3_exec", db); Aşağıdaki örnekte student_info veri tabanına sqlite3_exec fonksiyonu ile bir kayıt eklenmiştir. * Örnek 1, #include #include #include "sqlite3.h" void sqlite3_exit(const char *msg, sqlite3 *db); int main(void) { sqlite3 *db; if (sqlite3_open("student.db", &db) != SQLITE_OK) sqlite3_exit("sqlite3_open", db); if (sqlite3_exec(db, "INSERT INTO student_info(student_name, student_no) VALUES('Rasim Öztekin', 367)", NULL, NULL, NULL) != SQLITE_OK) sqlite3_exit("sqlite3_exec", db); printf("Success...\n"); sqlite3_close(db); return 0; } void sqlite3_exit(const char *msg, sqlite3 *db) { fprintf(stderr, "%s failed => %s\n", msg, sqlite3_errmsg(db)); sqlite3_close(db); exit(EXIT_FAILURE); } Veritabanından kayıtların elde edilmesi biraz daha ayrıntılı bir konudur. İstenilen kayıtların elde edilmesi için iki yol vardır. Birincisinde önce sqlite3_prepare fonksiyonu ile SQL SELECT cümlesi VTYS’ye gönderilir. Sonra her bir kayıt tek tek sqlite3_step fonksiyonu çağrılarak elde edilir. sqlite3_prepare fonksiyonundan elde edilen kayıtların bir liste oluşturduğunu sqlite3_step fonksiyonunun da listede sonraki kayıta geçtiğini düşünebilirsiniz. Yani adeta sqlite3_step fonksiyonu imleci bir sonraki kayda konumlandırıyormuş gibidir. O andaki kayıtın sütun elemanları sqlite3_column_xxx fonksiyonlarıyla elde edilebilir. Burada xxx o sütunun türünü belirtmektedir. sqlite3_prepare fonksiyonunun prototipi şöyledir: int sqlite3_prepare( sqlite3 *db, /* Database handle */ const char *zSql, /* SQL statement, UTF-8 encoded */ int nByte, /* Maximum length of zSql in bytes. */ sqlite3_stmt **ppStmt, /* OUT: Statement handle */ const char **pzTail /* OUT: Pointer to unused portion of zSql */ ); Fonksiyonun birinci parametresi sqlite3_open fonksiyonundan elde edilen handle değeridir. İkinci parametre SELECT cümlesini belirtir. Üçüncü parametre ikinci parametredeki SELECT cümlesine ilişkin yazının uzunluğunu belirtir. Bu parametre negatif değer geçilirse (örneğin -1) bu yazı null karaktere kadar ele alınır. Fonksiyonun dördüncü parametresi sqlite3_stmt türünden bir yapı göstericisinin adresini almaktadır. Bu da bir handle değeri gibidir. Kayıtlar elde edilirken bu handle değeri kullanılmaktadır. Son parametre NULL geçilebilir. Fonksiyon başarı durumunda SQLITE_OK değerine geri dönmektedir. Fonksiyon başarılı olduğunda imleç ilk kayıdın bir gerisini göstermektedir. Yani işleme önce bir kez sqlite3_step çağrısı yaparak başlamak gerekir. Her sqlite3_step çağrısı select edilen kayıtlardan bir sonrasına konumlanma sağlar. sqlite3_step fonksiyonunun prototipi şöyledir: int sqlite3_step(sqlite3_stmt*); Fonksiyonun parametresi sqlite3_prepare fonksiyonundan alınan handle değeridir. Son kayda erişildikten sonra sqlite3_step fonksiyonu SQLITE_DONE değerine geri dönmektedir. O halde bu yöntemde önce bir kez sqlite3_prepare fonksiyonu çağrılır. Sonra bir döngü içerisinde sqlite2_step çağrıları yapılır. İmleç konumlandırıldıktan sonra sütun değerleri sqlite3_column_xxx fonksiyonlarıyla elde edilmektedir. Her sütun türü için ayrı bir fonksiyon vardır. Bu fonksiyonlardan bazılarının prototipleri aşağıda verilmiştir: const void *sqlite3_column_blob(sqlite3_stmt*, int iCol); int sqlite3_column_bytes(sqlite3_stmt*, int iCol); int sqlite3_column_bytes16(sqlite3_stmt*, int iCol); double sqlite3_column_double(sqlite3_stmt*, int iCol); int sqlite3_column_int(sqlite3_stmt*, int iCol); sqlite3_int64 sqlite3_column_int64(sqlite3_stmt*, int iCol); const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol); const void *sqlite3_column_text16(sqlite3_stmt*, int iCol); int sqlite3_column_type(sqlite3_stmt*, int iCol); sqlite3_value *sqlite3_column_value(sqlite3_stmt*, int iCol); Bu işlemler bittikten sonra sqlite3_finalize fonksiyonu çağrılmalıdır. Fonksiyonun prototipi şöyledir: int sqlite3_finalize(sqlite3_stmt *pStmt); Fonksiyon başarı durumunda SQLITE_OK değeri ile geri dönmektedir. * Örnek 1, Aşağıdaki örnekte komut satırında bir menü çıkartılmış ve seçilen seçeneğe göre uygun işlemler yapılmıştır. Menü aşağıdaki gibidir: 1) Kayıt Ekle 2) Kayıt Sil 3) Kayıt Bul 4) Çıkış Seçiminiz: Örnekteki "student.db" veritabanı yoksa yaratılmaktadır varsa olan veritabanı açılmaktadır. Eğer veritabanı yoksa aşağıdaki SQL komutuyla tablo yaratılmıştır: "CREATE TABLE IF NOT EXISTS student_info(student_id INTEGER PRIMARY KEY AUTOINCREMENT, student_name VARCHAR(64), student_no INTEGER );" Görüldüğü gibi student_infıo tablosunda üç sütun vardır. Sütunlardan biri otomatik artırımlı PRIMARY KEY sütunudur. #include #include #include #include #include "sqlite3.h" int disp_menu(void); void clear_stdin(void); void add_record(sqlite3 *db); void del_record(sqlite3 *db); void find_record(sqlite3 *db); void sqlite3_exit(const char *msg, sqlite3 *db); int main(void) { sqlite3 *db; int option; setlocale(LC_ALL, "tr_TR.UTF-8"); if (sqlite3_open("student.db", &db) != SQLITE_OK) sqlite3_exit("sqlite3_open", db); if (sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS student_info(" "student_id INTEGER PRIMARY KEY AUTOINCREMENT, " "student_name VARCHAR(64), " "student_no INTEGER);", NULL, NULL, NULL != SQLITE_OK)) sqlite3_exit("sqlite3_exec", db); for (;;) { if ((option = disp_menu()) == -1) { printf("Geçersiz seçenek!..\n"); clear_stdin(); continue; } switch (option) { case 1: add_record(db); break; case 2: del_record(db); break; case 3: find_record(db); break; case 4: goto EXIT; default: printf("Geçersiz seçenek!..\n"); } } EXIT: sqlite3_close(db); return 0; } int disp_menu(void) { int option; printf("1) Kayıt Ekle\n"); printf("2) Kayıt Sil\n"); printf("3) Kayıt Bul\n"); printf("4) Çıkış\n"); printf("Seçiminiz: "); fflush(stdout); if (scanf("%d", &option) != 1) return -1; clear_stdin(); return option; } void clear_stdin(void) { while (getchar() != '\n') ; } void add_record(sqlite3 *db) { char name[64]; char sql[1024]; char *str; int no; printf("Adı Soyadı:"); fflush(stdout); fgets(name, 64, stdin); if ((str = strchr(name, '\n')) != NULL) *str = '\0'; printf("No:"); fflush(stdout); scanf("%d", &no); clear_stdin(); sprintf(sql, "INSERT INTO student_info(student_name, student_no) VALUES('%s', %d);", name, no); if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) fprintf(stderr, "cannot add record!..\n"); } void del_record(sqlite3 *db) { char condition[1024]; char sql[4096]; char *str; int no; printf("Koşul:"); fflush(stdout); fgets(condition, 1024, stdin); if ((str = strchr(condition, '\n')) != NULL) *str = '\0'; sprintf(sql, "DELETE FROM student_info WHERE %s;", condition); if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) fprintf(stderr, "cannot delete record!..\n"); } void find_record(sqlite3 *db) { char condition[1024]; char sql[4096]; char *str; int no; sqlite3_stmt *stmt; unsigned char *name; printf("Koşul:"); fflush(stdout); fgets(condition, 1024, stdin); if ((str = strchr(condition, '\n')) != NULL) *str = '\0'; printf("\n"); if (*str == '\0') strcpy(sql, "SELECT student_name, student_no FROM student_info"); else sprintf(sql, "SELECT student_name, student_no FROM student_info WHERE %s;", condition); if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK) { fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db)); return; } while (sqlite3_step(stmt) != SQLITE_DONE) { name = sqlite3_column_text(stmt, 0); no = sqlite3_column_int(stmt, 1); printf("%s, %d\n", name, no); } sqlite3_finalize(stmt); printf("\n"); } void sqlite3_exit(const char *msg, sqlite3 *db) { fprintf(stderr, "%s failed => %s\n", msg, sqlite3_errmsg(db)); sqlite3_close(db); exit(EXIT_FAILURE); } >> "MySQL" : Şimdi de yukarıdaki işlemlerin benzerlerinin MySQL'de nasıl yapılacağını görelim. MySQL'in de C için API'leri vardır. MySQL VTYS’si ile işlemler MySQL’in gerekli kütüphanelerinin client tarafta kurulması gerekir. Bunun için Windows’ta, macOS sistemlerinde ve Linux’ta “MySQL C++ Connector” denilen kurulum yapılabilir. Bu paket aşağıdaki bağlantıdan indirilebilir: https://downloads.mysql.com/archives/c-c/ Bu kurulum yapıldığında tipik olarak gerekli olan kütüphaneler ve include dosyaları "C:\Program Files\MySQL\MySQL Connector C 6.1" gibi bir dizine kurulacaktır. Debian türevi (Ubuntu, Mint vs.) sistemlerde aşağıdaki apt-get komutu bu paketin indirilerek kurulmasını sağlamaktadır: $ sudo apt-get install libmysqlclient-dev Windows ortamında VisualStudio IDE'sinde çalışıyorsanız projenizde "Additional Include Drectories" elemanında MySQL Connector'ü kurduğunuz dizindeki include dizinini burada belirtmelisiniz. Ayrıca Windows'ta link işlemi için "libmysql.lib" import kütüphanesinin de projede belirtilmesi gerekmektedir. Ancak programın çalışabilmesi için "libmysql.dll" dosyasının ya sistem tarafından bakılan dizinlerin birinde olması ya da PATH çevre değişkeni ile belirtilen dizinlerden birinde olması gerekmektedir. Linux'ta MySQL include dosyaları "/usr/include/mysql" dizini içerisindedir. Dolayısıyla include işlemi biçiminde yapılmalıdır. Linux'ta ayrıca "libmysqlclient" kütüphanesinin de bağlama işlemine dahil edilmesi gerekmektedir. Derleme işlemini şöyle yapmalısınız: $ gcc -o sample sample.c -lmysqlclient Bir MYSQL C programda ilk yapılacak şey mysql_init fonksiyonu çağırarak bir handle elde etmektir: MYSQL *MySQL_init(MYSQL *MySQL); Bu fonksiyon parametre olarak bizden MYSQL türünden bir nesnenin adresini ister onun içini doldurur. Eğer parametre NULL girilirse fonksiyon bu nesneyi kendisi tahsis edip bize aresini verecektir. Fonksiyon başarısız olabilir. Başarısızlık durumunda NULL adrese geri döner. Örneğin: MYSQL *db; if ((db = mysql_init(NULL)) == NULL) { fprintf(stderr, "MySQL_init failed\n"); exit(EXIT_FAILURE); } Aşağıda örnek bir kullanım verilmiştir: * Örnek 1, #include #include #include int main(void) { MYSQL *db; if ((db = mysql_init(NULL)) == NULL) { fprintf(stderr, "mysqlL_init failed\n"); exit(EXIT_FAILURE); } printf("Ok\n"); return 0; } MYSQL * türünden handle elde ediltiktan sonra artık "IP adresi", "port numarası", "kullanıcı adı", "parola" ve bağlanılacak veritabını velirtilerek bağlantı mysl_real_connect fonksiyonuyla sağlanır. Fonksiyonun prototipi şöyledir: MYSQL *mysql_real_connect(MYSQL *MySQL, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag); Fonksiyonun birinci parametresi mysql_init fonksiyonundan elde edilmiş olan handle değeridir. İkinci parametre host'un IP adresini üçüncü parametre MySQL'deki kullanıcı ismini almaktadır. MySQL'i ilk kurduğunuzda tüm yetkilere sahip bir "root" kullanıcısı bulunmaktadır. Bu kullanıcı tüm veritabanlarına erişebilmektedir. Tabii siz isterseniz kısıtlı kullanılarda yaratabilirsiniz. Fonksiyonun dördüncü parametresi kullanıcıya ilişkin parolayı belirtmektedir. "root" kullanıcısının parolası kurulum sırasında belirlenmektedir. Beşinci parametre kullanılacak veritabaınının ismini almaktadır. Altıncı parametre port numarasını belirtmektedir. MySQL Server programlarının kullandığı default port numarası 3306'dır. Son iki parametre NULL ve 0 biçiminde geçilebilir. Örneğin: MYSQL *db; if ((db = mysql_init(NULL)) == NULL) { fprintf(stderr, "mysql_init failed!..\n"); exit(EXIT_FAILURE); } if (mysql_real_connect(db, "localhost", "root", "maviay", "student_info", 3306, NULL, 0) == NULL) exit_err("mysql_real_connect failed", NULL); MySQL ile çlışırken hata durumlarında bağlantıyı kapatıp prosesi sonlandırmak için aşağıdaki gibi bir fonksiyondan faydalanabiliriz: void exit_err(const char *msg, MYSQL *db) { fprintf(stderr, "%s: %s\n", msg, mysql_error(db)); mysql_close(db); exit(EXIT_FAILURE); } mysql_close fonksiyonuna NULL adres geçilirse close işlemi yapılmaz ama mysql_init fonkisyonuyla tahsis edilmiş olan handle alanu boşaltılır. Server'a bağlanırken birkaç problem ortaya çıkabilir. Server default SSL kullanıyor olabilir. Bu durumda SSL konfigürasyonunu yapmadıysanız bağlantı sırasında sorun oluşabilir. Bağlanırken SSL'i pasif hale getirmek için (disable etmek için) iki yöntem kullanılabilir. Birincisi "mysql.ini" ya da "my.cnf" dosyasına aşağıdaki satırlar girilerek SSL kullanımı devra dışı bırakılabilir: [mysqld] ssl=0 skip_ssl Windows sistemlerinde "mysql.ini" dosyası "C:\ProgramData\MySQL\MySQL Server 8.0" dizin içerisinde bulunmaktadır. Linux sistemlerinde "my.cnf" dosyası "/etc/mysql" dizini içerisindedir. Bu dosyaları edit etmek için editörünüzü "Administrator" ya da "sudo" hakkıya açmalısınız. İkinci yöntem programa özgü bir biçimde SSL'i mysql_options fonksiyonu ile devre dışı bırakmaktır. Bu işlem şöyle yapıabilir: int ssl_mode = SSL_MODE_DISABLED; if (mysql_options(db, MYSQL_OPT_SSL_MODE, &ssl_mode)) exit_err("mysql_options failed", db); Eskiden MySQL server programı default olarak uzak bağlantıları kabul ediyordu. Sonra default durumda uzak bağlantılara izin verilmemeye başlandı. Yani siz yerel ağınızda bile olsa başka bir makinedeki MySQL Server programına IP adresi ve port numarası belirterek bağlanamayabilirsiniz. Uzak bağlantılara izin vermek için konfügrasyonda bazı ayarlamaların yapılması gerekmektedir. SQL cümlesini server’a gönderip işletmek için MySQL_query fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: int mysql_query(MYSQL *MySQL, const char *stmt_str); Fonksiyonun birinci parametresi handle değerini ikinci paarametresi SQL komut yazısını alır. Fonksiyon başarı durumunda sıfır başarısızlık durumunda sıfır dışı bir değere geri döner. Örneğin: if (mysql_query(db, u8"INSERT INTO student_info(student_name, student_no) VALUES('Ali Eker', 345)") != 0) exit_err("mysql_query", db); Aşağıda program bir bütün olarak verilmiştir. * Örnek 1, #include #include #include void exit_err(const char *msg, MYSQL *db); int main(void) { MYSQL *db; int ssl_mode; if ((db = mysql_init(NULL)) == NULL) exit_err("mysql_init failed", NULL); ssl_mode = SSL_MODE_DISABLED; if (mysql_options(db, MYSQL_OPT_SSL_MODE, &ssl_mode)) exit_err("mysql_options failed", db); if (mysql_real_connect(db, "localhost", "root", "kaanaslan1966", "student", 3306, NULL, 0) == NULL) exit_err("mysql_real_connect failed", db); if (mysql_query(db, u8"INSERT INTO student_info(student_name, student_no, student_school_id) VALUES('Ali Eker', 122, 1)") != 0) exit_err("mysql_query", db); printf("Ok\n"); mysql_close(db); return 0; } void exit_err(const char *msg, MYSQL *db) { fprintf(stderr, "%s: %s\n", msg, mysql_error(db)); if (db != NULL) mysql_close(db); exit(EXIT_FAILURE); } MySQL server'ı Linux'ta kullanıyorsanız tüm ayarlar default durumda Unicode UTF-8 enconding'ine ilişkindir. Linux ortamında uzun süredir default encoding zaten Unicode UTF-8 olduğu için bu ortamda çalışırken bir encoding sorunu ortaya çıkmayacaktır. Ancak Windows sistemlerinde MySQL server kurulduğunda default encoding Unicode UTF-8 yerine Microsoft'un 1254 Code Page'i olabilir. MySQL'de encoding çeşitli düzeylerde değiştirilebilmektedir. Örneğin: -> Server Düzeyinde -> Veritabanı Düzeyinde -> Tablo Düzeyinde -> Tablonun Sütunu Düzeyinde Aşağıdaki belirleme yukarıda yapılan belirlemeyi devre dışı bırakmaktadır. Ayrıca MySQL'de client ptogramlar da server ile bağlandığında server'dan belli bir encoding kullanmasını isteyebilmektedir. Yani server ayarları yapılmış olsa bile client programların da encoding isteklerinin uygun olması gerekebilmektedir. Client'ın default encoding davranış da aslında server ayarlarından belilenebilmektedir. Client için bu ayar uygun değile client program bağlantıdan sonra mysql_options fonksiyonu ile bunu aşağıdaki gibi değiştirebilir: if (mysql_options(db, MYSQL_SET_CHARSET_NAME, "utf8") != 0) exit_err("mysql_options_failed", db); Belli koşulu sağlayan kayıtların ele geçirilmesi için başka bir deyişle SELECT cümlesi ile seçilen kayıtların elde edilmesi için birkaç yol vardır. Bunun için önce SELECT cümlesi yine sql_query fonksiyonuyla uygulanır. Sonra select edilen kayıtların elde edilmesi için şu işlemler yaplır: -> mysql_store_result fonksiyonu çağrılarak bir “result handle değeri” elde edilir: MYSQL_RES *mysql_store_result(MYSQL *MySQL); Fonksiyon parametre olarak bizden mysql_init ile elde edilen handle değerini alır ve bize kayıtları elde etmemiz için gereken MYSQL_RES * türünden bir handle değeri verir. Fonksiyon başarısız olursa NULL adrese geri dönmektedir. 2) Kayıtların tek tek ele geçirilmesi için mysql_fetch_row fonksiyonu bir döngü içerisinde çağrılır. Bu fonksiyonun prototipi şöyledir: MYSQL_ROW mysql_fetch_row(MYSQL_RES *result); Fonksiyon mysql_store_result fonksiyonundan elde edilen handle değerini alır ve MYSQL_ROW türüyle geri döner. Bu tür aslında char ** biçiminde typedef edilmiştir. Yani char türünden göstericileri tutan dizinin adresini belirtir. İşte bu fonksiyon NULL adres döndürene kadar döngü içerisinde ilerlenir. Arık kayıtlara ilişkin sütun bilgilerine MYSQL_ROW türünden göstericiye sütun numarası indeks yapılarak erişilir. Ancak bu türden erişim bize tüm sütunları yazı gibi vermektedir. Kayıtlar elde ediltikten sonra mysql_store_result ile elde edilen alan mysql_free_result fonksiyonu ile boşaltılmalıdır. Fonksiyonun parametrik yapısı şöyledir: void mysql_free_result(MYSQL_RES *result); Örneğin: if (mysql_query(db, "SELECT student_name, student_no FROM student_info") != 0) exit_err("mysql_query", db); if ((res = mysql_store_result(db)) == NULL) exit_err("mysql_store_result", db); while ((row = mysql_fetch_row(res)) != NULL) printf("%s, %s\n", row[0], row[1]); mysql_free_result(res); Burada önce SELECT cümlesi server'a gönderilmiş, sonra mysql_store_result fonksiyonu ile sonuçlar alınıp satırlar da mysql_fetch_row çağrıları ile tek tek elde edilmiştir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include #include void exit_err(const char *msg, MYSQL *db); int main(void) { MYSQL *db; int ssl_mode; MYSQL_RES *res; MYSQL_ROW row; if ((db = mysql_init(NULL)) == NULL) exit_err("mysql_init failed", NULL); if (mysql_options(db, MYSQL_SET_CHARSET_NAME, "utf8") != 0) exit_err("mysql_options_failed", db); ssl_mode = SSL_MODE_DISABLED; if (mysql_options(db, MYSQL_OPT_SSL_MODE, &ssl_mode) != 0) exit_err("mysql_options failed", db); if (mysql_real_connect(db, "localhost", "root", "kaanaslan1966", "student", 3306, NULL, 0) == NULL) exit_err("mysql_real_connect failed", db); if (mysql_query(db, "SELECT student_name, student_no FROM student_info") != 0) exit_err("mysql_query", db); if ((res = mysql_store_result(db)) == NULL) exit_err("mysql_store_result", db); while ((row = mysql_fetch_row(res)) != NULL) printf("%s, %s\n", row[0], row[1]); mysql_free_result(res); mysql_close(db); return 0; } void exit_err(const char *msg, MYSQL *db) { fprintf(stderr, "%s: %s\n", msg, mysql_error(db)); if (db != NULL) mysql_close(db); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (205_23_03_2025) & (206_11_04_2025) & (207_18_04_2025) & (208_20_04_2025) > GUI Çalışma Modelinin Temelleri : Eskiden monitörler yalnızca kalıp karakter basabiliyordu. Yani pixel temelinde bir grafik gösterim yoktu. Eğer ekranda görüntülenecek en küçük bir karakter ise bu çalıma moduna "text mode" ya da "console mode" denilmektedir. Grafik mod çalışmada ekranda kontrol edilebilecek en küçük birim bir karakter değil bir pixel'dir. (Pixel sözcüğü "picture element") sözcüklerinden kısaltma yapılarak uydurulmuştur. Grafik modda her şey küçük bir nokta olan pixel'lerin bir araya gelmesiyle oluşturulmaktadır. Bu nedenle text modda yalnızca yazılar görüntülenirken grafik modda resimler de görüntülenebilmektedir. Grafik modda her pixel Kırmızı, Yeşil ve Mavinin (kısaca RGB denilmektedir) tonal birleşimleriyle oluşturulmaktadır. Kırmızının, yeşilin ve mavinin 256 farklı tonal birleşimleri vardır. Böylece bukünkü modern grafik ekranlarda her pixel diğerlerin bağımsız olarak 2^24 (yaklaşık 16 milyon) (2^8 * 2^8 * 2^8) renkten biriyle boyanabilmektedir. İşte aslında grafik modda bir resim pixel'lerden oluşmaktadır. Her pixel uygun renge boyanınca biz onu bir resim gibi algılarız. Monitörler çeşitli çözünürlük modlarına da sahiptir. Bugün kullanılan en yaygın çözünürlük 1920x1080 (buna HD çözünürlük de denilmektedir) çözünürlüktür. Monitör bu çözünürlüğü kullandığında ekran adeta 1920x1080'lik bir pixel matrisi gibidir. Monitörler tamamen text modlarıda hala desteklemektedir. Monitör text moda geçirildiğinde artık pixel gösteremez yalnızca kalıp karakterleri gösterebilir. Eskiden monitörler hep text modda çalışırdı. Bu nedenle 80'li yıllarda bilgisayar ekranında film seyredemiyorduk, resim görüntüleyemiyorduk. (O zamanlarda monitörler ve grafik kartları grafik modlara yavaş yavaş sahip olsa da çok düşük çözünürlük sunuyordu.) Monitörün bir bölümü text bir bölümü grafik modda olamaz. Bu nedenle biz grafik ekranda çalıştığımız bilgisayarlarda bir terminal penceresi açtığımızda o pencere text modda olmaz. O pencere yalnızca text modu taklit eden bir emülasyon oluşturmaktadır. En basit, hızlı ve geleneksel çalışma modu text mod olduğu için Windows ve Linux gibi işletim sistemleri grafik arayüzünü kullanıyor olsa bile böyle text mod emülasyonunu sağlamaktadır. Pekiyi mademki GUI çalışma çok zengin olanaklar sunmaktadır neden o zaman hala konsol uygulamaları kullanılıyor? İşte GUI programların yazılması oldukça zordur. GUI programların yazılmasını kolaylaştırmak için pek çok yüksek seviyeli kütüphaneler oluşturulmuştur. Oysa text modda klasik konsol çalışma modeli oldukça basittir. Bazı sistemler ise hiç GUI arayüzüne sahip olmayabilmektedir. Örneğin UNIX/Linux sistemlerinde hala klasik konsol çalılma modeli çok yaygın biçimde kullanılmaktadır. (UNIX/Linux sistemleri özellikle sunucularda kullanıldığı için ve sunucuların grafik arayüze sahip olması onların hızını ve kaynak kullanımını yavaşlatacağı için bu sistemlerde klasik konsol çalışma modeli hala en yaygın modeldir.) GUI çalışma modeli ile klasik konsol çalışma modelini karşılaştırarısak şunları söyleyebiliriz: -> GUI çalışma modelinin uygulanması için bilgisayar sisteminin bazı gelişmiş özelliklere sahip olması gerekir. Halbuki konsol çalışma modeli DOS gibi basit işletim sistemlerinde ve onların çalıştığı eski donanımlarda bile oldukça verimli bir biçimde uygulanabilmektedir. -> GUI çalışma modelini kullanarak program yazmak oldukça zordur. Programcılar işlerini kolaylaştırmak için yüksek sebviyeli GUI kütüphaneler kullanmaktadır. -> GUI çalışma modeli konsol çalışma modeline göre yavaştır ve çok daha fazla sistem kaynağına gereksinimn duyar. -> GUI çalışma modeli modern grafiksel bir girdi ve çıktı ortamı sunmaktadır. Halbuki konsol çalışma ortamında her şey karakterlerin kalıp olarak konsol ekranına yazılmasıyla oluşturulmaktadır. Windows’un çekirdek (kernel) ile entegre edilmiş bir GUI alt sistemi vardır. Başka bir deyişle Windows’ta pencereli çalışma başka bir katman tarafından değil doğrudan işletim sisteminin çekirdeği tarafından sağlanmaktadır. Windows’u GUI alt sistemi olmadan kullanmak mümkün olsa da uygulamada çok zordur, Windows'ta GUI arayüz olmadan çalışma anlamlı değildir. UNIX/Linux sistemlerinde grafik arayüz çekirdeğin üzerine oturtulan ve ismine X11 (ya da XWindow) denilen bir alt sistem tarafından sağlanmaktadır. Yani örneğin Linux’un çekirdeğinin kaynak kodlarında pencere kavramına ilişkin hiçbir şey yoktur. Ancak Windows’ta vardır. Zaten Linux sistemlerinde doğal çalışma grafik arayüz ile değil text ekrandaki konsol arayüzü sağlanmaktadır. Server olarak kullanılan Linux sistemlerinde de genellikle sistemi yavaşlattığı gerekçesiyle grafik arayüz kullanılmamaktadır. Son yıllarda UNIX/Linux dünyasında klasik X11 GUI alt sistemine bir alternatif olarak "Wayland" isimli yeni bir alt sistem de tasarlanmıştır. Wayland alt sistemi X11'e göre daha modern ve hızlı bir tasarıma sahip olmakla birlikte henüz yaygınlaşmamıştır. X11 grafik sistemi client-server tarzda çalışmaktadır. Yani sanki X11 bir server program gibidir, pencere açmak ve pencereler üzerinde işlemler yapmak isteyen programlar da client programlar gibidir. X11 sisteminde işlem yapabilmek için oluşturulmuş temel kütüphaneye Xlib denilmektedir. Xlib'i X11’in API kütüphanesi olarak düşünebiliriz. Son yıllarda Xlib’in XCB isimli daha modern bir versiyonu da oluşturulmuştur. Xlib ve XCB temelde C Programlama Dilinden kullanılmak için tasarlanmıştır. Ancak bu kütüphaneler başka dillerden de kullanılabilmektedir. Grafik arayüze sahip pencereli sistemlerde genel olarak "mesaj tabanlı (message driven)" ya da "olay tabanlı (event driven)" denilen çalışma modeli kullanılmaktadır. Mesaj tabanlı çalışma modelinin ayrıntıları sistemden sisteme değişebilmekle birlikte burada biz her sistemde geçerli olan bazı temel bilgileri vermekle yetineceğiz. Mesaj tabanlı programlama modelinde klavye ve fare gibi aygıtlarda oluşan girdileri programcı kendisi almaya çalışmaz. klavye gibi, fare gibi girdi aygıtlarını işletim sisteminin (ya da GUI alt sistemin) kendisi izler. Oluşan girdi olayı hangi pencereye ilişkinse işletim sistemi ya da GUI alt sistem, bu girdi olayını “mesaj” adı altında bir yapıya dönüştürerek o pencerenin ilişkin olduğu (yani o pencereyi yaratan) programın “mesaj kuyruğu (message queue)” denilen bir kuyuk sistemine yerleştirir. Mesaj kuyruğu içerisinde mesajların bulunduğu FIFO prensibiyle çalışan bir kuyruk veri yapısıdır. Sistemin daha iyi anlaşılması için süreci maddeler halinde özetlemek istiyoruz: -> Her programın (thread'li sistemlerde her thread’in) “mesaj kuyruğu” denilen bir kuyruk veri yapısı vardır. Mesaj kuyruğu mesajlardan oluşmaktadır. -> İşletim sistemi ya da GUI alt sistem gerçekleşen girdi olaylarını “mesaj (message)” adı altında bir yapı formatına dönüşürmekte ve bunu pencerenin ilişkin olduğu programın (ya da thread’in) mesaj kuyruğuna eklemektedir. -> Mesajlar ilgili olayı betimleyen ve ona ilişkin bazı bilgileri barındıran yapı (structure) nesleridir. Örneğin Windows’ta mesajlar MSG isimli bir yapıyla temsil edilmişleridir. Bu yapının elemanlarında mesajın ne mesajı olduğu (yani neden gönderildiği) ve mesajın gönderilmesine neden olan olaya ilişkin bazı parametrik bilgiler bulunur. Görüldüğü gibi GUI programlama modelinde girdileri programcı elde etmeye çalışmamaktadır. Girdileri bizzat işletim sisteminin kendisi ya da GUI alt sistemi elde edip programcıya mesaj adı altında iletmektedir. GUI programlama modelinde işletim sisteminin (ya da GUI alt sistemin) oluşan mesajı ilgili programın (ya da thread’in) mesaj kuyruğuna eklemenin dışında başka bir sorumluluğu yoktur. Mesajların kuyruktan alınarak işlenmesi ilgili programın sorumluluğundadır. Böylece GUI programcısının mesaj kuyruğuna bakarak sıradaki mesajı alması ve ne olmuşsa ona uygun işlemleri yapması gerekir. Bu modelde programcı kodunu şöyle düzenler: "Bir döngü içerisinde sıradaki mesajı kuyruktan al, onun neden gönderildiğini belirle, uygun işlemleri yap, kuyrukta mesaj yoksa da blokede bekle”. İşte GUI programlarındaki mesaj kuyruğundan mesajı alıp işleyen döngüye mesaj döngüsü (message loop) denilmektedir. Bir GUI programının işleyişini tipik akışı aşağıdaki gibi bir kodla temsil edebiliriz: int main(void) { for (;;) { if (mesaj oencerinin x tuşuna basma mesajı mı) break; } return 0; } Bu temsili koddan da görüldüğü gibi tipik bir GUI programında programcı bir döngü içerisinde mesaj kuyruğundan sıradaki mesajı alır ve onu işler. Mesajın işlenmesi ise “ne olmuş ve ben buna karşı ne yapmalıyım?” biçiminde oluşturulmuş olan kodlarla yapılmaktadır. Pekiyi bir GUI programı nasıl sonlanmaktadır? İşte pencerenin sağındaki (bazı sistemlerde solundaki) X simgesine kullanıcı tıkladığında işletim sistemi ya da GUI alt sistem bunu da bir mesaj olarak o pencerenin ilişkin olduğu prosesin (ya da thread’in) mesaj kuyruğuna bırakır. Programcı da kuyruktan bu mesajı alarak mesaj döngüsünden çıkar ve program sonlanır. GUI ortamımız (framewok) ister .NET, ister Java, ister MFC olsun, isterse Qt olsun, işletim sisteminin ya da GUI alt sistemin çalışması hep burada ele açıklandığı gibidir. Yani örneğin biz .NET'te ya da Java'da işlemlerin sanki başka biçimlerde yapıldığını sanabiliriz. Aslında işlemler bu ortamlar tarafından aşağı seviyede yine burada anlatıldığı gibi yapılmaktadır. Bu ortamlar (frameworks) ya da kütüphaneler çeşitli yükleri üzerimizden alarak bize daha rahat bir çalışma modeli sunarlar. Ayrıca şunu da belirtmek istiyoruz: GUI programlama modeli özellikle nesne yönelimli programlama modeline çok uygun düşmektedir. Bu nedenle bu konuda kullanılan kütüphanelerin büyük bölümü sınıflar biçiminde nesne yönelimli diller için oluşturulmuş durumdadır. Örneğin Qt framework C++ ile, .NET Forms ve WPF framework'leri C# ile (ya da diğer nesne yönelimli .NET dilleri ile) kullanılmaktadır. Şimdi GUI programlama modelindeki mesaj kavramını biraz daha açalım. Yukarıda da belirttiğimiz gibi bu modelde programcıyı ilgilendiren çeşitli olaylara “mesaj” denilmektedir. Örneğin klavyeden bir tuşa basılması, pencere üzerinde fare ile tıklanması, pencere içerisinde farenin hareket ettirilmesi gibi olaylar hep birer mesaj oluşturmaktadır. İşletim sistemleri ya da GUI alt sistemler mesajları birbirinden ayırmak için onlara birer numara karşılık getirirler. Örneğin Windows’ta mesaj numaraları WM_XXX biçiminde sembolik sabitlerle kodlanmıştır. Programcılar da konuşurken ya da kod yazarken mesaj numaralarını değil, bu sembolik sabitleri kullanırlar. (Örneğin WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_KEYDOWN gibi) Mesajların numaraları yalnızca gerçekleşen olayın türünü belirtmektedir. Oysa bazı olaylarda gerçekleşen olaya ilişkin bazı bilgiler de söz konusudur. İşte bir mesaja ilişkin o mesaja özgü bazı parametrik bilgiler de işletim sistemi ya da GUI alt sistem tarafından mesajın bir parçası olarak mesajın içerisine kodlanmaktadır. Örneğin Windows’ta biz klavyeden bir tuşa bastığımızda Windows WM_KEYDOWN isimli mesajı programın mesaj kuyruğuna bırakır. Bu mesajı kuyruktan alan programcı mesaj numarasına bakarak klavyenin bir tuşuna basılmış olduğunu anlar. Fakat hangi tuşa basılmıştır? İşte Windows basılan tuşun bilgisini de ayrıca bu mesajın içerisine kodlamaktadır. Örneğin WM_LBUTTONDOWN mesajını Windows farenin sol tuşuna tıklandığında kuyruğa bırakır. Ancak ayrıca basım koordinatını da mesaja ekler. Yani bir mesaj oluştuğunda yalnızca o mesajın hangi tür bir olay yüzünden oluştuğu bilgisini değil aynı zamanda o olayla ilgili bazı bilgileri de kuyruktaki mesajın içerisinden alabilmekteyiz. Windows'ta GUI programları en aşağı seviyede Windows API fonksiyonları kullanılarak yazılmaktadır. Ekrana boş bir pencere çıkartan iskelet bir GUI programı aşağıdaki gibi yazılabilir: #include LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow) { WNDCLASS wndClass; HWND hWnd; MSG message; if (!hPrevInstance) { wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; wndClass.hIcon = LoadIcon(NULL, IDI_QUESTION); wndClass.hbrBackground = GetStockObject(WHITE_BRUSH); wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.lpszMenuName = NULL; wndClass.lpszClassName = "Generic"; wndClass.lpfnWndProc = (WNDPROC)WndProc; if (!RegisterClass(&wndClass)) return -1; } hWnd = CreateWindow("Generic", "Sample Windows", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) return -1; ShowWindow(hWnd, SW_RESTORE); UpdateWindow(hWnd); while (GetMessage(&message, 0, 0, 0)) { TranslateMessage(&message); DispatchMessage(&message); } return message.wParam; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } Windows sistemlerinde iskelet GUI programında şunların sırasıyla yapılması gerelmektedir: -> Önce WNDCLASS türünden bir yapı nesnesi tanımlanıp bunun içi doldurulur ve bu yapı RegisterClass APIO fonksiyonu ile sisteme register ettirilir. Bu WNDCKASS belirlemelerine "pencere sınıfı " denilmektedir. Örneğin: WNDCLASS wndClass; if (!hPrevInstance) { wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; wndClass.hIcon = LoadIcon(NULL, IDI_QUESTION); wndClass.hbrBackground = GetStockObject(WHITE_BRUSH); wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.lpszMenuName = NULL; wndClass.lpszClassName = "Generic"; wndClass.lpfnWndProc = (WNDPROC)WndProc; if (!RegisterClass(&wndClass)) return -1; } -> Programın ana penceresi pencere sınıfı kullanılarak yaratılır: hWnd = CreateWindow("Generic", "Sample Windows", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) return -1; Ana pencere yaratıldıktan sonra pencerenin görünür hale getirilmesi gerekmektedir: ShowWindow(hWnd, SW_RESTORE); UpdateWindow(hWnd); -> Artık program mesaj döngüsüne girmelidir. Mesaj döngüsü kuyruktan sıradaki mesajı alıp bunu işleyen döngüdür. Mesaj döngüsü şöyle oluşturulmaktadır: while (GetMessage(&message, 0, 0, 0)) { TranslateMessage(&message); DispatchMessage(&message); } Burada GetMessage API fonksiyonu mesaj kuruğundan mesajı alır. TranslateMessage klavye mesajları için bazı dönüştürmeleri yapmaktadır. Mesajın işlenmesine yol açan fonksiyon DispatchMessage isimli API fonksiyonudur. Ancak DispatchMessage aslında pencere sınıfında belirtilen fonksiyonun çağrılmasına yol açmaktadır. Örneğimizde bu fonksiyon WndProc ismindedir. Yani DisptachMessage yapıldığında aslında WndProc fonksiyonu çağrılmaktadır. Buna "pencere fonsiyonu" denir. Programcı mesajı bu fonksiyon içerisinde işler. WndProc fonksiyonu şöyle yazılmıştır: LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } Windows'ta kuyruğa bırakılan bazı mesajların mutlaka işlenmesi gerekir. Bu işlem de çok sıkıcı olduğu için DefWindowProc isimli bir fonksiyon bulundurulmuştur. Programcı tarafından işlenmeyen mesajlar DefWindowProc fonksiyonuna verilir. Bu fonksiyon mesaj için gereken bazı default işlemler varsa onu yapar. Programın sonlanması pencerenin X simgesine tıklanarak yapılır. Bu durumda Windows kuyruğa WM_CLOSE isimli mesajı bırakır. DefWindowProc bu mesaj için DestroyWindow fonksiyonunu çağırır. Bu fonksiyon da WM_DESTROY mesajını oluşturur. Bu mesajda programcı PostQuitMessage API fonksiyonunu çağırır. Bu API fonksiyonu da kuyruğa WM_QUIT mesajını bırakır. WM_QUIT mesajını alan GetMessage fonksiyonu 0 ile geri döner. Böylece döngü sonlanır ve program da biter. Aşağıda bu konuya ilişkin kapsayıcı bir örnek verilmiştir: #include LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow) { WNDCLASS wndClass; HWND hWnd; MSG message; if (!hPrevInstance) { wndClass.style = CS_HREDRAW | CS_VREDRAW; wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; wndClass.hIcon = LoadIcon(NULL, IDI_QUESTION); wndClass.hbrBackground = GetStockObject(WHITE_BRUSH); wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); wndClass.lpszMenuName = NULL; wndClass.lpszClassName = "Generic"; wndClass.lpfnWndProc = (WNDPROC)WndProc; if (!RegisterClass(&wndClass)) return -1; } hWnd = CreateWindow("Generic", "Sample Windows", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if (!hWnd) return -1; ShowWindow(hWnd, SW_RESTORE); UpdateWindow(hWnd); while (GetMessage(&message, 0, 0, 0)) { TranslateMessage(&message); DispatchMessage(&message); } return message.wParam; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; case WM_LBUTTONDOWN: MessageBox(hWnd, u8"Farenin sol tuşuna basıldı", "Mesaj", MB_OK); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } Daha önceden de belirttiğmiz gibi UNIX/Linux sistemlerinde XWindow ya da X11 denilen bir katman kullanılmaktadır. Bu katmanın API fonksiyonlarına XLIB ya da bunun modern biçimine XCB denilmektedir. XLib ya da XCB çok aşağı seviyeli bir kütüphanedir. Bu kütüphane kullanılarak GUI elemanlarını oluşturan X-Toolkit ya da kısaca Xt isimli ayrı bir kütüphane vardır. Bu Xt üzerine kurulan Motif gibi başka kütüphaneler de vardır. Ayrıca CLibb ya XCB üzerine kurulmuş olan iki önemli kütüphane de Qt ve GTK (GTK+ da denilmektedir) kütüphanelerdir. Qt kütüphanesi C++ ile yazılmıştır, dolayısıyla Qt için kodlar da C++ ile yazılmaktadır. GTK ise C'de yaaılmıştır. +----------------------------+ | GUI Uygulamaları | +----------------------------+ / | \ / | \ / | \ +--------+ +-----------+ +----------+ | GTK | | Qt | | Motif | +--------+ +-----------+ +----------+ | | | +----------+----+ +--+--+--+ +---+--+ | Xlib / Wayland| |Xlib/XCB| | Xt | +----------+----+ +--+--+--+ +---+--+ | | | | | +--+--+--+ | | |Xlib/XCB| | | +--+--+--+ | | | +-----------+----------------+-----------------+-----------------+ | X Window System (X11) | Wayland (alternatif) | +----------------------------------+-----------------------------+ | Donanım / OS (Çekirdek, GPU) | +----------------------------------------------------------------+ Şimdi de bu kütüphanelerin hiyeararşisini tek tek gösterelim. GTK kütüphanesini şöyle gösterebiliriz: GTK XlIb / Wayland X Window / Wayland Xt kütüphanesini şöyle gösterebiliriz: Xt XlIb X Window Motif kütüphanesini şöyle gösterebiliriz: Xt XlIb X Window Qt kütüphanesini de şöyle gösterebiliriz: Qt XlIb / Wayland X Window / Wayland Bugün artık Xt ve Motif yeni uygulamalar tarafından pek kullanılmamaktadır. Bu nedenle yüksek seviyeli kütüphaneler için önemli iki alternatif GTK ve Qt kütüphaneleridir. GTK kütüphanesi C ile kullanılabilir. Ancak Qt için C++ bilmek gerekir. Xlib ya da XCB kütüphaneleri oldukça düşük seviyeli kütüphanelerdir. Bunlar aslında X Window sistemlerinin aşağı seviyeli API kütüphanesi gibi düşünülebilir. Xlib ile GUI uygulmaları yazmak çok zordur. Çünkü Xlib içerisinde pencere yaratan öğeler olsa da GUI uygulamalarında kullanılan düğmeler (push buttons), listsmele kutuları (listbox), checkbox gibi grafik elemnlar bulunmamaktadır. Eğer C kullanılarak bu GUI elemanlar ile gelişmiş GUI programla roluşturmak istiyorsanız GTK kütüphanesini kullanabilirsiniz. Şimdi de UNIX/Linux sistemlerinde en düşük seviyede Xlib kullana iskelet bir GUI programını yazmaya çalışalım. Bu program ekrana boş bir pencere çıkartacaktır. Bunun öncelikle Xlib kütüphanesinin kurulması gerekmektedir. Bu işlem Debian tabanlı sistemlerde şöyle yapılabilir: $ sudo apt-get install libx11-dev Derleme işlemi sırasında X11 kütüphanesinin "-lX11" seçeneği ile belirtilmesi gerekmektedir. Örneğin: $ gcc -o generic-xlib generic-xlib.c -lX11 Ekrana boş bir pencere çıkartan iskelet GUI programı şöyle yazılabilir: /* generic-xlib.c */ #include #include #include int main(void) { Display *disp; Window w; XEvent e; int scr; disp = XOpenDisplay(NULL); if (disp == NULL) { fprintf(stderr, "Cannot open display\n"); exit(1); } scr = DefaultScreen(disp); w = XCreateSimpleWindow(disp, RootWindow(disp, scr), 10, 10, 100, 100, 1, BlackPixel(disp, scr), WhitePixel(disp, scr)); XSelectInput(disp, w, ExposureMask | KeyPressMask); XMapWindow(disp, w); for (;;) { XNextEvent(disp, &e); if (e.type == KeyPress) break; } XCloseDisplay(disp); return 0; } Burada sırasıyla şu işlemler yapılmıştır: -> XOpenDisplay fonksiyonu XWindow sunucusu ile bağlantı kurmak için kullanılmaktadır. Bu fonksiyon başarı durumunda bize Display türünden bir handle verir. -> Daha sonra biz bu handle’ı vererek bir ekran (screen) nesnesi yaratmamız gerekir. Bu işlem de DeafultScreen fonksiyonuyla yapılmaktadır. Bu fonksiyon bize ilgili ekranı betimleyen int türden bir değer vermektedir. -> Örnek programımızda daha sonra uygulamanın ana penceresi XCreateSimpleWindow fonksiyonuyla yaratılmıştır. Bu fonksiyon bize Window * türünden yaratılan pencereye ilişkin bir handle değeri vermektedir. -> Programda daha sonra mesaj döngüsüne girmeden önce hangi girdi olaylarının izleneceğini belirlemek için XSelectInput fonksiyonu çağrılmıştır. -> Mesaj döngüsünden sıradaki mesaj XNextEvent fonksiyonuyla elde edilmektedir. (Bu fonksiyonu Windows'ta GetMessage API fonksiyonuna benzetebilirsiniz. Bu fonksiyon bize kuyruktaki mesajı XEvent isimli bir yapı olarak verir. Örnek programımızda bir tuşa basıldığında mesaj döngüsünden çıkılmaktadır.) -> Mesaj döngüsünden çıkıldığında XCloseDisplay fonksiyonu ile daha önce alınmış olan ekran geri bırakılmıştır. Tabii ekran yok edildiğinde tüm pencereler de yok edilecektir. Ayrıca program sonlandığında X11 sistemi ile bağlantı da otomatik koparılmaktadır. GTK pek çok ayrıntıya sahip olan C tabanlı bir GUI kütüphanesidir. GTK kürüphanesinin son versiyonu GTK 4'tür. Bu version 2020'de oluşturulmuştur. Ancak önceki versiyon olan GTK 3 halen daha yoğun olarak kullanılmaktadır. GTK 3 Debian tabanlı sistemlerde şöyle kurulabilir: $ sudo apt-get install libgtk-3-dev GTK 4 ise şöyle kurulbailir: $ sudo apt-get install libgtk-4-dev GTK 3 ile GTK 4 birbirine çok benzemekle birlikte tam uyumlu değildir. Biz burada GTK 4 için bazı küçük örnekler vereceğiz. Ekrana boş bir pencere çıkartan iskelet GTK 4 programı şöyle oluşturulabilir: #include void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; // Yeni pencere oluştur window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Sample Window"); gtk_window_set_default_size(GTK_WINDOW(window), 400, 300); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char **argv) { GtkApplication *app; int status; // Uygulamayı nesnesini oluştur app = gtk_application_new("com.generic.application", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); // Uygulamayı çalıştır status = g_application_run(G_APPLICATION(app), argc, argv); // Hafızayı temizle g_object_unref(app); return status; } Bu program şöyle derlenebilir: $ gcc -o generic-gtk generic-gtk.c $(pkg-config --cflags --libs gtk4) Burada $(pkg-config --cflags --libs gtk4) ifadesi pkg-config programının çıktısının komut satırına yerleştirilmesini sağlamaktadır. GTK 4 programlarının derlenmesi için komut satırında çeşitli include dizinlerine ilişkin seçeneklerin ve birden fazla dinamik kütüphanenin devreye sokulması gerelmektedir. Bu $(pkg-config --cflags --libs gtk4) ifadesi aslında gereken komut satırı argümanlarını olutşurmaktadır. Yukarıdaki iskelet programın açıklamasını şöyle yapabiliriz: -> Bir GTK 4 uygulamasında önce bir GtkApplication nesnesinin oluşturulması gerekir. GtkApplication yapısı uygulama ile ilgili çeşitli bilgileri tutmaktadır. Bu işlem iskelet programda şöyle yapılmıştır: app = gtk_application_new("com.generic.application", G_APPLICATION_FLAGS_NONE); -> İskelet programda daha sonra uygulama çalıştırıldığında oluşan "activate" mesajı için activate isimli fonksiyonun çağrılması sağlanmıştır. GTK'da mesaj yerine "sinyal (signal)" sözcüğü kullanılmaktadır: g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); Bu sinyal bağlantısındna sonra artık activate isimli fonksiyon çağrılacaktır. Bizim bu fonksiyon içerisinde programın ana penceresini yaratmamız gerekir. -> activate fonksiyonu şöyle yazılmıştır: void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Sample Window"); gtk_window_set_default_size(GTK_WINDOW(window), 400, 300); gtk_window_present(GTK_WINDOW(window)); } Uygulamanın ana penceresi gtk_application_window_new fonksiyonu ile yaratılmaktadır. Ana pencere diğer pencereler gibi GtkWidget yapısı ile temsil edilmektedir. gtk_window_set_title fonksiyonu yaratılan pencerenin başlık kısmına (caption) çıkacak yazının set edilmesini sağlamaktadır. Ana pencerenin default genişlik ve yüksekliği gtk_window_set_default_size fonksiyonuyla oluşturulmaktadır. Bu işlemlerden sonra iskelet programda gtk_window_present fonksiyonu ile ana pencere görünür hale getirilmiştir. -> Ana pencere yaratıldıktan sonra artık mesaj döngüsü oluşturulmalıdır. GTK 4'te mesaj döngüsü manuel oluşturulmaz. Mesaj döngüsü g_application_run fonksiyonu ile oluşturulmaktadır: status = g_application_run(G_APPLICATION(app), argc, argv); Program hayatını bu fonksiyon içerisinde oluşturulan mesaj döngüsünden geçirmektedir. -> Mesaj döngüsünden yine pencerenin X tuşuna basıldığında çıkılır. İskelet programımızda mesaj döngüsünden çıkıldığında heap'te tahsis edilen çeşitli nesnelerin yok edilmesi için g_object_unref fonksiyonu çağrılmıştır. GTK içerisinde pek çok GUI eleman hazır biçimde bulunmaktadır. Bu GUI elemanların yaratılması için fonksiyonlar vardır. Sinyal mekanizması yoluyla bu GUI elemanlarda birtakım olaylar gerçekletiğinde bizim belirlediğimiz fonksiyonun çağrılması sağlanabilmektedir. Örneğin: button_ok = gtk_button_new_with_label("Ok"); gtk_window_set_child(GTK_WINDOW(window), button_ok); g_signal_connect(button_ok, "clicked", G_CALLBACK(on_button_clicked), window); Burada bir düğme yaaratılmıştı. Bu düğmeye tıklandığında on_button_clicked isimli fonksiyon çağrılacaktır. Aşağıdaki örnekte biz fonksiyon çağrıldığında bir diyalog penceresinin çıkmasını sağladık: void on_button_clicked(GtkButton *button, gpointer user_data) { GtkWindow *parent_window = GTK_WINDOW(user_data); GtkWidget *dialog = gtk_message_dialog_new( parent_window, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "Düğmeye tıkladınız!" ); g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL); gtk_window_present(GTK_WINDOW(dialog)); } Aşağıda kapsayıcı bir örnek verilmiştir: #include void on_button_clicked(GtkButton *button, gpointer user_data) { GtkWindow *parent_window = GTK_WINDOW(user_data); GtkWidget *dialog = gtk_message_dialog_new( parent_window, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "Düğmeye tıkladınız!" ); g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL); gtk_window_present(GTK_WINDOW(dialog)); } void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; GtkWidget *button_ok; GtkWidget *box; window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Sample Window"); gtk_window_set_default_size(GTK_WINDOW(window), 320, 200); button_ok = gtk_button_new_with_label("Ok"); gtk_window_set_child(GTK_WINDOW(window), button_ok); g_signal_connect(button_ok, "clicked", G_CALLBACK(on_button_clicked), window); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char **argv) { GtkApplication *app; int status; app = gtk_application_new("com.generic.application", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; } Bugün çeşitli programalam dillerinden kullanılabilen pek çok GUI kütüphanesi ve GUI ortamları (GUI frameworks) bulunmaktadır. GUI kütüphaneleri ve GUI ortamları zamanla evrim geçirerek bugünkü durumlarına gelmiştir. Günümüzde GUI ortamlarında iki önemli tasarım seçeneği belirginleşmiştir: -> GUI elemanları için otomatik yerleştirme yapan nesnelerin kullanılması. -> GUI arayüzünü mümkün olduğunca koddan ayırma girişimleri. Eskiden kullanıcı arayüzlerindeki düğme gibi, edit alanları gibi, listeleme kutuları gibi GUI elemanları tek tek pixel temelinde programcı tarafından konumlandırılıyordu. Bu da ekran çözünürlüğü değiştiğinde görünümün bozulmasına yol açıyordu. Ancak son yıllarda artık GUI ortamlarında otomatik yerleştirme yapan nesneler bulundurulmaya başlanmıştır. Bu otomatik "yerleştirme nesneleri (layout objects)" çözürlük değişse bile yerleştirmeyi makul olarak kendisi yapmaktadır. Bir uygulamada GUI arayüzünün diğer kodlardan ayrılması uygulamanın "önyüz (frontend)" ve "arkayüz (backend)" kodlamasının farklı ekipler tarafından yapılabilmesine olanak sağlamaktadır. Son yıllarda artık GUI ortamları görsel arayüzle diğer kodları birbirinden ayırabilmek için mekanizmalar sunmaktadır. Böylece görsel arayüz XML ya da ona benzer bir dille bir text editörle oluşturulabilmekte ve uygulamaya kolayca dahil edilebilmektedir. Örneğin, GTK 3'te bu olanak daha sınırlıydı. GTK 4'te bu olanak güçlendirişmiştir. Aşağıda GTK 4 iel oluşturulmuş bir grid yerleştirme (layout) nesnesinin kullanımı verilmiştir. #include // Düğmeye basıldığında çalışacak fonksiyon static void on_button_clicked(GtkButton *button, gpointer user_data) { GtkEntry *entry = GTK_ENTRY(user_data); const char *text = gtk_entry_buffer_get_text(gtk_entry_get_buffer(entry)); // Ana pencereyi entry'den al (up-cast) GtkWindow *parent_window = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(entry))); GtkWidget *dialog = gtk_message_dialog_new( parent_window, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "Editbox içeriği:\n%s", text ); gtk_window_set_transient_for(GTK_WINDOW(dialog), parent_window); g_signal_connect(dialog, "response", G_CALLBACK(gtk_window_destroy), NULL); gtk_window_present(GTK_WINDOW(dialog)); } // Uygulama başlatıldığında çağrılır static void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; GtkWidget *grid; GtkWidget *entry; GtkWidget *button; // Pencere oluştur window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Editbox + Düğme"); gtk_window_set_default_size(GTK_WINDOW(window), 400, 100); // Grid oluştur grid = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(grid), 10); gtk_grid_set_row_spacing(GTK_GRID(grid), 10); gtk_window_set_child(GTK_WINDOW(window), grid); // Editbox entry = gtk_entry_new(); gtk_widget_set_margin_start(entry, 20); gtk_widget_set_margin_top(entry, 20); gtk_grid_attach(GTK_GRID(grid), entry, 0, 0, 1, 1); // (widget, col, row, width, height) // Düğme button = gtk_button_new_with_label("Göster"); gtk_widget_set_margin_start(button, 20); gtk_widget_set_margin_top(button, 20); gtk_grid_attach(GTK_GRID(grid), button, 1, 0, 1, 1); // Sinyal bağla g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), entry); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char **argv) { GtkApplication *app; int status; app = gtk_application_new("com.ornek.editdugme", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; } /*================================================================================================================================*/