/*================================================================================================================================*/ (03_04_06_2023) > C dilinde Tanımsız Davranış, Derleyiciye Bağlı Davranış ve Belirsiz Davranış Kavramları: C programcısı olarak bu durumlardan GÖZ ARDI ETMEMELİYİZ. >> Tanımsız Davranış: Dilin sentaks kurallarına uygundur fakat yapıldığı taktirde her şey olabilir demektir. Çalışma zamanında ne olacağı kestirilemeyen şeyler için veya farklı derleyiciler arasında ortak bir şey belirlenmeyen durumlar için Tanımsız Davranış terimi kullanılır. İngilizce'si "Undefined Behavior" demektir. >> Derleyiciye Bağlı: Bir kodun çıktısının derleyiciye bağlı olarak değişmesi durumudur. Derleyiciyi yazanlar böylesi durumları dökümante etmelidir. İngilizce'si "Implementation Defined" demektir. >> Belirsiz Davranış: Derleyiciye Bağlı durumların dökümante edilmemiş versiyonlarıdır. Sonuçta patolojik bir durum meydana gelmez fakat işleyişin nasıl yapılacağı derleyiciye yazanlara bırakılmıştır. Fakat derleyiciyi yazanlar bu tip durumları dökümante etmek zorunda değildir. İngilizce'si "Unspecified Behavior" demektir. > Derleyicilerin Hata Mesajları ve Standartlara Uyum: C derleyicileri hata/uyarı mesajlarını vermekle yükümlüdür fakat hatalı bir şekilde programı da derleyebilir. Standartlara göre yükümlü olduğu şey hata/uyarı mesajlarının verilmesi. Öte yandan derleyiciler ekstra uyarı mesajları da verebilirler, bu konuda da bir kısıtlama getirilmemiştir. Dolayısıyla BAŞARILI BİR ŞEKİLDE DERLENEN C KODU İÇİN GEÇERLİ BİR KOD DENMEZ. Burada bizler hangi kodların Tanımsız Davranış, Derleyiciye Bağlı Davranış veya Belirsiz Davranış içerdiğini bilmeliyiz ve yazdığımız kodlarda bunları göz önünde BULUNDURMALIYIZ. > C Programlarının Derlenmesi: >> "Microsoft Visual Studio" programını yüklediğimiz zaman aynı zamanda "Microsoft" derleyicisi de yüklenir. Bu programın adı "cl" biçimindedir. Dolayısıyla "sample.c" isimli dosyayı bu derleyici ile derlemek için "cl sample.c" demeliyiz. >>> "cl" derleyicisine ilişkini bazı seçenekler: "Developer Command Prompt" isimli "shell" programını kullanmalıyız. >>>> "/Fe" seçeneğini kullanırsak, oluşturulacak "executable" dosyasının adını değiştirebiliriz. >>>> "/c" seçeneğini kullanırsak, sadece derler. >>>> "link" programını kullanırsak, ilgili ".obj" dosyasını link edecektir. >> UNIX/Linux türevi sistemlerde "gcc" derleyicisi kullanılır. Varsayılan bir bileşen olarak işletim sistemini kurarken kurulur. "gcc" derleyicisine ek olarak "clang" derleyicisi de kullanılmaktadır. Apple ailesinde genellikle "clang" derleyicisi kullanılır. Bu derleyiciyi kurmak için sudo apt-get install clang komutunu "shell" programından çalıştırmalıyız. Bu iki derleyicinin komut satırı argümanları birbiriyle uyumludur. Öte yandan "gcc" derleyicisinin "Windows" uyumlu hale getirilmiş versiyonuna "MinGW" denmektedir. /*================================================================================================================================*/ (04_10_06_2023) > "Linux" komut satırı komutlarından çoğunu öğrenmeliyiz. "%80-%20" kuralına göre bir şeyin %20'si yapacağımız şeyin %80'ini karşılamaktadır. >> "file" komutu, argüman olarak aldığı dosyanın özelliklerini ekrana yazdırır. Örneğin, "file sample.c" biçiminde kullanılır. > "Microsoft Visual Studio" ile çalışırken; >> "Solution" kavramı bünyesinde birden fazla "proje" barındırabilmektedir. Dolayısıyla "Solution" kavramı ile "Project" kavramı birbirinden farklı kavramlardır. Yani bir "Solution" içerisine birden fazla "Project" ekleyebiliriz. >> Proje Özellikleri'ni açıp "C/C++" başlığı altındaki "Genel" sekmesine geldikten sonra "SDL Denetimleri" özelliğini devre dışı bırakmalıyız. Aksi halde "_CRT_SECURE_NO_WARNINGS" sembolik sabitini "define" etmemiz gerekmektedir. >> "Ctrl + F5" yaparsak derler ve çalıştırır. "Ctrl + Shift + B" yaparsak derlenmiş ise onu bağlar, derlenmemiş ise derler ve bağlar. > Bazı az kullanılan standart C fonksiyonları: >> "strtok" fonksiyonu: Özellikle sistem programlamada çok kullanılan bir fonksiyondur. Bir yazıyı çeşitli biçimlerde ayrıştırmaya yaramaktadır. Fonksiyonun prototipi aşağıdaki gibidir: #include char* strtok(char* str, const char* delimiters); Fonksiyonun birinci parametresi "const" olmadığı için buraya geçilen yazı değiştirilecektir. Yani parçalara ayrılacak yazının başlangıç adresi bu parametreye geçilmelidir. Buraya geçilecek yazının sonunda "\0" karakteri OLMALIDIR. Fonksiyonun ikinci parametresi ise ayraç olarak kullanılacak karakterlerin oluşturduğu yazının başlangıç adresidir. Fonksiyonun geri dönüş değeri, ilk parçalanan yazının başlangıç adresidir. Dolayısıyla bir yazıyı tam anlamıyla parçalarına ayırmak için bu fonksiyonu tekrar tekrar çağırmalıyız. Fakat ilk çağrıdan sonraki çağrılarda bu fonksiyonun birinci parametresine artık "NULL" değerini geçmemiz gerekmektedir. Dolayısıyla bu fonksiyonu bizler bir döngü içerisinde çağırmalıyız. * Örnek 1, #include #include int main(void) { /* # OUTPUT # Ahmet Kandemir .,,...,,,,Ahmet,,,...,,.Kandemir,..,, */ char string_to_parse[] = ".,,...,,,,Ahmet,,,,...,,.Kandemir.,..,,"; char delimeters[] = ".,"; for (char* parsed_string = strtok(string_to_parse, delimeters); parsed_string != NULL; parsed_string = strtok(NULL, delimeters)) puts(parsed_string); for (int i = 0; i < 40; ++i) printf("%c", string_to_parse[i]); return 0; } Bu fonksiyonun çalışmasını da şu şekilde açıklayabiliriz; Birinci parametresi ile belirtilen adresten başlayarak ilk ayraç karakteri olmayan karakterin yerini elde eder. Sonra ayıraç karakteri görmeyene dek ilerlemeye devam eder. İlk ayıraç karakteri gördüğünde oraya "\0" karakterini yerleştirir. Daha sonra yukarıda elde etmiş olduğu yer adresi ile fonksiyon geri döner. Yani; 1. Turda: .,,...,,,,Ahmet,,,,...,,.Kandemir.,..,, ^ ^^ a cb Burada ilk "a" konumunun adresi elde tutulur. Daha sonra "b" konumu bulunur ve bu konuma "\0" karakteri yerleştirilir. Artık yazı şu şekildedir: .,,...,,,,Ahmet\0,,,...,,.Kandemir.,..,, ^ ^ a c Fonksiyon "a" ile "c" konumları arasındaki yazıyı geri döndürür. Artık esas yazının son hali şu şekildedir: 2. Turda: .,,...,,,,Ahmet,,,...,,.Kandemir.,..,, ^ ^ d e Daha sonra bu fonksiyonu tekrar çağırırsak, "e" konumuna "\0" karakteri yerleştirip, "d" ve "e" konumları arasındaki yazıyı döndürecektir. Dolayısıyla esas yazının son hali aşağıdaki gibi olacaktır: .,,...,,,,Ahmet,,,...,,.Kandemir,..,, Görüldüğü üzere esas yazı bozulmuştur. Eğer bu bozulmanın olmasını istemiyorsak, ilgili diziyi kopyalamalıyız. Öte yandan bu program ile bir yazıyı parçalarken, ilgili yazının "static" alanda olması ve bir gösterici tarafından gösterilmediğine dikkat ediniz. Anımsayacağınız üzere C dilinde "string literal" lerin değiştirilmesi Tanımsız Davranış oluşturulacaktır. Bunun bir istisnası diziye ilk değer verilmesi durumudur. * Örnek 1, Aşağıdaki kodu "Microsoft Visual Studio" ile "Debugger" kullanarak çalıştırdığınızda programın çöktüğü görülecektir. #include #include int main(void) { /* # OUTPUT # */ char* string_to_parse = "Ali, Veli, Selami, Fatma"; /* Statik alandaki yazı bir gösterici tarafından gösterilmektedir. */ const char* delimeters = ".,"; char* parsed_string; for (parsed_string = strtok(string_to_parse, delimeters); parsed_string != NULL; parsed_string = strtok(NULL, delimeters)) puts(parsed_string); /* for (parsed_string = strtok("Ali, Veli, Selami, Fatma", delimeters); parsed_string != NULL; parsed_string = strtok(NULL, delimeters)) puts(parsed_string); */ for (int i = 0; i < 40; ++i) printf("%c", string_to_parse[i]); return 0; } * Örnek 2, Aşağıdaki kod Linux sisteminde çalıştırılmıştır. #include #include int main(void) { /* # OUTPUT # Segmentation fault */ char* string_to_parse = "Ali, Veli, Selami, Fatma"; /* Statik alandaki yazı bir gösterici tarafından gösterilmektedir. */ const char* delimeters = ".,"; char* parsed_string; for (parsed_string = strtok(string_to_parse, delimeters); parsed_string != NULL; parsed_string = strtok(NULL, delimeters)) puts(parsed_string); for (int i = 0; i < 40; ++i) printf("%c", string_to_parse[i]); return 0; } Şimdi de bu fonksiyonu bizler yazmaya çalışalım: * Örnek 1, #include #include char* mystrtok(char* s, const char* delims); int main(void) { /* # OUTPUT # Ali Veli Selami Fatma Ali Veli Selami Fatma ------------------------------------------------------------------------------------------- Ali Veli Selami Fatma Ali Veli Selami ,,,,, Fatma */ { char string_to_parse[] = "Ali, Veli, Selami, Fatma"; const char* delimeters = "., "; char* parsed_string; for (parsed_string = strtok(string_to_parse, delimeters); parsed_string != NULL; parsed_string = strtok(NULL, delimeters)) puts(parsed_string); for (int i = 0; i < 25; ++i) printf("%c", string_to_parse[i]); } puts("\n-------------------------------------------------------------------------------------------"); { char string_to_parse[] = "Ali, Veli, Selami, ,,,,, Fatma "; char* parsed_string; for (parsed_string = mystrtok(string_to_parse, ", "); parsed_string != NULL; parsed_string = mystrtok(NULL, ", ")) puts(parsed_string); for (int i = 0; i < 40; ++i) printf("%c", string_to_parse[i]); } return 0; } char* mystrtok(char* s, const char* delims) { static char* str; char* beg; /* Eğer fonksiyona geçilen adreste bir yazı varsa, bu yazı "str" ile gösterilecektir. */ if (s != NULL) str = s; /* * Daha sonra "str" adresinden başlayarak, ilk ayıraç karakteri olmayan karakter görülene kadar * ilgili gösterici ötelenecektir. Çünkü "strchr" fonksiyonu "NULL" değeri döndürmezse, ayıraç * karakterinin konumunu döndürür. Böylelikle bizler her ayıraç karakteri gördüğümüzde, göstericiyi * bir öteleriz. */ while (*str != '\0' && strchr(delims, *str) != NULL) ++str; /* Eğer yazının sonuna gelmişsek veya yazının içinde '\0' karakteri görmüşsek, geri döneriz. */ if (*str == '\0') return NULL; /* * Bu aşamada ne yazı sonundayız ne de bir ayıraç karakteri gördük. Artık ayıraç karakteri olmayan * bir karakterin adresini saklıyoruz. */ beg = str; /* * Daha sonra aramaya devam edelim ki ayıraç olmayan karakterler nerede son buluyor öğrenelim. "strchr" * fonksiyonu "NULL" değerini döndürdüğünde, ayıraç karakteri bulamamış demektir. Bu sefer de ayıraç olmayan * karakter gördükçe ilgili göstericiyi bi' öteliyoruz. */ while (*str != '\0' && strchr(delims, *str) == NULL) ++str; /* * Artık bu aşamada "str" göstericisi ayıraç karakteri göstermektedir. Eğer yazının sonu değilse, o karaktere * '\0' karakteri yerleştirip göstericiyi bir öteliyoruz. Böylelikle bizler ayıraç karakteri olmayan bir yazıyı * elde etmiş olduk. */ if (*str != '\0') *str++ = '\0'; /* * Bu aşamada yazının baş kısmını "beg" göstericisi, yazının sonundaki '\0' karakterinden sonraki konumu da "str" * göstericisi göstermektedir. */ /* printf("Obtained string: "); for (char* index = beg; index != str;) { printf("[%c] ", *index++); } */ return beg; } Şimdi de bu fonksiyonun "thread-safe" versiyonunu inceleyelim. Fonksiyonun ismi "strtok_s" biçimindedir. İleride de görüleceği üzere "static" ömürlü nesne kullanılması "thread-safe" değildir. Bu sebeple "Microsoft" standartlarınca böylesi bir fonksiyon oluşturulmuştur. Bu fonksiyon "static" ömürlü nesne yerine, üçüncü parametresinde geçilen göstericiyi kullanmaktadır. Bu fonksiyonun "UNIX" karşılığı da "strtok_r" isimli fonksiyondur. Fonksiyonların prototipi aşağıdaki gibidir: #include char* strtok_s(char* str,const char* delimiters,char** context ); char *strtok_r(char* s, const char* sep, char** state); * Örnek 1, Aşağıdaki örnekte "Microsoft" versiyonu kullanılmıştır. #include #include int main(void) { /* # OUTPUT # Ali Veli Selami Fatma Ali Veli Selami Fatma */ char string_to_parse[] = "Ali, Veli, Selami, Fatma"; const char* delimeters = "., "; char* parsed_string; char* saved_string; for (parsed_string = strtok_s(string_to_parse, delimeters, &saved_string); parsed_string != NULL; parsed_string = strtok_s(NULL, delimeters, &saved_string)) puts(parsed_string); for (int i = 0; i < 25; ++i) printf("%c", string_to_parse[i]); return 0; } * Örnek 2, Aşağıdaki örnekte ise "POSIX" versiyonu kullanılmıştır. #include #include int main(void) { /* # OUTPUT # Ali Veli Selami Fatma Ali Veli Selami Fatma */ char string_to_parse[] = "Ali, Veli, Selami, Fatma"; const char* delimeters = "., "; char* parsed_string; char* saved_string; for (parsed_string = strtok_r(string_to_parse, delimeters, &saved_string); parsed_string != NULL; parsed_string = strtok_r(NULL, delimeters, &saved_string)) puts(parsed_string); for (int i = 0; i < 25; ++i) printf("%c", string_to_parse[i]); return 0; } Şimdi de bir dosyadaki yazıları ayrıştırmayı görelim: * Örnek 1, /* test.csv */ ali,29,ankara veli,30,istanbul selami,17,sivas turgut,21,kastamonu /* main.c */ #include #include #include #define MAX_LINE 4096 int is_all_space(const char* string); int main(void) { /* # OUTPUT # ali 29 ankara ------------------ veli 30 istanbul ------------------ selami 17 sivas ------------------ turgut 21 kastamonu ------------------ */ FILE* f; if ((f = fopen("test.csv", "r")) == NULL) { fprintf(stderr, "cannot open file...\n"); exit(EXIT_FAILURE); } char line[MAX_LINE]; char* parsed_string; while (fgets(line, MAX_LINE, f) != NULL) /* "fgets" fonksiyonu, satır sonundaki '\n' karakterini de alacaktır. */ { /* Eğer okunan satır sadece boşluk karakterlerinden oluşmuşsa, o satırlar atlanacaktır. */ if(is_all_space(line)) continue; /* Satır sonundaki bu '\n' karakteri '\0' karakterine dönüştürülür. */ if ((parsed_string = strchr(line, '\n')) != NULL) *parsed_string = '\0'; /* Daha sonra her bir satır ayrıştırılır. */ for (parsed_string = strtok(line, ","); parsed_string != NULL; parsed_string = strtok(NULL, ",")) printf("%s\n", parsed_string); printf("------------------\n"); } fclose(f); return 0; } int is_all_space(const char* string) { while(*string != '\0' && isspace(*string)) ++string; return *string == '\0'; } Pekiyi ayrıştırma yaparken elimizdeki yazının bozulmasını istemiyorsak ne yapmalıyız? Tabii ki bu yazıyı başka bir yerde saklamalı. * Örnek 1, Aşağıdaki örnekte "strtok" fonksiyonunun esas yazıyı bozmayan versiyonunu yazmış olduk. Fakat bunu yapmak yerine esas diziyi başka yere kopyaladıktan sonra işlemlere devam da edebilirdik. #include #include char* mystrtok(const char* str, const char* delims, char* dest); int main(void) { /* # OUTPUT # Ali Veli Selami Fatma ---------------------------- Ali, Veli, Selami, ,,,,, Fatma */ char string_to_parse[] = "Ali, Veli, Selami, ,,,,, Fatma "; char buffer[1024]; char* parsed_string; for (parsed_string = mystrtok(string_to_parse, ", ", buffer); parsed_string != NULL; parsed_string = mystrtok(NULL, ", ", buffer)) puts(parsed_string); puts("----------------------------\n"); puts(string_to_parse); return 0; } char* mystrtok(const char* s, const char* delims, char* dest) { static const char* str; const char* beg; if (s != NULL) str = s; /* * Yazının içerisinde ayıraç karakteri bulunduğu müddetçe ilgili gösterici bir ötelenmektedir. */ while (*str != '\0' && strchr(delims, *str) != NULL) ++str; /* * Eğer yazı sonuna gelinmişse fonksiyondan dönülecektir. */ if (*str == '\0') return NULL; /* * Akış buraya gelmişse ayıraç olmayan bir karakteri bulunmuş demektir. Artık o karakterin konumu * saklandı. */ beg = str; /* * Artık ayıraç olmayan karakter bulunduğu müddetçe ilgili gösterici bir ötelenmektedir. */ while (*str != '\0' && strchr(delims, *str) == NULL) ++str; /* * Akış buraya gelmişse ayıraç olan bir karakteri bulunmuştur. Artık "beg" konumundan başlayan ve "str" * konumunda son bulan bu yazıyı "dest" adresinden itibaren kopyalayabiliriz. */ strncpy(dest, beg, str - beg); /* * Son olarak "dest" adresine kopyalanan yazının sonuna da '\0' karakteri koyalım. */ dest[str - beg] = '\0'; /* * Şimdi "str" konumu ayıraç karakterinin yerini göstermektedir. Birazdan fonksiyondan çıkılacağı için, * tekrardan göstericiyi ötelemeye lüzum yoktur. Fonksiyon yeniden çağrıldığında, bu karakterin konumundan * devam edecektir. */ if (*str != '\0') ++str; return dest; } * Örnek 2, Aşağıdaki örnekte ise parçalara ayrılacak olan yazı başka bir yere kopyalanmıştır. #include #include int main(void) { /* # OUTPUT # Ali Veli Selami Fatma ---------------------------- Ali, Veli, Selami, ,,,,, Fatma */ const char string_to_parse[] = "Ali, Veli, Selami, ,,,,, Fatma "; char copied_string[40]; char* parsed_string; strcpy(copied_string, string_to_parse); for (parsed_string = strtok(copied_string, ", "); parsed_string != NULL; parsed_string = strtok(NULL, ", ")) puts(parsed_string); puts("----------------------------\n"); puts(string_to_parse); return 0; } * Örnek 3, Aşağıdaki örnekte ise dinamik bellek tahsisatı yapılmıştır. #include #include #include char* mystrtok(const char* str, const char* delims); int main(void) { /* # OUTPUT # Ali Veli Selami Fatma ---------------------------- Ali, Veli, Selami, ,,,,, Fatma */ const char string_to_parse[] = "Ali, Veli, Selami, ,,,,, Fatma "; char* parsed_string; for (parsed_string = mystrtok(string_to_parse, ", "); parsed_string != NULL; parsed_string = mystrtok(NULL, ", ")) { puts(parsed_string); free(parsed_string); } puts("----------------------------\n"); puts(string_to_parse); return 0; } char* mystrtok(const char* s, const char* delims) { static const char* str; const char* beg; char* dest; if (s != NULL) str = s; /* * Yazının içerisinde ayıraç karakteri bulunduğu müddetçe ilgili gösterici bir ötelenmektedir. */ while (*str != '\0' && strchr(delims, *str) != NULL) ++str; /* * Eğer yazı sonuna gelinmişse fonksiyondan dönülecektir. */ if (*str == '\0') return NULL; /* * Akış buraya gelmişse ayıraç olmayan bir karakteri bulunmuş demektir. Artık o karakterin konumu * saklandı. */ beg = str; /* * Artık ayıraç olmayan karakter bulunduğu müddetçe ilgili gösterici bir ötelenmektedir. */ while (*str != '\0' && strchr(delims, *str) == NULL) ++str; /* * Bu aşamada ayıraç olmayan karakterlerin kopyalanacağı alan dinamik bir biçimde tahsis edilmiştir. Hata * kodunun daha iyi anlaşılabilir olması için "errno" değişkeninden faydalanabiliriz. */ if ((dest = (char*)malloc(str - beg + 1)) == NULL) return NULL; /* * Akış buraya gelmişse ayıraç olan bir karakteri bulunmuştur. Artık "beg" konumundan başlayan ve "str" * konumunda son bulan bu yazıyı "dest" adresinden itibaren kopyalayabiliriz. */ strncpy(dest, beg, str - beg); /* * Son olarak "dest" adresine kopyalanan yazının sonuna da '\0' karakteri koyalım. */ dest[str - beg] = '\0'; /* * Şimdi "str" konumu ayıraç karakterinin yerini göstermektedir. Birazdan fonksiyondan çıkılacağı için, * tekrardan göstericiyi ötelemeye lüzum yoktur. Fonksiyon yeniden çağrıldığında, bu karakterin konumundan * devam edecektir. */ if (*str != '\0') ++str; return dest; } > Hatırlatıcı Notlar: >> C dilinde çift tırnak içerisindeki karakterleri değiştirmek Tanımsız Davranıştır. Bunun istisnai durumu bir diziye ilk değer verilmesi durumudur. * Örnek 1, Elemanları "char" olan bir diziye ilk değer verirken kullanılan çift tırnak, bu durumda istisnaidir. //... int main() { /* * Burada "string" isminde bir dizi oluşturulu ve her bir indisie sırasıyla * "A", "h", "m", "e", "t" ve "\0" karakterleri getirilir. */ char string[] = "Ahmet"; /* * Dolayısıyla yukarıda yapılan şey aslında aşağıdaki ile aynıdır. Yani ikisi * arasında bir fark yoktur. */ char str[] = { 'a', 'h','m','e','t','\0' }; } * Örnek 2, //... int main() { /* * Burada aslında "Ahmet" yazısı bellekte bir yere yerleştiriliyor. "string" isimli gösterici * ise bu yazıyı gösterir durumdadır. Bellekte yerleştirilen yeri bizler değiştiremiyoruz. */ char* string = "Ahmet"; } /*================================================================================================================================*/ (05_11_06_2023) (https://vimeo.com/835239024) (163b6091-4) > Bazı az kullanılan standart C fonksiyonları (devam): >> "remove" fonksiyonu: Standart bir C fonksiyonudur. Dosya silmek için kullanılır. Fakat her kullanıcı her bir dosyayı da silemez. Bir takım güvenlik mekanizması arka planda işletilmektedir ki bu mekanizmaya ileride değinilecektir. Fonksiyonun prototipi aşağıdaki gibidir: #include int remove(const char *path); Fonksiyon argüman olarak silinmek istenen dosyanın yol ifadesini alır. Başarı durumunda "0", hata durumunda ise "0" dışı bir değeri döndürür. Muhtelif nedenlerden dolayı bu fonksiyon başarısız olabilir. Dolayısıyla geri dönüş değeri mutlaka kontrol edilmelidir. * Örnek 1, #include #include int main(void) { /* # OUTPUT # file removed... */ /* Prosesin "cwd" içerisindeki "test.csv" ismine sahip dosya silinmeye çalışılacaktır. */ if (remove("test.csv")) { fprintf(stderr, "cannot remove the file...\n"); exit(EXIT_FAILURE); } puts("file removed...\n"); return 0; } Pekiyi halihazırda kullanılan bir dosyayı silmek istersek ne olur? Bu durum işletim sistemine göre değişiklik göstermektedir. UNIX türevi sistemlerde, dosya yine silinir fakat gerçek silme işlemi ilgili dosya kapatıldıktan sonra gerçekleştirilir. Windows işletim sisteminde ise o dosya oluşturulurken kullanılan bir takım bayraklara göre davranış değişiklik göstermektedir. * Örnek 1, Aşağıdaki program Windows bir işletim sisteminde çalıştırılmıştır. #include #include int main(void) { /* # OUTPUT # File opened!... cannot remove the file... */ FILE* f; if ((f = fopen("test.txt", "r")) == NULL) { fprintf(stderr, "cannot open file...\n"); exit(EXIT_FAILURE); } puts("File opened!...\n"); if (remove("test.txt")) { fprintf(stderr, "cannot remove the file...\n"); exit(EXIT_FAILURE); } puts("file removed...\n"); fclose(f); return 0; } * Örnek 2, Aşağıdaki program UNIX türevi bir işletim sisteminde çalıştırılmıştır. #include #include int main(void) { /* # OUTPUT # File opened!... file removed... */ FILE* f; if ((f = fopen("test.txt", "r")) == NULL) { fprintf(stderr, "cannot open file...\n"); exit(EXIT_FAILURE); } puts("File opened!...\n"); if (remove("test.txt")) { fprintf(stderr, "cannot remove the file...\n"); exit(EXIT_FAILURE); } puts("file removed...\n"); fclose(f); return 0; } Öte yandan belirtmek gerekir ki bu fonksiyon kullanılarak silinen dosyalar Geri Dönüşüm Kutusu'nda saklanmaz. Geri Dönüşüm Kutusu, yüksek seviyeli bir "shell" organizasyonudur. İşletim sisteminin çekirdeğinin bir ilgisi yoktur. Hakeza Gizli Dosyaları da bu fonksiyonu kullanarak silebiliriz. Son olarak bir dosya silindiğinde diskteki baytlar silinmez, ekseriyetle. Sadece diskin o bölümünün kullanıma açık ilan edilir. Dolayısıyla o bölüme başka programlar bir şey yazmamışsa ve belli şartlara da haizsek, silinen dosyayı geri getirmemiz mümkündür. >> "rename" fonksiyonu: Bir dosyanın ismini değiştirmek için kullanılır. Fonksiyonun prototipi şu şekildedir: #include int rename(const char *old, const char *new); Fonksiyon birinci parametresi ile ismi değiştirilecek dosyanın ismini almaktadır. İkinci parametre ile bu dosyanın yeni ismini almaktadır. Başarı durumunda "0", hata durumunda "0" dışı bir değer döndürmektedir. Eğer yeni isim halihazırda başka dosya ismi olarak kullanılıyorsa, sonucun ne olacağı "Implementation Defined" biçimindedir. * Örnek 1, Bu programı ilk çalıştırdığımızda ismin değiştiğini, ikinci ve sonraki çalıştırmalarda işlemin başarısız olduğu görülecektir. #include #include int main(void) { /* # OUTPUT # Ok... */ if (rename("test.txt", "mest.txt")) { fprintf(stderr, "cannot rename the file...\n"); exit(EXIT_FAILURE); } puts("Ok...\n"); return 0; } * Örnek 2, Son olarak başka dizin içerisinden de isim verebiliriz. Bu durumuda ilgili dosya bir nevi taşınacaktır. #include #include int main(void) { /* # OUTPUT # */ if (rename("test.txt", "C:\\Users\\ahmet\\Desktop\\aqua.txt")) { fprintf(stderr, "cannot rename the file...\n"); exit(EXIT_FAILURE); } puts("Ok...\n"); return 0; } >> "system" fonksiyonu: Yine standart bir C fonksiyonudur. Argüman olarak aldığı ifadelerle işletim sisteminin terminal programını çalıştırır. Fonksiyonun prototipi aşağıdaki gibidir: #include int system(const char *command); Fonksiyon parametre olarak çalıştırılacak terminal komutlarını alır. Geri dönüş değeri ise derleyici yazanların isteğine bırakılmıştır. Ancak Windows ve UNIX türevi sistemlerde fonksiyon başarı durumunda "0", hata durumunda "0" dışı bir değer döndürmektedir. Eğer fonksiyona "NULL" değerini geçersek, ilgili sistemde bir terminal programının olup olmadığını sorgulamış oluruz. Bu durumda "0" dışı herhangi bir değere dönerse ilgili sistemde terminal programı vardır, "0" değerine dönerse ilgili sistemde terminal programı yoktur demektir. Fonksiyon detaylı olması hasebiyle ileride tekrardan ele alınacaktır. Burada dikkat etmemiz gereken şey ise bu fonksiyona geçeceğimiz argümanlar belli bir sistemdeki terminal programları için anlamlı olmalıdır. * Örnek 1, Aşağıdaki çıktıyı Windows işletim sistemindeki terminal programında "dir main.c" yaparak da elde edebiliriz. #include #include int main(void) { /* # OUTPUT # Volume in drive D is Archieve Volume Serial Number is 8CDD-C14B Directory of D:\CSD\SysProg-1\src\CFunctions 06/12/2023 06:08 PM 152 main.c 1 File(s) 152 bytes 0 Dir(s) 718,262,374,400 bytes free */ system("dir main.c"); return 0; } * Örnek 2, Aşağıdaki çıktıyı UNIX türevi bir işletim sisteminin "shell" programında "ls -l c_functions.c" yaparak da elde edebiliriz. #include #include int main(void) { /* # OUTPUT # -rw-r--r-- 1 ahmopasa ahmopasa 449 Jun 12 18:10 c_functions.c */ system("ls -l c_functions.c"); return 0; } * Örnek 3, #include #include int main(void) { /* # OUTPUT # Volume in drive D is Archieve Volume Serial Number is 8CDD-C14B Directory of D:\CSD\SysProg-1\src\CFunctions 06/12/2023 06:08 PM 152 main.c 1 File(s) 152 bytes 0 Dir(s) 718,262,374,400 bytes free */ if (system(NULL)) /* "0" ile dönmesi durumunda bir terminal programının olmadığı anlaşılacaktır. */ system("dir main.c"); else printf("terminal program does not exist!...\n"); return 0; } * Örnek 4, Aşağıdaki programda "system" fonksiyonuna geçilen komut aslında Windows işletim sistemindeki terminal programında anlamlıdır. #include #include int main(void) { /* # OUTPUT # Volume in drive D is Archieve Volume Serial Number is 8CDD-C14B Directory of D:\CSD\SysProg-1\src\CFunctions 06/12/2023 06:08 PM 152 main.c 1 File(s) 152 bytes 0 Dir(s) 718,262,374,400 bytes free */ if (system(NULL)) system("dir main.c"); else printf("terminal program does not exist!...\n"); printf("Press ENTER to continue!...\n"); getchar(); system("cls"); /* Linux işletim sistemi için "cls" yerine "clear" yazılmalıdır. */ return 0; } > Geçici Dosyalar: Programın çalışması sırasında belli bir amaçla oluşturulan ve amaca ulaşıldıktan sonra silinen kısa ömürlü dosyalara Geçici Dosyalar denmektedir. Geçici Dosya oluşturulurken karşılaşabileceğimiz önemli bir sorun, bu dosyalara vereceğimiz ismin çakışmasıdır. İşte bu problemin çözümü için iki adet standart C fonksiyonu geliştirilmiştir. Bunlar "tmpfile" ve "tmpnam". >> "tmpfile" fonksiyonu: Olmayan bir isimle bir dosyayı "wb+" modunda oluşturur. Fonksiyonun prototipi aşağıdaki gibidir: #include FILE *tmpfile(void); Fonksiyon başarı durumunda oluşturulan dosyaya ilişkin "FILE" türünden yapının adresini döndürür. Hata durumunda ise "NULL" ile geri döner. Bu fonksiyon ile açılan geçici dosyalar, "fclose" çağrısı sonrasında veya programın sonlanmasıyla kapatılırlar. * Örnek 1, #include #include int main(void) { /* # OUTPUT # */ FILE* f; if ((f = tmpfile()) == NULL) { fprintf(stderr, "cannot create temporary file...\n"); exit(EXIT_FAILURE); } int i; for (i = 0; i < 100000; ++i) if (fwrite(&i, sizeof(int), 1, f) != 1) { fprintf(stderr, "cannot write to temporary file...\n"); exit(EXIT_FAILURE); } rewind(f); int value; while (fread(&value, sizeof(int), 1, f) == 1) printf("%d\n", value); if (ferror(f)) { fprintf(stderr, "cannot read from temporary file...\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } >> "tmpnam" fonksiyonu: Bu fonksiyon bizlere bir yol ifadesi verir. Fakat bu yol ifadesini kullanarak bir dosya açmaz. Programcılar bu fonksiyon ile elde ettikleri yol ifadesinde belirtilen dosyayı açmakla yükümlüdürler. Fonksiyonun prototipi şu şekildedir: #include char *tmpnam(char *s); Fonksiyon parametre olarak elde edilecek yol ifadesinin yazılacağı alanın başlangıç adresini alır. Bu alanın yeterli uzunlukta olması bizim sorumluluğumuzdadır. "L_tmpnam" sembolik sabiti, iş bu alanın olması gerektiği maksimum büyüklük bilgisidir. NULL adres geçilmesi durumunda, fonksiyonun bünyesindeki statik ömürlü dizinin adresini bize döndürürler. Bu fonksiyonun üreteceği isim %100 biçimde "unique" olması garanti değildir. Dolayısıyla belli bir şeyden sonra fonksiyon başarısız olabilir. Fonksiyon başarısız olduğunda "NULL" değerine, başarılı olduğunda da ilgili ismin bulunduğu adresi verir. * Örnek 1, Aşağıdaki program UNIX türevi bir işletim sisteminde çalıştırılmıştır. Komut satırı argümanı olarak belirtilen dosyadaki "#" karakterlerini silmektedir. #include #include #include #include #define MAX_LINE 4096 bool is_sharped(const char* string); int main(int argc, char** argv) { /* # OUTPUT # */ /* Programa geçilen komut satırı argümanları kontrol ediliyor. */ if (2 != argc) { fprintf(stderr, "wrong number of arguments!...\n"); exit(EXIT_FAILURE); } /* Esas dosyayı açıyoruz. */ FILE* f_source; if((f_source = fopen(argv[1], "r")) == NULL) { fprintf(stderr, "cannot open file: %s\n", argv[1]); exit(EXIT_FAILURE); } /* Geçici Dosya için bir isim elde ediyoruz. */ char* t_path; if((t_path = tmpnam(NULL)) == NULL) { fprintf(stderr, "cannot get temporary file name!...\n"); exit(EXIT_FAILURE); } /* Elde ettiğimiz o isimle Geçici Dosyayı açıyoruz. */ FILE* f_temp; if((f_temp = fopen(t_path, "w")) == NULL) { fprintf(stderr, "cannot open temporary file\n"); exit(EXIT_FAILURE); } /* * Daha sonra esas dosyayı satır satır okuyoruz. * Her bir satırı da işleyip, "buffer" alanına yazıyoruz. * En sonunda da işlenmiş halini Geçici Dosyaya yazıyoruz. */ char buffer[MAX_LINE]; while(fgets(buffer, MAX_LINE, f_source) != NULL) { if(!is_sharped(buffer)) { if(fputs(buffer, f_temp) == EOF) { fprintf(stderr, "cannot write temporary file\n"); goto FAILED; } } } /* Esas dosyadan okuma yaparken bir hatanın olup olmadığı kontrol ediliyor. */ if(ferror(f_source)) { fprintf(stderr, "cannot read file: %s\n", argv[1]); goto FAILED; } /* Daha sonra her iki dosya da kapatılıyor. */ fclose(f_source); fclose(f_temp); /* Esas dosyayı siliyoruz. */ if(remove(argv[1])) { fprintf(stderr, "cannot remove file: %s\n", argv[1]); goto FAILED; } /* En sonunda da Geçici Dosyanın ismini değiştiriyoruz. */ if(rename(t_path, argv[1])) { fprintf(stderr, "cannot rename file: %s\n", argv[1]); goto FAILED; } printf("Ok...\n"); return EXIT_SUCCESS; FAILED: /* Eğer herhangi bir hata oluşursa da oluşturulan Geçici Dosya da kapatılıp siliniyor. */ fclose(f_temp); if(remove(t_path)) { fprintf(stderr, "cannot delete temporary file!...\n"); exit(EXIT_FAILURE); } printf("Failed...\n"); return EXIT_FAILURE; } bool is_sharped(const char* string) { while(isspace(*string)) ++string; return *string == '#'; } > Hatırlatıcı Notlar: >> Bir fonksiyon bir işi başlatsa fakat onun bitmesini beklemezse, bu duruma asenkron işlemler denir. Eğer başlattığı işin bitmesini bekleseydi, senkron işlemler denilecekti. Senkron işlemlerde beklenen işin bittiği garanti altındadır. >> "thread-safe" olması için statik ömürlü nesne kullanmaması gerekmektedir. >> Algoritma dünyasında "merge" işlemi demek kendi içerisinde sıralı dizileri birleştirerek yine sıralı hale getirmektir. /*================================================================================================================================*/ (06_17_06_2023) > Programın Komut Satırı Argümanları: Komut satırından bir programrama argüman geçmek için kullanılan argümanlardır. Fakat Windows gibi pencereli ortamlardan bir programı çalıştırırken de argümanları geçebiliriz. Fakat Windows sistemler pencereli ortamlar kullanırken, UNIX/Linux sistemler ağırlıklı olarak komut satırını kullanmaktadır. Dolayısıyla UNIX/Linux sistemlerdeki temel komut satırı argümanlarını bilmemiz ÇOK ÖNEMLİDİR. Öte yandan bir programa geçtiğimiz argümanlar, o programın "main" fonksiyonunun "argc" ve "argv" isimli parametreleri üzerinden aktarılır. Buradaki isimlendirme bir koşul değil, gelenektir. C standartlarına göre "main" fonksiyonunun parametrik yapısı iki biçimdedir; "void" parametreli veya "int" ve "char**" parametreli. Fakat standartlar bu iki biçimi zorunlu KILMAMIŞTIR, ilgili derleyiciye yazan kişilere de başka biçimler oluşturmasına imkan kılmıştır. Fonksiyonun geri dönüş değeri de "int" türden olmalıdır, standartlar bunu zorunlu KILMIŞTIR. Diğer yandan bu "main" fonksiyonu bir "entry-point" olarak görülmektedir. Yani programın akışı ilk olarak bu "main" programından başlamaktadır. Diğer yandan "main" fonksiyonunun ikinci parametresinin son elemanı ise "NULL" değerinde bir gösterici olmalıdır. C standartları bunu garanti altına almışlardır. Buradan hareketle "argv[argc]" ifadesinin değeri "NULL" olacaktır. Yine unutmamalıyız ki "argv" dizisinin ilk elemanı program ismidir. Bir diğer husus da komut satırına geçtiğimiz argümanların sonuna "\0" karakterinin eklenmesidir. Dolayısıyla bu argümanları işlerken bu hususa da dikkat etmeliyiz. Pekiyi komut satırına geçtiğimiz argümanlar nasıl "main" fonksiyonuna geçilmekte? Aslında burada ilgili "shell" programı devreye giriyor. * Örnek 1, Komut Satırı Argümanlarının ekrana basılması: #include #include int main(int argc, char** argv) { /* # OUTPUT # 0. Index: D:\CSD\SysProg-1\src\CFunctions\x64\Debug\CFunctions.exe 1. Index: Ahmet 2. Index: Kandemir 3. Index: Pehlivanli */ for (int i = 0; i < argc; ++i) { printf("%d. Index: %s\n", i, argv[i]); } return 0; } * Örnek 2, Aşağıdaki örnekte ise klavyeden girilen yazı bir diziye yerleştirilmiştir. #include #include #define MAX_CMD_LINE 4096 #define MAX_ARGV 1024 int main(void) { /* # OUTPUT # */ char cmd_line[MAX_CMD_LINE]; fgets(cmd_line, MAX_CMD_LINE, stdin); char* argv[MAX_ARGV]; int argc = 0; for (char* buffer = strtok(cmd_line, " \t"); buffer != NULL; buffer = strtok(NULL, " \t")) argv[argc++] = buffer; argv[argc] = NULL; printf("Arguments: "); for (int i = 0; i < argc; ++i) printf("%s ", argv[i]); return 0; } Komut satırı argümanlarındaki bir diğer kavram ise seçenek kavramıdır. Yani argümanların seçeneklendirilmesi mevzusudur. Örneğin, UNIX/Linux türevi sistemlerde "ls" komutunun "-l" seçeneği vardır. Bu seçenek kullanıldığında ilgili komutun davranışı da değişmektedir. Benzer bir husus Windows sistemlerinde de vardır. Fakat bu sistemlerde seçenekleri kullanabilmek için "-" karakteri yerine "/" karakteri kullanılmaktadır. Örneğin, "gcc -c sample.c" ifadesini UNIX/Linux türevi sistemlerde "shell" programına geçtiğimiz zaman "sample.c" dosyası sadece derlenir. Aynı etkiyi Windows sistemlerinde oluşturmak için, "cl /c sample.c" ifadesini ilgili "shell" programına geçmeliyiz. Şimdi de komut satırı argümanlarının oluşturulma biçimlerini inceleyelim. Şöyleki; >> UNIX/Linux sistemlerinde GNU stili kullanılır. Bu stile göre üç farklı seçenek bulunmaktadır. Bunlar Argümansız Seçenekler, Argümanlı Seçenekler ve Seçeneksiz Argümanlar biçimindedir. >>> Argümansız Seçenekler: Yalnız "-" karakteri ve bunun yanındada tek bir karakterin olduğu seçeneklerdir. Örneğin, "ls -l -i" ifadesindeki "-l" ve "-i" seçenekleri Argümansız Seçeneklerdir. Bazı durumlarda "-" karakterinin yanında birden fazla karakter daha yazıldığı görülmüştür. Bu tür durumlarda iş bu iki karakterlerin her birisi ayrı ayrı Argümansız Seçeneklerdir. Örneğin, "ls -li" ifadesindeki "-li" aslında "-l -i" ifadesinin kısaltılmış biçimidir. Son olarak böylesi seçeneklerin yazım sırasında karakterlerin sıralamasının bir önemi yoktur. >>> Argümanlı Seçenekler: Öyle seçeneklerdir ki tek başlarına kullanılamaz, mutlaka peşinden bir argümanı gelmelidir. Örneğin, "prog -t x.txt" ifadesindeki "-t" aslında Argümanlı Seçenek olup, "x.txt" ise bunun argümanıdır. Böylesi seçeneklerden sonra geçilen ifadeler, o seçeneğin argümanı olarak ele alınırlar. Öte yandan böylesi seçeneker, yukarıdaki argümansız seçenekler ile de birleştirilip yazılabilir. Örneğin, "gcc -lt x.txt" ifadesindeki "x.txt" aslında "-t" seçeneğinin argümanıdır ve "-l" argümansız seçeneği ile birleştirilerek yazılmıştır. Fakat böylesi kullanım pek tavsiye edilmez. Bir diğer yandan argümanlı seçeneklerin argümanları, seçenekleri ile de birleştirilip yazılabilmektedir. Örneğin, "gcc -osample sample.c" ifadesinde "sample" ifadesi aslında "-o" seçeneğinin argümanıdır. Fakat bazı programlar bu tip yazımı kabul etmemektedir. >>> Seçeneksiz Argümanlar: Hiç bir seçenek ile alakası olmayan argümanlardır. Örneğin, "gcc -o sample sample.c" ifadesindeki "sample.c" ifadesi seçeneksiz argümandır. Hiç bir seçeneğe ait değildir. Normal bir komut satırı argümanıdır. Öte yandan "GNU" stilinde tek karakterden oluşmayan uzun seçenekler de kullanılmaya başlandı. Yani öyle seçenekler ki birden fazla karakterin kullanıldığı seçenekler. Bu tip seçeneklerdeki amaç yukarida detayları açıklanan tek karakterli seçeneklerin getirdiği bir takım karmaşıklıkları gidermektir. Fakat POSIX standartları bu uzun seçenekleri desteklememektedir. Ancak UNIX/Linux dünyasında yaygın biçimde kullanılmaktadır. Pekiyi bu uzun seçenekler nasıl oluşturulur? Yine burada da Argümanlı Uzun Seçenek, Argümansız Uzun Seçenek ve İsteğe Bağlı Argümanlı Uzun Seçenek biçimleri vardır. Artık bu uzun seçenekleri kullanırken "-" karakteri yerine "--" ifadesi kullanılır. Örneğin, "prog --count -a -b -c --length 100" ifadesini ele alalım. Buradaki "count" ve "length" artık uzun seçenektir. Şimdi de bu uzun seçenekleri inceleyelim: >>> Argümanlı Uzun Seçenek: Yukarıda anlatılan Argümanlı Seçeneklerdeki mevzunun aynısıdır. Yukarıdaki örneği ele alırsak, "--length 100" ifadesindeki "length" seçeneği argümanlı uzun seçenektir. Dolayısıyla "100" ifadesi de bu seçeneğin argümanıdır. >>> Argümansız Uzun Seçenek: Yukarıda anlatılan Argümansız Seçeneklerdeki mevzunun aynısıdır. Yukarıdaki örneği ele alırsak, "--count" bir argümansız uzun seçenektir. >>> İsteğe Bağlı Argümanlı Uzun Seçenek: Bu ise sadece uzun seçeneklere has bir konudur. İlgili seçeneğe herhangi bir seçeneği özel olarak geçmediğimiz zaman, varsayılan argümanın geçilmesini olanak kılar. Burada artık "=" karakteri de kullanıma girmektedir. Şöyleki; "prog --size=512" ifadesinde "size" uzun seçeneğine argüman olarak "512" geçilmiştir. Eğer bizler sistem tarafından atanan varsayılan değeri kullanmak isteseydik, komutumuz şu şekilde olacaktı: "prog --size" Burada dikkat etmemiz gereken husus ilgili uzun seçeneğin, "=" karakterinin ve bu seçeneğe geçilecek argümanın arasında herhangi bir boşluk karakterinin olmaması gerektiğidir. Dolayısıyla aşağıdaki kullanım farklı bir anlam içermektedir; "prog --size 512" Günümüzde programlar bazı kısa seçeneklere alternatif olarak uzun seçenek, bazı uzun seçeneklere alternatif olarak da kısa seçenek barındırmaktadır. Örneğin, "ls" komutunda "-a" seçeneği ile "--all" seçeneği aynı işlevi görmektedir. Yine benzer biçimde "-A" ile "--almost-all" seçeneği de aynı işlevi görmektedir. Buradan hareketle şunu da belirtmeliyiz ki UNIX/Linux dünyasındaki seçenekler "case-sensitive" durumdadır. Fakat unutmamalıyız ki POSIX standartları uzun SEÇENEKLERİ DESTEKLEMEMEKTEDİR. Dolayısıyla bu noktada ilgili işletim sisteminin ilgili programlar hakkında sunduğu programlara bakmalıyız. Son olarak belirtmekte fayda vardır ki bu "GNU" stili bir zorunluluk değil, herkesin benimsediği bir stildir. Dolayısıyla kendi programlarımızda komut satırı argümanlarının işlerken başka stilleri takip edebiliriz. İş bu nedenden dolayı bazı programlar, özellikle çok eski programlar, bu "GNU" stilini takip etmemektedir. Örneğin, "gcc sample.c -o sample -Wall" ifadesinde de görüleceği üzere "-Wall" seçeneksiz argümanı, yukarıdaki "GNU" stiline uygun olmadığı görülecektir. >> Windows sistemlerinde ise yukarıdaki gibi bir stil mevcut değildir. Microsoft firmasının kendi oluşturduğu bir stil vardır. Kabaca belirtmek gerekirse; "-" karakteri yerine "/" karakteri kullanılır. Argümanlı Seçeneklerde argümanlar ":" karakteri ile bitişik yazılır veya ":" olmadan direkt olarak bitişik yazılır. Fakat Windows ailesi genel itibariyle pencereli bir çalışma ortamı sunduğu için komut satırı kullanımı UNIX dünyası kadar aktif değildir. Pekiyi bizler komut satırı argümanlarını nasıl "parse" edeceğiz? >> UNIX/Linux dünyasında "parse" işlemi için POSIX standartlarında "getopt" isimli bir fonksiyon vardır. POSIX standartları uzun seçenekleri desteklemediği için, bu fonksiyon da sadece kısa seçenekleri desteklemektedir. Uzun seçenekleri "parse" işlemi için ise "getopt_long" isimli fonksiyonu kullanacağız. >>> "getopt" fonksiyonu: Yukarıda da açıklandığı üzere bir POSIX fonksiyonudur. Fonksiyonun prototipi aşağıdaki gibidir: #include int getopt(int argc, char * const argv[], const char *optstring); Fonksiyonun birinci ve ikinci parametreleri, "main" fonksiyonunun parametreleridir. Üçüncü parametre ise programımızın kullanacağı seçenekleri içeren yazının başlangıç adresidir. Bu yazıda argümanlı ve argümansız seçenekleri belirtmeliyiz. Argümansız seçenekleri direkt belirtirken, argümanlı seçenekleri ise ":" atomu ile birlikte belirtmeliyiz. Seçeneksiz argümanları ise bu fonksiyon, ikinci parametresine geçilen dizinin belli bir konuma, otomatik olarak yerleştirmektedir. Fonksiyonun geri dönüş değeri ise "parse" edecek bir şey kalmadığında "-1", bulduğu seçenekler için seçeneğin kendisine, geçersiz bir seçenek veya argümanlı bir seçeneğin argümanı girilmediğinde "?" ile geri dönmektedir. Burada seçeneğin kendisine dönerken "char" türden değil, karakterin ASCII tablosundaki "int" türüne geri dönmektedir. * Örnek 1, #include #include int main(int argc, char** argv) { /* # Command Line Arguments # -a -b Ahmet -c */ /* # OUTPUT # a is given b is given c is given */ /* * "a" ve "c" seçenekleri argümansız, * "b" seçeneği ise argümanlıdır. */ const char* options = "ab:c"; int result; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 'a' : printf("%c is given\n", result); break; case 'b' : printf("%c is given\n", result); break; case 'c' : printf("%c is given\n", result); break; case '?' : printf("Invalid option is given: %c", result); break; default : break; } } return 0; } Öte yandan fonksiyon, geçersiz bir seçenek girildiğide, "?" ile geri döneceğinden bahsetmiştik. Pekiyi böylesi bir senaryoda işler nasıl ilerlemektedir? Aşağıdaki örneğin inceleyelim: * Örnek 1, #include #include int main(int argc, char** argv) { /* # Command Line Arguments # -a -b Ahmet -c -D */ /* # OUTPUT # a is given b is given c is given ./a.out: invalid option -- 'D' Invalid option is given: ? */ const char* options = "ab:c"; int result; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 'a' : printf("%c is given\n", result); break; case 'b' : printf("%c is given\n", result); break; case 'c' : printf("%c is given\n", result); break; case '?' : printf("Invalid option is given: %c", result); break; default : break; } } return 0; } Çıktıdan da görüleceği üzere hem bizim "case '?'" ile belirttiğimiz hata mesajı ekrana yazıldı hem de sistemin kendi oluşturduğu hata mesajı. Eğer bu hata mesajlarının yazılmasını biz üzerimize almak istiyorsak, işin başında "opterr" isimli değişkenin değerini sıfıra çekmeliyiz. Böylelikle artık sistem hata mesajı oluşturmayacaktır. İlgili değişkenin "extern" bildirimi ilgili başlık dosyalarının içerisinde yapılmıştır. * Örnek 1, #include #include int main(int argc, char** argv) { /* # Command Line Arguments # -a -b Ahmet -c -D */ /* # OUTPUT # a is given b is given c is given Invalid option is given: ? */ opterr = 0; // Artık hata mesajlarının işlenmesi bizim sorumluluğumuzda. const char* options = "ab:c"; int result; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 'a' : printf("%c is given\n", result); break; case 'b' : printf("%c is given\n", result); break; case 'c' : printf("%c is given\n", result); break; case '?' : printf("Invalid option is given: %c", result); break; default : break; } } return 0; } Anımsayacağınız üzere fonksiyonun geri dönüş değeri "?" biçimindeydi eğer argümanlı bir seçeneğin argümanı girilmemişse veya belirtilmeyen bir seçenek girilmişse. Pekiyi bizler bunu nasıl ayırt edeceğiz? İşte burada devreye "optopt" isimli değişken girmektedir. Yine bu değişkenin de "extern" bildirimi ilgili başlık dosyasında yapılmıştır. Fonksiyon "?" ile geri dönerse "optopt" değişkeninin değeri iş bu sorunlu seçenek olacaktır. * Örnek 1, #include #include int main(int argc, char** argv) { /* # Command Line Arguments # -a -c -D -b */ /* # OUTPUT # a is given c is given Unknown option: D b option is given w/o an argument. */ opterr = 0; // Artık hata mesajlarının işlenmesi bizim sorumluluğumuzda. const char* options = "ab:c"; int result; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 'a' : printf("%c is given\n", result); break; case 'b' : printf("%c is given\n", result); break; case 'c' : printf("%c is given\n", result); break; case '?' : { if(optopt == 'b') fprintf(stderr, "%c option is given w/o an argument.\n", optopt); else fprintf(stderr, "Unknown option: %c\n", optopt); break; } default : break; } } return 0; } Pekiyi argümanlı seçeneklerin argümanlarını bizler nasıl "parse" edeceğiz? Bu durumda da devreye "optarg" isimli gösterici girmektedir. İlgili seçeneğin bulunduğu noktada bu gösterici ilgili argümanı gösterecektir. Döngünün diğer turunda ise eğer yeni bir argümanlı seçenek bulursa, onun argümanını gösterecektir. Buradan hareketle bizler her argümanlı seçenek için başka göstericiler de oluşturmalıyız ki ilgili argümanların değerlerini saklayabilelim. Çünkü "optarg" nin değeri döngünün her turunda değişme ihtimaline sahiptir. Yine bu değişkenin de bildirimi ilgili başlık dosyası içerisinde yapılmıştır. * Örnek 1, Aşağıdaki örnekte argümanlı seçeneğin argümanı saklanmamıştır. #include #include int main(int argc, char** argv) { /* # Command Line Arguments # -a -c -D -b Ahmet */ /* # OUTPUT # a is given c is given Unknown option: D b is given with an argument Ahmet */ opterr = 0; // Artık hata mesajlarının işlenmesi bizim sorumluluğumuzda. const char* options = "ab:c"; int result; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 'a' : printf("%c is given\n", result); break; case 'b' : printf("%c is given with an argument %s\n", result, optarg); break; case 'c' : printf("%c is given\n", result); break; case '?' : { if(optopt == 'b') fprintf(stderr, "%c option is given w/o an argument.\n", optopt); else fprintf(stderr, "Unknown option: %c\n", optopt); break; } default : break; } } return 0; } Şimdi geriye seçeneksiz argümanların değerlendirilmesi kaldı. Anımsayacağınız üzere bu argümanlar "getopt" fonksiyonunun ikinci parametresindeki dizinin sonuna doğru ötelenmektedir. İşte bu argümanların başladığı indis, "optind" isimli değişkende saklanır. "optind" ile dizinin sonundaki bölüm seçeneksiz argümanların bulunduğu yer olacaktır. Yine bu değişkenin de "extern" bildirimi ilgili başlık dosyası içerisinde yapılmıştır. * Örnek 1, #include #include int main(int argc, char** argv) { /* # Command Line Arguments # Kandemir -a -c Pehlivanli -D -b Ahmet */ /* # OUTPUT # a is given c is given Unknown option: D b is given with an argument Ahmet Arguments w/o options are: Kandemir,Pehlivanli */ opterr = 0; // Artık hata mesajlarının işlenmesi bizim sorumluluğumuzda. const char* options = "ab:c"; int result; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 'a' : printf("%c is given\n", result); break; case 'b' : printf("%c is given with an argument %s\n", result, optarg); break; case 'c' : printf("%c is given\n", result); break; case '?' : { if(optopt == 'b') fprintf(stderr, "%c option is given w/o an argument.\n", optopt); else fprintf(stderr, "Unknown option: %c\n", optopt); break; } default : break; } } if(optind != argc) printf("Arguments w/o options are: "); for(int i = optind; i < argc; ++i) printf("%s%s", argv[i], i == argc - 1 ? " " : ","); return 0; } Bizlerin buradaki asıl amacı, girilecek olan seçenekleri işlemektir. Bunun için şöyle bir kalıp önerilmektedir; her seçenek için bir "flag" değişken ve her argümanlı seçenek için ayrı bir gösterici belirlenir. Daha sonra ilgili "while" döngüsünde bu değişkenlere uygun değerleri atayıp, "while" döngüsünden sonra da bu "flag" ve göstericileri kullanarak gereken işlemleri gerçekleştirmek. Aşağıda bu kalıp için kapsayıcı bir örnek verilmiştir. * Örnek 1, #include #include int main(int argc, char** argv) { /* # Command Line Arguments # Kandemir -a -c Pehlivanli -D -b Ahmet */ /* # OUTPUT # Unknown option: D -a option is given... -b option is given with argument: Ahmet -c option is given... Arguments w/o options are: Kandemir,Pehlivanli */ int a_flag, b_flag, c_flag; char* b_arg; const char* options = "ab:c"; int result; a_flag = b_flag = c_flag = 0; opterr = 0; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 'a' : a_flag = 1; break; case 'b' : b_flag = 1; b_arg = optarg; break; case 'c' : c_flag = 1; break; case '?' : { if(optopt == 'b') fprintf(stderr, "%c option is given w/o an argument.\n", optopt); else fprintf(stderr, "Unknown option: %c\n", optopt); break; } default : break; } } if(a_flag) printf("-a option is given...\n"); if(b_flag) printf("-b option is given with argument: %s\n", b_arg); if(c_flag) printf("-c option is given...\n"); if(optind != argc) printf("Arguments w/o options are: "); for(int i = optind; i < argc; ++i) printf("%s%s", argv[i], i == argc - 1 ? " " : ","); return 0; } /*================================================================================================================================*/ (07_18_06_2023) > Programın Komut Satırı Argümanları (devam): >> UNIX/Linux dünyasında komut satırı argümanlarını "parse" etmek için "getopt" fonksiyonu kullanılır. POSIX standartları, uzun seçenekleri desteklemediği için, bu fonksiyon da sadece kısa seçenekleri desteklemektedir. Uzun seçenekleri "parse" işlemi için "getopt_long" isimli fonksiyonu kullanacağız. >>> "getopt" fonksiyonu (devam): Şimdi de bu fonksiyonu kullanarak, yukarıdaki örneğin daha gelişmiş bir versiyonunu yazalım. * Örnek 1, Aşağıdaki örnekte "-t", "-o" ve "-x" seçeneklerinden yalnızca birisini kullanabiliriz. "-n" seçeneği argümanlı bir seçenek olup, varsayılan argümanı ise "DEFAULT_LINE_CHAR" değerindedir ve sadece "-o" ve "-x" seçenekleri ile birlikte kullanılabilir. Öte yandan hiç seçenek kullanmazsak, varsayılan seçenek olarak "-t" seçeneği kullanılacaktır. Son olarak bir dosya ismi de komut satırı argümanı olarak fonksiyona geçilmelidir ki bu dosya ismi seçeneksiz argüman olarak değerlendirilecektir. #include #include #include #include #define DEFAULT_LINE_CHAR 16 void disp_text(FILE* f); void disp_hex(FILE* f, int n_arg); void disp_octal(FILE* f, int n_arg); int check_number(const char* str); int main(int argc, char** argv) { /* # Command Line Arguments # [-t -o -x -n ] default: -t, -n 16 */ int t_flag, o_flag, x_flag, n_flag, err_flag; int n_arg = DEFAULT_LINE_CHAR; const char* options = "toxn:"; int result; opterr = t_flag = o_flag = x_flag = n_flag = err_flag = 0; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 't' : t_flag = 1; break; case 'o' : o_flag = 1; break; case 'x' : x_flag = 1; break; case 'n' : { n_flag = 1; if((n_arg = check_number(optarg)) < 0) { fprintf(stderr, "-n argument is invalid!...\n"); err_flag = 1; } break; } case '?' : { err_flag = 1; if(optopt == 'n') fprintf(stderr, "%c option is given w/o an argument.\n", optopt); else fprintf(stderr, "Unknown option: %c\n", optopt); break; } } } if(err_flag) exit(EXIT_FAILURE); if(t_flag && n_flag) { fprintf(stderr, "-t cannot be used with -n!...\n"); exit(EXIT_FAILURE); } if(t_flag + o_flag + x_flag > 1) { fprintf(stderr, "only one of -[tox] can be used!...\n"); exit(EXIT_FAILURE); } if(t_flag + o_flag + x_flag == 0) t_flag = 1; if(argc - optind == 0) { fprintf(stderr, "A file name must be specified!...\n"); exit(EXIT_FAILURE); } FILE* f; if((f = fopen(argv[optind], t_flag ? "r" : "rb")) == NULL) { fprintf(stderr, "cannot open file: %s\n", argv[optind]); exit(EXIT_FAILURE); } if(t_flag) disp_text(f); else if(x_flag) disp_hex(f, n_arg); else if(o_flag) disp_octal(f, n_arg); fclose(f); return 0; } void disp_text(FILE* f) { fprintf(stdout, "\n"); int ch; while((ch = fgetc(f)) != EOF) putchar(ch); puts(""); if(ferror(f)) { fprintf(stderr, "file read could not be completed!...\n"); exit(EXIT_FAILURE); } else if(feof(f)) { fprintf(stdout, "\n"); return; } else { fprintf(stderr, "Something has happened!...\n"); exit(EXIT_FAILURE); } } void disp_hex(FILE* f, int n_arg) { fprintf(stdout, "\n"); size_t i; int ch; for(i = 0;(ch = fgetc(f)) != EOF; ++i) { if(i % n_arg == 0) { if(i != 0) putchar('\n'); printf("%08zX ", i); } printf("%02X ",ch); } puts(""); if(ferror(f)) { fprintf(stderr, "file read could not be completed!...\n"); exit(EXIT_FAILURE); } else if(feof(f)) { fprintf(stdout, "\n"); return; } else { fprintf(stderr, "Something has happened!...\n"); exit(EXIT_FAILURE); } } void disp_octal(FILE* f, int n_arg) { fprintf(stdout, "\n"); size_t i; int ch; for(i = 0;(ch = fgetc(f)) != EOF; ++i) { if(i % n_arg == 0) printf("%08zo ", i); printf("%03o ",ch); if(i % n_arg == n_arg - 1) putchar('\n'); } puts(""); if(ferror(f)) { fprintf(stderr, "file read could not be completed!...\n"); exit(EXIT_FAILURE); } else if(feof(f)) { fprintf(stdout, "\n"); return; } else { fprintf(stderr, "Something has happened!...\n"); exit(EXIT_FAILURE); } } int check_number(const char* str) { const char* temp; while(isspace(*str)) ++str; temp = str; while(isdigit(*str)) ++str; if(*str != '\0') return -1; int result; if((result = atoi(temp)) == 0) return -1; return result; } * Örnek 2, Aşağıdaki örnek ise yukarıdaki örneğin biraz daha düzenlenmiş halidir: #include #include #include #include #define DEFAULT_LINE_CHAR 16 #define HEX_FORM 16 #define OCT_FORM 8 void disp_text(FILE* f); void disp_hex_octal(FILE* f, int n_arg, int base); int check_number(const char* str); int main(int argc, char** argv) { /* # Command Line Arguments # [-t -o -x -n ] default: -t, -n 16 */ /* # OUTPUT # */ int t_flag, o_flag, x_flag, n_flag, err_flag; int n_arg = DEFAULT_LINE_CHAR; const char* options = "toxn:"; int result; opterr = t_flag = o_flag = x_flag = n_flag = err_flag = 0; while((result = getopt(argc, argv, options)) != -1) { switch(result) { case 't' : t_flag = 1; break; case 'o' : o_flag = 1; break; case 'x' : x_flag = 1; break; case 'n' : { n_flag = 1; if((n_arg = check_number(optarg)) < 0) { fprintf(stderr, "-n argument is invalid!...\n"); err_flag = 1; } break; } case '?' : { err_flag = 1; if(optopt == 'n') fprintf(stderr, "%c option is given w/o an argument.\n", optopt); else fprintf(stderr, "Unknown option: %c\n", optopt); break; } } } if(err_flag) exit(EXIT_FAILURE); if(t_flag && n_flag) { fprintf(stderr, "-t cannot be used with -n!...\n"); exit(EXIT_FAILURE); } if(t_flag + o_flag + x_flag > 1) { fprintf(stderr, "only one of -[tox] can be used!...\n"); exit(EXIT_FAILURE); } if(t_flag + o_flag + x_flag == 0) t_flag = 1; if(argc - optind == 0) { fprintf(stderr, "A file name must be specified!...\n"); exit(EXIT_FAILURE); } FILE* f; if((f = fopen(argv[optind], t_flag ? "r" : "rb")) == NULL) { fprintf(stderr, "cannot open file: %s\n", argv[optind]); exit(EXIT_FAILURE); } if(t_flag) disp_text(f); else if(x_flag) disp_hex_octal(f, n_arg, HEX_FORM); else if(o_flag) disp_hex_octal(f, n_arg, OCT_FORM); fclose(f); return 0; } void disp_text(FILE* f) { fprintf(stdout, "\n"); int ch; while((ch = fgetc(f)) != EOF) putchar(ch); puts(""); if(ferror(f)) { fprintf(stderr, "file read could not be completed!...\n"); exit(EXIT_FAILURE); } else if(feof(f)) { fprintf(stdout, "\n"); return; } else { fprintf(stderr, "Something has happened!...\n"); exit(EXIT_FAILURE); } } void disp_hex_octal(FILE* f, int n_arg, int base) { fprintf(stdout, "\n"); size_t i; int ch; for(i = 0;(ch = fgetc(f)) != EOF; ++i) { if(i % n_arg == 0) { if(i != 0) putchar('\n'); printf(base == HEX_FORM ? "%08zX " : "%08zo ", i); } base == HEX_FORM ? printf("%02X ",ch) : printf("%03o ",ch); } puts(""); if(ferror(f)) { fprintf(stderr, "file read could not be completed!...\n"); exit(EXIT_FAILURE); } else if(feof(f)) { fprintf(stdout, "\n"); return; } else { fprintf(stderr, "Something has happened!...\n"); exit(EXIT_FAILURE); } } int check_number(const char* str) { const char* temp; while(isspace(*str)) ++str; temp = str; while(isdigit(*str)) ++str; if(*str != '\0') return -1; int result; if((result = atoi(temp)) == 0) return -1; return result; } > Homework - 1, Çözüm: Cümleyi kelimelerine ayırtmak. * Örnek 1, Aşağıdaki örnekte büyütme birer birer yapılmaktadır. Sadece cümlenin kendisi için dinamik bellek yönetimi uygulanmıştır. #include #include #include #define CHUNK_SIZE 5 char** split(char* str, const char* delim); int main(void) { char names[] = "Ahmet Kandemir Pehlivanli, Ulya Yürük"; char** strs; int i; if((strs = split(names, ", ")) == NULL) { fprintf(stderr, "cannot split!...\n"); exit(EXIT_FAILURE); } for(i = 0; strs[i] != NULL; ++i) { puts(strs[i]); } free(strs); return 0; } char** split(char* str, const char* delim) { char* pos; char** strs, **temp; size_t count = 0; if((strs = (char**)malloc(sizeof(char*))) == NULL) return NULL; for(pos = strtok(str, delim); pos != NULL; pos = strtok(NULL, delim)) { strs[count++] = pos; temp = (char**)realloc(strs, (count + 1) * sizeof(char*)); if(temp == NULL){ free(strs); return NULL; } strs = temp; } strs[count] = NULL; return strs; } * Örnek 2, Aşağıdaki örnekte "CHUNK_SIZE" kadarlık arttırma yapılmıştır. Sadece cümlenin kendisi için dinamik bellek yönetimi uygulanmıştır. #include #include #include #define CHUNK_SIZE 5 char** split(char* str, const char* delim); int main(void) { char names[] = "Ahmet Kandemir Pehlivanli, Ulya Yürük"; char** strs; int i; if((strs = split(names, ", ")) == NULL) { fprintf(stderr, "cannot split!...\n"); exit(EXIT_FAILURE); } for(i = 0; strs[i] != NULL; ++i) { puts(strs[i]); } free(strs); return 0; } char** split(char* str, const char* delim) { char* pos; char** strs, **temp; size_t count = 0; if((strs = (char**)malloc(sizeof(char*) * CHUNK_SIZE)) == NULL) return NULL; for(pos = strtok(str, delim); pos != NULL; pos = strtok(NULL, delim)) { strs[count++] = pos; if(count % CHUNK_SIZE == 0){ temp = (char**)realloc(strs, (count + CHUNK_SIZE) * sizeof(char*)); if(temp == NULL){ free(strs); return NULL; } strs = temp; } } strs[count] = NULL; return strs; } * Örnek 3, Aşağıdaki örnekte ise cümlenin kendisi ve her bir kelimesi için dinamik bellek yönetimi uygulanmıştır. #include #include #include #define CHUNK_SIZE 5 char** split2(const char* str, const char* delim); int main(void) { char names[] = "Ahmet Kandemir Pehlivanli, Ulya Yürük"; char** strs; int i; if((strs = split2(names, ", ")) == NULL) { fprintf(stderr, "cannot split!...\n"); exit(EXIT_FAILURE); } for(i = 0; strs[i] != NULL; ++i) { puts(strs[i]); } for(i = 0; strs[i] != NULL; ++i) { free(strs[i]); } free(strs); return 0; } char** split2(const char* str, const char* delim) { char* pos, *strbuf; char** strs, **temp; size_t count = 0; /* Dinamik bellek yönetimi + ilgili yazının kopyalanması. */ if((strbuf = strdup(str)) == NULL) return NULL; if((strs = (char**)malloc(sizeof(char*) * CHUNK_SIZE)) == NULL) return NULL; for(pos = strtok(strbuf, delim); pos != NULL; pos = strtok(NULL, delim)) { if((strs[count++] = strdup(pos)) == NULL){ free(strbuf); return NULL; } if(count % CHUNK_SIZE == 0){ temp = (char**)realloc(strs, (count + CHUNK_SIZE) * sizeof(char*)); if(temp == NULL){ free(strbuf); free(strs); return NULL; } strs = temp; } } strs[count] = NULL; free(strbuf); return strs; } > Hatırlatıcı Notlar: >> "strdup" fonksiyonunun temsili implementasyonu: * Örnek 1, char* my_strdup(const char* str) { char* strbuf; if((strbuf = (char*)malloc(strlen(str) + 1)) == NULL) return NULL; return strcpy(strbuf, str); } /*================================================================================================================================*/ (08_01_07_2023) > Programın Komut Satırı Argümanları (devam): >> UNIX/Linux dünyasında komut satırı argümanlarını "parse" etmek için "getopt" fonksiyonu kullanılır. POSIX standartları, uzun seçenekleri desteklemediği için, bu fonksiyon da sadece kısa seçenekleri desteklemektedir. Uzun seçenekleri "parse" işlemi için "getopt_long" isimli fonksiyonu kullanacağız. >>> "getopt_long" fonksiyonu: Komut satırı argümanlarının uzun seçenekleri için çağrılan fonksiyondur. POSIX standart fonksiyonu değildir. İşlevsel olarak "getopt" fonksiyonunu kapsamasına karşın, kullanım açısından biraz daha karmaşıktır. Fonksiyonun prototipi şu şekildedir: #include int getopt_long( int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex ); Fonksiyonun ilk iki parametresi "main" fonksiyonundan geçirilecek olan argümanlardır. Üçüncü parametre ise kısa seçeneklerin geçildiği yazıdır. Fonksiyonun dördüncü parametresi ise elemanları "struct option" türünden olan bir dizinin başlangıç adresini istemektedir. İş bu yapı türü "getopt.h" başlık dosyasında aşağıdaki gibi tanımlanmıştır: struct option { const char *name; /* Uzun seçeneğin ismi */ int has_arg; /* Uzun seçeneğin argüman alıp almadığı bilgisi */ int *flag; int val; }; Bu adrese geçilen dizinin son elemanı "NULL" değerinde OLMALIDIR. Böylelikle ilgili dizinin eleman sayısı belirlenebilsin. "struct option" yapısının, >>>> "name" isimli elemanı, uzun seçeneğin ismini belirtmektedir. >>>> "has_arg" isimli elemanı, uzun seçeneğin argüman alıp almadığını belirtmektedir. Bu eleman "no_argument", "required_argument" veya "optional_argument" değerlerinden birisini almalıdır. Aslında bu değerler sırasıyla "0", "1" ve "2" rakamlarının "typedef" edilmiş halleridir. >>>> "flag" isimli eleman, ilgili uzun seçeneğin bulunması durumunda "getopt_long" fonksiyonunun hangi değer ile döneceğini belirtmektedir. Eğer bu elemana bir adres geçilirse ve ilgili uzun seçenek bulunursa, "struct option" yapısının "val" isimli elemanına geçilen değer bu elemana geçilen adrese yazılacak. "getopt_long" fonksiyonu da "0" ile geri dönecektir. Eğer bu eleman "NULL" değeri alırsa, ilgili uzun seçeneğin bulunması durumunda, "getopt_long" fonksiyonu "val" isimli elemana geçilen değer ile geri dönecektir. >>>> "val" elemanı ise "getopt_long" fonksiyonunun geri dönüş değeri ile ilişkilidir. Fonksiyonun beşinci parametresi ise ilgili uzun seçeneğin, dördüncü parametresine geçilen dizinin kaçıncı elemanı olduğu bilgi ile ilgilidir. İlgili indeks değeri, bu parametreye geçilen adrese yazılmaktadır. Fakat çok nadir kullanılan bir parametredir, dolayısıyla genelde "NULL" değeri geçilir. Eğer "getopt_long" fonksiyonunun dördüncü ve beşinci parametrelerine "NULL" değerini geçersek, artık "getopt" fonksiyonu gibi çalışacaktır. Yine "getopt_long" fonksiyonu da bir döngü içerisinde çağırmalıyız, tıpkı "getopt" fonksiyonunu çağırdığımız gibi. "getopt_long" fonksiyonunun geri dönüş değeri seçeneğin kısa ya da uzun olmasına göre değişkenlik göstermektedir. Şöyleki; >>>> Kısa seçenek bulmuşsa, kısa seçeneğin karakter kodu ile geri döner. >>>> Uzun seçenek bulmuş ve "struct option" yapısının "flag" parametresinde "NULL" değeri vardır, "struct option" yapısının "val" parametresindeki değer ile geri döner. >>>> Uzun seçenek bulmuş ve "struct option" yapısının "flag" parametresinde "NULL" değeri yoktur. Bu durumda "struct option" yapısının "val" parametresindeki değer, "flag" parametresindeki adrese yazılır. "getopt_long" fonksiyonu ise "0" ile geri döner. >>>> Fonksiyon olmayan bir kısa yada uzun seçenek ile karşılaşmıştır veya argümanlı bir kısa seçenek yada argümanlı uzun seçeneğin argümanı girilmemiştir. İşte bu durumda "?" ile geri dönmektedir. >>>>> Argümanlı bir kısa seçeneğin argümanı girilmediğinde, o kısa seçeneğin ASCII karşılığını "optopt" değişkeni alır. >>>>> Argümanlı bir uzun seçeneğin argümanı girilmediğinde, "struct option" yapısının "val" isimli elemanı "optopt" değişkenine atanır. >>>>> Geçersiz bir kısa seçenek ile karşılaşılmışsa, "optopt" geçersiz kısa seçeneğin karakter karşılığını alır. >>>>> Geçersiz bir uzun seçenek ile karşılaşılmışsa, "optopt" değişkenine "0" değeri atanır. Fakat bu geçersiz uzun seçeneğin ne olduğunu bize VERMEMEKTEDİR. Fakat GNU'nun "getopt_long" fonksiyonu incelendiğinde, iş bu geçersiz uzun seçeneğe "main" dizisine geçilen "argv" dizisinin "optind - 1" indeksinde görülmektedir. Fakat bu ilgili standartlarda BELİRTİLMEMİŞTİR. >>>> Eğer "parse" edecek bir argüman kalmamışsa "-1" ile geri dönecektir. * Örnek 1, #include #include #include int main(int argc, char** argv) { /* # Command Line Arguments # -a -b -c 500 --length */ /* # OUTPUT # --length is called -a option is used... -b option is used... -c or --count option is used with [500]... --length is used... */ /* * "count" seçeneği bulunursa, 'c' karakteri "count_flag" değişkeni * içerisine yerleştirilecek ve "getopt_long" fonksiyonu "0" ile geri * dönecektir. * "length" seçeneği bulunursa, "getopt_long" fonksiyonu 'l' değeri * ile geri dönecektir. */ int count_flag = 0; struct option long_options[] = { {"length", no_argument, &count_flag, 'l'}, {"count", required_argument, NULL, 'c'}, {0, 0, 0, 0} }; const char* short_options = "abc:"; int a_flag, b_flag, c_flag, l_flag, error_flag; a_flag = b_flag = c_flag = l_flag = error_flag = 0; char* c_arg; opterr = 0; int result; while((result = getopt_long(argc, argv, short_options, long_options, NULL)) != -1){ switch(result){ case 'a' : a_flag = 1; break; case 'b' : b_flag = 1; break; case 'c' : /* * "count" uzun seçeneği veya "c" kısa seçeneği bulunursa, * ilgili fonksiyon "c" değeri ile geri dönecektir. Böylelikle * bir uzun seçenek ile bir kısa seçeneği birbirinin alternatifi * haline getirmiş olduk. */ c_flag = 1; c_arg = optarg; break; case 0 : printf("--length is called\n"); break; case '?' : { error_flag = 1; if(optopt == 'c') fprintf(stderr, "-c or --count is used w/o an argument!...\n"); else if(optopt == 0) fprintf(stderr, "invalid long option!...\n"); else if(optopt != 0) fprintf(stderr, "invalid short option: -%c\n", optopt); else fprintf(stderr, "FATAL ERROR!...\n"); exit(EXIT_FAILURE); } } } if(error_flag) exit(EXIT_FAILURE); if(a_flag) printf("-a option is used...\n"); if(b_flag) printf("-b option is used...\n"); if(c_flag) printf("-c or --count option is used with [%s]...\n", c_arg); if(count_flag) printf("--length is used...\n"); if(optind != argc) puts("Arguments w/o Options:"); for(int i = optind; i < argc; ++i) printf("%s\n", argv[i]); return 0; } * Örnek 2, Bu örnekte ise bir yada birden fazla dosyanın içeriği görüntülenebilir. #include #include #include #include #define DEF_LINE 10 #define HEX_OCTAL_LINE_LEN 16 int print_text(FILE *f, int nline); int print_hex_octal(FILE *f, int nline, int hexflag); int main(int argc, char *argv[]) { int result, err_flag = 0; int x_flag = 0, o_flag = 0, t_flag = 0, top_flag = 0, header_flag = 0; char *top_arg; struct option options[] = { {"top", optional_argument, NULL, 1}, {"header", no_argument, NULL, 'h'}, {0, 0, 0, 0} }; FILE *f; int i, nline = -1; opterr = 0; while ((result = getopt_long(argc, argv, "xoth", options, NULL)) != -1) { switch (result) { case 'x': x_flag = 1; break; case 'o': o_flag = 1; break; case 't': t_flag = 1; break; case 'h': header_flag = 1; break; case 1: top_flag = 1; top_arg = optarg; break; case '?': if (optopt != 0) fprintf(stderr, "invalid switch: -%c\n", optopt); else fprintf(stderr, "invalid switch: %s\n", argv[optind - 1]); /* argv[optind - 1] dokümante edilmemiş */ err_flag = 1; } } if (err_flag) exit(EXIT_FAILURE); if (x_flag + o_flag + t_flag > 1) { fprintf(stderr, "only one option must be specified from -o, -t, -x\n"); exit(EXIT_FAILURE); } if (x_flag + o_flag + t_flag == 0) t_flag = 1; if (top_flag) nline = top_arg != NULL ? (int)strtol(top_arg, NULL, 10) : DEF_LINE; if (optind == argc) { fprintf(stderr, "at least one file must be specified!..\n"); exit(EXIT_FAILURE); } for (i = optind; i < argc; ++i) { if ((f = fopen(argv[i], "rb")) == NULL) { fprintf(stderr, "cannot open file: %s\n", argv[i]); continue; } if (header_flag) printf("%s\n\n", argv[i]); if (t_flag) result = print_text(f, nline); else if (x_flag) result = print_hex_octal(f, nline, 1); else result = print_hex_octal(f, nline, 0); if (i != argc - 1) putchar('\n'); if (!result) fprintf(stderr, "cannot read file: %s\n", argv[i]); fclose(f); } return 0; } int print_text(FILE *f, int nline) { int ch; int count; if (nline == -1) while ((ch = fgetc(f)) != EOF) putchar(ch); else { count = 0; while ((ch = fgetc(f)) != EOF && count < nline) { putchar(ch); if (ch == '\n') ++count; } } return !ferror(f); } int print_hex_octal(FILE *f, int nline, int hexflag) { int ch, i, count; const char *off_str, *ch_str; off_str = hexflag ? "%07X " : "%012o"; ch_str = hexflag ? "%02X%c" : "%03o%c"; if (nline == -1) for (i = 0; (ch = fgetc(f)) != EOF; ++i) { if (i % HEX_OCTAL_LINE_LEN == 0) printf(off_str, i); printf(ch_str, ch, i % HEX_OCTAL_LINE_LEN == HEX_OCTAL_LINE_LEN - 1 ? '\n' : ' '); } else { count = 0; for (i = 0; (ch = fgetc(f)) != EOF && count < nline; ++i) { if (i % HEX_OCTAL_LINE_LEN == 0) printf(off_str, i); printf(ch_str, ch, i % HEX_OCTAL_LINE_LEN == HEX_OCTAL_LINE_LEN - 1 ? '\n' : ' '); if (ch == '\n') ++count; } } if (i % HEX_OCTAL_LINE_LEN != 0) putchar('\n'); return !ferror(f); } > Standart C fonksiyonları, POSIX fonksiyonları ve Windows API fonksiyonları: Standart C fonksiyonları demek bütün C derleyicilerinde bulunması gereken fonksiyonlar demektir. POSIX fonksiyonları ise bütün UNIX türevi sistemlerde bulunması gereken fonksiyonlarıdır. Dolayısıyla bu fonksiyonları Windows ailesinde kullanamayız. Windows API fonksiyonları ise Windows işletim sistemine özgü olan fonksiyonlardır. Bu fonksiyonları da UNIX türevi sistemlerde kullanamayız. Dolayısıyla bu üç fonksiyonlar arasından taşınabilirliği en çok olan Standart C fonksiyonlarıdır çünkü hem Windows ailesinde hem de UNIX türevi sistemlerde kullanılabilir. Daha sonra POSIX ve Windows API fonksiyonları gelir ki bu iki fonksiyon ailesi aslında birbiri ile aynı kotadadır, yani her biri kendi ailesine özgüdür. En sonunda da işletim sisteminin sistem fonksiyonları gelir ki bu fonksiyonlar aslında işletim sistemine özgüdür. Hatta aynı işletim sisteminin farklı versiyonları arasında değişiklik bile görülebilir. >> Sistem Fonksiyonları: Sistem fonksiyonları aslında o işletim sisteminin çekirdeğinde yer alır. Bazıları dışarıdan çağrılabilir durumdayken, bazılarını dışarıdan çağıramayız. Dolayısıyla iş bu sistem fonksiyonları, işletim sistemi ile geliştirilen uygulama arasında bir köprü vazifesi görmektedir. Örneğin, dosya açma işlemini ele alalım. İster C#, ister Java, ister C++ ile bir dosya açmak isteyelim. Eninde sonunda bu dillerdeki ilgili fonksiyonlar, işletim sisteminin sistem fonksiyonlarını çağırmaktadır. Çağrılan bu sistem fonksiyonları ilgili dosyayı açmaktadır. Bu iş işletim sisteminin sorumluluğunda, programlama dilleri ise bir nevi "wrapper" olarak işlev görmektedir. Fakat bu demek değildir ki o programlama dilindeki bütün fonksiyonlar aslında bir sistem fonksiyonunu çağırır. Örneğin, bir yazının uzunluğunu bulan bir fonksiyon. Böyle bir fonksiyonun sistem fonksiyonu ile işi yoktur. Özetle sistem fonksiyonlarını çağırmak, işletim sistemine, o işin yapılması için rica etmek demektir. >> POSIX Fonksiyonları: UNIX türevi sistemlerde ortak arayüz oluşturan fonksiyonlardır. Seviye olarak sistem fonksiyonlarından yüksektir. Örneğin, "open" fonksiyonunu ele alalım. Bu fonksiyon Linux sistemlerinde o işletim sistemindeki sistem fonksiyonlarını çağırırken, macOS sistemlerde ise o işletim sistemindeki sistem fonksiyonlarını çağırmaktadır. Böylelikle UNIX türevi sistemlerde bir taşınabilirlik sağlanmış oluyor. Bazı POSIX fonksiyonları hiç sistem fonksiyonu çağırmazken, bazıları birden fazla sistem fonksiyonu çağırabilir. >> Windows API Fonksiyonları: POSIX fonksiyonlarının Windows ailesindeki karşılığıdır, diyebiliriz. POSIX fonksiyonlarından farklılıkları vardır fakat amaçları Windows ailesi arasında arayüz oluşturmaktır. Yine POSIX fonksiyonlarında olduğu gibi bazıları sistem fonksiyonu çağırmazken, bazıları birden fazlasını çağırmaktadır. >> Standart C Fonksiyonları: Her C derleyicisinde bulunan fonksiyonlardır. UNIX türevi sistemlerde POSIX fonksiyonlarını çağırırken, Windows ailesinde ise Windows API fonksiyonlarını çağırmaktadır. Örneğin, "fopen" isimli Standart C fonksiyonu UNIX türevi sistemlerde "open" isimli POSIX fonksiyonunu çağırırken, Windows ailesinde "CreateFile" isimli Windows API fonksiyonunu çağırmaktadır. Bu fonksiyonlarda ilgili sistem fonksiyonlarını çağırmaktadır. Tabii bu demek değildir ki Standart C fonksiyonları sadece bu çağrıları yapmaktadır; yine ek işlerde yapmaktadır. > Hatırlatıcı Notlar: >> Eğer isteğe bağlı argümanlı bir uzun seçenek "getopt_long" ile bulunmuşsa fakat bu seçenek için argüman geçilmemişse, "optarg" değişkeni "NULL" değerini alacaktır. /*================================================================================================================================*/ (09_02_07_2023) > Standart C fonksiyonları, POSIX fonksiyonları ve Windows API fonksiyonları (devam): Eğer işimizi Standart C fonksiyonları görüyorsa, o fonksiyonları kullanmalıyız. Eğer yeterli gelmiyorsa Windows için Windows API, UNIX için POSIX fonksiyonlarını kullanmalıyız. Eğer bunlar da yeterli gelmiyorsa, ilgili işletim sisteminin sistem fonksiyonlarını çağırmalıyız. Örneğin, bir oluşturmak isteyelim. Standar C fonksiyonlarından işimizi görecek bir fonksiyon yoktur. Dolayısıyla Windows ailesinde "CreateDirectory", UNIX dünyasında ise "mkdir" isimli fonksiyonu çağırmalıyız. Şimdi de bu Windows API ve POSIX fonksiyonlarını sırasıyla inceleyelim: >> Windows API Fonksiyonları Hakkında Temel Bilgiler: Bu fonksiyonların çok büyük çoğunluğu "windows.h" isimli başlık dosyasını projemize dahil etmeliyiz. Windows dünyasında dosya isimlerinin büyük harf duyarlılığı yoktur. Yani işleme sokulan "test.txt" ile "TesT.txt" dosyaları aynı dosya kabul edilir. Sadece dosya ismini saklarken büyük harfleri de kullanır. Yani bir dosyaya "TEST.txt" biçiminde isim versek, "TEST.txt" biçiminde saklanır. Dolayısıyla "Windows.h" ile "windows.h" arasında bir fark yoktur. Öte yandan bu fonksiyonlar hakkında bilgi almak için "msdn" sisteminden destek alabiliriz. Öte yandan yazı parametresi alan bazı Windwows API fonksiyonları "xxxA" ve "xxxW" biçimindedir. Bizler bu tip fonksiyonları kullanırken "xxx" biçiminde kullanacağız fakat ön işlemci programı bu fonksiyon çağrısını "xxxA" veya "xxxW" haline çevirmektedir. Sonu "A" ile bitenler "ASCII" karakter kodu isterken, sonu "W" ile bitenler "UNICODE" istemektedir. Ön işlemci programının hangisine dönüştürmesini istiyorsak, projemizin "Properties" kısmına gelip "Configuration Properties/Advanced" bölümündeki "Character Set" isimli özelliği "Not Set" olarak değiştirmeliyiz. Böylelikle "ASCII" isteyen versiyona çevirecektir. Burada dikkat etmemiz gereken şey "xxx" isminde bir fonksiyonun olmadığı, sadece kullanım olarak "xxx" biçimiyle kullandığımızdır. Ön işlemci programı yukarıdaki ayara göre "xxxA" veya "xxxW" haline getirmektedir. Diğer yandan Windows ailesi arasında taşınabilirliği daha arttırmak ve okunabilirliği kolaylaştırmak adına Microsoft firması çeşitli "typedef" isimler oluşturmuştur. Bu isimlere aşina olunmalıdır. Bu tür isimlerin hepsi BÜYÜK HARFLERLE ile oluşturulmuştur. Dolayısıyla kendi fonksiyonlarımızı yazarken de mümkün olduğunca bu isimleri kullanmalıyız. Bu isimler "windows.h" içerisinde tanımlanmıştır. En önemlileri şunlardır: "BYTE", "WORD", "DWORD", "QWORD". >>> "BYTE" : 1 bayt uzunlukta, işaretsiz tam sayı türünün eş ismidir. Tipik olarak "unsigned char". >>> "WORD" : 2 bayt uzunlukta, işaretsiz tam sayı türünün eş ismidir. Tipik olarak "unsigned short". >>> "DWORD": 4 bayt uzunlukta, işaretsiz tam sayı türünün eş ismidir. Tipik olarak "unsigned int". >>> "QWORD": 8 bayt uzunlukta, işaretsiz tam sayı türünün eş ismidir. Tipik olarak "unsigned long long int". Adres türleri ile başına "P" yada "LP" öneki almaktadır ("16-bit" Windows sistemlerde "P" demek "near-pointer", "LP" demek "far-pointer" anlamındadır. Ancak "32-bit" sistemlerle birlikte bu ayrım ortadan kalkmıştır. Dolayısıyla "P" ve "LP" önekleri arasında bir fark kalmamıştır. Geleneksel olarak "LP" öneki tercih edilmektedir). Eğer gösterici "const" ise "P" veya "LP" öneklerinden sonra "C" öneki gelmektedir. Dolayısıyla "PC" veya "LPC" önekleri kullanılır. Göstericinin türü ise bu öneklerden hemen sonra gelmektedir. Burada, >>> "LPCDWORD" demek "const DWORD*" anlamına gelmektedir. >>> "LPDWORD" demek "DWORD*" anlamına gelmektedir. >>> "void" türü "VOID" olarak nitelendiği için, "LPVOID" demek "void*" demektir. "LPCVOID" ise "const void*" demektir. Öte yandan "HANDLE" tür ismi de "void*" demektir. Eğer göstericimiz bir yazıyı gösteriyorsa, "STR" öneki kullanılmaktadır. Yani, >>> "LPSTR" demek ise "char*" demektir. Eğer "ASCII" seçeneği seçilmişse. >>> "LPCSTR" demek ise "const char*" demektir. Eğer "ASCII" seçeneği seçilmişse. >>> "LPTSTR" demek ise "char*" demektir. Eğer "UNICODE" seçeneği seçilmişse. >>> "LPCTSTR" demek ise "const char*" demektir. Eğer "UNICODE" seçeneği seçilmişse. C dilindeki diğer standart türler de "typedef" edilmiştir. Örneğin, "INT", "LONG", "CHAR", "DOUBLE" vb. isimler orjinal türlerinin eş ismidir. Yapı türleri ise yine büyük harfle "typedef" edilmiştir. Fakat yapının kendisini "tag" ön eki getirilmiştir. Yani, >>> "SAMPLE" demek "struct tagSAMPLE" demektir. Dolayısıyla bizler "SAMPLE" da kullanabiliriz "tagSAMPLE" da. Bu yapı türünden göstericiler de yine başına "LP" / "P" / "PC" / "LPC" ön ekleri almaktadır. "BOOL" demek de aslında "int" demektir. Bir API fonksiyonunun bu türden değer döndürmesi, başarı/başarısızlık anlamı vermektedir. Diğer yandan Windows API fonksiyonları kullanan programlar, isimlendirme sistemi olarak "Macar Notasyonu (Hungarian Notation)" denilen bir sistem kullanmaktadır. Fakat bu notasyon bazı programcılar tarafından daha gevşek kullanılmaktadır. >>> Macar Notasyonu : Değişkenin isminin başına ön ek olarak o değişkenin türünü belirten şeyler yazmaktır. En belirgin özelliklerinden birisi budur. Örneğin, >>>> "b" demek "BYTE" veya "BOOL" demektir. >>>> "w" demek "WORD" demektir. >>>> "dw" demek "DWORD" demektir. >>>> "p" / "lp" demek gösterici demektir. >>>> "sz" demek elemanları "CHAR" türden olan dizi demektir. >>>> "psz" / "lpsz" demek "CHAR" türden yazı belirten bir gösterici. >>>> "pc" / "lpc" demek "CHAR" türden bir gösterici. >>>> "h" demek "HANDLE" demektir. >>>> "l", "u", "lu" demek de sırasıyla "LONG", "UNSIGNED INT" ve "UNSIGNED LONG" demektir. >>>> Yapı türleri için, o yapıya ilişkin kısa kelimeler kullanılmaktadır. Örneğin, "rect" demek "RECT" yapısını belirtmektedir. >>>> Değişken isimleri, onun türünü belirten ön ekten sonra, her sözcüğün ilk harfi büyük yazılarak isimlendirilmektedir. Örneğin, DWORD dwNumberOfSectors; LONG lStudentNumber; LPSTR lpszFileName; Eğer değişken isimlendirmesinde Macar Notasyonu kullanılmayacaksa, "Deve Notasyonu (Camel Casting)" kullanılmalıdır. >>>>> "Deve Notasyonu" : Değişken ismi tek sözcükten oluşuyorsa tamamının küçük harfle yazılması, birden fazla sözcükten oluşuyorsa sadece ilk sözcüğün tamamının küçük harfle diğer sözcüklerin de sadece ilk harflerinin büyük harfle yazılmasıdır. Örneğin, int counter; int numberOfStudents; Görüldüğü üzere Macar Notasyonundaki değişken isimleri uzun olma eğilimdedir. Bu da programcıları yormaktadır. >>>> Fonksiyon isimlerinde de "Pascal Casting" kullanılır. >>>>> "Pascal Casting": Her sözcüğün ilk harfi büyük yazılır. Birden fazla sözcük içermesi durumunda ilk olarak yapılacak fiili anlatan, daha sonra bu fiilden etkilenen nesneye ilişkin sözcük yazılır. Örneğin, "CreateFile", "SetWindowText" vb. Diğer yandan Windows API fonksiyonlarının bulunduğu kütüphane dosyaları, C ve C++ derleyicileri tarafından otomatik olarak referans edilmektedir. Dolayısıyla bu fonksiyonları kullanırken özel bir şey yapmamıza gerek yoktur. >> POSIX Fonksiyonları Hakkında Temel Bilgiler: Bu tip fonksiyonlar çeşitli başlık dosyalarına dağılmış durumdadır fakat genel olarak bir çoğu "unistd.h" başlık dosyasında toplanmıştır. İsimlendirme olarak C notasyonu kullanılır ki bu notasyonda sözcükler "_" ile birbirinden ayrılır ve büyük harf kullanılmaz. Windows API fonksiyonlarındakiler gibi "typedef" çok fazla yoktur, olanlar da "sys/types.h" başlık dosyasındadır. İsimlerin sonuna gelen "_t" son ek, ilgili ismin bir "typedef" olduğunu belirtmektedir. Örneğin, "pid_t", "pthread_mutex_t" vs. Diğer yandan POSIX fonksiyonlarının kullanılabilmesi için genellikle özel bir kütüphaneye referans verilmesine gerek yoktur. Fakat bazı POSIX fonksiyonları başka kütüphaneler içerisinde olduğu için ilgili kütüphaneyi referans etmeliyiz. Bunu da komut satırından derlerken uygun seçenekleri belirtmeliyiz. >> Fonksiyonların çağrılmasında hata kontrolü: Sistem programlamada fonksiyonların başarısının test edilmesi önemli bir yer kaplamaktadır. Burada fonksiyonları üç gruba ayırabiliriz: Her daim hata kontrolü yapmamız gereken fonksiyonlar, eğer her şeyi doğru yaptıysak zaten doğru çalışacak olan fonksiyonlar ve başarı kontrolünün mümkün olmadığı fonksiyonları. Buradaki ilk grubu daima test ederken, ikinci grup opsiyoneldir. Örneğin, "malloc", "fopen" gibi fonksiyonları daima test etmeliyiz. Bizler her şeyi doğru yapmış olsak bile sistemdeki aksaklıktan dolayı bu fonksiyon yine başarısız olabilir. Diğer yandan "fclose" gibi bir fonksiyonu test etmeye lüzum yoktur. Çünkü bu fonksiyona geçilen argümanı bizler bozmamışsak, fonksiyonu test etmeye lüzum yoktur. Hakeza "free" gibi bir fonksiyonun başarısını da test edemeyiz. /*================================================================================================================================*/ (10_08_07_2023) & (11_09_07_2023) > Standart C fonksiyonları, POSIX fonksiyonları ve Windows API fonksiyonları (devam): >> Fonksiyonların çağrılmasında hata kontrolü (devam): Fonksiyonların başarısızlığının tespiti ne kadar önemliyse, başarısızlık nedenlerinin tespit edilmesi de bir o kadar önemlidir. Dolayısıyla başarısız olan bir fonksiyonun neden başarısız olduğunu da programcıya iletmeliyiz ki ilgili kullanıcı telafi edebilme imkanı bulabilsin. Şimdi de Windows API ve POSIX fonksiyonlarının başarısız olma nedenlerini inceleyelim; >>> Windows API Fonksiyonları: "GetLastError" fonksiyonunu, ilgili fonksiyon başarısız olduğunda çağırmalıyız. Çünkü bu fonksiyon en son başarısız olan fonksiyonu baz almaktadır. "thread-safe" bir fonksiyondur. "thread" konusuna ileride değinilecektir. >>>> "GetLastError" : Fonksiyonun prototipi aşağıdaki gibidir; DWORD GetLastError(void); Fonksiyonun geri dönüş değerinin türü, hata kodunun nedenine ilişkin rakamsal bir değerdir. Her bir hata kodu için farklı bir değer döndürülmektedir. Aynı zamanda bu değerler, "windows.h" başlık dosyası içinde "ERROR_" ön ekiyle "#define" edilmişlerdir ki okunabilirlik artsın. * Örnek 1, Aşağıdaki örnekte "CreateFile" fonksiyonunun başarısız olma nedeni ekrana yazdırılmıştır. #include #include #include int main(void) { /* # OUTPUT # CreateFile failed: 2 */ HANDLE hFile; if((hFile = CreateFile("test.xxx", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) { fprintf(stderr, "CreateFile failed: %lu\n", GetLastError()); exit(EXIT_FAILURE); } puts("Ok"); return 0; } Öte yandan Windows sistemlerinde, belli bir API fonksiyonunun başarısızlık durumunda hangi değerleri döndüreceği belirtilmemiştir. Bu nedenden dolayı Windows programcıları "GetLastError" ile elde ettikleri sayısal değeri bir "switch-case" içerisinde kullanamamaktadır. Böylesi bir durumda ilgili hata kodunun ne anlama geldiğine aşağıdaki internet sitesinden bakılabilir: https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes Diğer yandan "GetLastError" ile elde ettiğimiz hata kodunu bir mesaj olarak ekrana yazdırmak da mümkündür. Bu durumda da "FormatMessage" isimli fonksiyona çağrı yapmalıyız. >>>> "FormatMessage" : Fonksiyonun prototipi aşağıdaki gibidir. DWORD FormatMessage( DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanguageId, LPTSTR lpBuffer, DWORD nSize, va_list *Arguments ); Fakat bu fonksiyonun kullanımı biraz zordur. Kursun geri kalanında hata mesajlarının ekrana yazılması için "ExitSys" isminde "wrapper" bir fonksiyon yazıp kullanacağız. >>>> "ExitSys" : Fonksiyonun tanımı aşağıdaki biçimde olacaktır. void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Fonksiyon bizden bir yazıyı argüman olarak alır. İlk önce bu yazıyı ekrana yazdırır. Devamında " : " karakterini, en sonunda ise "GetLastError()" ile elde ettiğimiz hata koduna ilişkin yazıyı ekrana yazdıracaktır. * Örnek 1, #include #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { /* # OUTPUT # CreateFile : Sistem belirtilen dosyayı bulamıyor. */ HANDLE hFile; if((hFile = CreateFile("test.xxx", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) ExitSys("CreateFile"); puts("Ok"); return 0; } void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Özetlemek gerekirse; Bir API fonksiyonu başarısız olduğunda "GetLastError" fonksiyonu ile başarısızlığın nedenine ilişkin rakamsal değeri temin ediyoruz. Daha sonra "FormatMessage" fonksiyonuna bu değeri geçerek, başarısızlığın nedeninin metinsel olarak temin ediyoruz. Fakat "FormatMessage" kullanımı zor olduğu için, "ExitSys" isimli bir "wrapper" fonksiyon kullanılacaktır. Dolayısıyla başarısızlığın nedenini ekrana yazdırmak için "ExitSys" isimli fonksiyonu kullanacağız. Diğer yandan programcı "last-error" değerini belli bir değere çekebilir. Bunun için "SetLastError" fonksiyonunu çağırması gerekmektedir. >>>> "SetLastError" : Fonksiyonun prototipi aşağıdaki gibidir. void SetLastError(DWORD dwErrCode); Fonksiyon parametre olarak hata kodunun "#define" edilmiş halini almaktadır. * Örnek 1, void ExitSys(LPCSTR lpszMsg); BOOL Foo(LPCSTR lpszName); int main(void){ if(!Foo("")) ExitSys("Foo"); return 0; } BOOL Foo(LPCSTR lpszName){ if(strlen(lpszName) == 0){ SetLastError(ERROR_BAD_LENGTH); return FALSE; } return TRUE; } void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Bunlara ek olarak Windows API fonksiyonları ekseriyetle "BOOL" türüyle geri dönmektedir. Böylesi fonksiyonlar için başarı durumunda "Non-zero", hata durumunda "0" ile geri döndüğünü söyleyebiliriz. Dolayısıyla böylesi fonksiyonların geri dönüş değerini aşağıdaki biçimlerde kontrol edebiliriz; if(SomeAPIFunc() == FALSE){ } veya if(!SomeAPIFunc()){ } Fakat aşağıdaki biçimde kontrol etmemeliyiz; if(SomeAPIFunc() == TRUE){ } veya if(SomeAPIFunc()){ } Çünkü "FALSE" ve "TRUE" sembolik sabitleri, "windows.h" dosyası içerisinde, aşağıdaki biçimde "#define" edilmiştir: #define FALSE 0 #define TRUE 1 >>> POSIX Fonksiyonlarında: Bu fonksiyonların büyük çoğunluğu "int" türden değer döndürmektedir. Başarı durumunda "0", hata durumunda "-1" değerine geri dönmektedir. Bazı POSIX fonksiyonları da bir adres değeri ile geri dönmektedir. Böylesi fonksiyonlar hata durumunda "NULL" değerine geri dönerler. UNIX dünyasında fonksiyonların başarısızlık nedeni için "errno" isimli global değişkene bakmamız gerekmektedir. Bu değişken ise "errno.h" isimli başlık dosyasında bildirilmiştir. Bazı sistemlerde "errno" ismi doğrudan bir değişken olarak, bazı sistemlerde ise bir makro olarak tanımlanmıştır. Fakat POSIX standartlarınca "errno" nun değer atanabilir bir şey olması gerekmektedir. Bu nedenden dolayıdır ki biz programcılar "errno" ismini kendi programlarımızda kullanmaktan kaçınmalıyız. Diğer yandan POSIX standartlarınca hiç bir fonksiyon "errno" ya "0" değerini atamayacağı GARANTİ ETMEKTEDİR. Yine Windows sistemlerinde olduğu gibi "errno" değişkeninin aldığı değerler "#define" edilmiştir ki burada sadece "E" öneki almaktadır ve hangi fonksiyonun "errno" değişkenine hangi değeri atayacağı önceden belirlenmiştir. Dolayısıyla fonksiyonun başarısızlık nedenini irdelerdek, "#define" edilmiş hallerini kullanmalıyız. Çünkü standart haline getirilen bu "E" öneki alanlardır. Aynı ön ek, farklı sistemlerde farklı rakamlara ait olabilir. Şimdi bizler hatanın sebebini ekrana yazdırırken nasıl bir yöntem izlemeliyiz? Her ne kadar hangi fonksiyonun "errno" değişkenine hangi değeri atayacağı belli olsa da, bu iş zahmetli olabilmektedir. İşte böylesi durumlarda bizler "strerror" fonksiyonunu kullanmalıyız. >>>> "strerror" : Fonksiyonun prototipi aşağıdaki gibidir: #include char *strerror(int errnum); Parametre olarak "errno" değerini alır ve karşılık gelen yazının adresine geri döner. Bu adres "static" bir dizi adresidir fakat "thread-safe" DEĞİLDİR. Bu fonksiyon aynı zamanda standart bir C fonksiyonudur fakat "errno" kavramı standart C içerisinde kullanımı sınırlıdır. * Örnek 1, #include #include #include #include #include int main(void) { /* # OUTPUT # open failed: No such file or directory */ int fd; if((fd = open("test.dat", O_RDONLY)) == -1){ fprintf(stderr, "open failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } puts("Ok"); return 0; } "strerror" fonksiyonuna alternatif olarak birde "perror" fonksiyonu vardır. >>>> "perror" : Fonksiyonun prototipi aşağıdaki gibidir. #include void perror(const char *s); Unutmamalıyız ki bu fonksiyon "errno" değişkeninin en sonki değerini kullanır. Dolayısıyla fonksiyonumuz başarısız olduğunda bu fonksiyonu çağırmalıyız. Parametre olarak bir yazı alır. Bu yazı ile "errno" değişkenine karşılık gelen yazıyı birleştirir. Bu birleştirme esnasında ise iki yazı arasına " : " karakterlerini ekler. En son oluşturulan yazıyı da "stderr" dosyasına yazar. * Örnek 1, #include #include #include #include #include #include int main(void) { /* # OUTPUT # open: No such file or directory */ int fd; if((fd = open("test.dat", O_RDONLY)) == -1){ perror("open"); exit(EXIT_FAILURE); } puts("Ok"); return 0; } Tıpkı Windows sistemleri için oluşturduğumuz "wrapper" fonksiyon için UNIX sistemleri için de bir "wrapper" fonksiyon kullanacağız. Bu sefer fonksiyonumuzun ismi "exit_sys" biçiminde olacaktır. >>>> "exit_sys" : Fonksiyonun tanımı aşağıdaki gibidir. void exit_sts(const char* msg){ perror(msg); exit(EXIT_FAILURE); } > "HANDLE" Sistemleri: "HANDLE" sözcüğü bir grup bilgiye erişmek için kullanılan, anahtar niteliğindeki tekil bilgiler anlamındadır. "HANDLE" kavramı karşımıza çeşitli formlarda çıkabilir. Örneğin, "int" türden değer olabilir, adres türünden olabilir vs. * Örnek 1, Aşağıdaki "fd", kavramsal olarak bir "HANDLE" belirtmektedir. Çünkü bu "fd" değeri ile bizler bir grup bilgiye ulaşabiliriz. #include #include #include int main(void) { /* # OUTPUT # open: No such file or directory */ int fd; if((fd = open("test.dat", O_RDONLY)) == -1){ perror("open"); exit(EXIT_FAILURE); } puts("Ok"); return 0; } Diğer yandan bir "HANDLE" sisteminde üç tür fonksiyon bulunmaktadır. Bunlar "HANDLE" sistemini kuran fonksiyonlar, "HANDLE" sistemini kullanan fonksiyonlar ve "HANDLE" sistemini yok eden fonksiyonlardır. >> Kurucu Fonksiyonlar: Bu tip fonksiyonlar, "HANDLE" sistemi için alan tahsisi yaparlar. Bu alandaki değişkenlere ilk değerini verirler ve "HANDLE" olarak kullanılacak değer ile geri dönerler. Örneğin, standart C fonksiyonu olan "fopen" fonksiyonu böyle bir fonksiyondur. Bu kategorideki fonksiyon isimleri genellikle "create" veya "open" ön ekini içermektedir. >> Kullanan Fonksiyonlar: "HANDLE" değerini argüman olarak alan ve tahsis edilmiş olan ilgili alandaki değerleri kullanan fonksiyonlardır. Örneğin, standart C fonksiyonu olan "fgetc" fonksiyonu böyle bir fonksiyondur. >> Yok Eden Fonksiyonlar: "HANDLE" değerini argüman olarak alarak tahsis edilmiş olan ilgili alanı tamamen yok eden ve "HANDLE" sistemi kurulurken yapılan bir takım işlemleri de geri alan fonksiyonlardır. Örneğin, standart C fonksiyonu olan "fclose" fonksiyonu böyle bir fonksiyondur. * Örnek 1, Aşağıdaki örnekte "HANDLE" sistemine bir örnek verilmiştir. #include #include typedef struct tagMATRIX{ size_t row_size; size_t col_size; int* matrix; }MATRIX, *HMATRIX; HMATRIX CreateMatrix(size_t row_size, size_t col_size); void SetElem(HMATRIX hMatrix, size_t row_index, size_t col_index, size_t value); int GetElem(HMATRIX hMatrix, size_t row_index, size_t col_index); void DispMatrix(HMATRIX hMatrix); void CloseMatrix(HMATRIX hMatrix); int main(void) { /* # OUTPUT # 0 0 0 0 1 2 0 2 4 */ HMATRIX hMatrix; if((hMatrix = CreateMatrix(3,3)) == NULL){ fprintf(stderr, "Cannot create a matrix!..\n"); exit(EXIT_FAILURE); } for(int row = 0; row < 3; ++row) for(int col = 0; col < 3; ++col) SetElem(hMatrix, row, col, row * col); DispMatrix(hMatrix); CloseMatrix(hMatrix); return 0; } HMATRIX CreateMatrix(size_t row_size, size_t col_size){ HMATRIX hMatrix; if((hMatrix = (HMATRIX)malloc(sizeof(MATRIX))) == NULL) return NULL; hMatrix->row_size = row_size; hMatrix->col_size = col_size; if((hMatrix->matrix = (int*)malloc(sizeof(int) * row_size * col_size)) == NULL){ free(hMatrix); return NULL; } return hMatrix; } void SetElem(HMATRIX hMatrix, size_t row_index, size_t col_index, size_t value) { size_t index = row_index * hMatrix->col_size + col_index; hMatrix->matrix[index] = value; } int GetElem(HMATRIX hMatrix, size_t row_index, size_t col_index) { size_t index = row_index * hMatrix->col_size + col_index; return hMatrix->matrix[index]; } void DispMatrix(HMATRIX hMatrix) { // Version - I for(size_t i = 0; i < hMatrix->row_size * hMatrix->col_size; ++i){ printf( "%d%c", hMatrix->matrix[i], i % hMatrix->col_size == hMatrix->col_size - 1 ? '\n' : ' ' ); } /* // Version - II for(size_t row = 0; row < hMatrix->row_size; ++row){ for(size_t col = 0; col < hMatrix->col_size; ++col) printf("%d ", hMatrix->matrix[row * hMatrix->col_size + col]); printf("\n"); } */ } void CloseMatrix(HMATRIX hMatrix) { free(hMatrix->matrix); free(hMatrix); } * Örnek 2, Aşağıdaki örnekte ise "HANDLE" değeri "void*" olarak belirtilmiş ve arka plandaki esas tür gizlenmiştir. /* matrix.h */ #ifndef MATRIX_H_ #define MATRIX_H_ #include /* Type Declarations */ typedef void* HMATRIX; /* Function Prototypes */ HMATRIX CreateMatrix(size_t row_size, size_t col_size); void SetElem(HMATRIX hMatrix, size_t row_index, size_t col_index, size_t value); int GetElem(HMATRIX hMatrix, size_t row_index, size_t col_index); void DispMatrix(HMATRIX hMatrix); void CloseMatrix(HMATRIX hMatrix); #endif /* matrix.c */ #include "matrix.h" #include /* Type Declarations */ typedef struct tagMATRIX{ size_t row_size; size_t col_size; int* matrix; }MATRIX; /* Function Definitons */ HMATRIX CreateMatrix(size_t row_size, size_t col_size){ MATRIX* matrix; if((matrix = (HMATRIX)malloc(sizeof(MATRIX))) == NULL) return NULL; matrix->row_size = row_size; matrix->col_size = col_size; if((matrix->matrix = (int*)malloc(sizeof(int) * row_size * col_size)) == NULL){ free(matrix); return NULL; } return matrix; } void SetElem(HMATRIX hMatrix, size_t row_index, size_t col_index, size_t value) { MATRIX* matrix = (MATRIX*)hMatrix; size_t index = row_index * matrix->col_size + col_index; matrix->matrix[index] = value; } int GetElem(HMATRIX hMatrix, size_t row_index, size_t col_index) { MATRIX* matrix = (MATRIX*)hMatrix; size_t index = row_index * matrix->col_size + col_index; return matrix->matrix[index]; } void DispMatrix(HMATRIX hMatrix) { MATRIX* matrix = (MATRIX*)hMatrix; // Version - I for(size_t i = 0; i < matrix->row_size * matrix->col_size; ++i){ printf( "%d%c", matrix->matrix[i], i % matrix->col_size == matrix->col_size - 1 ? '\n' : ' ' ); } /* // Version - II for(size_t row = 0; row < matrix->row_size; ++row){ for(size_t col = 0; col < matrix->col_size; ++col) printf("%d ", matrix->matrix[row * matrix->col_size + col]); printf("\n"); } */ } void CloseMatrix(HMATRIX hMatrix) { MATRIX* matrix = (MATRIX*)hMatrix; free(matrix->matrix); free(matrix); } /* main.c */ #include #include "matrix.h" int main(void) { /* # OUTPUT # 0 0 0 0 1 2 0 2 4 */ HMATRIX hMatrix; if((hMatrix = CreateMatrix(3,3)) == NULL){ fprintf(stderr, "Cannot create a matrix!..\n"); exit(EXIT_FAILURE); } for(int row = 0; row < 3; ++row) for(int col = 0; col < 3; ++col) SetElem(hMatrix, row, col, row * col); DispMatrix(hMatrix); CloseMatrix(hMatrix); return 0; } Öte yandan Macar Notasyonunda "H" öneki almış olan bütün "typedef" isimler bir "HANDLE" belirtmektedir ve Windows API sisteminde arka planda "void*" türüne açılmaktadır. Burada "void*" kullanılma sebebi ise esas gerçek türün programcıdan gizlenmek istenmesidir. > Homework - 1, Çözüm: Geçici dosya kullanımı. * Örnek 1, Aşağıdaki kodda hata nedenleri teker teker özel olarak ele alınmıştır. #include #include #include #include #define RANDOM_FILE_PATH "random.dat" #define RESULT_FILE_PATH "result.dat" #define EPOCH 100000 #define TOTAL_PART 1 #define EACH_PART (EPOCH / TOTAL_PART) typedef struct tagMERGE_INFO{ FILE* f; int current_value; }MERGE_INFO; FILE * create_random_file(void); void bubble_sort(int* array, size_t size); size_t getmin_index(const MERGE_INFO* mi, size_t size); bool is_sorted(FILE* f); int main(void) { /* # OUTPUT # Success */ FILE* f; if((f = create_random_file()) == NULL){ fprintf(stderr, "Cannot create/write random file!..\n"); exit(EXIT_FAILURE); } FILE* fd; if((fd = fopen(RESULT_FILE_PATH, "w+b")) == NULL){ fprintf(stderr, "Cannot create result file!..\n"); exit(EXIT_FAILURE); } int* array; if((array = (int*)malloc(EACH_PART * sizeof(int))) == NULL){ fprintf(stderr, "Cannot allocate memory!..\n"); exit(EXIT_FAILURE); } rewind(f); MERGE_INFO mi[TOTAL_PART]; for(int i = 0; i < TOTAL_PART; ++i){ if(fread(array, sizeof(int), EACH_PART, f) != EACH_PART){ fprintf(stderr, "Cannot read random file!..\n"); exit(EXIT_FAILURE); } bubble_sort(array, EACH_PART); if((mi[i].f = tmpfile()) == NULL){ fprintf(stderr, "Cannot create temporary file!..\n"); free(array); exit(EXIT_FAILURE); } if(fwrite(array, sizeof(int), EACH_PART, mi[i].f) != EACH_PART){ fprintf(stderr, "Cannot write temporary file!..\n"); free(array); exit(EXIT_FAILURE); } rewind(mi[i].f); if(fread(&mi[i].current_value, sizeof(int), 1, mi[i].f) != 1){ fprintf(stderr, "Cannot read temporary file!..\n"); free(array); exit(EXIT_FAILURE); } } size_t size = TOTAL_PART; size_t min_index; while(size > 0){ min_index = getmin_index(mi, size); if(fwrite(&mi[min_index].current_value, sizeof(int), 1, fd) != 1){ fprintf(stderr, "Cannot write result file!..\n"); free(array); exit(EXIT_FAILURE); } if(fread(&mi[min_index].current_value, sizeof(int), 1, mi[min_index].f) != 1){ if(ferror(mi[min_index].f)){ fprintf(stderr, "Cannot create temporary file!..\n"); free(array); exit(EXIT_FAILURE); } fclose(mi[min_index].f); mi[min_index] = mi[size - 1]; --size; } } printf(is_sorted(fd) ? "Success\n" : "Failed!\n"); free(array); return 0; } FILE * create_random_file(void) { srand(time(NULL)); FILE *f; if((f = fopen(RANDOM_FILE_PATH, "w+b")) == NULL) return NULL; /* * "fwrite" kullandığımız için bütün rakamlar * "sizeof int" kadar yer kaplayacaktı. Eğer * "fprintf" kullansaydık, rakamları yazıya * dönüştürüp yazacaktı. Bu durumda da her bir * rakam farklı miktarda bayt kaplayacaktı, * kaç basamaklı olduğuna göre. */ int val; for(int i = 0; i < EPOCH; ++i) { val = rand(); if(fwrite(&val, sizeof(int), 1, f) != 1){ fclose(f); return NULL; } } return f; } void bubble_sort(int* array, size_t size) { size_t temp; for(size_t i = 0; i < size - 1; ++i) for(size_t k = 0; k < size - 1 - i; ++k) if(array[k] > array[k + 1]){ temp = array[k]; array[k] = array[k+1]; array[k+1] = temp; } } size_t getmin_index(const MERGE_INFO* mi, size_t size) { size_t min_index = 0; for(size_t i = 1; i < size; ++i){ if(mi[i].current_value < mi[min_index].current_value) min_index = i; } return min_index; } bool is_sorted(FILE* f) { rewind(f); int val, next_val; next_val = -1; while(fread(&next_val, sizeof(int), 1, f) == 1){ if(next_val < val) return false; val = next_val; } if(ferror(f)) return false; return ftell(f) / sizeof(int) == EPOCH; } * Örnek 2, Aşağıdaki örnekte ise hata nedenleri "goto" deyimleri kullanılarak biraz daha düzenli hale getirilmiştir. #include #include #include #include #define RANDOM_FILE_PATH "random.dat" #define RESULT_FILE_PATH "result.dat" #define EPOCH 100000 #define TOTAL_PART 100 #define EACH_PART (EPOCH / TOTAL_PART) typedef struct tagMERGE_INFO{ FILE* f; int current_value; }MERGE_INFO; FILE * create_random_file(void); void bubble_sort(int* array, size_t size); size_t getmin_index(const MERGE_INFO* mi, size_t size); bool is_sorted(FILE* f); int main(void) { /* # OUTPUT # Success */ int status = EXIT_FAILURE; FILE* f; if((f = create_random_file()) == NULL){ fprintf(stderr, "Cannot create/write random file!..\n"); goto EXIT_I; } FILE* fd; if((fd = fopen(RESULT_FILE_PATH, "w+b")) == NULL){ fprintf(stderr, "Cannot create result file!..\n"); goto EXIT_II; } int* array; if((array = (int*)malloc(EACH_PART * sizeof(int))) == NULL){ fprintf(stderr, "Cannot allocate memory!..\n"); goto EXIT_III; } rewind(f); MERGE_INFO mi[TOTAL_PART]; for(int i = 0; i < TOTAL_PART; ++i){ if(fread(array, sizeof(int), EACH_PART, f) != EACH_PART){ fprintf(stderr, "Cannot read random file!..\n"); goto EXIT_IV; } bubble_sort(array, EACH_PART); if((mi[i].f = tmpfile()) == NULL){ fprintf(stderr, "Cannot create temporary file!..\n"); goto EXIT_IV; } if(fwrite(array, sizeof(int), EACH_PART, mi[i].f) != EACH_PART){ fprintf(stderr, "Cannot write temporary file!..\n"); goto EXIT_IV; } rewind(mi[i].f); if(fread(&mi[i].current_value, sizeof(int), 1, mi[i].f) != 1){ fprintf(stderr, "Cannot read temporary file!..\n"); goto EXIT_IV; } } size_t size = TOTAL_PART; size_t min_index; while(size > 0){ min_index = getmin_index(mi, size); if(fwrite(&mi[min_index].current_value, sizeof(int), 1, fd) != 1){ fprintf(stderr, "Cannot write result file!..\n"); goto EXIT_IV; } if(fread(&mi[min_index].current_value, sizeof(int), 1, mi[min_index].f) != 1){ if(ferror(mi[min_index].f)){ fprintf(stderr, "Cannot create temporary file!..\n"); goto EXIT_IV; } fclose(mi[min_index].f); mi[min_index] = mi[size - 1]; --size; } } printf(is_sorted(fd) ? "Success\n" : "Failed!\n"); status = EXIT_SUCCESS; EXIT_IV: free(array); EXIT_III: if(remove(RESULT_FILE_PATH) != 0) fprintf(stderr, "cannot remove result file!...\n"); EXIT_II: if(remove(RANDOM_FILE_PATH) != 0) fprintf(stderr, "cannot remove random file!...\n"); EXIT_I: return status; } FILE * create_random_file(void) { srand(time(NULL)); FILE *f; if((f = fopen(RANDOM_FILE_PATH, "w+b")) == NULL) return NULL; /* * "fwrite" kullandığımız için bütün rakamlar * "sizeof int" kadar yer kaplayacaktı. Eğer * "fprintf" kullansaydık, rakamları yazıya * dönüştürüp yazacaktı. Bu durumda da her bir * rakam farklı miktarda bayt kaplayacaktı, * kaç basamaklı olduğuna göre. */ int val; for(int i = 0; i < EPOCH; ++i) { val = rand(); if(fwrite(&val, sizeof(int), 1, f) != 1){ fclose(f); return NULL; } } return f; } void bubble_sort(int* array, size_t size) { size_t temp; for(size_t i = 0; i < size - 1; ++i) for(size_t k = 0; k < size - 1 - i; ++k) if(array[k] > array[k + 1]){ temp = array[k]; array[k] = array[k+1]; array[k+1] = temp; } } size_t getmin_index(const MERGE_INFO* mi, size_t size) { size_t min_index = 0; for(size_t i = 1; i < size; ++i){ if(mi[i].current_value < mi[min_index].current_value) min_index = i; } return min_index; } bool is_sorted(FILE* f) { rewind(f); int val, next_val; next_val = -1; while(fread(&next_val, sizeof(int), 1, f) == 1){ if(next_val < val) return false; val = next_val; } if(ferror(f)) return false; return ftell(f) / sizeof(int) == EPOCH; } CloseHandle - close > Hatırlatıcı Notlar: >> Konsol ekranına yazılan mesajları Türkçe yazdırmak için: * Örnek 1, #include #include #include #include #include #include int main(void) { if(setlocale(LC_ALL, "tr_TR.utf-8") == NULL){ fprintf(stderr, "cannot set locale!..\n"); exit(EXIT_FAILURE); } int fd; if((fd = open("test.dat", O_RDONLY)) == -1){ fprintf(stderr, "open failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } puts("Ok"); return 0; } >> POSIX fonksiyonları da tıpkı standart C fonksiyonları gibi birer kütüphane fonksiyonlarıdır. Çekirdeğin içerisinde değillerdir. Çekirdeğin içerisinde bulunanlar sistem fonksiyonlarıdır. /*================================================================================================================================*/ (12_16_07_2023) & (13_22_07_2023) & (14_29_07_2023) > İşletim Sistemlerinin Dosya Sistemleri: İşletim sistemlerinin dosya işlemleri ile ilgilenen alt sistemlerine Dosya Sistemleri("File System") denmektedir. Bu sistem, dosyaların ikinci bellekteki organizasyonu ve onların kullanımına ilişkin temel işlemleri yerine getirmektedir. Bunun için bir grup sistem fonksiyonu bulunmaktadır. Bu sistem fonksiyonları toplamda 5 gruba ayrılmıştır. Bunlar dosyayı açmak, dosyayı kapatmak, dosyadan okuma yapmak, dosyaya yazmak ve dosya göstericisini konumlandırmak içindir. Hangi programlama dilini kullanırsak kullanalım, işletim sistemi değişmediği müddetçe, yine bu sistem fonksiyonları çağrılmaktadır. Buradaki bütün sorumluluk işletim sistemine aittir. Yine buradaki kullanım sırasıda şu şekilde olmalıdır: Standart C > POSIX / Windows API > Sistem Fonksiyonlar. Windows API ve POSIX fonksiyonlarının karşılaştırması ise aşağıdaki gibidir: İşlevi Windows API - POSIX Dosyya Açmak CreateFile - open Dosyadan Okuma Yapmak ReadFile - read Dosyaya Yazmak WriteFile - write Dosya Göstericisini Konumlandırmak SetFilePointer - lseek Dosyayı Kapatmak CloseHandle - close Şimdi de bu fonksiyonları incelemeye başlayalım: >> Windows API fonksiyonları: >>> "CreateFile" : Bir dosya açmak veya oluşturmak için kullanılır. Fonksiyonun prototipi aşağıdaki gibidir. HANDLE CreateFileA( LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ); -> Fonksiyonun birinci parametresi, açılacak olan dosyanın yol ifadesidir. -> Fonksiyonun ikinci parametresi, bu dosyayı açma niyetimizdir. Bu parametreye ya "GENERIC_READ", "GENERIC_WRITE" veya "GENERIC_READ | GENERIC_WRITE" değerlerinden birisi geçilmelidir. Sırasıyla okuma, yazma ve hem okuma hem yazma amacı taşımaktadır. -> Fonksiyonun üçüncü parametresi, dosyayı açmamız halinde, başkalarının bu dosya üzerinde hangi işlemleri yapabileceğini belirten parametredir. Bu parametre ise şu argümanları almaktadır: "FILE_SHARE_DELETE", "FILE_SHARE_READ" ve "FILE_SHARE_WRITE". Bu argümanlar ise açtığımız dosyamız üzerinde sırasıyla silme, okuma ve yazma işlemi yapmasına imkan vermektedir. Tabii bu değerlerden birisini girmek zorunda değiliz. Bu değerleri "bit-wise OR" işlemine tabii tutarak da fonksiyona gönderebiliriz. Eğer bu parametreye "0" değerini geçersek, başka bir proses dosyayı açamayacaktır. -> Fonksiyonun dördüncü parametresi, "Security Attributes" konusu ile alakalıdır. Bu konuya kursta değinilmeyeceği için "NULL" değerini geçeceğiz. Böylelikle ilgili "attribute" nesnesi kullanılmamış, varsayılan değerler kullanılmış olacaktır. Fakat kabaca belirtmek gerekirse; bu parametre sayesinde belirli kullanıcıların dosyayı açabilmesini ancak diğerlerinin açamamasını sağlayabiliriz. -> Fonksiyonun beşinci parametresi, bir nevi şöyle bir parametredir: bu dosya varsa ne yapayım, yoksa ne yapayım. Bu parametreye şu argümanlardan birisini geçebiliriz: "OPEN_EXISTING", "CREATE_NEW", "OPEN_ALWAYS", "CREATE_ALWAYS", "TRUNCATE_EXISTING". -> "OPEN_EXISTING" : Dosya yoksa fonksiyon başarısız olacaktır. Ancak halihazırda var olan bir dosya açılabilir. -> "CREATE_NEW" : Olmayan dosyanın oluşturulmasını sağlar ve dosyayı açar. Eğer dosya halihazırda varsa fonksiyon başarısız olur. Yani dosyanın açılabilmesi için var olmaması GEREKMEKTEDİR. -> "OPEN_ALWAYS" : Dosya varsa açılır. Yoksa oluşturulur ve açılır. -> "CREATE_ALWAYS" : Dosya yoksa oluşturulur ve açılır. Dosya varsa sıfırlanarak açılır. -> "TRUNCATE_EXISTING" : Dosya yoksa fonksiyon başarısız olur. Dosya varsa sıfırlanarak açılır. -> Fonksiyonun altıncı parametresi, bir "attribute" parametresidir. Dosya oluşturulacaksa veya varolan açılacaksa farklı argümanlar geçmeliyiz. Örneğin, dosyayı sıfırdan oluşturacaksak "FILE_ATTRIBUTE_NORMAL" değerini geçebiliriz. Eğer halihazırda varolan bir dosyayı açacaksak, "0" değerini geçebiliriz. -> Fonksiyonun yedinci parametresi ise daha önce açılmış olan dosynın "HANDLE" değerini almaktadır. Böylelikle dosyayı açarken, bu "HANDLE" değeri kullanılarak zaten açılmış olan dosya referans alınacaktır. Bu istenmiyorsa, bu parametreye "NULL" değerini geçebiliriz. -> Fonksiyonun geri dönüş değeri ise bir "HANDLE" değeridir, başarı durumunda. Hata durumunda ise "INVALID_HANDLE_VALUE" değerine geri dönmektedir. Bu değer "NULL" göstericisi DEĞİLDİR, özel bir değerdir. * Örnek 1, Aşağıdaki program ile varolan bir dosya açılmaya çalışılmıştır. #include #include #include int main(void){ HANDLE hFile; if((hFile = CreateFile("sample.c", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE){ fprintf(stderr, "CreateFile failed! : %lu\n", GetLastError()); exit(EXIT_FAILURE); } puts("Ok..."); } * Örnek 2, Aşağıdaki program ile bir dosyanın sıfırdan oluşturulması hedeflenmiştir. #include #include #include int main(void){ HANDLE hFile; if((hFile = CreateFile("test.txt", GENERIC_WRITE, 0, NULL, CREATE_NEW, 0, NULL)) == INVALID_HANDLE_VALUE){ fprintf(stderr, "CreateFile failed! : %lu\n", GetLastError()); exit(EXIT_FAILURE); } puts("Ok..."); } >>> "CloseHandle" : Halihazırda açık olan bir dosyayı kapatmak için kullanılır. Fonksiyonun prototipi aşağıdaki gibidir. BOOL CloseHandle( HANDLE hObject ); -> Fonksiyon açık dosyaya ilişkin "HANDLE" değerini parametre olarak alacaktır. -> Fonksiyonun geri dönüş değeri başarı durumunda "non-zero", hata durumunda ise "0" değerine geri dönmektedir. Bu fonksiyonun geri dönüş değerinin kontrolüne de gerek yoktur. Çünkü argüman olarak geçilen değerin bozulmadığından eminsek, fonksiyon başarısız olma lüksü yoktur. Bir diğer yandan, eğer prosesimiz sonlanacaksa, o dosyayı özel olarak kapatmaya gerek yoktur çünkü proses sonlanırken zaten açık olan dosyalar da kapatılacaktır. * Örnek 1, #include #include #include int main(void){ HANDLE hFile; if((hFile = CreateFile("test.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, 0, NULL)) == INVALID_HANDLE_VALUE){ fprintf(stderr, "CreateFile failed! : %lu\n", GetLastError()); exit(EXIT_FAILURE); } puts("Ok..."); CloseHandle(hFile); } >>> "ReadFile" : Bir dosyadan okuma yapmak için kullanılır. Fonksiyonun prototipi aşağıdaki gibidir. BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ); -> Fonksiyonun birinci parametresi, açık dosyaya ilişkin "HANDLE" değeridir. -> Fonksiyonun ikinci parametresi, aslında dosyadan okunan bilgilerin yerleştirileceği alanın başlangıç adresidir. -> Fonksiyonun üçüncü parametresi, dosya göstericisinin konumundan itibaren okunacak toplam "byte" sayısını belirtmektedir. -> Fonksiyonun dördüncü parametresi, gerçekte okunan "byte" miktarının yazılacağı adres bilgisidir. Fonksiyon başarılı olmuşsa fakat buradaki adrese yazılan değer de "0" ise "EOF" konumuna gelindiğini belirtmektedir.Öte yandan bu sistemlerde okunmak istenen "byte" miktarı ile gerçekte okunan "byte" miktarı ARASINDA FARK OLMASI NORMAL KARŞILANMAKTADIR. -> Fonksiyonun beşinci parametresi, "Asincron IO" işlemleri için kullanılmaktadır. Bu parametre "NULL" değer geçilebilir. -> Fonksiyonun geri dönüş değeri başarı durumunda "non-zero", başarısızlık durumunda "zero" değerine geri dönmektedir. * Örnek 1, #include #include #include int main(void) { HANDLE hFile; if((hFile = CreateFile("test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE){ fprintf(stderr, "CreateFile failed! : %lu\n", GetLastError()); exit(EXIT_FAILURE); } puts("Ok..."); char buffer[10 + 1]; DWORD dwRead; if(!ReadFile(hFile, buffer, 10, &dwRead, NULL)){ fprintf(stderr, "ReadFile failed! : %lu\n", GetLastError()); exit(EXIT_FAILURE); } buffer[dwRead] = '\0'; puts(buffer); puts("Ok..."); CloseHandle(hFile); } * Örnek 2, #include #include #include #define BUFFER_SIZE 10 void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFile; if((hFile = CreateFile("test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE){ fprintf(stderr, "CreateFile failed! : %lu\n", GetLastError()); exit(EXIT_FAILURE); } puts("Ok..."); char buffer[BUFFER_SIZE + 1]; DWORD dwRead; BOOL bResult; while((bResult = ReadFile(hFile, buffer, BUFFER_SIZE, &dwRead, NULL)) != 0 && dwRead > 0){ buffer[dwRead] = '\0'; printf("%s\n", buffer); } if(!bResult){ ExitSys("ReadFile"); } puts("Ok..."); CloseHandle(hFile); } void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> "WriteFile" : Bir dosyaya yazma yapmak için kullanılır. "ReadFile" fonksiyonuna çok benzerdir. Sadece transferin yönü değişmektedir. Fonksiyonun parametresi aşağıdaki gibidir. BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped ); -> Fonksiyonun parametrik yapısı neredeyse "ReadFile" ile aynıdır. Sadece ikinci parametreye, dosyaya yazılacak bilgilerin nereden okunduğunu belirten adresi geçmemiz gerekmektedir. Bu yüzden bu parametre "const" olarak betimlenmiştir. -> Yine üçüncü ve dördüncü parametre sırasıyla dosyaya yazılmak istenen "byte" miktarı ve yazılan "byte" miktarını belirtmektedir. Bellekte yer kalmaması durumunda, artık istenilen büyüklükten daha az büyüklükte "byte" yazılacaktır. -> Birinci, beşinci parametreler ve fonksiyonun geri dönüş değeri "ReadFile" ile aynıdır. * Örnek 1, #include #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { /* # OUTPUT # */ HANDLE hFile; if((hFile = CreateFile("test.txt", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE){ ExitSys("CreateFile"); } puts("Ok..."); char buffer[] = "Ulya Yuruk"; DWORD dwWrite; if(!WriteFile(hFile, buffer, strlen(buffer), &dwWrite, NULL)) ExitSys("WriteFile"); puts("Ok..."); CloseHandle(hFile); } void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> "SetFilePointer" : Dosya konum göstericisini yeniden konumlandırmak için kullanılır. Fonksiyonun prototipi aşağıdaki gibidir. DWORD SetFilePointer( HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod ); -> Fonksiyonun birinci parametresi, açık dosyaya ilişkin "HANDLE" değeridir. -> Fonksiyonun ikinci ve üçüncü parametresi, konumlandırmaya ilişkin "offset" belirtmektedir. İkinci parametre konumlandırma "offset" değerinin düşük anlamlı 32-bit'lik değerini belitmektedir. Eğer üçüncü parametreye "NULL" değeri geçilirse, konumlandırma ancak dört "byte" lık "offset" temelinde yapılacaktır. Aksi halde ikinci ve üçüncü parametreler 64-bit'lik bir "offset" belirtmektedir. -> Fonksiyonun dördüncü parametresi, konumlandırmanın nereden itibaren yapılacağını belirtmektedir. Yani bir orjin belirtmektedir ki şu değerlerden birisi olabilir: "FILE_BEGIN", "FILE_CURRENT" ve "FILE_END". -> "FILE_BEGIN" : Konumlandırma dosyanın başından itibaren yapılır. -> "FILE_CURRENT" : Konumlandırma, dosya göstericisinin o andaki "offset" değerine göre yapılmaktadır. -> "FILE_END" : Konumlandırma "EOF" konumuna göre yapılmaktadır. -> Fonksiyon başarı durumunda konumlandırılmış olan "offset" in, dosyanın başından itibaren yerine geri dönmektedir. Eğer üçüncü parametreye "NULL" değeri geçilmemişse, konumlandırmanın yapıldığı "offset" in yüksek anlamlı 32-bit'Lik değeri buraya geçilen adrese yazılmaktadır. Fonksiyon başarısız olduğunda "INVALID_SET_FILE_POINTER" özel değerine geri dönmektedir. Ancak bu değer geçerli bir "offset" değeri de olabilmektedir. MSDN standartlarına göre, fonksiyon başarısız olduğunda, "GetLastError" fonksiyonundan elde edilen değerin "NO_ERROR" dışında bir değer olduğunu garanti etmektedir. * Örnek 1, Aşağıdaki örnekte dosyanın sonuna ilgili yazı eklenmiştir. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFile; if((hFile = CreateFile("test.txt", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE){ fprintf(stderr, "CreateFile failed! : %lu\n", GetLastError()); exit(EXIT_FAILURE); } puts("Ok..."); if(SetFilePointer(hFile, 0, NULL, FILE_END) == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR){ ExitSys("SetFilePointer"); } char buffer[] = "Ulya Yuruk"; DWORD dwWrite; if(!WriteFile(hFile, buffer, strlen(buffer), &dwWrite,NULL)) ExitSys("WriteFile"); puts(buffer); puts("Ok..."); CloseHandle(hFile); } void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte dosya konum göstericisi "EOF" konumuna alınmış, daha sonra 64-bit "offset" ile "-2" değeri kullanılarak 2 birim geriye konumlandırılmıştır. Bu noktadan okuma yapıldığında, dosyanın sonundaki son iki "byte" okunmuş olacaktır. #include #include #include #define BUFFER_SIZE 512 void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFile; if((hFile = CreateFile("test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE){ fprintf(stderr, "CreateFile failed! : %lu\n", GetLastError()); exit(EXIT_FAILURE); } puts("Ok..."); if(SetFilePointer(hFile, 0, NULL, FILE_END) == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR){ ExitSys("SetFilePointer"); } // WAY - I LONG high = -1; if(SetFilePointer(hFile, -2, &high, FILE_CURRENT) == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR){ ExitSys("SetFilePointer"); } /* // WAY - II long long offset; long high = (long)(offset >> 32); if(SetFilePointer(hFile, offset & 0xFFFFFFFF, &high, FILE_CURRENT) == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR){ ExitSys("SetFilePointer"); } */ char buffer[BUFFER_SIZE + 1]; DWORD dwRead; if(!ReadFile(hFile, buffer, BUFFER_SIZE, &dwRead,NULL)) ExitSys("ReadFile"); buffer[dwRead] = '\0'; puts(buffer); puts("Ok..."); CloseHandle(hFile); } void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Buradaki "offset" değeri 64-bitlik biçimde oluşturulmuştur. Bunu yaparken yüksek anlamlı ve düşük anlamlı olmak üzere bitleri iki grup 32-bit haline getirmemiz gerekmektedir. Örneğin, 64-bit "offset" olarak "-2" değerini şöyle oluşturabiliriz: FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFE Buradaki yüksek anlamlı 32-bit, "decimal" olarak, "-1" değerini ifade ederken düşük anlamlı 32-bit ise "-2" değerini ifade etmektedir. Şimdi de bu fonksiyonları kullandığımız örnekleri inceleyelim: bir dosyayı kopyalan program yazalım: * Örnek 1, Aşağıdaki program bir dosyayı kopyalamaktadır. Kullanılan "BUFFER_SIZE" büyüklüğünün ilgili işletim sistemindeki disk blok büyüklüğü ile aynı büyüklük olarak belirlenmesi genellikle performansı arttırmaktadır. #include #include #include #include #define BUFFER_SIZE 512 #define SOURCE_FILE_PATH "source.txt" #define DEST_FILE_PATH "dest.txt" void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFileSource, hFileDest; if((hFileSource = CreateFile(SOURCE_FILE_PATH, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE){ ExitSys("CreateFile"); } if((hFileDest = CreateFile(DEST_FILE_PATH, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL)) == INVALID_HANDLE_VALUE){ ExitSys("CreateFile"); } BOOL bResult; DWORD dwRead, dwWrite; char buffer[BUFFER_SIZE]; while((bResult = ReadFile(hFileSource, buffer, BUFFER_SIZE, &dwRead, NULL)) != 0 && dwRead > 0){ if(!WriteFile(hFileDest, buffer, dwRead, &dwWrite)){ ExitSys("WriteFile"); } if(dwWrite != dwRead){ fprintf(stderr, "Partial write error!..\n"); exit(EXIT_FAILURE); } } if(!bResult){ ExitSys("ReadFile"); } CloseHandle(hFileDest); CloseHandle(hFileSource); } void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte ise ilgili program dosya isimlerini komut satırından almaktadır. #include #include #include #include #define BUFFER_SIZE 512 void ExitSys(LPCSTR lpszMsg); int main(int argc, char** argv) { if(3 != argc){ fprintf(stderr, "Wrong number of arguments!..\n"); exit(EXIT_FAILURE); } HANDLE hFileSource, hFileDest; if((hFileSource = CreateFile(argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE){ ExitSys("CreateFile"); } if((hFileDest = CreateFile(argv[2], GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL)) == INVALID_HANDLE_VALUE){ ExitSys("CreateFile"); } BOOL bResult; DWORD dwRead, dwWrite; char buffer[BUFFER_SIZE]; while((bResult = ReadFile(hFileSource, buffer, BUFFER_SIZE, &dwRead, NULL)) != 0 && dwRead > 0){ if(!WriteFile(hFileDest, buffer, dwRead, &dwWrite)){ ExitSys("WriteFile"); } if(dwWrite != dwRead){ fprintf(stderr, "Partial write error!..\n"); exit(EXIT_FAILURE); } } if(!bResult){ ExitSys("ReadFile"); } CloseHandle(hFileDest); CloseHandle(hFileSource); } void ExitSys(LPCSTR lpszMsg){ DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL ) ) { fprintf(stderr, "%s : %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Tabii Windows sistemlerinde, dosyayı açmadan direkt dosya kopyalaması yapan, "CopyFile" isminde bir fonksiyon daha vardır. Dolayısıyla yukarıdaki örneklerde dosya açma/kapama sürelerini harcamak istemiyorsak, bu fonksiyonu kullanabiliriz. >> UNIX/Linux POSIX fonksiyonları: Bu fonksiyonların bildirimlerinin bulunduğu başlık dosyaları, Windows sistemlerine nazaran tek bir dosya içerisinde değildir. Muhtelif başlık dosyalarında mevcutturlar. Dolayısıyla ilgili başlık dosyasının da eklenmesi gerekmektedir. >>> "open" : UNIX/Linux sistemlerinde var olan bir dosyayı açmak ya da yeni bir dosya yaratmak için open isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int open(const char *path, int oflag, ...); "open" fonksiyonu ya iki argümanla ya da üç argümanla çağrılmaktadır. Eğer "open" fonksiyonu üç argümanla çağrılacaksa fonksiyon sanki "mode_t" türünden bir üçüncü parametreye sahipmiş gibi çağrılmalıdır. Yani "open" fonksiyonu sanki aşağıdaki iki biçimden biri gibi kullanılmaktadır: int open(const char *path, int oflag); int open(const char *path, int oflag, mode_t mode); "open" fonksiyonun birinci parametresi açılacak dosyanın yol ifadesini almaktadır. İkinci parametre açış modunu belirtmektedir. Bu ikinci parametre "O_XXX" biçimindeki sembolik sabitlerin "bit OR" işlemine sokulmasıyla oluşturulur. Bu ikinci parametrenin en azından aşağıdakilerden yalnızca birini içermesi gerekmektedir: "O_RDONLY" "O_WRONLY" "O_RDWR" "O_EXEC" "O_SEARCH" Bu sembolik sabitlerden, -> "O_EXEC" ve "O_SEARCH" bayrakları sonnradan eklenmiştir. Biz bunların üzerinde durmayacağız. -> "O_RDONLY" "yalnızca okuma amaçlı", "O_WRONLY" "yalnızca yazma amaçlı", "O_RDWR" ise "hem okuma hem de yazma amaçlı" dosyayı açma anlamına gelmektedir. Bu bayraklardan biri ile bazı bayraklar birlikte kullanılabilmektedir. -> "O_CREAT": Bu bayrak dosya yoksa dosyanın yaratılarak açılmasını sağlar. Ancak dosya varsa bu bayrağın hiçbir etkisi yoktur. Yani olan dosya açılr. -> "O_TRUNC": Bu bayrak dosya varsa dosyanın içinin sıfırlanarak açılacağını belirtir. Dosya yoksa bu bayrağın bir etkisi yoktur. Ancak bu bayrağın "O_WRONLY" ya da "O_RDWR" bayrağı ile kullanılıyor olması gerekmeketdir. Aksi takdirde tanımsız davranış söz konusu olur. -> "O_EXCL": Bu bayrak "O_CREAT" bayrağı ile birlikte kullanılmak zorundadır. "O_CREAT|O_EXCL" ile "dosya yoksa onu yarat ancak dosya varsa başarısız ol" anlamına gelmektedir. Böylec programcı var olmayan bir dosyayı kendisinin yarattığına emin olmaktadır. -> "O_APPEND": Bu modda eğer, bayraklar uygunsa, dosyanın herhangi bir yerinden okuma yapılabilir ancak tüm yazma işlemleri dosyanın sonuna yapılacaktır. Başka bir deyişle yazma işleminden önce atomik bir biçimde dosya göstericisi "EOF" durumuna çekilmektedir. "open" fonksiyonunda eğer yeni bir dosyanın yataılma potansiyeli varsa (yani ikinci parametrede "O_CREAT" bayrağı belirtilmilşse) bu durumda programcının fonksiyona "dosya erişim haklarını belirten" üçüncü bir argümanı girmesi gerekir. Bu konu izleyen paragraflarda ele alınacaktır. Ancak yeni bir dosyanın yaratılması gibi bir potansil yoksa (yani ikinci parametrede "O_CREAT" kullanılmamışsa) bu durumda programcı üçüncü argümanı girmemelidir. Tabii fonksiyonun ikinci aparametresinde "O_CREAT" bayrağı kullanılmışsa ancak dosya zaten varsa bu durumda programcının girdiği üçüncü argüman fonksiyon tarafından kullanılmayacaktır. "open" fonksiyonu başarı durumunda "dosya betimleyici (file descriptor)" denilen bir "handle" değerine başarısızlık durumunda ise "-1" değerine geri dönmektedir. Tipik kullanım biçimi aşağıdaki gibidir: int fd; ... if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); >>> "close" : UNIX/Linux sistemlerinde dosyayı kapatmak için "close" isimli POSIX fonksiyonu kullanılmaktadır. "close" fonksiyonuyla kapatılmamış olan dosyalar işletim sistemi tarafından proses sonlandığında otomatik biçimde kapatılmaktadır. Tabii açık dosyalar belli bir sistem kaynağı harcarlar. Bir dosya ile işi biten programcının dosyayı kapatması iyi bir tekniktir. "close" fonksiyonun prototipi şöyledir: #include int close(int fd); Fonksiyon dosya betimleyicisini parametre olarak alır ve dosyayı kapatır. Fonksiyon başarı durumunda "0" değerine, başarısızlık durumunda "-1" değerine geri dönmektedir. Ancak fonksiyonun başarısının kontrol edilmesine çoğu kez gerek yoktur. >>> "read" : Dosyadan okuma yapmak için "read" isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include ssize_t read(int fd, void *buf, size_t nbyte); Fonksiyonun birinci parametresi okuma işleminin yapılacağı dosyaya ilişkin dosya betimleyicini belirtmektedir. İkinci parametre okunan bilgilerin yerleştirileceği bellekteki transfer adresini belirtir. Üçüncü parametre ise okunmak istenen "byte" sayısını belirtmektedir. Fonksiyon başarı durumunda okunabilen "byte" sayısına, başarısızlık durumunda "-1" değerine geri dönmektedir. Fonksiyonun üçüncü parametresine dosya göstericinin gösterdiği yerden dosya sonuna kadar var olan "byte" sayısından daha büyük bir değer girilirse fonksiyon okuyabildiği kadar "byte" ı okur ve okuyabildiği "byte" sayısına geri döner. Eğer dosya göstericisi "EOF" durumundaysa bu durumda dosyadan hiç "byte" okunamaz ve fonksiyon "0" ile geri döner. Tabii bu durum fonksiyonun başarısız olduğu anlamına gelmez. "read" fonksiyonu ile "0" byte okunmak istenebilir. Bu durumda fonksiyon hata kontrollerini yapar, eğer bir hata söz konusu değilse "0" ile geri döner. Fonksiyonun geri dönüş değerinin "ssize_t" türünden olduğuna dikkat ediniz. "ssize_t" standart C typedef ismi değildir. POSIX sistemlerinde bulunmaktadır."ssize_t" türü "" dosyası içerisinde ve "" dosyası içerisinde "typedef" edilmiştir. "ssize_t" aslında "size_t" türünün işaretli versiyonu olarak bulundurmuştur. * Örnek 1, Aşağıda bir dosyadan "BUFFER_SIZE" kadar bilgiler döngü içerisinde read fonksiyonu ile okunarak ekrana ("stdout" dosyasına) yazıdırlmıştır. #include #include #include #include #define BUFFER_SIZE 512 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; char buf[BUFFER_SIZE + 1]; ssize_t result; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fd, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; printf("%s", buf); } if (result == -1) exit_sys("read"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "write" : UNIX/Linux sistemlerinde dosyaya yazma yapmak için "write" isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototiği şöyledir: #include ssize_t write(int fd, const void *buf, size_t nbyte); Fonksiyonun parametrik yapısı "read" fonksiyonundaki gibidir. "write" fonksiyonu da başarı durumunda yazılabilen "byte" sayısına başarısızlık durumunda "-1" değerine geri dönmektedir. "write" fonksiyonu ile istediğimiz kadar "byte" ın tamamanın yazılaması disk dosyaları için çok seyrek karşılaşılabilecek bir durumdur. "write" fonksiyonu ile de "0" "byte" yazılmak istenebilir. Bu durumda fonksiyon bazı hata kontrollerini yapar. Eğer bir hata söz konusu olmazsa 0 değeri ile geri döner. * Örnek 1, Aşağıdaki örnekte var olan bir dosyanın başına write fonksiyonu ile bir yazı yazılmıştır. Hedef dosyanın yol ifadesi komut satırı argümanı olarak alınmaktadır. #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; char buf[] = "this is a test"; ssize_t result; size_t len; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_WRONLY)) == -1) exit_sys("open"); len = strlen(buf); if ((result = write(fd, buf, len)) == -1) exit_sys("write"); if (result != len) { fprintf(stderr, "partial write error!...\n"); exit(EXIT_FAILURE); } close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "lseek" : UNIX/Linux sistemlerinde dosya göstericisinin konumlandırılması için "lseek" isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include off_t lseek(int fd, off_t offset, int whence); Fonksiyon C'nin "fseek" fonksiyonuna çok benzemektedir. Fonksiyonun birinci parametresi dosya betimleyicisini, ikinci parametresi konumlandırma "offset" ini ve üçüncü parametresi de konumlandırma orijinini almaktadır. Üçücncü parametre tamamen "fseek" fonksiyonundaki gibidir ve şu değerlerden birini alabilir: #define SEEK_SET 0 #define SEEK_CUR 1 #define SEEK_SEND 2 Fonksiyon başarı durumunda dosya göstericisinin dosyanın başından itibaren "offset" değerine, başarısızlık durumunda "-1" değerine geri dönmektedir. "off_t" türü "" ve "" içerisinde "işaretli bir tamsayı belirtmek koşuluyla" "typedef" edilmi olan bir türdür. "lseek" fonksiyonun başarısı da çoğu kez programsılar tarafındna kontrol edilmemektedir. > UNIX/Linux dünyasında dosya ve proseslere ilişkin ID kavramı: >> Proseslere İlişkin ID Kavramı: UNIX/Linux sistemlerinde bri kullanıcının sisteme girebilmesi için bir kullanıcı ismi ve bir de parolasının olması gerekir. Bu sistemlerde proseslere ilişkin "proses kontrol bloğunda turulan" şu önemli erişim bilgileri vardrı: -> Gerçek Kullanıcı Id'si (Real User Id) -> Gerçek Grup Id'si (Real Group Id) -> Etkin Kullanıcı Id'si (Effective User Id) -> Etkin Grup Id'si (Effective Group Id) Varsayılan durumda prosesin gerçek kullanıcı id'si ile etkin kullanıcı id'si, gerçek grup id'si ile etkin grup id'si aynıdır. Test işlemlerine her zaman etkin kullanıcı id'si ve etkin grup id'si sokulmaktadır. Pekiyi prosesin kullanıcı ve grup id'leri nasıl belirlenmektedir? İşte bize sisteme sokan yani bize kullanıcı ismini ve parolayı sorun aslında "login" isimli programdır. Bu program girdiğimiz kullanıcı ve parola doğruysa "/etc/passwd" dosyasında yazan kullanıcı id'si ve grup id'si ile yine burada belirtilen programı çalıştırmaktadır. Bu program da çoğu kez kabuk programı olmaktadır. Dolayısıyla biz sisteme "login" olduğumuzda aslında bizin gerçek ve etkin id'lerimiz "/etc/passwd" dsoyasında yazan değerlerle "set" edilmiş olmaktadır. Bu id'ler üst prosesten alt prosese aktarılırlar. Yani biz kabuktan bir program çalıştırdığımızda kabuğun gerçek ve etkin id'leri bizim programımıza yani prosesimize aktarılmaktadır. >>> "/etc/passwd" dosyası satırlardan oluşmaktadır. Her satır bir kullanıcıya ilişkindir ve ':' karakterleriyle 7 alana ayrılmıştır. Örneğin, student:x:1001:1001:Student,,,:/home/student:/bin/bash Bu satırdaki, -> İlk alan kullanıcı ismini belirtir. Burada kullanıcı ismi "student" biçimindedir. -> İkinci alan kullanıcı şifresine ilişkindir. Burada 'x' varsa bu şifre bilgisinin "/etc/shadow" dosyası içerisinde olduğunu belirtir. Şifre doğrudan bu alanda da tutulabilmektedir. Ancak bu alanda şifre "şifrelenmiş bir biçimde" tutulmaktadır. -> Üçüncü alanda kullanıya ilişkin "gerçek kullanıcı id'si (real user id)" bulunmaktadır. "login" programı prosesin etkin kullanıcı id'sini ve gerçek kullanıcı id'sini burada belirtilen biçimde set etmektedir. -> Dördüncü alanda kullanıcının ilişkin olduğu gerçek grup id'si "(real group id)" tutulmaktadır. "login" programı prosesin gerçek grup id'si ve etkin grup id'sini bu değer olarak "set" etmektedir. -> Beşinci alanda kullanıcıya ilişkin bazı bilgiler bulunmaktadır. Ancak bu bilgiler sistemin işleyişi ile ilgili değildir. -> Altıncı alanda kullanıya prosesin "çalışma dizini (current working directory)" tutulmaktadır. -> Yedinci alanda login başarılıyken çalıştırılacak prgram bulunur. Buradaki "bash (Bourne Again Shell)" programı en çok kullanılan kabuk programıdır. >> Dosyalara İlişkin ID Kavramı: UNIX/Linux sistemlerinde aynı zamanda her dosyanın ve dizin'in de bir "kullanıcı id'si (user id)" ve "grup id'si (group id)" vardır. Ancak dosyalar söz konusu olduğunda "gerçek" ve "etkin" kavramları söz konusu değildir. Yani dosyaların gerçek ve etkin biçiminde iki id'si yoktur. Tek bir id'si vardır. Bir dosyanın kullanıcı ve grup id'leri "ls -l" komutuyla görüntülenebilir. Örneğin: kaan@kaan-virtual-machine:~/Study/SysProg$ ls -l toplam 76 -rwxr-xr-x 1 kaan study 16136 Haz 17 18:51 a.out -rw-r--r-- 1 kaan study 3570 Haz 18 18:57 disp.c -rw-r--r-- 1 kaan study 1622 Haz 11 20:40 mample.c ...... Aslında dosya sisteminde dosyaların kullanıcı id'leri ve grup id'leri sayısal biçimde tutulmaktadır. Ancal "ls" programı "/etc/passwd" ve "/etc/group" dosyalarına başvurarak onların isimlerini yazdırmaktadır. UNIX/Linux sistemlerinde her dosyanın "erişim hakları" vardır. Bu erişim hakları "ls -l" komutunda 10 tane karakterden oluşan bir sütunda görüntülenmektedir. Örneğin: -rw-r--r-- 1 kaan study 3570 Haz 18 18:57 disp.c Buradaki karakterlerin en solundaki karakter dosyanın türünü belirtmektedir. Dosya türü olarak '-' "sıradan bir disk dosyası (regular file)" anlamına gelmektedir. Budada 'd' varsa bu dosyanın bir "dizin (directory)" olduğu anlamına gelir. Başka dosya türleri de vardır. Dosya türünden sonraki karakterler üçlü üç grup oluşturmaktadır: - rwx rwx rwx İlk üçlü gruba "owner", sonraki üçlü gruba "group" ve sonraki üçlü gruba "other" denilmektedir. Bu gruplar dosyanın sahibinin, dosya ile aynı grupta olanların ve herhangi kişilerin bu dosya üzerinde ne yapabileceklerini belirtir. Eğer burada bir hak varsa biz ilgili pozisyonda "r", "w" ya da "x" harflerini görürüz. Eğer burada bir hak yoksa biz ilgili pozisyonda "-" karakterini görürüz. Örneğin bir dosyanın erişim hakları şöyle olsun: -rw-r----- Bu dosyaya dosyanın sahibi "(owner)" okuma ve yazma yapabilir. Dosyanın grubuyla aynı gruptan olan kişiler bu dosyadan yalnızca okuma yapabilirler. Herhangi kişiler bu dosya üzerinde işlem yapamazlar. Bir dosyanın erişim haklarındaki 'x' kısmında '-' var ise bu durum ya "dosyanın zaten çalıştırılabili bir dosya olmadığı" ya da "ilgili kişi çalıştırma hakkı verilmediği" anlamına gelmektedir. >>> Dosya erişim hakları "open" fonksiyonu tarafından kontrol edilmektedir. Kontrol işlemi sırasıyla şöyle yapılmaktadır: -> "open" fonksiyonu önce prosesin etkin kullanıcı id'sine bakar. Eğer prosesin etkin kullanıcı id'si 0 ise bu durumda bir kontrol yapılmaz. İstek kabul edilir. Etkin kullanıcı id'si 0 olan proseslere "root process" ay da "super user process" denilmektedir. -> "open" fonksiyonu prosesin etkin kullanıcı id'si ile dosyanın kullanıcı id'sine bakar. Eğer bunlar aynıysa dosyanın sahibi ilgili prosese ilişkin kullanıcıdır. Bu durumda erişim haklarındaki "owner" kısmı dikkate alınarak erişim onayı verilir. -> "open" fonksiyonu prosesin etkin grup id'si ile dosyanın grup id'sine bakar. Eğer bunlar aynıysa proses dosyanın grubuyla aynı grupta olan bir kullanıcıya ilişkindir. Bu durumda erişim haklarının "group" kısmı dikkate alınarak erişim onayı verilir. -> open fonksiyonu dosyanın erişim haklarının "other" kısmına bakarak erişim onayı verir. * Örnek 1.0, bir prosesin etkin kullanıcı id'si "kaan" (1000) ve etkin grup id'si, "study" (1002) olsun ve aşağıdaki gibi bir dosyanın bulunduğu varsayalım: -rw-r--r-- 1 ali study 3570 Haz 18 18:57 test.txt Bu proses dosyayı "open" fonksiyonula şöyle açmaya çalışmış olsun: "fd = open("test.txt", O_RDWR);" Burada "open" fonksiyonu başarısız olacaktır. "open" fonksiyonunu çağıran prosesin etkin kullanıcı id'si "0" değildir. Prosesin etkin kullanıcı id'si dosyanın kullanıcı id'si ile de aynı değildir. Ancak prosesin etkin grup id'si dosyanın grup id'si ile aynıdır. Bu durumda erişim haklarının "group" kısmı dikkate alınır. Erişim haklarının group kısmında "r--" vardır. Halbuki proses dosyaysı "O_RDWR" modunda açmak istemiştir. O halde "open" başarısız olur ve "errno" "EPERM" değeri ile "set" edilir. Tabii aynı proses dosyayı şöyle açabilirdi: "fd = open("test.txt", O_RDONLY);" * Örnek 1.1, Şimdi aynı dosyaya etkin kullanıcı id'si "veli (1005)" olan etkin group id'si de "school (1004)" olan bir proses aşağıdaki gibi "open" uygulamış olsun: "fd = open("test.txt", O_RDONLY);" Burada prosess dosyaya göre "other" durumundadır. O zaman erişimde "other" kısım dikkate alınacaktır. Dosyanın "other" erişim hakları "r--" biçimindedir. Bu durumda "open" başarılı olarak dosyayı açar. * Örnek 2, Diğer bir dosyanın erişim hakları şöyle olsun ---------- 1 ali study 3570 Haz 18 18:57 x.txt Bu dosyayı etkin kullanıcı id'si 0 olan bir proses aşağıdaki gibi open fonksiyonuyla açmak istesin: "fd = open("x.txt", O_RDWR);" Etkin kullanıcı id'si 0 olan proseslere hiçbir kontrol uygulanmadığı için open başarılı olacaktır. Bir kullanıcının dosyasına aşağıdaki gibi bir erişim hakkı vermesi geçerli olsa da tuhaftır: "-r--rw-rw-" Burada dosyanın sahibi bu dosyaya yazma yapamayacaktır. Ancak dosyayla aynı gruptaki prosesler ve herhangi prosesler dosyaya yazma yapabilecektir. Bir programı sudo yaparak çalıştırdığımızda aslıdna program etkin kullanıcı id'si 0 olacak biçimde çalıştırılmaktadır. Dolayısıyla biz yetkisizlikten bir şeyi yapamıyorsak "sudo" ile bunu yapmaya çalışabiliriz. Tabii modern UNIX/Linux sistemlerinde her kullanıcı "sudo" yapamamaktadır. Ayrıca "sudo" yapabilmek için kurulum sırasındaki "root parolasının" kullanıcı tarafından biliniyor olması gerekmektedir. UNIX/Linux sistemlerindeki güvenlik mekanizması "ya hep ya hiç" esasıyla tasarlanmıştır. Eğer proses "root" proses ise (yani prosesin etkin kullanıcı id'si 0 ise) proses her şeyi yapabilmektedir. Ancak proses "root" prosesi değilse (yani prosesin etkin kullanıcı id'si 0 değilse) proses yalnızca kendisiyle ilgili şeyleri yapabilmektedir. İşte bu "yap hep ya hiç" sistemi bazı UNIX türevi sistemlerde çeşitli biçimlerde geliştirilmek istenmiştir. Örneğin, Linux sistemleri "capability" denilen bir özelliğe sahiptir. Linux sistemlerinde bir proses "root" olmadığı halde bazı "capability" özelliklerine sahip olabilir. Bu durumda o konuya ilişkin işlemleri sanki "root" prosesmiş gibi yapabilir. Linux sistemlerinde bu biçimde çeşitli konulara ilişkin "capablity" ler oluşturulmuştur. Bazı sistemler, (Linux'ta isteğe bağlı) "ACL (access Contol List)" denilen bir mekanizmaya da sahiptir. POSIX standartları "capability" ya da "ACL" konularını kapsamamaktadır. Ancak POSIX standartları UNIX türevi işletim sistemlerinin bu tür özelliklere sahip olabileceği fikriyle tasarlanmıştır. Bu nedenle POSIX standartlarında "root önceliği" ya da "etkin kullanıcı id'nin 0 olması" gibi ifadeler yerine "uygun öncelik (appropriate privilege)" terimi kullanılmaktadır. POSIX'in "uygun öncelik" terimi Linux için "ya prosesin etkin kullanıcı id'sinin 0 olması ya da prosesin o işlemi yapabilecek capability'ye sahip olması" anlamına gelmektedir. Biz de kursa notlarımızda "root önceliği ya da etkin kullanıcı id'sinin 0 olması" yerine "prosesin uygun önceliğe sahip olması" terimini kullanacağız. >>> Pekiyi UNIX/Linux sistemlerinde bir dosyanın kullanıcı ve grup id'leri nasıl "set" edilmektedir? İşte dosyanın kullanıcı ve grup id'leri bu dosya ilk kez yaratılırken belirlenmektedir. Dosyanın kullanıcı id'si her zaman onu yaratan prosesin etkin kullanıcı id'si olarak "set" edilmektedir. Ancak yeni yaratılan dosyanın grup id'sinin "set" edilmesi konusunda sistemlerde bir anlaşmazlık olmuştur. Bu nedenle POSIX standartları oluşturulurken o anki mevcut sistemler için ikili semantik benimsenmiştir. Şöyle ki "yeni yaratılan dosyanın grup id'si ya onu yaratan prosesin etkin grup id'si olarak (System 5 semantiği) ya da o dosyanın içinde bulunduğu dizin'in grup id'si olarak (BSD semantiği) set edilmektedir. Linux default durumda yeni yaratılan dosyanın grup id'sini onu yaratan prosesin etkin grup id'si olarak set etmektedir. Pekiyi dosyanın erişim hakları nasıl belirlenmektedir? İşte dosyalar her zaman open fonksiyonuyla yaratılır. "open" fonksiyonunda fonksiyonun üçüncü parametresi erişim haklarını belirtmektedir. Daha önceden de belirttiğimiz gibi eğer "open" fonksiyonun ikinci parametresinde "O_CREAT" bayrağı kullanılmışsa dosyanın yaratılması söz konusu olabilmektedir. Bu durumda programcı üçüncü parametreyi erişim hakları olacak biçimde oluşturmalıdır. Erişim hakları UNIX/Linux sistemlerinde genel olarak "mode_t" türü ile temsil edilmektedir. Bu tür "" ve "" dosyası içerisinde bir tamsayı türünden olacak biçimde "typedef" edilmiştir. "open" fonksiyonunda erişim haklarını oluşturmak için içerisinde bulunan "S_IXXX" biçimindeki sembolik sabitler bit OR işlemine sokulmaktadır. Bu sembolik sabitlerin listesi şöyledir: S_IRUSR S_IWUSR S_IXUSR S_IRGRP S_IWGRP S_IXGRP S_IROTH S_IWOTH S_IXOTH Buradaki sembolik sabitlerin hepsi "S_I" öneki ile başlatılmıştır. Bunu "R", "W" ya da "X" harfleri izler. Bu harfleri de "USR", "GRP" ya da "OTH" harfleri izlemektedir. Örneğin, "S_IRUSR|S_IWUSR|S_IRGRP|_SIROTH" hakları "rw-r--r--" anlamına gelmektedir. Yukarıda sembolik sabitlerin sayısal değerleri POSIX 2008'e kadar sşstemdne sisteme değişebilir biçimdeydi. Ancak daha sonra POSIX standartları bu sembolik sabitlerin değerlerini tamamen bir tamsayının düşük anlamlı 9 biti olarak belirlemiştir. Aşağıda bir tamsayının 9 bitine karşılık gelen erişim hakları verilmiştir: "rwx rwx rwx" Bir octal digit "3-bit" ile açıldığına göre POSIX 2008 ve sonrasında artık bu erişim hakları octal bir sayı biçiminde kolay bir şekilde girilebilmektedir. Örneğin "0644" octal sayısı ikilik sistemde "110 100 100" biçimindedir. Bu da "rw-r--r--" anlamına gelmektedir. Başka bir deyişle "S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH" işleminin eşdeğeri "0644" tür. Ancak eski sistemler bu sembolik sabitleri bu biçimde define etmemiş olabilirler. Dolayısıyla erişim hakları için doğrudan bir syaı girmek yerine "S_IXXX" sembolik sabitlerini kullanmak daha uygun olabilir. Ayrıca "" dosyası içerisinde aşağıdaki gibi üç sembolik sabit daha vardır: #define S_IRWXU (S_IRUSR|S_IWUSR|S_IXUSR) #define S_IRWXG (S_IRGRP|S_IWGRP|S_IXGRP) #define S_IRWXO (S_IROTH|S_IWOTH|S_IXOTH) Bu durumda tüm erişim haklarını vermek için "S_IRWXU|S_IRWXG|S_IRWXO" işlemi yapılabilir. Bu zaten "0777" ile aynı anlamdadır. Aşağıda "open" fonksiyonu ile dosya yaratmaya bir örnek verilmiştir: int fd; ... if ((fd = open("y.txt", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); Burada bir noktaya yeniden dikkatinizi çekmek istiyoruz: "open" fonksiyonun ikinci parametresinde "O_CREAT" bayrağının kullanılm olması üçüncü parametrenin fonksiyon tarafından kesinlikle kullanılacağı anlamına gelmemektedir. Eğer dosya yoksa dosya yaratılırken bu üçüncü parametre kullanılmaktadır. "open" fonksiyonun üçüncü parametresi eğer dosya yaratılacaksa ikinci parametresi üzerinde etkili olmamaktadır. Örneğin: fd = open("z.tzt", O_RDWR|O_CREAT, 0); Burada dosyanın yaratılacağını varsayalım. Dosyayı yaratan kendine "read" ve "write" hakkı da vermemiştir. Ancak "open" fonksiyonu bu yaratımda başarılı olacaktır. Tabii bundan sonra artık dosyanın sahibi "open" fonksiyonu ile dosyayı herhangi bir modda açamayacaktır. Aslında "open" fonksiyonunda üçüncü parametrede verilen erişim hakları nihai erişim hakları değildir. Bu erişim hakları "prosesin umask değeri" denilen bir değerle işleme sokulur. Nihai erişim hakları bu işlem sonucunda belirlenir. Prosesin "umask" değerinde belirtilen haklar "open" fonksiyonunda girilse bile dosyaya verilmemektedir. Prosesin "umask" değeri üst prosesten alt prosese aktarılmaktadır. Kabuk programının "(bash)" "umask" değeri "umask" isimli komutla elde edilebilir. Örneğin: $umask 0022 Buradaki "umask" değeri "octal" değer olarak görüntülenmektedir. Yukarıdaki "octal" değerini "binary" açılımı şöyledir: 000 010 010 Burada gruba "write" hakkı ve "other" a "write" hakkı ortadan kaldırılmıştır. Başka bir deyişle open fonksiyonuna girdiğimiz erişim hakları mode olmak üzere nihai erişim hakları "mode & ~umask" biçimindedir. Yani "umask" değerindeki 1 olan bitlere karşı gelen haklar aslında silinmektedir. Kabuk programının umask değeri de değiştirilebilir. Bu durumda kabuktan çalıştırdığımız programların da umask değeri değişecektir. Örneğin: $umask 0 $umask 0000 Bir proses kendi umask değerini istediği zaman "umask" isimli POSIX fonksiyonu ile değiştirebilir. Bunun için bir koşul gerekmemektedir. >>>> "umask" fonksiyonu: Fonksiyonun prototipi şöyledir: #include mode_t umask(mode_t cmask); Fonksiyon yeni "umask" değerini parametre olarak alır ve eski umask değerini verir. Parametre için argümanı POSIX 2008 ve sonrasında sayısal biçimde verebiliriz. Ancak "S_IXXX" sembolik sabitleriyle vermek iyi bir tekniktir. Fonksiyon başarısız olamamaktadır. O halde biz "open" fonksiyonunda verdiğimiz erişim haklarının aynısının dosyaya yansıtılmasını istiyorsak programın başında prosesin "umask" değerini 0'a çekebiliriz. Örneğin: int fd; ... umask(0); if ((fd = open("z.txt", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)) == -1) exit_sys("open"); Prosesin "umask" değerini alan arı bir fonksiyon yoktur. O zaman mecburen prosesin umask değerini almak için onu set ediyormuş gibi yapmak gerekir. Örneğin: mode_t mode; ... mode = umask(0); umask(mode); Pekiyi prosesler için "umask" değerine nedne gereksinim duyulmuştur? Bunun birkaç nedeni vardır. Birincisi "umask" dikkatsizlikle verilen erişimlerin ortadan otomatik biçimde kaldırılmasını sağlamaktadır. "umask" değeri aynı zamanda başka programlarda "default" erişim haklarını ayarlamak için de kullanılmaktadır. Örneğin biz bir C kütüphanesi yazacak olalım. fopen fonksiyonunda dosya yaratırken erişim haklarını nasıl vermeliyiz? İşte UNIX/Linux sistemleri için standart C kütüphanesini yazanlar genellikle "default" erişim haklarını "rw-rw-rw" biçiminde vermektedirler. Fakat bu işlem prosesin umask değerinden etkileneceği için biz programı çalıştırmadan önce ya da programın içerisinde umask değerini değiştirerek bunu dışarıdan değiştirmiş gibi oluruz. Başka bir deyişle prosesin umask değeri başkaları tarafından yazılmış olan kodlarda yaratılan dosyalara ilişkin erişim haklarını değiştirmekte kullanılabilmektedir. > Hatırlatıcı Notlar: >> Bir bit'i bir, diğerleri sıfır olan değerleri "&" operatörü ile işleme sokmak: #include #define GENERIC_READ 0x0001 #define GENERIC_WRITE 0x0010 void foo(int flags){ if(flags & GENERIC_READ) printf("Read\n"); if(flags & GENERIC_WRITE) printf("Write\n"); } int main(void){ /* # OUTPUT # Read Write Read Write */ foo(GENERIC_READ); foo(GENERIC_WRITE); foo(GENERIC_READ | GENERIC_WRITE); } >> "Visual Studio" ile "debugger" çalıştırdığımız vakit, yani F11 tuşuna bastığımızda, standart C fonksiyonlarının içerisine girmesini istiyorsak, "Araçlar > Seçenekler > Hata Ayıklama / Yalnızca Kendi Kodumu Etkinleştir" özelliğini "uncheck" yapıyoruz. >> Anımsayacağınız üzere C dilinde "char" türü dışındaki türlerin kaç "byte" yer kaplayacağı, bazı asgari kısıtlar konularak, derleyicileri yazanlara bırakılmıştır. Windows ve Linux sistemlerinde 32-bit ve 64-bit C ve C++ derleyicilerinde, "int" türü dört "byte" yer kaplamaktadır. 32-bit ve 64-bit Windows sistemlerinde "long" türü de dört "byte" yer kaplamaktadır. Ancak 64-bit Linux sisteminde "long" türü sekiz "byte" uzunluk kaplamaktadır. C11 ile birlikte "long long" türü ise bütün sistemlerde sekiz "byte" uzunluktadır. >> "Visual Studio" üzerinden komut satırı argümanı belirtmek için " / Proje Özellikleri / Yapılandırma Özellikleri / Hata Ayıklama" kısmına gelip "Bağımsız Komut Değişkenleri" özelliğine ilgili argümanları aralarında boşluk bırakarak geçmeliyiz. Eğer "shell" programı üzerinden girmek istiyorsak, o anki çalışma dizinimiz ilgili ".exe" dosyasının bulunduğu dizin olmalıdır. Sonrasında "sample ..\test.txt mest.txt" biçimindeki bir komut çalıştırarak, bir önceki dizin içerisinde bulunan "test.txt" isimli dosyayı şu anki dizinimize "mest.txt" ismi ile kopyalamış oluyoruz. >> Windows sistemlerinde aslında bir kullanıcının oluşturduğu dosyaya erişim pekala kısıtlanabilmektedir. Ancak Windows sistemlerindeki karmaşık güvenlik mekanizmasından dolayı, bu konunun detaylarınd Windows Sistem Programlama kursunda değinilecektir. >> Aslında POSIX'teki bütün "typedef" isimleri toplu halde "" içerisinde typedef edilmiş durumdadır. Ancak kolaylık sağlamak amacıyla bazı "typedef" isimleri "" dosyasının yanı sıra başka başlık dosyalarında da "typedef" edilmiş durumdadır. >> UNIX/Linux dünyasında kullanılan dosya sistemleri ve Microsoft'un "NTFS" dosya sistemi "dosya deliği (file hole)" denilen bir özelliğe sahiptir. Bu sistemlerde dosya göstericisi "EOF" ötesine konumlandırılıp "WriteFile/write" fonksiyonu ile yazma yapılırsa aradaki bölgeye "delik (hole)" denilmektedir. Dosya delikleri gerçek anlamda diskte tahsis edilemzler ve dosya deliklerinden okuma yapıldığında "0" "byte" ları okunur. * Örnek 1, Aşağıda dosya deliği oluşturma bir örnek verilmiştir. Program çalıştırıldıktan sonra dosyanın uzunluğu ve dosyanın diskte kapladığı alan aşağıdaki gibidir: $ls -l test.txt -rw-r--r-- 1 kaan study 5000001 Tem 22 20:07 test.txt $du test.txt 8 test.txt Burada dosyanın uzunluğu "5000001" byte gözüktüğü halde diskte kapladığı alan 8 * 1024 = 8192 "byte" tır. Aşağıda çalıştırılan programın kodu verilmiştir: #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("test.txt", O_WRONLY)) == -1) exit_sys("open"); if (lseek(fd, 5000000, SEEK_SET) == -1) exit_sys("open"); if (write(fd, "x", 1) == -1) exit_sys("write"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >> "Text-mode" ve "binary-mode" kavramları işletim sistemlerine özgü kavramlar değildir. İşletim sistemlerinin sistem fonksiyonları dosyaları "byte" toplulukları olarak görür. Dosyanın "text" dosya mı "binary" dosya mı olduğu işletim sisteminin çekirdeğini ilgilendirmemektedir. Bunlar yüksek seviyeli kavramlardır. >> UNIX/Linux sistemlerinde geleneksel olarak her kullanıcı için "/home" dizininin altında bir dizin bulundurulmaktadır. UNIX/Linux sistemlerinde bir grup kullanıcının oluşturduğu topluluğa "grup (group)" denilmektedir. Grup bilgileri "/etc/group" dosyasında saklanmaktadır. Bu sistemlerde "/etc" dizini sistem ile ilgili çeşitli konfigürasyon bilgilerinin tutulduğu ana bir dizindir. Linux sistemleri kurulurken zaten kurulum programları aynı zamanda bir kullanıcı da oluşturmaktadır. Ancak daha sonra başka kullanıcılar da oluşturulabilir. Bu işlemler "adduser" ya da "useradd" komutlarıyla yapılabieceği gibi manuel olarak "/etc/passwd" dosyasına satır ekleyerek de yapılabilmektedir. >> UNIX/Linux sistemlerinde dosya kopyalamaya bir örnek. * Örnek 1, "./mycp [-i -n][--interactive --no-clobber] ". Bir dosyanın olup olmadığını anlamak için ve ilgili prosesin dosyaya okuma/yazma/çalıştırma işlemini yapıp yapamayacağını anlayabilmek için "access" isimli bir POSIX fonksiyonu kullanılmaktadır. Bu fonksiyon "open" ile dosyayı açmaktan daha etkin bu işlemi yapabilmektedir. #include #include #include #include #include #include #include #define BUF_SIZE 8192 void exit_sys(const char *msg); int main(int argc, char *argv[]) { int result; int i_flag, n_flag; int err_flag; char buf[BUF_SIZE]; int fds, fdd; ssize_t n; struct option options[] = { {"interactive", no_argument, NULL, 'i'}, {"no-clobber", required_argument, NULL, 'n'}, {0, 0, 0, 0 }, }; opterr = 0; i_flag = n_flag = 0; err_flag = 0; while ((result = getopt_long(argc, argv, "in", options, NULL)) != -1) { switch (result) { case 'i': i_flag = 1; break; case 'n': n_flag = 1; break; case '?': 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 != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (n_flag || i_flag) { if (access(argv[optind + 1], F_OK) == 0) { if (n_flag) { fprintf(stderr, "file already exits! (-n specified)\n"); exit(EXIT_FAILURE); } if (i_flag) { printf("file already exists! Overwrite? (Y/N):"); if (tolower(getchar()) != 'y') exit(EXIT_FAILURE); } } } if ((fds = open(argv[optind], O_RDONLY)) == -1) exit_sys("open"); if ((fdd = open(argv[optind + 1], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("open"); while ((n = read(fds, buf, BUF_SIZE)) > 0) if (write(fdd, buf, n) == -1) exit_sys("write"); if (n == -1) exit_sys("read"); close(fds); close(fdd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (15_30_07_2023) > "Text Mode" & "Graphical Mode" : Bugün kullandığımız ekranlarda temelde iki çalışma modu vardır: Text mod ve grafik mod. >> Text modda karakterler bir kalıp biçiminde ekrana basılır. Bu modda pixel temelinde bir kontrol yoktur. Her karakter için matriste belli bir yer ayrılmıştır. O yere kaarakterler kalıp olarak basılmaktadır. Text mod ekranlara konsol ekranı da denilmektedir. Eskiden yalnızca text modda çalışma söz konusuydu. Sonra zamanla donanım teknolojisi gelişince grafik modda çalışma da yaygınlaşmaya başladı. Eğer biz programı text modda çalışacak biçimde oluşturmuşsak bu tür programlara "konsol tabanlı (console based)" programlar da denilmektedir. Text modda bir imleç (cursor) vardır. stdout dosyasına yazılan şeyler bu imlecin bulunduğu yerden itibaren yazılırlar. Sonra imleç yazılan miktar kadar ilerletilir. Örneğin printf fonksiyonu stdout dosyasına yazar. Bu stdout dosyası default durumda pek çok sistemde terminal aygıt sürücüsüne yönlendirilmiştir. Terminal aygıt sürücüsü de yazılacak şeyleri imlecin bulunduğu yere yazar ve imleci yazılan karakter sayısı kadar ilerletir. Text modda imlecin ilerletilmesi sütun bittiğinde sonraki satırın başından itibaren yapılmaktadır. Text modda son satırdan sonra ekrana bir şeyler yazılmak istendiğinde her satır bir yukarı kaydırılır. Bu işleme "scroll" ya da "scroll up" denilmektedir. Text modda çalışmak çok hızlıdır. Text mod genel olarak basit bir arayüz oluşturmaktadır. Bu nedenle pek çok programalama dili çıktı bağlamında text modda uygun tasarlanmıştır. 25 satırlı 80 sütun eskiden beri en yaygın kullanılan text mod çözünürlüğüydü. Buna standart text mod çözünürlüğü denilmektedir. >> Grafik modda ekrandaki en küçük birim bir pixel'dir. Pixel ("picture element" sözcüklerinden kısaltılmıştır) ekranda görüntülenecek en küçük grafik öğedir. Grafik modda her şey ğixel'lerin uygun biçimde bir araya getirilmesiyle oluşturulmaktadır. Ekran çözünürlükleri pixel matrisinin genişlik ve yükseklik değeri ile belirtilmektedir. Örneğin 1920X1080 çözünürlük demek ekranda toplam "1980 * 1020" tane pixel var demektir. Text modda yalnızca karakter görüntülenebilmektedir. Halbuki grafik modda pixel'lerle her şey görüntülenebilmektedir. Biz bir resmi text modda görüntüleyemeyiz. Ancak grafik modda görüntüleyebiliriz. Bir program çıktısını pixel düzeyinde oluşturuyorsa buı tür programlara "GUI (Graphical User Interface) tabanlı programlar" denilmektedir. Örneğin Excel, Word gibi programlar GUI tabanlı programlardır. Diğer yandan, >> Text mod için program yazmak oldukça kolaydır. Ancak grafik modda program yazmak için ayrı bir bilgi gerekmektedir. C'nin standart fonksiyonlarıyla GUI tabanlı programlar yazılamaz. C'de GUI tabanlı programlar yazabilmek için özel kütüphanelerden faydalanmak gerekir. Bugün GUI tabanlı programlama oldukça yaygın kullanılmaktadır. Ancak GUI çalışma modeli konsole çalışma modeline göre oldukça yavaştır. Bu nedenle pek çok temel araç GUI çalışma modeli yerine konsole çalışma modeline göre tasarlanmış durumdadır. Örneğin derleyiciler GUI arayüzü arayüzü kullanmazlar. Klasik konsole tabanlı bir arayüz kullanırlar. Windows ve macOS sistemleri GUI çalışmanın ön planda olduğu sistemlerdir. Ancak UNIX/Linux dünyasında hala ağırlık konsol tabanlı çalışma modelindedir. Örneğin Linux sistemleri ağırlıklı olarak server sistemlerinde ve gömülü sistemlerde kullanılmaktadır. Burada grafik tabanlı çalışma yavaş olduğu gerekçesiyle ya da güçlü donanım gerektirmesinden dolayı tercih edilmemektedir. Tabii bugün kullanıdğımız Linux dağıtımları genellşkle bir grafik arayüz bulundurmaktadır. Ancak bu grafik arayüz kolaylıkla devre dışı bırakılabilmektedir. >> Bugün kullandığımız grafik kartlarında ekranın bir bölümü text bir bölümü grafik modda olamamaktadır. Ancak konsole tabanlı uygulamalar için GUI arayüzleri konsole ekranını bir pencere içerisinde pixel işlemleriyle emüle edebilmektedir. Örneğin Windows sistemlerinde biz aslında grafik modda çalışmaktayız. Ancak burada "cmd.exe" programını çalıştırdığımızda sanki klasik konsole sisteminde çalışıyormuşuz gibi bir emülasyon yapılmaktadır. Yani burada text mod görüntüsü sahte bir görüntüdür. Yalnızca klasikj konsol programlarının çalışabilmesi için pixel işlemleriyle oluşturulmuştur. Yani bugün grafik arayüze sahip işletim sistemlerinde biz bir konsol terminali açtığımızda sanki o terminal penceresi eski devirdeki text modda çalışan bir konsol penceresini taklit etmektedir. >> Renkli görüntünün bilgisayar ekranlarına oluşturulması 80'li yılların ortalarında başlamıştır. Text modda da grafik modda da bir renk olgusu vardır. Grafik modda her pixel ayrı bir renkle gösterilebilmektedir. Yaygın teknolojide Kırmızı (red), Yeşil (Green) ve Mavi (Blue) olmak üzere üç temel renk vardır. Diğer bütün renkler bu üç temel rengin tonal birleşimleriyle oluşturulmaktadır. Günümüz teknolojisinde bu üç ana renk toplam 256 farklı ([0, 255 arasında]) tonal değere sahiptir. Dolayısıyla grafik ekranlarda her pixel "256 * 256 * 256 ~= 16 milyon" renkten biri ile renklendirilebilmektedir. Ayrıca her pixel için modern grafik kartlarında ismine "alpha channel" denilen yine [0, 255] değer alan bir bilşen daha bulunudurlmaktadır. Bu bileşen pixel'in transparanlığını ayarlamakta kullanılmaktadır. Pekiyi eskiden text modda renk kavramı var mıydı? Evet grafik modun çok seyrek kullanıldığı yıllarda da yavaş yavaş text moda renk olgusu sokulmultur. Text moddaki her bir karakter matrisinin zemini ve şekli yarı ayrı boyanabilmektedir. Ayrıca "bold" ve "reverse" özel durumlar da söz konusu olmaktadır. Tabii bugün grafik arayüzlerdeki terminal ekranları aslında pixel temelinde emüle edildiğinden bu zemin ve şekil renkleri de aslında pixel temelinde oluşturulmaktadır. Pekiyi konsole ekranında renkli bir yazıyı nasıl oluşturabiliriz? Ya da konsol ekranında ekranın istediğimiz yerine bir yazıyı nasıl yazdırabiliriz? Bilindiği gibi C'de bunları yapabilecek standart fonksiyonlar yoktur. İşte C'de text ekranda imleçle ilgili işlemler ya da renkli yazım işlemleri temelde iki yolla yapılmaktadır: -> ANSI terminal komutları ile -> Özel kütüphane fonksiyonları ile Bu yöntemlerden, >> ANSI Terminal Komutları: stdout için aygıt sürücülerini yazanlar standart bazı özel karakter için bazı özel işlemleri yapacak biçimde aygıt sürücülerini yazmaktadır. Bu özel karakterler kullanılarak yazılmış olan komutlara ANSI terminal komutları denilmektedir. ANSI terminal komutları 0x1B karakteri ile başlatılır. ASCII tablosounda 0x1B karakterine "escape karakteri" denilmektedir. Dolayısıyla ANSI terminal komutlarına "ANSI escape komutları" da denilebilmektedir. Tüm ANSI terminal komutları bir escape karakteriyle başlatılır ve bu escape karakterini bazı başka karakterler izler. Örneğin imleci (cursor) belli bir yere taşımak için stdout dosyasına (aygıt sürücüye) gönderilecek escape komutu şöyledir: "\x1B[row;colH" Buradaki \x1B aslında tek bir karakterdir. Buna ASCII tablosunda "escape karakteri" denilmeketedir. Komuttaki row ve col birer sayı olmalıdır. row imlecin taşıanacağı satır numarasını col ise imlecin taşınacağı sütun numarasını belirtmektedir. Örneğin: "\x1B[12;18H" Bu komut imleci 12'inci satır 18'üncü sütuna taşır. Biz imleci taşıyan bir C fonksiyonunu da aşağıdaki gibi yazabiliriz: void move_cursor(int row, int col) { printf("\x1B[%d;%dH", row, col); } İmleci yok etmek şu ANSI terminal komutu kullanılmaktadır: "\x1B[?25l" İmleci yeniden görünür yapmak için ise şu komut kullanılmaktadır: "\x1B[?25h" Tabii bu işlemleri birer fonksiyon haline de getirebiliriz: void hide_cursor(void) { printf("\x1B[?25l"); } void show_cursor(void) { printf("\x1B[?25h"); } İmlecin konumunu aygıt sürücünün saklaması için şu escape komutu kullanılmaktadır: "\x1B\x37" İmlecin son saklanan pozisyona geri yerleştirilmesi için ise şu komut komut kullanılmaktadır: "\x1B\x38" Bu işlemleri fonksiyon olarak da yazabiliriz: void save_cursor(void) { printf("\x1B\x37"); } void restore_cursor(void) { printf("\x1B\x38"); } ANSI escape komutları için Internet'teki çeşitli kaynaklara başvurabilirsiniz. Text modda yazının karakterlerinin zemini ve şekli ayrı ayrı renklendirilebilmektedir. Renklendirme işlemi de ANSI escape kodlarıyla yapılabilmektedir. Örneğin yazıların kırmızı yazılması için (yani yalnızca yazıların şekil renklerini kırmızı yapmak için) şu komut kullanılır: "\x1b[31m" Her renk için buradaki sayı değişmektedir. Şöyle genel bir fonksiyon da yazılabilir: #define BLACK_COLOR 30 #define RED_COLOR 31 #define WHITE_COLOR 37 #define BLUE_COLOR 34 .... void change_color(int color) { printf("\x1b[%dm", color); } Renkelndirme için aşağıdaki dokümana başvurabilirsiniz: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 Aşağıda escape karakterle bazı işlemeler yapan bir program örneği verilmiştir. * Örnek 1, #include #include #include #define BLACK_COLOR 30 #define RED_COLOR 31 #define WHITE_COLOR 37 #define BLUE_COLOR 34 void move_cursor(int row, int col) { printf("\x1B[%d;%dH", row, col); } void hide_cursor(void) { printf("\x1B[?25l"); } void show_cursor(void) { printf("\x1B[?25h"); } void change_color(int color) { printf("\x1b[%dm", color); } int main(void) { time_t t, prev_t; struct tm *pt; int count; change_color(BLUE_COLOR); hide_cursor(); prev_t = 0; count = 0; for (;;) { t = time(NULL); pt = localtime(&t); move_cursor(1, 80); printf("%02d:%02d:%02d", pt->tm_hour, pt->tm_min, pt->tm_sec); if (prev_t != t) { ++count; if (count == 10) break; } prev_t = t; } show_cursor(); change_color(WHITE_COLOR); return 0; } Diğer yandan Windows'un Console API fonksiyonlarını kullanabilmek sırasıyla şu işlemlerin yapılması gerekmektedir: -> İlk olarak terminale ilişkin stdout dosyasının handle değerinin elde edilmesi gerekmektedir. Bu işlem GetStdHandle isimli API fonksiyonuyla yapılır. Fonksiyonun prototipi şöyledir: HANDLE WINAPI GetStdHandle( DWORD nStdHandle ); Fonksiyon parametre olarak handle değeri alınmak istenen standart dosyayı belirten özel değeri alır. Bu değer şunlardan biri olabilir: STD_INPUT_HANDLE STD_OUTPUT_HANDLE STD_ERROR_HANDLE -> WrtiteConsole API fonksiyonu bir adresten itibaren belli miktardaki karakteri imlecin bulunduğu yerden itibaren yazmaktadır. Bu bakımdan puts gibi bir işleve sahiptir. (Tabii imleci aşağı satırın başına geçirmez.) Fonksiyonun prototipi şöyledir: BOOL WINAPI WriteConsole( HANDLE hConsoleOutput, const VOID *lpBuffer, DWORD nNumberOfCharsToWrite, LPDWORD lpNumberOfCharsWritten, LPVOID lpReserved ); Fonksiyonun birinci parametresi stdout dosyasının handle değerini almaktadır. Fonksiyon ikinci parametresiyle belirtilen adresten itibaren üçüncü parametresiyle belirtilen miktarda karakteri imlecin bulunduğu yere yazar. Başarılı bir biçimde yazabildiği karakter sayısını dördüncü parametresiyle girdiğimiz DWORD nesye yerleştirmektedir. Son parametre reserved durumdadır, NULL geçilmelidir. Fonksiyon başarı durumuna geri döner. Ancak genellikle başarınn kontrol edilmesine gerek yoktur. -> İmleci konumlandırmak için SetConsoleCursorPosition isimli API fonksiyonu kulanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOutput, COORD dwCursorPosition ); Fonksiyonun birinci parametresi stdout dosyasının handle değerini ikinci parametresi ise konumlandırma yapılacak koordinatı belirtmektedir. COORD yapısı şöyle bildirilmiştir: typedef struct _COORD { SHORT X; SHORT Y; } COORD, *PCOORD; Fonksiyon işlemin başarısı durumuyla geri dönmektedir. Örneğin: COORD coord; ... coord.X = 10; coord.Y = 20; SetConsoleCursorPosition(hConsole, coord); C99 ile birlikte dile eklenen "bileşik sabitler (compound literals)" ile aynı kod şöyle de yazılabilmektedir: SetConsoleCursorPosition(hConsole, (COORD){10, 12}); -> Yazının şekil ve zemin rengini değiştirmek için SetConsoleTextAttribute API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL WINAPI SetConsoleTextAttribute( HANDLE hConsoleOutput, WORD wAttributes ); Fonsiyonun ikinci parametresi özellik bilgisini belirtmektedir. Özellik bir renk ya da diğer bazı bileşenlerdne oluşmaktadır. Aşağıdaki sembolik sabitler bit düzeyinde OR işlemiyle özellik oluşturmakta kullanılabilir: FOREGROUND_BLUE FOREGROUND_GREEN FOREGROUND_RED FOREGROUND_INTENSITY BACKGROUND_BLUE BACKGROUND_GREEN BACKGROUND_RED BACKGROUND_INTENSITY COMMON_LVB_REVERSE_VIDEO COMMON_LVB_UNDERSCORE Örneğin: SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE); WriteConsole(hConsole, buf, strlen(buf), &dwWritten, NULL); SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED); -> İmleci yok etmek için SetConsoleCursorInfo isimli fonksiyon kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL WINAPI SetConsoleCursorInfo( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo ); Fonksiyonun ikinci parametresi CONSOLE_CURSOR_INFO isimli bir yapı nesnesinin adresini almaktadır. Bu yapı şöyle bildirilmiştir: typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible; } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO; Yapının dwSize elemanı 1 ile 100 arasında bir değer almaktadır. Bu değer imlecin büyüklüğünü belirtir. Yapının bVisible elemanı TRUE ya da FALSE girilebilir. Eğer bu elemana FALSE girilirse imleç görünmez olur. Aslında GetConsole.CursorInfo isimli bu bilgileri alan da bir fonksiyon vardır. Önce get fonksiyonu kullanılıp sonra set fonksiyonu kullanılırsa yapının diğer elemanı değiştirilmeden işlem yapılabilir. Örneğin: CONSOLE_CURSOR_INFO cci; ... GetConsoleCursorInfo(hConsole, &cci); cci.bVisible = FALSE; SetConsoleCursorInfo(hConsole, &cci); -> Konsole penceresinin karakter genişliğini ve yüksekliğini almak için GetConsoleScreenBufferInfo API fonksiyonu kullakılmaktadır. Fonksiyonun prototipi şöyledir: BOOL WINAPI GetConsoleScreenBufferInfo( HANDLEhConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo ); Fonksiyonun ikinci parametresi CONSOLE_SCREEN_BUFFER_INFO isimli bir yapı nesnesinin adresini almaktadır. Bu yapı şöyle bildirilmiştir: typedef struct _CONSOLE_SCREEN_BUFFER_INFO { COORD dwSize; COORD dwCursorPosition; WORD wAttributes; SMALL_RECT srWindow; COORD dwMaximumWindowSize; } CONSOLE_SCREEN_BUFFER_INFO; Bu fonksiyonla imlecin konumuda alınabilmektedir. Konsole ekranın genişlike ve yüksekliği şöyle edilir: CONSOLE_SCREEN_BUFFER_INFO csbi; ... GetConsoleScreenBufferInfo(hConsole, &csbi); width = csbi.srWindow.Right - csbi.srWindow.Left + 1; height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; Aşağıda konsol API fonksiyonlarının kullanımına genel bir verilmiştir. * Örnek 1, #include #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hConsole; char buf[1024] = "this is a test"; DWORD dwWritten; CONSOLE_CURSOR_INFO cci; CONSOLE_SCREEN_BUFFER_INFO csbi; int width, height; int len; if ((hConsole = GetStdHandle(STD_OUTPUT_HANDLE)) == INVALID_HANDLE_VALUE) ExitSys("GetStdHandle"); GetConsoleCursorInfo(hConsole, &cci); cci.bVisible = FALSE; SetConsoleCursorInfo(hConsole, &cci); SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE); SetConsoleCursorPosition(hConsole, (COORD){ 10, 12 }); WriteConsole(hConsole, buf, strlen(buf), &dwWritten, NULL); GetConsoleScreenBufferInfo(hConsole, &csbi); width = csbi.srWindow.Right - csbi.srWindow.Left + 1; height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; SetConsoleCursorPosition(hConsole, (COORD){ 10, 13 }); len = snprintf(buf, 1024, "Width: %d, Height: %d\n", width, height); WriteConsole(hConsole, buf, len, &dwWritten, NULL); SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED); getchar(); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >> Özel Kütüphane Fonksiyonlarıyla: Text ekranda ayrıntılı işlemler yapmak için özel kütüphaneler de kullanılmaktadır. Örneğin UNIX/Linux dünyasında "curses" ya da bunun daha yeni versiyonu olan "ncurses" kütüphaneleri bu amaçla çokça kullanılmaktadır. Windows dğnyasında zaten text ekranda işlemler yapmak için ismine "Console API Fonksiyonları" denilen bir grup API fonksiyonu bulundurulmuştur. Yukarıda da belirttiğimiz gibi eskiden grafik çalışma yoktu. Yalnızca text modda konsol tabanlı bir çalışma modeli vardı. Text modda karakterlerin kalıp olarak basıldığını pixel temelinde bir kontrolün sağlanamadığını belirtmiştik. Bu nedenle text modda yani konsol ekranında pixel işlemleri gereken "resim gösterme" gibi işlemler yapılamamaktadır. Ancak yine text modda özel karakterlerle çerçeve çizimleri yapılabilmektedir. Bunun için çeşitli karakter kümelerinde ve code page'lerde özel "çerçeve karakterleri (bozing character)" bulundurulmuştur. Bugün bilgisyaralarımızda pek çok karakter kümesi ve code page kullanılabilmektedir. Ancak UNICODE karakter kümesi ve bunun UTF-8 denilen encoding'i en yaygın kullanıma sahip olanlardan biridir. Çerçeve karakterleri biribirini kapatan özel karakterlerdir. Örneğin UNICODE tabloda tipik çerçeve karakterlerinin code point'leri şunlardır: #define BOX_LL "\u2514" #define BOX_V "\u2502" #define BOX_H "\u2500" #define BOX_UL "\u250C" #define BOX_UR "\u2510" #define BOX_LR "\u2518" Bu UNICODE karakterlerin aynılarının çift çizgili biçimleri de vardır. Ayrıca çerçeve ortası için çerçeveye bağlanma için ayrı karakterler de bulunmaktadır. Yukarıdaki sembolik sabitlerde \uxxxx biçimindeki karakterler UNICODE UTF-16 code point'lerini bellirtmektedir. Ancak bu code point'ler derleyicilerin "execution character set" denilen ayarlarına bakılarak derleyiciler tarafından dönüştürülmektedir. Bugün pek çok standart C derleyicisinin default "execution character set" ayarı UNICODE UTF-8 biçimindedir. Eğer terminal de bu encoding'e ayarlanmışsa sorun çıkmayacaktır. Burada kullandığımız kavramlar kursumuzun "karakter kodlamalarının (character encoding)" anlatıldığı bölümde ayrıntılı biçimde ele alınacaktır. >> Visual Studio'da derleyicinin "execution character set"i eğer UTF-8 değilse onu proje seçeneklerinden C/C++ komut satırı "Ek Seçenekler"de "/utf-8" girerek UTF-8 olarak dğeiştirebilirsiniz. Eğer console ekranınızın code page'i UTf-8 değilse aşağıdaki registry ayarından onu "65001" yaparak UTF-9'e geçirebilirsiniz: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage\OEMCP Ayrıca default olarak bir konsol penceresinin UTF-8 encoding'i ile açılmasını sağlamak için aşağıdaki anahtara "@chcp 65001>nul" girmelisiniz: HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\Autorun Aşağıda bir çerçeve çizimine örnek verilmiştir: * Örnek 1, #include #include #include #define BOX_LL "\u2514" #define BOX_V "\u2502" #define BOX_H "\u2500" #define BOX_UL "\u250C" #define BOX_UR "\u2510" #define BOX_LR "\u2518" void save_cursor(void) { printf("\x1B\x37"); } void restore_cursor(void) { printf("\x1B\x38"); } void move_cursor(int row, int col) { printf("\x1B[%d;%dH", row, col); } void hide_cursor(void) { printf("\x1B[?25l"); } void show_cursor(void) { printf("\x1B[?25h"); } int main(int argc, char *argv[]) { save_cursor(); hide_cursor(); move_cursor(10, 10); printf(BOX_LL); move_cursor(9, 10); printf(BOX_V); move_cursor(10, 11); printf(BOX_H); move_cursor(10, 12); printf(BOX_H); move_cursor(8, 10); printf(BOX_UL); move_cursor(8, 11); printf(BOX_H); move_cursor(8, 12); printf(BOX_H); move_cursor(8, 13); printf(BOX_UR); move_cursor(9, 13); printf(BOX_V); move_cursor(10, 13); printf(BOX_LR); restore_cursor(); show_cursor(); return 0; } /*================================================================================================================================*/ (16_05_08_2023) & (17_06_08_2023) & (18_12_08_2023) > İşletim Sistemlerinin Yardımcı Dosya Sistemleri: Bu yardımcı dosya sistemleri aşağıdaki fonksiyonlar çağrılarak kurulmuştur: İşlevi Windows API - POSIX Dosya Silmek DeleteFile - unlink Dosya Taşımak MoveFile - ??? Dosya Uzunluk Bilgisi GetFileSize - ??? Dizin Oluşturmak CreateDirectory - mkdir Dizin Silmek RemoveDirectory - rmdir Dosyanın Erişim Haklarının Değişim ??? - chmod Dosyaya İlişkin Bilgiler ??? - stat, lstat ve fstat Bu fonksiyonlardan, >> Windows API Fonksiyonları: Windows API fonksiyonlarının büyük bölümü çok eskiden tasarlanmıştır. Zamanla yeni birtakım özelliklerin de bazen etkisiyle bu fonksiyonlarda genişletme (yani işlevlerini gemişletme) yapma gereksinimi ortaya çıkmıştır. Bu durumda Microsoft eski fonksiyony bulunurmaya devam ederek onun daha gelişmiş yeni bir versiyonunu da oluşturmuştur. Genel olarak Micrsoft bir fonksiyonun genişletilmiş yeni versyionunu Ex sonekiyle isimlendirmektedir. Örneğin LockFile fonksiyonunun yeni genişletilmiş ismi LockFileEx biçimindedir. APı fonksiyonlarının Ex'li versiyonları nıormal versiyonlarını da kapsar niteliktedir. Ancak genel olarak Ex'li versiyonların daha fazla parametreye sahip olma eğilimi vardır. Eğer sizin bu Ex'li versiyonşları kullanmak için nedeniniz yoksa bunların Ex'siz normal versiyonlarını kullanabilirsiniz. API fonksiyonlarının Ex'li versyonları oluşturulduğunda genel olarak Ex'siz versiyonları "deprecated" yapılmamaktadır. Bir fonksiyonun "deprecated" yapılması "şimdilik muhafa edildiği ancak gelecekte kaldırılabileceği dolayıyla da artık bu fonksiyonu kullanmak isteyenlerin bu fonksiyonu kullanmamaları gerektiği" anlamına gelmektedir. Tabii bir fonksiyon "deprecated" yapılmışsa onun yerini tutan başka bir fonksiyon da bulunuyor durumdadır. Dokğmanlar "deprecated" olan fonksiyon yerine hangi fonksiyonun tercih edilmesi gerektiğini de belirtmektedir. "Deprecated" sözcüğü yalnızca Windows API fonksiyonlarında değil aynı zamanda C standartlarında da kullanılan bir sözcüktür. Şimdi de tablodaki fonksiyonları incelemeye başlayalım: >>> "DeleteFile" : Windows sistemlerinde dosya silmek için DeleteFile isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL DeleteFile( LPCTSTR lpFileName ); Fonksiyon silinecek dosyanın yol ifadesini parametre olarak alır. Başarı durumunda sıfır dışı bir değere başarıszlık durumunda 0 değerine geri döner. Bir dosya çeşitli nedenlereden dolayı silinemeyebilir. Bu nedenle fonksiyonun başarsı kontrol edilmeli ve başarısızlık durumunda uygun hata mesajı verilmelidir. Örneğin: if (!DeleteFile("test.txt")) ExitSys("DeleteFile"); Windows'ta bir dosya açıkken dosyayı silebilir miyiz? Windows'ta bu durum dosyanın nasıl açıldığına göre değişebilmektedir. Eğer bir dosya CreateFile fonksiyonu ile açılırken fonksiyonun üçüncü parametresinde FILE_SHARE_DELETE bayrağı eklenmezse bu durumda dosya açıkken başka bir proses dosyayı silemez. Dolayısıyla DeleteFile başarısız olur. Ancak CreateFile fonksiyonunun üçüncü parametresine FILE_SHARE_DELETE baytrağı eklenirse bu durumda başka bir proses artık dosyayı silebilir. Tabii bu durumda dosya dizin girişinden silinir. Ancak dosya açmış olan prosesler dosyayı kullanmaya devam ederler. Dosya kullanan son proses de dosyayı kapattığı zaman dosya gerçek anlamda silinecektir. * Örnek 1, #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { if (!DeleteFile("test.txt")) ExitSys("DeleteFile"); printf("File successfully deleted...\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> "MoveFile" : Bir dosyanın Windows'ta "taşınması (move edilmesi)" nasıl yapılmaktadır? Örneğin Windows'ta elimizde "c:\Study" dizini içerisinde "test.txt" isimli bir dosya bulunuyor olsun. Biz de bu dosyayı "c:\temp" dizinine taşımak isteyelim. Normal olarak biz dosyaalara "dizin girişleri (directory enty)" yoluyla erişiriz. Dosyaların gerçek verileri (yani dosyaların içindeki bilgiler) diskte başka bir yerdedir. Dizin girişleri aslında dosyanın içindeki bilgilerin bulunduğu disk alanına referans eden bilgileri içermektedir. Dolayısıyla bir dosyanın taşınması sırasında aslında dosyanın içerisindeki bilgilerde genellikle bir taşıma yapılmamaktadır. Yalnızca eski dizin girişi silinip yeni dizin girişi aynı dosyanın diskteki bilgilerine referans edecek biçimde oluşturulmaktadır. Yani biz "C:\Study" dizininin içerisindeki "test.txt" dosyasını "C:\temp" dizinine taşırken aslında dosyanın içerisindeki bilgileri diskte bir yerden bir yere taşımamaktayız. Bu durumda işletim sistemi aslında "C:\Study" dizinindeki "test.txt" dizin girişini siler, "C:\Sample" dizininde aynı dosyanın bilgilerine referans eden yeni bir "test.txt" dizin girişi oluşturur. Bu işlem dosyanın içindeki bilgileri taşımaktan çok daha hızlı yapılabilen bir işlemdir. Bu durumda işletim sistemlerinde "isim değiştirme (rename)" "taşıma (move)" aslında aynı anlama gelmektedir. Yani biz bir dosyanın ismini değiştirdiğimizde aslında sanki onu aynı dizin içerisine başka bir isimle taşıyor gibi olmaktayız. Tabii bazen işletim sistemi gerçekten dosyanın içerisindeki bilgileri de taşımak zorunda kalabilmektedir. Örneğin Windows'ta iki hard diskimiz olsun. Biz diskteki dosyayı diğer diske taşırken mecburen işletim sistemi dosya içeriğini de fiziksel olarak taşıyacaktır. Windows sistemlerinde dosya taşımak için (ve tabii dosyanın ismini değiştirmek için) MoveFile isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL MoveFile( LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName ); Fonksiyonun birinciparametresi kaynak yol ifadesini ikinci parametresi hedef yol ifadesini almaktadır. Fonksiyonun geri dönüş değeri işlemin başarısını belirtmektedir. Dosyanın taşınması aslında dosyanın aynı zamanda silinmesi gibi bir etki de yaratmaktadır. Dolayısıyla bir proses dosyayı FILE_SHARE_DELETE bayrağını kullanmadan açmışsa başka bir proses dosyayı MoveFile ile taşıyamaz. Aşağıdaki örnekte prosesin çalışma dizinindeki "test.txt" dosyasının ismi "mest.txt" olarak değiştirilmektedir. * Örnek 1, #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { if (!MoveFile("test.txt", "mest.txt")) ExitSys("MoveFile"); printf("File successfully moved...\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> "GetFileSize" : Windows sistemlerinde GetFileSize API fonksiyonu açılmış bir dosyanın uzunluğunu bize verir. Bu fonksiyonun GetFileSizeEx isminde genişletilmiş bir biçimi de vardır. GetFileSiz fonksiyonun prototipi şöyledir: DWORD GetFileSize( HANDLE hFile, LPDWORD lpFileSizeHigh ); Fonksiyonun birinci parametresi açılmış dosyanın handle değerini almaktadır. Fonksiyonun geri dönüş değeri dosya uzunluğunun düşük anlamlı DWORD kısmıdır. Fonksiyonun ikinci parametresi dosya uzunluğunun yüksek anlamlı DWORD kısmının yerleştirileceği DWORD nesnenin adresini almaktadır. İkinci pparametre NULL geçilebilir. Bu durumda dosyanın ununluğunun yüksek anlamlı DWORD değeri elde edilemez. Fonksiyonun ikinci parametresi NULL girilmediğinde fonksiyon başarısız olursa fonksiyon INVALID_FILE_SIZE özel değerine geri dönmektedir. Tabii bu özel değerin aslında dosyanın gerçek uzunluk değeri olma olasılığı da vardır. Bu nedenle MSDN dokümanları böylesi bir durumda ayrıca GetLastError fonksiyonun çağrılması gerektiğini belirtmektedir. Fonksiyon başarısız ise GetLastError kesinlikle NO_ERROR dışında bir değer vermektedir. Ancak fonksiyon da şöyle bir kusur vardır: Eğer fonksiyonun ikinci aparametresi NULL geçilirse bu durumda fonksiyonun geri dönüş değerinde başarı ya da başarısızlık anlaşılamamaktadır. dwSizeLow = GetFileSize(hFile, &dwLow); if (dwSizeLow == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) ExitSys("GetFileSize"); Açık bir dosyanın uzunluğunu elde etmenin diğer bir yolu da dosya göstericisini EOF durumuna yerleştirip dosya göstericisinin konumunu almak olabilir. Tabii bir dosyanın uzunluğunu almak için dosyanın açılması oldukça zahmetlidir. Aslında dosyayı hiç açmadan dosya uzunluğunun elde edilmesi de mümkündür. Bu işlemin yapılabileceği izleyen paragraflarda ele alınmaktadır. * Örnek 1, Aşağıdaki örnekte dosya uzunluğu GetFileSize fonksiyonu ile elde edilmiştir. Bu örnekte fonksiyonun ikinci aparamtresi NULL girilmiştir. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFile; DWORD dwSizeLow, dwSizeHigh; if ((hFile = CreateFile("Test.c", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) ExitSys("CreateFile"); if ((dwSize = GetFileSize(hFile, &dwSizeHigh)) == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) ExitSys("GetFileSize"); printf("%lu\n", (unsigned long)dwSize); /* prints only low part */ CloseHandle(hFile); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } GetFileSize fonksiyonundaki tasarım hatası GetFileSizeEx fonksiyonuyal gidirilmiştir. >>>> "GetFileSizeEx" : GetFileSizeEx fonksiyonunun prototipi şöyledir: BOOL GetFileSizeEx( HANDLE hFile, PLARGE_INTEGER lpFileSize ); Fonksiyon dosya uzunluğunu LARGE_INTEGER türündne bir yapı nesnesinin içerisine yerleştirmektedir. Fonksiyonun geri dönüş değeri işlemin başarısını belirtmektedir. LARGE_INTEGER yapısı şöyle bildirilmiştir: typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; } DUMMYSTRUCTNAME; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER; Aşağıda GetFileSizeEx API fonksiyonunun kullanımına bir örnek verilmiştir. * Örnek 1, #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFile; LARGE_INTEGER li; if ((hFile = CreateFile("Test.c", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) ExitSys("CreateFile"); if (!GetFileSizeEx(hFile, &li)) ExitSys("GetFileSizeEx"); printf("%lld\n", li.QuadPart); CloseHandle(hFile); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> "CreateDirectory" : Bir dizin (directory) de aslında bir çeşit dosyadır. Normal bir dosyanın içerisinde dosyanın bilgileri bulunur. Ancak bir dizin dosyasının içerisinde o dizindeki dosyaların neler olduğuna yönelik bilgiler bulunmaktadır. İşletim sistemleri genel olarak normal dosyalarla dizinleri aynı biçimde organize derler. Windows'ta bir dizin yaratmak için CreateDirectory isimli API fonksiyonu kullanılır. Fonksiyonun prototipi şöyledir: BOOL CreateDirectory( LPCSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes ); Fonksiyonun birinci parametresi yaratılacak dizin'in yol ifadesini belirtmektedir. İkinci parametre yaratılcacak dizin'in güvenlik özelliklerini almaktadır. Bu parametre NULL eçilebilir. Fonksiyonun geri dönüş değeri işlemin başarsını belirtmektedir. Bir dizin yaratıldığında içerisine otomatik olarak iki dizin girişi yerleştirilmektedir. Bu dizin girişlerinin isimleri "." ve ".." biçimindedir. "." ve ".." girişleri de birer dizin belirtir. "." girişi içinde bulunulan dizini, ".." girişi ise içinde bulunulan dizinin üst dizinini belirtir. Bu dizin girişleri silinememektedir. Kök dizinin dışında tüm alt dizinlerde her zaman "." ve ".." dizin girişleri bulunmaktadır. * Örnek 1, Aşağıdaki Windows'ta CreateDirectory fonksiyonunun kullanımına bir örnek verilmiştir. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { if (!CreateDirectory("TestDir", NULL)) ExitSys("CreateDirectory"); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> "RemoveDirectory" : Windows'ta bir dizin silmek için RemoveDirectory isimli API fonksiyonu kullanılır. Fonksiyonun prototipi şöyledir: BOOL RemoveDirectory( LPCSTR lpPathName ); Fonksiyon parametre olarak silinecek dizin'in yol ifadesini almaktadır. Geri dönüş değeri işlemin başarısını belirtir. RemoevDirectory fonksiyonu ile biz ancak boş dizinleri silebiliriz. Bir dizin'in boş olması demek onun içerisinde "." ve ".." dışında hiçbir dizin girişinin olmaması demektir. (Zaten bu "." ve ".." girişlerinin silinemediğine dikkat ediniz.) Fonksiyonun yalnızca boş dizinleri silmesi güvenli bir kullanım için öngörülmüştür. Aksi takdirde yanlıklıkla büyük bir dizin ağacı silinebilirdi. Geri Dönüşüm Kutusu (Recycle Bin) işletim sisteminin çekirdeği ile ilgili bir oragizasyon değildir. Kabuk kısmı ile ilgili bir organizasyondur. Bu nedenle DeleteFile ya da RemoveFile API fonksiyonları silinen öğeleri geri dönüş kutusuna atamaz. Biz "Dosya Gezgini (File Explorer)" ile bir dosyayı ya da dizini sildiğimizde dosya gezgini default durumda silinen bu öğeleri geri dönüş kutusuna atmaktadır. Bu durum anlaşılabilir. Çünkü geri dönüşüm kutusu zaten kabuk tarafından organize edilmektedir. Dosya gezgininde bir dizinin üzerine gelip DEL tuşu ile dizini silmenin de bu bakımdan bir geri dönüş vardır. Halbuki RemoveDirectory ile bu biçimde geri dönüş mümkün değildir. * Örnek 1, Aşağıda bir dizinin silinmesi örneği verilmiştir. Bju örneği test ederken dizin'in içerisine dosya yerleştirerek de programı çalıştırınız. Bu durumda RemoveDirectory fonksiyonunun başarısız olduğunu göreceksiniz. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { if (!RemoveDirectory("TestDir", NULL)) ExitSys("CreateDirectory"); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >> UNIX/Linux POSIX Fonksiyonları: >>> "unlink" : UNIX/Linux sistemlerinde dosya unlink isimli POSIX fonksiyonu ile silinmektedir. Bu sistemlerde aslında remove standart C fonksiyonu doğrudan bu unlink fonksiyonunu çağırmaktadır. Başka bir deyişle bu sistemlerde remove fonksiyonu ile unlink fonksiyonu arasında bir fark yoktur. unlink fonksiyonun prototipi şöyledir: #include int unlink(const char *pathname); Fonksiyon parametre olarak silinecek dosyanın yol ifadesini alır. Başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri döner. errno değişkeni uygun biçimde set edilir. Yukarıda da aslında dizinlerin normal dosyalar gibi olduğunu belirtmiştik. Dizinler aslında "dizinler içerisindeki girişlerin tutulduğu" normal dosyalar gibidir. Bir dosyanın silinmesi aslında o dosyanın içinde bulunduğu dizinde bir yazma işlemini gerektirmektedir. Bu nedenle UNIX/Linux sistemlerinde bir dosyayı silebilmek için prosesin dosyaya "w" hakkının olması gerekmez. Önemli olan prosesin dosyanın içinde bunduğu dizine "w" hakkının olmasıdır. O halde bu sistemlerde bir dosyanın silinebilmesi için remove ya da unlink fonksiyonunu çağıran prosesin dosyanın içinde bulunduğu dizine yazma hakkının olmasıdır. * Örnek 1, Aşağıda komut satırı argümanı olarak alınan dosyaların unlink fonksiyonuyla silinmesine yönelik bir örnek verilmiştir. #include #include #include #include #include int main(int argc, char *argv[]) { if (argc == 1) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) if (unlink(argv[i]) == -1) fprintf(stderr, "cannot unlink %s: %s\n", argv[i], strerror(errno)); return 0; } >>> "chmod" : Anımsanacağı gibi UNIX/Linux sistemlerinde dosyanın erişim hakları dosya open fonksiyonuyla yaratılırken fonksiyonun üçüncü parametresiyle belirleniyordu. İşte dosyanın erişim hakları daha sonra da chmod isimli POSIX fonkisyonuyla değiştirilebilmektedir. Fonksiyonun prototipi şöyledir: #include int chmod(const char *pathname, mode_t mode); Fonksiyonun birinci parametresi erişim hakları değiştirilecek dosyanın yol ifadesini ikinci parametresi yeni erşim haklarını belirtmektedir. Erişim hakları değiştirlirken prosesin umask değeri işlemde etkili olmamaktadır. Açık dosyaların erişim hakları da fchmod fonksiyonuyla değiştirilmektedir. >>>> "fchmod" : Fonksiyonun prototipi şöyledir: #include int fchmod(int fd, mode_t mode); Fonksiyonun birinci parametresi dosya betimleyicisini ikinci parametresi erişim haklarını belirtmektedir. Fonksiyonlar başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno değişkeni uygun biçimde set edilmektedir. * Örnek 1, Aşağıda fchmod fonksiyonun kullanılmasına bir örnek verilmiştir. #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); if (fchmod(fd, S_IRUSR | S_IWUSR) == -1) exit_sys("fchmod"); close(fd); printf("success...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Eğer dosya zaten açıksa dosyanın diskte isimsel olarak aranmasına gerek kalmayacağı için fchmod fonksiyonu chmod fonksiyonuna göre daha hızlı işlem yapma potansiyelindedir. chmod fonksiyonuyla bir dosyanın erişim haklarının değiştirilebilmesi için fonksiyonu çağıran prosesin etkin kullanıcı id'si dosyanın kullanıcı id'si ile aynı olması ya da prosesin "uygun önceliğe (appropriate privilige)" sahip olması gerekmektedir. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { if (chmod("test.txt", S_IRUSR | S_IWUSR) == -1) exit_sys("chmod"); printf("success...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte komut satırından alınan dosyaların erişim hakları komut satırından verilen erişim hakkı değiştirilmektedir. Programın kullanımı aşağıdaki gibidir: "./mychmod 666 x.dat y.dat ..." #include #include #include #include #include #include bool check_octal(const char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int mode; if (argc < 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (!check_octal(argv[1])) { fprintf(stderr, "file mode incorrect!..\n"); exit(EXIT_FAILURE); } sscanf(argv[1], "%o", &mode); for (int i = 2; i < argc; ++i) if (chmod(argv[i], mode) == -1) fprintf(stderr, "chmode failed for %s: %s\n", argv[i], strerror(errno)); return 0; } bool check_octal(const char *str) { while (*str != '\0') { if (*str < '0' || *str > '8') return false; ++str; } return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "mkdir" : UNIX/Linux sistemlerinde bir "dizin (directory)" yaratmak için mkdir isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int mkdir(const char *pathname, mode_t mode); Fonksiyonun birinci parametresi dizin'in yol ifadesini ikinci parametresi ise dizin'in erişim haklarını almaktadır. Erişim haklarında prosesin umask değeri etkili olmaktadır. Dizinlerdeki erişim haklarında "x" hakkı özel ve başka bir anlama gelmektedir. Normal olarak dizinlerde "x" var olduğuna dikkat ediniz. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri dönmektedir. başarısızlık durumunda errno değişkeni uygun biçimde set edilmektedir. Bir dizin yaratabilmek için ve bir dosya yaratabilmek için o diznin'in ya da dosyanın yaratılacağı dizine "w" hakkının olması gerekir. Çünkü yukarıda da belirttiğimiz gibi dizinler aslında "içerisinde dosya bilgilerinin bulunduğu dosyalar" gibidir. Dolayısıyla bir dizinde dosya ya da dizin yaratmak aslında o dizin dosyası üzerinde bir değişiklik yapma anlamına gelmektedir. Bu nedenle de prosesin ilgili dizine "w" hakkının bulunması gerekmektedir. Tabii "uygun önceliğe (appropriate privilege)" sahip prosesler her yerde dizin yaratabilirler. Yine UNIX/Linux sistemlerinde de bir dizin yaratıldığında dizin içerisinde "." ve ".." isimli iki dizin girişi oluşturulmaktadır. * Örnek 1, Aşağıda mkdir programının bir benzeri mymkdir ismiyle yazılmıştır. #include #include #include #include #include #include #include #include #include bool check_octal(const char *str); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int m_flag; int err_flag; char *m_arg; int result; int mode; 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_arg = optarg; m_flag = 1; 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: %s\n", argv[optind - 1]); err_flag = 1; break; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind == 0) { fprintf(stderr, "requires at least one path name!..\n"); exit(EXIT_FAILURE); } if (m_flag) { if (!check_octal(m_arg)) { fprintf(stderr, "invalid file mode: %s\n", m_arg); exit(EXIT_FAILURE); } sscanf(m_arg, "%o", &mode); umask(0); } else mode = 0777; /* POSIX 2008 ve sonrasında doğrudna sayı girilebiliyor */ for (int i = optind; i < argc; ++i) if (mkdir(argv[i], mode) == -1) perror(argv[i]); return 0; } bool check_octal(const char *str) { while (*str != '\0') { if (*str < '0' || *str > '8') return false; ++str; } return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "rmdir" : UNIX/Linux sistemlerinde dizin silmek için rmdir isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int rmdir(const char *pathname); Fonksiyon dizin'in yol ifadesini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri döner. Yine bir dizin'in silinmnesi için prosesin dizin'in içinde bulunduğu dizine "w" hakkına sahip olması ya da "uygun önceliğe (appropriate privilege)" sahip olması gerekmektedir. * Örnek 1, Aşağıda dizin silmeye yönelik bir örnek verilmiştir. #include #include #include int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) if (rmdir(argv[i]) == -1) perror(argv[i]); return 0; } >>> "stat", "lstat" ve "fstat" : Bir dosyanın çeşitli bilgileri stat, lstat ve fstat fonksiyonlarıyla elde edilmeketdir. Aslında "ls -l" komutu bu fonksiyonlar kullanılarak yazılmıştır. Biz bu fonksiyonlarla bir dosyanın eriim haklarını, kullanıcı ve grup id'lerini, uzunluğunu vs. elde edebiliriz. Bu üç fonksiyon aslında aynı amaca hizmet etmektedir. Aralarında küçük farklılılar vardır. Fonksiyonların prototipleri şöyledir: #include int stat(const char *path, struct stat *buf); int fstat(int fd, struct stat *buf); int lstat(const char *path, struct stat *buf); stat fonksiyonu yol ifadesiyle belirtilen dosyanın bilgilerini elde eder. fstat fonksiyonu ise dosya zaten açılmışsa dosya betimleyicisinden hareketle dosya bilgilerini elde etmektedir. lstat fonksiyonu da yol ifadesinden hareketle dosya bilgilerini elde eder. Ancak lstat sembolik bağlantı dosyalarını izlememektedir. Sembolik bağlantı dosyaları hakkında ileride bilgiler verilecektir. Fonksiyonlar dosya bilgilerini ikinci parametreleriyle belirtilen struct stat isimli bir yapı nesnesinin içerisine yerleştirmektedir. Fonksiyonlar başarı durumunda 0 değrine başarısızlık durumunda -1 değerine geri dönmektedir. errno değişkeni başarısızlık durumunda uygun biçimde set edilmektedir. struct stat yapısı dosyası içerisinde şöyle bildirilmiştir: 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 */ 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_mtine st_mtim.tv_sec #define st_ctime st_ctim.tv_sec }; Yapının elemanlarında kullanılan xxx_t biçimindeki tür isimlerinin hepsi ve dosyaları içerisinde typedef edilmiştir. Yapının, -> st_dev elemanı dosya içinde bulunduğu aygıtın aygıt numarasını belirtmektedir. -> dev_t türü bir tamsayı türü biçiminde typedef edilmek zorundadır. -> st_ino elemanı dosyanın i-node numarasını belirtmektedir. inode numarası konusu ileride ele alınacaktır. ino_t türü işaretsiz bir tamsayı türü biçiminde typedef edilmek zorundadır. -> st_mode elemanı dosyanın türünü ver erişim haklarını içermektedir. mode_t türü bir tamsayı türü biçiminde typedef edilmek zorundadır. -> st_nlink elemanı dosyanın "hard link sayısını" belirtmektedir. Hard link kavramı ileride ele alınacaktır. nlink_t bir tamsayı türü olacak biçimde typedef edilmek zorundadır. -> st_uid ve st_gid elemanları sırasıyla dosyanın kullanıcı ve grup id değerlerini belirtmektedir. uid_t ve gid_t tamsı birer tamsayı türü olarak typedef edilmek zorundadır. -> st_rdev elemanı eğer dosya bir aygıt sürücü dosyası (device file) ise o aygıt sürücü dosyasının aygıt numarasını belirtmektedir. -> st_size elemanı dosyanın uzunluğunu belirtmektedir. off_t işaretli bir tamsayı türü biçiminde typedef edilmek zorundadır. -> st_blksize elemanı dosyanın içinde bulunduğu aygıttaki dosya parçalarının tutulduğu bloğun uzunluğunu belirtmektedir. Bu uzunluk sektör katlarında (512'nin katlarında) olacaktır. Bugün tipik olarak ext dosya sistemleri formatlanırken bir blok 8 sektör alınmaktadır. Ancak bu durum çeşitli faktörlere bağlı olarak değişebilmektedir. Dosya bloklarının ne anlam ifade etiiği kursumuzun ilerleyen bölümlerinde ele alınacaktır. blksize_t işaretli bir tamsayı türü biçiminde typedef edilmek zorundadır. -> st_blocks elemanı dosyanın diskte kaç sektörde (yani kaç tane 512 byte içerisinde) bulunduğunu belirtmektedir. blkcnt_t işaretli bir tamsayı türü biçiminde typedef edilmek zorundadır. -> st_atim, st_mtim ve st_ctim elemanları sırasıyla dosyadan son okuma yazpıldığı zamanı, dosyaya son yazma yapıldığı zamanı ve dosyanın i-node bilgilerinin (yani stat fonksiyonu şle elde ettiğimiz meta-data bilgilerinin) değiştirildiği zamanı belirtmektedir. Eskiden bu elemanlar time_t türündendi ve 01/01/1970 tarihinden geçen saniyesi sayısını belirtmekteydi. Sonra POSIX Standartlarının 2008 versiyonunda bu elemanlar detaylandırılmış ve timespec isimli bir yapı türünden yapılmışitır. timepec yapısı hem 01/01/1970'ten geçen saniye sayısını ve aynı zamanda o saniyeden sonraki nano saniye sayısını tutmaktadır. Yani timespec yapısı tarih ve zamanı nano saniye duyarlılığında tutmaktadır. timespec yapısı şöyle bildirilmiştir: struct timespec { time_t tv_sec; long tv_nsec; }; Eskiden stat yapısının bu tarih zaman bilgisini içeren elemanlarının isimleri ve türleri şöyleydir: time_t st_atime; time_t st_mtime; time_t st_ctime; POSIX 2008 ile bu konuda değişiklik yapılınca eski programların derlenebilmesi için şu makrolar bulundurulmaktadır: #define st_atime st_atim.tv_sec #define st_mtine st_mtim.tv_sec #define st_ctime st_ctim.tv_sec Böylece programcılar dilerlerse eski isimleri de kullanabilmektedir. Dosya sistemi ile ilgili hangi POSIX fonksiyonlarının bu üç tarih zaman bilgisinin hangilerini değiştirdiği POSIX standartlarında açıkça belirtilmiştir. Örneğin write POSIX fonksiyonu dosyanın st_mtim ve st_ctim elemanlarını değiştirmektedir. Örneğin rmdir fonksyonu üst dizinin st_mtim ve st_ctim elemanlarını değiştirmektedir. stat yapısının st_mode elemanının dosyanın türü ve erişim bilgilerini verdiğini belirtmiştik. Dosyanın türü ve erişim hakları bu st_mode elemanının çeşitli bitlerine kodlanmıştır. Programcının bu bilgilerin hangi bitlere kodlandığını bilmesine gerek yoktur. POSIX standartları bu tür bilgilerinin st_mode elemanının hangi bitlerine kodlandığı konusunda da bir açıklama bulunmamaktadır. Ancak dosyası içerisinde S_ISXXX biçiminde çeşitli tür makroları bulundurulmuştur. Programcı yapının st_mode elemanını bu makrolara argüman olarak verdiğinde bu makrolar elemanın ilgili bitlerine bakarak dosyanın ilgili türden olup olmadığını belirlerler. Eğer dosya ilgili türdense bu makrolar sıfır dışı bir değer ilgili türden değilse 0 değerine geri dönmektedir. Bu makrolar şunlardır: S_ISBLK(m) Dosya bir blok aygıt sürücü dosyası mı? (ls -l komutunda "b" harfi ile temsil ediliyor) S_ISCHR(m) Dosya bir karakter aygıt sürücü dosyası mı? (ls -l komutunda "c" harfi ile temsil ediliyor) S_ISDIR(m) Dosya bir dizin dosyası mı? (ls -l komutunda "d" harfi ile temsil ediliyor) S_ISFIFO(m) Dosya bir boru (pipe) dosyası mı? (ls -l komutunda "p" harfi ile temsil ediliyor) S_ISREG(m) Dosya sıradan bir disk dosyası mı? (regular file) (ls -l komutunda "-" harfi ile temsil ediliyor) S_ISLNK(m) Dosya bir sembolik bağlantı dosyası mı? (ls -l komutunda "l" harfi ile temsil ediliyor) S_ISSOCK(m) Dosya bir UNIX Domain soket dosyası mı? (ls -l komutunda "s" harfi ile temsil ediliyor) O halde örneğin dosyanın türünü şöyle belirleyebiliriz: struct stat finfo; ... if (stat(path, &finfo) == -1) exit_sys("stat"); ... if (S_ISBLK(finfo.st_mode)) putchar('b'); else if (S_ISCHR(finfo.st_mode)) putchar('c'); else if (S_ISDIR(finfo.st_mode)) putchar('d'); else if (S_ISFIFO(finfo.st_mode)) putchar('p'); else if (S_ISREG(finfo.st_mode)) putchar('-'); else if (S_ISLNK(finfo.st_mode)) putchar('l'); else if (S_ISSOCK(finfo.st_mode)) putchar('s'); else putchar('?'); Dosyanın türünü stat yapısının st_mode elemanından elde etmenin diğer bir yolu da bu st_mode elemanını önce S_IFMT değeriyle bit AND çekmek ve bunun sonucunu switch deyimine sokmak olabilir. S_IFMT st_mode elemanı ile bit AND işlemi dosya türüne ilişkin olan bitlerininin elde edilmesine yol açmaktadır. Bit AND işlemi sonucunda elde edilen değerler şunlardan birine eşit olmak zorundadır: S_IFBLK S_IFCHR S_IFIFO S_IFREG S_IFDIR S_IFLNK S_IFSOCK Bu durumda dosya türü şöyle de tespit edebilir: struct stat finfo; ... if (stat(path, &finfo) == -1) exit_sys("stat"); ... switch (finfo.st_mode & S_IFMT) { case S_IFBLK: putchar('b'); break; case S_IFCHR: putchar('c'); break; case S_IFIFO: putchar('p'); break; case S_IFREG: putchar('-'); break; case S_IFDIR: putchar('d'); break; case S_IFLNK: putchar('l'); break; case S_IFSOCK: putchar('s'); break; default: putchar('?'); break; } Dosyanın erişimn haklarını elde etmenin bir yolu doğrudan yapının st_mode elemanını open fonksiyonunda görmüş olduğumuz S_IXXX değerleriyle bit AND işlemine sokmaktır. Eğer bu işlemin sonucu sıfır dışı bir değerse bu durumda ilgili erişim hakkı dosyada vardır. Eğer 0 ise ilgili erişim hakkı dosyada yoktur. Bu durumda dosyanın erişim haklarını ls -l formatında şöyle yazdırabiliriz: mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; ... for (int i = 0; i < 9; ++i) putchar(finfo.st_mode & modes[i] ? "rwx"[i % 3] : '-'); Daha önceden de belirttiğimiz gibi POSIX 2008 ile birlikte artık S_IXXX sembolik sabitlerinin değerleri açıkça standartlarda belirtilmiştir. Bu değerler incelendiğinde aslında bir sayının düşük anlamlı 9 bitine karşılık geldiği örülmektedir. Başka bir deyişle bu sembolik sabitler aslında bütün bitleri 0 yalnızca düşük anlamlı 9 bitinden bir 1 olan sembolik sabitlerdir. Böylece biz aynı işlemleri aslında POSIX 2008 ve sonrasında değerleri bir diziye yerleştirmeden aşağıdaki gibi de yapabilmekteyiz: for (int i = 8; i >= 0; --i) putchar(finfo.st_mode >> i & 1 ? "rwx"[(8 - i) % 3] : '-'); Şimdi de bu fonksiyonların kullanımına ilişkin örnekleri inceleyelim: * Örnek 1, Aşağıda stat fonksiyonun kullanılmasına bir örnek verilmiştir. #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct stat finfo; mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; char buf[32]; struct tm *ptm; if (setlocale(LC_ALL, "tr_TR.utf-8") == NULL) { fprintf(stderr, "cannot set locale!..\n"); exit(EXIT_FAILURE); } if (argc == 1) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) { if (stat(argv[i], &finfo) == -1) { perror(argv[i]); continue; } printf("File Name: %s\n", argv[i]); printf("File Mode: "); /* if (S_ISBLK(finfo.st_mode)) putchar('b'); else if (S_ISCHR(finfo.st_mode)) putchar('c'); else if (S_ISDIR(finfo.st_mode)) putchar('d'); else if (S_ISFIFO(finfo.st_mode)) putchar('p'); else if (S_ISREG(finfo.st_mode)) putchar('-'); else if (S_ISLNK(finfo.st_mode)) putchar('l'); else if (S_ISSOCK(finfo.st_mode)) putchar('s'); else putchar('?'); */ switch (finfo.st_mode & S_IFMT) { case S_IFBLK: putchar('b'); break; case S_IFCHR: putchar('c'); break; case S_IFIFO: putchar('p'); break; case S_IFREG: putchar('-'); break; case S_IFDIR: putchar('d'); break; case S_IFLNK: putchar('l'); break; case S_IFSOCK: putchar('s'); break; default: putchar('?'); break; } for (int i = 0; i < 9; ++i) putchar(finfo.st_mode & modes[i] ? "rwx"[i % 3] : '-'); /* for (int i = 8; i >= 0; --i) putchar(finfo.st_mode >> i & 1 ? "rwx"[(8 - i) % 3] : '-'); */ putchar('\n'); printf("Hard Link Count: %ju\n", (uintmax_t)finfo.st_nlink); printf("User Id: %ju\n", (uintmax_t)finfo.st_uid); printf("Group Id: %ju\n", (uintmax_t)finfo.st_gid); printf("Size: %jd\n", (intmax_t)finfo.st_size); ptm = localtime(&finfo.st_mtim.tv_sec); strftime(buf, 32, "%b %2e %Y %H:%M\n", ptm); printf("Last Modification Time: %s\n", buf); printf("--------------------------\n"); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aağıdaki örnekte dosya bilgileri "ls -l" formatına benzer biçimde stdout dosyasına yazdırılmıştır. Ancak burada dosyanın kullanıcı ve grup id'leri isimsel olarak değil sayısal olarak yazdırılmaktadır. İzleyen paragraflarda bu işlemin nasıl yapıldığını göreceğiz. #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct stat finfo; mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; char buf[32]; struct tm *ptm; if (setlocale(LC_ALL, "tr_TR.utf-8") == NULL) { fprintf(stderr, "cannot set locale!..\n"); exit(EXIT_FAILURE); } if (argc == 1) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) { if (stat(argv[i], &finfo) == -1) { perror(argv[i]); continue; } switch (finfo.st_mode & S_IFMT) { case S_IFBLK: putchar('b'); break; case S_IFCHR: putchar('c'); break; case S_IFIFO: putchar('p'); break; case S_IFREG: putchar('-'); break; case S_IFDIR: putchar('d'); break; case S_IFLNK: putchar('l'); break; case S_IFSOCK: putchar('s'); break; default: putchar('?'); break; } for (int i = 0; i < 9; ++i) putchar(finfo.st_mode & modes[i] ? "rwx"[i % 3] : '-'); printf("%ju ", (uintmax_t)finfo.st_nlink); printf("%ju ", (uintmax_t)finfo.st_uid); printf("%ju ", (uintmax_t)finfo.st_gid); printf("%jd ", (intmax_t)finfo.st_size); ptm = localtime(&finfo.st_mtim.tv_sec); strftime(buf, 32, "%b %2e %Y %H:%M ", ptm); printf("%s ", buf); printf("%s\n", argv[i]); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi "ls -l" komutunun yaptığı gibi ekrana kullanıcı adını ve grup adını nasıl yazdırabiliriz? Bunun için yine bir takım POSIX fonksiyonları bulunmaktadır. Bu fonksiyonları ise şunlardır; "getpwnam", "getpwuid", "getgrnam" ve "getgrgid" isimli fonksiyonlardır. Bu fonksiyonlardan, >>> "getpwnam" ve "getpwuid" : Bu fonksiyonlar o kullanıcıya ait bilgileri, sırasıyla kullanıcının adını ve kullanıcı ID değerini kullanarak, "/etc/passwd" dosyasından temin etmektedirler. Fonksiyonların prototipleri aşağıdaki gibidir: #include struct passwd *getpwnam(const char *name); struct passwd *getpwuid(uid_t uid); Fonksiyonlar başarı durumunda içi doldurulan "passwd" türünden statik ömürlü bir nesneye, hata durumunda ise "NULL" değerine dönerler. "struct passwd" yapısı aşağıdaki gibi tanımlanmıştır: struct passwd { char *pw_name; /* username */ char *pw_passwd; /* user password */ uid_t pw_uid; /* user ID */ gid_t pw_gid; /* group ID */ char *pw_gecos; /* user information */ char *pw_dir; /* home directory */ char *pw_shell; /* shell program */ }; Fonksiyon herhangi bir hatadan dolayı da yine "NULL" değerine döner fakat bu sefer "errno" değişkenini de uygun değere çeker. Dolaysıyla bizler bu fonksiyonları çağırmadan evvel "errno" değişkeninini sıfıra çekmeli, çağrıdan sonra da değerini kontrol etmeliyiz. >>> "getgrnam" ve "getgrgid" : Bu fonksiyonlar ise o kullanıcıya ait grup bilgilerini, sırasıyla kullanıcının adını ve kullanıcı ID değerini kullanarak, "/etc/group" dosyasından temin etmektedir. Fonksiyonların prototipleri aşağıdaki gibidir: #include struct group *getgrnam(const char *name); struct group *getgrgid(gid_t gid); Fonksiyonlar başarı durumunda içi doldurulan "group" türünden statik ömürlü bir nesneye, hata durumunda ise "NULL" değerine dönerler. "struct group" yapısı aşağıdaki gibi tanımlanmıştır: struct group { char *gr_name; /* group name */ char *gr_passwd; /* group password */ gid_t gr_gid; /* group ID */ char **gr_mem; /* NULL-terminated array of pointers to names of group members */ }; Fonksiyon herhangi bir hatadan dolayı da yine "NULL" değerine döner fakat bu sefer "errno" değişkenini de uygun değere çeker. Dolaysıyla bizler bu fonksiyonları çağırmadan evvel "errno" değişkeninini sıfıra çekmeli, çağrıdan sonra da değerini kontrol etmeliyiz. Aşağıda bu fonksiyonların kullanımına ilişkin örnekler verilmiştir: * Örnek 1, #include #include #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { struct stat finfo; mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; char buf[32]; struct tm *ptm; struct passwd *pwd; struct group *grp; if (setlocale(LC_ALL, "tr_TR.utf-8") == NULL) { fprintf(stderr, "cannot set locale!..\n"); exit(EXIT_FAILURE); } if (argc == 1) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } for (int i = 1; i < argc; ++i) { if (stat(argv[i], &finfo) == -1) { perror(argv[i]); continue; } switch (finfo.st_mode & S_IFMT) { case S_IFBLK: putchar('b'); break; case S_IFCHR: putchar('c'); break; case S_IFIFO: putchar('p'); break; case S_IFREG: putchar('-'); break; case S_IFDIR: putchar('d'); break; case S_IFLNK: putchar('l'); break; case S_IFSOCK: putchar('s'); break; default: putchar('?'); break; } for (int i = 0; i < 9; ++i) putchar(finfo.st_mode & modes[i] ? "rwx"[i % 3] : '-'); printf("%ju ", (uintmax_t)finfo.st_nlink); if ((pwd = getpwuid(finfo.st_uid)) != NULL) printf("%s ", pwd->pw_name); else printf("%ju ", (uintmax_t)finfo.st_uid); if ((grp = getgrgid(finfo.st_gid)) != NULL) printf("%s ", grp->gr_name); else printf("%ju ", (uintmax_t)finfo.st_gid); printf("%jd ", (intmax_t)finfo.st_size); ptm = localtime(&finfo.st_mtim.tv_sec); strftime(buf, 32, "%b %2e %Y %H:%M ", ptm); printf("%s ", buf); printf("%s\n", argv[i]); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Diğer yandan POSIX dünyasında bütün kullanıcıların bilgilerini elde edebilmek için de bir takım fonksiyonlar bulundurulmuştur. Bu fonksiyonlar ise şunlardır; "setpwent", "getpwent" ve "endpwent" ile "setgrent", "getgrent" ve "endgrent" isimli fonksiyonlardır. Bunlardan, >>> "setpwent", "getpwent" ve "endpwent" : Bu grup fonksiyonlar, "/etc/passwd" dosyasındaki bütün kullanıcıların bilgilierini elde etmek için kullanılır. Fonksiyonların prototipleri aşağıdaki gibidir: #include void setpwent(void); struct passwd *getpwent(void); void endpwent(void); Fonksiyonlardan ilk önce "setpwent" işin başında çağrılır. Bir nevi "init." işlemi gibi. Daha sonra bir döngü içerisinde "getpwent" çağrılır ve böylelikle kullanıcılara ait bilgiler her bir döngü sonunda "struct passwd" ile elde edilir. Dosyanına sonuna gelindiğinde ise "getpwent" çağrısı, "NULL" ile geri döner. Ancak herhangi bir hata durumunda yine "NULL" ile geri döner ve "errno" değişkenini de uygun değere çeker. Dolaysıyla döngünün her turunda "errno" değişkenini sıfıra çekmeliyiz. İşimiz bittikten sonra da sonlandırma işlemleri için "endpwent" çağrılmalıdır. * Örnek 1, Aşağıdaki örnekte sistemdeki tüm kullanıcıların listesi yazdırılmıştır. #include #include #include #include void exit_sys(const char *msg); int main(void) { struct passwd *pwd; setpwent(); while (errno = 0, (pwd = getpwent()) != NULL) printf("%s\n", pwd->pw_name); if (errno != 0) exit_sys("getpwent"); endpwent(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "setgrent", "getgrent" ve "endgrent" : Bu grup fonksiyonlar, "/etc/group" dosyasındaki bütün kullanıcıların bilgilierini elde etmek için kullanılır. Fonksiyonların prototipleri aşağıdaki gibidir: #include void setgrent(void); struct group *getgrent(void); void endgrent(void); Yine işin başında, "init." amacıyla, "setgrent" çağrılır. Sonrasında bir döngü ile "getgrent" çağrılır. İşin sonunda ise "endgrent" çağrılır. Pekala "errno" değişkeni de "getgrent" döngüsünün her turunun başında yine sıfıra çekilir ki hatanın nedenini görebilelim. * Örnek 1, Aşağıdaki örnekte sistemdeki tüm grup isimleri yazdırılmıştır. #include #include #include #include void exit_sys(const char *msg); int main(void) { struct group *grp; setgrent(); while (errno = 0, (grp = getgrent()) != NULL) printf("%s\n", grp->gr_name); if (errno != 0) exit_sys("getgrent"); endgrent(); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> UNIX/Linux dünyasında, "shell" programı üzerinden "man" komutu ile bir komuta ilişkin döküman araştırırken ya da internet üzerinden kaynağını incelerken "(1)" ibaresinin olması o komutun birinci ciltte arandığını, "(2)" ibaresinin olması ise ikinci ciltte arandığını ve "(3)" ibaresinin olması ise üçüncü ciltte arandığını belirtir. Genellikle birinci cilttekiler komut satırı halini, ikinci cilttekiler sistem fonksiyonu çağıran halini ve üçüncü cilttekiler ise sistem fonksiyonu çağırmayan halini belirtir. >> Aslında UNIX/Linux sistemlerinde komut satırından dosyaların erişim haklarını değiştirmek için kullanılan "chmod" isimli bir POSIX komutu vardır. Tabii bu komut /bin dizinine yerleştirilmiş bir programdır ve bu program da aslında chmod POSIX fonksiyonu kullanılarak yazılmıştır. Komutun değişik kullanım biçimleri vardır. Örneğin: chmod 666 x.dat y.dat chmod a+w x.dat chmod +w x.dat chmod o+w x.dat chmod ugo-w x.dat Komutun detaylı kullanımı için uygun dokümanlara başvurabilirsiniz. >> Komut satırında da dizin yaratmakl için "mkdir" isimli bir POSIX komutu bulunmaktadır. mkdir komurunda -m ya da --mode ile erişim hakları verilmezse komut default olarak tüm erişim haklarını prosesin umask değerine sokarak oluşturmaktadır. Ancak -m ya da --mode seçeneği ile açıkça erişim hakları verilirse umask değeri dikkate alınmamaktadır. >> Komut satrtından bir dizini silmek için "rmdir" isimli POSIX fonksiyonu da kullanılmaktadır. Tabii rmdir komutu aslında /bin dizinindeki bir programdır. Bu program rmdir POSIX fonksiyonu kullanılarak yazılmıştır. >> UNIX/Linux sistemlerinde stat fonksiyonunun yanı sıra stat isimli bir kabuk komutu da vardır. Bu komut dosyaların stat bilgilerini elde edip onları yazdırmaktadır. Komutun örnek bir kullanımı ve çıktısı şöyledir: $ stat sample.c Dosya: sample.c Boyut: 2008 Bloklar: 8 Kimlik bloku: 4096 normal dosya Device: 803h/2051d Inode: 280141 Links: 1 Erişim: (0644/-rw-r--r--) Uid: ( 1000/ kaan) Gid: ( 1000/ study) Erişim: 2023-08-12 19:28:23.516502007 +0300 Değiştirme: 2023-08-12 19:28:23.500502009 +0300 Değişiklik: 2023-08-12 19:28:23.500502009 +0300 Doğum: 2023-06-11 19:48:04.987271791 +0300 /*================================================================================================================================*/ (19_13_08_2023) > Dizin İçerisindeki Dosyaların (Dizin Girişlerinin) Elde Edilmesi: Dizinlerin içerisinde dosyalar ve dizinler olabilmektedir. İşletim sistemleri dünyasında dizin içerisindeki öğelere genellikle "dizin girişi (directory entry)" denilmektedir. Bir dizin içerisinde dosyaların kendisi bulunmaz. Yalnızca onların isimleri ve bazı önemli bilgileri bulundurulur. Daha önceden de belirttiğimiz gibi dizinler aslında "içerisinde dizin girişlerinin bulunduğu" dosyalar gibi organize edilmiştir. Dizinlerin içerisindeki girişlerin elde edilmesi dizinlerle ilgili önemli işlemlerden biridir. Aşağıda iki Windows ve UNIX/Linux sistemlerinde kullanılan fonksiyonların karşılaştırma tablosu verilmiştir: Windows - UNIX/Linux FindFirstFile - opendir FindNextFile - readdir FindClose - closedir Şimdi de bunları incelemeye başlayalım. >> Windows Sistemlerinde: Windows sistemlerinde bir dizin'in içerisindeki dizin girişlerini elde etmek için "FindFirstFile", "FindNextFile" ve "FindClose" API fonksiyonları kullanılmaktadır. Bu fonksiyonların biraz daha gelişmiş olan Ext'li biçimleri de vardır. Bu fonksiyonlardan, >>> "FindFirstFile" : Fonksiyonun prototipi aşağıdaki gibidir: HANDLE FindFirstFile( LPCSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData ); FindFirstFile fonksiyonunun birinci parametresi yol ifadesini almaktadır. Bu fonksiyonla biz tek bir girişin bilgilerini alabileceğimiz gibi * ve ? gibi joker karakterlerini kullanarak birden fazla girişin bilgilerini de alabiliriz. Tabii eğer FindFirstFile ile birden fazla girişin bilgileri alınıyorsa bu fonksiyon onların yalnızca ilkinin bilgisini vermektedir. FindFirstFile girişe ilişkin bilgileri WIN32_FIND_DATA isimli bir yapı nesnesinin içerisine yerleştirmektedir. Fonksiyon başarı durumunda sonraki dizin girişlerini elde etmek için gereken HANDLE değerine başarısızlık durumunda INVALID_HANDLE_VALUE değerine geri dönmektedir. Başarısızlığın nedeni dizinde belirtilen kalıba uygun dosyanın bulunamaması ise GetLastError ERROR_FILE_NOT_FOUND değerine geri dönmektedir. >>> "FindNextFile" : Fonksiyonun prototipi aşağıdaki gibidir: BOOL FindNextFile( HANDLE hFindFile, LPWIN32_FIND_DATA lpFindFileData ); Eğer dizin içerisindeki birden fazla girişin bilgileri elde edilecekse bunların ilki FindFirstFile fonsiyonu tarafından diğerleri ise FindNextFile fonksiyonu tarafından elde edilmelidir. FindNextFile fonksiyonu bir kez değil döngü içerisinde çağrılmalıdır. Bu fonksiyon her çağrılışta yeni bir girişin bilgilerini elde etmektedir. FindNextFile fonksiyonunun birinci parametresi FindFirstFile fonksiyonundan elde edilen HANDLE değeridir. İkinci parametre yine bulunan dizin girişinin bilgilerinin yerleştirileceği WIN32_FIND_DATA türünden nesnenin adresini almaktadır. Fonksiyonun geri dönüş değeri işlemin başarısını belirtmektedir. FindNextFile fonksiyonu iki nedenden dolayı başarısız olmaktadır. Birincisi artık tüm dizin girişlerinin elde edilmiş olmasıdır. İkincisi ise bir IO hatasının oluşmuş olmasıdır. Programcı döngüden çıkınca bir IO hatası olup olmadığını tespit etmek ister. Eğer GetLastError ERROR_NO_MORE_FILES değerine geri dönüyorsa bu durum gayet normal olarak fonksiyonun tüm girişleri elde ettiğinden dolayı başarısız olduğu anlamına gelmektedir. >>> "FindClose" : Fonksiyonun prototipi aşağıdaki gibidir: BOOL FindClose( HANDLE hFindFile ); İşlemler bitince elde edilen HANDLE alanının serbest bhırakılması için FindClose fonksiyonun çağrılması gerekir. Bu fonksiyon da işlemin başarısına geri dönmektedir. Tabii bu fonksiyonunun başarısının kontrl edilmesine gerek yoktur. Bu fonksiyonlara argüman olarak geçilen WIN32_FIND_DATA yapısı ise şöyledir: typedef struct _WIN32_FIND_DATA { DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD dwReserved0; DWORD dwReserved1; CHAR cFileName[MAX_PATH]; CHAR cAlternateFileName[14]; DWORD dwFileType; // Obsolete. Do not use. DWORD dwCreatorType; // Obsolete. Do not use WORD wFinderFlags; // Obsolete. Do not use } WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA; Bu yapının, -> Burada bulunan dizin girişinin ismi yapının cFileName elemanında bulunmaktadır. -> Bulunan dosyanın uzunluğu iki ayrı DWORD biçiminde yapının nFileSizeHigh ve nFileSizeLow elemanlarında bulunmaktadır. Dizin girişlerinin uzunlukları 0 biçiminde verilmektedir. -> Yapının dwFileAttributes elemanı dizin girişinin özelliklerini vermektedir. Bu eleman bit bit anlamlıdır. Bu elemanın her biri bir özelliğin olup olmadığını belirtmektedir. Dizin girişi özellikleri FILE_ATTRIBUTE_XXX biçiminde sembolik sabitlerle belirtilmiştir. Örneğin, bir dizin girişinin bir dizine ilişkin olup olmadığını anlamak için FILE_ATTRIBUTE_DIRECTORY değeriyle bu özellik elemanının "bit-AND" işlemine sokulması gerekir. -> Windows dosya sistemine de bağlı olarak dosyalar ve dizinler için üç zaman bilgisi tutmaktadır: Son yazma zamanı, son okuma zamanı ve ilk yaratma zamanı. WIN32_FIND_DATA yapısı içerisinde bu bilgiler FILETIME isimli bir yapı biçiminde tutulmaktadır. Bu yapı 32 bitlik iki parçadan oluşmaktadır. Tarih ve zaman bilgisi bu yapı içerisinde 01/01/1601'den geçen 100 nano saniyelerin sayısı biçiminde tutulmaktadır. Windows her zaman dizin girişlerindeki zamanları UTC (Universial Time Clock) olarak tutmaktadır. Eğer zamanları yerel saata dönüştüreceksek önce FileTimeToLocalFileTime fonksiyonuna sokmalıyız. FILETIME değerini parçalarına ayırmak için FileTimeToSystemTime fonksiyonu kullanılmaktadır. Aşağıda bir dizin girişinin komut satırındaki "dir" komutu tarzıyla yazdırılmasına bir örnek verilmiştir. * Örnek 1, #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFileFind; WIN32_FIND_DATA findData; unsigned long long ullSize; SYSTEMTIME sysTime; if ((hFileFind = FindFirstFile("*.*", &findData)) == INVALID_HANDLE_VALUE) ExitSys("FindFirstFile"); do { FileTimeToLocalFileTime(&findData.ftLastWriteTime, &findData.ftLastWriteTime); FileTimeToSystemTime(&findData.ftLastWriteTime, &sysTime); printf("%02d.%02d.%04d %02d:%02d ", sysTime.wDay, sysTime.wMonth, sysTime.wYear, sysTime.wHour, sysTime.wMinute); if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) printf("%-14s", ""); else { ullSize = (unsigned long long)findData.nFileSizeHigh << 32 | findData.nFileSizeLow; printf("%14llu", ullSize); } printf(" %s\n", findData.cFileName); } while (FindNextFile(hFileFind, &findData)); if (GetLastError() != ERROR_NO_MORE_FILES) ExitSys("FindNextFile"); FindClose(hFileFind); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >> UNIX/Linux Sistemlerinde: UNIX/Linux sistemlerinde bir dizin'in içerisindeki dizin girişlerini elde etmek için "opendir", "readdir" ve "closedir" POSIX fonksiyonları kullanılmaktadır. Bu fonksiyonlardan, >>> "opendir" : Fonksiyonun prototipi aşağıdaki gibidir: #include DIR *opendir(const char *name); opendir fonksiyonu, bir dizini açmak için kullanılmaktadır. Prosesin dizini açabilmesi için dizine "r" hakkının bulunuyor olması gerekir. Fonksiyon başarı durumunda DIR isimli yapı türünden adrese, başarısızlık durumunda NULL adrese geri dönmektedir. DIR yapısı dökümante edilmemiştir. Ancak programcının bu yapının içeriğini bilmek zorunda değildir. Bu yapı bir handle olarak kullanılmaktadır. >>> "readdir" : Fonksiyonun prototipi aşağıdaki gibidir: #include struct dirent *readdir(DIR *dirp); Dizin girişleri bir döngü içerisinde readdir fonksiyonu çağrılarak elde edilmektedir. Her readdir çağrıldıında bir dizin girişi dirent isimli bir yapı adresi biçiminde bize verilmektedir. readdir fonksiyonun geri döndürdüğü adresteki yapı nesnesi statik düzeyde tahsis edilmiş durumdadır. readdir fonksiyonu dizin listesinin sonuna gelindiğinde ya da IO hatası oluştuğunda NULL adrese geri dönmektedir. Ancak eğer dizin listenin sonuna gelinmişse (bu normal durumdur) errno değişkeninin değeri değiştirilmemektedir. Eğer fonksiyon IO hatasından dolayı başarısız olmuşsa errno değişkeni uygun biçimde set edilmektedir. Burada programcı fonksiyonu çağırmadan önce errno değişkenine 0 atamalı döngüden çıkıldığında errno değişkeninin değerine bakmalıdır. readdir fonksiyonu her çağrıldığında dizin girişi bilgilerini dirent isimli bir yapı nesnesinin içerisine yerleştirir. Bu yapı nesnesinin adresiyle geri döner. Yani programcı dizin girişlerine ilişkin bilgileri bu yapıdan almaktadır. dirent yapısı şöyle bildirilmiştir: struct dirent { ino_t d_ino; /* Inode number */ char d_name[256]; /* Null-terminated filename */ }; Windows sistemlerinde dizin girişlerinde dosyaya ya da dizine ilişkin pek çok bilgi tutulmaktadır. Ancak UNIX/Linux sistemlerinde dizin girişlerinde yalnızca (kabaca) "dizin girinin ismi" ve "i-node numarası" tutulmaktadır. Şöyleki: dizin_girişi_ismi i-node_no dizin_girişi_ismi i-node_no dizin_girişi_ismi i-node_no dizin_girişi_ismi i-node_no ... Görüldüğü gibi UNIX/Linux sistemlerinde biz dizin girişlerindne yalnızca girişin ismini ve i-node numarasını elde etmekteyiz. Ayrıca UNIX/Linux sistemlerinde Windows sistemlerinde olduğu gibi "*" ve "?" gibi joker karakterlerini kullanamadığımıza dikkat ediniz. Bu sistemlerde biz dizindeki belli girişleri değil tüm girişleri elde etmekteyiz. Pekiyi bizler dizin girişlerindeki dosya isimlerini ve i-node numaralarını elde ettikten sonra o dosyaya ilişkin diğer bilgileri nasıl elde edeceğiz? İşte burada yapılması gereken readdir ile dizin girişinin ismi elde edildikten sonra ayrıca stat fonksiyonuyla dosyanın bilgilerinin elde edilmesidir. Yani tek başına readdir kullanmak yerine aynı zamanda stat fonksiyonunun da kullanılması gerekmektedir. Pekiyi readdir fonksiyonu ile okuduğumuz dizin girişlerindeki i-node numarası be anlama gelmektedir? >>>> "I-Node" Numarası: Anımsanacağı gibi dosya bilgileri stat fonksiyonları ile elde edilirken stat yapısınında st_ino elemanı da ilgili dosyanın i-node numarasını belirtiyordu. Bir disk i-node tabanlı bir dosya sistemi ile (örneğin ext-2, ext-3 gibi) formatlandığında diskte dört farklı bölüm oluşturulmaktadır: Boot Block Super Block I-Node Block Data Block Bu bloklardan, -> Boot Block 1024 byte uzunluğunda sistemin boot edilmesi için gereken bilgileri içermektedir. -> Formatlanmış diskin tüm parametrik bilgileri Super Block'ta bulunmaktadır. -> Dosyaların parçaları yani dosya bilgileri ise Data Block içerisinde tutulmaktadır. -> İşte I-Node block, i-node elemanlarından oluşmaktadır: I-Node Block ---------------- i-node elemanı i-node elemanı i-node elemanı ... i-node elemanı i-node elemanı ... Bir dosyaya ilişkin tüm bilgiler o dosyaya ilişkin i-node elemanından elde edilmektedir. Başka bir deyişle stat, lstat ve fstat fonksiyonları aslında dosya bilgilerini dosyaya ilişkin i-node elemanından elde ederler. İşte her I-Node Block'taki her i-node elemanının ilk i-node elemanın numarası 0 olacak biçimde bir sıra numarası vardır: I-Node Block ---------------- 0 i-node elemanı 1 i-node elemanı 2 i-node elemanı ... 1200 i-node elemanı 1201 i-node elemanı ... İşte bir dosyanın i-node numarası o dosyanın bilgilerinin i-node block'taki kaçıncı i-node elemanında bulunduğunu belirtmektedir. Ancak bir dosyanın i-node numarasından hareketle bilgilerinin elde edilmesi için kullanılabilecek bir POSIX fonksiyonu yoktur. Dosya bilgileri yol ifadelerinden ya da dosya betimleyicilerinden hareketle elde edilebilmektedir. Tabii işletim sistemi kendisi bu i-node numarasından hareketle dosya bilgilerine erişmektedir. Dosyaların i-node numaraları sistem genelinde tektir. >>> "closedir" : Fonksiyonun prototipi aşağıdaki gibidir: #include int closedir(DIR *dirp); Nihayet programcı işini bitirdikten sonra closedir fonksiyonu ile dizini kapatmalıdır. closedir başarı durumunda 0 değerine başarısızllık durumunda -1 değerine geri dönmektedir.Fonksiyonun başarısının kontrol edilmesine gerek yoktur. Şimdi de konuyla ilgili örnekleri inceleyelim: * Örnek 1, Aşağıdkai örnekte belli bir dizindeki dizin girişleri elde edilip stat fonksiyonuna sokulmuş ve ilgili girişe ilişkin dosya bilgileri elde edilmiştir. Bu örnekte bir noktaya dikkat ediniz. readdir fonksiyonu ile elde ettiğimiz giriş isminde bir yol ifadesi yoktur. Halbuki bizim stat fonksiyonu için uygun bir yol ifadesine gereksnimimiz vardır. Bu nedenle kodda sprintf fonksiyonu ile uygun yol ifadesi elde edilmiştir. #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { DIR *dir; struct dirent *ent; struct stat finfo; char path[PATH_MAX]; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((dir = opendir(argv[1])) == NULL) exit_sys("opendir"); while (errno = 0, (ent = readdir(dir)) != NULL) { snprintf(path, PATH_MAX, "%s/%s", argv[1], ent->d_name); if (stat(path, &finfo) == -1) perror(path); printf("%-30s%jd\n", ent->d_name, (intmax_t)finfo.st_size); } if (errno != 0) exit_sys("readdir"); closedir(dir); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0, Aşağıdaki program sayesinde dizin içerisindeki dosyaları da "ls -l" formatında ekrana yazdırabiliriz. Ancak hizalamaya dikkat edilmemiştir. /* Ekran Çıktısı */ -rw-r--r--1 kaan study 99 Haz 11 2023 18:17 y.c -rwxr-xr-x1 kaan study 16136 Haz 17 2023 18:51 a.out -rw-r--r--1 kaan study 0 Tem 23 2023 19:56 y.txt drwxr-xr-x2 kaan study 4096 Ağu 13 2023 19:29 xxx -rw-r--r--1 kaan study 895 Ağu 26 2023 17:09 sample.c -rw-r--r--1 kaan study 2645 Ağu 26 2023 17:37 mample.c -rw-rw-rw-1 kaan study 0 Tem 23 2023 20:57 m.txt prw-r--r--1 kaan study 0 Ağu 12 2023 17:35 mypipe -rw-r--r--1 kaan study 0 Tem 23 2023 19:31 x.txt -rwxr-xr-x1 kaan study 16328 Haz 18 2023 19:17 mycalc -rwxr-xr-x1 kaan study 16472 Ağu 26 2023 17:09 sample -rw-r--r--1 kaan study 3570 Haz 18 2023 18:57 disp.c -rw-r--r--1 kaan study 1161 Haz 18 2023 19:16 mycalc.c -rw-r--r--1 kaan study 99 Haz 11 2023 18:17 x.c drwxrwxr-x8 kaan study 4096 Ağu 14 2023 22:15 .. -rw-r--r--1 kaan study 5000001 Tem 22 2023 20:07 test.txt -rwxr-xr-x1 kaan study 16816 Ağu 26 2023 17:37 mample -rw-rw-rw-1 kaan study 0 Tem 23 2023 20:45 z.txt drwxrwxr-x3 kaan study 4096 Ağu 26 2023 17:37 . /* Programın Kendisi */ #include #include #include #include #include #include #include #include #include #include void disp_ls_style(const struct stat *finfo, const char *name); void exit_sys(const char *msg); int main(int argc, char *argv[]) { DIR *dir; struct dirent *ent; char path[PATH_MAX]; struct stat finfo; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (setlocale(LC_ALL, "tr_TR.utf-8") == NULL) { fprintf(stderr, "cannot set locale!..\n"); exit(EXIT_FAILURE); } if ((dir = opendir(argv[1])) == NULL) exit_sys("opendir"); while (errno = 0, (ent = readdir(dir)) != NULL) { snprintf(path, PATH_MAX, "%s/%s", argv[1], ent->d_name); if (stat(path, &finfo) == -1) perror(path); disp_ls_style(&finfo, ent->d_name); } if (errno != 0) exit_sys("readdir"); closedir(dir); } void disp_ls_style(const struct stat *finfo, const char *name) { mode_t modes[9] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; char buf[32]; struct tm *ptm; struct passwd *pwd; struct group *grp; switch (finfo->st_mode & S_IFMT) { case S_IFBLK: putchar('b'); break; case S_IFCHR: putchar('c'); break; case S_IFIFO: putchar('p'); break; case S_IFREG: putchar('-'); break; case S_IFDIR: putchar('d'); break; case S_IFLNK: putchar('l'); break; case S_IFSOCK: putchar('s'); break; default: putchar('?'); break; } for (int i = 0; i < 9; ++i) putchar(finfo->st_mode & modes[i] ? "rwx"[i % 3] : '-'); printf("%ju ", (uintmax_t)finfo->st_nlink); if ((pwd = getpwuid(finfo->st_uid)) != NULL) printf("%s ", pwd->pw_name); else printf("%ju ", (uintmax_t)finfo->st_uid); if ((grp = getgrgid(finfo->st_gid)) != NULL) printf("%s ", grp->gr_name); else printf("%ju ", (uintmax_t)finfo->st_gid); printf("%jd ", (intmax_t)finfo->st_size); ptm = localtime(&finfo->st_mtim.tv_sec); strftime(buf, 32, "%b %2e %Y %H:%M ", ptm); printf("%s ", buf); printf("%s\n", name); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.1, Yukarıdaki programın hizalamaya dikkat eden versiyonu. Ancak bu program direkt ekrana değil, önce statik ömürlü bir tampona yazmaktadır. #include #include #include #include #include #include #include #include #include #include #include void exit_sys(const char *msg); const char *get_ls(const char *path, int hlink_digit, int uname_digit, int gname_digit, int size_digit); int main(int argc, char *argv[]) { DIR *dir; struct dirent *dent; struct stat finfo; char path[PATH_MAX]; struct passwd *pass; struct group *gr; int len; int hlink_digit, uname_digit, gname_digit, size_digit; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((dir = opendir(argv[1])) == NULL) exit_sys("open"); hlink_digit = uname_digit = gname_digit = size_digit = 0; while (errno = 0, (dent = readdir(dir)) != NULL) { snprintf(path, PATH_MAX, "%s/%s", argv[1], dent->d_name); if (stat(path, &finfo) == -1) exit_sys("stat"); len = (int)log10(finfo.st_nlink) + 1; if (len > hlink_digit) hlink_digit = len; if ((pass = getpwuid(finfo.st_uid)) == NULL) exit_sys("getppuid"); len = (int)strlen(pass->pw_name); if (len > uname_digit) uname_digit = len; if ((gr = getgrgid(finfo.st_gid)) == NULL) exit_sys("getgrgid"); len = (int)strlen(gr->gr_name); if (len > gname_digit) gname_digit = len; len = (int)log10(finfo.st_size) + 1; if (len > size_digit) size_digit = len; } if (errno != 0) exit_sys("readdir"); rewinddir(dir); while (errno = 0, (dent = readdir(dir)) != NULL) { sprintf(path, "%s/%s", argv[1], dent->d_name); if (stat(path, &finfo) == -1) exit_sys("stat"); printf("%s\n", get_ls(path, hlink_digit, uname_digit, gname_digit, size_digit)); } if (errno != 0) exit_sys("readdir"); closedir(dir); return 0; } const char *get_ls(const char *path, int hlink_digit, int uname_digit, int gname_digit, int size_digit) { struct stat finfo; static char buf[4096]; static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH }; struct passwd *pass; struct group *gr; char *str; int index = 0; int i; if (stat(path, &finfo) == -1) return NULL; if (S_ISREG(finfo.st_mode)) buf[index] = '-'; else if (S_ISDIR(finfo.st_mode)) buf[index] = 'd'; else if (S_ISCHR(finfo.st_mode)) buf[index] = 'c'; else if (S_ISBLK(finfo.st_mode)) buf[index] = 'b'; else if (S_ISFIFO(finfo.st_mode)) buf[index] = 'p'; else if (S_ISLNK(finfo.st_mode)) buf[index] = 'l'; else if (S_ISSOCK(finfo.st_mode)) buf[index] = 's'; ++index; for (i = 0; i < 9; ++i) buf[index++] = (finfo.st_mode & modes[i]) ? "rwx"[i % 3] : '-'; buf[index] = '\0'; index += sprintf(buf + index, " %*llu", hlink_digit, (unsigned long long)finfo.st_nlink); if ((pass = getpwuid(finfo.st_uid)) == NULL) return NULL; index += sprintf(buf + index, " %-*s", uname_digit, pass->pw_name); if ((gr = getgrgid(finfo.st_gid)) == NULL) return NULL; index += sprintf(buf + index, " %-*s", gname_digit, gr->gr_name); index += sprintf(buf + index, " %*lld", size_digit, (long long)finfo.st_size); index += strftime(buf + index, 100, " %b %e %H:%M", localtime(&finfo.st_mtime)); str = strrchr(path, '/'); sprintf(buf + index, " %s", str ? str + 1 : path); return buf; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> UNIX/Linux Sistemlerinde Dizinlerin "x" Hakları: UNIX/Linux sistemlerinde dizinler için "x" hakkı farklı bir anlama gelmektedir. Dizinler için "x" hakkı yol ifadelerinde "içinden geçilebilirlik" anlamına gelmektedir. Örneğin "/home/kaan/Study/SysProg/test.txt" biçiminde bir yol ifadesi söz konusu olsun. İşletim sistemi bir yol ifadesinde hedeflenen dizin girişine önceki dizin girişlerinden geçerek erişmektedir. Bu sürece İngilizce "pathname resolution" ya da "pathname lookup" denilmektedir. Pathname resolution işlemi sırasında prosesin yol ifadesindeki tüm dizinler için "x" hakkına sahip olması gerekmektedir. Örneğin yukarıdaki yol ifadesinin çözülebilmesi için prosesin kök dizine, "home" dizinine "kaan" dizinine, "study" dizinine, SysProg dizinine "x" hakkına sahip olması gerekmektedir. Benzer biçimde "test.txt" biçiminde göreli bir yol ifadesi için de yine prosesin prosesin çalışma dizinine "x" hakkına sahip olması gerekmektedir. Dizinler için "x" hakkını kaldırırsak artık onun ötesine geçilemez. Bir yol ifadesinde hedef dosyanın içinde bulunduğu dizilere ve önceki dizinlere prosesin "r" hakkının olması gerekmemektedir. Önemli olan hedefteki dosyaya erişim hakkıdır. Örneğin, "/home/kaan/Study/SysProg/test.txt" biçiminde bir yol ifadesi olsun. Bizim bu dosyayı O_RDONLY modunda açabilmemiz için bu dosyaya "r" hakkına sahip olmamız gerekir. Buradaki dizinler için "r" hakkına sahip olmamız gerekmez. Ancak buradaki diziler için "x" hakkına sahip olmamız gerekir. Yani "pathname resolution" işlemi bir okuma anlamına gelmemektedir. Diğer yandan komut satırındaki mkdir komutu zaten default olarak yaratılan dizinde herkese "x" hakkını vermektedir. mkdir POSIX fonksiyonunda bizim fonksiyonun ikinci parametresinde bu hakları vermemiz gerekir. >> "printf" ve türevi fonksiyonlarla ile yazıların arasında değişken uzunlukta boşluk karakteri eklemek için: * Örnek 1, Uzun Yol: #include int main() { /* # OUTPUT # width: 5 Used Format: %-5d%5d 0 0 1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 */ int width; char format[64]; printf("width: "); scanf("%d", &width); snprintf(format, 64, "%%-%dd%%5d\n", width); printf("Used Format: %s\n", format); for (int i = 0; i < 10; ++i) { printf(format, i, i*i); } return 0; } * Örnek 2, Aşağıdaki '*' karakteri için de ekstradan değişkeni argüman olarak geçmemiz gerekmektedir. #include int main() { /* # OUTPUT # width: 5 0 0 1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 */ int width; char format[64]; printf("width: "); scanf("%d", &width); for (int i = 0; i < 10; ++i) { printf("%-*d%5d\n", width, i, i*i); } return 0; } /*================================================================================================================================*/ (20_26_08_2023) & (21_27_08_2023) & (22_02_09_2023) & (23_03_09_2023) > Fonksiyon Göstericileri: Aslında fonksiyonlar makine komutlarından oluşmaktadır. Fonksiyonların makine komutları ardışıl bir biçimde bellekte bulunmaktadır. Bir fonksiyonu çağırmak ise aslında o fonksiyonun başlangıcındaki komutun bulunduğu yere akışın aktarılmasıdır. Makine dillerinde fonksiyon çağırma işlemi için genellikle CALL biçiminde isimlendirilen makine komutları bulundurulmuştur. Bu makine komutları çalıştırılacak fonksiyonun başlangıç adresini operand olarak almaktadır. Örneğin: "CALL address" Pekiyi CALL makine komutuyla goto işlemini yapan JMP (bu komut BRANCH olarak da isimlendirilmektedir) komutları arasındaki fark nedir? İşte CALL makine komutları geri dönüş imkanını verirken JMP komutları geri dönüş imkanını vermemektedir. Pekiyi geri dönüş nasıl olmaktadır? İşte CALL makine komutu adrese dallanmadan önce sonraki komutun adresini stack denilen alamda saklar sonra adrese dallanır. Fonksiyonun sonunda bir RET makine komutu bulunur. Bu RET makime komutu da stack'te saklanan adrese dallanır. Yani geri dönüşün mümkün olmasının sebebi aslında geri dönüş adresinin saklanmış olmasındadır. Halbuki JMP komutları geri dönüş adreslerini saklamamaktadır. Biz fonksiyonları ardışıl makşine komutları olarak düşünebiliriz. Onları çağırmak için onların yalnızca başlangıç adreslerini bilmemiz yeterli olmaktadır. İşte fonksiyon adreslerini tutan özel göstericilere "fonksiyon göstericileri (pointer to function)" denilmektedir. Bir fonksiyon göstericisi tanımlamanın genel biçimi şöyledir: (*)([parametrik yapı]); Burada önemli olan noktalardan biri * ve gösterici isminin parantez içerisinde olmasıdır. Örneğin: int (*pf1)(int); double (*pf2)(int, int); int (*pf3)(struct stat *); Bir fonksiyon göstericisine herhangi bir fonksiyonun adresi atanamaz. Geri dönüş değeri ve parametre türleri belli biçimde olan fonksiyonların adresleri atanabilmektedir. Yukarıdaki örnekte pf1 göstericisine "geri dönüş değeri int türden olan parametresi int türdne olan" herhangi bir fonksiyonun adresi atanabilir. pf2 göstericisine "geri dönüş değeri double türden olan ve parametreleri int ve int türden olan" fonksiyonların adresleri atanabilmektedir. pf3 göstericisine ise "geri dönüş değeri int türden olan ve parametresi struct stat türünden gösterici olan" fonksiyonların adresleri ataanbilir. Fonksiyon göstericisi bildiriminde parametre parantezinin içerisine yalnızca parametrelerin türleri yazılabilir ya da istenirse onlara ilişkin isimler de yazılabilir. Tabii bu isimlerin herhangi isimlerle uyuşması gerekemektedir. Bu isimler okunabilirliği artırmak için bulundurulabilir. Ancak C programcıları genellikle parametre isimlerini belirtmezler. Örneğin: int (*pf)(int, int); int (*pf)(int a, int b); Bu iki prototip arasında hiçbir farklılık yoktur. Fonksiyon gösterici bildiriminde dekleratördeki parantezler kullanılmazsa bildirimin tamamne başka bir anlam ifade edeceğine dikkat ediniz. Örneğin: int (*pf)(int, int); Burada geri dönüş değeri int olan ve parametreleri int, int olan fonksiyonların adreslerini tutabielcek bir fonksiyon göstericisi tanımlanmıştır. Fakat örneğin: int *pf(int, int); Bu bildirim ise "geri dönüş değeri int * olan parametreleri int, int olan pf isimli bir fonksiyonun" prototip bildirimidir. Örneğin: int (*pf)(int, int); Buradaki fonksiyon gösterici bildiriminde dekleratör (*pf)(int, int) biçimindedir. Fonskiyon göstericisi bildiriminde C'de parametre parantezlerinin içinin boş bırakılmasıyla içine void yazılması arasında önemli bir farklılık vardır. Eğer bildirimde parametre parantezlerinin içi boş bırakılırsa bu durum "fonksiyon göstericisine geri dönüş değeri uyumu korunmak üzere herhangi bir parametrik yapıya sahip fonksiyonların adreslerinin atanabileceği" anlamına gelmektedir. Örneğin: int (*pf1)(); Burada pf1 göstericisine geri dönüş değeri int türden olmak koşuluyla parametrik yapısı nasıl olursa olsun her fonksiyonun adresi atanabilmektedir. Halbuki örneğin: int (*pf2)(void); Burada pf2 göstericisine geri dönüş değeri int olan ve parametresi olmayan (ya da void olan da diyebiliriz) fonksiyonların adresleri atanabilmektedir. Ancak C++'ta yukarıdaki iki biçim tamamen aynı anlama gelmektedir. Bu iki biçimin her ikisi de C++'ta geri dönüş değeri int olan parametresi olmayan fonksiyonların adreslerini atayabileceğimiz göstericilere ilişkindir. C'de tür dönüştürmelerinde kullanabileceğimiz türlerin sembolik isimleri vardır. Çrneğin: int a; int *pi; int b[10]; int *c[20]; Burada a "int" türden, pi "int *" türünden, b "int[10]" türünden ve c de "int *[20]" türündendir. Bir fonksiyon göstericisinin bu biçimdeki sembolik tür isimleri belirtilirken yine * atomu parantez içerisine alınır. Örneğin: int (*pf)(void); Burada pf göstericisi "int (*)(void)" türündendir. Diğer yandan C'de bir fonksiyonun yalnızca ismi o fonksiyonun başlangıç adresi anlamına gelmektedir. Örneğin: int foo(void) { ... } Burada yalnızca foo ismi bu fonksiyonun bellekteki başlangıç adresini belirtir. foo ismi bir nesne belirtmemektedir. Tıpkı dizi isimlerinde olduğu gibi bir sağ taraf değeri belirtmektedir. Zaten C'de fonksiyonların çağrılmasını sağlayan operatör (...) operatörüdür. Bu durumda foo ifadesi ile foo() ifadesi tamamen farklıdır. foo ifadesi foo fonksiyonunun bellekteki başlangıç adresi anlamına gelmektedir. Ancak foo() ifadesinde bu adreste bulunan fonksiyon çağrılmış ve int değeri elde edilmiştir. Bu durumda foo ifadesi "int (*)(void)" türünden, foo() ifadesi ise "int" türdendir. Fonksiyon çağırma operatörü aslında "beli bir adresten başlayan fonksiyon kodlarının çalıştırılması" işlemini yapar. Örneğin: foo(); Burada aslında "foo adresinden başlayan fonksiyon kodları" çalıştırılmaktadır. Bu durumda biz bir fonksiyon göstericisine bir fonksiyonun adresini atamak istediğimizde fonksiyonun yalnızca ismini atamalıyız. Örneğin: int foo(void) { .... } ... int (*pf)(void); pf = foo; /* geçerli */ Atama işlemini şöyle yapamayız: pf = foo(); /* geçersiz! */ Burada pf değişkenine bir fonksiyon adresi değil düz bir int değer atanmaya çalışılmaktadır. Tabii biz bir fonksiyon göstericisine uyumlu bir fonksiyon adresi ile ilkdeğer de verebiliriz. Örneğin: int (*pf)(void) = foo; Örneğin: int a, *pi, (*pf)(int); Bu bildirimde a int türden, pi int * türünden ve pf ise int (*)(int) türündendir. Şimdi aşağıdaki gibi bir fonksiyon göstericisine uyumlu bir fonksiyonun adresini atayalım: void foo(void) { ... } .... void (*pf)(void); pf = foo; Pekiyi bu pf yoluyla bu fonksiyonu nasıl çağırabiliriz? İşte bunun C'de iki yolu vardır: -> pf(...) sentaksı ile çağrıma -> (*pf)(...) sentaksı ile çağırma her ikisi de eşdeğerdir. Her iki sentaksta da pf göstericisinin içerisindeki adrste bulunan fonksiyon çağrılmaktadır. Aslında genel olarak bir fonksiyon da zaten bu iki sentaksla da çağrılabilmektedir. Örneğin: void foo(void) { ... } ... foo(); (*foo)(); Şimdi de bu konuyla ilgili örneklere bakalım: * Örnek 1, #include void foo(void) { printf("foo\n"); } int main(void) { void (*pf)(void) = foo; pf(); (*pf)(); foo(); (*foo)(); return 0; } * Örnek 2, Aşağıda fonksiyon göstericileri yoluyla fonksiyon çöağrılmasına bir örnek verilmektedir. #include int add(int a, int b) { return a + b; } int mul(int a, int b) { return a * b; } int main(void) { int (*pf)(int, int); int result; pf = add; result = pf(10, 20); printf("%d\n", result); pf = mul; result = pf(10, 20); printf("%d\n", result); return 0; } Fonksiyon Göstericileri hakkında şu noktalara da dikkat etmeliyiz: >> Bir fonksiyon göstericisine farklı türdne bir fonksiyonun adresini atayamayız. Eğer atamaya çalışırsak bu durum C'de geçerli olmadığıu için derleme işlemi başarılı olmaz.i (Tabii bazı derleyiciler bu durumda bir uyarı mesajı verip programcıyı affedebilmektedir.) Örneğin: void foo(int a) { ... } ... void (*pf)(int); pf = foo; /* geçersiz' */ Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include void foo(void (*pf)(void)) { pf(); } void bar(void) { printf("bar\n"); } int main(void) { foo(bar); return 0; } >> Fonksiyon göstericilerine bazı işlemlerin yapılabilmesi için gereksinim duyulmaktadır. Örneğin callback fonksiyon mekanizması fonksiyon göstericileri yoluyla sağlanmaktadır. Bir fonksiyon belli bir durum oluştuğunda parametresiyle aldığı fonksiyonu çağırıyorsa bu duruma callback fonksiyon mekanizaması denilmektedir. Örneğin: void for_each(int *pi, size_t size, void (*pf)(int *)) { for (size_t i = 0; i < size; ++i) pf(&pi[i]); } Burada for_each fonksiyonu int bir dizinin tüm elemanlarını dolaşmakta ancak dolaşırken bizim verdiğimiz bir fonksiyonu da çağırmaktadır. Bizim verdiğimiz callback fonksiyona for_each dizinin elemanlarının adreslerini geçirmektedir. Böylece callback fonksiyon dizi elemanlarını değiştirebilecektir. Örneğin: void disp(int *pi) { printf("%d\n", *pi); } void square(int *pi) { *pi = *pi * *pi; } ... int a[] = {10, 20, 30, 40, 50}; for_each(a, 5, disp); for_each(a, 5, square); for_each(a, 5, disp); Görüldüğü gibi fonksiyon göstericileri bir fonksiyonun davranışının dışarıdan değiştirilerek fonksiyonun genelleştirilmesi amacıyla kullanılabilmektedir. Örneğin bir GUI uygulamasında bir düğme GUI elemanına (pushbutton) tıklandığında biz belli bir işlemin yapılmasını isteyebiliriz. Ancak düğmenin kodlarını biz değil ilgili kütüphaneyi yazanlar yazmıştır. İşte düğmenin kodlarını yazan kişiler farfenin onun üzerinde tıklanıp tıklanmadığını kendiileri kontrol etmekte, eğer fare düğme üzerinde tıklanmışsa bizim verdiğimiz bir fonksiyonu çağırmaktadır. * Örnek 1, #include void for_each(int *pi, size_t size, void (*pf)(int *)) { for (size_t i = 0; i < size; ++i) pf(&pi[i]); } void disp(int *pi) { printf("%d\n", *pi); } void square(int *pi) { *pi = *pi * *pi; } int main(void) { int a[] = {10, 20, 30, 40, 50}; for_each(a, 5, disp); for_each(a, 5, square); for_each(a, 5, disp); return 0; } >> Fonksiyon parametrelerinde fonksiyon göstericileri (tıpkı normal göstericilerde olduğu gibi) alternatif sentaksla da belirtilebilmektedir. Örneğin: void foo(void pf(void)) { ... } Bu bildirim tamamne geçerlidir ve aşağıdakiyle tamamen eşdeğerdir: void foo(void (*pf)(void)) { ... } >> Fonksiyon adresleri typedef edilerek daha kolay bir kullanım sağlanabilmektedir. Örneğin: typedef void (*PF)(void); Burada PF geri dönüş değeri void olan ve parametresi void olan bir fonksiyon adresi türüdür. Yani sembolik olarak PF aslında void (*)(void) türünü temsil etmektedir. Örneğin: PF pf; bildirimi ile, void (*pf)(void); bildirimi tamamen yanı anlama gelmeketedir. >> Her elemanı bir fonksiyon göstericisi olan fonksiyon gösterici dizileri de bildirilebilir. Bu bildirimde köşeli parantezler ve * atomu parantez içerisine alınmalıdır. Örneğin: void (*pfs[5])(void); Burada pfs 5 eleanlı bir dizidir. Bu dizinin her elemanı "geri dönüş değeri void olan ve parametresi void olan" bir fonksiyon göstericisidir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } void tar(void) { printf("tar\n"); } int main(void) { void (*pfs[3])(void); pfs[0] = foo; pfs[1] = bar; pfs[2] = foo; for (int i = 0; i < 3; ++i) pfs[i](); return 0; } >> Fonksiyon gösterici dizilerine de küme parantezleri içerisinde ilkdeğer verebiliriz. Tabii verdiğimiz ilkdeğerlerin aynı türden fonksiyon adresleri olması gerekir. Örneğin: void (*pfs[3])(void) = {foo, bar, tar}; Tabii yine ilkdeğer verirken dizi uzunluğu belirtilmeyebilir. Örneğin: void (*pfs[])(void) = {foo, bar, tar}; Burada pfs dizisine üç elemanla ilkdeğer verildiği için pfs üç elemanlı bir dizidir. Yukarıda da belirtitğimiz gibi typedef bildirimi ile bu tür tanımlşamaları daha kolay yapabiliriz. Örneğin: typedef void (*PF)(void); ... PF pfs[] = {foo, bar, tar}; Tabii istersek fonksiyon gösterici dizileri için de typedef işlemi yapabiliriz. Örneğin: typedef void (*PFS[3])(void); ... PFS pfs = {foo, bar, tar}; Burada artık PFS üç elemanlı geri dönüş değeri void parametresi void olan fonksiyon göstericisi dizisini temsil etmektedir. Yani PFS türünün sembolik gösterimi void (*[3])(void) biçimindedir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } void tar(void) { printf("tar\n"); } int main(void) { void (*pfs[3])(void) = {foo, bar, tar}; for (int i = 0; i < 3; ++i) pfs[i](); return 0; } >> Her ne kadar fonksiyon isimleri zaten fonksiyonların adreslerini belirtiyorsa da istenirse fonksiyon isimleri & operatörüyle de kullanılanilir. Yani aslında C'de foo bir fonksiyon belirtmek üzere foo ifadesi ile &foo ifadesi eşdeğerdir. Benzer biçimde foo bir fonksiyon adresi belirtmek üzere bu fonksiyonu çağırmak için foo() ifadesiyle tamamen eşdeğer (*foo)() ifadesi de kullanılabilmektedir. Örneğin: * Örnek 1, #include void foo(void) { printf("foo\n"); } int main(void) { void (*pf)(void); pf = &foo; /* eşdeğeri: pf = foo */ (*pf)(); /* eşdeğer: pf() */ return 0; } >> C'de (ve C++'ta) data adreslerinden fonksiyon adreslerine fonksiyon adreslerinden data adreslerine bir dönüştürme yapılamamaktadır. Yani C standartlarına göre tür dönüştürmesi yapılsa bile bu dönüştürme geçerli değildir. Örneğin: void (*pf)(void); int pi; pi = (int *)pf; /* geçersiz! böyle bir dönüştürme yok! */ Her ne kadar böylesi dönüştürmeler C'de geçerli değilse de Microsoft, gcc ve clang derleyicilerinde bu dönüştürmelere izin verilmektedir. C'de (C++'ta da böyle) void göstericiler data göstericisi olarak kabul edilmektedir. Yani void göstericilere biz türü ne olursa olsun nesnelerin adreslerini atayabiliriz ancak fonksiyon adreslerini atayamayız. Örneğin: int a; double b; void foo(void); void *pv; ... pv = &a; /* geçerli */ pv = &b; /* geçerli */ pv = foo; /* geçersiz! fonksiyon adreslerinden data adreslerine, data adreslerinden fonksiyon adreslerine dönüştürme yok! */ Ancak Microsoft derleyicileri, gcc ve clang derleyicileri bunu kabul etmektedir. Fonksiyon adreslerinin data adreslerine dönüştürülmesi ve data adreslerinin fonksiyon adreslerine dönüştürülmesi aslında dolaylı bir biçimde sağlanabilmektedir. Örneğin elimizde bir fonksiyon adresi olsun biz de bunu void göstericiye atamak isteyelim: void (*pf)(void); void *pv; ... pv = (void *)pf; /* geçersiz! */ Burada pf'nin adresi bir fonksiyon adresi değildir. Fonksiyon göstericisinin adresidir. O halde bu işlem şöyle yapılabilir. pv = *(void **) &pf; Buradaki anahtar nokta bir fonksiyon göstericisinin adresinin bir fonksiyon adresi değil data adresi olduğudur. Bunun tersi de şöyle yapılabilir: void (*pf)(void); void *pv; ... *(void **)&pf = pv; C'de (ve C++'ta) NULL adres fonksiyon göstericilerine de atanabilmektedir. Örneğin: void (*pf)(void) = 0; Burada pf göstericisine int 0 değil, o sistemdeki NULL adres atanmıştır. Tabii C standartlarına göre (ancak C++'ta böyle değil) NULL adres (void *)0 biçiminde de belirtilebilir. Bu özel bir durum olduğu için data adresi kabul edilmemektedir. Örneğin: void (*pf)(void) = (void *)0; Burada her ne kadar sanki void bir adres fonksiyon göstericisine atanıyor gibiyse de "(void *)0" ifadesinin NULL adres biçiminde özel bir anlamı vardır. >> Bir fonksiyon adresi başka türden bir fonksiyon adresine dönüştürülebilir. Örneğin: void foo(int a); ... void (*pf)(int); pf = foo; /* geçeriz! */ pf (void (*)(void))foo /* geçerli */ Tabii bu tür dönüştürmeleri typedef işlemleriyle daha kolay yapabiliriz. Örneğin: typedef void(*PF)(void); void foo(int a); ... void (*pf)(int); pf = foo; /* geçeriz! */ pf (PF)foo /* geçerli */ >> Bir fonksiyonun geri dönüş değeri de bir fonksiyon adresi olabilir. Bu durumda * atomu yine parantez içerisine alınmalı parantezin soluna geri dönüş değerine ilişkin fonksiyonun geri dönüş değerinin türü parantezin sağına ise geri dönüş değerine ilişkin fonksiyonun parametresi yazılmalıdır. Örneğin: void (*foo(int a))(int) { ... } Burada foo fonksiyonunun parametresi int türdendir. Ancak geri dönüş değeri paramtresi int türden olan geri dönüş değeri void türden olan bir fonksiyon adresidir. Buradaki tanımla adım adım şöyle oluşturulmaktadır: -> foo fonksiyonunun parametresi int türdendir. Örneğin, "...foo(int a)..." -> foo fonksiyonunun geri dönüş değeri bir fonksiyon adresidir. Örneğin, "...(*foo(int a))..." -> foo fonksiyonunun geri dönüş değerine ilişkin fonksiyonun geri dönüş değeri void türdendir. Örneğin, "void (*foo(int a))..." -> foo fonksiyonunun geri dönüş değerine ilişkin fonksiyonun parametresi ise int türdendir. Örneğin: "void (*foo(int a))(int)" Aşağıda böylesi bir fonksiyon göstericisinin kullanımına ilişkin örnek verilmiştir: * Örnek 1, #include void bar(void) { printf("bar\n"); } void (*foo(void))(void) { return bar; } int main(void) { void (*pf)(void); pf = foo(); pf(); return 0; } Bu tür bildirimlerde typedef işlemleri bildirimleri oldukça kolaylaştırmaktadır. Örneğin: * Örnek 1, #include typedef void (*PF)(void); void bar(void) { printf("bar\n"); } PF foo(void) { return bar; } int main(void) { PF pf; pf = foo(); pf(); return 0; } >> C'de fonksiyon göstericilerine ilişkin daha karmaşık bildirimler söz konusu olabilmektedir. Ancak neyse ki bu tür bildirimlerle oldukça seyrek karşılaşılmaktadır. Örneğin şçyle bir fonksiyon yazmak isteyelim: -> Fonksiyonumuz ismi foo olsun ve parametresi void türden olsun, -> Fonksiyonumuzun geri dönüş değeri bir fonksiyon adresi olsun ve o fonksiyon adresinin parametresi int türden olsun. -> Fonksiyonumuzun geri dönüş değerine ilişkin fonksiyonun geri dönüş değeri de parametresi void geri dönüş değeri void olan bir fonksiyon adresi olsun. Şimdi yuakrıdaki foo fonksiyonunu tek bir cümleyle ifade etmeye çalışalım: "foo parametresi void olan geri dönüş değeri parametresi int olan geri dönüş değeri paramtresi void olan geri dönüş değeri void olan bir fonksiyon adresidir." Şimdi bu fonksiyonu adım adım yazmaya çalışalım: -> foo fonksiyonun kendi parametresi void "...foo(void)..." -> foo fonksiyonunun geri dönüş değeri bir fonksiyon adresi "...(*foo(void))..." -> foo fonksiyonunun geri dönüş değerine ilişkin fonksiyonun parametresi int "...(*foo(void))(int)..." -> foo fonksiyonun geri dönüş değerine ilişkin fonksiyon adresinin geri dönüş değeri de fonksiyon adresi "...(*(*foo(void))(int))..." -> foo fonksiyonun geri dönüş değerine ilişkin fonksiyon adresinin geri dönüş değerine ilişkin fonksiyonun parametresi void "...(*(*foo(void))(int))(void)" -> foo fonksiyonun geri dönüş değerine ilişkin fonksiyon adresinin geri dönüş değerine ilişkin geri dönüş değeri void "void (*(*foo(void))(int))(void)" İş bu foo fonksiyonu da şöyle tanımlanabilir: void(*(*foo(void))(int))(void) { ... } Pekiyi burada foo fonksiyonu nasıl bir fonksiyon adresine geri dönmektedir? Tabii parametresi int olan geri dönüş değeri parametresi void olan geri dönüş değeri void olan bir fonksiyon adresine. Örneğin: void (*bar(int a))(void) { ... } void(*(*foo(void))(int))(void) { return bar; } Pekiyi bar fonksiyonun neye geri dönmesi gerekmektedir? Tabii geri dönüş değeri void olan, parametresi void olan bir fonksiyonun adresine. * Örnek 1, #include void tar(void) { printf("tar\n"); } void (*bar(int a))(void) { return tar; } void(*(*foo(void))(int))(void) { return bar; } int main(void) { void (*(*pf1)(int))(void); void (*pf2)(void); pf1 = foo(); pf2 = pf1(0); pf2(); foo()(0)(); return 0; } Tabii typedef işlemi ile yukarıdaki karmaşık bildirimler daha basit biçimde de ifade edilebilirdi. * Örnek 1, #include typedef void (*PF1)(void); typedef PF1(*PF2)(int); void tar(void) { printf("tar\n"); } PF1 bar(int a) { return tar; } PF2 foo(void) { return bar; } int main(void) { PF2 pf2; PF1 pf1; pf2 = foo(); pf1 = pf2(0); pf1(); return 0; } Bu tür bildirimler daha karmaşık hale de getirilebilir. Ancak uygulamada bu kadar karmaşık bildirimlerle karşılaşılmamaktadır. Uygulamada en fazla bir fonksiyonun geri dönüş değerinin basit bir fonksiyon adresi olması durumuyla karşılaşılmaktadır. * Örnek 1, #include void tar(void) { printf("tar\n"); } void (*bar(int a))(void) { return tar; } void(*(*foo(void))(int))(void) { return bar; } int main(void) { void (*(*pf1)(int))(void); void (*pf2)(void); pf1 = foo(); pf2 = pf1(0); pf2(); return 0; } * Örnek 2, signal isimli bir POSIX fonksiyonunun prototipi aşağıdaki gibidir: #include void (*signal(int sig, void (*func)(int)))(int); Burada signal fonksiyonu iki parametreli bir fonksiyondur. Fonksiyonun birinci parametresi int türden ikinci parametresi ise bir fonksiyon türündendir. Yani fonksiyonun ikinci parametresi, geri dönüş değeri void olan parametresi int olan bir fonksiyon adresini almaktadır. Burada signal fonksiyonunun geri dönüş değeri void, parametresi int olan bir fonksiyon adresidir. başlık dosyası içerisinde aşağıdaki gibi bir typede file bildirim kolaylaştırılmıştır: typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); * Örnek 3.0, Yukarıda da belirtildiği gibi fonksiyon göstericileri özellikle birtakım işlemleri genelleştirmek amacıyla kullanılmaktadır. Örneğin, Windows için ListDir isimli bir fonksiyon yazmak isteyelim. Fonksiyon bir dizindeki dizin girişlerini bulsun ancak onları yazdırmak yerine bizim ona verdiğimiz bir fonksiyonu çağırsın. Bu sayede biz o fonksiyona istediğimiz bir şeyi yaptırabiliriz. Böyle bir fonksiyonun parametrik yapısı aşağıdaki gibi olabilir: BOOL ListDir(const char *szPath, BOOL (*Proc)(WIN32_FIND_DATA *)); Fonksiyonun birinci parametresi joker karakterleri de içeren dizine ilişkin bir yol ifadesidir. İkinci parametre ilgili dizinde bir dizin girişi bulunduğunda onun bilgilerinin bulunduğu WIN32_FIND_DATA yapısının adresi ile çağrılacak olan fonksiyonu belirtmektedir. Yani ListDir fonksiyonu her dizin girişini buldukça o dizin girişi ile ikinci paramtresinde belirtilen bizim fonksiyonumuzu çağırmaktadır. Tabii bu tür durumlarda callback fonksiyon yoluyla işlemin durdurulması da gerekebilmektedir. Bu nedenle ikinci parametedeki fonksiyon BOOL bir değere geri döndürülmüştür. Bu fonksiyon sıfır dışı bir değerleile geri döndürülürse işlemler devam edecek 0 ile geri döndürülürse dolaşım durdurulacaktır. Pekiyi burada ListDir fonksiyonunun geri dönüş değeri neyi belirtmektedir? İşte fonksiyonun geri dönüş değeri işlemin başarısını belirtebilir. Aşağıdabu fonksiyonun yazımına ve kullanımına ilişkin bir örnek verilmiştir: #include #include #include #include BOOL ListDir(const char *szPath, BOOL(*Proc)(WIN32_FIND_DATA *)); void ExitSys(LPCSTR lpszMsg); BOOL FindFile(WIN32_FIND_DATA *fd) { printf("%s\n", fd->cFileName); if (!_strcmpi(fd->cFileName, "notepad.exe")) { printf("notepad.exe found\n"); return FALSE; } return TRUE; } BOOL DispFile(WIN32_FIND_DATA *fd) { unsigned long long ullSize; SYSTEMTIME sysTime; FileTimeToLocalFileTime(&fd->ftLastWriteTime, &fd->ftLastWriteTime); FileTimeToSystemTime(&fd->ftLastWriteTime, &sysTime); printf("%02d.%02d.%04d %02d:%02d ", sysTime.wDay, sysTime.wMonth, sysTime.wYear, sysTime.wHour, sysTime.wMinute); if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) printf("%-14s", ""); else { ullSize = (unsigned long long)fd->nFileSizeHigh << 32 | fd->nFileSizeLow; printf("%14llu", ullSize); } printf(" %s\n", fd->cFileName); return TRUE; } int main(void) { if (!ListDir("c:\\windows\\*.*", FindFile)) ExitSys("ListDir"); printf("----------------------------\n"); if (!ListDir("c:\\windows\\*.*", DispFile)) ExitSys("ListDir"); return 0; } BOOL ListDir(const char *szPath, BOOL(*Proc)(WIN32_FIND_DATA *)) { HANDLE hFileFind; WIN32_FIND_DATA findData; BOOL bResult = TRUE;; if ((hFileFind = FindFirstFile(szPath, &findData)) == INVALID_HANDLE_VALUE) return FALSE; do { if (!Proc(&findData)) goto EXIT; } while (FindNextFile(hFileFind, &findData)); if (GetLastError() != ERROR_NO_MORE_FILES) return FALSE; EXIT: FindClose(hFileFind); return TRUE; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 3.1, Yukarıdaki işlemin UNIX/Linux eşdeğeri de aşağıdaki gibi yazılabilir: #include #include #include #include #include #include int list_dir(const char *path, int (*proc)(const struct stat *, const char *name)); void exit_sys(const char *msg); int disp_file(const struct stat *finfo, const char *name) { printf("%-30s%jd\n", name, (intmax_t)finfo->st_size); return 1; } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (list_dir(argv[1], disp_file) == -1) exit_sys("list_dir"); return 0; } int list_dir(const char *path, int (*proc)(const struct stat *, const char *name)) { DIR *dir; struct dirent *de; char fpath[PATH_MAX]; struct stat finfo; int status = 0; if ((dir = opendir(path)) == NULL) return -1; while ((errno = 0, de = readdir(dir)) != NULL) { snprintf(fpath, PATH_MAX, "%s/%s", path, de->d_name); if (stat(fpath, &finfo) == -1) { status = -1; goto EXIT; } if (!proc(&finfo, de->d_name)) goto EXIT; } if (errno) status = -1; EXIT: closedir(dir); return status; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 4.0, C'de her türden diziyi sıraya dizebilen bir fonksiyon yazabilir miyiz? Yazarsak bunu nasıl yazabiliriz? Örneğin aşağıdaki gibi bir fonksiyon yalnızca int türden dizileri sıraya dizebilir: void sort(int *pi, size_t size); Her tür için bu fonksiyonu içi aynı olacak biçimde yeniden yazarsak belki bir ölçüde hedefimize ulaşabiliriz. Örneğin: void sort_int(int *pi, size_t size); void sort_long(long *pi, size_t size); void sort_double(double *pi, size_t size); ... Ancak burada yine bir problem vardır. Biz kendi yapımızı sıraya dizecek olsak yapımıza uygun yeni bir fonksiyon yazmamız gerekir. Türden bağımsız dizi işlemleri programlama dilelrinde önemli bir sorundur. C++ bunu sağlamak için ismine "şablon (template)" denilen bir mekanizma oluşturmuştur. Önce C# sonra da Java C++'ın şablon mekanizmasından esinlenerek benzer bir mekanizmayı "generic" adı altında oluşturmuştur. Yeni tasarlanan dilelrin çoğunda (Swift gibi, Kotlin gibi Rust gibi) bu generic mekanizma artık bulundurulmaktadır. Ancak C'de böyle bir mekanizma yoktur. Python gibi Ruby gibi dinamik tür sistemine sahip programlama dillerinde zaten bu generic mekanizma doğuştan vardır. Sort işlemi hangi alagoritmaya göre yapılırsa yapılsın dizinin iki elemanın karşılaştırılması ve duruma göre yer değiştirilmesi işlemlerini uygulamaktadır. Yani biz quick sort algoritmasını uygulasak da boubble sort algoritmasını uygulasak da eninde sonunda dizinin iki elemanını karşılaştırıp duruma göre onları yer değiştiririz. Türden bağımsız bir sort fonksiyonu yazılacaksa fonksiyon sıraya dizeceği dizinin başlangıç adresini void gösterici biçiminde almalıdır. Bu fonksiyon bu dizinin türünü bilmediğine göre iki elemanı karşılaştıramaz. Ancak dizinin türünü onu çağıran programcı bilmektedir. O zaman fonksiyon dizinin iki elemanını karşılaştırma işlemini onu çağıran programcıya bırakabilir. Tabii bunu bir fonksiyon gösterici kullanarak sağlayacaktır. Fonksiyonun dizinin iki elemanını yer değiştirebilmesi için dizinin elemanlarının kaç byte uzunlukta olduğunu biliyor olması gerekir. O zaman böyle bir fonksiyon dizinin bir elemanın byte uzunluğunu da parametre olarak almalıdır. Tabii fonksiyonun dizinin kaç eleman uzunluğunda olduğunu da biliyor olması gerekir. Bu durumda böyle bir fonksiyonun aşağidaki parametrik yapıya sahip olması uygun olacaktır: void sort(void *pv, size_t count, size_t width, int (*compare)(const void *, const void *)) Fonksiyonun birinci parametresi dizinin başlangıç adresini belirtir. İkinci parametre dizinin eleman sayısını belirtmektedir. Üçüncü parametre dizinin bir elemanının byte uzunluğunu belirtir. Son parametre ise karşılaştırma fonksiyonun adresini almaktadır. Fonksiyon ne zaman bir karşılaştırma yapılacak olsa dizinin karşılaştırılacak iki elemanının adreslerini bu fonksiyona geçirir ve bu fonksiyonu çağırır. Karşılaştırma fonksiyonunu yazan programcı "birinci parametresiyle belirtilen dizi elemanı ikinci parametresiyle belirtilen dizi elemanından büyükse fonksiyonu pozitif herhangi bir değerle, ikinci parametresiyle belirtilen dizi elemanı birinci parametresiyle belirtilen dizi elemanındna büyükse negatif herhangi bir değerle ve bu iki eleman eşitse sıfır değeri ile" geri döndürmelidir. Örneğin böyle bir fonksiyonla int bir dizi şöyel sıraya dizilebilir: int cmp(const void *ptr1, const void *ptr2); int main(void) { int a[10] = {3, 6, 2, 12, 1, 87, 45, 23, 54, 78}; sort(a, 10, sizeof(int), cmp); for (int i = 0; i < 10; ++i) printf("%d ", a[i]); printf("\n"); return 0; } int cmp(const void *ptr1, const void *ptr2) { const int *elem1 = (const int *)ptr1; const int *elem2 = (const int *)ptr2; if (*elem1 > *elem2) return 1; if (*elem1 < *elem2) return -1; return 0; } Aşağıda boubble sort algoritması ile türden bağımsız sort işlemi yapan bir örnek verilmiştir. #include #include void bsort(void *pv, size_t count, size_t width, int (*compare)(const void *, const void *)); int cmp(const void *ptr1, const void *ptr2); int main(void) { int a[10] = {3, 6, 2, 12, 1, 87, 45, 23, 54, 78}; bsort(a, 10, sizeof(int), cmp); for (int i = 0; i < 10; ++i) printf("%d ", a[i]); printf("\n"); return 0; } void bsort(void *pv, size_t count, size_t width, int (*compare)(const void *, const void *)) { unsigned char *pc = (unsigned char *)pv; unsigned char *elem1, *elem2; unsigned char temp; for (size_t i = 0; i < count - 1; ++i) for (size_t k = 0; k < count - 1 - i; ++k) { elem1 = pc + k * width; elem2 = pc + (k + 1) * width; if (compare(elem1, elem2) > 0) for (size_t j = 0; j < width; ++j) { temp = elem1[j]; elem1[j] = elem2[j]; elem2[j] = temp; } } } int cmp(const void *ptr1, const void *ptr2) { const int *elem1 = (const int *)ptr1; const int *elem2 = (const int *)ptr2; if (*elem1 > *elem2) return 1; if (*elem1 < *elem2) return -1; return 0; } * Örnek 4.1, Aslında yukarıda yazdığımız bsort fonksiyonu ile aynı parametrik yapıya sahip qsort isimli bir standart C fonksiyonu zaten bulunmaktadır. void qsort(void *base, size_t nmemb, size_t size, int (*compare)(const void *, const void *)); Fonksiyonun yine birinci parameresi dizinin başlangıç adresini, ikinci parametresi onun eleman sayısını, üçüncü parametresi bir elemanının byte uzunluğunu, dördüncü parametresi ise karşılaştımra fonksiyonunu belirtmektedir. Bu karşılaştırma fonksiyonu programcı tarafından eğer soldaki eleman sağdaki elemandan büyükse pozitif herhangi bir değere, eğer sağdaki eleman soldaki elemandan büyükse negatif herhangi bir değere ve iki eleman biribine eşitse 0 değerine geri dönmelidir. C standartlarında fonksiyonun hangi algoritmayı kullanması gerektiği belirtilmemiş olsa da tipik olarak fonksiyon mevcut C derleyicilerde "quick sort" algoritmasını kullanarak gerçekleştirilmektedir. #include #include #include struct PERSON { char name[64]; int no; }; int cmp_no(const void *ptr1, const void *ptr2); int cmp_name(const void *ptr1, const void *ptr2); int main(void) { struct PERSON persons[10] = { {"Kazim Tanis", 654}, {"Hasan Aslan", 145}, {"Kamil Zorlu", 487}, {"Necati Ergin", 534}, {"Ali Serce", 931}, {"Guray Sonmez", 245}, {"Fehmi Oz", 543}, {"Ayse Er", 321}, {"Sibel Cetinsoy", 423}, {"Mualla Yilmaz", 739} }; qsort(persons, 10, sizeof(struct PERSON), cmp_no); for (size_t i = 0; i < 10; ++i) printf("%-20s%d\n", persons[i].name, persons[i].no); printf("-------------------------------------------\n"); qsort(persons, 10, sizeof(struct PERSON), cmp_name); for (size_t i = 0; i < 10; ++i) printf("%-20s%d\n", persons[i].name, persons[i].no); return 0; } int cmp_no(const void *ptr1, const void *ptr2) { const struct PERSON *per1 = (const struct PERSON *)ptr1; const struct PERSON *per2 = (const struct PERSON *)ptr2; if (per1->no > per2->no) return 1; if (per1->no < per2->no) return -1; return 0; } int cmp_name(const void *ptr1, const void *ptr2) { const struct PERSON *per1 = (const struct PERSON *)ptr1; const struct PERSON *per2 = (const struct PERSON *)ptr2; return strcmp(per1->name, per2->name); } Biz bu örnekte küçükten büyüğe sıralama yaptık. eğer büyükten küçüğe sıralama yapılmak istenirse bu durumda karşılaştırma fonksiyonu ters yazılmalıdır. Yani birinci parametre ikinci parametreden büyükse fonksiyon negatif herhangi bir değere, küçükse pozitif herhangi bir değere ve eşitse sıfır değerine geri döndürülmelidir. Tabii karşılatırma fonksiyonu uzunsa ya da bizim tarafımızdan yazılmadıysa karşılatırma fonksiyonunu sarmalayan bir fonksiyon yazıp onun negatifi ile geri dönülebilir. * Örnek 4.2.0, Bir gösterici dizisini qsort fonksiyonuyla sıraya dizerken aslında göstericilerin adresleriyle karşılaştırma fonksiyonu çağrılmaktadır. Aşağıdaki örnekte isimlerden oluşan const char * türünden elemanlara sahip olan bir gösterici dizisi sıraya dizilmiştir. Burada aslında sıraya dizilen gösterici dizisindeki adreslerdir. #include #include #include struct PERSON { char name[64]; int no; }; int cmp(const void *ptr1, const void *ptr2); int main(void) { const char *names[10] = { "Kazim Tanis", "Hasan Aslan", "Kamil Zorlu", "Necati Ergin", "Ali Serce", "Guray Sonmez", "Fehmi Oz", "Ayse Er", "Sibel Cetinsoy", "Mualla Yilmaz" }; qsort(names, 10, sizeof(const char *), cmp); for (size_t i = 0; i < 10; ++i) printf("%s\n", names[i]); return 0; } int cmp(const void *ptr1, const void *ptr2) { const char **elem1 = (const char **)ptr1; const char **elem2 = (const char **)ptr2; return strcmp(*elem1, *elem2); } * Örnek 4.2.1, UNIX/Linux sistemlerinde ls -l işleminden elde edilen dosyaların isme göre sıraya dizilerek yazdırılması: #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_PATH 4096 #define BLOCK_SIZE 32 struct lsinfo { struct stat finfo; char *name; }; void exit_sys(const char *msg); const char *get_ls(const struct lsinfo *lsinfo, int hlink_digit, int uname_digit, int gname_digit, int size_digit); int cmp_name(const void *lsinfo1, const void *lsinfo2); int main(int argc, char *argv[]) { DIR *dir; struct dirent *dent; struct lsinfo *lsinfo; int count; char path[MAX_PATH]; struct passwd *pass; struct group *gr; int len; int hlink_digit, uname_digit, gname_digit, size_digit; int i; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((dir = opendir(argv[1])) == NULL) exit_sys("open"); lsinfo = NULL; for (count = 0, errno = 0; (dent = readdir(dir)) != NULL; ++count) { sprintf(path, "%s/%s", argv[1], dent->d_name); if (count % BLOCK_SIZE == 0) if ((lsinfo = realloc(lsinfo, (count + BLOCK_SIZE) * sizeof(struct lsinfo))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if ((lsinfo[count].name = (char *)malloc(strlen(dent->d_name) + 1)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } strcpy(lsinfo[count].name, dent->d_name); if (stat(path, &lsinfo[count].finfo) == -1) exit_sys("stat"); } if (errno != 0) exit_sys("readdir"); closedir(dir); hlink_digit = uname_digit = gname_digit = size_digit = 0; for (i = 0; i < count; ++i) { len = (int)log10(lsinfo[i].finfo.st_nlink) + 1; if (len > hlink_digit) hlink_digit = len; if ((pass = getpwuid(lsinfo[i].finfo.st_uid)) == NULL) exit_sys("getppuid"); len = (int)strlen(pass->pw_name); if (len > uname_digit) uname_digit = len; if ((gr = getgrgid(lsinfo[i].finfo.st_gid)) == NULL) exit_sys("getgrgid"); len = (int)strlen(gr->gr_name); if (len > gname_digit) gname_digit = len; len = (int)log10(lsinfo[i].finfo.st_size) + 1; if (len > size_digit) size_digit = len; } qsort(lsinfo, count, sizeof(struct lsinfo), cmp_name); for (i = 0; i < count; ++i) printf("%s\n", get_ls(&lsinfo[i], hlink_digit, uname_digit, gname_digit, size_digit)); for (i = 0; i < count; ++i) free(lsinfo[i].name); free(lsinfo); return 0; } const char *get_ls(const struct lsinfo *lsinfo, int hlink_digit, int uname_digit, int gname_digit, int size_digit) { static char buf[4096]; static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH }; struct passwd *pass; struct group *gr; int index = 0; int i; if (S_ISREG(lsinfo->finfo.st_mode)) buf[index] = '-'; else if (S_ISDIR(lsinfo->finfo.st_mode)) buf[index] = 'd'; else if (S_ISCHR(lsinfo->finfo.st_mode)) buf[index] = 'c'; else if (S_ISBLK(lsinfo->finfo.st_mode)) buf[index] = 'b'; else if (S_ISFIFO(lsinfo->finfo.st_mode)) buf[index] = 'p'; else if (S_ISLNK(lsinfo->finfo.st_mode)) buf[index] = 'l'; else if (S_ISSOCK(lsinfo->finfo.st_mode)) buf[index] = 's'; ++index; for (i = 0; i < 9; ++i) buf[index++] = (lsinfo->finfo.st_mode & modes[i]) ? "rwx"[i % 3] : '-'; buf[index] = '\0'; index += sprintf(buf + index, " %*llu", hlink_digit, (unsigned long long)lsinfo->finfo.st_nlink); if ((pass = getpwuid(lsinfo->finfo.st_uid)) == NULL) return NULL; index += sprintf(buf + index, " %-*s", uname_digit, pass->pw_name); if ((gr = getgrgid(lsinfo->finfo.st_gid)) == NULL) return NULL; index += sprintf(buf + index, " %-*s", gname_digit, gr->gr_name); index += sprintf(buf + index, " %*lld", size_digit, (long long)lsinfo->finfo.st_size); index += strftime(buf + index, 100, " %b %e %H:%M", localtime(&lsinfo->finfo.st_mtime)); sprintf(buf + index, " %s", lsinfo->name); return buf; } int istrcmp(const char *s1, const char *s2) { while (tolower(*s1) == tolower(*s2)) { if (*s1 == '\0') return 0; ++s1; ++s2; } return tolower(*s1) - tolower(*s2); } int cmp_name(const void *pv1, const void *pv2) { const struct lsinfo *lsinfo1 = (const struct lsinfo *)pv1; const struct lsinfo *lsinfo2 = (const struct lsinfo *)pv2; return istrcmp(lsinfo1->name, lsinfo2->name); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5.0, Fonksiyon göstericilerine iyi bir alıştırma için scandir fonksiyonu kullanılabilir. scandir bir dizindeki struct dirent bilgilerini elde eden bir POSIX fonksiyonudur. Fonksiyonun tasarımı biraz karışık biçimdedir. Bu karışıklık fonksiyonun rahat kullanılmasını engellemektedir. Fonksiyonun prototipi şöyledir: #include int scandir(const char *dirp, struct dirent ***namelist, int (*filter)(const struct dirent *), int (*compar)(const struct dirent **, const struct dirent **) ); Fonksiyonun birinci parametresi dizin listesi elde edilecek dizin'in yol ifadesini almaktadır. Fonksiyon o dizindeki girişlerin struct dirent bilgilerini malloc ile tahsis ettiği dinamik alana kopyalar ve alanların adreslerini de bir gösterici dizisine yerleştirir. Gösterici dizisinin adresine de bizim ikinci parametreyle verdiğimiz göstericiyi gösteren göstericinin içerisine yerleştirmektedir. Tabii bu gösterici dizisi de dinamik biçimde tahsis edilmiştir. Dolayısıyla bunların free hale getirilmesi programcının sorumluluğundadır. Fonksiyonun üçüncü parametresi filtreleme yapmak için kullanılan callback fonksiyonun adresini almaktadır. scandir her bulduğu giriş için bu fonksiyonu çağırır. Eğer bu fonksiyon sıfır dışı bir değerle geri dönerse o girişi listeye dahil eder. Eğer bu callback fonksiyon sıfır ile geri dönerse o girişi listeye dahil etmez. scandir fonksiyonunun son parametresi girişlerin sıraya dizilmesi için kullanılan karşılaştırma fonksiyonu almaktadır. Gerçi dirent yapısı zaten inode ve giriş isminde oluştuğu için anlamlı olan sıraya dizme giriş ismine göre olacaktır. Giriş ismine göre sıraya dizme işlemi için alpasort isimli hazır bir karşılaştımr fonksiyonu da bulundurulmuştur. Aslında fonksiyonun son iki parametresine NULL adres de geçilebilmektedir. scandir fonksiyonu çaşarı durumunda diziye yerleştirilen giriş sayısı ile başarısızlık durumunda -1 değeri ile geri dönmektedir. Aşağıda scandir fonksiyonunun kullanımına bir örnek verilmiştir. #include #include #include #include #include void exit_sys(const char *msg); int mycallback(const struct dirent *de) { struct stat finfo; char path[4096]; sprintf(path, "/usr/include/%s", de->d_name); if (stat(path, &finfo) == -1) exit_sys("stat"); return finfo.st_size < 1000; } int main(void) { struct dirent **dents; int i, count; if ((count = scandir("/usr/include", &dents, mycallback, NULL)) == -1) exit_sys("scandir"); for (i = 0; i < count; ++i) printf("%s\n", dents[i]->d_name); for (i = 0; i < count; ++i) free(dents[i]); free(dents); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 5.1, #include #include #include #include void exit_sys(const char *msg); int mycallback(const struct dirent *de) { if (tolower(de->d_name[0]) == 'a') return 1; return 0; } int main(void) { struct dirent **dents; int i, count; if ((count = scandir("/usr/include", &dents, mycallback, NULL)) == -1) exit_sys("scandir"); for (i = 0; i < count; ++i) printf("%s\n", dents[i]->d_name); for (i = 0; i < count; ++i) free(dents[i]); free(dents); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 6, Komut satırı (REPL) oluşturan bir örnek. Bu örnekte komut bulunduğunda belirlenen bir fonksiyon çağrılmaktadır. /* myshell.c */ #include #include #include #include #include #define PROMPT "CSD>" #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 #define BUFFER_SIZE 8192 void parse_cmdline(void); void cat_cmd(void); void cp_cmd(void); void rename_proc(void); typedef struct tagCMD { char *cmd_name; void (*cmd_proc)(void); } CMD; char g_cmdline[MAX_CMD_LINE]; char *g_params[MAX_CMD_PARAMS]; int g_nparams; CMD g_cmds[] = { {"cat", cat_cmd}, {"cp", cp_cmd}, {"rename", rename_proc}, {NULL, NULL} }; int main(void) { char *str; int i; for (;;) { printf("%s", PROMPT); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmdline(); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].cmd_name != NULL; ++i) if (!strcmp(g_cmds[i].cmd_name, g_params[0])) { g_cmds[i].cmd_proc(); break; } if (g_cmds[i].cmd_name == NULL) { printf("invalid command: %s\n", g_params[0]); } } return 0; } void parse_cmdline(void) { char *str; g_nparams = 0; for (str = strtok(g_cmdline, " \t"); str != NULL; str = strtok(NULL, " \t")) g_params[g_nparams++] = str; } void cat_cmd(void) { FILE *f; int ch; if (g_nparams != 2) { printf("cat command missing file!..\n"); return; } if ((f = fopen(g_params[1], "r")) == NULL) { printf("file not found or cannot open file: %s\n", g_params[1]); return; } while ((ch = fgetc(f)) != EOF) putchar(ch); if (ferror(f)) printf("cannot read file: %s\n", g_params[1]); fclose(f); } void cp_cmd(void) { int fds, fdd; char buf[BUFFER_SIZE]; ssize_t result; if (g_nparams != 3) { printf("source and destination path must be specified!..\n"); return; } if ((fds = open(g_params[1], O_RDONLY)) == -1) { printf("file not found or cannot open: %s\n", g_params[1]); return; } if ((fdd = open(g_params[2], O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) { printf("file not found or cannot open: %s\n", g_params[2]); goto EXIT2; } while ((result = read(fds, buf, BUFFER_SIZE)) > 0) if (write(fdd, buf, result) != result) { printf("cannot write file: %s\n", g_params[2]); goto EXIT1; } if (result == -1) { printf("cannot read file: %s\n", g_params[1]); goto EXIT1; } printf("1 file copied...\n"); EXIT1: close(fdd); EXIT2: close(fds); } void rename_proc(void) { printf("rename command...\n"); } >> Bir gösterici dizisinin ismi onun ilk elemanının adresi olacağına göre göstericiyi gösteren göstericiye atanmalıdır. * Örnek 1, int x = 10, y = 20, z = 30; int *a[] = {&x, &y, &z}; int **ppi; ppi = a; /* geçerli */ * Örnek 2, #include #include int main(void) { const char *names[] = {"ali", "veli", "selami", "ayse", "fatma", NULL}; const char **pps; pps = names; for (int i = 0; pps[i] != NULL; ++i) puts(pps[i]); return 0; } >> C'de parametre parantezi içerisindeki dizisel gösterim tamamen "gösterici" anlamına gelmektedir. Köşeli parantezlerin içerisindeki sayıların da hiçbir önemi yoktur. Dolayısıyla aşağıdaki prototiplerin hepsi aynıdır: void foo(int *a); void foo(int a[]); void foo(int a[100]); void foo(int a[5]); Bunların hepsi void foo(int *) anlamına gelmektedir. >> Fonksiyonun parametresi göstericiyi gösteren gösterici ise o parametre yine dizi sentaksıyla belirtilebilir. Dolayısıyla aşağıdaki prototipler de yine eşdeğerdir: void bar(int **a); void bar(int *a[10]); void bar(int *a[]); Bunların hepsi void bar(int **) anlamına gelmektedir. Bu durumda örneğin aslında main fonksiyonunun ikinci parametresi de şöyle belirtilebilir: int main(int argc, char **argv) { ... } Ancak geleneksel olarak programcılar bu ikinci parametreyi aşağıdaki gibi belirtmektedir: int main(int argc, char *argv[]) { ... } Ancak bir fonksiyonun parametresi sanki çok boyutlu diziymiş gibi belirtilirse bu tamamen başka bir anlama gelemketdir. Örneğin: void foo(int a[][3]) { ... } Bu bildirmin ne anlamı geldiği izleyen paragraflarda açıklanacaktır. Aşağıda bu kullanıma ilişkin bir örnek verilmiştir: * Örnek 1, #include void foo(int *a[], size_t size) { for (size_t i = 0; i < size; ++i) printf("%d\n", *a[i]); } int main(void) { int x = 10, y = 20, z = 30; int *a[] = {&x, &y, &z}; foo(a, 3); return 0; } >> Fonksiyon parametresi fonksiyon göstericisi ise o da alternatif biçimde fonksiyon sentaksıyla belirtilebilmektedir. Aşağıdaki iki prototip eşdeğerdir. Örneğin: void foo(int (*a)(double)); void foo(int a(double)); Tabii prototipte değişken ismi belirtilmek zorunda olmadığına göre aşağıdaki prototip de yukarıdakilerle eşdeğerdir: void foo(int (double)); > C'de Çok Boyutlu Diziler: Çok boyutlu diziler aslında C'de dizi dizileri olarak düşünülmelidir. Öneğin: int a[3][2]; Burada a aslında 3 elemanlı bir dizidir. Ancak dizinin her elemanı "int[2] türünden yani 2 elemanlı bir int dizidir. İlk köşeli parantez her zaman asıl dizinin uzunluğunu belirtmektedir. Diğer köşeli parantezler eleman olan dizinin türü ile ilgilidir. Bir dizinin ismi dizinin ilk elemanın adresini belirttiğine göre yukarıdaki iki boyutlu dizide a ifadesi aslında iki elemanlı bir int dizinin adresini belirtir. Biz C'de dizilerin adreslerini alabiliriz. Bu durumda elde edilen adres dizi türünden adres olur. Dizi türünden adresler sembolik olarak "tür (*)[uzunuk]" biçiminde ifade edilir. Dizi türünden adresleri tutan göstericilere "dizi göstericileri (pointer to array)" denilmektedir. Örneğin: int a[2]; int (*pa)[2]; pa = &a; Mademki bir dizinin ismi o dizinin ilk elemanın adresini belirtmektedir. O halde aşağıdaki iki boyutlu dizinin ismi hangi türden adres belirtir? int a[3][2]; Burada a 3 elemanlı, her elemanı 2 elemanlı birint dizi olan bir dizidir. Yani a dizisinin türü int[2] biçimindedir. O halde a ifadesi de aslında int (*)[2] türündendir. Biz a adresini aynı türden bir dizi göstericisine yerleştirebiliriz. Örneğin: int a[3][2]; int(*pa)[2]; pa = a; C'de Çok Boyutlu Diziler ile çalışırken şu noktalara da dikkat etmeliyiz: >> Dizi göstericileri bildirilirken ilk boyut dışındaki boyut uzunlukları belirtilemk zorundadır. Örneğin: int (*pa)[]; /* geçersiz! */ >> Bir dizinin adresini, aynı uzunluğa ilişkin bir dizi göstericisine atayamayız. Örneğin: int a[3]; int (*pa)[2]; pa = &a; /* geçersiz! */ Burada pa göstericisinin aşağıdaki gibi tanımlanması gerekirdi: int (*pa)[3]; >> Çok boyutlu dizilerin adresleri de benzer biçimde aynı türden bir dizi göstericisine atanabilir. Örneğin: int a[2][3][4]; int (*pa)[3][4]; pa = a; /* geçerli */ >> C'de çok yapılan bir hata bir matrisin adresinin gösterici göstericiye atanmaya çalışılmasıdır. Örneğin: void foo(int **ppi) { ... } ... int a[3][2]; ... foo(a); /* geçersiz! */ Burada foo fonksiyonunun parametresinin aşağıdaki gibi olması gerekirdi: void foo(int (*pa)[2]) { ... } ... int a[3][2]; ... foo(a); /* geçerli ! */ Maalesef C'de her uzunlukta çok boyutlu dizinin adresinin atanabileceği gösterici oluşturmak mümkün değildir. Bu tür durumlarda ne yapılması gerektiği izleyen paragraflarda açıklanmaktadır. >> Bir dizi göstericisinin gösterdiği yere erişirsek o dizinin tamamına erişmiş oluruz. Örneğin: int a[3][2]; int (*pa)[2]; pa = a; Burada pa aslında matrisin ilk elemanı olan iki elemanlı int dizinin tamamını göstermektedir. Dolayısıyla *pa ifadesi ya da pa[0] ifadesi aslında 2 elemanlıbir int diziyi temsil eder. Burada pa[1] ifadesi bu matrisin ikinci satırını oluşturan 2 elemanlı int diziyi temsil etmektedir. >> Dizi isimler dizinin tamamını temsil etmektedir. Ancak C'de dizi isimleri ifade içerisinde kullanıldığında aslında dizinin ilk elemanının adresi anlamına gelmektedir. Dizi isimleri bu nedenle nesne belirtmez. Dizi isimleri C'de adeta bir sembolik sabit gibi düşünülmelidir. Örneğin: int a[3] = {1, 2, 3}; a = 100; /* a nesne belirtmiyor, bir adres sabiti belirtiyor */ Bu durumda örneğin: int a[3][2]; burada a[i] ifadesi de *a ifadesi de nesne belirtmemektedir. Bu ifadeler adeta dizi isimleri gibi düşünülmelidir. >> Biz C'de çok boyutlu dizilerin elemanlarına birden fazla [...] ile erişiriz. Bunun neden böyle olduğu açıktır. Örneğin: int a[3][2] = {{1, 2}, {3, 4}, {5, 6}}; Burada a[i] aslında a matrisinin i'inci satırındaki diziyi belirtmektedir. O halde a[i][k] ifadesi de aslında a matrisinin i'inci satırındaki dizinin k'ıncı sütunundaki eleman anlamına gelmektedir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include int main(void) { int a[3][2] = { {1, 2}, {3, 4}, {5, 6} }; int(*pa)[2]; pa = a; printf("%d\n", (*pa)[0]); printf("%d\n", (*pa)[1]); printf("%d\n", pa[1][0]); printf("%d\n", pa[1][1]); printf("%d\n", pa[2][0]); printf("%d\n", pa[2][1]); return 0; } >> Pekiyi bir matrisi fonksiyona partametre olarak nasıl geçirebiliriz? İşte fonksiyonun parametre değişkeninin bir dizi göstricisi olması gerekir. Ayrıca yukarıda da belirtitğimiz gibi matrisin satır uzunluğunun da fonksiyona aktarılması uygun olur. Ne de olsa biz dizilerin uzunluklarını da fonksiyona aktarmaktayız. Örneğin: void foo(int (*pa)[2], size_t size); Bu fonksiyon sütun sayısı 2 olan ancak herhangi miktarda satıra sahip olan iki boyutlu diziler üzerinde işlem yapabilme potansiyelindedir. * Örnek 1, #include void foo(int(*pa)[2], size_t size) { size_t i, k; for (i = 0; i < size; ++i) { for (k = 0; k < 2; ++k) printf("%d ", pa[i][k]); printf("\n"); } } int main(void) { int a[3][2] = { {1, 2}, {3, 4}, {5, 6} }; foo(a, 3); return 0; } >> Çok boyutlu dizi kavramı aslında yapay bir kavramdır. Çünkü bellek çok boyutlu değildir tek boyutludur. Dolayısıyla çok boyutlu dizileri derleyici aslında tek boyutlu dizilermiş gibi bellekte tutmaktadır. C standartları çok boyutlu dizilerin tüm elemanlarının ardışıl olduğunu ve bu ardışıllığın satır tabanlı biçimde olduğunu belirtmektedir. Örneğin: int a[3][2] = { {1, 2}, {3, 4}, {5, 6} }; Buradaki a dizisi aslında iki elemanlı int dizilerin dizisi olduğuna göre elemanların bellekteki yerleşimi aşağıdaki olacaktır: 1 2 3 4 5 6 Örneğin: int a[2][2][3] = { { {1, 2, 3}, {4, 5, 6} }, { {7, 8, 9}, {10, 11, 12} } }; Bu dizinin bellekteki yerleşimi ise şöyle olacaktır: 1 2 3 4 5 6 7 8 9 10 11 12 Bu durumda çok boyutlu bir dizinin ismi tek boyutlu bir göstericiye atanıp çok boyutlu dizinin bütün elemanlarına ulaşılabilir. >> Tek boyutlu bir dizi de bir dizi göstericisine tür dönüştürme operatörü kullanılarak dönüştürülebilir. Bundan sonra dizi elemanlarına matris sentaksıyla erişilebilir. * Örnek 1, int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; int (*pa)[3]; pa = (int (*)[3]) a; for (int i = 0; i < 4; ++i) { for (int k = 0; k < 3; ++k) printf("%d ", pa[i][k]); putchar('\n'); } * Örnek 2, #include int main(void) { int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; int (*pa)[3]; pa = (int (*)[3]) a; for (int i = 0; i < 4; ++i) { for (int k = 0; k < 3; ++k) printf("%d ", pa[i][k]); putchar('\n'); } return 0; } >> Bir matiris için de dinamik tahsisatlar yapabiliriz. Tabii buradaki önemli nokta tahsis edilen alanın başlangıç adresinin atanacağı dizi göstericisinin nasıl tanımlanacağıdır. Örneğin biz 4x3'lik bir matris için malloc fonksiyonu ile tahsisat yapmak isteyelim. Bu durumda tahsis edilen alanın adresi aşağıdaki gibi bir dizi göstericisine atanbilir: int (*pa)[3]; Örneğin: int(*pa)[3]; if ((pa = (int(*)[3])malloc(4 * 3 * sizeof(int))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } Aşağıda bu kullanıma ilişkin bir örnek verilmiştir: * Örnek 1, #include #include int main(void) { int(*pa)[3]; if ((pa = (int(*)[3])malloc(4 * 3 * sizeof(int))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 4; ++i) for (int k = 0; k < 3; ++k) pa[i][k] = i + k; for (int i = 0; i < 4; ++i) { for (int k = 0; k < 3; ++k) printf("%d ", pa[i][k]); putchar('\n'); } free(pa); return 0; } >> Fonksiyon göstericileri konusunda da belirttiğimiz belli bir türden her uzunluktaki matrisi (çok boyutlu diziyi) parametre olarak alabilecek genel bir fonksiyon yazılamamaktadır. Bu tür genel fonksiyonları yazmanın en pratik yolu onları tek boyutluymuş gibi düşünmektir. Örneğin iki boyutlu int türden bir matrisi parametre olarak alan bir fonksiyonun parametrik yapısı şöyle olabilir: void foo(int *pi, size_t rowsize, size_t colsize); Fonksiyon içerisinde bu matrisin i'inci satır k'ıncı sütun elemanlarına pi[i * colsize + k] ifadesiyle erişebiliriz. Aslında bu erişim doğal matris erişiminde daha yavaş değildir. Çünkü matrisler zaten doğal türler değildir. Yani bir matrisin elemanına a[i][k] biçiminde eriştiğimizde de zaten derleyici aynı işlemleri yapmaktadır. Tabii bu fonksiyonu çağırırken tür dönüştürmesi yapmak gerekir. Örneğin: int a[3][2] = { {1, 2}, {3, 4}, {5, 6} }; ... foo(a, 3, 2); /* geçersiz! */ foo((int *)a, 3, 2); /* geçerli Tabii tür dönüştürmesi yapılmak istenmiyorsa fonksiyonun parametresi void gösterici alınıp tür dönüştürmesi fonksiyonun içinde de yapılabilir. Örneğin: void foo(void *pv, size_t rowsize, size_t colsize) { int *pi = (int *)pv; ... } Artık tür dönüştürmesine gerek yoktur: foo(a, 3, 2); Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include void disp_matrix(void *pv, size_t rowsize, size_t colsize) { int *pi = (int *)pv; for (size_t i = 0; i < rowsize; ++i) { for (size_t k = 0; k < colsize; ++k) printf("%d ", pi[i * colsize + k]); putchar('\n'); } } int main(void) { int a[3][2] = {{1, 2}, {3, 4}, {5, 6}}; disp_matrix(a, 3, 2); return 0; } >> Her elemanı bir dizi göstericisi olan bir dizi de oluşturulabilir. Örneğin: int (*a[10])[2]; Burada a 10 elemanlı bir dizidir. Ancak bu dizinin her elemanı int (*)[2] türünden yani sütun uzunluğu 2 olan matrisleri gösteren dizi göstericisidir. >> Fonksiyonun parametre parantezi içerisinde çok boyutlu dizi sentaksı dizi göstericisi anlamına gelmektedir. Örneğin: void foo(int a[][3]) { ... } Bu tanımlama tamamen aşağıdakiyle eşdeğerdir: void foo(int (*a)[3]) { ... } Tabii dizisel gösterimde ikinci koşeli parantezin (çok boyutlu dizilerde ilki haricindeki tüm köşeli parantezlerin) içinde uzunluk belirten bir sabit ifadesinin bulunuyor olması gerekir. Ancak ilk köşeli parantezin içi boş olabilir. İlk köşeli parantezin içine yazılacak uzunluğun hiçbir önemi yoktur. Örneğin aşağıdaki prototipler tamamen eşdeğerdir: void foo(int a[][3]); void foo(int a[10][3]); void foo(int (*a)[3]); Aşağıda buna ilişkin bir örnek verilmiştir: * Örnek 1, void foo(int a[][5]) /* int (*a)[5] */ { /* ... */ } int main(void) { int a[5]; foo(&a); return 0; } >> Fonksiyonların geri dönüş değerleri de dizi türünden adresler olabilir. Bu durumda dekleratörde yine * atomu paranteze içerisine alınır. Parantezin soluna geri dönüş değerine ilişkin dizinin türü, sağında geri dönüş değerine ilişkin didizinin uzunluğu yazılır. Örneğin: int (*foo(int a))[3] { ... } Burada foo fonksiyonunun parametresi int türdendir. Ancak geri dönüş değeri 3 elemanlı bir dizinin adresidir. * Örnek 1, #include int(*foo(void))[5] { static int a[3] = {1, 2, 3, 4, 5}; return &a; } int main(void) { int(*pa)[5]; int i; pa = foo(); for (i = 0; i < 5; ++i) printf("%d ", (*pa)[i]); printf("\n"); return 0; } > Hatırlatıcı Notlar: >> Bir bildirim işleminin C'deki genel biçimi şöyledir: [yer belirleyicisi] [tür niteleyicileri] [= [= Proseslerin Çalışma Dizinleri: İşletim sistemlerinde bir dosyanın yerini belirten yazısal ifadelere "yol ifadeleri (path names)" denilmektedir. Yol ifadelerinde dizin geçişleri kullanılabilir. Windows sistemlerinde dizin geçişleri için "\" karakteri UNIX/Linux ve macOS sistemlerinde ise "/" karakteri kullanılmaktadır. Bir yol ifadesindeki "/" ya da "\" arasındaki her bir dizin girişine "yol ifadesi bileşeni (pathname component)" denilmektedir. Örneğin: "/home/kaan/Study/test.txt" Burada "home", "kaan", "Study", "test.txt" birer yol ifadesi girişidir. Windows sistemlerinde "sürücü (drive)" kavramı da vardır. Bu sistemlere her sürücünün ayrı bir kökü bulunmaktadır. Dolayısıyla yol ifadelerinde sürücüler de belirtilebilmektedir. Örneğin: "F:\Dropbox\Study\test.txt" Halbuki UNIX/Linux sistemlerinde ve macOS sistemlerinde sürücü kavramı yoktur. Bu sistemlerde tek bir kök vardır. Başka aygıtlar bu kökte herhangi bir yere monte edilirler. Bu işleme İngilizce "mount" denilmektedir. Mount işleminin yapıldığı dizin artık mount edilen aygıtın kök dizini gibi işlem görmektedir. Yol ifadeleri "mutlak (absolute)" ve "göreli (relative)" olmak üzere ikiye ayrılmaktadır. Eğer bir yol ifadesinin ilk karakteri "/" ya da "\" ise bu tür yol ifadelerine mutlak yol ifadeleri denilmektedir. Örneğin: "/home/kaan/Study/test.txt" Bu yol ifadesi UNIX/Linux sistemlerinde mutlak bir yol ifadesidir. Örneğin: "\Windows\notepad.exe" Bu yol ifadesi de Windows sistemleri için mutlak bir yol ifadesidir. Mutlak yol ifadeleri her zaman kök dizinden itibaren yer belirtirler. Eğer yol ifadelerindeki ilk karakter "/" ya da "\" değilse böyle yol ifadelerine de "göreli yol ifadeleri" denilmektedir. Örneğin: "Study/C/test.txt" "Doc\Temp\test.txt" "samle.c" Bu yol ifadeleri görelidir. İşletim sistemleri her proses için prosesin kontrol bloğu içerisinde ismine "prosesin çalışma dizini (process current working directory)" denilen bir dizin tutmaktadır. İşte göreli yol ifadeleri prosesin çalışma dizini orijin yapılarak çözülmektedir. Yani prosesin çalışma dizini göreli yol ifadelerinin nereden itibaren yol ifadesi belirttiğini göstermektedir. Pekiyi prosesin çalışma dizini proses yaratıldığında hangi dizin olarak set edilmiştir ve program çalışırken değiştirilebilir mi? İşte UNIX/Linux sistemlerinde bir proses yaratıldığında (yani bir program çalıştırıldığında) onun çalışma dizini üst prosesten (yani onu çalıştıran prosesten) alınmaktadır. Örneğin "sample" programının çalışma dizini "/home/kaan" olsun. "sample" programı da "mample" programını çalıştırmış olsun. O halde işin başında "mample" programının çalışma dizini de "home/kaan" olacaktır. Windows sistemlerinde de bir proses yaratıldığında yeni prosesin çalışma dizini ya onu yaratan prosesin çalışma dizini olarak üst prosesten alınır ya da istenilen bir dizin olarak set edilir. Tabii Windows sistemlerinde prosesin çalışma dizini sürücü bilgisini de içermektedir. Windows sistemlerinde bir yol ifadesinde sürücü bilgisi de varsa buna "tam yol ifadesi (full path)" denilmektedir. Örneğin: "C:\Windows\temp\x.txt" Pekiyi bu sistemlerde mutlak bir yol ifadesi sürücü içermezse default sürücü ne olacaktır? Örneğin: "\temp\test.txt" Bu mutlak ifadesi hangi sürücünün kök dizininden itibaren yer belirtmektedir? İşte Windows sistemlerinde sürücüsü belirtilmemiş olan yol mutlak yol ifadeleri prosesin çalışma dizini hangi sürücüye ilişkinse o sürücüde yer beelirtmektedir. Örneğin prosesimizin çalışma dizini "F:\Dropbox"ise yukarıdaki yol ifadesi F sürücüsüün kökünden itibaren yer belirtmektedir. Eğer prosesimizin çalışma dizini örneğin "D:\Ali\Study" olsaydı yukarıdaki yol ifadesi D dizinin kökünden itibaren yer belirtecekti. Windows sistemlerinde bir yol ifadesinde sürücü varsa ancak yoli fadesi göreli ise bu durumda proseste bazı özel çevre değişkenlerine bakılmaktadır. Eğer bu çevre değişkenleri yoksa bu duurmda ilgili sürücünün kök dizini orijin kabul edilmektedir. Örneğin: "D:Ali\test.txt" Burada yol ifadesinde sürücü belirtilmiştir. Ancak yol ifadesi görelidir. İşte bu durumda orijin noktası için bazı çevre değişkenlerine bakılır. Ancak bu çevre değişkenleri yoksa bu yol ifadesi "D:\Ali\test.txt" ile eşdeğer kabul edilir. Tabii programcının böylesi yol ifadelerinden kaçınması daha uygun olur. Proseslerin Çalışma Dizinlerinin elde edilmesi ve değiştirilmesi için bir takım fonksiyonlar kullanılır. Bu fonksiyonlar Windows sistemleri için GetCurrentDirectory SetCurrentDirectory isimli fonksiyonlarken, UNIX/Linux Sistemleri için getcwd chdir fonksiyonlar kullanılır. Şimdi de bu fonksiyonları sırasıyla inceleyelim: >> Windows API : >>> "GetCurrentDirectory" : Windows sistemlerinde prosesin çalışma dizini (current working directory) GetCurrentDirectory API fonksiyonuyla elde edilmektedir. Fonksiyonun prototipi şöyledir: DWORD GetCurrentDirectory( DWORD nBufferLength, LPTSTR lpBuffer ); Fonksiyonun ikinci parametresi prosesin çalışma dizininin yerleştirileceği char türden dizinin başlangıç adresini almaktadır. Birinci parametre bu dizinin uzunluğunu belirtmektedir. Fonksiyon başarısızlık durumunda 0 değerine geri dönmektedir. Ancak programcının fonksiyona verdiği dizinin uzunluğu yetersiz kalırsa fonksiyon diziye yerleştirme yapmaz fakat geri dönüş değeri olarak null karakter dahil olmak üzere gereken dizi uzunluğunu verir. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısına geri dönmektedir. Ancak başarı durumundaki bu karakter sayısına null karakter dahil değildir. Eğer fonksiyonunun birinci parametresi 0 ve ikinci parametresi NULL adres geçilirse fonksiyon prosesin çalışma dizininin yerleştirilmesi için gereken karakter sayısını (null karakter dahil olmak üzere) bize vermektedir. Windows sistemlerinde bir yol ifadesinin maksimum uzunluğu MAX_PATH değeri ile önceden belirlenmiştir. Bu tür durumlarda dizi uzunluklarını MAX_PATH kadar açınız. MAX_PATH mevcut Windows sistemlerinde 260 olarak define edilmiştir. GetCurrentDirectory fonksiyonunun başarısı aşağıdaki gibi kontrol edilebilir: char cwd[BUFFER_SIZE]; DWORD dwResult; if (!(dwResult = GetCurrentDirectory(BUFFER_SIZE, cwd))) ExitSys("GetCurrentDirectory"); if (dwResult > BUFFER_SIZE) { fprintf(stderr, "Buffer too small!..\n"); exit(EXIT_FAILURE); } Aşağıdaki örnekte prosesin çalışma dizini elde edilip yazdırılmıştır. Burada MAX_PATH değeri kullanıldığı için alanın yeterli büyüklükte olup olmadığı ayrıca kontrol edilmemiştir. * Örnek 1, #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char cwd[MAX_PATH]; if (!GetCurrentDirectory(MAX_PATH, cwd)) ExitSys("GetCurrentDirectory"); puts(cwd); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, GetCurrentDirectory fonksiyonunda birinci parametre 0, ikinci parametre NULL geçilirse çalışma dizini için gereken karakter uzunluğu (byte değil) elde edilir. Aşağıdaki örnekte bu yöntem kullanılmıştır. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char *cwd; DWORD dwBufSize; if (!(dwBufSize = GetCurrentDirectory(0, NULL))) ExitSys("GetCurrentDirectory"); if ((cwd = (char *)malloc(dwBufSize)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if (!GetCurrentDirectory(dwBufSize, cwd)) ExitSys("GetCurrentDirectory"); puts(cwd); free(cwd); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> "SetCurrentDirectory" : Windows sistemlerinde prosesin çalışma dizinini değiştirmek için SetCurrentDirectory isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL SetCurrentDirectory( LPCTSTR lpPathName ); Fonksiyon set edilecek çalışma dizinini parametre olarak alır. Başarı durumunda 0, başarısızlık durumunda sıfır dışı bir değere geri döner. Aşağıdaki örnekte önce prosesin çalışma dizini SetCurrentDirectory API fonksiyonu ile "C:\windows" olarak değiştirilmiştir. Sonra prosesin çalışma dizini yine GetCurrentDirectory API fonksiyonu ile alınıp yazdırılmıştır. * Örnek 1, #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char cwd[MAX_PATH]; DWORD dwResult; if (!SetCurrentDirectory("c:\\windows")) ExitSys("SetCurrentDirectory"); if (!(dwResult = GetCurrentDirectory(MAX_PATH, cwd))) ExitSys("GetCurrentDirectory"); puts(cwd); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Aşağıda ise bu iki fonksiyon kullanılarak oluşturulan basit bir shell uygulaması verilmiştir: * Örnek 1, #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 64 struct CMD { char *cmd_text; void (*proc)(void); }; void ExitSys(LPCSTR lpszMsg); void ParseCmdLine(void); void DirProc(void); void CopyProc(void); void ClsProc(void); void RenameProc(void); void ChangeDirProc(void); void DispDirectory(LPCTSTR lpszPath); char g_cmd_line[MAX_CMD_LINE]; struct CMD g_cmds[] = { {"dir", DirProc}, {"copy", CopyProc}, {"cls", ClsProc}, {"rename", RenameProc}, {"cd", ChangeDirProc}, {NULL, NULL}, }; char *g_params[MAX_CMD_PARAMS]; int g_nparams; char g_cwd[MAX_PATH]; int main(void) { char *str; int i; if (!GetCurrentDirectory(MAX_PATH, g_cwd)) ExitSys("GetCurrentDirectory"); for (;;) { printf("%s>", g_cwd); fgets(g_cmd_line, MAX_CMD_LINE, stdin); if ((str = strchr(g_cmd_line, '\n')) != NULL) *str = '\0'; ParseCmdLine(); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].cmd_text != NULL; ++i) if (!strcmp(g_cmds[i].cmd_text, g_params[0])) { g_cmds[i].proc(); break; } if (g_cmds[i].cmd_text == NULL) printf("command not found: %s\n\n", g_params[0]); } return 0; } void ParseCmdLine(void) { char *str; g_nparams = 0; for (str = strtok(g_cmd_line, " \t"); str != NULL; str = strtok(NULL, " \t")) g_params[g_nparams++] = str; g_params[g_nparams] = NULL; } void DirProc(void) { if (g_nparams > 2) { printf("too many arguments!..\n"); return; } DispDirectory(g_nparams == 1 ? g_cwd : g_params[1]); } void CopyProc(void) { if (g_nparams != 3) { printf("argument too few or too many!..\n\n"); return; } } void ClsProc(void) { if (g_nparams != 1) { printf("too many arguments!\n\n"); return; } printf("cls command\n"); } void RenameProc(void) { printf("rename command\n"); } void ChangeDirProc(void) { if (g_nparams > 2) { printf("too many arguments!..\n\n"); return; } if (g_nparams == 1) { printf("%s\n\n", g_cwd); return; } if (!SetCurrentDirectory(g_params[1])) { printf("directory not found or cannot change: %s\n\n", g_params[1]); return; } if (!GetCurrentDirectory(MAX_PATH, g_cwd)) ExitSys("GetCurrentDirectory"); } void DispDirectory(LPCTSTR lpszPath) { WIN32_FIND_DATA wfd; char lpszDirPath[MAX_PATH]; HANDLE hFF; sprintf(lpszDirPath, "%s/*.*", lpszPath); if ((hFF = FindFirstFile(lpszDirPath, &wfd)) == INVALID_HANDLE_VALUE) { printf("directory not found or cannot display!\n\n"); return; } do { if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) printf("%-10s", ""); else printf("%-10lu", wfd.nFileSizeLow); printf("%s\n", wfd.cFileName); } while (FindNextFile(hFF, &wfd)); printf("\n"); } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } >> UNIX/Linux : >>> "getcwd" : UNIX/Linux sistemlerinde prosesin çalışma dizini getcwd isimli POSIX fonksiyonuyla elde edilmektedir. Fonksiyonun prototipi şöyledir: #include char *getcwd(char *buf, size_t size); Fonksiyonun birinci prosesin çalışma dizininin yerleştirileceği dizinin adresini, ikinci parametresi ise onun null karakter dahil olmak üzere uzunluğunu almaktadır. Eğer yol ifadesi belirtilen uzunluktan null karakter dahil olmak üzere büyükse fonksiyon başarısız olur. Fonksiyon başarı durumunda birinci parametresiyle belirtilen adresin aynısına, başarısızlık durumunda NULL adrese geri dönmektedir. UNIX/Linux sistemlerinde yol ifadelerinin maksimum değerleri içerisinde bildirilmiş olan PATH_MAX isimli bir sembolik sabitle ifade edilmektedir. Ancak bu sembolik sabitin define edilmiş olma zorunluluğu yoktur. Eğer bu sembolik sabit define edilmemişse bu durumda maksimum yol ifadesinin uzunluğu pathconf isimli POSIX fonksiyonuyla elde edilmektedir. Linux'ta PATH_MAX sembolik sabiti 4096 olarak define edilmiştir. Aşağıdaki örnekte prosesin çalışma dizini getcwd fonksiyonuyla alınp stdout dosyasına yazdırılmıştır. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { char cwd[4096]; if (getcwd(cwd, 4096) == NULL) exit_sys("getcwd"); puts(cwd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "chdir" : UNIX/Linux sistemlerinde proseslerin çalışma dizinini değiştirmek için chdir isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int chdir(const char *path); Fonksiyon çalışma dizini yapılacak dizin'in yol ifadesini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. Aşağıdaki örnekte önce prosesin çalışma dizini alınarak ekrana (stdout dosyasına) yazdırılmıştır. Sonra çalışma değiştirilip yeniden elde edilip yazdırılmıştır. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { char cwd[4096]; if (getcwd(cwd, 4096) == NULL) exit_sys("getcwd"); puts(cwd); if (chdir("/usr/include") == -1) exit_sys("getcwd"); if (getcwd(cwd, 4096) == NULL) exit_sys("getcwd"); puts(cwd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Aşağıdaki örnekte daha önce UNIX/Linux sistemleri için yapmış olduğumuz "myshell" programına pwd, cd, ls ve clear komutları eklenmiştir. Programda yine myshell programı kendi çalışma dizininin prompt olarak ekran basmaktadır. cd komutu da çalışma dizinini değiştirmektedir. Normal olarak UNIX/Linux sistemlerinde cd komutu argüman almazsa çalışma dizinini "home dizin" olarak değiştirmektedir. Ancak aşağıdaki örnekte biz bu özelliği sağlamıyoruz. * Örnek 1, /* myshell.c */ #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 #define BUFFER_SIZE 8192 #define MAX_PATH_SIZE 4096 void parse_cmdline(void); void cat_cmd(void); void cp_cmd(void); void rename_proc(void); void pwd_proc(void); void cd_proc(void); void ls_proc(void); void clear_proc(void); const char *get_ls(const char *path, int hlink_digit, int uname_digit, int gname_digit, int size_digit); void exit_sys(const char *msg); typedef struct tagCMD { char *cmd_name; void (*cmd_proc)(void); } CMD; char g_cmdline[MAX_CMD_LINE]; char *g_params[MAX_CMD_PARAMS]; int g_nparams; CMD g_cmds[] = { {"cat", cat_cmd}, {"cp", cp_cmd}, {"rename", rename_proc}, {"pwd", pwd_proc}, {"cd", cd_proc}, {"ls", ls_proc}, {"clear", clear_proc}, {NULL, NULL} }; char g_cwd[MAX_PATH_SIZE]; int main(void) { char *str; int i; if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL) exit_sys("getcwd"); for (;;) { printf("CSD:%s>", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmdline(); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].cmd_name != NULL; ++i) if (!strcmp(g_cmds[i].cmd_name, g_params[0])) { g_cmds[i].cmd_proc(); break; } if (g_cmds[i].cmd_name == NULL) { printf("invalid command: %s\n", g_params[0]); } } return 0; } void parse_cmdline(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 cat_cmd(void) { FILE *f; int ch; if (g_nparams != 2) { printf("cat command missing file!..\n"); return; } if ((f = fopen(g_params[1], "r")) == NULL) { printf("file not found or cannot open file: %s\n", g_params[1]); return; } while ((ch = fgetc(f)) != EOF) putchar(ch); if (ferror(f)) printf("cannot read file: %s\n", g_params[1]); fclose(f); } void cp_cmd(void) { int fds, fdd; char buf[BUFFER_SIZE]; ssize_t result; if (g_nparams != 3) { printf("source and destination path must be specified!..\n"); return; } if ((fds = open(g_params[1], O_RDONLY)) == -1) { printf("file not found or cannot open: %s\n", g_params[1]); return; } if ((fdd = open(g_params[2], O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) { printf("file not found or cannot open: %s\n", g_params[2]); goto EXIT2; } while ((result = read(fds, buf, BUFFER_SIZE)) > 0) if (write(fdd, buf, result) != result) { printf("cannot write file: %s\n", g_params[2]); goto EXIT1; } if (result == -1) { printf("cannot read file: %s\n", g_params[1]); goto EXIT1; } printf("1 file copied...\n"); EXIT1: close(fdd); EXIT2: close(fds); } void rename_proc(void) { printf("rename command...\n"); } void pwd_proc(void) { puts(g_cwd); } void cd_proc(void) { if (g_nparams == 1) { printf("argument missing!..\n"); return; } if (g_nparams > 2) { printf("too many arguments!..\n"); return; } if (chdir(g_params[1]) == -1) { printf("%s: %s!..\n", g_params[1], strerror(errno)); return; } if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL) exit_sys("getcwd"); } #include void ls_proc(void) { int result; int l_flag; int hlink_digit, uname_digit, gname_digit, size_digit; DIR *dir; struct dirent *dent; char path[PATH_MAX]; struct stat finfo; int len; struct passwd *pass; struct group *gr; char *target_path; l_flag = 0; opterr = 0; optind = 0; while ((result = getopt(g_nparams, g_params, "l")) != -1) { switch (result) { case 'l': l_flag = 1; break; case '?': printf("invalid option: -%c\n", optopt); break; } } if (g_nparams - optind > 1) { printf("too many arguments!..\n"); return; } target_path = optind == g_nparams ? "." : g_params[optind]; if ((dir = opendir(target_path)) == NULL) { printf("%s: %s\n", target_path, strerror(errno)); return; } hlink_digit = uname_digit = gname_digit = size_digit = 0; while (errno = 0, (dent = readdir(dir)) != NULL) { snprintf(path, PATH_MAX, "%s/%s", target_path, dent->d_name); if (stat(path, &finfo) == -1) { printf("%s: %s\n", path, strerror(errno)); continue; } len = (int)log10(finfo.st_nlink) + 1; if (len > hlink_digit) hlink_digit = len; if ((pass = getpwuid(finfo.st_uid)) == NULL) exit_sys("getppuid"); len = (int)strlen(pass->pw_name); if (len > uname_digit) uname_digit = len; if ((gr = getgrgid(finfo.st_gid)) == NULL) exit_sys("getgrgid"); len = (int)strlen(gr->gr_name); if (len > gname_digit) gname_digit = len; len = (int)log10(finfo.st_size) + 1; if (len > size_digit) size_digit = len; } if (errno != 0) exit_sys("readdir"); rewinddir(dir); while (errno = 0, (dent = readdir(dir)) != NULL) { sprintf(path, "%s/%s", target_path, dent->d_name); if (stat(path, &finfo) == -1) { printf("%s: %s\n", path, strerror(errno)); continue; } if (l_flag) printf("%s\n", get_ls(path, hlink_digit, uname_digit, gname_digit, size_digit)); else printf("%s\t", dent->d_name); } if (errno != 0) exit_sys("readdir"); if (!l_flag) putchar('\n'); closedir(dir); } void clear_proc(void) { printf("\033[2J"); printf("\033[0;0f"); } const char *get_ls(const char *path, int hlink_digit, int uname_digit, int gname_digit, int size_digit) { struct stat finfo; static char buf[4096]; static mode_t modes[] = { S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH }; struct passwd *pass; struct group *gr; char *str; int index = 0; int i; if (stat(path, &finfo) == -1) return NULL; if (S_ISREG(finfo.st_mode)) buf[index] = '-'; else if (S_ISDIR(finfo.st_mode)) buf[index] = 'd'; else if (S_ISCHR(finfo.st_mode)) buf[index] = 'c'; else if (S_ISBLK(finfo.st_mode)) buf[index] = 'b'; else if (S_ISFIFO(finfo.st_mode)) buf[index] = 'p'; else if (S_ISLNK(finfo.st_mode)) buf[index] = 'l'; else if (S_ISSOCK(finfo.st_mode)) buf[index] = 's'; ++index; for (i = 0; i < 9; ++i) buf[index++] = (finfo.st_mode & modes[i]) ? "rwx"[i % 3] : '-'; buf[index] = '\0'; index += sprintf(buf + index, " %*llu", hlink_digit, (unsigned long long)finfo.st_nlink); if ((pass = getpwuid(finfo.st_uid)) == NULL) return NULL; index += sprintf(buf + index, " %-*s", uname_digit, pass->pw_name); if ((gr = getgrgid(finfo.st_gid)) == NULL) return NULL; index += sprintf(buf + index, " %-*s", gname_digit, gr->gr_name); index += sprintf(buf + index, " %*lld", size_digit, (long long)finfo.st_size); index += strftime(buf + index, 100, " %b %e %H:%M", localtime(&finfo.st_mtime)); str = strrchr(path, '/'); sprintf(buf + index, " %s", str ? str + 1 : path); return buf; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> Windows sistemlerinde yol ifadesinin maksimum uzunluğu MAX_PATH sembolik sabitiyle belirlenmiştir. Ancak UNIX/Linux sistemlerinde bunun karşılığı olan PATH_MAX sembolik sabiti define edilmemiş olabilir. (Linux sistemlerinde PATH_MAX sembolik sabitinin 4096 olarak define edildiğini anımsayınız.) İşte UNIX/Linux sistemlerinde yol ifadesinin maksimum uzunluğu taşınabilir bir biçimde elde edenbilmek için aşağıdaki gibi bir fonksiyonun yazılması gerekmektedir: long path_max(void) { static long result = 0; #define PATH_MAX_INDETERMINATE_GUESS 4096 #ifdef PATH_MAX result = PATH_MAX; #else if (result == 0) { errno = 0; if ((result = pathconf("/", _PC_PATH_MAX)) == -1 && errno == 0) result = PATH_MAX_INDETERMINATE_GUESS; } #endif return result; } Aşağıdaki örnekte önce yokl ifadesi için gerekli olan alan path_max fonksiyonu ile elde edilmiş sonra da yol ifadesinin yerleştirileceği alan malloc fonksiyonuyla tahsis edilmiştir. * Örnek 1, #include #include #include #include void exit_sys(const char *msg); long path_max(void) { static long result = 0; #define PATH_MAX_INDETERMINATE_GUESS 4096 #ifdef PATH_MAX result = PATH_MAX; #else if (result == 0) { errno = 0; if ((result = pathconf("/", _PC_PATH_MAX)) == -1 && errno == 0) result = PATH_MAX_INDETERMINATE_GUESS; } #endif return result; } int main(void) { char *cwd; long size; size = path_max(); if ((cwd = (char *)malloc(size)) == NULL) exit_sys("malloc"); if (getcwd(cwd, size) == NULL) exit_sys("getcwd"); printf("size: %ld, cwd = %s\n", size, cwd); free(cwd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (26_16_09_2023) & (27_17_09_2023) & (28_23_09_2023) > Recursive Functions : Özyineleme (recursion) doğada da karşımıza çıkan bir kavramdır. Bir olgunun kendisine benzer bir olguyu içermesi anlamına gelmektedir. Örneğin tohumdan ağaç olur, ağaç da yeniden tohum vermektedir. Matruşkalar da özyineleme içermektedir. Programlamada özyineleme bazı özel algoritmalarda karşımıza çıkmaktadır. Biz algoritmalar dünyasında bir problemi çözmek için yola çıktığımızda belli bir yol kat ettikten sonra ilk duruma benzer bir durumla karşılaşırsak muhtemelen özyinelemeli bir algoritmik problem söz konusu olmaktadır. Örneğin bir dizin listesini alt dizinlerle dolaşmak isteyelim. Bunun için kök dizindeki dizin girişlerini elde ederiz. Eğer o dizin girişlerinin biri de bir dizine ilişkinse o dizine geçtiğimizde yine ilk işe başladığımız duruma benzer bir durum içerisinde oluruz. O zaman dizin ağacının dolaşılması özyienelemeli bir algoritma içermektedir. Bir algoritmik problemin özyineleme olmadan çözümüne "iteratif çözüm" de denilmektedir. O halde algoritmalar bu bakımdan üçe ayrılabilir: -> Yalnızca iteratif olarak çözülen algoritmalar -> Hem iteratif hem de özyinemelei biçimde çözülebilen algoritmalar -> Yalnızca özyinelemeli olarak çözülebilen algoritmalar Bazı algoritmaların özyinelemeyle bir bağı yoktur. Bunlar döngüler yoluyla klasik yöntemlerle çzöülmektedir. Ancak bazı algoritmalar hem özyineleme ile hem de klasik iteratif yöntemlerle çözülebilmektedir. Ancak bazı algoritmaların iteratif çözümü ya yoktur ya da makul değildir. Özyinelemeli karaktere sahip olan algoritmalar tipik olarak kendi kendini çağıran fonksiyonlar yoluyla gerçekleştirilmektedir. Aslında özyinelemeli algoritmalar yapay stack kullanılarak sanki iteratifmiş gibi de çözülebilmektedir. Ancak bunların en etkin çözümü kendi kendini çağıran fonksiyonlarla yapılmaktadır. Kendi kendini çağıran fonksiyonlara "özyinelemeli fonksiyonlar (recursive functions)" da denilmektedir. Bir algoritma hem iteratif yöntemle hem de özyinelemeli yöntemle çözülebiliyorsa genellikle (ancak her zaman değil) iteratif yöntem daha etkin bir gerçekleştirim sunmaktadır. Yani bu tür durumlarda genellikle iteratif yöntem tercih edilmelidir. Ancak yukarıda da belirtitğimiz gibi bazı algoritmaların iteratif çözümleri ya yaoktur ya da makul değildir. Bu durumda biz mecburen özyinelemeli çözümü uygularız. Genellikle bir fonksiyonun kendini çağırması insanlar tarafından tuhaf ve karmaşık olarak algılanmaktadır. Aslında bir fonksiyonun kendisini çağırması ile başka bir fonksiyonu çağırması arasında hiçbir farklılık yoktur. Nasıl bir fonksiyon başka bir fonksiyonu çağırdığında çağrılan fonksiyon bittiğinde akış çağırma noktasından sonraki deyimle devam ediyorsa benzer biçimde bir fonksiyon kendisini çağırdığında o çağrı bitince yine akış çağrılan noktadan sonraki deyimle devam edecektir. Ancak özyinelemede önemli bir problem "sonsuz döngü" oluşabilmesidir. Yani bir fonksiyon kendisini kontrolsüz bir biçimde çağırırsa sonsuz döngü oluşur. Örneğin: void foo(void) { printf("foo\n"); foo(); } Burada foo fonksiyonu çağrıldığında fonksiyon hep kendisini yeniden çağıracağı için sonsuz döngü oluşacaktır. Tabii aslında fonksiyonun hiç yerel değişkeni olmasa bile CALL işlemi sonucunda geri dönüş adresi stack'te kaydedildiği için bir stack taşması olaşacak ve Windows, Linux, macOS sistemlerinde program çökecektir. Özyinelemeli fonksiyonlarda anahtar noktalardan biri fonksiyon her kendini çağırdığında fonksiyonun yerel değişkenlerinin yeniden yeni kopyasının yaratılmasıdır. Örneğin: void foo(void) { int a, b; a = 10; b = 20; foo(); ... } Burada foo kendisini çağırdığında stack'te yeni bir a ve b nesneleri yaratılacaktır. Her çağrının a ve b nesneleri farklı olacaktır. Fonksiyonun parametre değişkenleri de fonksiyonunun her kendini çağırmasında yeniden yaratılmaktadır. Bir fonksiyon kendini kontrolsüz bir biçimde çağırmamalıdır. Bir noktaya kadar kendi kendini çağırmalı sonra çıkış sürecine girmelidir. Örneğin: void foo(int n) { if (n == 0) return; printf("n before call: %d\n", n); foo(n - 1); printf("n after call: %d\n", n); } ... foo(3); Burada her foo çağrısında n isimli o çağrıya özgü yeni bir parametre değişkeni yaratılmaktadır. Bu örnekte fonksiyon üç kere kendisini çağırmış sonra çıkış sürecine girmiştir. n == 0 durumunda fonksiyon return ile sonlandırılınca bir önceki çağırdan çalışma devam edeceğine dikkat ediniz. Aşağıda bu kullanıma ilişkin bir örnek verilmiştir: * Örnek 1, #include void foo(int n) { if (n == 0) return; printf("n before call: %d\n", n); foo(n - 1); printf("n after call: %d\n", n); } int main(void) { foo(3); return 0; } Şimdi de bir takım örneklerle konuyu pekiştirmeye çalışalım: * Örnek 1.0, Aşağıda faktöriyel hesabı yapan iteratif bir fonksiyon örneği veilmiştir. #include unsigned long long factorial(int n) { unsigned long long total = 1; for (int i = 2; i <= n; ++i) total *= i; return total; } int main(void) { unsigned long long result; result = factorial(10); printf("%llu\n", result); return 0; } * Örnek 1.1, Şimdi faktörüyel hesabını aşağıdaki gibi özyineleme ile yapalım: unsigned long long factorial(int n) { unsigned long long temp; if (n == 0) return 1; temp = n * factorial(n - 1); return temp; } Aslında burada temp ara değişkeninin kullanılmasına hiç gerek yoktur. Ancak özyinelemenin daha iyi anlaşılması için böyle bir ara değişken kullandık. Şimdi fonksiyonun aşağıdaki gibi çağrıldığını düşünelim: factorial(4); Burada özyinemeli çağırmalarda şöyle bir durum oluşacaktır: factorial(4) ************ temp = 4 * factorial(3); factorial(3) ************ temp = 3 * factorial(2); factorial(2) ************ temp = 2 * factorial(1); factorial(1) ************ temp = 1 * factorial(0); factorial(0) ************ return 1; En sonunda factorial(0) çağrısı 1 ile geri dçnünce çıkış sürecine girilmektedir. Tabii aslında buradaki temp değişkeninin kullanılmasına gerek yoktur: unsigned long long factorial(int n) { if (n == 0) return 1; return n * factorial(n - 1);; } Aşağıda bu konuya ilişkin örnek verilmiştir: #include unsigned long long factorial(int n) { if (n == 0) return 1; return n * factorial(n - 1);; } int main(void) { unsigned long long result; result = factorial(4); printf("%llu\n", result); return 0; } * Örnek 2.0.0, Bir yazıyı tersten yazdıran bir fonksiyon iteratif biçimde aşağıdaki şöyle yazılabilir. #include void putsrev(const char *str) { size_t i; for (i = 0; str[i] != '\0'; ++i) ; while (i-- > 0) putchar(str[i]); putchar('\n'); } int main(void) { char s[] = "ankara"; putsrev(s); return 0; } * Örnek 2.0.1, Şimdi de bu hesabı özyienelemeli olarak yapalım: Genel olarak düzden yapılan bir işlemi tersten yapmak için özyineleme kullanılabilir. Fonksiyon kendini çağırarak ilerler. Sona gelindiğinde çıkış sürecinde işlemler yaptırılır. Şöyleki; void putsrev(const char *str) { if (*str == '\0') return; putsrev(str + 1); putchar(*str); } Burada biz putsrev fonksiyonunu putsrev("ali") biçiminde çağırmış olalım. İlk çağrıda str göstericisi "ali" yazısını gösteriyor olur. İkinci çağrıda "li" yazısını, üçüncü çağrıda "i" yazısını ve son çağrıda null karakteri gösteriyor olur. Bir sonraki çağırmadan çıkıldığında bir önceki çağırmanın str göstericisini kullanıyor olmaktayız. Burada aslında bir str değişkeninin olmadığına her özyinelemede yeni bir str değişkeninin yaratıldığına dikkat ediniz. Tabii biz burada yalnızca özyineleme çalışması yapıyoruz. Yoksa bu problemin iteratif çözümü çok daha etkindir. Aşağıda bu konuya ilişkin örnek verilmiştir: #include void putsrev(const char *str) { if (*str == '\0') return; putsrev(str + 1); putchar(*str); } int main(void) { putsrev("ankara"); putchar('\n'); return 0; } * Örnek 2.1.0, Bir yazıyı iteratif yolla in-place biçimde ters çevirmek isteyelim. Klasik yöntem yazının sonuna kadar gidip baştan ve sondan karşılıklı elemanları yazının uzunluğunun yarısı kadar yer değiştirmektir. #include void revstr(char *str) { size_t n; char temp; for (n = 0; str[n] != '\0'; ++n) ; for (size_t k = 0; k < n / 2; ++k) { temp = str[k]; str[k] = str[n - k - 1]; str[n - k - 1] = temp; } } int main(void) { char s[] = "ankara"; revstr(s); puts(s); return 0; } * Örnek 2.1.1, Yukarıdaki fonksiyonun iteratif olarak başka bir yazım biçimi de aşağıdaki gibi olabilir. Burada fonksiyon ilk ve son karakterlerin indeks numaralarıyla çağrılmaktadır. #include #include void revstr(char *str, size_t left, size_t right) { char temp; while (left < right) { temp = str[left]; str[left] = str[right]; str[right] = temp; ++left, --right; } } int main(void) { char s[] = "ankara"; revstr(s, 0, strlen(s) - 1); puts(s); return 0; } * Örnek 2.2, Şimdi yukarıdaki fonksiyonu özyinelemeli hale dönüştürelim: void revstr(char *str, size_t left, size_t right) { char temp; if (right <= left) return; temp = str[left]; str[left] = str[right]; str[right] = temp; revstr(str, left + 1, right - 1); } Burada hger defasında left bir artırılarak, right ise bir eksiltilerek özyineleme yapılmıştır. Her özyinelemde yazının bir karakteri yer değiştirilmektedir. Tabii biz burada yalnızca özyineleme çalışması yapıyoruz. Yoksa bu problemin iteratif çözümü çok daha etkindir. Aşağıda bu konuya ilişkin örnek verilmiştir. #include #include void revstr(char *str, size_t left, size_t right) { char temp; if (right <= left) return; temp = str[left]; str[left] = str[right]; str[right] = temp; revstr(str, left + 1, right - 1); } int main(void) { char s[] = "ankara"; revstr(s, 0, strlen(s) - 1); puts(s); return 0; } * Örnek 3, Bazen özyinelemeli fonksiyonun parametrik yapısı kolay kullanıma izin vermeyebilir. Bu durumda programcı bir sarma fonksiyon (wrapper function) yazarak özyinelemeli fonksiyonu çağırır. #include #include void revstr_recur(char *str, size_t left, size_t right) { char temp; if (right <= left) return; temp = str[left]; str[left] = str[right]; str[right] = temp; revstr_recur(str, left + 1, right - 1); } void revstr(char *s) { size_t n; for (n = 0; s[n] != '\0'; ++n) ; revstr_recur(s, 0, n ? n - 1 : 0); } int main(void) { char s[] = "ankara"; revstr(s); puts(s); return 0; } * Örnek 4.0, Bir int sayıyı binary olarak iteratif biçimde ekrana yazdıran bir fonksiyon şöyle yazılabilir: void binprint(unsigned val) { for (int i = sizeof(val) * 8 - 1; i >= 0; --i) putchar((val >> i & 1) + '0'); putchar('\n'); } Burada sayının başındaki 0'lar da yazdırılmaktadır. Eğer sayının başındaki 0'ları yazdırmak istemeseydik kodu şöyle yazabilirdik: void binprint(unsigned val) { int i; for (i = sizeof(val) * 8 - 1; val >> i == 0 && i >= 0; --i) ; for (; i >= 0; --i) putchar((val >> i & 1) + '0'); putchar('\n'); } Aşağıda bu konuya ilişkin örnek verilmiştir: #include void binprint(unsigned val) { int i; for (i = sizeof(val) * 8 - 1; val >> i == 0 && i >= 0; --i) ; for (; i >= 0; --i) putchar((val >> i & 1) + '0'); putchar('\n'); } int main(void) { binprint(255); return 0; } * Örnek 4.1, Şimdi sayıyı ikilik sistemde bastıran fonksiyonu özyinelemeli olarak yazmak isteyelim. Burada biz sağdan sola öteleme yaparak fonksiyonu özyinelemeli biçimde çağırırız. Sonra çıkış sürecinde sayının en düşün anlamlı bitini yazdırırız. Yani aslında burada özyineleme ile düzden yapılan işlem tersine çevrilmektedir: void binprint(unsigned val) { if (val == 0) return; binprint(val >> 1); putchar((val & 1) + '0'); } Aşağıda bu konuya ilişkin örnek verilmiştir: #include void binprint(unsigned val) { if (val == 0) return; binprint(val >> 1); putchar((val & 1) + '0'); } int main(void) { binprint(255); return 0; } * Örnek 5.0, Bir bilgisayar sisteminde bir ekran varsa genel olarak o ekrana text modda yalnızca karakterler bastırılabilmektedir. Sayılar aslında karakterle dönüştürülüp yazıdırılırlar. Örneğin printf fonksiyonu "%d" format karakteri ile aslında int değeri karakterlere dönüştürür bu karakterleri ekrana basar. Başka bir deyişle aslında temel bir bilgisayar sisteminde programcının elinde yalnızca putchar benxeri bir fonksiyon vardır. Pekiyi biz int bir değeri yalnızca putchar kullnarak nasıl yazdırabiliriz? İlk akla gelen yöntem sayıyı basamaklarına ayrıştırıp onları putchar kullanarak yazdırmaktır. Ancak sürekli 10'a bölme yöntemi ile basamak ayrıştırılması yapıldığında basamaklar ters sırada elde edilmektedir. Örneğin: while (val) { digit = val % 10; val /= 10; } O halde bizim basamakları ters sırada elde edip bir diziye yerleştirmemiz sonra da o diziyi ters sırada yazdırmamız gerekir. Sayı negatifse onun negatif olduğu bilgisini saklayıp sayıyı pozitif hale dönüştürmek uygun olur. Aşağıda muhtemel bir iteratif çözüm örneği verilmiştir. Aşağıda bu konuya ilişkin örnek verilmiştir: #include void printd(int val) { int digit; char buf[64]; int nflag; int i; nflag = 0; i = 0; if (val < 0) { nflag = 1; val = -val; } for (i = 0; val != 0; ++i) { digit = val % 10; buf[i] = '0' + digit; val /= 10; } if (nflag) buf[i++] = '-'; for (--i; i >= 0; --i) putchar(buf[i]); putchar('\n'); } int main(void) { printd(-123456); return 0; } * Örnek 5.1, Aslında bir sayıyı 10'luk sistemde yalnızca putchar kullanarak yazdırma işlemi özyinelemeli biçimde çok daha kolay yapılabilmektedir. Yani bu problemin özyinelemeli çözümü iteratif çözümünden daha etkindir. Özyinelemeli çözüm şöyle olabilir: void printd(int val) { if (val < 0) { putchar('-'); val = -val; } if (val / 10) printd(val / 10); putchar(val % 10 + '0'); } Burada fonksiyonu -12345 değeri ile çağırmış olalım. Fonksiyon ilk çağrıda bir kez '-' karakterini ekrana basacak sonra bir daha bu ekrana basmayacaktır.Sonra özyinelemenin aşağıdaki gibi yapıldığına dikkat ediniz: if (val / 10) printd(val / 10); Burada 12345 değerinin 10'a bölümü 0 olmadığı için fonksiyon kendini 1234 değeri ile çağıracaktır. Benzer durumlar aşağıdaki gibi tekrarlanacaktır: val = 12345 val = 1234 val = 123 val = 12 val = 1 Burada artık fonksiyon sürecine girecektir. Çıkarken de en düşük basamağı yazdıracaktır. Aşağıda bu konuya ilişkin örnek verilmiştir: #include void printd(int val) { if (val < 0) { putchar('-'); val = -val; } if (val / 10) printd(val / 10); putchar(val % 10 + '0'); } int main(void) { printd(-123456); return 0; } * Örnek 5.2, Aslında yukarıdaki algoritma diğer tabanlar için de benzer biçimde çalışabilmektedir. Fakat burada dikkat edilmesi gereken nokta 10'luk sistemden büyük sistemlerde (örneğin 16'lık sistemde) basamakların artık A, B, C, ... biçiminde isimlendirilmesidir. ASCII tablosunda (ve diğer tablolarda da böyle) 0-9 arasındaki sayılar peşi sıra gitmektedir. Ancak '9' karakterinden sonra 'A' gelmemektedir. Arada 8 tane farklı karakter vardır. O halde eğer taban 10'dan büyükse bu durumu dikkate almak gerekir. Örneğin: putchar(n % base > 9 ? n % base - 10 + 'A' : n % base + '0'); Buraada taban 10'dan büyükse düzeltme yapılmıştır. Aşağıda bu konuya ilişkin örnek verilmiştir: #include void printd(int val, int base) { if (val < 0) { putchar('-'); val = -val; } if (val / base) printd(val / base, base); putchar(val % base > 9 ? val % base - 10 + 'A' : val % base + '0'); } int main(void) { printd(255, 16); return 0; } * Örnek 6, Kapalı bir şeklin içinin boyanması için birkaç algoritma kullanılmaktadır. Bunlardan birine "floodfill" algoritmsı denilmektedir. Bu algoritmada iç bir noktadan başlanır. Fonksiyon dört yönde kendini çağırır. Böylece bir su baskını gibi şekle nüfuz eder. Tabii sınıf noktası gelindiğinde ya da da önce boyabnış olan bir noktaya gelindiğinde fonksiyon hemen return etmelidir. Aşağıdaki örnekte floodfill algoritması 10x20'lik karakter tabanlı bir bitmap resim üzerine uygulanmıştır. Resim aşağıdaki olabilir: ####### ##### # ####### ###### # # # # # ###### #### # ##### # ## # ### Biz örneğimizde önce resmi bir text dosyaya kaydedip bu text dosyayı char türden bir matrise okumaktayız. Aşağıda bu konuya ilişkin örnek verilmiştir: #include #include #include #define ROWSIZE 10 #define COLSIZE 20 #define FILLCHAR '.' char g_bitmap[ROWSIZE][COLSIZE]; void read_bitmap(void) { FILE *f; char buf[1024]; if ((f = fopen("bitmap.txt", "r")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < ROWSIZE; ++i) { fgets(buf, 1024, f); memcpy(g_bitmap[i], buf, COLSIZE); } fclose(f); } void disp_bitmap(void) { for (int i = 0; i < ROWSIZE; ++i) { for (int k = 0; k < COLSIZE; ++k) putchar(g_bitmap[i][k]); putchar('\n'); } } void floodfill(int row, int col) { if (g_bitmap[row][col] == '#' || g_bitmap[row][col] == FILLCHAR) return; g_bitmap[row][col] = FILLCHAR; floodfill(row + 1, col); floodfill(row, col - 1); floodfill(row - 1, col); floodfill(row, col + 1); } int main(void) { read_bitmap(); disp_bitmap(); floodfill(6, 10); printf("\n\n"); disp_bitmap(); return 0; } int main(void) { if (!read_bitmap("bitmap.txt")) { fprintf(stderr, "cannot read bitmap!..\n"); exit(EXIT_FAILURE); } floodfill(5, 5, '.'); disp_bitmap(); return 0; } /* ####### ##### # ####### ###### # # # # # ###### #### # ##### # ## # ### */ * Örnek 7.0, Bilindiği gibi "seçerek sıralama (selection sort)" algoritması bir dizinin en küçük elemanın bulunup dizinin ilk elemanı ile yer değiştirilmesi temeline daynmaktadır. Bu biçimde dizi her defasında bir eleman daraltılarak aynı işlemler yapılmaktadır. Selection sort algortmasının iteratif çözümü aşağıda verilmiştir. #include void ssort(int *pi, size_t size) { size_t i, k; size_t min_index; int min; for (i = 0; i < size - 1; ++i) { min_index = i; min = pi[i]; for (k = i + 1; k < size; ++k) if (pi[k] < min) { min = pi[k]; min_index = k; } pi[min_index] = pi[i]; pi[i] = min; } } void disp(const int *pi, size_t size) { size_t i; for (i = 0; i < size; ++i) printf("%d ", pi[i]); printf("\n"); } #define SIZE 10 int main(void) { int a[SIZE] = { 34, 12, 7, 84, 72, 39, 75, 45, 59, 21 }; ssort(a, SIZE); disp(a, SIZE); return 0; } * Örnek 7.1, Seçerek sıralama yöntemini özyinelemeli bir biçimde de uygulayabiliriz. Tabii bu yöntemin özyinelemeli uygulaması makul değildir. Ancak biz burada özyineleme çalışması yapmak istiyoruz. Algoritmanın özyinelemeli versiyonunda dizinin en büyük elemanı bulunur. Bu eleman son elemanla yer değiştirilir. Sonra fonksiyon bir eksik uzunlukla kendini çağırır. Tabii uzunluk 1'e geldiğinde algoritma artık çıkış sürecine girmelidir. #include void selection_sort(int *pi, size_t size) { int max; size_t max_index; if (size <= 1) return; max = pi[0]; max_index = 0; for (size_t i = 1; i < size; ++i) if (pi[i] > max) { max = pi[i]; max_index = i; } pi[max_index] = pi[size - 1]; pi[size - 1] = max; selection_sort(pi, size - 1); } int main(void) { int a[10] = {41, 23, 12, 7, 37, 98, 29, 16, 82, 66}; selection_sort(a, 10); for (int i = 0; i < 10; ++i) printf("%d ", a[i]); printf("\n"); return 0; } * Örnek 8, 8 vezir problemi bir satranç tahtasına birbirini yemeyen 8 vezirin yerleştirilmesi problemidir. Problem özyinelemeli bir karakterdedir. Tipik bir çözümde satranç tahtası global bir matris ile tamsil edilir. Vezirlerin yemediği yerler ve vezirlerin yediği yerler matriste işaretlenir. Özyinelemeli fonksiyon global matrise bakarak o anda vezirlerin yemediği ilk kareye verizi yerleştirir. Global matirisi günceller ve kendisini çağırır. Eğer tahtada vezirlerin yemediği hiç kare kalmazsa fonksiyon kendisini sonlandırır. Eğer 8 vezir yerleştirilebilmişse tahta print edilir. Bir çağrı sonlandığında global dizinin eski haline getirilmesi gerekmektedir. Aşağıda 8 vezir probleminin çözümüne ilişkin bir örnek verilmiştir. #include #include #define SIZE 8 int g_qcount; int g_count; char g_board[SIZE][SIZE]; void init_board(void) { int r, c; for (r = 0; r < SIZE; ++r) for (c = 0; c < SIZE; ++c) g_board[r][c] = '.'; } void print_board(void) { int r, c; printf("%d\n", g_count); for (r = 0; r < SIZE; ++r) { for (c = 0; c < SIZE; ++c) printf("%c", g_board[r][c]); printf("\n"); } printf("\n"); } void locate_queen(int row, int col) { int r, c; g_board[row][col] = 'V'; r = row; for (c = col + 1; c < SIZE; ++c) g_board[r][c] = 'o'; for (c = col - 1; c >= 0; --c) g_board[r][c] = 'o'; c = col; for (r = row - 1; r >= 0; --r) g_board[r][c] = 'o'; for (r = row + 1; r < SIZE; ++r) g_board[r][c] = 'o'; for (r = row - 1, c = col - 1; r >= 0 && c >= 0; --r, --c) g_board[r][c] = 'o'; for (r = row - 1, c = col + 1; r >= 0 && c < SIZE; --r, ++c) g_board[r][c] = 'o'; for (r = row + 1, c = col - 1; r < SIZE && c >= 0; ++r, --c) g_board[r][c] = 'o'; for (r = row + 1, c = col + 1; r < SIZE && c < SIZE; ++r, ++c) g_board[r][c] = 'o'; } void queen8(int row, int col) { char board[SIZE][SIZE]; for (; row < SIZE; ++row) { for (; col < SIZE; ++col) { if (g_board[row][col] == '.') { memcpy(board, g_board, sizeof(board)); ++g_qcount; locate_queen(row, col); if (g_qcount == SIZE) { ++g_count; print_board(); } queen8(row, col); --g_qcount; memcpy(g_board, board, sizeof(board)); } } col = 0; } } int main(void) { init_board(); queen8(0, 0); return 0; } * Örnek 9.0, Dizin ağacının dolaşılması özyineleme gerektiren tipik algoritmalardandır. Dolaşım bir fonksiyon ile şöyle yapılmaktadır: Önce prosesin çalışma dizini fonksiyonun başında dolaşılacak dizin olarak set edilir. Sonra o dizin içerisindeki bütün dizin girişleri bulunur. Bir dizin girişi eğer bir dizin belirtiyorsa fonksiyon o dizin ile kendini çağırır. Ancak bir dizinde dizin listesinin sonuna gelindiğinde yine prosesin çalışma dizini o dizinin üst dizini olarak set edilir. Ancak gerek Windows sistemlerinde gerekse UNIX/Linux sistemlerinde bir dizinin içeriği erişim hakkı nedeniyle okunamayabilir. Bu durumda tamamen özyineleme durdurulabilir ya da bu hatalar görmezden gelinebilir. Windows sistemlerinde bir dizin var olduğu halde SetCurrentDirectory fonksiyonu ile erişim hakları yüzünden o dizin prosesin çalışma dizini yapılamayabilmektedir. Halbuki UNIX/Linux sistemlerinde chdir fonksiyonu bir dizine erişim hakkı olmasa bile başarılı olmaktadır. Aşağıda Windows sistemlerinde dizin ağacının dolaşılmasına bir örnek verilmiştir. Bu örnekte SetCurrentDirectory fonksiyonu başarısız olursa prosesin çalışma dizini değiştirilemediği için üst dizine geri dönüş yapılmamamktadır. #include #include #include void PutSysErr(LPCSTR lpszMsg); void WalkDir(LPCSTR lpszDirPath) { HANDLE hFF; WIN32_FIND_DATA fd; if (!SetCurrentDirectory(lpszDirPath)) { PutSysErr("SetCurrentDirectory"); return; } if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) { PutSysErr("FindFirstFile"); goto EXIT; } do { if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue; /* printf("%s\n", fd.cFileName); */ if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { printf("%s\n", fd.cFileName); WalkDir(fd.cFileName); } } while (FindNextFile(hFF, &fd)); if (GetLastError() != ERROR_NO_MORE_FILES) PutSysErr("FindNextFile"); FindClose(hFF); EXIT: if (!SetCurrentDirectory("..")) PutSysErr("SetCurrentDirectory"); } int main(void) { WalkDir("C:\\windows"); return 0; } void PutSysErr(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } } * Örnek 9.1.0, Dizin ağacını dolaşırken onları kademeli bir biçimde yazdırdabiliriz. Bunun için WalkDir fonksiyonuna kademeyi belirten bir level parametresi eklenebilir. Sonra her özyinelemede bu level parametresi bir artırılabilir. Kademeli yazdırmak için printf fonksiyonunda "%*s" format karakterini kullanabiliriz. Örneğin: printf("%*s%s\n", level * TABSIZE, "", fd.cFileName); Burada "%*s" format karakterinde "*" format karakterine level * TABSIZE argümanı, s format karakterine ise "" argümanı karşılık gelmektedir. Dolayısıyla bu çağrıda soldan level * TABSIZE kadar boşluk bırakılmış olmaktadır. Aşağıdaki örnekte kademeli yazım uygulanmıştır. Ancak bir dizine geçme ya da dizin listesini almada bir problem ortaya çıkarsa ve stderr dosyası yönlendirilmemişse kademede bozukluk gözükebilecektir. #include #include #include #define TABSIZE 4 void PutSysErr(LPCSTR lpszMsg); void WalkDir(LPCSTR lpszDirPath, int level) { HANDLE hFF; WIN32_FIND_DATA fd; if (!SetCurrentDirectory(lpszDirPath)) { PutSysErr("SetCurrentDirectory"); return; } if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) { PutSysErr("FindFirstFile"); goto EXIT; } do { if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue; /* printf("%s\n", fd.cFileName); */ if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { printf("%*s%s\n", level * TABSIZE, "", fd.cFileName); WalkDir(fd.cFileName, level + 1); } } while (FindNextFile(hFF, &fd)); if (GetLastError() != ERROR_NO_MORE_FILES) PutSysErr("FindNextFile"); FindClose(hFF); EXIT: if (!SetCurrentDirectory("..")) PutSysErr(lpszDirPath); } int main(void) { WalkDir("C:\Dropbox\Shared\Kurslar\SysProg-1"", 0); return 0; } void PutSysErr(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } } * Örnek 9.1.1, Yukarıdaki WalkDir fonksiyonlarında önemli bir kusur fonksiyonun prosesin çalışma dizinini değiştirerek işini sonlandırmasıdır. Fonksiyonun çağrılmadan önceki çalışma dizinini yeniden set etmesi için bir sarma fonksiyon kullanılabilir. (access POSIX fonksiyonu Windows sistemlerinde _access ismiyle de bulunmaktadır.) Bu sayede level parametresi de sarma fonksiyon tarafından asıl özyinelemeli fonksiyon çağrılırken kullanılacaktır. Dolayısıyla asıl fonksiyonun parametrik yapısı daha sade olacaktır. #include #include #include #define TABSIZE 4 void PutSysErr(LPCSTR lpszMsg); void WalkDirRecur(LPCSTR lpszDirPath, int level) { HANDLE hFF; WIN32_FIND_DATA fd; if (!SetCurrentDirectory(lpszDirPath)) { PutSysErr("SetCurrentDirectory"); return; } if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) { PutSysErr("FindFirstFile"); goto EXIT; } do { if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue; /* printf("%s\n", fd.cFileName); */ if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { printf("%*s%s\n", level * TABSIZE, "", fd.cFileName); WalkDirRecur(fd.cFileName, level + 1); } } while (FindNextFile(hFF, &fd)); if (GetLastError() != ERROR_NO_MORE_FILES) PutSysErr("FindNextFile"); FindClose(hFF); EXIT: if (!SetCurrentDirectory("..")) PutSysErr(lpszDirPath); } void WalkDir(LPCTSTR lpszDirPath) { char cwd[MAX_PATH]; if (!GetCurrentDirectory(MAX_PATH, cwd)) PutSysErr("GetCurrentDirectory"); WalkDirRecur(lpszDirPath, 0); if (!SetCurrentDirectory(cwd)) PutSysErr("SetCurrentDirectory"); } int main(void) { WalkDir("C:\\Dropbox"); return 0; } void PutSysErr(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } } * Örnek 9.2, Dizin ağacını dolaşırken bulunan dizin girişlerini ekrana yazdırmak yerine başka bir şey yapmak isteyebiliriz. Böyle bir fonksiyonun genelleştirilmesi için fonksiyon göstericilerinden faydalanılabilir. Aşağıdaki örnekte dizin girişleri bulundukça bir callback fonksiyon çağrılmaktadır. Böylece WalkDir fonksiyonunu kullanan kişiler dosyaları ekrana yazdırmak yerine başka işlemler de yapabilirler. #include #include #include #define TABSIZE 4 void PutSysErr(LPCSTR lpszMsg); void WalkDirRecur(LPCSTR lpszDirPath, int level, void (*Proc)(const WIN32_FIND_DATA *, int)) { HANDLE hFF; WIN32_FIND_DATA fd; if (!SetCurrentDirectory(lpszDirPath)) { PutSysErr("SetCurrentDirectory"); return; } if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) { PutSysErr("FindFirstFile"); goto EXIT; } do { if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue; Proc(&fd, level); if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) WalkDirRecur(fd.cFileName, level + 1, Proc); } while (FindNextFile(hFF, &fd)); if (GetLastError() != ERROR_NO_MORE_FILES) PutSysErr("FindNextFile"); FindClose(hFF); EXIT: if (!SetCurrentDirectory("..")) PutSysErr(lpszDirPath); } void WalkDir(LPCTSTR lpszDirPath, void (*Proc)(const WIN32_FIND_DATA *, int)) { char cwd[MAX_PATH]; if (!GetCurrentDirectory(MAX_PATH, cwd)) PutSysErr("GetCurrentDirectory"); WalkDirRecur(lpszDirPath, 0, Proc); if (!SetCurrentDirectory(cwd)) PutSysErr("SetCurrentDirectory"); } void DispFile(const WIN32_FIND_DATA *fd, int level) { printf("%*s%s\n", level * TABSIZE, "", fd->cFileName); } int main(void) { WalkDir("C:\\Dropbox\\Shared\\Kurslar\\SysProg-1", DispFile); return 0; } void PutSysErr(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } } * Örnek 10.0, Özyinelemeli fonksiyonlarda belli bir koşul sağlandığında özyinelemenin sonlandırılması da istenebilmektedir. Aşağıdaki örnekte callback fonksiyonun prototipi şöyledir: BOOL Proc(const WIN32_FIND_DATA *fd, int level); Fonksiyon her dizin girişi bulundukça çağrılmaktadır. Callback fonksiyon eğer sıfır dırşı bir değerle geri dönerse bu durum özyinelemenin devam edeceği anlamına gelmektedir. Eğer fonksiyon 0 ile geri dönerse bu durum özyinelemenin sonlandırılacağı anlamına gelmektedir. Tabii özyineleme sonlandırılırken eğer kaynak tahsisatı yapılmışsa (örneğimiz FindFirstFile fonksiyonunun geri döndürdüğü handle gibi) bunların serbest bırakılmasına dikkat edilmelidir. Aşağıdaki örnekte "sample.c" dosyası bulunduğunda özyineleme sonlandırılmaktadır. Bu örnekte Microsoft'a özgü _stricmp fonksiyonu kullanılmıştır. Bu fonksiyon büyük hard küçük harf duyarlılığı olmadan karşılaştırma yapmaktadır. #include #include #include #define TABSIZE 4 void PutSysErr(LPCSTR lpszMsg); BOOL WalkDirRecur(LPCSTR lpszDirPath, int level, BOOL (*Proc)(const WIN32_FIND_DATA *, int)) { HANDLE hFF; WIN32_FIND_DATA fd; BOOL bResult; bResult = TRUE; if (!SetCurrentDirectory(lpszDirPath)) { PutSysErr("SetCurrentDirectory"); return TRUE; } if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) { PutSysErr("FindFirstFile"); goto EXIT1; } do { if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue; if (!Proc(&fd, level)) { bResult = FALSE; goto EXIT2; } if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) if (!WalkDirRecur(fd.cFileName, level + 1, Proc)) { bResult = FALSE; goto EXIT2; } } while (FindNextFile(hFF, &fd)); if (GetLastError() != ERROR_NO_MORE_FILES) PutSysErr("FindNextFile"); EXIT2: FindClose(hFF); EXIT1: if (!SetCurrentDirectory("..")) PutSysErr(lpszDirPath); return bResult; } BOOL WalkDir(LPCTSTR lpszDirPath, BOOL (*Proc)(const WIN32_FIND_DATA *, int)) { char cwd[MAX_PATH]; BOOL bResult; if (!GetCurrentDirectory(MAX_PATH, cwd)) PutSysErr("GetCurrentDirectory"); bResult = WalkDirRecur(lpszDirPath, 0, Proc); if (!SetCurrentDirectory(cwd)) PutSysErr("SetCurrentDirectory"); return bResult; } BOOL DispFile(const WIN32_FIND_DATA *fd, int level) { printf("%*s%s\n", level * TABSIZE, "", fd->cFileName); if (!_stricmp(fd->cFileName, "sample.c")) return FALSE; return TRUE; } int main(void) { WalkDir("C:\\Dropbox\\Shared\\Kurslar\\SysProg-1", DispFile); return 0; } void PutSysErr(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } } * Örnek 10.1, Pekiyi callback fonksiyon içerisinde dosyaya ilişkin tam yol ifadesi (full path) nasıl elde edilebilir? Yukarıdaki örneğimizde zaten callback fonksiyon çağrıldığında prosesin çalışma dizini dizin girişinin içinde bulunduğu dizin biçimindedir. Dolayısıyla callback fonksiyon içerisinde GetCurrentDirectory fonksiyonu uygulanıp buradan elde edilen yol ifadesi ile callback fonskyiona geçirilen WIN32_FIND_DATA içerisindeki dosya birleştirilirse tam yol ifadesi eld edilebilir. Aşağıdaki örnekte "sample.c" dosyalarının bulunduğu tam yol ifadeleri yazdırılmıştır. #include #include #include #define TABSIZE 4 void PutSysErr(LPCSTR lpszMsg); BOOL WalkDirRecur(LPCSTR lpszDirPath, int level, BOOL(*Proc)(const WIN32_FIND_DATA *, int)) { HANDLE hFF; WIN32_FIND_DATA fd; BOOL bResult; bResult = TRUE; if (!SetCurrentDirectory(lpszDirPath)) { PutSysErr("SetCurrentDirectory"); return TRUE; } if ((hFF = FindFirstFile("*.*", &fd)) == INVALID_HANDLE_VALUE) { PutSysErr("FindFirstFile"); goto EXIT1; } do { if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue; if (!Proc(&fd, level)) { bResult = FALSE; goto EXIT2; } if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) if (!WalkDirRecur(fd.cFileName, level + 1, Proc)) { bResult = FALSE; goto EXIT2; } } while (FindNextFile(hFF, &fd)); if (GetLastError() != ERROR_NO_MORE_FILES) PutSysErr("FindNextFile"); EXIT2: FindClose(hFF); EXIT1: if (!SetCurrentDirectory("..")) PutSysErr(lpszDirPath); return bResult; } BOOL WalkDir(LPCTSTR lpszDirPath, BOOL(*Proc)(const WIN32_FIND_DATA *, int)) { char cwd[MAX_PATH]; BOOL bResult; if (!GetCurrentDirectory(MAX_PATH, cwd)) PutSysErr("GetCurrentDirectory"); bResult = WalkDirRecur(lpszDirPath, 0, Proc); if (!SetCurrentDirectory(cwd)) PutSysErr("SetCurrentDirectory"); return bResult; } BOOL DispFile(const WIN32_FIND_DATA *fd, int level) { char buf[1024]; if (!_stricmp(fd->cFileName, "sample.c")) { GetCurrentDirectory(MAX_PATH, buf); strcat(buf, "\\"); strcat(buf, fd->cFileName); printf("%s\n", buf); } return TRUE; } int main(void) { WalkDir("C:\\Dropbox\\Shared\\Kurslar\\SysProg-1", DispFile); return 0; } void PutSysErr(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } } * Örnek 11.0, UNIX/Linux sistemlerinde de dizin ağacı benzer biçimde dolaşılır. Yine özyinelemeli fonksiyon girişte prosesin çalışma dizinini set eder, çıkışta da çalışma dizinini yeniden üst dizin olacak biçimde ayarlar. Aşağıda tipik bir örnek verilmiştir. UNIX/Linux sistemlerinde readdir fonksiyonu ile yalnızca dizin girişinin isminin ve i-node numarasının elde edildiğini anımsayınız. Dosya bilgilerinin elde edilmesi için stat fonksiyonları kullanılmalıdır. Ancak bu tür uygulamalarda stat yerine lstat fonksiyonu tercih edilmelidir. stat fonksiyonunun sembolik bağlantıları izlemesi sonsuz döngü oluşumuna yol açabilmektedir. #include #include #include #include #include #include #include void walkdir(const char *path) { DIR *dir; struct dirent *de; struct stat finfo; if (chdir(path) == -1) { perror("chdir"); return; } if ((dir = opendir(".")) == NULL) { perror("opendir"); goto EXIT; } while (errno = 0, (de = readdir(dir)) != NULL) { if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (lstat(de->d_name, &finfo) == -1) { perror("lstat"); continue; } if (S_ISDIR(finfo.st_mode)) { printf("%s\n", de->d_name); walkdir(de->d_name); } } if (errno != 0) perror("readdir"); closedir(dir); EXIT: if (chdir("..") == -1) perror("chdir"); } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } walkdir(argv[1]); return 0; } * Örnek 11.1, Yukarıdaki programı yine Windows sistemlerinde yaptığımız gibi kademeli görüntüleyecek aşağıdaki gibi biçimde değiştebiliriz. #include #include #include #include #include #include #include #define TABSIZE 4 void walkdir(const char *path, int level) { DIR *dir; struct dirent *de; struct stat finfo; if (chdir(path) == -1) { perror("chdir"); return; } if ((dir = opendir(".")) == NULL) { perror("opendir"); goto EXIT; } while (errno = 0, (de = readdir(dir)) != NULL) { if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (lstat(de->d_name, &finfo) == -1) { perror("lstat"); continue; } if (S_ISDIR(finfo.st_mode)) { printf("%*s%s\n", level * TABSIZE, "", de->d_name); walkdir(de->d_name, level + 1); } } if (errno != 0) perror("readdir"); closedir(dir); EXIT: if (chdir("..") == -1) perror("chdir"); } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } walkdir(argv[1], 0); return 0; } * Örnek 11.2, Fonksiyonun prosesin çalışma dizinini geri set eden sarmalı biçimi de aşağıdaki gibi olabilir. #include #include #include #include #include #include #include #define TABSIZE 4 void walkdir_recur(const char *path, int level) { DIR *dir; struct dirent *de; struct stat finfo; if (chdir(path) == -1) { perror("chdir"); return; } if ((dir = opendir(".")) == NULL) { perror("opendir"); goto EXIT; } while (errno = 0, (de = readdir(dir)) != NULL) { if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (lstat(de->d_name, &finfo) == -1) { perror("lstat"); continue; } if (S_ISDIR(finfo.st_mode)) { printf("%*s%s\n", level * TABSIZE, "", de->d_name); walkdir_recur(de->d_name, level + 1); } } if (errno != 0) perror("readddir"); closedir(dir); EXIT: if (chdir("..") == -1) perror("chdir"); } void walkdir(const char *path) { char cwd[4096]; if (getcwd(cwd, 4096) == NULL) { perror("getcwd"); return; } walkdir_recur(path, 0); if (chdir(cwd) == -1) { perror("chdir"); return; } } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } walkdir(argv[1]); return 0; } * Örnek 11.3, Aşağıda dizin ağacını dolaşırken callback fonksiyonu çağıran bir örnek verilmiştir. Burada callback fonksiyon Windows sistemlerinde yaptığımız örneğe benzerdir: bool walkproc(const char *name, const struct stat *finfo, int level); Yine callback fonksiyon çağrıldığında prosesin çalışma dizini dizin girişinin bulunduğu dizindedir. Fonksiyon sıfır dışı bir değerle geri döndürülürse dolaşma devam ettirlir, 0 değeri ile geri döndürülürse dolaşma sonlandırılır. #include #include #include #include #include #include #include #include #define TABSIZE 4 bool walkdir_recur(const char *path, bool (*proc)(const char *, const struct stat *, int), int level) { DIR *dir; struct dirent *de; struct stat finfo; bool retval; retval = true; if (chdir(path) == -1) { perror("chdir"); return true; } if ((dir = opendir(".")) == NULL) { perror("opendir"); goto EXIT1; } while (errno = 0, (de = readdir(dir)) != NULL) { if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (lstat(de->d_name, &finfo) == -1) { perror("lstat"); continue; } if (!proc(de->d_name, &finfo, level)) { retval = false; goto EXIT2; } if (S_ISDIR(finfo.st_mode)) if (!walkdir_recur(de->d_name, proc, level + 1)) { retval = false; goto EXIT2; } } if (errno != 0) perror("readddir"); EXIT2: closedir(dir); EXIT1: if (chdir("..") == -1) perror("chdir"); return retval; } void walkdir(const char *path, bool (*proc)(const char *, const struct stat *, int)) { char cwd[4096]; if (getcwd(cwd, 4096) == NULL) { perror("getcwd"); return; } walkdir_recur(path, proc, 0); if (chdir(cwd) == -1) { perror("chdir"); return; } } bool disp(const char *name, const struct stat *finfo, int level) { printf("%*s%s\n", level * TABSIZE, "", name); return true; } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } walkdir(argv[1], disp); return 0; } * Örnek 12, Aşağıdaki örnekte belli bir kök dizinden başlanarak "sample.c" dosyaları bulunmuş, onların mutlak yol ifadeleri ve uzunlukları yazdırılmıştır. #include #include #include #include #include #include #include #include #define TABSIZE 4 bool walkdir_recur(const char *path, bool (*proc)(const char *, const struct stat *, int), int level) { DIR *dir; struct dirent *de; struct stat finfo; bool retval; retval = true; if (chdir(path) == -1) { perror("chdir"); return true; } if ((dir = opendir(".")) == NULL) { perror("opendir"); goto EXIT1; } while (errno = 0, (de = readdir(dir)) != NULL) { if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; if (lstat(de->d_name, &finfo) == -1) { perror("lstat"); continue; } if (!proc(de->d_name, &finfo, level)) { retval = false; goto EXIT2; } if (S_ISDIR(finfo.st_mode)) if (!walkdir_recur(de->d_name, proc, level + 1)) { retval = false; goto EXIT2; } } if (errno != 0) perror("readddir"); EXIT2: closedir(dir); EXIT1: if (chdir("..") == -1) perror("chdir"); return retval; } void walkdir(const char *path, bool (*proc)(const char *, const struct stat *, int)) { char cwd[4096]; if (getcwd(cwd, 4096) == NULL) { perror("getcwd"); return; } walkdir_recur(path, proc, 0); if (chdir(cwd) == -1) { perror("chdir"); return; } } bool disp(const char *name, const struct stat *finfo, int level) { char buf[4096]; if (!strcmp(name, "sample.c")) { getcwd(buf, 4096); strcat(buf, "/"); strcat(buf, name); printf("%s, %lld\n", buf, (long long)finfo->st_size); } return true; } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } walkdir(argv[1], disp); return 0; } > Hatırlatıcı Notlar: >> Stack (yığın) sözcüğü hem işlemciler tarafından kullanılan bir mekanizmayı hem de bir veri yapısını anlatmaktadır. Yani işlemciler kendi çalışmaları sırasında ismine "stack" denilen bir alanı kullanmaktadır. Ancak aynı zamanda algoritmalar ve veri yapıları dünyasında "stack" isimli bir veri yapısı da vardır. (Tabii stack veri yapısına işlemcilerin kullandığı "stack mekanizmasına" benzemekten dolayı "stack" ismi verilmiştir.) Biz C derlerinden fonksiyonların yerel değişkenlerinin stack'te yaratıldığını biliyoruz. Fonksiyonların parametre değişkenleri de bazı sistemlerde stack'te bazı sistemlerde CPU yazmaçlarında yaratılmaktadır. Stack denilen alan RAM'in bir bölgesidir. Yani RAM'in içerisinde bir yerdir. Bir programın (genel olarak thread'in) kullanacağı stack alanı ve miktarı işletim sistemi tarafından belirlenmektedir. Yani program yüklendiğinde işlemcinin kullanacağı stack alanı ve miktarı zaten belirlenmiş durumdadır. Stack aalanı genellikle işlemcilerde aşağıdan yukarıya doğru kullanılmaktadır. Bir fonksiyon çağrıldığında o fonksiyonun yerel değişkenleri stack'te yaratılır. Stack'in aktif noktası (top of the stack) işlemcinin bir yazmacı (register) tutulmaktadır. Bu yazamaca genel olarak "stack pointer" denilmektedir. Her fonksiyon çağrılmasında stack pointer o fonksiyonun yerel değişkenleri kadar yukarı çekilir ve fonksiyonun yerel değişkenleri orada yaratılır. Örneğin: void bar(void) { int x, y; } void foo(void) { int a, b; bar(); } Bu tanımlamalar doğrultusunda, -> Buraada başlangıç noktasının aşağıdaki gibi olduğunu düşünelim: SP ----> Stack'in sonu -> foo çağrıldığında şöyle bir durum oluşacaktır: SP ----> a b Stack'in sonu -> Şimdi foo fonksiyonu bar fonksiyonunu çağırmış olsun: SP ----> x y a b Stack'in sonu -> Şimdi bar fonksiyonunun sonlandığını düşünelim: SP ----> a b Stack'in sonu şeklinde olacaktır. Stack konusundaki şu noktalara da dikkat etmeliyiz: >>> Fonksiyonun yerel değişkenlerinin stack'teki konumları için standart bir belirleme yapılmamıştır. Genellikle derleyiciler fonksiyonların yerel değişkenlerini ardışıl bir biçimde stack'te oluştururlar. Bazı derleyiciler ilk bildirilen yerel değişken düşük adreste olacak biçimde bazıları ise yüksek adreste olacak biçimde oluşturmaktadır. Ancak stack yalnızca yerel değişkenler için kullanılmaktadır. >>> Bir fonksiyon CALL makine komutu ile çağrıldığında geri dönüşün mümkün olabilmesi için CALL makine komutunda işlemci sonraki komutun adresini stack'te saklar. ret makine komutu da stack'ten bu adresi alarak geri dönüşü sağlar. Yani bir fonksiyon çağırdığımızda da stack'te dolaylı bir biçimde yer ayrılmaktadır. Yukarıdaki foo fonksiyonunun bar fonksiyonu çağırması durumunun daha gerçekçi stack götüntüsü şöyle olacaktır: SP ----> x y a b Stack'in sonu >>> Stack'te yerel değişkenlerin tahsisatı çok hızlı bir biçimde tek bir makine komutuyla yapılabilmektedir. Örneğin bir fonksiyonun toplam 100 byte uzunluğunda yerel değişkenleri olsun. Tahisat stack pointer'ın 100 byte azaltılması ile yani tek bir makine komutuyla yapılabilmektedir. Benzer biçimde tahsisatın geri alınması da tek bir makine komutuyla stack pointer'ın 100 byte artırılması ile yapılabilmektedir. Pekiyi stack için ayrılan alan yetmezse ne olur? Eğer fonksiyon çağrılarıyle stack'ta çok fazla alan tahsis edilirse stack yukarıdan taşabilir. Buna İngilizce "stack overflow" denilmektedir. Tabii stack'in taşması durumu derleme aşamasında tespit edilemez. Çünkü derleme sırasında hangi fonksiyonun hangi fonksiyonu çağıracağı kesin olarak bilinememektedir. Stack taşması durumunda koruma mekanizmasının olduğu Windows gibi, Linux gibi, macOS gibi sistemlerde taşma işlemci tarafından belirlenip işletim sistemine bildirilmektedir. İşletim sistemi de taşmaya yol açan prosesi sonlandırmaktadır. 32 bit ve 64 bit Windows sistemlerinde default stack 1MB'tır. Ancak 32 bit ve 64 bit Linux sistemlerinde default stack 8MB'dir. 1MB stack aslında genel olarak büyük bir stack'tir. >>> Pekiyi fonksiyonların parametre değişkenleri nerede ve nasıl yaratılmaktadır? Yukarıda da belirttiğimiz gibi parametre aktarımı stack yoluyla ya da yazmaç yoluyla yapılabilmektedir. Örneğin 32 bit Windows, Linux ve macOS sistemlerinde aktarım stack yoluyla yapılırken, 64 bir Windows Linux ve macOS sistemlerinde yazmaç yoluyla yapılmaktadır. Fonksiyon çağrısı sırasındaki aşağı seviyeli bu ayrıntılara genel olarak "Application Binary Interface (ABI)" denilmektedir. Fonksiyon çağrısı özelinde uygulanan yönteme "fonksiyon çağırma biçimi (function calling convention)" da denilmektedir. Eğer parametreler yazmaç yoluyla aktarılıyorsa iç içe çağırmalarda yazmaçların korunması çağrılan fonksiyonun (callee) sorumluluğundadır. Tabii içteki fonksiyon bunun için stack kullanır. O halde özünde parametre değişkenlerinin de stack yoluyla saklandığını söyleyebiliriz. /*================================================================================================================================*/ (29_24_09_2023) & (30_30_09_2023) & (31_01_10_2023) > Yapılarıda Hizalama (Alignment in Struct): C'de derleyiciler yapı elemanlarına erişimi hızlandırmak için elemanların arasında belli bir kurala göre boşluklar bırakabilmektedir. Bu duruma "yapı elemanlarının hizalanması (struct member alignment)" ya da kısaca "hizalama (alignment)" denilmektedir. Örneğin: struct SAMPLE { short a; int b; short c; int d; }; struct SAMPLE s; Burada short türünün 2 byte int türünün 4 byte olduğu bir sistemde ilk bakışta s nesnesinin sizeof değeri 12 olacakmış gibi gözükmektedir. Ancak muhtemelen bu sizeof değeri yazdırıldığında 16 olduğu görülecektir. Çünkü derleyici a ile b arasında ve s ile d arasında 2 byte'lık boşluklar bırakacaktır. Yani s nesnesinin bellekteki organizasyonu şöyle olacaktır: a (2) Boşluk (2) b (4) c (2) Boşluk (2) d (4) Derleyicinin bıraktığı bu boşluklara İnglizce "padding" denilmektedir. Yapı elemanlarına erişim sırasında derleyici hangi elemanlar arasında ne kadar boşluk bıraktığını bildiği için bir sorun oluşmamaktadır. Örneğin struct SAMPLE türünden ps isimli bir gösterici olsun. Biz de ps->c ifadesiyle ps adresinden başlayan yapının c elemanına erişmek ieteyelim. Derleyci boşlukları nerelerde bıraktığını bildiği için c elemanının ps adresinden 8 byte ileride olduğunu hesaplayabilmektedir. C standartlarına göre yapı elemanları ilk eleman düşük adreste olacak biçimde sırasıyla dizilmektedir. Yapı elemanları arasında boşlukların bırakılabileceği standartlarda belirtilmiştir. Başka bir deyişle elemanlar arasında bırakılan boşluklar (paddings) elemen ardışıllığını aslında bozmamaktadır. Pekiyi derleyici yapı elemanları arasında neden boşluklar bırakabilmektedir? Bunun en önemli gerekçesi "hız kazancı" sağlamaktır. Hız kazancının nasıl sağlandığı işlemci ile RAM arasındaki bağlantı biçi ile açıklanabilir. Örneğin 32 bit işlemcilerde işlemci RAM ile bilgileri dörder byte'lık bloklar biçiminde transfer etmektedir. Yani işlemci bellekten 2 byte'lık bir bilgiyi 2 byte olarak okumaz. Onun içinde bulunduğu 4 byte'ın tamamını okuyup onun içerisindeki 2 byte'ı ayrıştırır. Bu sistemlerde RAM'in işlemciye göre görüntüsü şöyledir: xxxx xxxx xxxx xxxx .... xxxx xxxx xxxx xxxx Burada her satır dördün katlarındadır. Şimdi işlemcinin aşağıdaki gibi 4 byte'lık int bir nesneye erişmek istediğini düşünelim: xxxx xxxx xxxx xxxx .... xxxx xxii iixx xxxx Burada erişelecek int nesne 4'ün katında değildir. İşte işlemci iki bus ahareketi yaparak int nesnenin parçalarını ayrı ayrı 4 byte'ı okuyup kendi içinde birleştirmektedir. Tabii bu erişim makine komutuna yansımamaktadır. Makine komutunu uygulayan programcı yine tek bir komut olarak onu yazmıştır. Ancak bu komut kendi içerisinde iki kere RAM erişimiş yaptığı için nano düzeyde daha yavaş çalışacaktır. Pekiyi aynı int nesne aşağıdakiş gibi 4'ün katlarında bulunsaydı ne olurdu? xxxx xxxx xxxx xxxx .... xxxx xxxx iiii xxxx Burada işlemci tek bir RAM erişimi ile int nesneyi tek hamlede elde edebilecekti. Buradan çıkan basit sonuç şudur: 32 bit işlemcilerde 4 byte'lık nesnelere (örneğin int bir nesne) hızlı erişilebilmesi için bu nesnelerin 4'ün katlarında bulunması gerekir. Pekiyi 2 byte'lık (örneğin short türden) bir nesne için bu böyle bir hizalamaya gerek var mıdır? İşte 2 byte'lık nesnenin 4 byte'ın neresinden başladığına göre bu durum değişebilir. Örneğin: xxxx xxxx xxxx xxxx .... xxxx xxxx xxss xxxx Burada iki byte'lık short nesneye erişimde bir yavaşlık söz konusu olmayacaktır. Ancak örneğin: xxxx xxxx xxxx xxxx .... xxxx xxxx xxxs sxxx Burada 2 byte'lık short nesneye erişim diğer duruma göre nano düzeyde yavaş olacaktır. Pekiyi 1 byte'lık char gibi bir nesne için hizalama önemli midir? Bunun yanıtı 1 byte'lık nesneler için hizalamanın önemli olmadığıdır. Bu 1 byte'lık nesneler 4 byte'tın neresinde olursa olsun işlemci tarafından tek hamlede alınabilmektedir. Örneğin: cxxx xxxx xcxx xxxx .... xxxc xxxx xxcx xxxx Burada tüm 1 byte'lık char nesnelere aynı hızda erişilecektir. Pekiyi bu durumda derleyicinin nasıl bir strateji izlemesi anlamlı olur? Aslında 32 bit bir işlemci için izlenecek strateji basittir: Tüm nesnelerin kendi uzunluklarının katlarına yerleştirilmesi hızlı erişim için yeterli olmaktadır. Yani örneğin int bir nesne 4'ün katlarına, short bir nesne 2'nin katlarına char bir nesne 1'in katlarına yerleştirilmelidir. Pekiyi 8 byte'lık double türü kaçın katlarına yerleştirilmelidir? İşte matematik işlemci bağlantısı da dikkate alındığında bu nesnelrin de 8'in katlarında olması en iyi durumdur. Şimdi yapı elemanları arasında derleyicinin nedne ne nasıl boşluk bıraktığı artık anlaşılabilir. Pekiyi hizalama yalnızca yapı elemanları için mi önemlidir. Yerel değişkenler de benzer biçimde hizalanmakta mıdır? Derleyici aslında hizalamayı tüm nesneler için yapmaktadır. Ancak zaten C ve C++ standartları yerel değişkenlerin yerleşimi hakkında bir şey söylememektedir. Örneğin: void foo(void) { char a; int b; ... } Burada bu iki yerel değişkenin ardışıl olmasının bir garantisi yoktur. İlk bildirilen değişkenin stack'te düşük adreste olmasının da bir garantisi yoktur. Dolayısıyla genellikle bu durum programcıyı ilgilendirmemektedir. Ancak dizi elemanları her zaman ardışıldır. Yani bir elemanın bittiği yerde boşluk olmaksızın diğeri başlamıdır. Örneğin: short a[10]; Derleyici bu diziyi 2'nin katlarına yerleştirirse zaten dizinin tüm elemanları 2'nin katlarında olacaktır. Örneğin: int b[10]; Burada da derleyici b dizisini 4'ün katına yerleştirirse zaten dizinin tüm elemanları 4'ün katlarında olur. Pekiyi dinamik bellek fonksiyonlarında durum nasıldır? Çünkü biz malloc gibi bir fonksiyonun geri döndürdüğü değeri herhangi biçimde kullanabiliriz. Örneğin: struct SAMPLE { char a; int b; char c; int d; }; ... struct SAMPLE *ps; ps = malloc(sizeof(struct SAMPLE)); Burada malloc alan tahsis ederken zaten oranın herhangi bir türden olabileceği fikriyle uygun değerin katlarında olan bir adres verecektir. Örneğin burada malloc fonksiyonun 4'ün katlarında bir adres vermesi gerekir. Tabii malloc fonksiyonu bizim bu adresi nasıl kullanıcığımızı bilmemektedir. Bu nedenle en kötü olasılığa göre bir hizalama uygulayacaktır. Hizalama konusunda şu noktalara dikkat ediniz: >> Derleyiciler genel olarak beş farklı hizalama stratejisi izleyebilmektedir: -> 1 Byte Hizalama (Byte Alignment) -> 2 Byte Hizalama (Word Alignment) -> 4 Byte Hizalama (Double Word Alignment) -> 8 Byte Hizalama (Quad Word Alignemnet) -> 16 Byte Hizalama (Double Quad Word Alignment) N byte hizalama şu anlama gelmektedir: "Nesnenin uzunluğu ve N değerinin hangisi küçükse nesne o değerin katlarına hizalanır". Nesnenin tamamı da N'in katlarına hizalanmaktadır. Örneğin 4 byte hizalama söz konusu olsun. Bu durumda 1 byte'lık bir nesne (örneğin char bir nesne) 1'in katlarına, 2 byte'lık bir nesne (örneğin short bir nesne) 2'nin katlarına, 4 byte'lık bir nesne (örneğin int bir nesne) 4'ün katlarına ve 8 byte'lık bir nesne 4'ün katlarına yerleştirilir. Örneğin 8 byte (Quad Word alignment) söz konusu olsun. Bu durumda 1 byte'lık nesne 1'in katlarına, 2 byte'lık bir nesne 2'nin katlarına, dört byte'lık bir nesne 4'ün katlarına, 8 byte'lık bir nesne 8'in katlarına hizalanır. "1 byte hizalamanın aslında hizalama yapmamakla" aynı anlama geldiğine dikkat ediniz. Dolayısıyla programcı hizalamayı kaldıracaksa derleyiciyi 1 byte hizalama ayaralayabilir. >> Derleyicinin yaptığı hizalamalar bazen programcının işine gelmeyebilir. Yani programcı çeşitli gerekçelerle derleyicinin hizalama yapmasını istemeyebilir. Hizalamanın programcı tarafından kontrolü genellikle derleyicilerin sunduğu ek özelliklerle sağlanmaktadır. Microsoft derleyicilerinde hizalama komut satırından derleme yapılırken /ZpN (buradaki N 1, 2, 4, 8, 16 olabilir) seçeneği ile ayarlanmaktadır. Hizalama Visual Studio IDE'sinde proje seçeneklerinden "C-C++/Code Generation/Struct Member Alignment" combo box seçneğinden ayarlanabilmektedir. Eğer bu ayarlamalar yapılmazsa 32 bit derleme için default durum /Zp8 (yani quad word alignment), 64 bit derleme için /Zp16 biçimindedir. Microsoft derleyicileri bir yapının en büyük elemanı neyse yapının tamamını da o en büyük elemanı referans alarak hizalamaktadır. Yani bu durumda yapı nesnenin sonunda da yapının en büyük elemanının hizalama bilgisi kadar boşluk bulundurulacaktır. gcc ve clang derleyicilerinde de default durumda Microsoft'taki gibi 32 bit derleyiciler için 8 byte hizalama, 64 bit derleyiciler için 16 byte hizalama kullanılmaktadır. Hizalamayı değiştirmek için -fpack-struct=N komut satırı seçeneği kullanılmalıdır. Örneğin, gcc -fpack-struct=1 -o sample sample.c >> Hizalama konusunda default belirlemeye neden müdahale etmek isteriz? İşte bunun en tipik örneği bir dosyanın içerisindeki bilgilerin fread ya da read gibi bir fonksiyonla bir yapı nesnesinin içerisine okunmasının istendiği durumlardır. Örneğin bir BMP dosyasının başındaki 12 byte'a BMP başlığı denilmektedir. BMP başlığı şu biçimde içeriğe sahiptir: Uzunluk İçerik 2 byte Magic Number 4 byte Dosya uzunluğu 2 byte Reserved 2 byte Reserved 4 byte Image bilgilerinin bulunduğu offset Şimdi biz bu dosyanın başından 12 byte okuyarak okuduklarımızı bir yapı ile çakıştırmak isteyelim: struct BITMAP_HEADER { char magic[2]; /* 2 byte */ uint32_t size; /* 4 byte */ uint16_t reserved1; /* 2 byte */ uint16_t reserved2; /* 2 byte */ uint32_t dataloc; /* 4 byte */ }; İşte okudğumuz 12 byte bu elemanlarla default hizalama yüzünden çakışmayacaktır. Bu çakışmayı sağlamak için bizim hizalamayı byte hizalaması olarak ayarlamamız gerekir. Aşağıdaki örneği "test.bmp" isimli bir BMP dosyası oluşturup hep default hizalama ile hem de 1 byte hizalama ile deneyiniz. * Örnek 1, #include #include #include struct BITMAP_HEADER { char magic[2]; /* 2 byte */ uint32_t size; /* 4 byte */ uint16_t reserved1; /* 2 byte */ uint16_t reserved2; /* 2 byte */ uint32_t dataloc; /* 4 byte */ }; #pragma pack(4) int main(void) { FILE *f; struct BITMAP_HEADER bh; if ((f = fopen("test.bmp", "rb")) == NULL) { fprintf(stderr, "Cannot open file!..\n"); exit(EXIT_FAILURE); } fread(&bh, sizeof(struct BITMAP_HEADER), 1, f); printf("Magic: %c%c\n", bh.magic[0], bh.magic[1]); printf("Size: %u\n", bh.size); printf("Bitmap Data Locatiion: %u\n", bh.dataloc); fclose(f); return 0; } >> Pekiyi hizalama program kodunun içerisinde ayaralanabilir mi? İşte hizalamayı değiştirmek için hem Microsoft hem gcc hem de clang derleyicilerinde #pragma pack(N) direktifi kullanılabilmektedir. Bu direktif komut satırında belirtilen hizalama seçeneğine göre daha yüksek önceliklidir. (Yani hem komut satırında belirleme yapıp hem de £pragma pack ile belirleme yaparsak #pragma pack belirlemesi dikkate alınır. * Örnek 1, #include #pragma pack(1) struct SAMPLE { char a; int b; char c; int d; }; int main(void) { struct SAMPLE s; printf("%zd\n", sizeof s); /* 10 */ return 0; } Buradaki #pragma pack direktifi sonraki #pragma pack direktifine kadar etkili olmaktadır. Böylece programcı isterse programın farklı yerlerinde farklı hizalamalar kullanabilir. * Örnek 1, #include #pragma pack(1) struct SAMPLE { char a; int b; char c; int d; }; #pragma pack(8) struct MAMPLE { char a; int b; char c; int d; }; int main(void) { struct SAMPLE s; struct MAMPLE m; printf("%zd\n", sizeof s); /* 10 */ printf("%zd\n", sizeof m); /* 16*/ return 0; } >> C11 ile birlikte bir nesnenin (bir yapının elemanı için de söz konusu olabilir) hizalaması _Alignas(N) belirleyicisi ile değiştirilebilmektedir. * Örnek 1, #include struct SAMPLE { int a; _Alignas(8) int b; }; int main(void) { struct SAMPLE s; printf("%zd\n", sizeof s); /* 16 */ return 0; } Burada yapının b elemanının önüne _Alignas(8) belirleyicisi getirilmiştir. Bu belirleyici aslında dördün katlarına yerleştirilecek int nesnesnin 8'in katlarına yerleştirilmesini sağlamaktadır. Ancak C11 standartlarına göre _Alignas(N) belirleyicisi ile yüksek bir hizalama gereksinimi düşük bir hizalamaya çevrielemez. Örneğin: struct SAMPLE { int a; _Alignas(1) int b; /* geçersiz! */ }; _Alignas ile belli nesnelerin ya da belli yapı elemanlarının hizalama gereksinimlerinin değiştirilebildiğine dikkat ediniz. Ayrıca C11 ile birlikte _Alignof(tür_ismi) isminde bir operatör de dile eklenmiştir. Bu operatör o anda o tür için derleyicinin uyguladığı hizalamayı bize vermektedir. Örneğin: #include int main(void) { printf("%zd\n", _Alignof(int)); /* 4 */ return 0; } _Alignas belirleyicisi bir tür ismiyle de kullanılabilmektedir. Örneğin: _Alignas(int) char c; _Alignas(tür_ismi) aslında _Alignas(_Alignof(tür_ismi)) anlamına gelmektedir. Yani bir _Alignas(int) dediğimizde int türünün hizalama gereksinimi neyse o sayıyı parantez içerisine yazmış gibi oluruz. >> Anımsanacağı derleyicilerin yapı elemanlarının arasında hizalama amacıyla bıraktığı boşluklara "padding" deniyordu. Aynı türden iki yapı nesnesi birbirine atandığında "padding" kısımlarının birbirine atanması konusunda bir garanti verilmemektedir. Örneğin: struct SAMPLE { char a; int b; }; ... struct SAMPLE x = {'a', 3}, y; x = y; Burada C standartları a elemanı ile b elemanı arasındaki muhtemelen 3 byte'lık padding alanının atama sırasında hedefe kopyalanaacağı konusunda bir garanti vermemektedir. Programcının bu padding alanlarını kullanmaya çalışması iyi bir teknik değildir. > Cache Sistemler: Sistem programalama karşımıza çıkan önemli olgulardan biri de "cache sistemleridir". Bir sistem programcısının cache sistemlerine ilişkin temel bilgileri edinmiş olması gerekmektedir. Bu bölümde cache sistemlerinde üzerinde durulacaktır. Bir bilgiye erişilmek istenen durumlarda çoğu kez karşımıza iki bellek türü çıkmaktadır: Yavaş bellek ve hızlı bellek. Yavaş bellek genellikle bol ve ucuzdur. Hızlı bellek ise kıt ve pahalıdır. Dolayısıyla tüm belleğin hızlı bellek olarak tasarlanması uygun olamayabilmektedir. Ayrıca bazı sistemlerde sistemin doğası gereği yavaş bellekler hızlı belleklerle yer değiştirememektedir. Genellikle asıl bilgiler yavaş belleklerde saklanır. Hızlı bellekler yavaş belleklere erişimi azaltarak hız kazancı sağlamak için kullanılırlar. Bir cache sisteminde yavaş belleğin belli bölümleri hızla bellekte tutulur. Yavaş bellekteki bilgiye başvurmak isteyen kişi önce bilgiyi hızlı bellekte arar. Eğer yavaş belleğin o kısmı hızlı bellek içerisinde bulunuyorsa bilgi hızlı bir biçimde elde edilir. Eğer yavaş belleğin içerisindeki bilgi o anda hızlı bellekte bulunmuyorsa bu durumda gerçekten yavaş belleğe başvurularak bilgi oradan elde edilir. Bu sistemde yavaş belleğin belli bölümlerini tutan hızlı belleğe "cache bellek" denilmektedir. Bu sistemler de "cache sistemleri" olarak adlandırılmaktadır. (Cache sözcüğünün genellikle Türkçe karşılığı "ön bellek" biçiminde ifade edilmektedir. Biz bazen "cache" sözcüğünü bazen de "ön bellek" sözcüğünü kullanacağız.) Cache sisteminde bilgiye erişilmek istendiğinde eğer bilgi cache bellekte bulunuyorsa bu duruma İngilizce "cache hit (ön belleğin isabet ettirilmesi") denilmektedir. Eğer bilgi cache bellekte yoksa bu duruma da İngilizce "cache miss (cache belleğin ıskalanması)" denilmektedir. İyi bir cache sisteminde "cache hit" oranının yükseltimesi istenir. Cache hit oranı N tane erişimin yüzde kaçının cache'ten karşılandığına denilmektedir. Bunu şöyle gösterebiliriz: Cache hit oranı = cache hit sayısı / N Pekiyi bir cache sisteminde cache hit oranı nasıl artırılabilir? Şüphesiz cache bellek büyütüldükçe bunun cache hit oranı üzerinde olumlu bir etkisi olacaktır. Ancak bu durumda maliyet de artacaktır. Cache hit oranı üzerinde en önemli etkenlerden biri yavaş belleğin nerelerinin hangi sürelerde hızlı bellekte tutulacağına ilişkin stratejilerdir. Bunlara İngilizce "cache replacement" algoritmaları denilmektedir. Bir cache sistemi donanımsal ya da yazılımsal biçimde oluşturulabilmektedir. Donanımsaşl cache sistemlerinde gerçekleştirim elektrik devreleriyle yapılmıştır dolayısıyla gebellikle yazılımsal bir müdahale söz konusu olamamaktadır. Yazılımsal cache sistemleri ise tamamen programalama yoluyla oluşturulmuş cache sistemleridir. Sistem programlama faaliyetlerinde cache sistemleri değişik biçimlerde karşımıza çıkabilmektedir. Bunlara birkaç örnek vermek istiyoruz: -> Bugün bilgisayar sistemlerinde genellikle ana bellek olarak DRAM (Dynamic RAM) denilen bellekler kullanılmaktadır. DRAM belleklerde belleğin bir biti tipik olarak bir kapasitif elemanla bir transistörden oluşturulmaktadır. Bu yapı DRAM'ların az yer kaplamasını ve ucuz olmasını sağlamaktadır. Bugün kullandığımız DRAM bellekler 10 nanosaniyeye kadar hızlandırılmış durumdadır. Eskiden (80'li yılların oralarına kadar) CPU'lar DRAM belleklerden daha yavaştı. Bu yıllarda dolayısıyla CPU'lar DRAM bellekleri beklemiyordu. Ancak 80'li yılların sonlarına doğru CPU'lar DRAM bellekleri hız bakımından geçti. Bu durumda CPU DRAM'dan bir bilgi talep ettiğinde o bilgi DRAM'dan gelene kadar bekliyordu (buna CPU terminolojisinde "wait state" denilmektedir) İşte 80'li yılların sonlarına doğu yavaş yavaş kişisel bilgisauarlarda DRAM beklemeleri azaltmak için donanımsal biçimde çalışan cache bellekler oluşturulmaya başlandı. Bu tasarımda SRAM (static RAM) teknolojisi ile üretilen hızlı RAM'ler cache olarak kullanılıyordu. (SRAM'lerde bir bir tipik olarak SR Latch denilen flip flop devresi ile gerçekleştirilmektedir. Bu tasarım bir bir için fazla sayıda transistörün kullanılmasına yol açmaktadır. Böylece SRAM'ler hem daha fazla yer kaplamakta hem de daha pahalı olmaktadır.) İşte o yıllarda artık CPU'lar önce cache balleğe vuruyor eğer cache "miss olursa" DRAM bellekten bilgiyi alıyordu. Daha sonraları CPU'lar daha da hızlanınca artık CPU'ların içerisine de SRAM olarak üretilmiş cache bellekler yerleştirilmeye başlandı. Böylece çok kademeli cache sistemleri uygulandı. CPU bilgiyi önce en hızlı biçimde kendi içerisindeki L1 cache denilen bellekte arıyor orada bulamazsa CPU dışındaki L2 cache cache denilen belleğe başvuruyordu. Eğer bilgi L2 cache'te de bulunamazsa DRAM erişimi yapılıyordu. Bugünkü bilgisayar sistemlerinde genellikle 3 kademe cache kullanılmaktadır. L1 ve L2 cache'ler CPU içerisinde L3 cache CPU dışında konuşlandırılmaktadır. Çok çekirdekli sistemlerde her çekirdeğin ayrı bir L1 ve L2 cache'i bulunmaktadır. -> Çok karşılaşılan bir cache sistemi de işletim sistemleri tarafından oluşturulan ve ismine "disk cache" sistemi, "buffer cache" sistemi ya da "page cache" denilen cache sistemleridir. İşletim sistemleri yazılımsal olarak diskte okunan blokları RAM'de bir cache alanında tutmaktadır. Böylece bir disk okuması yapılmak istendiğinde önce RAM'deki bu cache'e başvurulmakta blok zaten orada varsa hiç okuması yapılmadan oradan alınmaktadır. Bu disk cache (buffer cache) sistemi işletim sistemlerinin performanslarında önemli bir etkiye sahiptir. Çünkü bilgisayar sistemlerinin en yavaş bileşenlerinden biri disk sistemidir. Bu cache sisteminde yavaş bellek diski hızlı bellek DRAM belleği temsil etmektedir. -> İşletim sistemlerinin çekirdekleri disk cache (buffer cache) sisteminin yanı sıra son işleme sokulan dosya bilgilerinin saklandığı "i-node cache" ve son işleme sokulan dizin girişlerinin saklandığı "directory entry cache (dentry cache)" denilen cache sistemlerini de kullanmaktadır. Böylece bir dosya işlemi yapılırken dosya bilgilerine hızlı bir biçimde erişilebilmektedir. -> Web tarayıcıları da kendi içerisinde bir cache sistemi kullanabilmektedir. Bir web sayfasına eriştiğimizde o sayfanın içeriği tarayıcının cache sisteminde bulunuyor olabilir. Bu durumda doğrudan sayfa kullanıcıya gösterilebilir. Burada yavaş bellek web sayfasının barındırıldığı sunucuyu, hızlı bellek ise yerel bilgisayarı temsil etmektedir. -> Dağıtık uygulamalarda da cache sistemleriyle çokça karşılaşılmaktadır. Örneğin bir sosyal medya uygulamasında resimler o coğrafi bölgeye yakın olan daha hızlı erişilebilen CDN denilem server'larda tutulabilmektedir. -> Standart C kütüphaenesindeki dosya fonksiyonları da ileride detaylı biçimde ele alacağımız gibi bir cache sistemi kullanmaktadır. -> Manyetik temelli Hard Disklerde de bir cache sistemi bulundurulmaktadır. Sistem programcısı HDD'den bir blok okumak istediğinde eğer o blok HDD'nin kendi cache'i içerisinde varsa doğrudan oradan alınmaktadır. Eğer yoksa disk kafaları hareklet ettirilip bilgi elektro mekanik bir biçimde diskten elde edilmektedir. Cache bellek genel olarak bloklardan (yani ardışıl byte topluluklarından) oluşmaktadır.Her bloğu yavaş belleğin farklı bir yerini tutabilmektedir. Eğer cache sisteminde cache bellek yalnızca okuma amaçlı kullanılıyorsa yazma işlemi doğruan yavaş belleğe yapılıyorsa bu tür cache sistemlerine "read only cache sistemleri" denilmektedir. Eğer yazma işlemi de cache belleğe yapılabiliyorsa bu tür cache sistemlerine de "read-write sistemleri" ya da "write throuh cache sistemleri" denilmektedir. Şüphesiz read-write cache sistemleri daha yüksek bir performans sunmaktadır. Ancak bu sistemlerde birtakım anomaliler oluştuğunda cache belleğe yazılan ancak yavaş belleğe henüz aktarılmamış bilgilerin kaybedilme olasılığı vardır. Read-write cache sistemlerinde bir cache bloğu cache'ten atılacağı zaman onun daha önce değiştirilip değiştirilmediğine bakılması gerekir. Eğer o cache bloğuna yazma yapılmışsa o cache bloğu cache'ten atılmadan önce yavaş belleğe yazılmalıdır. Genel olarak cache terminolojisinde bir cache bloğunun içeriğinin güncellenmiş olmasına o cache bloğunun ""kirlenmiş (dirty)" olması denilmektedir. Bir cache bloğunun yavaş belleğe içeriği değiştiğinden dolayı geri yazılmasına "flush" işlemi de denilmektedir. Pekiyi cache blokları ne zaman flush edilmelidir? Bir cache bloğu cache'ten atılacağı zaman eğer kirlenmişse flush edilmelidir. Ancak kirlenmemişse onun flush edilmesine gerek yoktur. Ancak kirlenmiş cache bloklarının uzun süre flush edilmeden bekletilmesi de anomali durumlarında bilgi kayıplarına yol açabilmektedir. Örneğin işletim sistemlerinin disk cache (buffer cache) sistemleri read-write cache sistemleridir. Ancak kirlenmiş cache blokları çok da fazla bekletilmeden belli periyotlarla çeşitli kernel thread'ler tarafından diske flush edilmektedir. Bu tür yazımlara "delayed write" denilmektedir. Yukarıda da belirttiğimiz gibi cache bellek tipik olarak blok ya da "line" denilen ardışıl byte topluluklarındanm oluşmaktadır. Genel olarak cache'teki her bir bloğun bir numarası vardır. Örneğin elimizde 100K'lık bir cache olsun. Bir cache bloğu da 1K'dan oluşsun. Bu cache içeisinde toplam 100 tane cache bloğu vardır. İşte bu bloklara sırasıyla numaraler verilmektedir. * Örnek 1.0, Cache 0. Blok (1K) 1. Blok (1K) 2. Blok (1K) ... 97. Blok (1K) 98. Blok (1K) 99. Blok (1K) * Örnek 1.1, Yavaş belleğin de 1MB olduğunu varsayalım. İşte yavaş bellek de yine cache bloklarında olduğu gibi bloklara ayrılmaktadır: Yavaş Bellek 0. Blok (1K) 1. Blok (1K) 2. Blok (1K) ... 997. Blok (1K) 998. Blok (1K) 999. Blok (1K) Bu örnekte toplam 1000 bloktan oluşan yavaş belleğin ardışıl olmayan 100 bloğu cache bellekte tutulmaktadır. Bu sistem yazılımsal olarak oluşturulacaksa bizim cache bloklerının ahngisinin yavaş belleğin hangi bloğundaki bilgiyi tuttuğunu bir biçimde izlememiz gerekir. Bu sistemde örneğin biz yavaş belleğin 718'inci bloğuna erişmek isteyelim. Sistemin önce bu 718'inci bloğun cache içerisinde olup olmadığına bakması gerekir. Örneğin bu 718'inci bloğun içeriği cache belleğin 54'üncü bloğunda olabilir. O zaman erişim hızlı bir biçimde cache'in 54'üncü bloğundan sağlanacaktır. Ancak eğer bu 718'inci blok cache'te yoksa bu durumda bu blok gerçekten yavaş belleğin 718'inci bloğundan elde edilecektir. Mademki her blok işleminde önce o blok cache'te mi diye bakılmaktadır o halde bu arama işleminin hızlı yapılması gerekir. Bu tür sistemlerde sıralı arama (sequential search) iyi bir fikir değildir. Böylesi aramalarda tipik olarak "hash tabloları (hash table)" denilen veri yapıları tercih edilmektedir. Kursumuzun algortimaalar ve veri yapıları kısmında hash tabloları incelenmektedir. Yukarıda açıkladığımız gibi bir cache sistemi olsun. Yavaş belleğin 1000 bloktan cache belleğin ise 100 bloktan oluştuğunu varsayalım. Cache sistemlerindeki en önemli algoritmik sorun şudur: Biz yavaş belleğin hangi bloklarını cache bellekte tutmalıyız? Yani yukarıdaki örnekte 1000 blokluk yavaş belleğin hangi 100 bloğunu cache bellekte tutmalıyız? Bizim amacımız "cache hit" oranını yükseltmektir. Eğer biz yavaş belleğin nerelerinin çok kullanıldığını önceden biliyor olsak problem çok basit olurdu. Bu duurmda en çok kullanılacak blokları cache'te tutardık. Ancak böyle bir bilgiye genellikle sistem programcıları sahip olmazlar. Cache sistemlerinde genellikle cache bellekteki bloklar dinamik bir biçimde kullanılmaktadır. Yani duruma göre bir cache bloğunun içeriği atılıp yavaş belleğin daha uygun bir bloğu cache'e alınabilmektedir. Yavaş belleğin hangi bloklarının cache'te tutulacağına ilişkin algoritmalara İngilizce "cache replacement" algortimaları denilmektedir. Biz buna "cache algoritmaları" diyeceğiz. Literatürde değişik durumlar için düşünülmüş çeşitli cache algoritmaları vardır. Biz burada en fazla kullanılan algoritmaları tanıtacağız. -> En Az Kullanılanın Cache'ten Atılması Algoritması (Least Frequently Used (LFU)): Bu yöntemde yavaş bellekten okunan bloklar cache'e alınır. Cache'teki her blok için bir sayaç tutulur. Her "cache hit" oluştuğunda o bloğun sayacı bir artırlır. Cache dolduğunda ve bir "cache miss" oluştuğunda o zamana kadar en az kullanılan blok cache'ten çıkarılır. Cache miss olan yavaş bellek bloğu cache'e alınır. Böylece cache'te o zamana kadar fazla kullanılan bloklar tutulmuş olur. Buradaki varsayım şudur: Şimdiye kadar az kullanılmış olan bloklar bundan sonra da az kullanılacaktır. Bu algoritmada "thrashing" denilen bir problemin giderilmesi gerekmektedir. Cache'e yeni çekilen bloğun başlangıç sayacı 0'da tutulursa ilk cache miss olduğunda bu blok yeniden cache'ten atılır. O halde cache'e alınan bloğun başlangıç sayacının 0 yapılması uygun değildir. -> Son Zamanlarda En Az Kullanılanın Cache'ten Atılması (Least Recently Used (LRU)): Açık ara en fazla tercih edilen cache algoritması budur. Örneğin Linux çekirdeğindeki çeşitli cache sistemlerinde hep bu algortima kullanılmaktadır. Bu algoritmada cache'ten blok çıkatılacağı zaman onların toplam hit sayaçlarına bakılmaz. Onların son zamanlarda çok kullanılıp kullanılmadığına bakılır. Bilgisayar sistemlerinde tipik çalışmada birtakım bloklar bir zaman dilimi içerisinde çok kullanılıp sonra bir daha seyrek kullanılmakta ya da hiç kullanılmamaktadır. Örneğin işletim sisteminin disk cache sistemini düşünelim. Bir program çalıştığında o program belli disk bloklarını sürekli kullanıyor olabilir. Ancak programın çalışması bittiğinde artık o bloklar çok seyrek kullanılıyor olacaktır. Bu algoritma tipik olarak bri bağlı liste ile gerçekleştirilmektedir. Cache hit olan bloklar bağlı listede öne çekilir. Böylece son zamanlara en az kullanılan bloklar bağlı listenin sonunda kalır. Cache'ten blok çıkartılacağı zaman bağlı listenin sonundaki bloklar çıkartılır. * Örnek 1, Aşağıda read-only LRU cache sistemine bir örnek verilmiştir. Örneğimizde diski temsil eden "test.dat" isimli her bloğu 32 byte olan 100 bloktan oluşaşn (toplam 3200 byte) bir dosya kullanılmaktadır. İşin başında bu dosyaya rastgele text içerik atanmıştır. Burada oluşturulan cache sistemindeki fonksiyonlar şunlardır: HCACHE open_disk(const char *path, int flags); int read_disk(HCACHE hc, int blockno, void *buf); int close_disk(HCACHE hc); open_disk fonksiyonu "test.dat" dosyasını açar, cache veri yapısına ilkdeğerlerini verir. read_disk fonksiyonu diskten cache sistemini kullanarak bir blok okumaktadır. close_disk fonksiyonu ise cache sistemini kapatmaktadır. Kullanılan cache veri yapısı şöyledir: typedef struct tagCACHELINE { char buf[LINE_SIZE]; int blockno; size_t count; } CACHE_LINE; typedef struct tagCACHE { int fd; CACHE_LINE clines[NCACHE_LINES]; size_t tcount; } CACHE, *HCACHE; Burada cache sistemi CACHE isimli bir yapıyla temsil edilmiştir. Her cache bloğu CACHE_LINE isimli bir yapıyla temsil edilmiştir. Tüm cache blokları CACHE_LINE türünden bir dizide toplanmıştır. Her cache bloğunda cache içeriği, diskteki blok numarası ve hit sayacı tutulmaktadır. Buradaki simülasyon kodundaki algoritmik kusurlar şunlardır; Okunmak istenen bloğun tek tek cache bloklarında sıralı bir biçimde aranması. Tabii buradaki simülasyonda 10 tane cache bloğu vardır. Dolayısıyla sıralı arama çok hızlı bir biçimde gerçekleştilmektedir. Az sayıda (örneğin 10 civarında) elemanın bulunduğu durumda sıralı arama iyi bir yöntemdir. Yukarıda da belirttiğimiz gibi arama işlemi için genellikle "hash tabloları" kullanılmaktadır. Bir diğer kusur ise en düşük sayaca sahip olan cache bloğu da sıralı arama ile bulunmasıdır. Aşağıda bu konuya ilişkin program kodları verilmiştir: /* diskcache.h */ #ifndef DISKCACHE_H_ #define DISKCACHE_H_ /* Symbolic Constants */ #define LINE_SIZE 32 #define NCACHE_LINES 10 #define INITIAL_COUNT 2 #ifdef DEBUG #define DEBUG_PRINT(fmt, ...) fprintf(stderr, fmt, ## __VA_ARGS__) #else #define DEBUG_PRINT(fmt, ...) #endif /* Type Definitions */ typedef struct tagCACHELINE { char buf[LINE_SIZE]; int blockno; size_t count; } CACHE_LINE; typedef struct tagCACHE { int fd; CACHE_LINE clines[NCACHE_LINES]; size_t tcount; } CACHE, *HCACHE; /* Function Prototypes */ HCACHE open_disk(const char *path, int flags); int read_disk(HCACHE hc, int blockno, void *buf); void close_disk(HCACHE hc); #endif /* diskcache.c */ #define DEBUG #include #include #include #include #include #include #include "diskcache.h" #ifdef DEBUG static void print_cline_counts(HCACHE hc); #endif static size_t select_line(HCACHE hc); HCACHE open_disk(const char *path, int flags) { HCACHE hc; int i; if ((hc = (HCACHE)malloc(sizeof(CACHE))) == NULL) return NULL; if ((hc->fd = open(path, flags)) == -1) { free(hc); return NULL; } for (i = 0; i < NCACHE_LINES; ++i) { hc->clines[i].blockno = -1; hc->clines[i].count = 0; } hc->tcount = 0; return hc; } int read_disk(HCACHE hc, int blockno, void *buf) { int i; int rline; for (i = 0; i < NCACHE_LINES; ++i) if (hc->clines[i].blockno == blockno) { DEBUG_PRINT("Cache hit block %d, used cache line %d\n", blockno, i); memcpy(buf, hc->clines[i].buf, LINE_SIZE); ++hc->clines[i].count; ++hc->tcount; #ifdef DEBUG print_cline_counts(hc); #endif return 0; } rline = select_line(hc); if (lseek(hc->fd, (off_t)blockno * LINE_SIZE, SEEK_SET) == -1) return -1; if (read(hc->fd, hc->clines[rline].buf, LINE_SIZE) == -1) return -1; hc->clines[rline].blockno = blockno; hc->clines[rline].count = hc->tcount / NCACHE_LINES + 1; hc->tcount += hc->clines[rline].count; memcpy(buf, hc->clines[rline].buf, LINE_SIZE); DEBUG_PRINT("Cache miss block %d, used cache line %d\n", blockno, rline); #ifdef DEBUG print_cline_counts(hc); #endif return 0; } void close_disk(HCACHE hc) { close(hc->fd); DEBUG_PRINT("Cache closed!\n"); free(hc); } static size_t select_line(HCACHE hc) { size_t min_count; size_t min_index; int i; min_count = hc->clines[0].count; min_index = 0; for (i = 1; i < NCACHE_LINES; ++i) { if (hc->clines[i].count < min_count) { min_count = hc->clines[i].count; min_index = i; } } return min_index; } #ifdef DEBUG static void print_cline_counts(HCACHE hc) { int i; putchar('\n'); printf("Total count: %llu\n", (unsigned long long)hc->tcount); for (i = 0; i < NCACHE_LINES; ++i) printf("Cachle Line %d --> Block: %d, Count: %llu\n", i, hc->clines[i].blockno, (unsigned long long)hc->clines[i].count); putchar('\n'); } #endif /* diskcache-test.c */ #include #include #include #include #include "diskcache.h" int create_test_file(const char *path, int nblocks); int main(void) { HCACHE hc; char buf[32 + 1]; int blockno; if (create_test_file("test.dat", 100) == -1) { perror("create_test_file"); exit(EXIT_FAILURE); } if ((hc = open_disk("test.dat", O_RDONLY)) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } for (;;) { printf("Block No:"); scanf("%d", &blockno); putchar('\n'); if (blockno == -1) break; if (read_disk(hc, blockno, buf) == -1) { perror("read_file"); exit(EXIT_FAILURE); } buf[32] = '\0'; printf("Block content: %s\n\n", buf); } close_disk(hc); return 0; } int create_test_file(const char *path, int nblocks) { FILE *f; int i, k; char buf[LINE_SIZE]; srand(time(NULL)); if ((f = fopen(path, "wb")) == NULL) return -1; for (i = 0; i < nblocks; ++i) { sprintf(buf, "%04d ", i); for (k = 5; k < 32; ++k) buf[k] = rand() % 26 + 'A'; if (fwrite(buf, LINE_SIZE, 1, f) != 1) return -1; } fclose(f); } -> En Fazla Kullanılanın Cache'ten Atılması (Most Frequenly Used (MRU)): Bu algroritma LFU algoritmasının tersini yapmaktadır. Yani toplamda o zamana kadar en fazla kullanılan bloğu cache'ten atmaktadır. Bu yöntem ilk başta saçma gelebilir. Ancak bazı seyrek sistemlerde her bloğun yaklaşık eşit sayıda kullanılacağı baştan biliniyor olabilir. Bu durumda o zamana kadar çok hit almış cache blokları gelecekte daha az kullanılma potansiyilene sahiptir. Tabii bu algoritma çok seyrek kullanılmaktadır. -> Rastgele Blokların Cache'ten Atılması (Random Cache Replacement): Bazı sistemlerde hiçbir öngürü yapılamamaktadır. Bu durumda rastgele blokların cache'ten atılması uygun olabilmektedir. Diğer yandan read-write cache sistemlerinde cache belleğin yazma sırasında da kullanıldığını belirtmiştik. Yani bir blok yazılmak istendiğinde eğer o blok cache'te varsa doğrudan cache bloğu üzerine yazma yapılır. Tabii bu durumda bu blok atılacağı zaman güncellenmiş (kirlenmiş) olduğu için yavaş belleğe geri yazılmalıdır. Pekiyi yazma sırasında "cache miss" olursa ne yapılmalıdır? Burada iki strateji izlenebilir: -> Blok doğrudan yavaş belleğe yazılabilir. -> Blok önce cache'e çekilip yazma cache'e yapılabilir. Tabii burada aslında yavaş bellekteki bloğun cache'e çekilmesine gerek yoktur. Doğurdan yazma cache üzerine yapılabilir. Bazen yazma sırasında "cache hit" olduğu durumda yazma işlemi hem cache'e ham de yavaş belleğe yapılabilmektedir. Böylece anomalilerde yavaş bellekte bir kayıp oluşmayacaktır. * Örnek 1, Aşağıdaki örnekte daha önce yapmış olduğumuz disk cache simülasyonundaki cache sistemi read/write cache haline gtirilmiştir. /* filecache.h */ #ifndef FILECACHE_H_ #define FILECACHE_H_ /* Symbolic Constants */ #define LINE_SIZE 32 #define NCACHE_LINES 10 #define INITIAL_COUNT 2 #ifdef DEBUG #define DEBUG_PRINT(fmt, ...) fprintf(stderr, fmt, ## __VA_ARGS__) #else #define DEBUG_PRINT(fmt, ...) #endif /* Type Definitions */ typedef struct tagCACHELINE { char buf[LINE_SIZE]; int blockno; size_t count; int dirty; } CACHE_LINE; typedef struct tagCACHE { int fd; CACHE_LINE clines[NCACHE_LINES]; size_t tcount; } CACHE, *HCACHE; /* Function Prototypes */ HCACHE open_disk(const char *path, int flags); int read_disk(HCACHE hc, int blockno, void *buf); int write_disk(HCACHE hc, int blockno, const void *buf); int fluch_disk(HCACHE hc); int close_disk(HCACHE hc); #endif /* filecahe.c */ #define DEBUG #include #include #include #include #include #include #include "diskcache.h" #ifdef DEBUG static void print_cline_counts(HCACHE hc); #endif static size_t select_line(HCACHE hc); HCACHE open_disk(const char *path, int flags) { HCACHE hc; int i; if ((hc = (HCACHE)malloc(sizeof(CACHE))) == NULL) return NULL; if ((hc->fd = open(path, flags)) == -1) { free(hc); return NULL; } for (i = 0; i < NCACHE_LINES; ++i) { hc->clines[i].blockno = -1; hc->clines[i].count = 0; hc->clines[i].dirty = 0; } hc->tcount = 0; return hc; } int read_disk(HCACHE hc, int blockno, void *buf) { int i; int rline; for (i = 0; i < NCACHE_LINES; ++i) if (hc->clines[i].blockno == blockno) { DEBUG_PRINT("Cache hit block %d, used cache line %d\n", blockno, i); memcpy(buf, hc->clines[i].buf, LINE_SIZE); ++hc->clines[i].count; ++hc->tcount; #ifdef DEBUG print_cline_counts(hc); #endif return 0; } rline = select_line(hc); if (hc->clines[rline].dirty) { if (lseek(hc->fd, hc->clines[rline].blockno * LINE_SIZE, SEEK_SET) == -1) return -1; if (write(hc->fd, hc->clines[rline].buf, LINE_SIZE) == -1) return -1; hc->clines[rline].dirty = 0; } if (lseek(hc->fd, (off_t)blockno * LINE_SIZE, SEEK_SET) == -1) return -1; if (read(hc->fd, hc->clines[rline].buf, LINE_SIZE) == -1) return -1; hc->clines[rline].blockno = blockno; hc->clines[rline].count = hc->tcount / NCACHE_LINES + 1; hc->tcount += hc->clines[rline].count; memcpy(buf, hc->clines[rline].buf, LINE_SIZE); DEBUG_PRINT("Cache miss block %d, used cache line %d\n", blockno, rline); #ifdef DEBUG print_cline_counts(hc); #endif return 0; } int write_disk(HCACHE hc, int blockno, const void *buf) { int rline; for (int i = 0; i < NCACHE_LINES; ++i) if (hc->clines[i].blockno == blockno) { DEBUG_PRINT("Cache hit block %d, used cache line %d\n", blockno, i); memcpy(hc->clines[i].buf, buf, LINE_SIZE); ++hc->clines[i].count; ++hc->tcount; hc->clines[i].dirty = 1; #ifdef DEBUG print_cline_counts(hc); #endif return 0; } rline = select_line(hc); if (hc->clines[rline].dirty) { if (lseek(hc->fd, hc->clines[rline].blockno * LINE_SIZE, SEEK_SET) == -1) return -1; if (write(hc->fd, hc->clines[rline].buf, LINE_SIZE) == -1) return -1; } memcpy(hc->clines[rline].buf, buf, LINE_SIZE); hc->clines[rline].blockno = blockno; hc->clines[rline].count = hc->tcount / NCACHE_LINES + 1; hc->clines[rline].dirty = 1; DEBUG_PRINT("Cache miss block %d, used cache line %d\n", blockno, rline); #ifdef DEBUG print_cline_counts(hc); #endif return 0; } int flush_disk(HCACHE hc) { for (int i = 0; i < NCACHE_LINES; ++i) { if (hc->clines[i].dirty) { if (lseek(hc->fd, hc->clines[i].blockno * LINE_SIZE, SEEK_SET) == -1) return -1; if (write(hc->fd, hc->clines[i].buf, LINE_SIZE) == -1) return -1; } } return 0; } int close_disk(HCACHE hc) { if (flush_disk(hc) == -1) return -1; close(hc->fd); DEBUG_PRINT("Cache closed!\n"); free(hc); return 0; } static size_t select_line(HCACHE hc) { size_t min_count; size_t min_index; int i; min_count = hc->clines[0].count; min_index = 0; for (i = 1; i < NCACHE_LINES; ++i) { if (hc->clines[i].count < min_count) { min_count = hc->clines[i].count; min_index = i; } } return min_index; } #ifdef DEBUG static void print_cline_counts(HCACHE hc) { int i; putchar('\n'); printf("Total count: %llu\n", (unsigned long long)hc->tcount); for (i = 0; i < NCACHE_LINES; ++i) printf("Cachle Line %d --> Block: %d, Count: %llu, Dirty: %d\n", i, hc->clines[i].blockno, (unsigned long long)hc->clines[i].count, hc->clines[i].dirty); putchar('\n'); } #endif /* filecache-test.c */ #include #include #include #include #include "diskcache.h" int create_test_file(const char *path, int nblocks); int main(void) { HCACHE hc; char buf[32 + 1]; int rw; int blockno; if (create_test_file("test.dat", 100) == -1) { perror("create_test_file"); exit(EXIT_FAILURE); } if ((hc = open_disk("test.dat", O_RDWR)) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } for (;;) { printf("Block No:"); scanf("%d", &blockno); if (blockno == -1) break; printf("(r)ead/(w)rite?"); while (getchar() != '\n') ; rw = getchar(); if (rw == 'r') { if (read_disk(hc, blockno, buf) == -1) { perror("read_file"); exit(EXIT_FAILURE); } } else if (rw == 'w') { for (int k = 5; k < 32; ++k) buf[k] = rand() % 26 + 'A'; if (write_disk(hc, blockno, buf) == -1) { perror("read_file"); exit(EXIT_FAILURE); } } else { printf("invalid operation!...\n"); continue; } buf[32] = '\0'; printf("Block content: %s\n\n", buf); } close_disk(hc); return 0; } int create_test_file(const char *path, int nblocks) { FILE *f; int i, k; char buf[LINE_SIZE]; srand(time(NULL)); if ((f = fopen(path, "wb")) == NULL) return -1; for (i = 0; i < nblocks; ++i) { sprintf(buf, "%04d ", i); for (k = 5; k < 32; ++k) buf[k] = rand() % 26 + 'A'; if (fwrite(buf, LINE_SIZE, 1, f) != 1) return -1; } fclose(f); } > Hatırlatıcı Notlar: >> Bazen "cache" sözcüğü ile "buffer (tampon)" sözcükleri biribirine karıştırılmaktadır. Cache sistemleri hızlandırma amacıyla kullnılan sistemlerdir. Halbuki buffer mekanizması "bilgileri sırası bozulmadan geçici süre tutmak için oluşturulmuş" sistemlerdir /*================================================================================================================================*/ (32_07_10_2023) > İşlemcilerin Koruma Mekanizması: Bugün kullandığımız güçlü mikroişlemciler "koruma mekanizması (protection mechanism)" denilen önemli bir özelliğe sahiptir. Örneğin A serisi ARM işlemcileri, Intel işlemcileri, Power PC işlemcileri, Alpha işlemcileri, Itanium işlemcileri koruma mekanizmalarına sahiptir. Ancak mikrodenetleyiciler ve küçük kapasiteli işlemcilerde bu mekanizma bulunmamaktadır. Intel işlemcileri 80286 modelleriyle birlikte segment tabanlı, 80386 modelleriyle birlikte sayfa tabanlı koruma mekanizmasına sahip olmuşlardır. A (Applivation) serisi cortex'lere sahip ARM işlemcilerinde koruma mekanizması vardır. Ancak M (Microcontroller) cortext'lerine sahip ARM işlemcilerinde genel olarak bu mekanizma bulunmamaktadır. Bugün Windows, Linux, macOS gibi işletim sistemleri ancak koruma mekanizmasına sahip işlemcilerde çalışabilmektedir. Koruma mekanizmasının temelde iki işlevi vardır: -> Bellek Koruması -> Komut Koruması Bu korumalarda, >> Bellek Koruması: Çok prosesli işletim sistemlerinde farklı programlar aynı anda RAM'de bulunmaktadır. Bir program kendi bellek alanının dışına çıkıp başka programların (ve hatta işletim sisteminin) bellek alanına erişip orada değişiklikler yaparsa bundan tüm sistem olumsuz etkilenebilir, diğer prgramın çalışması bozulabilir, o programlar yaznlı çalışabilirler. Bir programın değişiklik yapmadan başka bir programın bellek alanına erişmesi de tehlikeldir. Bu sayede kötü amaçlı programlar başka programın çalışması hakkında casusluk yapabilirler. Halbuki C'de bir göstericiye RAM'deki herhangi bir adresi yerleştirip RAM'in o bölgesine erişebiliriz. Pekiyi bir programın kendi bellek alanı dışına erişmesi nasıl engellenebilir? Bu engelleme yalnızca işletim sistemi tarafından yapılamaz. Engellemenin en alt düzeyde işlemci tarafından yapılması gerekir. Bir program kendi bellek alanının dışına eriştiği zaman bu erişim birinci elden işlemci tarafından tespit edilir. İşlemci bu durumu işletim sistemine bildirir. İşletim sistemi de programı cezalandırarak sonlandırır. >> Komut Koruması: Bazı makine komutları uygunsuz kullanıldığında tüm sistemin çökmesine yol açabilmektedir. İşte işlemcilerin koruma mekanizmaları bu makine komutlarını da denetlemektedir. Buna "komut koruması" denilmektedir. Bu tür komutları kullanan programlar işlemci tarafından tespit edilir. İşlemci durumu işletim sistemine bildirir. İşletim sistemi de programı cezalandırarak sonlandırır. Yukarıda açıkladığımız koruma mekanizmasının işlemci tarafından nasıl sağlandığı ve ihlallerin işletim sistemine nasıl iletildiği kişiler tarafından merak edilmektedir. Bu konu kursumuzun konusu dışındadır. Mekanizmanın ayrıntıları "Sembolik Makine Dili" kurslarında ele alınmaktadır. Öte yandan işletim sistemleri bellekte her yere erişebilmeli her makine komutunu kullanabilmeldir. Aksi takdirde faaliyetlerini yürütemezler. O halde koruma mekanizması işletim sistemi için uygulanmamalı yalnızca sıradan uygulama programları için uygulanmalıdır. İşte işlemcileri tasarlayanlar genellikle çalışmakta olan akış için iki çalışma modu kullanırlar: -> Kernel mod -> User mod. Bir kod kernel modda çalışıyorsa ona koruma mekanizması uygulanmaz. Böylece kernel modda çalışan programlar bellekte her yere erişebilirler her makine komutunu kullanabilirler. Ancak user modda çalışan kodlara koruma mekanizması uygulanmaktadır. Programların hemen hepsi user modda çalımaktadır. örneğin Windows'taki Excel, Word, Visual Studio, Internet Explorer vs hep user modda çalışırlar. Benzer biçimde UNIX/Linux sistemlerindeki programlar kabuğun kendisi (örneğin bash), dışsal komutlar user modda çalışırlar. UNIX/linux sistemlerindeki root proseslerin bu konuyla hiçbir ilgisi yoktur. Yani bu sistemlerde biz bir programı sudo yaparak çalıştırdığımızda yine o program user modda çalışmaktadır. Örneğin, bir işletim sisteminin kodları, aygıt sürücü kodları kernel modda çalışmaktadır. Programcı bir kod parçasının koruma engeline takılmadan kernel modda çalışmasını istiyorsa onu "aygıt sürücüsü (device driver)" olarak yazmalıdır. Intel işlemcilerinde dört çalışma modu vardır. Bunlara "ring" de denilmektedir. Bu çalışma modlarına 0, 1, 2, 3 biçiminde numara verilmiştir. Korumanın hiç uygulanmadığı mod 0, korumanın tam uygulandığı mod 3'tür. Windows, Linux ve macOS sistemleri Intel işlemcilerinin iki modunu kullanmaktadır: 0 ve 3. Yani her ne kadar Intel işlemcilerinde 4 çalışam modu olsa da aktif olarak zaten iki mod kullanılmaktadır. Pekiyi user mod bir program işletim sisteminin istem fonksiyonunu çağırdığında ne olur? İşletim sisteminin sistem fonksiyonları bellekte her yere erişebilmekte ve özel makine komutlarını kullanabilmektedir. Eğer user mod program işletim sisteminin sistem fonksyonarını user modda çalıştırsa hemen koruma mekanizmasına takılırdı. İşte user mod bir program işletim sisteminin sistem fonksiyonlarını çağırdığında geçici süre otomatik olarak user mod'tan kernel mod'a geçirilmektedir. Böylece sistem fonksiyonları kernel modda çalıştırılmaktadır. Sistem fonksiyonundan çıkılırken yeniden user moda geri dönülmektedir. Bu duruma "prosesin user mod'tan kernel moda geçmesi" ve "kernel moddan user moda geri dönmesi" denilmektedir. Tabii kernel moda geçişin ve geri dönüşün bir zaman maliyeti vardır. Yani işletim sisteminin sistem fonksiyonlarını çağırmanın diğer fonksiyonları çağırmaktan zamansal bakımdan daha maliyetli olduğu söylenebilir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, Bir dosyanın bütün byte'ları üzerinde bir işlem yapacak olalım. Dosyanın her byte'ını tek tek read fonksiyonu ile aşağıdaki gibi okumaya çalışmak kötü bir tekniktir: while ((result = read(fd, &ch, 1)) > 0) { /* ... */ } Çünkü her byte için read fonksiyonun çağrılması (read fonksiyonu da sistem fonksiyonunu doğrudan çağırmaktadır) user mod, kernel mod geçişleri nedneiyle zaman kaybının oluşmasına yol açar. Bu durumda ilk akla gelen alternatif yöntem okuma işlmelerini blok blok yapmaktır. Örneğin: while ((result = read(fd, buf, 8192)) > 0) for (ssize_t i = 0; i < result; ++i) { /* ... */ } Burada sistem fonksiyonu çok daha az çağrılacak ve önemli bir hız kanacı oluşacaktır. Aşağıda bu durumun simülasyonuna ilişkin bir örnek verilmiştir. Örneğimizde içi sıfırlarla dolu 5120000 uzunluğunda bir dosya kullanılmıştır. Bu dosya aşağıdaki gibi yaratılabilir: dd if=/dev/zero of=test.dat bs=512 count=10000 Aşağıdaki örnekte test1 programı her byte için read fonksiyonunu çağırmıştır. test2 programı ise 8192 byte'lık bloklarlarla okumayaı yapmıştır. test1 programının çalışma süresi 3.040 saniye, test2 programının çalışma süresi ise 0.014 saniye sürmüştür. İki yöntem arasında yaklaşık 300 kat bir hız farkı vardır. /* test1.c */ #include #include #include #include #include void exit_sys(const char *msg); void proc(int ch) { /* EMPTY */ } int main(int argc, char *argv[]) { int fd; char ch; ssize_t result; clock_t start, stop; double telapsed; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } start = clock(); if ((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fd, &ch, 1)) > 0) proc(ch); if (result == -1) exit_sys("read"); close(fd); stop = clock(); telapsed = (double) (stop - start) / CLOCKS_PER_SEC; printf("%.3f\n", telapsed); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* test2.c */ #include #include #include #include #include void exit_sys(const char *msg); void proc(int ch) { /* empty */ } int main(int argc, char *argv[]) { int fd; ssize_t result; clock_t start, stop; double telapsed; char buf[8192]; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } start = clock(); if ((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); while ((result = read(fd, buf, 8192)) > 0) for (ssize_t i = 0; i < result; ++i) proc(buf[i]); if (result == -1) exit_sys("read"); close(fd); stop = clock(); telapsed = (double) (stop - start) / CLOCKS_PER_SEC; printf("%.3f\n", telapsed); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Biz kendi programımızın belleğin her yerine erişmesini istiyorsak, onun özel makine komutlarını kullanmasını sağlamak isiyorsak programamızı "aygıt sürücü (device driver)" biçiminde organize etmeliyiz. Tabii aygıt sürücüleri sisteme sıradan kullanıcılar yükleyememektedir. Aygıt sürücüler sistemin yöneticileri tarafından (örneğin Linux'ta root kullanıcısı tarafından) yüklenebilmektedir. Aygıt sürücüler sistemin boot edilmesi sırasında işletim sisteminin bir parçası olarak yüklenebilir ya da sistem çalışırken çeşitli komutlarla da yklenebilmektedir. Bir diğer husus da işlemcileri tasarlayanlar genellikle koruma mekanizamasını "açılıp kapatılacak biçimde" tasarlamalarıdır. Pek çok işlemci reset edildiğinde koruma mekanizması aktif değildir. Koruma mekanizasması genellikle işletim sistemi yüklenirken işletim sisteminn kodları tarafından aktif hale getirilmektedir. Koruma mekanizmasını kapatabilmek için yine kernel modda olnması gerekmektedir. Ancak işletim sistemleri bir kere koruma mekanizmasını aktif hale getirince artık bir daha pasif hale getirmemektedir. > C Dilindeki Tamponlu IO Sistemleri ("Buffered IO in C") : C'nin prototipleri içerisinde bulunan dosya fonksiyonları sistem fonksiyonlarının daha az çağrılmasına sağlamk içim tıpkı yukarıdaki örnekte yaptığımız gibi bir cache sistemi oluşturmaktadır. Standart C fonksiyonlarının oluşturduğu bu cache sistemine genellikle "cache" yerine "buffer" denilmektedir. Bu nedenle C'nin standart dosya fonksiyonlarına "tamponlu IO fonksiyonları (buffered IO functions)" da denilmektedir. Biz örneğin fgetc fonksiyonu ile dosyadan bir byte bile okumak istesek aslında fgetc bir grup byte'ı tek bir sistem fonksiyonu ile okuyup bize okunan tampondan byte'ı verir. Böylece diğer fgetc çağırmaları için yeniden sistem fonksiyonu çağrılmamakta doğrudan bilgiler bu tampondan alınmaktadır. Bu nedenle bir dosyadan bu biçimde byte byte okumalar için standart C fonksiyonları tercih edilmelidir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, Şimdi yukarıda örneği standart C fonksiyonlarıyla deneyelim. Okumayı şöyle yapacağız: while ((ch = fgetc()) != EOF) { /* ... */ } Burada programın bu versiyonu aynı koşullar altında 0.025 saniye zaman almıştır. Üç deneyin aldığı zamanları yeniden anımsatmak istiyoruz Her defasında, -> read çağrılması (test1.c): 3.040 -> read fonksiyonu ile blok blok okuma yapılması (test2.c): 0.014 -> C'nin tamponlu fgetc fonksiyonu il byte byte okuma yapıması (test3.c): 0.025 Programın kodları ise şu şekildedir: /* test3.c */ #include #include #include void proc(int ch) { /* EMPTY */ } int main(int argc, char *argv[]) { FILE *f; int ch; double telapsed; clock_t start, stop; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } start = clock(); if ((f = fopen(argv[1], "rb")) == NULL) { fprintf(stderr, "cannot open file!...\n"); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) proc(ch); if (ferror(f)) { fprintf(stderr, "IO error!..\n"); exit(EXIT_FAILURE); } fclose(f); stop = clock(); telapsed = (double) (stop - start) / CLOCKS_PER_SEC; printf("%.3f\n", telapsed); return 0; } * Örnek 2, Bir dosyayı fgetc fonksiyonuyla byte byte okumak isteyelim. Bu fonksiyon tamponlu (yani cache sistemi ile) çalıştığına göre etkin bir kullanıma yol açacaktır. Pekiyi yukarıdaki örnekteki zamanların aşağıdaki gibi olmasının anlamı nedir? Her defasında, -> read çağrılması (test1.c): 3.040 -> read fonksiyonu ile blok blok okuma yapılması (test2.c): 0.014 -> C'nin tamponlu fgetc fonksiyonu il byte byte okuma yapıması (test3.c): 0.025 Neden fgetc fonksiyonun kullanıldığı versiyon bloklo read okumalarının yapıldığı versiyondan daha yavaş çalışmıtır? İki yöntemde de tampon 8192 byte uzunluğundadır. Programın fgetc versiyonunda her byte için fgetc fonksiyonu çaağrılmıştır. Bir fonksiyon çağırmanın da mikro düzeyde bir zmana kaybı vardır. Ayrıca fgetc fonksiyonu tampondan bilgiyi almak için de biraz zaman kaybı oluşturmaktadır. Bu nedenle standart C kütüphanesinde fgetc fonksiyonunun yanı sıra getc isimli bener bir fonksiyon da bulundurulmuştur. Bu iki fonksiyon arasındaki tek fark getc fonksiyonunun bir makro olarak yazılmasına izin verilmesidir. Eğer getc fonksiyonu bir makro olarak yazılmışsa fonksiyon çağırmanın oluşturduğu göreli zaman kaybı (function call overhead) elimine edilmiş olacaktır. Ancak getc fonksiyonun bir makro biçiminde yazılması zorunlu tutulmamıştır. Yukarıdaki örnekte fgetc yerine getc fonksiyonun çağrılması gcc derleyicilerinde bir zaman kazancı sağlamamaktadır. > Hatırlatıcı Notlar: >> Terminolojiye ilişkin bir noktayı belirtmek istiyoruz. C'nin standart dosya fonksiyonlarının kullandığı FILE yapısına ilişkin göstericiye İngilizce "stream" denilmektedir. Biz buna Derneğimizde "dosya bilgi göstericisi" diyoruz. >> C'de her fopen fonksiyonu ile dosya açıldığında o dosya için o açıma ilişkin ayrı bir tampon oluşturulmaktadır. Yani tampon toplamda bir tane değildir, dosya başına da bir tane değildir. Her fopen çağrısı ayrı bir tampon oluşturmaktadır. Aslında tampon bilgileri fopen donksiyonunun geri döndürdüğü FILE türünden yapı nesnesinin içerisinde saklanmaktadır. Biz aynı dosyayı birden fazla kez fopen fonksiyonu ile açabiliriz. Bu durumda bu açımlardan elde edilen FILE nesnelerinin ayrı tamponları ve ayrı dosya göstericileri olacaktır. /*================================================================================================================================*/ (33_08_10_2023) & (34_14_10_2023) & (35_15_10_2023) & (36_22_10_2023) > C Dilindeki Dosya Fonkisyonlarında Kullanılan Tamponlama Stratejileri: C'nin fonksiyonları kütüphanelerde genellikle dosyanın ardışıl tek bir kısmını tamponda tutmaktadır. Bu tampon read/write bir cache siştemi gibidir. Dolayısıyla bizim dosyaya yazdığımız şeyler de aslında önce tampona yazılmaktadır. Pekiyi tampona yazılan bilgiler ne zaman dosyaya aktarılacaktır? İşte tampondaki bilgilerin dosyaya yazılması tipik olarak şu durumlarda sağlanmaktadır: -> Tampona dosyanın yeni bir kısmı çekilirken eğer tampon güncellenmişse tampondakiler önce dosyaya yazılırlar. Bu durum tamponlama stratejisi ile de ilgilidir. -> Dosya fclose fonksiyonu ile kapatılırken eğer tampona bir yazma yapılmışsa tampondakiler dosyaya yazılırlar. -> exit standart C fonksiyonu zaten tüm dosyaları kapatııktan sonra prosesi sonlandırmaktadır. Dolayısıyla en kötü olasılıkla program sonlanırken tampona yazılmış olan bilgiler dosyaya aktarılmaktadır. -> fflsuh fonksiyonu her çağrıldığında tampondakiler açıkça dosya yazılmaktadır. Aşağıdaki programı çalıştırınız akış getchar fonksiyonunda beklerken Ctrl+C tuşlarına basarak programı sinyal yoluyla sonlandırınız. Bu durumda tampona yazılan bilgiler flush edilmeden program sonlandırılacak ve dosyada 0 byte görülecektir. * Örnek 1, #include #include int main(void) { FILE *f; if ((f = fopen("test.txt", "w")) == NULL) { fprintf(stderr, "Cannot open file!..\n"); exit(EXIT_FAILURE); } fputc('a', f); fputc('b', f); getchar(); fclose(f); return 0; } C'nin dosya fonksiyonlarında tamponlu çalışmadan kaynaklanan bir nedenle okumadan yazmaya yazmadan okumaya geçişte ya fflush fonksiynu çağrılmalı ya da fseek fonksiyonu çağrılmalıdır. Örneğin aşağıdaki işlem hatalıdır ve tanımsız davranışa yol açmaktadır: ch = fgetc(f); fputc('x', f); Burada fgetc ile 1 byte okunduğunda dosya göstericisi 1 byte ilerletilmektedir. Böylece fputc fonksiyonu sonraki offset'e yazılmak istenmiştir. Ancak işlem hatalıdır. Çünkü okumadan yazmaya yazmadan okumaya geçiş doğrudan yapılmaz. Bu kod şöyle düzeltilebilir: ch = fgetc(f); fseek(f, 0, SEEK_CUR); fputc('x', f); Burada aslında fseek fonksiyonu dosya göstericisini hareket ettirmemktedir. Ancak fseek okumadan yazmaya geçişte gerekli olan tampon hazırlığını da yapmaktadır. C'nin standart dosya fonksiyonları üç farklı tamponlama stratejisi (yani yöntemi) kullanabilmektedir. Buna dosyanın (stream'in) tamponlama modu da denilmektedir. Bu modlar şunlardır: -> Tam Tamponlamalı Mod (Full Buffered Mode) -> Satır Tamponlamalı Mod (Line Buffered Mode) -> Sıfır Tamponlamalı Mod (No Buffered Mode) Bu tamponlama modlarından, >> Tam tamponlamalı modda tampon (yani cache) tam kapasiteyle kullanmaya çalışılmaktadır. Dosyanın ardışıl bölümü tapona çekilir. Okunmak istenen bilgiler mümkün olduğunca tampondan verilir. Tamponun dışna çıkılmışsa dosyanın yeni bir bölümü tampona aktarılır. Tabii bu sırada tampon kirlenmişse flush edilir. Bu en doğal çalışma modudur. >> Satır tamponlamalı modda tampona tek bir satır çekilir. Satırın sonında da '\n' karakteri vardır. Dosyadan okuma yapıldıkça buradan verilir. Satır bittiğinde yeni bir satır tampona çekilir. Yani tamponda tipik olarak tek bir satır bulunmaktadır. Tabii yine tampon kirlenmişse yeni satır tampona çekilirken flush işlemi yapılmaktadır. Bu modda tampona yazılan '\n' karakteri kesinlikle flush işlemine yol açmaktadır. Halbuki tam tamponlamada flush oluşturan özel bir byte yoktur. >> Sıfır tamponlamalı modda tampon hiç kullanılmaz. C'nin standart dosya fonksiyonları tamponlamayı kullanmadan doğrudan işletim sisteminin sistem fonksiyonlarıyla (UNIX/Linux sistemlerinde POSIX fonksiyonlarıyla, Windows sistemlerinde Windows API fonksiyonlarıyla) transferi gerçekleştirirler. Biz yukarıda tamponlama modlarının tipik davranışlarını belirttik. Aslında C standartları bu konudaki belirlemeleri biraz muğlak ve kesin olmayan biçimde yani bir "niyet biçiminde" belirtmiştir. Detayları derleyiciyi yazanlara bırakmıştır. Bu durumda örneğin staır tabanlı tamponlama seçilse bile işlemler tam tamponlamalı gibi yapılabilir. Standart C kütüphaneleri mümkün olduğunca tamponlama davranışını yukarıda açıkladıpımız gibi gerçekleştirmektedir. Ancak örneğin normal disk dosyasının '\n' görene kadar satırsal bir biçimde okunması uygun olmayabilmektdir. Bu nedenle örneğin Microsoft derleyicileri, gcc ve clang derleyicileri (glibc kütüpahnesi) normal disk dosyaları için satır tamponalamlı modda '\n' karakteri tampona yazıldığında flush işlemi yapmakta ancak okuma sırasında tamponu tamamen doldurmaktadırlar. Pekiyi bir dosyayı fopen fonksiyonu ile açtığımızda dosyanın default tamponlama modu nedir? İşte C standartları bu konuda bir şey söylememektedir. Fakat en normal durum disk dosyaları için default tamponalama modunun tam tamponlama olmasıdır. Yaygın derleyiciler default tamponlama modunu tam tamponlamalı mod olarak belirlemektedir. Ancak standratlarda stdin, stdout ve stderr dosyalarının default tamponlama modu için bazı şeyler söylenmiştir. Bunu izleyen graflarda ele alacağız. Dosyanın tamponlama modu ve tamponun yeri setbuf ve setvbuf standart C fonksiyonlarıyla değiştirilebilmektedir. Bu fonksiyonlardan, >> "setbuf": setbuf fonksiyonu temel olarak dosya için ayrılan tamponun yerini değiştirmek için kullanılmaktadır. Bu fonksiyon tamponun boyutunu değiştirmez, Tampon her zaman BUFSIZ uzunluğundadır. Fonksiyon yalnızca tamponun yerini değiştirmektedir. Fonksiyonun prototipi şöyledir: void setbuf(FILE *stream, char *buf); Fonksiyonun birinci parametresi tamponu değiştirilecek dosyaya ilişkin dosya bilgi göstericisini belirtmektedir. İkinci parametre yeni tamponun yerini belirtmektedir. Bu parametreye en azından BUFSIZ uzunluğunda tahsis edilmiş bir alanın adresi geçirilmelidir. Fonksiyonun ikinci parametresine NULL adres geçirilirse dosya sıfır tamponlamalı moda sokulmaktadır. Aşağıda tam tamponlamanın nasıl etki gösterdiğine yönelik basit bir örnek verilmiştir. Örnekte önce tamponun yeri setbuf fonksiyonuyla değiştirilmiş sonra dosyadan okuma yapılıp tampon görüntülenmiştir. Sonra da dosyaya yazma yapılmış yeniden tampon görüntülenmiştir. Okumadan yazmaya geçerken bir fseek çağırmasının yapıldığında dikkat ediniz. Genellikle fseek çağrıları tamamen tamponu yeniden doldurmaktadır. * Örnek 1, #include #include #include void disp_hex(const void *buf, size_t size, size_t lbytes) { size_t i, k, remainder; unsigned char *cbuf = (unsigned char *)buf; for (i = 0; i < size; ++i) { if (i % lbytes == 0) printf("%08x ", (unsigned int)i); printf("%02X ", cbuf[i]); if (i % lbytes == lbytes - 1) { for (k = 0; k < lbytes; ++k) printf("%c", iscntrl(cbuf[i - lbytes + k]) ? '.' : cbuf[i - lbytes + k]); putchar('\n'); } } remainder = size % lbytes; for (k = 0; k < 3 * (lbytes - remainder); ++k) putchar(' '); for (k = 0; k < remainder; ++k) printf("%c", iscntrl(cbuf[i - remainder + k]) ? '.' : cbuf[i - remainder + k]); putchar('\n'); } int main(void) { FILE *f; char buf[BUFSIZ]; if ((f = fopen("test.txt", "r+")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } setbuf(f, buf); fgetc(f); disp_hex(buf, BUFSIZ, 16); printf("---------------------------------------------\n\n"); fseek(f, 0, SEEK_CUR); fputc('x', f); disp_hex(buf, BUFSIZ, 16); fclose(f); return 0; } >> "setvbuf" : sevbuf fonksiyonu setbuf fonksiyonunu işlevsel olarak kapsamaktadır. Zaten bu fonksiyon setbuf fonksiyonunun yetersizlikleri nedeniyle tasarlanmıştır. setvbuf fonksiyonu ile biz dosyanın tamponlama modunu hem de tamponun yerini ve büyüklüğünü değiştirebiliriz. Fonksiyonun prototipi şöyledir: int setvbuf(FILE *stream, char *buf, int mode, size_t size); Fonksiyonun birinci parametresi ilgili dosyaya ilişkin dosya bilgi göstericisini, ikinci parametresi yeni tamponun yerini, üçüncü parametresi yeni tamponlama modunu ve dördüncü parametresi de tamponun yeni uzunluğunu belirtmektedir. Tamponlama modu için şu değerlerden biri girilmelidir: -> _IOFBF (Tam tamponlama) -> _IOLBF (Sator tamponlaması) -> _IONBF (Sıfır tamponlama) Tamponlama modunu belirten üçünü parametreye _IONBF değeri girilirse artık ikinci dördüncü parametreler fonksiyon tarafından kullanılmamaktadır. Fonksiyonun ikinci parametresine NULL adres geçilebilir. Bu durumda tampon fonksiyon tarafından dördüncü parametre belirtilen uzunlukta tahsis edilmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda sıfır dışı herhangi bir değere geri dönmektedir. Aşağıdaki örnekte biz açılmış olan dosyanın hem tampon büyüklüğü hem de tamponun yeri değiştirilmiştir. * Örnek 1, #include #include #include void disp_hex(const void *buf, size_t size, size_t lbytes) { size_t i, k, remainder; unsigned char *cbuf = (unsigned char *)buf; for (i = 0; i < size; ++i) { if (i % lbytes == 0) printf("%08x ", (unsigned int)i); printf("%02X ", cbuf[i]); if (i % lbytes == lbytes - 1) { for (k = 0; k < lbytes; ++k) printf("%c", iscntrl(cbuf[i - lbytes + k]) ? '.' : cbuf[i - lbytes + k]); putchar('\n'); } } remainder = size % lbytes; for (k = 0; k < 3 * (lbytes - remainder); ++k) putchar(' '); for (k = 0; k < remainder; ++k) printf("%c", iscntrl(cbuf[i - remainder + k]) ? '.' : cbuf[i - remainder + k]); putchar('\n'); } int main(void) { FILE *f; char buf[BUFSIZ * 2]; if ((f = fopen("test.txt", "r+")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } setvbuf(f, buf, _IOFBF, BUFSIZ * 2); fgetc(f); disp_hex(buf, BUFSIZ * 2, 16); printf("---------------------------------------------\n\n"); fseek(f, 0, SEEK_CUR); fputc('x', f); disp_hex(buf, BUFSIZ * 2, 16); fclose(f); return 0; } setbuf ve setvbuf fonksiyonları dosya açıldıktan sonra ancak dosya üzerinde henüz hiçbir işlem yapılmadan kullanılmalıdır. Aksi tadirde "tanımsız davranış (undefined behavior)" oluşmaktadır. > stdin, stdout, stderr Dosyaları: Bilgisayar sistemlerinde tüm işlemler aslında elektriksel düzeyde gerçekleştirilmektedir. Örneğin bilgiyarımızın genişleme yuvasına bir kart taktıldığını düşünelim. Bu kartın üzerinde birtakım entegre devreler potansiyel olarak birtakım işleri yapabilecek biçimde yerleştirilmiştir. Ancak bu tür donanım birimlerinin de hedef doğruktusunda programlanması gerekmektedir. Bu donanım birimlerine iş yaptırmak için komutlar elektriksel işaretler biçiminde gönderilmektedir. Ancak bu gönderim de makine komutlarıyla sağlanmaktadır. Yani genişleme yuvasına takılan kart aslında elektrisksel olarak programlanmakta ancak o elektiriksel işaretlerin karta gönderilmesi için de programlar yazılmaktadır. Bu tür donanım birimlerinin programlanması genellikle sembolik makine dili düzeyinde yapılmaktadır. Ancak programlama sırasında gerekli pek çok makine komutu özel komutlardır ve işlemcinin koruma mekanizamasına takılma potansiyelindedir. Bu nedenle bu tür kodların kernel modda çalştırılması gerekmektedir. İşte bir donanım birimini programlayan ve kernel modda çalışan kodlara "aygıt sürücüler (device drivers)" denilmektedir. O halde nerede bir donanım aygıtı varsa onu programlayan aşağı seviyeli kodların bulunuyor olması gerekir. Bu kodlar da aygıt sürücü biçimde yazılımalıdır. Örneğin bir ses kartını genişleme yuvasına taktiğimızda onun aygıt sürücüsü yüklenmedikten sonra kart bir işe yaramaktadır. Dolaysıyla aygıt sürücüler işletim sisteminin kernel yapısına uygun bir mimari ile yazılmak zorundadır. Her işletim sisteminin belli bir aygıt sürücü mimarisi vardır. Aygıt sürücü yazımı işletim işletim sistemine hatta aynı işletim sisteminin versiyonlarına göre değişebilmektedir. Nasıl masaüstü bilgisayarların genişleme yuvalarına kart takmak için o kartın belli özelliklere sahip olması gerekiyorsa bir aygıt sürücünün de işletim sisteminin belirlediği bazı özelliklere sahip olması gerekmektedir. Pekiyi aygıt sürücüler ne zaman ve nasıl yüklenmektedir? Aygıt sürücülerin bir bölümü kernel içerisine entegre edilmiş durumdadır. Bir bölümü sistem boot edilirken yüklenmektedir. Diğer bir bölümü ise gerektiğinde donanın aygıtı sisteme bağlandığında otomatik yüklenmektedir. Örneğin modern sistemlerde genişleme yuvalarına bir kart takıldığında ya da USB soketine bir donanım birimi takıldığında bu kartlar ve donanım birimleri kendini sisteme tanıtmaktadır. İşletim sistemelri de kendi aygıt sürücü istesinde bu kartlara ya da donanım birimlerine ilişkin aygıt sürücüler varsa onları otomatik olarak yüklemektedir. Eskiden bu tür işlemler tamamen manuel yapılıyordu. Her durumda modern sistemlerde bir aygıt sürücünün yüklenebilmesi ancak sistem yöneticisinin onayıyla yapılabilmektedir. Örneğin UNIX/Linux sistemlerinde aygıt sürücüleri ancak root kullancısı sisteme yükleyebilir. Windows sistemlerinde default kullanıcı genellike admin durumundadır. Aygıt sürücüler yüklenirken bir pop pencereyle sistem yöneticisi bilgilendirilir ve onun onayı alınır. Bazı aygıt sürücleri bir donanım arayüzü biimindedir. Yani sistem programcısından komutlar alıp onu donanıma iletmektedir. Örneğin biz programcı olarak ses kartına ilişkin aygıt sürücüye komut gönderebiliriz. Bu aygıt sürücü de ses kartını programlayarak işlemleri gerçekleştirebilir. Aslında aygıt sürücüler genel olarak birer dosya gibi kullanılmaktadır. Yani aygıt sürücünün bir ismi vardır. Bir dosya nasıl açılıyorsa aygıt sürücü öyle açılır. Aygıt sürücü dosya gibi açıldıktan sonra ona yazma yapıldığında yazılanlar aygıt sürücüyel gönderilir. Aygıt sürücüden yine dosya fonksiyonlarıyla okuma yapılır. Ayrıca aygıt sürücülerin içerisindeki önceden belirlenmiş fonksiyonlar user modtan çağrılabilmektedir. * Örnek 1, UNIX/Linux sistemlerinde bir aygıt sürücü open fonksiyonuyla açılır. Ona write fonksiyonuyla bilgi gönderilir. Ondan read fonksiyonuyla okuma yapılır. ioctl isimli bir fonksiyonla da aygıt sürücüdeki belli fonksiyonlar çalıştırılır. En sonunda aygıt sürücü close fonksiyonuyla kapatılır. * Örnek 2, Windows sistemlerinde yine aygıt ssürücü CreateFile API fonksiyonuyla bir dosya biçiminde açılır. WriteFile API fonksiyonuyla aygıt sürücüye bilgi gönderilir. ReadFile API fonksiyonuyla aygıt sürücüden okuma yapılır. DeviceIOControl API fonksiyonuyla aygıt sürücü içerisindeki fonksiyonlar çağrılır. Pekiyi aygıt sürücüler kabaca nasıl yazılmaktadır? İşte aygıt sürücüleri yazanlar aygıt sürücü açıldığında belli bir fonksiyonun çağrılmasını, aygıt sürücüye yazma yapıldığında belli bir fonksiyonun çağrılmasını, aygıt sürücüden okuma yapıldığında belli bir fonksiyonun çağrılamsını, aygıt sürücü kapatıldığında belli bir fonksiyonun çağrılmasını sağlamaktadır. Ayrıca aygıt sürücü içerisindeki bazı fonksiyonlara numaralar verip user moddan bu fonksiyonların çağrılması sağlarlar. * Örnek 1, Bir termometre devresini kontrol eden bir aygıt sürücü yazacak olalım. Aygıt sürücümüzün bir ismi vardır. Aygıt sürücü bir dosya gibi açıldığında aygıt sürücümüzün bir fonskyionu çağrılır. Biz orada gerekiyorsa birtakım ilk işlemleri yaparız. Aygıt sürücümüzden okuma yapıldığında yine bizim bir fonksiyonumuz çağrılır. Örneğin bu fonksiyonda biz termometre devresinden ısıyı alarak okuma yapan programa veririz. Aygıt sürücü kapatılınca yine bizim bir fonksiyonumuz çağrılır biz de gereken bazı son işlemleri yaparız. Aygıt sürücüsü içerisindeki fonksiyonlar user moddan çağrılırken yine sistem fonksiyonlarında olduğu gibi proses kernel moddan user moda otomatik olarak geçirilmektedir. Yani aygıt sürücü içerisindeki fonksiyonlar kernel modda çalıştırılmaktadır. Fonksiyonun çalışması bittiğinde yeniden user moddan kernel moda geri dönülmektedir. İşte bilgisayarlarda kullanılan klavye ve ekran aslında donanımsal birimlerdir. Dolayısıyla bunlar aygıt sürücüler tarafından yönetilirler. Yani aslında biz ekrana bir şeyler yazdırabilmek için yazılacak şeyleri aygıt sürücüye göndeririz. Aygıt sürücü onları ekrana çıkartır. Benzer biçimde biz aslında klavyeden okuma yaparken aygıt sürücüden okuma yaparız. Aygıt sürücü de kalvyeden alınanları bize verir. Ekran ve klavye birimelrine "terminal" bunlara aygıt sürücülere de "terminal aygıt sürücüleri" denilmektedir. C standartlarında ekran ve klavye sözcükleri kullanılmamıştır. Çünkü bir bilgisayar sisteminde ekran ve klavyenin bulunması zorunlu değildir. Ekran ve klavye yerine C standartlarında "stdout (standart output)" ve "stdin (standart input)" terimleri kullanılmıştır. C standartlarında stdout ve stdin birer dosya olarak geçmektedir. Böylece örneğin printf fonksiyonu ekrana yazmamaktadır. stdout isimli bir dosyaya yazamktadır. scanf fonksiyonu klavyeden okumamaktadır. stdin isimli bir dosyadan okuma yapmaktadır. stdout ve stdin dosyalarının gerçekte ne olduğu standartlarda belirtilmemiştir. Tabii modern masaüstü sistemlerinde stdout dosyası aslında ekranı kontrol eden terminal aygıt sürücüsünü, stdin dosyası ise klavyeyi kontrol eden terminal aygıt sürücüsünü temsil etmektedir. Aygıt sürücüler birer dosya gibi kullanıldığı için bunların bir dosya olması da tasarımla uyumludur. Böylece biz bu sistemlerde aslında stdout dosyasına bir şeyler yazdığımızda yazdığımız şeyler aygıt sürücüsüne gider. Aygıt sürücüsü de onları ekrana çıkartır. Benzer biçimde biz stdin dosyasından bir şeyler okumak istediğimizde aslında terminal klavyeyi kontrol eden aygıt sürücüden okuma yaparız. O da klavyeden girilenleri bize verir. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, UNIX/Linux dünyasında aygıt sürücüleri açmakta kullanılan dizin girişleri genel olarak /dev dizininde bulundurulmuştur. Bulunduğunuz terminalde tty komutunu vererek aygıt sürücünüzün ismini öğrenebilrisiniz. Örneğin: $tty /dev/pts/1 Aygıt sürücüler dosya gibi açılıp kullanıldığına göre biz bu aygıt sürücüyü open ile açıp, ona write ile yazma yaparsak yazılanlar o terminale çıkacaktır. Şöyleki: int fd; if ((fd = open("/dev/pts/1", O_WRONLY)) == -1) exit_sys("open"); write(fd, "ankara\n", 7); close(fd); İşte aslında C'nin stdout dosyasına yazma yapan fonksiyonları da neticede write fonksiyonunu kullanarak ekranda çıkacak şeyleri bu aygıt sürücüye yollamaktadır. Örneğin puts fonksiyonunu çağırdığımızda kabaca şu işlemler gerçekleşmektedir: puts ---> write(stdout, ....) ----> Aygıt sürücü ----> Ekrana basma Aşaıdaki örnekte önce terminal aygıt sürücüsünden (kalvyeden) okuma yapılıp sonra okunanlar ona (ekrana) yazdırılmıştır. Genellikle ekran ve klavye işlemleri tek bir aygıt sürücü ile yapılmaktadır. Yani bu aygıt sürücü hem ekranı hem de klavyeyi konrol etmektedir. Zaten terminal kavramı "ekran ve klavyeyi" içeren bir kavramdır. Dolayısıyla terminal aygıt sürücüsü de her iki aygıtı kontrol eden aygıt sürücüsüdür. Aşağıda ilgili programın kodları verilmiştir: #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char buf[4096]; ssize_t result; if ((fd = open("/dev/pts/1", O_RDWR)) == -1) exit_sys("open"); if ((result = read(fd, buf, 4096)) == -1) exit_sys("read"); buf[result] = '\0'; if (write(fd, buf, strlen(buf)) == -1) exit_sys("write"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Öte yandan modrn işletim sistemlerinde açık dosyanın hedefi değiştirebilmektedir. Örneğin stdout dosyasına yazılanlar aslında terminal aygıt sürücüsüne gönderilmektedir. Ancak biz istersek bu dosyaya yazılanların başka bir yere örneğin diskte bir dosyaya gönderilmesini sağlayabiliriz. Bu işleme işletim sistemleri dünyasında "IO yönlendirmesi (IO redirection)" denilmektedir. O halde biz örneğin IO yöönlendirmesi yoluyla stdout ve stdin dosyalarını başka hedeflere de yönlendirebiliriz. C'de stdin, stdout ve stderr değişken isimleri aslında FILE * türünden dosya bilgi göstericisi belirtmektedir. Bunlar masaüstü sistemlerde ekran ve klavyeyi kontrol eden aygıt sürücü dosyaları olarak açılmışlardır. Yani biz stdout dosyasına bir şey yazdığımızda aslında yazdığımız şeyler terminal aygıt sürücüsüne gönderilmektedir. Ancak bu değişkenlerin FILE * türünden yani tamponlu bir dosya belittiğine dikkat ediniz. Yani diğer dosyalar için nasıl bir tampon eşliğinde aktarım yapılıyorsa bu aygıt sürücü dosyalarına da yine bir tampon eşliğinde aktarım yapılmaktadır. içerisinde başı f ile başlamayan fonskiyonlar da aslında birer dosya fonksiyonudur. Ancak onlar default olarak stdout ve stdin dosyalarını kullanmaktadır. Yani örneğin aşağıdaki iki fonksiyon çağrısı tamamen eşdeğerdir: fprintf(stdout, ...); printf(....); Aşağıdaki gibi çağrı da eşdeğerdir: fscanf(stdin, ...); scanf(....); Zaten C standartları örneğin fprintf fonksiyonunu açıklayıp printf fonksiyonu için fprintf fosnksiyonunun stdout dosyasına yazan biçimi diye kısa açıklama bulundurmuştur. Bir proses çalışmaya başladığında, genel olarak 0, 1 ve 2 numaralı dosya betimleyicileri zaten açık durumdadır. Sistemlerden, >> UNIX/Linux Sistemlerinde: 0 numaralı betimleyici klavyeyi temsil eden terminal aygıt sürücüsüne ilişkindir. Yani bu betimleyiciden read fonksiyonu ile okuma yapılmak istenirse aslında klavyeden okuma yapılacaktır. stdin betimleyicisinin O_RDONLY modda açıldığı varsayılmaktadır. 1 numaralı betimleyici de yine terminal aygıt sürücüsüne ilişkindir. Ancak bu betimleyici ile yazma yapıldığında terminal aygıt sürücüsü yazılanları ekrana bastırmaktadır. 1 numaralı betimleyicinin O_WRONLY açıldığı kabul edilmektedir. 2 numaralı betimleyici default durumda tamamen 1 numaralı betimleyicde olduğu gibi terminal aygıt sürücüsüne ilişkindir. 2 numaralı betimleyici ile yazma yapılırsa yazılanlar yine ekrana basılacaktır. UNIX/Linux dünyasında, -> 0 numaralı betimleyiciye "stdin betimleyicisi", -> 1 numaralı betimleyiciye "stdout betimleycisi", -> 2 numaralı betimelyiciye de "stderr betimleyicisi" denilmektedir.Bizim açmadığımız 0, 1 ve 2 numaralaı betimleyicileri özel bir durum yoksa biz kapatmamalıyız. C'deki stdin, stdout ve stderr dosya bilgi göstericileri arka planda UNIX/Linux sistemlerinde sırasıyla 0, 1 ve 2 numaralı betimleyicileri kullanmaktadır. * Örnek 1, #include #include #include #define BUFFER_SIZE 4096 void exit_sys(const char *msg); int main(void) { char buf[BUFFER_SIZE]; ssize_t result; if ((result = read(0, buf, BUFFER_SIZE)) == -1) exit_sys("read"); if (write(1, buf, result) == -1) exit_sys("write"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >> Windows Sistemlerinde: Benzer biçimde Windows sistemlerinde de bir proses çalışmaya başladığında genellikle (ama her zaman değil) klavye, ekran ve hata dosyalarını temsil eden dosyalar açık durumdadır. Bu dosyalar yine terminal aygıt sürücüsüne ilişkindir. Ancak bunların Windows'taki HANDLE değerleri önceden belli değildir. Dolayısıyla program çalışırken programcı tarafından GetStdHandle isimli bir API fonksiyonuyla elde edilirler. Fonksiyonun prototipi şöyledir: HANDLE WINAPI GetStdHandle( DWORD nStdHandle ); Fonksiyona parametre olarak aşağıdakiklerden biri girilebilir: STD_INPUT_HANDLE STD_OUTPUT_HANDLE STD_ERROR_HADNLER Bu değerler biçizm hangi standart handle'ı elde edeceğimizi belirtmektedir. Fonksiyon başarısızlık durumunda INVALID_HANDLE_VALUE değerine geri döner. Prosesin standart handle'ları olmayabilir. Bu durumda fonksiyon NULL değerine geri döner. Ancak GetLastError değeri bu durumda set edilmemektedir. Windows sistemlerinde de standart C kütühanesindeki stdin, stdout ve stderr dosya bilgi göstericileri GetStdHandle ile elde eidlen aygıt sürücülere ilişkin dosyaları kullanmaktadır. Aşağıdaki örnekte Windows sistemlerinde önce stdin ve stdout dosyalarının handle değerleri elde edilmiş sonra stdin dfosyasından okuma yapılıp okunanlar stdout dosyasına yazdırılmıştır. * Örnek 1, #include #include #include #define BUFFER_SIZE 4096 void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hStdIn, hStdOut; char buf[BUFFER_SIZE]; DWORD dwRead, dwWritten; if ((hStdIn = GetStdHandle(STD_INPUT_HANDLE)) == INVALID_HANDLE_VALUE) ExitSys("GetStdHandle"); if ((hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)) == INVALID_HANDLE_VALUE) ExitSys("GetStdHandle"); if (hStdIn == NULL || hStdOut == NULL) { fprintf(stderr, "Standard input or standard output handle doesn't exist!..\n"); exit(EXIT_FAILURE); } if (!ReadFile(hStdIn, buf, BUFFER_SIZE, &dwRead, NULL)) ExitSys("ReadFile"); if (!WriteFile(hStdOut, buf, dwRead, &dwWritten, NULL)) ExitSys("WriteLile"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Biz default olarak stderr dosyasının (hem C'de hem de POSIX ve API düzeyinde) stdout dosyasıyla aynı terminal aygıt sürücüsüne yönlendirildiğini belirtmiştik. Pekiyi stdout ile stderr arasında ne farklılık vardır. Biz stdout dosyasına da stderr dosyasına bir şeyler yazdırdığımızda ikisi de ekranda görüntülenmektedir. Bir dosya dosyanın bir hedefi vardır. Ancakbu hedef değiştirilebilmektedir. Dosyanın hedefenin hedefinin değiştirilemsine işletim sistemi dünyasında "IO Yönlendirmesi (IO Redirection)" denilmektedir. Örneğin stdout dosyasının hedefi ekranı kontrol eden eaygıt sürücüsü iken biz onu bir disk dosyasına yönlendirebiliriz. Bu durumda printf fonksiyonu gibi stdout dosyasına yazan fonksiyonlar aslında dosya yazmış olurlar. Benzer biçimde biz stdin ve stderr dosyalarını da yönlendirebiliriz. IO yönlendirmelerinin kernel tarafından nasıl yapıldığı ileride ayrı bir başlıka ele alınacaktır. Biz şimdilik kullanıcı düzeyinde IO yönlenidrmesi ile ilgileneceğiz. Biz bir programı kabuk üzerindne çalıştırırken program isminden sonra ">" karakterini kullanırsak çalıştırdığımız programın stdout dosyasını yönlendirmiş oluruz. Örneğin: ./sample > test.txt Burada stdou dosyasına yazılanlar artık ekrana çıkmatacak "test.txt" dosyasına yazılacaktır. Tabii stderr dosyasına yazılanlar yine ekrana çıkmaya devam edecektir. Kabuk üzerinde program isminden sonra "2>" karakterlerini kullandığımızda ise stderr dosyasını yönlendirmiş oluruz. Örneğin: ./sample 2> test.txt Burada sample programının stderr dosyasına yazdığı şeyler artık ekranda görülmeyecek "test.txt" dosyasına yazılacaktır. Tabii iki yönlendirmeyi birlikte de yapabliriz. Örneğin: ./sample > test.txt 2> mest.txt Kabukta yönlendirme Windows'un komut satırında da aynı biçimde yapılmaktadır. Biz programımızdaki hata mesajlarını stderr dosyasına yazdırmalıyız. Default durumda bu hata mesajları ekrana çıkacaktır. Ancak programı çalıştıran kişiler IO yönlendirmesi ile hedefi değiştirebilecekleridir. Eğer bi hata mesajlarını doğrudan stdout dosyasına yazdırırsak bu durumda programın hata mesajlarıyla normal mesajları ayırmanın bir yolu kalmaz. Örneğin find isimli programla bir dosyayı dizin ağacında aşağıdaki gibi arayacak olalım: $>find / -name "sample.c" find bu ii yaparken erişim haklarından dolayı bazı dizinler okuyamayacaktır. Okuyamadığı dizinler için hata mesajlarını program stderr dosyasına yazdırmaktadır. Default durumda stderr dosyasına yazılanlar ekrana çıkacaktır. Böylece ekranda hem hata mesajları hem de normal mesajlar görünecektir. Ancak biz stderr dosyasını bir dosyaya yönlendirirsek iki tür mesajın da ekranda gözükmesini engellmiş oluruz. Örneğin: $>find / -name "sample.c" 2> err.txt Burada kullanıcı hata mesajlarının dosyada gereksiz yer kaplamasını da istemeyebilir. UNIX/Linux sistemlerinde /dev dizinin altında bazı özel aygıt sürücüler vardır. Örneğin /dev/null aygıt sürücüsü kendisne yazılan bilgileri doğrudan atmaktadır. O halde find programını şöyle de öalıştırabiliriz: $>find / -name "sample.c" 2> /dev/null Bu kullanımın bir benzeri Windows'ta NUL biçiminde bulunmaktadır. stdout ve stderr dosyalarını aynı hedefe yönlendirme aaşağıdaki gibi yapılmamalıdır: ./sample > test.txt 2> test.txt Çünkü genel olarak kabuk programları her yönlendirme sembolünde ilgili dosyayı yeniden "truncate" modunda açmaktadır. Eğer böyle bir şey isteniyorsa aşağıdkai gibi yapılmalıdır: ./sample > test.txt 2>&1 Burada "2>&1" karakterleri "2 numaralı betimleyiciyi (stderr betimleyicisini) bir numaralı betimleyici ile (stdout betimleyicisi) aynı dosyaya yönlendir" anlamına gelmektedir. Normal olarak klavyeden okunacak bilgiler sanki klavyeden girilmiş gibi dosyadan da okunabilir. Bunun için kabuk üzerinde "<" sembolü ile stdin dosyasının yönlendirilmesi gerekir. Örneğin: $>sample < test.txt Burada sample programının stdin dosyasından okudukları aslında "test.txt" dosyasından okunacaktır. Yani biz bu işlemle default olarak terminal aygıt sürücüsüne yönlendirilmiş olan stdin dosyasını açıkça "test.txt" dosyasına yönlendirmiş olduk. Biz bir dosyadan okuma yapan dosya sonuna geldiğinde sonlanan bir program yazmış olalım. Bu programı stdin dosyasından çalışır hale getirdiğimizde EOF etkisi nasıl oluşturulacaktır. Ne de olsa terminal gerçek bir dosya değildir. Yani terminalin (kalvyenin) sonuna gelmek biçiminde bir kavram yoktur. İşte terminal aygıt sürücüleri EOF etkisi yaratan özel tuş kombinasyonları kullanmaktadır. UNIX/Linux dünyasında "Ctrl+d" tuşları Windows dünyasında "Ctrl+Z" tuşları "dosya sonu" etkisi oluşturmaktadır. Tabii bu tuşlara bastığımızda terminali kapatmış olmayız. Yaşnızca anlık bir EOF etkisi oluşturulmaktadır. Yine stdin dosyasından okuma yapmaya devam edebiliriz. EOF etkisi yaratmak için Ctrl+z ve Ctrl+d tuşlarına satır başlarında basınız. Pek çok terminal aygıt sürücüsü ancak satır başlarında bu özel karakterlere basılmışsa EOF etkisi yaratmaktadır. Aşağıda bir dosyayı okuyarak ekrana yazdıran bir C programı verilmiştir. Eğer program komut satırı argümanı verilmeden çalıştırılırsa stdin dosyasından (yani klavyeden) okuma yapmaktadır. İşte bu durumda bu programdan çıkabilmek için UNIX/Linux sistemlerinde "Ctrl+d" tuşlarına, Windows sistemlerinde ise "Ctrl+Z" tuşlarına basmak gerekir. * Örnek 1, #include #include int main(int argc, char *argv[]) { FILE *f; int ch; if (argc > 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (argc == 1) f = stdin; else if ((f = fopen(argv[1], "r")) == NULL) { fprintf(stderr, "cannot open file: %s\n", argv[1]); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) putchar(ch); if (ferror(f)) { fprintf(stderr, "cannot read file!..\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } Öte yandan yüksek seviyeli biçimde "dosya yönlendirmesi (IO redirection)" freopen isimli standart C fonksiyonu kullanılmaktadır. Ancak bunun tersini yapan (yüksek seviyeli) bir fonksiyon mevcut değildir. >> "freopen" : Fonksiyonun prototipi şöyledir: #include FILE *freopen(const char *path, const char *mode, FILE *stream); Fonksiyonun birinci parametresi yönlendirmenin yapılacağı disk dosyasını belirtmektedir. İkinci parametre bu dosyanın nasıl açılacağını belirtir. Eğer bu ikinci parametre "w" içeren bir parametre ise yönlendirme bu dosyaya yazılacak biçimde yapılmaktadır. Eğer bu parametre "r" içeren biçimdeyse yönlendirme bu dosyadan okunacak biçimde yapılmaktadır. Fonksiyonun son parametresi yönlendirilecek dosyaya ilişkin dosya bilgi göstericisini (stream) almaktadır. Fonksiyon başarı durumunda dosyaya ilişkin dosya bilgi göstericisine, başarısızlık durumunda NULL adrese geri dönmektedir. Fonksiyonun bize verdiği dosya bilgi göstericisi birinci parametreyle belirttiğimiz dosyaya ilişkin dosya bilgi göstericisidir. Bu işlem sonrasında son parametreyle belirtilmiş olan gösterici ile fonksiyonun geri döndürdüğü gösterici aynı FILE nesnesini gösteriyor durumda olur. Örneğin: FILE *f; if ((f = freopen("test.txt", "w", stdout)) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } Burada stdout dosyası "test.txt" dosyasına yönlendirilmiş durumdadır. Yani artık stdout dosyasına yazılacak şeyler "test.txt" dosyasına yazılacaktır. Aslında fonksiyon stdout göstericisinin gösterdiği yerdeki FILE nesnesi üzerinde değişiklik yapmaktadır. Dolayısıyla stdout gösteicisinin gösterdiği yer aslında değişmemekte ve freopen fonksiyonu yine aynı adresi geri döndürmektedir. Dolayısıyla freopen fonksiyonunun geri döndürdüğü dosya bilgi göstericisine aslında gerçek anlamda gereksinim duyulmamaktadır. Aşağıda freopen fonksiyonunun kullanımına bir örnek verilmiştir. * Örnek 1, #include #include int main(void) { FILE *f; if ((f = freopen("test.txt", "w", stdout)) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) printf("%d\n", i); fprintf(f, "Ok\n"); return 0; } * Örnek 2, #include #include int main(void) { FILE *f; int ch; if ((f = freopen("test.txt", "r", stdin)) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } while ((ch = getchar()) != EOF) putchar(ch); if (ferror(stdin)) { fprintf(stderr, "cannot read file!..\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } C'deki (işletim sisteminin değil) stdin, stdout ve stderr dosyaları da diğer dosyalarda olduğu gibi tamponlu çalışmaktadır. Yani örneğin biz stdout dosyasına bir şeyler yazdığımız zaman önce o tampona yazılmakta sonra işletim sisteminin sistem fonksiyonlarıyla (POSIX fonksiyonları ya da Windows API fonksiyonlarıyla) aktarım yapılmaktadır. Pekiyi C'nin stdin, stdout ve stderr dosyaları iiçin default tamponlama modu nedir? Standartlar şunları söylemektedir -> stdout ve stdin dosyaları işin başında eğer interaktif bir aygıta yönlendirilmişse hiçbir zaman tamponlamalı modda olamaz. Ancak satır tamponlamalı ya da sıfır tamponlamalı modda olabilir. Ancak bu dosyalar interaktif olmayan bir aygıta yönlendirilmişse işin başında tam tamponlamalı modda olmak zorundadır. Terminal (klavye ve ekran) interaktif aygıt kabul edilmektedir. Normal disk dosyaları interaktif olmayan aygıtı temsil etmektedir. -> stderr dosyası ister interaktif bir aygıta isterse interaktif olmayan bir aygıta yönlendirilmiş olsun hiçbir zaman işin başında tam tamponlamalı modda olamaz. Standartlardaki bu anlatımdan çıkan sonuçlar şunlardır: -> Biz stdin ve stdout dosyalarını diskte bir dosyaya yönlendirirsek kesinlikle tam tamponlamalı modda olurlar. Ancak bir yönlendirme yapmazsak C derleyicilerini yazanlara bağlı olarak satır tamponlamalı ya da sıfır tamponlamalı modda olabilirler. Gerçekten de Windows sistemlerinde işin başında stdout "sıfır tamponalamalı" modda ilen Linux sistemlerinde "satır tamponlamalı" moddadır. -> stderr dosyası bir disk dosyasına yönlendirilmiş olsa bile tam işin başında tamponalamalı modda olamamaktadır. Ancak satır tamponlamalı ya da sıfır tamponlamalı modda olabilmektedir. Bu durumda örneğin aşağıdaki gibi bir printf çağrısında yazılanların ekranda görünmesi garanti değildir: printf("this is a test"); Bu durumda, -> Eğer ilgili derleyici satır tamponlamalı mod kullanıyorsa (Linux derleyicilerinde olduğu gibi) bu yazılanlar henüz ekranda görünmeyecektir. -> Eğer ilgil derleyici sıfır tamponlamalı mod kullanıyorsa (Windows derleyicilerinde olduğu) gibi bu yazılanlar ekranda görünecektir. Pekiyi yukarıdaki printf çağrısında çağrı bittikten sonra yazdırılanların ekranda çıkmasını nasıl garanti ederiz? Bunu garanti etmek için şunlar yapılabilir: -> Yazdırılacak şeylerin sonuna "\n" karakteri eklenebilir. En kötü olasılık satır tamponalaması olacğına göre yzılanlar kesinlikle printf bitince ekranda görünecektir. Örneğin: printf("this is a test\n"); Tabii burada imleç aşağı satırın başına da geçecektir. -> printf çağrısından sonra stdout tamponu flush edilebilir. Örneğin: printf("this is a test"); fflush(stdout); -> stdout dosyası setbuf ya da setvbuf fonksiyonlarıyla sıfır tamponlamalı moda sokulabilir. Ancak bunun hemen programın başında yapılması gerekir. Örneğin: setbuf(stdout, NULL); printf("this is a test"); C derleyicilerinin çoğunda stdin dosyasından okuma yapılmak istendiğinde bu fonksiyonlar kendi içlerinde önce stdout dosyasını flush etmektedir. Bu nedenle aşağıdaki bir kodda stdout default durumda satır tamponlamalı olsa bile yazı ekranda görünebilecektir: printf("input a number:"); scanf("%d", &val); Ancak C standartlarında böyle bir garanti verilmemiştir. Taşınabilirlik için fflush işleminin açıkça uygulanması gerekir: printf("input a number:"); fflush(stdout); scanf("%d", &val); Her ne kadar C'nin stdin dosyası standartlarda işin başında "satır tamponlamalı ya da sıfır tamponlamalı" modda olabilir denmişse de pratikte hep bu dosya satır tamponlamalı biçimde oluşturulmaktadır. Bunun nedeni işletim sistemlerinin terminal aygıt sürücülerinin klavyeden satır satır okuma yapmasıdır. stdin dosyası istense de bu sistemlerde sıfır tamponlamalı moda sokulamamaktadır. Aşağıdaki programı Windows ve Linux sistemlerinde ayrı ayrı derleyerek çalıştrınız. * Örnek 1, #include int main(void) { printf("ankara"); for (;;) ; return 0; } stdin dosyasından (klavyeden) biz bir karakter okumak istesek bile bu dosya "satır tamponlamalı" modda olduğu için bir satırlık bilgi klavyeden okunup tampona yerleştirilecek ve tampondaki, ilk karakter okunacaktır. Her zaman satır tamponlamada satırın sonunda '\n' karakteri tampona yerleştirilmektedir. Örneğin: ch = getchar(); ch = getchar(); Burada birinci getchar fonksiyonu eğer stdin tamponununda karakter varsa hemen karakteri tampondan alır. Ancak stdin tamponu boşsa bir satırlık bilgiyi tampona yerleştirir ve onun ilk karakterini verir. Biz kalvyeden "a" tuşuna basıp ENTER tuşuna basmış olalım. stdin tamponu şöyle olacaktır: a\n İlk getchar fonksiyonu a karakterini tampondna alıp geri döncektir. İkinci getchar fonksiyonu da tampon dolu olduğu için tampondaki \n karakterini alıp hemen geri dönecektir. C'ye yeni başlayanlar ikinci getchar fonksiyonunun neden klavyeden okumaya yol açmadığına şaşırmaktadır. Muhtemelen burada satır tamponlamalı çalışma mekanizması bilinmemektedir. * Örnek 1.0, Aşağıdaki programda birinci getchar stdin tamponu boş olduğu için bir satırlık bilgiyi stdin tamponuna yerleştirecek ve tampondaki ilk karakter ile geri dönecektir. İkinci getchar tampon boş olmadığı tamponda \n karakteri olduğu için onu alıp onunla geri dönecektir. Dolayısıyla ikinci getchar klavyeden bir giriş beklemeyecektir. #include int main(void) { int ch; ch = getchar(); printf("%c (%d)\n", ch, ch); ch = getchar(); printf("%c (%d)\n", ch, ch); return 0; } * Örnek 1.1, stdin işin başında açılmış olan bir C dosyası olduğuna göre onun toplamda tek bir tamponu vardır. Yani stdin dosyasından okuma yapan standart C fonksiyonları aynı tamponu kullanmaktadır. Aşağıdaki örnekte birinci getchar stdin tamponunu bir satır ile dolduracaktır. Tamponun sonunda da \n karakteri bulunacakır. gets fonksiyonu (her ne kadar C11 ile C'den kaldırıldıysa da) \n karakterine kadar stdin dosyasından okuma yapıp okudukarını parametresiyle belirtilen adrsten itibaren yerleştirmektedir. gets fonksiyonu stdin dosyasında \n karakterini gördüğünde onu okur ancak parametresiyle aldığı adrese yerleştirmez. Onun yerine bu adrese null karakter yerleştirmektedir. Dolayısıyla aşağıdaki örnekte gets fonksiyonu klavyeden bir giriş beklemeyecektir. #include int main(void) { int ch; char buf[1024]; ch = getchar(); printf("%c\n", ch); gets(buf); printf("%s\n", buf); return 0; } * Örnek 1.2, Aşağıdaki döngü nasıl bir etki oluşturur? int ch; ... while ((ch = getchar()) != EOF) putchar(ch); Burada ilk getchar stdin tamponu boş olduğu için bir satır bilgi isteyip onu tampona yerleştirecektir. Biz burada "ankara" yazısını girip ENTER tuşuna basmış olalım. Tamponun durumu şöyle olacaktır: ankara\n İlk getchar tampondan 'a' karakterini alıp bunu ekrana (stdout dosyasına) yazdıracaktır. Diğer getchar çağrıları tampon dolu olduğu için diğer karakterleri alıp yazdıracktır. Tamponun sonundaki \n karakteri de alınıp ekrana yazdırılacaktır. Ancak bu karakter ekrana yazdırıldığında imleç aşağı satırın başına geçecektir. Artık tampon boştur. O halde yazdığımız satırın aynısı ekrana yazılmış olacaktır. Tampon boş olduğu için aynı olaylar yinelenecektir. Burada Windows'ta Ctrl+z tuşları ile UNIX/Linux sistemlerinde Ctrld+d tuşları ile EOF etkisi yaratarak döngünden çıkabiliriz. #include int main(void) { int ch; while ((ch = getchar()) != EOF) putchar(ch); return 0; } * Örnek 1.3.0, Biz gerçekten klavyedem yeni bir giriş yapılması istiyorsak bu durumda stdin tamponunu manuel bir biçimde boşaltmamız gerekir. stdin tamponunu başaltabilen standart bir C fonksiyonu yoktur. '\n' karakteri görene kadar stdin tamponundan karakterleri atmak için aşağıdaki gibi bir döngü yeterli olmaktadır: while (getchar() != '\n') ; Bu döngüde en son '\n' karakteri okunacak ve döngüden çıkılacaktır. Döngüden çıkıldığında stdin tamponu artık boş olduğuna göre sonraki fonksiyonlar klavyeden yeni bir giriş isteyecektir. Bu tür durumlarda stdin dosyasının bir disk dosyasına yönlendirilmesi ve dosyanın sonunda '\n' karakterinin olmaması bir sonsuz döngü oluşumuna yol açabilir. Bu durumu ihmal edebilirsiniz. Eğer bu durumu da ele almak istiyorsanız döngüyü aşağıdaki gibi düzenleyebilirsiniz: while ((ch = getchar()) != '\n' && ch != EOF) ; stdin taponun boşaltılması için bir fonksiyon yazılabilir: void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } Tabii bu fonksiyonu biz zaten boşken çağırırsak fonksiyon bizden giriş ister. Bu fonksiyonu tampon doluyken çağırmalıyız. #include void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } int main(void) { int ch; char buf[1024]; ch = getchar(); printf("%c\n", ch); clear_stdin(); ch = getchar(); printf("%c\n", ch); clear_stdin(); gets(buf); puts(buf); return 0; } * Örnek 1.3.1, Anımsanacağı gibi blok içeren makroların do-while biçiminde yazılması gerekmektedir. Yukarıdaki fonksiyon makro biçiminde aşağıdaki gibi yazılabilir: #define clear_stdin() \ do \ { \ int ch; \ \ while ((ch = getchar()) != '\n' && ch != EOF) \ ; \ } \ while (0) Aşağıda bu makronun kullanımına ilişkin bir örnek verilmiştir: #define clear_stdin() \ do \ { \ int ch; \ \ while ((ch = getchar()) != '\n' && ch != EOF) \ ; \ } \ while (0) int main(void) { int ch; char buf[1024]; ch = getchar(); printf("%c\n", ch); clear_stdin(); ch = getchar(); printf("%c\n", ch); clear_stdin(); gets(buf); puts(buf); return 0; } İstersek işin başında stdout dosyasının da default tapmonlama modunu setbuf ya da setvbuf fonksiyonlarıyla değiştirebiliriz. * Örnek 1, #include int main(void) { setvbuf(stdout, NULL, _IONBF, 0); printf("ali"); for (;;) ; return 0; } Şimdi de stdin dosyasından okuma yapan standart C fonksiyonları üzerinde biraz duralım. Bu fonksiyonlardan, >> "gets" : Bu fonksiyon yukarıda belirttiğimiz gibi C99'da deprecated yapılmış C11'de C'den çıkartılmıştır. Ancak bu fonksiyon yine de yaygın derleyicilerde geçmişe uyum için bulundurulmaktadır. Fonksiyonun prototipi şöyledir: #include char *gets(char *s); gets fonksiyonu stdin dosyasından okuma yapar okuduğu karakterleri programcının belirlediği diziye yerleştirir. En son '\n' karakterini okuduğunda ya da dosya sonuna gelindiğinde bu karakter yerine hedef diziye '\0' yerleştirip işlemini sonlandırmaktadır. Yani gets fonksiyonu '\n' karakteri görene kadar ya da dosya sonunagelinene kadar stdin dosyasından okuma yapmaktadır. Ancak '\n' karakterini hedef diziye yerleştirmemektedir. gets fonksiyonu başarı durumunda parametresiyle belirtilen adresin aynısına geri dönmektedir. Eğer gets hiçbir karakter okuyamadan EOF ile karşılaşırsa diziye bir yerleştirme yapmaz ve NULL adresle geri döner. gets aynı zamanda IO hatası oluştuğunda da NULL adresle geri dönmektedir. Bu durumda dizinin içeriği yarı doldurulmuş bir biçimde olabilir. gets fonksiyonu genellikle getchar fonksiyonu kullanılarak yazılmaktadır. Tipik bir gerçekleştirimi aşağıdaki gibi olabilir. * Örnek 1, #include char *mygets(char *s); int main(void) { char buf[1024]; printf("Bir yazi giriniz:"); fflush(stdout); mygets(buf); puts(buf); return 0; } char *mygets(char *s) { int ch; size_t i; for (i = 0; (ch = getchar()) != '\n' && ch != EOF; ++i) s[i] = ch; if (ch == EOF) if (i == 0 || ferror(stdin)) return NULL; s[i] = '\0'; return s; } >> "gets_s" : Yukarıda da belirtitğimiz gibi C11'de gets fonksiyonu kaldırılmış ve yerine isteğe bağlı biçimde gets_s fonksiyonu eklenmiştir. gets_s Microsoft derleyicilerinde zaten uzun süredir bulunmaktadır. Ancak gcc ve clang'ın kullandığı glibc kütüphanesinde bu fonksiyon bulunmamaktadır. gets_s fonksiyonunun prototipi şöyledir: char *gets_s(char *s, rsize_t n); Fonksiyon stdin dosyasındna en fazla iinci parametresiyle beliritlen sayıdan bir eksik karakter okur. Yine'\n' karakterini gördüğünde yada dosya sonuna geldiğinde ya da IO hatası oluştuğunda işlemini sonlandırır. Fonksiyonun geri dönüş değeri gets fonksiyonundaa olduğu gibidir. * Örnek 1, Aşağıda bu fonksiyonun gerçekleştirimi yapılmıştır. #include char *mygets_s(char *str, size_t size); int main(void) { char buf[5]; mygets_s(buf, 5); puts(buf); return 0; } char *mygets_s(char *str, size_t n) { size_t i; int ch; if (str == NULL || n == 0) return NULL; for (i = 0; i < n - 1; ++i) { if ((ch = getchar()) == '\n') break; if (ch == EOF) { if (i == 0 || ferror(stdin)) return NULL; break; } str[i] = ch; } str[i] = '\0'; return str; } >> "fgets" : gets fonksiyonu gğvensiz olduğundan ve C'den kaldırıldığı için ve gets_s fonksiyonu da isteğe bağlı bulundurulduğu için programcılar gets yerine fgets fonksiyonunu tercih etmektedir. fgets fonksiyonunun prototipini anımsayınız: #include char *fgets(char *s, int size, FILE *stream); fgets fonksiyonu gets_s fonksiyonuna benzemekle birlikte önemli bir farklılığa sahiptir. fgtes eğer '\n' karakterini erken görürse bu '\n' karakterini de diziye yerleştirmektedir. Oysa gets ve gets_s hiçbir zaman '\n' karakterini diziye yerleştirmemektedir. fgets fonksiyonunun diğer davranışları gets_s fonksiyonundaki gibidir. Örneğin: fgets(buf, 10, stdin); Burada fgtes en fazla 9 karakter okuyacaktır. Çünkü dizinin sonuna null karakteri de yerleştirecektir. Girilen karakterler şunlar olsun: ankara\n Burada fgtes '\n' dahil olmak üzere bu karakterlerin hepsini diziye yerleştirir ayrıca yazının sonuna null karakteri de ekler. Şimdi girilen karakterler şunlar olsun: kastamonu\n Burada fgets 9 karakter okurken '\n' karakterini görmediği için '\n' karakterini diziye yerleştirmez. Ancak null karakteri diziye yerleştirir. Yine fgets henüz hiçbir karakter okunmadan dosya sonuna gelinmişse NULL adresle geri dönmektedir. Normal durumda fgets parametresi ile belirtilen adresin aynısına geri dönmektedir. Görüldüğü fgets maalesef bazı durumlarda '\n' karakterini diziye yerleştirmekte bazı durumlarda yerleştirmemektedir. İşte programcılar genellikle fgets çağrısında sonra "eğer diziye '\n' karakteri yerleştirilmişse onun yerine null karakteri yerleştirmektedir. fgets fonksiyonu ile stdin dosyasından yazı okuma işlemi tipik olarak şöyle yapılmaktadır: char buf[10]; char *str; ... fgets(buf, 10, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; Burada dizinin sonunda '\n' karakterinin olup olmadığına bakılmış eğer '\n' karakteri dizisinin sonunda varsa onun yerine null karakter diziye yerleştirilmiştir. Burada fgtes hiçbir okuma yapılmadan dosya sonu ile karşılaşırsa (yani örneğin kullanıcı hemen Ctrl+z ya da Ctrl+d tuşlarına basarsa) bir anomali oluşacaktır. Programcı bu durumu da eğer gerekiyorsa kontrol etmelidir. >> "scanf" : scanf fonksiyonunun prototipi şöyledir: #include int scanf(const char *format, ...); Fonksiyon aslında fscanf fonksiyonunun stdin dosyasından çalışan biçimidir: #include int fscanf(FILE *f, const char *format, ...); scanf fonksiyonunun bazı ayrıntıları vardır. Biz burada fonksiyonun tamponlamayı ilgilendiren bazı özelliklerini açıklayacağız. scanf fonksiyonu stdin dosyasından karakter karakter okuma yapar. Eğer format karakterine uygun olmayan bir karakter ile karşılaşırsa onu dosyaya (tampona) geri yerleştirir ve işlemi sonlandırır. scanf normal durumda başarılı olarak yerşletirdiği parçasayısına geri dönmektedir. Örneğin: result = scanf("%d%d", &a, &b); Şimdi dosyada (tamponda) şu karakterler bulunyor olsun: 123istanbul\n scanf 123 sayısını a nesnesine yerleştirir. Ancak 'i' karakteri "%d" format karakterine uymadığı için işlemini keser. Yani b nesnesine bir yerleştirme yapmaz ve 1 değerine geri döner. Dosyadaki (tampondaki) karakterler şöyle olsun: istanbul\n Burada scanf hiç yerleştirme yapamayacağı için 0 ile geri dönecektir. scanf fonksiyonu baştaki boşluk karakterlerini (leading space) atmaktadır. Ancak baştaki boşluk karakterlerini atıp henüz format karakterine uygun olmayan bir karakterle de karşılaşmadan dosya sonunu görürse bu durumda EOF özl değerine geri dönmektedir. Örneğin dosyanın içeriği şöyle olsun (_ karakteri SPACE karakterlerini belirtmektedir): ___EOF Bu durumda scanf EOF değerine geri dönecektir. Ancak örneğin: ___istanbulEOF Bu durumda scanf 0 değerine geri döncektir. scanf işle birden fazla okuma yapılırken scanf baştaki ve girişler arasındaki boşluk karakterlerini atmaktadır. Örneğin: result = scanf("%d%d", &a, &b); stdin dosyasında (tamponunda) şu karakterler olsun (_ karakteri SPACE karakterlerini belirtmektedir): __10___20__\n scanf burada iki yerleştirmeyi de başarılı bir biçimde yapacak ve 2 değerine geri dönecektir. scanf fonksiyonun baştaki boşluk karakterlerini (leading space)" attığına ancak sondakileri (trailing space) atmadığına dikkat ediniz. Yukarıdaki okumadna sonra stdin dosyasında (tamponda) şu karakterler kalacaktır: __\n Aşağıda bu fonksiyonun kullanımına ilişkin bir örnek verilmiştir. * Örnek 1, scanf fonksiyonu ile menü oluştururken geçirsiz girişlerin ele alınması: #include #include void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } int disp_menu(void) { int choice; printf("1) Kayit ekle\n"); printf("2) Kayit sil\n"); printf("3) Arama\n"); printf("4) Cikis\n\n"); printf("Seciminiz:"); fflush(stdout); if (scanf("%d", &choice) == 0) { clear_stdin(); return 0; } return choice; } int main(void) { int choice; for (;;) { choice = disp_menu(); switch (choice) { case 1: printf("kayit ekleniyor\n\n"); break; case 2: printf("kayit siliniyor\n\n"); break; case 3: printf("arama yapiliyor\n\n"); break; case 4: goto EXIT; default: printf("gecersiz giris!..\n\n"); break; } } EXIT: return 0; } * Örnek 2, scanf fonksiyonunda format karakterleri arasındaki karakterlerin girişlerde mutlaka bulundurulması gerekir. Örneğin: scanf("%d/%d/%d", &day, &month, &year); Burada her ilk iki int değerden hemen sonra bir tane '/' karakterinin kullanılması gerekir. Uygun bir giriş şöyle olabilir: 12/10/2009 '/' karakterelerinin solunda ve sağında boşluk karakterleri bulundurulamaz. Aşağıda bu kullanıma ilişkin program kodları verilmiştir: #include int main(void) { int day, month, year; printf("Tarihi dd/mm/yyyy biçiminde giriniz:"); scanf("%d/%d/%d", &day, &month, &year); printf("%d-%d-%d\n", day, month, year); return 0; } * Örnek 3, scanf fonksiyonunda format karakterlerindeki herhangi bir boşluk karakteri (SPACE ve \n olabilir) "boşluk karakteri görmeyene kadar stdin'den okuma yap ve onları at" anlamına gelmektedir. C'yi yeni öğrenenler bu durumu bilmedikleri için scanf fonksiyonunu yanlışlıkla printf gibi kullanabilmektedir. Örneğin: scanf("%d%d\n", &a, &b); Burada programcı yanlışlıkla format karakterlerinin sonuna '\n' karakterini yerleştirmiştir. (Burada '\n' yerine SPACE karakteri de yerleştirilseydi davranışta bir değişiklik olmayacaktı.) scanf fonksiyonu formatlarinde bir boşluk karakteri gördüğünde "boşluk görmeyene kadar stdin dosyasından okuma" yapmaya çalışmaktadır. FDolayısıyla burada iki değer girildikten sonra csanf fonksiyonu hemen işlemini bitirmeyecektir. Örneğin: scanf("%d / %d / %d", &day, &month, &year); Burada artık '/' karakterlerinin solunda ve sağında boşluk karakterleri olabilmektedir. scanf fonksiyonunun baştaki boşluk karakterlerini (leading space) attığına, ancak sondakileri (trailing space) atmadığına dikkat edniz. Bu durumda scanf fonksiyonundan sonra gets, getchar gibi fonksiyonları kullanmadan önce stdin tamponun boşaltılması gerekebilir. #include void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } int main(void) { int a; char ch; printf("Bir sayi giriniz:"); fflush(stdout); scanf("%d", &a); clear_stdin(); printf("Bir karakter giriniz:"); fflush(stdout); ch = getchar(); printf("%d %c\n", a, ch); return 0; } * Örnek 4, Daha önceden de belirttiğimiz gibi scanf eğer baştaki bpşluk karakterlerini attıktan sonra EOF ile karşılaşırsa EOF özel değerine geri dönmektedir. #include int main(void) { int val; while (scanf("%d", &val) != EOF) printf("%d\n", val); return 0; } Öte yandan POSIX veya Windows API fonksiyonları ile elde ettiğimiz dosya betimleyicisini veya o dosyaya ilişkin HANDLE değerini, tamponlu işlemler yapabilmek için direkt olarak standart C fonksiyonlarının çağrılmasında kullanamamaktayız. Bunun için ilgili dosya betimleyicisini veya HANDLE değerini, FILE * türünden bir betimleyiciye dönüştürmemiz gerekmektedir. İşte bunun için, >> UNIX/Linux Sistemlerinde "fdopen" isimli fonksiyon kullanılır. Bu fonksiyon open ile elde ettiğimiz ilgili dosya betimleyicisini FILE * türünden bir dosya betimleyicisine dönüştürür. Fonksiyonun prototipi şöyledir: #include FILE *fdopen(int fd, const char *mode); Fonksiyonun birinci parametresi open fonksiyonundan eldee dilmş olan dosya betimleyicisini, ikinci parametresi ise dosya açış modunu belirtmektedir. Fonksiyon başarı durumunda dosya bilgi göstericisine (stream), başarısızlık durumunda NULL adrese geri dönmektedir. Şüphesiz burada belirtilen açış modu ile open fonksiyonunda belirtilen açış modunun uyumlu olması gerekmektedir. Tabii bu biçimde bir dönüştürmeden sonra dosya fclose ile kapatıldığında fclose zaten söz konusu betimleyici ile close fonksiyonunu çağıracaktır. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; FILE *f; int ch; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); if ((f = fdopen(fd, "r")) == NULL) exit_sys("fdopen"); while ((ch = fgetc(f)) != EOF) putchar(ch); if (ferror(f)) { fprintf(stderr, "cannot read!..\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Şüphesiz bu sistemlerde fdopen fonksiyonunun tersini yapan fileno isimli bir fonksiyon da bulunmaktadır. Bu fonksiyon FILE * türünden dosya bilgi göstericisini bizden alır bize aşağı seviyeli POSIX dosya fonksiyonlarında kullanılabilecek dosya betimleyicisini verir. Fonksiyonun prototipi şöyledir: #include int fileno(FILE *stream); Fonksiyon başarı durumunda dosya betimleyicisine, balarısızlık durumunda -1 değerine geri dönmektedir. errno değişkeni uygun biçimde set edilmektedir. * Örnek 1, Aşağıdaki örnekte önce bir dosya fopen fonksiyonuyla açılmış sonra da fileno fonksiyonuya dosya betimleyicisi elde edilip read fonksiyonu ile dosyadna okuma yapılmıştır. #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; FILE *f; char buf[30 + 1]; ssize_t result; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((f = fopen(argv[1], "r")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } if ((fd = fileno(f)) == -1) exit_sys("fileno"); if ((result = read(fd, buf, 30)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); fclose(f); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >> Windows Sistemlerinde, fdopen ve fileno fonksiyonlarının mantıksal olarak eşdeğerleri yoktur. Anımsanacağı gibi Windows'ta dosya betimleyicisi yerine HANDLE türü ile temsil edilen handle değerlei kullanılıyordu. Ancak Microsoft DOS zamanlarından beri POSIX fonksiyonlarına benzer dosya fonksiyonlarını da bulundurmaktadır. Tabii bı fonksiyonlar işletim sistemi çekirdeği tarafından desteklenen fonksiyonlar değildir. Bu fonksiyonlar user modda çalışan bir çeşit "sarma (wrapper)" fonksiyonlardır. Yani söz konsu bu fonksiyonlar aslında arka planda Windows'un API fonksiyonları çağrılarak gerçekleştirilmiştir. Microsoft'un UNIX/Linux sistemlerindeki POSIX fonksiyonlarına benzer fonksiyonlarının prototipleri dosyası içerisinde bulunmaktadır. Bu fonksiyonlar klasik UNIX tarzı harflendirme ile isimlendirilmiştir ve bunların başlarında bir '_' karakteri bulunmaktadır. Örneğin: _open ve _sopen_s _read _write _close _lseek ... İşte bu içerisinde _fdopen ve _fileno fonksiyonları da bulunmaktadır. Ancak bu fonksiyonlar UNIX/Linux sistemlerindeki gibi parametrik yapıya sahiptirler. * Örnek 1, Aşağıda Microsoft'a özgü "sarma fonksiyonları" kullanılarak UNIX/Linux stili küçük bir program yazılmıştır. #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; FILE *f; int ch; if ((fd = _open("sample.c", _O_RDONLY)) == -1) exit_sys("_open"); if ((f = _fdopen(fd, "r")) == NULL) exit_sys("_fdopen"); 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); } Pekiyi biz sıfırdan C'nin stdio kütüphanesini yazmak istersek bunu nasıl yapabiliriz? Böyle bir standart stdio kütüphanesi yazabilmek için kabaca aşağıdaki işlemlerin yapılması gerekmektedir: >> Öncelikle bir FILE yapısının oluşturulması gerekir. C standartları FILE yapısının elemanları hakkında hiçbir açıklama yapmamıştır. Dolayısıyla FILE yapısını istediğiniz gibi oluşturabilirsiniz. Ana nokta bu FILE yapısının oluşturulmaıdır. Örneğin: typedef struct { .... } FILE; Pekiyi bu FILE yapısının içerisindeki elemanlar neler olmalıdır? Bu FILE yapısının elemanları tamponu yönetebilmek için gerekli bilgileri barındırmalıdır. Kabaca bu yapıda tutulması gereken elemanlar şunlardır: -> Aşağı seviyeli dosya işlemlerinde kullanılacak işletim sistemine özgü handle değeri (yani UNIX/Linux sistemleri için dosya betimleyicisi, Windows sistemleri için HANDLE değeri). -> Dosyanın açış modu -> Tamponun yeri ve uzunluğu -> Tamponun aktif noktası (yani dosya işlemi yapıldığında tamponun neresinden işlem yapılacaktır) -> Tamponun kirli (dirty) olup olmadığı bilgisi -> Asıl dosyanın neresinin tamponda olduğu bilgisi -> Son işlem EOF yüzünden başarısızsa bu bilgi FILE yapısının içerisindeki bir bayrakta tutulmalıdır. Benzer biçimde son yapılan IO işlemi başarısızsa bu da bir bayrakla tutulmalıdır. feof ve ferror fonksiyonları bu bayraklarla geri dönebilir. -> Diğer bilgiler >> fopen fonksiyonu yazılırken önce FILE yapısı ve tampon tahsis edilmeli sonra dosya işletim sisteminni aşağı seviyeli fonksiyonlarıyla açılmalıdır. Örneğin: FILE *fopen(const char *path, const char *mode) { 1) mode parametresi parse edilmeli 2) FILE yapısı tahsis edilmeli 3) Tampon tahsis edilip bilgileri FILE yapısının içerisinde saklanmalı 4) Diğer işlemler 5) FILE yapısının adresi ile geri dönülmeli. } >> ... C'nin Stdio ktüphenesinin yazımı için bir ipucu: * Örnek 1, Tam bitilmiş bir kod değil: /* csd_stdio.h */ #ifndef CSD_STDIO_H_ #define CSD_STDIO_H_ #define CSD_EOF -1 #define CSD_BUFSIZ 5 #include typedef struct { int fd; unsigned char *beg; /* starting buffer address */ size_t size; /* buffer size */ size_t count; /* number of bytes in the buffer */ unsigned char *pos; /* current buffer address */ off_t offset; /* file offset */ int dirty; int error; int eof; /* .... */ } CSD_FILE; CSD_FILE *csd_fopen(const char *path, const char *mode); int csd_fgetc(CSD_FILE *f); #endif /* csd_stdio.c */ #include #include #include #include #include "csd_stdio.h" static ssize_t refresh_buffer(CSD_FILE *f); CSD_FILE *csd_fopen(const char *path, const char *mode) { CSD_FILE *f; int i; static char *modes[] = { "r", "r+", "w", "w+", "a", "a+", "rb", "r+b", "wb", "w+b", "ab", "a+b", NULL }; static int flags[] = { O_RDONLY, O_RDWR, O_WRONLY | O_CREAT | O_TRUNC, O_RDWR | O_CREAT | O_TRUNC, O_WRONLY | O_CREAT | O_APPEND, O_WRONLY | O_CREAT | O_APPEND, O_WRONLY | O_CREAT | O_APPEND }; if ((f = (CSD_FILE *)malloc(sizeof(CSD_FILE))) == NULL) return NULL; for (i = 0; modes[i] != NULL; ++i) if (!strcmp(mode, modes[i])) break; if (modes[i] == NULL) { free(f); return NULL; } if ((f->fd = open(path, flags[i % 6], S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) { free(f); return NULL; } if ((f->beg = (unsigned char *)malloc(CSD_BUFSIZ)) == NULL) { free(f); return NULL; } f->size = CSD_BUFSIZ; f->count = 0; f->pos = NULL; f->offset = 0; f->dirty = 0; f->error = 0; return f; } static ssize_t refresh_buffer(CSD_FILE *f) { ssize_t result; if (f->dirty) { if (lseek(f->fd, f->offset, SEEK_SET) == -1) return -1; if (write(f->fd, f->beg, f->pos - f->beg) == -1) return -1; } if (lseek(f->fd, f->offset + f->count, SEEK_SET) == -1) return -1; if ((result = read(f->fd, f->beg, f->size)) == -1) return -1; f->pos = f->beg; f->offset = f->offset + f->count; f->count = result; return result; } int csd_fgetc(CSD_FILE *f) { ssize_t result; if (f->pos == NULL || f->pos == f->beg + f->count) { if ((result = refresh_buffer(f)) == -1) { f->error = 1; return CSD_EOF; } if (result == 0) { f->eof = 1; return CSD_EOF; } } return *f->pos++; } int csd_ferror(CSD_FILE *f) { return f->error; } /* sample.c */ #include #include #include #include "csd_stdio.h" void exit_sys(const char *msg); int main(void) { CSD_FILE *f; int ch; if ((f = csd_fopen("test.txt", "r")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } ch = csd_fgetc(f); putchar(ch); while ((ch = csd_fgetc(f)) != CSD_EOF) putchar(ch); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > Hatırlatıcı Notlar: >> Hex Editor: HxD - Freeware Hex Editor and Disk Editor >> C'nin dosya fonksiyonlarının kullandığı tamponun büyüklüğü içerisindeki BUFSIZ sembolik sabitiyle dış dünyaya ifade edilmiştir. gcc derleyicilerinde (yani glibc kütüphanesinde) BUFIZ değeri 8192, Microsoft derleyicilerinde 512 biçimindedir. Tabii biz BUFSIZ değerini değiştirmekle bu tamponun büyüklüğünü değiştirmiş olmayız. Çünkü kütüphane kodları çoktan derlenmiştir. BUFSIZ sembolik sabiti bizim kullanılan tampon büyüklüğünü öğrenebilmemiz için içerisinde bulundurulmuştur. >> getchar fonksiyonu stdin dosyasından tek bir karakter okur ve okudğu karakterin sıra numarasına geri döner. getchar dosyanın sonuna gelindiğinde ya da IO hatası oluştuğunda EOF değerine geri dönmektedir. Fonksiyonun prototipi şöyledir: int getchar(void); getchar fonksyonu aşağıdakiyle eşdeğerdir: fgetc(stdin) Hatta bazı eski C derleyicilerinde getchar bir makro biçiminde aşağıdkai gibi yazılmıştır: #define getchar() fgetc(stdin) /*================================================================================================================================*/ (37_28_10_2023) & (38_04_11_2023) > Proseslerin Çevre Değişkenleri: İşletim sistemleri dünyasında sıkça karşımıza çıkan önemli bir kavram da "çevre değişkenleri (environment variables)" denilen kavramdır. Sistem programcısının bu kavramı bilmesi ve gerektiğinde bundan faydalanması önemlidir. Biz de bu bölümde "proseslerin çevre değişkenleri" üzerinde duracağız. Proseslerin çevre değişkenleri "anahtar-değer (key-value)" çiftlerini tutan, anahtar verildiğinde ona karşı gelen değerin elde edildiği sözlük tarzı bir listedir. Anahtarlar ve değerler birer yazı (string) biçimindedir. Çevre değişkenleri konusunda söz konusu anahtar yazıya "çevre değişkeni (environment variable)" ona karşılık gelen yazıya da "çevre değişkeninin değeri" denilmektedir. * Örnek 1, Bir çevre değişkeni "CITY" isminde olabilir. Ona karşı gelen değer de "Ankara" olabilir. Çevre değişkenleri ilk bakışta "basit bir sözlük veri yapısı" gibi düşünülmektedir. Konuya yeni başlayan kişiler böyle bir veri yapısının neden işletim sistemi düzeyinde oluşturulduğunu neden kütüphane fonksiyonlarıyla oluşturulmadığını merak etmektedir. Gerçekten de pek çok programlama dilinin standart kütüphanesinde ya da bizzat sentaks ve semantik yapısı içerisinde sözlük (dictiobary) tarzı veri yapıları hazır bir biçimde bulunmaktadır. Genellikle işletim sistemleri (örneğin Windows, Linux, macOS) prosesin çevre değişkenlerini "anahtar-değer" çiftleri biçiminde liste tarzı bir veri yapısında saklamaktadır. Çevre değişkenleri için bu sistemlerde prosese özgü bellek bölgelerei bulundurulmaktadır. Yani çevre değişkenleri pek çok işletim sisteminde proses kontrol blokta değil bizzat prosesin bellek alanında tutulmaktadır. Prosesin çevre değişken listesinin tutulduğu alan İngilizce genellikle "environment block" biçiminde isimlendirilmektedir. İşletim sisteminde prosesin çevre değişkenleri default durumda genellikle proses yaratılırken üst prosesten alt prosese aktarılmaktadır. Yani bir programı hangi program çalıştırıyorsa çalıştırılan program (alt process) çevre değişkenlerini çalıştıran programdan (üst proses) almaktadır. Ancak program çalışmaya başladıktan sonra programcı bu çevre değişkenleri üzerinde değişiklikler yapabilmektedir. * Örnek 1, Biz Linux ya da macOS sistemlerinde komut satırından bir program çalıştırdığımızda çalıştırdığımız programın çevre değişkenleri onu çalıştıran komut satırı programında (tipik olarak bash) aktarılmaktadır. Aynı dırım Windows sistemlerinde de böyledir. Windows sistemlerinde bir simgeye çift tıklayarak bir programı çalıştırdığımızda çalıştırılan programın çevre değişkenleri masaüstü prossi olan "explorer.exe" isimli üst prosesten alınmaktadır. Bir sistem programcısının çevre değişikenleri üzerinde bazı işlemleri yapabiliyor olması gerekmektedir. İzleyen paragraflarda çevre değişkenlerine ilişkin bazı önemli ilemlerin nasıl yapıldığı üzerinde duracağız. Tabii çevre değişkenleri programlana dilinden bağımzıs aşağı seviyeli bir konu olduğu için ilgili sistemdeki düşük seviyeli fonksiyonlarla gerçekleştirilmektedir. Çevre değişkenleri üzerinde işlemler Windows sistemlerinde API fonksiyonlarıyla, UNIX/Linux ve macOS sistemlerinde ise POSIX fonksiyonlarıyla yapılmaktadır. Çevre değişkenlerinin (yani anahtarların) Windows sistemlerinde büyük harf küçük harf duyarlılığı yoktur. Ancak UNIX/Linux sistemlerinde ve macOS sistemlerinde büyük harf küçük harf duyarlılığı vardır. Şimdi de proseslerin çevre değişkenlerine ilişkin fonksiyonları görelim: >> Standart C Fonksiyonları: >>> "getenv" : Bir çevre değişkeninin anahtarı verildiğinde onun değerini elde etmek için getenv isimli bir standart C fonksiyonu bulundurulmuştur. Bu fonksiyon genel olarak ilgili sistemdeki daha aşağı seviyeli, fonksiyonları çağırarak işlemi yapmaktadır. getenv fonksiyonunun prototipi şöyledir: #include char *getenv(const char *name); Fonksiyon parametre olarak çevre değişkenin ismini alır. Çevre değişkeninin değerinin bulunduğu char türden dizinin adresine geri döner. Fonksiyonun geri döndürdüğü adresteki dizi statik biçimde tahsis edilmiştir. Dolayısıyla güvenli bir biçimde kullanılabilir. Ancak bu adresteki dizi üzerinde değişiklik yapılmamalıdır. Eğer proseste ilgili çevre değişkeni yoksa fonksiyon NULL adresle geri dönmektedir. Aşağıdaki örnekte komut satırı argümanı ile verilen çevre değişkeninin değeri ekrana (stdout dosyasına) yazdırılmaktadır. * Örnek 1, #include #include int main(int argc, char *argv[]) { char *value; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((value = getenv(argv[1])) == NULL) { fprintf(stderr, "environment variable not found: %s\n", argv[1]); exit(EXIT_FAILURE); } puts(value); return 0; } >> UNIX/Linux (POSIX) Fonksiyonları: Bu tip fonksiyonları ele almadan önce UNIX türevi işletim sisteminin çevre değişkenlerini bellekte nasıl tuttuğunu açıklamak istiyoruz. Çevre değişkenleri için "environ" isminde global bir göstericiyi gösteren gösterici bulundurulmaktadır. Bu environ değişkeni bir gösterici dizisini göstermektedir. Gösterici dizisinin sonunda NULL adres vardır. environ (char **) -----> adres (char *) -----> "anahtar=değer" adres (char *) -----> "anahtar=değer" adres (char *) -----> "anahtar=değer" ... adres (char *) -----> "anahtar=değer" adres (char *) -----> "anahtar=değer" NULL Örneğin yukarıdaki çevre değişken bloğuna anahtarı "CITY" olan değeri "ankara" olan bir çevre değişkeni ekleyelim. environ dizisi şu hale gelecektir: environ (char **) -----> adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş) adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş) adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş) ... adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş) adres (char *) -----> "anahtar=değer" (malloc ile tahsis edilmiş) YENİ EKLENEN ADRES -----> "CITY=ankara" (burası malloc ile tahsis ediliyor) NULL Ancak UNIX/Linux sistemlerinde iş bu "environ" isimli göstericinin extern bildirimi herhangi bir başlık dosyasında bulundurulmamıştır. Bu nedenle programcının environ değişkeninin extern bildirimini kendisinin yapması gerekmektedir: extern char **environ; Tabii biz bu biçimde eçre değişkenlerini elde edeken onları tek bir yazı halinde "anahtar=değer" biçiminde elde edeceğiz. Anahtarla değerleri '=' karaketerini dikkate alarak ayrıştırabilirsiniz. Ayrıştırma işlemini yaparken geçici biçimde '=' karakterini '\0' ile değiştirebilirsiniz. Ancak çevre değişken bloğunu mümkün olduğunca değiştirmeye çalışmayınız. * Örnek 1, Aşağıdaki UNIX/Linux sistemlerinde prosesin tüm çevre değişkenleri elde edilmiştir. #include #include #include extern char **environ; int main(void) { char *str; for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); puts("--------------------------"); for (int i = 0; environ[i] != NULL; ++i) { if ((str = strchr(environ[i], '=')) != NULL) { *str = '\0'; printf("%s --> %s\n", environ[i], str + 1); *str = '='; } } return 0; } Şimdi de bu sistemdeki fonksiyonları inceleyelim: >>> "getenv" : UNIX/Linux sistemlerinde ve macOS sistemlerinde getenv aynı zamanda bir POSIX fonksiyonudur. (Aslında tüm standart C fonksiyonları aynı zamanda bir POSIX fonksiyonu kabul edilmektedir.) Dolayısıyla bu sistemlerde getenv zaten düşük seviyeli bir fonksiyondur. >>> "setenv" : UNIX/Linux sistemlerinde ve macOS sistemlerinde program çalışırken prosesin çevre değişkenine bir ekleme yapmak için setenv isimli POSIX fonksiyonu kullanılmaktadır. Tabii bu fonksiyonla eklenen çevre değişkenleri kalıcı değildir. Proses sonlandığında prosesin bütün çevre değişkenleri zaten yok edilmektedir. setenv fonksiyonunun protoripi şöyledir: #include int setenv(const char *envname, const char *envval, int overwrite); Fonksiyonun birinci parametresi çevre değişkeninin ismini (anahtar değeri), ikinc parametresi ise değerini almaktadır. Son parametre sıfır ya da sıfır dışı bir değer biçiminde girilir. Bu parametre sıfır dışı bir değer rolarak geçilirse söz konusu çevre değişkeninin olduğu durumda artık yeni değer set edilir. Bu parametre 0 geçilirse bu durumda çevre değişkeni varsa fonksiyon yine başarılı olur ancak çevre değişkeninin değeri değişmez. Aşağıdaki örnekte program çevre değişkeninin ismini vedeğerini komut satırı argümanlarıyla almıştır. Önce eçre değişkenini set etmiş sonra da get ederek yazdırmıştır. * Örnek 1, #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { char *value; if (argc != 3) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (setenv(argv[1], argv[2], 1) == -1) exit_sys("setenv"); 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); } >>> "putenv" : putenv isimli POSIX fonksiyonu da prosesin çevre değişkenlerine ekleme yapmak için kullanılabilir. Fonksiyonun prototipi şöyledir: #include int putenv(char *string); Fonksiyon parametre anahtar=değer" biçiminde yazının adresini almaktadır. Alında putenv fonksiyonu yukarıda açıkladığımız çevre değişkenlerine ek yaparken environ göstericisini gösterdiği yeri malloc ile tahsis etmemktedir. Doprudna bizim verdiğimiz adresi o gösterisi dizisine yazmaktadır. Dolayısıyla setenv fonksiyonundan farklı bir çalışması vardır. Örneğin: char buf[] = "COUNTRY=turkey"; putenv(buf); Bu işlemi yaptıktan sonra çevre değişken bloğu aşağıdaki gibi bir hale gelecektir: environ (char **) -----> adres (char *) -----> "anahtar=değer" adres (char *) -----> "anahtar=değer" adres (char *) -----> "anahtar=değer" ... adres (char *) -----> "anahtar=değer" adres (char *) -----> "anahtar=değer" YENI EKLENEN ADRES (buf) -----> "COUNTRY=turkey" NULL Dolayısıyla putenv fonksiyonu ile verilen adresin program sonlanana kadar yaşıyor olması gerekmektedir. putenv fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda sıfır dışı bir değere geri dönmektedir. putenv fonksiyonu ile olan bir çevre değişkeninin değerini de değiştirebiliriz. * Örnek 1, #include #include void exit_sys(const char *msg); int main(void) { char *value; if (putenv("CITY=istanbul") != 0) /* string'ler statik ömürlüdür */ exit_sys("putenv"); if ((value = getenv("CITY")) == NULL) { fprintf(stderr, "cannot find environment variable!..\n"); exit(EXIT_FAILURE); } puts(value); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> "unsetenv" : UNIX/Linux sistemlerinde çevre değişkenlerinin silinmesi için unsetenv POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int unsetenv(const char *name); Fonksiyon silinecek çevre değişkeninin ismini (anahtarını) parametre olarak alır ve siler. unsetenv fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. >>> "clearenv" : Prosesin tüm çevre değişken bloğunu yok etmek için UNIX/Linux sistemlerinde clearenv POSIX fonksiyonu ile yapılmaktadır. Fonksiyonun prototipi şöyledir: #include int clearenv(void); Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda sıfır dışı bir değere (-1 değerine değil) geri dönmektedir. Bu sistemlerde proseslerin çevre değişkenleri ile alakalı kabuk komutları ise şunlardır: >>> "env" : Bu kabuk komutu kabuğun çevre değişken listesini tıpkı yukarıda yazdığımız programda aolduğu gibi görüntülemektedir. Programcılar kendi programlarına aktarılacak çevre değişkenlerini bu komutla görüntüleyebilirler. >>> "export" : Bu kabuk komutu ise kabuk programının kendi çevre değişken listesine ekleme yapmak için kullanılmaktadır. Komutun genel biçimi şöyledir: export değişken=değer Örneğin: export CITY=Ankara Eğer değer boşluk içeriyorsa tırnaklanması gerekir. Örneğin: export CITY="NewYork City" Değişken bir kere export edildikten sonra artık değeri export yazılmadan da değiştirilebilir. Örneğin: CITY=Trabzon Aynı işlem Windows sistemlerinde set komutuyla u yapılmaktadır. Örneğin: set CITY=Ankara Bu sistemlerde değeri değiştirmek için yeniden set kullanmak zorunludur. UNIX/Linux sistemlerinde komut satırında $isim biçimindeki sentaks "bunu çevre değişkeninin değeri ile değiştir" anlamına gelmektedir. Örneğin: $ export CITY=ankara $ $CITY ankara: komut bulunamadı Burada $CITY yazılıp ENTER tuşuna basıldığnda sanki "ankara" yazılıp ENTER tuşuna basılmış gibi bir etki oluşmaktadır. >>> "echo" : Kabuktaki bu komut, yazıları ekrana yazdırmak için kullanılmaktadır. Örneğin: $ echo bugün hava çok güzel bugün hava çok güzel O halde bir eçvre değişkeninin değerini ekrana "echo $isim" komutunu uygulayarak yazdırabiliriz. Örneğin: $ echo $CITY ankara Şimdi de bu kabuk komutlarının kullanımına ilişkin bir örnek verelim: * Örnek 1, Aşağıdaki örnekte de bir çevre değişkeninin değerinin sonuna ekeleme yaapalım: $ export CITY=Ankara $ echo $CITY Ankara $ CITY=$CITY"-Istanbul" $ echo $CITY Ankara-Istanbul Eğer bir çevre değişkeni yoksa kabuk programı, $isim yerine boşluk karakteri yerleştirmektedir. Yani bu durum bir hataya yol açmamaktadır. >> Windows Sistemlerinde: >>> "GetEnvironmentVariable" : Windows sistemlerinde prosesin çevre değişkenini elde edebilmek için GetEnvironmentVariable isimli bi API fonksiyonu kullanılmaktadır. getenv standart C fonksiyonu Windows sistemlerinde aslında bu API fonksiyonu kullanılarak yazılmıştır. GetEnvironmentVariable fonksiyonunun prototipi şöyledir: DWORD GetEnvironmentVariable( LPCTSTR lpName, LPTSTR lpBuffer, DWORD nSize ); Fonksiyonunun birinci parametresi aranacak çevre değişkenini belirtmektedir. İkinci parametre çevre değişkeninin değerinin yerleştirileceği char türden dizinin adresini almaktadır. Üçüncü parametre ise bu dizinin uzunluğunu alır. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısına geri dönmektedir. Bu sayıya null karakter dahil değildir. Eğer verilen dizinin uzunluğu yetersiz olursa fonksiyon dizi için gereken uzunluğu (null karakter dahil olacak biçimde) bize verir. Windows sistemlerinde bir çevre değişkeninin maksimum değeri 32768 karakter olabilmektedir. Fonksiyonda hata kontrolü yapılırken bir noktaya dikkat etmek gerekir. Bir eçvre değişkeni var olduğu halde değeri boş olabilir. Bu durumda GetEnvironmentVariable fonksiyonu yine 0 değerine geri dönecektir. O halde fonksiyonun 0 değerine dönmesinin iki gerekçesi olabilir. Birincisi çevre değişkenini bulamaması, ikincisi çevre değişkenini bulması ancak onun değerinin boş olması. Bu nedenle fonksiyon 0 ile geri döndüğü zaman GetLastError fonksiyonu ile durum tespit edilmelidir. Aşağıda GetEnvironmentVariable fonksiyonunun kullanımına bir örnek verilmiştir. * Örnek 1, #include #include #include #define BUFFER_SIZE 4096 int main(void) { char value[BUFFER_SIZE]; DWORD dwResult; dwResult = GetEnvironmentVariable("PATH", value, BUFFER_SIZE); if ((dwResult == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND)) { fprintf(stderr, "envirionment variable not found!..\n"); exit(EXIT_FAILURE); } if (dwResult > BUFFER_SIZE) { fprintf(stderr, "buffer too small, requşred size: %u\n", dwResult); exit(EXIT_FAILURE); } puts(value); return 0; } >>> "SetEnvironmentVariable" : Windows sistemlerinde çevre değişkenini değiştirmek ve çevre değişken bloğuna yeni bir çevre değişkeni eklemek için SetEnvironmentVariable isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL SetEnvironmentVariable( LPCTSTR lpName, LPCTSTR lpValue ); Fonksiyonun birinci parametresi çevre değişkeninin ismini (yani anahtarını), ikinci parametresi ise değerini almaktadır. Fonksiyon başarı durumunda sıfır dışı bir değere, balarısızlık durumunda 0 değerine geri dönmektedir. Eğer ilgili çevre değişkeni zaten varsa fonksiyon onun değerini değiştirmektedir. Fonksiyonun ikinci parametresi NULL adres geçilebilir. Bu durumda ilgili eçvre değişkeni silinmektedir. SetEnvironmentVariable fonksiyonunda ikinci parametre NULL adres geçilirse ilgili çevre değişkeni silinmektedir. Ancak çevre değişkenlerinin silinmesi seyrek olarak karşımıza çıkabilecek bir durumdur. Nasıl prosesin çevre değişken bloğuna çevre değişkenleri ekleniyorsa aynı zamanda çevre değişken bloğundan bir çevre değişkeni de silinebilir. Ancak oluşturulmuş çevre değişkenlerinin değerlerinin değiştirilmesi çok karşılaşılan bir durum iken çevre değişkenlerinin silinmesi çok seyrek karşılaşılabilecek bir durumdur. Windows sistemlerinde SetenvironmentVariable fonksiyonunun ikinci parametresi NULL adres girilierek ilgili çevre dğeişkeni silinebilir. Örneğin: if (!SetEnvironmentVariable("CITY", NULL)) { fprintf(stderr, "Cannot set environment variable!..\n"); exit(EXIT_FAILURE); } Şimdi de bu konuya ilişkin örneklere bakalım: * Örnek 1, Aşağıdaki SetEnvironmentVariable API fonksiyonunun kullanımına bir örnek verilmiştir. #include #include #include #define SIZE 4096 int main(void) { char val[SIZE]; DWORD dwResult; if (!SetEnvironmentVariable("CITY", "Ankara")) { fprintf(stderr, "Cannot set environment variable!..\n"); exit(EXIT_FAILURE); } if ((dwResult = GetEnvironmentVariable("CITY", val, SIZE)) == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND) { fprintf(stderr, "Environment variable not found!..\n"); exit(EXIT_FAILURE); } if (dwResult > SIZE) { fprintf(stderr, "Insufficient buffer!..\n"); exit(EXIT_FAILURE); } printf("%s\n", val); return 0; } * Örnek 2, Aşağıdaki Windows örneğinde önce PATH çevre değişkeni elde edilip değeri yazdırılmıştır. Sonra bu çevre değişkeni silinmiştir. Sonra yeniden elde edilmeye çalışıldığında sorun oluşacaktır. #include #include #include #define SIZE 4096 int main(void) { char val[SIZE]; DWORD dwResult; if ((dwResult = GetEnvironmentVariable("PATH", val, SIZE)) == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND) { fprintf(stderr, "Environment variable not found!..\n"); exit(EXIT_FAILURE); } if (dwResult > SIZE) { fprintf(stderr, "Insufficient buffer!..\n"); exit(EXIT_FAILURE); } printf("%s\n", val); if (!SetEnvironmentVariable("PATH", NULL)) { fprintf(stderr, "Cannot set environment variable!..\n"); exit(EXIT_FAILURE); } if ((dwResult = GetEnvironmentVariable("PATH", val, SIZE)) == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND) { fprintf(stderr, "Environment variable not found!..\n"); exit(EXIT_FAILURE); } if (dwResult > SIZE) { fprintf(stderr, "Insufficient buffer!..\n"); exit(EXIT_FAILURE); } printf("%s\n", val); return 0; } >>> "GetEnvironmentStrings" : Windows sistemlerinde GetEnvironmentStrings isimli API fonksiyonu çevre değişken listesini tek bir adresle aşağıdaki gibi vermektedir: Anahtar=Değer\0Anahtar=Değer\0Anahtar=Değer\0\0 Fonksiyonun prototipi şöyledir: LPCH GetEnvironmentStrings(VOID); Burada LPCH aslında char türden bir adres türünü belirtmektedir. (LPSTR türü yanlış izlenmim verebileceği gerekçesiyle kullanılmamıştır.) * Örnek 1, Aşağıdaki programda prosesin çevre değişken listesi yazıdırlmıştır. #include #include #include int main(void) { LPCH envStr; if ((envStr = GetEnvironmentStrings()) == NULL) { fprintf(stderr, "Cannot get environment strings!..\n"); exit(EXIT_FAILURE); } while (*envStr != '\0') { puts(envStr); envStr += strlen(envStr) + 1; } return 0; } >>> "FreeEnvironmentStringsA" : Prosesin tüm çevre değişken bloğunu yok etmek için Windows sistemlerinde FreeEnvironmentStrings API fonksiyonu kullanılmaktadır: BOOL FreeEnvironmentStringsA( LPCH penv ); Fonksiyon parametre olarak GetEnvirionmentStrings fonksiyonundan elde edilen adresi almaktadır. Başarı durumunda 0 dışı herhangi bir değere başarısızlık durumunda 0 değerine geri dönmektedir. Bu sistemlerde proseslerin çevre değişkenleri ile alakalı kabuk komutları ise şunlardır: >>> "set" : Windows'ta komut satırında kabuğun tüm çevre değişkenlerini görüntülemek için "set" komutu kullanılmaktadır. Örneğin, C:\>set CITY=ankara >>> "echo" : Windows'ta da yine, tıpkı UNIX/Linux sistemlerinde olduğu gibi, yazıları ekrana bastırmak için kullanılır. Fakat bu sistemlerde %isim% sentaksı uygulanır. Örneğin, C:\>echo %CITY% ankara Şimdi de bu kabuk komutlarının kullanımına ilişkin bir örnek verelim: * Örnek 1, Aşağıdaki örnekte de bir çevre değişkeninin değerinin sonuna ekeleme yaapalım: C:\>set CITY=Ankara C:\>echo %CITY% Ankara C:\>set CITY=%CITY%-Istanbul C:\>echo %CITY% Ankara-Istanbul Peikiyi prosesin çevre değişkenlerinden kim ve nasıl faydalanmaktadır? İşte çevre dğeişkenleri genel olarak kullanıcılar tarafından (ya da bazen programlar tarafından) set edilir. Birtakım fonksiyonlar ve programlar da onlara bakarak eylemleri üzerinde bazı belirlemeler yaparlar. Örneğin biz Microsoft ya da gcc derleyicilerinde derleme işlemini yaparken açısal parantezler içerisinde ya da iki tırnak içerisinde berlirtilmiş olan include dosyaları nerede bu derleyiciler tarafından nerede aranmaktadır? İşte derleyiciler bunları belli bir dizinde ararlar. Ancak komut satırı argümanlarıyla verilen dizinlere ve bazı çevre değişkenlerinin belirttiği dizinlere de bakarlar. Bu sayede biz kendi include dosyalarımızı bir dizine yerleştirebiliriz ve derleyicimizin o dizine bakmasını çevre değişkenini set ederek sağlayabiliriz. Diğer yandan aşağı seviyeli programı kullanırken o programların başvurduğu çevre değişkenlerinin neler olduğunu onların dokümanlarından öğrenebilirsiniz. Örneğin biz bir program yazacak olalım. Programımız bir "data dosyası" kullanacak olsun. Bu data dosyası default durumda programın çalıştığı dizinde "data.dat" isminde bulunabilir. Ancak programımız kullanıcının bu adat dosasının ismini ve yerini değiştirmesine izin verebilir. Bunun için biz DATA_LOCATION isminde bir çevre değişkeni uydurabiliriz. Programı kullanan kişinin bu data dosyasını yerleştirdikten sonra yol ifadesini bu çevre değişkenine yazmasını isteyebiliriz. Böylece kullanıcılar için daha esnek kullanım sunabiliriz. Örneğin: #define DEFAULT_DATA_PATH "data.dat" ... char *data_path; FILE *f; ... if ((data_path = getenv("DATA_LOCATION")) == NULL) data_path = DEFAULT_DATA_PATH; if ((f = fopen(data_path, "r+b")) == NULL) { fprintf(stderr, "cannot open data file: %s\n", data_path); exit(EXIT_FAILURE); } Buarada eğer kullanıcı DATA_LOCATION isimli eçvre değişkenini oluşturmamışsa data dosyası bulunulan dizinde "data.dat" ismiyle aranacaktır. Eğer kullanıcı bu çevre değişkenini set etmişse bu durumda bu çevre değişkeninin belirttiği yol ifadesi ile data dosyasının yeri tespit edilecektir. Çevre değişkenlerini işletim sistemi düzeyindeki global değişkenler gibi düşünebilirsiniz. Bunları birileri set edip birileri kullanmaktadır. Kursumuz ilerledikçe çeşitli eçvre değişkenlerinin kim tarafından ve nasıl kullanıldığına yönelik gerçek örneklerle karşılaşacağız. Aslında çevre değişkenleri üst prosesten alt prosese aktarılmaktadır. Program kabuk üzerinde çalıştırılıyorsa kabuğun çevre değişkenleri kabuktan çalıştırılan programlara aktaralacaktır. Başka bir terminal penceresi açtığımızda o eçvre değişkeni orada görünmeyecektir. Çünkü her terminalde aynı kabuk çalışıyor olsa bile bunlar farklı proseslerdir. (Örneğin biz sample programını birden fazla kez çalıştırabiliriz. Bunların her biri ayrı prosesler olur.) Pekiyi biz çevre değişkenlerinin her kabuk tarafından görünmesini nasıl sağlayabiliriz. İşte bu işlemler genel Windows ve UNIX/Linux (macOS'te UNIX türevi bir sistem gibidir) sistemlerinde farklı biçimlerde sağlanmaktadır. Fakat aslında gerçekleştirilen şey, o programı çalıştıran kabuk programının kendisinin çevre değişkenlerini değiştirilmesidir. Şimdi de ilgili sistemlerde bu işin nasıl yapıldığını inceleyelim: >> UNIX/Linux Sistemlerinde: UNIX/Linux sistemlerinde bir kabuk programı çalıştırılırken önce bazı "script dosyalarına" bakıp oradaki komutları çalıştırmaktadır. Kabuk programlarının çalışmaya başladığında otomatik olarak baktığı bu dosyalara İngilizce "shell start-up files" denilmektedir. Bu dosyaların neler olduğu kabuktan kabuğa ve kabuğun çalıştırma biçimlerine göre değişebilmektedir. Bix burada bash kabuğunun start-up dosyalarına değineceğiz. bash kabuğunun baktığı start-up dosyalar onun nasıl çalıştırıldığına göre değişebilmektedir. bash programı üç biçimde çalıştırılabilmektdir: -> Interactive login shell -> Interactive non-login shell -> Non-interactive shell Burada "interactive" sözcüğü komut satırlı çalışmayı belirtmektedir. Yani kullanıcı bir komut uygular komut çalıştırılır yeniden prompt'a düşülür. Buradaki "login shell" kabuk çalıştırıldığında "user name/password" sorulup sorulmayacağını belirtmektedir. Nihayet kabuk programları da tek bir komut çalıştırılarak sonlandırılabilmektedir. Bu çalışma biçimine de "non-interactive" çalıştırma biçimi denilmektedir. Örneğin biz bash programını "Ctrl+F+N" tuşlarına basarak çalıştrırsak "interactive login shell" biçiminde çalıştırmış oluruz. (Ctrl+F+N tuşlarıya açılan terminallere "sanal terminal (virtual terminal)" da denilmektedir. Eğer biz grafik arayüz içerisinde terminal açarsak buradaki bash programını "interactive non-login shell" biçiminde çalıştırmış oluruz. Bu tür terminal açmaya "sahte terminal (pseudo termimal)" de denilmektedir. Eğer bash "interactive login shell" biçiminde çalıştırılmışsa önce "/etc/profile" dosyasına başvurur. Eğer bu dosya varsa buradaki komutları çalıştırır. Sonra "~/.bash_profile", "~/.bash_login", "~/.profile" dosyalarından ilk olarak hangisini bulursa yalnızca onu çalıştırır. Nihayet biz bash programını "-c" seçenei ile çalıştırırsak bir tek komutu çalıştırıp bash programını sonlandırırız. Bu biçimdeki çalışma da "non-interactive" çalışmadır. Eğer bash "interactive non-login shell" biçiminde çalıştırılırsa bu durumda "~/.bashrc" dosyasını okuyup çalıştıurmaktadır. İŞ bu dosya bazı programlar tarafından oluşturulmuş da olabilir. Bu durumda komutları dosyanın sonuna ekleyebilirsiniz. bash programının dokğmanlarında bu konuyla ilgili bilgileri bulabilirsiniz: https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html >> Windows Sistemlerinde: Windows sistemlerinde aslında masaüstü "explorer.exe" denilen bir prosestir. Dolayısıyla aslında biz masaüstünden bir program çalıştırdığımızda (terminal programı da dahil olmak üzere) bu programı bu "explorer.exe" programı çalıştırmaktadır. Çevre değişkenleri de üst prosesten alt prosese aktarıldığına göre bizim yapmamız gereken şey çevre değişkenlerini kalıcı bir biçimde "explorer.exe" prosesine yerleştirmektir. Bunu sağlamak için Windows 11'de "Ayarlar/Sistem Bilgisi/Gelişmiş Sistem Ayarları/Ortam Değişkenleri" penceresi açılır. Buradaki pencerede alt alta iki bölüm bulunmaktadır. Üstteki o anki aktif kullanıya ilişkin çevre değişkenlerini, aşağıdaki ise sistem genelindeli çevre değişkenlerini belirtmektedir. Eğer biz bir çevre dğeişkeninin belli bir kullanıcı için eklersek o çevre değişkeni yalnızca o kullanıcı ile giriş yapıldığında etkili olmaktadır. Eğer biz bir çevre değişkeninin sistem genelinde eklersek bu durumda bundan tüm prosesler yani başka kullanılar da etkilenecektir. Tabii buradaki pencerelerde çevre değişkenleri üzerinde güncellemeler yapıldığı zaman bu güncellemeler o anda çalışmakta olan prosesleri etkilemeyecektir. Çünkü çevre değişkenleri proses yaratılırken üst prosesten aktarılmaktadır. Bu tür durumlarda programlardan çıkıp onları yeniden çalıştırmalısınız. Pekiyi proses içerisinde pek çok çevre dğeişkeni görmekteyiz. Bunlar nereden gelmektedir? Aslında işletim sistemlerinde sistem boot edilirken peşi sıra bir grup proses yaratılmaktadır. Yani bir program başkasını o da başkasını çalıştırmaktadır. İşte bu sırada çeşitli prosesler yeni çevre değişkenlerini eklerler. Böylece çevre değişkenleri arta arta oluşmaktadır. Tabii en sonunda kabuk programı da yukarıda belirttiğimiz start-up dosyalardaki komutları çalıştırdığında oradan da çevre değişkenleri gelmektedir. Örneğin Linux'ta "user name/password" login isimli program tarafından sorulmaktadır. login programı girişi başarılı bir biçimde yaparsa "/etc/passwd" dosyası içerisinden elde ettiği bilgilerden hareketler HOME, USER, SHELL, PATH, LOGNAME, MAIL çevre değişkenlerini oluşturmaktadır. Kabuk programı login programı tarafından çalıştırılmaktadır. İşte bash isimli kabuk programının kendisi de birtakım çevre değişkenlerini prosese eklemektedir. > Hatırlatıcı Notlar: >> "https://calcoen.web.cern.ch/Linux_keys.htm" adresi üzerinden Linux sistemlerindeki alternatif terminal açma yöntemlerine bakabiliriz. /*================================================================================================================================*/ (39_05_11_2023) & (40_11_11_2023) & (41_12_11_2023) & (42_18_11_2023) & (43_19_11_2023) & (44_25_11_2023) & (45_02_12_2023) (46_03_12_2023) > İşletim Sistemlerinde Prosesler: Daha önceden de belirttiğimiz gibi çalışmakta olan programalara "proses (process)" denilmektedir. İşletim sistemleri bir proses yaaratıldığında prosesi izlemek için ismine "Proses Kontrol Block (Process Control Block)" bir veri yapısı oluşturmaktadır. Windows, Linux, macOS gibi koruma mekanizmasına sahip işlemlerin kullanıldığı işletim sistemlerinde prosesler genel olarak birbirinden izole edilmiştir. Yani biz bir programı birden fazla kez çalıştırdığımızda oluşturulan prosesler birbirinden bağımsızdır. Bundan dolayıdır ki işletim sistemlerinde pek çok özellik, prosese özgdür. Dolaysıyla her prosesin ayrı bir "çalışma dizini (current working directory)"si vardır. Her prosesin açtığı dosyalar diğerinden farklıdır. Yetkiler yine prose özgüdür. Her prosesin bellek alanı biribirinden ayrılmıştır. Örneğin sistemlerin çoğunda "heap" alanı da prosese özgüdür. Eskiden proseslerin tek bir akışı oluyordu. Ancak 90'lı yıllarla birlikte "thread" kavramı işletim sistemlerine sokulmuştur. Bugünkü modern işlem sistemleri "multithread" çalışmaya izin vermektedir. Thread'ler proseslerin bağımsız çizelgelenen akışlarıdır. Örneğin programın bir akışı foo fonksiyonunda ilerlerken diğer bir akışı bar fonksiyonunda ilerleyebilir. İşte böyle prosesin bağımsız akışlarına "thread" denilmektedir. Bir proses tek bir akışka (yani thread ile) çalışmaya başlar. Prosesin diğer thread'lerini programcı kendisi yaratmaktadır. Bugün kullandığımız kapasiteli modern işletim sistemleri ise thread temelinde "zaman paylaşımlı (time sharing)" bir çalışma mekanizması oluşturmaktadır. Bu mekanizmada proseslerin thread'leri bir kuyruk sisteminde toplanır. Buna "çalışma kuyruğu (run queue)" denilmektedir. Çalışma kuyruğundan sıradaki thread alınır, CPU'ya atanır. Belli bir süre onun CPU'da çalışmasına izin verilir. O süre dolduğunda thread CPU'dan alınarak sıradaki diğer thread CPU'ya atanır. O da belli bir süre çalıştırılır. CPU'ya atanan bir thread'in quanta süresini bitirdiğinde CPU'dan kopartılması donanım kesmeleri yoluyla zorla yapılmaktadır. Bu tür sistemlere "preemtive" sistemler denilmektedir. Bazı sistemlerde thread CPU atandığında zorla koparma işlemi yapılmaz. Thread CPU'yu kendisi kendi isteğiyle bırakır. Aslında bu tür sistemler eskiden genellikle thread'siz sistemler olarak karşımıza çıkıyordu. Thread'siz sistem tek thread'li sistem gibi düşünülebilir. Bu tür işletim sistem sistemlerine "non-preemptive" sistemler ya da "cooperative multitask" sistemler denilmektedir. Örneğin Windows 3.X işletim sistemleri, PalmOS işletim sistemleri böyle sistemlerdi. Ancak günümüzde sistemler hep preemtive biçimdedir. Öte yandan bir thread'in parçalı çalışma süresine "quanta süresi (time quantum)" denilmektedir. Quanta süreleri işlemcinin hızı da göz önünde bulundurularak işletim sistemleri tarafından belirlenmektedir. Quanta süresi uzun tutulursa interaktivite azalır, kısa turulursa birim zamanda yapılan iş miktarı (througput) düşer. Kullanıcı programının sürekli çalıştığı gibi bir illüzyon yaşamaktadır. Aslında kullanıcının programı kesikli kesikli çalıştırılmaktadır. Bir thread'in CPU'ya atanıp belli bir süre çalıştırılması ve çalışma bittikten sonra diğer bir thread'in CPU'ya atanması sürecine "bağlamsal geçiş (context switch)" denilmektedir. Tabii context switch işlemi de belli bir zman almaktadır. İşletim sistemelrinin bu işlemlerle uğraşan alt sistemlerine "çizelgeleyici (scheduler)" denilmektedir. Artık günümüzde bilgisayarlarımızda birden fazla işlemci ya da çekşrdek vardır. Ancak zaman paylaşımlı çalışma modelinde bir farklılık oluşmamaktadır. Nasıl bir işletmeden tek bir yemel servis noktası yerine birden fazla servis noktası olduğunda temel kuyruk mekanizmasında bir farklılık oluşmuyorsa aynı durum çok işlemcili ya da çekirdekli sistemlerde de benzer biçimdedir. Tipik olarak işletim sistemleri her CPU ya da çekirdek için ayrı çalışma kuyrukları oluşturmaktadır. Tabii işletim sistemi bu CPU ya da çekirdeklerdeki iş yükünü de dengelemeye çalışır. (Yani örneğin bir CPU ya da çekirdeğin açlışma kuyruğunda az sayıda thread kalmışsa başka kuyruktaki thread'leri buraya taşıyabilmektedir.) İşin özünde prosesler konusunda en temel işlem bir prosesin yaratılmasıdır. Bir prosesin yaratılması demekle aslında dolaylı olarak bir programın çalıştırılması kastedilmektedir. Biz programları işletim sisteminin sunduğu kabuk ortamından faydalanarak çalıştırmaktayız (örneğin Windows'ta bir dosyaya çift tıklayarak, Linux'ta kabukta programın ismini yazarak.) Aslında proseslerin yaratılması işletim sisteminin sistem fonksiyonlarıyla yapılmaktadır. Dolayısıyla kabuk programları da aslında bu sistem fonksiyonlarını kullanmaktadır. Proses yaratmak için Windows'ta sistem fonksiyonlarını çağıran API fonksiyonları UNIX/Linux macOS sistemlerinde de POSIX fonksiyonları bulunmaktadır. Biz bu bölümde önce Windows sistemlerinde sonra UNIX/Linux ve macOS sistemlerinde proses yaratımı üzerinde duracağız. >> Windows Sistemleri: Windows sistemlerinde proses yaratmak için (yani bir programı çalıştırabilmek için) CreateProcess isimli API fonksiyonu kullanılmaktadır. >>> "CreateProcess" : Fonksiyonun prototipi şöyledir: BOOL CreateProcess( LPCTSTR lpApplicationName, LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); Fonskiyonun, >>>> Birinci parametresi çalıştırılacak olan programın yol ifadesini almaktadır. Bu yol ifadesi mutlak ya da göreli olabilir. Fonksiyonun ikinci parametresi programın komt satırı argümanlarını belirtmektedir. Bu komut satırı argümanları tek bir yazı biçiminde oluşturulur. Ancak bu parametrenin const olamayan bir gösterici olduğuna dikkat ediniz. Fonksiyon verilen adreste değişiklik yapıp sonra onu eski haline getirmektedir. Bu nedenle ikinci parametrenin bir string olarak girilmemesi gerekir. (C'de stringlerin karakterlerinin değiştirilmesi tanımsız davranışa yol açmaktadır.) Komut satırı argümanları fonksiyon tarafından boşluk karakterlerinden ayrıştırılıp gösterici dizisine yerleştirilmekte ve main fonksiyonun argv parametresi bu biçimde oluşturulmaktadır. Tabii ilk komut satırı argümanının programın ismi olması zorunlu olmasa da genel bir kabuldür. Örneğin: char szCmdLine[] = "C:\\windows\\notepad.exe test.txt"; ... CreateProcess("C:\\windows\\notepad.exe", szCmdLine, ...); Burada "C:\windows\notepad.exe" programı çalıştırılmak istenmiştir. Komut satırı argümanları boşluk parse edildiğinde iki tane olacaktır: C:\\windows\\notepad.exe test.txt Program için uzantı verilmezse programın default ".exe" uzantılı olduğu kabul edilmektedir. Fonksiyonun birinci parametresi NULL adres geçilebilmektedir. Bu durumda çalıştırılacak program ikicni parametredeki boşluksuz ilk string olarak belirlenmektedir. Örneğin: char szCmdLine[] = "C:\\windows\\notepad.exe test.txt"; ... CreateProcess(NULL, szCmdLine, ...); Burada birinci parametre NULL adres geçilmiştir. Bu durumda fonksiyon ikinci parametrededki ilk string'i çalıştırılacak program olarak ele alır. İkinci parametrenin tamamı yine aynı zamanda komut satırı argümanı olmaktadır. Genellikle programcılar bu birinciyi parametreyi NULL geçip çalıştırılacak programı ve komut satırı argümanlarını ikinci parametreye girerler. Ancak bu durumda çalıştırılacak program boşluklardan parse edildiği için dikkat etmek gerekir. Windows eğer iki tırnaklanmamışsa boşluklardan sırasıyla keserek aramayı yapar. Örneğin ikinci parametre şöyle girilmiş olsun: "c:\\program files\\sub dir\\program name" Burada sistem sırasıyla şu aramaları yapar ve ilk bulduğu dosyayı çalıştırmaya çalışır: c:\program.exe c:\program files\sub.exe c:\program files\sub dir\program.exe c:\program files\sub dir\program name.exe Bu tür durumlarda boşluk içeren kısım iki tırnak içerisine alımabilir. Örneğin: "\"c:\\my folder\\prog.exe\" ali veli selami" >>>> İkinci parametresinde önemli bir özellik vardır. (Ancak bu özellik birinci parametresinde yoktur.) Eğer CreateProcess fonksiyonun birinci NULL geçilip ikinci parametresindeki ilk boşluksuz yazı ile belirtilen program isminde hiçbir ters bölü karakteri kullanılmamışsa bu durum özel bir anlama gelmektedir. Bu durumda söz konusu "çalıtırılabilir (executable)" dosyanın aranması sırasıyla şu biçimde yapılmaktadır: -> CreateProcess uygulayan prosesin ".exe" dosyasının bulunduğu dizin -> CreateProcess uygulayan prosesin çalışma dizini (current working directory) -> 32 Bit Windows Sistem Dizini ("Windows" dizininin içerisindeki "System32" dizini) -> 16 bit Windows Sistem Dizini ("Windows" dizininin içerisindeki "System" dizini) -> Windows dizininin kendisi -> CreateProcess uygulayan prosesin PATH çevre değişkininde belirtlen dizinler sırasıyla Tabii bu sırada arama yapılırken eğer söz konusu dosya bulunursa aramaya deavm edilmemektedir. Burada dikkat edilmesi gereken önemli nokta şudur. Yukarıdaki arama davranışı yalnızca "CreateProcess fonksiyonun birinci paraöetresi NULL geçilip, ikinci paramtresindeki ilk boşluksuz yazının hiçbir ters bölü karakteri içermemesi durumunda" gösterilmektedir. Yukarıdaki arama listesinin sonundaki PATH çevre değişkenine dikkat ediniz. Eğer CreateProcess söz konusu dosyayı diğer dizinlerde bulamazsa PATH çevre değişkeninin değerinde belirtilen dizinlerde de aramaktadır. PATH çevre değişkeninin değeri Windows sistemlerinde ";" karakteriyle ayrılan alanlardan oluşnmaktadır. Her ";" arası ayrı bir dizini belirtmektedir. Örneğin: C:\Program Files (x86)\VMware\VMware Player\bin\;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.4\bin; C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.4\libnvvp;C:\Program Files\Java\jdk-11.0.2\bin; C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\; ... O halde CreateProcess son maddedeki aramaya yapmak için PATH çevre değişkeninin değerini elde etmekte ve buradaki yazıyı ";" karakterlerinden ayrıştırmaktadır. Tabii eğer söz konusu çalıştırılabilir dosya bu PATH çevre değişkeni ile belirtilen birden fazla dizinde varsa ilk bulunan dizindeki program çalıştırılmaktadır. Windows'un komut satırı uygulaması olan "cmd.exe" programında biz komut satırında bir şeyler yazıp ENTER tuşuna bastığımızda CreateProcess bu "cmd.exe" tarafından uygulanmaktadır. Dolayısıyla eğer komutun ilk boşluksuz kısmında hiçbir "\" karakteri yoksa dosya nihayetinde PATH çevre değişkeni ile belirtilen dizinlerde de aranacaktır. Tersten gidersek "eğer biz komut satırında bir programın ismini yazdığımızda onun çalışmasını istiyorsak onun bulunduğu dizini PATH çevre değişkenin değerinde belirtmelitiz." >>>> Üçüncü ve dördüncü parametreleri (lpProcessAttributes ve lpThreadAttributes) kernel tarafından yaratılacak olan proses ve thread nesnelerinin güvenlik bilgilerini (yetki derecelerini) belirtmektedir. Ancak Windows sistemlerinde proseslerin yetki dereceleri UNIX/Linux sistemlerindeki kadar basit değildir. Biz bu kursta bu konu üzerinde durmayacağız. Bu konu "Windows Sistem Programlama" kurslarında ele alınmaktadır. Buradaki SECURITY_ATTRIBUTES bir yapı belirtmektedir. Bu iki parametreye default olarak NULL geçilebilmektedir. >>>> Beşinci parametresi (bInheritHandles) kernel nesnelerinin yaratılmakta olan alt prosese aktarılıp aktarılmayacağnı belirtmektedir. Bu bir ana şalter görevindedir. Bu parametreye sıfır dışı bir değer (örneğin TRUE değeri) geçirilirse aktarım yapılmaktadır. sıfır değeri (FALSE) geçirilirse aktarım yapılmamaktadır. Aslında her kernel nesnesi yaratılırken LPSECURITY_ATTRIBUTES parametresiyle o kernel nesnenin bireysel olarak alt proseslere aktarılabilirliği belirtilmektedir. Ancak buradaki parametre ana şalter görevindedir. Yani ilgili kernek nesnesi yaratılırken "alt prosese aktarılsın" demiş olsak bile bu parametre FALSE geçildikten sonra aktarım yapılmamaktadır. Programcı "özel bir gerekçesi yoksa" bu parametreye TRUE geçebilir. >>>> Altıncı parametresi (dwCreationFlags) yaratılacak prosesin bazı özelliklerini belirlemek için kullanılmaktadır. Bu parametreye bazı sembolik sabitler bit düzeyinde OR işlemine sokularak girilebilir. Kursumuzda buradaki bayraklar üzerinde durmayacağız. Ancak thread'ler konusunda buraya atıfta bulunacağız. Bu nedenle bu parametreyi 0 olarak geçebiliriz. >>>> Yedinci parametresi (lpEnvironment) yaratılacak alt prosin çevre değişken listesini belirtmektedir. Buraya aşağıdaki gibi bir bloğun adresi geçirilmelidir: değişken=değer\0değişken=değer\0....değişken=değer\0\0 Eğer bu parametreye NULL adres geçilirse alt prosesin çevre değişken listesi üzet prosesten alınmaktadır. Genellikle bu parametreye NULL adres geçilmektedir. >>>> Sekizinci parametresi (lpCurrentDirectory) oluşturulacak olan alt prosesin çalışma dizinini belirtmektedir. Yani üst proses isterse alt proses yaratıldığında onun çalışma dizininin ne olması gerektiğini belirleyebilmektedir. Bu parametre de NULL adres geçilebilir. Bu durumda alt prosesin çalışma dizini üst prosesle aynı olur. Genellikle bu parametre NULL adres biçiminde geçilmektedir. >>>> Dokuzuncu parametresi STARTUPINFO isimli bir yapı nesnesinin adresini almaktadır. Programcı bu yapı nesnesinin içini doldurmalı ve adresini fonksiyona geçirmelidir. Bu oldukça fazla elemana sahiptir. Yaratılacak prosesin bazı ikincil özelliklerinin belirlenmesi amacıyla kullanılmaktadır. Bu yapıdaki 0 elemanları default değer anlamına gelir. Dolayısıyla programcı yapı elemanlarını sıfırlayarak default değerler geçebilir. Bu parametreye NULL adres girilememektedir. Burada küçük ayrıntı vardır. STARTUPINFO yapısının ilk elemanı olan cb elemanına yapının sizeof değeri geçirilmelidir. Bunun nedeni ileriye doğru uyumun korunmak istenmesidir. Bu tür yapılara ileride elemanlar eklenebilmektedir. Bu durumda yapının hangi versiyonunun kullanıldığının anlaşılabilmesi için bu elemandan faydalanılmaktadır. Bu yapı nesnesinin elemanlarının sıfırlanması şöyle yapılabilir: STARTUPINFO si = {sizeof(STARTUPINFO)}; >>>> Onuncu parametresi (lpProcessInformation) PROCESS_INFORMATION isimli bir yapı nesnesinin adresini almaktadır. Bu yapı nesnesini programcı doldurmaz. Bu yapı nesnesi fonksiyon tarafından doldurulmalıdır. Bu yapı aşağıdaki gibi bildirilmiştir. typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION; Bir proses yaratıldığında iki kernel nesnesi de yaratılmaktadır. >>>> Windows işletim sisteminde "kernel nesnesi (kernel object)" denildiğinde kendi HANDLE değeri olan, kernel tarafından izlenen, bir grup nesne anlaşılmaktadır. Bu terim UNIX/Linux sistemlerinde kullanılmamaktadır. Örneğin CreateFile fonksiyonu ile açmış olduğumuz dosyalar da "kernel nesnesi" grubundadır. Tüm kernel nesneleri Windows sistemlerinde ortak bazı özelliklere sahiptir. Örneğin hepsi CloseHandle API fonksiyonuyla boşaltılmaktadır. Örneğin tüm kernel nesnelerinin bir güvenlik (yetki) bilgisi vardır. Bu güvenlik bilgisi o kernel nesnesinin yaratılması sırasında CreateXXX fonksiyonlarının LPSECURITY_ATTRIBUTES parametresiyle temsil edilmektedir. Bu iki kerner nesnesinden biri proses için diğeri ise ana thread içindir. Bu kernel nesnelerinin handle değerleri ve id değerleri vardır. İşte bu fonksiyon bu değerleri yerleştirmektedir. Bu parametreye de NULL adres geçilememektedir. 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. Fonksiyonun örnek bir kullanımı şöyle olabilir: char szPath[] = "notepad.exe"; STARTUPINFO si = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION pi; ... if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) ExitSys("CreateProcess"); Aşağıdaki örnekte "prog1" ve "prog2" biçiminde iki program bulunmaktadır. prog1 programı CreateProcess uygulayarak prog2 programını çalıştırmaktadır. Bu çalıştırmada birinci NULL geçildiği için ve ikinci parametrede programın ismi "\" karakteri olmadan belirtildiği için yukarıda açıklamış olduğumuz arama işlemleri belirttiğimiz dizinlerde en sonunda da PATH çevre değişkeniin belirtildiği dizinde uygulanacaktır. Bu örnekte "prog2" programının aranan dizinlerin birinde olması gerekmektedir. * Örnek 1, /* Prog1.c */ #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char szPath[] = "prog2.exe prog1.c"; char env[] = "ALI=10\0Veli=20\0"; STARTUPINFO si = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION pi; if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, env, "c:\\windows", &si, &pi)) ExitSys("CreateProcess"); Sleep(1000); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } /* Prog2.c */ #include #include #include void ExitSys(LPCSTR lpszMsg); int main(int argc, char *argv[]) { LPCH envStr; char cwd[MAX_PATH]; printf("Prog2 command line arguments:\n"); printf("--------------------------\n"); for (int i = 0; i < argc; ++i) puts(argv[i]); printf("--------------------------\n"); printf("Prog2 environment variables:\n"); if ((envStr = GetEnvironmentStrings()) == NULL) { fprintf(stderr, "Cannot get environment strings!..\n"); exit(EXIT_FAILURE); } while (*envStr != '\0') { puts(envStr); envStr += strlen(envStr) + 1; } printf("--------------------------\n"); printf("Prog2 current workşng directory:\n"); if (!GetCurrentDirectory(MAX_PATH, cwd)) ExitSys("GetCurrentDirectory"); printf("%s\n", cwd); printf("--------------------------\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } * Örnek 2, Aşağıdaki örnekte chrome programı komut satırı argümanları verilerek çalıştırılmıştır. Ancak "chrome" programı sizin bilgisayarınızda farklı bir dizine yüklenmiş olabilir. Sizin chrome programının kendi bilgisayarınızdaki yol ifadesini vermeniz gerekir. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char szPath[] = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", csystem.org cumhuriyet.com\""; STARTUPINFO si = { sizeof(STARTUPINFO) }; PROCESS_INFORMATION pa; if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pa)) ExitSys("CreateProcess"); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } * Örnek 3, Yukarıda da belirttiğimiz gibi Windows sistemlerinde HANDLE türüyle belirtilen tüm nesnelere (dosyalara, thread'lere, proseslere vs.) "kernel nesneleri" denilmektedir. Tüm kernel nesneleri prosese özgü "proses handle tablosu" denilen bir tabloda giriş belirtmektedir. Tüm kernel nesnelerinin yok edilmesi (ya da kapatılması) CloseHandle isimli API fonksiyonu ile yapılmaktadır. Bir prosesi yarattıktan sonra PROCESS_INFORMATION yapısı ile elde ettiğiniz proses ve thread handle alanlarını CloseHandle fonksiyonuyla kapatabilirsiniz. Bunları kapatıyor olmanız bu prosesin sonlandırılacağı anlamına geşmemektedir. Zaten program sonlandığında bütün kernel nesneleri kapatılmaktadır. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { char szPath[] = "notepad.exe"; STARTUPINFO si = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION pi; if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) ExitSys("CreateProcess"); printf("Ok\n"); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } Aslında CreateProcess API fonksiyonu bir kernel fonksiyonudur. Yani en alt seviyedki proses yaratan fonksiyondur. Windows'ta kernel API fonksiyonlarının yanı sıra ismine "Shell API fonksiyonları" denilen bir grup API fonksiyonu da bulunmaktadır. Shell API fonksiyonarı daha yüksek seviyeli fonksiyonlardır. Bunlar işlemlerini yaparken kernel API fonksiyonlarını çağırmaktadır. Örneğin bir programı çalıştırmak için en aşağı seviyeli API fonksiyonu CreateProcess fonksiyonudur. Ancak ShellExecute isimli bir shell API fonksiyonu da bulunmaktadır. ShallExecute fonksiyonu yüksek seviyeli bir fonksiyondur ve nihai olarak CreateProcess fonksiyonunu çağırmaktadır. Burada biz ShellExecute fonksiyonu üzerinde de duracağız. >>>> "ShellExecute" : ShellExecute kabuk fonksiyonunun en önemli özelliği "dosya ilişkilendirmesini" dikkate almasıdır. Yani biz bu fonksiyon ile örneğin bir ".docx" dosyasını çalıştırmak istersek ShellExecute bu uzantının ilişkin olduğu programı tespit eder ve CreateProcess fonksiyonu ile o programı ("word.exe" programını) çalıştırır. Bu ".docx" dosyasını da bu programa ("word.exe" programına) komut satıı argümanı yapar. Hangi uzantılı dosyaların hangi programla ilişkilendirildiği "registry" denilen dosyalarda tutulmaktadır. Bu registry dosyalarında manuel işlem yapabilmek için "regedit" isimli bir program da bulundurulmaktadır. Biz masaüstünde bir dosyaya çift tıkladığımız zaman ya da komut satırında bir dosyanın ismini yazıp ENTER tuşuna bastığımız zaman aslında ShellExecute fonksiyonu ile çalıştırma işlemi yapılmaktadır. Dosya ilişkilendirmelerine bakan ShellExecute fonksiyondur. Ancak proses aslında CreateProcess fonksiyonu tarafından yaratılmaktadır. ShellExecute ---> Dosya ilişkilendirmesine bak ----> CreateProcess ShellExecute API fonksiyonunun parametrik yapısı şöyledir: HINSTANCE ShellExecuteA( HWND hwnd, LPCSTR lpOperation, LPCSTR lpFile, LPCSTR lpParameters, LPCSTR lpDirectory, INT nShowCmd ); Fonksiyonun, >>>>> Birinci parametresi (hwnd) UI mesajlarının yazdırılacağı pencerenin üst penceresini belirtmektedir. Biz bu konu üzerinde durmayacağız. Ancak bu parametreye NULL adres geçilebilir. >>>>> İkinci parametresi yapılacak işleme ilişkin bir komut yazısını almaktadır. omut belirten bu yazılar şunlar olabilir: "edit" "explore" "find" "open" "print" "runas" Program çalıştırmak için bu komut yazsının "open" biçiminde girilmesi gerekmektedir. >>>>> Üçüncü parametresi (lpFile) işlem uygulanacak dosyanın yol ifadesini belirtmektedir. Bu yol ifadesi mutlak ya da göreli olabilir. Burada belirtilen dosya çalıştırılabilir bir dosya olabileceği gibi bir doküman dosyası da (doküman dosyası demekle çalıştırılabilir olmayan dosyalar kastedişmektedir) olabilir. Yukarıda belirttiğimiz gibi bu durumda ShellExecute aslında bu doküman dosyasının ilişkin olduğu program dosyasını çalıştırıp bu doküman dosyasını komut satırı argümanı yapmaktadır. Buradaki dosya ismi çalıştırılabilir bir dosya ise ve bu dosya ismi hiç "\" içermiyorsa yine CreateProcess fonksiyonunda açıkladığımız dizinlere ve PATH çevre dğeişkenine bakılmaktadır. >>>>> Dördüncü parametresi (lpParameters) üçüncü parametre çalıştırılabilir bir dosya ise ona aktarılacak komut satırı argümanlarını belirtmektedir. Doküman dosyalarında bu parametre NULL geçilebilir. >>>>> Beşinci parametre (lpDirectory) çalıştırılacak programın çalışma dizinini belirtmektedir. Bu parametre NULL geçilebilir. Bu durumda ShellExecute fonksiyonunu uygulayan prosesin çalışma dizini kullanılır. >>>>> Fonksiyonun son parametresi (nCmdShow) GUI penceresinin hangi boyutta açılacağını belirtmektedir. Bu parametre SW_NORMAL olarak geçilebilir. Bu durumda programa ilişkin GUI penceresi normal boyutla (restore boyutunda) açılmaktadır. ShellExecute fonksiyonu başarı durumunda >= 32 olan bir değere geri dönmektedir. Ancak fonksiyonun geri dönüş değeri 16 bit Windows uyumunu korumak için HINSTANCE olarak alınmıştır. HINSTACE void bir adres biiminde typedef edilmiştir. Dolayısıyla karşılaştırma öncesinde bu değerin adres ile aynı uzunlukta bulunan bir tamsayı türüne dönüştürülmesi gerekir. Bu tür içerisinde INT_PTR olarak typedef edilmiştir. Aşağıda ShellExecute fonksiyonunun kullanımına bir örnek verilmiştir. * Örnek 1, #include #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HINSTANCE hResult; hResult = ShellExecute(NULL, "open", "test.txt", NULL, NULL, SW_NORMAL); if ((INT_PTR)hResult < 32) ExitSys("ShellExecute"); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } * Örnek 2, ShellExecute fonksiyonunun ikinci parametresinde "explore" komutu kullanılırsa "windows explorer" açılmaktadır. Bu durumda üçüncü parametrenin bir dizin belirtmesi gerekmektedir. Aşağıdaki örnekte "windows dizini" windows explorer ile programlama yoluyla açılmıştır. #include #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HINSTANCE hResult; hResult = ShellExecute(NULL, "explore", "c:\\windows", NULL, NULL, SW_NORMAL); if ((INT_PTR)hResult < 32) ExitSys("ShellExecute"); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } Pekiyi Windows'ta kendi uzantılı dosyalarımızı kendi programlarımızla nasıl ilişkilendirebiliriz? Bunun için öncelikle bizim programımızı komut satırı argümanı alacak biçimde organize etmemiz gerekir. Yani pogramımızda en azsından argc > 1 koşulu sağlanmaıdır. Çünkü ShellExecute ilgili dokğman dosyasını bizim programımıza komut satırı argümanı olarak (argv[1]) geçirecektir. Tabii bizim dosya ilişkilendirmesini de yapmamız gerekir. Dosya ilişkilendirmesi yukarıda da belirttiğimiz Windows'un "registry" kayıt dosyalarına yazılmalıdır. Ancak bu işlem manuel olarak da yapılabilmektedir. Manuel ilişkilendirme ve silme Windows versiyonları arasında farklılıklar gösterebilmektedir. Programlama yoluyla dosya ilişkilendirmesi için hazır shell API fonksiyonu yoktur. Windows registry dosyalarına erişim için ismine "Registry API'leri" denilen API fonksiyonları bulundurmaktadır. Registry API fonksiyonlarının hepsi RegXXX biçiminde isimlendirilmiştir. Registry işlemlerini yapabilmek için hangi ayarın registry içerisinde nerede tutulduğunun bilinmesi gerekmektedir. Bu bilgiye Microsoft dokümanlarından erişilebilir. Öte yandan Windows sistemlerinde her prosesin sistem genelinde tek (unique) olan bir "proses id" değeri vardır. Proses id değerleri DWORD türü ile temsil edilen tamsayı değerlerdir. Ancak bir prosesle ilgili işlem yapabilmek için onun handle değerinin elde edilmesi gerekmektedir. Anımsanacağı gibi CreateProcess API fonksiyonunda biz bir proses yarattığımız zaman onun id ve handle değerlerini fonksiyonun PROCESS_INFORMATION parametresi yoluyla elde edebiliyorduk. Windows sistemlerinde proses id değerleri sistem genelinde tek olduğu halde handle değerleri tek değildir. Biz bir proses nesnesini her açtığımızda değişik bir handle değeri elde edebiliriz. Proseslerle ilgili sık gereksinim duyulan bir işlem de proses listesinin elde edilmesidir. Bu işlem Windows sistemlerinde bir grup API fonksiyonuyla yapılmaktadır. Bu fonksiyonlar, >>> "EnumProcesses" : EnumProcesses isimli API fonksiyonu sistemdeki tüm proseslerin (bazı ayrtıntılar vardır) ID değerlerini elde etmek için kullanılmaktadır. EnumProcesses fonksiyonunun prototipi şöyledir: #include BOOL EnumProcesses( DWORD *lpidProcess, DWORD cb, LPDWORD lpcbNeeded ); Fonksiyonun, >>>> Birinci parametresi prosslerin id değerlerinin yerleştirileceği DWORD dizinin adresini almaktadır. >>>> İkinci parametresi, birinci parametresindeki dizinin byte uzunluğunu (eleman uzunluğunu değil) belirtmektedir. >>>> Üçüncü parametre ise diziye yerleştirilen byte sayısının (eleman sayısının değil) yerleştirileceği DOWORD nesnenin adresini almaktadır. Fonksiyon başarı durumunda 0 dışı değerine, başarısızlık durumunda 0 değerine geri dönmektedir. * Örnek 1, #include #include #include #include #define NPROC_IDS 4096 void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwProcessIds[NPROC_IDS]; DWORD dwNeeded; DWORD i; if (!EnumProcesses(dwProcessIds, sizeof(dwProcessIds), &dwNeeded)) ExitSys("EnumProcesses"); for (i = 0; i < dwNeeded / sizeof(DWORD); ++i) printf("%lu\n", (unsigned long)dwProcessIds[i]); printf("%lu process Ids listed...\n", dwNeeded / sizeof(DWORD)); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } >>> "OpenProcess" : Sistemdeki prosesler hakkında bilgi elde edebilmek için id değeri yetmemektedir. Bunun için proseslere ilişkin handle değerlerine gereksinim vardır. İşte prosesin id değerinden handle değerinin elde edilmesi için OpenProcess isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: HANDLE OpenProcess( DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId ); Fonksiyonun, >>>> Birinci parametresi açış işleminin ne amaçla yapılacağnı belirten bayrak değerlerinin bit OR işlemine sokulmasıyla oluşturulmaktadır. Buradaki bayrak değerleri PROCESS_XXXX biçiminde isimlendirilmiş sembolik sabitlerden oluşmaktadır. Proses bilgisini almak için bu parametreye en azından PROCESS_QUERY_INFORMATION|PROCESS_VM_READ bayraklarının girilmesi gerekmektedir. >>>> İkinci parametresi bu handle değerinin alt prosese aktarılabilirliğini belirtmektedir. Bu parametre FALSE olarak geçilebilir. >>>> Üçünü parametre ilgili prosesin id değerini belirtmektedir. Fonksiyon başarı durumunda proses nesnesinin handle değerine, başarısızlık durumunda NULL adrese geri dönmektedir. Windows sistemlerinde bir proses nesnenin açılabilmesi için ilgili prosesin bazı yetkilere sahip olması gerekmektedir. Dolayısıyla biz bu fonksiyonla id değerini bildiğimiz her prosesi açamayız. >>> "EnumProcessModules" : Windows sistemlerinde bağımsız olarak yüklenme bilgilerine sahip olan "exe" ve "dll"" dosyalarına "modül (module)" denilmektedir. Bir uygulama bir "exe dosya" ve birtakım "dll dosyalarından" oluşmaktadır. Uygulamayı oluşturan modüllerin elde edilmesi EnumProcessModules fonksiyonuyla yapılmaktadır. Fonksiyonun prototipi şöyledir: BOOL EnumProcessModules( HANDLE hProcess, HMODULE *lphModule, DWORD cb, LPDWORD lpcbNeeded ); Fonksiyonun, >>>> Birinci parametresi prosesin handle değerini belirtmektedir. >>>> İkinci parametre proses modüllerinin handle değerlerinin yerleştirileceği HMODULE türünden dizinin adresini almaktadır. HMODULE proses modüllerini temsil eden handle değeridir. >>>> Üçüncü parametresi bu dizinin byte uzunluğunu (eleman uzunluğunu değil) belirtmektedir. >>>> Son parametresi diziye yerleştirilen byte sayısınının (eleman sayısının değil) yerleştirileceği DOWORD nesnenin adresini almaktadır. 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. Her zaman prosesin ilk modülü "exe dosyaya ilişkin" modüldür. >>> "GetModuleBaseName" : Nihayet modülün handle değerindne hareketle GetModuleBaseName API fonksiyonu ile modülün ismi elde edilebilmektedir. Fonksiyonun prototipi şöyledir: DWORD GetModuleBaseName( HANDLE hProcess, HMODULE hModule, LPSTR lpBaseName, DWORD nSize ); Fonksiyonun, >>>> İlk iki parametresi sırasıyla ilgili prosesin ve modülün handle değerlerini belirtmektedir. >>>> Üçüncü parametre modül isminin yerleştirileceği dizinin adresini alır. >>>> Son parametresi ismin yerleştirileceği dizinin uzunluğunu belirtmektedir. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısına başarısızlık durumunda 0 değerine geri dönmektedir. Aşağıdaki örnekte prosesimizin yetki bakımdan elde edebileceği proses bilgileri yazdırılmıştır. * Örnek 1, #include #include #include #include #define NPROCESSES 4096 #define NMODULES 4096 void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwProcessIds[NPROCESSES]; DWORD cbNeeded; HANDLE hProcess; HMODULE hModules[NMODULES]; DWORD dwProcessCount; DWORD dwModuleCount; char szModuleName[MAX_PATH]; if (!EnumProcesses(dwProcessIds, sizeof(dwProcessIds), &cbNeeded)) ExitSys("EnumProcesses"); dwProcessCount = cbNeeded / sizeof(DWORD); for (DWORD i = 0; i < dwProcessCount; ++i) { if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessIds[i])) == NULL) continue; if (!EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded)) { CloseHandle(hProcess); continue; } dwModuleCount = cbNeeded / sizeof(DWORD); for (DWORD k = 0; k < dwModuleCount; ++k) { if (!GetModuleBaseName(hProcess, hModules[k], szModuleName, MAX_PATH)) continue; if (k == 0) { printf("%s: ", szModuleName); continue; } if (k != 1) printf(", "); printf("%s", szModuleName); } printf("\n\n"); CloseHandle(hProcess); } printf("%lu process(es) listed...\n", dwProcessCount); getchar(); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } >> UNIX/Linux Sistemlerinde: UNIX/Linux ve macOS sistemlerinde her prosesin sistem genelinde tek olan (unique) bir "proses id" değeri vardır. Bu sistemlerde Windows sistemlerindeki gibi ayrıca proseslerin bir handle değerleri yoktur. Bu sistemlerde bir prosesi teşhis etmek için ve onun üzerinde işlem yapmak için proses id değeri yetmektedir. UNIX/Linux ve macOS sistemlerinde proseslerin id değerleri pid_t türü ile temsil edilmektedir. POSIX standartlarına göre pid_t türü işaretli bir tamsayı türü olmak üzere işletim sistemlerini yazanlar tarafından herhangi bir olarak ve dosyalarında typedef edilmek zorundadır. Bu tür genellikle "int" ya da "long" olarak typedef edilmektedir. UNIX/Linux sistemleri boot edildiğinde boot kodu kendini bir proses yapmakta ve 0 numaralı id'ye sahip olmaktadır. Bu prose "swapper" ya da "pager" da denilmektedir. Bu proses "init" isimli prosesi yaratmaktadır. Bu "init" prosesi her zaman 1 numaralı id'ye sahip olur. Bu sistemler her yaratılan proses için sırasıyla bir proses id değeri üretirler. Üst limite ulaşıldığında yeniden başa dönülmektedir. UNIX/Linux sistemlerinde çekirdek proses id verildiğinde çok hızlı bir biçimde o prorsese ilişkin proses kontrol bloğuna erişebilmektedir. Yani bu sistemlerde proses id'ler proses kontrol bloğuna erişmekte kullanılan bir handle değeri gibidir. Bunun için UNIX/Linux çekirdekleri tipik olarak bir hash tablosu kullanmaktadır. UNIX/Linux sistemlerinde proseslerin yaratılması Windows sistemlerindekinde oldukça farklıdır. UNIX/Linux sistemlerinde yeni bir proses yaratmak için fork isimli POSIX fonksiyonu kullanılmaktadır. fork POSIX fonksiyonu genel olarak doğrudan işletim sisteminin ilgili sistem fonksiyonunu çağırmaktadır. Bu sistemlerde proses yaratmanın başkaca yolu yoktur. (fork benzeri vfork isimli bir fonksiyon olsa da bu fonksiyonun artık bir kullanım gerekliliği kalmamıştır.) fork işlemi UNIX/Linux ve macOS sistemlerindeki en önemli kavramsal süreçlerden biridir. >>> "fork" : fork fonksiyonu bir prosesin özdeş bir kopyasını oluşturmaktadır. fork işlemi sırasında kabaca şunlar yapılmaktadır: -> Proses kontrol bloğunun yeni bir kopyası oluşturulur. Yani yeni bir proses kontrol bloğu yaratılıp üst prosesin proses kontrol bloğu içeriği bazı istisnalarla alt prosesin pross kontrol bloğuna kopyalanır. Dolayısıyla bu işlemden sonra üst ve alt proseslerin gerçek ve etkin kullancı ve grup id'leri, çalışma dizinleri vs. tamamen aynı olmaktadır. -> Üst prosesin bellek alanı da tamamen alt proses için alt prosesin bellek alanına kopyalanmaktadır. Artık üst proses ve alt proses bağımsız iki ayrı proses olmaktadır. Bu iki prosesin geçmişleri aynı gibidir ancak birbirinden bağımsızdırlar. Burada üst proses ile alt prosesin aynı program koduna sahip olacağına da dikkat ediniz. -> Yeni bir çizelgele elemanı oluşturulup alt proses de zaman paylaşımlı biçimde bağımısz çalışmaya devam eder. -> Çatallanma fork fonksiyonun içinde olmaktadır. Böylece yeni proses (yani alt proses) hayatına fork fonksiyonun içinden başlamaktadır. Böylece hem üst proses hem de alt proses fork fonksiyonundan çıkacaktır. fork fonksiyonun prototipi şöyledir: #include pid_t fork(void); Hem üst proses hem de alt proses fork fonksiyonundan çıkamktadır. Üst proseste fork fonksiyonu alt prosesin proses id değeri ile, alt proseste ise 0 değeriyle geri dönmektedir. (Alt prosesin fork fonksiyonundan 0 ile geri dönmesi onun proses id'sinin 0 olduğu anlamına gelmemektedir.) fork başarısız olabilir. Bu durumda fork -1 değerine geri döner. fork fonksiyonunun kullaımına ilişkin tipik kalıp şöyledir: pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ ... } else { /* chile process */ ... } Üst prosesin ve alt prosesin her ikisin de fork fonksiyonundan çıktığını söylemiştik. Ancak hangi prosesin fork fonksiyonundan önce çıkacağının hiçbir garantisi yoktur. Aşağıda tipik bir fork uygulama kalıbı görülmektedir. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ printf("parent process...\n"); } else { printf("child process...\n"); /* child process */ } printf("common code...\n"); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } fork işleminden sonra artık üst proses ile alt proses arasında hiçbir bağlantı kalmamıştır. Dolayısıyla birinin yaptığı bir şey diğerini etkilemez. * Örnek 1, Aşağıdaki kodda üst proses g_x global değişkenine 10 değerini atamıştır ancak bu g_x üst prosesin kendi kopyasındaki g_x'tir. Dolayısıyla alt proses bu g_x nesnesinin değerini değişmiş görmez. #include #include #include void exit_sys(const char *msg); int g_x; int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* parent process */ g_x = 10; printf("parent: %d\n", g_x); } else { sleep(1); printf("child: %d\n", g_x); } printf("common code...\n"); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2.0, Aşağıdaki kodda ekrana (stdout dosyasına) kaç tane "common code..." yazısı çıkacaktır (kontroller yapılmamıştır)? fork(); fork(); fork(); printf("common code...\n"); Birinci fork'a bir proses girer ondan 2 proses çıkar. İkinci fork'a 2 proses girer ondan 4 proses çıkar. Üçüncü fork'a 4 proses girer ondan 8 proses çıkar. Yani bu yazı toplamda 8 kere ekrana basılacaktır. * Örnek 2.1, #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if ((pid = fork()) == -1) exit_sys("fork"); if ((pid = fork()) == -1) exit_sys("fork"); printf("common code...\n"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } fork fonksiyonu başka bir programın çalışmasına yol açmamaktadır. Aynı programın özdeş bir kopyasının çalışmasına yol açmaktadır. Oysa Windows'taki CreateProcess başka bir programı çalıştırarak proses oluşturmaktadır. O halde UNIX/Linux sistemlerinde fork ne işe yaramaktadır ve başka bir program nasıl çalıştırılmaktadır? Sonraki paragraflarda açıklayacağımız gibi aslında genellikle fork fonksiyonu tek başına değil exec fonksiyonlarıyla birlikte kullanılmaktadır. Eskiden thread'ler yoktu. Bir işi birden fazla akışa yaptırabilmek için yeni prosesler yaratmak gerekiyordu. Böylece üst proses ile alt proses aynı işin farklı kısımlarını birlikte yapabiliyordu. Örneğin: if ((pid = fork()) == -1) exit_sys("fork"); if (pid != 0) { /* üst proses */ /* üst proses işin bir parçasını yapıyor */ } else { /* üst proses */ /* üst proses işin diğer bir parçasını yapıyor */ } Ancak artık bu tür işlemler için thread'ler kullanılmaktadır. Thread'ler proseslere göre çok daha az sistem kaynağı harcamaktadır. Dolayısıyla yaratılmaları ve yok edilmeleri de proseslere kıyasla daha hızlıdır. Ayrıca bir işi birden fazla akışa yaptırırken onların koordine edilmesi de gerekmektedir. Bu koordinasyon için fork modelinde proseslerarası haberleşme yöntemleri kullanılmaktadır. Bunun da ek maliyetleri vardır. Halbuki thread'ler global değişkenler yoluyla haberleşebilmektedir. >>> "exec" fonksiyonları : Başka program dosyasını çalıştırabilmek için UNIX/Linux sistemlerinde "exec fonksiyonları" diye isimlendirilen POSIX fonksiyonları kullanılmaktadır. exec fonksiyonları bir aile belirtmektedir. Aslında bu fonksiyonların hepsi aynı işlemleri biraz değişik parametrelerle yapmaktadır. exec ailesinde 7 farklı fonksiyon vardır: execl execlp execv execvp execle execvpe execve (Linux'ta yalnızca bu bir sistem fonksiyonu olarak gerçekleştirilmiştir) Aslında yalnızca execve bir sistem fonksiyonu olarak gerçekleştirilmiştir. Diğer exec fonksiyonları kütüphane fonksiyonlarıdır ve aslında bu execve fonksiyonunu çağırarak işlemlerini yaparlar. exec fonksiyonları prosesin bellek alanını boşaltıp başka bir program dosyasını o bellek alanına yükleyip onu çalıştırmaktadır. exec fonksiyonları proses yaratmazlar. Mevcut prosesin başka bir kodla çalışmaya devam etmesini sağlarlar. Yani exec işlemi yapıldığında artık exec yapan kod yok olacak exec işleminde belirtilen program dosyası çalıştırılacaktır. Ancak exec işlemi proses kontrol bloğunu değiştirmemektedir. Dolayısıyla exec işleminden sonra prosesin id'si, etkin kullanıcı id'si, etkin grup id'si, çalışma dizini vs. aynı kalır. Şimdi de sırasıyla bu fonksiyonları inceleyelim: >>>> "execl" : En çok kullanılan exec fonksiyonlarından biri "execl" isimli fonksiyondur. Fonksiyonun prototipi şöyledir: #include int execl(const char *path, const char *arg0, ... /*, (char *)0 */); Fonksiyonun, -> Birinci parametresi çalıştırılacak olan program dosyasının yol ifadesini belirtmektedir. -> Diğer parametreler o programa geçirilecek olan komut satırı argümanlarıdır. -> Fonksiyonun "..." parametresi aldığına dikkat ediniz. C'de bu parametre fonksiyonun istenildiği kadar çok argümanla çağrılabileceğini belirtmektedir. Tabii buradaki "..." için girilen argümanların hepsi komut satırı argüman yazılarının adresleri olmalıdır. Ancak execl fonksiyonunun argüman listesinin bittiğini anlayabilmesi için buradaki argüman listesinin sonu NULL adresle bitirilmelidir. Ancak burada NULL adesi düz sıfır olarak ya da NULL sembolik sabitiyleoluşturmayınız. Çünkü C'de "..." için kullanılan argümanlarda düz sıfır int olarak ele alınacaktır. Benzer biçimde NULL sembolik sabitide düz sıfır olarak define edilmiş olabileceği için benzer soruna yol açabilmektedir. Bu nedenle programcının argüman listesinin sonuna açıkça NULL adresi (char *)0 biçiminde yerleştirmesi uygun olmaktadır. Programın ilk komut satırı argümanının program simi olması zorunlu değildir. Ancak genel beklenti bu yöndedir. execl fonksiyonu başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyon başarı durumunda geri dönmez. Çünkü zaten başarı durumunda başka bir program kodu çalışmaya başlayacaktır. Örneğin: if (execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1) exit_sys("execl"); Burada "/bin/ls" programı (yani ls komutu uygulandığında çalıştırılan program) çalıştırılmak isteniştir. Programın ilk komut satırı argümanı kendisi olmalıdır. Komut satırı argüman listesinin (char *)0 ifadesi ile sonlandırıldığına dikkat ediniz. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { printf("main begins...\n"); if (execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1) exit_sys("execl"); /* unreachable code */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte "sample" programı execl fonksiyonu ile aşağıdaki gibi mample programını çalıştırmaktadır: if (execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1) exit_sys("execl"); "mample" programı komut satırı argümanlarını yazdıran bir programdır. mample programının çıktısı şöyledir: main begins... argv[0] ==> mample argv[1] ==> ali argv[2] ==> veli argv[3] ==> selami İlgili programın kodları aşağıdaki gibidir: /* sample.c */ #include #include #include void exit_sys(const char *msg); int main(void) { printf("main begins...\n"); if (execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1) exit_sys("execl"); /* unreachable code */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include int main(int argc, char *argv[]) { for (int i = 0; i < argc; ++i) printf("argv[%d] ==> %s\n", i, argv[i]); return 0; } >>>> "execv" : execv (v vector'den geliyor) fonksiyonu execl fonksiyonun komut satırı argümanlarını tek tek değil de bir gösterici dizisi biçiminde tek bir parametreyle alan biçimidir. Yani çalıştırılacak programın komut satırı argümanları char türünden bir gösterici dizisine yerleşltirilip bu dizinin adresi execv fonksiyonuna verilmektedir. Fonksiyonun prototipi şöyledir: #include int execv(const char *path, char * const argv[]); Fonksiyonun birinci parametresi çalıştırılacak program dosyasının yol ifadesini almaktadır. İkinci parametre komut satırı argümanlarının bulunduğu dizinin başlangıç adresini alır. Bu gösterici dizisinin son elemanı NULL adres olmalıdır. Yine fonksiyon başarısızlık durumunda -1 değerine geri döner. Başrı durumunda zaten çağrıldığı yere geri dönememektedir. Örneğin: const char *args[] = {"ls", "-l", NULL}; ... if (execv("/bin/ls", args) == -1) exit_sys("execv"); Aşağıda execv fonksiyonunun kullanımına bir örnek verilmiştir. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *args[] = {"ls", "-l", NULL}; printf("main begins...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execv("/bin/ls", args) == -1) exit_sys("execl"); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bazen execv fonksiyonu execl fonksiyonundan daha uygun olabilmektedir. Örneğin komut satırı argümanlarıyla aldığımız bir propgramı çalıştırmak istesek execl işimizi çok zorlaştıracaktır. Burada execv çok daha uygundur. "sample" programının komut satırı argümanlarıyla aldığı programı çalıştırdığını düşünelim: ./sample /bin/ls -l -i Burada çalıştırılacak program "/bin/ls" programıdır. Diğerleri onun komut satırı argümanlarıdır. İlk komut satırı argümanının program ismiyle aynı olması gerektiğini de anımsayınız. Burada "sample" programının argv[1] parametresi "/bin/ls" yazısını göstermektedir. &argv[1] ise buradan başlayan bir gösterici dizisi gibidir. argv listesinin sonunda zaten NULL adres bulunmaktadır. O halde exec işlemi şöyle basit bir biçimde yapılabilir: if (execv(argv[1], &argv[1]) == -1) exit_sys("execv"); Aşağıda programın nasıl yazıldığı görülmektedir. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; if (argc < 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execv(argv[1], &argv[1]) == -1) exit_sys("execl"); sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>> exec fonksiyonlarının p'li versiyonları (yani sonunda p soneki olan versiyonları) eğer yol ifadesinde hiç '/' karakteri yoksa ilgili dosyayı PATH çevre değişkeni ile belirtilen dizinlerde tek tek aramaktadır. Eğer dosyanın yol ifadesinde en az bir '/' karakteri varsa bu durumda dosya belirtilen yol ifadesinde aranır. Yani bu durumda exec fonksiyonlarının p'li versiyonlarıyla p'siz versiyonları arasında bir fark olmaz. execlp ve execvp fonksiyonlarının prototipleri exel ve execv ile aynıdır. Yalnızca kavramsal farkılığı vurgulamak için birinci parametre "path" yerine "file" olarak isimlendirilmiştir: #include int execlp(const char *file, const char *arg0, ... /*, (char *)0 */); int execvp(const char *file, char *const argv[]); PATH çevre değişkeni UNIX/Linux sistemlerinde ':' karakterinden ayrıştırılmaktadır. (Windows sistemlerinde ';' karakteri ile ayrıştırıldığını anımsayınız.) Linux sistemlerindeki örnek bir PATH değişkeninin değerine bakınız: /home/kaan/anaconda3/bin:/home/kaan/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin "/bin", "/usr/bin", "/usr/local/bin" gibi prgram dosyalarının bulunduğu dizinlerin PATH çevre değişkeninde bulunduğuna dikkat ediniz. Artık UNIX/Linux sistemlerinde bir programı çalıştırırken neden "./sample" biçiminde program isminin önüne "./" getirdiğimizi anlamış olmalısınız. Kabul programları exec fonksiyonlarının p'li versiyonlarını kullanmaktadır. execv fonksiyonlarının p'li versiyonları eğer çalıştırılacak program dosyasına ilişkin yoli fadesinde hiç '/' karakteri yoksa onu PATH çevre değişkeni ile belirtilen dizinlerde sırasıyla aramaktadır. Bu durumda prosesin çalışma dizinin de hiç arama yapmamaktadır. (Halbuki Windows'ta benzer durumda dosya önce çalışma dizininde arandıktan sonra PATH çevre değişkenine bakılmaktadır.) Örneğin biz bulunduğumuz dizindeki "sample" programını şöyle çalıştırmaktayız: ./sample Bu programı biz aşağıdaki gibi çalıştırsaydık prgram dosyası bulunamaz: sample Çünkü burada exec fonksiyonlarının p'li versiyonları dosya yol ifadesinde hiç '/' karakteri geçmediği için onu yalnızca PATH çevre değişkeni ile belirtilen dizinlerde arayacaktır. Halbuki biz programı "./sample" biçiminde çalıştırmak istediğimizde artık işin içine bir '/' karakteri sokulduğu için exec fonksiyonlarının p'li versyionları PATH çevre değişkenine bakmayacaktır. '.' karakterinin "prosesin çalışma dizini" anlamına geldiğine dikkat ediniz. Biz çalıştırmayı örneğin "/sample" biçiminde yapamazdık. Çünkü bu durumda "sample" programı kök dizinde aranırdı. Pekiyi exec fonksiyonlarının p'li biçimleri neden Windows gibi önce prosesin çalışma dizinine bakmamaktadır? Ya da kabuk programları neden prosesin çalışma dizinine de bakmamaktadır? İşte çok eskiden kabuk programları Windows'taki gibi kabuğun çalışma dizinine de bakıyordu. Ancak bu tasarımın bazı kötüye kullanımlarıyla karşılaşıldı. (Örneğin birisi birisinin dizinine bir komutla aynı isimli bir program dosyası yerleştirebilir ve kişi komutu çalıştırdığını sanırken başkasının programını çalıştırabilir. Ya da kişi bir komutun ismini yanlış yazarak yanlışlıkla çalışma dizinindeki başka bir programı da çalıştırabilir.) Bugün artık bu sistemlerde kabuk programlarının exec fonksiyonlarının p'li versiyonlarıyla exec yapması oturmuş bir kural biçimindedir. Aşağıda komut satırı argümanlarıyla aldığı programı çalıtıran programı bu kez execvp fonksiyonunu kullanarak yazıyoruz. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { pid_t pid; if (argc < 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { execvp(argv[1], &argv[1]); exit_sys("execvp"); } sleep(1); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>> exec fonksiyonlarının bir de e'li biçimleri vardır. Bunlar exec işlemi sırasında çalıştırılacak program için yeni bir çevre değişken takımı oluşturmakltadır. execle ve execve fonksiyonlarının prototipleri şöyledir: #include int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/); int execve(const char *path, char *const argv[], char *const envp[]); Bu fonksiyonlar p'li olmadığı için PATH çevre deişkenine hiç bakmamaktadır. Her iki fonksiyonun da ilk parametresi çalıştırılacak programın yol ifadesini belirtmektedir. execle fonksiyonu önce komut satırı argümanlarını bir liste olarak alır. Bu argümanların sonunda NULL adres bulunmalıdır. Bu NULL adresten sonra çevre değişkenleri "anahtar=değer" yazıları biçiminde sonu NULL adresle biten bir gösterici dizisi biçiminde girilmelidir. execve fonksiyonu ise hem komut satırı argümanlarını ehm de çevre değişkenlerini gösterici dizisi biçiminde almaktadır. Her iki fonksiyon da yine başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. UNIX/Linux sistemlerinde çevre değişkenlerinin genel olarak prsoesin bellek alanı içerisinde tutulduğunu anımsayınız. Çevre değişkenleri aslında fork işlemi sırasında üst prosesin tüm bellek alanı alt prosese kopyalandığından dolayı alt prosese geçirilmektedir. Ancak exec fonksiyonları prosesin bellek alanını yok edip onun yerine başka bir programı bu bellek alanına yüklediklerinden dolayı bu çevre değişkenlerinin kaybolması beklenir. İşte exec fonksiyonlarının e'siz biçimleri fork sonrasında bu eçvre değişkenlerini saklayıp exec işlemi ile çalıştırılan program için ayrılan bellek alanına aktarmaktadır. Yani exec fonksiyonlarının e'siz versiyonlarında biz programı çalıştırdığımızda çevre değişkenleri üst prosesle aynı olmaktadır. Ancak exec fonksiyonlarının e'li biçimleri programcının belirlediği çevre değişkenlerini exec işlemi sırasında yeni programın bellek alanına aktarmaktadır. execle fonksiyonu tipik olarak şöyle kullanılmaktadır: pid_t pid; char *env[] = {"city=eskişehir", "plate=26", NULL}; ... if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execle("mample", "mample", "ali", "veli", "selami", (char *)NULL, env) == -1) exit_sys("execl"); Aşağıda bu fonksiyonların kullanımına ilişkin bir program verilmiştir. * Örnek 1, /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *envs[] = {"city=eskişehir", "plate=26", NULL}; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execle("mample", "mample", "ali", "veli", "selami", (char *)NULL, envs) == -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 extern char **environ; int main(int argc, char *argv[]) { printf("Command line arguments:\n"); for (int i = 0; i < argc; ++i) printf("%s\n", argv[i]); printf("\nEnvironment Variables:\n"); for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } * Örnek 2, Aşağıda execve fonksiyonunun kullanımına bir örnek verilmiştir. execve fonksiyonunda hem komut satırı argümanlarının hem de çevre değişkenlerinin gösterici dizisi biçiminde verildiğine dikkat ediniz. /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *args[] = {"mample", "ali", "veli", "selami", NULL}; char *envs[] = {"city=eskişehir", "plate=26", NULL}; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execve("mample", args, envs) == -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 extern char **environ; int main(int argc, char *argv[]) { printf("Command line arguments:\n"); for (int i = 0; i < argc; ++i) printf("%s\n", argv[i]); printf("\nEnvironment Variables:\n"); for (int i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } Aslında pek çok UNIX türevi sistemde yalnızca execve fonksiyonu bir sistem fonksiyonu olarak yazılmıştır. Başka bir deyişle yalnızca execve fonksiyonu çekirdeğin içerisindedir. Diğer exec fonksiyonlarının hepsi bir çeşit "sarma (wrapper) fonksiyon" gibidir ve normal user mod kütüphane içerisinde bulunmaktadır. Örneğin biz execl fonksiyonunu çağırdığımızda bu komut satırı argümanları bir gösterici dizisine yerleştirilip environ değişkenini kullanarak execve fonksiyonunu çağırmaktadır. Örneğin execlp fonksiyonu programı PATH çevre değişkeni ile belirtilen dizinlerde tek tek execve uygulayarak aramaktadır. Yani aslında PATH çevre değişkenine çekirdek kodları bakmamaktadır. Bu işlem kütüphane fonksiyonu tarafından user modda yapılmaktadır. exec fonksiyonlarının execve fonksiyonu çağrılarak nasıl gerçekleştirildiğine yönelik "Advanced Programming in the UNIX Environment" kitabının 254'üncü sayfasındaki şekli inceleyebilirsiniz. Aşağıda bir execve kullanımı görülmektedir. * Örnek 1, /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; char *args[] = {"./mample", "mample", "ali", "veli", "selami", NULL}; char *env[] = {"city=eskişehir", "plaka=26", NULL}; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execve("./mample", args, env) == -1) exit_sys("execve"); if (wait(NULL) == -1) exit_sys("wait"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include extern char **environ; int main(int argc, char *argv[]) { int i; printf("Command line arguments:\n"); for (i = 0; i < argc; ++i) printf("%s\n", argv[i]); printf("Environment Variables:\n"); for (i = 0; environ[i] != NULL; ++i) puts(environ[i]); return 0; } >>>> exec fonksiyonlarının sonuncusu fexecve isimli fonksiyondur. Bu fonksiyonun execve fonksiyonundan tek farkı çalıştırılacak dosyanın yol ifadesini değil onun dosya betimleyicisini almasıdır. Bu fonksiyon çok seyrek kullanılmaktadır. Prototipi şöyledir: #include int fexecve(int fd, char *const argv[], char *const envp[]); exec fonksiyonları ile aslında çalıştırılabilir olmayan dosyalar da (örneğin text dosyalar) çalıştırılmak istenebilir. Bu durumda exec fonksiyonları ilgili dosyayı açıp onun ilk iki karakterine bakmaktadır. Eğer dosyada ilk iki karakter #! biçimindeyse bu karakterlere "shebang" denilmektedir. shebang karakterlerini gerçek çalıştırılabilir bir dosyanın yol ifadesi izlemelidir. (Shebang'ten sonra boşluk karakterleri bulunabilir.) İşte exec fonksiyonları aslında burada belirtilen dosyayı çalıştırılar. Çalıştırdıkları dosyaya da exec yapılan dosyayı komut satır argümanı olarak geçirirler. Yukarıda da belirttiğimiz gibi UNIX/Linux sistemlerinde yalnızca execve fonksiyonu bir sistem fonksiyonu olarak gerçekleştirilmiştir. Bu shebang kontrolü kernel tarafından bu execve içerisinde yapılmaktadır. execve burada belirtilen çalıştılabilir dosyayı bir yol ifadesi olarak kabul eder. Genel olarak buradaki dosyanın mutlak yol ifadesi belirtmesi istenmektedir. Ancak bugünkü sistemler göreli yol ifadelerini de kabul etmektedir. Buradaki dosya execve tarafından PATH çevre değişkenine bakılmadan doğrudan ele alınmaktadır. Burada belirtilen dosyadan sonra yazılan komut satırı argümanları buradaki çalıştırılabilen dosyaya tek bir komut satırı argümanı biçiminde aktarılmaktadır. exec fonksiyonlarının kendisinde belirtilen argümanlar ise son komut satırı argümanları olarak kullılmaktadır. Aşağıdaki örnekte bir shebang mekanizması uygulanmıştır. "sample.c" programı komut satırı argümanlarıyla alınan dosyayı execv ile çalıştırmaktadır. Biz bu programla "test.txt" dosyasını aşağıdaki gibi çalıştıracağız: #! /home/kaan/Study/SysProg/mample Burada execp fonksiyonunda programın komut satırı argümanları "test.txt", "xxx" ve "yyy" durumundadır. "test.txt" dosyasının başındaki shebang kısmı da şöyle olsun: #! /home/kaan/Study/SysProg/mample mample programı da komut satırı argümanlarını ekrana yazan program olsun. Bu durumda ekrana şunlar çıkacaktır: argv[0]: /home/csd/Study/SysProg-2020/mample argv[1]: ali veli selami argv[2]: test.txt argv[3]: xxx argv[4]: yyy Shebang içeren dosyanın ne olursa olsun x özelliklerine sahip olması gerekir. Çünkü exec fonksiyonları bunu kontrol etmektedir. Bir dosyaya x hakları vermenin en pratik yolu şöyledir: chmod +x test.txt Tabii aslında text dosyalar eğer "x" hakkına sahipse doğrudan kabuk üzerinden de çalıştırılabilmektedir. Zaten bu durumda kabuk programları bunları exevp ile çalıştırmaktadır. Örneğin "text.txt" dosyası aşağıdaki gibi olsun: #! /home/kaan/Study/SysProg/mample Şimdi biz bu dosyayı komut satırından çalıştırmak istediğimizde aslında "mample" programı çelıştırılacaktır. "mample" programına da "ali veli selami" ve "test.txt" parametre olarak geçirilecektir. Örneğin: $ ./test.txt argv[0]: /home/kaan/Study/SysProg/mample argv[1]: ./test.txt $ ./test.txt xxx yyy argv[0]: /home/kaan/Study/SysProg/mample argv[1]: ./test.txt argv[2]: xxx argv[3]: yyy Shebang kullanımının en önemli faydası script dosyalarının sanki çalıştırılabilir bir dosya gibi çalıştırılmasını sağlamaktır. Örneğin aşağıdaki gibi "sample.py" isminde bir python programı bulunyor olsun: #! /usr/bin/python3 for i in range(10) : print(' {}'.format(i)) Biz bu "sample.py" dosyasına "x" hakkı vererek onu çalıştırdığımızda aslında "/usr/bin/python3" programı çalıştırılacaktır. Bu programa da "sample.py" komut satırı argümanı olarak verildiği için sanki çalıştırma aşağıdaki gibi yapılıyormuş etkisi oluşacaktır: python3 sample.py Windows sistemlerinde shebang gibi bir kullanım yoktur. Benzer işlemler dosya ilişkilendirmesi yoluyla yapılmaktadır. ShellExecute fonksiyonun dosya ilişkilendirmesine baktığını biliyorsunuz. #!/usr/bin/python3 for i in range(10) : print(' {}'.format(i)) Aslında kabuk komutları kendi içlerinde yorumlayıcı (interpreter) da içermektedir. Yani biz kabuk komutlarını bir dosyada bulundurup onları sanki bir program gibi çalıştırabiliriz. Kabukların ayrı bir script dili vardır. Shell scirpt dilleri basit olsa da ayrıca öğrenilmesi gerekir. Örneğin aşağıdaki gibi "sample.sh" isminde bir bash script dosyası oluşturup ona "x" hakkı vererek komut satırından çalıştırabiliriz: #!/bin/bash for n in {1..10}; do echo $n done Çalıştırma şöyle yapılabilir: ./sample.sh Aslında bu script bash tarafından aşağıdaki gibi çalıştırılmaktadır: /bin/bash sample.sh Bir çalıştırılabilir text dosyanın başında shebang bölümü yoksa bu durumda bu dosya kabuk programı tarafından bir kabuk scipt dosyası gibi çalıştırılmaktadır. Örneğin: for n in {1..10}; do echo $n done Buradaki dosyanın başında "shebang" karakterşeri yoktur. Bu dosya çalıştırılmak istendiğinde doğrudan sanki bir shell script gibi çalıştırılacaktır. Burada aslında kabuk önce dosyayı "execvp" ile çalıştırmak ister. Dosya çalıştırılamazsa (örneğin dosyanın başında shebang) yoksa bu kez onu script dosyası gibi kendisi çalıştırır. Ancak bu tarzda çalıştırma daha maliyetlidir. Bu nedenle shell script dosyalarının başında "gerekmese bile" shebang bulundurulmalıdır. Tek başına fork bir prosesin özdeş kopyasını oluştup çalıştırmaktadır. Tek başına exec ise prosesin başka bir kodla çalışmasını sağlamaktadır. O halde hem üst prosesin kodu çalışsın hem de başka bir programın kodu çalışsın isteniyorsa fork ve exec birlikte kullanılmalıaıdır. Yani önce bir kez fork yapılır. Alt proseste exec uygulanır. fork sırasında üst prosesin kopyasından çıkartılacaktır. Bu sırada üst prosesin bellek alanın da kopyasından çıkarılır. Alt proseste exec yapıldığında o kod bellekten yok edilip exec yapılan programın kodu belleğe yüklenip çalıştırılır. Tipik fork/exec kalıbı şöyledir: if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { if (execl(....) == -1) exit_sys("execl"); /* dikkat burada sonlanan alt proses */ /* unreachable code */ } /* buraya yalnızca üst proses akışı gelecektir */ && operatörünün kısa devre özelliği kullanılarak bu işlem daha kompakt biçimde de ifade edilebilirdi: if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execl(....) == -1) { exit_sys("execl"); /* dikkat burada sonlanan alt proses */ /* unreachable code */ } /* buraya yalnızca üst proses akışı gelecektir */ Bazı programcılar ise aşağıdaki kalıbı tercih etmektedir: if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { execl(....); exit_sys("execl"); /* exec başarısızsa akış buraya gelecektir */ } /* buraya yalnızca üst proses akışı gelecektir */ Microsoft sistemlerindeki CreateProcess API fonksiyonunun fork/exec ikilisi gibi işlem gördüğüne dikkat ediniz. * Örnek 1, #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; printf("main begins...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execl("/bin/ls", "/bin/ls", "-l", (char *)0) == -1) exit_sys("execl"); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, fork/exec modelinde etkin olmayan şöyle bir durum kişilerin aklına gelmektedir: fork işleminden sonra alt proseste exec uyguladığımızda üst prosesin bellek alanının gereksiz bir kopyası oluşturulmuş olmuyor mu? İlk bakışta burada gerçekten bir performans sorunu olduğu düşünülmektedir. Eski sistemlerde gerçekten bu bir sorundu. Bu nedenle fork fonksiyonun vfork isminde bir kardeşi bulundurulmuştu. vfork fonksiyonunun fork fonksiyonundan tek farkı üst prosesin bellek alanının kopyasından çıkarmamasıdır. Tabii vfork fonksiyonundan sonra artık kesinlikle exec işlemi yapılmalıdır. Eğer vfork fonksiyonundan sonra exec işlemi yapılmazsa "tanımsız davranış" oluşmaktadır. Bugün vfork fonksiyonu POSIX standratlarında hala muhafaza ediliyor olsa da faydalı bir kullanımı kalmamış gibidir. Çünkü uzun süredir UNIX/Linux sistemlerinin çalıştığı makinelerdeki işlemcilerin "sayfalama (paging) ve sanal bellek (virtual memory) mekanizması" vardır. Bu mekanizma sayesinde aslında üst prosesin bellek alanın kopyası zaten gerektiğinde çıkartılmaktadır. Bu mekanizmaya "copy on write" denilmektedir. Dolayısıyla bugün zaten bir fork yapsak bile alt proseste henüz bir işlem yapmadıktan sonra ciddi bir kopyalama yapılmamaktadır. Bu nednele artık fork/exec işleminde yukarıdaki gibi bir performans problemi oluşmayacaktır. Aşağıdaki örnekte sample programı mample programını çalıştırmıştır. /* sample.c */ #include #include #include void exit_sys(const char *msg); int main(void) { pid_t pid; printf("main begins...\n"); if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execl("mample", "mample", "ali", "veli", "selami", (char *)0) == -1) exit_sys("execl"); for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include int main(int argc, char *argv[]) { printf("mample running...\n"); for (int i = 0; i < argc; ++i) printf("argv[%d] ==> %s\n", i, argv[i]); return 0; } Aslında UNIX/Linux sistemlerinde shell programlarındaki komutların çok büyük çoğunluğu çalıştırılabilir dosyalardır. Yani örneğin "ls" komutu aslında "/bin/ls" programıdır, "cat" komutu aslında "/bin/cat" programıdır. Çok az sayıda "internal" komut vardır. Bunlardan biri "cd" komutudur. (Zaten "cd" komutu bir program olarak yazılamazdı. Eğer "cd" bir program olsaydı üst prosesin çalışma dizinini değil (yani shell'in değil) kendi prosesinin çalışma dizinini değiştirirdi.) Hiçbir proses üst prosesinin çalışma dizinini değiştirememektedir. * Örnek 1, Aşağıda daha önce yazmış olduğumuz basit shell programının komutları external biçimde çalıştıran versiyonunu veriyoruz. Bu programda henüz görmediğimiz wait isimli bir fonksiyonu kullandık. Bu fonksiyon alt proses bitene kadar üst prosesi blokede bekletmektedir. /* myshell.c */ #include #include #include #include #include #include #define MAX_CMD_LINE 4096 #define MAX_CMD_PARAMS 128 #define BUFFER_SIZE 8192 #define MAX_PATH_SIZE 4096 void parse_cmdline(void); void cd_proc(void); void exit_sys(const char *msg); typedef struct tagCMD { char *cmd_name; void (*cmd_proc)(void); } CMD; char g_cmdline[MAX_CMD_LINE]; char *g_params[MAX_CMD_PARAMS]; int g_nparams; CMD g_cmds[] = { {"cd", cd_proc}, {NULL, NULL} }; char g_cwd[MAX_PATH_SIZE]; int main(void) { char *str; int i; pid_t pid; if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL) exit_sys("getcwd"); for (;;) { printf("CSD:%s>", g_cwd); if (fgets(g_cmdline, MAX_CMD_LINE, stdin) == NULL) continue; if ((str = strchr(g_cmdline, '\n')) != NULL) *str = '\0'; parse_cmdline(); if (g_nparams == 0) continue; if (!strcmp(g_params[0], "exit")) break; for (i = 0; g_cmds[i].cmd_name != NULL; ++i) if (!strcmp(g_cmds[i].cmd_name, g_params[0])) { g_cmds[i].cmd_proc(); break; } if (g_cmds[i].cmd_name == NULL) { if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execvp(g_params[0], &g_params[0]) == -1) { printf("invalid command: %s\n", g_params[0]); exit(EXIT_FAILURE); } if (wait(NULL) == -1) exit_sys("wait"); } } return 0; } void parse_cmdline(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) { if (g_nparams == 1) { printf("argument missing!..\n"); return; } if (g_nparams > 2) { printf("too many arguments!..\n"); return; } if (chdir(g_params[1]) == -1) { printf("%s: %s!..\n", g_params[1], strerror(errno)); return; } if (getcwd(g_cwd, MAX_PATH_SIZE) == NULL) exit_sys("getcwd"); } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } > C'de Komut Satırı Argümanları ile "Predefined Symbolic Constans" kullanımı: C derleyicilerinin çok büyük kısmında komut satırı argümanlarıyla "önceedn tanımlanmış (predefined) sembolik sabitler" oluşturulabilmektedir. Bunun için Microsoft derleyicilerinde /D seçeneği, gcc ve clang derleyicilerinde -D seçeneği kullanılmaktadır. Bu seçeneklerin argümanı sembolik sabitin ismini almaktadır. Örneğin: cl -D TestXXX sample.c Burada sanki "sample.c" dosyasının tepesinde aşağıdaki gibi bir satır varmış gibi işlem uygulanmaktadır: #define TestXXX Tabii sembolik sabite değer de verilebilir. Örneğin: cl /D TestXXX=123 sample.c Burada da sanki kaynak kodun tepesinde aşağıdaki gibi bir #define komutu varmış gibi işlem uygulanmaktadır: #define TestXXX 123 Aynı durum gcc ve clang derleyicilerinde -D seçeneği ile yapılmaktadır. Örneğin: gcc /D TestXXX=123 -o sample sample.c Tabii IDE'lerle bu seçenkler görsel biçimde de oluşturulabilmektedir. Microsoft Visual Studio IDE'sinde bu işlem için proje seçeneklerinden "Properties/C-C++ Preprocessor/Preprocessor Definitions" edit alanı yoluyla da yapılabilmektedir. > C'de wchar_t türü: C standartlarında wchar_t isimli bir tür belirtilmiştir. Ancak wchar_t C'de bir anahtar sözcük değil typedef ismidir. Bu tür aşağıdaki başlık dosyalarında typedef edilmiştir: C standartları bu türün herhangi bir tamsayı türü olarak typedef edilmesi gerektiğini belirtmektedir. Ancak hangi tür olarak typedef edileceği derleyicileri yazanların isteğine bırakılmıştır. Microsoft C derleyicilerinde wchar_t türü unsigned short int olarak typedef edilmiştir. gcc ve clang derleyicilerinde ise wchar_t türü unsigned int olarak typedef edilmiştir. wchar_t türü karakterleri bir byte'tan uzun olan karakter tablolarının karakterlerini temsil etmek için düşünülmüştür. Ancak C standartları wchar_t türünün hangi karakter tablosunu temsil edeceği konusunda bir belirlemede bulunmamıştır. Microsoft'un C ve C++ derleyicilerinde wchar_t türü UNICODE UTF-16 encoding'ini temsil etmektedir. gcc ve clang derleyicilerinde ise wchar_t türü UNICODE UTF-32 encoding'ini temsil etmektedir. C standartlarında wchar_t türünden karakter sabitleri tek tırnağa bitişik L ile ifade edilmektedir. Örneğin: wchar_t ch = L'ş'; Benzer biçimde wchar_t türünden yazılar da yine iki tırnağa bitişik L harfile temsil edilmektedir. Örneğin: wchar_t s[] = L"ağrı dağı"; char_t *ps = L"kpnya ovası"; Aşağıdaki ilkdeğer vermeler geçersizdir: wchar_t s[] = "ağrı dağı"; /* geçersiz! */ char k[] = L"ağrı dağı"; /* geçersiz! */ O halde bir fonksiyon bizden bir UNICODE yazı isteyecekse onu "char *" türü ile değil "wchar_t *" türüyle istemelidir. Bizim de o fonksiyona yazıyı L önekli bir string ile vermemiz gerekir. Öte yandan C'deki klasik string fonksiyonları bir byte'lık karakter tabloları için düşünülmüştür. Örneğin strlen fonksiyonu null karakter görene kadar birer byte'lık karakterlerin sayısını bize verir. printf fonksiyonu bir byte'lık karakter tablosuna ilişkin yazıları stdout dosyasına yazdırmaktadır. İşte C'de dosyası içerisinde "wide character" türü ile çalışabilen standrat C fonksiyonları da bulundurulmuştur. char türü ile çalışan standart C fonksiyonlarının isimleri strxxx olmak üzere wchar_t türü ile çalışan standart C fonksiyonlarının isimleri wcsxxx biçimindedir. Örneğin bir bir UNICODE yazının uzunluğunu strlen ile elde edemeyiz. Bunun için bizim wcslen fonksiyonunu kullanmamız gerekir. printf fonksiyonun wchar_t ile çalışan biçiminin ismi wprintf biçimindedir. C standartlarından ya da ilgili dokğmanlardan char türü ile çalışan fonksiyonların wchar_t türü ile çalışan versiyonlarının isimlerine bakabilirsiniz. Örneğin: wchar_t s[] = L"ali"; wprintf(L"%zd\n", wcslen(s)); > Windows Sistemlerinde UNICODE ve ASCII uyumlu kodlar yazmak: Eskiden Windows'un 3'lü versiyonlarında işletim sistemi tamamen ASCII yazılarla çalışıyordu. Windows 95, Windows 98 ve Milenium versiyonlarında kernel ASCII olarak çalışıyordu. UNICODE yazılar ASCII'ye dönüştürülüp işlem yapılıyordu. Ancak Windows NT grubu sistemlerin kernel kodları tam tersine UNICODE olarak çalışıyordu. Bu kernel versiyonları ASCII yazıları UNICODE dönüştürerek işleme sokuyordu. Artık çok uzun bir süredir Windows kernel kodları yalnızca UNICODE kullanmaktadır. Dolayısıyla ASCII yazılar alan API fonksiyonları kernel tarafından UNICODE yazılara dönüştürülerek işleme sokulmaktadır. Bugün Windows sistemlerinde bütün yazı parametresi alan API fonksiyonları A'lı ve W'lu iki ayrı versiyon olarak bulunmaktadır. Örneğin yazı parametresi alan API fonksiyonunun ismi XXX olmak üzere aslında XXXA ve XXXW biçiminde iki ayrı API fonksiyonu vardır. XXX biçiminde bir API fonksiyonu yoktur. Örneğin CreateFile isimli bir API fonksiyonu aslında yoktur. CreateFileA ya da CreateFileW isimli API fonksiyonları vardır. İşte ileride açıklayacağımız biçimde biz kodumuzda CreateFile ismini kullandığımızda bu isim önişlemci tarafından aslında CreateFileA ya da CreateFileW biçimine dönüştürülmektedir. Pekiyi XXX isimli bir API fonksiyonu önişlemci tarafından nasıl XXXA ya da XXXW biçimine dönüştürülmektedir? İşte bunun için UNICODE isimli bir sembolik sabit kullanılmaktadır. dosyası içerisinde fonksiyonların isimlerini dönüştüren aşağıdaki makrolar bulunmaktadır: #ifdef UNICODE #define CreateFile CreateFileW #define CreateProcess CreateProcessW #define SetCurrentDirecory SetCurrentDirectoryW ... #else #define CreateFile CreateFileA #define CreateProcess CreateProcessA #define SetCurrentDirecory SetCurrentDirectoryA ... #endif Görüldüğü gibi yazı parametresi alan API fonksiyonlarının isimleri UNICODE sembolik sabitine bağlı olarak A'lı ya da W'lu biçime dönüştürülmektedir. UNICODE sembolik sabiti define edilmemişse program ASCII uyumlu define edilmişse UNICODE uyumlu hale gelmektedir. Eğer bu sembolik sabit define edilecekse dosyasının yukarısında define edilmeli ya da "-D seçeneği ile predefined" yapılmalıdır. Ancak Visual Studio IDE'sinde bu işlem de görsel olarak yapılabilmektedir. Proje seçeneklerine gelinip "Properties/Configuration Properties/Advanced"/Character Set" eğer "Use Unicode Character Set" olarak girilirse aslında IDE "cl" derleyicisini "/D UNICODE" komut satırı argümanını da ekleyerek çalıştırmaktadır. Dolayısıyla Visual Studio IDE'sinde çalışıyorsak bu seçeneği seçerek API fonksiyonlarının UNICODE versiyonlarının kullanılmasını sağlayabiliriz. Tabii bu seçenek seçilmezse UNICODE sembolik sabiti define edilmemiş olacağı için API fonksiyonlarının ASCII versiyonları kullanılacaktır. Ancak programımızı UNICODE uyumlu yazabilmek için yalnızca API fonksiyonlarının UNICODE versiyonlarının kullanılması yetmemektedir. Çünkü API fonksiyonlarının ASCII ve UNICODE versiyonlarının yazı parametreleri farklı türlerdendir. * Örnek 1, Aşağıda bir Windows API programının UNICODE olarak yazılmasını görüyorsunuz: #define UNICODE #include #include #include void ExitSys(LPCWSTR lpszMsg); int main(void) { HANDLE hFile; if ((hFile = CreateFile(L"test.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) ExitSys(L"CreateFile"); printf("success...\n"); CloseHandle(hFile); return 0; } void ExitSys(LPCWSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPWSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fwprintf(stderr, L"%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } İşte bir Windows programını UNICODE uyumlu yazmak demek "yalnızca UNICODE ve _UNICODE sembolik sabitlerine dayalı olarak kodda hiçbir değişiklik yapılmadan kodun hem ASCII hem de UNICODE derlemesinde sorun çıkarmaması" demektir. UNICODE uyumlu Windows API kodu yazabilmek için, -> Program içerisindeki tüm string'ler TEXT makrosuyla ya da _TEXT makrosuyla kullanılmalıdır. TEXT makrosu içerisinde _TEXT makrosu ise içerisinde bulunmaktadır. dosyası da Microsoft spesifik bir dosyadır. Biz TEXT makrosunu kullanacaksak dosyasının include edilmesi yeterlidir. Ancak biz _TEXT makrosunu kullanacaksak dosyasının include edilmesi gerekir. Genel olarak programcılar her iki dosyayı da include ederler. Başı "_" ile başlayan ve ilk harfi büyük olan tüm isimler "reserved" olduğu için _TEXT makrosunun kullanılması C için daha uygun gibi görünmektedir. İki makronun da yaptığı işlem aynıdır. TEXT makrosu UNICODE sembolik sabitine, _TEXT makrosu ise _UNICODE sembolik sabitine bakmaktadır. TEXT makrosu şöyle yazılmıştır: #ifdef UNICODE #define TEXT(s) L##s #else #define TEXT(s) s #endif _TEXT makrosu ise şöyle yazılmıştır: #ifdef _UNICODE #define _TEXT(s) L##s #else #define _TEXT(s) s #endif Genellikle programcıların hem UNICODE hem de _UNICODE sembolik sabitlerini define etmeleri gerekmektedir. Zaten Visual Studio IDE'sinde "character set" UNICODE olarak değiştirildiğinde her iki sembolik sabit de define edilmektedir. -> char türü yerine TCHAR isimli typedef türünün kullanılması gerekir. Çünkü TCHAR türü UNICODE sembolik sabitine göre char ya da wchar_t türü anlamına gelmektedir. TCHAR makrosu içerisinde aşağıdaki gibi oluşturulmuştur: #ifdef UNICODE #define TCHAR wchar_t #else #define TCHAR char #endif Karakter sabitleri için de içerisindeki _T makrosu kullanılmaktadır. Bu makro da şöyle oluşturulmuştur: #ifdef _UNICODE #define _T(c) L##c #else #define _T(c) c #endif -> LPCSTR const char * anlamına, LPSTR ise char * anlamına gelir. Bunların UNICODE versiyonları LPCWSTR ve LPWSTR biçimindedir. Ancak T li versiyonlar yine UNICODE sembolik sabitine bağlı olarak char * ya da wchar_t * türünü temsil etmektedir. Bu nedenle yazılara ilişkin gösterici parametreleri ya TCHAR * ve const TCHAR * biçiminde ya da LPTSTR ve LPCTSTR biçiminde belirtilmelidir. Burada örneğin LPCSTR türü ile LPCTSTR türü arasındaki farka dikkat ediniz. LPCSTR türü her zaman const char * anlamına gelmektedir. Halbuki LPCTSTR türü duruma göre const char * duruma göre ise const wchar_t * anlamına gelmektedir. -> Standart C fonksiyonları için de aynı durum söz konusudur. Örneğin ASCII programlarda printf kullanılırken UNICODE programlarda wprintf kullanılmaktadır. Ancak uyumlu printf ismi için yine dosyası içerisindeki _xxx biçimindeki özel fonksiyon isimleri kullanılır. Her standart C fonksiyonun Windows sistemlerindeki UNICODE uyumlu ismi bilinmelidir. Örneğin _tprintf ismi printf fonksiyonun UNICODE uyumlu ismidir. Bu isim _UNICODE sembolik sabiti define edilmişse wprintf olarak, edilmemişse printf olarak değiştirilmektedir. -> Microsoft aslında main fonksiyonun da iki versiyonunu bulundurmaktadır: main ve wmain. Her ne kadar C standartlarında wmain diye bir fonskiyon yoksa da Microsoft Windows sistemlerinde main fonksiyonunun UNICODE versiyonu wmain biçimindedir. Yani siz eğer main fonksiyonunun argv parametresinin UNICODE olmasını istiyorsanın artık main yerine wmain kullanmalısınız. main fonksiyonunun _UNICODE uyumlu ismi _tmain biçimindedir. O halde main fonksiyonu da şöyle tanımlanabilir: int _tmain(int args, TCHAR *argv[]) { //... } gibi hususlara dikkat etmeliyiz. * Örnek 1, Aşağıda basit bir Windows API programının UNICODE uyumlu yazımı verilmiştir. #include #include #include #include void ExitSys(LPCTSTR lpszMsg); int _tmain() { HANDLE hFile; TCHAR path[MAX_PATH] = _TEXT("test.txt"); if ((hFile = CreateFile(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) ExitSys(_TEXT("CreateFile")); _tprintf(_TEXT("Ok")); CloseHandle(hFile); return 0; } void ExitSys(LPCTSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { _ftprintf(stderr, _TEXT("%s: %s"), lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Şimdi de daha önce ASCII uyumlu olarak yazdığımız proses listesini alan programı UNICODE uyumlu olarak yazalım. Aşağıdaki örneği inceleyiniz. Programda yukarıda sıraladığımız maddelere uyulduğuna dikkat ediniz. #include #include #include #include #include #define NPROCESSES 4096 #define NMODULES 4096 void ExitSys(LPCTSTR lpszMsg); int _tmain(void) { DWORD dwProcessIds[NPROCESSES]; DWORD cbNeeded; HANDLE hProcess; HMODULE hModules[NMODULES]; DWORD dwProcessCount; DWORD dwModuleCount; TCHAR szModuleName[MAX_PATH]; if (!EnumProcesses(dwProcessIds, sizeof(dwProcessIds), &cbNeeded)) ExitSys(_TEXT("EnumProcesses")); dwProcessCount = cbNeeded / sizeof(DWORD); for (DWORD i = 0; i < dwProcessCount; ++i) { if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessIds[i])) == NULL) continue; if (!EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded)) { CloseHandle(hProcess); continue; } dwModuleCount = cbNeeded / sizeof(DWORD); for (DWORD k = 0; k < dwModuleCount; ++k) { if (!GetModuleBaseName(hProcess, hModules[k], szModuleName, MAX_PATH)) continue; if (k == 0) { _tprintf(_TEXT("%s: "), szModuleName); continue; } if (k != 1) _tprintf(_TEXT(", ")); _tprintf(_TEXT("%s"), szModuleName); } _tprintf(_TEXT("\n\n")); CloseHandle(hProcess); } _tprintf(_TEXT("%lu process(es) listed...\n"), dwProcessCount); getchar(); return 0; } void ExitSys(LPCTSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { _ftprintf(stderr, _TEXT("%s: %s"), lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Gördüğünüz üzere programların UNICODE uyumlu yazılması konusu Microsoft Windows sistemlerine özgüdür. UNIX/Linux sistemlerinde böyle bir konu yoktur. Bütün POSIX fonksiyonları ASCII uyumludur. Dolayısıyla Windows sistemlerinde olduğu gibi bir POSIX fonksiyonunun UNICODE ve ASCII versiyonları yoktur. Zaten Linux sistemlerinde UNICODE gerektiği zaman UNICODE UTF-8 encoding'i kullanılmaktadır. Bu encoding de "multibyte" bir kodlama oluşturduğu için char * türü ile ifade edilebilmektedir. > Proseslerin "exit code" Bilgileri: Bir proses sonlandığında işletim sistemine "exit kodu" ya da "exit durumu" denilen bir kod iletmektedir. Proseslerin exit kodları tamsayı bir değerdir. İşletim sistemleri bu exit kodunu alır. Ancak onun hangi değerde olduğuna bakmaz. Bu exit kod prosesi yaratan proses (yani üst proses) isterse ona verilmektedir. Böylece biz bir programı çalıştırdıktan sonra onun işini başarılı bir biçimde yapıp yapmadığını exit koduna bakarak anlayabiliriz. Proseslerin exkit kodları fonksiyonların geri dönüş değerlerine benzetilebilir. Geleneksel olarak işletim sistemlerinde başarılı sonlanmalar için sıfır değeri başarısız sonlanmalar için sıfır dışı değerler kullanılmaktadır. Bir C programında prosesin exit odu standart exit fonksiyonunun argümanı ile belirlenmektedir. exit fonksiyonunun prototipini anımsayınız: void exit(int status); Ayrıca C'de okunabilirliği artırmak için içerisinde EXIT_SUCCESS ve EXIT_FAILURE sembolik sabitleri de bulundurulmuştur. C standartlarında bu sembolik sabitlerin değerleri belirtilmemiş olsa da tipik olarak 0 ve 1 biçimindedir: #define EXIT_SUCCESS 0 #define EXIT_FAILURE 1 Pekiyi biz hiç exit fonksiyonunu çağırmazsak prosesin exit kodu ne olacaktır? İşte bu durumda main fonksiyonun geri dönüş değeri prosesin exit kodu olmaktadır. C standartları main fonksiyonunun exit(main(...)) biçiminde çağrıldığını belirtmektedir. Pekiyi bir program normal dışı sonlanamaz mı? Örneğin standart abort fonksiyonunun bir exit kod parametresi yoktur: void abort(void); Bir program abort fonksiyonuyla sonlanırsa ya da çökerse exit kodu ne olacaktır? İşte bu tür durumlarda prosesin exit kodları belirsiz durumda olur. Dolayısıyla programcının eğer program normal olarak sonlanmışsa exit koduna bakması uygun olur. UNIX/Linux kabuk programlarında son çalıştırılan programın exit kodu $? kabuk değişkeni ile elde edilebilmektedir. Örneğin: $ ./sample $ echo $? 123 Aynı şey Windows kabuklarında errorlevel değişkeni ile yapılmaktadır. Örneğin: C:\>sample C:\>echo %errorlevel% 123 C standartlarında main fonksiyonuna özgü şöyle bir durum belirtilmiştir: Eğer main fonksiyonunda hiç return kullanılmamışsa akışi ana bloğu bitirerek main sonlanmışsa sanki ana bloğun sonunda "return 0" deyim varmış gibi bir etki oluşmaktadır. Yani örneğin: int main(void) { ... return 0; } ile aşağıdakinin arasında hiçbir farklılık yoktur: int main(void) { ... } Tipik olarak bir prosesin sonlandırılması exit standart C fonksiyonuyla yapılmaktadır. (Anımsanacağı gibi eğer bu fonksiyon hiç çağrılmazsa zaten main bittiğinde çağrılmaktadır.) exit fonksiyonu aslında sonlandırmayı işletim sisteminin prosesi sonlandıran sistem fonksiyonunu çağırarak yapmaktadır. Bu sistem fonksiyonu Windows sistemlerinde ExitProcess, UNIX/Linux ve macOS sistemlerinde _exit isimli fonksiyondur. Ancak exit standart C fonksiyonu bundan önce sırasıyla şu işlemleri de yapmaktadır: -> atexit fonksiyonuyla kaydettirilmiş olan fonksiyonları ters sırada çağırır. -> Açık olan bütün dosyaların stdio tamponlarını flush eder ve bu dosyaları kapatır. Şimdi de sırasıyla bu Windows ve POSIX fonksiyonlarını inceleyelim: >> Windows Sistemlerinde: >>> "ExitProcess" : Windows sistemlerinde prosesi sonlandıran API fonksiyonu ExitProcess isimli fonksiyondur. Zaten Windows sistemlerinde C'nin standart exit fonksiyonu bu fonksiyonu çağırmaktadır. Fonksiyonun prototipi şöyledir: void ExitProcess(UINT uExitCode); Fonksiyon parametre olarak prosesin exit kodunu almaktadır. Bu fonksiyon başarısız olamamaktadır. Bu fonksiyon normal olarak standart C fonksiyonları için herhangi bir sonlandırma işlemi yapmaz. (Microsoft standart C kütüphanesinde bir süredir ExitProcess tarafından çağrılan DLL_THREAD_DETACH bildirimi yoluyla stdio tamponlarını flush etmeye başlamıştır. Bu nedenle ExitProcess uygulandığında stdio tamponları flush edilmektedir. Ancak atexit fonksiyonu ile kaydettirilen fonksiyonlar çağrılmamaktadır.) >>> "GetExitCodeProcess" : Windows sistemlerinde bir proses bir program çalıştırdığında çalıştırdığı programın exit kodunu GetExitCodeProcess isimli API fonksiyonuyla elde edebilmektedir. Bu işlemi yapabilen herhangi bir standart C fonksiyonu yoktur. Fonksiyonun prototipi şöyşedir: BOOL GetExitCodeProcess( HANDLE hProcess, LPDWORD lpExitCode ); Fonksiyonun, >>>> Birinci parametresi exit kodun elde edileceği prosesin handle değerini belirtmektedir. >>>> İkinci parametresi exit kodunun yerleştirileceği DWORD türünden nesnenin adresini almaktadır. 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. Aşağıdaki örnekte "prog1" programı "prog2" programını CreateProcess ile çalıştırıp onun exit kodunu GetExitCodeProcess API fonksiyonu ile elde etmiştir. Programlar UNICODE/ASCII uyumlu biçimde yazılmıştır. Burada "prog1" programı çalıştırdığı "prog2" programının bitmesini WaitForSingleObject fonksiyonuyla beklemektedir. Bu fonksiyonu thread'ler konusunda ele alacağız. * Örnek 1, /* prog1.c */ #include #include #include #include void ExitSys(LPCTSTR lpszMsg); int main(void) { TCHAR szPath[] = _TEXT("prog2.exe"); STARTUPINFO si = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION pi; DWORD dwExitCode; if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) ExitSys(_TEXT("CreateProcess")); _tprintf(_TEXT("waiting for process to exit...\n")); if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED) ExitSys(_TEXT("WaitForSingleObject")); if (!GetExitCodeProcess(pi.hProcess, &dwExitCode)) ExitSys(_TEXT("GetExitCodeProcess")); if (dwExitCode == STILL_ACTIVE) { _tprintf(_TEXT("Proces still active...\n")); exit(EXIT_FAILURE); } _tprintf(_TEXT("Exit Code: %lu\n"), dwExitCode); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } void ExitSys(LPCTSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { _ftprintf(stderr, _TEXT("%s: %s"), lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* prog2.c */ #include #include int _tmain(void) { _tprintf(_TEXT("press ENTER to exit...\n")); getchar(); return 123; } >>> "TerminateProcess" : exit standart C fonksiyonu ve ExitProcess API fonksiyonu kendi prosesini sonlandırmaktadır. Ancak bazen bir prosesi zorla sonlandırmak da isteyebiliriz. Bunun için Windows sistemlerinde TerminateProcess isimli API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL TerminateProcess( HANDLE hProcess, UINT uExitCode ); Fonksiyonun, >>>> Birinci parametresi sonlandırılacak prosesin handle değerini, >>>> İkinci parametresi sonlandırılacak prosesin exit kodunu belirtmektedir. Fonksiyon başarı durumunda sıfır dışı bir değere başarıızlık durumunda sıfır değerine geri dönmektedir. TerminateProcess fonksiyonu geri döndüğünde proses sonlanmış olmak zorunda değildir. Ancak sonlanma süreci başlatılmıştır. Sonlanmanın yine WaitForSingleObject gibi bir fonksiyonla beklenmesi uygun olur. Öte yandan her proses handle değerini bildiği her prosesi sonlandıramaz. Ancak üst proses genel olarak kendi alt proseslerini sonlandırabilmektedir. Windows'taki yetkisel özellikler karmaşık bir konusudur. Bu kursta ele alınmamaktadır. ("Windows Sistem Programalama" kursunda bu konu ele alınmaktadır.) TerminateProcess fonksiyonu son çare olarak uygulanmalıdır. Çünkü programın ansızın sonlandırılması o programın çalışması sırasında olumsuzluklar oluşturabilir. Proses kritik birtakım işlemleri yaparken sonlandırılırsa sorunlar oluşabilmektedir. * Örnek 1, Aşağıdaki örnekte "prog1" programı ENTER tuşuna basıldığında "prog2" programını zorla TerminateProcess API fonksiyonuyla sonlandırmaktadır. Kod yine UNICODE/ASCII uyumlu yazılmıştır. /* prog1.c */ #include #include #include #include void ExitSys(LPCTSTR lpszMsg); int main(void) { TCHAR szPath[] = _TEXT("prog2.exe"); STARTUPINFO si = {sizeof(STARTUPINFO)}; PROCESS_INFORMATION pi; DWORD dwExitCode; if (!CreateProcess(NULL, szPath, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) ExitSys(_TEXT("CreateProcess")); _tprintf(_TEXT("Press ENTER to to terminate child process...\n")); getchar(); if (!TerminateProcess(pi.hProcess, 999)) ExitSys(_TEXT("TerminateProcess")); if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED) ExitSys(_TEXT("WaitForSingleObject")); if (!GetExitCodeProcess(pi.hProcess, &dwExitCode)) ExitSys(_TEXT("GetExitCodeProcess")); if (dwExitCode == STILL_ACTIVE) { _tprintf(_TEXT("Proces still active...\n")); exit(EXIT_FAILURE); } _tprintf(_TEXT("Exit Code: %lu\n"), dwExitCode); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } void ExitSys(LPCTSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { _ftprintf(stderr, _TEXT("%s: %s"), lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include int _tmain(void) { for (int i = 0; i < 100; ++i) { _tprintf(_TEXT("%d\n"), i); Sleep(1000); } return 0; } >> UNIX/Linux Sistemleri: Biz Windows sistemlerinde prosesin exit kodunu GetExitCodeProcess isimli API fonksiyonuyla elde etmiştik. WaitForSingleObject fonksiyonunu ise Proses sonlanana kadar beklemek için kullanmıştık. Şimdi de UNIX/Linux sistemlerinde benzer işlemlerin nasıl yapılacağını göreceğiz. UNIX/Linux sistemlerinde alt proses sonlanana kadar bekleme işlemi ve alt prosesin exit kodunu alma işlemi wait, waitpid ve waitid isimli üç POSIX fonksiyonu kullanılmaktadır. Linux sistemlerinde wait3 ve wait4 isimli wait fonksiyonları da vardır. wait fonksiyonlarının iki önemli işlevi vardır: Alt proses bitene kadar üst prosesi blokede bekletmek ve alt prosesin exit kodunu elde etmek. Çok eskidne yalnızca wait fonksiyonu vardı. Sonra bu fonksiyonun yetersizlikleri nedeniyle waitpid ve sonra da waitid fonksiyonları tasarlandı. Bu fonksiyonunların hepsinin prototipleri dosyası içerisindedir. Şimdi de sırasıyla bu wait fonksiyonları inceleyelim: >>> "wait" : wait fonksiyonunun prototipi şöyleid:r #include pid_t wait(int *status); Fonksiyon parametre olarak alt prosesin exit kodunun ve sonlanma biçiminin (buna "status" de denilmektedir) yerleştirileceği int nesnenin adresini almaktadır. Fonksiyon başarı durumunda beklenen alt prosesin id değeri ile başarısızlık durumunda -1 değeri ile geri dönmektedir. Fonksiyon çağrıldığında bir ya da birden fazla ilt prosesin hiçbiri henüz sonlanmamış olabilir. Bu durumda fonksiyon ilk sonlanacal alt prosese kadar bekleme yapar. Eğer fonksiyon çağrıldığında zaten bir ya da birden fazla alt proses sonlanmış durumdaysa fonksiyon bunlardan herhangi birinin durumunu (status) elde ederek hemen geri döner. Fonksiyonun ilk sonlanmış olan prosesin durumuyla geri dönmesi garanti edilmemiştir. Fonksiyonun parametresi NULL adres de geçilebilir. Bu durumda fonksiyon yine bekleme işlevini yerine getirir ancak alt prosesin durum bilgisini programcıya iletmez. (Yani programcı yalnızca alt prosesi beklemek istiyorsa, onun durum bilgisini elde etmek istemiyorsa parametre olarak NULL adres geçebilir.) Fonksiyonun bize verdiği durumsal bilgide hem prosesin nasıl sonlandığı hem de exit kodu bulunmaktadır. Bu bilgilerin int nesnenin neresinde depolandığı standart olarak belirlenmemiştir. Bunun için WIFEXITED ve WIFSIGNALED makroları kullanılmaktadır. Bu iki makro da wait fonksiyonun parametresine geçirilen int nesneyi parametre olarak almaktadır. WIFEXITED normal sonlanma durumunu, WIFSIGNALED sinyal dolayısıyla normal olmayan sonlanma durumunu test etmektedir. Bu makrolar sıfır ya da sıfır dışı değere geri dönmektedir. int nesne içerisindeki prosesin exit kodu WEXITSTATUS makrosuyla elde edilmektedir. Tabii programcı ancak proses normal sonlanmışsa exit kodunu bu makroyla almaya çalışmalıdır. Örneğin: int status; ... if (wait(&status) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("Exit code: %d\n", WEXITSTATUS(status)); else /* if (WIFSIGNALED(status)) */ printf("child terminates via signal!..\n"); Aşağıdaki örnekte "sample" programı komut satırı argümanlarıyla aldığı programı fork/exec ile çalıştırıp onun sonlanmasını beklemektedir. "sample" programını aşağıdaki gibi çalıştırabilirsiniz: $ ./sample mample Buradaki "mample" programı birer saniye aralıklarla ekrana sayıları basıp 123 exit koduyla geri dönmektedir. Sonra da "sample" programını standart komutlar için çalıştırarak durumu gözlemleyiniz: $ ./sample /bin/ls Pek çok UNIX/Linux sisteminde prosesin exit kodu 1 byte uzunlukta işaretsiz tamsayı türündendir. * Örnek 1, /* sample.c */ #include #include #include #include void exit_sys(const char* msg); int main(int argc, char* argv[]) { pid_t pid; int status; if (argc == 1) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0 && execv(argv[1], &argv[1]) == -1) exit_sys("execv"); printf("parent process waiting for the child to exit...\n"); if (wait(&status) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("child exited with %d\n", WEXITSTATUS(status)); else /* if (WIFSIGNALED(status)) */ printf("child terminates via signal!..\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include #include int main(int argc, char* argv[]) { for (int i = 0; i < 10; ++i) { printf("%d\n", i); sleep(1); } exit(123); return 0; } * Örnek 2, Aşağıdaki örnekte üst proses üç farklı alt proses yaratmıştır ve hepsini wait fonksiyonu ile beklemiştir. Ekranda aşağıdaki gibi bir çıktı göreceksiniz: child created: 3471 child running... child created: 3472 child running... child created: 3473 child (3472) terminated with 20 child running... child (3471) terminated with 10 child (3473) terminated with 30 Tabii buradaki alt proseslerin sonlanma sıraları birbirlerinden farklı olabilecektir. #include #include #include #include #include void exit_sys(const char* msg); int main(void) { pid_t pid1, pid2, pid3, pid; int status; if ((pid1 = fork()) == -1) exit_sys("fork"); if (pid1 == 0) { printf("child running...\n"); exit(10); } else printf("child created: %jd\n", (intmax_t)pid1); if ((pid2 = fork()) == -1) exit_sys("fork"); if (pid2 == 0) { printf("child running...\n"); exit(20); } else printf("child created: %jd\n", (intmax_t)pid2); if ((pid3 = fork()) == -1) exit_sys("fork"); if (pid3 == 0) { printf("child running...\n"); exit(30); } else printf("child created: %jd\n", (intmax_t)pid3); for (int i = 0; i < 3; ++i) { if ((pid = wait(&status)) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("child (%jd) terminated with %d\n", (intmax_t)pid, WEXITSTATUS(status)); else /* if (WIFSIGNALED(status)) */ printf("child terminates via signal!..\n"); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >>> "waitpid" : waitpid fonksiyonu wait fonksiyonunun daha gelişmiş bir biçimidir. Fonksiyonun prototipi şöyledir: #include pid_t waitpid(pid_t pid, int *status, int options); Fonksiyonun birinci parametresi beklenecek prosesin id'sini belirtmektedir. Anımsanacağı gibi wait fonksiyonu herhangi bir alt prosesi bekliyordu. Oysa waitpid fonksiyonunda biz fonksiyonun hangi alt prosesi bekleyeceğini belirleyebiliyoruz. Fonksiyonun ikinci parametresi yine durum bilgisinin yerleştirileceği int nesnesnenin adresini almaktadır. Üçüncü parametre aşağıdaki aşağıdkai sembolik sabitlerin bir ya da birden fazlasının bit OR ile işleme sokulmasıyla oluşturulabilir: WCONTINUED WNOHANG WUNTRACED Bu parametre 0 da geçilebilmektedir. WNOHANG bekleme yapmadan alt prosesin sonlanıp sonlanmadığını belirlemek amacıyla kullanılmaktadır. Fonksiyonun birinci parametresi özel olarak -1 biçiminde girilirse herhangi bir alt proses beklenir. Başka bir deyişle wait(&status) çağrısı ile waitpid(-1, &status, 0) çağrısı eşdeğerdir. Eğer bu parametre 0 girilirse fonksiyonu çağıran proses ile aynı proses grubunundaki alt proseslerden herhangi bir beklenmektedir. Eğer pid değeri -1 değerindne küçükse fonksiyon bu negatif değerin mutlak değerine ilişkin proses grubunundaki herhangi bir alt prosesi beklemektedir. Proses grupları konusu bu kursta ele alınmayacaktır. waitpid fonksiyonun da ikinci parametresi NULL adres geçilebilir. Bu durumda alt proses beklenir ancak durumsal bilgi elde edilmez. waitpid fonksiyonu da başarı durumunda beklenen prosesin id değerine başarısızlık durumunda -1 değerine geri dönmektedir. Aşağıdaki örnekte üst proses ğüç alt proses yaratmıştır. Bu alt prosesler farklı sürelerde sleep uygulamaktadır. Ancak waitpid fonksiyonu ile bekleme alt proseslerin yaratılma sırasına göre yapılmıştır. Programı çalıştırınca aşağıdaki gibi bir çıktı elde edeceksiniz: $ ./sample child created: 3313 child created: 3314 child running... child created: 3315 child running... child running... child (3313) terminated with 10 child (3314) terminated with 20 child (3315) terminated with 30 Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include #include #include void exit_sys(const char* msg); int main(void) { pid_t pids[3], pid; int status; if ((pids[0] = fork()) == -1) exit_sys("fork"); if (pids[0] == 0) { printf("child running...\n"); sleep(3); exit(10); } else printf("child created: %jd\n", (intmax_t)pids[0]); if ((pids[1] = fork()) == -1) exit_sys("fork"); if (pids[1] == 0) { printf("child running...\n"); sleep(2); exit(20); } else printf("child created: %jd\n", (intmax_t)pids[1]); if ((pids[2] = fork()) == -1) exit_sys("fork"); if (pids[2] == 0) { printf("child running...\n"); sleep(1); exit(30); } else printf("child created: %jd\n", (intmax_t)pids[2]); for (int i = 0; i < 3; ++i) { if ((pid = waitpid(pids[i], &status, 0)) == -1) exit_sys("wait"); if (WIFEXITED(status)) printf("child (%jd) terminated with %d\n", (intmax_t)pid, WEXITSTATUS(status)); else /* if (WIFSIGNALED(status)) */ printf("child terminates via signal!..\n"); } return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } UNIX/Linux dünyasında "hortlak (zomibie)" proses denilen bir kavram vardır. Üst proses fork ile alt prosesi yarattıktan sonra alt proses üst prosesten önce sonlanırsa işletim sistemi üst prosesin wait fonksiyonlarıyla sonlanmış olan alt prosesin durum bilgisini alabileceğini düşünerek onun proses kontrol bloğunu ve dolayısıyla da proses id'sini boşaltmaz. Proseslerin durum bilgisi genel olarak proses kontrol bloklarında tutulmaktadır. Ancak sonlanmış olan alt prosesler için üst proses wait fonksiyonlarını uygulamazsa alt prosesin proses kontrol bloğu ve proses id'si sistem tarafından boşaltılmadığından dolayı bir sızıntı (leak) oluşturmaktadır. Bu sızıntı birkaç proses için önemsiz olsa da binlerce alt proses yaratan ve bunları wait ile beklemeyen uzun ömürlü prosesler söz konusu olduğunda sistemi çalışamaz hale getirecek derecede ciddi sonuçlar oluşturabilmektedir. İşte sonlandığı halde üst prosesin wait fonksiyonlarını uygulamadığı alt proseslere UNIX/Linux dünyasında "hortlak (zomibe)" proses denilmektedir. Zombie proses sonlanmıştır ancak proses kontrol bloğu tam olarak boşaltılamamıştır. Zaten zombie sözcüğü tam ölememiş, yaşamakla ölmek arasında kalmış canlılar için uydurulmuş bir sözcüktür. Bu anlamda zombie insan bulunmamaktadır. Zombie'lik ancak üst proses devam ederken alt proses sonlanmışsa ve üst proses alt prosesi wait fonksiyonlarıyla beklememişse bu süreç içerisinde oluşmaktadır. Üst prosesin de sonlanmasıyla artık alt prosesin durum bilgisini alacak bir proses kalmadığı için işletim sistemi alt prosesin kaynaklarını (proses kontrol bloğunu) boşaltır ve alt prosesin zombie'lik durumu sona erer. Eğer üst proses alt prosesten daha önce sonlanırsa bu durumdaki alt proseslere "öksüz (orphan)" prosesler denilmektedir. İşletim sistemi bir proses öksüz duruma geldiğinde 1 numaralı proses id'ye sahip olan "init" prosesini öksüz prosesin üst prosesi yapmaktadır. "init" prosesi de öksüz proses sonlandığında wait işlemi uygulayarak onun kaynaklarını boşaltmaktadır. Aşağıdaki örnekte üst proses fork ile alt proses yaratmış ancak alt proses hemen sonlanmıştır. Üst proses ise getchar fonksiyonunda bekletilmiştir. Başka bir ekrandan girip ps komutuyla bu proseslere bakıldığında alt prosesin durum bilgisinin 'Z' harfi ile gösterildiğini ve onun yanında ibaresinin bulunduğunu göreceksiniz. * Örnek 1, #include #include #include #include void exit_sys(const char* msg); int main(void) { pid_t pid; if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { /* child process */ exit(EXIT_SUCCESS); } printf("Press ENTER to exit...\n"); getchar(); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bazen üst proses alt prosesi yarattıktan sonra onu beklemeden bazı şeyler yapmak isteyebilir. Bu tür durumlarda üst proses wait fonksiyonlarını uygulamadığından dolayı alt proses zombie durumda kalacaktır. Zombie oluşmasının wait ile bekleme yapmadan otomatik engellenmesinin bu sistemlerde iki yolu vardır: -> Üst proses SIGCHLD sinyalini set eder. Alt proses sonlandığında işletim sistemi bu sinyali üst prosese göndermektedir. Üst proses de bu sintyalde asenkron biçimde wait uygular. -> Üst proses işin başında SIGCHLD sinyalini "ignore" edebilir. Bu durumda alt proses sonlanır sonlanmaz işletim sistemi alt prosesin kaynaklarını yok etmektedir. Biz bu kursumuzda UNIX/Linux sistemlerinde "sinyal (signal)" kavramını görmeyeceğiz. Bu konu "UNIX/Linux Sistem Programlama" kurslarında ele alınmaktadır. > Hatırlatıcı Notlar: >> Çeşitli uzantılara sahip dosyaların önceden belirlenmiş bir formatı vardır. Buna "dosya formatı (file format)" denilmektedir. Dosya formatları bazen standardizasyon kurumları tarafından çoğu zaman ise şirketler ve kurumlar tarafından oluşturulmaktadır. Dünyada çeşitli konulara ilişkin yüzlerce dosya formatı bulunmaktadır. Bir dosya formatını anlayabilmek için o konu hakkında bilgiye sahip olmamız gerekir. Hiç Autocad kullanmamış birisi Autocad dosya formatını anlayamaz. Dosya formatlarının başında hemen her zaman bir "başlık kısmı (header)" bulunmaktadır. Dosyaya ilişkin kritik "meta-data" bilgileri bu başlık kısmında tutulmaktadır. Genellikle dosya formatlarının ilk birkaç byte'ı "magic number" denilen özel bazı değerler içermektedir. Bunun nedeni o dosya formatını okuyup anlamlandıracak programların dosyanın doğru formatta olup olmadığını kabaca (ama kesin değil) test etmesini sağlamaktır. İşte derleyicilerin ürettikleri "amaç dosyaların (object files)" ve bağlayıcıların ürettikleri "çalıştırılabilir dosyaların (executable files)" da belli formatları vardır. Tabii bu dosya formatlarını anlayabilmek için yine aşağı seviyeli pek çok kavramın bilinmesi gerekmektedir. Microsoft'un kullandığı amaç dosya formatına "COFF (Common Object File Format)" denilmektedir. Microsoft daha önce DOS zamanlarında "OMF (Object Module Format)" basit bir format kullanıyodu. Microsoft'un bugün Windows sistemlerinde kullandığı "çalıştırılabilir (executable)" dosya formatına "PE (Portable Executable)" dosya formatı denilmektedir. COFF formatı ile PE formatı birbirine çok benzerdir. Bugün Linux sistemleri ve diğer UNIX türevi sistemler amaç dosya formatı olarak ve çalıştırılabilir dosya formatı olarak "ELF (Executable and Linkable Format)" denilen formatı kullanmaktadır. ELF hem bir amaç dosya formatı hem de çalıştırılabilir dosya formatıdır. Uzun süredir macOS sistemlri Mach-O isimli bir amaç dosya ve çalıştırılabilir dosya formatı kullanmaktadır. /*================================================================================================================================*/ (47_09_12_2023) & (48_10_12_2023) & (49_16_12_2023) & (50_17_12_2023) > UNIX/Linux Sistemlerinde Dosya Betimleyicileri: Anımsayacağınız gibi open fonksiyonu dosya başarılı bir biçimde açıldığında "dosya betimleyici (file descriptor)" denilen int türden bir handle değeri vermektedir. Bu dosya betimleyicisi read, write, lseek, close fonksiyonlarında hangi dosya üzerinde işlem yapılacağını belirlemek için kullanılmaktadır. UNIX/Linux sistemlerinde sistem programcılarının "dosya betimleyicilerinin (file descriptors)" ne anlam ifade ettiğini bilmesi gerekmektedir. UNIX/Linux sistemlerinde proses kontrol blok içerisinde "dosya betimleyici tablosu (file descriptor table)" bir tablonun adresini tutan bir eleman vardır. Dosya betimleyici tablosu dosya nesnelerini gösteren bir gösterici dizisidir. Dosya nesneleri (file object) çekirdeğin açık bir dosya üzerinde işlem yapabilmesi için gereken bilgileri tutmaktadır. Bu durumu aşağıdaki şekille temsil edebiliriz. Proses Kontrol Blok ... ... Dosya Betimleyici Tablosu ... pfds ----------------> 0 adres --------------------------> Dosya Nesnesi ... 1 adres --------------------------> Dosya Nesnesi (aşağıdaki ile aynı nesneyi gösteriyor) ... 2 adres --------------------------> Dosya Nesnesi (yukarıdaki ile aynı nesneyi gösteriyor) ... 3 BOŞ 4 BOŞ 5 BOŞ ... 1022 BOŞ 1023 BOŞ Örneğin Linux çekirdeğinde proses kontrol blok "task_struct" isimli yapı ile temsil edilmiştir. Dosya nesneleri de "file" isimli yapı ile temsil edilmiş durumdadır. Dosya betimleyici tablosu da aslında file türünden adreslerden olulan bir gösterici dizisidir. Bu gösterici dizisindeki her elemana bir slot da diyebiliriz. Buradaki her elemanın bir indeksi numarası vardır. Proses çalışmaya başladığında hemen her zaman dosya betimleyici tablosunun ilk üç slotu (yani 0, 1 ve 2 numaralı indeks elemanları) doludur. İşte "dosya betimleyicisi (file descriptor)" aslında dosta betimleyici tablosunda bir indeks belirtmektedir. 0 numaralı betimleyici (yani dizinin ilk slotu) "stdin dosyası" dediğimiz klavyeyi temsil eden terminal aygıt sürücüsüne ilişkin dosya nesnesini göstermektedir. 1 ve 2 numaralı betimleyiciler de ekranı temsil eden terminal aygıt sürücüsüne ilişkin aynı dosya nesnesini göstermektedir. Yani 0 numaralı betimleyici ile okuma yapıldığında klavyeden okuma yapılacak, 1 ve 2 numaralı betimleyici kullanılarak yazma yapıldığında yazılanlar ekrana çıkartılacaktır. open fonksiyonu ile bir dosya açıldığında işletim sistemi önce bir dosya nesnesi oluşturur. Sonra dosya betimleyici tablosundaki ilk boş slotun bu nesneyi göstermesini sağlar ve dosya betimleyicisi olarak bu slotun numarasıyla yani (dizideki indeks numarasıyla) geri döner. Örneğin yukarıdaki şekli temel alarak prosesin open fonksiyonuyla bir dosya açmış olduğunu varsayalım: Proses Kontrol Blok ... ... Dosya Betimleyici Tablosu ... pfds ----------------> 0 adres --------------------------> Dosya Nesnesi ... 1 adres --------------------------> Dosya Nesnesi (aşağıdaki ile aynı nesneyi gösteriyor) ... 2 adres --------------------------> Dosya Nesnesi (yukarıdaki ile aynı nesneyi gösteriyor) ... 3 adres --------------------------> Dosya Nesnesi 4 BOŞ 5 BOŞ ... 1022 BOŞ 1023 BOŞ Burada bize dosya betimeleyicisi olarak 3 değeri verilecektir. open fonksiyonunun dosya betimelyici tablosundaki ilk boş betimleyiciyi vermesi garanti edilmiştir. Yukarıda da belirttiğimiz gibi genellikle UNIX/Linux sistemlerinde proses çalışmaya başladığında 0, 1 ve 2 numaralı betimleyiciler dolu durumdadır. Dolayısıyla ilk boş betimleyici 3 numaralı betimleyicidir. * Örnek 1, Aşağıdaki programda önce bir dosya açılmış ve oradan 3 numaralı betimleyici elde edilmiştir. Sonra bir dosya daha açılmış oaradan da 4 numaralı betimleyici elde edilmiştir. Sonra 3 numaralı betimelyici kapatılıp yenidne bir dosya açıldığında 3 numaralı betimeleyici elde edilmiştir. Çünkü open fonksiyonu her zaman eldeki ilk boş betimleyiciyi vermektedir. #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd1, fd2, fd3; if ((fd1 = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); printf("%d\n", fd1); /* 3 */ if ((fd2 = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); printf("%d\n", fd2); /* 4 */ close(fd1); if ((fd3 = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); printf("%d\n", fd3); /* 3 */ return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bir dosya ile ilgili işlem yaapabilmek için gereken her şey "dosya nesnesi (file object)" içerisinde bulunmaktadır. Dosya nesneleri diskteki normal bir dosyaya ilişkin olabildiği gibi aygıt sürücü dosyalarına da ilişkin olabilmektedir. Aslında dosya nesnelerinin içerisinde read, write, lseek, close gibi işlemlerde çağrılacak fonksiyonaların adresleri bulunmaktadır. fd ----> Dosya Nesnesi ... okuma fonksiyonunun adresi yazma fonksiyonun adresi konumlandırma fonksiyonun adresi kapatma fonksiyonun adresi ... Böylece örneğin bir betimleyici kullanılarak read fonksiyonu çağrıldığında aslında read fonksiyonu dosya nesnesinin içerisinde adresi bulunan okuma fonksiyonunu çağırmaktadır. Tabii işlemlerin bazı yarıntıları vardır. Biz burada bu ayrıntıları basitleştirerek genel mekanizmayı açıklamak istiyoruz: ssize_t sys_read(int fd, ...) { 1) prosesin dosya betimeleyici tablosunun fd numaralı elemanından dosya nesnesine eriş 2) Dosya nesnesinde belirtilen okuma fonksiyonunu çağır } write fonksiyonu ve temel dosya fonksiyonları da benzerdir. Linux sistemlerinde bir prosesin dosya betimleyici tablosu default olarak 1024 elemanlıdır. Dolayısıyla proses hiç kapatmadan en fazla 1024 dosyayı aynı anda açık tutabilir. Tabii işin başında dosya betimelyici tablosunun ilk üç girişi zaten dolu biçimdedir. Bu durumda proses onları kapamazsa ancak 1021 dosya açabilir. Aşağıda bu testi yapan bir program verilmiştir. * Örnek 1, #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int i; for (i = 0;; ++i) { if ((fd = open("test.txt", O_RDONLY)) == -1) { perror("open"); break; } printf("%d\n", fd); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Linux sistemlerinde bir proses isterse dosya betimelyici tablosunu 1048576 kadar büyütebilir. Bunun için setrlimit POSIX fonksiyonu kullanılmaktadır. Dosya betiemleyici tablosunun uzunluğu ise getrlimit ya da sysconf fonksiyonu ile alınabilmektedir. Ancak bu konular kursumuzun kapsamı dışındadır. Aşağıda prosesin dosya betimelyici tablosunu 5000 uzunluğunda yapan örnek bir program verilmiştir. * Örnek 1, #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; struct rlimit rl; rl.rlim_cur = 5000; rl.rlim_max = 5000; if (setrlimit(RLIMIT_NOFILE, &rl) == -1) exit_sys("setrlimit"); printf("%ld\n", sysconf(_SC_OPEN_MAX)); for (int i = 0;; ++i) { if ((fd = open("sample.c", O_RDONLY)) == -1) exit_sys("open"); printf("%d\n", fd); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Mademki UNIX/Linux sistemlerinde open fonksiyonu en düşük boş betimleyiciyi bize vermektedir. O halde biz stdout aygıt sürücüne ilişkin 1 numaralı betimleyiciyi kapatıp hemen arkasından open fonksiyonu ile bir dosya açarsak open bize 1 numaralı betimleyiciyi verecektir. Yani artık 1 numaralı betimleyici stdout dosyasına ilişkin dosya nesnesini değil, disk dosyasına ilişkin dosya nesnesini gösteriyor durumda olur. İşte dosya yönlendirmeleri böyle bir mekanizmayla yapılmaktadır. Yukarıda da belirttiğimiz gibi printf, puts vs. gibi tüm standart C fonksiyonları ve diğer dillerdeki (Java, C#, Python vs.) ekrana yazan tüm fonksiyonlar eninde sonunda aslında write fonksiyonuyla 1 numaralı betimleyiciyi kullanarak yazımı yaparlar. Dolayısıyla bu yönlendirmeden sonra ekrana yazdırma için kullanılan tüm fonksiyonlar aslında yönlendirilen bu dosyaya yazacaktır.Aşağıda bu işleme bir örnek verilmiştir. * Örnek 1, Aşağıda örnekte açtığımız dosyayı biz kapatmadık. 0, 1 ve 2 numaralı betimelyiciler zaten program sona erdiğinde exit fonksiyonunda kapatılmaktadır. #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; close(1); if ((fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("open"); for (int i = 0; i < 10; ++i) printf("Test: %d\n", i); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte aynı fikirle stdin dosyası yönlendirilmiştir. Burada "test.txt" dosyası içerisinde boşluk karakterleriyle ayrılmış sayıların olduğunu varsayıyoruz. Burada scanf fonksiyonu klavyedne değil bu dosyadan okuma yapacaktır. #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; int val; close(0); if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); while (scanf("%d", &val) == 1) printf("%d\n", val); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Bir dosya betimelyicisnin gösterdiği dosya nesnesi başka bir betimelyici tarafında gösteriliyorsa ne olur? Örneğin fd1 betimelyeicisi ile fd2 betiemleyicisinin aynı dosya nesnesini gösterdiğini varsayalım. Bu durumda read, write, lseek gibi dosya fonksiyonlarına fd1 ya da fd2 betimeleyicilerinin geçirilmesinde bir fark oluşmayacaktır. Çünkü dosya işlemleri neticede bu dosya nesnesinin içerisindeki bilgilerden hareketle yapılmaktadır. Pekiyi böyle bir durumun bir faydası olabilir mi? Dosya betimleyicilerini çiftlemek (duplicate etmek) için dup ve dup2 isimli iki POSIX fonksiyonu kullanılmaktadır. Tabii bu POSIX fonksiyonları da aslında doğrudan işletim sisteminin sistem fonksiyonlarını çağırmaktadır. Bu fonksiyonlardan, >> "dup" : Fonksiyonunun prototipi şöyledir: #include int dup(int fd); Fonksiyon parametresi ile belirtilen dosya betimleyicisinin gösteridği dosya nesnesini gösteren yeni bir betimelyici tahsis etmektedir. Yani fonksiyon başarılı olursa fonksiyonun geri döndürdüğü dosya betimleyicisi ile parametrede belirtilen betimleyicinin aynı dosya nesnesini gösterir durumda olur. dup başarısızlık durumunda -1 değerine geri dönmektedir. dup fonksiyonunun dosya betimelyici tablosundaki ilk boş betimelyiciyi vermesi garanti edilmiştir. * Örnek 1, Aşağıdaki örnekte önce test.txt dosyası açılmış sonra da bu betimleyici çiftlenmiştir. Dosya göstericisinin dosya nesnesi içerisinde bulunduğunu anımsayınız. Dolayısıyla aşağıdaki örnekte dosyadan ıkuma yaparken hangi dosya betimleyicisinin kullanıldığının bir önemi kalmamaktadır. #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd1; int fd2; char buf[10 + 1]; ssize_t n; if ((fd1 = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); if ((fd2 = dup(fd1)) == -1) exit_sys("dup"); if ((n = read(fd1, buf, 10)) == -1) exit_sys("read"); buf[n] = '\0'; puts(buf); if ((n = read(fd2, buf, 10)) == -1) exit_sys("read"); buf[n] = '\0'; puts(buf); close(fd1); close(fd2); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >> "dup2" : dup2 fonksiyonu da dup fonksiyonu gibi dosya betimleyicisini çiftlemekte kullanılır. Fonksiyonun prototipi şöyleidR. #include int dup2(int fd, int fd2); Fonksiyon birinci parametresiyle belirtilen dosya betimleyicisi ile aynı dosya nesnesini gösteren bir betimleyici oluşturur. Ancak bu betimleyici en düşük boş betimleyici değil ikinci parametrede belirtilen betimleyicidir. Yani fonksiyon başarılı olduğunda birinci ve ikinci parametresiyle belirtilen dosya betimleyicileri aynı dosya nesnesini gösteriyor durumda olur. Eğer ikinci parametreyle verilen betimleyici zaten açık bir dosyanın betimleyicisi ise bu durumda o dosya önce kapatılır, betimelyici boşaltılır sonra bu betimleyicinin birinci parametresiyle belirtilen betimelycinin gösterdiği dosya nesnesini göstermesi sağlanır. dup2 fonksiyonu başarısızlık durumunda yine -1 değerine geri dönmektedir. Dosya yönlendirmeleri genellikle dup2 fonksiyonuyla yapılmaktadır. Çünkü close işlemi ve open işlemi arasında prosese ilişkin bir thread varsa ve o thread de tesadüfen open fonksiyonunu çağırırsa ilk boş betimelyiciyi o thread elde edebilir. Ayrıca ilk boş betimeleyici duruma göre farklılıklar da gösterebilir. Örneğin stdout betimeleyicisini bir dosyaya yönlendirmek isteyelim. Bu işlemi şöyle yapabiliriz kontroller uygulanmamıştır: fd = open(....); dup2(fd, 1); close(fd); Aşağıda stdout dosyasının dup2 ile doğru teknik kullanılarak yönlendirilmesi örneği verilmiştir. * Örnek 1, #include #include #include #include #include void exit_sys(const char *msg); int main(void) { 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"); if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); for (int i = 0; i < 10; ++i) printf("test: %d\n", i); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi kabuk programları nasıl IO yönlendirmesi yapmaktadır. Kabuk programları tipik olarak komut satırından girilen program için bir kez fork uygulayıp alt yarattıktan sonra, alt proseste IO yönlendirmesini yapıp sonra exec uygulamaktadır. exec işlemi sırasında prsesin kontrol bloğu değişmediği için dosya betimleyici tablosu da değişmemektedir. Dolayısıyla exec sonrasında çalıştırılan program aslında IO yönlendirmesinin etkisi altında kalacaktır. UNIX/Linux sistemlerinde exec yapıldığı zaman o ana kadar açılmış olan dosyaların exec işleminden sonra açık olarak kalması istenmeyebilir. Örneğin biz 100 tane dosya açmış olalım. Sonra fork ve exec uygulamış olalım. Şimdi exec yaptığımız program çalışırken aslında kendisinin ilgilenmediği 100 dosyayı açık olarak görecektir. Bu da exec yapan kodun dosya betimleyici tablosunu boş değil kısmen dolu olarak çalışması anlamına gelecektir. İşte UNIX/Linux sistemlrinde her betimleyici için "close on exec" isimli bir bayrak tutulmaktadır. Eğer bu bayrak set edilmişse bu durumda exec işlemi sırasında o betimleyici ekrnel taarafından otomatik olarak kapatılmaktadır. Dosyalar açıldığında default durumda betimleyicinin "close on exec" bayrığı "reset" durumdadır. Yani dosya exec işlemleri sırasında kapatılmayacaktır. Betimleyicinin "close on exec" bayrağını set etmek için open fonksiyonunda O_CLOEXEC baurağı kullanılabilir. Ya da fcntl fonksiyonu ile bu işlem yapılabilir. Biz kursumuzda bu konunun ayrıntılarına girmeyeceğiz. * Örnek 1, Aşağıdaki örnekte "redirect" isimli bir program yazılmıştır. Program kabuk programının yaptığı yönlendirmenin benzerini yapmaktadır. Programın komut satırı argümanı kabukta girilen yönlendirme komutunu almaktadır. Örneğin: ./redirect "ls -l -i > test.txt" Programın içerisinde check_arg isimli fonksiyon '>' karakterinden yazıyı iki parçaya ayırmış ve soldaki programın komut satırı argümanlarını da ayrıştırmıştır. Yönlendirmenin aşağıdaki gibi yapıldığına dikkat ediniz: if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { /* child process */ if ((fd = open(rargs.redirect_path, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); if (execvp(rargs.exe_args[0], rargs.exe_args) == -1) exit_sys("execvp"); /* unreacable code */ } Programın kodları ise aşağıdaki gibidir: /* redirect.c */ #include #include #include #include #include #include #include #include #define MAX_ARGS 1024 typedef struct tagREDIRECT_ARGS { char *exe_args[MAX_ARGS]; char *redirect_path; } REDIRECT_ARGS; bool check_arg(char *arg, REDIRECT_ARGS *rargs); void exit_sys(const char *msg); /* ./redirect "./sample > test" */ int main(int argc, char *argv[]) { char *arg; REDIRECT_ARGS rargs; pid_t pid; int fd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((arg = strdup(argv[1])) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if (!check_arg(arg, &rargs)) { fprintf(stderr, "invalid argument: \"%s\"\n", argv[1]); exit(EXIT_FAILURE); } if ((pid = fork()) == -1) exit_sys("fork"); if (pid == 0) { // child process if ((fd = open(rargs.redirect_path, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("open"); if (dup2(fd, 1) == -1) exit_sys("dup2"); close(fd); if (execvp(rargs.exe_args[0], rargs.exe_args) == -1) exit_sys("execvp"); /* unreacable code */ } if (wait(NULL) == -1) exit_sys("wait"); free(arg); return 0; } bool check_arg(char *arg, REDIRECT_ARGS *rargs) { char *str; size_t i = 0; if ((str = strchr(arg, '>')) == NULL || strchr(str + 1, '>') != NULL) return false; *str = '\0'; if ((rargs->redirect_path = strtok(str + 1, " \t")) == NULL) return false; for (str = strtok(arg, " \t"); str != NULL; str = strtok(NULL, " \t")) rargs->exe_args[i++] = str; if (i == 0) return false; rargs->exe_args[i] = NULL; return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } fork işlemi sırasında üst prosesin dosya betimleyici tablosu "sığ kopyalama (shallow copy)" yoluyla alt prosese kopyalanmaktadır. Sığ kopyalama bir nesnenin (yapının) elemanlarının diğerine kopyalanması anlamına gelmektedir. Eğer yapının bir elemanı başka bir nesneyi gösteriyorsa o nesnenin kopyasından çıkartılmamaktadır. Yalnızca ana nesnenin kopyasından çıkartılmaktadır. Dolayısıyşa sığ kopyalama sonucunda iki nesnenin gösterici elemanları aynı nesneyi gösteriyor durumda olur. fork işlemi sırasında alt prosesin kontrol bloğunda yeni bir dosya betimelyici tablosu oluşturulur. Üst prosesin dosya betimleyici tablosundaki dosya nesnelerinin adresleri alt prosesin dosya betimeleyici tablosuna kopyalanır. Böylece prosesle alt proses aynı dosya nesnesini gösteriyor durumda olur. Tabii bu surada dosya nesnelerinin referans sayaçları da 1 artırılmaktadır. fork işlemi sırasında işlemlerin bu biçimde yapıldığını şöyle ispatlayabiliriz. Örneğin dosya göstericisi dosya nesnesinin içerisinde tutulmaktadır. Bu durumda üst proses dosya nesnesini konumlandırırsa alt proses de onu konumlandırılmış görecektir. Aşağıdaki örnekte üst proses açtığı dosyanın dosya göstericisini 10'uncu offset'e konumlandırmıştır. Alt proses o betimelyciden okuma yaptığında 10 numaralı offset'ten itibaren okuma yapmış olacaktır. * Örnek 1, #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; pid_t pid; char buf[10 + 1]; ssize_t result; if ((fd = open("test.txt", O_RDONLY)) == -1) exit_sys("open"); if ((pid = fork()) == -1) exit_sys(""); if (pid != 0) { /* parent process */ lseek(fd, 10, SEEK_CUR); } else { /* child process */ sleep(1); if ((result = read(fd, buf, 10)) == -1) exit_sys("read"); buf[result] = '\0'; puts(buf); close(fd); exit(EXIT_SUCCESS); } if (waitpid(pid, NULL, 0) == -1) exit_sys("waitpid"); close(fd); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } > C'de Değişken Sayıda Argüman Alan Fonksiyonlar: C'de bir fonksiyonun istenildiği kadar çok argümanla çağrılmasını sağlamak için fonksiyon prototoipinde ve/veya tanımlamasında ... (ellipsis) atmonun bulundurulması gerekir. Örneğin: void foo(int a, ...); Burada foo fonksiyonu en azından bir argümanla çağrılmak zorundadır. Ancak istenildiği kadar çok argümanla çağrılabilir. Örneğin: foo(10); /* geçerli */ foo(10, 20); /* geçerli */ foo(10, 20, 30); /* geçerli */ foo(10, 20, 30, 40); /* geçerli */ foo(10, 20, 30, 40, 50); /* geçerli */ Fonksiyonun ... parametresi parametre listesinin sonunda bulunmak zorundadır. Örneğin aşağıdkai fonksiyon bildirimi geçersizdir: void bar(int a, ..., int b); /* geçersiz! ... parametresi parametre listesinin sonunda bulunmak zorunda */ Fonksiyonun ... parametresinden önce en az bir parametresi olmak zorundadır. Örneğin: void tar(...); /* geçersiz! ... parametresinden önce en az bir parametrenin olması gerekirdi */ Değişken sayıda argüman alan fonksiyonlar denildiğinde akla printf, fprintf, scanfi fscanf, sprintf, snprintf gibi fonksiyonlar gelmektedir. Örneğin printf fonksiyonunun prototipi şöyledir: int printf(const char *format, ...); Görüldüğü gibi printf en azından char türden bir adresle çağrılmak zorundadır. Ancak bu argümandna sonra sıfır tane ya da n tane argüman girilebilir. printf fonksiyonu stdout dosyasına yazılan karakter sayısı ile geri dönmektedir. Tabii aslında printf de başarısız olabilir. Bu durumda negatif herhangi bir değere geri döner. Pekiyi değişken sayıda parametre alabilen fonksiyonlar nasıl yazılmaktadır? Bu fonksiyonların yazımındaki temel sorun ... parametresine karşılık gelen argümanların elde edilmesidir. Örneğin: void foo(int a, ...) { /* ... */ } foo fonksiyonunu şöyle çağırmış olalım: foo(10, 20, "ankara", 40.5, 50); Biz fonksiyon içerisinde a parametre değişkeni yoluyla yalnızca 10 değerini elde edebiliriz. Pekiyi ya diğer değerler? İşte ... parametresine karşılık gelen argümanların elde edilmesi için dosyası içerisinde bulunan aşağıdaki makrolar kullanılmaktadır: void va_start(va_list ap, argN); type va_arg(va_list ap, type); void va_end(va_list ap); Bu makrolar va_list isimli bir tür ile çalışmaktadır. Programcı önce va_start makrosunu çağırmalıdır. va_start makrosunun birinci parametresi va_list türünden bir nesne, ikinci parametresiise ... parametresinden bir önceki parametreyi almaktadır. Örneğin: void foo(int a, ...) { va_list va; va_start(va, a); /* ... */ } Programcı argümanlarla işini bitirdikten sonra va_list türünden nesne ile son kez va_end makrosunu çağırmalıdır. void foo(int a, ...) { va_list va; va_start(va, a); /* ... */ va_end(va); } ... parametresine karşı gelen argümanların elde edilmesi için argümanların türlerinin biliniyor olması gerekmektyedir. Argümanları elde eden asıl makro va_arg isimli makrodur. Bu makronun birinci parametresi va_list nesnesini, ikinci parametresi argümanın türünü almaktadır. Bu makro her çağrıldığında bize sırasıyla argümanların değerleri verecektir. Programcı ... parametresi için geçilen argümanaların sayısını da bilmemektedir. Bu sayıyı programcı bir biçimde ... parametresinden önceki parametreler için geçilen argümanlardan elde edebilir. va_arg makrosunun kullanımı şöyledir: arg1 = va_arg(va, int); arg2 = va_arg(va, const char *); ... Programcının va_arg makrosuyla eksik argüman argüman çekmesinde bir sorun yoktur. Ancak fazla sayıda argüman çekildiğinde "tanımsız davranış (undefined behavior)" oluşmakatadır. Bu tür durumlarda bir çökmeyle karşılaşılmayabilir. Ancak çöp değerler elde edilir. * Örnek 1, Aşağıdaki örnekte add isimli fonksiyonun birinci parametresi ... parametresi için girilen argümanların sayısını belirtmektedir. Örnekte tüm argümanların int türden olduğu varsayılmaktadır. Fonksiyonun prototipi şöyledir: int add(int count, ...); Fonksiyon argümanların toplamına geri dönmektedir. #include #include int add(int count, ...) { va_list va; int total, val; va_start(va, count); total = 0; for (int i = 0; i < count; ++i) { val = va_arg(va, int); total += val; } va_end(va); return total; } int main(void) { int total; total = add(5, 10, 20, 30, 40, 50); printf("%d\n", total); return 0; } * Örnek 2, Aşağıda birden fazla yazıyı alt alta yazdıran vputs isimli bir fonksiyon örneği verilmiştir. Fonksiyonun prototip şöyledir: void vputs(const char *str, ...); Fonksiyonu çağıracak kişinin argüman listesinin sonuna NULL adres yerleştirmesi gerekmektedir. Çünkü fonksiyon NULL adres görene kadar va_arg makrosuyla argüman çekmektedir. Örneğin: vputs("ali", "veli", "selami", "ayse", "fatma", (char *)0); execl ve execlp fonksiyonlarının da bu biçimde olduğunu anımsayınız. Burada NULL adres girilirken tür dönüştürmesi yapılmalıdır. Bu tür dönüştürmesinin neden gerektiğini exec fonksiyonlarının l'li versiyonlarında açıklamıştık. Burada bir kez daha açıklamak istiyoruz. C'de argümana karşılık gelen parametre ... ise bu durumda derleyici "default argüman dönüştürmesi (default argument conversion)" denilen bir dönüştürme yapmaktadır. Default argüman dönüştürmesinde int türünden küçük olan türler int türüne (integer promotion), float türü double türüne ve 0 sabiti de int türden 0 olarak fonksiyona gönderilmektyedir. Yani biz argümanda düz 0 kullanırsak bu artık NULL adres anlamına gelmez. Tabii sistemlerin hemen hepsinde NULL adres zaten tüm bitleri 0 olan adrestir. Ancak int türü ile adres türlerinin farklı uzunluklarda olduğu 64 bit sistemlerde bu durumun soruna yol açma olasılığı yüksek olmaktadır. #include #include void vputs(const char *str, ...) { va_list va; const char *arg; va_start(va, str); arg = str; for (;;) { if (arg == NULL) break; puts(arg); arg = va_arg(va, const char *); } va_end(va); } int main(void) { vputs("ali", "veli", "selami", (char *)NULL); return 0; } * Örnek 3, Aşağıda printf fonksiyonun nasıl yazılmış olabileceğine ilişkin bir ipucu vermek verilmiştir. Buradaki myprintf fonksiyonunda '%' karakteri olmadığı sürece ilerlenmiş ve o karakterler stdout dosyasına yazdırılmıştır. '%' karakteri görüldüğünde onun yanındaki karaktere bakılıp hangi türden argüman çekileceğine karar verilmiştir. Tabii orijinal printf fonksiyonu stdout dosyasının tamponuna yazmaktadır. Biz bu örneğimizde putchar fonksiyonu ile yazdırma kısmını yaptık. Bu tür durumlarda elde genellikle bir karakterin ekrana yazdırılması için temel bir fonksiyon zaten bulunmaktadır. Tabii putchar fonksiyonu zaten standart C fonksiyonu olduğu için örneğimizde tampona yazmaktadır. Zaten genel olarak standart C'deki kütüphaneleri yazılırken önce tamponlu çalışacak tek bir karakteri yazan fonksiyon oluşturulur (bizim örneğimizde bu putchar) sonra o fonksiyon kullanılarak diğer fonksiyonlar yazılır. #include #include #include void disp_int(int val) { if (val < 0) { putchar('-'); val = -val; } if (val / 10) disp_int(val / 10); putchar(val % 10 + '0'); } int myprintf(const char *format, ...) { va_list va; int total; int val_int; const char *val_str; va_start(va, format); total = 0; while (*format != '\0') { if (*format == '%') { switch (*++format) { case 'd': val_int = va_arg(va, int); disp_int(val_int); if (val_int < 0) { val_int = -val_int; ++total; } total += log10(val_int) + 1; break; case 'c': putchar(va_arg(va, int)); ++total; break; case 's': val_str = va_arg(va, const char *); while (*val_str != '\0') { putchar(*val_str++); ++total; } break; /* ... */ } } else { putchar(*format); ++total; } ++format; } va_end(va); return total; } int main(void) { int a = -10; char c = 'x'; char s[] = "ankara"; int result; result = myprintf("a = %d, ch = %c, s = %s\n", a, c, s); myprintf("%d\n", result); return 0; } C'nin "stdio" kütüphanesinde printf ailesi fonksiyonların va_list parametreli başı "v" ile başlayan versiyonları vardır. Bu sayede biz bu printf ailesi fonksiyonları sarmalayabilen fonksiyonlar yazabiliriz. Örneğin printf fonksiyonun v'li versiyonun ismi vprintf, fprintf fonksiyonunun v'li versiyonun ismi vfprintf fonksiyonudur. Bu fonksiyonların listesi şöyledir: printf ===> vprintf scanf ===> vscanf fprintf ===> vfprintf fscanf ===> vfscanf sprintf ===> vsprintf snprintf ===> vsnprintf sscanf ===> vsscanf Bu fonksiyonların v'li versiyonları v'siz versiyonlarından bir parametre daha fazla parametreye sahiptir. Bu fazla parametre son parametredir ve va_list türündendir. Örneğin vprintf fonksiyonunun parametrik yapısı ile printf fonksiyonunun parametrik yapısını karşılaştırınız: int printf(const char *format, ...); int vprintf(const char *format, va_list ap); printf fonksiyonunu sarmalayan bir fonksiyon vprintf kullanılarak şöyle yazılabilir: int wrapper_printf(const char *format, ...) { va_list va; int result; va_start(va, format); result = vprintf(format, va); va_end(va); return result; } Aslında burada yapılan şey "..." parametresi ile alınan bütün argümanları doğrudan vprintf fonksiyonuna geçirmektir. Aşağıda buna bir örnek verilmiştir. * Örnek 1, #include #include #include int wrapper_printf(const char *format, ...) { va_list va; int result; va_start(va, format); result = vprintf(format, va); va_end(va); return result; } int main(void) { int a = 10; double b = 3.14; wrapper_printf("%d, %f\n", a, b); return 0; } printf ailesi fonksiyonların sarmalanmak istenmesinin en önemli nedeni araya girip bir şeyler yapmaktır. Örneğin biz UNIX/Linux sistemlerinde bir hata olduğunda errno değişkenine karşı gelen yazıyı stderr dosyasına yazdırarak programı sonlandıran aşağıdaki gibi bir fonksiyon kullanıyorduk: void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Burada perror fonksiyonu önce yazıyı sonra ':' karakterini ve sonra da bir boşluk bırakıp errno değerine karşı gelen yazıyı stderr dosyasına yazdırmaktadır. Nihayetinde program exit fonksiyonuyla sonlandırılmıştır. Biz burada bu fonksiyondan daha yetenekli olan printf gibi kullanılan bir fonksiyonu vprintf fonksiyonunu sarmalayacak biçimde de yazabiliriz. Örneğin: void exit_vsys(const char *format, ...) { va_list va; va_start(va, format); vfprintf(stderr, format, va); fprintf(stderr, ": %s\n", strerror(errno)); va_end(va); exit(EXIT_FAILURE); } Burada önce vfprintf fonksiyonu ile printf gibi bilgiler stderr dosyasına yazdırılmış sonra ':' karamteri ve boşluk karakterinden sonra errno değerinin yazısı yazdırılmıştır. Fonksiyon bu haliyle daha yenekli hale gelmiştir. Örneğin artık biz fonksiyonu şöyle kullanabiliriz: if ((fd = open(argv[1], O_RDONLY)) == -1) exit_vsys("%s cannot open", argv[1]); Hata durumunda stderr dosyasına aşağıdaki gibi bir yazı basılacaktır: xxx cannot open: No such file or directory Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include #include #include #include #include void exit_vsys(const char *format, ...); int main(int argc, char *argv[]) { int fd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDONLY)) == -1) exit_vsys("%s cannot open", argv[1]); close(fd) ; return 0; } void exit_vsys(const char *format, ...) { va_list va; va_start(va, format); vfprintf(stderr, format, va); fprintf(stderr, ": %s\n", strerror(errno)); va_end(va); exit(EXIT_FAILURE); } * Örnek 2, Aşağıdaki örnekte exit_vsys fonksiyonunun tanımlaması "libsys.c" isimli dosyaya, prototipi de "libsys.h" isimli dosyaya yerleştirilmiştir. Bu dosya bir kez derlenip aşağıdaki gibi link aşamasına dahil edilebilir: gcc -c libsys.c gcc -Wall -o sample sample.c libsys.o Aşağıda buna ilişkin program kodları verilmiştir: /* sample.c */ #include #include #include #include #include "libsys.h" int main(int argc, char *argv[]) { int fd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDONLY)) == -1) exit_vsys("%s cannot open", argv[1]); close(fd); return 0; } /* libsys.c */ #include #include #include #include #include void exit_vsys(const char *format, ...) { va_list va; va_start(va, format); vfprintf(stderr, format, va); fprintf(stderr, ": %s\n", strerror(errno)); va_end(va); exit(EXIT_FAILURE); } /* libsys.h */ #ifndef LIBSYS_H_ #define LIBSYS_H_ /* Function Prototypes */ void exit_vsys(const char *format, ...); #endif > İşlemcilerin Sayfalama Mekanizması: Masaüstü (desktop) işletim sistemlerinin çalıştığı kapasiteli işlemlerin "sayflama (paging)" denilen bir mekanizması vardır. Sistem programcılarının bu sayfalama mekanizmasını temel düzeyde biliyor olmaları gerekir. Sayfalama mekanizması "sanal bellek (virtual memory)" denilen bellek yönetim tekniği için de bir araç durumundadır. Intel işlemcileri 80386 ile birlikte (ilk kez 1985) sayfalama mekanizmasına sahip olmuştur. ARM işlemcilerinin A'lı (Application) serisi cortex'leri bu mekanizmaya sahiptir. PwerPC; Itanimum, Alpha gibi yaygın işlemlerin de sayfalama mekanizması bulunmaktadır. Ancak düşük kapasiteli işlemcilerde ve mikrodenetleyicilerde genel olarak bu mekanizma yoktur. Örneğin ARM işlemcilerinin M'li (Microcontoller) serisi genel olarak sayfalama mekanizmasına sahip değildir. Şimdi de adım adım bu konu hakkındaki detayları inceleyelim: >> Sayfa (page) bellekteki ardışıl byte topluluklarına denilmektedir. Sayfalama mekanizmasında işlemci RAM'e byte düzeyinde erişebilir. Ancak aynı zamanda RAM'i sayfalardan (bloklardan) oluşan bir birim gibi ele almaktadır. Sayfa büyüklükleri farklı işlemcilerde farklı olabilmektedir. Bazı işlemciler farklı büyüklükteki sayfaları destekleyebilmektedir. Ancak işlemcilerin desteklediği en yaygın kullanılan sayfa büyüklüğü 4K'dır. * Örnek 1, Windows, Linux, macOS sistemleri işlemcinin 4K sayfalama mekanizmasını kullanmaktadır. Bu nedenle biz kursumuzda sayfa denildiğinde onun büyüklüğünün default olarak 4K olduğunu varsayacağız. >> İşlemciler RAM'in her sayfasına ilk sayfa 0 olmak üzere bir numara verirler. Örneğin RAM'in tepesindeki ilk 4096 byte 0'ıncı sayfa, sınraki 4096 byte 1'inci sayfadır ve bu böyle devam etmektedir. Bir adres aslında RAM'de belirli bir sayfanın belirli bir offset'indedir. Burada "offset" demekle ilgili sayfanın başından itibaren olan uzaklığı kastediyoruz. Sayfa uzunluğunun 4K olduğunu varsayalım (gerçekte uygulanan tipik durum).İşlemcinin belli bir adresin hangi sayfada o sayfanın hangi offset'inde olduğunu anlaması oldukça kolaydır. Adresin sağdaki 12 biti sayfa offsetini verir. Adres değeri 12 kere sağa ötelenirse bu da adresin sayfa numarasını verecektir. Örneğin 3F7C42 biçiminde bir adres olsun. Bu adres RAM'in 3F7'inci sayfasındadır ve bu sayfanın C42'inci offset'indedir: 3F7 ==> sayfa numarası C42 ==> Sayfa offset'i >> Sayfalama mekanizmasını kullanan işletim sistemleri programları ardşıl bir biçimde RAM'e yüklememektedir. Program işletim sistemi tarafından sayfa büyüklüklerine göre parçalara ayrılır, parçalar boş sayfalara yüklenir. * Örnek 1, elimizde 20K'lık bir program olsun. Eğer sistemde sayfalama mekanizması olmasaydı bu 20K ardışıl bir biçimde RAM'e yüklenecekti. Ancak sayfalama mekanizması söz konusu olduğunda bu 20K'lık program 4K'lık 5 parçaya ayrılır ve bu 5 parça ardışıl olmayabilen 5 boş syfaya yüklenir. Böylece 20K'lık programın 4'er K'lık parçaları farklı sayfaalarda bulunuyor olacaktır. Örneğin fiziksel belleğin belli bir bölümünün sayfaları aşağıdaki gibi olsun: Sayfa Numarası Sayfanın Durumu ... ... 3FC5 3FC6 3FC7 3FC8 3FC9 3FCA 3FCB 3FCC 3FCD 3FCE 3FCF 3FD0 ... .... Şimdi işletim sistemi programın sayfalarını boş sayfalara yğkleyecektir. Şöyle bir durum oluşabilecektir: Sayfa Numarası Sayfanın Durumu ... ... 3FC5 3FC6 Programın 1'inci Parçası 3FC7 3FC8 3FC9 Programın 2'inci Parçası 3FCA Programın 3'üncü Parçası 3FCB 3FCC 3FCD Programın 4'üncü Parçası 3FCE 3FCF Programın 5'inci Parçası 3FD0 ... .... Burada iki soru karşımıza çıakacaktır: -> Birincisi programın ardışıl yüklenmemesinin avantajı nedir? -> İkincisi de programın hangi parçasının hangi sayfalarda olduğunu işletim sistemi nasıl bilmektedir? Sayfalama mekanizmasına sahip işlemciler çalışırken ismine "sayfa tablosu (page table)" denilen bir tabloya balarak çalışırlar. Sayfa tablosu işletim sistemi tarafından her proses için ayruı bir biçimde oluşturulur ve Proses Kontrol Bloğunda tutulur. İşlemciler sayfa tablolarını belli bir yazmacın gösteridği yerde ararlar. Böylece işletim sistemi prosesin sayfa tablosunu oluşturur. İşlemcinin ilgili yazmacına sayfa tablosunun adresini yerleştirir. İşlemci de o sayfa tablosunu kullanır. >> Bugünkü işletim sistemleri daha önceden de belirttiğimiz gibi thread temelinde preemptive zaman paylaşımlı sistemlerdir. Bir thread işlemciye atanır. Belli bir süre çalıştırılır, sonra çalışmasına ara verilip diğer bir thread işlemciye atanır. Eğer threadler arası geçiş (context switch) sırasında ara verilen thread ile geçilen thread farklı proseslerin thread'leri ise işletim sistemi işlemcinin ilgili yazmacındaki değeri değiştirerek işlemcinin yeni geçilen thread'in ilişkin olduğu prosesin sayfa tablosunu kullanmasını sağlar. Yani belli bir anda hangi program çalışıyorsa işlemci onun sayfa tablosunu kullanıyor durumda olur. Intel İşlemcilerinde sayfa tablosunun yeri CR3 yazmacıyla belirlenmektedir. Sayfa tablosu aslında sanal sayfaları fiziksel sayfalara eşleyen bir tablodur. Sayfa tablosunun kaba biçimi aşağıdaki gibidir: Sanal Sayfa No Fiziksel Sayfa No ... ... 4000 3FC6 4001 3FC9 4002 3FCA 4003 3FCB 4004 3FCF ... .... Buradan hareketle, * Örnek 1, 32 bitlik bir sistemde derleyici sanki program 4GB'lik boş bir RAM'e tek başına yüklenecekmiş gibi kod üretmektedir. Yani 32 bit Windows sistemlerinde derleyici ve linker sanki program boş bir 4GB'lik belleğin 4MB'sinden itibaren yüklenecekmiş gibi kod üretir. İşletim sistemi programı 4K'lık sayfalara ayırıp anları o parçaları boş sayfalara yerleştirmektedir. Böylece programın sanal adres alanı ardışılmış gibi bir durum oluşturulur. İşlemci sayfa tablosuna bakarak çalıştığı için bir sorun çıkmamaktadır. İşlemci karşılaştığı her adresi "sayfa numarası" ve "sayfa offset'i" biçiminde iki parçaya ayırmakta, sonra sayfa numarasını sayfa tablosundan fiziksel sayfa numarasına dönüştürüp aslında o fiziksel sayfada ilgili offset'e erişmektedir. Örneğin işlemci aşağıdaki gibi bir makine komutu ile karşılaşmış olsun: MOV EAX, [4003F12] Burada 4003F12 adresi 4003 sayfa numarasının F12 offset'ini belirtmektedir. İşlemci sayfa tablosunda 4003 numaralı sayfanın 3FCB numaralı fiziksel sayfayla eşleştirildiğini görür aslında fiziksel bellekte 3FCBF12 adresine erişir. >> Program içerisindeki bütün adresler gerçek fiziksel adresler değildir. Bunlara "sanal adres (virtaul address)" ya da "doğrusal adres (linear address)" denilmektedir. İşlemci sanal adresleri fiziksel adreslere dönüştürüp fiziksel RAM'de gerçek yere erişmektedir. Prosesin sanal adres alanının ardışıl olduğuna ancak fiziksel adres alanının ardışıl olmadığına dikkat ediniz. Bu durumda iki programdaki aynı sanal adresler aslında aynı fiziksel adres belirtmemektedir. Çünkü işletim sistemi her prosesin sayfa tablosunda sanal adresleri farklı fiziksel sayfalara yönlendirmektedir. * Örnek 1, aynı programı ikinci kez çalıştırdığımızda aslında program içerisindeki sanal adresler değişmez. Çünkü her program sanal belleğe onun belli bir noktasından itibaren tek başına yüklenecekmiş gibi koda sahiptir. Aşağıdaki örnekte 20K uzunluğunda bir programın ikinci kez çalıştırımasındaki sayfat atblosu temsili olarak verilmiştir. Sanal Sayfa No Fiziksel Sayfa No ... ... 4000 3FC6 4001 3FC9 4002 3FCA 4003 3FCB 4004 3FCF ... .... Sanal Sayfa No Fiziksel Sayfa No ... ... 4000 4F12 4001 7B14 4002 F12A 4003 47C8 4004 16CB ... .... Burada örneğin 40013FC adresi her iki proseste aynı sanal adres olsa da farklı fiziksel adres belirtmektedir. >> Sayfalama mekanizmasının kullanıldığı sistemlerde işletim sistemi zaten prosesleri sayfa tabloları yoluyla izole etmektedir. Yani bir iki prosesin sayfa tablolarındaki fiziksel sayfa adresleri zaten farklıdır. Bu durumda bir proses diğerinin fiziksel sayfalarına zaten erişemez. Sayfalama "sanal bellek (virtual memory)" denilen bellek yönetim tekniğini uygulanması için gerekli bir mekanizmadır. Sanal bellek bir programın tamamının değil yalnızca belli sayfalarının RAM'e yüklenip disk ile RAM arasında yer değiştirmeli biçimde çalıştırılması anlamına gelmektedir. Bugün Windows, Linux, macOS gibi işletim sistemleri sanal bellek mekanizmasını kullanmaktadır. Sanal bellek mekanizmasında işletim sistemi prosesin tüm sayfalarını fiziksel RAM'e yüklemez. Yalnızca bazı sayfalarını yükleyerek programı çalıştırır. Yani sayfa tabşlosunda bazı sanal sayfalara fiziksel bir sayfa karşılık düşürülmemiştir. * Örnek 1, Sanal Sayfa No Fiziksel Sayfa No ... ... 4000 3FC6 4001 - 4002 3FCA 4003 - 4004 3FCF ... .... Buradaki '-' karakterleri ilgili sanal sayfa için bir fiziksel sayfanın karşı düşürülmediğ anlamına gelmektedir. Pekiyi işlemci böyle bir durumla karşılaştığında ne yapmaktadır? * Örnek 1, işlemci aşağıdaki gibi bir makine komutuyla karşılaşsın: MOV EAX, [400117C] Bu makine komutunda belleğin 400117C adresindeki 4 byte CPU yazmacına çekilmek istenmektedir. Komuttaki adresin sanal sayfa numarası 4001'dir. İşlemci sayfa tablosuna başvurup bu sanal sayfa için bir fiziksel sayfanın karşı gelmediğini gördüğünde bir "içsel kesme (internal interrupt)" oluşturmaktadır. Bu içsel kesmeye "page fault" da denilmektedir. >> Page fault oluşturğunda işlemci sistem programcısının belirlediği bir kodu çalıştırır. Bu koda "page fault handler" denilmektedir. Page fault handler işletim sistemini yazanlar tarafından yazılmıştır. Böylece page fault oluştuğunda aslında otomatik olarak işletim sisteminin bir kodu devreye girmektedir. İşletim sisteminin bu page fault kodu önce buna yol açan adresi incelemektedir. Bu kod söz konusu adresteki adresteki sanal sayfa numarısına bakar. Bunun programın neresine (yani hangi 4K'lık kısmına) karşı geldiğini tespit eder. Programın o 4K'lık kısmını diskten alarak boş bir sayfaya yükler. Sonra sayfa tablosunu düzeltir. Artık sayfa tablosunda ilgili sayfaya karşı bir fiziksel sayfa bulunmaktadır. Sonra kesme kodunu sonlandırır. Page fault kodunun çalışması bittiğinde işlemci bu fault'a yol açan makine komutuyla çalışmasına devam eder. Dolayısıyla artık ilgili adresin sanal sayfa numarası fiziksel bir sayfa numarasına yönlendirilmiş durumdadır. Kullanıcılar programın kesiksiz çalıştığını sanmaktadır. Aslında program ilgili sayfaların gerektiğinde diskten RAM'e yüklenmesiyle çalışmaktadır. (Örneğin diskinizin ışığına bakarsanız onun sürekli yanıp sçmdüğünü fark edersiniz. Çünkü programlar çalışırken sürekli "page fault" oluşmaktadır.) * Örnek 1, Yukarıdaki örneğimizde 400117C adresine erişldiğinde oluşan page fault'ta işletim sisteminin programın ilgili 4K'lık parçasını RAM'de boş olan 418F numaralı fiziksel sayfaya yüklediğini düşünelim. Artık sayfa tablosunun ilgili kısmı şöyle olacaktır: Sanal Sayfa No Fiziksel Sayfa No ... ... 4000 3FC6 4001 418F 4002 3FCA 4003 - 4004 3FCF ... .... Pekiyi page fault oluştuğunda fiziksel RAM'deki tüm sayfalar doluysa ne olacaktır? Bu durumda işletim sistemi dolu olan bir sayfayı fiziksel RAM'den atacak ve programa ilişkin 4K'lık kısmı bu bu fiziksel sayfaya yükleyecektir. Tabii işletim sistemi "ileride en az kullanılabilecek" bir sayfayı RAM'den atmaya çalışmaktadır. Bunun için kullanılan algprtimalara "page replacement" algoritmaları denilmektedir. En çok tercih edilen "page replacment" algoritması "LRU (Least Recently Used)" algoritmasıdır. Bit fiziksel sayfanın RAM'den atılmasına İngilizce "swap-out", diskteki bir sayfanın RAM'e çekilmesine ise "swap-in" denilmektedir. Disk ile RAM arasındaki bu tür değiştirmelere ise "swapping" ismi verilmektedir. Pekiyi swap-out işlemi yapılacakken ya RAM'deki sayfanın içeriği değişmişse ne olacaktır? Eğer RAM'deki sayfanın içeriği değişmemişse ve bu sayfa çalıştırılabilir dosyanın (executable file) içerisinde zaten varsa o sayfa doğrudan RAM'den atılabilir. Çünkü gerektiğinde yine çalıştırılabilir dosyanın içerisinde alınabilecektir. Ancak eğere RAM'deki sayfa değişmişse (buna "dirty" hale gelmek de denilmektedir) bu durumda o sayfa daha sonra yeniden swap-in yapılabileceği için sayfanın diskte saklanması gerekir. İşte güncellenmiş olan (kirlenmiş olan) bu tür sayfaların diskte saklanabilmesi için işletim sistemleri "swap dosyası (swap files)" oluşturmaktadır. Tabii işletim sistemlerinin oluşturduğu swap dosyalarının da bir büyüklüğü vardır. Eğer o büyüklük aşılırsa sanal belleğin de limitine ulaşılmış olur. O halde kabaca aslında sanal belleğin toplam büyüklüğü kabaca "fiziksel RAM + swap dosyası" kadardır. Swap dosyaları genellikle işletim sisteminin çalışma zamanı sırasında dinamik olarak büyütülmezler. Bunlar için genellikle baştan bir yer ayrılmaktadır. Bu yerin büyüklüğünü sistem yöneticisi belirleyebilmektedir. * Örnek 1, Windows sistemlerinde "Denetim Masası/System/Advanced System Settings/Performance/Advanced/Virtual Memory" penceresinden değiştirilebilmektedir. * Örnek 2, Linux sistemleri bu bakımdan da esnektir. Genellikle Linux sistemlerinde swap dosyası bir "disk bölümü (disk paritition)" olarak ayrılır. Ancak sistem yöneticisi isterse disk dosyası yaratıp onu da swap alanına dahil edebilmektedir. Bu sistemlerde "free" komutu ile ya da "swapon -s" komutu ile swap alanlarının listesi alınabilmektedir. Linux'ta bir dosyanın swap alanına dahil edilmesi için ne yapılması gerektiğine ilgili dokümanlardan erişebilirsiniz. >> Pekiyi sanal bellek kullanan sistemlerde dinamik bellek tahsisatı nasıl yapılmaktadır? Dinamik alan çalıştırılabilir dosyanın içerisinde olmadığına göre ve bunun için swap dosyası içerisinde bir yer ayrılmalıdır. malloc gibi bir fonksiyonu çağırdığımızda bu fonksiyon swap dosyası içerisinde sayfalar için yer ayırmakla birlikte gerçek fiziksel RAM'de henüz bir tahsisat yapmamaktadır. Dinamik tahsis edilen alanın sayfalarına erişildiğinde swap-in yapılmaktadır. Biz eldeki fiziksel RAM'im ötesinde dinamik tahsisatlar yapabiliriz. Ancak tabii swap dosyalarımızın toplam uzunluğu da bizim için bir limit oluşturmaktadır. Pekiyi işletim sistemi bir sayfanın güncellendiğini (kirlendiğini) nasıl anlamaktadır? İşte aslında sayfa tablosunda her sayfanın bilgilerinin tutulduğu bir yer de vardır. Yani sayfa tablosunun organizasyonu aşağıdaki gibidir: Sanal Sayfa No Fiziksel Sayfa No Sayfa Özellikleri ... ... ... 4000 3FC1 read-write-user 4001 1FC2 read-kernel 4002 4D02 read-write-user ... ... ... >> İşlemci ne zaman bir sayfaya erişse o sayfanın sayfa tablosundaki "D (Dirty)" bitini set etmektedir. İşletim sistemi de baştan bu reset eder sonra swap-out yapacağı zaman bu bite bakar. eğer bu bit set edilmişse sayfanın güncellenmiş olduğunu düşünür. Sayfalara "read only" ya da "read/write" biçiminde özellikler verilebilmektedir. "read-only" bir sayfaya yazma yapıldığında "page fault" oluşmaktadır. Böyle bir durumda işletim sistemi prosesi cezalandırıp sonlandırmaktadır. * Örnek 1, C derleyicileri string ifadelerini "read-only" section'lara yerleştirmektedir. İşletim sistemi de bu sectionlar'ı RAM'e sayfa sayfa yüklemektedir. String'lerin bulunduğu bu sayfaların sayfa özelliklerini "read-only" yapmaktadır. Dolayısıyla Windows sistemlerinde, Linux ve macOS sistemlerinde bir string'in herhangi bir karakterini değiştirmek istediğimizde page fault oluşmakta bunun sonucu olarak da prosesimiz işletim sistemi tarafından sonlandırılmaktadır. Zaten C'de bir string'in karakterlerini değiştirmek "tanımsız davranışa" yol açmaktadır. >> Sayfaların diğer bir özelliği de "user mode/kernel mode" özelliğidir. Bir sayfa "user mode" özelliğindeyse o sayfaya user modda çalışan prosesler ve kernel modda çalışan (örneğin işletim sistemi) prosesler erişebilir. Ancak sayfa eğer kernel modda ise o sayfaya yalnızca kernel modda çalışan prosesler erişebilir. User modda çalışan prosesler kernel mod özelliğine sahip sayfalara erişmek istediğinde page fault oluşmaktadır. İşletim sistemi de bu prosesleri cezalandırarak sonlandırmaktadır. >> Bir proses çalışırken işletim sisteminin kodları da sayfa tablosunda fiziksel sayfalara yönlendirilmiş durumdadır. Çünkü proses sistem fonksiyonlarını çağırdığında akışın işletim sisteminin içerisine girip o sistem fonksiyonun kodlarını çalıştırabilmesi için işletim sisteminin kodlarının bulunduğu sayfalarında sayfa tablosunda bulunuyor olması gerekir. İşletim sistemi kendisini user mode proseslerden korumak için kendi kodlarının bulunduğu sayfaları kernel mode sayfa olarak özelliklendirir. Böylece user mode programlar kernel mode sayafalara erişmek istediğinde page fault oluşmakta ve proses sonlandırılmaktadır. Sistem fonksiyonları çağrıldığında prosesin çalışma modunun user mode'dan kernel mode'a geçtiğini anımsayınız. Pekiyi işletim sistemi farklı iki prosesin farklı sanal sayfalarını aynı fiziksel sayfaya yönlendirirse ne olur? * Örnek 1, Aşağıda iki prosesin sayfa tablosuna temsil bir örnek verilmiştir: Sanal Sayfa No Fiziksel Sayfa No ... ... 4000 3FC6 4001 56F1 4002 3FCA 4003 26C3 4004 3FCF ... .... Sanal Sayfa No Fiziksel Sayfa No ... ... 4000 246C 4001 17F3 4002 5421 4003 5421 4004 3FC6 ... .... Burada birinci prosesin 4000 numaralı sanal sayfası ve ikinci prosesin 4004 numaralı sanal sayfası 3FC6 fiziksel sayfsına yönlendirilmiştir. O halde birinci proses 4000000-400FFF sanal adreslerine bir şey yazdığında diğer proses bu yazılanları 4004000-4004FFF sanal adreslerinden okuyabilecektir. Yani iki prosesin farklı sanal sayfaları aslında aynı fiziksel sayfayı görmektedir. Bu biçimdeki proseslerarası haberleşme yöntemine "shared memory" denilmektedir. Şimdi aynı programı ikinci kez çalıştırdığımızı düşünelim. Bu durumda aslında yanı kodlar farklı bir proses olarak kullanılacaktır. Bu tür durumlarda işletim sistemi başlangıçta mümkün olduğunca ikinci kopyası çalıştırılan prosesin sayfa tablosundaki girişleri birinci kopyanın kullandığı fiziksel sayfalara yönlendirmektedir. Yani işletim sistemi iki prosesin sayfa tablosundaki fiziksel sayfa numaralarını mümkün olduğunca aynı yapar. Tabii bu prosesler biribirinden farklı olduğuna göre proseslerden birinin fiziksel sayfaya yazma yaptığında diğerinin bu yazmadan etkilenmemesi gerekir. Pekiyi bu nasıl sağlanacaktır? İşte işletim sistemleri başlangıçta aynı fiziksel sayfaları kullanan proseslerin birinin bu fiziksel sayfaya yazma yaptığı zaman bu fiziksel sayfanın kopyasını çıkarmaktadır. Yani iki proses (daha fazla da olabilir) başlangıçta aynı fiziksel sayfayı kullanıyor olsa da bir yazma olayı gerçekleştiğinde artık bu fiziksel sayfalar birbirinden ayrılmaktadır. Bu mekanizmaya işletim sistemlerinde "copy on write" denilmektedir. >> Pekiyi "copy on write" mekanizmasını işletim sistemi nasıl gerçekleştirmektedir. Genellikle kullanılan yöntem şöyledir: İşletim sistemi aslında read-write özelliğe sahip olan sayfalara "copy on write" uygulayabilmek için "read-ony" özellik atamaktadır. Bu sayfaya proseslerden biri yazma yapmaya çalıştığında "page fault" oluşmakta ve işletim sistemi devreye girip sayfanın kopyasını çıkartmaktadır. Tabii artık işletim sistemi kopyası çıkartılan sayfanın özelliklerini "read-write" olarak değiştirecektir. * Örnek 1, UNIX/Linux sistemlerinde fork işlemi sırasında aslında baştan tüm prosesin bellek alanının gerçek bir kopyası oluşturulmamaktadır. Alt prosesin sayfa tablosunun içeriği üzt prosesteki gibi yapılmaktadır. Ancak alt ya da üst proseslerden biri bu sayfalardan birine yazma yaptığında o sayfanın kopyasından çıkarılmaktadır. Bu nedenle fork sonrasında alt proseste exec işlemi yapıldığında aslında bunun bellek kopyalama maliyeti oluşmamaktadır. Zaten artık exec öncesinde cfork kullanılmamasının bir nedeni de budur. > Hatırlatıcı Notlar: >> Aygıt sürücüler kernel modda çalışan modüllerdir. Bir aygıt sürücüyü yazan programcı "read ile okuma yapıldığında şu fonksiyonum çalışsın, write ile yazma yapıldığında şu fonksiyonu çalışsın, lseek ile konumlandırma yapıldığında şu fonksiyonun çalışsın, close ile kapatma yapıldığında şu fonksiyonu çalışsın" biçiminde yazar. Aygıt sürücüler dosya gibi açılırlar. Bir aygıt sürücü açıldığında onun dosya nesnesinde artık okuma, yazma, konumlandırma kapatma dosya göstericileri o aygıt sürücünün içerisindeki fonksiyonları göstermiş olur. Başka bir deyişle aslında biz bir aygıt sürücüden okuma ve yazma yaptığımızda o aygıt sürücüdeki ilgili fonksiyonları çağırmış oluyoruz. Örneğin 1 numaralı betimelyici ekranı kontrol eden terminal aygıt sürücüsü ile ilgilidir. Biz write(1, .....) çağrısı yaptığımızda aslında terminal aygıt sürücüsü içerisindeki bir fonksiyonu çağırmış oluruz. O fonksiyon yazıyı ekrana yazar. Aygıt sürücü içerisinde okuma, yazma, konumlandırma ve kapatma işlemlerinin dışında da yararlı başka fonksiyonlar bulunabilmektedir. Aygıt sürücüleri yazanlar bunların da user mode programlar tarafından çağrılmasını mümkün hale getirmişlerdir. Bunun için UNUX/Linux sistemlerinde ioctl POSIX fonsiyonu, Windows sistemlerinde de DeviceIOControl isimli API fonksiyonu bulunmaktadır. Yani yalnızca aygıt sürücülerdeki okuma yazma fonksiyonları değil diğer bazı fonksiyonlar da user mode programlar tarafından çağrılabilmektedir. >> Biz daha önce standart C fonksiyonlarının bir tampon (cache) kullandığını belirtmiştik. Aslında UNIX/Linux sistemlerinde tüm dosya işlemlerinin wninde sonında read ve write POSIX fonksiyonlarıyla yapıldığını biliyorsunuz. Bu POSIX fonksiyonları da zaten ilgili sistemdeki sistem fonksiyonlarını çağırmaktadır. O halde örneğin C'deki stdout dosyasına yazma yapan fonksiyonlar eninde sonunda write fonksiyonunu 1 numaralı betimelyici ile çağırarak bu işlemi yapacaklardır. Benzer biçimde stdin dosyasından okuma yapan C fonksiyonları da aslında read fonksiyonunu 0 numaralı betimelyici ile çağırarak okumayı yapmaktadır. >> 32 bit Windows sistemleri 32 bit işlemciler için yazılmıştır. Bu işlemcilerin toplam adresleyebildiği fiziksel RAM 4GB'dir. Dolayısıyla bu sistemlerde zaten sayfa tabloları 4GB'lik bir fiziksel alanı haritalandırmaktadır. 32 bit Windows sistemlerinde her proses sanki 4GB'lik bir sanal belleğe tek başına yükleniyormuş gibi sanal bellek kullanmaktadır. Ancak Windows 4GB alanın 2GB'sini user için 2GB'sini de kernel için ayırmıştır. Bu durumda 32 bit Windows sistemlerinde normal bir prosesin kullanabileceği maksimum sanal bellek 2GB'dir. 64 bit Windows sistemleri 64 bit işlemciler için yazılmıştır. Bu işlemciler teorik olarak 16EB belleği adresleyebilmektedir. 64 bit Windows sistemleri eskiden yalnızca belleğin ilk 16TB'sini kullanıyordu. Ancak Windows 10 ile birlikte bu sistemlerde 128TB alan user prosesler için 128TB alan da kernel tarafından kullanılmaktadır. Bu konun ayrıntıları vardır. 32 bit Linux sistemlerinde sanal bellek alanının ilk 3GB'si user prosesler tarafından kalan 1GB'si kernel tarafından ayrılmıştır. 64 bit Linux sistemlerinde ise user alanı ve kernel alanı 128TB kadar olabilmektedir. İşlemci destekliyor olsa da işletim sistemleri gereksiz büyüklükleri onları kontrol etmek için alan gerektiği için desteklememektedir. /*================================================================================================================================*/ (51_23_12_2023) & (52_24_12_2023) & (53_06_01_2024) & (54_07_01_2024) & (55_13_01_2024) > Prosesler Arası Haberleşme Yöntemleri: Bir prosesin diğer bir prosese byte düzeyinde bir bilgi göndermesine v o prosesin de bu bilgiyi almasına "proseslerarası haberleşme (inteprocess communication ya da IOPC) denilmektedir. Proseslerarsı haberleşme sistem programlamanın önemli konularındandır. Modern işletim sistemlerinde proseslerin bellek alanları sayfa tabloları yoluyla biribirinden izole edildiği için bir prosesin diğerine bilgi gönderip ondan bilgi alması ancak özel birtakım yöntemlerle gerçekleştirilmektedir. Proseslerarası haberleşme kendi içinde iki ana bölüme ayrılabilir: -> Aynı makinenin prosesleri arasında haberleşme -> Farklı makinelerin prosesleri arasında haberleşme Aynı makinenin prosesleri arasındaki haberleşmelerde farklı işletim sistemlerinde benzer mekanizmalar geliştirilmiştir. Farklı makinelerin prosesleri arasındaki haberleşmenin farklı unsurları da bulunmaktadır. İki makinedeki farklı proseslerin haberleşebilmesi için bir haberleşme ortamının oluşturulmuş olması gerekir. Genel olarak haberleşmede uyulması gereken kurallar topluluğuna "protokol (protocol)" denilmektedir. Farklı maikenelerin prosesleri arasındaki haberleşmeler iyi tanıomlanmış protokoller yoluyla yapılmaktadır. Bunun için çeşitli protokol aileleri geliştirilmiştir. Günümüzde en yaygın kullanılan protok ailesi IP denilen protokol ailesidir. Biz kursumuzun bu bölümünde öne "aynı makinenin prosesleri arasındaki haberleşmeleri" inceleyeciz. Daha sonra başka bir bölümde IP protokol ailesi ile haberleşme üzerinde duracağız. Haberleşme sistemleri verilerin gönderilip alınma biçimine göre iki kısma ayrılmaktadır: -> Stream Tarzı (Stream Oriented) Haberleşme -> Paket ya da Mesaj Tarzı (Message Oriented) Haberşleme Buradaki, >> Stream tarzı haberleşme denildiğinde gönderen ve alan tarafın istediği kadar byte'ı gönderip alabilmesi anlaşılmaktadır. Örneğin borular stream tarzı bir haberleşme sunmaktadır. Yani borularda gönderen taraf istediği kadar byte'ı üst üste gönderebilir. Bunlar byte düzeyinde sıraya dizilirler. Alan taraf da istediği kadar byte'ı alabilir. Gönderen taraf n byte gönderdiğinde alan taraf bu n byte'ı tek hamlede almak zorunda değildir. >> Paket ya da mesaj tabanlı haberleşmede gönderen taraf bir grup bilgiyi bir paket ya da mesaj adı altında gönderir. Alan taraf byte düzeyinde alma yapamaz. Gönderilen paketin hepsini almak zorundadır. Ağ haberleşmelerinde bu tarz haberleşmelere "datagram" haberleşmesi de denilmektedir. İşte borular stream tabanlı bir haberleşme sunarken mesaj kuyrukları mesaj tabanlı (ya da paket tabanlı da diyebiliriz) bir haberleşme sunmaktadır. Şimdi de prosesler arası haberleşme yöntemlerini irdeleyelim: >> Aynı Makinenin Prosesleri Arasında Haberleşme : Aynı makinenin prosesleri arasındaki haberleşmelerde Windows sistemleri ile UNIX/Linux (ve macOS) sistemleri arasındaki yöntemler çok benzerdir. Tabii bu yöntemler bu sistemlerde farklı fonksiyonlarla gerçekleştirilmektedir. Yöntemler tema olarak birbirilerine bzense de ayrıntılarda farklılıklar bulunmaktadır. Biz de kurusumuzun bu bölümünde önce değişik yöntemleri önce UNIX/Linux (dolayısıla macOS) sistemlerinde sonra da Windows sistemlerinde göreceğiz. >>> UNIX/Linux Sistemlerinde: >>>> Boru Haberleşmesi : Boru haberleşmesi en yalın ve en çok kullanılan proseslerarası haberleşme yöntemidir. Boru FIFO prensibiyle çalışan bir kuyruk sistemidir. Borunun bir ucu bir proseste diğer ucu diğer prosestedir. Proseslerden biri boruya yazma yaptığında diğeri yazılanları yazıldığı sırada okumaktadır. Boruların belli bir uzunluğu vardır. Boruya yazma yapan proses eğer borudan okuma yapan proses yavaş kalırsa boruyu doldurabilir. Dolu bir boruya yazma yapıldığında yazma yapan taraf bloke olur (yani CPU zamanı harcamadan bekler) ta ki okuyan taraf borıda yer açana kadar. Benzer biçimde borudan okuma yapan taraf boru boşsa okunacek bir şey kalmadığı için bloke olmaktadır. Ta ki yazan taraf boruya bir şey yazana kadar. Böylece boru haberleşmesinde yazan taraf okuyan tarafı okuyan taraf da yazan tarafı beklemektedir. Dolayısıyla bu yöntem kendi içerisinde bir senkroniasyon da içermektedir. Boru haberleşmesi her zaman önce yazan tarafın boruyu kapatmasıyla sonlandırılır. Bu durumda borudan okuma yapan taraf önce boruda kalanları okur, sonra okunacak bir şey kalmayınca o da boruyu kapatır. Haberşelmede önce okuyan tarafın boruyu kapatması patolojik bir durumdur. Boru haberleşmeleri kendi aralarında ikiye ayrılmaktadır: -> İsimsiz Boru Haberleşmeleri (Unnamed Pipes ya da Anonymous Pipe) -> İsimli Boru Haberleşmeleri (Named Pipes ya da FIFO) İsimiz boru haberleşmesi üst ve alt prosesler arasında kullanılmaktadır. Ancak isimli boru haberleşmesi herhangi iki proses arasında kullanılabilmektedir. >>>>> İsimsiz Boru Haberleşmesi : UNIX/Linux (ve macOS) sistemlerinde isimsiz boru haberleşmesi şu adımlardan geçilerek gerçekleştirilmektedir: -> Önce üst proses pipe POSIX fonksiyonuyla isimsiz boruyu yaratır. pipe fonksiyonun prototipi şöyledir: #include int pipe(int pipefd[2]); pipe fonksiyonunun parametresi int türden göstericidir. Programcı 2 elemanlı int bir dizi açarak dizinin adresini fonksiyona geçirir. (Prototipteki dizi sentaksının göstericiden hiçbir farkı yoktur. Dolayısıyla burada programcı aslında fonksiyona iki elemanlı int dizinin adresini geçirmek zorunda dğeildir. Burada okunabilirliğin artırılması için böyle bir prototip yazılmıştır.) Fonksiyon boruyu yaratır. Boruya ilişkin iki betimleyiciyi argüman olarak verdiğimiz int diziye yerleştirir. Dizinin ilk elemanındaki (0'ıncı indeksteki) betimleyici "read-only" bir betimleyicidir ve borudan okuma yapmak için kullanılmalıdır. Dizinin ikinci elemanındaki (1 numaralı indeksindeki) betimleyici "write only" bir betimleycidir boruya yazma yapmak için kullanılmalıdır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. Örneğin: int pfds[2]; if (pipe(pfds) == -1) exit_sys("pipe"); /* pfds[0] betimleyicisi okuma yapmak için pfds[1] betimleyicisi yazma yapmak için kullanılmalıdır */ -> Artık alt proses yaratılır. Böylece üst prosesteki boru betimleyicileri de alt proses aktarlmış olur. Örneğin: int pfds[2]; int pid; if (pipe(pfds) == -1) exit_sys("pipe"); if ((pid = fork()) == -1) exit_sys("pid"); if (pid != 0) { /* parent process */ ... } else { /* child process */ ... } -> Boru ve alt proses yaratıldıktan sonra hangi tarafın okuma yapacağına ve hangi tarafın yazma yapacağına programcının karar vermesi gerekir. Örneğin üst proses yazma yapacak, alt proses okuma yapacak olabilir. Ya da bunun tersi olabilir. Borular sanki birer dosyaymış gibi ele alınmaktadır. Dolayısıyla borulardan okuma yapmak için read fonksiyonu, borulara yazma yapmak için write fonksiyonu kullanılmaktadır. fork işlemi sonrasında üst prosesteki iki boru betimleyicisi alt prosese aktarılmış olur. Böylece hem üst proseste hem de alt proseste okuma ve yazma betimleyicileri bulunacaktır. Normal olarak boruya yazma potansiyelinde olan ve borudan okuma potansiyelinde olan tek bir betimleyici bulunmalıdır. Dolaysıyla yazan taraf okuma betimleyicisini, okuyan taraf ise yazma betimelyicisini kapatmalıdır. Örneğin üst proses yazma yapacak olsun alt proses de okuma yapacak olsun: int pfds[2]; int pid; if (pipe(pfds) == -1) exit_sys("pipe"); if ((pid = fork()) == -1) exit_sys("pid"); if (pid != 0) { /* üst proses boruya yazma yapacak */ close(pfds[0]); ... } else { /* alt proses borudan okuma yapacak */ close(pfds[1]); ... } read fonksiyonu ile borudan n byte okuma yapılmak istendiğinde eğer boruda hiçbir byte yoksa read fonksiyonu bloke olur ve en az 1 byte okuyana kadar beklemeye yol açar. Eğer borudan n byte okunmak istediniğinde boruda en az 1 byte bilgi oluşmuşsa bu durumda read fonksiyonu n byte'ın tamamı okunan kadar blokede beklemez. Okuyabildiği kadar byte'ı okur ve okuyabildiği byte sayısı ile geri döner. Eğer boruda okunacak bir şey yoksa ancak boruya yazma potansiyelinde olan tüm betimleyiciler de kapatılmışsa bu durumda read fonksiyonu bloke olmaz ve 0 değeri ile geri döner. Yani read fonksiyonu 0 ile geri dönmüşse bu durum "boruda bir şey kalmadı yazan taraf da boruyu kapatmış" anlamına gelmektedir. write fonksiyonu ile boruya n byte yazma yapılmak istendiğinde write fonksiyonu n byte'ın hepsi yazlana kadar blokede beklemektedir. Yani borulara kısmi yazım (partial write) mümkün değildir. Örneğin boruda 10 byte'lık boş alan olsun. Biz de write fonksiyonu ile boruya 15 byte yazmak isteyelim. Bu durumda bu 15 byte'ın tamamı yazılana kadar write fonksiyonu blokede bekleyecektir. Borudan okuma potansiyeline sahip hiçbir betimleyici kalmadığı durumda boruya write fonksiyonu ile bir şey yazılmak istendiğinde UNIX/Linux sistemlerinde SIGPIPE isimli sinyal oluşmaktadır. Bu sinyal de prosesin sonandırılmasına yol açmaktadır. * Örnek 1, Aşağıdaki örnekte üst proses alt prosese 1000000 tane int değeri boruyu yoluyla iletmekte ve alt proses de bunları borudan alarak stdout dosyasına yazdırmaktadır. #include #include #include #include void exit_sys(const char *msg); int main(void) { int pfds[2]; int pid; ssize_t result; int val; if (pipe(pfds) == -1) exit_sys("pipe"); if ((pid = fork()) == -1) exit_sys("pid"); if (pid != 0) { /* parent process writes to pipe */ close(pfds[0]); for (int i = 0; i < 10; ++i) if (write(pfds[1], &i, sizeof(int)) == -1) exit_sys("write"); close(pfds[1]); if (wait(NULL) == -1) exit_sys("wait"); } else { /* child process reads from pipe */ close(pfds[1]); while ((result = read(pfds[0], &val, sizeof(int))) > 0) { printf("%d ", val); fflush(stdout); } if (result == -1) exit_sys("write"); close(pfds[0]); } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } İsimsiz boru haberleşmesinde neden okuyan taraf yazma betimleyicini yazan taraf da okuma betimleyicini kapatmaktadır? Eğer okuyan taraf yazma betimeleyicisini kapatmazsa yazab taraf yazma betimleyicisini kapatsa bile okuyan taraftaki read fonksiyonu "hala boruya yazma potansiyelinde olan bir betimleyici bulunduğu için" 0 ile geri dönmeyecektir ve bloke oluşturacaktır. Yazan tarafın okuma betimelyicisini kapatmaması önceki kadar önemlib bir probleme yol açmayacaksa da betimleyicilerin boşuna betimelyici tablosunda yer kaplaması iyi bir teknik değildir. Yazna tarafın da okuma betimelyicisini kapatması en normal durumdur. * Örnek 1, Aşağıdaki örnekte "sample" programı komut satırıyla aldığı programı (örneğimizde "mample") fork/exec yaparak çalıştırmaktadır. Ancak "sample" programı fork işlemi öncesinde isimsiz boru yaratıp exec yapmaktadır. exec yapılan program kodu boru betimleyicisini bilemeyeceği için "sample" programı bu betimelyiciyi çalıştırdığı programa ("mample") komut satırı argümanı olarak aktarmaktadır. /* sample.c */ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int pfds[2]; int pid; ssize_t result; int val; char buf[10]; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if (pipe(pfds) == -1) exit_sys("pipe"); if ((pid = fork()) == -1) exit_sys("pid"); if (pid != 0) { /* parent process writes to pipe */ close(pfds[0]); for (int i = 0; i < 1000000; ++i) if (write(pfds[1], &i, sizeof(int)) == -1) exit_sys("write"); close(pfds[1]); if (wait(NULL) == -1) exit_sys("wait"); } else { /* child process reads from pipe */ close(pfds[1]); sprintf(buf, "%d", pfds[0]); if (execlp(argv[1], argv[1], buf, (char *)NULL) == -1) exit_sys("execvp"); /* unreachable code */ } return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int pfd; ssize_t result; int val; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } pfd = atoi(argv[1]); while ((result = read(pfd, &val, sizeof(int))) > 0) { printf("%d ", val); fflush(stdout); } printf("\n"); if (result == -1) exit_sys("read"); close(pfd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>>> İsimli Boru Haberleşmesi : UNIX/Linux (ve macOS) sistemlerinde isimli borular sanki bir dosyaymış gibi oluşturulup kullanılmaktadır. Programcı önce bir isimli boru dosyasını oluşturmalıdır. Bu sistemlerde isimli borulara "fifo" da dendiğini anımsayınız. İsimli boru dosyaları mkfifo isimli POSIX fonksiyonuyla yaratılmaktadır. Fonksiyonun prototipi şöyledir: #include int mkfifo(const char *path, mode_t mode); Boru dosyaları gerçek birer dosya değildir. Bunların yalnızca dizin girişleri vardır. Bu dosylar açıldığında aslında boru yine işletim sistemi tarafından kernel alanı içerisinde oluşturulmaktadır. Buradaki dizin girişi yalnızca farklı proseslerin aynı isim altında anlaşmaları için kullanılmaktadır. mkfifo fonksiyonunun birinci parametresi yaratılacak boru dosyasının yol ifadesini belirtir. İkinci parametre ise erişim haklarını belirtmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin: if (mkfifo("myfifo", S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) == -1) exit_sys("mkfifo"); mkfifo fonksiyonu prosesin umask değerini dikkat ealmaktadır. Eğer yaratacağınız borunun tam olarak sizin belirlediğiniz umask değerine sahip olmasını istiyorsanız önce umask(0) çağrısını yapmalısınız. Boru dosyaları "ls -l" komutunda dosya türü olarak "p" ile temsil edilmektedir. Örneğin: prw-r--r-- 1 kaan study 0 Ara 24 17:34 myfifo Aşağıda örnek olarak "mkfifo" komutunun bir benzeri verilmiştir. Komuta isteğe bağlı olarak -m seçeneği girilebilmektedir. * Örnek 1, /* mymkfifo.c */ #include #include #include #include #include #include bool check_octal(const char *arg); void exit_sys(const char *msg); int main(int argc, char *argv[]) { int m_flag, err_flag; int result; char *m_arg; int mode; m_flag = err_flag = 0; mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH; opterr = 0; while ((result = getopt(argc, argv, "m:")) != -1) { switch (result) { case 'm': m_flag = 1; m_arg = optarg; break; case '?': if (optopt == 'b') fprintf(stderr, "-b option given without argument!..\n"); else fprintf(stderr, "invalid option: -%c\n", optopt); err_flag = 0; break; } } if (err_flag) exit(EXIT_FAILURE); if (argc - optind == 0) { fprintf(stderr, "pipe name(s) must be specified!..\n"); exit(EXIT_FAILURE); } if (m_flag) { if (!check_octal(m_arg)) { fprintf(stderr, "invalid mode parameter: %s\n", m_arg); exit(EXIT_FAILURE); } sscanf(m_arg, "%o", &mode); } umask(0); for (int i = optind; i < argc; ++i) if (mkfifo(argv[i], mode) == -1) perror(argv[i]); return 0; } bool check_octal(const char *arg) { if (strlen(arg) > 3) return false; for (int i = 0; arg[i] != '\0'; ++i) if (arg[i] < '0' || arg[i] > '7') return false; return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } İsimli boru mkfifo fonksiyonuyla ya da mkfifo komutuyla yaratıldktan sonra artık haberleşecek iki proses de boruyu open fonksiyonuyla açmalıdır. Boruya yazma yapacak prosese O_WRONLY bayrağını, borudan okuma yapacak proses O_RDONLY bayrağını kullanmalıdır. Boruların O_RDWR modunda açılması Linux sistemlerinde geçerli olsa da POSIX standartlarında "tanımsız davranış" oluşturmaktadır. İki proses de uygun bir biçimde open fonksiyonuyla boruyu açtıktan sonra artık tıpkı isimsiz borularda oludğu gibi write ve read fonksiyonlarıyla haberleşme sağlanır. write ve read fonksiyonlarınınm davranışı isimsiz borularda olduğu gibidir. İsimli borular sanki bir dosyaymış gibi kullanıldığı halde aslında isimsiz borulardaki gibi kernel alanında bir FIFO kuyruk sistemini kullanırlar. Boruyu yine yazan taraf kapatmalıdır. Pkuyan taraf yine önce boruda kalanları okur sonra read fonksiyonu 0 ile geri döner. read fonksiyonu 0 ile geri döndüğünde okuyan taraf da boruyu kapatır. open fonksiyonuyla isimli boru O_WRONLY modunda açılmak istendiğinde open fonksiyonu boru başka bir proses tarafından O_RDONLY modunda açılana kadar blokeye yol açmaktadır. Benzer biçimde boruyu bir proses O_RDONLY modunda açmaya çalıştığında başka bir proses boruyu O_WRONLY modunda açana kadar open blokeye yol açmaktadır. Alında isimli borular da "blokesiz modda" açılabilmektedir. Ancak bu konu kursumuzun kapsamı dışındadır. * Örnek 1, Aşağıdaki örnekte "pwrite.c" ve "pread.c" isimli iki program verilmiştir. pwrite programı boruyu O_WRONLY modunda açıp boruya yazma yapmaktadır. pread fonksiyonu ise boruyu O_RDONLY modunda açıp borudan okuma yapmaktadır. /* pwrite.c */ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int pfd; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((pfd = open(argv[1], O_WRONLY)) == -1) exit_sys("open"); for (int i = 0; i < 1000000; ++i) if (write(pfd, &i, sizeof(i)) == -1) exit_sys("write"); close(pfd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* pread.c */ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int pfd; ssize_t result; int val; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((pfd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); while ((result = read(pfd, &val, sizeof(int))) > 0) printf("%d ", val), fflush(stdout); if (result == -1) exit_sys("read"); printf("\n"); close(pfd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Pekiyi iki proses aynı anda isimli boruya yazma yaapmak isterse ne olur? İşte POSIX standrtlarına göre eğer yazılmak istenen bilgi dosyası içerisindeki PIPE_BUF sembolik sabitinden büyük olmadıktan sonra iç içe geçme oluşmamaktadır. Yani bu durumda iki proses aynı anda boruya yazma yapmaya çalışsa bile önce birinin sonra diğerinin yazdıkları boruda görünür. Ancak PIPE_BUF değerinden daha büyük miktarda bilgi boruya yazılmak istendiğinde iç içe geçme oluşabilmektedir. Şimdi de kabuk programlarının boru işlemlerini nasıl yaptığını açıklayan bir örnek yapalım. Örneğimizdeki "shellpipe.c" programı kabuk programının yaptığı gibi boru işlemini yapmaktadır. Bu program aşağıdaki gibi çalıştırılmalıdır: ./shellpipe "ls -l | wc" Programda önce '|' karakterinin yeri bulunmuş ve bu karakterin iki tarafı da parse edilmiştir. Daha sonra işlemler şu sırada yürütülmüştür: -> Üst proses ("shellpipe.c") bir boru yaratmıştır. Burada iki betimleyici elde etmiştir. -> Üst proses '|' karakterin solundaki program için fork yapmıştır. Bu durumda boru betimleyicileri ayaratılan alt prosese aktarılmıştır. Sonra alt proseste okuma yapılmak için kullanılan boru betimelyeicisi kapatılmış stdout betimelyicisi de boruya yönlendirilmiştir. Tabii diğer boru betimelyicisi de bu işlemden sonra kapatılmıştır. Nihayet bu işlemlerden sonra exec işlemi uygulanmıştır: if (pid1 == 0) { /* first child */ close(pfds[0]); if (dup2(pfds[1], 1) == -1) exit_sys("dup2"); close(pfds[1]); if (execvp(pargs.prog1[0], &pargs.prog1[0]) == -1) exit_sys("execvp"); /* unreachable code */ } -> Benzer işlemler '|' karakterinin sağındaki program için de yapılmıştır. Tabii '|' karakterinin sağındaki program için fork yapıldığında artık alt proseste stin betimleyicisi boruya yönlendirilmiştir: if ((pid1 = fork()) == -1) exit_sys("fork"); if ((pid2 = fork()) == -1) exit_sys("fork"); if (pid2 == 0) { /* first child */ close(pfds[1]); if (dup2(pfds[0], 0) == -1) exit_sys("dup2"); close(pfds[0]); if (execvp(pargs.prog2[0], &pargs.prog2[0]) == -1) exit_sys("execvp"); /* unreachable code */ } -> Artık üst prosesin de boru betimelycisilerini kapatması gerekmektedir. Böylece toplamda boru ile ilgili iki betimleyici kalacaktır. Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include #include #include #include #define MAX_ARGS 1024 typedef struct tagPIPE_ARGS { char *prog1[MAX_ARGS]; char *prog2[MAX_ARGS]; } PIPE_ARGS; bool check_arg(char *arg, PIPE_ARGS *rargs); void exit_sys(const char *msg); int main(int argc, char *argv[]) { char *arg; PIPE_ARGS pargs; int pfds[2]; pid_t pid1, pid2; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((arg = strdup(argv[1])) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if (!check_arg(arg, &pargs)) { fprintf(stderr, "invalid argument: \"%s\"\n", argv[1]); exit(EXIT_FAILURE); } if (pipe(pfds) == -1) exit_sys("pipe"); if ((pid1 = fork()) == -1) exit_sys("fork"); if (pid1 == 0) { /* first child */ close(pfds[0]); if (dup2(pfds[1], 1) == -1) exit_sys("dup2"); close(pfds[1]); if (execvp(pargs.prog1[0], &pargs.prog1[0]) == -1) exit_sys("execvp"); /* unreachable code */ } if ((pid2 = fork()) == -1) exit_sys("fork"); if (pid2 == 0) { /* first child */ close(pfds[1]); if (dup2(pfds[0], 0) == -1) exit_sys("dup2"); close(pfds[0]); if (execvp(pargs.prog2[0], &pargs.prog2[0]) == -1) exit_sys("execvp"); /* unreachable code */ } free(arg); close(pfds[0]); close(pfds[1]); if (waitpid(pid1, NULL, 0) == -1) exit_sys("waitpid"); if (waitpid(pid2, NULL, 0) == -1) exit_sys("waitpid"); return 0; } bool check_arg(char *arg, PIPE_ARGS *pargs) { char *pos, *str; size_t i; if ((pos = strchr(arg, '|')) == NULL || strchr(pos + 1, '|') != NULL) return false; *pos = '\0'; i = 0; for (str = strtok(arg, " \t"); str != NULL; str = strtok(NULL, " \t")) pargs->prog1[i++] = str; pargs->prog1[i] = NULL; if (i == 0) return false; i = 0; for (str = strtok(pos + 1, " \t"); str != NULL; str = strtok(NULL, " \t")) pargs->prog2[i++] = str; pargs->prog2[i] = NULL; if (i == 0) return false; return true; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>> Paylaşılan Bellek Alanları: Proseslerarası haberleşme yöntemlerinden bir diğeri de "paylaşılan bellek alanları (shared memory)" denilen yöntemdir. Aslında bu yöntem hakkında biz sayfa tablolarını anlattığımız bölümde bazı ipuçları vermiştik. Bu yöntemde farklı proseslerin sayfa tablolarındaki farklı sanal sayfa numaraları aynı fiziksel sayfaya yönlendirilmektedir. Böylece proseslerden biri o sanal sayfaya yazma yaptığında diğeri onu diğer sanal sayfa yoluyla görebilmektedir. Örneğin: Proses-1 Sayfa Tablosu Sanal Sayfa No Fiziksel Sayfa No .... .... 1784 3641 .... .... Proses-2 Sayfa Tablosu Sanal Sayfa No Fiziksel Sayfa No .... .... 1432 3641 .... .... Buradaki numaraların 16'lık sistemde olduğunu varsayalım. Burada Proses-1'in 1784 numaralı sanal sanal sayfa adresiyle [1784000-17844FFF] yaptığı erişimlerle Proses-2'nin 1432 numaralı sanal sayfa adresiyle [1432000-1432FFF] yaptığı erişimler aslında aynı fiziksel bölgeyi belirtmektedir. UNIX/Linux (ve macOS) sistemlerinde paylaşılan bellek alanlarının oluşturulması için iki farklı fonksiyon grubu kullanılabilmektedir. Bunlardan biri eski System-5 fonksiyonlarıdır. Diğeri daha modern fonksiyonlardır. Bu modern fonksiyon grubuna halk arasında "POSIX Shared Memory" fonksiyonları da denilmektedir. Aslında her iki fonksiyon grubu da POSIX standartlarında bulunmaktadır. System-5 paylaşılan bellek aşanı fonksiyonları çok esikden beri UNIX türevi sistemlerde bulunmaktadır. POSIX paylaşılan bellek alanı fonksiyonları ise sistemlere 90'lı yılların ortalarında girmeye başlamıştır. Ancak bugün her iki grup fonksiyonun da kullanıldığını söyleyebiliriz. Biz kurusumuzda yalnızca POSIX paylaşılan bellek alanı fonksiyonlarını göreceğiz. POSIX paylaşılan bellek alanları tipik olarak şu adımlardan geçilerek kullanılmaktadır: -> POSIX paylaşılan bellek alanı nesnesi iki proses tarafından shm_open fonksiyonuyla yaratılabilir ya da zaten var olan nesne shm_open fonksiyonu ile açılabilir. Fonksiyonun prototipi şöyledir: #include int shm_open(const char *name, int oflag, mode_t mode); Fonksiyonun birinci parametresi paylaşılan bellek alanı nesnesinin ismini belirtmektedir. Bu ismi kök dizinde bir dosya ismi gibi verilmesi gerekmektedir. (POSIX standartlarında bazı sistemlerin buradaki dosya isminin başka dizinlerde olmasına izin verebildiğini belirtmiştir.) Fonksiyonun ikinci parametresi paylaşılan bellek alanının açış bayraklarını belirtmektedir. Bu bayraklar şunlardan birini içerebilir: O_RDONLY: Bu durumda paylaşılan bellek alanından yalnızca okuma yapılabilir. O_RDWR: Bu durumda paylaşılan bellek alanından hem okuma yapılabilir hem de oraya yazma yapılabilir. Aşağıdaki bayraklar da açış moduna eklenebilir: O_CREAT: Paylaşılan bellek alanı yoksa yaratılır, varsa olan açılır. O_EXCL: O_CREAT bayrağı ile birlikte kullanılabilir. Paylaşılan bellek alanı zaten varsa fonksiyon başarısız olur. O_TRUNC: Paylaşılan bellek alanı varsa sıfırlanarak açılır. Bu mod için O_RDWR bayrağının kullanılmış olması gerekmektedir. Fonksiyonun üçüncü parametresi paylaşılan bellek alanının erişim haklarını belirtmektedir. Tabii ancak ikinci parametrede O_CREAT bayrağı kullanılmışsa bu parametreye gereksinim duyulmaktadır. İkinci parametrede O_CREAT bayrağı kullanılmamışsa üçüncü parametre fonksiyon tarafından hiç kullanılmamaktadır. shm_open bize tıpkı bir disk dosyasında olduğu gibi bir dosya betimleyicisi vermektedir. Fonksiyon başarısız olursa yine -1 değerine geri döner ve errno değişkeni uygun biçimde set edilir. Burada yaratılan paylaşılan bellek alanı nesnesi için bir dizin girişi oluşturulmamaktadır. Yani paylaşılan bellek alanı nesnesi bir dosyaymış gibi ele alınmakla birlikte aslında bir dosya değildir. Örneğin: int fdshm; if ((fdshm = shm_open(SHM_NAME, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shm_open"); -> Paylaşılan bellek alanı yaratıldıktan sonra ftruncate fonksiyonu ile ona bir büyüklük vermek gerekir. Örneğin: if (ftruncate(fdshm, SHM_SIZE) == -1) exit_sys("ftruncate"); Tabii paylaşılan bellek alanı zaten yaratılmışsa ve biz onu açıyorsak ftruncate fonksiyonunu aynı uzunlukta çağırdığımızda aslında fonksiyon herhangi bir şey yapmayacaktır. Yani aslında ftruncate fonksiyonu paylaşılan bellek alanıilk kez yaratılırken bir kez çağrılır. Ancak yukarıda da belirttiğimiz gibi aynı uzunlukta ftruncate işleminin bir etkisi yoktur. ftruncate aslında bir dosyanın büyüklüğünü değiştirmek için kullanılan genel bir fonksiyondur. Fonksiyonun prototipi şöyledir: #include int ftruncate(int fd, off_t length); Fonksiyonun birinci parametresi dosya betimleyicisini ikinci parametresi dosyanın yeni uzunluğunu almaktadır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri döner. ftruncate POSIX fonksiyonunun truncate isminde yol ifadeis ile çalışanm bir biçimi de vardır. POSIX paylaşılan bellek alanı nesneleri Linux'ta dosya sisteminde /dev/shm dizini içerisinde görüntülenmektedir. Yani programcı isterse bu dizin içerisindeki nesneleri komut satırında rm komutuyla silebilir. -> Artık paylaşılan bellek alanı nesnesinin belleğe "map" edilmesi gerekmektedir. Bunun için mmap isimli bir POSIX fonksiyonu kullanılmaktadır. mmap fonksiyonu pek çok UNIX türevi sistemde bir sistem fonksiyonu olarak gerçekleştirilmiştir. mmap paylaşılan bellek alanlarının dışında başka amaçlarla da kullanılabilen ayrıntılı bir sistem fonksiyonudur. Bu nedenle biz burada önce fonksiyonun paylaşılan bellek alanlarında kullanımına kısaca değineceğiz. Sonra bu fonksiyonu ayrıca daha ayrıntılı biçimde ele alacağız. mmap fonksiyonunun prototipi şöyledir: #include void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off); Fonksiyonun birinci parametresi, "mapping için" önerilen sanal adresi belirtmektedir. Programcı belli bir sanal adresin mapping için kullanılmasını isteyebilir. Ancak fonksiyon flags parametresinde MAP_FIXED geçilmemişse bu adresi tam (exact) olarak yani verildiği gibi kullanmayabilir. Fonksiyon bu önerilen adresin yakınındaki bir sayfayı tahsis edebilir. Bu tahsisatın burada belirtilen adresin neresinde yapılacağı garanti edilmemiştir. Yani buradaki adres eğer fonksiyonun flags parametresinde MAP_FIXED kullanılmamışsa bir öneri niteliğindedir. Eğer bu adres NULL olarak geçilirse bu durumda mapping işlemi işletim sisteminin kendi belirlediği bir adresten itibaren yapılır. Tabii en tipik durum bu parametrenin NULL adres olarak geçilmesidir. Fonksiyonun ikinci parametresi, paylaşılan bellek alanının ne kadarının map edileceğini belirtir. Örneğin paylaşılan bellek alanı nesnesi 1MB olabilir. Ancak biz onun 100K'lık bir kısmını map etmek isteyebiliriz. Ya da tüm paylaşılan bellek alanını da map etmek isteyebiliriz. Bu uzunluk sayfa katlarında olmak zorunda değildir. Ancak pek çok sistem bu uzunluğu sayfa katlarına doğru yukarı yuvarlamaktadır. Yani biz uzunluğu örneğin 100 byte verebiliriz. Ancak sistem 100 byte yerine sayfa uzunluğu olan 4096 byte'ı map edecektir. Fonksiyonun üçüncü parametresi, mapping işleminin koruma özelliklerini belirtmektedir. Başka bir deyişle bu parametre paylaşılan bellek alanı için ayrılacak fiziksel sayfaların işlemci düzeyinde koruma özelliklerini belirtir. Bu özellikler şunlardan oluşturulabilir: PROT_READ PROT_WRITE PROT_EXEC PROT_NONE PROT_READ sayfanın "read only" olduğunu belirtir. Böyle sayfalara yazma yapılırsa işlemci exception oluşturur ve program SIGSEGV sinyali ile sonlandırılır. PROT_WRITE sayfaya yazma yapılabileceğini belirtmektedir. Örneğin PROT_READ|PROT_WRITE hem okuma hem de yazma anlamına gelmektedir. PROT_EXEC ilgili sayfada bir kod varsa (örneğin oraya bir fonksiyon yerleştirilmişse) o kodun çalıştırılabilirliği üzerinde etkili olmaktadır. Örneğin Intel ve ARM işlemcilerinde fiziksel sayfa PROT_EXEC ile özelliklendirilmemişse o sayfadaki bir kod çalıştırılamamaktadır. PROT_NONE o sayfaya herhangi bir erişimin mümkün olamayacağını belirtmektedir. Yani PROT_NONE olan bir sayfa ne okunabilir ne de yazılabilir. Bu tür sayfa özellikleri "guard page" oluşturmak için kullanılabilmektedir. Tabii bir sayfanın koruma özelliği daha sonra da değiştirilebilir. Aslında bütün işlemciler buradaki koruma özelliklerinin hepsini desteklemeyebilirler. Örneğin Intel işlemcilerinde PROT_WRITE zaten okuma özelliğini de kapsamaktadır. Bazı işlemciler sayfalarda PROT_EXEC özelliğini hiç bulundurmamaktadır. Ancak ne olursa olsun programcı sanki bu özelliklerin hepsi varmış gibi bu parametreyi oluşturmalıdır. Fonksiyonun dördüncü parametresi olan flags aşağıdaki değerlerden yalnızca birini alabilir: MAP_PRIVATE MAP_SHARED MAP_PRIVATE ile oluşturulan mapping'e "private mapping", MAP_SHARED ile oluşturulan mapping'e ise "shared mapping" denilmektedir. MAP_PRIVATE "copy on write" denilen semantik için kullanılmaktadır. "Copy on write" işlemi "yazma yapılana kadar sanal sayfaların aynı fiziksel sayfalara yönlendirilmesi ancak yazmayla birlikte o sayfaların bir kopyalarının çıkartılıp yazmanın o prosese özel olarak yapılması ve yapılan yazmaların paylaşılan bellek alanına yansıtılmaması" anlamına gelmektedir. Başka bir deyişle MAP_PRIVATE şunlara yol açmaktadır; - Okuma yapılınca paylaşılan bellek alanından okuma yapılmış olur. - Ancak yazma yapıldığında bu yazma paylaşılan bellek alanına yansıtılmaz. O anda yazılan sayfanın bir kopyası çıkartılarak yazma o kopya üzerine yapılır. Dolayısıyla başka bir proses bu yazma işlemini göremez. Bir proses ilgili paylaşılan bellek alanı nesnesini MAP_PRIVATE ile map ettiğinde diğer proses o alana yazma yaptığında onun yazdığını MAP_PRIVATE yapan prosesin görüp görmeyeceği POSIX standartlarında belirsiz (unspecified) bırakılmıştır. Linux sistemlerinin man sayfasında da aynı "unspecified" durum belirtilmiş olsa da mevcut Linux çekirdeklerinde başka bir proses private mapping yapılmış yere yazma yaptığında bu yazma private mapping'in yapıldığı proseste görülmektedir. Ancak sayfaya yazma yapıldığında artık o sayfanın kopyasından çıkartılacağı için bu yazma işleminden sonraki diğer prosesin yaptığı yazma işlemleri görülmemektedir. MAP_SHARED ise yazma işleminin paylaşılan bellek alanına yapılacağını yani "copy on write" yapılmayacağını belirtmektedir. Dolayısıyla MAP_SHARED bir mapping'te paylaşılan alana yazılanlar diğer prosesler tarafından görülür. Normal olarak programcılar amaç doğrultusunda shared mapping kullanırlar. Private mapping (yani "copy on write") bazı özel durumlarda tercih edilmektedir. Örneğin işletim sistemi (exec fonksiyonları) çalıştırılabilir dosyanın ".data" bölümünü private mapping yaparak belleğe mmap ile yüklemektedirler. Fonksiyonun flags parametresinde MAP_PRIVATE ve MAP_SHARED değerlerinin yalnızca biri kullanılabilir. Ancak bu değerlerden biri ile MAP_FIXED değeri bit düzeyinde OR işlemine sokulabilmektedir. MAP_FIXED bayrağı, fonksiyonun birinci parametresindeki adres NULL geçilmemişse bu adresin kendisinin aynen (hiç değiştirilmeden) kullanılacağını belirtmektedir. Yani bu adresin yakınındaki herhangi bir sayfa değil kendisi tahsis edilecek ve fonksiyon bu adresin aynısıyla geri dönecektir. Eğer MAP_FIXED bayrağı belirtilmişse Linux sistemlerinde birinci parametredeki adresin sayfa katlarında olma zorunlululuğu vardır. Ancak POSIX standartlarının son versiyonları "may require" ifadesiyle bunun zorunlu olmayabileceğini belirtmektedir. Fonksiyonun son iki parametresi dosya betimleyicisi ve bir de offset içermektedir. Paylaşılan bellek alanının belli bir offset'ten sonraki kısmı map edilebilmektedir. Örneğin paylaşılan bellek alanı nesnemiz 4MB olsun. Biz bu nesnenin 1MB'sinden itibaren 64K'lık kısmını map edebiliriz. O halde mmap fonksiyonunu örneğin şöyle çağırabiliriz: shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, fdshm, 0); Burada paylaşılan bellek alanı nesnesinin SHM_SIZE kadar alanı map edilmek istenmiştir. İlgili sayfalar PROT_WRITE özelliğine sahip olacaktır. Yani bu sayfalara yazma yapılabilecektir. Bu sayfalara yazma yapıldığında paylaşılan bellek alanı nesnesi bundan etkilenecek yani aynı nesneyi kullanan diğer proseslerde de eğer shared mapping yapılmışsa bu durum gözükecektir. Burada paylaşılan bellek alanı nesnesinin 0'ıncı offset'inden itibaren SHM_SIZE kadar alanın map edildiğine dikkat ediniz. mmap fonksiyonun son parametresindeki offset değeri MAP_FIXED belirtilmişse, birinci parametre ile son parametrenin sayfa katlarına bölümünden elde edilen kalan aynı olmak zorundadır. (Yani örneğin POSIX standartlarında işletim sistemi eğer 5000 adresini kabul ediyorsa 5000 % 4096 = 4'tür. Bu durumda son parametrenin de 4096'ya bölümünden elde edilen kalan 4 olmalıdır.) Ancak MAP_FIXED belirtilmemişse POSIX standartları bu offset değerinin sayfa katlarında olup olmayacağını işletim sistemini yazanların isteğine bırakmıştır. Linux çekirdeklerinde, MAP_FIXED belirtilsin ya da belirtilmesin bu offset değeri her zaman sayfa katlarında olmak zorundadır. mmap fonksiyonu başarı durumunda mapping yapılan sanal bellek adresine geri dönmektedir. Fonksiyon başarısızlık durumunda MAP_FAILED özel değerine geri döner. Pek çok sistemde MAP_FAILED bellekteki son adres olarak aşağıdaki biçimde define edilmiştir: #define MAP_FAILED ((void *) -1) Kontrol şöyle yapılabilir: shmaddr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, fdshm, 0); if (shmaddr == MAP_FAILED) exit_sys("mmap"); Paylaşılan bellek alanı betimleyicisi mapping işleminden sonra close fonksiyonuyla kapatılabilir. Bu durum mapping'i etkilememektedir. -> Programcı paylaşılan bellek alanı ile işini bitirdikten sonra artık map ettiği alanı boşaltabilir. Bu işlem munmap POSIX fonksiyonu ile yapılmaktadır. (mmap fonksiyonunu malloc gibi düşünürsek munmap fonksiyonunu da free gibi düşünebiliriz.) munmap fonksiyonunun prototipi şöyledir: #include int munmap(void *addr, size_t len); Fonksiyonun birinci parametresi, daha önce map edilen alanın başlangıç adresini belirtir. İkinci parametre unmap edilecek alanın uzunluğunu belirtmektedir. Fonksiyon birinci parametresinde belirtilen adresten itibaren ikinci parametresinde belirtilen miktardaki byte'ı kapsayan sayfaları unmap etmektedir. (Örneğin buradaki adres bir sayfanın ortalarında ise ve uzunluk da başka bir sayfanın ortalarına kadar geliyorsa bu iki sayfa da tümden unmap edilmektedir.) POSIX standartları işletim sistemlerinin birinci parametrede belirtilen adresin sayfa katlarında olmasını zorlayabileceğini (may require) belirtmektedir. Linux'ta birinci parametrede belirtilen adres sayfa katlarında olmak zorundadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno değişkeni uygun biçimde set edilir. munmap ile zaten map edilmemiş bir alan unmap edilmeye çalışılırsa fonksiyon bir şey yapmaz. Bu durumda fonksiyon başarısızlıkla geri dönmemektedir. Paylaşılan bellek alanına ilişkin dosya betimleyicisi close fonksiyonu ile kapatılabilir. Paylaşılan bellek alanı betimleyicisi close ile kapatıldığında munmap işlemi yapılmamaktadır. Zaten paylaşılan bellek alanı nesnesi map edildikten sonra hemen close ile kapatılabilir. Bunun mapping işlemine bir etkisi olmaz. Unmap işlemi mevcut mapping'in bir kısmına yapılabilmektedir. Bu durumda işletim sistemi mapping işlemini ardışıl olmayan parçalara kendisi ayırmaktadır. Örneğin: xxxxmmmmmmmmmxxxx Burada "m" map edilmiş sayfaları "x" ise diğer sayfaları belirtiyor olsun. Biz de mapping'in içerisinde iki sayfayı unmap edelim: xxxxmmmmxxmmmxxxx Görüldüğü gibi artık sanki iki ayrı mapping varmış gibi bir durum oluşmaktadır. Proses bittiğinde map edilmiş bütün alanlar zaten işletim sistemi tarafından unmap edilmektedir. -> Paylaşılan bellek alanı nesnesine ilişkin betimleyici close fonksiyonu ile sanki bir dosyaymış gibi kapatılır. Yukarıda da belirttiğimiz gibi bu kapatma işlemi aslında mapping işleminden hemen sonra da yapılabilir. -> Artık paylaşılan bellek alanı nesnesi shm_unlink fonksiyonu ile silinebilir. Eğer bu silme yapılmazsa sistem reboot edilene kadar nesne hayatta kalmaya devam edecektir (kernel persistant). shm_unlink fonksiyonun prototipi şöyledir: #include int shm_unlink(const char *name); Fonksiyon paylaşılan bellek alanı nesnesinin ismini alır onu yok eder. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Örneğin: if (shm_unlink(SHM_PATH) == -1) exit_sys("shm_unlink"); Aşağıdaki örnekte prog1 programı paylaşılan bellek alanına bir şeyler yazmakta prog2 programı da bunu okumaktadır. prog1 programı işlemini bitirince paylaşılan bellek alanı nesnesini shm_unlink fonksiyonuyla silmektedir. * Örnek 1, /* prog1.c */ #include #include #include #include #include #include #include #define SHM_PATH "/this_is_a_sample_shared_memory" void exit_sys(const char *msg); int main(void) { int fdshm; char *shmem; if ((fdshm = shm_open(SHM_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, 4096) == -1) exit_sys("ftruncate"); if ((shmem = (char *)mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) exit_sys("mmap"); printf("press ENTER to write shared memory...\n"); getchar(); strcpy(shmem, "this is a test..."); printf("press ENTER to exit...\n"); getchar(); if (munmap(shmem, 4096) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink(SHM_PATH) == -1) exit_sys("shm_unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #include #include #define SHM_PATH "/this_is_a_sample_shared_memory" void exit_sys(const char *msg); int main(void) { int fdshm; char *shmem; if ((fdshm = shm_open(SHM_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, 4096) == -1) exit_sys("ftruncate"); if ((shmem = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) exit_sys("mmap"); printf("press ENTER to read shared memory...\n"); getchar(); puts(shmem); printf("press ENTER to exit...\n"); getchar(); if (munmap(shmem, 4096) == -1) exit_sys("munmap"); close(fdshm); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Paylaşılan bellek alanları çok hızlı bir haberleşme sağlıyor olsa da kendi içerisinde bir senkronizasyona sahip değildir. Bir prosesin birden fazla bilgiyi farklı zamanlarda diğerine bu yöntemle iletmesi sırasında iki prosesin senkronize olması yani koordineli bir biçimde çalışması gerekir. Bu senkronizasyon problemine "üretici tüketici problemi (producer-conseumer problem)" denilmektedir. Bu konu ileride kursumuzun thread'ler kısmında ele alınacaktır. >>>> Bellek Tabanlı Dosyalar: Bellek tabanlı dosyalar (memory mapped files) 90'lı yıllarla birlikte işletim sistemlerine sokulmuştur. Microsoft ilk kez 32 bit Windows sistemlerinde (Windows NT ve sonra da Windows 95) bellek tabanlı dosyaları işletim sisteminin çekirdeğine dahil etmiştir. 90'ların ortalarında bellek tabanlı dosyalar POSIX IPC nesneleriyle birlikte UNIX türevi sistemlere de resmi olarak sokulmuştur. macOS sistemleri de bellek tabanlı dosyaları desteklemektedir. Bellek tabanlı dosyalar (memory mapped files) adeta diskte bulunan bir dosyanın prosesin sanal bellek alanına çekilmesi anlamına gelmektedir. Biz bir disk dosyasını bellek tabanlı biçimde açıp kullandığımızda dosya sanki bellekteymiş gibi bir durum oluşturulur. Biz bellekte göstericilerle dosyanın byte'larına erişiriz. Bellekte birtakım değişikler yapıldığında bu değişiklikler dosyaya yansıtılmaktadır. Böylece dosya üzerinde işlemler yapılırken read ve write sistem fonksiyonları yerine doğrudan göstericilerle bellek üzerinde işlem yapılmış olur. read fonksiyonu ile dosyanın bir kısmını okumak isteyelim: result = read(fd, buf, size); Burada genellikle işletim sistemlerinde arka planda iki işlem yapılmaktadır: Önce dosyanın ilgili bölümü işletim sisteminin çekirdeği içerisindeki bir alana (bu alana buffer cache ya da page cache denilmektedir) çekilir. Sonra bu alandan bizim belirttiğimiz alana aktarım yapılır. Halbuki bellek tabanlı dosyalarda genel olarak bu iki aktarım yerine dosya doğrudan prosesin bellek alanına map edilmektedir. Yani bu anlamda bellek tabanlı dosyalar hız ve bellek kazancı sağlamaktadır. Ayrıca her read ve write işleminin kontrol edilme zorunluluğu da bellek tabanlı dosyalarda ortadan kalkmaktadır. Bir dosya üzerinde dosyanın farklı yerlerinden okuma ve yazma işlemlerinin sürekli yapıldığı durumlarda bellek tabanlı dosyalar klasik read/write sistemine göre oldukça avantaj sağlamaktadır. Bu noktada kişilerin akıllarına şu soru gelmektedir? Biz bir dosyayı open ile açsak dosyanın tamamını read ile belleğe okusak sonra işlemleri bellek üzerinde yapsak sonra da write fonksiyonu ile tek hamlede yine onları diske yazsak bu yöntemin bellek tabanlı dosyalardan bir farkı kalır mı? Bu soruda önerilen yöntem bellek tabanlı dosya çalışmasına benzemekle birlikte bellek tabanlı dosyalar bu sorudaki çalışma biçiminden farklıdır. Birincisi, dosyanın tamamının belleğe okunması yine iki tamponun devreye girmesine yol açmaktadır. İkincisi, bellek tabanlı dosyaların bir mapping oluşturması ve dolayısıyla prosesler arasında etkin bir kullanıma yol açmasıdır. Yani örneğin iki proses aynı dosyayı bellek tabanlı olarak açtığında işletim sistemi her proses için ayrı bir alan oluşturmamakta dosyanın parçalarını fiziksel bellekte bir yere yerleştirip o proseslerin aynı yerden çalışmasını sağlamaktadır. Bir dosya bellek tabanlı (memory mapped) biçimde sırasıyla şu adımlardan geçilerek kullanılmaktadır: -> Dosya open fonksiyonuyla açılır ve bir dosya betimleyicisi elde edilir. Örneğin: int fd; ... if ((fd = open("test.txt", O_RDWR)) == -1) exit_sys("open"); İleride de belirteceğimiz gibi dosyalar bellek tabanlı olarak yaratılamamakta ve dosyalara bellek tabanlı biçimde eklemeler yapılamamaktadır. Yani zaten var olan dosyalar bellek tabanlı biçimde kullanılabilirler. -> Açılmış olan dosya mmap fonksiyonu ile prosesin sanal bellek alanına map edilir. Yani işlemler adeta önceki konuda gördüğümüz POSIX paylaşılan bellek alanlarına benzer bir biçimde yürütülmektedir. (Burada shm_open yerine open fonksiyonunun kullanıldığını varsayabilirsiniz.) Mapping işleminde genellikle shared mapping (MAP_SHARED) tercih edilir. Eğer private mapping (MAP_PRIVATE) yapılırsa mapping yapılan alana yazma yapıldığında bu dosyaya yansıtılmaz, "copy on write" mekanizması devreye girer. mmap fonksiyonun son iki parametresi dosya betimleyicisi ve dosyada bir offset belirtmektedir. İşte dosya betimleyicisi olarak açmış olduğumuz dosyanın betimleyicisini verebiliriz. Offset olarak da dosyanın neresini map edeceksek oranın başlangıç offsetini verebiliriz. Mapping sırasında dosya göstericisinin konumunun bir önemi yoktur. Örneğin: char *maddr; struct stat finfo; ... if (fstat(fd, &finfo) == -1) exit_sys("fstat"); if ((maddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); Burada biz önce dosyanın uzunluğunu fstat fonksiyonu ile elde ettik sonra da mmap fonksiyonu ile dosyanın hepsini shared mapping yaparak map ettik. Artık dosya bellektedir ve biz dosya işlemleri yerine gösterici işlemleri ile bellekteki dosyayı kullanabiliriz. Örneğin: for (off_t i = 0; i < finfo.st_size; ++i) putchar(maddr[i]); mapping işleminden sonra artık dosya betimleyicisi close fonksiyonuyla kapatılabilir. Yani kapatım için unmap işleminin beklenmesine gerek yoktur. -> Tıpkı POSIX paylaşılan bellek alanlarında olduğu gibi işimiz bittikten sonra yapılan mapping işlemini munmap fonksiyonu ile serbest bırakabiliriz. Eğer bu işlemi yapmazsak proses sonlandığında zaten map edilmiş alanlar otomatik olarak unmap edilecektir. Örneğin: if (munmap(maddr, finfo.st_size) == -1) exit_sys("munmap"); -> Nihayet dosya betimleyicisi close fonksiyonuyla kapatılabilir. Yukarıda da belirttiğimiz gibi aslında map işlemi yapıldıktan sonra hemen de close fonksiyonu ile dosya betimleyicisini kapatabilirdik. Örneğin: close(fd); Aşağıdaki örnekte komut satırından alınan dosya bellek tabanlı biçimde açılmış ve dosyanın içindekiler ekrana (stdout dosyasına) yazdırılmıştır. Aynı zamanda dosyanın başındaki ilk 6 karakter değiştirilmiştir. Programı çalıştırırken dosyanın başındaki ilk 6 karakterin bozulacağına dikkat ediniz. * Örnek 1, #include #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; struct stat finfo; char *fmaddr; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDWR)) == -1) exit_sys("open"); if (fstat(fd, &finfo) == -1) exit_sys("fstat"); if ((fmaddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); for (off_t i = 0; i < finfo.st_size; ++i) putchar(fmaddr[i]); putchar('\n'); memcpy(fmaddr, "XXXXXX", 6); if (munmap(fmaddr, finfo.st_size) == -1) exit_sys("munmap"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } UNIX/Linux sistemlerindeki bellek tabanlı dosyaları (memory mapped files) açarken ve kullanırken bazı ayrıntılara dikkat edilmesi gerekir. Burada bu ayrıntılar üzerinde duracağız. -> Dosyayı bizim mmap fonksiyonundaki sayfa koruma özelliklerine uygun açmamız gerekmektedir. Örneğin biz dosyayı O_RDONLY modunda açıp buna ilişkin sayfaları mmap fonksiyonunda PROT_READ|PROT_WRITE olarak belirlersek mmap başarısız olacak ve errno EACCESS değeri ile set edilecektir. Eğer biz dosyayı O_RDWR modunda açtığımız halde mmap fonksiyonunda yalnızca PROT_READ kullanırsak bu durumda dosyaya yazma hakkımız olsa da sayfa özellikleri "read only" olduğu için o bellek bölgesine yazma yapılırken program SIGSEGV sinyali ile çökecektir. -> Bellek tabanlı dosyaların O_WRONLY modunda açılması probleme yol açabilmektedir. Çünkü böyle açılmış olan bir dosyanın mmap fonksiyonunda PROT_WRITE olarak map edilmesi gerekir. Halbuki Intel gibi bazı işlemcilerde PROT_WRITE zaten aynı zamanda okuma izni anlamına da gelmektedir. Yani örneğin Intel'de PROT_READ diye bir sayfa özelliği yoktur. PROT_WRITE aslında PROT_READ|PROT_WRITE anlamına gelmektedir. Dolayısıyla biz dosyayı O_WRONLY modunda açıp mmap fonksiyonunda PROT_WRITE özelliğini belirtirsek bu PROT_WRITE aynı zamanda okuma izni anlamına da geldiği için mmap başarısız olacak ve errno EACCESS değeri ile set edilecektir. POSIX standartlarında da bellek tabanlı dosyaların (aynı durum shm_open için de geçerli) açılırken "read" özelliğinin olması gerektiği belirtilmiştir. Yani POSIX standartları da bellek tabanlı dosyaların O_WRONLY modda açılamayacağını açılırsa mmap fonksiyonun başarısız olacağını ve errno değerinin EACCESS olarak set edileceğini belirtmektedir. -> Bir dosyanın uzunluğu 0 ise biz mmap fonksiyonunda length parametresini 0 yapamayız. Fonksiyon doğrudan başarısızlıkla sonlanıp errno değeri EINVAL olarak set edilmektedir. Yani 0 uzunlukta bir dosya map edilememektedir. -> Anımsanacağı gibi mmap fonksiyonunun offset parametresi Linux sistemlerinde sayfa uzunluğunun katlarında olması gerekiyordu (POSIX bunu "may require" biçimde belirtmiştir). Yani Linux'ta biz dosyayı zaten sayfa katlarından itibaren map edebilmekteyiz. Bu durumda Linux'ta zaten map edilen adres sayfanın başında olmaktadır. -> Biz normal bir dosyayı büyütmek için dosya göstericisini EOF durumuna çekip yazma yapıyorduk. Ya da benzer işlemi truncate, ftruncate fonksiyonlarıyla da yapabiliyorduk. Halbuki bellek tabanlı olarak açılmış olan dosyalar bellek üzerinde hiçbir biçimde büyütülememektedir. Biz bir dosyayı mmap fonksiyonu ile dosya uzunluğundan daha fazla uzunlukta map edebiliriz. Örneğin dosya 10000 byte uzunlukta olduğu halde biz dosyayı 20000 byte olarak map edebiliriz. Bu durumda dosyanın sonundan o sayfanın sonuna kadarki alana biz istediğimiz gibi erişebiliriz. Dosyanın uzunluğu 10000 byte ise dosyanın son sayfasında dosyaya dahil olmayan 2288 byte bulunacaktır (3 * 4096 - 10000). İşte bizim bu son sayfadaki 2288 byte'a erişmemizde hiçbir sakınca yoktur. Ancak bu sayfanın ötesinde erişim yapamayız. Yani bizim artık 3 * 4096 = 12288'den 20000'e kadarki alana erişmeye çalışmamamız gerekir. Eğer bu alana erişmeye çalışırsak SIGBUS sinyali oluşur ve prosesimiz sonlandırılır. Pekiyi bir dosyanın uzunluğundan fazla yerin map edilmesinin bir anlamı olabilir mi? Daha önceden de belirttiğimiz gibi bellek tabanlı dosyalar bellek üzerinde büyütülemezler. Yani biz dosyayı uzunluğunun ötesinde map ederek oraya yazma yapmak suretiyle dosyayı büyütemeyiz. Ancak dosyalar dışarıdan büyütülebilmektedir. Bellek tabanlı dosyaların çalışma mekanizmasının iyi anlaşılması için öncelikle dosya işlemlerinde read ve write işlemlerinin nasıl yapıldığı hakkında bilgi sahibi olunması gerekmektedir. Biz write POSIX fonksiyonuyla dosyaya bir yazma yaptığımızda write fonksiyonu genellikle doğrudan bu işlemi yapan bir sistem fonksiyonunu çağırmaktadır. Örneğin Linux sistemlerinde write fonksiyonu, prosesi kernel moda geçirerek doğrudan sys_write isimli sistem fonksiyonunu çağırır. Pekiyi bu sys_write sistem fonksiyonu ne yapmaktadır? Genellikle işletim sistemlerinde dosyaya yazma yapan sistem fonksiyonları hemen yazma işlemini diske yapmazlar. Önce kernel içerisindeki bir tampona yazma yaparlar. Bu tampona Linux sistemlerinde eskiden "buffer cache" denirdi. Sonradan sistem biraz değiştirildi ve "page cache" denilmeye başlandı. İşte bu tampon sistemi işletim sisteminin bir "kernel thread'i" tarafından belli periyotlarla diske flush edilmektedir. Yani biz diske write fonksiyonu ile yazma yaptığımızda aslında bu yazılanlar önce kernel içerisindeki bir tampona (Linux'ta page cache) yazılmakta ve işletim sisteminin bağımsız çalışan başka bir akışı tarafından çok bekletilmeden bu tamponlar diske flush edilmektedir. Pekiyi neden write fonksiyonu doğrudan diske yazmak yerine önce bir tampona (page cache) yazmaktadır? İşte bunun amacı performansın artırılmasıdır. Bu konuya genel olarak "IO çizelgelemesi (IO scheduling)" denilmektedir. IO çizelgelemesi diske yazılacak ya da diskten okunacak bilgilerin bazılarının bir araya getirilerek belli bir sırada işleme sokulması anlamına gelmektedir. (Örneğin biz dosyaya peşi sıra birkaç write işlemi yapmış olalım. Bu birkaç write işlemi aslında kernel içerisindeki page cache'e yapılacak ve bu page cache'teki sayfa tek hamlede işletim sistemi tarafından diske flush edilecektir.) Tabii işletim sisteminin arka planda bu tamponları flush eden kernel thread'i çok fazla beklemeden bu işi yapmaya çalışmaktadır. Aksi takdirde elektrik kesilmesi gibi durumlarda bilgi kayıpları daha yüksek düzeyde olabilmektedir. Pekiyi biz write fonksiyonu ile yazma yaptığımızda mademki yazılanlar hemen diskteki dosyaya aktarılmıyor o halde başka bir proses tam bu işlemden hemen sonra open fonksiyonu ile dosyayı açıp ilgili yerden okuma yapsa bizim en son yazdıklarımızı okuyabilecek midir? POSIX standartlarına göre write fonksiyonu geri döndüğünde artık aynı dosyadan bir sonraki read işlemi ne olursa olsun write yapılan bilgiyi okumalıdır. İşte işletim sistemleri zaten bir dosya açıldığında read işleminde de write işleminin kullandığı aynı tamponu kullanmaktadır. Bu tasarıma "unified file system" da denilmektedir. Bu tasarımdan dolayı zaten ilgili dosya üzerinde işlem yapan her proses işletim sistemi içerisindeki aynı tamponları kullanmaktadır. Yani işletim sisteminin sistem fonksiyonları önce bu tamponlara bakmaktadır. Dolayısıyla bu tamponların o anda flush edilip edilmediğinin bir önemi kalmamaktadır. (Tabii bir proses işletim sistemini bypass edip doğrudan disk sektörlerine erişirse bu durumda gerçekten henüz write fonksiyonu ile yazılanların dosyaya yazılmamış olduğunu görebilir.) Pekiyi biz bir dosyayı bellek tabanlı olarak açarak o bellek alanını güncellediğimizde oradaki güncellemeler başka prosesler tarafından read işlemi sırasında görülecek midir? Ya da tam tersi olarak başka prosesler dosyaya write yaptığında bizim map ettiğimiz bellek otomatik bu yazılanları görecek midir? İşte POSIX standartları bunun garantisini vermemiştir. POSIX standartlarında bellek tabanlı dosyanın bellek içeriğinde değişiklik yapıldığında bu değişikliğin diğer prosesler tarafından görülebilmesi için ya da diğer proseslerin yaptığı write işleminin bellek tabanlı dosyanın bellek alanına yansıtılabilmesi için msync isimli bir POSIX fonksiyonunun çağrılması gerekmektedir. Her ne kadar POSIX standartları bu msync fonksiyonunun çağrılması gerektiğini belirtiyorsa da Linux gibi pek çok UNIX türevi sistem "unified file system" tasarımı nedeniyle aslında msync çağrısına gereksinim duymamaktadır. Örneğin Linux'ta biz bir bellek tabanlı dosyayı map ettiğimizde aslında sayfa tablosunda bizim map ettiğimiz kısım doğrudan zaten işletim sisteminin tamponunu (page cache) göstermektedir. Yani zaten Linux sistemlerinde bütün prosesler dosya işlemlerinde önce bu page cahche'e bakmaktadır. Dolayısıyla biz bellek tabanlı dosyanın bellekteki alanına yazma yaptığımızda diğer prosesler dosyayı bellek tabanlı açmamış olsa bile page cache olarak aynı alana baçvuracaklarından dolayı bizim yazdıklarımızı hemen görecektir. Benzer biçimde başka bir proses dosyaya yazma yaptığında da aslında aynı tampona (page cache) yazma yapmaktadır. Ancak ne olursa olsun taşınabilir programların bu msync fonksiyonunu aşağıda belirteceğimiz biçimde çağırması gerekmektedir. * Örnek 1, Aşağıdaki örnekte "sample.c" programı bir dosyayı bellek tabanlı olarak açıp onun başına bir şeyler yazıp beklemektedir. "mample.c" programı ise bir dosyanın ilk 32 karakterini ekrana (stdout dosyasına) yazdırmaktadır. Bu örnekten amaç Linux sistemlerinde hiç msync çağırması yapılmadan bir bir prosesin bellek tabanlı dosyanın bellekteki alanına yazma yaptığında diğer bir prosesin dosyayı bellek tabanlı açmasa bile o değişikliği gördüğünün ispat edilmesidir. Programları "test.txt" gibi örnek bir dosya oluşturup onun üzerinde farklı terminallerden çalıştırarak deneyebilirsiniz. Şöyleki: ./sample test.txt ./mample test.txt Programın kodları aşağıdaki gibidir: /* sample.c */ #include #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; struct stat finfo; char *fmaddr; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDWR)) == -1) exit_sys("open"); if (fstat(fd, &finfo) == -1) exit_sys("fstat"); if ((fmaddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); printf("press ENTER to write...\n"); getchar(); memcpy(fmaddr, "XXXXXX", 6); printf("press ENTER to exit...\n"); getchar(); if (munmap(fmaddr, finfo.st_size) == -1) exit_sys("munmap"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* mample.c */ #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; char buf[32 + 1]; ssize_t result; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDONLY)) == -1) exit_sys("open"); if ((result = read(fd, buf, 32)) == -1) exit_sys("read"); for (ssize_t i = 0; i < result; ++i) putchar(buf[i]); putchar('\n'); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Yukarıda da belirttiğimiz gibi her ne kadar Linux gibi "unified file system" tasarımını kullanan işletim sistemlerinde msync fonksiyonu gerekmiyorsa da bellek tabanlı dosyada yapılan değişikliklerin diskteki dosyaya yansıtılması, diskteki dosyada yapılan değişikliklerin bellek tabanlı dosyanın bellek alanına yansıtılması için msync isimli POSIX fonksiyonun çağrılması gerekmektedir. msync fonksiyonunun prototipi şöyledir: #include int msync(void *addr, size_t len, int flags); Fonksiyonun birinci parametresi flush edilecek bellek tabanlı dosyanın bellek adresini, ikinci parametresi bunun uzunluğunu belirtmektedir. POSIX standartlarına göre birinci parametrede belirtilen adresin "sayfa katlarında olması zorunlu değildir, ancak işletim sistemi bunu zorunlu yapabilir (may require)". Linux sistemlerinde bu adresin sayfa katlarında olması zorunlu tutulmuştur. Fonksiyonun ikinci parametresi flush edilecek byte miktarını belirtmektedir. Burada belirtilen byte miktarı ve girilen adresi kapsayan tüm sayfalar işleme sokulmaktadır. (Örneğin birinci parametrede belirtilen adres sayfa katlarında olsun. Biz ikinci parametre için 7000 girsek sayfa uzunluğu 4K ise sanki 8192 girmiş gibi etki oluşacaktır.) Fonksiyonun son parametresi flush işleminin yönünü belirtmektedir. Bu parametre aşağıdaki bayraklardan yalnızca birini alabilir: -> MS_SYNC: Burada yön bellekten diske doğrudur. Yani biz bellek tabanlı dosyanın bellek alanında değişiklik yaptığımızda bunun diskteki dosyaya yansıtılabilmesi için MS_SYNC kullanabiliriz. Bu bayrak aynı zamanda msync fonksiyonu geri döndüğünde flush işleminin bittiğinin garanti edilmesini sağlamaktadır. Yani bu bayrağı kullandığımızda msync flush işlemi bitince geri dönmektedir. -> MS_ASYNC: MS_SYNC bayrağı gibidir. Ancak bu bayrakta flush işlemi başlatılıp msync fonksiyonu hemen geri dönmektedir. Yani bu bayrakta msync geri döndüğünde flush işlemi başlatılmıştır ancak bitmiş olmak zorunda değildir. -> MS_INVALIDATE: Buradaki yön diskten belleğe doğrudur. Yani başka bir proses diskteki dosyayı güncellendiğinde bu güncellemenin bellek tabanlı dosyanın bellek alanına yansıtılması sağlanmaktadır. munmap işlemi ile bellek tabanlı dosyanın bellek alanı unmap edilirken zaten msync işlemi yapılmaktadır. Benzer biçimde proses munmap yapmadan sonlanmış olsa bile sonlanma sırasında munmap işlemi işletim sistemi tarafından yapılmakta ve bu flush işlemi de gerçekleştirilmektedir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Bu durumda biz POSIX standartlarına uygunluk bakımından örneğin bir bellek tabanlı dosyanın bellek alanına bir şeyler yazdığımızda o alanın flush edilmesi için MS_SYNC ya da MS_ASYNC bayraklarıyla msync çağrısını yapmamız gerekir: if ((maddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); memcpy(maddr, "ankara", 6); if (msync(maddr, finfo.st_size, MS_SYNC) == -1) /* bellekteki değişiklikler diske yansıtılıyor */ exit_sys("msync"); Yine POSIX standartlarına uygunluk bakımından dışarıdan bir prosesin bellek tabanlı dosyada değişiklik yapması durumunda onun bellek tabanlı dosyanın bellek alanına yansıtılabilmesi için MS_INVALIDATE bayrağı ile msync fonksiyonunun çağrılması gerekmektedir. Örneğin: if ((maddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); /* başka bir proses dosya üzerinde değişiklik yapmış olsun */ if (msync(maddr, finfo.st_size, MS_INVALIDATE) == -1) /* diskteki değişiklikler belleğe yansıtılıyor */ exit_sys("msync"); msync fonksiyonunda yalnızca tek bir bayrak kullanılabilmektedir. Bu nedenle iki işlemi MS_SYNC|MS_INVALIDATE biçiminde birlikte yapmaya çalışmayınız. * Örnek 1, Aşağıda msync fonksiyonunun kullanımına ilişkin bir örnek verilmiştir. #include #include #include #include #include #include #include void exit_sys(const char *msg); int main(int argc, char *argv[]) { int fd; struct stat finfo; char *fmaddr; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } if ((fd = open(argv[1], O_RDWR)) == -1) exit_sys("open"); if (fstat(fd, &finfo) == -1) exit_sys("fstat"); if ((fmaddr = (char *)mmap(NULL, finfo.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) exit_sys("mmap"); printf("press ENTER to write...\n"); getchar(); memcpy(fmaddr, "XXXXXX", 6); if (msync(fmaddr, 4096, MS_SYNC) == -1) exit_sys("msync"); if (munmap(fmaddr, finfo.st_size) == -1) exit_sys("munmap"); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>>> Mesaj Kuyrukları : Diğer bir proseslerarası haberleşme yöntemi de "mesaj kuyrukları (message queues)" denilen yöntemdir. Mesaj kuyrukları UNIX/Linux (ve macOS) sistemlerinde kullanılan bir yöntemdir. UNIX/Linux sistemlerinde mesaj kuyrukları tıpkı paylaşaılan bellek alanlarında olduğu gibi iki farklı fonksiyon grubuyla kullanılabilmektedir. Eski mesaj kuyruklarına genellikle "Sistem 5 mesaj kuyrukları" denilmektedir. Modern mesaj kuyruklarına POSIX mesaj kuyrukları denir. POSIX mesaj kuyrukları 90'lı yılların ortalarında tasarlanmıştir. Tabii aslında her iki grup fonksiyon da POSIX standartlarında bulunmaktadır. Sistem 5 mesaj kuyrukları çok eskiden beri var olduğu için geniş bir taşınabilirliğe sahiptir. Bugün programcılar her iki mesaj kuyruklarını da kullanmaktadır. Biz kursumuzda yalnızca POSIX mesaj kuyruklarını göreceğiz. POSIX mesaj kuyrukları şöyle kullanılmaktadır: -> İki proses de (daha fazla proses de olabilir) mesaj kuyruğunu mq_open fonksiyonuyla ortak bir isim altında anlaşarak açar. mq_open fonksiyonunun prototipi şöyledir: #include mqd_t mq_open(const char *name, int oflag, ...); Fonksiyon ya iki argümanla ya da dört argümanla çağrılmaktadır. Eğer mesaj kuyruğu zaten varsa fonksiyon iki argümanla çağrılır. Ancak mesaj kuyruğunun yaratılması gibi bir durum söz konusu ise fonksiyon dört argümanla çağrılır. Eğer mesaj kuyruğunun yaratılması söz konusu ise son iki parametreye sırasıyla IPC nesnesinin erişim hakları ve "özellikleri (attribute)" girilmelidir. Yani mesaj kuyruğu yaratılacaksa adeta fonksiyonun parametrik yapısının aşağıdaki gibi olduğu varsayılmalıdır: mqd_t mq_open(const char *name, int oflag, mode_t mode, const struct mq_attr *attr); Fonksiyonun birinci parametresi IPC nesnesinin kök dizindeki dosya ismi gibi uydurulmuş olan ismini belirtir. İkinci parametre açış bayraklarını belirtmektedir. Burada open fonksiyonundaki bayrakların bazıları kullanılmaktadır. Açış bayrakları aşağıdakilerden yalnızca birini içermek zorundadır: O_RDONLY O_WRONLY O_RDWR Açış bayraklarına aşağıdaki değerlerin bir ya da birden fazlası da bit OR işlemiyle eklenebilir: O_CREAT O_EXCL O_NONBLOCK O_CREAT bayrağı yine "yoksa yarat, varsa olanı aç" anlamına gelmektedir. O_EXCL yine O_CREAT birlikte kullanılabilir. Eğer nesne zaten varsa bu durumda fonksiyonun başarısız olmasını sağlar. O_NONBLOCK blokesiz okuma-yazma yapmak için kullanılmaktadır. Eğer açış bayrağında O_CREAT belirtilmişse bu durumda programcının fonksiyona iki argüman daha girmesi gerekir. Tabii eğer nesne varsa bu iki argüman zaten kullanılmayacaktır. Yani bu argüman IPC nesnesi yaratılacaksa (yoksa) kullanılmaktadır. Mesaj kuyruğu yaratılırken erişim haklarını tıpkı dosyalarda olduğu gibi kuyruğu yaratan kişi S_IXXX sembolik sabitleriyle (ya da 2008 sonrasında doğrudan sayısal biçimde) vermelidir. Eğer mesaj kuyruğu yaratılacaksa son parametre mq_attr isimli yapı türünden bir nesnenin adresi biçiminde girilmelidir. mq_attr yapısı şöyle bildirilmiştir: struct mq_attr { long mq_flags; /* Flags: 0 or O_NONBLOCK */ long mq_maxmsg; /* Max. # of messages on queue */ long mq_msgsize; /* Max. message size (bytes) */ long mq_curmsgs; /* # of messages currently in queue */ }; Yapının mq_flags parametresi yalnızca O_NONBLOCK içerebilir. max_msg elemanı kuyruktaki tutulacak maksimum mesaj sayısını belirtmektedir. Yapının mq_msgsize elemanı bir mesajın maksimum uzunluğunu belirtmektedir. mq_curmsgs elemanı ise o anda kuyruktaki mesaj sayısını belirtmektedir. Programcı mesaj kuyruğunu yaratırken yapının mq_maxmsg ve mq_msgsize elemanlarına uygun değerler girip mesaj kuyruğunun istediği gibi yaratılmasını sağlayabilir. Yani mesaj kuyruğu yaratılırken programcı mq_attr yapısının yalnızca mq_maxmsg ve mq_msgsize elemanlarını doldurur. Yapının diğer elemanları mq_open tarafından dikkate alınmamaktadır. Ancak bu özellik parametresi NULL adres biçiminde de geçilebilir. Bu durumda mesaj kuyruğu default değerlerle yaratılır. Bu default değerler değişik sistemlerde değişik biçimlerde olabilir. Linux sistemlerinde genel olarak default durumda maksimum mesaj mq_maxmsg değeri 10, mq_msgsize değeri ise 8192 alınmaktadır. mq_open fonksiyonunda kuyruk özelliklerini girerken mq_maxmsg ve mq_msgsize elemanlarına girilecek değerler için işletim sistemleri alt ve üst limit belirlemiş olabilirler. Eğer yapının bu elemanları bu limitleri aşarsa mq_open fonksiyonu başarısız olur ve errno değişkeni EINVAL olarak set edilir. Örneğin Linux sistemlerinde sıradan prosesler (yani root olmayan prosesler) mq_maxmsg değerini 10'un yukarısına çıkartamamaktadır. Ancak uygun önceliğe sahip prosesler bu değeri 10'un yukarısında belirleyebilmektedir. Ancak POSIX standartları bu default limitler hakkında bir şey söylememiştir. Linux sistemlerinde sonraki paragraflarda açıklanacağı gibi bu limitler proc dosya sisteminden elde edilebilmektedir. mq_open fonksiyonu başarı durumunda yaratılan mesaj kuyruğunu temsil eden betimleyici değeriyle, başarısızlık durumunda -1 değeriyle geri dönmektedir. Fonksiyonun geri döndürdüğü "mesaj kuyruğu betimleyicisi (message queue descriptor)" diğer fonksiyonlarda bir handle değeri gibi kullanılmaktadır. Linux çekirdeği aslında mesaj kuyruklarını tamamen birer dosya gibi ele almaktadır. Yani mq_open fonksiyonu Linux sistemlerinde dosya betimleyici tablosunda bir betimleyici tahsis edip ona geri dönmektedir. Ancak POSIX standartları, fonksiyonun geri dönüş değerini mqd_t türüyle temsil etmiştir. Bu durum değişik çekirdeklerde mesaj kuyruklarının dosya sisteminin dışında başka biçimlerde de gerçekleştirilebileceği anlamına gelmektedir. POSIX standartlarına göre mqd_t herhangi bir tür olarak (yapı da dahil olmak üzere) dosyasında typedef edilebilir. -> POSIX mesaj kuyruğuna mesaj yollamak için mq_send fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio); Fonksiyonun birinci parametresi mesaj kuyruğunun betimleyicisini belirtir. Fonksiyonun ikinci parametresi mesajın bulunduğu dizin'in adresini almaktadır. Ancak bu parametrenin void bir adres olmadığına dikkat ediniz. Eğer mesaj başka türlere ilişkinse tür dönüştürmesinin yapılması gerekmektedir. Üçüncü parametre gönderilecek mesajın uzunluğunu belirtir. Dördüncü parametre mesajın öncelik derecesini belirtmektedir. Bu öncelik derecesi >= 0 bir değer olarak girilmelidir. POSIX mesaj kuyruklarında öncelik derecesi yüksek olan mesajlar FIFO sırasına göre önce alınmaktadır. Bu mesaj kuyruklarının klasik Sistem 5 mesaj kuyruklarında olduğu gibi belli bir öncelik derecesine sahip mesajları alabilme yeteneği yoktur. Buradaki öncelik derecesinin içerisindeki MQ_PRIO_MAX değerinden küçük olması gerekmektedir. Bu değer ise işletim sistemlerini yazanlar tarafından belirlenmektedir. Ancak bu değer _POSIX_MQ_PRIO_MAX (32) değerinden düşük olamaz. Yani başka bir deyişle buradaki desteklenen değer 32'den küçük olamamaktadır. (Mevcut Linux sistemlerinde bu değer 32768 biçimindedir.) Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner ve errno değişkeni uygun biçimde set edilir. POSIX mesaj kuyruklarının da izleyen paragraflarda açıklanacağı üzere belli limitleri vardır. Eğer mesaj kuyruğu dolarsa mq_send fonksiyonu bloke olmaktadır. Ancak açış sırasında O_NONBLOCK bayrağı belirtilmişse mq_send kuyruk doluysa bloke olmaz. Kuyruğa hiçbir şey yazmadan başarısızlıkla (-1 değeriyle) geri döner ve errno EAGAIN değeri ile set edilir. Örneğin: for (int i = 0; i < 100; ++i) { if (mq_send(mqdes, (const char *)&i, sizeof(int), 0) == -1) exit_sys("mq_send"); } Burada 0'dan 100'e kadar 100 tane int değer mesaj kuyruğuna mesaj olarak yazılmıştır. Mesaj kuyruklarının kendi içerisinde bir senkronizasyon da içerdiğine dikkat ediniz. Kuyruğa yazan taraf kuyruk dolarsa (Linux'taki default değerin 10 olduğunu anımsayınız) blokede beklemektedir. Ta ki diğer taraf kuyruktan mesajı alıp kuyrukta yer açana kadar. -> POSIX mesaj kuyruklarından mesaj almak için mq_receive fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio); Fonksiyonun birinci parametresi mq_open fonksiyonundan elde edilen mesaj kuyruğu betimleyicisidir. İkinci parametre mesajın yerleştirileceği adresi belirtmektedir. Yine bu parametrenin void bir gösterici olmadığına char türden bir gösterici olduğuna dikkat ediniz. Yani char türünden farklı bir adres buraya geçilecekse tür dönüştürmesi uygulanmalıdır. Üçüncü parametre, ikinci parametredeki mesajın yerleştirileceği alanın uzunluğunu belirtir. Ancak dikkat edilmesi gereken nokta buradaki uzunluk değerinin mesaj kuyruğundaki mq_msgsize değerinden küçük olmaması gerektiğidir. Eğer bu parametreye girilen değer mesaj kuyruğuna ilişkin mq_msgsize değerinden küçük ise fonksiyon hemen başarısız olmaktadır. Bu durumda errno değişkeni EMSGSIZE değeri ile set edilmektedir. Pekiyi bu değeri mq_receive fonksiyonunu uygulayacak programcı nasıl bilecektir? Eğer kuyruğu kendisi yaratmışsa ve yaratım sırasında mq_attr parametresiyle özellik belirtmişse programcı zaten bunu biliyor durumdadır. Ancak genellikle mq_receive fonksiyonunu kullanan programcılar bunu bilmezler. Çünkü genellikle kuyruk mq_receive yapan programcı tarafından yaratılmamıştır ya da kuyruk default özelliklerle yaratılmıştır. Bu durumda mecburen programcı mq_getattr fonksiyonu ile bu bilgiyi elde etmek zorunda kalır. Tabii bu işlem programın çalışma zamanında yapıldığına göre programcının mesajın yerleştirileceği alanı da malloc fonksiyonu ile dinamik bir biçimde tahsis etmesi gerekmektedir. mq_receive fonksiyonun son parametresi kuyruktan alınan mesajın öncelik derecesinin yerleştirileceği unsigned int türden nesnenin adresini almaktadır. Ancak bu parametre NULL adres biçiminde geçilebilir. Bu durumda fonksiyon mesajın öncelik derecesini yerleştirmez. Fonksiyon başarı durumunda kuyruktaki mesajın uzunluğu ile, başarısızlık durumunda -1 ile geri dönmektedir ve errno değişkeni uygun biçimde set edilmektedir. Örneğin: char buf[65536]; ... if (mq_receive(mqdes, buf, 65536, NULL) == -1) exit_sys("mq_receive"); Burada biz mesajın önceliğini almak istemedik. Bu nedenle son parametreye NULL adres geçtik. Tampon uzunluğunu öylesine büyük bir değer olarak uydurduk. Aslında yukarıda da belirttiğimiz gibi mq_receive uyguladığımız noktada bizim tampon uzunluğunu biliyor durumda olmamız gerekir. -> Pekiyi POSIX mesaj kuyruklarında mesaj haberleşmesi nasıl sonlandırılacaktır? Burada karşı taraf betimleyiciyi kapattığında diğer taraf bunu anlayamamaktadır. O halde heberleşmenin sonlanması için gönderen tarafın özel bir mesajı göndermesi ya da 0 uzunlukta bir mesajı göndermesi gerekir. Eğer 0 uzunluklu mesaj gönderilirse alan tarafta mq_receive fonksiyonu 0 ile geri dönecek ve alan taraf haberleşmenin bittiğini anlayabilecektir. -> POSIX mesaj kuyruğu ile işlemler bitince programcı mesaj kuyruğunu mq_close fonksiyonu ile kapatmalıdır. Fonksiyonun prototipi şöyledir: #include int mq_close(mqd_t mqdes); Fonksiyon parametre olarak mesaj kuyruğu betimleyicicisini alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. Programcı her şeyi doğru yaptığına inanıyorsa başarının kontrol edilmesine gerek yoktur. Tabii eğer programcı mq_close fonksiyonunu hiç kullanmazsa proses bittiğinde otomatik olarak betimleyici kapatılmaktadır. -> POSIX mesaj kuyrukları mq_unlink fonksiyonu ile silinmektedir. Tabii yukarıda da belirttiğimiz gibi mesaj kuyruğu açıkça silinmezse reboot edilene kadar (kernel persistant) yaşamaya ve içerisindeki mesajları tutmaya devam etmektedir. Bir POSIX mesaj kuyruğu mq_unlink fonksiyonu ile silindiğinde halen mesaj kuyruğunu kullanan programlar varsa onlar kullanmaya devam ederler. Mesaj kuyruğu gerçek anlamda son mesaj kuyruğu betimleyicisi kapatıldığında yok edilmektedir. mq_unlink fonksiyonunun prototipi şöyledir: #include int mq_unlink(const char *name); Fonksiyon mesaj kuyruğunun ismini alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. Mesaj kuyruğunu, kuyruğu yaratan tarafın silmesi en normal durumdur. Ancak kuyruğu kimin yarattığı bilinmiyorsa taraflardan biri kuyruğu silebilir. * Örnek 1, Aşağıda tipik bir POSIX mesaj kuyruğu örneği verilmiştir. Bu örnekte "prog1" programı stdin dosyasındna okuduklaırnı mesaj kuyruğuna yazmaktadır. "prog2" programı da mesaj kuyruğdan mesajları alarak stdout dosyasına yazmaktadır. "prog1" programı "quit" mesajını kuyruğa yazdıktan sonra işlemini sonlandırmaktadır. Benzer biçimde "prog2" programı da bu mesajı aldıktan sonra işlemini sonlandırır. /* prog1.c */ #include #include #include #include #define MESSAGE_QUEUE_NAME "/TestMessageQueue" #define BUFFER_SIZE 8192 void exit_sys(const char *msg); int main(void) { mqd_t mqd; char buf[BUFFER_SIZE]; char *str; if ((mqd = mq_open(MESSAGE_QUEUE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, NULL)) == -1) exit_sys("mq_open"); for (;;) { printf("Message:"); if (fgets(buf, BUFFER_SIZE, stdin) == NULL) continue; if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (mq_send(mqd, buf, strlen(buf), 0) == -1) exit_sys("mq_send"); if (!strcmp(buf, "quit")) break; } mq_close(mqd); if (mq_unlink(MESSAGE_QUEUE_NAME) == -1) exit_sys("mq_unlink"); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #include #define MESSAGE_QUEUE_NAME "/TestMessageQueue" #define BUFFER_SIZE 8192 void exit_sys(const char *msg); int main(void) { mqd_t mqd; char buf[BUFFER_SIZE]; ssize_t result; if ((mqd = mq_open(MESSAGE_QUEUE_NAME, O_RDONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, NULL)) == -1) exit_sys("mq_open"); for (;;) { if ((result = mq_receive(mqd, buf, BUFFER_SIZE, NULL)) == -1) exit_sys("mq_receive"); buf[result] = '\0'; if (!strcmp(buf, "quit")) break; printf("%s\n", buf); } mq_close(mqd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } >>> Windows Sistemlerinde: >>>> Boru Haberleşmesi : UNIX/Linux sistemindekilere benzer bir biçimde yapılmaktadır. Bu sistemlerde de borular "isimsiz (anonymous)" ve "isimli (named)" olmak üzere ikiye ayrılmaktadır. >>>>> İsimli Boru Haberleşmesi : İki farklı Windows makinelerindeki proseslerin haberleşmesi için de kullanılabilmektedir. Bu nedenle Windows sistemlerindeki isimli borular bazı ayrıntılara sahiptir. Biz bu kursumuzda Windows sistemlerindeki isimli borular üzerinde durmayacağız. Bu konu "Windows Sistem Programlama" kurslarında ele alınmaktadır. >>>>> İsimsiz Boru Haberleşmesi : İsimsiz borular yine üst ve alt proseslerin haberleşmesinde kullanılmaktadır. Bu sistemlerde fork fonksiyonun olmadığını CreateProcess API fonksiyonunun adresta fork/exec ikilisi gibi bir işleve sahip olduğunu anımsayınız. Dolayısıyla bu sistemlerde bizim alt prosese boruların handle değerlerini de geçirmemiz gerekir. Windows sistemlerinde üst ve alt prosesler arasında isimsiz boru haberleşmesi tipik olarak şu adımlardan geçilerek gerçekleştirilmektedir: -> Üst proses boruyu CreatePipe isimli API fonksiyonuyla yaratır. Fonksiyonun prototipi şöyledir: BOOL CreatePipe( PHANDLE hReadPipe, PHANDLE hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize ); Fonksiyonun birinci ve ikinci parametresi boruya yazma yapmakta kullanılacak ve borudan okuma yapmakta kullanılacak HANDLE değerlerinin yerleştirileceği nesnelerin adreslerini almaktadır. Windows sistemlerinde borular çift yönlüdür. Fonksiyonun üçüncü parametresi yaratılan boru nesnesinin güvenlik bilgilerini içermektedir. Bu patametre NULL geçilebilir. Son parametre borunun byte cinsinden uzunluğunu belirtmektedir. Bu parametre için girilecek değer yalnızca bir ipucu (hint) anlamındadır. Bu parametreye 0 girilirse bu durumda boru default uzunlukla yaratılmaktadır. Fonksiyon başarı durumunda sıfır dışı bir değre, başarısızlık durumunda sıfır değerine geri dönmektedir. -> Borudan okuma için ReadFile fonksiyonu, boruya yazma için WriteFile API fonksiyonu kullanılmaktadır. Bu fonksiyonları zaten daha önce görmüştük. ReadFile fonksiyonu ile borudan okuma yapılmak istendiğinde eğer boruda hiçbir bte yoksa ReadFile blokede bekler. Benzer biçimde WriteFile fonksiyonu ile boruya yazma yapılırken eğer boruda yazılmak istenen kadar boş yer yoksa bu bilgilerin hepsinin yazılabilmesi için gerekli alan açılana kadar WriteFile bloke oluşturmaktadır. Buradaki davranış UNIX/Linux sistemlerindeki write ve read fonksiyonlaırnın davranışların gibidir. Yine haberleşme yazan tarafın boruyu CloseHandle fonksiyonu ile kapatmasıyla sonlandırılmalıdır. Bu durumda okuyan taraf önce boruda kalanları okur, sonra ReadFile başarısız olarak 0 ile geri döner. Okuyan taraf da boruyu kapatır. Eğer önce okuyan taraf boruyu kapatırsa (bu normal bir durum değildir) bu durumda yazan taraf boruya yazma yapmak istediğinde WriteFile fonksiyonu başarısız olmaktadır. ReadFile ya da WriteFile fonksiyonlarında karşı taraf boruyu kapattığından dolayı bu fonksiyonlar başarısız olmuşsa GetLastError fonksiyonu ERROR_BROKEN_PIPE değerini vermektedir. -> Windows sistemlerinde üst prosesteki HANDLE değerleri alt prosese default durumda aktarılmamaktadır. (Halbuki UNIX/Linux sistemlerinde default durumda üst prosesin betimeleyicilerinin exec işlemi sırasında korunduğunu anımsayınız.) Eğer üst prosesin HANDLE değerlerinin alt prosese aktarılması isteniyorsa handle üzerinde SetHandleInformation fonksiyonu ile aşağda çağrı uygulanmalıdır: if (!SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) ExitSys(TEXT("CreatePipe")); Ayrıca aktarımın yapıması için ana şalter görevinde olan CreateProcess fonksiyonundaki bInheritHandles parametresinin TRUE olarak geçilmesi gerekmektedir. -> Artık hangi prosesin boruya yazma yapacağına, hangisinin buradan yazma yapacağına karar verilir. Yine her iki proses kullanmadıkları betimelyicileri kapatmalıdır. -> CreateProses fonksiyonu ile alt proses yaratılır. Ancak boru HANDLE değerinin alt prosese komut satırı argümanlarıyla aktarılması gerekir. Aşağıdaki örnekte bir proses yukarıda belirtilen adımları uygulayarak alt prosesle boru haberleşmesi yapmaktadır. * Örnek 1, /* Parent.c */ #include #include #include #define CHILD_PATH "Child.exe" void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hPipeRead, hPipeWrite; DWORD dwWritten, dwRead; char args[4096]; STARTUPINFO si = { sizeof(STARTUPINFO) }; PROCESS_INFORMATION pi; if (!CreatePipe(&hPipeRead, &hPipeWrite, NULL, 0)) ExitSys("CreatePipe"); if (!SetHandleInformation(hPipeRead, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) ExitSys("SetHandleInformation"); sprintf(args, "%s %p", CHILD_PATH, hPipeRead); if (!CreateProcess(NULL, args, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) ExitSys("CreateProcess"); CloseHandle(hPipeRead); for (int i = 0; i < 1000000; ++i) if (!WriteFile(hPipeWrite, &i, sizeof(int), &dwWritten, NULL)) ExitSys("WriteFile"); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); CloseHandle(hPipeWrite); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* Child.c */ #include #include #include void ExitSys(LPCSTR lpszMsg); int main(int argc, char *argv[]) { HANDLE hPipeRead; DWORD dwRead; int val; if (argc != 2) { fprintf(stderr, "wrong number of arguments!..\n"); exit(EXIT_FAILURE); } sscanf(argv[1], "%p", &hPipeRead); while (ReadFile(hPipeRead, &val, sizeof(int), &dwRead, 0)) { printf("%d ", val); fflush(stdout); } if (GetLastError() != ERROR_BROKEN_PIPE) ExitSys("ReadFile"); printf("\n"); CloseHandle(hPipeRead); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>>> Paylaşılan Bellek Alanları: Windows'ta paylaşılan bellek alanları yoluyla proseslerarası haberleşme UNIX/Linux sistemlerindekine benzer biçimde yürütlmektedir. Tabii bu sistemlerde kullanılan API fonksiyonları farklıdır. Windows'ta Paylaşılan bellek alanları tipik olarak şu aşamalardan geçilerek kullanılmaktadır: -> Önce iki proses de CreateFileMapping API fonksiyonuyla bir "file mapping" nesnesi oluşturur. CreateFileMapping fonksiyonunun prototipi şöyledir: HANDLE CreateFileMappingA( HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCSTR lpName ); Fonksiyonun birinci parametresi bellek tabanlı dosya oluşturmak için kullanılmaktadır. Paylaşılan bellek alanları için bu parametre için INVALID_HANDLE_VALUE özel değeri geçilmelidir. İkinci parametre kernel nesnesinin güvenlik bilgilerine ilişkindir. Bu parametre NULL geçilebilir. Üçüncü parametre file mapping nesnesinin koruma özelliklerini belirtir. Bu parametre aşağıdakilerden biri olarak girilmelidir: PAGE_EXECUTE_READ PAGE_EXECUTE_READWRITE PAGE_EXECUTE_WRITECOPY PAGE_READONLY PAGE_READWRITE PAGE_WRITECOPY Burada PAGE_READONLY yalnıcz okuma yapmak için, PAGE_READWRITE hem okuma hem de yazma yapmak için, PAGE_WRITECOPY "copy on write" işlemi için, PAGE_EXECUTE_XXX bayrakları ise paylaşılan alana yerleştirilecek programın çalıştırılabilmesi için kullanılmaktasır. Burada en yaygın kullanılan bayrak PAGE_READWRITE bayrağıdır. Fonksiyonun sonraki iki parametresi file mapping nesnesinin 8 byte'lık uzunlupunun yüksek ve düşük anlamlı dörder byte'lık değerlerini almaktadır. Bu iki parametre bellek tabanlı dosya oluştururken 0 geçilebilir. Bu durumda file mapping nesnesinin uzunuğu ilgili dosyanın uzunluğu kadar olur. Fonksiyonun son parametresi proseslerin aynı file mapping nesnesini görebilmesi için gerekli olan ismi belirtir. Bu isim bir dosya ismi değildir. Programcının uydurduğu herhangi bir isim olabilir. Eğer haberleşme üst ve alt prosesler arasında yapılacaksa ya da bellek tabanlı dosya kullanılacaksa bu parametre NULL geçilebilir. Fonksiyon zaten bu isimde bir file mapping nesnesi yoksa onu yaratır, varsa olanı açar. Fonksiyon başarı durumunda file mapping nesnesinin handle değerine başarısızlık durumunda NULL adrese geri dönmektedir. Eğer file mapping nesnesi varsa ve biz onu açıyorsak fonksiyon başarılı olur, ancak GetLestError fonksiyonu fonksiyonu çağrıldığında fonksiyon ERROR_ALREADY_EXISTS değeri ile geri döner. CreateFileMapping fonksiyonu UNIX/Linux sistemlerindeki işlevsel olarak shm_open fonksiyonuna benzetilebilir. Örneğin: HANDLE hFileMapping; ... if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL) ExitSys("CreateFileMapping"); -> Artık file mapping nesnesi için sanal bellekte yer tahsis etmek gerekir. Yani başka bir deyişle nesneyi belleğe "map etmek" gerekir. Bu işlem MapViewOfFile API fonksiyonuyla yapılmaktadır. Bu fonksiyonun prototipi şöyledir: LPVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap ); Fonksiyonun birinci parametresi file mapping nesnesinin handle değerini almaktadır. Fonksiyonun ikinci parametresi map edilecek alandaki sayfaların koruma özelliklerini belirtmektedir. (Yani örneğin biz file mapping nesnesini read/write olarak oluşturmuş olabiliriz. Ancak "read only" yapabiliriz.) Buradaki bayraklar aşağıdı derlerin bit OR işlemine sokulmasıyla oluşturulmaktadır: FILE_MAP_READ FILE_MAP_WRITE FILE_MAP_ALL_ACCESS FILE_MAP_COPY FILE_MAP_EXECUTE FILE_MAP_LARGE_PAGES FILE_MAP_TARGETS_INVALID Bu parametre tipik olarak FILE_MAP_READ|FILE_MAP_WRITE biçiminde girilir. Tabii bu durumda file mapping nesnesinin de PAGE_READWRITE biçiminde yaratılmış olması gerekir. Fonksiyoun sonraki iki parametresi file mapping nesnesinin neresinin map edileceğini belirtmektedir. Bu parametreler 8 byte'lık uzunluk değerinin yüksek anlamlı ve düşük anlamlı dört byte'ını almaktadır. Bu offset değerinin sayfa katlarında olması gerekmektedir. Son parametre map edilecek uzunluğu belirtmektedir. Bu değer 0 girilirse file mapping nesnesinin belirtilen offset'ten itibaren geri kalan hepsi map edilmektedir. Fonksiyon başarı durumunda map edilen sanal bellek adresine, başarısızlık durumunda NULL adrese geri dönmektedir. MapViewOfFile API fonksiyonu işlevsel olarak UNIX/Linux sistemlerindeki mmap POSIX fonksiyonuna benzetilebilir. Örneğin: char *pcMapAddr; ... if ((pcMapAddr = (char*)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); -> Haberleşme bittikten sonra artık iki proses de map edilen sanal bellek alanını UnmapViewOfFile API fonksiyonuyla serbest bırakabilir. Fonksiyonun prototipi şöyledir: BOOL UnmapViewOfFile( LPCVOID lpBaseAddress ); Fonksiyon parametre olarak MapViewOfFile fonksiyonu ile tahsis edilmiş olan sanal bellek adresini alır. Başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır değerine geri döner. Bu fonksiyon UNIX/Linux sistemlerindeki işlevsel olarak munmap fonksiyonuna benzetilebilir. Ancak maunmap fonksiyonunun parçalı geri bırakmaya izin verdiğini anımsayınız. Bu fonksiyon ise tüm taksis edilen alanı geri bırakmaktadır. -> Nihayet CreateFileMapping fonksiyonu ile elde edilmiş olan file mapping nesnesi CloseHandle API fonksiyonuyle kapatılmalıdır. Fonkisyonun prototipini daha önce vermiştik: BOOL CloseHandle( HANDLE hObject ); FileMapping nesneleri son proses de nesneyi kapattığında otomatik olark sistemden silinmektedir. Halbuki UNIX/Linux sistemlerinde bu nesnelerin reboot edilene kadar (kernel persistent) ya da silinene kadar kaldığını anımsayınız. Aşağıdaki örnekte "Prog1.c" ve "Prog2.c" programları paylaşılan bellek alanları yoluyla haberleşmektedir. "Prog1.c" programı paylaşılan bellek alanına 0'dan 100'e kadar int sayıları yerleştirir. "Prog2.c" programı da bunları okuyup ekrana (stdout dosyasına) yazdırmaktadır. * Örnek 1, /* Prog1.c */ #include #include #include #define FILE_MAPPING_NAME "TestFileSharedMemory" void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFileMapping; int *mapAddr; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((mapAddr = (int *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); for (int i = 0; i < 100; ++i) mapAddr[i] = i; printf("Press ENTER to EXIT...\n"); getchar(); UnmapViewOfFile(mapAddr); CloseHandle(hFileMapping); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* Prog2.c */ #include #include #include #define FILE_MAPPING_NAME "TestFileSharedMemory" void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFileMapping; int *mapAddr; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((mapAddr = (int *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); printf("Press ENTER to read...\n"); for (int i = 0; i < 100; ++i) printf("%d ", mapAddr[i]); printf("\n"); UnmapViewOfFile(mapAddr); CloseHandle(hFileMapping); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>>> Bellek Tabanlı Dosyalar : Windows sistemlerinde bellek tabalı dosyaların oluşturulması da oldukça kolaydır. Sırasıyla şu işlemlerin yapılması gerekmektedir; -> İlgili dosya CreateFile API fonksiyonuyla açılır. -> Açılan dosyanın handle değeri verilerek CreateFileMapping fonksiyonu ile mapping nesnesi elde edilir. File mapping nesnesi oluşturulurken artık buna bir isim verilmesine gerek yoktur. -> File mapping nesnesinin handle değeri verilerek MapViewOfFile fonksiyonu çağrılır ve sanal bellek adresi elde edilir. -> Kullanım bittikten sonra map edilen adres UnmapViewFile fonksiyonu ile serbest bırakılır. Yine file mapping nesnesi ve açılmış olan dosya CloseHandle fonksiyonlarıyla kapatılır. Windows sistemlerinde de "unified file system" kullanılmaktadır. Yani mapping yapılan dosya için verilen adres tıpkı Linux sistemlerinde olduğu gibi işletim sisteminin kullandığı "page cache" buffer adresidir. Yani bir proses yine bu sistemlerde paylaşılan bellek alanına bir şey yazdığı zaman diğer prosesler (eğer dosya sharing modda açılmışsa) bu yazılanı görmektedir. Bunun tersi de geçerlidir. Windows sistemlerinde bu durum Microsoft tarafındna garanti edildiği için bu sistemlerde UNIX/Linux sistemlerinde olduğu gibi msync benzeri bir fonksiyon yoktur. * Örnek 1, Aşağıda Windows sistemlerinde bellek tabanlı dosya örneği verilmektedir. #include #include #include void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFile; HANDLE hFileMapping; char *mapAddr; DWORD dwSize; if ((hFile = CreateFile("test.txt", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) ExitSys("CreateFile"); if ((dwSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) ExitSys("GetFileSize"); if ((hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL)) == NULL) ExitSys("CreateFileMapping"); if ((mapAddr = (char *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); for (DWORD i = 0; i < dwSize; ++i) putchar(mapAddr[i]); putchar('\n'); memcpy(mapAddr, "XXXXX", 5); UnmapViewOfFile(mapAddr); CloseHandle(hFileMapping); CloseHandle(hFile); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Aşağıda bir BMP dosyasını bellek tabanlı olarak açıp ona ilişkin bilgileri alan bir Windows programı verişmiştir. BMP dosya formatı için çeşitli kaynaklara başvurabilirsiniz. Örneğin aşağıdaki bağlantıda BMP dosya formatı temel düzeyde açıklanmıştır: https://www.ece.ualberta.ca/~elliott/ee552/studentAppNotes/2003_w/misc/bmp_file_format/bmp_file_format.htm Aşağıdaki programda Windows ile ilgili kısım Macar Notasyonu ile diğer kısımlar klasik C notasyonu ile yazılmıştır. #include #include #include #include void ExitSys(LPCSTR lpszMsg); #define ROUND_UP(val, n) (((val) + (n) - 1) / (n) * (n)) #pragma pack(1) typedef struct tagBITMAP_FILE_HEADER { uint8_t signature[2]; uint32_t file_size; uint32_t reserved; uint32_t data_offset; } BITMAP_FILE_HEADER; typedef struct tagBITMAP_INFO_HEADER { uint32_t header_size; uint32_t image_width; uint32_t image_height; uint16_t plane; uint16_t bits_per_pixel; uint32_t compression; uint32_t compressed_image_size; uint32_t horizontal_res; uint32_t vertical_res; uint32_t color_used; uint32_t important_colors; } BITMAP_INFO_HEADER; typedef struct tagBITMAP_FORMAT { BITMAP_FILE_HEADER file_header; BITMAP_INFO_HEADER info_header; /* ... */ } BITMAP_FORMAT; typedef struct tagRGB { uint8_t red, green, blue; } RGB; void get_pixel24(uint8_t *dbmp, int width, int height, int row, int col, RGB *rgb); int main(void) { HANDLE hFile; HANDLE hFileMapping; DWORD dwSize; BITMAP_FORMAT *bmp; const char *compression_types[] = {"NO COMPRESSION", "8 BIT RLE ENCODING", "4 BIT RLE_ENCODING"}; uint8_t *dbmp; RGB rgb; if ((hFile = CreateFile("test.bmp", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) ExitSys("CreateFile"); if ((dwSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) ExitSys("GetFileSize"); if ((hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL)) == NULL) ExitSys("CreateFileMapping"); if ((bmp= (BITMAP_FORMAT *)MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); printf("Signature: %c%c (%02X %02X)\n", bmp->file_header.signature[0], bmp->file_header.signature[1], bmp->file_header.signature[0], bmp->file_header.signature[1]); printf("File Size:%lu (%lX)\n", (unsigned long)bmp->file_header.file_size, (unsigned long)bmp->file_header.file_size); printf("Data Offset: %lu (%lX)\n", (unsigned long)bmp->file_header.data_offset, (unsigned long)bmp->file_header.data_offset); printf("Info Header Size: %lu (%lX)\n", (unsigned long)bmp->info_header.header_size, (unsigned long)bmp->info_header.header_size); printf("Image Width: %lu (%lX)\n", (unsigned long)bmp->info_header.image_width, (unsigned long)bmp->info_header.image_width); printf("Image Height: %lu (%lX)\n", (unsigned long)bmp->info_header.image_height, (unsigned long)bmp->info_header.image_height); printf("Bits Per Pixel: %lu (%lX)\n", (unsigned long)bmp->info_header.bits_per_pixel, (unsigned long)bmp->info_header.bits_per_pixel); printf("Compression: %s\n", compression_types[bmp->info_header.compression]); /* .... */ dbmp = (uint8_t *)bmp + bmp->file_header.data_offset; get_pixel24(dbmp, bmp->info_header.image_width, bmp->info_header.image_height, 46, 157, &rgb); printf("Red: %d, Green: %d, Blue: %d\n", rgb.red, rgb.green, rgb.blue); UnmapViewOfFile(bmp); CloseHandle(hFileMapping); CloseHandle(hFile); return 0; } void get_pixel24(uint8_t *dbmp, int width, int height, int row, int col, RGB *rgb) { uint32_t destoff; destoff = (height - row - 1) * ROUND_UP(width * 3, 4) + col * 3; rgb->blue = dbmp[destoff]; rgb->green = dbmp[destoff + 1]; rgb->red = dbmp[destoff + 2]; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /*================================================================================================================================*/ (56_14_01_2024) & (57_20_01_2024) & (58_21_01_2024) & (59_27_01_2024) & (60_28_01_2024) & (61_03_02_2024) & (62_04_02_2024) (63_10_02_2024) > Veri Yapıları ve Algoritmalar: Kursumuzun bu bölümünde dikkatimizi veri yapıları ve algoritmalar konusuna yönelteceğiz. Şimdi sırasıyla incelemelerde bulunalım: >> Bir sistem programcınının temel veri yapıları ve algoritmalar konusunda belli düzeyde bilgi sahibi olması gerekmektedir. Aralarında fiziksel ya da mantıksal ilişki olan bir grup nesnenin oluşturduğu topluluğa "veri yapısı (data structure)" denilmektedir. Veri yapısı denildiğinde tekil bir nesne değil belli bir düzen içerisinde bulunan bir grup nesne anlaşılmaktadır. Çok temel veri yapıları programlama dillerinin sentaksı tarafından built-in biçimde desteklenmektedir. Örneğin C'de "diziler (arrrays)", "yapılar (structures)", "birlikler (unions)" dilin sentaksı tarafından desteklenen built-in veri yapılarıdır. Ancak C tarafından doğrudan desteklenen bu veri yapıları dış dünyadaki olayları modellemekte yetersiz kalmaktadır. Programcının dilin sentaksı ile doğrudan desteklenen veri yapılarını kullanarak diğer veri yapılarını oluşturması gerekebilmektedir. Nesne Yönelimli Programlama Dillerinde genellikle o dillerin kütüphanelerinde pek çok veri yapısı sınıflar biçiminde zaten hazır olarak bulunudurlmaktadır. Örneğin C++'ın standart kütüphanesinde, C# (.NET) ve Java'nın temel kütüphanelerinde "kuyruk sistsmleri (queues)" gibi, "bağlı listeler (linked lists)" gibi "hash tabloları (hash tables)" gibi veri yapıları hazır bir biçimde bulunmaktadır. Halbuki C'de bu veri yapıları C'nin standart kütüphanesi tarafından desteklenmemektedir. Her ne kadar pek çok nesne yönelimli programlama dilinin kütüphanelerinde bu bölümde ele alacağımız veri yapıları zaten hazır bir biçimde bulunuyorsa da sistem programcısının bu veri yapılarını yazabilecek düzeyde tanıması önemli olmaktadır. >> Bir problemi kesin çözüme götüren adımlar topluluğuna "algoritma (algorithm)" denilmektedir. Algoritma sözcüğü Fars bilim adamı Ebû Ca'fer Muhammed bin Mûsâ el-Hârizmî'nin isminden hareketle (bazı yanlış anlaşılmalarla) uydurulmuş bir sözcüktür. Algoritmaların problemi kesin çözüme götürmesi gerekir. Eğer söz konusu adımlar problemi kesin çözüme götürmüyor ancak iyi bir çözüm sunuyorsa buna "algoritma" yerine "sezgisel yöntemler (heuristic)" denilmektedir. Pekiyi algoritmalar nasıl ifade edilmektedir? Örneğin "bir dizinin en büyük elemanını bulan algoritma" teknik olarak nasıl açıklanabilir? Algoritmaları cümlelerle sözel biçimde açıklamak hala kullanılan temel yöntemlerden biridir. Örneğin "dizini ilk elemanını en büyük kabul edilip bir değişkende saklanır. Sonra diğer tüm elemanlar bir döngü içerisinde bu elemanla karşılaştırılır. Daha büyük eleman görülürse bu değişkene yerleştirilir" gibi. Ancak sözel açıklama teknik anlamda belirsiz olabilmektedir. Sözükler yerine sembollerin kullanılması daha kesin belirlemelerin yapılmasına olanak sağlamaktadır. İşte algoritmalar "pseude kodlar" yoluyla, "akış diyagramları" yoluyla ya da "belli bir programlama dilinde yazılmış kodlar" yoluyla da ifade edilebilmektedir. Eskiden "pseudo kodlar" ve akış diyagramları daha çok kullanılırdı. Artık algoritmanın ifadesi için mevcut programlama dillerinden biri seçilmektedir. Tabii her zaman sözel açıklama da sürece eşlik etmektedir. Pekiyi algoritmaları ifade etmek için hangi programlama dili tercih edilmelidir? Çok yüksek seviyeli diller bu iş için uygun değildir. Dil ne kadar alçak seviyeli olursa algoritmadaki inceliği o kadar iyi yansıtabilmektedir. Bu bakımdan C Programalama Dili tercihler arasında ön plandadır. Ancak son 20 yıldır algoritmaların ifade edilmesinde Java gibi, C# gibi nesne yönelimli diller de yoğun olarak kullanılmaktadır. Python kanımızca algoritmaları ifade etmek için fazlaca yüksek seviyeli bir dildir. Ancak Python üzeirnden de algoritmaları anlatan kitaplar ve dokümanlar bulunmaktadır. Gerçekten de popüler algoritma kitaplarına bakıldığında "Algorithms in C" gibi, "Algorithms in Java" gibi, anlatımın mevcut bir dil kullanılarak yapıldığı görülmektedir. Özete günümüzde algoritmalar hem sözel olarak hem de bir programlama dilinde yazılmış kodlar biçiminde ifade edilmektedir. Algoritmalar konusunda en bilindik kişi Donald Knuth isimli araştırmacıdır. Knuth'un ansiklopedik nitelikte üç ciltlik klasikleşmiş "The Art of Compuer Programming" isimli kitabı vardır. Daha sonra Knuth bu seriye birkaç ince fasikül de eklemiştir. Knuth bu önemli başvuru kitabında algoritmaları ifade etmek için kendisinin uydurduğu MIX isimli bir sembolik makine dili kullanmıştır. Daha sonra bu MIX dilinin RMIX isimli RISC tabanlı biçimini de geliştirmiştir. Bir problemi çözebilen pek çok alternatif algoritmalar söz konusu olabilmektedir. Bunların hangisinin daha iyi olduğunun belirlenmesi gerekir. Tabii bunun için bir "iyilik ölçütü" tespit edilmelidir. Algoritmaları kıyaslamak için ölçüt ne olmalıdır? Kıyaslama için iki önemli ölçüt "hız" ve "kaynak kullanımıdır". Ancak baskın ve default ölçüt hızdır. Yani genellikle hangi alternatif algoritma daha hızlı sonuç veriyorsa o algoritma tercih edilmektedir. Tabii bazen kısıtlı kaynakların söz konusu olduğu durumlarda programcı hız yerine kaynak kullanımı (tipik olarak bellek kullanımı)" ölçütünü de ön plana alabilir. İki algoritmanın hızlarını kıyaslamak kolay bir işlem değildir. Çünkü algoritmada koşula dayalı işlemler yapılyor olabilr. Bu koşulların sağlanması ya da sağlanmaması girdilere bağlı olabilir. Algoritmaların önemli bir bölümü dizi gibi veri yapıları üzerinde işlemler yapmaktadır. Bu durumda örneğin dizinin dağılımı hız üzerinde etkili olabilmektedir. Algoritmaların hızlarını kıyaslamak için "simülasyon yöntemi" kullanılabilir. Bu yöntemde alternatif algoritmaların kodları yazılır. Bu algoritmalar rastgele girdilerle çok fazla kez çalıştırılır, bir ortalama zaman hesaplanır. Simülasyon yöntemi bazen başvurulan yardımcı yöntemlerden biridir. Ancak simülasyon yöntemini uygulamak oldukça zordur. Her algoritmayı simüle edecek kodun yazılması ve bunun çok kez çalıştırılması zor bir süreçtir. Bunun yerine tamamen cebrik yöntemlerin kullanılması daha uygundur. Algoritmaların cebrik yöntemlerle hızlarının ve kaynak kullanımlarının analizine ilişkin sürece "algoritma analizi (analysis of algorithms)" denilmektedir. Pekiyi algoritma analizi nasıl yapılmaktadır? Bunun temel yolu algoritmanın çözümü için gereken işlem sayısının bulunmasıdır. Tabii bu işlem sayısı girdi büyüklüğüne bağlı olarak parametrik bir biçimde hesaplanacaktır. Ancak yukarıda da belirttiğimiz gibi algortimadaki işlemlerin sayısı girdilere bağlı olarak da değişebilmektedir. (Tabii aslında her işlem aynı sürede yapılmayabilir. Örneğin pek çok işlemcide çarpma işlemi toplama işlemine göre çok daha yavaş yapılmaktadır.) Örneğin bir dizi içerisinde var olduğunu bildiğimiz belli bir değerin kaç işlemde bulunacağını hesaplamak isteyelim. * Örnek 1, Algoritma şöyle olsun: int a[N]; for (int i = 0; i < N; ++i) if (a[i] == VAL) break; /* bulundu */ Burada işlem sayısına şu işlemler dahildir: int i = 0 i < N ++i a[i] == VAL Ancak burada aranacak olan VAL değerinin dizinin kaçıncı elemanında olduğu diziye bağlıdır. Algoritmaya bağlı değildir. İşte bu tür durumlarda algoritma analiz edilirken tipik olarak iki durum için hesaplama yapılır: -> En kötü durum analizi (worst case analyisis) -> Ortalama durum analizi (average case analysis) En iyi durum analizi de söz konusu olabilirse de pratikte çok iyimser bir yaklaşım olduğu için uygulanmamaktadır. Yukarıdaki sıralı aramamın en kötü durum analizini yapmaya çalışalım. En köü durumda eleman dizinin son elemanı olarak bulunur. Böylece en kötü durumdaki işlem sayısı 1 + 3N kadardır. Bu algoritmadaki ortalama işlem sayısı aranacak değerin 1'inci, 2'inci, 3'üncü, ... N'inci indekslerde bulunması durumunun toplamının N'e bölünmesiyle elde edilebilir. Bunu matematiksel olarak şöyle yazabiliriz: 1 + (3 * (1 + 2 + 3 + ... + N) / N) 1'den N'e kadar sayıların toplamı N * (N + 1) olduğuna göre buradan şu değer elde edilir: 1 + (3 * (N + 1) / 2) Tabii bu tür hesaplamalarda bizim sabit olmayan kısmın artışına bakmamız doğru olur. Buradaki 1 toplamı, 3 çarpımı ve 2'ye bölüm sabittir. Bu tür durumlarda bizim için önemli olan bu N değerinin artması ile algoritmanın davranışıdır. Algoritmalar dünyasında girdi büyüklüğüne (örneğin dizinin uzunluğuna) bağlı olarak algoritmada yapılan işlemlerin sayısını elde etmekte kullanılan ifadelere "algoritmanın karmaşıklığı (complexity of algorithms)" denilmektedir. Yani algoritmanın karmaşıklığı girdi büyüklüüne bağlı olarak algortimanın hızının betimlenmesi için kullanılan ifadelere denilmektedir. Örneğin yukarıdaki sıralaı aramanın karmaşıklığı 1 + (3 * (N + 1) / 2) olarak belirtilebilir. Bir algoritmanın en kötü durum ve özellikle de ortalama durumdaki karmaşıklığının (yani girdiye dayalı olarak işlem sayısının) hesaplanması oldukça zor olabilmektedir. Bu nedenle genellikle karmaşıklık için kesin bir değer bulma yerine karmaşıklıkları kategorilere ayırıp karşılaştırma yoluna gidilmektedir. Bu tür kategorik karşılaştırmalar her ne kadar algoritmanın aldığı zaman konusunda ince bir değer vermese de onun karakteri hakkında kısa sürece içerisinde önemli bir bilgi vermektedir. Algoritmaları kategorik olarak sınıflandırmak için "asimtotik notasyonlar" denilen özel notasyonlar kullanılmaktadır. Asimtotik notasyonlar aslında matematikte programlamanın dışında ele alınıp kullanılmış olan tekniklerdir. Başka bir deyişle asimtoto, k notasyonlar bir fonksiyonun büyüme karakteristiği ile ilgili çalışmalardan kaynaklanmaktadır. Ancak bu konu algoritma analizinde önemli bir uygulama alanı bulmuştur. Algoritma karmaşıklığında kullanılan asimtotik notasyonlar şunlardır: -> Big O Notasyonu ->Büyük Teta Notasyonu -> Büyük Omega Notasyonu -> Küçük o Notasyonu Ancak Big O notasyonu en çok kullanılan notasyondur. Bu notasyonlar aslında bir grup fonksiyonu karakterize etmek için düşünülmüştür. >>> Big O Notasyonu : Aşağıdaki gibi belirtilmektedir: f(x) = O(g(x)) Burada f(x) ve g(x) iki fonksiyondur. Notasyondaki eşittir karakteri eleştirilmektedir. Buradaki eşittir karakteri aslında matematikteki eşitliği anlatmamaktadır. Burada aslında "f(x) fonksiyonunun Big O notasyonuna göre g(x) kategorisinden olduğu" anlatılmaktadır. Örneğin: 3x = O(X^2) 5X^2 = O(X^2) Burada 3x ve 5X^2 fonksiyonları big O notasyonuna göre X^2 kategorisindendir. Big O notasyonunun matematiksel sembollerle biçimsel (formal) açıklaması biraz ayrıntılıdır. Bu açıklamalar için Ebook klasöründeki "Algorithms and Complexity" kitabının Birinci bölümüne başvurabilirsiniz. Biz burada önce biçimsel olmayan açıklama yapacağız. Sonra biçimsel açıklama üzerinde duracağız. Big O notasyonunda kategori olarak O(gx(x)) ifadesiyle belirtilmiş olan fonksiyon ile üstel bakımdan aynı olan ve bundan küçük olan tüm fonksiyonlar kstedilmektedir. Örneğin O(x^2) kategorisi üssü en fazla iki olan tüm fonksiyonları ve üssü bundan küçük olan tüm fonksiyonları belirtmektedir. Örneğin aşağıdaki fonksiyonların hepsi O(x^2) kategorisine sahiptir, dolayısıyla f(x) = O(x^2) biçiminde ifade edilebilir: f(x) = 100 f(x) = 3x f(x) = 3x^2 f(x) = 100x^2 + 80x +5 f(x) = 2X^2 - 5x - 8 Burada x^2'nin katsayısının bir önemi olmadığına dikkat ediniz. Başka bir deyişle O(x^2) kategorisi k x^2 ve x^1 ve sabitin tüm toplamlarına ilişkin fonksiyonları belirtmektedir. Benzer biçimde O(x) kategorisine ilişkin bazı fonksiyonlar şunlardır: f(x) = 3x f(x) 0 100x - 5 f(x) = x - 1 f(x) = 10 Burada da x'in katsayısının bir önemi yoktur. Big O notasyonunda O(1) karmaşıklığına sahip (Burada 1 yerine başka bir sayıda kullanılabilir. Ancak en düşük sayı 1 olduğu için 1 tercih edilmektedir. Yani O(5) ile O(1) aynı anlamdadır.) fonksiyonlardan bazıları şunlardır: f(x) = 123 f(x) = 1234567 f(x) = 1 Big O notasyonunda g(x) fonksiyonundaki çarpanın önemli olmadığına dikkat ediniz. Örneğin O(x^2) kategorisi ile O(3x^2) kategorisi arasında hiçbir farklılık yoktur. Benzer biçimde bunlarla O(5X^2 + 2) kategorisi arasında da bir farklılık yoktur. Bu nedenle kategoriler belirtilirken gereksiz biçimde g(x) fonksiyonunda çarpan ve toplam değerler belirtilmez. Big O natsyonunun biçimsel açıklaması şöylşe yapılabilir: f(x) = O(g(x)) (x → ∞) if ∃ C, x0 öyle ki |f(x)| < Cg(x) tüm x > x0 değerleri için Burada şu belirtilmektedir: f(x) = O(g(x)) olabilmesi için |f(x)| < Cg(x) eşitliğini sağlayabilecek bir C değerinin bulunması gerekir. Örneğin 3x^2 + 5x + 2 = O(x^2) yazılabilir. Çünkü burada örneğin C = 5 gibi bir C değeri bulunabilir. Böylece 5X^2 belli bir x0 değerinde sonra her zaman 3X^2 + 5x + 2 değerinden büyük kalacaktır. Algoritmaları kıyaslamak için iyiden kötüye doğru çeşitli Big O kategorileri kullanılmaktadır. Bu kategoriler şunlardır (Burada x yerine N gösterimini kullanacağız): O(1) Sabit Karmaşıklık O(log log N) Çifte Logaritmik Karmaşıklık O(log N) Logaritmik Karmaşıklık O((log N)^c) c > 1 olmak üzere Polylogaritmik Karmaşıklık O(N^c) c < 0 < 1 olmak üzere Oransal Kuvvet Karmaşıklığı O(N) Doğrusal Karmaşıklık O(N log N) N Log N karmaşıklığı O(N^2) Karesel Karmaşıklık O(N^3) Küpsel Karmaşıklık O(N^c) Polinomsal Karmaşıklık O(c^N) c > 1 olmak üzere Üstel Karmaşıklık O(N!) Faktöriyel Karmaşıklığı Algoritmaları Big O notasyonuna göre kategorik olarak karşılaştırmak kolaydır. Alternatif algoritmaların kategorileri belirlenir. Hangi kategorinin diğerinden daha iyi olup olmadığına bakılır. Aynı kategoriye giren algoritmalar arasında bir farklılık söz konusu olsa da bu farklılık kategorik bir farklılık değildir. Biz aynı Big O kategorisindeki algoritmaları aynı karakterde kabul edebiliriz. Pekiyi bir algoritmanın yukarıda belirttiğimiz Big O kategorisi nasıl tespit edilir? Algoritmanın girdisinin N olduğunu varsayalım. Örneğin N bir dizinin uzunluğu olabilir. Bazı kategorilerin tespiti şöyle yapılabilir: - Tekil ifadeler içeren ve N değrine bağlı olmayan sabit uzunluklu döngüler içeren algoritmalar O(1) karmaşıklıktadır. Örneğin üçgenin alanın hesaplanması, dikdörtgenin çevresinin hesaplanması, bir yılın artık yıl olup olmadığının tespit edilmesi gibi algoritmalar O(1) yani sabit karmaşıklıktadır. Sabit karmaşıklık en iyi karmaşıklık kategorisidir. Örneğin bir problemde "bunun için sabit karmaşıklığa sahip bir algoritma kullanın" ifadesi "N değerine dayalı döngü kullanmadan tekil ifadelerle çözümü bulun" anlamına gelmektedir. Örneğin, dizinin bir elemanına erişilmesi sabit karmaşıklıklı bir işlemdir. - Bir algoritmada bir döngü varsa ancak döngü N'in logaritması kadar dönüyorsa (tabii algoritma tekiş ilemler de içerebilir) bu algoritma O(log N) karmaşıklıktadır. Örneğin "ikili arama (binary search)" algoritmasında N değeri arttıkça döngü log2 N kadar artmaktadır. - Bir algoritma N'e dayalı tekil döngüler içeriyorsa ve tekil işlemler içeriyorsa bu algoritma O(N) karmaşıklıktadır. Bu tür karmaşıklıklara sözel olarak "doğrusal karmaşıklık" da denilmektedir. Örneğin "dizinin en büyük elemanının bulunması", "dizinin aritmetik ortalamasının bulunması", "dizide bir elemanın bulunması" gibi algoritmalar O(N) karmaşıklıktadır. Sıralı bir dizi bulunyor olsun. Biz de bu sıralı dizide belli bir elemanı bulmak isteyelim. İkili arama (binary search) O(log N) karmaşıklıkta, sıralı arama O(N) karmaşıklıktadır. O halde ikili arama sıralı aramdan daha iyidir. - O(N log N) karmaşıklıkta iç içe iki döngü bulunabilir. Dıştaki döngü N'e dayalı dönerken içteki dönü N'nin logaritmasına dayalı dönmektedir. Tabii algoritma başka N'e dayalı döngüler ve tekil işlemler içerebilir. Uick Sort gibi, Heap sort gibi kaliteli sort algoritmaları O(N log N) karmaşıklıktadır. - N'e dayalı iç içe iki döngü varsa (tabii ayrık N'e dayalı döngüler ve tekil işlemler de olabilir) bu tür algoritmalar O(N^2) karmaşıklıktadır. Örneğin "boubble sort", "selection sort" gibi algoritmalar O(N^2) karmaşıklıktadır. Bu karmaşıklığa "karesel karmaşıklık" da denilmektedir. Bu durumda örneğin "selection sort" algoritması "quick sort" algoritmasından daha kötüdür. - N'e dayalı iç içe üç döngü varsa (tabii N'e dayalı iç içe döngüler, tekil döngüler ve tekil işlemler de olabilir) bu tür algoritmalar O(N^3) yani küpsel karmaşıklığa sahiptir. Örneğin tipik matris çarpımı küpsel karmaşıklığa sahiptir. Ancak karesel kamaşıklığa sahip çözümleri de vardır. - İç içe k tane döngü içerebilen tüm algoritmalara genel olarak "polinomsal karmaşıklıktaki (polynomial complexity) algortimalar" denilmektedir. - Alt küme işlemlerine ilişkin algoritmalar tipik olarak O(c^n) yani üstel karmaşıklıktadır. Bir kümenin tüm alt kğmelerinin sayısının 2^n olduğunu anımsayınız. - Bazı graf ptoblemleri O(N!) yani faktöriyelsel karmaşıklıktadır. Örneğin "gezgin satıcı problemi (traveling salesmen problem)" faktöriyelsel karmaşıklıktadır. Faktöriyelsel karmaşıklık en kötü karmaşıklık kategorisidir. Üstel karmaşıklığa ve faktöriyelsel karmaşıklığa "polinomsal olmayan (nonpolynomial)" karmaşıklar denilmektedir. Polinomsal olmayan karmaşıklık kısaca "NP karmaşıklık" olarak da ifade edilmektedir. NP karmaşıklıktaki problemler bugünkü bilgisayarlarda bile binlerce sene zaman alabilecek boyutlara gelebilmektedir. Pek çok NP algoritma için polinomsal karmaşıklığa sahip daha makul algoritmalar aranmaktadır. Ancak bu konuda önemli başarılar elde edilmemiştir. Pekiyi NP karmaşıklıktaki problemler için neler yapılabilir? İşte bu alanda "sezgisel yöntemler (heuristic)" denilen yöntemler kullanılmaktadır. Anımsanacağı gibi "sezgisel yöntemler (heuristic)" en iyi çözümü hedeflemeyen ancak tatmin edici bir çözümü makul bir zaman içerisinde bulmayı sağlayan yöntemlerdir. >> C'de "diziler", "yapılar" ve "birlikler" built-in veri yapılarıdır. Yani dilin sentaksı taarafından birnci elde desteklenmektedir. Ancak bu veri yapıları bazı uygulamaları gerçekleştirmek için yetersiz kalmaktadır. İşte bu bölümde C'nin sentaksı tarafından doğrudan desteklenmeyen ancak uygulamalarda sıkça karşılaşılan veri yapılarını inceleyeceğiz. Bu veri yapıları pek çok nesne yönelimli programlama dilinin temel kütüphanelerinde birer sınıf biçiminde bulunmaktadır. Yani eğer C++, Java ve C# fillerle çalışıyorsanız burada göreceğimiz veri yapıları zaten o dilelrin temel kütüphanlerinde hazır bir biçimde bulunmaktadır. Ancak ne olursa olsun bu veri yapılarının nasıl çalıştığının ve nasıl gerçekleştirildiğinin sistem programcıları tarafından bilinmesi gerekmektedir. >> Veri yapıları dünyasında bir süredir çok karşılaşılan terimlerden biri de "abstract data type" terimidir. Bu terimi Türkçe'ye "soyut veri türü" biçiminde çevirebiliriz. Abstract data type denildiğinde Belli bir veri yapısını gerçekleştiren data ve fonksiyon grubu anlaşılmalıdır. Burada "abstract" sözcüğü "soyutlama" yani "aytrıntıların göz ardı edilerek işlevlere dikkatin yöneltilmesi" anlamına gelmektedir. Abstract data type denildiğinde bir veri yapısı üzerinde işlem yapan API arayüzü anlaşılır. Tipik olarak bu kavran nesne yönelimli programlama tekniğinde bir sınıf ve o sınıfın desteklediği arayüz biçiminde oluiturulmaktadır. Şimdi de sırasıyla bu veri yapılarını ve algoritmalarını kategori bazlı incelemeye koyulalım: >> Dinamik Diziler : En çok kullanılan veri yapılarından biri "dinamk dizi (dynamic array)" denilen veri yapısıdır. Dinamik dizi "gerektiğinde büyütülen" dizi anlamına gelmektedir. Bu veri yapısını kullanan programcı veri yapısına bir eleman eklediğinde ekleme fonksiyonu eğer gerekiyorsa tahsis etmiş olduğu diziyi büyütmektedir. Ancak veri yapısını kullanan kişi işin bu kısmıyla uğraşmamaktadır. Bu veri yapısı C++'ın standart kütüphanesinde "vector" ismi ile C# ve Java ortamlarının temel kütüphanelerinde "ArrayList" ismiyle bulunmaktadır. Dinamik dizilerin gerçekleştirilmesinde önemli olan birkaç nokta vardır. Bunlardan en önemlisi dinamik dizi için ayrılan alan yetmediğinde dizinin ne kadar büyütüleceğidir. Diziyi birer birer büyütmek iyi bir fikir değildir. Çünkü dinamik tahsisatlar yavaş olma eğilimindedir. Genellikle büyütme "eskisinin iki katı olacak" biçimde yapılmaktadır. Böylece büyütme (reallocation) dizi uzunluğuna göre logaritmik karmaşıklıkta (yani O(log N) karmaşıklıkta) yapılmış olur. Gerçekleştirimde programcının o anda dinamk dizi içerisinde kaç elemanın bulunduğunu ve o anda dizi için tahsis edilen elemanın kaç eleman uzunluğunda olduğunu tutması gerekir. Dizide var olan eleman sayısına "count" ya da "size" denilmektedir. Dizi için tahsis edilmiş olan eleman sayısına ise genellikle "capacity" debilmektedir. Dinamik dizinin sonuna eleman eklerken eleman count (ya da size) ile belirtien indekse eklenir ve count değer bir artılır. count değeri capacity değerine ulaştığında yeniden tahsisat (reallocation) yapılarak dinamik dizi büyütülmelidir. Dinamik dizilerde araya eleman da benzer biçimde eklenmektedir. Genel olarka dinamik dizilerin gerçekleştirimlerinde eleman silindiğinde kapasite azaltılması yapılmamaktadır. Dinamik dizilerin gerçekleştirilmesinde kullanıcılara sunulacak temel işlevler şunlardır: -> Dinamik dizinin yaratılması -> Dinamik dizinin yok edilmesi -> Dinamik dizinin sonuna eleman eklenmesi -> Araya elemanın insert edilmesi -> Count ve capacity değerleriin dışarıya verilmesi -> Elemanların get ve set edilmesi -> Belli bir indeksteki elemanın silinmesi -> Dizideki tüm elemanların silinmesi -> Capacity değerinin büyütülmesi -> Capacity değerinin count değerine çekilmesi Genel olarak dinamik büyütülen dizilerde bir elemanın silinmesi ya da tüm elemanların silinmesi capaciry değerinde bir değişiklik yaratmamaktadır. Genellikle bu tür veri yapılarında capacity değerinin büyütülmesine izin verilir. Ancak küçültülmsine izin verilmez. Ancak özel bir durum olarak capacity değeri count değerine çekilebilmektedir. Aşağıdaki dinamik dizilerin gerçekleştirimine ilişkin bir örnek verilmiştir. Bu örnekte bazı küçük fonksiyonlar makro yerine static inline fonksiyon biçiminde başlık dosyasının içerisinde tanımlanmıştır. * Örnek 1, /* dynamicarray.h */ #ifndef DYNAMICARRAY_H_ #define DYNAMICARRAY_H_ /* Symbolic Constants */ #define DARRAY_DEF_CAPACITY 8 #define DARRAY_FAILED ((size_t)-1) /* Type Declaratrions */ typedef int DATATYPE; typedef struct tagDARRAY { DATATYPE *darray; size_t capacity; size_t count; } DARRAY, *HDARRAY; /* Function Prototypes */ HDARRAY create_darray(void); void destroy_darray(HDARRAY hdarray); size_t add_darray(HDARRAY hdarray, DATATYPE val); size_t multiadd_darray(HDARRAY hdarray, const DATATYPE *vals, size_t size); size_t addp_darray(HDARRAY hdarray, const DATATYPE *val); size_t insert_darray(HDARRAY hdarray, size_t index, DATATYPE val); size_t remove_darray(HDARRAY hdarray, size_t index); size_t reserve_darray(HDARRAY hdarray, size_t newcapacity); /* inine function definitions */ static inline size_t capacity_darray(HDARRAY hdarray) { return hdarray->capacity; } static inline size_t count_darray(HDARRAY hdarray) { return hdarray->count; } static inline DATATYPE get_darray(HDARRAY hdarray, size_t index) { return hdarray->darray[index]; } static inline void set_darray(HDARRAY hdarray, size_t index, DATATYPE val) { hdarray->darray[index] = val; } static inline void setp_darray(HDARRAY hdarray, size_t index, const DATATYPE *val) { hdarray->darray[index] = *val; } static inline void getp_darray(HDARRAY hdarray, size_t index, DATATYPE *val) { *val = hdarray->darray[index]; } static inline void clear_darray(HDARRAY hdarray) { hdarray->count = 0; } #endif /* dynamic.arry.c */ #include #include #include "dynamicarray.h" static int reallocate(HDARRAY hdarray, size_t newcapacity); HDARRAY create_darray(void) { HDARRAY hdarray; if ((hdarray = (HDARRAY)malloc(sizeof(DARRAY))) == NULL) return NULL; if ((hdarray->darray = (DATATYPE *)malloc(sizeof(DATATYPE) * DARRAY_DEF_CAPACITY)) == NULL) { free(hdarray); return NULL; } hdarray->capacity = DARRAY_DEF_CAPACITY; hdarray->count = 0; return hdarray; } void destroy_darray(HDARRAY hdarray) { free(hdarray->darray); free(hdarray); } size_t add_darray(HDARRAY hdarray, DATATYPE val) { if (hdarray->count == hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2) == -1) return DARRAY_FAILED; hdarray->darray[hdarray->count++] = val; return hdarray->count - 1; } size_t multiadd_darray(HDARRAY hdarray, const DATATYPE *vals, size_t size) { if (hdarray->count + size > hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2 + size) == -1) return DARRAY_FAILED; memmove(hdarray->darray + hdarray->count, vals, sizeof(DATATYPE) * size); hdarray->count += size; return hdarray->count - 1; } size_t addp_darray(HDARRAY hdarray, const DATATYPE *val) { if (hdarray->count == hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2) == -1) return DARRAY_FAILED; hdarray->darray[hdarray->count++] = *val; return hdarray->count - 1; } size_t insert_darray(HDARRAY hdarray, size_t index, DATATYPE val) { if (index > hdarray->count) return DARRAY_FAILED; if (hdarray->count == hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2) == -1) return DARRAY_FAILED; memmove(hdarray->darray + index + 1, hdarray->darray + index, (hdarray->count - index) * sizeof(DATATYPE)); hdarray->darray[index] = val; ++hdarray->count; return index; } size_t insertp_darray(HDARRAY hdarray, size_t index, const DATATYPE *val) { if (index > hdarray->count) return DARRAY_FAILED; if (hdarray->count == hdarray->capacity && reallocate(hdarray, hdarray->capacity * 2) == -1) return DARRAY_FAILED; memmove(hdarray->darray + index + 1, hdarray->darray + index, (hdarray->count - index) * sizeof(DATATYPE)); hdarray->darray[index] = *val; ++hdarray->count; return index; } size_t remove_darray(HDARRAY hdarray, size_t index) { if (index >= hdarray->count) return DARRAY_FAILED; memmove(hdarray->darray + index, hdarray->darray + index + 1, (hdarray->count - index - 1) * sizeof(DATATYPE)); --hdarray->count; return index; } size_t reserve_darray(HDARRAY hdarray, size_t newcapacity) { if (newcapacity <= hdarray->capacity) return 0; if (reallocate(hdarray, newcapacity) == -1) return 0; return newcapacity; } static int reallocate(HDARRAY hdarray, size_t newcapacity) { DATATYPE *new_darray; if ((new_darray = (DATATYPE *)realloc(hdarray->darray, sizeof(DATATYPE) * newcapacity)) == NULL) return -1; hdarray->darray = new_darray; hdarray->capacity = newcapacity; return 0; } >> Aralarında öncelik-sonralık ilişkisi olan veri yapılarına "liste (list)" de denilemektedir. Örneğin bu tanıma göre diziler de "liste tarzı" veri yapılarıdır. Liste tarzı veri yapılarının en yaygın kullanılanlarında biri "bağlı liste (linkes list)" denilen veri yapısıdır. Önceki elemanının sonraki elemanın yerini gösterdiği dolayısıyla elemanların ardışıl olma zorunluluğunun ortadan kaldırıldığı dizilere "bağlı liste" denilmektedir. Dizi elemanlarının bellekte fiziklsel olarak ardışıl biçimde bulunduğunu anımsayınız. Bağlı listeler adeta "elemanları bellekte ardışıl olmayan diziler" gibidir. Bağlı listelerin her elemanına "düğüm (node)" denilmektedir. Biz kursumuzda bağlı liste elemanlarına "düğüm" ya da "eleman" diyeceğiz. Bağlı listelerde her düğüm sonraki düğümün yerini tutarsa ve ilk elemanın yeri de biliniyorsa liste elemanlarının hepsine erişilebilmektedir. Örneğin: head ----> node ---> node ---> node ----> node (NULL) Bağlı listelerde her düğüm hem elemanın değerini hem de sonraki elemanın adresini tutmaktadır. Bunun için bağlı liste deüğümleri bir yapı ile ifade edilmektedir. Örneğin: typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; Bağlı listelerde ilk elemanın yeri bir biçimde bir gösterici tutulmalıdır. Genellikle ilk elemanın yerini tutan göstericiye "head göstericisi" denilmektdir. Örneğin: NODE *head; /* ilk elemanın yeri */ Bağlı listenin sonuna elemanın hızla eklenebilmesi için genellikle son elemanın yeri de tutulmaktadır. Son elemanın yerini tutan göstericiye de "tail göstericisi" denilmektedir. NODE *tail; /* son elemanın yeri */ Tipik olarak bağlı listenin son elemanın (düğümünün) next göstericisinde NULL adres bulunur. Bu durum listenin sonuna gelindiğini belirtmektedir. Bağlı listenin tüm elemanlarını gözden geçirmek basit döngüyle yapılabilir: NODE *node; ... node = head; while (node != NULL) { ... node = node->next; } Tabii bu dolaşımı for döngüsüyle daha kolay da ifade edebiliriz: for (NODE *node = head; node != NULL; node = node->next) { ... } Her düğümün yalnızca sonraki düğümün değil aynı zamanda önceki düğümün de yerini tuttuğu bağlı listelere "çift bağlı listeler (double linked list)" denilmektedir. Çift bağlı listeler belli bir düğümün adresini biliyorsak yalnızca ileriye doğru değil, geriye doğru da gidebiliriz. Çift bağlı listelerin düğümleri de aşağıdaki gibi bir yapıyla temsil edilebilir: typedef struct tagNODE { DATATYPE val; struct tagNODE *next; struct tagNODE *prev; } NODE; Çift bağlı listelerin bir düğümünün bellekte daha fazla yer kaplayacağına dikkat ediniz. Çift bağlı listelerin tek bağlı listelere göre en önemli özelliği "adresi bilinen bir düğümün" silinebilmesidir. Tek bağlı listelerde bu durum mümkün değildir. Uygulamalarda buna çok sık gereksinim duyulmaktadır. Eğer bir bağlı listede son eleman ilk elemanı gösteriyorsa bu tür bağlı listelere de "döngüsel bağlı listeler (circular linkes lists)" denilmektedir. Pekiyi bağlı listelere neden gereksinim duyulmaktadır? Diziler varken bağlı listelere gerek var mıdır? Dizilerle bağlı listeler arasındaki farklılkları, benzerlikleri ve bağlı listelere neden gereksinim duyulduğunu birkaç maddede açıklayabiliriz: -> Diziler ardışıl alana gereksinim duymaktadır. Ancak belleğin bölündüğü (fragmente olduğu) durumlarda bellekte yeteri kadar küçük boş alanlar olduğu halde bunlar ardışıl olmadığı için dizi tahsisatı mümkün olamamaktadır. Bu tür durumlarda ardışıllık gereksinimi olmayan bağlı listeler kullanılabilir. Özellikle heap gibi bir alanda çok sayıda dinamik diziler bellek kullanımı açısından verimsizliğe yol açabilmektedir. Bu dinamik diziler zamanla büyüdükçe birbirini engeller hale gelebilmektedir. İşte uzunluğu baştan belli olmayan çok sayıda dizinin oluşturulacağı durumlarda dinamik dizi yerine bağlı listeler toplamda daha iyi performans gösterebilmektedir. Dinamik dizilerde dinamik dizinin büyütülmesi yavaş bir işlemdir. Çünkü büyütme sırasında bloklar yer değiştirebilmektedir. -> Dizilerde araya eleman ekleme (insert etme) ve aradaki bir elemanı silme dizinin kaydırılmasına (expand ve shrink edilmesine) yol açacağından yavaş bir işlemdir. Teknik olarak dizilerde eleman isert etme ve eleman silme O(N) karmaşıklıkta bir işlemdir. Halbuki bağlı listelerde eğer düğümün yerini biliyorsak bu işlem O(1) karmaşıklıkta (yani döngü olmadan tekil işlemlerle) yapılabilmektedir. O halde araya eleman eklemenin ve aradan eleman silmenin çok yapıldığı sistemlerde bağlı diziler yerine bağlı listeler tercih edilebilmektedir. -> Bağlı listelerde belli bir indeksteki elemana erişmek O(N) karmaşıklıkta bir işlemdir. Halbuki dizilerde elemana erişim O(1) karmaşıklıkta yani çok hızlıdır. O halde belli bir indeks değeri ile elemana erişimin yoğun yapıldığı durumlarda bağlı listeler yerine diziler tercih edilmelidir. -> Bağlı listeler toplamda bellekte daha fazla yer kaplama eğilimindedir. Çünkü bağlı listenin her düğümü sonraki (ve duruma göre önceki) elemanın yerini de tutmaktadır. O halde bağlı listeler tipik şu durumlarda dizilere tercih edilmelidir: -> Eleman insert etmenin ve eleman silmenin çok yapıldığı durumlarda -> Uzunluğu baştan belli olmayan çok sayıda dizinin kullanıldığı durumda -> İndeks yoluyla erişimin az yapıldığı durumda -> Bellek miktarının yeteri kadar fazla olduğu sistemlerde Soyut bir veri türü (abstract data type) olarak bağlı listeler üzerinde şu işlemlerin yapılması beklenmektedir: -> Bağlı listenin başına ve sonuna eleman eklenmesi -> Bağlı listenin dolaşılması ve belli bir indeksteki elemanın elde edilmesi -> Adresi bilinen bir düğümün önüne ya da arkasına eleman insert edilmesi -> Adresi bilinen bir düğümün silinmesi Aşağıda çift bağlı listelerin gerçekleştirilmesine ilişkin bir örnek verilmiştir. * Örnek 1, /* llist.h */ #ifndef LLIST_H_ #define LLIST_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; struct tagNODE *prev; } NODE; typedef struct tagLLIST { NODE *head; NODE *tail; size_t count; } LLIST, *HLLIST; /* Function Prototypes */ HLLIST create_llist(void); NODE *add_tail(HLLIST hllist, DATATYPE val); NODE *addp_tail(HLLIST hllist, const DATATYPE *val); NODE *add_head(HLLIST hllist, DATATYPE val); NODE *addp_head(HLLIST hllist, const DATATYPE *val); NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val); NODE *insert_prev(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_prev(HLLIST hllist, NODE *node, const DATATYPE *val); void remove_node(HLLIST hllist, NODE *node); DATATYPE *getp_item(HLLIST hllist, size_t index); bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)); bool walk_llist_rev(HLLIST hllist, bool (*proc)(DATATYPE *)); void clear_llist(HLLIST hllist); void destroy_llist(HLLIST hllist); /* inline Function Definitions */ static inline size_t count_llist(HLLIST hllist) { return hllist->count; } #endif /* llist.c */ #include #include #include "llist.h" /* static Functions Prototypes */ static bool disp(DATATYPE *val); /* Function Definitions */ HLLIST create_llist(void) { HLLIST hllist; if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL) return NULL; hllist->head = hllist->tail = NULL; hllist->count = 0; return hllist; } NODE *add_tail(HLLIST hllist, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (hllist->head != NULL) /* is list is empty? */ hllist->tail->next = new_node; else hllist->head = new_node; new_node->prev = hllist->tail; new_node->next = NULL; hllist->tail = new_node; ++hllist->count; return new_node; } NODE *addp_tail(HLLIST hllist, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (hllist->head != NULL) /* is list is empty? */ hllist->tail->next = new_node; else hllist->head = new_node; new_node->prev = hllist->tail; new_node->next = NULL; hllist->tail = new_node; ++hllist->count; return new_node; } NODE *add_head(HLLIST hllist, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; new_node->next = hllist->head; new_node->prev = NULL; if (hllist->head == NULL) hllist->tail = new_node; else hllist->head->prev = new_node; hllist->head = new_node; ++hllist->count; return new_node; } NODE *addp_head(HLLIST hllist, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; new_node->next = hllist->head; new_node->prev = NULL; if (hllist->head == NULL) hllist->tail = new_node; else hllist->head->prev = new_node; hllist->head = new_node; ++hllist->count; return new_node; } NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (node != hllist->tail) node->next->prev = new_node; else hllist->tail = new_node; new_node->next = node->next; node->next = new_node; new_node->prev = node; ++hllist->count; return new_node; } NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (node != hllist->tail) node->next->prev = new_node; else hllist->tail = new_node; new_node->next = node->next; node->next = new_node; new_node->prev = node; ++hllist->count; return new_node; } NODE *insert_prev(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (node != hllist->head) node->prev->next = new_node; else hllist->head = new_node; new_node->next = node; new_node->prev = node->prev; node->prev = new_node; ++hllist->count; return new_node; } NODE *insertp_prev(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (node != hllist->head) node->prev->next = new_node; else hllist->head = new_node; new_node->next = node; new_node->prev = node->prev; node->prev = new_node; ++hllist->count; return new_node; } void remove_node(HLLIST hllist, NODE *node) { if (node == hllist->head) hllist->head = node->next; else node->prev->next = node->next; if (node == hllist->tail) hllist->tail = node->prev; else node->next->prev = node->prev; --hllist->count; free(node); } DATATYPE *getp_item(HLLIST hllist, size_t index) { NODE *node; if (index >= hllist->count) return NULL; node = hllist->head; for (size_t i = 0; i < index; ++i) node = node->next; return &node->val; } bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->head; node != NULL; node = node->next) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } bool walk_llist_rev(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->tail; node != NULL; node = node->prev) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } void clear_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } hllist->head = hllist->tail = NULL; hllist->count = 0; } void destroy_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } free(hllist); } static bool disp(DATATYPE *val) { printf("%d ", *val); fflush(stdout); return true; } /* app.c */ #include #include #include "llist.h" int main(void) { HLLIST hllist; NODE *node, *pos_node; DATATYPE *val; if ((hllist = create_llist()) == NULL) { fprintf(stderr, "cannot create linked list...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) { if ((node = add_tail(hllist, i)) == NULL) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } if (i == 9) pos_node = node; } walk_llist(hllist, NULL); remove_node(hllist, pos_node); walk_llist(hllist, NULL); if ((val = getp_item(hllist, 5)) == NULL) { fprintf(stderr, "invalid index!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); clear_llist(hllist); for (int i = 0; i < 10; ++i) if (add_tail(hllist, i) == NULL) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } walk_llist(hllist, NULL); destroy_llist(hllist); return 0; } * Örnek 2, Çift bağlı liste gerçekleştiriminde handle alanında head ve tail göstericilerini ayrı ayrı tutmak yerine NODE nesnesi tutulursa özel durumlar ortadan kaldırılabilir ve gerçekleştirim daha sade hale getirilebilir. Bu tasarımda ilk düğümün prev göstericisi ve son düğümün next göstericisi handle alanındaki düğümü göstermelidir. Benzer biçimde handle alanındaki düğümün next göstericisi ilk düğümün yerini prev göstericisi ise son düğümün yerini göstermelidir. Tabii bu durumda handle alanında tutulan NODE nesnesinin val elemanı aslında listeye dahil olmadığı için kullanılmayacaktır. Dolayısıyla bu eleman boşa yer kaplayacaktır. Ancak genellikle bu durum öncemsizdir. Bu tarzda gerçekleştirim programcılar tarafından daha fazla tercih edilmektedir. Bu biçimdeki tasarımda handle alanı aşağıdaki gibi olacaktır: typedef struct tagNODE { DATATYPE val; struct tagNODE *next; struct tagNODE *prev; } NODE; typedef struct tagLLIST { NODE head; size_t count; } LLIST, *HLLIST; Başlangıç durumunda handle alanı içerisindeki düğümün next ve prev göstericilerinin kendisini göstermesi gerekir. Tabii dolaşım yapılırken artık dolaşımın biteceği son düğümdeki NULL adresinden değil next göstericisinin handle alanı içerisindeki düğümün adresine eşit olmaması ile tespit edilebilecektir. Aşağıda çift bağlı listelerde handle alanında düğüm tutma yoluyla özel durumların elimine edilmesine bir örnek verilmiştir. Aşağıda ilgili programın kodları paylaşılmıştır: /* llist.h */ #ifndef LLIST_H_ #define LLIST_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; struct tagNODE *prev; } NODE; typedef struct tagLLIST { NODE head; size_t count; } LLIST, *HLLIST; /* Function Prototypes */ HLLIST create_llist(void); NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val); NODE *insert_prev(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_prev(HLLIST hllist, NODE *node, const DATATYPE *val); NODE *add_tail(HLLIST hllist, DATATYPE val); NODE *addp_tail(HLLIST hllist, const DATATYPE *val); NODE *add_head(HLLIST hllist, DATATYPE val); NODE *addp_head(HLLIST hllist, const DATATYPE *val); void remove_node(HLLIST hllist, NODE *node); DATATYPE *getp_item(HLLIST hllist, size_t index); bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)); bool walk_llist_rev(HLLIST hllist, bool (*proc)(DATATYPE *)); void clear_llist(HLLIST hllist); void destroy_llist(HLLIST hllist); /* inline Function Definitions */ static inline size_t count_llist(HLLIST hllist) { return hllist->count; } #endif /* llist.c */ #include #include #include "llist.h" /* static Functions Prototypes */ static bool disp(DATATYPE *node); /* Function Definitions */ HLLIST create_llist(void) { HLLIST hllist; if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL) return NULL; hllist->head.next = &hllist->head; hllist->head.prev = &hllist->head; hllist->count = 0; return hllist; } NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; node->next->prev = new_node; new_node->next = node->next; node->next = new_node; new_node->prev = node; ++hllist->count; return new_node; } NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; node->next->prev = new_node; new_node->next = node->next; node->next = new_node; new_node->prev = node; ++hllist->count; return new_node; } NODE *insert_prev(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; node->prev->next = new_node; new_node->next = node; new_node->prev = node->prev; node->prev = new_node; ++hllist->count; return new_node; } NODE *insertp_prev(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; node->prev->next = new_node; new_node->next = node; new_node->prev = node->prev; node->prev = new_node; ++hllist->count; return new_node; } NODE *add_tail(HLLIST hllist, DATATYPE val) { return insert_prev(hllist, &hllist->head, val); } NODE *addp_tail(HLLIST hllist, const DATATYPE *val) { return insertp_prev(hllist, &hllist->head, val); } NODE *add_head(HLLIST hllist, DATATYPE val) { return insert_next(hllist, &hllist->head, val); } NODE *addp_head(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, &hllist->head, val); } void remove_node(HLLIST hllist, NODE *node) { node->prev->next = node->next; node->next->prev = node->prev; --hllist->count; free(node); } DATATYPE *getp_item(HLLIST hllist, size_t index) { NODE *node; if (index >= hllist->count) return NULL; node = hllist->head.next; for (size_t i = 0; i < index; ++i) node = node->next; return &node->val; } bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } bool walk_llist_rev(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->head.prev; node != &hllist->head; node = node->prev) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } void clear_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } hllist->head.next = &hllist->head; hllist->head.prev = &hllist->head; hllist->count = 0; } void destroy_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } free(hllist); } static bool disp(DATATYPE *val) { printf("%d ", *val); fflush(stdout); return true; } /* app.c */ #include #include #include "llist.h" int main(void) { HLLIST hllist; NODE *node, *pos_node; DATATYPE *val; if ((hllist = create_llist()) == NULL) { fprintf(stderr, "cannot create linked list...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) { if ((node = add_tail(hllist, i)) == NULL) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } if (i == 0) pos_node = node; } walk_llist(hllist, NULL); remove_node(hllist, pos_node); walk_llist(hllist, NULL); if ((val = getp_item(hllist, 5)) == NULL) { fprintf(stderr, "invalid index!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); clear_llist(hllist); for (int i = 0; i < 10; ++i) if (add_tail(hllist, i) == NULL) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } walk_llist(hllist, NULL); destroy_llist(hllist); return 0; } * Örnek 3, Tek bağlı listelerde her düğüm yalnızca sonraki düğümün adresini tutmaktadır. Dolayısıyla geriye doğru ilerleme mümkün olmaz. Tak bağlı listelerde adresini bildiğimiz düğümün yalnızca önüne eleman insert edebiliriz. Benzer biçimde adresini bildiğimiz düğümü silemeyiz. Ancak adresini bildiğimiz düğümün önündeki düğümü silebiliriz. Bunun için oluşturulacak handle alanı şöyle olabilir: typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE *head; NODE *tail; size_t count; } LLIST, *HLLIST; Soyut bir veri türü olarak tek baalı listelerde gerçekleştirilebilecek işlemler şunlar olabilir: -> Listenin başına ve sonuna eleman eklenmesi -> Listenin dolaşılması ve belli bir indeksteki elemanın elde edilmesi -> Adresi bilinen bir düğümün önüne eleman insert edilmesi -> Adresi bilinen bir düğümün önündeki elemanın silinmesi Aşağıdaki örnekte bir tek bağlı liste gerçekleştirimi verilmiştir. /* llist.h */ #ifndef LLIST_H_ #define LLIST_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE *head; NODE *tail; size_t count; } LLIST, *HLLIST; /* Function Prototypes */ HLLIST create_llist(void); NODE *add_tail(HLLIST hllist, DATATYPE val); NODE *addp_tail(HLLIST hllist, const DATATYPE *val); NODE *add_head(HLLIST hllist, DATATYPE val); NODE *addp_head(HLLIST hllist, const DATATYPE *val); NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val); void remove_next(HLLIST hllist, NODE *node); DATATYPE *getp_item(HLLIST hllist, size_t index); bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)); void clear_llist(HLLIST hllist); void destroy_llist(HLLIST hllist); /* inline Function Definitions */ static inline size_t count_llist(HLLIST hllist) { return hllist->count; } #endif /* llist.c */ #include #include #include "llist.h" /* static Functions Prototypes */ static bool disp(DATATYPE *val); /* Function Definitions */ HLLIST create_llist(void) { HLLIST hllist; if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL) return NULL; hllist->head = hllist->tail = NULL; hllist->count = 0; return hllist; } NODE *add_tail(HLLIST hllist, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (hllist->tail != NULL) hllist->tail->next = new_node; else hllist->head = new_node; hllist->tail = new_node; new_node->next = NULL; ++hllist->count; return new_node; } NODE *addp_tail(HLLIST hllist, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (hllist->tail != NULL) hllist->tail->next = new_node; else hllist->head = new_node; hllist->tail = new_node; new_node->next = NULL; ++hllist->count; return new_node; } NODE *add_head(HLLIST hllist, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; new_node->next = hllist->head; if (hllist->head == NULL) hllist->tail = new_node; hllist->head = new_node; ++hllist->count; return new_node; } NODE *addp_head(HLLIST hllist, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; new_node->next = hllist->head; if (hllist->head == NULL) hllist->tail = new_node; hllist->head = new_node; ++hllist->count; return new_node; } NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } void remove_next(HLLIST hllist, NODE *node) { NODE *next_node; if (node == hllist->tail) return; if (node->next == hllist->tail) hllist->tail = node; next_node = node->next; node->next = next_node->next; --hllist->count; free(next_node); } bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->head; node != NULL; node = node->next) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } DATATYPE *getp_item(HLLIST hllist, size_t index) { NODE *node; if (index >= hllist->count) return NULL; node = hllist->head; for (size_t i = 0; i < index; ++i) node = node->next; return &node->val; } void clear_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } hllist->head = hllist->tail = NULL; hllist->count = 0; } void destroy_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } free(hllist); } static bool disp(DATATYPE *val) { printf("%d ", *val); fflush(stdout); return true; } /* app.c */ #include #include #include "llist.h" int main(void) { HLLIST hllist; NODE *node, *pos_node; int *val; if ((hllist = create_llist()) == NULL) { fprintf(stderr, "cannot create linked list...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) { if ((node = add_tail(hllist, i)) == NULL) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } if (i == 8) pos_node = node; } walk_llist(hllist, NULL); if ((val = getp_item(hllist, 5)) == NULL) { fprintf(stderr, "invalid index!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); remove_next(hllist, pos_node); walk_llist(hllist, NULL); clear_llist(hllist); for (int i = 0; i < 10; ++i) if ((node = add_tail(hllist, i)) == NULL) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } walk_llist(hllist, NULL); destroy_llist(hllist); return 0; } * Örnek 4, Tek bağlı listelerde de handle alanı içerisinde bir düğüm tutulursa özel durumlar elimine edilebilir. Ancak bunun sağlayacağı fayda çift bağlı listelerdeki gibi olmayacaktır. Çünkü tek bağlı listelerde düğüm içerisinde yalnızca next göstericisi bulunduğu için tail göstericisinin ayrıca handle alanında tutulması gerekecektir. Burada da son düğümün next göstericisinin artık NULL adresi değil handle alanındaki düğümü göstermesi gerekir. Tabii işin başında hem handle alanındaki düğümün next göstericisi hem de tail göstericisi handle alanındaki düğümün kendisini göstermlidir. Bu tasarımda handla alanı aşağıdaki gibi oluşturuabilir: typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE head; NODE *tail; size_t count; } LLIST, *HLLIST; Yapının head elemanının bir gösterici olmadığına NODE nesnesi olduğuna dikkat ediniz. Aşağıda bu tasarıma ilişkin bir örnek verilmiştir. /* llist.h */ #ifndef LLIST_H_ #define LLIST_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE head; NODE *tail; size_t count; } LLIST, *HLLIST; /* Function Prototypes */ HLLIST create_llist(void); NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val); NODE *add_tail(HLLIST hllist, DATATYPE val); NODE *addp_tail(HLLIST hllist, const DATATYPE *val); NODE *add_head(HLLIST hllist, DATATYPE val); NODE *addp_head(HLLIST hllist, const DATATYPE *val); void remove_next(HLLIST hllist, NODE *node); void remove_head(HLLIST hllist); DATATYPE *getp_item(HLLIST hllist, size_t index); bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)); void clear_llist(HLLIST hllist); void destroy_llist(HLLIST hllist); /* inline Function Definitions */ static inline size_t count_llist(HLLIST hllist) { return hllist->count; } #endif /* llist.c */ #include #include #include "llist.h" /* static Functions Prototypes */ static bool disp(DATATYPE *val); /* Function Definitions */ HLLIST create_llist(void) { HLLIST hllist; if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL) return NULL; hllist->head.next = &hllist->head; hllist->tail = &hllist->head; hllist->count = 0; return hllist; } NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *add_tail(HLLIST hllist, DATATYPE val) { return insert_next(hllist, hllist->tail, val); } NODE *addp_tail(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, hllist->tail, val); } NODE *add_head(HLLIST hllist, DATATYPE val) { return insert_next(hllist, &hllist->head, val); } NODE *addp_head(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, &hllist->head, val); } void remove_next(HLLIST hllist, NODE *node) { NODE *next_node; if (node == hllist->tail) return; if (node->next == hllist->tail) hllist->tail = node; next_node = node->next; node->next = next_node->next; --hllist->count; free(next_node); } void remove_head(HLLIST hllist) { remove_next(hllist, &hllist->head); } DATATYPE *getp_item(HLLIST hllist, size_t index) { NODE *node; if (index >= hllist->count) return NULL; node = hllist->head.next; for (size_t i = 0; i < index; ++i) node = node->next; return &node->val; } bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } void clear_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } hllist->head.next = &hllist->head; hllist->tail = &hllist->head; hllist->count = 0; } void destroy_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } free(hllist); } static bool disp(DATATYPE *val) { printf("%d ", *val); fflush(stdout); return true; } /* app.c */ #include #include #include "llist.h" int main(void) { HLLIST hllist; NODE *node, *pos_node; int *val; if ((hllist = create_llist()) == NULL) { fprintf(stderr, "cannot create linked list...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) { if ((node = add_tail(hllist, i)) == NULL) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } if (i == 5) pos_node = node; } walk_llist(hllist, NULL); if ((val = getp_item(hllist, 5)) == NULL) { fprintf(stderr, "invalid index!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); remove_next(hllist, pos_node); walk_llist(hllist, NULL); remove_head(hllist); walk_llist(hllist, NULL); clear_llist(hllist); for (int i = 0; i < 10; ++i) if ((node = add_tail(hllist, i)) == NULL) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } walk_llist(hllist, NULL); destroy_llist(hllist); return 0; } * Örnek 5, Tabii bağlı listenin tuttuğu elemanlar (yani DATATYPE türü) int yerine başka türlerden de olabilir. Eğer DATATYPE türü bir yapı ise fonksiyonların p'li versiyonlarını kullanmak daha uygun olacaktır. Aşağıdaki tek bağlı liste örneğinde DATATYPE bir yapı biçiminde oluşturulmuştur. /* llist.h */ #ifndef LLIST_H_ #define LLIST_H_ #include #include /* Type Declarations */ typedef struct tagPERSON { char name[32]; int no; } PERSON; typedef PERSON DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE head; NODE *tail; size_t count; } LLIST, *HLLIST; /* Function Prototypes */ HLLIST create_llist(void); NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val); NODE *add_tail(HLLIST hllist, DATATYPE val); NODE *addp_tail(HLLIST hllist, const DATATYPE *val); NODE *add_head(HLLIST hllist, DATATYPE val); NODE *addp_head(HLLIST hllist, const DATATYPE *val); void remove_next(HLLIST hllist, NODE *node); void remove_head(HLLIST hllist); DATATYPE *getp_item(HLLIST hllist, size_t index); bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)); void clear_llist(HLLIST hllist); void destroy_llist(HLLIST hllist); /* inline Function Definitions */ static inline size_t count_llist(HLLIST hllist) { return hllist->count; } #endif /* llist.c */ #include #include #include "llist.h" /* static Functions Prototypes */ static bool disp(DATATYPE *node); /* Function Definitions */ HLLIST create_llist(void) { HLLIST hllist; if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL) return NULL; hllist->head.next = &hllist->head; hllist->tail = &hllist->head; hllist->count = 0; return hllist; } NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *add_tail(HLLIST hllist, DATATYPE val) { return insert_next(hllist, hllist->tail, val); } NODE *addp_tail(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, hllist->tail, val); } NODE *add_head(HLLIST hllist, DATATYPE val) { return insert_next(hllist, &hllist->head, val); } NODE *addp_head(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, &hllist->head, val); } void remove_next(HLLIST hllist, NODE *node) { NODE *next_node; if (node == hllist->tail) return; if (node->next == hllist->tail) hllist->tail = node; next_node = node->next; node->next = next_node->next; --hllist->count; free(next_node); } void remove_head(HLLIST hllist) { remove_next(hllist, &hllist->head); } DATATYPE *getp_item(HLLIST hllist, size_t index) { NODE *node; if (index >= hllist->count) return NULL; node = hllist->head.next; for (size_t i = 0; i < index; ++i) node = node->next; return &node->val; } bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } void clear_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } hllist->head.next = &hllist->head; hllist->tail = &hllist->head; hllist->count = 0; } void destroy_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } free(hllist); } static bool disp(DATATYPE *val) { printf("%s, %d\n", val->name, val->no); fflush(stdout); return true; } /* app.c */ #include #include #include #include #include "llist.h" bool disp_person(PERSON *per); void clear_stdin(void); int main(void) { HLLIST hllist; PERSON per; char *str; if ((hllist = create_llist()) == NULL) { fprintf(stderr, "cannot create linked list...\n"); exit(EXIT_FAILURE); } for (;;) { printf("Adi soyadi:"); if (fgets(per.name, 32, stdin) == NULL) continue; if ((str = strchr(per.name, '\n')) != NULL) *str = '\0'; if (!strcmp(per.name, "quit")) break; printf("No:"); scanf("%d", &per.no); clear_stdin(); addp_tail(hllist, &per); } walk_llist(hllist, disp_person); destroy_llist(hllist); return 0; } bool disp_person(PERSON *per) { printf("%s, %d\n", per->name, per->no); return true; } void clear_stdin(void) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) ; } >> Çok karşılaşılan diğer bir veri yapısı da "kuyruk (queue)" veri yapısıdır. Kuyruklar FIFO ve LIFO olmak üzere ikiye ayrılmaktadır. >>> Ancak kuyruk denildiğinde default olarak FIFO kuyruklar anlaşılmaktadır. Kuyruk veri yapısında iki eylem vardır: Kuyruğua eleman yerleştirmek ve kuyruktan eleman almak. Kuyruğun arasına eleman insert etmenin bir anlamı yoktur. Aradan eleman silmenin de bir anlamı yoktur. Kuyruklara yalnızca eleman yerleştirilip kuyruklardan yalnızca eleman alınmaktadır. FIFO kuyruk sistemine eleman yerleştirileceği zaman eleman her zaman sona yerleştirilmektedir. Bu kuyruk sisteminden eleman alınacağı zaman eleman baştan alınmaktadır. FIFO kuyruk sistemleri gerçek hayatta çokça karşımıza çıkmaktadır. Örneğin gerçek hayattaki kuyrukların çoğu bir FIFO kuyruk sistemidir. Yemekhanede kuyruğun sonuna gireriz. Kuyruğun başındaki kişiye yemek verilir. Bu yönüyle FIFO kuyruk sistemi adil bir kuyruk sistemidir. FIFO kuyruk sistemleri "bilgilerin sırası bozulmadan geçici olarak bekletileceği" durumlarda kullanılmaktadır. Yani tampon sistemleri genellikle FIFO kuyruk sistemi biçiminde oluşturulmaktadır. Örneğin bir yerden bilgi geliyor olsun. Bu bilgiyi o anda işleyemiyor olalım. O zaman gelen bilgiyi geçici süre bir FIFO kuyruk sisteminde tutabiliriz. Daha sonra oradan alarak geldiği sırada işleyebiliriz. Aslında daha önce görmüş olduğumuz borularda bir FIFO kuyruk sistemi ile gerçekleştirilmektedir. Örneğin boruya bir şey yazıldığında bu şeyler borunun sonuna yazılır. Biz de boruyu okuduğumuzda başındakileri alırız. Kuyruk sistemine eleman yerleştirmeye İngilizce genellikle "put" ya da "enqueue" işlemi denilmektedir. Kuyruktan eleman alamaya da İngilizce genellikle "get" ya da "dequeue" işlemi denilmektedir. Kuyruk sistemlerinin bir uzunlukları olabilir. Kuyruk dolduğunda artık kuyruğa put yapılamamaktadır. Benzer biçimde kuruk tamamen boşaldığında artık "get" yapılamamaktadır. FIFO kuyruk sistemlerinin gerçekleştirilmesi için üç yöntem kullanılmaktadır: -> Dizi Kaydırması Yöntemi: Bu yöntem ilk akla gelen yöntemdir. Ancak diğer yöntemlerden bariz kötü olduğu için tercih edilmemektedir. Bu yöntemde kuyruk için bir dizi yaratılır. Dizinin sonu (elemanların bittiği yer dizinin gerçek sonu değil) bir indeksle ya da gösterici ile tutulur. Örneğin: abcdexxxxxxxxxxxxx ^ Burada a, b, c, d, e kuruktaki elemanları x'ler ise dizide henüz kullanılmayan boşi alanları belirtmektedir. Bu yöntemde kuyuğa eleman ekleneceği zaman sona ekleme yapılır. Örneğin: abcdefxxxxxxxxxxxx ^ Kuyruktan eleman alınacağı zaman eleman kuruğun başından alınır ve kuyruktaki elemanlar bir kaydırılır. Örneğin; bcdefxxxxxxxxxxxx ^ Bu tasarımda kuyuğa eleman yerleştirmek O(1) karmaşıklıktayken kuyruktan eleman almak O(N) karmaşıklıktadır. -> İndeks Kaydırması Yöntemi: En çok kullanılan yöntemlerden biridir. Bu yöntemde kuyruğun başı ve sonu iki indeks ya da gösterici ile tutturulur. Kuruğun bgaşını gösteren indeks ya da göstericiye genellikle İngilizce "head" göstericisi, kuyrun sonunu gösteren indeks ya da göstericiye de "tail" göstericisi denilmektedir. Örneğin: xxxxabcdefgxxxxxx ^(h) ^(t) Eleman tail göstericisinin gösterdeiği yere yerleştirilir ve tail göstericisi 1 artırılır. Ancak eğer tail göstericisi dizinin sonuna gelmişse yeniden başa geçirilir. Örneğin: xxxxabcdefghxxxxxxx ^(h) ^(t) Eleman head göstericisinin gösteridği yerden alınır ve head göstericisi 1 artırılır. Tabii yine head göstericisi kuyruğun sonuna geldiğinde yeniden başa geçirilir. Bu yöntemde head ve tail göstericileri aynı yeri gösteriyorsa ya kuyruk tamamen doludur ya da kuyruk tamamen boştur. Genellikle kuyruktaki eleman sayısı da bir değişkenle tutulmaktadır. Kuyruğun tam dolu ya da tam boş olduğuna bu değişkene bakılarak karar verilir. Bu yöntemde eleman ekleme de eleman alma da O(1) karmaşıklıkta yapılabilmektedir. İndeks kaydırma yönteminde kuyruk dolduğunda tıpkı dinamik dizilerde olduğu gibi kuyruk büyütülebilir. Ancak kuyruğun büyütülüp büyütülmeyeceği uygulamadan uygulamaya değişebilmektedir. * Örnek 1, Aşağda indeks kaydırma yöntemiyle bir kuyruk sistemi oluşturulmuştur. Kuyruk bilgileri aşağdaki yapıyla temsil edilmiştir: typedef struct tagQUEUE { DATATYPE *queue; size_t head; size_t tail; size_t size; size_t count; } QUEUE, *HQUEUE; Handle alanı içerisinde kuyruk verilerinin bulunduğu dizi, head ve tail indeksleri, kuyruğun toplam uzunluğu ve kuyruktaki dolu elemanların sayısının tutulduuna dikkat ediniz. /* queue.h */ #ifndef QUEUE_H_ #define QUEUE_H_ #include #include typedef int DATATYPE; typedef struct tagQUEUE { DATATYPE *queue; size_t head; size_t tail; size_t size; size_t count; } QUEUE, *HQUEUE; /* Function Prototypes */ HQUEUE create_queue(size_t size); bool put_queue(HQUEUE hqueue, DATATYPE val); bool putp_queue(HQUEUE hqueue, const DATATYPE *val); bool get_queue(HQUEUE hqueue, DATATYPE *val); bool resize_queue(HQUEUE hqueue, size_t newsize); void clear_queue(HQUEUE hqueue); void destroy_queue(HQUEUE hqueue); /* inline Function Definitions */ static inline bool isempty_queue(HQUEUE hqueue) { return hqueue->count == 0; } static inline bool count_queue(HQUEUE hqueue) { return hqueue->count; } #endif /* queue.c */ #include #include #include "queue.h" HQUEUE create_queue(size_t size) { HQUEUE hqueue; if ((hqueue = (HQUEUE)malloc(sizeof(QUEUE))) == NULL) return NULL; if ((hqueue->queue = (DATATYPE *)malloc(sizeof(DATATYPE) * size)) == NULL) { free(hqueue); return NULL; } hqueue->head = hqueue->tail = 0; hqueue->size = size; hqueue->count = 0; return hqueue; } bool put_queue(HQUEUE hqueue, DATATYPE val) { if (hqueue->count == hqueue->size) return false; hqueue->queue[hqueue->tail++] = val; hqueue->tail %= hqueue->size; ++hqueue->count; return true; } bool putp_queue(HQUEUE hqueue, const DATATYPE *val) { if (hqueue->count == hqueue->size) return false; hqueue->queue[hqueue->tail++] = *val; hqueue->tail %= hqueue->size; ++hqueue->count; return true; } bool get_queue(HQUEUE hqueue, DATATYPE *val) { if (hqueue->count == 0) return false; *val = hqueue->queue[hqueue->head++]; hqueue->head %= hqueue->size; --hqueue->count; return true; } bool resize_queue(HQUEUE hqueue, size_t newsize) { DATATYPE *new_queue; if (newsize <= hqueue->size) return false; if ((new_queue = (DATATYPE *)malloc(newsize * sizeof(DATATYPE))) == NULL) return false; for (size_t i = 0, head = hqueue->head; i < hqueue->size; ++i) { new_queue[i] = hqueue->queue[head++]; head %= hqueue->size; } hqueue->head = 0; hqueue->tail = hqueue->count; hqueue->size = newsize; free(hqueue->queue); hqueue->queue = new_queue; return true; } void clear_queue(HQUEUE hqueue) { hqueue->count = 0; hqueue->head = 0; hqueue->tail = 0; } void destroy_queue(HQUEUE hqueue) { free(hqueue->queue); free(hqueue); } /* app.c */ #include #include #include "queue.h" int main(void) { HQUEUE hqueue; DATATYPE val; if ((hqueue = create_queue(10)) == NULL) { fprintf(stderr, "cannot create queue!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 8; ++i) if (!put_queue(hqueue, i)) { fprintf(stderr, "cannot put queue!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 5; ++i) get_queue(hqueue, &val); if (!resize_queue(hqueue, 20)) { fprintf(stderr, "cannot resize queue!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 15; ++i) if (!put_queue(hqueue, i)) { fprintf(stderr, "cannot put queue!..\n"); exit(EXIT_FAILURE); } while (!isempty_queue(hqueue)) { get_queue(hqueue, &val); printf("%d ", val); fflush(stdout); } destroy_queue(hqueue); return 0; } -> Bağlı Liste Yöntemi: Bu yöntemde bir bağlı liste oluşturulur. Eleman bağlı listenin sonuna eklenir ve başından alınır. Bağlı liste tek bağlı liste biçiminde oluşturulabilir. Bu yöntemde sanki eleman ekleme ve alma O(1) karmaşıklıkta yapılıyormuş gibi gözükse de eğer elemanlar dinamik olarak tahsis edilip free edeilecekse genellikle bu işlemler O(N) karmaşıklıkta yapılmaktadır. (Tabii O(1) karmaşıklıkta çalışan tahsisat algoritmaları da vardır. Ancak Windows ve UNIX/Linux sistemlerindeki malloc ve free algoritmaları O(N) karmaşıklıkta gerçekleştirilmiştir. Bu yöntemin avantajı baştan kuyruk uzunluğunun belirli olmasına gerek kalmamasıdır.) * Örnek 1, Aşağıdaki örnekte FIFO kuyruk bağlı liste yöntemi gerçekleştirilmiştir. Bu örnekte handle alanı aşağıdaki gibi bir yapı ile temsil edilmiştir: typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagQUEUE { NODE *head; NODE *tail; size_t count; } QUEUE, *HQUEUE; Handle alanı içerisinde tek bağlı listenin head ve tail düğümleri ve kuyruktaki eleman sayısı tutulmuştur. Örneğimizde kuyruğa eleman yerleştirirken bağlı listenin sonuna elemanı ekledik. Kuyruktan eleman alınırken de bağlı listenin başından elemanı aldık. /* queue.h */ #ifndef QUEUE_H_ #define QUEUE_H_ #include #include typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagQUEUE { NODE *head; NODE *tail; size_t count; } QUEUE, *HQUEUE; /* Function Prototypes */ HQUEUE create_queue(voids); bool put_queue(HQUEUE hqueue, DATATYPE val); bool putp_queue(HQUEUE hqueue, const DATATYPE *val); bool get_queue(HQUEUE hqueue, DATATYPE *val); void clear_queue(HQUEUE hqueue); void destroy_queue(HQUEUE hqueue); /* inline Function Definitions */ static inline bool isempty_queue(HQUEUE hqueue) { return hqueue->count == 0; } static inline bool count_queue(HQUEUE hqueue) { return hqueue->count; } #endif /* queue.c */ #include #include #include "queue.h" HQUEUE create_queue(void) { HQUEUE hqueue; if ((hqueue = (HQUEUE)malloc(sizeof(QUEUE))) == NULL) return NULL; hqueue->head = hqueue->tail = NULL; hqueue->count = 0; return hqueue; } bool put_queue(HQUEUE hqueue, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return false; new_node->val = val; new_node->next = NULL; if (hqueue->tail != NULL) hqueue->tail->next = new_node; else hqueue->head = new_node; hqueue->tail = new_node; ++hqueue->count; return true; } bool putp_queue(HQUEUE hqueue, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return false; new_node->val = *val; new_node->next = NULL; if (hqueue->tail != NULL) hqueue->tail->next = new_node; else hqueue->head = new_node; hqueue->tail = new_node; ++hqueue->count; return true; } bool get_queue(HQUEUE hqueue, DATATYPE *val) { NODE *node; if (hqueue->head == NULL) return false; node = hqueue->head; *val = node->val; if (node->next == NULL) hqueue->tail = NULL; hqueue->head = node->next; --hqueue->count; free(node); return true; } void clear_queue(HQUEUE hqueue) { NODE *node, *temp_node; node = hqueue->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } hqueue->head = hqueue->tail = NULL; hqueue->count = 0; } void destroy_queue(HQUEUE hqueue) { NODE *node, *temp_node; node = hqueue->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } free(hqueue); } /* app.c */ #include #include #include "queue.h" int main(void) { HQUEUE hqueue; DATATYPE val; if ((hqueue = create_queue(10)) == NULL) { fprintf(stderr, "cannot create queue!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 8; ++i) if (!put_queue(hqueue, i)) { fprintf(stderr, "cannot put queue!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 15; ++i) if (!put_queue(hqueue, i)) { fprintf(stderr, "cannot put queue!..\n"); exit(EXIT_FAILURE); } while (!isempty_queue(hqueue)) { get_queue(hqueue, &val); printf("%d ", val); fflush(stdout); } destroy_queue(hqueue); return 0; } >>> LIFO (Last In First Out) kuyruk sistemlerine "Stack Veri Yapısı" da denilmektedir. Stack veri yapısında yine iki eylem vardır. Stack'e eleman eklemek ve stack'ten eleman almak. Geleneksel olarak stack'e eleman yerleştirmeye "push" işlemi, stack'ten eleman almaya da "pop" işlemi denilmektedir. LIFO kuyruk sistemleri FIFO kuyruk sistemlerine göre daha seyrek kullanılmaktadır. Dış dünyada seyrek de olsa stack sistemleriyle karşılaşılmaktadır. Örneğin: -> Tabaklar üst üste konulduğunda en üsttek ilk alınmaktadır. -> Asansöre son binen çoğu kez ilk inmektedir. -> Bazı oyun programlarında oyun kağıtları üst üste yere atıldığında oyuncular son atılan kağıdı yerden alabilmektedir. Bilgisayar dünyasında da stack sistemleriyle karşılaşılmaktadır. Örneğin "undo mekanizması" stack veri yapısıyla gerçekleştirilmektedir. Yani örneğin biz bir editörde birtakım şeyler yaptıktan sonra "Ctrl+Z" tuşlarına basarsak son yapılandna ilk yapılana doğru eylemler geri alınmaktadır. Mikroişlemciler de stack sistemini fonksiyon çağrılarında ve yerel değişkenleri depolamada kullanmaktadır. Örneğin birkaç fonksiyon peşi sıra çağrıldığında geri dönüşler son çağırmadan ilk çağırmaya doğru yapılmaktadır. Parsing algoritmalarında stack veri yapısı yoğun olarak kullanılmaktadır. Örneğin RPN (Rverse Polish Notation) hesap makineleri stack veri yapısı kullanmaktadır. Stack bir bilgiyi ters yüz etmek için kullanılabilir. Stack veri yapısı yine "dizi yoluyla" ya da "bağlı liste yoluyla" gerçekleştirilebilmektedir. Dizi gerçekleştiriminde belli uzunlukta bir dizi yaratılır. Stack'in aktif noktası bir göstericiyle (ya da indeksle) belirlenir. Stack'in aktif noktasını tutan bu göstericiye geleneksel olarak "stack göstericisi (stack pointer)" denilmektedir. Başlangıçta stack boştur. Stack göstericisi dizinin soununu göstermektedir. Örneğin: x x x x x SP --> Push işleminde stack göstericisi önce bir azaltılır. Sonra push edilecek değer göstericisinin gösterdiği yere yerleştirilir. Örneğin, a değerini yukarıdaki stack'e yerleştirelim: x x x x x SP ---> a Şimdi de b değerine stack'e push edelim: x x x x SP ---> b a Şmdi de c değerini push edelim: x x x SP ---> c b a Pop işleminde tam tersi yapılır. Yani Stack göstericisinin gösterdiği yerdne bilgi alınır ve stack göstericisi 1 ilerletilir. Örneğin yukarıdaki durumda pop işlemi yapalım: x x x c SP ---> b a Şimdi bir daha pop yapalım: x x x c b SP ---> a Eğer stack uzunluğundan daha fazla psuh işlemi yapılırsa (yani stack doluyken push işlemi yapılırsa) stack için ayrılan alanı yukarından taşırmış oluruz. Bu duruma geleneksel olarak "stack'in yukarıdan taşması (stack overflow)" denilmektedir. eğer çok fazla pop işlemi yaparsak (yani boş bir stack'ta pop işlemi yaparsak) bu durumda stack için ayrılan diziyi aşağıdan taşırmış oluruz. Buna da geleneksel olarak "stack underflow" denilmektedir. Stack sistemleri tek bağlı listelerle de gerçekleştirilebilir. Bir bağlı listenin elemanlar önünne eklenip öününden alınırsa zaten bu stack olur. * Örnek 1, Aşağıda stack veri yapısının dizi kullanılarak gerçekleştirimine bir örnek verilmiştir. Bu örnekte stack veri yapısı (handle alanı) STACK isimli bir yapı ile temsil edilmiştir: typedef struct tagSTACK { DATATYPE *stack; DATATYPE *sp; size_t size; size_t count; } STACK, *HSTACK; Yapı içerisinde stack için kullanılacak dizinin başlangıç adresi, stack göstericinin durumu, stack dizinin uzunluğu ve stack'teki eleman sayısı tutulmuştur. /* stack.h */ #ifndef STACK_H_ #define STACK_H_ #include #include typedef int DATATYPE; typedef struct tagSTACK { DATATYPE *stack; DATATYPE *sp; size_t size; size_t count; } STACK, *HSTACK; /* Function Prototypes */ HSTACK create_stack(size_t size); bool push_stack(HSTACK hstack, DATATYPE val); bool pushp_stack(HSTACK hstack, const DATATYPE *val); bool pop_stack(HSTACK hstack, DATATYPE *val); void clear_stack(HSTACK hstack); void destroy_stack(HSTACK hstack); /* inline Function Definitions */ static inline size_t count_stack(HSTACK hstack) { return hstack->count; } static inline bool isempty_stack(HSTACK hstack) { return hstack->count == 0; } #endif /* stack.c */ #include #include #include "stack.h" HSTACK create_stack(size_t size) { HSTACK hstack; if ((hstack = (HSTACK)malloc(sizeof(STACK))) == NULL) return NULL; if ((hstack->stack = (DATATYPE *)malloc(sizeof(DATATYPE) * size)) == NULL) { free(hstack); return NULL; } hstack->sp = hstack->stack + size; hstack->size = size; hstack->count = 0; return hstack; } bool push_stack(HSTACK hstack, DATATYPE val) { if (hstack->count >= hstack->size) return false; *--hstack->sp = val; ++hstack->count; return true; } bool pushp_stack(HSTACK hstack,const DATATYPE *val) { if (hstack->count >= hstack->size) return false; *--hstack->sp = *val; ++hstack->count; return true; } bool pop_stack(HSTACK hstack, DATATYPE *val) { if (hstack->count == 0) return false; *val = *hstack->sp++; --hstack->count; return true; } void clear_stack(HSTACK hstack) { hstack->sp = hstack->stack + hstack->size; hstack->count = 0; } void destroy_stack(HSTACK hstack) { free(hstack->stack); free(hstack); } /* app.c */ #include #include #include "stack.h" int main(void) { HSTACK hstack; DATATYPE val; if ((hstack = create_stack(10)) == NULL) { fprintf(stderr, "cannot create stack!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) if (!push_stack(hstack, i)) { fprintf(stderr, "cannot push stack!..\n"); exit(EXIT_FAILURE); } while (!isempty_stack(hstack)) { pop_stack(hstack, &val); printf("%d ", val); fflush(stdout); } printf("\n"); destroy_stack(hstack); return 0; } * Örnek 2, Aşağıda stack veri yapısının tek bağlı liste ile gerçekleştirime ilişkin bir örnek verilmiştir. Burada NODE yapısı ve handle alanını temsil eden STACK yapısı aşağıdaki gibi bildirilmiştir: typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagSTACK { NODE *head; size_t count; } STACK, *HSTACK; Bu gerçekleştirimde düğüm bağlı listenin önüne eklenip önünden alınmaktadır. Handle alanında tail göstericisinin tutulmasına gerek olmadığına dikkat ediniz. /* stack.h */ #ifndef STACK_H_ #define STACK_H_ #include #include typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagSTACK { NODE *head; size_t count; } STACK, *HSTACK; /* Function Prototypes */ HSTACK create_stack(void); bool push_stack(HSTACK hstack, DATATYPE val); bool pushp_stack(HSTACK hstack, const DATATYPE *val); bool pop_stack(HSTACK hstack, DATATYPE *val); void clear_stack(HSTACK hstack); void destroy_stack(HSTACK hstack); /* inline Function Definitions */ static inline size_t count_stack(HSTACK hstack) { return hstack->count; } static inline bool isempty_stack(HSTACK hstack) { return hstack->count == 0; } #endif /* stack.c */ #include #include #include "stack.h" HSTACK create_stack(void) { HSTACK hstack; if ((hstack = (HSTACK)malloc(sizeof(STACK))) == NULL) return NULL; hstack->head = NULL; hstack->count = 0; return hstack; } bool push_stack(HSTACK hstack, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return false; new_node->val = val; new_node->next = hstack->head; hstack->head = new_node; ++hstack->count; return true; } bool pushp_stack(HSTACK hstack,const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return false; new_node->val = *val; new_node->next = hstack->head; hstack->head = new_node; ++hstack->count; return true; } bool pop_stack(HSTACK hstack, DATATYPE *val) { NODE *node; if (hstack->head == NULL) return false; node = hstack->head; hstack->head = node->next; *val = node->val; free(node); --hstack->count; return true; } void clear_stack(HSTACK hstack) { NODE *node, *temp_node; node = hstack->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } hstack->head = NULL; hstack->count = 0; } void destroy_stack(HSTACK hstack) { NODE *node, *temp_node; node = hstack->head; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } free(hstack); } /* app.c */ #include #include #include "stack.h" int main(void) { HSTACK hstack; DATATYPE val; if ((hstack = create_stack()) == NULL) { fprintf(stderr, "cannot create stack!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) if (!push_stack(hstack, i)) { fprintf(stderr, "cannot push stack!..\n"); exit(EXIT_FAILURE); } while (!isempty_stack(hstack)) { pop_stack(hstack, &val); printf("%d ", val); fflush(stdout); } printf("\n"); for (int i = 0; i < 10; ++i) if (!push_stack(hstack, i)) { fprintf(stderr, "cannot push stack!..\n"); exit(EXIT_FAILURE); } while (!isempty_stack(hstack)) { pop_stack(hstack, &val); printf("%d ", val); fflush(stdout); } printf("\n"); destroy_stack(hstack); return 0; } >> Çift Yönlü Dinamik Diziler: Diğer önemli bir veri yapısı da "çift yönlü dinamik diziler (double-ended queue)" denilen veri yapılarıdır. Çift yönlü dinamik diziler normal dinamik dizilere benzemekle birlikte bunların başına ve sonuna eleman eklenmesi, baştan ve sondan eleman silinmesi O(1) karmaşıklıktadır. (Halbuki normal dinamik dizilerde başa eleman eklenmesi ve baştaki elemanın silinmesi dizinin tümden kaydırılmasını gerektirdiği için O(N) karmaşlıklıktadır.) Çift yönlü dizmaik dizilere kısaca İngilizce "deque" de denilmektedir. Bu sözcük "deck" gibi okunmaktadır. Bazı yazarlar bu veri yapısının kısa ismi için "dequeue" ismini kullanıyorsa da bu isim "kuyruktan veri almak" anlamına gelen sözcükle aynı olduğu karışıklığa yol açmaktadır. Bu veri yaısının yaygın kısa ismi "deque" biçimindedir. Çift yönlü dinamik dizilerde elemana erişim yine sabit karmaşıklıkta yapılmaktadır. Araya eleman insert edilmesi ve aradan eleman silinmesi O(N) karmaşıklıktadır. O halde bu veri yapısının en önemli özelliği başa eleman eklemenin ve baştaki elemanı silmenin çok hızlı olmasıdır. Çift yönlü dinamik diziler pek çok kütüphanede (örneğin C++'ın Standart Kütüphanesinde) "kuyruk" ve "stack" veri yapısının gerçekleştiriminde temel bir veri yapısı olarak kullanılmaktadır. Örneğin C++ Standart Kütüphanesinde FIFO kuyruk sistemi "bir deque'in sonuna eleman insert edip başındaki elemanı almak" biçiminde gerçekleştirilmiştir. Bnezer biçimde "stack" veri yapısı da "deque'in başına eleman yerleştirip başından, eleman almakla gerçekleştirilmiştir. Çift yönlü dinamik diziler tipik olarak üç biçimde gerçekleştirilmektedir. Bu gerçekleştirimlerin etkinliği birbirine yakındır. Özel durumlar dikkat alınarak hangi gerçekleştirimin tercih edileceğine karar verilebilir. Bu gerçekleştirimler şunlardır: >>> İndeks Kaydırma Yöntemi ile Gerçekleştirim: Burada bir kuyruk sistemi oluşturulur. Yine kuyruğun başı ve sonu birer gösterici ya da indeks ile tutulur. Sona ekleme tail göstericisinin gösteriği yere, başa ekleme head göstericisinin gösterdiği yere yapılır. Diğer işlemler kuyruk sistemlerindeki gerçekleştirim ile benzerdir. Kuyruk dolduğunda yine normal dinamik dizilerde olduğu gibi iki kat artırım yoluna gidilir. Başa ve sona eleman ekleme, baştaki ve sondaki elemanın silinmesi O(1) karmaşıklıkta gerçekleştirilebilir. Elemana O(1) karmaşıklıkta erişilebilir. >>> Dinamik Dizi Yoluyla Gerçekleştirim: Bu gerçekleştirimde bir dinamik dizi oluşturulur. Başlangıçta head ve tail göstericileri bu dinamik dizinin ortasında bir yeri gösterir. Sona eklemeler yine tail göstericisinin bulunduğu yere, başa eklemeler ise head göstericisinin bulunduğu yere yapılır. Yani eklemelerde tail sağa doğru, head sola doğru ilerler. Baştan eleman silinmesinde ise head sağa doğru, sondan eleman silinmesinde ise tail sola doğru ilerleyecektir. head ve tail göstericileri iki uçtan herhangi birine eriştiğinde dizi büyütülecektir. Bu gerçekleştirimde kullanılmayan boş alanların bulunma olasılığı artmaktadır. Bazen çift yönlü dinamik dizi yalnızca bir yöne ilerleyebilir. Yani sona ekleme ve baştan silme işlemleri yoğun olabilir. Bu durumda head ve tail göstericileri dizinin sonlarına yakın bölgelerde konumlanabilirler. Ancak yeniden tahsisat yapıldığında bunlar yeni uygun konumlarına taşınabilirler. >>> Birden Fazla Dizinin Kullanılması Yöntemi: Bu gerçekleştirimde tek bir dizi değil belli uzunluklarda birden fazla dizi kullanılır. Örneğin her biri 64 byte uzunluğunda dizilerin kullanıldığını varsayalım. Bir dizi yetmeyince yeni bir 64 byte'lık dizi tahsis edilecektir. Tabii bu dizilerin adreslerinin de bir yerde saklanması gerekir. Tipik olarak bu dizilerin adresleri dinamik bir gösterici dizisinde saklanmaktadır. Tabii bu durumda bu dinamik gösterici dizisinin kaydırılması gerekecektir. Ancak bu dizi çok bütük olmazsa bu kaydırma önemli bir zamana yol açmayabilir. Bu gerçekleştirimde yine elemana erişim O(1) karmaşıklıkta tutulabilir. Başa ve sona eleman ekleme, baştan ve sondan eleman silme yine O(1) karmaşıkıkta yapılabilir. Ancak eleman ekleme sırasında dizilerin adreslerini tutan dizide kaydırmalar söz konusu olabilecektir. Bu nedenle eleman ekleme ve silme işlemi aslında tam O(1) karmaşıklıkta değil "amortized O(1)" karmaşıklıktadır. Aşağıda, bir deque gerçekleştirimine örnek verilmiştir: * Örnek 1, Bu gerçekleştirimde deque bir juruk sistemi gibi oluturulmuştur. Veri yaosının bilgilerini tutan handle alanı şöyledir: typedef int DATATYPE; typedef struct tagDEQUE { DATATYPE *deque; size_t capacity; size_t count; size_t head; size_t tail; } DEQUE, *HDEQUE; Bu gerçekleştirimde deque için başlangıçta DEQUE_DEF_CAPACITY (8) elemanlık yer ayrılmaktadır. Deque dolunca öncekinin iki katı uzunluğunda yeni bir aalan tahsis edilmiş ve eski alandan yeni alana kopyalama yapılmıştır. Deque veri yapısında başa ve sona eleman eklemek, baştan ve sondan eleman silmek O(1) karmaşıklıktadır. Ancak araya eleman insert etmek ya da aradan eleman silmek O(N) karmaşıklıktadır. Biz aşağıdaki gerçekleştirimde araya eleman ekleme ve aradan eleman işlemini kuyruğun sonunu referans alarak kaydırma yoluyla yaptık. Aslında insert ve remove pozisyonları hangi uca daha yakınsa kaydırma ona göre de yapılabilirdi. /* deque.h */ #ifndef DEQUE_H_ #define DEQUE_H_ #include #include /* Symbolic Constants */ #define DEQUE_DEF_CAPACITY 8 #define DEQUE_FAILED ((size_t)-1) #define MIN(a, b) ((a) < (b) ? (a) : (b)) /* Type Declerations */ typedef int DATATYPE; typedef struct tagDEQUE { DATATYPE *deque; size_t capacity; size_t count; size_t head; size_t tail; } DEQUE, *HDEQUE; /* Function Prototypes */ HDEQUE create_deque(void); size_t add_back_deque(HDEQUE hdeque, DATATYPE val); size_t add_backp_deque(HDEQUE hdeque, const DATATYPE *val); size_t add_front_deque(HDEQUE hdeque, DATATYPE val); size_t add_frontp_deque(HDEQUE hdeque, const DATATYPE *val); DATATYPE at_deque(HDEQUE hdeque, size_t index); void atp_deque(HDEQUE hdeque, size_t index, DATATYPE *val); DATATYPE pop_front_deque(HDEQUE hdeque); void pop_frontp_deque(HDEQUE hdeque, DATATYPE *val); size_t insert_deque(HDEQUE hdeque, size_t index, DATATYPE val); size_t remove_deque(HDEQUE hdeque, size_t index); DATATYPE *set_capacity_deque(HDEQUE hdeque, size_t new_capacity); void clear_deque(HDEQUE hdeque); void destroy_deque(HDEQUE hdeque); /* inline Function Definitions */ static inline size_t count_deque(HDEQUE hdeque) { return hdeque->count; } static inline size_t capacity_deque(HDEQUE hdeque) { return hdeque->capacity; } #endif /* deque.c */ #include #include #include #include "deque.h" HDEQUE create_deque(void) { HDEQUE hdeque; if ((hdeque = (HDEQUE)malloc(sizeof(DEQUE))) == NULL) return NULL; if ((hdeque->deque = (DATATYPE *)malloc(DEQUE_DEF_CAPACITY * sizeof(DATATYPE))) == NULL) { free(hdeque); return NULL; } hdeque->count = 0; hdeque->capacity = DEQUE_DEF_CAPACITY; hdeque->head = hdeque->tail = 0; return hdeque; } size_t add_back_deque(HDEQUE hdeque, DATATYPE val) { if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL) return DEQUE_FAILED; hdeque->deque[hdeque->tail++] = val; hdeque->tail %= hdeque->capacity; ++hdeque->count; return hdeque->count - 1; } size_t add_backp_deque(HDEQUE hdeque, const DATATYPE *val) { if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL) return DEQUE_FAILED; hdeque->deque[hdeque->tail++] = *val; hdeque->tail %= hdeque->capacity; ++hdeque->count; return hdeque->count - 1; } size_t add_front_deque(HDEQUE hdeque, DATATYPE val) { if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL) return DEQUE_FAILED; if (hdeque->head == 0) hdeque->head = hdeque->capacity - 1; else --hdeque->head; hdeque->deque[hdeque->head] = val; ++hdeque->count; return 0; } size_t add_frontp_deque(HDEQUE hdeque, const DATATYPE *val) { if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL) return DEQUE_FAILED; if (hdeque->head == 0) hdeque->head = hdeque->capacity - 1; else --hdeque->head; hdeque->deque[hdeque->head] = *val; ++hdeque->count; return 0; } DATATYPE *set_capacity_deque(HDEQUE hdeque, size_t new_capacity) { size_t size1, size2; DATATYPE *new_deque; if ((new_deque = (DATATYPE *)malloc(new_capacity * sizeof(DATATYPE))) == NULL) return NULL; size1 = MIN(hdeque->capacity - hdeque->head, hdeque->count); size2 = hdeque->count - size1; memcpy(new_deque, &hdeque->deque[hdeque->head], size1 * sizeof(DATATYPE)); if (size2 != 0) memcpy(new_deque + size1, hdeque->deque, size2 * sizeof(DATATYPE)); free(hdeque->deque); hdeque->deque = new_deque; hdeque->capacity = new_capacity; hdeque->head = 0; hdeque->tail = hdeque->count; return new_deque; } DATATYPE at_deque(HDEQUE hdeque, size_t index) { return hdeque->deque[(hdeque->head + index) % hdeque->capacity]; } void atp_deque(HDEQUE hdeque, size_t index, DATATYPE *val) { *val = hdeque->deque[(hdeque->head + index) % hdeque->capacity]; } DATATYPE pop_front_deque(HDEQUE hdeque) { size_t head; head = hdeque->head++; hdeque->head %= hdeque->capacity; return hdeque->deque[head]; } void pop_frontp_deque(HDEQUE hdeque, DATATYPE *val) { *val = hdeque->deque[hdeque->head++]; hdeque->head %= hdeque->capacity; } size_t insert_deque(HDEQUE hdeque, size_t index, DATATYPE val) { size_t k; if (index > hdeque->count) return DEQUE_FAILED; if (hdeque->count == hdeque->capacity && set_capacity_deque(hdeque, hdeque->capacity * 2) == NULL) return DEQUE_FAILED; k = hdeque->tail; for (size_t i = 0; i < hdeque->count - index; ++i) { if (k == 0) { k = hdeque->capacity - 1; hdeque->deque[0] = hdeque->deque[k]; } else { hdeque->deque[k] = hdeque->deque[k - 1]; --k; } } hdeque->deque[k] = val; hdeque->tail = (hdeque->tail + 1) % hdeque->capacity; ++hdeque->count; return index; } size_t remove_deque(HDEQUE hdeque, size_t index) { size_t k; if (index >= hdeque->count) return DEQUE_FAILED; k = (hdeque->head + index) % hdeque->capacity; for (size_t i = 0; i < hdeque->count - index - 1; ++i) { if (k == hdeque->capacity - 1) { hdeque->deque[k] = hdeque->deque[0]; k = 0; } else { hdeque->deque[k] = hdeque->deque[k + 1]; ++k; } } hdeque->tail = (hdeque->tail + hdeque->capacity - 1) % hdeque->capacity; --hdeque->count; return index; } void clear_deque(HDEQUE hdeque) { hdeque->head = hdeque->tail = 0; hdeque->count = 0; } void destroy_deque(HDEQUE hdeque) { free(hdeque->deque); free(hdeque); } /* app.c */ #include #include #include "deque.h" void disp_deque(HDEQUE hdeque); int main(void) { HDEQUE hdeque; if ((hdeque = create_deque()) == NULL) { fprintf(stderr, "cannot create deque!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 10; ++i) add_back_deque(hdeque, i); disp_deque(hdeque); remove_deque(hdeque, 1); disp_deque(hdeque); destroy_deque(hdeque); return 0; } void disp_deque(HDEQUE hdeque) { DATATYPE val; for (int i = 0; i < count_deque(hdeque); ++i) { val = at_deque(hdeque, i); printf("%d ", val); fflush(stdout); } printf("\n"); printf("head: %zd, tail: %zd, count: %zd, capacity: %zd\n", hdeque->head, hdeque->tail, count_deque(hdeque), capacity_deque(hdeque)); } /*================================================================================================================================*/ (64_11_02_2024) & (65_17_02_2024) & (66_18_02_2024) & (67_24_02_2024) & (69_03_03_2024) & (70_09_03_2024) & (71_10_03_2024) (72_16_03_2024) & (73_17_03_2024) & (74_23_03_2024) > Veri Yapıları ve Algoritmalar: >> Arama İşlemleri (Search Algorithms) : Algoritmalar ve veri yapıları konusunun en önemli alt konularından biri "arama (searching)" işlemleridir. Bir öğenin hızlı bir biçimde bulunması bazı uygulamaların performasını önemli ölçüde etkileyebilmektedir. Arama işlemleri doğrudan ana bellek (RAM) üzerinde ya da diskteki dosyalar üzerinde yapılabilir. Ana belek üzeirndeki aramalara "içsel aramalar (internal searches)" işlemleri denilmektedir. Diskteki dosyalar üzerinde yapılan aramalara ise "dışsal arama (external searches)" işlemleri denilmektedir. Örneğin bir dizideki elemanın aranması içsel aramaya örnektir. Ancak bir dosya üzerinde yapılan arama dışsal aramaya örnektir. Veritabanı işlemlerini gerçekleştiren araçlar dışsal aramayı etkin bir biçimde yapmaya çalışırlar. Dışsal arama yöntemlerinde içsel arama yöntemlerinden farklı algoritmalar kullanılabilmektedir. Biz bu kurusumuzda "içsel arama yöntemleri" üzerinde duracağız. Dışsal arama yöntemleri "Sistem Programlama ve İleri C Uygulamarı II" kursunda ele alınmaktadır. >>> Elimizde liste tarzı bir veri yapısı varsa ilk akla gelen arama yöntemi "sıralı arama (sequential search)" denilen yöntemdir. Eğer listenin elemanları arasında hiçbir ilişki yoksa (yani elemanlar gelişi güzel biçimde sıralanmışsa) sıralı aramadan başka bir yol yoktur. Sıralı aramda listenin başından itibaren her elemana ilgili bulunana kadar bakılır. Eğer aranacak eleman liste içerisinde varsa buna "başarılı arama successful search" denilmektedir. Eğer aranacak eleman listede yoksa buna da "başrısız arama (unsuccessful search)" denilmektedir. Başarılı sıralı aramada dizinin uzunluğu N olmak üzere ortalama (N + 1) / 2 karşılaştırma yapılmaktadır. Tabii en kötü durum senaryosunda bu değer N olacaktır. Big O notasyonuna göre arama işlemi O(N) karmaşıklıktadır. O(N) karmaşıklık arama işlemleri için kötü bir karmaşıklıktır. Örneğin 1000000 elamanın bulunduğu bir listede elemanın bulunması için ortalama 500000 karşılaştırma gerekmektedir. Ancak eleman sayısının çok az olduğu listelerde (örneğin eleman sayısının 20'den az olduğu listelerde) en hızlı arama yöntemi yine de sıralı aramadır. * Örnek 1, Aşağıda tipik bir sıralı arama işlemini yapan fonksiyon örneği verilmiştir. #include #include typedef int DATATYPE; DATATYPE *linear_search(const DATATYPE *array, size_t size, DATATYPE val) { for (size_t i = 0; i < size; ++i) if (array[i] == val) return (DATATYPE *)&array[i]; return NULL; } int main(void) { DATATYPE array[10] = {4, 67, 34, 12, 45, 32, 98, 11, 9, 85}; DATATYPE *val; if ((val = linear_search(array, 10, 98)) == NULL) { fprintf(stderr, "cannot find!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); return 0; } Aslında Knuth "The Art of Computer Programming" kitabının "The Sorting and Searching" isimli üçüncü cildinde sıralı arama sırasında her defasında "dizi bitti mi karşılaştırmasını" elimine etmek için bir yöntem önermiştir. Bu yönteme göre aranacak değer önce dizinin sonuna yerleştirilir. Sonra gereksiz biçimde her yinelemede bu kontrol yapılmaz. Çünkü eleman en kötü olasılıkla dizinin sonuna gelindiğinde bulunmuş olacaktır. Tabii bunun için dizinin bir fazl uzunlukta açılmış olması gerekir. Aşağıda bu yöntem uygulanmıştır. * Örnek 1, #include #include typedef int DATATYPE; DATATYPE *linear_search(DATATYPE *array, size_t size, DATATYPE val) { size_t i; array[size] = val; for (i = 0;; ++i) if (array[i] == val) break; return i != size ? &array[i] : NULL; } int main(void) { DATATYPE array[10 + 1] = {4, 67, 34, 12, 45, 32, 98, 11, 9, 85}; DATATYPE *val; if ((val = linear_search(array, 10, 98)) == NULL) { fprintf(stderr, "cannot find!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); return 0; } Eğer aramanın yapılacağı liste sıraya diziliyse ve O(1) karmaşıklıktakli erişime izin veriyorsa (yani başka bir deyişle arama sıralı bir dizi üzerinde yapılacaksa) bu durumda "ikili arama (binary search)" denilen yöntem tercih edilmelidir. İkili arama yönteminde aralık sürekli bir biçimde ikiye bölünerek daraltılır. İkili aramanın en kötü durumdaki karşılaştırma sayısı log2 N kadardır. (Yani örneğin 1 milyon eleman için 20 karşılaştırma). Big O notasyonuna göre en kötü durumdaki algoritma karçalıklığı O(log N) biçimindedir. Bir diziyi önce sıraya dizip onun üzerinde ikili arama uygulamak çoğu kez uygun bir yöntem değildir. Çünkü sıraya dizmenin maliyeti sıralı aramadan daha yüksektir. En iyi sıraya dizme algoritmaları O(N log N) karmaşıklıktadır. Tabii eğer dizi güncellenmeyecekse ve çok sayıda arama yaapılacaksa bir kez O(N log N) maliyeti karşılanıp diğer aramalar O(log N) karmaşıklıkta yürütülebilir. İkili arama yapılırken tipik olarak left ve right biçiminde iki çubuk alınır. Bunun orta noktası bulunur. Orat noktasındaki dizi elemanı aranacak elemanla karşılaştırılır. Eğer aranacak eleman orat noktadaki elemandan büyükse soldaki çubuk, küçükse sağdaki çubuk orta noktanın yanına çekilir. Başarısız aramda sağ çubuk sol çubuğun soluna gelmektedir. Burada çubukların ora noktasının şöyle bulunduğuna dikkat ediniz: mid = (left + right) / 2; Bu işlem aslında aşağıdakiyle eşdeğerdir: mid = left + (right - left) / 2; Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include #include typedef int DATATYPE; DATATYPE *binary_search(const DATATYPE *array, size_t size, DATATYPE val) { size_t left, right, mid; left = 0, right = size - 1; while (left <= right) { mid = (right + left) / 2; if (val > array[mid]) left = mid + 1; else if (val < array[mid]) right = mid - 1; else return (DATATYPE *)&array[mid]; } return NULL; } int main(void) { DATATYPE array[10] = {4, 11, 18, 24, 38, 43, 52, 68, 74, 89}; DATATYPE *val; if ((val = binary_search(array, 10, 100)) == NULL) { fprintf(stderr, "cannot find!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); return 0; } Eğer sıralı dizinin eleman sayısı bilinmiyorse (unbounded array) bu durumda önce elemanın bulunduğu bölge üstel bir biçimde (2 ile çarpılarak) belirlenir. Sonra o bölgede ikili arama uygulanabilir. Bu yönteme literatürde "üstel arama(exponential serach)" de denilmektedir. Yukarıda da belirttiğimiz gibi üstel arama üst sınıfın bilinmediği sıralı dizilerde özellikle uygulanmaktadır. Sınırın bilindiği durumlarda doğrudan ikili aramaya geçilebilir. Örneğin: 1 3 6 9 11 17 23 28 36 41 48 54 61 67 72 78 82 86 90 92 ..... Böyle bir dizide bir sınır olmadığı için ikili armanın sağ çubuğunun yerini de eblirleyemeyiz. İşte onu belirleyebilmek için önce 1'den başlatılan bir index sürekli iki ile çarpılarak ilerlenir. Örneğin: right = 1; while (array[right] < val && right < size) right *= 2; left = right / 2; Burada artık döngüden çıkıldığında aranacak eleman left ile right arasındadır. Bu noktada klasik ikili arama uygulaanabilir. Örneğin yukarıdaki örnek dizide aranacak eleman 78 olsun. Önce 1'inci indeksteki elemana bakılır (3). Sonra 2'inci indeksteki elemana bakılır (6) Ondan sonra 4'üncü indeksteki sonra 8'inci (36) sonra 16'ıncı indeksteki (82) elemanlara bakılır. 16'ıncı indeksteki eleman artık aranan eleman olan 78'den büyüktür. Döngü çıkılır ve dizinin 8'inci ve 16'ıncı indeksi arasında ikili arama uygulanır. Aşağıda üstel aramaya bir örnek verilmiştir. Her ne kadar bu arama yöntemi aslında sınırı bilinmeyen bir dizi için tercih ediliyorsa da sıralı normal dizilerde de kullanılabilir. (Normal sıralı ve sınırlı dizilerde bu algoritmanın kullanılması gerçek anlamda bir fayda sağlamamaktadır.) * Örnek 1, #include #include typedef int DATATYPE; DATATYPE *binary_search(const DATATYPE *array, size_t size, DATATYPE val) { size_t left, right, mid; left = 0, right = size - 1; while (left <= right) { mid = (right + left) / 2; if (val > array[mid]) left = mid + 1; else if (val < array[mid]) right = mid - 1; else return (DATATYPE *)&array[mid]; } return NULL; } DATATYPE *exponential_search(const DATATYPE *array, size_t size, DATATYPE val) { size_t left, right; if (array[0] == val) return (DATATYPE *)&array[0]; right = 1; while (right < size && val > array[right]) right *= 2; if (right >= size) right = size - 1; left = right / 2; return binary_search(array + left, right - left + 1, val); } int main(void) { DATATYPE array[] = {1, 3, 6, 9, 11, 17, 23, 28, 36, 41, 48, 54, 61, 67, 72, 78, 82, 86, 90, 92}; DATATYPE *val; if ((val = exponential_search(array, sizeof(array) / sizeof(*array), 61)) == NULL) { fprintf(stderr, "cannot find!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); return 0; } Sıralı dizilerde arama yapmak için kullanılan diğer bir yöntem de "enterpolasyon araması (interpolation search)" denilen yöntemdir. Bu yöntem ikili arama gibidir ancak aralık orta noktadan değil daha uygun yerden daraltılmaya çalışılır. Örneğin elimizde bir sözlük olsun. Bu sözlükte y harfi ile başlayan biz sözcüğü aramak isteyelim. Aramayı sözlüğün ortasından mı yoksa sonlarına doğru bir noktadan mı başlatırız? İşte ikili aramada arama orta noktalar temelinde yapılır. Ancak enterpolasyon aramasında arama orta nokta temelinde değil aranack anahtarla oranlı bir biçimde yapılmaktadır. Bu yöntemde de yine left ve right biçiminde iki çubuk alınır. Bu yöntemin ikili aramadan farkı orta noktalar yerine otantılı npktalara bakılmasıdır. Örneğin aranacak değer val olsun. Aranak ye şu orantıyla tespit edilir: mid = left + ((right - left) /(array[right] - array[left]) * (val - array[left])) İfadedeki şu kısma dikkat ediniz: (right - left) / (array[right] - array[left]) Burada yapılmak istenen şey dizi elemanlarındaki bir birim artımın kaç indeks artırımına karşı geldiğinin tespit edilmesidir. Bu tespit edildikten sonra bu değer (val - array[left]) değeri ile çarpılmıştır. Böylece orta nokta değil orantılı bir nokta elde edilmiştir. Pekiyi bu yöntem ikili aramadan daha mı iyidir? Eğer dizideki elemanların arasındaki artırım miktarı n,speten stabil ise yani düzgün bir artırım söz konusu ise bu yöntem ikili aramadan daha hızlı bir aramaya yol açar. Ancak uç değerlerin olduğu durumda bu yöntemin performansı çok düşmektedir. Örneğin: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1000000 Bu gibi durumlarda enterpolasyon araması O(N) karmaşıklığa kadar gerilemektedir. O halde sıralı dizinin elemanlarının genel artırımı hakkında bir bilgi sahibi değilsek ikili aramayı tercih etmeliyiz. Ancak dizideki elemanlar nispeten birbirine yakın artırımlarla ilerliyorsa ve dizide uç değerler yoksa bu ymntem daha iyi performans gösterebilmektedir. * Örnek 1, #include #include typedef int DATATYPE; DATATYPE *interpolation_search(const DATATYPE *array, size_t size, DATATYPE val) { size_t left, right, mid; left = 0, right = size; while (left <= right) { mid = left + ((right - left) / (array[right] - array[left]) * (val - array[left])); if (val > array[mid]) left = mid + 1; else if (val < array[mid]) right = mid - 1; else return (DATATYPE *)&array[mid]; } return NULL; } int main(void) { DATATYPE array[] = {1, 3, 6, 9, 11, 17, 23, 28, 36, 41, 48, 54, 61, 67, 72, 78, 82, 86, 90, 92}; DATATYPE *val; if ((val = interpolation_search(array, sizeof(array) / sizeof(*array), 12)) == NULL) { fprintf(stderr, "cannot find!..\n"); exit(EXIT_FAILURE); } printf("%d\n", *val); return 0; } >>> Aramalar aslında anahtara göre yapılıp aramanın sonucunda o anahtara ilişkin değer elde edilir. Biz yukarıdaki örneklerde yalnızca anahtarların bulunduğu dizide anahtar aradık. Dolayısıyla yukarıdaki örneklerde elde edebileceğimiz tek bilgi "anahtarın dizide var olup olmadığı" bilgisidir. Halbuki aramalarda önce anahtar-değer çiftleri veri yapısına yerleştirilir. Sonra anahtar verildiğinde onun değeri elde edilir. Örneğin öğrencilerin bilgileri numaralarına göre aranabilir. Böylece aramayı yapacak kişi öğrencinin numarasını verir. Arama sonucunda o numaralı öğrencinin bilgileri elde edilir. Veri yapıları dünyasında "bir anahtar verildiğinde ona iliştirilmiş olan değerin elde edilmesini" sağlayan veri yapılarına "sözlük (dictionary)" tarzı veri yapıları denilmektedir. Sözlük tarzı veri yapılarının en önemli özelliği anahtar verildiğinde değeri çok hızlı bir biçimde bulmasıdır. Anahtar ve değerlerin bir dizide tutulması ve onların sıralı bir biçimde aranması çok yavaş bir yöntemdir. Bu tür durumlarda hızlı aramalar için "algoritmik arama yöntemleri" kullanılmaktadır. Algoritmik arama yötemlerinin en çok kullanılanları "hash tabloları (hash tables)" ve "arama ağaçları (search trees)" denilen yöntemlerdir. İdeal bir anahtar-değer araması nasıl olabilir? Şüphesiz ideal durumda aramın O(1) karmaşıklıkta yapılması istenir. Anahtarın bir int değer olduğunu ve kişinin numarasını belirttiğini düşünelim. Biz de numara verildiğinde o kişinin bilgilerini elde etmek isteyelim. Kişilerin bilgilerini PERSON isimli bir yapıyla temesil edebiliriz: struct PERSON { .... }; Sonra da PERSON türünden büyük bir dizi açabiliriz: struct PERSON people[MAX_SIZE]; Sonra da kişilerin numaralarını indeks yaparak bu diziye yerleştirebiliriz. Örneğin numarası 123 olan kişinin bilgileri diziye şöyle yerleştirilebilir: people[123] = person_info; Numarası 123 olan kişinin bilgilerini O(1) karmaşıklıkta çok hızlı bir biçimde aşağıdaki gibi elde edebiliriz: person_info = people[123]; Bu yöntem ilk bakışta çok iyi bir yöntem gibi gözükse de genellikle kullanılabilir bir yöntem değildir. Çünkü burada anahtar int türdendir. Ancak anahtarlar farklı tür olabilir. Örneğin anahtar kişinin adı soyadı olabilir. Yazısal bir bilgi indeks belirtmemektedir. Bu yöntemin diğer bir sakıncası örneğin kişi numaralarının yüksek basamaklardan oluştuğu durumda o kadar büyük bir dizinin açılması gerekliliğidir. Örrneğin kişinin TC numarasına göre onun bilgilerinin elde edileceği durumda TC numarası 11 digit bir sayıdır. Yani skalası 100 milyar sınırındadır. 100 milyarlık bir yapı dizisini bu amaçla oluşturmak ya mümkün değildir, mümkün olsa da etkin değildir. Bu yönteme "indeskli arama index search" denilmektedir. Ancak çok özel durumlarda bu yöntem kullanılabilmektedir. >>> Algoritmik aramalarda en çok kullanılan yöntemlerden biri "hash tabloları (hash tables)" denilen yöntemdir. Hash tabloları aslında yukarıda belirttiğimiz indeksli arama ile sıralı aramanın hibrit bir biçimidir. Yöntemde ismine "hash tablosu (hash table)" makul bir uzunlukta dizi oluşturulur. Sonra anahtarlar ismine "hash fonksiyonu (hash function)" denilem bir fonksiyona sokularak dizi indeksine dönüştürülür. Sonra da dizinin o indeksteki elemanına başvurulur. Örneğinkişinin bilgilerini TC numaralarına göre saklayıp geri almak isteyelim. Hash tablomuzun uzunluğu da 1000 olsun. Hash fonksiyonunun "1000'e bölümden elde edilen kalan" değerini veren fonksiyon olduğunu varsayalım. Bu durumda örneğin 2566198712 TC kimlik numarasına sahip kişinin bilgileri hash tablosunun 712'inci indeksteki elemanında saklanabilir. 72484926820 TC kimlik numarasına sahip kişinin belgileri de dizinin 820'inci indeksteki elemanında saklanacktır. Ancak farklı kişilerin TC numaraları hash fonksiyonuna sokuldupunda aynı değer elde edilebilir. Örneğin 6238517712 TC numarasına sahip kişi de dizinin 712'inci indeskteki elemanına yerleşmek isteyecektir. İşte tablosu yönteminde bu duruma "çakışma durumu (collison)" denilmektedir. Hash tablosu yöntemi çakışma durumunda izlenecek stratejiye göre değişik varyasyonlara sahiptir. Hash tabloları yönteminde çakışma durumunda bu sorunu çözmek için iki ana yöntem grubu kullanılmaktadır: Ayrı zincir oluşturma yöntemi (separate chaining) ve açık adresleme (open addresiing) yöntemi. Açık adresleme yöntemi de kendi aralarında "doğrusal yoklama (linear probing)", "karesel yoklama (quadratic probing)", "çift hash'leme (double hasing)" gibi alternatif alt yöntemlere ayrılmaktadır. Ayrı zincir oluşturma ve açık adresleme yöntemlerinin dışında başka çakışma çözümleme stratejileri de vardır. Ancak ağırlıklı olarak bu ikisi tercih edilmektedir. Ayrı zincir oluşturma yönteminde (separate chaining) hash tablosu aslında bir bağlıntı liste dizisi gibi oluşturulur. Yani hash tablosunun her elemanı bağlı listenin ilk elemanını (head pointer) göstermektedir. Eklenecek anahtar hash fonksiyonuna sokulur ve bağlı listenin hemen önüne (ya da duruma göre arkasına) eklenir. Eleman aranırken yine anahtar hash fonksiyonuna sokulur ve dizinin ilgili indeksindeki bağlı listede sıralı arama yapılır. Hash tablolarına eleman insert etmek O(1) karmaşıklıktadır. Tabii burada kullanılacak hash fonksiyonu da önemlidir. Küçük dönüler içeren hash fonksiyonları O(1) karmaşıklığı yükseltmemektedir. elemanın silinmesi de benzer biçimdedir. Eleman aramanın O(1) karmaşıklıkta olabilmesi için bağlı listelerdeki zincir uzunluklarının kısa olması gerekir. 10 kadar eleman için en hızlı arama yöntemi sıralı aramdır. Bu koşulda sıralı aramanın O(1) karmaşıkta oludğu söylenebilir. O halde eğer zincirlerdeki oratalama eleman 10 civarında makul bir düzeyde tutulursa arama işleminin de O(1) karmaşıklıkta yapılabileceği söylenebilir. Sözlük tarzı veri yapılarında genel olarak aynı anahtara ilişkin birden fazla anahtar-değer çifti veri yapısına yerleştirilememektedir. Bazı kütüphanelerde buna izin verilebilmektedir. Eğer aynı anahtara ilişkin yeni bir değer insert edilmeye çalışılırsa eski değer yeni değerle yer değiştirmektedir. Yani başka bir deyişle anahtarın değeri değişitirilmektedir. Bazı tasarımlar ise aynı anahtara ilişkin insert yapmayı engellemektedir. Yani bu tasarımlarda yalnızca olmayan elemanı insert edebiliriz. Pekiyi hash tablolarında kullanılacak iyi bir hash fonksiyonu nasıl olmalıdır? İyi bir hash fonksiyonunun "hızlı" olması gerekir. Çünkü her türlü insert gibi arama gibi işlemlerde hash fonksiyonu kullanılacaktır. İyi bir hash fonksiyonunun "anahtarlar yanlı bile olsa" tabloya onları iyi bir biçimde yaydırması gerekir. Örneğin aslında sayısal anahtarlar için "bölümden elde edilen kalan" iyi bir hash fonksiyonu değildir. Hash tablolarında tablonun asal sayı uzunluğunda olması hash fonksiyonlarının daha iyi yaydırmasına yardımcı olmaktadır. (Örneğin tablo uzunluğu için 100 yerine 101 değeri tercih edilmelidir.) Hash fonksiyonları "sayıyı indekse dönüştüren" ve "yazıyı indekse" dönüştüren fonksiyonlar biçiminde oluşturulabilir. Hash tablolarının büyütülmesi önemli bir zaman kaybına yol açabilmektedir. Çünkü tablodaki tüm elemanların yeni tablo uzunluğuna göre yeniden hash'lenip yeni tablodaki uygun slotlara yerleştirilmesi gerekmektedir. Pekiyi büyütme ne zaman yapılamalıdır? Tablodaki toplam eleman sayısının tablo büyüklüğüne oranına "yükleme faktörü (load factor)" denilmektedir. Genellikle yükleme faktörü 0.75 gibi çok küçük bir değerde tutulur. Ancak yukarıda da belirttiğimiz gibi zincirlerdeki ortalama eleman sayısı 10 civarında olduğunda hash tabloları yine çok hızlı çalışmaktadır. Aşağıda ayrı zincir oluşturma yöntemi (separate chaining) için bir örnek verilmiştir. Bu örnekte çift bağlı liste kullanılmıtır. Aslında çift bağlı liste kullanılmasının bu örnekte bize bir faydası yoktur. Daha önceden de belirttiğimiz gibi çift bağlı listeler özellike düğüm adresi verildiğinde hızlı silme yapmak için tercih edilmektedir. Örneğimizde düğüme dayalı silme yapılamamaktadır. Çünkü silinecek düğüm eğer ilk düğümse tablonun güncellenmesi gerekir. Onun için de anahtarın bilinmesi gerekir. Tabii biz tabloda bağlı listelerin ilk düğümlerinin adreslerini tutmak yerine doğrudan bir düğüm de tutabilirdik. Bu durumda düğüme dayalı silmeyi yapabilirdik. Örneğimizde hash tablosunu eleman sayısı yükleme faktörüne eriştiğinde büyüttük. Default yükleme faktörünü 0.75 aldık. Yani örneğin tablonun uzunluğu 100 olsun. Tablodaki eleman sayısı 75'e geldiğinde iki kat büyütme sağlanmaktadır. Tabii aslında yükleme faktörünü bu kadar düşük tutmayabiliriz. * Örnek 1, /* htable.h */ #ifndef HTABLE_H_ #define HTABLE_H_ #include #include /* Type Decalarations */ typedef struct tagPERSON { char name[32]; int city; int no; } PERSON; typedef struct tagNODE { char key[32]; PERSON value; struct tagNODE *next; struct tagNODE *prev; } NODE; typedef struct tagHTABLE { NODE **table; size_t tsize; size_t count; double lf; } HTABLE, *HHTABLE; /* Function Prototypes */ HHTABLE create_lf_ht(size_t tsize, double lf); NODE *insert_ht(HHTABLE hhtable, const char *key, const PERSON *value); NODE *update_ht(HHTABLE hhtable, const char *key, const PERSON *value); PERSON *find_ht(HHTABLE hhtable, const char *key); bool remove_ht(HHTABLE hhtable, const char *key); bool resize_ht(HHTABLE hhtable, size_t new_size); void clear_ht(HHTABLE hhtable); void destroy_ht(HHTABLE hhtable); /* inline Function Definitions */ static inline size_t count_ht(HHTABLE hhtable) { return hhtable->count; } static inline size_t tsize_ht(HHTABLE hhtable) { return hhtable->tsize; } static inline HHTABLE create_ht(size_t tsize) { return create_lf_ht(tsize, 0.75); } #endif /* htable.c */ #include #include #include #include "htable.h" static size_t hash_func(const char *str, size_t tsize); HHTABLE create_lf_ht(size_t tsize, double lf) { HHTABLE hhtable; if ((hhtable = (HHTABLE)malloc(sizeof(HTABLE))) == NULL) return NULL; if ((hhtable->table = (NODE **)malloc(tsize * sizeof(NODE *))) == NULL) { free(hhtable); return NULL; } for (size_t i = 0; i < tsize; ++i) hhtable->table[i] = NULL; hhtable->tsize = tsize; hhtable->count = 0 ; hhtable->lf = lf; return hhtable; } NODE *insert_ht(HHTABLE hhtable, const char *key, const PERSON *value) { NODE *new_node; size_t hash; hash = hash_func(key, hhtable->tsize); for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next) if (!strcmp(key, node->key)) return NULL; if (((double)hhtable->count / hhtable->tsize) >= hhtable->lf) if (!resize_ht(hhtable, hhtable->tsize * 2)) return NULL; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; strcpy(new_node->key, key); new_node->value = *value; new_node->next = hhtable->table[hash]; hhtable->table[hash] = new_node; ++hhtable->count; return new_node; } NODE *update_ht(HHTABLE hhtable, const char *key, const PERSON *value) { NODE *new_node; size_t hash; hash = hash_func(key, hhtable->tsize); for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next) if (!strcmp(key, node->key)) { node->value = *value; return node; } if (((double)hhtable->count / hhtable->tsize) >= hhtable->lf) if (!resize_ht(hhtable, hhtable->tsize * 2)) return NULL; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; strcpy(new_node->key, key); new_node->value = *value; new_node->next = hhtable->table[hash]; hhtable->table[hash] = new_node; ++hhtable->count; return new_node; } PERSON *find_ht(HHTABLE hhtable, const char *key) { size_t hash; hash = hash_func(key, hhtable->tsize); for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next) if (!strcmp(key, node->key)) return &node->value; return NULL; } bool remove_ht(HHTABLE hhtable, const char *key) { size_t hash; NODE *node, *prev_node; hash = hash_func(key, hhtable->tsize); prev_node = NULL; node = hhtable->table[hash]; while (node != NULL) { if (!strcmp(key, node->key)) { if (hhtable->table[hash] == node) hhtable->table[hash] = node->next; else prev_node->next = node->next; free(node); --hhtable->count; return true; } prev_node = node; node = node->next; } return false; } bool resize_ht(HHTABLE hhtable, size_t new_size) { NODE **new_table; NODE *node, *temp_node; size_t hash; if (new_size <= hhtable->tsize) return false; if ((new_table = (NODE **)malloc(new_size * sizeof(NODE *))) == NULL) return false; for (size_t i = 0; i < new_size; ++i) new_table[i] = NULL; for (size_t i = 0; i < hhtable->tsize; ++i) { node = hhtable->table[i]; while (node != NULL) { temp_node = node->next; hash = hash_func(node->key, new_size); node->next = new_table[hash]; new_table[hash] = node; node = temp_node; } } free(hhtable->table); hhtable->table = new_table; hhtable->tsize = new_size; return true; } void clear_ht(HHTABLE hhtable) { NODE *node, *temp_node; for (size_t i = 0; i < hhtable->tsize; ++i) { node = hhtable->table[i]; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } hhtable->table[i] = NULL; } hhtable->count = 0; } void destroy_ht(HHTABLE hhtable) { NODE *node, *temp_node; for (size_t i = 0; i < hhtable->tsize; ++i) { node = hhtable->table[i]; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } } free(hhtable); } static size_t hash_func(const char *str, size_t tsize) { size_t hash = 0; while (*str != '\0') { hash = (13 * hash + *str) % tsize; ++str; } return hash; } /* app.c */ #include #include #include #include #include "htable.h" void get_random_record(char *key, PERSON *per); int main(void) { HHTABLE hhtable; char name[32]; PERSON per; PERSON specific_per = {"Kaan Aslan", 34, 123456}; PERSON *retper; srand((unsigned)time(NULL)); if ((hhtable = create_ht(10)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 1000; ++i) { if (i == 500) { if (insert_ht(hhtable, "Kaan Aslan", &specific_per) == NULL) { fprintf(stderr, "cannot insert item!..\n"); exit(EXIT_FAILURE); } } else { get_random_record(name, &per); if (insert_ht(hhtable, name, &per) == NULL) { fprintf(stderr, "cannot insert item!..\n"); exit(EXIT_FAILURE); } } } if ((retper = find_ht(hhtable, "Kaan Aslan")) != NULL) printf("Found: %s, %d, %d\n", retper->name, retper->city, retper->no); else printf("cannot find key!..\n"); strcpy(per.name, "Kaan Aslan"); per.city = 35; per.no = 1000; if (update_ht(hhtable, "Kaan Aslan", &per) == NULL) printf("Update failed!..\n"); if ((retper = find_ht(hhtable, "Kaan Aslan")) != NULL) printf("Found: %s, %d, %d\n", retper->name, retper->city, retper->no); else printf("cannot find key!..\n"); printf("count: %zd\n", count_ht(hhtable)); printf("tsize: %zd\n", tsize_ht(hhtable)); destroy_ht(hhtable); return 0; } void get_random_record(char *key, PERSON *per) { int i; for (i = 0; i < 31; ++i) key[i] = per->name[i] = rand() % 26 + 'A'; per->name[i] = key[i] = '\0'; per->city = rand() % 81; per->no = rand() % 1000000; } * Örnek 2, Yukarıdaki örneği tek bağlı liste kullanarak aşağıdaki gibi yeniden yazabiliriz. /* htable.h */ #ifndef HTABLE_H_ #define HTABLE_H_ #include #include #define HT_DEF_LOAD_FACTOR 0.75 /* Type Decalarations */ typedef struct tagPERSON { char name[32]; int city; int no; } PERSON; typedef struct tagNODE { char key[32]; PERSON value; struct tagNODE *next; } NODE; typedef struct tagHTABLE { NODE **table; size_t tsize; size_t count; double lf; } HTABLE, *HHTABLE; /* Function Prototypes */ HHTABLE create_lf_ht(size_t tsize, double lf); NODE *insert_ht(HHTABLE hhtable, const char *key, const PERSON *value); NODE *update_ht(HHTABLE hhtable, const char *key, const PERSON *value); PERSON *find_ht(HHTABLE hhtable, const char *key); bool remove_ht(HHTABLE hhtable, const char *key); bool resize_ht(HHTABLE hhtable, size_t new_size); void clear_ht(HHTABLE hhtable); void destroy_ht(HHTABLE hhtable); /* inline Function Definitions */ static inline size_t count_ht(HHTABLE hhtable) { return hhtable->count; } static inline size_t tsize_ht(HHTABLE hhtable) { return hhtable->tsize; } static inline HHTABLE create_ht(size_t tsize) { return create_lf_ht(tsize, HT_DEF_LOAD_FACTOR); } #endif /* htable.c */ #include #include #include #include "htable.h" static size_t hash_func(const char *str, size_t tsize); HHTABLE create_lf_ht(size_t tsize, double lf) { HHTABLE hhtable; if ((hhtable = (HHTABLE)malloc(sizeof(HTABLE))) == NULL) return NULL; if ((hhtable->table = (NODE **)malloc(tsize * sizeof(NODE *))) == NULL) { free(hhtable); return NULL; } for (size_t i = 0; i < tsize; ++i) hhtable->table[i] = NULL; hhtable->tsize = tsize; hhtable->count = 0 ; hhtable->lf = lf; return hhtable; } NODE *insert_ht(HHTABLE hhtable, const char *key, const PERSON *value) { NODE *new_node; size_t hash; hash = hash_func(key, hhtable->tsize); for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next) if (!strcmp(key, node->key)) return NULL; if ((hhtable->count / hhtable->tsize) >= hhtable->lf) if (!resize_ht(hhtable, hhtable->tsize * 2)) return NULL; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; strcpy(new_node->key, key); new_node->value = *value; new_node->next = hhtable->table[hash]; hhtable->table[hash] = new_node; ++hhtable->count; return new_node; } NODE *update_ht(HHTABLE hhtable, const char *key, const PERSON *value) { NODE *new_node; size_t hash; hash = hash_func(key, hhtable->tsize); for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next) if (!strcmp(key, node->key)) { node->value = *value; return node; } if ((hhtable->count / hhtable->tsize) >= hhtable->lf) if (!resize_ht(hhtable, hhtable->tsize * 2)) return NULL; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; strcpy(new_node->key, key); new_node->value = *value; new_node->next = hhtable->table[hash]; hhtable->table[hash] = new_node; ++hhtable->count; return new_node; } PERSON *find_ht(HHTABLE hhtable, const char *key) { size_t hash; hash = hash_func(key, hhtable->tsize); for (NODE *node = hhtable->table[hash]; node != NULL; node = node->next) if (!strcmp(key, node->key)) return &node->value; return NULL; } bool remove_ht(HHTABLE hhtable, const char *key) { size_t hash; NODE *node, *prev_node; hash = hash_func(key, hhtable->tsize); prev_node = NULL; node = hhtable->table[hash]; while (node != NULL) { if (!strcmp(key, node->key)) { if (hhtable->table[hash] == node) hhtable->table[hash] = node->next; else prev_node->next = node->next; free(node); --hhtable->count; return true; } prev_node = node; node = node->next; } return false; } bool resize_ht(HHTABLE hhtable, size_t new_size) { NODE **new_table; NODE *node, *temp_node; size_t hash; if (new_size <= hhtable->tsize) return false; if ((new_table = (NODE **)malloc(new_size * sizeof(NODE *))) == NULL) return false; for (size_t i = 0; i < new_size; ++i) new_table[i] = NULL; for (size_t i = 0; i < hhtable->tsize; ++i) { node = hhtable->table[i]; while (node != NULL) { temp_node = node->next; hash = hash_func(node->key, new_size); node->next = new_table[hash]; new_table[hash] = node; node = temp_node; } } free(hhtable->table); hhtable->table = new_table; hhtable->tsize = new_size; return true; } void clear_ht(HHTABLE hhtable) { NODE *node, *temp_node; for (size_t i = 0; i < hhtable->tsize; ++i) { node = hhtable->table[i]; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } hhtable->table[i] = NULL; } hhtable->count = 0; } void destroy_ht(HHTABLE hhtable) { NODE *node, *temp_node; for (size_t i = 0; i < hhtable->tsize; ++i) { node = hhtable->table[i]; while (node != NULL) { temp_node = node->next; free(node); node = temp_node; } } free(hhtable); } static size_t hash_func(const char *str, size_t tsize) { size_t hash = 0; while (*str != '\0') { hash = (13 * hash + *str) % tsize; ++str; } return hash; } /* app.c */ #include #include #include #include #include "htable.h" void get_random_record(char *key, PERSON *per); int main(void) { HHTABLE hhtable; char name[32]; PERSON per; PERSON specific_per = {"Kaan Aslan", 34, 123456}; PERSON *retper; srand((unsigned)time(NULL)); if ((hhtable = create_ht(10)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 1000; ++i) { if (i == 500) { if (insert_ht(hhtable, "Kaan Aslan", &specific_per) == NULL) { fprintf(stderr, "cannot insert item!..\n"); exit(EXIT_FAILURE); } } else { get_random_record(name, &per); if (insert_ht(hhtable, name, &per) == NULL) { fprintf(stderr, "cannot insert item!..\n"); exit(EXIT_FAILURE); } } } if ((retper = find_ht(hhtable, "Kaan Aslan")) != NULL) printf("Found: %s, %d, %d\n", retper->name, retper->city, retper->no); else printf("cannot find key!..\n"); strcpy(per.name, "Kaan Aslan"); per.city = 35; per.no = 1000; if (update_ht(hhtable, "Kaan Aslan", &per) == NULL) printf("Update failed!..\n"); if ((retper = find_ht(hhtable, "Kaan Aslan")) != NULL) printf("Found: %s, %d, %d\n", retper->name, retper->city, retper->no); else printf("cannot find key!..\n"); printf("count: %zd\n", count_ht(hhtable)); printf("tsize: %zd\n", tsize_ht(hhtable)); destroy_ht(hhtable); return 0; } void get_random_record(char *key, PERSON *per) { int i; for (i = 0; i < 31; ++i) key[i] = per->name[i] = rand() % 26 + 'A'; per->name[i] = key[i] = '\0'; per->city = rand() % 81; per->no = rand() % 1000000; } Anımsanacağı gibi hash tablolarının diğer bir gerçekleştirimi de "açık adresleme (open addressing)" denilen yöntem grubuydu. Açık adresleme "yoklama (probing)" biçimine göre çeşitli alt yöntemlere ayrılıyordu. Açık adreslemenin en yaygın ve basit biçimi "doğrusal yoklama (linear probing)" denilen biçimidir. Doğrusal yoklama (linear probing) oldukça basit bir fikre dayanmaktadır. Bu yöntemde yine hash tablosu oluşturulur. Ancak hash tablosunda bağlı listelerin adresleri tutulmaz bizzat değerlerin kendisi tutulur. Tabloya eleman ekleneceği zaman yine anahtardan bir hash değeri elde edilir. Doğrudan değer tablonun hash ile elde edilen indeksine yerleştirilir. Başka bir anahtar aynı hash değerini verdiğinde (yani çakışma durumu oluştuğunda) o indeksten itibaren boş yer bulunana kadar yan yana indekslere sırasıyla bakılır. Örneğin hash olarak 123 değerini elde etmiş olalım. Tablonun 123'üncü elemanın dolu olduğunu düşünelim. Bu durumda 124'üncü elemanına bakarız. O da doluysa 125'inci elemanına bakarız. Ta ki boş bir indeks bulunana kadar. Değeri ilk boş indekse yerleştiririz. Tabii bu durumda nasıl başka bir değer bizim indeksimize yerleşmişse biz de aslında başka bir değerin indeksine yerleşmiş oluruz. Ancak bizim yerleştiğimiz indeks için hash'e sahip olan değer de bizim yaptığımız gibi ilk boş yer bulunana kadar ilerleyecektir. Bu yöntemde arama işlemi de benzer biçimde yapılmaktadır. Yani aranacak elemanın hash değeri elde edilir. O indekse başvurulur. Değer o indekste değilse değer bulunana kadar ya da boş bir slot (bucket) görülene kadar yan yana diğer indekslere bakılır. Anahtara dayalı eleman silme de benzer biçimde yapılmaktadır. Ancak eleman silindiğinde ilgili slotun (bucket) boşaltılması arama işlemlerinde sorunlara yol açabilecektir. Burada yöntemlerden biri silinen elemanın slotunu boş yapmayıp silinmenin özel bir değerler belirtilmesidir. Örneğin her slot için bir status bayrağı tutulabilir. Bu status bayrağı ilgili slotun "dolu" olduğunu", "boş" olduğubnu ya da "silinmiş" olduğunu belirtebilir. Böylece arama sırasında "silinmiş" slotlar görüldüğünde durulmaz. İlk boş slot görüldüğünde durulur. Tabii silinmiş slotlara yeni elemanlar eklenebilir. * Örnek 1, Aşağıda "linear probing" yöntemine bir örnek verilmiştir. /* htable.h */ #ifndef HASHTABLE_ #define HASHTABLE_ #include #include /* Symbolic Constants */ #define HT_DEF_LOAD_FACTOR 0.75 #define HT_STATUS_EMPTY 0 #define HT_STATUS_INUSE 1 #define HT_STATUS_DELETED 2 /* Type Declarations */ typedef struct tagPERSON { char name[32]; int city; int no; } PERSON; typedef struct tagBUCKET { int status; char key[32]; PERSON value; } BUCKET; typedef struct tagHTABLE { BUCKET *ht; size_t tsize; size_t count; double lf; } HTABLE, *HHTABLE; /* Function Prototypes */ HHTABLE create_lf_ht(size_t tsize, double lf); bool insert_ht(HHTABLE hhtable, const char *key, const PERSON *value); bool update_ht(HHTABLE hhtable, const char *key, const PERSON *value); bool remove_ht(HHTABLE hhtable, const char *key); void clear_ht(HHTABLE hhtable); void destroy_ht(HHTABLE hhtable); PERSON *find_ht(HHTABLE hhtable, const char *key); bool resize_ht(HHTABLE hhtable, size_t new_size); /* inline Function Definitions */ static inline size_t count_ht(HHTABLE hhtable) { return hhtable->count; } static inline size_t tsize_ht(HHTABLE hhtable) { return hhtable->tsize; } static inline HHTABLE create_ht(size_t tsize) { return create_lf_ht(tsize, HT_DEF_LOAD_FACTOR); } #endif /* htable.c */ #include #include #include #include "hashtable.h" static size_t hash_func(const char *str, size_t tsize); HHTABLE create_lf_ht(size_t tsize, double lf) { HHTABLE hhtable; if ((hhtable = (HHTABLE)malloc(sizeof(HTABLE))) == NULL) return NULL; if ((hhtable->ht = (BUCKET *)malloc(tsize * sizeof(BUCKET))) == NULL) { free(hhtable); return NULL; } for (size_t i = 0; i < tsize; ++i) hhtable->ht[i].status = HT_STATUS_EMPTY; hhtable->tsize = tsize; hhtable->lf = lf; hhtable->count = 0; return hhtable; } bool insert_ht(HHTABLE hhtable, const char *key, const PERSON *value) { size_t index; if (hhtable->count >= hhtable->tsize) return false; if (((double)hhtable->count / hhtable->tsize) >= hhtable->lf) if (!resize_ht(hhtable, hhtable->tsize * 2)) return false; index = hash_func(key, hhtable->tsize); while (hhtable->ht[index].status != HT_STATUS_EMPTY) { if (!strcmp(hhtable->ht[index].key, key)) return false; index = (index + 1) % hhtable->tsize; } strcpy(hhtable->ht[index].key, key); hhtable->ht[index].value = *value; hhtable->ht[index].status = HT_STATUS_INUSE; ++hhtable->count; return true; } bool update_ht(HHTABLE hhtable, const char *key, const PERSON *value) { size_t index; if (hhtable->count >= hhtable->tsize) return false; if (((double)hhtable->count / hhtable->tsize) >= hhtable->lf) if (!resize_ht(hhtable, hhtable->tsize * 2)) return false; index = hash_func(key, hhtable->tsize); while (hhtable->ht[index].status != HT_STATUS_EMPTY) { if (!strcmp(hhtable->ht[index].key, key)) { hhtable->ht[index].value = *value; return true; } index = (index + 1) % hhtable->tsize; } strcpy(hhtable->ht[index].key, key); hhtable->ht[index].value = *value; hhtable->ht[index].status = HT_STATUS_INUSE; ++hhtable->count; return true; } bool remove_ht(HHTABLE hhtable, const char *key) { size_t index, hash; hash = index = hash_func(key, hhtable->tsize); do { if (hhtable->ht[index].status == HT_STATUS_EMPTY) break; if (hhtable->ht[index].status == HT_STATUS_INUSE && !strcmp(hhtable->ht[index].key, key)) { hhtable->ht[index].status = HT_STATUS_DELETED; --hhtable->count; return true; } index = (index + 1) % hhtable->tsize; } while (index != hash); return false; } PERSON *find_ht(HHTABLE hhtable, const char *key) { size_t index, hash; hash = index = hash_func(key, hhtable->tsize); do { if (hhtable->ht[index].status == HT_STATUS_EMPTY) break; if (hhtable->ht[index].status == HT_STATUS_INUSE && !strcmp(hhtable->ht[index].key, key)) return &hhtable->ht[index].value; index = (index + 1) % hhtable->tsize; } while (index != hash); return NULL; } bool resize_ht(HHTABLE hhtable, size_t new_size) { BUCKET *new_ht; size_t index; if (new_size <= hhtable->tsize) return false; if ((new_ht = (BUCKET *)malloc(new_size * sizeof(BUCKET))) == NULL) return false; for (size_t i = 0; i < new_size; ++i) new_ht[i].status = HT_STATUS_EMPTY; for (size_t i = 0; i < hhtable->tsize; ++i) { if (hhtable->ht[i].status == HT_STATUS_INUSE) { index = hash_func(hhtable->ht[i].key, new_size); while (new_ht[index].status != HT_STATUS_EMPTY) index = (index + 1) % new_size; new_ht[index] = hhtable->ht[i]; } } free(hhtable->ht); hhtable->ht = new_ht; hhtable->tsize = new_size; return true; } void clear_ht(HHTABLE hhtable) { for (size_t i = 0; i < hhtable->tsize; ++i) hhtable->ht[i].status = HT_STATUS_EMPTY; hhtable->count = 0; } void destroy_ht(HHTABLE hhtable) { free(hhtable->ht); free(hhtable); } static size_t hash_func(const char *str, size_t tsize) { size_t hash = 0; while (*str != '\0') { hash = (13 * hash + *str) % tsize; ++str; } return hash; } /* app.c */ #include #include #include #include "hashtable.h" void get_random_record(char *key, PERSON *per); int main(void) { HHTABLE hhtable; char name[32]; PERSON per, *pper; PERSON specific_per = {"Kaan Aslan", 34, 123456}; srand((unsigned)time(NULL)); if ((hhtable = create_ht(10)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 1000; ++i) { if (i == 500) { if (!insert_ht(hhtable, "Kaan Aslan", &specific_per)) { fprintf(stderr, "cannot insert item!..\n"); exit(EXIT_FAILURE); } } else { get_random_record(name, &per); if (!insert_ht(hhtable, name, &per)) { fprintf(stderr, "cannot insert item!..\n"); exit(EXIT_FAILURE); } } } if (remove_ht(hhtable, "Kaan Aslan")) printf("Item removed...\n"); else printf("cannot remove item!..\n"); if ((pper = find_ht(hhtable, "Kaan Aslan")) != NULL) printf("Found: %s %d %d\n", pper->name, pper->city, pper->no); else printf("cannot find record!..\n"); destroy_ht(hhtable); return 0; } void get_random_record(char *key, PERSON *per) { int i; for (i = 0; i < 31; ++i) key[i] = per->name[i] = rand() % 26 + 'A'; per->name[i] = key[i] = '\0'; per->city = rand() % 81; per->no = rand() % 1000000; } Aslında hash tablolarının oluşturulmasında kullanılan "açık adresleme (open addresing)" yönteminde "linear probing" dışında başka yoklama yöntemleri de vardır. Örneğin "quadratic probing" yönteminde çakışma durumunda slotlara sırasıyla bakılmaz. İkinci derece bir polinom yoluyla bakılır. Örneğin ekleme yapmaya çalışalım ve anahtara ilişkin hash değeri 20 olsun. 20 numaralı slot'tun dolu olduğunu düşünelim. Biz bu yöntemde 20 + 1, 20 + 2, 20 + 9, 20 + 16 gibi karesel değerlerle yoklama yaparız. Çift hash'leme (double hasing) yönteminde ise önce bir hash fonksiyonu ile indeks elde edilir. Eğer çakışma olursa atlanacak miktar ikinci bir hash fonksiyonuna başvurularak belirlenir. Yukarıdaki yöntemlerin en çok tercih edilenleri "ayrı zincir oluşturma "separate chaining" ve "açık adresleme linear probing" yöntemleridir. >>> Biz tek bağlı listelerde bir önceki düğümün sonraki düğümü göstermesini sağladık. Çift bağlı listelerde de bir düğüm hem önceki hem de sonraki düğümü gösteriyordu. Pekiyi bir düğüm birden fazla düğümü gösterirse bu nasıl veri yapısı olur? İşte düğümlerin birden fazla düğümü göstermesi durumunda oluşan veri yapılarına "ağaç (tree)" ve "graf (graph)" denilmektedir. Biz önce ağaçları sonra grafları inceleyeceğiz. >>>> "Ağaç (tree)" : Her ağacın bir kökü vardır. Kök kendisine gelinemeyen ancak kendisinden her düğüme erişilebilen özel bir düğümdir. Ağaçlarda kök düğümden her düğüme gitmenin yalnızca tek bir yolu vardır. Eğer bir düğüme birden fazla yoldan gidilebiliyorsa bu tür veri yapılarına "ağaç" değil "graf" denilmektedir. Ağaçlar konusunu iyi anlayabilmek için ağaç terminolojisini biliyor olmak gerekir. Ağaçlarda bir düğümden gidel sonraki düğümlere "o düğümün alt düğümleri (child nodes)" denilmektedir. Kök düğüm haricindeki her düğümün tek bir "üst düğümü (parent node)" vardır. Zaten kök düğüm "üst düğümü olmayan dolayısıyla kendisine bir yerden gelinemeyen" tek düğümdür. Ağacın en altında artık başka bir düğüme gidilemeyen düğümlere "yaprak (leaf)" düğümler denilmektedir. Bir ağaçta bir düğümlerin maksimum sahip olabileceği alt düğüm sayısı belirlenmiş olabilir. Bunun n olduğunu varsayalım. Böyle ağaçlara "'li ağaç" denilmektedir. Örneğin bir ağaçta her düğümün "en fazla düğümü" olabiliyorsa böyle ağaçlara "ikili ağaçlar (binary trees)", en fazla üç düğümü olabiliyorsa bunlara "üçlü ağaçlar (ternary tree)", en fazla n düğümü olabiliyorsa bunlara "n'li ağaçlar (n'ary tree)" denilmektedir. Uygulamada en fazla karşılaşılan ağaçlar ikili ağaçardır. İkili ağaçlarda bir düğümün sıfır tane alt düğümü olabilir. Zaten bu durumda bu düğüm bir yaprak durumundadır. Bir tane alt düğümü olabilir. Ya da en fazla ki tane alt düğümü olabilir. Ağaçlarda "her düğümün bir yükseliği (height)" vardır. Düğümün yüksekliği kök düğümden o düğüme gelirken kat edilen yol (edge) sayısı ile belirlenir. Kök düğüm yüksekliği 0'dır. Ağacın en yüksek düğümünün yüksekliğine "ağacın yüksekliği" de denilmektedir. Bir ağaçta bütün yaprak düğümlerin yükseklikleri arasındaki fark belli bir değerden büyük değilse (bu değer 1, 2, 3 gibi olabilir) bu ağaçlara "dengelenmiş ağaçlar (balanced trees)" denilmektedir. Örneğin bu farklılık 1 olarak tespit edilmişse bu durum yaprakların yükseklikleri arasında en fazla 1 fark olacağı anlamına gelmektedir. Bir N'li ağaçta ağacın en alt kademe dışında tüm kademelerindeki düğümlerinin tam olarak N tane alt düğümü varsa ve ağacın tüm yaprakları aynı yüksekliğe sahipse böyle ağaçlara "tam dolu olan ağaçlar (full trees)" denilmektedir. Bir N'li ağaç en son kademe (en alt kademe) dışında tam dolu ise ve son kademedeki düğümler solda soldan sağa toplanmışsa böyle ağaçlara "tam ağaçlar (complete tree)" denilmektedir. Uygulamada en fazla kullanılan ağaçlar ikili ağaçlardır. Yukarıda da belirttiğimiz gibi ikili ağaçlarda her düğümün en fazla iki alt düğümü olabilmektedir. Bir ikili ağaca ilişkin düğüm aşağıdaki gibi bir yapıyla temsil edilebilir: typedef struct tagNODE { DATATYPE val; struct tagNODE *left; struct tagNODE *right; } NODE; Burada val elemanı düğüme iliştirilen bilgiyi belirtmektedir. left ve right elemanları alt düğümlerin yerlerini tutmaktadır. Eğer düğümün sol ya da sağ alt düğümü yoksa bu left ve right elemanları NULL adres içerebilir. Tabii ağacın kök düğümünün yerini de bir biçimde tutmamız gerekir. Bu durumda ağacı temsil eden yapı da şöyle olabilir: typedef struct tagBTREE { NODE *root; size_t count; } BTREE, *HBTREE; Ağaçlar en çok "arama (search)" amaçlı kullanılmaktadır. Arama amacıyla kullanılan ağaçlara "arama ağaçları (search trees)" denilmektedir. Arama ağaçları için en çok kullanılan ağaçlar ikili ağaçlardır. Bunlara "ikili arama ağaçları (binary search trees)" de denilmektedir. Arama ağaçları oluşturulurken bilgiler düğümlerin içerisine yerleştirilir. Tabii düğümler belli bir kurala göre ağaca eklenir. Arama işlemi bir anahtara göre yapılacağından arama ağaçlarındaki düğümlerin de "anahtar" ve "değer" çiftlerini tutaması gerekir. Örneğin: typedef struct tagNODE { KEY key; VALUE value; struct tagNODE *left; struct tagNODE *right; } NODE; Burada key arama için kullanılan anahtarı value ise arama sonucunda elde edilecek olan değeri belirtmektedir. Örneğin anahtar bir kişinin numarasını belirtiyor olabilir değer de o kişinin bilgilerini belirtiyor olabilir: typedef struct tagNODE { int key; PERSON value; struct tagNODE *left; struct tagNODE *right; } NODE; Bir ikili arama ağacında eleman eklenirken ekleme noktası "küçükler sola büyükler sağa" olacak biçimde yapılmaktadır. Yani kök düğümden girilir. Eklenecek anahtar düğümdeki anahtarla karşılaştırılır. Eklenecek anahtar düğümdeki anahtardan küçükse sola doğru, büyükse sağa doğru ilerlenir. Örneğin ağaca sırasıyla 40 60 25 16 52 8 değerleri eklenecek olsun. Ağaöç henüz boş olduğuna göre 40 değeri köke eklenir: 40 Sonra 60 değeri 40'dan büyük olduğu için 40'ın sağına eklenir: 40 60 Sonra 25 değeri 40'tan küçük olduğu için 40'ın soluna eklenir: 40 25 60 Sonra 16 değeri 40'tan küçük ve 25'ten küçük olduğu için 25'in soluna eklenir: 40 25 60 16 Sonra 52 değeri 40'tan büyük 60'tan küçük olduğuna göre 60'ın soluna eklenir: 40 25 60 16 52 8 değeri 40'tan küçük, 25'ten küçük ve 16'dan küçük olduğuna göre 16'nın soluna eklenir: 40 25 60 16 52 8 İkili ağaçlarda eleman arama benzer biçimde yapılmaktadır. Kök düğümden girilir aranan anahtar ile düğümdeki anahtar karşılaştırılır. Eğer aranan anahtar düğümdeki anahtardan küçükse sola, büyükse sağa doğru ilerlenir. Yapraklara gelindiğinde yani daha fazla sola ya da sağa gidilemediğinde arama sonlandırılır. İkili arama ağacına eleman eklemenin en kötü durumdaki (worst case) karmaşıklığı O(N) biçimindedir. Dengelenmiş ağaçlarda ortalama karmaşıklık O(log N) biçimindedir. Arama işlemleri de en kötü durumda O(N) dengelenmiş ağaçlarda O(log N) karmaşıklıktadır. Çünkü arama sırasında aslında tıpkı "ikili aramada (binary search)" olduğu gibi sürekli iki bölme yaaparak ilerlenmektedir. Ağaçların dolaşılması özyinelemeli bir biçimde yapılabilir. İkili ağaçlar dört biçimde dolaşılabilmektedir: -> In-Order dolaşım -> Pre-Order dolaşım -> Post-Order dolaşım -> Breadt-First Dolaşım Buradaki "in", "pre" ve "post" sözcükleri alt düğümlere göre üst düğümün hangi sırada dolaşılacağını belirtmektedir. Bu dolaşımlar soldan-sağa ya da sağdan sola yapılabilmektedir. Bu dolaşımlardan, >>>>> "In-order" : soldan sağa dolaşım aşağıdaki temsili koddaki (pseudo code) gibi yapılmaktadır: void inorder_walk_lr(Node *node) { if (node->left != NULL) inorder_walk_lr(node->left); printf(node->val); if (node->right != NULL) inorder_walk_lr(node->right); } In-order soldan sağa dolaşım ağacın anahtara göre küçükten büyüğe doğru dolaşımını sağlamaktadır. In-order soldan sağa dolaşımda önce sol koldan ilerlenmeye çalışıldığına sonra ana düğümün ziyaret edildiğine, sonra da sağ koldan ilerlenmeye çalışıldığına dikkat ediniz. In-order sağdan sola dolaşımda bunun tersi yapılmaktadır: void inorder_walk_rl(Node *node) { if (node->right != NULL) inorder_walk_rl(node->right); printf(node->val); if (node->left != NULL) inorder_walk_rl(node->left); } In-order sağdan sola dolaşım anahtara göre büyükten küçüğe bir dolaşıma yol açmaktadır. >>>>> "Post-order" : dolaşımda önce alt düğümler sonra da ana düğün ziyaret edilmektedir. Bu dolaşım da soldan sağa ya da sağdan sola yapılabilmektedir. Temsili kodu aşağıdaki gibidir: void postorder_walk_lr(Node *node) { if (node->left != NULL) postorder_walk_lr(node->left); if (node->right != NULL) postorder_walk_lr(node->right); printf(node->val); } postoreder dolaşım sağdan sola da yapılabilir. Ancak genel olarak bu ikisi arasında programcıyı ilgilendirecek önemli bir farklılık yoktur. Post-order dolaşım ikili ağaçtaki düğümleri free hale getirmek için gerekmektedir. Çünkü ikili ağaçtaki düğümleri free hale getirme işlemi önce alt düğümlerin sonra ana düğümün free hale getirilmesiyle gerçekleştirilmektedir. >>>>> "Pre-order" : dolaşımda ise önce ana düğüm ziyaret edilir. Sonra alt düğümler ziyaret edilir. Tabii bu da soldan sağa ya da sağdan sola yapılabilmektedir. Ancak dolsdan sağa dolaşımla sağdan sola dolaşım arasında programcıyı ilgilendiren önemli bir farklılık yoktur. Pre-order soldan sağa dolaşımın temsili kodu şöyledir: void preorder_walk_lr(Node *node) { printf(node->val); if (node->left != NULL) preorder_walk_lr(node->left); if (node->right != NULL) preorder_walk_lr(node->right); } >>>>> "breadth-first" : Kademe Kademe dolaşım denilmektedir. Örneğin: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Buradaki numaralar breadt-first dolaşımın nasıl yapıldığını açıklamaktadır. Breadth-first dolaşım için bir kuyruk sistemine gereksinim vardır. Algoritmada bir düğüm üzerinde işlem yapıldıktan sonra hemen onun alt düğümleri FIFO kuyruk sistemine atılır. İşlem yapılacak düğümler kuyruk sisteminden çekilir. Dolaşımın temsili kodu şöyledir: put(root); while ((node = get()) != NULL) { process(node); if (node->left != NULL) put(node->left); if (node->right != NULL) put(node->right); } İkili ağaçlarda en karmaşık işlemlerden biri belli bir düğümün silinmesidir. Bu işlemin karmaşık olmasının nedeni birden fazla özel duruma sahip olmasındadır. İkili ağaçta düğüm silerken özel durumlar şunlar olabilir: -> Silinecek düğüm bir yaprak düğüm ise tek yapılacak şey onun üst düğümünün ilgili göstericisini NULL yapmaktır. -> Silinecek düğümün tek bir alt düğümü varsa işlem yine kolaydır. Silinecek düğümün var olan düğümü silinecek düğümün yerine taşınır. -> Silincek düğümün iki alt düğümünün de olması durumunda işlem biraz karmaşık hale gelmektedir. Bu durumda silinecek düğüm yerine geçecek düğüm "ya sol kolun en büyük düğümü" ya da "sağ kolun en küçük düğümü" olabilir. Fakat bu durumda ağaçta birkaç güncellemenin yapılması gerekmektedir. Silinecek düğümünyerine geçecek olan düğümün süt düğümünün ve silinecek düğümün üst düğümünün gencellenmesi gerekmektedir. -> Silinecek düğümün kök düğüm olması durumunda ağacın kökünü gösteren göstericinin de güncellenmesi gerekmektedir. Aşağıda ikili arama ağacının gerçekleştirilmesine ilişkin bir örneek verilmiştir. * Örnek 1, /* binarytree. h */ #ifndef BINARYTREE_H_ #define BINARYTREE_H_ #include #include /* Type Declararions */ typedef struct tagPERSON { char name[32]; int city; } PERSON; typedef struct tagNODE { int key; PERSON value; struct tagNODE *left; struct tagNODE *right; } NODE; typedef struct tagQNODE { struct tagQNODE *next; NODE *node; } QNODE; typedef struct tagBTREE { NODE *root; size_t count; QNODE *head; QNODE *tail; } BTREE, *HBTREE; /* Function Prototypes */ HBTREE create_bt(void); bool insert_item_bt(HBTREE hbtree, int key, const PERSON *value); bool insert_item_alternative_bt(HBTREE hbtree, int key, const PERSON *value); bool walk_inorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)); bool walk_inorder_rl_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)); bool walk_postorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)); bool walk_preorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)); bool walk_breadth_first_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)); bool delete_bt(HBTREE hbtree, int key); void clear_bt(HBTREE hbtree); void destroy_bt(HBTREE hbtree); /* inline Function Fefinitions */ static inline size_t count_bt(HBTREE hbtree) { return hbtree->count; } #endif /* binarytree.c */ #include #include #include "binarytree.h" static bool walk_inorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *)); static bool walk_inorder_rl_recur(NODE *node, bool (*proc)(int, PERSON *)); static bool walk_postorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *)); static bool walk_preorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *)); static void clear_recur(NODE *node); static void create_queue(HBTREE hbtree); static NODE *put_queue(HBTREE hbtree, NODE *node); static NODE *get_queue(HBTREE hbtree); static void destroy_queue(HBTREE hbtree); static void subtree_shift(HBTREE hbtree, NODE *node1, NODE *node2, NODE *node3); HBTREE create_bt(void) { HBTREE hbtree; if ((hbtree = (HBTREE)malloc(sizeof(BTREE))) == NULL) return NULL; hbtree->root = NULL; hbtree->count = 0; return hbtree; } bool insert_item_bt(HBTREE hbtree, int key, const PERSON *value) { NODE *new_node, *node, *parent_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return false; new_node->key = key; new_node->value = *value; new_node->left = NULL; new_node->right = NULL; if (hbtree->root == NULL) { hbtree->root = new_node; ++hbtree->count; return true; } node = hbtree->root; while (node != NULL) { parent_node = node; if (key < node->key) node = node->left; else if (key > node->key) node = node->right; else { node->value = *value; return true; } } if (key < parent_node->key) parent_node->left = new_node; else parent_node->right = new_node; ++hbtree->count; return true; } bool insert_item_alternative_bt(HBTREE hbtree, int key, const PERSON *value) { NODE *new_node, *node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return false; new_node->key = key; new_node->value = *value; new_node->left = NULL; new_node->right = NULL; if (hbtree->root == NULL) { hbtree->root = new_node; ++hbtree->count; return true; } node = hbtree->root; while (node != NULL) { if (key < node->key) if (node->left == NULL) { node->left = new_node; break; } else node = node->left; else if (key > node->key) if (node->right == NULL) { node->right = new_node; break; } else node = node->right; else { node->value = *value; return true; } } ++hbtree->count; return true; } bool walk_inorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)) { if (hbtree->root != NULL) return walk_inorder_lr_recur(hbtree->root, proc); return true; } bool walk_inorder_rl_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)) { if (hbtree->root != NULL) return walk_inorder_rl_recur(hbtree->root, proc); return true; } bool walk_postorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)) { if (hbtree->root != NULL) return walk_postorder_lr_recur(hbtree->root, proc); return true; } bool walk_preorder_lr_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)) { if (hbtree->root != NULL) return walk_preorder_lr_recur(hbtree->root, proc); return true; } void clear_bt(HBTREE hbtree) { if (hbtree->root != NULL) { clear_recur(hbtree->root); hbtree->root = NULL; hbtree->count = 0; } } void destroy_bt(HBTREE hbtree) { if (hbtree->root != NULL) clear_recur(hbtree->root); free(hbtree); } bool walk_breadth_first_bt(HBTREE hbtree, bool (*proc)(int, PERSON *)) { NODE *node; bool result = true; create_queue(hbtree); put_queue(hbtree, hbtree->root); while ((node = get_queue(hbtree)) != NULL) { if (!proc(node->key, &node->value)) { result = false; break; } if (node->left != NULL) if (put_queue(hbtree, node->left) == NULL) { fprintf(stderr, "put_queue cannot allocate memory!..\n"); result = false; break; } if (node->right != NULL) if (put_queue(hbtree, node->right) == NULL) { result = false; fprintf(stderr, "put_queue cannot allocate memory!..\n"); break; } } destroy_queue(hbtree); return result; } bool delete_bt(HBTREE hbtree, int key) { NODE *delete_node, *delete_parent_node, *replace_parent_node, *replace_node; delete_node= hbtree->root; delete_parent_node = NULL; while (delete_node != NULL) { if (delete_node->key == key) break; delete_parent_node = delete_node; if (key < delete_node->key) delete_node = delete_node->left; else if (key > delete_node->key ) delete_node = delete_node->right; } if (delete_node == NULL) return false; if (delete_node->left == NULL) subtree_shift(hbtree, delete_parent_node, delete_node, delete_node->right); else if (delete_node->right == NULL) subtree_shift(hbtree, delete_parent_node, delete_node, delete_node->left); else { replace_parent_node = delete_node; replace_node = delete_node->right; while (replace_node->left!= NULL) { replace_parent_node = replace_node; replace_node= replace_node->left; } if (replace_parent_node != delete_node) { subtree_shift(hbtree, replace_parent_node, replace_node, replace_node->right); replace_node->right= delete_node->right; } subtree_shift(hbtree, delete_parent_node, delete_node, replace_node); replace_node->left = delete_node->left; } free(delete_node); --hbtree->count; return true; } static bool walk_inorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *)) { if (node->left != NULL) if (!walk_inorder_lr_recur(node->left, proc)) return false; if(!proc(node->key, &node->value)) return false; if (node->right != NULL) if (!walk_inorder_lr_recur(node->right, proc)) return false; return true; } static bool walk_inorder_rl_recur(NODE *node, bool (*proc)(int, PERSON *)) { if (node->right != NULL) if (!walk_inorder_rl_recur(node->right, proc)) return false; if (!proc(node->key, &node->value)) return false; if (node->left != NULL) if (!walk_inorder_rl_recur(node->left, proc)) return false; return true; } static bool walk_postorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *)) { if (node->left != NULL) if (!walk_postorder_lr_recur(node->left, proc)) return false; if (node->right != NULL) if (!walk_postorder_lr_recur(node->right, proc)) return false; if (!proc(node->key, &node->value)) return false; return true; } static bool walk_preorder_lr_recur(NODE *node, bool (*proc)(int, PERSON *)) { if (!proc(node->key, &node->value)) return false; if (node->left != NULL) if (!walk_preorder_lr_recur(node->left, proc)) return false; if (node->right != NULL) if (!walk_preorder_lr_recur(node->right, proc)) return false; return true; } static void clear_recur(NODE *node) { if (node->left != NULL) clear_recur(node->left); if (node->right != NULL) clear_recur(node->right); free(node); } static void create_queue(HBTREE hbtree) { hbtree->head = NULL; hbtree->tail = NULL; } static NODE *put_queue(HBTREE hbtree, NODE *node) { QNODE *new_qnode; if ((new_qnode = (QNODE *)malloc(sizeof(QNODE))) == NULL) return NULL; new_qnode->node = node; new_qnode->next = NULL; if (hbtree->tail != NULL) hbtree->tail->next = new_qnode; else hbtree->head = new_qnode; hbtree->tail = new_qnode; return node; } static NODE *get_queue(HBTREE hbtree) { QNODE *qnode; NODE *rnode; if (hbtree->head == NULL) return NULL; qnode = hbtree->head; hbtree->head = qnode->next; if (hbtree->head == NULL) hbtree->tail = NULL; rnode = qnode->node; free(qnode); return rnode; } static void destroy_queue(HBTREE hbtree) { QNODE *qnode, *temp_qnode; qnode = hbtree->head; while (qnode != NULL) { temp_qnode = qnode->next; free(qnode); qnode = temp_qnode; } } static void subtree_shift(HBTREE hbtree, NODE *node1, NODE *node2, NODE *node3) { if (node1 == NULL) hbtree->root = node3; else if (node1->left == node2) node1->left = node3; else node1->right = node3; } /* app.c */ #include #include #include "binarytree.h" bool disp_node(int key, PERSON *value); int main(void) { HBTREE hbtree; PERSON per = {"Noname", 0}; int keys[] = {70, 50, 34, 56, 19, 80, 67, 43, 27, 76, 79, 105, 82, 65, 0}; if ((hbtree = create_bt()) == NULL) { fprintf(stderr, "cannot create binary tree!..\n"); exit(EXIT_FAILURE); } for (int i = 0; keys[i] != 0; ++i) if (!insert_item_alternative_bt(hbtree, keys[i], &per)) { fprintf(stderr, "cannot insert item: %d\n", keys[i]); exit(EXIT_FAILURE); } walk_inorder_lr_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_inorder_rl_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_postorder_lr_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_inorder_lr_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_preorder_lr_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_breadth_first_bt(hbtree, disp_node); printf("\n------------------------------\n"); if (!delete_bt(hbtree, 70)) { fprintf(stderr, "cannot find item!...\n"); exit(EXIT_FAILURE); } walk_breadth_first_bt(hbtree, disp_node); printf("\n------------------------------\n"); printf("total item: %jd\n", count_bt(hbtree)); destroy_bt(hbtree); return 0; } bool disp_node(int key, PERSON *value) { printf("%d ", key); fflush(stdout); return true;; } Biz yukarıdaki örnekte "dengelenmemiş (unbalanced)" arama ağacı oluşturduk. Dengelenmemiş ağaçlar bir arama performansını düşürebilmektedir. Dengelenmemiş ikili ağaçlarda eleman eklemenin ve aramanın "en kötü durum senaryosu (worst case)" O(N) karmaşıklığa kadar kötüleşmektedir. Örneğin ağaca aşağıdaki gibi sıralı bilgilerin geldiğini düşünelim: 10, 20, 30, 40, 50, 60, 70, ... Bu durumda ikili arama ağacının bağlı listeden bir farkı kalmamaktadır. Bu nedenle uygulamalarda "dengelenmiş (balanced)" ağaçlar tercih edilmektedir. Dengelenmiş ağaçlarda ağaca her eleman eklendiğinde ve eleman silindiğinde ağacın dengede kalması sağlanmaktadır. Pekiyi dengelenmiş ağaçlar nasıl oluşturulmaktadır? Dengeleme işlemi her eleman eklendiğinde ve silindiğinde O(1) karmaşıklıkta ya da O(log N) karmaşıklıkta yapılan bir işlemdir. Böylece ağacın bir yandan uzamaıs engellenir. Ağacın tüm yapraklarının benzer yükseklikte kalması sağlanır. Bunun sonucunda arama işlemi de en kütü durum senaryosunda O(log N) olacaktır. Dengeleme için çeşitli algoritmalar önerilmiştir. En önemli iki dengeleme algoritmasına AVL (Adelson-Velsky-Landis) ve "Red Black Tree" algoritması denilmektedir. Bu iki algoritma arasında özel durumlara göre birinin lehine farlılıklar oluşabilmektedir. Uyglamada en çok kullanılan dengeleme algoritması AVL algoritmasıdır. Bu dengeleme algoritmasının kullanıldığı arama ağaçlarına "AVL ağacı (AVL tree)" da denilmektedir. Biz kursumunda ikili ağaçların dengelenmesi üzerinde durmayacağız. Bu konu ve diğer ağaç algoritmaları "Sistem Programalama ve İleri C Uygulamaları-II" kursunun konusu içerisindedir. Pekiyi 3'lü, 4'lü 5'li gibi arama ağaçları nasıl oluşturulmaktadır? Bu tür arama ağaçlarına "B-Tree" denilmektedir. 2'den daha fazla alt düğüme sahip olan ağaçlar bellek üzerindeki aramalarda (internal search) tercih edilmemektedir. Bunlar tipik olarak veritabanı yönetim sistemleri (DBMS) tarafından disk üzerindeki aramalarda (external search) kullanılmaktadır. Pekiyi örneğin 3'lü bir arama ağacı nasıl organize edilmektedir. İşte bu tür durumlarda düğüm üzerinde tek değer değil birden fazla değer tutulur. Örneğin 3'lü bir amarama ağacında göstericilerden biri a değerinden küçük olan düğümü, diğeri a ile b arasında olan düğümü diğeri de b'den büyük olan düğümü gösterir. Böylece ağacın yükseklüği azaltılmış olur. Disk işlemlerinde her bir düğümden diğerine geçiş için bir disk okuması gerektiği için bu durum ilgili kayda daha hızlı erişilmesine olanak sağlamaktadır. Çok kullanılan diğer bir ağaç türüne de "heap ağacı" ya da kısaca "heap" denilmektedir. Buradaki "heap" teriminin dinamik bellek yönetiminde geçen "heap" terimi ile bir ilgisi yoktur. Heap ağaçları "öncelik kuyruklarını (priority queues)" oluşturmak için kullanılmaktadır. Öncelik kuyruklarında kuyruğa yerleştirilen her elemanın bir öncelik derecesi (numarası) vardır. Kuyruktan eleman alınacağı zaman baştaki ya da sondaki eleman değil öncelik numarası en yüksek olan eleman alınmaktadır. Örneğin kuyrukta şu elemanlar olsun: 4 10 7 2 14 6 5 Kuyruğun başının sol taraf olduğunu düşünelim. Yani kuyruğa son eklenen eleman 5, ilk eklenen eleman ise 4 olsun. Eğer bu kuyruk sistemi FIFO olsaydı biz 4'ü alırdık. Kuyruk sistemi LIFO (stack) olsaydı biz kuyruktan 5'i alırdık. İşte eğer bir öncelik kuyruğu söz konusu ise biz kuyruktan 14'ü alırız. Çünkü en öncelikli eleman 14'tür. Bundan sonra yeniden kuyruktan eleman almak istesek bu kez 10'u alırız. Öncelik kuyruklarından eleman alma bu haliyle O(N) karmaşıklıkta bir işlemdir. İşte heap ağaçları bu işlemi O(log N) karmaşıklığa düşürmek için kullanılmaktadır. Heap ağaçları "heap sort" denilen sıraya dizme algortimesının gerçekleştirilmesinde de kullanılmaktadır. Heap ağaçları tipik olarak ikili ağaç biçiminde karşımıza çıkmaktadır. Her ne kadar heap ağaçları 2'den fazla düğüme sahip olabilirse de uygulamada 2'li heap ağaçları tercih edilmektedir. İkili heap ağaçlarına İngilizce "binary heap" de denilmektedir. Biz bu bağlamda "heap ağacı" ya da "heap" dediğimizde "ikili heap ağaçları" anlaşılmalıdır. Bir heap ağacının iki önemli karaktersitiği vardır: -> Heap ağaçları her zaman "tam ağaç (complete tree)" biçimindedir. Tam ağaçlarda yaprak düğümlerin dışında tüm düğümlerin 2 alt düğüme sahip olduğunu yaprakların ise en alt kademede soldan sağa bulunduğunu anımsayınız. -> Heap ağacında bir düğümün tüm alt düğümleri o düğümden düşük ya da yüksek değerleri sahip olmak zorundadır. Eğer bir düğüm alt düğümlerinden daha yüksek değere sahipse böyle heap ağaçlarına "max-heap", eğer bir düğüm alt düğümlerinden daha düşük değere sahipse böyle heap ağaçlarına "min-heap" denilmektedir. Örneğin: 100 50 70 30 40 60 35 17 24 32 Bu örnekteki ağaç yukarıdaki iki kuralı karşıladığı için bir "max-heap" ağacıdır. Heap ağacına eleman eklenirken eleman önce "tam ağacı (complete tree)" bozmayacak biçimde en alta en soldaki boş yere eklenir. Sonra eklenen eleman kuralı bozabileceği için üst düğümle karşılaştırılarak yer değiştirilir. Bu işlem kural bozulmayana kadar devam ettirilir. Bu işleme İngilizce "heapify" da denilmektedir. Örneğin yukarıdaki ağaca 75 eklemek isteyelim. Önce eklemeyi en altta en sol boş pozisyona yaparız: 100 50 70 30 40 60 35 17 24 32 75 Sonra bu 75 değeri üst düğümle karşılatırılıp duruma yer göre yer değiştirilir: 100 50 70 30 75 60 35 17 24 32 40 Bu işlem devam ettirlir: 100 75 70 30 50 60 35 17 24 32 40 Artık kural korunmuştur ve ekleme süreci bitirilir. Heap ağacından eleman alınacağı zaman her zaman kökten eleman alınır. Örneğin yukarıdaki ağaçtan eleman alacaksak 100'ü alırız. Bu durumda 100'ün yerine geçecek eleman onun alt düğümlerinin büyük olanıdır: 75 75 70 30 50 60 35 17 24 32 40 Tabii alt düğümün yerine onun alt düğümlerinden en büyük olan getirilir: 75 75 70 30 50 60 35 17 24 32 40 Böyle devam edilir: 75 50 70 30 75 60 35 17 24 32 40 Ta ki yapraklara iniline kadar: 75 50 70 30 40 60 35 17 24 32 75 Alınan değer en aşağı indiğinde işleme son verilir ve tam ağaç bozulmayacak biçimde o eleman silinir. Heap ağacına eleman eklemek en kötü durumda O(log N) karmaşıklıktadır. Eleman almak da aynı biçimdedir. Aslında heap ağaçlarında gerçekte ağaç oluşturulmamaktadır. Çünkü tam ağaçlar her zaman diziye dönüştürülebilmektedir. Bir heap ağacını diziye dönüştürürken dizinin 0'ıncı indisli elemanını kolaylık olsun diye boş bırakabiliriz. Diziye dönüştürme şu kurala göre yapımaktadır: -> Her zaman üst düğümün alt düğümleri üzet düğümün indeksinin iki katı ve iki katından bir fazla yerde bulunur. Aşağıdaki heap ağacına bakınız: 100 50 70 30 40 60 35 17 24 32 75 Bu heap ağacının diziye dönüştürülmesi şöyle yapılacaktır: 0 1 2 3 4 5 6 7 8 9 10 11 x 100 50 70 30 40 60 35 17 26 32 75 Burada örneğin 70 numaralı elemanın indeksi 3'tür. Bu durumda bu elemanın alt düğümleri 6 ve 7'inci indekstedir. İşte aslında heap ağaçlarında hiç ağaç oluşturulmaz. Sanki ağaç oluşturulmuş gibi her şey dizi üzerinde yukarıdaki indeks kuralına uygun bir biçimde yapılır. Aşağıda heap ağaçları ile öncelik kuyruklarının oluşturulmasına bir örnek verilmektedir. * Örnek 1, /* hpqueue.h */ #ifndef PQUEUE_H_ #define PQUEUE_H_ #include #include #define PQUEUE_DEF_CAPACITY 8 /* Type Declarartion */ typedef struct tagPERSON { char name[32]; } PERSON; typedef PERSON VALUETYPE; typedef struct tagHEAP_ITEM { int prio; VALUETYPE value; } HEAP_ITEM; typedef struct PQUEUE { HEAP_ITEM *hitems; size_t capacity; size_t count; } PQUEUE, *HPQUEUE; /* Function Prototypes */ HPQUEUE create_pq(void); bool put_pq(HPQUEUE hpqueue, int prio, VALUETYPE *value); bool get_pq(HPQUEUE hpqueue, int *prio, VALUETYPE *value); bool resize_pq(HPQUEUE hpqueue, size_t new_capacity); void destroy_pq(HPQUEUE hqueue); /* inline Function Definitions */ static inline bool isempty_pq(HPQUEUE hpqueue) { return hpqueue->count == 0; } static inline void clear_pq(HPQUEUE hpqueue) { hpqueue->count = 0; } #endif /* hpqueue.c */ #include #include #include "pqueue.h" HPQUEUE create_pq(void) { HPQUEUE hpqueue; if ((hpqueue = (HPQUEUE)malloc(sizeof(PQUEUE))) == NULL) return NULL; if ((hpqueue->hitems = (HEAP_ITEM *)malloc(sizeof(HEAP_ITEM) * PQUEUE_DEF_CAPACITY)) == NULL) { free(hpqueue); return NULL; } hpqueue->capacity = PQUEUE_DEF_CAPACITY; hpqueue->count = 0; return hpqueue; } bool put_pq(HPQUEUE hpqueue, int prio, VALUETYPE *value) { size_t index; if (hpqueue->count + 1 == hpqueue->capacity) if (!resize_pq(hpqueue, hpqueue->capacity * 2)) return false; index = hpqueue->count + 1; while (index > 1 && prio > hpqueue->hitems[index / 2].prio) { hpqueue->hitems[index] = hpqueue->hitems[index / 2]; index /= 2; } hpqueue->hitems[index].prio = prio; hpqueue->hitems[index].value = *value; ++hpqueue->count; return true; } bool get_pq(HPQUEUE hpqueue, int *prio, VALUETYPE *value) { size_t i, ci; if (hpqueue->count == 0) return false; *prio = hpqueue->hitems[1].prio; *value = hpqueue->hitems[1].value; i = 1; ci = 2; while (ci <= hpqueue->count) { if (ci + 1 < hpqueue->count && hpqueue->hitems[ci + 1].prio > hpqueue->hitems[ci].prio) ++ci; if (hpqueue->hitems[hpqueue->count].prio > hpqueue->hitems[ci].prio) break; hpqueue->hitems[i] = hpqueue->hitems[ci]; i = ci; ci *= 2; } hpqueue->hitems[i] = hpqueue->hitems[hpqueue->count]; --hpqueue->count; return true; } bool resize_pq(HPQUEUE hpqueue, size_t new_capacity) { HEAP_ITEM *hitems; if (new_capacity <= hpqueue->capacity) return false; if ((hitems = (HEAP_ITEM *)realloc(hpqueue->hitems, new_capacity * sizeof(HEAP_ITEM))) == NULL) return false; hpqueue->hitems = hitems; hpqueue->capacity = new_capacity; return true; } void destroy_pq(HPQUEUE hpqueue) { free(hpqueue->hitems); free(hpqueue); } /* app.c */ #include #include #include "pqueue.h" void disp_pq(HPQUEUE hpqueue) { for (size_t i = 1; i <= hpqueue->count; ++i) printf("%d ", hpqueue->hitems[i].prio); printf("\n"); } int main(void) { HPQUEUE hpqueue; int prios[] = {100, 85, 90, 60, 70, 10, 40, -1}; VALUETYPE values[] = {{"Ali Serce"}, {"Sacit Bulut"}, {"Ayse Er"}, {"Necati Ergin"}, {"Guray Sonmez"}, {"Ziya Taskent"}, {"Gencay Coskun"}}; int prio; VALUETYPE value; if ((hpqueue = create_pq()) == NULL) { fprintf(stderr, "cannot create priority queue!...\n"); exit(EXIT_FAILURE); } for (int i = 0; prios[i] != -1; ++i) if (!put_pq(hpqueue, prios[i], &values[i])) { fprintf(stderr, "cannot add item!..\n"); exit(EXIT_FAILURE); } disp_pq(hpqueue); while (!isempty_pq(hpqueue)) { if (!get_pq(hpqueue, &prio, &value)) { fprintf(stderr, "cannot get value!..\n"); exit(EXIT_FAILURE); } printf("prio: %d, value: %s\n", prio, value.name); disp_pq(hpqueue); printf("----------------------\n"); } destroy_pq(hpqueue); return 0; } >>>> Graph Veri Yapısı: En sık karşılaşılan veri yapılaran biri de "graph" denilebn veri yapısıdır. Bir graph düğümlerden ve kenarlardan oluşur. Düğümlere İngilizce "vertex" ya da "node" denilmektedir. Düğüm arasındaki bağlantıyı temsil eden kenarlara da İngilizce "edge" denilmektedir. Aslında düğümlerden Ve kenarlardan oluşan en genel veri yapısı "graph" veri yapısıdır. Ağaçlar aslında bir çeşit graph'tır. Belli bir düğüm kök yapıldığında her düğüme bu kök düğümden yalnızca tek bir yol ile gelinen özel graph'lara "ağaç (tree)" denilmektedir. Yani aslında her ağaç bir graph'tır, ancak her graph bir ağaç değildir. Dolayısıyla genel olarak graph'larda bir düğüme biren fazla yerden gelinebilmektedir. Graph veri yapılarının pratikte pek çok uygulama alanı vardır. Graplar üzerinde yüzden fazla uygulaması olan problem tanımlanmıştır. Graph'lar konusunda çalışmadan önce temel graph terminolojisini bilmek gerekir. Tmel graph terminolojisi şöyledir: >>>>> Yönlü ve Yönsüz Graflar (Directed and Undirected): Eğer düğümler arasındaki yollarda bir yön belirtiliyorsa bu tür graflara "yönlü graflar" denilmektedir. Düğümler arasındaki kenarlarda bir yön bilgisi yoksa bu tür graflara da "yönsüz graflar" denilmektedir. Aslında yönsüz graflar iki yönlü graflar gibi de düşünülebilir. Örneğin kara yolları yönlü bir grafla, sosyal ağlar (hepsi değil) yönsüz bir grafla temsil edilebilir. >>>>> Döngüsel Olan (Cyclic) ve Döngüsel Olmayan (Acyclic) Graflar: Bir grafta bir düğümden başlanarak aynı düğüme gelebilmenin bir yolu varsa bu tür graflara döngüsel graflar denilmektedir. Döngüsel graflarda her düğüm için kendine gelen bir yol olması gerekmez. Herhangi bir düğümden kendisine gelen herhangi bir yol varsa bu grafı döngüsel graf yapmaktadır. Döngüsellik tipik olarak yönlü graflar için kullanılan bir terimdir. İngilizce buna "directed acyclic grapg (DAG)" da denilmektedir. >>>>> Yol (Path): Bir grafta bir düğümden diğerine varabilmek için geçilen kenarların sırasıyla dizilimine "yol (path)" denilmektedir. >>>>> Tur (Cycle): Bir yolun ilk ve son düğümü aynıysa başka bir deyişle bir düğümden çıkılıp yeniden aynı düğüme gelen yola "tur" denilmektedir. Tuların tüm düğümleri kapsaması gerekmez. >>>>> Bağlantılı Graflar (Connected Graphs): Her düğümle her düğüm arasında en az bir yolun olduğu yönsüz graflara "bağlantılı graflar (connected graphs)" denilmektedir. Graflardaki düğümleri bilye, kenarları da bunları bağlayan ip olarak düşünürsek bağlantılı graflarda herhangi bir bilyeyi tutup yukarı kaldırdığımızda tüm bilyelerin kalkması gerekir. >>>>> Öz döngülü (self-loop) Graflar: Eğer graflarda kendindne kendine bir kenar tanımlanabiliyorsa böylesi graflara öz döngülü graflar (self-loop graphs) denilmektedir. Genellikle gragflar öz döngülü olmazlar. >>>>> Alt Graflar (Subgraphs): Bir grafın belli bir alt kümesine alt graph denilmektedir. >>>>> Tam Graflar (Complete Graphs): Bir yönsüz grafta her düğümden her düğüme bir kenar varsa bu tür graflara "tam graflar (complete graphs)" denilmektedir. Graflarla çalışmak için öncelikle grafları veri yapısı olarak temsil etmek gerekir. Grafların temsil edilmesi için temel iki yöntem kullanılmaktadır. -> Komşuluk Matrisi (Adjacency-Matrix) Yöntemi -> Komşuluk Listeleri (Adjacency-Lists) Yöntemi Bu yöntemlerden, >>>>> Komşuluk matrisi yönteminde düğümlerden bir kare matris oluşturulur. Matrisin elemanları 0 ve 1'lerden oluşturulabilir. 0 ilgili iki düğüm arasında bir kenar olmadığını 1 ise olduğunu gösterebilir. Örneğin: A B C D A 0 1 0 1 B 0 0 1 1 C 1 1 0 0 D 0 1 1 0 Burada örneğin C'den C'ye kenar yoktur, ancak C'den D'ye kenar vardır. Tabii eğer kenarlara bilgiler iliştirilecekse (örneğin bir yol uzunluğu gibi) bu durumda matrisin elemanları birer gösterici olabilir. Kenar bilgileri bu göstericilerin gösterdiği yerdedir. İki düğüm arasında yol yoksa matrisin ilgili elemanı NULL adres içerebilir. Yönsüz graflarda komşuluk matirisinin simetrik bir matris olması gerektiğine dikkat ediniz. >>>>> Komşuluk listeleri yönteminde her düğümün bir listesi vardır. Bu liste o düğümden gidilebilecek düğümleri belirtir. Buradakli liste dinamik büyütülen bir dizi olabilir ya da bağlı liste olabilir. Yukarıdaki graf'ın komşuluk listeleri yoluyla temsili şöyle olacaktırÇ: A: B, C B: C, D C: A, B D: B, C BUrada eğer yollara bilgiler iliştirilecekse listenin elemanları birer gösterici olabilir ya da o bilgilerdne oluşan birer yapı da olabilir. Pekiyi hangi temsil daha iyidir? Aslında iki yöntemin de avantajları ve dezavantajları bulunmaktadır. Komşul matrisi yönteminde kenara iliştirilen bilgiye O(1) karmaşıklıkta erişilebilir. Komşuluk listeleri yönteminde sıralı arama gerekir. Eğer kenar sayısı çok fazla ise bu yöntemde her düğüm için bir hash tablosu ya da ikili arama ağacı kullanılabilir. Uygulamada genellikle "komşuluk listeleri" yöntemi tercih edilmektedir. Bu temsillerde düğümlerin nasıl temsil edileceği de bir problemdir. Düğümlere isim verilirse ismin aranması uzun sürebilir. Bu durumda en etkin yöntem düğümlere isim değil numara vermektir. Tabii numaradan isim elde edilebilir ya da isimden numara alde edilebilir. Graflarlar üzerinde algoritmalar için değişik graf kütüphaneleri oluşturulmuştur. En yaygın kullanılan graf kütüphanesi C++'taki "Boost Graf Kütüphanesi (Boost Graph Library (BGL))" denilen kütüphanedir. Bu kütüphanede düğümler ve kenarlar sınıflarla temsil edilmiştir. Boost Graf Kütüphanesi pek çok graf kütüphanesine de ilham kaynağı olmuştur. Benzer biçimde Java için, C# için Python için çeşitli graf kütüphaneleri geliştirilmiştir. C için yaygın kullanılan bir graf kütüphanesi bulunmamaktadır. Ancak incelemek için "igraph" kütüphanesi önerilebilir. Kütüphanenin kaynak kodlarına aşağıdan erişebilirisiniz: https://igraph.org/ Yukarıda da belirttiğimiz gibi graf veri yapısı için iki yöntem kullanılmaktadır. Ancak ağırlıklı tercih "komşuluk listeleri" yöntemidir. Bu yöntemin türden bağımsız uygulaması biraz zor olabilmektedir. Biz burada makul bir gerçekleştirim üzerinde duracağız. Gerçekleştiririmizde DGRAPH nesnesi VERTEX nesnelerini, VERTEX nesneleri de EDGE nesnelerini tutacaktır. Komşuluk listeleri yönteminde her vertex'in o vertex'ten gidilebilecek vertex'leri tuttuğunu belirtmiştik. Ancak biz burada bir vertex'ten gidilecek vertex'leri değil gidilecek vertex'lere ilişkin kenar nesnelerini tutacağız. DGRAPH yapısı şöyleolabilir: typedef struct tagDGRAPH{ VERTEX **vertices; size_t count; size_t capacity; } DGRAPH; Görüldüğü gibi burada graf veri yapısı vertex'lerin kendilerini değil adreslerini tutmaktadır. Bunun nedeni bu dizi büyütüldüğünde VERTEX nesnelerinin adreslerinin değişmemesinin sağlanmasıdır. Buradaki count bu gösterici dizisinin dolu olan eleman sayısını, capacity ise gösterici dizisi için ayrılan kapasiteyi belirtmektedir. VERTEX yapısı aşağıdakai gibi olabilir: typedef struct tagVERTEX { EDGE **edges; size_t count; size_t capacity; /* VERTEX INFO */ char name[32]; } VERTEX; Görüldüğü gibi VERTEX nesneleri EDGE nesnelerinin adreslerini tutmaktadır. Yine bu dizinin dolu olan eleman sayısı count elemanı ile dizi için tahsis edilen toplam alan ise capacity elemanı ile tutulmaktadır. Burada biz vertex'e bir isim verdik. İsterseniz burada vertex'lere başka bilgiler de iliştirebilirsiniz. EDGE yapısı da şöyle olabilir: typedef struct tagEDGE { VERTEX *v1; VERTEX *v2; /* EDGE INFO */ int length; } EDGE; Görüldüğü gibi bir EDGE nesnesi başlangıç ve bitiş VERTEX nesnelerinin adreslerini tutmaktadır. Burada EDGE nesnesine bir uzunluk bilgisi iliştirilmiştir. Tabi başka bilgiler de iliştirilebilir. Bu tasarımda biz DGRAPH nesnesinin tuttuğu vertex'leri dinamik büyütülen bir dizi ile, vertex'lerin tuttuğu kenarları da dinamik büyütülen bir dizi ile temsil ettik. Burada bağlı listeler ya da hash tabloları da kullanılabilir. Graf veri yapısını oluşturduktan sonra graflar üzerinde işlem yapan algoritmaların oluşturulması gerekir. Ancak çok çeşitli graf algoritmaları vardır. Biz kursumuzda yalnızca birkaç algoritma üzerinde duracağız. Bunlardan biri belli bir düğümden başlanarak grafın gidilebilen tüm düğümlerinin dolaşılmasıdır. Tabii bu da özyinelemeli bir biçimde yapılmalıdır. Yani bir düğümden gidilebilen tüm düğümler için fonksiyon kendini çağırmalıdır. Ancak graflarda düğümlere birden fazla yolla gelinebildiği için döngüsel bir sonsuz döngüye girmemek gerekir. Bunu engellemenin tipik yolu her ziyaret edilen düğümün ziyaret edildiğini bir yerde tutmak ve eğer düğüm daha önce ziyaret edilmişse onu yeniden ziyaret etmemektir. Graf veri yapısını kurduktan sonra graf üzerinde pek çok problemin çözümü yapılabilir. Literatürden yüzün üzerinde graf problemi tanımlanmıştır. Graf problemlerinin en çok bilinenlerinden bazıları şunlardır: -> Grafın dolaşılması -> Grafta iki düğüm arasındaki tüm yolların (paths) elde edilmesi (shortest path problem) -> Grafta iki düğüm arasında en kısa yolun elde edilmesi (path finding problem) -> En küçük örten ağaç problemi (minimum spanning tree) -> Gezgin satıcı problemi (travelling salesperson problem) -> Maximum akış problemi (maximum flow problem) -> Graf boyama problemi (graph coloring problem) -> Graf çizdirme problemi (graph drawing problem) Aşağıda graf veri yapısının oluşturulmasına ilişkin bir örnek verilmiştir. Buradaki veri yapılarını kullanarak yukarıdaki problemleri çözecek algoritmaları deneyebilirsiniz. * Örnek 1, /* graph.h */ #ifndef GRAPH_H_ #define GRAPH_H_ #include #include /* Symbolic Constants */ #define DEF_VERTEX_CAPACITY 4 #define DEF_EDGE_CAPACITY 4 #define MAX_CSV_LINE_LENGTH 1024 /* Type Declarations */ typedef struct tagEDGE { struct tagVERTEX *v1; struct tagVERTEX *v2; /* EDGE INFO */ double length; } EDGE; typedef struct tagVERTEX { EDGE **edges; size_t count; size_t capacity; size_t index; /* VERTEX INFO */ char name[32]; } VERTEX; typedef struct tagDGRAPH{ VERTEX **vertices; size_t count; size_t capacity; size_t ecount; } DGRAPH, *HDGRAPH; /* Function Prototypes */ HDGRAPH create_dgraph(void); VERTEX *add_vertex(HDGRAPH hdgraph, VERTEX *vertex); VERTEX *add_vertex_info(HDGRAPH hdgraph, const char *name); EDGE *add_edge(HDGRAPH hdgraph, VERTEX *v1, VERTEX *v2, double length); EDGE *add_edge_info(HDGRAPH hdgraph, const char *name1, const char *name2, double length); DGRAPH *build_graph(const char *path); bool traverse_depth_first_vertex(HDGRAPH hdgraph, VERTEX *vertex, bool (*proc)(VERTEX *)); bool traverse_depth_first_name(HDGRAPH hdgraph, const char *name, bool (*proc)(VERTEX *)); void clear_dgraph(HDGRAPH hdgraph); void destroy_dgraph(HDGRAPH hdgraph); /* inline Funcrion Definitions */ static inline size_t vertex_count(HDGRAPH hdgraph) { return hdgraph->count; } #endif /* graph.c */ #include #include #include #include "graph.h" static VERTEX *find_vertex_name(HDGRAPH hdgraph, const char *name); static EDGE *create_edge(VERTEX *v1, VERTEX *v2, double length); static VERTEX *create_vertex(const char *name); static bool traverse_depth_first_vertex_recur(VERTEX *vertex, int *visiteds, bool (*proc)(VERTEX *)); HDGRAPH create_dgraph(void) { HDGRAPH hdgraph; if ((hdgraph = (HDGRAPH)malloc(sizeof(DGRAPH))) == NULL) return NULL; if ((hdgraph->vertices = (VERTEX **)malloc(DEF_VERTEX_CAPACITY * sizeof(VERTEX *))) == NULL) { free(hdgraph); return NULL; } hdgraph->count = 0; hdgraph->capacity = DEF_VERTEX_CAPACITY; hdgraph->ecount = 0; return hdgraph; } VERTEX *add_vertex(HDGRAPH hdgraph, VERTEX *vertex) { VERTEX **new_vertices; if (find_vertex_name(hdgraph, vertex->name) != NULL) return NULL; if (hdgraph->count == hdgraph->capacity) { if ((new_vertices = realloc(hdgraph->vertices, hdgraph->capacity * sizeof(VERTEX *) * 2)) == NULL) return NULL; hdgraph->vertices = new_vertices; hdgraph->capacity *= 2; } vertex->index = hdgraph->count; hdgraph->vertices[hdgraph->count] = vertex; ++hdgraph->count; return vertex; } VERTEX *add_vertex_info(HDGRAPH hdgraph, const char *name) { VERTEX *vertex; VERTEX **new_vertices; if (find_vertex_name(hdgraph, name) != NULL) return NULL; if ((vertex = create_vertex(name)) == NULL) return NULL; if (hdgraph->count == hdgraph->capacity) { if ((new_vertices = realloc(hdgraph->vertices, hdgraph->capacity * sizeof(VERTEX *) * 2)) == NULL) return NULL; hdgraph->vertices = new_vertices; hdgraph->capacity *= 2; } vertex->index = hdgraph->count; hdgraph->vertices[hdgraph->count] = vertex; ++hdgraph->count; return vertex; } EDGE *add_edge(HDGRAPH hdgraph, VERTEX *v1, VERTEX *v2, double length) { EDGE *edge; EDGE **new_edges; if ((edge = create_edge(v1, v2, length)) == NULL) return NULL; if (v1->count == v1->capacity) { if ((new_edges = realloc(v1->edges, v1->capacity * 2 * sizeof(EDGE *))) == NULL) { free(edge); return NULL; } v1->capacity *= 2; } v1->edges[v1->count] = edge; ++v1->count; ++hdgraph->ecount; return edge; } EDGE *add_edge_info(HDGRAPH hdgraph, const char *name1, const char *name2, double length) { VERTEX *v1, *v2; EDGE *edge; if ((v1 = find_vertex_name(hdgraph, name1)) == NULL) { if ((v1 = create_vertex(name1)) == NULL) return NULL; if (add_vertex(hdgraph, v1) == NULL) goto EXIT1; } if ((v2 = find_vertex_name(hdgraph, name2)) == NULL) { if ((v2 = create_vertex(name2)) == NULL) goto EXIT1; if (add_vertex(hdgraph, v2) == NULL) goto EXIT2; } if ((edge = add_edge(hdgraph, v1, v2, length)) == NULL) goto EXIT2; return edge; EXIT2: free(v2); EXIT1: free(v1); return NULL; } DGRAPH *build_graph(const char *path) { FILE *f; HDGRAPH hdgraph; char buf[MAX_CSV_LINE_LENGTH + 2]; const char *name1, *name2, *length; char *str; if ((f = fopen(path, "r")) == NULL) goto EXIT1; if ((hdgraph = create_dgraph()) == NULL) goto EXIT2; while (fgets(buf, MAX_CSV_LINE_LENGTH + 2, f) != NULL) { if ((name1 = strtok(buf, ",\n")) == NULL) continue; if ((name2 = strtok(NULL, ",\n")) == NULL) goto EXIT3; if ((length = strtok(NULL, ",\n")) == NULL) goto EXIT3; if (strtok(NULL, ",\n") != NULL) goto EXIT3; if (add_edge_info(hdgraph, name1, name2, atof(length)) == NULL) goto EXIT3; } return hdgraph; EXIT3: destroy_dgraph(hdgraph); EXIT2: fclose(f); EXIT1: return NULL; } bool traverse_depth_first_vertex(HDGRAPH hdgraph, VERTEX *vertex, bool (*proc)(VERTEX *)) { int *visiteds; bool result; if (!proc(vertex)) return false; if ((visiteds = (int *)calloc(hdgraph->count, sizeof(int))) == NULL) return false; result = traverse_depth_first_vertex_recur(vertex, visiteds, proc); free(visiteds); return result; } bool traverse_depth_first_name(HDGRAPH hdgraph, const char *name, bool (*proc)(VERTEX *)) { VERTEX *vertex; if ((vertex = find_vertex_name(hdgraph, name)) == NULL) return false; return traverse_depth_first_vertex(hdgraph, vertex, proc); } void clear_dgraph(HDGRAPH hdgraph) { for (size_t v = 0; v < hdgraph->count; ++v) { for (size_t e = 0; e < hdgraph->vertices[v]->count; ++e) free(hdgraph->vertices[v]->edges[e]); hdgraph->vertices[v]->count = 0; free(hdgraph->vertices[v]); } hdgraph->count = 0; } void destroy_dgraph(HDGRAPH hdgraph) { for (size_t v = 0; v < hdgraph->count; ++v) { for (size_t e = 0; e < hdgraph->vertices[v]->count; ++e) free(hdgraph->vertices[v]->edges[e]); free(hdgraph->vertices[v]->edges); } free(hdgraph->vertices); free(hdgraph); } static VERTEX *create_vertex(const char *name) { VERTEX *vertex; if ((vertex = (VERTEX *)malloc(sizeof(VERTEX))) == NULL) return NULL; if ((vertex->edges = (EDGE **)malloc(DEF_EDGE_CAPACITY * sizeof(EDGE *))) == NULL) { free(vertex); return NULL; } vertex->count = 0; vertex->capacity = DEF_EDGE_CAPACITY; strcpy(vertex->name, name); return vertex; } static VERTEX *find_vertex_name(HDGRAPH hdgraph, const char *name) { for (size_t i = 0; i < hdgraph->count; ++i) if (!strcmp(name, hdgraph->vertices[i]->name)) return hdgraph->vertices[i]; return NULL; } static EDGE *create_edge(VERTEX *v1, VERTEX *v2, double length) { EDGE *edge; if ((edge = (EDGE *)malloc(sizeof(EDGE))) == NULL) return NULL; edge->v1 = v1; edge->v2 = v2; edge->length = length; return edge; } static bool traverse_depth_first_vertex_recur(VERTEX *vertex, int *visiteds, bool (*proc)(VERTEX *)) { VERTEX *v; visiteds[vertex->index] = 1; for (size_t e = 0; e < vertex->count; ++e) { v = vertex->edges[e]->v2; if (visiteds[v->index]) continue; if (!proc(v)) return false; if (!traverse_depth_first_vertex_recur(v, visiteds, proc)) return false; } return true; } /* app.c */ #include #include #include #include "graph.h" void err_exit(const char *msg); bool disp_vertex(VERTEX *v); int main(void) { HDGRAPH hdgraph; if ((hdgraph = build_graph("graph.csv")) == NULL) { fprintf(stderr, "cannot build graph!..\n"); exit(EXIT_FAILURE); } traverse_depth_first_name(hdgraph, "A", disp_vertex); destroy_dgraph(hdgraph); return 0; } void err_exit(const char *msg) { fprintf(stderr, "%s\n", msg); exit(EXIT_FAILURE); } bool disp_vertex(VERTEX *v) { printf("%s\n", v->name); return true; } >> Veri Yapılarının Genelleştirilmesi : Şimdiye kadar yaptığımız örneklerde ilgili veri yapılarının türleri baştan belirlenmiş durumdaydı. Her ne kadar bazı örneklerde biz veri yapısı içerisinde tutulacak dğerlerin türlerini DATATYPE, VALUETYPE gibisi isimlerle typedef ettiysek de bu durum bir genelleme oluşturmamaktadır. Başka bir deyişle şimdiye kadar yaptığımız örneklerde veri yapılarımız toplamda tek bir türe ilişkin olabilmektedir. Örneğin bağlı listeler için şu yapıları kullanmıştık: typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE head; NODE *tail; size_t count; } LLIST, *HLLIST; Burada biz birden fazla bağlı liste oluşturabilecek bir mekanizma sağlamıştık. Ancak tüm bağlı listelerimiz DATATYPE türünü tutmaktadır. DATATYPE türü int olarak typedef edilldiğine göre biz ne kadar bağlı liste yaratırsak yaratalım hepsi int değerleri tutan bağlı listeler olacaktır. Örneğin İkili arama ağaçları için şu yapıları kullanmıştık: typedef struct tagPERSON { char name[32]; int city; } PERSON; typedef struct tagNODE { int key; PERSON value; struct tagNODE *left; struct tagNODE *right; } NODE; typedef struct tagBTREE { NODE *root; size_t count; } BTREE, *HBTREE; Burada da biz birden fazla bağlı liste oluşturabiliyorduk. Ancak bu bağlı listelerin hepsinin anahtarı int değerleri için PERSON türünden olmak zorundadır. Özetle şimdiye oluşturduğumuz veri yapıları "genel" değil belli bir tür için oluşturuldu. Şimdi biz "türden bağımsız" yani her türle çalışabilecke veri yapılarının nasıl oluşturalacağı üzerinde duracağız. Böylelikle veri yapılarını her tür için çalışabilir hale getirebileceğiz. Türden bağımsız veri yapılarının oluşturulabilmesi için iki teknik kullanılabilir: -> void Gösterici Tekniği -> Gömme Tekniği Bu yöntemlerden, >> void gösterici tekniği aslında etkin bir yöntem değildir. Bu yöntemin hem kullanımı zordur hem de bu yöntem diğerine göre yavaştır. Bu nedenle programcılar gömme tekniğini tercih etmektedir. Örneğin Linux kaynak kodlarında veri yapılarında genellik sağlamak için hep bu teknik kullanılmıştır. void gösterici tekniğinde veri yapısının tutacağı değerlerin türleri yerine onların adresleri ve uzunlukları kullanılır. Eğer karşılaştırma yapılmak istenirse callback fonksiyonlardan faydalanılmaktadır. Örneğin ikili arama ağacını bu teknikle gerçekleştirmek isteyelim. Biz artık ağaç düğümlerinde tutulacak anahtar ve değerin türünü bilmek zorunda değiliz. Ancak onların uzunluklarını bilmek zorundayız. Bunun için gereken yapı bildirimleri şöyle olabilir: typedef struct tagNODE { struct tagNODE *left; struct tagNODE *right; char key_value[]; } NODE; typedef struct tagQNODE { struct tagQNODE *next; NODE *node; } QNODE; typedef struct tagBTREE { NODE *root; size_t count; QNODE *head; QNODE *tail; size_t key_size; size_t value_size; bool (*compare)(const void *, const void *); } BTREE, *HBTREE; Burada tagNODE yapısının son elemanına dikkat ediniz. Burada C99 ile birlikte C'ye eklenmiş olan "flexible array member" özelliği kullanılmıştır. Bu özelliğe göre bir yapının son elemanı (başka elemanlarında bu yapılamaz) uzunluğu belirtilmemiş bir dizi olabilir. Derleyici bu dizi için bir yer ayırmamaktadır. Bu diziden amaç ilgili yapı nesnesinin bellekteki bitim adresinin kolay bir biçimde tespit edilmesini sağlamaktır. Handle alanında (tagBTREE yapısı) anahtar ve değer olarak kullanılacak bilgilerin uzunluğunun ve bir karşılaştırma fonksiyonunun adresinin bulundurulduğna dikkat ediniz. Biz ikili arama ağacını yaratırken bu bilgileri vermek zorundayız. * Örnek 1, Aşağıda void göstericiler kullanılarak türden bağımsız ikili arama ağacına bir örnek verilmiştir. /* binarytree.h */ #ifndef BINARYTREE_H_ #define BINARYTREE_H_ #include #include /* Type Declararions */ typedef struct tagNODE { struct tagNODE *left; struct tagNODE *right; char key_value[]; } NODE; typedef struct tagQNODE { struct tagQNODE *next; NODE *node; } QNODE; typedef struct tagBTREE { NODE *root; size_t count; QNODE *head; QNODE *tail; size_t key_size; size_t value_size; int (*compare)(const void *, const void *); } BTREE, *HBTREE; /* Function Prototypes */ HBTREE create_bt(size_t key_size, size_t value_size, int (*compare)(const void *, const void *)); bool insert_item_bt(HBTREE hbtree, const void *key, const void *value); bool walk_inorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)); bool walk_inorder_rl_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)); bool walk_postorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)); bool walk_preorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)); bool walk_breadth_first_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)); bool delete_bt(HBTREE hbtree, const void *key); void clear_bt(HBTREE hbtree); void destroy_bt(HBTREE hbtree); /* inline Function Fefinitions */ static inline size_t count_bt(HBTREE hbtree) { return hbtree->count; } #endif /* binarytree.c */ #include #include #include #include "binarytree.h" static bool walk_inorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *)); static bool walk_inorder_rl_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *)); static bool walk_postorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *)); static bool walk_preorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *)); static void create_queue(HBTREE hbtree); static NODE *put_queue(HBTREE hbtree, NODE *node); static NODE *get_queue(HBTREE hbtree); static void destroy_queue(HBTREE hbtree); static void subtree_shift(HBTREE hbtree, NODE *node1, NODE *node2, NODE *node3); static void clear_recur(NODE *node); HBTREE create_bt(size_t key_size, size_t value_size, int (*compare)(const void *, const void *)) { HBTREE hbtree; if ((hbtree = (HBTREE)malloc(sizeof(BTREE))) == NULL) return NULL; hbtree->root = NULL; hbtree->count = 0; hbtree->key_size = key_size; hbtree->value_size = value_size; hbtree->compare = compare; return hbtree; } bool insert_item_bt(HBTREE hbtree, const void *key, const void *value) { NODE *new_node, *node, *parent_node = NULL; int result; if ((new_node = (NODE *)malloc(sizeof(NODE) + hbtree->key_size + hbtree->value_size)) == NULL) return false; memcpy(new_node->key_value, key, hbtree->key_size); memcpy(new_node->key_value + hbtree->key_size, value, hbtree->value_size); new_node->left = NULL; new_node->right = NULL; if (hbtree->root == NULL) { hbtree->root = new_node; ++hbtree->count; return true; } node = hbtree->root; while (node != NULL) { parent_node = node; result = hbtree->compare(key, node->key_value); if (result < 0) node = node->left; else if (result > 0) node = node->right; else { memcpy(node->key_value + hbtree->key_size, value, hbtree->value_size); return true; } } if (hbtree->compare(key, parent_node->key_value) < 0) parent_node->left = new_node; else parent_node->right = new_node; ++hbtree->count; return true; } bool walk_inorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)) { if (hbtree->root != NULL) return walk_inorder_lr_recur(hbtree->root, hbtree->key_size, proc); return true; } bool walk_inorder_rl_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)) { if (hbtree->root != NULL) return walk_inorder_rl_recur(hbtree->root, hbtree->key_size, proc); return true; } bool walk_postorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)) { if (hbtree->root != NULL) return walk_postorder_lr_recur(hbtree->root, hbtree->key_size, proc); return true; } bool walk_preorder_lr_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)) { if (hbtree->root != NULL) return walk_preorder_lr_recur(hbtree->root, hbtree->key_size, proc); return true; } static bool walk_preorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *)) { if (!proc(node->key_value, node->key_value + key_size)) return false; if (node->left != NULL) if (!walk_preorder_lr_recur(node->left, key_size, proc)) return false; if (node->right != NULL) if (!walk_preorder_lr_recur(node->right, key_size, proc)) return false; return true; } bool walk_breadth_first_bt(HBTREE hbtree, bool (*proc)(const void *, const void *)) { NODE *node; bool result = true; create_queue(hbtree); put_queue(hbtree, hbtree->root); while ((node = get_queue(hbtree)) != NULL) { if (!proc(node->key_value, node->key_value + hbtree->key_size)) { result = false; break; } if (node->left != NULL) if (put_queue(hbtree, node->left) == NULL) { fprintf(stderr, "put_queue cannot allocate memory!..\n"); result = false; break; } if (node->right != NULL) if (put_queue(hbtree, node->right) == NULL) { result = false; fprintf(stderr, "put_queue cannot allocate memory!..\n"); break; } } destroy_queue(hbtree); return result; } static void create_queue(HBTREE hbtree) { hbtree->head = NULL; hbtree->tail = NULL; } static NODE *put_queue(HBTREE hbtree, NODE *node) { QNODE *new_qnode; if ((new_qnode = (QNODE *)malloc(sizeof(QNODE))) == NULL) return NULL; new_qnode->node = node; new_qnode->next = NULL; if (hbtree->tail != NULL) hbtree->tail->next = new_qnode; else hbtree->head = new_qnode; hbtree->tail = new_qnode; return node; } static NODE *get_queue(HBTREE hbtree) { QNODE *qnode; NODE *rnode; if (hbtree->head == NULL) return NULL; qnode = hbtree->head; hbtree->head = qnode->next; if (hbtree->head == NULL) hbtree->tail = NULL; rnode = qnode->node; free(qnode); return rnode; } bool delete_bt(HBTREE hbtree, const void *key) { NODE *delete_node, *delete_parent_node, *replace_parent_node, *replace_node; int result; delete_node = hbtree->root; delete_parent_node = NULL; while (delete_node != NULL) { result = hbtree->compare(key, delete_node->key_value); if (result == 0) break; delete_parent_node = delete_node; if (result < 0) delete_node = delete_node->left; else if (result > 0) delete_node = delete_node->right; } if (delete_node == NULL) return false; if (delete_node->left == NULL) subtree_shift(hbtree, delete_parent_node, delete_node, delete_node->right); else if (delete_node->right == NULL) subtree_shift(hbtree, delete_parent_node, delete_node, delete_node->left); else { replace_parent_node = delete_node; replace_node = delete_node->right; while (replace_node->left != NULL) { replace_parent_node = replace_node; replace_node = replace_node->left; } if (replace_parent_node != delete_node) { subtree_shift(hbtree, replace_parent_node, replace_node, replace_node->right); replace_node->right = delete_node->right; } subtree_shift(hbtree, delete_parent_node, delete_node, replace_node); replace_node->left = delete_node->left; } free(delete_node); --hbtree->count; return true; } void clear_bt(HBTREE hbtree) { if (hbtree->root != NULL) { clear_recur(hbtree->root); hbtree->root = NULL; hbtree->count = 0; } } void destroy_bt(HBTREE hbtree) { if (hbtree->root != NULL) clear_recur(hbtree->root); free(hbtree); } static bool walk_inorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *)) { if (node->left != NULL) if (!walk_inorder_lr_recur(node->left, key_size, proc)) return false; if (!proc(node->key_value, node->key_value + key_size)) return false; if (node->right != NULL) if (!walk_inorder_lr_recur(node->right, key_size, proc)) return false; return true; } static bool walk_inorder_rl_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *)) { if (node->right != NULL) if (!walk_inorder_rl_recur(node->right, key_size, proc)) return false; if (!proc(node->key_value, node->key_value + key_size)) return false; if (node->left != NULL) if (!walk_inorder_rl_recur(node->left, key_size, proc)) return false; return true; } static bool walk_postorder_lr_recur(NODE *node, size_t key_size, bool (*proc)(const void *, const void *)) { if (node->left != NULL) if (!walk_postorder_lr_recur(node->left, key_size, proc)) return false; if (node->right != NULL) if (!walk_postorder_lr_recur(node->right, key_size, proc)) return false; if (!proc(node->key_value, node->key_value + key_size)) return false; return true; } static void destroy_queue(HBTREE hbtree) { QNODE *qnode, *temp_qnode; qnode = hbtree->head; while (qnode != NULL) { temp_qnode = qnode->next; free(qnode); qnode = temp_qnode; } } static void subtree_shift(HBTREE hbtree, NODE *node1, NODE *node2, NODE *node3) { if (node1 == NULL) hbtree->root = node3; else if (node1->left == node2) node1->left = node3; else node1->right = node3; } static void clear_recur(NODE *node) { if (node->left != NULL) clear_recur(node->left); if (node->right != NULL) clear_recur(node->right); free(node); } /* app.c */ #include #include #include "binarytree.h" typedef struct tagPERSON { char name[32]; int no; } PERSON; bool disp_node(const void *key, const void *value); int comp_person(const void *key1, const void *key2); int main(void) { HBTREE hbtree; PERSON per = {"Noname", 0}; int keys[] = {70, 50, 34, 56, 19, 80, 67, 43, 27, 76, 79, 105, 82, 65, 0}; int del_key = 70; if ((hbtree = create_bt(sizeof(int), sizeof(PERSON), comp_person)) == NULL) { fprintf(stderr, "cannot create binary tree!..\n"); exit(EXIT_FAILURE); } for (int i = 0; keys[i] != 0; ++i) if (!insert_item_bt(hbtree, &keys[i], &per)) { fprintf(stderr, "cannot insert item: %d\n", keys[i]); exit(EXIT_FAILURE); } walk_inorder_lr_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_inorder_rl_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_postorder_lr_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_preorder_lr_bt(hbtree, disp_node); printf("\n------------------------------\n"); walk_breadth_first_bt(hbtree, disp_node); printf("\n------------------------------\n"); if (!delete_bt(hbtree, &del_key)) { fprintf(stderr, "cannot find item!...\n"); exit(EXIT_FAILURE); } walk_breadth_first_bt(hbtree, disp_node); destroy_bt(hbtree); return 0; } bool disp_node(const void *key, const void *value) { const int *ikey = (const int *)key; printf("%d ", *ikey); fflush(stdout); return true; } int comp_person(const void *key1, const void *key2) { const int *ikey1 = (const int *)key1; const int *ikey2 = (const int *)key2; if (*ikey1 > *ikey2) return 1; if (*ikey1 < *ikey2) return -1; return 0; } >> Genelleştirmede void gösterici tekniği yerine gömme tekniğinin tercih edildiğini belirtmiştik. Bu tekniği açıklamadna önce hazırlık amacıyla C'ye ilişkin bir konunun üzerinde duracağız. C'nin içerisinde bildirilmiş olan offsetof makrosu yapının tür ismini ve yapıdaki bir eleman ismini parametre olarak alır, o elemanın yapının başından itibaren kaçıncı offsette olduğunu verir. Örneğin: struct SAMPLE { int a; int b; char c[32]; char d; int e; }; size_t result; result = offsetof(struct SAMPLE, e); Burada yapının e elemanı yapı nesnesinin başından itibaren muhtemelen 44'üncü offset'indedir. Elemanların offset numaraları aşağıdaki gibi olacaktır: struct SAMPLE { int a; /* 0 */ int b; /* 4 */ char c[32]; /* 8 */ char d; /* 40 */ int e; /* 44 */ }; İşte offsetof makrosu bir yapı elemanının yapın nesnesnin kaçıncı offsetinde olduğunu elde etmek için kullanılmaktadır. offsetof makrosu size_t türündne bir değer vermektedir. * Örnek 1, #include #include struct SAMPLE { int a; int b; char c[32]; char d; int e; }; int main(void) { size_t result; result = offsetof(struct SAMPLE, e); printf("%zd\n", result); /* 44 */ return 0; } offsetof makrosu tipik olarak aşağıdakine benzer bir biçimde yazılmaktadır: #define myoffsetof(type, member) ((size_t)&((type *)0)->member) Burada 0 adresi önce ilgili yapı türünden adrese dönüştürülmüştür. (0 değerini herhangi türden bir adrese dönüştürürsek bu NULL adres anlamına gelmemektedir. 0 dğeri void * türüne dönüştürülürse bu NULL ares anlamına gelmektedir.) Sonra yapının elemanına eriilip onun adresi alınmıştır. Bu durumda adresin sayısal bileşeni aslında ilgili eelemanın offset'i olacaktır. Nihayet bu değer son olarak size_t türüne dönüştürülmüştür. Yukarıdaki 0 adresinden başlayan yapının elemanına erişip onun adresini alma işlemi C'de "tanımsız davranış" değildir. Çünkü burada standartlara göre gerçek bir erişim yapılmamaktadır. Yalnızca oranın adresi elde edilmiştir. * Örnek 1, #include #include #include #define myoffsetof(type, member) ((size_t)&((type *)0)->member) struct SAMPLE { int a; int b; char c[32]; char d; int e; }; int main(void) { size_t result; result = myoffsetof(struct SAMPLE, e); printf("%zd\n", result); /* 44 */ return 0; } Bir yapı içerisindeki bir elemanın adresini, o elemanın ismini ve yapının türünü bildiğimizi varsayalım. Bu durumda elemanın adresine ilişkin yapı nesnesinin başlangıç adresini veren container_of biçiminde bir makro yazabiliriz: #define container_of(ptr, type, member) ((type *) ((char *) (ptr) - offsetof(type, member))) Makronun birinci parametresi nesne içerisindeki elemanın adresi, ikinci parametresi yapının tür ismi ve üçüncü parametresi de elemanın ismini almaktadır. C standartlarında offsetof bir makro bulunmakla birlikte bu işi yapabilecek bir makro bulunmamaktadır. Aşağıdaki container_of makrosunn kullanımına ilişkin bir örnek verilmektedir. * Örnek 1, #include #include #define container_of(ptr, type, member) ((type *) ((char *) (ptr) - offsetof(type, member))) struct SAMPLE { int a; char b; int c; char d; double e; int f; }; int main(void) { struct SAMPLE s = { 10, 'x', 20, 'y', 12.4, 100 }; double *pd = &s.e; struct SAMPLE *ps; ps = container_of(pd, struct SAMPLE, e); printf("%d, %f\n", ps->a, ps->e); return 0; } Gömme yöntemiyle veri yapıları genelleştirilirken aslında veri yapıları adeta kendi çekirdek kısımlarından oluşturulur. Örneğin bir bağlı listenin türden bağımsız bir biçimde bu teknikle oluşturulmak istendiğini düşünelim. Bağlı liste belli türdne bilgileri değil NODE isimli düğümleri birbirine bağlayacaktır. Ancak NODE isimli düğümler bağlı liste elemanlarının tutacağı nesneleri içermeyecektir. Örneğin: typedef struct tagNODE { struct tagNODE *next; struct tagNODE *prev; } NODE; Görüldüğü gibi burada NODE yapısı düğümğn tuttuğu bilgiyi içermemektedir. İşte bağlı liste bu biçimdeki düğümleri birbirine bağlamaktadır. Örneğin: head_node <---> node <----> node <---> node <---> .... Pekiyi bağlı liste düğümleri yalnızca link tuttuğuna göre böyle bir bağlı listeninin nasıl bir faydası olabilir? İşte bu düğümler aslında başka bir yapının içerisinde bir eleman olarak tutulacaktır. Örneğin: struct PERSON { char name[32]; int no; NODE link; }; Görüldüğü gibi PERSON yapısının bir elemanı bağlı listenin düğümündne oluşmaktadır. Yani biz aslında düğümlerdne bağlı liste oluşturmaktayız ancak bu düğümler de ilgili türden nesnelerin elemanı durumundadır. Nasıl olsa biz yapının bir elemanının adresini bildiğimizde o yapı nesnesinin başlangıç adresini yukarıda gösterdiğimiz gibi container_of makrosu ile elde edebiliriz. Bu yöntemde bir yapı nesnesi tek bir bağlı liste içerisinde bulunmak zorunda değildir. Örneğin Linux çekirdeğinde proses kontrol bloğu temsil eden task_struct yapısı pek çok bağlı listede bulunmaktadır. Burada yapılacak şey yapı içerisinde birden fazla link elemanı tutmaktır. Örneğin: struct PERSON { char name[32]; int no; NODE link1; NODE link2; }; Aşağıdaki örnekte bir handle sistemi kullanılarak gömme tekniği ile türden bağımsız bağlı liste oluşturulmuştur. Burada yine bağlı listeler create_llist gibi bir fonksiyonla yaratılmaktadır. Gömme tekniğinde handle sisteminin kullanılması aşağı seviyeli çekirdek kodları için uygun bir yöntem olmayabilir. Ancak biz aşağıda önce handle sistemi ile gömme tekniğine örnek vereceğiz. Sonra handle sistemi olmadan aynı tekniği kullanacağız. Yukarıda da belirttiğimiz gibi aslında örneğin Linux, BSD, CSD gibi işletim sistemi kodlarında bu handle sistemi kullanılmamaktadır. * Örnek 1, /* llist.h */ #ifndef LLIST_H_ #define LLIST_H_ #include #include /* Type Declarations */ typedef struct tagNODE { struct tagNODE *next; struct tagNODE *prev; } NODE; typedef struct tagLLIST { NODE head; size_t count; } LLIST, *HLLIST; /* Function Prototypes */ HLLIST create_llist(void); NODE *insert_next(HLLIST hllist, NODE *node, NODE *new_node); NODE *insert_prev(HLLIST hllist, NODE *node, NODE *new_node); NODE *add_tail(HLLIST hllist, NODE *new_node); NODE *add_head(HLLIST hllist, NODE *new_node); void remove_node(HLLIST hllist, NODE *node); NODE *getp_item(HLLIST hllist, size_t index); bool walk_llist(HLLIST hllist, bool (*proc)(NODE *)); bool walk_llist_rev(HLLIST hllist, bool (*proc)(NODE *)); void clear_llist(HLLIST hllist); void destroy_llist(HLLIST hllist); /* inline Function Definitions */ static inline size_t count_llist(HLLIST hllist) { return hllist->count; } static inline NODE *head_llist(HLLIST hllist) { return hllist->head.next; } static inline NODE *tail__llist(HLLIST hllist) { return hllist->head.prev; } /* Macro Definitions */ #define container_of(ptr, type, member) ((type *) ((char *) (ptr) - offsetof(type, member))) #define FOR_EACH(hllist, node) for (node = (hllist)->head.next; node != &(hllist)->head; node = node->next) #define FOR_EACH_REV(hllist, node) for (node = (hllist)->head.prev; node != &(hllist)->head; node = node->prev) #define FREE_LLIST(hllist, node) for (NODE *tnode = (hllist)->head.next; node = tnode, tnode = tnode->next, node != &(hllist)->head; ) #endif /* llist.c */ #include #include #include "llist.h" /* Function Definitions */ HLLIST create_llist(void) { HLLIST hllist; if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL) return NULL; hllist->head.next = &hllist->head; hllist->head.prev = &hllist->head; hllist->count = 0; return hllist; } NODE *insert_next(HLLIST hllist, NODE *node, NODE *new_node) { node->next->prev = new_node; new_node->next = node->next; node->next = new_node; new_node->prev = node; ++hllist->count; return new_node; } NODE *insert_prev(HLLIST hllist, NODE *node, NODE *new_node) { node->prev->next = new_node; new_node->next = node; new_node->prev = node->prev; node->prev = new_node; ++hllist->count; return new_node; } NODE *add_tail(HLLIST hllist, NODE *new_node) { return insert_prev(hllist, &hllist->head, new_node); } NODE *add_head(HLLIST hllist, NODE *new_node) { return insert_next(hllist, &hllist->head, new_node); } void remove_node(HLLIST hllist, NODE *node) { node->prev->next = node->next; node->next->prev = node->prev; --hllist->count; } NODE *getp_item(HLLIST hllist, size_t index) { NODE *node; if (index >= hllist->count) return NULL; node = hllist->head.next; for (size_t i = 0; i < index; ++i) node = node->next; return node; } bool walk_llist(HLLIST hllist, bool (*proc)(NODE *)) { for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next) if (!proc(node)) return false; return true; } bool walk_llist_rev(HLLIST hllist, bool (*proc)(NODE *)) { for (NODE *node = hllist->head.prev; node != &hllist->head; node = node->prev) if (!proc(node)) return false; return true; } void clear_llist(HLLIST hllist) { hllist->head.next = &hllist->head; hllist->head.prev = &hllist->head; hllist->count = 0; } void destroy_llist(HLLIST hllist) { free(hllist); } /* app.c */ #include #include #include "llist.h" typedef struct tagPERSON { char name[32]; int no; NODE link; } PERSON; bool disp_person(NODE *node); int main(void) { HLLIST hllist; PERSON *per; NODE *node; if ((hllist = create_llist()) == NULL) { fprintf(stderr, "cannot create linked list...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < 100; ++i) { if ((per = (PERSON *)malloc(sizeof(PERSON))) == NULL) { fprintf(stderr, "ccano tallocate memory!..\n"); exit(EXIT_FAILURE); } per->no = i; per->name[0] = '\0'; add_tail(hllist, &per->link); } walk_llist(hllist, disp_person); printf("\n--------------------\n"); FOR_EACH (hllist, node) { per = container_of(node, PERSON, link); printf("%d ", per->no); fflush(stdout); } printf("\n--------------------\n"); FOR_EACH_REV (hllist, node) { per = container_of(node, PERSON, link); printf("%d ", per->no); fflush(stdout); } FREE_LLIST(hllist, node) { per = container_of(node, PERSON, link); free(per); } return 0; } bool disp_person(NODE *node) { PERSON *per; per = container_of(node, PERSON, link); printf("%d ", per->no); fflush(stdout); return true; } Graph çizdirmek için en fazla kullanılan araç "graphviz" isimli programdır. Bu programda bir grafı çizdirmek için önce graf bir dosyada ismine "dot dili (dot language)" bir dilde betimlenir. Sonra "dot" isimli programa bu dosya girdi olarak verilir. Bu program da bu girdi dosyasından hareketle istenilen formatta bir çıktı dosyası üretir. Tabii bunları yapabilmek için önce graphviz programını bilgisayarımıza kurmamız gerekir. Graphviz aşağıdaki bağlantıdan indirilebilir: https://graphviz.org/download/ Graphviz'de bir graf çizmek için grafın türü belirtilerek blok oluşturulur. Sonra bloğun içerisinde grafın düğümleri ve kenarları belirtilir. Örneğin: digraph G { size="30, 40!" A -> B B -> D B -> C C -> A D -> C D -> E E -> D E -> F F -> C C -> F G -> F C -> G C -> E D -> H H -> I I -> D } Buradaki size ibaresi inch cinsinden grafin genişlik ve yülksekliğiniş belirtmektedir. Satırların yanlarına köşeli parantez içerisinde label eklenebilir. Bu durumda belirtilen yazılar kenarların yanlarında görüntülenir. Örneğin: digraph G { A -> B [label="10"] B -> D [label="7"] B -> C [label="15"] C -> A [label="5"] D -> C [label="20"] D -> E [label="18"] E -> D [label="18"] E -> F [label="19"] F -> C [label="16"] C -> F [label="16"] G -> F [label="11"] C -> G [label="25"] C -> E [label="12"] D -> H [label="11"] H -> I [label="9"] I -> D [label="20"] } Aşağıdaki örnekte Windows'ta CSV dosyasından hareketle bir ".dot" dosyasını programlama yoluyla oluşturulmuş sonra da "dot" programı çalıştırılarak ".png" dosyası elde edilmiştir. Nihayet bu ".png" dosyası da yine programlama yoluyla görüntülenmiştir. Burada kullanılabielcek örnek "graph.csv" dosyası şöyledir: A,B,10 B,D,7 B,C,15 C,A,5 D,C,20 D,E,18 E,D,18 E,F,19 F,C,16 C,F,16 G,F,11 C,G,25 C,E,12 D,H,11 H,I,9 I,D,20 A,I,40 I,J,11 J,D,21 D,A,17 F,H,30 G,J,40 Programın kodları ise aşağıdaki gibidir: * Örnek 1, /* graphmake.c */ #include #include #include #include #include #define MAX_LINE_LEN 4096 void ExitSys(LPCSTR lpszMsg); bool csv2dot(const char *path_csv, const char *path_dot) { FILE *f_csv, *f_dot; char buf[MAX_LINE_LEN]; char *name1, *name2, *length; bool retval = false; if ((f_csv = fopen(path_csv, "r")) == NULL) return false; if ((f_dot = fopen(path_dot, "w")) == NULL) goto EXIT1; if (fprintf(f_dot, "digraph G {\n") < 0) goto EXIT2; while (fgets(buf, MAX_LINE_LEN, f_csv) != NULL) { if ((name1 = strtok(buf, ",\n")) == NULL) continue; if ((name2 = strtok(NULL, ",\n")) == NULL) goto EXIT2; if ((length = strtok(NULL, ",\n")) == NULL) goto EXIT2; if (strtok(NULL, ",\n") != NULL) goto EXIT2; if (fprintf(f_dot, "\t%s -> %s [label=\"%s\"]\n", name1, name2, length) < 0) goto EXIT2; } if (fprintf(f_dot, "}\n") < 0) goto EXIT2; retval = true; EXIT2: fclose(f_dot); EXIT1: fclose(f_csv); return retval; } bool dot2png(const char *path_dot, char *path_png) { char cmdline[4096]; STARTUPINFO si = {sizeof(si)}; PROCESS_INFORMATION pi; bool result; sprintf(cmdline, "dot -Tpng %s -o %s", path_dot, path_png); result = CreateProcess(NULL, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return result; } int main(void) { HINSTANCE hResult; if (!csv2dot("graph.csv", "test.dot")) { fprintf(stderr, "cannot generate dot file!..\n"); exit(EXIT_FAILURE); } if (!dot2png("test.dot", "test.png")) { fprintf(stderr, "cannot generate png file!..\n"); exit(EXIT_FAILURE); } hResult = ShellExecute(NULL, "open", "test.png", NULL, NULL, SW_NORMAL); if ((INT_PTR)hResult < 32) ExitSys("ShellExecute"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } /*================================================================================================================================*/ (75_24_03_2024) & (76_30_03_2024) & (77_06_04_2024) & (78_07_04_2024) & (79_20_04_2024) & (80_21_04_2024) & (81_27_04_2024) (82_28_04_2024) & (83_04_05_2024) & (84_05_05_2024) & (85_11_05_2024) & (86_12_05_2024) & (87_18_05_2024) & (88_25_05_2024) > Threads : Kursumuzun bu bölümünde thread kavramını göreceğiz. Çok thread'li çalışma modeli ve thread senkronizasyonu konularını ele alacağız. Bir prosesin bağımsız olarak çizelgelenen farklı akışlarına "thread" denilmektedir. (Thread sözcüğü İngilizce "iplik" anlamına gelmektedir. Akışlar ipliğe benzetilerek bu sözcük uydurulmuştur.) Proses kavramı çalışmakta olan programın tüm bilgilerini temsil eden bir kavramdır. Thread'ler proseslerin akışlarını temsil etmektedir. Yani thread'ler proses'lerin bir unsurudur. Bir proses tek bir akışa (yani thread'e) sahip olabileceği gibi birden fazla akışa (yani thread'e) sahip olabilmektedir. Thread'ler 90'lı yılların ortalarında işletim sistemlerine sokulmuştur. Bugün artık thread'ler programlamanın önemli konularından sayılmaktadır. Windows sistemi ilk kez Windows NT ile (1993) sonra da Windows 95 ile thread'li çalışma modeline sahip olmuştur. Benzer biçimde UNIX/Linux sistemlerine de yine 90'lı yılların ortalarında thread'ler eklenmiştir. İşletim sistemlerinde prosesler çalışmaya tek bir thread'le başlamaktadır. Bu thread proses yaratılırken işletim sistemi tarafından yaratılmaktadır. Prosesin bu thread'ine "ana thread (main thread)" denilmektedir. Prosesin diğer thread'leri program çalışırken sistem fonksiyonlarıyla ya da bunları çağıran kütüphane fonksiyonlarıyla yaratılmaktadır. Yani program tek bir thread'le çalışmaya başlamaktadır. Diğer thread'ler programcı tarafından oluşturulmaktadır. Thread'ler tamamen işletim sisteminin kontrolü altında yaratılıp çalıştırılmaktadır. Dolayısıyla Windows sistemlerinde thread'ler Windows API fonksiyonlaır ile, UNIX/Linux ve macOS sistemlerinde ise POSIX fonksiyonlaır ile yaratılıp idare edilmektedir. Bazı programlama dillerinin standart kütüphanelerinde platform bağımzıs thread sınıfları ya da fonksiyonları bulunabilmektedir. Tabii burada platform bağımsızlık kaynak kod temelindedir. Bu kütüphaneler aslında farklı sistemlerde o sistemlerin sistem fonksiyonlarını çağırarak thread işlemlerinş yapmaktadır. Örneğin C++11 ile birlikte C++'a bir thread kütüphanesi eklenmiştir. Biz C++'ta threda işlemlerini hep aynı biçimde yapabiliriz. Ancak buradaki fonksiyonlar ve sınıflar Windows sistemlerinde Windows API fonksiyonları, UNIX/Linux sistemleridne POSIX fonksiyonları çağrılarak işlemlerini gerçekleştirmektedir. Pekiyi thread'lere neden gereksinim duyulmaktadır? Bunu birkaç maddeyle açıklayabiliriz: -> Thread'ler arkaplan işlemlerin yapılması için iyi araç oluşturmaktadır. Örneğin biz bir yandan bir şeyler yaparken arka planda da periyodik birtakım işlemleri yapmak isteyebiliriz. Thread'ler yokken bu tür işlemler çok zor yapılıyordu. Ancak thread'ler işletim sistemlerine eklenenince bu işlemleri yapmak çok kolaylaştı. Tread'ler sayesinde biz normal işlemlerimizi yaparken bir thread oluşturup arkaplan işlemleri o thread'e havale edebiliriz. -> Thread'ler programları hızlandırmak amacıyla kullanılabilmektedir. Bir işi birden fazla kaışa yaptırmak hız kazancı sağlamaktadır. Sistemimizde tek bir işlemci olsa bile bizim prosesimiz diğer proseslere göre daha fazla CPU zamanı kullanabilir hale gelebilmektedir. -> Thread'ler blokeye yol açan durumlarda işlemlerin devam ettirilmesini sağlayabilmektedir. Çünkü işletim sistemleri thread temelinde bloke uygulamaktadır. Örneğin prosesin bir thread'i bir kalvye okuması sırasında blokede beklerken diğer thread'leri çalışmaya devam edecektir. Bu durum da prosesin başka işlemlere yanıt verebilmesini sağlamaktadır. -> Thread'ler paralel programlama için mecburen kullanılması gereken unsurlardır. Paralel programlama "birden fazla CPU ya da çekirdeğin olduğu durumda prosesin thread'lerinin farklı CPU ya da çekirdeklere atanarak aynı anda çalıştırıması" anlamına geşmektedir. Örneğin çok büyük bir diziyi sıraya dizecek olalım. Makinemizde 10 tane çekirdek olsun. Biz bu işlemi tek bir thread'le yaparsak makinemizdeki 10 çekirdek olmasının avantajından faydalanamayız. Halbuki biz dizimizi 10 parçaya ayırıp 10 farklı thread'i farklı çekirdeklere atarsak bunlar paralel bir biçimde dizinin çeşitli parçalarını aynı anda sort edecektir. Sonra bunları birleştirirsek toplamda zamanı çok kısaltmış olabiliriz. -> Thread'ler GUI programlama ortamlarında bir mesaj geldiğinde uzun süren işlemlerin yapılabilmesine olanak sağlamaktadır. Bir proses çalışmaya tek bir thread ile başlar. Buna prosesin ana thread'i (main thread) denilmektedir. Bu ana thread C programlarında tipik olarak akışın main fonksiyonundan girdiği thread'tir. Diğer thread'ler işletim sistem fonksiyonlarıyla yaratılmaktadır. Tabii Windows'ta bu sistem fonksiyonlarını çağıran API fonksiyonları UNIX/Linux sistemlerinde de POSIX fonksiyonları bulunmaktadır. Modern işletim sisemlerinin çizelgeleyici (scheduler) alt sistemleri thread'leri çizelgelemektedir. Yani işletim sistemi hangi prosese ilişkin olursa olsun sıradaki thread'i CPU'ya atar, onu belli süre çalıştırır. Sonra çalışmasına ara vererek sonraki thread'i CPU'ya atar. Modern çok thread'li (multithreaded) işletim sistemlerinde prosesler değil thread'ler çizelgelenmektedir. Yukarıda da belirttiğimiz gibi prosesin bir thread'i bloke olduğunda diğerleri çalışmaya devam edebilmektedir. Bir thread'in çalışmasına ara verilerek diğer bir thread'in kaldığı yerden çalışmasına devam ettirilmesi sürecine "bağlamsal geçiş (context switch)" denilmektedir. Bağlamsal geçiş sırasında önceki thread ile sonraki thread aynı prosesin thread'leri olabildiği gibi farklı proseslerin thread'leri de olabilir. Çok işlemcili ya da çekirdekli sistemlerde zaman paylaşımlı çalışma modeli değişmemektedir. Yalnızca servis veren birden fazla işlemci ya da çekirdek söz konusu olmaktadır. Thread'lerin sıra beklediği kuyruk sistemine işletim sistemleri terminolojisinde "çalışma kuyruğu (run queue)" denilmektedir. Çok işlemcili ya da çekirdekli sistemlerde çalışma kuyruğu bir tane olabilir ya da her işlemci ya da çekirdek için ayrı bir çalışma kuyruğu söz konusu olabilir. Örneğin Linux bir ara O(1) çizelgelemesi adı altında toplamda bir tane çalışma kuyruğu oluşturuyordu. Hangi işlemci ya da çekirdekteki thread'in işi biterse o çalışma kuyruğundan o işlemci ya da çekirdeğe atama yapıyordu. Ancak daha sonra bu çizelgeleme algoritması yine değiştirildi. Bugünkü çizelgeleme algoritmasında her işlemci ya da çekirdeğin ayrı bir çalışma kuyruğu bulunmaktadır. Tabii işletim sistemi bu tür durumlarda işlemci ya da çekirdeklerin çalışma kuyruklarını iş yükü bakımından dengelemeye çalışmaktadır. Thread'lerin stack'leri birbirinden ayrılmıştır. Yerel değişkenlerin "stack" denilen alanda yaratıldığını anımsayınız. Thread'lerin stack'leri biribirindne ayrıldığı için bir thread akışı bir fonksiyon üzerinde ilerlerken o fonksiyonun yerel değişkenleri o stack üzerinde yaratılmaktadır. Diğer bir thread de aynı fonksiyon üzerinde ilerliyorsa o yerel değişkenlee de o thread'in stack'inde yaratılacaktır. Böylece iki thread aynı kod üzerinde ilerliyor olsa da aslında yerel değişkenlerin farklı kopyalarını kullanıyor olacaklardır. Başka bir deyişle yerel değişkenlerin her thread için ayrı bir kopyası bulunmaktadır. Örneğin: void foo(void)) { int a; a = 10; ... ++a; ... ++a; ... } İki farklı thread bu foo fonksiyonunda ilerliyor olsun. Burada aslında her thread'in ayrı bir a değişkeni vardır. Dolayısıyla thread'lerden biri bu a değişkenini değiştirdiğinde kendi thread'inin stack'indeki a değişkenini değiştirmiş olur. Bu değişiklikten diğer thread'in a değişkeni etkilenmeyecektir. Ancak global değişkenler tüm thread'ler tarafından ortak biçimde kullanılmaktadır. Başka bir deyişle ".data" ve ".bss" bölümleri thread'e özgü değil prosese özgüdür. Örneğin bir thread bir global değişkeni değiştirdiğinde diğer thread o global değişkeni değişmiş görmektedir. Benzer biçimde heap alanı da prosese özgüdür. Yani thread'leri ayrı heap'leri yoktur. Toplamda bir tane heap vardır. O da prosesin heap alanıdır. Bir işin birden fazla akışa yaptırılması gerektiği durumlarda thread'ler proseslere göre çok daha etkin bir çözüm sunmaktadır. Çünkü yaratılması proseslerin yaratılmasından daha hızlı yapılmakta ve thread'ler proseslere göre daha az sistem kaynağı harcamaktadır. Prosesler önceki konularda da gördüğümüz gibi proseslerarası haberleşme yöntemleri (IPC) denilen yöntemlerle haberleşmektedir. Oysa thread'ler global nesneler yoluyla haberleşebilmektedir. Eskiden thread'ler yokken bir işin birden fazla akışa yaptırılması prosesler yoluyla gerçekleştiriliyordu. Oysa thread'ler işletim sistemlerine girince artık bunun için thread'ler kullanılmaya başlanmıştır. Artık günümüzde thread'ler ptogramlamanın temel unsurları durumuna gelmiştir. Pek çok programla dilinde thread'lerle kolay işlemler yapabilmek için standart kütüpaheneler bulunmaktadır. Hatta bazı dillerde artık thread'ler dilin sentaksının da içine sokulmuştur. Eskiden işlemciler mega hertz düzeyinde çalışıyordu. Zamanla bunların hızları 1000 kat civarında artırıldı. Ancak artırmanın fiziksel bir sınırına da yaklaşıldı. Artık hızlandırma işlemciyi bireysel olarak hızlandırmak yerine birden fazla işlemci (ya da çekirdek) kullanarak sağlanmaktadır. İşletim sistemleri de işlemcilere ya da çekirdeklere thread'leri atamaktadır. İşletim sistemlerinin çizelgeleyici alt sistemlerinin atama birimleri thread'lerdir. Eskiden board'lara birden fazla işlemci takılabiliyordu. Ancak zamanla teknoloji gelişince birden fazla işlemci tek bir chip'e yerleştirilmeye başlandı. Tek bir chip içindeki farklı işlemcilere "çekirdek (core)" denilmeye başlandı. Yukarıda da belirttiğimiz gibi thread'ler işletim sistemlerinin sistem fonksiyonlarıyla yaratılmaktadır. Windows'ta bu sistem fonksiyonlarını çağıran API fonksiyonları UNIX/Linux be macOS sistemlerinde de POSIX fonksiyonları vardır. Yüksek seviyeli programlama dillerinin kütüphanelerinde buluan thread fonksiyonları da aslında bu fonksiyonlar kullanılarak yazılmıştır. Örneğin biz C# ya da Java'da thread yarattığımızda aslında bu dillerin kütüphaneleri yaratımı Windows sistemlerinde Windows API fonksiyonlarını çağırarak UNIX/Linux ve macOS sistemlerinde de POSIX fonksiyonlarını çağırarak yapmaktadır. Thread'ler çeşitli biçimlerde sonlanabilmektedir. Bir thread'in en doğal sonlanması thread fonksiyonunun bitmesi ile gerçekleşir. Hem Windows sistemlerinde hem de UNIX/Linux ve macOS sistemlerinde thread fonksiyonu bittiğinde thread'ler de otomatik olarak sonlanmaktadır. Aşağıdaki örneklerimizde sonlanma bu biçimde doğal yolla gerçekleşmiştir. Tavsiye edilen sonlanma biçimi böyledir. Thread'lerin stack'leri birbirlerinden ayrılmıştır. Bu nedenle farklı thread akışları aynı fonksiyon üzerinde ilerlerken o fonksiyondaki yerel değişkenlerin ve parametre değişkenlerinin farklı kopyalarını kullanıyor durumda olurlar. Aşağıdaki Windows örneğinde ana thread ve yaratılan thread aynı Foo fonksiyonunu çağırmıştır. Ancak Foo içerisindeki i nesnesi iki threda için de farklı i nesneleridir. Bu örneği i yerel değişkenini global değişken yaparak da çalışırınız. İki çalıştırma arasındaki farkı gözlemleyiniz. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void Foo(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); Foo("main thread"); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { Foo("other thread"); return 0; } int i = 0; void Foo(LPCSTR pszName) { while (i < 10) { printf("%s: %d\n", pszName, i); Sleep(1000); ++i; } } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } Şimdi de sistemler özelinde thread'leri inceleyelim: >> Windows Sistemlerinde: Windows sistemlerinde thread'ler CreateThread isimli API fonksiyonuyla yaratılmaktadır. Fonksiyonun prototipi şöyledir: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); Fonksiyonun birinci parametresi thread kernel nesnesinin güvenlik bilgilerini belirtmektedir. Bu parametre NULL geçilebilir. İkinci parametre yaratılacak thread'in stack miktarını byte olarak belirtmektedir. Bu parametre 0 geçilirse default stack uzunluğu çalıştırılabilir dosyada (PE formatında) belirtilen uzunluk olarak alınır. Genel olarak default uzunluk 1MB'dir. Fonksiyonun üçüncü parametresi thread akıının başlatılacağı fonksiyonun adresini almaktadır. Her thread bizim belirlediğimiz bir fonksiyondan çalışmaya başlar. LPTHREAD_START_ROUTINE aşağıdaki gibi typedef edilmiştir: typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(LPVOID lpThreadParameter); Görüldüğü gibi LPTHREAD_START_ROUTINE geri dönüş değeri DWORD, parametresi LPVOID (yani void *) türünden olan bir fonksiyon adresini belirtmektedir. 32 bit Windows sistemlerinde "fonksiyon çağırma (calling convention)" biçimi __stdcall olmak zorundadır. Bu __stdcall anahtar sözcüğü Microsoft'a özgü olan bir eklentidir (extension). __stdcall WINAPI olarak da define edişmiştir: #define WINAPI __stdcall Bu çağırma biçimi Microsoft derleyicilerinde fonksiyon isminin solunda bulundurulmak zorundadır. Örnek bir thread fonksiyonu şöyle olabilir: DWORD WINAPI ThreadProc(LPVOID param) { ... } 64 bit Windows sistemlerinde "çağırma biçimi (calling convention)" kaldırılmıştır. Bu nedenle bus sistemlerde __stdcall önişlemci komutlarıyla aşağıdaki gibi silinmiştir: #define __stdcall O halde Windows programımızı 32 bit ve 64 bit uyumlu yazmak istiyorsak bu çağırma biçimini fonksiyonun soluna yazabiliriz. Nasıl olsa 64 bit derlemede bu çağırma biçimi silinecektir. CreateThread fonksiyonunun dördüncü parametresi thread fonksiyonuna geçirilecek olan parametreyi belirtmektedir. Thread'leryaratıldığında akış başlatılacağı thread fonksiyonuna bu parametrede belirtilen değer aktarılmaktadır. Biz böyle bir parametre geçmek istemiyorsak bu parametreyi NULL biçimde belirtebiliriz. Fonksiyonun beşinci parametresi tipik olarak 0 biçiminde ya da CREATE_SUSPENDED biçiminde geçilir. Eğer bu parametre 0 geçilirse thread yaratılır yaratılmaz çalışmaya başlar. Eğer bu parametreye CREATE_SUSPENDED değeri geçilirse threda yaratılır ancak henüz çalışmaya başlamaz. Thread'i çalıştırmak için ResumeThread API fonksiyonu kullanılmalıdır. Fonksiyonun son parametresi thread'in ID değerinin yerleştirileceği DWORD nesnesinin adresini almaktadır. Bu ID değeri bazı durumlarda gerekebilmektedir. Eğer thread'in ID değeri alınmayacaksa bu parametreye NULL geçilebilir. CreateThread fonksiyonu başarı durumunda yaratılan thread'in handle değerine, başarısızlık durumunda NULL adres değerine geri dönmektedir. Buradan elde edilen handle değeri diğer thread işlemlerinde kullanılmaktadır. Thread'in değeri thread işlemlerinde kullanılmaz. Handle değeri thread işlemlerinde kullanılmaktadır. Örneğin: HANDLE hThread; DWORD dwThreadId; DWORD __stdcall ThreadProc(LPVOID param); ... if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); ... Thread fonksiyonu bittiğinde thread kaynaklarının önemli bir bölümü zaten boşaltılmaktadır. Ancak thread kernel nesnesinin tam olarak boşaltımını sağlamak için CloseHandle fonksiyonu uygulanabilir. Aşağıdaki örnekte bir thread yaratılmıştır. Hem ana threadhem de yaratılan thread çalışmaktadır. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); for (int i = 0; i < 10; ++i) { printf("Main thread: %d\n", i); Sleep(1000); } CloseHandle(hThread); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); Sleep(1000); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } Bir thread herhangi bir noktada Windows'ta ExitThread API fonksiyonu ile sonlandırabilir. Bu fonksiyonları hangi thread akışı çağırırsa o thread sonlanmaktadır. exit fonksiyonunun tüm prosesi sonlandırdığına ancak ExitThread fonksiyonunun yalnızca tek bir thread'i sonlandırdığına dikkat ediniz. Therad'lerin de tıpkı prosesler gibi exit kodları vardır. Windows sistemlerinde thread'lerin exit kodları DWORD değerle temsil edilmektedir. Thread'in exit kodları thread sonlandığında ilgili prosesler tarafından alınıp çeşitli amaçlarla kullanılabilmektedir. Ancak uygulamaların çoğunda bu exit kodunu kullanamaya gerek duyulmamaktadır. Windows'taki ExitThread API fonksiyonunun prototipi şöyledir: void ExitThread( DWORD dwExitCode ); Fonksiyon thread'in exit kodunu parametre olarak almaktadır. Aşağıda Windows sistemlerinde ExitThread fonksiyonu ile thread'in sonlandırılmasına bir örnek verilmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); for (int i = 0; i < 10; ++i) { printf("Main thread: %d\n", i); Sleep(1000); } CloseHandle(hThread); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); if (i == 5) ExitThread(0); Sleep(1000); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } Threadler arasında Windows sistemlerinde ve UNIX/Linux sistemlerinde üstlük/altlık (parent/child) ilişkisi yoktur. Yani bir thread'i hangi thread'in yarattığının genel olarak bir önemi yoktur. (Bu konuda bazı ayrıntılar bulunmaktadır.) Bir thread başka bir thread'i Windows sistemlerinde TerminateThread API fonksiyonuyla o anda zorla sonlandırabilir. Bu tür sonlandırmalar tavsiye edilmemektedir. Çünkü bir thread'in belli bir noktada (örneğin printf fonksiyonunun içerisinde) zorla sonlandırılması programın çökmesine yol açabilmektedir. TerminateThread API fonksiyonunun prototipi şöyledir: BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode); Fonksiyonun birinci parametresi sonlandırılack thread'inb handle değerini almaktadır. İkinci parametre ise thread'in exit kodunu 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. Aşağıdaki örnekte ana thread diğer thread'i zorla TerminateThread API fonksiyonuyla sonlandırmıştır. Burada tüm program bu zorla sonlandırmadan olumsuz etkilenebilir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); for (int i = 0; i < 10; ++i) { if (i == 5) if (!TerminateThread(hThread, 0)) ExitSys("TerminateThread"); printf("Main thread: %d\n", i); Sleep(1000); } CloseHandle(hThread); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); Sleep(1000); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } Bir thread'in sonlanmasının blokede beklenmesi sıkça gerekebilmektedir. Örneğin ana thread bir thread yaratıp belli bir noktada thread sonlanana kadar beklemek isteyebilir. Windows sistemlerinde thread sonlanana kadar bekleme yapmak için WaitForSingleObject ve WaitForMultipleObjects isimli API fonksiyonları bulunmaktadır. Bu fonksiyonlar yalnızca thread'leri beklemek için değil diğer kernel senkronizasyon nesnelerini de beklemek için kullanılmaktadır. Yani bu fonksiyonlar genel bir bekleme amacıyla tasarlanmıştır. Biz bu fonksiyonların kernel senkronizasyon nesneleriyle nasıl kullanılacağını ilerleyen paragraflarda göreceğiz. Ancak burada yalnızca bu fonksiyonların thread'leri beklemek için nasıl kullanılacağı üzerinde duracağız. >>> WaitForSingleObject fonksiyonun prototipi şöyledir: DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds ); Yukarıda da belirttiğimiz gibi WaitForSingleObject fonksiyonu genel bir fonksiyondur. Bu fonksiyon bir senkronizasyon nesnesi kapalı (nonsignaled) olduğu sürece bekleme yapar. Senktronizasyon nesnesi açık duruma (signaled) geçtiğinde bekleme sonlanır. Her senkronizasyon nesnesinin hangi durumda kapalı hangi durumda açık olduğu ayrıca öğrenilmelidir. İşte thread'ler de bir senkronizasyon nesnesi gibi kullanılabilmektedir. Thread senkronizasyon nesnesi thread devam ettiği sürece kapalı durumdadır. Thread sonlanınca açık duruma geçer. O halde bu fonksiyon thread bitene kadar bekleme sağlamaktadır. Fonksiyonun ikinci parametresi milisaniye cinsinden "zaman aşımı (timeout)" belirtmektedir. Eğer nesne burada belirtilen zaman aşımı dolana kadar açık hale gelmezse en fazla bu zaman aşıma kadar bekleme yapılmaktadır. Bu parametre için INFINITE özel değeri girilirse zaman aşımı kullanılmaz. NEsne açık duruma geçene kadar bekleme yapılır. WaitForSingleObject fonksiyonu başarısızlık durumunda WAIT_FAILED değeri ile geri dönmektedir. Bunun dışında diğer geri dönüş değerleri şunlardan biri olabilir: -> WAIT_OBJECT_0: Nesne açık duruma geçiği için fonksiyon sonlanmıştır. Bu en normal durumdur. -> WAIT_TIMEOUT: Zaman aşımı nedenyiyle fonksiyon sonlanmıştır. -> WAIT_ABONDONED: Mutex'in beklendiği durumda mutex'in sahipliğini alan thread'in sonlanması nedeniyle fonksiyon sonlanmıştır. Bu duruma "abondoned mutex" denilmektedir. WaitForSingleObject fonksyionu çağrıldığında zaten nesne açık durumdaysa (signaled) fonksiyon hiç bekleme yapmaz, WAIT_OBJECT_0 değeri ile hemen geri döner. Bu durumda bir thread sonlanana kadar bekleme yapmak şöyle sağlanabilir: if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED) ExitSys("WaitForSingleObject") Aşağıda bu programa ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void Foo(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); printf("main thread waits at WaitForSingleObject...\n"); if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED) ExitSys("WaitForSingleObject"); CloseHandle(hThread); printf("main thread continues...\n"); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); Sleep(1000); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } >>> WaitForMultipleObjects fonksiyonu birden fazla senkronizasyon nesnesini beklemek için kullanılmaktadır. Örneğin biz 10 thread yaratmış olalım. Bunların hepsi sonlanana kadar bekleme yapmak isteyelim. Bunun bir yolu WaitForSingleObject fonksiyonunu 10 kez çağırmaktır. Diğer bir yolu ise bir kez WaitForMultipleObject fonksiyonunu kullanmaktır. WaitForMultipleObjects fonksiyonunun prototipi şöyledir: DWORD WaitForMultipleObjects( DWORD nCount, const HANDLE *lpHandles, BOOL bWaitAll, DWORD dwMilliseconds ); Fonksiyonun birinci parametresi kaç senkronizasyon nesnesinin bekleneceğini belirtmektedir. (Yani bu parametre ikinci parametredeki dizinin uzunluğunu belirtir.) İkinci parametre beklenecek senkronizasyon nesnelerinin hhandle değerlerinin bulundurğu dizinin adresini almaktadır. Üçüncü parametre tek bir nesnenin mi yoksa bütün nesnelerin mi açık duruma geçince beklemenin sonlandırılacağını belirtir. Eğer bu parametre TRUE geçilirse tüm nesneler açık duruma geçene kadar bekleme yapılır. Eğer bu parametre FALSE geçilirse en az bir nesne açık duruma geçene kadar bekleme yapılır. Son parametre yine zaman aşımı belirtmektedir. Bu parametre INFINITE geçilebilir. WaitForMultiplrObjects fonksiyonu başarısız olursa WAIT_FAILED değerine geri dönmektedir. Eğer fonksiyon zaman aşımı dolayısıyla sonlanmışsa yine WAIT_TIMEOUT değerine geri döner. Eğer fonksiyonun üçüncü parametresi TRUE geçilirse tüm senkronizasyon nesneleri açıldığında fonksiyon WAIT_OBJECT_0 değerinden WAIT_OBJECT_0 + nCunt değerine kadar herhangi bir değerle geri dönebilir. Eğer fonksiyonun üçüncü parametresi FALSE geçilirse fonksiyon hangi senkronizasyon nesnesi açık duruma geçtiyse ona ilişkin WAIT_OBJET_0 + n değerine geri döner. Burada n açık duruma geçen nesnenin dizideki indeksini belirtmektedir. Bu durumda birden fazla nesne açık duruma geçerse fonksiyon en düşük indeksle geri dönmektedir. Aşağıdaki örnekte 10 thread yaratılmış ve 10 thread'in sonlanması WaitForMultipleObjects fonksiyonu ile beklenmiştir. * Örnek 1, #include #include #include #define NTHREADS 10 DWORD __stdcall ThreadProc(LPVOID param); void Foo(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadIds[NTHREADS]; HANDLE hThreads[NTHREADS]; char szName[64]; char *pszName; printf("main thread begins...\n"); for (int i = 0; i < NTHREADS; ++i) { sprintf(szName, "Thread %d", i); if ((pszName = strdup(szName)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if ((hThreads[i] = CreateThread(NULL, 0, ThreadProc, pszName, 0, &dwThreadIds[i])) == NULL) ExitSys("CreateThread"); } printf("main thread waits at WaitForSingleObject...\n"); if (WaitForMultipleObjects(NTHREADS, hThreads, TRUE, INFINITE) == WAIT_FAILED) ExitSys("WaitForMultipkeObjects"); for (int i = 0; i < NTHREADS; ++i) CloseHandle(hThreads[i]); printf("main thread continues...\n"); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { const char *pszName = (const char *)param; for (int i = 0; i < 10; ++i) { printf("%s: %d\n", pszName, i); Sleep(1000); } free(pszName); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } Thread'lerin de exit kodları vardır. Windows'ta thread'lerin exit kodları GetExitCodeThread fonksiyonu ile, UNIX/Linux sistemlerinde sonraki paragrafta göreceğimiz pthread_join fonksiyonu ile elde edilmeketdir. Tabii sonlanmamış bir thread'in exit kodunun elde edilmeye çalışılması anlamsızdır. Therad'lerin exit kodları thread fonksiyonlarının geri dönüş değerleridir. Windows'ta thread'lerin exit kodları DWORD bir değerken UNIX/Linux sistemlerinde void * türünden bir değerdir. Anımsanacağı gibi thread'lerde üstlük-altlık (parent-child) durumu yoktur. Bir thread'in exit kodu herhangi bir thread tarafındna alınabilir. Windows'ta GetExitCodeThread fonksiyonunun protoipi şöyledir: BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode ); Foksiyonun birinci parametresi exit kodu elde edilecek thread'in HANDLE değerini belirtir. İkinci parametre exit kodunun yerleştirileceği DWORD nesnenin adresini almaktadır. Fonksiyon başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri döner. Aşağıdaki örnekte Windows'ta yaratılan bir thread'in exit kodu elde eidlmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc(LPVOID param); void Foo(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwThreadId; HANDLE hThread; DWORD dwExitCode; printf("main thread begins...\n"); if ((hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadId)) == NULL) ExitSys("CreateThread"); printf("main thread waits at WaitForSingleObject...\n"); if (WaitForSingleObject(hThread, INFINITE) == WAIT_FAILED) ExitSys("WaitForSingleObject"); if (!GetExitCodeThread(hThread, &dwExitCode)) ExitSys("GetExitCodeThread"); CloseHandle(hThread); printf("Exit Code: %u\n", dwExitCode); return 0; } DWORD __stdcall ThreadProc(LPVOID param) { for (int i = 0; i < 10; ++i) { printf("Other thread: %d\n", i); Sleep(1000); } return 123; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); 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); } Thread'ler konusunun en önemli alt konusu thread senkronizasyonudur. Bir grup thread birlikte bir işi gerçekleştirirken kimi zaman birbirlerini beklemesi, birbirleriyle koordineli bir biçimde çalışması gerekmektedir. İşte işletim sistemlerinde bunu sağlamaya yönelik mekanizmalara "thread senkronizasyonu" denilmektedir. Thread senkronizasyonunun önemi basit örnekle anlaşılabilir. Thread'lerin aynı global nesneleri kullandığını belirtmiştik. İki thread aynı global değişkeni bir döngü içerisinde bir milyon kere artırıyor olsun. Bu global değişkenin değerinin iki milyon olması beklenir. Ancak senkronizasyon problemi yüzünden muhtemelen iki milyon olamayacaktır. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); int g_count; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); printf("%d\n", g_count); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 1000000; ++i) ++g_count; return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 1000000; ++i) ++g_count; return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Thread senkronizasyon problemleri tipik olarak birden fazla thread'in ortak bir kaynak üzerinde çalıştığı durumda ortaya çıkmaktadır. Thread'lerden biri bu ortak kaynak üzerinde yazma (gemel olarka güncelleme) yaptığı bir durumda bu işlemler sırasında thread'ler arası geçiş oluşursa ve başka bir thread de bu kararsız durumda kalmış kaynağı kullanırsa ya da o da bu kaynağa yazma yaparsa diğer thread kaldığı yerden çalışmasına devam ettiğinde sorun oluşacaktır. Burada kaynak (resource) demekle ortak kullanılan bir nesne kastedilmektedir. Bu nesne global bir değişken olabileceği gibi dış dünyadaki gerçek bir donanımsal aygıt olabilir. Örneğin iki thread dış dünyadaki bir makineyi sırasıyla 1, 2, 3, 4, 5 konumlarına sokarak kullanıyor olsun. Bu kodu aşağıdaki gibi temsil edelim: ... ... Şimdi thread'lerden biri aşağıdaki noktada threadler arası geçiş (context switch) oluşup kesilmiş olsun: ... ----> Bu noktada "context switch" olsun ... Başka bir thread aynı makineyi kullanmak istesin ve başından sonuna kadar kesilmeden makineyi konumlara sokarak kullansın. Makine şimdi beşinci konumdadır. Şimdi daha önce kesilen thread kaldığı noktadn çalışmaya devam etsin. Bu thread makineyi 3'üncü konumda sanmaktadır. Halbuki makine şu anda 5'inci konumdadır. Muhtemelen beklenmedik sorunlar oluşacaktır. Şimdi de yukarıdaki sayaç örneğinin neden düzgün çalışmadığını açıklayalım. Bu örnekte iki thread de ++g_count ile global değişkeni 1 artırmaktadır. Ancak derleyiciler bu ++g_count işlemini tipik olarak üç makine komutuyla yapmaktadır: MOV reg, g_count INC reg MOV g_count, reg Önce g_count CPU yazmacına çekilmiştir. Sonra bu yazmaç değer 1 artırılmıştır. Sonra da artırılmış değer yeniden g_count nesnesine yerleştirilmiştir. Şimdi tam aşağıdaki noktada tesadüfen bir thread'ler arası geçişin oluştuğunu varsayalım: MOV reg, g_count ----> Bu noktada "context switch" olsun INC reg MOV g_count, reg Bu noktada g_cunt değerinin 1250305 olduğunu varsayalım. Şimdi diğer thread kesilmedne bir quanta çalışıp g_count değerini örneğin 1756340'a getirmiş olsun. Önceki thread kalan noktadan çalışmaya devam ettiğinde yazmaçta 1250305 değeri olacaktır. Bunu 1 artırdığında 1250306 elde edilir. Bu değeri yeniden g_count nesnesine yerleştrecek ve g_count'taki değeri bozacaktır. Thread'ler arası geçiş bir makine komutu çalışırken oluşmaz, iki makine komutu arasında oluşabilir. Ancak hangi makine komutunda bu geçişin oluşacağu "quanta" durumuna bağlıdır. Dolayısıyla bir thread herhangi bir makine komutunda kesilebilmektedir. Başından sonuna kadar tek bir thread akışı tarafından çalıştırılması gereken kod bloklarına "kritik kod blokları (critical sections)" denilmektedir. Kritik kod bloklarına bir thread girdiğinde thread'ler arası geçiş (context switch) oluşabilir. Ancak diğer thread'ler bu bloğa girmek istediğinde daha girmiş olan thread'in buradan çıkmasını beklerler. Böylece başından sonuna kadar tek bir threda akışı tarafından kritik kodlar çalıştırılmış olur. Yukarıdaki örneklerimizde g_count nesnesinin artırılması bir kritik koddur. Örneğin: ... MOV reg, g_count INC reg MOV g_count, reg ... Bu üç makine komutu bir kritik kod oluşturmaktadır. Yani thread bu kodları çalıştırırken kesilebilir ancak başka bir thread diğeri çıkana kadar bu kritik koda girmez. Tabii kritik kod oluşturmanın diğer bir yolu geçici süre thread'ler arası geçişi engellemek olabilir. Ancak bu yöntemin user mode'tan uygulanması mümkün değildir. Yukarıdaki makine örneğimizde de makineyi kullanan kod kritik bir koddur: ... ... Bir thread bu kritik koda girdiğinde arada kesilse bile başka thread bu thread kritik koddan çıkana kadar kritik koda girmemelidir. Senkronizasyon bağlamında bir grup makine komutunun sanki tek bir makine komutuymuş gibi kesilmeden çalıştırılmasına "atomiklik (atomicity)" denilmektedir. Yani "bir işlemin atomik bir biçimde yapılması" demek "kesilmeden başka bir deyişle thread'ler arası geçiş oluşmadan" yapılması demektir. Kritik kodların oluşturulabilmesi işletim sistemi tarafından sağlanan özel sistem fonksiyonları ya da bunları kullanan kütüphane fonksiyonlarıyla sağlanabilmektedir. Kritik kodlar manuel biçimde işletim sisteminin desteği olmadan ya da özel birtakım yööntemler kullanılmadan oluşturulamaz. Örneğin aşağıdaki gibi bir kritik kod oluşturma mümkün değildir: int g_flag = 0; ... while (g_flag == 1) ; g_flag = 1; ...... ...... ...... g_falg = 0; Buradaki kodun iki önemli problemi vardır: -> Burada tam while döngüsünden çıkılmışken ancak g_flag = 1 işlemi yapılmadan thread'ler arası geçiş oluşabilir: while (g_flag == 1) ; ----> Dikkat tan bu noktada "context switch" olabilir g_flag = 1; ...... ...... ...... g_falg = 0; Bu durumda g_flag 0 konumunda kalmıştır ancak thread kritik koda girmiştir. Yani bir thread de bu thread de kritik kodda ilerleyebilir. -> Burada bekleme "meşgul bir döngüyle (busy loop)" yapılmaktadır. Yani bekleme yapılırken gereksiz biçimde CPU zamanı harcanmaktadır. İşte bu tür kodlar özel birtakım mekanizmaalr olmadan oluşturulamamaktadır. Kritik kodlar tek bir blok biçiminde olmayabilir. Birden fazla yere yayılmış olarak bulunabilirler. Örneğin global bir bağlı listeye thread'lerden biri ekleme yaparken diğer bir thread ekleme de silme de hatta dolaşma da yapmamı gerekir. Burada bağlı listeye ekleme yapan, bağlı listeden silme yapan ve bağlı listeyi dolaşan kodlar kritik kodlardır. Windows'ta kritik kod oluşturmak için en yalın ve hızlı yöntem CRITICAL_SECTION isimli nesneyi kullanmaktır. Bu nesne ile kritik kodlar şöyle oluşturulmaktadır: -> Önce global bir değişken biçiminde CRITICAL_SECTION türünden bir nesne yaratılır. CRITICAL_SECTION typedef edilmiş bir yapı türünü belirtmektedir. Ancak programcının bu yapının içeriğini bilmesine gerek yoktur. Örneğin: CRITICAL_SECTION g_cs; -> Yaratılan bu nesneye initializeCriticalSection API fonksiyonuyla ilkdeğerleri verilir. Fonksiyonun prototipi şöyledir: void InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); Fonksiyon CRITICAL_SECTION nesneninin adresini parametre olarak almaktadır. Bu ilkdeğer verme işlemi henüz thread'ler yaratılmadan yapılabilir. Örneğin: InitializeCriticalSection(&g_cs); -> Kritik kod EnterCriticalSection ve LeaveCriticalSection API fonksiyonları arasına alınır. Örneğin: EnterCriticalSection(&g_cs); ... ... ... LeaveCriticalSection(&g_cs); Fonksiyonların prototipleri şöyledir: void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); Her iki fonksiyon da CRITICAL_SECTION nesnesinin adresini almaktadır. Bir thread EnterCriticalSection fonksiyonundan girdiğinde LeaveCriticalSection fonksiyonunu çağırana kaadar nesneyi kilitlemiş olur. Böylece başka bir thread EnterCriticalSection fonksiyonundan geçiş yapma istediğinde bloke olur. Ta ki önceki thread LeaveCriticalSection fonksiyonunu çağırana kadar. CRITICAL_SECTION nesnesinin bir kilit gibi davrandığına dikkat ediniz. Bir thread bu fonksiyondan geçtiğinde nesne kilitlenmekte başka thread'ler kritik koda girememektedir. LeaveCriticalSection kritik kodun kilidini açmaktadır. Bir thread EnterCriticalSection fonksiyonundan geçerek nesneyi kilitlemiş olsun. Bu sırada birden fazla thread nesne kilitli olduğu için EnterCriticalSection fonksiyonunda bekliyor olsun. İlk thread kritik koddan çıktığında EnterCriticalSection fonskiyonunda bloke olmuş olan hangi thread kritik koda girecektir? En adil durumun ilk gelen thread'in girmesi olduğunu düşünebilirsiniz. Ancak Windows sistemleri çeşitli nedenlerden dolayı bunun garantisini vermemektedir. -> Kullanım bittikten sonra CRITICAL_SECTION nesnesi DeleteCriticalSection fonksiyonu ile yok edilmeldir. Örneğin: DeleteCriticalSection(&g_cs); Fonksiyonun prototipi şöyledir: VOID DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); Fonksiyon yine CRITICAL_SECTION nesnesinin adresini almaktradır. Kritik kod bloğu birden fazla yere yayılmış olarak bulunabilir. Önemli olan Bu fonksiyonlarda kullanılan nesnedir. Aynı nesne aynı kilidi temsil etmektedir. Örneğin: void insert_item(...) { ... EnterCriticalSection(&g_s); .... .... .... LeaveCriticalSection(&g_s); ... } void delete_item(...) { ... EnterCriticalSection(&g_s); .... .... .... LeaveCriticalSection(&g_s); ... } Burada bir thread eleman insert ederken diğer thread elemanı silemeyecektir, bir thread eleman silerken diğer thread eleman insert edemeyecektir. Çünkü bu iki kritik kod aynı nesneyi yani kilidi kullanmaktadır. Dolayısıyla aslında aynı kritik kodun değişik parçalarıdır. * Örnek 1, Aşağıda daha önce yapmış olduğumuz sayaç artırma örneğini CRITICAL_SECTION nensnesi kullanarak düzeltiyoruz: #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); int g_count; CRITICAL_SECTION g_cs; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; InitializeCriticalSection(&g_cs); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); printf("%d\n", g_count); DeleteCriticalSection(&g_cs); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 1000000; ++i) { EnterCriticalSection(&g_cs); ++g_count; LeaveCriticalSection(&g_cs); } return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 1000000; ++i) { EnterCriticalSection(&g_cs); ++g_count; LeaveCriticalSection(&g_cs); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Aağıdaki örnekte iki thread aynı makineyi sırasıyla 1, 2, 3, 4 ve 5 numaralı konumlara sokmaktadır. Kritik kod bloğu sayesinde thread'lerden biri kritik koda girdiğinde diğeri kiritk koda girmemektedir. #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void UseMachine(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); CRITICAL_SECTION g_cs; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; InitializeCriticalSection(&g_cs); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); DeleteCriticalSection(&g_cs); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 10; ++i) UseMachine("Therad-1"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 10; ++i) UseMachine("Thread-2"); return 0; } void UseMachine(LPCSTR pszName) { EnterCriticalSection(&g_cs); printf("-------------------\n"); printf("%s: 1. Step\n", pszName); Sleep(rand() % 500); printf("%s: 2. Step\n", pszName); Sleep(rand() % 500); printf("%s: 3. Step\n", pszName); Sleep(rand() % 500); printf("%s: 4. Step\n", pszName); Sleep(rand() % 500); printf("%s: 5. Step\n", pszName); Sleep(rand() % 500); LeaveCriticalSection(&g_cs); } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 3, Örneğin iki thread aynı global bağlı listeye ekleme yapacak olsun. Eğer işlemler senkronize edilmezse program çökebilir ya da tanımsız davranışlar oluşabilir. Aşağıda buna bir örnek verilmiştir. Örneği kritk kodları kaldırarak da test ediniz. /* sample.c */ #include #include #include #include "llist.h" DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); CRITICAL_SECTION g_cs; HLLIST g_hllist; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; InitializeCriticalSection(&g_cs); if ((g_hllist = create_llist()) == NULL) { fprintf(stderr, "cannot create linked list!..\n"); exit(EXIT_FAILURE); } if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); DeleteCriticalSection(&g_cs); printf("%zd\n", count_llist(g_hllist)); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { for (int i = 0; i < 1000000; ++i) { EnterCriticalSection(&g_cs); if (add_tail(g_hllist, i) == NULL) { fprintf(stderr, "cannot add item..\n"); exit(EXIT_FAILURE); } LeaveCriticalSection(&g_cs); } return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { for (int i = 0; i < 1000000; ++i) { EnterCriticalSection(&g_cs); if (add_tail(g_hllist, i) == NULL) { fprintf(stderr, "cannot add item..\n"); exit(EXIT_FAILURE); } LeaveCriticalSection(&g_cs); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* llist.h */ #ifndef LLIST_H_ #define LLIST_H_ #include #include /* Type Declarations */ typedef int DATATYPE; typedef struct tagNODE { DATATYPE val; struct tagNODE *next; } NODE; typedef struct tagLLIST { NODE head; NODE *tail; size_t count; } LLIST, *HLLIST; /* Function Prototypes */ HLLIST create_llist(void); NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val); NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val); NODE *add_tail(HLLIST hllist, DATATYPE val); NODE *addp_tail(HLLIST hllist, const DATATYPE *val); NODE *add_head(HLLIST hllist, DATATYPE val); NODE *addp_head(HLLIST hllist, const DATATYPE *val); void remove_next(HLLIST hllist, NODE *node); void remove_head(HLLIST hllist); DATATYPE *getp_item(HLLIST hllist, size_t index); bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)); void clear_llist(HLLIST hllist); void destroy_llist(HLLIST hllist); /* inline Function Definitions */ static inline size_t count_llist(HLLIST hllist) { return hllist->count; } #endif /* llist.c */ #include #include #include "llist.h" /* static Functions Prototypes */ static bool disp(DATATYPE *val); /* Function Definitions */ HLLIST create_llist(void) { HLLIST hllist; if ((hllist = (HLLIST)malloc(sizeof(LLIST))) == NULL) return NULL; hllist->head.next = &hllist->head; hllist->tail = &hllist->head; hllist->count = 0; return hllist; } NODE *insert_next(HLLIST hllist, NODE *node, DATATYPE val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *insertp_next(HLLIST hllist, NODE *node, const DATATYPE *val) { NODE *new_node; if ((new_node = (NODE *)malloc(sizeof(NODE))) == NULL) return NULL; new_node->val = *val; if (node == hllist->tail) hllist->tail = new_node; new_node->next = node->next; node->next = new_node; ++hllist->count; return new_node; } NODE *add_tail(HLLIST hllist, DATATYPE val) { return insert_next(hllist, hllist->tail, val); } NODE *addp_tail(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, hllist->tail, val); } NODE *add_head(HLLIST hllist, DATATYPE val) { return insert_next(hllist, &hllist->head, val); } NODE *addp_head(HLLIST hllist, const DATATYPE *val) { return insertp_next(hllist, &hllist->head, val); } void remove_next(HLLIST hllist, NODE *node) { NODE *next_node; if (node == hllist->tail) return; if (node->next == hllist->tail) hllist->tail = node; next_node = node->next; node->next = next_node->next; --hllist->count; free(next_node); } void remove_head(HLLIST hllist) { remove_next(hllist, &hllist->head); } DATATYPE *getp_item(HLLIST hllist, size_t index) { NODE *node; if (index >= hllist->count) return NULL; node = hllist->head.next; for (size_t i = 0; i < index; ++i) node = node->next; return &node->val; } bool walk_llist(HLLIST hllist, bool (*proc)(DATATYPE *)) { bool retval = true; bool def_flag = false; if (proc == NULL) { proc = disp; def_flag = true; } for (NODE *node = hllist->head.next; node != &hllist->head; node = node->next) if (!proc(&node->val)) { retval = false; break; } if (def_flag) putchar('\n'); return retval; } void clear_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } hllist->head.next = &hllist->head; hllist->tail = &hllist->head; hllist->count = 0; } void destroy_llist(HLLIST hllist) { NODE *node, *temp_node; node = hllist->head.next; while (node != &hllist->head) { temp_node = node->next; free(node); node = temp_node; } free(hllist); } static bool disp(DATATYPE *val) { printf("%d ", *val); fflush(stdout); return true; } * Örnek 4, Aşağıda C++'ta birden fazla thread'in "vector" isimli dinamik diziye ekleme yapmasına ilişkin benzer bir örnek verilmiştir. Bu örneği de krtik kodları kaldırarak ve muhafaza ederek ayrı ayrı test ediniz. #include #include #include #include #include void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); CRITICAL_SECTION g_cs; std::vector g_v; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; int i; srand(time(NULL)); InitializeCriticalSection(&g_cs); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); DeleteCriticalSection(&g_cs); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 1000; ++i) { EnterCriticalSection(&g_cs); g_v.push_back(i); LeaveCriticalSection(&g_cs); } return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 1000; ++i) { EnterCriticalSection(&g_cs); g_v.push_back(i); LeaveCriticalSection(&g_cs); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Kritik kod oluşturmak için kullanılan diğer bir senkronizasyon mekanizması da "mutex (mutual exclusion)" denilen mekanizmadır. Mutex nesneleri pek çok işletim sisteminde benzer bir biçimde bulunmaktadır. Örneğin CRITICAL_SECTION nesnesi Windows sistemlerine özgü olduğu halde mutex nesneleri hem Windows, hem UNIX/Linux hem de macOS sistemlerinde benzer biçimde bulunmaktadır. Mutex nesnelerinin thread temelinde bir "sahipliği (ownership)" vardır. Bir mutex nesnesinin sahipliğini bir thread almış ise o thread o mutex nesnesini kilitlemiştir. Başka bir thread mutex nesnesinin sahipliğine almaya çalışırsa diğer thread sahipliği bırakana kadar blokede bekler. Mutex nesnesinin sahipliğini nesnenin sahipliğin almış olan thread bırakabilmektedir. Eğer bir thread bir mutex nesnesinin sahipliğini almışken onu bırakmadan sonlanırsa böyle mutex nesnelerine "terkedilmiş mutex nesneleri (abondened mutexes)" denilmektedir. Windows'ta mutex nesneleri hem aynı prosesin thread'leri arasında hem de farklı proseslerin thread'leri arasında senkronizasyon amacıyla kullanılabilmektedir. Windows sistemlerinde aynı prosesin thread'leri arasında kritik kod oluşturmak için CRITICAL_SECTION nesneleri mutex nesnelerinden daha hızlı çalışmaktadır. Windows'ta mutex nesneleri şöyle kullanılmaktadır: -> Önce mutex nesnesi CreateMutex isimli API fonksiyonuyla yaratılır. Tabii bu yaratım henüz thread'ler yaratılmadan önce yapılabilir. CreateMutex fonksiyonun prototipi şöyledir: HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName ); Fonksiyonun birinci parametresi mutex nesnesinin güvenlik bilgilerini belirlemek için kullanılmaktadır. Bu parametre NULL geçilebilir. (Windows'ta mutex nesneleri birer kernel nesnesidir. Tüm kernel nesnelerinin SECURITY_ATTRIBUTES türünden bir güvenlik parametresi vardır. Bu konu oldukça karmaşık bir konu olduğu için "Windows Sistem Programlama" kursunda ele alınmaktadır.) Fonksiyonun ikinci parametresi TRUE geçilirse mutex nesnesini yaratan thread aynı zamanda sahipliğinde alır (yani aynı zamanda mutex nesnesini kilitler.) Bu parametre tipik olarak FALSE biçiminde geçilmektedir. Fonksiyonun son parametresi mutex nesnesi farklı prosesler tarafından kullanılacaksa nesneyi temsil eden ismi belirtmektedir. Bu isim programcı tarafından herhangi bir biçimde verilebilir. Eğer aynı prosesin thread'leri arasında senkronizasyon yapılacaksa bu parametre NULL geçilmelidir. Fonksiyon başarı durumunda yaratılan mutex nesnesinin handle değerine başarısızlık durumunda NULL adrese geri dönmektedir. Eğer aynı prosesin thread'leri arasında senkronizasyon yapılacaksa bu durumda CreateMutex fonksiyonundan elde edilen handle değeri global bir değişkene atanmalıdır. Böylece bu global değişken farklı thread'lerden kullanılabilecektir. Örneğin: HANDLE g_hMutex; ... if ((g_hMutex = CreateMutex(NULL, FALSE, NULL)) == NULL) ExitSys("CreateMutex"); -> Kritik kod aşağıdaki gibi WaitForSingleObject (ya da WaitForMultipleObjects) ve ReleseMutex API fonksiyonları arasına yerleştirilmektedir. WaitForSingleObject(g_hMutex, INFINITE); ... ... KRİTİK KOD ... ReleaseMutex(g_hMutex); WaitForSingleObject fonksiyonunu daha önce görmüştük. Bu fonksiyon (ve WaitForMultipleObjects fonksiyonu) senkronizasyon nesnelerini beklemek için kullanılan genel bir fonksiyondu. Eğer WaitForSingleObejct fonksiyonu ile bir mutex nesnesi bekleniyorsa fonksiyon nesnenin sahipliğini başka bir thread almamışsa nesnenin sahipliğini alarak kritik koda girişi sağlar. Eğer nesnenin sahipliği başka bir thread tarafından alınmışsa WaitForSingleObject fonksiyonu nesnenin sahipliğini almış olan thread bu sahipliği bırakana kadar blokede beklemektedir. Böylece aynı anda tek bir thread'in kritik koda girişine izin verilmektedir. Eğer nesnenin sahipliğini almış olan thread sahipliğini bırakmadan sonlanırsa bu durumda WaitForSingleObject fonksiyonu "kilitlenme (deadlock)" oluşmasını engellemek için WAIT_ABANDONED özel değeri ile geri dönmektedir. ReleaseMutex fonksiyonu mutex nesnesinin sahipliğini bırakmak için kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL ReleaseMutex( HANDLE hMutex ); Fonksiyon mutex nesnesinin handle değerini parametre olarak alır ve nesnenin sahipliğini bırakır. Başarı durumunda sıfır dışı bir değere, başarısızlık durumunda sıfır değerine geri dönmektedir. -> Mutex nesnesinin kullanımı bittikten sonra nesne CloseHandle fonksiyonu ile yok edilebilir. (Anımsanacağı gibi Windows'te ismine "kernel nesneleri (kernel objects)" denilen tüm nesneler ortak biçimde CloseHandle fonksiyonuyla kapatılmaktadır). Aşağıda Windows'ta mutex nesneleri ile kritik kod oluşturulmasına bir örnek verilmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void UseMachine(LPCSTR pszName); void ExitSys(LPCSTR lpszMsg); HANDLE g_hMutex; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_hMutex = CreateMutex(NULL, FALSE, NULL)) == NULL) ExitSys("CreateMutex"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); CloseHandle(g_hMutex); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 10; ++i) UseMachine("Therad-1"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 10; ++i) UseMachine("Thread-2"); return 0; } void UseMachine(LPCSTR pszName) { WaitForSingleObject(g_hMutex, INFINITE); printf("-------------------\n"); printf("%s: 1. Step\n", pszName); Sleep(rand() % 500); printf("%s: 2. Step\n", pszName); Sleep(rand() % 500); printf("%s: 3. Step\n", pszName); Sleep(rand() % 500); printf("%s: 4. Step\n", pszName); Sleep(rand() % 500); printf("%s: 5. Step\n", pszName); Sleep(rand() % 500); ReleaseMutex(g_hMutex); } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Windows'ta mutex nesneleri farklı proseslerin thread'lerini senkronize etmek için de kullanılabilmektedir. Bunun için CreateMutex fonksiyonunun üçüncü (son) parametresine mutex nesnesi için bir isim girilir. İki farklı proses CreateMutex fonksiyonunda mutex nesnlerine aynı isimleri verirse bu durumda iki farklı mutex yaratılmamakta aynı mutex nesnesi üzerinde çalışılmaktadır. Başka bir deyişle CreateMutex fonksiyonu son parametresiyle belirtilen isimde daha önce bir mutex nesnesi yaratılmışsa yeni bir mutex nesnesi yaratmaz. Zaten yaratılmış olan mutex nesnesi açmış olur. Böylece iki proses de aynı isimle CreateMutex fonksiyonunu çağırdığında yalnızca bunlardan biri mutex nesnesini yaratacak diğer yaratılmış olan nesneyi açacaktır. Tabii mutex nesnesine verilen isme dikkat edilmelidir. Eğer sistemde o isimli bir mutex nesnesi zaten başkaları tarafından yaratılmışsa böyle bir yaratım yapılmayacaktır. Pekiyi farklı proseslerin thread'lerinin senkronize edilmesi neden gerekebilir? İşte prosesler kendi aralarında "paylaşılan bellek alanlarıyla" ortak veriler üzerinde işlem yapıyor olabilirler. Bu durumda bu paylaşılan bellek alanındaki verilerin prosesler arası çalışan senkronizasyon nesneleriyle senkronize edilmesi gerekebilmektedir. Windows sistemlerinde mutex nesneleri "özyinelemeli (recursive)" davranışa sahiptir. Bir mutex nesnesinin özyinelemeli olması demek mutex nesnesinin thread tarafından sahipliği alındığında aynı thread'in yeniden aynı mutex nesnesinin sahipliğini bloke olmadan alabilmesi demektir. Örneğin: void foo(void) { ... WaitForSingleObject(g_hMutex, INFINITE); ... bar(); ... ReleaseMutex(g_hMutex); ... } void bar(void) { ... WaitForSingleObject(g_hMutex, INFINITE); ... ... ... ReleaseMutex(g_hMutex); ... } Burada programcının foo fonksiyonunu çağırdığını varsayalım. foo fonksiyonu Mutex'in sahipliğini aldıktan sonra bar fonksiyonunu çağırdığında aynı thread aynı mutex'in sahipliğini ikinci kez almaktadır. İşte Windows'ta bu durumda bir sorun oluşmamaktadır. Ancak thread mutex'in sahipliğini ne kadar almışsa ReleaseMutex ile o kadar bırakmalıdır. Aşağıdaki örnekte iki proses paylaşılan bellek alanı oluşturup oradaki bir sayacı belli miktar artırmaktadır. Senkronizasyon yapılmaığı durumda bu sayaç değeri yanlış çıkacaktır. Senkronizasyon yapıldığında sayacın artırılması seri hale getirildiği için sorun oluşmayacaktır. * Örnek 1, /* prog1.c */ #include #include #include #define SHARED_MEMORY_NAME "MySharedMemory" #define MUTEX_NAME "MyMutexObject" void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFileMapping; HANDLE hMutex; long long *pCount; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, SHARED_MEMORY_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((pCount = (long long *) MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 4096)) == NULL) ExitSys("MapViewOfFile"); if ((hMutex = CreateMutex(NULL, FALSE, MUTEX_NAME)) == NULL) ExitSys("CreateMutex"); for (long long i = 0; i < 10000000; ++i) { WaitForSingleObject(hMutex, INFINITE); ++*pCount; ReleaseMutex(hMutex); } printf("Press ENTER to continue...\n"); getchar(); printf("%lld\n", *pCount); UnmapViewOfFile(pCount); CloseHandle(hFileMapping); getchar(); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* prog2.c */ #include #include #include #define SHARED_MEMORY_NAME "MySharedMemory" #define MUTEX_NAME "MyMutexObject" void ExitSys(LPCSTR lpszMsg); int main(void) { HANDLE hFileMapping; HANDLE hMutex; long long *pCount; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, SHARED_MEMORY_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((pCount = (long long *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 4096)) == NULL) ExitSys("MapViewOfFile"); if ((hMutex = CreateMutex(NULL, FALSE, MUTEX_NAME)) == NULL) ExitSys("CreateMutex"); for (long long i = 0; i < 10000000; ++i) { WaitForSingleObject(hMutex, INFINITE); ++*pCount; ReleaseMutex(hMutex); } UnmapViewOfFile(pCount); CloseHandle(hFileMapping); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Diğer çok kullanılan bir senkronizasyon nesneleri de "semaphore" denilen nesnelerdir. (Semaphore trafikteki dur-geç lambalarına denilmektedir. eskiden trafik ışıkları yokken onun yerine insanların manuel idare ettiği bayraklar kullanılıyormuş.) Semapore'lar sayaçlı senkronizasyon nesneleridir. Bir kritk koda en fazla N tane thread'in (akışın) girmesini sağlamak için kullanılmaktadır. Daha önce görmüş olduğumuz mutex nesneleri ve Windows'taki CRITCAL_SECTION nesneleri kritik koda yalnızca tek bir akışın girmesini sağlamaktadır. Semaphore'lar bilgisayar bilimlerinin öncülerinden Edsger Dijkstra ("edsger daystra" gibi okunuyor) tarafından bulunmuştur. Bir kritik koda birden fazla akışın girmesinin anlamı nedir? Normalde kritik koda giren iki akış bile ortak kullanılan kaynakları bozabilir. İşte semaphore'lar özellikle "kaynak paylaşımını sağlamak" için kullanılmaktadır. Örneğin elimizde 3 makine olsun. Biz de bu 3 makineyi 10 thread'e paylaştırmak isteyelim. Yani thread'ler makineyi talep etsin, eğer bir makine boştaysa o makine ilgili thread'e atansın. Ancak tüm makineler doluysa makine talep eden thread makinelerden biri boşaltılana kadar CPU zamanı harcamadan blokede beklesin. İşte bu tür problemler tipik olarak semaphore nesneleriyle çözülmektedir. Semaphore nesnelerinin bir sayacı vardır. Kritik koda her giren thread eğer sayaç 0'dan büyükse sayacı 1 eksiltir. Eğer sayaç 0 ise blokede sayacın 0'dan büyük olmasını bekler. Örneğin başlangıçtaki semaphore sayacı 3 olsun. Bir thread kritik koda girdiğinde semaphore sayacı 2 olacaktır. Diğer bir thread kritik koda girdiğinde semaphore sayacı 1 olacaktır. Diğer bir thread kritik koda girdiğinde ise semaphore sayacı 0 olacaktır. Artık gelen thread'ler kritik koda giremeyip blokede bekleyecektir. Artık kritik kodun içerisinde 3 tane thread vardır. Şimdi bir thread kritik koddan çıkıyor olsun. Thread kritik koddan çıkarken semaphore sayacı 1 artırılır. Böylece semaphore sayacı 1 olur. Kritik kodda şu anda 2 thread vardır. İşte semaphore sayacı artık 0'dan büyük olduğu için bekleyen thread'lerden biri de kritik koda girgirer. Böylece semaphore sayacı yeniden 0 olur. Şimdi kritik kod içerisinde yine 3 thread vardır. Başlangıçtaki semaphore sayacı kaç ise kritik kodun içerisinde "en fazla" o kadar thread bulunacaktır. Başlangıçtaki semaphore sayacı 1 ise kritik koda en fazla 1 thread girebilir. Bu tür semaphore'lara "ikili semaphore'lar (binary semaphores)" denilmektedir. İkili semaphore'lar mutex nesnelerine benzese de onlardan önemli bir farklılığa sahiptir. Mutex nesnelerinin sahipliğini (yani kilidini) ancak sahipliğini almış olan thread bırakabilir. Ancak semaphore sayaçları başka thread'ler tarafından artırılabilmektedir. Windows sistemlerinde semaphore nesneleri şu adımlardan geçilerek kullanılmaktadır: -> Önce semaphore nesnesi CreateSemaphore API fonksiyonuyla yaratılır. CreateSemaphore API fonksiyonunun prototipi şöyledir: HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCSTR lpName ); Fonksiyonun birinci parametresi semaphore nesnesinin güvenlik bilgilerini belirtir. Bu parametre NULL adres geçilebilir. İkinci parametre semaphore sayacının başlangıçtaki sayaç değerini belirtir. Üçüncü parametre semaphore sayacının erişebileceği maksimum sayaç değeridir. Genellikle ikinci ve üçüncü parametreye aynı değer girilmektedir. Son parametre semaphore nesnesinin proseslerarası kullanımdaki ismini belirtmektedir. Eğer semaphore nesnesi aynı prosesin thread'leri arasında kullanılacaksa bu parametreye NULL adres geçilebilir. Fonksiyon başarı durumunda semaphore nesnesinin handle değerine başarısızlık durumunda NULL adrese geri dönmektedir. Semaphore nesnesi aynı prosesin thread'leri arasında kullanılacaksa CreateSemaphore fonksiyonunun geri dönüş değeri (yani nesnenin handle değeri) global bir değişkende tutulmalıdır. Örneğin: HANDLE g_hSemaphore; ... if ((g_hSemaphore = CreateSemaphore(NULL, 1, 1, NULL)) == NULL) ExitSys("CreateSemaphore"); -> Kritik kod aşağıdaki gibi oluşturulmaktadır: WaitForSingleObject(g_hSemaphore, INFINITE); ... ... KRİTİK KOD ... ReleaseSemaphore(g_hSemahore, 1, NULL); Burada akış WaitForSingleObject fonksiyonuna geldiğinde eğer semaphore sayacı 0'dan büyükse bloke olunmadan kritik koda girilir. Ancak semaphore sayacı atomik bir biçimde 1 eksiltilir. Eğer semaphore sayacı 0 ise WaitForSingleObject bloke oluşturarak thread'i bekletir. ReleaseSemaphore fonksiyonu semaphore sayacını ikinci parametresinde belirtilen miktar kadar (genellikle 1) artırmaktadır. ReleaseSemaphore fonksiyonun prototipi şöyledir: BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount ); Fonksiyonun birinci parametresi semaphore nesnesinin handle değerini almaktadır. İkinci parametre artırım değerini belirtmektedir. (Bu değer hemen her zaman 1 olur) son parametre semaphore sayacının önceki değerinin yerleştirileceği nesnenin adresini almaktadır. Bu parametre NULL adres biçiminde geçilirse önceki sayaç değeri yerleştirilmez. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda sıfır dışı bir değere geri dönmektedir. Örneğin: for (int i = 0; i < 1000000; ++i) { WaitForSingleObject(g_hSemaphore, INFINITE); ... ... ... ReleaseSemaphore(g_hSemaphore, 1, NULL); } -> Semaphore kullanımı bittiğinde semaphore nesnesi diğer kernel nesnelerinde oluduğu gibi CloseHandle fonksiyonu ile yok edilmelidir. Örneğin: CloseHandle(g_hSemaphore); Aşağıdaki daha önce yaptığımız mutex örneği semaphore nesneleriyle gerçekleştirilmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); HANDLE g_hSemaphore; int g_count; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_hSemaphore = CreateSemaphore(NULL, 1, 1, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(g_hSemaphore); CloseHandle(hThread1); CloseHandle(hThread2); printf("%d\n", g_count); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { for (int i = 0; i < 1000000; ++i) { WaitForSingleObject(g_hSemaphore, INFINITE); ++g_count; ReleaseSemaphore(g_hSemaphore, 1, NULL); } return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { for (int i = 0; i < 1000000; ++i) { WaitForSingleObject(g_hSemaphore, INFINITE); ++g_count; ReleaseSemaphore(g_hSemaphore, 1, NULL); } return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Üretici-Tüketici Problemi (Producer-Consumer Problem) gerçek hayatta en sık karşılaşılan senkronizasyon problemlerinden biridir. Bu problemde thread'lerden biri (üretici thread) bir döngü içerisinde bir değer üretir ve onu paylaşılan bellek alanına yerleştirir. Diğer thread de (tüketici thread) bir döngü içerisinde onu oradan alıp kullanır. Burada şöyle bir sorun vardır: Üretici thread henüz tüketici thread eski değeri almdan paylaşılan alana yeni değeri yerleştirirse eski değer ezilir. Benzer biçimde tüketici thread de üretici thread paylaşılan alana yeni bir değer koymadan paylaşılan alandan değeri alırsa eski değeri yeniden almış olur. O halde iki thread'in bu işi düzgün yapabilmesi için uygun biçimde senkronize edilmesi gerekmektedir. Öyle ki üretici thread tüketici thread eski değeri almadan paylaşılan alana yeni değeri yerleştirmemeli, tüketici thread de üretici thread paylaşılan alana yeni bir değer yerleştirmeden eski değeri yeniden almamalıdır. Üretici-Tüketici probleminde aslında üretici thread'ler ve tüketici thread'ler birden fazla olabilmektedir. Örneğin üç thread üretici iki thread tüketici olabilir. Ancak problemin en basit halinde tek üretici ve tek tüketici vardır. Pekiyi üretici-tüketici probleminde neden üretici thread elde ettiği değeri kendisi işlemiyor da onu tüketici thread'e pas edip onun işlemesini sağlıyor? İşte bunun en açık nedeni hız kazancı sağlamaktır. Tek bir thread bu işi yaptığında önce değeri elde edip işleyecek ve sonra yeni değeri elde edip işleyecektir. Ancak thread'lerden biri değeri elde ederken diğeri onu işlerse işlemler toplamda daha hızlı yürütülmüş olur. Üretici-Tüketici probleminde paylaşılan alan tek bir değeri içerecek biçimde olmayabilir. Bu alan bir kuyruk sistemi biçiminde de olabilir. Böylece üretici ve tüketici birbirlerini daha az bekler. Uygulamada genellikle paylaşılan alan bir kuyruk sistemi biçiminde olarak organize edilmektedir. * Örnek 1, Windows sistemlerinde üretici-tüketici probleminin semaphore nesneleriyle çözümüne ilişkin bir örnek aşağıda verilmiştir. Bu örnek aşağıda verilen UNIX/Linux örneğinin Windows eşdeğeri gibidir. #include #include #include #include void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProducer(LPVOID lpvParam); DWORD __stdcall ThreadConsumer(LPVOID lpvParam); HANDLE g_hSemProducer; HANDLE g_hSemConsumer; int g_shared; int main(void) { HANDLE hThreadProducer, hThreadConsumer; DWORD dwThreadIDProducer, dwThreadIDConsumer; srand(time(NULL)); if ((g_hSemProducer = CreateSemaphore(NULL, 1, 1, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((g_hSemConsumer = CreateSemaphore(NULL, 0, 1, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((hThreadProducer = CreateThread(NULL, 0, ThreadProducer, NULL, 0, &dwThreadIDProducer)) == NULL) ExitSys("CreateThread"); if ((hThreadConsumer = CreateThread(NULL, 0, ThreadConsumer, NULL, 0, &dwThreadIDConsumer)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThreadProducer, INFINITE); WaitForSingleObject(hThreadConsumer, INFINITE); CloseHandle(hThreadProducer); CloseHandle(hThreadConsumer); CloseHandle(g_hSemProducer); CloseHandle(g_hSemConsumer); return 0; } DWORD __stdcall ThreadProducer(LPVOID lpvParam) { int val; val = 0; for (;;) { Sleep(rand() % 300); WaitForSingleObject(g_hSemProducer, INFINITE); g_shared = val; ReleaseSemaphore(g_hSemConsumer, 1, NULL); if (val == 99) break; ++val; } return 0; } DWORD __stdcall ThreadConsumer(LPVOID lpvParam) { int val; for (;;) { WaitForSingleObject(g_hSemConsumer, INFINITE); val = g_shared; ReleaseSemaphore(g_hSemProducer, 1, NULL); printf("%d ", val); fflush(stdout); if (val == 99) break; Sleep(rand() % 300); } putchar('\n'); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Yukarıda da belirttiğmiz gibi üretici-tüketici probleminde paylaşılan alan bir kuyruk sistemi olursa üretici ve tüketicinin birbirlerini bekleme olasılıkları azaltılmış olur. Çünkü bu durumda üretici yalnızca "kuyruk tamamen doluyken", tüketici ise yalnızca "kuyruk tamamen boşken" bekleyecektir. Üretici-Tüketici probleminin kuyruklu versiyonunda üretici semaphore sayacının başlangıçta kuyruk uzunluğuna kurulması gerekmektedir. (Yani tüketici hiç çalışmasa üretici tüm kuyruğu doldurup bekleyecektir.) #include #include #include #include #define QUEUE_BUFFER_SIZE 10 void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProducer(LPVOID lpvParam); DWORD __stdcall ThreadConsumer(LPVOID lpvParam); HANDLE g_hSemProducer; HANDLE g_hSemConsumer; int g_queueBuf[QUEUE_BUFFER_SIZE]; size_t g_head; size_t g_tail; int main(void) { HANDLE hThreadProducer, hThreadConsumer; DWORD dwThreadIDProducer, dwThreadIDConsumer; srand(time(NULL)); if ((g_hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((g_hSemConsumer = CreateSemaphore(NULL, 0, QUEUE_BUFFER_SIZE, NULL)) == NULL) ExitSys("CreateSemaphore"); if ((hThreadProducer = CreateThread(NULL, 0, ThreadProducer, NULL, 0, &dwThreadIDProducer)) == NULL) ExitSys("CreateThread"); if ((hThreadConsumer = CreateThread(NULL, 0, ThreadConsumer, NULL, 0, &dwThreadIDConsumer)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThreadProducer, INFINITE); WaitForSingleObject(hThreadConsumer, INFINITE); CloseHandle(hThreadProducer); CloseHandle(hThreadConsumer); CloseHandle(g_hSemProducer); CloseHandle(g_hSemConsumer); return 0; } DWORD __stdcall ThreadProducer(LPVOID lpvParam) { int val; val = 0; for (;;) { Sleep(rand() % 300); WaitForSingleObject(g_hSemProducer, INFINITE); g_queueBuf[g_tail++] = val; g_tail = g_tail % QUEUE_BUFFER_SIZE; ReleaseSemaphore(g_hSemConsumer, 1, NULL); if (val == 99) break; ++val; } return 0; } DWORD __stdcall ThreadConsumer(LPVOID lpvParam) { int val; for (;;) { WaitForSingleObject(g_hSemConsumer, INFINITE); val = g_queueBuf[g_head++]; g_head = g_head % QUEUE_BUFFER_SIZE; ReleaseSemaphore(g_hSemProducer, 1, NULL); printf("%d ", val); fflush(stdout); if (val == 99) break; Sleep(rand() % 300); } putchar('\n'); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 3, Windows sistemlerinde de proseslerarası üretici-tüketici problemi benzer biçimde gerçekleştirilmektedir. Aşağıda buna ilişkin bir örnek verilmiştir. Bu örnekte de yine "producer" ve "consumer" isimli iki program vardır. Bu örneği daha da aşağıda verdiğimiz UNIX/Linux sistemlerindeki örneğin Windows karşılığı olarak düşünebilirsiniz. /* producer.c */ #include #include #include #define FILE_MAPPING_NAME "ProducerConsumerSharedMemoryName" #define PRODUCER_SEMAPHORE_NAME "ProducerSemaphoreName" #define CONSUMER_SEMAPHORE_NAME "ConsumerSemaphoreName" #define QUEUE_BUFFER_SIZE 10 void ExitSys(LPCSTR lpszMsg); struct SHARED_OBJECT { int qbuf[QUEUE_BUFFER_SIZE]; size_t head; size_t tail; }; int main(void) { HANDLE hFileMapping; HANDLE hSemProducer; HANDLE hSemConsumer; int val; struct SHARED_OBJECT *sharedObject; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((sharedObject = (struct SHARED_OBJECT *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); if ((hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, PRODUCER_SEMAPHORE_NAME)) == NULL) ExitSys("CreateSemaphore"); if ((hSemConsumer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, CONSUMER_SEMAPHORE_NAME)) == NULL) ExitSys("CreateSemaphore"); val = 0; for (;;) { Sleep(rand() % 300); WaitForSingleObject(hSemProducer, INFINITE); sharedObject->qbuf[sharedObject->tail++] = val; ReleaseSemaphore(hSemConsumer, 1, NULL); sharedObject->tail = sharedObject->tail % QUEUE_BUFFER_SIZE; if (val == 99) break; ++val; } CloseHandle(hSemProducer); CloseHandle(hSemConsumer); UnmapViewOfFile(sharedObject); CloseHandle(hFileMapping); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* consumer.c */ #include #include #include #define FILE_MAPPING_NAME "ProducerConsumerSharedMemoryName" #define PRODUCER_SEMAPHORE_NAME "ProducerSemaphoreName" #define CONSUMER_SEMAPHORE_NAME "ConsumerSemaphoreName" #define QUEUE_BUFFER_SIZE 10 void ExitSys(LPCSTR lpszMsg); struct SHARED_OBJECT { int qbuf[QUEUE_BUFFER_SIZE]; size_t head; size_t tail; }; int main(void) { HANDLE hFileMapping; HANDLE hSemProducer; HANDLE hSemConsumer; struct SHARED_OBJECT *sharedObject; int val; if ((hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, FILE_MAPPING_NAME)) == NULL) ExitSys("CreateFileMapping"); if ((sharedObject = (struct SHARED_OBJECT *)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0)) == NULL) ExitSys("MapViewOfFile"); if ((hSemProducer = CreateSemaphore(NULL, QUEUE_BUFFER_SIZE, QUEUE_BUFFER_SIZE, PRODUCER_SEMAPHORE_NAME)) == NULL) ExitSys("CreateSemaphore"); if ((hSemConsumer = CreateSemaphore(NULL, 0, QUEUE_BUFFER_SIZE, CONSUMER_SEMAPHORE_NAME)) == NULL) ExitSys("CreateSemaphore"); for (;;) { WaitForSingleObject(hSemConsumer, INFINITE); val = sharedObject->qbuf[sharedObject->head++]; ReleaseSemaphore(hSemProducer, 1, NULL); sharedObject->head = sharedObject->head % QUEUE_BUFFER_SIZE; printf("%d ", val); fflush(stdout); if (val == 99) break; Sleep(rand() % 300); } putchar('\n'); CloseHandle(hSemProducer); CloseHandle(hSemConsumer); UnmapViewOfFile(sharedObject); CloseHandle(hFileMapping); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastError = GetLastError(); LPSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Windows sistemlerinde "event senkronizasyon nesnesi" denilen önemli bir senkronizasyon nesnesi daha vardır. Bu senkronizasyon nesnesinin UNIX/Linux sistemlerinde tam bir karşılığı yoktur. Ancak UNIX/Linux sistemlerindeki "koşul değişkenleri (conditioned variable)" işlev olarak Windows'taki event senkronizasyon nesnelerinin görevini de yapabilmektedir. Windows'taki event senkronizasyon nesneleri belli bir thread akışının başka bir thread bir işlemi bitirene kadar bir noktada bekletilmesi amacıyla kullanılmaktadır. Örneğin bir thread global bir diziyi sıraya dizecek olsun. Ancak bu dizi başka bir thread tarafından oluşturulacak olsun. Thread'ler asenkron çalıştığına göre sıraya dizme işlemini yapacak thread sıraya dizmenin yapıldığı noktaya geldiğinide diğer thread'in diziyi oluşturmuş olması gerekmektedir. Eğer diğer thread diziyi oluşturmamışsa sıraya dizmeyi yapacak thread o noktada beklemeli diğer thread diziyi oluşturduktan sonra bu işe başlamalıdır. Thread'in bir noktada bekletilmesi semaphore nesneleriyle de kısmen yapılabilir. Örneğin sayacı 0 olan bir semaphore blokeye yol açacağı için thread'i bekletebilir. Diğer thread de semaphore sayacını artırarak onu oradan kurtarabilir. Ancak semaphore'lar event nesnelerinin sağladığı bazı durumları sağlayamamaktadır. Örneğin diğer thread bekleyen thread'in çalışmasına devam etmesini istediğinde artık aynı senkronizasyon nesnesinde thread'in beklememesi gerekebilir. Semaphore'lar bunu doğrudan sağlaymamaktadır. Ya da örneğin diğerini beklemekten kurtaran thread bunu birden fazla kez yaptığında semaphore'lar sayaçlı olduğu için diğer thread'in bekletilmesini sağlayamayabilirler. Windows sistemlerindeki event senkronizasyon nesnesi aşağıdaki adımlardan geçilerek kullanılmaktadır: -> Event senkronizasyon nesnesi CreateEvent API fonksiyonuyla yaratılır. Fonksiyonun prototipi şöyledir: HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCSTR lpName ); Fonksiyonun birinci parametresi kernel nesnesinin güvenlik bilgilerini belirtmektedir. Bu parametre NULL geçilebilir. Fonksiyonun ikinci parametresi event nesnesinin "manuel mi otomatik mi" olduğunu belirtmektedir. Eğer bu parametreye TRUE girilirse nesne manuel olur, FALSE girilirse otomatik olur. Event nesnesi manuel ise SetEvent yapıldığında nesne açık (signaled) durumda kalır. Nesne otomatik ise SetEvent yapıldığında geçiş sağlanınca nesne yeniden otomatik olarak kapalı duruma getirilir. Bunun anlamı izleyen paragraflarda daha iyi anlaşılacaktır. Fonksiyonun üçüncü parametresi nesnenin başlangıçta açık mı (signaled) yoksa kapalı mı (nonsignaled) olacağını belirtmektedir. Bu parametre TRUE girilirse nesne başlangıçta açık, FALSE girilirse kapalı durumda olur. Genellikle event nesnesi yaratılıken kapalı bir biçimde yaratılmaktadır. Fonksiyonun son parametresi nesnenin proseslerarası kullanılması için gerekli olan ismini belirtmektedir. Eğer nesne aynı prosesin thread'leri arasında kullanılacaksa bu parametre NULL adres biçiminde geçilebilir. Fonksiyon başarı durumunda event nesnesinin handle değerine, başarısızlık durumunda NULL adrese geri dönemktedir. Örneğin: HANDLE g_hEvent; ... if ((g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL)) == NULL) Exitrys("CreateEvent"); -> Event nesnesi yine diğer kernel senkronizasyon nesnelerinde olduğu gibi WaitForSingleObject ve WaitForMultipleObjects fonksiyonlarıyla yapılmaktadır. WaitForSingleObject fonksiyonu eğer event nesnesi kapalıysa blokeye yol açar, eğer nesne açık durumdaysa geçiş yapar. Yani akış WaitForSingleObject fonksiyonunda beklemeden hemen fonksiyondan çıkarak devam eder. -> Event nesnesini açık duruma geçirmek için SetEvent API fonksiyonu kullanılır. SetEvent fonksiyonunun prototipi şöyledir: BOOL SetEvent( HANDLE hEvent ); Fonksiyon event nesnesinin handle değerini parametre olarak alır ve nesneyi açık duruma geçirir. Artık nesne kapalı olduğundan dolayı bekleyen thread WaitForSingleObject fonksiyonundan çıkar. 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: SetEvent(g_hEvent); Nesne SetEvent fonksiyonu ile açık duruma geçirildiktin sonra WaitForSingleObject fonksiyonunda bekleyen thread blokeden çıkar. İşte eğer nesne otomatik ise bu durumda WaitForSİngleObject fonksiyonundan çıkılırken nesne otomatik olarak yeniden kapalı duruma geçmektedir. Eğer nesne manuel durumdaysa WaitForSingleObject fonksiyonu sonlandığında nesne hala açık durumda olmaya devam eder. Event nesnelerini kapalı duruma geçirmek için ResetEvent API fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: BOOL ResetEvent( HANDLE hEvent ); Fonksiyon event nesnesinin handle değerini parametre olarak alır, başarı durumunda sıfır değerine başarısızlık durumunda sıfır dışı bir değere geri döner. WaitForSingleObject (ya da WaitForMultipleObjects) fonksiyonu ile birden fazla thread otomatik moddaki event nesnesini bekliyorsa, bu event nesnesi açık duruma geçtiğinde yalnızca tek bir thread'in blokesi çözülür. Çünkü event nesnesi otomatik durumda olduğu için WaitForSingleObject fonksiyonundan çıkılır çıkılmaz nesne atomik bir biçimde kapalı duruma geçirilecektir. Tabii eğer event nesnesi manuel modda ise SetEvent yapıldığında event nesnesini bekleyen thread'lerin blokesi çözülecektir. Aşağıda event senkronizasyon nesnesinin kullanımına bir örnek verilmiştir. * Örnek 1, #include #include #include DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); HANDLE g_hEvent; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL)) == NULL) ExitSys("CreateEvent"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); printf("Press ENTER to set event...\n"); getchar(); SetEvent(g_hEvent); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); CloseHandle(hThread1); CloseHandle(hThread2); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { printf("Thread1 running...\n"); printf("waiting for the event object...\n"); WaitForSingleObject(g_hEvent, INFINITE); printf("Ok, thread1 resumes...\n"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { printf("Thread2 running...\n"); printf("waiting for the event object...\n"); WaitForSingleObject(g_hEvent, INFINITE); printf("Ok, thread2 resumes...\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Sık karşılaşılan diğer bir senkronizasyon nesnesi de "okuma-yazma kilitleri (reder-writer lock)" denilen nesnelerdir. Önce bu nesnelere neden gereksinim duyulduğunu açıklayalım. Bir kaynak üzerinde bir grup thread'in "okuma" bir grup thread'in de "yazma" eyleminde bulunduğunu düşünelim. Burada "okuma" eylemi demekle "kaynakta değişiklik yaratmayan", yazma eylemi demekle de "kaynakta değişiklik yaratan" eylemleri kastediyoruz. Örneğin global bir bağlı liste söz konusu olsun. Thread'lern bazıları bu bağlı liste üzerinde "arama" işlemi yaparken bazıları da bu bağlı liste üzerinde insert işlemi yapıyor olsunlar. Burada arama işlemi okuma eylemini, insert işlemi ise yazma eylemini temsil etmektedir. Birden fazla thread'in aynı anda bağlı listede arama yapmasının sakıncası yoktur. Ancak bir thread bağlı listede insert işlemi yaparken başka thread'lerin o işlem bitene kadar arama işlemi ya da insert işlemi yapmaması gerekir. Benzer biçimde bir thread bağlı liste üzerinde arama işlemi yaparken başka bir thread'in insert işlemi de yapmaması gerekir. İki thread için buradaki olası durumları şöyle ifade edebiliriz: Thread1 Thread2 Ne Yapılmalı? ------------------------------------------------------- Okuma Okuma İzin Verilmeli Okuma Yazma Senkronize Edilmeli Yazma Okuma Senkronize Edilmeli Yazma Yzma Senkronize Edilmeli O halde buradan çıkan sonuç şudur: -> Bir thread yazma yaparken yazma ve okuma yapacak thread'ler bu yazma olayının bitmesini beklemelidir. -> Bir thread okuma yaparken yazma yapacak thread'ler bu okuma işlminin bitmesini beklemelidir. -> Bir thread okuma yaparken, okuma yapacak diğer thread'ler eş zamanlı olarak bu işlemi yapabilirler. Okuma-yazma kilitleri Windows sistemlerinde de UNIX/Linux sistemlerinde de var olan senkronizasyon nesnelerindendir. Biz burada önce Windows sistemlerindeki okuma-yazma kilitlerini daha sonra UNIX/Linux sistemlerindeki okuma-yazma kilitlerini göreceğiz. Pekiyi reader-writer lock yerine yukarıdaki işlem mutex nesneleri ile yapılamaz mı? Eğer yukarıdaki problem mutex nesneleriyle çözülmeye çalışılırsa bu durumda mecburen okuma sırasında da kilitleme yapılır. Dolayısıyla gereksiz bir biçimde okuma yapmak isteyen birden fazla thread birbirlerini beklemek zorunda kalır. Aşağıdaki temsili (pseudo) kodu inceleyiniz: pthread_mutex_t g_mutex; ... read() { pthread_mutex_lock(&g_mutex); ... pthread_mutex_unlock(&g_mutex); } write() { pthread_mutex_lock(&g_mutex); ... pthread_mutex_unlock(&g_mutex); } Burada görüldüğü gibi bireden fazla read işlemi birlikte yapılamamaktadır. Reader-writer lock nesneleri aslında taban (base) senkronizasyon nesnelerinden değildir. Bunlar mutex ve koşul değişkenleri (condition variable) kullanılarak gerçekleştirilebilmektedir. Windows sistemlerinde okuma-yazma kilitleri şöyle kullanılmaktadır: -> Okuma-yazma kilit nesneleri SRWLOCK türüyle temsil edilmektedir. Önce global düzeyde SRWLOCK türünden bir nesne yaratılır. Bu nesneye InitializeSRWLock fonksiyonu ile ilkdeğerleri verilir. Fonksiyonun prototipi şöyledir: void InitializeSRWLock( PSRWLOCK SRWLock ); Fonksiyon SRWLOCK nesnesinin başlangıç adresini parametre olarak almaktadır. Örneğin: SWRLOCK g_srwLock; ... InitializeSRWLock(&g_srwLock); Dolaysıyla okuma amaçlı kritik kod şöyle oluşturulmaktadır: AcquireSRWLockShared(&g_srwLock); ... ... ... ReleaseSRWLockShared(&g_srwLock) Burada okuma amaçlı kilidin alınması AcquireSRWLockShared fonksiyonu ile yapılmaktadır. Fonksiyon eğer kilit başka bir thread tarafından yazma amaçlı olarak alındıysa blokeye yol açmaktadır. Ancak kilit başka bir thread tarafından okuma amaçlı alındıysa blokeye yol açmadan kritik koda geçişi sağlamaktadır. Okuma amaçlı kritik koddan çıkılırken kilit ReleaseSRWLockShared fonksiyonu ile serbest bırakılmalıdır. -> Yazma Amaçlı kritik kod da şöyle oluşturulmaktadır: AcquireSRWLockExclusive(&g_srwLock); ... ... ... ReleaseSRWLockExclusive(g_srwLock) Eğer kilit başka bir thread tarafından okuma amaçlı ya da yazma amaçlı olarak alınmışsa AcquireSRWLockExclusive fonksiyonu blokede bekler. Eğer kilit okuma ya da yazma amaçlı hiçbir thread tarafından alınmadıysa kritik koda geçiş yapılır. Yazma amaçlı kritik koddan çıkılırken kilit ReleaseSRWLockExclusive fonksiyonu ile serbest bırakılmalıdır. Fonksiyonların prototipileri şöyledir: void AcquireSRWLockShared( PSRWLOCK SRWLock ); void ReleaseSRWLockShared( PSRWLOCK SRWLock ); void AcquireSRWLockExclusive( PSRWLOCK SRWLock ); void ReleaseSRWLockExclusive( PSRWLOCK SRWLock ); Fonksiyonlar SRWLOCK nesnelerinin adreslerini parametre olarak almaktadır. -> Windows'ta reader-writer lock nesnelerinin serbest bırakılması gibi bir durum söz konusu değildir. Bunlar zaten bir kaynak tutmamaktadır. Aşağıda Windows sistemlerinde reader-write lock nesnelerinin kullanımına ilişkin bir örnek verilmiştir. Bu örnekte belli sayıda thread yaratılıp bunların rastgele read-write işlemleri yapması sağlanmıştır. Ekrandaki çıktı incelendiğinde write işlemi başladığında bitene kadar başka hiç read ya da write işleminin yapılmadığı görülecektir. Ancak read işlemleri birlikte yapılabilmektedir. Programın ekran çıktısının bir kısmının örnek bir görüntüsü şöyledir. ... Thread-1 thread begins writing... Thread-1 thread ends writing... Thread-3 thread begins reading... Thread-3 thread ends reading... Thread-6 thread begins writing... Thread-6 thread ends writing... Thread-2 thread begins reading... Thread-7 thread begins reading... Thread-7 thread ends reading... Thread-2 thread ends reading... ... Burada örneğin Thread-2 ve Thread-7'nin read işlemine birlikte girdiği görülmektedir. Ancak bir write işlemi başladığında o write işlemi bitene kadar read ya da write yapılamamaktadır. * Örnek 1, #include #include #include #include #include #define NTHREADS 10 DWORD __stdcall ThreadProc(LPVOID lpvParam); void Read(const char *pszThreadName); void Write(const char *pszThreadName); void ExitSys(LPCSTR lpszMsg); SRWLOCK g_srwLock; int main(void) { HANDLE hThreads[NTHREADS]; DWORD dwThreadIds[NTHREADS]; char szThreadName[32]; char *pszThreadName; srand(time(NULL)); InitializeSRWLock(&g_srwLock); for (int i = 0; i < NTHREADS; ++i) { sprintf(szThreadName, "Thread-%d", i + 1); if ((pszThreadName = strdup(szThreadName)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if ((hThreads[i] = CreateThread(NULL, 0, ThreadProc, pszThreadName, 0, &dwThreadIds[i])) == NULL) ExitSys("CreateThread"); } if (WaitForMultipleObjects(NTHREADS, hThreads, TRUE, INFINITE) == WAIT_FAILED) ExitSys("CreateThread"); for (int i = 0; i < NTHREADS; ++i) CloseHandle(hThreads[i]); return 0; } DWORD __stdcall ThreadProc(LPVOID lpvParam) { const char *pszThreadName = (LPCTSTR)lpvParam; for (int i = 0; i < 10; ++i) { Sleep(rand() % 100); if (rand() % 2 == 0) Read(pszThreadName); else Write(pszThreadName); } free(lpvParam); return 0; } void Read(const char *pszThreadName) { AcquireSRWLockShared(&g_srwLock); printf("%s thread begins reading...\n", pszThreadName); Sleep(rand() % 300); printf("%s thread ends reading...\n", pszThreadName); ReleaseSRWLockShared(&g_srwLock); } void Write(const char *pszThreadName) { AcquireSRWLockExclusive(&g_srwLock); printf("%s thread begins writing...\n", pszThreadName); Sleep(rand() % 300); printf("%s thread ends writing...\n", pszThreadName); ReleaseSRWLockExclusive(&g_srwLock); } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Daha önce de belirttiğimiz gibi belli bir zamandan sonra Windows sistemlerine de durum değişkenleri eklenmiştir. Windows sistemlerindeki durum değişkenleri mutex nesneleriyle değil CRITICAL_SECTION ve Read/Write Lock nesneleriyle kullanılabilmektedir. Genel kullanım biçimi UNIX/Linux sistemlerindekine çok benzemektedir. >> UNIX/Linux Sistemlerinde: UNIX/Linux sistemlerinde thread işlemleri başı "pthread_" ila başlatılan POSIX fonksiyonlarıyla yapılmaktadır. Thread işlemleri için kullanılan bu fonksiyonların oluşturduüu topluluğa "pthread kütüphanesi" de denilmektedir. Yukarıda da belirttiğimiz gibi bu kütüpahendeki tüm fonksiyonlar pthread_xxx biçiminde isimlendirilmiştir. Tüm thread fonksiyonlarının prototipleri isimli başlık dosyasının içerisindedir. UNIX/Linux sistemlerinde thread yaratmak için pthread_create isimli POSIX fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); Fonksiyonun birinci parametresi thread'in id'sinin yerleştirileceği pthread_t türünden nesnenin adresini almaktadır. Bu sistemlerde thread'lerin yalnızca id'leri vardır. Thread işlemleri de id'lerle yapılmaktadır. pthread_t türü işletim sistemini yazanlar tarafından herhangi bir olarak typedef edilebilmektedir. Linux sistemlerinde bu tür unsigned long olarak typedef edilmiştir. Ancak başka sistemlerde bir yapı biçiminde de typedef edilmiş olabilir. Fonksiyonun ikinci parametresi yaratılacak thread'e ilişkin bazı özelliklerin belirtildiği thread özellik nesnesinin adresinin almaktadır. Programcı thread özelliklerini bu nesne ile oluşturup bu nesnenin adresini fonksiyona vermektedir. Ancak bu parametre NULL adres olarak da geçilebilir. Bu durumda thread default özelliklerle yaratılacaktır. Fonksiyonun üçüncü parametresi thread akışının başlatılacağı fonksiynun adresini belirtmektedir. Thread fonksiyonlarının geri dönüş değerlerinin void * türünden parametrelerinin de void * türünden olması gerekir. Örneğin: void *thread_proc(void *param) { .... } Fonksiyonun son parametresi thread fonksiyonuna geçirilecek olan argümanı belirtmektedir. Tabii eğer thread fonksiyonuna bir parametre geçirilmek istenmiyorsa bu parametre için NULL adres kullanılabilir. pthread_create fonksiyonu başarı durumunda 0 değerine geri dönmektedir. Fonksiyon başarısızlık durumunda errno değişkeninin set etmez. Başarısızlığı belirten errno değeri ile geri döner. Biz de bu değeri strerror fonksiyonu ile yazıya dönüştürüp yazdırabiliriz. Örneğin: pthread_t tid; int result; void *thread_proc(void *param); ... if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) { fprintf(stderr, "pthread_create: %s\n", strerror(result)); exit(EXIT_FAILURE); } ... Tabii işlemleri kısaltmak için hata durumunu ele alan bir fonksiyon da yazabiliriz: void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } pthread_create fonksiyonuyla yaratılmış olan thread'ler hemen çalışmaya başlamaktadır. Aşağıda UNIX/Linux sistemlerinde thread yaratmaya bir örnek verilmiştir. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { 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 thread: %d\n", i); sleep(1); } CloseHandle(hThread); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); sleep(1); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Bir thread herhangi bir noktada UNIX/Linux ve macOS sistemlerinde pthread_exit fonksiyonu ile sonlandırabilir. Bu fonksiyonları hangi thread akışı çağırırsa o thread sonlanmaktadır. exit fonksiyonunun tüm prosesi sonlandırdığına ancak pthread_exit fonksiyonlarının yalnızca tek bir thread'i sonlandırdığına dikkat ediniz. Therad'lerin de tıpkı prosesler gibi exit kodları vardır. UNIX/Linux ve macOS sistemlerinde ise void * bir değerle temsil edilmektedir. Thread'in exit kodları thread sonlandığında ilgili prosesler tarafından alınıp çeşitli amaçlarla kullanılabilmektedir. Ancak uygulamaların çoğunda bu exit kodunu kullanamaya gerek duyulmamaktadır. pthread_exit fonksiyonunun prototipi şöyledir: #include void pthread_exit(void *retval); Aşağıda UNIX/Linux ve macOS sistemlerinde thread'in pthread_exit fonksiyonu ile sonlandırılmasına bir örnek verilmiştir. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { 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 thread: %d\n", i); sleep(1); } CloseHandle(hThread); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); if (i == 5) pthread_exit(NULL); sleep(1); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde de bir thread başka bir thread'i zorla pthread_cancel fonksiyonu ile sonlandırabilir. Ancak bu fonksiyon Windows sistemlerindeki TerminateThread fonksiyonu gibi çalışmamaktadır. UNIX/Linux sistemlerinde bir thread'e pthread_cancel fonksiyonu uygulanırsa thread akışı ancak bazı POSIX fonksiyonlarında sonlandırılmaktadır. Dolayısıyla bu sistemlerde pthread_cancel fonksiyonu TerminateThread fonksiyonuna göre daha güvenlidir. pthread_cancel uygulandığında thread akışının sonlandırılabileceği POSIX fonksiyonlarına İngilizce "cancellation points" denilmektedir. Bu fonksiyonarın listesi POSIX standartrlarında belirtilmiştir. pthread_cancel fonksiyonunun prototipi şöyledir: #include int pthread_cancel(pthread_t thread); Fonksiyon parametre olarak sonlandırılacak thread'in id değerini almaktadır. Başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte ana thread'teki döngü 5 kez yibelendikten sonra diğer thread'i pthread_cancel fonksiyonu ile sonlandırmaktadır. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { 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 thread: %d\n", i); if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_sys_errno("pthread_cancel", result); sleep(1); } return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); if (i == 5) pthread_exit(NULL); sleep(1); } 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 pthread_cancel fonksiyonu sonsuz döngüdeki thread'i sonlandıramayacaktır. Çünkü bu thread sonlandırma için gereken POSIX fonksiyonlarına (cancellation points) girmemiştir. Thread'in sonlandırılıp sonlandırılmadığını başka bir terminalden ps komutunda -T seçeneğini kullanarak görebilirsiniz. Komut şöyle uygulanabilir: $ ps -t /dev/pts/0 -T Burada /dev/pts/0 thread'li programın çalıştığı termşnali belirtmektedir. Bu terminal sizin denemenizde farklı olabilir. Bu terminali tty komutu ile öğrenebilirisniz. #include #include #include #include #include void *thread_proc(void *param); void exit_errno(const char *msg, int result); int main(int argc, char *argv[]) { pthread_t tid; int result; int i; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_errno("pthread_create", result); for (i = 0; i < 10; ++i) { if (i == 5) if ((result = pthread_cancel(tid)) != 0) exit_errno("pthread_cancel", result); printf("main thread: %d\n", i); sleep(1); } printf("press ENTER to exit..\n"); getchar(); return 0; } void *thread_proc(void *param) { for (;;) ; return NULL; } void exit_errno(const char *msg, int result) { fprintf(stderr, "%s: %s\n", msg, strerror(result)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde thread'lerin sonlanmasının beklenmesi ve exit kodlarının alınması pthread_join fonksiyonuyla sağlanmaktadır. Yani fonksiyon hem bekleme yapıp hem de exit kodu almaktadır. Fonksyonunun prototipi şöyledir: #include int pthread_join(pthread_t thread, void **retval); Fonksiyonun birinci parametresi beklemecek thread'in id değerini belirtmektedir. İkinci parametre exit kodunun yerleştirileceği void göstericinin adresini belirtmektedir. Eğer ikinci parametre NULL geçilirse thread'in birmesi beklenir ancak exit kodu çağıran fonksiyona iletilmez. Fonksiyon baları durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte main fonksiyonu içerisinde (yani ana thread'te) bir thread yaratılmış e thread'in sonlanması pthread_join fonksiyonuyla beklenmiştir. Bu örnekte exit kodu bir tamsayı olduğu halde sanki bir adresmiş gibi oluşturulmuştur. Yine exit kod göstericinin içerisinden alınarak int tüürne dönüştürülüp kullanılmıştır. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; void *exit_code; printf("main begins...\n"); if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); printf("main thread waits at pthread_join...\n"); if ((result = pthread_join(tid, &exit_code)) != 0) exit_sys_errno("pthread_join", result); printf("Exit code: %d\n", (int)exit_code); return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); sleep(1); } return (void *)123; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde "zombie proses" kavramı vardı. Pekiyi zombie thread kavramı da var mıdır? Aslında bu sistemlerde işletim sistemi tıpkı proseslerde olduğu gibi therad'in ezit kodu alınmamışsa belli bir sistem kaynağını serbest bırakmadan bekletmektedir. Yani zombie thread kavramı zombie proses kavramı gibi bu sistemlerde söz konsudur. Ancak zombie thread'ler zombie prosesler kadar probleme yol açma potansiyelinde değildir. Fakat yine pthread_join fonksiyonuyla zombie thread'lerin oluşması engellenmelidir. Eğer programcı thread'in exit kodu ile ilgilenmiyorsa thread biter bitmez kaynakalrın boşaltılmasını işletim sisteminden isteyebilir. Bunun için pthread_detach fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_detach(pthread_t thread); Fonksiyon parametre olarak thread'in id değerini alır, başarı drumunda sıfır değerine, başarısızlık durumunda errno değerine geri döner. Tabii detach moda sokulmuş thread'ler artık pthread_join fonksiyonuyla beklenemezler. Eğer bunlar beklenmeye çaışılırsa ptherad_join fonksiyonu başarısız olmaktadır. Thread'ler konusunun en önemli alt konusu thread senkronizasyonudur. Bir grup thread birlikte bir işi gerçekleştirirken kimi zaman birbirlerini beklemesi, birbirleriyle koordineli bir biçimde çalışması gerekmektedir. İşte işletim sistemlerinde bunu sağlamaya yönelik mekanizmalara "thread senkronizasyonu" denilmektedir. Thread senkronizasyonunun önemi basit örnekle anlaşılabilir. Thread'lerin aynı global nesneleri kullandığını belirtmiştik. İki thread aynı global değişkeni bir döngü içerisinde bir milyon kere artırıyor olsun. Bu global değişkenin değerinin iki milyon olması beklenir. Ancak senkronizasyon problemi yüzünden muhtemelen iki milyon olamayacaktır. Aşağıda bu örnek Unix/Linux sistemleri için oluşturulmuştur. Programın farklı çalıştırılmalarında elde edilen bazı değerler şunlardır: $ ./sample 1140644 $ ./sample 1175870 $ ./sample 1900343 Aşağıda ilgili programa ilişkin örnek verilmiştir: * Örnek 1, #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); int g_count; int main(void) { 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); printf("%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_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Yukarıda da belirtitğimiz gibi UNIX/Linux sistemlerinde Windows'taki gibi bir CRITICAL_SECTION nesnesi yoktur. Kritik kod oluşturmak için mutex nesneleri kullanılmaktadır. UNIX/Linux sistemlerinde mutex nesneleri aynı prosesin thread'leri arasındaki senkronizasyon için tasarlandığından dolayı Windows'taki mutex nesnelerine göre daha hızlıdır. Ancak istenirse biraz zor olsa da UNIX/Linux sistemlerindeki mutex nesneleri prosesler arasında da kullanılabilir. UNIX/Linux sistemlerinde mutex nesneleri şu adımlardan geçilerek kullanılmaktadır: -> Henüz thread'ler yaratılmadan pthread_mutex_init fonksiyonu ile mutex nesnesi yaraılır. Fonksiyonun prototiği şöyledir: #include int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); Fonksiyonun birinci parametresi pthread_mutex_t türünden bir nesnenin adresini almaktadır. Fonksiyon bu nesneye bazı ilkdeğeri vermektedir. Buna mutex nesnesi diyebiliriz. pthread_mutex_t bir yapı biçiminde typedef edilmiştir. Programcı aynı prosesin thread'leri arasında senkronizasyon uygulamak için bu nesneyi global düzeyde ranımlamalıdır. Fonksiyonun ikinci parametresi yaparılacak mutex nesnesinin bazı özelliklerini belirlemek için kullanılmaktadır. Bu parametre NULL geçilebilir. Bu durumda mutex nesnesi default özelliklerle yaratılacaktır. Biz bu kursta mutex nesnelerinin özellikleri üzerinde durmayacağız. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: pthread_mutex_t g_mutex; ... if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", result); Mutex nesnelerine pthread_mutex_init yerine doğrudan PTHREAD_MUTEX_INITIALIZER makrosuyla da ilkdeğer verilebilmektedir. Örneğin: pthread_mutex_t g_mutex = PTHERAD_MUTEX_INIALIZER; -> Kritik kod pthread_muutex_lock ve pthread_mutex_unlock çağrıları arasında yerleştirilir: pthread_mutex_lock(&g_mutex); ... ... KRİTİK KOD ... pthread_mutex_unlock(&g_mutex); Bir thread pthread_mutex_lock fonksiyonuna girdiğinde eğer mutex'in sahipliği başka bir thread tarafından alınmışsa o thread sahipliği bırakana kadar fonksiyon blokede bekler. Eğer mutex nesnesinin sahipli alınmamışsa pthread_mutex_lock nensnenin sahipliğini alarak kritik koda giriş yapar. Nesnenin sahipliği pthread_mutex_unlock fnksiyonuyla bırakılmaktadır. Fonksiyonların prototipleri şöyledir: #include int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); Fonksiyonlar mutex nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönerler. -> Mutex ile çalışma bittikten sonra mutex nesnesi pthread_mutex_destroy fonksiyonuyla yok edilir. Fonksiyonun prototipi şöyledir: #include int pthread_mutex_destroy(pthread_mutex_t *mutex); Fonksiyon mutex nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri döner. Aslında pek çok kütüphanede bu fonksiyon bir şey yapmamaktadır. Ancak başka gerçekleştirimlerde bu fonksiyon birtakım kaynakları boşaltıyor olabilir. Aşağıda UNIX/Linux sistemlerinde mutex kullanımına bir örnek verilmiştir. Yine örnekte global bir sayaç değişkeni alınmıştır. İki thread de bu sayacı artırmaktadır. Ancak artırım sırasında mutex koruması uygulanmıştır. * Örnek 1, #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); pthread_mutex_t g_mutex; int g_count; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", 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_muex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); printf("%d\n", g_count); 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_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("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_errno("pthread_mutex_lock", result); ++g_count; if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); } 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ğıda daha önce yapmış olduğumuz makine örneğininin UNIX/Linux sistemleriyle mutex nesneleriyle gerçekleştirimini veriyoruz. #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); void use_machine(const char *name); pthread_mutex_t g_mutex; int main(void) { pthread_t tid1, tid2; int result; srand(time(NULL)); if ((result = pthread_mutex_init(&g_mutex, NULL)) != 0) exit_sys_errno("pthread_mutex_init", 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); return 0; } void use_machine(const char *name) { int result; if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("-------------------\n"); printf("%s: 1. Step\n", name); usleep(rand() % 300000); printf("%s: 2. Step\n", name); usleep(rand() % 300000); printf("%s: 3. Step\n", name); usleep(rand() % 300000); printf("%s: 4. Step\n", name); usleep(rand() % 300000); printf("%s: 5. Step\n", name); usleep(rand() % 300000); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } void *thread_proc1(void *param) { int i; for (i = 0; i < 10; ++i) use_machine("Thread-1"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 10; ++i) use_machine("Thread-2"); return NULL; } 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 default durumda özyinelemeli değildir. Bu sistemlerde mutex nesnelerin özyinelemeli yapmak için önce pthread_mutexattr_t türünden bir nesne oluşturulur. Sonra bu nesne pthread_mutexattr_init fonksiyonuyla ilkdeğerlenir. Sonra da pthread_mutexattr_settype fonksiyonu ile PTHREAD_MUTEX_RECURSIVE parametresi kullanılarak mutex özelliği özyinelemeli olarak set edilir. Nihayet bu attribute nesnesi pthread_mutex_create fonksiyonunda kullanılır. Sonunda da bu attribute nesnesi pthread_mutexattr_destroy fonksiyonuyla yok edilir.Buradaki fonksiyonların prototipleri şöyledir: #include int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); Bu fonksiyonların birinci parametreleri mutex attribute nesnesinin adresini almaktadır. Fonksiyonlar başarı durumunda sıfır değerine başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: pthread_mutex_t g_mutex; ... pthread_mutexattr_t mattr; ... if ((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if ((result = pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE)) != 0) exit_sys_errno("pthread_mutexattr_settype", result); if ((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if ((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); Mutex attribute nesnesinin yalnızca mutex nesnesinin yaratılması sırasında kullanıldığına dikkat ediniz. Tabii UNIX/Linux sistemlerinde de özyinelemeli mutex'lerin kilidini açmak için pthread_mutex_lock işlemi kadar pthread_mutex_unlock işleminin yapılması gerekmektedir. Aşağıda buna yönelik bir örnek verilmiştir. * Örnek 1, #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); void do_machine(const char *name); void use_machine(const char *name); pthread_mutex_t g_mutex; int main(void) { pthread_t tid1, tid2; int result; pthread_mutexattr_t mattr; srand(time(NULL)); if ((result = pthread_mutexattr_init(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_init", result); if ((result = pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE)) != 0) exit_sys_errno("pthread_mutexattr_settype", result); if ((result = pthread_mutex_init(&g_mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if ((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", 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); return 0; } void do_machine(const char *name) { int result; if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); use_machine(name); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } void use_machine(const char *name) { int result; if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); printf("-------------------\n"); printf("%s: 1. Step\n", name); usleep(rand() % 300000); printf("%s: 2. Step\n", name); usleep(rand() % 300000); printf("%s: 3. Step\n", name); usleep(rand() % 300000); printf("%s: 4. Step\n", name); usleep(rand() % 300000); printf("%s: 5. Step\n", name); usleep(rand() % 300000); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } void *thread_proc1(void *param) { int i; for (i = 0; i < 10; ++i) do_machine("Thread-1"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 10; ++i) do_machine("Thread-2"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde mutex nesnesini prosesler arasında kullanabilmek için mutex nesnesinin paylaşılan bellek alanında yaratılması gerekir. (Yani bizim pthread_mutex_t türünden nesnseyi paylaşılan bellek alanında yaratmamız gerekir.) Böylece iki proses de aynı mutex nesnesini görecektir. Ancak ayrıca mutex'in prosesler arası kullanımını mümkün hale getirmek için mutex attribute nesnesinde pthread_mutexattr_setpshared fonksiyonu ile belirleme yapmak gerekir. Bu fonksiyonun prototipi şöyledir: int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); Fonksiyonun birinci parametresi mutex attribute nesnesinin adresini almaktadır. İkinci parametre için PTHREAD_PROCESS_SHARED değeri proseslerarası paylaşım yapılabileceğini, PTHREAD_PROCESS_PRIVATE değeri ise proseslerarası paylaşım yapılamayacağını belirtmektedir. Fonksiyon başarı durumunda sıfır değerine başarısızlık durumunda errno değerine geri dönmektedir. Aşağıdaki örnekte prog1 programı önce paylaşılan bellek alanını ve mutex nesnesini oluşturmuştur. prog2 ise paylaşılan bellek alanındaki mutex nesnesini kullanmıştır. Paylaşılan bellek alanının başı aşağıdaki gibi bir yapı ile temsilk edilmiştir: struct SHARED_OBJECT { pthread_mutex_t mutex; long long count; }; Aşağıdaki örnekte önce prog1 programını çalıştırmalısınız. Çünkü bu örnekte paylaşılan bellek alanını ve mutex nesnesini prog1 programı yaratmaktadır. * Örnek 1, /* prog1.c */ #include #include #include #include #include #include #include #include void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int eno); struct SHARED_OBJECT { pthread_mutex_t mutex; long long count; }; int main(void) { int fdshm; int result; void *shmaddr; pthread_mutexattr_t mattr; struct SHARED_OBJECT *so; if ((fdshm = shm_open("/sample_shared_memory_name", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, 4096) == -1) exit_sys("ftruncate"); if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) exit_sys("mmap"); so = (struct SHARED_OBJECT*)shmaddr; so->count = 0; 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(&so->mutex, &mattr)) != 0) exit_sys_errno("pthread_mutex_init", result); if ((result = pthread_mutexattr_destroy(&mattr)) != 0) exit_sys_errno("pthread_mutexattr_destroy", result); for (long long int i = 0; i < 1000000000; ++i) { if ((result = pthread_mutex_lock(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++so->count; if ((result = pthread_mutex_unlock(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } printf("Press ENTER to continue...\n"); getchar(); printf("%lld\n", so->count); if ((result = pthread_mutex_destroy(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); if (munmap(shmaddr, 4096) == -1) exit_sys("munmap"); close(fdshm); if (shm_unlink("/sample_shared_memory_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); } /* prog2.c */ #include #include #include #include #include #include #include #include void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int eno); struct SHARED_OBJECT { pthread_mutex_t mutex; long long int count; }; int main(void) { int fdshm; int result; void *shmaddr; struct SHARED_OBJECT *so; if ((fdshm = shm_open("/sample_shared_memory_name", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) exit_sys("mmap"); so = (struct SHARED_OBJECT*)shmaddr; for (long long int i = 0; i < 1000000000; ++i) { if ((result = pthread_mutex_lock(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); ++so->count; if ((result = pthread_mutex_unlock(&so->mutex)) != 0) exit_sys_errno("pthread_mutex_unlock", result); } if (munmap(shmaddr, 4096) == -1) exit_sys("munmap"); close(fdshm); 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 tıpkı paylaşılan bellek alanlarında olduğu gibi semaphore'lar için de iki ayrı arayüz fonksiyon grubu bulunmaktadır. Eskiden beri var olan klasik semaphore fonksiyonlarına "Sistem 5 semaphore'ları" denilmektedir. Bu semaphore fonksiyonlarının kullanımı oldukça zordur. 90'lı yılların ortalarında UNIX/Linux sistemlerinde "POSIX semaphore'ları" da denilen modern semaphore fonksiyonları eklenmiştir. Artık programcılar genellikle bu POSIX semaphore fonksiyonlarını tercih etmektedir. Tabii her iki fonksiyon grubu da aslında POSIX standartlarında yer almaktadır. Ancak "POSIX semaphore fonksiyonları" denildiğinde daha sonra tasarlanmış olan modern semaphore fonksiyonları kastedilmektedir. Klasik Sistem 5 semaphore'ları proseslerarası kullanım için tasarlanmıştır. Halbuki modern POSIX semaphore'ları hem aynı prosesin thread'leri arasında hem de farklı proseslerin thread'leri arasında kullanılabilmektedir. Biz kursumuzda modern POSIX semaphore fonksiyonlarını göreceğiz. Eski tipi "Sistem 5 Semaphore" fonksiyonları "UNIX/Linux Sistem Programlama" kurslarında ele alınmaktadır. POSIX semaphore fonksiyonları aşağıdaki adımlardan geçilerek kullanılmaktadır: -> POSIX semapore nesneleri sem_t türüyle temsil edilmektedir. sem_t bir yapıyı belirten typedef ismidir. Eğer semaphore nesnesi aynı prosesin thread'leri arasında kullanılacaksa sem_t türünden global bir nesne tanımlanır ve bu nesneye sem_init fonksiyonu ile ilkdeğerleri verilir. sem_init fonksiyonunun prototipi şöyledir: #include int sem_init(sem_t *sem, int pshared, unsigned int value); Fonksiyonun birinci parametresi sem_t türünden nesnenin adresini almaktadır. Fonksiyonun ikinci parametresi bu nesnenin prosesler arasında paylaşılıp paylaşılmayacağını belirtmektedir. Eğer nesne prosesler arasında paylaşılacaksa bu parametreye sıfır dışı bir değer, paylaşılmayacaksa sıfır değeri geçilmelidir. Ancak zaten proseslerarası kullanım için başka bir fonksiyon bulunduğundan genellikle bu parametre sıfır geçilir. Fonlsiyonun üçüncü parametresi semaphore sayacının başlangıç değerini belirtmektedir. Yani bu değer kritik koda en fazla kaç akışın gireceğini belirtir. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri dönmektedir. Fonksiyon errno değerini set etmektedir. (Diğer thread fonksiyonlarının errno değerini set etmediğini bizzat errno değerine geri döndüğünü anımsayınız.) Eğer semaphore nesnesi prosesler arasında kullanılacaksa bu durumda sem_t nesnesi sem_open fonksiyonu ile elde edilmelidir. Bu fonksiyon üzerinde daha sonra durulacaktır. -> Kritik kod sem_wait ve sem_post çağrıları arasına yerleştirilir: sem_wait(&g_sem); ... ... ... sem_post(&g_sem); Akış sem_wait fonksiyonuna geldiğinde eğer semaphore sayacı 0 ise sem_wait blokeye yol açar ve semaphore sayacı 0'dan büyük olana kadar bekler. Eğer semaphore sayacı 0'dan büyükse akış sem_wait fonksiyonundan geçer ancak semaphore sayacı 1 eksiltilir. sem_post fonksiyonu semaphore sayacını 1 artırmaktadır. Bu fonksiyonların prototipleri şöyledir: #include int sem_wait(sem_t *sem); int sem_post(sem_t *sem); Fonksiyonlar semaphore nesnelerinin adresini parametre olarak almakta, başarı durumunda sıfır değerine başarısızlık durumunda -1 değerine geri dönmektedirler. Yine başarısızlık durumunda errno değişkeni set edilmektedir. -> Semaphore nesnesinin kullanımı bittikten sonra nesne sem_destroy fonksiyonu ile yok edilmelidir. Fonksiyonun prototipi şöyledir: #include int sem_destroy(sem_t *sem); Fonksiyon semaphore nesnesinin adresini parametre olarak alır. Başarı durumunda 0 değerine, başarısızlık durumunda -1 değerine geri döner. Başarısızlık durumunda errno değişkeni set edilmektedir. Eğer semaphore nesnesi proseslerarası kullanım için sem_open fonksiyonuyla yaratılmışsa nesnenin yok edilmesi sem_close fonksiyonu ile yapılmalıdır. Aşağıda daha önce yapmış olduğumuz global değişkeninin iki thread tarafından artırılması binary POSIX semaphore'larıyla sağlanmıştır. * Örnek 1, #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 eno); sem_t g_sem; int g_count; int main(void) { pthread_t tid1, tid2; int result; if (sem_init(&g_sem, 0, 1) == -1) exit_sys("sem_init"); 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 (sem_destroy(&g_sem) == -1) exit_sys("sem_destroy"); printf("%d\n", g_count); return 0; } void *thread_proc1(void *param) { for (int i = 0; i < 1000000; ++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_proc2(void *param) { for (int i = 0; i < 1000000; ++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ğıda daha önce yapmış olduğumuz makine konumlandırma örneği POSIX semaphore nesneleriyle gerçekleştirilmiştir. #include #include #include #include #include #include #include void *thread_proc1(void* param); void *thread_proc2(void* param); void do_machine(const char* name); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); sem_t g_sem; int main(void) { int result; pthread_t tid1, tid2; srand(time(NULL)); if (sem_init(&g_sem, 0, 1) == -1) exit_sys("sem_init"); 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); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&g_sem); return 0; } void *thread_proc1(void *param) { int i; for (i = 0; i < 10; ++i) do_machine("thread-1"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 10; ++i) do_machine("thread-2"); return NULL; } void do_machine(const char *name) { if (sem_wait(&g_sem) == -1) exit_sys("sem_wait"); printf("---------------\n"); printf("1) %s\n", name); usleep(rand() % 300000); printf("2) %s\n", name); usleep(rand() % 300000); printf("3) %s\n", name); usleep(rand() % 300000); printf("4) %s\n", name); usleep(rand() % 300000); printf("5) %s\n", name); usleep(rand() % 300000); if (sem_post(&g_sem) == -1) exit_sys("sem_post"); } 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 semaphore nesneleriyle çözülmektedir. Problemin çözümü için iki semaphore nesnesi yatatılır. Semaphore'lardan biri üretici semaphore'u diğeri ise tüketici semaphore'u olur. Başlangıçta üretici semaphore'u 1'e tüketici semaphore'u ise 0'a kurulur: sem_t g_sem_producer; sem_t g_sem_consumer; ... sem_init(&g_sem_producer, 0, 1); sem_init(&g_sem_consumer, 0, 0); Üretici ve tüketici döngülerinin temsili biçimi şöyledir: ÜRETİCİ ------- for (;;) { sem_wait(&g_sem_producer); sem_post(&g_sem_consumer); } TÜKETİCİ -------- for (;;) { sem_wait(&g_sem_consumer); sem_post(&g_sem_producer); } Burada üretici semaphore'unun başlangıçtaki sayaç değeri 1 olduğu için üretici sem_wait fonksiyonundan geçip elde ettiği değeri paylaşılan alana bırakacaktır. Bu sırada tüketici semaphore'unun başlangıç değeri 0 olduğu için tüketici sem_wait fonksiyonunda bekleyecektir. Üretici thread'in tüketicinin semaphore sayacını, tüketici thread'in ise üreticinin semaphore sayacını 1 artırdığına dikkat ediniz. Böylece üretici tüketiciyi tüketici de üreticiyi sem_wait fonksiyonundan geçirmektedir. Bunu tbir tahteravalliye benzetebilirsiniz. * Örnek 1, Aşağıda UNIX/Linux sistemlerinde üretici-tüketici probleminin POSIX semaphore nesneleriyle çözümüne bir örnek verilmiştir. Bu örnekte üretici thread 0'dan 100'a kadar sayıları rastgele beklemelerle paylaşılan alana yerleştirmekte tüketici thread de bunları rastgele beklemelerle almaktadır. Örneği senkronizasyonu kaldırarak çalıştırıp nasıl bir durum oluştuğunu gözleyiniz. #include #include #include #include #include #include #include void *thread_proc_producer(void* param); void *thread_proc_consumer(void* param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); sem_t g_sem_producer; sem_t g_sem_consumer; int g_shared; int main(void) { int result; pthread_t tid1, tid2; srand(time(NULL)); 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"); if ((result = pthread_create(&tid1, NULL, thread_proc_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&g_sem_producer); sem_destroy(&g_sem_consumer); return 0; } void *thread_proc_producer(void *param) { int val; val = 0; for (;;) { usleep(rand() % 300000); 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 == 99) break; ++val; } return NULL; } void *thread_proc_consumer(void *param) { 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"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if (val == 99) break; } putchar('\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 2, Aşağıda UNIX/Linux sistemlerinde üretici-tüketici probleminin kuyruklu versiyonuna bir örnek verilmiştir. Burada kuyruğun eşzamanlı erişimler için senkronize edilmesine gerek yoktur. Çünkü aslında işleyiş dikkatle incelendiğinde iki thread'in aynı nesnelere eş zamanlı erişmediği görülecektir. #include #include #include #include #include #include #include #define QUEUE_BUFFER_SIZE 10 void *thread_proc_producer(void* param); void *thread_proc_consumer(void* param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); sem_t g_sem_producer; sem_t g_sem_consumer; int g_qbuf[QUEUE_BUFFER_SIZE]; int g_head; int g_tail; int main(void) { int result; pthread_t tid1, tid2; srand(time(NULL)); if (sem_init(&g_sem_producer, 0, QUEUE_BUFFER_SIZE) == -1) exit_sys("sem_init"); if (sem_init(&g_sem_consumer, 0, 0) == -1) exit_sys("sem_init"); if ((result = pthread_create(&tid1, NULL, thread_proc_producer, NULL)) != 0) exit_sys_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc_consumer, NULL)) != 0) exit_sys_errno("pthread_create", result); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&g_sem_producer); sem_destroy(&g_sem_consumer); return 0; } void *thread_proc_producer(void *param) { int val; val = 0; for (;;) { usleep(rand() % 300000); if (sem_wait(&g_sem_producer) == -1) exit_sys("sem_wait"); g_qbuf[g_tail++] = val; g_tail = g_tail % QUEUE_BUFFER_SIZE; /* burası kritik kodun dışına alınabilir */ if (sem_post(&g_sem_consumer) == -1) exit_sys("sem_post"); if (val == 99) break; ++val; } return NULL; } void *thread_proc_consumer(void *param) { int val; for (;;) { if (sem_wait(&g_sem_consumer) == -1) exit_sys("sem_wait"); val = g_qbuf[g_head++]; g_head = g_head % QUEUE_BUFFER_SIZE; /* burası kritik kodun dışına alınabilir */ if (sem_post(&g_sem_producer) == -1) exit_sys("sem_post"); printf("%d ", val); fflush(stdout); usleep(rand() % 300000); if (val == 99) break; } putchar('\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); } Konunun başında da belirttiğimiz gibi UNIX/Linux sistemlerinde semaphore nesneleri isim verilerek de prosesler arasında kullanılabilmektedir. Bunun için sem_open fonksiyonu ile iki prosesin de ortak bir isimde anlaşarak semaphore nesnesini açması gerekmektedir. sem_open fonksiyonunun prototipi şöyledir: #include sem_t *sem_open(const char *name, int oflag, ...); Fonksiyon ya iki parametreyle ya da dört parametreyle kullanılmaktadır. Eğer fonksiyon dört parametreyle kullanılacaksa parametrik yapı aşağıdaki gibi olmalıdır: sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); Fonksiyonun birinci parametresi prosesler arasında paylaşımı sağlamak için belirlenen ismi belirtmektedir. POSIX standartlarına göre bu ismin "kök dizindeki bir dosya ismi gibi" oluşturulması gerekmektedir. (Burada bir dosya yaratılmamaktadır. Kök dizindeki dosya yalnızca bir temsildir.) Fonksiyonun ikinci parametresi yaratım bayraklarını belirtmektedir. Bu ikinci parametre üç değerden biri olarak geçilmelidir: O_CREAT O_CREAT|O_EXCL 0 O_CREAT bayrağı yine "nesne yaratılmamışsa yarat, nesne yaratılmışsa yaratılanı kullan" anlamına gelmektedir. O_CREAT ile O_EXCL birlikte kullanılırsa bu durumda "nesne zaten yaratılmış ise" fonksiyon baaşarısız olmaktadır. Bu parametreye 0 değeri geçilirse "yaratılmış olan semaphore nesnesi" kullanılmak üzere açılmaktadır. Eğer fonksiyonun ikinci parametresinde O_CREAT kullanılmışsa ve nesne daha önce yaratılmamışsa bu durumda fonksiyon üçünüc ve dördüncü parametreleri kullanmaktadır. Diğer durumlarda bu parametreleri kullanmamaktadır. Başka bir deyişle üçüncü ve dördüncü parametreler nesne ilk kez yaratılırken kullanılmaktadır. Semaphore nesnelerinde açış bayraklarında O_RDONLY, O_WRONLY ve O_RDWR kullanılması POSIX standartlarında "belirsiz (unspecified)" bırakılmıştır. Ancak Linux sistemlerinde bu bayrakların etkileri vardır. Bu konu "UNIX Linux Sistem Programlama" kurslarında ele alınmaktadır. sem_open fonksiyonu ile yaratılan isimli POSIX semaphore nesnesi yine paylaşılan bellek alanlarında olduğu gibi sistem reboot edilene kadar yaşamaya devam etmektedir (kernel persistent). Bu nesneyi reboot etmeden silmek için sem_unlik fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int sem_unlink(const char *name); Fonksiyon isimli semaphore nesnesinin ismini parametre olarak alır. Başarı durumunda 0 değerine başarısızlık durumunda -1 değerine geri döner. Bu fonksiyonları kullanırken "librt" ve "libpthread" kütüphanelerini link aşamasına dahil etmelisiniz. Derleme aşağıdaki gibi yapılmalıdr: gcc -Wall -o sample sample.c -lrt -lpthread Aşağıda UNIX/Linux sistemlerinde üretici-tüketici probleminin proseslerarası kullanımına bir örnek verilmiştir. Bu örnekte "producer" ve "consumer" isimli iki program vardır. producer programı üretici, consumer programı ise tüketici programdır. Programlarda bir tane paylaşılan bellek alanı iki tane de proseslerarası kullanılabilen isimli semaphore nesnesi oluşturulmuştur. Buradaki programların hangisinin önce çalıştırıdığının bir önemi yoktur. Çünkü ilk çalıştırılan program nesneleri yaratmakta sonra çalıştırılan program yaratılmış olanları açmaktadır. Her iki programda da nesneler yok edilmeye çalışılmıştır. Ancak bu nesneleri diğer program yok etmişse bu durum normal karşılanıp bir hata rapor edilmemiştir. Örneğimizde kuyruk sistemi paylaşılan bellek alanında oluşturulmuştur. Programları aşağıdaki gibi derleyebilirsiniz: $ gcc -o producer producer.c -lrt -lpthread $ gcc -o consumer consumer.c -lrt -lpthread Programları farklı terminallerden çalıştırmalısınız. * Örnek 1, /* producer.c */ #include #include #include #include #include #include #include #include #include #include #include #define SHARED_MEM_NAME "/sample_shared_memory_name" #define SEM_PRODUCER_NAME "/sample_mutex_producer_name" #define SEM_CONSUMER_NAME "/sample_mutex_consumer_name" #define QUEUE_BUFFER_SIZE 10 void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int eno); struct SHARED_OBJECT { int qbuf[QUEUE_BUFFER_SIZE]; size_t head; size_t tail; }; int main(void) { int fdshm; sem_t *sem_producer; sem_t *sem_consumer; void *shmaddr; struct SHARED_OBJECT *so; int val; srand(time(NULL)); if ((fdshm = shm_open(SHARED_MEM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, 4096) == -1) { perror("ftruncate"); goto EXIT1; } if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) { perror("mmap"); goto EXIT2; } if ((sem_producer = sem_open(SEM_PRODUCER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, QUEUE_BUFFER_SIZE)) == NULL) { perror("sem_open"); goto EXIT3; } if ((sem_consumer = sem_open(SEM_CONSUMER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, 0)) == NULL) { perror("sem_open"); goto EXIT4; } so = (struct SHARED_OBJECT *)shmaddr; so->head = 0; so->tail = 0; val = 0; for (;;) { usleep(rand() % 300000); if (sem_wait(sem_producer) == -1) { perror("sem_wait"); goto EXIT5; } so->qbuf[so->tail++] = val; if (sem_post(sem_consumer) == -1) { perror("sem_wait"); goto EXIT5; } so->tail = so->tail % QUEUE_BUFFER_SIZE; if (val == 99) break; ++val; } EXIT5: sem_destroy(sem_consumer); if (sem_unlink(SEM_CONSUMER_NAME) == -1 && errno != ENOENT) exit_sys("sem_unlink"); EXIT4: sem_destroy(sem_producer); if (sem_unlink(SEM_PRODUCER_NAME) == -1 && errno != ENOENT) exit_sys("sem_unlink"); EXIT3: if (munmap(shmaddr, 4096) == -1) exit_sys("munmap"); EXIT2: close(fdshm); EXIT1: if (shm_unlink(SHARED_MEM_NAME) == -1 && errno != ENOENT) 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.c */ #include #include #include #include #include #include #include #include #include #include #include #define SHARED_MEM_NAME "/sample_shared_memory_name" #define SEM_PRODUCER_NAME "/sample_mutex_producer_name" #define SEM_CONSUMER_NAME "/sample_mutex_consumer_name" #define QUEUE_BUFFER_SIZE 10 void exit_sys(const char* msg); void exit_sys_errno(const char *msg, int eno); struct SHARED_OBJECT { int qbuf[QUEUE_BUFFER_SIZE]; size_t head; size_t tail; }; int main(void) { int fdshm; sem_t *sem_producer; sem_t *sem_consumer; void *shmaddr; struct SHARED_OBJECT *so; int val; srand(time(NULL)); if ((fdshm = shm_open(SHARED_MEM_NAME, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) exit_sys("shm_open"); if (ftruncate(fdshm, 4096) == -1) { perror("ftruncate"); goto EXIT1; } if ((shmaddr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fdshm, 0)) == MAP_FAILED) { perror("mmap"); goto EXIT2; } if ((sem_producer = sem_open(SEM_PRODUCER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, QUEUE_BUFFER_SIZE)) == NULL) { perror("sem_open"); goto EXIT3; } if ((sem_consumer = sem_open(SEM_CONSUMER_NAME, O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH, 0)) == NULL) { perror("sem_open"); goto EXIT4; } so = (struct SHARED_OBJECT *)shmaddr; for (;;) { if (sem_wait(sem_consumer) == -1) { perror("sem_wait"); goto EXIT5; } val = so->qbuf[so->head++]; if (sem_post(sem_producer) == -1) { perror("sem_wait"); goto EXIT5; } so->head = so->head % QUEUE_BUFFER_SIZE; usleep(rand() % 300000); printf("%d ", val); fflush(stdout); if (val == 99) break; } putchar('\n'); EXIT5: sem_destroy(sem_consumer); if (sem_unlink(SEM_CONSUMER_NAME) == -1 && errno != ENOENT) exit_sys("sem_unlink"); EXIT4: sem_destroy(sem_producer); if (sem_unlink(SEM_PRODUCER_NAME) == -1 && errno != ENOENT) exit_sys("sem_unlink"); EXIT3: if (munmap(shmaddr, 4096) == -1) exit_sys("munmap"); EXIT2: close(fdshm); EXIT1: if (shm_unlink(SHARED_MEM_NAME) == -1 && errno != ENOENT) 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); } UNIX/Linux sistemlerinde de okuma-yazma kilitlerinin temel kullanım biçimi benzerdir. İşlemler şöyle yürütülmektedir: -> Okuma yazma kilitleri pthread_rwlock_t türüyle temsil edilmektedir. Programcı bu türden global bir nesne tanımlar. Nesneye ilkdeğer vermenin iki yolu vardır. Birincisi statik düzeyde PTHREAD_RWLOCK_INITIALIZER isimli maroyu kullanmaktadır. Örneğin: #include pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER; İkincisi ise pthread_rwlock_init fonksiyonunu kullanmaktır. Örneğin: pthread_rwlock_t g_rwlock; ... int pthread_rwlock_init(&g_rwlock); pthread_rwlock_init fonksiyonunun prototipi şöyledir: int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); Fonksiyonun birinci parametresi ilkdeğer verilecek pthread_rwlock_t nesnesinin adresini almaktadır. İkinci parametre reader-writer lock nesnesinin özelliklerinin belirtildiği pthread_rwlockattr_t türünden nesnenin adresini almaktadır. İkinci parametre NULL geçilebilir. Bu durumda nesne default özelliklerle yaratılmaktadır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. -> Okuma amaçlı kritik kod şöyle oluşturulmaktadır: pthread_rwlock_rdlock(&g_rwlock); ... ... ... pthread_rwlock_unlock(&g_rwlock); Yazma amaçlı kritik kod ise şöyle oluşturulmaktadır: pthread_rwlock_wrlock(&g_rwlock); ... ... ... pthread_rwlock_unlock(&g_rwlock); Fonksiyonların prototipleri şöyledir: #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 pthread_rwlock_t türünden nesnenin adresini almaktadır. Genel çalışma biçimi yukarıda Windows sistemlerinde açıkladığımız gibidir. Burada Windows sistemlerinden farklı olarak unlock işlemi için tek fonksiyon bulunmaktadır. Yani kilit okuma amaçlı da alınsa, yazma amaçlı da alınsa kilidin bırakılması pthread_rwlock_unlock fonksiyonu ile yapılmaktadır. Fonksiyonlar başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. -> Programcı reader-writer lock nesnesi ile işini bitirsikten sonra pthread_rwlock_destroy fonksiyonu ile nesneyi yok edilmelidir. Fonksiyonun prototipi şöyledir: #include int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); Fonksiyon yine pthread_rwlock_t nesnesinin adresini parametre olarak alır, başarı durumunda sıfır değerine başrısızlık durumunda ise errno değerine geri döner. Aşağıda UNIX/Linux sistemlerinde reader-writer lock nesnelerinin kullanımına örnek verilmiştir. Bu örnek aslında yukarıda Windows sistemeri için yaptiğimız örneğin UNIX/linux versiyonu gibidir. * Örnek 1, #include #include #include #include #include #include #define NTHREADS 10 void *thread_proc(void *param); void read_proc(const char *ptname); void write_proc(const char *ptname); void exit_sys_errno(const char *msg, int eno); pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER; int main(void) { pthread_t tids[NTHREADS]; char tname[32], *ptname; int result; srand(time(NULL)); for (int i = 0; i < NTHREADS; ++i) { sprintf(tname, "Thread-%d", i + 1); if ((ptname = strdup(tname)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } if ((result = pthread_create(&tids[i], NULL, thread_proc, ptname)) != 0) exit_sys_errno("pthread_create", result); } for (int i = 0; i < NTHREADS; ++i) pthread_join(tids[i], NULL); if ((result = pthread_rwlock_destroy(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_destroy", result); return 0; } void *thread_proc(void *param) { const char *ptname = (const char *)param; for (int i = 0; i < 10; ++i) { usleep(rand() % 100000); if (rand() % 2 == 0) read_proc(ptname); else write_proc(ptname); } return NULL; } void read_proc(const char *ptname) { int result; if ((result = pthread_rwlock_rdlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_rdlock", result); printf("%s thread begins reading...\n", ptname); usleep(rand() % 300000); printf("%s thread ends reading...\n", ptname); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_unlock", result); } void write_proc(const char *ptname) { int result; if ((result = pthread_rwlock_wrlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_wrlock", result); printf("%s thread begins writing...\n", ptname); usleep(rand() % 300000); printf("%s thread ends writing...\n", ptname); if ((result = pthread_rwlock_unlock(&g_rwlock)) != 0) exit_sys_errno("pthread_rwlock_unlock", result); } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } UNIX/Linux sistemlerinde ismine "koşul değişkenleri (condition variables)" denilen önemli bir senkronizasyon nesnesi vardır. Koşul değişkenleri eskiden Windows sistemlerinde yoktu. Ancak belli bir zamandan sonra Windows sistemlerine de sokuldu. Windows'ta event nesneleri olduğu için koşul değişkenleri yerine bu nesneler onların görevlerini tam olmasa da yaklaşık olarak yerine getirebiliyordu Ancak yukarıda da belirttiğimiz gibi daha sonraları Windows sistemlerine de koşul değişkenleri eklenmiştir. Koşul değişkenlerinin amacı bir koşul sağlanmadığı sürece thread'i blokede bekletmek, koşul sağlanınca thread'in blokesini çözmektir. Ancak koşul değişkenlerinin kullanılması ve çalışma biçiminin anlaşılması şimdiye kadar gördüğümüz senkronizasyon nesnelerine göre daha zordur. UNIX/Linux sistemlerinde koşul değişkenleri tipik olarak aşağıdaki adımlardan geçilerek kullanılmaktadır: -> UNIX/Linux sistemlerinde koşul değişkenleri tek başlarına kullanılmamaktadır. Mutex nesneleri ile birlikte kullanılmaktadır. Koşul değişkenleri pthread_cond_t türüyle temsil edilmektedir. Programcı koşul değişkenlerini yine global düzeyde bir mutex ile birlikte tanımlar. Koşul değişkenlerine ilkdeğer verilmesi statik düzeyde PTHREAD_COND_INITIALIZER makrosuyle yapılabilir. Örneğin: pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; Kooşul değişkenlerine ilkdeğer vermek için pthread_cond_init fonksiyonu da kullanılabilmektedir. Fonksiyonun prototipi şöyledir: #include int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); Fonksiyonun birinci parametresi pthread_cond_t nesnesini adresini, ikinci parametresi koşul değişkeninin özelliklerin belirtildiği pthread_condattr_t nesnesinin adresini almaktadır. Biz bu kursta koşul değişkenlerinin özelliklerini ele almayacağız. Bu ikinci parametreyi NULL olarak geçebilirsiniz. Bu durumda koşul değişkenleri default özelliklerle yaratılmaktadır. Zaten PTHREAD_COND_INITIALIZER makrosu da koşul değişkenlerini default özelliklerle yaratmaktadır. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda errno değerine geri dönmektedir. -> Koşul değişkenleri ile koşul sağlanana kadar bekleme yapmak için pthread_cond_wait isimli fonksiyon kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); Fonksiyonun birinci parametresi koşul değişkeninin adresini, ikinci parametresi mutex nesnesinin adresini almaktadır. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedir. Örneğin: pthread_cond_wait(&g_cond, &g_mutex); Ancak koşul sağlanana kadar bekleme böyle yapılmamaktadır. Belli bir kalıba uygun biçimde bu fonksiyon çağrılmalıdır. Koşul değişkenini bekleme kalıbı aşağıdaki gibidir: pthread_mutex_lock(&g_mutex); while (koşul_sağlanmadığı_sürece) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada dikkat edilmesi gereken durumlar şunlardır: pthread_cond_wait bir döngü içerisinde çağrılmalıdır. Döngünün aşağıdaki gibi oluşturulduğuna dikkat ediniz: while (koşul_sağlanmadığı_sürece) pthread_cond_wait(&g_cond, &g_mutex); Burada döngünün içerisindeki koşul "koşulun sağlanmadığı sürece uykuda kalınmasına ilişkin" koşuldur. Yani bu koşul sağlanmadığı zaman thread uyandırılacaktır. Örneğin: while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); Burada g_flag değişkeni sıfır olduğu sürece thread blokede bekleyecektir. Yani blokenin çözülmesi için g_flag değişkeninin 0'dan farkllı bir değerde olması gerekmektedir. Yukarıdaki kalıptaki diğer önemli bir nokta da koşul döngüsüne girmeden mutex nesnesinin sahipliğinin alındığı ve çıkışta da sahipliğin bırakıldığıdır. Yukarıdaki kalıbın ne anlama geldiği izleyen paragraflarda açıklanacaktır. -> Koşul değişkenini bekleyen thread'i uyandırmak için iki POSIX fonksiyonu bulunmaktadır: pthread_cond_signal ve pthread_cond_broadcast. pthread_cond_signal fonksiyonu koşul değişkeninde bekleyen tek bir thread'i uyandırma amacındadır. pthread_cond_broadcast ise koşul değişkeninde bekleyen tüm thread'leri uyandırma amacındadır. pthread_cond_signal fonksiyonu aslında tek bir thread'i uyandırmak istemektedir. Ancak işletim sistemlerinde bu durum her zaman mümkün olamadığı için istemeden birden fazla thread de uyandırılabilmektedir. Bu duruma "yanlış uyanma (spurious wakeup)" denilmektedir. Bu iki uyandırma biçimini şu örneğe benzetebiliriz: Bir odada 10 kişi uyuyor olsun. Siz de yalnızca Ahmet'i uyandırmak isteyin. Ancak yaptığınız gürültüden istemeden Mehmet ve Selami de uyanmış olabilir. İşte Mehmet ve Selami'nin uyanması "yanlış uyanma (spurious wakeup)" durumudur. Ancak siz bir megafonla bağırarak odadaki herkesi de uyandırabilirsiniz. Bu işlem pthread_cond_broadcast işlemine benzemektedir. Şimdi siz "yanlışlıkla uyandırma (spurious wakeup)" gibi bir sürecin nasıl olup da yüksek teknoloji gereken işletim sistemlerinde söz konusu olabildiğini merak edebilirsiniz. İşte işletim sistemlerinin etkin tasarımında mecburen böylesi durumlar oluşabilmektedir. pthread_cond_signal fonksiyonunun koşul değişkeninde bekleyen bir thread'i değil "en az bir thread'i" uyandırdığına dikkat ediniz. Ayrıca yanlış uyandırmanın yalnızca pthread_cond_signal yoluyla değil başka nedenlerden dolayı da oluşabileceğini belirtmek istiyoruz. Koşul değişkeninde bekleyen bir thread'in pthread_cond_signal ya da pthread_cond_broadcast fonksiyonu ile uyandırılması koşulun sağlandığı anlamına gelmemektedir. Uyanan thread'ler koşulu test etmeli, koşul sağlanmıyorsa yeniden uykuya dalacak biçimde hareket etmelidir. O halde pthread_cond_signal ya da pthread_cond_broadcast uygulanmadan önce bu fonksiyonları uygulayan kitarafın koşulun sağlanmasına yol açması gerekir. Aşağıdaki beklemeye dikkat ediniz: pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada diğer bir thread g_flag değişkenini değiştrmeden pthread_cond_signal ya da pthread_cond_broadcast uygularsa thrad uyandırılıp pthread_cond_wait fonksiyonundan çıksa bile koşul sağlanmadığından dolayı yeniden uykuya dalacaktır. Tipik olarak programcı pthread_cond_signal ya da pthread_cond_broadcast uygulamadan önce mutex nesnesinin sahipliğini almalı koşulu kritik kod içerisinde oluşturmalı, bu fonksiyonları çağırdıktan sonra mutex nesnesinin sahipliğini bırakmalıdır. Yani tipik uygulama şöyle olmalıdır: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_cond_broadcast(&g_cond); pthread_mutex_unlock(&g_mutex); pthread_cond_signal ve pthread_cond_broadcast fonksiyonlarının prototipleri şöyledir: #include int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); Fonksiyonlar koşul değişken nesnesinin adresini paranetre olarak almakta, başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönmektedirler. -> Koşul değişkeninin ve mutex nesnesinin kullanımı bittiğinde bunlar pthread_cond_destroy ve pthread_mutex_destroy fonksiyonuyla yok edilmelidir. pthread_cond_destroy fonksiyonunun prototipi şöyledir: #include int pthread_cond_destroy(pthread_cond_t *cond); Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda errno değerine geri dönmektedir. Aslında genel olarak pthread_cond_destroy ve pthread_mutex_destroy fonksiyonları pek çok kütüphanede bir şey yapmamaktadır. Ancak yine de bu fonksiyonların uyumluluk bakımından çağrılması gerekir. Aşağıda koşul değişkenlerinin kullanımına tipik bir örnek verilmiştir. Örneğimizde iki thread g_flag değişkeninin sıfır dışı bir değer olmasını beklemektedir. Ana thread'te g_flag değişkeni 1 değerine set edilip pthread_broadcast işlemi uygulandığında bekleyen iki thread de uyanmaktadır. * Örnek 1, #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); 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_flag = 0; int main(void) { 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); printf("press ENTER to continue...\n"); getchar(); if ((result = pthread_mutex_lock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lock", result); g_flag = 1; if ((result = pthread_cond_broadcast(&g_cond)) != 0) exit_sys_errno("pthread_cond_broadcast", result); if ((result = pthread_mutex_unlock(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_lunock", 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_cond_destroy(&g_cond)) != 0) exit_sys_errno("pthread_cond_destroy", result); if ((result = pthread_mutex_destroy(&g_mutex)) != 0) exit_sys_errno("pthread_mutex_destroy", result); return 0; } void *thread_proc1(void *param) { int result; printf("thread-1 is waiting at the condition variable..\n"); 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, &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); printf("ok, thread-1 resumes...\n"); return NULL; } void *thread_proc2(void *param) { int result; printf("thread-2 is waiting at the condition variable..\n"); 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, &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); printf("ok, thread-2 resumes...\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Şimdi de koşul değişkenlerini kullanmak için belirttiğimiz kalıpların ne anlam ifade ettiği üzerinde duracağız. Bekleme işleminin mutex nesnesinin sahipliği alınarak yapılması gerektiğini belirtmiştik. Örneğin: pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada önce mutex nesnesinin sahipliğinin alındığını görüyorsunuz. pthread_cond_wait fonksiyonu uykuya dalmadan önce atomik bir biçimde mutex nesnesinin sahipliğini bırakmaktadır. Şimdi aşağıdaki koda bakalım: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_mutex_unlock(&g_mutex); pthread_cond_broadcast(&g_cond); Burada pthread_cond_broadcast uygulandığında bu koşul değişkeninde bekleyen tüm thread'ler uyanacaktır. İşte pthread_cond_wait fonksiyonu uykuya dalmadan önce mutex nesnesinin sahipliğini nasıl bırakıyorsa uyanırken de sahipliği alarak uyanmaktadır. Böylece thread uyandığında koşul eğer sağlanıyorsa döngüden çıkıp yine mutex'i sahipliği bırakacaktır. Şimdi yukarıdaki kodda g_flag değerinin zaten 1 olduğunu düşünelim. Bu durumda mutex nesnesinin sahipliği alınacak g_flag == 1 olduğu için de döngüye girilmeyecektir. Böylece mutex nesnesinin sahipliği bırakılacaktır. Pekiyi neden yukarıdaki kalıpta bir döngü kullanılmıştır. Neden döngü yerine if deyimi kullanılmamıştır? İşte bunun nedeni "yalancı uyanma (spurious wakeup)" yüzündendir. Döngü yerine aşağıdaki gibi bir if deyiminin olduğunu varsayalım: pthread_mutex_lock(&g_mutex); if (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada yanlış uyandırma oluştuğunda eğer koşul sağlanmıyorsa thread'in yeniden uykuya dalması gerekir. Bunun için de bir döngünün kullanılması gerekmektedir. Şimdi programcının koşul değişkeninde bekleyen thread'lerden yalnızca birini çözmek istediğini düşünelim. Aşağıdaki kalıp bunu yapamayacaktır: pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); pthread_mutex_unlock(&g_mutex); Burada diğer tarafın pthread_cond_signal uyguladığını kabul edelim. Bu durumda 2 thread'in de uyandırıldığını düşünelim. Bu iki thread de mutex nesnesinin sahipliğini almaya çalışacak ancak yalnızca biri alacaktır. Dolayısıyla diğeri bu mutex nesnesini bekleyecektir. Ancak mutex nesnesinin sahipliğini almış olan thread onu bıraktığında bu kez diğer thread mutex nesnesinin sahipliğini alarak koşul değişkeninden çıkacaktır. O halde programcının mutex kontrolünde yeniden koşul değişkenini eski haline getirmesi gerekmektedir. Örneğin: pthread_mutex_lock(&g_mutex); while (g_flag == 0) pthread_cond_wait(&g_cond, &g_mutex); g_flag = 0; pthread_mutex_unlock(&g_mutex); pthread_cond_signal ya da pthread_cond_broadcast uygulayan tarafın koşul değişkenlerini aşağıdaki gibi mutex kontrolü içerisinde değiştirmesi aslında mutlak anlamda gerekmemektedir: pthread_mutex_lock(&g_mutex); g_flag = 1; pthread_cond_broadcast(&g_cond); pthread_mutex_unlock(&g_mutex); Ancak koşulların birden fazla ğeğişkene bağlı olduğu durumda önce mutex kontrolü ile kritik kod içerisinde koşulların ayarlanması gerekebilmektedir. C'deki aslında basit bir atama işlemi bile çok thread'li ortamda senkronizasyon sorunlarına yol açabilmektedir. UNIX/Linux sistemlerinde durum değişkenleri daha çok aynı prosesin thread'leri arasında kullanılıyor olsa da aslında prosesler arasında da kullanılabilmektedir. Tabii bu durumda koşul değişkeninin ve mutex nesnesinin paylaşılan bellek alanı üzerinde yaratılması ve yaratım sırasında da attribute nesneleri ile prosesler arası kullanım durumunun set edilmesi gerekmektedir. Bu işlem mutex nesnelerindekine benzer biçimde yapılmaktadır: pthread_cond_t g_cond; ... pthread_condattr_t attr; pthread_condattr_init(&attr); pthread_condattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); pthread_cond_init(&g_cond, &attr); pthread_condattr_destroy(&attr); Bir proses sonlandığında prosesin bütün thread'leri de sonlandırılmaktadır. Örneğin biz exit fonksiyonunu çağırdığımızda exit fonksiyonu tüm prosesi sonlandıracağı için bütün thread'lerde sonlanacaktır. C' de main fonksiyonu bittiğinde exit fonksiyonu ile prosesin sonlandırıldığını anımsayınız. Bu durumda main fonksiyonu biterse prosesin tüm thread'leri de sonlanacaktır. Thread'ler konusuna yeni başlayan kişiler bu hatayı çok sık yapmaktadır. Aşağıdaki örnekte main fonksiyonunda bir thread yaratılmış ancak main fonksiyonu hemen sonlanmıştır. Bu durumda yaratılmış olan thread de tüm program da sonlanacaktır. * Örnek 1, #include #include #include #include #include void *thread_proc(void *param); void exit_sys_errno(const char *msg, int eno); int main(void) { pthread_t tid; int result; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_sys_errno("pthread_create", result); /* dikkat! main bitince yaratılmış olan thread de sonlandırılacaktır */ return 0; } void *thread_proc(void *param) { for (int i = 0; i < 10; ++i) { printf("other thread: %d\n", i); if (i == 5) pthread_exit(NULL); sleep(1); } return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } Şimdi de "thread" ler ile ilgili diğer noktalara değinelim: >> Bir thread'in çalışmasına ara verilmesi preemptive bir biçimde donanım kesmeleri yoluyla yapılmaktadır. İşlemci bir makine komutunu çalıştırırken kesme oluşsa bile makine komutunun çalışması bitmeden kesme konumuna geçmez. Yani iki komut arasında kesmeleri işleme sokmaktadır. Thread'ler dünyasında bir işlemin "atomik (atomic)" yapılması "kesilmeden yapılması" anlamına gelmektedir. O halde makine komutları atomiktir. Aşağıdaki işleme bakınız: ++g_count; Böyle bir kod genellikle derleyiciler tarafından tek bir makine komutuyla değil birkaç makine komutuyla yapılmaktadır. Örneğin Intel işlemcilerinde bu işlem için derleyici aşağıdaki gibi üç makine komutu üretebilmektedir: MOV EAX, g_count INC EAX MOV g_count, EAX İşte komutlar arasında thread'ler arası geçiş (context switch) oluşursa daha önce ele aldığımız senkronizasyon sorunları oluşacaktır. Aslında ++g_count işlemi bazı işlemcilerde tek bir makine komutuyla da yapılabilmektedir. Örneğin bu işlem Intel işlemcilerinde aşağıdaki gibi tek bir makine komutuyla da yapılabilmektedir: INC g_count Makine komutları atomik olduğuna göre bu işlem çok thread'li ortamlarda tamamen güvenli midir? İşte makine komutları arasında thread'ler arası geçiş oluşmamakla birlikte çok işlemcili ya da çok çekirdekli sistemlerde başka bir sorun da ortaya çıkmaktadır. İşlemcilerin bazılarında birden fazla çekirdek aynı bellek adresine erişirken oradaki bilgiyi bozabilmektedir. Bunun donanımsal olarak nasıl gerçekleştiği üzerinde burada durmayacağız. Böyle bir olasılık düşük olsa da maalesef yine de bulunmaktadır. Yine bir çekirdek bir bellek adresine bir değer yazarken diğer bir çekirdek oradan değer okumak istediğinde okuyan taraf önceki ya da sonraki değeri değil bozuk bir değeri de okuyabilmektedir. Bu durumda maalesef tek bir değişken bile iki thread arasında anlık kullanılacak olsa yine de bu işlemin senkronize edilmesi gerekmektedir. Ancak bir tek değişkene bir değer atamak için mutex gibi bir senkronizasyon nesnesini kullanmak performansı düşürmektedir. Aslında işlemcileri tasarlayanlar bunun çaresini de düşünmüşlerdir. Bir makine komutunun diğer çekirdekleri ya da işlemcileri o komutluk durdurarak yalnızca o komutu çalıştıran işlemci ya da çekirdeğin belleğe erişmesi sağlanabilmektedir. Örneğin Intel işlemcilerinde bir makine komutunun başına LOCK öneki getirilirse işlemci o komutu diğer işlemcileri durdurarak çalıştırmaktadır: LOCK INC g_count Bu işlem artık Intel işlemcilerinde çok çekirdekli sistemlerde de sorunu çözecektir. Tabii buradaki LOCK öneki performansı da düşürmektedir. Derleyicinin her komutta bu öneki kullanması kodun yavaş çalışmasına yol açacaktır. Pekyi biz C programcısı olarak derleyicinin böyle bir kod üretmesini nasıl sağlayabiliriz? Windows sistemlerinde diğer işlemcileri bir komutluk durdurarak bellek işlemi yapabilmek için tasarlanmış başı Interlocked ile başlayan InterlockedXXX biçiminde API fonksiyonları vardır. Tabii bu fonksiyonların içi sembolik makine dilinde yazılmıştır. Önemli birkaç Interlocked fonksiyonlarının prototiplerini aşağıda veriyoruz: LONG InterlockedIncrement(LONG volatile *Addend); LONG InterlockedAdd(LONG volatile *Addend, LONG Value); LONG InterlockedDecrement(LONG volatile *Addend); LONG InterlockedExchange(LONG volatile *Target, LONG Value); Aslında çok fazla Interlocked fonksiyon bulunmaktadır. Bunların listesi için aşağıdaki MSDN dokümanlarına başvurabilirsiniz: https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-functions Yukarıdaki fonksiyonlar long türden nesnelerin adreslerini parametre olarak almaktadır. Interlocked fonksiyonlarının genellikle tek bir makine komutuyla yapılabilecek işlemler için bulundurulduğuna dikkat ediniz. Aşağıdaki örnekte iki thread de aynı global değişkeni bir milyon kez artırmıştır. Ancak bu işlemi yukarıda görmüş olduğumuz InterllockedIncrement fonksiyonuyla yapmıştır. Bu fonksiyon bir nesneyi atomik olarak tek bir makine komutuyla diğer işlemcileri durdurarak artırmaktadır. * Örnek 1, #include #include #include void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); long g_count; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); printf("%ld\n", g_count); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { long i; for (i = 0; i < 1000000; ++i) InterlockedIncrement(&g_count); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { long i; for (i = 0; i < 1000000; ++i) InterlockedIncrement(&g_count); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >> "built in" ve "__atomic_xxx" Fonksiyonlar : Derleyiciler tarafından doğrudan tanınan çağrılması için prototipe gereksinim duyulmayan ve genellikle derleyicilerin CALL makine komutu yerine inline fonksiyon gibi açım yaptığı özel fonksiyonlara "built-in" ya da "intrinsic" fonksiyon denilmektedir. C derleyicilerinin bir bölümü bazı standart C fonksiyonlarını ve kendilerine özgü bazı eklenti (extension) fonksiyonları bu biçimde ele almaktadır. Microsoft C derleyicilerinde, gcc ve clang derleyicilerinde "built-in" ya da "intrinsic" fonksiyonlar bulunmaktadır. Microsoft C derleyicilerinin "intrinsic" fonksiyon listesine aşağıdaki bağlantıdan ulaşabilirsiniz: https://learn.microsoft.com/en-us/cpp/intrinsics/compiler-intrinsics?view=msvc-170 gcc Derleyicilerinin built-in fonksiyonlarına da aşağıdaki bağlantılardan ulaşabilirsiniz: https://gcc.gnu.org/onlinedocs/gcc/Target-Builtins.html https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html gcc'de bazı built-in fonksiyonlar atomik işlemler için bulundurulmuştur. Bunları Microsoft'un InterlockedXXX fonksiyonlarına benzetebilirsiniz. Ancak gcc'de önceleri bu atomic built-in fonksiyonlar __sync_xxx biçiminde isimlendirilmişti. Sonra C++'a kütüphanesi eklenince C++ kütüphanesi de kullanabilsin diye bu eski __sync_xxx isimli fonksiyonlar yerine bunların "memory model" parametresi alan __atomic_xxx versiyonları oluşturuldu. Artık yeni programların __atomic_xxx biçimindeki bu yeni fonksiyonları kullanması tavsiye edilmektedir. Her iki fonksiyon grubunun dokümanlarına ilişkin bağlantıları aşağıda veriyoruz: https://gcc.gnu.org/onlinedocs/gcc-4.9.0/gcc/_005f_005fatomic-Builtins.html https://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html __atomic_xxx fonksiyonlarındaki "memory model" parametresi __ATOMIC_SEQ_CST olarak girilebilir. Biz burada bu memory model parametresinin ne anlam ifade ettiği üzerinde durmayacağız. Örneğin bir nesneyi atomic bir biçimde 1 artırmak isteyelim. Hiç mutex kullanmadan bunu gcc derleyicilerinde şöyle yapabiliriz: __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); Bir değişkene yalnızca değer atamak için aşağıdaki atomic fonksiyon kullanılabilir: void __atomic_store_n(type *ptr, type val, int memmodel); void __atomic_store(type *ptr, type *val, int memmodel); Benzer biçimde bir nesnenin içerisindeki değeri almak için de aşağıdaki atomic fonksiyonlar kullanılabilir: void __atomic_load_n (type *ptr, int memmodel); void __atomic_load (type *ptr, type *ret, int memmodel); Aşağıdaki örnekte iki thread aynı global değişkeni artırmaktadır. Biz daha önce bu örneği çeşitli senkronizasyon nesneleriyle zaten yapmıştık. Burada hiç senkronizasyon nesnesi kullanmadan gcc'nin atomic built-in fonksiyonlarıyla aynı şeyi yapıyoruz. * Örnek 1, /* atomic.c */ #include #include #include #include #define MAX_COUNT 100000000 void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); int g_count; int main(void) { 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); printf("%d\n", g_count); return 0; } void *thread_proc1(void *param) { for (int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void *thread_proc2(void *param) { for (int i = 0; i < MAX_COUNT; ++i) __atomic_fetch_add(&g_count, 1, __ATOMIC_SEQ_CST); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } C11 ile birlikte C'ye isteğe bağlı olarak _Atomic biçiminde bir tür belirleyicisi ve niteleyicisi de eklenmiştir. _Atomic ile tanımlanan nesneler için derleyiciler atomik işlem yapacak biçimde kod üretmektedir. Tabii bu _Atomic belirleyicisi atomic yapılamayack işlemlerde bir etki göstermemektedir. Bir C11 derleyicisinin _Atomic belirleyicisini destekleyip desteklemediği __STDC_NO_ATOMICS__ makrosuna bakılarak belirlenebilir. Microsoft C derleyicileri henüz _Atomic belirleyicisini desteklememektedir. Ancak gcc derleyicileri belli bir sürümden sonra bu belirleyiciyi desteklemektedir. Ayrıca C11 ile birlikte başlık dosyasında aşağıdaki gibi tür makroları da bulundurulmuştur: atomic_bool _Atomic _Bool atomic_char _Atomic char atomic_schar _Atomic signed char atomic_uchar _Atomic unsigned char atomic_short _Atomic short atomic_ushort _Atomic unsigned short atomic_int _Atomic int atomic_uint _Atomic unsigned int atomic_long _Atomic long atomic_ulong _Atomic unsigned long atomic_llong _Atomic long long ... Bunların tam listesi için standartlara başvurabilirsiniz. Yine C++11 ile birlikte atomik işlem yapan fonksiyonlar da isteğe bağlı olarak standart hale getirilmiştir. Bunların bazılarını aşağıda veriyoruz: atomic_store atomic_store_explicit atomic_load atomic_load_explicit atomic_exchange atomic_exchange_explicit atomic_compare_exchange_strong atomic_compare_exchange_strong_explicit atomic_compare_exchange_weak atomic_compare_exchange_weak_explicit atomic_fetch_add atomic_fetch_add_explicit atomic_fetch_sub atomic_fetch_sub_explicit Aşağıdaki örnekte g_count global değişkeni _Atomic ile tanımlandığı için zaten ++g_count işlemi derleyici tarafından atomik bir biçimde yapılacaktır. Başka bir deyişle derleyici bu değişken işleme sokulurken onu tek bir makine komutuyla ve lock işlemi ile işleme sokmaktadır. * Örnek 1, #include #include #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); _Atomic int g_count; int main(void) { pthread_t tid1, tid2; int result; srand(time(NULL)); 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); printf("%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_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } C++'ta C'nin _Atomic belirleyicisi yoktur. (Genel olarak C++ Programlama Dili C Programlama Dilini kapsıyor olsa da bu kapsama mükemmel düzeyde değildir.) C++'ta atomik işlemler başlık dosyasında bildirilen std::atomic isimli sınıf şabşlonuyla yapılmaktadır. Bu sınıfın operatör fonksiyonları gerçekten çok işlemcili sistemler için atomik işlemler yapmaktadır. Dolayısıyla yukarıdaki işlemlerin C++'taki eşdeğerleri aşağıdaki gibi oluşturulabilir. * Örnek 1, #include #include #include #include using namespace std; void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); atomic g_count; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); printf("%ld\n", (int)g_count); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { long i; for (i = 0; i < 1000000; ++i) ++g_count; return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { long i; for (i = 0; i < 1000000; ++i) ++g_count; return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Daha önceden de belirttiğimiz gibi Windows sistemlerindeki ReadFile ve WriteFile API fonksiyonları, UNIX/Linux sistemlerindeki read ve write POSIX fonksiyonları işletim sisteminin sistem fonksiyonlarını doğrudan çağırmaktadır. İşletim sisteminin sistem fonksiyonaları da okuma ve yazma bağlamında sistem genelinde atomiktir. Bunun anlamı şudur: Biz iki farklı thread'ten ya da prosesten aynı dosyaya, dosyanın aynı yerine aynı anda yazma ya da okuma yapsak bile iç içe geçme asla oluşmaz. Bu durum kernel tarafından senkronize edilmektedir. Örneğin thread ya da proseslerden biri tam bir dosyanın belli bir offset'ine WriteFile ya da write fonksiyonuyla 100 byte yazıyor olsun. Tam o sırada da aynı offset'ten başka bir thread ya da proses 100 byte okuyaacak olsun. Bu durumda okuyan taraf ya 100 yazılmadan önceki 100 byte'ı okur ya da diğerinin yazdığı 100 byte'ı okur. Ancak asla yarısı eski 100 byte'tan yarısı diğerinin yazdığı 100 byte'tan oluşan bir 100 byte okumaz. Benzer biçimde iki thread ya da proses WriteFile ya da write ile aynı dosyanın aynı offset'ine yazıyor olsalar bile iç içe geçme oluşmamaktadır. Nihai durumda ya birinin ya da diğerinin tam yazdığı şeyler dosyada gözükür. Tabii birden fazla read ya da write işlemi sırasında bu işlemler iç içe geçebilir. Örneğin: thread-1 -------- write(...) write(...) thread-2 --------- write(...) write(...) Burada write çapırmaları atomşktir. Ancak thread'in iki write çağrısı arasına thread2'nin write çağrıları gerebilir. >> "Thread Safety" : Bir fonksiyon farklı thread'lerden aynı anda çağrıldığında herhangi bir sorun oluşmuyorsa o fonksiyon thread güvenlidir. Yani "thread güvenlilik (thread safety)" birden fazla thread tarafından aynı anda çağrılan fonksiyonların sorun çıkartmaması anlamına gelmektedir. Pekiyi thread güvenliliği bozan faktörler nelerdir? İşte eğer fonksiyon bir "statik data" kullanıyorsa (yani global bir nesneyi ya da static yerel bir nesneyi kullanıyorsa) o fonksiyon thread güvenli olamaz. Çünkü global değişkenlerin ve static yerel değişkenlerin toplamda tek bir kopyası vardır. Thread akışları aynı fonksiyonda ilerlerken aynı kopya üzerinde işlem yaptıklarından dolayı bir anomali oluşabilecektir. Örneğin: void foo(void) { static int a = 0; ++a; ... ++a ... ++a; ... } Burada bu foo fonksiyonu farklı thread'lerden aynı anda çağrıldığında bu farklı thread'ler static nesnenin aynı kopyasını kullanacağından dolayı bir bozulma oluşacaktır. static yerel nesnelerin stack'te değil "data" ya da "bss" alanlarında yaratıldığını anımsayınız. Aynı durum global nesne kullanan fonksiyonlar için de geçerlidir. Yukarıda da belirttiğimiz gibi "thread güvenlilik (thread safety)" bir fonksiyonun farklı thread'ler tarafından aynı anda çağrıldığında sorun oluşmaması anlamına gelmektedir. Thread güvenliliği bozan en faktör "static data" kullanımıdır. Yani programın static yerel ya da global nesneleri kullanmasıdır. Örneğin: char *myitoa(int a) { static char buf[32]; sprintf(buf, "%d", a); return buf; } Burada myitoa thread güvenli değildir. Çünkü bu fonksiyon birden fazla thread tarafından aynı anda (iç içe geçecek biçimde) çağrılırsa sorun oluşacaktır. Çünkü iki çağrının kullandığı yerel nesne aynı nesnedir. Pekiyi fonksiyonu "thread güvensiz" yapan şey nedir? Eğer bir fonksiyon statik veri kullanıyorsa (yani static yerel nesneler ya da global nesneler) fonksiyon thread güvenli olmaz. Ortak kaynakları kullanan fonksiyonlar thread güvenli değildir. Mademki fonksiyonu thread güvenli olmaktan çıkartan faktör fonksiyonun statik veri kullanmasıdır. O halde biz fonksiyonu static data kullanmaktan çıkartırsak thread güvenli hale getirmiş oluruz. Örneğin: char *myitoa_tsafe(char *buf, int a) { sprintf(str, "%d", a); return buf; } Burada myitoa fonksiyonu artık static yerel bir dizinin adresiyle geri dönmemektedir. Ona geçirilen adresle geri dönmektedir. Örnek bir kullanım şöyle olabilir: char s[100]; myitoa_tsafe(s, 123456) Burada myitoa_tsafe fonksiyonu artık farklı thread'ler tarafından aynı anda çağrılsa bile bir sorun oluşmayacaktır. (Tabii burada s dizisinin yerel bir dizi olduğunu kabul ediyoruz.) C'nin de bazı standartc fonksiyonları static yerel nesne ya da dizi kullanmaktadır. Dolayısıyla bu fonksiyonlar thread güvenli değildir. Örneğin localtime fonksiyonu thread güvenli değildir. local time fonksiyonunun parametrik yapısını anımsayınız: struct tm *localtime(const time_t *timep); Burada localtime fonksiyonu struct tm türünden static yerel bir nesnenin adresiyle geri dönmektedir. struct tm *localtime(const time_t *timep) { static struct tm lt; .... return < } Görüldüğü gibi fonksiyon aslında bize bir adres vermektedir, ancak bu adres static yerel bir nesnenin adresidir. Dolayısıyla onun tek bir kopyası vardır. Biz bu fonksiyonu farklı thread'lerden aynı anda çağırırsak aslında aynı nesne üzerinde işlem yapılacağı için bu nesnenin içeriği bozulacaktır. Benzer biçimde ctime fonksiyonu da static yerel bir dizinin adresiyle dönmektedir. Bu fonksiyon da thread güvenli değildir. Benzer biçimde rassal sayı üretmekte kullanılan rand fonksiyonu da global bir nesne (tohum nesnesi) kullanmaktadır. Dolayısıyla bu fonksiyon da thread güvenli değildir. Aşağıda C'de thread güvenli olmama potansiyelinde olan standart C fonksiyonlarının listesini veriyoruz: strtok strerror ctime gmtime localtime asctime rand srand tmpnam tempnam setlocal Pekiyi thread güvenli olmayan bir fonksiyonu nasıl thread güvenli hale getiririz? İlk yapılacak şey şüphesiz fonksiyonun static data (statik yerel ya da global nesneleri kastediyoruz) kullanmasını engellemektir. Static data kullanan programlar thread güvenli olamazlar. Bu durumda fonksiyonu thread güvenli hale getirmek için yapılacak şey onun static data kullanmasını engellemektir. Pekiyi C'nin standart kütüphanesindeki bazı fonksiyonlar thread güvenli doğaya sahip değilse ne yapabiliriz? Bu fonksiyonlar izleyen paragraflarda ele alacağımız gibi "thread'e özgü global alanlar" oluşturularak kütüphaleri yazanlar tarafından thread güvenli hale getirilebilirler. Çalıştığınız C derleyicisinin standart kütüphanesinin thrad güvenli olup olmadığını öğrenmelisiniz. C'nin 2011 versiyonuna kadar (C11) C standartlarında thread lafı edilmemişti. Ancak ilk kez C11'de thread kavramı standartlara sokuldu ve isteğe bağlı (optional) bir thread kütüphanesi de standartla eklendir. Ancak bu standartlar da yukarıda belirttiğimiz fonksiyonların thread güvenli olup olmadığı konusunda bir açıklama yapmamıştır. Yani özet olarak bir C derleyicisindeki yukarıdakine benzer standart C fonksiyonları thread gğvenli olabilir ya da olmayabilir. Microsoft 2005 yılına kadar standart C kütüphanesinin thread güvenli olan ve thread güvenli olmayan iki farklı versiyonunu bulundurmaktaydı. Ancak 2005'ten itibaren tek bir standart C kütüphanesi bulundurmaya başlamıştır. O da thread güvenli kütüphanedir. POSIX sistemlerinde static data kullanan sorunlu standart C fonksiyonlarının hepsinin xxxxx_r isimli thread güvenli bir versiyonu da bulundurulmuştur. Bu versiyonlar genel olarak static data kullanmak yerine ekstra bir parametre ile static data için kullanılacak alanı fonksiyonu çağırandan istemeketdir. Örneğin localtime ve localtime_r fonksiyonlarının prototipleri şöyledir: struct tm *localtime(const time_t *timep); struct tm *localtime_r(const time_t *timep, struct tm *result); localtime_r fonksiyonu static yerel nesne kullanmamakta parametre olarak alınan struct tm nesnesine yerleştirme yapmaktadır. Tabii fonksiyon parametresiyle aldığı nesnenin adresine geri dönmektedir. Örneğin, rand ve rand_r fonksiyonlarının prototipleri şöyledir: int rand(void); int rand_r(unsigned int *seedp); rand_r fonksiyonunun global tohum değişkeni kullanmadığına tohum değişkenini parametre olarak aldığına dikkat ediniz. Aşağıdaki örnekte ctime fonksiyonun thread güvenli versiyonu olan ctime_r fonksiyonu kullanılmıştır. * Örnek 1, #include #include #include #include #include void *thread_proc1(void* param); void *thread_proc2(void* param); void foo(void); void exit_errno(const char* msg, int result); int main(void) { int result; pthread_t tid1, tid2; if ((result = pthread_create(&tid1, NULL, thread_proc1, NULL)) != 0) exit_errno("pthread_create", result); if ((result = pthread_create(&tid2, NULL, thread_proc2, NULL)) != 0) exit_errno("pthread_create", result); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; } void foo(void) { time_t t; char s[64]; t = time(NULL); ctime_r(&t, s); printf("%s", s); } void *thread_proc1(void *param) { foo(); return NULL; } void *thread_proc2(void *param) { foo(); return NULL; } void exit_errno(const char *msg, int result) { fprintf(stderr, "%s: %s\n", msg, strerror(result)); exit(EXIT_FAILURE); } C'nin standart FILE stream işlemlerinin thread güvenli olup olmadığı hakkında da bir şey söylenmemiştir. Ancak hem Microsoft C kütüphanesi hem de GNU C kütüphanesi FILE stream işlemlerini thread güveli biçimde yapmaktadır. Yani iki ayrı thread global bir stream nesnesi üzerinde işlem yapıyorsa kullanılan tampon bu fonskiyonlar tarafından kritik kodlarla zaten korunmuş durumdadır. İç içe geçme durumu oluşmamaktadır. Benzer biçimde C++'ın kürüphanesi de aynı dosya üzerinde işlem yapılırken bile thread güvenlidir. Ancak standartlar bunu garanti etmemktedir. Aşağıda Windows sistemlerinde iki thread eş zamanlı olarak aynı dosyaya yazma yapaktadır. Dosya tamponunun her açılan dosya için ayrı bir biçimde oluşturulduğunu anımsayınız. Eğer Windows'ta dosya nesneleri thread güvenli olmasaydı bu yazma işlemlerinde iç içe geçme olabilirdi. Buradaki örnekte "test.txt" dosyasının içini inceleyiniz. İç içe geçmenin olmadığını göreceksiniz. Örneğin: ... thread-1 Thread-1 Thread-1 Thread-1 Thread-1 Thread-2 Thread-2 Thread-2 Thread-2 Thread-2 Thread-2 Thread-1 Thread-1 Thread-1 Thread-1 Thread-1 Thread-1 Thread-1 ... Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, #include #include #include #include void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); FILE *g_f; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_f = fopen("test.txt", "w")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { int i; for (i = 0; i < 1000; ++i) fprintf(g_f, "Thread-1\n"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { int i; for (i = 0; i < 1000; ++i) fprintf(g_f, "Thread-2\n"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Yukarıda da belirttiğimiz gibi GNU C Kütüphanesindeki stream işlemleri de thread güvenlidir. Yukarıda vermiş olduğumuz örneğin UNIX/Linux karşılığı da aşağıdaki gibidir. #include #include #include #include void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); FILE *g_f; int main(void) { pthread_t tid1, tid2; int result; if ((g_f = fopen("test.txt", "w")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } 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; for (i = 0; i < 10000; ++i) fprintf(g_f, "Thread-1\n"); return NULL; } void *thread_proc2(void *param) { int i; for (i = 0; i < 10000; ++i) fprintf(g_f, "Thread-2\n"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } * Örnek 3, C++'ın iostream kütüphanesi de genel olarak Microsoft ve UNIX/Linux sistemlerinde thread güvenli yazılmıştır. Ancak standartlar bağlamında bunun için bir garanti bulunmamaktadır. #include #include #include #include using namespace std; fstream g_f; void thread_proc1() { for (int i = 0; i < 1000; ++i) g_f << "thread-1 running...\n"; } void thread_proc2() { for (int i = 0; i < 1000; ++i) g_f << "Thread-2 running...\n"; } int main(void) { g_f.open("test.txt", ios::out); thread t1(thread_proc1); thread t2(thread_proc2); t1.join(); t2.join(); g_f.close(); return 0; } >> C++'da C++11 ile birlikte standart bir thread kütüphanesi oluşturulmuştur. Tabii aslında bu kütüphane Windows sistemlerinde Windows API fonksiyonlarını, UNIX/Linux sistemlerinde POSIX'in pthread fonksiyonalrını kullanmaktadır. Yalnızca arayüz standart hale getirilmiştir. Aşağıda C++'da thread yaratımına bir örnek verilmiştir. * Örnek 1, #include #include #include using namespace std; class Sample { public: Sample(const char *name) : m_name(name) {} void operator()() { cout << m_name + "\n"; } private: string m_name; }; void thread_proc1() { cout << "thread-1 running...\n"; } void thread_proc2() { cout << "thread-2 running...\n";; } int main(void) { Sample s1("thread-1"), s2("thread-2"); thread t1(s1); thread t2(s2); thread t3(thread_proc1); thread t4(thread_proc1); t1.join(); t2.join(); t3.join(); t4.join(); return 0; } Genel olarak C++'ın sınıfları aynı nesne üzerinde okuma konusunda thread güvenli ancak yazma konusunda thread güvenli değildir. Fakat farklı nesneler üzerinde okuma ve yazma işlemleri thread güvenlidir. Bunların standartlarda bu anlamda thread güvenli versionları yoktur. >> Thread'e özgü global değişkenler olabilir mi? Yani örneğin iki thread akışı bir global değişkeni kullanırken aslında bunlar farklı global değişkenler olabilir mi? İşte işletim sistemleri uzun süredir bunu sağlamak için mekanizmalar bulundurmaktadır. Windows sistemlerinde thread'e özgü global değişken oluşturma mekanizmasına "Thread Local Storage (TLS)", UNIX/Linux sistemlerinde ise "Thread Specific Data (TSD)" denilmektedir. Hatta C11 ile birlikte C standartlarına _Thread_local isimli, C++11 ile C++ standartlarına thread_local isimli yer belirleyici (storage class specifier) anahtar sözcükler sokulmuştur. Yani artık C ve C++'ta işletim sisteminin API fonksiyonlarını ve POSIX fonksiyonlarını kullanmadan da thread'e özgü global dğeişkenler kullanılabilmektedir. Örneğin: _Thread_local int g_tl; Burada aslında bir g_tl nesnesi yoktur. Yaratılmış olan ve yaratılacak olan tüm thread'lerin birbirinden ayrı birer g_tl nesneleri vardır. Aşağıdaki örnekte C11 ile C'e sokulan _Thread_local belirleyicisi kullanılarak bir global değişken oluşturulmuştur. Bu global değişkenin her thread için ayrı bir kopyası bulunmaktadır. Aşağıdaki örnekte bu durum görülmektedir. * Örnek 1, #include #include #include #include void Foo(const char *str); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); _Thread_local int g_tl; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); Foo("Main Thread"); /* Main Thread: 0 */ return 0; } void Foo(const char *str) { printf("%s: %d\n", str, g_tl); } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { g_tl = 10; Sleep(5000); Foo("Thread1"); /* THread: 10 */ return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { g_tl = 20; Foo("Thread2"); /* Thread2: 20 */ return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Yukarıda da belirttiğimiz gibi aslında thread'e özgü global alanlar işletim sisteminin desteğiyle oluşturulmaktadır. C ve C++ derleyicileri de arka planda aslında izleyen paragraflarda ele alacağımız gibi işletim sisteminin bu mekanizmalarını kullanarak thread'e özgü global değişkenler oluşturabilmektedir. >> "Thread Specific Data" : >>> Windows Sistemlerinde : Windows'ta her thread için işletim sistemi TLS adı altında slotlardan oluşan bir dizi ayırmaktadır. Slotların indeksleri DWORD türüyle temsil edilmektedir. Belli bir slot indeksi her thread'te ayrı bir alan belirtir. Windows'ta TLS (Thread Local Storage) kullanımı şu adımlardan geçilerek sağlanmaktadır: -> Önce henüz thread'ler yaratılmadan TlsAlloc API fonksiyonuyla bir TLS slotu yaratılır. TlsAlloc fonksiyonunun prototipi şöyledir: DWORD TlsAlloc(void); Fonksiyon parametre almamaktadır. Geri dönüş değeri olarak TLS alanında bir slot indeksi vermektedir. Bu slot indeksi yaratılmış olan ve yaratılacak olan her thread için kullanılabilir ve farklı bir alan belirtmektedir. Programcı tipik olarak bu slot indeksini global nesnede saklar. Fonksiyon başarısızlık durumunda TLS_OUT_OF_INDEXES özel değerine geri dönmektedir. Windows sistemlerinde toplam 1086 slot bulunmaktadır. Örneğin: DWORD g_slot; ... if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES) ExitSys("TlsAlloc"); -> Artık her thread aynı slot indeksini kullanarak slota bir adres yerleştirebilir. Slot indeksleri aynı olsa da aslında bu slotlar farklı thread'lerde olduğu için her thread kendi slotunu kullanıyor olacaktır. Slota adres yerleştiren TlsSetValue fonksiyonunun prototipi şöyledir: BOOL TlsSetValue( DWORD dwTlsIndex, LPVOID lpTlsValue ); Fonksiyonun birinci parametresi TLS slot indeksini, ikinci parametresi ise slota yerleştirilecek adres belirtmektedir. Fonksiyon başarı durumunda sıfır dışı bir değer başarısızlık durumunda 0 değerine geri dönmektedir. Biz aslında malloc ile tahsisat yapıp slota tahsis ettiğimiz alanın adresini yerleştirebiliriz. Böylece tek bir slot ile istediğimiz kadar thread'e özgü global nesne oluşturmuş oluruz. Örneğin: struct THREAD_LOCAL_DATA { int a; int b; ibt c; }; struct THREAD_LOCAL_DATA *tld; ... if ((tld = (struct THREAD_LOCAL_DATA *)malloc(sizeof(struct THREAD_LOCAL_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } tld->a = 10; tld->b = 20; tld->c = 30; if (!TlsSetValue(g_slot, tld)) ExitSys("TlsSetValue"); -> Değer TLS slotundan geri almak için TLSGetValue fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: LPVOID TlsGetValue( DWORD dwTlsIndex ); Fonsiyon TLS slot indeksini parametre olarak alır ve oraya set edilmiş adresi geri dönüş değeri olarak verir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. Tabii yaratılmamış bir slot adresi geçmedikten sonra fonksiyonun başarısız olma olasılığı yoktur. Dolayısyla fonksiyonun geri dönüş değerini kontrol etmeyebilirsiniz. Örneğin: struct THREAD_LOCAL_DATA *tld; ... if ((tld = (struct THREAD_LOCAL_DATA *)TlsGetValue(g_slot)) == NULL) ExitSys("TlsGetValue"); Ancak gerçekten solota NULL adres de yerleştirilebilir. Bu durumda fonksiyon NULL adrese geri döndüğünde GetLastError değerine bakılmalıdır. Eğer GetLastError ERROR_SUCESS değerindeyse işlem başarılıdır ve gerçekten slota yerleştirilen NULL adres geri alınmıştır. Ancak GetLastError başka bir değer geri döndürürse işlemin başarısız olduğu sonucu çıkartılmalıdır. -> Slot kullanımı bittikten sonra slotun TlsFree fonksiyonu ile serbest hale getirilmesi gerekir. TlsFree fonksiyonunun prototipi şöyledir: BOOL TlsFree( DWORD dwTlsIndex ); 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. Tabii her şey doğrıu yapılmışsa fonksiyonun başarısız olma olsaılığı da yoktur. Aşağıda bir örnek verilmiştir. * Örnek 1, #include #include #include #include void Foo(const char *str); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); DWORD g_slot; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES) ExitSys("TlsAlloc"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); Foo("Main Thread"); TlsFree(g_slot); return 0; } void Foo(const char *str) { int *pVal; if ((pVal = (int *)TlsGetValue(g_slot)) == NULL && GetLastError() != ERROR_SUCCESS) ExitSys("TlsGetValue"); printf("%d\n", (int)pVal); } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { if (!TlsSetValue(g_slot, (LPVOID)10)) /* g_tl = 10 */ ExitSys("TlsSetValue"); Sleep(5000); Foo("Thread1"); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { if (!TlsSetValue(g_slot, (LPVOID)20)) /* g_tl = 20 */ ExitSys("TlsSetValue"); Foo("Thread2"); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Her ne kadar TlsSetValue ile slota yalnızca bir adres yerleştirebiliyorsak da aslında dinamik tahsisat yapıp bu alanın adresini slota yerleştirebiliriz. Böylece istediğimiz kadar çok bilgiyi thread'e özgü biçimde kullanabiliriz. Örneğin: struct THREAD_DATA { int a; int b; int c; }; ... struct THREAD_DATA *td; td = (struct THREAD_DATA *)malloc(sizeof(struct THREAD_DATA)); td->a = 10; td->b = 20; td->c = 30; TlsSetValue(g_slot, td); Aşağıda bu yöntemle birden fazla bilginin slota yerleştirilmesine ilişkin bir örnek verilmiştir. * Örnek 1, #include #include #include #include void Foo(const char *str); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); void ExitSys(LPCSTR lpszMsg); DWORD g_slot; struct THREAD_DATA { int a; int b; int c; }; int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if ((g_slot = TlsAlloc()) == TLS_OUT_OF_INDEXES) ExitSys("TlsAlloc"); if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); Foo("Main Thread"); TlsFree(g_slot); return 0; } void Foo(const char *str) { struct THREAD_DATA* td; if ((td = (struct THREAD_DATA *)TlsGetValue(g_slot)) == NULL && GetLastError() != ERROR_SUCCESS) ExitSys("TlsGetValue"); printf("%s: %d, %d, %d\n", str, td->a, td->b, td->c); } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { struct THREAD_DATA* td; if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } td->a = 10; td->b = 20; td->c = 30; if (!TlsSetValue(g_slot, td)) ExitSys("TlsSetValue"); Sleep(5000); Foo("Thread1"); free(td); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { struct THREAD_DATA* td; if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } td->a = 100; td->b = 200; td->c = 300; if (!TlsSetValue(g_slot, td)) ExitSys("TlsSetValue"); Foo("Thread2"); free(td); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Standart C kütüphanesi fonksiyonların parametrik yapılarını değiştirmeden nasıl thread güvenli hale getirilir? İşte böyle bir faaliyette bazı fonksiyonlar statik nesneleri kullanmak yerine TLS içerisindeki nesneleri kullanmalıdır. Aşağıdaki örnekte bu işlemin mantıksal olarak nasıl yapılabileceği gösterilmiştir. Bu örnekte thread güvenli kütüphane için programın başında ve sonunda, her thread'in başında ve sonunda init ve exit fonksiyonlarının çağrılması gerekmektedir. Aslında bu çağrılar programcıdan gizlenebilir. Şöyle ki: Eğer dinamik kütüphane söz konusuysa dinamik kütüphanenin bazı fonksiyonları bu tür durumlarda otomatik olarak çağrılmaktadır. İşte programcı da bu kodları aslında kendi kütüphanesinin içerine alabilmektedir. Ancak statik kütüphanelerde böyle bir callback mekanizması yoktur. Microsoft bunun için standart C kütüphanesinin statik versiyonunu yazarken mecburen sarma thread fonksiyonları kullanmıştır. _beginthreadex fonksiyonu statik standart C kütüphanesi kullanılacaksa thread yaratmak için tercih edilmelidir. Bu fonksiyon aslında arka planda CreateThread API fonksiyonunu zaten çağırmaktadır. Ancak threda yaratılmadan önce ve thread sonlanırken aşağıdaki kodda bulunan init ve exit gibi fonksiyonlar bu sarma fonksiyon tarafından çağrılır. Benzer biçimde eğer Microsoft sistemlerinde statik kütüphane kullanılıyorsa thread sonlanırken _endthreadex fonksiyonu çağrılmalıdır. Dinamik kütüphanelerde bu sarma fonksiyonların kullanılmasına gerek yoktur. Ayrıca gcc derleyicilerinde standart C fonksiyonlarının thread güvenli olmadığını bunların thread güvenli versiyonlarının farklı parametrik yapılarla statik nesne kullanmayack biçimde xxxxx_r ismiyle yazıldığını anımsayınız. * Örnek 1, /* cstdlib.h */ #ifndef CSTDLIB_H_ #define CSTDLIB_H_ #include /* Type Definitions */ typedef struct tagCSTDLIB_STATIC_DATA { char *strtok_pos; size_t rand_next; } CSTDLIB_STATIC_DATA; /* Function Prototypes */ int init_cstdlib(void); void exit_cstdlib(void); int init_csdlib_thread(void); int exit_csdlib_thread(void); char *csd_strtok(char *str, const char *delim); void csd_srand(size_t seed); int csd_rand(void); #endif /* cstdlib.c */ #include #include #include #include "cstdlib.h" static DWORD g_cstdSlot; int init_cstdlib(void) { CSTDLIB_STATIC_DATA *clib; if ((g_cstdSlot = TlsAlloc()) == TLS_OUT_OF_INDEXES) return 0; return 1; } int init_csdlib_thread(void) { CSTDLIB_STATIC_DATA *clib; if ((clib = (CSTDLIB_STATIC_DATA *)malloc(sizeof(CSTDLIB_STATIC_DATA))) == NULL) { TlsFree(g_cstdSlot); return 0; } clib->rand_next = 1; if (!TlsSetValue(g_cstdSlot, clib)) return 0; } int exit_csdlib_thread(void) { CSTDLIB_STATIC_DATA *clib; if ((clib = (CSTDLIB_STATIC_DATA *)malloc(sizeof(CSTDLIB_STATIC_DATA))) == NULL) { TlsFree(g_cstdSlot); return 0; } free(clib); } void exit_cstdlib(void) { TlsFree(g_cstdSlot); } char *csd_strtok(char *str, const char *delim) { CSTDLIB_STATIC_DATA *clib; char *beg; if ((clib = TlsGetValue(g_cstdSlot)) == NULL) return NULL; if (str != NULL) clib->strtok_pos = str; while (*clib->strtok_pos != '\0' && strchr(delim, *clib->strtok_pos) != NULL) ++clib->strtok_pos; if (*clib->strtok_pos == '\0') return NULL; beg = clib->strtok_pos; while (*clib->strtok_pos != '\0' && strchr(delim, *clib->strtok_pos) == NULL) ++clib->strtok_pos; if (*clib->strtok_pos != '\0') *clib->strtok_pos++ = '\0'; return beg; } void csd_srand(size_t seed) { CSTDLIB_STATIC_DATA *clib; if ((clib = TlsGetValue(g_cstdSlot)) == NULL) return NULL; clib->rand_next = seed; } int csd_rand(void) { CSTDLIB_STATIC_DATA *clib; if ((clib = TlsGetValue(g_cstdSlot)) == NULL) return NULL; clib->rand_next = clib->rand_next * 1103515245 + 12345; return (unsigned int)(clib->rand_next / 65536) % 32768; } /* Sample.c */ #include #include #include #include #include "cstdlib.h" void ExitSys(LPCSTR lpszMsg); DWORD __stdcall ThreadProc1(LPVOID lpvParam); DWORD __stdcall ThreadProc2(LPVOID lpvParam); int main(void) { HANDLE hThread1, hThread2; DWORD dwThreadID1, dwThreadID2; if (!init_cstdlib()) { fprintf(stderr, "cannot initialize CSD Standard C Library!..\n"); exit(EXIT_FAILURE); } if ((hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID1)) == NULL) ExitSys("CreateThread"); if ((hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID2)) == NULL) ExitSys("CreateThread"); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); exit_cstdlib(); return 0; } DWORD __stdcall ThreadProc1(LPVOID lpvParam) { char s[] = "ali, veli, selami, ayşe, fatma"; char *str; int i, val; if (!init_csdlib_thread()) { fprintf(stderr, "CSDLib initialization failed!..\n"); exit(EXIT_FAILURE); } for (str = csd_strtok(s, ", "); str != NULL; str = csd_strtok(NULL, ", ")) printf("threadproc1 --> %s\n", str); for (i = 0; i < 10; ++i) { val = csd_rand(); printf("threadproc1_rand --> %d\n", val); } exit_csdlib_thread(); return 0; } DWORD __stdcall ThreadProc2(LPVOID lpvParam) { char s[] = "adana, izmir, balikesir, muğla"; char *str; int i, val; if (!init_csdlib_thread()) { fprintf(stderr, "CSDLib initialization failed!..\n"); exit(EXIT_FAILURE); } for (str = csd_strtok(s, ", "); str != NULL; str = csd_strtok(NULL, ", ")) printf("threadproc2 --> %s\n", str); for (i = 0; i < 10; ++i) { val = csd_rand(); printf("threadproc2_rand --> %d\n", val); } exit_csdlib_thread(); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> UNIX/Linux Sistemlerinde : UNIX/Linux sistemlerinde thread'e özgü statik alanlara "Thread Specific Data (TSD)" denilmektedir. Genel kullanım biçimi Windows sistemlerindekilere oldukça benzemektedir. Sırasıyla şunlar yapılmalıdır: -> Önce pthread_key_create fonksiyonu ile TSD için bir slot yaratılır. pthread_key_create fonksiyonunu işlev olarak TlsAlloc fonksiyonuna benzetebiliriz. Fonksiyonun prototipi şöyledir: #include int pthread_key_create(pthread_key_t *key, void (*destructor)(void *)); Fonksiyonun birinci parametresi slot anahtarının yerleştirileceği pthread_key_t türünden nesnenin adresini almaktadır. Fonksiyon slot anahtar bilgisini bu nesnenin içerisine yerleştirmektedir. Tipik olarak programcı bu türden global bir nesne tanımlar. Onun adresini fonksiyona geçirir. Fonksiyonun ikinci parametresi thread sonlandığında çağrılacak callback fonksiyonun adresini almaktadır. Bu parametre NULL geçilebilir. Bu durumda sonlanma sırasında fonksiyon çağrılmaz. Fonksiyon başarı durumunda sıfır değerine başarısızlık durumunda errno değerine geri dönmektedir. -> TSD slotuna yerleştirme yapmak için pthread_setspecific slottan değer almak için ise pthread_getspecific fonksiyonları kullanılmaktadır. Fonksiyonların prototipleri şöyledir: #include int pthread_setspecific(pthread_key_t key, const void *value); void *pthread_getspecific(pthread_key_t key); pthread_setspecific fonksiyonunun birinci parametresi slotu belirten pthread_key_t türünden nesneyi ikinci parametresi ise o slota yerleştirilecek adresi almaktadır. pthread_getspecific fonksiyonu da slota yerleştirilmiş olan adres değerini vermektedir.Bu fonksiyonları Windows'taki TlsSetValue ve TlsGetValue fonksiyonlarına benzetebiliriz. pthread_setspecific fonksiyonu başarı durumunda 0 değerine başarısızlık durumunda ise errno değerine geri dönmektedir. pthread_getspecific fonksiyonu başarısızlık durumunda NULL adrese geri dönmektedir. -> Kullanım bittikten sonra elde edilen slot pthread_key_delete fonksiyonu ile isteme iade edilir. Fonksiyonun prototipi şöyledir: #include int pthread_key_delete(pthread_key_t key); Fonksiyon slota ilişkin pthread_key_t türünden nesneyi parametre olarak alır. Başarı durumunda sıfır değerine, başarısızlık durumunda errno değerine geri döner. pthread_key_create fonksiyonunun ikinci parametresine geçirilecek destructor fonksiyonunun parametrik yapısı şöyle olmalıdır: void destructor(void *value); Fonksiyona TSD alanına yerleştirilmiş olan slottaki adres parametre olarak geirilmektedir. Tipik olarak programcılar bu fonksiyonda dinamik tahsis edilen alanları free hale getirirler. Aşağıda UNIX/Linux sistemlerinde TSD kullanımına ilişkin bir örnek verilmiştir. * Örnek 1, #include #include #include #include #include void foo(const char *str); void destructor(void *value); void *thread_proc1(void *param); void *thread_proc2(void *param); void exit_sys_errno(const char *msg, int eno); struct THREAD_DATA { int a; int b; int c; }; pthread_key_t g_tsdkey; int main(void) { pthread_t tid1, tid2; int result; if ((result = pthread_key_create(&g_tsdkey, destructor)) != 0) exit_sys_errno("pthread_key_create", 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_key_delete(g_tsdkey)) != 0) exit_sys_errno("pthread_key_delete", result); return 0; } void foo(const char *str) { struct THREAD_DATA *td; if ((td = (struct THREAD_DATA *)pthread_getspecific(g_tsdkey)) == NULL) { fprintf(stderr, "cannot get key value!..\n"); exit(EXIT_FAILURE); } printf("%s: %d, %d, %d\n", str, td->a, td->b, td->c); } void destructor(void *value) { free(value); } void *thread_proc1(void *param) { struct THREAD_DATA* td; int result; if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } td->a = 10; td->b = 20; td->c = 30; if ((result = pthread_setspecific(g_tsdkey, td)) != 0) exit_sys_errno("pthread_setspecific", result); sleep(5); foo("Thread1"); return NULL; } void *thread_proc2(void *param) { struct THREAD_DATA* td; int result; if ((td = (struct THREAD_DATA*)malloc(sizeof(struct THREAD_DATA))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } td->a = 100; td->b = 200; td->c = 300; if ((result = pthread_setspecific(g_tsdkey, td)) != 0) exit_sys_errno("pthread_setspecific", result); foo("Thread2"); return NULL; } void exit_sys_errno(const char *msg, int eno) { fprintf(stderr, "%s: %s\n", msg, strerror(eno)); exit(EXIT_FAILURE); } >> İşletim Sistemlerinde Kullanılan Çizelgeleme Teknikleri : Bugün işletim sistemlerinde en çok kullanılan çizelgeleme tekniği öncelik dereceleri dikkate alınarak uygulanan "döngüsel çizelgeleme (round robin scheduling)" denilen tekniktir. Döngüsel çizelgelemede her thread sırasıyla çalıştırılmaktadır. Ancak bu sistemin işletim sistemine özgü ayrıntıları vardır. Döngüsel çizelgelemede işletim sistemi thread'lere öncelik derecelerini de dikkate alarak quanta süreleri atar. Quanta süresini dolduran thread'lerin quanta süreleri yeniden doldurulmadan önce çalışma kuyruğundaki tüm thread'lerin quanta sürelerinin bitmesi beklenmektedir. Tabii bir thread çalışmaya başladığında bloke olabileceği için quanta süresini sonuna kadar kullanamayabilmektedir. İşletim sistemi her thread'in quanta süresinden harcağı zamanı tutmaktadır. Örneğin işletim sisteminin her thread'e 60 ms. quanta süresi verdiğini düşünelim. O anda da çalışma kuyruğunda(run queue) aşağıdaki thread'ler bulunuyor olsun: T1 (40 ms) T2 (0 ms) ==> quanta süresi bitti T3 (10 ms) T4 (25 ms) Burada T2 thread'inin ona atanan 60 ms'lik quanta süresinin dolduğunu varsayalım. İşletim sistemi T2 thread'ine hemen 60 ms quanta doldurmayacaktır. Önce çalışma kuyruğundaki tüm thread'lerin kullandığı quanta sürelerinin 0'a düşmesini bekleyecek sonra hepsini birlikte 60 ms ile dolduracaktır. Pekiyi bu durumda bloke olup bekleme kuyruğunda beklemekte olan thread'lerin quantda süreleri ne olacaktır. Örneğin T5 thread'inin bloke beklediğini ve kalan quanta süresinin 30 ms. olduğunu varsayalım. Çalışma kuyruğundaki tüm thread'lerin quanta süreleri sıfıra geldiğinde onlara 60 ms. quanta doldurulurken blokede bekleyen threa'2lere doldurma yapılacak mıdır? İşte işletim sistemleri genellikle blokede bekleyen thread'lere de doldurma yapmaktadır. Çünkü onlar uyandıklarında diğerleriyle eşit hakka sahip olacak biçimde daha fazla çalıştırılmalıdırlar. Ancak tabii blokede uzun süre bekleyen thread'lerin quanta'larının sürekli doldurulması da anomali yaratabilmektedir. Bunun için işletim sistemleri blokede bekleyen thread'ler için maksimum bir üst sınır da belirleyebilmektedir. Pekiyi çalışma kuyruğundaki tüm thread'lerin quanta süreleri 0'a düştüğünde tüm thread'ler aynı quanta değeri ile mi doldurulmaktadır? İşte bu konuda işletim sistemleri arasında farklılıklar vardır. Windows'ta genel olarak aynı quanta süresi doldurulmaktadır. Ancak Linux sistemlerinde SCHED_OTHER çizelgeleme politikasında her thread'e o threadin önceliği ile orantılı quanta süreleri doldurulmaktadır. Yani Linux sistemlerinde bir thread'in önceliğini artırdığımızda onun diğerlerine göre daha fazla quanta süresi kullanmaısnı sağlayabiliriz. Thread'li işletim sistemlerinde thread'lerin zaman paylaşımlı biçimde çizelgelendiğini belirtmiştik. Ancak bu sistemlerde sisteme bağlı olarak thread'lere öncelik dereceleri atanabilmektedir. Böylece yksek öncelikli thread'lerin CPU'dan daha fazla zaman alması sağlanabilmektedir. Thread öncelikleri konusu yukarıda da belirttiğimiz gibi işletim sistemine özgü farklılıklar içermektedir. Biz burada önce Windows sistemlerindeki durumu sonra da UNIX/Linux sistemlerindeki durumu kabaca ele alacağız. >>> Windows Sistemlerinde : Windows sistemlerinin kullandığı thread izelgeeleme algoritmasına "öncelik sınıflarıyla döngüsel çizelgeleme (priority class based round robin scheduling )" denilmektedir. Windows'ta her thread'in [0, 31] arasında bir öncelik derecesi vardır. Şimdiye kadar yaratmış olduğumuz thread'lerin default öncelik dereceleri 8'dir. Windows'ta thread'ler şöyle çizelgelenmektedir: Önce en yüksek önceliğe sahip thread'lerden bir grup oluşturulur. Sanki diğer thread'ler hiç yokmuş gibi yalnızca bu thread'ler zaman paylaşımlı biçimde çalıştırılır. Bu thread'ler sonlanırsa ya da bloke olursa bu kez daha düşük öncelikli en yüksek grup aynı biçimde kendi aralarında izelgelenir. Bu yöntemde düşük öncelikli thread'lerin çalışabilmesi için yüksek öncelikli thread'lerin sonlanması ya da bloke olması gerekmektedir. Yüksek öncelikli bir thread'in blokesi çözüldüğünde işletim sistemi düşük öncelikli thread'in çalışmasına ara vererek yeniden bu yüksek öncelikli thread grubunu çizelgelemektedir. Örneğin sistemde aşağıdaki öncelikte thread'ler bulunuyor olsun: T1 -> 12 T2 -> 12 T3 -> 10 T4 -> 9 T5 -> 9 T6 -> 8 T7 -> 8 T8 -> 8 Burada sistem sanki diğer thread'ler yokmuş gibi T1 ve T2 thread'lerini kendi aralarında zaman paylaşımlı olarak çalıştırmaktadır. Bu thread'ler sonlanırsa ya da bloke olursa T3 thread'i çalışma imkanı bulacaktır. T3 thread'i de sonlanırsa ya da bloke olursa bu durumda T4 thread'i, o da sonlanırsa ya da bloke olursa T5, T6, T7 ve T8 thread'leri kendi aralarında zaman paylaşımlı olarak çizelgelenecektir. Görüldüğü gibi bu sistemde düşük öncelikli bir thread'in çalışanilmesi için yüksek öncelikli thread'lerin bloke olması ya da sonlanması gerekmektedir. Windows'un çizelgeleme algoritması kabaca yukarıdaki gibi olsa da aslında oludukça ayrıntılar içermektedir. Çok işlemcili ya da çok çekirdekli sistemlerde eeğer boşta işlemci ya da çekirdek varsa işletim sistemi daha düşük öncelikli sınıfları da bu işlemci ya da çekirdeklere atayabilmektedir. Windows'ta bir thread'in [0, 31] arasındaki öncelik derecesi iki değerin toplamıyla elde edilmektedir: Prosesin Öncelik Sınıfı + Thread'in Göreli Öncelik Derecesi. Prosesin öncelik sınıfı bir taban değer belirtir. Thread'in göreli önceliği de bu taban değere toplanır. Prosesin öncelik sınıflarının taban değerleri şöyledir: NORMAL_PRIORITY_CLASS (8 default) ABOVE_NORMAL_PRIORITY_CLASS (10) BELOW_NORMAL_PRIORITY_CLASS (6) HIGH_PRIORITY_CLASS (13) REALTIME_PRIORITY_CLASS (24) IDLE_PRIORITY_CLASS (4) Thread'in göreli öncelik dereceleri de şöyledir: THREAD_PRIORITY_NORMAL (0 default) THREAD_PRIORITY_IDLE (Öncelik sınıfına göre değişmektedir) THREAD_PRIORITY_LOWEST (-2) THREAD_PRIORITY_BELOW_NORMAL (-1) THREAD_PRIORITY_ABOVE_NORMAL (+1) THREAD_PRIORITY_HIGHEST (+2) THREAD_PRIORITY_TIME_CRITICAL (Öncelik sınıfına göre değişmektedir) Herhangi bir öncelik oluşturmak için bu ikşi ayarlamadın da yapıması gerekir. Tabii aynı değeri veren birden fazla kombinasyon olabilir. Default durumda prosesin öncelik sınıfı NORMAL_PRIORITY_CLASS, thread'in göreli önceliği THREAD_PRIORITY_NORMAL biçimdedir. Bu durumda thread'in öncelik derecesi 8 + 0 = 8 biçimindedir. Örneğin thread önceliğini 15 yapmak isteyelim. Bunun birkaç yolu olabilir: IDLE_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL BELOW_NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL ABOVE_NORMAL_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL HIGH_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL HIGH_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL Thread'in önceliğini 31'e çekmek isteyelim. Bunun tek yolu şöyleidr: REALTIME_PRIORITY_CLASS + THREAD_PRIORITY_TIME_CRITICAL Thread önceliklerinin belirlenmesine ilişkin Microsoft dokğmanlarına aşağıdaki bağlantıdan erişebilirsiniz: https://learn.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities Prosesin öncelik sınıfı GetPriorityClass API fonksiyonuyla alınıp SetPriorityClass API fınksiyonuyla set edilebilir. Fonksiyonarın prototipleri şöyledir: DWORD GetPriorityClass( HANDLE hProcess ); BOOL SetPriorityClass( HANDLE hProcess, DWORD dwPriorityClass ); Fonksiyonların birinci parametreleri öncelik sınıfı değiştirilecek prosesin HANDLE değerini belirtmektedir. GetPriorityClass API fonksiyonu prosesin öncelik sınıfına geri dönmektedir. SetPriorityClass API fonksiyonu ise prosesin öncelik sınıfını ikinci parametresiyle belirtilen sınıf haline getirmektedir. GetPriorityClass fonksiyonu başarısız olamamaktadır. SetPriorityClass fonksiyonu ise başarı durumunda sıfır dışı bir değere başarsızlık durumunda sıfır değerine geri dönmektedir. Anımsanacağı gibi o anda çalışmakta olan prosesin handle değeri GetCurrentProcess API fonksiyonuyla elde edilmektedir. Biz bir prosesin öncelik sınıfını istediğimiz gibi değiştirebilir miyiz? Windows'ta karmaşık bir güvenlik mekanizması vardır. Bu kursta bu konuya girmeyeceğiz. Ancak bu tür uygulamalarda programı "Run As Administrator" seçeneği ile çalıştırmalısınız. Aşağıdaki örnekte prosesin öncelik değiştirilmiş ve yazdırılmıştır. * Örnek 1, #include #include #include LPCSTR GetPriorityClassName(DWORD dwPriority); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwPriorityClass; if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0) ExitSys("GetPriorityClass"); puts(GetPriorityClassName(dwPriorityClass)); if (!SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS)) ExitSys("SetPriorityClass"); if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0) ExitSys("GetPriorityClass"); puts(GetPriorityClassName(dwPriorityClass)); return 0; } LPCSTR GetPriorityClassName(DWORD dwPriority) { const char *pszName = "NONE"; switch (dwPriority) { case ABOVE_NORMAL_PRIORITY_CLASS: pszName = "ABOVE_NORMAL_PRIORITY_CLASS"; break; case BELOW_NORMAL_PRIORITY_CLASS: pszName = "BELOW_NORMAL_PRIORITY_CLASS"; break; case HIGH_PRIORITY_CLASS: pszName = "HIGH_PRIORITY_CLASS"; break; case IDLE_PRIORITY_CLASS: pszName = "IDLE_PRIORITY_CLASS"; break; case NORMAL_PRIORITY_CLASS: pszName = "NORMAL_PRIORITY_CLASS"; break; case REALTIME_PRIORITY_CLASS: pszName = "REALTIME_PRIORITY_CLASS"; break; } return pszName; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } Thread'in göreli önceliğini almak ve set etmek için GetThreadPriorty ve SetThreadPriority API fonksiyonları kullanılmaktadır. Fonksiyonların prototipleri şöyledir: int GetThreadPriority( HANDLE hThread ); BOOL SetThreadPriority( HANDLE hThread, int nPriority ); Fonksiyonların birinci parametreleri göreli önceliği alınacak ya da dğeiştirilecek thread'in HANDLE değerini almaktadır. SetThreadPriority fonksiyonunun ikinci parametresi thread'in göreli önceliğini belirtmektedir. GetThreadPriority fonksiyonu başarısız olamaz, thread'in göreli önceliği ile geri dönmektedir. SetThreadPriority fonksiyonu başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır değerine geri dönmektedir. O anda çalışmakta olan thread'in HANDLE değeri GetCurrentThread API fonksiyonu ile elde edilebilmektedir. Aşağıda thread'in göreli öncelik derecesi alınıp set edilmiştir. * Örnek 1, #include #include #include LPCSTR GetThreadPriorityName(int threadPriority); void ExitSys(LPCSTR lpszMsg); int main(void) { int threadPriority; if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN) ExitSys("GetPriorityClass"); puts(GetThreadPriorityName(threadPriority)); if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)) ExitSys("SetThreadPriority"); if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN) ExitSys("GetPriorityClass"); puts(GetThreadPriorityName(threadPriority)); return 0; } LPCSTR GetThreadPriorityName(int threadPriority) { const char *pszName = "NONE"; switch (threadPriority) { case THREAD_PRIORITY_ABOVE_NORMAL: pszName = "THREAD_PRIORITY_ABOVE_NORMAL"; break; case THREAD_PRIORITY_BELOW_NORMAL: pszName = "THREAD_PRIORITY_BELOW_NORMAL"; break; case THREAD_PRIORITY_HIGHEST: pszName = "THREAD_PRIORITY_HIGHEST"; break; case THREAD_PRIORITY_IDLE: pszName = "THREAD_PRIORITY_IDLE"; break; case THREAD_PRIORITY_LOWEST: pszName = "THREAD_PRIORITY_LOWEST"; break; case THREAD_PRIORITY_NORMAL: pszName = "THREAD_PRIORITY_NORMAL"; break; case THREAD_PRIORITY_TIME_CRITICAL: pszName = "THREAD_PRIORITY_TIME_CRITICAL"; break; } return pszName; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } * Örnek 2, Aşağıcdaki örnekte proesin ana thread'i maksimum öncelik olan 31 önceliğe çekilmek istenmiştir. Programı "Arun As Administrator" ile çalıştırınız. #include #include #include LPCSTR GetPriorityClassName(DWORD dwPriority); LPCSTR GetThreadPriorityName(int threadPriority); void ExitSys(LPCSTR lpszMsg); int main(void) { DWORD dwPriorityClass; int threadPriority; if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0) ExitSys("GetPriorityClass"); puts(GetPriorityClassName(dwPriorityClass)); if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN) ExitSys("GetPriorityClass"); puts(GetThreadPriorityName(threadPriority)); printf("-----------------------\n"); if (!SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS)) ExitSys("SetPriorityClass"); if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)) ExitSys("SetThreadPriority"); if ((dwPriorityClass = GetPriorityClass(GetCurrentProcess())) == 0) ExitSys("GetPriorityClass"); puts(GetPriorityClassName(dwPriorityClass)); if ((threadPriority = GetThreadPriority(GetCurrentThread())) == THREAD_PRIORITY_ERROR_RETURN) ExitSys("GetPriorityClass"); puts(GetThreadPriorityName(threadPriority)); getchar();PCSTR GetPriorityClassName(DWORD dwPriority) const char *pszName = "NONE"; switch (dwPriority) { case ABOVE_NORMAL_PRIORITY_CLASS: pszName = "ABOVE_NORMAL_PRIORITY_CLASS"; brezName = "BELOW_NORMAL_PRIORITY_CLASS"; break; case HIGH_PRIORITY_CLASS: pszName = "HIGH_PRIORITY_CLASS"; break; case IDLE_PRIORITY_CLASS: pszName = "IDLE_PRIORITY_CLASS"; break; case NORMAL_PRIORITY_CLASS: pszName = "NORMAL_PRIORITY_CLASS"; break; case REALTIME_PRIORITY_CLASS: pszName = "REALTIME_PRIORITY_CLASS"; break; } return pszName; } LPCSTR GetThreadPriorityName(int threadPriority) { const char *pszName = "NONE"; switch (threadPriority) { case THREAD_PRIORITY_ABOVE_NORMAL: pszName = "THREAD_PRIORITY_ABOVE_NORMAL"; break; case THREAD_PRIORITY_BELOW_NORMAL: pszName = "THREAD_PRIORITY_BELOW_NORMAL"; break; case THREAD_PRIORITY_HIGHEST: pszName = "THREAD_PRIORITY_HIGHEST"; break; case THREAD_PRIORITY_IDLE: pszName = "THREAD_PRIORITY_IDLE"; break; case THREAD_PRIORITY_LOWEST: pszName = "THREAD_PRIORITY_LOWEST"; break; case THREAD_PRIORITY_NORMAL: pszName = "THREAD_PRIORITY_NORMAL"; break; case THREAD_PRIORITY_TIME_CRITICAL: pszName = "THREAD_PRIORITY_TIME_CRITICAL"; break; } return pszName; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } >>> UNIX/Linux Sistemlerinde : UNIX/Linux sistemlerinde her thread'in belirlenmiş olan bir "çizelgeleme politikası (scheduling policy)" vardır. POSIX standartlarına göre şu çizelgeleme politikaları bulunmaktadır: SCHED_FIFO SCHED_RR SCHED_OTHER SCHED_SPORADIC Bu politikalardan, -> SCHED_SPORADIC politikasının desteklenesi isteğe bağlı bırakılmıştır. -> SCHED_FIFO ve SCHED_RR politikalarıba "gerçek zamanlı (real time)" çizelgeleme politikaları denilmektedir. -> SCHED_OTHER politikasının ayrıntılarına girilmemiş ve bu belirleme işletim sistemini yazanların isteğine bırakılmıştır. Default çizelgeleme politikasının ne olacağı POSIX standartlarında açıkça belirtilmemiştir. O da işletim sistemini yazanların isteğine bırakılmıştır. Linux ve pek çok UNIX türevi sistemdeki default çizelgeleme politikası SCHED_OTHER biçimindedir. POSIX standartlarına göre SCHED_OTHER thread'lerin bir dinamik öncelik derecesi vardır. Dinamik öncelik [0, 39] arasındadır. Default dinamik öncelik 20'dir. Dinamik öncelikte yüksek değer düşük öncelik, düşük değer yüksek öncelik belirtmektedir. Linux sistemlerinde thread'lere quanta doldurulurken her thread'e o thread'in dinamik önceliği ile orantılı bir quanta süresi atanmaktadır. Yani dinamik önceliği yüksek olan thread'lere daha fazla quanta süresi, dinamik önceliği düşük thread'lere daha az quanta süresi atanmaktadır. Böylece programcı thread'in diğer thread'lere göre daha fazla CPU zamanı almasını istiyorsa thread'inin dinamik önceliğini yükseltmektedir. Dinamik öncelikle quanta süresi arasındaki ilişki sistemden sisteme hatta aynı sistemde versiyona bile değişiklik österebilmektedir. Örneğin eskiden Linux sistemlerinde dinamik önceliğin etkisi nispeten azdı daha sonra bu etki yükseltilmiştir. Eskiden UNIX/Linux sistemlerinde thread kavramı yoktu. O zamanlarda thread'ler yerine proseslerin dinamik önceliği vardı. Sonra thread'ler sistemlere eklenince thread'lerin de dinamik öncelikleri oluşturuldu. Ancak eski POSIX fonksiyonları da muhafaze edildi. POSIX standartlarına göre bir prosesin dinamik önceliğ değiştirildiğinde prosesin tüm thread'lerinin dinamik önceliği değiştirilmiş olmalıdır. Ancak Linux sistemleri bu kurala uymamaktadır. Linux sistemlerinde prosesin dinamik önceliği değiştirildiğinde bundan yalnızca prosesin ana thread'i etkilenmektedir. (Halbuki POSIX standartlarında o anda yaratılmış olan ve daha sonra yaratılacak olan tüm thread'lerin dinamik önceliğinin değişmesi gerekmektedir.) Dinamik önceliği değiştirmede en çok kullanılan fonksiyon nice isimli POSIX fonksiyonudur. nice fonksiyonu prosesin dinamik önceliğini değiştirmektedir. (Linux sistemlerinde bu durum tüm thread2lerin değil ana thread'in dinamik önceliğinin değiştirilmesine yol açmaktadır.) nice fonksiyonun protoripi şöyledir: #include int nice(int inc); nice fonksiyonu prosesin dinamik önceliğini o anki dinamik öncelikten parametresi belirtilen miktarda yükseltir ya da alçaltır. Ancak parametrenin anlamı terstir. Yani pozitif değerler "düşürme", negatif değerler "yükseltme" anlamına gelmektedir. Fonksiyon parametre olarak [-20, +19] arasında bir değer almaktadır. Böylelikle biz bu fonksiyona argüman olarak 19 değerini verirsek dinamik öncelik en düşük değeri belirten 39 olur, -20 verirsek dinamik öncelik en yükske değeri belirten 0 olur. Fonksiyon başarı durumunda yeni nice değerine başarısızlık durumunda -1 değerine geri dönmektedir. nice fonksiyonu ile sıradan prosesler önceliklerini düşürebilirler (pozitif parametre) ancak yükseltemezler (negatif parametre). Yükseltme işlemi için prosesin uygun önceliğe sahip olması (örneğin root olması, yani sudo ile çalıştırılması) gerekmektedir. Prosesin nice değeri getpriority POSIX fonksiyonyla elde edilebilmektedir. nice fonksiyonun prosesin dinamik önceliğini değiştirdiğini belirtmiştik. Yalnızca belli bir thread'in dinamik önceliğini değiştirmek için pthread_schedsetparam fonksiyonu kullanılmaktadır. Bu kodudaki ayrıntılar UNIX/Linux sistem programlama kurslarında ele alınmaktadır. Yukarıda da belirtildiği gibi SCHED_FIFO ve SCHED_RR çizelgeleme politikalarına "gerçek zamanlı çizelgeleme politikaları" denilmektedir. UNIX/Linux sistemlerinde çalışma kuyruğunda (yani bloke olmamış) SCHED_FIFO ya da SCHED_RR thread'ler varsa hiçbir zaman SCHED_OTHER thread çizelgelenmemektedir. Tabii çok işlemcili ya da çekirdekli sistemlerde SCHED_FIFO ya da SCHED_RR thread'ler CPU'ya atabndıktan sonra çalışma kuyruğunda artık hiç SCHED_FIFO ya da SCHED_RR thread yoksa SCHED_OTHER thread'ler diğer işlemci ya da çekirdeklerde çizelgelenebilmektedir. Yani SCHED_FIFO ve SCHED_RR thread'lerin SCHED_OTHER thread'lere tam bir üstünlüğü vardır. Pekiyi SCHED_FIFO ve SCHED_RR thread'lerin kendi aralarındaki durum nasıldır? SCHED_FIFO ve ve SCHED_RR thread'lerin de birer önceliği vardır. (Bu öncelik SCHED_OTHER thread'lerin dinamik önceliğinden farklıdır.) Linux sistemlerinde SCHED_FIFO ve SCHED_RR thread'lerin öncelik derecesi 1 ile 99 arasındadır. Burada Yüksek değer yüksek öncelik belirtmektedir. Ancak POSIX standartları bu 1 ve 99 değerlerinin sistemden sisteme değişebileceğini o sistemdeki değerlerin sched_get_priority_min ve sched_get_priority_max fonksiyonlarıyla elde edilmesi gerektiğini belirtmektedir. Çalışma kuyruğunda yalnızca SCHED_FIFO ve SCHED_RR thread'lerin olduğunu varsayalım. (Zaten SCHED_OTHER thread'ler olsa bile çizelgelenmeyecektir.) Çizelgeleme algoritması şöyledir: -> Çizelgeleyici kuyruktaki en yüksek önceliğe sahip olanlar arasında önde bulunan SCHED_FIFO ya da SCHED_RR thread'i CPU'ya atar. Eğer atanan thread SCHED_FIFO ise bloke olana kadar sürekli çalıştrılır. Yani SCHED_FIFO bir thread bloke olmadıktan sonra CPU'yu bırakmamaktadır. Ancak atanan CPU'ya atanan thread SCHED_RR ise bir quanta çalıştırılıp kuyruğun sonuna alınır. -> SCHED_FIFO thread bloke olursa bu durumda kuyrukta en yüksek öncelikte ve önde olan ilk SCHED_FIFO ya da SCHED_RR thread CPU'ya atanır. -> Bloke olmuş olan bir thread'in blokesi çözüldüğünde eğer o anda çalışmakta olan thread'ten daha yüksek öncelikliyse o anda çalışmakta olan thread'in çalışması kesilir ve blokesi çözülen thred çalıştırılır. Tabii bu yeni thread SCHED_FIFO ise sürekli çalıştırılacak, SCHED_RR ise bir quanta çalıştırılıp sona alınacaktır. Eğer blokesi çözülmüş olan thread o anda çalışmakta olan thread'le eşit öncelikli ya da ondan daha düşük öncelikli ise kuyruğun sonuna alınmaktadır. Blokesi çözülmüş yüksek öncelikli thread tarafından kesilen thread eğer SCHED_FIFO thread'se kuyruğun önüne yerleştirilir. SCHED_RR thread'se kuyruğun sonuna alınır. Bu çizelgeleme sisteminde şu durumlara dikkat ediniz. -> Bu sistemde çalışma kuyruğundaki bütün geçek zamanlı zamanlı thread'lerin SCHED_RR olduğunu varsayalım. Bu durumdaki çizelgeleme Windows'taki çizelgelemeye çok benzer durumdadır Yani en yüksek öncelikteki thread'ler kendi aralarında döngülsel çizelgelenecektir. -> CPU'ya atanmış olan SCHED_FIFO thread'in CPU'yu bırakması ancak daha yüksek öncelikli SCHED_FIFO ya da SCHED_RR thread'in uykudan uyanmasıyla ya da yüksek öncelikli yeni bir thread'in yaratılmasıyla ya da o thread'ın bloke olmasıyla mümkündür. SCHED_FIFO politikası bu sistemlerde "sürekli çalışan tüm CPU'yu tek başına kullanan thread'lerin oluşturulması" amacıyla bulundurulmuştur. Yüksek öncelikli SCHED_FIFO thread'ler diğer thread'lerin çalışmasını engelleyebilmektedir. Aşağıdaki örnekte gerçek zamanlı çizelgeleme politikalarına ilişkin öncelik derecelerinin en düşük ve en yüksek değeri sched_get_priority_min ve sched_get_priority_max fonksiyonları ile elde edilip yazdırılmıştır. SCHED_OTHER politikası için bu fonksiyonlar 0 değerini geri döndürmektedir. * Örnek 1, #include #include #include #include #include #include #include #define QUEUE_SIZE 10 void *thread_producer(void *param); void *thread_consumer(void *param); void exit_sys(const char *msg); void exit_sys_errno(const char *msg, int eno); sem_t g_sem_producer; sem_t g_sem_consumer; int g_queue[QUEUE_SIZE]; size_t g_head; size_t g_tail; int main(void) { pthread_t tid1, tid2; int result; if (sem_init(&g_sem_producer, 0, QUEUE_SIZE) == -1) exit_sys("sem_init"); if (sem_init(&g_sem_consumer, 0, 0) == -1) exit_sys("sem_init"); 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) { int val; unsigned seed; seed = time(NULL) + 123; val = 0; for (;;) { usleep(rand_r(&seed) % 300000); if (sem_wait(&g_sem_producer) == -1) exit_sys("sem_wait"); g_queue[g_tail] = val; g_tail = (g_tail + 1) % QUEUE_SIZE; if (sem_post(&g_sem_consumer) == -1) exit_sys("sem_post"); if (val == 99) break; ++val; } return NULL; } void *thread_consumer(void *param) { int val; unsigned seed; seed = time(NULL) + 456; for (;;) { if (sem_wait(&g_sem_consumer) == -1) exit_sys("sem_wait"); val = g_queue[g_head]; g_head = (g_head + 1) % QUEUE_SIZE; if (sem_post(&g_sem_producer) == -1) exit_sys("sem_post"); usleep(rand_r(&seed) % 300000); printf("%d ", val); fflush(stdout); if (val == 99) break; } printf("\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 bir prosesin ya da thread'in çizelgeleme politikası ve gerçek zamanlı thread'lerin öncelikleri nasıl belirlenmektedir? Bir prosesin çizelgeleme politikası sched_setscheduler fonksiyonuyla set edilip sched_getcheduler fonksiyonu ile alınabilmektedir. Bu POSIX fonksiyonları Linux sistemlerinde doğrudan ilgili sistem fonksiyonlarını çağırmaktadır. Fonksiyonların prototipileri şöyledir: #include int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param); int sched_getscheduler(pid_t pid); Fonksiyonların birinci parametreleri prosesin id değerini belirtmektedir. Bu parametre 0 girilirse fonksiyonları çağıran proses için işlem yapılmaktadır. sched_setscheduler fonksiyonun ikinci parametresi çizelgeleme politikasını üçüncü paarametresi ise gerçek zamanlı çizelgeleme politikalarına ilişkin öncelik derecesini belirtmektedir. Vu üçüncü parametrede SCHED_OTHER için öncelik belirtilememektedir. Bu parametre yalnızca SCHED_FIFO ve SCHED_RR için anlamlıdır. Fonksiyon başarı durumunda eski çizelgeleme politikasına başarısızlık durumunda -1 değerine geri dönmektedir. sched_getscheduler fonksiyonu da başarı durumunda çizelgeleme politikasına, başarısızlık durumunda -1 değerine geri dönmektedir. Prosesin çizelgeleme politikasını değiştirebilmek için sched_setscheduler fonksiyonunu çağıran prosesin uygun önceliğe sahip olması gerekir. Aşağıdaki programda proses kendi çizelgeleme politikasını SCHED_FIFO yapıp önceliğini de Linux'taki maksimum öncelik olan 99 yapmıştır. Tabii programı sudo ile çalıştırmalısınız. * Örnek 1, #include #include #include #include void* thread_proc(void* param); void exit_errno(const char* msg, int result); int main(void) { int result; pthread_t tid;; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_errno("pthread_create", result); pthread_join(tid, NULL); return 0; } void* thread_proc(void* param) { struct sched_param sparam; int result; long i; sparam.sched_priority = 99; if ((result = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sparam)) != 0) exit_errno("pthread_setschedparam", result); for (i = 0; i < 1000000000; ++i) ; return NULL; } void exit_errno(const char* msg, int result) { fprintf(stderr, "%s: %s\n", msg, strerror(result)); exit(EXIT_FAILURE); } Yukarıda da belirttiğimiz gibi POSIX standartlarına göre aslında prosesin çizelgeleme politikası ya da öncelik dereceleri değiştirildiğinde bundan prosesin tüm thread'leri etkilenmektedir. Ancak Linux'ta bundan yalnızca prosesin ana thread'i etkilenmektedir. Ancak biz belli bir thread'imizin de diğerlerin bağımsız olarak çizelgeleme politikasını değiştirip elde edebiliriz. Bunlar için pthread_setschedparam ve pthread_getschedparam fonksiyonları kullanılmaktadır: #include int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param); int pthread_getschedparam(pthread_t thread, int *policy, struct sched_param *param); Fonksiyonlar sched_setscheduler ve sched_getscheduler fonksiyonları gibi çalışmaktadır. Yalnızca proses id yerine thread id değerini parametre olarak almaktadır. Diğer thread fonksiyonlarında olduğu gibi bu fonksiyonlar da başarı durumunda 0 değerine, başarısızlık durumunda errno değerine geri dönemektedir. Aşağıdaki örnekte bir thread'in çizelgeleme politikası ve önceliği değiştirilmektedir. * Örnek 1, #include #include #include #include void* thread_proc(void* param); void exit_errno(const char* msg, int result); int main(void) { int result; pthread_t tid;; if ((result = pthread_create(&tid, NULL, thread_proc, NULL)) != 0) exit_errno("pthread_create", result); pthread_join(tid, NULL); return 0; } void* thread_proc(void* param) { struct sched_param sparam; int result; sparam.sched_priority = 99; if ((result = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sparam)) != 0) exit_errno("pthread_setschedparam", result); for (long i = 0; i < 1000000000; ++i) ; return NULL; } void exit_errno(const char* msg, int result) { fprintf(stderr, "%s: %s\n", msg, strerror(result)); exit(EXIT_FAILURE); } /*================================================================================================================================*/ (89_26_05_2024) & (90_01_06_2024) & (91_02_06_2024) & (92_08_06_2024) > Kütüphaneler : Kütüphane (library) kavramı genel olarak "kullanıma hazır halde bulunan fonksiyonlar ve sınıfların oluşturduğu" topluluklar için kullanılan bir terimdir. Ancak aşağı seviyeli dünyada kütüphane "içerisinde derlenmiş bir biçimde fonksiyonların (ve sınıfların) bulunduğu dosyalara denilmektedir. Kütüphaneler statik ve dinamik olmak üzere ikiye ayrılmaktadır. Statik kütüphanelerin Windows sistemlerinde uzantıları ".lib"", UNIX/Linux ve macOS sistemlerindeki uzantıları ".a" biçimindedir. Dinamik kütüphanelerin ise Windows sistemlerindeki uzantıları ".dll", UNIX/Linux ve macOS sistemlerindeki uzantıları ise ".so" biçimindedir. ".lib" uzantısı "library" sözcüğünde, ".a" uzantısı "archive" sözcüğünden, "dll" uzantısı "dynamic link library" sözcüklerinden ve ".so" uzantısı da "shared object" sözüklerinden kısaltılmıştır. Kütüphane konusunun aslında aşağı seviyeli oldukça ayrıntıları vardır. Biz bu kursumuzda temel düzeyde bu kavramlar üzerinde uygulamalı bilgiler edineceğiz. Derleyicilerin ürettiği dosyalara "amaç dosyalar (object files)" ya da "yeniden yüklemebilen dosyalar (relocatable object files)" denilmektedir. Bağlayıcılar bir grup amaç dosyayı (object files) girdi olarak alıp çeşitli kütüphanelere de başvurarak çalıştırılabilir doysalar oluşturmaktadır. Bu sayede programlar farklı ekipler tarafından parça parça yazılıp bağlama aşamasında birleştirilebilmektedir. Örneğin tipik bir C projesi şöyle derlenip oluşturulmaktadır: a1.c ----> Derleyici ----> a1.obj ----> \ a2.c ----> Derleyici ----> a2.obj ----> \ a3.c ----> Derleyici ----> a3.obj ----> ===> Bağlayıcı ------> Çalıştırılabilir Dosya a4.c ----> Derleyici ----> a4.obj ----> / / a5.c ----> Derleyici ----> a5.obj ----> / / Kütüphane Dosyaları Amaç dosyaların Windows sistemlerindeki uzantıları ".obj" biçiminde UNIX/Linux ve macOS sistemlerindeki uzantıları ise ".o" biçimindedir. Pekiyi bağlayıcı tam olarak ne yapmaktadır? Farklı amaç dosyaları birleştirmek basit bir işlem değildir. Örneğin iki kaynak dosyadan oluşan bir program söz konusu olsun: /* a1.c */ extern int g_x; void foo(void); int main(void) { g_x = 10; foo(); CALL adres return 0; } /* a2.c */ int g_x; void foo(void) { printf("foo\n"); } Proje oluşturan bu "a1.c" ve "a2.c" dosyalarının birbirinden habersiz bağımsız bir biçimde derlendiğine dikkat ediniz. "a1.c" programı derlendiğinde foo fonksiyonu o dosya yoktur. g_x global değişkeni de o dosyada yoktur. Pekiyi oradaki makine komutları nasıl üretilmektedir? İşte derleyiciler bu yüt durumlarda makine kodlarının ilgili kısımlarını boş bırakıp bağlayıcının orayı doldurmasını istemektedir. Bu işleme "relocation" denilmektedir. "a1.c" dosyası içerisindeki fonksiyon çağrısına dikkat ediniz: foo(); Bir fonksiyonun çağrılabilmesi için CALL makine komutu kullanılmaktadır. CALL makine komutu da operand olarak çağrılacak fonksiyonu adresini almaktadır. Örneğin: CALL Oysa derleyici derleme aşamasında foo fonksiyonunu görmediği için onun adresini bilememktedir. İşte derleyici bağlayıcıdan "foo fonksiyonun yerini bulunup ilgili adresin düzeltilmesini" talep etmektedir. Bu relocation bilgileri amaç dosyanın içerisinde bulunmaktadır. Yani bağlayıcı yalnızca amaç dosyaları birleştirmemekte aynı zamanda çeşitli makine komutlarını da düzeltmektedir. Pekiyi yukarıdaki örnekte foo fonksiyonunu bağlayıcı nerelrde arayacaktır? Bağlayıcılar öncelikle fonksiyonları ve global nesneleri kendilerine girdi olarak verilmiş olan kütğphane amaç dosyalarda aramaktadır. Onlar amaç dosyalarda bulunamazsa bağlayıcılar programcının belirlediği kütüphane dosyalarına da bakmaktadır. Standart C fonksiyonları, Windows API fonksiyonları, POSIX fonksiyonları çeşitli kütüphane dosyalarının içerisindedir. Bunlar bağlama aşamasında bağlayıcı tarafından ele alınmaktadır. Şimdi de statik ve dinamik kütüphaneleri sırasıyla inceleyelim: >> Statik Kütüphaneler : Statik kütüphane dosyaları "amaç dosyaları tutan kap" gibidir. Yani statik kütüphane dosyalarının içerisinde fonksiyonlar ya da sınıflar değil amaç dosyalar bulunmaktadır. Yani örneğin biz bir grup fonksiyonu statik kütüphaneye yerleştirmek istesek önce onları bir C dosyasının içerisine yazarız. Sonra dosyayı derleyip amaç dosya oluştururuz. Bu amaç dosyayı da statik kütüphane içerisine yerleştiririz. Microsoft C derleyicilerinde dosyayı yalnızca derlemek için "/c" seçeneği, gcc ve clang derleyicilerinde ise "-c" seçeneği kullanılmaktadır. Örneğin cl /c myutil.c gcc -c myutil.c Şimdi de sistem özelinde incelemelerimize devam edelim: >>> Windows Sistemlerinde : Microsoft Windows sistemlerinde statik kütüphane dosyaları özel bir formata sahiptir. Bunu oluşturmak için Microsoft "lib.exe" isimli yardımcı bir program bulundurmuştur. "lib.exe" programı Microsoft C derleyicisi kurulduğunda (tabii Visual Studio IDE'si de kurulduğunda) otomatik olarak kurulmaktadır. lib.exe programının en önemli birkaç kullanımı şöyledir: lib /OUT:myutil.lib a.obj b.obj Burada "myutil.lib" dosyası yeniden yaratılır ve onun içerisine "a.obj" ve "b.obj" dosyaları eklenir. Eğer bu dosya varsa içerği sıfırlanarak yeniden oluşturulmaktadır. lib myutil.lib c.obj Burada "myutil.lib" "dosyasının var olması gerekir. Onun içerisinde eğer "c.obj" dosyası yoksa eklenir varsa dosya değiştirilir. lib /LIST myutil.lib Burada "myutil.lib" içerisindeki amaç dosyalar görüntülenir. lib /REMOVE:a.obj myutil.lib Burada "myutil.lib" içerisinden "a.obj" dosyası silinir. lib /EXTRACT:a.obj myutil.lib Burada "myutil.lib" içerisindeki "a.obj" dosyası dışarıda "a.obj" "olarak save edilir. Fakat kütüphanenin içeriisinden silinmez. "lib.exe" hakkında ayrıntılı açıklamalar için aşağıdaki MSDN bağlantısından faydalanabilirsiniz: https://learn.microsoft.com/en-us/cpp/build/reference/overview-of-lib?view=msvc-170 Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, Aşağıdaki örnekte "a.c" ve "b.c" dosyaları aşağıdaki gibi derlenip "myutil.lib" isimli bir statik kütüphane dosyası oluşturulmuştur: cl /c a.c cl /c b.c lib /OUT:mmyutil.lib a.obj b.obj Programın kodları ise şu şekildedir; /* a.c */ #include int g_x; 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; } /* b.c */ #include int g_y; void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } Bir statik kütüphaneden bir fonksiyon çağrıldığında bağlayıcı çağrılmış olan fonksiyonun statik kütüphane içerisindeki hangi amaç dosyada olduğunu belirleri, o amaç dosyayı çalıştırılabilen dosyaya ekler. Böylece statik kütüphane kullanan bir program çalıştırılırken artık o statik kütüphaneye gereksinim duyulmaz. Şu noktalara dikkat ediniz: -> Statik kütüphane içerisinde bir fonksiyon bile çağırsak bağlayıcı onun bulunduğu amaç dosyanın tamamını çalıştırılabilen dosyaya yazmaktadır. Amaç dosya içerisindeki tek bir fonksiyonun çalıştırılabilen dosyaya yazılması mümkün değildir. -> Statik kütüphane kullanımında statik kütüphane içerisidnen çağrılan fonksiyonlar nihayetinde çalıştırılabilen dosyanın içerisine yazıldığı için aynı fonksiyonları kullanan farklı programlar aynı fonksiyon kodlarını çalıştırılabilen dosya içerisinde barındırmış olacaklardır. Bu da çalıştırılabilen dosyaların diskte daha fazla yer kaplamasına yol açmaktadır. -> Statik bağlanmış fonksiyonları kullanan farklı programlar birlikte çalıştırıldığında işletim sistemi mecburen onların ortak kullandığı fonksiyonları tekrar belleğe yüklemektedir. Bu da yalnızca disk kullanımı bakımından değil ana bellek yönetini bakımından da etkin olmayan bir durum oluşturmaktadır. -> Statik kütüphanelerin en önemli avantajı onları kullanan programların konuşlandırılmasının (deployment) kolay olmasıdır. Tek yapılacak şey tek bir çalıştırılabilir dosyanın hedef makineye taşınmasıdır. Aslında C ve C++ IDE'lerinde de hiç komut satırı işlemi yapmadan statik kütüphane oluşturulabilmektedir. Örneğin Visual Studio IDE'sinde yeni boş bir bir proje oluşturulduktan sonra proje seçeneklerinden "Configuration Type" için "Application" yerine "Static library" seçilirse proje build edildiğinde statik kütüphane dosyası oluşturulmaktadır. Visual Studio derleyicisi default durumda standart C kütüphanesinin dinamik versiyonunu kullanmaktadır. Eğer projenizde standart C kütüphanesinin statik versyionunu kullanmak istiyorsanız komut satırında "/MT" seçeneği eklenmelidir. Aynı işlem proje seçeneklerinde "C-C++/Code Generation/Runtime Library" sekmesinden de ayarlanabilmektedir. Visual Studio IDE'sinde (diğer IDE'lerde de benzer) bir projede başka bir statik kütüphanenin içerisindeki fonksiyonları çağırmak istediğimizi düşünelim. Pekiyi bu statik kütüphaneye IDE içerisinden nasıl referans ederiz? İşte kullanılacak ek kütüphanelere bağlayıcının bakmasını sağlamak için Visual Studio IDE'sinde proje seçeneklerinden "Linker/Input/Additional Dependencies" edit alanına kütüphanenin ismi eklenmelidir. (Bu alandaki kütüphane isimleri ';' karakterleriyle birbirinden ayrılmaktadır.) Burada kütüphane yalnızca ismi girilebilir. Bir yol ifadesi girilmez. Eğer kütüphane özel bazı dizinlerde ya da projenin çalışma dizininde değilse ayrıca kütüphanenin bulunduğu dizin "Linker/General/Additional Library Directories" edit alanına girilmelidir. Burada dizinler mutlak ya da göreli biçimde girilebilir. >> UNIX/Linux Sistemlerinde : UNIX/Linux sistemlerinde statik kütüphane dosyalarını oluşturmak ve onlarla ilgili işlem yapmak için "ar" isimli program kullanılmaktadır. UNIX/Linux sistemlerindeki "ar" programı aslında Microsoft sistemlerindeki "LIB.EXE" programının karşılığı olarak düşünülebilir. Bu "ar" programının birkaç önemli komut satırı argümanı vardır. Bu program çok eskiden tasarlandığı için seçenekler '-' karakteri ile değil'-' karakteri olmadan doğrudan belirtilmektedir. "r" komut satırı argümanı ("replace" sözcüğünden geliyor) amaç dosyaları kütüphaneye eklemek için kullanılmaktadır. Bu seçenekte kütüphane dosyası yoksa aynı zamanda yaratılmaktadır. Kullan ım şöyledir: ar r <.o dosyaları> Eğer eklenecek amaç dosya zaten kütüphanenin içerisinde varsa o değiştirilmektedir. Örneğin: $ gcc -c a.c $ gcc -c b.c $ ar r libmyutil.a a.o $ ar r libmyutil.a b.o "t" seçeneği kütüphane içerisindeki amaç dosyaları görüntülemektedir. Kullanımı şöyledir: ar t Örneğin: $ ar t libmyutil.a a.o b.o "d" seçeneği ("delete" sözcüğünden geliyor) kütüphane içerisindeki bir amaç dosyayı kütüphaneden silmek için kullanılmaktadır. Kullanımı şöyledir: ar d <.o dosyaları> Örneğin: $ ar t libmyutil.a a.o b.o $ ar d libmyutil.a a.o $ ar t libmyutil.a b.o "x" seçeneği ("extract" sözcüğünden geliyor) kütüphane içerisindeki bir amaç dosyayı kütüphaneden silmeden dışarıya save etmekte kullanılmaktadır. Kullanımı şöyledir: ar x <.o dosyaları> Örneğin: $ ar x libmyutil.a a.o UNIX/Linux sistemlerinde geleneksel olarak kütüphane dosyaları "lib" öneki başlatılarak isimlendirilmektedir. UNIX/Linux sistelerinde bir statik kütüphaneye başvuru işlemi gcc komut satırında ".a" dosyasının belirtilmesi yoluyla yapılabilmektedir. Örneğin: $ gcc -o app app.c libmyutil.a Burada gcc programı ".a" uzantılı dosyaları "ld" bağlayıcısını çalıştırırken ona gendermektedir. Tabii aynı işlem iki parça halinde şöyle de yapılabilirdi: $ gcc -c app.c $ gcc -o app app.o libmyutil.a Daha önceden de belirttiğimiz gibi standart C fonksiyonları ve POSIX fonksiyonları "libc" isimli bir kütüphanede bulunmaktadır. Bu kütüphanenin static ve dinamik biçimleri vardır. Default durumda bu kütüphanenin dinamik versiyonu kullanılmaktadır. Ancak komut satırında "-static" seçeneği ile bu kütüphanenin statik versiyonunun kullanılması sağlanabilir. "-static" komut satırı seçeneği "tüm kütüphanelerin statik versiyonlarının" kullanılacağı anlamına gelmektedir. (Yani biz "-static" seçeneğini beelirttikten sonra başka bir dinamik kütüphaneyi de bağlama aşamasında kullanamayız.) Örneğin: $ gcc -o app app.c -static UNIX/Linux sistemlerinde kütüphane dosyası başka bir dizindeyse bu durumda o dosyaya referans ederken yol ifadesi kullanılabilir. Öneğin: $ gcc -o app app.c xxx/libmyutil.a Eğer kütüphanenin ismi "lib" öneki ile başlatılmışsa kütüphaneye referans etme (yani kütüphanenin bağlayıcı tarafından işleme sokulmasını sağlama) "-l" seçeneği ile yapılabilmektedir. Ancak bu "-l" seçeneğinde kütüphanenin başındaki "lib" öneki ve ".a" ya da ".so" sonekleri kullanılmamaktadır. Örneğin: $ gcc -o app app.c -lmyutil Burada bağlama işlemine "libmyutil.a" dosyası sokulacaktır. Ancak "-l" seçeneği ile kütüphane bu biçimde belirtildiğinde bu kütüphane "/lib" ve "/usr/lib" dizinlerinde aranmaktadır. Yani bu durumda buradaki "libmyutil.a" dosyasının bu dizinlerden birine çekilmesi gerekir. Ancak "-l" seçeneğine ek olarak "-L" seçeneği ile ek arama dizi belirtilebilmektedir. Örneğin: $ gcc -o app app.c -lmyutil -L/home/kaan Burada "libmyutil.a" dosaysı yukarıda belirttiğmiz dizinlerin yanı sıra "/home/kaan" dizininde de aranacaktır. Tabii biz "-L" seçeneğinde o anda bulunulan dizini "." ile de belirtebiliriz. Örneğin: $ gcc -o app app.c -lmyutil -L. Örneğin daha önceden de kullandığımız thread kürüphanesi aslında "libpthread.a" ve "libpthread.so" isimli dosyalardır. Biz de bu ktüphaneiyi bağlama aşamasında şöyle devreye sokuyorduk: $ gcc -o app app.c -lpthread >> Dinamik Kütüphaneler : Dinamik kütüphaneler statik kütüphanelerden oldukça farklıdır. Windows'ta dinamik kütüphanelerin uzantısı ".dll", UNIX/Linux sistemlerinde ".so" biçimindedir. ".dll" uzantısı "dynamic link library" sözcüklerinden ".so" uzantısı ise "shared object" sözcüklerinden kısaltılmıştır. Bir program dinamik kütüphaneden bir fonksiyon çağırdığında bağlayıcı o fonksiyonun kodunu dinamik kütüphaneden çekip çalıştırılabilen dosyaya yazmaz. Bağlayıcı bunun yerine çalıştırılabilen dosyaya "falanca dinamik kütüphaneden filanca fonksiyonlar çağrıldı" biçiminde bir bilgi yazmaktadır. Dinamik kütüphane kullanan bir program çalıştırılmak istendiğinde ise işletim sistemi bu programla birlikte bu programın kullandığı dinamik kütüphaneleri prosesin sanal bellek alanına bütünsel bir biçimde yükler. Program çalışırken akış fonksiyonun çağrılma noktasına geldiğinde bellekte dinamik kütüphanenin yüklü olduğu alana atlayarak fonksiyonu çalıştırır. Dinamik kütüphane kullanımının şu avantajları vardır: -> Dinamik kütüphane kullanan programlar diskte daha az yer kaplarlar. -> Dinamik kütüphane kullanan programlarda dinamik kütüphane içerisindeki fonksiyonlarda değişiklikler yapıldığında çalıştırılabilen programın yeniden derlenmesine ve link edilmesine gerek kalmaz. -> Farklı proseslerin aynı dinamik kütüphaneyi kullanması durumunda bu dinamik kütüphane fiziksel RAM'e tekrar tekrar yüklenmemektedir. Bu da aslında toplamda daha iyi bir RAM optimizasyonu sağlamaktadır. Ancak dinamik kütüphane kullanan programlar başka bir makineye konuşlandırılırken (deploy edilirken) yalnızca çalıştırılabilen dosya değil onun kullandığı bütün dinamik kütüphaneler de o makineye çekilmek zorundadır. Ayni artık program tek bir çalıştırılabilen dosyadan oluşmamaktadır. O dosyanın kullandığı dinamik kütüphaneler de programın bir parçası durumundadır. Dinamik kütüphane kullanımının şu dezavantajları söz konusu olabilmektedir: -> Dinamik kütüphane kullanan programların hedef makineye konuşlandırılması daha zahmetlidir. -> Dinamik kütüphane kullanan programların yüklenmesi daha uzun zaman alma eğilimindedir. -> Dinamik kütüphane kullanan programlar prosesin sanal bellek alanında daha fazla yer kaplamaktadır. Şimdi de sistemler genelinde incelemelerimize devam edelim: >>> Windows Sistemleri : Microsoft Windows sistemlerinde bir DLL oluşturmak için tek yapılacak şey link aşamasında "/DL"L seçeneğini kullanmaktır. Örneğin a.c ve b.c dosyalarını derleyerek bunlardan DLL yapmak isteyelim. Bu işlemi parça paça şöyle gerçekleştirebiliriz: cl /c a.c cl /c b.c link /DLL a.obj b.obj Buradan ilk dosyanın ismine ilişkin "a.dll" dosyası elde edilecektir. Tabii "/DL"L seçeneği kullanılmasaydı default durumda bu dosyalardan ".exe" dosyası oluşturulmaya çalıştırılacaktı. Tabii bu durumda bir main fonksiyonun buluması gerekecekti. Hedef dosyanın ismi "/OUT" seçeneği ile belirlenebilmektedir. Örneğin: cl /c a.c cl /c b.c link /OUT:myutil.dll /DLL a.obj b.obj Burada "myutil.dll" dosyası oluşturulacaktır. Yukarıdaki işlem aslında tek hamlede "cl" programının komut satırında "/LD" seçeneği kullanılarak aşağıda gibi de yapılabilmektedir: cl /LD a.c b.c Burada ilk dosyanın ismine ilişkin DLL dosyası (yani "a.dll" dosyası) oluşturulacaktır. Ancak dinamik kütüphaneye istediğimiz ismi vermek istiyorsak "cl" komut satırında "/Fe" kullanılması gerekir. "/OUT" seöeneği bir bağlayıcı seçeneğidir. "/Fe" seçeneği ise bir "cl" derleyicisinin seçeneğidir. (Başka bir deyişle biz "cl" derleyicisini "/Fe" seçeneği ile çalıştırdığımızda aslında "cl" derleyicisi "link" isimli bağlayıcıyı "/OUT" seçeneği ile çalıştırmaktadır.) Örneğin: cl /Fe:myutil.dll /LD a.c b.c Bura artık "myutil.dll" isimli dosya oluşturulacaktır. Teknik anlamda Microsoft Windows sistemlerinde ".EXE" dosyası ile ".DLL" dosyası arasında format bakımından farklılık yoktur. He iki dosya da "PE (Portable Executable)" dosya formaına ilişkindir. ".EXE" doaysının ".DLL" dosyasından tek farkı bir "entry point" yani main fonksiyonuna sahip olmasıdır. Bir DLL içerisinde main fonksiyonunun olması o DLL dosyasını EXE dosya yapmaz. DLL dosyasının içerisindeki main fonksiyonu sıradan bir fonksiyon gibi DLL'in içerisinde bulunur. Windows'ta bir DLL içerisindeki fonksiyonun ya da global nesnenin dışarıdan kullanılabilmesi için o fonksiyonun ya da global nesnenin adresinin PE formatının "export tablosu" denilen bir yerinde bulunması gerekir. İşte DLL içerisindeki bir global nesnenin ya da fonksiyonun adresinin export tablosuna yazılması için Micrososft derleyicilerine özgü __declspec(dllexport) belirleyicisinin kullanılması gerekmektedir. Aksi takdirde o fonksiyonlar ve nesneler başka bir modülden (yani başka bir DLL ya da EXE programdan) kullanılamazlar. Aşağıdaki örnekte DLL içerisinde foo ve g_a dışarıdan kullanılbilir ancak bar g_b dışarıdan kullanılmaz. Tabii bu bar ve g_b DLL'in kendi içeriisndeki fonksiyonlardan kullanılabilir. Örneğin: #include __declspec(dllexport) int g_y; __declspec(dllexport) void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } Buarada g_y global değişkeni ve foo fonksiyonu dışarıdan (örneğin bir exe dosya tarafından) kullanılabilir. Ancak bar fonksiyonu dışarıdan kullanılamaz. Aslında C++'taki sınıf (class) kavramı yapay ve mantıksal bir kavramdır. Aşağı seviyeli dünyada sınıf diye bir kavram yoktur. Aslında C++'ta sınıflardaki üye fonksiyonlar global fonksiyonlar gibi derlenirler. Dolayısıyla C++'ta bir sınıfı DLL'e yerleştirmek için yine üye fonksiyonların önünde hangi üye fonksiyonlar dışarıdan kullanılacaksa __declspec(dllexport) bildirimlerinin yapılması gerekir. Örneğin: class Sample { public: __declspec(dllexport) Sample(int a); __declspec(dllexport) void foo() const; __declspec(dllexport) void disp() const; void bar() const; private: int m_a; }; Bu örnekte sınıfın "yapıcı fonksiyonu (constructor)" foo ve disp fonksiyonları dışarıdan kullanılabilir. Ancak bar fonksiyonu dışarıdan kullanılamaz. __declspec(dllexport) belirleyicisi tanımlama sırasında kullanılmak zorunda değildir. * Örnek 1, #include using namespace std; class Sample { public: __declspec(dllexport) Sample(int a); __declspec(dllexport) void foo() const; __declspec(dllexport) void disp() const; void bar() const; private: int m_a; }; Sample::Sample(int a) { m_a = a; } void Sample::foo() const { cout << "foo" << endl; } void Sample::disp() const { cout << m_a << endl; } void Sample::bar() const { cout << "bar" << endl; } * Örnek 2, Bir sınıfın bütün üye fonksiyonlarının dışarıdan kullanılması isteniyorsa Windows sistemlerinde __declspec(dllexport) belirleyicisi class anahtar sözcüğü ile sınıf ismi arasına getirilebilir. class __declspec(dllexport) Sample { public: Sample(int a); void foo() const; void disp() const; void bar() const; private: int m_a; }; Burada sınıfın bütün üye fonksiyonları dışarıdan kullanılabilir. #include using namespace std; class __declspec(dllexport) Sample { public: Sample(int a); void foo() const; void disp() const; void bar() const; private: int m_a; }; Sample::Sample(int a) { m_a = a; } void Sample::foo() const { cout << "foo" << endl; } void Sample::disp() const { cout << m_a << endl; } void Sample::bar() const { cout << "bar" << endl; } Windows'ta bir DLL içerisindeki fonksiyonlar dışarıdan kullanılırken yalnızca prototip yazılması aslında yeterlidir. Benzer biçimde bir DLL içerisindeki global nesneler dışarıdan kullanılırken yalnızca extern bildirimi yeterlidir. Ancak ilgili fonksiyonun ve global nesnenin DLL içerisinde olduğu derleyiciye bildirilirse derleyici daha etkin kod üretebilmektedir. (Bu konudaki ayrıntılar "Windows Sistem Programlama Kursu" içerisinde ele alınmaktadır.) İşte derleyiciye ilgili fonksiyonun ya da global nesnenin bir DLL içeirisinde olduğunu anlatabilmek için fonksiyonun prototipinin önüne __declspec(dllimport) belirleyicisi getirilir. * Örnek 1, #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ __declspec(dllimport) double add(double a, double b); __declspec(dllimport) double sub(double a, double b); __declspec(dllimport) double multiply(double a, double b); __declspec(dllimport) double divide(double a, double b); __declspec(dllimport) void foo(void); /* extern declarations */ __declspec(dllimport) extern int g_x; __declspec(dllimport) extern int g_y; #endif Burada DLL içerisindeki fonksiyonların prototiplerinin başına __declspec(dllimport) belirleyicisi getirilmiştir. Ancak DLL içerisinden export edilmiş fonksiyonlar ve global nesneler kullanılırken ayrıca bağlama aşamasında bağlayıcının hangi DLL'den hangi sembollerin kullanıldığını bilmesi gerekir. İşte DLL kullanan programlar derlenirken bağlama aşamsında "DLL'in import kütüphanesi" denilen bir kütüphanenin de bulundurulması gerekmektedir. DLL'in import kütüphanesi DLL yaratılırken bağlayıcı tarafından zaten yaratılmaktadır. Örneğin: cl /Fe:myutil.dll /LD a.c b.c Burada biz "myutil.dll" isimli dinamik kütüphaneyi yaratmak istemekteyiz. İşte bu kütüphane yaratılırken aynı zamanda DLL'in import kütüphanesi de yaratılmaktadır. DLL'in import kütüphanesinin uzantısı ".lib" biçimindedir. Yukarıdaki derleme işleminin sonucunda aynı zamanda "myutil.lib" isimli DLL'in import kütüphanesi de oluşturulmaktadır. Her ne kadar import kütüphanelerinin uzantıları da ".lib" biçiminde olsa da bu dosyalar statik kütüphane dosyaları değildir. Bu dosya içerisinde yalnızca "DLL içerisindeki fonksiyonlara ilişkin bazı bilgiler" vardır. İşte bu dosya dinamik kütüphaneyi kullanan program bağlanırken bağlama aşamasında kulanılmalıdır. Örneğin: cl app.c myutil.lib Windows'ta bir DLL'i kullanabilmek için yalnızca DLL dosyası değil aynı zamanda o DLL'in import kütüphanesinin de elimizde bulunuyor olması gerekir. DLL'in import kütüphanesi yalnızca bağlama aşamasında kullanılmaktadır. Programın hedef makineye konuşlandırılması sırasında import kütüphanesinin hedef makineye kopyalanmasına gerek yoktur. Aşağıdaki örnekte "a.c" ve "b.c" dosyalarından "myutil.dll" dosyası oluşturulmuştur. Sonra bu DLL'i kullanan "app.c" programı derlenerek çalıştırılmıştır. Bu işlemleri komut satırında şöyle yapabilirsiniz: cl /OUT:myutil.dll /LD a.c b.c cl app.c myutil.lib Aşağıda program kodları da verilmiştir: /* app.c */ #include __declspec(dllexport) int g_x; __declspec(dllexport) double add(double a, double b) { return a + b; } __declspec(dllexport) double sub(double a, double b) { return a - b; } __declspec(dllexport) double multiply(double a, double b) { return a * b; } __declspec(dllexport) double divide(double a, double b) { return a / b; } /* b.c */ #include __declspec(dllexport) int g_y; __declspec(dllexport) void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } /* app.c */ #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ __declspec(dllimport) double add(double a, double b); __declspec(dllimport) double sub(double a, double b); __declspec(dllimport) double multiply(double a, double b); __declspec(dllimport) double divide(double a, double b); __declspec(dllimport) void foo(void); /* extern declarations */ __declspec(dllimport) extern int g_x; __declspec(dllimport) extern int g_y; #endif /* app.c */ #include #include "myutil.h" int main(void) { double result; result = add(10, 20); printf("%f\n", result); result = sub(10, 20); printf("%f\n", result); result = multiply(10, 20); printf("%f\n", result); result = divide(10, 20); printf("%f\n", result); foo(); return 0; } Kütüphaneler için birer başlık dosyası oluşturmanın iyi bir teknik olduğunu belirtmiştik. Bu başlık dosyası hem kütüphane derlenirken hem de kütüphane kullanılırken include edilir. Pekiyi bu başlık dosyasının içerisinde fonksiyon prototiplerinin önünde __declspec(dllexport) belirleyicisi mi yoksa __declspec(dllimport) belirleyicisi mi bulunmalıdır? İşte kütüphanenin kendisi derlenirken __declspec(dllexport) belirleyicisi kütüphaneyi kullanan kodlar derlenirken __declspec(dllimport) belirleyicisi bulunmalıdır. Bu işlem basit bir sembolik sabite dayalı olarak gerçekleştirilebilir. * Örnek 1, #ifdef DLLBUILD #define DLLSPEC __declspec(dllexport) #else #define DLLSPEC __declspec(dllimport) #endif Burada DLLBUILD isimli sembolik sabit define edilmişse DLLSPEC makrosu yerine __declspec(dllexport) belirleyicisi define edilmemişse DLLSPEC makrosu yerine __declspec(dllimport) belirleyicisi yerleştirilecektir. Bu durumda fonksiyon prototipleri şöyle belirtilebilir: DLLSPEC double add(double a, double b); DLLSPEC double sub(double a, double b); DLLSPEC double multiply(double a, double b); DLLSPEC double divide(double a, double b); DLLSPEC void foo(void); İşte eğer DLL'in kendisi oluşturulacaksa bu başlık dosyasının include edildiği yerin başına DLLBUILD sembolik sabiti define edilir, eğer DLL kullanılacaksa bu sembolik sabit define edilmez. Böylece tek bir başlık dosyası hem DLL'deki dosyalar tarafından hem de o DLL'i kullanan dosyalar tarafından include edilebilir. Aşağıda program kodları verilmiştir: /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ #ifdef DLLBUILD #define DLLSPEC __declspec(dllexport) #else #define DLLSPEC __declspec(dllimport) #endif /* Function Prototypes */ DLLSPEC double add(double a, double b); DLLSPEC double sub(double a, double b); DLLSPEC double multiply(double a, double b); DLLSPEC double divide(double a, double b); DLLSPEC void foo(void); /* extern declarations */ DLLSPEC extern int g_x; DLLSPEC extern int g_y; #endif /* a.c */ #define DLLBUILD #include #include "myutil.h" int g_x; 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; } /* b.c */ #define DLLBUILD #include #include "myutil.h" int g_y; void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } /* app.c */ #include #include "myutil.h" int main(void) { double result; result = add(10, 20); printf("%f\n", result); result = sub(10, 20); printf("%f\n", result); result = multiply(10, 20); printf("%f\n", result); result = divide(10, 20); printf("%f\n", result); foo(); return 0; } Bir DLL'i kullanan bir program çalıştırılmak istendiğinde işletim sistemi o programın kullandığı DLL'i sırasıyla belirli bazı dizinlerde aramaktadır. Çalıştırılabilen dosyanın içerisine kullanılan DLL'in yalnızca ismi yazılmaktadır. Onun bulunduğu yer yazılmamaktadır. Windows'ta çalıştırılabilen dosyanın kullandığı DLL'ler sırasıyla şu dizinlerde aranmaktadır: -> Çalıştırılabilen program dosyasının bulunduğu dizin -> Windows\System32 dizini -> Window\System dizini -> Windows dizininin kendisi -> Programı çalıştırmak isteyen prosesin (yani CreateProcess uygulayan prosesin) çalışma dizini -> PATH çevre değişkeni ile belirtilen dizinlerde sırasıyla Dinamik kütüphaneler Windows sistemlerinde IDE'lerle daha kolay yaratılabilirler. Visual Studio IDE'sinde proje oluşturulurken proje türü olarak "Dynamic Link Library" seçilirse bu proje build edildiğinde DLL dosyası elde edilecektir. Ancak bu proje seçeneği projeye içinde yalnızca DllMain fonksiyonu bulunan bir C++ dosyası barındırmaktadır. Tabii boş olarak da bir DLL projesi oluşturulabilir. Bunun için boş bir console projesi yaratılır. İçerisine kaynak dosyalar yerleştirilir. Sonra proje ayarlarından "General/Build Type" seçeneği "Application" yerine "Dynamic Link Library" olarak seçilir. Artık proje build edildiğinde EXE dosyası yerine DLL dosyası dosyası oluşturulacaktır. Tabii DLL'in import kütüphanesi de aynı dizinde yaratılmış olacaktır. Dinamik kütüphaneler programın çalışma zamanı sırasında programcının istediği bir noktada programcı tarafından da yüklenebilmektedir. Bu duruma "dinamik kütüphanelerin dinamik yüklenmesi" denilmektedir. Dinamik yükleme özelliği hem Windows sistemlerinde hem de UNIX/Linux ve macOS sistemlerinde bulunmaktadır. Biz önce Windows sistemlerinde sonra da UNIX/linux sistemlerinde bunun yapılacağını göreceğiz. macOS sistemlerinde işlemler tamamen UNIX/Linux sistemlerinde olduğu gibi yapılmaktadır. Windows sistemlerinde dinamik kütüphanelerin dinamaik biçimde yüklenerek kullanılması sırasıyla şu aşamalardan geçilerek yapılmaktadır: -> Önce LoadLibrary API fonksiyonuyla DLLprosesin sanal bellek alanına yüklenir. LoadLibrary API fonksiyonunun prototipi şöyledir : HMODULE LoadLibraryA( LPCSTR lpLibFileName ); Fonksiyon yüklenecek olan DLL'in yol ifadesini parametre olaraj almaktadır. Eğer yol ifadesinde en az bir '\' karakteri varsa DLL yalnızca o dizinde aranmaktadır. Eğer yol ifadesinde hiçbir '\' karakteri yoksa bu durumda DLL daha önce açıkladığımız gibi bazı dizinlerde sırasıyla aranır. Bu fonksiyon dinamik kütüphaneyi yükleyerek yüklenen sanal bellek adresine geri dönmektedir. Geri dönüş değeri HMODULE türündendir. Bu tür de aslında void * olarak typedef edilmiştir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. Örneğin: HMODULE hModule; if ((hModule = LoadLibrary("MyUtil.dll")) == NULL) ExitSys("LoadLibrary"); -> DLL içerisinden çağrılacak fonksiyonun ya da kullanılacak global değişkenin adresi GetProcAddress fonksiyonuyla elde edilir. Fonksiyonun prototipi şöyledir: FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName ); Fonksiyonun birinci parametresi LoadLibrary fonksiyonundan elde edilen modül yükleme adresini, ikinci parametresi ise adresi elde edlecek fonksiyonun ya da global değişkenin ismini almaktadır. Fonksiyon başarı durumunda ilgili fonksiyonun ya da global değişkenin adresine başarısızlık durumunda NULL adrese gerei dönmektedir. Fonksiyonun geri dönüş değeri olan FARPROC 32 bit Windows sistemlerinde aşağıdaki typedef edilmiştir: typedef int (WINAPI *FARPROC)(); 64 bit Windows sistemlerinde ise şöyle typedef edilmiştir: typedef INT_PTR (WINAPI *FARPROC)(); Bu geri dönüş değeri genel bir tür biçiminde bulundurulmuştur. Uygun fonksiyon türüne dönüştürrülmelidir. Örneğin: typedef double (*PROCADDR)(double, double); ... PROCADDR padd; if ((padd = (PROCADDR)GetProcAddress(hModule, "add")) == NULL) ExitSys("GetProcAddress"); GetProcAddress fonksiyonunda bir noktaya dikkat edilmesi gerekir. Derleyiciler C'deki değişken isimlerini amaç dosyaya dekore ederek yazabilmektedir. Bizim GetProcAddress fonksiyonunda bu dekore edilmiş isimleri kullanmamız gerekmektedir. Microsoft C derleyicilerinde cdecl çağırma biçiminde sembollerin başına '_' karakteri getirerek bir dekorasyon uygulamamaktadır. Fakat Microsoft bağlayıcıları sembolleri DLL'in export tablosuna yazarken bu baştaki '_' karakterlerini atmaktadır. C++'ta farklı parametrik yapılara ilişkin aynı isimli fonksiyonlar bulunabileceği için C++ derleyicilerinin hepsi meecburen bir isim dekorasyonu uygulamaktadır. İşte programcının export sembol isimlerin emin olabilmesi için dumpbin programı ile DLL dosyasının export tablosuna bakabilir. Örneğin: C:\Dropbox\Shared\Kurslar\SysProg-1\Src\Libraries\Dynamic>DUMPBIN /EXPORTS myutil.dll Microsoft (R) COFF/PE Dumper Version 14.39.33523.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file myutil.dll File Type: DLL Section contains the following exports for myutil.dll 00000000 characteristics FFFFFFFF time date stamp 0.00 version 1 ordinal base 7 number of functions 7 number of names ordinal hint RVA name 1 0 00001000 add 2 1 00001060 divide 3 2 00001080 foo 4 3 0001B39C g_x 5 4 0001B398 g_y 6 5 00001040 multiply 7 6 00001020 sub Summary 2000 .data 7000 .rdata 1000 .reloc 12000 .text Ancak eğer biz bir C++ dosyasını derleyip ondan dinmaik kütüphane yapmış olsaydık sembol isimleri oldukça farklılaşacaktı. Örneğin: C:\Dropbox\Shared\Kurslar\SysProg-1\Src\Libraries\Dynamic>DUMPBIN /EXPORTS myutilcpp.dll Microsoft (R) COFF/PE Dumper Version 14.39.33523.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file myutilcpp.dll File Type: DLL Section contains the following exports for myutilcpp.dll 00000000 characteristics FFFFFFFF time date stamp 0.00 version 1 ordinal base 7 number of functions 7 number of names ordinal hint RVA name 1 0 00001000 ?add@@YANNN@Z 2 1 00001060 ?divide@@YANNN@Z 3 2 00001080 ?foo@@YAXXZ 4 3 0001A968 ?g_x@@3HA 5 4 0001A96C ?g_y@@3HA 6 5 00001040 ?multiply@@YANNN@Z 7 6 00001020 ?sub@@YANNN@Z Summary 2000 .data 7000 .rdata 1000 .reloc 12000 .text -> Artık adresi elde edilmiş olan fonksiyon çağrılabilir, global değişken kullanılabilir. Örneğin: result = padd(10, 20); -> Dinamik kütüphanenin kullanımı bittikten sonra kütüphane FreeLibrary API fonksiyonu ile prosesin adres alanından boşaltılabilir. FreeLibrary fonksiyonunun prototipi şöyledir: BOOL FreeLibrary( HMODULE hLibModule ); Fonksiyon modülün yüklenme adresini parametre olarak alır, başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır değerine geri döner. Tabii proses sonlaadığında bütün dinamik kütüphaneler zaten adres alanından boşaltılmaktadır. Aşağıda Windows sistemlerinde dinamik kütüphanenin dinamik yüklenmesine ilişkin bir örnek verilmiştir. Bu örnekteki "myutil.dll" dosyasının proje dizininde bulunduğundan emin olunuz. DLL'in derlenmesini komut satırından şöyle yapabilirsiniz. cl /Fe:myutil.dll a.c b.c /LD Aşağıda programın kodları verilmiştir: /* app.c */ #include #include #include void ExitSys(LPCSTR lpszMsg); typedef double (*PROCADDR)(double, double); int main(void) { HMODULE hModule; PROCADDR padd, psub, pmultiply, pdivide; double result; if ((hModule = LoadLibrary("myutilcpp.dll")) == NULL) ExitSys("LoadLibrary"); if ((padd = (PROCADDR)GetProcAddress(hModule, "?add@@YANNN@Z")) == NULL) ExitSys("GetProcAddress"); result = padd(10, 20); printf("%f\n", result); if ((psub = (PROCADDR)GetProcAddress(hModule, "sub")) == NULL) ExitSys("GetProcAddress"); result = psub(10, 20); printf("%f\n", result); if ((pmultiply = (PROCADDR)GetProcAddress(hModule, "multiply")) == NULL) ExitSys("GetProcAddress"); result = pmultiply(10, 20); printf("%f\n", result); if ((pdivide = (PROCADDR)GetProcAddress(hModule, "divide")) == NULL) ExitSys("GetProcAddress"); result = pdivide(10, 20); printf("%f\n", result); FreeLibrary(hModule); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ #ifdef DLLBUILD #define DLLSPEC __declspec(dllexport) #else #define DLLSPEC __declspec(dllimport) #endif /* Function Prototypes */ DLLSPEC double add(double a, double b); DLLSPEC double sub(double a, double b); DLLSPEC double multiply(double a, double b); DLLSPEC double divide(double a, double b); /* extern declarations */ DLLSPEC extern int g_x; #endif /* myutil.c */ #define DLLBUILD #include #include "myutil.h" int g_x; 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; } >>> UNIX/Linux Sistemleri : UNIX/Linux sistemlerinde dinamik kütüphane oluşturmak için önce dinamik kütüphane oluşturulacak ".c" dosyaları "position indepenedent code" tekniği ile derlenmelidir. "Position Independent Code" ilgili dinamik kütüphanenin yüklenme yerinden bağımsız çalışabilmesini sağşamaktadır. Windows bu tekniği tercih etmemiştir. Windows'un yükleyicisi "relocation" işlemi ile DLL'leri farklı yerlere yükleyebilmektedir. Position Independent Code tekniği ile derlenmiş olan dosyalar "-shared" bağlama seçeneği ile bağlanırsa dinamik kütüphane oluşturulabilmektedir. UNIX/Linux sistemlerinde kütüphane içerisindeki global sembollerin hepsi otomatik olarak export edilmektedir. Dolayısıyla bu sistemlerde Windows sistemlerinde olduğu gibi __declspec(dllexport) ve __declspec(dllimport) biçiminde bildirimler yoktur. Tabii bu konuda da bazı ayrıntılar bulunmaktadır. gcc ve clang derleyicilerinde bir dosyanın "konumdan bağımsız" biçimde derlenmesi için "-fPIC" seçeneğinin kullanılması gerekir. Örneğin: $ gcc -c -fPIC a.C $ gcc -c -fPIC b.C Burada "a.o" ve "b.o" dosyaları oluşturulacktır. Ayrıca yukarıda da belirttiğimiz Bağlama işleminde "-shared" seçeneğinin kullanılması gerekmektedir. Örneğin: $ gcc -o libmyutil.so -shared a.o b.o Aslında bu iki işlem tek hamlede de aşağıdaki gibi yapılabilir: $ gcc -o libmyutil.so -shared -fPIC a.c b.c Yine UNIX/Linux sistemlerinde dinamik kütüphane dosyaları başına "lib" öneki getirilerek isimlendirilmelidir. * Örnek 1, Aşağıda dinamik kütüphaneyi oluşturan "a.c" ve "b.c" dosyaları verilmiştir. Dinamik kütüphaneyi yukarıda belirttiğimiz biçimde oluşturmayı deneyiniz. /* a.c */ #include int g_x; 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; } /* b.c */ #include int g_y; void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } UNIX/Linux sistemlerinde "dinamik kütüphanelerin import kütüphanesi" denilen kütüphaneleri yoktur. Dinamik kütüphane kullanan programlar doğrudan bu dinamik kütüphane dosyasının kendisini bağlama aşamasında kullanmaktadır. Örneğin "app.c" programı "libmyutil.so" dinamik kütüphanesini kullanıyor olsun. Bu "app.c" programının derlenmesi şöyle yapılmaktadır: gcc -o app app.c libmyutil.so Aşağıdaki "app.c" programını bu biçimde derleyiniz. * Örnek 1, /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ double add(double a, double b); double sub(double a, double b); double multiply(double a, double b); double divide(double a, double b); void foo(void); void bar(void); /* extern declarations */ extern int g_x; extern int g_y; #endif /* app.c */ #include #include "myutil.h" int main(void) { double result; result = add(10, 20); printf("%f\n", result); result = sub(10, 20); printf("%f\n", result); result = multiply(10, 20); printf("%f\n", result); result = divide(10, 20); printf("%f\n", result); foo(); bar(); return 0; } UNIX/Linux sistemlerinde yine çalıştırılabilen dosyanın içerisine yalnızca dinamik kullanılan dinamik kütüphanelerin isimleri yazılmaktadır. Dinamik kütüphaneler yine bu sistemlerde de belli yerlerde aranmaktadır. Ancak bu sistemlerde dinamik kütüphaneler çalıştırılabilen dosyanın bulunduğu dizinde ya da onu çalıştıran prosesin çalışma dizininde aranmamaktadır. Bu nedenle örneğin Linux sistemlerinde dinamik kütüphane kullanan bir programla dinamik kütüphanenin kendisi aynı dizinde bulunuyor olsa bile program çalıştırıldığında dinamik kütüphane bulunamayacaktır. UNIX/Linux sistemlerinde dinamik kütüphanelerin aranma prosedürlerine ilişkin bazıı ayrıntılar vardır. Biz burada bu ayrıntılar üzerinde durmayacağız. Ancak kabaca arama için üç önemli adımı şöyle belirtebiliriz: -> Programı çalıştıran (yani exec yapan) prosesin LD_LIBRARY_PATH çevre değişkeni ile belirtilen dizinlerinde tek tek arama yapılmaktadır. Bu eçvre değişkeninin değeri ':' karakterleriyle ayrılan dizinler biçiminde olabilir. Örneğin: $ export LD_LIBRARY_PATH=/home/kaan:/home/kaan/Study:. Burada dinamik kütüphaneler sırasıyla "/home/kaan" dizininde, "/home/kaan/Study" dizininde ve o anda exec yapan prosesin çalışma dizininde aranacaktır. Tabii kabuk üzerinde çalıtırmayı aşağıdaki gibi de yapabiliriz: $ LD_LIBRARY_PATH=. ./app -> Çalıştırılacak programın ".dynamic" bölümünde DT_RUNPATH özelliği varsa oradaki dizinde aranmaktadır. -> "/lib" ve "/usr/lib" dizinlerine bakılır. Bazı 64 bit Linux dağıtımlarında "/lib64" ve "/usr/lib64" dizinlerine de bakılmaktadır. Eğer bu sistemlerde oluşturduğumuz dinamik kütüphaneler başka programlar tarafından da kullanılıyorsa onun yerleştirilmesi gereken en doğal "/usr/lib" dizinidir. "/lib" dizini daha aşağı seviyeli işletim sistemi tarafıdan kullanılan dinamik kütüphanelere ayrılmıştır. Dinamik kütüphanelerin UNIX/Linux sistemlerinde dinamik yüklenmesi aslında Windows sistemlerindekine benzemektedir. İşlemler sırasıyla şöyle yürütülür: -> Önce dinamik kütüphane ldopen fonksiyonuyla adres alanına yüklenir. Bu fonksiyonu Windows sistemlerindeki LoadLibrary fonksiyonuna benzetebilirsiniz. Bu fonksiyon yine dinamik kütüphanenin bellekteki yükleme adresine geri dönmektedir. Fonksiyonun prototipi şöyledir: #include void *dlopen(const char *filename, int flags); Fonksiyonun birinci parametresi yüklenecek dinamik kütüphane dosyasının yol ifadesini belirtmektedir. Burada yol ifadesinde hiçbir '/' karakteri kullanılmazsa dosya yukarıda belirtilen dizinlerde sırasıyla aranmaktadır. Ancak buradaki yol ifadesinde en az bir '/' kullanılırsa dosya yalnızca o yol ifadesinde belirtilen dizinde aranır. İkinci parametre sembol çözümlemesinin ne zaman yapılacağına ilişkin bayraklardan oluşmaktadır. Bu parametre RTLD_LAZY ya da RTLD_NOW değerlerinden biri biçiminde girilebilir. RTLD_LAZY sembol çözümlemesinin başvuru (yani fonksiyonun çağrılması ya da global değişkenin kullanılması sırasında) sırasında yapılacağını RTLD_NOW ise yükleme sırasında yapılacağını belirtmektedir. Diğer bayraklar için dokümanalara başvurulabilir. Fonksiyon başarı durumunda yükleme adresine, başarısızlık durumunda NULL adrese geri döner. Başarısızlık nedeni için errno değişkeni set edilmez. Başarısızlık nedeni yazısal olarak dlerror fonksiyonuyla elde edilmelidir. dlerror fonksiyonun prototipi şöyledir: char *dlerror(void); Fonksiyon statik bir biçimde tahsis edilmiş olan hata yazısının adresine geri dönmektedir. Örneğin: void *soaddr; ... if ((soaddr = dlopen("./libmyutil.so", RTLD_LAZY)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } Tıpkı Windows sistemlerinde olduğu gibi dlopen fonksiyonu dinamik kütüphanenin yol ifadesi belirtilirken eğer hiç '/' karakteri kullanılmazsa bu durumda dosya Linux sistemlerindeki yukarıda belirttiğimiz dizinlerde aranmaktadır. (Linux sistemlerinde arama sırasında çalışma dizinine bakılmadığını anımsayınız). Eğer yol ifadesinde en az bir tane '/' karakteri kullanılmışsa dosya belirtilen yol ifadesinde aranmaktadır. -> dlsym fonksiyonuyla dinamik kütüpahane içerisindeki herhangi bir fonksiyon ya da nesnenin adresi elde edilebilmektedir. dlsym fonksiyonun prototipi şöyledir: #include void *dlsym(void *handle, const char *symbol); Fonksiyonun birinci parametresi dlopen fonksiyonundan elde edilen adresi, ikinci parametresi ise adresi elde edilecek fonksiyon ya da global nesnenin ismini belirtmektedir. Fonksiyon başarı dıurumunda ilgili nesnenin adresine başarısızlık durumunda NULL adrese geri dönmektedir. Yine başarısızlık nedeni dlerror fonksiyonuyla elde edilmelidir. C'de data adreslerinden fonksiyon adreslerine, fonksiyon adreslerinden data adreslerine tür dönüştürme operatör ile bile dönüştürmenin geçerli olmadığına dikkat ediniz. (Ancak derleyicilerin çoğu buna izin vermektedir.) Anımsayanacağınız gibi void * türü bir data adresi kabul edilmektedir. Örneğin: typedef double (*PROCADDR)(double); ... PROCADDR padd; ... if ((*(void **)&padd = dlsym(soaddr, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } dlsym fonksiyonunu Windows sistemlerindeki GeProcAddress fonksiyonuna benzetebilirsiniz. -> Artık adresi elde edilmiş olan fonksiyon çağrılabilir, global değişken kullanılabilir. -> Dinamik kütüphanenin kullanımı bittiğinde kütüphane dlclose fonksiyonuyla boşaltılır. Fonksiyonun prototipi şöyledir: #include int dlclose(void *handle); Fonksiyon parametre olarak dinamik kütüphanenin yükleme adres,n, alıp onu adres alanından yok etmektedir. Fonksiyon başarı durumunda sıfır değrine başarısızlık durumunda sıfır dışı bir değere geri dönemektedir. Yine başarısızlığın nedeni dlerror fonksiyonuyla yazdırılabilir. Örneğin: dlclose(soaddr); Bütün bu fonksiyonlar "libdl.so" kütüphanesi içerisinde bulunmaktadır. Bu nedenle derleme yaparken komut satırında "-ldl" argümanını bulundurunuz. Örnek bir kullanım aşağıda verilmiştir. Derlemeleri aşağıdaki gibi yapabilirsiniz: $ gcc -fPIC -o libmyutil.so libmyutil.c -shared $ gcc -o app app.c -ldl Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, /* app.c */ #include #include #include void exit_sys(const char *msg); typedef double (*PROCADDR)(double, double); int main(void) { void *soaddr; PROCADDR padd, psub, pmultiply, pdivide; double result; if ((soaddr = dlopen("./libmyutil.so", RTLD_LAZY)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } if ((*(void **)&padd = dlsym(soaddr, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = padd(10, 20); printf("%f\n", result); if ((*(void **)&psub = dlsym(soaddr, "sub")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = psub(10, 20); printf("%f\n", result); if ((*(void **)&pmultiply = dlsym(soaddr, "multiply")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = pmultiply(10, 20); printf("%f\n", result); if ((*(void **)&pdivide = dlsym(soaddr, "divide")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = pdivide(10, 20); printf("%f\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* 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; } Bir kütüphanedeki fonksiyonlar çağrılabilmesi için çağrılan kaynak dosyada o fonksiyonların prototipilerinin bulundurulması gerekir. Benzer biçimde kütüphane içerisindeki global değişkenlerin kullanılabilmesi için de o global değişkenlerin extern bildirimlerinin bulundurulması gerekir. Her kütüphane için o kütüphanedeki bildirimleri barındıran bir başlık dosyasının bulundurulması iyi bir tekniktir. * Örnek 1, /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ double add(double a, double b); double sub(double a, double b); double multiply(double a, double b); double divide(double a, double b); void foo(void); void bar(void); /* extern Declarations */ extern int g_x; extern int g_y; #endif Böylece bu kütüphaneyi kullanacak kişiler bu başlık dosyasını include ederek bütün kütüphane ile ilgili bildirimleri bulundurmuş olurlar. Eğer kütüphane çok fazla öğeden oluşyorsa tek bir başlık dosyası önişlemci (preprocessor) zamanını uzatabilir. Bu durumda bir tane değil birden çok başlık dosyası bulundurulabilir. Örneğin aslında standart C fonksiyonlarının hepsi tek bir kütüphane içerisindedir. Ancak onlara ilişkin bildirimler çeşitli başlık dosyalarına yaydırılmıştır. Bağlayıcılar onlara verilen amaç dosyalar dışında ayrıca temel bazı kütüphanelere otomatik olarak bakmaktadır. Microsoft'un bağlayıcı programı olan "link.exe" hiç belirtilmese bile standart C fonksiyonlarının bulunduğu, Windows API fonksiyonlarının bulunduğu kütüphanelere zaten bakmaktadır. Ancak programcı bağlayıcının kendi kütüphanelerine de bakmasını istiyorsa bunu bağlayıcıya komut satırından söylemelidir. Anımsanacağı gibi Microsoft'un Cve C++ derleyicisi olan "cl.exe" programı "/c" seçeneği belirtilmezse derleme sonrasında "link.exe" bağlayıcısını kendisi çalıştırmaktadır. İşte biz de "cl.exe" programının komut satırında derlenecek kaynak dosyalardan sonra uzantısı ".lib" olan kütüphane dosyalarını belirtirsek "link.exe" bağlayıcısı o kütüphane dosyalarına da bağlama aşamasında bakmaktadır. Örneğin: cl app.c myutil.lib Burada "app.c" programı derlenerek "app.obj" dosyası oluşturulacak sonra bağlama aşamasında "myutil.lib" dosyasına da bakılacaktır. Oluşturulacak çalıştırılabilir dosyanın ismi ilk kaynak dosyanın ismi olarak (örneğimizde "app.exe") alınacaktır. Tabii istenirse "cl.exe" komut satırında /Fe: argümanı ile çalıştırılabilir dosyaya arzu edilen bir isim de verilebilmektedir. Örneğin: cl /Fe:project.exe app.c myutil.lib Tabii istersek derleyici ve bağlayıcıyı ayrı ayrı da çalıştırabiliriz. Örneğin: cl /c app.c link app.obj myutil.lib Burada yine default olarak "link.exe" programı ilk amaç dosyanın ismini çalıştırılabilir dosyaya vermektedir. Ancak çalıştırılabilen dosyanın ismi /OUT: seçeneği ile de bağlayıcıya verilebilmektedir. Örneğin: cl /c app.c link /OUT:project.exe app.obj myutil.lib Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, örnekte "a.c" ve "b.c" dosyalarından "myutil.lib" statik kütüphanesi oluiştuurulmuş ve bu statik kütüphane de "app.c" programından kullanılmıştır. İşlemleri komut satırından aşağıdaki sırada yapabilirsiniz: cl -c a.c cl -c b.c lib /OUT:myutil.lib a.obj b.obj cl app.c myutil.lib Programın kodları ise şu şekildedir: /* a.c */ #include int g_x; 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; } /* b.c */ #include int g_y; void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ double add(double a, double b); double sub(double a, double b); double multiply(double a, double b); double divide(double a, double b); void foo(void); void bar(void); /* extern declarations */ extern int g_x; extern int g_y; #endif /* app.c*/ #include #include "myutil.h" int main(void) { double result; result = add(10, 20); printf("%f\n", result); result = sub(10, 20); printf("%f\n", result); result = multiply(10, 20); printf("%f\n", result); result = divide(10, 20); printf("%f\n", result); foo(); bar(); return 0; } /*================================================================================================================================*/ (93_09_06_2024) & (94_16_06_2024) & (95_22_06_2024) & (96_23_06_2024) > TCP/IP Socket Programming : Kursumuzun bu bölümünde IP prokol ailesi ile ağa bağlı birimler arasında haberleşmelerin nasıl yapıldığı üzerinde duracağız. Bu bağlamda TCP ve UDP protokollerini inceleyeceğiz ve bu protokolleri kullanarak soket arayüzü ile temel programların nasıl yazıldığnı açıklayacağız. 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 önceden yapılmış olması gerekir. İşte ağ haberleşmesinde önceden belirlenmiş kurallar topluluğuna "protokol" denilmektedir. Ağ haberleşmesi için tarihsel süreç içerisinde pek çok protokol ailesi gerçekletirilmiştir. Bunların bazıları büyük şirketlerin kontrolü altındadır ve 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 protokol ailesidir. Protokol ailesi (protocol family) denildiğinde birbirleriyle ilişkili bir grup protokol anlaşılmaktadır. Bir protokol ailesinin pek çok protokolü başka protokollerin üzerine konumlandırılmış olabilmektedir. Böylece protokol aileleri katmanlı (layered) bir yapıya sahip olmuştur. Üst seviye bir protokol alt seviye protokolün "zaten var olduğu fikriyle" o alt seviye protokol kullanılarak oluşturulmaktadır. Bu katmanlı yapıyı prosedürel programlama tekniğinde "zaten var olan bir fonksiyonu kullanarak daha yüksek seviyeli bir fonksiyon yazmaya" benzetebiliriz. Ağ haberleşmesi için katmanlı bir protokol yapısının kavramsal olarak nasıl oluşturulması gerektiğine yönelik ISO tarafından 80'li yılların başlarında "OSI Model (Open System Interconnection Model)" isimli bir referans dokümanı oluşturulmuştur. OSI model bir gerçekleştirim değildir. Kavramsal bir referans dokümanıdır. Ancak bu referans dokümanı pek çok çalışma için bir zemin oluşturmuştur. OSI referans modeline göre bir protokol ailesinde tipik olarak 7 katman bulunmalıdır. Bu katmanlar aşağıdaki gibi birbirlerini üzerine oturtulmuştur: 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, >> En aşağı seviyeli elektriksel tanımlamaların yapıldığı katmana "fiziksel katman (physical layer)" denilmektedir. (Örneğin kabloların, konnektörlerin özellikleri, akım, gerilim belirlemeleri vs. gibi.) Yani bu katman iletişim için gereken fiziksel 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ü (IEEE 802.11)" denilen bir protokole uygun tasarlanmıştır. Bu ethernet protokolü OSI'nin fiziksel ve veri bağlantı katmanına karşılık gelmektedir. Ethernet protokolünde yerel ağa bağlı olan her birimin ismine "MAC adresi" denilen 6 byte'lı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 byte'lar bir paket adı altında bir araya getirilir sonra ilgili fiziksel katmanla seri bir biçimde gönderilir. Bugün kullandığımız yerel ağlarda aslında bilgi bir birimden diğerine değil hub'lar yoluyla ağa bağlı olan tüm birimleregönderilmektedir. Ancak bunlardan yalnızca biri gelen bilgiyi sahiplenmektedir. Bugün kablosuz haberleşmede kullanılan "IEEE 802.11" protokolü de tıpkı Ethernet protokolü gibi hem bir fiziksel katman hem de veri bağlantı katmanı tanımlamaktadır. Fiziksel katman ve veri katmanı oluşturulduğunda artık biz yerel ağda bir birimden diğerine paket adı altında bir grup byte'ı gönderip alabilir duruma gelmekteyiz. >> Ağ 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 "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 bağlı birimler için fiziksel adresler kullanılamaz. Bu ortamlarda ağa bağlı birimlere mantıksal bir adreslerin atanması gerekmektedir. İşte "network 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 adresleme değil, mantıksal adresleme sistemi kullanılmaktadır. Ayrıca bilgilerin paketlere ayrılarak router'lardan dolaşıp hedefe varması için rotalama mekanizması da bu katmanda tanımlanmaktadır. Yani elimizde yalnızca ağ katmanı ve onun aşağısındaki katmanlar varsa biz artık "internetworking" ortamında belli bir kaynaktan belli bir hedefe paketler yollayıp alabiliriz. >> Aktarım katmanı (transport layer) network katmanının üzerindedir. Aktarım katmanında artık kaynak ile hedef arasında mantıksal bir bağlantı oluşturulabilmekte ve veri aktarımı daha güvenli olarak yapılabilmektedir. Aynı zamanda aktarım katmanı "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. Bu durumda aktarım katmanında tipik şu işlemlere yönelik belirlemeler bulunmaktadır: -> Bağlantnın nasıl yapılacağına ilişkin belirlemeler -> Ağ katmanından gelen paketlerin stream tabanlı organizasyonuna ilşkin belirlemeler -> Veri aktarımını güvenli hale getirmek için akış kontrolüne ilişkin belirlemeler -> Gönderilen bilgilerin hedefte ayrıştıtılmasını sağlayan protokol port numaralandırmasına ilişkin belirlemeler >> Oturum katmanı (session) katmanı pek çok protokol ailesinde yoktur. Görevi oturum açma kapama gibi yüksek seviyeli bazı belirlemeleri yapmaktır. Örneğin bu katmanda bir grup kullanıcıyı bir araya getiren oturumların nasıl açılacağına ve nasıl kapatılacağına ilişkin belirlemeler bulunmaktadır. IP protokol ailesinde OSI'de belirtilen biçimde bir oturum katmanı yoktur. >> Sunum katmanı (presentation layer) verilerin sıkıştırılması, şifrelenmesi gibi tanımlamalar içermektedir. Yine bu katman IP protokol ailesinde OSI'de belirtildiği biçimde bulunmamaktadır. >> Nihayet protokol ailesini kullanarak yazılmış olan tüm 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, FTP gibi protokoller uygulama katmanı protokolleridir. Bugün farklı makinelerin prosesleri arasında en çok kullanılan protokol ailesi IP (Internet Protocol) denilen protokol ailesidir. IP protokol ailesi temel ve yardımcı pek çok protokolden oluşmaktadır. Aileye ismini veren ailenin ağ katmanı (network layer) protokolü olan IP protoküdür. Pekiyi pekiyi IP ailesi neden bu kadar popüler olmuştur? Bunun en büyük nedeni 1983 yılında hepimizin katıldığı Internet'in (I'nin büyük yazıldığına dikkat ediniz) bu aileyi kullanmaya başlamasıdır. 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 IP ailesinin açık bir (yani bir şirketin malı değil) protokol olması da cazibeyi çok artırmıştır. IP ailesi 70'li yıllarda Vint Cerf ve Bob Kahn tarafından geliştirilmiştir. IP ismi Internet Protocol'den gelmektedir. Burada internet "internetworking" anlamında kullanılmıştır. Cerf ve Kahn 1974 yılında önce TCP protokolü üzerinde sonra da IP protokolü üzerinde çalışmışlar ve bu protokollerin ilk versiyonlarını oluşturmuşlardır. 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 protokol 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 protokol 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. IP protokol ailesi 4 katmanlı bir ailedir. Bu ailede "fiziksel ve veri bağlantı katmanı" bir arada düşünülebilir. Bugün bunlar Ethernet ve Wireless protokolleri biçiminde kullanılmaktadır. IP ailesinin ağ katmanı aileye ismini veren IP protokolünden oluşmaktadır. Aktarım katmanı ise TCP ve UDP protokollerinden oluşur. Nihayet TCP üzerine oturtulmuş olan HTTP, TELNET, SSH, POP3, IMAP gibi pek çok protokol ailenin uygulama katmanını oluşturmaktadır. Tabii IP protokol ailesinde bu hiyerarşik yapıyla ilgili olmayan irili ufaklı pek çok protokol de bulunmaktadır. +---------------------+-------------------------------+ | Application Layer | HTTP, SSH, POP3, IMAP, ... | +---------------------+---------------+---------------+ | Transport Layer | TCP | UDP | +---------------------+---------------+---------------+ | Network Layer | IP | +---------------------+-------------------------------+ | Physical/Data Link | Ethernet | | Layer | Wireless | +---------------------+-------------------------------+ IP protokolü tek başına kullanılırsa ancak ağa bağlı bir birimden diğerine bir paket gönderip alma işini yapar. Bu nedenle bu protokolün tek başına kullanılması çok seyrektir. Uygulamada genellikle "aktarım (transport) katmanına" ilişkin TCP ve UDP ptotokolleri kullanılmaktadır. IP ailesinin uygulama katmanındaki HTTP, SSH, POP3, IMAP, FTP gibi önemli protokollerinin hepsi TCP protokolü üzerine oturtulmuştur. Ailede genellikle TCP protokolü kullanıldığı için buna kısaca "TCP/IP" de denilmektedir. IP protokolü ailenin en önemli ve taban protokolüdür. IP protokolünde ağa bağlı olan ve kendisine IP adresiyle erişilebilen her birime "host" denilmektedir. IP protokolü bir host'tan diğerine bir paket (buna IP paketi denilmektedir) bilginin gönderimine ilişkin tanımlamaları içermektedir. IP protokolünde her host'un ismine "IP adresi" denilen mantıksal bir adresi vardır. Paketler belli bir IP adresinden diğerine gönderilmektedir. IP protokolünün iki önemli versiyonu vardır: IPv4 ve IPv6. Bugün her iki versiyon da aynı anda kullanılmaktadır. IPv4'te IP adresleri 4 byte uzunluktadır. (Protokolün tasarlandığı 70'li yıllarda 4 byte adres alanı çok geniş sanılmaktaydı). IPv6'da ise IP adresleri 16 byte uzunluğundadır. TCP bağlantılı (connection-oriented), UDP bağlantısız (connectionless) bir protokoldür. Buradaki bağlantı IP paketleriyle yapılan mantıksal bir bağlantıdır. Bağlantı sırasında gönderici ve alıcı birbirlerini tanır ve haberleşme boyunca haberleşmenin güvenliği için birbirleriyle konuşabilirler. Bağlantılı protokol "client-server" tarzı bir haberleşmeyi akla getirmektedir. Bu nedenle TCP/IP denildiğinde akla "client-server" haberleşme gelmektedir. TCP modelinde client önce server'a bağlanır. Sonra iletişim güvenli bir biçimde karşılıklı konuşmalarla sürdürürlür. Tabii TCP bunu yaparken IP paketlerini yani IP protokolünü kullanmaktadır. UDP protokolü bağlantısızdır. Yani UDP protokolünde bizim bir host'a UDP paketi gönderebilmemiz için bir bağlantı kurmamıza gerek kalmaz. Örneğin biz televizyon yayını UDP modeline benzemektedir. 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. TCP "stream tabanlı", UDP ise "datagram (paket) tabanlı" bir protokoldür. Stream tabanlı protokol demek tamamen 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. Datagram tabanlı haberleşme demek ise tamamen mesaj kuyruklarında olduğu gibi bilginin paket paket iletilmesi demektir. Yani datagram haberleşmede alıcı taraf gönderen tarafın tüm paketini tek hamlede almak zorundadır. Stream tabanlı haberleşmenin oluşturulabilmesi için IP paketlerine bir numara verilmesi ve bunların hedefte birleştirilmesi gerekmektedir. Örneğin biz bir host'tan diğerine 10K'lık bir bilgi gönderelim. TCP'de bu bilgi IP paketlerine ayrılıp numaralandırılır. Bunlar hedefte birleştirilir ve sanki 10000 byte'lık ardışıl bir bilgiymiş gibi gösterilir. Halbuki UDP'de paketler birbirinden bağımsızdır. Dolayısıyla bunların hedefte birleştirilmesi zorunlu değildir. IP protokolünde bir host birtakım paketleri diğer host'a gönderdiğinde alıcı taraf bunları aynı sırada almayabilir. Bu özelliğinden dolayı TCP, ailenin en çok kullanılan aktarım (transport) katmanı protokolüdür. TCP güvenilir (reliable), UDP güvenilir olmayan (unreliable) bir protokoldür. TCP'de mantıksal bir bağlantı oluşturulduğu için yolda kaybolan paketlerin telafi edilmesi mümkündür. Alıcı taraf gönderenin bilgilerini eksiksiz ve bozulmadan aldığını bilir. Aynı zamanda TCP'de "bir akış kontrolü (flow control)" de uygulanmaktadır. Akış kontrolü sayesinde alıcı taraf tampon taşması durumuna karşı gönderici tarafı durdurabilmektedir. Halbuki UDP'de böyle bir mekanizma yoktur. Gönderen taraf alıcının bilgiyi alıp almadığını bilmez. Tüm bunlar eşliğinde IP ailesinin en çok kullanılan aktarım (transport) katmanının neden TCP olduğunu anlayabilirsiniz. Uygulama katmanındaki protokoller hep TCP kullanmaktadır. Yukarıda da belirttiğimiz gibi IP protokol ailesinde ağa bağlı olan birimlere "host" denilmektedir. Host bir bilgisayar olmak zorunda değildir. İşte bu protokol ailesinde her host'un mantıksal bir adresi vardır. Bu adrese IP adresi denilmektedir. IP adresi IPv4'te 4 byte uzunlukta, IPv6'da 16 byte uzunluktadır. Ancak bir host'ta farklı programlar farklı host'larla haberleşiyor olabilir. İşte aynı host'a gönderilen IP paketlerinin o host'ta ayrıştırılması için "protokol port numarası" diye isimlendirilen içsel bir numara uydurulmuştur. Port numarası bir şirketin içerisinde çalışanların dahili numarası gibi düşünülebilir. Port numaraları IPv4'te ve IPv6'da 2 byte'la ifade edilmektedir. İlk 1024 port numarası IP ailesinin uygulama katmanındaki protokoller için ayrılmıştır. Bunlara İngilizce "well known ports" denilmektedir. Bu nedenle programcıların port numaralarını 1024'ten büyük olacak biçimde seçmeleri gerekir. 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. IP numarası ve port numarası çiftine "IP End Point" de denilmektedir. Bilgiyi almak isteyen program kendisinin hangi portla ilgilendiğini de belirtmek durumundadır. Örneğin biz bir host'ta çalışacak bir TCP/IP ya da UDP/IP program yazmak istiyorsak o host'un belli bir port numarasına gelen bilgilerle ilgileniriz. Port numarası kavramının IP protokolünde olmadığına TCP ve UDP protokollerinde bulunduğuna dikkat ediniz. TCP ve UDP protokollerinin IP protokolü üzerine oturdulduğunu belirtmiştik. Bu ne anlama gelmektedir? Biz TCP ile belli bir IP numarası ve port numarası (end point) belirterek bir grup byte'ı göndermiş olalım. Aslında bu byte topluluğu bir TCP paketi oluşturularak bir IP paketi biçiminde yola çıkarılmaktadır. Şöyle ki: IP paketlerinin yapısı şöyledir: +-------------------------+ | IP Header | +-------------------------+ | IP Data | +-------------------------+ Burada IP Header'da söz konusu IP paketinin hedefe ulaştırılabilmesi için gerekli bilgiler bulunur. Gönderilecek asıl bilgi bu paketin "IP Data" kısmındadır. İşte bir TCP paketi aslında bir IP paketi olarak IP paketinin "IP Data" kısmına gömülerek gönderilmektedir. Bu durumda TCP paketinin genel görünümü şöyledir: +-------------------------+ | IP Header | +-------------------------+ <---+ | TCP Header | | +-------------------------+ IP Data | TCP Data | | +-------------------------+ <---+ TCP paketinin de bir header ve data alanı olduğuna ancak paketin tamamının IP paketinin data alanında yolculuk ettirildiğine dikkat ediniz. Yani TCP paketinin header ve data kısmı aslında IP paketinin data kısmı gibi oluşturulmaktadır. Böylece yolculuk eden paket aslında bir TCP paketi değil IP paketidir. TCP bilgileri bu IP paketinin data kısmında bulunmaktadır. IPv4 başlık uzunluğu 20 byte'dır. IPv4 paket başlık alanları aşağıdaki verilmiştir. <------- 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) | +----------------------------------------------------------------------------------------------+ TCP header'ı 20 byte'tan 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) | | +----------------------------------------------+-----------------------------------------------+ | | 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 header'ı 8 byte'tan 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 | Header Length | Checksum | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ IP haberleşmesi (yani paketlerin, oluşturulması, gönderilmesi alınması vs.) işletim sistemlerinin çekirdekleri tarafından yapılmaktadır. Tabii User mode programlar için sistem çağrılarını yapan API fonksiyonlarına ve kütüphanelerine gereksinim vardır. İşte bunların en yaygın kullanılanı "soket kütüphanesi" denilen kütüphanedir. Bu kütüphane ilk kez 1983 yılında BSD 4.2'de gerçekleştirilmiştir ve pek çok UNIX türevi sistem bu kütüphaneyi aynı biçimde benimsemiştir. Sonra bu kütüphane POSIX standartlarına da dahil edilmiştir. Microsoft Windows sistemleri için kendi soket kütüphanesini oluşturmuştur. Buna "Windows Socket API (WSA)" denilmektedir. Ancak Microsoft aynı zamanda klasik BSD soket arayüzünü de desteklemektedir. Yani biz Windows sistemlerinde hem başı WSAXXX ile başlayan Windows'a özgü soket fonksiyonlarını hem de klasik Berkeley soket fonksiyonlarını kullanabilmekteyiz. Böylece UNIX/Linux sistemlerinde yazdığımız soket programlarını küçük değişikliklerle Windows sistemlerine taşıyabilmekteyiz. Berkeley soket 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. Biz soket fonksiyonlarını 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 soket 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. Soket kütüphanesinin yalnızca bir API arayüzü olduğuna dikkat ediniz. Yukarıda da belirttiğimiz gibi Berkeley soket kütüphanesi POSIX tarafından desteklenmektedir. Yani burada göreceğimiz soket fonksiyonları aynı zamanda birer POSIX fonksiyonudur. Bir TCP/IP uygulamasında 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. Tabii TCP server programın üzerinde dururken zaten bazı ortak soket fonksiyonlarını da göreceğiz. >> "TCP Server" Program : Bir TCP server program tipik olarak aşağıdaki soket API'lerinin sırayla çağrılmasıyla gerçekleştirilmektedir: (Windows'ta WSAStartup --->) socket ---> bind ---> listen ---> accept ---> send/recv (ya da UNIC/Linux'ta read/write) ---> shutdown ---> close (Windows'ta closesocket) (---> Windows'ta WSACleanup) Buradan da gördüğünüz gibi her ne kadar Windows UNIX tarzı Bekeley soket kütüphanesini destekliyorsa da şu küçük farklılıklara sahiptir: -> Windows'ta soket sistemini başlatmak ve sonlandırmak için WSAStartup ve WSACleanup API fonksiyonları kullanılmaktadır. -> Windows'ta soket nesnesini yok etmek için close yerine closesocket API fonksiyonu bulunmaktadır. -> Windows'ta bazı istisnalar dışında bir soket fonksiyonu başarısız olduğunda başarısızlığın nedeni GetLastError fonksiyonuyla değil WSAGetLastError fonksiyonuyla elde edilmektedir. Bu nedenle Windows örneğimizde hataları rapor etmek için ExitSys fonksiyonunu aşağıdaki biçimde tanımlayacağız: void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } -> Windows'ta soket kütüphanesi API fonksiyonlarının bulunduğu kütüphaneler içerisinde değildir. Bu nedenle link amasında soket API'lerinin bulunduğu dinamik kütüphanenin import kütüphanesi ("Ws2_32.lib") linker ayarlarında belirtilmelidir. -> Windows sistemlerinde soket fonksiyonlarının protoipleri dosyası içerisindedir. Halbuki UNIX/Linux sistemlerinde fonksiyonların prototipleri farklı başlık dosyalarında bulunabilmektedir. Eğer dosyası da include edilecekse önce dosyası sonra dosyası include edilmelidir. Şimdi de bu fonksiyonları sırasıyla inceleyelim: >>> "WSAStartup" : Windows'ta soket sistemini başlatmak için WSAStartup fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int WSAStartup( WORD wVersionRequired, LPWSADATA lpWSAData ); Fonksiyonun birinci parametresi talep edilen soket kütüphanesinin versiyonunu belirtmektedir. Buradaki WORD değer iki ayrı byte'ın birleşiminden oluşmaktadır. MAKEWORD(high, low) makrosu ile bu parametre için argüman oluşturulabilir. Windows soket kütüphanesinin son versiyonu 2.2 versiyonudur. Dolayısıyla bu parametre için argümanı MAKEWORD(2, 2) biçiminde geçebiliriz. Eğer talep edilen versiyon yüksekse bu durum hataya yol açmamakta talep edilenden küçük olan en yüksek versiyon kullanıma hazır hale getirilmektedir. Fonksiyonun ikinci parametresi WSADATA isimli bir yapı nesnesinin adresini almaktadır. Fonksiyon bu yapıya bazı bilgiler yerleştirmektedir. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda hata kodunun kendisine geri dönmektedir. (Yani bu fonksiyon için WSAGetLastError çağrısına gerek yoktur.) Fonksiyon tipik olarak şöyle kullanılır: WSADATA wsaData; int result; ... if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", result); >>> "socket" : Haberleşme için öncelikle bir soket nesnesinin yaratılması gerekmektedir. Bu işlem socket isimli fonksiyonla yapılmaktadır. socket fonksiyonu bir soket nesnesi (handle alanı) yaratır ve bize handle değeri verir. Windows sistemlerinde socket fonksiyonunun geri döndürdüğü handle değeri SOCKET isimli bir türdendir. Ancak UNIX/Linux sistemlerinde bir dosya betimleyicisi biçimindedir. Yani UNIX/Linux sistemlerinde socket nesneleri tamamen bir dosya gibi kullanılmaktadır. Windows sistemlerinde socket fonksiyonunun prototipi şöyledir: #include SOCKET socket( int af, int type, int protocol ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int socket(int domain, int type, int protocol); Fonksiyonların parametreleri her iki sistemde de aynıdır. Yukarıda da gördüğünüz gibi tek fark Windows sistemlerinde soketin handle değerinin SOCKET türüyle UNIX/Linux sistemlerinde ise int türüyle temsil edilmesidir. socket fonksiyonunun 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. UNIX domain soketler için bu parametre AF_UNIX olarak girilmelidir. Fonksiyonun ikinci parametresi kullanılacak protokolün stream tabanlı mı yoksa datagram tabanlı mı olacağını belirtmektedir. Stream soketler için SOCK_STREAM, datagram soketler için SOCK_DGRAM kullanılmalıdır. Ancak başka soket türleri de vardır. TCP protokolünde bu parametre SOCK_STREAM biçiminde UDP protokülünde ise bu parametre SOCK_DGRAM biçiminde girilmelidir. Fonksiyonun üçüncü parametresi aktarım (transport) katmanındaki protokolü belirtmektedir. Ancak zaten ikinci parametreden aktarım protokolü anlaşılıyorsa üçüncü parametre 0 olarak geçilebilmektedir. Örneğin IP protokol ailesinde üçüncü parametreye gerek duyulmamaktadır. Çünkü ikinci parametredeki SOCK_STREAM zaten TCP'yi, SOCK_DGRAM ise zaten UDP'yi anlatmaktadır. Fakat yine de bu parametreye istenirse IP ailesi için IPPROTO_TCP ya da IPPROTO_UDP girilebilir. (Bu sembolik sabitler UNIX/Linux sistemlerinde içerisindedir.) socket fonksiyonu başarı durumunda Windows'ta soket handle değerine UNIX/Linux sistemlerinde ise soket betimeleyicisine geri dönemktedir. Fonksiyon başarısızlık durumunda Windows'ta SOCKT_ERROR değerine UNIX/Linux sistemlerinde ise -1 değerine geri dönmektedir. Windows sistemlerindeki SOCKET_ERROR sembolik sabiti de zaten -1 biçiminde define edilmiştir. Ancak Windows sistemlerinde SOCKET_ERROR sembolik sabitinin kullanılması gerekir. Örneğin, Windows sistemlerinde socket nesnesi şöyle yaratılabilir: SOCKET serverSock; ... if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR) ExitSys("socket", WSAGetLastError()); Aynı işlem UNIX/Linux sistemlerinde şöyle yapılabilir: int server_sock; ... if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); >>> "bind" : Server program soketi yarattıktan sonra onu bağlamalıdır (bind etmelidir). bind işlemi sırasında server'ın hangi portu dinleyeceği ve hangi network arayüzünden (kartından) gelen bağlantı isteklerini kabul edeceği belirlenir. Ancak bind fonksiyonu dinleme işlemini başlatmaz. Yalnızca soket nesnesine bu bilgileri yerleştirir. Fonksiyonun Windows sistemlerindeki prototipi şöyledir: #include int bind( SOCKET s, const sockaddr *addr, int namelen ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int bind(int socket, const struct sockaddr *addr, socklen_t addrlen); Görüldüğü gibi fonksiyonların iki sistemde de prototipi aynıdır. Ancak Windows sistemlerinde soket nesnesi SOCKET türüyle temsil edilmektedir. Biz kursumuzda anlatımı kolaylaştırmak için her iki sistemde de socket handle değerine soket betimelyicisi diyeceğiz. bind fonksiyonunun birinci parametresi yaratılmış olan soket betimleyicisini belirtir. İkinci parametre her ne kadar sockaddr isimli bir yapı türünden gösterici ise de de aslında her protokol için ayrı bir yapı nesnesinin adresini almaktadır. Yani sockaddr yapısı burada genelliği (void gösterici gibi) temsil etmek için kullanılmıştır. IPv4 için kullanılacak yapı sockaddr_in, IPv6 için sockaddr_in6 ve örneğin UNIX domain soketler için ise sockaddr_un biçiminde olmalıdır. Üçüncü parametre, ikinci parametredeki yapının uzunluğu olarak girilmelidir. sockaddr_in yapısı UNIX/Linux sistemlerinde dosyası içerisindedir. Windows sistemlerinde bu yapı şöyle bildirilmiştir: #include typedef struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; } SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN; UNIX/Linux sistemlerinde ise bu yapı şöyle bildirilmiştir: #include struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; }; Yapı elemanlarının türleri için iki sistemde farklı typedef isimleri kullanılmış olasa da elemanlar aynı anlamdadır. Yapının sin_family elemanına protokol ailesini belirten AF_XXX değeri girilmelidir. Bu eleman tipik olarak short biçimde bildirilmiştir. Yapının sin_port elemanı her iki sistemde de unsigned short türdendir. Server programın hangi portu dinleyeceği bu elemanla belirlenmektedir. Yapının sin_addr elemanı IP numarası belirten bir elemandır. Bu eleman in_addr isimli bir yapı türündendir. Bu yapı Windows sistemlerinde şöyle bildirilmiştir: struct in_addr { union { struct { u_char s_b1; u_char s_b2; u_char s_b3; u_char s_b4; } S_un_b; struct { u_short s_w1; u_short s_w2; } S_un_w; u_long S_addr; } S_un; }; #define s_addr S_un.S_addr UNIX/Linux sistemlerinde ise in_addr yapısı dosyası içerisinde şöyle bildirilmiştir: #include struct in_addr { in_addr_t s_addr; }; Aslında her iki sistemde de yapının s_addr elemanı IPV4 için 4 byte'lık işaretsiz tamsayı türü belirtmektedir. İşte bu 4 byte'lık işaretsiz tamsayı türü IPV4 için IP adresini belirtmektedir. Eğer buradaki IP adresi INADDR_ANY biçiminde geçilirse bu durum "herhangi bir network kartından gelen bağlantı isteklerinin kabul edileceği" anlamına gelmektedir. Yukarıda da belirttiğimiz gibi IP ailesinde tüm sayısal değerler "big endian" formatıyla belirtilmek zorundadır. Bu ailede "network byte ordering" denildiğinde "big endian" format anlaşılır. Oysa makinelerin belli bir bölümü (örneğin Intel ve default ARM) "little endian" kullanmaktadır. İşte elimizdeki makinenin endian'lığı ne olursa olsun onu big endian formata dönüştüren htons (host to network byte ordering short) ve htonl (host to network byte ordering long) isimli iki fonksiyon vardır. Bu işlemlerin tersini yapan da ntohs (network byte ordering to host short) ve ntohl (network byte ordering to host long) fonksiyonları da bulunmaktadır. Fonksiyonların Windows sistemlerindeki prototipleri şöyledir: #include u_short htons( u_short hostshort ); u_long htonl( u_long hostlong ); u_short ntohs( u_short netshort ); u_long ntohl( u_long netlong ); Fonksiyonların UNIX/Linux sistemlerindeki prototipleri şöyledir: #include uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); Bu durumda sockaddr_in yapısı her iki sistemde de tipik olarak şöyle doldurulabilir: struct sockaddr_in sinaddr; sinaddr.sin_family = AF_INET; sinaddr.sin_port = htons(SERVER_PORT); sinaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind fonksiyonu başarı durumunda sıfır değerine, başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR değerine, UNIX/Linux sistemlerinde ise -1 değerine geri dönmektedir. Örneğin Windows sistemlerinde fonksiyon şöyle çağrılabilir: if (bind(serverSock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); UNIX/Linux sistemlerinde de çağrı şöyle olabilir: if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); >>> "listen" : server program bind işleminden sonra soketi aktif dinleme konumuna sokmak için listen fonkiyonunu çağırmalıdır. Fonksiyonun Windows sistemlerindeki prototipi şöyledir: #include int listen( SOCKET s, int backlog ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int listen(int socket, int backlog); Fonksiyonun birinci parametresi soket betimleyicisini, ikinci parametresi kuyruk uzunluğunu belirtir. listen işlemi blokeye yol açmamaktadır. İşletim sistemi listen işleminden sonra ilgili porta gelen bağlantı isteklerini uygulama için oluşturduğu bir bağlantı kuyruğuna yerleştirmektedir. Kuyruk uzunluğunu yüksek tutmak meşgul server'larda bağlantı isteklerinin kaçırılmamasını sağlayabilir. Linux'ta default durumda verilebilecek en yüksek değer 128'dir. Ancak "/proc/sys/net/core/somaxconn" dosyasındaki değer değiştirilerek bu default uzunluk artırılabilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda Windows sistelerinde SOCKET_ERROR değerine, UNIX/Linux sistemlerinde ise -1 değerine geri dönmektedir. Örneğin Windows sistemlerinde fonksiyon şöyle çağrılabilir: if (listen(serverSock, 8) == SOCKET_ERROR) ExitSys("listen", WSAGetLastError()); UNIX/Linux sistemlerinde de çağrı şöyle yapılabilir: if (listen(server_sock, 8) == -1) exit_sys("listen"); Bu fonksiyon işletim sistemlerinin "firewall mekanizması" tarafından denetlenebilmektedir. Eğer çalıştığınız sistemde söz konusu port firewall tarafından kapatılmışsa bunu açmanız gerekir. (Windows sistemlerinde listen fonksiyonu bir pop pencere çıkartarak uyarı mesajı görüntülemektedir.) Yukarıda da belirttiğimiz gibi listen fonksiyonu herhangi bir blokeye yol açmaz. Bu fonksiyon çağrıldıktan sonra işletim sistemi ilgili porta gelecek bağlantı isteklerini prosese özgü bir bağlantı kuyruğuna yerleştirmektedir. Bu bağlantı kuyruğundan bağlantı isteklerini alarak bağlantısıyı sağlayan asıl fonksiyon accept fonksiyonudur. >>> "accept" : accept fonksiyonu bağlantı kuyruğuna bakar. Eğer kuyrukta 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 blokede bekler. Ancak accept fonksiyonu blokesiz modda da kullanılabilmektedir. Fonksiyonun Windows sistemlerindeki prototipi şöyledir: #include SOCKET accept( SOCKET s, struct sockaddr *addr, int *addrlen ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int accept(int socket, struct sockaddr *address, socklen_t *address_len); Fonksiyonların birinci parametreleri dinleme soketinin dosya betimleyicisini almaktadır. İkinci parametre bağlanılan client'a ilişkin bilgilerin yerleştirileceği sockaddr_in yapısının adresini almaktadır. Bu parametre yine genel bir sockaddr yapısı türünden gösterici ile temsil edilmiştir. Bizim bu parametre için IPv4'te sockaddr_in türünden, IPv6'da sockaddr_in6 türünden bir yapı nesnesinin adresini argüman olarak vermemiz gerekir. sockaddr_in yapısının üç elemanı olduğunu anımsayınız. Biz bu parametre sayesinde bağlanan client programın IP adresini ve client makinedeki port numarasını elde edebilmekteyiz. Client program server programa bağlanırken bir IP adresi ve port numarası belirtir. Ancak kendisinin de bir IP adresi ve port numarası vardır. Client'ın port numarası kendi makinesindeki (host'undaki) port numarasıdır. Client'ın IP adresine ve oradaki port numarasına "remote end point" de denilmektedir. Örneğin 178.231.152.127 IP adresinden bir client programın 52310 port'u ile server'ın bulunduğu 176.234.135.196 adresi ve 55555 numaralı portuna bağlandığını varsayalım. Burada remote endpoint "178.231.152.127:52310" biçiminde ifade edilmektedir. İşte biz accept fonksiyonunun ikinci parametresinden client hakkında bu bilgileri almaktayız. Client (178.231.152.127:52310) ---> Server (176.234.135.196:55555) accept fonksiyonunun üçüncü parametresi yine ikinci parametredeki yapının (yani sockaddr_in yapısının) byte uzunluğunu belirtmektedir. Ancak bu parametre bir adres olarak alınmaktadır. Yani programcı Windows'ta int türünden UNIX/Linux sistemlerinde socklen_t türünden bir nesne tanımlamalı, bu nesneye bu sizeof değerini yerleştirmeli ve nesnenin adresini de fonksiyonun üçüncü parametresine geçirmelidir. Fonksiyon bağlanılan client'a ilişkin soket bilgilerinin byte uzunluğunu yine bu adrese yerleştirmektedir. Tabii IP protokol ailesinde her iki taraf da aynı yapıyı kullanıyorsa fonksiyon çıkışında bu sizeof değerinde bir değişiklik olmayacaktır. Ancak tasarım genel yapıldığı için böyle bir yola gidilmiştir. accept fonksiyonu başarı durumunda bağlanılan client'a ilişkin yeni bir soket betimleyicisine geri dönmektedir. Artık bağlanılan client ile bu soket yoluyla konuşulacaktır. accept fonksiyonu başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR değerine UNIC/Linux sistemlerinde ise -1 değeri ile geri dönmektedir. Örneğin Windows sistemlerine accpet fonksiyonu şöyle kullanılabilir: struct sockaddr_in sin_client; int sin_len; SOCKET serverSock; ... sin_len = sizeof(sin_client); if ((clientSock = accept(serverSock, (struct sockaddr *)&sin_client, &sin_len)) == SOCKET_ERROR) ExitSys("accept", WSAGetLastError()); UNIX/Linux sistemlerinde de fonksiyon şöyle kullanılabilir: struct sockaddr_in sin_client; socklen_t sin_len; int client_sock; ... sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); Server tarafta temelde iki soket bulunmaktadır. Birincisi bind, listen, accept işlemini yapmakta kullanılan sokettir. Bu sokete TCP/IP terminolojisinde ""pasif soket (passive socket)" ya da "dinleme soketi (listening socket)" denilmektedir. İkinci soket ise client ile konuşmakta kullanılan accept fonksiyonunun geri döndürdüğü sokettir. Buna da "aktif soket (active socket)" denilmektedir. Tabii server program birden fazla client ile konuşacaksa accept fonksiyonunu bir kez değil, çok kez uygulamalıdır. Her accept o anda bağlanılan client ile konuşmakta kullanılabilecek yeni bir soket vermektedir. bind, listen işlemleri bir kez yapılmaktadır. Halbuki accept işlemi her client bağlantısı için ayrıca uygulanmalıdır. accept fonksiyonu default durumda blokeli modda çalışmaktadır. Eğer accept çağrıldığında o anda bağlantı kuyruğunda hiç bir client isteği yoksa accept fonksiyonu blokeye yol açmaktadır. Eğer accept çağrıldığında zaten bağlantı kuyruğunda bağlantı için bekleyen client varsa accept bloke olmadan bağlantıyı gerçekleştirir ve geri döner. accept fonksiyonu ile elde edilen client bilgilerindeki IP adresini ve port numaraları "big endian" formatında yani "network byte ordering" formatındadır. Bunları sayısal olarak görüntülemek için ntohl ve ntohs fonksiyonlarının kullanılması gerekir. Tabii izleyen paragrafta ele alacağımız gibi aslında IP adresleri genellikle "noktalı desimal format (dotted decimal format)" denilen bir format ile yazı biçiminde görüntülenmektedir. IPV4 adresini alarak noktalı desimal formata dönüştüren inet_ntoa isimli bir fonksiyon vardır. Fonksiyonun prototipi her iki sistemde de şöyledir: #include #include char *inet_ntoa(struct in_addr in); Fonksiyon parametre olarak sockaddr_in yapısının içerisindeki IP adresinin bulunduğu in_addr yapısını (adresini değil) parametre olarak alır ve noktalı desimal format yazısının adresiyle geri döner. Bu durumda accept işlemindne elde edilen client bilgileri aşağıdaki gibi ekrana yazdırabilir: printf("waiting for client...\n"); sin_len = sizeof(sin_client); if ((clientSock = accept(serverSock, (struct sockaddr *)&sin_client, &sin_len)) == SOCKET_ERROR) ExitSys("accept", WSAGetLastError()); printf("Connected %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); inet_nto fonksiyonunun tersini yapan inet_aton isimli bir fonksiyon da bulunmaktadır. Bu fonksiyon noktalı desimal formattaki ip adresini in_addr yapısınn içerisine yerleştirmektedir. Bu fonksiyon Windows sistemlerinde yoktur. Yalonızca UNIX/Linux sistemlerinde bulunmaktadır: #include int inet_aton(const char *cp, struct in_addr *inp); Fonksiyonun birinci parametresi noktalı desimal formattaki ip adresini almaktadır. İkinci paranetresi ise ip adresinin yerleştirileceği in_addr yapısının adresini almaktadır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda ise -1 değerine geri dönmektedir. inet_ntoa ve inet_aton fonksiyonlarının inet_ntop ve inet_pton isimli versiyonları da vardır. Aslında artık inet_ntoa ve inet_aton fonksiyonları "deprecated" yapılmıştır. Ancak inet_ntop ve inet_pton fonksiyonlarının kullanımı biraz zordur. Biz kursumuzda "deprecated" olmasına karşın kolaylığı nedeniyle inet_ntoa ve inet_aton fonksiyonlarını kullanacağız. >>> "WSACleaanup" : Windows'ta WSACleaanup fonksiyonun prototipi ise şöyledir: #include int WSACleanup(void); Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda SOCKET_ERROR özel değerine (bu değer genellikle -1 olarak define edilmektedir) geri dönmektedir. Hatanın nedeni için WSAGetLastError fonksiyonuna başvurulmalıdır. Aşağıda Windows ve UNIX/Linux sistemlerinde geldiğimiz noktaya kadar server programların kodları bir bütün olarak verilmiştir. * Örnek 1, /* Windows ""server.c" */ #include #include #include #include #define PORT_NO 55555 void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr); int main(void) { WSADATA wsaData; int result; SOCKET serverSock, clientSock; struct sockaddr_in sinServer, sinClient; int sin_len; if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", result); if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR) ExitSys("socket", WSAGetLastError()); sinServer.sin_family = AF_INET; sinServer.sin_port = htons(PORT_NO); sinServer.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(serverSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); if (listen(serverSock, 8) == SOCKET_ERROR) ExitSys("listen", WSAGetLastError()); printf("waiting for client...\n"); sin_len = sizeof(sinClient); if ((clientSock = accept(serverSock, (struct sockaddr *)&sinClient, &sin_len)) == SOCKET_ERROR) ExitSys("accept", WSAGetLastError()); printf("Connected %s:%d\n", inet_ntoa(sinClient.sin_addr), ntohs(sinClient.sin_port)); WSACleanup(); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* UNIX/Linux "server.c" */ #include #include #include #include #define PORT_NO 55555 void exit_sys(const char* msg); int main(void) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(PORT_NO); 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("waiting for client..\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("Client connected %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "TCP Client" Program : TCP client program, server programa bağlanabilmek için tipik bazı adımları uygulamak zorundadır. Bu adımlar sırasında çağrılacak fonksiyonlar şunlardır: (Windows'ta WSAStartup) ---> socket ---> bind (isteğe bağlı) ---> gethostbyname (isteğe bağlı) ---> connect ---> send/recv (ya da read/write) ---> shutdown ---> close (Windows'ta closesocket) (---> Windows'ta WSACleanup) Şimdi de bu fonksiyonları sırasıyla inceleyelim: >>> Client taraf önce yine socket fonksiyonuyla bir soket yaratır. Soketin bind edilmesi gerekmez. Zaten genellikle client taraf soketi bind etmez. Eğer client taraf belli bir port'tan bağlanmak istiyorsa bu durumda bind işlemini uygulayabilir. Eğer client bind işlemi yapmazsa zaten işletim sistemi connect işlemi sırasında sokete boş bir port numarasını atamaktadır. İşletim sisteminin bind edilmemiş client programa connect işlemi sırasında atadığı bu port numarasına İngilizce "ephemeral port (ömrü kısa olan port)" denilmektedir. Seyrek olarak bazı server programlar client için belli bir remote port numarası talep edebilmektedir. Bu durumda client'ın bu remote port'a sahip olabilmesi için bind işlemini uygulaması gerekir. Client bağlantı için server'ın IP adresini ve port numarasını bilmek zorundadır. IP adreslerinin akılda tutulması zordur. Bu nedenle IP adresleri ile eşleşen "host isimleri" oluşturulmuştur. Ancak IP protokol ailesi host isimleriyle değil, IP numaralarıyla çalışmaktadır. İşte host isimleriyle IP numaralarını eşleştiren ismine DNS (Domain Name Server) denilen özel server'lar bulunmaktadır. Bu server'lar IP protokol ailesindeki DNS isimli bir protokol ile çalışmaktadır. 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. >>>> "DNS Servers" : DNS server'lar dağıtık biçimde bulunmaktadır. Bir kayıt bir DNS server'da yoksa başka bir DNS server'a referans edilmektedir. DNS server'larda host isimleriyle IP numaraları bire bir karşılık gelmemektedir. Belli bir host ismine birden fazla IP numarası eşleştirilmiş olabileceği gibi belli bir IP numarasına da birden fazla host ismi eşleştirilmiş olabilmektedir. DNS işlemleri yapan iki geleneksel fonksiyon vardır: gethostbyname ve gethostbyaddr. Bu fonksiyonların kullanımları kolaydır. Ancak bu fonksiyonlar artık "deprecated" yapılmış ve POSIX standartlarından da silinmiştir. Bunların yerine getnameinfo ve getaddrinfo fonksiyonları oluşturulmuştur. Bu fonksiyonlar POSIX standartlarında bulunmaktadır. Biz kursumuzda artık "deprecated" hale getirilmiş "gethostbyname" ve "gethostbyaddress" kullanmayacağız. Bunun yerine "getaddrinfo" fonksiyonunu açıklayıp onu kullanacağız. >>> "getaddrinfo" : getaddrinfo isimli fonksiyon inet_addr ve gethosybyname fonksiyonlarının IPv6'yı da içerecek biçimde genişletilmiş bir biçimidir. Yani getaddrinfo hem noktalı desimal formatı nümerik adrese dönüştürür hem de eğer geçersiz bir noktalı desimal format söz konusuysa (bu durumda server isimsel olarak girilmiş olabilir) DNS işlemi yaparak ilgili host'un IP adresini elde eder. Maalesef fonksiyon biraz karışık tasarlanmıştır. Fonksiyonun Windows sistemlerindeki prototipi şöyledir: #include INT getaddrinfo( PCSTR pNodeName, PCSTR pServiceName, const ADDRINFOA *pHints, PADDRINFOA *ppResult ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); Aslında her iki sistemde de typedef isimleri farklı olmasına karşın fonksiyon aynı biçimde kullanılmaktadır. Fonksiyonun birinci parametresi "noktalı desimal formatlı IP adresi" ya da "host ismini" belirtmektedir. İkinci parametre NULL geçilebilir ya da buraya port numarası girilebilir. Ancak bu parametreye port numarası girilecekse yazısal biçimde girilmelidir. Fonksiyon bu port numarasını htons yaparak "big endian" formata dönüştürüp bize verecektir. Bu parametreye aynı zamanda IP ailesinin uygulama katmanına ilişkin spesifik bir protokolün ismi de girilebilmektedir (Örneğin "http" gibi, "ftp" gibi). Bu durumda bu protokollerin port numaraları bilindiği için sanki o port numaraları girilmiş gibi işlem yapılır. Eğer bu parametreye NULL girilirse bize port olarak 0 verilecektir. Port numarasını biz yerleştiriyorsak bu parametreye NULL girebiliriz. Fonksiyonun üçüncü parametresi nasıl bir adres istediğimizi anlatan filtreleme seçeneklerini belirtir. Bu parametre addrinfo isimli bir yapı türündendir. Bu yapının yalnızca ilk dört elemanı programcı tarafından girilebilmektedir. Ancak POSIX standartları bu yapının elemanlarının sıfırlanmasını öngörmektedir (buradaki sıfırlanmak terimi normal türdeki elemanlar için 0 değerini, göstericiler için NULL adres değerini belirtmektedir). addrinfo yapısı şöyledir: 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; }; 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ı AF_INET girilirse host'a ilişkin IPv4 adresleri, AF_INET6 girilirse host'a ilişkin IPv6 adresleri, AF_UNSPEC girilirse hem IPv4 hem de IPv6 adresleri elde edilir. Yapının ai_socktype elemanı 0 girilebilir ya da SOCK_STREAM veya SOCK_DGRAM girilebilir. Fonksiyonun ayrıntılı açıklaması için dokümanlara başvurunuz. Bu parametre NULL adres de girilebilir. Bu durumda ilgili host'a ilişkin tüm adresler elde edilir. getaddrinfo fonksiyonunun son parametresine bir bağlı listenin ilk elemanını gösteren adres yerleştirilmektedir. Buradaki bağlı listenin bağ elemanı addrinfo yapısının ai_next elemanıdır. Bu bağlı listenin boşaltımı freeaddrinfo fonksiyonu tarafından yapılmaktadır. getaddrinfo fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda doğrudan hata koduna geri döner. Bu hata kodları Windows şistemlerinde WSAGetLastError fonksiyonuyla elde ettiğimiz hata kodlarıdır. Ancak UNIX/Linux sistemlerinde bu hata kodları errno kodları değildir. Bu hata kodlarının UNIX/Linux sistemlerinde gai_strerror fonksiyonuyla yazıya dönüştürülmesi gerekir. Bağlı listenin düğümlerini free hale getirmek için freeaddrinfo fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include void freeaddrinfo(struct addrinfo *ai); Fonksiyon getaddrinfo fonksiyonunun verdiği bağlı listenin ilk düğümünün (head pointer) adresini parametre olarak alır ve tüm bağlı listeyi boşaltır. gai_strerror fonksiyonunun prototipi de şöyledir: #include const char *gai_strerror(int ecode); getaddrinfo fonksiyonunun client programda tipik kullanımı aşağıda verilmiştir. Bu fonksiyon bize connect için gereken sockaddr_in ya da sockadd_in6 yapı nesnelerini kendisi oluşturup sockaddr türünden bir adres gibi vermektedir. Örneğin biz "microsoft.com" host isminin bütün IPV4 AIP adreslerini aşağıdaki gibi elde edebiliriz: struct addrinfo *ainfoHead, *ainfo; struct sockaddr_in *sinHost; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; ... if ((result = getaddrinfo(HOST_NAME, "55555", &hints, &ainfoHead)) != 0) ExitSys("bind", result); for (ainfo = ainfoHead; ainfo != NULL; ainfo = ainfo->ai_next) { sinHost = (struct sockaddr_in *)ainfo->ai_addr; printf("%s\n", inet_ntoa(sinHost->sin_addr)); } freeaddrinfo(ainfoHead); Programcı host isminden elde ettiği tüm IP numaralarını bağlantı için deneyebilir. getaddrinfo fonksiyonunun tersini yapan getnameinfo isminde bir fonksiyon da sonraları soket kütüphanesine eklenmiştir. Fonksiyon temel olarak ilgili host'un IP adresini alıp bize aynı biçimde host isimlerini vermektedir. Biz burada bu fonksiyonu açıklamayacağız. Eğer elimizde zaten server'ın IP adresi noktalı desimal formatta varsa biz getaddrinfo fonksiyonunu kullanmak yerine inet_addr ile de bu noktalı desimal formatı IP adresine dönüştürebiliriz. Ancak genel olarak getaadrinfo fonksiyonu daha genel ve daha yeteneklidir. Bu fonksiyonun kullanılması tavsiye edilebilir. inet_addr fonksiyonunun Windows sistemlerindeki prototipi şöyledir: #include unsigned long inet_addr(const char *cp); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include in_addr_t inet_addr(const char *cp); Fonksiyonlar noktasıl desimal formattaki IP adresini 4 byte'lık IPV4 adresine dönüştürmektedir. Fonksiyon başarısızlık durumunda Windows sistemlerinde INADDR_NONE değerine, UNIX/Linux sistemlrinde ise -1 değerine geri dönmektedir. >> "connect" : Artık client program connect fonksiyonuyla TCP bağlantısını sağlayabilir. connect fonksiyonunun Windows sistemlerindeki prototipi şöyledir: #include int WSAAPI connect( SOCKET s, const sockaddr *name, int namelen ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int connect(int socket, const struct sockaddr *address, socklen_t address_len); Fonksiyonun birinci parametresi soket betimleyicisini belirtir. İkinci parametre bağlanılacak server'a ilişkin sockaddr_in yapı nesnesinin adresini belirtmektedir. Fonksiyonun üçüncü parametresi, ikinci parametredeki yapının uzunluğunu almaktadır. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Eğer connect fonksiyonu çağrıldığında server program çalışmıyorsa ya da server programın bağlantı kuyruğu doluysa connect belli bir zaman aşımı süresi kadar bekler ve sonra başarısız olur ve errno değeri ECONNREFUSED ("Connection refused") ile set edilir. Örneğin: if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); Aşağıda örnektlerde bağlantı için gereken minimum client program verilmiştir. Burada henüz görmediğimiz işlemleri hiç uygulamadık. Client programda bind işlemini yorum satırı içerisine aldık. Yukarıda da belirttiğimiz gibi eğer client program bind işlemi yapmazsa (ki genellikle yapmaz) bu durumda işletim sistemi client program için o programın çalıştığı makinede boş bir port numarası atamaktadır. Ayrıca biz bu örneklerde inet_addr fonksiyonunun nasıl kullanılacağını da yorum satırları içerisinde gösterdik. * Örnek 1, /* Windows "client.c" */ #include #include #include #include #include #define PORT_NO "55555" #define HOST_NAME "127.0.0.1" void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr); int main(void) { WSADATA wsaData; int result; SOCKET clientSock; struct addrinfo *ainfoHead, *ainfo; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", result); if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR) ExitSys("socket", WSAGetLastError()); /* { #define CLIENT_PORT_NO 50000 struct sockaddr_in sinClient; sinClient.sin_family = AF_INET; sinClient.sin_port = htons(CLIENT_PORT_NO); sinClient.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); } */ if ((result = getaddrinfo(HOST_NAME, PORT_NO, &hints, &ainfoHead)) != 0) ExitSys("getaddrinfo", result); for (ainfo = ainfoHead; ainfo != NULL; ainfo = ainfo->ai_next) if (connect(clientSock, ainfo->ai_addr, sizeof(struct sockaddr_in)) == 0) break; if (ainfo == NULL) ExitSys("connect", WSAGetLastError()); freeaddrinfo(ainfoHead); /* { struct sockaddr_in sinServer; sinServer.sin_family = AF_INET; sinServer.sin_port = htons(55555); if ((sinServer.sin_addr.s_addr = inet_addr(HOST_NAME)) == INADDR_NONE) ExitSys("inet_addr", WSAGetLastError()); if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR) ExitSys("connect", WSAGetLastError()); } */ printf("connected...\n"); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* Unix/Linux "client.c" */ #include #include #include #include #include #define PORT_NO "55555" #define HOST_NAME "127.0.0.1" void exit_sys(const char* msg); int main(void) { int client_sock; struct addrinfo *ainfo_head, *ainfo; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; int result; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); /* { #define CLIENT_PORT_NO 50000 struct sockaddr_in sin_client; sin_client.sin_family = AF_INET; sin_client.sin_port = htons(CLIENT_PORT_NO); 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 ((result = getaddrinfo(HOST_NAME, PORT_NO, &hints, &ainfo_head)) != 0) exit_sys("getaddrinfo"); for (ainfo = ainfo_head; ainfo != NULL; ainfo = ainfo->ai_next) if (connect(client_sock, ainfo->ai_addr, sizeof(struct sockaddr_in)) == 0) break; if (ainfo == NULL) exit_sys("connect"); freeaddrinfo(ainfo_head); /* { struct sockaddr_in sin_server; sin_server.sin_family = AF_INET; sin_server.sin_port = htons(55555); if ((sin_server.sin_addr.s_addr = inet_addr(HOST_NAME)) == -1) exit_sys("inet_addr"); if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); } */ printf("connected...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bağlantı sağlandıktan sonra artık gönderme ve alma işlemleri yapılabilir. Soketten karşı tarafa byte göndermek için send karşı taraftan byte okumak için recv fonksiyonları kullanılmaktadır. UNIX/Linux sistemlerinde soketler de dosya gibi betimleyici oldukları için bu sistemlerde send yerine write, recv yerine read POSIX fonksiyonları da kullanılabilir. TCP full duplex bir haberleşme sunmaktadır. Yani client ve server programlar aynı soket ile hem bilgi gönderip hem de bilgi alabilmektedir. recv fonksiyonunun Windows sistemlerindeki prototipi şöyledir: #include int recv( SOCKET s, char *buf, int len, int flags ); UNIX/linux sistemlerindeki prototipi ise şöyledir: #include ssize_t recv(int socket, void *buffer, size_t length, int flags); Fonksiyonların birinci parametresi aktif soketin betimleyicisini belirtmektedir. İkinci parametre alınacak bilginin yerleştirileceği dizinin adresini almaktadır. Üçüncü parametre ise okunmak istenen byte sayısını belirtmektedir. Fonksiyonun son parametresi aşağıdaki üç sembolik sabitin bit OR işlemine sokulmasıyla oluşturulabilir: MSG_PEEK MSG_OOB MSG_WAITALL Biz şimdilik bu değerlerin anlamlarını açıklamayacağız. Ancak MSG_PEEK değeri bilginin network tamponundan alındıktan sonra oradan atılmayacağını belirtmektedir. Bu parametre 0 da geçilebilir. Zaten recv fonksiyonunun read fonksiyonundan tek farkı bu son parametredir. Bu son parametrenin 0 geçilmesiyle read kullanılması arasında hiçbir farklılık yoktur. recv fonksiyonu blokeli modda (default durum blokeli moddur) tıpkı borularda olduğu gibi eğer hazırda en az 1 byte varsa okuyabildiği kadar bilgiyi okur ve okuyabildiği byte sayısına geri döner. Eğer o anda network tamponunda hiç byte yoksa recv fonksiyonu en az 1 byte okuyana kadar blokede bekler. (Yani başka bir deyişle recv tıpkı borularda olduğu gibi eğer okunacak bir şey yoksa blokede bekler, ancak okunacak en az 1 byte varsa okuyabildiğini okur ve beklemeden geri döner.) recv fonksiyonu başarı durumunda okunabilen byte sayısına, başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR, UNIX/Linux sistemlerinde -1 değerine geri dönmektedir. Eğer karşı taraf soketi (peer socket) kapatmışsa bu durumda tıpkı borularda olduğu gibi recv fonksiyonu 0 ile geri dönmektedir. Soketlerle boruların kullanımlarının birbirlerine çok benzediğine dikkat ediniz. Soketten bilgi göndermek için send ya da UNIX/Linux sistemleribde write fonksiyonu kullanılmaktadır. send fonksiyonunun Windows sistemlerindeki prototipi şöyledir: #include int send( SOCKET s, const char *buf, int len, int flags ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include ssize_t send(int socket, const void *buffer, size_t length, int flags); Fonksiyoların birinci parametresi aktif soketin betimleyicisini belirtmektedir. İkinci parametre gönderilecek bilgilerin bulunduğu dizinin adresini belirtir. Üçüncü parametre ise gönderilecek byte miktarını belirtmektedir. Son parametre aşağıdaki sembolik sabitlerin bit düzeyinde OR işlemine sokulmasıyla oluşturulabilir: MSG_EOR MSG_OOB MSG_NOSIGNAL Bu parametre 0 da geçilebilir. Biz şimdilik bu bayraklar üzerinde durmayacağız. send fonksiyonu bilgileri karşı tarafa o anda göndermez. Onu önce network tamponuna yerleştirir. İşletim sistemi o tampondan TCP (dolayısıyla IP) paketleri oluşturarak mesajı göndermektedir. Yani send fonksiyonu geri döndüğünde bilgiler network tamponuna yazılmıştır, ancak henüz karşı tarafa gönderilmemiş olabilir. Pekiyi o anda network tamponu doluysa ne olacaktır? İşte UNIX/Linux sistemlerinde send fonksiyonu, gönderilecek bilginin tamamı network tamponuna aktarılana kadar blokede beklemektedir. Ancak bu konuda işletim sistemleri arasında farklılıklar olabilmektedir. Örneğin Windows sistemlerinde send fonksiyonu eğer network tamponununda gönderilmek istenen kadar yer yoksa ancak en az bir byte'lık boş bir yer varsa tampona yazabildiği kadar byte'ı yazıp hemen geri dönmektedir. Diğer UNIX/Linux sistemleri arasında da send fonksiyonunun davranışı bakımından bu yönde farklılıklar olabilmektedir. Ancak POSIX standartları blokeli modda tüm bilginin network tamponuna yazılana kadar send fonksiyonunun bloke olacağını belirtmektedir. Linux çekirdeği de buna uygun biçimde çalışmaktadır. send fonksiyonu network tamponuna yazılan byte sayısı ile geri dönmektedir. Blokeli modda bu değer UNIX/Linux sistemlerinde yazılmak istenen değerle aynı olur. Ancak Windows sistemlerinde daha az olabilmektedir. send fonksiyonu Windows'ta başarısızlık durumunda SOCKET_ERROR değeri ile UNIX/Linux sistemlerinde ise -1 değeri ile geri döner.Tıpkı borularda olduğu gibi UNIX/Linux sistemlerinde send fonksiyonunda da eğer karşı taraf soketi kapatmışsa send fonksiyonu default durumda SIGPIPE sinyalinin oluşmasına yol açmaktadır. Eğer bu sinyalin oluşturulması istenmiyorsa bu durumda send fonksiyonunun son parametresi (flags) MSG_NOSIGNAL olarak geçilmelidir. Bu durumda karşı taraf soketi kapatmışsa send fonksiyonu başarısız olur ve errno değeri EPIPE olarak set edilir. send fonksiyonunun soketlerdeki davranışının borulardaki davranışa çok benzediğine dikkat ediniz. send fonksiyonunun son parametresi 0 geçildiğinde bu fonksiyonun davranışı tamamen write fonksiyonunda olduğu gibidir. Şimdi de örnekler üzerinden ilerleyelim: * Örnek 1.0, Aşağıdaki iskelet bir TCP/IP client-server uygulama verilmiştir. Burada client klavyeden birtakım yazılar girer. Bunu server'a gönderir. Server da bu yazının tersini client'a geri yollamaktadır. Client "exit" yazdığında her iki taraf da işlemini sonlandırmaktadır. /* Server.c */ #include #include #include #define SERVER_PORTNO 55000 #define BUFFER_SIZE 1024 void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr); int main(void) { WSADATA wsaData; DWORD dwResult; SOCKET serverSock, clientSock; struct sockaddr_in sinServer, sinClient; int addrLen; char buf[BUFFER_SIZE]; char *str; int result; if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", dwResult); if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) /* third parameter IPPROTO_TCP */ ExitSys("socket", WSAGetLastError()); sinServer.sin_family = AF_INET; sinServer.sin_port = htons(SERVER_PORTNO); sinServer.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(serverSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); if (listen(serverSock, 8) == SOCKET_ERROR) ExitSys("listen", WSAGetLastError()); printf("Waiting for client connection....\n"); addrLen = sizeof(sinClient); if ((clientSock = accept(serverSock, (struct sockaddr *)&sinClient, &addrLen)) == INVALID_SOCKET) ExitSys("accept", WSAGetLastError()); printf("Connected: %s:%d\n", inet_ntoa(sinClient.sin_addr), ntohs(sinClient.sin_port)); for (;;) { if ((result = recv(clientSock, buf, BUFFER_SIZE - 1, 0)) == SOCKET_ERROR) ExitSys("recv", WSAGetLastError()); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "exit")) break; printf("%d bytes received: %s\n", result, buf); _strrev(buf); if (send(clientSock, buf, strlen(buf), 0) == SOCKET_ERROR) ExitSys("send", WSAGetLastError()); } shutdown(clientSock, SD_BOTH); closesocket(clientSock); closesocket(serverSock); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* Client.c */ #include #include #include #include #define SERVER_PORTNO 55000 #define CLIENT_PORTNO 62000 #define BUFFER_SIZE 1024 #define SERVER_NAME "127.0.0.1" void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr); int main(void) { WSADATA wsaData; DWORD dwResult; SOCKET clientSock; struct sockaddr_in sinServer; struct hostent *hostEnt; char buf[BUFFER_SIZE]; char *str; int result; if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", dwResult); if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) ExitSys("socket", WSAGetLastError()); /* { struct sockaddr_in sinClient; sinClient.sin_family = AF_INET; sinClient.sin_port = htons(CLIENT_PORTNO); sinClient.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); } */ sinServer.sin_family = AF_INET; sinServer.sin_port = htons(SERVER_PORTNO); sinServer.sin_addr.s_addr = inet_addr(SERVER_NAME); if (sinServer.sin_addr.s_addr == INADDR_NONE) { if ((hostEnt = gethostbyname(SERVER_NAME)) == NULL) ExitSys("gethostbyname", WSAGetLastError()); memcpy(&sinServer.sin_addr.s_addr, hostEnt->h_addr_list[0], hostEnt->h_length); } if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR) ExitSys("connect", WSAGetLastError()); printf("Connected...\n"); for (;;) { printf("Text:"); fgets(buf, BUFFER_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(clientSock, buf, strlen(buf), 0) == SOCKET_ERROR) ExitSys("send", WSAGetLastError()); if (!strcmp(buf, "exit")) break; if ((result = recv(clientSock, buf, BUFFER_SIZE - 1, 0)) == SOCKET_ERROR) ExitSys("recv", WSAGetLastError()); if (result == 0) break; buf[result] = '\0'; printf("%d bytes received: %s\n", result, buf); } shutdown(clientSock, SD_BOTH); closesocket(clientSock); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /*================================================================================================================================*/ (97_09_08_2024) & (98_11_08_2024) & (99_16_08_2024) & (100_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 /*================================================================================================================================*/ (101_20_09_2024) & (102_20_09_2024) & (103_27_09_2024) & (104_29_09_2024) & (105_06_10_2024) & (106_11_10_2024) & (107_13_10_2024) (108_18_10_2024) & (109_20_10_2024) & (110_25_10_2024) & (111_27_10_2024) & (112_01_11_2024) & (113_03_11_2024) & (114_08_11_2024) (115_10_11_2024) & (116_17_11_2024) & (117_22_11_2024) & (118_24_11_2024) & (119_01_12_2024) & (120_06_12_2024) & (121_08_12_2024) (122_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. /*================================================================================================================================*/ (123_22_12_2024) & (124_27_12_2024) & (125_05_01_2025) & (126_10_01_2025) & (127_12_01_2025) & (128_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. /*================================================================================================================================*/ (129_26_01_2025) & (130_31_01_2025) & (131_03_02_2025) & (132_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 /*================================================================================================================================*/ (133_09_02_2025) & (134_16_02_2025) & (135_21_02_2025) & (136_23_02_2025) & (137_07_03_2025) & (138_09_03_2025) (139_16_03_2025) & (140_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); } /*================================================================================================================================*/ (141_23_03_2025) & (142_11_04_2025) & (143_18_04_2025) & (144_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; } /*================================================================================================================================*/